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
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/** The worktree/process contention checks, split out of doctor-checks.ts (the project's
|
|
2
|
+
* largest module, ~6x the file-size median, holding four unrelated check domains): the merge
|
|
3
|
+
* lock, the worktree pool, the three `_wait` contention rows (slot, provider, check), and the
|
|
4
|
+
* parallel-work-instance, tumwater-pin, leftover-role-branch and provider-concurrency audits.
|
|
5
|
+
* Read-only like every doctor check; they share the wait fold (sumWaits) and the pin/worktree
|
|
6
|
+
* leftovers helpers, all of which live here beside the checks that use them. The environment
|
|
7
|
+
* and repo-setup checks stay in doctor-checks.ts; the project's own state, disk and build
|
|
8
|
+
* checks live in doctor-project-checks.ts; model readiness lives in doctor-model-checks.ts;
|
|
9
|
+
* the per-document backlog checks live in doctor-backlog-plans.ts and doctor-backlog-bugs.ts
|
|
10
|
+
* beside doctor-backlog.ts's shared helpers and queue checks;
|
|
11
|
+
* doctor.ts remains the composition layer
|
|
12
|
+
* that calls each check in fixed order. */
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { defaultConfig, knownRoleIds, slotCount } from "../config/config.js";
|
|
15
|
+
import { CUSTOM_NAME_RE } from "../config/config-schema.js";
|
|
16
|
+
import { subdirectoryNames } from "../files/files.js";
|
|
17
|
+
import { finiteNumber, stringList } from "../files/json-object.js";
|
|
18
|
+
import { commitExists, listBranches, refsUnder } from "../git/git.js";
|
|
19
|
+
import { readSlotsState } from "../git/slots-state.js";
|
|
20
|
+
import { eventsSince } from "../events/event-window.js";
|
|
21
|
+
import { DAY_MS } from "../text/datetime.js";
|
|
22
|
+
import { shortSha } from "../text/format.js";
|
|
23
|
+
import { errorMessage, joinParts, textOr } from "../text/text.js";
|
|
24
|
+
import { rankCountEntries } from "../collections.js";
|
|
25
|
+
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
26
|
+
import { INSTANCE_ROLES, runnerEnabled, runnerIds } from "../roles/runner-ids.js";
|
|
27
|
+
import { CLAIM_IDLE_MAX_MS, listedKeys } from "../scheduling/claims.js";
|
|
28
|
+
import { piModelsPath } from "../pi/pi-models.js";
|
|
29
|
+
import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
|
|
30
|
+
import { seamConcurrencyKey } from "../config/config-views.js";
|
|
31
|
+
import { seamsInUse } from "../config/config-fallbacks.js";
|
|
32
|
+
import { enforcedKeyCaps } from "../concurrency/semaphore.js";
|
|
33
|
+
import { classifyLock, readLockPid } from "../concurrency/lock.js";
|
|
34
|
+
import { branchName, mergeLockDir, worktreesDir } from "../paths.js";
|
|
35
|
+
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
36
|
+
import { agree, plural, shortSpanPhrase } from "../text/phrases.js";
|
|
37
|
+
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
38
|
+
/** Merge lock — read-only classification via classifyLock (the same three cases the breaker
|
|
39
|
+
* uses). A stale lock is a warning, not a failure: it self-heals on the next merge. */
|
|
40
|
+
export function checkMergeLock(root) {
|
|
41
|
+
const dir = mergeLockDir(root);
|
|
42
|
+
switch (classifyLock(dir)) {
|
|
43
|
+
case "absent":
|
|
44
|
+
return okOutcome("not held");
|
|
45
|
+
case "live": {
|
|
46
|
+
const pid = readLockPid(dir);
|
|
47
|
+
return okOutcome(pid !== null ? `held by running role (pid ${pid})` : "held (pid not yet written)");
|
|
48
|
+
}
|
|
49
|
+
case "stale":
|
|
50
|
+
return warnOutcome("stale — will be broken on next merge");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** A pin older than a day is the doctor's stuck-pin signal: a paused or retired role is holding
|
|
54
|
+
* a pooled checkout nobody will reuse (plans/worktree-pool.md, "Observability"). */
|
|
55
|
+
const PIN_STALE_MS = DAY_MS;
|
|
56
|
+
/** Directory names under the worktrees root that the pool no longer owns: a legacy per-role
|
|
57
|
+
* `<role>` checkout (the pre-pool layout) or a `_land-<role>` checkout from the merge queue. The
|
|
58
|
+
* dedicated `director`, `_slot-<n>`, `_merge`, `_main`, `_gate-main` and `_build` checkouts are
|
|
59
|
+
* expected and never reported. An unreadable worktrees dir means nothing to report. */
|
|
60
|
+
function leftoverWorktreeDirs(root) {
|
|
61
|
+
return subdirectoryNames(worktreesDir(root), (name) => name !== DIRECTOR_ROLE && !(name.startsWith("_") && !name.startsWith("_land-")));
|
|
62
|
+
}
|
|
63
|
+
/** The window of `slot_wait` events the pool row sums, and the total wait inside it that warns. */
|
|
64
|
+
const SLOT_WAIT_CHECK_WINDOW_MS = DAY_MS;
|
|
65
|
+
const SLOT_WAIT_WARN_TOTAL_MS = 10 * 60_000;
|
|
66
|
+
const PROVIDER_WAIT_CHECK_WINDOW_MS = DAY_MS;
|
|
67
|
+
const PROVIDER_WAIT_WARN_TOTAL_MS = 10 * 60_000;
|
|
68
|
+
const CHECK_WAIT_CHECK_WINDOW_MS = DAY_MS;
|
|
69
|
+
const CHECK_WAIT_WARN_TOTAL_MS = 10 * 60_000;
|
|
70
|
+
/** Fold a window's `*_wait` events into the three numbers every contention check reads: how
|
|
71
|
+
* many waits there were (each event counts once), the total ms they waited, and the longest
|
|
72
|
+
* single wait — plus the longest event itself, for the check that names the role/purpose off
|
|
73
|
+
* it. The single home of that fold for the three contention checks (checkWorktreePool's
|
|
74
|
+
* `slot_wait` half, checkProviderContention, checkCheckContention), so the sum and the
|
|
75
|
+
* longest-wait selection cannot drift between them. A wait that omits or corrupts `waitedMs`
|
|
76
|
+
* reads as 0 for the sum, exactly as each inline `finiteNumber(ev.waitedMs, 0)` fold did;
|
|
77
|
+
* the longest-wait tie keeps the first event that reached it. */
|
|
78
|
+
function sumWaits(waits) {
|
|
79
|
+
let count = 0;
|
|
80
|
+
let totalMs = 0;
|
|
81
|
+
let longestMs = 0;
|
|
82
|
+
let longest = waits[0];
|
|
83
|
+
for (const ev of waits) {
|
|
84
|
+
count += 1;
|
|
85
|
+
const waited = finiteNumber(ev.waitedMs, 0);
|
|
86
|
+
totalMs += waited;
|
|
87
|
+
if (waited > longestMs) {
|
|
88
|
+
longestMs = waited;
|
|
89
|
+
longest = ev;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { count, totalMs, longestMs, longest };
|
|
93
|
+
}
|
|
94
|
+
/** Worktree pool (plans/worktree-pool.md, part 5/5): report the pooled slots and warn on the
|
|
95
|
+
* states an operator should act on — a legacy `<role>` or `_land-*` checkout the pool migration
|
|
96
|
+
* left behind, a pin older than PIN_STALE_MS (a paused role holding a checkout), and the last
|
|
97
|
+
* 24 h of `slot_wait` events totalling SLOT_WAIT_WARN_TOTAL_MS (the pool is too small). Read-only,
|
|
98
|
+
* like every doctor check; `now` is a test seam for the pin-age arithmetic. */
|
|
99
|
+
export function checkWorktreePool(root, now = Date.now()) {
|
|
100
|
+
const slots = readSlotsState(root).slots;
|
|
101
|
+
const leased = slots.filter((slot) => slot.lease !== null);
|
|
102
|
+
const pins = slots.filter((slot) => slot.pinnedFor !== null);
|
|
103
|
+
const stale = pins.filter((slot) => slot.pinnedAt != null && now - slot.pinnedAt > PIN_STALE_MS);
|
|
104
|
+
const leftover = leftoverWorktreeDirs(root);
|
|
105
|
+
const parts = [`${plural(slots.length, "workspace")}, ${leased.length} leased, ${pins.length} pinned`];
|
|
106
|
+
if (leftover.length > 0)
|
|
107
|
+
parts.push(`legacy worktrees still present: ${leftover.join(", ")}`);
|
|
108
|
+
if (stale.length > 0) {
|
|
109
|
+
parts.push(stale
|
|
110
|
+
.map((slot) => `${textOr(slot.pinnedFor)} pinned on ${path.basename(slot.dir)} for ${Math.round((now - (slot.pinnedAt ?? now)) / 3_600_000)}h`)
|
|
111
|
+
.join("; "));
|
|
112
|
+
}
|
|
113
|
+
let contended = false;
|
|
114
|
+
try {
|
|
115
|
+
const waits = eventsSince(root, SLOT_WAIT_CHECK_WINDOW_MS, now, (ev) => ev.type === "slot_wait");
|
|
116
|
+
if (waits.length > 0) {
|
|
117
|
+
const { count, totalMs, longestMs, longest } = sumWaits(waits);
|
|
118
|
+
contended = totalMs >= SLOT_WAIT_WARN_TOTAL_MS;
|
|
119
|
+
const purpose = longest.purpose === "verify" ? "check" : "run";
|
|
120
|
+
parts.push(`${plural(count, "wait")} to start in the last 24h, ${shortSpanPhrase(totalMs)} total, longest ${shortSpanPhrase(longestMs)} (${textOr(longest.role)} ${purpose})${contended ? ` — raise worktreeSlots above ${slots.length}` : ""}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
parts.push(`could not read the event log for waits to start: ${errorMessage(err)}`);
|
|
125
|
+
}
|
|
126
|
+
const warn = leftover.length > 0 || stale.length > 0 || contended;
|
|
127
|
+
return (warn ? warnOutcome : okOutcome)(parts.join(" — "));
|
|
128
|
+
}
|
|
129
|
+
/** Provider-slot contention: sums the last 24 h of `provider_wait` events (each a role run
|
|
130
|
+
* whose semaphore admission waited PROVIDER_WAIT_EVENT_MS or more, recorded in
|
|
131
|
+
* orchestrator-launch.ts) and warns when the total reaches PROVIDER_WAIT_WARN_TOTAL_MS — the
|
|
132
|
+
* provider cap is too small. Names every key that waited, busiest first (the key-less
|
|
133
|
+
* bucket, which also takes every team-cap wait whatever its key, reads "the run cap") and, on
|
|
134
|
+
* a warn, each key's maxConcurrentPerProvider cap to raise, plus maxConcurrent for the run cap.
|
|
135
|
+
* A counterpoint to checkWorktreePool's slot-wait half; read-only, like every doctor check.
|
|
136
|
+
* `now` is a test seam for the window. */
|
|
137
|
+
export function checkProviderContention(root, config, now = Date.now()) {
|
|
138
|
+
const perKey = new Map();
|
|
139
|
+
let count = 0;
|
|
140
|
+
let totalMs = 0;
|
|
141
|
+
let longestMs = 0;
|
|
142
|
+
try {
|
|
143
|
+
const waits = eventsSince(root, PROVIDER_WAIT_CHECK_WINDOW_MS, now, (ev) => ev.type === "provider_wait");
|
|
144
|
+
const sum = sumWaits(waits);
|
|
145
|
+
count = sum.count;
|
|
146
|
+
totalMs = sum.totalMs;
|
|
147
|
+
longestMs = sum.longestMs;
|
|
148
|
+
for (const ev of waits) {
|
|
149
|
+
const waited = finiteNumber(ev.waitedMs, 0);
|
|
150
|
+
const key = ev.cap !== "team" && typeof ev.key === "string" && ev.key.length > 0 ? ev.key : "";
|
|
151
|
+
perKey.set(key, (perKey.get(key) ?? 0) + waited);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
// A read error leaves the level ok, exactly like the slot-wait branch's own read: the
|
|
156
|
+
// check cannot measure, so it says so rather than crying wolf.
|
|
157
|
+
return okOutcome(`could not read the event log for provider waits: ${errorMessage(err)}`);
|
|
158
|
+
}
|
|
159
|
+
if (count === 0)
|
|
160
|
+
return okOutcome("no provider waits");
|
|
161
|
+
// Rank the buckets by wait total, ties breaking on key name so the detail is deterministic.
|
|
162
|
+
// The key-less bucket reads "the run cap" — maxConcurrent, since no seam keys to a provider.
|
|
163
|
+
const ranked = rankCountEntries(perKey).map(([key, total]) => ({ key, total }));
|
|
164
|
+
const bucketLabel = (key) => (key === "" ? "the run cap" : `the ${key} slot`);
|
|
165
|
+
const cells = ranked.map((b) => `${bucketLabel(b.key)} ${shortSpanPhrase(b.total)}`);
|
|
166
|
+
// A single bucket keeps the pre-existing wording, a bare `(the X slot)` without the total
|
|
167
|
+
// (the total is already named by the `shortSpanPhrase(total)` in the clause before it);
|
|
168
|
+
// two or more name each bucket's total so the ranking reads at a glance.
|
|
169
|
+
const parenthetical = ranked.length === 1
|
|
170
|
+
? `(${bucketLabel(ranked[0].key)})`
|
|
171
|
+
: `(${cells[0]}, also ${cells.slice(1).join(", ")})`;
|
|
172
|
+
let detail = `${plural(count, "wait")} for a provider slot in the last 24h, ${shortSpanPhrase(totalMs)} total, longest ${shortSpanPhrase(longestMs)} ${parenthetical}`;
|
|
173
|
+
const contended = totalMs >= PROVIDER_WAIT_WARN_TOTAL_MS;
|
|
174
|
+
if (contended) {
|
|
175
|
+
const advice = ranked
|
|
176
|
+
.filter((b) => b.key !== "" && config?.maxConcurrentPerProvider?.[b.key] !== undefined)
|
|
177
|
+
.map((b) => `raise maxConcurrentPerProvider.${b.key} above ${config?.maxConcurrentPerProvider?.[b.key]}`)
|
|
178
|
+
.join(" and ");
|
|
179
|
+
const teamAdvice = perKey.has("") && config !== undefined ? [`raise maxConcurrent above ${config.maxConcurrent}`] : [];
|
|
180
|
+
const all = [...(advice ? [advice] : []), ...teamAdvice].join(" and ");
|
|
181
|
+
if (all)
|
|
182
|
+
detail += ` — ${all}`;
|
|
183
|
+
}
|
|
184
|
+
return (contended ? warnOutcome : okOutcome)(detail);
|
|
185
|
+
}
|
|
186
|
+
/** Names the kind of check a `heldBy` label stands for, in checkCheckContention's tally. */
|
|
187
|
+
function checkHolderKind(label) {
|
|
188
|
+
if (label === "main baseline")
|
|
189
|
+
return "the main baseline";
|
|
190
|
+
const scope = label.slice(label.lastIndexOf(" ") + 1);
|
|
191
|
+
return scope === "gate" || scope === "merge" || scope === "batch" ? `${scope} checks` : "other checks";
|
|
192
|
+
}
|
|
193
|
+
/** Check-slot contention: sums the last 24 h of `check_wait` events (each a scoped check whose
|
|
194
|
+
* permit wait reached CHECK_WAIT_EVENT_MS, recorded in build-check-events.ts) and warns when
|
|
195
|
+
* the total reaches CHECK_WAIT_WARN_TOTAL_MS — maxConcurrentChecks is too small. The third
|
|
196
|
+
* sibling of checkWorktreePool's slot-wait half and checkProviderContention; read-only.
|
|
197
|
+
* When events carry `heldBy` (the labels of the permit holders the check parked behind), the
|
|
198
|
+
* detail gains `(behind <kind> <span>, …)` after the longest clause, kinds busiest first: a
|
|
199
|
+
* wait counts once toward each distinct kind that held a slot. `now` is a test seam for the
|
|
200
|
+
* window. */
|
|
201
|
+
export function checkCheckContention(root, config, now = Date.now()) {
|
|
202
|
+
const perKind = new Map();
|
|
203
|
+
let count = 0;
|
|
204
|
+
let totalMs = 0;
|
|
205
|
+
let longestMs = 0;
|
|
206
|
+
try {
|
|
207
|
+
const waits = eventsSince(root, CHECK_WAIT_CHECK_WINDOW_MS, now, (e) => e.type === "check_wait");
|
|
208
|
+
const sum = sumWaits(waits);
|
|
209
|
+
count = sum.count;
|
|
210
|
+
totalMs = sum.totalMs;
|
|
211
|
+
longestMs = sum.longestMs;
|
|
212
|
+
for (const ev of waits) {
|
|
213
|
+
const waited = finiteNumber(ev.waitedMs, 0);
|
|
214
|
+
for (const kind of new Set(stringList(ev.heldBy).map(checkHolderKind))) {
|
|
215
|
+
perKind.set(kind, (perKind.get(kind) ?? 0) + waited);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
return okOutcome(`could not read the event log for check waits: ${errorMessage(err)}`);
|
|
221
|
+
}
|
|
222
|
+
if (count === 0)
|
|
223
|
+
return okOutcome("no check waits");
|
|
224
|
+
let detail = `${plural(count, "wait")} for a check slot in the last 24h, ${shortSpanPhrase(totalMs)} total, longest ${shortSpanPhrase(longestMs)}`;
|
|
225
|
+
if (perKind.size > 0) {
|
|
226
|
+
const ranked = rankCountEntries(perKind);
|
|
227
|
+
detail += ` (behind ${ranked.map(([kind, ms]) => `${kind} ${shortSpanPhrase(ms)}`).join(", ")})`;
|
|
228
|
+
}
|
|
229
|
+
if (totalMs < CHECK_WAIT_WARN_TOTAL_MS)
|
|
230
|
+
return okOutcome(detail);
|
|
231
|
+
detail += ` — raise maxConcurrentChecks above ${config?.maxConcurrentChecks ?? defaultConfig().maxConcurrentChecks}`;
|
|
232
|
+
return warnOutcome(detail);
|
|
233
|
+
}
|
|
234
|
+
/** The "leftover role branches" row: `tumwater/<id>` branches whose role is no longer enabled
|
|
235
|
+
* (disabled in tumwater.json, or an instance above the configured count). Retirable ids are
|
|
236
|
+
* the ones `tumwater retire --role <id>` accepts (knownRoleIds, or any well-formed custom-role name
|
|
237
|
+
* with a leftover branch); any other id is listed by
|
|
238
|
+
* full branch name with a manual-delete hint. The director and enabled roles are skipped.
|
|
239
|
+
* Read-only, like every check: it only lists refs; an unreadable tumwater.json skips it. */
|
|
240
|
+
export async function checkLeftoverRoleBranches(root, config) {
|
|
241
|
+
if (!config)
|
|
242
|
+
return okOutcome("no readable tumwater.json — nothing to verify");
|
|
243
|
+
const prefix = branchName("");
|
|
244
|
+
const known = new Set(knownRoleIds(config));
|
|
245
|
+
const retirable = [];
|
|
246
|
+
const unknown = [];
|
|
247
|
+
for (const branch of await listBranches(root)) {
|
|
248
|
+
if (!branch.startsWith(prefix))
|
|
249
|
+
continue;
|
|
250
|
+
const id = branch.slice(prefix.length);
|
|
251
|
+
if (!id || id.includes("/") || id === DIRECTOR_ROLE || runnerEnabled(config, id))
|
|
252
|
+
continue;
|
|
253
|
+
if (known.has(id) || CUSTOM_NAME_RE.test(id))
|
|
254
|
+
retirable.push(id);
|
|
255
|
+
else
|
|
256
|
+
unknown.push(branch);
|
|
257
|
+
}
|
|
258
|
+
const parts = [];
|
|
259
|
+
if (retirable.length > 0) {
|
|
260
|
+
const n = retirable.length;
|
|
261
|
+
parts.push(`${retirable.join(", ")} ${agree(n, "is", "are")} disabled but still ${agree(n, "has", "have")} a branch — \`tumwater retire --role <id>\` removes each (\`--dry-run\` previews)`);
|
|
262
|
+
}
|
|
263
|
+
if (unknown.length > 0) {
|
|
264
|
+
parts.push(`${unknown.join(", ")} ${agree(unknown.length, "names", "name")} no role retire accepts — delete with \`git branch -D\` once its commits are not needed`);
|
|
265
|
+
}
|
|
266
|
+
return parts.length === 0 ? okOutcome("no branches left by disabled roles") : warnOutcome(parts.join("; "));
|
|
267
|
+
}
|
|
268
|
+
/** The "tumwater pins" row: fails when a `refs/tumwater/` pin (merge, rejected or legacy landing)
|
|
269
|
+
* names a commit that is gone. It fails rather than warns because such a pin stops `git gc`
|
|
270
|
+
* (and the `gc --auto` commits trigger), and a slot reset or leftover recovery only drops one
|
|
271
|
+
* when it reaches it. Read-only: it lists refs and names the `git update-ref -d` fix. */
|
|
272
|
+
export async function checkTumwaterPins(root) {
|
|
273
|
+
const pins = await refsUnder(root, "refs/tumwater/");
|
|
274
|
+
const broken = [];
|
|
275
|
+
for (const pin of pins)
|
|
276
|
+
if (!(await commitExists(root, pin.sha)))
|
|
277
|
+
broken.push(pin);
|
|
278
|
+
if (broken.length === 0) {
|
|
279
|
+
return okOutcome(pins.length === 0 ? "no tumwater pins" : `${plural(pins.length, "tumwater pin")}, each naming a commit that exists`);
|
|
280
|
+
}
|
|
281
|
+
const n = broken.length;
|
|
282
|
+
return failOutcome(`${broken.map((p) => `${p.ref} names ${shortSha(p.sha)}, which is gone`).join("; ")} — git gc and a detached checkout abort on ${agree(n, "it", "them")}; \`git update-ref -d <ref>\` drops each (the work ${agree(n, "it", "they")} pinned is already lost)`);
|
|
283
|
+
}
|
|
284
|
+
/** The ok detail of the instance check: non-zero counts, plans first, each pluralized. */
|
|
285
|
+
function claimCounts(claims) {
|
|
286
|
+
const bugs = claims.filter((c) => c.file === "BUGS.md").length;
|
|
287
|
+
const plans = claims.length - bugs;
|
|
288
|
+
const joined = joinParts([
|
|
289
|
+
plans > 0 ? plural(plans, "claimed plan") : "",
|
|
290
|
+
bugs > 0 ? plural(bugs, "claimed bug") : "",
|
|
291
|
+
], ", ");
|
|
292
|
+
return joined || "no claimed plans or bugs";
|
|
293
|
+
}
|
|
294
|
+
/** Parallel work instances (plans/parallel-work-instances.md "Observability", part 6/7):
|
|
295
|
+
* each instance's claimed plan or bug and the two states an operator should act on — a claim
|
|
296
|
+
* whose key is no longer listed under its role's section (its instance is stuck on an entry
|
|
297
|
+
* that moved), and a claim idle for more than CLAIM_IDLE_MAX_MS (the scheduler releases
|
|
298
|
+
* these, so seeing one means the reader's view and the scheduler's disagree). An enabled
|
|
299
|
+
* multi-instance role configured with fewer pooled slots than maxConcurrent also warns:
|
|
300
|
+
* instances then contend for the same checkouts. A disabled role runs none of its
|
|
301
|
+
* configured instances, so it never contends and
|
|
302
|
+
* stays out of that warning.
|
|
303
|
+
* Read-only, like every check; `now` is the claim-age test seam, and an unreadable
|
|
304
|
+
* tumwater.json (config undefined) skips the walk — checkInit reports the config itself. */
|
|
305
|
+
export function checkWorkInstances(root, config, now = Date.now()) {
|
|
306
|
+
if (!config)
|
|
307
|
+
return okOutcome("no readable tumwater.json — nothing to verify");
|
|
308
|
+
const claims = [];
|
|
309
|
+
for (const id of runnerIds(config)) {
|
|
310
|
+
const claim = loadRunnerState(root, id).claim;
|
|
311
|
+
if (claim)
|
|
312
|
+
claims.push({ id, title: claim.title, key: claim.key, at: claim.at, file: claim.file });
|
|
313
|
+
}
|
|
314
|
+
const gone = claims.filter((c) => !listedKeys(root, c.id).has(c.key));
|
|
315
|
+
const stale = claims.filter((c) => !gone.includes(c) && now - c.at > CLAIM_IDLE_MAX_MS);
|
|
316
|
+
const multi = Object.entries(config.roles)
|
|
317
|
+
.filter(([id, role]) => INSTANCE_ROLES.has(id) && role.enabled && (role.instances ?? 1) > 1)
|
|
318
|
+
.map(([id]) => id);
|
|
319
|
+
const slots = slotCount(config);
|
|
320
|
+
const smallPool = multi.length > 0 && slots < config.maxConcurrent;
|
|
321
|
+
if (gone.length === 0 && stale.length === 0 && !smallPool)
|
|
322
|
+
return okOutcome(claimCounts(claims));
|
|
323
|
+
const noun = (c) => (c.file === "BUGS.md" ? "bug" : "plan");
|
|
324
|
+
const parts = [
|
|
325
|
+
...gone.map((c) => `claimed ${noun(c)} "${c.title}" (${c.id}) names an entry no longer listed`),
|
|
326
|
+
...stale.map((c) => `claimed ${noun(c)} "${c.title}" (${c.id}) has been idle ${Math.round((now - c.at) / 3_600_000)}h`),
|
|
327
|
+
];
|
|
328
|
+
if (smallPool)
|
|
329
|
+
parts.push(`${multi.join(", ")} run instances but worktreeSlots ${slots} is below maxConcurrent ${config.maxConcurrent}`);
|
|
330
|
+
return warnOutcome(parts.join("; "));
|
|
331
|
+
}
|
|
332
|
+
/** Provider concurrency caps (plans/provider-concurrency.md §8): audit the
|
|
333
|
+
* `maxConcurrentPerProvider` map against the seams the config can actually spawn — each role,
|
|
334
|
+
* the review gate, the resolver, the director, and each tier's fallback — so the operator
|
|
335
|
+
* sees whether every cap names a provider in use and whether any cap actually binds. Warns on
|
|
336
|
+
* a cap key no seam resolves to (a typo, or a provider not in use), a cap at or above
|
|
337
|
+
* `maxConcurrent` (which never binds — the team total is the real ceiling), and a capped
|
|
338
|
+
* team whose seams include one with no key (pi's default provider is unreadable, so that
|
|
339
|
+
* seam cannot be capped). A config with no per-provider map or no caps is fine, and so is a
|
|
340
|
+
* well-formed map; a broken config is reported by the init check. Takes `root` to read pi's
|
|
341
|
+
* own default model (piSettingsPath), the same seam the orchestrator's admission uses, so a
|
|
342
|
+
* bare no-model pi seam keys exactly as the team would key it. */
|
|
343
|
+
export function checkProviderConcurrency(config, root) {
|
|
344
|
+
if (!config)
|
|
345
|
+
return okOutcome("no config to audit");
|
|
346
|
+
const caps = config.maxConcurrentPerProvider;
|
|
347
|
+
const capEntries = enforcedKeyCaps(caps);
|
|
348
|
+
if (capEntries.length === 0)
|
|
349
|
+
return okOutcome("no per-provider caps configured");
|
|
350
|
+
// The pi default model the orchestrator's admission seam carries: a bare no-model pi seam
|
|
351
|
+
// resolves its key only through it, so a team whose default is readable has keyed seams and
|
|
352
|
+
// only an unreadable default leaves a key-less seam to warn about.
|
|
353
|
+
const piDefault = readPiDefaultModel(piSettingsPath(piModelsPath()), root);
|
|
354
|
+
const seams = seamsInUse(config);
|
|
355
|
+
const keyed = [];
|
|
356
|
+
for (const s of seams) {
|
|
357
|
+
const k = seamConcurrencyKey(s, piDefault);
|
|
358
|
+
if (k !== undefined && !keyed.includes(k))
|
|
359
|
+
keyed.push(k);
|
|
360
|
+
}
|
|
361
|
+
const keySet = new Set(keyed);
|
|
362
|
+
const capKeys = capEntries.map(([k]) => k);
|
|
363
|
+
const warnings = [];
|
|
364
|
+
for (const key of capKeys) {
|
|
365
|
+
if (!keySet.has(key))
|
|
366
|
+
warnings.push(`cap key "${key}" resolves to no model job (a typo, or a provider not in use)`);
|
|
367
|
+
}
|
|
368
|
+
for (const [key, cap] of capEntries) {
|
|
369
|
+
if (cap >= config.maxConcurrent)
|
|
370
|
+
warnings.push(`cap "${key}" ${cap} is at or above maxConcurrent ${config.maxConcurrent} — it never binds`);
|
|
371
|
+
}
|
|
372
|
+
const capSet = new Set(capKeys);
|
|
373
|
+
const keyless = seams.filter((s) => seamConcurrencyKey(s, piDefault) === undefined);
|
|
374
|
+
if (capSet.size > 0 && keyless.length > 0) {
|
|
375
|
+
warnings.push(`${keyless.length} model job(s) resolve to no provider key (pi's default provider is unreadable, so they cannot be capped): ${keyless.map((s) => s.label).join(", ")}`);
|
|
376
|
+
}
|
|
377
|
+
if (warnings.length === 0)
|
|
378
|
+
return okOutcome(`${plural(capEntries.length, "provider cap")} — all model jobs resolve`);
|
|
379
|
+
return warnOutcome(warnings.join("; "));
|
|
380
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import { roleLabel } from "../roles/role-labels.js";
|
|
3
2
|
import { pausedRoles, pausedRolesUntil, standingTeamPause } from "../team/team-state.js";
|
|
4
3
|
import { MODEL_TIERS } from "../config/config-schema.js";
|
|
@@ -9,34 +8,37 @@ import { roleCapVerdicts } from "../gates/role-cap-gates.js";
|
|
|
9
8
|
import { readMergeQueue } from "../merge/merge-queue.js";
|
|
10
9
|
import { MAINTENANCE_DAILY_FLOOR } from "../gates/maintenance-quota.js";
|
|
11
10
|
import { heldByMainRed, mainCheckForPoll, maintenanceQuotaForPoll } from "../status/status-polls.js";
|
|
12
|
-
import {
|
|
13
|
-
import { baseRoleOf,
|
|
14
|
-
import { ERROR_STREAK_BREAKER, ERROR_STREAK_WARN } from "../
|
|
11
|
+
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
12
|
+
import { baseRoleOf, runnerIds, runnerPaused } from "../roles/runner-ids.js";
|
|
13
|
+
import { ERROR_STREAK_BREAKER, ERROR_STREAK_WARN } from "../run/run-apply.js";
|
|
15
14
|
import { squash } from "../text/text.js";
|
|
16
15
|
import { usd } from "../text/format.js";
|
|
17
16
|
import { parseQuietHours, quietHoursStatus, quietWindowEnd, roleQuietHold } from "../scheduling/quiet-hours.js";
|
|
18
|
-
import { humanUntil } from "../text/datetime.js";
|
|
17
|
+
import { DAY_MS, HOUR_MS, humanUntil } from "../text/datetime.js";
|
|
19
18
|
import { budgetPhrase, pauseReasonSuffix, plural } from "../text/phrases.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
19
|
+
import { readAccount, utilizationPct, windowEntries, windowLabel } from "../agent/subscription-account.js";
|
|
20
|
+
import { backendDisplayName } from "../agent/backend-traits.js";
|
|
21
|
+
import { windowObservation } from "../team/team-polls.js";
|
|
22
22
|
import { readEventsSince } from "../events/event-window.js";
|
|
23
23
|
import { errorMessage } from "../text/text.js";
|
|
24
24
|
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
25
|
+
import { redMainWarning, revertWarnings } from "../baseline/main-guardrails.js";
|
|
26
|
+
import { loadQuarantine } from "../review/known-flakes.js";
|
|
25
27
|
/** The standing-hold checks: `pause` (checkPause), `quiet hours` (checkQuietHours), and `budget`
|
|
26
|
-
* (checkBudget), `claude window` (
|
|
27
|
-
* (checkMaintenanceQuota), and `error streaks` (checkErrorStreaks), all
|
|
28
|
-
* holds on
|
|
29
|
-
export function
|
|
30
|
-
return
|
|
31
|
-
.map((id) =>
|
|
28
|
+
* (checkBudget), the `claude window`/`codex window` rows (checkSubscriptionWindow),
|
|
29
|
+
* `maintenance quota` (checkMaintenanceQuota), and `error streaks` (checkErrorStreaks), all
|
|
30
|
+
* read-only reports of holds on running. */
|
|
31
|
+
export function loadRunnerStates(root, config) {
|
|
32
|
+
return runnerIds(config)
|
|
33
|
+
.map((id) => loadRunnerState(root, id))
|
|
32
34
|
.sort((a, b) => a.role.localeCompare(b.role));
|
|
33
35
|
}
|
|
34
36
|
/** The `pause` check: names a standing operator pause so doctor does not print "ready to run"
|
|
35
|
-
* over a team that starts no
|
|
37
|
+
* over a team that starts no run. Read-only: it reads the same team and per-role pause
|
|
36
38
|
* markers the scheduler reads (standingTeamPause, pausedRoles, pausedRolesUntil — all treat
|
|
37
39
|
* an expired marker as absent), so its verdict cannot disagree with what the team does. A
|
|
38
|
-
* team pause is a warn because it stops every
|
|
39
|
-
* chose them by name and every other
|
|
40
|
+
* team pause is a warn because it stops every role; paused roles are ok because the operator
|
|
41
|
+
* chose them by name and every other role keeps running. `now` only drives the countdowns. */
|
|
40
42
|
export function checkPause(root, now = Date.now()) {
|
|
41
43
|
const team = standingTeamPause(root);
|
|
42
44
|
const roles = pausedRoles(root);
|
|
@@ -53,10 +55,10 @@ export function checkPause(root, now = Date.now()) {
|
|
|
53
55
|
return warnOutcome(`${head}${pauseReasonSuffix(team.reason)}${rolesPhrase ? `; ${rolesPhrase}` : ""}`);
|
|
54
56
|
}
|
|
55
57
|
/** The `quiet hours` check: names the team quiet window and any per-role windows holding now,
|
|
56
|
-
* so doctor does not print "ready to run" over a team that starts no
|
|
58
|
+
* so doctor does not print "ready to run" over a team that starts no run until the window
|
|
57
59
|
* ends. Read-only: membership comes from the scheduler's own parser and predicate
|
|
58
60
|
* (quietHoursStatus, roleQuietHold), so the verdict cannot disagree with the gate. A team
|
|
59
|
-
* window holding is a warn because it stops every
|
|
61
|
+
* window holding is a warn because it stops every role (the director is exempt); held per-role
|
|
60
62
|
* windows are ok because the operator chose those roles by name. A malformed value reads as
|
|
61
63
|
* unset here — checkInit owns that error. `now` is injected so tests never read the clock. */
|
|
62
64
|
export function checkQuietHours(config, now = new Date()) {
|
|
@@ -89,13 +91,13 @@ export function checkQuietHours(config, now = new Date()) {
|
|
|
89
91
|
* own `maxDailyCostUsdPerRole` cap. Read-only and built on the scheduler's own definitions
|
|
90
92
|
* (teamDailyCost, budgetReached, roleCapVerdicts), so the verdict cannot disagree with the gate.
|
|
91
93
|
* `published` is the running scheduler's figure (it includes runs charged since the last save)
|
|
92
|
-
* and wins over the persisted sum, as in status. A reached team cap is a warn because
|
|
94
|
+
* and wins over the persisted sum, as in status. A reached team cap is a warn because roles
|
|
93
95
|
* run on their tier fallback or hold until local midnight (the `fallback` row says which);
|
|
94
96
|
* per-role caps reached alone are ok because the operator chose them by name. */
|
|
95
97
|
export function checkBudget(root, config, published, now = Date.now()) {
|
|
96
98
|
if (config === undefined)
|
|
97
99
|
return okOutcome("not checked — tumwater.json unreadable");
|
|
98
|
-
const states =
|
|
100
|
+
const states = loadRunnerStates(root, config);
|
|
99
101
|
const spent = published?.spentUsd ?? teamDailyCost(states, now);
|
|
100
102
|
const caps = config.maxDailyCostUsdPerRole;
|
|
101
103
|
const capped = [...roleCapVerdicts(states.map((s) => ({ role: s.role, state: s })), caps, now)]
|
|
@@ -116,63 +118,65 @@ export function checkBudget(root, config, published, now = Date.now()) {
|
|
|
116
118
|
return okOutcome(`${phrase} today${cappedPhrase}`);
|
|
117
119
|
}
|
|
118
120
|
/** The `maintenance quota` check: rolling-24 h maintenance merges against the allowance
|
|
119
|
-
* (`maintenancePerWorkMerge` per work merge + the daily floor), naming the maintenance
|
|
121
|
+
* (`maintenancePerWorkMerge` per work merge + the daily floor), naming the maintenance roles held
|
|
120
122
|
* when it is full. Read-only and built on the same verdict the scheduler and status use
|
|
121
|
-
* (maintenanceQuotaForPoll). Only a live team's running
|
|
123
|
+
* (maintenanceQuotaForPoll). Only a live team's running roles and merge queue count as in flight,
|
|
122
124
|
* since a dead team's leftover `running` flag and queue drain nothing. A held allowance is a warn
|
|
123
125
|
* because it stops a whole tier. */
|
|
124
126
|
export function checkMaintenanceQuota(root, config, aliveTeam, now = Date.now()) {
|
|
125
127
|
if (config === undefined)
|
|
126
128
|
return okOutcome("not checked — tumwater.json unreadable");
|
|
127
|
-
const
|
|
129
|
+
const runners = loadRunnerStates(root, config).map((s) => ({ role: s.role, running: aliveTeam && s.running === true }));
|
|
128
130
|
const queued = aliveTeam ? new Set(readMergeQueue(root).entries.map((e) => e.role)) : new Set();
|
|
129
|
-
const { allowance, used, held } = maintenanceQuotaForPoll(root, config.maintenancePerWorkMerge,
|
|
131
|
+
const { allowance, used, held } = maintenanceQuotaForPoll(root, config.maintenancePerWorkMerge, runners, queued, now);
|
|
130
132
|
if (held.length > 0) {
|
|
131
133
|
return warnOutcome(`allowance reached — ${used}/${allowance} maintenance merges in the last 24h; held until the window rolls under: ${[...held].sort().join(", ")}`);
|
|
132
134
|
}
|
|
133
135
|
const base = `${used}/${allowance} maintenance merges in the last 24h (${config.maintenancePerWorkMerge} per work merge + ${MAINTENANCE_DAILY_FLOOR} floor)`;
|
|
134
|
-
const anyMaintenance =
|
|
136
|
+
const anyMaintenance = runners.some((l) => commitTier(l.role) === "maintenance");
|
|
135
137
|
return okOutcome(anyMaintenance ? base : `${base}; no maintenance role enabled`);
|
|
136
138
|
}
|
|
137
|
-
/** The
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
/** The `<backend> window` check, one row per subscription backend in use (claude and codex):
|
|
140
|
+
* whether a rejected subscription window holds the subscription `<backend>` seams. Read-only and
|
|
141
|
+
* built on windowObservation (the backend's own), the verdict the scheduler's team hold uses, so
|
|
142
|
+
* it cannot disagree with the hold. A hold is a warn because it stops every subscription seam on
|
|
143
|
+
* that backend until the window resets; otherwise the most constrained window's state is
|
|
144
|
+
* reported as ok (a rejected window spending overage reads `rejected` here: overage spends, it
|
|
145
|
+
* does not hold). Returns null (omitting the row from doctor) when the backend has no
|
|
146
|
+
* subscription account — the same omit-a-row pattern as `pi extensions` — because an absent or
|
|
147
|
+
* API-billed account never holds. */
|
|
148
|
+
export function checkSubscriptionWindow(backend, root, now = Date.now(), chain, config) {
|
|
149
|
+
const account = readAccount(root, backend, now);
|
|
145
150
|
if (account?.billing !== "subscription")
|
|
146
|
-
return
|
|
147
|
-
const held =
|
|
151
|
+
return null;
|
|
152
|
+
const held = windowObservation(backend, account, now);
|
|
148
153
|
if (held?.kind === "subscription-window" && held.windowType !== undefined && held.resetsAt !== undefined) {
|
|
149
|
-
const label =
|
|
154
|
+
const label = windowLabel(held.windowType);
|
|
150
155
|
const resets = humanUntil(held.resetsAt, now);
|
|
151
|
-
const moved = MODEL_TIERS.filter((t) =>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
156
|
+
const moved = MODEL_TIERS.filter((t) => (chain?.[t]?.reason === "window" || chain?.[t]?.reason === "reserve") &&
|
|
157
|
+
config !== undefined &&
|
|
158
|
+
withModelOverrides(config, t, {}).agent === backend);
|
|
155
159
|
if (moved.length > 0) {
|
|
156
|
-
const waiting = MODEL_TIERS.filter((t) => !chain?.[t] && config !== undefined && withModelOverrides(config, t, {}).agent ===
|
|
160
|
+
const waiting = MODEL_TIERS.filter((t) => !chain?.[t] && config !== undefined && withModelOverrides(config, t, {}).agent === backend);
|
|
157
161
|
return warnOutcome(`${label} window rejected — ${moved.join(", ")} moved to fallbacks until it resets in ${resets}${waiting.length > 0 ? `; ${waiting.join(", ")} wait` : ""}`);
|
|
158
162
|
}
|
|
159
|
-
return warnOutcome(`${label} window rejected — subscription
|
|
163
|
+
return warnOutcome(`${label} window rejected — subscription ${backendDisplayName(backend)} model jobs wait until it resets in ${resets}`);
|
|
160
164
|
}
|
|
161
|
-
const top =
|
|
165
|
+
const top = windowEntries(account)[0];
|
|
162
166
|
if (top === undefined)
|
|
163
167
|
return okOutcome("subscription; no window reported yet");
|
|
164
|
-
const pct = typeof top.utilization === "number" ? ` (${
|
|
165
|
-
const label =
|
|
168
|
+
const pct = typeof top.utilization === "number" ? ` (${utilizationPct(top.utilization)}% used)` : "";
|
|
169
|
+
const label = windowLabel(top.type);
|
|
166
170
|
return okOutcome(top.status === "allowed_warning" ? `${label} window nearing its limit${pct}` : `${label} window ${top.status}${pct}`);
|
|
167
171
|
}
|
|
168
172
|
/** How far back the `provider hold` check reads for hold/resume crossings. The longest a
|
|
169
173
|
* hold can stay is HOLD_CAP_MS (team-hold's 15-minute cap that escalation doubles against),
|
|
170
174
|
* so a window a comfortable multiple of that covers any standing hold's crossing event plus
|
|
171
175
|
* its resume — an older resumed event must not be missed or a re-opened provider reads held. */
|
|
172
|
-
const HOLD_CHECK_WINDOW_MS = 2 *
|
|
176
|
+
const HOLD_CHECK_WINDOW_MS = 2 * HOUR_MS;
|
|
173
177
|
/** The `provider hold` check: names each provider whose team-wide failure hold
|
|
174
178
|
* (src/team/team-hold.ts, polled by pollTeamHold) stands now — a 429/backend-failure storm
|
|
175
|
-
* that stops the roles on that provider from starting new
|
|
179
|
+
* that stops the roles on that provider from starting new runs until it re-opens. Read-only:
|
|
176
180
|
* it infers the standing set from the event log's own `rate_limit_hold`/`rate_limit_resumed`
|
|
177
181
|
* crossings, the events pollTeamHold writes, so it cannot disagree with the hold. A standing
|
|
178
182
|
* hold is a warn (as with the `claude window` row) because it stops every role on that
|
|
@@ -210,16 +214,16 @@ export function checkProviderHolds(root, now = Date.now()) {
|
|
|
210
214
|
});
|
|
211
215
|
return warnOutcome(lines.join("; "));
|
|
212
216
|
}
|
|
213
|
-
/** The `error streaks` check: names each enabled
|
|
217
|
+
/** The `error streaks` check: names each enabled role whose `consecutiveErrors` is at or past
|
|
214
218
|
* ERROR_STREAK_WARN, with its streak and last error, and whether it is paused. Read-only; it reads
|
|
215
219
|
* the same `consecutiveErrors` the error-streak breaker and `teamAlerts` read, and the same
|
|
216
220
|
* `pausedRoles` marker the scheduler reads. The marker does not record who wrote it, so a paused
|
|
217
|
-
*
|
|
218
|
-
* fail: a failing
|
|
221
|
+
* role at ERROR_STREAK_BREAKER's bar is described, not attributed to the breaker. A warn, not a
|
|
222
|
+
* fail: a failing role does not stop the team from running. */
|
|
219
223
|
export function checkErrorStreaks(root, config) {
|
|
220
224
|
if (config === undefined)
|
|
221
225
|
return okOutcome("not checked — tumwater.json unreadable");
|
|
222
|
-
const failing =
|
|
226
|
+
const failing = loadRunnerStates(root, config)
|
|
223
227
|
.filter((s) => (s.consecutiveErrors ?? 0) >= ERROR_STREAK_WARN);
|
|
224
228
|
if (failing.length === 0)
|
|
225
229
|
return okOutcome("no role failing repeatedly");
|
|
@@ -227,7 +231,7 @@ export function checkErrorStreaks(root, config) {
|
|
|
227
231
|
const phrases = failing.map((s) => {
|
|
228
232
|
const n = s.consecutiveErrors ?? 0;
|
|
229
233
|
let phrase = `${roleLabel(s.role)}: ${n} failed runs`;
|
|
230
|
-
if (
|
|
234
|
+
if (runnerPaused(paused, s.role)) {
|
|
231
235
|
const key = paused.has(s.role) ? s.role : paused.has(baseRoleOf(s.role)) ? baseRoleOf(s.role) : `${s.role}-1`;
|
|
232
236
|
phrase +=
|
|
233
237
|
n >= ERROR_STREAK_BREAKER
|
|
@@ -240,23 +244,35 @@ export function checkErrorStreaks(root, config) {
|
|
|
240
244
|
});
|
|
241
245
|
return warnOutcome(`failing run after run: ${phrases.join("; ")}`);
|
|
242
246
|
}
|
|
243
|
-
/** The `main red` check: names the code
|
|
244
|
-
* each blocked
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
|
|
247
|
+
/** The `main red` check: names the code roles the red-main gate (mainRedGate) holds, ending
|
|
248
|
+
* each blocked run `main_red` while main's suite is red, and warns when main has been red over
|
|
249
|
+
* RED_MAIN_WARN_MS even with no role held (the red-main gate's guardrail, G1). Read-only: it
|
|
250
|
+
* asks heldByMainRed, the predicate the status phase's `main red` label reads, so the two
|
|
251
|
+
* cannot disagree on the held roles, and redMainWarning over the event trail, so both phrases
|
|
252
|
+
* can appear in one row. A warn, not a fail: a red main does not stop the team from running.
|
|
253
|
+
* `now` is injected so a test can pin a red's age. */
|
|
254
|
+
export function checkMainRed(root, config, now = Date.now()) {
|
|
248
255
|
if (config === undefined)
|
|
249
256
|
return okOutcome("not checked — tumwater.json unreadable");
|
|
250
257
|
const mainCheck = mainCheckForPoll(root, config);
|
|
251
|
-
const ids =
|
|
252
|
-
const held =
|
|
258
|
+
const ids = runnerIds(config);
|
|
259
|
+
const held = loadRunnerStates(root, config)
|
|
253
260
|
.filter((s) => heldByMainRed(s, mainCheck));
|
|
254
|
-
|
|
261
|
+
const phrases = [];
|
|
262
|
+
if (held.length > 0) {
|
|
263
|
+
const err = held.find((s) => s.lastError)?.lastError;
|
|
264
|
+
const cause = err ? ` — ${squash(err, 160)}` : "";
|
|
265
|
+
const healer = ids.some((id) => baseRoleOf(id) === "bugfix")
|
|
266
|
+
? "; the bugfix role is exempt and repairs it"
|
|
267
|
+
: "; no bugfix role is enabled to repair it — fix main by hand or enable bugfix";
|
|
268
|
+
phrases.push(`main's check is failing; ${plural(held.length, "code role")} waiting: ${held.map((s) => roleLabel(s.role)).join(", ")}${cause}${healer}`);
|
|
269
|
+
}
|
|
270
|
+
const events = readEventsSince(root, DAY_MS).events;
|
|
271
|
+
const red = redMainWarning(events, now);
|
|
272
|
+
if (red !== null)
|
|
273
|
+
phrases.push(red);
|
|
274
|
+
phrases.push(...revertWarnings(events, loadQuarantine(root), now));
|
|
275
|
+
if (phrases.length === 0)
|
|
255
276
|
return okOutcome("no role waiting on main's check");
|
|
256
|
-
|
|
257
|
-
const cause = err ? ` — ${squash(err, 160)}` : "";
|
|
258
|
-
const healer = ids.some((id) => baseRoleOf(id) === "bugfix")
|
|
259
|
-
? "; the bugfix role is exempt and repairs it"
|
|
260
|
-
: "; no bugfix role is enabled to repair it — fix main by hand or enable bugfix";
|
|
261
|
-
return warnOutcome(`main's check is failing; ${plural(held.length, "code role")} waiting: ${held.map((s) => roleLabel(s.role)).join(", ")}${cause}${healer}`);
|
|
277
|
+
return warnOutcome(phrases.join("; "));
|
|
262
278
|
}
|