elanous 0.2.14 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +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 -0
- package/apps/pwa/out/ceo/index.txt +33 -0
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +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/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/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/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -2
- package/package.json +1 -1
- package/scripts/brand/check.ts +7 -5
- package/scripts/ci-typecheck-changed.ts +50 -18
- package/scripts/docs/generate-graph-anatomy.ts +315 -0
- package/scripts/expose-rubric-suggest.ts +18 -12
- package/scripts/lecture-note/node.ts +31 -11
- package/scripts/lessons/handbook-page.ts +30 -3
- package/scripts/marketing/generate-ax-brief.ts +140 -0
- package/scripts/marketing/generate-feature-map.ts +118 -0
- package/scripts/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/gate-node.ts +80 -14
- package/scripts/release-loop/vault-note-node.ts +240 -8
- package/scripts/release-story/draft.ts +57 -9
- package/scripts/rhythm/daily.ts +263 -76
- package/scripts/rhythm/weekly.ts +30 -9
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/src/agent-env/profile.ts +161 -0
- package/src/agent-substrate/pr-reviewer.ts +2 -0
- package/src/ask-user-question/pending-questions.ts +5 -0
- package/src/auth/github-app-token.ts +112 -13
- package/src/autopilot/build/llm-conflict-merge.ts +32 -6
- package/src/briefing/brief-items.ts +121 -0
- package/src/claims/claims-render.ts +14 -5
- package/src/cli/agent-env-cli.ts +21 -0
- package/src/cli/decisions-cli.ts +9 -0
- package/src/cli/doctor-cli.ts +23 -1
- package/src/cli/doctor-node-modules.ts +147 -0
- package/src/cli/intake-cli.ts +46 -17
- package/src/cli/pod-cli.ts +2 -0
- package/src/cli/release-cli.ts +61 -27
- package/src/cli/seat-requests-cli.ts +52 -0
- package/src/context-bus/context-now-public.ts +16 -6
- package/src/context-bus/context-now.ts +3 -1
- package/src/context-bus/coord-events.ts +6 -4
- package/src/decisions/decision-cards.ts +33 -7
- package/src/decisions/proact-meter.ts +286 -0
- package/src/decisions/proact1-lite.ts +86 -0
- package/src/decisions/web-push-decision-cards.ts +1 -1
- package/src/domains/alert-audit.ts +271 -0
- package/src/domains/core-tools.ts +4 -3
- package/src/domains/ops-facts-tool.ts +31 -1
- package/src/harness/harness-cli-command.ts +111 -15
- package/src/harness/harness-incidents.ts +2 -0
- package/src/harness/harness-pod-dispatch.ts +100 -11
- package/src/harness/harness-queue.ts +380 -50
- package/src/harness/helper-repair.ts +87 -0
- package/src/harness/helper-scan.ts +16 -4
- package/src/harness/launch-stamp.ts +236 -0
- package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
- package/src/harness/ledger-write-guard.ts +35 -0
- package/src/harness/pod-memory-policy.ts +15 -0
- package/src/harness/repository-name.ts +55 -0
- package/src/hq/fence-audit.ts +90 -0
- package/src/hq/hq.ts +62 -4
- package/src/hq/lease.ts +4 -4
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +31 -0
- package/src/index.ts +77 -7
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +37 -15
- package/src/intake-plane/front-classifier.ts +6 -0
- package/src/intake-plane/items.ts +3 -2
- package/src/intake-plane/lens.ts +177 -0
- package/src/intake-plane/route.ts +145 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
- package/src/launch-head/seat-goal-queue.ts +198 -0
- package/src/loops/budget.ts +36 -5
- package/src/loops/checker.ts +2 -2
- package/src/loops/orchestrator/author-depth.ts +65 -9
- package/src/loops/orchestrator/author-input-check.ts +8 -3
- package/src/loops/orchestrator/author-ledger.ts +3 -1
- package/src/loops/orchestrator/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +11 -4
- package/src/loops/orchestrator/traffic.ts +117 -11
- package/src/loops/registry.ts +142 -61
- package/src/maturity/feature-maturity.ts +2 -1
- package/src/maturity/tui-slash-availability.ts +31 -0
- package/src/mss/logging/log-store.ts +35 -4
- package/src/nexus/api/http-server.ts +28 -14
- package/src/nexus/api/loop-edges.ts +173 -0
- package/src/nexus/api/schedules-read.ts +13 -0
- package/src/nexus/api/seat-requests.ts +79 -56
- package/src/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +6 -3
- package/src/release-loop/checklist.ts +13 -4
- package/src/release-loop/feature-store.ts +58 -31
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +86 -26
- package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
- package/src/seat-dispatch/seat-request-ledger.ts +133 -0
- package/src/seat-loop/seat-loop.ts +126 -10
- package/src/self-dev/dev-pipeline.ts +8 -4
- package/src/self-dev/draft-sweep.ts +90 -10
- package/src/self-dev/draft-triage-rules.ts +25 -8
- package/src/self-dev/orchestrate.ts +15 -1
- package/src/self-dev/review-provider-fallback.ts +6 -1
- package/src/self-dev/run-supervisor.ts +49 -2
- package/src/self-implement/frozen-merges.ts +3 -2
- package/src/self-implement/goal-author.ts +43 -5
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/heal-intake.ts +120 -2
- package/src/self-implement/host-regate.ts +9 -2
- package/src/self-implement/l8-merge-queue.ts +132 -0
- package/src/self-implement/orchestrator.ts +187 -40
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/revert-guard.ts +42 -7
- package/src/self-implement/run-ledger.ts +2 -0
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +1 -1
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
- package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
- package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
- package/src/telegram.ts +25 -4
- package/src/user-config.ts +29 -5
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/nodes/bash.ts +9 -1
- package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
- package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
- package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { Database } from 'bun:sqlite';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export type SeatRequestStatus = 'pending' | 'queued' | 'rejected' | 'done';
|
|
7
|
+
export type SeatRequestRow = {
|
|
8
|
+
key: string;
|
|
9
|
+
seat: string;
|
|
10
|
+
status: SeatRequestStatus;
|
|
11
|
+
text: string;
|
|
12
|
+
queuedAt: string;
|
|
13
|
+
reason?: string;
|
|
14
|
+
closedAt?: string;
|
|
15
|
+
[field: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type SeatRequestFilters = { seat?: string; status?: SeatRequestStatus };
|
|
19
|
+
export type CloseSeatRequestsOptions = {
|
|
20
|
+
reason: string;
|
|
21
|
+
status?: 'rejected' | 'done';
|
|
22
|
+
/** Minimum age in milliseconds, measured from queuedAt; only strictly older requests qualify. */
|
|
23
|
+
olderThan?: number;
|
|
24
|
+
now?: Date;
|
|
25
|
+
dryRun?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ledgerPath = (root: string): string => join(root, 'seat-requests', 'requests.jsonl');
|
|
29
|
+
|
|
30
|
+
/** SQLite releases its transaction lock on process death, including SIGKILL. */
|
|
31
|
+
export function withSeatRequestLedgerLock<T>(path: string, work: () => T): T {
|
|
32
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
33
|
+
const db = new Database(`${path}.lock.sqlite`);
|
|
34
|
+
try {
|
|
35
|
+
db.exec('PRAGMA busy_timeout = 10000');
|
|
36
|
+
db.exec('BEGIN IMMEDIATE');
|
|
37
|
+
try {
|
|
38
|
+
const result = work();
|
|
39
|
+
db.exec('COMMIT');
|
|
40
|
+
return result;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
db.exec('ROLLBACK');
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
} finally { db.close(); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Called under the journal transaction lock; never expose a partial JSON line. */
|
|
49
|
+
export function appendSeatRequestRows<T>(path: string, rows: readonly T[],
|
|
50
|
+
write: (fd: number, data: Buffer, offset: number, length: number) => number = writeSync): void {
|
|
51
|
+
if (!rows.length) return;
|
|
52
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
53
|
+
const prior = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
54
|
+
if (prior && !prior.endsWith('\n')) throw new Error('invalid seat request journal terminator');
|
|
55
|
+
const data = Buffer.from(prior + rows.map((row) => `${JSON.stringify(row)}\n`).join(''));
|
|
56
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
57
|
+
let fd: number | undefined;
|
|
58
|
+
try {
|
|
59
|
+
fd = openSync(temp, 'wx', 0o600);
|
|
60
|
+
let offset = 0;
|
|
61
|
+
while (offset < data.length) {
|
|
62
|
+
const count = write(fd, data, offset, data.length - offset);
|
|
63
|
+
if (count <= 0) throw new Error('seat request journal write stalled');
|
|
64
|
+
offset += count;
|
|
65
|
+
}
|
|
66
|
+
fsyncSync(fd);
|
|
67
|
+
closeSync(fd);
|
|
68
|
+
fd = undefined;
|
|
69
|
+
renameSync(temp, path);
|
|
70
|
+
const directory = openSync(dirname(path), 'r');
|
|
71
|
+
try { fsyncSync(directory); } finally { closeSync(directory); }
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (fd !== undefined) closeSync(fd);
|
|
74
|
+
try { unlinkSync(temp); } catch { /* temp was not created or already renamed */ }
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function latestRows(text: string): Map<string, SeatRequestRow> {
|
|
80
|
+
const latest = new Map<string, SeatRequestRow>();
|
|
81
|
+
for (const line of text.split('\n')) {
|
|
82
|
+
if (!line.trim()) continue;
|
|
83
|
+
const row: unknown = JSON.parse(line);
|
|
84
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) throw new Error('invalid seat request row');
|
|
85
|
+
const request = row as Record<string, unknown>;
|
|
86
|
+
if (typeof request.key !== 'string' || !request.key || typeof request.seat !== 'string' || !request.seat
|
|
87
|
+
|| !['pending', 'queued', 'rejected', 'done'].includes(String(request.status))
|
|
88
|
+
|| typeof request.text !== 'string' || typeof request.queuedAt !== 'string') {
|
|
89
|
+
throw new Error('invalid seat request row');
|
|
90
|
+
}
|
|
91
|
+
latest.set(request.key, request as SeatRequestRow);
|
|
92
|
+
}
|
|
93
|
+
return latest;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Filters apply to the latest row, never to a historical row superseded by a closing event. */
|
|
97
|
+
export function listSeatRequests(root: string, filters: SeatRequestFilters = {}): SeatRequestRow[] {
|
|
98
|
+
const path = ledgerPath(root);
|
|
99
|
+
return [...latestRows(existsSync(path) ? readFileSync(path, 'utf8') : '').values()].filter((row) =>
|
|
100
|
+
(filters.seat === undefined || row.seat === filters.seat)
|
|
101
|
+
&& (filters.status === undefined || row.status === filters.status));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Append at most one terminal event for each distinct, currently open request key. */
|
|
105
|
+
export function closeSeatRequests(root: string, keys: readonly string[], options: CloseSeatRequestsOptions): SeatRequestRow[] {
|
|
106
|
+
const reason = options.reason?.trim();
|
|
107
|
+
if (!reason) throw new Error('seat request close requires a reason');
|
|
108
|
+
const status = options.status ?? 'rejected';
|
|
109
|
+
if (status !== 'rejected' && status !== 'done') throw new Error('invalid seat request close status');
|
|
110
|
+
if (options.olderThan !== undefined && (!Number.isSafeInteger(options.olderThan) || options.olderThan < 0)) {
|
|
111
|
+
throw new Error('invalid seat request older-than duration');
|
|
112
|
+
}
|
|
113
|
+
const now = options.now ?? new Date();
|
|
114
|
+
if (!Number.isFinite(now.getTime())) throw new Error('invalid seat request close time');
|
|
115
|
+
const path = ledgerPath(root);
|
|
116
|
+
const close = (): SeatRequestRow[] => {
|
|
117
|
+
const latest = latestRows(existsSync(path) ? readFileSync(path, 'utf8') : '');
|
|
118
|
+
const closed = [...new Set(keys)].flatMap((key) => {
|
|
119
|
+
const row = latest.get(key);
|
|
120
|
+
if (!row || (row.status !== 'pending' && row.status !== 'queued')) return [];
|
|
121
|
+
if (options.olderThan !== undefined) {
|
|
122
|
+
const queuedAt = Date.parse(row.queuedAt);
|
|
123
|
+
if (!Number.isFinite(queuedAt) || now.getTime() - queuedAt <= options.olderThan) return [];
|
|
124
|
+
}
|
|
125
|
+
return [{ ...row, status, reason, closedAt: now.toISOString(),
|
|
126
|
+
...(row.status === 'queued' && typeof row.ref !== 'string' && typeof row.receiptId === 'string'
|
|
127
|
+
? { ref: row.receiptId } : {}) } satisfies SeatRequestRow];
|
|
128
|
+
});
|
|
129
|
+
if (!options.dryRun) appendSeatRequestRows(path, closed);
|
|
130
|
+
return closed;
|
|
131
|
+
};
|
|
132
|
+
return options.dryRun ? close() : withSeatRequestLedgerLock(path, close);
|
|
133
|
+
}
|
|
@@ -21,6 +21,7 @@ import { loadLayeredPersonaDirs, resolveRepositoryPersonaDir, resolveStatePerson
|
|
|
21
21
|
import { orderedPersonaTodos, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
|
|
22
22
|
import { PersonaRegistry } from '../persona/registry.js';
|
|
23
23
|
import { recordSeatAskShadow } from '../seat-dispatch/seat-ask.js';
|
|
24
|
+
import { appendSeatRequestRows, listSeatRequests, withSeatRequestLedgerLock } from '../seat-dispatch/seat-request-ledger.js';
|
|
24
25
|
import { answerCrossCheck, answerSeat, askCrossCheck, askSeat, crossCheckAnswer, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatCrossChecks, seatQuestions, type CrossCheckJudgment, type SeatId } from '../seat-dispatch/seat-questions.js';
|
|
25
26
|
import { queryRunningRuns } from '../self-implement/running-runs.js';
|
|
26
27
|
import { checklistHistory, listChecklist, type Checklist } from '../release-loop/checklist.js';
|
|
@@ -42,8 +43,14 @@ export type TcCandidate =
|
|
|
42
43
|
| { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
|
|
43
44
|
| { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
|
|
44
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 };
|
|
45
|
-
export type SeatEntry = { seat: string; ts?: string; at: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
|
|
46
|
-
export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off' };
|
|
46
|
+
export type SeatEntry = { seat: string; ts?: string; at: string; modeDowngradeReason?: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'queued' | 'refused' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'awaiting-xcheck' | 'awaiting-resolution' | 'resolved-by-neighbor' | 'rejected-no-evidence' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; queueId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean; xcheckRequestedAt?: string; xcheckQuestionId?: number; xcheckNeighbor?: SeatId; xcheckNote?: string; xcheckJudgmentEvidence?: string; xcheckWaitMinutes?: number };
|
|
47
|
+
export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off'; modeDowngradeReason?: string };
|
|
48
|
+
|
|
49
|
+
export function seatLoopTickLine(result: SeatLoopResult): string {
|
|
50
|
+
const suffix = (result.status === 'shadow' || result.status === 'skipped-off') && result.modeDowngradeReason
|
|
51
|
+
? ` (live-safe 요청 · 이유: ${result.modeDowngradeReason})` : '';
|
|
52
|
+
return `seat loop ${result.seat}: ${result.status}${'runId' in result && result.runId ? ` ${result.runId}` : ''}${suffix}`;
|
|
53
|
+
}
|
|
47
54
|
export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null; action?: 'decision' | 'harness' | 'wait'; what?: string };
|
|
48
55
|
export type SeatDeps = {
|
|
49
56
|
personaConfig?: PersonaLoopConfig;
|
|
@@ -77,6 +84,8 @@ export type SeatDeps = {
|
|
|
77
84
|
stallDelivery?: (message: MessageEnvelope, key: string, root: string) => boolean;
|
|
78
85
|
/** Open cards from the decision ledger; injected only for isolated ledger tests. */
|
|
79
86
|
pendingDecisions?: () => Array<Pick<DecisionEntry, 'id' | 'title' | 'category' | 'dueAt'>>;
|
|
87
|
+
/** Running-run observation for OP cutoff; incomplete observations cannot establish that a run is absent. */
|
|
88
|
+
cutoffRuns?: (runIds: readonly string[]) => Pick<ReturnType<typeof queryRunningRuns>, 'completeness' | 'entries' | 'pty'>;
|
|
80
89
|
/** Read-only PR snapshot for TC judgment; defaults to GitHub CLI reads. */
|
|
81
90
|
pullRequests?: () => Promise<TcPullRequest[]>;
|
|
82
91
|
ledgerFiles?: (directory: string) => string[];
|
|
@@ -200,6 +209,10 @@ async function pickPersonaShadows(deps: SeatDeps, name?: string): Promise<Person
|
|
|
200
209
|
|
|
201
210
|
export async function runSeatLoopTurn(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
|
|
202
211
|
const result = await runSeatLoopOnce(seat, deps);
|
|
212
|
+
if ((result.status === 'shadow' || result.status === 'skipped-off') && result.modeDowngradeReason) {
|
|
213
|
+
try { debug.log('seat-loop', 'mode-downgraded', { seat, requested: 'live-safe', effective: result.status, reason: result.modeDowngradeReason }); }
|
|
214
|
+
catch { /* logging cannot change execution */ }
|
|
215
|
+
}
|
|
203
216
|
try { await runPersonaShadowOnce(deps); }
|
|
204
217
|
catch (error) { observe('persona-shadow-error', { error: String(error).slice(0, 200) }); }
|
|
205
218
|
try {
|
|
@@ -301,8 +314,18 @@ function decisionRef(seat: string, item: SeatItem): string {
|
|
|
301
314
|
return `seat-loop:${seat}:${encodeURIComponent(itemKey(item))}`;
|
|
302
315
|
}
|
|
303
316
|
|
|
317
|
+
// The decision ledger counts Unicode characters and sentence boundaries before it stores a card.
|
|
318
|
+
function decisionSummary(text: string): string {
|
|
319
|
+
const sentences = text.replace(/\s+/g, ' ').trim().split(/(?<=[.!?。!?])\s+/u).slice(0, 2).join(' ');
|
|
320
|
+
return Array.from(sentences).slice(0, 240).join('').trim();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function decisionSourceRef(seat: string, item: SeatItem): string {
|
|
324
|
+
return item.source === 'request' && item.id.startsWith('coord:') ? item.id : decisionRef(seat, item);
|
|
325
|
+
}
|
|
326
|
+
|
|
304
327
|
function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEntry | undefined {
|
|
305
|
-
const ref =
|
|
328
|
+
const ref = decisionSourceRef(seat, item);
|
|
306
329
|
return new DecisionLedger({ stateDir: root }).list({ status: 'all' }).find((card) => card.refs?.includes(ref));
|
|
307
330
|
}
|
|
308
331
|
|
|
@@ -456,6 +479,62 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
|
|
|
456
479
|
return { requests: pending, checklist, role: read(join(deps.repo ?? repoRoot, 'docs', 'roles', `${seat}.md`)).slice(0, 4_000) };
|
|
457
480
|
}
|
|
458
481
|
|
|
482
|
+
const OP_CUTOFF_WINDOW_MS = 2 * 60 * 60_000;
|
|
483
|
+
const RUN_REFERENCE = /\brun-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
|
484
|
+
|
|
485
|
+
function checkOpLandingCutoff(deps: SeatDeps, now: Date, path: string, stateRoot: string): SeatEntry | undefined {
|
|
486
|
+
const releaseRoot = deps.root ?? releaseLedgerRoot();
|
|
487
|
+
const shipped = releasedVersion(releaseRoot);
|
|
488
|
+
const schedule = (deps.schedules ?? (() => readSchedules(releaseRoot)))()
|
|
489
|
+
.filter((row) => /^\d+\.\d+\.\d+$/.test(row.version) && (!shipped || versionOrder(row.version, shipped) > 0))
|
|
490
|
+
.sort((a, b) => versionOrder(a.version, b.version))[0];
|
|
491
|
+
if (!schedule?.landBy) return;
|
|
492
|
+
const untilLanding = Date.parse(schedule.landBy) - now.getTime();
|
|
493
|
+
if (!Number.isFinite(untilLanding) || untilLanding < 0 || untilLanding > OP_CUTOFF_WINDOW_MS) return;
|
|
494
|
+
const append = deps.append ?? defaultAppend;
|
|
495
|
+
let items: ReturnType<NonNullable<SeatDeps['checklistItems']>>;
|
|
496
|
+
try { items = (deps.checklistItems ?? ((version: string) => listChecklist(version, releaseRoot).items))(schedule.version); }
|
|
497
|
+
catch (error) {
|
|
498
|
+
const entry: SeatEntry = { seat: 'OP', ts: now.toISOString(), at: now.toISOString(), status: 'shadow',
|
|
499
|
+
reason: `체크리스트 못 읽음: ${schedule.version} (${String(error).slice(0, 200)})`,
|
|
500
|
+
modeDowngradeReason: 'OP 판정은 shadow 기록만 구현' };
|
|
501
|
+
append(path, entry);
|
|
502
|
+
return entry;
|
|
503
|
+
}
|
|
504
|
+
const yellow = items.filter((item) => item.status === 'yellow');
|
|
505
|
+
const runIds = [...new Set(yellow.flatMap((item) => [...(item.evidence ?? '').matchAll(RUN_REFERENCE)].map(([id]) => id.toLowerCase())))];
|
|
506
|
+
let running = new Set<string>();
|
|
507
|
+
if (runIds.length) {
|
|
508
|
+
try {
|
|
509
|
+
const observed = (deps.cutoffRuns ?? ((ids: readonly string[]) => queryRunningRuns({ runIds: ids, caller: 'seat-loop.op-cutoff' })))(runIds);
|
|
510
|
+
if (observed.completeness !== 'complete' || observed.pty.unreadable.length) {
|
|
511
|
+
observe('op-cutoff-runs-unreadable', { version: schedule.version });
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
running = new Set(observed.entries.filter((row) => row.status === 'running' || row.status === 'probable-running' || row.status === 'unknown')
|
|
515
|
+
.map((row) => row.runId.toLowerCase()));
|
|
516
|
+
} catch (error) {
|
|
517
|
+
observe('op-cutoff-runs-unreadable', { version: schedule.version, error: String(error).slice(0, 200) });
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const candidates = yellow.filter((item) => ![...(item.evidence ?? '').matchAll(RUN_REFERENCE)]
|
|
522
|
+
.some(([id]) => running.has(id.toLowerCase())))
|
|
523
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
524
|
+
.map(({ id, title, owner }) => ({ id, title, owner: owner ?? '미배정' }));
|
|
525
|
+
try { debug.log('seat-loop', 'op-cutoff-check', { version: schedule.version, landBy: schedule.landBy, candidates }); }
|
|
526
|
+
catch { /* Observation cannot change a request. */ }
|
|
527
|
+
if (!candidates.length) return;
|
|
528
|
+
const key = `seat-loop:op-cutoff:${schedule.version}:${seatDay(now)}`;
|
|
529
|
+
const requestPath = join(stateRoot, 'seat-requests', 'requests.jsonl');
|
|
530
|
+
withSeatRequestLedgerLock(requestPath, () => {
|
|
531
|
+
if (listSeatRequests(stateRoot).some((row) => row.key === key)) return;
|
|
532
|
+
appendSeatRequestRows(requestPath, [{ key, seat: 'OP', status: 'pending' as const, queuedAt: now.toISOString(),
|
|
533
|
+
text: `${schedule.version} 착지 마감 ${schedule.landBy} 전 노랑 칸 점검 — 다음 판으로 옮길지 OP 판단:\n${candidates.map((item) => `${item.id} · ${item.title} · 주인 ${item.owner}`).join('\n')}`,
|
|
534
|
+
version: schedule.version, landBy: schedule.landBy, candidates }]);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
459
538
|
function opCandidates(deps: SeatDeps, now: Date): OpCandidate[] {
|
|
460
539
|
const releaseRoot = deps.root ?? releaseLedgerRoot();
|
|
461
540
|
const shipped = releasedVersion(releaseRoot);
|
|
@@ -827,10 +906,16 @@ function judgeSeatNeighbors(seat: string, config: SeatLoopConfig, deps: SeatDeps
|
|
|
827
906
|
export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promise<SeatLoopResult> {
|
|
828
907
|
if (!/^(?:MK|OP|TC|UX)$/.test(seat)) throw new Error(`unknown seat: ${seat}`);
|
|
829
908
|
const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'shadow' };
|
|
830
|
-
if (config.mode === 'off'
|
|
909
|
+
if (config.mode === 'off') {
|
|
831
910
|
observe('skipped-off', { seat });
|
|
832
911
|
return { seat, status: 'skipped-off' };
|
|
833
912
|
}
|
|
913
|
+
if (!(config.seats ?? ['MK']).includes(seat)) {
|
|
914
|
+
// An excluded seat does not read inputs or run the shadow loop; never report it as shadow.
|
|
915
|
+
observe('skipped-off', { seat });
|
|
916
|
+
return { seat, status: 'skipped-off',
|
|
917
|
+
...(config.mode === 'live-safe' ? { modeDowngradeReason: '자리 목록에 없음 (loops.seat.seats)' } : {}) };
|
|
918
|
+
}
|
|
834
919
|
const now = (deps.now ?? (() => new Date()))();
|
|
835
920
|
const path = seatLedgerPath(seat, deps.root ?? effectiveInstanceRoot(), now);
|
|
836
921
|
const directory = dirname(path);
|
|
@@ -928,6 +1013,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
928
1013
|
return entry;
|
|
929
1014
|
}
|
|
930
1015
|
}
|
|
1016
|
+
let tcDowngrade: SeatEntry | undefined;
|
|
931
1017
|
if (seat === 'TC') {
|
|
932
1018
|
const candidates = await tcCandidates(deps, now);
|
|
933
1019
|
for (const candidate of candidates) {
|
|
@@ -938,19 +1024,27 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
938
1024
|
&& row.candidate.version === candidate.version && row.candidate.id === candidate.id && row.candidate.to === candidate.to);
|
|
939
1025
|
if (!inLedger) {
|
|
940
1026
|
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
|
|
1027
|
+
...(config.mode === 'live-safe' ? { modeDowngradeReason: 'TC 타 자리 결함 질문은 shadow 기록만 구현' } : {}),
|
|
941
1028
|
item: { source: 'checklist', kind: 'cell', id: candidate.id, title: candidate.title, text: candidate.title, version: candidate.version, seat: candidate.to },
|
|
942
1029
|
action: 'seat-question', inquiry: { to: candidate.to, question } };
|
|
943
1030
|
(deps.append ?? defaultAppend)(path, entry);
|
|
1031
|
+
tcDowngrade ??= entry;
|
|
944
1032
|
}
|
|
945
1033
|
if (!inLedger || recorded) observe('ask-shadow', { from: seat, to: candidate.to, itemId: candidate.id });
|
|
946
1034
|
continue;
|
|
947
1035
|
}
|
|
948
1036
|
if (ledger.some((row) => row.status === 'shadow' && JSON.stringify(row.candidate) === JSON.stringify(candidate))) continue;
|
|
949
|
-
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate
|
|
1037
|
+
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
|
|
1038
|
+
...(config.mode === 'live-safe' ? { modeDowngradeReason: 'TC PR 판정은 shadow 기록만 구현' } : {}) };
|
|
950
1039
|
(deps.append ?? defaultAppend)(path, entry);
|
|
1040
|
+
tcDowngrade ??= entry;
|
|
951
1041
|
observe('tc-shadow-candidate', { seat, candidate });
|
|
952
1042
|
}
|
|
953
1043
|
}
|
|
1044
|
+
if (seat === 'OP' && config.mode === 'live-safe') {
|
|
1045
|
+
const unreadable = checkOpLandingCutoff(deps, now, path, stateRoot);
|
|
1046
|
+
if (unreadable) return unreadable;
|
|
1047
|
+
}
|
|
954
1048
|
if (seat === 'OP' && !seatQuestions(stateRoot, 'OP').some((question) =>
|
|
955
1049
|
!hasSeatAnswer(stateRoot, question)
|
|
956
1050
|
&& !handledKeys(ledger, config.mode !== 'on' || (config.questions ?? 'shadow') !== 'on', stateRoot).has(`seat-question::${question.id}`))) {
|
|
@@ -987,7 +1081,8 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
987
1081
|
if (id !== currentDecision.id) candidates.push({ kind: 'decision-delegation', id, verdict: 'none' });
|
|
988
1082
|
}
|
|
989
1083
|
}
|
|
990
|
-
const entries = candidates.map((candidate): SeatEntry => ({ seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate
|
|
1084
|
+
const entries = candidates.map((candidate): SeatEntry => ({ seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
|
|
1085
|
+
...(config.mode === 'live-safe' ? { modeDowngradeReason: 'OP 판정은 shadow 기록만 구현' } : {}) }));
|
|
991
1086
|
for (const entry of entries) {
|
|
992
1087
|
const candidate = entry.candidate!;
|
|
993
1088
|
const previous = [...ledger].reverse().find((row) => row.candidate?.kind === candidate.kind &&
|
|
@@ -1003,7 +1098,12 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1003
1098
|
}
|
|
1004
1099
|
return entries[0]!;
|
|
1005
1100
|
}
|
|
1101
|
+
const failedDecisions = new Set<string>();
|
|
1102
|
+
let lastDecisionFailure: SeatEntry | undefined;
|
|
1103
|
+
while (true) {
|
|
1006
1104
|
const inputs = await gatherSeatInputs(seat, deps, ledger);
|
|
1105
|
+
inputs.requests = inputs.requests.filter((candidate) => !failedDecisions.has(itemKey(candidate)));
|
|
1106
|
+
inputs.checklist = inputs.checklist.filter((candidate) => !failedDecisions.has(itemKey(candidate)));
|
|
1007
1107
|
const questionsMode = config.mode === 'on' ? config.questions ?? 'shadow' : 'shadow';
|
|
1008
1108
|
const questionHandled = handledKeys(ledger, questionsMode !== 'on', stateRoot);
|
|
1009
1109
|
const pendingQuestion = inputs.requests.find((candidate) => candidate.source === 'seat-question'
|
|
@@ -1085,6 +1185,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1085
1185
|
entry.action = 'decision';
|
|
1086
1186
|
}
|
|
1087
1187
|
if (questionsMode !== 'on') {
|
|
1188
|
+
if (config.mode === 'live-safe') entry.modeDowngradeReason = '질문 전달 선행 조건 미충족 (questions shadow)';
|
|
1088
1189
|
if (config.mode === 'live-safe' && !isRescueAllowedAction('seat-question')) {
|
|
1089
1190
|
observe('refused', { seat, item: item.id, action: 'seat-question', reason: 'question delivery is shadow-only' });
|
|
1090
1191
|
}
|
|
@@ -1192,9 +1293,11 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1192
1293
|
if (item && config.mode === 'live-safe' && deps.inquire && !isRescueAllowedAction('seat-question')) {
|
|
1193
1294
|
observe('refused', { seat, item: item.id, action: 'seat-question', reason: 'outbound inquiry not in rescue policy' });
|
|
1194
1295
|
}
|
|
1296
|
+
if (!item && config.mode === 'live-safe' && tcDowngrade) return tcDowngrade;
|
|
1195
1297
|
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: item ? 'shadow' : 'skipped-empty',
|
|
1196
1298
|
...(item ? { item, action: planned!.kind, ...(planned!.reason ? { reason: planned!.reason } : {}) } : { action: 'skipped-empty' as const }) };
|
|
1197
1299
|
if (config.mode === 'shadow' || !item) {
|
|
1300
|
+
if (!item && lastDecisionFailure) return lastDecisionFailure;
|
|
1198
1301
|
append(path, entry);
|
|
1199
1302
|
if (config.mode === 'shadow') observe('shadow', { seat, item, status: entry.status });
|
|
1200
1303
|
return entry;
|
|
@@ -1282,11 +1385,14 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1282
1385
|
append(path, { ...entry, status: 'attempting' });
|
|
1283
1386
|
try {
|
|
1284
1387
|
if (action.kind === 'decision') {
|
|
1285
|
-
const title = `${seat}: ${action.text
|
|
1388
|
+
const title = `${seat}: ${decisionSummary(action.text)}`;
|
|
1389
|
+
const source = decisionSourceRef(seat, item);
|
|
1286
1390
|
const raised = await run(['decisions', 'raise', '--title', title, '--category', 'other',
|
|
1287
|
-
'--s', `${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')}
|
|
1391
|
+
'--s', decisionSummary(`${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')} 역할: ${inputs.role}`),
|
|
1392
|
+
'--c', '자동 실행 금지 문면에 해당하여 사람 결정이 필요하다',
|
|
1288
1393
|
'--option', 'a=사람 승인:승인 후 별도로 집행', '--option', 'b=보류:집행하지 않음',
|
|
1289
|
-
'--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②',
|
|
1394
|
+
'--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②',
|
|
1395
|
+
'--agent', 'seat-loop', '--ref', source, '--json']);
|
|
1290
1396
|
const decision: unknown = JSON.parse(raised.trim());
|
|
1291
1397
|
if (!decision || typeof decision !== 'object' || typeof (decision as { id?: unknown }).id !== 'string') {
|
|
1292
1398
|
throw new Error('decisions raise returned no decision id');
|
|
@@ -1332,9 +1438,19 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
1332
1438
|
observe('launched', { seat, item, runId });
|
|
1333
1439
|
return entry;
|
|
1334
1440
|
} catch (error) {
|
|
1335
|
-
|
|
1441
|
+
const failure: SeatEntry = { ...entry, status: 'outcome-unknown' };
|
|
1442
|
+
append(path, failure);
|
|
1443
|
+
if (action.kind === 'decision') {
|
|
1444
|
+
lastDecisionFailure = failure;
|
|
1445
|
+
const source = decisionSourceRef(seat, item);
|
|
1446
|
+
try { debug.log('seat-loop', 'decision-raise-failed', { source, error: String(error) }); }
|
|
1447
|
+
catch { /* A failed observation cannot interrupt the remaining items. */ }
|
|
1448
|
+
failedDecisions.add(itemKey(item));
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1336
1451
|
throw error;
|
|
1337
1452
|
}
|
|
1453
|
+
}
|
|
1338
1454
|
} catch (error) {
|
|
1339
1455
|
turnError = error;
|
|
1340
1456
|
throw error;
|
|
@@ -35,6 +35,7 @@ import type { DocumentReferenceStatus } from '../self-implement/self-implement-r
|
|
|
35
35
|
import type { SelfDevGoal, OrchestrateSelfDevOptions, SelfDevJobResult } from './orchestrate.js';
|
|
36
36
|
import { launchCapabilityObservation, resolveLaunchCapabilities } from './launch-capabilities.js';
|
|
37
37
|
import { lookupEntrance, type EntranceId } from './entrance-registry.js';
|
|
38
|
+
import { resolveLaunchStamp } from '../harness/launch-stamp.js';
|
|
38
39
|
import type { IngestionEntry } from '../agent-substrate/execution/ingestion-policy.js';
|
|
39
40
|
import type { ReviewerContextItem } from '../agent-substrate/pr-reviewer.js';
|
|
40
41
|
import { resolveRunIdentity, type RunIdSource } from '../harness/harness-space.js';
|
|
@@ -42,7 +43,7 @@ import { addSelfDevRunParticipant, processBirthId, saveSelfDevRun } from './run-
|
|
|
42
43
|
import { getUserConfig, resolveRoleLlm } from '../user-config.js';
|
|
43
44
|
import { reviewReasoningEffort } from '../model-tier/review-effort.js';
|
|
44
45
|
import { getProvider, inferProviderFromModel } from '../llm.js';
|
|
45
|
-
import { buildReviewProviderAttempts, runReviewWithFallback, reviewFallbackModelsFromConfig } from './review-provider-fallback.js';
|
|
46
|
+
import { buildReviewProviderAttempts, defaultReviewFallbackModel, runReviewWithFallback, reviewFallbackModelsFromConfig } from './review-provider-fallback.js';
|
|
46
47
|
import type { DefaultSeamsOptions } from '../self-implement/seams.js';
|
|
47
48
|
import { harnessTargetOptions, resolveHarnessTarget, revalidateHarnessTarget, type HarnessTargetResolution } from '../self-implement/harness-target-options.js';
|
|
48
49
|
import { provisionRepository, type RepoProvisionResult } from '../self-implement/repo-provision.js';
|
|
@@ -609,6 +610,7 @@ export function toSelfImplementOptions(text: string, plan: ResolvedDevPlan, seam
|
|
|
609
610
|
...(s.correlationId !== undefined ? { correlationId: s.correlationId } : {}),
|
|
610
611
|
...(s.branchName ? { branchName: s.branchName } : {}),
|
|
611
612
|
...(s.maxReworkRounds !== undefined ? { maxReworkRounds: s.maxReworkRounds } : {}),
|
|
613
|
+
launch: resolveLaunchStamp({ entrance: plan.entrance, queueId: process.env.ELANOUS_HARNESS_QUEUE_LAUNCH }),
|
|
612
614
|
};
|
|
613
615
|
}
|
|
614
616
|
|
|
@@ -1093,10 +1095,12 @@ export async function buildDefaultSelfImplementSeams(
|
|
|
1093
1095
|
provider: inferProviderFromModel(model) ? getProvider(model) : undefined,
|
|
1094
1096
|
label,
|
|
1095
1097
|
});
|
|
1098
|
+
const primaryReviewAttempt = reviewAttemptForModel(reviewModel, 'primary');
|
|
1096
1099
|
const reviewAttempts = buildReviewProviderAttempts(
|
|
1097
|
-
|
|
1098
|
-
reviewFallbackModels
|
|
1099
|
-
|
|
1100
|
+
primaryReviewAttempt,
|
|
1101
|
+
[...reviewFallbackModels, defaultReviewFallbackModel(primaryReviewAttempt.provider?.name)]
|
|
1102
|
+
.map((model) => reviewAttemptForModel(model, model)),
|
|
1103
|
+
).slice(0, 2);
|
|
1100
1104
|
const defaultApiReview = async (prompt: string): Promise<string> => {
|
|
1101
1105
|
const r = await runReviewWithFallback(
|
|
1102
1106
|
reviewAttempts,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CLAIM_IDLE_HOURS, PR_LABELS, STALLED_DRAFT_HOURS } from '../github/pr-labels.js';
|
|
2
2
|
import { debug } from '../debug/log.js';
|
|
3
|
-
import { decideDraft, type DraftTriagePr } from './draft-triage-rules.js';
|
|
3
|
+
import { decideDraft, sameGoalPr, type DraftTriagePr } from './draft-triage-rules.js';
|
|
4
4
|
|
|
5
5
|
export interface SweepDraft extends DraftTriagePr {
|
|
6
6
|
readonly title: string;
|
|
@@ -33,6 +33,18 @@ export interface DraftSweepAdapters {
|
|
|
33
33
|
getLatestFileChanges?(draft: SweepDraft, repository: string): Promise<Readonly<Record<string, string>> | undefined>;
|
|
34
34
|
/** Optional merge-time file inventory for the existing all-files-landed decision. */
|
|
35
35
|
getPrFiles?(repository: string, number: number): Promise<readonly string[] | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* Review and gate posture for the daily count. Omitted means unknown — not harvestable and not blocked.
|
|
38
|
+
* `review: 'pass'` is a review PASS; `mustFix` means must-fix remains; `gate: 'pass'` is a green gate.
|
|
39
|
+
*/
|
|
40
|
+
getReviewGate?(draft: SweepDraft, repository: string): Promise<SweepReviewGate | undefined>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Review PASS and gate posture used only by the daily old-draft count. */
|
|
44
|
+
export interface SweepReviewGate {
|
|
45
|
+
readonly review?: 'pass' | 'fail';
|
|
46
|
+
readonly mustFix?: boolean;
|
|
47
|
+
readonly gate?: 'pass' | 'fail';
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
export interface DraftSweepOptions {
|
|
@@ -67,9 +79,23 @@ export interface DraftSweepResult {
|
|
|
67
79
|
claimed?: number;
|
|
68
80
|
/** Running claims older than the idle window, including those kept by liveness. */
|
|
69
81
|
claimExpired?: number;
|
|
82
|
+
/**
|
|
83
|
+
* One line per sweep: harness drafts older than 24h, split into closable (a same-goal landing exists),
|
|
84
|
+
* harvestable (review PASS and gate pass), and blocked (must-fix remains).
|
|
85
|
+
*/
|
|
86
|
+
daily?: DraftSweepDaily;
|
|
70
87
|
error?: string;
|
|
71
88
|
}
|
|
72
89
|
|
|
90
|
+
/** Daily old-draft census. `date` is the sweep clock's UTC calendar day. */
|
|
91
|
+
export interface DraftSweepDaily {
|
|
92
|
+
readonly date: string;
|
|
93
|
+
readonly over24h: number;
|
|
94
|
+
readonly closable: number;
|
|
95
|
+
readonly harvestable: number;
|
|
96
|
+
readonly blocked: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
73
99
|
const stateLabels = PR_LABELS.filter((label) => label.axis === 'state');
|
|
74
100
|
const label = (action: 'mark-stalled' | 'close', name?: string): string => {
|
|
75
101
|
const match = PR_LABELS.find((entry) => entry.axis === 'state' && entry.sweep.action === action && (name === undefined || entry.name.endsWith(name)));
|
|
@@ -115,7 +141,7 @@ export async function supersedeDraftsOnMerge(input: {
|
|
|
115
141
|
const mergedFiles = await adapters.getPrFiles?.(repository, merged.number);
|
|
116
142
|
const landed = { ...merged, changedFiles: mergedFiles };
|
|
117
143
|
for (const draft of drafts) {
|
|
118
|
-
if (draft.number !== merged.number && !liveBranches.has(draft.branch)
|
|
144
|
+
if (draft.number !== merged.number && draft.branch.startsWith('self-impl/') && !liveBranches.has(draft.branch)
|
|
119
145
|
&& Date.parse(draft.createdAt) < Date.parse(merged.mergedAt)) {
|
|
120
146
|
statuses.set(draft.number, await adapters.getRunStatus(draft, repository));
|
|
121
147
|
if (draft.changedFiles?.length && adapters.getLatestFileChanges) {
|
|
@@ -125,7 +151,7 @@ export async function supersedeDraftsOnMerge(input: {
|
|
|
125
151
|
}
|
|
126
152
|
for (const listedDraft of drafts) {
|
|
127
153
|
const draft = { ...listedDraft, latestFileChanges: latestChanges.get(listedDraft.number) };
|
|
128
|
-
if (draft.number === merged.number || liveBranches.has(draft.branch)
|
|
154
|
+
if (draft.number === merged.number || !draft.branch.startsWith('self-impl/') || liveBranches.has(draft.branch)
|
|
129
155
|
|| Date.parse(draft.createdAt) >= Date.parse(merged.mergedAt)) {
|
|
130
156
|
kept.push(draft.number);
|
|
131
157
|
continue;
|
|
@@ -134,7 +160,6 @@ export async function supersedeDraftsOnMerge(input: {
|
|
|
134
160
|
mergedTwins: [landed], liveBranches, ageHours: NaN });
|
|
135
161
|
if (decision.action !== 'close' || !decision.reason.startsWith(`superseded-by #${merged.number}`)
|
|
136
162
|
|| PR_LABELS.filter((entry) => entry.axis === 'state' && draft.labels.includes(entry.name)).length > 1
|
|
137
|
-
|| (draft.labels.length === 0 && !draft.branch.startsWith('self-impl/'))
|
|
138
163
|
|| closeAttempts >= DRAFT_SWEEP_CLOSE_CAP) {
|
|
139
164
|
kept.push(draft.number);
|
|
140
165
|
continue;
|
|
@@ -144,7 +169,8 @@ export async function supersedeDraftsOnMerge(input: {
|
|
|
144
169
|
try {
|
|
145
170
|
// Label first: a failed label write must never strand a closed, unlabelled PR outside open-draft sweeps.
|
|
146
171
|
if (!draft.labels.includes(supersededLabel)) await adapters.setLabels(repository, draft.number, { add: supersededLabel, remove });
|
|
147
|
-
await adapters.closeDraft(repository, draft.number,
|
|
172
|
+
await adapters.closeDraft(repository, draft.number,
|
|
173
|
+
`Draft sweep: superseded-by #${merged.number} (https://github.com/${repository}/pull/${merged.number}). Branch preserved.`);
|
|
148
174
|
closed.push(draft.number);
|
|
149
175
|
} catch (failure) {
|
|
150
176
|
kept.push(draft.number);
|
|
@@ -171,6 +197,46 @@ async function collectPages<T>(fetch: (page: number, perPage: number) => Promise
|
|
|
171
197
|
throw new Error('PR listing exceeded pagination limit');
|
|
172
198
|
}
|
|
173
199
|
|
|
200
|
+
/** First line of a failure, with no stack — the draft-cleanup loop log shows this line as-is. */
|
|
201
|
+
export function sweepFailureReason(error: unknown): string {
|
|
202
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
203
|
+
const line = message.split(/\r?\n/).map((part) => part.trim()).find((part) => part.length > 0 && !/^\s*at\s/.test(part));
|
|
204
|
+
return line && line.length > 0 ? line : 'sweep failed';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function harnessDraft(draft: SweepDraft): boolean {
|
|
208
|
+
return draft.branch.startsWith('self-impl/') || draft.labels.some((name) => name.startsWith('elanous:'));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Census of harness drafts older than 24h. Same-goal landing uses `sameGoalPr` unchanged.
|
|
213
|
+
* A draft counts as closable when a same-goal merged PR exists, harvestable when review PASS and the gate passed,
|
|
214
|
+
* and blocked when must-fix remains. Unknown review/gate is neither harvestable nor blocked.
|
|
215
|
+
*/
|
|
216
|
+
export function draftSweepDaily(
|
|
217
|
+
drafts: readonly SweepDraft[],
|
|
218
|
+
merged: readonly SweepMergedPr[],
|
|
219
|
+
now: Date,
|
|
220
|
+
reviewGate: ReadonlyMap<number, SweepReviewGate | undefined>,
|
|
221
|
+
): DraftSweepDaily {
|
|
222
|
+
const cutoff = now.getTime() - STALLED_DRAFT_HOURS * 3_600_000;
|
|
223
|
+
let over24h = 0;
|
|
224
|
+
let closable = 0;
|
|
225
|
+
let harvestable = 0;
|
|
226
|
+
let blocked = 0;
|
|
227
|
+
for (const draft of drafts) {
|
|
228
|
+
if (!harnessDraft(draft)) continue;
|
|
229
|
+
const created = Date.parse(draft.createdAt);
|
|
230
|
+
if (!Number.isFinite(created) || created > cutoff) continue;
|
|
231
|
+
over24h += 1;
|
|
232
|
+
if (merged.some((pr) => pr.number !== draft.number && sameGoalPr(draft, pr))) closable += 1;
|
|
233
|
+
const posture = reviewGate.get(draft.number);
|
|
234
|
+
if (posture?.mustFix === true) blocked += 1;
|
|
235
|
+
else if (posture?.review === 'pass' && posture.gate === 'pass') harvestable += 1;
|
|
236
|
+
}
|
|
237
|
+
return { date: now.toISOString().slice(0, 10), over24h, closable, harvestable, blocked };
|
|
238
|
+
}
|
|
239
|
+
|
|
174
240
|
/** A failed inventory or liveness lookup is never treated as proof that a draft can be closed. */
|
|
175
241
|
export async function runDraftSweep({ repository, adapters, apply = false, now = new Date() }: DraftSweepOptions): Promise<DraftSweepResult> {
|
|
176
242
|
const result: DraftSweepResult = { repository, apply, complete: false, entries: [], counts: {} };
|
|
@@ -181,10 +247,12 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
|
|
|
181
247
|
};
|
|
182
248
|
const finish = (): DraftSweepResult => {
|
|
183
249
|
debug.log('drafts.cleanup', 'summary', result.counts);
|
|
250
|
+
if (result.daily) debug.log('harness.drafts', 'daily', result.daily);
|
|
251
|
+
if (!result.complete && result.error) debug.log('drafts.cleanup', 'failed', { reason: sweepFailureReason(result.error) });
|
|
184
252
|
return result;
|
|
185
253
|
};
|
|
186
|
-
let drafts: SweepDraft[];
|
|
187
|
-
let merged: SweepMergedPr[];
|
|
254
|
+
let drafts: SweepDraft[] = [];
|
|
255
|
+
let merged: SweepMergedPr[] = [];
|
|
188
256
|
let liveBranches: ReadonlySet<string> | undefined;
|
|
189
257
|
try {
|
|
190
258
|
[drafts, merged, liveBranches] = await Promise.all([
|
|
@@ -196,7 +264,8 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
|
|
|
196
264
|
if (drafts.some((pr) => !Number.isInteger(pr.number) || !pr.branch || !Array.isArray(pr.labels) || !Number.isFinite(Date.parse(pr.createdAt)))
|
|
197
265
|
|| merged.some((pr) => !Number.isInteger(pr.number) || !pr.branch)) throw new Error('Invalid PR inventory');
|
|
198
266
|
} catch (error) {
|
|
199
|
-
result.error =
|
|
267
|
+
result.error = sweepFailureReason(error);
|
|
268
|
+
if (drafts.length > 0) result.daily = draftSweepDaily(drafts, merged, now, new Map());
|
|
200
269
|
return finish();
|
|
201
270
|
}
|
|
202
271
|
let statuses: Map<number, string | undefined>;
|
|
@@ -218,9 +287,19 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
|
|
|
218
287
|
}
|
|
219
288
|
}
|
|
220
289
|
} catch (error) {
|
|
221
|
-
result.error =
|
|
290
|
+
result.error = sweepFailureReason(error);
|
|
222
291
|
return finish();
|
|
223
292
|
}
|
|
293
|
+
let reviewGate = new Map<number, SweepReviewGate | undefined>();
|
|
294
|
+
if (adapters.getReviewGate) {
|
|
295
|
+
try {
|
|
296
|
+
for (const draft of drafts) reviewGate.set(draft.number, await adapters.getReviewGate(draft, repository));
|
|
297
|
+
} catch (error) {
|
|
298
|
+
result.error = sweepFailureReason(error);
|
|
299
|
+
return finish();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
result.daily = draftSweepDaily(drafts, merged, now, reviewGate);
|
|
224
303
|
result.complete = true;
|
|
225
304
|
let closes = 0;
|
|
226
305
|
let unobserved = 0;
|
|
@@ -330,7 +409,8 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
|
|
|
330
409
|
if (action === 'close') {
|
|
331
410
|
await adapters.closeDraft(repository, draft.number, decision.reason === 'stale-unobserved'
|
|
332
411
|
? `Draft sweep: run unobserved — no run record reachable here, no host worktree for this branch, idle ≥${STALLED_DRAFT_HOURS}h. Closed as stalled. Branch preserved; reopen to restore.`
|
|
333
|
-
: `Draft sweep: ${decision.reason}.
|
|
412
|
+
: `Draft sweep: ${decision.reason}${decision.reason.startsWith('superseded-by #')
|
|
413
|
+
? ` (https://github.com/${repository}/pull/${decision.reason.slice('superseded-by #'.length).match(/^\d+/)![0]})` : ''}. Branch preserved.`);
|
|
334
414
|
}
|
|
335
415
|
entry.applied = labelsChanged || action === 'close';
|
|
336
416
|
} catch (error) {
|