tumwater 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +42 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +4 -3
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +402 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +246 -55
- package/dist/src/backlog/backlog-cli-view.js +280 -0
- package/dist/src/backlog/backlog-cli.js +85 -274
- package/dist/src/backlog/backlog-commit.js +4 -5
- package/dist/src/backlog/backlog-eligibility.js +175 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +254 -0
- package/dist/src/backlog/backlog-release.js +117 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-culprit.js +94 -0
- package/dist/src/baseline/main-guardrails.js +209 -0
- package/dist/src/baseline/main-red.js +139 -32
- package/dist/src/baseline/main-revert.js +133 -0
- package/dist/src/baseline/main-watch.js +184 -0
- package/dist/src/brief/brief-edit.js +9 -11
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget.js +30 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +118 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +150 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +308 -590
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-role-command.js +420 -0
- package/dist/src/cli/cli-start-summary.js +160 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
- package/dist/src/cli/completion.js +149 -22
- package/dist/src/cli/config-commands.js +41 -18
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +189 -73
- package/dist/src/cli/log-commands.js +148 -82
- package/dist/src/cli/question-commands.js +205 -10
- package/dist/src/cli.js +106 -43
- package/dist/src/collections.js +26 -10
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +13 -1
- package/dist/src/config/config-fallbacks.js +93 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +141 -199
- package/dist/src/config/config-views.js +127 -53
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +131 -264
- package/dist/src/config/config.js +48 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
- package/dist/src/doctor/doctor-backlog-plans.js +263 -0
- package/dist/src/doctor/doctor-backlog.js +100 -496
- package/dist/src/doctor/doctor-check-names.js +81 -0
- package/dist/src/doctor/doctor-checks.js +52 -579
- package/dist/src/doctor/doctor-claude-checks.js +145 -0
- package/dist/src/doctor/doctor-codex-checks.js +279 -0
- package/dist/src/doctor/doctor-contention-checks.js +380 -0
- package/dist/src/doctor/doctor-holds.js +82 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +236 -402
- package/dist/src/doctor/doctor-notify.js +3 -4
- package/dist/src/doctor/doctor-opencode-checks.js +162 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-project-checks.js +303 -0
- package/dist/src/doctor/doctor.js +121 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +7 -11
- package/dist/src/events/event-format.js +181 -86
- package/dist/src/events/event-labels.js +12 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +39 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +60 -24
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +56 -56
- package/dist/src/failure/failure-render.js +31 -23
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +99 -35
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +177 -84
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +108 -58
- package/dist/src/gates/maintenance-quota.js +10 -13
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +77 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +36 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +42 -18
- package/dist/src/gui/gui-backlog-endpoints.js +155 -0
- package/dist/src/gui/gui-command.js +8 -13
- package/dist/src/gui/gui-config-commands.js +94 -72
- package/dist/src/gui/gui-endpoint-commands.js +4 -3
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +108 -13
- package/dist/src/gui/gui-prompt-commands.js +11 -14
- package/dist/src/gui/gui-report-endpoints.js +90 -0
- package/dist/src/gui/gui-server.js +11 -4
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +147 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +193 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +25 -9
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +81 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +150 -11
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +90 -0
- package/dist/src/merge/merge-queue-render.js +49 -11
- package/dist/src/merge/merge-queue.js +31 -15
- package/dist/src/merge/merge-requeue.js +102 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +33 -11
- package/dist/src/merge/merge-to-main.js +86 -27
- package/dist/src/merge/merge-top.js +69 -0
- package/dist/src/merge/merge-verify.js +12 -10
- package/dist/src/notes-cli.js +30 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +320 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
- package/dist/src/orchestrator/orchestrator.js +128 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +201 -0
- package/dist/src/orchestrator/scheduling-skip.js +185 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +116 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +18 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +121 -211
- package/dist/src/prompt/notes-backlog.js +181 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +129 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +161 -77
- package/dist/src/report/report-fold.js +129 -52
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +133 -64
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +26 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/known-flakes.js +100 -1
- package/dist/src/review/post-merge-review.js +106 -0
- package/dist/src/review/protected-edits.js +4 -5
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +155 -360
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +69 -12
- package/dist/src/roles/role-guidance.js +55 -20
- package/dist/src/roles/role-labels.js +1 -2
- package/dist/src/roles/role-render.js +67 -50
- package/dist/src/roles/role-view.js +59 -47
- package/dist/src/roles/roles.js +31 -31
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/{tick → run}/qa-coverage.js +9 -27
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +179 -0
- package/dist/src/run/run-detail-render.js +98 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +172 -0
- package/dist/src/run/stage-path-checks.js +730 -0
- package/dist/src/{loop → runner}/leftover.js +80 -25
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +202 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +107 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +102 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +43 -16
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +28 -28
- package/dist/src/status/status-roles.js +39 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +5 -6
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +63 -17
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +57 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +67 -13
- package/dist/src/ui/badges.js +65 -37
- package/dist/src/ui/gui/gui-client-backlog.js +292 -0
- package/dist/src/ui/gui/gui-client-boot.js +32 -26
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
- package/dist/src/ui/gui/gui-client-drawer.js +126 -285
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +434 -67
- package/dist/src/ui/gui/gui-client-labels.js +1 -2
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +55 -51
- package/dist/src/ui/gui/gui-client-pending.js +227 -23
- package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
- package/dist/src/ui/gui/gui-client-report.js +87 -427
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +56 -4
- package/dist/src/ui/gui/gui-client-team.js +25 -465
- package/dist/src/ui/gui/gui-client.js +27 -25
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +9 -7
- package/dist/src/ui/gui/gui-styles.js +34 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +136 -49
- package/dist/src/ui/status-render.js +101 -82
- package/dist/src/ui/team-alerts.js +248 -57
- package/dist/src/ui/tone.js +5 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +51 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +3 -5
- package/dist/src/ui/tui/tui-editor-backends.js +132 -0
- package/dist/src/ui/tui/tui-editor-brief.js +10 -12
- package/dist/src/ui/tui/tui-editor-budget.js +35 -31
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
- package/dist/src/ui/tui/tui-editor-config.js +86 -0
- package/dist/src/ui/tui/tui-editor-entry.js +36 -47
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +37 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
- package/dist/src/ui/tui/tui-editor-interval.js +11 -9
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +3 -5
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
- package/dist/src/ui/tui/tui-editor-retire.js +4 -6
- package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +60 -34
- package/dist/src/ui/tui/tui-editor-slots.js +23 -0
- package/dist/src/ui/tui/tui-editors.js +35 -2
- package/dist/src/ui/tui/tui-frame.js +56 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +24 -9
- package/dist/src/ui/tui/tui-pane-activity.js +14 -11
- package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +58 -8
- package/dist/src/ui/tui/tui-pane-history.js +402 -82
- package/dist/src/ui/tui/tui-pane-pending.js +308 -13
- package/dist/src/ui/tui/tui-pane-queued.js +71 -55
- package/dist/src/ui/tui/tui-pane-report.js +46 -14
- package/dist/src/ui/tui/tui-pane-session.js +25 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +80 -23
- package/dist/src/ui/tui/tui-panes.js +100 -30
- package/dist/src/ui/tui/tui.js +14 -14
- package/dist/src/verdict/fix-claim.js +82 -32
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +42 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
package/dist/src/collections.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* utilities module. */
|
|
5
5
|
/** Group items by a key, preserving first-seen key order and input order within each group.
|
|
6
6
|
* The single home of the "append to a Map of arrays" step, shared by the event bucketers
|
|
7
|
-
* (history-
|
|
7
|
+
* (history-queued.ts's bucketMergeEvents, failure/time-spend.ts's rejected-by-role pass) and
|
|
8
8
|
* the team-hold poll's observations-by-provider grouping (team-polls.ts's pollTeamHold), so
|
|
9
9
|
* those sites cannot drift on how a missing key is seeded. Returns a fresh Map; the input is
|
|
10
10
|
* never mutated. */
|
|
@@ -35,8 +35,8 @@ export function getOrCreate(map, key, make) {
|
|
|
35
35
|
* `map.set(k, (map.get(k) ?? 0) + 1)` step, shared by the failure-spread kind tally
|
|
36
36
|
* (src/team/failure-spread.ts), the diff multiset's line tally (merge/merge-diff.ts), the
|
|
37
37
|
* section-title tally (backlog/backlog-structure.ts), the once-summary outcome tally
|
|
38
|
-
* (cli/cli-
|
|
39
|
-
* (doctor/doctor-checks.ts), and the multi-runner base tally
|
|
38
|
+
* (cli/cli-start.ts), the logs' per-type tally (cli/log-commands.ts), the flaky-check tally
|
|
39
|
+
* (doctor/doctor-project-checks.ts), and the multi-runner base tally
|
|
40
40
|
* (orchestrator/orchestrator-scheduling.ts), so those sites cannot drift on how a missing key is
|
|
41
41
|
* seeded. */
|
|
42
42
|
export function increment(map, key) {
|
|
@@ -46,13 +46,13 @@ export function increment(map, key) {
|
|
|
46
46
|
* sibling of `increment`, the single home of the `record[key] = (record[key] ?? 0) + n`
|
|
47
47
|
* step (the seed matters because `noUncheckedIndexedAccess` reads every entry as possibly
|
|
48
48
|
* undefined), shared by the failure digest's outcome tally per role (failure-data.ts), the
|
|
49
|
-
* usage report's window role totals (report-render.ts), and its per-day fold's
|
|
49
|
+
* usage report's window role totals (report-render.ts), and its per-day fold's run and cost
|
|
50
50
|
* tallies (report-fold.ts), so those sites cannot drift on how a missing key is seeded. The
|
|
51
51
|
* test duration reporter (test/test-durations-reporter.ts) keeps its own copy: it imports no
|
|
52
52
|
* tumwater module by design, since node --test loads it into its own parent process.
|
|
53
53
|
*
|
|
54
54
|
* The seed reads an OWN property (Object.hasOwn), not a bare `record[key] ?? 0`: the
|
|
55
|
-
* custom-
|
|
55
|
+
* custom-role name charset admits "constructor", an Object.prototype key, whose inherited
|
|
56
56
|
* function would otherwise be the seed — storing `function Object() …5` and turning every
|
|
57
57
|
* later `roleSum`/report total into string concatenation (BUGS.md 2026-10-10). */
|
|
58
58
|
export function addTo(record, key, amount) {
|
|
@@ -78,6 +78,20 @@ export function memoSetLru(cache, key, value, max) {
|
|
|
78
78
|
}
|
|
79
79
|
cache.set(key, value);
|
|
80
80
|
}
|
|
81
|
+
/** Trim `arr` in place so it holds at most `max` elements, keeping only the newest (last) `max`
|
|
82
|
+
* and dropping the whole overflow from the front in one splice — the single home of the
|
|
83
|
+
* "keep the newest capped buffer" discipline that src/ui/transcript.ts's feedEntries and
|
|
84
|
+
* src/history/history.ts's `logs --follow` buffer both hand-rolled before, so those sites
|
|
85
|
+
* cannot drift on how an over-cap buffer is drained. Batch appends may overflow by more than
|
|
86
|
+
* one at a time, so the whole excess is dropped rather than a single element; an `arr` already
|
|
87
|
+
* at or under `max` is untouched. Mutates `arr` in place and returns nothing. (Deliberately
|
|
88
|
+
* unlike the per-element ring drains of src/scheduling/backoff.ts and src/orchestrator/
|
|
89
|
+
* orchestrator-launch.ts, which shift one entry at a time as they push — each is a different
|
|
90
|
+
* shape and keeps its own copy.) */
|
|
91
|
+
export function trimToLast(arr, max) {
|
|
92
|
+
if (arr.length > max)
|
|
93
|
+
arr.splice(0, arr.length - max);
|
|
94
|
+
}
|
|
81
95
|
/** One deterministic ranking rule: count descending, then key ascending, so equal counts always
|
|
82
96
|
* order the same way. These two helpers are the single home of that rule; every surface that
|
|
83
97
|
* ranks counters by count calls one of them rather than re-implementing the comparator —
|
|
@@ -88,15 +102,16 @@ export function memoSetLru(cache, key, value, max) {
|
|
|
88
102
|
* failure-data.ts's outcome table, and time-spend.ts's time-and-spend table). `sortByValueDesc`
|
|
89
103
|
* is the deliberate sibling for the same value-descending shape that wants a code-unit tiebreak
|
|
90
104
|
* instead of localeCompare, and unlike these two it sorts its input array in place. The one
|
|
91
|
-
* deliberate exception to both orderings is ui/gui/gui-client-report.ts's reportRoleOrder,
|
|
92
|
-
* keeps its own copy by design: it runs in the browser, where harness modules cannot be
|
|
105
|
+
* deliberate exception to both orderings is ui/gui/gui-client-report-chart.ts's reportRoleOrder,
|
|
106
|
+
* which keeps its own copy by design: it runs in the browser, where harness modules cannot be
|
|
107
|
+
* imported.
|
|
93
108
|
* These two helpers return a fresh sorted array; their input is never mutated. */
|
|
94
109
|
export function rankByCount(items, count, key) {
|
|
95
110
|
return [...items].sort((a, b) => count(b) - count(a) || key(a).localeCompare(key(b)));
|
|
96
111
|
}
|
|
97
112
|
/** The lower median of a non-empty numeric list: the middle value, or the smaller of the
|
|
98
113
|
* middle two for an even count. Returns a fresh sorted copy; the input is never mutated.
|
|
99
|
-
* Shared by report-data.ts's per-day medians, doctor-checks.ts's and doctor-backlog.ts's
|
|
114
|
+
* Shared by report-data.ts's per-day medians, doctor-project-checks.ts's and doctor-backlog.ts's
|
|
100
115
|
* 24 h phrase medians. (failure-data.ts's `median` is deliberately different: the mean of
|
|
101
116
|
* the middle two for an even count.) */
|
|
102
117
|
export function lowerMedian(values) {
|
|
@@ -105,7 +120,7 @@ export function lowerMedian(values) {
|
|
|
105
120
|
}
|
|
106
121
|
/** Ascending string comparator by UTF-16 code unit — the locale-independent order a bare `.sort()`
|
|
107
122
|
* uses, unlike `localeCompare` (which `rankByCount` uses for its tiebreak). Used directly by
|
|
108
|
-
* src/cli/cli-
|
|
123
|
+
* src/cli/cli-start-summary.ts, src/cli/log-commands.ts and src/history/history-data.ts, and by
|
|
109
124
|
* `sortByValueDesc` for its tiebreak. */
|
|
110
125
|
export function compareCodeUnits(a, b) {
|
|
111
126
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -121,7 +136,8 @@ export function sortByValueDesc(entries) {
|
|
|
121
136
|
return entries.sort((a, b) => b[1] - a[1] || compareCodeUnits(a[0], b[0]));
|
|
122
137
|
}
|
|
123
138
|
/** rankByCount for `[key, count]` entry pairs (count first): src/report/report-render.ts's
|
|
124
|
-
* rankedRoleMap
|
|
139
|
+
* rankedRoleMap, src/team/failure-spread.ts's strongest-kind pick, and
|
|
140
|
+
* src/doctor/doctor-contention-checks.ts's check-wait and provider-wait rankings. */
|
|
125
141
|
export function rankCountEntries(entries) {
|
|
126
142
|
return rankByCount(entries, (e) => e[1], (e) => e[0]);
|
|
127
143
|
}
|
|
@@ -9,7 +9,7 @@ import { Semaphore } from "./semaphore.js";
|
|
|
9
9
|
* execution and classification (detect → spawn → classify) — because permit accounting is
|
|
10
10
|
* scheduling policy, not check semantics: it changes when the concurrency or fairness policy
|
|
11
11
|
* changes, not when the check's execution does. Separate from the orchestrator's maxConcurrent
|
|
12
|
-
* semaphore: a role
|
|
12
|
+
* semaphore: a role run holds one of those while it waits here, but a check permit is held only
|
|
13
13
|
* around the check process itself — never across a pi run, the merge lock, or another check — so no
|
|
14
14
|
* permit holder waits on anything a waiter holds. Sized at each acquire from the caller's live
|
|
15
15
|
* config (checkCap), so a tumwater.json edit applies to the next check; Semaphore.setCapacity never
|
|
@@ -20,6 +20,9 @@ const checkPermits = new Semaphore(defaultConfig().maxConcurrentChecks);
|
|
|
20
20
|
* second wait could never be granted (its own holder is the one blocking it). No call path
|
|
21
21
|
* nests today; this keeps a future one from deadlocking the team's checks. */
|
|
22
22
|
const holdingPermit = new AsyncLocalStorage();
|
|
23
|
+
/** Labels of the checks holding a permit now, keyed by a per-hold token; insertion order is
|
|
24
|
+
* acquisition order. A check_wait event names them (PermitWaitHooks.waited). */
|
|
25
|
+
const permitHolders = new Map();
|
|
23
26
|
/** Waiting-queue tiers (Semaphore.acquire): a merge-scope check runs inside the merge lock, so it
|
|
24
27
|
* is granted the next free permit ahead of queued gate and baseline checks — every check it waited
|
|
25
28
|
* behind would be lock-hold time for every other merge. A running check is never preempted. */
|
|
@@ -32,23 +35,37 @@ function checkCap(config) {
|
|
|
32
35
|
const n = config?.maxConcurrentChecks;
|
|
33
36
|
return typeof n === "number" && Number.isInteger(n) && n >= 1 ? n : defaultConfig().maxConcurrentChecks;
|
|
34
37
|
}
|
|
38
|
+
/** The check pool's live state for observers (the orchestrator's poll publishes it): permits held,
|
|
39
|
+
* the cap, and checks queued for one. `limit` is the cap set by the most recent withCheckPermit
|
|
40
|
+
* call, or the default before any check has run. */
|
|
41
|
+
export function checkPermitSnapshot() {
|
|
42
|
+
return { inUse: checkPermits.snapshot().inUse, limit: checkPermits.limit, waiting: checkPermits.waiting };
|
|
43
|
+
}
|
|
35
44
|
/** Run `run` under one process-wide check permit (see checkPermits), resizing the cap from the
|
|
36
45
|
* live config first and releasing in a finally — a check that fails, times out, or throws still
|
|
37
46
|
* gives its permit back. Reentrant (holdingPermit): called again from inside `run`, it runs the
|
|
38
|
-
* inner work under the permit already held. `hooks` hear about a wait, if there is one
|
|
39
|
-
|
|
47
|
+
* inner work under the permit already held. `hooks` hear about a wait, if there is one; `label`
|
|
48
|
+
* names this check to the waiters it holds up ("a check" when omitted). */
|
|
49
|
+
export async function withCheckPermit(config, tier, run, hooks, label) {
|
|
40
50
|
if (holdingPermit.getStore())
|
|
41
51
|
return run();
|
|
42
52
|
checkPermits.setCapacity(checkCap(config));
|
|
43
53
|
if (!checkPermits.tryAcquire()) {
|
|
54
|
+
const waitStarted = Date.now();
|
|
55
|
+
// Snapshot at park time; not revised if holders change mid-wait.
|
|
56
|
+
const parked = { heldBy: [...permitHolders.values()], limit: checkPermits.limit };
|
|
44
57
|
hooks?.waiting();
|
|
45
58
|
await checkPermits.acquire(tier);
|
|
46
59
|
hooks?.granted();
|
|
60
|
+
hooks?.waited?.(Date.now() - waitStarted, parked);
|
|
47
61
|
}
|
|
62
|
+
const token = Symbol();
|
|
63
|
+
permitHolders.set(token, label ?? "a check");
|
|
48
64
|
try {
|
|
49
65
|
return await holdingPermit.run(true, run);
|
|
50
66
|
}
|
|
51
67
|
finally {
|
|
68
|
+
permitHolders.delete(token);
|
|
52
69
|
checkPermits.release();
|
|
53
70
|
}
|
|
54
71
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { pidAlive } from "../process/process.js";
|
|
5
4
|
import { errorMessage, parsePositiveInt } from "../text/text.js";
|
|
6
|
-
import { ensureParentDir, removeTree } from "../files/files.js";
|
|
5
|
+
import { dirEntryNames, ensureParentDir, removeTree } from "../files/files.js";
|
|
7
6
|
import { errCode } from "../errno.js";
|
|
7
|
+
import { primaryWorktreeLockPath } from "../paths.js";
|
|
8
8
|
/** How old a lock dir must be before it is stale on age alone — regardless of whether its
|
|
9
9
|
* recorded pid still looks alive, so a reused pid cannot latch a dead holder as live. */
|
|
10
10
|
const STALE_MS = 10 * 60 * 1000;
|
|
@@ -70,7 +70,7 @@ export function classifyLock(dir) {
|
|
|
70
70
|
return pidAlive(pid) ? "live" : "stale";
|
|
71
71
|
}
|
|
72
72
|
/** Name the lock's holder for the timeout message: the pid its pid file records, or a note
|
|
73
|
-
* that none was readable. The message surfaces as a
|
|
73
|
+
* that none was readable. The message surfaces as a run's lastError, and the pid is the one
|
|
74
74
|
* handle an operator needs to tell "waiting on a slow holder" from "an orphan dir nobody can
|
|
75
75
|
* break" — without it every timeout reads the same and the holder has to be found by hand. */
|
|
76
76
|
function lockHolderNote(dir) {
|
|
@@ -117,7 +117,7 @@ function acquireStep(dir, timeoutMs, waitedMs) {
|
|
|
117
117
|
}
|
|
118
118
|
/** The tail every wait-step in acquireStep shares once a lock is held (or was lost in the
|
|
119
119
|
* publish race): break it if stale, then hand back "retry" for another pass — or throw the one
|
|
120
|
-
* timeout error once the wait budget is spent. That error surfaces as a
|
|
120
|
+
* timeout error once the wait budget is spent. That error surfaces as a run's lastError, and
|
|
121
121
|
* naming the holder is what distinguishes a slow holder from a wedged one (and says which
|
|
122
122
|
* process to look at). Single-homed so the held path and the lost-race path cannot drift. */
|
|
123
123
|
function retryOrTimeout(dir, timeoutMs, waitedMs) {
|
|
@@ -156,14 +156,8 @@ function publishLockDir(dir) {
|
|
|
156
156
|
function sweepStaleTemps(dir) {
|
|
157
157
|
const parent = path.dirname(dir);
|
|
158
158
|
const prefix = `${path.basename(dir)}.acquiring-`;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
entries = fs.readdirSync(parent);
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
return; // No parent to sweep; the acquire's own mkdir reports the real cause.
|
|
165
|
-
}
|
|
166
|
-
for (const name of entries) {
|
|
159
|
+
// No parent to sweep reads empty; the acquire's own mkdir reports the real cause.
|
|
160
|
+
for (const name of dirEntryNames(parent)) {
|
|
167
161
|
if (!name.startsWith(prefix))
|
|
168
162
|
continue;
|
|
169
163
|
const candidate = path.join(parent, name);
|
|
@@ -176,7 +170,7 @@ function sweepStaleTemps(dir) {
|
|
|
176
170
|
}
|
|
177
171
|
}
|
|
178
172
|
}
|
|
179
|
-
/** Directory-and-pid mutex shared by all
|
|
173
|
+
/** Directory-and-pid mutex shared by all roles (and processes) of one project: the holder
|
|
180
174
|
* publishes a dir with its pid in it by atomic rename. */
|
|
181
175
|
export async function withLock(dir, fn, timeoutMs = 120_000) {
|
|
182
176
|
const startedAt = Date.now();
|
|
@@ -235,3 +229,13 @@ export async function withStateLockAsync(lock, fn, timeoutMs = 10_000) {
|
|
|
235
229
|
ensureParentDir(lock);
|
|
236
230
|
return withLock(lock, fn, timeoutMs);
|
|
237
231
|
}
|
|
232
|
+
/** withStateLock on the primary-worktree lock: the one lock every read-modify-write of a tracked
|
|
233
|
+
* file in the primary checkout (a backlog file, the brief) takes, which a merge's working-tree
|
|
234
|
+
* fast-forward takes too. */
|
|
235
|
+
export function withPrimaryWorktreeLock(root, fn) {
|
|
236
|
+
return withStateLock(primaryWorktreeLockPath(root), fn);
|
|
237
|
+
}
|
|
238
|
+
/** The async twin of withPrimaryWorktreeLock, for a section that awaits git. */
|
|
239
|
+
export function withPrimaryWorktreeLockAsync(root, fn) {
|
|
240
|
+
return withStateLockAsync(primaryWorktreeLockPath(root), fn);
|
|
241
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/** The AsyncLocalStorage store that lets a
|
|
2
|
-
* (plans/provider-concurrency.md §5): a lease rides here for the length of a
|
|
3
|
-
* or a resolver run, and `
|
|
4
|
-
* actually uses. The one choke point every run a
|
|
5
|
-
* passes through, so a budget handback or a model-failure switch that moves a
|
|
1
|
+
/** The AsyncLocalStorage store that lets a run's lease follow its runs across providers
|
|
2
|
+
* (plans/provider-concurrency.md §5): a lease rides here for the length of a run, a verify task,
|
|
3
|
+
* or a resolver run, and `RunnerPi.runWithTransientRetry` re-keys it to the provider each run
|
|
4
|
+
* actually uses. The one choke point every run a role makes (authoring, gate, merge and resolver)
|
|
5
|
+
* passes through, so a budget handback or a model-failure switch that moves a run in flight to
|
|
6
6
|
* another provider moves the lease with it instead of leaving it parked on the key it was admitted
|
|
7
7
|
* on. The check-permit.ts `holdingPermit` store is the pattern: nothing here is a separate permit,
|
|
8
8
|
* only the single lease the run path already holds.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* The store carries the `piDefault` (the caller's cached readPiDefaultModel result) beside the
|
|
11
11
|
* lease so `ensureProviderLease` can resolve a bare no-model pi seam's key exactly as the
|
|
12
12
|
* admission/review seam did — a run whose key were computed without it would re-key a default
|
|
13
|
-
* team's
|
|
13
|
+
* team's run off pi's own provider. It also carries the semaphore and, optionally, wait
|
|
14
14
|
* hooks, so a re-key that has to park on a full target key reports its wait the way a parked
|
|
15
15
|
* resolver does (merge-verify.ts's providerWaitStage). */
|
|
16
16
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -26,7 +26,7 @@ export function withProviderLease(lease, semaphore, run, opts = {}) {
|
|
|
26
26
|
return providerLeaseStore.run({ lease, semaphore, piDefault: opts.piDefault, hooks: opts.hooks }, run);
|
|
27
27
|
}
|
|
28
28
|
/** Re-key the calling lease to the provider `cfg` runs on, if it is not there yet. No-op when
|
|
29
|
-
* the current async context carries no lease (a direct
|
|
29
|
+
* the current async context carries no lease (a direct RunnerPi unit test, `tumwater history`) or
|
|
30
30
|
* when the run is already on the lease's key. When it does re-key and `hooks` are present and
|
|
31
31
|
* the target key is at its cap, the hooks hear `waiting` before the lease parks and `granted`
|
|
32
32
|
* once the key frees — a re-key never waits on the team total (it already holds a team
|
|
@@ -58,7 +58,7 @@ function keyAtCap(ctx, key) {
|
|
|
58
58
|
}
|
|
59
59
|
/** The pool-slot wait rule (plans/provider-concurrency.md §5 "No deadlock"): a lease never holds a
|
|
60
60
|
* provider key while it waits for a worktree pool slot. Called by leaseSlot just before it parks on
|
|
61
|
-
* a busy pool; it re-keys the calling lease to no key (which never waits), so a
|
|
61
|
+
* a busy pool; it re-keys the calling lease to no key (which never waits), so a run or verify
|
|
62
62
|
* waiting for a slot does not hold — and cannot deadlock — a provider slot whose in-use a re-key
|
|
63
63
|
* waiter is waiting to drain. No-op without a lease in context. */
|
|
64
64
|
export async function dropProviderLeaseKey() {
|
|
@@ -67,3 +67,39 @@ export async function dropProviderLeaseKey() {
|
|
|
67
67
|
return;
|
|
68
68
|
await ctx.lease.rekey(undefined);
|
|
69
69
|
}
|
|
70
|
+
/** Run `wait`, an idle stretch inside the calling lease's work, with the lease's team permit and
|
|
71
|
+
* key slot handed back (SemaphoreLease.pause), so a parked run can use them meanwhile, then take
|
|
72
|
+
* them back (resume) before returning. A failed review's in-place retry pause is the caller: with
|
|
73
|
+
* one team permit, a reviewer that kept failing held it through two 30 s pauses and starved every
|
|
74
|
+
* other role. Once `signal` has fired, nothing is taken back and the caller's release() settles a
|
|
75
|
+
* still-paused lease. No-op without a lease in context. */
|
|
76
|
+
export async function idleWithoutProviderLease(wait, signal) {
|
|
77
|
+
const ctx = providerLeaseStore.getStore();
|
|
78
|
+
if (!ctx)
|
|
79
|
+
return wait();
|
|
80
|
+
ctx.lease.pause();
|
|
81
|
+
try {
|
|
82
|
+
await wait();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
if (!signal?.aborted)
|
|
86
|
+
await untilAborted(ctx.lease.resume(), signal);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** `p`, or an earlier return once `signal` fires: a resume parked on a full team must not keep an
|
|
90
|
+
* aborted verification waiting for a permit it no longer needs. */
|
|
91
|
+
async function untilAborted(p, signal) {
|
|
92
|
+
if (!signal)
|
|
93
|
+
return p;
|
|
94
|
+
let onAbort = () => { };
|
|
95
|
+
const aborted = new Promise((resolve) => {
|
|
96
|
+
onAbort = resolve;
|
|
97
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
await Promise.race([p, aborted]);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
signal.removeEventListener("abort", onAbort);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* provider never blocks a waiter on another (plans/provider-concurrency.md §3). The keyless
|
|
7
7
|
* entry points — acquire/tryAcquire/release — are unchanged; only acquireLease, forceLease
|
|
8
8
|
* and setKeyCaps touch the keyed path. */
|
|
9
|
+
/** The entries of a `maxConcurrentPerProvider` map the semaphore enforces: positive integers
|
|
10
|
+
* only, so a non-positive or non-integer cap is dropped rather than read as a limit. */
|
|
11
|
+
export function enforcedKeyCaps(caps) {
|
|
12
|
+
return Object.entries(caps ?? {}).filter(([, v]) => Number.isInteger(v) && v > 0);
|
|
13
|
+
}
|
|
9
14
|
export class Semaphore {
|
|
10
15
|
capacity;
|
|
11
16
|
/** A parked WAITING (admission) waiter: its scheduling tier, optional provider key, and the
|
|
@@ -29,8 +34,8 @@ export class Semaphore {
|
|
|
29
34
|
/** The live per-key caps, from `maxConcurrentPerProvider` (plans/provider-concurrency.md
|
|
30
35
|
* §1). An absent key is uncapped — bounded by `capacity` alone. */
|
|
31
36
|
keyCaps = new Map();
|
|
32
|
-
/** Permits withheld from tier ≥ 1 acquirers so a work
|
|
33
|
-
* one without waiting for a maintenance
|
|
37
|
+
/** Permits withheld from tier ≥ 1 acquirers so a work role that has work to take can claim
|
|
38
|
+
* one without waiting for a maintenance run to release (plans/parallel-work-instances.md
|
|
34
39
|
* "Priority headroom", part 7/7). Tiers ≤ 0 (work, VERIFY_TIER, MERGE_TIER) ignore it. */
|
|
35
40
|
reserve = 0;
|
|
36
41
|
constructor(capacity) {
|
|
@@ -64,15 +69,7 @@ export class Semaphore {
|
|
|
64
69
|
return;
|
|
65
70
|
}
|
|
66
71
|
await new Promise((resolve) => {
|
|
67
|
-
|
|
68
|
-
let i = this.waiters.length;
|
|
69
|
-
while (i > 0) {
|
|
70
|
-
const prev = this.waiters[i - 1];
|
|
71
|
-
if (!prev || prev.tier <= tier)
|
|
72
|
-
break; // insert before strictly-greater tiers only
|
|
73
|
-
i -= 1;
|
|
74
|
-
}
|
|
75
|
-
this.waiters.splice(i, 0, waiter);
|
|
72
|
+
this.enqueue({ tier, resolve });
|
|
76
73
|
});
|
|
77
74
|
}
|
|
78
75
|
/** Admit by both the team total and an optional provider key at once, waiting until both
|
|
@@ -92,10 +89,31 @@ export class Semaphore {
|
|
|
92
89
|
// CANCEL the pending grant rather than undo a slot this lease never took. Null when no
|
|
93
90
|
// rekey is parked.
|
|
94
91
|
let parkedRekey = null;
|
|
92
|
+
// True while pause() has handed the team permit and the key's slot back: release() then owes
|
|
93
|
+
// nothing, and resume() re-admits at this lease's tier on its current key.
|
|
94
|
+
let paused = false;
|
|
95
|
+
// resume()'s admission waiter while it is queued. release() cancels it if drive has not
|
|
96
|
+
// granted it yet, and gives the grant back if it has.
|
|
97
|
+
let parkedResume = null;
|
|
95
98
|
const release = () => {
|
|
96
99
|
if (closed)
|
|
97
100
|
return;
|
|
98
101
|
closed = true;
|
|
102
|
+
if (parkedResume !== null) {
|
|
103
|
+
const w = parkedResume;
|
|
104
|
+
parkedResume = null;
|
|
105
|
+
const idx = this.waiters.indexOf(w);
|
|
106
|
+
if (idx >= 0) {
|
|
107
|
+
// Still queued: it took nothing, so cancel it and owe nothing.
|
|
108
|
+
this.waiters.splice(idx, 1);
|
|
109
|
+
w.resolve();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// drive granted it this microtask: its permit and key slot are taken, so give them back.
|
|
113
|
+
paused = false;
|
|
114
|
+
}
|
|
115
|
+
if (paused)
|
|
116
|
+
return; // pause() already gave the permit and the key's slot back
|
|
99
117
|
if (parkedRekey !== null) {
|
|
100
118
|
// A re-key is parked waiting on its new key. Cancel it: pull it off the queue (so drive
|
|
101
119
|
// can't grant it) and, if the grant already arrived this microtask, give the new key's slot
|
|
@@ -119,6 +137,12 @@ export class Semaphore {
|
|
|
119
137
|
const rekey = async (to) => {
|
|
120
138
|
if (to === currentKey || closed)
|
|
121
139
|
return;
|
|
140
|
+
if (paused) {
|
|
141
|
+
// Nothing is held while paused: the move only changes the key resume() admits on.
|
|
142
|
+
currentKey = to;
|
|
143
|
+
lease.key = to;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
122
146
|
// Give the old key back first, waking that key's waiters.
|
|
123
147
|
if (currentKey !== undefined) {
|
|
124
148
|
this.bumpKeyInUse(currentKey, -1);
|
|
@@ -143,7 +167,36 @@ export class Semaphore {
|
|
|
143
167
|
this.bumpKeyInUse(to, 1);
|
|
144
168
|
this.drive();
|
|
145
169
|
};
|
|
146
|
-
const
|
|
170
|
+
const pause = () => {
|
|
171
|
+
if (closed || paused || parkedRekey !== null)
|
|
172
|
+
return;
|
|
173
|
+
paused = true;
|
|
174
|
+
if (currentKey !== undefined)
|
|
175
|
+
this.bumpKeyInUse(currentKey, -1);
|
|
176
|
+
this.inUse -= 1;
|
|
177
|
+
this.drive();
|
|
178
|
+
};
|
|
179
|
+
const resume = async () => {
|
|
180
|
+
if (closed || !paused || parkedResume !== null)
|
|
181
|
+
return;
|
|
182
|
+
if (this.canLeaseAtOnce(tier, currentKey)) {
|
|
183
|
+
this.inUse += 1;
|
|
184
|
+
if (currentKey !== undefined)
|
|
185
|
+
this.bumpKeyInUse(currentKey, 1);
|
|
186
|
+
paused = false;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
await new Promise((resolve) => {
|
|
190
|
+
const waiter = { tier, key: currentKey, resolve };
|
|
191
|
+
parkedResume = waiter;
|
|
192
|
+
this.enqueue(waiter);
|
|
193
|
+
});
|
|
194
|
+
parkedResume = null;
|
|
195
|
+
// drive granted it (the permit and key slot are taken), unless release() cancelled it.
|
|
196
|
+
if (!closed)
|
|
197
|
+
paused = false;
|
|
198
|
+
};
|
|
199
|
+
const lease = { key: currentKey, release, rekey, pause, resume };
|
|
147
200
|
if (this.inUse < this.capacity - this.reserveFor(tier) && (key === undefined || !this.keyAtCap(key))) {
|
|
148
201
|
this.inUse += 1;
|
|
149
202
|
if (key !== undefined)
|
|
@@ -151,18 +204,23 @@ export class Semaphore {
|
|
|
151
204
|
return lease;
|
|
152
205
|
}
|
|
153
206
|
await new Promise((resolve) => {
|
|
154
|
-
|
|
155
|
-
let i = this.waiters.length;
|
|
156
|
-
while (i > 0) {
|
|
157
|
-
const prev = this.waiters[i - 1];
|
|
158
|
-
if (!prev || prev.tier <= tier)
|
|
159
|
-
break;
|
|
160
|
-
i -= 1;
|
|
161
|
-
}
|
|
162
|
-
this.waiters.splice(i, 0, waiter);
|
|
207
|
+
this.enqueue({ tier, key, resolve });
|
|
163
208
|
});
|
|
164
209
|
return lease;
|
|
165
210
|
}
|
|
211
|
+
/** Queue an admission waiter behind every waiter of its tier or a lower one, so it inserts
|
|
212
|
+
* before strictly-greater tiers only (tier order, FIFO within a tier) — the one home of the
|
|
213
|
+
* queue's ordering, shared by acquire, acquireLease and a paused lease's resume. */
|
|
214
|
+
enqueue(waiter) {
|
|
215
|
+
let i = this.waiters.length;
|
|
216
|
+
while (i > 0) {
|
|
217
|
+
const prev = this.waiters[i - 1];
|
|
218
|
+
if (!prev || prev.tier <= waiter.tier)
|
|
219
|
+
break;
|
|
220
|
+
i -= 1;
|
|
221
|
+
}
|
|
222
|
+
this.waiters.splice(i, 0, waiter);
|
|
223
|
+
}
|
|
166
224
|
/** Synchronously report whether acquireLease(tier, key) would return at once — both the
|
|
167
225
|
* team total and the key (when set) have headroom right now, and the tier's reserve is
|
|
168
226
|
* satisfied. Mirrors the exact fast-path condition acquireLease's first branch uses, so a
|
|
@@ -180,8 +238,10 @@ export class Semaphore {
|
|
|
180
238
|
if (currentKey !== undefined) {
|
|
181
239
|
this.bumpKeyInUse(currentKey, 1);
|
|
182
240
|
}
|
|
241
|
+
// True while pause() has given the key's slot back (a forced lease holds no team permit).
|
|
242
|
+
let paused = false;
|
|
183
243
|
const release = () => {
|
|
184
|
-
if (currentKey !== undefined) {
|
|
244
|
+
if (currentKey !== undefined && !paused) {
|
|
185
245
|
this.bumpKeyInUse(currentKey, -1);
|
|
186
246
|
}
|
|
187
247
|
this.drive();
|
|
@@ -189,16 +249,32 @@ export class Semaphore {
|
|
|
189
249
|
const rekey = async (to) => {
|
|
190
250
|
if (to === currentKey)
|
|
191
251
|
return;
|
|
192
|
-
if (currentKey !== undefined) {
|
|
252
|
+
if (currentKey !== undefined && !paused) {
|
|
193
253
|
this.bumpKeyInUse(currentKey, -1);
|
|
194
254
|
}
|
|
195
255
|
currentKey = to;
|
|
196
256
|
lease.key = to;
|
|
197
|
-
if (to !== undefined)
|
|
257
|
+
if (to !== undefined && !paused)
|
|
198
258
|
this.bumpKeyInUse(to, 1);
|
|
199
259
|
this.drive();
|
|
200
260
|
};
|
|
201
|
-
const
|
|
261
|
+
const pause = () => {
|
|
262
|
+
if (paused)
|
|
263
|
+
return;
|
|
264
|
+
paused = true;
|
|
265
|
+
if (currentKey !== undefined)
|
|
266
|
+
this.bumpKeyInUse(currentKey, -1);
|
|
267
|
+
this.drive();
|
|
268
|
+
};
|
|
269
|
+
// Like the forced lease itself, taking the slot back never waits.
|
|
270
|
+
const resume = async () => {
|
|
271
|
+
if (!paused)
|
|
272
|
+
return;
|
|
273
|
+
paused = false;
|
|
274
|
+
if (currentKey !== undefined)
|
|
275
|
+
this.bumpKeyInUse(currentKey, 1);
|
|
276
|
+
};
|
|
277
|
+
const lease = { key: currentKey, release, rekey, pause, resume };
|
|
202
278
|
return lease;
|
|
203
279
|
}
|
|
204
280
|
/** Hand every parked waiter a permit it can take — both team and key headroom — in one
|
|
@@ -289,7 +365,7 @@ export class Semaphore {
|
|
|
289
365
|
* holder — it only withholds future grants on that key until releases drain it under the new
|
|
290
366
|
* cap, the same contract as setCapacity. Non-positive and non-integer entries are dropped. */
|
|
291
367
|
setKeyCaps(caps) {
|
|
292
|
-
this.keyCaps = new Map(
|
|
368
|
+
this.keyCaps = new Map(enforcedKeyCaps(caps));
|
|
293
369
|
this.drive();
|
|
294
370
|
}
|
|
295
371
|
/** The pool's live state for observability (plans/provider-concurrency.md §8): the team
|
|
@@ -30,7 +30,6 @@ const CONFIG_KEY_RENAMES = [
|
|
|
30
30
|
{ from: "minTickIntervalSeconds", to: "minRunIntervalSeconds", scope: "top" },
|
|
31
31
|
{ from: "minTickIntervalSeconds", to: "minRunIntervalSeconds", scope: "role" },
|
|
32
32
|
{ from: "customLoops", to: "customRoles", scope: "top" },
|
|
33
|
-
{ from: "agent", to: "backend", scope: "top" },
|
|
34
33
|
{ from: "agents", to: "backends", scope: "top" },
|
|
35
34
|
];
|
|
36
35
|
/** Move every old key of one object to its new name. Returns a copy when anything moved. */
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* This is config vocabulary, not HTTP vocabulary — it names which tumwater.json keys are
|
|
9
9
|
* browser-editable — so it lives beside the other config-key lists (config-schema.ts's
|
|
10
10
|
* TOP_LEVEL_KEYS, config-write.ts's per-key validators) rather than in the endpoint module
|
|
11
|
-
* that first consumed it. The
|
|
11
|
+
* that first consumed it. The four run-concurrency knobs (maxConcurrent,
|
|
12
|
+
* maxConcurrentPerProvider, maxConcurrentChecks, worktreeSlots) joined the set because
|
|
13
|
+
* tumwater doctor's own contention checks name them for the operator to raise
|
|
14
|
+
* (checkWorktreePool, checkProviderContention, checkCheckContention); the per-role maps and
|
|
15
|
+
* customRoles still stay CLI/director territory. The browser side mirrors the set for its labels
|
|
12
16
|
* (ui/gui/gui-client-settings.ts's SETTINGS_KEYS), a manual copy; the GET /api/config test
|
|
13
17
|
* pins the served key set to this list, and the settings test pins the browser copy's keys
|
|
14
18
|
* and order to it.
|
|
@@ -18,6 +22,14 @@ export const EDITABLE_CONFIG_KEYS = [
|
|
|
18
22
|
"model",
|
|
19
23
|
"fallback",
|
|
20
24
|
"maxDailyCostUsd",
|
|
25
|
+
// The run-concurrency knobs tumwater doctor's contention checks name for the operator to
|
|
26
|
+
// raise (checkWorktreePool, checkProviderContention) — grouped so the Settings card that
|
|
27
|
+
// shows the advice can apply it.
|
|
28
|
+
"maxConcurrent",
|
|
29
|
+
"maxConcurrentPerProvider",
|
|
30
|
+
"maxConcurrentChecks",
|
|
31
|
+
"worktreeSlots",
|
|
32
|
+
"claudeWindowReserve",
|
|
21
33
|
"quietHours",
|
|
22
34
|
"notify",
|
|
23
35
|
// The per-backend section (plans/agent-backends.md, part 4a/7): the whole `agents` object is
|