tumwater 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +25 -12
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +1 -8
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +9 -0
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +50 -27
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +1 -1
- package/dist/src/agent/subscription-account.js +3 -3
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +58 -102
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +1 -1
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-scoped.js +40 -16
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +34 -12
- package/dist/src/cli/cli-query-commands.js +1 -1
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-start.js +91 -58
- package/dist/src/cli/completion.js +82 -46
- package/dist/src/cli/config-commands.js +82 -71
- package/dist/src/cli/help.js +4 -6
- package/dist/src/cli/log-commands.js +43 -23
- package/dist/src/cli/notify-command.js +2 -2
- package/dist/src/cli/question-commands.js +86 -60
- package/dist/src/cli.js +181 -135
- package/dist/src/collections.js +31 -16
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +22 -0
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +5 -20
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +153 -102
- package/dist/src/config/config-schema.js +9 -11
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +4 -52
- package/dist/src/config/config-validation.js +4 -38
- package/dist/src/config/config-views.js +9 -34
- package/dist/src/config/config-write.js +6 -23
- package/dist/src/config/config.js +4 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +24 -24
- package/dist/src/doctor/doctor-backlog.js +7 -5
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +5 -5
- package/dist/src/doctor/doctor-claude-checks.js +44 -28
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +44 -112
- package/dist/src/doctor/doctor-fallback-checks.js +2 -2
- package/dist/src/doctor/doctor-holds.js +6 -45
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +120 -20
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +3 -3
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +30 -54
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +1 -3
- package/dist/src/events/{notify.js → event-notify.js} +2 -2
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/failure/failure-cluster.js +6 -6
- package/dist/src/failure/failure-data.js +8 -4
- package/dist/src/failure/failure-render.js +44 -34
- package/dist/src/failure/failure-state-change.js +4 -34
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +27 -68
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +22 -23
- package/dist/src/gui/gui-prompt-commands.js +23 -23
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +51 -33
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +8 -11
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +10 -13
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-verify.js +8 -18
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +76 -44
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +27 -32
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +1 -1
- package/dist/src/orchestrator/scheduling-skip.js +1 -1
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +8 -7
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +1 -1
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +32 -6
- package/dist/src/pi/pi-watchdogs.js +12 -5
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +49 -5
- package/dist/src/process/process.js +8 -8
- package/dist/src/process/run-marker.js +31 -13
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +1 -1
- package/dist/src/review/review-precheck.js +11 -1
- package/dist/src/review/review.js +40 -153
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +1 -14
- package/dist/src/roles/role-view.js +9 -21
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/run-apply.js +12 -6
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +135 -84
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +119 -50
- package/dist/src/run/stage-check.js +49 -32
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/runner-outcome.js +6 -1
- package/dist/src/runner/runner-pi.js +3 -10
- package/dist/src/runner/runner-recovery.js +1 -1
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
- package/dist/src/runner/runner-run.js +97 -35
- package/dist/src/runner/runner-state-load.js +12 -9
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-data.js +2 -2
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -17
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +98 -65
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
- package/dist/src/team/team-polls.js +28 -126
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +20 -15
- package/dist/src/text/text.js +3 -3
- package/dist/src/ui/alert-feeds.js +49 -73
- package/dist/src/ui/badges.js +3 -13
- package/dist/src/ui/gui/gui-client-boot.js +2 -2
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +4 -4
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +53 -40
- package/dist/src/ui/run-progress-model.js +9 -2
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +3 -7
- package/dist/src/ui/status-render.js +5 -9
- package/dist/src/ui/team-alert-backlog.js +6 -6
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +7 -8
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +1 -7
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +159 -92
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +3 -3
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -228
- package/dist/src/ui/tui/tui-pane-history.js +61 -70
- package/dist/src/ui/tui/tui-pane-pending.js +284 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +11 -7
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +29 -32
- package/dist/src/ui/tui/tui.js +3 -4
- package/dist/src/verdict/fix-claim.js +76 -54
- package/dist/src/verdict/refusal.js +1 -1
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
- /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
|
@@ -18,12 +18,11 @@ import { show } from "./config-field-checks.js";
|
|
|
18
18
|
import { canonicalConfigKey } from "./config-aliases.js";
|
|
19
19
|
import { unknownRoleMessage } from "../roles/roles.js";
|
|
20
20
|
import { loadConfig, collapseBackendAliases, knownRoleIdsCached, } from "./config.js";
|
|
21
|
-
import { checkDailyBudgetUsd,
|
|
21
|
+
import { checkDailyBudgetUsd, checkQuietHours, parseConfigKey, PER_KEY_VALIDATORS, unknownConfigKeyError, unknownRoleFieldError, } from "./config-keys.js";
|
|
22
22
|
import { writeConfigMutation, fail, rawConfigObject, without, preservingFileRoles, preserveFileRoles, withSectionOrDropped, } from "./config-write-core.js";
|
|
23
23
|
/** Drop `field` from the `backends.<backend>` entry of the config `next`, dropping the whole
|
|
24
24
|
* backend entry when it empties. The one home of the "remove a nested backend field, drop the
|
|
25
|
-
* backend when empty" step the legacy `agentBin`/`piArgs` collapse
|
|
26
|
-
* collapse share, so the empty-entry rule cannot drift between the two. */
|
|
25
|
+
* backend when empty" step the legacy `agentBin`/`piArgs` collapse uses. */
|
|
27
26
|
function dropNestedBackendField(next, backend, field) {
|
|
28
27
|
const record = next;
|
|
29
28
|
const backends = { ...(record.backends ?? {}) };
|
|
@@ -62,8 +61,7 @@ export function setDailyBudgetUsd(root, value) {
|
|
|
62
61
|
* that setConfigKey's JSON.parse would read past validateConfig's integer check and silently
|
|
63
62
|
* write — the same silent misread the per-role instances/interval guards, the TUI team-key
|
|
64
63
|
* editors, and the GUI Settings rows refuse — so setConfigKey refuses it, naming the key and
|
|
65
|
-
* writing nothing.
|
|
66
|
-
* checkProviderConcurrencyCap screen and is not in this set. */
|
|
64
|
+
* writing nothing. */
|
|
67
65
|
const PLAIN_INTEGER_TOP_LEVEL_KEYS = new Set([
|
|
68
66
|
"maxConcurrent",
|
|
69
67
|
"mergeBatchMax",
|
|
@@ -93,7 +91,6 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
93
91
|
// and the per-kind merge groups close over that call's `value`/`unchanged`/`oldValue`.
|
|
94
92
|
const exponentRefusal = (parsed, key, trimmed) => {
|
|
95
93
|
const isPlainIntegerKey = (parsed.kind === "top" && PLAIN_INTEGER_TOP_LEVEL_KEYS.has(key)) ||
|
|
96
|
-
(parsed.kind === "map" && parsed.map === "maxConcurrentPerProvider") ||
|
|
97
94
|
(parsed.kind === "role" && parsed.field === "instances") ||
|
|
98
95
|
(parsed.kind === "section" && parsed.section === "bootstrap" && parsed.field === "untilPlansDone");
|
|
99
96
|
if (isPlainIntegerKey && trimmed !== "" && isExponentSpelling(trimmed))
|
|
@@ -112,9 +109,7 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
112
109
|
const label = `${parsed.map}.${parsed.role}`;
|
|
113
110
|
const validator = parsed.map === "maxDailyCostUsdPerRole"
|
|
114
111
|
? checkDailyBudgetUsd
|
|
115
|
-
:
|
|
116
|
-
? checkProviderConcurrencyCap
|
|
117
|
-
: checkQuietHours;
|
|
112
|
+
: checkQuietHours;
|
|
118
113
|
const problem = validator(value, label);
|
|
119
114
|
if (problem)
|
|
120
115
|
return fail(problem);
|
|
@@ -185,12 +180,6 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
185
180
|
else
|
|
186
181
|
delete nextRecord.piArgs;
|
|
187
182
|
}
|
|
188
|
-
// Setting the nested claude reserve drops the top-level spelling, so the file can never
|
|
189
|
-
// hold both (validateConfig rejects a file that does, mirroring the agentBin/piArgs pair).
|
|
190
|
-
if (parsed.id === "claude" && parsed.field === "windowReserve") {
|
|
191
|
-
const nextRecord = next;
|
|
192
|
-
delete nextRecord.claudeWindowReserve;
|
|
193
|
-
}
|
|
194
183
|
return preservingFileRoles(root, next);
|
|
195
184
|
};
|
|
196
185
|
const mergeRoleEntry = (record, parsed) => {
|
|
@@ -281,10 +270,6 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
281
270
|
// Writers keep one spelling: a legacy top-level set drops the matching backends.pi field, so
|
|
282
271
|
// the file can never hold both (validateConfig's raw pass rejects a file that does).
|
|
283
272
|
const dropLegacyPiAlias = (next, key) => dropNestedBackendField(next, "pi", key === "agentBin" ? "bin" : "args");
|
|
284
|
-
// Writers keep one spelling: setting the top-level `claudeWindowReserve` drops the nested
|
|
285
|
-
// `backends.claude.windowReserve`, so a supported `config set` never writes the rejected pair
|
|
286
|
-
// (validateConfig still refuses a hand-edited file holding both).
|
|
287
|
-
const dropNestedReserve = (next) => dropNestedBackendField(next, "claude", "windowReserve");
|
|
288
273
|
const result = writeConfigMutation(root, (cfg) => {
|
|
289
274
|
oldValue = cfg[key];
|
|
290
275
|
if (isDeepStrictEqual(oldValue, value)) {
|
|
@@ -306,8 +291,6 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
306
291
|
return collapseBackendAliases(next);
|
|
307
292
|
if (key === "agentBin" || key === "piArgs")
|
|
308
293
|
return dropLegacyPiAlias(next, key);
|
|
309
|
-
if (key === "claudeWindowReserve")
|
|
310
|
-
return dropNestedReserve(next);
|
|
311
294
|
return next;
|
|
312
295
|
});
|
|
313
296
|
if (!result.ok)
|
|
@@ -322,8 +305,8 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
322
305
|
return fail(parsed.error);
|
|
323
306
|
// Shape, before the JSON.parse, for every plain-positive-integer key this verb can write —
|
|
324
307
|
// the top-level run-concurrency knobs and legacy merge batch cap, plus the integer-bound
|
|
325
|
-
// team-scope keys that arrive as a dotted key (`
|
|
326
|
-
// `
|
|
308
|
+
// team-scope keys that arrive as a dotted key (`roles.<id>.instances`, the
|
|
309
|
+
// `bootstrap.untilPlansDone` section field): refuse an exponent
|
|
327
310
|
// spelling ("4e1" → 40) that JSON.parse would read past validateConfig's integer check and
|
|
328
311
|
// silently write, naming the key and writing nothing — the twin of the per-role guards, the
|
|
329
312
|
// TUI team-key editors, and the GUI Settings rows. A non-exponent invalid falls through for
|
|
@@ -34,7 +34,7 @@ export function parseJsonConfig(file, basename) {
|
|
|
34
34
|
* defaults for concurrency, timeouts, log size, retention, thrash detection, idle backoff,
|
|
35
35
|
* self-redeploy, and review settings. The return type is the resolved view (config-views.ts): a
|
|
36
36
|
* freshly defaulted config has no model map to resolve — only a loaded file can carry one — so
|
|
37
|
-
* every call site that hands a default into a pi run (run
|
|
37
|
+
* every call site that hands a default into a pi run (agent-run.ts's PiRunOptions, tests)
|
|
38
38
|
* satisfies ResolvedModelConfig directly. */
|
|
39
39
|
export function defaultConfig() {
|
|
40
40
|
const roles = {};
|
|
@@ -285,7 +285,7 @@ function cloneConfig(c) {
|
|
|
285
285
|
* reconfiguration (BUGS.md 2026-09-23: a merge's fast-forward deleted it and the team ran
|
|
286
286
|
* 8.6 h on defaults), so callers keep their last-known-good exactly as for a broken file, or
|
|
287
287
|
* fall back to defaults only when they never saw one. Startup's "missing ⇒ not initialized"
|
|
288
|
-
* gate (startup-
|
|
288
|
+
* gate (startup-gates.ts) and loadConfig's first-run defaults are unaffected. */
|
|
289
289
|
export function loadConfigCached(root) {
|
|
290
290
|
const file = configPath(root);
|
|
291
291
|
try {
|
|
@@ -353,13 +353,12 @@ function stableJson(value) {
|
|
|
353
353
|
}
|
|
354
354
|
/** Sorted names of the config settings whose value differs between two loaded configs, for the
|
|
355
355
|
* orchestrator's live-reload event. `maxConcurrent` and `sessionRetentionDays` are skipped: each
|
|
356
|
-
* already has its own, more informative event. `
|
|
357
|
-
* changed provider fires its own `provider_concurrency_changed` event. The `roles` map is never
|
|
356
|
+
* already has its own, more informative event. The `roles` map is never
|
|
358
357
|
* reported whole — only
|
|
359
358
|
* each differing `roles.<id>` — so a `customRoles` add/remove names both the section and the
|
|
360
359
|
* affected role. */
|
|
361
360
|
export function changedConfigKeys(prev, next) {
|
|
362
|
-
const skip = new Set(["maxConcurrent", "
|
|
361
|
+
const skip = new Set(["maxConcurrent", "sessionRetentionDays", "roles"]);
|
|
363
362
|
const keys = [];
|
|
364
363
|
const prevRecord = prev;
|
|
365
364
|
const nextRecord = next;
|
|
@@ -2,14 +2,14 @@ import { andMore, quoteHeading, readBacklogChecked, FIX_CLAIMS_CHECKED, REFUSAL_
|
|
|
2
2
|
import { truncate } from "../text/text.js";
|
|
3
3
|
import { plural, agree } from "../text/phrases.js";
|
|
4
4
|
import { ageInDays, filingDate, fixedBugEntries, openBugEntries, openQuestionEntries, plannedPlanEntries } from "../backlog/backlog.js";
|
|
5
|
-
import { questionFilingDate } from "../backlog/question-answer.js";
|
|
5
|
+
import { questionFilingDate } from "../backlog/backlog-question-answer.js";
|
|
6
6
|
import { needsReviewNote, refusalNote } from "../backlog/backlog-eligibility.js";
|
|
7
7
|
import { plannedPartKeys, plansWaitingOn, prerequisitesMerged } from "../backlog/backlog-parts.js";
|
|
8
8
|
import { lastDate } from "../backlog/backlog-md.js";
|
|
9
9
|
import { formatTimestamp } from "../text/datetime.js";
|
|
10
10
|
import { stringList } from "../files/json-object.js";
|
|
11
11
|
import { readEventsSince } from "../events/event-window.js";
|
|
12
|
-
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
12
|
+
import { okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
13
13
|
/** The doctor's open-bug and question checks, split out of doctor-backlog.ts: questions that
|
|
14
14
|
* have waited too long for an answer (checkStaleQuestions), bugs left unfixed (checkStaleBugs),
|
|
15
15
|
* fixed records whose quoted test regressed (checkRegressedFixes), and
|
|
@@ -17,7 +17,8 @@ import { okOutcome, warnOutcome } from "./outcome.js";
|
|
|
17
17
|
* (readBacklogChecked, andMore) from doctor-backlog.ts, plus the age/trim bounds, and
|
|
18
18
|
* all warn instead of failing: a stale or
|
|
19
19
|
* parked entry is operator signal, not a broken environment. doctor.ts composes them beside the
|
|
20
|
-
* plan-structure and queue checks; the shared CheckOutcome shape is imported from
|
|
20
|
+
* plan-structure and queue checks; the shared CheckOutcome shape is imported from
|
|
21
|
+
* doctor-outcome.ts. */
|
|
21
22
|
/** Stale questions — open QUESTIONS.md questions that have waited more than 7 days for an
|
|
22
23
|
* answer. A team can stay parked on a question for days while `tumwater doctor` reports
|
|
23
24
|
* nothing; the `--list` age (`questionAgeSuffix` on `tumwater questions`) is the only other
|
|
@@ -143,7 +144,7 @@ function sinceWindowMs(sinces) {
|
|
|
143
144
|
* cannot drift on the selection. `since` is the day-after the fix date — the recurrence scan's
|
|
144
145
|
* boundary (the heading carries only a date, so a same-day failure cannot be told from the one
|
|
145
146
|
* the fix answered). Returns `[]` when no candidate has both a quoted test and a date. */
|
|
146
|
-
|
|
147
|
+
function regressedFixCandidates(root) {
|
|
147
148
|
const candidates = fixedBugEntries(root).slice(0, FIX_CLAIMS_CHECKED).flatMap((entry) => {
|
|
148
149
|
const name = quotedTestName(entry.title);
|
|
149
150
|
const date = lastDate(entry.title, /(\d{4}-\d{2}-\d{2})/);
|
|
@@ -159,14 +160,14 @@ export function regressedFixCandidates(root) {
|
|
|
159
160
|
* `failureMentions` per candidate over `events`, keeping the last mention (the doctor's exact
|
|
160
161
|
* match). The caller supplies the event window — the doctor reads back to the oldest candidate's
|
|
161
162
|
* day-after, the always-on alert the trailing day. */
|
|
162
|
-
|
|
163
|
+
function matchedRegressedFixes(candidates, events) {
|
|
163
164
|
return candidates.flatMap((c) => {
|
|
164
165
|
const newest = lastFailureMention(events, c.name, c.since);
|
|
165
166
|
return newest ? [{ ...c, newest }] : [];
|
|
166
167
|
});
|
|
167
168
|
}
|
|
168
169
|
/** The doctor's `regressed fixes` warn sentence, verbatim, or null when nothing regressed. */
|
|
169
|
-
|
|
170
|
+
function regressedFixesPhrase(regressed) {
|
|
170
171
|
const [first, ...rest] = regressed;
|
|
171
172
|
if (!first)
|
|
172
173
|
return null;
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { andMore, quoteHeading, readDoc, readBacklogChecked, FIX_CLAIMS_CHECKED, REFUSAL_NOTE_MAX } from "./doctor-backlog.js";
|
|
2
|
+
import { agree } from "../text/phrases.js";
|
|
2
3
|
import { cachedByStat } from "../files/stat-cache.js";
|
|
3
4
|
import { VALIDATION_GAP_TAGS } from "../roles/role-guidance.js";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { truncate } from "../text/text.js";
|
|
6
7
|
import { donePlans, fixedBugEntries, openBugEntries, plannedPlanEntries } from "../backlog/backlog.js";
|
|
7
|
-
import { entryHold, entryKindLabel,
|
|
8
|
+
import { entryHold, entryKindLabel, isPlanStalled } from "../backlog/backlog-eligibility.js";
|
|
8
9
|
import { planDependencyProblems } from "../backlog/backlog-parts.js";
|
|
9
10
|
import { BACKLOG_FILES, FINISHED_ARCHIVES, validationGapTag } from "../backlog/backlog-md.js";
|
|
10
11
|
import { statOrNull } from "../files/files.js";
|
|
11
12
|
import { duplicateHeadings, strandedPlanEntriesFor } from "../backlog/backlog-structure.js";
|
|
12
13
|
import { liveFinishedHistory, liveFinishedRowList } from "../backlog/backlog-archive.js";
|
|
13
14
|
import { fixSymbols, missingSymbolNames, cachedSourceHaystack, unbackedSymbols } from "../verdict/fix-claim.js";
|
|
14
|
-
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
15
|
+
import { okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
15
16
|
/** The doctor's PLANS.md-structure and fix-claim checks, split out of doctor-backlog.ts: the
|
|
16
17
|
* checks that re-verify BUGS.md's newest Fixed records against the tree (checkFixClaims,
|
|
17
18
|
* checkValidationGaps) and the PLANS.md / backlog-file integrity checks that read sections and
|
|
@@ -21,7 +22,7 @@ import { okOutcome, warnOutcome } from "./outcome.js";
|
|
|
21
22
|
* heading-trim bounds,
|
|
22
23
|
* and all of them warn instead of failing: damaged documentation is operator signal, not a
|
|
23
24
|
* broken environment. doctor.ts composes them beside the open-item and queue checks; the shared
|
|
24
|
-
* CheckOutcome shape is imported from outcome.ts. */
|
|
25
|
+
* CheckOutcome shape is imported from doctor-outcome.ts. */
|
|
25
26
|
/** Fix claims — the standalone half of the merge gate's false-fix check (src/verdict/fix-claim.ts):
|
|
26
27
|
* that gate fires only when an md-only diff moves a BUGS.md entry to Fixed, so a phantom fix that
|
|
27
28
|
* reached main any other way (merged before the gate existed, or through a path it never sees) was
|
|
@@ -44,7 +45,7 @@ export function checkFixClaims(root) {
|
|
|
44
45
|
const { heading, missing } = first;
|
|
45
46
|
const names = missingSymbolNames(missing);
|
|
46
47
|
// Every other suspect is still named, shortened: a doctor check is one line.
|
|
47
|
-
const more = andMore(rest, (p) => quoteHeading(p.heading), "record
|
|
48
|
+
const more = andMore(rest, (p) => quoteHeading(p.heading), agree(rest.length, "record", "records"));
|
|
48
49
|
return warnOutcome(`BUGS.md records "${heading}" as Fixed, but none of the symbols its Fix paragraph names ` +
|
|
49
50
|
`exist on this tree: ${names}${more} — land the fix or keep the bug Open / refresh a stale record`);
|
|
50
51
|
}
|
|
@@ -147,14 +148,14 @@ export function checkValidationGaps(root) {
|
|
|
147
148
|
if (!first) {
|
|
148
149
|
if (unpinned.length > 0) {
|
|
149
150
|
const up = unpinned[0];
|
|
150
|
-
const more = andMore(unpinned.slice(1), (s) => quoteHeading(s.title),
|
|
151
|
+
const more = andMore(unpinned.slice(1), (s) => quoteHeading(s.title), agree(unpinned.slice(1).length, "record", "records"));
|
|
151
152
|
return warnOutcome(`BUGS.md record "${up.title}" is an \`unclear-invariant\` fix but names no ` +
|
|
152
153
|
`docs/invariants.md section — record the reconstructed guarantee there (a "Pinned by" line) in the ` +
|
|
153
154
|
`same commit so the next bug in that area does not reconstruct it again${more}`);
|
|
154
155
|
}
|
|
155
156
|
return okOutcome(`newest ${validationGapRecords(root).length} Fixed record(s) carry a known Validation gap tag`);
|
|
156
157
|
}
|
|
157
|
-
const more = andMore(rest, (s) => quoteHeading(s.title), "record
|
|
158
|
+
const more = andMore(rest, (s) => quoteHeading(s.title), agree(rest.length, "record", "records"));
|
|
158
159
|
const what = first.kind === "missing"
|
|
159
160
|
? `BUGS.md record "${first.title}" has no **Validation gap:** line`
|
|
160
161
|
: `BUGS.md record "${first.title}" names unrecognized Validation gap tag "${first.tag}"`;
|
|
@@ -309,24 +310,27 @@ export function checkBacklogHeadings(root) {
|
|
|
309
310
|
`later entries are invisible; keep one heading per name (the gate blocks only a merge ` +
|
|
310
311
|
`that adds another copy — remove the duplicate in a deliberate edit)`);
|
|
311
312
|
}
|
|
312
|
-
/** Stalled entries — plans or bugs
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
313
|
+
/** Stalled entries — plans or bugs parked with the plan role by a **Needs review …** note
|
|
314
|
+
* that the plan role clears by splitting or replanning (the feature role's too-large note, the
|
|
315
|
+
* convergence hold, a final-round rejection). The bugfix role's wait-for-the-next-failure
|
|
316
|
+
* parks ("by bugfix:") are excluded — selfcheck lifts them, not the plan role, so they are
|
|
317
|
+
* not the plan role's stalled work. No role takes a held entry (the claim gate refuses one)
|
|
318
|
+
* and nothing clears a plan-role note on its own, so the state is visible only as the
|
|
316
319
|
* `[needs review]` marks in the backlog listings; this names them so an
|
|
317
320
|
* operator gets a single-line health signal from `tumwater doctor`. Reads the tree at `root`
|
|
318
321
|
* — the primary checkout IS main's tree — like checkRefusedEntries, which is also where
|
|
319
|
-
* refusal notes (a different hold) are surfaced. entryHold
|
|
320
|
-
* already
|
|
321
|
-
* exactly needs-review
|
|
322
|
+
* refusal notes (a different hold) are surfaced. entryHold + isPlanStalled
|
|
323
|
+
* (src/backlog/backlog-eligibility.ts) already classify the notes, so this check adds no
|
|
324
|
+
* parsing: only the entries whose hold is exactly needs-review and which isPlanStalled
|
|
325
|
+
* accepts are collected, from both PLANS.md
|
|
322
326
|
* 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
|
|
324
|
-
*
|
|
327
|
+
* operator signal, not a broken environment; the plan role resolves it by splitting or
|
|
328
|
+
* replanning. */
|
|
325
329
|
export function checkStalledEntries(root) {
|
|
326
330
|
const planned = plannedPlanEntries(root);
|
|
327
331
|
const collect = (entry, file) => {
|
|
328
332
|
const hold = entryHold(entry, planned);
|
|
329
|
-
return hold === "needs-review"
|
|
333
|
+
return hold === "needs-review" && isPlanStalled(entry)
|
|
330
334
|
? { file, entry }
|
|
331
335
|
: null;
|
|
332
336
|
};
|
|
@@ -336,15 +340,11 @@ export function checkStalledEntries(root) {
|
|
|
336
340
|
].filter((x) => x !== null);
|
|
337
341
|
const [first, ...rest] = stalled;
|
|
338
342
|
if (!first)
|
|
339
|
-
return okOutcome("no planned plan or open bug is held by a Needs-review
|
|
343
|
+
return okOutcome("no planned plan or open bug is held by a Needs-review note");
|
|
340
344
|
const more = andMore(rest, (s) => quoteHeading(s.entry.title));
|
|
341
345
|
const kind = entryKindLabel(first.file); // "Bug" | "Plan"
|
|
342
346
|
const head = `${first.file === "bugs" ? "BUGS.md" : "PLANS.md"} ${kind.toLowerCase()}`;
|
|
343
|
-
const hold = entryHold(first.entry, planned);
|
|
344
347
|
const note = (first.entry.body.split(/\n/).find((line) => line.trimStart().startsWith("**Needs review ")) ?? "").replace(/^\s*\*\*/, "").replace("**", "");
|
|
345
|
-
if (hold === "held-for-operator") { // vocabulary-ok: the held-for-operator hold's key
|
|
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
|
-
}
|
|
348
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
|
}
|
|
@@ -375,16 +375,16 @@ export function planDependencyProblem(root) {
|
|
|
375
375
|
}
|
|
376
376
|
if (firstDangling) {
|
|
377
377
|
return { kind: "dangling", message: danglingMsg(firstDangling) +
|
|
378
|
-
andMore(restDangling, (d) => `${quote(d.title)} → ${d.ref}`, "ref
|
|
378
|
+
andMore(restDangling, (d) => `${quote(d.title)} → ${d.ref}`, agree(restDangling.length, "ref", "refs")) +
|
|
379
379
|
others([[unparsed.length, "unparsed clause(s)"], [misworded.length, "misworded dependency(ies)"]]) };
|
|
380
380
|
}
|
|
381
381
|
if (firstUnparsed) {
|
|
382
|
-
return { kind: "unparsed", message: unparsedMsg(firstUnparsed) + andMore(restUnparsed, quote, "clause
|
|
382
|
+
return { kind: "unparsed", message: unparsedMsg(firstUnparsed) + andMore(restUnparsed, quote, agree(restUnparsed.length, "clause", "clauses")) +
|
|
383
383
|
others([[misworded.length, "misworded dependency(ies)"]]) };
|
|
384
384
|
}
|
|
385
385
|
if (firstMisworded) {
|
|
386
386
|
return { kind: "misworded", message: miswordedMsg(firstMisworded) +
|
|
387
|
-
andMore(restMisworded, (w) => `${quote(w.title)} → ${w.ref}`, "dependency
|
|
387
|
+
andMore(restMisworded, (w) => `${quote(w.title)} → ${w.ref}`, agree(restMisworded.length, "dependency", "dependencies")) };
|
|
388
388
|
}
|
|
389
389
|
return null;
|
|
390
390
|
}
|
|
@@ -8,7 +8,7 @@ import { humanSeconds, secondsSince, HOUR_MS, DAY_MS } from "../text/datetime.js
|
|
|
8
8
|
import { eventsSince } from "../events/event-window.js";
|
|
9
9
|
import { finiteNumber } from "../files/json-object.js";
|
|
10
10
|
import { lowerMedian } from "../collections.js";
|
|
11
|
-
import { plural, shortSpanPhrase } from "../text/phrases.js";
|
|
11
|
+
import { agree, plural, shortSpanPhrase } from "../text/phrases.js";
|
|
12
12
|
import { queuedMerges, unreadableMergeQueueFiles } from "../merge/merge-queue.js";
|
|
13
13
|
import { mergeChanges, readMergeMarker } from "../merge/merge-slot.js";
|
|
14
14
|
import { listWideScope, queuedListRows } from "../inbox/inbox.js";
|
|
@@ -19,7 +19,7 @@ import { stampedSubject } from "../git/commit-message.js";
|
|
|
19
19
|
import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
|
|
20
20
|
import { briefFile } from "../brief/brief.js";
|
|
21
21
|
import { BACKLOG_FILES } from "../backlog/backlog-md.js";
|
|
22
|
-
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
22
|
+
import { okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
23
23
|
/** The doctor's backlog modules' shared scaffolding and the check groups that do not read a
|
|
24
24
|
* single backlog document: the merge-queue, post-merge-review and queued-prompt checks
|
|
25
25
|
* (checkStaleQueuedMerges, checkMergeQueueFiles, checkPostReviews, checkStalePrompts and the
|
|
@@ -30,7 +30,8 @@ import { okOutcome, warnOutcome } from "./outcome.js";
|
|
|
30
30
|
* checks live in doctor-backlog-plans.ts (the plan-document structure and fix-claim checks) and
|
|
31
31
|
* doctor-backlog-bugs.ts (open bugs, questions and holds). All of them warn instead of failing:
|
|
32
32
|
* damaged documentation is operator signal, not a broken environment. doctor.ts composes them
|
|
33
|
-
* beside the environment checks; the shared CheckOutcome shape is imported from
|
|
33
|
+
* beside the environment checks; the shared CheckOutcome shape is imported from
|
|
34
|
+
* doctor-outcome.ts. */
|
|
34
35
|
/** How many Fixed records the two newest-first Fixed-record checks (fix claims and validation
|
|
35
36
|
* gaps) verify: the newest, since the section is newest-first by template convention. Older
|
|
36
37
|
* records drift as the code evolves — a symbol legitimately fixed long ago gets renamed later,
|
|
@@ -55,7 +56,8 @@ export const STALE_BUG_DAYS = 3;
|
|
|
55
56
|
* shortened, since a doctor check renders one line. `render` makes each extra item's short
|
|
56
57
|
* form — the fix-claim and stranded-plan sites quote a heading trimmed to
|
|
57
58
|
* FIX_CLAIM_HEADING_MAX, the duplicate-headings site passes its items through as built;
|
|
58
|
-
* `noun` names what the extras are
|
|
59
|
+
* `noun` names what the extras are and must already agree with `rest.length` — callers pass
|
|
60
|
+
* `agree(rest.length, "record", "records")`, or empty where no noun is named.
|
|
59
61
|
* Empty string when there are no extras. */
|
|
60
62
|
export function andMore(rest, render, noun = "") {
|
|
61
63
|
return rest.length > 0
|
|
@@ -175,7 +177,7 @@ export function checkMergeQueueFiles(root) {
|
|
|
175
177
|
if (files.length === 0)
|
|
176
178
|
return okOutcome("the queue reader skips no merge-queue file");
|
|
177
179
|
const [first, ...rest] = files;
|
|
178
|
-
const more = andMore(rest, (f) => f, "file
|
|
180
|
+
const more = andMore(rest, (f) => f, agree(rest.length, "file", "files"));
|
|
179
181
|
return warnOutcome(`the merge queue holds ${plural(files.length, "file")} the reader skips — ${first}${more}; ` +
|
|
180
182
|
`a torn or foreign file is never cleared, so nothing ages it or surfaces its merge — remove it (a crashed enqueue's merge ref still recovers on the next run)`);
|
|
181
183
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/** Every row name `tumwater doctor` can print, in report order — the names `tumwater doctor
|
|
2
2
|
* <check>...` accepts. A leaf with no imports: cli.ts loads doctor.ts with a dynamic import()
|
|
3
3
|
* while cli-command-args.ts is imported eagerly, so the argument parser reads the names from
|
|
4
|
-
* here instead of pulling every check module into each CLI start. `pi extensions
|
|
5
|
-
*
|
|
6
|
-
* returns null leaves no row. */
|
|
4
|
+
* here instead of pulling every check module into each CLI start. `pi extensions` and `funes`
|
|
5
|
+
* are conditional: a named one whose check returns null leaves no row. */
|
|
7
6
|
export const DOCTOR_CHECK_NAMES = [
|
|
8
7
|
"node",
|
|
9
8
|
"node binary",
|
|
@@ -17,11 +16,8 @@ export const DOCTOR_CHECK_NAMES = [
|
|
|
17
16
|
"pause",
|
|
18
17
|
"quiet hours",
|
|
19
18
|
"budget",
|
|
20
|
-
"claude window",
|
|
21
|
-
"codex window",
|
|
22
19
|
"provider waits",
|
|
23
20
|
"maintenance allowance",
|
|
24
|
-
"provider concurrency",
|
|
25
21
|
"provider contention",
|
|
26
22
|
"check contention",
|
|
27
23
|
"repeated failures",
|
|
@@ -29,6 +25,7 @@ export const DOCTOR_CHECK_NAMES = [
|
|
|
29
25
|
"over-cap model",
|
|
30
26
|
"fallback chain",
|
|
31
27
|
"models for each tier",
|
|
28
|
+
"image input",
|
|
32
29
|
"claude auth",
|
|
33
30
|
"claude api key",
|
|
34
31
|
"claude version",
|
|
@@ -17,13 +17,13 @@ import { errorMessage } from "../text/text.js";
|
|
|
17
17
|
import { formatBytes } from "../text/format.js";
|
|
18
18
|
import { BRIEF_TEXT_MAX_CHARS, briefFile, promptBlockOf, readBrief } from "../brief/brief.js";
|
|
19
19
|
import { plural } from "../text/phrases.js";
|
|
20
|
-
import { backendsInUse, unregisteredBackendInUse } from "../gates/startup-
|
|
20
|
+
import { backendsInUse, unregisteredBackendInUse } from "../gates/startup-gates.js";
|
|
21
21
|
import { claudeSeamsInUse, minVersionOutcome, opencodeSeamsInUse, versionFromProbe, piSeamsInUse } from "./doctor-model-checks.js";
|
|
22
22
|
import { claudeConfigDir } from "../agent/claude/claude-backend.js";
|
|
23
|
-
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./probe.js";
|
|
23
|
+
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./doctor-probe.js";
|
|
24
24
|
import { childEnv } from "../process/process.js";
|
|
25
25
|
import { opencodeBackend, OPENCODE_MIN_VERSION } from "../agent/opencode/opencode-backend.js";
|
|
26
|
-
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
26
|
+
import { failOutcome, noConfigOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
27
27
|
import { spawnSync } from "node:child_process";
|
|
28
28
|
/** The floor doctor falls back to when package.json — the one source of the declared
|
|
29
29
|
* engines.node spec, read below — cannot be read (a broken install, the version command's
|
|
@@ -238,7 +238,7 @@ export function checkBackendBinary(root, pathEnv = envPath()) {
|
|
|
238
238
|
export async function checkOpencodeVersion(root, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
239
239
|
const { config } = loadConfigSafe(root);
|
|
240
240
|
if (config === undefined)
|
|
241
|
-
return
|
|
241
|
+
return noConfigOutcome();
|
|
242
242
|
if (!opencodeSeamsInUse(config))
|
|
243
243
|
return okOutcome("no model jobs run on opencode");
|
|
244
244
|
const resolved = opencodeBackend.resolveBin(config);
|
|
@@ -255,7 +255,7 @@ export async function checkOpencodeVersion(root, pathEnv = envPath(), timeoutMs
|
|
|
255
255
|
* `homeDir` and `env` are test seams. */
|
|
256
256
|
export function checkClaudeTranscripts(config, homeDir = os.homedir(), env = process.env) {
|
|
257
257
|
if (!config)
|
|
258
|
-
return
|
|
258
|
+
return noConfigOutcome();
|
|
259
259
|
if (!claudeSeamsInUse(config))
|
|
260
260
|
return okOutcome("no model jobs run on claude");
|
|
261
261
|
const configDir = resolvedBackendConfig(config, "claude").configDir;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import { childEnv } from "../process/process.js";
|
|
3
|
-
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./probe.js";
|
|
3
|
+
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./doctor-probe.js";
|
|
4
4
|
import { claudeBackend, claudeConfigDir } from "../agent/claude/claude-backend.js";
|
|
5
5
|
import { claudeEnv } from "../agent/claude/claude-env.js";
|
|
6
6
|
import { claudeAuthUsesSubscription, parseClaudeAuthStatus } from "../agent/claude/claude-account.js";
|
|
7
7
|
import { resolvedBackendConfig } from "../config/config-views.js";
|
|
8
8
|
import { envPath } from "../files/files.js";
|
|
9
9
|
import { belowNodeFloor } from "../version.js";
|
|
10
|
-
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
10
|
+
import { failOutcome, okOutcome, warnOutcome } from "./doctor-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
13
|
* doctor-model-checks.ts (whose shared seam/model framework and the pi and tier-models
|
|
@@ -16,9 +16,9 @@ import { claudeSeamsInUse, loadConfigForCheck, versionFromProbe, seamedModels, }
|
|
|
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
|
|
18
18
|
* (CheckOutcome) and the seam/model framework (loadConfigForCheck, claudeSeamsInUse,
|
|
19
|
-
* seamPairs) come from doctor-model-checks.ts, and the AUTH_CHECK_* probe budgets
|
|
20
|
-
* with the shared report contract (CheckOutcome) from
|
|
21
|
-
* modules share them. */
|
|
19
|
+
* seamPairs) come from doctor-model-checks.ts, and the AUTH_CHECK_* probe budgets
|
|
20
|
+
* from doctor-probe.ts, with the shared report contract (CheckOutcome) from
|
|
21
|
+
* doctor-outcome.ts, exactly as the other doctor-* modules share them. */
|
|
22
22
|
/** One bounded probe of the resolved `claude` binary under the scrubbed run environment a real
|
|
23
23
|
* run gets: resolve the binary through the claude adapter that would spawn it and run `args`
|
|
24
24
|
* with a short deadline. Null when the probe could not run, the child errored, or the deadline
|
|
@@ -95,39 +95,27 @@ export async function checkAnthroApiKey(root, env = process.env, pathEnv = envPa
|
|
|
95
95
|
return okOutcome("ANTHROPIC_API_KEY is set, but claude reports no claude.ai login");
|
|
96
96
|
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");
|
|
97
97
|
}
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
100
|
-
|
|
101
|
-
* highest floor among the claude-routed models, naming which model needs it; an ok detail
|
|
102
|
-
* prints the installed version and the floor it satisfied (or no floor when none applies).
|
|
103
|
-
* The `off`/`minimal` thinking clamp is noted here too, since this check already has the
|
|
104
|
-
* claude seams in hand. Stands down when no seam routes to claude. */
|
|
105
|
-
export async function checkClaudeVersion(root, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
106
|
-
const config = loadConfigForCheck(root);
|
|
107
|
-
if ("level" in config)
|
|
108
|
-
return config;
|
|
109
|
-
const seams = seamedModels(config, "claude");
|
|
110
|
-
if (seams.length === 0)
|
|
111
|
-
return okOutcome("no model jobs run on claude");
|
|
112
|
-
// The floor each configured model needs; a model the adapter does not gate contributes none.
|
|
98
|
+
/** The floor each configured model needs, keyed by model; a model the adapter does not gate
|
|
99
|
+
* contributes none. */
|
|
100
|
+
function claudeVersionFloors(seams) {
|
|
113
101
|
const floors = new Map();
|
|
114
102
|
for (const seam of seams) {
|
|
115
103
|
const min = claudeBackend.minVersion?.(seam.model);
|
|
116
104
|
if (min !== undefined)
|
|
117
105
|
floors.set(seam.model, min);
|
|
118
106
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
107
|
+
return floors;
|
|
108
|
+
}
|
|
109
|
+
/** The clamp note when any claude seam clamps thinking, else empty. */
|
|
110
|
+
function claudeVersionClampNote(seams) {
|
|
123
111
|
const clamped = seams.some((seam) => seam.thinking === "off" || seam.thinking === "minimal");
|
|
124
112
|
const clampNote = clamped
|
|
125
113
|
? "; `off`/`minimal` thinking clamps to `--effort low` (claude has no lower setting)"
|
|
126
114
|
: "";
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
115
|
+
return clampNote;
|
|
116
|
+
}
|
|
117
|
+
/** The highest floor decides: whichever model needs the newest CLI. */
|
|
118
|
+
function claudeVersionNeededBy(floors) {
|
|
131
119
|
let required = "";
|
|
132
120
|
let neededBy = "";
|
|
133
121
|
for (const [model, min] of floors) {
|
|
@@ -136,7 +124,35 @@ export async function checkClaudeVersion(root, pathEnv = envPath(), timeoutMs =
|
|
|
136
124
|
neededBy = model;
|
|
137
125
|
}
|
|
138
126
|
}
|
|
127
|
+
return { required, neededBy };
|
|
128
|
+
}
|
|
129
|
+
/** Verdict on the found version against the claude seams' floors, carrying the clamp note. */
|
|
130
|
+
function claudeVersionOutcome(found, floors, clampNote) {
|
|
131
|
+
if (floors.size === 0)
|
|
132
|
+
return okOutcome(`claude ${found}${clampNote}`);
|
|
133
|
+
const { required, neededBy } = claudeVersionNeededBy(floors);
|
|
139
134
|
if (belowNodeFloor(found, `>=${required}`))
|
|
140
135
|
return failOutcome(`claude ${found} is older than ${required} required by ${neededBy} — run \`claude update\``);
|
|
141
136
|
return okOutcome(`claude ${found} (>= ${required} for ${neededBy})${clampNote}`);
|
|
142
137
|
}
|
|
138
|
+
/** claude CLI-version readiness (plans/agent-backends.md finding 9): an installed CLI below a
|
|
139
|
+
* configured model's minimum fails or misprices that model, and team runs disable the
|
|
140
|
+
* auto-updater, so the version an operator has is the version agent runs use. Fails below the
|
|
141
|
+
* highest floor among the claude-routed models, naming which model needs it; an ok detail
|
|
142
|
+
* prints the installed version and the floor it satisfied (or no floor when none applies).
|
|
143
|
+
* The `off`/`minimal` thinking clamp is noted here too, since this check already has the
|
|
144
|
+
* claude seams in hand. Stands down when no seam routes to claude. */
|
|
145
|
+
export async function checkClaudeVersion(root, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
146
|
+
const config = loadConfigForCheck(root);
|
|
147
|
+
if ("level" in config)
|
|
148
|
+
return config;
|
|
149
|
+
const seams = seamedModels(config, "claude");
|
|
150
|
+
if (seams.length === 0)
|
|
151
|
+
return okOutcome("no model jobs run on claude");
|
|
152
|
+
const floors = claudeVersionFloors(seams);
|
|
153
|
+
const result = await claudeProbe(config, ["--version"], pathEnv, timeoutMs);
|
|
154
|
+
const found = versionFromProbe(result);
|
|
155
|
+
if (found === undefined)
|
|
156
|
+
return warnOutcome("could not check — `claude --version` did not run or its output was unreadable");
|
|
157
|
+
return claudeVersionOutcome(found, floors, claudeVersionClampNote(seams));
|
|
158
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { childEnv } from "../process/process.js";
|
|
4
|
-
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./probe.js";
|
|
4
|
+
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./doctor-probe.js";
|
|
5
5
|
import { codexBackend, CODEX_MIN_VERSION } from "../agent/codex/codex-backend.js";
|
|
6
6
|
import { codexEffort, EFFORT_BY_THINKING } from "../agent/codex/codex-args.js";
|
|
7
7
|
import { codexEnv } from "../agent/codex/codex-env.js";
|
|
8
8
|
import { codexHome, readCodexCatalog } from "../agent/codex/codex-catalog.js";
|
|
9
9
|
import { codexHookTrust } from "../agent/codex/codex-hook-trust.js";
|
|
10
|
-
import { parseCodexLoginStatus } from "../agent/codex/codex-account.js";
|
|
10
|
+
import { codexApiKeyRefusalMessage, parseCodexLoginStatus } from "../agent/codex/codex-account.js";
|
|
11
11
|
import { codexWriteGuardDefinition } from "../agent/codex/codex-write-guard.js";
|
|
12
12
|
import { resolvedBackendConfig } from "../config/config-views.js";
|
|
13
13
|
import { envPath, isDirectory, readTextOrNull, statOrNull, treeBytes, walkFiles } from "../files/files.js";
|
|
14
14
|
import { formatBytes, tokenWindowLabel } from "../text/format.js";
|
|
15
15
|
import { errorMessage } from "../text/text.js";
|
|
16
|
-
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
16
|
+
import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
17
17
|
import { codexSeamsInUse, loadConfigForCheck, minVersionOutcome, 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
|
|
@@ -25,8 +25,8 @@ import { codexSeamsInUse, loadConfigForCheck, minVersionOutcome, versionFromProb
|
|
|
25
25
|
* pi-only team shows no codex row and spawns nothing and reads nothing. The shared
|
|
26
26
|
* report contract (CheckOutcome) and the seam/model framework (loadConfigForCheck,
|
|
27
27
|
* codexSeamsInUse, seamPairs) come from doctor-model-checks.ts, the AUTH_CHECK_* probe budgets
|
|
28
|
-
* from probe.ts, and the shared report contract (CheckOutcome) from
|
|
29
|
-
* doctor-* modules share them. */
|
|
28
|
+
* from doctor-probe.ts, and the shared report contract (CheckOutcome) from
|
|
29
|
+
* doctor-outcome.ts, as the other doctor-* modules share them. */
|
|
30
30
|
/** The config and codex home a codex readiness row reads, or the warn outcome a broken
|
|
31
31
|
* `tumwater.json` earns, or null when no seam routes to codex (the row's stand-down). All
|
|
32
32
|
* eight rows repeat the same three-line guard and the same home lookup
|
|
@@ -83,7 +83,7 @@ export async function checkCodexAuth(root, pathEnv = envPath(), timeoutMs = AUTH
|
|
|
83
83
|
if (status === "subscription")
|
|
84
84
|
return okOutcome("logged in using ChatGPT — billing: subscription");
|
|
85
85
|
if (status === "api")
|
|
86
|
-
return failOutcome(
|
|
86
|
+
return failOutcome(codexApiKeyRefusalMessage(home));
|
|
87
87
|
if (/Not logged in/i.test(result.stdout ?? ""))
|
|
88
88
|
return failOutcome(`codex is not logged in — run \`CODEX_HOME=${home} codex login --device-auth\``);
|
|
89
89
|
return warnOutcome("could not check — `codex login status` reply was unreadable");
|