tumwater 0.1.2 → 0.2.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 +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,42 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { publishInfoFields, writeOrchestratorInfo } from "../team/orchestrator-info.js";
|
|
2
2
|
import { loopEnabled, loopIds } from "../roles/loop-ids.js";
|
|
3
3
|
import { newLiveConfigReload } from "../config/config-live.js";
|
|
4
4
|
import { FALLBACK_BREAKER_POLICY, fallbackProbeDuePair, } from "../budget/fallback-breaker.js";
|
|
5
5
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
6
6
|
import { modelPairName } from "../budget/budget.js";
|
|
7
|
-
import { roleSeamTier,
|
|
7
|
+
import { roleSeamTier, seamConcurrencyKey } from "../config/config-views.js";
|
|
8
8
|
import { launchDueTicks } from "./orchestrator-launch.js";
|
|
9
|
-
import { pollRunnerReasons } from "./orchestrator-scheduling.js";
|
|
10
|
-
import { openBugs
|
|
9
|
+
import { pollRunnerReasons, reviewHoldKey } from "./orchestrator-scheduling.js";
|
|
10
|
+
import { openBugs } from "../backlog/backlog.js";
|
|
11
|
+
import { eligibleEntries } from "../backlog/backlog-eligibility.js";
|
|
11
12
|
import { LoopRunner } from "../loop/loop.js";
|
|
12
13
|
import { branchHead, currentBranch } from "../git/git.js";
|
|
13
14
|
import { removeLegacyLandWorktrees } from "../git/worktree.js";
|
|
14
15
|
import { retireLegacyRoleWorktrees } from "../git/worktree-pool.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { bestEffortEventLogger, logEventBestEffort } from "../events/events.js";
|
|
16
|
+
import { queuedMergeFiles, queuedMergeRoles } from "../merge/merge-queue.js";
|
|
17
|
+
import { drainMergeQueue, settleAbortedApproved } from "../merge/merge-drain.js";
|
|
18
|
+
import { abortableMerges, mergeTasks, newMergePipeline } from "../merge/merge-pipeline.js";
|
|
19
|
+
import { bestEffortEventLogger, logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
19
20
|
import { removeQuiet } from "../files/files.js";
|
|
20
21
|
import { OnceRound } from "../scheduling/once-round.js";
|
|
21
22
|
import { newNotifier } from "../events/notify.js";
|
|
22
23
|
import { consumeAbortRequests, consumeReclaimRequest, consumeRestartRequest, consumeResetRequest, consumeWakeRequest, } from "../operator/operator-requests.js";
|
|
23
24
|
import { piModelsPath } from "../pi/pi-models.js";
|
|
25
|
+
import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
|
|
24
26
|
import { Semaphore } from "../concurrency/semaphore.js";
|
|
25
27
|
import { orchestratorStatePath } from "../paths.js";
|
|
26
28
|
import { RetentionPruner } from "./retention.js";
|
|
27
|
-
import {
|
|
28
|
-
import { drainInFlightWork,
|
|
29
|
-
import {
|
|
30
|
-
import { ReclaimController } from "../
|
|
31
|
-
import { heldProviders } from "../
|
|
32
|
-
import { workReserveCount } from "../scheduling/claims.js";
|
|
29
|
+
import { WorkMergedCache } from "../scheduling/work-merged-cache.js";
|
|
30
|
+
import { drainInFlightWork, HANDOFF_MERGE_WINDOW_MS, p75TickDurationMs, sleepInterruptible, } from "../tick/tick-timing.js";
|
|
31
|
+
import { newTeamGateStates, pollTeamGates } from "../gates/gate-polls.js";
|
|
32
|
+
import { ReclaimController } from "../team/reclaim.js";
|
|
33
|
+
import { heldProviders, windowHeldProviders } from "../team/team-hold.js";
|
|
34
|
+
import { planBacklogFull, workReserveCount } from "../scheduling/claims.js";
|
|
35
|
+
import { runStartupMigrations } from "./startup-migration-steps.js";
|
|
36
|
+
import { archiveAtTeamStart } from "../backlog/backlog-archive.js";
|
|
33
37
|
const POLL_MS = 2000;
|
|
34
38
|
/** Run all enabled loops until the signal aborts — or until a pending self-redeploy has drained
|
|
35
|
-
* the
|
|
39
|
+
* the team and swapped the new build into dist/ (then `restart` is true). */
|
|
36
40
|
export async function runOrchestrator(opts) {
|
|
37
41
|
const { root, config, mainBranch, signal: externalSignal } = opts;
|
|
38
42
|
const pollMs = opts.pollMs ?? POLL_MS;
|
|
39
43
|
const modelsPath = opts.modelsPath ?? piModelsPath();
|
|
44
|
+
// pi's saved default model — the same stat-cached read status-roles.ts makes — used as the
|
|
45
|
+
// fallback provider key for a pi seam that names no model (plans/provider-concurrency.md §2).
|
|
46
|
+
const piDefault = readPiDefaultModel(piSettingsPath(modelsPath), root);
|
|
40
47
|
const breakerPolicy = opts.fallbackBreakerPolicy ?? FALLBACK_BREAKER_POLICY;
|
|
41
48
|
const breakerNow = opts.breakerNow ?? Date.now;
|
|
42
49
|
// Parallel work instances (part 5b/7): the runner list is every loop id the config enables
|
|
@@ -51,15 +58,15 @@ export async function runOrchestrator(opts) {
|
|
|
51
58
|
throw new Error('no roles enabled in tumwater.json — enable at least one role in its "roles" section (e.g. `"feature": { "enabled": true }`), or remove that section to restore every role\'s default');
|
|
52
59
|
// Two internal stop controllers ride beside the caller's (Ctrl+C/SIGTERM): internalStop, the
|
|
53
60
|
// graceful shutdown the once-mode exit and the hand-off's deadline abort fire — it cuts role
|
|
54
|
-
// ticks and
|
|
61
|
+
// ticks and merges alike — and internalRoleStop, which ONLY the restart drain fires when it
|
|
55
62
|
// gives up on its permit-holding ROLE ticks (they then end as `aborted`, resumable on the new
|
|
56
63
|
// build, exactly like a shutdown). The split is the point: the drain's abort must not cut off
|
|
57
|
-
// an in-flight
|
|
58
|
-
// (BUGS.md 2026-09-30: the
|
|
59
|
-
// tick wrapper watch roleSignal — the full signal PLUS the role-only stop — while the
|
|
64
|
+
// an in-flight merge, which then never reaches the hand-off window the wait announces
|
|
65
|
+
// (BUGS.md 2026-09-30: the merge died 13 ms after the warning). So the runners and the timed
|
|
66
|
+
// tick wrapper watch roleSignal — the full signal PLUS the role-only stop — while the merge
|
|
60
67
|
// tasks keep wiring the full signal alone. A director tick is never aborted by either
|
|
61
|
-
// controller: poll holds for it without a cap, so by the time `restart`
|
|
62
|
-
// and the
|
|
68
|
+
// controller: poll holds for it without a cap, so by the time `restart` takes effect only
|
|
69
|
+
// role ticks and the merge can remain.
|
|
63
70
|
const internalStop = new AbortController();
|
|
64
71
|
const internalRoleStop = new AbortController();
|
|
65
72
|
const signal = AbortSignal.any([externalSignal, internalStop.signal]);
|
|
@@ -71,34 +78,49 @@ export async function runOrchestrator(opts) {
|
|
|
71
78
|
const launchServicesWatch = opts.once ? null : (opts.launchServicesWatch ?? null);
|
|
72
79
|
// Pressure reclaim (plans/disk-floor.md, part 2/4): one background pass at a time, driven by
|
|
73
80
|
// the disk gate's poll below. In-process state (the use registry) is enough, because this
|
|
74
|
-
// process is the one that runs ticks and
|
|
81
|
+
// process is the one that runs ticks and merges.
|
|
75
82
|
const reclaim = new ReclaimController(root);
|
|
76
83
|
let restart = false;
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// landing is not exempt.
|
|
84
|
+
// The shared concurrency cap: role ticks and the merges (each verify, and the merge's conflict
|
|
85
|
+
// resolver — merge-drain.ts) all hold a permit; see its MergePipelineContext for why a
|
|
86
|
+
// merge is not exempt.
|
|
81
87
|
const semaphore = new Semaphore(Math.max(1, config.maxConcurrent));
|
|
88
|
+
// Seed the pool's per-provider caps from the startup config (plans/provider-concurrency.md
|
|
89
|
+
// §7): a role tick's lease is bounded by its provider key from the first launch onward, not
|
|
90
|
+
// just after a live edit. A config without the key passes an empty map, the no-change case.
|
|
91
|
+
semaphore.setKeyCaps(config.maxConcurrentPerProvider ?? {});
|
|
82
92
|
const infoFile = orchestratorStatePath(root);
|
|
83
93
|
const info = { pid: process.pid, startedAt: Date.now(), roles: enabled };
|
|
84
94
|
if (redeploy)
|
|
85
95
|
info.build = redeploy.status();
|
|
96
|
+
if (opts.stopsAt !== undefined)
|
|
97
|
+
info.stopsAt = opts.stopsAt;
|
|
86
98
|
writeOrchestratorInfo(root, info);
|
|
87
99
|
// Best-effort, like every other event write in this function (see `emit` below): this runs
|
|
88
100
|
// BEFORE the try/finally and its cleanup, so a raw logEvent throw here would not just stop
|
|
89
|
-
// the
|
|
101
|
+
// the team from starting over an informational event — it would also skip the finally that
|
|
90
102
|
// disposes the notifier and removes the info file just written. An unwritable feed
|
|
91
|
-
// (ENOSPC, EACCES, the path replaced by a directory) must not turn a
|
|
103
|
+
// (ENOSPC, EACCES, the path replaced by a directory) must not turn a team that can run
|
|
92
104
|
// into one that refuses to start; logEventBestEffort reports the write failure on stderr so
|
|
93
105
|
// it stays visible, and startup proceeds to the poll loop, which is already best-effort.
|
|
94
106
|
logEventBestEffort(root, {
|
|
95
|
-
|
|
107
|
+
source: TUMWATER_SOURCE,
|
|
96
108
|
type: "orchestrator_start",
|
|
97
109
|
pid: process.pid,
|
|
98
110
|
roles: enabled,
|
|
99
111
|
...(redeploy ? { build: redeploy.build.sha } : {}),
|
|
100
112
|
});
|
|
101
|
-
//
|
|
113
|
+
// The first per-runner state read is each LoopRunner constructor's loadLoopState, so the
|
|
114
|
+
// persisted-name migrations run before it. (After the info write and start event above, which
|
|
115
|
+
// an in-process test or a second `tumwater run` probe reads synchronously.)
|
|
116
|
+
await runStartupMigrations(root);
|
|
117
|
+
// Finished backlog entries still in the live files move to the archives now
|
|
118
|
+
// (plans/backlog-archive.md), while no merge is in progress and no run is in flight: a
|
|
119
|
+
// repository migrates on its first start with this build, and an entry a hand commit moved is
|
|
120
|
+
// swept. One commit on main, or a warning; the team starts either way.
|
|
121
|
+
await archiveAtTeamStart(root);
|
|
122
|
+
const runners = enabled.map((role) => new LoopRunner(root, role, config, mainBranch, roleSignal));
|
|
123
|
+
// Legacy merger checkouts (`_land-<role>`) from a pre-pool build hold nothing now that verifies
|
|
102
124
|
// and merges lease pooled slots: remove them once, before any tick, so the retired layout
|
|
103
125
|
// never lingers on disk. (After the info write and start event above, so startup's published
|
|
104
126
|
// state is on disk synchronously for an in-process test or a second `tumwater run` probe.)
|
|
@@ -107,64 +129,64 @@ export async function runOrchestrator(opts) {
|
|
|
107
129
|
// window, right after the LoopRunners above: a role with a pending resume keeps its dir as a
|
|
108
130
|
// pinned slot, every other legacy dir goes. Pass every configured loop id, not `enabled` (a
|
|
109
131
|
// `--once --role` filter): a scoped run must not retire or unpin the other roles, whose
|
|
110
|
-
// pending resumes the next full-
|
|
132
|
+
// pending resumes the next full-team start still owns. See retireLegacyRoleWorktrees.
|
|
111
133
|
await retireLegacyRoleWorktrees(root, loopIds(config));
|
|
112
134
|
// Session retention (src/orchestrator/retention.ts owns the whole concern): construction runs the
|
|
113
|
-
// startup prune, seeding the once-per-day gate so an unchanged
|
|
135
|
+
// startup prune, seeding the once-per-day gate so an unchanged team prunes at most once
|
|
114
136
|
// per day — and 0 disables pruning, the same convention as quietTimeoutSeconds.
|
|
115
137
|
const retentionPruner = new RetentionPruner(root, config.sessionRetentionDays);
|
|
116
138
|
// In-flight tasks, split by who requested them: the redeploy drain caps role ticks at its
|
|
117
139
|
// window but waits for a director tick without one — an explicit human prompt outranks the
|
|
118
|
-
// self-redeploy (BUGS.md 2026-09-08). The
|
|
119
|
-
// in-process tasks (the
|
|
140
|
+
// self-redeploy (BUGS.md 2026-09-08). The merge tasks are deliberately OUTSIDE this split:
|
|
141
|
+
// in-process tasks (the verifies and the one merge, since 2026-09-18 under the same maxConcurrent
|
|
120
142
|
// permits as role ticks) that shutdown still awaits — to the end on an operator stop, and for a
|
|
121
|
-
// bounded hand-off on a restart (drainInFlightWork) — an aborted
|
|
143
|
+
// bounded hand-off on a restart (drainInFlightWork) — an aborted merge keeps its ref and
|
|
122
144
|
// drops its entry, recovering on next start. roleInFlight holds every RESERVED role tick
|
|
123
145
|
// (permit holders and waiters parked in the semaphore queue alike), since shutdown awaits
|
|
124
146
|
// both; what the restart drain waits on, aborts, and reports as abortedTicks is only
|
|
125
147
|
// rolePermitHolders, the role ticks actually holding a permit. A parked waiter has nothing to
|
|
126
148
|
// drain — the start gate (tickStartHeld below) keeps it from starting while a restart is
|
|
127
149
|
// pending — and counting it held drains open for ticks that had not begun and inflated
|
|
128
|
-
// abortedTicks (12 reported against 3 aborted
|
|
150
|
+
// abortedTicks (12 reported against 3 aborted run_ends on 2026-09-21; BUGS.md 2026-09-23).
|
|
129
151
|
const roleInFlight = new Set();
|
|
130
152
|
const rolePermitHolders = new Set();
|
|
131
153
|
const directorInFlight = new Set();
|
|
132
|
-
// The restart drain's window tracks the
|
|
154
|
+
// The restart drain's window tracks the team's real tick duration (BUGS.md 2026-09-18): the
|
|
133
155
|
// durations of recent COMPLETED role ticks feed a p75 that poll uses in place of the
|
|
134
156
|
// cold-start constant. Bounded (orchestrator-launch.ts's ROLE_TICK_DURATION_SAMPLES) so a
|
|
135
|
-
// long-running
|
|
157
|
+
// long-running team's memory stays flat; aborted ticks are
|
|
136
158
|
// excluded — their short cut-off durations would drag the window down and cause more aborts.
|
|
137
159
|
const roleTickDurationsMs = [];
|
|
138
|
-
// Every
|
|
139
|
-
// changes they
|
|
140
|
-
const
|
|
160
|
+
// Every merge task (merge-drain.ts's MergePipeline): the verification stage's tasks, the
|
|
161
|
+
// changes they approved, and the merge slot.
|
|
162
|
+
const merges = newMergePipeline();
|
|
141
163
|
// Once-mode bookkeeping (`tumwater run --once`) — the tick-count snapshot, the per-role
|
|
142
164
|
// settle reasons, and the quiet-poll exit rule — lives in src/scheduling/once-round.ts.
|
|
143
165
|
const once = new OnceRound(runners, opts.once === true);
|
|
144
|
-
// The
|
|
166
|
+
// The team-wide gates' and alarms' cross-poll memory (src/gates/gate-polls.ts owns the family's
|
|
145
167
|
// wiring — budget, pause, quiet hours, failure hold, and the two storm alarms): the gates'
|
|
146
168
|
// previous values for the edge-triggered events, the fallback breaker, and the hold's own
|
|
147
169
|
// memory (deadline, kind, relapse count). In memory only — a restart re-trusts the
|
|
148
170
|
// fallback, starts with the hold open, and can re-log at most one event per gate. The
|
|
149
171
|
// budget gate's breaker is also the fallback ticks' evidence sink: the start pass below
|
|
150
172
|
// records every tick's outcome into it and runs its probe (src/budget/fallback-breaker.ts), and
|
|
151
|
-
// the start gate reads gateStates.
|
|
173
|
+
// the start gate reads gateStates.teamHold at permit time, so its closures always see
|
|
152
174
|
// the latest poll's hold verdict rather than the poll that created them.
|
|
153
|
-
const gateStates =
|
|
175
|
+
const gateStates = newTeamGateStates(config);
|
|
154
176
|
// The live config the last successful reload produced (last-known-good while the file is
|
|
155
177
|
// broken or missing). The reload bookkeeping itself lives in src/config/config-live.ts.
|
|
156
178
|
const liveReload = newLiveConfigReload({ root, config, mainBranch, runnerSignal: roleSignal, runners, semaphore, roleFilter: opts.roleFilter });
|
|
157
179
|
// The pause gates (src/gates/pause-gates.ts owns the concern): the operator pause's and the
|
|
158
180
|
// per-role pause's cross-poll bookkeeping, so each pause/resume crossing logs exactly one
|
|
159
181
|
// event instead of once per ~2s poll.
|
|
160
|
-
// The primary checkout's branch, for the edge-triggered divergence warning: the
|
|
182
|
+
// The primary checkout's branch, for the edge-triggered divergence warning: the team
|
|
161
183
|
// resolved its target branch at startup, and a human checking out something else mid-run
|
|
162
|
-
// must not silently change what the
|
|
184
|
+
// must not silently change what the team merges into — every role worktree is based on
|
|
163
185
|
// the resolved branch. One warning per episode, re-armed when the checkout returns.
|
|
164
186
|
let warnedBranchDivergence = false;
|
|
165
|
-
// Need-based deferral: whether qualifying work has
|
|
166
|
-
// since a role's last-seen main head, cached per head (see scheduling/work-
|
|
167
|
-
const
|
|
187
|
+
// Need-based deferral: whether qualifying work has merged
|
|
188
|
+
// since a role's last-seen main head, cached per head (see scheduling/work-merged-cache.ts).
|
|
189
|
+
const workMergedSince = new WorkMergedCache(root, mainBranch);
|
|
168
190
|
// Per-role deferred-due state for one-shot tick_deferred events: the previous poll's
|
|
169
191
|
// deferral per role (like prevBudgetPaused/prevUserPaused, but per role), so each episode
|
|
170
192
|
// logs exactly once — on the transition in, never while merely not-due and never on exit.
|
|
@@ -176,21 +198,21 @@ export async function runOrchestrator(opts) {
|
|
|
176
198
|
let holdForRestart = false;
|
|
177
199
|
// Whether the last poll's disk gate held new work (plans/disk-floor.md, part 1/4). Hoisted
|
|
178
200
|
// beside holdForRestart because tickStartHeld reads it at permit time, between polls — the
|
|
179
|
-
// disk hold closes the same gate point every
|
|
201
|
+
// disk hold closes the same gate point every team-wide hold uses.
|
|
180
202
|
let diskHeld = false;
|
|
181
203
|
// The start gate every reserved tick passes the moment its permit is granted
|
|
182
204
|
// (runTimedRoleTick's `held`): closed while a restart is pending, and after one is decided so
|
|
183
205
|
// no waiter the shutdown hands a permit to starts on the build being replaced. One predicate,
|
|
184
|
-
// so another
|
|
206
|
+
// so another team-wide hold on new ticks can close the same gate point.
|
|
185
207
|
const tickStartHeld = () => holdForRestart || restart || diskHeld;
|
|
186
208
|
// The operator notify hook (src/events/notify.ts owns the whole concern): one configured shell
|
|
187
|
-
// command run on notable events (budget_paused, role_streak_paused,
|
|
209
|
+
// command run on notable events (budget_paused, role_streak_paused, merge_failed,
|
|
188
210
|
// restart_blocked). Subscribed here so it sees every event this process logs from start;
|
|
189
211
|
// disposed beside the orchestrator_stop event below.
|
|
190
212
|
const notifier = newNotifier(root);
|
|
191
213
|
// Best-effort event logging in the poll loop (bestEffortEventLogger, events.ts): this loop
|
|
192
214
|
// and its finally run with no catch, so an unwritable feed (ENOSPC, EACCES, the path replaced
|
|
193
|
-
// by a directory) must not end the
|
|
215
|
+
// by a directory) must not end the team — and, in the finally, must not skip the
|
|
194
216
|
// notifier/quiet-file cleanup or mask an in-flight error. The feed is the failure; the poll
|
|
195
217
|
// and the shutdown still run.
|
|
196
218
|
const { emit, warn } = bestEffortEventLogger(root);
|
|
@@ -222,37 +244,37 @@ export async function runOrchestrator(opts) {
|
|
|
222
244
|
consumeWakeRequest(root, runners);
|
|
223
245
|
consumeRestartRequest(root, redeploy);
|
|
224
246
|
consumeReclaimRequest(root, reclaim);
|
|
225
|
-
consumeAbortRequests(root, runners,
|
|
226
|
-
await
|
|
247
|
+
consumeAbortRequests(root, runners, abortableMerges(merges));
|
|
248
|
+
await settleAbortedApproved(root, merges);
|
|
227
249
|
// branchHead reads the ref files first (microsecond-scale; this runs every poll) and
|
|
228
250
|
// spawns `git rev-parse` only when they cannot resolve it. "" means main does not exist
|
|
229
251
|
// yet — isEligible treats an empty head as "no wake".
|
|
230
252
|
const mainHead = (await branchHead(root, mainBranch)) ?? "";
|
|
231
253
|
// Branch-divergence watch, once per poll (see warnedBranchDivergence above): the
|
|
232
|
-
//
|
|
254
|
+
// team keeps fast-forwarding the branch it resolved at startup — the safe behavior,
|
|
233
255
|
// since role worktrees are based on it — and one warning names the divergence
|
|
234
256
|
// instead of leaving it mysterious. Back on the target branch re-arms the check.
|
|
235
257
|
const liveBranch = await currentBranch(root);
|
|
236
258
|
if (liveBranch !== null && liveBranch !== mainBranch) {
|
|
237
259
|
if (!warnedBranchDivergence) {
|
|
238
260
|
warnedBranchDivergence = true;
|
|
239
|
-
warn(`
|
|
261
|
+
warn(`your checkout moved to ${liveBranch} — the team keeps merging into ${mainBranch}`);
|
|
240
262
|
}
|
|
241
263
|
}
|
|
242
264
|
else {
|
|
243
265
|
warnedBranchDivergence = false;
|
|
244
266
|
}
|
|
245
267
|
const now = Date.now();
|
|
246
|
-
// The
|
|
268
|
+
// The team's gates and alarms, one poll of the family (src/gates/gate-polls.ts): the daily
|
|
247
269
|
// budget gate — with its derived-state publish and the fallback handback to the
|
|
248
|
-
// primary — the operator and per-role pause gates, quiet hours, the
|
|
270
|
+
// primary — the operator and per-role pause gates, quiet hours, the team-wide failure
|
|
249
271
|
// hold, and the two observational storm alarms. The family's cross-poll memory lives
|
|
250
272
|
// in gateStates, advanced in place; this returns this poll's verdicts for the
|
|
251
273
|
// scheduling and start passes. The failure hold is deliberately not among them — the
|
|
252
|
-
// start gate and the
|
|
253
|
-
// time from gateStates.
|
|
274
|
+
// start gate and the merges' startHeld read the LATEST poll's verdict at permit
|
|
275
|
+
// time from gateStates.teamHold, so a waiter granted its permit after later polls
|
|
254
276
|
// ran meets the hold as it stands then, not as this poll left it.
|
|
255
|
-
const { roleConfig, userPaused, pausedRoles: pausedRolesSet, capPaused, maintenanceQuota, bootstrapHeld, bootstrapActive, budgetPausedRoles, servingResolved, priceResolved, budgetActive, roleQuietHeld, quietNow, diskHeld: polledDiskHeld, } =
|
|
277
|
+
const { roleConfig, directorConfig, userPaused, pausedRoles: pausedRolesSet, capPaused, maintenanceQuota, bootstrapHeld, bootstrapActive, budgetPausedRoles, servingResolved, priceResolved, budgetActive, roleQuietHeld, quietNow, diskHeld: polledDiskHeld, } = pollTeamGates(gateStates, {
|
|
256
278
|
root,
|
|
257
279
|
runners,
|
|
258
280
|
liveConfig,
|
|
@@ -264,23 +286,33 @@ export async function runOrchestrator(opts) {
|
|
|
264
286
|
// The latest poll's disk verdict, hoisted for tickStartHeld's permit-time reads
|
|
265
287
|
// (plans/disk-floor.md, part 1/4).
|
|
266
288
|
diskHeld = polledDiskHeld;
|
|
289
|
+
// Publish the permit pool's live state (plans/provider-concurrency.md §8): observers
|
|
290
|
+
// (status, TUI, GUI) read one file instead of coupling to this Semaphore, and the team
|
|
291
|
+
// line shows per-provider counts only while a cap map is configured. Written only when
|
|
292
|
+
// it changes, like budget and disk; a map-less team publishes `byKey` empty, which
|
|
293
|
+
// renders exactly as before.
|
|
294
|
+
publishInfoFields(root, info, [["permits", semaphore.snapshot()]]);
|
|
267
295
|
// The failure hold, keyed per provider (PLANS.md 2026-10-05): a role is held when ITS
|
|
268
296
|
// tick model's provider stands under a hold, and every role is held when the
|
|
269
|
-
// reviewer's provider is (with the review gate on, nothing could
|
|
297
|
+
// reviewer's provider is (with the review gate on, nothing could merge — and the merge
|
|
270
298
|
// queue below reads the same reviewHeld verdict). Key presence in the map is relapse
|
|
271
299
|
// memory, not a hold: heldProviders() reads only standing holds (`until` non-null),
|
|
272
|
-
// so a lifted hold never keeps blocking. Read fresh from gateStates.
|
|
300
|
+
// so a lifted hold never keeps blocking. Read fresh from gateStates.teamHold — the
|
|
273
301
|
// permit-time closures below see the latest poll's verdicts, not this poll's snapshot.
|
|
274
|
-
const
|
|
302
|
+
const teamHeldProviders = heldProviders(gateStates.teamHold);
|
|
275
303
|
// Fresh reads for the permit-time closures: they run after later polls have advanced
|
|
276
|
-
// gateStates.
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
304
|
+
// gateStates.teamHold, so they re-derive from the LATEST poll's map, not this snapshot.
|
|
305
|
+
const teamHeldNow = () => heldProviders(gateStates.teamHold);
|
|
306
|
+
// The reviewer's seam is read from the config merges actually review under — the poll's
|
|
307
|
+
// roleConfig, which a used-up claude window or the cap has already moved to a fallback —
|
|
308
|
+
// so a reviewer that left claude no longer holds the whole team on the claude hold.
|
|
309
|
+
const reviewSeamKey = reviewHoldKey(roleConfig);
|
|
310
|
+
const reviewHeldNow = () => liveConfig.review.enabled && teamHeldNow().has(reviewSeamKey);
|
|
311
|
+
const reviewHeld = liveConfig.review.enabled && teamHeldProviders.has(reviewSeamKey);
|
|
280
312
|
const roleProviders = new Map(runners.map((r) => [r.role, r.runProvider(now)]));
|
|
281
313
|
// Self-redeploy (src/redeploy/redeployer.ts): with main's head in hand, let the state
|
|
282
314
|
// machine observe it. `hold` starts no new ticks at all — director included; a restart
|
|
283
|
-
//
|
|
315
|
+
// takes effect within the drain's window plus a bounded merge hand-off, and its prompt waits
|
|
284
316
|
// in the inbox for the new build — while the green check/compile/drain run in the
|
|
285
317
|
// background. That covers ticks
|
|
286
318
|
// reserved before the hold too: a waiter parked in the semaphore queue that is granted a
|
|
@@ -299,15 +331,14 @@ export async function runOrchestrator(opts) {
|
|
|
299
331
|
roleTickP75Ms: p75TickDurationMs(roleTickDurationsMs),
|
|
300
332
|
}, liveConfig.autoRestart, now);
|
|
301
333
|
const build = redeploy.status(now);
|
|
302
|
-
|
|
303
|
-
writeOrchestratorInfoBestEffort(root, info);
|
|
334
|
+
publishInfoFields(root, info, [["build", build]]);
|
|
304
335
|
if (action === "restart") {
|
|
305
336
|
restart = true;
|
|
306
337
|
// Only permit holders have a pi run to cut off (the director is guaranteed finished by
|
|
307
338
|
// then): a parked waiter meets the closed start gate whenever the shutdown hands it a
|
|
308
339
|
// permit, so it needs no abort — and aborting for it alone would also cut off an
|
|
309
|
-
// in-flight
|
|
310
|
-
// keeps that promise: the
|
|
340
|
+
// in-flight merge that no permit-holding tick put at stake. The role-only controller
|
|
341
|
+
// keeps that promise: the merge, wired to the full signal, runs on into the hand-off
|
|
311
342
|
// window the wait announces (BUGS.md 2026-09-30).
|
|
312
343
|
if (rolePermitHolders.size > 0)
|
|
313
344
|
internalRoleStop.abort();
|
|
@@ -315,13 +346,13 @@ export async function runOrchestrator(opts) {
|
|
|
315
346
|
}
|
|
316
347
|
holdForRestart = action === "hold";
|
|
317
348
|
}
|
|
318
|
-
// Merge queue 3/5 — drain the durable
|
|
319
|
-
// hold is pending (the scheduler's WHEN;
|
|
320
|
-
// stage, its merge slot, and the dedupe against main). A held poll starts no
|
|
321
|
-
// merge, exactly as it starts no tick; what is already in flight runs on, and a
|
|
349
|
+
// Merge queue 3/5 — drain the durable merge queue while neither a restart nor a failure
|
|
350
|
+
// hold is pending (the scheduler's WHEN; merge-drain.ts owns the HOW — the verification
|
|
351
|
+
// stage, its merge slot, and the dedupe against main). A held poll starts no verify and no
|
|
352
|
+
// merge, exactly as it starts no tick; what is already in flight runs on, and a verify
|
|
322
353
|
// parked for its permit meets the same start gate as a parked tick when the permit comes.
|
|
323
354
|
if (!holdForRestart && !reviewHeld && !diskHeld) {
|
|
324
|
-
await
|
|
355
|
+
await drainMergeQueue({
|
|
325
356
|
root,
|
|
326
357
|
mainBranch,
|
|
327
358
|
signal,
|
|
@@ -329,17 +360,21 @@ export async function runOrchestrator(opts) {
|
|
|
329
360
|
runners,
|
|
330
361
|
liveConfig,
|
|
331
362
|
roleConfig,
|
|
363
|
+
directorConfig,
|
|
364
|
+
piDefault,
|
|
332
365
|
startHeld: () => tickStartHeld() || reviewHeldNow(),
|
|
333
|
-
},
|
|
366
|
+
}, merges);
|
|
334
367
|
}
|
|
335
|
-
// Backlog-aware deferral: while PLANS.md `## Planned` or BUGS.md `## Open` on main
|
|
336
|
-
//
|
|
337
|
-
//
|
|
368
|
+
// Backlog-aware deferral: while PLANS.md `## Planned` or BUGS.md `## Open` on main
|
|
369
|
+
// holds an entry feature or bugfix may take (eligibleEntries), idle maintenance ticks
|
|
370
|
+
// stay deferred — queued feature/bugfix work outranks
|
|
371
|
+
// them regardless of what merged. Stat-cached reads (backlog.ts): one stat per file per
|
|
338
372
|
// poll while the files are unchanged. bugfix is not a maintenance role, but while BUGS.md
|
|
339
373
|
// `## Open` is empty it defers like one (deferTick) — openBugsNow is that emptiness, read
|
|
340
374
|
// once here and reused for both predicates.
|
|
341
375
|
const openBugsNow = openBugs(root).length > 0;
|
|
342
|
-
const workBacklogOpen =
|
|
376
|
+
const workBacklogOpen = eligibleEntries(root, "feature").length > 0 || eligibleEntries(root, "bugfix").length > 0;
|
|
377
|
+
const plansFull = planBacklogFull(root, liveConfig);
|
|
343
378
|
// A demoted fallback's half-open window (part 5c/8): the pair whose probe is due, and
|
|
344
379
|
// the roles whose model tier WOULD run on that pair absent the demotion (the price-based
|
|
345
380
|
// resolution — the serving resolution has already dropped the pair, which is exactly
|
|
@@ -347,7 +382,7 @@ export async function runOrchestrator(opts) {
|
|
|
347
382
|
// admits exactly one of them as the probe — a probe blind to the pair's tier would lift
|
|
348
383
|
// the pause for every role (objection to an earlier draft), and a probe with no
|
|
349
384
|
// eligible runner must not hijack the poll's other launches (the launch pass continues
|
|
350
|
-
// only the eligible runners it cannot admit, never the rest of the
|
|
385
|
+
// only the eligible runners it cannot admit, never the rest of the team).
|
|
351
386
|
const probePair = fallbackProbeDuePair(gateStates.budget.breakers, breakerNow());
|
|
352
387
|
const probeRoles = new Set();
|
|
353
388
|
if (probePair !== null) {
|
|
@@ -360,7 +395,7 @@ export async function runOrchestrator(opts) {
|
|
|
360
395
|
}
|
|
361
396
|
}
|
|
362
397
|
// The review-on strong pause (budgetGate): with the review gate on, an unresolvable
|
|
363
|
-
// strong tier means nothing could
|
|
398
|
+
// strong tier means nothing could merge, so EVERY role is held — the per-tier pause set
|
|
364
399
|
// alone would let small/default roles author changes no reviewer could judge. The probe
|
|
365
400
|
// of the strong pair pierces this hold for exactly the probed tier's roles too.
|
|
366
401
|
const reviewStrongPaused = budgetActive && liveConfig.review.enabled && servingResolved.strong.pair === null;
|
|
@@ -383,13 +418,15 @@ export async function runOrchestrator(opts) {
|
|
|
383
418
|
roleQuietHeld,
|
|
384
419
|
holdForRestart,
|
|
385
420
|
diskHeld,
|
|
386
|
-
heldProviders:
|
|
421
|
+
heldProviders: teamHeldProviders,
|
|
422
|
+
windowHeldProviders: windowHeldProviders(gateStates.teamHold),
|
|
387
423
|
reviewHeld,
|
|
388
424
|
roleProviders,
|
|
389
425
|
openBugsNow,
|
|
390
426
|
workBacklogOpen,
|
|
427
|
+
planBacklogFull: plansFull,
|
|
391
428
|
deferredDue,
|
|
392
|
-
|
|
429
|
+
workMergedSince,
|
|
393
430
|
});
|
|
394
431
|
launchDueTicks({
|
|
395
432
|
root,
|
|
@@ -405,40 +442,61 @@ export async function runOrchestrator(opts) {
|
|
|
405
442
|
probeRoles,
|
|
406
443
|
budgetActive,
|
|
407
444
|
semaphore,
|
|
445
|
+
piDefault,
|
|
408
446
|
rolePermitHolders,
|
|
409
447
|
roleInFlight,
|
|
410
448
|
directorInFlight,
|
|
411
449
|
roleTickDurationsMs,
|
|
412
450
|
startHeld: (role) => tickStartHeld() ||
|
|
413
|
-
(role !== DIRECTOR_ROLE
|
|
414
|
-
|
|
451
|
+
(role !== DIRECTOR_ROLE
|
|
452
|
+
? reviewHeldNow() || teamHeldNow().has(roleProviders.get(role))
|
|
453
|
+
: windowHeldProviders(gateStates.teamHold).has(roleProviders.get(role))),
|
|
415
454
|
});
|
|
416
455
|
// Priority headroom (plans/parallel-work-instances.md part 7/7): withhold a few permits
|
|
417
456
|
// from maintenance while a work loop has work to take, so a due work tick never waits out
|
|
418
457
|
// a whole maintenance tick. Sized after the launch pass, so a runner launched this poll
|
|
419
458
|
// already reads as running and reserves nothing for itself.
|
|
459
|
+
const poolSnapshot = semaphore.snapshot();
|
|
460
|
+
// The provider-headroom predicate receives the structural ClaimRunner; the config that
|
|
461
|
+
// names the provider lives on the real LoopRunners, so look the runner up by role.
|
|
462
|
+
const runnerByRole = new Map(runners.map((r) => [r.role, r]));
|
|
420
463
|
const reserve = workReserveCount(root, runners, {
|
|
421
464
|
maxConcurrent: liveConfig.maxConcurrent,
|
|
422
465
|
enabled: new Set(runners.filter((r) => loopEnabled(r.config, r.role)).map((r) => r.role)),
|
|
423
|
-
|
|
466
|
+
queuedMergeRoles: queuedMergeRoles(root),
|
|
467
|
+
// Per-provider headroom (plans/provider-concurrency.md §6): an idle work loop reserves a
|
|
468
|
+
// permit only when its own provider could use it — capped and full, it would only idle
|
|
469
|
+
// the slot another provider's maintenance tick could take. Read from the pool's live
|
|
470
|
+
// snapshot; an uncapped key (or the team-total-only case) always has headroom.
|
|
471
|
+
providerHasHeadroom: (r) => {
|
|
472
|
+
const runner = runnerByRole.get(r.role);
|
|
473
|
+
const cfg = runner && runner.role !== DIRECTOR_ROLE ? runner.runConfig(now) : null;
|
|
474
|
+
const key = cfg ? seamConcurrencyKey(cfg, piDefault) : undefined;
|
|
475
|
+
if (key === undefined)
|
|
476
|
+
return true;
|
|
477
|
+
const k = poolSnapshot.byKey[key];
|
|
478
|
+
if (k?.limit === undefined)
|
|
479
|
+
return true;
|
|
480
|
+
return k.inUse < k.limit;
|
|
481
|
+
},
|
|
424
482
|
});
|
|
425
483
|
semaphore.setReserve(reserve);
|
|
426
484
|
if ((lastReserve === 0) !== (reserve === 0)) {
|
|
427
|
-
emit({
|
|
485
|
+
emit({ source: TUMWATER_SOURCE, type: "permit_reserve", reserve });
|
|
428
486
|
}
|
|
429
487
|
lastReserve = reserve;
|
|
430
|
-
// Once mode's exit: every enabled role settled, no tick in flight, and the
|
|
431
|
-
// empty with no
|
|
488
|
+
// Once mode's exit: every enabled role settled, no tick in flight, and the merge queue
|
|
489
|
+
// empty with no merge in flight for one full poll cycle — then fire the internal
|
|
432
490
|
// stop, the same graceful shutdown the restart drain uses (the `finally` awaits
|
|
433
|
-
// in-flight
|
|
434
|
-
// against dropping a queued-but-not-yet-started
|
|
491
|
+
// in-flight merge tasks to the end). The one-poll-cycle requirement is the guard
|
|
492
|
+
// against dropping a queued-but-not-yet-started merge: a stop that arrives during the
|
|
435
493
|
// shutdown drain drops the queue entry, so the slot gets its poll to pick the entry up
|
|
436
494
|
// before the stop is trusted.
|
|
437
495
|
if (once.exitReady({
|
|
438
496
|
roleTicks: roleInFlight.size,
|
|
439
497
|
directorTicks: directorInFlight.size,
|
|
440
|
-
|
|
441
|
-
|
|
498
|
+
merges: mergeTasks(merges).length,
|
|
499
|
+
queuedMerges: queuedMergeFiles(root).length,
|
|
442
500
|
})) {
|
|
443
501
|
internalStop.abort();
|
|
444
502
|
break;
|
|
@@ -447,8 +505,8 @@ export async function runOrchestrator(opts) {
|
|
|
447
505
|
}
|
|
448
506
|
}
|
|
449
507
|
finally {
|
|
450
|
-
await drainInFlightWork(root, roleInFlight, directorInFlight,
|
|
451
|
-
emit({
|
|
508
|
+
await drainInFlightWork(root, roleInFlight, directorInFlight, mergeTasks(merges), restart, opts.handoffMergeWindowMs ?? HANDOFF_MERGE_WINDOW_MS, () => internalStop.abort());
|
|
509
|
+
emit({ source: TUMWATER_SOURCE, type: "orchestrator_stop" });
|
|
452
510
|
notifier.dispose();
|
|
453
511
|
removeQuiet(infoFile);
|
|
454
512
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** Session retention (the `sessionRetentionDays` setting): the once-per-day gate and the prune
|
|
2
3
|
* itself, split out of orchestrator.ts — the poll-loop runtime carried both a startup prune and
|
|
3
4
|
* a near-identical in-poll prune plus the last-applied-window bookkeeping, and scheduling.ts
|
|
4
5
|
* held the due-gate despite being about tick policy, not file retention. Dependency direction:
|
|
5
6
|
* orchestrator → retention; this module owns the whole "which files age out, and when" concern
|
|
6
7
|
* and the orchestrator only hands it the (possibly live-reloaded) window. */
|
|
7
|
-
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
8
|
+
import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
8
9
|
import { pruneOldFiles } from "../files/files.js";
|
|
9
|
-
import { pruneSessionLedgers } from "../agent/session-ledger.js";
|
|
10
|
-
import {
|
|
10
|
+
import { pruneSessionLedgers, pruneOpencodeSessions } from "../agent/session-ledger.js";
|
|
11
|
+
import { loadConfigSafe } from "../config/config.js";
|
|
12
|
+
import { opencodeBackend, opencodeSpillDir } from "../agent/opencode/opencode-backend.js";
|
|
13
|
+
import { opencodeDbPath, sessionsRootDir, toolOutputDir } from "../paths.js";
|
|
11
14
|
/** Is a once-per-day session prune due? Due when retention is enabled (> 0) and a full day
|
|
12
15
|
* has passed since the last prune (or no prune has run yet). */
|
|
13
16
|
export function dueForPrune(lastPruneAt, now, retentionDays) {
|
|
@@ -34,8 +37,8 @@ export class RetentionPruner {
|
|
|
34
37
|
this.prune(root, retentionDays);
|
|
35
38
|
}
|
|
36
39
|
/** Apply a (possibly live-reloaded) retention window: a mid-run edit re-prunes immediately;
|
|
37
|
-
* independently of edits, an unchanged
|
|
38
|
-
*
|
|
40
|
+
* independently of edits, an unchanged team prunes at most once per day so a never-restarted
|
|
41
|
+
* team still honors its window. Both paths log the startup warning shape only when files
|
|
39
42
|
* were actually deleted — quiet polls stay silent. */
|
|
40
43
|
poll(root, retentionDays) {
|
|
41
44
|
if (retentionDays !== this.lastRetention || dueForPrune(this.lastPruneAt, Date.now(), retentionDays)) {
|
|
@@ -46,7 +49,7 @@ export class RetentionPruner {
|
|
|
46
49
|
// only for a positive window.
|
|
47
50
|
if (retentionDays !== this.lastRetention) {
|
|
48
51
|
logEventBestEffort(root, {
|
|
49
|
-
|
|
52
|
+
source: TUMWATER_SOURCE,
|
|
50
53
|
type: "retention_changed",
|
|
51
54
|
from: this.lastRetention,
|
|
52
55
|
to: retentionDays,
|
|
@@ -61,15 +64,29 @@ export class RetentionPruner {
|
|
|
61
64
|
}
|
|
62
65
|
prune(root, days) {
|
|
63
66
|
const sessions = sessionsRootDir(root);
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
67
|
+
// OpenCode's sessions live outside .tumwater (design §4, finding 17): resolve its binary
|
|
68
|
+
// and OPENCODE_DB exactly as a run's invocation would, and purge its expired sessions and
|
|
69
|
+
// spill files from the ledger BEFORE the file pass below ages it out — an idle role's
|
|
70
|
+
// whole ledger file is deleted once its mtime passes the window, so only reading it first
|
|
71
|
+
// hands every expired OpenCode session id to `opencode session delete`. Each half warns its
|
|
72
|
+
// own unit.
|
|
73
|
+
const { config } = loadConfigSafe(root);
|
|
74
|
+
const bin = config !== undefined ? opencodeBackend.resolveBin(config).bin : undefined;
|
|
75
|
+
const deleted = pruneOpencodeSessions(sessions, days, {
|
|
76
|
+
bin,
|
|
77
|
+
env: { ...process.env, OPENCODE_DB: opencodeDbPath(root) },
|
|
78
|
+
spillDir: opencodeSpillDir(root),
|
|
79
|
+
});
|
|
80
|
+
if (deleted > 0)
|
|
81
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `deleted ${deleted} expired opencode session(s)`);
|
|
82
|
+
// Age out session/tool-output files now: an entirely stale dir is gone before the line
|
|
83
|
+
// pass reads it. A running role's ledger mtime stays young, so pruneOldFiles never ages
|
|
84
|
+
// that file out; each warning must name the unit it pruned.
|
|
68
85
|
const pruned = pruneOldFiles(sessions, days) + pruneOldFiles(toolOutputDir(root), days);
|
|
69
86
|
if (pruned > 0)
|
|
70
|
-
warnEventBestEffort(root,
|
|
87
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `pruned ${pruned} old agent session/tool-output file(s)`);
|
|
71
88
|
const prunedLines = pruneSessionLedgers(sessions, days);
|
|
72
89
|
if (prunedLines > 0)
|
|
73
|
-
warnEventBestEffort(root,
|
|
90
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `pruned ${prunedLines} stale session ledger line(s)`);
|
|
74
91
|
}
|
|
75
92
|
}
|