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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/**
|
|
3
|
+
* The dashboard's POST prompt-queueing command handlers (barrelled into
|
|
4
|
+
* gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts): prompt,
|
|
5
|
+
* prompt-role, prompt-cancel, prompt-edit, and prompt-top — the `checkedPromptImages` /
|
|
6
|
+
* `checkedPromptDefer` / `checkedPromptFields` validators they share ride with them. Each
|
|
7
|
+
* handler adapts HTTP onto the domain work one layer down (inbox-submit, inbox-cancel,
|
|
8
|
+
* inbox-edit, inbox-top, operator-intent) and touches no socket beyond its own `res`.
|
|
9
|
+
*/
|
|
10
|
+
import { submitPrompt } from "../inbox/inbox-submit.js";
|
|
11
|
+
import { imagePromptGuidance } from "../inbox/inbox-submit.js";
|
|
12
|
+
import { promptImagesProblem } from "../inbox/inbox-attachments.js";
|
|
13
|
+
import { promptPreview } from "../inbox/inbox.js";
|
|
14
|
+
import { cancelQueuedFile } from "../inbox/inbox-cancel.js";
|
|
15
|
+
import { editQueuedFile } from "../inbox/inbox-edit.js";
|
|
16
|
+
import { topQueuedFile } from "../inbox/inbox-top.js";
|
|
17
|
+
import { parseDurationMs } from "../cli/cli-args.js";
|
|
18
|
+
import { PAUSE_FOR_MAX_MS, submitRolePromptAndWake } from "../operator/operator-intent.js";
|
|
19
|
+
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
20
|
+
import { rejectBadQueueFileName, rejectBadRole, requirePromptText } from "./gui-args.js";
|
|
21
|
+
import { badRequest, OK_GONE, sendJson } from "./http-body.js";
|
|
22
|
+
import { errorMessage, gotSuffix } from "../text/text.js";
|
|
23
|
+
import { readPostBody, readRoleBody } from "./gui-endpoint-common.js";
|
|
24
|
+
/** Handle POST /api/prompt: queue a director prompt. An over-long prompt
|
|
25
|
+
* (DIRECTOR_PROMPT_MAX_CHARS) is a user-input error, not a server fault: the shared length
|
|
26
|
+
* rule answers 400 — an unexpected submit failure (EACCES) still reaches the server's outer
|
|
27
|
+
* catch as the 500 its gui-server-transport test pins. An optional images array (the composer's
|
|
28
|
+
* dropped/pasted attachments) is validated before anything is queued: a bad image — wrong
|
|
29
|
+
* extension, too many, undecodable, oversized — answers 400 naming the rule, with no queue
|
|
30
|
+
* file and no image written. */
|
|
31
|
+
export async function handlePrompt(req, res, root) {
|
|
32
|
+
const body = await readPostBody(req, res, '{"text": "..."}');
|
|
33
|
+
if (!body)
|
|
34
|
+
return;
|
|
35
|
+
const fields = checkedPromptFields(res, body);
|
|
36
|
+
if (!fields)
|
|
37
|
+
return;
|
|
38
|
+
submitPrompt(root, fields.text, fields.images, fields.notBeforeMs);
|
|
39
|
+
sendJson(res, 200, { ok: true });
|
|
40
|
+
}
|
|
41
|
+
/** The images field of a prompt endpoint's body, validated before anything touches the disk.
|
|
42
|
+
* Three outcomes, deliberately distinct: `undefined` — no images field, a text-only submit,
|
|
43
|
+
* continue without attachments; an array — the field was present and valid, attach it (a
|
|
44
|
+
* present-but-empty array is fine and saves nothing); `null` — the field was present and
|
|
45
|
+
* invalid, the 400 naming the rule is already sent, stop. The absent and invalid cases must
|
|
46
|
+
* not share a value: a text-only POST is the common path and must proceed. */
|
|
47
|
+
function checkedPromptImages(res, body) {
|
|
48
|
+
if (body.images === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
const problem = promptImagesProblem(body.images);
|
|
51
|
+
if (problem) {
|
|
52
|
+
badRequest(res, problem);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return body.images;
|
|
56
|
+
}
|
|
57
|
+
/** The defer field of a prompt endpoint's body, validated before anything touches the disk.
|
|
58
|
+
* Three outcomes, deliberately distinct like checkedPromptImages: `undefined` — no defer
|
|
59
|
+
* field, a prompt that runs as soon as its turn comes; a number — the epoch ms the prompt is
|
|
60
|
+
* deliverable from (`Date.now() + ms`); `null` — the field was present but not a duration
|
|
61
|
+
* this grammar accepts, or its duration is over the 90d cap, and the 400 naming the rule is
|
|
62
|
+
* already sent, so stop. The grammar is cli-args.ts's parseDurationMs — the same one
|
|
63
|
+
* `tumwater prompt --at` and `pause --for` accept — so a deferral spelled here computes the
|
|
64
|
+
* same `tumwater:not-before` marker the CLI writes. */
|
|
65
|
+
function checkedPromptDefer(res, body) {
|
|
66
|
+
if (body.defer === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
const ms = typeof body.defer === "string" ? parseDurationMs(body.defer) : null;
|
|
69
|
+
if (ms === null) {
|
|
70
|
+
badRequest(res, `defer needs a duration like 45s, 90m, 1h30m, or 2d${gotSuffix(body.defer)}`);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (ms > PAUSE_FOR_MAX_MS) {
|
|
74
|
+
badRequest(res, `defer is capped at 90d${gotSuffix(body.defer)}`);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return Date.now() + ms;
|
|
78
|
+
}
|
|
79
|
+
/** The prompt endpoints' shared body tail: requirePromptText (400 on a null return) then
|
|
80
|
+
* checkedPromptImages then checkedPromptDefer (400 on a null return from either), so
|
|
81
|
+
* /api/prompt and /api/prompt-role read the same three fields in the same order and a body
|
|
82
|
+
* wrong in several still answers the text problem first. Null means a 400 is already sent;
|
|
83
|
+
* callers keep their own `if (!fields) return` bail. */
|
|
84
|
+
function checkedPromptFields(res, body, role = DIRECTOR_ROLE) {
|
|
85
|
+
const text = requirePromptText(res, body, role);
|
|
86
|
+
if (text === null)
|
|
87
|
+
return null;
|
|
88
|
+
const images = checkedPromptImages(res, body);
|
|
89
|
+
if (images === null)
|
|
90
|
+
return null;
|
|
91
|
+
const notBeforeMs = checkedPromptDefer(res, body);
|
|
92
|
+
if (notBeforeMs === null)
|
|
93
|
+
return null;
|
|
94
|
+
return { text, images, notBeforeMs };
|
|
95
|
+
}
|
|
96
|
+
/** Handle POST /api/prompt-role: queue a prompt for one loop — the dashboard's per-row
|
|
97
|
+
* prompt affordance submits here, calling the same path `tumwater prompt --role <id>` uses
|
|
98
|
+
* (submitRolePrompt enqueues into that loop's own queue and logs under it, then a single-role
|
|
99
|
+
* wake brings the live loop in within one poll; the wake message rides back for the flash).
|
|
100
|
+
* The role validates exactly like /api/transcript (the shared rejectBadRole wording), and the
|
|
101
|
+
* body discipline is /api/prompt's: readJsonObject → 400 malformed/non-object, 413 oversized,
|
|
102
|
+
* text a non-empty string within the shared length rule → 400 otherwise — plus the same
|
|
103
|
+
* optional images array and its validate-before-anything-is-written discipline. */
|
|
104
|
+
export async function handlePromptRole(req, res, root) {
|
|
105
|
+
const body = await readRoleBody(req, res, '{"role": "feature", "text": "..."}', root);
|
|
106
|
+
if (!body)
|
|
107
|
+
return;
|
|
108
|
+
const role = body.role;
|
|
109
|
+
const fields = checkedPromptFields(res, body, role);
|
|
110
|
+
if (!fields)
|
|
111
|
+
return;
|
|
112
|
+
// Image-input submit gate (plans/image-input.md "Attachments", part 5/5): an attachment the
|
|
113
|
+
// target loop cannot read (and that no tier reroutes) answers 400 naming the fix before
|
|
114
|
+
// anything is queued — the same gate submitRolePrompt enforces, surfaced as a user-input
|
|
115
|
+
// error rather than the server's 500. A loop that reroutes a blind read is not refused; its
|
|
116
|
+
// notice rides in the confirmation the composer flashes.
|
|
117
|
+
let notice = "";
|
|
118
|
+
if (fields.images && fields.images.length > 0) {
|
|
119
|
+
const guidance = imagePromptGuidance(root, role);
|
|
120
|
+
if (guidance.refusal) {
|
|
121
|
+
badRequest(res, guidance.refusal);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
notice = guidance.notice ?? "";
|
|
125
|
+
}
|
|
126
|
+
const message = [
|
|
127
|
+
submitRolePromptAndWake(root, role, fields.text, fields.images, fields.notBeforeMs),
|
|
128
|
+
notice,
|
|
129
|
+
]
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.join("\n");
|
|
132
|
+
sendJson(res, 200, { ok: true, message });
|
|
133
|
+
}
|
|
134
|
+
/** Handle POST /api/prompt-cancel: retract one queued prompt addressed by its queue file —
|
|
135
|
+
* the dashboard's per-row cancel affordance, the file-addressed twin of the CLI's
|
|
136
|
+
* position-addressed `prompt --cancel`. The role is optional (absent cancels from the
|
|
137
|
+
* director's queue) and validates exactly like /api/transcript; a file name failing the
|
|
138
|
+
* shared rejectBadQueueFileName guard is a user-input error answered 400 with
|
|
139
|
+
* nothing touched on disk. A vanished file answers 200 { status: "gone" } — the loop already
|
|
140
|
+
* dequeued the prompt, which is data for the flash line, not a server fault. Same body
|
|
141
|
+
* discipline as /api/prompt (readJsonObject → 400 malformed/non-object, 413 oversized). */
|
|
142
|
+
export async function handlePromptCancel(req, res, root) {
|
|
143
|
+
const body = await readPostBody(req, res, '{"role": "feature", "file": "<stamp>-<seq>-<pid>.md"}');
|
|
144
|
+
if (!body)
|
|
145
|
+
return;
|
|
146
|
+
if (body.role !== undefined && rejectBadRole(root, res, body.role))
|
|
147
|
+
return;
|
|
148
|
+
const role = body.role === undefined ? DIRECTOR_ROLE : body.role;
|
|
149
|
+
if (rejectBadQueueFileName(res, body.file))
|
|
150
|
+
return;
|
|
151
|
+
const outcome = cancelQueuedFile(root, role, body.file);
|
|
152
|
+
sendJson(res, 200, outcome.status === "cancelled"
|
|
153
|
+
? { ok: true, status: "cancelled", preview: promptPreview(outcome.text) }
|
|
154
|
+
: OK_GONE);
|
|
155
|
+
}
|
|
156
|
+
/** Handle POST /api/prompt-edit: rewrite one queued prompt addressed by its queue file — the
|
|
157
|
+
* dashboard's per-row edit affordance, the file-addressed twin of the CLI's position-addressed
|
|
158
|
+
* `prompt --edit`. The role is optional (absent edits the director's queue) and validates
|
|
159
|
+
* exactly like /api/prompt-cancel; a file name failing the shared rejectBadQueueFileName
|
|
160
|
+
* guard is a user-input error answered 400 with nothing touched on disk. Text
|
|
161
|
+
* and images validate through the same checkedPromptFields /api/prompt and /api/prompt-role
|
|
162
|
+
* use; editQueuedFile throws for a bad name or an over-cap text — both user-input errors
|
|
163
|
+
* answered 400 (a filesystem failure reads as 400 like handleConfigSet's failed write). A
|
|
164
|
+
* vanished file answers 200 { ok: true, status: "gone" } — the loop already dequeued the
|
|
165
|
+
* prompt, which is data for the flash line, not a server error; an edit answers 200 with the
|
|
166
|
+
* old and new operator-facing text the client flashes. Same body discipline as /api/prompt
|
|
167
|
+
* (readJsonObject → 400 malformed/non-object, 413 oversized). */
|
|
168
|
+
export async function handlePromptEdit(req, res, root) {
|
|
169
|
+
const body = await readPostBody(req, res, '{"role": "qa", "file": "<stamp>.md", "text": "..."}');
|
|
170
|
+
if (!body)
|
|
171
|
+
return;
|
|
172
|
+
if (body.role !== undefined && rejectBadRole(root, res, body.role))
|
|
173
|
+
return;
|
|
174
|
+
const role = body.role === undefined ? DIRECTOR_ROLE : body.role;
|
|
175
|
+
if (rejectBadQueueFileName(res, body.file))
|
|
176
|
+
return;
|
|
177
|
+
const fields = checkedPromptFields(res, body, role);
|
|
178
|
+
if (!fields)
|
|
179
|
+
return;
|
|
180
|
+
// Image-input gate (plans/image-input.md "Attachments", part 5/5), the edit twin of
|
|
181
|
+
// handlePromptRole's: an image edited into the queued prompt answers 400 naming the fix
|
|
182
|
+
// when the target loop cannot read it (and no tier reroutes it) — the same gate
|
|
183
|
+
// submitRolePrompt enforces — before anything is written.
|
|
184
|
+
if (fields.images && fields.images.length > 0) {
|
|
185
|
+
const guidance = imagePromptGuidance(root, role);
|
|
186
|
+
if (guidance.refusal) {
|
|
187
|
+
badRequest(res, guidance.refusal);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
let outcome;
|
|
192
|
+
try {
|
|
193
|
+
outcome = editQueuedFile(root, role, body.file, fields.text, fields.images);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
badRequest(res, errorMessage(err));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
sendJson(res, 200, outcome.status === "edited"
|
|
200
|
+
? { ok: true, status: "edited", oldText: outcome.oldText, newText: outcome.newText }
|
|
201
|
+
: OK_GONE);
|
|
202
|
+
}
|
|
203
|
+
/** Handle POST /api/prompt-top: move one queued prompt to the head of its role's queue,
|
|
204
|
+
* addressed by its queue file — the dashboard's per-row Move to top affordance, the
|
|
205
|
+
* file-addressed twin of the CLI's position-addressed `prompt --top`. The role is optional
|
|
206
|
+
* (absent moves in the director's queue) and validates exactly like /api/prompt-cancel; a
|
|
207
|
+
* file name failing the shared rejectBadQueueFileName guard is a user-input error answered
|
|
208
|
+
* 400 with nothing touched on disk. A vanished file answers 200 { status: "gone" } and the
|
|
209
|
+
* queue head answers 200 { status: "already_first" } — both data for the flash line, not
|
|
210
|
+
* server faults, so a stale poll can never reorder the wrong prompt. Same body discipline
|
|
211
|
+
* as /api/prompt (readJsonObject → 400 malformed/non-object, 413 oversized). */
|
|
212
|
+
export async function handlePromptTop(req, res, root) {
|
|
213
|
+
const body = await readPostBody(req, res, '{"role": "qa", "file": "<stamp>.md"}');
|
|
214
|
+
if (!body)
|
|
215
|
+
return;
|
|
216
|
+
if (body.role !== undefined && rejectBadRole(root, res, body.role))
|
|
217
|
+
return;
|
|
218
|
+
const role = body.role === undefined ? DIRECTOR_ROLE : body.role;
|
|
219
|
+
if (rejectBadQueueFileName(res, body.file))
|
|
220
|
+
return;
|
|
221
|
+
let outcome;
|
|
222
|
+
try {
|
|
223
|
+
outcome = topQueuedFile(root, role, body.file);
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
badRequest(res, errorMessage(err));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
sendJson(res, 200, outcome.status === "topped"
|
|
230
|
+
? { ok: true, status: "topped", preview: outcome.preview }
|
|
231
|
+
: outcome.status === "already_first"
|
|
232
|
+
? { ok: true, status: "already_first" }
|
|
233
|
+
: OK_GONE);
|
|
234
|
+
}
|
|
@@ -8,10 +8,10 @@ import http from "node:http";
|
|
|
8
8
|
import { GUI_PAGE } from "../ui/gui/gui-page.js";
|
|
9
9
|
import { statusPayload } from "../ui/status-payload.js";
|
|
10
10
|
import { captureStartupBuild, createReloadWatch, reexecSelf, watchReloadSupervisor, } from "../redeploy/self-reload.js";
|
|
11
|
-
import { warnEvent } from "../events/events.js";
|
|
11
|
+
import { warnEvent, TUMWATER_SOURCE } from "../events/events.js";
|
|
12
12
|
import { errorMessage } from "../text/text.js";
|
|
13
|
-
import { handleBacklog, handleConfig, handleDiff, handleFailures, handleReport, handleHistory, handleLogs, handlePromptText, handleTick, handleRole, handleTranscript, handleDoctor, } from "./gui-endpoints.js";
|
|
14
|
-
import { handleAbort, handleBudget, handleConfigSet, handleConfigUnset, handlePause, handlePauseRole, handlePrompt, handleBacklogFile, handleBacklogCancel, handleBacklogEdit, handlePromptCancel, handlePromptEdit, handlePromptRole, handleQuestionAnswer, handleReclaim, handleRestart, handleResetCounters, handleRoleNotes, handleWake, handleWakeCancel, } from "./gui-endpoint-commands.js";
|
|
13
|
+
import { handleBacklog, handleBrief, handleConfig, handleDiff, handleFailures, handleReport, handleHistory, handleLogs, handlePromptText, handleTick, handleRole, handleTranscript, handleDoctor, handleRetireList, } from "./gui-endpoints.js";
|
|
14
|
+
import { handleAbort, handleBriefEdit, handleBudget, handleConfigSet, handleConfigUnset, handleRoleCap, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest, handlePause, handlePauseRole, handlePrompt, handleBacklogFile, handleBacklogCancel, handleBacklogRelease, handleBacklogTop, handleBacklogEdit, handlePromptCancel, handlePromptEdit, handlePromptRole, handlePromptTop, handleQuestionAnswer, handleReclaim, handleRestart, handleStopSchedule, handleResetCounters, handleRoleNotes, handleRetire, handleWake, handleWakeCancel, } from "./gui-endpoint-commands.js";
|
|
15
15
|
import { sendJson } from "./http-body.js";
|
|
16
16
|
/** Constant-time credential comparison for the shared-token gate: `timingSafeEqual` throws
|
|
17
17
|
* on unequal lengths, so the length equality is the guard. A naive `===` string compare
|
|
@@ -50,7 +50,10 @@ function crossOriginRequest(req) {
|
|
|
50
50
|
const raw = req.headers.origin;
|
|
51
51
|
if (raw === undefined)
|
|
52
52
|
return false; // no Origin: not a browser — allow
|
|
53
|
-
|
|
53
|
+
// A `typeof` guard narrows to a concrete `string[]` (so `raw[0]` is `string | undefined`
|
|
54
|
+
// under noUncheckedIndexedAccess) instead of `Array.isArray`'s `any[]`, whose `[0]` is `any`;
|
|
55
|
+
// an empty array would be undefined either way, so `?? ""` only spells the first value out.
|
|
56
|
+
const origin = typeof raw === "string" ? raw : (raw[0] ?? "");
|
|
54
57
|
if (origin === "null")
|
|
55
58
|
return true;
|
|
56
59
|
try {
|
|
@@ -99,7 +102,7 @@ export function loopbackHostAllowed(hostHeader) {
|
|
|
99
102
|
* handler's purpose. The warning is best-effort, the same policy the cleanup warn sites apply. */
|
|
100
103
|
function recordServerError(root, err) {
|
|
101
104
|
try {
|
|
102
|
-
warnEvent(root,
|
|
105
|
+
warnEvent(root, TUMWATER_SOURCE, `dashboard server error: ${errorMessage(err)}`);
|
|
103
106
|
}
|
|
104
107
|
catch {
|
|
105
108
|
// The log is another process's to fix; the dashboard stays up either way.
|
|
@@ -107,7 +110,7 @@ function recordServerError(root, err) {
|
|
|
107
110
|
}
|
|
108
111
|
/** Start the dashboard server. Binds to 127.0.0.1 by default; with `allInterfaces` it
|
|
109
112
|
* binds the unspecified address (every interface, IPv4 and IPv6), making the dashboard —
|
|
110
|
-
* including the director prompt box, which anyone reaching it can use to steer the
|
|
113
|
+
* including the director prompt box, which anyone reaching it can use to steer the team —
|
|
111
114
|
* available to the whole network. There is no authentication by default — exposing it is
|
|
112
115
|
* the caller's deliberate choice — and an optional shared token (`gui --token <secret>`,
|
|
113
116
|
* passed as `token`) gates every route: requests must carry it as `Authorization: Bearer
|
|
@@ -145,29 +148,46 @@ export function startGui(root, port, allInterfaces = false, token = "", watch =
|
|
|
145
148
|
"GET /api/logs": (_req, res, target) => handleLogs(target.searchParams, res, root),
|
|
146
149
|
"GET /api/transcript": (_req, res, target) => handleTranscript(target.searchParams, res, root),
|
|
147
150
|
"GET /api/prompt-text": (_req, res, target) => handlePromptText(target.searchParams, res, root),
|
|
148
|
-
"GET /api/
|
|
151
|
+
"GET /api/run": (_req, res, target) => handleTick(target.searchParams, res, root),
|
|
149
152
|
"GET /api/role": (_req, res, target) => handleRole(target.searchParams, res, root),
|
|
150
153
|
"GET /api/diff": (_req, res, target) => handleDiff(target.searchParams, res, root),
|
|
151
154
|
"GET /api/backlog": (_req, res, target) => handleBacklog(target.searchParams, res, root),
|
|
152
155
|
"GET /api/config": (_req, res) => handleConfig(res, root),
|
|
156
|
+
"GET /api/brief": (_req, res) => handleBrief(res, root),
|
|
153
157
|
"GET /api/doctor": (_req, res) => handleDoctor(res, root),
|
|
158
|
+
"GET /api/retire": (_req, res) => handleRetireList(res, root),
|
|
159
|
+
"POST /api/brief": (req, res) => handleBriefEdit(req, res, root),
|
|
154
160
|
"POST /api/config-set": (req, res) => handleConfigSet(req, res, root),
|
|
155
161
|
"POST /api/config-unset": (req, res) => handleConfigUnset(req, res, root),
|
|
162
|
+
"POST /api/notify-test": (req, res) => handleNotifyTest(req, res, root),
|
|
156
163
|
"POST /api/prompt": (req, res) => handlePrompt(req, res, root),
|
|
157
164
|
"POST /api/prompt-role": (req, res) => handlePromptRole(req, res, root),
|
|
158
165
|
"POST /api/backlog-file": (req, res) => handleBacklogFile(req, res, root),
|
|
159
166
|
"POST /api/backlog-cancel": (req, res) => handleBacklogCancel(req, res, root),
|
|
167
|
+
"POST /api/backlog-release": (req, res) => handleBacklogRelease(req, res, root),
|
|
168
|
+
"POST /api/backlog-top": (req, res) => handleBacklogTop(req, res, root),
|
|
160
169
|
"POST /api/backlog-edit": (req, res) => handleBacklogEdit(req, res, root),
|
|
161
170
|
"POST /api/prompt-cancel": (req, res) => handlePromptCancel(req, res, root),
|
|
171
|
+
"POST /api/prompt-top": (req, res) => handlePromptTop(req, res, root),
|
|
162
172
|
"POST /api/prompt-edit": (req, res) => handlePromptEdit(req, res, root),
|
|
163
173
|
"POST /api/question-answer": (req, res) => handleQuestionAnswer(req, res, root),
|
|
164
174
|
"POST /api/budget": (req, res) => handleBudget(req, res, root),
|
|
175
|
+
"POST /api/role-cap": (req, res) => handleRoleCap(req, res, root),
|
|
176
|
+
"POST /api/role-enable": (req, res) => handleRoleEnable(req, res, root),
|
|
177
|
+
"POST /api/role-disable": (req, res) => handleRoleDisable(req, res, root),
|
|
178
|
+
"POST /api/role-interval": (req, res) => handleRoleInterval(req, res, root),
|
|
179
|
+
"POST /api/role-instances": (req, res) => handleRoleInstances(req, res, root),
|
|
180
|
+
"POST /api/role-instructions": (req, res) => handleRoleInstructions(req, res, root),
|
|
181
|
+
"POST /api/role-model": (req, res) => handleRoleModel(req, res, root),
|
|
182
|
+
"POST /api/role-quiet-hours": (req, res) => handleRoleQuietHours(req, res, root),
|
|
165
183
|
"POST /api/pause": (req, res) => handlePause(req, res, root),
|
|
166
184
|
"POST /api/wake": (req, res) => handleWake(req, res, root),
|
|
167
185
|
"POST /api/wake-cancel": (req, res) => handleWakeCancel(req, res, root),
|
|
168
186
|
"POST /api/reset-counters": (req, res) => handleResetCounters(req, res, root),
|
|
169
187
|
"POST /api/role-notes": (req, res) => handleRoleNotes(req, res, root),
|
|
188
|
+
"POST /api/retire": (req, res) => handleRetire(req, res, root),
|
|
170
189
|
"POST /api/restart": (req, res) => handleRestart(req, res, root),
|
|
190
|
+
"POST /api/stop-schedule": (req, res) => handleStopSchedule(req, res, root),
|
|
171
191
|
"POST /api/reclaim": (req, res) => handleReclaim(req, res, root),
|
|
172
192
|
"POST /api/abort": (req, res) => handleAbort(req, res, root),
|
|
173
193
|
"POST /api/pause-role": (req, res) => handlePauseRole(req, res, root),
|
|
@@ -207,7 +227,7 @@ export function startGui(root, port, allInterfaces = false, token = "", watch =
|
|
|
207
227
|
// Cross-origin gate, ahead of every state-changing route: a hostile webpage can POST
|
|
208
228
|
// to the open localhost server with a CORS-safelisted text/plain body (no preflight,
|
|
209
229
|
// no readable response) and the side effect alone is the payload — a forged director
|
|
210
|
-
// prompt, a
|
|
230
|
+
// prompt, a team pause, an abort. Browsers mark every cross-site POST with an Origin
|
|
211
231
|
// naming the hostile server, so one header check here refuses the forgery while the
|
|
212
232
|
// dashboard's own POSTs (same-host Origin) and non-browser clients (no Origin) pass.
|
|
213
233
|
if (req.method === "POST" && crossOriginRequest(req)) {
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dashboard's POST team-pause/wake command handlers (barrelled into
|
|
3
|
+
* gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts): pause,
|
|
4
|
+
* pause-role, wake, and wake-cancel — with the `pauseForSecondsMs` / `pauseReasonField` /
|
|
5
|
+
* `wakeInSecondsMs` body rules they share. Each handler adapts HTTP onto the domain work one
|
|
6
|
+
* layer down (team/team-state, operator-intent) and touches no socket beyond its own `res`.
|
|
7
|
+
*/
|
|
8
|
+
import { baseRoleOf, INSTANCE_ROLES } from "../roles/loop-ids.js";
|
|
9
|
+
import { normalizePauseReason, pauseTeam, pauseRole, resumeTeam, resumeRole } from "../team/team-state.js";
|
|
10
|
+
import { cancelWakes, PAUSE_FOR_MAX_MS, requestWake } from "../operator/operator-intent.js";
|
|
11
|
+
import { requirePausedFlag, validRoleIds } from "./gui-args.js";
|
|
12
|
+
import { badRequest, sendJson } from "./http-body.js";
|
|
13
|
+
import { gotSuffix } from "../text/text.js";
|
|
14
|
+
import { readPostBody, readRoleBody } from "./gui-endpoint-common.js";
|
|
15
|
+
/** The shared optional-seconds rule behind the timed pause's `forSeconds` and the scheduled
|
|
16
|
+
* wake/stop's `inSeconds`: the field is absent (undefined) or a positive finite number of
|
|
17
|
+
* seconds within the CLI's 90-day cap — the two parallel validators used to restate this
|
|
18
|
+
* smell, one per field name. Its null return means the 400 is already sent and the caller must
|
|
19
|
+
* stop; its number return is the delay in milliseconds, which the timed-pause callers add
|
|
20
|
+
* their own clock to (the marker's `until` deadline) and the wake/stop-schedule callers use
|
|
21
|
+
* directly. `capHint` rides after the cap message for a field whose capped form earns an extra
|
|
22
|
+
* pointer (pause's standing-pause note); the messages name the field so pause (`forSeconds`)
|
|
23
|
+
* and wake/stop-schedule (`inSeconds`) each read correctly. */
|
|
24
|
+
function boundedSecondsMs(res, body, field, capHint = "") {
|
|
25
|
+
const seconds = body[field];
|
|
26
|
+
if (seconds === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) {
|
|
29
|
+
badRequest(res, `${field} must be a positive number of seconds${gotSuffix(seconds)}`);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (seconds * 1000 > PAUSE_FOR_MAX_MS) {
|
|
33
|
+
badRequest(res, `${field} is capped at ${PAUSE_FOR_MAX_MS / 1000} seconds (90 days)${gotSuffix(seconds)}${capHint}`);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return Math.round(seconds * 1000);
|
|
37
|
+
}
|
|
38
|
+
/** The shared `forSeconds` rule of /api/pause and /api/pause-role: undefined when absent, the
|
|
39
|
+
* deadline in epoch milliseconds when valid, null after sending the 400 (only with
|
|
40
|
+
* `paused: true`, a positive finite number, within the CLI's 90-day cap — the numeric body
|
|
41
|
+
* rides boundedSecondsMs). */
|
|
42
|
+
function pauseForSecondsMs(res, body, pausing) {
|
|
43
|
+
const forSeconds = body.forSeconds;
|
|
44
|
+
if (forSeconds === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (!pausing) {
|
|
47
|
+
badRequest(res, "forSeconds only applies when pausing (paused: true)");
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const ms = boundedSecondsMs(res, body, "forSeconds", " — pause without it for a standing pause");
|
|
51
|
+
return ms === null || ms === undefined ? ms : Date.now() + ms;
|
|
52
|
+
}
|
|
53
|
+
/** The optional `reason` of /api/pause: undefined when absent or blank, the raw string when
|
|
54
|
+
* given with `paused: true` (pauseTeam folds and caps it), null after sending the 400. */
|
|
55
|
+
function pauseReasonField(res, body, pausing) {
|
|
56
|
+
const reason = body.reason;
|
|
57
|
+
if (reason === undefined)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (!pausing) {
|
|
60
|
+
badRequest(res, "reason only applies when pausing (paused: true)");
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (typeof reason !== "string") {
|
|
64
|
+
badRequest(res, `reason must be a string${gotSuffix(reason)}`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return reason.trim() === "" ? undefined : reason;
|
|
68
|
+
}
|
|
69
|
+
/** Handle POST /api/pause: the dashboard's pause control — the same operator gate
|
|
70
|
+
* `tumwater pause [--for <duration>]` and `resume` write, via team/team-state.ts's shared writers
|
|
71
|
+
* (pauseTeam/resumeTeam) so the CLI and the GUI cannot drift on the marker's format or
|
|
72
|
+
* idempotence. Same body discipline as /api/prompt and /api/budget (readJsonObject → 400
|
|
73
|
+
* malformed/non-object, 413 oversized); the target state is explicit (`paused: true|false`)
|
|
74
|
+
* rather than a toggle, so a retried request is idempotent. An optional `forSeconds` makes it
|
|
75
|
+
* a timed pause (the marker's `until`, capped like the CLI's `--for`); it only accompanies
|
|
76
|
+
* `paused: true`, as does an optional `reason` (the CLI's `pause --reason`, stored folded and
|
|
77
|
+
* capped by pauseTeam; the 200 body echoes what was stored). */
|
|
78
|
+
export async function handlePause(req, res, root) {
|
|
79
|
+
const body = await readPostBody(req, res, '{"paused": true, "forSeconds": 3600}');
|
|
80
|
+
if (!body)
|
|
81
|
+
return;
|
|
82
|
+
const value = requirePausedFlag(res, body);
|
|
83
|
+
if (value === null)
|
|
84
|
+
return;
|
|
85
|
+
const untilMs = pauseForSecondsMs(res, body, value);
|
|
86
|
+
if (untilMs === null)
|
|
87
|
+
return;
|
|
88
|
+
const reason = pauseReasonField(res, body, value);
|
|
89
|
+
if (reason === null)
|
|
90
|
+
return;
|
|
91
|
+
if (value)
|
|
92
|
+
pauseTeam(root, untilMs, reason);
|
|
93
|
+
else
|
|
94
|
+
resumeTeam(root);
|
|
95
|
+
const stored = normalizePauseReason(reason);
|
|
96
|
+
sendJson(res, 200, { ok: true, paused: value, ...(untilMs === undefined ? {} : { until: untilMs }), ...(stored ? { reason: stored } : {}) });
|
|
97
|
+
}
|
|
98
|
+
/** The `inSeconds` rule of /api/wake: undefined when absent, the delay in milliseconds when
|
|
99
|
+
* valid, null after sending the 400 (a positive finite number within the CLI's 90-day cap).
|
|
100
|
+
* A duration, not a deadline: requestWake takes `inMs` and reads its own clock. */
|
|
101
|
+
export function wakeInSecondsMs(res, body) {
|
|
102
|
+
return boundedSecondsMs(res, body, "inSeconds");
|
|
103
|
+
}
|
|
104
|
+
/** Handle POST /api/wake: the dashboard's per-row wake control — the same marker-writing
|
|
105
|
+
* core `tumwater wake` calls (requestWake), so the CLI and the GUI cannot drift on the
|
|
106
|
+
* state-file edits or the marker. `{}`/a missing role targets every configured role (the
|
|
107
|
+
* CLI's all-roles default); a given role validates exactly like /api/transcript. Same body
|
|
108
|
+
* discipline as /api/pause (readJsonObject → 400 malformed/non-object, 413 oversized). An
|
|
109
|
+
* optional `inSeconds` schedules the wake for later, like `tumwater wake --in` (same
|
|
110
|
+
* 90-day cap). */
|
|
111
|
+
export async function handleWake(req, res, root) {
|
|
112
|
+
const body = await readRoleBody(req, res, '{"role": "feature"}', root, true);
|
|
113
|
+
if (!body)
|
|
114
|
+
return;
|
|
115
|
+
const inMs = wakeInSecondsMs(res, body);
|
|
116
|
+
if (inMs === null)
|
|
117
|
+
return;
|
|
118
|
+
sendJson(res, 200, {
|
|
119
|
+
ok: true,
|
|
120
|
+
message: requestWake(root, body.role === undefined ? validRoleIds(root) : [body.role], inMs),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Handle POST /api/wake-cancel: the dashboard's cancel of pending scheduled wakes — the same
|
|
124
|
+
* core `tumwater wake --cancel` calls (cancelWakes). A missing role cancels every pending wake;
|
|
125
|
+
* a given role validates like /api/wake and drops only its share. */
|
|
126
|
+
export async function handleWakeCancel(req, res, root) {
|
|
127
|
+
const body = await readRoleBody(req, res, '{"role": "feature"}', root, true);
|
|
128
|
+
if (!body)
|
|
129
|
+
return;
|
|
130
|
+
sendJson(res, 200, { ok: true, message: cancelWakes(root, body.role) });
|
|
131
|
+
}
|
|
132
|
+
/** Resume a row: lift every marker key that pauses this loop id (its own, its base role's, and
|
|
133
|
+
* instance 1's `<role>-1`), since the row shows Resume whenever `loopPaused` holds and the key
|
|
134
|
+
* actually in the marker may differ from the row's id. Reports whether any key was lifted. */
|
|
135
|
+
function resumeLoopKeys(root, loopId) {
|
|
136
|
+
const keys = new Set([loopId, baseRoleOf(loopId), ...(INSTANCE_ROLES.has(loopId) ? [`${loopId}-1`] : [])]);
|
|
137
|
+
let changed = false;
|
|
138
|
+
for (const key of keys)
|
|
139
|
+
changed = resumeRole(root, key) || changed;
|
|
140
|
+
return changed;
|
|
141
|
+
}
|
|
142
|
+
/** Handle POST /api/pause-role: the dashboard's per-row pause/resume control — the same
|
|
143
|
+
* marker-writing core `tumwater pause --role` / `resume --role` call (pauseRole/resumeRole),
|
|
144
|
+
* so the CLI and the GUI cannot drift on the marker's format or idempotence. The target state
|
|
145
|
+
* is explicit (`paused: true|false`) like /api/pause, so a retried request is idempotent; the
|
|
146
|
+
* team-wide pause composes freely — its gate is checked first at scheduling, and the role
|
|
147
|
+
* marker still records the operator's per-row intent. Same body discipline as /api/wake
|
|
148
|
+
* (readJsonObject → 400 malformed/non-object, 413 oversized; the role validates through the
|
|
149
|
+
* shared rejectBadRole wording). */
|
|
150
|
+
export async function handlePauseRole(req, res, root) {
|
|
151
|
+
const body = await readRoleBody(req, res, '{"role": "feature", "paused": true}', root);
|
|
152
|
+
if (!body)
|
|
153
|
+
return;
|
|
154
|
+
const value = requirePausedFlag(res, body);
|
|
155
|
+
if (value === null)
|
|
156
|
+
return;
|
|
157
|
+
const untilMs = pauseForSecondsMs(res, body, value);
|
|
158
|
+
if (untilMs === null)
|
|
159
|
+
return;
|
|
160
|
+
const changed = value
|
|
161
|
+
? pauseRole(root, body.role, untilMs)
|
|
162
|
+
: resumeLoopKeys(root, body.role);
|
|
163
|
+
sendJson(res, 200, { ok: true, changed, paused: value, ...(untilMs === undefined ? {} : { until: untilMs }) });
|
|
164
|
+
}
|
|
@@ -6,6 +6,22 @@ export function sendJson(res, status, body) {
|
|
|
6
6
|
res.writeHead(status, { "content-type": "application/json" });
|
|
7
7
|
res.end(JSON.stringify(body));
|
|
8
8
|
}
|
|
9
|
+
/** Send a 400 Bad Request carrying a single client-error message — the one shape every /api
|
|
10
|
+
* handler's validation/authoring failure replies with ({ error: "..." }), so the status and
|
|
11
|
+
* the `{ error }` wrapper live here instead of being open-coded at each call site. */
|
|
12
|
+
export function badRequest(res, message) {
|
|
13
|
+
sendJson(res, 400, { error: message });
|
|
14
|
+
}
|
|
15
|
+
/** Send 200 with the {"ok":true,"status":"gone"} body every file-addressed handler that answers
|
|
16
|
+
* for a vanished entry (the loop already dequeued it) replies with — data for the UI's flash
|
|
17
|
+
* line, not a server fault — so the status and the `{ ok, status }` wrapper live here instead
|
|
18
|
+
* of being open-coded at each call site. */
|
|
19
|
+
export function okGone(res) {
|
|
20
|
+
sendJson(res, 200, OK_GONE);
|
|
21
|
+
}
|
|
22
|
+
/** The {"ok":true,"status":"gone"} body okGone sends, also exposed for the couple of handlers
|
|
23
|
+
* that wrap it as one branch of a larger 200 reply (prompt-cancel/prompt-edit). */
|
|
24
|
+
export const OK_GONE = { ok: true, status: "gone" };
|
|
9
25
|
/** Max request body for the /api POST endpoints, in wire bytes. Sized for the prompt
|
|
10
26
|
* endpoints' image attachments: 4 images × 5 MiB × 4/3 base64 expansion ≈ 27 MiB must fit one
|
|
11
27
|
* POST, with headroom — the cap exists to bound memory on a local dashboard, not to be tight.
|
|
@@ -130,7 +146,7 @@ export async function readJsonObject(req, res, example) {
|
|
|
130
146
|
// because it sends a different status.
|
|
131
147
|
const parsed = parseJsonObject(body);
|
|
132
148
|
if (!parsed) {
|
|
133
|
-
|
|
149
|
+
badRequest(res, `body must be a JSON object like ${example}`);
|
|
134
150
|
return null;
|
|
135
151
|
}
|
|
136
152
|
return parsed;
|