tumwater 0.1.2 → 0.2.0
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 +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { changedConfigKeys, loadConfigCached } from "./config.js";
|
|
2
2
|
import { loopIds } from "../roles/loop-ids.js";
|
|
3
|
-
import { configForRole,
|
|
4
|
-
import {
|
|
3
|
+
import { configForRole, teamModelLabel, modelSelectorField } from "./config-views.js";
|
|
4
|
+
import { seamsInUse } from "./config-fallbacks.js";
|
|
5
|
+
import { bestEffortEventLogger, TUMWATER_SOURCE } from "../events/events.js";
|
|
5
6
|
import { LoopRunner } from "../loop/loop.js";
|
|
6
7
|
import { configPath } from "../paths.js";
|
|
8
|
+
import { seedClaudeAccount } from "../agent/claude/claude-account.js";
|
|
9
|
+
import { agentMissingMessage, findAgentBinary, resolveAgentBin } from "../pi/pi-bin.js";
|
|
7
10
|
/** A live config reloader over the given wiring; poll it once per scheduler cycle (see
|
|
8
11
|
* LiveConfigReload for the contract). */
|
|
9
12
|
export function newLiveConfigReload(deps) {
|
|
@@ -21,9 +24,17 @@ export function newLiveConfigReload(deps) {
|
|
|
21
24
|
// The cap last applied to the semaphore (live-resized on each reload), so a change logs
|
|
22
25
|
// exactly one event per distinct value — not once per poll.
|
|
23
26
|
let lastMaxConcurrent = Math.max(1, deps.config.maxConcurrent);
|
|
27
|
+
// The per-provider caps last applied to the semaphore (live-resized on each reload), so a
|
|
28
|
+
// change logs exactly one event per distinct value — not once per poll. Seeded from the
|
|
29
|
+
// startup config, so the first poll of an unchanged file emits nothing.
|
|
30
|
+
let lastKeyCaps = deps.config.maxConcurrentPerProvider ?? {};
|
|
31
|
+
// Roots this process has already attempted a live-edit claude-account seed for (see the
|
|
32
|
+
// reload block below), so repeated polls of a config that keeps a claude seam cannot respawn
|
|
33
|
+
// `claude auth status` on every cycle.
|
|
34
|
+
const liveClaudeSeedAttempted = new Set();
|
|
24
35
|
// Best-effort event logging: poll() runs inside the orchestrator's poll loop
|
|
25
36
|
// (src/orchestrator/orchestrator.ts), which has no catch, so an unwritable feed (ENOSPC,
|
|
26
|
-
// EACCES, the path replaced by a directory) would otherwise end the
|
|
37
|
+
// EACCES, the path replaced by a directory) would otherwise end the team — and, worse, skip
|
|
27
38
|
// the config application that follows the emission. bestEffortEventLogger (events.ts) holds
|
|
28
39
|
// the shared "logging must not throw" policy.
|
|
29
40
|
const { emit, warn } = bestEffortEventLogger(deps.root);
|
|
@@ -31,8 +42,8 @@ export function newLiveConfigReload(deps) {
|
|
|
31
42
|
poll() {
|
|
32
43
|
const reloaded = loadConfigCached(deps.root);
|
|
33
44
|
// A missing file keeps the last-known-good config too, and never reloads as defaults: the
|
|
34
|
-
//
|
|
35
|
-
// mid-run is an incident — a
|
|
45
|
+
// team was started on a real file (startup refuses to run without one), so its vanishing
|
|
46
|
+
// mid-run is an incident — a merge's fast-forward deleted it on 2026-09-22 and the team
|
|
36
47
|
// silently ran 8.6 h on defaults (BUGS.md 2026-09-23). One warning per vanish, not per
|
|
37
48
|
// poll; its return logs one line, then the normal reload below diffs it against the
|
|
38
49
|
// retained config — so config_changed names only what the returned file really changed.
|
|
@@ -54,11 +65,11 @@ export function newLiveConfigReload(deps) {
|
|
|
54
65
|
// changed (maxConcurrent and sessionRetentionDays have their own events elsewhere).
|
|
55
66
|
const changedKeys = changedConfigKeys(prevLive, reloaded.config);
|
|
56
67
|
if (changedKeys.length > 0) {
|
|
57
|
-
emit({
|
|
68
|
+
emit({ source: TUMWATER_SOURCE, type: "config_changed", keys: changedKeys });
|
|
58
69
|
// A real model-wiring change gets its own event naming the new selector(s), which the
|
|
59
70
|
// key list cannot show; a non-model edit (instructions, an interval) emits nothing.
|
|
60
|
-
const fromModel =
|
|
61
|
-
const toModel =
|
|
71
|
+
const fromModel = teamModelLabel(prevLive);
|
|
72
|
+
const toModel = teamModelLabel(reloaded.config);
|
|
62
73
|
const roleDiffs = changedKeys
|
|
63
74
|
.filter((key) => key.startsWith("roles."))
|
|
64
75
|
.map((key) => {
|
|
@@ -70,13 +81,34 @@ export function newLiveConfigReload(deps) {
|
|
|
70
81
|
.filter((d) => d.from !== d.to);
|
|
71
82
|
if (fromModel !== toModel || roleDiffs.length > 0)
|
|
72
83
|
emit({
|
|
73
|
-
|
|
84
|
+
source: TUMWATER_SOURCE,
|
|
74
85
|
type: "model_changed",
|
|
75
86
|
from: fromModel,
|
|
76
87
|
to: toModel,
|
|
77
88
|
...(roleDiffs.length > 0 ? { roles: roleDiffs } : {}),
|
|
78
89
|
});
|
|
79
90
|
}
|
|
91
|
+
// A live edit that routes a seam to a backend the previous config did not use: the
|
|
92
|
+
// team keeps running, but that backend must be verified and (for claude) its account
|
|
93
|
+
// seeded, exactly as startup would — so a claude reviewer added to a pi team does not
|
|
94
|
+
// start uncheckable. A missing binary is a warning event, never a crash; a present one
|
|
95
|
+
// logs its verdict as a backend_binary event. The seed is attempted once per root.
|
|
96
|
+
const prevAgents = new Set(seamsInUse(prevLive).map((s) => s.agent ?? "pi"));
|
|
97
|
+
for (const agent of new Set(seamsInUse(reloaded.config).map((s) => s.agent ?? "pi"))) {
|
|
98
|
+
if (prevAgents.has(agent))
|
|
99
|
+
continue;
|
|
100
|
+
const resolved = resolveAgentBin(reloaded.config, agent);
|
|
101
|
+
if (findAgentBinary(resolved)) {
|
|
102
|
+
emit({ source: TUMWATER_SOURCE, type: "backend_binary", agent, found: true });
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
warn(`newly routed ${agent} backend: ${agentMissingMessage(resolved, agent)}`);
|
|
106
|
+
}
|
|
107
|
+
if (agent === "claude" && !liveClaudeSeedAttempted.has(deps.root)) {
|
|
108
|
+
liveClaudeSeedAttempted.add(deps.root);
|
|
109
|
+
seedClaudeAccount(deps.root, reloaded.config);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
80
112
|
prevLive = reloaded.config;
|
|
81
113
|
for (const r of deps.runners)
|
|
82
114
|
r.config = reloaded.config;
|
|
@@ -86,9 +118,29 @@ export function newLiveConfigReload(deps) {
|
|
|
86
118
|
const newMaxConcurrent = Math.max(1, reloaded.config.maxConcurrent);
|
|
87
119
|
if (newMaxConcurrent !== lastMaxConcurrent) {
|
|
88
120
|
deps.semaphore.setCapacity(newMaxConcurrent);
|
|
89
|
-
emit({
|
|
121
|
+
emit({ source: TUMWATER_SOURCE, type: "max_concurrent_changed", from: lastMaxConcurrent, to: newMaxConcurrent });
|
|
90
122
|
lastMaxConcurrent = newMaxConcurrent;
|
|
91
123
|
}
|
|
124
|
+
// Live-resize the per-provider caps (plans/provider-concurrency.md §7): an edit
|
|
125
|
+
// re-applies the whole map to the pool, and each changed provider fires its own
|
|
126
|
+
// `provider_concurrency_changed` event. setKeyCaps never preempts — shrinking only
|
|
127
|
+
// withholds future grants on that key until releases drain it under the new cap.
|
|
128
|
+
const nextKeyCaps = reloaded.config.maxConcurrentPerProvider ?? {};
|
|
129
|
+
deps.semaphore.setKeyCaps(nextKeyCaps);
|
|
130
|
+
for (const provider of new Set([...Object.keys(lastKeyCaps), ...Object.keys(nextKeyCaps)])) {
|
|
131
|
+
const from = lastKeyCaps[provider];
|
|
132
|
+
const to = nextKeyCaps[provider];
|
|
133
|
+
if (from !== to) {
|
|
134
|
+
emit({
|
|
135
|
+
source: TUMWATER_SOURCE,
|
|
136
|
+
type: "provider_concurrency_changed",
|
|
137
|
+
provider,
|
|
138
|
+
from: from ?? 0,
|
|
139
|
+
to: to ?? 0,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
lastKeyCaps = nextKeyCaps;
|
|
92
144
|
const nowEnabled = loopIds(reloaded.config);
|
|
93
145
|
// Enabling a role — or raising `roles.<id>.instances` — mid-run starts it: create the
|
|
94
146
|
// runner for each newly present loop id (its persisted state survives). A lowered
|
|
@@ -103,10 +155,10 @@ export function newLiveConfigReload(deps) {
|
|
|
103
155
|
}
|
|
104
156
|
for (const role of prevEnabled)
|
|
105
157
|
if (!nowEnabled.includes(role))
|
|
106
|
-
warn(`role ${role} disabled —
|
|
158
|
+
warn(`role ${role} disabled — no new runs`);
|
|
107
159
|
for (const role of nowEnabled)
|
|
108
160
|
if (!prevEnabled.has(role))
|
|
109
|
-
warn(`role ${role} enabled — starting
|
|
161
|
+
warn(`role ${role} enabled — starting runs`);
|
|
110
162
|
prevEnabled = new Set(nowEnabled);
|
|
111
163
|
lastConfigError = null;
|
|
112
164
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** The tumwater.json config schema shape: the types `config-validation.ts` validates a
|
|
2
3
|
* loaded config into and every config read site consumes, plus the key allow-lists and
|
|
3
4
|
* value shapes (`TOP_LEVEL_KEYS` and friends) that say what a valid file may hold at each
|
|
@@ -5,7 +6,7 @@
|
|
|
5
6
|
* its default (config.ts), and its allow-list entry sit beside each other. Split out of the former
|
|
6
7
|
* types.ts grab-bag — the runtime types a tick produces now live beside their consumers
|
|
7
8
|
* (TickResult and TickOutcome in tick-outcome.ts, LoopState in loop-state.ts, HarnessEvent
|
|
8
|
-
* in events.ts,
|
|
9
|
+
* in events.ts, MergeEntry in merge-queue.ts, and the pi-run types in run-agent.ts and
|
|
9
10
|
* pi-run-result.ts) — so the schema the file on disk is validated against and the runtime
|
|
10
11
|
* state the harness runs with
|
|
11
12
|
* are two layers, changed for different reasons by different readers: a tumwater.json knob
|
|
@@ -17,11 +18,13 @@ import { objectOrEmpty } from "../files/json-object.js";
|
|
|
17
18
|
* `model` map inherits `default`'s model; `fallback` map values may also be `"pause"`
|
|
18
19
|
* (each tier's own entry is consulted when that tier runs at the cap; part 5a/8). */
|
|
19
20
|
export const MODEL_TIERS = ["small", "default", "strong"];
|
|
21
|
+
/** The keys an object fallback step may carry (validation's checkKnownKeys list). */
|
|
22
|
+
export const FALLBACK_STEP_KEYS = ["model", "maxDailyCostUsd"];
|
|
20
23
|
/** The agent backends an `agent` value may name (plans/agent-backends.md "Config"). The
|
|
21
24
|
* registry (src/agent/agent-backend.ts) is the runtime source, but the schema reserves these
|
|
22
25
|
* ids so validation rejects a typo'd backend at load time instead of falling back to pi. The
|
|
23
26
|
* agent id doubles as the selector provider namespace: `claude/<id>` always routes to claude. */
|
|
24
|
-
export const AGENT_IDS = ["pi", "claude"];
|
|
27
|
+
export const AGENT_IDS = ["pi", "claude", "opencode"];
|
|
25
28
|
/** The tier-map key list validation feeds checkKnownKeys: a `model` or `fallback` map whose
|
|
26
29
|
* keys are not exactly these tiers is a typo that would silently never resolve. */
|
|
27
30
|
export const TIER_MAP_KEYS = MODEL_TIERS;
|
|
@@ -41,32 +44,34 @@ export function modelTierMap(model) {
|
|
|
41
44
|
/** Every key tumwater.json may hold, by level. Anything else is a typo that would be
|
|
42
45
|
* silently ignored at runtime — the intended setting falls back to its default with no
|
|
43
46
|
* warning — so validation (config-validation.ts's checkKnownKeys, fed from these tables)
|
|
44
|
-
* fails fast instead (e.g. `
|
|
47
|
+
* fails fast instead (e.g. `runTimeoutSecondss` does nothing). Kept in sync with the
|
|
45
48
|
* interfaces above in this same file: adding a key edits the interface and its level's
|
|
46
49
|
* list side by side. */
|
|
47
50
|
export const TOP_LEVEL_KEYS = [
|
|
48
51
|
"provider",
|
|
49
52
|
"model",
|
|
50
53
|
"thinking",
|
|
51
|
-
"
|
|
54
|
+
"backend",
|
|
52
55
|
"baseBranch",
|
|
53
56
|
"agentBin",
|
|
54
|
-
"
|
|
57
|
+
"backends",
|
|
55
58
|
"check",
|
|
56
59
|
"piArgs",
|
|
57
60
|
"maxConcurrent",
|
|
58
|
-
"
|
|
61
|
+
"maxConcurrentPerProvider",
|
|
62
|
+
"claudeWindowReserve",
|
|
63
|
+
"mergeBatchMax",
|
|
59
64
|
"maxConcurrentChecks",
|
|
60
65
|
"worktreeSlots",
|
|
61
|
-
"
|
|
62
|
-
"
|
|
66
|
+
"minRunIntervalSeconds",
|
|
67
|
+
"runTimeoutSeconds",
|
|
63
68
|
"quietTimeoutSeconds",
|
|
64
69
|
"toolCallStallSeconds",
|
|
65
70
|
"logMaxBytes",
|
|
66
71
|
"sessionRetentionDays",
|
|
67
72
|
"maxDailyCostUsd",
|
|
68
73
|
"maxDailyCostUsdPerRole",
|
|
69
|
-
"
|
|
74
|
+
"maintenancePerWorkMerge",
|
|
70
75
|
"quietHours",
|
|
71
76
|
"quietHoursPerRole",
|
|
72
77
|
"diskHoldGB",
|
|
@@ -81,13 +86,17 @@ export const TOP_LEVEL_KEYS = [
|
|
|
81
86
|
"autoRestart",
|
|
82
87
|
"bootstrap",
|
|
83
88
|
"review",
|
|
84
|
-
"
|
|
89
|
+
"customRoles",
|
|
85
90
|
"roles",
|
|
86
91
|
];
|
|
87
92
|
export const BACKOFF_KEYS = ["initialSeconds", "factor", "maxSeconds"];
|
|
88
|
-
/** The keys an `
|
|
89
|
-
* field (`
|
|
93
|
+
/** The keys an `backends.<id>` entry may hold; validation feeds checkKnownKeys so a typo'd
|
|
94
|
+
* field (`backends.claude.binary`) is a load error rather than a silently inert entry.
|
|
95
|
+
* `configDir` is claude-only (CLAUDE_AGENT_KEYS), so it is not silently inert on pi/opencode. */
|
|
90
96
|
export const AGENT_KEYS = ["bin", "args"];
|
|
97
|
+
/** The keys a claude `backends.claude` entry may hold — the shared agent keys plus `configDir`,
|
|
98
|
+
* the one per-backend field only the claude adapter reads. */
|
|
99
|
+
export const CLAUDE_AGENT_KEYS = [...AGENT_KEYS, "configDir"];
|
|
91
100
|
/** The keys tumwater.json's `bootstrap` section may hold. */
|
|
92
101
|
export const BOOTSTRAP_KEYS = ["untilPlansDone"];
|
|
93
102
|
export const ROLE_ENTRY_KEYS = [
|
|
@@ -96,7 +105,7 @@ export const ROLE_ENTRY_KEYS = [
|
|
|
96
105
|
"provider",
|
|
97
106
|
"model",
|
|
98
107
|
"thinking",
|
|
99
|
-
"
|
|
108
|
+
"minRunIntervalSeconds",
|
|
100
109
|
"instances",
|
|
101
110
|
];
|
|
102
111
|
export const REVIEW_KEYS = ["enabled", "exemptPaths", "provider", "model", "thinking", "timeoutSeconds"];
|
|
@@ -110,7 +119,7 @@ export const CHECK_KEYS = ["command", "gateCommand", "cwd", "timeoutSeconds"];
|
|
|
110
119
|
export const MODEL_TRIPLE_KEYS = ["provider", "model", "thinking"];
|
|
111
120
|
/** pi's accepted `--thinking` levels (pi's own `--help`). pi WARNS and falls back to its own
|
|
112
121
|
* default on any other value rather than failing, so a misspelled level would silently run the
|
|
113
|
-
*
|
|
122
|
+
* team at the wrong reasoning depth — the same silent-ignore class validation exists to
|
|
114
123
|
* catch. Kept in sync with pi's CLI (dist/cli/args.js VALID_THINKING_LEVELS). */
|
|
115
124
|
export const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
116
125
|
/** pi flags the harness passes itself (src/pi/pi-args.ts's piArgs: `--print --mode json
|
|
@@ -121,17 +130,20 @@ export const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high
|
|
|
121
130
|
* `--continue` resumes a session the harness did not choose. Each flag maps to why it is
|
|
122
131
|
* refused; the model triple points at the setting that owns it. */
|
|
123
132
|
export const HARNESS_PI_FLAGS = new Map([
|
|
124
|
-
["--print", "
|
|
125
|
-
["-p", "
|
|
126
|
-
["--mode", "
|
|
127
|
-
["--session-dir", "
|
|
128
|
-
["--continue", "
|
|
129
|
-
["-c", "
|
|
130
|
-
["-n", "
|
|
131
|
-
["--name", "
|
|
133
|
+
["--print", "tumwater already runs pi non-interactively"],
|
|
134
|
+
["-p", "tumwater already runs pi non-interactively"],
|
|
135
|
+
["--mode", "tumwater requires pi's json output mode"],
|
|
136
|
+
["--session-dir", "tumwater owns each role's session directory"],
|
|
137
|
+
["--continue", "tumwater decides when to resume a session"],
|
|
138
|
+
["-c", "tumwater decides when to resume a session"],
|
|
139
|
+
["-n", "tumwater names the session"],
|
|
140
|
+
["--name", "tumwater names the session"],
|
|
132
141
|
["--provider", "set the top-level or per-role `provider` instead"],
|
|
133
142
|
["--model", "set the top-level or per-role `model` instead"],
|
|
134
143
|
["--thinking", "set the top-level or per-role `thinking` instead"],
|
|
144
|
+
// The harness passes --no-extensions itself (pi-args.ts's piArgs) so pi cannot load discovered
|
|
145
|
+
// extensions; a piArgs entry that re-enabled them would put operator MCP servers back in.
|
|
146
|
+
["--no-extensions", "tumwater skips pi's discovered extensions itself"],
|
|
135
147
|
]);
|
|
136
148
|
export const CUSTOM_LOOP_KEYS = ["name", "task"];
|
|
137
149
|
/** A custom loop's name becomes a worktree dir and a git ref, so it is validated strictly:
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
import { AGENT_IDS, AGENT_KEYS, CLAUDE_AGENT_KEYS, BACKOFF_KEYS, BOOTSTRAP_KEYS, CHECK_KEYS, CUSTOM_LOOP_KEYS, CUSTOM_NAME_RE, CUSTOM_TASK_MAX_CHARS, FALLBACK_STEP_KEYS, HARNESS_PI_FLAGS, MODEL_TRIPLE_KEYS, REVIEW_KEYS, ROLE_ENTRY_KEYS, TOP_LEVEL_KEYS, ROLE_INSTRUCTIONS_MAX_CHARS, TIER_MAP_KEYS, } from "./config-schema.js";
|
|
2
3
|
import { allRoleIds } from "../roles/roles.js";
|
|
3
4
|
import { INSTANCE_ROLES, baseRoleOf } from "../roles/loop-ids.js";
|
|
4
5
|
import { isJsonObject } from "../files/json-object.js";
|
|
6
|
+
import { expandHomeTilde } from "../files/files.js";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
5
9
|
import { isNonBlankString, tooLongMessage } from "../text/text.js";
|
|
10
|
+
import { typoSuffix } from "../text/suggest.js";
|
|
6
11
|
import { PREFILL_REASON } from "../text/phrases.js";
|
|
7
12
|
import { parseQuietHours } from "../scheduling/quiet-hours.js";
|
|
8
13
|
import { isAgentId, parseModelSelector } from "./model-selector.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
14
|
+
import { TUMWATER_SOURCE } from "../events/events.js";
|
|
15
|
+
import { reservedPassthroughFlag } from "../agent/backend-traits.js";
|
|
16
|
+
import { AT_LEAST_ONE, checkBooleanField, checkKnownKeys, checkKnownRoleId, checkModelTripleField, checkNumberField, checkObjectSection, checkReservedArgs, checkStringArrayField, checkStringField, NON_NEGATIVE, NON_NEGATIVE_OR_DISABLED, DOLLAR_CAP, MAX_DURATION_SECONDS, POSITIVE, POSITIVE_INTEGER, show, typeName, } from "./config-field-checks.js";
|
|
11
17
|
/** Schema validation for tumwater.json: validateConfig, the gate every load and save passes
|
|
12
18
|
* through (config.ts's load/save, config-write.ts's budget editing and director config
|
|
13
19
|
* requests), judged against the key allow-lists and value shapes that live beside the types
|
|
@@ -18,12 +24,6 @@ import { AT_LEAST_ONE, checkBooleanField, checkKnownKeys, checkKnownRoleId, chec
|
|
|
18
24
|
* unknown-key and known-role guards, the NumberRule table, and the per-field type checkers —
|
|
19
25
|
* lives in config-field-checks.ts; this module is the section-by-section rules plus the
|
|
20
26
|
* cross-field checks, and the one export gate (validateConfig) for them. */
|
|
21
|
-
/** The upper bound every seconds field that feeds a ×1000 duration shares: 2147483 s
|
|
22
|
-
* × 1000 stays inside node's signed-32-bit setTimeout/setInterval range (2^31−1 ms,
|
|
23
|
-
* about 24.8 days). Above it node clamps the delay to 1ms — a one-zero typo like
|
|
24
|
-
* `"tickTimeoutSeconds": 1e300` became Infinity ms, and every pi run died the moment it
|
|
25
|
-
* started (BUGS.md 2026-10-03). */
|
|
26
|
-
const MAX_DURATION_SECONDS = 2147483;
|
|
27
27
|
const DURATION_POSITIVE = {
|
|
28
28
|
ok: (n) => n > 0 && n <= MAX_DURATION_SECONDS,
|
|
29
29
|
what: `a number greater than 0, at most ${MAX_DURATION_SECONDS}`,
|
|
@@ -64,7 +64,7 @@ function checkPerRoleMap(r, key, expectation, customNames, problems, checkValue)
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
/** Validate tumwater.json values, so a typo fails fast with an actionable message instead
|
|
67
|
-
* of misbehaving at runtime — e.g. a non-numeric
|
|
67
|
+
* of misbehaving at runtime — e.g. a non-numeric runTimeoutSeconds becomes NaN and kills
|
|
68
68
|
* every pi run instantly, a non-numeric logMaxBytes rotates the event log on every write, an
|
|
69
69
|
* unknown role id (a misspelled entry under `roles`) spawns a phantom loop that errors every
|
|
70
70
|
* tick, and an unknown key is silently ignored so the intended setting never takes effect.
|
|
@@ -113,9 +113,9 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
113
113
|
// The agent backend for bare/no-model selectors (plans/agent-backends.md "Config"): a
|
|
114
114
|
// non-blank id from the schema's reserved set. An unknown id would fall back to pi at run
|
|
115
115
|
// time (src/agent/run-agent.ts), silently running the wrong backend against a named one.
|
|
116
|
-
checkString(r, "", "
|
|
117
|
-
if (typeof r.
|
|
118
|
-
problems.push(`
|
|
116
|
+
checkString(r, "", "backend", false);
|
|
117
|
+
if (typeof r.backend === "string" && !isAgentId(r.backend))
|
|
118
|
+
problems.push(`backend must name a backend (${AGENT_IDS.join(", ")}; got ${show(r.backend)})${typoSuffix(r.backend, AGENT_IDS)}`);
|
|
119
119
|
checkStringArray(r, "", "piArgs");
|
|
120
120
|
// A piArgs entry that repeats a harness-managed flag is appended after the harness's own and
|
|
121
121
|
// pi's parser is last-wins, so it silently overrides it (see HARNESS_PI_FLAGS). Name the
|
|
@@ -127,25 +127,29 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
127
127
|
// that repeats a flag the backend's own harness invocation sets is appended last and wins,
|
|
128
128
|
// so it silently overrides the harness — refused here for the two backends whose reserved
|
|
129
129
|
// sets are known (claude-args.ts HARNESS_CLAUDE_FLAGS, config-schema.ts HARNESS_PI_FLAGS).
|
|
130
|
-
checkObjectSection(r, "
|
|
130
|
+
checkObjectSection(r, "backends", problems, (o) => {
|
|
131
131
|
for (const [id, entry] of Object.entries(o)) {
|
|
132
|
-
const prefix = `
|
|
132
|
+
const prefix = `backends.${id}`;
|
|
133
133
|
if (!isJsonObject(entry)) {
|
|
134
134
|
problems.push(`${prefix} must be an object with optional bin and args (got ${show(entry)})`);
|
|
135
135
|
continue;
|
|
136
136
|
}
|
|
137
|
-
checkKnownKeys(entry, AGENT_KEYS, prefix, problems);
|
|
137
|
+
checkKnownKeys(entry, id === "claude" ? CLAUDE_AGENT_KEYS : AGENT_KEYS, prefix, problems);
|
|
138
138
|
checkString(entry, `${prefix}.`, "bin", false);
|
|
139
139
|
checkStringArray(entry, `${prefix}.`, "args");
|
|
140
|
+
// claude's per-backend config dir (plans/notes-command.md "Keeping funes out of team runs"):
|
|
141
|
+
// a leading `~` expands; the value must be an absolute path after expansion, or claude would
|
|
142
|
+
// resolve a relative CLAUDE_CONFIG_DIR from the tick's cwd and write transcripts somewhere
|
|
143
|
+
// unexpected. Only claude accepts the key (CLAUDE_AGENT_KEYS), so it is inert nowhere.
|
|
144
|
+
if (id === "claude") {
|
|
145
|
+
checkString(entry, `${prefix}.`, "configDir", false);
|
|
146
|
+
if (typeof entry.configDir === "string" && entry.configDir !== "")
|
|
147
|
+
if (!path.isAbsolute(expandHomeTilde(entry.configDir, os.homedir())))
|
|
148
|
+
problems.push(`${prefix}.configDir must be an absolute path after ~ expansion (got ${show(entry.configDir)})`);
|
|
149
|
+
}
|
|
140
150
|
if (!Array.isArray(entry.args))
|
|
141
151
|
continue; // Already reported by checkStringArray.
|
|
142
|
-
checkReservedArgs(problems, entry.args, `${prefix}.args`, (flag) => id
|
|
143
|
-
? HARNESS_CLAUDE_FLAGS.has(flag)
|
|
144
|
-
? "it is harness-managed"
|
|
145
|
-
: undefined
|
|
146
|
-
: id === "pi"
|
|
147
|
-
? HARNESS_PI_FLAGS.get(flag)
|
|
148
|
-
: undefined);
|
|
152
|
+
checkReservedArgs(problems, entry.args, `${prefix}.args`, (flag) => reservedPassthroughFlag(id, flag));
|
|
149
153
|
}
|
|
150
154
|
});
|
|
151
155
|
// Legacy back-compat (plans/agent-backends.md "Back-compat"): a file must not spell one pi
|
|
@@ -154,21 +158,21 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
154
158
|
// value from the default. The effective value resolves through config-views.ts
|
|
155
159
|
// resolvedAgentConfig; a load-time mirror into `agents` would instead leak a duplicate
|
|
156
160
|
// spelling into every later config write.
|
|
157
|
-
if (rawFile && isJsonObject(r.
|
|
158
|
-
const pi = r.
|
|
161
|
+
if (rawFile && isJsonObject(r.backends)) {
|
|
162
|
+
const pi = r.backends.pi;
|
|
159
163
|
if (isJsonObject(pi)) {
|
|
160
164
|
if (r.agentBin !== undefined && pi.bin !== undefined)
|
|
161
|
-
problems.push("agentBin and
|
|
165
|
+
problems.push("agentBin and backends.pi.bin set the same value — keep one (backends.pi.bin is the current spelling; agentBin is its old name)");
|
|
162
166
|
if (r.piArgs !== undefined && pi.args !== undefined)
|
|
163
|
-
problems.push("piArgs and
|
|
167
|
+
problems.push("piArgs and backends.pi.args set the same value — keep one (backends.pi.args is the current spelling; piArgs is its old name)");
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
170
|
checkNumber(r, "", "maxConcurrent", POSITIVE_INTEGER);
|
|
167
|
-
checkNumber(r, "", "
|
|
171
|
+
checkNumber(r, "", "mergeBatchMax", POSITIVE_INTEGER);
|
|
168
172
|
checkNumber(r, "", "maxConcurrentChecks", POSITIVE_INTEGER);
|
|
169
173
|
checkNumber(r, "", "worktreeSlots", POSITIVE_INTEGER);
|
|
170
|
-
checkNumber(r, "", "
|
|
171
|
-
checkNumber(r, "", "
|
|
174
|
+
checkNumber(r, "", "minRunIntervalSeconds", DURATION_NON_NEGATIVE);
|
|
175
|
+
checkNumber(r, "", "runTimeoutSeconds", DURATION_POSITIVE);
|
|
172
176
|
checkNumber(r, "", "quietTimeoutSeconds", DURATION_OR_DISABLED);
|
|
173
177
|
checkNumber(r, "", "toolCallStallSeconds", DURATION_OR_DISABLED);
|
|
174
178
|
checkNumber(r, "", "logMaxBytes", POSITIVE);
|
|
@@ -176,7 +180,7 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
176
180
|
checkNumber(r, "", "maxDailyCostUsd", DOLLAR_CAP);
|
|
177
181
|
// Work ratio 1a/4: the maintenance allowance ratio is a number of 0 or more (a larger value
|
|
178
182
|
// effectively disables the cap, so there is no separate off switch).
|
|
179
|
-
checkNumber(r, "", "
|
|
183
|
+
checkNumber(r, "", "maintenancePerWorkMerge", NON_NEGATIVE);
|
|
180
184
|
// Disk floor (plans/disk-floor.md, part 1/4): 0 disables the hold, so only a negative
|
|
181
185
|
// value or a non-number is a problem.
|
|
182
186
|
checkNumber(r, "", "diskHoldGB", NON_NEGATIVE);
|
|
@@ -281,19 +285,26 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
281
285
|
if (isJsonObject(f)) {
|
|
282
286
|
checkKnownKeys(f, TIER_MAP_KEYS, "fallback", problems);
|
|
283
287
|
for (const [tier, v] of Object.entries(f)) {
|
|
284
|
-
if (
|
|
285
|
-
|
|
288
|
+
if (Array.isArray(v))
|
|
289
|
+
checkFallbackList(`fallback.${tier}`, v, problems);
|
|
290
|
+
else if (isJsonObject(v))
|
|
291
|
+
checkFallbackStep(`fallback.${tier}`, v, problems);
|
|
292
|
+
else if (!isNonBlankString(v))
|
|
293
|
+
problems.push(`fallback.${tier} must be a non-empty model id or "pause" (got ${show(v)})`);
|
|
286
294
|
else if (v !== "pause")
|
|
287
295
|
// An entry is a pure selector (config-views' fallbackSelectorFields parses it with
|
|
288
296
|
// no legacy provider in scope); "pause" names no model and stays valid.
|
|
289
297
|
checkSelectorValue(`fallback.${tier}`, v, undefined, problems);
|
|
290
298
|
}
|
|
291
299
|
}
|
|
300
|
+
else if (Array.isArray(f)) {
|
|
301
|
+
checkFallbackList("fallback", f, problems);
|
|
302
|
+
}
|
|
292
303
|
else if (!isNonBlankString(f)) {
|
|
293
|
-
problems.push(`fallback must be a
|
|
304
|
+
problems.push(`fallback must be a model id or a map by tier (got ${show(f)})`);
|
|
294
305
|
}
|
|
295
306
|
else if (f === "pause") {
|
|
296
|
-
problems.push(`fallback "pause" is only valid as a tier map value — a string fallback must name a
|
|
307
|
+
problems.push(`fallback "pause" is only valid as a tier map value — a string fallback must name a model id`);
|
|
297
308
|
}
|
|
298
309
|
else {
|
|
299
310
|
// A selector-string fallback still parses to halves: report an empty one.
|
|
@@ -311,13 +322,46 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
311
322
|
/** A selector string that parses to an empty provider or model half (`"/id"`, `"p/"`) passes
|
|
312
323
|
* the non-empty-string rule but never reaches pi: piArgs skips an empty value when it builds
|
|
313
324
|
* its flags (config-field-checks' model-triple note), and fallbackPair drops an empty half, so
|
|
314
|
-
* the
|
|
325
|
+
* the team silently runs pi's default model — the same silent-ignore class the empty-string
|
|
315
326
|
* rules exist to prevent. Checked only when no legacy provider is in scope: under one, the
|
|
316
327
|
* whole string is the id (config-views' parse), so a `/` in it is ordinary text and the parse
|
|
317
328
|
* cannot produce an empty half. */
|
|
318
329
|
function checkSelectorHalves(obj, prefix, key, legacyProvider, problems) {
|
|
319
330
|
checkSelectorValue(`${prefix}${key}`, obj[key], legacyProvider, problems);
|
|
320
331
|
}
|
|
332
|
+
/** Validate a `fallback` list (a tier's ordered chain): non-empty, each element a step — a
|
|
333
|
+
* non-blank model id that is not `"pause"` (a whole-tier value only) and parses to both
|
|
334
|
+
* halves, or a `{ model, maxDailyCostUsd? }` object (checkFallbackStep). */
|
|
335
|
+
function checkFallbackList(name, list, problems) {
|
|
336
|
+
if (list.length === 0)
|
|
337
|
+
problems.push(`${name} list must not be empty — name at least one model id`);
|
|
338
|
+
list.forEach((el, i) => {
|
|
339
|
+
const at = `${name}[${i}]`;
|
|
340
|
+
if (isJsonObject(el))
|
|
341
|
+
checkFallbackStep(at, el, problems);
|
|
342
|
+
else if (!isNonBlankString(el))
|
|
343
|
+
problems.push(`${name} list element ${i} must be a non-empty model id or a step object (got ${show(el)})`);
|
|
344
|
+
else if (el === "pause")
|
|
345
|
+
problems.push(`${name} list element ${i} is "pause" — "pause" is only valid as a whole-tier value`);
|
|
346
|
+
else
|
|
347
|
+
checkSelectorValue(at, el, undefined, problems);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/** Validate one object step `{ model, maxDailyCostUsd? }`: only those keys, `model` a non-blank
|
|
351
|
+
* selector (not `"pause"`) and `maxDailyCostUsd`, when present, a positive finite number. */
|
|
352
|
+
function checkFallbackStep(at, step, problems) {
|
|
353
|
+
checkKnownKeys(step, FALLBACK_STEP_KEYS, at, problems);
|
|
354
|
+
const m = step.model;
|
|
355
|
+
if (!isNonBlankString(m))
|
|
356
|
+
problems.push(`${at}.model must be a non-empty model id (got ${show(m)})`);
|
|
357
|
+
else if (m === "pause")
|
|
358
|
+
problems.push(`${at}.model is "pause" — "pause" is only valid as a whole-tier value`);
|
|
359
|
+
else
|
|
360
|
+
checkSelectorValue(`${at}.model`, m, undefined, problems);
|
|
361
|
+
const cap = step.maxDailyCostUsd;
|
|
362
|
+
if (cap !== undefined && !(typeof cap === "number" && Number.isFinite(cap) && cap > 0))
|
|
363
|
+
problems.push(`${at}.maxDailyCostUsd must be a positive number (got ${show(cap)})`);
|
|
364
|
+
}
|
|
321
365
|
/** Report one selector string that parses to an empty provider or model half. `name` is the
|
|
322
366
|
* config path the message names — `model`, a map entry's `model.small`, a section's
|
|
323
367
|
* `roles.feature.model`. Split out of checkSelectorHalves so the map forms, which enumerate
|
|
@@ -337,16 +381,16 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
337
381
|
// they are validated strictly — a colliding name would silently shadow a built-in's prompt
|
|
338
382
|
// (roleById wins in tickPrompt), validation makes that impossible instead of subtle. The
|
|
339
383
|
// collected names feed the roles cross-check below: an id under `roles` is valid when it is
|
|
340
|
-
// in the catalog OR listed as a
|
|
384
|
+
// in the catalog OR listed as a customRoles name.
|
|
341
385
|
const customNames = new Set();
|
|
342
|
-
if ("
|
|
343
|
-
const cl = r.
|
|
386
|
+
if ("customRoles" in r) {
|
|
387
|
+
const cl = r.customRoles;
|
|
344
388
|
if (!Array.isArray(cl)) {
|
|
345
|
-
problems.push(`
|
|
389
|
+
problems.push(`customRoles must be an array of { name, task } entries (got ${show(cl)})`);
|
|
346
390
|
}
|
|
347
391
|
else {
|
|
348
392
|
cl.forEach((entry, i) => {
|
|
349
|
-
const where = `
|
|
393
|
+
const where = `customRoles[${i}]`;
|
|
350
394
|
if (!isJsonObject(entry)) {
|
|
351
395
|
problems.push(`${where} must be an object with keys name and task (got ${show(entry)})`);
|
|
352
396
|
return;
|
|
@@ -357,16 +401,19 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
357
401
|
problems.push(`${where}.name must be a string matching /^[a-z0-9][a-z0-9_-]{0,31}$/ (got ${show(name)})`);
|
|
358
402
|
}
|
|
359
403
|
else if (!CUSTOM_NAME_RE.test(name)) {
|
|
360
|
-
problems.push(`${where}.name "${name}" is not a valid
|
|
404
|
+
problems.push(`${where}.name "${name}" is not a valid role name: start with a lowercase letter or digit, then at most 31 more of [a-z0-9_-]`);
|
|
405
|
+
}
|
|
406
|
+
else if (name === TUMWATER_SOURCE) {
|
|
407
|
+
problems.push(`${where}.name "${name}" is reserved: events tumwater writes about itself carry that source, so a role's events would be indistinguishable from them — pick another name`);
|
|
361
408
|
}
|
|
362
409
|
else if (allRoleIds().includes(name)) {
|
|
363
410
|
problems.push(`${where}.name "${name}" collides with a built-in role id — pick another name`);
|
|
364
411
|
}
|
|
365
412
|
else if (baseRoleOf(name) !== name) {
|
|
366
|
-
problems.push(`${where}.name "${name}" would shadow the ${baseRoleOf(name)}
|
|
413
|
+
problems.push(`${where}.name "${name}" would shadow the ${baseRoleOf(name)} role's instance ids — pick another name`);
|
|
367
414
|
}
|
|
368
415
|
else if (customNames.has(name)) {
|
|
369
|
-
problems.push(`${where}.name "${name}" is duplicated in
|
|
416
|
+
problems.push(`${where}.name "${name}" is duplicated in customRoles — names must be unique`);
|
|
370
417
|
}
|
|
371
418
|
else {
|
|
372
419
|
customNames.add(name);
|
|
@@ -392,7 +439,7 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
392
439
|
}
|
|
393
440
|
else {
|
|
394
441
|
for (const [id, rc] of Object.entries(roles)) {
|
|
395
|
-
// An id outside the catalog and
|
|
442
|
+
// An id outside the catalog and customRoles cannot work: tickPrompt has no prompt for
|
|
396
443
|
// it and the loop would error every tick forever (checkKnownRoleId).
|
|
397
444
|
if (!checkKnownRoleId("roles", id, customNames, problems))
|
|
398
445
|
continue;
|
|
@@ -411,7 +458,7 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
411
458
|
checkModelTriple(o, `roles.${id}.`, "tier-name");
|
|
412
459
|
checkSelectorHalves(o, `roles.${id}.`, "model", o.provider ?? r.provider, problems);
|
|
413
460
|
checkBoolean(o, `roles.${id}.`, "enabled");
|
|
414
|
-
checkNumber(o, `roles.${id}.`, "
|
|
461
|
+
checkNumber(o, `roles.${id}.`, "minRunIntervalSeconds", DURATION_NON_NEGATIVE);
|
|
415
462
|
// Only feature and bugfix may split (src/roles/loop-ids.ts INSTANCE_ROLES); an
|
|
416
463
|
// instances count under any other role would never spawn a runner, so reject it
|
|
417
464
|
// rather than let the operator believe it did something.
|
|
@@ -425,7 +472,7 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
425
472
|
}
|
|
426
473
|
}
|
|
427
474
|
// Per-role daily cost caps (src/gates/role-cap-gates.ts): each key must name a known role —
|
|
428
|
-
// built-in or
|
|
475
|
+
// built-in or customRoles — because a typo'd id would silently no-op the cap, the exact
|
|
429
476
|
// silent-ignore class the `roles.<id>` check exists to prevent (the customNames set above
|
|
430
477
|
// is fully collected by this point, so a custom loop can be capped). Each value is a
|
|
431
478
|
// spend threshold with the NON_NEGATIVE_OR_DISABLED semantics: 0 disables that role's cap.
|
|
@@ -440,7 +487,7 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
440
487
|
// Per-role quiet hours (src/scheduling/quiet-hours.ts): the same known-role gate as the
|
|
441
488
|
// caps — a typo'd role id would silently no-op the window. Each value must parse as a
|
|
442
489
|
// quiet-hours window (an empty string means off for that role, the same disablement the
|
|
443
|
-
//
|
|
490
|
+
// team-wide key takes), and the message names the key and the offending id/value verbatim.
|
|
444
491
|
checkPerRoleMap(r, "quietHoursPerRole", '"HH:MM-HH:MM" windows', customNames, problems, (id, value) => {
|
|
445
492
|
if (typeof value !== "string") {
|
|
446
493
|
problems.push(`quietHoursPerRole.${id} must be a "HH:MM-HH:MM" string like "23:00-07:00" (an empty string means off) (got ${show(value)})`);
|
|
@@ -452,6 +499,43 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
|
|
|
452
499
|
if (!parsed.ok)
|
|
453
500
|
problems.push(parsed.error);
|
|
454
501
|
});
|
|
502
|
+
// Per-provider concurrency caps (plans/provider-concurrency.md §1): a map from provider
|
|
503
|
+
// key to a positive integer. Unlike the per-role maps above, an unknown provider name is
|
|
504
|
+
// NOT a validation error — providers live in pi's models.json, which validation does not
|
|
505
|
+
// read, and a fallback's provider may only come into use later (doctor warns in part 5/5).
|
|
506
|
+
// Each value must be a positive integer under a non-empty key.
|
|
507
|
+
if (r.maxConcurrentPerProvider !== undefined) {
|
|
508
|
+
const map = r.maxConcurrentPerProvider;
|
|
509
|
+
if (!isJsonObject(map)) {
|
|
510
|
+
problems.push(`maxConcurrentPerProvider must be an object mapping provider names to concurrency caps (got ${show(map)})`);
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
for (const [provider, value] of Object.entries(map)) {
|
|
514
|
+
if (provider.trim() === "") {
|
|
515
|
+
problems.push(`maxConcurrentPerProvider.<empty> — a provider key must not be empty`);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !POSITIVE_INTEGER.ok(value)) {
|
|
519
|
+
problems.push(`maxConcurrentPerProvider.${provider} must be ${POSITIVE_INTEGER.what} (got ${show(value)})`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// The subscription-window reserve: a map from window type to a fraction, 0 < f <= 1.
|
|
525
|
+
if (r.claudeWindowReserve !== undefined) {
|
|
526
|
+
const map = r.claudeWindowReserve;
|
|
527
|
+
if (!isJsonObject(map)) {
|
|
528
|
+
problems.push(`claudeWindowReserve must be an object mapping window types to fractions (got ${show(map)})`);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
for (const [type, value] of Object.entries(map)) {
|
|
532
|
+
if (type.trim() === "")
|
|
533
|
+
problems.push(`claudeWindowReserve.<empty> — a window type must not be empty`);
|
|
534
|
+
else if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1)
|
|
535
|
+
problems.push(`claudeWindowReserve.${type} must be a fraction above 0 and at most 1 (got ${show(value)})`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
455
539
|
// Cross-field (judged on the merged config, where both sides are always present; on a raw
|
|
456
540
|
// file only when the file itself names both): scheduleBackoff clamps every idle wait — the
|
|
457
541
|
// first included — to min(initialSeconds, maxSeconds), so a smaller max silently discards
|