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
|
@@ -80,35 +80,158 @@ function buildCandidate(root, current, entries, checkCommand) {
|
|
|
80
80
|
const customRoles = entries === undefined ? current.customRoles : entries;
|
|
81
81
|
const checkChanged = checkCommand !== undefined && (current.check?.command ?? "") !== checkCommand;
|
|
82
82
|
const unchanged = !checkChanged && isDeepStrictEqual(customRoles, current.customRoles);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
const requestedNames = requestedRoleNames(customRoles);
|
|
84
|
+
const fileRoles = fileRolesAfterRemoval(root, current.customRoles, requestedNames);
|
|
85
|
+
const candidate = { ...current, customRoles };
|
|
86
|
+
withCheckCommand(candidate, current.check, checkCommand, checkChanged);
|
|
87
|
+
withRolesSection(candidate, fileRoles.raw, fileRoles.roles);
|
|
88
|
+
return { candidate: candidate, changed: !unchanged };
|
|
89
|
+
}
|
|
90
|
+
/** The structurally valid role names a request's customRoles array carries (see
|
|
91
|
+
* applyConfigRequest's docstring): only names on structurally valid entries feed the roles
|
|
92
|
+
* strip; anything else stays for validateConfig to reject — never dereference a request
|
|
93
|
+
* entry before validation. */
|
|
94
|
+
function requestedRoleNames(customRoles) {
|
|
86
95
|
const requestedNames = new Set();
|
|
87
96
|
for (const entry of customRoles) {
|
|
88
97
|
if (isJsonObject(entry) && typeof entry.name === "string")
|
|
89
98
|
requestedNames.add(entry.name);
|
|
90
99
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
return requestedNames;
|
|
101
|
+
}
|
|
102
|
+
/** The FILE's own roles section (not the loaded, catalog-merged map — this whole-config
|
|
103
|
+
* write must not re-expand a partial section to every built-in, preserveFileRoles' rule)
|
|
104
|
+
* with the removed custom roles' entries stripped: only the requested names the caller
|
|
105
|
+
* passes are removed, and only from what the file actually holds. Returns both the raw value
|
|
106
|
+
* (for when the file had no roles section) and the stripped map. */
|
|
107
|
+
function fileRolesAfterRemoval(root, currentRoles, requestedNames) {
|
|
108
|
+
const raw = rawConfigObject(root).roles;
|
|
109
|
+
const roles = isJsonObject(raw) ? { ...(raw) } : {};
|
|
110
|
+
for (const c of currentRoles) {
|
|
98
111
|
if (!requestedNames.has(c.name))
|
|
99
112
|
delete roles[c.name];
|
|
100
113
|
}
|
|
101
|
-
|
|
114
|
+
return { raw, roles };
|
|
115
|
+
}
|
|
116
|
+
/** Replace only `check.command` on the candidate when the request declares one, preserving
|
|
117
|
+
* the rest of the check section (gateCommand/mergeCommand/timeoutSeconds). */
|
|
118
|
+
function withCheckCommand(candidate, currentCheck, checkCommand, checkChanged) {
|
|
102
119
|
if (checkChanged && checkCommand !== undefined) {
|
|
103
|
-
|
|
104
|
-
// only `command` is declared by the request.
|
|
105
|
-
candidate.check = { ...current.check, command: checkCommand };
|
|
120
|
+
candidate.check = { ...currentCheck, command: checkCommand };
|
|
106
121
|
}
|
|
122
|
+
}
|
|
123
|
+
/** Set the candidate's roles section to the stripped map, or drop it when the file had none. */
|
|
124
|
+
function withRolesSection(candidate, rawRolesCandidate, roles) {
|
|
107
125
|
if (rawRolesCandidate === undefined)
|
|
108
126
|
delete candidate.roles;
|
|
109
127
|
else
|
|
110
128
|
candidate.roles = roles;
|
|
111
|
-
|
|
129
|
+
}
|
|
130
|
+
/** Parse the request JSON into its customRoles entries and raw `check` value: the isJsonObject
|
|
131
|
+
* throw, the old-key normalization and the top-level disallowed-key scan into `ignored`; the
|
|
132
|
+
* `check` value is returned untouched for checkCommandFrom. */
|
|
133
|
+
function parseConfigRequest(raw, ignored) {
|
|
134
|
+
const unnormalized = JSON.parse(raw);
|
|
135
|
+
if (!isJsonObject(unnormalized))
|
|
136
|
+
throw new Error(`config request must be a JSON object (got ${show(unnormalized)})`);
|
|
137
|
+
// Old key spellings read as the new ones; a request setting both fails with the pair named.
|
|
138
|
+
const normalized = normalizeConfigKeys(unnormalized);
|
|
139
|
+
if (normalized.problems.length > 0)
|
|
140
|
+
throw new Error(normalized.problems.join("; "));
|
|
141
|
+
const parsed = normalized.raw;
|
|
142
|
+
for (const key of Object.keys(parsed)) {
|
|
143
|
+
if (key !== "customRoles" && key !== "check")
|
|
144
|
+
ignored.push(key);
|
|
145
|
+
}
|
|
146
|
+
return { entries: parsed.customRoles, check: parsed.check };
|
|
147
|
+
}
|
|
148
|
+
/** Extract the declared check command from a request's `check` object: the isJsonObject throw,
|
|
149
|
+
* the sub-key scan that pushes the dropped `check.<key>` names into `ignored`, and the
|
|
150
|
+
* non-blank `command` (or its collected drop). */
|
|
151
|
+
function checkCommandFrom(check, ignored) {
|
|
152
|
+
// A check request is read only for its `command` (a non-blank string). Every other
|
|
153
|
+
// sub-key — and a blank `command` — is dropped into `ignored`, so it can never turn the
|
|
154
|
+
// safety gates off; a blank/absent command simply declares nothing.
|
|
155
|
+
if (check !== undefined) {
|
|
156
|
+
const checkReq = check;
|
|
157
|
+
if (!isJsonObject(checkReq))
|
|
158
|
+
throw new Error(`config request's check must be an object (got ${show(checkReq)})`);
|
|
159
|
+
for (const key of Object.keys(checkReq)) {
|
|
160
|
+
if (key !== "command")
|
|
161
|
+
ignored.push(`check.${key}`);
|
|
162
|
+
}
|
|
163
|
+
if (isNonBlankString(checkReq.command))
|
|
164
|
+
return checkReq.command;
|
|
165
|
+
else if (checkReq.command !== undefined)
|
|
166
|
+
ignored.push("check.command");
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
/** Verify a declared check.command before it is written (part 1/2): a declared check is
|
|
171
|
+
* fail-closed — the gate's pre-check rejects a red run and a failure that reproduces on main
|
|
172
|
+
* parks every queued pin behind a red main no role can repair by code — so the declared
|
|
173
|
+
* command must be observed green in the requesting worktree BEFORE it is written. Only a
|
|
174
|
+
* `passed` outcome writes; failed, timed-out, or skipped writes nothing and returns an error
|
|
175
|
+
* naming what was observed. Each candidate is run at `check.cwd` resolved against the
|
|
176
|
+
* worktree (default `.`) under the config-resolved timeout, and when a `check.gateCommand` is
|
|
177
|
+
* set and differs the gate command is verified first too — the declared command is the
|
|
178
|
+
* fail-closed promise, the gate command is what the gate scope will actually run — so a green
|
|
179
|
+
* here is a green at the gate for every command the write leaves live. */
|
|
180
|
+
async function verifyDeclaredCheck(root, wt, entries, checkCommand, runCheck) {
|
|
181
|
+
// Read the current config only for the guard — the write reloads fresh under the lock.
|
|
182
|
+
const currentForGuard = loadConfig(root);
|
|
183
|
+
// Pre-validate what the request would write BEFORE the declared check runs: a request
|
|
184
|
+
// that pairs a valid check.command with an invalid customRoles entry must be rejected
|
|
185
|
+
// without ever spawning the check (it would fail validation anyway). The locked write
|
|
186
|
+
// re-loads fresh and re-validates, so a config change between here and the write is
|
|
187
|
+
// still caught.
|
|
188
|
+
const { candidate: preCandidate } = buildCandidate(root, currentForGuard, entries, checkCommand);
|
|
189
|
+
validateConfig(collapseBackendAliases(preCandidate));
|
|
190
|
+
for (const check of guardChecks(currentForGuard, wt, checkCommand)) {
|
|
191
|
+
const outcome = await runCheck(wt, check, currentForGuard);
|
|
192
|
+
if (outcome.status !== "passed") {
|
|
193
|
+
throw new Error(`check.command not declared: \`${outcome.script}\` ${describeGuardFailure(outcome)} in the worktree`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** The locked whole-config write of a config request: the load-to-write sequence is one
|
|
198
|
+
* critical section under withConfigLock — a concurrent `config set`/dashboard write between
|
|
199
|
+
* the load and this write must not be dropped by the whole-file overwrite (see
|
|
200
|
+
* writeConfigMutation). Returns whether the request changed nothing and the now-live custom
|
|
201
|
+
* role names. */
|
|
202
|
+
function writeConfigRequest(root, entries, checkCommand) {
|
|
203
|
+
let applied = [];
|
|
204
|
+
let unchanged = false;
|
|
205
|
+
withConfigLock(root, () => {
|
|
206
|
+
const current = loadConfig(root); // fresh — bypasses the stat cache: a writer sees the latest file
|
|
207
|
+
const { candidate, changed } = buildCandidate(root, current, entries, checkCommand);
|
|
208
|
+
// Nothing the request would change: write nothing, like writeConfigMutation's no-op skip.
|
|
209
|
+
if (!changed) {
|
|
210
|
+
unchanged = true;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Collapse legacy/new pi spellings before writing so a merged config (which always
|
|
214
|
+
// carries the default piArgs) never lands both forms in the file — the next load's raw
|
|
215
|
+
// pass rejects a file holding both `piArgs` and `backends.pi.args`.
|
|
216
|
+
const collapsed = collapseBackendAliases(candidate);
|
|
217
|
+
validateConfig(collapsed); // throws listing every problem — nothing is written on failure
|
|
218
|
+
writeJsonAtomic(configPath(root), collapsed, true);
|
|
219
|
+
applied = collapsed.customRoles.map((c) => c.name);
|
|
220
|
+
});
|
|
221
|
+
return { unchanged, applied };
|
|
222
|
+
}
|
|
223
|
+
/** Delete the request on EVERY path — success, rejection, malformed JSON — so it can never be
|
|
224
|
+
* staged by commitAll, reach a review gate, or retry forever. A failed unlink is surfaced as
|
|
225
|
+
* an error so the caller logs it (the file would otherwise survive into the diff); a passed-in
|
|
226
|
+
* error outranks the failed-deletion message. Returns the error to surface. */
|
|
227
|
+
function discardConfigRequest(requestFile, error) {
|
|
228
|
+
try {
|
|
229
|
+
fs.unlinkSync(requestFile);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
return error ?? `config request file could not be deleted: ${errorMessage(err)}`;
|
|
233
|
+
}
|
|
234
|
+
return error;
|
|
112
235
|
}
|
|
113
236
|
/** Consume the role's config-write request file (plans/portability.md §3/7). After its pi
|
|
114
237
|
* run the director may leave `.tumwater-config-request.json` at its worktree root:
|
|
@@ -154,7 +277,9 @@ function buildCandidate(root, current, entries, checkCommand) {
|
|
|
154
277
|
* unless the write happened); `ignored` lists the discarded keys (top-level keys and, named
|
|
155
278
|
* `check.<key>`, the check sub-keys that were dropped); `error`, when set, names why nothing
|
|
156
279
|
* (or only part of the work) was done — the caller turns it and `ignored` into warning events.
|
|
157
|
-
* `unchanged` is set when the request changes nothing; nothing is written.
|
|
280
|
+
* `unchanged` is set when the request changes nothing; nothing is written. The body delegates
|
|
281
|
+
* each phase to a named helper — parseConfigRequest, checkCommandFrom, verifyDeclaredCheck,
|
|
282
|
+
* writeConfigRequest and discardConfigRequest. */
|
|
158
283
|
export async function applyConfigRequest(root, wt, runCheck = defaultCheckGuard) {
|
|
159
284
|
const requestFile = configRequestPath(wt);
|
|
160
285
|
const raw = readTextOrNull(requestFile);
|
|
@@ -165,98 +290,24 @@ export async function applyConfigRequest(root, wt, runCheck = defaultCheckGuard)
|
|
|
165
290
|
let error;
|
|
166
291
|
let unchanged;
|
|
167
292
|
try {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
throw new Error(
|
|
175
|
-
const parsed = normalized.raw;
|
|
176
|
-
for (const key of Object.keys(parsed)) {
|
|
177
|
-
if (key !== "customRoles" && key !== "check")
|
|
178
|
-
ignored.push(key);
|
|
179
|
-
}
|
|
180
|
-
// A check request is read only for its `command` (a non-blank string). Every other
|
|
181
|
-
// sub-key — and a blank `command` — is dropped into `ignored`, so it can never turn the
|
|
182
|
-
// safety gates off; a blank/absent command simply declares nothing.
|
|
183
|
-
let checkCommand;
|
|
184
|
-
if (parsed.check !== undefined) {
|
|
185
|
-
const checkReq = parsed.check;
|
|
186
|
-
if (!isJsonObject(checkReq))
|
|
187
|
-
throw new Error(`config request's check must be an object (got ${show(checkReq)})`);
|
|
188
|
-
for (const key of Object.keys(checkReq)) {
|
|
189
|
-
if (key !== "command")
|
|
190
|
-
ignored.push(`check.${key}`);
|
|
191
|
-
}
|
|
192
|
-
if (isNonBlankString(checkReq.command))
|
|
193
|
-
checkCommand = checkReq.command;
|
|
194
|
-
else if (checkReq.command !== undefined)
|
|
195
|
-
ignored.push("check.command");
|
|
196
|
-
}
|
|
197
|
-
const entriesRaw = parsed.customRoles;
|
|
198
|
-
if (entriesRaw !== undefined && !Array.isArray(entriesRaw))
|
|
199
|
-
throw new Error(`config request's customRoles must be an array (got ${show(entriesRaw)})`);
|
|
200
|
-
const entries = entriesRaw;
|
|
201
|
-
// Verified declaration (part 1/2): a declared check is fail-closed — the gate's pre-check
|
|
202
|
-
// rejects a red run and a failure that reproduces on main parks every queued pin behind a
|
|
203
|
-
// red main no role can repair by code — so the declared command must be observed green in
|
|
204
|
-
// the requesting worktree BEFORE it is written. Only a `passed` outcome writes; failed,
|
|
205
|
-
// timed-out, or skipped writes nothing and returns an error naming what was observed. Each
|
|
206
|
-
// candidate is run at `check.cwd` resolved against the worktree (default `.`) under the
|
|
207
|
-
// config-resolved timeout, and when a `check.gateCommand` is set and differs the gate
|
|
208
|
-
// command is verified first too — the declared command is the fail-closed promise, the
|
|
209
|
-
// gate command is what the gate scope will actually run — so a green here is a green at
|
|
210
|
-
// the gate for every command the write leaves live.
|
|
293
|
+
const { entries, check } = parseConfigRequest(raw, ignored);
|
|
294
|
+
const checkCommand = checkCommandFrom(check, ignored);
|
|
295
|
+
// The array-shape check runs after the check-subkey scan in checkCommandFrom, preserving the
|
|
296
|
+
// original ordering so a doubly-malformed request still reports its dropped `check.<key>`
|
|
297
|
+
// warnings before the malformed-customRoles error.
|
|
298
|
+
if (entries !== undefined && !Array.isArray(entries))
|
|
299
|
+
throw new Error(`config request's customRoles must be an array (got ${show(entries)})`);
|
|
211
300
|
if (checkCommand !== undefined) {
|
|
212
|
-
|
|
213
|
-
const currentForGuard = loadConfig(root);
|
|
214
|
-
// Pre-validate what the request would write BEFORE the declared check runs: a request
|
|
215
|
-
// that pairs a valid check.command with an invalid customRoles entry must be rejected
|
|
216
|
-
// without ever spawning the check (it would fail validation anyway). The locked write
|
|
217
|
-
// re-loads fresh and re-validates, so a config change between here and the write is
|
|
218
|
-
// still caught.
|
|
219
|
-
const { candidate: preCandidate } = buildCandidate(root, currentForGuard, entries, checkCommand);
|
|
220
|
-
validateConfig(collapseBackendAliases(preCandidate));
|
|
221
|
-
for (const check of guardChecks(currentForGuard, wt, checkCommand)) {
|
|
222
|
-
const outcome = await runCheck(wt, check, currentForGuard);
|
|
223
|
-
if (outcome.status !== "passed") {
|
|
224
|
-
throw new Error(`check.command not declared: \`${outcome.script}\` ${describeGuardFailure(outcome)} in the worktree`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
301
|
+
await verifyDeclaredCheck(root, wt, entries, checkCommand, runCheck);
|
|
227
302
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
withConfigLock(root, () => {
|
|
232
|
-
const current = loadConfig(root); // fresh — bypasses the stat cache: a writer sees the latest file
|
|
233
|
-
const { candidate, changed } = buildCandidate(root, current, entries, checkCommand);
|
|
234
|
-
// Nothing the request would change: write nothing, like writeConfigMutation's no-op skip.
|
|
235
|
-
if (!changed) {
|
|
236
|
-
unchanged = true;
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
// Collapse legacy/new pi spellings before writing so a merged config (which always
|
|
240
|
-
// carries the default piArgs) never lands both forms in the file — the next load's raw
|
|
241
|
-
// pass rejects a file holding both `piArgs` and `backends.pi.args`.
|
|
242
|
-
const collapsed = collapseBackendAliases(candidate);
|
|
243
|
-
validateConfig(collapsed); // throws listing every problem — nothing is written on failure
|
|
244
|
-
writeJsonAtomic(configPath(root), collapsed, true);
|
|
245
|
-
applied = collapsed.customRoles.map((c) => c.name);
|
|
246
|
-
});
|
|
303
|
+
const written = writeConfigRequest(root, entries, checkCommand);
|
|
304
|
+
applied = written.applied;
|
|
305
|
+
unchanged = written.unchanged ? true : undefined;
|
|
247
306
|
}
|
|
248
307
|
catch (err) {
|
|
249
308
|
applied = []; // no write happened, or it must not be reported as applied
|
|
250
309
|
error = errorMessage(err);
|
|
251
310
|
}
|
|
252
|
-
|
|
253
|
-
// staged by commitAll, reach a review gate, or retry forever. A failed unlink is surfaced as
|
|
254
|
-
// an error so the caller logs it (the file would otherwise survive into the diff).
|
|
255
|
-
try {
|
|
256
|
-
fs.unlinkSync(requestFile);
|
|
257
|
-
}
|
|
258
|
-
catch (err) {
|
|
259
|
-
error ??= `config request file could not be deleted: ${errorMessage(err)}`;
|
|
260
|
-
}
|
|
311
|
+
error = discardConfigRequest(requestFile, error);
|
|
261
312
|
return unchanged ? { applied, ignored, error, unchanged } : { applied, ignored, error };
|
|
262
313
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* its default (config.ts), and its allow-list entry sit beside each other. Split out of the former
|
|
6
6
|
* types.ts grab-bag — the runtime types a run produces now live beside their consumers
|
|
7
7
|
* (RunResult and RunOutcome in run-outcome.ts, RunnerState in runner-state.ts, HarnessEvent
|
|
8
|
-
* in events.ts, MergeEntry in merge-queue.ts, and the pi-run types in run
|
|
8
|
+
* in events.ts, MergeEntry in merge-queue.ts, and the pi-run types in agent-run.ts and
|
|
9
9
|
* pi-run-result.ts) — so the schema the file on disk is validated against and the runtime
|
|
10
10
|
* state the harness runs with
|
|
11
11
|
* are two layers, changed for different reasons by different readers: a tumwater.json knob
|
|
@@ -59,8 +59,6 @@ export const TOP_LEVEL_KEYS = [
|
|
|
59
59
|
"check",
|
|
60
60
|
"piArgs",
|
|
61
61
|
"maxConcurrent",
|
|
62
|
-
"maxConcurrentPerProvider",
|
|
63
|
-
"claudeWindowReserve",
|
|
64
62
|
"mergeBatchMax",
|
|
65
63
|
"maxConcurrentChecks",
|
|
66
64
|
"worktreeSlots",
|
|
@@ -93,16 +91,16 @@ export const TOP_LEVEL_KEYS = [
|
|
|
93
91
|
export const BACKOFF_KEYS = ["initialSeconds", "factor", "maxSeconds"];
|
|
94
92
|
/** The keys an `backends.<id>` entry may hold; validation feeds checkKnownKeys so a typo'd
|
|
95
93
|
* field (`backends.claude.binary`) is a load error rather than a silently inert entry.
|
|
96
|
-
* `configDir
|
|
97
|
-
* so
|
|
94
|
+
* `configDir` is claude/codex-only (CLAUDE_AGENT_KEYS / CODEX_BACKEND_KEYS),
|
|
95
|
+
* so it is not silently inert on pi/opencode. */
|
|
98
96
|
export const BACKEND_KEYS = ["bin", "args"];
|
|
99
|
-
/** The keys a claude `backends.claude` entry may hold — the shared agent keys plus `configDir
|
|
100
|
-
*
|
|
101
|
-
export const CLAUDE_AGENT_KEYS = [...BACKEND_KEYS, "configDir"
|
|
97
|
+
/** The keys a claude `backends.claude` entry may hold — the shared agent keys plus `configDir`,
|
|
98
|
+
* the per-backend field only the claude adapter reads. */
|
|
99
|
+
export const CLAUDE_AGENT_KEYS = [...BACKEND_KEYS, "configDir"];
|
|
102
100
|
/** The keys a codex `backends.codex` entry may hold — the same as claude's: the shared agent keys
|
|
103
|
-
* plus `configDir
|
|
104
|
-
* §4)
|
|
105
|
-
export const CODEX_BACKEND_KEYS = [...BACKEND_KEYS, "configDir"
|
|
101
|
+
* plus `configDir`, which becomes `CODEX_HOME` for the codex processes (plans/codex-backend.md
|
|
102
|
+
* §4). */
|
|
103
|
+
export const CODEX_BACKEND_KEYS = [...BACKEND_KEYS, "configDir"];
|
|
106
104
|
/** The keys tumwater.json's `bootstrap` section may hold. */
|
|
107
105
|
export const BOOTSTRAP_KEYS = ["untilPlansDone"];
|
|
108
106
|
export const ROLE_ENTRY_KEYS = [
|
|
@@ -3,7 +3,7 @@ import { checkKnownKeys, show } from "./config-field-checks.js";
|
|
|
3
3
|
import { isJsonObject } from "../files/json-object.js";
|
|
4
4
|
import { isNonBlankString } from "../text/text.js";
|
|
5
5
|
import { typoSuffix } from "../text/suggest.js";
|
|
6
|
-
import { isBackendId, parseModelSelector } from "./model-selector.js";
|
|
6
|
+
import { isBackendId, parseModelSelector } from "./config-model-selector.js";
|
|
7
7
|
/** Model-selector and fallback-chain checks that validateConfig (config-validation.ts) runs
|
|
8
8
|
* on each model-choosing section: empty selector halves, `fallback` lists and steps, and an
|
|
9
9
|
* entry's `backend` against its model. Each takes the `problems` list to append to and reads
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { BACKEND_IDS, BACKEND_KEYS, CLAUDE_AGENT_KEYS, CODEX_BACKEND_KEYS, HARNESS_PI_FLAGS, } from "./config-schema.js";
|
|
13
13
|
import { isJsonObject } from "../files/json-object.js";
|
|
14
14
|
import { typoSuffix } from "../text/suggest.js";
|
|
15
|
-
import { isBackendId } from "./model-selector.js";
|
|
15
|
+
import { isBackendId } from "./config-model-selector.js";
|
|
16
16
|
import { reservedPassthroughFlag } from "../agent/backend-traits.js";
|
|
17
17
|
import { checkObjectSection, checkReservedArgs, show, } from "./config-field-checks.js";
|
|
18
18
|
/** Validate the agent-execution block: the bare `backend`, `agentBin`, `baseBranch` and
|
|
@@ -27,7 +27,7 @@ export function validateAgentsBlock(r, problems, rawFile, helpers) {
|
|
|
27
27
|
checkString(r, "", "agentBin", false);
|
|
28
28
|
// The agent backend for bare/no-model selectors (plans/agent-backends.md "Config"): a
|
|
29
29
|
// non-blank id from the schema's reserved set. An unknown id would fall back to pi at run
|
|
30
|
-
// time (src/agent/run
|
|
30
|
+
// time (src/agent/agent-run.ts), silently running the wrong backend against a named one.
|
|
31
31
|
checkString(r, "", "backend", false);
|
|
32
32
|
if (typeof r.backend === "string" && !isBackendId(r.backend))
|
|
33
33
|
problems.push(`backend must name a backend (${BACKEND_IDS.join(", ")}; got ${show(r.backend)})${typoSuffix(r.backend, BACKEND_IDS)}`);
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/** The per-role and cross-field section rules of tumwater.json validation: the `customRoles`
|
|
2
2
|
* collect-and-check block, the `roles.<id>` entry block, `review.afterMerge`'s name check,
|
|
3
|
-
* the per-role daily cost caps and quiet hours, `
|
|
4
|
-
* `claudeWindowReserve` and its mutually-exclusive spelling pair, and the `idleBackoff`
|
|
3
|
+
* the per-role daily cost caps and quiet hours, and the `idleBackoff`
|
|
5
4
|
* cross-field pair. Extracted from the single validateConfig gate (config-validation.ts) so a
|
|
6
5
|
* per-role or cross-field edit lands as a per-theme diff instead of a whole-gate one; this
|
|
7
6
|
* module runs once, at the gate's end, after every other section has appended its problems,
|
|
8
7
|
* so the overall problem order is unchanged. These rules sit on top of the generic machinery
|
|
9
8
|
* in config-field-checks.ts; the helpers passed in are the closures the gate binds once (the
|
|
10
|
-
* problems-bound checkers and the shared model-triple
|
|
9
|
+
* problems-bound checkers and the shared model-triple sequence), and the
|
|
11
10
|
* cross-cutting `customNames` set — collected in the customRoles block, consumed by the
|
|
12
11
|
* roles.<id>, afterMerge, cap and quiet-hour blocks — is owned here, so validateConfig
|
|
13
12
|
* passes no set across the boundary. */
|
|
@@ -18,11 +17,7 @@ import { isJsonObject } from "../files/json-object.js";
|
|
|
18
17
|
import { isNonBlankString } from "../text/text.js";
|
|
19
18
|
import { parseQuietHours } from "../scheduling/quiet-hours.js";
|
|
20
19
|
import { TUMWATER_SOURCE } from "../events/events.js";
|
|
21
|
-
import { DOLLAR_CAP,
|
|
22
|
-
const DURATION_NON_NEGATIVE = {
|
|
23
|
-
ok: (n) => n >= 0 && n <= MAX_DURATION_SECONDS,
|
|
24
|
-
what: `a number of 0 or more, at most ${MAX_DURATION_SECONDS}`,
|
|
25
|
-
};
|
|
20
|
+
import { DOLLAR_CAP, DURATION_NON_NEGATIVE, checkKnownKeys, checkKnownRoleId, checkNumberField, show, } from "./config-field-checks.js";
|
|
26
21
|
/** A per-role instance count (`roles.feature.instances`): an integer from 1 to 8. Bounded so
|
|
27
22
|
* a typo like `instances: 800` cannot spawn hundreds of runners, and integer so
|
|
28
23
|
* `feature-1.5` — which no role id can name — is refused. */
|
|
@@ -52,7 +47,7 @@ function checkPerRoleMap(r, key, expectation, customNames, problems, checkValue)
|
|
|
52
47
|
}
|
|
53
48
|
/** Validate the per-role and cross-field sections that close the gate: `customRoles`, the
|
|
54
49
|
* `roles.<id>` entries, `review.afterMerge`'s role names, the per-role caps and quiet hours,
|
|
55
|
-
*
|
|
50
|
+
* and the `idleBackoff` cross-field pair,
|
|
56
51
|
* each appended to `problems` in the order the gate produced. The single call site in
|
|
57
52
|
* validateConfig is the point where these problems were appended, so the order across the
|
|
58
53
|
* whole pass is unchanged. */
|
|
@@ -62,8 +57,6 @@ export function validateRolesAndCrossField(r, problems, helpers) {
|
|
|
62
57
|
validateReviewAfterMerge(r, problems, customNames);
|
|
63
58
|
validatePerRoleCostCaps(r, problems, customNames);
|
|
64
59
|
validatePerRoleQuietHours(r, problems, customNames);
|
|
65
|
-
validateMaxConcurrentPerProvider(r, problems);
|
|
66
|
-
validateClaudeWindowReserve(r, problems, helpers);
|
|
67
60
|
validateIdleBackoff(r, problems);
|
|
68
61
|
}
|
|
69
62
|
/** Collect the `customRoles` names and refuse this block's bad shapes, appending to
|
|
@@ -215,47 +208,6 @@ function validatePerRoleQuietHours(r, problems, customNames) {
|
|
|
215
208
|
problems.push(parsed.error);
|
|
216
209
|
});
|
|
217
210
|
}
|
|
218
|
-
/** Validate per-provider concurrency caps (plans/provider-concurrency.md §1): a map from
|
|
219
|
-
* provider key to a positive integer. Unlike the per-role maps, an unknown provider name is
|
|
220
|
-
* NOT a validation error — providers live in pi's models.json, which validation does not
|
|
221
|
-
* read, and a fallback's provider may only come into use later (doctor warns in part 5/5).
|
|
222
|
-
* Each value must be a positive integer under a non-empty key. */
|
|
223
|
-
function validateMaxConcurrentPerProvider(r, problems) {
|
|
224
|
-
if (r.maxConcurrentPerProvider !== undefined) {
|
|
225
|
-
const map = r.maxConcurrentPerProvider;
|
|
226
|
-
if (!isJsonObject(map)) {
|
|
227
|
-
problems.push(`maxConcurrentPerProvider must be an object mapping provider names to concurrency caps (got ${show(map)})`);
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
for (const [provider, value] of Object.entries(map)) {
|
|
231
|
-
if (provider.trim() === "") {
|
|
232
|
-
problems.push(`maxConcurrentPerProvider.<empty> — a provider key must not be empty`);
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
if (typeof value !== "number" || !Number.isFinite(value) || !POSITIVE_INTEGER.ok(value)) {
|
|
236
|
-
problems.push(`maxConcurrentPerProvider.${provider} must be ${POSITIVE_INTEGER.what} (got ${show(value)})`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
/** Validate `claudeWindowReserve` (the subscription-window reserve: a map from window type to
|
|
243
|
-
* a fraction, 0 < f <= 1) and its mutually-exclusive spelling pair with
|
|
244
|
-
* `backends.claude.windowReserve`. */
|
|
245
|
-
function validateClaudeWindowReserve(r, problems, helpers) {
|
|
246
|
-
const { checkWindowReserve } = helpers;
|
|
247
|
-
// The subscription-window reserve: a map from window type to a fraction, 0 < f <= 1.
|
|
248
|
-
if (r.claudeWindowReserve !== undefined)
|
|
249
|
-
checkWindowReserve("claudeWindowReserve", r.claudeWindowReserve);
|
|
250
|
-
// The reserve's two spellings are mutually exclusive (plans/codex-backend.md part 2b/9): the
|
|
251
|
-
// nested key is the current one; `claudeWindowReserve` is kept as claude's older spelling. A
|
|
252
|
-
// file holding both is ambiguous about which drives the window, so it is rejected naming both.
|
|
253
|
-
if (r.claudeWindowReserve !== undefined &&
|
|
254
|
-
isJsonObject(r.backends) &&
|
|
255
|
-
isJsonObject(r.backends.claude) &&
|
|
256
|
-
r.backends.claude.windowReserve !== undefined)
|
|
257
|
-
problems.push("claudeWindowReserve and backends.claude.windowReserve set the same value — keep one (backends.claude.windowReserve is the current spelling; claudeWindowReserve is its old name)");
|
|
258
|
-
}
|
|
259
211
|
/** Validate the `idleBackoff` cross-field pair (judged on the merged config, where both sides
|
|
260
212
|
* are always present; on a raw file only when the file itself names both): scheduleBackoff
|
|
261
213
|
* clamps every idle wait — the first included — to min(initialSeconds, maxSeconds), so a
|
|
@@ -7,8 +7,8 @@ import path from "node:path";
|
|
|
7
7
|
import { isNonBlankString, tooLongMessage } from "../text/text.js";
|
|
8
8
|
import { PREFILL_REASON } from "../text/phrases.js";
|
|
9
9
|
import { parseQuietHours } from "../scheduling/quiet-hours.js";
|
|
10
|
-
import { isBackendId } from "./model-selector.js";
|
|
11
|
-
import { AT_LEAST_ONE, checkBooleanField, checkKnownKeys, checkModelTripleField, checkNumberField, checkObjectSection, codexConfigKey, checkStringArrayField, checkStringField, NON_NEGATIVE, NON_NEGATIVE_OR_DISABLED, DOLLAR_CAP,
|
|
10
|
+
import { isBackendId } from "./config-model-selector.js";
|
|
11
|
+
import { AT_LEAST_ONE, checkBooleanField, checkKnownKeys, checkModelTripleField, checkNumberField, checkObjectSection, codexConfigKey, checkStringArrayField, checkStringField, NON_NEGATIVE, NON_NEGATIVE_OR_DISABLED, DOLLAR_CAP, DURATION_NON_NEGATIVE, DURATION_OR_DISABLED, DURATION_POSITIVE, POSITIVE, POSITIVE_INTEGER, show, typeName, } from "./config-field-checks.js";
|
|
12
12
|
import { checkEntryBackend, checkRetiredSelector, checkSelectorHalves, } from "./config-selector-checks.js";
|
|
13
13
|
import { validateAgentsBlock } from "./config-validation-agents.js";
|
|
14
14
|
import { validateFallbackBlock, validateModelBlock, } from "./config-validation-model.js";
|
|
@@ -24,18 +24,6 @@ import { validateRolesAndCrossField } from "./config-validation-roles.js";
|
|
|
24
24
|
* unknown-key and known-role guards, the NumberRule table, and the per-field type checkers —
|
|
25
25
|
* lives in config-field-checks.ts; this module is the section-by-section rules plus the
|
|
26
26
|
* cross-field checks, and the one export gate (validateConfig) for them. */
|
|
27
|
-
const DURATION_POSITIVE = {
|
|
28
|
-
ok: (n) => n > 0 && n <= MAX_DURATION_SECONDS,
|
|
29
|
-
what: `a number greater than 0, at most ${MAX_DURATION_SECONDS}`,
|
|
30
|
-
};
|
|
31
|
-
const DURATION_NON_NEGATIVE = {
|
|
32
|
-
ok: (n) => n >= 0 && n <= MAX_DURATION_SECONDS,
|
|
33
|
-
what: `a number of 0 or more, at most ${MAX_DURATION_SECONDS}`,
|
|
34
|
-
};
|
|
35
|
-
const DURATION_OR_DISABLED = {
|
|
36
|
-
ok: (n) => n >= 0 && n <= MAX_DURATION_SECONDS,
|
|
37
|
-
what: `a number of 0 or more, at most ${MAX_DURATION_SECONDS} (0 disables)`,
|
|
38
|
-
};
|
|
39
27
|
/** Validate tumwater.json values, so a typo fails fast with an actionable message instead
|
|
40
28
|
* of misbehaving at runtime — e.g. a non-numeric runTimeoutSeconds becomes NaN and kills
|
|
41
29
|
* every pi run instantly, a non-numeric logMaxBytes rotates the event log on every write, an
|
|
@@ -72,37 +60,16 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
72
60
|
if (typeof value === "string" && value.length > max)
|
|
73
61
|
problems.push(tooLongMessage(label, value.length, max, PREFILL_REASON));
|
|
74
62
|
};
|
|
75
|
-
// The window-reserve map validation shared by a backend's nested `windowReserve` (via
|
|
76
|
-
// checkBackendConfig) and the top-level `claudeWindowReserve` legacy spelling, so their
|
|
77
|
-
// object/empty-type/fraction messages cannot drift. `label` is the message path — the
|
|
78
|
-
// `.windowReserve`-suffixed prefix for a backend, `claudeWindowReserve` at top level.
|
|
79
|
-
const checkWindowReserve = (label, value) => {
|
|
80
|
-
if (!isJsonObject(value)) {
|
|
81
|
-
problems.push(`${label} must be an object mapping window types to fractions (got ${show(value)})`);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
for (const [type, v] of Object.entries(value)) {
|
|
85
|
-
if (type.trim() === "")
|
|
86
|
-
problems.push(`${label}.<empty> — a window type must not be empty`);
|
|
87
|
-
else if (typeof v !== "number" || !Number.isFinite(v) || v <= 0 || v > 1)
|
|
88
|
-
problems.push(`${label}.${type} must be a fraction above 0 and at most 1 (got ${show(v)})`);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
63
|
// claude's and codex's per-backend config dir (plans/notes-command.md "Keeping funes out
|
|
92
64
|
// of team runs" and plans/codex-backend.md §4): a leading `~` expands; the value must be an
|
|
93
65
|
// absolute path after expansion, or claude would resolve a relative CLAUDE_CONFIG_DIR (and
|
|
94
66
|
// codex a relative CODEX_HOME) from the run's cwd and write transcripts somewhere
|
|
95
|
-
// unexpected. Only claude and codex accept the key, so it is inert nowhere else.
|
|
96
|
-
// reserve spelling (part 2b/9): the same 0 < f ≤ 1 rule the top-level `claudeWindowReserve`
|
|
97
|
-
// map gets, so a bad entry can never silently no-op a reserve.
|
|
67
|
+
// unexpected. Only claude and codex accept the key, so it is inert nowhere else.
|
|
98
68
|
const checkBackendConfig = (entry, prefix) => {
|
|
99
69
|
checkString(entry, `${prefix}.`, "configDir", false);
|
|
100
70
|
if (typeof entry.configDir === "string" && entry.configDir !== "")
|
|
101
71
|
if (!path.isAbsolute(expandHomeTilde(entry.configDir, os.homedir())))
|
|
102
72
|
problems.push(`${prefix}.configDir must be an absolute path after ~ expansion (got ${show(entry.configDir)})`);
|
|
103
|
-
if (entry.windowReserve === undefined)
|
|
104
|
-
return;
|
|
105
|
-
checkWindowReserve(`${prefix}.windowReserve`, entry.windowReserve);
|
|
106
73
|
};
|
|
107
74
|
// codex's `-c <key>=…` / `--config <key>=…` passthrough (plans/codex-backend.md §2/§3): a
|
|
108
75
|
// key tumwater sets on every run (HARNESS_CODEX_CONFIG_KEYS) — or one the write guard owns,
|
|
@@ -227,7 +194,7 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
227
194
|
if (!parsed.ok)
|
|
228
195
|
problems.push(parsed.error);
|
|
229
196
|
}
|
|
230
|
-
// The operator notify hook (src/events/notify.ts): absent or empty string means off,
|
|
197
|
+
// The operator notify hook (src/events/event-notify.ts): absent or empty string means off,
|
|
231
198
|
// otherwise the value is the shell command the orchestrator runs on notable events — so it
|
|
232
199
|
// must be a string when present, and an empty string stays valid (it is how `config set
|
|
233
200
|
// notify ""` clears the hook).
|
|
@@ -296,7 +263,6 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
296
263
|
checkNumber,
|
|
297
264
|
checkPromptLength,
|
|
298
265
|
checkModelTripleEntry,
|
|
299
|
-
checkWindowReserve,
|
|
300
266
|
});
|
|
301
267
|
if (problems.length > 0) {
|
|
302
268
|
throw new Error(`invalid ${label}:\n - ${problems.join("\n - ")}`);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* saving, the role-selection helpers) lives in config.ts, the write side in config-write.ts
|
|
8
8
|
* (editor verbs) and config-request.ts (the director's staged request). */
|
|
9
9
|
import { MODEL_TIERS } from "./config-schema.js";
|
|
10
|
-
import { formatModelSelector, parseModelSelector, routeModel } from "./model-selector.js";
|
|
10
|
+
import { formatModelSelector, parseModelSelector, routeModel } from "./config-model-selector.js";
|
|
11
11
|
import { modelPairName } from "../budget/budget.js";
|
|
12
12
|
import { isJsonObject } from "../files/json-object.js";
|
|
13
13
|
import { DIRECTOR_ROLE, roleById } from "../roles/roles.js";
|
|
@@ -31,20 +31,6 @@ export function resolvedBackendConfig(config, id) {
|
|
|
31
31
|
}
|
|
32
32
|
return { bin: entry?.bin ?? config.agentBin, args: (entry?.args ?? config.piArgs ?? []).slice() };
|
|
33
33
|
}
|
|
34
|
-
/** A backend's subscription-window reserve (plans/codex-backend.md part 2b/9): the nested
|
|
35
|
-
* `backends[backend].windowReserve` when the backend has one, else the top-level
|
|
36
|
-
* `claudeWindowReserve` for claude (its older spelling), else undefined. The two spellings are
|
|
37
|
-
* mutually exclusive in a loaded file (validateConfig rejects one holding both), so the
|
|
38
|
-
* fallback only ever answers for a config written before the rename. Pure: it never writes a
|
|
39
|
-
* spelling back into the file. */
|
|
40
|
-
export function windowReserveFor(config, backend) {
|
|
41
|
-
const nested = config.backends?.[backend]?.windowReserve;
|
|
42
|
-
if (nested !== undefined)
|
|
43
|
-
return nested;
|
|
44
|
-
if (backend === "claude")
|
|
45
|
-
return config.claudeWindowReserve;
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
34
|
/** True when `s` names one of the three tiers — the form `roles.<id>.model` /
|
|
49
35
|
* `review.model` may take to point at that tier of the top-level `model` map. */
|
|
50
36
|
export function isTierName(s) {
|
|
@@ -109,7 +95,12 @@ export function tierModel(config, tier) {
|
|
|
109
95
|
* key it overrides would.
|
|
110
96
|
*
|
|
111
97
|
* `tier` is the seam's effective tier, already resolved by the caller from any tier-name
|
|
112
|
-
* override — so `o.model` here is always a selector string, never a tier reference.
|
|
98
|
+
* override — so `o.model` here is always a selector string, never a tier reference.
|
|
99
|
+
*
|
|
100
|
+
* Exported so imageRerouteTarget (src/agent/image-input.ts) can probe a peer tier's
|
|
101
|
+
* top-level model (`withModelOverrides(config, tier, {})`, the same shape resolverConfig
|
|
102
|
+
* builds strong with) and reshape a tier's fallback pair into a runnable config — the
|
|
103
|
+
* tier-config builder the plan names. */
|
|
113
104
|
export function withModelOverrides(config, tier, o) {
|
|
114
105
|
// The top-level model resolves at the seam's tier: a string form is the default tier's
|
|
115
106
|
// selector; a map form takes the tier's entry, else `default`'s.
|
|
@@ -266,8 +257,8 @@ export function reviewRunConfig(config) {
|
|
|
266
257
|
/** The optional provider/model/thinking fields of a resolved model seam, with each unset
|
|
267
258
|
* (empty) field left out so the result carries only what was resolved — the one home of that
|
|
268
259
|
* assembly. The fallback layer (config-fallbacks.ts's fallbackSelectorFields and
|
|
269
|
-
* fallbackPair/tierOwnFallback
|
|
270
|
-
*
|
|
260
|
+
* fallbackPair/tierOwnFallback) and role-view's RoleViewPayload (its model fields) all
|
|
261
|
+
* build a triple this way and follow the same rule — an absent or empty
|
|
271
262
|
* value is
|
|
272
263
|
* omitted rather than written as undefined/"" (an empty selector field would read as
|
|
273
264
|
* "override present," so it must not appear). */
|
|
@@ -317,22 +308,6 @@ export function seamHoldKey(cfg) {
|
|
|
317
308
|
export function seamPairName(cfg) {
|
|
318
309
|
return modelPairName({ provider: seamHoldKey(cfg), model: cfg.model });
|
|
319
310
|
}
|
|
320
|
-
/** The key a resolved seam's run counts against the per-provider cap
|
|
321
|
-
* (plans/provider-concurrency.md §2): its hold key, else — for a pi seam that names no model
|
|
322
|
-
* at all and so runs pi's saved default — the default provider from pi's settings
|
|
323
|
-
* (`piDefault`, the caller's cached readPiDefaultModel result, so this view stays free of
|
|
324
|
-
* I/O). A bare id with no provider, or a seam whose pi settings cannot name a default, yields
|
|
325
|
-
* undefined: only the team total bounds it. HF sub-provider pins (`huggingface/…:deepinfra`)
|
|
326
|
-
* share the base provider, and billing does not split claude, because this reuses the hold key
|
|
327
|
-
* — "omlx is held" and "omlx is full" name the same thing on every surface. */
|
|
328
|
-
export function seamConcurrencyKey(cfg, piDefault) {
|
|
329
|
-
const hold = seamHoldKey(cfg);
|
|
330
|
-
if (hold !== undefined)
|
|
331
|
-
return hold;
|
|
332
|
-
if (cfg.model === undefined)
|
|
333
|
-
return piDefault?.provider;
|
|
334
|
-
return undefined;
|
|
335
|
-
}
|
|
336
311
|
/** The team's model wiring as one human-readable label, for the live-reload `model_changed`
|
|
337
312
|
* event: null when no tier resolves to a model (pi's own default), the single selector when
|
|
338
313
|
* every set tier agrees (the string-form `model`), and the `small=…, default=…, strong=…`
|