tumwater 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +55 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +6 -4
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +404 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +281 -55
- package/dist/src/backlog/backlog-cli-view.js +255 -0
- package/dist/src/backlog/backlog-cli.js +91 -335
- package/dist/src/backlog/backlog-commit.js +20 -13
- package/dist/src/backlog/backlog-eligibility.js +193 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +264 -0
- package/dist/src/backlog/backlog-release.js +134 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-guardrails.js +136 -0
- package/dist/src/baseline/main-red.js +153 -36
- package/dist/src/baseline/main-watch.js +232 -0
- package/dist/src/brief/brief-edit.js +12 -12
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +31 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +65 -3
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +181 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +183 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +291 -598
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +525 -0
- package/dist/src/cli/cli-start-summary.js +167 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
- package/dist/src/cli/completion.js +169 -22
- package/dist/src/cli/config-commands.js +58 -26
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +258 -82
- package/dist/src/cli/log-commands.js +164 -85
- package/dist/src/cli/question-commands.js +211 -10
- package/dist/src/cli.js +173 -45
- package/dist/src/collections.js +56 -13
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +16 -1
- package/dist/src/config/config-fallbacks.js +94 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +169 -222
- package/dist/src/config/config-views.js +136 -54
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +164 -265
- package/dist/src/config/config.js +53 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
- package/dist/src/doctor/doctor-backlog-plans.js +398 -0
- package/dist/src/doctor/doctor-backlog.js +249 -512
- package/dist/src/doctor/doctor-check-names.js +85 -0
- package/dist/src/doctor/doctor-checks.js +61 -593
- package/dist/src/doctor/doctor-claude-checks.js +142 -0
- package/dist/src/doctor/doctor-codex-checks.js +276 -0
- package/dist/src/doctor/doctor-contention-checks.js +506 -0
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +80 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +216 -537
- package/dist/src/doctor/doctor-notify.js +9 -5
- package/dist/src/doctor/doctor-opencode-checks.js +164 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +397 -0
- package/dist/src/doctor/doctor.js +126 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +21 -12
- package/dist/src/events/event-format.js +185 -89
- package/dist/src/events/event-labels.js +11 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +52 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +80 -26
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +65 -60
- package/dist/src/failure/failure-render.js +29 -21
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +35 -35
- package/dist/src/files/file-queue.js +17 -0
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +88 -225
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +101 -143
- package/dist/src/gates/maintenance-quota.js +37 -30
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +89 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +54 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +43 -19
- package/dist/src/gui/gui-backlog-endpoints.js +156 -0
- package/dist/src/gui/gui-command.js +26 -13
- package/dist/src/gui/gui-config-commands.js +168 -80
- package/dist/src/gui/gui-endpoint-commands.js +5 -4
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +235 -13
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +97 -0
- package/dist/src/gui/gui-server.js +26 -5
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +155 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +190 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +28 -17
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +58 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +45 -19
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +214 -0
- package/dist/src/merge/merge-queue-render.js +48 -11
- package/dist/src/merge/merge-queue.js +52 -15
- package/dist/src/merge/merge-requeue.js +103 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +27 -11
- package/dist/src/merge/merge-to-main.js +77 -26
- package/dist/src/merge/merge-top.js +83 -0
- package/dist/src/merge/merge-verify.js +75 -18
- package/dist/src/notes-cli.js +35 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +352 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
- package/dist/src/orchestrator/orchestrator.js +129 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +245 -0
- package/dist/src/orchestrator/scheduling-skip.js +195 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +126 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +26 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +150 -217
- package/dist/src/prompt/notes-backlog.js +184 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +131 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +188 -124
- package/dist/src/report/report-fold.js +130 -54
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +136 -65
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +23 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +112 -1
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +239 -0
- package/dist/src/review/protected-edits.js +24 -10
- package/dist/src/review/quarantine-render.js +65 -0
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +180 -401
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +127 -12
- package/dist/src/roles/role-guidance.js +65 -20
- package/dist/src/roles/role-labels.js +7 -5
- package/dist/src/roles/role-render.js +108 -56
- package/dist/src/roles/role-view.js +95 -56
- package/dist/src/roles/roles.js +33 -33
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/run/qa-coverage.js +125 -0
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +175 -0
- package/dist/src/run/run-detail-render.js +91 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +178 -0
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +231 -0
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +216 -0
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +191 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +109 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +103 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claim-entries.js +49 -0
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +49 -17
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +43 -28
- package/dist/src/status/status-roles.js +50 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +7 -8
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +65 -18
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +60 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +78 -13
- package/dist/src/ui/badges.js +99 -38
- package/dist/src/ui/gui/gui-client-backlog.js +314 -0
- package/dist/src/ui/gui/gui-client-boot.js +89 -61
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
- package/dist/src/ui/gui/gui-client-drawer.js +155 -289
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +437 -68
- package/dist/src/ui/gui/gui-client-labels.js +6 -3
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +60 -52
- package/dist/src/ui/gui/gui-client-pending.js +405 -24
- package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
- package/dist/src/ui/gui/gui-client-report.js +105 -430
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +163 -5
- package/dist/src/ui/gui/gui-client-team.js +36 -465
- package/dist/src/ui/gui/gui-client.js +38 -26
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +10 -7
- package/dist/src/ui/gui/gui-styles.js +38 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +364 -49
- package/dist/src/ui/status-render.js +109 -82
- package/dist/src/ui/team-alerts.js +728 -62
- package/dist/src/ui/tone.js +12 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +53 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +4 -12
- package/dist/src/ui/tui/tui-editor-backends.js +127 -0
- package/dist/src/ui/tui/tui-editor-brief.js +14 -36
- package/dist/src/ui/tui/tui-editor-budget.js +36 -39
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
- package/dist/src/ui/tui/tui-editor-config.js +99 -0
- package/dist/src/ui/tui/tui-editor-entry.js +39 -64
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +46 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
- package/dist/src/ui/tui/tui-editor-interval.js +29 -16
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +9 -29
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
- package/dist/src/ui/tui/tui-editor-retire.js +5 -13
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +101 -34
- package/dist/src/ui/tui/tui-editor-slots.js +24 -0
- package/dist/src/ui/tui/tui-editors.js +44 -2
- package/dist/src/ui/tui/tui-frame.js +68 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +33 -9
- package/dist/src/ui/tui/tui-pane-activity.js +35 -19
- package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +168 -8
- package/dist/src/ui/tui/tui-pane-history.js +446 -82
- package/dist/src/ui/tui/tui-pane-pending.js +539 -15
- package/dist/src/ui/tui/tui-pane-queued.js +78 -57
- package/dist/src/ui/tui/tui-pane-report.js +54 -16
- package/dist/src/ui/tui/tui-pane-session.js +34 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +91 -28
- package/dist/src/ui/tui/tui-panes.js +153 -34
- package/dist/src/ui/tui/tui.js +18 -15
- package/dist/src/verdict/fix-claim.js +129 -33
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +77 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/leftover.js +0 -130
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/qa-coverage.js +0 -108
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,107 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
1
|
+
/** Harness-mediated config WRITES — the four operator-editor mutators behind the operator
|
|
2
|
+
* surfaces (the TUI's Ctrl+B editor, the GUI's /api/budget endpoint, `tumwater config
|
|
3
|
+
* set`/`unset`), addressed by key through config-keys.ts. The shared write MACHINERY these
|
|
4
|
+
* verbs run on — the load → validate → atomic-write idiom, the cross-process lock, and the
|
|
5
|
+
* raw/preserve-role helpers — lives in config-write-core.ts, which the director's staged
|
|
6
|
+
* request (config-request.ts) also consumes; every writer here loads fresh (bypassing
|
|
7
|
+
* config.ts's stat cache), validates before writing, and writes atomically, because readers
|
|
8
|
+
* poll tumwater.json every ~2 s. The read side — defaults, loading, saving, and the
|
|
9
|
+
* role-selection helpers — lives in config.ts, the per-run model derivations in
|
|
10
|
+
* config-views.ts, and the config KEY language — how keys are parsed, addressed and given
|
|
11
|
+
* per-key value screeners — lives in config-keys.ts (both the write verbs here and the read
|
|
12
|
+
* side / doctor share it). */
|
|
13
|
+
import { isDeepStrictEqual } from "node:util";
|
|
11
14
|
import { modelTierMap } from "./config-schema.js";
|
|
12
|
-
import { configPath, configLockPath, configRequestPath, CONFIG_BASENAME } from "../paths.js";
|
|
13
|
-
import { errorMessage } from "../text/text.js";
|
|
14
|
-
import { writeJsonAtomic } from "../files/json-files.js";
|
|
15
15
|
import { isJsonObject, objectOrEmpty } from "../files/json-object.js";
|
|
16
|
-
import { withStateLock } from "../concurrency/lock.js";
|
|
17
16
|
import { show } from "./config-field-checks.js";
|
|
18
|
-
import {
|
|
19
|
-
import { canonicalConfigKey, normalizeConfigKeys } from "./config-aliases.js";
|
|
17
|
+
import { canonicalConfigKey } from "./config-aliases.js";
|
|
20
18
|
import { unknownRoleMessage } from "../roles/roles.js";
|
|
21
|
-
import { loadConfig,
|
|
19
|
+
import { loadConfig, collapseBackendAliases, knownRoleIdsCached, } from "./config.js";
|
|
22
20
|
import { checkDailyBudgetUsd, checkProviderConcurrencyCap, checkQuietHours, parseConfigKey, PER_KEY_VALIDATORS, unknownConfigKeyError, unknownRoleFieldError, } from "./config-keys.js";
|
|
23
|
-
|
|
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
|
-
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
|
-
function fail(error) {
|
|
39
|
-
return { ok: false, error };
|
|
40
|
-
}
|
|
41
|
-
/** The one load → mutate → validate → atomic-write idiom every top-level config writer
|
|
42
|
-
* shares (setDailyBudgetUsd, setConfigKey): a fresh loadConfig that bypasses config.ts's
|
|
43
|
-
* stat cache (a writer must see the latest file), the caller's mutation applied in memory,
|
|
44
|
-
* validateConfig over the whole merged candidate, then writeJsonAtomic (tmp file + rename)
|
|
45
|
-
* because readers poll tumwater.json every ~2 s. The whole load-to-write sequence runs under
|
|
46
|
-
* withConfigLock, so two processes racing different keys cannot each write from the same
|
|
47
|
-
* pre-race snapshot and lose one update. On any failure — a broken on-disk file, a
|
|
48
|
-
* 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
|
-
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 = collapseAgentAliases(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
|
-
}
|
|
21
|
+
import { writeConfigMutation, fail, rawConfigObject, without, preservingFileRoles, preserveFileRoles, withSectionOrDropped, } from "./config-write-core.js";
|
|
91
22
|
/** Set the daily cost budget cap in tumwater.json through the shared writer idiom (see
|
|
92
23
|
* writeConfigMutation), after checkDailyBudgetUsd has screened the value with its own
|
|
93
|
-
* actionable message — the one both dashboards flash.
|
|
24
|
+
* actionable message — the one both dashboards flash. A cap the config already resolves to
|
|
25
|
+
* (an omitted key resolves to the default) returns `unchanged: true` and writes nothing. */
|
|
94
26
|
export function setDailyBudgetUsd(root, value) {
|
|
95
27
|
const problem = checkDailyBudgetUsd(value);
|
|
96
28
|
if (problem)
|
|
97
29
|
return fail(problem);
|
|
98
|
-
|
|
30
|
+
let unchanged = false;
|
|
31
|
+
const result = writeConfigMutation(root, (cfg) => {
|
|
32
|
+
if (cfg.maxDailyCostUsd === value) {
|
|
33
|
+
unchanged = true;
|
|
34
|
+
return cfg; // the loaded config itself: writeConfigMutation skips the write
|
|
35
|
+
}
|
|
99
36
|
const next = { ...cfg, maxDailyCostUsd: value };
|
|
100
37
|
// A whole-config write: keep the file's own roles section (see preserveFileRoles).
|
|
101
38
|
preserveFileRoles(root, next);
|
|
102
39
|
return next;
|
|
103
40
|
});
|
|
41
|
+
return result.ok && unchanged ? { ok: true, unchanged: true } : result;
|
|
104
42
|
}
|
|
43
|
+
/** The top-level team keys whose value is a plain positive integer (validateConfig's
|
|
44
|
+
* POSITIVE_INTEGER checks, config-validation.ts): the run-concurrency knobs plus the legacy
|
|
45
|
+
* merge batch cap. For these, an exponent spelling like "4e1" is itself a JSON number (40)
|
|
46
|
+
* that setConfigKey's JSON.parse would read past validateConfig's integer check and silently
|
|
47
|
+
* write — the same silent misread the per-role instances/interval guards, the TUI team-key
|
|
48
|
+
* editors, and the GUI Settings rows refuse — so setConfigKey refuses it, naming the key and
|
|
49
|
+
* writing nothing. The per-provider map (maxConcurrentPerProvider.<provider>) has its own
|
|
50
|
+
* checkProviderConcurrencyCap screen and is not in this set. */
|
|
51
|
+
const PLAIN_INTEGER_TOP_LEVEL_KEYS = new Set([
|
|
52
|
+
"maxConcurrent",
|
|
53
|
+
"mergeBatchMax",
|
|
54
|
+
"maxConcurrentChecks",
|
|
55
|
+
"worktreeSlots",
|
|
56
|
+
]);
|
|
105
57
|
/** Set ONE top-level key of tumwater.json from its raw CLI text (`tumwater config set`'s
|
|
106
58
|
* engine): the key must be a member of TOP_LEVEL_KEYS — the same protection checkKnownKeys
|
|
107
59
|
* gives the file itself, so `config set modle x` cannot write a dead key the runtime would
|
|
@@ -116,12 +68,33 @@ export function setDailyBudgetUsd(root, value) {
|
|
|
116
68
|
* ROLE_ENTRY_KEYS (a did-you-mean suggestion otherwise, via text/suggest.ts's typoSuffix); a
|
|
117
69
|
* typo'd role id and a type-invalid value are left for validateConfig to reject with its own
|
|
118
70
|
* actionable message. Returns the parsed value and the previous one so the caller can
|
|
119
|
-
* confirm the change; on any failure the file is untouched
|
|
71
|
+
* confirm the change; on any failure the file is untouched
|
|
72
|
+
* (see writeConfigMutation). A value equal to the resolved one is a no-op: nothing is written and
|
|
73
|
+
* the result carries `unchanged`. */
|
|
120
74
|
export function setConfigKey(root, key, rawValue) {
|
|
121
75
|
key = canonicalConfigKey(key);
|
|
122
76
|
const parsed = parseConfigKey(key);
|
|
123
77
|
if (parsed.kind === "error")
|
|
124
78
|
return fail(parsed.error);
|
|
79
|
+
// Shape, before the JSON.parse, for every plain-positive-integer key this verb can write —
|
|
80
|
+
// the top-level run-concurrency knobs and legacy merge batch cap, plus the integer-bound
|
|
81
|
+
// team-scope keys that arrive as a dotted key (`maxConcurrentPerProvider.<provider>`,
|
|
82
|
+
// `roles.<id>.instances`, the `bootstrap.untilPlansDone` section field): refuse an exponent
|
|
83
|
+
// spelling ("4e1" → 40) that JSON.parse would read past validateConfig's integer check and
|
|
84
|
+
// silently write, naming the key and writing nothing — the twin of the per-role guards, the
|
|
85
|
+
// TUI team-key editors, and the GUI Settings rows. A non-exponent invalid falls through for
|
|
86
|
+
// validateConfig (whose message names the key). Empty clears the key below.
|
|
87
|
+
const trimmed = rawValue.trim();
|
|
88
|
+
const isPlainIntegerKey = (parsed.kind === "top" && PLAIN_INTEGER_TOP_LEVEL_KEYS.has(key)) ||
|
|
89
|
+
(parsed.kind === "map" && parsed.map === "maxConcurrentPerProvider") ||
|
|
90
|
+
(parsed.kind === "role" && parsed.field === "instances") ||
|
|
91
|
+
(parsed.kind === "section" && parsed.section === "bootstrap" && parsed.field === "untilPlansDone");
|
|
92
|
+
if (isPlainIntegerKey &&
|
|
93
|
+
trimmed !== "" &&
|
|
94
|
+
!/^\d*\.?\d+$/.test(trimmed) &&
|
|
95
|
+
/^\d+(\.\d+)?[eE][+-]?\d+$/.test(trimmed))
|
|
96
|
+
return fail(`${key} must be a plain integer — no exponent notation`);
|
|
97
|
+
let unchanged; // set by a mutation callback when value equals the resolved one
|
|
125
98
|
let value;
|
|
126
99
|
try {
|
|
127
100
|
value = JSON.parse(rawValue);
|
|
@@ -155,6 +128,10 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
155
128
|
if (parsed.kind === "map") {
|
|
156
129
|
const existing = record[parsed.map] ?? {};
|
|
157
130
|
oldValue = existing[parsed.role];
|
|
131
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
132
|
+
unchanged = true;
|
|
133
|
+
return cfg;
|
|
134
|
+
}
|
|
158
135
|
const next = { ...cfg, [parsed.map]: { ...existing, [parsed.role]: value } };
|
|
159
136
|
return preservingFileRoles(root, next);
|
|
160
137
|
}
|
|
@@ -166,19 +143,39 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
166
143
|
// there and nothing is written.
|
|
167
144
|
const existing = modelTierMap(record.model);
|
|
168
145
|
oldValue = existing[parsed.tier];
|
|
146
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
147
|
+
unchanged = true;
|
|
148
|
+
return cfg;
|
|
149
|
+
}
|
|
169
150
|
const next = { ...cfg, model: { ...existing, [parsed.tier]: value } };
|
|
170
151
|
return preservingFileRoles(root, next);
|
|
171
152
|
}
|
|
153
|
+
if (parsed.kind === "section") {
|
|
154
|
+
// Merge one field into the loaded section, keeping its other fields; validateConfig in
|
|
155
|
+
// writeConfigMutation owns the value type, so a bad value fails there and writes nothing.
|
|
156
|
+
const existing = record[parsed.section] ?? {};
|
|
157
|
+
oldValue = existing[parsed.field];
|
|
158
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
159
|
+
unchanged = true;
|
|
160
|
+
return cfg;
|
|
161
|
+
}
|
|
162
|
+
const next = { ...cfg, [parsed.section]: { ...existing, [parsed.field]: value } };
|
|
163
|
+
return preservingFileRoles(root, next);
|
|
164
|
+
}
|
|
172
165
|
if (parsed.kind === "agent") {
|
|
173
166
|
// Merge one field of one backend's entry, preserving the other field and the other
|
|
174
167
|
// backends. validateConfig in writeConfigMutation owns the value type, so a bad
|
|
175
168
|
// value fails there and nothing is written.
|
|
176
|
-
const
|
|
177
|
-
const entry = { ...(
|
|
169
|
+
const backends = { ...record.backends };
|
|
170
|
+
const entry = { ...(backends[parsed.id] ?? {}) };
|
|
178
171
|
oldValue = entry[parsed.field];
|
|
172
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
173
|
+
unchanged = true;
|
|
174
|
+
return cfg;
|
|
175
|
+
}
|
|
179
176
|
entry[parsed.field] = value;
|
|
180
|
-
|
|
181
|
-
const next = { ...cfg, backends:
|
|
177
|
+
backends[parsed.id] = entry;
|
|
178
|
+
const next = { ...cfg, backends: backends };
|
|
182
179
|
// Writers keep one spelling: the new pi form drops its legacy alias so the file can
|
|
183
180
|
// never hold both (validateConfig's raw pass rejects a file that does).
|
|
184
181
|
if (parsed.id === "pi") {
|
|
@@ -188,6 +185,12 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
188
185
|
else
|
|
189
186
|
delete nextRecord.piArgs;
|
|
190
187
|
}
|
|
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
|
+
}
|
|
191
194
|
return preservingFileRoles(root, next);
|
|
192
195
|
}
|
|
193
196
|
// Base the merge on the FILE's own roles, not the loaded (catalog-merged) map: an
|
|
@@ -199,6 +202,10 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
199
202
|
const rawEntryCandidate = rawRoles[parsed.id];
|
|
200
203
|
const rawEntry = objectOrEmpty(rawEntryCandidate);
|
|
201
204
|
oldValue = record.roles?.[parsed.id]?.[parsed.field];
|
|
205
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
206
|
+
unchanged = true;
|
|
207
|
+
return cfg;
|
|
208
|
+
}
|
|
202
209
|
const roles = {
|
|
203
210
|
...rawRoles,
|
|
204
211
|
[parsed.id]: { ...rawEntry, [parsed.field]: value },
|
|
@@ -210,6 +217,18 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
210
217
|
});
|
|
211
218
|
if (!result.ok)
|
|
212
219
|
return result;
|
|
220
|
+
if (unchanged)
|
|
221
|
+
return { ok: true, value, oldValue, unchanged: true };
|
|
222
|
+
// A string replaces a whole object tier entry, so its `backend` goes with it.
|
|
223
|
+
if (parsed.kind === "tier" && isJsonObject(oldValue)) {
|
|
224
|
+
const dropped = typeof oldValue.backend === "string" ? `backend ${show(oldValue.backend)}` : "its other keys";
|
|
225
|
+
return {
|
|
226
|
+
ok: true,
|
|
227
|
+
value,
|
|
228
|
+
oldValue,
|
|
229
|
+
note: `model.${parsed.tier} was an object entry — the string replaces it, dropping ${dropped}`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
213
232
|
return { ok: true, value, oldValue };
|
|
214
233
|
}
|
|
215
234
|
const unknown = unknownConfigKeyError(key);
|
|
@@ -240,6 +259,10 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
240
259
|
let oldValue;
|
|
241
260
|
const result = writeConfigMutation(root, (cfg) => {
|
|
242
261
|
oldValue = cfg[key];
|
|
262
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
263
|
+
unchanged = true;
|
|
264
|
+
return cfg;
|
|
265
|
+
}
|
|
243
266
|
const next = { ...cfg, [key]: value };
|
|
244
267
|
// `roles` merges per-entry at load (overlayDefaults seeds the catalog and lets the file
|
|
245
268
|
// override one entry), so an unrelated whole-config write must not re-expand the file's
|
|
@@ -251,30 +274,46 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
251
274
|
// legacy aliases the new pi form now carries, so the file can never hold both.
|
|
252
275
|
// Writers keep one spelling: a whole-section `backends` set (the GUI Save path) drops the
|
|
253
276
|
// legacy agentBin/piArgs aliases the new pi form now carries, so the file can never hold
|
|
254
|
-
// both — the same
|
|
277
|
+
// both — the same collapseBackendAliases rule the whole-config writers (above and in
|
|
255
278
|
// config.ts) use, so the alias policy cannot drift between the per-key arm and the others.
|
|
256
279
|
if (key === "backends")
|
|
257
|
-
return
|
|
280
|
+
return collapseBackendAliases(next);
|
|
258
281
|
// Writers keep one spelling: a legacy top-level set drops the matching backends.pi field, so
|
|
259
282
|
// the file can never hold both (validateConfig's raw pass rejects a file that does).
|
|
260
283
|
if (key === "agentBin" || key === "piArgs") {
|
|
261
284
|
const record = next;
|
|
262
|
-
const
|
|
263
|
-
const entry = { ...(
|
|
285
|
+
const backends = { ...(record.backends ?? {}) };
|
|
286
|
+
const entry = { ...(backends.pi ?? {}) };
|
|
264
287
|
delete entry[key === "agentBin" ? "bin" : "args"];
|
|
265
288
|
if (Object.keys(entry).length === 0)
|
|
266
|
-
delete
|
|
289
|
+
delete backends.pi;
|
|
267
290
|
else
|
|
268
|
-
|
|
269
|
-
return { ...next, backends:
|
|
291
|
+
backends.pi = entry;
|
|
292
|
+
return { ...next, backends: backends };
|
|
293
|
+
}
|
|
294
|
+
// Writers keep one spelling: setting the top-level `claudeWindowReserve` drops the nested
|
|
295
|
+
// `backends.claude.windowReserve`, so a supported `config set` never writes the rejected pair
|
|
296
|
+
// (validateConfig still refuses a hand-edited file holding both).
|
|
297
|
+
if (key === "claudeWindowReserve") {
|
|
298
|
+
const record = next;
|
|
299
|
+
const backends = { ...(record.backends ?? {}) };
|
|
300
|
+
const entry = { ...(backends.claude ?? {}) };
|
|
301
|
+
delete entry.windowReserve;
|
|
302
|
+
if (Object.keys(entry).length === 0)
|
|
303
|
+
delete backends.claude;
|
|
304
|
+
else
|
|
305
|
+
backends.claude = entry;
|
|
306
|
+
return { ...next, backends: backends };
|
|
270
307
|
}
|
|
271
308
|
return next;
|
|
272
309
|
});
|
|
273
310
|
if (!result.ok)
|
|
274
311
|
return result;
|
|
312
|
+
if (unchanged)
|
|
313
|
+
return { ok: true, value, oldValue, unchanged: true };
|
|
275
314
|
return { ok: true, value, oldValue };
|
|
276
315
|
}
|
|
277
|
-
/** Set a role's provider/model/thinking override
|
|
316
|
+
/** Set a role's provider/model/thinking/backend override set in tumwater.json (`roles.<base>`),
|
|
278
317
|
* the write behind the dashboard role drawer's Model overrides editor: one writeConfigMutation
|
|
279
318
|
* rebuilds `roles.<base>` from the FILE's own `roles` section (the `rawConfigObject(root).roles` /
|
|
280
319
|
* `objectOrEmpty` idiom setConfigKey's role branch uses, so the write never re-expands the
|
|
@@ -297,7 +336,8 @@ export function setRoleModelFields(root, base, fields) {
|
|
|
297
336
|
// a bogus id "updated".
|
|
298
337
|
if (!knownRoleIdsCached(root).includes(base))
|
|
299
338
|
return fail(unknownRoleMessage(base, knownRoleIdsCached(root)));
|
|
300
|
-
|
|
339
|
+
let unchanged; // set when no field differs from the file
|
|
340
|
+
const result = writeConfigMutation(root, (cfg) => {
|
|
301
341
|
const rawRolesCandidate = rawConfigObject(root).roles;
|
|
302
342
|
const rawRoles = objectOrEmpty(rawRolesCandidate);
|
|
303
343
|
const rawEntryCandidate = rawRoles[base];
|
|
@@ -318,8 +358,10 @@ export function setRoleModelFields(root, base, fields) {
|
|
|
318
358
|
changed = true;
|
|
319
359
|
}
|
|
320
360
|
}
|
|
321
|
-
if (!changed)
|
|
361
|
+
if (!changed) {
|
|
362
|
+
unchanged = true;
|
|
322
363
|
return cfg; // writeConfigMutation skips the write — file bytes identical
|
|
364
|
+
}
|
|
323
365
|
const nextRoles = { ...rawRoles };
|
|
324
366
|
// Clearing the entry's last override empties it — never leave an empty `roles.<base>: {}`
|
|
325
367
|
// (the file-wide no-empty-object invariant unsetConfigKey's `without` keeps); an emptied
|
|
@@ -333,51 +375,9 @@ export function setRoleModelFields(root, base, fields) {
|
|
|
333
375
|
return without(cfg, "roles");
|
|
334
376
|
return { ...cfg, roles: nextRoles };
|
|
335
377
|
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
* returns when it did (an emptied map, the last field of a `roles.<id>` entry, an emptied
|
|
340
|
-
* `model` map), so tumwater.json never keeps an empty object. */
|
|
341
|
-
function without(obj, key) {
|
|
342
|
-
const copy = { ...obj };
|
|
343
|
-
delete copy[key];
|
|
344
|
-
return copy;
|
|
345
|
-
}
|
|
346
|
-
/** The raw parsed tumwater.json as a plain object — the pre-defaults view `unsetConfigKey`'s
|
|
347
|
-
* presence check needs, because the merged config always carries defaults a top-level key or a
|
|
348
|
-
* `roles.<id>.<field>` (e.g. the seeded `minRunIntervalSeconds`) may never have been set to.
|
|
349
|
-
* Read inside writeConfigMutation's lock; a missing or unparsable file is `{}` (loadConfig has
|
|
350
|
-
* already rejected a broken file before a mutation runs, and an absent file is the bare
|
|
351
|
-
* defaults — there is nothing to retract). */
|
|
352
|
-
function rawConfigObject(root) {
|
|
353
|
-
const { raw } = parseJsonConfig(configPath(root), CONFIG_BASENAME);
|
|
354
|
-
return objectOrEmpty(raw);
|
|
355
|
-
}
|
|
356
|
-
/** Reinstall the raw tumwater.json's `roles` section onto a whole-config write candidate (or
|
|
357
|
-
* drop it when the file had none), so a bare `config set`/`unset` of an unrelated key never
|
|
358
|
-
* re-expands the file's roles to the built-in catalog. `roles` merges per-entry at load, so
|
|
359
|
-
* this is about the bytes the operator can read and edit, not the resolved view: omission
|
|
360
|
-
* still cannot disable a role, and a partial section stays exactly as written. */
|
|
361
|
-
function preserveFileRoles(root, candidate) {
|
|
362
|
-
const record = candidate;
|
|
363
|
-
const rawRoles = rawConfigObject(root).roles;
|
|
364
|
-
if (rawRoles === undefined)
|
|
365
|
-
delete record.roles;
|
|
366
|
-
else
|
|
367
|
-
record.roles = rawRoles;
|
|
368
|
-
}
|
|
369
|
-
/** preserveFileRoles, returning the candidate so a whole-config write branch can
|
|
370
|
-
* `return preservingFileRoles(root, next)` after building it. */
|
|
371
|
-
function preservingFileRoles(root, candidate) {
|
|
372
|
-
preserveFileRoles(root, candidate);
|
|
373
|
-
return candidate;
|
|
374
|
-
}
|
|
375
|
-
/** The config with top-level `key` set to `section`, or removed when `section` is empty — the
|
|
376
|
-
* unset shape for the per-role maps, `model` tiers and `backends`, which never leave an empty
|
|
377
|
-
* object in the file. A whole-config write, so the file's own roles section is kept. */
|
|
378
|
-
function withSectionOrDropped(root, cfg, key, section) {
|
|
379
|
-
const next = Object.keys(section).length === 0 ? without(cfg, key) : { ...cfg, [key]: section };
|
|
380
|
-
return preservingFileRoles(root, next);
|
|
378
|
+
if (result.ok && unchanged)
|
|
379
|
+
return { ok: true, unchanged: true };
|
|
380
|
+
return result;
|
|
381
381
|
}
|
|
382
382
|
/** Remove ONE value of tumwater.json from its raw CLI text (`tumwater config unset <key>`):
|
|
383
383
|
* the inverse of setConfigKey. A bare top-level key is deleted (reverting to its default or
|
|
@@ -447,24 +447,34 @@ export function unsetConfigKey(root, key) {
|
|
|
447
447
|
const next = without(existing, parsed.tier);
|
|
448
448
|
return withSectionOrDropped(root, cfg, "model", next);
|
|
449
449
|
}
|
|
450
|
+
if (parsed.kind === "section") {
|
|
451
|
+
// Presence is judged against the FILE: a field only the default carries is a no-op
|
|
452
|
+
// success, and an emptied section leaves the file.
|
|
453
|
+
const rawSection = objectOrEmpty(raw[parsed.section]);
|
|
454
|
+
if (!Object.hasOwn(rawSection, parsed.field))
|
|
455
|
+
return cfg;
|
|
456
|
+
oldValue = rawSection[parsed.field];
|
|
457
|
+
const existing = record[parsed.section] ?? {};
|
|
458
|
+
return withSectionOrDropped(root, cfg, parsed.section, without(existing, parsed.field));
|
|
459
|
+
}
|
|
450
460
|
if (parsed.kind === "agent") {
|
|
451
461
|
// Presence is judged against the FILE, so unsetting a field only the default (or the
|
|
452
462
|
// legacy alias's resolved view) carries is a no-op success. The entry and the `backends`
|
|
453
463
|
// section are dropped when they empty, so the file never keeps an empty object.
|
|
454
|
-
const
|
|
455
|
-
const rawEntryCandidate =
|
|
464
|
+
const rawBackends = objectOrEmpty(raw.backends);
|
|
465
|
+
const rawEntryCandidate = rawBackends[parsed.id];
|
|
456
466
|
const rawEntry = objectOrEmpty(rawEntryCandidate);
|
|
457
467
|
if (!Object.hasOwn(rawEntry, parsed.field))
|
|
458
468
|
return cfg; // absent file field: a no-op success
|
|
459
469
|
oldValue = rawEntry[parsed.field];
|
|
460
|
-
const
|
|
461
|
-
const entry = { ...(
|
|
470
|
+
const backends = { ...record.backends };
|
|
471
|
+
const entry = { ...(backends[parsed.id] ?? {}) };
|
|
462
472
|
delete entry[parsed.field];
|
|
463
473
|
if (Object.keys(entry).length === 0)
|
|
464
|
-
delete
|
|
474
|
+
delete backends[parsed.id];
|
|
465
475
|
else
|
|
466
|
-
|
|
467
|
-
return withSectionOrDropped(root, cfg, "backends",
|
|
476
|
+
backends[parsed.id] = entry;
|
|
477
|
+
return withSectionOrDropped(root, cfg, "backends", backends);
|
|
468
478
|
}
|
|
469
479
|
const rawRolesCandidate = raw.roles;
|
|
470
480
|
const rawRoles = objectOrEmpty(rawRolesCandidate);
|
|
@@ -489,114 +499,3 @@ export function unsetConfigKey(root, key) {
|
|
|
489
499
|
return result;
|
|
490
500
|
return { ok: true, oldValue };
|
|
491
501
|
}
|
|
492
|
-
/** Consume the director's config-write request file (plans/portability.md §3/7). After its pi
|
|
493
|
-
* run the director may leave `.tumwater-config-request.json` at its worktree root:
|
|
494
|
-
* `{ "customRoles": [ { "name", "task" }, … ] }` — the whole array, replacing the current one.
|
|
495
|
-
* Applied atomically to the live config with setDailyBudgetUsd's idiom (fresh loadConfig that
|
|
496
|
-
* bypasses the stat cache, validateConfig, then writeJsonAtomic — readers poll every ~2 s), so
|
|
497
|
-
* the orchestrator's live reload starts the new loop with no commit, no review gate, and no
|
|
498
|
-
* merge, and the request file never enters a diff.
|
|
499
|
-
*
|
|
500
|
-
* The permitted key set is enforced here, not in prose: `customRoles` is accepted and every
|
|
501
|
-
* other top-level key is collected into `ignored` and dropped (the caller logs the warning that
|
|
502
|
-
* names them). Validation runs BEFORE any write and nothing here dereferences a request entry
|
|
503
|
-
* first — a structurally malformed entry (`[null]`, a non-string name) is left for
|
|
504
|
-
* validateConfig to reject, so on failure nothing is written and the previous config stays
|
|
505
|
-
* live. The request file is deleted on EVERY path (including every failure), so a malformed
|
|
506
|
-
* request cannot retry forever and the file cannot survive into `git add -A`.
|
|
507
|
-
*
|
|
508
|
-
* Roles entries for custom loops the request removes are stripped before validation, from the
|
|
509
|
-
* FILE's own `roles` section rather than the catalog-merged map (the rule every other
|
|
510
|
-
* whole-config write follows, see preserveFileRoles): loadConfig seeds one `roles.<name>` per
|
|
511
|
-
* current custom loop and validateConfig rejects an id that is neither a catalog role nor a
|
|
512
|
-
* requested custom-loop name, so without the strip a removal would never apply. Only
|
|
513
|
-
* structurally valid names feed the strip — a malformed entry stays in the array for
|
|
514
|
-
* validateConfig to reject.
|
|
515
|
-
*
|
|
516
|
-
* Returns null when no request file exists. `applied` names the custom loops now live (empty
|
|
517
|
-
* unless the write happened); `ignored` lists the discarded top-level keys; `error`, when set,
|
|
518
|
-
* names why nothing (or only part of the work) was done — the caller turns it and `ignored`
|
|
519
|
-
* into warning events. */
|
|
520
|
-
export function applyConfigRequest(root, wt) {
|
|
521
|
-
const requestFile = configRequestPath(wt);
|
|
522
|
-
let raw;
|
|
523
|
-
try {
|
|
524
|
-
raw = fs.readFileSync(requestFile, "utf8");
|
|
525
|
-
}
|
|
526
|
-
catch {
|
|
527
|
-
return null; // no request this tick
|
|
528
|
-
}
|
|
529
|
-
const ignored = [];
|
|
530
|
-
let applied = [];
|
|
531
|
-
let error;
|
|
532
|
-
try {
|
|
533
|
-
const unnormalized = JSON.parse(raw);
|
|
534
|
-
if (!isJsonObject(unnormalized))
|
|
535
|
-
throw new Error(`config request must be a JSON object (got ${show(unnormalized)})`);
|
|
536
|
-
// Old key spellings read as the new ones; a request setting both fails with the pair named.
|
|
537
|
-
const normalized = normalizeConfigKeys(unnormalized);
|
|
538
|
-
if (normalized.problems.length > 0)
|
|
539
|
-
throw new Error(normalized.problems.join("; "));
|
|
540
|
-
const parsed = normalized.raw;
|
|
541
|
-
for (const key of Object.keys(parsed)) {
|
|
542
|
-
if (key !== "customRoles")
|
|
543
|
-
ignored.push(key);
|
|
544
|
-
}
|
|
545
|
-
const loops = parsed.customRoles;
|
|
546
|
-
if (!Array.isArray(loops))
|
|
547
|
-
throw new Error(`config request's customRoles must be an array (got ${show(loops)})`);
|
|
548
|
-
// The load-to-write sequence is one critical section under withConfigLock: a concurrent
|
|
549
|
-
// `config set`/dashboard write between the load and this write must not be dropped by the
|
|
550
|
-
// whole-file overwrite (see writeConfigMutation).
|
|
551
|
-
withConfigLock(root, () => {
|
|
552
|
-
const current = loadConfig(root); // fresh — bypasses the stat cache: a writer sees the latest file
|
|
553
|
-
// Strip roles.<id> entries for custom loops this request removes (see docstring).
|
|
554
|
-
// Structurally valid names only; anything else stays for validateConfig to reject — never
|
|
555
|
-
// dereference a request entry before validation.
|
|
556
|
-
const requestedNames = new Set();
|
|
557
|
-
for (const entry of loops) {
|
|
558
|
-
if (isJsonObject(entry) && typeof entry.name === "string")
|
|
559
|
-
requestedNames.add(entry.name);
|
|
560
|
-
}
|
|
561
|
-
// Base the roles rewrite on the FILE's own section, not the loaded (catalog-merged)
|
|
562
|
-
// map: this whole-config write must not re-expand a partial section to every built-in
|
|
563
|
-
// (preserveFileRoles' rule). Only the removed custom loops' entries are stripped, and
|
|
564
|
-
// only from what the file actually holds.
|
|
565
|
-
const rawRolesCandidate = rawConfigObject(root).roles;
|
|
566
|
-
const roles = isJsonObject(rawRolesCandidate)
|
|
567
|
-
? { ...(rawRolesCandidate) }
|
|
568
|
-
: {};
|
|
569
|
-
for (const c of current.customRoles) {
|
|
570
|
-
if (!requestedNames.has(c.name))
|
|
571
|
-
delete roles[c.name];
|
|
572
|
-
}
|
|
573
|
-
const candidate = { ...current, customRoles: loops };
|
|
574
|
-
const candidateRecord = candidate;
|
|
575
|
-
if (rawRolesCandidate === undefined)
|
|
576
|
-
delete candidateRecord.roles;
|
|
577
|
-
else
|
|
578
|
-
candidateRecord.roles = roles;
|
|
579
|
-
// Collapse legacy/new pi spellings before writing so a merged config (which always
|
|
580
|
-
// carries the default piArgs) never lands both forms in the file — the next load's raw
|
|
581
|
-
// pass rejects a file holding both `piArgs` and `backends.pi.args`.
|
|
582
|
-
const collapsed = collapseAgentAliases(candidate);
|
|
583
|
-
validateConfig(collapsed); // throws listing every problem — nothing is written on failure
|
|
584
|
-
writeJsonAtomic(configPath(root), collapsed, true);
|
|
585
|
-
applied = collapsed.customRoles.map((c) => c.name);
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
catch (err) {
|
|
589
|
-
applied = []; // no write happened, or it must not be reported as applied
|
|
590
|
-
error = errorMessage(err);
|
|
591
|
-
}
|
|
592
|
-
// Delete the request on EVERY path — success, rejection, malformed JSON — so it can never be
|
|
593
|
-
// staged by commitAll, reach a review gate, or retry forever. A failed unlink is surfaced as
|
|
594
|
-
// an error so the caller logs it (the file would otherwise survive into the diff).
|
|
595
|
-
try {
|
|
596
|
-
fs.unlinkSync(requestFile);
|
|
597
|
-
}
|
|
598
|
-
catch (err) {
|
|
599
|
-
error ??= `config request file could not be deleted: ${errorMessage(err)}`;
|
|
600
|
-
}
|
|
601
|
-
return { applied, ignored, error };
|
|
602
|
-
}
|