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,14 +1,32 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import { loadConfigSafe } from "../config/config.js";
|
|
4
|
+
import { CUSTOM_NAME_RE } from "../config/config-schema.js";
|
|
5
|
+
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
3
6
|
import { loopEnabled } from "../roles/loop-ids.js";
|
|
4
|
-
import { branchExists, currentBranch, deleteRef, isDirty, isMergedInto, refSha, targetBranch, } from "../git/git.js";
|
|
7
|
+
import { branchExists, countCommitsInRange, currentBranch, deleteRef, isDirty, isMergedInto, refSha, targetBranch, } from "../git/git.js";
|
|
5
8
|
import { errorMessage } from "../text/text.js";
|
|
6
9
|
import { loadLoopState } from "../loop/loop-state.js";
|
|
7
|
-
import { branchName,
|
|
10
|
+
import { branchName, mergeRefName, legacyLandingRefName, worktreePath } from "../paths.js";
|
|
8
11
|
import { isUsableWorktree, removeWorktree } from "../git/worktree.js";
|
|
9
12
|
import { roleWorktreeDir, slotForDir, updateSlotsState } from "../git/slots-state.js";
|
|
10
|
-
import { resumeRole } from "../
|
|
13
|
+
import { pausedRoles, resumeRole } from "../team/team-state.js";
|
|
11
14
|
import { git } from "../git/git-run.js";
|
|
15
|
+
/** `tumwater retire --role <id>`: remove a disabled loop's persistent worktree and branch, plus the
|
|
16
|
+
* per-role merge ref and paused-state marker that outlive it. The safety rails and the `--force`
|
|
17
|
+
* override live here, so the CLI layer (operator/operator-commands.ts) stays a thin renderer. */
|
|
18
|
+
/** The role's merge pin, falling back to the legacy-named one a not-yet-migrated repo holds. */
|
|
19
|
+
async function mergeRefSha(root, role) {
|
|
20
|
+
return (await refSha(root, mergeRefName(role))) ?? (await refSha(root, legacyLandingRefName(role)));
|
|
21
|
+
}
|
|
22
|
+
/** The id gate for ids no config names (a lowered `instances`, a removed custom role): the id is
|
|
23
|
+
* well-formed (CUSTOM_NAME_RE keeps `..` and `/` out of the paths retire builds), is not the
|
|
24
|
+
* director, and still has its branch or merge ref. Read-only. */
|
|
25
|
+
export async function hasRetirableLeftovers(root, role) {
|
|
26
|
+
if (!CUSTOM_NAME_RE.test(role) || role === DIRECTOR_ROLE)
|
|
27
|
+
return false;
|
|
28
|
+
return (await branchExists(root, branchName(role))) || (await mergeRefSha(root, role)) !== null;
|
|
29
|
+
}
|
|
12
30
|
/** Collect what exists today for `role`, with every probe tolerant of partial state — a retired
|
|
13
31
|
* role's leftovers are exactly the messy half-state this command exists to clean up. */
|
|
14
32
|
export async function collectRetire(root, role) {
|
|
@@ -25,51 +43,51 @@ export async function collectRetire(root, role) {
|
|
|
25
43
|
throw new Error(`cannot read tumwater.json: ${errorMessage(config.error)}`);
|
|
26
44
|
}
|
|
27
45
|
const mainBranch = targetBranch(config.config.baseBranch, await currentBranch(root));
|
|
28
|
-
// Count
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
46
|
+
// Count unmerged work from the branch ref at root, not from the worktree's HEAD: the worktree may
|
|
47
|
+
// be gone or unusable (the messy half-state this command exists to clean up) while the branch
|
|
48
|
+
// still holds commits — reading HEAD through the worktree would report 0 and let retireRole's
|
|
49
|
+
// `branch -D` delete unmerged work without objection. When the branch is absent, or a usable
|
|
50
|
+
// worktree sits on it, the rev-list range is empty or matches HEAD.
|
|
33
51
|
const branch = branchName(role);
|
|
34
52
|
const branchThere = await branchExists(root, branch);
|
|
35
53
|
const ahead = branchThere
|
|
36
|
-
?
|
|
54
|
+
? await countCommitsInRange(root, `${mainBranch}..${branch}`)
|
|
37
55
|
: 0;
|
|
38
56
|
const dirty = usable && wt !== null ? await isDirty(wt) : false;
|
|
39
|
-
const
|
|
40
|
-
// A
|
|
41
|
-
//
|
|
42
|
-
const
|
|
57
|
+
const mergeRef = await mergeRefSha(root, role);
|
|
58
|
+
// A merge ref pins a committed-but-unmerged sha (plans/merge-queue.md invariant 4): count it as
|
|
59
|
+
// unmerged work too — the branch can sit at main while the pin holds the crash survivor.
|
|
60
|
+
const pinnedUnmerged = mergeRef !== null && !(await isMergedInto(root, mergeRef, mainBranch)) ? 1 : 0;
|
|
43
61
|
return {
|
|
44
62
|
role,
|
|
45
63
|
worktreePresent: present,
|
|
46
64
|
worktreeUsable: usable,
|
|
47
65
|
branchPresent: branchThere,
|
|
48
|
-
|
|
49
|
-
aheadOfMain: ahead +
|
|
66
|
+
mergeRefPresent: mergeRef !== null,
|
|
67
|
+
aheadOfMain: ahead + pinnedUnmerged,
|
|
50
68
|
dirty,
|
|
51
69
|
enabledInConfig: loopEnabled(config.config, role),
|
|
52
|
-
|
|
70
|
+
midRun: loadLoopState(root, role).running === true,
|
|
53
71
|
};
|
|
54
72
|
}
|
|
55
73
|
/** The artifacts retire manages, in removal order. */
|
|
56
|
-
const ARTIFACTS = ["worktree", "branch", "
|
|
74
|
+
const ARTIFACTS = ["worktree", "branch", "mergeRef", "pausedMarker"];
|
|
57
75
|
/** The safety rails: each returns a one-line objection, or null when the state is clean. */
|
|
58
76
|
function objections(status) {
|
|
59
77
|
const reasons = [];
|
|
60
78
|
if (status.enabledInConfig)
|
|
61
79
|
reasons.push(`role ${status.role} is still enabled in tumwater.json — disable it first`);
|
|
62
80
|
if (status.aheadOfMain > 0)
|
|
63
|
-
reasons.push(`branch ${branchName(status.role)} holds ${status.aheadOfMain}
|
|
81
|
+
reasons.push(`branch ${branchName(status.role)} holds ${status.aheadOfMain} unmerged commit(s) — merge or discard them first`);
|
|
64
82
|
if (status.worktreePresent && !status.worktreeUsable)
|
|
65
83
|
reasons.push("the worktree is unusable and cannot be checked for uncommitted changes — remove it manually or use --force");
|
|
66
84
|
if (status.dirty)
|
|
67
85
|
reasons.push("the worktree has uncommitted changes");
|
|
68
|
-
if (status.
|
|
69
|
-
reasons.push("a
|
|
86
|
+
if (status.midRun)
|
|
87
|
+
reasons.push("a run is in progress for this role");
|
|
70
88
|
return reasons;
|
|
71
89
|
}
|
|
72
|
-
/** Remove the role's worktree, branch,
|
|
90
|
+
/** Remove the role's worktree, branch, merge ref, and paused-state marker. Refuses — removing
|
|
73
91
|
* nothing — when any safety rail objects, unless `force` overrides every objection. */
|
|
74
92
|
export async function retireRole(root, role, { force }) {
|
|
75
93
|
const status = await collectRetire(root, role);
|
|
@@ -117,15 +135,32 @@ export async function retireRole(root, role, { force }) {
|
|
|
117
135
|
}
|
|
118
136
|
else
|
|
119
137
|
mark("branch", false);
|
|
120
|
-
if (status.
|
|
121
|
-
await deleteRef(root,
|
|
122
|
-
|
|
138
|
+
if (status.mergeRefPresent) {
|
|
139
|
+
await deleteRef(root, mergeRefName(status.role));
|
|
140
|
+
await deleteRef(root, legacyLandingRefName(status.role)); // a team stopped before its first start here
|
|
141
|
+
mark("mergeRef", true);
|
|
123
142
|
}
|
|
124
143
|
else
|
|
125
|
-
mark("
|
|
144
|
+
mark("mergeRef", false);
|
|
126
145
|
mark("pausedMarker", resumeRole(root, status.role));
|
|
127
146
|
return { status, removed, skipped };
|
|
128
147
|
}
|
|
148
|
+
/** Preview `retireRole` without touching the worktree, refs, slot state, or paused marker. */
|
|
149
|
+
export async function previewRetire(root, role) {
|
|
150
|
+
const status = await collectRetire(root, role);
|
|
151
|
+
const there = {
|
|
152
|
+
worktree: status.worktreePresent,
|
|
153
|
+
branch: status.branchPresent,
|
|
154
|
+
mergeRef: status.mergeRefPresent,
|
|
155
|
+
pausedMarker: pausedRoles(root).includes(role),
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
status,
|
|
159
|
+
wouldRemove: ARTIFACTS.filter((a) => there[a]),
|
|
160
|
+
skipped: ARTIFACTS.filter((a) => !there[a]),
|
|
161
|
+
objections: objections(status),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
129
164
|
/** The one-line human description of an artifact name, for the CLI renderer. */
|
|
130
165
|
export function artifactPhrase(name, role) {
|
|
131
166
|
switch (name) {
|
|
@@ -135,8 +170,8 @@ export function artifactPhrase(name, role) {
|
|
|
135
170
|
return `the worktree`;
|
|
136
171
|
case "branch":
|
|
137
172
|
return `the branch (${branchName(role)})`;
|
|
138
|
-
case "
|
|
139
|
-
return `the
|
|
173
|
+
case "mergeRef":
|
|
174
|
+
return `the merge ref (${mergeRefName(role)})`;
|
|
140
175
|
case "pausedMarker":
|
|
141
176
|
return `the paused-state marker`;
|
|
142
177
|
default:
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
import { loadConfigSafe } from "../config/config.js";
|
|
3
|
+
import { withStateLockAsync } from "../concurrency/lock.js";
|
|
4
|
+
import { currentBranch, headOf, isMergedInto, targetBranch } from "../git/git.js";
|
|
5
|
+
import { COMMIT_IDENT, gitTry } from "../git/git-run.js";
|
|
6
|
+
import { stampedSubject } from "../git/commit-message.js";
|
|
7
|
+
import { primaryWorktreeLockPath } from "../paths.js";
|
|
8
|
+
import { shortSha } from "../text/format.js";
|
|
9
|
+
/** `tumwater revert <sha>`: undo one merged change on main, committing the inverse as
|
|
10
|
+
* `tumwater(operator): Revert <short sha>` on the primary checkout. Mirrors
|
|
11
|
+
* `commitOperatorBacklogEdit`'s lock-and-branch discipline (src/backlog/backlog-commit.ts): the
|
|
12
|
+
* inverse commit lands under the primary-worktree lock so it cannot interleave with a
|
|
13
|
+
* fast-forward, and it refuses — writing nothing — when the primary is not on the base branch.
|
|
14
|
+
* Fail-closed: an unknown ref, a ref not on the base branch, or a revert that conflicts writes
|
|
15
|
+
* no commit and leaves the checkout as it was. */
|
|
16
|
+
/** Revert one already-merged commit (`ref` resolved to a commit) and commit the inverse on the
|
|
17
|
+
* primary checkout under the primary-worktree lock. Never throws; every refusal and every git
|
|
18
|
+
* failure comes back as `{ ok: false, error }` after restoring the checkout. `head` is captured
|
|
19
|
+
* before the revert so a future caller can compare; the resolved full sha is what the commit
|
|
20
|
+
* message shortens. */
|
|
21
|
+
export async function revertMergedCommit(root, ref) {
|
|
22
|
+
try {
|
|
23
|
+
// Yield first: a synchronous caller (the TUI key handler) may write again right after this
|
|
24
|
+
// call, and that write takes the same lock synchronously — a lock already held across our
|
|
25
|
+
// awaits would block its event loop until the lock times out.
|
|
26
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
27
|
+
return await withStateLockAsync(primaryWorktreeLockPath(root), async () => {
|
|
28
|
+
const primary = await currentBranch(root);
|
|
29
|
+
const base = targetBranch(loadConfigSafe(root).config?.baseBranch, primary);
|
|
30
|
+
if (primary === null || base !== primary)
|
|
31
|
+
return { ok: false, error: `the primary checkout is not on ${base} — run from a checkout of it` };
|
|
32
|
+
const full = await gitTry(root, "rev-parse", "--verify", ref + "^{commit}");
|
|
33
|
+
if (full === null)
|
|
34
|
+
return { ok: false, error: `no such commit: ${ref}` };
|
|
35
|
+
// `head` is captured before the revert so a future caller can compare before/after.
|
|
36
|
+
const head = await headOf(root, "HEAD");
|
|
37
|
+
if (!(await isMergedInto(root, full, primary)))
|
|
38
|
+
return { ok: false, error: `${shortSha(full)} is not on ${primary}` };
|
|
39
|
+
// Fail-closed on any staged or unstaged change to a tracked path: we refuse to revert a
|
|
40
|
+
// checkout that has one (untracked files are tolerated — the lock dir we just created is
|
|
41
|
+
// one). This keeps the inverse commit free of unrelated content and, just as important,
|
|
42
|
+
// makes `git revert --abort` below safe to run after a conflict — with nothing dirty in
|
|
43
|
+
// the index or tree there is nothing for its hard reset to discard, so the operand's
|
|
44
|
+
// pre-existing work can never be lost.
|
|
45
|
+
if ((await gitTry(root, "status", "--porcelain", "--untracked-files=no")) !== "")
|
|
46
|
+
return { ok: false, error: "the worktree has staged or unstaged changes; commit or stash them first, nothing was committed" };
|
|
47
|
+
if ((await gitTry(root, "revert", "--no-commit", full)) === null) {
|
|
48
|
+
// The tree was clean when the revert started, so --abort restores exactly that state.
|
|
49
|
+
await gitTry(root, "revert", "--abort");
|
|
50
|
+
return { ok: false, error: `reverting ${shortSha(full)} conflicted; nothing was committed and the checkout is unchanged` };
|
|
51
|
+
}
|
|
52
|
+
const commitMsg = stampedSubject("operator", "Revert " + shortSha(full)); // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
|
|
53
|
+
// Mirror commitOperatorBacklogEdit: `--only` with the revert's own staged paths commits
|
|
54
|
+
// just the inverse, so it cannot pick up any unrelated change of another path staged
|
|
55
|
+
// after the clean check above. `--no-renames` keeps a rename's two halves as separate
|
|
56
|
+
// path lines: the default name-only collapses a detected rename to its destination, which
|
|
57
|
+
// would commit only half and leave the other half staged with a dirty tree.
|
|
58
|
+
const pathsSpec = ((await gitTry(root, "diff", "--cached", "--name-only", "--no-renames")) ?? "")
|
|
59
|
+
.split("\n")
|
|
60
|
+
.filter((p) => p.length > 0);
|
|
61
|
+
if ((await gitTry(root, ...COMMIT_IDENT, "commit", "--only", "-m", commitMsg, "--", ...pathsSpec)) === null) {
|
|
62
|
+
await gitTry(root, "revert", "--abort");
|
|
63
|
+
return { ok: false, error: `the revert of ${shortSha(full)} did not commit; nothing was committed` };
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, headSha: await headOf(root, "HEAD"), beforeSha: head };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { ok: false, error: "the revert did not complete (git failed); nothing was committed" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** The orchestrator's launch pass: the half of the per-poll runner sweep that admits due
|
|
2
3
|
* roles to their ticks. The decision half (isEligible, the pause/backoff/deferral gates,
|
|
3
4
|
* once-mode settling) stays in runOrchestrator (src/orchestrator/orchestrator.ts) — it reads
|
|
@@ -12,21 +13,28 @@
|
|
|
12
13
|
* in and mutated in place, exactly as the inline version did: the drain's `finally` and the
|
|
13
14
|
* redeployer read it between polls. */
|
|
14
15
|
import { abandonFallbackProbeAt, recordFallbackTickAt, startFallbackProbeAt, } from "../budget/fallback-breaker.js";
|
|
15
|
-
import {
|
|
16
|
+
import { seamHoldKey } from "../config/config-views.js";
|
|
17
|
+
import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
16
18
|
import { errorMessage } from "../text/text.js";
|
|
17
19
|
import { fairOrder } from "../scheduling/scheduling.js";
|
|
18
20
|
import { modelPairName } from "../budget/budget.js";
|
|
21
|
+
import { withProviderLease } from "../concurrency/provider-lease.js";
|
|
22
|
+
import { seamConcurrencyKey } from "../config/config-views.js";
|
|
19
23
|
import { DIRECTOR_ROLE, roleTier } from "../roles/roles.js";
|
|
20
24
|
import { runTimedRoleTick } from "../tick/tick-timing.js";
|
|
21
25
|
/** How many recent work-bearing tick durations the p75 sample keeps (drainInFlightWork's
|
|
22
|
-
*
|
|
26
|
+
* HANDOFF_MERGE_WINDOW_MS input): the drain window tracks recent tick pace, not all-time
|
|
23
27
|
* pace, so the sample is a ring buffer. */
|
|
24
28
|
export const ROLE_TICK_DURATION_SAMPLES = 50;
|
|
29
|
+
/** The admission wait threshold below which a role tick's semaphore wait logs no event — a
|
|
30
|
+
* tick that starts at once stays silent. Mirrors SLOT_WAIT_EVENT_MS in src/git/worktree-pool.ts:
|
|
31
|
+
* the wait that signals maxConcurrentPerProvider (or maxConcurrent) caps the team. */
|
|
32
|
+
export const PROVIDER_WAIT_EVENT_MS = 30_000;
|
|
25
33
|
/** Launch one due runner's tick: reserve it, park it in the semaphore, and start it under
|
|
26
34
|
* runTimedRoleTick with the breaker-evidence wiring. Returns the task promise already added
|
|
27
35
|
* to its in-flight bucket (its `finally` self-removes and folds the duration sample). */
|
|
28
36
|
export function launchDueTicks(ctx) {
|
|
29
|
-
const { root, reasons, signal, gateStates, breakerPolicy, breakerNow, probePair, probeRoles, budgetActive, semaphore, rolePermitHolders, roleInFlight, directorInFlight, roleTickDurationsMs, startHeld, } = ctx;
|
|
37
|
+
const { root, reasons, signal, gateStates, breakerPolicy, breakerNow, probePair, probeRoles, budgetActive, semaphore, piDefault, rolePermitHolders, roleInFlight, directorInFlight, roleTickDurationsMs, startHeld, } = ctx;
|
|
30
38
|
let admittedProbe = false;
|
|
31
39
|
for (const runner of fairOrder([...reasons.keys()])) {
|
|
32
40
|
if (signal.aborted)
|
|
@@ -47,7 +55,7 @@ export function launchDueTicks(ctx) {
|
|
|
47
55
|
}
|
|
48
56
|
const reason = reasons.get(runner);
|
|
49
57
|
if (reason && reason !== "scheduled" && reason !== "startup") {
|
|
50
|
-
logEventBestEffort(root, {
|
|
58
|
+
logEventBestEffort(root, { source: runner.role, type: "wake", reason });
|
|
51
59
|
}
|
|
52
60
|
runner.state.running = true; // Reserve before the semaphore wait so we don't double-schedule.
|
|
53
61
|
// The director never queues behind role loops: a user prompt starts immediately,
|
|
@@ -58,8 +66,13 @@ export function launchDueTicks(ctx) {
|
|
|
58
66
|
// Mark the parked waiter while it waits: it holds no permit yet, so the dashboards
|
|
59
67
|
// render `awaiting slot` (an inactive state) and the active rows keep tracking
|
|
60
68
|
// maxConcurrent (BUGS.md 2026-09-24). Cleared the moment the permit is granted. The
|
|
61
|
-
// director never queues, so it is never a parked waiter.
|
|
69
|
+
// director never queues, so it is never a parked waiter. Its provider key rides beside
|
|
70
|
+
// `parkedSince` (provider-concurrency part 5/5): the key the admission lease will
|
|
71
|
+
// acquire, computed from the same runConfig admission uses, so a waiter named by a
|
|
72
|
+
// full key renders `awaiting <key> slot` on the dashboards.
|
|
73
|
+
const parkKey = runner.role === DIRECTOR_ROLE ? null : runner.runConfig(Date.now());
|
|
62
74
|
runner.state.parkedSince = usesSlot ? Date.now() : undefined;
|
|
75
|
+
runner.state.parkedOn = usesSlot && parkKey ? seamConcurrencyKey(parkKey, piDefault) : undefined;
|
|
63
76
|
// The tick's own run time (null when it never ran, was cut off, or ended without
|
|
64
77
|
// work): the drain-window sample is taken only for a work-bearing tick that finished
|
|
65
78
|
// on its own (runTimedRoleTick; BUGS.md 2026-09-30).
|
|
@@ -67,22 +80,61 @@ export function launchDueTicks(ctx) {
|
|
|
67
80
|
// Whether runner.tick() was ever called — false when the start gate (or a shutdown)
|
|
68
81
|
// turned the tick away at its permit.
|
|
69
82
|
let started = false;
|
|
83
|
+
// The tick's keyed semaphore lease (plans/provider-concurrency.md §4): acquired on the
|
|
84
|
+
// authoring seam's provider key and released in place of the bare team release, so the
|
|
85
|
+
// cap is real for role ticks from part 2/5 on. The director never queues, so its lease is
|
|
86
|
+
// never created here.
|
|
87
|
+
let lease = null;
|
|
88
|
+
// The director's forced provider lease (plans/provider-concurrency.md §4): never queues and
|
|
89
|
+
// never counts toward maxConcurrent, but DOES count toward its provider key while its tick
|
|
90
|
+
// runs — so a full provider admits no further grants until the director releases.
|
|
91
|
+
let directorLease = null;
|
|
70
92
|
const task = (async () => {
|
|
71
93
|
durationMs = await runTimedRoleTick(signal, usesSlot
|
|
72
94
|
? async () => {
|
|
73
|
-
|
|
95
|
+
// The key is the authoring seam's concurrency key, computed here at admission
|
|
96
|
+
// (runConfig applies any budget or model-failure fallback already in force).
|
|
97
|
+
const cfg = runner.role === DIRECTOR_ROLE ? null : runner.runConfig(Date.now());
|
|
98
|
+
const key = cfg ? seamConcurrencyKey(cfg, piDefault) : undefined;
|
|
99
|
+
const admittedAt = Date.now();
|
|
100
|
+
lease = await semaphore.acquireLease(roleTier(runner.role), key);
|
|
101
|
+
// A wait of PROVIDER_WAIT_EVENT_MS or more for the provider/team slot is
|
|
102
|
+
// provider contention worth surfacing: name it so `tumwater doctor`'s provider
|
|
103
|
+
// contention row can sum it and the operator can size maxConcurrentPerProvider
|
|
104
|
+
// (or maxConcurrent) from real waits. Best-effort like the launch pass's other
|
|
105
|
+
// events, so an unwritable log never blocks a start; a tick admitted at once
|
|
106
|
+
// stays silent.
|
|
107
|
+
if (Date.now() - admittedAt >= (ctx.providerWaitMs ?? PROVIDER_WAIT_EVENT_MS)) {
|
|
108
|
+
logEventBestEffort(ctx.root, {
|
|
109
|
+
source: TUMWATER_SOURCE,
|
|
110
|
+
type: "provider_wait",
|
|
111
|
+
role: runner.role,
|
|
112
|
+
waitedMs: Date.now() - admittedAt,
|
|
113
|
+
...(key !== undefined ? { key } : {}),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
74
116
|
// Permit granted: the tick is now an active, permit-holding state until the
|
|
75
117
|
// release below. A tick the start gate turns away releases within the same
|
|
76
118
|
// microtask chain, so no poll ever counts it as a permit holder.
|
|
77
119
|
runner.state.parkedSince = undefined;
|
|
120
|
+
runner.state.parkedOn = undefined;
|
|
78
121
|
rolePermitHolders.add(runner);
|
|
79
122
|
}
|
|
80
|
-
: async () => {
|
|
123
|
+
: async () => {
|
|
124
|
+
// The director wraps its tick in a forced lease on its run's provider key, never
|
|
125
|
+
// queuing behind role loops: a user prompt starts at once even on a full provider,
|
|
126
|
+
// and the key's in-use rises by one — overshooting by one — so no further grant on
|
|
127
|
+
// that key happens until its release drains it back under the cap.
|
|
128
|
+
const cfg = runner.runConfig(Date.now());
|
|
129
|
+
directorLease = semaphore.forceLease(cfg ? seamConcurrencyKey(cfg, piDefault) : undefined);
|
|
130
|
+
}, usesSlot
|
|
81
131
|
? () => {
|
|
82
132
|
rolePermitHolders.delete(runner);
|
|
83
|
-
|
|
133
|
+
lease.release();
|
|
84
134
|
}
|
|
85
|
-
: () => {
|
|
135
|
+
: () => {
|
|
136
|
+
directorLease.release();
|
|
137
|
+
}, async () => {
|
|
86
138
|
started = true;
|
|
87
139
|
// A role tick that starts while the budget holds runs on the fallback pair its tier
|
|
88
140
|
// resolved to (the gate's per-tier view was installed in its config), or — when a
|
|
@@ -94,11 +146,19 @@ export function launchDueTicks(ctx) {
|
|
|
94
146
|
// part 1/2").
|
|
95
147
|
const now = Date.now();
|
|
96
148
|
const cfg = runner.role === DIRECTOR_ROLE ? null : runner.runConfig(now);
|
|
149
|
+
// The breaker is keyed by the seam that ran (seamHoldKey), like the tick pair and
|
|
150
|
+
// the team-hold stamps (PLANS.md "Mixed claude + pi fleets, part 2/4"): a claude
|
|
151
|
+
// fallback tick demotes the `claude/<model>` breaker entry, never `?/…`.
|
|
97
152
|
const ranPair = budgetActive && cfg?.model !== undefined
|
|
98
|
-
? modelPairName({ provider: cfg
|
|
153
|
+
? modelPairName({ provider: seamHoldKey(cfg), model: cfg.model })
|
|
99
154
|
: null;
|
|
100
155
|
const ranOn = ranPair !== null ? gateStates.budget.breakers[ranPair] : undefined;
|
|
101
|
-
|
|
156
|
+
// The tick's lease rides in the provider-lease store for the length of the tick, so
|
|
157
|
+
// every run it makes re-keys it to the provider that run actually uses
|
|
158
|
+
// (plans/provider-concurrency.md §5) — a budget or model-failure switch that moves a
|
|
159
|
+
// mid-tick run to another provider moves the lease with it. The director's forced
|
|
160
|
+
// lease re-keys the same way, and its rekey never waits.
|
|
161
|
+
const outcome = await withProviderLease(runner.role === DIRECTOR_ROLE ? directorLease : lease, semaphore, () => runner.tick(), { piDefault });
|
|
102
162
|
if (ranPair !== null && ranOn !== undefined) {
|
|
103
163
|
const at = breakerNow();
|
|
104
164
|
const evidence = outcome.recoveredLeftover ? "skipped" : outcome.result;
|
|
@@ -123,7 +183,7 @@ export function launchDueTicks(ctx) {
|
|
|
123
183
|
// A reservation whose tick never started hands itself back, so the role re-schedules
|
|
124
184
|
// once the hold lifts (or on the new build) instead of sitting `running` forever with
|
|
125
185
|
// nothing in flight. Memory only, on purpose: nothing started, so no state write and
|
|
126
|
-
// no
|
|
186
|
+
// no run_start/run_end — the persisted state still reads exactly as the last real
|
|
127
187
|
// tick left it, and the next generation schedules the role from that.
|
|
128
188
|
if (!started)
|
|
129
189
|
runner.state.running = false;
|
|
@@ -138,14 +198,14 @@ export function launchDueTicks(ctx) {
|
|
|
138
198
|
// should resolve. A throw outside that fold (a finalize-time file write, a runner bug)
|
|
139
199
|
// rejects it instead; the drain awaits the bucket with allSettled, but chaining cleanup
|
|
140
200
|
// off a bare `void task.finally(...)` would leave the derived promise's rejection
|
|
141
|
-
// unhandled and take the whole
|
|
201
|
+
// unhandled and take the whole team down. Surface it as a warning (warnEventBestEffort,
|
|
142
202
|
// which keeps a failed warning write from becoming that unhandled rejection) and hand the
|
|
143
203
|
// reservation back so the role can schedule again, exactly as the unstarted path above
|
|
144
204
|
// does; the drain's allSettled is otherwise silent about the failure.
|
|
145
205
|
void task
|
|
146
206
|
.catch((err) => {
|
|
147
207
|
runner.state.running = false;
|
|
148
|
-
warnEventBestEffort(root, runner.role, `
|
|
208
|
+
warnEventBestEffort(root, runner.role, `run task rejected: ${errorMessage(err)}`);
|
|
149
209
|
})
|
|
150
210
|
.finally(() => {
|
|
151
211
|
bucket.delete(task);
|