tumwater 0.1.2 → 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 +74 -108
- 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-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- 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 +197 -319
- 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 +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- 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 +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- 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 +29 -224
- package/dist/src/cli.js +121 -40
- 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 +119 -337
- 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 +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- 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 +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- 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 +137 -38
- 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 +12 -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 +86 -55
- 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 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- 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 +28 -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 +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- 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 +170 -80
- 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 +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -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 -21
- 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 +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- 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 +128 -37
- 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 +47 -43
- 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 +47 -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 +35 -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 +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- 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 +81 -39
- 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 +125 -135
- 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 +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- 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 +77 -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 +68 -33
- 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 +86 -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} +62 -37
- 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 +147 -26
- 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 +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- 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 +91 -47
- 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 +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- 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 +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -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 +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -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 +11 -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 +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- 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 +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- 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 -31
- package/dist/src/landing/landing-slot.js +0 -246
- 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
|
@@ -1,671 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The dashboard's POST operator endpoint
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* (readPostBody, readRoleBody,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
2
|
+
* The dashboard's POST operator endpoint namespace (src/gui/gui-server.ts routes to these
|
|
3
|
+
* handlers): one file per domain, each adapting HTTP onto the domain work one layer down, all
|
|
4
|
+
* gathered here. The GET data endpoints (transcript, backlog, report, failures, history, tick,
|
|
5
|
+
* diff, config) stay in gui/gui-endpoints.ts; the shared request-body helpers
|
|
6
|
+
* (readPostBody, readRoleBody, sendRequestResult) live in gui/gui-endpoint-common.ts. The
|
|
7
|
+
* per-domain command modules, each a thin HTTP adapter touching no socket beyond its own `res`:
|
|
8
|
+
* prompt queueing/cancel/edit (gui/gui-prompt-commands), backlog entry file/cancel/edit +
|
|
9
|
+
* question-answer (gui/gui-backlog-commands), config set/unset + budget + notify-test
|
|
10
|
+
* (gui/gui-config-commands), team pause/role-pause/wake (gui/gui-team-commands),
|
|
11
|
+
* and operator lifecycle restart/reclaim/abort/reset-counters/stop-schedule/role-notes/
|
|
12
|
+
* retire/brief-edit (gui/gui-operator-commands).
|
|
12
13
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import { promptImagesProblem } from "../inbox/inbox-attachments.js";
|
|
19
|
-
import { tryAnswerQuestion, UNCLOSED_FENCE_MESSAGE } from "../cli/question-commands.js";
|
|
20
|
-
import { parseDurationMs } from "../cli/cli-args.js";
|
|
21
|
-
import { checkDailyBudgetUsd, setConfigKey, setDailyBudgetUsd, unsetConfigKey } from "../config/config-write.js";
|
|
22
|
-
import { normalizePauseReason, pauseFleet, pauseRole, resumeFleet, resumeRole } from "../fleet/fleet-state.js";
|
|
23
|
-
import { PAUSE_FOR_MAX_MS, cancelWakes, requestAbort, requestResetCounters, requestRestart, requestWake, runManualReclaim, submitRolePromptAndWake } from "../operator/operator-intent.js";
|
|
24
|
-
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
25
|
-
import { saveRoleNote } from "../agent/role-notes-write.js";
|
|
26
|
-
import { roleNotesPath } from "../paths.js";
|
|
27
|
-
import { rejectBadQueueFileName, rejectBadRole, requireConfigKey, requirePausedFlag, requirePromptText, validRoleIds } from "./gui-args.js";
|
|
28
|
-
import { readJsonObject, sendJson } from "./http-body.js";
|
|
29
|
-
import { collapseWhitespace, errorMessage, gotSuffix } from "../text/text.js";
|
|
30
|
-
/** Every POST handler's shared opening: readJsonObject reads the body and — when it is
|
|
31
|
-
* oversized, malformed, or not a JSON object — has already written the 413/400 itself. Its
|
|
32
|
-
* null return therefore means the response is sent and the handler must stop without touching
|
|
33
|
-
* it again; each handler used to restate that contract in its own inline comment, and this is
|
|
34
|
-
* the one home for it. Handlers keep their own `if (!body) return` bail so the stop stays
|
|
35
|
-
* visible in their flow. */
|
|
36
|
-
async function readPostBody(req, res, example) {
|
|
37
|
-
const body = await readJsonObject(req, res, example);
|
|
38
|
-
if (body === null)
|
|
39
|
-
return null; // 4xx already sent — oversized or not a JSON object
|
|
40
|
-
return body;
|
|
41
|
-
}
|
|
42
|
-
/** The role-targeting POST handlers' shared prologue: readPostBody (400/413 sent on a null
|
|
43
|
-
* return) plus rejectBadRole's shared role validation (400 sent on a null return), so the
|
|
44
|
-
* wake/abort/prompt-role/pause-role family states the body-discipline-and-role-check pairing
|
|
45
|
-
* once instead of two guard lines per handler. Its null return means the response is already
|
|
46
|
-
* written and the handler must stop; callers keep their own `if (!body) return` bail. Exactly
|
|
47
|
-
* seven call sites — handlePromptRole, handleWake, handleWakeCancel and handleResetCounters (allowMissing, the
|
|
48
|
-
* {} → all-roles default), handleAbort, handlePauseRole, and handleRoleNotes; handlePromptCancel deliberately does not ride it, since
|
|
49
|
-
* its role is optional and validates only when present. */
|
|
50
|
-
async function readRoleBody(req, res, example, root, allowMissing = false) {
|
|
51
|
-
const body = await readPostBody(req, res, example);
|
|
52
|
-
if (body === null)
|
|
53
|
-
return null; // 4xx already sent — oversized, malformed, or not an object
|
|
54
|
-
if (rejectBadRole(root, res, body.role, allowMissing))
|
|
55
|
-
return null; // 400 already sent
|
|
56
|
-
return body;
|
|
57
|
-
}
|
|
58
|
-
/** Handle POST /api/prompt: queue a director prompt. An over-long prompt
|
|
59
|
-
* (DIRECTOR_PROMPT_MAX_CHARS) is a user-input error, not a server fault: the shared length
|
|
60
|
-
* rule answers 400 — an unexpected submit failure (EACCES) still reaches the server's outer
|
|
61
|
-
* catch as the 500 its gui-server-transport test pins. An optional images array (the composer's
|
|
62
|
-
* dropped/pasted attachments) is validated before anything is queued: a bad image — wrong
|
|
63
|
-
* extension, too many, undecodable, oversized — answers 400 naming the rule, with no queue
|
|
64
|
-
* file and no image written. */
|
|
65
|
-
export async function handlePrompt(req, res, root) {
|
|
66
|
-
const body = await readPostBody(req, res, '{"text": "..."}');
|
|
67
|
-
if (!body)
|
|
68
|
-
return;
|
|
69
|
-
const fields = checkedPromptFields(res, body);
|
|
70
|
-
if (!fields)
|
|
71
|
-
return;
|
|
72
|
-
submitPrompt(root, fields.text, fields.images, fields.notBeforeMs);
|
|
73
|
-
sendJson(res, 200, { ok: true });
|
|
74
|
-
}
|
|
75
|
-
/** The images field of a prompt endpoint's body, validated before anything touches the disk.
|
|
76
|
-
* Three outcomes, deliberately distinct: `undefined` — no images field, a text-only submit,
|
|
77
|
-
* continue without attachments; an array — the field was present and valid, attach it (a
|
|
78
|
-
* present-but-empty array is fine and saves nothing); `null` — the field was present and
|
|
79
|
-
* invalid, the 400 naming the rule is already sent, stop. The absent and invalid cases must
|
|
80
|
-
* not share a value: a text-only POST is the common path and must proceed. */
|
|
81
|
-
function checkedPromptImages(res, body) {
|
|
82
|
-
if (body.images === undefined)
|
|
83
|
-
return undefined;
|
|
84
|
-
const problem = promptImagesProblem(body.images);
|
|
85
|
-
if (problem) {
|
|
86
|
-
sendJson(res, 400, { error: problem });
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
return body.images;
|
|
90
|
-
}
|
|
91
|
-
/** The defer field of a prompt endpoint's body, validated before anything touches the disk.
|
|
92
|
-
* Three outcomes, deliberately distinct like checkedPromptImages: `undefined` — no defer
|
|
93
|
-
* field, a prompt that runs as soon as its turn comes; a number — the epoch ms the prompt is
|
|
94
|
-
* deliverable from (`Date.now() + ms`); `null` — the field was present but not a duration
|
|
95
|
-
* this grammar accepts, or its duration is over the 90d cap, and the 400 naming the rule is
|
|
96
|
-
* already sent, so stop. The grammar is cli-args.ts's parseDurationMs — the same one
|
|
97
|
-
* `tumwater prompt --at` and `pause --for` accept — so a deferral spelled here computes the
|
|
98
|
-
* same `tumwater:not-before` marker the CLI writes. */
|
|
99
|
-
function checkedPromptDefer(res, body) {
|
|
100
|
-
if (body.defer === undefined)
|
|
101
|
-
return undefined;
|
|
102
|
-
const ms = typeof body.defer === "string" ? parseDurationMs(body.defer) : null;
|
|
103
|
-
if (ms === null) {
|
|
104
|
-
sendJson(res, 400, { error: "defer needs a duration like 45s, 90m, 1h30m, or 2d" });
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
if (ms > PAUSE_FOR_MAX_MS) {
|
|
108
|
-
sendJson(res, 400, { error: "defer is capped at 90d" });
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
return Date.now() + ms;
|
|
112
|
-
}
|
|
113
|
-
/** The prompt endpoints' shared body tail: requirePromptText (400 on a null return) then
|
|
114
|
-
* checkedPromptImages then checkedPromptDefer (400 on a null return from either), so
|
|
115
|
-
* /api/prompt and /api/prompt-role read the same three fields in the same order and a body
|
|
116
|
-
* wrong in several still answers the text problem first. Null means a 400 is already sent;
|
|
117
|
-
* callers keep their own `if (!fields) return` bail. */
|
|
118
|
-
function checkedPromptFields(res, body, role = DIRECTOR_ROLE) {
|
|
119
|
-
const text = requirePromptText(res, body, role);
|
|
120
|
-
if (text === null)
|
|
121
|
-
return null;
|
|
122
|
-
const images = checkedPromptImages(res, body);
|
|
123
|
-
if (images === null)
|
|
124
|
-
return null;
|
|
125
|
-
const notBeforeMs = checkedPromptDefer(res, body);
|
|
126
|
-
if (notBeforeMs === null)
|
|
127
|
-
return null;
|
|
128
|
-
return { text, images, notBeforeMs };
|
|
129
|
-
}
|
|
130
|
-
/** Handle POST /api/prompt-role: queue a prompt for one loop — the dashboard's per-row
|
|
131
|
-
* prompt affordance submits here, calling the same path `tumwater prompt --role <id>` uses
|
|
132
|
-
* (submitRolePrompt enqueues into that loop's own queue and logs under it, then a single-role
|
|
133
|
-
* wake brings the live loop in within one poll; the wake message rides back for the flash).
|
|
134
|
-
* The role validates exactly like /api/transcript (the shared rejectBadRole wording), and the
|
|
135
|
-
* body discipline is /api/prompt's: readJsonObject → 400 malformed/non-object, 413 oversized,
|
|
136
|
-
* text a non-empty string within the shared length rule → 400 otherwise — plus the same
|
|
137
|
-
* optional images array and its validate-before-anything-is-written discipline. */
|
|
138
|
-
export async function handlePromptRole(req, res, root) {
|
|
139
|
-
const body = await readRoleBody(req, res, '{"role": "feature", "text": "..."}', root);
|
|
140
|
-
if (!body)
|
|
141
|
-
return;
|
|
142
|
-
const role = body.role;
|
|
143
|
-
const fields = checkedPromptFields(res, body, role);
|
|
144
|
-
if (!fields)
|
|
145
|
-
return;
|
|
146
|
-
sendJson(res, 200, { ok: true, message: submitRolePromptAndWake(root, role, fields.text, fields.images, fields.notBeforeMs) });
|
|
147
|
-
}
|
|
148
|
-
/** Handle POST /api/prompt-cancel: retract one queued prompt addressed by its queue file —
|
|
149
|
-
* the dashboard's per-row cancel affordance, the file-addressed twin of the CLI's
|
|
150
|
-
* position-addressed `prompt --cancel`. The role is optional (absent cancels from the
|
|
151
|
-
* director's queue) and validates exactly like /api/transcript; a file name failing the
|
|
152
|
-
* shared rejectBadQueueFileName guard is a user-input error answered 400 with
|
|
153
|
-
* nothing touched on disk. A vanished file answers 200 { status: "gone" } — the loop already
|
|
154
|
-
* dequeued the prompt, which is data for the flash line, not a server fault. Same body
|
|
155
|
-
* discipline as /api/prompt (readJsonObject → 400 malformed/non-object, 413 oversized). */
|
|
156
|
-
export async function handlePromptCancel(req, res, root) {
|
|
157
|
-
const body = await readPostBody(req, res, '{"role": "feature", "file": "<stamp>-<seq>-<pid>.md"}');
|
|
158
|
-
if (!body)
|
|
159
|
-
return;
|
|
160
|
-
if (body.role !== undefined && rejectBadRole(root, res, body.role))
|
|
161
|
-
return;
|
|
162
|
-
const role = body.role === undefined ? DIRECTOR_ROLE : body.role;
|
|
163
|
-
if (rejectBadQueueFileName(res, body.file))
|
|
164
|
-
return;
|
|
165
|
-
const outcome = cancelQueuedFile(root, role, body.file);
|
|
166
|
-
sendJson(res, 200, outcome.status === "cancelled"
|
|
167
|
-
? { ok: true, status: "cancelled", preview: promptPreview(outcome.text) }
|
|
168
|
-
: { ok: true, status: "gone" });
|
|
169
|
-
}
|
|
170
|
-
/** Handle POST /api/prompt-edit: rewrite one queued prompt addressed by its queue file — the
|
|
171
|
-
* dashboard's per-row edit affordance, the file-addressed twin of the CLI's position-addressed
|
|
172
|
-
* `prompt --edit`. The role is optional (absent edits the director's queue) and validates
|
|
173
|
-
* exactly like /api/prompt-cancel; a file name failing the shared rejectBadQueueFileName
|
|
174
|
-
* guard is a user-input error answered 400 with nothing touched on disk. Text
|
|
175
|
-
* and images validate through the same checkedPromptFields /api/prompt and /api/prompt-role
|
|
176
|
-
* use; editQueuedFile throws for a bad name or an over-cap text — both user-input errors
|
|
177
|
-
* answered 400 (a filesystem failure reads as 400 like handleConfigSet's failed write). A
|
|
178
|
-
* vanished file answers 200 { ok: true, status: "gone" } — the loop already dequeued the
|
|
179
|
-
* prompt, which is data for the flash line, not a server fault; an edit answers 200 with the
|
|
180
|
-
* old and new operator-facing text the client flashes. Same body discipline as /api/prompt
|
|
181
|
-
* (readJsonObject → 400 malformed/non-object, 413 oversized). */
|
|
182
|
-
export async function handlePromptEdit(req, res, root) {
|
|
183
|
-
const body = await readPostBody(req, res, '{"role": "qa", "file": "<stamp>.md", "text": "..."}');
|
|
184
|
-
if (!body)
|
|
185
|
-
return;
|
|
186
|
-
if (body.role !== undefined && rejectBadRole(root, res, body.role))
|
|
187
|
-
return;
|
|
188
|
-
const role = body.role === undefined ? DIRECTOR_ROLE : body.role;
|
|
189
|
-
if (rejectBadQueueFileName(res, body.file))
|
|
190
|
-
return;
|
|
191
|
-
const fields = checkedPromptFields(res, body, role);
|
|
192
|
-
if (!fields)
|
|
193
|
-
return;
|
|
194
|
-
let outcome;
|
|
195
|
-
try {
|
|
196
|
-
outcome = editQueuedFile(root, role, body.file, fields.text, fields.images);
|
|
197
|
-
}
|
|
198
|
-
catch (err) {
|
|
199
|
-
sendJson(res, 400, { error: errorMessage(err) });
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
sendJson(res, 200, outcome.status === "edited"
|
|
203
|
-
? { ok: true, status: "edited", oldText: outcome.oldText, newText: outcome.newText }
|
|
204
|
-
: { ok: true, status: "gone" });
|
|
205
|
-
}
|
|
206
|
-
/** Handle POST /api/question-answer: record the operator's answer to the Nth open question
|
|
207
|
-
* exactly as `tumwater questions answer` does, through the non-exiting tryAnswerQuestion core.
|
|
208
|
-
* The body carries the `title` the operator saw at `position`; a different title there, or no
|
|
209
|
-
* such position, answers 200 { ok: true, status: "gone" } (the outbox moved on — data for the
|
|
210
|
-
* flash line, like /api/prompt-cancel). The decision keeps its lines (`questions answer --file`'s
|
|
211
|
-
* rule): a `## `/`### ` heading line or an unclosed fence in it answers 400 (bodyStructureProblem)
|
|
212
|
-
* with nothing written. An unclosed fence in `## Open` answers 409; a blank
|
|
213
|
-
* decision, a non-positive-integer position, or a missing title answers 400, and so does a
|
|
214
|
-
* filesystem failure (handleConfigSet's precedent). */
|
|
215
|
-
export async function handleQuestionAnswer(req, res, root) {
|
|
216
|
-
const body = await readPostBody(req, res, '{"position": 1, "title": "<heading>", "decision": "..."}');
|
|
217
|
-
if (!body)
|
|
218
|
-
return;
|
|
219
|
-
const { position, title, decision } = body;
|
|
220
|
-
if (typeof position !== "number" || !Number.isInteger(position) || position < 1) {
|
|
221
|
-
sendJson(res, 400, { error: "position must be a positive integer" });
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (typeof title !== "string" || title === "") {
|
|
225
|
-
sendJson(res, 400, { error: "title must be a non-empty string" });
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
if (typeof decision !== "string" || decision.trim() === "") {
|
|
229
|
-
sendJson(res, 400, { error: 'questions answer needs a decision: questions answer <n> "<decision>"' });
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const keptDecision = normalizeEntryBody(decision);
|
|
233
|
-
const problem = bodyStructureProblem(keptDecision);
|
|
234
|
-
if (problem !== null) {
|
|
235
|
-
sendJson(res, 400, { error: problem });
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
let outcome;
|
|
239
|
-
try {
|
|
240
|
-
outcome = tryAnswerQuestion(root, position, keptDecision, title, true);
|
|
241
|
-
}
|
|
242
|
-
catch (err) {
|
|
243
|
-
sendJson(res, 400, { error: errorMessage(err) });
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
if (outcome.status === "answered") {
|
|
247
|
-
sendJson(res, 200, { ok: true, status: "answered", question: outcome.title, decision: outcome.decision });
|
|
248
|
-
}
|
|
249
|
-
else if (outcome.status === "fenced") {
|
|
250
|
-
sendJson(res, 409, { ok: false, error: UNCLOSED_FENCE_MESSAGE });
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
sendJson(res, 200, { ok: true, status: "gone" });
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
/** Handle POST /api/budget: the dashboard's budget-badge editor saves the daily cost cap —
|
|
257
|
-
* same body discipline as /api/prompt (readJsonObject), and the same shared validation rule +
|
|
258
|
-
* atomic setter the TUI's Ctrl+B uses, so both surfaces write tumwater.json identically and
|
|
259
|
-
* the running orchestrator picks the change up on its next ~2 s poll. */
|
|
260
|
-
export async function handleBudget(req, res, root) {
|
|
261
|
-
const body = await readPostBody(req, res, '{"maxDailyCostUsd": 25}');
|
|
262
|
-
if (!body)
|
|
263
|
-
return;
|
|
264
|
-
const value = body.maxDailyCostUsd;
|
|
265
|
-
if (value === undefined) {
|
|
266
|
-
sendJson(res, 400, { error: "maxDailyCostUsd required" });
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
// The shared rule (finite ≥ 0; 0 disables): missing/non-finite/negative → 400 with
|
|
270
|
-
// the offending value named.
|
|
271
|
-
const problem = checkDailyBudgetUsd(value);
|
|
272
|
-
if (problem) {
|
|
273
|
-
sendJson(res, 400, { error: problem });
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
const result = setDailyBudgetUsd(root, value);
|
|
277
|
-
if (!result.ok) {
|
|
278
|
-
// The value was valid — this is a server-side failure (broken tumwater.json or
|
|
279
|
-
// disk), not the client's fault.
|
|
280
|
-
sendJson(res, 500, { error: result.error });
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
sendJson(res, 200, { ok: true, maxDailyCostUsd: value });
|
|
284
|
-
}
|
|
285
|
-
/** Handle POST /api/config-set: the Settings view's per-key Save — one curated key set to
|
|
286
|
-
* one JSON value, through setConfigKey so the browser cannot drift from the CLI's rules
|
|
287
|
-
* (unknown-key refusal, the per-key validators' messages, whole-candidate validateConfig,
|
|
288
|
-
* atomic write the running fleet picks up live). A key outside EDITABLE_CONFIG_KEYS is
|
|
289
|
-
* refused 400 with the key named — a known-but-not-curated key (customLoops) included, so
|
|
290
|
-
* the panel's reach stays exactly EDITABLE_CONFIG_KEYS. The body's value is re-encoded with
|
|
291
|
-
* JSON.stringify before setConfigKey parses it back, so `set model gpt-5`'s
|
|
292
|
-
* JSON-or-literal rule is bypassed harmlessly: the browser already sent parsed JSON.
|
|
293
|
-
* Same body discipline as /api/budget (readPostBody → 400 malformed/non-object, 413
|
|
294
|
-
* oversized). */
|
|
295
|
-
export async function handleConfigSet(req, res, root) {
|
|
296
|
-
const body = await readPostBody(req, res, '{"key": "model", "value": "gpt-5"}');
|
|
297
|
-
if (!body)
|
|
298
|
-
return;
|
|
299
|
-
const key = requireConfigKey(res, body);
|
|
300
|
-
if (key === null)
|
|
301
|
-
return;
|
|
302
|
-
if (body.value === undefined) {
|
|
303
|
-
sendJson(res, 400, { error: `value required for ${key}` });
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
// setConfigKey takes the raw CLI text; a JSON round-trip of an already-parsed value is
|
|
307
|
-
// exact (JSON.parse(JSON.stringify(v)) === v), so the shared write path applies unchanged.
|
|
308
|
-
const result = setConfigKey(root, key, JSON.stringify(body.value));
|
|
309
|
-
if (!result.ok) {
|
|
310
|
-
sendJson(res, 400, { error: `${key}: ${result.error}` });
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
sendJson(res, 200, { ok: true, key, value: result.value, oldValue: result.oldValue });
|
|
314
|
-
}
|
|
315
|
-
/** Handle POST /api/config-unset: the Settings view's per-key Clear — one curated key removed,
|
|
316
|
-
* through unsetConfigKey so the browser cannot drift from `tumwater config unset`'s rules
|
|
317
|
-
* (unknown-key refusal, whole-candidate validateConfig, atomic write the running fleet picks
|
|
318
|
-
* up live). The same EDITABLE_CONFIG_KEYS gate as /api/config-set, so the panel's reach stays
|
|
319
|
-
* exactly the curated set; a key outside it is refused 400 with the key named. Same body
|
|
320
|
-
* discipline (readPostBody → 400 malformed/non-object, 413 oversized). The 200 body omits
|
|
321
|
-
* `oldValue` when the file never held the key (JSON.stringify drops an undefined), so the
|
|
322
|
-
* client can tell a retraction from a no-op; a file value of `null` still serializes and is a
|
|
323
|
-
* real retraction. */
|
|
324
|
-
export async function handleConfigUnset(req, res, root) {
|
|
325
|
-
const body = await readPostBody(req, res, '{"key": "model"}');
|
|
326
|
-
if (!body)
|
|
327
|
-
return;
|
|
328
|
-
const key = requireConfigKey(res, body);
|
|
329
|
-
if (key === null)
|
|
330
|
-
return;
|
|
331
|
-
const result = unsetConfigKey(root, key);
|
|
332
|
-
if (!result.ok) {
|
|
333
|
-
sendJson(res, 400, { error: `${key}: ${result.error}` });
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
sendJson(res, 200, { ok: true, key, oldValue: result.oldValue });
|
|
337
|
-
}
|
|
338
|
-
/** The shared `forSeconds` rule of /api/pause and /api/pause-role: undefined when absent, the
|
|
339
|
-
* deadline in epoch milliseconds when valid, null after sending the 400 (only with
|
|
340
|
-
* `paused: true`, a positive finite number, within the CLI's 90-day cap). */
|
|
341
|
-
function pauseForSecondsMs(res, body, pausing) {
|
|
342
|
-
const forSeconds = body.forSeconds;
|
|
343
|
-
if (forSeconds === undefined)
|
|
344
|
-
return undefined;
|
|
345
|
-
if (!pausing) {
|
|
346
|
-
sendJson(res, 400, { error: "forSeconds only applies when pausing (paused: true)" });
|
|
347
|
-
return null;
|
|
348
|
-
}
|
|
349
|
-
if (typeof forSeconds !== "number" || !Number.isFinite(forSeconds) || forSeconds <= 0) {
|
|
350
|
-
sendJson(res, 400, { error: `forSeconds must be a positive number of seconds${gotSuffix(forSeconds)}` });
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
if (forSeconds * 1000 > PAUSE_FOR_MAX_MS) {
|
|
354
|
-
sendJson(res, 400, { error: `forSeconds is capped at ${PAUSE_FOR_MAX_MS / 1000} seconds (90 days)${gotSuffix(forSeconds)} — pause without it for a standing pause` });
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
return Date.now() + Math.round(forSeconds * 1000);
|
|
358
|
-
}
|
|
359
|
-
/** The optional `reason` of /api/pause: undefined when absent or blank, the raw string when
|
|
360
|
-
* given with `paused: true` (pauseFleet folds and caps it), null after sending the 400. */
|
|
361
|
-
function pauseReasonField(res, body, pausing) {
|
|
362
|
-
const reason = body.reason;
|
|
363
|
-
if (reason === undefined)
|
|
364
|
-
return undefined;
|
|
365
|
-
if (!pausing) {
|
|
366
|
-
sendJson(res, 400, { error: "reason only applies when pausing (paused: true)" });
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
if (typeof reason !== "string") {
|
|
370
|
-
sendJson(res, 400, { error: `reason must be a string${gotSuffix(reason)}` });
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
return reason.trim() === "" ? undefined : reason;
|
|
374
|
-
}
|
|
375
|
-
/** Handle POST /api/pause: the dashboard's pause control — the same operator gate
|
|
376
|
-
* `tumwater pause [--for <duration>]` and `resume` write, via fleet/fleet-state.ts's shared writers
|
|
377
|
-
* (pauseFleet/resumeFleet) so the CLI and the GUI cannot drift on the marker's format or
|
|
378
|
-
* idempotence. Same body discipline as /api/prompt and /api/budget (readJsonObject → 400
|
|
379
|
-
* malformed/non-object, 413 oversized); the target state is explicit (`paused: true|false`)
|
|
380
|
-
* rather than a toggle, so a retried request is idempotent. An optional `forSeconds` makes it
|
|
381
|
-
* a timed pause (the marker's `until`, capped like the CLI's `--for`); it only accompanies
|
|
382
|
-
* `paused: true`, as does an optional `reason` (the CLI's `pause --reason`, stored folded and
|
|
383
|
-
* capped by pauseFleet; the 200 body echoes what was stored). */
|
|
384
|
-
export async function handlePause(req, res, root) {
|
|
385
|
-
const body = await readPostBody(req, res, '{"paused": true, "forSeconds": 3600}');
|
|
386
|
-
if (!body)
|
|
387
|
-
return;
|
|
388
|
-
const value = requirePausedFlag(res, body);
|
|
389
|
-
if (value === null)
|
|
390
|
-
return;
|
|
391
|
-
const untilMs = pauseForSecondsMs(res, body, value);
|
|
392
|
-
if (untilMs === null)
|
|
393
|
-
return;
|
|
394
|
-
const reason = pauseReasonField(res, body, value);
|
|
395
|
-
if (reason === null)
|
|
396
|
-
return;
|
|
397
|
-
if (value)
|
|
398
|
-
pauseFleet(root, untilMs, reason);
|
|
399
|
-
else
|
|
400
|
-
resumeFleet(root);
|
|
401
|
-
const stored = normalizePauseReason(reason);
|
|
402
|
-
sendJson(res, 200, { ok: true, paused: value, ...(untilMs === undefined ? {} : { until: untilMs }), ...(stored ? { reason: stored } : {}) });
|
|
403
|
-
}
|
|
404
|
-
/** The `inSeconds` rule of /api/wake: undefined when absent, the delay in milliseconds when
|
|
405
|
-
* valid, null after sending the 400 (a positive finite number within the CLI's 90-day cap).
|
|
406
|
-
* A duration, not a deadline: requestWake takes `inMs` and reads its own clock. */
|
|
407
|
-
function wakeInSecondsMs(res, body) {
|
|
408
|
-
const inSeconds = body.inSeconds;
|
|
409
|
-
if (inSeconds === undefined)
|
|
410
|
-
return undefined;
|
|
411
|
-
if (typeof inSeconds !== "number" || !Number.isFinite(inSeconds) || inSeconds <= 0) {
|
|
412
|
-
sendJson(res, 400, { error: `inSeconds must be a positive number of seconds${gotSuffix(inSeconds)}` });
|
|
413
|
-
return null;
|
|
414
|
-
}
|
|
415
|
-
if (inSeconds * 1000 > PAUSE_FOR_MAX_MS) {
|
|
416
|
-
sendJson(res, 400, { error: `inSeconds is capped at ${PAUSE_FOR_MAX_MS / 1000} seconds (90 days)${gotSuffix(inSeconds)}` });
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
return Math.round(inSeconds * 1000);
|
|
420
|
-
}
|
|
421
|
-
/** Handle POST /api/wake: the dashboard's per-row wake control — the same marker-writing
|
|
422
|
-
* core `tumwater wake` calls (requestWake), so the CLI and the GUI cannot drift on the
|
|
423
|
-
* state-file edits or the marker. `{}`/a missing role targets every configured role (the
|
|
424
|
-
* CLI's all-roles default); a given role validates exactly like /api/transcript. Same body
|
|
425
|
-
* discipline as /api/pause (readJsonObject → 400 malformed/non-object, 413 oversized). An
|
|
426
|
-
* optional `inSeconds` schedules the wake for later, like `tumwater wake --in` (same 90-day cap). */
|
|
427
|
-
export async function handleWake(req, res, root) {
|
|
428
|
-
const body = await readRoleBody(req, res, '{"role": "feature"}', root, true);
|
|
429
|
-
if (!body)
|
|
430
|
-
return;
|
|
431
|
-
const inMs = wakeInSecondsMs(res, body);
|
|
432
|
-
if (inMs === null)
|
|
433
|
-
return;
|
|
434
|
-
sendJson(res, 200, {
|
|
435
|
-
ok: true,
|
|
436
|
-
message: requestWake(root, body.role === undefined ? validRoleIds(root) : [body.role], inMs),
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
/** Handle POST /api/wake-cancel: the dashboard's cancel of pending scheduled wakes — the same
|
|
440
|
-
* core `tumwater wake --cancel` calls (cancelWakes). A missing role cancels every pending wake;
|
|
441
|
-
* a given role validates like /api/wake and drops only its share. */
|
|
442
|
-
export async function handleWakeCancel(req, res, root) {
|
|
443
|
-
const body = await readRoleBody(req, res, '{"role": "feature"}', root, true);
|
|
444
|
-
if (!body)
|
|
445
|
-
return;
|
|
446
|
-
sendJson(res, 200, { ok: true, message: cancelWakes(root, body.role) });
|
|
447
|
-
}
|
|
448
|
-
/** Handle POST /api/reset-counters: the dashboard's counter reset — the same marker-writing
|
|
449
|
-
* core `tumwater reset-counters` calls (requestResetCounters). Same body discipline and
|
|
450
|
-
* `{}` → every configured role default as /api/wake. */
|
|
451
|
-
export async function handleResetCounters(req, res, root) {
|
|
452
|
-
const body = await readRoleBody(req, res, '{"role": "feature"}', root, true);
|
|
453
|
-
if (!body)
|
|
454
|
-
return;
|
|
455
|
-
sendJson(res, 200, {
|
|
456
|
-
ok: true,
|
|
457
|
-
message: requestResetCounters(root, body.role === undefined ? validRoleIds(root) : [body.role]),
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
/** Handle POST /api/backlog-file: the Plans/Bugs tabs' New control — the same entry and wake
|
|
461
|
-
* `tumwater bug`/`tumwater plan` produce (fileEntryWithBody, then the shared wake text to the
|
|
462
|
-
* bugfix/feature loop). Body `{"kind": "bug"|"plan", "title": "...", "body": "optional markdown"}`. The empty-title and
|
|
463
|
-
* wake-length checks run here, before anything is written: the body is normalized and checked by bodyStructureProblem too, and an over-long wake would throw after the entry landed. */
|
|
464
|
-
export async function handleBacklogFile(req, res, root) {
|
|
465
|
-
const body = await readPostBody(req, res, '{"kind": "plan", "title": "...", "body": "optional markdown"}');
|
|
466
|
-
if (!body)
|
|
467
|
-
return;
|
|
468
|
-
const kind = body.kind;
|
|
469
|
-
if (kind !== "bug" && kind !== "plan") {
|
|
470
|
-
sendJson(res, 400, { error: 'kind must be "bug" or "plan"' });
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
const rawTitle = typeof body.title === "string" ? body.title : "";
|
|
474
|
-
if (collapseWhitespace(rawTitle) === "") {
|
|
475
|
-
sendJson(res, 400, { error: "title required" });
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
const { role, wakeText } = OPERATOR_FILE_KINDS[kind];
|
|
479
|
-
const problem = promptLengthProblem(wakeText(collapseWhitespace(rawTitle)), role);
|
|
480
|
-
if (problem) {
|
|
481
|
-
sendJson(res, 400, { error: problem });
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
const rawBody = body.body;
|
|
485
|
-
if (rawBody !== undefined && typeof rawBody !== "string") {
|
|
486
|
-
sendJson(res, 400, { error: "body must be a string" });
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const entryBody = normalizeEntryBody(rawBody ?? "");
|
|
490
|
-
const bodyProblem = bodyStructureProblem(entryBody);
|
|
491
|
-
if (bodyProblem) {
|
|
492
|
-
sendJson(res, 400, { error: bodyProblem });
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
const filed = fileEntryWithBody(root, kind, rawTitle, entryBody);
|
|
496
|
-
submitRolePromptAndWake(root, role, wakeText(filed.title));
|
|
497
|
-
sendJson(res, 200, { ok: true, file: filed.file, title: filed.title });
|
|
498
|
-
}
|
|
499
|
-
/** Handle POST /api/backlog-cancel: the entry drawer's cancel — the same lock and splice as
|
|
500
|
-
* `tumwater plan|bug --cancel <n>` through the non-exiting tryCancelEntry. Body `{"kind":
|
|
501
|
-
* "bug"|"plan", "position": <1-based>, "title": "<heading>"}`; a missing position or a
|
|
502
|
-
* different heading there answers 200 { ok: true, status: "gone" } (the list moved since the
|
|
503
|
-
* poll). No wake and no inbox prompt: removing work must not wake a loop. */
|
|
504
|
-
export async function handleBacklogCancel(req, res, root) {
|
|
505
|
-
const body = await readPostBody(req, res, '{"kind": "bug", "position": 1, "title": "<heading>"}');
|
|
506
|
-
if (!body)
|
|
507
|
-
return;
|
|
508
|
-
const { kind, position, title } = body;
|
|
509
|
-
if (kind !== "bug" && kind !== "plan") {
|
|
510
|
-
sendJson(res, 400, { error: 'kind must be "bug" or "plan"' });
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
if (typeof position !== "number" || !Number.isInteger(position) || position < 1) {
|
|
514
|
-
sendJson(res, 400, { error: "position must be a positive integer" });
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
if (typeof title !== "string" || title === "") {
|
|
518
|
-
sendJson(res, 400, { error: "title must be a non-empty string" });
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
let outcome;
|
|
522
|
-
try {
|
|
523
|
-
outcome = tryCancelEntry(root, kind, position, title);
|
|
524
|
-
}
|
|
525
|
-
catch (err) {
|
|
526
|
-
sendJson(res, 400, { error: errorMessage(err) });
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
if (outcome.status === "cancelled") {
|
|
530
|
-
sendJson(res, 200, { ok: true, status: "cancelled", file: outcome.file, title: outcome.title });
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
sendJson(res, 200, { ok: true, status: "gone" });
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
/** Handle POST /api/backlog-edit: the entry drawer's edit — tryEditEntry's title-checked,
|
|
537
|
-
* in-place rewrite under the state lock. Body `{"kind", "position", "title", "newTitle",
|
|
538
|
-
* "newBody"}`; a missing position or a different heading there answers 200 { ok: true, status:
|
|
539
|
-
* "gone" }. The body keeps its line structure. No wake and no inbox prompt: whichever tick picks
|
|
540
|
-
* the entry up refines it. */
|
|
541
|
-
export async function handleBacklogEdit(req, res, root) {
|
|
542
|
-
const body = await readPostBody(req, res, '{"kind": "bug", "position": 1, "title": "<heading>", "newTitle": "...", "newBody": "..."}');
|
|
543
|
-
if (!body)
|
|
544
|
-
return;
|
|
545
|
-
const { kind, position, title, newTitle, newBody } = body;
|
|
546
|
-
if (kind !== "bug" && kind !== "plan") {
|
|
547
|
-
sendJson(res, 400, { error: 'kind must be "bug" or "plan"' });
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
if (typeof position !== "number" || !Number.isInteger(position) || position < 1) {
|
|
551
|
-
sendJson(res, 400, { error: "position must be a positive integer" });
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
if (typeof title !== "string" || title === "") {
|
|
555
|
-
sendJson(res, 400, { error: "title must be a non-empty string" });
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
if (typeof newTitle !== "string" || collapseWhitespace(newTitle) === "") {
|
|
559
|
-
sendJson(res, 400, { error: "newTitle required" });
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
if (typeof newBody !== "string") {
|
|
563
|
-
sendJson(res, 400, { error: "newBody must be a string" });
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
const problem = bodyStructureProblem(newBody.replace(/\r\n?/g, "\n"));
|
|
567
|
-
if (problem !== null) {
|
|
568
|
-
sendJson(res, 400, { error: problem });
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
let outcome;
|
|
572
|
-
try {
|
|
573
|
-
outcome = tryEditEntry(root, kind, position, title, newTitle, newBody);
|
|
574
|
-
}
|
|
575
|
-
catch (err) {
|
|
576
|
-
sendJson(res, 400, { error: errorMessage(err) });
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
if (outcome.status === "edited") {
|
|
580
|
-
sendJson(res, 200, { ok: true, status: "edited", file: outcome.file, title: outcome.title });
|
|
581
|
-
}
|
|
582
|
-
else {
|
|
583
|
-
sendJson(res, 200, { ok: true, status: "gone" });
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
/** Reply for a boolean-claiming marker request (RequestResult): success maps to 200 with its
|
|
587
|
-
* message, refusal to 409 — the request is valid but nothing can act on it, the same conflict
|
|
588
|
-
* reading requestAbort's not-live error uses. /api/restart and /api/abort share it. */
|
|
589
|
-
function sendRequestResult(res, result) {
|
|
590
|
-
if (!result.ok) {
|
|
591
|
-
sendJson(res, 409, { error: result.error });
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
sendJson(res, 200, { ok: true, message: result.message });
|
|
595
|
-
}
|
|
596
|
-
/** Handle POST /api/restart: the dashboard's build-stale alert's refresh button — the same
|
|
597
|
-
* marker-writing core a CLI surface would call (requestRestart), so the wording and the
|
|
598
|
-
* not-pending refusal cannot drift. The not-pending error comes back 409 like /api/abort's
|
|
599
|
-
* not-live one: the request is valid but nothing can act on it. Same body discipline as
|
|
600
|
-
* /api/wake (readJsonObject → 400 malformed/non-object, 413 oversized; the body itself is
|
|
601
|
-
* ignored — there are no options to force a restart). */
|
|
602
|
-
export async function handleRestart(req, res, root) {
|
|
603
|
-
const body = await readPostBody(req, res, "{}");
|
|
604
|
-
if (!body)
|
|
605
|
-
return;
|
|
606
|
-
sendRequestResult(res, requestRestart(root));
|
|
607
|
-
}
|
|
608
|
-
/** Handle POST /api/reclaim: the dashboard disk row's reclaim button — the same manual pass
|
|
609
|
-
* `tumwater reclaim` runs (runManualReclaim), answering its confirmation verbatim. Same body
|
|
610
|
-
* discipline as /api/restart (the body is ignored); a throw reaches the server's outer 500. */
|
|
611
|
-
export async function handleReclaim(req, res, root) {
|
|
612
|
-
const body = await readPostBody(req, res, "{}");
|
|
613
|
-
if (!body)
|
|
614
|
-
return;
|
|
615
|
-
sendJson(res, 200, { ok: true, message: await runManualReclaim(root) });
|
|
616
|
-
}
|
|
617
|
-
/** Handle POST /api/abort: the dashboard's per-row abort control — the same marker-writing
|
|
618
|
-
* core `tumwater abort` calls (requestAbort). The role is required and validated like
|
|
619
|
-
* /api/transcript; requestAbort's not-live error comes back 409 — the marker is valid but
|
|
620
|
-
* nothing can consume it, a conflict rather than a client 400. The director variant's
|
|
621
|
-
* message (the discarded-prompt note) rides through verbatim. */
|
|
622
|
-
export async function handleAbort(req, res, root) {
|
|
623
|
-
const body = await readRoleBody(req, res, '{"role": "feature"}', root);
|
|
624
|
-
if (!body)
|
|
625
|
-
return;
|
|
626
|
-
sendRequestResult(res, requestAbort(root, body.role));
|
|
627
|
-
}
|
|
628
|
-
/** Handle POST /api/pause-role: the dashboard's per-row pause/resume control — the same
|
|
629
|
-
* marker-writing core `tumwater pause --role` / `resume --role` call (pauseRole/resumeRole),
|
|
630
|
-
* so the CLI and the GUI cannot drift on the marker's format or idempotence. The target state
|
|
631
|
-
* is explicit (`paused: true|false`) like /api/pause, so a retried request is idempotent; the
|
|
632
|
-
* fleet-wide pause composes freely — its gate is checked first at scheduling, and the role
|
|
633
|
-
* marker still records the operator's per-row intent. Same body discipline as /api/wake
|
|
634
|
-
* (readJsonObject → 400 malformed/non-object, 413 oversized; the role validates through the
|
|
635
|
-
* shared rejectBadRole wording). */
|
|
636
|
-
export async function handlePauseRole(req, res, root) {
|
|
637
|
-
const body = await readRoleBody(req, res, '{"role": "feature", "paused": true}', root);
|
|
638
|
-
if (!body)
|
|
639
|
-
return;
|
|
640
|
-
const value = requirePausedFlag(res, body);
|
|
641
|
-
if (value === null)
|
|
642
|
-
return;
|
|
643
|
-
const untilMs = pauseForSecondsMs(res, body, value);
|
|
644
|
-
if (untilMs === null)
|
|
645
|
-
return;
|
|
646
|
-
const changed = value
|
|
647
|
-
? pauseRole(root, body.role, untilMs)
|
|
648
|
-
: resumeRole(root, body.role);
|
|
649
|
-
sendJson(res, 200, { ok: true, changed, paused: value, ...(untilMs === undefined ? {} : { until: untilMs }) });
|
|
650
|
-
}
|
|
651
|
-
/** Handle POST /api/role-notes: the loop drawer's notebook Edit/Clear — the same saveRoleNote
|
|
652
|
-
* `tumwater role <id> --notes-file` / `--clear-notes` call, so the 4 KB cap and its wording
|
|
653
|
-
* cannot drift. `"text": ""` clears. The director has no notebook (400, the CLI's wording); a
|
|
654
|
-
* non-string text is a 400 before anything is written; an over-cap note is a 400 carrying
|
|
655
|
-
* saveRoleNote's byte-count message. */
|
|
656
|
-
export async function handleRoleNotes(req, res, root) {
|
|
657
|
-
const body = await readRoleBody(req, res, '{"role": "feature", "text": "…"}', root);
|
|
658
|
-
if (!body)
|
|
659
|
-
return;
|
|
660
|
-
const role = body.role;
|
|
661
|
-
if (role === DIRECTOR_ROLE)
|
|
662
|
-
return sendJson(res, 400, { error: "the director has no notebook" });
|
|
663
|
-
if (typeof body.text !== "string")
|
|
664
|
-
return sendJson(res, 400, { error: `"text" must be a string — the full replacement note, or "" to clear it${gotSuffix(body.text)}` });
|
|
665
|
-
try {
|
|
666
|
-
sendJson(res, 200, { ok: true, message: saveRoleNote(roleNotesPath(root, role), body.text) });
|
|
667
|
-
}
|
|
668
|
-
catch (err) {
|
|
669
|
-
sendJson(res, 400, { error: errorMessage(err) });
|
|
670
|
-
}
|
|
671
|
-
}
|
|
14
|
+
export { handlePrompt, handlePromptRole, handlePromptCancel, handlePromptEdit, handlePromptTop } from "./gui-prompt-commands.js";
|
|
15
|
+
export { handleQuestionAnswer, handleBacklogFile, handleBacklogCancel, handleBacklogRelease, handleBacklogTop, handleBacklogEdit } from "./gui-backlog-commands.js";
|
|
16
|
+
export { handleConfigSet, handleConfigUnset, handleBudget, handleRoleCap, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest } from "./gui-config-commands.js";
|
|
17
|
+
export { handlePause, handlePauseRole, handleWake, handleWakeCancel } from "./gui-team-commands.js";
|
|
18
|
+
export { handleResetCounters, handleRestart, handleReclaim, handleAbort, handleStopSchedule, handleRoleNotes, handleRetire, handleBriefEdit } from "./gui-operator-commands.js";
|