tumwater 0.4.1 → 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/backend-traits.js +14 -0
- package/dist/src/agent/claude/claude-stream.js +27 -31
- package/dist/src/agent/codex/codex-rollout.js +38 -24
- package/dist/src/agent/codex/codex-stream.js +71 -38
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +4 -4
- package/dist/src/agent/run-agent.js +11 -2
- package/dist/src/agent/run-settle.js +22 -13
- package/dist/src/agent/run-spawn.js +3 -12
- package/dist/src/agent/run-stream.js +2 -10
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/backlog/backlog-archive.js +64 -165
- package/dist/src/backlog/backlog-eligibility.js +50 -58
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +2 -2
- package/dist/src/backlog/backlog-md.js +38 -27
- package/dist/src/backlog/backlog-release.js +11 -19
- package/dist/src/backlog/backlog-structure.js +14 -1
- package/dist/src/backlog/backlog-write.js +1 -1
- package/dist/src/backlog/backlog.js +5 -4
- package/dist/src/backlog/question-answer.js +30 -14
- package/dist/src/baseline/main-baseline.js +30 -20
- package/dist/src/baseline/main-guardrails.js +54 -17
- package/dist/src/baseline/main-watch.js +1 -1
- package/dist/src/budget/budget-wiring.js +0 -5
- package/dist/src/budget/budget.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-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +1 -4
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +27 -50
- package/dist/src/cli/cli-start.js +62 -12
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +6 -7
- package/dist/src/cli/log-commands.js +3 -1
- package/dist/src/cli.js +3 -7
- package/dist/src/collections.js +21 -7
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +19 -26
- package/dist/src/doctor/doctor-backlog-bugs.js +17 -115
- package/dist/src/doctor/doctor-backlog-plans.js +8 -8
- package/dist/src/doctor/doctor-backlog.js +22 -36
- package/dist/src/doctor/doctor-check-names.js +2 -2
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-contention-checks.js +1 -1
- package/dist/src/doctor/doctor-holds.js +13 -11
- 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 +85 -48
- package/dist/src/doctor/doctor-project-checks.js +66 -17
- package/dist/src/doctor/doctor-role-notes.js +6 -24
- package/dist/src/doctor/doctor.js +6 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +5 -15
- 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/failure/failure-state-change.js +183 -68
- package/dist/src/failure/time-spend.js +1 -1
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/git/slot-salvage.js +5 -4
- package/dist/src/gui/gui-admin-endpoints.js +2 -3
- package/dist/src/gui/gui-backlog-commands.js +1 -1
- package/dist/src/gui/gui-backlog-endpoints.js +1 -7
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-operator-commands.js +0 -12
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +2 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/history/history.js +102 -67
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/prompt-commands.js +24 -19
- package/dist/src/init/init.js +68 -15
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-post-review-drain.js +1 -1
- package/dist/src/operator/operator-commands.js +68 -47
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +0 -6
- package/dist/src/pi/pi-watchdogs.js +20 -1
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/process/process.js +23 -0
- package/dist/src/process/supervisor.js +17 -1
- package/dist/src/prompt/gate-notes.js +9 -9
- package/dist/src/prompt/notes-backlog.js +11 -24
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +4 -5
- package/dist/src/report/report-data.js +10 -225
- package/dist/src/report/report-fold.js +20 -295
- package/dist/src/report/report-main-red.js +45 -42
- package/dist/src/report/report-render.js +66 -39
- 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 +3 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +9 -9
- package/dist/src/review/quarantine-render.js +2 -1
- package/dist/src/roles/role-catalog.js +18 -30
- package/dist/src/roles/role-guidance.js +18 -43
- package/dist/src/roles/role-view.js +1 -2
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-detail-data.js +23 -7
- package/dist/src/run/run-detail-render.js +6 -0
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-prompt.js +100 -56
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +4 -21
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +8 -73
- package/dist/src/run/stage-backlog-checks.js +2 -29
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-run.js +2 -79
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +3 -10
- package/dist/src/status/status-data.js +5 -9
- package/dist/src/status/status-role-notes.js +18 -14
- package/dist/src/status/status-roles.js +2 -35
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +7 -212
- package/dist/src/ui/badges.js +2 -3
- package/dist/src/ui/gui/gui-client-boot.js +19 -4
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-report-chart.js +10 -8
- package/dist/src/ui/gui/gui-client-report.js +1 -2
- package/dist/src/ui/gui/gui-client-settings.js +0 -23
- package/dist/src/ui/gui/gui-client-team.js +26 -0
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/status-model.js +2 -2
- package/dist/src/ui/status-payload.js +3 -3
- package/dist/src/ui/status-render.js +3 -3
- package/dist/src/ui/team-alert-backlog.js +23 -54
- package/dist/src/ui/team-alert-shared.js +23 -10
- package/dist/src/ui/team-alert-status.js +8 -3
- package/dist/src/ui/team-alert-tail.js +1 -14
- package/dist/src/ui/team-alerts.js +6 -17
- package/dist/src/ui/transcript-tail.js +137 -94
- 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 +105 -93
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- 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-pause.js +209 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +44 -34
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +121 -108
- package/dist/src/ui/tui/tui-editor-release.js +50 -42
- 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 +58 -46
- package/dist/src/ui/tui/tui-editor-role-key.js +49 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-frame.js +1 -1
- package/dist/src/ui/tui/tui-keys.js +1 -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-health.js +3 -24
- package/dist/src/ui/tui/tui-pane-history.js +20 -24
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +3 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +151 -128
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +2 -3
- package/dist/src/ui/tui/tui.js +3 -3
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +30 -6
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- package/dist/src/ui/team-alert-backlog-feed.js +0 -181
- package/dist/src/ui/team-alert-merge.js +0 -183
|
@@ -309,24 +309,24 @@ export function checkBacklogHeadings(root) {
|
|
|
309
309
|
`later entries are invisible; keep one heading per name (the gate blocks only a merge ` +
|
|
310
310
|
`that adds another copy — remove the duplicate in a deliberate edit)`);
|
|
311
311
|
}
|
|
312
|
-
/** Stalled entries — plans or bugs held by a **Needs review …**
|
|
312
|
+
/** Stalled entries — plans or bugs held by a **Needs review …** note,
|
|
313
313
|
* parked until the plan role splits or replans them, plus the held-for-operator entries
|
|
314
314
|
* parked until the operator releases them. No role takes a held entry (the claim gate refuses
|
|
315
315
|
* one) and nothing clears the note on its own, so the state is visible only as the
|
|
316
|
-
* `[needs review]
|
|
316
|
+
* `[needs review]` marks in the backlog listings; this names them so an
|
|
317
317
|
* operator gets a single-line health signal from `tumwater doctor`. Reads the tree at `root`
|
|
318
318
|
* — the primary checkout IS main's tree — like checkRefusedEntries, which is also where
|
|
319
319
|
* refusal notes (a different hold) are surfaced. entryHold (src/backlog/backlog-eligibility.ts)
|
|
320
320
|
* already classifies the notes, so this check adds no parsing: only the entries whose hold is
|
|
321
|
-
* exactly needs-review
|
|
321
|
+
* exactly needs-review or held-for-operator are collected, from both PLANS.md
|
|
322
322
|
* and BUGS.md. A warn, never a fail — the checkRefusedEntries precedent: a parked entry is
|
|
323
|
-
* operator signal, not a broken environment; the plan role resolves the first
|
|
323
|
+
* operator signal, not a broken environment; the plan role resolves the first itself, and
|
|
324
324
|
* a held-for-operator entry is the operator's to release (`tumwater plan|bug --release`). */
|
|
325
325
|
export function checkStalledEntries(root) {
|
|
326
326
|
const planned = plannedPlanEntries(root);
|
|
327
327
|
const collect = (entry, file) => {
|
|
328
328
|
const hold = entryHold(entry, planned);
|
|
329
|
-
return hold === "needs-review" || hold === "
|
|
329
|
+
return hold === "needs-review" || hold === "held-for-operator" // vocabulary-ok: the held-for-operator hold's key
|
|
330
330
|
? { file, entry }
|
|
331
331
|
: null;
|
|
332
332
|
};
|
|
@@ -336,16 +336,16 @@ export function checkStalledEntries(root) {
|
|
|
336
336
|
].filter((x) => x !== null);
|
|
337
337
|
const [first, ...rest] = stalled;
|
|
338
338
|
if (!first)
|
|
339
|
-
return okOutcome("no planned plan or open bug is held by a Needs-review
|
|
339
|
+
return okOutcome("no planned plan or open bug is held by a Needs-review or held-for-operator note"); // vocabulary-ok: names the held-for-operator hold
|
|
340
340
|
const more = andMore(rest, (s) => quoteHeading(s.entry.title));
|
|
341
341
|
const kind = entryKindLabel(first.file); // "Bug" | "Plan"
|
|
342
342
|
const head = `${first.file === "bugs" ? "BUGS.md" : "PLANS.md"} ${kind.toLowerCase()}`;
|
|
343
343
|
const hold = entryHold(first.entry, planned);
|
|
344
|
-
const note = (first.entry.body.split(/\n/).find((line) => line.trimStart().startsWith("**Needs review ")
|
|
344
|
+
const note = (first.entry.body.split(/\n/).find((line) => line.trimStart().startsWith("**Needs review ")) ?? "").replace(/^\s*\*\*/, "").replace("**", "");
|
|
345
345
|
if (hold === "held-for-operator") { // vocabulary-ok: the held-for-operator hold's key
|
|
346
346
|
return warnOutcome(`${head} ${quoteHeading(first.entry.title)} is held for the operator ${heldCount(first.entry.body)} times${more} — release it with tumwater ${kind.toLowerCase()} --release`); // vocabulary-ok: a held-for-operator entry is the operator's to release
|
|
347
347
|
}
|
|
348
|
-
return warnOutcome(`${head} ${quoteHeading(first.entry.title)} is held by a
|
|
348
|
+
return warnOutcome(`${head} ${quoteHeading(first.entry.title)} is held by a Needs review note ` +
|
|
349
349
|
`(${truncate(note, REFUSAL_NOTE_MAX)})${more} — the plan role resolves it by splitting or replanning`);
|
|
350
350
|
}
|
|
351
351
|
/** Plan dependencies — mistakes in the planned plans' `requires … landed` clauses that the claim
|
|
@@ -50,13 +50,6 @@ export const STALE_QUESTION_DAYS = 7;
|
|
|
50
50
|
* opinionated default with no knob, like STALE_QUESTION_DAYS. Three days is long enough that a
|
|
51
51
|
* bug filed late one evening and fixed the next day never warns. */
|
|
52
52
|
export const STALE_BUG_DAYS = 3;
|
|
53
|
-
/** The whole days a bug may stay held by a Waiting for the next failure note before the
|
|
54
|
-
* waiting-bugs check warns that its flake has not come back. A note parks a bug until it fails
|
|
55
|
-
* again, but the failure may never come, so a hold has to lapse or the entry parks indefinitely;
|
|
56
|
-
* one week without a recurrence is long enough that a recurring flake would almost surely have
|
|
57
|
-
* shown up, and short enough that a bug which never returns stops squatting in the backlog as
|
|
58
|
-
* an ok. An opinionated default with no knob, like STALE_BUG_DAYS. */
|
|
59
|
-
export const STALE_WAIT_DAYS = 7;
|
|
60
53
|
/** The `(and N more …)` suffix three one-line doctor details append after naming their first
|
|
61
54
|
* suspect (fix claims, stranded plans, duplicated headings): the rest stay visible but
|
|
62
55
|
* shortened, since a doctor check renders one line. `render` makes each extra item's short
|
|
@@ -100,7 +93,7 @@ export function readDocChecked(p, file, missingDetail) {
|
|
|
100
93
|
}
|
|
101
94
|
/** The standard single-file check's read prologue for a backlog document at `root`: the
|
|
102
95
|
* readDocChecked call every per-file doctor check (checkFixClaims, checkValidationGaps,
|
|
103
|
-
* checkStrandedPlans, checkStaleQuestions, and the stale
|
|
96
|
+
* checkStrandedPlans, checkStaleQuestions, and the stale bug checks) repeats, with the
|
|
104
97
|
* `no <file> — nothing to verify` missing-detail derived from the file name so the file triple
|
|
105
98
|
* (path, display name, message) and the message template cannot drift across the seven call
|
|
106
99
|
* sites. Returns the document, or the outcome the caller should return as-is. */
|
|
@@ -111,11 +104,9 @@ export function readBacklogChecked(root, file) {
|
|
|
111
104
|
* warns: 6 hours, fixed. A verify plus a merge completes in minutes (plans/merge-queue.md), so a
|
|
112
105
|
* full six-hour silence while the team is healthy — not on a hold, which doctor already reports
|
|
113
106
|
* separately — is never a merely busy pipeline: it reads as a drain that has stalled. The one
|
|
114
|
-
* opinionated default, like STALE_QUESTION_DAYS, so no knob
|
|
115
|
-
* alert (src/ui/alert-feeds.ts staleMerges) reuses this same window through
|
|
116
|
-
* staleQueuedMergeRows, so the doctor's row and the alert cannot drift on the threshold (the
|
|
107
|
+
* opinionated default, like STALE_QUESTION_DAYS, so no knob (the dead-alert feed is gone; the
|
|
117
108
|
* STALE_PROMPT_MS/STALE_BUG_DAYS precedent). */
|
|
118
|
-
|
|
109
|
+
const MERGE_QUEUE_STALE_MS = 6 * HOUR_MS;
|
|
119
110
|
/** The window of the merge-wait phrase on the stale merge-queue row. */
|
|
120
111
|
const MERGE_WAIT_WINDOW_MS = DAY_MS;
|
|
121
112
|
/** Stale queued merges — a change that has sat in the durable merge queue too long without starting
|
|
@@ -151,15 +142,14 @@ export function checkStaleQueuedMerges(root, now) {
|
|
|
151
142
|
return withWait(warnOutcome(`merge-queue entry of ${roleLabel(first.role)} — ${quoteHeading(first.summary)} — has waited ` +
|
|
152
143
|
`${first.age} to start${more}; a queued change the pipeline neither approves nor flags reads as healthy, so the merge queue has stopped moving`));
|
|
153
144
|
}
|
|
154
|
-
/** The one traversal
|
|
155
|
-
* (checkStaleQueuedMerges)
|
|
156
|
-
* (src/ui/alert-feeds.ts staleMerges): every queued merge whose role is not in the in-flight
|
|
145
|
+
/** The one traversal for the doctor's `stale merge-queue entries` row
|
|
146
|
+
* (checkStaleQueuedMerges): every queued merge whose role is not in the in-flight
|
|
157
147
|
* marker and whose wait strictly exceeds MERGE_QUEUE_STALE_MS (6 h), in queue-file order (oldest
|
|
158
148
|
* first). The in-flight set comes from readMergeMarker via mergeChanges (an absent or
|
|
159
149
|
* never-throwing unreadable marker contributes the empty set) — a slow review is not a stalled
|
|
160
|
-
* drain. Each row carries { role, summary, waitedMs }
|
|
161
|
-
*
|
|
162
|
-
|
|
150
|
+
* drain. Each row carries { role, summary, waitedMs }, which the row renders with its age
|
|
151
|
+
* phrase, so the rule and its threshold live in one place and cannot drift. */
|
|
152
|
+
function staleQueuedMergeRows(root, now) {
|
|
163
153
|
const marker = readMergeMarker(root);
|
|
164
154
|
const inFlight = new Set(marker ? mergeChanges(marker).map((c) => c.role) : []);
|
|
165
155
|
return queuedMerges(root)
|
|
@@ -167,9 +157,8 @@ export function staleQueuedMergeRows(root, now) {
|
|
|
167
157
|
.map((e) => ({ role: e.role, summary: e.summary, waitedMs: now - e.enqueuedAt }));
|
|
168
158
|
}
|
|
169
159
|
/** The merge-queue files `readMergeQueue` skips — torn, foreign, or vanished mid-listing — as
|
|
170
|
-
* checkout-relative paths.
|
|
171
|
-
* the
|
|
172
|
-
* the two surfaces select the same files and name the same paths and cannot drift. */
|
|
160
|
+
* checkout-relative paths. The one traversal behind the doctor's `checkMergeQueueFiles` row, so
|
|
161
|
+
* the row names the same paths the reader actually skips and cannot drift. */
|
|
173
162
|
export function skippedMergeQueueFiles(root) {
|
|
174
163
|
return unreadableMergeQueueFiles(root).map((f) => path.relative(root, f));
|
|
175
164
|
}
|
|
@@ -203,9 +192,8 @@ export const POST_REVIEW_AT_RISK_FAILURES = POST_REVIEW_FAILURE_LIMIT - 1;
|
|
|
203
192
|
* is not waiting on a verdict and must not read as one failed run from being dropped.
|
|
204
193
|
* A review that has already landed its fix (`fixed` set) is left out too — it is a settled
|
|
205
194
|
* record naming both shas, not a verdict-less entry the next run could drop.
|
|
206
|
-
* One traversal
|
|
207
|
-
*
|
|
208
|
-
* two surfaces select the same entries and cannot drift. Reads .tumwater/state/post-review.json
|
|
195
|
+
* One traversal used by the doctor's `post-merge reviews` row (checkPostReviews), so the row's
|
|
196
|
+
* selection lives in one place. Reads .tumwater/state/post-review.json
|
|
209
197
|
* through readPostReviews, which degrades a missing or torn file to the well-formed subset or
|
|
210
198
|
* `[]` and never throws. */
|
|
211
199
|
export function atRiskPostReviews(root) {
|
|
@@ -215,8 +203,8 @@ export function atRiskPostReviews(root) {
|
|
|
215
203
|
}
|
|
216
204
|
/** The doctor's one sentence naming the post-merge reviews one failed run from being dropped
|
|
217
205
|
* (the checkStaleQueuedMerges phrase precedent): the first in full — role, short commit, summary
|
|
218
|
-
* and the failures count — the rest folded through andMore,
|
|
219
|
-
*
|
|
206
|
+
* and the failures count — the rest folded through andMore, the one sentence the row shows, so
|
|
207
|
+
* the wording lives in one place. null for an empty list. */
|
|
220
208
|
export function atRiskPostReviewsPhrase(rows) {
|
|
221
209
|
const [first, ...rest] = rows;
|
|
222
210
|
if (!first)
|
|
@@ -229,8 +217,8 @@ export function atRiskPostReviewsPhrase(rows) {
|
|
|
229
217
|
* drain drops an entry at POST_REVIEW_FAILURE_LIMIT failed runs) — the last moment an operator
|
|
230
218
|
* can fix the reviewer backend or deliberately accept the commit unreviewed before the drain
|
|
231
219
|
* drops it from the queue and it stays on main unreviewed. Reads .tumwater/state/post-review.json
|
|
232
|
-
* through the shared atRiskPostReviews traversal the
|
|
233
|
-
*
|
|
220
|
+
* through the shared atRiskPostReviews traversal, so the selection and wording live in one
|
|
221
|
+
* place. A warn,
|
|
234
222
|
* never a fail — the checkStaleQueuedMerges precedent: a failing reviewer is operator signal,
|
|
235
223
|
* not a broken environment; readPostReviews never throws (a missing or torn file degrades to the
|
|
236
224
|
* well-formed subset or `[]`), so the check needs no read-error arm. */
|
|
@@ -242,16 +230,14 @@ export function checkPostReviews(root) {
|
|
|
242
230
|
/** How long a queued prompt may wait for its role to dequeue it before the stale-prompt check
|
|
243
231
|
* warns: a day, fixed. An opinionated default with no knob, like MERGE_QUEUE_STALE_MS: a day is
|
|
244
232
|
* long enough that a healthy role's next run would have dequeued the prompt, and short enough
|
|
245
|
-
* that a paused or disabled role's stranded prompt shows up. The
|
|
246
|
-
*
|
|
247
|
-
|
|
248
|
-
export const STALE_PROMPT_MS = DAY_MS;
|
|
233
|
+
* that a paused or disabled role's stranded prompt shows up. The stale-prompt check reads this
|
|
234
|
+
* same threshold, so it cannot drift — the STALE_BUG_DAYS precedent. */
|
|
235
|
+
const STALE_PROMPT_MS = DAY_MS;
|
|
249
236
|
/** The queued inbox prompts the doctor's stale-prompt rule calls stale — deliverable now (a
|
|
250
237
|
* `--at` prompt still counting down is never stale) and queued more than STALE_PROMPT_MS,
|
|
251
238
|
* oldest-first, `queuedAtMs` narrowed to a number. One traversal shared by the doctor's
|
|
252
|
-
* `checkStalePrompts` check
|
|
253
|
-
|
|
254
|
-
export function staleQueuedPromptRows(root, now) {
|
|
239
|
+
* `checkStalePrompts` check, so the rule lives in one place at one threshold. */
|
|
240
|
+
function staleQueuedPromptRows(root, now) {
|
|
255
241
|
return queuedListRows(root, null, listWideScope(root))
|
|
256
242
|
.filter((p) => p.queuedAtMs !== null && deliverableAt(p.notBeforeMs, now) && now - p.queuedAtMs > STALE_PROMPT_MS)
|
|
257
243
|
.sort((a, b) => a.queuedAtMs - b.queuedAtMs)
|
|
@@ -264,7 +250,7 @@ export function staleQueuedPromptRows(root, now) {
|
|
|
264
250
|
* views (`tumwater prompt --list`, the dashboard Inbox tab, the TUI Inbox pane) show its age —
|
|
265
251
|
* and only when the operator looks. This names the oldest due-but-undelivered prompt, its role,
|
|
266
252
|
* and the deliver-or-drop commands. Reads the queue through the shared `staleQueuedPromptRows`
|
|
267
|
-
* traversal
|
|
253
|
+
* traversal (no new reader in src/inbox/), keeping only
|
|
268
254
|
* rows that are deliverable now — a `--at 7d` prompt still counting down is never stale — and
|
|
269
255
|
* stamped more than STALE_PROMPT_MS ago. A warn, never a fail — the checkStaleQueuedMerges
|
|
270
256
|
* precedent: a stranded prompt is operator signal, not a broken environment. */
|
|
@@ -29,7 +29,6 @@ export const DOCTOR_CHECK_NAMES = [
|
|
|
29
29
|
"over-cap model",
|
|
30
30
|
"fallback chain",
|
|
31
31
|
"models for each tier",
|
|
32
|
-
"image input",
|
|
33
32
|
"claude auth",
|
|
34
33
|
"claude api key",
|
|
35
34
|
"claude version",
|
|
@@ -74,11 +73,12 @@ export const DOCTOR_CHECK_NAMES = [
|
|
|
74
73
|
"plan dependencies",
|
|
75
74
|
"stale questions",
|
|
76
75
|
"stale bugs",
|
|
77
|
-
"waiting bugs",
|
|
78
76
|
"regressed fixes",
|
|
79
77
|
"stale merge-queue entries",
|
|
80
78
|
"merge-queue files",
|
|
81
79
|
"post-merge reviews",
|
|
80
|
+
"main diff rules",
|
|
81
|
+
"dropped post-merge reviews",
|
|
82
82
|
"stale prompts",
|
|
83
83
|
"uncommitted backlog",
|
|
84
84
|
"update",
|
|
@@ -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
|
|
@@ -517,7 +517,7 @@ export function providerConcurrencyProblems(config, piDefault) {
|
|
|
517
517
|
const capSet = new Set(capKeys);
|
|
518
518
|
const keyless = seams.filter((s) => seamConcurrencyKey(s, piDefault) === undefined);
|
|
519
519
|
if (capSet.size > 0 && keyless.length > 0) {
|
|
520
|
-
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(", ")}`);
|
|
521
521
|
}
|
|
522
522
|
return warnings;
|
|
523
523
|
}
|
|
@@ -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";
|
|
@@ -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,7 +223,7 @@ 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
228
|
.filter((s) => errorStreakAtWarn(s));
|
|
227
229
|
if (failing.length === 0)
|
|
@@ -252,7 +254,7 @@ export function checkErrorStreaks(root, config) {
|
|
|
252
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
|