tumwater 0.2.0 → 0.3.0
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/README.md +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +42 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +4 -3
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +402 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +246 -55
- package/dist/src/backlog/backlog-cli-view.js +280 -0
- package/dist/src/backlog/backlog-cli.js +85 -274
- package/dist/src/backlog/backlog-commit.js +4 -5
- package/dist/src/backlog/backlog-eligibility.js +175 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +254 -0
- package/dist/src/backlog/backlog-release.js +117 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-culprit.js +94 -0
- package/dist/src/baseline/main-guardrails.js +209 -0
- package/dist/src/baseline/main-red.js +139 -32
- package/dist/src/baseline/main-revert.js +133 -0
- package/dist/src/baseline/main-watch.js +184 -0
- package/dist/src/brief/brief-edit.js +9 -11
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget.js +30 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +118 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +150 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +308 -590
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-role-command.js +420 -0
- package/dist/src/cli/cli-start-summary.js +160 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
- package/dist/src/cli/completion.js +149 -22
- package/dist/src/cli/config-commands.js +41 -18
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +189 -73
- package/dist/src/cli/log-commands.js +148 -82
- package/dist/src/cli/question-commands.js +205 -10
- package/dist/src/cli.js +106 -43
- package/dist/src/collections.js +26 -10
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +13 -1
- package/dist/src/config/config-fallbacks.js +93 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +141 -199
- package/dist/src/config/config-views.js +127 -53
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +131 -264
- package/dist/src/config/config.js +48 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
- package/dist/src/doctor/doctor-backlog-plans.js +263 -0
- package/dist/src/doctor/doctor-backlog.js +100 -496
- package/dist/src/doctor/doctor-check-names.js +81 -0
- package/dist/src/doctor/doctor-checks.js +52 -579
- package/dist/src/doctor/doctor-claude-checks.js +145 -0
- package/dist/src/doctor/doctor-codex-checks.js +279 -0
- package/dist/src/doctor/doctor-contention-checks.js +380 -0
- package/dist/src/doctor/doctor-holds.js +82 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +236 -402
- package/dist/src/doctor/doctor-notify.js +3 -4
- package/dist/src/doctor/doctor-opencode-checks.js +162 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-project-checks.js +303 -0
- package/dist/src/doctor/doctor.js +121 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +7 -11
- package/dist/src/events/event-format.js +181 -86
- package/dist/src/events/event-labels.js +12 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +39 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +60 -24
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +56 -56
- package/dist/src/failure/failure-render.js +31 -23
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +99 -35
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +177 -84
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +108 -58
- package/dist/src/gates/maintenance-quota.js +10 -13
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +77 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +36 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +42 -18
- package/dist/src/gui/gui-backlog-endpoints.js +155 -0
- package/dist/src/gui/gui-command.js +8 -13
- package/dist/src/gui/gui-config-commands.js +94 -72
- package/dist/src/gui/gui-endpoint-commands.js +4 -3
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +108 -13
- package/dist/src/gui/gui-prompt-commands.js +11 -14
- package/dist/src/gui/gui-report-endpoints.js +90 -0
- package/dist/src/gui/gui-server.js +11 -4
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +147 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +193 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +25 -9
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +81 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +150 -11
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +90 -0
- package/dist/src/merge/merge-queue-render.js +49 -11
- package/dist/src/merge/merge-queue.js +31 -15
- package/dist/src/merge/merge-requeue.js +102 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +33 -11
- package/dist/src/merge/merge-to-main.js +86 -27
- package/dist/src/merge/merge-top.js +69 -0
- package/dist/src/merge/merge-verify.js +12 -10
- package/dist/src/notes-cli.js +30 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +320 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
- package/dist/src/orchestrator/orchestrator.js +128 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +201 -0
- package/dist/src/orchestrator/scheduling-skip.js +185 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +116 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +18 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +121 -211
- package/dist/src/prompt/notes-backlog.js +181 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +129 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +161 -77
- package/dist/src/report/report-fold.js +129 -52
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +133 -64
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +26 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/known-flakes.js +100 -1
- package/dist/src/review/post-merge-review.js +106 -0
- package/dist/src/review/protected-edits.js +4 -5
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +155 -360
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +69 -12
- package/dist/src/roles/role-guidance.js +55 -20
- package/dist/src/roles/role-labels.js +1 -2
- package/dist/src/roles/role-render.js +67 -50
- package/dist/src/roles/role-view.js +59 -47
- package/dist/src/roles/roles.js +31 -31
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/{tick → run}/qa-coverage.js +9 -27
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +179 -0
- package/dist/src/run/run-detail-render.js +98 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +172 -0
- package/dist/src/run/stage-path-checks.js +730 -0
- package/dist/src/{loop → runner}/leftover.js +80 -25
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +202 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +107 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +102 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +43 -16
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +28 -28
- package/dist/src/status/status-roles.js +39 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +5 -6
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +63 -17
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +57 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +67 -13
- package/dist/src/ui/badges.js +65 -37
- package/dist/src/ui/gui/gui-client-backlog.js +292 -0
- package/dist/src/ui/gui/gui-client-boot.js +32 -26
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
- package/dist/src/ui/gui/gui-client-drawer.js +126 -285
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +434 -67
- package/dist/src/ui/gui/gui-client-labels.js +1 -2
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +55 -51
- package/dist/src/ui/gui/gui-client-pending.js +227 -23
- package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
- package/dist/src/ui/gui/gui-client-report.js +87 -427
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +56 -4
- package/dist/src/ui/gui/gui-client-team.js +25 -465
- package/dist/src/ui/gui/gui-client.js +27 -25
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +9 -7
- package/dist/src/ui/gui/gui-styles.js +34 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +136 -49
- package/dist/src/ui/status-render.js +101 -82
- package/dist/src/ui/team-alerts.js +248 -57
- package/dist/src/ui/tone.js +5 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +51 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +3 -5
- package/dist/src/ui/tui/tui-editor-backends.js +132 -0
- package/dist/src/ui/tui/tui-editor-brief.js +10 -12
- package/dist/src/ui/tui/tui-editor-budget.js +35 -31
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
- package/dist/src/ui/tui/tui-editor-config.js +86 -0
- package/dist/src/ui/tui/tui-editor-entry.js +36 -47
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +37 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
- package/dist/src/ui/tui/tui-editor-interval.js +11 -9
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +3 -5
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
- package/dist/src/ui/tui/tui-editor-retire.js +4 -6
- package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +60 -34
- package/dist/src/ui/tui/tui-editor-slots.js +23 -0
- package/dist/src/ui/tui/tui-editors.js +35 -2
- package/dist/src/ui/tui/tui-frame.js +56 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +24 -9
- package/dist/src/ui/tui/tui-pane-activity.js +14 -11
- package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +58 -8
- package/dist/src/ui/tui/tui-pane-history.js +402 -82
- package/dist/src/ui/tui/tui-pane-pending.js +308 -13
- package/dist/src/ui/tui/tui-pane-queued.js +71 -55
- package/dist/src/ui/tui/tui-pane-report.js +46 -14
- package/dist/src/ui/tui/tui-pane-session.js +25 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +80 -23
- package/dist/src/ui/tui/tui-panes.js +100 -30
- package/dist/src/ui/tui/tui.js +14 -14
- package/dist/src/verdict/fix-claim.js +82 -32
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +42 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** Session retention (the `sessionRetentionDays` setting): the once-per-day gate and the prune
|
|
3
2
|
* itself, split out of orchestrator.ts — the poll-loop runtime carried both a startup prune and
|
|
4
3
|
* a near-identical in-poll prune plus the last-applied-window bookkeeping, and scheduling.ts
|
|
5
|
-
* held the due-gate despite being about
|
|
4
|
+
* held the due-gate despite being about run policy, not file retention. Dependency direction:
|
|
6
5
|
* orchestrator → retention; this module owns the whole "which files age out, and when" concern
|
|
7
6
|
* and the orchestrator only hands it the (possibly live-reloaded) window. */
|
|
8
7
|
import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { DAY_MS } from "../text/datetime.js";
|
|
9
|
+
import { ensureParentDir, pruneOldFiles } from "../files/files.js";
|
|
10
|
+
import { pruneSessionLedgers, pruneOpencodeSessions, pruneCodexSessions } from "../agent/session-ledger.js";
|
|
11
11
|
import { loadConfigSafe } from "../config/config.js";
|
|
12
|
+
import { resolvedBackendConfig } from "../config/config-views.js";
|
|
12
13
|
import { opencodeBackend, opencodeSpillDir } from "../agent/opencode/opencode-backend.js";
|
|
13
|
-
import {
|
|
14
|
+
import { codexBackend } from "../agent/codex/codex-backend.js";
|
|
15
|
+
import { codexHome } from "../agent/codex/codex-catalog.js";
|
|
16
|
+
import { opencodeDbPath, salvageDir, sessionsRootDir, toolOutputDir } from "../paths.js";
|
|
14
17
|
/** Is a once-per-day session prune due? Due when retention is enabled (> 0) and a full day
|
|
15
18
|
* has passed since the last prune (or no prune has run yet). */
|
|
16
19
|
export function dueForPrune(lastPruneAt, now, retentionDays) {
|
|
@@ -18,7 +21,7 @@ export function dueForPrune(lastPruneAt, now, retentionDays) {
|
|
|
18
21
|
return false; // 0 disables pruning — never due.
|
|
19
22
|
if (lastPruneAt === null)
|
|
20
23
|
return true; // Never pruned yet — due immediately.
|
|
21
|
-
return now - lastPruneAt >=
|
|
24
|
+
return now - lastPruneAt >= DAY_MS;
|
|
22
25
|
}
|
|
23
26
|
/** The retention state machine across one orchestrator's lifetime: which window was last
|
|
24
27
|
* applied and when we last pruned. Constructed once at startup — construction runs the startup
|
|
@@ -71,14 +74,39 @@ export class RetentionPruner {
|
|
|
71
74
|
// hands every expired OpenCode session id to `opencode session delete`. Each half warns its
|
|
72
75
|
// own unit.
|
|
73
76
|
const { config } = loadConfigSafe(root);
|
|
74
|
-
|
|
77
|
+
let bin = config !== undefined ? opencodeBackend.resolveBin(config).bin : undefined;
|
|
78
|
+
const dbPath = opencodeDbPath(root);
|
|
79
|
+
try {
|
|
80
|
+
ensureParentDir(dbPath);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// The directory cannot be created, so no OpenCode DB is there to delete from; this pass
|
|
84
|
+
// runs in the poll loop, where a throw ends the team. Skip only the session deletes.
|
|
85
|
+
bin = undefined;
|
|
86
|
+
}
|
|
75
87
|
const deleted = pruneOpencodeSessions(sessions, days, {
|
|
76
88
|
bin,
|
|
77
|
-
env: { ...process.env, OPENCODE_DB:
|
|
89
|
+
env: { ...process.env, OPENCODE_DB: dbPath },
|
|
78
90
|
spillDir: opencodeSpillDir(root),
|
|
79
91
|
});
|
|
80
92
|
if (deleted > 0)
|
|
81
93
|
warnEventBestEffort(root, TUMWATER_SOURCE, `deleted ${deleted} expired opencode session(s)`);
|
|
94
|
+
// Codex's sessions also live outside .tumwater — under the team's CODEX_HOME (part 8/9).
|
|
95
|
+
// Resolve its binary and home exactly as a run's invocation would and purge its expired
|
|
96
|
+
// sessions from the ledger BEFORE the file pass below ages it out. Like the OpenCode twin,
|
|
97
|
+
// an unavailable config skips the deletes rather than guessing at a binary and home: a
|
|
98
|
+
// bare `codex` under the operator's inherited CODEX_HOME could act on the wrong home
|
|
99
|
+
// (review objection 2). CODEX_HOME is always set explicitly — to the resolved team home,
|
|
100
|
+
// or to undefined when the config is absent — so the inherited CODEX_HOME never reaches
|
|
101
|
+
// the purge (its rollout fallback would otherwise unlink the operator's own home).
|
|
102
|
+
const codexBin = config !== undefined ? codexBackend.resolveBin(config).bin : undefined;
|
|
103
|
+
const codexHomeDir = config !== undefined ? codexHome(resolvedBackendConfig(config, "codex")) : undefined;
|
|
104
|
+
const deletedCodex = pruneCodexSessions(sessions, days, {
|
|
105
|
+
bin: codexBin,
|
|
106
|
+
env: { ...process.env, CODEX_HOME: codexHomeDir },
|
|
107
|
+
});
|
|
108
|
+
if (deletedCodex > 0)
|
|
109
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `deleted ${deletedCodex} expired codex session(s)`);
|
|
82
110
|
// Age out session/tool-output files now: an entirely stale dir is gone before the line
|
|
83
111
|
// pass reads it. A running role's ledger mtime stays young, so pruneOldFiles never ages
|
|
84
112
|
// that file out; each warning must name the unit it pruned.
|
|
@@ -88,5 +116,11 @@ export class RetentionPruner {
|
|
|
88
116
|
const prunedLines = pruneSessionLedgers(sessions, days);
|
|
89
117
|
if (prunedLines > 0)
|
|
90
118
|
warnEventBestEffort(root, TUMWATER_SOURCE, `pruned ${prunedLines} stale session ledger line(s)`);
|
|
119
|
+
// Salvaged slot leftovers (src/git/slot-salvage.ts) age out on the same window as the
|
|
120
|
+
// sessions that made them: a patch still waiting by then belongs to a role that has not run
|
|
121
|
+
// since, and the operator had the window's length to take a kept one.
|
|
122
|
+
const prunedPatches = pruneOldFiles(salvageDir(root), days);
|
|
123
|
+
if (prunedPatches > 0)
|
|
124
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `pruned ${prunedPatches} old salvaged workspace patch(es)`);
|
|
91
125
|
}
|
|
92
126
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** The poll loop's per-runner admission tail (part 3/3, split out of pollRunnerReasons): the
|
|
2
|
+
* stand-down turn-down and the parallel-work-instance claim assignment, run after every skip
|
|
3
|
+
* gate has admitted a runner. Returns "admitted" when the runner may start a run, or "skipped"
|
|
4
|
+
* (having recorded its skip reason and settled its once-round answer) when it stands down or
|
|
5
|
+
* yields its place. Split out of orchestrator-scheduling.ts so the post-gate admission policy
|
|
6
|
+
* reads on its own screen; orchestrator-scheduling.ts's loop owns the binder: the final
|
|
7
|
+
* `noteSkip(root, runner, undefined)`, the `reasons` entry and the `writing` increment when an
|
|
8
|
+
* admitted runner actually starts. */
|
|
9
|
+
import { assignNext } from "../scheduling/claims.js";
|
|
10
|
+
import { eligibleEntries } from "../backlog/backlog-eligibility.js";
|
|
11
|
+
import { logEventBestEffort } from "../events/events.js";
|
|
12
|
+
import { freshlyWoken, hasOutstandingWork } from "../runner/runner-state.js";
|
|
13
|
+
import { BUGFIX_ROLE } from "../roles/roles.js";
|
|
14
|
+
import { baseRoleOf, instanceIndex } from "../roles/runner-ids.js";
|
|
15
|
+
import { noteSkip } from "./scheduling-scope.js";
|
|
16
|
+
/** Decide a runner's post-gate admission. `reason` is the runner's due reason — the
|
|
17
|
+
* `skipRunner` verdict's reason, itself `isEligible`'s — which the stand-down block keyed on
|
|
18
|
+
* scheduled/main-moved and the claim-group block keyed on inbox/resume read. Mutates the
|
|
19
|
+
* runner's claim/heldElsewhere state, the claim group's held set, and once-mode settlement on
|
|
20
|
+
* the skip paths. */
|
|
21
|
+
export function admitRunner(scope, runner, reason) {
|
|
22
|
+
// Parallel work instances (part 4/7): a multi-runner base role's claim assignment, after
|
|
23
|
+
// every gate has admitted this runner. A runner that already holds a claim proceeds — its
|
|
24
|
+
// revision, resume or retry is the same entry. An unclaimed runner with outstanding work
|
|
25
|
+
// (a revision) is the mid-entry runner the group hold waits on: it proceeds unassigned —
|
|
26
|
+
// holding it too would deadlock the whole group, since only its run can settle the hold.
|
|
27
|
+
// Otherwise, when the run is not an inbox/resume run, it takes the first free entry; an
|
|
28
|
+
// extra instance with none free is skipped without touching its backoff, and the primary
|
|
29
|
+
// stands down too while siblings hold every eligible entry, unless an operator wake is fresh
|
|
30
|
+
// (then it runs unassigned).
|
|
31
|
+
// Stand-down: the feature charter says "If PLANS.md is empty or everything is done, there is
|
|
32
|
+
// nothing to do" and the bugfix charter says "when every open entry carries [a Refused note],
|
|
33
|
+
// there is nothing to do", so a feature runner (primary included) with no eligible plan at all,
|
|
34
|
+
// or a bugfix runner whose open bugs are all held (no open bugs at all is search mode, which
|
|
35
|
+
// still runs), skips its scheduled and main-moved runs instead of paying a model run that
|
|
36
|
+
// can only end in
|
|
37
|
+
// nothing-to-do. Startup, inbox and resume runs, a fresh operator wake, and a runner with a
|
|
38
|
+
// claim or outstanding work still run. nextRunAt and backoff stay put, so the first poll
|
|
39
|
+
// after a plan turns eligible starts a run for it.
|
|
40
|
+
const standDownBase = baseRoleOf(runner.role);
|
|
41
|
+
if ((standDownBase === "feature" || (standDownBase === BUGFIX_ROLE && scope.openBugsNow)) &&
|
|
42
|
+
(reason === "scheduled" || reason === "main moved") &&
|
|
43
|
+
!freshlyWoken(runner.state) &&
|
|
44
|
+
runner.state.claim === undefined &&
|
|
45
|
+
!hasOutstandingWork(runner.state)) {
|
|
46
|
+
let eligible = scope.eligibleMemo.get(standDownBase);
|
|
47
|
+
if (eligible === undefined) {
|
|
48
|
+
eligible =
|
|
49
|
+
scope.claimGroups.get(standDownBase)?.eligible ??
|
|
50
|
+
eligibleEntries(scope.root, standDownBase);
|
|
51
|
+
scope.eligibleMemo.set(standDownBase, eligible);
|
|
52
|
+
}
|
|
53
|
+
if (eligible.length === 0) {
|
|
54
|
+
noteSkip(scope.root, runner, "no-free-entry");
|
|
55
|
+
runner.state.heldElsewhere = undefined;
|
|
56
|
+
if (scope.once.active)
|
|
57
|
+
scope.once.settle(runner.role, "no free entry");
|
|
58
|
+
return "skipped";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const claimGroup = scope.claimGroups.get(baseRoleOf(runner.role));
|
|
62
|
+
if (claimGroup) {
|
|
63
|
+
if (runner.state.claim) {
|
|
64
|
+
// keeps its claim
|
|
65
|
+
}
|
|
66
|
+
else if (hasOutstandingWork(runner.state)) {
|
|
67
|
+
// finishes its own unrecorded entry
|
|
68
|
+
}
|
|
69
|
+
else if (reason !== "inbox" && reason !== "resume") {
|
|
70
|
+
// The group holds an unclaimed mid-entry sibling: assign nothing this poll, or a
|
|
71
|
+
// sibling could be handed that same entry (the reported `feature-2`/`bugfix-2`
|
|
72
|
+
// duplication). It resolves the moment the sibling settles.
|
|
73
|
+
if (claimGroup.hold) {
|
|
74
|
+
noteSkip(scope.root, runner, "claim-hold");
|
|
75
|
+
return "skipped";
|
|
76
|
+
}
|
|
77
|
+
const free = claimGroup.eligible.filter((e) => !claimGroup.held.has(e.key));
|
|
78
|
+
const pick = assignNext(free);
|
|
79
|
+
// An unassigned runner is told which entries its siblings hold (cleared when none).
|
|
80
|
+
const heldTitles = claimGroup.eligible
|
|
81
|
+
.filter((e) => claimGroup.held.has(e.key))
|
|
82
|
+
.map((e) => e.title);
|
|
83
|
+
runner.state.heldElsewhere = !pick && heldTitles.length > 0 ? heldTitles : undefined;
|
|
84
|
+
if (pick) {
|
|
85
|
+
runner.state.claim = {
|
|
86
|
+
file: baseRoleOf(runner.role) === BUGFIX_ROLE ? "BUGS.md" : "PLANS.md",
|
|
87
|
+
key: pick.key,
|
|
88
|
+
title: pick.title,
|
|
89
|
+
at: scope.now,
|
|
90
|
+
source: "assigned",
|
|
91
|
+
};
|
|
92
|
+
claimGroup.held.add(pick.key);
|
|
93
|
+
logEventBestEffort(scope.root, {
|
|
94
|
+
source: runner.role,
|
|
95
|
+
type: "claim",
|
|
96
|
+
action: "assigned",
|
|
97
|
+
key: pick.key,
|
|
98
|
+
title: pick.title,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else if (instanceIndex(runner.role) >= 2 ||
|
|
102
|
+
(heldTitles.length > 0 && !freshlyWoken(runner.state))) {
|
|
103
|
+
// An idle extra instance, or a primary whose eligible entries siblings all hold
|
|
104
|
+
// (unless an operator wake is fresh): no run, no backoff climb.
|
|
105
|
+
noteSkip(scope.root, runner, "no-free-entry");
|
|
106
|
+
if (scope.once.active)
|
|
107
|
+
scope.once.settle(runner.role, "no free entry");
|
|
108
|
+
return "skipped";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
runner.state.heldElsewhere = undefined; // no claim group (e.g. instances lowered to 1)
|
|
114
|
+
}
|
|
115
|
+
return "admitted";
|
|
116
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/** One poll's scheduling-scope precompute, built by orchestrator-scheduling.ts's
|
|
2
|
+
* pollRunnerReasons: the shared pass context and the values computed once per poll before the
|
|
3
|
+
* per-runner loop — the merge-queue snapshot, the director pin, per-base claim groups, the
|
|
4
|
+
* gate predicates, and the mutable admission state (quota counter, ordered runners, eligible
|
|
5
|
+
* memo, reasons map). Parts 2/3 and 3/3 lift the loop's skip and admit halves against this
|
|
6
|
+
* scope. */
|
|
7
|
+
import { increment } from "../collections.js";
|
|
8
|
+
import { DIRECTOR_ROLE, commitTier } from "../roles/roles.js";
|
|
9
|
+
import { baseRoleOf, runnerEnabled, runnerPaused } from "../roles/runner-ids.js";
|
|
10
|
+
import { hasOutstandingWork, saveRunnerStateBestEffort } from "../runner/runner-state.js";
|
|
11
|
+
import { queuedMergeRoles } from "../merge/merge-queue.js";
|
|
12
|
+
import { hasUnmergedPin } from "../runner/leftover.js";
|
|
13
|
+
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
14
|
+
import { eligibleEntries } from "../backlog/backlog-eligibility.js";
|
|
15
|
+
import { CLAIM_IDLE_MAX_MS, claimReleaseReason, heldKeys, listedKeys, } from "../scheduling/claims.js";
|
|
16
|
+
/** Record why the poll skipped a due role on its state (RunnerState.skipReason), saving only when
|
|
17
|
+
* the value changes: a skipped role gets no run to save its own state, and a role that stays
|
|
18
|
+
* skipped must not rewrite its file every poll. */
|
|
19
|
+
export function noteSkip(root, runner, reason) {
|
|
20
|
+
if (runner.state.skipReason === reason)
|
|
21
|
+
return;
|
|
22
|
+
runner.state.skipReason = reason;
|
|
23
|
+
saveRunnerStateBestEffort(root, runner.state);
|
|
24
|
+
}
|
|
25
|
+
/** Compute the once-per-poll precompute the scheduling pass's per-runner loop reads. Has the
|
|
26
|
+
* same side effects the poll body's own precompute had: the merge-queue snapshot, the
|
|
27
|
+
* hasUnmergedPin director pin, the per-base claim-group build (with its claim-release loop,
|
|
28
|
+
* which mutates runner state, persists and logs), and the writing count. */
|
|
29
|
+
export async function buildSchedulingScope(ctx) {
|
|
30
|
+
const { root, runners, once, now, mainHead, userPaused, quietNow, pausedRoles, capPaused, maintenanceQuota, bootstrapHeld, budgetPausedRoles, probeRoles, reviewStrongPaused, roleQuietHeld, heldProviders, windowHeldProviders, reviewHeld, roleProviders, openBugsNow, workBacklogOpen, planBacklogFull, holdForRestart, diskHeld, deferredDue, workMergedSince, } = ctx;
|
|
31
|
+
// Merge-queue interlock data, listed ONCE per poll (was once per runner per poll — each
|
|
32
|
+
// queuedMergeFiles() re-lists the merge-queue directory and re-stats every queued entry): the
|
|
33
|
+
// check below only asks whether the runner's role has a queued entry. The queue changes
|
|
34
|
+
// only when a merge completes or a run enqueues — both asynchronous events this pass
|
|
35
|
+
// observes on its next poll — so one snapshot is as fresh as per-runner reads, and a
|
|
36
|
+
// merge that completes mid-pass just keeps its role blocked one extra poll (conservative).
|
|
37
|
+
const queuedRoles = queuedMergeRoles(root);
|
|
38
|
+
// The director's kept merge pin (a non-terminal merge failure kept it): due on its own min-run
|
|
39
|
+
// clock, since its only other run is an operator prompt. A queued or in-flight pin is the
|
|
40
|
+
// merge-queue interlock's to hold, so the leftover reason is not offered then.
|
|
41
|
+
const pinnedDirector = !queuedRoles.has(DIRECTOR_ROLE) && (await hasUnmergedPin(root, DIRECTOR_ROLE));
|
|
42
|
+
// Parallel work instances (plans/parallel-work-instances.md "Claims", part 4/7): a base role
|
|
43
|
+
// with more than one runner assigns each due instance a distinct eligible entry, and releases
|
|
44
|
+
// the claims whose entry left, turned ineligible, was disabled or went stale. Computed once
|
|
45
|
+
// per poll and only for a multi-runner base, so a single-runner team is unchanged. The
|
|
46
|
+
// release runs before assignment so a freed key counts as free below.
|
|
47
|
+
const claimGroups = new Map();
|
|
48
|
+
const baseCounts = new Map();
|
|
49
|
+
for (const runner of runners) {
|
|
50
|
+
const base = baseRoleOf(runner.role);
|
|
51
|
+
increment(baseCounts, base);
|
|
52
|
+
}
|
|
53
|
+
for (const [base, count] of baseCounts) {
|
|
54
|
+
if (count <= 1)
|
|
55
|
+
continue;
|
|
56
|
+
const groupRunners = runners.filter((r) => baseRoleOf(r.role) === base);
|
|
57
|
+
const eligible = eligibleEntries(root, base);
|
|
58
|
+
const eligibleKeys = new Set(eligible.map((e) => e.key));
|
|
59
|
+
const listed = listedKeys(root, base);
|
|
60
|
+
// A group whose runner is mid-entry with no recorded claim (it took the entry while it was
|
|
61
|
+
// the only runner, before a live reload raised `instances`) cannot be told WHICH entry that
|
|
62
|
+
// is from state alone. Assigning a free entry to a sibling could then hand it the same
|
|
63
|
+
// work: hold the whole group's assignment until that runner settles (merges, drops or goes
|
|
64
|
+
// idle), at which point its own staging may have recorded a claim. A busy runner that DOES
|
|
65
|
+
// hold a claim is fine — `held` excludes its key and siblings take other entries.
|
|
66
|
+
let hold = false;
|
|
67
|
+
for (const r of groupRunners) {
|
|
68
|
+
const busy = r.state.running ||
|
|
69
|
+
queuedRoles.has(r.role) ||
|
|
70
|
+
hasOutstandingWork(r.state);
|
|
71
|
+
if (r.state.claim === undefined && busy)
|
|
72
|
+
hold = true;
|
|
73
|
+
const released = claimReleaseReason(r, {
|
|
74
|
+
listedKeys: listed,
|
|
75
|
+
eligibleKeys,
|
|
76
|
+
now,
|
|
77
|
+
hasQueuedMerge: queuedRoles.has(r.role),
|
|
78
|
+
// Per instance, not per base role: a lowered `instances` leaves the surplus runner in
|
|
79
|
+
// place with `runnerEnabled` false, and its idle claim must be released so a sibling can
|
|
80
|
+
// take the entry instead of the key staying held until the 24h stale sweep.
|
|
81
|
+
enabled: runnerEnabled(r.config, r.role),
|
|
82
|
+
// Only this instance paused (not its whole role; instance 1 under `<role>-1`): a sibling
|
|
83
|
+
// can take the entry. With the base role paused no sibling runs, so the claim stays.
|
|
84
|
+
paused: runnerPaused(pausedRoles, r.role) && !pausedRoles.has(base),
|
|
85
|
+
});
|
|
86
|
+
if (released === null)
|
|
87
|
+
continue;
|
|
88
|
+
const title = r.state.claim?.title ?? "";
|
|
89
|
+
r.state.claim = undefined;
|
|
90
|
+
// Persist the release: an idle extra instance gets no run to save its state, so the dead
|
|
91
|
+
// claim would stay on disk for status/doctor to show and for a restart to release again.
|
|
92
|
+
saveRunnerStateBestEffort(root, r.state);
|
|
93
|
+
const reason = released;
|
|
94
|
+
logEventBestEffort(root, { source: r.role, type: "claim", action: "released", title, reason });
|
|
95
|
+
if (reason === "stale") {
|
|
96
|
+
warnEventBestEffort(root, r.role, `released "${title}", which a stopped instance was working on (idle ${Math.round(CLAIM_IDLE_MAX_MS / 3_600_000)}h)`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
claimGroups.set(base, {
|
|
100
|
+
eligible,
|
|
101
|
+
eligibleKeys,
|
|
102
|
+
held: heldKeys(groupRunners, eligibleKeys, listed),
|
|
103
|
+
hold,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// The team gates' block predicate (the director exempt — an explicit human prompt
|
|
107
|
+
// outranks any autonomous gate, quiet hours included): the once-mode settle below and
|
|
108
|
+
// the start gate must agree on exactly when a team gate holds, so the compound lives
|
|
109
|
+
// here once. Quiet hours fold in beside the operator pause and the budget gate — the
|
|
110
|
+
// schedule is not probe-worthy the way a paused budget is, so no probeDue exception.
|
|
111
|
+
// The per-role windows (quietHoursPerRole) fold in beside the team one as a fourth
|
|
112
|
+
// disjunct — a role held by either window (or both, held once) starts no new runs;
|
|
113
|
+
// the director stays exempt by the same role check that covers the other disjuncts.
|
|
114
|
+
// The budget hold, per role (part 5c/8): a tier that resolved to pause holds its own roles,
|
|
115
|
+
// and — with review on — an unresolvable strong tier holds every role (nothing could merge).
|
|
116
|
+
// The probe of a demoted pair pierces the hold for EXACTLY the probed pair's tier's roles:
|
|
117
|
+
// the earlier draft's pair-blind pierce lifted the pause team-wide on any due probe.
|
|
118
|
+
// Quiet hours fold in beside the operator pause — the schedule is not probe-worthy the way a
|
|
119
|
+
// paused budget is.
|
|
120
|
+
const budgetHolds = (role) => (reviewStrongPaused || budgetPausedRoles.has(role)) && !probeRoles.has(role);
|
|
121
|
+
const operatorPauseBlocks = (role) => (userPaused || quietNow || roleQuietHeld.has(role) || budgetHolds(role)) &&
|
|
122
|
+
role !== DIRECTOR_ROLE;
|
|
123
|
+
// Fresh operator demand: a queued prompt or a wake newer than the role's last run end. The
|
|
124
|
+
// same override the need-based deferral honors (an explicit "try again now" is not idle
|
|
125
|
+
// maintenance); shared by the bootstrap and maintenance-allowance holds so the two cannot
|
|
126
|
+
// drift on what counts as demand. Computed ONCE per runner per poll (the scheduling loop's
|
|
127
|
+
// per-runner `demand`), so a role's queue is listed a single time a poll instead of once per
|
|
128
|
+
// hold check; every consumer takes the boolean rather than re-calling inboxSize.
|
|
129
|
+
// The new-project bootstrap hold (part 2/2): held unless the role has a fresh operator wake
|
|
130
|
+
// or a queued prompt.
|
|
131
|
+
const bootstrapHeldNow = (runner, demand) => bootstrapHeld.has(runner.role) && !demand;
|
|
132
|
+
// The maintenance allowance's team-level hold (part 1b/4): `used` has reached `allowance`,
|
|
133
|
+
// so no maintenance role starts unless it carries demand (`tumwater wake <maintenance-role>`
|
|
134
|
+
// gets exactly one run). The per-poll admission headroom is spent separately, after each
|
|
135
|
+
// role's due check, where the pass knows what actually runs; the verdict alone cannot.
|
|
136
|
+
const maintenanceQuotaOverLine = (runner, demand) => commitTier(baseRoleOf(runner.role)) === "maintenance" &&
|
|
137
|
+
maintenanceQuota.used >= maintenanceQuota.allowance &&
|
|
138
|
+
!demand;
|
|
139
|
+
// Runs writing code per multi-runner base role (plans/fast-merge.md "Lever 1"): a runner in
|
|
140
|
+
// flight, not one whose change waits in the merge queue. `instances` limits this count, so the
|
|
141
|
+
// role's extra runner ids start work only while fewer than `instances` are running; admissions
|
|
142
|
+
// this poll count at once, so one pass cannot overshoot.
|
|
143
|
+
const writing = new Map();
|
|
144
|
+
for (const r of runners) {
|
|
145
|
+
if (claimGroups.has(baseRoleOf(r.role)) && r.state.running && !queuedRoles.has(r.role)) {
|
|
146
|
+
increment(writing, baseRoleOf(r.role));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Admission order: a runner of a multi-runner base with a pending revision or conflict
|
|
150
|
+
// hand-back comes first (its change came back; it holds no new claim), then the rest in runner
|
|
151
|
+
// order, so the lowest-index free runner takes new work. Other roles keep their place.
|
|
152
|
+
const ownsOutstanding = (r) => claimGroups.has(baseRoleOf(r.role)) && hasOutstandingWork(r.state);
|
|
153
|
+
const ordered = [
|
|
154
|
+
...runners.filter((r) => ownsOutstanding(r)),
|
|
155
|
+
...runners.filter((r) => !ownsOutstanding(r)),
|
|
156
|
+
];
|
|
157
|
+
// The stand-down's eligible lists per base role, read at most once per poll (see the check
|
|
158
|
+
// below).
|
|
159
|
+
const eligibleMemo = new Map();
|
|
160
|
+
return {
|
|
161
|
+
root,
|
|
162
|
+
runners,
|
|
163
|
+
once,
|
|
164
|
+
now,
|
|
165
|
+
mainHead,
|
|
166
|
+
userPaused,
|
|
167
|
+
quietNow,
|
|
168
|
+
pausedRoles,
|
|
169
|
+
capPaused,
|
|
170
|
+
maintenanceQuota,
|
|
171
|
+
bootstrapHeld,
|
|
172
|
+
budgetPausedRoles,
|
|
173
|
+
probeRoles,
|
|
174
|
+
reviewStrongPaused,
|
|
175
|
+
roleQuietHeld,
|
|
176
|
+
heldProviders,
|
|
177
|
+
windowHeldProviders,
|
|
178
|
+
reviewHeld,
|
|
179
|
+
roleProviders,
|
|
180
|
+
openBugsNow,
|
|
181
|
+
workBacklogOpen,
|
|
182
|
+
planBacklogFull,
|
|
183
|
+
holdForRestart,
|
|
184
|
+
diskHeld,
|
|
185
|
+
deferredDue,
|
|
186
|
+
workMergedSince,
|
|
187
|
+
queuedRoles,
|
|
188
|
+
pinnedDirector,
|
|
189
|
+
claimGroups,
|
|
190
|
+
writing,
|
|
191
|
+
ownsOutstanding,
|
|
192
|
+
ordered,
|
|
193
|
+
eligibleMemo,
|
|
194
|
+
reasons: new Map(),
|
|
195
|
+
quotaAdmitted: 0,
|
|
196
|
+
budgetHolds,
|
|
197
|
+
operatorPauseBlocks,
|
|
198
|
+
bootstrapHeldNow,
|
|
199
|
+
maintenanceQuotaOverLine,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { logEventBestEffort } from "../events/events.js";
|
|
2
|
+
import { baseRoleOf, configuredInstances, runnerPaused } from "../roles/runner-ids.js";
|
|
3
|
+
import { BUGFIX_ROLE, DIRECTOR_ROLE, commitTier } from "../roles/roles.js";
|
|
4
|
+
import { deferRunReason, isEligible } from "../scheduling/scheduling.js";
|
|
5
|
+
import { inboxSize } from "../inbox/inbox.js";
|
|
6
|
+
import { freshlyWoken } from "../runner/runner-state.js";
|
|
7
|
+
import { MERGE_CONFLICT_LIMIT } from "../runner/leftover.js";
|
|
8
|
+
import { noteSkip } from "./scheduling-scope.js";
|
|
9
|
+
/** Decide whether this runner is due this poll, running every skip gate and the once/deferral
|
|
10
|
+
* bookkeeping against scheduling-scope.ts's scope. Returns skip:true when a gate declined the
|
|
11
|
+
* run, and skip:false with the due reason otherwise (the admit tail in scheduling-admit.ts
|
|
12
|
+
* consumes the reason). Mutates the shared scope —
|
|
13
|
+
* deferredDue, quotaAdmitted, once-mode settlement — and writes skip reasons as its only
|
|
14
|
+
* side effects. */
|
|
15
|
+
export async function skipRunner(scope, runner) {
|
|
16
|
+
const { root, once, now, mainHead, pausedRoles, capPaused, holdForRestart, diskHeld, windowHeldProviders, roleProviders, reviewHeld, heldProviders, openBugsNow, workBacklogOpen, planBacklogFull, deferredDue, workMergedSince, queuedRoles, pinnedDirector, claimGroups, writing, maintenanceQuota, maintenanceQuotaOverLine, bootstrapHeldNow, operatorPauseBlocks, } = scope;
|
|
17
|
+
// The role's inbox size and operator demand, computed once per runner per poll: every
|
|
18
|
+
// consumer below (the holds, isEligible's inbox count, the maintenance admission) shares
|
|
19
|
+
// this one directory listing + per-file stat read instead of each re-scanning the queue.
|
|
20
|
+
// The queue is append-only while this pass runs (nothing dequeues until launch, after the
|
|
21
|
+
// pass), so one read per runner is stable, and `now` here matches the poll's own clock.
|
|
22
|
+
const inboxCount = inboxSize(root, runner.role);
|
|
23
|
+
const demand = inboxCount > 0 || freshlyWoken(runner.state);
|
|
24
|
+
// Once mode: a paused role gets no run this round and must be reported as skipped,
|
|
25
|
+
// so it settles here — before the gates that would otherwise skip it silently (a
|
|
26
|
+
// once round has to end even when a pause marker is left over; a later resume within
|
|
27
|
+
// the same round cannot un-settle it, which is the at-most-one-run contract).
|
|
28
|
+
if (once.active &&
|
|
29
|
+
!once.isSettled(runner) &&
|
|
30
|
+
!runner.state.running &&
|
|
31
|
+
(runnerPaused(pausedRoles, runner.role) ||
|
|
32
|
+
capPaused.has(runner.role) ||
|
|
33
|
+
maintenanceQuotaOverLine(runner, demand) ||
|
|
34
|
+
bootstrapHeldNow(runner, demand) ||
|
|
35
|
+
operatorPauseBlocks(runner.role))) {
|
|
36
|
+
once.settle(runner.role, "paused");
|
|
37
|
+
}
|
|
38
|
+
// Once mode's at-most-one-run contract (src/scheduling/once-round.ts): a role that already had
|
|
39
|
+
// its run — or was settled with a skip reason — runs nothing further this round, even
|
|
40
|
+
// when its backoff has expired or the clock override would admit it again. Without this
|
|
41
|
+
// gate the one-run guarantee held only for deferrable built-ins (their deferral
|
|
42
|
+
// settles them); a custom role or work role with a short backoff re-qualified every
|
|
43
|
+
// poll and the round never ended.
|
|
44
|
+
if (once.active && !runner.state.running && once.isSettled(runner))
|
|
45
|
+
return { skip: true };
|
|
46
|
+
if (holdForRestart || diskHeld)
|
|
47
|
+
return { skip: true }; // a restart or a low disk is pending: nothing new starts, on any role
|
|
48
|
+
// The per-role pause gates BEFORE the team check and exempts nothing — the director
|
|
49
|
+
// included (the operator named that one role deliberately).
|
|
50
|
+
if (runnerPaused(pausedRoles, runner.role))
|
|
51
|
+
return { skip: true };
|
|
52
|
+
// The role's own daily cost cap (src/gates/role-cap-gates.ts): no start-gate change — a
|
|
53
|
+
// parked waiter finishes (in-flight runs finish, NEW runs are gated at scheduling,
|
|
54
|
+
// exactly like every per-role pause), and there is no fallback-probe exception: the
|
|
55
|
+
// stop is about spend, and probing it adds noise, not signal. The lift is a live
|
|
56
|
+
// config edit or local midnight — no marker exists for `resume --role` to touch.
|
|
57
|
+
if (capPaused.has(runner.role))
|
|
58
|
+
return { skip: true };
|
|
59
|
+
// The maintenance allowance (plans/work-ratio.md, part 1b/4): no new maintenance run while
|
|
60
|
+
// the rolling 24 h window is over its line. A fresh operator wake or a queued prompt admits
|
|
61
|
+
// one anyway (maintenanceQuotaOverLine), the same demand override the bootstrap hold honors.
|
|
62
|
+
if (maintenanceQuotaOverLine(runner, demand))
|
|
63
|
+
return { skip: true };
|
|
64
|
+
// The new-project bootstrap (part 2/2): no new maintenance runs until the plan target is
|
|
65
|
+
// met; a fresh wake or queued prompt admits one anyway (bootstrapHeldNow).
|
|
66
|
+
if (bootstrapHeldNow(runner, demand))
|
|
67
|
+
return { skip: true };
|
|
68
|
+
if (operatorPauseBlocks(runner.role))
|
|
69
|
+
return { skip: true }; // no new role runs while either gate holds
|
|
70
|
+
// The director honors only the subscription-window hold: the per-run 429 retry that backs
|
|
71
|
+
// its other exemptions does not answer a window rejection, so its prompt would fail and
|
|
72
|
+
// re-queue every poll until the repeated-failure pause stops it.
|
|
73
|
+
if (runner.role === DIRECTOR_ROLE && windowHeldProviders.has(roleProviders.get(runner.role)))
|
|
74
|
+
return { skip: true };
|
|
75
|
+
if (runner.role !== DIRECTOR_ROLE &&
|
|
76
|
+
(reviewHeld || heldProviders.has(roleProviders.get(runner.role))))
|
|
77
|
+
return { skip: true }; // nor while ITS provider's failure storm holds — or the reviewer's (nothing could merge)
|
|
78
|
+
// The role's OWN queue decides inbox due-ness: the director counts its historical
|
|
79
|
+
// inbox, every other role its per-role queue (tumwater prompt --role <id>) — so a
|
|
80
|
+
// queued prompt makes its role due by itself, no wake marker needed (isEligible).
|
|
81
|
+
const { run, reason, skip } = isEligible(runner, now, mainHead, inboxCount, {
|
|
82
|
+
once: once.active,
|
|
83
|
+
planBacklogOpen: !planBacklogFull,
|
|
84
|
+
// A pin at the conflict cap whose lineage is not yet handed back is owed an authoring run
|
|
85
|
+
// (the promptless path would skip it unchanged every poll), so only a prompt makes it due.
|
|
86
|
+
leftoverPending: pinnedDirector &&
|
|
87
|
+
!((runner.state.mergeConflicts?.count ?? 0) >= MERGE_CONFLICT_LIMIT &&
|
|
88
|
+
runner.state.conflictHandback === undefined),
|
|
89
|
+
});
|
|
90
|
+
if (!run) {
|
|
91
|
+
// Not due this poll: any deferral episode has ended (or never started). No event —
|
|
92
|
+
// the run's own events cover it.
|
|
93
|
+
if (deferredDue.get(runner.role))
|
|
94
|
+
deferredDue.set(runner.role, false);
|
|
95
|
+
// Once mode: idle and not due — settleSkipped records the skip reason (the caller's
|
|
96
|
+
// summary reports it).
|
|
97
|
+
if (once.active && !runner.state.running)
|
|
98
|
+
once.settleSkipped(runner);
|
|
99
|
+
// Thread a gate-specific skip through (e.g. the yield-stretched gap) so a runner the
|
|
100
|
+
// poll declined and will keep declining does not read as "due — starts on the next
|
|
101
|
+
// poll"; stays undefined for every ordinary not-due path.
|
|
102
|
+
noteSkip(root, runner, skip);
|
|
103
|
+
return { skip: true };
|
|
104
|
+
}
|
|
105
|
+
// Merge queue 3/5 interlock (invariant 3): a role with a QUEUED or IN-FLIGHT merge never
|
|
106
|
+
// starts a run — the entry stays in the queue until its merge completes, so one check
|
|
107
|
+
// covers both. Uniform over every role, director included: its prompt is not finished until it
|
|
108
|
+
// merges. Placed before the need-based deferral block on purpose — no episode bookkeeping is
|
|
109
|
+
// needed (the run that queued the merge finished, which closed any deferral episode; its
|
|
110
|
+
// `queued` can leave a prior no_change in lastResult — runner-state.ts records only completed
|
|
111
|
+
// results — but the merge's outcome replaces it before the interlock lets the role back into
|
|
112
|
+
// this pass), and skipping here saves that branch's workMergedSince git-range query for the
|
|
113
|
+
// skipped role.
|
|
114
|
+
if (queuedRoles.has(runner.role)) {
|
|
115
|
+
noteSkip(root, runner, undefined);
|
|
116
|
+
return { skip: true };
|
|
117
|
+
}
|
|
118
|
+
// The role's `instances` limit on concurrent runs: a multi-runner base admits a runner only
|
|
119
|
+
// while fewer than `instances` of its runners are running. A parked runner (queued change)
|
|
120
|
+
// was skipped by the interlock above and holds no place.
|
|
121
|
+
const writingBase = baseRoleOf(runner.role);
|
|
122
|
+
if (claimGroups.has(writingBase) && !runner.state.running) {
|
|
123
|
+
if ((writing.get(writingBase) ?? 0) >= configuredInstances(runner.config, runner.role)) {
|
|
124
|
+
noteSkip(root, runner, "instances-full");
|
|
125
|
+
return { skip: true };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Need-based deferral: a due maintenance run (scheduled or main-moved wake) whose last
|
|
129
|
+
// run did nothing stays deferred while the feature/bugfix backlog is open or no new
|
|
130
|
+
// work has merged to react to — nextRunAt is left untouched, so it re-checks every poll
|
|
131
|
+
// until the backlog drains and qualifying work merges. Resume wakes precede this check in
|
|
132
|
+
// isEligible, the director's "inbox" reason skips it, work roles are not in
|
|
133
|
+
// DEFERRABLE_ROLES (bugfix is the exception while its backlog is empty — deferRun), and
|
|
134
|
+
// a fresh operator wake overrides it (deferRun's woken check — an explicit "try again
|
|
135
|
+
// now" is a demand, not idle maintenance). Sitting before reasons.set also keeps a
|
|
136
|
+
// deferred role out of the wake-event pass below.
|
|
137
|
+
if (reason === "scheduled" || reason === "main moved") {
|
|
138
|
+
const s = runner.state;
|
|
139
|
+
// The git range is only consulted when the other conditions already hold — a
|
|
140
|
+
// never-run role, or one with pending business, runs without paying for it, and an
|
|
141
|
+
// open backlog defers regardless of what merged. bugfix in search mode is the
|
|
142
|
+
// exception: its deferral keys on the work-merged verdict alone, so the backlog-open
|
|
143
|
+
// shortcut must not stand in for the query.
|
|
144
|
+
const searchBugfix = baseRoleOf(runner.role) === BUGFIX_ROLE && !openBugsNow;
|
|
145
|
+
const merged = (!workBacklogOpen || searchBugfix) && s.lastMainHead !== ""
|
|
146
|
+
? await workMergedSince.since(s.lastMainHead, mainHead)
|
|
147
|
+
: true;
|
|
148
|
+
const deferral = deferRunReason(s, runner.role, merged, workBacklogOpen, openBugsNow, now, planBacklogFull);
|
|
149
|
+
const deferredNow = deferral !== null;
|
|
150
|
+
if (deferredNow !== (deferredDue.get(runner.role) ?? false)) {
|
|
151
|
+
if (deferral)
|
|
152
|
+
logEventBestEffort(root, { source: runner.role, type: "run_deferred", reason: deferral });
|
|
153
|
+
deferredDue.set(runner.role, deferredNow);
|
|
154
|
+
}
|
|
155
|
+
if (deferredNow) {
|
|
156
|
+
// Once mode: a maintenance run that deferred itself — its once-round
|
|
157
|
+
// answer, so it settles instead of holding the round open for DEFER_MAX_MS.
|
|
158
|
+
if (once.active)
|
|
159
|
+
once.settle(runner.role, "deferred");
|
|
160
|
+
noteSkip(root, runner, deferral === "backlog"
|
|
161
|
+
? "deferred-backlog"
|
|
162
|
+
: deferral === "plans-full"
|
|
163
|
+
? "deferred-plans-full"
|
|
164
|
+
: "deferred-no-work");
|
|
165
|
+
return { skip: true };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (deferredDue.get(runner.role)) {
|
|
169
|
+
deferredDue.set(runner.role, false); // an inbox/resume run ends the episode
|
|
170
|
+
}
|
|
171
|
+
// The allowance's in-flight admission (plans/work-ratio.md, part 1b/4): `used` counts the
|
|
172
|
+
// maintenance roles already in flight, so a due maintenance run admitted here spends one
|
|
173
|
+
// of the remaining `allowance - used` slots. This must sit after the due check — the pure
|
|
174
|
+
// verdict cannot know which roles will be eligible this poll, so the reservation lives in
|
|
175
|
+
// the pass that does; without it two due maintenance roles start in the same poll. Demand
|
|
176
|
+
// spends no slot: it is an explicit operator request, not idle maintenance.
|
|
177
|
+
if (commitTier(baseRoleOf(runner.role)) === "maintenance" && !demand) {
|
|
178
|
+
if (scope.quotaAdmitted >= maintenanceQuota.allowance - maintenanceQuota.used) {
|
|
179
|
+
noteSkip(root, runner, "maintenance-due-cap");
|
|
180
|
+
return { skip: true };
|
|
181
|
+
}
|
|
182
|
+
scope.quotaAdmitted++;
|
|
183
|
+
}
|
|
184
|
+
return { skip: false, reason: reason };
|
|
185
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errCode } from "../errno.js";
|
|
1
2
|
import { legacyMigrateLandQueueDir } from "../merge/merge-queue.js";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import { deleteRefIfAt, refSha, refsUnder, setRef } from "../git/git.js";
|
|
@@ -13,11 +14,11 @@ registerStartupMigration({
|
|
|
13
14
|
run: async (root) => {
|
|
14
15
|
try {
|
|
15
16
|
fs.unlinkSync(legacyLandingStatePath(root));
|
|
16
|
-
return ["removed the old landing.json marker"]; // vocabulary-ok: names the legacy file
|
|
17
|
+
return { moved: ["removed the old landing.json marker"], kept: [], failed: [] }; // vocabulary-ok: names the legacy file
|
|
17
18
|
}
|
|
18
19
|
catch (err) {
|
|
19
|
-
if (err
|
|
20
|
-
return [];
|
|
20
|
+
if (errCode(err) === "ENOENT")
|
|
21
|
+
return { moved: [], kept: [], failed: [] };
|
|
21
22
|
throw err;
|
|
22
23
|
}
|
|
23
24
|
},
|
|
@@ -26,7 +27,7 @@ registerStartupMigration({
|
|
|
26
27
|
name: "mergeRefs",
|
|
27
28
|
run: async (root) => {
|
|
28
29
|
const legacyPrefix = "refs/tumwater/landing/"; // vocabulary-ok: legacy name read from old refs or commits
|
|
29
|
-
const notes = [];
|
|
30
|
+
const notes = { moved: [], kept: [], failed: [] };
|
|
30
31
|
for (const { ref, sha } of await refsUnder(root, legacyPrefix)) {
|
|
31
32
|
const role = ref.slice(legacyPrefix.length);
|
|
32
33
|
const target = mergeRefName(role);
|
|
@@ -35,14 +36,14 @@ registerStartupMigration({
|
|
|
35
36
|
if (!(await setRef(root, target, sha)))
|
|
36
37
|
continue; // keep the legacy pin; the next start retries
|
|
37
38
|
await deleteRefIfAt(root, ref, sha);
|
|
38
|
-
notes.push(`moved ${ref} to ${target}`);
|
|
39
|
+
notes.moved.push(`moved ${ref} to ${target}`);
|
|
39
40
|
}
|
|
40
41
|
else if (existing === sha) {
|
|
41
42
|
await deleteRefIfAt(root, ref, sha);
|
|
42
|
-
notes.push(`removed ${ref}: ${target} already names the same commit`);
|
|
43
|
+
notes.moved.push(`removed ${ref}: ${target} already names the same commit`);
|
|
43
44
|
}
|
|
44
45
|
else
|
|
45
|
-
notes.push(`kept ${ref} and ${target}: they name different commits`);
|
|
46
|
+
notes.kept.push(`kept ${ref} and ${target}: they name different commits`);
|
|
46
47
|
}
|
|
47
48
|
return notes;
|
|
48
49
|
},
|