tumwater 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -13,12 +13,28 @@
|
|
|
13
13
|
import { isDeepStrictEqual } from "node:util";
|
|
14
14
|
import { modelTierMap } from "./config-schema.js";
|
|
15
15
|
import { isJsonObject, objectOrEmpty } from "../files/json-object.js";
|
|
16
|
+
import { isExponentSpelling } from "../text/text.js";
|
|
16
17
|
import { show } from "./config-field-checks.js";
|
|
17
18
|
import { canonicalConfigKey } from "./config-aliases.js";
|
|
18
19
|
import { unknownRoleMessage } from "../roles/roles.js";
|
|
19
20
|
import { loadConfig, collapseBackendAliases, knownRoleIdsCached, } from "./config.js";
|
|
20
21
|
import { checkDailyBudgetUsd, checkProviderConcurrencyCap, checkQuietHours, parseConfigKey, PER_KEY_VALIDATORS, unknownConfigKeyError, unknownRoleFieldError, } from "./config-keys.js";
|
|
21
22
|
import { writeConfigMutation, fail, rawConfigObject, without, preservingFileRoles, preserveFileRoles, withSectionOrDropped, } from "./config-write-core.js";
|
|
23
|
+
/** Drop `field` from the `backends.<backend>` entry of the config `next`, dropping the whole
|
|
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 and the `claudeWindowReserve`
|
|
26
|
+
* collapse share, so the empty-entry rule cannot drift between the two. */
|
|
27
|
+
function dropNestedBackendField(next, backend, field) {
|
|
28
|
+
const record = next;
|
|
29
|
+
const backends = { ...(record.backends ?? {}) };
|
|
30
|
+
const entry = { ...(backends[backend] ?? {}) };
|
|
31
|
+
delete entry[field];
|
|
32
|
+
if (Object.keys(entry).length === 0)
|
|
33
|
+
delete backends[backend];
|
|
34
|
+
else
|
|
35
|
+
backends[backend] = entry;
|
|
36
|
+
return { ...next, backends: backends };
|
|
37
|
+
}
|
|
22
38
|
/** Set the daily cost budget cap in tumwater.json through the shared writer idiom (see
|
|
23
39
|
* writeConfigMutation), after checkDailyBudgetUsd has screened the value with its own
|
|
24
40
|
* actionable message — the one both dashboards flash. A cap the config already resolves to
|
|
@@ -72,37 +88,21 @@ const PLAIN_INTEGER_TOP_LEVEL_KEYS = new Set([
|
|
|
72
88
|
* (see writeConfigMutation). A value equal to the resolved one is a no-op: nothing is written and
|
|
73
89
|
* the result carries `unchanged`. */
|
|
74
90
|
export function setConfigKey(root, key, rawValue) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
(parsed.kind === "role" && parsed.field === "instances") ||
|
|
91
|
-
(parsed.kind === "section" && parsed.section === "bootstrap" && parsed.field === "untilPlansDone");
|
|
92
|
-
if (isPlainIntegerKey &&
|
|
93
|
-
trimmed !== "" &&
|
|
94
|
-
!/^\d*\.?\d+$/.test(trimmed) &&
|
|
95
|
-
/^\d+(\.\d+)?[eE][+-]?\d+$/.test(trimmed))
|
|
96
|
-
return fail(`${key} must be a plain integer — no exponent notation`);
|
|
97
|
-
let unchanged; // set by a mutation callback when value equals the resolved one
|
|
98
|
-
let value;
|
|
99
|
-
try {
|
|
100
|
-
value = JSON.parse(rawValue);
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
value = rawValue; // not JSON: the literal string, so `set model gpt-5` needs no quotes
|
|
104
|
-
}
|
|
105
|
-
if (parsed.kind !== "top") {
|
|
91
|
+
// The helpers are declared here — above the body — so they close over `root`; the two
|
|
92
|
+
// write dispatches (writeDottedKey / writeTopLevelKey) each own one writeConfigMutation
|
|
93
|
+
// and the per-kind merge groups close over that call's `value`/`unchanged`/`oldValue`.
|
|
94
|
+
const exponentRefusal = (parsed, key, trimmed) => {
|
|
95
|
+
const isPlainIntegerKey = (parsed.kind === "top" && PLAIN_INTEGER_TOP_LEVEL_KEYS.has(key)) ||
|
|
96
|
+
(parsed.kind === "map" && parsed.map === "maxConcurrentPerProvider") ||
|
|
97
|
+
(parsed.kind === "role" && parsed.field === "instances") ||
|
|
98
|
+
(parsed.kind === "section" && parsed.section === "bootstrap" && parsed.field === "untilPlansDone");
|
|
99
|
+
if (isPlainIntegerKey && trimmed !== "" && isExponentSpelling(trimmed))
|
|
100
|
+
return `${key} must be a plain integer — no exponent notation`;
|
|
101
|
+
return null;
|
|
102
|
+
};
|
|
103
|
+
// One named handler per parsed dotted-key kind: the shape/validator screen, then one
|
|
104
|
+
// writeConfigMutation whose callback is the `switch (parsed.kind)` over the merge helpers.
|
|
105
|
+
const writeDottedKey = (parsed, value) => {
|
|
106
106
|
const fieldError = parsed.kind === "role" ? unknownRoleFieldError(parsed.id, parsed.field) : null;
|
|
107
107
|
if (fieldError)
|
|
108
108
|
return fail(fieldError);
|
|
@@ -122,77 +122,78 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
122
122
|
if (parsed.kind === "tier" && (typeof value !== "string" || value === "")) {
|
|
123
123
|
return fail(`model.${parsed.tier} must be a model id (e.g. \`config set model.strong huggingface/zai-org/GLM-5.3-Flash:together:low\`) — got ${show(value)}`);
|
|
124
124
|
}
|
|
125
|
+
// The two merge groups close over these locals of the one writeConfigMutation below.
|
|
125
126
|
let oldValue;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return cfg;
|
|
134
|
-
}
|
|
135
|
-
const next = { ...cfg, [parsed.map]: { ...existing, [parsed.role]: value } };
|
|
136
|
-
return preservingFileRoles(root, next);
|
|
127
|
+
let unchanged;
|
|
128
|
+
const mergeMapEntry = (record, parsed) => {
|
|
129
|
+
const existing = record[parsed.map] ?? {};
|
|
130
|
+
oldValue = existing[parsed.role];
|
|
131
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
132
|
+
unchanged = true;
|
|
133
|
+
return record;
|
|
137
134
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return
|
|
135
|
+
const next = { ...record, [parsed.map]: { ...existing, [parsed.role]: value } };
|
|
136
|
+
return preservingFileRoles(root, next);
|
|
137
|
+
};
|
|
138
|
+
const mergeTier = (record, parsed) => {
|
|
139
|
+
// Setting a tier on a string `model` promotes it to the map form rather than
|
|
140
|
+
// overwriting the bare string — the other tiers' models survive (modelTierMap owns
|
|
141
|
+
// that reading). validateConfig in writeConfigMutation owns the
|
|
142
|
+
// type truth of the merged map, so the cast is safe: a type-invalid merge fails
|
|
143
|
+
// there and nothing is written.
|
|
144
|
+
const existing = modelTierMap(record.model);
|
|
145
|
+
oldValue = existing[parsed.tier];
|
|
146
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
147
|
+
unchanged = true;
|
|
148
|
+
return record;
|
|
152
149
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return
|
|
150
|
+
const next = { ...record, model: { ...existing, [parsed.tier]: value } };
|
|
151
|
+
return preservingFileRoles(root, next);
|
|
152
|
+
};
|
|
153
|
+
const mergeSection = (record, parsed) => {
|
|
154
|
+
// Merge one field into the loaded section, keeping its other fields; validateConfig in
|
|
155
|
+
// writeConfigMutation owns the value type, so a bad value fails there and writes nothing.
|
|
156
|
+
const existing = record[parsed.section] ?? {};
|
|
157
|
+
oldValue = existing[parsed.field];
|
|
158
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
159
|
+
unchanged = true;
|
|
160
|
+
return record;
|
|
164
161
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (parsed.id === "claude" && parsed.field === "windowReserve") {
|
|
191
|
-
const nextRecord = next;
|
|
192
|
-
delete nextRecord.claudeWindowReserve;
|
|
193
|
-
}
|
|
194
|
-
return preservingFileRoles(root, next);
|
|
162
|
+
const next = { ...record, [parsed.section]: { ...existing, [parsed.field]: value } };
|
|
163
|
+
return preservingFileRoles(root, next);
|
|
164
|
+
};
|
|
165
|
+
const mergeAgent = (record, parsed) => {
|
|
166
|
+
// Merge one field of one backend's entry, preserving the other field and the other
|
|
167
|
+
// backends. validateConfig in writeConfigMutation owns the value type, so a bad
|
|
168
|
+
// value fails there and nothing is written.
|
|
169
|
+
const backends = { ...record.backends };
|
|
170
|
+
const entry = { ...(backends[parsed.id] ?? {}) };
|
|
171
|
+
oldValue = entry[parsed.field];
|
|
172
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
173
|
+
unchanged = true;
|
|
174
|
+
return record;
|
|
175
|
+
}
|
|
176
|
+
entry[parsed.field] = value;
|
|
177
|
+
backends[parsed.id] = entry;
|
|
178
|
+
const next = { ...record, backends: backends };
|
|
179
|
+
// Writers keep one spelling: the new pi form drops its legacy alias so the file can
|
|
180
|
+
// never hold both (validateConfig's raw pass rejects a file that does).
|
|
181
|
+
if (parsed.id === "pi") {
|
|
182
|
+
const nextRecord = next;
|
|
183
|
+
if (parsed.field === "bin")
|
|
184
|
+
delete nextRecord.agentBin;
|
|
185
|
+
else
|
|
186
|
+
delete nextRecord.piArgs;
|
|
195
187
|
}
|
|
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
|
+
return preservingFileRoles(root, next);
|
|
195
|
+
};
|
|
196
|
+
const mergeRoleEntry = (record, parsed) => {
|
|
196
197
|
// Base the merge on the FILE's own roles, not the loaded (catalog-merged) map: an
|
|
197
198
|
// unrelated dotted write must not re-expand the operator's partial section to every
|
|
198
199
|
// built-in (see preserveFileRoles). A role absent from the file gets a fresh entry with
|
|
@@ -204,7 +205,7 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
204
205
|
oldValue = record.roles?.[parsed.id]?.[parsed.field];
|
|
205
206
|
if (isDeepStrictEqual(oldValue, value)) {
|
|
206
207
|
unchanged = true;
|
|
207
|
-
return
|
|
208
|
+
return record;
|
|
208
209
|
}
|
|
209
210
|
const roles = {
|
|
210
211
|
...rawRoles,
|
|
@@ -213,7 +214,22 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
213
214
|
// The merged entry is only partially typed here (the value is unknown until
|
|
214
215
|
// validation); validateConfig below owns the type truth, so the cast is safe —
|
|
215
216
|
// a type-invalid merge fails there and nothing is written.
|
|
216
|
-
return { ...
|
|
217
|
+
return { ...record, roles };
|
|
218
|
+
};
|
|
219
|
+
const result = writeConfigMutation(root, (cfg) => {
|
|
220
|
+
const record = cfg;
|
|
221
|
+
switch (parsed.kind) {
|
|
222
|
+
case "map":
|
|
223
|
+
return mergeMapEntry(record, parsed);
|
|
224
|
+
case "tier":
|
|
225
|
+
return mergeTier(record, parsed);
|
|
226
|
+
case "section":
|
|
227
|
+
return mergeSection(record, parsed);
|
|
228
|
+
case "agent":
|
|
229
|
+
return mergeAgent(record, parsed);
|
|
230
|
+
case "role":
|
|
231
|
+
return mergeRoleEntry(record, parsed);
|
|
232
|
+
}
|
|
217
233
|
});
|
|
218
234
|
if (!result.ok)
|
|
219
235
|
return result;
|
|
@@ -230,88 +246,102 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
230
246
|
};
|
|
231
247
|
}
|
|
232
248
|
return { ok: true, value, oldValue };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (problem)
|
|
257
|
-
return fail(problem);
|
|
258
|
-
}
|
|
259
|
-
let oldValue;
|
|
260
|
-
const result = writeConfigMutation(root, (cfg) => {
|
|
261
|
-
oldValue = cfg[key];
|
|
262
|
-
if (isDeepStrictEqual(oldValue, value)) {
|
|
263
|
-
unchanged = true;
|
|
264
|
-
return cfg;
|
|
249
|
+
};
|
|
250
|
+
// One named handler for a top-level key: the unknown-key refusal, the legacy
|
|
251
|
+
// provider/fallbackModel absence refusal, the per-key validator, then one writeConfigMutation
|
|
252
|
+
// whose callback applies the legacy/whole-section spelling collapses.
|
|
253
|
+
const writeTopLevelKey = (key, value) => {
|
|
254
|
+
const unknown = unknownConfigKeyError(key);
|
|
255
|
+
if (unknown)
|
|
256
|
+
return fail(unknown);
|
|
257
|
+
// Legacy `provider` / `fallbackModel` are accepted to FIX an existing value (a config
|
|
258
|
+
// that already has one keeps the old meaning), but `config set` never adds them to a
|
|
259
|
+
// config that lacks them — the new-form writers emit `model` and `fallback` only.
|
|
260
|
+
if (key === "provider" || key === "fallbackModel") {
|
|
261
|
+
const absent = (() => {
|
|
262
|
+
try {
|
|
263
|
+
const c = loadConfig(root); // fresh — bypasses the stat cache on purpose
|
|
264
|
+
return c[key] === undefined;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return false; // a broken file fails in writeConfigMutation with its own message
|
|
268
|
+
}
|
|
269
|
+
})();
|
|
270
|
+
if (absent)
|
|
271
|
+
return fail(`config set ${key}: this config has no ${key}, and a writer never adds legacy ${key} — set \`${key === "provider" ? "model" : "fallback"}\` instead`);
|
|
265
272
|
}
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// legacy aliases the new pi form now carries, so the file can never hold both.
|
|
275
|
-
// Writers keep one spelling: a whole-section `backends` set (the GUI Save path) drops the
|
|
276
|
-
// legacy agentBin/piArgs aliases the new pi form now carries, so the file can never hold
|
|
277
|
-
// both — the same collapseBackendAliases rule the whole-config writers (above and in
|
|
278
|
-
// config.ts) use, so the alias policy cannot drift between the per-key arm and the others.
|
|
279
|
-
if (key === "backends")
|
|
280
|
-
return collapseBackendAliases(next);
|
|
273
|
+
const perKeyValidator = PER_KEY_VALIDATORS[key];
|
|
274
|
+
if (perKeyValidator) {
|
|
275
|
+
const problem = perKeyValidator(value);
|
|
276
|
+
if (problem)
|
|
277
|
+
return fail(problem);
|
|
278
|
+
}
|
|
279
|
+
let oldValue;
|
|
280
|
+
let unchanged;
|
|
281
281
|
// Writers keep one spelling: a legacy top-level set drops the matching backends.pi field, so
|
|
282
282
|
// the file can never hold both (validateConfig's raw pass rejects a file that does).
|
|
283
|
-
|
|
284
|
-
const record = next;
|
|
285
|
-
const backends = { ...(record.backends ?? {}) };
|
|
286
|
-
const entry = { ...(backends.pi ?? {}) };
|
|
287
|
-
delete entry[key === "agentBin" ? "bin" : "args"];
|
|
288
|
-
if (Object.keys(entry).length === 0)
|
|
289
|
-
delete backends.pi;
|
|
290
|
-
else
|
|
291
|
-
backends.pi = entry;
|
|
292
|
-
return { ...next, backends: backends };
|
|
293
|
-
}
|
|
283
|
+
const dropLegacyPiAlias = (next, key) => dropNestedBackendField(next, "pi", key === "agentBin" ? "bin" : "args");
|
|
294
284
|
// Writers keep one spelling: setting the top-level `claudeWindowReserve` drops the nested
|
|
295
285
|
// `backends.claude.windowReserve`, so a supported `config set` never writes the rejected pair
|
|
296
286
|
// (validateConfig still refuses a hand-edited file holding both).
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
287
|
+
const dropNestedReserve = (next) => dropNestedBackendField(next, "claude", "windowReserve");
|
|
288
|
+
const result = writeConfigMutation(root, (cfg) => {
|
|
289
|
+
oldValue = cfg[key];
|
|
290
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
291
|
+
unchanged = true;
|
|
292
|
+
return cfg;
|
|
293
|
+
}
|
|
294
|
+
const next = { ...cfg, [key]: value };
|
|
295
|
+
// `roles` merges per-entry at load (overlayDefaults seeds the catalog and lets the file
|
|
296
|
+
// override one entry), so an unrelated whole-config write must not re-expand the file's
|
|
297
|
+
// own roles section to the catalog — that would silently rewrite what the operator wrote.
|
|
298
|
+
// When the key IS `roles` the value is the replacement, so it is left alone.
|
|
299
|
+
if (key !== "roles")
|
|
300
|
+
preserveFileRoles(root, next);
|
|
301
|
+
// Writers keep one spelling: a whole-section `backends` set (the GUI Save path) drops the
|
|
302
|
+
// legacy agentBin/piArgs aliases the new pi form now carries, so the file can never hold
|
|
303
|
+
// both — the same collapseBackendAliases rule the whole-config writers (above and in
|
|
304
|
+
// config.ts) use, so the alias policy cannot drift between the per-key arm and the others.
|
|
305
|
+
if (key === "backends")
|
|
306
|
+
return collapseBackendAliases(next);
|
|
307
|
+
if (key === "agentBin" || key === "piArgs")
|
|
308
|
+
return dropLegacyPiAlias(next, key);
|
|
309
|
+
if (key === "claudeWindowReserve")
|
|
310
|
+
return dropNestedReserve(next);
|
|
311
|
+
return next;
|
|
312
|
+
});
|
|
313
|
+
if (!result.ok)
|
|
314
|
+
return result;
|
|
315
|
+
if (unchanged)
|
|
316
|
+
return { ok: true, value, oldValue, unchanged: true };
|
|
317
|
+
return { ok: true, value, oldValue };
|
|
318
|
+
};
|
|
319
|
+
key = canonicalConfigKey(key);
|
|
320
|
+
const parsed = parseConfigKey(key);
|
|
321
|
+
if (parsed.kind === "error")
|
|
322
|
+
return fail(parsed.error);
|
|
323
|
+
// Shape, before the JSON.parse, for every plain-positive-integer key this verb can write —
|
|
324
|
+
// 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 (`maxConcurrentPerProvider.<provider>`,
|
|
326
|
+
// `roles.<id>.instances`, the `bootstrap.untilPlansDone` section field): refuse an exponent
|
|
327
|
+
// spelling ("4e1" → 40) that JSON.parse would read past validateConfig's integer check and
|
|
328
|
+
// silently write, naming the key and writing nothing — the twin of the per-role guards, the
|
|
329
|
+
// TUI team-key editors, and the GUI Settings rows. A non-exponent invalid falls through for
|
|
330
|
+
// validateConfig (whose message names the key). Empty clears the key below.
|
|
331
|
+
const trimmed = rawValue.trim();
|
|
332
|
+
const refusal = exponentRefusal(parsed, key, trimmed);
|
|
333
|
+
if (refusal)
|
|
334
|
+
return fail(refusal);
|
|
335
|
+
let value;
|
|
336
|
+
try {
|
|
337
|
+
value = JSON.parse(rawValue);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
value = rawValue; // not JSON: the literal string, so `set model gpt-5` needs no quotes
|
|
341
|
+
}
|
|
342
|
+
if (parsed.kind !== "top")
|
|
343
|
+
return writeDottedKey(parsed, value);
|
|
344
|
+
return writeTopLevelKey(key, value);
|
|
315
345
|
}
|
|
316
346
|
/** Set a role's provider/model/thinking/backend override set in tumwater.json (`roles.<base>`),
|
|
317
347
|
* the write behind the dashboard role drawer's Model overrides editor: one writeConfigMutation
|
|
@@ -299,7 +299,8 @@ export function loadConfigCached(root) {
|
|
|
299
299
|
/** The live resolved config for `root`: the stat-cached read (loadConfigCached) with a broken
|
|
300
300
|
* or vanished file degrading to the defaults — the one home of that fallback, shared by every
|
|
301
301
|
* mid-run reader that must keep working when tumwater.json is briefly broken (the baseline
|
|
302
|
-
* check's per-call config, the red
|
|
302
|
+
* check's per-call config, the main-red heal prompt (`mainRedHealNote`), the
|
|
303
|
+
* stall threshold), so a change to the policy
|
|
303
304
|
* (what degrades, what logs) cannot drift between them. Missing is degraded here too, unlike
|
|
304
305
|
* loadConfigCached's own contract — callers of this helper want A config, never an incident. */
|
|
305
306
|
export function liveConfig(root) {
|