tumwater 0.4.5 → 0.4.6
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 +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/backlog/backlog-md.js +32 -17
- package/dist/src/baseline/main-red.js +72 -8
- package/dist/src/baseline/main-watch.js +5 -4
- package/dist/src/cli/cli-args.js +14 -1
- package/dist/src/cli/cli-flag-specs.js +5 -3
- package/dist/src/cli/cli-log-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +21 -9
- package/dist/src/cli/help.js +5 -2
- package/dist/src/cli.js +1 -1
- package/dist/src/config/config-write.js +76 -16
- package/dist/src/doctor/doctor-claude-checks.js +4 -11
- package/dist/src/doctor/doctor-codex-checks.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +9 -1
- package/dist/src/doctor/doctor-project-checks.js +6 -6
- package/dist/src/events/event-format.js +50 -38
- package/dist/src/events/event-labels.js +1 -0
- package/dist/src/failure/failure-render.js +1 -0
- package/dist/src/failure/failure-time-spend.js +1 -0
- package/dist/src/git/slot-salvage.js +6 -0
- package/dist/src/gui/gui-history-endpoints.js +4 -4
- package/dist/src/history/history.js +2 -2
- package/dist/src/{run → history}/run-detail-data.js +2 -2
- package/dist/src/{run → history}/run-detail.js +2 -2
- package/dist/src/inbox/inbox-top.js +2 -1
- package/dist/src/merge/merge-batch.js +9 -0
- package/dist/src/merge/merge-core.js +13 -0
- package/dist/src/merge/merge-drain.js +13 -2
- package/dist/src/merge/merge-pipeline.js +5 -4
- package/dist/src/merge/merge-queue.js +8 -1
- package/dist/src/merge/merge-requeue.js +16 -18
- package/dist/src/merge/merge-to-main.js +4 -0
- package/dist/src/merge/merge-verify.js +13 -2
- package/dist/src/merge/merge-withdrawn.js +71 -0
- package/dist/src/operator/operator-commands.js +2 -2
- package/dist/src/operator/operator-revert.js +1 -1
- package/dist/src/orchestrator/orchestrator.js +63 -15
- package/dist/src/paths.js +19 -3
- package/dist/src/prompt/gate-prompts.js +6 -1
- package/dist/src/prompt/prompt.js +11 -0
- package/dist/src/redeploy/redeployer.js +41 -30
- package/dist/src/report/report.js +2 -2
- package/dist/src/review/known-flakes.js +101 -40
- package/dist/src/review/post-merge-review.js +40 -33
- package/dist/src/review/review-verdict.js +25 -17
- package/dist/src/roles/role-catalog.js +10 -4
- package/dist/src/roles/role-view.js +33 -12
- package/dist/src/run/run-apply.js +3 -2
- package/dist/src/run/run-dismiss.js +2 -2
- package/dist/src/run/run-prompt.js +4 -4
- package/dist/src/run/run-stage.js +7 -1
- package/dist/src/run/run-verdict.js +1 -1
- package/dist/src/runner/runner-leftover.js +48 -1
- package/dist/src/runner/runner-outcome.js +1 -1
- package/dist/src/runner/runner-run.js +11 -2
- package/dist/src/runner/runner-state-load.js +29 -13
- package/dist/src/scheduling/claims.js +98 -19
- package/dist/src/scheduling/scope.js +16 -2
- package/dist/src/{run → stage}/stage-path-checks.js +3 -3
- package/dist/src/status/status-polls.js +1 -1
- package/dist/src/text/phrases.js +2 -2
- package/dist/src/text/text.js +1 -1
- package/dist/src/ui/gui/gui-client-model.js +1 -0
- package/dist/src/ui/gui/gui-client-settings.js +20 -10
- package/dist/src/ui/tone.js +2 -1
- package/dist/src/ui/tui/tui-editor-attach.js +2 -6
- package/dist/src/ui/tui/tui-editor-brief.js +2 -6
- package/dist/src/ui/tui/tui-editor-release.js +2 -6
- package/dist/src/ui/tui/tui-editor-retire.js +2 -6
- package/dist/src/ui/tui/tui-editor-role-enabled.js +2 -6
- package/dist/src/ui/tui/tui-editor-session.js +14 -0
- package/dist/src/ui/tui/tui-guard.js +16 -0
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +4 -14
- package/dist/src/ui/tui/tui-pane-history.js +2 -8
- package/dist/src/ui/tui/tui-pane-pending.js +8 -22
- package/dist/src/ui/tui/tui-pane-trail.js +4 -9
- package/dist/src/ui/tui/tui-pane-transcript.js +2 -7
- package/dist/src/verdict/reply-contract.js +12 -1
- package/package.json +1 -1
- /package/dist/src/{run → history}/run-detail-render.js +0 -0
- /package/dist/src/{run → observer}/ci-parity.js +0 -0
- /package/dist/src/{run → observer}/feedback-filings.js +0 -0
- /package/dist/src/{run → observer}/qa-coverage.js +0 -0
- /package/dist/src/{run → observer}/selfcheck-digest.js +0 -0
- /package/dist/src/{run → stage}/acceptance-greps.js +0 -0
- /package/dist/src/{run → stage}/stage-backlog-checks.js +0 -0
- /package/dist/src/{run → stage}/stage-check.js +0 -0
- /package/dist/src/{run → stage}/stage-checks-shared.js +0 -0
- /package/dist/src/{run → stage}/stage-name-checks.js +0 -0
- /package/dist/src/{run → stage}/stage-prose-checks.js +0 -0
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ and is also outside `npm test`.
|
|
|
42
42
|
## Status
|
|
43
43
|
|
|
44
44
|
<!-- tumwater:status:start -->
|
|
45
|
-
**v0.4.
|
|
45
|
+
**v0.4.6**: working tumwater. The director and 15 of the 17 roles are enabled by default; `selfcheck`,
|
|
46
46
|
which files tumwater bugs from tumwater's own event log, and `feedback`, which files them as GitHub
|
|
47
47
|
issues on znation/tumwater, are opt-in (`roles.selfcheck.enabled`, `roles.feedback.enabled`).
|
|
48
48
|
|
package/dist/build-info.json
CHANGED
|
@@ -52,27 +52,42 @@ export function fenceTracker() {
|
|
|
52
52
|
return fence !== null;
|
|
53
53
|
},
|
|
54
54
|
inside(line) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (fence === null) {
|
|
59
|
-
// CommonMark: an info string for a backtick fence cannot contain a backtick, so a
|
|
60
|
-
// line like ````md / ## Done / ````` quoted in prose is paragraph text, not a fence
|
|
61
|
-
// opener — taken as one, no later bare fence line can close it and the fence runs to
|
|
62
|
-
// EOF, swallowing the rest of the document as fenced content.
|
|
63
|
-
const info = line.replace(/^ {0,3}/, "").slice(marker.length);
|
|
64
|
-
if (marker.startsWith("`") && info.includes("`"))
|
|
65
|
-
return false;
|
|
66
|
-
fence = { char: marker.charAt(0), length: marker.length };
|
|
67
|
-
}
|
|
68
|
-
else if (marker.startsWith(fence.char) && marker.length >= fence.length && line.trim() === marker)
|
|
69
|
-
fence = null;
|
|
70
|
-
return true; // The fence line itself is fence syntax, never structure.
|
|
55
|
+
if (fence === null) {
|
|
56
|
+
fence = fenceOpener(line);
|
|
57
|
+
return fence !== null; // An opener is fence syntax, never structure.
|
|
71
58
|
}
|
|
72
|
-
|
|
59
|
+
if (closesFence(line, fence))
|
|
60
|
+
fence = null;
|
|
61
|
+
return true; // Inside a fence, every line (its closer too) is fence syntax or content.
|
|
73
62
|
},
|
|
74
63
|
};
|
|
75
64
|
}
|
|
65
|
+
/** A CommonMark fence line's leading marker run, or null: ```` ``` ```` or `~~~` (three or more)
|
|
66
|
+
* after at most three spaces of indent. */
|
|
67
|
+
function fenceMarkerRun(line) {
|
|
68
|
+
return /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1] ?? null;
|
|
69
|
+
}
|
|
70
|
+
/** The fence `line` opens, or null when it opens none — CommonMark's opening rule, shared by
|
|
71
|
+
* fenceTracker and review-verdict.ts's quoted-code stripping. An info string may follow the
|
|
72
|
+
* marker, except that a backtick fence's info string may not contain a backtick: a line like
|
|
73
|
+
* ````md / ## Done / ````` quoted in prose is paragraph text. Taken as an opener, no later bare
|
|
74
|
+
* fence line could close it and the fence would run to EOF, swallowing the rest of the document
|
|
75
|
+
* as fenced content. */
|
|
76
|
+
export function fenceOpener(line) {
|
|
77
|
+
const marker = fenceMarkerRun(line);
|
|
78
|
+
if (!marker)
|
|
79
|
+
return null;
|
|
80
|
+
const info = line.replace(/^ {0,3}/, "").slice(marker.length);
|
|
81
|
+
if (marker.startsWith("`") && info.includes("`"))
|
|
82
|
+
return null;
|
|
83
|
+
return { char: marker.charAt(0), length: marker.length };
|
|
84
|
+
}
|
|
85
|
+
/** Whether `line` closes `fence` — CommonMark's closing rule: a bare fence line (no info
|
|
86
|
+
* string) of the same character, at least as long as the opener. */
|
|
87
|
+
export function closesFence(line, fence) {
|
|
88
|
+
const marker = fenceMarkerRun(line);
|
|
89
|
+
return marker !== null && marker.startsWith(fence.char) && marker.length >= fence.length && line.trim() === marker;
|
|
90
|
+
}
|
|
76
91
|
/** The title of a markdown heading line at `prefix` level — or the empty string when the heading
|
|
77
92
|
* carries no text. The prefix's own length drives the slice (never a hard-coded `3`/`4`), so the
|
|
78
93
|
* two heading levels cannot drift. The single home of the heading-title slice, shared by
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
2
|
-
import { enqueueRolePrompt } from "../inbox/inbox.js";
|
|
3
|
+
import { enqueueRolePrompt, queuedRolePromptRecords } from "../inbox/inbox.js";
|
|
4
|
+
import { cancelQueuedFile } from "../inbox/inbox-cancel.js";
|
|
3
5
|
import { liveConfig } from "../config/config.js";
|
|
4
6
|
import { mergeCommandOf, rerunCommandOf } from "../build/build-check-detect.js";
|
|
5
7
|
import { canRerun, rerunCommandLine, rerunTests } from "../build/build-check-rerun.js";
|
|
@@ -37,7 +39,8 @@ let lastMainRedSha = null;
|
|
|
37
39
|
* names, or the red SHA when the red names none. Two red commits with the same failing set send
|
|
38
40
|
* one heal prompt, not one per commit; a green main observed by the ci handoff
|
|
39
41
|
* (`mainRedHealNote`) or the main watch (`clearHealKeyOnGreen`) clears it, so a set that
|
|
40
|
-
* returns after green is healed again.
|
|
42
|
+
* returns after green is healed again. The key is per process; what bounds ci's inbox across
|
|
43
|
+
* restarts is queueHealPrompt's replacement of a waiting heal prompt. */
|
|
41
44
|
let lastHealKey = null;
|
|
42
45
|
/** The failing tests a red verdict names, when the list is complete — non-empty and short of
|
|
43
46
|
* FAILING_TESTS_MAX, which may have been cut short (the same rule as onlyKnownMainFailures).
|
|
@@ -60,7 +63,8 @@ function redMainMessage(red, action) {
|
|
|
60
63
|
* lastMainRedSha) so the ci handoff (`mainRedHealNote`) and mainTipVerdict share one guard:
|
|
61
64
|
* whichever observes the red first logs it, the other stays silent. `foundBy` is the role
|
|
62
65
|
* whose path verified the red — when that role is not ci itself, one prompt is enqueued to
|
|
63
|
-
* ci's own inbox
|
|
66
|
+
* ci's own inbox (replacing a heal prompt already waiting there, queueHealPrompt) so the
|
|
67
|
+
* healer runs on the orchestrator's next poll instead of waiting out
|
|
64
68
|
* its own interval or deferral; ci finding its own red skips the enqueue because its running
|
|
65
69
|
* run already carries the note and IS the heal (and enqueuing from inside it would queue a
|
|
66
70
|
* second run). A queued per-role prompt makes the role due by itself (`isEligible`,
|
|
@@ -95,7 +99,7 @@ function warnMainRedOnce(root, red, foundBy) {
|
|
|
95
99
|
lastHealKey = healKey;
|
|
96
100
|
if (baseRoleOf(foundBy) !== "ci") {
|
|
97
101
|
try {
|
|
98
|
-
|
|
102
|
+
queueHealPrompt(root, mainRedHealPrompt(red.sha));
|
|
99
103
|
}
|
|
100
104
|
catch (err) {
|
|
101
105
|
// Clear the key so a later verification retries the heal. lastMainRedSha stays set, so a
|
|
@@ -113,7 +117,7 @@ function warnMainRedOnce(root, red, foundBy) {
|
|
|
113
117
|
* before the healer consumes the prompt, mainRedHealNote returns undefined (no `<main-red>`
|
|
114
118
|
* note is appended) and the "verify green" clause makes the run a no-op. */
|
|
115
119
|
export function mainRedHealPrompt(sha, detail) {
|
|
116
|
-
const base =
|
|
120
|
+
const base = `${HEAL_PROMPT_OPENING}${shortSha(sha)}. Verify main is green now; if it is still red, reproduce the failure and land the fix.`;
|
|
117
121
|
if (!detail)
|
|
118
122
|
return base;
|
|
119
123
|
const tests = detail.failingTests.length > 0 ? ` Failing: ${detail.failingTests.join("; ")}.` : "";
|
|
@@ -126,6 +130,63 @@ export function mainRedHealPrompt(sha, detail) {
|
|
|
126
130
|
function rerunAdvice(command) {
|
|
127
131
|
return `Run only the failing tests, to reproduce them and again to prove the fix, with: ${command}`;
|
|
128
132
|
}
|
|
133
|
+
/** How every heal prompt opens: mainRedHealPrompt writes it, and queuedHealPrompts recognizes a
|
|
134
|
+
* waiting heal prompt in ci's inbox by it and reads back the SHA that follows. */
|
|
135
|
+
const HEAL_PROMPT_OPENING = "Main was reported red at commit ";
|
|
136
|
+
/** The heal prompts waiting in ci's inbox, oldest first: each queue file's basename and the
|
|
137
|
+
* short SHA it was reported red at. Every other prompt in the inbox (an operator's, a resume's
|
|
138
|
+
* re-queued non-heal request) is left out. An inbox that cannot be listed is warned and reads
|
|
139
|
+
* as holding none. */
|
|
140
|
+
function queuedHealPrompts(root) {
|
|
141
|
+
try {
|
|
142
|
+
return queuedRolePromptRecords(root, "ci").flatMap((e) => {
|
|
143
|
+
if (!e.text.startsWith(HEAL_PROMPT_OPENING))
|
|
144
|
+
return [];
|
|
145
|
+
const sha = /^[0-9a-f]+(?=\.)/.exec(e.text.slice(HEAL_PROMPT_OPENING.length))?.[0];
|
|
146
|
+
return sha ? [{ file: e.file, sha }] : [];
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `could not list ci's inbox for waiting heal prompts: ${errorMessage(err)}`);
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** Remove waiting heal prompts from ci's inbox, each logged as a prompt_cancelled under ci
|
|
155
|
+
* (cancelQueuedFile). Best-effort: a prompt that cannot be removed is warned and stays queued,
|
|
156
|
+
* so the heal just queued or the green just settled never fails on it. */
|
|
157
|
+
function dropHealPrompts(root, files) {
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
try {
|
|
160
|
+
cancelQueuedFile(root, "ci", file);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `could not drop the stale heal prompt ${file} from ci's inbox: ${errorMessage(err)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Queue `prompt` for the ci healer in place of every heal prompt already waiting. A waiting one
|
|
168
|
+
* names an older red, and the healer verifies main as it is now whichever prompt it reads, so
|
|
169
|
+
* one waiting heal prompt is enough: without the replacement each red commit on a long red
|
|
170
|
+
* added one, and a healer that clears one per run fell 69 behind (2026-10-30 to 11-01). The new
|
|
171
|
+
* prompt is written first, so a write that fails throws with the older prompts still queued. */
|
|
172
|
+
function queueHealPrompt(root, prompt) {
|
|
173
|
+
const queued = path.basename(enqueueRolePrompt(root, "ci", prompt));
|
|
174
|
+
dropHealPrompts(root, queuedHealPrompts(root).filter((p) => p.file !== queued).map((p) => p.file));
|
|
175
|
+
}
|
|
176
|
+
/** Drop each heal prompt waiting in ci's inbox whose red commit `greenSha` contains: main has
|
|
177
|
+
* passed its check since that red, so the run the prompt would start could only find main
|
|
178
|
+
* green. A prompt naming a commit `greenSha` does not contain (a red reported on a newer tip)
|
|
179
|
+
* stays queued. `wt` is any checkout of the repository, for the ancestry check. Called beside
|
|
180
|
+
* clearHealKeyOnGreen by the main watch's settled green and the ci hand-off's own green.
|
|
181
|
+
* Never throws. */
|
|
182
|
+
export async function dropHealPromptsCoveredBy(root, wt, greenSha) {
|
|
183
|
+
const covered = [];
|
|
184
|
+
for (const { file, sha } of queuedHealPrompts(root)) {
|
|
185
|
+
if (await isMergedInto(wt, sha, greenSha))
|
|
186
|
+
covered.push(file);
|
|
187
|
+
}
|
|
188
|
+
dropHealPrompts(root, covered);
|
|
189
|
+
}
|
|
129
190
|
/** The command line that re-runs `red`'s named failures on their own (rerunCommandLine), or
|
|
130
191
|
* undefined when they cannot be re-run alone (canRerun) — the healer is then left to the full
|
|
131
192
|
* check. */
|
|
@@ -137,14 +198,15 @@ function healRerunLine(root, config, red) {
|
|
|
137
198
|
/** Queue the heal prompt for ci with the culprit's detail: the one way a named red-main culprit
|
|
138
199
|
* is answered (Forward, never back, part 4/6): the watch's onCulprit hand-off routes through
|
|
139
200
|
* this, and nothing is ever reverted. (The ci gate's own red-main hand-off is the separate
|
|
140
|
-
*
|
|
201
|
+
* queueHealPrompt call in warnMainRedOnce; both replace a heal prompt already waiting.) Never
|
|
202
|
+
* throws: a prompt that cannot be
|
|
141
203
|
* queued (an unwritable inbox) is warned, like that hand-off guard in warnMainRedOnce — so
|
|
142
204
|
* the watch's onCulprit (whose caller discards the result) cannot derail a watch run or end
|
|
143
205
|
* the orchestrator. */
|
|
144
206
|
export function queueCulpritHeal(root, tip, detail) {
|
|
145
207
|
try {
|
|
146
208
|
const rerun = healRerunLine(root, liveConfig(root), { failingTests: [...detail.failingTests], failingTestFiles: detail.failingTestFiles });
|
|
147
|
-
|
|
209
|
+
queueHealPrompt(root, mainRedHealPrompt(tip, { ...detail, ...(rerun ? { rerun } : {}) }));
|
|
148
210
|
}
|
|
149
211
|
catch (err) {
|
|
150
212
|
warnEventBestEffort(root, TUMWATER_SOURCE, `could not queue the heal prompt for ci: ${errorMessage(err)}`);
|
|
@@ -180,8 +242,10 @@ export async function mainRedHealNote(root, role, wt) {
|
|
|
180
242
|
? { baseline: await watchedBaseline(wt) }
|
|
181
243
|
: await checkMainBaseline(wt, config, baselineCheckLogger(root, role));
|
|
182
244
|
const red = baseline.baseline;
|
|
183
|
-
if (red?.status === "green")
|
|
245
|
+
if (red?.status === "green") {
|
|
184
246
|
clearHealKeyOnGreen();
|
|
247
|
+
await dropHealPromptsCoveredBy(root, wt, red.sha);
|
|
248
|
+
}
|
|
185
249
|
if (red?.status !== "red")
|
|
186
250
|
return undefined;
|
|
187
251
|
warnMainRedOnce(root, red, role);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { distinctOrdered } from "../collections.js";
|
|
2
2
|
import { checkMainBaseline, settleRedByRerun } from "./main-baseline.js";
|
|
3
3
|
import { canRerun, rerunTests } from "../build/build-check-rerun.js";
|
|
4
|
-
import { baselineCheckLogger, clearHealKeyOnGreen, withMainTipWorktree } from "./main-red.js";
|
|
4
|
+
import { baselineCheckLogger, clearHealKeyOnGreen, dropHealPromptsCoveredBy, withMainTipWorktree } from "./main-red.js";
|
|
5
5
|
import { CHECK_TIER } from "../concurrency/check-permit.js";
|
|
6
6
|
import { FAILING_TESTS_MAX } from "../build/build-check-report.js";
|
|
7
7
|
import { detectBuildCheck } from "../build/build-check-detect.js";
|
|
@@ -267,11 +267,12 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
267
267
|
// names the whole tip as the culprit, so the two red kinds are judged independently.
|
|
268
268
|
await watchRules(wt, previous, baseline.sha);
|
|
269
269
|
// A settled non-red verdict (a green, or a red naming only quarantined tests) clears the
|
|
270
|
-
// heal-dedup key, so a failing set that returns after a green main is healed again
|
|
271
|
-
//
|
|
272
|
-
// is red.
|
|
270
|
+
// heal-dedup key, so a failing set that returns after a green main is healed again, and
|
|
271
|
+
// drops the heal prompts still waiting for a red this tip has passed. A settled red
|
|
272
|
+
// verdict below does NOT clear it: the heal-dedup key stays set while main is red.
|
|
273
273
|
if (!red) {
|
|
274
274
|
clearHealKeyOnGreen();
|
|
275
|
+
await dropHealPromptsCoveredBy(root, wt, baseline.sha);
|
|
275
276
|
redSpellOpeners.delete(root); // a green closes the red spell; the next red names its own opener
|
|
276
277
|
return;
|
|
277
278
|
}
|
package/dist/src/cli/cli-args.js
CHANGED
|
@@ -398,6 +398,14 @@ export function failOverDurationCap(flag, ms, maxMs, hint) {
|
|
|
398
398
|
export function failRivalShapes(subject, rival, why) {
|
|
399
399
|
fail(`${subject} cannot be combined with ${rival} (${why})`);
|
|
400
400
|
}
|
|
401
|
+
/** The shared why-clause for a trailing-window view's `-f/--follow` rival: follow is an
|
|
402
|
+
* unbounded "from now on" read, while `--since` opens a bounded past window, so the two are
|
|
403
|
+
* mutually exclusive on the same grounds in all three windowed views (logs, history, report).
|
|
404
|
+
* One clause so the three sites cannot drift on what follow means and why it excludes
|
|
405
|
+
* `--since`; the subject keeps the command's own name. */
|
|
406
|
+
export function failFollowSinceRival(command) {
|
|
407
|
+
return failRivalShapes(`${command} --since`, "-f/--follow", 'follow means "from now on"; --since shows a bounded past window');
|
|
408
|
+
}
|
|
401
409
|
/** Guard a trailing-window command's `--until <window>` against its `--since <window>` pair —
|
|
402
410
|
* the shared fail sentences the three windowed views (logs, history, report) print, so the
|
|
403
411
|
* wording cannot drift per command. When `--until` is present but `--since` is not, it fails
|
|
@@ -459,11 +467,16 @@ export function parseRoleFlag(args, validIds, flag = "--role", missingValue = RO
|
|
|
459
467
|
}
|
|
460
468
|
/** Parse an optional `--branch <name>` flag: the named branch, or null when absent — the same
|
|
461
469
|
* shape as parseRoleFlag, so every command that scopes to one branch validates identically.
|
|
462
|
-
* A missing value fails fast instead of silently falling back to the checked-out branch.
|
|
470
|
+
* A missing value fails fast instead of silently falling back to the checked-out branch. A
|
|
471
|
+
* `--`-token in the slot is the operator's slipped next flag (`run --branch --json`), never a
|
|
472
|
+
* branch name — a branch name never starts with `-` — so refuse it up front with
|
|
473
|
+
* rejectFlagLookingValue's shared wording, the same guard every other value slot applies,
|
|
474
|
+
* instead of reporting the next flag as a missing name. */
|
|
463
475
|
export function parseBranchFlag(args) {
|
|
464
476
|
const branch = flagValue(args, "--branch");
|
|
465
477
|
if (branch === null)
|
|
466
478
|
return null;
|
|
479
|
+
rejectFlagLookingValue("--branch", branch);
|
|
467
480
|
if (!branch || branch.startsWith("-"))
|
|
468
481
|
fail(BRANCH_VALUE_ERROR);
|
|
469
482
|
return branch;
|
|
@@ -240,7 +240,8 @@ export const N_FLAG = {
|
|
|
240
240
|
* whole spec, and the render-vs-JSON switch stays in each command body beside its render. */
|
|
241
241
|
export const JSON_FLAG = { names: ["--json"] };
|
|
242
242
|
/** The `-f/--follow` flag spec, shared by every command with a follow view (logs, history,
|
|
243
|
-
* status, queue, report, backlog and
|
|
243
|
+
* status, queue, report, backlog, diff and quarantine — each snapshot command's `-f/--follow`
|
|
244
|
+
* twin): one
|
|
244
245
|
* definition of the flag's spelling, beside ROLE_FLAG and JSON_FLAG, so the gate's accepted
|
|
245
246
|
* vocabulary cannot drift apart. It takes no value, like JSON_FLAG; each body renders its
|
|
246
247
|
* own live view, and detects the flag through hasFollowFlag, so the spelling lives in exactly
|
|
@@ -248,8 +249,9 @@ export const JSON_FLAG = { names: ["--json"] };
|
|
|
248
249
|
export const FOLLOW_FLAG = { names: ["-f", "--follow"] };
|
|
249
250
|
/** Whether an arg array carries the `-f/--follow` flag — the detection every follow-capable
|
|
250
251
|
* command body spelled out as an `includes("-f") || includes("--follow")` one-liner over
|
|
251
|
-
* its own arg array (`args` for status, queue, report and
|
|
252
|
-
* backlog). Now derived from FOLLOW_FLAG's own spelling, so the detection cannot
|
|
252
|
+
* its own arg array (`args` for status, queue, report, diff and quarantine; `rest` for logs,
|
|
253
|
+
* history and backlog). Now derived from FOLLOW_FLAG's own spelling, so the detection cannot
|
|
254
|
+
* drift from
|
|
253
255
|
* the gate's accepted vocabulary. */
|
|
254
256
|
export function hasFollowFlag(args) {
|
|
255
257
|
return FOLLOW_FLAG.names.some((name) => args.includes(name));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fail, say, sayJsonLine } from "./cli-output.js";
|
|
2
2
|
import { hasFollowFlag, LOGS_TRANSCRIPT_VALUE_ERROR } from "./cli-flag-specs.js";
|
|
3
|
-
import { betweenWindow, durationLabel, failRivalShapes, failUntilShorterThanSince, flagValue, parseCountFlag, parseGrepFlag, parseRoleListScope, parseRoleScope, parseSinceFlag, parseTypeFlag } from "./cli-args.js";
|
|
3
|
+
import { betweenWindow, durationLabel, failFollowSinceRival, failRivalShapes, failUntilShorterThanSince, flagValue, parseCountFlag, parseGrepFlag, parseRoleListScope, parseRoleScope, parseSinceFlag, parseTypeFlag } from "./cli-args.js";
|
|
4
4
|
import { LOGS_SINCE_MAX_MS, readEventsSince, SPARSE_WINDOW_NOTE, } from "../events/event-window.js";
|
|
5
5
|
import { compareCodeUnits, increment } from "../collections.js";
|
|
6
6
|
import { eventTypeTally } from "../events/event-tally.js";
|
|
@@ -229,7 +229,7 @@ function parseLogsFeed(root, args) {
|
|
|
229
229
|
* prose, the `--count` tally, the per-event print and the sparse-window note. */
|
|
230
230
|
function sayLogsWindow(root, feed, ms, untilMs) {
|
|
231
231
|
if (feed.follow)
|
|
232
|
-
|
|
232
|
+
failFollowSinceRival("logs");
|
|
233
233
|
if (feed.rest.includes("-n"))
|
|
234
234
|
failRivalShapes("logs --since", "-n", "a count and a window are rival shapes");
|
|
235
235
|
rejectTranscriptViewRival(feed.rest, "--since", true);
|
|
@@ -342,22 +342,34 @@ export async function cmdBacklog(root, args, seams = {}) {
|
|
|
342
342
|
const result = readBacklog();
|
|
343
343
|
sayJsonOrRender(rest, result.view, () => renderBacklogText(result));
|
|
344
344
|
}
|
|
345
|
-
// `tumwater quarantine [--release <n>] [--json]` — the view of the review
|
|
346
|
-
// quarantine, read-only except the one destructive `--release <n>` mode that
|
|
347
|
-
// entry the list prints at the operator's request.
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
// the
|
|
352
|
-
|
|
345
|
+
// `tumwater quarantine [-f/--follow] [--release <n>] [--json]` — the view of the review
|
|
346
|
+
// gate's flake quarantine, read-only except the one destructive `--release <n>` mode that
|
|
347
|
+
// removes the Nth entry the list prints at the operator's request. `-f/--follow` repaints the
|
|
348
|
+
// list every couple of seconds until Ctrl+C or the process that started it exits, so a
|
|
349
|
+
// backgrounded follow does not outlive its parent (the same rule `queue -f` uses). The read
|
|
350
|
+
// path keeps its no-gate refuseTeamWorkspace stance: loadQuarantine degrades an absent or
|
|
351
|
+
// malformed file to the empty list, so the command prints the empty line in any directory and
|
|
352
|
+
// the read never writes quarantine.json (a test otherwise leaves the list through
|
|
353
|
+
// releaseQuarantine). --json serves the same positioned rows to scripts.
|
|
354
|
+
export async function cmdQuarantine(root, args, seams = {}) {
|
|
353
355
|
// `--release <n>` is the read-only command's one destructive mode: route it to its own
|
|
354
356
|
// branch before the read gate, exactly as cmdReviews routes `--accept`.
|
|
355
357
|
if (args.includes("--release")) {
|
|
356
358
|
await cmdQuarantineRelease(root, args);
|
|
357
359
|
return;
|
|
358
360
|
}
|
|
359
|
-
rejectUnknownArgs("quarantine", args, [JSON_FLAG]);
|
|
361
|
+
rejectUnknownArgs("quarantine", args, [JSON_FLAG, FOLLOW_FLAG]);
|
|
360
362
|
refuseTeamWorkspace(root);
|
|
363
|
+
const follow = hasFollowFlag(args);
|
|
364
|
+
if (follow && args.includes("--json"))
|
|
365
|
+
fail("quarantine --json prints one document; -f/--follow repaints a live view — drop one of them");
|
|
366
|
+
// Each frame re-reads the quarantine file and reads the clock once, so an entry that
|
|
367
|
+
// appears, ages toward the ceiling, or leaves when its bug closes shows up in the next
|
|
368
|
+
// repaint — the same per-frame read `queue -f` uses.
|
|
369
|
+
if (follow) {
|
|
370
|
+
await followView(() => renderQuarantine(quarantineRows(loadQuarantine(root)), Date.now()), seams);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
361
373
|
const rows = quarantineRows(loadQuarantine(root));
|
|
362
374
|
sayJsonOrRender(args, { tests: rows }, () => renderQuarantine(rows, Date.now()));
|
|
363
375
|
}
|
package/dist/src/cli/help.js
CHANGED
|
@@ -287,14 +287,17 @@ Usage:
|
|
|
287
287
|
-f/--follow repaints the backlog every couple of seconds until
|
|
288
288
|
Ctrl+C or the process that started it exits, so a backgrounded
|
|
289
289
|
follow does not outlive its parent; it refuses --json
|
|
290
|
-
tumwater quarantine [--release <n>] [--json]
|
|
290
|
+
tumwater quarantine [-f/--follow] [--release <n>] [--json]
|
|
291
291
|
List the flake quarantine — the tests the review gate recorded
|
|
292
292
|
as failing then passing on the same tree
|
|
293
293
|
(.tumwater/state/quarantine.json), oldest first, each naming
|
|
294
294
|
its first-seen and last-seen age, flake count, and the BUGS.md
|
|
295
295
|
entry filed for it (or "not filed"); empty prints
|
|
296
296
|
"flake quarantine is empty"; --json prints the same rows as
|
|
297
|
-
{ tests: [...] };
|
|
297
|
+
{ tests: [...] }; -f/--follow repaints the list every couple of
|
|
298
|
+
seconds until Ctrl+C or the process that started it exits, so a
|
|
299
|
+
backgrounded follow does not outlive its parent, and it refuses
|
|
300
|
+
--json; read-only except --release <n>, which
|
|
298
301
|
removes the Nth entry the list prints at the user's
|
|
299
302
|
request, logging a warning — for a flake fixed or judged gone
|
|
300
303
|
before its bug closes; otherwise a test leaves the list when
|
package/dist/src/cli.js
CHANGED
|
@@ -33,7 +33,7 @@ import { PACKAGE_JSON, nodeFloorProblem, packageEnginesNode, packageVersion } fr
|
|
|
33
33
|
* Arity is gated before the ready-repo check so a malformed call fails with its usage in any
|
|
34
34
|
* directory; cmdRunDetail owns the value validation and the --last/numbered rivals. */
|
|
35
35
|
async function runTrailCommand(root, args) {
|
|
36
|
-
const { cmdRunDetail, RUN_DETAIL_USAGE } = await import("./
|
|
36
|
+
const { cmdRunDetail, RUN_DETAIL_USAGE } = await import("./history/run-detail.js");
|
|
37
37
|
const { HISTORY_GREP_VALUE_ERROR, HISTORY_RESULT_VALUE_ERROR, HISTORY_MODEL_VALUE_ERROR } = await import("./history/history.js");
|
|
38
38
|
// parseGrepFlag removes the pattern token, so `--grep two` cannot read "two" as a positional.
|
|
39
39
|
const { rest: ungrepped, pattern: grep } = parseGrepFlag(args, HISTORY_GREP_VALUE_ERROR);
|
|
@@ -68,6 +68,70 @@ const PLAIN_INTEGER_TOP_LEVEL_KEYS = new Set([
|
|
|
68
68
|
"maxConcurrentChecks",
|
|
69
69
|
"worktreeSlots",
|
|
70
70
|
]);
|
|
71
|
+
/** The duration-typed team keys whose value is a plain decimal second count (validateConfig's
|
|
72
|
+
* DURATION_* rules, config-validation.ts): the top-level run-timing knobs plus the section
|
|
73
|
+
* fields a dotted key names (`review.timeoutSeconds`, `check.timeoutSeconds`,
|
|
74
|
+
* `idleBackoff.initialSeconds`/`maxSeconds`) and the per-role
|
|
75
|
+
* `roles.<id>.minRunIntervalSeconds`. For these, an exponent spelling like "1e2" is itself a
|
|
76
|
+
* JSON number (100) that setConfigKey's JSON.parse would read past validateConfig's numeric
|
|
77
|
+
* check and silently write as the misread — the same silent misread the TUI Ctrl+S interval
|
|
78
|
+
* editor, the CLI's `role --min-run-interval` and the GUI interval drawer refuse — so
|
|
79
|
+
* setConfigKey refuses it, naming the key and writing nothing. `idleBackoff.factor` is a
|
|
80
|
+
* multiplier, not a second count, so it is not in this set. */
|
|
81
|
+
const PLAIN_DURATION_KEYS = new Set([
|
|
82
|
+
"minRunIntervalSeconds",
|
|
83
|
+
"runTimeoutSeconds",
|
|
84
|
+
"quietTimeoutSeconds",
|
|
85
|
+
"toolCallStallSeconds",
|
|
86
|
+
]);
|
|
87
|
+
// The key-shape predicates for exponentRefusal's two numeric families, lifted out of its
|
|
88
|
+
// body so "which keys take a plain integer or a plain second count" is read from the
|
|
89
|
+
// key families instead of inline OR chains over `parsed`.
|
|
90
|
+
/** The integer-bound keys: the top-level run-concurrency knobs (see
|
|
91
|
+
* PLAIN_INTEGER_TOP_LEVEL_KEYS), the per-role `instances` count and the
|
|
92
|
+
* `bootstrap.untilPlansDone` section field. For these, an exponent spelling is refused
|
|
93
|
+
* with the "plain integer" message. */
|
|
94
|
+
function isPlainIntegerKey(parsed, key) {
|
|
95
|
+
return ((parsed.kind === "top" && PLAIN_INTEGER_TOP_LEVEL_KEYS.has(key)) ||
|
|
96
|
+
(parsed.kind === "role" && parsed.field === "instances") ||
|
|
97
|
+
(parsed.kind === "section" && parsed.section === "bootstrap" && parsed.field === "untilPlansDone"));
|
|
98
|
+
}
|
|
99
|
+
/** The duration-typed keys: a top-level second-count knob, the section fields a dotted
|
|
100
|
+
* key names (review/check's timeoutSeconds, idleBackoff's second counts), or the
|
|
101
|
+
* per-role minRunIntervalSeconds. They take plain decimals, not integers, so the
|
|
102
|
+
* refusal says "plain second count" — the wording the TUI interval editor, the CLI's
|
|
103
|
+
* `role --min-run-interval`, and the GUI drawer all use for the same misread. */
|
|
104
|
+
function isPlainDurationKey(parsed) {
|
|
105
|
+
return ((parsed.kind === "top" && PLAIN_DURATION_KEYS.has(parsed.key)) ||
|
|
106
|
+
(parsed.kind === "section" &&
|
|
107
|
+
((parsed.section === "review" && parsed.field === "timeoutSeconds") ||
|
|
108
|
+
(parsed.section === "check" && parsed.field === "timeoutSeconds") ||
|
|
109
|
+
(parsed.section === "idleBackoff" && (parsed.field === "initialSeconds" || parsed.field === "maxSeconds")))) ||
|
|
110
|
+
(parsed.kind === "role" && parsed.field === "minRunIntervalSeconds"));
|
|
111
|
+
}
|
|
112
|
+
/** The dollar-typed keys: the team-wide daily cost cap and the per-role cap map. For
|
|
113
|
+
* these, an exponent spelling is refused with the "plain dollar amount" message. */
|
|
114
|
+
function isPlainDollarKey(parsed) {
|
|
115
|
+
return ((parsed.kind === "top" && parsed.key === "maxDailyCostUsd") ||
|
|
116
|
+
(parsed.kind === "map" && parsed.map === "maxDailyCostUsdPerRole"));
|
|
117
|
+
}
|
|
118
|
+
// The shape screen that stops setConfigKey's JSON.parse from silently writing an exponent
|
|
119
|
+
// spelling ("4e1" → 40, "1e2" → 100) past a plain-integer, plain-second-count or
|
|
120
|
+
// plain-dollar key's numeric check: each family's refusal names the key and writes
|
|
121
|
+
// nothing — the twin of the per-role guards, the TUI team-key and budget editors, and the
|
|
122
|
+
// GUI Settings rows and cap drawer. A blank or non-exponent spelling passes through to
|
|
123
|
+
// the parse below (a non-exponent invalid is left for validateConfig's own message).
|
|
124
|
+
function exponentRefusal(parsed, key, trimmed) {
|
|
125
|
+
if (trimmed === "" || !isExponentSpelling(trimmed))
|
|
126
|
+
return null;
|
|
127
|
+
if (isPlainIntegerKey(parsed, key))
|
|
128
|
+
return `${key} must be a plain integer — no exponent notation`;
|
|
129
|
+
if (isPlainDurationKey(parsed))
|
|
130
|
+
return `${key} must be a plain second count like 300, 0.5, or .5 — no +/-, exponent, or hex notation`;
|
|
131
|
+
if (isPlainDollarKey(parsed))
|
|
132
|
+
return `${key} must be a plain dollar amount like 25, 12.50, or .5 — no +/-, exponent, or hex notation`;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
71
135
|
/** Set ONE top-level key of tumwater.json from its raw CLI text (`tumwater config set`'s
|
|
72
136
|
* engine): the key must be a member of TOP_LEVEL_KEYS — the same protection checkKnownKeys
|
|
73
137
|
* gives the file itself, so `config set modle x` cannot write a dead key the runtime would
|
|
@@ -89,14 +153,6 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
89
153
|
// The helpers are declared here — above the body — so they close over `root`; the two
|
|
90
154
|
// write dispatches (writeDottedKey / writeTopLevelKey) each own one writeConfigMutation
|
|
91
155
|
// and the per-kind merge groups close over that call's `value`/`unchanged`/`oldValue`.
|
|
92
|
-
const exponentRefusal = (parsed, key, trimmed) => {
|
|
93
|
-
const isPlainIntegerKey = (parsed.kind === "top" && PLAIN_INTEGER_TOP_LEVEL_KEYS.has(key)) ||
|
|
94
|
-
(parsed.kind === "role" && parsed.field === "instances") ||
|
|
95
|
-
(parsed.kind === "section" && parsed.section === "bootstrap" && parsed.field === "untilPlansDone");
|
|
96
|
-
if (isPlainIntegerKey && trimmed !== "" && isExponentSpelling(trimmed))
|
|
97
|
-
return `${key} must be a plain integer — no exponent notation`;
|
|
98
|
-
return null;
|
|
99
|
-
};
|
|
100
156
|
// One named handler per parsed dotted-key kind: the shape/validator screen, then one
|
|
101
157
|
// writeConfigMutation whose callback is the `switch (parsed.kind)` over the merge helpers.
|
|
102
158
|
const writeDottedKey = (parsed, value) => {
|
|
@@ -303,14 +359,18 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
303
359
|
const parsed = parseConfigKey(key);
|
|
304
360
|
if (parsed.kind === "error")
|
|
305
361
|
return fail(parsed.error);
|
|
306
|
-
// Shape, before the JSON.parse, for every plain-positive-integer
|
|
307
|
-
// the top-level run-concurrency knobs and legacy
|
|
308
|
-
// team-scope keys that arrive as a dotted key
|
|
309
|
-
// `bootstrap.untilPlansDone` section field)
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
362
|
+
// Shape, before the JSON.parse, for every plain-positive-integer, plain-second-count or
|
|
363
|
+
// plain-dollar key this verb can write — the top-level run-concurrency knobs and legacy
|
|
364
|
+
// merge batch cap, the integer-bound team-scope keys that arrive as a dotted key
|
|
365
|
+
// (`roles.<id>.instances`, the `bootstrap.untilPlansDone` section field), the duration-typed
|
|
366
|
+
// keys (the top-level second-count knobs plus `review`/`check`/`idleBackoff`'s second fields
|
|
367
|
+
// and `roles.<id>.minRunIntervalSeconds`), and the dollar-typed keys
|
|
368
|
+
// (`maxDailyCostUsd`, the `maxDailyCostUsdPerRole.<role>` map entry): refuse an exponent
|
|
369
|
+
// spelling ("4e1" → 40, "1e2" → 100) that JSON.parse would read past the key's numeric
|
|
370
|
+
// check and silently write, naming the key and writing nothing — the twin of the per-role
|
|
371
|
+
// guards, the TUI team-key and budget editors, and the GUI Settings rows and cap drawer.
|
|
372
|
+
// A non-exponent invalid falls through for validateConfig (whose message names the key).
|
|
373
|
+
// Empty clears the key below.
|
|
314
374
|
const trimmed = rawValue.trim();
|
|
315
375
|
const refusal = exponentRefusal(parsed, key, trimmed);
|
|
316
376
|
if (refusal)
|
|
@@ -8,7 +8,7 @@ import { resolvedBackendConfig } from "../config/config-views.js";
|
|
|
8
8
|
import { envPath } from "../files/files.js";
|
|
9
9
|
import { belowNodeFloor } from "../version.js";
|
|
10
10
|
import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
11
|
-
import { claudeSeamsInUse, loadConfigForCheck, seamStandDown, versionFromProbe, seamedModels, } from "./doctor-model-checks.js";
|
|
11
|
+
import { claudeSeamsInUse, loadConfigForCheck, probeUnreadableWarning, seamStandDown, versionFromProbe, seamedModels, } from "./doctor-model-checks.js";
|
|
12
12
|
/** The doctor's claude credential and version readiness checks, split out of
|
|
13
13
|
* doctor-model-checks.ts (whose shared seam/model framework and the pi and tier-models
|
|
14
14
|
* checks stay there): checkClaudeAuth, checkAnthroApiKey and checkClaudeVersion.
|
|
@@ -62,13 +62,6 @@ async function claudeAuthProbe(config, pathEnv, timeoutMs) {
|
|
|
62
62
|
return null;
|
|
63
63
|
return parseClaudeAuthStatus(result.stdout);
|
|
64
64
|
}
|
|
65
|
-
/** The warn verdict both auth-driven claude rows return when `claude auth status` could not
|
|
66
|
-
* run or its reply was unreadable — the one home of the wording so checkClaudeAuth and
|
|
67
|
-
* checkAnthroApiKey cannot drift on the unreadable-probe message. Each caller makes its own
|
|
68
|
-
* solitary `claudeAuthProbe` call and branches on null through it. */
|
|
69
|
-
function claudeAuthUnreadable() {
|
|
70
|
-
return warnOutcome("could not check — `claude auth status` did not run or its output was unreadable");
|
|
71
|
-
}
|
|
72
65
|
/** claude credential readiness (plans/agent-backends.md "Every seam, checked"): a claude seam
|
|
73
66
|
* whose login is gone fails every run before it can work, so doctor runs `claude auth status`
|
|
74
67
|
* under the scrubbed run environment and fails when it reports `loggedIn: false`. An ok detail
|
|
@@ -82,7 +75,7 @@ export async function checkClaudeAuth(root, pathEnv = envPath(), timeoutMs = AUT
|
|
|
82
75
|
return config;
|
|
83
76
|
const auth = await claudeAuthProbe(config, pathEnv, timeoutMs);
|
|
84
77
|
if (auth === null)
|
|
85
|
-
return
|
|
78
|
+
return probeUnreadableWarning("claude auth status");
|
|
86
79
|
if (!auth.loggedIn)
|
|
87
80
|
return failOutcome("claude reports logged in: false — run `claude auth login`");
|
|
88
81
|
const subscription = claudeAuthUsesSubscription(auth);
|
|
@@ -108,7 +101,7 @@ export async function checkAnthroApiKey(root, env = process.env, pathEnv = envPa
|
|
|
108
101
|
return okOutcome("no ANTHROPIC_API_KEY set");
|
|
109
102
|
const auth = await claudeAuthProbe(config, pathEnv, timeoutMs);
|
|
110
103
|
if (auth === null)
|
|
111
|
-
return
|
|
104
|
+
return probeUnreadableWarning("claude auth status");
|
|
112
105
|
if (!auth.loggedIn || !claudeAuthUsesSubscription(auth))
|
|
113
106
|
return okOutcome("ANTHROPIC_API_KEY is set, but claude reports no claude.ai login");
|
|
114
107
|
return warnOutcome("ANTHROPIC_API_KEY is set while claude reports a claude.ai login — model jobs on claude will bill the API key, not the subscription; unset it or route every anthropic model job on pi explicitly");
|
|
@@ -171,6 +164,6 @@ export async function checkClaudeVersion(root, pathEnv = envPath(), timeoutMs =
|
|
|
171
164
|
const result = await claudeProbe(config, ["--version"], pathEnv, timeoutMs);
|
|
172
165
|
const found = versionFromProbe(result);
|
|
173
166
|
if (found === undefined)
|
|
174
|
-
return
|
|
167
|
+
return probeUnreadableWarning("claude --version");
|
|
175
168
|
return claudeVersionOutcome(found, floors, claudeVersionClampNote(seams));
|
|
176
169
|
}
|
|
@@ -14,7 +14,7 @@ import { envPath, isDirectory, readTextOrNull, statOrNull, treeBytes, walkFiles
|
|
|
14
14
|
import { formatBytes, tokenWindowLabel } from "../text/format.js";
|
|
15
15
|
import { errorMessage } from "../text/text.js";
|
|
16
16
|
import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
17
|
-
import { codexSeamsInUse, loadConfigForCheck, minVersionOutcome, versionFromProbe, seamedModels, } from "./doctor-model-checks.js";
|
|
17
|
+
import { codexSeamsInUse, loadConfigForCheck, minVersionOutcome, probeUnreadableWarning, versionFromProbe, seamedModels, } from "./doctor-model-checks.js";
|
|
18
18
|
/** The doctor's codex rows (part 9a/9, plans/codex-backend.md §4, §7, "Hand steps"): one
|
|
19
19
|
* exported check per row — install/version, login, home, models, account, leaks, sessions
|
|
20
20
|
* size and the write guard. These are the doctor's only checks that spawn the `codex` CLI and
|
|
@@ -78,7 +78,7 @@ export async function checkCodexAuth(root, pathEnv = envPath(), timeoutMs = AUTH
|
|
|
78
78
|
const { home } = base;
|
|
79
79
|
const result = await codexProbe(base.config, ["login", "status"], pathEnv, timeoutMs);
|
|
80
80
|
if (result?.code !== 0)
|
|
81
|
-
return
|
|
81
|
+
return probeUnreadableWarning("codex login status");
|
|
82
82
|
const status = parseCodexLoginStatus(result.stdout);
|
|
83
83
|
if (status === "subscription")
|
|
84
84
|
return okOutcome("logged in using ChatGPT — billing: subscription");
|
|
@@ -107,11 +107,19 @@ export function versionFromProbe(result, requireVPrefix = false) {
|
|
|
107
107
|
* two backends' version verdicts from drifting. */
|
|
108
108
|
export function minVersionOutcome(name, found, min, upgradeHint) {
|
|
109
109
|
if (found === undefined)
|
|
110
|
-
return
|
|
110
|
+
return probeUnreadableWarning(`${name} --version`);
|
|
111
111
|
if (belowNodeFloor(found, `>=${min}`))
|
|
112
112
|
return failOutcome(`${name} ${found} is older than ${min} — ${upgradeHint}`);
|
|
113
113
|
return okOutcome(`${name} ${found} (>= ${min})`);
|
|
114
114
|
}
|
|
115
|
+
/** The warn a backend probe that could not run or whose output was unreadable earns: it says
|
|
116
|
+
* the check could not happen, naming the command, rather than guessing a verdict. The one home
|
|
117
|
+
* of that phrase, shared by minVersionOutcome (this file), the claude auth and version rows
|
|
118
|
+
* (doctor-claude-checks.ts) and the codex login row (doctor-codex-checks.ts), so the wording
|
|
119
|
+
* cannot drift between the backends' probes. */
|
|
120
|
+
export function probeUnreadableWarning(command) {
|
|
121
|
+
return warnOutcome(`could not check — \`${command}\` did not run or its output was unreadable`);
|
|
122
|
+
}
|
|
115
123
|
/** Whether any seam runs on the claude backend — the doctor's claude checks stand down when
|
|
116
124
|
* none does (a pi-only team reads no `~/.claude` state and spawns no claude). */
|
|
117
125
|
export function claudeSeamsInUse(config) {
|