tumwater 0.4.0 → 0.4.2
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 +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { enabledRoleIds, parseJsonConfig, loadConfig, loadConfigSafe, } from "../config/config.js";
|
|
5
5
|
import { exampleConfigProblem, exampleDrift } from "../config/config-example.js";
|
|
6
6
|
import { bootstrapStatus } from "../gates/bootstrap-gates.js";
|
|
7
|
-
import { envPath, findOnPath, readTextOrNull, treeBytes } from "../files/files.js";
|
|
7
|
+
import { envPath, findOnPath, readTextOrNull, treeBytes, xdgConfigHomeDir } from "../files/files.js";
|
|
8
8
|
import { readJsonFile } from "../files/json-files.js";
|
|
9
9
|
import { PACKAGE_JSON, belowNodeFloor, packageEnginesNode } from "../version.js";
|
|
10
10
|
import { GIT_MISSING_MESSAGE } from "../git/git-run.js";
|
|
@@ -15,7 +15,7 @@ import { resolvedBackendConfig } from "../config/config-views.js";
|
|
|
15
15
|
import { CONFIG_BASENAME, EXAMPLE_CONFIG_BASENAME, configPath, owningTeamRoot } from "../paths.js";
|
|
16
16
|
import { errorMessage } from "../text/text.js";
|
|
17
17
|
import { formatBytes } from "../text/format.js";
|
|
18
|
-
import { briefFile } from "../brief/brief.js";
|
|
18
|
+
import { BRIEF_TEXT_MAX_CHARS, briefFile, promptBlockOf, readBrief } from "../brief/brief.js";
|
|
19
19
|
import { plural } from "../text/phrases.js";
|
|
20
20
|
import { backendsInUse, unregisteredBackendInUse } from "../gates/startup-gate.js";
|
|
21
21
|
import { claudeSeamsInUse, minVersionOutcome, opencodeSeamsInUse, versionFromProbe, piSeamsInUse } from "./doctor-model-checks.js";
|
|
@@ -157,8 +157,20 @@ export function checkBrief(root) {
|
|
|
157
157
|
catch (err) {
|
|
158
158
|
return failOutcome(errorMessage(err));
|
|
159
159
|
}
|
|
160
|
-
if (owner)
|
|
160
|
+
if (owner) {
|
|
161
|
+
const prompt = readBrief(root)?.prompt ?? "";
|
|
162
|
+
if (prompt === "") {
|
|
163
|
+
return warnOutcome(`${owner} carries an empty tumwater:prompt block — every role would run without its project brief; add the brief text or re-run \`tumwater init\``);
|
|
164
|
+
}
|
|
165
|
+
const raw = readTextOrNull(path.join(root, owner));
|
|
166
|
+
if (raw !== null) {
|
|
167
|
+
const block = promptBlockOf(raw);
|
|
168
|
+
if (block !== null && block.length > BRIEF_TEXT_MAX_CHARS) {
|
|
169
|
+
return warnOutcome(`brief in ${owner} is ${block.length} characters, over the ${BRIEF_TEXT_MAX_CHARS}-character read cap every role sees (the prompt is truncated at ${BRIEF_TEXT_MAX_CHARS} chars); hand-edit ${owner} to shrink the block below ${BRIEF_TEXT_MAX_CHARS} characters, or paste a shorter brief in the dashboard's Brief card`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
161
172
|
return okOutcome(`brief in ${owner}`);
|
|
173
|
+
}
|
|
162
174
|
if (fs.existsSync(path.join(root, "README.md"))) {
|
|
163
175
|
return warnOutcome(`README.md carries no tumwater:prompt markers — the team would run without its project brief; add the block or re-run \`tumwater init\``);
|
|
164
176
|
}
|
|
@@ -353,7 +365,7 @@ export function checkPiExtensions(config, homeDir = os.homedir(), pathEnv = envP
|
|
|
353
365
|
/** The operator's global opencode config files, in XDG order — the same home `opencode-config.ts`
|
|
354
366
|
* reads for MCP names. Home is the operator's home; `$XDG_CONFIG_HOME`, when set, wins. */
|
|
355
367
|
function opencodeGlobalConfigPaths(homeDir) {
|
|
356
|
-
const configHome = process.env
|
|
368
|
+
const configHome = xdgConfigHomeDir(process.env, homeDir);
|
|
357
369
|
return ["config.json", "opencode.json", "opencode.jsonc"].map((file) => path.join(configHome, "opencode", file));
|
|
358
370
|
}
|
|
359
371
|
/** Report the operator's global opencode config file that mentions funes, or null.
|
|
@@ -10,8 +10,8 @@ import { belowNodeFloor } from "../version.js";
|
|
|
10
10
|
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
11
11
|
import { claudeSeamsInUse, loadConfigForCheck, versionFromProbe, seamedModels, } from "./doctor-model-checks.js";
|
|
12
12
|
/** The doctor's claude credential and version readiness checks, split out of
|
|
13
|
-
* doctor-model-checks.ts (whose shared seam/model framework and the pi
|
|
14
|
-
*
|
|
13
|
+
* doctor-model-checks.ts (whose shared seam/model framework and the pi and tier-models
|
|
14
|
+
* checks stay there): checkClaudeAuth, checkAnthroApiKey and checkClaudeVersion.
|
|
15
15
|
* These are the doctor's only checks that spawn the `claude` binary and read its auth — a
|
|
16
16
|
* distinct concern from the filesystem/git checks, kept together so a change to how doctor
|
|
17
17
|
* spawns or scrubs claude does not touch the other model checks. The shared report contract
|
|
@@ -10,7 +10,7 @@ import { codexHookTrust } from "../agent/codex/codex-hook-trust.js";
|
|
|
10
10
|
import { parseCodexLoginStatus } from "../agent/codex/codex-account.js";
|
|
11
11
|
import { codexWriteGuardDefinition } from "../agent/codex/codex-write-guard.js";
|
|
12
12
|
import { resolvedBackendConfig } from "../config/config-views.js";
|
|
13
|
-
import { envPath, isDirectory, readTextOrNull, statOrNull, treeBytes } from "../files/files.js";
|
|
13
|
+
import { envPath, isDirectory, readTextOrNull, statOrNull, treeBytes, walkFiles } from "../files/files.js";
|
|
14
14
|
import { formatBytes, tokenWindowLabel } from "../text/format.js";
|
|
15
15
|
import { errorMessage } from "../text/text.js";
|
|
16
16
|
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
@@ -152,26 +152,15 @@ function codexSessionCredits(home) {
|
|
|
152
152
|
if (!isDirectory(sessions))
|
|
153
153
|
return null;
|
|
154
154
|
const rollouts = [];
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
// The walk-and-swallow recursion is files.ts's walkFiles; each rollout file is statted so
|
|
156
|
+
// the newest (by mtime) can win below.
|
|
157
|
+
walkFiles(sessions, (file, entry) => {
|
|
158
|
+
if (/^rollout-.*\.jsonl$/.test(entry.name)) {
|
|
159
|
+
const st = statOrNull(file);
|
|
160
|
+
if (st !== null)
|
|
161
|
+
rollouts.push({ file, mtimeMs: st.mtimeMs });
|
|
159
162
|
}
|
|
160
|
-
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
for (const entry of entries) {
|
|
164
|
-
const p = path.join(dir, entry.name);
|
|
165
|
-
if (entry.isDirectory())
|
|
166
|
-
walk(p);
|
|
167
|
-
else if (entry.isFile() && /^rollout-.*\.jsonl$/.test(entry.name)) {
|
|
168
|
-
const st = statOrNull(p);
|
|
169
|
-
if (st !== null)
|
|
170
|
-
rollouts.push({ file: p, mtimeMs: st.mtimeMs });
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
walk(sessions);
|
|
163
|
+
});
|
|
175
164
|
rollouts.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
176
165
|
for (const r of rollouts) {
|
|
177
166
|
const text = readTextOrNull(r.file);
|
|
@@ -70,7 +70,7 @@ export function staleWorkspacePins(root, now = Date.now()) {
|
|
|
70
70
|
}));
|
|
71
71
|
}
|
|
72
72
|
/** The shared one-pin clause of the doctor's `worktree pool` row (checkWorktreePool) and the
|
|
73
|
-
* standing `workspace-pins` alert (team-
|
|
73
|
+
* standing `workspace-pins` alert (team-alert-tail.ts): `<role> pinned on <dir> for <Nh>`. One
|
|
74
74
|
* home so the row and the alert cannot drift on how a pin's age is phrased (whole hours, off
|
|
75
75
|
* the pin's whole-ms age). `role` is a non-optional string on StaleWorkspacePin, but the
|
|
76
76
|
* clause renders it through textOr like the row always did, so a malformed pin can never
|
|
@@ -433,7 +433,9 @@ function claimCounts(claims) {
|
|
|
433
433
|
* multi-instance role configured with fewer pooled slots than maxConcurrent also warns:
|
|
434
434
|
* instances then contend for the same checkouts. A disabled role runs none of its
|
|
435
435
|
* configured instances, so it never contends and
|
|
436
|
-
* stays out of that warning.
|
|
436
|
+
* stays out of that warning. The pool-mismatch clause comes from the shared
|
|
437
|
+
* `smallPoolProblems` selector, so the row and the always-on `instance-pool` alert cannot
|
|
438
|
+
* drift on the trigger, the role list, or the wording.
|
|
437
439
|
* Read-only, like every check; `now` is the claim-age test seam, and an unreadable
|
|
438
440
|
* tumwater.json (config undefined) skips the walk — checkInit reports the config itself. */
|
|
439
441
|
export function checkWorkInstances(root, config, now = Date.now()) {
|
|
@@ -443,17 +445,29 @@ export function checkWorkInstances(root, config, now = Date.now()) {
|
|
|
443
445
|
const entries = staleClaimEntries(root, runnerIds(config), now);
|
|
444
446
|
const gone = entries.filter((e) => e.kind === "gone");
|
|
445
447
|
const stale = entries.filter((e) => e.kind === "idle");
|
|
448
|
+
const smallPool = smallPoolProblems(config);
|
|
449
|
+
if (gone.length === 0 && stale.length === 0 && smallPool.length === 0)
|
|
450
|
+
return okOutcome(claimCounts(claims));
|
|
451
|
+
const parts = [...gone.map(staleClaimDetail), ...stale.map(staleClaimDetail)];
|
|
452
|
+
parts.push(...smallPool);
|
|
453
|
+
return warnOutcome(parts.join("; "));
|
|
454
|
+
}
|
|
455
|
+
/** An enabled multi-instance role contending for a worktree pool smaller than the run cap —
|
|
456
|
+
* the pool-mismatch clause of the doctor's `parallel instances` row (checkWorkInstances) and
|
|
457
|
+
* the always-on `instance-pool` alert (alert-feeds.ts instancePoolProblems). Returns the
|
|
458
|
+
* `INSTANCE_ROLES` ids with more than one configured instance while `slotCount(config)`
|
|
459
|
+
* (worktreeSlots, else maxConcurrent + 1) is below `config.maxConcurrent`, phrased as the
|
|
460
|
+
* row's one clause; `[]` when no such role or the pool is at least `maxConcurrent`. Shared by
|
|
461
|
+
* the doctor row and the alert so they cannot drift on the trigger, the role list, or the
|
|
462
|
+
* wording. */
|
|
463
|
+
export function smallPoolProblems(config) {
|
|
446
464
|
const multi = Object.entries(config.roles)
|
|
447
465
|
.filter(([id, role]) => INSTANCE_ROLES.has(id) && role.enabled && (role.instances ?? 1) > 1)
|
|
448
466
|
.map(([id]) => id);
|
|
449
467
|
const slots = slotCount(config);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const parts = [...gone.map(staleClaimDetail), ...stale.map(staleClaimDetail)];
|
|
454
|
-
if (smallPool)
|
|
455
|
-
parts.push(`${multi.join(", ")} run instances but worktreeSlots ${slots} is below maxConcurrent ${config.maxConcurrent}`);
|
|
456
|
-
return warnOutcome(parts.join("; "));
|
|
468
|
+
if (multi.length === 0 || slots >= config.maxConcurrent)
|
|
469
|
+
return [];
|
|
470
|
+
return [`${multi.join(", ")} run instances but worktreeSlots ${slots} is below maxConcurrent ${config.maxConcurrent}`];
|
|
457
471
|
}
|
|
458
472
|
/** Provider concurrency caps (plans/provider-concurrency.md §8): audit the
|
|
459
473
|
* `maxConcurrentPerProvider` map against the seams the config can actually spawn — each role,
|
|
@@ -466,17 +480,22 @@ export function checkWorkInstances(root, config, now = Date.now()) {
|
|
|
466
480
|
* well-formed map; a broken config is reported by the init check. Takes `root` to read pi's
|
|
467
481
|
* own default model (piSettingsPath), the same seam the orchestrator's admission uses, so a
|
|
468
482
|
* bare no-model pi seam keys exactly as the team would key it. */
|
|
469
|
-
|
|
483
|
+
/** The warnings behind checkProviderConcurrency, as a pure function of the config and pi's
|
|
484
|
+
* default model name: a cap key no seam resolves to (a typo, or a provider not in use), a cap
|
|
485
|
+
* at or above `maxConcurrent` (which never binds), and a capped team whose seams include one
|
|
486
|
+
* with no key (pi's default provider is unreadable, so that seam cannot be capped). The doctor's
|
|
487
|
+
* `provider concurrency` row and the always-on `provider-caps` team alert both read this one
|
|
488
|
+
* computation, so the row and the alert cannot drift on the trigger or the wording. `[]` for no
|
|
489
|
+
* config, a map with no enforcedKeyCaps entries, or a map with no problem. `piDefault` is the
|
|
490
|
+
* bare default model the orchestrator's admission seam carries, read once by the callers
|
|
491
|
+
* through readPiDefaultModel(piSettingsPath(piModelsPath()), root). */
|
|
492
|
+
export function providerConcurrencyProblems(config, piDefault) {
|
|
470
493
|
if (!config)
|
|
471
|
-
return
|
|
494
|
+
return [];
|
|
472
495
|
const caps = config.maxConcurrentPerProvider;
|
|
473
496
|
const capEntries = enforcedKeyCaps(caps);
|
|
474
497
|
if (capEntries.length === 0)
|
|
475
|
-
return
|
|
476
|
-
// The pi default model the orchestrator's admission seam carries: a bare no-model pi seam
|
|
477
|
-
// resolves its key only through it, so a team whose default is readable has keyed seams and
|
|
478
|
-
// only an unreadable default leaves a key-less seam to warn about.
|
|
479
|
-
const piDefault = readPiDefaultModel(piSettingsPath(piModelsPath()), root);
|
|
498
|
+
return [];
|
|
480
499
|
const seams = seamsInUse(config);
|
|
481
500
|
const keyed = [];
|
|
482
501
|
for (const s of seams) {
|
|
@@ -498,9 +517,23 @@ export function checkProviderConcurrency(config, root) {
|
|
|
498
517
|
const capSet = new Set(capKeys);
|
|
499
518
|
const keyless = seams.filter((s) => seamConcurrencyKey(s, piDefault) === undefined);
|
|
500
519
|
if (capSet.size > 0 && keyless.length > 0) {
|
|
501
|
-
warnings.push(`${keyless.length
|
|
520
|
+
warnings.push(`${plural(keyless.length, "model job")} ${agree(keyless.length, "resolves", "resolve")} to no provider key (pi's default provider is unreadable, so they cannot be capped): ${keyless.map((s) => s.label).join(", ")}`);
|
|
502
521
|
}
|
|
503
|
-
|
|
522
|
+
return warnings;
|
|
523
|
+
}
|
|
524
|
+
export function checkProviderConcurrency(config, root) {
|
|
525
|
+
if (!config)
|
|
526
|
+
return okOutcome("no config to audit");
|
|
527
|
+
const caps = config.maxConcurrentPerProvider;
|
|
528
|
+
const capEntries = enforcedKeyCaps(caps);
|
|
529
|
+
if (capEntries.length === 0)
|
|
530
|
+
return okOutcome("no per-provider caps configured");
|
|
531
|
+
// The pi default model the orchestrator's admission seam carries: a bare no-model pi seam
|
|
532
|
+
// resolves its key only through it, so a team whose default is readable has keyed seams and
|
|
533
|
+
// only an unreadable default leaves a key-less seam to warn about.
|
|
534
|
+
const piDefault = readPiDefaultModel(piSettingsPath(piModelsPath()), root);
|
|
535
|
+
const problems = providerConcurrencyProblems(config, piDefault);
|
|
536
|
+
if (problems.length === 0)
|
|
504
537
|
return okOutcome(`${plural(capEntries.length, "provider cap")} — all model jobs resolve`);
|
|
505
|
-
return warnOutcome(
|
|
538
|
+
return warnOutcome(problems.join("; "));
|
|
506
539
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { roleLabel } from "../roles/role-labels.js";
|
|
2
|
+
import { sortEntriesByKey } from "../collections.js";
|
|
2
3
|
import { pausedRoles, pausedRolesUntil, standingTeamPause } from "../team/team-state.js";
|
|
3
4
|
import { MODEL_TIERS } from "../config/config-schema.js";
|
|
4
5
|
import { withModelOverrides } from "../config/config-views.js";
|
|
@@ -10,7 +11,7 @@ import { MAINTENANCE_DAILY_FLOOR } from "../gates/maintenance-quota.js";
|
|
|
10
11
|
import { heldByMainRed, mainCheckForPoll, maintenanceQuotaForPoll } from "../status/status-polls.js";
|
|
11
12
|
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
12
13
|
import { baseRoleOf, runnerIds, runnerPaused } from "../roles/runner-ids.js";
|
|
13
|
-
import { ERROR_STREAK_BREAKER,
|
|
14
|
+
import { ERROR_STREAK_BREAKER, errorStreakAtWarn } from "../run/run-apply.js";
|
|
14
15
|
import { squash } from "../text/text.js";
|
|
15
16
|
import { usd } from "../text/format.js";
|
|
16
17
|
import { parseQuietHours, quietHoursStatus, quietWindowEnd, roleQuietHold } from "../scheduling/quiet-hours.js";
|
|
@@ -32,6 +33,12 @@ export function loadRunnerStates(root, config) {
|
|
|
32
33
|
.map((id) => loadRunnerState(root, id))
|
|
33
34
|
.sort((a, b) => a.role.localeCompare(b.role));
|
|
34
35
|
}
|
|
36
|
+
/** A doctor row's `not checked` outcome when tumwater.json is unreadable: the row blanks its
|
|
37
|
+
* verdict rather than claim a state it cannot see (checkInit owns that error). Every check that
|
|
38
|
+
* reads the config returns this same outcome when the file is unreadable. */
|
|
39
|
+
function uncheckedConfigOutcome() {
|
|
40
|
+
return okOutcome("not checked — tumwater.json unreadable");
|
|
41
|
+
}
|
|
35
42
|
/** The `pause` check: names a standing operator pause so doctor does not print "ready to run"
|
|
36
43
|
* over a team that starts no run. Read-only: it reads the same team and per-role pause
|
|
37
44
|
* markers the scheduler reads (standingTeamPause, pausedRoles, pausedRolesUntil — all treat
|
|
@@ -95,14 +102,11 @@ export function checkQuietHours(config, now = new Date()) {
|
|
|
95
102
|
* per-role caps reached alone are ok because the operator chose them by name. */
|
|
96
103
|
export function checkBudget(root, config, published, now = Date.now()) {
|
|
97
104
|
if (config === undefined)
|
|
98
|
-
return
|
|
105
|
+
return uncheckedConfigOutcome();
|
|
99
106
|
const states = loadRunnerStates(root, config);
|
|
100
107
|
const spent = published?.spentUsd ?? teamDailyCost(states, now);
|
|
101
108
|
const caps = config.maxDailyCostUsdPerRole;
|
|
102
|
-
const capped = [...roleCapVerdicts(states.map((s) => ({ role: s.role, state: s })), caps, now)]
|
|
103
|
-
.filter(([, v]) => v.over)
|
|
104
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
105
|
-
.map(([base, v]) => `${base} (${budgetPhrase(v.spentUsd, caps?.[base])})`);
|
|
109
|
+
const capped = sortEntriesByKey([...roleCapVerdicts(states.map((s) => ({ role: s.role, state: s })), caps, now)].filter(([, v]) => v.over)).map(([base, v]) => `${base} (${budgetPhrase(v.spentUsd, caps?.[base])})`);
|
|
106
110
|
const cappedPhrase = capped.length > 0 ? `; capped roles: ${capped.join(", ")}` : "";
|
|
107
111
|
const cap = config.maxDailyCostUsd;
|
|
108
112
|
if (!(cap > 0))
|
|
@@ -124,7 +128,7 @@ export function checkBudget(root, config, published, now = Date.now()) {
|
|
|
124
128
|
* because it stops a whole tier. */
|
|
125
129
|
export function checkMaintenanceQuota(root, config, aliveTeam, now = Date.now()) {
|
|
126
130
|
if (config === undefined)
|
|
127
|
-
return
|
|
131
|
+
return uncheckedConfigOutcome();
|
|
128
132
|
const runners = loadRunnerStates(root, config).map((s) => ({ role: s.role, running: aliveTeam && s.running === true }));
|
|
129
133
|
const queued = aliveTeam ? new Set(readMergeQueue(root).entries.map((e) => e.role)) : new Set();
|
|
130
134
|
const { allowance, used, held } = maintenanceQuotaForPoll(root, config.maintenancePerWorkMerge, runners, queued, now);
|
|
@@ -201,9 +205,7 @@ export function checkProviderHolds(root, now = Date.now()) {
|
|
|
201
205
|
standing.delete(typeof ev.provider === "string" ? ev.provider : undefined);
|
|
202
206
|
}
|
|
203
207
|
}
|
|
204
|
-
const held = [...standing.entries()]
|
|
205
|
-
.filter(([, h]) => h.end > now)
|
|
206
|
-
.sort(([a], [b]) => (a ?? "").localeCompare(b ?? ""));
|
|
208
|
+
const held = sortEntriesByKey([...standing.entries()].filter(([, h]) => h.end > now));
|
|
207
209
|
if (held.length === 0)
|
|
208
210
|
return okOutcome("no provider holds");
|
|
209
211
|
const lines = held.map(([provider, h]) => {
|
|
@@ -221,9 +223,9 @@ export function checkProviderHolds(root, now = Date.now()) {
|
|
|
221
223
|
* fail: a failing role does not stop the team from running. */
|
|
222
224
|
export function checkErrorStreaks(root, config) {
|
|
223
225
|
if (config === undefined)
|
|
224
|
-
return
|
|
226
|
+
return uncheckedConfigOutcome();
|
|
225
227
|
const failing = loadRunnerStates(root, config)
|
|
226
|
-
.filter((s) => (s
|
|
228
|
+
.filter((s) => errorStreakAtWarn(s));
|
|
227
229
|
if (failing.length === 0)
|
|
228
230
|
return okOutcome("no role failing repeatedly");
|
|
229
231
|
const paused = new Set(pausedRoles(root));
|
|
@@ -243,16 +245,16 @@ export function checkErrorStreaks(root, config) {
|
|
|
243
245
|
});
|
|
244
246
|
return warnOutcome(`failing run after run: ${phrases.join("; ")}`);
|
|
245
247
|
}
|
|
246
|
-
/** The `main red` check:
|
|
247
|
-
*
|
|
248
|
-
* RED_MAIN_WARN_MS even with no role held (the
|
|
249
|
-
* asks heldByMainRed, the predicate the status phase's `main red`
|
|
250
|
-
* cannot disagree on the held
|
|
251
|
-
* can appear in one row. A warn, not a fail: a red main does not stop
|
|
252
|
-
* `now` is injected so a test can pin a red's age. */
|
|
248
|
+
/** 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 parts 3d/3–3f/3
|
|
250
|
+
* land — and warns when main has been red over RED_MAIN_WARN_MS even with no role held (the
|
|
251
|
+
* guardrail, G1). Read-only: it asks heldByMainRed, the predicate the status phase's `main red`
|
|
252
|
+
* label reads, so the two cannot disagree on the held runs, and redMainWarning over the event
|
|
253
|
+
* trail, so both phrases can appear in one row. A warn, not a fail: a red main does not stop
|
|
254
|
+
* the team from running. `now` is injected so a test can pin a red's age. */
|
|
253
255
|
export function checkMainRed(root, config, now = Date.now()) {
|
|
254
256
|
if (config === undefined)
|
|
255
|
-
return
|
|
257
|
+
return uncheckedConfigOutcome();
|
|
256
258
|
const mainCheck = mainCheckForPoll(root, config);
|
|
257
259
|
const ids = runnerIds(config);
|
|
258
260
|
const held = loadRunnerStates(root, config)
|
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
import { childEnv } from "../process/process.js";
|
|
2
2
|
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./probe.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { cacheReadUnpriced, piModelsPath, piPairFree, readPiProviders } from "../pi/pi-models.js";
|
|
7
|
-
import { MODEL_TIERS } from "../config/config-schema.js";
|
|
8
|
-
import { imageReroute, seamImageInput } from "../agent/image-input.js";
|
|
9
|
-
import { modelLabel, usesPiModels } from "../agent/backend-traits.js";
|
|
10
|
-
import { backendById } from "../agent/agent-backend.js";
|
|
11
|
-
import { imageProbeEntryFresh, imageProbeKey, readImageProbeCache, runImageProbe, writeImageProbeCache, } from "./image-probe.js";
|
|
12
|
-
import { readClaudeAccount } from "../agent/claude/claude-account.js";
|
|
3
|
+
import { defaultConfig, loadConfigSafe } from "../config/config.js";
|
|
4
|
+
import { seamsInUse } from "../config/config-fallbacks.js";
|
|
5
|
+
import { cacheReadUnpriced, piModelsPath, readPiProviders } from "../pi/pi-models.js";
|
|
13
6
|
import { envPath } from "../files/files.js";
|
|
14
7
|
import { piListedModels, piProviderAuth } from "./doctor-pi-checks.js";
|
|
15
|
-
import { readPiListedModels } from "../pi/pi-listed-models.js";
|
|
16
8
|
import { agree } from "../text/phrases.js";
|
|
17
|
-
import {
|
|
18
|
-
import { errorMessage, joinParts } from "../text/text.js";
|
|
9
|
+
import { joinParts } from "../text/text.js";
|
|
19
10
|
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
20
11
|
import { belowNodeFloor } from "../version.js";
|
|
21
12
|
/** The one phrase a configured-models check uses when the config declares no models at all:
|
|
22
13
|
* informational (capability falls to pi's own defaults), not a warning. Shared by
|
|
23
|
-
* checkTierModels
|
|
24
|
-
* stance
|
|
14
|
+
* checkTierModels so the check words the same "nothing configured"
|
|
15
|
+
* stance. */
|
|
25
16
|
const PHRASE_NO_MODELS_DECLARED = "no models declared — every model job uses pi's own default";
|
|
26
17
|
/** The one phrase a configured-models check uses when pi's definitions file could not be read
|
|
27
18
|
* at all: the check cannot answer what it was asked, so it warns instead of guessing. Shared by
|
|
28
|
-
* checkTierModels
|
|
19
|
+
* checkTierModels so the check words the "cannot check" stance
|
|
29
20
|
* identically; each appends its own modelsPath after the shared stem, and checkTierModels
|
|
30
21
|
* also appends its ompNote. */
|
|
31
22
|
const PHRASE_CANNOT_READ_MODELS = "cannot check — could not read pi's model definitions at ";
|
|
@@ -33,7 +24,7 @@ const PHRASE_CANNOT_READ_MODELS = "cannot check — could not read pi's model de
|
|
|
33
24
|
* repo-setup checks stay there, and whose state/disk/build checks live in
|
|
34
25
|
* doctor-project-checks.ts), and split apart again so one theme owns one module: this file holds
|
|
35
26
|
* the shared config guard, the seam framework (seamPairs and the per-backend seam predicates),
|
|
36
|
-
* and the declared-tier
|
|
27
|
+
* and the declared-tier check (checkTierModels). The cap
|
|
37
28
|
* fallback price and fallback-chain checks (checkFallbackModel, checkFallbackChain) live in
|
|
38
29
|
* doctor-fallback-checks.ts, importing this file's loadConfigForCheck just as the per-backend
|
|
39
30
|
* modules do. The claude, opencode, codex and pi provider checks live in their own per-backend
|
|
@@ -277,173 +268,6 @@ export async function checkTierModels(root, modelsPath = piModelsPath(), auth =
|
|
|
277
268
|
return failOutcome(`${base}${ompNote}`);
|
|
278
269
|
return (warns.length > 0 || ompVars.length > 0 ? warnOutcome : okOutcome)(`${base}${ompNote}`);
|
|
279
270
|
}
|
|
280
|
-
/** The doctor's image-input capability check (plans/image-input.md "Doctor", parts 1-2/5):
|
|
281
|
-
* which of the team's seams claim to read images, and whether every text-only enabled role can
|
|
282
|
-
* reroute to one that does. Part 2 adds the live probe — with `probeImages` "missing" (the
|
|
283
|
-
* terminal `tumwater doctor` default) each claimed pi model with no fresh cached verdict is
|
|
284
|
-
* shown a real image and the answer is checked; with "all" (the `--reprobe` flag, the
|
|
285
|
-
* dashboard's Re-probe models button and the TUI Health tab's Ctrl+P) every claimed model is
|
|
286
|
-
* re-probed; with "never" (the TUI Health tab and GUI doctor endpoint on entry and refresh)
|
|
287
|
-
* nothing is spawned and only cached verdicts are read, so a refresh never spends a model call.
|
|
288
|
-
* claude seams are never probed — every Claude model reads images, and a probe would spend
|
|
289
|
-
* subscription window to confirm it.
|
|
290
|
-
*
|
|
291
|
-
* Covers the same seams as `tier models` (seamPairs — the tiers, enabled roles and the
|
|
292
|
-
* reviewer) plus the tiers' fallback pairs (the reroute part picks its target among them, so a
|
|
293
|
-
* text-only fallback would promise a reroute no fallback can serve). A claimed model whose probe
|
|
294
|
-
* failed (fresh verdict) is dropped from the readers and reads false from seamImageInput, so the
|
|
295
|
-
* reroute sweep never sends a text-only role to a model that lost its probe. `providers`,
|
|
296
|
-
* `account`, `pathEnv` and `probe` are injectable so tests need no real pi install or account. */
|
|
297
|
-
export async function checkImageInput(root, modelsPath = piModelsPath(), providers = readPiProviders(modelsPath), account, probeImages = "never", pathEnv = envPath(), probe = runImageProbe) {
|
|
298
|
-
const config = loadConfigForCheck(root);
|
|
299
|
-
if ("level" in config)
|
|
300
|
-
return config;
|
|
301
|
-
// `tier models` already fails on a missing/unknown pair, so this check names only what it can
|
|
302
|
-
// answer; unknown (null) pairs are left to that check. A missing definitions file only blocks
|
|
303
|
-
// this check when a seam actually needs it — an all-claude team reads images without it.
|
|
304
|
-
const accountNow = account === undefined ? readClaudeAccount(root) : account;
|
|
305
|
-
// The free-ness lookup reads the image-input check's one claude account (part 2a/9).
|
|
306
|
-
const accounts = (b) => (b === "claude" ? accountNow : null);
|
|
307
|
-
const listed = readPiListedModels(root);
|
|
308
|
-
const pairs = seamPairs(config);
|
|
309
|
-
const needsDefinitions = pairs.some((p) => usesPiModels(p.agent ?? "pi"));
|
|
310
|
-
if (providers === null && needsDefinitions)
|
|
311
|
-
return warnOutcome(`${PHRASE_CANNOT_READ_MODELS}${modelsPath}`);
|
|
312
|
-
const label = modelLabel;
|
|
313
|
-
// The catalog claims (raw `input`, ignoring the cache): what to probe and report, including a
|
|
314
|
-
// model whose earlier probe failed (it must still be named and re-probed by --reprobe).
|
|
315
|
-
const claimed = new Map();
|
|
316
|
-
for (const pair of pairs) {
|
|
317
|
-
if (pair.model === undefined)
|
|
318
|
-
continue;
|
|
319
|
-
if (seamImageInput(pair, providers, undefined, listed) === true)
|
|
320
|
-
claimed.set(imageProbeKey(pair.agent, pair.provider, pair.model), {
|
|
321
|
-
agent: pair.agent,
|
|
322
|
-
provider: pair.provider,
|
|
323
|
-
model: pair.model,
|
|
324
|
-
thinking: pair.thinking,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
// The tiers' fallback pairs: a reroute target must itself read images to help, and part 4's
|
|
328
|
-
// fallback regime picks among these, so a text-only fallback team is a coverage gap here.
|
|
329
|
-
const resolved = resolveTierFallbacks(config, (p) => piPairFree(providers, p, accounts));
|
|
330
|
-
for (const tier of MODEL_TIERS) {
|
|
331
|
-
const pair = resolved[tier].pair;
|
|
332
|
-
if (pair?.model === undefined)
|
|
333
|
-
continue;
|
|
334
|
-
// The readers (seamImageInput, imageReroute) resolve a pair with no backend to the "pi"
|
|
335
|
-
// agent, so the cache key must carry "pi" — keying it `undefined` would write a verdict the
|
|
336
|
-
// readers never read.
|
|
337
|
-
const agent = pair.backend ?? "pi";
|
|
338
|
-
if (seamImageInput({ ...pair, agent }, providers, undefined, listed) === true)
|
|
339
|
-
claimed.set(imageProbeKey(agent, pair.provider, pair.model), {
|
|
340
|
-
agent,
|
|
341
|
-
provider: pair.provider,
|
|
342
|
-
model: pair.model,
|
|
343
|
-
thinking: pair.thinking,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
const probes = readImageProbeCache(root);
|
|
347
|
-
const now = Date.now();
|
|
348
|
-
const env = childEnv({ PATH: pathEnv });
|
|
349
|
-
const readsItems = [];
|
|
350
|
-
const failed = [];
|
|
351
|
-
const couldNot = [];
|
|
352
|
-
let probedAny = false;
|
|
353
|
-
// Settle one claimed seam: probe when asked and uncached, read a fresh cached verdict
|
|
354
|
-
// otherwise, or report "not verified yet". Only pass/fail are cached; could-not-verify is
|
|
355
|
-
// reported but never cached.
|
|
356
|
-
for (const seam of claimed.values()) {
|
|
357
|
-
const agent = seam.agent;
|
|
358
|
-
// A backend that registers an imageInput hook trusts the hook (claude today, which reads
|
|
359
|
-
// every model); a backend without one is probed through its model catalog.
|
|
360
|
-
if (agent !== undefined && backendById(agent)?.imageInput !== undefined) {
|
|
361
|
-
readsItems.push(label(agent, seam.provider, seam.model));
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
const key = imageProbeKey(agent, seam.provider, seam.model);
|
|
365
|
-
const cached = probes[key];
|
|
366
|
-
const valid = imageProbeEntryFresh(cached, now);
|
|
367
|
-
const wantProbe = probeImages !== "never" && (probeImages === "all" || !valid);
|
|
368
|
-
if (wantProbe) {
|
|
369
|
-
probedAny = true;
|
|
370
|
-
const result = await probe(config, { provider: seam.provider, model: seam.model, thinking: seam.thinking }, env);
|
|
371
|
-
if (result.verdict === "pass") {
|
|
372
|
-
probes[key] = { verdict: "pass", detail: result.detail, at: now, claim: true };
|
|
373
|
-
readsItems.push(`${label(agent, seam.provider, seam.model)} (verified ${formatDate(new Date(now))})`);
|
|
374
|
-
}
|
|
375
|
-
else if (result.verdict === "fail") {
|
|
376
|
-
probes[key] = { verdict: "fail", detail: result.detail, at: now, claim: true };
|
|
377
|
-
failed.push(`${label(agent, seam.provider, seam.model)} failed the image probe — ${result.detail}`);
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
couldNot.push(`${label(agent, seam.provider, seam.model)} could not be verified — ${result.detail}`);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
else if (cached && imageProbeEntryFresh(cached, now)) {
|
|
384
|
-
if (cached.verdict === "pass") {
|
|
385
|
-
readsItems.push(`${label(agent, seam.provider, seam.model)} (verified ${formatDate(new Date(cached.at))})`);
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
failed.push(`${label(agent, seam.provider, seam.model)} failed the image probe — ${cached.detail}`);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
else {
|
|
392
|
-
readsItems.push(`${label(agent, seam.provider, seam.model)} (not verified yet — run \`tumwater doctor\`)`);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
let saveProblem = null;
|
|
396
|
-
if (probedAny) {
|
|
397
|
-
try {
|
|
398
|
-
writeImageProbeCache(root, probes);
|
|
399
|
-
}
|
|
400
|
-
catch (err) {
|
|
401
|
-
saveProblem = errorMessage(err);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
const verdict = () => {
|
|
405
|
-
// Text-only enabled roles: which can reroute to a tier that reads images (honoring failed
|
|
406
|
-
// probes), and which cannot.
|
|
407
|
-
const reroutes = [];
|
|
408
|
-
const noTarget = [];
|
|
409
|
-
for (const role of enabledRoleIds(config)) {
|
|
410
|
-
const rc = configForRole(config, role);
|
|
411
|
-
if (rc.model === undefined)
|
|
412
|
-
continue;
|
|
413
|
-
if (seamImageInput(rc, providers, probes, listed) !== false)
|
|
414
|
-
continue; // reads images, or unknown (to tier models)
|
|
415
|
-
const rr = imageReroute(config, role, "primary", modelsPath, accountNow, probes, null, listed);
|
|
416
|
-
if (rr && seamImageInput(rr.config, providers, probes, listed) === true)
|
|
417
|
-
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)})`);
|
|
418
|
-
else
|
|
419
|
-
noTarget.push(`${role} text-only (${label(rc.agent, rc.provider, rc.model)})`);
|
|
420
|
-
}
|
|
421
|
-
// A team that declared no models at all leaves capability to pi's own defaults — the same
|
|
422
|
-
// "nothing configured" stance as tier models, informational rather than a warning.
|
|
423
|
-
const declared = pairs.some((p) => p.model !== undefined) ||
|
|
424
|
-
MODEL_TIERS.some((t) => resolved[t].pair?.model !== undefined);
|
|
425
|
-
if (!declared)
|
|
426
|
-
return okOutcome(PHRASE_NO_MODELS_DECLARED);
|
|
427
|
-
const readsLine = readsItems.length > 0 ? `reads images: ${[...readsItems].sort().join(", ")}` : "";
|
|
428
|
-
const details = joinParts([readsLine, ...reroutes]);
|
|
429
|
-
if (failed.length > 0)
|
|
430
|
-
return failOutcome(`${failed.join("; ")} — remove "image" from its input in pi's models.json or fix the endpoint, then run \`tumwater doctor --reprobe\``);
|
|
431
|
-
if (couldNot.length > 0)
|
|
432
|
-
return warnOutcome(`${couldNot.join("; ")}; ${details}`.replace(/;\s*$/, ""));
|
|
433
|
-
if (noTarget.length > 0)
|
|
434
|
-
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`);
|
|
435
|
-
if (readsItems.length === 0)
|
|
436
|
-
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");
|
|
437
|
-
return okOutcome(details);
|
|
438
|
-
};
|
|
439
|
-
const outcome = verdict();
|
|
440
|
-
if (saveProblem === null)
|
|
441
|
-
return outcome;
|
|
442
|
-
return {
|
|
443
|
-
level: outcome.level === "ok" ? "warn" : outcome.level,
|
|
444
|
-
detail: `${outcome.detail} — could not save the image-probe cache (${saveProblem}), so the next doctor probes again`,
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
271
|
/** `gh` readiness for the opt-in `feedback` observer (plans/feedback-role.md "Shape" → "Part 1"):
|
|
448
272
|
* the role files issues as the operator's GitHub login through `gh`, so doctor confirms `gh`
|
|
449
273
|
* resolves and `gh auth status` exits 0 before a run tries to file — the codex-login pattern
|
|
@@ -6,11 +6,8 @@ import { errorMessage, squash } from "../text/text.js";
|
|
|
6
6
|
import { okOutcome, warnOutcome } from "./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
|
-
* up, short enough that a page that did not go through is still the operator's business.
|
|
10
|
-
|
|
11
|
-
* threshold and the same isNotifyFailure window, so it cannot drift from the doctor's check
|
|
12
|
-
* — the STALE_BUG_DAYS precedent. */
|
|
13
|
-
export const NOTIFY_CHECK_WINDOW_MS = DAY_MS;
|
|
9
|
+
* up, short enough that a page that did not go through is still the operator's business. */
|
|
10
|
+
const NOTIFY_CHECK_WINDOW_MS = DAY_MS;
|
|
14
11
|
/** The `notify` check: whether a notify command is configured, and a warning when it failed to
|
|
15
12
|
* page within the last day (a nonzero exit, a kill, or a failed start — the warnings
|
|
16
13
|
* spawnNotifyCommand logs). Warns but never fails, like the hold rows: a dead webhook must not
|