elanous 0.2.15 → 0.2.17
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-33ffcc49ab45123b.js → 1307-aac7332576c4d6fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-0e2c57fe6add4b2a.js → page-3143b07a817d1da9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-17ba086ef82d339b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-6e5c3a867557298b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-9a41d65355fc25b9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-1d4901eb7c2d8822.js → page-41afec0b255ed2bb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-4187666132fc98c4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-09328d499162bae4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-29ff3a8f84f03b6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-80ec7401bc83f511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-fc7d86467291ae38.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-de43a186e5ba5683.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-043a9111e74f6b24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-23a4601ceb7f452e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-10d958605a77b3bd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-a04260b8ccc968d4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-12916918988ac97e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-5a64cb5208c6a8fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-1407ec83ea64c1f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-7f49fe645d5132f7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-73735b64bfe1ae67.js → page-424659bacfb8cdd7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-fc2df261f8a2c7df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-5d4fef9ccd7d406b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +1 -0
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
- package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +15 -15
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +15 -15
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +15 -15
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +15 -15
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +15 -15
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +15 -15
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +15 -15
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +15 -15
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +15 -15
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +15 -15
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +15 -15
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +15 -15
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +15 -15
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +15 -15
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +15 -15
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +15 -15
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +15 -15
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +15 -15
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +15 -15
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +15 -15
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +15 -15
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +15 -15
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +15 -15
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +15 -15
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +16 -16
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +15 -15
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +15 -15
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +15 -15
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +15 -15
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +15 -15
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +15 -15
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +15 -15
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +15 -15
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +15 -15
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +15 -15
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +15 -15
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +15 -15
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +15 -15
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +15 -15
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +15 -15
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +15 -15
- package/graphs/doc-gen/doc-gen.yaml +24 -0
- package/graphs/doc-gen/recipes.yaml +3 -0
- package/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- package/graphs/orchestrator/orchestrator.yaml +7 -0
- package/graphs/orchestrator/recipes.yaml +3 -0
- package/graphs/steward/recipes.yaml +2 -1
- package/package.json +1 -1
- package/scripts/__fixtures__/intake-news.xml +7 -0
- package/scripts/ci-public-export-import-gate.ts +154 -0
- package/scripts/ci-test-interference-gate.ts +5 -1
- package/scripts/ci-typecheck-changed.ts +34 -15
- package/scripts/docs/doc-rot.ts +164 -0
- package/scripts/docs/generate-graph-anatomy.ts +131 -9
- package/scripts/docs-cli-check.ts +4 -4
- package/scripts/goals/goal-census.ts +343 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/measure/author-section-contribution.ts +84 -0
- package/scripts/release-loop/cut-branch.ts +43 -13
- package/scripts/release-loop/docs-land-node.ts +69 -10
- package/scripts/release-loop/gate-node.ts +3 -1
- package/scripts/release-loop/publish-node.ts +2 -2
- package/scripts/release-loop/release-docs.ts +134 -0
- package/scripts/release-loop/vault-note-node.ts +19 -2
- package/scripts/release-loop/version-node.ts +7 -4
- package/scripts/release-story/draft.ts +42 -14
- package/scripts/rhythm/monthly.ts +97 -0
- package/src/acp/client.ts +17 -2
- package/src/agent-mission/driver.ts +104 -11
- package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
- package/src/agent-mission/supervisor-metrics.ts +218 -0
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +5 -3
- package/src/briefing/brief-items.ts +191 -36
- package/src/budget/usage-outcomes.ts +204 -0
- package/src/cli/autopilot-cli.ts +66 -0
- package/src/cli/brief-cli.ts +73 -0
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/intake-cli.ts +1 -1
- package/src/cli/know-cli.ts +20 -0
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/logs-cli.ts +43 -1
- package/src/cli/pr-cli.ts +92 -13
- package/src/cli/provider-cli.ts +14 -0
- package/src/cli/release-cli.ts +11 -8
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +399 -1
- package/src/cli/usage-cli.ts +26 -0
- package/src/context-card/index.ts +45 -0
- package/src/context-card/task.ts +76 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
- package/src/decisions/decision-ledger.ts +10 -1
- package/src/discord-self-message.ts +9 -0
- package/src/flow-loop/split.ts +24 -0
- package/src/harness/harness-cli-command.ts +178 -12
- package/src/harness/harness-pod-dispatch.ts +36 -55
- package/src/harness/harness-queue.ts +86 -7
- package/src/harness/harness-seams.ts +13 -3
- package/src/harness/helper-repair.ts +139 -8
- package/src/harness/helper-scan.ts +13 -2
- package/src/harness/nested-elanous-policy.ts +133 -0
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +16 -2
- package/src/hq/lease.ts +5 -3
- package/src/hq/transfer.ts +185 -0
- package/src/index.ts +168 -13
- package/src/intake-plane/digest.ts +85 -28
- package/src/knowledge/know-find.ts +105 -0
- package/src/llm.ts +75 -19
- package/src/loops/orchestrator/finish-rate.ts +14 -3
- package/src/loops/orchestrator/tick.ts +96 -25
- package/src/maturity/bot-slash-catalog.ts +11 -2
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/maturity/tui-slash-availability.ts +9 -0
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/oauth/codex.ts +82 -2
- package/src/oauth/overload-failover-launch.ts +75 -0
- package/src/oauth/overload-failover.ts +137 -0
- package/src/release-loop/checklist.ts +10 -5
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +18 -10
- package/src/release-loop/placement.ts +17 -8
- package/src/seat-dispatch/seat-ask.ts +33 -6
- package/src/seat-loop/seat-loop.ts +172 -21
- package/src/self-dev/ask-launch-flow.ts +48 -8
- package/src/self-dev/authored-goal-trace.ts +64 -0
- package/src/self-dev/dev-cli.ts +19 -1
- package/src/self-dev/dev-pipeline.ts +6 -5
- package/src/self-dev/entrance-registry.ts +10 -6
- package/src/self-dev/old-door.ts +73 -0
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +7 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/app-api-budget.ts +166 -0
- package/src/self-implement/budget-gate.ts +7 -5
- package/src/self-implement/follow-up-goals.ts +289 -0
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/goal-author-clarification.ts +81 -0
- package/src/self-implement/goal-author-cli.ts +33 -5
- package/src/self-implement/goal-author.ts +107 -25
- package/src/self-implement/goal-clarification-escalation.ts +37 -1
- package/src/self-implement/goal-document-write.ts +28 -0
- package/src/self-implement/heal-intake.ts +4 -0
- package/src/self-implement/heal-triage.ts +44 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/orchestrator.ts +157 -8
- package/src/self-implement/run-ledger.ts +4 -0
- package/src/self-implement/seams.ts +44 -15
- package/src/session-runtime/index.ts +2 -0
- package/src/session-runtime/retry-api.ts +87 -0
- package/src/session-runtime/retry-policy.ts +107 -6
- package/src/steward/working-backwards.ts +16 -1
- package/src/task-agent/actions.ts +118 -0
- package/src/task-agent/judge.ts +96 -0
- package/src/task-cards/card-cli.ts +25 -0
- package/src/task-cards/card-store.ts +9 -4
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
- package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +95 -18
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +168 -37
- package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
- package/src/telegram-commands.ts +6 -1
- package/src/telegram-loops-command.ts +21 -14
- package/src/telegram.ts +47 -0
- package/src/user-config.ts +59 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-f5597e894c02cd33.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
- package/src/launch-head/seat-goal-queue.ts +0 -198
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
4
|
+
import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
|
|
5
5
|
|
|
6
6
|
export interface LandingFreeze {
|
|
7
7
|
reason: string;
|
|
@@ -60,15 +60,23 @@ function mergesDir(root: string): string { return join(root, 'landing-merges');
|
|
|
60
60
|
|
|
61
61
|
export interface LandingMergeGuard { frozen: LandingFreeze | null; end: () => void }
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
/** Mark both authorities before reading either freeze: the host and the child each drain their own in-flight merges. */
|
|
64
|
+
export function beginLandingMerge(root = effectiveInstanceRoot(), now = new Date(), prodFreezeRoot = prodInstanceRoot(), forceReason?: string): LandingMergeGuard {
|
|
65
|
+
const markers: string[] = [];
|
|
66
|
+
const end = () => { for (const marker of markers) rmSync(marker, { force: true }); };
|
|
67
|
+
try {
|
|
68
|
+
for (const authority of new Set([prodFreezeRoot, root])) {
|
|
69
|
+
const dir = mergesDir(authority);
|
|
70
|
+
mkdirSync(dir, { recursive: true });
|
|
71
|
+
const marker = join(dir, `${process.pid}-${randomUUID()}.json`);
|
|
72
|
+
markers.push(marker);
|
|
73
|
+
writeFileSync(marker, `${JSON.stringify({ pid: process.pid, startedAt: now.toISOString() })}\n`, { mode: 0o600 });
|
|
74
|
+
}
|
|
75
|
+
} catch (error) { end(); throw error; }
|
|
69
76
|
let frozen: LandingFreeze | null;
|
|
70
|
-
try { frozen = readLandingFreeze(
|
|
71
|
-
|
|
77
|
+
try { frozen = readLandingFreeze(prodFreezeRoot, now) ?? (root === prodFreezeRoot ? null : readLandingFreeze(root, now)); }
|
|
78
|
+
catch (error) { end(); throw error; }
|
|
79
|
+
if (frozen && !forceReason?.trim()) { end(); return { frozen, end: () => {} }; }
|
|
72
80
|
return { frozen: null, end };
|
|
73
81
|
}
|
|
74
82
|
|
|
@@ -10,7 +10,7 @@ import { move, releasedVersion } from './feature-store.js';
|
|
|
10
10
|
import { listSchedules, type ReleaseSchedule } from './release-schedule.js';
|
|
11
11
|
|
|
12
12
|
export type PlacementPriority = 'P0' | 'P1' | 'P2';
|
|
13
|
-
export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string }
|
|
13
|
+
export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }
|
|
14
14
|
export interface PlacementDecision { id: string; from: string | null; version: string; reason: string; displaced: Array<{ id: string; from: string; to: string; reason: string }> }
|
|
15
15
|
export interface RebalanceResult {
|
|
16
16
|
decisions: PlacementDecision[];
|
|
@@ -122,6 +122,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
122
122
|
validateCeoLoad(input);
|
|
123
123
|
const seat = parseOwner(input.owner).seat;
|
|
124
124
|
if (!['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
|
|
125
|
+
if (input.accelerator !== undefined && input.accelerator !== true && input.accelerator !== false) throw new CliUserError('가속 등급은 true 이거나 생략한다');
|
|
125
126
|
const now = (deps.now ?? new Date()).getTime();
|
|
126
127
|
const schedules = [...(deps.schedules ?? listSchedules())].sort((a, b) => versionOrder(a.version, b.version));
|
|
127
128
|
const released = deps.released ?? releasedVersion();
|
|
@@ -142,6 +143,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
142
143
|
if (from && released && versionOrder(from, released) <= 0) throw new CliUserError(`이미 발행된 판의 칸은 배치하지 않는다: ${input.id}`);
|
|
143
144
|
if (existing?.status === 'done') throw new CliUserError(`끝난 칸은 배치하지 않는다: ${input.id}`);
|
|
144
145
|
if (existing && (existing.title !== input.title || existing.owner !== input.owner)) throw new CliUserError(`기존 칸 제목·담당 불일치: ${input.id}`);
|
|
146
|
+
const accelerator = input.accelerator === true || (input.accelerator === undefined && existing?.accelerator === true);
|
|
145
147
|
const load = { id: input.id, ceoMinutes: input.ceoMinutes ?? existing?.ceoMinutes, ceoDate: input.ceoDate ?? existing?.ceoDate };
|
|
146
148
|
const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
|
|
147
149
|
const predecessors = input.predecessors.map((id) => {
|
|
@@ -170,6 +172,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
170
172
|
let chosen: ReleaseSchedule | undefined;
|
|
171
173
|
let loadIssue: string | null = null;
|
|
172
174
|
const feasible = candidates.filter((row) => !dependencyViolation(snapshots, locations, new Map([[input.id, row.version]]), input));
|
|
175
|
+
// 판은 «마감 안 가장 이른 판»부터 본다(기존 불변식). 가속 등급은 그 판이 찼을 때 같은 우선순위 비가속 칸을 밀 수 있게만 한다.
|
|
173
176
|
for (const row of feasible) {
|
|
174
177
|
const placement = new Map([[input.id, row.version]]);
|
|
175
178
|
const items = occupied(row.version);
|
|
@@ -180,11 +183,15 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
180
183
|
chosen = row;
|
|
181
184
|
break;
|
|
182
185
|
}
|
|
183
|
-
|
|
186
|
+
const mayDisplace = input.priority === 'P1' ? from !== row.version : accelerator && from !== row.version;
|
|
187
|
+
if (!mayDisplace) { loadIssue ??= directIssue; continue; }
|
|
184
188
|
const next = open.find((later) => versionOrder(later.version, row.version) > 0);
|
|
185
189
|
if (!next || items.length - 1 >= capacity(row)) { loadIssue ??= directIssue; continue; }
|
|
190
|
+
const victimOk = (priority: PlacementPriority | undefined) => (input.priority === 'P1' && priority === 'P2')
|
|
191
|
+
|| (accelerator && priority === input.priority);
|
|
186
192
|
const p2 = [...items].reverse().find((item) => {
|
|
187
|
-
if (item.
|
|
193
|
+
if (item.accelerator === true) return false;
|
|
194
|
+
if (!victimOk(item.priority) || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input)) return false;
|
|
188
195
|
if (item.deadlineVersion && versionOrder(next.version, item.deadlineVersion) > 0) return false;
|
|
189
196
|
if (items.length < capacity(row) && !(seatItems.length >= seatLimit && ownerMatches(item.owner, seat))) return false;
|
|
190
197
|
if (!item.owner) return false;
|
|
@@ -202,22 +209,24 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
202
209
|
return true;
|
|
203
210
|
});
|
|
204
211
|
if (p2) {
|
|
205
|
-
displaced.push({ id: p2.id, from: row.version, to: next.version, reason:
|
|
212
|
+
displaced.push({ id: p2.id, from: row.version, to: next.version, reason: accelerator
|
|
213
|
+
? `가속 등급 ${input.id} 를 같은 우선순위 안에서 먼저 두기 위해 ${p2.priority ?? 'P2'} ${p2.id} 이월`
|
|
214
|
+
: `P1 ${input.id} 마감 판 ${row.version} 용량 확보를 위해 P2 이월` });
|
|
206
215
|
chosen = row;
|
|
207
216
|
break;
|
|
208
217
|
}
|
|
209
218
|
loadIssue ??= directIssue;
|
|
210
219
|
}
|
|
211
220
|
if (!chosen) throw new CliUserError(loadIssue ?? `${input.id} 배치할 판이 없다 — 용량·선행·마감·동결을 확인하라`);
|
|
212
|
-
const reason = `${input.priority} ${input.priority === 'P0' ? '사고·회귀·발행 막음: 다음 판' : input.priority === 'P1' ? `지시·행사 마감 판 ${deadlineVersion}` : '착지 마감 내 가장 이른 여유 판'} · PR/24h ${rate} · 용량 ${capacity(chosen)} · 자리 ${seatLimit}`;
|
|
221
|
+
const reason = `${accelerator ? '가속 등급 · ' : ''}${input.priority} ${input.priority === 'P0' ? '사고·회귀·발행 막음: 다음 판' : input.priority === 'P1' ? `지시·행사 마감 판 ${deadlineVersion}` : '착지 마감 내 가장 이른 여유 판'}${accelerator ? ' · 같은 우선순위 안에서 가속 등급을 먼저' : ''} · PR/24h ${rate} · 용량 ${capacity(chosen)} · 자리 ${seatLimit}`;
|
|
213
222
|
const decision = { id: input.id, from, version: chosen.version, reason, displaced };
|
|
214
223
|
if (!deps.dryRun) {
|
|
215
224
|
const by = deps.by ?? 'OP';
|
|
216
225
|
for (const shifted of displaced) move(shifted.id, shifted.from, shifted.to, by, undefined, undefined, shifted.reason);
|
|
217
226
|
if (from && from !== chosen.version) move(input.id, from, chosen.version, by, undefined, undefined, reason);
|
|
218
|
-
if (from && (existing?.priority !== input.priority || JSON.stringify(existing.predecessors ?? []) !== JSON.stringify(input.predecessors) || (input.deadlineVersion !== undefined && existing.deadlineVersion !== input.deadlineVersion) || (input.ceoMinutes !== undefined && existing.ceoMinutes !== input.ceoMinutes) || (input.ceoDate !== undefined && existing.ceoDate !== input.ceoDate)))
|
|
219
|
-
setItem(chosen.version, input.id, { priority: input.priority, predecessors: input.predecessors, ...(input.deadlineVersion ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}) }, by);
|
|
220
|
-
if (!from) addItem(chosen.version, { id: input.id, title: input.title, owner: input.owner, priority: input.priority, predecessors: input.predecessors, deadlineVersion, ...(load.ceoMinutes !== undefined ? { ceoMinutes: load.ceoMinutes } : {}), ...(load.ceoDate !== undefined ? { ceoDate: load.ceoDate } : {}) });
|
|
227
|
+
if (from && (existing?.priority !== input.priority || JSON.stringify(existing.predecessors ?? []) !== JSON.stringify(input.predecessors) || (input.deadlineVersion !== undefined && existing.deadlineVersion !== input.deadlineVersion) || (input.ceoMinutes !== undefined && existing.ceoMinutes !== input.ceoMinutes) || (input.ceoDate !== undefined && existing.ceoDate !== input.ceoDate) || (input.accelerator !== undefined && (existing?.accelerator === true) !== (input.accelerator === true))))
|
|
228
|
+
setItem(chosen.version, input.id, { priority: input.priority, predecessors: input.predecessors, ...(input.deadlineVersion ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator !== undefined ? { accelerator: input.accelerator === true ? true : null } : {}) }, by);
|
|
229
|
+
if (!from) addItem(chosen.version, { id: input.id, title: input.title, owner: input.owner, priority: input.priority, predecessors: input.predecessors, deadlineVersion, ...(load.ceoMinutes !== undefined ? { ceoMinutes: load.ceoMinutes } : {}), ...(load.ceoDate !== undefined ? { ceoDate: load.ceoDate } : {}), ...(accelerator ? { accelerator: true } : {}) });
|
|
221
230
|
}
|
|
222
231
|
return decision;
|
|
223
232
|
}
|
|
@@ -20,7 +20,7 @@ function createTable(store: MsgStore): void {
|
|
|
20
20
|
store.db.exec(`CREATE TABLE IF NOT EXISTS seat_asks (
|
|
21
21
|
id TEXT PRIMARY KEY, seat TEXT NOT NULL, origin TEXT NOT NULL,
|
|
22
22
|
deadline INTEGER NOT NULL, timeout_minutes INTEGER NOT NULL DEFAULT 120, status TEXT NOT NULL DEFAULT 'pending',
|
|
23
|
-
answer TEXT, answered_at INTEGER
|
|
23
|
+
answer TEXT, answered_at INTEGER, asked_at INTEGER
|
|
24
24
|
); CREATE TABLE IF NOT EXISTS seat_ask_outbox (
|
|
25
25
|
id TEXT PRIMARY KEY, origin TEXT NOT NULL, text TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
|
|
26
26
|
claim_until INTEGER NOT NULL DEFAULT 0, claim_token TEXT
|
|
@@ -28,6 +28,7 @@ function createTable(store: MsgStore): void {
|
|
|
28
28
|
const askColumns = store.db.query('PRAGMA table_info(seat_asks)').all() as Array<{ name: string }>;
|
|
29
29
|
if (!askColumns.some(({ name }) => name === 'answer')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answer TEXT');
|
|
30
30
|
if (!askColumns.some(({ name }) => name === 'answered_at')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answered_at INTEGER');
|
|
31
|
+
if (!askColumns.some(({ name }) => name === 'asked_at')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN asked_at INTEGER');
|
|
31
32
|
const columns = store.db.query('PRAGMA table_info(seat_ask_outbox)').all() as Array<{ name: string }>;
|
|
32
33
|
if (!columns.some(({ name }) => name === 'claim_until')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_until INTEGER NOT NULL DEFAULT 0');
|
|
33
34
|
if (!columns.some(({ name }) => name === 'claim_token')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_token TEXT');
|
|
@@ -87,8 +88,9 @@ export async function askSeat(text: string, origin: AskOrigin, command: CeoComma
|
|
|
87
88
|
const store = (deps.open ?? openMsgStore)();
|
|
88
89
|
try {
|
|
89
90
|
createTable(store);
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
const askedAt = (deps.now ?? Date.now)();
|
|
92
|
+
store.db.query('INSERT INTO seat_asks (id, seat, origin, deadline, timeout_minutes, status, asked_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
93
|
+
.run(id, 'TC', JSON.stringify(origin), askedAt + timeout, minutes, 'pending', askedAt);
|
|
92
94
|
} finally { store.close(); }
|
|
93
95
|
try {
|
|
94
96
|
const result = await (deps.dispatch ?? dispatchCeoTask)('TC', `질문: ${body}\n답장 요청: ${id}\n회신: elanous seat answer ${id} "<답>"`, command,
|
|
@@ -119,12 +121,25 @@ export function answerSeatAsk(id: string, text: string, deps: Pick<SeatAskDeps,
|
|
|
119
121
|
} finally { store.close(); }
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
/** Return only the recorded reply's return address and measured ask time. */
|
|
125
|
+
export function seatAskReplyInfo(id: string, open: () => MsgStore = openMsgStore): { origin: AskOrigin; askedAt: number | null; answeredAt: number } {
|
|
126
|
+
const store = open();
|
|
127
|
+
try {
|
|
128
|
+
createTable(store);
|
|
129
|
+
const row = store.db.query('SELECT origin, asked_at, answered_at FROM seat_asks WHERE id = ? AND answer IS NOT NULL')
|
|
130
|
+
.get(id) as { origin: string; asked_at: number | null; answered_at: number } | null;
|
|
131
|
+
if (!row) throw new Error(`seat ask answer not recorded: ${id}`);
|
|
132
|
+
return { origin: JSON.parse(row.origin) as AskOrigin, askedAt: row.asked_at, answeredAt: row.answered_at };
|
|
133
|
+
} finally { store.close(); }
|
|
134
|
+
}
|
|
135
|
+
|
|
122
136
|
/** Deliver recorded seat answers or expiration notices via the original surface. */
|
|
123
|
-
export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
|
|
137
|
+
export async function deliverSeatAnswers(deps: SeatAskDeps & { askId?: string }): Promise<void> {
|
|
124
138
|
const store = (deps.open ?? openMsgStore)();
|
|
125
139
|
try {
|
|
126
140
|
createTable(store);
|
|
127
|
-
const pending = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE status = 'pending'
|
|
141
|
+
const pending = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE status = 'pending' AND (? IS NULL OR id = ?)")
|
|
142
|
+
.all(deps.askId ?? null, deps.askId ?? null) as AskRow[];
|
|
128
143
|
const decide = store.db.transaction((id: string, now: number): 'answered' | 'expired' | null => {
|
|
129
144
|
const ask = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE id = ? AND status = 'pending'").get(id) as AskRow | null;
|
|
130
145
|
if (!ask) return null;
|
|
@@ -150,7 +165,8 @@ export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
|
|
|
150
165
|
const status = decide(ask.id, (deps.now ?? Date.now)());
|
|
151
166
|
if (status) debug.log('seat.ask', status, { seat: ask.seat, id: ask.id, via: origin.channel });
|
|
152
167
|
}
|
|
153
|
-
const outbox = store.db.query("SELECT id, origin, text FROM seat_ask_outbox WHERE status = 'pending'
|
|
168
|
+
const outbox = store.db.query("SELECT id, origin, text FROM seat_ask_outbox WHERE status = 'pending' AND (? IS NULL OR id = ?)")
|
|
169
|
+
.all(deps.askId ?? null, deps.askId ?? null) as Array<{ id: string; origin: string; text: string }>;
|
|
154
170
|
for (const item of outbox) {
|
|
155
171
|
const origin = JSON.parse(item.origin) as AskOrigin;
|
|
156
172
|
if (deps.channel && origin.channel !== deps.channel) continue;
|
|
@@ -177,6 +193,17 @@ export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
|
|
|
177
193
|
} finally { store.close(); }
|
|
178
194
|
}
|
|
179
195
|
|
|
196
|
+
/** A pending outbox with a live claim is being delivered by another worker. */
|
|
197
|
+
export function seatAskDeliveryInProgress(id: string, open: () => MsgStore = openMsgStore): boolean {
|
|
198
|
+
const store = open();
|
|
199
|
+
try {
|
|
200
|
+
createTable(store);
|
|
201
|
+
const row = store.db.query("SELECT claim_until FROM seat_ask_outbox WHERE id = ? AND status = 'pending' AND claim_token IS NOT NULL")
|
|
202
|
+
.get(id) as { claim_until: number } | null;
|
|
203
|
+
return row !== null && row.claim_until > Date.now();
|
|
204
|
+
} finally { store.close(); }
|
|
205
|
+
}
|
|
206
|
+
|
|
180
207
|
/** Confirm only this browser's displayed answers; polling alone never removes them. */
|
|
181
208
|
export function acknowledgeSeatAnswers(clientId: string, ids: string[], open: () => MsgStore = openMsgStore): void {
|
|
182
209
|
const store = open();
|
|
@@ -10,7 +10,7 @@ import { surfaceEventsDbPath } from '../domains/surface-events.js';
|
|
|
10
10
|
import { judgeNeighbor } from '../loops/neighbors.js';
|
|
11
11
|
import { addHarnessQueue, harnessQueueIdForKey, harnessQueueOutcome, listHarnessQueue } from '../harness/harness-queue.js';
|
|
12
12
|
import { isRescueAllowedAction } from '../steward/rescue.js';
|
|
13
|
-
import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
13
|
+
import { DecisionLedger, type DecisionEntry, type DecisionOption, type Recommendation } from '../decisions/decision-ledger.js';
|
|
14
14
|
import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
|
|
15
15
|
import { loopEvent } from '../loops/observe.js';
|
|
16
16
|
import { getUserConfig, type PersonaLoopConfig, type SeatLoopConfig, type UserConfig } from '../user-config.js';
|
|
@@ -43,7 +43,7 @@ export type TcCandidate =
|
|
|
43
43
|
| { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
|
|
44
44
|
| { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
|
|
45
45
|
export type TcPullRequest = { number: number; title: string; body?: string; state: 'OPEN' | 'MERGED' | 'CLOSED'; isDraft: boolean; createdAt: string; updatedAt?: string; mergedAt?: string | null; readyAt?: string; approvalWaitAt?: string; paths?: string[]; reviewDecision?: string };
|
|
46
|
-
export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
|
|
46
|
+
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; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
|
|
47
47
|
export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off'; modeDowngradeReason?: string };
|
|
48
48
|
|
|
49
49
|
export function seatLoopTickLine(result: SeatLoopResult): string {
|
|
@@ -53,6 +53,8 @@ export function seatLoopTickLine(result: SeatLoopResult): string {
|
|
|
53
53
|
}
|
|
54
54
|
export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null; action?: 'decision' | 'harness' | 'wait'; what?: string };
|
|
55
55
|
export type SeatDeps = {
|
|
56
|
+
/** Version stamp for decision cards the seat loop raises. Default = the ledger's resolver (`git reflog` on the repo, up to 10 s) — tests inject a fixed one. */
|
|
57
|
+
resolveDecisionVersion?: (at: string) => { released: string | null; dev: string | null; codename: string | null };
|
|
56
58
|
personaConfig?: PersonaLoopConfig;
|
|
57
59
|
personaRegistry?: PersonaRegistry;
|
|
58
60
|
personaDir?: string;
|
|
@@ -256,6 +258,59 @@ function queueKey(seat: string, item: SeatItem): string {
|
|
|
256
258
|
return `seat-loop:${seat}:${createHash('sha256').update(handledItemKey(item)).digest('hex')}`;
|
|
257
259
|
}
|
|
258
260
|
|
|
261
|
+
/** A decision ledger that raises cards — forwards the injected version resolver so tests never run the repo's `git reflog`. */
|
|
262
|
+
function cardLedger(root: string, deps: SeatDeps, now?: Date): DecisionLedger {
|
|
263
|
+
return new DecisionLedger({ stateDir: root, ...(now ? { now: () => now } : {}), ...(deps.resolveDecisionVersion ? { resolveVersion: deps.resolveDecisionVersion } : {}) });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const DEFAULT_REPEAT_STOP = 3;
|
|
267
|
+
|
|
268
|
+
function repeatStopLimit(config: SeatLoopConfig): number {
|
|
269
|
+
const value = config.repeatStop;
|
|
270
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : DEFAULT_REPEAT_STOP;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** A finished attempt that never landed (no merged-PR evidence). `attempting` is the intent row of the same launch. A `refused` row whose reason is the AUTOQ retry note is the reconcile of a queued row, not a second try. */
|
|
274
|
+
function unlandedAttempt(entry: SeatEntry): boolean {
|
|
275
|
+
if (!entry.item || entry.action === 'seat-question' || entry.action === 'seat-answer' || entry.inquiry) return false;
|
|
276
|
+
if (entry.status === 'launched' || entry.status === 'hitl' || entry.status === 'held' || entry.status === 'attempting') return false;
|
|
277
|
+
if (entry.status === 'refused' && entry.reason === 'AUTOQ failed or cancelled; eligible for retry') return false;
|
|
278
|
+
return entry.status === 'queued' || entry.status === 'refused' || entry.status === 'outcome-unknown';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function repeatStopReason(attempts: number): string {
|
|
282
|
+
return `같은 칸 ${attempts}회 착지 0 — 사람 판단`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function personReopened(ledger: readonly SeatEntry[], seat: string, item: SeatItem, root: string): boolean {
|
|
286
|
+
const key = handledItemKey(item);
|
|
287
|
+
const lastHold = [...ledger].reverse().find((entry) => entry.seat === seat && entry.item && handledItemKey(entry.item) === key && entry.status === 'held');
|
|
288
|
+
if (!lastHold) return false;
|
|
289
|
+
const heldAt = Date.parse(lastHold.at || lastHold.ts || '');
|
|
290
|
+
if (!Number.isFinite(heldAt)) return false;
|
|
291
|
+
const ref = decisionRef(seat, item);
|
|
292
|
+
return new DecisionLedger({ stateDir: root }).list({ status: 'all' }).some((card) => {
|
|
293
|
+
if (!card.refs?.includes(ref) || card.status !== 'decided' || !card.decidedAt) return false;
|
|
294
|
+
const decidedAt = Date.parse(card.decidedAt);
|
|
295
|
+
return Number.isFinite(decidedAt) && decidedAt > heldAt;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Prior attempts of this seat and handled item that finished without a landing. A queued row whose queue is still retryable has not finished. Queue rows with the same key count when the ledger has no receipt. */
|
|
300
|
+
function unlandedAttemptCount(seat: string, item: SeatItem, ledger: readonly SeatEntry[], root: string, deps: SeatDeps): number {
|
|
301
|
+
const key = handledItemKey(item);
|
|
302
|
+
const idem = queueKey(seat, item);
|
|
303
|
+
const outcome = deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot }));
|
|
304
|
+
const finished = (entry: SeatEntry) => unlandedAttempt(entry) && !(entry.status === 'queued' && entry.queueId && outcome(entry.queueId, root) === 'retryable');
|
|
305
|
+
const fromLedger = ledger.filter((entry) => entry.seat === seat && entry.item && handledItemKey(entry.item) === key && finished(entry));
|
|
306
|
+
const seen = new Set(fromLedger.flatMap((entry) => entry.queueId ? [entry.queueId] : []));
|
|
307
|
+
let rows: ReturnType<typeof listHarnessQueue> = [];
|
|
308
|
+
try { rows = (deps.queueItems ?? ((stateRoot: string) => listHarnessQueue({ root: stateRoot })))(root); }
|
|
309
|
+
catch (error) { observe('repeat-stop-queue-unreadable', { seat, item: item.id, error: String(error).slice(0, 200) }); }
|
|
310
|
+
const fromQueue = rows.filter((row) => row.seat === seat && row.idempotencyKey === idem && row.status === 'finished' && !seen.has(row.id));
|
|
311
|
+
return fromLedger.length + fromQueue.length;
|
|
312
|
+
}
|
|
313
|
+
|
|
259
314
|
/** A crash (or a failed write) between AUTOQ enqueue and the `queued` row leaves an `attempting`/`outcome-unknown` row with no queue id. Find its queue row by the
|
|
260
315
|
* seat-loop idempotency key so the outcome below (retry · launched · still queued) applies to it too (LOOP-LIVE1 review must-fix). */
|
|
261
316
|
function recoverAttemptingQueueIds(ledger: readonly SeatEntry[], root: string, deps: SeatDeps): SeatEntry[] {
|
|
@@ -320,6 +375,23 @@ function decisionSummary(text: string): string {
|
|
|
320
375
|
return Array.from(sentences).slice(0, 240).join('').trim();
|
|
321
376
|
}
|
|
322
377
|
|
|
378
|
+
function decisionCardMaterial(input: {
|
|
379
|
+
what: string; soFar: string; current: string; question: string; answer: string;
|
|
380
|
+
options: DecisionOption[]; recommendation: Recommendation; ifUntouched: string; evidence: string; confidence: string;
|
|
381
|
+
}): { scqa: { s: string; c: string; q: string; a: string }; pendingQuestion: string } {
|
|
382
|
+
const short = (value: string) => decisionSummary(value) || '관측되지 않음';
|
|
383
|
+
return {
|
|
384
|
+
scqa: { s: short(input.what), c: short(input.current), q: short(input.question), a: short(input.answer) },
|
|
385
|
+
pendingQuestion: [
|
|
386
|
+
`무엇: ${input.what}`, `지금까지: ${input.soFar}`, `지금 상태 재측: ${input.current}`,
|
|
387
|
+
...input.options.map((option) => `선택지 ${option.key} (${option.label}) 결과: ${option.consequence}`),
|
|
388
|
+
'skipped' in input.recommendation ? `권고 비움: ${input.recommendation.reason}` : `권고: ${input.recommendation.option} — ${input.recommendation.why}`,
|
|
389
|
+
`확신: ${input.confidence}`, `그냥 두면: ${input.ifUntouched}`, `근거: ${input.evidence}`,
|
|
390
|
+
`Q: ${input.question}`, `A: ${input.answer}`,
|
|
391
|
+
].join('\n'),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
323
395
|
function decisionSourceRef(seat: string, item: SeatItem): string {
|
|
324
396
|
return item.source === 'request' && item.id.startsWith('coord:') ? item.id : decisionRef(seat, item);
|
|
325
397
|
}
|
|
@@ -333,12 +405,12 @@ function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEn
|
|
|
333
405
|
function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: string, liveSafe = false): Set<string> {
|
|
334
406
|
const inquiryRow = (entry: SeatEntry) => entry.action === 'seat-question' || entry.action === 'seat-answer' || !!entry.inquiry;
|
|
335
407
|
return new Set(ledger.filter((entry, index) => entry.status === 'asked' || entry.status === 'answered'
|
|
336
|
-
|| entry.status === 'resolved-by-neighbor'
|
|
408
|
+
|| entry.status === 'resolved-by-neighbor' || entry.status === 'resolved'
|
|
337
409
|
// RM3a: only TC's other-seat defect ask-shadows are deduplicated here — an ordinary inquiry recorded in shadow must still be delivered once questions turn on.
|
|
338
410
|
|| (entry.status === 'shadow' && entry.candidate?.kind === 'other-seat-cell-defect')
|
|
339
411
|
|| (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
|
|
340
412
|
|| (entry.inquiry?.to === 'CEO' && !!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))
|
|
341
|
-
|| (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown'))
|
|
413
|
+
|| (!inquiryRow(entry) && (entry.status === 'queued' || entry.status === 'outcome-unknown' || (entry.status === 'held' && !personReopened(ledger, entry.seat, entry.item!, root ?? ''))))
|
|
342
414
|
|| (!inquiryRow(entry) && entry.status === 'attempting' && !(entry.item?.source === 'checklist' && ledger.slice(index + 1).some((later) =>
|
|
343
415
|
later.seat === entry.seat && later.status === 'launched' && later.item?.source === 'checklist'
|
|
344
416
|
&& later.item.version === entry.item?.version && later.item.id === entry.item?.id
|
|
@@ -798,7 +870,7 @@ const SEAT_ACTION_RULES: ReadonlyArray<{ kind: 'wait' | 'harness' | 'decision';
|
|
|
798
870
|
{ kind: 'wait', signal: /실측\s*대기|(?:사람|실제)\s*기기(?!\s*(?:(?:실측|측정|확인)\s*)?완료)(?:\s*(?:실측|측정|확인|대기))?|실물\s*측정(?!\s*완료)/iu, source: 'content', resolvedBy: /(?:실측|측정|(?:사람|실제)\s*기기|실물\s*측정)\s*완료/iu },
|
|
799
871
|
{ kind: 'wait', signal: /\u{1F451}\s*확인\s*대기/iu, source: 'content', resolvedBy: /\u{1F451}\s*확인\s*완료/iu },
|
|
800
872
|
{ kind: 'decision', signal: TRACK_AGENT_FORBIDDEN_ACTION_REGEX, source: 'task' },
|
|
801
|
-
{ kind: 'decision', signal: /(?:마켓|레지스트리)(?:에|로)?\s*(?:게시|등록|발행)|사이트\s*운영\s*반영|SNS(?:에|로)?\s*(?:게시|발행|업로드)|공개\s*발행/iu, source: '
|
|
873
|
+
{ kind: 'decision', signal: /(?:마켓|레지스트리)(?:에|로)?\s*(?:게시|등록|발행)|사이트\s*운영\s*반영|SNS(?:에|로)?\s*(?:게시|발행|업로드)|공개\s*발행/iu, source: 'task' },
|
|
802
874
|
{ kind: 'harness', signal: /(?:마켓|레지스트리|사이트|SNS|공개)?(?:에|로)?\s*(?:게시|발행|배포|등록|운영\s*반영)(?:를?\s*위한)?\s*(?:준비|초안|원고|자료|구현|점검|테스트)|(?:마켓|레지스트리|사이트|SNS|공개)?(?:에|로)?\s*(?:게시|발행|배포|등록)(?:용|를?\s*위한)\s*(?:초안|원고|자료|코드|기능)/iu, source: 'task' },
|
|
803
875
|
];
|
|
804
876
|
|
|
@@ -806,9 +878,9 @@ export function planAction(item: SeatItem, seat?: string): { kind: 'decision' |
|
|
|
806
878
|
const task = `${item.id}\n${item.title}\n${item.text}`;
|
|
807
879
|
const content = `${task}\n${item.evidence ?? ''}`;
|
|
808
880
|
const preparation = SEAT_ACTION_RULES.find(({ kind }) => kind === 'harness')!;
|
|
809
|
-
const prepared = [...
|
|
881
|
+
const prepared = [...item.text.matchAll(new RegExp(preparation.signal.source, `${preparation.signal.flags.replace('g', '')}g`))];
|
|
810
882
|
const rule = SEAT_ACTION_RULES.map(({ kind, signal, source, resolvedBy }) => {
|
|
811
|
-
const text = source === 'content' ? content : task;
|
|
883
|
+
const text = kind === 'decision' ? item.text : source === 'content' ? content : task;
|
|
812
884
|
if (resolvedBy?.test(item.evidence ?? '')) return null;
|
|
813
885
|
const hits = [...text.matchAll(new RegExp(signal.source, `${signal.flags.replace('g', '')}g`))];
|
|
814
886
|
const hit = kind === 'decision' ? hits.find((match) => !prepared.some((prep) =>
|
|
@@ -881,13 +953,16 @@ function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps
|
|
|
881
953
|
if (judgment.state === 'absent') {
|
|
882
954
|
if (config.mode === 'live-safe' && judgment.action?.action === 'escalate' && isRescueAllowedAction('decision-card')) {
|
|
883
955
|
const ref = `seat-loop:neighbor:${seat}:${neighbor.id}:${lastSeenAt}`;
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
956
|
+
const options = [{ key: 'a', label: '사람 판단 후 별도 조치', consequence: '자리 루프는 대행하지 않는다' },
|
|
957
|
+
{ key: 'b', label: '현상 유지', consequence: '결측 상태를 계속 관측한다' }];
|
|
958
|
+
const recommendation = { option: 'b', why: '새 사건을 확인하기 전에는 이웃 업무를 대행하지 않는다' };
|
|
959
|
+
cardLedger(root, deps, now).raiseOnce({ title: `${seat}: 이웃 ${neighbor.id} 결측`, category: 'other',
|
|
960
|
+
...decisionCardMaterial({ what: `${seat} 이웃 ${neighbor.id} 결측`, soFar: '마지막 맥락 버스 사건 이후 새 ACK 없음',
|
|
961
|
+
current: `현재 시각 ${now.toISOString()}; 마지막 사건 ${lastSeenAt}; heartbeat ${neighbor.heartbeat.everyMinutes}분 × ${neighbor.heartbeat.missedTicks}회 결측`,
|
|
962
|
+
question: '결측 이웃의 업무를 어떻게 처리할까?', answer: '새 ACK까지 현상 유지하고 이웃 업무는 대행하지 않는다',
|
|
963
|
+
options, recommendation, confidence: '중간 — 사건의 부재만 관측됨',
|
|
964
|
+
ifUntouched: '결측 상태를 계속 관측하며 대행은 하지 않는다', evidence: `맥락 버스 마지막 사건 ${lastSeenAt}; ${ref}` }),
|
|
965
|
+
options, recommendation,
|
|
891
966
|
raisedBy: { agent: 'seat-loop' }, refs: [ref], raisedAt: now.toISOString(),
|
|
892
967
|
crossCheckSkipped: '이웃 결측으로 교차 확인 불가' }, ref);
|
|
893
968
|
action = 'decision-card';
|
|
@@ -957,10 +1032,17 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
957
1032
|
const question = proposal.inquiry?.question ?? proposal.item!.title;
|
|
958
1033
|
const note = `${proposal.xcheckNote ?? ''} — 해결 가능하다고 했으나 ${waited}분 안 해소 안 됨`.trim();
|
|
959
1034
|
if (!decisionReceipt(stateRoot, seat, proposal.item!)) {
|
|
1035
|
+
const options = [{ key: 'a', label: '승인', consequence: '사람 판단으로 진행' }, { key: 'b', label: '보류', consequence: '진행하지 않음' }];
|
|
1036
|
+
const recommendation = { option: 'b', why: `기한 ${waited}분 뒤에도 ${cell?.status ?? '질문 미해결'}이므로 착지 확인 전에는 진행하지 않는다` };
|
|
1037
|
+
const material = decisionCardMaterial({ what: `${seat} 질문 · ${proposal.item!.title}`, soFar: `이웃 ${neighbor} 해결 제안: ${proposal.xcheckNote ?? '기록 없음'}`,
|
|
1038
|
+
current: `기한 ${waited}분 경과 · 칸 상태 ${cell?.status ?? '질문 미해결'}`, question, answer: '재측상 미해결이므로 보류를 권고한다',
|
|
1039
|
+
options, recommendation, confidence: '중간 — 기한 경과와 미해결을 재측함', ifUntouched: '질문/칸은 미해결로 남는다',
|
|
1040
|
+
evidence: `${proposal.item!.evidence ?? '원천 근거 미기재'} · ${note} · ${decisionRef(seat, proposal.item!)}` });
|
|
960
1041
|
await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${question}`, '--category', 'other',
|
|
961
|
-
'--s',
|
|
1042
|
+
'--s', material.scqa.s, '--c', material.scqa.c, '--q', material.scqa.q, '--a', material.scqa.a,
|
|
1043
|
+
'--pending-question', material.pendingQuestion,
|
|
962
1044
|
'--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
|
|
963
|
-
'--
|
|
1045
|
+
'--recommend', recommendation.option, '--why', recommendation.why, '--xcheck', `${neighbor}:${note}`,
|
|
964
1046
|
'--agent', 'seat-loop', '--ref', decisionRef(seat, proposal.item!), '--json']);
|
|
965
1047
|
}
|
|
966
1048
|
const entry: SeatEntry = { ...proposal, ts: now.toISOString(), at: now.toISOString(), status: 'hitl' };
|
|
@@ -1258,10 +1340,18 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1258
1340
|
const check = answer ? ['--xcheck', `${requestedNeighbor}:${answer.note}`,
|
|
1259
1341
|
...(answer.agree ? [] : ['--dissent', `${requestedNeighbor}: ${answer.note}`])]
|
|
1260
1342
|
: ['--no-xcheck', `이웃 ${requestedNeighbor} 무응답 ${entry.xcheckWaitMinutes}분`];
|
|
1343
|
+
const options = [{ key: 'a', label: '승인', consequence: '사람 판단으로 진행' }, { key: 'b', label: '보류', consequence: '진행하지 않음' }];
|
|
1344
|
+
const recommendation = { option: 'b', why: '교차 확인에서 해소 근거가 확보되지 않았다' };
|
|
1345
|
+
const material = decisionCardMaterial({ what: `${seat} 질문 · ${item.title}`, soFar: `이웃 ${requestedNeighbor}에 교차 확인 요청`,
|
|
1346
|
+
current: answer ? `이웃 응답: ${answer.note}` : `이웃 ${requestedNeighbor} 무응답 ${entry.xcheckWaitMinutes}분`,
|
|
1347
|
+
question: ask.question, answer: '해소 근거 확인 전까지 보류를 권고한다', options, recommendation,
|
|
1348
|
+
confidence: answer ? '중간 — 이웃 응답을 확인함' : '낮음 — 기한 내 이웃 응답 없음',
|
|
1349
|
+
ifUntouched: '사람 판단 전 집행하지 않는다', evidence: `${item.evidence ?? '원천 근거 미기재'} · ${decisionRef(seat, item)}` });
|
|
1261
1350
|
await (deps.run ?? defaultRun)(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
|
|
1262
|
-
'--s',
|
|
1351
|
+
'--s', material.scqa.s, '--c', material.scqa.c, '--q', material.scqa.q, '--a', material.scqa.a,
|
|
1352
|
+
'--pending-question', material.pendingQuestion,
|
|
1263
1353
|
'--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
|
|
1264
|
-
'--
|
|
1354
|
+
'--recommend', recommendation.option, '--why', recommendation.why, ...check, '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
|
|
1265
1355
|
if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
|
|
1266
1356
|
entry.status = 'hitl';
|
|
1267
1357
|
xcheckEvent('raised', seat, requestedNeighbor, itemKey(item), answer ? answer.agree ? 'agree' : 'dissent' : 'timed-out');
|
|
@@ -1387,11 +1477,27 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1387
1477
|
if (action.kind === 'decision') {
|
|
1388
1478
|
const title = `${seat}: ${decisionSummary(action.text)}`;
|
|
1389
1479
|
const source = decisionSourceRef(seat, item);
|
|
1480
|
+
const options = [{ key: 'a', label: '사람 승인', consequence: '승인 후 별도로 집행' }, { key: 'b', label: '보류', consequence: '집행하지 않음' }];
|
|
1481
|
+
const recommendation = { option: 'b', why: `요청에서 걸린 문면 «${action.reason}»은 자동 집행 대상이 아니므로 보류한다` };
|
|
1482
|
+
let currentStatus: string | undefined;
|
|
1483
|
+
try {
|
|
1484
|
+
currentStatus = item.source === 'checklist' && item.version
|
|
1485
|
+
? (deps.checklistItems ?? ((version: string) => listChecklist(version, deps.root ?? releaseLedgerRoot()).items))(item.version)
|
|
1486
|
+
.find((cell) => cell.id === item.id && cell.owner === seat)?.status
|
|
1487
|
+
: item.source === 'request' ? listSeatRequests(stateRoot).find((row) => row.key === item.id && row.seat === seat)?.status
|
|
1488
|
+
: undefined;
|
|
1489
|
+
} catch (error) { observe('decision-source-unreadable', { seat, item: item.id, error: String(error).slice(0, 200) }); }
|
|
1490
|
+
const material = decisionCardMaterial({ what: `${seat} 배정 항목: ${item.title} 역할: ${inputs.role || '역할 근거 없음'}`, soFar: `요청 내용: ${item.text}`,
|
|
1491
|
+
current: `밑바탕 재측: ${currentStatus ?? '상태 미확인'}; 요청에서 금지 문면 «${action.reason}» 발견 — 실행 전 사람 승인 필요`,
|
|
1492
|
+
question: `금지 문면 «${action.reason}»이 걸린 요청을 승인할까?`, answer: '승인 전에는 집행하지 않고 보류한다',
|
|
1493
|
+
options, recommendation, confidence: '높음 — 요청에 금지 문면 직접 일치',
|
|
1494
|
+
ifUntouched: '자동 집행되지 않고 해당 칸은 보류 상태로 남는다',
|
|
1495
|
+
evidence: `${item.evidence?.trim() || '추가 근거 미기재'} · 요청 ${source} · 걸린 낱말 ${action.reason}` });
|
|
1390
1496
|
const raised = await run(['decisions', 'raise', '--title', title, '--category', 'other',
|
|
1391
|
-
'--s',
|
|
1392
|
-
'--
|
|
1497
|
+
'--s', material.scqa.s, '--c', material.scqa.c, '--q', material.scqa.q, '--a', material.scqa.a,
|
|
1498
|
+
'--pending-question', material.pendingQuestion,
|
|
1393
1499
|
'--option', 'a=사람 승인:승인 후 별도로 집행', '--option', 'b=보류:집행하지 않음',
|
|
1394
|
-
'--
|
|
1500
|
+
'--recommend', recommendation.option, '--why', recommendation.why, '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②',
|
|
1395
1501
|
'--agent', 'seat-loop', '--ref', source, '--json']);
|
|
1396
1502
|
const decision: unknown = JSON.parse(raised.trim());
|
|
1397
1503
|
if (!decision || typeof decision !== 'object' || typeof (decision as { id?: unknown }).id !== 'string') {
|
|
@@ -1403,6 +1509,51 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1403
1509
|
return entry;
|
|
1404
1510
|
}
|
|
1405
1511
|
if (config.mode === 'live-safe') {
|
|
1512
|
+
const limit = repeatStopLimit(config);
|
|
1513
|
+
const attempts = unlandedAttemptCount(seat, item, ledger, stateRoot, deps);
|
|
1514
|
+
if (attempts >= limit && !personReopened(ledger, seat, item, stateRoot)) {
|
|
1515
|
+
// Re-read the underlying item at the decision boundary, not the picked snapshot.
|
|
1516
|
+
const current = item.source === 'checklist' && item.version
|
|
1517
|
+
? (deps.checklistItems ?? ((version: string) => listChecklist(version, deps.root ?? releaseLedgerRoot()).items))(item.version)
|
|
1518
|
+
.find((cell) => cell.id === item.id && cell.owner === seat)?.status
|
|
1519
|
+
: item.source === 'request' ? listSeatRequests(stateRoot).find((row) => row.key === item.id && row.seat === seat)?.status
|
|
1520
|
+
: undefined;
|
|
1521
|
+
if (current === 'green' || current === 'done') {
|
|
1522
|
+
entry.status = 'resolved';
|
|
1523
|
+
entry.reason = `밑바탕 재측: ${current} — 반복 정지 카드 불필요`;
|
|
1524
|
+
append(path, entry);
|
|
1525
|
+
observe('repeat-recovered', { seat, item: item.id, status: current });
|
|
1526
|
+
return entry;
|
|
1527
|
+
}
|
|
1528
|
+
const reason = repeatStopReason(attempts);
|
|
1529
|
+
entry.status = 'held';
|
|
1530
|
+
entry.reason = reason;
|
|
1531
|
+
const ref = decisionRef(seat, item);
|
|
1532
|
+
const prior = new DecisionLedger({ stateDir: stateRoot }).list({ status: 'all' }).find((card) => card.refs?.includes(ref) && card.status === 'open');
|
|
1533
|
+
if (!prior) {
|
|
1534
|
+
const options = [
|
|
1535
|
+
{ key: 'a', label: '다시 연다', consequence: '결정 뒤에 자리 루프가 한 번 더 시도할 수 있다' },
|
|
1536
|
+
{ key: 'b', label: '멈춘다', consequence: '같은 칸은 다시 발사하지 않는다' },
|
|
1537
|
+
];
|
|
1538
|
+
const recommendation = { option: 'b', why: `착지 없는 ${attempts}회 재시도보다 현재 칸을 멈추는 편이 중복 실행을 피한다` };
|
|
1539
|
+
cardLedger(stateRoot, deps, now).raiseOnce({
|
|
1540
|
+
title: `${seat}: ${item.id} 반복 착지 0`, category: 'other',
|
|
1541
|
+
...decisionCardMaterial({ what: `${seat} 칸 ${item.id} · ${item.title}`, soFar: `${attempts}회 착지 없이 종료`,
|
|
1542
|
+
current: `발행 직전 밑바탕 재측: ${current ?? '상태 미확인'}; 같은 칸 착지 0`,
|
|
1543
|
+
question: '이 칸을 계속할까, 멈출까?', answer: '중복 발사를 멈추고 사람의 재개 결정을 기다린다',
|
|
1544
|
+
options, recommendation, confidence: current ? '높음 — 밑바탕 상태 재측됨' : '낮음 — 밑바탕 상태 미확인',
|
|
1545
|
+
ifUntouched: '같은 칸은 다시 발사되지 않는다', evidence: `${item.evidence ?? '원천 근거 미기재'} · ${ref} · 완료되지 않은 시도 ${attempts}회` }),
|
|
1546
|
+
options, recommendation,
|
|
1547
|
+
raisedBy: { agent: 'seat-loop' },
|
|
1548
|
+
refs: [ref],
|
|
1549
|
+
raisedAt: now.toISOString(),
|
|
1550
|
+
crossCheckSkipped: '반복 정지는 사람 판단',
|
|
1551
|
+
}, ref);
|
|
1552
|
+
}
|
|
1553
|
+
append(path, entry);
|
|
1554
|
+
observe('repeat-stopped', { seat, item: item.id, attempts, lastQueueId: [...ledger].reverse().find((row) => row.seat === seat && row.item && handledItemKey(row.item) === handledItemKey(item) && row.queueId)?.queueId ?? null });
|
|
1555
|
+
return entry;
|
|
1556
|
+
}
|
|
1406
1557
|
let queued: { id: string };
|
|
1407
1558
|
try {
|
|
1408
1559
|
queued = await (deps.enqueue ?? ((assigned, text, root, idempotencyKey) => addHarnessQueue({ seat: assigned, say: text, idempotencyKey }, { root })))(seat, action.text, stateRoot, queueKey(seat, item));
|