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,106 +1,44 @@
|
|
|
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
|
}
|
|
105
43
|
/** Set ONE top-level key of tumwater.json from its raw CLI text (`tumwater config set`'s
|
|
106
44
|
* engine): the key must be a member of TOP_LEVEL_KEYS — the same protection checkKnownKeys
|
|
@@ -116,12 +54,15 @@ export function setDailyBudgetUsd(root, value) {
|
|
|
116
54
|
* ROLE_ENTRY_KEYS (a did-you-mean suggestion otherwise, via text/suggest.ts's typoSuffix); a
|
|
117
55
|
* typo'd role id and a type-invalid value are left for validateConfig to reject with its own
|
|
118
56
|
* 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
|
|
57
|
+
* confirm the change; on any failure the file is untouched
|
|
58
|
+
* (see writeConfigMutation). A value equal to the resolved one is a no-op: nothing is written and
|
|
59
|
+
* the result carries `unchanged`. */
|
|
120
60
|
export function setConfigKey(root, key, rawValue) {
|
|
121
61
|
key = canonicalConfigKey(key);
|
|
122
62
|
const parsed = parseConfigKey(key);
|
|
123
63
|
if (parsed.kind === "error")
|
|
124
64
|
return fail(parsed.error);
|
|
65
|
+
let unchanged; // set by a mutation callback when value equals the resolved one
|
|
125
66
|
let value;
|
|
126
67
|
try {
|
|
127
68
|
value = JSON.parse(rawValue);
|
|
@@ -155,6 +96,10 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
155
96
|
if (parsed.kind === "map") {
|
|
156
97
|
const existing = record[parsed.map] ?? {};
|
|
157
98
|
oldValue = existing[parsed.role];
|
|
99
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
100
|
+
unchanged = true;
|
|
101
|
+
return cfg;
|
|
102
|
+
}
|
|
158
103
|
const next = { ...cfg, [parsed.map]: { ...existing, [parsed.role]: value } };
|
|
159
104
|
return preservingFileRoles(root, next);
|
|
160
105
|
}
|
|
@@ -166,19 +111,39 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
166
111
|
// there and nothing is written.
|
|
167
112
|
const existing = modelTierMap(record.model);
|
|
168
113
|
oldValue = existing[parsed.tier];
|
|
114
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
115
|
+
unchanged = true;
|
|
116
|
+
return cfg;
|
|
117
|
+
}
|
|
169
118
|
const next = { ...cfg, model: { ...existing, [parsed.tier]: value } };
|
|
170
119
|
return preservingFileRoles(root, next);
|
|
171
120
|
}
|
|
121
|
+
if (parsed.kind === "section") {
|
|
122
|
+
// Merge one field into the loaded section, keeping its other fields; validateConfig in
|
|
123
|
+
// writeConfigMutation owns the value type, so a bad value fails there and writes nothing.
|
|
124
|
+
const existing = record[parsed.section] ?? {};
|
|
125
|
+
oldValue = existing[parsed.field];
|
|
126
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
127
|
+
unchanged = true;
|
|
128
|
+
return cfg;
|
|
129
|
+
}
|
|
130
|
+
const next = { ...cfg, [parsed.section]: { ...existing, [parsed.field]: value } };
|
|
131
|
+
return preservingFileRoles(root, next);
|
|
132
|
+
}
|
|
172
133
|
if (parsed.kind === "agent") {
|
|
173
134
|
// Merge one field of one backend's entry, preserving the other field and the other
|
|
174
135
|
// backends. validateConfig in writeConfigMutation owns the value type, so a bad
|
|
175
136
|
// value fails there and nothing is written.
|
|
176
|
-
const
|
|
177
|
-
const entry = { ...(
|
|
137
|
+
const backends = { ...record.backends };
|
|
138
|
+
const entry = { ...(backends[parsed.id] ?? {}) };
|
|
178
139
|
oldValue = entry[parsed.field];
|
|
140
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
141
|
+
unchanged = true;
|
|
142
|
+
return cfg;
|
|
143
|
+
}
|
|
179
144
|
entry[parsed.field] = value;
|
|
180
|
-
|
|
181
|
-
const next = { ...cfg, backends:
|
|
145
|
+
backends[parsed.id] = entry;
|
|
146
|
+
const next = { ...cfg, backends: backends };
|
|
182
147
|
// Writers keep one spelling: the new pi form drops its legacy alias so the file can
|
|
183
148
|
// never hold both (validateConfig's raw pass rejects a file that does).
|
|
184
149
|
if (parsed.id === "pi") {
|
|
@@ -188,6 +153,12 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
188
153
|
else
|
|
189
154
|
delete nextRecord.piArgs;
|
|
190
155
|
}
|
|
156
|
+
// Setting the nested claude reserve drops the top-level spelling, so the file can never
|
|
157
|
+
// hold both (validateConfig rejects a file that does, mirroring the agentBin/piArgs pair).
|
|
158
|
+
if (parsed.id === "claude" && parsed.field === "windowReserve") {
|
|
159
|
+
const nextRecord = next;
|
|
160
|
+
delete nextRecord.claudeWindowReserve;
|
|
161
|
+
}
|
|
191
162
|
return preservingFileRoles(root, next);
|
|
192
163
|
}
|
|
193
164
|
// Base the merge on the FILE's own roles, not the loaded (catalog-merged) map: an
|
|
@@ -199,6 +170,10 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
199
170
|
const rawEntryCandidate = rawRoles[parsed.id];
|
|
200
171
|
const rawEntry = objectOrEmpty(rawEntryCandidate);
|
|
201
172
|
oldValue = record.roles?.[parsed.id]?.[parsed.field];
|
|
173
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
174
|
+
unchanged = true;
|
|
175
|
+
return cfg;
|
|
176
|
+
}
|
|
202
177
|
const roles = {
|
|
203
178
|
...rawRoles,
|
|
204
179
|
[parsed.id]: { ...rawEntry, [parsed.field]: value },
|
|
@@ -210,6 +185,18 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
210
185
|
});
|
|
211
186
|
if (!result.ok)
|
|
212
187
|
return result;
|
|
188
|
+
if (unchanged)
|
|
189
|
+
return { ok: true, value, oldValue, unchanged: true };
|
|
190
|
+
// A string replaces a whole object tier entry, so its `backend` goes with it.
|
|
191
|
+
if (parsed.kind === "tier" && isJsonObject(oldValue)) {
|
|
192
|
+
const dropped = typeof oldValue.backend === "string" ? `backend ${show(oldValue.backend)}` : "its other keys";
|
|
193
|
+
return {
|
|
194
|
+
ok: true,
|
|
195
|
+
value,
|
|
196
|
+
oldValue,
|
|
197
|
+
note: `model.${parsed.tier} was an object entry — the string replaces it, dropping ${dropped}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
213
200
|
return { ok: true, value, oldValue };
|
|
214
201
|
}
|
|
215
202
|
const unknown = unknownConfigKeyError(key);
|
|
@@ -240,6 +227,10 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
240
227
|
let oldValue;
|
|
241
228
|
const result = writeConfigMutation(root, (cfg) => {
|
|
242
229
|
oldValue = cfg[key];
|
|
230
|
+
if (isDeepStrictEqual(oldValue, value)) {
|
|
231
|
+
unchanged = true;
|
|
232
|
+
return cfg;
|
|
233
|
+
}
|
|
243
234
|
const next = { ...cfg, [key]: value };
|
|
244
235
|
// `roles` merges per-entry at load (overlayDefaults seeds the catalog and lets the file
|
|
245
236
|
// override one entry), so an unrelated whole-config write must not re-expand the file's
|
|
@@ -251,27 +242,43 @@ export function setConfigKey(root, key, rawValue) {
|
|
|
251
242
|
// legacy aliases the new pi form now carries, so the file can never hold both.
|
|
252
243
|
// Writers keep one spelling: a whole-section `backends` set (the GUI Save path) drops the
|
|
253
244
|
// legacy agentBin/piArgs aliases the new pi form now carries, so the file can never hold
|
|
254
|
-
// both — the same
|
|
245
|
+
// both — the same collapseBackendAliases rule the whole-config writers (above and in
|
|
255
246
|
// config.ts) use, so the alias policy cannot drift between the per-key arm and the others.
|
|
256
247
|
if (key === "backends")
|
|
257
|
-
return
|
|
248
|
+
return collapseBackendAliases(next);
|
|
258
249
|
// Writers keep one spelling: a legacy top-level set drops the matching backends.pi field, so
|
|
259
250
|
// the file can never hold both (validateConfig's raw pass rejects a file that does).
|
|
260
251
|
if (key === "agentBin" || key === "piArgs") {
|
|
261
252
|
const record = next;
|
|
262
|
-
const
|
|
263
|
-
const entry = { ...(
|
|
253
|
+
const backends = { ...(record.backends ?? {}) };
|
|
254
|
+
const entry = { ...(backends.pi ?? {}) };
|
|
264
255
|
delete entry[key === "agentBin" ? "bin" : "args"];
|
|
265
256
|
if (Object.keys(entry).length === 0)
|
|
266
|
-
delete
|
|
257
|
+
delete backends.pi;
|
|
267
258
|
else
|
|
268
|
-
|
|
269
|
-
return { ...next, backends:
|
|
259
|
+
backends.pi = entry;
|
|
260
|
+
return { ...next, backends: backends };
|
|
261
|
+
}
|
|
262
|
+
// Writers keep one spelling: setting the top-level `claudeWindowReserve` drops the nested
|
|
263
|
+
// `backends.claude.windowReserve`, so a supported `config set` never writes the rejected pair
|
|
264
|
+
// (validateConfig still refuses a hand-edited file holding both).
|
|
265
|
+
if (key === "claudeWindowReserve") {
|
|
266
|
+
const record = next;
|
|
267
|
+
const backends = { ...(record.backends ?? {}) };
|
|
268
|
+
const entry = { ...(backends.claude ?? {}) };
|
|
269
|
+
delete entry.windowReserve;
|
|
270
|
+
if (Object.keys(entry).length === 0)
|
|
271
|
+
delete backends.claude;
|
|
272
|
+
else
|
|
273
|
+
backends.claude = entry;
|
|
274
|
+
return { ...next, backends: backends };
|
|
270
275
|
}
|
|
271
276
|
return next;
|
|
272
277
|
});
|
|
273
278
|
if (!result.ok)
|
|
274
279
|
return result;
|
|
280
|
+
if (unchanged)
|
|
281
|
+
return { ok: true, value, oldValue, unchanged: true };
|
|
275
282
|
return { ok: true, value, oldValue };
|
|
276
283
|
}
|
|
277
284
|
/** Set a role's provider/model/thinking override triple in tumwater.json (`roles.<base>`),
|
|
@@ -297,7 +304,8 @@ export function setRoleModelFields(root, base, fields) {
|
|
|
297
304
|
// a bogus id "updated".
|
|
298
305
|
if (!knownRoleIdsCached(root).includes(base))
|
|
299
306
|
return fail(unknownRoleMessage(base, knownRoleIdsCached(root)));
|
|
300
|
-
|
|
307
|
+
let unchanged; // set when no field differs from the file
|
|
308
|
+
const result = writeConfigMutation(root, (cfg) => {
|
|
301
309
|
const rawRolesCandidate = rawConfigObject(root).roles;
|
|
302
310
|
const rawRoles = objectOrEmpty(rawRolesCandidate);
|
|
303
311
|
const rawEntryCandidate = rawRoles[base];
|
|
@@ -318,8 +326,10 @@ export function setRoleModelFields(root, base, fields) {
|
|
|
318
326
|
changed = true;
|
|
319
327
|
}
|
|
320
328
|
}
|
|
321
|
-
if (!changed)
|
|
329
|
+
if (!changed) {
|
|
330
|
+
unchanged = true;
|
|
322
331
|
return cfg; // writeConfigMutation skips the write — file bytes identical
|
|
332
|
+
}
|
|
323
333
|
const nextRoles = { ...rawRoles };
|
|
324
334
|
// Clearing the entry's last override empties it — never leave an empty `roles.<base>: {}`
|
|
325
335
|
// (the file-wide no-empty-object invariant unsetConfigKey's `without` keeps); an emptied
|
|
@@ -333,51 +343,9 @@ export function setRoleModelFields(root, base, fields) {
|
|
|
333
343
|
return without(cfg, "roles");
|
|
334
344
|
return { ...cfg, roles: nextRoles };
|
|
335
345
|
});
|
|
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);
|
|
346
|
+
if (result.ok && unchanged)
|
|
347
|
+
return { ok: true, unchanged: true };
|
|
348
|
+
return result;
|
|
381
349
|
}
|
|
382
350
|
/** Remove ONE value of tumwater.json from its raw CLI text (`tumwater config unset <key>`):
|
|
383
351
|
* the inverse of setConfigKey. A bare top-level key is deleted (reverting to its default or
|
|
@@ -447,24 +415,34 @@ export function unsetConfigKey(root, key) {
|
|
|
447
415
|
const next = without(existing, parsed.tier);
|
|
448
416
|
return withSectionOrDropped(root, cfg, "model", next);
|
|
449
417
|
}
|
|
418
|
+
if (parsed.kind === "section") {
|
|
419
|
+
// Presence is judged against the FILE: a field only the default carries is a no-op
|
|
420
|
+
// success, and an emptied section leaves the file.
|
|
421
|
+
const rawSection = objectOrEmpty(raw[parsed.section]);
|
|
422
|
+
if (!Object.hasOwn(rawSection, parsed.field))
|
|
423
|
+
return cfg;
|
|
424
|
+
oldValue = rawSection[parsed.field];
|
|
425
|
+
const existing = record[parsed.section] ?? {};
|
|
426
|
+
return withSectionOrDropped(root, cfg, parsed.section, without(existing, parsed.field));
|
|
427
|
+
}
|
|
450
428
|
if (parsed.kind === "agent") {
|
|
451
429
|
// Presence is judged against the FILE, so unsetting a field only the default (or the
|
|
452
430
|
// legacy alias's resolved view) carries is a no-op success. The entry and the `backends`
|
|
453
431
|
// section are dropped when they empty, so the file never keeps an empty object.
|
|
454
|
-
const
|
|
455
|
-
const rawEntryCandidate =
|
|
432
|
+
const rawBackends = objectOrEmpty(raw.backends);
|
|
433
|
+
const rawEntryCandidate = rawBackends[parsed.id];
|
|
456
434
|
const rawEntry = objectOrEmpty(rawEntryCandidate);
|
|
457
435
|
if (!Object.hasOwn(rawEntry, parsed.field))
|
|
458
436
|
return cfg; // absent file field: a no-op success
|
|
459
437
|
oldValue = rawEntry[parsed.field];
|
|
460
|
-
const
|
|
461
|
-
const entry = { ...(
|
|
438
|
+
const backends = { ...record.backends };
|
|
439
|
+
const entry = { ...(backends[parsed.id] ?? {}) };
|
|
462
440
|
delete entry[parsed.field];
|
|
463
441
|
if (Object.keys(entry).length === 0)
|
|
464
|
-
delete
|
|
442
|
+
delete backends[parsed.id];
|
|
465
443
|
else
|
|
466
|
-
|
|
467
|
-
return withSectionOrDropped(root, cfg, "backends",
|
|
444
|
+
backends[parsed.id] = entry;
|
|
445
|
+
return withSectionOrDropped(root, cfg, "backends", backends);
|
|
468
446
|
}
|
|
469
447
|
const rawRolesCandidate = raw.roles;
|
|
470
448
|
const rawRoles = objectOrEmpty(rawRolesCandidate);
|
|
@@ -489,114 +467,3 @@ export function unsetConfigKey(root, key) {
|
|
|
489
467
|
return result;
|
|
490
468
|
return { ok: true, oldValue };
|
|
491
469
|
}
|
|
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
|
-
}
|