tumwater 0.1.3 → 0.2.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 +63 -28
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-eligibility.js +332 -43
- package/dist/src/backlog/backlog-md.js +79 -17
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +178 -441
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +32 -30
- package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
- package/dist/src/{brief.js → brief/brief.js} +33 -8
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +13 -11
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +9 -7
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +43 -10
- package/dist/src/cli/cli-editor.js +13 -1
- package/dist/src/cli/cli-flag-specs.js +31 -6
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-query-commands.js +439 -56
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +124 -68
- package/dist/src/cli/completion.js +70 -12
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +264 -124
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +13 -224
- package/dist/src/cli.js +78 -36
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +107 -325
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +494 -68
- package/dist/src/doctor/doctor-checks.js +509 -58
- package/dist/src/doctor/doctor-holds.js +139 -78
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +2 -3
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +76 -25
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +219 -106
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +80 -30
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +6 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +79 -59
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -716
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +105 -54
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +25 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +179 -104
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +29 -70
- package/dist/src/inbox/inbox-attachments.js +66 -10
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +114 -73
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +104 -50
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +130 -13
- package/dist/src/loop/loop-tick.js +190 -88
- package/dist/src/loop/loop.js +68 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +113 -45
- package/dist/src/operator/operator-intent.js +74 -53
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +47 -28
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
- package/dist/src/orchestrator/orchestrator.js +149 -96
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +124 -38
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +4 -3
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +36 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +29 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +259 -254
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +235 -44
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +4 -3
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +55 -34
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +109 -131
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +16 -14
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +15 -8
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +28 -25
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +72 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +50 -39
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +142 -25
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +23 -22
- package/dist/src/tick/tick-detail.js +37 -24
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +67 -53
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +75 -10
- package/dist/src/ui/badges.js +113 -69
- package/dist/src/ui/gui/gui-client-boot.js +202 -29
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +94 -37
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +90 -47
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +60 -43
- package/dist/src/ui/gui/gui-client-operator.js +73 -43
- package/dist/src/ui/gui/gui-client-pending.js +34 -31
- package/dist/src/ui/gui/gui-client-report.js +330 -47
- package/dist/src/ui/gui/gui-client-settings.js +126 -18
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +18 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +176 -102
- package/dist/src/ui/status-payload.js +114 -36
- package/dist/src/ui/status-render.js +121 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +9 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +61 -57
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +44 -50
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +87 -1821
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +52 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +44 -26
- package/dist/src/verdict/fix-claim.js +56 -33
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +43 -11
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -56
- package/dist/src/landing/landing-slot.js +0 -256
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dashboard's POST config/budget command handlers (barrelled into
|
|
3
|
+
* gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts):
|
|
4
|
+
* config-set, config-unset, budget, role-cap, role-enable, role-disable, and
|
|
5
|
+
* notify-test — the
|
|
6
|
+
* post-handler layer onto
|
|
7
|
+
* the config-write domain module. Each handler adapts HTTP and touches no socket beyond its
|
|
8
|
+
* own `res`.
|
|
9
|
+
*/
|
|
10
|
+
import { setConfigKey, setDailyBudgetUsd, setRoleModelFields, unsetConfigKey } from "../config/config-write.js";
|
|
11
|
+
import { checkDailyBudgetUsd, checkMinRunInterval } from "../config/config-keys.js";
|
|
12
|
+
import { testNotifyHook } from "../cli/notify-command.js";
|
|
13
|
+
import { rejectBadRole, requireConfigKey } from "./gui-args.js";
|
|
14
|
+
import { baseRoleOf } from "../roles/loop-ids.js";
|
|
15
|
+
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
16
|
+
import { badRequest, sendJson } from "./http-body.js";
|
|
17
|
+
import { gotSuffix } from "../text/text.js";
|
|
18
|
+
import { readPostBody } from "./gui-endpoint-common.js";
|
|
19
|
+
/** Handle POST /api/budget: the dashboard's budget-badge editor saves the daily cost cap —
|
|
20
|
+
* same body discipline as /api/prompt (readJsonObject), and the same shared validation rule +
|
|
21
|
+
* atomic setter the TUI's Ctrl+B uses, so both surfaces write tumwater.json identically and
|
|
22
|
+
* the running orchestrator picks the change up on its next ~2 s poll. */
|
|
23
|
+
export async function handleBudget(req, res, root) {
|
|
24
|
+
const body = await readPostBody(req, res, '{"maxDailyCostUsd": 25}');
|
|
25
|
+
if (!body)
|
|
26
|
+
return;
|
|
27
|
+
const value = body.maxDailyCostUsd;
|
|
28
|
+
if (value === undefined) {
|
|
29
|
+
badRequest(res, "maxDailyCostUsd required");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// The shared rule (finite ≥ 0; 0 disables): missing/non-finite/negative → 400 with
|
|
33
|
+
// the offending value named.
|
|
34
|
+
const problem = checkDailyBudgetUsd(value);
|
|
35
|
+
if (problem) {
|
|
36
|
+
badRequest(res, problem);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const result = setDailyBudgetUsd(root, value);
|
|
40
|
+
if (!result.ok) {
|
|
41
|
+
// The value was valid — this is a server-side failure (broken tumwater.json or
|
|
42
|
+
// disk), not the client's fault.
|
|
43
|
+
sendJson(res, 500, { error: result.error });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
sendJson(res, 200, { ok: true, maxDailyCostUsd: value });
|
|
47
|
+
}
|
|
48
|
+
/** Handle POST /api/role-cap: the dashboard role drawer's editor saves one role's daily cost
|
|
49
|
+
* cap — the control the amber role-cap alert remedies. Same body discipline as /api/budget
|
|
50
|
+
* (readPostBody → 400 malformed/non-object, 413 oversized); the value rides the team editor's
|
|
51
|
+
* shared checkDailyBudgetUsd screen (naming the per-role dotted key), then the write goes
|
|
52
|
+
* through the CLI's own setConfigKey dotted path so the per-key validator and whole-candidate
|
|
53
|
+
* validateConfig (which rejects an unknown role id) own the final truth. The director has no
|
|
54
|
+
* cap (role-view.ts's guard exempts it), so it is refused up front with wording that mirrors
|
|
55
|
+
* handleRoleNotes. A bare `feature-2` loop id maps to its base role's cap (baseRoleOf), since
|
|
56
|
+
* the cap is about the role. A write failure is configKeyError's 400 exactly as handleConfigSet
|
|
57
|
+
* reports it. */
|
|
58
|
+
export async function handleRoleCap(req, res, root) {
|
|
59
|
+
const body = await readPostBody(req, res, '{"role": "feature", "maxDailyCostUsd": 2}');
|
|
60
|
+
if (!body)
|
|
61
|
+
return;
|
|
62
|
+
const role = body.role;
|
|
63
|
+
if (typeof role !== "string" || role.trim() === "") {
|
|
64
|
+
badRequest(res, "role required");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (role === DIRECTOR_ROLE) {
|
|
68
|
+
badRequest(res, "the director has no daily cap");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const value = body.maxDailyCostUsd;
|
|
72
|
+
if (value === undefined) {
|
|
73
|
+
badRequest(res, "maxDailyCostUsd required");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const baseRole = baseRoleOf(role);
|
|
77
|
+
// The shared rule (finite ≥ 0; 0 disables), naming the per-role dotted key so a rejected
|
|
78
|
+
// write tells the operator which key the drawer would set.
|
|
79
|
+
const problem = checkDailyBudgetUsd(value, "maxDailyCostUsdPerRole." + baseRole);
|
|
80
|
+
if (problem) {
|
|
81
|
+
badRequest(res, problem);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// setConfigKey takes the raw CLI text; a JSON round-trip of an already-parsed value is
|
|
85
|
+
// exact, so the shared per-key validator + whole-config validateConfig apply unchanged.
|
|
86
|
+
const result = setConfigKey(root, "maxDailyCostUsdPerRole." + baseRole, JSON.stringify(value));
|
|
87
|
+
if (!result.ok)
|
|
88
|
+
return configKeyError(res, "maxDailyCostUsdPerRole." + baseRole, result);
|
|
89
|
+
sendJson(res, 200, { ok: true, role: baseRole, maxDailyCostUsd: value });
|
|
90
|
+
}
|
|
91
|
+
/** Handle POST /api/role-interval: the role drawer's min run interval editor sets or clears
|
|
92
|
+
* one role's override of the team-wide minRunIntervalSeconds — the knob that makes one role
|
|
93
|
+
* act rarely, distinct from the fleet-wide value. Same body discipline as /api/role-cap
|
|
94
|
+
* (readPostBody → 400 malformed/non-object, 413 oversized); a null, "" or whitespace-only
|
|
95
|
+
* value clears the key through unsetConfigKey (so the role falls back to the team-wide
|
|
96
|
+
* interval), otherwise the write rides the CLI's own setConfigKey dotted path so
|
|
97
|
+
* checkMinRunInterval and whole-candidate validateConfig (which rejects an unknown role id)
|
|
98
|
+
* own the final truth. The director is accepted here — it ticks on the same clock, unlike
|
|
99
|
+
* the cap's director exemption. A bare `feature-2` loop id maps to its base role's override
|
|
100
|
+
* (baseRoleOf), since the interval is about the role. A write failure is configKeyError's 400
|
|
101
|
+
* exactly as handleRoleCap reports it. */
|
|
102
|
+
export async function handleRoleInterval(req, res, root) {
|
|
103
|
+
const body = await readPostBody(req, res, '{"role": "feature", "minRunIntervalSeconds": 300}');
|
|
104
|
+
if (!body)
|
|
105
|
+
return;
|
|
106
|
+
const role = body.role;
|
|
107
|
+
if (typeof role !== "string" || role.trim() === "") {
|
|
108
|
+
badRequest(res, "role required");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const value = body.minRunIntervalSeconds;
|
|
112
|
+
if (value === undefined) {
|
|
113
|
+
badRequest(res, "minRunIntervalSeconds required");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const base = baseRoleOf(role);
|
|
117
|
+
// A null, empty or whitespace-only value clears the override, so the role falls back to the
|
|
118
|
+
// team-wide minRunIntervalSeconds (unsetConfigKey, like the CLI's config unset of the key).
|
|
119
|
+
if (value === null || (typeof value === "string" && value.trim() === "")) {
|
|
120
|
+
// unsetConfigKey's no-op map path (a role the file never holds, an unknown id included)
|
|
121
|
+
// skips validateConfig, so without this the set branch's unknown-role refusal would never
|
|
122
|
+
// fire on a clear and a bogus id would answer 200 "cleared" — the explicit check mirrors
|
|
123
|
+
// the CLI's knownRoleIds guard and the role-quiet-hours clear branch.
|
|
124
|
+
if (rejectBadRole(root, res, role))
|
|
125
|
+
return;
|
|
126
|
+
const clearResult = unsetConfigKey(root, `roles.${base}.minRunIntervalSeconds`);
|
|
127
|
+
if (!clearResult.ok)
|
|
128
|
+
return configKeyError(res, `roles.${base}.minRunIntervalSeconds`, clearResult);
|
|
129
|
+
sendJson(res, 200, { ok: true, role: base, message: `${base}: min run interval cleared` });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// The shared rule (finite 0..MAX_DURATION_SECONDS), naming the per-role dotted key so a
|
|
133
|
+
// rejected write tells the operator which key the drawer would set.
|
|
134
|
+
const problem = checkMinRunInterval(value, `roles.${base}.minRunIntervalSeconds`);
|
|
135
|
+
if (problem) {
|
|
136
|
+
badRequest(res, problem);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// setConfigKey takes the raw CLI text; a JSON round-trip of an already-parsed value is
|
|
140
|
+
// exact, so the shared per-key validator + whole-config validateConfig apply unchanged.
|
|
141
|
+
const result = setConfigKey(root, `roles.${base}.minRunIntervalSeconds`, JSON.stringify(value));
|
|
142
|
+
if (!result.ok)
|
|
143
|
+
return configKeyError(res, `roles.${base}.minRunIntervalSeconds`, result);
|
|
144
|
+
sendJson(res, 200, { ok: true, role: base, minRunIntervalSeconds: value, message: `${base}: min run interval set to ${value}s` });
|
|
145
|
+
}
|
|
146
|
+
/** Handle POST /api/role-instances: the role drawer's instance-count editor sets or clears one
|
|
147
|
+
* role's parallel-runner count — the count that lets `feature-2`…`feature-N` tick. Same body
|
|
148
|
+
* discipline as /api/role-interval (readPostBody → 400 malformed/non-object, 413 oversized); a
|
|
149
|
+
* null, "" or whitespace-only value clears the key through unsetConfigKey (so the role falls
|
|
150
|
+
* back to the default 1), otherwise the write rides the CLI's own setConfigKey dotted path so
|
|
151
|
+
* whole-candidate validateConfig owns the final truth. There is no own range or role check here
|
|
152
|
+
* (unlike handleRoleCap's director exemption): setConfigKey runs the candidate through
|
|
153
|
+
* validateConfig, whose INSTANCE_COUNT rejects 0, 9, 1.5 and `abc` and whose INSTANCE_ROLES
|
|
154
|
+
* branch rejects an instances key under any role but feature and bugfix (the director and
|
|
155
|
+
* custom roles included). A bare `feature-2` loop id maps to its base role's count (baseRoleOf),
|
|
156
|
+
* since the count is about the role. A write failure is configKeyError's 400 exactly as
|
|
157
|
+
* handleRoleInterval reports it. */
|
|
158
|
+
export async function handleRoleInstances(req, res, root) {
|
|
159
|
+
const body = await readPostBody(req, res, '{"role": "feature", "instances": 3}');
|
|
160
|
+
if (!body)
|
|
161
|
+
return;
|
|
162
|
+
const role = body.role;
|
|
163
|
+
if (typeof role !== "string" || role.trim() === "") {
|
|
164
|
+
badRequest(res, "role required");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const value = body.instances;
|
|
168
|
+
if (value === undefined) {
|
|
169
|
+
badRequest(res, "instances required");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const base = baseRoleOf(role);
|
|
173
|
+
// A null, empty or whitespace-only value clears the key, so the role falls back to the
|
|
174
|
+
// default 1 (unsetConfigKey, like the CLI's config unset of the key).
|
|
175
|
+
if (value === null || (typeof value === "string" && value.trim() === "")) {
|
|
176
|
+
// unsetConfigKey's no-op map path (a role the file never holds, an unknown id included)
|
|
177
|
+
// skips validateConfig, so without this the set branch's unknown-role refusal would never
|
|
178
|
+
// fire on a clear and a bogus id would answer 200 "cleared" — the explicit check mirrors
|
|
179
|
+
// the CLI's knownRoleIds guard and the role-interval clear branch.
|
|
180
|
+
if (rejectBadRole(root, res, role))
|
|
181
|
+
return;
|
|
182
|
+
const clearResult = unsetConfigKey(root, `roles.${base}.instances`);
|
|
183
|
+
if (!clearResult.ok)
|
|
184
|
+
return configKeyError(res, `roles.${base}.instances`, clearResult);
|
|
185
|
+
sendJson(res, 200, { ok: true, role: base, message: `${base}: instances cleared` });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// setConfigKey takes the raw CLI text; a JSON round-trip of an already-parsed value is
|
|
189
|
+
// exact, so the shared per-key validator + whole-config validateConfig (which owns which
|
|
190
|
+
// roles may split and the 1–8 integer bound) apply unchanged.
|
|
191
|
+
const result = setConfigKey(root, `roles.${base}.instances`, JSON.stringify(value));
|
|
192
|
+
if (!result.ok)
|
|
193
|
+
return configKeyError(res, `roles.${base}.instances`, result);
|
|
194
|
+
sendJson(res, 200, { ok: true, role: base, instances: value, message: `${base}: instances set to ${value}` });
|
|
195
|
+
}
|
|
196
|
+
/** Handle POST /api/role-model: the role drawer's Model overrides editor sets or clears one
|
|
197
|
+
* role's provider/model/thinking triple (roles.<base>.provider/.model/.thinking) in one
|
|
198
|
+
* atomic write, prefilled from the payload's raw overrides — the per-role model write the
|
|
199
|
+
* `Model` row it still shows read-only is missing (the CLI's --provider/--model/--thinking
|
|
200
|
+
* already write it). Same body discipline as /api/role-interval (readPostBody → 400
|
|
201
|
+
* malformed/non-object, 413 oversized); a missing/blank role is a 400 `role required`, each
|
|
202
|
+
* of the three must be a string when present (400 naming the field otherwise), and at least
|
|
203
|
+
* one must be present (400 `provider, model or thinking required`). A bare `feature-2` loop
|
|
204
|
+
* id maps to its base role's keys (baseRoleOf). The director is accepted — configForRole
|
|
205
|
+
* resolves roles.director.* like any role's, matching the CLI's accepted triple for it.
|
|
206
|
+
* Values are trimmed; `""` clears that field (never stored — checkStringField rejects an
|
|
207
|
+
* empty string). The write rides setRoleModelFields, whose validateConfig names
|
|
208
|
+
* roles.<base>.<field> on a bad value and refuses an unknown role, reported as a 400 exactly
|
|
209
|
+
* as handleRoleInterval reports a write failure. */
|
|
210
|
+
export async function handleRoleModel(req, res, root) {
|
|
211
|
+
const body = await readPostBody(req, res, '{"role": "feature", "provider": "claude", "model": "claude-opus-5", "thinking": "high"}');
|
|
212
|
+
if (!body)
|
|
213
|
+
return;
|
|
214
|
+
const role = body.role;
|
|
215
|
+
if (typeof role !== "string" || role.trim() === "") {
|
|
216
|
+
badRequest(res, "role required");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const fields = {};
|
|
220
|
+
for (const key of ["provider", "model", "thinking"]) {
|
|
221
|
+
const value = body[key];
|
|
222
|
+
if (value === undefined)
|
|
223
|
+
continue;
|
|
224
|
+
if (typeof value !== "string") {
|
|
225
|
+
badRequest(res, `"${key}" must be a string — a model override, or "" to clear it${gotSuffix(value)}`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
fields[key] = value.trim();
|
|
229
|
+
}
|
|
230
|
+
if (!("provider" in fields) && !("model" in fields) && !("thinking" in fields)) {
|
|
231
|
+
badRequest(res, "provider, model or thinking required");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const base = baseRoleOf(role);
|
|
235
|
+
const result = setRoleModelFields(root, base, fields);
|
|
236
|
+
if (!result.ok)
|
|
237
|
+
return badRequest(res, result.error);
|
|
238
|
+
sendJson(res, 200, { ok: true, role: base, message: `${base}: model updated` });
|
|
239
|
+
}
|
|
240
|
+
/** Handle POST /api/role-instructions: the role drawer's standing-instructions editor — the
|
|
241
|
+
* `Instructions override` row's Edit/Save. Same body discipline as /api/role-cap (readPostBody
|
|
242
|
+
* → 400 malformed/non-object, 413 oversized); an empty role id is a 400, and a bare `feature-2`
|
|
243
|
+
* loop id maps to its base role's override (baseRoleOf), the key tick-prompt reads — so an
|
|
244
|
+
* instance edits the instructions its runs carry (readRoleBody's shared guard is not used
|
|
245
|
+
* because it rejects instance ids, and the unknown-role check is validateConfig's, below).
|
|
246
|
+
* The director is editable here (its override reaches its runs), unlike handleRoleNotes'
|
|
247
|
+
* director refusal. An empty/whitespace-only text clears the key (unsetConfigKey, like the
|
|
248
|
+
* CLI's --clear-instructions); otherwise the write goes through the CLI's own setConfigKey
|
|
249
|
+
* with the same JSON.stringify wrapper --instructions-file uses, so validateConfig owns the
|
|
250
|
+
* 4096-char cap and the unknown-role check, and a write failure is configKeyError's 400
|
|
251
|
+
* naming the dotted key. */
|
|
252
|
+
export async function handleRoleInstructions(req, res, root) {
|
|
253
|
+
const body = await readPostBody(req, res, '{"role": "feature", "text": "…"}');
|
|
254
|
+
if (!body)
|
|
255
|
+
return;
|
|
256
|
+
const role = body.role;
|
|
257
|
+
if (typeof role !== "string" || role.trim() === "")
|
|
258
|
+
return badRequest(res, "role required");
|
|
259
|
+
if (typeof body.text !== "string")
|
|
260
|
+
return badRequest(res, `"text" must be a string — the full replacement instructions, or "" to clear it${gotSuffix(body.text)}`);
|
|
261
|
+
const base = baseRoleOf(role);
|
|
262
|
+
// An empty or whitespace-only buffer clears the override, like the CLI's --clear-instructions;
|
|
263
|
+
// otherwise the same JSON.stringify wrapper --instructions-file uses (setConfigKey takes raw
|
|
264
|
+
// CLI text, so a body that parses as JSON is stored verbatim), with validateConfig owning the
|
|
265
|
+
// 4096-char cap and the unknown-role check in writeConfigMutation. Unlike quiet hours the
|
|
266
|
+
// clear arm carries no role guard (the director may clear, and the unknown-role clear gap is
|
|
267
|
+
// pre-existing).
|
|
268
|
+
writeRoleJsonKey(res, root, base, `roles.${base}.instructions`, "instructions", body.text, (v) => JSON.stringify(v), (v) => `${base}: instructions saved (${v.length} chars)`);
|
|
269
|
+
}
|
|
270
|
+
/** The shared write behind POST /api/role-enable and POST /api/role-disable: the Settings
|
|
271
|
+
* card's Enable button and the role drawer's two-click Disable both write
|
|
272
|
+
* `roles.<base>.enabled` through the CLI's own setConfigKey (raw JSON literal, so JSON.parse
|
|
273
|
+
* yields the boolean checkBoolean expects), and validateConfig's unknown-role check refuses an
|
|
274
|
+
* unknown id as configKeyError's 400 before anything is written. Same body discipline as
|
|
275
|
+
* /api/role-cap (readPostBody → 400 malformed/non-object, 413 oversized); a missing,
|
|
276
|
+
* non-string, or blank role is a 400 `role required`, and a bare `feature-2` loop id maps to
|
|
277
|
+
* its base role's key (baseRoleOf), so an instance disables/enables the role it belongs to.
|
|
278
|
+
* The director cannot be disabled (with it disabled loopIds omits it and no loop dequeues the
|
|
279
|
+
* operator's inbox), so a disable is refused explicitly; enabling the director stays harmless
|
|
280
|
+
* and mirrors the CLI's --enable. */
|
|
281
|
+
async function setRoleEnabled(req, res, root, enabled) {
|
|
282
|
+
const body = await readPostBody(req, res, '{"role": "selfcheck"}');
|
|
283
|
+
if (!body)
|
|
284
|
+
return;
|
|
285
|
+
const role = body.role;
|
|
286
|
+
if (typeof role !== "string" || role.trim() === "")
|
|
287
|
+
return badRequest(res, "role required");
|
|
288
|
+
const base = baseRoleOf(role);
|
|
289
|
+
if (!enabled && base === DIRECTOR_ROLE)
|
|
290
|
+
return badRequest(res, "the director cannot be disabled");
|
|
291
|
+
const key = `roles.${base}.enabled`;
|
|
292
|
+
// setConfigKey takes raw CLI text; the raw JSON literal round-trips to the boolean
|
|
293
|
+
// checkBoolean expects, and validateConfig owns the unknown-role check in writeConfigMutation.
|
|
294
|
+
const result = setConfigKey(root, key, enabled ? "true" : "false");
|
|
295
|
+
if (!result.ok)
|
|
296
|
+
return configKeyError(res, key, result);
|
|
297
|
+
sendJson(res, 200, { ok: true, role: base, message: `${base}: ${enabled ? "enabled" : "disabled"}` });
|
|
298
|
+
}
|
|
299
|
+
/** Handle POST /api/role-enable: the Settings view's "Disabled roles" card Enable button —
|
|
300
|
+
* re-enables one disabled role (the GUI half of the CLI's `tumwater role <id> --enable`). Body
|
|
301
|
+
* discipline, role/base mapping, refusal semantics, and the write live in setRoleEnabled. */
|
|
302
|
+
export async function handleRoleEnable(req, res, root) {
|
|
303
|
+
await setRoleEnabled(req, res, root, true);
|
|
304
|
+
}
|
|
305
|
+
/** Handle POST /api/role-disable: the role drawer's two-click Disable action — disables one
|
|
306
|
+
* enabled role (the GUI half of the CLI's `tumwater role <id> --disable`, completing the pair
|
|
307
|
+
* the Settings card's Enable starts). Body discipline, role/base mapping, refusal semantics,
|
|
308
|
+
* and the write live in setRoleEnabled. */
|
|
309
|
+
export async function handleRoleDisable(req, res, root) {
|
|
310
|
+
await setRoleEnabled(req, res, root, false);
|
|
311
|
+
}
|
|
312
|
+
/** Handle POST /api/role-quiet-hours: the role drawer's own-window editor sets or clears one
|
|
313
|
+
* role's daily quiet-hours window — the control the `Quiet hours` row it already shows leads
|
|
314
|
+
* to (the counterpart of the in-flight `tumwater role <id> --quiet-hours` CLI write). Same body
|
|
315
|
+
* discipline as /api/role-cap (readPostBody → 400 malformed/non-object, 413 oversized); a
|
|
316
|
+
* missing/blank role is a 400, and a bare `feature-2` loop id maps to its base role's key
|
|
317
|
+
* (baseRoleOf), so an instance edits the window its runs read. The director is refused up front:
|
|
318
|
+
* `quietHoursPerRole.director` is inert (roleQuietHold and doctor both exempt the director). An
|
|
319
|
+
* empty/whitespace-only window clears the key (unsetConfigKey, like the CLI's --clear-quiet-hours);
|
|
320
|
+
* the clear branch checks the role id explicitly (rejectBadRole) because unsetConfigKey's no-op
|
|
321
|
+
* map path skips validateConfig — the guard the set branch gets free from validateConfig's
|
|
322
|
+
* unknown-role refusal; otherwise the write goes through setConfigKey with the same
|
|
323
|
+
* JSON.stringify wrapper, so checkQuietHours (naming quietHoursPerRole.<base>) and validateConfig
|
|
324
|
+
* own the truth, and a write failure is configKeyError's 400 naming the dotted key. */
|
|
325
|
+
export async function handleRoleQuietHours(req, res, root) {
|
|
326
|
+
const body = await readPostBody(req, res, '{"role": "feature", "window": "23:00-07:00"}');
|
|
327
|
+
if (!body)
|
|
328
|
+
return;
|
|
329
|
+
const role = body.role;
|
|
330
|
+
if (typeof role !== "string" || role.trim() === "")
|
|
331
|
+
return badRequest(res, "role required");
|
|
332
|
+
if (role === DIRECTOR_ROLE)
|
|
333
|
+
return badRequest(res, "the director has no quiet hours");
|
|
334
|
+
if (typeof body.window !== "string")
|
|
335
|
+
return badRequest(res, `"window" must be a string — a window like 23:00-07:00, or "" to clear it${gotSuffix(body.window)}`);
|
|
336
|
+
const base = baseRoleOf(role);
|
|
337
|
+
// An empty or whitespace-only window clears the hold, like the CLI's --clear-quiet-hours;
|
|
338
|
+
// the clear arm carries rejectBadRole because unsetConfigKey's no-op map path (a role the
|
|
339
|
+
// file never holds, an unknown id included) skips validateConfig, so without it the set
|
|
340
|
+
// branch's unknown-role refusal would never fire on a clear and a bogus id would answer 200
|
|
341
|
+
// "cleared" (mirrors the CLI's knownRoleIds guard). The set arm runs setConfigKey raw, its
|
|
342
|
+
// map branch checking the dotted key as the label and validateConfig owning the unknown role.
|
|
343
|
+
writeRoleJsonKey(res, root, base, `quietHoursPerRole.${base}`, "quiet hours", body.window, (v) => JSON.stringify(v), (v) => `${base}: quiet hours set to ${v}`, () => rejectBadRole(root, res, role));
|
|
344
|
+
}
|
|
345
|
+
/** The shared 400 for a failed config mutation — the set and unset write paths both nest the
|
|
346
|
+
* write's error under the key it names, so the Settings view can tell which field failed and a
|
|
347
|
+
* retried request gets the same wording from either surface. Its void return keeps the guard
|
|
348
|
+
* a one-liner: `if (!result.ok) return configKeyError(res, key, result);` */
|
|
349
|
+
function configKeyError(res, key, result) {
|
|
350
|
+
badRequest(res, `${key}: ${result.error}`);
|
|
351
|
+
}
|
|
352
|
+
/** The set-or-clear write the two per-role JSON-config-key handlers (quiet hours, standing
|
|
353
|
+
* instructions) share: an empty/whitespace `raw` unsets `key` (after `guardClear`, a per-call
|
|
354
|
+
* check the clear arm runs first — quiet hours' unknown-role guard; instructions has none),
|
|
355
|
+
* else sets it via setConfigKey (which JSON-encodes nothing, so `serialize` shapes the stored
|
|
356
|
+
* value). `noun` names the hold in the "cleared" 200 and `setMessage` is the exact set-arm 200
|
|
357
|
+
* message, threaded so each handler keeps its own wording. Always writes the response, so the
|
|
358
|
+
* caller never falls through to further output. */
|
|
359
|
+
function writeRoleJsonKey(res, root, base, key, noun, raw, serialize, setMessage, guardClear) {
|
|
360
|
+
if (raw.trim() === "") {
|
|
361
|
+
if (guardClear?.())
|
|
362
|
+
return;
|
|
363
|
+
const result = unsetConfigKey(root, key);
|
|
364
|
+
if (!result.ok)
|
|
365
|
+
configKeyError(res, key, result);
|
|
366
|
+
else
|
|
367
|
+
sendJson(res, 200, { ok: true, role: base, message: `${base}: ${noun} cleared` });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const result = setConfigKey(root, key, serialize(raw));
|
|
371
|
+
if (!result.ok)
|
|
372
|
+
configKeyError(res, key, result);
|
|
373
|
+
else
|
|
374
|
+
sendJson(res, 200, { ok: true, role: base, message: setMessage(raw) });
|
|
375
|
+
}
|
|
376
|
+
/** Handle POST /api/config-set: the Settings view's per-key Save — one curated key set to
|
|
377
|
+
* one JSON value, through setConfigKey so the browser cannot drift from the CLI's rules
|
|
378
|
+
* (unknown-key refusal, the per-key validators' messages, whole-candidate validateConfig,
|
|
379
|
+
* atomic write the running team picks up live). A key outside EDITABLE_CONFIG_KEYS is
|
|
380
|
+
* refused 400 with the key named — a known-but-not-curated key (customRoles) included, so
|
|
381
|
+
* the panel's reach stays exactly EDITABLE_CONFIG_KEYS. The body's value is re-encoded with
|
|
382
|
+
* JSON.stringify before setConfigKey parses it back, so `set model gpt-5`'s
|
|
383
|
+
* JSON-or-literal rule is bypassed harmlessly: the browser already sent parsed JSON.
|
|
384
|
+
* Same body discipline as /api/budget (readPostBody → 400 malformed/non-object, 413
|
|
385
|
+
* oversized). */
|
|
386
|
+
export async function handleConfigSet(req, res, root) {
|
|
387
|
+
const body = await readPostBody(req, res, '{"key": "model", "value": "gpt-5"}');
|
|
388
|
+
if (!body)
|
|
389
|
+
return;
|
|
390
|
+
const key = requireConfigKey(res, body);
|
|
391
|
+
if (key === null)
|
|
392
|
+
return;
|
|
393
|
+
if (body.value === undefined) {
|
|
394
|
+
badRequest(res, `value required for ${key}`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
// setConfigKey takes the raw CLI text; a JSON round-trip of an already-parsed value is
|
|
398
|
+
// exact (JSON.parse(JSON.stringify(v)) === v), so the shared write path applies unchanged.
|
|
399
|
+
const result = setConfigKey(root, key, JSON.stringify(body.value));
|
|
400
|
+
if (!result.ok)
|
|
401
|
+
return configKeyError(res, key, result);
|
|
402
|
+
sendJson(res, 200, { ok: true, key, value: result.value, oldValue: result.oldValue });
|
|
403
|
+
}
|
|
404
|
+
/** Handle POST /api/config-unset: the Settings view's per-key Clear — one curated key removed,
|
|
405
|
+
* through unsetConfigKey so the browser cannot drift from `tumwater config unset`'s rules
|
|
406
|
+
* (unknown-key refusal, whole-candidate validateConfig, atomic write the running team picks
|
|
407
|
+
* up live). The same EDITABLE_CONFIG_KEYS gate as /api/config-set, so the panel's reach stays
|
|
408
|
+
* exactly the curated set; a key outside it is refused 400 with the key named. Same body
|
|
409
|
+
* discipline (readPostBody → 400 malformed/non-object, 413 oversized). The 200 body omits
|
|
410
|
+
* `oldValue` when the file never held the key (JSON.stringify drops an undefined), so the
|
|
411
|
+
* client can tell a retraction from a no-op; a file value of `null` still serializes and is a
|
|
412
|
+
* real retraction. */
|
|
413
|
+
export async function handleConfigUnset(req, res, root) {
|
|
414
|
+
const body = await readPostBody(req, res, '{"key": "model"}');
|
|
415
|
+
if (!body)
|
|
416
|
+
return;
|
|
417
|
+
const key = requireConfigKey(res, body);
|
|
418
|
+
if (key === null)
|
|
419
|
+
return;
|
|
420
|
+
const result = unsetConfigKey(root, key);
|
|
421
|
+
if (!result.ok)
|
|
422
|
+
return configKeyError(res, key, result);
|
|
423
|
+
sendJson(res, 200, { ok: true, key, oldValue: result.oldValue });
|
|
424
|
+
}
|
|
425
|
+
/** Handle POST /api/notify-test: the Settings view's "Send test" — what `tumwater notify --test`
|
|
426
|
+
* runs (testNotifyHook, under the same NOTIFY_TEST_TIMEOUT_MS bound), with the hook's stdio
|
|
427
|
+
* ignored so its output never reaches the server's terminal. The body is ignored, like
|
|
428
|
+
* /api/restart's. No hook set is a 409 with the CLI's wording; a hook that fails or times out is
|
|
429
|
+
* still a 200 — the request worked, and `passed` carries the verdict. */
|
|
430
|
+
export async function handleNotifyTest(req, res, root) {
|
|
431
|
+
const body = await readPostBody(req, res, "{}");
|
|
432
|
+
if (!body)
|
|
433
|
+
return;
|
|
434
|
+
const outcome = await testNotifyHook(root, "ignore");
|
|
435
|
+
if (outcome.status === "not_configured") {
|
|
436
|
+
sendJson(res, 409, { error: "notify is not configured (set `notify` in tumwater.json)" });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
sendJson(res, 200, { ok: true, passed: outcome.passed, message: outcome.message });
|
|
440
|
+
}
|