elanous 0.2.18 → 0.2.20
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-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
- package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
- package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +16 -15
- 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 +16 -15
- 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 +24 -23
- 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/demo/editor-recipes.yaml +22 -0
- package/graphs/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/retro/daily.yaml +25 -0
- package/graphs/retro/recipes.yaml +24 -0
- package/package.json +1 -1
- package/scripts/ask-marker-check.ts +3 -1
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +3 -1
- package/scripts/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +24 -0
- package/scripts/morning-report.ts +18 -4
- package/scripts/pch-regress.ts +88 -0
- package/scripts/release-loop/gate-diff.ts +18 -0
- package/scripts/release-loop/gate-node.ts +628 -65
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +229 -0
- package/scripts/release-loop/preflight.ts +6 -4
- package/scripts/release-loop/publish-window.ts +141 -0
- package/scripts/release-loop/release-docs.ts +1 -4
- package/scripts/release-loop/release-universe.ts +38 -0
- package/scripts/release-loop/resume-release.ts +67 -5
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +12 -2
- package/scripts/release-story/draft.ts +5 -1
- package/scripts/retro/shadow-compare.ts +71 -0
- package/scripts/rhythm/daily.ts +6 -1
- package/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- package/src/agent-mission/driver.ts +34 -6
- package/src/autopilot/build/llm-conflict-merge.ts +132 -7
- package/src/autopilot/build/merge-intent.ts +135 -7
- package/src/away/presence.ts +40 -0
- package/src/away/release-watch.ts +272 -0
- package/src/chat/index.ts +2 -1
- package/src/chat/short-question.ts +3 -40
- package/src/cli/away-cli.ts +68 -0
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +13 -7
- package/src/cli/log-event-names.ts +5 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/pr-cli.ts +20 -0
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +52 -12
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +193 -14
- package/src/context-bus/context-now-public.ts +1 -1
- package/src/context-bus/context-now-surfaces.ts +16 -6
- package/src/context-bus/context-now.ts +97 -3
- package/src/core-turn/short-question.ts +100 -0
- package/src/dashboard/dashboard-status-lines.ts +24 -4
- package/src/dashboard/index.ts +50 -5
- package/src/dashboard/input/slash-executor.ts +6 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
- package/src/dashboard/slash-runtime/help-groups.ts +44 -0
- package/src/dashboard/slash-runtime/model-choices.ts +125 -0
- package/src/domains/guardian-tick.ts +138 -0
- package/src/domains/outbound-alert.ts +44 -13
- package/src/domains/outbound-log-probe.ts +6 -1
- package/src/domains/schedule-registry.ts +23 -0
- package/src/domains/telegram-kind-route.ts +39 -0
- package/src/domains/watch-default-scheduler.ts +95 -0
- package/src/esc-abort-gate.ts +44 -0
- package/src/graph-runner/graph-run-api.ts +205 -0
- package/src/graph-runner/runner.ts +36 -6
- package/src/harness/harness-cli-command.ts +420 -51
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +140 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +17 -2
- package/src/index.ts +118 -6
- package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
- package/src/instance/test-write-guard.ts +200 -0
- package/src/intake-plane/digest.ts +39 -15
- package/src/intake-plane/harness-target.ts +4 -3
- package/src/intake-plane/intake-sources.ts +1 -0
- package/src/intake-plane/items.ts +1 -0
- package/src/loops/orchestrator/degradation.ts +57 -0
- package/src/loops/orchestrator/finish-rate.ts +33 -3
- package/src/loops/orchestrator/tick.ts +166 -45
- package/src/maturity/feature-maturity.ts +6 -4
- package/src/maturity/tui-slash-maturity.ts +1 -0
- package/src/mss/logging/log-store.ts +3 -0
- package/src/nexus/api/graph-kinds.ts +10 -4
- package/src/nexus/api/graphs-api.ts +178 -6
- package/src/nexus/api/http-server.ts +54 -8
- package/src/nexus/api/loop-edges.ts +292 -17
- package/src/nexus/api/ops-api.ts +112 -29
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/nexus/index.ts +7 -0
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding.ts +24 -6
- package/src/release-loop/checklist.ts +2 -0
- package/src/release-loop/feature-store.ts +68 -18
- package/src/release-loop/gate-shards.ts +158 -0
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +51 -17
- package/src/release-loop/release-schedule.ts +6 -2
- package/src/release-loop/rubric.ts +57 -1
- package/src/retro/daily.ts +372 -0
- package/src/seat-loop/op-decide.ts +173 -0
- package/src/seat-loop/seat-loop.ts +60 -3
- package/src/seat-loop/seat-report.ts +58 -3
- package/src/self-dev/ask-launch-flow.ts +5 -4
- package/src/self-dev/ask-launch-io.ts +49 -1
- package/src/self-dev/ask-path-candidates.ts +40 -2
- package/src/self-dev/dev-cli.ts +36 -0
- package/src/self-dev/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +247 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/gate-cli.ts +29 -8
- package/src/self-implement/gate-env-retry.ts +83 -0
- package/src/self-implement/goal-author-clarification.ts +88 -13
- package/src/self-implement/goal-clarification-escalation.ts +2 -0
- package/src/self-implement/orchestrator.ts +220 -6
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -0
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/self-implement/salvage-retention.ts +123 -0
- package/src/self-implement/seams.ts +4 -0
- package/src/self-implement/self-implement-cli.ts +2 -0
- package/src/self-implement/sibling-resync.ts +223 -0
- package/src/self-implement/stop-autoheal.ts +204 -0
- package/src/task-agent/actions.ts +63 -2
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +292 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +25 -0
- package/src/task-agent/live-moves.ts +276 -0
- package/src/task-agent/mission.ts +33 -7
- package/src/task-agent/shadow.ts +192 -15
- package/src/task-agent/task-hand.ts +124 -17
- package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
- package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
- package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +52 -0
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +190 -5
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RUN-TTL (0.2.21 P1 · first piece · FINDING #24712) — an open harness PR has a «time to landing».
|
|
3
|
+
*
|
|
4
|
+
* Open self-impl PRs linger for days while main moves (~80 merges/day), so an old draft rots silently.
|
|
5
|
+
* Past `tools.selfImplement.runTtlHours` (default 24) since its last commit, the sweep makes one pre-emptive move:
|
|
6
|
+
* · live owning run → one resync memo in its control inbox (same path as SIBLING-RESYNC #24893/#24899),
|
|
7
|
+
* at most once per (PR, head sha) — a rebase moves the head and re-arms it
|
|
8
|
+
* · no live run → the registered `elanous:needs-rebase` label, once (QUIET-PR-COMMENTS: never a comment)
|
|
9
|
+
*
|
|
10
|
+
* Mode `tools.selfImplement.runTtl.mode`: `shadow` (default) only logs what it would do; `live` writes.
|
|
11
|
+
* Cost bound: a PR opened inside the TTL is still inside its own time-to-landing and is never fetched
|
|
12
|
+
* (`freshByCreation`), already-marked PRs without a live run are skipped before any fetch, at most `maxFetch` last-commit lookups per sweep with `concurrency` in flight, and at most `cap` actions.
|
|
13
|
+
*
|
|
14
|
+
* Pure core: every side effect is an injected adapter so tests never touch GitHub or the inbox.
|
|
15
|
+
*/
|
|
16
|
+
import { debug } from '../debug/log.js';
|
|
17
|
+
import type { ControlMemoPayload } from '../harness/control-inbox.js';
|
|
18
|
+
import { SIBLING_RESYNC_MARKER_LABEL, type SiblingRunTarget } from './sibling-resync.js';
|
|
19
|
+
|
|
20
|
+
export const RUN_TTL_CATEGORY = 'self-implement.run-ttl';
|
|
21
|
+
export const RUN_TTL_DEFAULT_HOURS = 24;
|
|
22
|
+
export const RUN_TTL_DEFAULT_CAP = 30;
|
|
23
|
+
export const RUN_TTL_DEFAULT_MAX_FETCH = 60;
|
|
24
|
+
export const RUN_TTL_DEFAULT_CONCURRENCY = 6;
|
|
25
|
+
export const RUN_TTL_MARKER_LABEL = SIBLING_RESYNC_MARKER_LABEL;
|
|
26
|
+
const HARNESS_BRANCH_PREFIX = 'self-impl/';
|
|
27
|
+
const KEEP_LABEL = 'elanous:keep';
|
|
28
|
+
|
|
29
|
+
export type RunTtlMode = 'shadow' | 'live';
|
|
30
|
+
|
|
31
|
+
export interface RunTtlCandidate {
|
|
32
|
+
readonly number: number;
|
|
33
|
+
readonly branch: string;
|
|
34
|
+
readonly labels: readonly string[];
|
|
35
|
+
readonly createdAt: string;
|
|
36
|
+
readonly headSha: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RunTtlAdapters {
|
|
40
|
+
/** Open PRs against the default branch. A rejection is not an empty list. */
|
|
41
|
+
listOpenPrs(): Promise<readonly RunTtlCandidate[]>;
|
|
42
|
+
/** ISO time of the head commit; `undefined` (or a rejection) means unknown and the PR is left alone. */
|
|
43
|
+
lastCommitAt(pr: RunTtlCandidate): Promise<string | undefined>;
|
|
44
|
+
resolveRun(pr: RunTtlCandidate): Promise<SiblingRunTarget>;
|
|
45
|
+
sendMemo(target: { runId: string; spaceId: string }, memo: ControlMemoPayload): Promise<void> | void;
|
|
46
|
+
addLabel(prNumber: number, label: string): Promise<void> | void;
|
|
47
|
+
/** Memo dedupe by `<pr>:<headSha>`. Omitted ⇒ no dedupe. */
|
|
48
|
+
memoSent?(key: string): boolean;
|
|
49
|
+
recordMemoSent?(key: string): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RunTtlResult {
|
|
53
|
+
readonly mode: RunTtlMode;
|
|
54
|
+
readonly ttlHours: number;
|
|
55
|
+
/** Open self-impl PRs considered (keep-labelled excluded). */
|
|
56
|
+
readonly considered: number;
|
|
57
|
+
readonly freshByCreation: number;
|
|
58
|
+
readonly fetched: number;
|
|
59
|
+
readonly fresh: number;
|
|
60
|
+
readonly unknown: readonly number[];
|
|
61
|
+
readonly stale: readonly number[];
|
|
62
|
+
/** Memo sent (live) or would be sent (shadow). */
|
|
63
|
+
readonly memo: readonly number[];
|
|
64
|
+
/** Label added (live) or would be added (shadow). */
|
|
65
|
+
readonly label: readonly number[];
|
|
66
|
+
readonly alreadyMarked: readonly number[];
|
|
67
|
+
readonly alreadyMemoed: readonly number[];
|
|
68
|
+
/** Owning run lookup failed — left alone (a failed lookup is not a dead run). */
|
|
69
|
+
readonly unresolved?: readonly number[];
|
|
70
|
+
readonly skippedOverCap: readonly number[];
|
|
71
|
+
readonly skippedFetchCap: readonly number[];
|
|
72
|
+
readonly failed: readonly number[];
|
|
73
|
+
readonly durationMs: number;
|
|
74
|
+
readonly reason?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function runTtlMemo(ageHours: number, ttlHours: number): ControlMemoPayload {
|
|
78
|
+
return {
|
|
79
|
+
version: 1,
|
|
80
|
+
kind: 'run-ttl-resync',
|
|
81
|
+
urgency: 'normal',
|
|
82
|
+
body: `[run-ttl] This PR's last commit is ${Math.floor(ageHours)}h old (time-to-landing ${ttlHours}h) while main keeps moving. `
|
|
83
|
+
+ 'Before continuing: git fetch origin main, rebase this branch onto origin/main (resolve conflicts keeping both intents), then re-run the gate on your changed files.',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function mapBounded<T, R>(items: readonly T[], limit: number, work: (item: T) => Promise<R>): Promise<R[]> {
|
|
88
|
+
const results = new Array<R>(items.length);
|
|
89
|
+
let next = 0;
|
|
90
|
+
const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
91
|
+
while (next < items.length) {
|
|
92
|
+
const index = next++;
|
|
93
|
+
results[index] = await work(items[index]!);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
await Promise.all(workers);
|
|
97
|
+
return results;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const nonNegativeInt = (value: number | undefined, fallback: number): number =>
|
|
101
|
+
value !== undefined && Number.isSafeInteger(value) && value >= 0 ? value : fallback;
|
|
102
|
+
|
|
103
|
+
export async function runRunTtlSweep(input: {
|
|
104
|
+
adapters: RunTtlAdapters;
|
|
105
|
+
mode?: RunTtlMode;
|
|
106
|
+
ttlHours?: number;
|
|
107
|
+
cap?: number;
|
|
108
|
+
maxFetch?: number;
|
|
109
|
+
concurrency?: number;
|
|
110
|
+
now?: () => number;
|
|
111
|
+
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
112
|
+
}): Promise<RunTtlResult> {
|
|
113
|
+
const { adapters } = input;
|
|
114
|
+
const now = input.now ?? Date.now;
|
|
115
|
+
const startedAt = now();
|
|
116
|
+
const mode: RunTtlMode = input.mode === 'live' ? 'live' : 'shadow';
|
|
117
|
+
const ttlHours = typeof input.ttlHours === 'number' && Number.isFinite(input.ttlHours) && input.ttlHours > 0
|
|
118
|
+
? input.ttlHours : RUN_TTL_DEFAULT_HOURS;
|
|
119
|
+
const cap = nonNegativeInt(input.cap, RUN_TTL_DEFAULT_CAP);
|
|
120
|
+
const maxFetch = nonNegativeInt(input.maxFetch, RUN_TTL_DEFAULT_MAX_FETCH);
|
|
121
|
+
const concurrency = Math.max(1, nonNegativeInt(input.concurrency, RUN_TTL_DEFAULT_CONCURRENCY));
|
|
122
|
+
const ttlMs = ttlHours * 3_600_000;
|
|
123
|
+
const log = input.log ?? ((category, event, data) => debug.log(category, event, data));
|
|
124
|
+
const emit = (event: string, data: Record<string, unknown>) => {
|
|
125
|
+
try { log(RUN_TTL_CATEGORY, event, { mode, ...data }); } catch { /* fail-soft */ }
|
|
126
|
+
};
|
|
127
|
+
let considered = 0;
|
|
128
|
+
let freshByCreation = 0;
|
|
129
|
+
let fetched = 0;
|
|
130
|
+
let fresh = 0;
|
|
131
|
+
const unknown: number[] = [];
|
|
132
|
+
const stale: number[] = [];
|
|
133
|
+
const memo: number[] = [];
|
|
134
|
+
const label: number[] = [];
|
|
135
|
+
const alreadyMarked: number[] = [];
|
|
136
|
+
const alreadyMemoed: number[] = [];
|
|
137
|
+
const unresolved: number[] = [];
|
|
138
|
+
const skippedOverCap: number[] = [];
|
|
139
|
+
const skippedFetchCap: number[] = [];
|
|
140
|
+
const failed: number[] = [];
|
|
141
|
+
const result = (reason?: string): RunTtlResult => ({
|
|
142
|
+
mode, ttlHours, considered, freshByCreation, fetched, fresh, unknown, stale, memo, label, alreadyMarked,
|
|
143
|
+
alreadyMemoed, unresolved, skippedOverCap, skippedFetchCap, failed, durationMs: Math.max(0, now() - startedAt),
|
|
144
|
+
...(reason ? { reason } : {}),
|
|
145
|
+
});
|
|
146
|
+
const summary = (out: RunTtlResult): RunTtlResult => {
|
|
147
|
+
emit('summary', {
|
|
148
|
+
ttlHours, cap, maxFetch, concurrency, considered, freshByCreation, fetched, fresh,
|
|
149
|
+
unknown: unknown.length, stale: stale.length, memo: memo.length, label: label.length,
|
|
150
|
+
alreadyMarked: alreadyMarked.length, alreadyMemoed: alreadyMemoed.length, unresolved: unresolved.length,
|
|
151
|
+
skippedOverCap: skippedOverCap.length, skippedFetchCap: skippedFetchCap.length, failed: failed.length,
|
|
152
|
+
memoPrs: memo, labelPrs: label, durationMs: out.durationMs, ...(out.reason ? { reason: out.reason } : {}),
|
|
153
|
+
});
|
|
154
|
+
return out;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let open: readonly RunTtlCandidate[];
|
|
158
|
+
try {
|
|
159
|
+
open = await adapters.listOpenPrs();
|
|
160
|
+
} catch (error) {
|
|
161
|
+
emit('skipped', { reason: 'inventory-failed', error: String(error) });
|
|
162
|
+
return summary(result('inventory-failed'));
|
|
163
|
+
}
|
|
164
|
+
const nowMs = now();
|
|
165
|
+
const pending: { pr: RunTtlCandidate; target: SiblingRunTarget }[] = [];
|
|
166
|
+
for (const pr of open) {
|
|
167
|
+
if (!pr.branch.startsWith(HARNESS_BRANCH_PREFIX) || pr.labels.includes(KEEP_LABEL)) continue;
|
|
168
|
+
considered += 1;
|
|
169
|
+
const created = Date.parse(pr.createdAt);
|
|
170
|
+
// A PR opened inside the TTL is inside its own time-to-landing — no fetch needed.
|
|
171
|
+
if (Number.isFinite(created) && nowMs - created < ttlMs) { freshByCreation += 1; continue; }
|
|
172
|
+
let target: SiblingRunTarget;
|
|
173
|
+
try { target = await adapters.resolveRun(pr); } catch (error) {
|
|
174
|
+
// A failed lookup is not proof the run is gone — never label (or memo) an unresolved PR.
|
|
175
|
+
unresolved.push(pr.number);
|
|
176
|
+
emit('run-unresolved', { pr: pr.number, error: String(error) });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// Dead run + marker already present: the quiet signal is in place («once»), nothing to fetch.
|
|
180
|
+
if (!target.alive && pr.labels.includes(RUN_TTL_MARKER_LABEL)) { alreadyMarked.push(pr.number); continue; }
|
|
181
|
+
pending.push({ pr, target });
|
|
182
|
+
}
|
|
183
|
+
// Oldest PR first: the most likely to have rotted.
|
|
184
|
+
pending.sort((a, b) => (Date.parse(a.pr.createdAt) || 0) - (Date.parse(b.pr.createdAt) || 0) || a.pr.number - b.pr.number);
|
|
185
|
+
const toFetch = pending.slice(0, maxFetch);
|
|
186
|
+
skippedFetchCap.push(...pending.slice(maxFetch).map(({ pr }) => pr.number));
|
|
187
|
+
const dated = await mapBounded(toFetch, concurrency, async (entry) => {
|
|
188
|
+
let at: string | undefined;
|
|
189
|
+
try { at = await adapters.lastCommitAt(entry.pr); } catch { at = undefined; }
|
|
190
|
+
fetched += 1;
|
|
191
|
+
const ms = at ? Date.parse(at) : NaN;
|
|
192
|
+
return { ...entry, ageMs: Number.isFinite(ms) ? nowMs - ms : undefined };
|
|
193
|
+
});
|
|
194
|
+
const stalePrs: (typeof dated[number] & { ageMs: number })[] = [];
|
|
195
|
+
for (const entry of dated) {
|
|
196
|
+
if (entry.ageMs === undefined) { unknown.push(entry.pr.number); continue; }
|
|
197
|
+
if (entry.ageMs < ttlMs) { fresh += 1; continue; }
|
|
198
|
+
stale.push(entry.pr.number);
|
|
199
|
+
stalePrs.push({ ...entry, ageMs: entry.ageMs });
|
|
200
|
+
}
|
|
201
|
+
stalePrs.sort((a, b) => b.ageMs - a.ageMs || a.pr.number - b.pr.number);
|
|
202
|
+
let actions = 0;
|
|
203
|
+
for (const { pr, target, ageMs } of stalePrs) {
|
|
204
|
+
const ageHours = ageMs / 3_600_000;
|
|
205
|
+
const key = `${pr.number}:${pr.headSha}`;
|
|
206
|
+
if (target.alive) {
|
|
207
|
+
let already: boolean | undefined;
|
|
208
|
+
try { already = adapters.memoSent?.(key) ?? false; } catch (error) {
|
|
209
|
+
// Without the journal «once» cannot be proven — skip rather than risk a repeat memo.
|
|
210
|
+
failed.push(pr.number);
|
|
211
|
+
emit('memo-journal-failed', { pr: pr.number, stage: 'read', error: String(error) });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (already) { alreadyMemoed.push(pr.number); continue; }
|
|
215
|
+
}
|
|
216
|
+
if (!target.alive && pr.labels.includes(RUN_TTL_MARKER_LABEL)) { alreadyMarked.push(pr.number); continue; }
|
|
217
|
+
if (actions >= cap) { skippedOverCap.push(pr.number); continue; }
|
|
218
|
+
actions += 1;
|
|
219
|
+
if (target.alive) {
|
|
220
|
+
if (mode === 'shadow') {
|
|
221
|
+
memo.push(pr.number);
|
|
222
|
+
emit('would-memo', { pr: pr.number, runId: target.runId, spaceId: target.spaceId, ageHours: Math.floor(ageHours) });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
// Journal first: a memo that cannot be recorded is not sent, so «once per (PR, head sha)» holds.
|
|
226
|
+
try { adapters.recordMemoSent?.(key); } catch (error) {
|
|
227
|
+
failed.push(pr.number);
|
|
228
|
+
emit('memo-journal-failed', { pr: pr.number, stage: 'write', error: String(error) });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
await adapters.sendMemo({ runId: target.runId, spaceId: target.spaceId }, runTtlMemo(ageHours, ttlHours));
|
|
233
|
+
memo.push(pr.number);
|
|
234
|
+
emit('memo', { pr: pr.number, runId: target.runId, spaceId: target.spaceId, ageHours: Math.floor(ageHours) });
|
|
235
|
+
continue;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
// A live run we could not reach still needs to be visible — fall through to the quiet marker.
|
|
238
|
+
emit('memo-failed', { pr: pr.number, runId: target.runId, error: String(error) });
|
|
239
|
+
if (pr.labels.includes(RUN_TTL_MARKER_LABEL)) { alreadyMarked.push(pr.number); continue; }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const reason = target.alive ? 'memo-failed' : target.reason;
|
|
243
|
+
if (mode === 'shadow') {
|
|
244
|
+
label.push(pr.number);
|
|
245
|
+
emit('would-label', { pr: pr.number, label: RUN_TTL_MARKER_LABEL, reason, ageHours: Math.floor(ageHours) });
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
await adapters.addLabel(pr.number, RUN_TTL_MARKER_LABEL);
|
|
250
|
+
label.push(pr.number);
|
|
251
|
+
emit('label', { pr: pr.number, label: RUN_TTL_MARKER_LABEL, reason, ageHours: Math.floor(ageHours) });
|
|
252
|
+
} catch (error) {
|
|
253
|
+
failed.push(pr.number);
|
|
254
|
+
emit('label-failed', { pr: pr.number, error: String(error) });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return summary(result());
|
|
258
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SALVAGE-VISIBLE — Pod 런이 멈추며 남긴 수확 가지(`salvage/<job>/<worktree>`)를 «런 하나 → PR | 수확 가지 | 없음 | 모름» 으로 보인다.
|
|
3
|
+
*
|
|
4
|
+
* S: Pod Job 은 자식 rc≠0 이면 워크트리를 `salvage/si-task-<job>/self-impl-…-r<run 16진 앞 6자>` 로 push 한다
|
|
5
|
+
* (`src/task-orchestrator/surfaces/self-implement-pod.ts` salvage 단계 · 이름은 `plannedSelfImplBranch` 의 `-r` 접미).
|
|
6
|
+
* C: 10-07 세 자리가 PR·`salvage-pushed` 로그만 보고 멈춘 Pod 런 여덟을 «수확 0» 으로 읽었다 — 여덟 다 수백 줄짜리 가지가 있었다.
|
|
7
|
+
* Q: 런 id 하나로 그 가지를 «한 번의 목록»에서 찾고, 크기를 재고, 못 읽었을 때 «없음» 이라 말하지 않으려면?
|
|
8
|
+
* A: `git ls-remote origin 'refs/heads/salvage/*'` 한 번 → `-r<6>` 접미로 Job 을 고르고, 같은 Job 의 형제(`/repo`·`-early`)까지
|
|
9
|
+
* numstat 로 재서 `.gitignore`·`docs/goals/**` 만 건드린 가지는 버린다. 목록 실패는 `unknown` 이다(⛔ `none` 이 아니다).
|
|
10
|
+
*
|
|
11
|
+
* 순수 모듈 — git 은 주입된 러너로만 부른다(시험은 네트워크 0).
|
|
12
|
+
*/
|
|
13
|
+
import { runGitCommand } from '../git-fs/runner.js';
|
|
14
|
+
|
|
15
|
+
export interface SalvageGitResult {
|
|
16
|
+
status: number | null;
|
|
17
|
+
stdout: string;
|
|
18
|
+
stderr: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SalvageGitRunner = (args: string[]) => SalvageGitResult;
|
|
22
|
+
|
|
23
|
+
export interface SalvageDiffstat {
|
|
24
|
+
files: number;
|
|
25
|
+
insertions: number;
|
|
26
|
+
deletions: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SalvageBranch {
|
|
30
|
+
branch: string;
|
|
31
|
+
sha: string;
|
|
32
|
+
diffstat: SalvageDiffstat;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type SalvageLookup =
|
|
36
|
+
| { status: 'found'; branches: SalvageBranch[] }
|
|
37
|
+
| { status: 'none'; trivialSkipped: number }
|
|
38
|
+
| { status: 'unknown'; reason: string }
|
|
39
|
+
/** PR 이 이겨서 가지를 «조회하지 않았다» — ⛔ 부재(`none`)가 아니다. */
|
|
40
|
+
| { status: 'not-checked' };
|
|
41
|
+
|
|
42
|
+
export interface RemoteSalvageRef {
|
|
43
|
+
branch: string;
|
|
44
|
+
sha: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const SALVAGE_REF_PREFIX = 'refs/heads/';
|
|
48
|
+
|
|
49
|
+
/** `run-bfe2b5c1-…` → `bfe2b5` — `plannedSelfImplBranch` 의 `-r` 접미와 같은 규칙. 6자가 안 되면 null(⛔ 짧은 접두는 남의 가지를 문다). */
|
|
50
|
+
export function runSuffixKey(runId: string): string | null {
|
|
51
|
+
// `run-` 뒤에 «연속된» 16진 6자가 있어야 한다 — 구분자를 지우고 이어 붙이면 `run-ab-cdef` 가 남의 `abcdef` 를 문다.
|
|
52
|
+
const m = /^(?:run-)?([0-9a-f]{6})/i.exec(runId.trim());
|
|
53
|
+
return m ? m[1]!.toLowerCase() : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `git ls-remote` 출력 → `{ branch, sha }`. salvage/ 가 아닌 «올바른» 줄은 버리고, 형식이 깨진 비어 있지 않은 줄은 `unparsed` 로 센다
|
|
57
|
+
* (⛔ rc=0 ⊕ 못 읽은 출력을 «없음» 으로 접지 않는다). */
|
|
58
|
+
export function parseLsRemote(stdout: string): RemoteSalvageRef[] & { unparsed?: number } {
|
|
59
|
+
const rows: RemoteSalvageRef[] & { unparsed?: number } = [];
|
|
60
|
+
let unparsed = 0;
|
|
61
|
+
const lines = stdout.split(/\r?\n/);
|
|
62
|
+
if (lines.at(-1) === '') lines.pop();
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
// 공백만 있는 줄도 «못 읽은 줄» 이다(⛔ 건너뛰면 깨진 목록이 «없음» 이 된다).
|
|
65
|
+
const m = /^([0-9a-f]{40}|[0-9a-f]{64})\t(refs\/\S+)$/i.exec(line);
|
|
66
|
+
if (!m) {
|
|
67
|
+
unparsed += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!m[2]!.startsWith(`${SALVAGE_REF_PREFIX}salvage/`)) continue;
|
|
71
|
+
rows.push({ sha: m[1]!.toLowerCase(), branch: m[2]!.slice(SALVAGE_REF_PREFIX.length) });
|
|
72
|
+
}
|
|
73
|
+
if (unparsed > 0) rows.unparsed = unparsed;
|
|
74
|
+
return rows;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 워크트리 조각(마지막 경로 성분)이 `-r<key>` 또는 `-r<key>-early` 로 끝나는가. */
|
|
78
|
+
export function branchMatchesRun(branch: string, key: string): boolean {
|
|
79
|
+
const leaf = branch.slice(branch.lastIndexOf('/') + 1).toLowerCase();
|
|
80
|
+
return leaf.endsWith(`-r${key}`) || leaf.endsWith(`-r${key}-early`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function jobDir(branch: string): string {
|
|
84
|
+
return branch.slice(0, branch.lastIndexOf('/'));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 런 하나에 속하는 원격 가지 — `-r<key>` 로 맞은 Job 디렉토리의 형제 전부(`/repo` 포함). */
|
|
88
|
+
export function selectRunSalvageRefs(refs: readonly RemoteSalvageRef[], key: string): RemoteSalvageRef[] {
|
|
89
|
+
const jobs = new Set(refs.filter((ref) => branchMatchesRun(ref.branch, key)).map((ref) => jobDir(ref.branch)));
|
|
90
|
+
return refs.filter((ref) => jobs.has(jobDir(ref.branch)));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 루트 `.gitignore`·`docs/goals/**` 만 바꾼 가지는 «수확할 일» 이 아니다(Pod 가 `/repo` 에 골 문서만 남긴다). */
|
|
94
|
+
export function isTrivialSalvagePath(path: string): boolean {
|
|
95
|
+
// ⛔ trim 하지 않는다 — `-z` 경로는 원 바이트이고 ` .gitignore` 는 다른 파일이다.
|
|
96
|
+
return path === '.gitignore' || path.startsWith('docs/goals/');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `git diff --numstat -z --no-renames` → diffstat ⊕ 경로. 레코드는 NUL 로 끊기고 경로는 따옴표 없이 «원 바이트» 그대로다
|
|
100
|
+
* (⛔ `-z` 없이는 한글·특수문자 경로가 `"docs/goals/\355…"` 로 감싸져 제외 규칙을 못 탄다). 바이너리(`-`)는 줄 수 0. */
|
|
101
|
+
export function parseNumstat(stdout: string): { diffstat: SalvageDiffstat; paths: string[]; unparsed: number } {
|
|
102
|
+
const paths: string[] = [];
|
|
103
|
+
let insertions = 0;
|
|
104
|
+
let deletions = 0;
|
|
105
|
+
let unparsed = 0;
|
|
106
|
+
const records = stdout.split('\0');
|
|
107
|
+
// `-z` 출력은 레코드마다 NUL 로 «끝난다» — 마지막 조각이 비어 있지 않으면 잘린 출력이다(⛔ 정상 레코드로 받지 않는다).
|
|
108
|
+
const tail = records.pop() ?? '';
|
|
109
|
+
if (tail !== '') unparsed += 1;
|
|
110
|
+
for (const record of records) {
|
|
111
|
+
if (record === '') {
|
|
112
|
+
unparsed += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const m = /^(\d+|-)\t(\d+|-)\t([^\0]+)$/.exec(record.replace(/^\n+/, ''));
|
|
116
|
+
if (!m) {
|
|
117
|
+
unparsed += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
insertions += m[1] === '-' ? 0 : Number(m[1]);
|
|
121
|
+
deletions += m[2] === '-' ? 0 : Number(m[2]);
|
|
122
|
+
paths.push(m[3]!);
|
|
123
|
+
}
|
|
124
|
+
return { diffstat: { files: paths.length, insertions, deletions }, paths, unparsed };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 러너가 «던져도» 결과로 접는다 — 실행 예외는 rc=null ⊕ 메시지(호출부가 `unknown` 으로 옮긴다). */
|
|
128
|
+
function runSafely(git: SalvageGitRunner, args: string[]): SalvageGitResult {
|
|
129
|
+
try {
|
|
130
|
+
return git(args);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return { status: null, stdout: '', stderr: `threw: ${error instanceof Error ? error.message : String(error)}` };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function failureReason(step: string, result: SalvageGitResult): string {
|
|
137
|
+
const tail = result.stderr.trim().split(/\r?\n/).at(-1) ?? '';
|
|
138
|
+
return `${step} rc=${result.status ?? 'signal'}${tail ? ` · ${tail.slice(0, 160)}` : ''}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface FindSalvageOptions {
|
|
142
|
+
/** 이미 읽은 ls-remote 목록(여러 런을 한 번의 목록으로). 없으면 러너로 한 번 읽는다. */
|
|
143
|
+
listing?: RemoteSalvageRef[] | { error: string };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 비교 기준 — fetch 가 함께 갱신하는 `origin/main` 하나로 고정한다(다른 기준은 fetch 와 어긋난다). */
|
|
147
|
+
const SALVAGE_DIFF_BASE = 'origin/main';
|
|
148
|
+
|
|
149
|
+
/** `git ls-remote origin 'refs/heads/salvage/*'` 한 번. 실패는 `{ error }` — 호출부가 `unknown` 으로 옮긴다. */
|
|
150
|
+
export function listRemoteSalvageRefs(git: SalvageGitRunner): RemoteSalvageRef[] | { error: string } {
|
|
151
|
+
const result = runSafely(git, ['ls-remote', 'origin', 'refs/heads/salvage/*']);
|
|
152
|
+
if (result.status !== 0) return { error: failureReason('ls-remote', result) };
|
|
153
|
+
const rows = parseLsRemote(result.stdout);
|
|
154
|
+
if (rows.unparsed) return { error: `ls-remote 출력 ${rows.unparsed}줄을 못 읽음` };
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 런 id → 그 런의 수확 가지. 목록·fetch·diff 중 하나라도 못 읽으면 `unknown`(⛔ `none` 으로 접지 않는다). */
|
|
159
|
+
export function findRunSalvageBranches(runId: string, git: SalvageGitRunner, options: FindSalvageOptions = {}): SalvageLookup {
|
|
160
|
+
const key = runSuffixKey(runId);
|
|
161
|
+
if (key === null) return { status: 'unknown', reason: `런 id 에서 16진 6자를 못 뽑았다 (${runId})` };
|
|
162
|
+
const listing = options.listing ?? listRemoteSalvageRefs(git);
|
|
163
|
+
if (!Array.isArray(listing)) return { status: 'unknown', reason: listing.error };
|
|
164
|
+
const refs = selectRunSalvageRefs(listing, key);
|
|
165
|
+
if (refs.length === 0) return { status: 'none', trivialSkipped: 0 };
|
|
166
|
+
const fetched = runSafely(git, ['fetch', '--no-tags', '--quiet', 'origin', 'main', ...refs.map((ref) => `${SALVAGE_REF_PREFIX}${ref.branch}`)]);
|
|
167
|
+
if (fetched.status !== 0) return { status: 'unknown', reason: failureReason('fetch', fetched) };
|
|
168
|
+
const branches: SalvageBranch[] = [];
|
|
169
|
+
let trivialSkipped = 0;
|
|
170
|
+
for (const ref of refs) {
|
|
171
|
+
const diff = runSafely(git, ['diff', '--numstat', '-z', '--no-renames', `${SALVAGE_DIFF_BASE}...${ref.sha}`]);
|
|
172
|
+
if (diff.status !== 0) return { status: 'unknown', reason: failureReason(`diff ${ref.branch}`, diff) };
|
|
173
|
+
const { diffstat, paths, unparsed } = parseNumstat(diff.stdout);
|
|
174
|
+
if (unparsed > 0) return { status: 'unknown', reason: `diff ${ref.branch} 출력 ${unparsed}줄을 못 읽음` };
|
|
175
|
+
if (paths.every(isTrivialSalvagePath)) {
|
|
176
|
+
trivialSkipped += 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
branches.push({ branch: ref.branch, sha: ref.sha, diffstat });
|
|
180
|
+
}
|
|
181
|
+
branches.sort((a, b) => b.diffstat.insertions + b.diffstat.deletions - (a.diffstat.insertions + a.diffstat.deletions));
|
|
182
|
+
return branches.length > 0 ? { status: 'found', branches } : { status: 'none', trivialSkipped };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 실물 러너 — 공용 git 심(`runGitCommand` · 락 재시도) 위에서 cwd 를 «인자로» 받는다. */
|
|
186
|
+
export function gitSalvageRunner(cwd: string): SalvageGitRunner {
|
|
187
|
+
return (args) => {
|
|
188
|
+
const result = runGitCommand(cwd, args, { encoding: 'utf8', timeout: 120_000, maxBuffer: 64 * 1024 * 1024 });
|
|
189
|
+
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export type RunPrState =
|
|
194
|
+
| { status: 'pr'; number: number }
|
|
195
|
+
| { status: 'none' }
|
|
196
|
+
| { status: 'unknown'; reason: string };
|
|
197
|
+
|
|
198
|
+
export type RunSalvageOutcome = 'pr' | 'salvage' | 'none' | 'unknown';
|
|
199
|
+
|
|
200
|
+
export interface RunSalvageVerdict {
|
|
201
|
+
runId: string;
|
|
202
|
+
outcome: RunSalvageOutcome;
|
|
203
|
+
pr: RunPrState;
|
|
204
|
+
salvage: SalvageLookup;
|
|
205
|
+
line: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function branchLine(branch: SalvageBranch): string {
|
|
209
|
+
return `수확 가지 ${branch.branch} (+${branch.diffstat.insertions}/-${branch.diffstat.deletions} · ${branch.diffstat.files} files)`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 판정 — PR 이 있으면 PR 이 이긴다. 없으면 수확 가지 → 없음 → 모름 순. PR 을 못 읽었으면 «PR 모름» 을 같이 단다. */
|
|
213
|
+
export function decideRunSalvage(runId: string, pr: RunPrState, salvage: SalvageLookup): RunSalvageVerdict {
|
|
214
|
+
const prNote = pr.status === 'unknown' ? ` · PR 모름(${pr.reason})` : '';
|
|
215
|
+
if (pr.status === 'pr') return { runId, outcome: 'pr', pr, salvage, line: `${runId} PR #${pr.number}` };
|
|
216
|
+
if (salvage.status === 'found') {
|
|
217
|
+
const [first, ...rest] = salvage.branches;
|
|
218
|
+
const more = rest.length > 0 ? ` ⊕ 가지 ${rest.length}개 더` : '';
|
|
219
|
+
return { runId, outcome: 'salvage', pr, salvage, line: `${runId} ${branchLine(first!)}${more}${prNote}` };
|
|
220
|
+
}
|
|
221
|
+
if (salvage.status === 'not-checked') return { runId, outcome: 'unknown', pr, salvage, line: `${runId} 모름(수확 가지 미조회)${prNote}` };
|
|
222
|
+
if (salvage.status === 'unknown') return { runId, outcome: 'unknown', pr, salvage, line: `${runId} 모름(${salvage.reason})${prNote}` };
|
|
223
|
+
const skipped = salvage.trivialSkipped > 0 ? ` (골 문서·.gitignore 만 있는 가지 ${salvage.trivialSkipped}개 제외)` : '';
|
|
224
|
+
// PR 을 못 읽었으면 «없음» 이라 단정하지 않는다.
|
|
225
|
+
if (pr.status === 'unknown') return { runId, outcome: 'unknown', pr, salvage, line: `${runId} 모름(수확 가지 없음${skipped} · PR 모름 — ${pr.reason})` };
|
|
226
|
+
return { runId, outcome: 'none', pr, salvage, line: `${runId} 없음${skipped}` };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** 원장 항목 → PR 상태. 마지막 `pr-opened` 의 번호(없으면 `run-rollup.prNumber`).
|
|
230
|
+
* ⛔ 번호가 없다고 곧 «PR 없음» 이 아니다 — 원장이 «종료(`run-status`)» 를 PR 아닌 단계로 적었을 때만 `none`.
|
|
231
|
+
* 원장 파일이 없거나(호스트가 모르는 Pod 런) 종료 기록이 없거나 종료가 PR 단계인데 번호가 없으면 `unknown`. */
|
|
232
|
+
export function prStateFromLedger(entries: ReadonlyArray<{ event: string; data: Record<string, unknown> }> | null): RunPrState {
|
|
233
|
+
if (entries === null) return { status: 'unknown', reason: '호스트 원장 없음' };
|
|
234
|
+
const pick = (value: unknown): number | null => (typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : null);
|
|
235
|
+
const prNumberOf = (data: Record<string, unknown>): number | null => pick(data.number) ?? pick(data.prNumber)
|
|
236
|
+
?? (typeof data.prUrl === 'string' ? pick(Number(/\/pull\/(\d+)/.exec(data.prUrl)?.[1])) : null);
|
|
237
|
+
const openedEntries = entries.filter((entry) => entry.event === 'pr-opened');
|
|
238
|
+
const opened = openedEntries.at(-1)?.data;
|
|
239
|
+
// 번호 «있는» 마지막 pr-opened — 뒤에 번호 없는 기록이 와도 알려진 PR 을 버리지 않는다.
|
|
240
|
+
const number = openedEntries.map((entry) => prNumberOf(entry.data)).filter((n): n is number => n !== null).at(-1)
|
|
241
|
+
?? pick(entries.filter((entry) => entry.event === 'run-rollup').at(-1)?.data.prNumber) ?? null;
|
|
242
|
+
if (number !== null) return { status: 'pr', number };
|
|
243
|
+
if (opened !== undefined) return { status: 'unknown', reason: '원장에 pr-opened 가 있으나 번호 없음' };
|
|
244
|
+
const terminal = entries.filter((entry) => entry.event === 'run-status').at(-1)?.data.stage;
|
|
245
|
+
if (typeof terminal !== 'string') return { status: 'unknown', reason: '원장에 PR·종료 기록 없음' };
|
|
246
|
+
if (!NO_PR_TERMINAL_STAGES.has(terminal)) return { status: 'unknown', reason: `마지막 단계 ${terminal} — PR 없는 종료로 확인 안 됨` };
|
|
247
|
+
return { status: 'none' };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** PR 을 열기 «전에» 끝나는 종료 단계(`run-status-mapping.ts` `SelfImplementStage` 중). ⛔ merged·pr-opened·merge-ready·merge-conflict 는
|
|
251
|
+
* PR 이 있었을 단계라 빠진다 — 그 단계인데 번호가 없으면 `unknown`. 진행 중 단계도 여기 없으므로 `unknown`. */
|
|
252
|
+
const NO_PR_TERMINAL_STAGES: ReadonlySet<string> = new Set([
|
|
253
|
+
'worktree-completed', 'gate-failed', 'review-blocked', 'no-changes', 'aborted', 'timed-out', 'pr-declined', 'soft-stopped', 'parked',
|
|
254
|
+
]);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SALVAGE-RETENTION (RFC-draft-pr-accumulation-root-fix R1 다음 조각) — 원격 `salvage/*` 가지의 보존 규칙.
|
|
3
|
+
*
|
|
4
|
+
* S: DRAFT-NOT-ARCHIVE 로 멈춘 런은 PR 대신 수확 가지를 남긴다. 가지는 이미 1,000개를 넘었다.
|
|
5
|
+
* C: 가지가 지워지지 않고 쌓이면 draft 재고가 «수확 가지 재고»로 옮겨 갈 뿐이다.
|
|
6
|
+
* Q: 어느 가지를 지워도 되나?
|
|
7
|
+
* A: 14일보다 오래됐고, 열린 PR 도(head·본문) 카드도(런 키·본문) 가리키지 않는 가지. 지금은 «그림자»로 수만 낸다.
|
|
8
|
+
* `tools.selfImplement.salvageRetention.mode` = 'shadow'(기본) | 'live'. ⛔ 이 조각은 live 여도 지우지 않는다(다음 조각).
|
|
9
|
+
*
|
|
10
|
+
* 순수 판정 + 주입된 조회. ⛔ 나이를 못 읽은 가지는 «오래됨»으로 세지 않는다(`unknownAge`).
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { branchMatchesRun, runSuffixKey } from './salvage-branches.js';
|
|
14
|
+
|
|
15
|
+
export type SalvageRetentionMode = 'shadow' | 'live';
|
|
16
|
+
export const SALVAGE_RETENTION_MAX_AGE_DAYS = 14;
|
|
17
|
+
|
|
18
|
+
export interface DatedSalvageRef {
|
|
19
|
+
readonly branch: string;
|
|
20
|
+
readonly committedAt: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SalvageReferenceSources {
|
|
24
|
+
/** 열린 PR — head 가지 ⊕ 본문(본문에 가지 이름이 적혀 있으면 참조로 센다). */
|
|
25
|
+
readonly openPrs: ReadonlyArray<{ number: number; headRefName: string; body?: string | null }>;
|
|
26
|
+
/** 과제 카드 — 런 id(가지의 `-r<6hex>` 와 맞추기) ⊕ 카드 직렬화 본문. */
|
|
27
|
+
readonly cards: ReadonlyArray<{ id: string; runId?: string; text: string }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SalvageRetentionReport {
|
|
31
|
+
readonly mode: SalvageRetentionMode;
|
|
32
|
+
readonly maxAgeDays: number;
|
|
33
|
+
readonly total: number;
|
|
34
|
+
readonly older: number;
|
|
35
|
+
readonly unknownAge: number;
|
|
36
|
+
readonly referenced: number;
|
|
37
|
+
/** 지울 후보 — 오래됐고 참조 0. 그림자에서는 수만 보고한다. */
|
|
38
|
+
readonly candidates: number;
|
|
39
|
+
readonly sample: readonly string[];
|
|
40
|
+
readonly deleted: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 가지를 가리키는 것이 있나 — 열린 PR head/본문 · 카드 런 키/본문. */
|
|
44
|
+
export function salvageBranchReferenced(branch: string, sources: SalvageReferenceSources): boolean {
|
|
45
|
+
if (sources.openPrs.some((pr) => pr.headRefName === branch || (pr.body ?? '').includes(branch))) return true;
|
|
46
|
+
return sources.cards.some((card) => {
|
|
47
|
+
if (card.text.includes(branch)) return true;
|
|
48
|
+
const key = card.runId ? runSuffixKey(card.runId) : null;
|
|
49
|
+
return key !== null && branchMatchesRun(branch, key);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function classifySalvageRetention(input: {
|
|
54
|
+
refs: readonly DatedSalvageRef[];
|
|
55
|
+
sources: SalvageReferenceSources;
|
|
56
|
+
now: Date;
|
|
57
|
+
mode: SalvageRetentionMode;
|
|
58
|
+
maxAgeDays?: number;
|
|
59
|
+
}): SalvageRetentionReport {
|
|
60
|
+
const maxAgeDays = input.maxAgeDays ?? SALVAGE_RETENTION_MAX_AGE_DAYS;
|
|
61
|
+
const cutoff = input.now.getTime() - maxAgeDays * 86_400_000;
|
|
62
|
+
let older = 0;
|
|
63
|
+
let unknownAge = 0;
|
|
64
|
+
let referenced = 0;
|
|
65
|
+
const candidates: string[] = [];
|
|
66
|
+
for (const ref of input.refs) {
|
|
67
|
+
const at = ref.committedAt ? Date.parse(ref.committedAt) : NaN;
|
|
68
|
+
if (!Number.isFinite(at)) { unknownAge += 1; continue; }
|
|
69
|
+
if (at >= cutoff) continue;
|
|
70
|
+
older += 1;
|
|
71
|
+
if (salvageBranchReferenced(ref.branch, input.sources)) { referenced += 1; continue; }
|
|
72
|
+
candidates.push(ref.branch);
|
|
73
|
+
}
|
|
74
|
+
return { mode: input.mode, maxAgeDays, total: input.refs.length, older, unknownAge, referenced, candidates: candidates.length, sample: candidates.slice(0, 5), deleted: 0 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function formatSalvageRetention(report: SalvageRetentionReport): string {
|
|
78
|
+
return `수확 가지 보존(${report.mode}): 전체 ${report.total} · ${report.maxAgeDays}일 초과 ${report.older} · 그중 참조 있음 ${report.referenced} · 지울 후보(참조 0) ${report.candidates}`
|
|
79
|
+
+ `${report.unknownAge ? ` · 나이 모름 ${report.unknownAge}` : ''} · 지운 것 0${report.mode === 'live' ? '(live 삭제는 다음 조각)' : ''}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type GhExecute = (args: string[]) => string;
|
|
83
|
+
const executeGh: GhExecute = (args) => execFileSync('gh', args, {
|
|
84
|
+
encoding: 'utf8', timeout: 60_000, maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'], env: process.env,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const REFS_QUERY = `query($owner:String!,$name:String!,$after:String){repository(owner:$owner,name:$name){refs(refPrefix:"refs/heads/salvage/",first:100,after:$after){pageInfo{hasNextPage endCursor} nodes{name target{... on Commit{committedDate}}}}}}`;
|
|
88
|
+
|
|
89
|
+
/** GitHub GraphQL 로 `salvage/*` 가지 ⊕ 마지막 커밋 시각(100개씩). ⛔ 페이지가 깨지면 던진다(부분 목록을 «전체»로 읽지 않는다). */
|
|
90
|
+
export function listDatedSalvageRefs(repository: string, execute: GhExecute = executeGh): DatedSalvageRef[] {
|
|
91
|
+
const [owner, name] = repository.split('/');
|
|
92
|
+
if (!owner || !name) throw new Error(`repository must be owner/name: ${repository}`);
|
|
93
|
+
const refs: DatedSalvageRef[] = [];
|
|
94
|
+
let after: string | null = null;
|
|
95
|
+
for (let page = 0; page < 200; page++) {
|
|
96
|
+
const args = ['api', 'graphql', '-f', `query=${REFS_QUERY}`, '-F', `owner=${owner}`, '-F', `name=${name}`, ...(after ? ['-F', `after=${after}`] : [])];
|
|
97
|
+
const parsed = JSON.parse(execute(args)) as { data?: { repository?: { refs?: { pageInfo?: { hasNextPage?: boolean; endCursor?: string | null }; nodes?: Array<{ name?: unknown; target?: { committedDate?: unknown } | null }> } } } };
|
|
98
|
+
const block = parsed.data?.repository?.refs;
|
|
99
|
+
if (!block || !Array.isArray(block.nodes) || !block.pageInfo) throw new Error('incomplete salvage ref listing');
|
|
100
|
+
for (const node of block.nodes) {
|
|
101
|
+
if (typeof node.name !== 'string') throw new Error('incomplete salvage ref listing');
|
|
102
|
+
refs.push({ branch: `salvage/${node.name}`, committedAt: typeof node.target?.committedDate === 'string' ? node.target.committedDate : null });
|
|
103
|
+
}
|
|
104
|
+
if (!block.pageInfo.hasNextPage) return refs;
|
|
105
|
+
after = block.pageInfo.endCursor ?? null;
|
|
106
|
+
if (!after) throw new Error('incomplete salvage ref listing');
|
|
107
|
+
}
|
|
108
|
+
throw new Error('salvage ref listing exceeded pagination limit');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const OPEN_PR_LIMIT = 5000;
|
|
112
|
+
|
|
113
|
+
/** 열린 PR(head ⊕ 본문). ⛔ 상한에 닿으면 «전부 읽었다»를 증명 못 하므로 던진다(참조 있는 가지를 후보로 세지 않는다). */
|
|
114
|
+
export function listOpenPrRefs(repository: string, execute: GhExecute = executeGh): SalvageReferenceSources['openPrs'] {
|
|
115
|
+
const rows = JSON.parse(execute(['pr', 'list', '--repo', repository, '--state', 'open', '--limit', String(OPEN_PR_LIMIT), '--json', 'number,headRefName,body'])) as unknown;
|
|
116
|
+
if (!Array.isArray(rows)) throw new Error('incomplete open PR listing');
|
|
117
|
+
if (rows.length >= OPEN_PR_LIMIT) throw new Error(`incomplete open PR listing: reached the ${OPEN_PR_LIMIT} limit`);
|
|
118
|
+
return rows.map((row) => {
|
|
119
|
+
const pr = row as { number?: unknown; headRefName?: unknown; body?: unknown };
|
|
120
|
+
if (typeof pr.number !== 'number' || typeof pr.headRefName !== 'string') throw new Error('incomplete open PR listing');
|
|
121
|
+
return { number: pr.number, headRefName: pr.headRefName, body: typeof pr.body === 'string' ? pr.body : null };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -1423,6 +1423,10 @@ export function defaultSeams(o: DefaultSeamsOptions = {}): SelfImplementSeams {
|
|
|
1423
1423
|
const { supersedeMergedGoalDrafts } = await import('../harness/harness-cli-command.js');
|
|
1424
1424
|
await supersedeMergedGoalDrafts(number, cwd);
|
|
1425
1425
|
},
|
|
1426
|
+
resyncSiblingsOnMerge: async (number, cwd) => {
|
|
1427
|
+
const { resyncMergedSiblings } = await import('../harness/harness-cli-command.js');
|
|
1428
|
+
await resyncMergedSiblings(number, cwd);
|
|
1429
|
+
},
|
|
1426
1430
|
followUp: {
|
|
1427
1431
|
mode: readFollowUpMode(),
|
|
1428
1432
|
stateRoot: elanousStateRoot(),
|
|
@@ -111,6 +111,8 @@ export function singleRunAsJobResult(feature: string, r: SelfImplementResultWith
|
|
|
111
111
|
...(r.harvestable === true && r.branch ? { harvestable: true } : {}),
|
|
112
112
|
...(r.worktreePath ? { worktreePath: r.worktreePath } : {}),
|
|
113
113
|
...(r.prUrl ? { prUrl: r.prUrl } : {}),
|
|
114
|
+
// TA-LIVE-REVIEW-POD — 작업 트리 없는 런(Pod)의 live review 가 «런이 낸 PR 머리»를 확인하는 근거.
|
|
115
|
+
...(r.checkedHeadCommit !== undefined ? { checkedHeadCommit: r.checkedHeadCommit } : {}),
|
|
114
116
|
...(r.merged !== undefined ? { merged: r.merged } : {}),
|
|
115
117
|
...(r.mergeReason ? { mergeReason: r.mergeReason } : {}),
|
|
116
118
|
...(r.completionDisposition ? { completionDisposition: r.completionDisposition } : {}),
|