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
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* saving, the role-selection helpers) lives in config.ts, the write side in config-write.ts
|
|
8
8
|
* (editor verbs) and config-request.ts (the director's staged request). */
|
|
9
9
|
import { MODEL_TIERS } from "./config-schema.js";
|
|
10
|
-
import { formatModelSelector, parseModelSelector, routeModel } from "./model-selector.js";
|
|
10
|
+
import { formatModelSelector, parseModelSelector, routeModel } from "./config-model-selector.js";
|
|
11
11
|
import { modelPairName } from "../budget/budget.js";
|
|
12
12
|
import { isJsonObject } from "../files/json-object.js";
|
|
13
13
|
import { DIRECTOR_ROLE, roleById } from "../roles/roles.js";
|
|
14
14
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
15
|
+
import { sampleHostLoad } from "../build/host-sleep.js";
|
|
16
|
+
import { starvedDeadlineMs } from "../build/build-check-events.js";
|
|
15
17
|
/** The effective `backends.<id>` entry as a run sees it (plans/agent-backends.md "Back-compat"):
|
|
16
18
|
* the file's own entry, with the legacy top-level `agentBin`/`piArgs` read as the pi backend's
|
|
17
19
|
* `bin`/`args` when the new form leaves a field absent. Pure — it never writes those legacy
|
|
@@ -31,20 +33,6 @@ export function resolvedBackendConfig(config, id) {
|
|
|
31
33
|
}
|
|
32
34
|
return { bin: entry?.bin ?? config.agentBin, args: (entry?.args ?? config.piArgs ?? []).slice() };
|
|
33
35
|
}
|
|
34
|
-
/** A backend's subscription-window reserve (plans/codex-backend.md part 2b/9): the nested
|
|
35
|
-
* `backends[backend].windowReserve` when the backend has one, else the top-level
|
|
36
|
-
* `claudeWindowReserve` for claude (its older spelling), else undefined. The two spellings are
|
|
37
|
-
* mutually exclusive in a loaded file (validateConfig rejects one holding both), so the
|
|
38
|
-
* fallback only ever answers for a config written before the rename. Pure: it never writes a
|
|
39
|
-
* spelling back into the file. */
|
|
40
|
-
export function windowReserveFor(config, backend) {
|
|
41
|
-
const nested = config.backends?.[backend]?.windowReserve;
|
|
42
|
-
if (nested !== undefined)
|
|
43
|
-
return nested;
|
|
44
|
-
if (backend === "claude")
|
|
45
|
-
return config.claudeWindowReserve;
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
36
|
/** True when `s` names one of the three tiers — the form `roles.<id>.model` /
|
|
49
37
|
* `review.model` may take to point at that tier of the top-level `model` map. */
|
|
50
38
|
export function isTierName(s) {
|
|
@@ -109,7 +97,12 @@ export function tierModel(config, tier) {
|
|
|
109
97
|
* key it overrides would.
|
|
110
98
|
*
|
|
111
99
|
* `tier` is the seam's effective tier, already resolved by the caller from any tier-name
|
|
112
|
-
* override — so `o.model` here is always a selector string, never a tier reference.
|
|
100
|
+
* override — so `o.model` here is always a selector string, never a tier reference.
|
|
101
|
+
*
|
|
102
|
+
* Exported so imageRerouteTarget (src/agent/image-input.ts) can probe a peer tier's
|
|
103
|
+
* top-level model (`withModelOverrides(config, tier, {})`, the same shape resolverConfig
|
|
104
|
+
* builds strong with) and reshape a tier's fallback pair into a runnable config — the
|
|
105
|
+
* tier-config builder the plan names. */
|
|
113
106
|
export function withModelOverrides(config, tier, o) {
|
|
114
107
|
// The top-level model resolves at the seam's tier: a string form is the default tier's
|
|
115
108
|
// selector; a map form takes the tier's entry, else `default`'s.
|
|
@@ -174,14 +167,6 @@ export function roleSeamTier(config, role) {
|
|
|
174
167
|
return "strong";
|
|
175
168
|
return roleById(role)?.tier ?? "default";
|
|
176
169
|
}
|
|
177
|
-
/** Whether the review gate reviews this role's commits AFTER they merge (review.afterMerge
|
|
178
|
-
* lists the role's base id). One home for the membership test the review gate (review.ts's
|
|
179
|
-
* reviewAttempt), the runner's `already_queued` recovery arm (runner-run.ts) and the role
|
|
180
|
-
* view (`tumwater role <id>` / the dashboard role drawer) all share, so an operator reading
|
|
181
|
-
* one surface sees the same answer every consumer enforces. Pure, no I/O. */
|
|
182
|
-
export function reviewedAfterMerge(config, role) {
|
|
183
|
-
return (config.review.afterMerge ?? []).includes(baseRoleOf(role));
|
|
184
|
-
}
|
|
185
170
|
/** The config as seen by one role: role-level provider/model/thinking overrides applied
|
|
186
171
|
* over the top-level values, plus the per-role minRunIntervalSeconds (a slow clock for
|
|
187
172
|
* roles that should act rarely) falling back to the global value when unset. The role's
|
|
@@ -240,7 +225,9 @@ export function resolverConfig(config) {
|
|
|
240
225
|
* the merge slot the whole queue waits on, and its only other limits were the run's budget
|
|
241
226
|
* and the quiet watchdog: reviews measured 2026-09-22/23 ran 2.3 min median, 9.2 min p90 and
|
|
242
227
|
* 24.6 min max, while earlier days had 1.5–3.5 h reviews. 15 min clears the ordinary review and
|
|
243
|
-
* kills a wedged one in minutes instead of the run's hours.
|
|
228
|
+
* kills a wedged one in minutes instead of the run's hours. Under a starved host the budget
|
|
229
|
+
* stretches by the load-to-cores ratio (see reviewRunConfig), so a progressing review on a
|
|
230
|
+
* loaded box is not killed at the wall by load it does not control. */
|
|
244
231
|
export const REVIEW_TIMEOUT_S = 900;
|
|
245
232
|
/** The reviewer's wall-clock floor while the budget fallback carries the team. The free model
|
|
246
233
|
* is typically local and far slower per turn than the budgeted one it replaces, but a review
|
|
@@ -253,21 +240,26 @@ export const REVIEW_TIMEOUT_S = 900;
|
|
|
253
240
|
export const FALLBACK_REVIEW_TIMEOUT_S = 3600;
|
|
254
241
|
/** The config as seen by the gate's reviewer run: the reviewer's model wiring (reviewConfig)
|
|
255
242
|
* with its own time budget — `review.timeoutSeconds`, default REVIEW_TIMEOUT_S — overriding
|
|
256
|
-
* the run's. A smaller runTimeoutSeconds still wins.
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
|
|
243
|
+
* the run's. A smaller runTimeoutSeconds still wins. Under a starved host the budget stretches
|
|
244
|
+
* by `starvedDeadlineMs` (the merge-scope precedent in build-check-events.ts): the load-to-cores
|
|
245
|
+
* ratio caps it at double the base, so on an ordinary quiet host the value is exactly today's
|
|
246
|
+
* budget while a progressing review on a loaded box is not killed at the wall by load it does
|
|
247
|
+
* not control. A still-progressing gate run gets one extension of the same length
|
|
248
|
+
* (src/pi/pi-watchdogs.ts), so the effective ceiling on a large diff's review is twice this
|
|
249
|
+
* stretched budget. `load` defaults to `sampleHostLoad` and is injectable for offline tests. */
|
|
250
|
+
export function reviewRunConfig(config, load = sampleHostLoad) {
|
|
260
251
|
const cfg = reviewConfig(config);
|
|
252
|
+
const baseMs = (config.review.timeoutSeconds ?? REVIEW_TIMEOUT_S) * 1000;
|
|
261
253
|
return {
|
|
262
254
|
...cfg,
|
|
263
|
-
runTimeoutSeconds: Math.min(cfg.runTimeoutSeconds,
|
|
255
|
+
runTimeoutSeconds: Math.min(cfg.runTimeoutSeconds, starvedDeadlineMs(baseMs, load()) / 1000),
|
|
264
256
|
};
|
|
265
257
|
}
|
|
266
258
|
/** The optional provider/model/thinking fields of a resolved model seam, with each unset
|
|
267
259
|
* (empty) field left out so the result carries only what was resolved — the one home of that
|
|
268
260
|
* assembly. The fallback layer (config-fallbacks.ts's fallbackSelectorFields and
|
|
269
|
-
* fallbackPair/tierOwnFallback
|
|
270
|
-
*
|
|
261
|
+
* fallbackPair/tierOwnFallback) and role-view's RoleViewPayload (its model fields) all
|
|
262
|
+
* build a triple this way and follow the same rule — an absent or empty
|
|
271
263
|
* value is
|
|
272
264
|
* omitted rather than written as undefined/"" (an empty selector field would read as
|
|
273
265
|
* "override present," so it must not appear). */
|
|
@@ -285,8 +277,8 @@ export function resolvedModelFields(provider, model, thinking) {
|
|
|
285
277
|
* provider/model/thinking triple formatted as one selector string, or an empty object when
|
|
286
278
|
* no model id is resolved (pi's own default) — the key stays absent rather than empty, the
|
|
287
279
|
* same empty-means-unset rule resolvedModelFields applies to the raw halves. One home of
|
|
288
|
-
* that assembly and guard, shared by the
|
|
289
|
-
*
|
|
280
|
+
* that assembly and guard, shared by the run-start event writer (runner-run.ts's run_start)
|
|
281
|
+
* and the per-role status snapshot (status-data.ts) so the two
|
|
290
282
|
* observability surfaces cannot disagree about which model a run starts on. A non-pi backend is
|
|
291
283
|
* named as the selector's provider namespace (backendQualifiedSelector), so a claude run reads
|
|
292
284
|
* `claude/claude-sonnet-5-5` in every surface instead of a bare model id. */
|
|
@@ -317,22 +309,6 @@ export function seamHoldKey(cfg) {
|
|
|
317
309
|
export function seamPairName(cfg) {
|
|
318
310
|
return modelPairName({ provider: seamHoldKey(cfg), model: cfg.model });
|
|
319
311
|
}
|
|
320
|
-
/** The key a resolved seam's run counts against the per-provider cap
|
|
321
|
-
* (plans/provider-concurrency.md §2): its hold key, else — for a pi seam that names no model
|
|
322
|
-
* at all and so runs pi's saved default — the default provider from pi's settings
|
|
323
|
-
* (`piDefault`, the caller's cached readPiDefaultModel result, so this view stays free of
|
|
324
|
-
* I/O). A bare id with no provider, or a seam whose pi settings cannot name a default, yields
|
|
325
|
-
* undefined: only the team total bounds it. HF sub-provider pins (`huggingface/…:deepinfra`)
|
|
326
|
-
* share the base provider, and billing does not split claude, because this reuses the hold key
|
|
327
|
-
* — "omlx is held" and "omlx is full" name the same thing on every surface. */
|
|
328
|
-
export function seamConcurrencyKey(cfg, piDefault) {
|
|
329
|
-
const hold = seamHoldKey(cfg);
|
|
330
|
-
if (hold !== undefined)
|
|
331
|
-
return hold;
|
|
332
|
-
if (cfg.model === undefined)
|
|
333
|
-
return piDefault?.provider;
|
|
334
|
-
return undefined;
|
|
335
|
-
}
|
|
336
312
|
/** The team's model wiring as one human-readable label, for the live-reload `model_changed`
|
|
337
313
|
* event: null when no tier resolves to a model (pi's own default), the single selector when
|
|
338
314
|
* every set tier agrees (the string-form `model`), and the `small=…, default=…, strong=…`
|
|
@@ -18,12 +18,11 @@ import { show } from "./config-field-checks.js";
|
|
|
18
18
|
import { canonicalConfigKey } from "./config-aliases.js";
|
|
19
19
|
import { unknownRoleMessage } from "../roles/roles.js";
|
|
20
20
|
import { loadConfig, collapseBackendAliases, knownRoleIdsCached, } from "./config.js";
|
|
21
|
-
import { checkDailyBudgetUsd,
|
|
21
|
+
import { checkDailyBudgetUsd, checkQuietHours, parseConfigKey, PER_KEY_VALIDATORS, unknownConfigKeyError, unknownRoleFieldError, } from "./config-keys.js";
|
|
22
22
|
import { writeConfigMutation, fail, rawConfigObject, without, preservingFileRoles, preserveFileRoles, withSectionOrDropped, } from "./config-write-core.js";
|
|
23
23
|
/** Drop `field` from the `backends.<backend>` entry of the config `next`, dropping the whole
|
|
24
24
|
* backend entry when it empties. The one home of the "remove a nested backend field, drop the
|
|
25
|
-
* backend when empty" step the legacy `agentBin`/`piArgs` collapse
|
|
26
|
-
* collapse share, so the empty-entry rule cannot drift between the two. */
|
|
25
|
+
* backend when empty" step the legacy `agentBin`/`piArgs` collapse uses. */
|
|
27
26
|
function dropNestedBackendField(next, backend, field) {
|
|
28
27
|
const record = next;
|
|
29
28
|
const backends = { ...(record.backends ?? {}) };
|
|
@@ -62,8 +61,7 @@ export function setDailyBudgetUsd(root, value) {
|
|
|
62
61
|
* that setConfigKey's JSON.parse would read past validateConfig's integer check and silently
|
|
63
62
|
* write — the same silent misread the per-role instances/interval guards, the TUI team-key
|
|
64
63
|
* editors, and the GUI Settings rows refuse — so setConfigKey refuses it, naming the key and
|
|
65
|
-
* writing nothing.
|
|
66
|
-
* checkProviderConcurrencyCap screen and is not in this set. */
|
|
64
|
+
* writing nothing. */
|
|
67
65
|
const PLAIN_INTEGER_TOP_LEVEL_KEYS = new Set([
|
|
68
66
|
"maxConcurrent",
|
|
69
67
|
"mergeBatchMax",
|
|
@@ -93,7 +91,6 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
93
91
|
// and the per-kind merge groups close over that call's `value`/`unchanged`/`oldValue`.
|
|
94
92
|
const exponentRefusal = (parsed, key, trimmed) => {
|
|
95
93
|
const isPlainIntegerKey = (parsed.kind === "top" && PLAIN_INTEGER_TOP_LEVEL_KEYS.has(key)) ||
|
|
96
|
-
(parsed.kind === "map" && parsed.map === "maxConcurrentPerProvider") ||
|
|
97
94
|
(parsed.kind === "role" && parsed.field === "instances") ||
|
|
98
95
|
(parsed.kind === "section" && parsed.section === "bootstrap" && parsed.field === "untilPlansDone");
|
|
99
96
|
if (isPlainIntegerKey && trimmed !== "" && isExponentSpelling(trimmed))
|
|
@@ -112,9 +109,7 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
112
109
|
const label = `${parsed.map}.${parsed.role}`;
|
|
113
110
|
const validator = parsed.map === "maxDailyCostUsdPerRole"
|
|
114
111
|
? checkDailyBudgetUsd
|
|
115
|
-
:
|
|
116
|
-
? checkProviderConcurrencyCap
|
|
117
|
-
: checkQuietHours;
|
|
112
|
+
: checkQuietHours;
|
|
118
113
|
const problem = validator(value, label);
|
|
119
114
|
if (problem)
|
|
120
115
|
return fail(problem);
|
|
@@ -185,12 +180,6 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
185
180
|
else
|
|
186
181
|
delete nextRecord.piArgs;
|
|
187
182
|
}
|
|
188
|
-
// Setting the nested claude reserve drops the top-level spelling, so the file can never
|
|
189
|
-
// hold both (validateConfig rejects a file that does, mirroring the agentBin/piArgs pair).
|
|
190
|
-
if (parsed.id === "claude" && parsed.field === "windowReserve") {
|
|
191
|
-
const nextRecord = next;
|
|
192
|
-
delete nextRecord.claudeWindowReserve;
|
|
193
|
-
}
|
|
194
183
|
return preservingFileRoles(root, next);
|
|
195
184
|
};
|
|
196
185
|
const mergeRoleEntry = (record, parsed) => {
|
|
@@ -281,10 +270,6 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
281
270
|
// Writers keep one spelling: a legacy top-level set drops the matching backends.pi field, so
|
|
282
271
|
// the file can never hold both (validateConfig's raw pass rejects a file that does).
|
|
283
272
|
const dropLegacyPiAlias = (next, key) => dropNestedBackendField(next, "pi", key === "agentBin" ? "bin" : "args");
|
|
284
|
-
// Writers keep one spelling: setting the top-level `claudeWindowReserve` drops the nested
|
|
285
|
-
// `backends.claude.windowReserve`, so a supported `config set` never writes the rejected pair
|
|
286
|
-
// (validateConfig still refuses a hand-edited file holding both).
|
|
287
|
-
const dropNestedReserve = (next) => dropNestedBackendField(next, "claude", "windowReserve");
|
|
288
273
|
const result = writeConfigMutation(root, (cfg) => {
|
|
289
274
|
oldValue = cfg[key];
|
|
290
275
|
if (isDeepStrictEqual(oldValue, value)) {
|
|
@@ -306,8 +291,6 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
306
291
|
return collapseBackendAliases(next);
|
|
307
292
|
if (key === "agentBin" || key === "piArgs")
|
|
308
293
|
return dropLegacyPiAlias(next, key);
|
|
309
|
-
if (key === "claudeWindowReserve")
|
|
310
|
-
return dropNestedReserve(next);
|
|
311
294
|
return next;
|
|
312
295
|
});
|
|
313
296
|
if (!result.ok)
|
|
@@ -322,8 +305,8 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
322
305
|
return fail(parsed.error);
|
|
323
306
|
// Shape, before the JSON.parse, for every plain-positive-integer key this verb can write —
|
|
324
307
|
// the top-level run-concurrency knobs and legacy merge batch cap, plus the integer-bound
|
|
325
|
-
// team-scope keys that arrive as a dotted key (`
|
|
326
|
-
// `
|
|
308
|
+
// team-scope keys that arrive as a dotted key (`roles.<id>.instances`, the
|
|
309
|
+
// `bootstrap.untilPlansDone` section field): refuse an exponent
|
|
327
310
|
// spelling ("4e1" → 40) that JSON.parse would read past validateConfig's integer check and
|
|
328
311
|
// silently write, naming the key and writing nothing — the twin of the per-role guards, the
|
|
329
312
|
// TUI team-key editors, and the GUI Settings rows. A non-exponent invalid falls through for
|
|
@@ -409,6 +392,106 @@ export function setRoleModelFields(root, base, fields) {
|
|
|
409
392
|
return { ok: true, unchanged: true };
|
|
410
393
|
return result;
|
|
411
394
|
}
|
|
395
|
+
/** Unset the top-level key `parsed.key` from the raw file, reverting it to its default or to
|
|
396
|
+
* absence (a whole-config write). Absent from the file (only a default carries it): nothing to
|
|
397
|
+
* remove — return the config unchanged so writeConfigMutation skips the write. */
|
|
398
|
+
function unsetTopKey(root, cfg, raw, parsed) {
|
|
399
|
+
// Absent from the file (only a default carries it): nothing to remove.
|
|
400
|
+
if (!Object.hasOwn(raw, parsed.key))
|
|
401
|
+
return { cfg, oldValue: undefined };
|
|
402
|
+
const oldValue = raw[parsed.key];
|
|
403
|
+
const record = cfg;
|
|
404
|
+
const next = without(record, parsed.key);
|
|
405
|
+
// An unrelated unset is a whole-config write too: keep the file's own roles section.
|
|
406
|
+
if (parsed.key !== "roles")
|
|
407
|
+
preserveFileRoles(root, next);
|
|
408
|
+
return { cfg: next, oldValue };
|
|
409
|
+
}
|
|
410
|
+
/** Unset ONE dotted-map entry (`maxDailyCostUsdPerRole.<role>` / `quietHoursPerRole.<role>`),
|
|
411
|
+
* preserving its siblings; the `parsed.map` section is dropped when it empties. An absent file
|
|
412
|
+
* entry is a no-op success that leaves the config unchanged. */
|
|
413
|
+
function unsetMapEntry(root, cfg, raw, parsed) {
|
|
414
|
+
const rawMap = objectOrEmpty(raw[parsed.map]);
|
|
415
|
+
if (!Object.hasOwn(rawMap, parsed.role))
|
|
416
|
+
return { cfg, oldValue: undefined }; // absent file entry: a no-op success
|
|
417
|
+
const oldValue = rawMap[parsed.role];
|
|
418
|
+
const record = cfg;
|
|
419
|
+
const existing = record[parsed.map] ?? {};
|
|
420
|
+
const next = without(existing, parsed.role);
|
|
421
|
+
return { cfg: withSectionOrDropped(root, cfg, parsed.map, next), oldValue };
|
|
422
|
+
}
|
|
423
|
+
/** Unset one `model.<tier>`; a string `model` resolves to { default } (modelTierMap), so unsetting
|
|
424
|
+
* a tier the FILE does not hold is a no-op that leaves the string untouched. The `model` section
|
|
425
|
+
* is dropped when it empties. */
|
|
426
|
+
function unsetModelTier(root, cfg, raw, parsed) {
|
|
427
|
+
// A string `model` resolves to { default } (modelTierMap); unsetting a tier the FILE
|
|
428
|
+
// does not hold is a no-op that leaves the string untouched.
|
|
429
|
+
const rawTiers = modelTierMap(raw.model);
|
|
430
|
+
if (!Object.hasOwn(rawTiers, parsed.tier))
|
|
431
|
+
return { cfg, oldValue: undefined };
|
|
432
|
+
const oldValue = rawTiers[parsed.tier];
|
|
433
|
+
const existing = modelTierMap(cfg.model);
|
|
434
|
+
const next = without(existing, parsed.tier);
|
|
435
|
+
return { cfg: withSectionOrDropped(root, cfg, "model", next), oldValue };
|
|
436
|
+
}
|
|
437
|
+
/** Unset ONE dotted section field (`backoff.<field>` / `bootstrap.<field>` …), preserving its
|
|
438
|
+
* siblings; the section is dropped when it empties. Presence is judged against the FILE: a field
|
|
439
|
+
* only the default carries is a no-op success. */
|
|
440
|
+
function unsetSectionField(root, cfg, raw, parsed) {
|
|
441
|
+
// Presence is judged against the FILE: a field only the default carries is a no-op
|
|
442
|
+
// success, and an emptied section leaves the file.
|
|
443
|
+
const rawSection = objectOrEmpty(raw[parsed.section]);
|
|
444
|
+
if (!Object.hasOwn(rawSection, parsed.field))
|
|
445
|
+
return { cfg, oldValue: undefined };
|
|
446
|
+
const oldValue = rawSection[parsed.field];
|
|
447
|
+
const record = cfg;
|
|
448
|
+
const existing = record[parsed.section] ?? {};
|
|
449
|
+
return { cfg: withSectionOrDropped(root, cfg, parsed.section, without(existing, parsed.field)), oldValue };
|
|
450
|
+
}
|
|
451
|
+
/** Unset ONE `backends.<id>` field (`bin`/`args`/`configDir`); the entry and the `backends`
|
|
452
|
+
* section are dropped when they empty, so the file never keeps an empty object. Presence is
|
|
453
|
+
* judged against the FILE, so a field only the default (or the legacy alias's resolved view)
|
|
454
|
+
* carries is a no-op success. */
|
|
455
|
+
function unsetBackendField(root, cfg, raw, parsed) {
|
|
456
|
+
// Presence is judged against the FILE, so unsetting a field only the default (or the
|
|
457
|
+
// legacy alias's resolved view) carries is a no-op success. The entry and the `backends`
|
|
458
|
+
// section are dropped when they empty, so the file never keeps an empty object.
|
|
459
|
+
const rawBackends = objectOrEmpty(raw.backends);
|
|
460
|
+
const rawEntry = objectOrEmpty(rawBackends[parsed.id]);
|
|
461
|
+
if (!Object.hasOwn(rawEntry, parsed.field))
|
|
462
|
+
return { cfg, oldValue: undefined }; // absent file field: a no-op success
|
|
463
|
+
const oldValue = rawEntry[parsed.field];
|
|
464
|
+
const record = cfg;
|
|
465
|
+
const backends = { ...record.backends };
|
|
466
|
+
const entry = { ...(backends[parsed.id] ?? {}) };
|
|
467
|
+
delete entry[parsed.field];
|
|
468
|
+
if (Object.keys(entry).length === 0)
|
|
469
|
+
delete backends[parsed.id];
|
|
470
|
+
else
|
|
471
|
+
backends[parsed.id] = entry;
|
|
472
|
+
return { cfg: withSectionOrDropped(root, cfg, "backends", backends), oldValue };
|
|
473
|
+
}
|
|
474
|
+
/** Unset ONE `roles.<id>` field, based on the FILE's own roles — not the loaded (catalog-merged)
|
|
475
|
+
* map — so the write must not re-expand the operator's partial section to every built-in. The
|
|
476
|
+
* entry is dropped when it empties, and the whole `roles` section when it was the last one. */
|
|
477
|
+
function unsetRoleField(cfg, raw, parsed) {
|
|
478
|
+
const rawRoles = objectOrEmpty(raw.roles);
|
|
479
|
+
const rawEntry = objectOrEmpty(rawRoles[parsed.id]);
|
|
480
|
+
if (!Object.hasOwn(rawEntry, parsed.field))
|
|
481
|
+
return { cfg, oldValue: undefined }; // absent file field: a no-op success
|
|
482
|
+
const oldValue = rawEntry[parsed.field];
|
|
483
|
+
// Base the removal on the FILE's own roles, not the loaded (catalog-merged) map: the
|
|
484
|
+
// write must not re-expand the operator's partial section to every built-in.
|
|
485
|
+
const record = cfg;
|
|
486
|
+
const nextEntry = without(rawEntry, parsed.field); // the file's own entry (its field was present, checked above)
|
|
487
|
+
if (Object.keys(nextEntry).length > 0)
|
|
488
|
+
return { cfg: { ...cfg, roles: { ...rawRoles, [parsed.id]: nextEntry } }, oldValue };
|
|
489
|
+
// The entry emptied: drop it, and the whole `roles` section when it was the last one.
|
|
490
|
+
const nextRoles = without(rawRoles, parsed.id);
|
|
491
|
+
if (Object.keys(nextRoles).length === 0)
|
|
492
|
+
return { cfg: without(record, "roles"), oldValue };
|
|
493
|
+
return { cfg: { ...cfg, roles: nextRoles }, oldValue };
|
|
494
|
+
}
|
|
412
495
|
/** Remove ONE value of tumwater.json from its raw CLI text (`tumwater config unset <key>`):
|
|
413
496
|
* the inverse of setConfigKey. A bare top-level key is deleted (reverting to its default or
|
|
414
497
|
* to absence); a dotted key (`maxDailyCostUsdPerRole.<role>`, `quietHoursPerRole.<role>`,
|
|
@@ -444,86 +527,29 @@ export function unsetConfigKey(root, key) {
|
|
|
444
527
|
let oldValue;
|
|
445
528
|
const result = writeConfigMutation(root, (cfg) => {
|
|
446
529
|
const raw = rawConfigObject(root); // the file's own keys, before defaults fill them in
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
530
|
+
let moved;
|
|
531
|
+
switch (parsed.kind) {
|
|
532
|
+
case "top":
|
|
533
|
+
moved = unsetTopKey(root, cfg, raw, parsed);
|
|
534
|
+
break;
|
|
535
|
+
case "map":
|
|
536
|
+
moved = unsetMapEntry(root, cfg, raw, parsed);
|
|
537
|
+
break;
|
|
538
|
+
case "tier":
|
|
539
|
+
moved = unsetModelTier(root, cfg, raw, parsed);
|
|
540
|
+
break;
|
|
541
|
+
case "section":
|
|
542
|
+
moved = unsetSectionField(root, cfg, raw, parsed);
|
|
543
|
+
break;
|
|
544
|
+
case "agent":
|
|
545
|
+
moved = unsetBackendField(root, cfg, raw, parsed);
|
|
546
|
+
break;
|
|
547
|
+
case "role":
|
|
548
|
+
moved = unsetRoleField(cfg, raw, parsed);
|
|
549
|
+
break;
|
|
458
550
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const rawMap = objectOrEmpty(rawMapCandidate);
|
|
462
|
-
if (!Object.hasOwn(rawMap, parsed.role))
|
|
463
|
-
return cfg; // absent file entry: a no-op success
|
|
464
|
-
oldValue = rawMap[parsed.role];
|
|
465
|
-
const existing = record[parsed.map] ?? {};
|
|
466
|
-
const next = without(existing, parsed.role);
|
|
467
|
-
return withSectionOrDropped(root, cfg, parsed.map, next);
|
|
468
|
-
}
|
|
469
|
-
if (parsed.kind === "tier") {
|
|
470
|
-
// A string `model` resolves to { default } (modelTierMap); unsetting a tier the FILE
|
|
471
|
-
// does not hold is a no-op that leaves the string untouched.
|
|
472
|
-
const rawTiers = modelTierMap(raw.model);
|
|
473
|
-
if (!Object.hasOwn(rawTiers, parsed.tier))
|
|
474
|
-
return cfg;
|
|
475
|
-
oldValue = rawTiers[parsed.tier];
|
|
476
|
-
const existing = modelTierMap(record.model);
|
|
477
|
-
const next = without(existing, parsed.tier);
|
|
478
|
-
return withSectionOrDropped(root, cfg, "model", next);
|
|
479
|
-
}
|
|
480
|
-
if (parsed.kind === "section") {
|
|
481
|
-
// Presence is judged against the FILE: a field only the default carries is a no-op
|
|
482
|
-
// success, and an emptied section leaves the file.
|
|
483
|
-
const rawSection = objectOrEmpty(raw[parsed.section]);
|
|
484
|
-
if (!Object.hasOwn(rawSection, parsed.field))
|
|
485
|
-
return cfg;
|
|
486
|
-
oldValue = rawSection[parsed.field];
|
|
487
|
-
const existing = record[parsed.section] ?? {};
|
|
488
|
-
return withSectionOrDropped(root, cfg, parsed.section, without(existing, parsed.field));
|
|
489
|
-
}
|
|
490
|
-
if (parsed.kind === "agent") {
|
|
491
|
-
// Presence is judged against the FILE, so unsetting a field only the default (or the
|
|
492
|
-
// legacy alias's resolved view) carries is a no-op success. The entry and the `backends`
|
|
493
|
-
// section are dropped when they empty, so the file never keeps an empty object.
|
|
494
|
-
const rawBackends = objectOrEmpty(raw.backends);
|
|
495
|
-
const rawEntryCandidate = rawBackends[parsed.id];
|
|
496
|
-
const rawEntry = objectOrEmpty(rawEntryCandidate);
|
|
497
|
-
if (!Object.hasOwn(rawEntry, parsed.field))
|
|
498
|
-
return cfg; // absent file field: a no-op success
|
|
499
|
-
oldValue = rawEntry[parsed.field];
|
|
500
|
-
const backends = { ...record.backends };
|
|
501
|
-
const entry = { ...(backends[parsed.id] ?? {}) };
|
|
502
|
-
delete entry[parsed.field];
|
|
503
|
-
if (Object.keys(entry).length === 0)
|
|
504
|
-
delete backends[parsed.id];
|
|
505
|
-
else
|
|
506
|
-
backends[parsed.id] = entry;
|
|
507
|
-
return withSectionOrDropped(root, cfg, "backends", backends);
|
|
508
|
-
}
|
|
509
|
-
const rawRolesCandidate = raw.roles;
|
|
510
|
-
const rawRoles = objectOrEmpty(rawRolesCandidate);
|
|
511
|
-
const rawEntryCandidate = rawRoles[parsed.id];
|
|
512
|
-
const rawEntry = objectOrEmpty(rawEntryCandidate);
|
|
513
|
-
if (!Object.hasOwn(rawEntry, parsed.field))
|
|
514
|
-
return cfg; // absent file field: a no-op success
|
|
515
|
-
oldValue = rawEntry[parsed.field];
|
|
516
|
-
// Base the removal on the FILE's own roles, not the loaded (catalog-merged) map: the
|
|
517
|
-
// write must not re-expand the operator's partial section to every built-in.
|
|
518
|
-
const existing = rawEntry; // the file's own entry (its field was present, checked above)
|
|
519
|
-
const nextEntry = without(existing, parsed.field);
|
|
520
|
-
if (Object.keys(nextEntry).length > 0)
|
|
521
|
-
return { ...cfg, roles: { ...rawRoles, [parsed.id]: nextEntry } };
|
|
522
|
-
// The entry emptied: drop it, and the whole `roles` section when it was the last one.
|
|
523
|
-
const nextRoles = without(rawRoles, parsed.id);
|
|
524
|
-
if (Object.keys(nextRoles).length === 0)
|
|
525
|
-
return without(record, "roles");
|
|
526
|
-
return { ...cfg, roles: nextRoles };
|
|
551
|
+
oldValue = moved.oldValue;
|
|
552
|
+
return moved.cfg;
|
|
527
553
|
});
|
|
528
554
|
if (!result.ok)
|
|
529
555
|
return result;
|
|
@@ -34,7 +34,7 @@ export function parseJsonConfig(file, basename) {
|
|
|
34
34
|
* defaults for concurrency, timeouts, log size, retention, thrash detection, idle backoff,
|
|
35
35
|
* self-redeploy, and review settings. The return type is the resolved view (config-views.ts): a
|
|
36
36
|
* freshly defaulted config has no model map to resolve — only a loaded file can carry one — so
|
|
37
|
-
* every call site that hands a default into a pi run (run
|
|
37
|
+
* every call site that hands a default into a pi run (agent-run.ts's PiRunOptions, tests)
|
|
38
38
|
* satisfies ResolvedModelConfig directly. */
|
|
39
39
|
export function defaultConfig() {
|
|
40
40
|
const roles = {};
|
|
@@ -143,15 +143,7 @@ export function defaultConfig() {
|
|
|
143
143
|
// explicit user instruction from being discarded by a rejected review — has nothing left
|
|
144
144
|
// to exempt. Removing it also closes the path where a director run could merge a mixed
|
|
145
145
|
// doc-plus-config diff unreviewed.
|
|
146
|
-
|
|
147
|
-
// readme, organize, plus clean, dedupe, coverage — are reviewed AFTER they merge, on their
|
|
148
|
-
// merged commit (src/review/post-merge-review.ts), instead of holding the merge queue for a
|
|
149
|
-
// reviewer run on their (usually trivial) diff; a rejection keeps the commit on main
|
|
150
|
-
// with the reviewer's reasons. feature, director, perf, improve and custom roles stay
|
|
151
|
-
// pre-merge. A
|
|
152
|
-
// config that omits `review.afterMerge` inherits this, and `"afterMerge": []` opts out (back
|
|
153
|
-
// to reviewing before merging).
|
|
154
|
-
review: { enabled: true, exemptPaths: ["*.md", "docs/**"], afterMerge: ["bugfix", "clean", "coverage", "dedupe", "organize", "readme"] },
|
|
146
|
+
review: { enabled: true, exemptPaths: ["*.md", "docs/**"] },
|
|
155
147
|
customRoles: [],
|
|
156
148
|
roles,
|
|
157
149
|
};
|
|
@@ -285,7 +277,7 @@ function cloneConfig(c) {
|
|
|
285
277
|
* reconfiguration (BUGS.md 2026-09-23: a merge's fast-forward deleted it and the team ran
|
|
286
278
|
* 8.6 h on defaults), so callers keep their last-known-good exactly as for a broken file, or
|
|
287
279
|
* fall back to defaults only when they never saw one. Startup's "missing ⇒ not initialized"
|
|
288
|
-
* gate (startup-
|
|
280
|
+
* gate (startup-gates.ts) and loadConfig's first-run defaults are unaffected. */
|
|
289
281
|
export function loadConfigCached(root) {
|
|
290
282
|
const file = configPath(root);
|
|
291
283
|
try {
|
|
@@ -353,13 +345,12 @@ function stableJson(value) {
|
|
|
353
345
|
}
|
|
354
346
|
/** Sorted names of the config settings whose value differs between two loaded configs, for the
|
|
355
347
|
* orchestrator's live-reload event. `maxConcurrent` and `sessionRetentionDays` are skipped: each
|
|
356
|
-
* already has its own, more informative event. `
|
|
357
|
-
* changed provider fires its own `provider_concurrency_changed` event. The `roles` map is never
|
|
348
|
+
* already has its own, more informative event. The `roles` map is never
|
|
358
349
|
* reported whole — only
|
|
359
350
|
* each differing `roles.<id>` — so a `customRoles` add/remove names both the section and the
|
|
360
351
|
* affected role. */
|
|
361
352
|
export function changedConfigKeys(prev, next) {
|
|
362
|
-
const skip = new Set(["maxConcurrent", "
|
|
353
|
+
const skip = new Set(["maxConcurrent", "sessionRetentionDays", "roles"]);
|
|
363
354
|
const keys = [];
|
|
364
355
|
const prevRecord = prev;
|
|
365
356
|
const nextRecord = next;
|
|
@@ -2,14 +2,14 @@ import { andMore, quoteHeading, readBacklogChecked, FIX_CLAIMS_CHECKED, REFUSAL_
|
|
|
2
2
|
import { truncate } from "../text/text.js";
|
|
3
3
|
import { plural, agree } from "../text/phrases.js";
|
|
4
4
|
import { ageInDays, filingDate, fixedBugEntries, openBugEntries, openQuestionEntries, plannedPlanEntries } from "../backlog/backlog.js";
|
|
5
|
-
import { questionFilingDate } from "../backlog/question-answer.js";
|
|
5
|
+
import { questionFilingDate } from "../backlog/backlog-question-answer.js";
|
|
6
6
|
import { needsReviewNote, refusalNote } from "../backlog/backlog-eligibility.js";
|
|
7
7
|
import { plannedPartKeys, plansWaitingOn, prerequisitesMerged } from "../backlog/backlog-parts.js";
|
|
8
8
|
import { lastDate } from "../backlog/backlog-md.js";
|
|
9
9
|
import { formatTimestamp } from "../text/datetime.js";
|
|
10
10
|
import { stringList } from "../files/json-object.js";
|
|
11
11
|
import { readEventsSince } from "../events/event-window.js";
|
|
12
|
-
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
12
|
+
import { okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
13
13
|
/** The doctor's open-bug and question checks, split out of doctor-backlog.ts: questions that
|
|
14
14
|
* have waited too long for an answer (checkStaleQuestions), bugs left unfixed (checkStaleBugs),
|
|
15
15
|
* fixed records whose quoted test regressed (checkRegressedFixes), and
|
|
@@ -17,7 +17,8 @@ import { okOutcome, warnOutcome } from "./outcome.js";
|
|
|
17
17
|
* (readBacklogChecked, andMore) from doctor-backlog.ts, plus the age/trim bounds, and
|
|
18
18
|
* all warn instead of failing: a stale or
|
|
19
19
|
* parked entry is operator signal, not a broken environment. doctor.ts composes them beside the
|
|
20
|
-
* plan-structure and queue checks; the shared CheckOutcome shape is imported from
|
|
20
|
+
* plan-structure and queue checks; the shared CheckOutcome shape is imported from
|
|
21
|
+
* doctor-outcome.ts. */
|
|
21
22
|
/** Stale questions — open QUESTIONS.md questions that have waited more than 7 days for an
|
|
22
23
|
* answer. A team can stay parked on a question for days while `tumwater doctor` reports
|
|
23
24
|
* nothing; the `--list` age (`questionAgeSuffix` on `tumwater questions`) is the only other
|
|
@@ -143,7 +144,7 @@ function sinceWindowMs(sinces) {
|
|
|
143
144
|
* cannot drift on the selection. `since` is the day-after the fix date — the recurrence scan's
|
|
144
145
|
* boundary (the heading carries only a date, so a same-day failure cannot be told from the one
|
|
145
146
|
* the fix answered). Returns `[]` when no candidate has both a quoted test and a date. */
|
|
146
|
-
|
|
147
|
+
function regressedFixCandidates(root) {
|
|
147
148
|
const candidates = fixedBugEntries(root).slice(0, FIX_CLAIMS_CHECKED).flatMap((entry) => {
|
|
148
149
|
const name = quotedTestName(entry.title);
|
|
149
150
|
const date = lastDate(entry.title, /(\d{4}-\d{2}-\d{2})/);
|
|
@@ -159,14 +160,14 @@ export function regressedFixCandidates(root) {
|
|
|
159
160
|
* `failureMentions` per candidate over `events`, keeping the last mention (the doctor's exact
|
|
160
161
|
* match). The caller supplies the event window — the doctor reads back to the oldest candidate's
|
|
161
162
|
* day-after, the always-on alert the trailing day. */
|
|
162
|
-
|
|
163
|
+
function matchedRegressedFixes(candidates, events) {
|
|
163
164
|
return candidates.flatMap((c) => {
|
|
164
165
|
const newest = lastFailureMention(events, c.name, c.since);
|
|
165
166
|
return newest ? [{ ...c, newest }] : [];
|
|
166
167
|
});
|
|
167
168
|
}
|
|
168
169
|
/** The doctor's `regressed fixes` warn sentence, verbatim, or null when nothing regressed. */
|
|
169
|
-
|
|
170
|
+
function regressedFixesPhrase(regressed) {
|
|
170
171
|
const [first, ...rest] = regressed;
|
|
171
172
|
if (!first)
|
|
172
173
|
return null;
|