tumwater 0.4.2 → 0.4.4
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 +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import { childEnv } from "../process/process.js";
|
|
3
|
-
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./probe.js";
|
|
3
|
+
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./doctor-probe.js";
|
|
4
4
|
import { claudeBackend, claudeConfigDir } from "../agent/claude/claude-backend.js";
|
|
5
5
|
import { claudeEnv } from "../agent/claude/claude-env.js";
|
|
6
6
|
import { claudeAuthUsesSubscription, parseClaudeAuthStatus } from "../agent/claude/claude-account.js";
|
|
7
7
|
import { resolvedBackendConfig } from "../config/config-views.js";
|
|
8
8
|
import { envPath } from "../files/files.js";
|
|
9
9
|
import { belowNodeFloor } from "../version.js";
|
|
10
|
-
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
10
|
+
import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
11
11
|
import { claudeSeamsInUse, loadConfigForCheck, versionFromProbe, seamedModels, } from "./doctor-model-checks.js";
|
|
12
12
|
/** The doctor's claude credential and version readiness checks, split out of
|
|
13
13
|
* doctor-model-checks.ts (whose shared seam/model framework and the pi and tier-models
|
|
@@ -16,9 +16,24 @@ import { claudeSeamsInUse, loadConfigForCheck, versionFromProbe, seamedModels, }
|
|
|
16
16
|
* distinct concern from the filesystem/git checks, kept together so a change to how doctor
|
|
17
17
|
* spawns or scrubs claude does not touch the other model checks. The shared report contract
|
|
18
18
|
* (CheckOutcome) and the seam/model framework (loadConfigForCheck, claudeSeamsInUse,
|
|
19
|
-
* seamPairs) come from doctor-model-checks.ts, and the AUTH_CHECK_* probe budgets
|
|
20
|
-
* with the shared report contract (CheckOutcome) from
|
|
21
|
-
* modules share them. */
|
|
19
|
+
* seamPairs) come from doctor-model-checks.ts, and the AUTH_CHECK_* probe budgets
|
|
20
|
+
* from doctor-probe.ts, with the shared report contract (CheckOutcome) from
|
|
21
|
+
* doctor-outcome.ts, exactly as the other doctor-* modules share them. */
|
|
22
|
+
/** The config a claude readiness row reads, or the warn outcome a broken `tumwater.json`
|
|
23
|
+
* earns, or the ok stand-down when no seam routes to claude. The claude rows repeat the same
|
|
24
|
+
* three-line guard and the same seam stand-down (`loadConfigForCheck` + `claudeSeamsInUse`);
|
|
25
|
+
* folding them here mirrors codexCheckBase so a change to how doctor guards a claude check
|
|
26
|
+
* touches one spot instead of three. Each caller still checks the seams it needs — the auth
|
|
27
|
+
* rows use `claudeSeamsInUse` directly (folded here), the version row goes on to
|
|
28
|
+
* `seamedModels` for the floors. */
|
|
29
|
+
function claudeCheckBase(root) {
|
|
30
|
+
const config = loadConfigForCheck(root);
|
|
31
|
+
if ("level" in config)
|
|
32
|
+
return config;
|
|
33
|
+
if (!claudeSeamsInUse(config))
|
|
34
|
+
return okOutcome("no model jobs run on claude");
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
22
37
|
/** One bounded probe of the resolved `claude` binary under the scrubbed run environment a real
|
|
23
38
|
* run gets: resolve the binary through the claude adapter that would spawn it and run `args`
|
|
24
39
|
* with a short deadline. Null when the probe could not run, the child errored, or the deadline
|
|
@@ -47,6 +62,13 @@ async function claudeAuthProbe(config, pathEnv, timeoutMs) {
|
|
|
47
62
|
return null;
|
|
48
63
|
return parseClaudeAuthStatus(result.stdout);
|
|
49
64
|
}
|
|
65
|
+
/** The warn verdict both auth-driven claude rows return when `claude auth status` could not
|
|
66
|
+
* run or its reply was unreadable — the one home of the wording so checkClaudeAuth and
|
|
67
|
+
* checkAnthroApiKey cannot drift on the unreadable-probe message. Each caller makes its own
|
|
68
|
+
* solitary `claudeAuthProbe` call and branches on null through it. */
|
|
69
|
+
function claudeAuthUnreadable() {
|
|
70
|
+
return warnOutcome("could not check — `claude auth status` did not run or its output was unreadable");
|
|
71
|
+
}
|
|
50
72
|
/** claude credential readiness (plans/agent-backends.md "Every seam, checked"): a claude seam
|
|
51
73
|
* whose login is gone fails every run before it can work, so doctor runs `claude auth status`
|
|
52
74
|
* under the scrubbed run environment and fails when it reports `loggedIn: false`. An ok detail
|
|
@@ -55,14 +77,12 @@ async function claudeAuthProbe(config, pathEnv, timeoutMs) {
|
|
|
55
77
|
* probe that could not run or a reply this cannot read warns rather than guesses. Stands down
|
|
56
78
|
* (ok) when no seam routes to claude, so a pi-only team spawns nothing. */
|
|
57
79
|
export async function checkClaudeAuth(root, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
58
|
-
const config =
|
|
80
|
+
const config = claudeCheckBase(root);
|
|
59
81
|
if ("level" in config)
|
|
60
82
|
return config;
|
|
61
|
-
if (!claudeSeamsInUse(config))
|
|
62
|
-
return okOutcome("no model jobs run on claude");
|
|
63
83
|
const auth = await claudeAuthProbe(config, pathEnv, timeoutMs);
|
|
64
84
|
if (auth === null)
|
|
65
|
-
return
|
|
85
|
+
return claudeAuthUnreadable();
|
|
66
86
|
if (!auth.loggedIn)
|
|
67
87
|
return failOutcome("claude reports logged in: false — run `claude auth login`");
|
|
68
88
|
const subscription = claudeAuthUsesSubscription(auth);
|
|
@@ -80,54 +100,40 @@ export async function checkClaudeAuth(root, pathEnv = envPath(), timeoutMs = AUT
|
|
|
80
100
|
* an API-key login (then there is no subscription to lose). `env` is injectable so the check
|
|
81
101
|
* stays hermetic; a probe that cannot run warns rather than guesses like checkClaudeAuth. */
|
|
82
102
|
export async function checkAnthroApiKey(root, env = process.env, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
83
|
-
const config =
|
|
103
|
+
const config = claudeCheckBase(root);
|
|
84
104
|
if ("level" in config)
|
|
85
105
|
return config;
|
|
86
|
-
if (!claudeSeamsInUse(config))
|
|
87
|
-
return okOutcome("no model jobs run on claude");
|
|
88
106
|
const apiKey = env.ANTHROPIC_API_KEY;
|
|
89
107
|
if (apiKey === undefined || apiKey === "")
|
|
90
108
|
return okOutcome("no ANTHROPIC_API_KEY set");
|
|
91
109
|
const auth = await claudeAuthProbe(config, pathEnv, timeoutMs);
|
|
92
110
|
if (auth === null)
|
|
93
|
-
return
|
|
111
|
+
return claudeAuthUnreadable();
|
|
94
112
|
if (!auth.loggedIn || !claudeAuthUsesSubscription(auth))
|
|
95
113
|
return okOutcome("ANTHROPIC_API_KEY is set, but claude reports no claude.ai login");
|
|
96
114
|
return warnOutcome("ANTHROPIC_API_KEY is set while claude reports a claude.ai login — model jobs on claude will bill the API key, not the subscription; unset it or route every anthropic model job on pi explicitly");
|
|
97
115
|
}
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
100
|
-
|
|
101
|
-
* highest floor among the claude-routed models, naming which model needs it; an ok detail
|
|
102
|
-
* prints the installed version and the floor it satisfied (or no floor when none applies).
|
|
103
|
-
* The `off`/`minimal` thinking clamp is noted here too, since this check already has the
|
|
104
|
-
* claude seams in hand. Stands down when no seam routes to claude. */
|
|
105
|
-
export async function checkClaudeVersion(root, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
106
|
-
const config = loadConfigForCheck(root);
|
|
107
|
-
if ("level" in config)
|
|
108
|
-
return config;
|
|
109
|
-
const seams = seamedModels(config, "claude");
|
|
110
|
-
if (seams.length === 0)
|
|
111
|
-
return okOutcome("no model jobs run on claude");
|
|
112
|
-
// The floor each configured model needs; a model the adapter does not gate contributes none.
|
|
116
|
+
/** The floor each configured model needs, keyed by model; a model the adapter does not gate
|
|
117
|
+
* contributes none. */
|
|
118
|
+
function claudeVersionFloors(seams) {
|
|
113
119
|
const floors = new Map();
|
|
114
120
|
for (const seam of seams) {
|
|
115
121
|
const min = claudeBackend.minVersion?.(seam.model);
|
|
116
122
|
if (min !== undefined)
|
|
117
123
|
floors.set(seam.model, min);
|
|
118
124
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
return floors;
|
|
126
|
+
}
|
|
127
|
+
/** The clamp note when any claude seam clamps thinking, else empty. */
|
|
128
|
+
function claudeVersionClampNote(seams) {
|
|
123
129
|
const clamped = seams.some((seam) => seam.thinking === "off" || seam.thinking === "minimal");
|
|
124
130
|
const clampNote = clamped
|
|
125
131
|
? "; `off`/`minimal` thinking clamps to `--effort low` (claude has no lower setting)"
|
|
126
132
|
: "";
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
return clampNote;
|
|
134
|
+
}
|
|
135
|
+
/** The highest floor decides: whichever model needs the newest CLI. */
|
|
136
|
+
function claudeVersionNeededBy(floors) {
|
|
131
137
|
let required = "";
|
|
132
138
|
let neededBy = "";
|
|
133
139
|
for (const [model, min] of floors) {
|
|
@@ -136,7 +142,35 @@ export async function checkClaudeVersion(root, pathEnv = envPath(), timeoutMs =
|
|
|
136
142
|
neededBy = model;
|
|
137
143
|
}
|
|
138
144
|
}
|
|
145
|
+
return { required, neededBy };
|
|
146
|
+
}
|
|
147
|
+
/** Verdict on the found version against the claude seams' floors, carrying the clamp note. */
|
|
148
|
+
function claudeVersionOutcome(found, floors, clampNote) {
|
|
149
|
+
if (floors.size === 0)
|
|
150
|
+
return okOutcome(`claude ${found}${clampNote}`);
|
|
151
|
+
const { required, neededBy } = claudeVersionNeededBy(floors);
|
|
139
152
|
if (belowNodeFloor(found, `>=${required}`))
|
|
140
153
|
return failOutcome(`claude ${found} is older than ${required} required by ${neededBy} — run \`claude update\``);
|
|
141
154
|
return okOutcome(`claude ${found} (>= ${required} for ${neededBy})${clampNote}`);
|
|
142
155
|
}
|
|
156
|
+
/** claude CLI-version readiness (plans/agent-backends.md finding 9): an installed CLI below a
|
|
157
|
+
* configured model's minimum fails or misprices that model, and team runs disable the
|
|
158
|
+
* auto-updater, so the version an operator has is the version agent runs use. Fails below the
|
|
159
|
+
* highest floor among the claude-routed models, naming which model needs it; an ok detail
|
|
160
|
+
* prints the installed version and the floor it satisfied (or no floor when none applies).
|
|
161
|
+
* The `off`/`minimal` thinking clamp is noted here too, since this check already has the
|
|
162
|
+
* claude seams in hand. Stands down when no seam routes to claude. */
|
|
163
|
+
export async function checkClaudeVersion(root, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
164
|
+
const config = claudeCheckBase(root);
|
|
165
|
+
if ("level" in config)
|
|
166
|
+
return config;
|
|
167
|
+
const seams = seamedModels(config, "claude");
|
|
168
|
+
if (seams.length === 0)
|
|
169
|
+
return okOutcome("no model jobs run on claude");
|
|
170
|
+
const floors = claudeVersionFloors(seams);
|
|
171
|
+
const result = await claudeProbe(config, ["--version"], pathEnv, timeoutMs);
|
|
172
|
+
const found = versionFromProbe(result);
|
|
173
|
+
if (found === undefined)
|
|
174
|
+
return warnOutcome("could not check — `claude --version` did not run or its output was unreadable");
|
|
175
|
+
return claudeVersionOutcome(found, floors, claudeVersionClampNote(seams));
|
|
176
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { childEnv } from "../process/process.js";
|
|
4
|
-
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./probe.js";
|
|
4
|
+
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./doctor-probe.js";
|
|
5
5
|
import { codexBackend, CODEX_MIN_VERSION } from "../agent/codex/codex-backend.js";
|
|
6
6
|
import { codexEffort, EFFORT_BY_THINKING } from "../agent/codex/codex-args.js";
|
|
7
7
|
import { codexEnv } from "../agent/codex/codex-env.js";
|
|
8
8
|
import { codexHome, readCodexCatalog } from "../agent/codex/codex-catalog.js";
|
|
9
9
|
import { codexHookTrust } from "../agent/codex/codex-hook-trust.js";
|
|
10
|
-
import { parseCodexLoginStatus } from "../agent/codex/codex-account.js";
|
|
10
|
+
import { codexApiKeyRefusalMessage, parseCodexLoginStatus } from "../agent/codex/codex-account.js";
|
|
11
11
|
import { codexWriteGuardDefinition } from "../agent/codex/codex-write-guard.js";
|
|
12
12
|
import { resolvedBackendConfig } from "../config/config-views.js";
|
|
13
13
|
import { envPath, isDirectory, readTextOrNull, statOrNull, treeBytes, walkFiles } from "../files/files.js";
|
|
14
14
|
import { formatBytes, tokenWindowLabel } from "../text/format.js";
|
|
15
15
|
import { errorMessage } from "../text/text.js";
|
|
16
|
-
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
16
|
+
import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
17
17
|
import { codexSeamsInUse, loadConfigForCheck, minVersionOutcome, versionFromProbe, seamedModels, } from "./doctor-model-checks.js";
|
|
18
18
|
/** The doctor's codex rows (part 9a/9, plans/codex-backend.md §4, §7, "Hand steps"): one
|
|
19
19
|
* exported check per row — install/version, login, home, models, account, leaks, sessions
|
|
@@ -25,8 +25,8 @@ import { codexSeamsInUse, loadConfigForCheck, minVersionOutcome, versionFromProb
|
|
|
25
25
|
* pi-only team shows no codex row and spawns nothing and reads nothing. The shared
|
|
26
26
|
* report contract (CheckOutcome) and the seam/model framework (loadConfigForCheck,
|
|
27
27
|
* codexSeamsInUse, seamPairs) come from doctor-model-checks.ts, the AUTH_CHECK_* probe budgets
|
|
28
|
-
* from probe.ts, and the shared report contract (CheckOutcome) from
|
|
29
|
-
* doctor-* modules share them. */
|
|
28
|
+
* from doctor-probe.ts, and the shared report contract (CheckOutcome) from
|
|
29
|
+
* doctor-outcome.ts, as the other doctor-* modules share them. */
|
|
30
30
|
/** The config and codex home a codex readiness row reads, or the warn outcome a broken
|
|
31
31
|
* `tumwater.json` earns, or null when no seam routes to codex (the row's stand-down). All
|
|
32
32
|
* eight rows repeat the same three-line guard and the same home lookup
|
|
@@ -83,7 +83,7 @@ export async function checkCodexAuth(root, pathEnv = envPath(), timeoutMs = AUTH
|
|
|
83
83
|
if (status === "subscription")
|
|
84
84
|
return okOutcome("logged in using ChatGPT — billing: subscription");
|
|
85
85
|
if (status === "api")
|
|
86
|
-
return failOutcome(
|
|
86
|
+
return failOutcome(codexApiKeyRefusalMessage(home));
|
|
87
87
|
if (/Not logged in/i.test(result.stdout ?? ""))
|
|
88
88
|
return failOutcome(`codex is not logged in — run \`CODEX_HOME=${home} codex login --device-auth\``);
|
|
89
89
|
return warnOutcome("could not check — `codex login status` reply was unreadable");
|
|
@@ -18,24 +18,20 @@ import { finiteNumber, stringList } from "../files/json-object.js";
|
|
|
18
18
|
import { commitExists, deleteRefIfAt, listBranches, refSha, refsUnder } from "../git/git.js";
|
|
19
19
|
import { readSlotsState } from "../git/slots-state.js";
|
|
20
20
|
import { eventsSince } from "../events/event-window.js";
|
|
21
|
-
import { DAY_MS } from "../text/datetime.js";
|
|
21
|
+
import { DAY_MS, HOUR_MS } from "../text/datetime.js";
|
|
22
22
|
import { shortSha } from "../text/format.js";
|
|
23
23
|
import { errorMessage, joinParts, textOr } from "../text/text.js";
|
|
24
24
|
import { rankCountEntries } from "../collections.js";
|
|
25
25
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
26
|
+
import { sampleHostLoad } from "../build/host-sleep.js";
|
|
26
27
|
import { INSTANCE_ROLES, runnerEnabled, runnerIds } from "../roles/runner-ids.js";
|
|
27
28
|
import { staleClaimDetail, staleClaimEntries } from "../scheduling/claim-entries.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
29
|
import { classifyLock, readLockPid } from "../concurrency/lock.js";
|
|
34
30
|
import { branchName, mergeLockDir, worktreesDir } from "../paths.js";
|
|
35
|
-
import { artifactPhrase, retireRole } from "../operator/retire.js";
|
|
31
|
+
import { artifactPhrase, retireRole } from "../operator/operator-retire.js";
|
|
36
32
|
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
37
33
|
import { agree, plural, shortSpanPhrase } from "../text/phrases.js";
|
|
38
|
-
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
34
|
+
import { failOutcome, noConfigOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
39
35
|
/** Merge lock — read-only classification via classifyLock (the same three cases the breaker
|
|
40
36
|
* uses). A stale lock is a warning, not a failure: it self-heals on the next merge. */
|
|
41
37
|
export function checkMergeLock(root) {
|
|
@@ -75,8 +71,8 @@ export function staleWorkspacePins(root, now = Date.now()) {
|
|
|
75
71
|
* the pin's whole-ms age). `role` is a non-optional string on StaleWorkspacePin, but the
|
|
76
72
|
* clause renders it through textOr like the row always did, so a malformed pin can never
|
|
77
73
|
* print nothing where the phrase expects a role. */
|
|
78
|
-
|
|
79
|
-
return `${textOr(pin.role)} pinned on ${pin.dir} for ${Math.round(pin.ageMs /
|
|
74
|
+
function stalePinPhrase(pin) {
|
|
75
|
+
return `${textOr(pin.role)} pinned on ${pin.dir} for ${Math.round(pin.ageMs / HOUR_MS)}h`;
|
|
80
76
|
}
|
|
81
77
|
/** Directory names under the worktrees root that the pool no longer owns: a legacy per-role
|
|
82
78
|
* `<role>` checkout (the pre-pool layout) or a `_land-<role>` checkout from the merge queue. The
|
|
@@ -86,11 +82,11 @@ function leftoverWorktreeDirs(root) {
|
|
|
86
82
|
return subdirectoryNames(worktreesDir(root), (name) => name !== DIRECTOR_ROLE && !(name.startsWith("_") && !name.startsWith("_land-")));
|
|
87
83
|
}
|
|
88
84
|
/** The window of `slot_wait` events the pool row sums, and the total wait inside it that warns. */
|
|
89
|
-
|
|
85
|
+
const SLOT_WAIT_CHECK_WINDOW_MS = DAY_MS;
|
|
90
86
|
const SLOT_WAIT_WARN_TOTAL_MS = 10 * 60_000;
|
|
91
|
-
|
|
87
|
+
const PROVIDER_WAIT_CHECK_WINDOW_MS = DAY_MS;
|
|
92
88
|
const PROVIDER_WAIT_WARN_TOTAL_MS = 10 * 60_000;
|
|
93
|
-
|
|
89
|
+
const CHECK_WAIT_CHECK_WINDOW_MS = DAY_MS;
|
|
94
90
|
const CHECK_WAIT_WARN_TOTAL_MS = 10 * 60_000;
|
|
95
91
|
/** Fold a window's `*_wait` events into the three numbers every contention check reads: how
|
|
96
92
|
* many waits there were (each event counts once), the total ms they waited, and the longest
|
|
@@ -116,26 +112,34 @@ function sumWaits(waits) {
|
|
|
116
112
|
}
|
|
117
113
|
return { count, totalMs, longestMs, longest };
|
|
118
114
|
}
|
|
115
|
+
/** The leading wait-summary clause every `*_wait` contention row shares — the wait count, the
|
|
116
|
+
* total and the longest span over the trailing day. Only the verb naming the waited-for thing
|
|
117
|
+
* differs between the three rows ("to start", "for a provider slot", "for a check slot"), so
|
|
118
|
+
* this single home keeps the shared wording — the same `plural("wait")`/`shortSpanPhrase`
|
|
119
|
+
* bucketing — from drifting between them. Each row appends its own bucket/knob tail after it. */
|
|
120
|
+
function waitDetail(count, verb, totalMs, longestMs) {
|
|
121
|
+
return `${plural(count, "wait")} ${verb} in the last 24h, ${shortSpanPhrase(totalMs)} total, longest ${shortSpanPhrase(longestMs)}`;
|
|
122
|
+
}
|
|
119
123
|
/** The `slot_wait` half of checkWorktreePool's rule: whether the window's waits cross
|
|
120
124
|
* SLOT_WAIT_WARN_TOTAL_MS and the pool row's exact detail clause — the wait count, total and
|
|
121
125
|
* longest spans (naming the role and whether it was a check or a run), and, on a warn, the
|
|
122
126
|
* knob to raise past `slotCount`. Shared by the doctor's `workspaces` row and the always-on
|
|
123
127
|
* contention alert, so the trigger and the advice cannot drift. */
|
|
124
|
-
|
|
128
|
+
function slotWaitContention(waits, slotCount) {
|
|
125
129
|
const { count, totalMs, longestMs, longest } = sumWaits(waits);
|
|
126
130
|
const contended = totalMs >= SLOT_WAIT_WARN_TOTAL_MS;
|
|
127
131
|
const purpose = longest.purpose === "verify" ? "check" : "run";
|
|
128
|
-
const detail = `${
|
|
132
|
+
const detail = `${waitDetail(count, "to start", totalMs, longestMs)} (${textOr(longest.role)} ${purpose})${contended ? ` — raise worktreeSlots above ${slotCount}` : ""}`;
|
|
129
133
|
return { warn: contended, detail };
|
|
130
134
|
}
|
|
131
135
|
/** Provider-slot contention's rule (the body of checkProviderContention from `sumWaits` on):
|
|
132
136
|
* sums the waits, ranks the per-key buckets busiest first (the key-less bucket, which also
|
|
133
137
|
* takes every team-cap wait whatever its key, reads "the run cap") and, on a warn past
|
|
134
|
-
* PROVIDER_WAIT_WARN_TOTAL_MS, names
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
|
|
138
|
+
* PROVIDER_WAIT_WARN_TOTAL_MS, names `maxConcurrent` to raise. Shared by the doctor's
|
|
139
|
+
* `provider contention` row and the always-on contention alert, so the trigger and the advice
|
|
140
|
+
* cannot drift. The shared rule leaves the count-zero and read-error arms to
|
|
141
|
+
* checkProviderContention. */
|
|
142
|
+
function providerWaitContention(waits, config) {
|
|
139
143
|
const perKey = new Map();
|
|
140
144
|
const { count, totalMs, longestMs } = sumWaits(waits);
|
|
141
145
|
for (const ev of waits) {
|
|
@@ -154,17 +158,13 @@ export function providerWaitContention(waits, config) {
|
|
|
154
158
|
const parenthetical = ranked.length === 1
|
|
155
159
|
? `(${bucketLabel(ranked[0].key)})`
|
|
156
160
|
: `(${cells[0]}, also ${cells.slice(1).join(", ")})`;
|
|
157
|
-
let detail = `${
|
|
161
|
+
let detail = `${waitDetail(count, "for a provider slot", totalMs, longestMs)} ${parenthetical}`;
|
|
158
162
|
const contended = totalMs >= PROVIDER_WAIT_WARN_TOTAL_MS;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const teamAdvice = perKey.has("") && config !== undefined ? [`raise maxConcurrent above ${config.maxConcurrent}`] : [];
|
|
165
|
-
const all = [...(advice ? [advice] : []), ...teamAdvice].join(" and ");
|
|
166
|
-
if (all)
|
|
167
|
-
detail += ` — ${all}`;
|
|
163
|
+
// The one configurable bound is the team total, so a warn names `maxConcurrent` to raise —
|
|
164
|
+
// but only for a team-cap wait, the only kind that bound can affect (a keyed-only wait from
|
|
165
|
+
// an old event shape has no knob to name).
|
|
166
|
+
if (contended && config !== undefined && perKey.has("")) {
|
|
167
|
+
detail += ` — raise maxConcurrent above ${config.maxConcurrent}`;
|
|
168
168
|
}
|
|
169
169
|
return { warn: contended, detail };
|
|
170
170
|
}
|
|
@@ -174,7 +174,7 @@ export function providerWaitContention(waits, config) {
|
|
|
174
174
|
* busiest first. Shared by the doctor's `check contention` row and the always-on contention
|
|
175
175
|
* alert, so the trigger and the advice cannot drift. The shared rule leaves the count-zero
|
|
176
176
|
* and read-error arms to checkCheckContention. */
|
|
177
|
-
|
|
177
|
+
function checkWaitContention(waits, config, load) {
|
|
178
178
|
const perKind = new Map();
|
|
179
179
|
const { count, totalMs, longestMs } = sumWaits(waits);
|
|
180
180
|
for (const ev of waits) {
|
|
@@ -183,21 +183,31 @@ export function checkWaitContention(waits, config) {
|
|
|
183
183
|
perKind.set(kind, (perKind.get(kind) ?? 0) + waited);
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
-
let detail =
|
|
186
|
+
let detail = waitDetail(count, "for a check slot", totalMs, longestMs);
|
|
187
187
|
if (perKind.size > 0) {
|
|
188
188
|
const ranked = rankCountEntries(perKind);
|
|
189
189
|
detail += ` (behind ${ranked.map(([kind, ms]) => `${kind} ${shortSpanPhrase(ms)}`).join(", ")})`;
|
|
190
190
|
}
|
|
191
191
|
if (totalMs < CHECK_WAIT_WARN_TOTAL_MS)
|
|
192
192
|
return { warn: false, detail };
|
|
193
|
-
|
|
193
|
+
// The cap is only the right lever while the host can actually run more work: once the
|
|
194
|
+
// one-minute load average exceeds the core count (the same "starved" bar build-check-events
|
|
195
|
+
// uses for an on-time deadline kill), the wait is the host's own load, not an under-sized
|
|
196
|
+
// pool — so name the load and suppress the raise advice rather than telling an overloaded
|
|
197
|
+
// host to admit still more concurrent checks. The check still warns: the waits are real.
|
|
198
|
+
if (load.load1 > load.cpus) {
|
|
199
|
+
detail += ` — host load average ${Math.round(load.load1)} on ${load.cpus} cores: saturated, so raising maxConcurrentChecks is not the lever`;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
detail += ` — raise maxConcurrentChecks above ${config?.maxConcurrentChecks ?? defaultConfig().maxConcurrentChecks}`;
|
|
203
|
+
}
|
|
194
204
|
return { warn: true, detail };
|
|
195
205
|
}
|
|
196
206
|
/** Worktree pool (plans/worktree-pool.md, part 5/5): report the pooled slots and warn on the
|
|
197
207
|
* states an operator should act on — a legacy `<role>` or `_land-*` checkout the pool migration
|
|
198
208
|
* left behind, a pin older than PIN_STALE_MS (a paused role holding a checkout), and the last
|
|
199
|
-
* 24 h of `slot_wait` events totalling SLOT_WAIT_WARN_TOTAL_MS (the pool is too small).
|
|
200
|
-
* pin is
|
|
209
|
+
* 24 h of `slot_wait` events totalling SLOT_WAIT_WARN_TOTAL_MS (the pool is too small). A stale
|
|
210
|
+
* pin is named by this row's stale-pins clause. Read-only,
|
|
201
211
|
* like every doctor check; `now` is a test seam for the pin-age arithmetic. */
|
|
202
212
|
export function checkWorktreePool(root, now = Date.now()) {
|
|
203
213
|
const slots = readSlotsState(root).slots;
|
|
@@ -209,8 +219,8 @@ export function checkWorktreePool(root, now = Date.now()) {
|
|
|
209
219
|
if (leftover.length > 0)
|
|
210
220
|
parts.push(`legacy worktrees still present: ${leftover.join(", ")}`);
|
|
211
221
|
if (stale.length > 0) {
|
|
212
|
-
// The stale-pin clause renders the shared staleWorkspacePins selection (the same list
|
|
213
|
-
//
|
|
222
|
+
// The stale-pin clause renders the shared staleWorkspacePins selection (the same list this
|
|
223
|
+
// row's detail words), so the row's wording and the shared selection cannot drift.
|
|
214
224
|
// cannot disagree on what a stale pin is or how old it is.
|
|
215
225
|
parts.push(stale.map(stalePinPhrase).join("; "));
|
|
216
226
|
}
|
|
@@ -236,13 +246,13 @@ export function checkWorktreePool(root, now = Date.now()) {
|
|
|
236
246
|
* ok-fallback arms the two siblings repeated, so they cannot drift on which inputs read ok;
|
|
237
247
|
* the row-specific rule (`providerWaitContention` / `checkWaitContention`) is `rule`. `noun`
|
|
238
248
|
* names the waits in the ok message. */
|
|
239
|
-
function waitContentionRow(root, windowMs, type, noun, rule, config, now) {
|
|
249
|
+
function waitContentionRow(root, windowMs, type, noun, rule, config, now, load = sampleHostLoad) {
|
|
240
250
|
let count = 0;
|
|
241
251
|
let verdict;
|
|
242
252
|
try {
|
|
243
253
|
const waits = eventsSince(root, windowMs, now, (ev) => ev.type === type);
|
|
244
254
|
count = sumWaits(waits).count;
|
|
245
|
-
verdict = rule(waits, config);
|
|
255
|
+
verdict = rule(waits, config, load());
|
|
246
256
|
}
|
|
247
257
|
catch (err) {
|
|
248
258
|
// A read error leaves the level ok, exactly like the slot-wait branch's own read: the
|
|
@@ -258,7 +268,7 @@ function waitContentionRow(root, windowMs, type, noun, rule, config, now) {
|
|
|
258
268
|
* orchestrator-launch.ts) and warns when the total reaches PROVIDER_WAIT_WARN_TOTAL_MS — the
|
|
259
269
|
* provider cap is too small. Names every key that waited, busiest first (the key-less
|
|
260
270
|
* bucket, which also takes every team-cap wait whatever its key, reads "the run cap") and, on
|
|
261
|
-
* a warn,
|
|
271
|
+
* a warn, names `maxConcurrent` for the run cap.
|
|
262
272
|
* A counterpoint to checkWorktreePool's slot-wait half; read-only, like every doctor check.
|
|
263
273
|
* `now` is a test seam for the window. */
|
|
264
274
|
export function checkProviderContention(root, config, now = Date.now()) {
|
|
@@ -279,8 +289,8 @@ function checkHolderKind(label) {
|
|
|
279
289
|
* detail gains `(behind <kind> <span>, …)` after the longest clause, kinds busiest first: a
|
|
280
290
|
* wait counts once toward each distinct kind that held a slot. `now` is a test seam for the
|
|
281
291
|
* window. */
|
|
282
|
-
export function checkCheckContention(root, config, now = Date.now()) {
|
|
283
|
-
return waitContentionRow(root, CHECK_WAIT_CHECK_WINDOW_MS, "check_wait", "check waits", checkWaitContention, config, now);
|
|
292
|
+
export function checkCheckContention(root, config, now = Date.now(), load = sampleHostLoad) {
|
|
293
|
+
return waitContentionRow(root, CHECK_WAIT_CHECK_WINDOW_MS, "check_wait", "check waits", checkWaitContention, config, now, load);
|
|
284
294
|
}
|
|
285
295
|
/** The ids `leftover role branches` calls retirable and the branches it leaves for manual
|
|
286
296
|
* deletion — the one walk the row and the reap share, so the two can never disagree on what a
|
|
@@ -317,7 +327,7 @@ export async function retirableLeftoverRoles(root, config) {
|
|
|
317
327
|
* an unreadable tumwater.json skips it. */
|
|
318
328
|
export async function checkLeftoverRoleBranches(root, config) {
|
|
319
329
|
if (!config)
|
|
320
|
-
return
|
|
330
|
+
return noConfigOutcome();
|
|
321
331
|
const { retirable, unknown } = await retirableLeftoverRoles(root, config);
|
|
322
332
|
const parts = [];
|
|
323
333
|
if (retirable.length > 0) {
|
|
@@ -440,7 +450,7 @@ function claimCounts(claims) {
|
|
|
440
450
|
* tumwater.json (config undefined) skips the walk — checkInit reports the config itself. */
|
|
441
451
|
export function checkWorkInstances(root, config, now = Date.now()) {
|
|
442
452
|
if (!config)
|
|
443
|
-
return
|
|
453
|
+
return noConfigOutcome();
|
|
444
454
|
const claims = runnerIds(config).map((id) => loadRunnerState(root, id).claim).filter((c) => c !== undefined);
|
|
445
455
|
const entries = staleClaimEntries(root, runnerIds(config), now);
|
|
446
456
|
const gone = entries.filter((e) => e.kind === "gone");
|
|
@@ -453,13 +463,11 @@ export function checkWorkInstances(root, config, now = Date.now()) {
|
|
|
453
463
|
return warnOutcome(parts.join("; "));
|
|
454
464
|
}
|
|
455
465
|
/** An enabled multi-instance role contending for a worktree pool smaller than the run cap —
|
|
456
|
-
* the pool-mismatch clause of the doctor's `parallel instances` row (checkWorkInstances)
|
|
457
|
-
* the
|
|
458
|
-
* `INSTANCE_ROLES` ids with more than one configured instance while `slotCount(config)`
|
|
466
|
+
* the pool-mismatch clause of the doctor's `parallel instances` row (checkWorkInstances):
|
|
467
|
+
* the `INSTANCE_ROLES` ids with more than one configured instance while `slotCount(config)`
|
|
459
468
|
* (worktreeSlots, else maxConcurrent + 1) is below `config.maxConcurrent`, phrased as the
|
|
460
469
|
* row's one clause; `[]` when no such role or the pool is at least `maxConcurrent`. Shared by
|
|
461
|
-
* the
|
|
462
|
-
* wording. */
|
|
470
|
+
* the row and its clause so they cannot drift on the trigger, the role list, or the wording. */
|
|
463
471
|
export function smallPoolProblems(config) {
|
|
464
472
|
const multi = Object.entries(config.roles)
|
|
465
473
|
.filter(([id, role]) => INSTANCE_ROLES.has(id) && role.enabled && (role.instances ?? 1) > 1)
|
|
@@ -469,71 +477,3 @@ export function smallPoolProblems(config) {
|
|
|
469
477
|
return [];
|
|
470
478
|
return [`${multi.join(", ")} run instances but worktreeSlots ${slots} is below maxConcurrent ${config.maxConcurrent}`];
|
|
471
479
|
}
|
|
472
|
-
/** Provider concurrency caps (plans/provider-concurrency.md §8): audit the
|
|
473
|
-
* `maxConcurrentPerProvider` map against the seams the config can actually spawn — each role,
|
|
474
|
-
* the review gate, the resolver, the director, and each tier's fallback — so the operator
|
|
475
|
-
* sees whether every cap names a provider in use and whether any cap actually binds. Warns on
|
|
476
|
-
* a cap key no seam resolves to (a typo, or a provider not in use), a cap at or above
|
|
477
|
-
* `maxConcurrent` (which never binds — the team total is the real ceiling), and a capped
|
|
478
|
-
* team whose seams include one with no key (pi's default provider is unreadable, so that
|
|
479
|
-
* seam cannot be capped). A config with no per-provider map or no caps is fine, and so is a
|
|
480
|
-
* well-formed map; a broken config is reported by the init check. Takes `root` to read pi's
|
|
481
|
-
* own default model (piSettingsPath), the same seam the orchestrator's admission uses, so a
|
|
482
|
-
* bare no-model pi seam keys exactly as the team would key it. */
|
|
483
|
-
/** The warnings behind checkProviderConcurrency, as a pure function of the config and pi's
|
|
484
|
-
* default model name: a cap key no seam resolves to (a typo, or a provider not in use), a cap
|
|
485
|
-
* at or above `maxConcurrent` (which never binds), and a capped team whose seams include one
|
|
486
|
-
* with no key (pi's default provider is unreadable, so that seam cannot be capped). The doctor's
|
|
487
|
-
* `provider concurrency` row and the always-on `provider-caps` team alert both read this one
|
|
488
|
-
* computation, so the row and the alert cannot drift on the trigger or the wording. `[]` for no
|
|
489
|
-
* config, a map with no enforcedKeyCaps entries, or a map with no problem. `piDefault` is the
|
|
490
|
-
* bare default model the orchestrator's admission seam carries, read once by the callers
|
|
491
|
-
* through readPiDefaultModel(piSettingsPath(piModelsPath()), root). */
|
|
492
|
-
export function providerConcurrencyProblems(config, piDefault) {
|
|
493
|
-
if (!config)
|
|
494
|
-
return [];
|
|
495
|
-
const caps = config.maxConcurrentPerProvider;
|
|
496
|
-
const capEntries = enforcedKeyCaps(caps);
|
|
497
|
-
if (capEntries.length === 0)
|
|
498
|
-
return [];
|
|
499
|
-
const seams = seamsInUse(config);
|
|
500
|
-
const keyed = [];
|
|
501
|
-
for (const s of seams) {
|
|
502
|
-
const k = seamConcurrencyKey(s, piDefault);
|
|
503
|
-
if (k !== undefined && !keyed.includes(k))
|
|
504
|
-
keyed.push(k);
|
|
505
|
-
}
|
|
506
|
-
const keySet = new Set(keyed);
|
|
507
|
-
const capKeys = capEntries.map(([k]) => k);
|
|
508
|
-
const warnings = [];
|
|
509
|
-
for (const key of capKeys) {
|
|
510
|
-
if (!keySet.has(key))
|
|
511
|
-
warnings.push(`cap key "${key}" resolves to no model job (a typo, or a provider not in use)`);
|
|
512
|
-
}
|
|
513
|
-
for (const [key, cap] of capEntries) {
|
|
514
|
-
if (cap >= config.maxConcurrent)
|
|
515
|
-
warnings.push(`cap "${key}" ${cap} is at or above maxConcurrent ${config.maxConcurrent} — it never binds`);
|
|
516
|
-
}
|
|
517
|
-
const capSet = new Set(capKeys);
|
|
518
|
-
const keyless = seams.filter((s) => seamConcurrencyKey(s, piDefault) === undefined);
|
|
519
|
-
if (capSet.size > 0 && keyless.length > 0) {
|
|
520
|
-
warnings.push(`${plural(keyless.length, "model job")} ${agree(keyless.length, "resolves", "resolve")} to no provider key (pi's default provider is unreadable, so they cannot be capped): ${keyless.map((s) => s.label).join(", ")}`);
|
|
521
|
-
}
|
|
522
|
-
return warnings;
|
|
523
|
-
}
|
|
524
|
-
export function checkProviderConcurrency(config, root) {
|
|
525
|
-
if (!config)
|
|
526
|
-
return okOutcome("no config to audit");
|
|
527
|
-
const caps = config.maxConcurrentPerProvider;
|
|
528
|
-
const capEntries = enforcedKeyCaps(caps);
|
|
529
|
-
if (capEntries.length === 0)
|
|
530
|
-
return okOutcome("no per-provider caps configured");
|
|
531
|
-
// The pi default model the orchestrator's admission seam carries: a bare no-model pi seam
|
|
532
|
-
// resolves its key only through it, so a team whose default is readable has keyed seams and
|
|
533
|
-
// only an unreadable default leaves a key-less seam to warn about.
|
|
534
|
-
const piDefault = readPiDefaultModel(piSettingsPath(piModelsPath()), root);
|
|
535
|
-
const problems = providerConcurrencyProblems(config, piDefault);
|
|
536
|
-
if (problems.length === 0)
|
|
537
|
-
return okOutcome(`${plural(capEntries.length, "provider cap")} — all model jobs resolve`);
|
|
538
|
-
return warnOutcome(problems.join("; "));
|
|
539
|
-
}
|