tumwater 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +42 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +4 -3
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +402 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +246 -55
- package/dist/src/backlog/backlog-cli-view.js +280 -0
- package/dist/src/backlog/backlog-cli.js +85 -274
- package/dist/src/backlog/backlog-commit.js +4 -5
- package/dist/src/backlog/backlog-eligibility.js +175 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +254 -0
- package/dist/src/backlog/backlog-release.js +117 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-culprit.js +94 -0
- package/dist/src/baseline/main-guardrails.js +209 -0
- package/dist/src/baseline/main-red.js +139 -32
- package/dist/src/baseline/main-revert.js +133 -0
- package/dist/src/baseline/main-watch.js +184 -0
- package/dist/src/brief/brief-edit.js +9 -11
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget.js +30 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +118 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +150 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +308 -590
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-role-command.js +420 -0
- package/dist/src/cli/cli-start-summary.js +160 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
- package/dist/src/cli/completion.js +149 -22
- package/dist/src/cli/config-commands.js +41 -18
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +189 -73
- package/dist/src/cli/log-commands.js +148 -82
- package/dist/src/cli/question-commands.js +205 -10
- package/dist/src/cli.js +106 -43
- package/dist/src/collections.js +26 -10
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +13 -1
- package/dist/src/config/config-fallbacks.js +93 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +141 -199
- package/dist/src/config/config-views.js +127 -53
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +131 -264
- package/dist/src/config/config.js +48 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
- package/dist/src/doctor/doctor-backlog-plans.js +263 -0
- package/dist/src/doctor/doctor-backlog.js +100 -496
- package/dist/src/doctor/doctor-check-names.js +81 -0
- package/dist/src/doctor/doctor-checks.js +52 -579
- package/dist/src/doctor/doctor-claude-checks.js +145 -0
- package/dist/src/doctor/doctor-codex-checks.js +279 -0
- package/dist/src/doctor/doctor-contention-checks.js +380 -0
- package/dist/src/doctor/doctor-holds.js +82 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +236 -402
- package/dist/src/doctor/doctor-notify.js +3 -4
- package/dist/src/doctor/doctor-opencode-checks.js +162 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-project-checks.js +303 -0
- package/dist/src/doctor/doctor.js +121 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +7 -11
- package/dist/src/events/event-format.js +181 -86
- package/dist/src/events/event-labels.js +12 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +39 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +60 -24
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +56 -56
- package/dist/src/failure/failure-render.js +31 -23
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +99 -35
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +177 -84
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +108 -58
- package/dist/src/gates/maintenance-quota.js +10 -13
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +77 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +36 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +42 -18
- package/dist/src/gui/gui-backlog-endpoints.js +155 -0
- package/dist/src/gui/gui-command.js +8 -13
- package/dist/src/gui/gui-config-commands.js +94 -72
- package/dist/src/gui/gui-endpoint-commands.js +4 -3
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +108 -13
- package/dist/src/gui/gui-prompt-commands.js +11 -14
- package/dist/src/gui/gui-report-endpoints.js +90 -0
- package/dist/src/gui/gui-server.js +11 -4
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +147 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +193 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +25 -9
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +81 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +150 -11
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +90 -0
- package/dist/src/merge/merge-queue-render.js +49 -11
- package/dist/src/merge/merge-queue.js +31 -15
- package/dist/src/merge/merge-requeue.js +102 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +33 -11
- package/dist/src/merge/merge-to-main.js +86 -27
- package/dist/src/merge/merge-top.js +69 -0
- package/dist/src/merge/merge-verify.js +12 -10
- package/dist/src/notes-cli.js +30 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +320 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
- package/dist/src/orchestrator/orchestrator.js +128 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +201 -0
- package/dist/src/orchestrator/scheduling-skip.js +185 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +116 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +18 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +121 -211
- package/dist/src/prompt/notes-backlog.js +181 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +129 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +161 -77
- package/dist/src/report/report-fold.js +129 -52
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +133 -64
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +26 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/known-flakes.js +100 -1
- package/dist/src/review/post-merge-review.js +106 -0
- package/dist/src/review/protected-edits.js +4 -5
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +155 -360
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +69 -12
- package/dist/src/roles/role-guidance.js +55 -20
- package/dist/src/roles/role-labels.js +1 -2
- package/dist/src/roles/role-render.js +67 -50
- package/dist/src/roles/role-view.js +59 -47
- package/dist/src/roles/roles.js +31 -31
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/{tick → run}/qa-coverage.js +9 -27
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +179 -0
- package/dist/src/run/run-detail-render.js +98 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +172 -0
- package/dist/src/run/stage-path-checks.js +730 -0
- package/dist/src/{loop → runner}/leftover.js +80 -25
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +202 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +107 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +102 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +43 -16
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +28 -28
- package/dist/src/status/status-roles.js +39 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +5 -6
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +63 -17
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +57 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +67 -13
- package/dist/src/ui/badges.js +65 -37
- package/dist/src/ui/gui/gui-client-backlog.js +292 -0
- package/dist/src/ui/gui/gui-client-boot.js +32 -26
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
- package/dist/src/ui/gui/gui-client-drawer.js +126 -285
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +434 -67
- package/dist/src/ui/gui/gui-client-labels.js +1 -2
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +55 -51
- package/dist/src/ui/gui/gui-client-pending.js +227 -23
- package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
- package/dist/src/ui/gui/gui-client-report.js +87 -427
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +56 -4
- package/dist/src/ui/gui/gui-client-team.js +25 -465
- package/dist/src/ui/gui/gui-client.js +27 -25
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +9 -7
- package/dist/src/ui/gui/gui-styles.js +34 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +136 -49
- package/dist/src/ui/status-render.js +101 -82
- package/dist/src/ui/team-alerts.js +248 -57
- package/dist/src/ui/tone.js +5 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +51 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +3 -5
- package/dist/src/ui/tui/tui-editor-backends.js +132 -0
- package/dist/src/ui/tui/tui-editor-brief.js +10 -12
- package/dist/src/ui/tui/tui-editor-budget.js +35 -31
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
- package/dist/src/ui/tui/tui-editor-config.js +86 -0
- package/dist/src/ui/tui/tui-editor-entry.js +36 -47
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +37 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
- package/dist/src/ui/tui/tui-editor-interval.js +11 -9
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +3 -5
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
- package/dist/src/ui/tui/tui-editor-retire.js +4 -6
- package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +60 -34
- package/dist/src/ui/tui/tui-editor-slots.js +23 -0
- package/dist/src/ui/tui/tui-editors.js +35 -2
- package/dist/src/ui/tui/tui-frame.js +56 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +24 -9
- package/dist/src/ui/tui/tui-pane-activity.js +14 -11
- package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +58 -8
- package/dist/src/ui/tui/tui-pane-history.js +402 -82
- package/dist/src/ui/tui/tui-pane-pending.js +308 -13
- package/dist/src/ui/tui/tui-pane-queued.js +71 -55
- package/dist/src/ui/tui/tui-pane-report.js +46 -14
- package/dist/src/ui/tui/tui-pane-session.js +25 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +80 -23
- package/dist/src/ui/tui/tui-panes.js +100 -30
- package/dist/src/ui/tui/tui.js +14 -14
- package/dist/src/verdict/fix-claim.js +82 -32
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +42 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,44 +1,51 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
1
|
import { childEnv } from "../process/process.js";
|
|
6
2
|
import { boundedProbe } from "./probe.js";
|
|
7
|
-
import { opencodeBackend, opencodeSpillDir } from "../agent/opencode/opencode-backend.js";
|
|
8
|
-
import { opencodeModelKey, readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
|
|
9
|
-
import { opencodeDbPath } from "../paths.js";
|
|
10
|
-
import { readTextOrNull, statOrNull } from "../files/files.js";
|
|
11
3
|
import { enabledRoleIds, defaultConfig, loadConfigSafe } from "../config/config.js";
|
|
12
|
-
import { configForRole
|
|
4
|
+
import { configForRole } from "../config/config-views.js";
|
|
13
5
|
import { fallbackPair, resolveTierFallbacks, seamsInUse, tierChainSteps } from "../config/config-fallbacks.js";
|
|
14
6
|
import { cacheReadUnpriced, fallbackModelPair, piModelsPath, piPairFree, piProviderBaseUrl, readPiProviders } from "../pi/pi-models.js";
|
|
15
7
|
import { MODEL_TIERS } from "../config/config-schema.js";
|
|
16
|
-
import { isAgentId } from "../config/model-selector.js";
|
|
17
8
|
import { imageReroute, seamImageInput } from "../agent/image-input.js";
|
|
18
9
|
import { modelLabel, usesPiModels } from "../agent/backend-traits.js";
|
|
19
|
-
import {
|
|
10
|
+
import { backendById } from "../agent/agent-backend.js";
|
|
20
11
|
import { imageProbeEntryFresh, imageProbeKey, readImageProbeCache, runImageProbe, writeImageProbeCache, } from "./image-probe.js";
|
|
21
|
-
import {
|
|
22
|
-
import { claudeAuthUsesSubscription, parseClaudeAuthStatus, readClaudeAccount } from "../agent/claude/claude-account.js";
|
|
23
|
-
import { claudeEnv } from "../agent/claude/claude-env.js";
|
|
12
|
+
import { readClaudeAccount } from "../agent/claude/claude-account.js";
|
|
24
13
|
import { parseJsonObject } from "../files/json-object.js";
|
|
25
14
|
import { envPath } from "../files/files.js";
|
|
26
15
|
import { modelPairName, pairDailyCost } from "../budget/budget.js";
|
|
27
16
|
import { usd, usdCap } from "../text/format.js";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
17
|
+
import { loadRunnerStates } from "./doctor-holds.js";
|
|
18
|
+
import { resolveBackendBin } from "../pi/pi-bin.js";
|
|
19
|
+
import { parsePiModelImages, parsePiModelTable, readPiListedModels, writePiListedModels } from "../pi/pi-listed-models.js";
|
|
30
20
|
import { agree } from "../text/phrases.js";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
21
|
+
import { formatClock, formatDate } from "../text/datetime.js";
|
|
22
|
+
import { errorMessage, joinParts } from "../text/text.js";
|
|
33
23
|
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
24
|
+
import { belowNodeFloor } from "../version.js";
|
|
25
|
+
/** The one phrase a configured-models check uses when the config declares no models at all:
|
|
26
|
+
* informational (capability falls to pi's own defaults), not a warning. Shared by
|
|
27
|
+
* checkTierModels and checkImageInput so the two checks word the same "nothing configured"
|
|
28
|
+
* stance identically. */
|
|
29
|
+
const PHRASE_NO_MODELS_DECLARED = "no models declared — every model job uses pi's own default";
|
|
30
|
+
/** The one phrase a configured-models check uses when pi's definitions file could not be read
|
|
31
|
+
* at all: the check cannot answer what it was asked, so it warns instead of guessing. Shared by
|
|
32
|
+
* checkTierModels and checkImageInput so the two checks word the "cannot check" stance
|
|
33
|
+
* identically; each appends its own modelsPath after the shared stem, and checkTierModels
|
|
34
|
+
* also appends its ompNote. */
|
|
35
|
+
const PHRASE_CANNOT_READ_MODELS = "cannot check — could not read pi's model definitions at ";
|
|
34
36
|
/** The doctor's model-readiness checks, split out of doctor-checks.ts (whose environment and
|
|
35
|
-
* repo checks stay there
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* and
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
37
|
+
* repo-setup checks stay there, and whose state/disk/build checks live in
|
|
38
|
+
* doctor-project-checks.ts): the cap fallback price and fallback-chain checks (checkFallbackModel,
|
|
39
|
+
* checkFallbackChain), the pi auth/probe checks (piProviderAuth, piListedModels), the seam
|
|
40
|
+
* framework (seamPairs and the per-backend seam predicates), and the declared-tier and
|
|
41
|
+
* image-input checks (checkTierModels, checkImageInput). The claude and opencode provider
|
|
42
|
+
* checks live in their own modules (doctor-claude-checks.ts, doctor-opencode-checks.ts), each
|
|
43
|
+
* importing this file's shared seam framework and the AUTH_CHECK_* probe budgets rather than
|
|
44
|
+
* duplicating them. The shared report contract (CheckOutcome) is imported from outcome.ts,
|
|
45
|
+
* exactly as the other doctor-* modules do. Together these are the doctor's only checks that
|
|
46
|
+
* read pi's model definitions and, for the probes, spawn an agent binary — a distinct concern
|
|
47
|
+
* from the filesystem/git checks, kept together so a change to how doctor reads models does not
|
|
48
|
+
* touch the environment checks. */
|
|
42
49
|
/** The config a readiness check reads, or the warn outcome a broken tumwater.json earns:
|
|
43
50
|
* checkInit already fails on a broken config, so a model check only reports that it could not
|
|
44
51
|
* run. One home for the guard and its wording, shared by checkFallbackModel and
|
|
@@ -46,7 +53,7 @@ import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
|
46
53
|
* files). The config or the warn outcome, so each check's one-line guard — `const config =
|
|
47
54
|
* loadConfigForCheck(root); if ("level" in config) return config;` — is all a check needs to
|
|
48
55
|
* read its config instead of repeating a load-and-unwrap prologue. */
|
|
49
|
-
function loadConfigForCheck(root) {
|
|
56
|
+
export function loadConfigForCheck(root) {
|
|
50
57
|
const { config, error } = loadConfigSafe(root);
|
|
51
58
|
if (config === undefined)
|
|
52
59
|
return warnOutcome(`cannot check — ${error}`);
|
|
@@ -58,7 +65,7 @@ function loadConfigForCheck(root) {
|
|
|
58
65
|
* behavior (spend must never climb past the cap), but a typo'd id would otherwise surface only
|
|
59
66
|
* after the day's budget is already spent — so doctor checks it up front, before the operator
|
|
60
67
|
* needs it. Read-only: it inspects pi's definitions file, never pi or the network. A price is not
|
|
61
|
-
* readiness, though: whether the backend can SERVE is known only from the fallback's own
|
|
68
|
+
* readiness, though: whether the backend can SERVE is known only from the fallback's own runs
|
|
62
69
|
* (src/budget/fallback-breaker.ts's breaker, BUGS.md 2026-09-20), so a free pair reads "serving not
|
|
63
70
|
* verified" — unless the running orchestrator has published that its breaker demoted the pair
|
|
64
71
|
* (`demoted`, from orchestrator.json; runDoctor passes it only while that orchestrator is alive),
|
|
@@ -77,11 +84,15 @@ export function checkFallbackModel(root, modelsPath = piModelsPath(), demoted =
|
|
|
77
84
|
// the price-resolved pair (a map with no `default` entry borrows another tier's), while the
|
|
78
85
|
// refusal names the pair the operator declared.
|
|
79
86
|
const nameOf = (pair) => pair.provider && pair.model ? `${pair.provider}/${pair.model}` : "a half-resolved pair";
|
|
80
|
-
|
|
87
|
+
// The claude account answer (6a/7); the fallback price lookup reads it only for "claude"
|
|
88
|
+
// (part 2a/9).
|
|
89
|
+
const account = readClaudeAccount(root);
|
|
90
|
+
const accounts = (b) => (b === "claude" ? account : null);
|
|
91
|
+
const free = fallbackModelPair(config, modelsPath, accounts);
|
|
81
92
|
if (free) {
|
|
82
93
|
const name = nameOf(free);
|
|
83
94
|
if (demoted)
|
|
84
|
-
return warnOutcome(`${name} is priced at zero but not serving — the running team demoted it after ${demoted.failures} consecutive failed runs, so roles pause at the cap; one probe run retries it from ${
|
|
95
|
+
return warnOutcome(`${name} is priced at zero but not serving — the running team demoted it after ${demoted.failures} consecutive failed runs, so roles pause at the cap; one probe run retries it from ${formatClock(demoted.probeAt)}`);
|
|
85
96
|
return okOutcome(`${name} — priced at zero (cost n/a), serving not verified`);
|
|
86
97
|
}
|
|
87
98
|
return warnOutcome(`${nameOf(declared)} is not priced at zero in ${modelsPath} — at the cap roles pause instead of switching`);
|
|
@@ -109,7 +120,7 @@ const isLocalUrl = (url) => {
|
|
|
109
120
|
/** The `fallback chain` check: one fragment per step of every tier's chain saying whether the
|
|
110
121
|
* step can run now — a local pi server that does not answer (warn), a step whose own daily cap is
|
|
111
122
|
* spent, a free pair, a claude step with its billing, or a priced pair. Reads config, pi's
|
|
112
|
-
* models.json and the
|
|
123
|
+
* models.json and the role states; only a pi step on 127.0.0.1/localhost makes a network
|
|
113
124
|
* request. `probe` is injectable so tests need no real server. */
|
|
114
125
|
export async function checkFallbackChain(root, modelsPath = piModelsPath(), probe = probeModels) {
|
|
115
126
|
const config = loadConfigForCheck(root);
|
|
@@ -118,16 +129,18 @@ export async function checkFallbackChain(root, modelsPath = piModelsPath(), prob
|
|
|
118
129
|
const steps = tierChainSteps(config);
|
|
119
130
|
if (steps.length === 0)
|
|
120
131
|
return okOutcome("none configured");
|
|
121
|
-
const states =
|
|
132
|
+
const states = loadRunnerStates(root, config);
|
|
122
133
|
const providers = readPiProviders(modelsPath);
|
|
123
134
|
const account = readClaudeAccount(root);
|
|
135
|
+
// The free-ness lookup reads the one claude account held here (part 2a/9).
|
|
136
|
+
const accounts = (b) => (b === "claude" ? account : null);
|
|
124
137
|
const now = Date.now();
|
|
125
138
|
let down = false;
|
|
126
139
|
const fragments = [];
|
|
127
140
|
for (const { tier, step, pair } of steps) {
|
|
128
141
|
// A provider that names a backend other than pi (claude today; opencode from part 2) never
|
|
129
142
|
// asks pi for a base URL or runs pi's price probe — only a pi step may.
|
|
130
|
-
const nonPiBackend =
|
|
143
|
+
const nonPiBackend = pair.backend !== undefined && pair.backend !== "pi";
|
|
131
144
|
const base = !nonPiBackend && pair.provider ? piProviderBaseUrl(modelsPath, pair.provider) : null;
|
|
132
145
|
const local = base !== null && isLocalUrl(base);
|
|
133
146
|
const up = local ? await probe(`${base}/models`) : null;
|
|
@@ -144,7 +157,7 @@ export async function checkFallbackChain(root, modelsPath = piModelsPath(), prob
|
|
|
144
157
|
else if (nonPiBackend) {
|
|
145
158
|
verdict = `claude ${pair.extraUsage ? "extra usage" : (account?.billing ?? "billing unknown")}`;
|
|
146
159
|
}
|
|
147
|
-
else if (piPairFree(providers, pair
|
|
160
|
+
else if (piPairFree(providers, pair, accounts)) {
|
|
148
161
|
verdict = up ? "free · server up" : "free";
|
|
149
162
|
}
|
|
150
163
|
else {
|
|
@@ -169,7 +182,7 @@ export const AUTH_CHECK_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
|
169
182
|
* resolver, the director, and each tier's budget/model-failure fallback — so a claude or pi
|
|
170
183
|
* pair that exists only as a fallback is checked exactly like a primary one. Deduplicated by
|
|
171
184
|
* backend/provider/model, so a model reached through several surfaces is checked once. */
|
|
172
|
-
function seamPairs(config) {
|
|
185
|
+
export function seamPairs(config) {
|
|
173
186
|
const pairs = new Map();
|
|
174
187
|
for (const seam of seamsInUse(config)) {
|
|
175
188
|
if (seam.model === undefined)
|
|
@@ -183,6 +196,26 @@ function seamPairs(config) {
|
|
|
183
196
|
}
|
|
184
197
|
return [...pairs.values()];
|
|
185
198
|
}
|
|
199
|
+
/** The model-carrying seam pairs for one backend — `seamPairs` with model guaranteed present
|
|
200
|
+
* (seamPairs never emits a pair without a model, so the guard only narrows the type) and
|
|
201
|
+
* narrowed to `agent`'s pairs. The backend readiness checks (claude, codex, opencode) each
|
|
202
|
+
* walk exactly this filtered list, so they share one filter rather than three copies. */
|
|
203
|
+
export function seamedModels(config, agent) {
|
|
204
|
+
return seamPairs(config).filter((p) => p.agent === agent && p.model !== undefined);
|
|
205
|
+
}
|
|
206
|
+
/** The warn/fail/ok verdict for a backend CLI's `--version` check against one flat minimum
|
|
207
|
+
* version (codex's checkCodexVersion and opencode's checkOpencodeVersion both require a
|
|
208
|
+
* single floor for the whole backend, unlike claude's per-model floors). `found` is the
|
|
209
|
+
* parsed dotted version, or undefined when the probe could not run or its output was
|
|
210
|
+
* unreadable. Centralizing the three messages and the belowNodeFloor comparison keeps the
|
|
211
|
+
* two backends' version verdicts from drifting. */
|
|
212
|
+
export function minVersionOutcome(name, found, min, upgradeHint) {
|
|
213
|
+
if (found === undefined)
|
|
214
|
+
return warnOutcome(`could not check — \`${name} --version\` did not run or its output was unreadable`);
|
|
215
|
+
if (belowNodeFloor(found, `>=${min}`))
|
|
216
|
+
return failOutcome(`${name} ${found} is older than ${min} — ${upgradeHint}`);
|
|
217
|
+
return okOutcome(`${name} ${found} (>= ${min})`);
|
|
218
|
+
}
|
|
186
219
|
/** Whether any seam runs on the claude backend — the doctor's claude checks stand down when
|
|
187
220
|
* none does (a pi-only team reads no `~/.claude` state and spawns no claude). */
|
|
188
221
|
export function claudeSeamsInUse(config) {
|
|
@@ -193,6 +226,11 @@ export function claudeSeamsInUse(config) {
|
|
|
193
226
|
export function opencodeSeamsInUse(config) {
|
|
194
227
|
return seamPairs(config).some((pair) => pair.agent === "opencode");
|
|
195
228
|
}
|
|
229
|
+
/** Whether any seam routes to codex — the doctor's codex checks stand down when none does
|
|
230
|
+
* (a pi-only team spawns no codex and reads no codex home state). */
|
|
231
|
+
export function codexSeamsInUse(config) {
|
|
232
|
+
return seamPairs(config).some((pair) => pair.agent === "codex");
|
|
233
|
+
}
|
|
196
234
|
/** Whether any seam routes to the pi backend — the doctor's pi-extension row stands down when
|
|
197
235
|
* none does (an all-claude or all-opencode team reads no `~/.pi` state). Unlike the model-based
|
|
198
236
|
* seam consult, this scans the routed seams directly (not seamPairs' model-filtered view) so a
|
|
@@ -202,151 +240,60 @@ export function piSeamsInUse(config) {
|
|
|
202
240
|
return seamsInUse(config).some((s) => s.agent === "pi");
|
|
203
241
|
}
|
|
204
242
|
/** The default credential probe: run the resolved agent binary's own auth check for one provider
|
|
205
|
-
* and read `
|
|
206
|
-
* — the same source a real
|
|
207
|
-
*
|
|
208
|
-
*
|
|
243
|
+
* and read `status` out of its JSON. The binary resolves exactly as the spawn does
|
|
244
|
+
* (resolveBackendBin — the same source a real run would launch), so doctor tests the auth of
|
|
245
|
+
* the pi it would actually run. pi's contract is `{"status": "ready"|"not_ready"|"invalid",
|
|
246
|
+
* provider, reason?, authType?}` with exit codes 0/1/2 — a non-zero exit there is a verdict, not
|
|
247
|
+
* a crash, so readiness is read from the body alone and a reply that did not run or does not
|
|
248
|
+
* parse is the only "unknown" (never not-ready, which would blame credentials). Never throws;
|
|
249
|
+
* every failure mode ends on "unknown". The bounded subprocess is the shared boundedProbe
|
|
250
|
+
* (probe.ts), also used by the claude probes. */
|
|
209
251
|
export async function piProviderAuth(config, provider, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
210
|
-
const resolved =
|
|
252
|
+
const resolved = resolveBackendBin(config);
|
|
211
253
|
const result = await boundedProbe(resolved.bin, ["auth", "check", "--provider", provider, "--json"], childEnv({ PATH: pathEnv }), timeoutMs, AUTH_CHECK_MAX_OUTPUT_BYTES);
|
|
212
254
|
if (result === null)
|
|
213
255
|
return "unknown";
|
|
214
|
-
if (result.code !== 0)
|
|
215
|
-
return "unknown";
|
|
216
256
|
// An unreadable reply is unknown, never not-ready (which would blame the credentials).
|
|
217
257
|
const doc = parseJsonObject(result.stdout);
|
|
218
258
|
if (doc === null)
|
|
219
259
|
return "unknown";
|
|
220
|
-
if (doc.
|
|
260
|
+
if (doc.status === "ready")
|
|
221
261
|
return "ready";
|
|
222
|
-
|
|
262
|
+
// `invalid` (a bad provider name, exit 2) is likewise not a healthy credential: it reads
|
|
263
|
+
// not-ready so the warning blames the setup, not the probe.
|
|
264
|
+
if (doc.status === "not_ready" || doc.status === "invalid")
|
|
223
265
|
return "not-ready";
|
|
224
266
|
return "unknown";
|
|
225
267
|
}
|
|
226
|
-
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const configDir = resolvedAgentConfig(config, "claude").configDir;
|
|
239
|
-
const expandedConfigDir = configDir !== undefined ? claudeConfigDir(process.env, os.homedir(), configDir) : undefined;
|
|
240
|
-
const env = claudeEnv(childEnv({ PATH: pathEnv }), {
|
|
241
|
-
quietTimeoutSeconds: 0,
|
|
242
|
-
...(expandedConfigDir !== undefined ? { configDir: expandedConfigDir } : {}),
|
|
243
|
-
});
|
|
244
|
-
return boundedProbe(resolved.bin, args, env, timeoutMs, AUTH_CHECK_MAX_OUTPUT_BYTES);
|
|
268
|
+
/** Read the table `pi --list-models` prints into a set of `provider/model` keys. Null unless the
|
|
269
|
+
* first non-blank line is the `provider model …` header, so a "No models matching" reply or any
|
|
270
|
+
* other text is never taken for an empty catalog. */
|
|
271
|
+
export function parsePiModelList(stdout) {
|
|
272
|
+
const table = parsePiModelTable(stdout);
|
|
273
|
+
if (table === null)
|
|
274
|
+
return null;
|
|
275
|
+
const listed = new Set();
|
|
276
|
+
for (const [provider, model] of table.rows)
|
|
277
|
+
if (model !== undefined)
|
|
278
|
+
listed.add(`${provider}/${model}`);
|
|
279
|
+
return listed;
|
|
245
280
|
}
|
|
246
|
-
/**
|
|
247
|
-
*
|
|
248
|
-
|
|
249
|
-
|
|
281
|
+
/** The models pi itself lists (`pi --list-models`), which include its built-in catalog that
|
|
282
|
+
* models.json does not repeat. pi lists only the providers it has credentials for. Null when the
|
|
283
|
+
* probe could not run or failed; never throws. Given a `root`, it also records the images column
|
|
284
|
+
* for `seamImageInput` (src/pi/pi-listed-models.ts). */
|
|
285
|
+
export async function piListedModels(config, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS, root) {
|
|
286
|
+
const result = await boundedProbe(resolveBackendBin(config).bin, ["--list-models"], childEnv({ PATH: pathEnv }), timeoutMs, AUTH_CHECK_MAX_OUTPUT_BYTES);
|
|
250
287
|
if (result?.code !== 0)
|
|
251
288
|
return null;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
* under the scrubbed run environment and fails when it reports `loggedIn: false`. An ok detail
|
|
257
|
-
* names the billing the reply implies — the subscription type, or an API key — so an operator
|
|
258
|
-
* telling subscription spend from API spend does not have to run the command themselves. A
|
|
259
|
-
* probe that could not run or a reply this cannot read warns rather than guesses. Stands down
|
|
260
|
-
* (ok) when no seam routes to claude, so a pi-only team spawns nothing. */
|
|
261
|
-
export async function checkClaudeAuth(root, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
262
|
-
const config = loadConfigForCheck(root);
|
|
263
|
-
if ("level" in config)
|
|
264
|
-
return config;
|
|
265
|
-
if (!claudeSeamsInUse(config))
|
|
266
|
-
return okOutcome("no model jobs run on claude");
|
|
267
|
-
const auth = await claudeAuthProbe(config, pathEnv, timeoutMs);
|
|
268
|
-
if (auth === null)
|
|
269
|
-
return warnOutcome("could not check — `claude auth status` did not run or its output was unreadable");
|
|
270
|
-
if (!auth.loggedIn)
|
|
271
|
-
return failOutcome("claude reports logged in: false — run `claude auth login`");
|
|
272
|
-
const subscription = claudeAuthUsesSubscription(auth);
|
|
273
|
-
const billing = subscription
|
|
274
|
-
? `subscription${auth.subscriptionType !== undefined ? ` (${auth.subscriptionType})` : ""}`
|
|
275
|
-
: "API key";
|
|
276
|
-
return okOutcome(`logged in — billing: ${billing}`);
|
|
277
|
-
}
|
|
278
|
-
/** The subscription-vs-API billing trap (plans/provider-concurrency.md §9 gap 6): an operator
|
|
279
|
-
* who exports `ANTHROPIC_API_KEY` for pi's `anthropic` provider will silently move every claude
|
|
280
|
-
* seam to API billing — claude prefers the key over a claude.ai login — and subscription seams
|
|
281
|
-
* stop counting as free. Doctor warns when the key is set, some seam routes to claude, and
|
|
282
|
-
* `claude auth status` reports a claude.ai login (the subscription the key would override). It
|
|
283
|
-
* stays silent (ok) when no seam runs on claude, when the key is unset, or when claude reports
|
|
284
|
-
* an API-key login (then there is no subscription to lose). `env` is injectable so the check
|
|
285
|
-
* stays hermetic; a probe that cannot run warns rather than guesses like checkClaudeAuth. */
|
|
286
|
-
export async function checkAnthroApiKey(root, env = process.env, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
287
|
-
const config = loadConfigForCheck(root);
|
|
288
|
-
if ("level" in config)
|
|
289
|
-
return config;
|
|
290
|
-
if (!claudeSeamsInUse(config))
|
|
291
|
-
return okOutcome("no model jobs run on claude");
|
|
292
|
-
const apiKey = env.ANTHROPIC_API_KEY;
|
|
293
|
-
if (apiKey === undefined || apiKey === "")
|
|
294
|
-
return okOutcome("no ANTHROPIC_API_KEY set");
|
|
295
|
-
const auth = await claudeAuthProbe(config, pathEnv, timeoutMs);
|
|
296
|
-
if (auth === null)
|
|
297
|
-
return warnOutcome("could not check — `claude auth status` did not run or its output was unreadable");
|
|
298
|
-
if (!auth.loggedIn || !claudeAuthUsesSubscription(auth))
|
|
299
|
-
return okOutcome("ANTHROPIC_API_KEY is set, but claude reports no claude.ai login");
|
|
300
|
-
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");
|
|
301
|
-
}
|
|
302
|
-
/** The dotted version in `claude --version`'s output, or undefined when it names none. */
|
|
303
|
-
function parseClaudeCliVersion(output) {
|
|
304
|
-
return /(\d+\.\d+\.\d+)/.exec(output)?.[1];
|
|
305
|
-
}
|
|
306
|
-
/** claude CLI-version readiness (plans/agent-backends.md finding 9): an installed CLI below a
|
|
307
|
-
* configured model's minimum fails or misprices that model, and team runs disable the
|
|
308
|
-
* auto-updater, so the version an operator has is the version ticks run. Fails below the
|
|
309
|
-
* highest floor among the claude-routed models, naming which model needs it; an ok detail
|
|
310
|
-
* prints the installed version and the floor it satisfied (or no floor when none applies).
|
|
311
|
-
* The `off`/`minimal` thinking clamp is noted here too, since this check already has the
|
|
312
|
-
* claude seams in hand. Stands down when no seam routes to claude. */
|
|
313
|
-
export async function checkClaudeVersion(root, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
314
|
-
const config = loadConfigForCheck(root);
|
|
315
|
-
if ("level" in config)
|
|
316
|
-
return config;
|
|
317
|
-
const seams = seamPairs(config).filter((pair) => pair.agent === "claude" && pair.model !== undefined);
|
|
318
|
-
if (seams.length === 0)
|
|
319
|
-
return okOutcome("no model jobs run on claude");
|
|
320
|
-
// The floor each configured model needs; a model the adapter does not gate contributes none.
|
|
321
|
-
const floors = new Map();
|
|
322
|
-
for (const seam of seams) {
|
|
323
|
-
const min = claudeBackend.minVersion?.(seam.model);
|
|
324
|
-
if (min !== undefined)
|
|
325
|
-
floors.set(seam.model, min);
|
|
326
|
-
}
|
|
327
|
-
const result = await claudeProbe(config, ["--version"], pathEnv, timeoutMs);
|
|
328
|
-
const found = result !== null && result.code === 0 ? parseClaudeCliVersion(result.stdout) : undefined;
|
|
329
|
-
if (found === undefined)
|
|
330
|
-
return warnOutcome("could not check — `claude --version` did not run or its output was unreadable");
|
|
331
|
-
const clamped = seams.some((seam) => seam.thinking === "off" || seam.thinking === "minimal");
|
|
332
|
-
const clampNote = clamped
|
|
333
|
-
? "; `off`/`minimal` thinking clamps to `--effort low` (claude has no lower setting)"
|
|
334
|
-
: "";
|
|
335
|
-
if (floors.size === 0)
|
|
336
|
-
return okOutcome(`claude ${found}${clampNote}`);
|
|
337
|
-
// The highest floor decides: whichever model needs the newest CLI. Below it, fail and name
|
|
338
|
-
// that model; at or above, print the version against the floor.
|
|
339
|
-
let required = "";
|
|
340
|
-
let neededBy = "";
|
|
341
|
-
for (const [model, min] of floors) {
|
|
342
|
-
if (required === "" || belowNodeFloor(required, `>=${min}`)) {
|
|
343
|
-
required = min;
|
|
344
|
-
neededBy = model;
|
|
289
|
+
const images = root === undefined ? null : parsePiModelImages(result.stdout);
|
|
290
|
+
if (root !== undefined && images !== null) {
|
|
291
|
+
try {
|
|
292
|
+
writePiListedModels(root, images);
|
|
345
293
|
}
|
|
294
|
+
catch { } // A failed write never changes the check.
|
|
346
295
|
}
|
|
347
|
-
|
|
348
|
-
return failOutcome(`claude ${found} is older than ${required} required by ${neededBy} — run \`claude update\``);
|
|
349
|
-
return okOutcome(`claude ${found} (>= ${required} for ${neededBy})${clampNote}`);
|
|
296
|
+
return parsePiModelList(result.stdout);
|
|
350
297
|
}
|
|
351
298
|
/** Declared model readiness — plans/model-tiers.md "Doctor" (part 7c/8): every model the config can
|
|
352
299
|
* put on a seam must resolve in pi's definitions, and its provider must report `ready` from the
|
|
@@ -362,13 +309,16 @@ export async function checkClaudeVersion(root, pathEnv = envPath(), timeoutMs =
|
|
|
362
309
|
* undercount it (BUGS.md 2026-10-06). When one of PI_SMOL_MODEL / PI_SLOW_MODEL / PI_PLAN_MODEL is
|
|
363
310
|
* set the check also reports that tumwater does not read it — the variables are oh-my-pi's, pi
|
|
364
311
|
* ignores them, and with omp as `agentBin` they still reach omp through the inherited environment
|
|
365
|
-
* (src/pi/pi-backend.ts, built on src/process/run-marker.ts's
|
|
366
|
-
* keys that do the same job (`model.small` / `model.strong`). `auth` is injectable so tests
|
|
367
|
-
* pi on PATH; `env` so the PI_* branches stay hermetic. */
|
|
368
|
-
export async function checkTierModels(root, modelsPath = piModelsPath(), auth = (provider) => piProviderAuth(loadConfigSafe(root).config ?? defaultConfig(), provider), env = process.env) {
|
|
312
|
+
* (src/pi/pi-backend.ts, built on src/process/run-marker.ts's backendBaseEnv) — pointing at the
|
|
313
|
+
* tier keys that do the same job (`model.small` / `model.strong`). `auth` is injectable so tests
|
|
314
|
+
* need no pi on PATH; `env` so the PI_* branches stay hermetic. */
|
|
315
|
+
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)) {
|
|
369
316
|
const config = loadConfigForCheck(root);
|
|
370
317
|
if ("level" in config)
|
|
371
318
|
return config;
|
|
319
|
+
// Asked lazily and at most once: only a pair models.json does not declare needs the listing.
|
|
320
|
+
let listing;
|
|
321
|
+
const listed = () => (listing ??= listModels());
|
|
372
322
|
const pairs = new Map();
|
|
373
323
|
// Routed (non-pi) models keyed by the backend they resolved to, so the routing note names
|
|
374
324
|
// each backend correctly; opencode is not claude.
|
|
@@ -394,19 +344,14 @@ export async function checkTierModels(root, modelsPath = piModelsPath(), auth =
|
|
|
394
344
|
? ""
|
|
395
345
|
: ` — ${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`;
|
|
396
346
|
if (pairs.size === 0 && routedModels.size === 0)
|
|
397
|
-
return (ompVars.length === 0 ? okOutcome : warnOutcome)(
|
|
398
|
-
|
|
347
|
+
return (ompVars.length === 0 ? okOutcome : warnOutcome)(`${PHRASE_NO_MODELS_DECLARED}${ompNote}`);
|
|
348
|
+
let providers = readPiProviders(modelsPath);
|
|
349
|
+
if (!providers && [...pairs.values()].some((p) => p.provider !== undefined) && (await listed()))
|
|
350
|
+
providers = new Map();
|
|
399
351
|
const fails = [];
|
|
400
352
|
const warns = [];
|
|
401
|
-
// Reserved agent ids: a pi provider whose name is an agent backend's id can never reach pi
|
|
402
|
-
// (the provider segment wins the routing), so its definitions are unreachable — warn.
|
|
403
|
-
if (providers) {
|
|
404
|
-
const reserved = [...providers.keys()].filter(isAgentId);
|
|
405
|
-
if (reserved.length > 0)
|
|
406
|
-
warns.push(`pi's definitions (${modelsPath}) declare provider ${reserved.map((p) => `"${p}"`).join(", ")} — the name is reserved for an agent backend, so model ids under it never reach pi`);
|
|
407
|
-
}
|
|
408
353
|
if (pairs.size > 0 && !providers)
|
|
409
|
-
return warnOutcome(
|
|
354
|
+
return warnOutcome(`${PHRASE_CANNOT_READ_MODELS}${modelsPath}${ompNote}`);
|
|
410
355
|
const readyProviders = new Set();
|
|
411
356
|
const checkedProviders = new Set();
|
|
412
357
|
for (const { provider, model } of pairs.values()) {
|
|
@@ -423,19 +368,29 @@ export async function checkTierModels(root, modelsPath = piModelsPath(), auth =
|
|
|
423
368
|
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`);
|
|
424
369
|
}
|
|
425
370
|
const defs = providers?.get(provider);
|
|
426
|
-
|
|
427
|
-
fails.push(`provider ${provider} is not in pi's definitions (${modelsPath})`);
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
const def = defs.find((d) => d.id === model);
|
|
371
|
+
const def = defs?.find((d) => d.id === model);
|
|
431
372
|
if (!def) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
373
|
+
const listedModels = await listed();
|
|
374
|
+
if (!listedModels?.has(`${provider}/${model}`)) {
|
|
375
|
+
const providerListed = listedModels != null && [...listedModels].some((k) => k.startsWith(`${provider}/`));
|
|
376
|
+
if (!defs && !providerListed) {
|
|
377
|
+
fails.push(listedModels === null
|
|
378
|
+
? `provider ${provider} is not in pi's definitions (${modelsPath})`
|
|
379
|
+
: `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`);
|
|
380
|
+
}
|
|
381
|
+
else if (listedModels === null) {
|
|
382
|
+
// No exact entry: pi clones the provider's default model for the id, inheriting that
|
|
383
|
+
// default's price and context window (BUGS.md 2026-10-06) — name that consequence
|
|
384
|
+
// instead of only the failed lookup.
|
|
385
|
+
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`);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
fails.push(`${provider}/${model} is neither in pi's definitions (${modelsPath}) nor listed by \`pi --list-models\``);
|
|
389
|
+
}
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
437
392
|
}
|
|
438
|
-
if (cacheReadUnpriced(providers, provider, model)) {
|
|
393
|
+
if (def && providers && cacheReadUnpriced(providers, provider, model)) {
|
|
439
394
|
// A priced model whose cache reads declare $0: pi bills cache-read tokens at zero and
|
|
440
395
|
// the daily budget undercounts the real spend (BUGS.md 2026-10-06).
|
|
441
396
|
warns.push(`${provider}/${model} prices cache reads at $0 — the daily budget will undercount its spend`);
|
|
@@ -455,37 +410,44 @@ export async function checkTierModels(root, modelsPath = piModelsPath(), auth =
|
|
|
455
410
|
const resolvedPi = `${pairs.size} declared ${agree(pairs.size, "model", "models")} resolve${readyProviders.size > 0 ? ` and ${agree(readyProviders.size, "provider", "all providers")} ${[...readyProviders].sort().join(", ")} report ready` : ""}`;
|
|
456
411
|
// One routing note per non-pi backend in use, so an opencode-routed model is never labeled
|
|
457
412
|
// as claude's. The opencode note also carries the design §2 caveat that a bare `opencode/…`
|
|
458
|
-
// id always means the OpenCode CLI, never pi's built-in `opencode` gateway.
|
|
413
|
+
// id always means the OpenCode CLI, never pi's built-in `opencode` gateway. Every note opens
|
|
414
|
+
// with the same `<n> <agent>-routed model[s] — ` frame — one phrase in one place (routedNote)
|
|
415
|
+
// so the branches cannot drift on it; the count prints beside the label, so the number form
|
|
416
|
+
// needs agree (not plural, which prints the count).
|
|
417
|
+
const routedNote = (agent, n, tail) => `${n} ${agent}-routed ${agree(n, "model", "models")} — ${tail}`;
|
|
459
418
|
const routedNotes = [];
|
|
460
419
|
for (const [agent, models] of routedModels) {
|
|
461
420
|
const n = models.size;
|
|
462
421
|
routedNotes.push(agent === "opencode"
|
|
463
|
-
?
|
|
464
|
-
:
|
|
422
|
+
? routedNote(agent, n, "`opencode/…` ids run on the OpenCode CLI, and pi's built-in `opencode` provider is not reachable from tumwater")
|
|
423
|
+
: agent === "codex"
|
|
424
|
+
? routedNote(agent, n, "resolved by the codex backend, not pi's definitions")
|
|
425
|
+
: routedNote(agent, n, "resolved by the claude backend, not pi's definitions"));
|
|
465
426
|
}
|
|
466
427
|
const routedText = routedNotes.length > 0 ? routedNotes.join("; ") : "";
|
|
467
428
|
const base = fails.length === 0 && warns.length === 0
|
|
468
|
-
? [pairs.size > 0 ? resolvedPi : "", routedText]
|
|
429
|
+
? joinParts([pairs.size > 0 ? resolvedPi : "", routedText])
|
|
469
430
|
: found;
|
|
470
431
|
if (fails.length > 0)
|
|
471
432
|
return failOutcome(`${base}${ompNote}`);
|
|
472
433
|
return (warns.length > 0 || ompVars.length > 0 ? warnOutcome : okOutcome)(`${base}${ompNote}`);
|
|
473
434
|
}
|
|
474
435
|
/** The doctor's image-input capability check (plans/image-input.md "Doctor", parts 1-2/5):
|
|
475
|
-
* which of the team's seams claim to read images, and whether every text-only enabled
|
|
436
|
+
* which of the team's seams claim to read images, and whether every text-only enabled role can
|
|
476
437
|
* reroute to one that does. Part 2 adds the live probe — with `probeImages` "missing" (the
|
|
477
438
|
* terminal `tumwater doctor` default) each claimed pi model with no fresh cached verdict is
|
|
478
|
-
* shown a real image and the answer is checked; with "all" (the `--reprobe` flag
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
439
|
+
* shown a real image and the answer is checked; with "all" (the `--reprobe` flag, the
|
|
440
|
+
* dashboard's Re-probe models button and the TUI Health tab's Ctrl+P) every claimed model is
|
|
441
|
+
* re-probed; with "never" (the TUI Health tab and GUI doctor endpoint on entry and refresh)
|
|
442
|
+
* nothing is spawned and only cached verdicts are read, so a refresh never spends a model call.
|
|
443
|
+
* claude seams are never probed — every Claude model reads images, and a probe would spend
|
|
444
|
+
* subscription window to confirm it.
|
|
483
445
|
*
|
|
484
446
|
* Covers the same seams as `tier models` (seamPairs — the tiers, enabled roles and the
|
|
485
447
|
* reviewer) plus the tiers' fallback pairs (the reroute part picks its target among them, so a
|
|
486
448
|
* text-only fallback would promise a reroute no fallback can serve). A claimed model whose probe
|
|
487
449
|
* failed (fresh verdict) is dropped from the readers and reads false from seamImageInput, so the
|
|
488
|
-
* reroute sweep never sends a text-only
|
|
450
|
+
* reroute sweep never sends a text-only role to a model that lost its probe. `providers`,
|
|
489
451
|
* `account`, `pathEnv` and `probe` are injectable so tests need no real pi install or account. */
|
|
490
452
|
export async function checkImageInput(root, modelsPath = piModelsPath(), providers = readPiProviders(modelsPath), account, probeImages = "never", pathEnv = envPath(), probe = runImageProbe) {
|
|
491
453
|
const config = loadConfigForCheck(root);
|
|
@@ -495,10 +457,13 @@ export async function checkImageInput(root, modelsPath = piModelsPath(), provide
|
|
|
495
457
|
// answer; unknown (null) pairs are left to that check. A missing definitions file only blocks
|
|
496
458
|
// this check when a seam actually needs it — an all-claude team reads images without it.
|
|
497
459
|
const accountNow = account === undefined ? readClaudeAccount(root) : account;
|
|
460
|
+
// The free-ness lookup reads the image-input check's one claude account (part 2a/9).
|
|
461
|
+
const accounts = (b) => (b === "claude" ? accountNow : null);
|
|
462
|
+
const listed = readPiListedModels(root);
|
|
498
463
|
const pairs = seamPairs(config);
|
|
499
464
|
const needsDefinitions = pairs.some((p) => usesPiModels(p.agent ?? "pi"));
|
|
500
465
|
if (providers === null && needsDefinitions)
|
|
501
|
-
return warnOutcome(
|
|
466
|
+
return warnOutcome(`${PHRASE_CANNOT_READ_MODELS}${modelsPath}`);
|
|
502
467
|
const label = modelLabel;
|
|
503
468
|
// The catalog claims (raw `input`, ignoring the cache): what to probe and report, including a
|
|
504
469
|
// model whose earlier probe failed (it must still be named and re-probed by --reprobe).
|
|
@@ -506,7 +471,7 @@ export async function checkImageInput(root, modelsPath = piModelsPath(), provide
|
|
|
506
471
|
for (const pair of pairs) {
|
|
507
472
|
if (pair.model === undefined)
|
|
508
473
|
continue;
|
|
509
|
-
if (seamImageInput(pair, providers) === true)
|
|
474
|
+
if (seamImageInput(pair, providers, undefined, listed) === true)
|
|
510
475
|
claimed.set(imageProbeKey(pair.agent, pair.provider, pair.model), {
|
|
511
476
|
agent: pair.agent,
|
|
512
477
|
provider: pair.provider,
|
|
@@ -516,16 +481,16 @@ export async function checkImageInput(root, modelsPath = piModelsPath(), provide
|
|
|
516
481
|
}
|
|
517
482
|
// The tiers' fallback pairs: a reroute target must itself read images to help, and part 4's
|
|
518
483
|
// fallback regime picks among these, so a text-only fallback team is a coverage gap here.
|
|
519
|
-
const resolved = resolveTierFallbacks(config, (p) => piPairFree(providers, p
|
|
484
|
+
const resolved = resolveTierFallbacks(config, (p) => piPairFree(providers, p, accounts));
|
|
520
485
|
for (const tier of MODEL_TIERS) {
|
|
521
486
|
const pair = resolved[tier].pair;
|
|
522
487
|
if (pair?.model === undefined)
|
|
523
488
|
continue;
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
const agent = "pi";
|
|
528
|
-
if (seamImageInput(pair, providers) === true)
|
|
489
|
+
// The readers (seamImageInput, imageReroute) resolve a pair with no backend to the "pi"
|
|
490
|
+
// agent, so the cache key must carry "pi" — keying it `undefined` would write a verdict the
|
|
491
|
+
// readers never read.
|
|
492
|
+
const agent = pair.backend ?? "pi";
|
|
493
|
+
if (seamImageInput({ ...pair, agent }, providers, undefined, listed) === true)
|
|
529
494
|
claimed.set(imageProbeKey(agent, pair.provider, pair.model), {
|
|
530
495
|
agent,
|
|
531
496
|
provider: pair.provider,
|
|
@@ -547,7 +512,7 @@ export async function checkImageInput(root, modelsPath = piModelsPath(), provide
|
|
|
547
512
|
const agent = seam.agent;
|
|
548
513
|
// A backend that registers an imageInput hook trusts the hook (claude today, which reads
|
|
549
514
|
// every model); a backend without one is probed through its model catalog.
|
|
550
|
-
if (agent !== undefined &&
|
|
515
|
+
if (agent !== undefined && backendById(agent)?.imageInput !== undefined) {
|
|
551
516
|
readsItems.push(label(agent, seam.provider, seam.model));
|
|
552
517
|
continue;
|
|
553
518
|
}
|
|
@@ -582,207 +547,76 @@ export async function checkImageInput(root, modelsPath = piModelsPath(), provide
|
|
|
582
547
|
readsItems.push(`${label(agent, seam.provider, seam.model)} (not verified yet — run \`tumwater doctor\`)`);
|
|
583
548
|
}
|
|
584
549
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const reroutes = [];
|
|
590
|
-
const noTarget = [];
|
|
591
|
-
for (const role of enabledRoleIds(config)) {
|
|
592
|
-
const rc = configForRole(config, role);
|
|
593
|
-
if (rc.model === undefined)
|
|
594
|
-
continue;
|
|
595
|
-
if (seamImageInput(rc, providers, probes) !== false)
|
|
596
|
-
continue; // reads images, or unknown (to tier models)
|
|
597
|
-
const rr = imageReroute(config, role, "primary", modelsPath, accountNow, probes);
|
|
598
|
-
if (rr && seamImageInput(rr.config, providers, probes) === true)
|
|
599
|
-
reroutes.push(`${role} text-only (${label(rc.agent, rc.provider, rc.model)}) — reroutes to ${rr.tier} (${label(rr.config.agent, rr.config.provider, rr.config.model)})`);
|
|
600
|
-
else
|
|
601
|
-
noTarget.push(`${role} text-only (${label(rc.agent, rc.provider, rc.model)})`);
|
|
602
|
-
}
|
|
603
|
-
// A team that declared no models at all leaves capability to pi's own defaults — the same
|
|
604
|
-
// "nothing configured" stance as tier models, informational rather than a warning.
|
|
605
|
-
const declared = pairs.some((p) => p.model !== undefined) ||
|
|
606
|
-
MODEL_TIERS.some((t) => resolved[t].pair?.model !== undefined);
|
|
607
|
-
if (!declared)
|
|
608
|
-
return okOutcome("no models declared — every model job uses pi's own default");
|
|
609
|
-
const readsLine = readsItems.length > 0 ? `reads images: ${[...readsItems].sort().join(", ")}` : "";
|
|
610
|
-
const details = [readsLine, ...reroutes].filter(Boolean).join("; ");
|
|
611
|
-
if (failed.length > 0)
|
|
612
|
-
return failOutcome(`${failed.join("; ")} — remove "image" from its input in pi's models.json or fix the endpoint, then run \`tumwater doctor --reprobe\``);
|
|
613
|
-
if (couldNot.length > 0)
|
|
614
|
-
return warnOutcome(`${couldNot.join("; ")}; ${details}`.replace(/;\s*$/, ""));
|
|
615
|
-
if (noTarget.length > 0)
|
|
616
|
-
return warnOutcome(`${noTarget.join("; ")} — no configured model reads images, so a run that reads an image fails; declare image input on a vision model in pi's models.json, or point a tier at one`);
|
|
617
|
-
if (readsItems.length === 0)
|
|
618
|
-
return warnOutcome("no configured model reads images — a run that reads an image fails; declare image input on a vision model in pi's models.json, or point a tier at one");
|
|
619
|
-
return okOutcome(details);
|
|
620
|
-
}
|
|
621
|
-
/** The provider of a non-gateway opencode model id (`opencode/<provider>/<model>`), or
|
|
622
|
-
* undefined for a gateway id (a bare `opencode/<model>` or `<model>`, served by OpenCode's
|
|
623
|
-
* own gateway with no credential). A model id with more than the `opencode/` namespace prefix
|
|
624
|
-
* is provider-qualified and needs a credential OpenCode lists. */
|
|
625
|
-
function opencodeProviderOf(model) {
|
|
626
|
-
const parts = model.split("/");
|
|
627
|
-
if (parts[0] === "opencode" && parts.length >= 3)
|
|
628
|
-
return parts[1];
|
|
629
|
-
if (parts[0] !== "opencode" && parts.length >= 2)
|
|
630
|
-
return parts[0];
|
|
631
|
-
return undefined;
|
|
632
|
-
}
|
|
633
|
-
/** Whether `opencode auth list --format json` (bounded) lists `provider` exactly. A probe that
|
|
634
|
-
* cannot run, exits nonzero, or omits the provider reads as NOT listed — the safe direction,
|
|
635
|
-
* since doctor is warning the operator to add the credential, never confirming one it did not
|
|
636
|
-
* see. The list is matched EXACTLY, not as a raw substring: when the output parses as an array
|
|
637
|
-
* (of names or of per-credential objects), a credential listed as `anthropic` must never vouch
|
|
638
|
-
* for `anthropic-foo` twice over; when it cannot be parsed as JSON (a shape the CLI may change),
|
|
639
|
-
* the text is matched word-bounded so a truncated or prose line still reads. */
|
|
640
|
-
async function opencodeAuthLists(config, provider, pathEnv, timeoutMs) {
|
|
641
|
-
const resolved = opencodeBackend.resolveBin(config);
|
|
642
|
-
const result = await boundedProbe(resolved.bin, ["auth", "list", "--format", "json"], childEnv({ PATH: pathEnv }), timeoutMs, AUTH_CHECK_MAX_OUTPUT_BYTES);
|
|
643
|
-
if (result?.code !== 0)
|
|
644
|
-
return false;
|
|
645
|
-
const text = result.stdout;
|
|
646
|
-
try {
|
|
647
|
-
const parsed = JSON.parse(text);
|
|
648
|
-
if (Array.isArray(parsed)) {
|
|
649
|
-
return parsed.some((item) => {
|
|
650
|
-
if (typeof item === "string")
|
|
651
|
-
return item === provider;
|
|
652
|
-
return (item !== null &&
|
|
653
|
-
typeof item === "object" &&
|
|
654
|
-
Object.values(item).some((v) => v === provider));
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
catch {
|
|
659
|
-
// Not an array-shaped JSON payload — fall through to the word-bounded text match.
|
|
660
|
-
}
|
|
661
|
-
const esc = provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
662
|
-
return new RegExp(`(^|[^A-Za-z0-9_-])${esc}([^A-Za-z0-9_-]|$)`).test(text);
|
|
663
|
-
}
|
|
664
|
-
/** Whether a JSON-ish key (optionally quoted, followed by a colon) appears in a config file's
|
|
665
|
-
* text — a loose match that tolerates JSONC comments without parsing them. */
|
|
666
|
-
function jsonKeyPresent(text, key) {
|
|
667
|
-
return new RegExp(`["']?${key}["']?\\s*:`).test(text);
|
|
668
|
-
}
|
|
669
|
-
/** The operator's global opencode config files and AGENTS.md (plans/opencode-backend.md §7
|
|
670
|
-
* "Leaks", part 7/7): the operator's own config and instructions still load into team runs,
|
|
671
|
-
* so each MCP server (disabled for team runs) or plugin/agent/instructions entry or the
|
|
672
|
-
* AGENTS.md file itself produces one warning naming the file and what leaks in: an MCP server
|
|
673
|
-
* is switched off by the harness, while a plugin/agent/instructions entry or AGENTS.md is NOT
|
|
674
|
-
* switched off and still loads. `configHome` is the config root to scan — the XDG config home
|
|
675
|
-
* (when set) or `~/.config`, the SAME folder opencode's own operatorMcpNames reads, so the leak
|
|
676
|
-
* scan matches the config a run actually loads. Config files
|
|
677
|
-
* are JSON/C, so the keys are matched loosely (a word followed by a colon) rather than fully
|
|
678
|
-
* parsed — enough to name what still loads before a run inherits it. */
|
|
679
|
-
function opencodeConfigLeaks(configHome) {
|
|
680
|
-
const dir = path.join(configHome, "opencode");
|
|
681
|
-
const warns = [];
|
|
682
|
-
for (const file of ["config.json", "opencode.json", "opencode.jsonc"]) {
|
|
683
|
-
const text = readTextOrNull(path.join(dir, file));
|
|
684
|
-
if (text === null)
|
|
685
|
-
continue;
|
|
686
|
-
const parts = [];
|
|
687
|
-
let mcp = false;
|
|
688
|
-
if (jsonKeyPresent(text, "mcp") && jsonKeyPresent(text, "servers")) {
|
|
689
|
-
parts.push("an MCP server");
|
|
690
|
-
mcp = true;
|
|
550
|
+
let saveProblem = null;
|
|
551
|
+
if (probedAny) {
|
|
552
|
+
try {
|
|
553
|
+
writeImageProbeCache(root, probes);
|
|
691
554
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
parts.push(`a ${key} entry`);
|
|
555
|
+
catch (err) {
|
|
556
|
+
saveProblem = errorMessage(err);
|
|
695
557
|
}
|
|
696
|
-
if (parts.length === 0)
|
|
697
|
-
continue;
|
|
698
|
-
const joined = parts.join(" and ");
|
|
699
|
-
// Only the MCP server is disabled for team runs; everything else still loads.
|
|
700
|
-
const verdict = mcp && parts.length === 1 ? " — disabled for team runs" : " — still loads into team runs";
|
|
701
|
-
warns.push(`${path.join(dir, file)} carries ${joined}${verdict}`);
|
|
702
558
|
}
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
else
|
|
718
|
-
|
|
559
|
+
const verdict = () => {
|
|
560
|
+
// Text-only enabled roles: which can reroute to a tier that reads images (honoring failed
|
|
561
|
+
// probes), and which cannot.
|
|
562
|
+
const reroutes = [];
|
|
563
|
+
const noTarget = [];
|
|
564
|
+
for (const role of enabledRoleIds(config)) {
|
|
565
|
+
const rc = configForRole(config, role);
|
|
566
|
+
if (rc.model === undefined)
|
|
567
|
+
continue;
|
|
568
|
+
if (seamImageInput(rc, providers, probes, listed) !== false)
|
|
569
|
+
continue; // reads images, or unknown (to tier models)
|
|
570
|
+
const rr = imageReroute(config, role, "primary", modelsPath, accountNow, probes, null, listed);
|
|
571
|
+
if (rr && seamImageInput(rr.config, providers, probes, listed) === true)
|
|
572
|
+
reroutes.push(`${role} text-only (${label(rc.agent, rc.provider, rc.model)}) — reroutes to ${rr.tier} (${label(rr.config.agent, rr.config.provider, rr.config.model)})`);
|
|
573
|
+
else
|
|
574
|
+
noTarget.push(`${role} text-only (${label(rc.agent, rc.provider, rc.model)})`);
|
|
719
575
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
576
|
+
// A team that declared no models at all leaves capability to pi's own defaults — the same
|
|
577
|
+
// "nothing configured" stance as tier models, informational rather than a warning.
|
|
578
|
+
const declared = pairs.some((p) => p.model !== undefined) ||
|
|
579
|
+
MODEL_TIERS.some((t) => resolved[t].pair?.model !== undefined);
|
|
580
|
+
if (!declared)
|
|
581
|
+
return okOutcome(PHRASE_NO_MODELS_DECLARED);
|
|
582
|
+
const readsLine = readsItems.length > 0 ? `reads images: ${[...readsItems].sort().join(", ")}` : "";
|
|
583
|
+
const details = joinParts([readsLine, ...reroutes]);
|
|
584
|
+
if (failed.length > 0)
|
|
585
|
+
return failOutcome(`${failed.join("; ")} — remove "image" from its input in pi's models.json or fix the endpoint, then run \`tumwater doctor --reprobe\``);
|
|
586
|
+
if (couldNot.length > 0)
|
|
587
|
+
return warnOutcome(`${couldNot.join("; ")}; ${details}`.replace(/;\s*$/, ""));
|
|
588
|
+
if (noTarget.length > 0)
|
|
589
|
+
return warnOutcome(`${noTarget.join("; ")} — no configured model reads images, so a run that reads an image fails; declare image input on a vision model in pi's models.json, or point a tier at one`);
|
|
590
|
+
if (readsItems.length === 0)
|
|
591
|
+
return warnOutcome("no configured model reads images — a run that reads an image fails; declare image input on a vision model in pi's models.json, or point a tier at one");
|
|
592
|
+
return okOutcome(details);
|
|
593
|
+
};
|
|
594
|
+
const outcome = verdict();
|
|
595
|
+
if (saveProblem === null)
|
|
596
|
+
return outcome;
|
|
597
|
+
return {
|
|
598
|
+
level: outcome.level === "ok" ? "warn" : outcome.level,
|
|
599
|
+
detail: `${outcome.detail} — could not save the image-probe cache (${saveProblem}), so the next doctor probes again`,
|
|
600
|
+
};
|
|
733
601
|
}
|
|
734
|
-
/**
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
|
|
742
|
-
* spawns nothing and reads nothing. `pathEnv` and `home` are injectable so tests need no real
|
|
743
|
-
* PATH or HOME. The version half of the opencode pre-flight is the sibling checkOpencodeVersion
|
|
744
|
-
* in doctor-checks.ts. */
|
|
745
|
-
export async function checkOpencode(root, pathEnv = envPath(), home = os.homedir(), xdgConfigHome = process.env.XDG_CONFIG_HOME ?? "", timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
602
|
+
/** `gh` readiness for the opt-in `feedback` observer (plans/feedback-role.md "Shape" → "Part 1"):
|
|
603
|
+
* the role files issues as the operator's GitHub login through `gh`, so doctor confirms `gh`
|
|
604
|
+
* resolves and `gh auth status` exits 0 before a run tries to file — the codex-login pattern
|
|
605
|
+
* (plans/codex-backend.md part 9/9). Shown only when the role is enabled: on a default install
|
|
606
|
+
* nothing files upstream, so there is nothing to check and the row is omitted (null). `probe`
|
|
607
|
+
* is injectable so tests use a fake gh (test/fakes/fake-gh.ts); the real probe runs `gh` under
|
|
608
|
+
* the scrubbed run environment with a bounded deadline. */
|
|
609
|
+
export async function checkFeedbackFiling(root, pathEnv = envPath(), probe = (args) => boundedProbe("gh", args, childEnv({ PATH: pathEnv }), AUTH_CHECK_TIMEOUT_MS, AUTH_CHECK_MAX_OUTPUT_BYTES)) {
|
|
746
610
|
const config = loadConfigForCheck(root);
|
|
747
611
|
if ("level" in config)
|
|
748
612
|
return config;
|
|
749
|
-
if (!
|
|
750
|
-
return
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const entry = cache.models[key];
|
|
759
|
-
if (entry === undefined) {
|
|
760
|
-
fails.push(`${pair.model} is not in the opencode catalog cache`);
|
|
761
|
-
continue;
|
|
762
|
-
}
|
|
763
|
-
const price = entry.free ? "free" : `$${entry.inputPerM ?? 0}/$${entry.outputPerM ?? 0} per M`;
|
|
764
|
-
const window = entry.context !== undefined ? `, ${entry.context.toLocaleString("en-US")}-token window` : "";
|
|
765
|
-
notes.push(`${pair.model} — ${price}${window}`);
|
|
766
|
-
// A model with no effort list at all supports no thinking level, so a requested level is
|
|
767
|
-
// dropped just the same as one the list omits.
|
|
768
|
-
if (pair.thinking !== undefined &&
|
|
769
|
-
pair.thinking !== "" &&
|
|
770
|
-
!entry.efforts?.includes(pair.thinking)) {
|
|
771
|
-
warns.push(`${pair.model} has no ${pair.thinking} effort — that thinking level is dropped`);
|
|
772
|
-
}
|
|
773
|
-
const provider = opencodeProviderOf(pair.model);
|
|
774
|
-
if (provider !== undefined && !(await opencodeAuthLists(config, provider, pathEnv, timeoutMs))) {
|
|
775
|
-
warns.push(`${pair.model} is a non-gateway provider opencode does not list a credential for`);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
warns.push(...opencodeConfigLeaks(xdgConfigHome !== "" ? xdgConfigHome : path.join(home, ".config")));
|
|
779
|
-
const db = statOrNull(opencodeDbPath(root));
|
|
780
|
-
const spillDir = opencodeSpillDir(root);
|
|
781
|
-
const spill = spillDir !== "" && statOrNull(spillDir)?.isDirectory() === true ? opencodeDirBytes(spillDir) : null;
|
|
782
|
-
const sizeNotes = `db ${opencodeBytes(db?.size ?? 0)}${spill !== null ? `, spill ${opencodeBytes(spill)}` : ", no spill dir"}`;
|
|
783
|
-
const found = [...fails, ...warns];
|
|
784
|
-
const base = found.length === 0 ? `opencode ready — ${notes.join("; ")}; ${sizeNotes}` : found.join("; ");
|
|
785
|
-
if (fails.length > 0)
|
|
786
|
-
return failOutcome(base);
|
|
787
|
-
return (warns.length > 0 ? warnOutcome : okOutcome)(base);
|
|
613
|
+
if (!config.roles.feedback?.enabled)
|
|
614
|
+
return null;
|
|
615
|
+
const version = await probe(["--version"]);
|
|
616
|
+
if (version?.code !== 0)
|
|
617
|
+
return failOutcome("gh is missing or unusable — install GitHub CLI (https://cli.github.com) so the feedback role can file issues as your login");
|
|
618
|
+
const auth = await probe(["auth", "status"]);
|
|
619
|
+
if (auth?.code !== 0)
|
|
620
|
+
return failOutcome("gh is not authenticated — run `gh auth login` — the feedback role files issues as your GitHub login");
|
|
621
|
+
return okOutcome("gh present and authenticated — the feedback role can file issues on znation/tumwater");
|
|
788
622
|
}
|