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
|
@@ -7,20 +7,22 @@ import { newMaintenanceQuotaGateState, pollMaintenanceQuotaGate, } from "./maint
|
|
|
7
7
|
import { newBootstrapGateState, pollBootstrapGate, } from "./bootstrap-gates.js";
|
|
8
8
|
import { newQuietHoursGateState, pollQuietHoursGate, roleQuietHold, } from "../scheduling/quiet-hours.js";
|
|
9
9
|
import { BYTES_PER_GB, newDiskGateState, pollDiskGate, sampleFreeBytes, } from "./disk-gate.js";
|
|
10
|
-
import { pollErrorStorm, pollFailureSpread,
|
|
10
|
+
import { pollErrorStorm, pollFailureSpread, pollTeamHold, pollClaudeWindowWarning, pollClaudeWindowReserve } from "../team/team-polls.js";
|
|
11
11
|
import { readClaudeAccount } from "../agent/claude/claude-account.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { standingHolds } from "../team/team-hold.js";
|
|
13
|
+
import { ERROR_STORM_QUIET } from "../team/error-storm.js";
|
|
14
|
+
import { FAILURE_SPREAD_QUIET } from "../team/failure-spread.js";
|
|
14
15
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
15
|
-
import { baseRoleOf, loopEnabled } from "../roles/loop-ids.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
16
|
+
import { baseRoleOf, instancePauseKey, loopEnabled } from "../roles/loop-ids.js";
|
|
17
|
+
import { queuedMergeRoles } from "../merge/merge-queue.js";
|
|
18
|
+
import { MODEL_TIERS } from "../config/config-schema.js";
|
|
19
|
+
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
20
|
+
import { publishInfoFields } from "../team/orchestrator-info.js";
|
|
19
21
|
import { roleSeamTier } from "../config/config-views.js";
|
|
20
|
-
/** A fresh gate family: nothing seen yet, so the first poll of an unpaused, unheld
|
|
22
|
+
/** A fresh gate family: nothing seen yet, so the first poll of an unpaused, unheld team
|
|
21
23
|
* logs nothing. The budget gate seeds its breaker and derived fallback view from the
|
|
22
24
|
* startup config; a live reload replaces the config on the next poll. */
|
|
23
|
-
export function
|
|
25
|
+
export function newTeamGateStates(config) {
|
|
24
26
|
return {
|
|
25
27
|
budget: newBudgetGateState(config),
|
|
26
28
|
pause: newPauseGateState(),
|
|
@@ -30,20 +32,21 @@ export function newFleetGateStates(config) {
|
|
|
30
32
|
bootstrap: newBootstrapGateState(),
|
|
31
33
|
quiet: newQuietHoursGateState(),
|
|
32
34
|
disk: newDiskGateState(),
|
|
33
|
-
|
|
35
|
+
teamHold: new Map(),
|
|
34
36
|
claudeWindowWarned: null,
|
|
37
|
+
claudeWindowReserved: null,
|
|
35
38
|
errorStorm: ERROR_STORM_QUIET,
|
|
36
39
|
failureSpread: FAILURE_SPREAD_QUIET,
|
|
37
40
|
};
|
|
38
41
|
}
|
|
39
|
-
/** Poll every
|
|
40
|
-
export function
|
|
42
|
+
/** Poll every team-wide gate and alarm, advancing `states` in place. */
|
|
43
|
+
export function pollTeamGates(states, ctx) {
|
|
41
44
|
const { root, runners, liveConfig, modelsPath, now, info } = ctx;
|
|
42
45
|
// Daily cost budget gate (src/gates/budget-gates.ts owns the reads, the edge-triggered
|
|
43
46
|
// budget_* events, the breaker re-key, and the fallback config view): the orchestrator
|
|
44
47
|
// owns only the wiring — the demotion publish below and the per-runner assignment at
|
|
45
48
|
// the bottom of this block.
|
|
46
|
-
const { gate, roleConfig, spentUsd, capUsd, resumed: gateResumed, pairs: fallbackPairs, priceResolved, servingResolved, budgetActive, } = pollBudgetGate(states.budget, {
|
|
49
|
+
const { gate, roleConfig, spentUsd, capUsd, onFallback, resumed: gateResumed, pairs: fallbackPairs, priceResolved, servingResolved, budgetActive, windowTiers, windowResumed, directorConfig, chain, } = pollBudgetGate(states.budget, {
|
|
47
50
|
root,
|
|
48
51
|
states: runners.map((r) => r.state),
|
|
49
52
|
liveConfig,
|
|
@@ -66,36 +69,68 @@ export function pollFleetGates(states, ctx) {
|
|
|
66
69
|
}
|
|
67
70
|
const engagedDemotion = states.budget.engaged !== null ? demotions[states.budget.engaged] : undefined;
|
|
68
71
|
const budget = { spentUsd, capUsd };
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
publishInfoFields(root, info, [
|
|
73
|
+
["fallbackDemotions", demotions],
|
|
74
|
+
["fallbackDemoted", engagedDemotion],
|
|
75
|
+
["budget", budget],
|
|
76
|
+
["chain", Object.keys(chain).length > 0 ? chain : undefined],
|
|
77
|
+
]);
|
|
78
|
+
// One fallback_switched per poll in which a tier's pair or step changed (a return to the
|
|
79
|
+
// primary reads `to: null`, reason "available").
|
|
80
|
+
const switched = {};
|
|
81
|
+
const prevChain = states.budget.prevChain;
|
|
82
|
+
for (const tier of MODEL_TIERS) {
|
|
83
|
+
const was = prevChain[tier];
|
|
84
|
+
const cur = chain[tier];
|
|
85
|
+
if (was?.pair === cur?.pair && was?.step === cur?.step)
|
|
86
|
+
continue;
|
|
87
|
+
switched[tier] = { from: was?.pair ?? null, to: cur?.pair ?? null, reason: cur?.reason ?? "available" };
|
|
88
|
+
}
|
|
89
|
+
if (Object.keys(switched).length > 0)
|
|
90
|
+
logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "fallback_switched", tiers: switched });
|
|
91
|
+
states.budget.prevChain = chain;
|
|
74
92
|
// The director keeps the live config — an explicit human prompt outranks the
|
|
75
93
|
// autonomous-spend cap (gateRoleConfig). Assigned every poll (not only on transitions)
|
|
76
94
|
// so a runner created mid-gate, or one left behind by a broken-file poll that skipped
|
|
77
95
|
// the reload, can never tick on the wrong model.
|
|
78
|
-
for (const r of runners)
|
|
79
|
-
r.config = gateRoleConfig(r.role, liveConfig, roleConfig);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// the
|
|
89
|
-
|
|
96
|
+
for (const r of runners) {
|
|
97
|
+
r.config = gateRoleConfig(r.role, liveConfig, roleConfig, directorConfig);
|
|
98
|
+
// An image reroute must never spend past the daily cap: while the gate has the team on
|
|
99
|
+
// its free fallback pairs, a reroute looks only among those pairs (plans/image-input.md
|
|
100
|
+
// "Reroute target" "Regime"). Set beside the config assignment so the scheduler's regime
|
|
101
|
+
// and the loops' never disagree.
|
|
102
|
+
r.budgetFallbackActive = onFallback;
|
|
103
|
+
}
|
|
104
|
+
// The budget reopened: a tick that started on a fallback pair keeps it until it ends, so hand
|
|
105
|
+
// those ticks back — abort them resumably (session and worktree edits kept) and let their next
|
|
106
|
+
// tick continue the same session on the primary, whose config the assignment above already
|
|
107
|
+
// installed (PLANS.md 2026-09-30). Per-tier fallbacks put different roles on different pairs
|
|
108
|
+
// (part 5c/8), so the match runs over EVERY pair the price-based resolution hands out, not only
|
|
109
|
+
// one. A tick started on the primary keeps running, the director is exempt, and the merges (the
|
|
110
|
+
// slot's own runs) are untouched. One event per pair names who was handed back, so the resulting
|
|
111
|
+
// aborted ticks read as the budget reopening, not as unexplained failures. The subscription
|
|
112
|
+
// window reset (below the cap): the same hand-back for the pairs the window move used. A window
|
|
113
|
+
// that resets while the cap still holds hands back nothing.
|
|
114
|
+
const handBack = gateResumed || (windowResumed && !budgetActive);
|
|
115
|
+
if (handBack && fallbackPairs.length > 0) {
|
|
116
|
+
// A cap reopen while the window is still used up leaves the window's tiers on their
|
|
117
|
+
// fallback pairs: handing those runs back would abort them to restart on the same pair.
|
|
118
|
+
const stillMoved = Object.keys(windowTiers).flatMap((t) => {
|
|
119
|
+
const p = priceResolved[t].pair;
|
|
120
|
+
return p ? [p] : [];
|
|
121
|
+
});
|
|
90
122
|
for (const leftPair of fallbackPairs) {
|
|
123
|
+
if (stillMoved.some((p) => p.provider === leftPair.provider && p.model === leftPair.model))
|
|
124
|
+
continue;
|
|
91
125
|
const matching = runners.filter((r) => r.role !== DIRECTOR_ROLE && tickOnPair(r.tickModel(), leftPair));
|
|
92
126
|
if (matching.length > 0) {
|
|
93
127
|
logEventBestEffort(root, {
|
|
94
|
-
|
|
128
|
+
source: TUMWATER_SOURCE,
|
|
95
129
|
type: "budget_handback",
|
|
96
130
|
roles: matching.map((r) => r.role),
|
|
97
131
|
provider: leftPair.provider,
|
|
98
132
|
model: leftPair.model,
|
|
133
|
+
...(gateResumed ? {} : { reason: "window" }),
|
|
99
134
|
});
|
|
100
135
|
for (const r of matching)
|
|
101
136
|
r.handBackTick();
|
|
@@ -103,8 +138,8 @@ export function pollFleetGates(states, ctx) {
|
|
|
103
138
|
}
|
|
104
139
|
}
|
|
105
140
|
// The pause gates (src/gates/pause-gates.ts owns the reads, the edge-triggered pause/resume
|
|
106
|
-
// events, and the cross-poll bookkeeping): the operator pause's marker and the per-role
|
|
107
|
-
//
|
|
141
|
+
// events, and the cross-poll bookkeeping): the operator pause's marker and the per-role pause's
|
|
142
|
+
// set, both read fresh per cycle so a marker change takes effect on the next poll.
|
|
108
143
|
const { userPaused, pausedRoles } = pollPauseGates(root, states.pause);
|
|
109
144
|
// The error-streak circuit breaker (src/gates/streak-gate.ts): a role past ERROR_STREAK_BREAKER
|
|
110
145
|
// consecutive failed ticks is paused through the same per-role marker the operator's
|
|
@@ -112,7 +147,7 @@ export function pollFleetGates(states, ctx) {
|
|
|
112
147
|
// director is not exempt; a failing director cannot process prompts anyway. Roles it just
|
|
113
148
|
// paused fold into this poll's paused-roles view, so the scheduler blocks them on this very
|
|
114
149
|
// poll instead of the next one.
|
|
115
|
-
const streakPaused = pollStreakGate(root, states.streak, runners, pausedRoles);
|
|
150
|
+
const streakPaused = pollStreakGate(root, states.streak, runners, pausedRoles, (role) => instancePauseKey(liveConfig, role));
|
|
116
151
|
// pollPauseGates advanced its edge-triggered bookkeeping BEFORE the breaker wrote its
|
|
117
152
|
// marker, so without this fold the NEXT poll would log a generic role_paused for a pause
|
|
118
153
|
// the breaker already announced as role_streak_paused — one pause, two events, the second
|
|
@@ -143,21 +178,20 @@ export function pollFleetGates(states, ctx) {
|
|
|
143
178
|
// pause gates' edge bookkeeping never sees it. A live config edit applies on the next poll;
|
|
144
179
|
// local midnight lifts the verdict by itself.
|
|
145
180
|
const capPaused = pollRoleCapGate(root, states.cap, runners, liveConfig.maxDailyCostUsdPerRole, now);
|
|
146
|
-
// The maintenance allowance (plans/work-ratio.md, part 1b/4): code-maintenance
|
|
147
|
-
// rolling 24 h window are capped at `
|
|
148
|
-
// MAINTENANCE_DAILY_FLOOR`.
|
|
181
|
+
// The maintenance allowance (plans/work-ratio.md, part 1b/4): code-maintenance merges in a
|
|
182
|
+
// rolling 24 h window are capped at `maintenancePerWorkMerge × work + MAINTENANCE_DAILY_FLOOR`.
|
|
149
183
|
// The gate counts the window, adds the maintenance loops already in flight (running a tick or
|
|
150
|
-
// holding a queued
|
|
184
|
+
// holding a queued merge, so six concurrent permits cannot overshoot), and logs the one
|
|
151
185
|
// hold/resume event per transition. Enabled only: a disabled loop runs no tick to count. The
|
|
152
186
|
// scheduling pass spends the returned headroom one due maintenance loop at a time, beside
|
|
153
187
|
// capPaused, with the fresh-wake/queued-prompt override the need-based deferral honors.
|
|
154
|
-
const queuedRoles =
|
|
188
|
+
const queuedRoles = queuedMergeRoles(root);
|
|
155
189
|
const maintenanceQuota = pollMaintenanceQuotaGate(root, states.quota, runners.map((r) => ({
|
|
156
190
|
role: r.role,
|
|
157
191
|
enabled: loopEnabled(r.config, r.role),
|
|
158
192
|
running: r.state.running === true,
|
|
159
|
-
|
|
160
|
-
})), liveConfig.
|
|
193
|
+
hasQueuedMerge: queuedRoles.has(r.role),
|
|
194
|
+
})), liveConfig.maintenancePerWorkMerge, now);
|
|
161
195
|
// New-project bootstrap (src/gates/bootstrap-gates.ts, plans/work-ratio.md part 2/2): a fresh
|
|
162
196
|
// project's maintenance loops start no new ticks until `bootstrap.untilPlansDone` plans are
|
|
163
197
|
// Done. The gate owns the latch write and the one `bootstrap_complete` event; the scheduling
|
|
@@ -166,7 +200,7 @@ export function pollFleetGates(states, ctx) {
|
|
|
166
200
|
const bootstrapHeld = pollBootstrapGate(root, states.bootstrap, runners, liveConfig, now);
|
|
167
201
|
const bootstrapActive = states.bootstrap.active;
|
|
168
202
|
// Per-role quiet hours (src/scheduling/quiet-hours.ts): a role whose own quietHoursPerRole window
|
|
169
|
-
// covers `now` starts no new ticks — the
|
|
203
|
+
// covers `now` starts no new ticks — the team window's semantics scoped to one loop, the
|
|
170
204
|
// director exempt like every autonomous gate. Stateless, recomputed per poll exactly like
|
|
171
205
|
// capPaused: no crossing events, no state — a live config edit applies on the next poll
|
|
172
206
|
// by construction. A role held by BOTH windows is held once: the scheduling pass folds
|
|
@@ -209,8 +243,7 @@ export function pollFleetGates(states, ctx) {
|
|
|
209
243
|
held: diskHeld,
|
|
210
244
|
...(ctx.reclaim?.lastReclaim ? { lastReclaim: ctx.reclaim.lastReclaim } : {}),
|
|
211
245
|
};
|
|
212
|
-
|
|
213
|
-
writeOrchestratorInfoBestEffort(root, info);
|
|
246
|
+
publishInfoFields(root, info, [["disk", disk]]);
|
|
214
247
|
// Each runner as the two provider-failure polls read it (HoldInputs): the role, its two
|
|
215
248
|
// episodic fields, and the provider its NEXT tick will run on (runProvider — the tier
|
|
216
249
|
// fallback pair while a model-fallback episode is active, else the role's resolved config;
|
|
@@ -218,54 +251,69 @@ export function pollFleetGates(states, ctx) {
|
|
|
218
251
|
// read the same list, like holdObservations on the reducer side. Keying on the effective
|
|
219
252
|
// provider is what lets a storm on the fallback pair form its own hold while the abandoned
|
|
220
253
|
// primary stays clear (PLANS.md "Model failure fallback, part 1/2").
|
|
254
|
+
//
|
|
255
|
+
// Each stamp (lastRateLimit/lastBackendFailure) carries the hold key of the backend that
|
|
256
|
+
// ACTUALLY failed — a reviewer run folded through the author's loop stamps `claude`, not
|
|
257
|
+
// the pi author's provider — and the polls' observation mapper files each stamp under that
|
|
258
|
+
// key, falling back to runProvider here for a stamp without one (PLANS.md "Mixed claude +
|
|
259
|
+
// pi fleets, part 2/4"). The spreads below carry the stamps wholesale, provider included.
|
|
221
260
|
const holdInputs = (rs) => rs.map((r) => ({
|
|
222
261
|
role: r.role,
|
|
223
262
|
provider: r.runProvider(now),
|
|
224
263
|
...(r.lastRateLimit ? { lastRateLimit: r.lastRateLimit } : {}),
|
|
225
264
|
...(r.lastBackendFailure ? { lastBackendFailure: r.lastBackendFailure } : {}),
|
|
226
265
|
}));
|
|
227
|
-
//
|
|
228
|
-
// roles' runs have ended on the SAME provider AND failure kind within a short window —
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
// and the permit-time closures read the LATEST poll's map there, so a waiter granted its
|
|
246
|
-
// permit after a later poll sees that poll's world.
|
|
266
|
+
// Team-wide failure hold, per provider (src/team/team-hold.ts, PLANS.md 2026-10-05): once two
|
|
267
|
+
// roles' runs have ended on the SAME provider AND failure kind within a short window — 429s, or a
|
|
268
|
+
// connection, timeout, 5xx, or model-load backend failure — role loops on THAT provider start no
|
|
269
|
+
// new ticks, and the merge queue starts no new verify, whose reviewer run has no retry and would
|
|
270
|
+
// spend a review strike on the storm — until the hold re-opens at its own deadline (Retry-After
|
|
271
|
+
// honored on the rate-limit kind, doubling on a relapse, capped). Roles on a healthy provider
|
|
272
|
+
// keep ticking; a hold on the reviewer's provider (with the review gate on) still blocks
|
|
273
|
+
// everything, since nothing could merge — the orchestrator's scheduling and start passes derive
|
|
274
|
+
// that verdict from this map. The director's ticks are exempt, as under the budget gate and the
|
|
275
|
+
// operator pause: an explicit human prompt outranks an autonomous gate, one director run is not
|
|
276
|
+
// the concurrency that sustains a storm, its 429 runs keep the per-run transient retry
|
|
277
|
+
// (backend-failure kinds ride this hold alone), and a prompt its tick fails to fulfill goes back
|
|
278
|
+
// to the inbox. In-flight ticks finish; NEW ticks are gated at scheduling like both siblings, and
|
|
279
|
+
// a role tick already parked in the semaphore meets the same hold at its permit (the start gate)
|
|
280
|
+
// and hands its reservation back instead of starting into the storm. The verdicts are written
|
|
281
|
+
// back to states.teamHold — one hold per provider, lifted holds kept keyed for their relapse
|
|
282
|
+
// memory but never reading as held — and the permit-time closures read the LATEST poll's map
|
|
283
|
+
// there, so a waiter granted its permit after a later poll sees that poll's world.
|
|
247
284
|
const claudeAccount = readClaudeAccount(root, now);
|
|
248
|
-
states.
|
|
285
|
+
states.teamHold = pollTeamHold(root, states.teamHold, holdInputs(runners), now, claudeAccount, windowTiers, liveConfig.claudeWindowReserve);
|
|
286
|
+
// The standings hold, published for observers (PLANS.md 2026-10-17 "the dashboards and
|
|
287
|
+
// `tumwater status` name a standing provider failure hold"): `standingHolds` turns the holds
|
|
288
|
+
// map into the per-provider list the status snapshot and the alert banner name, the same
|
|
289
|
+
// omit-when-empty idiom as `chain`/`disk`, so an open team leaves the file untouched and a
|
|
290
|
+
// hold change writes once.
|
|
291
|
+
const providerHolds = standingHolds(states.teamHold);
|
|
292
|
+
publishInfoFields(root, info, [
|
|
293
|
+
["providerHolds", providerHolds.length > 0 ? providerHolds : undefined],
|
|
294
|
+
]);
|
|
295
|
+
states.claudeWindowReserved = pollClaudeWindowReserve(root, states.claudeWindowReserved, claudeAccount, now, liveConfig.claudeWindowReserve);
|
|
249
296
|
// The subscription usage window's warning is observational (the hold above is the gate): the
|
|
250
297
|
// first allowed_warning logs once per window, and the memory lives beside the holds map.
|
|
251
298
|
states.claudeWindowWarned = pollClaudeWindowWarning(root, states.claudeWindowWarned, claudeAccount, now);
|
|
252
|
-
//
|
|
299
|
+
// Team-wide error-storm warning (src/team/error-storm.ts): when several roles' tick streaks
|
|
253
300
|
// fail consecutively on one shared cause, each role's own "consecutive tick failures"
|
|
254
|
-
// warning still fires alone — this adds the one
|
|
301
|
+
// warning still fires alone — this adds the one team-level warning that names the
|
|
255
302
|
// cause (and the config knob, when the cause has one) instead of leaving the operator
|
|
256
303
|
// to diff 14 streak lines. Observational only: it gates nothing, so unlike the failure
|
|
257
304
|
// hold above there is no re-open event — the members' own recoveries tell that story.
|
|
258
305
|
states.errorStorm = pollErrorStorm(root, states.errorStorm, runners);
|
|
259
|
-
//
|
|
306
|
+
// Team-wide wide-shallow storm alarm (src/team/failure-spread.ts): when many roles each
|
|
260
307
|
// fail a few times on one provider failure kind, the streak bar needs one role deep,
|
|
261
308
|
// the error storm needs several roles deep, and the hold needs the failures close
|
|
262
309
|
// together — this counts raw failures of one kind across roles in a rolling window,
|
|
263
|
-
// so a degraded backend that fails the
|
|
310
|
+
// so a degraded backend that fails the team widely and shallowly still names itself.
|
|
264
311
|
// Observational only, like the error storm: it gates nothing.
|
|
265
312
|
states.failureSpread = pollFailureSpread(root, states.failureSpread, holdInputs(runners), now);
|
|
266
313
|
return {
|
|
267
314
|
gate,
|
|
268
315
|
roleConfig,
|
|
316
|
+
directorConfig,
|
|
269
317
|
userPaused,
|
|
270
318
|
pausedRoles: pausedRolesNow,
|
|
271
319
|
capPaused,
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
/** Work ratio 1a/4 and 1b/4 (plans/work-ratio.md, "Maintenance follows work"): the arithmetic,
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
1
|
+
/** Work ratio 1a/4 and 1b/4 (plans/work-ratio.md, "Maintenance follows work"): the arithmetic, the
|
|
2
|
+
* setting, and the gate behind the maintenance allowance — code-maintenance merges in a rolling
|
|
3
|
+
* 24 h window are capped at `maintenancePerWorkMerge × work merges + MAINTENANCE_DAILY_FLOOR`, and
|
|
4
|
+
* `pollMaintenanceQuotaGate` puts that verdict in front of the scheduler so the enabled maintenance
|
|
5
|
+
* loops stop starting new ticks past it.
|
|
6
6
|
*
|
|
7
7
|
* The window counts `merged` events by `commitTier` (src/roles/roles.ts): feature/bugfix/director
|
|
8
8
|
* are the work count, the code-maintenance roles plus readme the maintenance count, and every other
|
|
9
9
|
* role (plan, steward, observers, custom) is neither. The count is an instant-shaped trailing
|
|
10
|
-
* window — `ts >= now − 24 h` — not a local-day bucket, so it cannot reuse the report's
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* window — `ts >= now − 24 h` — not a local-day bucket, so it cannot reuse the report's day-keyed
|
|
11
|
+
* fold cache (src/report/report-fold.ts): instead the counter folds only the bytes the append-only
|
|
12
|
+
* live log gained since the previous poll (`readCompleteLines`) and keeps the parsed merged records
|
|
13
|
+
* in memory, re-reading the window whole only on the first call or after rotation. Both readers
|
|
14
|
+
* share `parseEventLine`, so there is no second line parser.
|
|
15
15
|
*
|
|
16
16
|
* The verdict is pure and lives here beside the count so the scheduler and every status reader
|
|
17
17
|
* share one definition; `pollMaintenanceQuotaGate` is the one writer (the edge-triggered events)
|
|
@@ -25,18 +25,18 @@ import { finiteNumber } from "../files/json-object.js";
|
|
|
25
25
|
import { dayKey } from "../text/datetime.js";
|
|
26
26
|
import { commitTier } from "../roles/roles.js";
|
|
27
27
|
import { baseRoleOf } from "../roles/loop-ids.js";
|
|
28
|
-
import { logEventBestEffort } from "../events/events.js";
|
|
28
|
+
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
29
29
|
/** The fixed test-independent part of the allowance: a project whose backlog is empty still gets
|
|
30
|
-
* this many maintenance
|
|
30
|
+
* this many maintenance merges in a rolling day, so hygiene never stops entirely. */
|
|
31
31
|
export const MAINTENANCE_DAILY_FLOOR = 12;
|
|
32
32
|
/** The rolling window length: 24 h in ms. */
|
|
33
33
|
const MAINTENANCE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
34
|
-
/** The pure allowance verdict: `allowance =
|
|
34
|
+
/** The pure allowance verdict: `allowance = perWorkMerge × work + MAINTENANCE_DAILY_FLOOR`;
|
|
35
35
|
* `used = maint + inFlight`; held while `used >= allowance`. No I/O — the counts and the
|
|
36
36
|
* in-flight figure come from the caller, so the same verdict serves the scheduler and the
|
|
37
37
|
* dashboards. */
|
|
38
38
|
export function maintenanceQuota(input) {
|
|
39
|
-
const allowance = input.
|
|
39
|
+
const allowance = input.perWorkMerge * input.work + MAINTENANCE_DAILY_FLOOR;
|
|
40
40
|
const used = input.maint + input.inFlight;
|
|
41
41
|
return { allowance, used, held: used >= allowance };
|
|
42
42
|
}
|
|
@@ -95,15 +95,15 @@ function seed(counter, root, cutoff, stat) {
|
|
|
95
95
|
export function newMaintenanceQuotaGateState() {
|
|
96
96
|
return { counter: newMaintenanceWindowCounter(), prevHeld: false };
|
|
97
97
|
}
|
|
98
|
-
/** Step the gate by one orchestrator poll: count the rolling window, add the maintenance loops
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
98
|
+
/** Step the gate by one orchestrator poll: count the rolling window, add the maintenance loops in
|
|
99
|
+
* flight, and put 1a's verdict in front of the scheduler. Returns the verdict — `allowance`, `used`
|
|
100
|
+
* (merged maintenance plus the loops already in flight), and `held` (`used >= allowance`) — for the
|
|
101
|
+
* scheduling pass's per-poll admission: it spends the remaining `allowance - used` headroom one due
|
|
102
|
+
* maintenance loop at a time, so two due loops cannot start in the same poll. Logs exactly one
|
|
103
|
+
* team-level `maintenance_quota_hold` when the gate goes from open to held and one
|
|
104
|
+
* `maintenance_quota_resumed` when it re-opens — never one per loop. The director is never
|
|
105
105
|
* maintenance-tier, so no exemption is needed. */
|
|
106
|
-
export function pollMaintenanceQuotaGate(root, state, observers,
|
|
106
|
+
export function pollMaintenanceQuotaGate(root, state, observers, perWorkMerge, now) {
|
|
107
107
|
const { work, maint } = countMaintenanceWindow(root, state.counter, now);
|
|
108
108
|
let inFlight = 0;
|
|
109
109
|
for (const o of observers) {
|
|
@@ -111,14 +111,14 @@ export function pollMaintenanceQuotaGate(root, state, observers, perWorkLanding,
|
|
|
111
111
|
continue;
|
|
112
112
|
if (commitTier(baseRoleOf(o.role)) !== "maintenance")
|
|
113
113
|
continue;
|
|
114
|
-
if (o.running || o.
|
|
114
|
+
if (o.running || o.hasQueuedMerge)
|
|
115
115
|
inFlight++;
|
|
116
116
|
}
|
|
117
|
-
const verdict = maintenanceQuota({ work, maint, inFlight,
|
|
117
|
+
const verdict = maintenanceQuota({ work, maint, inFlight, perWorkMerge });
|
|
118
118
|
if (verdict.held !== state.prevHeld) {
|
|
119
119
|
if (verdict.held) {
|
|
120
120
|
logEventBestEffort(root, {
|
|
121
|
-
|
|
121
|
+
source: TUMWATER_SOURCE,
|
|
122
122
|
type: "maintenance_quota_hold",
|
|
123
123
|
work,
|
|
124
124
|
maint,
|
|
@@ -127,7 +127,7 @@ export function pollMaintenanceQuotaGate(root, state, observers, perWorkLanding,
|
|
|
127
127
|
}
|
|
128
128
|
else {
|
|
129
129
|
logEventBestEffort(root, {
|
|
130
|
-
|
|
130
|
+
source: TUMWATER_SOURCE,
|
|
131
131
|
type: "maintenance_quota_resumed",
|
|
132
132
|
work,
|
|
133
133
|
maint,
|
|
@@ -138,9 +138,9 @@ export function pollMaintenanceQuotaGate(root, state, observers, perWorkLanding,
|
|
|
138
138
|
}
|
|
139
139
|
return verdict;
|
|
140
140
|
}
|
|
141
|
-
/** Count `merged`
|
|
142
|
-
*
|
|
143
|
-
*
|
|
141
|
+
/** Count `merged` events by tier over the rolling 24 h ending `now`, folding only the live log's
|
|
142
|
+
* appended bytes since the previous call through `counter`. Returns `{ work, maint }`; records that
|
|
143
|
+
* have aged out of the window are dropped from the memo so it stays bounded. */
|
|
144
144
|
export function countMaintenanceWindow(root, counter, now) {
|
|
145
145
|
const cutoff = now - MAINTENANCE_WINDOW_MS;
|
|
146
146
|
// The log is read through a regular-file guard so a path that is missing or not a regular file
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/** The operator pause (`tumwater pause`) and the per-role pause (`tumwater pause --role <id>`), as
|
|
2
2
|
* the orchestrator polls them each cycle. Extracted from orchestrator.ts's poll loop: the marker
|
|
3
3
|
* reads, the edge-triggered pause/resume events, and the cross-poll bookkeeping are one concern
|
|
4
|
-
* beside the other
|
|
4
|
+
* beside the other team gates (src/budget/budget.ts's budget gate, src/team/team-hold.ts's team
|
|
5
5
|
* hold) — the orchestrator owns only the wiring. */
|
|
6
|
-
import {
|
|
7
|
-
import { logEventBestEffort } from "../events/events.js";
|
|
8
|
-
/** A fresh pause-gate state: nothing seen paused yet, so the first poll of an unpaused
|
|
6
|
+
import { isTeamPaused, pausedRoles } from "../team/team-state.js";
|
|
7
|
+
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
8
|
+
/** A fresh pause-gate state: nothing seen paused yet, so the first poll of an unpaused team
|
|
9
9
|
* logs nothing. */
|
|
10
10
|
export function newPauseGateState() {
|
|
11
11
|
return { prevUserPaused: false, prevPausedRoles: new Set() };
|
|
@@ -18,24 +18,24 @@ export function newPauseGateState() {
|
|
|
18
18
|
* only NEW ticks are blocked, because the gate sits before isEligible.
|
|
19
19
|
*
|
|
20
20
|
* Per-role pause (`tumwater pause --role <id>`): the operator pause's narrower sibling — the
|
|
21
|
-
* same persistent marker read fresh per cycle, but one named loop instead of the
|
|
22
|
-
* the
|
|
23
|
-
* queued prompts simply wait in the inbox (the same effect the
|
|
21
|
+
* same persistent marker read fresh per cycle, but one named loop instead of the team. Unlike
|
|
22
|
+
* the team pause the director is NOT exempt: the operator named the role deliberately, and its
|
|
23
|
+
* queued prompts simply wait in the inbox (the same effect the team pause has on the director).
|
|
24
24
|
* In-flight ticks finish; only NEW ticks are blocked (the gate sits before isEligible). */
|
|
25
25
|
export function pollPauseGates(root, state) {
|
|
26
|
-
const userPaused =
|
|
26
|
+
const userPaused = isTeamPaused(root);
|
|
27
27
|
if (userPaused !== state.prevUserPaused) {
|
|
28
|
-
logEventBestEffort(root, {
|
|
28
|
+
logEventBestEffort(root, { source: TUMWATER_SOURCE, type: userPaused ? "team_paused" : "team_resumed" });
|
|
29
29
|
state.prevUserPaused = userPaused;
|
|
30
30
|
}
|
|
31
31
|
const pausedRolesNow = pausedRoles(root);
|
|
32
32
|
const pausedRolesSet = new Set(pausedRolesNow);
|
|
33
33
|
for (const r of pausedRolesNow)
|
|
34
34
|
if (!state.prevPausedRoles.has(r))
|
|
35
|
-
logEventBestEffort(root, {
|
|
35
|
+
logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "role_paused", role: r });
|
|
36
36
|
for (const r of state.prevPausedRoles)
|
|
37
37
|
if (!pausedRolesSet.has(r))
|
|
38
|
-
logEventBestEffort(root, {
|
|
38
|
+
logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "role_resumed", role: r });
|
|
39
39
|
state.prevPausedRoles = pausedRolesSet;
|
|
40
40
|
return { userPaused, pausedRoles: pausedRolesSet };
|
|
41
41
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** The repo preconditions' shared wording behind the readiness gate. startup-gate.ts answers
|
|
2
3
|
* with the first unmet precondition (cmdRun and requireReadyRepo fail on it, the self-redeploy
|
|
3
4
|
* refuses a swap on it) and doctor.ts reports each one individually, but every surface
|
|
@@ -7,5 +8,5 @@
|
|
|
7
8
|
* the spawn in pi/pi-bin.ts. */
|
|
8
9
|
export const NOT_A_REPO_MESSAGE = "not a git repository (run `git init` first)";
|
|
9
10
|
export const NO_COMMITS_MESSAGE = "the repo has no commits yet; `tumwater init` creates the first one";
|
|
10
|
-
export const DETACHED_HEAD_MESSAGE = "
|
|
11
|
+
export const DETACHED_HEAD_MESSAGE = "your checkout is detached; check out your main branch first";
|
|
11
12
|
export const NOT_INITIALIZED_MESSAGE = "not initialized (run `tumwater init <prompt>` first — or a bare `tumwater init` when the project brief (TUMWATER.md or README.md) already carries the prompt)";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The per-role daily cost cap (PLANS.md 2026-09-30, part 1/2): a loop whose local-day spend
|
|
2
2
|
* has reached its own `maxDailyCostUsdPerRole` entry starts no new ticks until the next local
|
|
3
|
-
* day or a live config edit — so one runaway role cannot eat the
|
|
4
|
-
* every other loop for the rest of the day. The
|
|
3
|
+
* day or a live config edit — so one runaway role cannot eat the team-wide cap and starve
|
|
4
|
+
* every other loop for the rest of the day. The team budget gate (src/gates/budget-gates.ts)
|
|
5
5
|
* bounds the SUM; this bounds each member. The stateless verdict follows that gate's own rule
|
|
6
6
|
* (budgetPaused's doc: "resume is stateless, so … crossing midnight flips it on the next cycle
|
|
7
7
|
* and nothing can get stuck"): the verdict is recomputed from the loop state every poll, no
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Deliberate differences from the streak gate's shape (src/gates/streak-gate.ts), stated in
|
|
11
11
|
* the plan so no implementer re-litigates them:
|
|
12
|
-
* - This gate writes NOTHING to the shared per-role pause marker (src/
|
|
12
|
+
* - This gate writes NOTHING to the shared per-role pause marker (src/team/team-state.ts's
|
|
13
13
|
* pauseRole). The marker is anonymous — after a restart across midnight the harness could
|
|
14
14
|
* not tell a cap pause from an operator's, and would either refuse to lift the operator's
|
|
15
15
|
* pause or orphan it. The scheduler blocks the role through the poll's separate `capPaused`
|
|
@@ -20,18 +20,19 @@
|
|
|
20
20
|
* never silently no-ops a cap.
|
|
21
21
|
*
|
|
22
22
|
* Shape, like every gate in the family (src/gates/gate-polls.ts): the pure verdict and the
|
|
23
|
-
* edge-triggered bookkeeping live here so they are unit-testable without a
|
|
23
|
+
* edge-triggered bookkeeping live here so they are unit-testable without a team, and this
|
|
24
24
|
* module owns the only event emission — the orchestrator's poll owns only the wiring. The
|
|
25
25
|
* in-memory `prev` and `warned` sets are the whole cross-poll memory: a restart with a
|
|
26
26
|
* still-over-cap role re-logs one `role_cap_paused` on the first poll, and a role already past
|
|
27
27
|
* 80% re-logs one `role_cap_warning` (the durable-cause honest report the streak-gate doc
|
|
28
28
|
* accepts). */
|
|
29
|
+
import { groupBy } from "../collections.js";
|
|
29
30
|
import { budgetWarning, dailyCost, todayStamp } from "../budget/budget.js";
|
|
30
|
-
import { logEventBestEffort } from "../events/events.js";
|
|
31
|
+
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
31
32
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
32
33
|
import { baseRoleOf } from "../roles/loop-ids.js";
|
|
33
34
|
/** The stateless verdict: is this role over its own per-role cap as of `now`? True iff a cap
|
|
34
|
-
* is configured, it is a finite number above zero (0 disables that role's cap, like the
|
|
35
|
+
* is configured, it is a finite number above zero (0 disables that role's cap, like the team
|
|
35
36
|
* cap), and today's spend has reached it. The role→cap lookup stays with the caller
|
|
36
37
|
* (`caps?.[role]`), so part 2/2's observers (src/status/status-data.ts) share this single
|
|
37
38
|
* definition with the scheduler. */
|
|
@@ -48,17 +49,11 @@ export function roleCapPaused(state, cap, now = Date.now()) {
|
|
|
48
49
|
* is exempt. Keyed by base role in first-appearance order; each entry carries the group's loop
|
|
49
50
|
* ids so a caller can log the base (the gate) or flatten the ids (the observer). */
|
|
50
51
|
export function roleCapVerdicts(runners, caps, now) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const members = groups.get(base);
|
|
57
|
-
if (members)
|
|
58
|
-
members.push(r);
|
|
59
|
-
else
|
|
60
|
-
groups.set(base, [r]);
|
|
61
|
-
}
|
|
52
|
+
// Group by base role via the shared Map-of-arrays step (collections.groupBy, which
|
|
53
|
+
// time-spend, history-data, and team-polls also fold), then drop the exempt director's
|
|
54
|
+
// group so it never holds a verdict; first-appearance order is preserved.
|
|
55
|
+
const groups = groupBy(runners, (r) => baseRoleOf(r.role));
|
|
56
|
+
groups.delete(DIRECTOR_ROLE);
|
|
62
57
|
const verdicts = new Map();
|
|
63
58
|
for (const [base, members] of groups) {
|
|
64
59
|
const cap = caps?.[base];
|
|
@@ -94,7 +89,7 @@ export function pollRoleCapGate(root, state, runners, caps, now) {
|
|
|
94
89
|
const was = state.prev.has(base);
|
|
95
90
|
if (verdict.over && !was) {
|
|
96
91
|
logEventBestEffort(root, {
|
|
97
|
-
|
|
92
|
+
source: TUMWATER_SOURCE,
|
|
98
93
|
type: "role_cap_paused",
|
|
99
94
|
role: base,
|
|
100
95
|
spentUsd: verdict.spentUsd,
|
|
@@ -102,9 +97,9 @@ export function pollRoleCapGate(root, state, runners, caps, now) {
|
|
|
102
97
|
});
|
|
103
98
|
}
|
|
104
99
|
else if (!verdict.over && was) {
|
|
105
|
-
logEventBestEffort(root, {
|
|
100
|
+
logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "role_cap_resumed", role: base });
|
|
106
101
|
}
|
|
107
|
-
// The 80% early warning (PLANS.md 2026-10-09), the per-role twin of the
|
|
102
|
+
// The 80% early warning (PLANS.md 2026-10-09), the per-role twin of the team's
|
|
108
103
|
// budget_warning and edge-triggered with the same reset-on-below: crossing the threshold
|
|
109
104
|
// while still under the cap logs one role_cap_warning, a dip back under re-arms it. Warn
|
|
110
105
|
// only while the cap is open — a poll that jumps straight to the cap already logs its own
|
|
@@ -113,7 +108,7 @@ export function pollRoleCapGate(root, state, runners, caps, now) {
|
|
|
113
108
|
const warn = !verdict.over && cap !== undefined && budgetWarning({ spentUsd: verdict.spentUsd, capUsd: cap });
|
|
114
109
|
if (warn && !state.warned.has(base)) {
|
|
115
110
|
logEventBestEffort(root, {
|
|
116
|
-
|
|
111
|
+
source: TUMWATER_SOURCE,
|
|
117
112
|
type: "role_cap_warning",
|
|
118
113
|
role: base,
|
|
119
114
|
spentUsd: verdict.spentUsd,
|