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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request-body helpers shared by the dashboard's per-domain POST command modules
|
|
3
|
+
* (gui-prompt-commands, gui-backlog-commands, gui-config-commands, gui-team-commands, and
|
|
4
|
+
* gui-operator-commands, all barrelled into gui/gui-endpoint-commands.ts): readPostBody /
|
|
5
|
+
* readRoleBody — the body-discipline-and-role-check pair each mutating handler guards its
|
|
6
|
+
* inputs with — and sendRequestResult, the shared boolean-claiming marker reply. The
|
|
7
|
+
* per-domain `checked*` validators live with their domain's module, not here.
|
|
8
|
+
*/
|
|
9
|
+
import { rejectBadRole } from "./gui-args.js";
|
|
10
|
+
import { readJsonObject, sendJson } from "./http-body.js";
|
|
11
|
+
/** Every POST handler's shared opening: readJsonObject reads the body and — when it is
|
|
12
|
+
* oversized, malformed, or not a JSON object — has already written the 413/400 itself. Its
|
|
13
|
+
* null return therefore means the response is sent and the handler must stop without touching
|
|
14
|
+
* it again; each handler used to restate that contract in its own inline comment, and this is
|
|
15
|
+
* the one home for it. Handlers keep their own `if (!body) return` bail so the stop stays
|
|
16
|
+
* visible in their flow. */
|
|
17
|
+
export async function readPostBody(req, res, example) {
|
|
18
|
+
const body = await readJsonObject(req, res, example);
|
|
19
|
+
if (body === null)
|
|
20
|
+
return null; // 4xx already sent — oversized or not a JSON object
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
/** The role-targeting POST handlers' shared prologue: readPostBody (400/413 sent on a null
|
|
24
|
+
* return) plus rejectBadRole's shared role validation (400 sent on a null return), so the
|
|
25
|
+
* wake/abort/prompt-role/pause-role family states the body-discipline-and-role-check pairing
|
|
26
|
+
* once instead of two guard lines per handler. Its null return means the response is already
|
|
27
|
+
* written and the handler must stop; callers keep their own `if (!body) return` bail. Exactly
|
|
28
|
+
* seven call sites — handlePromptRole, handleWake, handleWakeCancel and handleResetCounters
|
|
29
|
+
* (allowMissing, the {} → all-roles default), handleAbort, handlePauseRole, and
|
|
30
|
+
* handleRoleNotes; handlePromptCancel deliberately does not ride it, since its role is
|
|
31
|
+
* optional and validates only when present. */
|
|
32
|
+
export async function readRoleBody(req, res, example, root, allowMissing = false) {
|
|
33
|
+
const body = await readPostBody(req, res, example);
|
|
34
|
+
if (body === null)
|
|
35
|
+
return null; // 4xx already sent — oversized, malformed, or not an object
|
|
36
|
+
if (rejectBadRole(root, res, body.role, allowMissing))
|
|
37
|
+
return null; // 400 already sent
|
|
38
|
+
return body;
|
|
39
|
+
}
|
|
40
|
+
/** Reply for a boolean-claiming marker request (RequestResult): success maps to 200 with its
|
|
41
|
+
* message, refusal to 409 — the request is valid but nothing can act on it, the same conflict
|
|
42
|
+
* reading requestAbort's not-live error uses. /api/restart and /api/abort share it. */
|
|
43
|
+
export function sendRequestResult(res, result) {
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
sendJson(res, 409, { error: result.error });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
sendJson(res, 200, { ok: true, message: result.message });
|
|
49
|
+
}
|
|
@@ -1,26 +1,42 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The dashboard's GET data endpoint handlers (src/gui/gui-server.ts routes to them): transcript,
|
|
3
|
+
* prompt-text, backlog, report, failures, history, tick, diff, config — the read-only surface.
|
|
4
|
+
* The POST
|
|
5
|
+
* operator endpoints (prompt, prompt-role, prompt-cancel, prompt-edit, budget, config-set, pause,
|
|
6
|
+
* wake, restart, abort, pause-role) and their body-discipline helpers live in
|
|
7
|
+
* gui/gui-endpoint-commands.ts. Each handler answers its request and touches no socket beyond
|
|
8
|
+
* its own `res`; server lifecycle, routing, the static page, and the token gate stay in
|
|
9
|
+
* gui/gui-server.ts. The domain work itself lives one layer down (transcript.ts, backlog.ts,
|
|
10
|
+
* report-data.ts, src/failure/failure-data.ts, history-data.ts, tick-detail-data.ts, config.ts)
|
|
11
|
+
* — this module only adapts HTTP onto it.
|
|
12
|
+
*/
|
|
13
|
+
import { refusalNote, waitingNote } from "../backlog/backlog-eligibility.js";
|
|
14
|
+
import { answeredQuestionEntries, donePlanEntries, fixedBugEntries, openBugEntries, openQuestionEntries, plannedPlanEntries } from "../backlog/backlog.js";
|
|
2
15
|
import { runDoctor } from "../doctor/doctor.js";
|
|
3
|
-
import { loadConfigSafe } from "../config/config.js";
|
|
16
|
+
import { knownRoleIds, loadConfigSafe } from "../config/config.js";
|
|
17
|
+
import { loopEnabled } from "../roles/loop-ids.js";
|
|
18
|
+
import { artifactPhrase, previewRetire } from "../operator/retire.js";
|
|
4
19
|
import { EDITABLE_CONFIG_KEYS } from "../config/config-editable-keys.js";
|
|
5
20
|
import { collectReport, collectReportSince } from "../report/report-data.js";
|
|
6
21
|
import { collectFailureReport, collectFailureReportSince } from "../failure/failure-data.js";
|
|
7
22
|
import { renderFailureMarkdown } from "../failure/failure-render.js";
|
|
8
23
|
import { renderSinceReportMarkdown } from "../report/report-render.js";
|
|
9
|
-
import { REPORT_SINCE_MAX_MS } from "../events/event-window.js";
|
|
24
|
+
import { REPORT_SINCE_MAX_MS, reportSinceMsg } from "../events/event-window.js";
|
|
10
25
|
import { humanSeconds } from "../text/datetime.js";
|
|
11
|
-
import {
|
|
26
|
+
import { collectTeamChanges, collectRoleChange } from "../change/change-data.js";
|
|
12
27
|
import { readTranscript } from "../ui/transcript.js";
|
|
13
|
-
import { HISTORY_DEFAULT_TICKS, HISTORY_MAX_TICKS, readTickRows, readTickRowsSince, tickRowsMatchingGrep } from "../history/history-data.js";
|
|
28
|
+
import { HISTORY_DEFAULT_TICKS, HISTORY_MAX_TICKS, readTickRows, readTickRowsSince, summarizeTickRows, tickRowsMatchingGrep } from "../history/history-data.js";
|
|
14
29
|
import { LOGS_SINCE_MAX_MS, SPARSE_WINDOW_NOTE } from "../events/event-window.js";
|
|
15
30
|
import { eventRole, readEvents } from "../events/event-read.js";
|
|
16
31
|
import { eventFeedItem, eventMatchesGrep } from "../events/event-format.js";
|
|
17
32
|
import { readTickDetail } from "../tick/tick-detail-data.js";
|
|
18
|
-
import { renderTickDetail, tickNotFoundMessage } from "../tick/tick-detail.js";
|
|
33
|
+
import { renderTickDetail, tickDetailStat, tickNotFoundMessage } from "../tick/tick-detail.js";
|
|
19
34
|
import { rolePayload } from "../roles/role-view.js";
|
|
20
35
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
21
36
|
import { readQueuedPromptBody } from "../inbox/inbox-edit.js";
|
|
22
37
|
import { intQuery, rejectBadQueueFileName, rejectBadRole, sinceMsQuery, windowDays } from "./gui-args.js";
|
|
23
|
-
import {
|
|
38
|
+
import { readBrief } from "../brief/brief.js";
|
|
39
|
+
import { badRequest, okGone, sendJson } from "./http-body.js";
|
|
24
40
|
import { typoSuffix } from "../text/suggest.js";
|
|
25
41
|
/** Handle GET /api/transcript?role=<id>&n=N: rendered transcript lines for one loop's pi
|
|
26
42
|
* log (same rendering as `tumwater logs --role <id>`). Unknown/missing role or a bad n → 400.
|
|
@@ -46,27 +62,33 @@ const BACKLOG_FILE_READERS = new Map([
|
|
|
46
62
|
["plans", plannedPlanEntries],
|
|
47
63
|
["bugs", openBugEntries],
|
|
48
64
|
["questions", openQuestionEntries],
|
|
65
|
+
["done", donePlanEntries],
|
|
66
|
+
["fixed", fixedBugEntries],
|
|
67
|
+
["answered", (root) => answeredQuestionEntries(root).reverse()],
|
|
49
68
|
]);
|
|
50
69
|
const BACKLOG_FILE_NAMES = [...BACKLOG_FILE_READERS.keys()];
|
|
51
70
|
const BACKLOG_FILE_VALUES = BACKLOG_FILE_NAMES.join(", ");
|
|
52
|
-
/** Handle GET /api/backlog?file=<plans|bugs|questions>&index=N: one
|
|
53
|
-
* text ({title, body}), fetched on demand so multi-KB bodies (long repros,
|
|
54
|
-
* ride the 1-second /api/status poll. index addresses the Nth entry of that
|
|
55
|
-
* section in the same order statusPayload lists its titles — PLANS.md ## Planned,
|
|
56
|
-
* BUGS.md ## Open, QUESTIONS.md ## Open
|
|
57
|
-
*
|
|
58
|
-
*
|
|
71
|
+
/** Handle GET /api/backlog?file=<plans|bugs|questions|done|fixed|answered>&index=N: one
|
|
72
|
+
* backlog entry's full text ({title, body}), fetched on demand so multi-KB bodies (long repros,
|
|
73
|
+
* whole plans) never ride the 1-second /api/status poll. index addresses the Nth entry of that
|
|
74
|
+
* file's section in the same order statusPayload lists its titles — plans: PLANS.md ## Planned,
|
|
75
|
+
* bugs: BUGS.md ## Open, questions: QUESTIONS.md ## Open, done: PLANS.md ## Done and fixed:
|
|
76
|
+
* BUGS.md ## Fixed (both newest first, the order of the dashboard's recent rows) and answered:
|
|
77
|
+
* QUESTIONS.md ## Answered, newest first (the file is appended to, so the reverse of file
|
|
78
|
+
* order, unlike done/fixed) — zero-based.
|
|
79
|
+
* Unknown/missing file, missing or bad index, and out-of-range index
|
|
80
|
+
* → 400 JSON error via sendJson.
|
|
81
|
+
* The query arrives pre-parsed (gui/gui-server.ts's parseRequestTarget), like every
|
|
82
|
+
* GET-data handler here. */
|
|
59
83
|
export function handleBacklog(q, res, root) {
|
|
60
84
|
const file = q.get("file");
|
|
61
85
|
if (file === null) {
|
|
62
|
-
|
|
86
|
+
badRequest(res, `file required (valid values: ${BACKLOG_FILE_VALUES})`);
|
|
63
87
|
return;
|
|
64
88
|
}
|
|
65
89
|
const reader = BACKLOG_FILE_READERS.get(file);
|
|
66
90
|
if (!reader) {
|
|
67
|
-
|
|
68
|
-
error: `unknown file ${JSON.stringify(file)} (valid values: ${BACKLOG_FILE_VALUES})${typoSuffix(file, BACKLOG_FILE_NAMES)}`,
|
|
69
|
-
});
|
|
91
|
+
badRequest(res, `unknown file ${JSON.stringify(file)} (valid values: ${BACKLOG_FILE_VALUES})${typoSuffix(file, BACKLOG_FILE_NAMES)}`);
|
|
70
92
|
return;
|
|
71
93
|
}
|
|
72
94
|
const entries = reader(root);
|
|
@@ -75,25 +97,25 @@ export function handleBacklog(q, res, root) {
|
|
|
75
97
|
return;
|
|
76
98
|
const entry = entries[index];
|
|
77
99
|
if (!entry) {
|
|
78
|
-
|
|
100
|
+
badRequest(res, `index ${index} out of range (${file} has ${entries.length} entries)`);
|
|
79
101
|
return;
|
|
80
102
|
}
|
|
81
|
-
sendJson(res, 200, { title: entry.title, body: entry.body });
|
|
103
|
+
sendJson(res, 200, { title: entry.title, body: entry.body, refused: refusalNote(entry.body) !== null, waiting: waitingNote(entry.body) !== null });
|
|
82
104
|
}
|
|
83
|
-
/** Handle GET /api/diff[?role=<id>]: the
|
|
84
|
-
* collectors — no `role` →
|
|
105
|
+
/** Handle GET /api/diff[?role=<id>]: the unmerged-change view the CLI prints from the same
|
|
106
|
+
* collectors — no `role` → collectTeamChanges (exactly `tumwater diff --json`'s document:
|
|
85
107
|
* `{ mainBranch, roles }`, no per-role patch halves); `?role=<id>` → collectRoleChange (the
|
|
86
108
|
* full per-role view with `diff`/`uncommittedDiff`, `tumwater diff --role <id> --json`). The
|
|
87
109
|
* role is a target, not a filter, so it validates through rejectBadRole exactly like
|
|
88
|
-
* /api/transcript and /api/
|
|
110
|
+
* /api/transcript and /api/run (unknown/missing → 400 naming the valid ids) — the same ids
|
|
89
111
|
* `parseRoleFlag` accepts on the CLI, including disabled and user-defined loops. The
|
|
90
112
|
* collectors degrade absent worktrees and missing base branches to their `state` instead of
|
|
91
113
|
* throwing, so a fresh or half-built repo still answers 200. Reads git plumbing directly, so
|
|
92
|
-
* it works whether or not the
|
|
114
|
+
* it works whether or not the team is running. */
|
|
93
115
|
export async function handleDiff(q, res, root) {
|
|
94
116
|
const role = q.get("role");
|
|
95
117
|
if (role === null) {
|
|
96
|
-
sendJson(res, 200, await
|
|
118
|
+
sendJson(res, 200, await collectTeamChanges(root));
|
|
97
119
|
return;
|
|
98
120
|
}
|
|
99
121
|
if (rejectBadRole(root, res, role))
|
|
@@ -104,10 +126,10 @@ export async function handleDiff(q, res, root) {
|
|
|
104
126
|
* usage report — the day series (collectReport's ReportData) as JSON the dashboard's report tab
|
|
105
127
|
* charts, or, when sinceMs is set, the trailing-window totals rendered through
|
|
106
128
|
* renderSinceReportMarkdown as { markdown } (the CLI's `report --since` shape), bounded by the
|
|
107
|
-
* reportSinceMs helper. The day window follows windowDays. An absent role is the
|
|
129
|
+
* reportSinceMs helper. The day window follows windowDays. An absent role is the team-wide
|
|
108
130
|
* report exactly as before; a present one is a filter, validated through rejectBadRole (unknown,
|
|
109
131
|
* including the empty string → 400 naming the valid ids) and handed to the collector's role
|
|
110
|
-
* scope. Reads files directly, so it works whether or not the
|
|
132
|
+
* scope. Reads files directly, so it works whether or not the team is running. */
|
|
111
133
|
export function handleReport(q, res, root) {
|
|
112
134
|
const role = q.get("role");
|
|
113
135
|
if (role !== null && rejectBadRole(root, res, role))
|
|
@@ -129,19 +151,19 @@ export function handleReport(q, res, root) {
|
|
|
129
151
|
* null once the 400 is sent. */
|
|
130
152
|
function reportSinceMs(q, res) {
|
|
131
153
|
if (q.get("days") !== null) {
|
|
132
|
-
|
|
154
|
+
badRequest(res, "days and sinceMs are mutually exclusive; days counts whole local days, sinceMs totals a trailing window");
|
|
133
155
|
return null;
|
|
134
156
|
}
|
|
135
|
-
return sinceMsQuery(q, res, REPORT_SINCE_MAX_MS,
|
|
157
|
+
return sinceMsQuery(q, res, REPORT_SINCE_MAX_MS, reportSinceMsg);
|
|
136
158
|
}
|
|
137
159
|
/** Handle GET /api/failures?days=N[&role=<id>] or /api/failures?sinceMs=<ms>[&role=<id>]: the
|
|
138
160
|
* same bounded Markdown failure digest the telemetry loop feeds on and `tumwater report
|
|
139
161
|
* --failures` prints, as JSON ({ markdown }) — the dashboard's failures tab renders it; with
|
|
140
162
|
* sinceMs it is the trailing-window digest (collectFailureReportSince) through the same render,
|
|
141
163
|
* the CLI's `report --failures --since` shape. The days window follows windowDays (so it can
|
|
142
|
-
* never drift from /api/report's). role rides the same absent-
|
|
143
|
-
* handleReport: absent → the
|
|
144
|
-
* directly, so it works with no
|
|
164
|
+
* never drift from /api/report's). role rides the same absent-team / present-filter rule as
|
|
165
|
+
* handleReport: absent → the team digest, present → validated and scoped. Reads files
|
|
166
|
+
* directly, so it works with no team running. */
|
|
145
167
|
export function handleFailures(q, res, root) {
|
|
146
168
|
const role = q.get("role");
|
|
147
169
|
if (role !== null && rejectBadRole(root, res, role))
|
|
@@ -158,16 +180,17 @@ export function handleFailures(q, res, root) {
|
|
|
158
180
|
/** Handle GET /api/history?role=<id>&n=N[&grep=<text>] — or ?role=<id>&sinceMs=<ms> for a
|
|
159
181
|
* trailing window: the per-tick rows the CLI's `tumwater history` derives from the event log,
|
|
160
182
|
* the dashboard's history tab renders them. The count shape ({ rows }) rides readTickRows (the
|
|
161
|
-
* shared scan, window growth included, so a role-filtered ask on a busy
|
|
183
|
+
* shared scan, window growth included, so a role-filtered ask on a busy team cannot silently
|
|
162
184
|
* come back short); n is optional: absent → HISTORY_DEFAULT_TICKS; present but not a plain
|
|
163
185
|
* non-negative integer → 400 (the handleBacklog index discipline — an explicit count that does
|
|
164
186
|
* not parse is a client error, unlike the report's degrade-to-default typo rule); then clamped
|
|
165
187
|
* to [1, HISTORY_MAX_TICKS] — the GUI clamps where the CLI fails fast, its established
|
|
166
188
|
* convention. grep is optional: absent or empty filters nothing; present → the rows are
|
|
167
|
-
* filtered through history-data.ts's shared tickRowsMatchingGrep rule, the same
|
|
168
|
-
* `tumwater history --grep` uses (shared, not
|
|
169
|
-
*
|
|
170
|
-
* ({ rows, covered, note }) rides
|
|
189
|
+
* filtered through history-data.ts's shared tickRowsMatchingGrep rule, the same
|
|
190
|
+
* case-insensitive haystack rule `tumwater history --grep` uses (shared, not
|
|
191
|
+
* re-implemented), after the n/role scan so n bounds the scanned window and not the
|
|
192
|
+
* printed rows — the CLI's rule. The window shape ({ rows, covered, note }) rides
|
|
193
|
+
* readTickRowsSince, the collector behind `history --since`,
|
|
171
194
|
* so the browser and the CLI cannot disagree about the window's rows; a simultaneous n is a
|
|
172
195
|
* rival shape → 400 (the CLI's count-vs-window rule), sinceMs parses and is capped through
|
|
173
196
|
* sinceMsQuery (gui-args.ts, LOGS_SINCE_MAX_MS — the shared `history --since` cap), and note is
|
|
@@ -175,13 +198,21 @@ export function handleFailures(q, res, root) {
|
|
|
175
198
|
* loops; present → passed straight through as the role filter with no config validation (a
|
|
176
199
|
* filter is not a target — an id with no ticks legitimately yields zero rows), so the endpoint
|
|
177
200
|
* reads nothing but files, like /api/report and /api/failures. Reads files directly, so it
|
|
178
|
-
*
|
|
201
|
+
* present → passed straight through as the role filter with no config validation (a
|
|
202
|
+
* filter is not a target — an id with no ticks legitimately yields zero rows), so the endpoint
|
|
203
|
+
* reads nothing but files, like /api/report and /api/failures. summary is optional: absent /
|
|
204
|
+
* empty adds nothing (the response is byte-for-byte today's); present (any value — the flag's
|
|
205
|
+
* mere presence, a bad value ignored like a missing one) adds an additive `summary` folding the
|
|
206
|
+
* exact served rows through the shared summarizeTickRows collector, the same tally
|
|
207
|
+
* `tumwater history --summary` prints, so the dashboard Summary and the table cannot disagree
|
|
208
|
+
* about what a tick was. Reads files directly, so it works whether or not the team is running. */
|
|
179
209
|
export function handleHistory(q, res, root) {
|
|
180
210
|
const role = q.get("role") || null; // "" and absent both read all loops
|
|
181
211
|
const grep = q.get("grep") || null; // "" and absent both filter nothing
|
|
212
|
+
const summary = q.get("summary") !== null; // the flag's presence, whatever its value
|
|
182
213
|
if (q.get("sinceMs") !== null) {
|
|
183
214
|
if (q.get("n") !== null) {
|
|
184
|
-
|
|
215
|
+
badRequest(res, "sinceMs cannot be combined with n (a count and a window are rival shapes)");
|
|
185
216
|
return;
|
|
186
217
|
}
|
|
187
218
|
const sinceMs = sinceMsQuery(q, res, LOGS_SINCE_MAX_MS, (ms) => `sinceMs is capped at ${humanSeconds(LOGS_SINCE_MAX_MS / 1000)} (got ${ms})`);
|
|
@@ -189,7 +220,7 @@ export function handleHistory(q, res, root) {
|
|
|
189
220
|
return;
|
|
190
221
|
const { rows, covered } = readTickRowsSince(root, sinceMs, role);
|
|
191
222
|
const matched = grep === null ? rows : tickRowsMatchingGrep(rows, grep);
|
|
192
|
-
sendJson(res, 200, { rows: matched, covered, note: covered ? "" : SPARSE_WINDOW_NOTE });
|
|
223
|
+
sendJson(res, 200, { rows: matched, covered, note: covered ? "" : SPARSE_WINDOW_NOTE, ...(summary ? { summary: summarizeTickRows(matched) } : {}) });
|
|
193
224
|
return;
|
|
194
225
|
}
|
|
195
226
|
const parsed = intQuery(q, res, "n", "non-negative", HISTORY_DEFAULT_TICKS);
|
|
@@ -199,7 +230,7 @@ export function handleHistory(q, res, root) {
|
|
|
199
230
|
let rows = readTickRows(root, n, role);
|
|
200
231
|
if (grep !== null)
|
|
201
232
|
rows = tickRowsMatchingGrep(rows, grep);
|
|
202
|
-
sendJson(res, 200, { rows });
|
|
233
|
+
sendJson(res, 200, summary ? { rows, summary: summarizeTickRows(rows) } : { rows });
|
|
203
234
|
}
|
|
204
235
|
/** How many of the log's newest events `GET /api/logs` scans by default, and the most it will
|
|
205
236
|
* scan however large `n` asks: the CLI's `-n` shape (n bounds the scanned tail, not the printed
|
|
@@ -220,7 +251,7 @@ const LOGS_SEARCH_MAX_EVENTS = 5000;
|
|
|
220
251
|
* Reads the tail through readEvents' stat-keyed cache (no covered byte end, unlike the CLI's
|
|
221
252
|
* follow), so a dashboard search that repeats on an unchanged log — the debounced filter fires
|
|
222
253
|
* one per filter change — costs one stat instead of re-reading and re-parsing the scanned tail.
|
|
223
|
-
* Reads files directly, so it works whether or not the
|
|
254
|
+
* Reads files directly, so it works whether or not the team is running. */
|
|
224
255
|
export function handleLogs(q, res, root) {
|
|
225
256
|
const parsed = intQuery(q, res, "n", "non-negative", LOGS_SEARCH_EVENTS);
|
|
226
257
|
if (parsed === null)
|
|
@@ -233,7 +264,7 @@ export function handleLogs(q, res, root) {
|
|
|
233
264
|
const matched = events.filter((e) => (role === null || eventRole(e) === role) && (needle === null || eventMatchesGrep(e, needle)));
|
|
234
265
|
sendJson(res, 200, { events: matched.map(eventFeedItem) });
|
|
235
266
|
}
|
|
236
|
-
/** Handle GET /api/
|
|
267
|
+
/** Handle GET /api/run?role=<id>&run=N: one tick's full event trail — readTickDetail's
|
|
237
268
|
* TickDetail, the collector `tumwater tick <role> <n>` prints from, plus `text`: the same
|
|
238
269
|
* payload through renderTickDetail, pre-rendered server-side like /api/transcript's lines so
|
|
239
270
|
* the browser shows the CLI's exact rendering instead of re-implementing the event formatting.
|
|
@@ -243,12 +274,12 @@ export function handleLogs(q, res, root) {
|
|
|
243
274
|
* through intQuery (missing or not a positive integer → 400); and a tick the scanned window
|
|
244
275
|
* does not hold — never ran, or rotation ate it — answers 404 with tickNotFoundMessage, the
|
|
245
276
|
* CLI's exact not-found wording,
|
|
246
|
-
* never a crash. Reads files directly, so it works whether or not the
|
|
247
|
-
export function handleTick(q, res, root) {
|
|
277
|
+
* never a crash. Reads files directly, so it works whether or not the team is running. */
|
|
278
|
+
export async function handleTick(q, res, root) {
|
|
248
279
|
const role = q.get("role");
|
|
249
280
|
if (rejectBadRole(root, res, role))
|
|
250
281
|
return;
|
|
251
|
-
const tick = intQuery(q, res, "
|
|
282
|
+
const tick = intQuery(q, res, "run", "positive");
|
|
252
283
|
if (tick === null)
|
|
253
284
|
return;
|
|
254
285
|
const detail = readTickDetail(root, role, tick);
|
|
@@ -256,15 +287,15 @@ export function handleTick(q, res, root) {
|
|
|
256
287
|
sendJson(res, 404, { error: tickNotFoundMessage(role, tick) });
|
|
257
288
|
return;
|
|
258
289
|
}
|
|
259
|
-
sendJson(res, 200, { ...detail, text: renderTickDetail(detail) });
|
|
290
|
+
sendJson(res, 200, { ...detail, text: renderTickDetail(detail), stat: await tickDetailStat(root, detail) });
|
|
260
291
|
}
|
|
261
292
|
/** Handle GET /api/role?role=<id>: one loop's inspection payload — rolePayload's object,
|
|
262
293
|
* the same collection behind `tumwater role <id>` — so the drawer's Notebook and Next tick
|
|
263
294
|
* prompt sections render what the CLI shows without re-implementing the assembly. The role
|
|
264
295
|
* is a target here, so it validates through rejectBadRole exactly like /api/transcript and
|
|
265
|
-
* /api/
|
|
296
|
+
* /api/run (unknown/missing → the shared 400 naming the valid ids and correcting typos);
|
|
266
297
|
* rolePayload is only reached with an id rejectBadRole accepted, so its own unknown-role
|
|
267
|
-
* throw never fires. Reads files directly, so it works whether or not the
|
|
298
|
+
* throw never fires. Reads files directly, so it works whether or not the team is running. */
|
|
268
299
|
export function handleRole(q, res, root) {
|
|
269
300
|
const role = q.get("role");
|
|
270
301
|
if (rejectBadRole(root, res, role))
|
|
@@ -278,9 +309,10 @@ export function handleRole(q, res, root) {
|
|
|
278
309
|
* marker and the `[image attached: …]` reference block are stripped, both plumbing the edit
|
|
279
310
|
* carries over unchanged. The role is optional (absent reads the director's queue) and
|
|
280
311
|
* validates exactly like /api/transcript; a file name failing the shared
|
|
281
|
-
* rejectBadQueueFileName guard is a user-input error answered 400 with nothing read. A
|
|
282
|
-
* { ok: true, status: "gone" } — the loop already dequeued
|
|
283
|
-
* a server fault. Reads one file directly, so it works
|
|
312
|
+
* rejectBadQueueFileName guard is a user-input error answered 400 with nothing read. A
|
|
313
|
+
* vanished file answers 200 { ok: true, status: "gone" } — the loop already dequeued
|
|
314
|
+
* it, which is data for the UI, not a server fault. Reads one file directly, so it works
|
|
315
|
+
* whether or not the team is running. */
|
|
284
316
|
export function handlePromptText(q, res, root) {
|
|
285
317
|
const roleParam = q.get("role");
|
|
286
318
|
if (roleParam !== null && rejectBadRole(root, res, roleParam))
|
|
@@ -291,21 +323,21 @@ export function handlePromptText(q, res, root) {
|
|
|
291
323
|
return;
|
|
292
324
|
const text = readQueuedPromptBody(root, role, file);
|
|
293
325
|
if (text === null) {
|
|
294
|
-
|
|
326
|
+
okGone(res);
|
|
295
327
|
return;
|
|
296
328
|
}
|
|
297
329
|
sendJson(res, 200, { ok: true, text });
|
|
298
330
|
}
|
|
299
331
|
/** Handle GET /api/doctor: the same DoctorReport `tumwater doctor --json` prints, so the
|
|
300
332
|
* Settings view's Health check card and the CLI cannot disagree. Read-only — the checks write
|
|
301
|
-
* nothing (runDoctor's own contract) — and it works whether or not the
|
|
333
|
+
* nothing (runDoctor's own contract) — and it works whether or not the team is running. `run`
|
|
302
334
|
* exists so tests can skip the host probes; a throw reaches the server's outer 500. */
|
|
303
|
-
export async function handleDoctor(res, root, run = runDoctor) {
|
|
335
|
+
export async function handleDoctor(res, root, run = (r) => runDoctor(r, undefined, undefined, "never")) {
|
|
304
336
|
sendJson(res, 200, await run(root));
|
|
305
337
|
}
|
|
306
338
|
/** Handle GET /api/config: the Settings view's resolved values for exactly
|
|
307
339
|
* EDITABLE_CONFIG_KEYS — read through the same load path `tumwater config get` uses
|
|
308
|
-
* (loadConfigSafe, defaults merged in), so the page shows what the
|
|
340
|
+
* (loadConfigSafe, defaults merged in), so the page shows what the team would actually
|
|
309
341
|
* load. A broken or invalid tumwater.json answers 500 with validateConfig's message instead
|
|
310
342
|
* of serving a half-empty panel. */
|
|
311
343
|
export function handleConfig(res, root) {
|
|
@@ -320,3 +352,31 @@ export function handleConfig(res, root) {
|
|
|
320
352
|
out[key] = record[key] ?? null;
|
|
321
353
|
sendJson(res, 200, out);
|
|
322
354
|
}
|
|
355
|
+
/** Handle GET /api/retire: the Settings view's "Disabled roles" card — every configured,
|
|
356
|
+
* disabled, non-director role, with what `tumwater retire` would remove (previewRetire, so the
|
|
357
|
+
* rails and wording are the CLI's) and the rails' objections. `wouldRemove` is empty when there
|
|
358
|
+
* is nothing to retire (a role disabled with a clean checkout), so the card can also re-enable
|
|
359
|
+
* a role via POST /api/role-enable. Read-only (previewRetire's contract) and works whether or
|
|
360
|
+
* not the team runs; a throw reaches the server's outer 500, an unreadable tumwater.json
|
|
361
|
+
* answers 500 like /api/config. */
|
|
362
|
+
export async function handleRetireList(res, root) {
|
|
363
|
+
const { config, error } = loadConfigSafe(root);
|
|
364
|
+
if (config === undefined) {
|
|
365
|
+
sendJson(res, 500, { error });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const roles = [];
|
|
369
|
+
for (const role of knownRoleIds(config).filter((id) => id !== DIRECTOR_ROLE && !loopEnabled(config, id))) {
|
|
370
|
+
const { wouldRemove, objections } = await previewRetire(root, role);
|
|
371
|
+
roles.push({ role, wouldRemove, phrases: wouldRemove.map((a) => artifactPhrase(a, role)), objections });
|
|
372
|
+
}
|
|
373
|
+
sendJson(res, 200, { roles });
|
|
374
|
+
}
|
|
375
|
+
/** Handle GET /api/brief: the brief file's name and the current initial prompt (what
|
|
376
|
+
* `tumwater brief --json` prints), or 404 when no brief carries a prompt block. */
|
|
377
|
+
export function handleBrief(res, root) {
|
|
378
|
+
const brief = readBrief(root);
|
|
379
|
+
if (brief === null)
|
|
380
|
+
return sendJson(res, 404, { error: "no project brief with a prompt block found — run `tumwater init` first" });
|
|
381
|
+
sendJson(res, 200, { file: brief.file, prompt: brief.prompt });
|
|
382
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/**
|
|
3
|
+
* The dashboard's POST operator-lifecycle command handlers (barrelled into
|
|
4
|
+
* gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts): reset-counters,
|
|
5
|
+
* restart, reclaim, abort, stop-schedule, role-notes, retire, and brief-edit — every POST
|
|
6
|
+
* handler that is not prompt/backlog/config/team. Each handler adapts HTTP onto the domain work
|
|
7
|
+
* one layer down (operator-intent, role-notes-write, brief-edit) and touches no socket beyond
|
|
8
|
+
* its own `res`.
|
|
9
|
+
*/
|
|
10
|
+
import { cancelScheduledStopMessage, requestAbort, requestResetCounters, requestRestart, requestScheduledStop, runManualReclaim } from "../operator/operator-intent.js";
|
|
11
|
+
import { saveRoleNote } from "../agent/role-notes-write.js";
|
|
12
|
+
import { roleNotesPath } from "../paths.js";
|
|
13
|
+
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
14
|
+
import { artifactPhrase, retireRole } from "../operator/retire.js";
|
|
15
|
+
import { editInitialPrompt } from "../brief/brief-edit.js";
|
|
16
|
+
import { validRoleIds } from "./gui-args.js";
|
|
17
|
+
import { badRequest, sendJson } from "./http-body.js";
|
|
18
|
+
import { errorMessage, gotSuffix } from "../text/text.js";
|
|
19
|
+
import { readPostBody, readRoleBody, sendRequestResult } from "./gui-endpoint-common.js";
|
|
20
|
+
import { wakeInSecondsMs } from "./gui-team-commands.js";
|
|
21
|
+
/** Handle POST /api/reset-counters: the dashboard's counter reset — the same marker-writing
|
|
22
|
+
* core `tumwater reset-counters` calls (requestResetCounters). Same body discipline and
|
|
23
|
+
* `{}` → every configured role default as /api/wake. */
|
|
24
|
+
export async function handleResetCounters(req, res, root) {
|
|
25
|
+
const body = await readRoleBody(req, res, '{"role": "feature"}', root, true);
|
|
26
|
+
if (!body)
|
|
27
|
+
return;
|
|
28
|
+
sendJson(res, 200, {
|
|
29
|
+
ok: true,
|
|
30
|
+
message: requestResetCounters(root, body.role === undefined ? validRoleIds(root) : [body.role]),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Handle POST /api/stop-schedule: the dashboard's "Stop the team later" control — the same
|
|
34
|
+
* cores `tumwater stop --in`/`--cancel` call (requestScheduledStop, cancelScheduledStopMessage).
|
|
35
|
+
* `{"cancel": true}` drops the marker (no live team needed, like the CLI); otherwise
|
|
36
|
+
* `inSeconds` is required and takes /api/wake's positive-finite-and-capped rule. A stopped
|
|
37
|
+
* team answers 409 with the not-running error. */
|
|
38
|
+
export async function handleStopSchedule(req, res, root) {
|
|
39
|
+
const body = await readPostBody(req, res, '{"inSeconds": 7200}');
|
|
40
|
+
if (!body)
|
|
41
|
+
return;
|
|
42
|
+
if (body.cancel === true) {
|
|
43
|
+
if (body.inSeconds !== undefined) {
|
|
44
|
+
badRequest(res, "cancel and inSeconds cannot be combined");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
sendJson(res, 200, { ok: true, message: cancelScheduledStopMessage(root) });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const inMs = wakeInSecondsMs(res, body);
|
|
51
|
+
if (inMs === null)
|
|
52
|
+
return;
|
|
53
|
+
if (inMs === undefined) {
|
|
54
|
+
badRequest(res, "inSeconds or cancel is required");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
sendRequestResult(res, requestScheduledStop(root, inMs));
|
|
58
|
+
}
|
|
59
|
+
/** Handle POST /api/restart: the dashboard's build-stale alert's refresh button — the same
|
|
60
|
+
* marker-writing core a CLI surface would call (requestRestart), so the wording and the
|
|
61
|
+
* not-pending refusal cannot drift. The not-pending error comes back 409 like /api/abort's
|
|
62
|
+
* not-live one: the request is valid but nothing can act on it. Same body discipline as
|
|
63
|
+
* /api/wake (readJsonObject → 400 malformed/non-object, 413 oversized; the body itself is
|
|
64
|
+
* ignored — there are no options to force a restart). */
|
|
65
|
+
export async function handleRestart(req, res, root) {
|
|
66
|
+
const body = await readPostBody(req, res, "{}");
|
|
67
|
+
if (!body)
|
|
68
|
+
return;
|
|
69
|
+
sendRequestResult(res, requestRestart(root));
|
|
70
|
+
}
|
|
71
|
+
/** Handle POST /api/reclaim: the dashboard disk row's reclaim button — the same manual pass
|
|
72
|
+
* `tumwater reclaim` runs (runManualReclaim), answering its confirmation verbatim. Same body
|
|
73
|
+
* discipline as /api/restart (the body is ignored); a throw reaches the server's outer 500. */
|
|
74
|
+
export async function handleReclaim(req, res, root) {
|
|
75
|
+
const body = await readPostBody(req, res, "{}");
|
|
76
|
+
if (!body)
|
|
77
|
+
return;
|
|
78
|
+
sendJson(res, 200, { ok: true, message: await runManualReclaim(root) });
|
|
79
|
+
}
|
|
80
|
+
/** Handle POST /api/abort: the dashboard's per-row abort control — the same marker-writing
|
|
81
|
+
* core `tumwater abort` calls (requestAbort). The role is required and validated like
|
|
82
|
+
* /api/transcript; requestAbort's not-live error comes back 409 — the marker is valid but
|
|
83
|
+
* nothing can consume it, a conflict rather than a client 400. The director variant's
|
|
84
|
+
* message (the discarded-prompt note) rides through verbatim. */
|
|
85
|
+
export async function handleAbort(req, res, root) {
|
|
86
|
+
const body = await readRoleBody(req, res, '{"role": "feature"}', root);
|
|
87
|
+
if (!body)
|
|
88
|
+
return;
|
|
89
|
+
sendRequestResult(res, requestAbort(root, body.role));
|
|
90
|
+
}
|
|
91
|
+
/** Handle POST /api/role-notes: the loop drawer's notebook Edit/Clear — the same saveRoleNote
|
|
92
|
+
* `tumwater role <id> --notes-file` / `--clear-notes` call, so the 4 KB cap and its wording
|
|
93
|
+
* cannot drift. `"text": ""` clears. The director has no notes (400, the CLI's wording); a
|
|
94
|
+
* non-string text is a 400 before anything is written; an over-cap note is a 400 carrying
|
|
95
|
+
* saveRoleNote's byte-count message. */
|
|
96
|
+
export async function handleRoleNotes(req, res, root) {
|
|
97
|
+
const body = await readRoleBody(req, res, '{"role": "feature", "text": "…"}', root);
|
|
98
|
+
if (!body)
|
|
99
|
+
return;
|
|
100
|
+
const role = body.role;
|
|
101
|
+
if (role === DIRECTOR_ROLE)
|
|
102
|
+
return badRequest(res, "the director has no notes");
|
|
103
|
+
if (typeof body.text !== "string")
|
|
104
|
+
return badRequest(res, `"text" must be a string — the full replacement note, or "" to clear it${gotSuffix(body.text)}`);
|
|
105
|
+
try {
|
|
106
|
+
sendJson(res, 200, { ok: true, message: saveRoleNote(roleNotesPath(root, role), body.text) });
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
badRequest(res, errorMessage(err));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Handle POST /api/retire: the Settings view's Retire button — the same retireRole core
|
|
113
|
+
* `tumwater retire --role <id>` calls, never forced (a `force` key is a 400 naming the CLI
|
|
114
|
+
* override). A rail's refusal or an unreadable tumwater.json is a 409 carrying the core's
|
|
115
|
+
* message and removes nothing; success answers what was removed in the CLI's wording. */
|
|
116
|
+
export async function handleRetire(req, res, root) {
|
|
117
|
+
const body = await readRoleBody(req, res, '{"role": "improve"}', root);
|
|
118
|
+
if (!body)
|
|
119
|
+
return;
|
|
120
|
+
const role = body.role;
|
|
121
|
+
if (body.force !== undefined)
|
|
122
|
+
return badRequest(res, "the dashboard never forces a retire — run `tumwater retire --role <id> --force` from the CLI");
|
|
123
|
+
try {
|
|
124
|
+
const { removed, skipped } = await retireRole(root, role, {});
|
|
125
|
+
const message = removed.length === 0 ? `nothing to remove for role ${role}` : removed.map((a) => `removed ${artifactPhrase(a, role)}`).join("; ");
|
|
126
|
+
sendJson(res, 200, { ok: true, removed, skipped, message });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
sendJson(res, 409, { error: errorMessage(err) });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Handle POST /api/brief: replace the initial prompt through editInitialPrompt — the same
|
|
133
|
+
* checks, locked write, and commit as `tumwater brief --file`. A non-string prompt or any
|
|
134
|
+
* refusal is a 400 and writes nothing. */
|
|
135
|
+
export async function handleBriefEdit(req, res, root) {
|
|
136
|
+
const body = await readPostBody(req, res, '{"prompt": "…"}');
|
|
137
|
+
if (!body)
|
|
138
|
+
return;
|
|
139
|
+
if (typeof body.prompt !== "string")
|
|
140
|
+
return badRequest(res, `"prompt" must be a string — the full replacement brief${gotSuffix(body.prompt)}`);
|
|
141
|
+
const outcome = await editInitialPrompt(root, body.prompt);
|
|
142
|
+
if (!outcome.ok)
|
|
143
|
+
return badRequest(res, outcome.error);
|
|
144
|
+
sendJson(res, 200, { file: outcome.file, changed: outcome.changed, committed: outcome.committed });
|
|
145
|
+
}
|