tumwater 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
/** The budget/tier FALLBACK side of the config views, split out of config-views.ts: how the
|
|
2
2
|
* team falls back at the daily spend cap — which free pair each tier runs on
|
|
3
|
-
* (resolveTierFallbacks and its BORROW_ORDER), the per-role fallback config a
|
|
4
|
-
*
|
|
5
|
-
* that name those pairs (seamsInUse,
|
|
3
|
+
* (resolveTierFallbacks and its BORROW_ORDER), the per-role fallback config a cap episode
|
|
4
|
+
* hands a role (applyFallbackModel), and the surfaces
|
|
5
|
+
* that name those pairs (seamsInUse, tierFallbackLabels). The per-seam
|
|
6
6
|
* resolution these build on — how one role/reviewer/resolver provider/model/thinking resolves
|
|
7
7
|
* (configForRole, reviewConfig, withModelOverrides, …) — stays in config-views.ts, so the
|
|
8
8
|
* one-way dependency runs fallback → seam and never back. Pure, like its sibling: no I/O and
|
|
9
9
|
* no caching. */
|
|
10
10
|
import { MODEL_TIERS, } from "./config-schema.js";
|
|
11
|
-
import { formatModelSelector, parseModelSelector, routeModel } from "./model-selector.js";
|
|
11
|
+
import { formatModelSelector, parseModelSelector, routeModel } from "./config-model-selector.js";
|
|
12
12
|
import { modelPairName } from "../budget/budget.js";
|
|
13
13
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
14
|
-
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
15
14
|
import { enabledRoleIds } from "./config.js";
|
|
16
|
-
import { modelFallbackActive } from "../runner/model-fallback.js";
|
|
17
15
|
import { FALLBACK_REVIEW_TIMEOUT_S, REVIEW_TIMEOUT_S, configForRole, isTierName, pairOverrides, resolvedModelFields, resolverConfig, reviewConfig, reviewRunConfig, roleSeamTier, topTierBackend, topTierSelector, withModelOverrides, } from "./config-views.js";
|
|
18
16
|
/** The resolved fields of one fallback step, keyed by the seam it routes to (PLANS.md "Mixed
|
|
19
17
|
* claude + pi fleets, part 2/4"): the backend is the step's own, else its tier primary's
|
|
@@ -80,6 +78,9 @@ const BORROW_ORDER = {
|
|
|
80
78
|
default: ["default", "strong", "small"],
|
|
81
79
|
strong: ["strong", "default"],
|
|
82
80
|
};
|
|
81
|
+
/** Exported for imageRerouteTarget (src/agent/image-input.ts), which walks a rerouting role's
|
|
82
|
+
* tier through the same borrow order the budget fallback uses (skipping the tier itself). */
|
|
83
|
+
export { BORROW_ORDER };
|
|
83
84
|
/** A tier's OWN fallback chain, first step first — what the `fallback` key declares at that
|
|
84
85
|
* tier's map entry (a selector or a list of them), or (default only) the string/list form of
|
|
85
86
|
* `fallback` or the legacy `fallbackModel` object (a one-element chain). Empty when the tier
|
|
@@ -163,27 +164,10 @@ export function resolveTierFallbacks(config, usable) {
|
|
|
163
164
|
};
|
|
164
165
|
return { small: resolve("small"), default: resolve("default"), strong: resolve("strong") };
|
|
165
166
|
}
|
|
166
|
-
/** The config a role's runs use while a model-fallback episode is active (PLANS.md "Model
|
|
167
|
-
* failure fallback, part 1/2"): the role's tier pair from resolveTierFallbacks (the tier's own
|
|
168
|
-
* pair, else BORROW_ORDER's borrow), shaped like configForRole — provider/model/thinking over
|
|
169
|
-
* the role's own and top-level values, plus the role's minRunIntervalSeconds. Null when the
|
|
170
|
-
* tier resolves to pause (no fallback configured), so the feature stays off. Pure. */
|
|
171
|
-
export function fallbackRoleConfig(config, role) {
|
|
172
|
-
const tier = roleSeamTier(config, role);
|
|
173
|
-
const pair = resolveTierFallbacks(config, () => true)[tier].pair;
|
|
174
|
-
if (pair === null)
|
|
175
|
-
return null;
|
|
176
|
-
const rc = config.roles[baseRoleOf(role)];
|
|
177
|
-
return {
|
|
178
|
-
...withModelOverrides(config, tier, pairOverrides(pair)),
|
|
179
|
-
minRunIntervalSeconds: rc?.minRunIntervalSeconds ?? config.minRunIntervalSeconds,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
167
|
/** Every seam the team can spawn, each with its label: every enabled role's resolved config
|
|
183
168
|
* (configForRole), the review gate when it is on (reviewRunConfig), the conflict resolver
|
|
184
169
|
* (resolverConfig), the director (a catalog role, named explicitly so it is never missed), and
|
|
185
|
-
* each tier's budget fallback (resolveTierFallbacks)
|
|
186
|
-
* (fallbackRoleConfig) runs the same tier pair, so one enumeration covers it. Deduplicated by
|
|
170
|
+
* each tier's budget fallback (resolveTierFallbacks). Deduplicated by
|
|
187
171
|
* label + routed pair so a seam reached through several surfaces is listed once. Read by the
|
|
188
172
|
* startup gate's agent list, doctor's pair sweep (and so its binary/credential/version checks
|
|
189
173
|
* and routing table), and the live-edit reload's newly-routed-backend check — no surface can
|
|
@@ -211,25 +195,6 @@ export function seamsInUse(config) {
|
|
|
211
195
|
}
|
|
212
196
|
return [...seams.values()];
|
|
213
197
|
}
|
|
214
|
-
/** Build a ModelFallbackView for an episode, or null while the episode is not running the
|
|
215
|
-
* fallback (no episode, or a due probe on the primary). role-view.ts's
|
|
216
|
-
* RoleViewPayload.modelFallback and status/status-data.ts's per-role row both build this
|
|
217
|
-
* shape, so one function keeps the two surfaces from disagreeing about the pair, its start,
|
|
218
|
-
* and its reason. `now` is the caller's clock (the status snapshot passes its own poll
|
|
219
|
-
* clock), so a row and its poll agree on what "active" means. */
|
|
220
|
-
export function modelFallbackView(episode, config, role, now = Date.now()) {
|
|
221
|
-
if (episode === undefined || !modelFallbackActive(episode, now))
|
|
222
|
-
return null;
|
|
223
|
-
const episodeConfig = fallbackRoleConfig(config, role);
|
|
224
|
-
if (episodeConfig === null)
|
|
225
|
-
return null;
|
|
226
|
-
return {
|
|
227
|
-
...resolvedModelFields(episodeConfig.provider, episodeConfig.model, episodeConfig.thinking),
|
|
228
|
-
...(episodeConfig.agent !== undefined && episodeConfig.agent !== "pi" ? { backend: episodeConfig.agent } : {}),
|
|
229
|
-
since: episode.since,
|
|
230
|
-
reason: episode.reason,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
198
|
/** The per-tier fallback labels the observability surfaces render (part 7b/8): tier →
|
|
234
199
|
* `provider/model[ (from <tier>)]` for every tier the resolution gives a pair, in tier order.
|
|
235
200
|
* The `(from …)` suffix marks a borrowed pair — a strong-tier borrow is the fact an operator
|
|
@@ -300,6 +265,74 @@ function freeRoleModels(config, free) {
|
|
|
300
265
|
return [id, isTierName(rc?.model) ? stripBackend(rc) : stripModel(rc)];
|
|
301
266
|
}));
|
|
302
267
|
}
|
|
268
|
+
/** A pair's selector as a tier-map entry. Keeps pi's provider; a non-pi pair's model is
|
|
269
|
+
* already the whole id. On a team whose backend is not pi — or a pair routed off pi — the
|
|
270
|
+
* entry names the pair's backend, so the team's own backend cannot take a pi pair's id as
|
|
271
|
+
* its own: a bare selector when the pair rides the team backend and it is pi, else an
|
|
272
|
+
* object entry naming the pair's backend. */
|
|
273
|
+
function pairSelectorOf(config, pair) {
|
|
274
|
+
if (!pair)
|
|
275
|
+
return undefined;
|
|
276
|
+
const selector = formatModelSelector({
|
|
277
|
+
provider: pair.backend === undefined ? pair.provider : undefined,
|
|
278
|
+
model: pair.model ?? "",
|
|
279
|
+
thinking: pair.backend === undefined ? pair.thinking : undefined,
|
|
280
|
+
});
|
|
281
|
+
if (pair.backend === undefined && (config.backend ?? "pi") === "pi")
|
|
282
|
+
return formatModelSelector({ provider: pair.provider, model: pair.model ?? "", thinking: pair.thinking });
|
|
283
|
+
return {
|
|
284
|
+
backend: pair.backend ?? "pi",
|
|
285
|
+
model: selector,
|
|
286
|
+
...(pair.thinking !== undefined ? { thinking: pair.thinking } : {}),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/** What a free seam keeps running: the tier's own primary selector — the backend namespace a
|
|
290
|
+
* resolved seam has cleared, plus the ambient thinking level `topTierSelector` alone omits.
|
|
291
|
+
* A moved seam gets its pair selector, which already carries that ambient level. */
|
|
292
|
+
function freeTierEntry(config, tier, seam) {
|
|
293
|
+
const own = topTierSelector(config, tier);
|
|
294
|
+
// A tier entry that names a backend keeps it as an object entry: a bare selector string
|
|
295
|
+
// would route by namespace and drop the backend the entry chose.
|
|
296
|
+
const backend = topTierBackend(config, tier);
|
|
297
|
+
if (own !== undefined) {
|
|
298
|
+
const thinking = own.thinking ?? config.thinking;
|
|
299
|
+
if (backend !== undefined)
|
|
300
|
+
return {
|
|
301
|
+
backend,
|
|
302
|
+
model: formatModelSelector({ ...own, thinking: undefined }),
|
|
303
|
+
...(thinking !== undefined ? { thinking } : {}),
|
|
304
|
+
};
|
|
305
|
+
else
|
|
306
|
+
return formatModelSelector({ ...own, thinking });
|
|
307
|
+
}
|
|
308
|
+
// A tier with no top-level selector — `agent: "claude"` riding its agent default —
|
|
309
|
+
// has no own selector to carry the level, so name the routed default plus it.
|
|
310
|
+
if (seam.model !== undefined) {
|
|
311
|
+
const routed = { provider: seam.provider, model: seam.model };
|
|
312
|
+
return backend !== undefined
|
|
313
|
+
? {
|
|
314
|
+
backend,
|
|
315
|
+
model: formatModelSelector(routed),
|
|
316
|
+
...(seam.thinking !== undefined ? { thinking: seam.thinking } : {}),
|
|
317
|
+
}
|
|
318
|
+
: formatModelSelector({ ...routed, thinking: seam.thinking });
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
/** A config with no resolved tier map installs the single fallback pair at the top level,
|
|
323
|
+
* keeping every other value (and the reviewer's time budget floor) as in the resolved-map
|
|
324
|
+
* path's legacy peer. */
|
|
325
|
+
function legacyPairConfig(config, pair) {
|
|
326
|
+
return {
|
|
327
|
+
...config,
|
|
328
|
+
...(pair.backend !== undefined || config.backend !== undefined ? { backend: pair.backend ?? "pi" } : {}),
|
|
329
|
+
provider: pair.backend === undefined ? pair.provider : undefined,
|
|
330
|
+
model: pair.model,
|
|
331
|
+
thinking: pair.thinking,
|
|
332
|
+
review: fallbackReview(config.review),
|
|
333
|
+
roles: stripRoleModels(config.roles, false),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
303
336
|
/** The config as seen by a role running on the free fallback model
|
|
304
337
|
* (plans/fallback-model.md): the fallback's provider/model/thinking installed as the top-level
|
|
305
338
|
* values AND every per-role and reviewer model override dropped, so that EVERY seam that could
|
|
@@ -323,26 +356,6 @@ function freeRoleModels(config, free) {
|
|
|
323
356
|
* model, while every priced seam still lands on its tier's fallback pair. */
|
|
324
357
|
export function applyFallbackModel(config, resolved, free) {
|
|
325
358
|
if (resolved) {
|
|
326
|
-
// A pair's selector keeps pi's provider; a non-pi pair's model is already the whole id. On a
|
|
327
|
-
// team whose backend is not pi — or a pair routed off pi — the entry names the pair's
|
|
328
|
-
// backend, so the team's own backend cannot take a pi pair's id as its own.
|
|
329
|
-
const selectorOf = (f) => {
|
|
330
|
-
const pair = f.pair;
|
|
331
|
-
if (!pair)
|
|
332
|
-
return undefined;
|
|
333
|
-
const selector = formatModelSelector({
|
|
334
|
-
provider: pair.backend === undefined ? pair.provider : undefined,
|
|
335
|
-
model: pair.model ?? "",
|
|
336
|
-
thinking: pair.backend === undefined ? pair.thinking : undefined,
|
|
337
|
-
});
|
|
338
|
-
if (pair.backend === undefined && (config.backend ?? "pi") === "pi")
|
|
339
|
-
return formatModelSelector({ provider: pair.provider, model: pair.model ?? "", thinking: pair.thinking });
|
|
340
|
-
return {
|
|
341
|
-
backend: pair.backend ?? "pi",
|
|
342
|
-
model: selector,
|
|
343
|
-
...(pair.thinking !== undefined ? { thinking: pair.thinking } : {}),
|
|
344
|
-
};
|
|
345
|
-
};
|
|
346
359
|
const model = {};
|
|
347
360
|
const stepCaps = {};
|
|
348
361
|
for (const tier of MODEL_TIERS) {
|
|
@@ -351,37 +364,12 @@ export function applyFallbackModel(config, resolved, free) {
|
|
|
351
364
|
// seam gets its pair selector, which already carries that ambient level.
|
|
352
365
|
const seam = withModelOverrides(config, tier, {});
|
|
353
366
|
if (free?.(seam)) {
|
|
354
|
-
const own =
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const backend = topTierBackend(config, tier);
|
|
358
|
-
if (own !== undefined) {
|
|
359
|
-
const thinking = own.thinking ?? config.thinking;
|
|
360
|
-
if (backend !== undefined)
|
|
361
|
-
model[tier] = {
|
|
362
|
-
backend,
|
|
363
|
-
model: formatModelSelector({ ...own, thinking: undefined }),
|
|
364
|
-
...(thinking !== undefined ? { thinking } : {}),
|
|
365
|
-
};
|
|
366
|
-
else
|
|
367
|
-
model[tier] = formatModelSelector({ ...own, thinking });
|
|
368
|
-
}
|
|
369
|
-
// A tier with no top-level selector — `agent: "claude"` riding its agent default —
|
|
370
|
-
// has no own selector to carry the level, so name the routed default plus it.
|
|
371
|
-
else if (seam.model !== undefined) {
|
|
372
|
-
const routed = { provider: seam.provider, model: seam.model };
|
|
373
|
-
model[tier] =
|
|
374
|
-
backend !== undefined
|
|
375
|
-
? {
|
|
376
|
-
backend,
|
|
377
|
-
model: formatModelSelector(routed),
|
|
378
|
-
...(seam.thinking !== undefined ? { thinking: seam.thinking } : {}),
|
|
379
|
-
}
|
|
380
|
-
: formatModelSelector({ ...routed, thinking: seam.thinking });
|
|
381
|
-
}
|
|
367
|
+
const own = freeTierEntry(config, tier, seam);
|
|
368
|
+
if (own !== undefined)
|
|
369
|
+
model[tier] = own;
|
|
382
370
|
continue;
|
|
383
371
|
}
|
|
384
|
-
const sel =
|
|
372
|
+
const sel = pairSelectorOf(config, resolved[tier].pair);
|
|
385
373
|
if (sel !== undefined)
|
|
386
374
|
model[tier] = sel;
|
|
387
375
|
const cap = resolved[tier].pair?.maxDailyCostUsd;
|
|
@@ -401,13 +389,5 @@ export function applyFallbackModel(config, resolved, free) {
|
|
|
401
389
|
const pair = fallbackPair(config);
|
|
402
390
|
if (!pair)
|
|
403
391
|
return config;
|
|
404
|
-
return
|
|
405
|
-
...config,
|
|
406
|
-
...(pair.backend !== undefined || config.backend !== undefined ? { backend: pair.backend ?? "pi" } : {}),
|
|
407
|
-
provider: pair.backend === undefined ? pair.provider : undefined,
|
|
408
|
-
model: pair.model,
|
|
409
|
-
thinking: pair.thinking,
|
|
410
|
-
review: fallbackReview(config.review),
|
|
411
|
-
roles: stripRoleModels(config.roles, false),
|
|
412
|
-
};
|
|
392
|
+
return legacyPairConfig(config, pair);
|
|
413
393
|
}
|
|
@@ -96,6 +96,22 @@ export const AT_LEAST_ONE = { ok: (n) => n >= 1, what: "a number of at least 1"
|
|
|
96
96
|
* duration rules (config-validation.ts) and checkMinRunInterval (config-keys.ts), so the
|
|
97
97
|
* dashboard's interval editor cannot drift from the schema's bound. */
|
|
98
98
|
export const MAX_DURATION_SECONDS = 2147483;
|
|
99
|
+
/** The seconds-valued duration rules, bounded by MAX_DURATION_SECONDS so a `×1000` delay stays
|
|
100
|
+
* inside node's signed-32-bit range. One home for the family, so the whole-gate section
|
|
101
|
+
* (config-validation.ts) and the per-role section (config-validation-roles.ts) name the same
|
|
102
|
+
* rules instead of each declaring DURATION_NON_NEGATIVE for itself. */
|
|
103
|
+
export const DURATION_POSITIVE = {
|
|
104
|
+
ok: (n) => n > 0 && n <= MAX_DURATION_SECONDS,
|
|
105
|
+
what: `a number greater than 0, at most ${MAX_DURATION_SECONDS}`,
|
|
106
|
+
};
|
|
107
|
+
export const DURATION_NON_NEGATIVE = {
|
|
108
|
+
ok: (n) => n >= 0 && n <= MAX_DURATION_SECONDS,
|
|
109
|
+
what: `a number of 0 or more, at most ${MAX_DURATION_SECONDS}`,
|
|
110
|
+
};
|
|
111
|
+
export const DURATION_OR_DISABLED = {
|
|
112
|
+
ok: (n) => n >= 0 && n <= MAX_DURATION_SECONDS,
|
|
113
|
+
what: `a number of 0 or more, at most ${MAX_DURATION_SECONDS} (0 disables)`,
|
|
114
|
+
};
|
|
99
115
|
/** Shared known-role guard for the per-role config sections (`roles.<id>` entries and
|
|
100
116
|
* `maxDailyCostUsdPerRole.<id>` keys): an id outside the role catalog and customRoles cannot
|
|
101
117
|
* work — a `roles` typo spawns a phantom role that errors every run forever; a caps typo
|
|
@@ -120,6 +136,53 @@ export function checkStringField(problems, obj, prefix, key, allowEmpty = true)
|
|
|
120
136
|
problems.push(`${prefix}${key} must not be empty (got ${show(v)})`);
|
|
121
137
|
});
|
|
122
138
|
}
|
|
139
|
+
function checkModelSiblingFields(problems, obj, prefix) {
|
|
140
|
+
for (const key of MODEL_TRIPLE_KEYS)
|
|
141
|
+
if (key !== "model")
|
|
142
|
+
checkStringField(problems, obj, prefix, key, false);
|
|
143
|
+
const t = obj.thinking;
|
|
144
|
+
if (isNonBlankString(t) && !THINKING_LEVELS.has(t))
|
|
145
|
+
problems.push(`${prefix}thinking must be one of ${[...THINKING_LEVELS].join(", ")} (got ${show(t)})${typoSuffix(t, [...THINKING_LEVELS])}`);
|
|
146
|
+
}
|
|
147
|
+
function checkTierMapModel(problems, obj, prefix, m) {
|
|
148
|
+
checkKnownKeys(m, TIER_MAP_KEYS, `${prefix}model`, problems);
|
|
149
|
+
if (obj.provider !== undefined)
|
|
150
|
+
problems.push(`${prefix}model as a map by tier cannot coexist with ${prefix ? prefix : "the legacy top-level "}provider — move the provider into each tier's model id (got provider ${show(obj.provider)})`);
|
|
151
|
+
for (const [tier, v] of Object.entries(m))
|
|
152
|
+
// An object entry (`{ backend?, model?, thinking? }`) is validated by validateConfig.
|
|
153
|
+
if (!isNonBlankString(v) && !isJsonObject(v))
|
|
154
|
+
problems.push(`${prefix}model.${tier} must be a non-empty model id (got ${show(v)})`);
|
|
155
|
+
}
|
|
156
|
+
function whatModelMustBe(modelShape) {
|
|
157
|
+
return modelShape === "tier-map"
|
|
158
|
+
? "a model id or a map by tier (small, default, strong)"
|
|
159
|
+
: modelShape === "tier-name"
|
|
160
|
+
? "a model id or a tier name (small, default, strong)"
|
|
161
|
+
: "a string";
|
|
162
|
+
}
|
|
163
|
+
function checkStringModel(problems, obj, prefix, m, modelShape) {
|
|
164
|
+
if (typeof m !== "string") {
|
|
165
|
+
problems.push(`${prefix}model must be ${whatModelMustBe(modelShape)} (got ${show(m)})`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (m.trim() === "") {
|
|
169
|
+
problems.push(`${prefix}model must not be empty (got ${show(m)})`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (MODEL_TIERS.includes(m)) {
|
|
173
|
+
if (modelShape === "tier-name")
|
|
174
|
+
return; // A valid tier reference.
|
|
175
|
+
problems.push(`${prefix}model must name a model id — tier names are valid only as roles.<id>.model or review.model values (got ${show(m)})`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (modelShape === "tier-name" && !m.includes("/") && obj.provider === undefined)
|
|
179
|
+
suggestTierFix(problems, prefix, m);
|
|
180
|
+
}
|
|
181
|
+
function suggestTierFix(problems, prefix, m) {
|
|
182
|
+
const tier = suggestClosest(m, MODEL_TIERS);
|
|
183
|
+
if (tier !== null)
|
|
184
|
+
problems.push(`${prefix}model ${show(m)} is not a tier name or a model id pi can resolve${didYouMean(tier)}`);
|
|
185
|
+
}
|
|
123
186
|
/** Every model-override section validates the same provider/model/thinking triple the same
|
|
124
187
|
* way: empty strings are rejected (see checkStringField) and thinking is value-checked —
|
|
125
188
|
* pi only recognizes THINKING_LEVELS and silently drops anything else, so a typo would run
|
|
@@ -137,49 +200,15 @@ export function checkStringField(problems, obj, prefix, key, allowEmpty = true)
|
|
|
137
200
|
* places that accept it, so a misplaced `"model": "strong"` is an error naming its fix instead of a
|
|
138
201
|
* bare model pattern pi can never resolve. */
|
|
139
202
|
export function checkModelTripleField(problems, obj, prefix, modelShape = "selector") {
|
|
140
|
-
|
|
141
|
-
if (key !== "model")
|
|
142
|
-
checkStringField(problems, obj, prefix, key, false);
|
|
143
|
-
const t = obj.thinking;
|
|
144
|
-
if (isNonBlankString(t) && !THINKING_LEVELS.has(t))
|
|
145
|
-
problems.push(`${prefix}thinking must be one of ${[...THINKING_LEVELS].join(", ")} (got ${show(t)})${typoSuffix(t, [...THINKING_LEVELS])}`);
|
|
203
|
+
checkModelSiblingFields(problems, obj, prefix);
|
|
146
204
|
const m = obj.model;
|
|
147
205
|
if (m === undefined)
|
|
148
206
|
return;
|
|
149
207
|
if (modelShape === "tier-map" && isJsonObject(m)) {
|
|
150
|
-
|
|
151
|
-
if (obj.provider !== undefined)
|
|
152
|
-
problems.push(`${prefix}model as a map by tier cannot coexist with ${prefix ? prefix : "the legacy top-level "}provider — move the provider into each tier's model id (got provider ${show(obj.provider)})`);
|
|
153
|
-
for (const [tier, v] of Object.entries(m))
|
|
154
|
-
// An object entry (`{ backend?, model?, thinking? }`) is validated by validateConfig.
|
|
155
|
-
if (!isNonBlankString(v) && !isJsonObject(v))
|
|
156
|
-
problems.push(`${prefix}model.${tier} must be a non-empty model id (got ${show(v)})`);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (typeof m !== "string") {
|
|
160
|
-
const what = modelShape === "tier-map"
|
|
161
|
-
? "a model id or a map by tier (small, default, strong)"
|
|
162
|
-
: modelShape === "tier-name"
|
|
163
|
-
? "a model id or a tier name (small, default, strong)"
|
|
164
|
-
: "a string";
|
|
165
|
-
problems.push(`${prefix}model must be ${what} (got ${show(m)})`);
|
|
208
|
+
checkTierMapModel(problems, obj, prefix, m);
|
|
166
209
|
return;
|
|
167
210
|
}
|
|
168
|
-
|
|
169
|
-
problems.push(`${prefix}model must not be empty (got ${show(m)})`);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (MODEL_TIERS.includes(m)) {
|
|
173
|
-
if (modelShape === "tier-name")
|
|
174
|
-
return; // A valid tier reference.
|
|
175
|
-
problems.push(`${prefix}model must name a model id — tier names are valid only as roles.<id>.model or review.model values (got ${show(m)})`);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (modelShape === "tier-name" && !m.includes("/") && obj.provider === undefined) {
|
|
179
|
-
const tier = suggestClosest(m, MODEL_TIERS);
|
|
180
|
-
if (tier !== null)
|
|
181
|
-
problems.push(`${prefix}model ${show(m)} is not a tier name or a model id pi can resolve${didYouMean(tier)}`);
|
|
182
|
-
}
|
|
211
|
+
checkStringModel(problems, obj, prefix, m, modelShape);
|
|
183
212
|
}
|
|
184
213
|
/** Validate one numeric field against a NumberRule when present. */
|
|
185
214
|
export function checkNumberField(problems, obj, prefix, key, rule) {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* writers and the read side show. */
|
|
12
12
|
import { BACKEND_KEYS, BACKOFF_KEYS, BOOTSTRAP_KEYS, CHECK_KEYS, CLAUDE_AGENT_KEYS, CODEX_BACKEND_KEYS, REVIEW_KEYS, ROLE_ENTRY_KEYS, TIER_MAP_KEYS, TOP_LEVEL_KEYS } from "./config-schema.js";
|
|
13
13
|
import { typoSuffix } from "../text/suggest.js";
|
|
14
|
-
import { MAX_DURATION_SECONDS,
|
|
14
|
+
import { MAX_DURATION_SECONDS, show } from "./config-field-checks.js";
|
|
15
15
|
import { parseQuietHours } from "../scheduling/quiet-hours.js";
|
|
16
16
|
/** One definition of "a valid daily budget cap" (the TUI's Ctrl+B editor and the GUI's
|
|
17
17
|
* /api/budget endpoint both run their input through it): a finite number of 0 or more —
|
|
@@ -31,18 +31,6 @@ export function checkDailyBudgetUsd(value, key = "maxDailyCostUsd") {
|
|
|
31
31
|
return `${key} must be a number of 0 or more, at most ${Number.MAX_SAFE_INTEGER}, 0 disables (got ${show(value)})`;
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
|
-
/** One definition of "a valid per-provider concurrency cap" (plans/provider-concurrency.md
|
|
35
|
-
* §1): a positive integer, accepted for any (possibly unknown) provider name — a fallback's
|
|
36
|
-
* provider may only come into use later, so the key is not checked against a catalog. The one
|
|
37
|
-
* validation both `config set maxConcurrentPerProvider.<provider>` and validateConfig show,
|
|
38
|
-
* so they cannot drift apart on what a valid cap is. `key` names the config key in the error,
|
|
39
|
-
* defaulting to the map name; the dotted map passes `maxConcurrentPerProvider.<provider>`
|
|
40
|
-
* so a rejected entry names the key actually set. */
|
|
41
|
-
export function checkProviderConcurrencyCap(value, key = "maxConcurrentPerProvider") {
|
|
42
|
-
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1)
|
|
43
|
-
return `${key} must be ${POSITIVE_INTEGER.what} (got ${show(value)})`;
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
34
|
/** One definition of "a valid quietHours window" (src/scheduling/quiet-hours.ts owns the format):
|
|
47
35
|
* "HH:MM-HH:MM" in local time, wrapping permitted, empty means off. parseQuietHours's
|
|
48
36
|
* message is the one wording every surface (validateConfig, `config set`, the TUI/GUI
|
|
@@ -78,12 +66,9 @@ export const PER_KEY_VALIDATORS = {
|
|
|
78
66
|
};
|
|
79
67
|
/** The per-role maps `config get/set` reaches with dotted keys: each names ONE entry
|
|
80
68
|
* (`maxDailyCostUsdPerRole.<role>`, `quietHoursPerRole.<role>`) that merges into the
|
|
81
|
-
* existing map, so raising one role's cap never requires re-typing another's. The
|
|
82
|
-
*
|
|
83
|
-
|
|
84
|
-
* check the per-role maps apply. The per-key value validator runs before the write, the same
|
|
85
|
-
* screening a bare top-level key gets. */
|
|
86
|
-
export const DOTTED_MAP_KEYS = ["maxDailyCostUsdPerRole", "quietHoursPerRole", "maxConcurrentPerProvider"];
|
|
69
|
+
* existing map, so raising one role's cap never requires re-typing another's. The per-key
|
|
70
|
+
* value validator runs before the write, the same screening a bare top-level key gets. */
|
|
71
|
+
export const DOTTED_MAP_KEYS = ["maxDailyCostUsdPerRole", "quietHoursPerRole"];
|
|
87
72
|
/** The fixed-shape top-level sections `config get/set/unset` reaches one field of with a
|
|
88
73
|
* two-segment key (`review.model`, `bootstrap.untilPlansDone`), each with its field list. */
|
|
89
74
|
export const DOTTED_SECTION_FIELDS = {
|
|
@@ -97,26 +82,36 @@ export const DOTTED_SECTION_FIELDS = {
|
|
|
97
82
|
* (`modle.small`, `maxDailyCostUsdPerRoll.feature`) gets the same did-you-mean the sibling
|
|
98
83
|
* unknown-key errors attach. */
|
|
99
84
|
const DOTTED_KEY_HEADS = [...DOTTED_MAP_KEYS, "roles", "model", "backends", ...Object.keys(DOTTED_SECTION_FIELDS)];
|
|
100
|
-
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
export function parseConfigKey(key) {
|
|
105
|
-
if (!key.includes("."))
|
|
106
|
-
return { kind: "top", key };
|
|
107
|
-
const parts = key.split(".");
|
|
85
|
+
/** The per-role map entry `maxDailyCostUsdPerRole.<role>` / `quietHoursPerRole.<role>` — a
|
|
86
|
+
* whole-value bare key with no deeper segment names one map entry. Returns undefined when
|
|
87
|
+
* the shape is not that of a two-segment per-role map entry. */
|
|
88
|
+
function parseMapEntry(parts) {
|
|
108
89
|
const first = parts[0] ?? "";
|
|
109
90
|
const second = parts.length > 1 ? parts[1] ?? "" : undefined;
|
|
110
91
|
const third = parts.length > 2 ? parts[2] ?? "" : undefined;
|
|
111
92
|
if (third === undefined && second !== undefined && second !== "" && DOTTED_MAP_KEYS.includes(first))
|
|
112
93
|
return { kind: "map", map: first, role: second };
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
/** One role-entry field `roles.<id>.<field>` (e.g. `roles.qa.model`), with both segments
|
|
97
|
+
* non-empty. Returns undefined when the shape is not a three-segment `roles.` key. */
|
|
98
|
+
function parseRoleField(parts) {
|
|
99
|
+
const first = parts[0] ?? "";
|
|
100
|
+
const second = parts.length > 1 ? parts[1] ?? "" : undefined;
|
|
101
|
+
const third = parts.length > 2 ? parts[2] ?? "" : undefined;
|
|
113
102
|
if (parts.length === 3 && first === "roles" && second !== undefined && second !== "" && third !== undefined && third !== "")
|
|
114
103
|
return { kind: "role", id: second, field: third };
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
/** `backends.<id>.<field>` names one field of one agent backend's entry (the way
|
|
107
|
+
* `roles.<id>.<field>` names one role field). The id is shape-checked only — routing owns
|
|
108
|
+
* which agent ids exist (part 4b); a typo'd field is refused here with the valid ones named. */
|
|
109
|
+
function parseBackendField(parts) {
|
|
110
|
+
const first = parts[0] ?? "";
|
|
111
|
+
const second = parts.length > 1 ? parts[1] ?? "" : undefined;
|
|
112
|
+
const third = parts.length > 2 ? parts[2] ?? "" : undefined;
|
|
118
113
|
if (parts.length === 3 && first === "backends" && second !== undefined && second !== "" && third !== undefined) {
|
|
119
|
-
// configDir
|
|
114
|
+
// configDir is claude/codex-only (CLAUDE_AGENT_KEYS / CODEX_BACKEND_KEYS):
|
|
120
115
|
// a config set backends.pi.configDir would write a key nothing reads, so it is refused
|
|
121
116
|
// everywhere but claude and codex.
|
|
122
117
|
const validKeys = second === "claude" ? CLAUDE_AGENT_KEYS : second === "codex" ? CODEX_BACKEND_KEYS : BACKEND_KEYS;
|
|
@@ -127,10 +122,15 @@ export function parseConfigKey(key) {
|
|
|
127
122
|
error: `unknown backend field "${third}" for backends.${second} (valid fields: ${validKeys.join(", ")})${typoSuffix(third, validKeys)}`,
|
|
128
123
|
};
|
|
129
124
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
/** `model.<tier>` names one entry of the tier model map — the way `roles.<id>.model`
|
|
128
|
+
* names one role field, `maxDailyCostUsdPerRole.<role>` names one map entry. The tier
|
|
129
|
+
* must be a real tier: `config set model.turbo x` would otherwise write a dead map key
|
|
130
|
+
* the runtime (and doctor's checkKnownKeys) would silently never resolve. */
|
|
131
|
+
function parseModelTier(parts) {
|
|
132
|
+
const first = parts[0] ?? "";
|
|
133
|
+
const second = parts.length > 1 ? parts[1] ?? "" : undefined;
|
|
134
134
|
if (parts.length === 2 && first === "model") {
|
|
135
135
|
if ((TIER_MAP_KEYS).includes(second ?? ""))
|
|
136
136
|
return { kind: "tier", map: "model", tier: second };
|
|
@@ -139,8 +139,13 @@ export function parseConfigKey(key) {
|
|
|
139
139
|
error: `unknown model tier "${second ?? ""}" — a dotted \`model.<tier>\` names one of ${TIER_MAP_KEYS.join(", ")} (e.g. \`config set model.strong huggingface/zai-org/GLM-5.3-Flash:together:low\`)${typoSuffix(second ?? "", TIER_MAP_KEYS)}`,
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
/** `<section>.<field>` names one field of a fixed-shape section (review, check, idleBackoff,
|
|
145
|
+
* bootstrap); the field must be one the schema lists, or the write would be a dead key. */
|
|
146
|
+
function parseSectionField(parts) {
|
|
147
|
+
const first = parts[0] ?? "";
|
|
148
|
+
const second = parts.length > 1 ? parts[1] ?? "" : undefined;
|
|
144
149
|
if (parts.length === 2 && Object.hasOwn(DOTTED_SECTION_FIELDS, first)) {
|
|
145
150
|
const section = first;
|
|
146
151
|
const fields = DOTTED_SECTION_FIELDS[section];
|
|
@@ -151,9 +156,14 @@ export function parseConfigKey(key) {
|
|
|
151
156
|
error: `unknown ${first} field "${second ?? ""}" (valid fields: ${fields.join(", ")})${typoSuffix(second ?? "", fields)}`,
|
|
152
157
|
};
|
|
153
158
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
/** The generic dotted-shape rejection: the head is not one of the accepted shapes in a way
|
|
162
|
+
* the earlier parsers would have claimed, so name the supported dotted forms wholesale. A
|
|
163
|
+
* typo'd first segment is the common miss (`maxDailyCostUsdPerRoll.feature`, `modle.small`);
|
|
164
|
+
* when the head is one of the valid ones the shape itself is wrong, so suggesting that head
|
|
165
|
+
* back would mislead — hence the hint only when the head is unknown. */
|
|
166
|
+
function dottedShapeError(key, first) {
|
|
157
167
|
const headHint = DOTTED_KEY_HEADS.includes(first)
|
|
158
168
|
? ""
|
|
159
169
|
: typoSuffix(first, DOTTED_KEY_HEADS);
|
|
@@ -162,6 +172,22 @@ export function parseConfigKey(key) {
|
|
|
162
172
|
error: `unknown config key "${key}" — dotted keys name one entry of a per-role map or one field of a role entry (e.g. \`maxDailyCostUsdPerRole.feature 1.5\`, \`roles.qa.model x\`, \`review.model x\`); a bare key names a whole top-level value${headHint}`,
|
|
163
173
|
};
|
|
164
174
|
}
|
|
175
|
+
/** Split a config key into its write/get shape: `foo` is top-level,
|
|
176
|
+
* `maxDailyCostUsdPerRole.feature` / `quietHoursPerRole.qa` name one map entry,
|
|
177
|
+
* `roles.qa.model` names one role-entry field, and anything else dotted is an error
|
|
178
|
+
* naming the supported dotted forms. */
|
|
179
|
+
export function parseConfigKey(key) {
|
|
180
|
+
if (!key.includes("."))
|
|
181
|
+
return { kind: "top", key };
|
|
182
|
+
const parts = key.split(".");
|
|
183
|
+
const first = parts[0] ?? "";
|
|
184
|
+
return (parseMapEntry(parts) ??
|
|
185
|
+
parseRoleField(parts) ??
|
|
186
|
+
parseBackendField(parts) ??
|
|
187
|
+
parseModelTier(parts) ??
|
|
188
|
+
parseSectionField(parts) ??
|
|
189
|
+
dottedShapeError(key, first));
|
|
190
|
+
}
|
|
165
191
|
/** The error `tumwater config get <key>` and `config set <key> <value>` both surface for a
|
|
166
192
|
* key outside TOP_LEVEL_KEYS — the valid-keys list plus text.ts's did-you-mean suggestion —
|
|
167
193
|
* or null when the key is known. One home for the phrasing so whichever verb misspells a
|
|
@@ -5,9 +5,9 @@ import { bestEffortEventLogger, TUMWATER_SOURCE } from "../events/events.js";
|
|
|
5
5
|
import { RoleRunner } from "../runner/runner.js";
|
|
6
6
|
import { configPath } from "../paths.js";
|
|
7
7
|
import { errorMessage } from "../text/text.js";
|
|
8
|
-
import { backendsInUse, seedOpencodeCatalog } from "../gates/startup-
|
|
8
|
+
import { backendsInUse, seedOpencodeCatalog } from "../gates/startup-gates.js";
|
|
9
9
|
import { seedClaudeAccount } from "../agent/claude/claude-account.js";
|
|
10
|
-
import { seedCodexAccount, readCodexAccount } from "../agent/codex/codex-account.js";
|
|
10
|
+
import { codexApiKeyRefusalMessage, seedCodexAccount, readCodexAccount } from "../agent/codex/codex-account.js";
|
|
11
11
|
import { backendMissingMessage, findBackendBinary, resolveBackendBin } from "../pi/pi-bin.js";
|
|
12
12
|
/** A live config reloader over the given wiring; poll it once per scheduler cycle (see
|
|
13
13
|
* LiveConfigReload for the contract). */
|
|
@@ -26,10 +26,6 @@ export function newLiveConfigReload(deps) {
|
|
|
26
26
|
// The cap last applied to the semaphore (live-resized on each reload), so a change logs
|
|
27
27
|
// exactly one event per distinct value — not once per poll.
|
|
28
28
|
let lastMaxConcurrent = Math.max(1, deps.config.maxConcurrent);
|
|
29
|
-
// The per-provider caps last applied to the semaphore (live-resized on each reload), so a
|
|
30
|
-
// change logs exactly one event per distinct value — not once per poll. Seeded from the
|
|
31
|
-
// startup config, so the first poll of an unchanged file emits nothing.
|
|
32
|
-
let lastKeyCaps = deps.config.maxConcurrentPerProvider ?? {};
|
|
33
29
|
// Roots this process has already attempted a live-edit claude-account seed for (see the
|
|
34
30
|
// reload block below), so repeated polls of a config that keeps a claude seam cannot respawn
|
|
35
31
|
// `claude auth status` on every cycle.
|
|
@@ -86,7 +82,7 @@ export function newLiveConfigReload(deps) {
|
|
|
86
82
|
if (codexLiveRefused)
|
|
87
83
|
return true;
|
|
88
84
|
codexLiveRefused = true;
|
|
89
|
-
warn(
|
|
85
|
+
warn(`refusing live edit: ${codexApiKeyRefusalMessage("<home>")}`);
|
|
90
86
|
return true;
|
|
91
87
|
}
|
|
92
88
|
// A poll that did not refuse clears the warn-once marker so a later re-entry into the
|
|
@@ -154,7 +150,7 @@ export function newLiveConfigReload(deps) {
|
|
|
154
150
|
}
|
|
155
151
|
}
|
|
156
152
|
}
|
|
157
|
-
// Live-resize the concurrency cap
|
|
153
|
+
// Live-resize the concurrency cap.
|
|
158
154
|
function resizeConcurrency(nextConfig) {
|
|
159
155
|
// Live-resize the concurrency cap: a mid-run edit changes how many pi runs execute
|
|
160
156
|
// concurrently within this poll — no restart. Growing admits already-queued runs;
|
|
@@ -165,26 +161,6 @@ export function newLiveConfigReload(deps) {
|
|
|
165
161
|
emit({ source: TUMWATER_SOURCE, type: "max_concurrent_changed", from: lastMaxConcurrent, to: newMaxConcurrent });
|
|
166
162
|
lastMaxConcurrent = newMaxConcurrent;
|
|
167
163
|
}
|
|
168
|
-
// Live-resize the per-provider caps (plans/provider-concurrency.md §7): an edit
|
|
169
|
-
// re-applies the whole map to the pool, and each changed provider fires its own
|
|
170
|
-
// `provider_concurrency_changed` event. setKeyCaps never preempts — shrinking only
|
|
171
|
-
// withholds future grants on that key until releases drain it under the new cap.
|
|
172
|
-
const nextKeyCaps = nextConfig.maxConcurrentPerProvider ?? {};
|
|
173
|
-
deps.semaphore.setKeyCaps(nextKeyCaps);
|
|
174
|
-
for (const provider of new Set([...Object.keys(lastKeyCaps), ...Object.keys(nextKeyCaps)])) {
|
|
175
|
-
const from = lastKeyCaps[provider];
|
|
176
|
-
const to = nextKeyCaps[provider];
|
|
177
|
-
if (from !== to) {
|
|
178
|
-
emit({
|
|
179
|
-
source: TUMWATER_SOURCE,
|
|
180
|
-
type: "provider_concurrency_changed",
|
|
181
|
-
provider,
|
|
182
|
-
from: from ?? 0,
|
|
183
|
-
to: to ?? 0,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
lastKeyCaps = nextKeyCaps;
|
|
188
164
|
}
|
|
189
165
|
// Sync the runner list to newly enabled roles and warn on enable/disable.
|
|
190
166
|
function syncRunners(nextConfig) {
|
|
@@ -65,12 +65,12 @@ const BACKEND_DEFAULT_MODELS = {
|
|
|
65
65
|
// belongs in tumwater.json, not in code.
|
|
66
66
|
opencode: {},
|
|
67
67
|
// codex's tier defaults (plans/codex-backend.md decision 4), from the catalog's own
|
|
68
|
-
// descriptions (design finding 14): strong is `gpt-6-
|
|
69
|
-
//
|
|
68
|
+
// descriptions (design finding 14): strong is `gpt-6.1-sol`, default and small are
|
|
69
|
+
// `gpt-6-luna`. The director runs the strong tier, as on claude.
|
|
70
70
|
codex: {
|
|
71
71
|
small: "gpt-6-luna",
|
|
72
|
-
default: "gpt-6
|
|
73
|
-
strong: "gpt-6-
|
|
72
|
+
default: "gpt-6-luna",
|
|
73
|
+
strong: "gpt-6.1-sol",
|
|
74
74
|
},
|
|
75
75
|
};
|
|
76
76
|
/** The backend and concrete model a resolved seam's provider/model pair routes to
|