tumwater 0.2.0 → 0.4.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 +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +55 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +6 -4
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +404 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +281 -55
- package/dist/src/backlog/backlog-cli-view.js +255 -0
- package/dist/src/backlog/backlog-cli.js +91 -335
- package/dist/src/backlog/backlog-commit.js +20 -13
- package/dist/src/backlog/backlog-eligibility.js +193 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +264 -0
- package/dist/src/backlog/backlog-release.js +134 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-guardrails.js +136 -0
- package/dist/src/baseline/main-red.js +153 -36
- package/dist/src/baseline/main-watch.js +232 -0
- package/dist/src/brief/brief-edit.js +12 -12
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +31 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +65 -3
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +181 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +183 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +291 -598
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +525 -0
- package/dist/src/cli/cli-start-summary.js +167 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
- package/dist/src/cli/completion.js +169 -22
- package/dist/src/cli/config-commands.js +58 -26
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +258 -82
- package/dist/src/cli/log-commands.js +164 -85
- package/dist/src/cli/question-commands.js +211 -10
- package/dist/src/cli.js +173 -45
- package/dist/src/collections.js +56 -13
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +16 -1
- package/dist/src/config/config-fallbacks.js +94 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +169 -222
- package/dist/src/config/config-views.js +136 -54
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +164 -265
- package/dist/src/config/config.js +53 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
- package/dist/src/doctor/doctor-backlog-plans.js +398 -0
- package/dist/src/doctor/doctor-backlog.js +249 -512
- package/dist/src/doctor/doctor-check-names.js +85 -0
- package/dist/src/doctor/doctor-checks.js +61 -593
- package/dist/src/doctor/doctor-claude-checks.js +142 -0
- package/dist/src/doctor/doctor-codex-checks.js +276 -0
- package/dist/src/doctor/doctor-contention-checks.js +506 -0
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +80 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +216 -537
- package/dist/src/doctor/doctor-notify.js +9 -5
- package/dist/src/doctor/doctor-opencode-checks.js +164 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +397 -0
- package/dist/src/doctor/doctor.js +126 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +21 -12
- package/dist/src/events/event-format.js +185 -89
- package/dist/src/events/event-labels.js +11 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +52 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +80 -26
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +65 -60
- package/dist/src/failure/failure-render.js +29 -21
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +35 -35
- package/dist/src/files/file-queue.js +17 -0
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +88 -225
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +101 -143
- package/dist/src/gates/maintenance-quota.js +37 -30
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +89 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +54 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +43 -19
- package/dist/src/gui/gui-backlog-endpoints.js +156 -0
- package/dist/src/gui/gui-command.js +26 -13
- package/dist/src/gui/gui-config-commands.js +168 -80
- package/dist/src/gui/gui-endpoint-commands.js +5 -4
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +235 -13
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +97 -0
- package/dist/src/gui/gui-server.js +26 -5
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +155 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +190 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +28 -17
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +58 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +45 -19
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +214 -0
- package/dist/src/merge/merge-queue-render.js +48 -11
- package/dist/src/merge/merge-queue.js +52 -15
- package/dist/src/merge/merge-requeue.js +103 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +27 -11
- package/dist/src/merge/merge-to-main.js +77 -26
- package/dist/src/merge/merge-top.js +83 -0
- package/dist/src/merge/merge-verify.js +75 -18
- package/dist/src/notes-cli.js +35 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +352 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
- package/dist/src/orchestrator/orchestrator.js +129 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +245 -0
- package/dist/src/orchestrator/scheduling-skip.js +195 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +126 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +26 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +150 -217
- package/dist/src/prompt/notes-backlog.js +184 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +131 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +188 -124
- package/dist/src/report/report-fold.js +130 -54
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +136 -65
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +23 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +112 -1
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +239 -0
- package/dist/src/review/protected-edits.js +24 -10
- package/dist/src/review/quarantine-render.js +65 -0
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +180 -401
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +127 -12
- package/dist/src/roles/role-guidance.js +65 -20
- package/dist/src/roles/role-labels.js +7 -5
- package/dist/src/roles/role-render.js +108 -56
- package/dist/src/roles/role-view.js +95 -56
- package/dist/src/roles/roles.js +33 -33
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/run/qa-coverage.js +125 -0
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +175 -0
- package/dist/src/run/run-detail-render.js +91 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +178 -0
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +231 -0
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +216 -0
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +191 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +109 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +103 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claim-entries.js +49 -0
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +49 -17
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +43 -28
- package/dist/src/status/status-roles.js +50 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +7 -8
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +65 -18
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +60 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +78 -13
- package/dist/src/ui/badges.js +99 -38
- package/dist/src/ui/gui/gui-client-backlog.js +314 -0
- package/dist/src/ui/gui/gui-client-boot.js +89 -61
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
- package/dist/src/ui/gui/gui-client-drawer.js +155 -289
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +437 -68
- package/dist/src/ui/gui/gui-client-labels.js +6 -3
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +60 -52
- package/dist/src/ui/gui/gui-client-pending.js +405 -24
- package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
- package/dist/src/ui/gui/gui-client-report.js +105 -430
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +163 -5
- package/dist/src/ui/gui/gui-client-team.js +36 -465
- package/dist/src/ui/gui/gui-client.js +38 -26
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +10 -7
- package/dist/src/ui/gui/gui-styles.js +38 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +364 -49
- package/dist/src/ui/status-render.js +109 -82
- package/dist/src/ui/team-alerts.js +728 -62
- package/dist/src/ui/tone.js +12 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +53 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +4 -12
- package/dist/src/ui/tui/tui-editor-backends.js +127 -0
- package/dist/src/ui/tui/tui-editor-brief.js +14 -36
- package/dist/src/ui/tui/tui-editor-budget.js +36 -39
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
- package/dist/src/ui/tui/tui-editor-config.js +99 -0
- package/dist/src/ui/tui/tui-editor-entry.js +39 -64
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +46 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
- package/dist/src/ui/tui/tui-editor-interval.js +29 -16
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +9 -29
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
- package/dist/src/ui/tui/tui-editor-retire.js +5 -13
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +101 -34
- package/dist/src/ui/tui/tui-editor-slots.js +24 -0
- package/dist/src/ui/tui/tui-editors.js +44 -2
- package/dist/src/ui/tui/tui-frame.js +68 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +33 -9
- package/dist/src/ui/tui/tui-pane-activity.js +35 -19
- package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +168 -8
- package/dist/src/ui/tui/tui-pane-history.js +446 -82
- package/dist/src/ui/tui/tui-pane-pending.js +539 -15
- package/dist/src/ui/tui/tui-pane-queued.js +78 -57
- package/dist/src/ui/tui/tui-pane-report.js +54 -16
- package/dist/src/ui/tui/tui-pane-session.js +34 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +91 -28
- package/dist/src/ui/tui/tui-panes.js +153 -34
- package/dist/src/ui/tui/tui.js +18 -15
- package/dist/src/verdict/fix-claim.js +129 -33
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +77 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/leftover.js +0 -130
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/qa-coverage.js +0 -108
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** The director's staged config-write request — one verb split out of config-write.ts (the
|
|
2
|
+
* operator-editor mutators stay there, addressed by key through config-keys.ts). The director
|
|
3
|
+
* stages a JSON request file in its worktree; this module consumes it: parse the request,
|
|
4
|
+
* keep only its `customRoles` array, apply it as a whole-config write, and delete the file on
|
|
5
|
+
* every path. A request is shaped and lives differently from the interactive `config set` /
|
|
6
|
+
* `unset` verbs (a staged file that must never reach a diff, not direct CLI text), so the
|
|
7
|
+
* runtime (run-verdict.ts) imports this module rather than the whole editor. It
|
|
8
|
+
* still shares the write machinery with the editor — withConfigLock and rawConfigObject from
|
|
9
|
+
* config-write-core.ts (the editor verbs live in config-write.ts), validateConfig from
|
|
10
|
+
* config-validation.ts, writeJsonAtomic from json-files.js — because a request is a
|
|
11
|
+
* whole-config write too. */
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import { isDeepStrictEqual } from "node:util";
|
|
14
|
+
import { configPath, configRequestPath } from "../paths.js";
|
|
15
|
+
import { errorMessage } from "../text/text.js";
|
|
16
|
+
import { readTextOrNull } from "../files/files.js";
|
|
17
|
+
import { writeJsonAtomic } from "../files/json-files.js";
|
|
18
|
+
import { isJsonObject } from "../files/json-object.js";
|
|
19
|
+
import { withConfigLock, rawConfigObject } from "./config-write-core.js";
|
|
20
|
+
import { show } from "./config-field-checks.js";
|
|
21
|
+
import { validateConfig } from "./config-validation.js";
|
|
22
|
+
import { normalizeConfigKeys } from "./config-aliases.js";
|
|
23
|
+
import { collapseBackendAliases, loadConfig } from "./config.js";
|
|
24
|
+
/** Consume the director's config-write request file (plans/portability.md §3/7). After its pi
|
|
25
|
+
* run the director may leave `.tumwater-config-request.json` at its worktree root:
|
|
26
|
+
* `{ "customRoles": [ { "name", "task" }, … ] }` — the whole array, replacing the current one.
|
|
27
|
+
* Applied atomically to the live config, sharing the editor's trap (fresh loadConfig that
|
|
28
|
+
* bypasses the stat cache, validateConfig, then writeJsonAtomic — readers poll every ~2 s), so
|
|
29
|
+
* the orchestrator's live reload starts the new role with no commit, no review
|
|
30
|
+
* gate, and no merge, and the request file never enters a diff.
|
|
31
|
+
*
|
|
32
|
+
* The permitted key set is enforced here, not in prose: `customRoles` is accepted and every
|
|
33
|
+
* other top-level key is collected into `ignored` and dropped (the caller logs the warning that
|
|
34
|
+
* names them). Validation runs BEFORE any write and nothing here dereferences a request entry
|
|
35
|
+
* first — a structurally malformed entry (`[null]`, a non-string name) is left for
|
|
36
|
+
* validateConfig to reject, so on failure nothing is written and the previous config stays
|
|
37
|
+
* live. The request file is deleted on EVERY path (including every failure), so a malformed
|
|
38
|
+
* request cannot retry forever and the file cannot survive into `git add -A`.
|
|
39
|
+
*
|
|
40
|
+
* Roles entries for custom roles the request removes are stripped before validation, from the
|
|
41
|
+
* FILE's own `roles` section rather than the catalog-merged map (the rule every other
|
|
42
|
+
* whole-config write follows, see preserveFileRoles): loadConfig seeds one `roles.<name>` per
|
|
43
|
+
* current custom role and validateConfig rejects an id that is neither a catalog role nor a
|
|
44
|
+
* requested custom-role name, so without the strip a removal would never apply. Only
|
|
45
|
+
* structurally valid names feed the strip — a malformed entry stays in the array for
|
|
46
|
+
* validateConfig to reject.
|
|
47
|
+
*
|
|
48
|
+
* Returns null when no request file exists. `applied` names the custom roles now live (empty
|
|
49
|
+
* unless the write happened); `ignored` lists the discarded top-level keys; `error`, when set,
|
|
50
|
+
* names why nothing (or only part of the work) was done — the caller turns it and `ignored`
|
|
51
|
+
* into warning events. `unchanged` is set when the request's array equals the live one;
|
|
52
|
+
* nothing is written. */
|
|
53
|
+
export function applyConfigRequest(root, wt) {
|
|
54
|
+
const requestFile = configRequestPath(wt);
|
|
55
|
+
const raw = readTextOrNull(requestFile);
|
|
56
|
+
if (raw === null)
|
|
57
|
+
return null; // no request this run
|
|
58
|
+
const ignored = [];
|
|
59
|
+
let applied = [];
|
|
60
|
+
let error;
|
|
61
|
+
let unchanged;
|
|
62
|
+
try {
|
|
63
|
+
const unnormalized = JSON.parse(raw);
|
|
64
|
+
if (!isJsonObject(unnormalized))
|
|
65
|
+
throw new Error(`config request must be a JSON object (got ${show(unnormalized)})`);
|
|
66
|
+
// Old key spellings read as the new ones; a request setting both fails with the pair named.
|
|
67
|
+
const normalized = normalizeConfigKeys(unnormalized);
|
|
68
|
+
if (normalized.problems.length > 0)
|
|
69
|
+
throw new Error(normalized.problems.join("; "));
|
|
70
|
+
const parsed = normalized.raw;
|
|
71
|
+
for (const key of Object.keys(parsed)) {
|
|
72
|
+
if (key !== "customRoles")
|
|
73
|
+
ignored.push(key);
|
|
74
|
+
}
|
|
75
|
+
const entries = parsed.customRoles;
|
|
76
|
+
if (!Array.isArray(entries))
|
|
77
|
+
throw new Error(`config request's customRoles must be an array (got ${show(entries)})`);
|
|
78
|
+
// The load-to-write sequence is one critical section under withConfigLock: a concurrent
|
|
79
|
+
// `config set`/dashboard write between the load and this write must not be dropped by the
|
|
80
|
+
// whole-file overwrite (see writeConfigMutation).
|
|
81
|
+
withConfigLock(root, () => {
|
|
82
|
+
const current = loadConfig(root); // fresh — bypasses the stat cache: a writer sees the latest file
|
|
83
|
+
// Same as the live array: write nothing, like writeConfigMutation's no-op skip.
|
|
84
|
+
if (isDeepStrictEqual(entries, current.customRoles)) {
|
|
85
|
+
unchanged = true;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Strip roles.<id> entries for custom roles this request removes (see docstring).
|
|
89
|
+
// Structurally valid names only; anything else stays for validateConfig to reject — never
|
|
90
|
+
// dereference a request entry before validation.
|
|
91
|
+
const requestedNames = new Set();
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (isJsonObject(entry) && typeof entry.name === "string")
|
|
94
|
+
requestedNames.add(entry.name);
|
|
95
|
+
}
|
|
96
|
+
// Base the roles rewrite on the FILE's own section, not the loaded (catalog-merged)
|
|
97
|
+
// map: this whole-config write must not re-expand a partial section to every built-in
|
|
98
|
+
// (preserveFileRoles' rule). Only the removed custom roles' entries are stripped, and
|
|
99
|
+
// only from what the file actually holds.
|
|
100
|
+
const rawRolesCandidate = rawConfigObject(root).roles;
|
|
101
|
+
const roles = isJsonObject(rawRolesCandidate)
|
|
102
|
+
? { ...(rawRolesCandidate) }
|
|
103
|
+
: {};
|
|
104
|
+
for (const c of current.customRoles) {
|
|
105
|
+
if (!requestedNames.has(c.name))
|
|
106
|
+
delete roles[c.name];
|
|
107
|
+
}
|
|
108
|
+
const candidate = { ...current, customRoles: entries };
|
|
109
|
+
const candidateRecord = candidate;
|
|
110
|
+
if (rawRolesCandidate === undefined)
|
|
111
|
+
delete candidateRecord.roles;
|
|
112
|
+
else
|
|
113
|
+
candidateRecord.roles = roles;
|
|
114
|
+
// Collapse legacy/new pi spellings before writing so a merged config (which always
|
|
115
|
+
// carries the default piArgs) never lands both forms in the file — the next load's raw
|
|
116
|
+
// pass rejects a file holding both `piArgs` and `backends.pi.args`.
|
|
117
|
+
const collapsed = collapseBackendAliases(candidate);
|
|
118
|
+
validateConfig(collapsed); // throws listing every problem — nothing is written on failure
|
|
119
|
+
writeJsonAtomic(configPath(root), collapsed, true);
|
|
120
|
+
applied = collapsed.customRoles.map((c) => c.name);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
applied = []; // no write happened, or it must not be reported as applied
|
|
125
|
+
error = errorMessage(err);
|
|
126
|
+
}
|
|
127
|
+
// Delete the request on EVERY path — success, rejection, malformed JSON — so it can never be
|
|
128
|
+
// staged by commitAll, reach a review gate, or retry forever. A failed unlink is surfaced as
|
|
129
|
+
// an error so the caller logs it (the file would otherwise survive into the diff).
|
|
130
|
+
try {
|
|
131
|
+
fs.unlinkSync(requestFile);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
error ??= `config request file could not be deleted: ${errorMessage(err)}`;
|
|
135
|
+
}
|
|
136
|
+
return unchanged ? { applied, ignored, error, unchanged } : { applied, ignored, error };
|
|
137
|
+
}
|
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** The tumwater.json config schema shape: the types `config-validation.ts` validates a
|
|
3
2
|
* loaded config into and every config read site consumes, plus the key allow-lists and
|
|
4
3
|
* value shapes (`TOP_LEVEL_KEYS` and friends) that say what a valid file may hold at each
|
|
5
4
|
* level — the schema facts, single-homed here so adding a key edits one file: its type,
|
|
6
5
|
* its default (config.ts), and its allow-list entry sit beside each other. Split out of the former
|
|
7
|
-
* types.ts grab-bag — the runtime types a
|
|
8
|
-
* (
|
|
6
|
+
* types.ts grab-bag — the runtime types a run produces now live beside their consumers
|
|
7
|
+
* (RunResult and RunOutcome in run-outcome.ts, RunnerState in runner-state.ts, HarnessEvent
|
|
9
8
|
* in events.ts, MergeEntry in merge-queue.ts, and the pi-run types in run-agent.ts and
|
|
10
9
|
* pi-run-result.ts) — so the schema the file on disk is validated against and the runtime
|
|
11
10
|
* state the harness runs with
|
|
12
11
|
* are two layers, changed for different reasons by different readers: a tumwater.json knob
|
|
13
|
-
* hunt starts here, a
|
|
14
|
-
* modules: config.ts (defaults + load), config-validation.ts (validation), config-write.ts
|
|
15
|
-
* (writes). */
|
|
12
|
+
* hunt starts here, a run-lifecycle trace starts beside the type it traces. Sibling
|
|
13
|
+
* modules: config.ts (defaults + load), config-validation.ts (validation), config-write-core.ts
|
|
14
|
+
* (machinery) + config-write.ts and config-request.ts (writes). */
|
|
16
15
|
import { objectOrEmpty } from "../files/json-object.js";
|
|
17
16
|
/** The model tiers a selector map may name (plans/model-tiers.md). A tier left out of the
|
|
18
17
|
* `model` map inherits `default`'s model; `fallback` map values may also be `"pause"`
|
|
19
18
|
* (each tier's own entry is consulted when that tier runs at the cap; part 5a/8). */
|
|
20
19
|
export const MODEL_TIERS = ["small", "default", "strong"];
|
|
21
20
|
/** The keys an object fallback step may carry (validation's checkKnownKeys list). */
|
|
22
|
-
export const FALLBACK_STEP_KEYS = ["model", "maxDailyCostUsd"];
|
|
21
|
+
export const FALLBACK_STEP_KEYS = ["backend", "model", "maxDailyCostUsd"];
|
|
23
22
|
/** The agent backends an `agent` value may name (plans/agent-backends.md "Config"). The
|
|
24
23
|
* registry (src/agent/agent-backend.ts) is the runtime source, but the schema reserves these
|
|
25
|
-
* ids so validation rejects a typo'd backend at load time instead of falling back to pi.
|
|
26
|
-
*
|
|
27
|
-
export const
|
|
24
|
+
* ids so validation rejects a typo'd backend at load time instead of falling back to pi. A
|
|
25
|
+
* model id never names a backend: the backend is the `backend` key's. */
|
|
26
|
+
export const BACKEND_IDS = ["pi", "claude", "opencode", "codex"];
|
|
28
27
|
/** The tier-map key list validation feeds checkKnownKeys: a `model` or `fallback` map whose
|
|
29
28
|
* keys are not exactly these tiers is a typo that would silently never resolve. */
|
|
30
29
|
export const TIER_MAP_KEYS = MODEL_TIERS;
|
|
@@ -41,6 +40,8 @@ export function modelTierMap(model) {
|
|
|
41
40
|
return { default: model };
|
|
42
41
|
return objectOrEmpty(model);
|
|
43
42
|
}
|
|
43
|
+
/** The keys an object tier entry may hold (validation feeds checkKnownKeys). */
|
|
44
|
+
export const SELECTOR_ENTRY_KEYS = ["backend", "model", "thinking"];
|
|
44
45
|
/** Every key tumwater.json may hold, by level. Anything else is a typo that would be
|
|
45
46
|
* silently ignored at runtime — the intended setting falls back to its default with no
|
|
46
47
|
* warning — so validation (config-validation.ts's checkKnownKeys, fed from these tables)
|
|
@@ -92,11 +93,16 @@ export const TOP_LEVEL_KEYS = [
|
|
|
92
93
|
export const BACKOFF_KEYS = ["initialSeconds", "factor", "maxSeconds"];
|
|
93
94
|
/** The keys an `backends.<id>` entry may hold; validation feeds checkKnownKeys so a typo'd
|
|
94
95
|
* field (`backends.claude.binary`) is a load error rather than a silently inert entry.
|
|
95
|
-
* `configDir`
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
* `configDir`/`windowReserve` are claude/codex-only (CLAUDE_AGENT_KEYS / CODEX_BACKEND_KEYS),
|
|
97
|
+
* so they are not silently inert on pi/opencode. */
|
|
98
|
+
export const BACKEND_KEYS = ["bin", "args"];
|
|
99
|
+
/** The keys a claude `backends.claude` entry may hold — the shared agent keys plus `configDir`
|
|
100
|
+
* and `windowReserve`, the per-backend fields only the claude adapter reads. */
|
|
101
|
+
export const CLAUDE_AGENT_KEYS = [...BACKEND_KEYS, "configDir", "windowReserve"];
|
|
102
|
+
/** The keys a codex `backends.codex` entry may hold — the same as claude's: the shared agent keys
|
|
103
|
+
* plus `configDir` (which becomes `CODEX_HOME` for the codex processes, plans/codex-backend.md
|
|
104
|
+
* §4) and `windowReserve` (the part 2 per-backend reserve). */
|
|
105
|
+
export const CODEX_BACKEND_KEYS = [...BACKEND_KEYS, "configDir", "windowReserve"];
|
|
100
106
|
/** The keys tumwater.json's `bootstrap` section may hold. */
|
|
101
107
|
export const BOOTSTRAP_KEYS = ["untilPlansDone"];
|
|
102
108
|
export const ROLE_ENTRY_KEYS = [
|
|
@@ -104,15 +110,16 @@ export const ROLE_ENTRY_KEYS = [
|
|
|
104
110
|
"instructions",
|
|
105
111
|
"provider",
|
|
106
112
|
"model",
|
|
113
|
+
"backend",
|
|
107
114
|
"thinking",
|
|
108
115
|
"minRunIntervalSeconds",
|
|
109
116
|
"instances",
|
|
110
117
|
];
|
|
111
|
-
export const REVIEW_KEYS = ["enabled", "exemptPaths", "provider", "model", "thinking", "timeoutSeconds"];
|
|
118
|
+
export const REVIEW_KEYS = ["enabled", "exemptPaths", "afterMerge", "provider", "model", "backend", "thinking", "timeoutSeconds"];
|
|
112
119
|
/** The `check` section's keys (plans/portability.md §6/7): the project's own verification
|
|
113
120
|
* command, the optional cheaper gate-only command, their
|
|
114
121
|
* working directory (relative to the worktree), and their timeout. */
|
|
115
|
-
export const CHECK_KEYS = ["command", "gateCommand", "cwd", "timeoutSeconds"];
|
|
122
|
+
export const CHECK_KEYS = ["command", "gateCommand", "mergeCommand", "cwd", "timeoutSeconds"];
|
|
116
123
|
/** The provider/model/thinking triple every model-override section shares — top level,
|
|
117
124
|
* `review`, `fallbackModel` (plans/fallback-model.md), and each `roles.<id>` entry — so one
|
|
118
125
|
* mental model and one validator cover them all. */
|
|
@@ -126,7 +133,7 @@ export const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high
|
|
|
126
133
|
* --session-dir …`, the provider/model/thinking triple, and the session resume/name flags).
|
|
127
134
|
* A piArgs entry equal to one of these is appended AFTER the harness's own copy, and pi's
|
|
128
135
|
* argument parser is last-wins — so it silently overrides the harness: `--mode text` makes
|
|
129
|
-
* every
|
|
136
|
+
* every run's stream unparseable, `--model` spends on a model tumwater.json never named, and
|
|
130
137
|
* `--continue` resumes a session the harness did not choose. Each flag maps to why it is
|
|
131
138
|
* refused; the model triple points at the setting that owns it. */
|
|
132
139
|
export const HARNESS_PI_FLAGS = new Map([
|
|
@@ -145,14 +152,14 @@ export const HARNESS_PI_FLAGS = new Map([
|
|
|
145
152
|
// extensions; a piArgs entry that re-enabled them would put operator MCP servers back in.
|
|
146
153
|
["--no-extensions", "tumwater skips pi's discovered extensions itself"],
|
|
147
154
|
]);
|
|
148
|
-
export const
|
|
149
|
-
/** A custom
|
|
155
|
+
export const CUSTOM_ROLE_KEYS = ["name", "task"];
|
|
156
|
+
/** A custom role's name becomes a worktree dir and a git ref, so it is validated strictly:
|
|
150
157
|
* lowercase alphanumerics plus dash/underscore, starting with an alphanumeric, ≤ 32 chars. */
|
|
151
158
|
export const CUSTOM_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
152
|
-
/** A custom
|
|
153
|
-
* unbounded text would be a standing per-
|
|
159
|
+
/** A custom role's task rides into every one of that role's run prefills, so it is capped:
|
|
160
|
+
* unbounded text would be a standing per-run cost. */
|
|
154
161
|
export const CUSTOM_TASK_MAX_CHARS = 4096;
|
|
155
|
-
/** A role's extra instructions ride into every one of that role's
|
|
156
|
-
* capped like a custom
|
|
162
|
+
/** A role's extra instructions ride into every one of that role's run prefills, so they are
|
|
163
|
+
* capped like a custom role's task: unbounded text would be a standing per-run cost (and
|
|
157
164
|
* could crowd the prompt toward the model's context ceiling). */
|
|
158
165
|
export const ROLE_INSTRUCTIONS_MAX_CHARS = 4096;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { BACKEND_IDS, FALLBACK_STEP_KEYS, MODEL_TIERS } from "./config-schema.js";
|
|
2
|
+
import { checkKnownKeys, show } from "./config-field-checks.js";
|
|
3
|
+
import { isJsonObject } from "../files/json-object.js";
|
|
4
|
+
import { isNonBlankString } from "../text/text.js";
|
|
5
|
+
import { typoSuffix } from "../text/suggest.js";
|
|
6
|
+
import { isBackendId, parseModelSelector } from "./model-selector.js";
|
|
7
|
+
/** Model-selector and fallback-chain checks that validateConfig (config-validation.ts) runs
|
|
8
|
+
* on each model-choosing section: empty selector halves, `fallback` lists and steps, and an
|
|
9
|
+
* entry's `backend` against its model. Each takes the `problems` list to append to and reads
|
|
10
|
+
* nothing else, so they live beside the generic checkers in config-field-checks.ts rather
|
|
11
|
+
* than inside validateConfig's body. */
|
|
12
|
+
/** A selector string that parses to an empty provider or model half (`"/id"`, `"p/"`) passes
|
|
13
|
+
* the non-empty-string rule but never reaches pi: piArgs skips an empty value when it builds
|
|
14
|
+
* its flags (config-field-checks' model-triple note), and fallbackPair drops an empty half, so
|
|
15
|
+
* the team silently runs pi's default model — the same silent-ignore class the empty-string
|
|
16
|
+
* rules exist to prevent. Checked only when no legacy provider is in scope: under one, the
|
|
17
|
+
* whole string is the id (config-views' parse), so a `/` in it is ordinary text and the parse
|
|
18
|
+
* cannot produce an empty half. */
|
|
19
|
+
export function checkSelectorHalves(obj, prefix, key, legacyProvider, problems) {
|
|
20
|
+
checkSelectorValue(`${prefix}${key}`, obj[key], legacyProvider, problems);
|
|
21
|
+
}
|
|
22
|
+
/** Reject the retired model-id spellings that named a backend: a first segment that is a backend
|
|
23
|
+
* id (`claude/claude-opus-5-5`) and a bare `claude-…` id resolving to pi. Each message names the
|
|
24
|
+
* value and the edit — `backend` is its own key, never part of the model id. `backend` is the
|
|
25
|
+
* one the selector resolves to; an opencode seam's `opencode/<id>` is a legitimate OpenCode id
|
|
26
|
+
* and stays. A tier-name or non-string value is not a selector and is skipped. */
|
|
27
|
+
export function checkRetiredSelector(name, v, backend, problems) {
|
|
28
|
+
if (!isNonBlankString(v) || MODEL_TIERS.includes(v))
|
|
29
|
+
return;
|
|
30
|
+
const slash = v.indexOf("/");
|
|
31
|
+
const first = slash === -1 ? undefined : v.slice(0, slash);
|
|
32
|
+
if ((first === "claude" || first === "opencode") && backend !== "opencode")
|
|
33
|
+
problems.push(`${name} ${show(v)} names the backend "${first}" in its model id — write backend: "${first}" beside model: ${show(v.slice(slash + 1))}`);
|
|
34
|
+
else if (slash !== -1 && backend === "claude")
|
|
35
|
+
problems.push(`${name} ${show(v)} is a provider/model id but the backend is claude, which would run it as a paid claude model — add backend: "pi" beside it`);
|
|
36
|
+
else if (slash === -1 && v.startsWith("claude-") && backend === "pi")
|
|
37
|
+
problems.push(`${name} ${show(v)} is a claude model id but the backend is pi — add backend: "claude" beside it, or write ${show(`anthropic/${v}`)} to keep it on pi`);
|
|
38
|
+
}
|
|
39
|
+
/** Validate a `fallback` list (a tier's ordered chain): non-empty, each element a step — a
|
|
40
|
+
* non-blank model id that is not `"pause"` (a whole-tier value only) and parses to both
|
|
41
|
+
* halves, or a `{ backend?, model, maxDailyCostUsd? }` object (checkFallbackStep). */
|
|
42
|
+
export function checkFallbackList(name, list, problems, inheritedBackend = "pi") {
|
|
43
|
+
if (list.length === 0)
|
|
44
|
+
problems.push(`${name} list must not be empty — name at least one model id`);
|
|
45
|
+
list.forEach((el, i) => {
|
|
46
|
+
const at = `${name}[${i}]`;
|
|
47
|
+
if (isJsonObject(el))
|
|
48
|
+
checkFallbackStep(at, el, problems, inheritedBackend);
|
|
49
|
+
else if (!isNonBlankString(el))
|
|
50
|
+
problems.push(`${name} list element ${i} must be a non-empty model id or a step object (got ${show(el)})`);
|
|
51
|
+
else if (el === "pause")
|
|
52
|
+
problems.push(`${name} list element ${i} is "pause" — "pause" is only valid as a whole-tier value`);
|
|
53
|
+
else {
|
|
54
|
+
checkSelectorValue(at, el, undefined, problems);
|
|
55
|
+
checkRetiredSelector(at, el, inheritedBackend, problems);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Validate one object step `{ backend?, model, maxDailyCostUsd? }`: only those keys, `backend` as
|
|
60
|
+
* checkEntryBackend checks it, `model` a non-blank
|
|
61
|
+
* selector (not `"pause"`) and `maxDailyCostUsd`, when present, a positive finite number. */
|
|
62
|
+
export function checkFallbackStep(at, step, problems, inheritedBackend = "pi") {
|
|
63
|
+
checkKnownKeys(step, FALLBACK_STEP_KEYS, at, problems);
|
|
64
|
+
checkEntryBackend(step, at, problems);
|
|
65
|
+
const m = step.model;
|
|
66
|
+
if (!isNonBlankString(m))
|
|
67
|
+
problems.push(`${at}.model must be a non-empty model id (got ${show(m)})`);
|
|
68
|
+
else if (m === "pause")
|
|
69
|
+
problems.push(`${at}.model is "pause" — "pause" is only valid as a whole-tier value`);
|
|
70
|
+
else {
|
|
71
|
+
checkSelectorValue(`${at}.model`, m, undefined, problems);
|
|
72
|
+
checkRetiredSelector(`${at}.model`, m, typeof step.backend === "string" ? step.backend : inheritedBackend, problems);
|
|
73
|
+
}
|
|
74
|
+
const cap = step.maxDailyCostUsd;
|
|
75
|
+
if (cap !== undefined && !(typeof cap === "number" && Number.isFinite(cap) && cap > 0))
|
|
76
|
+
problems.push(`${at}.maxDailyCostUsd must be a positive number (got ${show(cap)})`);
|
|
77
|
+
}
|
|
78
|
+
/** Validate the `backend` of one model-choosing entry (a tier entry, a role or `review`): it must
|
|
79
|
+
* name a registered backend, and — when the entry also names a model — the model must be a
|
|
80
|
+
* valid id for that backend: claude's `--model` takes its own ids (no `/`), pi needs
|
|
81
|
+
* `provider/id` and no backend id as the provider namespace. A tier-name `model` (roles,
|
|
82
|
+
* review) is a reference, not an id, and is skipped. */
|
|
83
|
+
export function checkEntryBackend(o, at, problems) {
|
|
84
|
+
const b = o.backend;
|
|
85
|
+
if (b === undefined)
|
|
86
|
+
return;
|
|
87
|
+
if (!isNonBlankString(b)) {
|
|
88
|
+
if (typeof b === "string")
|
|
89
|
+
problems.push(`${at}.backend must not be empty (got ${show(b)})`);
|
|
90
|
+
else
|
|
91
|
+
problems.push(`${at}.backend must be a string (got ${show(b)})`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!isBackendId(b)) {
|
|
95
|
+
problems.push(`${at}.backend must name a backend (${BACKEND_IDS.join(", ")}; got ${show(b)})${typoSuffix(b, BACKEND_IDS)}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const m = o.model;
|
|
99
|
+
if (!isNonBlankString(m) || MODEL_TIERS.includes(m))
|
|
100
|
+
return;
|
|
101
|
+
const sel = parseModelSelector(m);
|
|
102
|
+
if (b === "claude" && sel.provider !== undefined)
|
|
103
|
+
problems.push(`${at}.model ${show(m)} has a "/" but ${at}.backend is "claude" — claude takes its own model ids (e.g. "claude-sonnet-5-5")`);
|
|
104
|
+
if (b === "pi" && sel.provider === undefined)
|
|
105
|
+
problems.push(`${at}.model ${show(m)} has no provider but ${at}.backend is "pi" — pi needs \`provider/id\``);
|
|
106
|
+
if (b === "pi" && isBackendId(sel.provider))
|
|
107
|
+
problems.push(`${at}.model ${show(m)} names the backend "${sel.provider}" as its provider but ${at}.backend is "pi" — drop the "${sel.provider}/" namespace`);
|
|
108
|
+
}
|
|
109
|
+
/** Report one selector string that parses to an empty provider or model half. `name` is the
|
|
110
|
+
* config path the message names — `model`, a map entry's `model.small`, a section's
|
|
111
|
+
* `roles.feature.model`. Split out of checkSelectorHalves so the map forms, which enumerate
|
|
112
|
+
* their entries instead of looking one key up on an object, run the identical check: a
|
|
113
|
+
* selector's empty half is the same silent-ignore class wherever the string lives. */
|
|
114
|
+
export function checkSelectorValue(name, v, legacyProvider, problems) {
|
|
115
|
+
if (!isNonBlankString(v))
|
|
116
|
+
return;
|
|
117
|
+
const legacy = isNonBlankString(legacyProvider) ? legacyProvider : undefined;
|
|
118
|
+
const sel = parseModelSelector(v, legacy);
|
|
119
|
+
if (sel.provider?.trim() === "")
|
|
120
|
+
problems.push(`${name} ${show(v)} parses to an empty provider half — pi would silently run its default model`);
|
|
121
|
+
if (sel.model.trim() === "")
|
|
122
|
+
problems.push(`${name} ${show(v)} parses to an empty model half — pi would silently run its default model`);
|
|
123
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** The agent-execution section rules of tumwater.json validation: `agentBin`, `baseBranch`,
|
|
2
|
+
* the `backend` id, `piArgs` and the `backends` object (per-backend `bin`/`args`, the
|
|
3
|
+
* claude/codex config-dir scan, the codex `-c <key>=…` arity-aware scan, the reserved
|
|
4
|
+
* passthrough-flag screen), and the legacy `agentBin`/`piArgs` back-compat pair. Extracted
|
|
5
|
+
* from the single validateConfig gate (config-validation.ts) so an agent-key edit lands as a
|
|
6
|
+
* per-theme diff instead of a whole-gate one. Called from the gate where this block runs
|
|
7
|
+
* today, so problem order across the whole pass is unchanged.
|
|
8
|
+
*
|
|
9
|
+
* The helpers passed in are closures the gate binds once (the problems-bound field checkers
|
|
10
|
+
* and the per-backend config-dir / codex-config-key scans); everything else these rules need
|
|
11
|
+
* is imported directly. */
|
|
12
|
+
import { BACKEND_IDS, BACKEND_KEYS, CLAUDE_AGENT_KEYS, CODEX_BACKEND_KEYS, HARNESS_PI_FLAGS, } from "./config-schema.js";
|
|
13
|
+
import { isJsonObject } from "../files/json-object.js";
|
|
14
|
+
import { typoSuffix } from "../text/suggest.js";
|
|
15
|
+
import { isBackendId } from "./model-selector.js";
|
|
16
|
+
import { reservedPassthroughFlag } from "../agent/backend-traits.js";
|
|
17
|
+
import { checkObjectSection, checkReservedArgs, show, } from "./config-field-checks.js";
|
|
18
|
+
/** Validate the agent-execution block: the bare `backend`, `agentBin`, `baseBranch` and
|
|
19
|
+
* `piArgs` keys, the `backends` object in full, and the legacy spellings' back-compat pair. */
|
|
20
|
+
export function validateAgentsBlock(r, problems, rawFile, helpers) {
|
|
21
|
+
const { checkString, checkStringArray, checkKnownKeys, checkBackendConfig, checkCodexConfigKeys } = helpers;
|
|
22
|
+
// An empty baseBranch would silently fall back to the checked-out branch — the one value
|
|
23
|
+
// the operator's explicit setting must never degrade to unannounced.
|
|
24
|
+
checkString(r, "", "baseBranch", false);
|
|
25
|
+
// An empty agentBin would silently fall back to "pi" — the same silent-ignore class: the
|
|
26
|
+
// operator named a binary, so a blank value must fail validation, not run the default.
|
|
27
|
+
checkString(r, "", "agentBin", false);
|
|
28
|
+
// The agent backend for bare/no-model selectors (plans/agent-backends.md "Config"): a
|
|
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-agent.ts), silently running the wrong backend against a named one.
|
|
31
|
+
checkString(r, "", "backend", false);
|
|
32
|
+
if (typeof r.backend === "string" && !isBackendId(r.backend))
|
|
33
|
+
problems.push(`backend must name a backend (${BACKEND_IDS.join(", ")}; got ${show(r.backend)})${typoSuffix(r.backend, BACKEND_IDS)}`);
|
|
34
|
+
checkStringArray(r, "", "piArgs");
|
|
35
|
+
// A piArgs entry that repeats a harness-managed flag is appended after the harness's own and
|
|
36
|
+
// pi's parser is last-wins, so it silently overrides it (see HARNESS_PI_FLAGS). Name the
|
|
37
|
+
// position and the flag so one edit removes it.
|
|
38
|
+
if (Array.isArray(r.piArgs))
|
|
39
|
+
checkReservedArgs(problems, r.piArgs, "piArgs", (flag) => HARNESS_PI_FLAGS.get(flag));
|
|
40
|
+
// Agent backends (plans/agent-backends.md "Config"): each entry is an object with an
|
|
41
|
+
// optional non-empty `bin` string and an optional array-of-strings `args`. A passthrough arg
|
|
42
|
+
// that repeats a flag the backend's own harness invocation sets is appended last and wins,
|
|
43
|
+
// so it silently overrides the harness — refused here for the backends whose reserved sets
|
|
44
|
+
// are known (claude-args.ts HARNESS_CLAUDE_FLAGS, opencode-args.ts HARNESS_OPENCODE_FLAGS,
|
|
45
|
+
// codex-args.ts HARNESS_CODEX_FLAGS, config-schema.ts HARNESS_PI_FLAGS).
|
|
46
|
+
checkObjectSection(r, "backends", problems, (o) => {
|
|
47
|
+
for (const [id, entry] of Object.entries(o)) {
|
|
48
|
+
const prefix = `backends.${id}`;
|
|
49
|
+
if (!isJsonObject(entry)) {
|
|
50
|
+
problems.push(`${prefix} must be an object with optional bin and args (got ${show(entry)})`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
checkKnownKeys(entry, id === "claude" ? CLAUDE_AGENT_KEYS : id === "codex" ? CODEX_BACKEND_KEYS : BACKEND_KEYS, prefix, problems);
|
|
54
|
+
checkString(entry, `${prefix}.`, "bin", false);
|
|
55
|
+
checkStringArray(entry, `${prefix}.`, "args");
|
|
56
|
+
if (id === "claude" || id === "codex")
|
|
57
|
+
checkBackendConfig(entry, prefix);
|
|
58
|
+
if (id === "codex" && Array.isArray(entry.args))
|
|
59
|
+
checkCodexConfigKeys(entry, prefix);
|
|
60
|
+
if (!Array.isArray(entry.args))
|
|
61
|
+
continue; // Already reported by checkStringArray.
|
|
62
|
+
checkReservedArgs(problems, entry.args, `${prefix}.args`, (flag) => reservedPassthroughFlag(id, flag));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// Legacy back-compat (plans/agent-backends.md "Back-compat"): a file must not spell one pi
|
|
66
|
+
// field both ways. This is a property of the FILE's own keys — the merged config always
|
|
67
|
+
// carries a default `piArgs`, so only the raw pass (rawFile) can tell an explicit legacy
|
|
68
|
+
// value from the default. The effective value resolves through config-views.ts
|
|
69
|
+
// resolvedBackendConfig; a load-time mirror into `agents` would instead leak a duplicate
|
|
70
|
+
// spelling into every later config write.
|
|
71
|
+
if (rawFile && isJsonObject(r.backends)) {
|
|
72
|
+
const pi = r.backends.pi;
|
|
73
|
+
if (isJsonObject(pi)) {
|
|
74
|
+
if (r.agentBin !== undefined && pi.bin !== undefined)
|
|
75
|
+
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)");
|
|
76
|
+
if (r.piArgs !== undefined && pi.args !== undefined)
|
|
77
|
+
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)");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** The model-declaration section rules of tumwater.json validation: the top-level `model`
|
|
2
|
+
* selector-or-map block, and the fallback/fallbackModel block. Extracted from the single
|
|
3
|
+
* validateConfig gate (config-validation.ts) so a model-key edit lands as a per-theme diff
|
|
4
|
+
* instead of a whole-gate one. The two blocks run at different points of the gate — the
|
|
5
|
+
* top-level model block before the agent and numeric sections, the fallback block after the
|
|
6
|
+
* review section — so this module exports two entry points, each called where its block runs
|
|
7
|
+
* today, keeping problem order byte-identical.
|
|
8
|
+
*
|
|
9
|
+
* These rules sit on top of the generic machinery in config-field-checks.ts and
|
|
10
|
+
* config-selector-checks.ts; the helpers passed in are the closures the gate binds once
|
|
11
|
+
* (the problems-bound checkers and the r-derived backend/legacy-provider resolution shared
|
|
12
|
+
* with the gate's own remaining sections). */
|
|
13
|
+
import { MODEL_TRIPLE_KEYS, SELECTOR_ENTRY_KEYS, THINKING_LEVELS, TIER_MAP_KEYS, } from "./config-schema.js";
|
|
14
|
+
import { isJsonObject } from "../files/json-object.js";
|
|
15
|
+
import { isNonBlankString } from "../text/text.js";
|
|
16
|
+
import { checkKnownKeys, checkObjectSection, show, } from "./config-field-checks.js";
|
|
17
|
+
import { checkEntryBackend, checkFallbackList, checkFallbackStep, checkRetiredSelector, checkSelectorHalves, checkSelectorValue, } from "./config-selector-checks.js";
|
|
18
|
+
/** Validate the top-level `model` selector-or-map block and its provider/backend resolution.
|
|
19
|
+
* Keeps the exact problem order the gate produced at this point: the selector/tier-map check,
|
|
20
|
+
* the empty-half check, the retired-spelling no-provider check, the legacy-provider pair and
|
|
21
|
+
* each tier entry. */
|
|
22
|
+
export function validateModelBlock(r, problems, helpers) {
|
|
23
|
+
const { checkModelTriple, checkString, topBackend, checkLegacyProvider } = helpers;
|
|
24
|
+
// The top level's `model` may be a selector string or a map by tier (plans/model-tiers.md);
|
|
25
|
+
// provider/thinking validate as before. A string selector that parses to an empty provider
|
|
26
|
+
// or model half is reported separately (checkSelectorHalves).
|
|
27
|
+
checkModelTriple(r, "", "tier-map");
|
|
28
|
+
checkSelectorHalves(r, "", "model", r.provider, problems);
|
|
29
|
+
if (r.provider === undefined)
|
|
30
|
+
checkRetiredSelector("model", r.model, topBackend, problems);
|
|
31
|
+
checkLegacyProvider(r.provider, "", topBackend);
|
|
32
|
+
// A map's entries are selector strings too, resolved by config-views' topTierSelector with
|
|
33
|
+
// parseModelSelector; the keyed check above sees only the object, so each entry runs the
|
|
34
|
+
// same empty-half check (the map form cannot coexist with a legacy provider, so it parses
|
|
35
|
+
// as a pure selector).
|
|
36
|
+
if (isJsonObject(r.model))
|
|
37
|
+
for (const [tier, sel] of Object.entries(r.model)) {
|
|
38
|
+
if (!isJsonObject(sel)) {
|
|
39
|
+
checkSelectorValue(`model.${tier}`, sel, undefined, problems);
|
|
40
|
+
checkRetiredSelector(`model.${tier}`, sel, topBackend, problems);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// An object tier entry: the model written beside the backend that runs it.
|
|
44
|
+
const at = `model.${tier}`;
|
|
45
|
+
checkKnownKeys(sel, SELECTOR_ENTRY_KEYS, at, problems);
|
|
46
|
+
checkString(sel, `${at}.`, "model", false);
|
|
47
|
+
const t = sel.thinking;
|
|
48
|
+
if (t !== undefined && !(isNonBlankString(t) && THINKING_LEVELS.has(t)))
|
|
49
|
+
problems.push(`${at}.thinking must be one of ${[...THINKING_LEVELS].join(", ")} (got ${show(t)})`);
|
|
50
|
+
checkEntryBackend(sel, at, problems);
|
|
51
|
+
checkSelectorValue(`${at}.model`, sel.model, undefined, problems);
|
|
52
|
+
checkRetiredSelector(`${at}.model`, sel.model, typeof sel.backend === "string" ? sel.backend : topBackend, problems);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Validate the `fallback` string-or-map block and the free-fallback-model (`fallbackModel`)
|
|
56
|
+
* shape. The single call site in the gate is the point where these problems are appended
|
|
57
|
+
* today, so the order across the whole pass is unchanged. */
|
|
58
|
+
export function validateFallbackBlock(r, problems, helpers) {
|
|
59
|
+
const { checkModelTriple, checkLegacyProvider, tierBackend, eachTierBackend } = helpers;
|
|
60
|
+
// The free fallback model (plans/fallback-model.md): shape only — whether the named pair is
|
|
61
|
+
// actually cost-free is a question about pi's models.json, not about this file, so it is
|
|
62
|
+
// answered at run time (src/pi/pi-models.ts) rather than failing a load here. An empty object is
|
|
63
|
+
// rejected: it names nothing, so it would silently never engage.
|
|
64
|
+
// The selector-string shorthand `fallback` (plans/model-tiers.md) validates as one non-empty
|
|
65
|
+
// string; the map form takes per-tier entries whose values are selector strings or "pause"
|
|
66
|
+
// (each tier's entry is consulted when that tier runs at the cap; part 5a/8). Setting both
|
|
67
|
+
// forms — or the map form and `fallbackModel` — is an error naming both keys, since which
|
|
68
|
+
// one wins is not a question the file should ever pose.
|
|
69
|
+
if ("fallback" in r && r.fallback !== undefined) {
|
|
70
|
+
const f = r.fallback;
|
|
71
|
+
if (isJsonObject(f)) {
|
|
72
|
+
checkKnownKeys(f, TIER_MAP_KEYS, "fallback", problems);
|
|
73
|
+
for (const [tier, v] of Object.entries(f)) {
|
|
74
|
+
if (Array.isArray(v))
|
|
75
|
+
checkFallbackList(`fallback.${tier}`, v, problems, tierBackend(tier));
|
|
76
|
+
else if (isJsonObject(v))
|
|
77
|
+
checkFallbackStep(`fallback.${tier}`, v, problems, tierBackend(tier));
|
|
78
|
+
else if (!isNonBlankString(v))
|
|
79
|
+
problems.push(`fallback.${tier} must be a non-empty model id or "pause" (got ${show(v)})`);
|
|
80
|
+
else if (v !== "pause")
|
|
81
|
+
// An entry is a pure selector (config-views' fallbackSelectorFields parses it with
|
|
82
|
+
// no legacy provider in scope); "pause" names no model and stays valid.
|
|
83
|
+
{
|
|
84
|
+
checkSelectorValue(`fallback.${tier}`, v, undefined, problems);
|
|
85
|
+
checkRetiredSelector(`fallback.${tier}`, v, tierBackend(tier), problems);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (Array.isArray(f)) {
|
|
90
|
+
eachTierBackend((b, out) => checkFallbackList("fallback", f, out, b));
|
|
91
|
+
}
|
|
92
|
+
else if (!isNonBlankString(f)) {
|
|
93
|
+
problems.push(`fallback must be a model id or a map by tier (got ${show(f)})`);
|
|
94
|
+
}
|
|
95
|
+
else if (f === "pause") {
|
|
96
|
+
problems.push(`fallback "pause" is only valid as a tier map value — a string fallback must name a model id`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// A selector-string fallback still parses to halves: report an empty one.
|
|
100
|
+
checkSelectorHalves({ fallback: f }, "", "fallback", undefined, problems);
|
|
101
|
+
eachTierBackend((b, out) => checkRetiredSelector("fallback", f, b, out));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if ("fallback" in r && "fallbackModel" in r)
|
|
105
|
+
problems.push(`fallback and fallbackModel both name the budget fallback — keep only one (got fallback ${show(r.fallback)} and fallbackModel ${show(r.fallbackModel)})`);
|
|
106
|
+
checkObjectSection(r, "fallbackModel", problems, (o) => {
|
|
107
|
+
checkKnownKeys(o, MODEL_TRIPLE_KEYS, "fallbackModel", problems);
|
|
108
|
+
checkModelTriple(o, "fallbackModel.");
|
|
109
|
+
// legacyFallbackPair (config-fallbacks.ts) forces a fallbackModel that names its own
|
|
110
|
+
// provider onto backend pi whatever the team's backend, so a legacy provider that is a
|
|
111
|
+
// backend id there is the same silent-pi-pair flaw the checks above reject.
|
|
112
|
+
checkLegacyProvider(o.provider, "fallbackModel.", "pi");
|
|
113
|
+
if (!("provider" in o) && !("model" in o))
|
|
114
|
+
problems.push(`fallbackModel must name a provider or a model (got ${show(o)})`);
|
|
115
|
+
});
|
|
116
|
+
}
|