tumwater 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +42 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +4 -3
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +402 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +246 -55
- package/dist/src/backlog/backlog-cli-view.js +280 -0
- package/dist/src/backlog/backlog-cli.js +85 -274
- package/dist/src/backlog/backlog-commit.js +4 -5
- package/dist/src/backlog/backlog-eligibility.js +175 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +254 -0
- package/dist/src/backlog/backlog-release.js +117 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-culprit.js +94 -0
- package/dist/src/baseline/main-guardrails.js +209 -0
- package/dist/src/baseline/main-red.js +139 -32
- package/dist/src/baseline/main-revert.js +133 -0
- package/dist/src/baseline/main-watch.js +184 -0
- package/dist/src/brief/brief-edit.js +9 -11
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget.js +30 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +118 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +150 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +308 -590
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-role-command.js +420 -0
- package/dist/src/cli/cli-start-summary.js +160 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
- package/dist/src/cli/completion.js +149 -22
- package/dist/src/cli/config-commands.js +41 -18
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +189 -73
- package/dist/src/cli/log-commands.js +148 -82
- package/dist/src/cli/question-commands.js +205 -10
- package/dist/src/cli.js +106 -43
- package/dist/src/collections.js +26 -10
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +13 -1
- package/dist/src/config/config-fallbacks.js +93 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +141 -199
- package/dist/src/config/config-views.js +127 -53
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +131 -264
- package/dist/src/config/config.js +48 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
- package/dist/src/doctor/doctor-backlog-plans.js +263 -0
- package/dist/src/doctor/doctor-backlog.js +100 -496
- package/dist/src/doctor/doctor-check-names.js +81 -0
- package/dist/src/doctor/doctor-checks.js +52 -579
- package/dist/src/doctor/doctor-claude-checks.js +145 -0
- package/dist/src/doctor/doctor-codex-checks.js +279 -0
- package/dist/src/doctor/doctor-contention-checks.js +380 -0
- package/dist/src/doctor/doctor-holds.js +82 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +236 -402
- package/dist/src/doctor/doctor-notify.js +3 -4
- package/dist/src/doctor/doctor-opencode-checks.js +162 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-project-checks.js +303 -0
- package/dist/src/doctor/doctor.js +121 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +7 -11
- package/dist/src/events/event-format.js +181 -86
- package/dist/src/events/event-labels.js +12 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +39 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +60 -24
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +56 -56
- package/dist/src/failure/failure-render.js +31 -23
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +99 -35
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +177 -84
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +108 -58
- package/dist/src/gates/maintenance-quota.js +10 -13
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +77 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +36 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +42 -18
- package/dist/src/gui/gui-backlog-endpoints.js +155 -0
- package/dist/src/gui/gui-command.js +8 -13
- package/dist/src/gui/gui-config-commands.js +94 -72
- package/dist/src/gui/gui-endpoint-commands.js +4 -3
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +108 -13
- package/dist/src/gui/gui-prompt-commands.js +11 -14
- package/dist/src/gui/gui-report-endpoints.js +90 -0
- package/dist/src/gui/gui-server.js +11 -4
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +147 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +193 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +25 -9
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +81 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +150 -11
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +90 -0
- package/dist/src/merge/merge-queue-render.js +49 -11
- package/dist/src/merge/merge-queue.js +31 -15
- package/dist/src/merge/merge-requeue.js +102 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +33 -11
- package/dist/src/merge/merge-to-main.js +86 -27
- package/dist/src/merge/merge-top.js +69 -0
- package/dist/src/merge/merge-verify.js +12 -10
- package/dist/src/notes-cli.js +30 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +320 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
- package/dist/src/orchestrator/orchestrator.js +128 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +201 -0
- package/dist/src/orchestrator/scheduling-skip.js +185 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +116 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +18 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +121 -211
- package/dist/src/prompt/notes-backlog.js +181 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +129 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +161 -77
- package/dist/src/report/report-fold.js +129 -52
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +133 -64
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +26 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/known-flakes.js +100 -1
- package/dist/src/review/post-merge-review.js +106 -0
- package/dist/src/review/protected-edits.js +4 -5
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +155 -360
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +69 -12
- package/dist/src/roles/role-guidance.js +55 -20
- package/dist/src/roles/role-labels.js +1 -2
- package/dist/src/roles/role-render.js +67 -50
- package/dist/src/roles/role-view.js +59 -47
- package/dist/src/roles/roles.js +31 -31
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/{tick → run}/qa-coverage.js +9 -27
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +179 -0
- package/dist/src/run/run-detail-render.js +98 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +172 -0
- package/dist/src/run/stage-path-checks.js +730 -0
- package/dist/src/{loop → runner}/leftover.js +80 -25
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +202 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +107 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +102 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +43 -16
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +28 -28
- package/dist/src/status/status-roles.js +39 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +5 -6
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +63 -17
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +57 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +67 -13
- package/dist/src/ui/badges.js +65 -37
- package/dist/src/ui/gui/gui-client-backlog.js +292 -0
- package/dist/src/ui/gui/gui-client-boot.js +32 -26
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
- package/dist/src/ui/gui/gui-client-drawer.js +126 -285
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +434 -67
- package/dist/src/ui/gui/gui-client-labels.js +1 -2
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +55 -51
- package/dist/src/ui/gui/gui-client-pending.js +227 -23
- package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
- package/dist/src/ui/gui/gui-client-report.js +87 -427
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +56 -4
- package/dist/src/ui/gui/gui-client-team.js +25 -465
- package/dist/src/ui/gui/gui-client.js +27 -25
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +9 -7
- package/dist/src/ui/gui/gui-styles.js +34 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +136 -49
- package/dist/src/ui/status-render.js +101 -82
- package/dist/src/ui/team-alerts.js +248 -57
- package/dist/src/ui/tone.js +5 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +51 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +3 -5
- package/dist/src/ui/tui/tui-editor-backends.js +132 -0
- package/dist/src/ui/tui/tui-editor-brief.js +10 -12
- package/dist/src/ui/tui/tui-editor-budget.js +35 -31
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
- package/dist/src/ui/tui/tui-editor-config.js +86 -0
- package/dist/src/ui/tui/tui-editor-entry.js +36 -47
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +37 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
- package/dist/src/ui/tui/tui-editor-interval.js +11 -9
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +3 -5
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
- package/dist/src/ui/tui/tui-editor-retire.js +4 -6
- package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +60 -34
- package/dist/src/ui/tui/tui-editor-slots.js +23 -0
- package/dist/src/ui/tui/tui-editors.js +35 -2
- package/dist/src/ui/tui/tui-frame.js +56 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +24 -9
- package/dist/src/ui/tui/tui-pane-activity.js +14 -11
- package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +58 -8
- package/dist/src/ui/tui/tui-pane-history.js +402 -82
- package/dist/src/ui/tui/tui-pane-pending.js +308 -13
- package/dist/src/ui/tui/tui-pane-queued.js +71 -55
- package/dist/src/ui/tui/tui-pane-report.js +46 -14
- package/dist/src/ui/tui/tui-pane-session.js +25 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +80 -23
- package/dist/src/ui/tui/tui-panes.js +100 -30
- package/dist/src/ui/tui/tui.js +14 -14
- package/dist/src/verdict/fix-claim.js +82 -32
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +42 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** The config AS SEEN BY A RUN — the per-seam resolution half of the config views, split out
|
|
3
2
|
* of config.ts: which provider/model/thinking a role, the reviewer, or the conflict resolver
|
|
4
3
|
* resolves to, and which timeout a reviewer run gets. The budget/tier fallback side — which
|
|
5
4
|
* free pair the team runs on at the daily cap, and the per-role fallback config (fallbackPair,
|
|
6
5
|
* resolveTierFallbacks, applyFallbackModel, …) — lives in config-fallbacks.ts, which builds on
|
|
7
6
|
* this module one-way. No I/O and no caching — the persistence side (defaults, loading,
|
|
8
|
-
* saving, the role-selection helpers) lives in config.ts, the write side in config-write.ts
|
|
7
|
+
* saving, the role-selection helpers) lives in config.ts, the write side in config-write.ts
|
|
8
|
+
* (editor verbs) and config-request.ts (the director's staged request). */
|
|
9
9
|
import { MODEL_TIERS } from "./config-schema.js";
|
|
10
|
-
import { formatModelSelector,
|
|
10
|
+
import { formatModelSelector, parseModelSelector, routeModel } from "./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
|
-
import { baseRoleOf } from "../roles/
|
|
14
|
+
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
15
15
|
/** The effective `backends.<id>` entry as a run sees it (plans/agent-backends.md "Back-compat"):
|
|
16
16
|
* the file's own entry, with the legacy top-level `agentBin`/`piArgs` read as the pi backend's
|
|
17
17
|
* `bin`/`args` when the new form leaves a field absent. Pure — it never writes those legacy
|
|
18
18
|
* values back into `config.backends`, so a config write that re-serializes the merged config
|
|
19
19
|
* cannot leak a second spelling into the file (which validateConfig would then reject).
|
|
20
|
-
* pi-backend.ts and claude-backend.ts read `.bin` through
|
|
20
|
+
* pi-backend.ts and claude-backend.ts read `.bin` through resolveBackendBin (pi-bin.ts);
|
|
21
21
|
* claude-backend.ts reads `.args` for passthrough argv. */
|
|
22
|
-
export function
|
|
22
|
+
export function resolvedBackendConfig(config, id) {
|
|
23
23
|
const entry = config.backends?.[id];
|
|
24
24
|
// A view must own its array: returning the config's own `args` reference would let a caller
|
|
25
25
|
// that appends passthrough argv silently mutate the loaded config, breaking the
|
|
@@ -31,6 +31,20 @@ export function resolvedAgentConfig(config, id) {
|
|
|
31
31
|
}
|
|
32
32
|
return { bin: entry?.bin ?? config.agentBin, args: (entry?.args ?? config.piArgs ?? []).slice() };
|
|
33
33
|
}
|
|
34
|
+
/** A backend's subscription-window reserve (plans/codex-backend.md part 2b/9): the nested
|
|
35
|
+
* `backends[backend].windowReserve` when the backend has one, else the top-level
|
|
36
|
+
* `claudeWindowReserve` for claude (its older spelling), else undefined. The two spellings are
|
|
37
|
+
* mutually exclusive in a loaded file (validateConfig rejects one holding both), so the
|
|
38
|
+
* fallback only ever answers for a config written before the rename. Pure: it never writes a
|
|
39
|
+
* spelling back into the file. */
|
|
40
|
+
export function windowReserveFor(config, backend) {
|
|
41
|
+
const nested = config.backends?.[backend]?.windowReserve;
|
|
42
|
+
if (nested !== undefined)
|
|
43
|
+
return nested;
|
|
44
|
+
if (backend === "claude")
|
|
45
|
+
return config.claudeWindowReserve;
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
34
48
|
/** True when `s` names one of the three tiers — the form `roles.<id>.model` /
|
|
35
49
|
* `review.model` may take to point at that tier of the top-level `model` map. */
|
|
36
50
|
export function isTierName(s) {
|
|
@@ -43,13 +57,40 @@ export function isTierName(s) {
|
|
|
43
57
|
* leaves pi's own default in charge. Map-form + legacy provider is a validation error, so
|
|
44
58
|
* no legacy provider is in scope for a map's entries. */
|
|
45
59
|
export function topTierSelector(config, tier) {
|
|
60
|
+
return resolveTopTier(config, tier).sel;
|
|
61
|
+
}
|
|
62
|
+
function resolveTopTier(config, tier) {
|
|
46
63
|
if (typeof config.model === "string")
|
|
47
|
-
return parseModelSelector(config.model, config.provider);
|
|
48
|
-
if (isJsonObject(config.model))
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
return { sel: parseModelSelector(config.model, config.provider) };
|
|
65
|
+
if (!isJsonObject(config.model))
|
|
66
|
+
return {};
|
|
67
|
+
const raw = config.model[tier] ?? config.model.default;
|
|
68
|
+
if (raw === undefined)
|
|
69
|
+
return {};
|
|
70
|
+
if (typeof raw === "string")
|
|
71
|
+
return { sel: parseModelSelector(raw) };
|
|
72
|
+
const withThinking = (t) => {
|
|
73
|
+
if (raw.thinking === undefined)
|
|
74
|
+
return t;
|
|
75
|
+
return t.sel ? { ...t, sel: { ...t.sel, thinking: raw.thinking } } : { ...t, thinking: raw.thinking };
|
|
76
|
+
};
|
|
77
|
+
if (raw.model !== undefined) {
|
|
78
|
+
const sel = parseModelSelector(raw.model);
|
|
79
|
+
return withThinking({ sel, ...(raw.backend !== undefined ? { backend: raw.backend } : {}) });
|
|
51
80
|
}
|
|
52
|
-
|
|
81
|
+
// No model of its own: take `default`'s, but an explicit backend only when `default` routes
|
|
82
|
+
// there already (the inherited model then routes by its own spelling, exactly as `default` does).
|
|
83
|
+
const dflt = tier === "default" ? {} : resolveTopTier(config, "default");
|
|
84
|
+
if (raw.backend !== undefined &&
|
|
85
|
+
(tier === "default" || withModelOverrides(config, "default", {}).agent !== raw.backend))
|
|
86
|
+
return withThinking({ backend: raw.backend });
|
|
87
|
+
return withThinking(dflt);
|
|
88
|
+
}
|
|
89
|
+
/** The explicit `backend` a tier's entry names — undefined for a string entry or an object entry
|
|
90
|
+
* without one, or when the entry names a backend `default`'s model does not route to and so
|
|
91
|
+
* inherits nothing (the backend then rides only its own tier default). */
|
|
92
|
+
export function topTierBackend(config, tier) {
|
|
93
|
+
return resolveTopTier(config, tier).backend;
|
|
53
94
|
}
|
|
54
95
|
/** The selector one seam's tier resolves to at the top level — the tier's own map entry,
|
|
55
96
|
* else `default`'s, else none (pi's own default). The pricing view (teamModelsFree) and
|
|
@@ -77,21 +118,43 @@ export function tierModel(config, tier) {
|
|
|
77
118
|
export function withModelOverrides(config, tier, o) {
|
|
78
119
|
// The top-level model resolves at the seam's tier: a string form is the default tier's
|
|
79
120
|
// selector; a map form takes the tier's entry, else `default`'s.
|
|
80
|
-
const
|
|
121
|
+
const top = resolveTopTier(config, tier);
|
|
81
122
|
const ownSel = o.model !== undefined ? parseModelSelector(o.model, o.provider ?? config.provider) : undefined;
|
|
123
|
+
const configBackend = config.backend ?? "pi";
|
|
124
|
+
// A role's own `backend` binds only to its own model; alone it inherits the tier's model only
|
|
125
|
+
// when the tier already routes there, else the backend's tier default fills in. A role's
|
|
126
|
+
// `model` without `backend` routes by the legacy rules, never by the tier entry's backend.
|
|
127
|
+
const inherit = o.model === undefined &&
|
|
128
|
+
(o.backend === undefined ||
|
|
129
|
+
routeModel(top.sel?.provider, top.sel?.model, configBackend, tier, top.backend).agent === o.backend);
|
|
130
|
+
// The tier's thinking level rides every path but a backend-only role whose tier routes elsewhere.
|
|
131
|
+
const topThinking = o.model === undefined && !inherit ? undefined : (top.sel?.thinking ?? top.thinking);
|
|
132
|
+
const topSel = inherit ? top.sel : undefined;
|
|
133
|
+
const backend = o.model !== undefined ? o.backend : inherit ? top.backend : o.backend;
|
|
82
134
|
const provider = ownSel ? ownSel.provider : (o.provider ?? topSel?.provider ?? config.provider);
|
|
83
135
|
const model = ownSel ? ownSel.model : topSel?.model;
|
|
84
136
|
// The routed backend decides whether the provider half survives: a pi provider name is kept,
|
|
85
137
|
// a backend namespace such as `claude` is dropped (that backend takes no provider).
|
|
86
|
-
const routed = routeModel(provider, model,
|
|
138
|
+
const routed = routeModel(provider, model, configBackend, tier, backend);
|
|
87
139
|
return {
|
|
88
140
|
...config,
|
|
89
141
|
provider: routed.agent === "pi" ? provider : undefined,
|
|
90
142
|
model: routed.model,
|
|
91
|
-
thinking: o.thinking ?? ownSel?.thinking ??
|
|
143
|
+
thinking: o.thinking ?? ownSel?.thinking ?? topThinking ?? config.thinking,
|
|
92
144
|
agent: routed.agent,
|
|
93
145
|
};
|
|
94
146
|
}
|
|
147
|
+
/** A fallback pair as withModelOverrides' overrides: always an explicit backend (a pi pair is pi
|
|
148
|
+
* even on a claude team), a non-pi pair dropping the seam-key provider (its model already
|
|
149
|
+
* carries the whole id), a pi pair keeping its provider. */
|
|
150
|
+
export function pairOverrides(pair) {
|
|
151
|
+
return {
|
|
152
|
+
provider: pair.backend === undefined ? pair.provider : undefined,
|
|
153
|
+
model: pair.model,
|
|
154
|
+
backend: pair.backend ?? "pi",
|
|
155
|
+
thinking: pair.thinking,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
95
158
|
/** The provider/model/thinking overrides a config section (a role entry or `review`) carries
|
|
96
159
|
* into withModelOverrides: a tier-name `model` already chose the seam's tier, so it is not a
|
|
97
160
|
* selector and drops out here. Shared by configForRole and reviewConfig. */
|
|
@@ -99,6 +162,7 @@ function seamOverrides(s) {
|
|
|
99
162
|
return {
|
|
100
163
|
provider: s.provider,
|
|
101
164
|
model: isTierName(s.model) ? undefined : s.model,
|
|
165
|
+
backend: s.backend,
|
|
102
166
|
thinking: s.thinking,
|
|
103
167
|
};
|
|
104
168
|
}
|
|
@@ -130,25 +194,22 @@ export function configForRole(config, role) {
|
|
|
130
194
|
minRunIntervalSeconds: rc.minRunIntervalSeconds ?? config.minRunIntervalSeconds,
|
|
131
195
|
};
|
|
132
196
|
}
|
|
133
|
-
/** The config a role's
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* for the rule: isEligible enforces the same gap when it decides whether
|
|
140
|
-
*
|
|
141
|
-
* figure instead of the role default.
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
export function scheduleConfigForRole(config, role,
|
|
197
|
+
/** The config a role's runs SCHEDULE from: configForRole, except that while PLANS.md still
|
|
198
|
+
* has planning room (planBacklogOpen — below the plan role's eligible-plan target, or holding
|
|
199
|
+
* a plan that needs review/needs replan) the plan role schedules with a ZERO min gap, so a
|
|
200
|
+
* productive plan run reruns on the next poll and an unproductive one drops to the idle
|
|
201
|
+
* backoff. The plan role's own machine signal is `planBacklogFull` (src/scheduling/claims.ts):
|
|
202
|
+
* a fresh project's empty backlog is not full, so this subsumes the old placeholder-status
|
|
203
|
+
* trigger. One home for the rule: isEligible enforces the same gap when it decides whether
|
|
204
|
+
* plan may start, and runner-run.ts hands this config to applyRunOutcome so nextRunAt is
|
|
205
|
+
* scheduled from the same figure instead of the role default. Every other role, and the plan
|
|
206
|
+
* role once the target is met (which already holds the run via the plans-full deferral),
|
|
207
|
+
* schedule configForRole unchanged. */
|
|
208
|
+
export function scheduleConfigForRole(config, role, planBacklogOpen = false) {
|
|
145
209
|
const resolved = configForRole(config, role);
|
|
146
|
-
if (
|
|
210
|
+
if (planBacklogOpen && baseRoleOf(role) === "plan") {
|
|
147
211
|
return { ...resolved, minRunIntervalSeconds: 0 };
|
|
148
212
|
}
|
|
149
|
-
if (bootstrapActive && baseRoleOf(role) === "plan") {
|
|
150
|
-
return { ...resolved, minRunIntervalSeconds: config.minRunIntervalSeconds };
|
|
151
|
-
}
|
|
152
213
|
return resolved;
|
|
153
214
|
}
|
|
154
215
|
/** The config as seen by the review gate's pi runs: the top-level `review` section's
|
|
@@ -173,10 +234,10 @@ export function resolverConfig(config) {
|
|
|
173
234
|
return withModelOverrides(config, "strong", {});
|
|
174
235
|
}
|
|
175
236
|
/** Default wall-clock budget for one reviewer run (`review.timeoutSeconds`). The reviewer holds
|
|
176
|
-
* the merge slot the whole queue waits on, and its only other limits were the
|
|
237
|
+
* the merge slot the whole queue waits on, and its only other limits were the run's budget
|
|
177
238
|
* and the quiet watchdog: reviews measured 2026-09-22/23 ran 2.3 min median, 9.2 min p90 and
|
|
178
239
|
* 24.6 min max, while earlier days had 1.5–3.5 h reviews. 15 min clears the ordinary review and
|
|
179
|
-
* kills a wedged one in minutes instead of the
|
|
240
|
+
* kills a wedged one in minutes instead of the run's hours. */
|
|
180
241
|
export const REVIEW_TIMEOUT_S = 900;
|
|
181
242
|
/** The reviewer's wall-clock floor while the budget fallback carries the team. The free model
|
|
182
243
|
* is typically local and far slower per turn than the budgeted one it replaces, but a review
|
|
@@ -189,7 +250,7 @@ export const REVIEW_TIMEOUT_S = 900;
|
|
|
189
250
|
export const FALLBACK_REVIEW_TIMEOUT_S = 3600;
|
|
190
251
|
/** The config as seen by the gate's reviewer run: the reviewer's model wiring (reviewConfig)
|
|
191
252
|
* with its own time budget — `review.timeoutSeconds`, default REVIEW_TIMEOUT_S — overriding
|
|
192
|
-
* the
|
|
253
|
+
* the run's. A smaller runTimeoutSeconds still wins. A still-progressing gate run gets one
|
|
193
254
|
* extension of the same length (src/pi/pi-watchdogs.ts), so the effective ceiling on a large
|
|
194
255
|
* diff's review is twice this budget. */
|
|
195
256
|
export function reviewRunConfig(config) {
|
|
@@ -221,21 +282,21 @@ export function resolvedModelFields(provider, model, thinking) {
|
|
|
221
282
|
* provider/model/thinking triple formatted as one selector string, or an empty object when
|
|
222
283
|
* no model id is resolved (pi's own default) — the key stays absent rather than empty, the
|
|
223
284
|
* same empty-means-unset rule resolvedModelFields applies to the raw halves. One home of
|
|
224
|
-
* that assembly and guard, shared by the two run-start event writers (
|
|
225
|
-
* review.ts's review_start) and the per-
|
|
226
|
-
* observability surfaces cannot disagree about which model a run starts on. A non-pi
|
|
227
|
-
* named as the selector's provider namespace (
|
|
285
|
+
* that assembly and guard, shared by the two run-start event writers (runner-run.ts's run_start,
|
|
286
|
+
* review.ts's review_start) and the per-role status snapshot (status-data.ts) so the three
|
|
287
|
+
* observability surfaces cannot disagree about which model a run starts on. A non-pi backend is
|
|
288
|
+
* named as the selector's provider namespace (backendQualifiedSelector), so a claude run reads
|
|
228
289
|
* `claude/claude-sonnet-5-5` in every surface instead of a bare model id. */
|
|
229
290
|
export function modelSelectorField(cfg) {
|
|
230
291
|
const { model } = cfg;
|
|
231
|
-
return model ? { model:
|
|
292
|
+
return model ? { model: backendQualifiedSelector({ ...cfg, model }) } : {};
|
|
232
293
|
}
|
|
233
|
-
/** The resolved seam as one selector string,
|
|
294
|
+
/** The resolved seam as one selector string, backend-qualified: a non-pi backend id is the
|
|
234
295
|
* selector's provider namespace (`claude/claude-sonnet-5-5`), the form routeModel parses back
|
|
235
|
-
* into that
|
|
296
|
+
* into that backend; a pi seam keeps `provider/model[:thinking]` byte-identical to before, since
|
|
236
297
|
* pi's providers are real model providers, not a backend namespace. `model` is required — the
|
|
237
298
|
* caller has already guarded that a model resolved (modelSelectorField). */
|
|
238
|
-
export function
|
|
299
|
+
export function backendQualifiedSelector(cfg) {
|
|
239
300
|
const base = formatModelSelector({ provider: cfg.provider, model: cfg.model, thinking: cfg.thinking });
|
|
240
301
|
return cfg.agent && cfg.agent !== "pi" ? `${cfg.agent}/${base}` : base;
|
|
241
302
|
}
|
|
@@ -246,9 +307,9 @@ export function agentQualifiedSelector(cfg) {
|
|
|
246
307
|
export function seamHoldKey(cfg) {
|
|
247
308
|
return cfg.agent !== undefined && cfg.agent !== "pi" ? cfg.agent : cfg.provider;
|
|
248
309
|
}
|
|
249
|
-
/** The `modelPairName` a resolved seam's run is charged under (
|
|
310
|
+
/** The `modelPairName` a resolved seam's run is charged under (RunnerState.dayCostByPair): the
|
|
250
311
|
* routed backend's id stands in for the provider on a non-pi seam, exactly as the fallback
|
|
251
|
-
* pairs carry it (config-fallbacks.ts's
|
|
312
|
+
* pairs carry it (config-fallbacks.ts's seamKeyedPairFields), so a run and the chain
|
|
252
313
|
* step it ran on name one key. */
|
|
253
314
|
export function seamPairName(cfg) {
|
|
254
315
|
return modelPairName({ provider: seamHoldKey(cfg), model: cfg.model });
|
|
@@ -273,18 +334,31 @@ export function seamConcurrencyKey(cfg, piDefault) {
|
|
|
273
334
|
* event: null when no tier resolves to a model (pi's own default), the single selector when
|
|
274
335
|
* every set tier agrees (the string-form `model`), and the `small=…, default=…, strong=…`
|
|
275
336
|
* list when the tiers differ. Reading EVERY tier makes an edit to any tier's entry change the
|
|
276
|
-
* label. Each tier's label is agent-qualified (
|
|
337
|
+
* label. Each tier's label is agent-qualified (backendQualifiedSelector), so flipping the
|
|
277
338
|
* top-level `agent` over a bare-id model relabels every tier and the reload now emits
|
|
278
339
|
* `model_changed` — a team's backend is part of what it runs (PLANS.md "Mixed claude + pi
|
|
279
340
|
* fleets, part 3/4"), and an edit that moves a bare id to another backend must not read as
|
|
280
341
|
* "nothing changed". */
|
|
281
342
|
export function teamModelLabel(config) {
|
|
282
343
|
const entries = MODEL_TIERS.map((tier) => {
|
|
283
|
-
const sel =
|
|
284
|
-
if (!sel)
|
|
344
|
+
const { sel, backend, thinking } = resolveTopTier(config, tier);
|
|
345
|
+
if (!sel && backend === undefined)
|
|
346
|
+
return null;
|
|
347
|
+
const routed = routeModel(sel?.provider, sel?.model, config.backend ?? "pi", tier, backend);
|
|
348
|
+
if (routed.model === undefined)
|
|
285
349
|
return null;
|
|
286
|
-
|
|
287
|
-
|
|
350
|
+
// routeModel consumed an agent-naming provider (or an explicit backend) as the agent; keep
|
|
351
|
+
// the provider only for a pi seam so the label doesn't prefix the agent twice.
|
|
352
|
+
const provider = routed.agent === "pi" ? sel?.provider : undefined;
|
|
353
|
+
return {
|
|
354
|
+
tier,
|
|
355
|
+
label: backendQualifiedSelector({
|
|
356
|
+
provider,
|
|
357
|
+
model: routed.model,
|
|
358
|
+
thinking: sel?.thinking ?? thinking,
|
|
359
|
+
agent: routed.agent,
|
|
360
|
+
}),
|
|
361
|
+
};
|
|
288
362
|
}).filter((e) => e !== null);
|
|
289
363
|
if (entries.length === 0)
|
|
290
364
|
return null;
|
|
@@ -293,18 +367,18 @@ export function teamModelLabel(config) {
|
|
|
293
367
|
return entries.map((e) => `${e.tier}=${e.label}`).join(", ");
|
|
294
368
|
}
|
|
295
369
|
/** One fallback pair rendered for display, agent-qualified (PLANS.md "Mixed claude + pi
|
|
296
|
-
* fleets, part 3/4" gap 7): a pair routed to a non-pi backend — its `
|
|
297
|
-
*
|
|
370
|
+
* fleets, part 3/4" gap 7): a pair routed to a non-pi backend — its `backend`, per
|
|
371
|
+
* seamKeyedPairFields, so `claude` — renders as `claude/claude-sonnet-5-5`,
|
|
298
372
|
* the same agent-qualified form every other surface carries; a pi pair keeps the bare model
|
|
299
373
|
* byte-identical to before (the badge, the team alert and `tumwater role` all name the
|
|
300
374
|
* model a pi fallback runs, never its provider). Falls back through the provider to the
|
|
301
375
|
* caller's `<agent> default` label when both halves are absent. One home so the three display
|
|
302
376
|
* surfaces cannot render one fallback two ways. */
|
|
303
|
-
export function fallbackDisplayPair(pair,
|
|
377
|
+
export function fallbackDisplayPair(pair, backendDefault) {
|
|
304
378
|
const p = pair ?? {};
|
|
305
379
|
if (p.model !== undefined)
|
|
306
|
-
return p.
|
|
380
|
+
return p.backend !== undefined && p.backend !== "pi" ? `${p.backend}/${p.model}` : p.model;
|
|
307
381
|
if (p.provider !== undefined)
|
|
308
382
|
return p.provider;
|
|
309
|
-
return
|
|
383
|
+
return backendDefault;
|
|
310
384
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** The shared write MACHINERY for tumwater.json — the one home for the load →
|
|
2
|
+
* validate → atomic-write idiom, the cross-process lock, and the raw/preserve-role
|
|
3
|
+
* helpers that every writer shares. The operator-editor verbs (config-write.ts's
|
|
4
|
+
* setConfigKey/unsetConfigKey/setRoleModelFields/setDailyBudgetUsd) are whole-config writes
|
|
5
|
+
* with the
|
|
6
|
+
* same traps — a fresh loadConfig that bypasses config.ts's stat cache, validateConfig over
|
|
7
|
+
* the merged candidate, an atomic write because readers poll tumwater.json every ~2 s, a lock
|
|
8
|
+
* that serializes separate-process writers, and a roles section that must never be re-expanded
|
|
9
|
+
* from the catalog — so the idiom lives here, apart from the editor module.
|
|
10
|
+
* (config-request.ts's applyConfigRequest hits the same traps but inlines its own load →
|
|
11
|
+
* validate → write; it imports only withConfigLock and rawConfigObject from this module.)
|
|
12
|
+
* The read side — defaults, loading, saving,
|
|
13
|
+
* and the role-selection helpers — lives in config.ts, the per-run model derivations in
|
|
14
|
+
* config-views.ts, the config KEY language in config-keys.ts, and the operator-editor verbs
|
|
15
|
+
* in config-write.ts. */
|
|
16
|
+
import { configPath, configLockPath, CONFIG_BASENAME } from "../paths.js";
|
|
17
|
+
import { errorMessage } from "../text/text.js";
|
|
18
|
+
import { writeJsonAtomic } from "../files/json-files.js";
|
|
19
|
+
import { objectOrEmpty } from "../files/json-object.js";
|
|
20
|
+
import { withStateLock } from "../concurrency/lock.js";
|
|
21
|
+
import { validateConfig } from "./config-validation.js";
|
|
22
|
+
import { loadConfig, parseJsonConfig, collapseBackendAliases } from "./config.js";
|
|
23
|
+
/** Serialize a config write through the cross-process lock (paths.ts's configLockPath): the
|
|
24
|
+
* CLI, a standalone dashboard's server, and the running team are separate processes, so an
|
|
25
|
+
* unguarded read-modify-write lets one writer's whole-file write silently drop another's change
|
|
26
|
+
* (the same lost update paused-roles.json already guards against). A lock that cannot be
|
|
27
|
+
* acquired within the budget throws rather than writing unlocked — a silently lost config edit
|
|
28
|
+
* is worse than a write that reports an error. */
|
|
29
|
+
const CONFIG_LOCK_TIMEOUT_MS = 10_000;
|
|
30
|
+
export function withConfigLock(root, fn) {
|
|
31
|
+
// withStateLock creates the state dir the lock sits in (a CLI write may run before any state
|
|
32
|
+
// file).
|
|
33
|
+
return withStateLock(configLockPath(root), fn, CONFIG_LOCK_TIMEOUT_MS);
|
|
34
|
+
}
|
|
35
|
+
/** The error arm of the shared write-result union — one home for the `{ ok: false, error }`
|
|
36
|
+
* constructor every writer's failure path returns (a broken on-disk file, a validation
|
|
37
|
+
* problem, a failed write, a lost lock), so the shape cannot drift between callers. */
|
|
38
|
+
export function fail(error) {
|
|
39
|
+
return { ok: false, error };
|
|
40
|
+
}
|
|
41
|
+
/** The one load → mutate → validate → atomic-write idiom the operator-editor verbs share
|
|
42
|
+
* (setDailyBudgetUsd, setConfigKey, unsetConfigKey, setRoleModelFields): a fresh loadConfig that
|
|
43
|
+
* bypasses config.ts's stat cache (a writer must see the latest file), the caller's mutation
|
|
44
|
+
* applied in memory, validateConfig over the whole merged candidate, then writeJsonAtomic
|
|
45
|
+
* (tmp file + rename) because readers poll tumwater.json every ~2 s. The whole load-to-write
|
|
46
|
+
* sequence runs under withConfigLock, so two processes racing different keys cannot each write
|
|
47
|
+
* from the same pre-race snapshot and lose one update. On any failure — a broken on-disk file,
|
|
48
|
+
* a type-invalid candidate, a failed write, or a lock that cannot be acquired — the file is
|
|
49
|
+
* left untouched (and no tmp remnant is left behind); the error surfaces as a string so
|
|
50
|
+
* callers (both dashboards, the CLI) can print it without try/catch plumbing. */
|
|
51
|
+
export function writeConfigMutation(root, mutate) {
|
|
52
|
+
try {
|
|
53
|
+
return withConfigLock(root, () => {
|
|
54
|
+
let cfg;
|
|
55
|
+
try {
|
|
56
|
+
cfg = loadConfig(root); // fresh — bypasses the stat cache on purpose
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return fail(errorMessage(err)); // broken file: never overwrite it with defaults
|
|
60
|
+
}
|
|
61
|
+
const mutated = mutate(cfg);
|
|
62
|
+
// An unset that finds nothing to remove returns the loaded config unchanged. Write nothing:
|
|
63
|
+
// re-serializing the merged config would reformat tumwater.json (defaults expanded) and
|
|
64
|
+
// break the no-op's "changes nothing" contract. The bytes stay identical; validation of an
|
|
65
|
+
// already-validated config is unnecessary.
|
|
66
|
+
if (mutated === cfg)
|
|
67
|
+
return { ok: true };
|
|
68
|
+
// Collapse legacy/new pi spellings before writing so a merged config (which always
|
|
69
|
+
// carries the default piArgs) never lands both forms in the file.
|
|
70
|
+
const candidate = collapseBackendAliases(mutated);
|
|
71
|
+
try {
|
|
72
|
+
validateConfig(candidate); // throws listing every problem — nothing is written on failure
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return fail(errorMessage(err));
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
// The trailing newline is tumwater.json's convention (POSIX text file).
|
|
79
|
+
writeJsonAtomic(configPath(root), candidate, true);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return fail(errorMessage(err));
|
|
83
|
+
}
|
|
84
|
+
return { ok: true };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return fail(errorMessage(err)); // lock acquire timeout/failure — nothing written
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** A shallow copy of `obj` without `key` — the copy each `unsetConfigKey` branch edits
|
|
92
|
+
* before it decides whether the removal emptied its container, and the shape the branch then
|
|
93
|
+
* returns when it did (an emptied map, the last field of a `roles.<id>` entry, an emptied
|
|
94
|
+
* `model` map), so tumwater.json never keeps an empty object. */
|
|
95
|
+
export function without(obj, key) {
|
|
96
|
+
const copy = { ...obj };
|
|
97
|
+
delete copy[key];
|
|
98
|
+
return copy;
|
|
99
|
+
}
|
|
100
|
+
/** The raw parsed tumwater.json as a plain object — the pre-defaults view `unsetConfigKey`'s
|
|
101
|
+
* presence check needs, because the merged config always carries defaults a top-level key or a
|
|
102
|
+
* `roles.<id>.<field>` (e.g. the seeded `minRunIntervalSeconds`) may never have been set to.
|
|
103
|
+
* Read inside writeConfigMutation's lock; a missing or unparsable file is `{}` (loadConfig has
|
|
104
|
+
* already rejected a broken file before a mutation runs, and an absent file is the bare
|
|
105
|
+
* defaults — there is nothing to retract). */
|
|
106
|
+
export function rawConfigObject(root) {
|
|
107
|
+
const { raw } = parseJsonConfig(configPath(root), CONFIG_BASENAME);
|
|
108
|
+
return objectOrEmpty(raw);
|
|
109
|
+
}
|
|
110
|
+
/** Reinstall the raw tumwater.json's `roles` section onto a whole-config write candidate (or
|
|
111
|
+
* drop it when the file had none), so a bare `config set`/`unset` of an unrelated key never
|
|
112
|
+
* re-expands the file's roles to the built-in catalog. `roles` merges per-entry at load, so
|
|
113
|
+
* this is about the bytes the operator can read and edit, not the resolved view: omission
|
|
114
|
+
* still cannot disable a role, and a partial section stays exactly as written. */
|
|
115
|
+
export function preserveFileRoles(root, candidate) {
|
|
116
|
+
const record = candidate;
|
|
117
|
+
const rawRoles = rawConfigObject(root).roles;
|
|
118
|
+
if (rawRoles === undefined)
|
|
119
|
+
delete record.roles;
|
|
120
|
+
else
|
|
121
|
+
record.roles = rawRoles;
|
|
122
|
+
}
|
|
123
|
+
/** preserveFileRoles, returning the candidate so a whole-config write branch can
|
|
124
|
+
* `return preservingFileRoles(root, next)` after building it. */
|
|
125
|
+
export function preservingFileRoles(root, candidate) {
|
|
126
|
+
preserveFileRoles(root, candidate);
|
|
127
|
+
return candidate;
|
|
128
|
+
}
|
|
129
|
+
/** The config with top-level `key` set to `section`, or removed when `section` is empty — the
|
|
130
|
+
* unset shape for the per-role maps, `model` tiers and `backends`, which never leave an empty
|
|
131
|
+
* object in the file. A whole-config write, so the file's own roles section is kept. */
|
|
132
|
+
export function withSectionOrDropped(root, cfg, key, section) {
|
|
133
|
+
const next = Object.keys(section).length === 0 ? without(cfg, key) : { ...cfg, [key]: section };
|
|
134
|
+
return preservingFileRoles(root, next);
|
|
135
|
+
}
|