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
|
@@ -6,7 +6,7 @@ import { formatClock } from "../text/datetime.js";
|
|
|
6
6
|
import { loadRunnerStates } from "./doctor-holds.js";
|
|
7
7
|
import { modelPairName, pairDailyCost } from "../budget/budget.js";
|
|
8
8
|
import { usd, usdCap } from "../text/format.js";
|
|
9
|
-
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
9
|
+
import { okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
10
10
|
/** The doctor's cap-fallback checks, split out of doctor-model-checks.ts (whose seam framework
|
|
11
11
|
* and declared-tier/image checks stay in a module whose per-backend provider checks live in
|
|
12
12
|
* doctor-{claude,opencode,codex,pi}-checks.ts): the daily-cost budget's third state — whether
|
|
@@ -18,7 +18,7 @@ import { okOutcome, warnOutcome } from "./outcome.js";
|
|
|
18
18
|
* kept separate so a change to fallback pricing does not touch how doctor reads seams. The
|
|
19
19
|
* config guard (loadConfigForCheck) is imported from doctor-model-checks, exactly as the
|
|
20
20
|
* per-backend modules import that file's seam framework. The shared report contract
|
|
21
|
-
* (CheckOutcome) is imported from outcome.ts. */
|
|
21
|
+
* (CheckOutcome) is imported from doctor-outcome.ts. */
|
|
22
22
|
/** Fallback model readiness — the daily-cost budget's third state (plans/fallback-model.md): with
|
|
23
23
|
* `fallbackModel` set, pi's definitions must price that pair at zero or the gate refuses it and
|
|
24
24
|
* roles pause at the cap exactly as if no fallback existed. That refusal is the right runtime
|
|
@@ -77,6 +77,48 @@ const isLocalUrl = (url) => {
|
|
|
77
77
|
return false;
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
|
+
/** Which URL this step probes, and did it answer. A provider that names a backend other than
|
|
81
|
+
* pi (claude today; opencode from part 2) never asks pi for a base URL or runs pi's price
|
|
82
|
+
* probe — only a pi step may. Returns the step's backend-ness, the resolved base URL and
|
|
83
|
+
* whether a local pi step answered; a non-local or non-pi step never probes (up stays null). */
|
|
84
|
+
async function resolveStepProbe(modelsPath, pair, probe) {
|
|
85
|
+
const nonPiBackend = pair.backend !== undefined && pair.backend !== "pi";
|
|
86
|
+
const base = !nonPiBackend && pair.provider ? piProviderBaseUrl(modelsPath, pair.provider) : null;
|
|
87
|
+
const local = base !== null && isLocalUrl(base);
|
|
88
|
+
const up = local ? await probe(`${base}/models`) : null;
|
|
89
|
+
return { nonPiBackend, base, up };
|
|
90
|
+
}
|
|
91
|
+
/** The claude step's billing display: extra usage when the step follows an earlier claude
|
|
92
|
+
* model, else the persisted account's billing answer (or unknowns when the account is absent). */
|
|
93
|
+
function claudeVerdict(extra, account) {
|
|
94
|
+
return extra ? "extra usage" : (account?.billing ?? "billing unknown");
|
|
95
|
+
}
|
|
96
|
+
/** Why this step is not runnable now (or is, when no arm fires): the five-way verdict, and
|
|
97
|
+
* whether the chain is down. The `up === false` arm is the only one that marks the chain down.
|
|
98
|
+
* `accounts` prices a free/claude pair for the one claude account held by the caller. */
|
|
99
|
+
function stepVerdict(pair, ctx) {
|
|
100
|
+
const { up, base, cap, spent, providers, accounts } = ctx;
|
|
101
|
+
if (up === false) {
|
|
102
|
+
return { verdict: `not running (GET ${base}/models failed)`, down: true };
|
|
103
|
+
}
|
|
104
|
+
else if (cap !== undefined && spent >= cap) {
|
|
105
|
+
return { verdict: `cap reached (${usd(spent)}/${usdCap(cap)})`, down: false };
|
|
106
|
+
}
|
|
107
|
+
else if (pair.backend !== undefined && pair.backend !== "pi") {
|
|
108
|
+
return { verdict: `claude ${claudeVerdict(pair.extraUsage, accounts("claude"))}`, down: false };
|
|
109
|
+
}
|
|
110
|
+
else if (piPairFree(providers, pair, accounts)) {
|
|
111
|
+
return { verdict: up ? "free · server up" : "free", down: false };
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
verdict: cap !== undefined ? `priced, ${usd(spent)}/${usdCap(cap)} today` : "priced, team cap only",
|
|
115
|
+
down: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** One chain fragment's line: the tier, the 1-based step number and the step's pair and verdict. */
|
|
119
|
+
function chainFragment(tier, step, pair, verdict) {
|
|
120
|
+
return `${tier} ${step + 1}: ${pair.provider ?? "?"}/${pair.model ?? "?"} — ${verdict}`;
|
|
121
|
+
}
|
|
80
122
|
/** The `fallback chain` check: one fragment per step of every tier's chain saying whether the
|
|
81
123
|
* step can run now — a local pi server that does not answer (warn), a step whose own daily cap is
|
|
82
124
|
* spent, a free pair, a claude step with its billing, or a priced pair. Reads config, pi's
|
|
@@ -98,32 +140,12 @@ export async function checkFallbackChain(root, modelsPath = piModelsPath(), prob
|
|
|
98
140
|
let down = false;
|
|
99
141
|
const fragments = [];
|
|
100
142
|
for (const { tier, step, pair } of steps) {
|
|
101
|
-
|
|
102
|
-
// asks pi for a base URL or runs pi's price probe — only a pi step may.
|
|
103
|
-
const nonPiBackend = pair.backend !== undefined && pair.backend !== "pi";
|
|
104
|
-
const base = !nonPiBackend && pair.provider ? piProviderBaseUrl(modelsPath, pair.provider) : null;
|
|
105
|
-
const local = base !== null && isLocalUrl(base);
|
|
106
|
-
const up = local ? await probe(`${base}/models`) : null;
|
|
143
|
+
const { base, up } = await resolveStepProbe(modelsPath, pair, probe);
|
|
107
144
|
const cap = pair.maxDailyCostUsd;
|
|
108
145
|
const spent = cap !== undefined ? pairDailyCost(states, modelPairName(pair), now) : 0;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
verdict = `not running (GET ${base}/models failed)`;
|
|
113
|
-
}
|
|
114
|
-
else if (cap !== undefined && spent >= cap) {
|
|
115
|
-
verdict = `cap reached (${usd(spent)}/${usdCap(cap)})`;
|
|
116
|
-
}
|
|
117
|
-
else if (nonPiBackend) {
|
|
118
|
-
verdict = `claude ${pair.extraUsage ? "extra usage" : (account?.billing ?? "billing unknown")}`;
|
|
119
|
-
}
|
|
120
|
-
else if (piPairFree(providers, pair, accounts)) {
|
|
121
|
-
verdict = up ? "free · server up" : "free";
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
verdict = cap !== undefined ? `priced, ${usd(spent)}/${usdCap(cap)} today` : "priced, team cap only";
|
|
125
|
-
}
|
|
126
|
-
fragments.push(`${tier} ${step + 1}: ${pair.provider ?? "?"}/${pair.model ?? "?"} — ${verdict}`);
|
|
146
|
+
const { verdict, down: isDown } = stepVerdict(pair, { up, base, cap, spent, providers, accounts });
|
|
147
|
+
down ||= isDown;
|
|
148
|
+
fragments.push(chainFragment(tier, step, pair, verdict));
|
|
127
149
|
}
|
|
128
150
|
const detail = fragments.join("; ");
|
|
129
151
|
return down ? warnOutcome(detail) : okOutcome(detail);
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { roleLabel } from "../roles/role-labels.js";
|
|
2
2
|
import { sortEntriesByKey } from "../collections.js";
|
|
3
3
|
import { pausedRoles, pausedRolesUntil, standingTeamPause } from "../team/team-state.js";
|
|
4
|
-
import { MODEL_TIERS } from "../config/config-schema.js";
|
|
5
|
-
import { withModelOverrides } from "../config/config-views.js";
|
|
6
4
|
import { commitTier, DIRECTOR_ROLE } from "../roles/roles.js";
|
|
7
5
|
import { budgetReached, budgetWarning, teamDailyCost } from "../budget/budget.js";
|
|
8
6
|
import { roleCapVerdicts } from "../gates/role-cap-gates.js";
|
|
@@ -17,17 +15,13 @@ import { usd } from "../text/format.js";
|
|
|
17
15
|
import { parseQuietHours, quietHoursStatus, quietWindowEnd, roleQuietHold } from "../scheduling/quiet-hours.js";
|
|
18
16
|
import { DAY_MS, HOUR_MS, humanUntil } from "../text/datetime.js";
|
|
19
17
|
import { budgetPhrase, pauseReasonSuffix, plural } from "../text/phrases.js";
|
|
20
|
-
import { readAccount, utilizationPct, windowEntries, windowLabel } from "../agent/subscription-account.js";
|
|
21
|
-
import { backendDisplayName } from "../agent/backend-traits.js";
|
|
22
|
-
import { windowObservation } from "../team/team-polls.js";
|
|
23
18
|
import { readEventsSince } from "../events/event-window.js";
|
|
24
19
|
import { errorMessage } from "../text/text.js";
|
|
25
|
-
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
20
|
+
import { okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
26
21
|
import { redMainWarning } from "../baseline/main-guardrails.js";
|
|
27
|
-
/** The standing-hold checks: `pause` (checkPause), `quiet hours` (checkQuietHours),
|
|
28
|
-
* (checkBudget),
|
|
29
|
-
*
|
|
30
|
-
* read-only reports of holds on running. */
|
|
22
|
+
/** The standing-hold checks: `pause` (checkPause), `quiet hours` (checkQuietHours), `budget`
|
|
23
|
+
* (checkBudget), `maintenance quota` (checkMaintenanceQuota), and `error streaks`
|
|
24
|
+
* (checkErrorStreaks), all read-only reports of holds on running. */
|
|
31
25
|
export function loadRunnerStates(root, config) {
|
|
32
26
|
return runnerIds(config)
|
|
33
27
|
.map((id) => loadRunnerState(root, id))
|
|
@@ -139,39 +133,6 @@ export function checkMaintenanceQuota(root, config, aliveTeam, now = Date.now())
|
|
|
139
133
|
const anyMaintenance = runners.some((l) => commitTier(l.role) === "maintenance");
|
|
140
134
|
return okOutcome(anyMaintenance ? base : `${base}; no maintenance role enabled`);
|
|
141
135
|
}
|
|
142
|
-
/** The `<backend> window` check, one row per subscription backend in use (claude and codex):
|
|
143
|
-
* whether a rejected subscription window holds the subscription `<backend>` seams. Read-only and
|
|
144
|
-
* built on windowObservation (the backend's own), the verdict the scheduler's team hold uses, so
|
|
145
|
-
* it cannot disagree with the hold. A hold is a warn because it stops every subscription seam on
|
|
146
|
-
* that backend until the window resets; otherwise the most constrained window's state is
|
|
147
|
-
* reported as ok (a rejected window spending overage reads `rejected` here: overage spends, it
|
|
148
|
-
* does not hold). Returns null (omitting the row from doctor) when the backend has no
|
|
149
|
-
* subscription account — the same omit-a-row pattern as `pi extensions` — because an absent or
|
|
150
|
-
* API-billed account never holds. */
|
|
151
|
-
export function checkSubscriptionWindow(backend, root, now = Date.now(), chain, config) {
|
|
152
|
-
const account = readAccount(root, backend, now);
|
|
153
|
-
if (account?.billing !== "subscription")
|
|
154
|
-
return null;
|
|
155
|
-
const held = windowObservation(backend, account, now);
|
|
156
|
-
if (held?.kind === "subscription-window" && held.windowType !== undefined && held.resetsAt !== undefined) {
|
|
157
|
-
const label = windowLabel(held.windowType);
|
|
158
|
-
const resets = humanUntil(held.resetsAt, now);
|
|
159
|
-
const moved = MODEL_TIERS.filter((t) => (chain?.[t]?.reason === "window" || chain?.[t]?.reason === "reserve") &&
|
|
160
|
-
config !== undefined &&
|
|
161
|
-
withModelOverrides(config, t, {}).agent === backend);
|
|
162
|
-
if (moved.length > 0) {
|
|
163
|
-
const waiting = MODEL_TIERS.filter((t) => !chain?.[t] && config !== undefined && withModelOverrides(config, t, {}).agent === backend);
|
|
164
|
-
return warnOutcome(`${label} window rejected — ${moved.join(", ")} moved to fallbacks until it resets in ${resets}${waiting.length > 0 ? `; ${waiting.join(", ")} wait` : ""}`);
|
|
165
|
-
}
|
|
166
|
-
return warnOutcome(`${label} window rejected — subscription ${backendDisplayName(backend)} model jobs wait until it resets in ${resets}`);
|
|
167
|
-
}
|
|
168
|
-
const top = windowEntries(account)[0];
|
|
169
|
-
if (top === undefined)
|
|
170
|
-
return okOutcome("subscription; no window reported yet");
|
|
171
|
-
const pct = typeof top.utilization === "number" ? ` (${utilizationPct(top.utilization)}% used)` : "";
|
|
172
|
-
const label = windowLabel(top.type);
|
|
173
|
-
return okOutcome(top.status === "allowed_warning" ? `${label} window nearing its limit${pct}` : `${label} window ${top.status}${pct}`);
|
|
174
|
-
}
|
|
175
136
|
/** How far back the `provider hold` check reads for hold/resume crossings. The longest a
|
|
176
137
|
* hold can stay is HOLD_CAP_MS (team-hold's 15-minute cap that escalation doubles against),
|
|
177
138
|
* so a window a comfortable multiple of that covers any standing hold's crossing event plus
|
|
@@ -182,8 +143,8 @@ const HOLD_CHECK_WINDOW_MS = 2 * HOUR_MS;
|
|
|
182
143
|
* that stops the roles on that provider from starting new runs until it re-opens. Read-only:
|
|
183
144
|
* it infers the standing set from the event log's own `rate_limit_hold`/`rate_limit_resumed`
|
|
184
145
|
* crossings, the events pollTeamHold writes, so it cannot disagree with the hold. A standing
|
|
185
|
-
* hold is a warn
|
|
186
|
-
*
|
|
146
|
+
* hold is a warn because it stops every role on that provider; `now` is injected so a test
|
|
147
|
+
* can pin a hold standing before its deadline and
|
|
187
148
|
* re-opened after. The ended hold's kind rides the resumed event (see pollTeamHold), and any
|
|
188
149
|
* resumed event ends that provider's current hold whatever its kind. */
|
|
189
150
|
export function checkProviderHolds(root, now = Date.now()) {
|
|
@@ -246,8 +207,8 @@ export function checkErrorStreaks(root, config) {
|
|
|
246
207
|
return warnOutcome(`failing run after run: ${phrases.join("; ")}`);
|
|
247
208
|
}
|
|
248
209
|
/** The `main red` check: the row lists the runs the merge-side `main_red` pin ended — a red
|
|
249
|
-
* main is still recorded as the `main_red` outcome that ended those runs until
|
|
250
|
-
*
|
|
210
|
+
* main is still recorded as the `main_red` outcome that ended those runs until part 3f/3
|
|
211
|
+
* lands — and warns when main has been red over RED_MAIN_WARN_MS even with no role held (the
|
|
251
212
|
* guardrail, G1). Read-only: it asks heldByMainRed, the predicate the status phase's `main red`
|
|
252
213
|
* label reads, so the two cannot disagree on the held runs, and redMainWarning over the event
|
|
253
214
|
* trail, so both phrases can appear in one row. A warn, not a fail: a red main does not stop
|
|
@@ -8,7 +8,7 @@ import { readEventsSince } from "../events/event-window.js";
|
|
|
8
8
|
import { DAY_MS } from "../text/datetime.js";
|
|
9
9
|
import { plural, shortSpanPhrase } from "../text/phrases.js";
|
|
10
10
|
import { errorMessage, textOr } from "../text/text.js";
|
|
11
|
-
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
11
|
+
import { okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
12
12
|
/** How far back a failed `dep_install` event may sit and still explain the drift. */
|
|
13
13
|
const DEP_INSTALL_CHECK_WINDOW_MS = DAY_MS;
|
|
14
14
|
export function checkRootInstall(root, now = Date.now()) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* launchservicesd facts this reads — the thresholds and the warning text — stay in
|
|
7
7
|
* src/process/launch-services.ts, shared with the running team's watch. `platform` is a
|
|
8
8
|
* parameter so the macOS branches are testable on any host. */
|
|
9
|
-
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
9
|
+
import { okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
10
10
|
import { systemProcessProbe } from "../process/process-table.js";
|
|
11
11
|
import { LAUNCH_SERVICES_KILL_PORTS, LAUNCH_SERVICES_WARN_PORTS, launchServicesWarning, } from "../process/launch-services.js";
|
|
12
12
|
import { compactTokens } from "../text/format.js";
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { childEnv } from "../process/process.js";
|
|
2
|
-
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./probe.js";
|
|
2
|
+
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./doctor-probe.js";
|
|
3
3
|
import { defaultConfig, loadConfigSafe } from "../config/config.js";
|
|
4
4
|
import { seamsInUse } from "../config/config-fallbacks.js";
|
|
5
5
|
import { cacheReadUnpriced, piModelsPath, readPiProviders } from "../pi/pi-models.js";
|
|
6
|
+
import { roleImageInfo } from "../agent/image-input.js";
|
|
7
|
+
import { readPiListedModels } from "../pi/pi-listed-models.js";
|
|
8
|
+
import { runnerIds } from "../roles/runner-ids.js";
|
|
6
9
|
import { envPath } from "../files/files.js";
|
|
7
10
|
import { piListedModels, piProviderAuth } from "./doctor-pi-checks.js";
|
|
8
11
|
import { agree } from "../text/phrases.js";
|
|
9
12
|
import { joinParts } from "../text/text.js";
|
|
10
|
-
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
13
|
+
import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
11
14
|
import { belowNodeFloor } from "../version.js";
|
|
12
15
|
/** The one phrase a configured-models check uses when the config declares no models at all:
|
|
13
16
|
* informational (capability falls to pi's own defaults), not a warning. Shared by
|
|
@@ -30,8 +33,9 @@ const PHRASE_CANNOT_READ_MODELS = "cannot check — could not read pi's model de
|
|
|
30
33
|
* modules do. The claude, opencode, codex and pi provider checks live in their own per-backend
|
|
31
34
|
* modules (doctor-claude-checks.ts, doctor-opencode-checks.ts, doctor-codex-checks.ts,
|
|
32
35
|
* doctor-pi-checks.ts), each importing this file's shared seam framework and the AUTH_CHECK_*
|
|
33
|
-
* probe budgets from probe.ts rather than duplicating them. The shared report contract
|
|
34
|
-
* (CheckOutcome) is imported from outcome.ts, exactly as the other doctor-*
|
|
36
|
+
* probe budgets from doctor-probe.ts rather than duplicating them. The shared report contract
|
|
37
|
+
* (CheckOutcome) is imported from doctor-outcome.ts, exactly as the other doctor-*
|
|
38
|
+
* modules do. Together
|
|
35
39
|
* these are the doctor's only checks that read pi's model definitions and, for the probes, spawn
|
|
36
40
|
* an agent binary — a distinct concern from the filesystem/git checks, kept together so a change
|
|
37
41
|
* to how doctor reads models does not touch the environment checks. */
|
|
@@ -131,30 +135,8 @@ export function codexSeamsInUse(config) {
|
|
|
131
135
|
export function piSeamsInUse(config) {
|
|
132
136
|
return seamsInUse(config).some((s) => s.agent === "pi");
|
|
133
137
|
}
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
* agent binary's own auth check. A strong tier pi cannot resolve would fail every review, and so
|
|
137
|
-
* every merge — the check exists so the operator finds out before the team does, mid-review. Pairs
|
|
138
|
-
* collected in the same sweep teamModelsFree prices: each tier's top-level selector (a string is
|
|
139
|
-
* default's), every enabled role's resolved pair (per-role overrides included; tier-name overrides
|
|
140
|
-
* resolve through configForRole), and the reviewer's while review is on. A pair with no model is
|
|
141
|
-
* pi's own default and out of scope; fallback pairs are the sibling check's (checkFallbackModel
|
|
142
|
-
* prices the cap pair). Levels: an unresolvable pair fails, a provider that is not ready warns
|
|
143
|
-
* (credentials are fixable without a config edit), a probe that cannot run warns rather than
|
|
144
|
-
* guesses, and a priced model whose cache reads declare $0 warns — the daily budget would
|
|
145
|
-
* undercount it (BUGS.md 2026-10-06). When one of PI_SMOL_MODEL / PI_SLOW_MODEL / PI_PLAN_MODEL is
|
|
146
|
-
* set the check also reports that tumwater does not read it — the variables are oh-my-pi's, pi
|
|
147
|
-
* ignores them, and with omp as `agentBin` they still reach omp through the inherited environment
|
|
148
|
-
* (src/pi/pi-backend.ts, built on src/process/run-marker.ts's backendBaseEnv) — pointing at the
|
|
149
|
-
* tier keys that do the same job (`model.small` / `model.strong`). `auth` is injectable so tests
|
|
150
|
-
* need no pi on PATH; `env` so the PI_* branches stay hermetic. */
|
|
151
|
-
export async function checkTierModels(root, modelsPath = piModelsPath(), auth = (provider) => piProviderAuth(loadConfigSafe(root).config ?? defaultConfig(), provider), env = process.env, listModels = () => piListedModels(loadConfigSafe(root).config ?? defaultConfig(), envPath(), AUTH_CHECK_TIMEOUT_MS, root)) {
|
|
152
|
-
const config = loadConfigForCheck(root);
|
|
153
|
-
if ("level" in config)
|
|
154
|
-
return config;
|
|
155
|
-
// Asked lazily and at most once: only a pair models.json does not declare needs the listing.
|
|
156
|
-
let listing;
|
|
157
|
-
const listed = () => (listing ??= listModels());
|
|
138
|
+
/** One job: sweep the config's seam pairs into the pair map and the routed-models map. */
|
|
139
|
+
function collectTierPairs(config) {
|
|
158
140
|
const pairs = new Map();
|
|
159
141
|
// Routed (non-pi) models keyed by the backend they resolved to, so the routing note names
|
|
160
142
|
// each backend correctly; opencode is not claude.
|
|
@@ -175,75 +157,100 @@ export async function checkTierModels(root, modelsPath = piModelsPath(), auth =
|
|
|
175
157
|
};
|
|
176
158
|
for (const pair of seamPairs(config))
|
|
177
159
|
add(pair.agent, pair.provider, pair.model);
|
|
160
|
+
return { pairs, routedModels };
|
|
161
|
+
}
|
|
162
|
+
/** One job: compute the oh-my-pi variables note, and whether any is set. */
|
|
163
|
+
function ompTierNote(env) {
|
|
178
164
|
const ompVars = ["PI_SMOL_MODEL", "PI_SLOW_MODEL", "PI_PLAN_MODEL"].filter((v) => env[v] !== undefined && env[v] !== "");
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
:
|
|
182
|
-
|
|
183
|
-
|
|
165
|
+
return {
|
|
166
|
+
set: ompVars.length > 0,
|
|
167
|
+
note: ompVars.length === 0
|
|
168
|
+
? ""
|
|
169
|
+
: ` — ${ompVars.join(" and ")} ${agree(ompVars.length, "is", "are")} set: tumwater does not read ${agree(ompVars.length, "it", "them")} (oh-my-pi's variables; pi ignores them; with omp as agentBin they still reach omp through the inherited environment) — set model.small / model.strong instead`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** One job: read pi's provider definitions, or the empty map when the file is unreadable but
|
|
173
|
+
* every pair names a provider the listing can back (the unreadable case the caller warns on). */
|
|
174
|
+
async function readTierProviders(modelsPath, pairs, listed) {
|
|
184
175
|
let providers = readPiProviders(modelsPath);
|
|
185
176
|
if (!providers && [...pairs.values()].some((p) => p.provider !== undefined) && (await listed()))
|
|
186
177
|
providers = new Map();
|
|
187
|
-
|
|
178
|
+
return providers;
|
|
179
|
+
}
|
|
180
|
+
/** One job: the fail string for a pair neither pi's definitions nor (when reachable) its
|
|
181
|
+
* listing resolve, or undefined when the listing does contain it. */
|
|
182
|
+
function unresolvedModelFail(provider, model, defs, listedModels, modelsPath) {
|
|
183
|
+
if (listedModels?.has(`${provider}/${model}`))
|
|
184
|
+
return undefined;
|
|
185
|
+
const providerListed = listedModels != null && [...listedModels].some((k) => k.startsWith(`${provider}/`));
|
|
186
|
+
if (!defs && !providerListed) {
|
|
187
|
+
return listedModels === null
|
|
188
|
+
? `provider ${provider} is not in pi's definitions (${modelsPath})`
|
|
189
|
+
: `provider ${provider} is not in pi's definitions (${modelsPath}) and \`pi --list-models\` lists none of its models — pi lists only providers it has credentials for`;
|
|
190
|
+
}
|
|
191
|
+
if (listedModels === null) {
|
|
192
|
+
// No exact entry: pi clones the provider's default model for the id, inheriting that
|
|
193
|
+
// default's price and context window (BUGS.md 2026-10-06) — name that consequence
|
|
194
|
+
// instead of only the failed lookup.
|
|
195
|
+
return `${provider}/${model} does not resolve in pi's definitions (${modelsPath}) — pi would price it at the provider default's rates and context window`;
|
|
196
|
+
}
|
|
197
|
+
return `${provider}/${model} is neither in pi's definitions (${modelsPath}) nor listed by \`pi --list-models\``;
|
|
198
|
+
}
|
|
199
|
+
/** One job: the warn string for a priced model whose cache reads declare $0, or undefined. */
|
|
200
|
+
function cacheReadWarning(providers, provider, model, def) {
|
|
201
|
+
if (def && cacheReadUnpriced(providers, provider, model)) {
|
|
202
|
+
return `${provider}/${model} prices cache reads at $0 — the daily budget will undercount its spend`;
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
/** One job: probe one provider's auth once, reporting ready or the warn when it is not. */
|
|
207
|
+
async function probeTierProvider(provider, auth, checkedProviders) {
|
|
208
|
+
if (checkedProviders.has(provider))
|
|
209
|
+
return {};
|
|
210
|
+
checkedProviders.add(provider);
|
|
211
|
+
const verdict = await auth(provider);
|
|
212
|
+
if (verdict === "ready")
|
|
213
|
+
return { ready: true };
|
|
214
|
+
if (verdict === "not-ready")
|
|
215
|
+
return { warn: `provider ${provider} reports not ready — check its credentials` };
|
|
216
|
+
return { warn: `could not verify provider ${provider} — the auth check did not run or its output was unreadable` };
|
|
217
|
+
}
|
|
218
|
+
/** One job: resolve one pi pair — the per-token claude warn, the definitions lookup and its
|
|
219
|
+
* unresolved fail, the free-cache-reads warn, and the once-per-provider auth probe. */
|
|
220
|
+
async function resolveTierPair(provider, model, providers, listed, auth, checkedProviders, modelsPath) {
|
|
188
221
|
const warns = [];
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const checkedProviders = new Set();
|
|
193
|
-
for (const { provider, model } of pairs.values()) {
|
|
194
|
-
if (provider === undefined) {
|
|
195
|
-
warns.push(`${model} names no provider — pi resolves it with its own default, tumwater cannot verify it`);
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
// A Claude model routed to pi (plans/provider-concurrency.md "Routing a bare Claude model id
|
|
199
|
-
// to claude"): a named provider keeps it on pi, but since Anthropic's 2026-04-04
|
|
200
|
-
// third-party-harness change a Claude plan's limits cover only first-party tools, so it is
|
|
201
|
-
// billed per token even under a Claude subscription login — the bare `claude-…` form on the
|
|
202
|
-
// claude CLI uses the plan's included usage instead.
|
|
203
|
-
if (model?.startsWith("claude-")) {
|
|
204
|
-
warns.push(`${provider}/${model} runs on pi and is billed per token, not the Claude plan's included usage — use a bare claude-… id to route it to the claude CLI`);
|
|
205
|
-
}
|
|
206
|
-
const defs = providers?.get(provider);
|
|
207
|
-
const def = defs?.find((d) => d.id === model);
|
|
208
|
-
if (!def) {
|
|
209
|
-
const listedModels = await listed();
|
|
210
|
-
if (!listedModels?.has(`${provider}/${model}`)) {
|
|
211
|
-
const providerListed = listedModels != null && [...listedModels].some((k) => k.startsWith(`${provider}/`));
|
|
212
|
-
if (!defs && !providerListed) {
|
|
213
|
-
fails.push(listedModels === null
|
|
214
|
-
? `provider ${provider} is not in pi's definitions (${modelsPath})`
|
|
215
|
-
: `provider ${provider} is not in pi's definitions (${modelsPath}) and \`pi --list-models\` lists none of its models — pi lists only providers it has credentials for`);
|
|
216
|
-
}
|
|
217
|
-
else if (listedModels === null) {
|
|
218
|
-
// No exact entry: pi clones the provider's default model for the id, inheriting that
|
|
219
|
-
// default's price and context window (BUGS.md 2026-10-06) — name that consequence
|
|
220
|
-
// instead of only the failed lookup.
|
|
221
|
-
fails.push(`${provider}/${model} does not resolve in pi's definitions (${modelsPath}) — pi would price it at the provider default's rates and context window`);
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
fails.push(`${provider}/${model} is neither in pi's definitions (${modelsPath}) nor listed by \`pi --list-models\``);
|
|
225
|
-
}
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (def && providers && cacheReadUnpriced(providers, provider, model)) {
|
|
230
|
-
// A priced model whose cache reads declare $0: pi bills cache-read tokens at zero and
|
|
231
|
-
// the daily budget undercounts the real spend (BUGS.md 2026-10-06).
|
|
232
|
-
warns.push(`${provider}/${model} prices cache reads at $0 — the daily budget will undercount its spend`);
|
|
233
|
-
}
|
|
234
|
-
if (!checkedProviders.has(provider)) {
|
|
235
|
-
checkedProviders.add(provider);
|
|
236
|
-
const verdict = await auth(provider);
|
|
237
|
-
if (verdict === "ready")
|
|
238
|
-
readyProviders.add(provider);
|
|
239
|
-
else if (verdict === "not-ready")
|
|
240
|
-
warns.push(`provider ${provider} reports not ready — check its credentials`);
|
|
241
|
-
else
|
|
242
|
-
warns.push(`could not verify provider ${provider} — the auth check did not run or its output was unreadable`);
|
|
243
|
-
}
|
|
222
|
+
if (provider === undefined) {
|
|
223
|
+
warns.push(`${model} names no provider — pi resolves it with its own default, tumwater cannot verify it`);
|
|
224
|
+
return { warns };
|
|
244
225
|
}
|
|
245
|
-
|
|
246
|
-
|
|
226
|
+
// A Claude model routed to pi (plans/provider-concurrency.md "Routing a bare Claude model id
|
|
227
|
+
// to claude"): a named provider keeps it on pi, but since Anthropic's 2026-04-04
|
|
228
|
+
// third-party-harness change a Claude plan's limits cover only first-party tools, so it is
|
|
229
|
+
// billed per token even under a Claude subscription login — the bare `claude-…` form on the
|
|
230
|
+
// claude CLI uses the plan's included usage instead.
|
|
231
|
+
if (model?.startsWith("claude-")) {
|
|
232
|
+
warns.push(`${provider}/${model} runs on pi and is billed per token, not the Claude plan's included usage — use a bare claude-… id to route it to the claude CLI`);
|
|
233
|
+
}
|
|
234
|
+
const defs = providers?.get(provider);
|
|
235
|
+
const def = defs?.find((d) => d.id === model);
|
|
236
|
+
if (!def) {
|
|
237
|
+
const listedModels = await listed();
|
|
238
|
+
const fail = unresolvedModelFail(provider, model, defs, listedModels, modelsPath);
|
|
239
|
+
if (fail !== undefined)
|
|
240
|
+
return { warns, fail };
|
|
241
|
+
}
|
|
242
|
+
if (providers) {
|
|
243
|
+
const warning = cacheReadWarning(providers, provider, model, def);
|
|
244
|
+
if (warning !== undefined)
|
|
245
|
+
warns.push(warning);
|
|
246
|
+
}
|
|
247
|
+
const probe = await probeTierProvider(provider, auth, checkedProviders);
|
|
248
|
+
if (probe.warn !== undefined)
|
|
249
|
+
warns.push(probe.warn);
|
|
250
|
+
return { warns, ready: probe.ready };
|
|
251
|
+
}
|
|
252
|
+
/** One job: one routing note per non-pi backend in use, joined. */
|
|
253
|
+
function routedTierNotes(routedModels) {
|
|
247
254
|
// One routing note per non-pi backend in use, so an opencode-routed model is never labeled
|
|
248
255
|
// as claude's. The opencode note also carries the design §2 caveat that a bare `opencode/…`
|
|
249
256
|
// id always means the OpenCode CLI, never pi's built-in `opencode` gateway. Every note opens
|
|
@@ -260,13 +267,67 @@ export async function checkTierModels(root, modelsPath = piModelsPath(), auth =
|
|
|
260
267
|
? routedNote(agent, n, "resolved by the codex backend, not pi's definitions")
|
|
261
268
|
: routedNote(agent, n, "resolved by the claude backend, not pi's definitions"));
|
|
262
269
|
}
|
|
263
|
-
|
|
270
|
+
return routedNotes.length > 0 ? routedNotes.join("; ") : "";
|
|
271
|
+
}
|
|
272
|
+
/** One job: assemble the final fail/warn/ok outcome from the collected verdicts. */
|
|
273
|
+
function tierOutcome(fails, warns, pairsCount, readyProviders, routedText, ompNote, ompSet) {
|
|
274
|
+
const found = [...fails, ...warns].join("; ");
|
|
275
|
+
const resolvedPi = `${pairsCount} declared ${agree(pairsCount, "model", "models")} resolve${readyProviders.size > 0 ? ` and ${agree(readyProviders.size, "provider", "all providers")} ${[...readyProviders].sort().join(", ")} report ready` : ""}`;
|
|
264
276
|
const base = fails.length === 0 && warns.length === 0
|
|
265
|
-
? joinParts([
|
|
277
|
+
? joinParts([pairsCount > 0 ? resolvedPi : "", routedText])
|
|
266
278
|
: found;
|
|
267
279
|
if (fails.length > 0)
|
|
268
280
|
return failOutcome(`${base}${ompNote}`);
|
|
269
|
-
return (warns.length > 0 ||
|
|
281
|
+
return (warns.length > 0 || ompSet ? warnOutcome : okOutcome)(`${base}${ompNote}`);
|
|
282
|
+
}
|
|
283
|
+
/** Declared model readiness — plans/model-tiers.md "Doctor" (part 7c/8): every model the config can
|
|
284
|
+
* put on a seam must resolve in pi's definitions, and its provider must report `ready` from the
|
|
285
|
+
* agent binary's own auth check. A strong tier pi cannot resolve would fail every review, and so
|
|
286
|
+
* every merge — the check exists so the operator finds out before the team does, mid-review. Pairs
|
|
287
|
+
* collected in the same sweep teamModelsFree prices: each tier's top-level selector (a string is
|
|
288
|
+
* default's), every enabled role's resolved pair (per-role overrides included; tier-name overrides
|
|
289
|
+
* resolve through configForRole), and the reviewer's while review is on. A pair with no model is
|
|
290
|
+
* pi's own default and out of scope; fallback pairs are the sibling check's (checkFallbackModel
|
|
291
|
+
* prices the cap pair). Levels: an unresolvable pair fails, a provider that is not ready warns
|
|
292
|
+
* (credentials are fixable without a config edit), a probe that cannot run warns rather than
|
|
293
|
+
* guesses, and a priced model whose cache reads declare $0 warns — the daily budget would
|
|
294
|
+
* undercount it (BUGS.md 2026-10-06). When one of PI_SMOL_MODEL / PI_SLOW_MODEL / PI_PLAN_MODEL is
|
|
295
|
+
* set the check also reports that tumwater does not read it — the variables are oh-my-pi's, pi
|
|
296
|
+
* ignores them, and with omp as `agentBin` they still reach omp through the inherited environment
|
|
297
|
+
* (src/pi/pi-backend.ts, built on src/process/run-marker.ts's backendBaseEnv) — pointing at the
|
|
298
|
+
* tier keys that do the same job (`model.small` / `model.strong`). `auth` is injectable so tests
|
|
299
|
+
* need no pi on PATH; `env` so the PI_* branches stay hermetic. The inline jobs are split into
|
|
300
|
+
* the module-private helpers `collectTierPairs`, `ompTierNote`, `readTierProviders`,
|
|
301
|
+
* `resolveTierPair`, `unresolvedModelFail`, `cacheReadWarning`, `probeTierProvider`,
|
|
302
|
+
* `routedTierNotes` and `tierOutcome` so each is found by name. */
|
|
303
|
+
export async function checkTierModels(root, modelsPath = piModelsPath(), auth = (provider) => piProviderAuth(loadConfigSafe(root).config ?? defaultConfig(), provider), env = process.env, listModels = () => piListedModels(loadConfigSafe(root).config ?? defaultConfig(), envPath(), AUTH_CHECK_TIMEOUT_MS, root)) {
|
|
304
|
+
const config = loadConfigForCheck(root);
|
|
305
|
+
if ("level" in config)
|
|
306
|
+
return config;
|
|
307
|
+
// Asked lazily and at most once: only a pair models.json does not declare needs the listing.
|
|
308
|
+
let listing;
|
|
309
|
+
const listed = () => (listing ??= listModels());
|
|
310
|
+
const { pairs, routedModels } = collectTierPairs(config);
|
|
311
|
+
const { set: ompSet, note: ompNote } = ompTierNote(env);
|
|
312
|
+
if (pairs.size === 0 && routedModels.size === 0)
|
|
313
|
+
return (ompSet ? warnOutcome : okOutcome)(`${PHRASE_NO_MODELS_DECLARED}${ompNote}`);
|
|
314
|
+
const providers = await readTierProviders(modelsPath, pairs, listed);
|
|
315
|
+
const fails = [];
|
|
316
|
+
const warns = [];
|
|
317
|
+
if (pairs.size > 0 && !providers)
|
|
318
|
+
return warnOutcome(`${PHRASE_CANNOT_READ_MODELS}${modelsPath}${ompNote}`);
|
|
319
|
+
const readyProviders = new Set();
|
|
320
|
+
const checkedProviders = new Set();
|
|
321
|
+
for (const { provider, model } of pairs.values()) {
|
|
322
|
+
const result = await resolveTierPair(provider, model, providers, listed, auth, checkedProviders, modelsPath);
|
|
323
|
+
warns.push(...result.warns);
|
|
324
|
+
if (result.fail !== undefined)
|
|
325
|
+
fails.push(result.fail);
|
|
326
|
+
if (result.ready === true)
|
|
327
|
+
readyProviders.add(provider);
|
|
328
|
+
}
|
|
329
|
+
const routedText = routedTierNotes(routedModels);
|
|
330
|
+
return tierOutcome(fails, warns, pairs.size, readyProviders, routedText, ompNote, ompSet);
|
|
270
331
|
}
|
|
271
332
|
/** `gh` readiness for the opt-in `feedback` observer (plans/feedback-role.md "Shape" → "Part 1"):
|
|
272
333
|
* the role files issues as the operator's GitHub login through `gh`, so doctor confirms `gh`
|
|
@@ -289,3 +350,26 @@ export async function checkFeedbackFiling(root, pathEnv = envPath(), probe = (ar
|
|
|
289
350
|
return failOutcome("gh is not authenticated — run `gh auth login` — the feedback role files issues as your GitHub login");
|
|
290
351
|
return okOutcome("gh present and authenticated — the feedback role can file issues on znation/tumwater");
|
|
291
352
|
}
|
|
353
|
+
/** The `image input` row (plans/image-input.md "Surfaces", part 5/5): which enabled roles run
|
|
354
|
+
* on a model that cannot read images, and where each reroutes an image prompt — or that no
|
|
355
|
+
* usable reroute exists. Re-added probe-free by Subtract part 1d/6 after part 5f/6 deleted the
|
|
356
|
+
* probe-backed row: it reads roleImageInfo (the same resolver the status rows, role page, and
|
|
357
|
+
* submit gate share) rather than spawning a backend, so doctor and the surfaces cannot drift on
|
|
358
|
+
* who is text-only. No alert when none is (imageInput true or null — an unknown pair is not
|
|
359
|
+
* counted as text-only, mirroring the submit gate and the `models for each tier` stance). */
|
|
360
|
+
export function checkImageInput(root, modelsPath = piModelsPath(), listed = readPiListedModels(root)) {
|
|
361
|
+
const config = loadConfigForCheck(root);
|
|
362
|
+
if ("level" in config)
|
|
363
|
+
return config;
|
|
364
|
+
const textOnly = runnerIds(config).filter((role) => roleImageInfo(config, role, modelsPath, listed).imageInput === false);
|
|
365
|
+
if (textOnly.length === 0)
|
|
366
|
+
return okOutcome("every enabled role reads images");
|
|
367
|
+
const noTarget = textOnly.filter((role) => {
|
|
368
|
+
const rr = roleImageInfo(config, role, modelsPath, listed).imageReroute;
|
|
369
|
+
return rr === null || rr === "";
|
|
370
|
+
});
|
|
371
|
+
if (noTarget.length > 0)
|
|
372
|
+
return warnOutcome(`${noTarget.join(", ")} ${agree(noTarget.length, "runs", "run")} on models that can't read images and ${agree(noTarget.length, "has", "have")} no reroute target — a run that reads an image fails, and image attachments to these roles are refused. Declare image input on a vision model in pi's models.json, or point a tier at one.`);
|
|
373
|
+
const targets = new Set(textOnly.map((role) => roleImageInfo(config, role, modelsPath, listed).imageReroute ?? ""));
|
|
374
|
+
return okOutcome(`${textOnly.join(", ")} ${agree(textOnly.length, "runs", "run")} on models that can't read images — image prompts rerun on ${[...targets].join(", ")}.`);
|
|
375
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { eventsSince } from "../events/event-window.js";
|
|
2
|
-
import { isNotifyFailure } from "../events/notify.js";
|
|
2
|
+
import { isNotifyFailure } from "../events/event-notify.js";
|
|
3
3
|
import { DAY_MS, humanAgo } from "../text/datetime.js";
|
|
4
4
|
import { plural } from "../text/phrases.js";
|
|
5
5
|
import { errorMessage, squash } from "../text/text.js";
|
|
6
|
-
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
6
|
+
import { okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
7
7
|
/** How far back the `notify` check looks for failed pages: a day, fixed. An opinionated
|
|
8
8
|
* default with no knob, like STALE_BUG_DAYS: long enough that a transient hook failure shows
|
|
9
9
|
* up, short enough that a page that did not go through is still the operator's business. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os from "node:os";
|
|
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 { opencodeBackend, opencodeSpillDir } from "../agent/opencode/opencode-backend.js";
|
|
6
6
|
import { opencodeModelFree, opencodeModelKey, readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
|
|
7
7
|
import { formatDate } from "../text/datetime.js";
|
|
@@ -9,7 +9,7 @@ import { opencodeDbPath } from "../paths.js";
|
|
|
9
9
|
import { envPath, isDirectory, readTextOrNull, statOrNull, treeBytes } from "../files/files.js";
|
|
10
10
|
import { formatBytes, tokenWindowLabel } from "../text/format.js";
|
|
11
11
|
import { escapeRegExp } from "../text/text.js";
|
|
12
|
-
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
12
|
+
import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
13
13
|
import { loadConfigForCheck, opencodeSeamsInUse, seamedModels, } from "./doctor-model-checks.js";
|
|
14
14
|
/** The doctor's opencode credential and leak readiness check, split out of doctor-model-checks.ts
|
|
15
15
|
* (whose shared seam/model framework and the pi and tier-models checks stay there):
|
|
@@ -19,8 +19,9 @@ import { loadConfigForCheck, opencodeSeamsInUse, seamedModels, } from "./doctor-
|
|
|
19
19
|
* filesystem/git checks beside it, kept together so a change to how doctor reads opencode does
|
|
20
20
|
* not touch the other model checks. The shared report contract (CheckOutcome) and the
|
|
21
21
|
* seam/model framework (loadConfigForCheck, opencodeSeamsInUse, seamPairs) come from
|
|
22
|
-
* doctor-model-checks.ts, the AUTH_CHECK_* probe budgets from probe.ts, and the
|
|
23
|
-
* contract (CheckOutcome) from outcome.ts, as the other doctor-*
|
|
22
|
+
* doctor-model-checks.ts, the AUTH_CHECK_* probe budgets from doctor-probe.ts, and the
|
|
23
|
+
* shared report contract (CheckOutcome) from doctor-outcome.ts, as the other doctor-*
|
|
24
|
+
* modules do. */
|
|
24
25
|
/** The provider of a non-gateway opencode model id (`opencode/<provider>/<model>`), or
|
|
25
26
|
* undefined for a gateway id (a bare `opencode/<model>` or `<model>`, served by OpenCode's
|
|
26
27
|
* own gateway with no credential). A model id with more than the `opencode/` namespace prefix
|