tumwater 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +42 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +4 -3
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +402 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +246 -55
- package/dist/src/backlog/backlog-cli-view.js +280 -0
- package/dist/src/backlog/backlog-cli.js +85 -274
- package/dist/src/backlog/backlog-commit.js +4 -5
- package/dist/src/backlog/backlog-eligibility.js +175 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +254 -0
- package/dist/src/backlog/backlog-release.js +117 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-culprit.js +94 -0
- package/dist/src/baseline/main-guardrails.js +209 -0
- package/dist/src/baseline/main-red.js +139 -32
- package/dist/src/baseline/main-revert.js +133 -0
- package/dist/src/baseline/main-watch.js +184 -0
- package/dist/src/brief/brief-edit.js +9 -11
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget.js +30 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +118 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +150 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +308 -590
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-role-command.js +420 -0
- package/dist/src/cli/cli-start-summary.js +160 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
- package/dist/src/cli/completion.js +149 -22
- package/dist/src/cli/config-commands.js +41 -18
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +189 -73
- package/dist/src/cli/log-commands.js +148 -82
- package/dist/src/cli/question-commands.js +205 -10
- package/dist/src/cli.js +106 -43
- package/dist/src/collections.js +26 -10
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +13 -1
- package/dist/src/config/config-fallbacks.js +93 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +141 -199
- package/dist/src/config/config-views.js +127 -53
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +131 -264
- package/dist/src/config/config.js +48 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
- package/dist/src/doctor/doctor-backlog-plans.js +263 -0
- package/dist/src/doctor/doctor-backlog.js +100 -496
- package/dist/src/doctor/doctor-check-names.js +81 -0
- package/dist/src/doctor/doctor-checks.js +52 -579
- package/dist/src/doctor/doctor-claude-checks.js +145 -0
- package/dist/src/doctor/doctor-codex-checks.js +279 -0
- package/dist/src/doctor/doctor-contention-checks.js +380 -0
- package/dist/src/doctor/doctor-holds.js +82 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +236 -402
- package/dist/src/doctor/doctor-notify.js +3 -4
- package/dist/src/doctor/doctor-opencode-checks.js +162 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-project-checks.js +303 -0
- package/dist/src/doctor/doctor.js +121 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +7 -11
- package/dist/src/events/event-format.js +181 -86
- package/dist/src/events/event-labels.js +12 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +39 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +60 -24
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +56 -56
- package/dist/src/failure/failure-render.js +31 -23
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +99 -35
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +177 -84
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +108 -58
- package/dist/src/gates/maintenance-quota.js +10 -13
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +77 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +36 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +42 -18
- package/dist/src/gui/gui-backlog-endpoints.js +155 -0
- package/dist/src/gui/gui-command.js +8 -13
- package/dist/src/gui/gui-config-commands.js +94 -72
- package/dist/src/gui/gui-endpoint-commands.js +4 -3
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +108 -13
- package/dist/src/gui/gui-prompt-commands.js +11 -14
- package/dist/src/gui/gui-report-endpoints.js +90 -0
- package/dist/src/gui/gui-server.js +11 -4
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +147 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +193 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +25 -9
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +81 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +150 -11
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +90 -0
- package/dist/src/merge/merge-queue-render.js +49 -11
- package/dist/src/merge/merge-queue.js +31 -15
- package/dist/src/merge/merge-requeue.js +102 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +33 -11
- package/dist/src/merge/merge-to-main.js +86 -27
- package/dist/src/merge/merge-top.js +69 -0
- package/dist/src/merge/merge-verify.js +12 -10
- package/dist/src/notes-cli.js +30 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +320 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
- package/dist/src/orchestrator/orchestrator.js +128 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +201 -0
- package/dist/src/orchestrator/scheduling-skip.js +185 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +116 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +18 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +121 -211
- package/dist/src/prompt/notes-backlog.js +181 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +129 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +161 -77
- package/dist/src/report/report-fold.js +129 -52
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +133 -64
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +26 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/known-flakes.js +100 -1
- package/dist/src/review/post-merge-review.js +106 -0
- package/dist/src/review/protected-edits.js +4 -5
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +155 -360
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +69 -12
- package/dist/src/roles/role-guidance.js +55 -20
- package/dist/src/roles/role-labels.js +1 -2
- package/dist/src/roles/role-render.js +67 -50
- package/dist/src/roles/role-view.js +59 -47
- package/dist/src/roles/roles.js +31 -31
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/{tick → run}/qa-coverage.js +9 -27
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +179 -0
- package/dist/src/run/run-detail-render.js +98 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +172 -0
- package/dist/src/run/stage-path-checks.js +730 -0
- package/dist/src/{loop → runner}/leftover.js +80 -25
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +202 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +107 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +102 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +43 -16
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +28 -28
- package/dist/src/status/status-roles.js +39 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +5 -6
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +63 -17
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +57 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +67 -13
- package/dist/src/ui/badges.js +65 -37
- package/dist/src/ui/gui/gui-client-backlog.js +292 -0
- package/dist/src/ui/gui/gui-client-boot.js +32 -26
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
- package/dist/src/ui/gui/gui-client-drawer.js +126 -285
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +434 -67
- package/dist/src/ui/gui/gui-client-labels.js +1 -2
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +55 -51
- package/dist/src/ui/gui/gui-client-pending.js +227 -23
- package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
- package/dist/src/ui/gui/gui-client-report.js +87 -427
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +56 -4
- package/dist/src/ui/gui/gui-client-team.js +25 -465
- package/dist/src/ui/gui/gui-client.js +27 -25
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +9 -7
- package/dist/src/ui/gui/gui-styles.js +34 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +136 -49
- package/dist/src/ui/status-render.js +101 -82
- package/dist/src/ui/team-alerts.js +248 -57
- package/dist/src/ui/tone.js +5 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +51 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +3 -5
- package/dist/src/ui/tui/tui-editor-backends.js +132 -0
- package/dist/src/ui/tui/tui-editor-brief.js +10 -12
- package/dist/src/ui/tui/tui-editor-budget.js +35 -31
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
- package/dist/src/ui/tui/tui-editor-config.js +86 -0
- package/dist/src/ui/tui/tui-editor-entry.js +36 -47
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +37 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
- package/dist/src/ui/tui/tui-editor-interval.js +11 -9
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +3 -5
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
- package/dist/src/ui/tui/tui-editor-retire.js +4 -6
- package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +60 -34
- package/dist/src/ui/tui/tui-editor-slots.js +23 -0
- package/dist/src/ui/tui/tui-editors.js +35 -2
- package/dist/src/ui/tui/tui-frame.js +56 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +24 -9
- package/dist/src/ui/tui/tui-pane-activity.js +14 -11
- package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +58 -8
- package/dist/src/ui/tui/tui-pane-history.js +402 -82
- package/dist/src/ui/tui/tui-pane-pending.js +308 -13
- package/dist/src/ui/tui/tui-pane-queued.js +71 -55
- package/dist/src/ui/tui/tui-pane-report.js +46 -14
- package/dist/src/ui/tui/tui-pane-session.js +25 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +80 -23
- package/dist/src/ui/tui/tui-panes.js +100 -30
- package/dist/src/ui/tui/tui.js +14 -14
- package/dist/src/verdict/fix-claim.js +82 -32
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +42 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dashboard's GET run/event-log drill-down handlers (src/gui/gui-server.ts routes to them):
|
|
3
|
+
* transcript, history, logs, run, and role. Each answers its request and touches no socket
|
|
4
|
+
* beyond its own `res`; the domain work lives one layer down (ui/transcript.ts,
|
|
5
|
+
* history/history-data.ts, run/run-detail-data.ts, events/event-read.ts), so this module only
|
|
6
|
+
* adapts HTTP onto it. Barrelled into gui/gui-endpoints.ts beside the other per-family GET
|
|
7
|
+
* endpoint modules.
|
|
8
|
+
*/
|
|
9
|
+
import { humanSeconds } from "../text/datetime.js";
|
|
10
|
+
import { readTranscript } from "../ui/transcript.js";
|
|
11
|
+
import { HISTORY_DEFAULT_RUNS, HISTORY_MAX_RUNS, HISTORY_RESULT_FLAG_NAMES, historyClass, parseHistoryResultClass, readRunRows, readRunRowsSince, summarizeRunRows, summarizeRunRowsByModel, runRowsMatchingGrep, runRowsOnModels, } from "../history/history-data.js";
|
|
12
|
+
import { LOGS_SINCE_MAX_MS, SPARSE_WINDOW_NOTE, readEventsSince } from "../events/event-window.js";
|
|
13
|
+
import { eventRole, readEvents } from "../events/event-read.js";
|
|
14
|
+
import { eventFeedItem, eventMatchesGrep } from "../events/event-format.js";
|
|
15
|
+
import { readRunDetail, runCommitSha, runMergedSha } from "../run/run-detail-data.js";
|
|
16
|
+
import { renderRunDetail, runDetailPatch, runDetailStat, runTranscriptLines } from "../run/run-detail-render.js";
|
|
17
|
+
import { resolveRevertTarget } from "../operator/revert.js";
|
|
18
|
+
import { rolePayload } from "../roles/role-view.js";
|
|
19
|
+
import { clampCount, intQuery, rejectBadRole, sinceMsQuery, untilMsQuery } from "./gui-args.js";
|
|
20
|
+
import { badRequest, sendJson } from "./http-body.js";
|
|
21
|
+
import { typoSuffix } from "../text/suggest.js";
|
|
22
|
+
import { gotSuffix, splitCommaList, splitCommaListUnique } from "../text/text.js";
|
|
23
|
+
import { newestRunRow, runNotFoundLastMessage, runNotFoundMessage } from "../run/run-detail.js";
|
|
24
|
+
/** Handle GET /api/transcript?role=<id>&n=N[&grep=<text>]: rendered transcript lines for a role's
|
|
25
|
+
* pi log (same rendering as `tumwater logs --transcript <id>`). A present `prompt` flag also
|
|
26
|
+
* renders each agent run's prompt, the text `logs --transcript <id> --prompt` prints.
|
|
27
|
+
* Unknown/missing role
|
|
28
|
+
* or a bad n → 400. `grep` keeps only the entries containing the text, case-insensitively, each
|
|
29
|
+
* under its run's separator (the `logs --transcript <id> --grep` rule); `n` bounds the entries
|
|
30
|
+
* scanned.
|
|
31
|
+
* User-defined roles are valid targets too — the GUI marks them with an asterisk, so clicking
|
|
32
|
+
* one must open its transcript: ids validate through rejectBadRole. The 400 message lists
|
|
33
|
+
* exactly the ids accepted. The query arrives pre-parsed — the server parses the target
|
|
34
|
+
* once (gui/gui-server.ts's parseRequestTarget) and threads it down, so the parse idiom lives
|
|
35
|
+
* in one place and cannot disagree with the routing or the token gate about what the URL said. */
|
|
36
|
+
export function handleTranscript(q, res, root) {
|
|
37
|
+
const role = q.get("role");
|
|
38
|
+
if (rejectBadRole(root, res, role))
|
|
39
|
+
return;
|
|
40
|
+
const n = intQuery(q, res, "n", "positive", 50);
|
|
41
|
+
if (n === null)
|
|
42
|
+
return;
|
|
43
|
+
const grep = (q.get("grep") ?? "").trim().toLowerCase();
|
|
44
|
+
const includePrompts = q.get("prompt") !== null;
|
|
45
|
+
sendJson(res, 200, {
|
|
46
|
+
lines: readTranscript(root, role, n, { includePrompts, grep: grep === "" ? undefined : grep }),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** Parse `result=<class>[,...]` for /api/run?last and /api/history: the deduplicated classes in
|
|
50
|
+
* first-seen order (`[]` filters nothing), or null after sending the 400 for an unknown class. */
|
|
51
|
+
function resultClassesQuery(q, res) {
|
|
52
|
+
const classes = [];
|
|
53
|
+
const names = Object.values(HISTORY_RESULT_FLAG_NAMES);
|
|
54
|
+
for (const piece of splitCommaList(q.get("result") ?? "")) {
|
|
55
|
+
const cls = parseHistoryResultClass(piece);
|
|
56
|
+
if (cls === null) {
|
|
57
|
+
badRequest(res, `result must be one of ${names.join(", ")}${gotSuffix(piece)}${typoSuffix(piece, names)}`);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (!classes.includes(cls))
|
|
61
|
+
classes.push(cls);
|
|
62
|
+
}
|
|
63
|
+
return classes;
|
|
64
|
+
}
|
|
65
|
+
/** Handle GET /api/history?role=<id>&n=N[&grep=<text>] — or ?role=<id>&sinceMs=<ms>[&untilMs=<ms>]
|
|
66
|
+
* for a trailing window (untilMs ends it that long ago, via untilMsQuery; without sinceMs → 400):
|
|
67
|
+
* the per-run rows the CLI's `tumwater history` derives from the event log,
|
|
68
|
+
* the dashboard's history tab renders them. The count shape ({ rows }) rides readRunRows (the
|
|
69
|
+
* shared scan, window growth included, so a role-filtered ask on a busy team cannot silently
|
|
70
|
+
* come back short); n is optional: absent → HISTORY_DEFAULT_RUNS; present but not a plain
|
|
71
|
+
* non-negative integer → 400 (the handleBacklog index discipline — an explicit count that does
|
|
72
|
+
* not parse is a client error, unlike the report's degrade-to-default typo rule); then clamped
|
|
73
|
+
* to [1, HISTORY_MAX_RUNS] — the GUI clamps where the CLI fails fast, its established
|
|
74
|
+
* convention. grep is optional: absent or empty filters nothing; present → the rows are
|
|
75
|
+
* filtered through history-data.ts's shared runRowsMatchingGrep rule, the same
|
|
76
|
+
* case-insensitive haystack rule `tumwater history --grep` uses (shared, not
|
|
77
|
+
* re-implemented), after the n/role scan so n bounds the scanned window and not the
|
|
78
|
+
* printed rows — the CLI's rule. The window shape ({ rows, covered, note }) rides
|
|
79
|
+
* readRunRowsSince, the collector behind `history --since`,
|
|
80
|
+
* so the browser and the CLI cannot disagree about the window's rows; a simultaneous n is a
|
|
81
|
+
* rival shape → 400 (the CLI's count-vs-window rule), sinceMs parses and is capped through
|
|
82
|
+
* sinceMsQuery (gui-args.ts, LOGS_SINCE_MAX_MS — the shared `history --since` cap), and note is
|
|
83
|
+
* SPARSE_WINDOW_NOTE when covered is false, else "". role is optional: absent or empty → all
|
|
84
|
+
* roles; present → passed straight through as the role filter with no config validation (a
|
|
85
|
+
* filter is not a target — an id with no runs legitimately yields zero rows), so the endpoint
|
|
86
|
+
* reads nothing but files, like /api/report and /api/failures. Reads files directly, so it
|
|
87
|
+
* present → passed straight through as the role filter with no config validation (a
|
|
88
|
+
* filter is not a target — an id with no runs legitimately yields zero rows), so the endpoint
|
|
89
|
+
* reads nothing but files, like /api/report and /api/failures. summary is optional: absent /
|
|
90
|
+
* empty adds nothing (the response is byte-for-byte today's); present (any value — the flag's
|
|
91
|
+
* mere presence, a bad value ignored like a missing one) adds an additive `summary` folding the
|
|
92
|
+
* exact served rows through the shared summarizeRunRows collector, the same tally
|
|
93
|
+
* `tumwater history --summary` prints, so the dashboard Summary and the table cannot disagree
|
|
94
|
+
* about what a run was. It also adds `modelSummary`, `{ models, total }`, the
|
|
95
|
+
* `history --summary --by model` tally of the same rows. `model=<selector>[,...]` keeps only the
|
|
96
|
+
* runs that started on exactly one of those models (the `history --model` rule), after `grep` and
|
|
97
|
+
* before the summaries fold. `result=<class>[,...]` keeps only the runs in those outcome classes
|
|
98
|
+
* (the `history --result` rule, parsed by resultClassesQuery like /api/run's), after `grep` and
|
|
99
|
+
* before `model`; an unknown class is a 400. Reads files directly, so it works whether
|
|
100
|
+
* or not the team is running. */
|
|
101
|
+
export function handleHistory(q, res, root) {
|
|
102
|
+
const role = q.get("role") || null; // "" and absent both read all roles
|
|
103
|
+
const grep = q.get("grep") || null; // "" and absent both filter nothing
|
|
104
|
+
// `model` is a comma list of exact selectors (trimmed, deduplicated); "", absent and a list
|
|
105
|
+
// with no non-empty piece all filter nothing, and an unknown selector keeps no rows.
|
|
106
|
+
const classes = resultClassesQuery(q, res);
|
|
107
|
+
if (classes === null)
|
|
108
|
+
return;
|
|
109
|
+
const inClasses = (rows) => (classes.length ? rows.filter((r) => classes.includes(historyClass(r.result))) : rows);
|
|
110
|
+
const modelPieces = splitCommaListUnique(q.get("model") ?? "");
|
|
111
|
+
const onModels = (rows) => (modelPieces.length ? runRowsOnModels(rows, modelPieces) : rows);
|
|
112
|
+
const summary = q.get("summary") !== null; // the flag's presence, whatever its value
|
|
113
|
+
if (q.get("sinceMs") !== null) {
|
|
114
|
+
if (q.get("n") !== null) {
|
|
115
|
+
badRequest(res, "sinceMs cannot be combined with n (a count and a window are rival shapes)");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const sinceMs = sinceMsQuery(q, res, LOGS_SINCE_MAX_MS, (ms) => `sinceMs is capped at ${humanSeconds(LOGS_SINCE_MAX_MS / 1000)} (got ${ms})`);
|
|
119
|
+
if (sinceMs === null)
|
|
120
|
+
return;
|
|
121
|
+
const untilMs = untilMsQuery(q, res, sinceMs);
|
|
122
|
+
if (untilMs === null)
|
|
123
|
+
return;
|
|
124
|
+
const { rows, covered } = readRunRowsSince(root, sinceMs, role, untilMs ?? null);
|
|
125
|
+
const matched = onModels(inClasses(grep === null ? rows : runRowsMatchingGrep(rows, grep)));
|
|
126
|
+
sendJson(res, 200, { rows: matched, covered, note: covered ? "" : SPARSE_WINDOW_NOTE, ...(summary ? { summary: summarizeRunRows(matched), modelSummary: summarizeRunRowsByModel(matched) } : {}) });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (untilMsQuery(q, res, undefined) === null)
|
|
130
|
+
return; // a count has no window end to move
|
|
131
|
+
const parsed = intQuery(q, res, "n", "non-negative", HISTORY_DEFAULT_RUNS);
|
|
132
|
+
if (parsed === null)
|
|
133
|
+
return;
|
|
134
|
+
const n = clampCount(parsed, HISTORY_MAX_RUNS);
|
|
135
|
+
let rows = readRunRows(root, n, role);
|
|
136
|
+
if (grep !== null)
|
|
137
|
+
rows = runRowsMatchingGrep(rows, grep);
|
|
138
|
+
rows = onModels(inClasses(rows));
|
|
139
|
+
sendJson(res, 200, summary ? { rows, summary: summarizeRunRows(rows), modelSummary: summarizeRunRowsByModel(rows) } : { rows });
|
|
140
|
+
}
|
|
141
|
+
/** How many of the log's newest events `GET /api/logs` scans by default, and the most it will
|
|
142
|
+
* scan however large `n` asks: the CLI's `-n` shape (n bounds the scanned tail, not the printed
|
|
143
|
+
* rows), sized so the dashboard's search can reach well past the RECENT_EVENTS window the
|
|
144
|
+
* status poll carries. */
|
|
145
|
+
const LOGS_SEARCH_EVENTS = 500;
|
|
146
|
+
const LOGS_SEARCH_MAX_EVENTS = 5000;
|
|
147
|
+
/** Handle GET /api/logs?n=N|sinceMs=S[&untilMs=U][&grep=<text>][&role=<id>]: a slice of the raw
|
|
148
|
+
* event log scanned from its newest end, filtered with the CLI's own rules and shaped as the
|
|
149
|
+
* activity feed's items. `n` is optional (absent → LOGS_SEARCH_EVENTS) and clamps to [1,
|
|
150
|
+
* LOGS_SEARCH_MAX_EVENTS] after intQuery's non-negative parse (an explicit count that does not
|
|
151
|
+
* parse is a 400, the handleHistory discipline); it bounds the scanned tail, so a matching event
|
|
152
|
+
* older than the last n events is absent — the same scan-then-filter rule `tumwater logs -n N
|
|
153
|
+
* --grep` applies. grep (absent or empty filters nothing) matches through eventMatchesGrep, and
|
|
154
|
+
* role is a filter, not a target: an id with no events legitimately yields an empty list, so it
|
|
155
|
+
* does not validate through rejectBadRole (the handleHistory rule). The matched events answer
|
|
156
|
+
* oldest-first as eventFeedItem objects — the same shape the status payload's eventItems carry.
|
|
157
|
+
* Reads the tail through readEvents' stat-keyed cache (no covered byte end, unlike the CLI's
|
|
158
|
+
* follow), so a dashboard search that repeats on an unchanged log — the debounced filter fires
|
|
159
|
+
* one per filter change — costs one stat instead of re-reading and re-parsing the scanned tail.
|
|
160
|
+
* With `sinceMs` instead of `n` it serves the trailing window's events (readEventsSince, the
|
|
161
|
+
* `logs --since` reader) as { events, covered, note }, the /api/history window shape: sinceMs
|
|
162
|
+
* with n is a 400 (rival shapes), sinceMs parses and is capped through sinceMsQuery at
|
|
163
|
+
* LOGS_SINCE_MAX_MS, the window is not row-capped, and note is SPARSE_WINDOW_NOTE when the log
|
|
164
|
+
* does not reach back that far; untilMs (untilMsQuery) drops the events newer than now −
|
|
165
|
+
* untilMs, and without sinceMs it is a 400.
|
|
166
|
+
* Reads files directly, so it works whether or not the team is running. */
|
|
167
|
+
export function handleLogs(q, res, root) {
|
|
168
|
+
const role = q.get("role") || null; // "" and absent both read all roles
|
|
169
|
+
const grep = q.get("grep") || null; // "" and absent both filter nothing
|
|
170
|
+
const needle = grep === null ? null : grep.toLowerCase();
|
|
171
|
+
const keep = (e) => (role === null || eventRole(e) === role) && (needle === null || eventMatchesGrep(e, needle));
|
|
172
|
+
if (q.get("sinceMs") !== null) {
|
|
173
|
+
if (q.get("n") !== null) {
|
|
174
|
+
badRequest(res, "sinceMs cannot be combined with n (a count and a window are rival shapes)");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const sinceMs = sinceMsQuery(q, res, LOGS_SINCE_MAX_MS, (ms) => `sinceMs is capped at ${humanSeconds(LOGS_SINCE_MAX_MS / 1000)} (got ${ms})`);
|
|
178
|
+
if (sinceMs === null)
|
|
179
|
+
return;
|
|
180
|
+
const untilMs = untilMsQuery(q, res, sinceMs);
|
|
181
|
+
if (untilMs === null)
|
|
182
|
+
return;
|
|
183
|
+
const { cutoff, events: windowEvents, covered } = readEventsSince(root, sinceMs);
|
|
184
|
+
const events = untilMs === undefined ? windowEvents : windowEvents.filter((e) => e.ts < cutoff + sinceMs - untilMs);
|
|
185
|
+
sendJson(res, 200, {
|
|
186
|
+
events: events.filter(keep).map(eventFeedItem),
|
|
187
|
+
covered,
|
|
188
|
+
note: covered ? "" : SPARSE_WINDOW_NOTE,
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (untilMsQuery(q, res, undefined) === null)
|
|
193
|
+
return; // a count has no window end to move
|
|
194
|
+
const parsed = intQuery(q, res, "n", "non-negative", LOGS_SEARCH_EVENTS);
|
|
195
|
+
if (parsed === null)
|
|
196
|
+
return;
|
|
197
|
+
const n = clampCount(parsed, LOGS_SEARCH_MAX_EVENTS);
|
|
198
|
+
sendJson(res, 200, { events: readEvents(root, n).filter(keep).map(eventFeedItem) });
|
|
199
|
+
}
|
|
200
|
+
/** handleRun's `transcript` text: the run's transcript lines joined, else the miss note. */
|
|
201
|
+
function transcriptText(root, role, run, detail, includePrompts, grep) {
|
|
202
|
+
const { found, note } = runTranscriptLines(root, role, run, detail, { includePrompts, grep });
|
|
203
|
+
return found.length > 0 ? found.join("\n") : note;
|
|
204
|
+
}
|
|
205
|
+
/** Handle GET /api/run?role=<id>&run=N: one run's full event trail — readRunDetail's
|
|
206
|
+
* RunDetail, the collector `tumwater history <role> <n>` prints from, plus `text`: the same
|
|
207
|
+
* payload through renderRunDetail, pre-rendered server-side like /api/transcript's lines so
|
|
208
|
+
* the browser shows the CLI's exact rendering instead of re-implementing the event formatting.
|
|
209
|
+
* Serving the shared collector is the point: the CLI and this endpoint cannot drift on what one
|
|
210
|
+
* run's block contains. The role is a target here, not a filter, so it validates through
|
|
211
|
+
* rejectBadRole (unknown/missing → 400 naming the valid ids); the run is an explicit count
|
|
212
|
+
* through intQuery (missing or not a positive integer → 400); and a run the scanned window
|
|
213
|
+
* does not hold — never ran, or rotation ate it — answers 404 with runNotFoundMessage, the
|
|
214
|
+
* CLI's exact not-found wording,
|
|
215
|
+
* never a crash. `revertSha` is the run's landed commit's full sha when it is on the base branch
|
|
216
|
+
* of a primary checkout that is on it (resolveRevertTarget, the revert cores' own check), else
|
|
217
|
+
* null — the drill-down offers its revert button only when it is set. `hasCommit` says whether
|
|
218
|
+
* the run pinned a commit (runCommitSha), for the "Show patch" button; `patch` — the same text
|
|
219
|
+
* `history --patch` prints — is added only when the `patch` flag is present, whatever its
|
|
220
|
+
* value, so the default expand costs no extra `git show`. `transcript`, likewise flag-gated, is
|
|
221
|
+
* the text `history --transcript` prints (runTranscriptLines: the found lines joined, else its
|
|
222
|
+
* note), so the default expand reads no pi log; a present `prompt` flag also renders each agent
|
|
223
|
+
* run's prompt, the text `history --transcript --prompt` prints. A non-blank `grep` (with
|
|
224
|
+
* `transcript`) keeps only the entries containing the text, case-insensitively, each under its
|
|
225
|
+
* agent run's separator, the text `history --transcript --grep` prints, or its miss note. Reads
|
|
226
|
+
* files directly, so it works whether or not the team is running.
|
|
227
|
+
*
|
|
228
|
+
* A present `last` flag replaces `run` with the role's newest completed run, the one
|
|
229
|
+
* `tumwater history <role> --last` opens (newestRunRow, shared): `result` (a comma list of
|
|
230
|
+
* outcome classes) and `model` (a comma list of selectors) narrow it to the newest run in those
|
|
231
|
+
* classes or on those models, an absent or empty value filtering nothing. `last` with `run`, and
|
|
232
|
+
* `result`/`model` without `last`, are 400s; a class that is not one is a 400 worded like the
|
|
233
|
+
* CLI's; no match answers 404 with runNotFoundLastMessage. The payload is the numbered form's,
|
|
234
|
+
* the resolved number in `run`. With no `role`, `last` opens the newest completed run of any role
|
|
235
|
+
* (newestRunRow with a null role, the run bare `tumwater history --last` opens), and the
|
|
236
|
+
* payload's `role` is that run's; `result`/`model` narrow it the same way, and a miss is a 404
|
|
237
|
+
* worded for any role. */
|
|
238
|
+
export async function handleRun(q, res, root) {
|
|
239
|
+
let role = q.get("role");
|
|
240
|
+
const bareLast = role === null && q.get("last") !== null;
|
|
241
|
+
if (!bareLast && rejectBadRole(root, res, role))
|
|
242
|
+
return;
|
|
243
|
+
let run;
|
|
244
|
+
if (q.get("last") === null) {
|
|
245
|
+
if (q.get("result") !== null)
|
|
246
|
+
return badRequest(res, "result needs last");
|
|
247
|
+
if (q.get("model") !== null)
|
|
248
|
+
return badRequest(res, "model needs last");
|
|
249
|
+
run = intQuery(q, res, "run", "positive");
|
|
250
|
+
if (run === null)
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
if (q.get("run") !== null)
|
|
255
|
+
return badRequest(res, "last cannot be combined with run (a number and the newest run are rival selectors)");
|
|
256
|
+
const classes = resultClassesQuery(q, res);
|
|
257
|
+
if (classes === null)
|
|
258
|
+
return;
|
|
259
|
+
const models = splitCommaListUnique(q.get("model") ?? "");
|
|
260
|
+
const classFilter = classes.length ? classes : null;
|
|
261
|
+
const modelFilter = models.length ? models : null;
|
|
262
|
+
const row = newestRunRow(root, role, classFilter, modelFilter);
|
|
263
|
+
if (row !== null)
|
|
264
|
+
role = row.role;
|
|
265
|
+
run = row?.run ?? null;
|
|
266
|
+
if (run === null) {
|
|
267
|
+
sendJson(res, 404, { error: runNotFoundLastMessage(role, classFilter, modelFilter) });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const detail = readRunDetail(root, role, run);
|
|
272
|
+
if (detail === null) {
|
|
273
|
+
sendJson(res, 404, { error: runNotFoundMessage(role, run) });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const grep = (q.get("grep") ?? "").trim();
|
|
277
|
+
const mergedAs = runMergedSha(detail);
|
|
278
|
+
const target = mergedAs === null ? null : await resolveRevertTarget(root, mergedAs);
|
|
279
|
+
sendJson(res, 200, {
|
|
280
|
+
...detail,
|
|
281
|
+
text: renderRunDetail(detail),
|
|
282
|
+
stat: await runDetailStat(root, detail),
|
|
283
|
+
revertSha: target?.ok ? target.full : null,
|
|
284
|
+
hasCommit: runCommitSha(detail) !== null,
|
|
285
|
+
...(q.get("patch") !== null ? { patch: await runDetailPatch(root, detail) } : {}),
|
|
286
|
+
...(q.get("transcript") !== null ? { transcript: transcriptText(root, role, run, detail, q.get("prompt") !== null, grep === "" ? undefined : grep) } : {}),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/** Handle GET /api/role?role=<id>: one role's inspection payload — rolePayload's object,
|
|
290
|
+
* the same collection behind `tumwater role <id>` — so the drawer's Notes and Next run
|
|
291
|
+
* prompt sections render what the CLI shows without re-implementing the assembly. The role
|
|
292
|
+
* is a target here, so it validates through rejectBadRole exactly like /api/transcript and
|
|
293
|
+
* /api/run (unknown/missing → the shared 400 naming the valid ids and correcting typos);
|
|
294
|
+
* rolePayload is only reached with an id rejectBadRole accepted, so its own unknown-role
|
|
295
|
+
* throw never fires. Reads files directly, so it works whether or not the team is running. */
|
|
296
|
+
export function handleRole(q, res, root) {
|
|
297
|
+
const role = q.get("role");
|
|
298
|
+
if (rejectBadRole(root, res, role))
|
|
299
|
+
return;
|
|
300
|
+
sendJson(res, 200, rolePayload(root, role));
|
|
301
|
+
}
|
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/**
|
|
3
2
|
* The dashboard's POST operator-lifecycle command handlers (barrelled into
|
|
4
3
|
* 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,
|
|
6
|
-
* handler that is not prompt/backlog/config/team. Each handler
|
|
4
|
+
* restart, reclaim, abort, stop-schedule, role-notes, retire, revert, brief-edit, and
|
|
5
|
+
* queue-cancel — every POST handler that is not prompt/backlog/config/team. Each handler
|
|
6
|
+
* adapts HTTP onto the domain work
|
|
7
7
|
* one layer down (operator-intent, role-notes-write, brief-edit) and touches no socket beyond
|
|
8
8
|
* its own `res`.
|
|
9
9
|
*/
|
|
10
10
|
import { cancelScheduledStopMessage, requestAbort, requestResetCounters, requestRestart, requestScheduledStop, runManualReclaim } from "../operator/operator-intent.js";
|
|
11
|
+
import { runDoctor } from "../doctor/doctor.js";
|
|
12
|
+
import { killOrphans, orphanKillLines } from "../doctor/doctor-orphans.js";
|
|
11
13
|
import { saveRoleNote } from "../agent/role-notes-write.js";
|
|
12
14
|
import { roleNotesPath } from "../paths.js";
|
|
13
15
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
14
16
|
import { artifactPhrase, retireRole } from "../operator/retire.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
+
import { previewRevert, revertFollowUp, revertMergedCommit } from "../operator/revert.js";
|
|
18
|
+
import { cancelQueuedMergeBySha } from "../merge/merge-cancel.js";
|
|
19
|
+
import { readMergeQueue } from "../merge/merge-queue.js";
|
|
20
|
+
import { mergeChanges } from "../merge/merge-slot.js";
|
|
21
|
+
import { queueInFlight } from "../status/status-polls.js";
|
|
22
|
+
import { orchestratorAlive } from "../team/orchestrator-info.js";
|
|
23
|
+
import { shortSha } from "../text/format.js";
|
|
24
|
+
import { editBriefText } from "../brief/brief-edit.js";
|
|
25
|
+
import { requireStringField, validRoleIds } from "./gui-args.js";
|
|
17
26
|
import { badRequest, sendJson } from "./http-body.js";
|
|
18
27
|
import { errorMessage, gotSuffix } from "../text/text.js";
|
|
19
28
|
import { readPostBody, readRoleBody, sendRequestResult } from "./gui-endpoint-common.js";
|
|
@@ -77,6 +86,27 @@ export async function handleReclaim(req, res, root) {
|
|
|
77
86
|
return;
|
|
78
87
|
sendJson(res, 200, { ok: true, message: await runManualReclaim(root) });
|
|
79
88
|
}
|
|
89
|
+
/** Handle POST /api/kill-orphans: the dashboard Health card's Kill orphans button — the same
|
|
90
|
+
* killOrphans `tumwater doctor --kill-orphans` runs, answering its lines joined with "; ". Same
|
|
91
|
+
* body discipline as /api/reclaim; `ok` is false when the scan failed or a signal was refused. */
|
|
92
|
+
export async function handleKillOrphans(req, res, root, kill = killOrphans) {
|
|
93
|
+
const body = await readPostBody(req, res, "{}");
|
|
94
|
+
if (!body)
|
|
95
|
+
return;
|
|
96
|
+
const report = await kill(root);
|
|
97
|
+
sendJson(res, 200, { ok: !("error" in report) && report.failed.length === 0, message: orphanKillLines(report).join("; ") });
|
|
98
|
+
}
|
|
99
|
+
/** Handle POST /api/doctor-reprobe: the dashboard Health card's Re-probe models button, the
|
|
100
|
+
* twin of `tumwater doctor --reprobe`. A POST because it spends one model call per claimed pi
|
|
101
|
+
* image-input model and rewrites `.tumwater/state/image-probes.json`; GET /api/doctor stays
|
|
102
|
+
* probe-free. Same body discipline as /api/kill-orphans; `run` takes the probe mode so a test
|
|
103
|
+
* can pin that the handler asks for "all" without spawning a probe. */
|
|
104
|
+
export async function handleDoctorReprobe(req, res, root, run = (r, p) => runDoctor(r, undefined, undefined, p)) {
|
|
105
|
+
const body = await readPostBody(req, res, "{}");
|
|
106
|
+
if (!body)
|
|
107
|
+
return;
|
|
108
|
+
sendJson(res, 200, await run(root, "all"));
|
|
109
|
+
}
|
|
80
110
|
/** Handle POST /api/abort: the dashboard's per-row abort control — the same marker-writing
|
|
81
111
|
* core `tumwater abort` calls (requestAbort). The role is required and validated like
|
|
82
112
|
* /api/transcript; requestAbort's not-live error comes back 409 — the marker is valid but
|
|
@@ -88,7 +118,7 @@ export async function handleAbort(req, res, root) {
|
|
|
88
118
|
return;
|
|
89
119
|
sendRequestResult(res, requestAbort(root, body.role));
|
|
90
120
|
}
|
|
91
|
-
/** Handle POST /api/role-notes: the
|
|
121
|
+
/** Handle POST /api/role-notes: the role drawer's notes Edit/Clear — the same saveRoleNote
|
|
92
122
|
* `tumwater role <id> --notes-file` / `--clear-notes` call, so the 4 KB cap and its wording
|
|
93
123
|
* cannot drift. `"text": ""` clears. The director has no notes (400, the CLI's wording); a
|
|
94
124
|
* non-string text is a 400 before anything is written; an over-cap note is a 400 carrying
|
|
@@ -100,10 +130,11 @@ export async function handleRoleNotes(req, res, root) {
|
|
|
100
130
|
const role = body.role;
|
|
101
131
|
if (role === DIRECTOR_ROLE)
|
|
102
132
|
return badRequest(res, "the director has no notes");
|
|
103
|
-
|
|
104
|
-
|
|
133
|
+
const text = requireStringField(res, body, "text", `the full replacement note, or "" to clear it`);
|
|
134
|
+
if (text === null)
|
|
135
|
+
return;
|
|
105
136
|
try {
|
|
106
|
-
sendJson(res, 200, { ok: true, message: saveRoleNote(roleNotesPath(root, role),
|
|
137
|
+
sendJson(res, 200, { ok: true, message: saveRoleNote(roleNotesPath(root, role), text) });
|
|
107
138
|
}
|
|
108
139
|
catch (err) {
|
|
109
140
|
badRequest(res, errorMessage(err));
|
|
@@ -129,16 +160,80 @@ export async function handleRetire(req, res, root) {
|
|
|
129
160
|
sendJson(res, 409, { error: errorMessage(err) });
|
|
130
161
|
}
|
|
131
162
|
}
|
|
132
|
-
/** Handle POST /api/
|
|
163
|
+
/** Handle POST /api/revert: the History drill-down's "Revert this change" button — the same
|
|
164
|
+
* cores `tumwater revert <sha> [--dry-run]` calls. A `sha` that is not 4-64 hex digits (so never
|
|
165
|
+
* a flag or a ref name) or a non-boolean `dryRun` is a 400 and runs nothing. `dryRun: true`
|
|
166
|
+
* answers `{ ok, preview }` (previewRevert, writing nothing); otherwise the revert commits and
|
|
167
|
+
* answers `{ ok, headSha, beforeSha, message }`. A core's refusal is a 409 with its wording. */
|
|
168
|
+
export async function handleRevert(req, res, root) {
|
|
169
|
+
const body = await readPostBody(req, res, '{"sha": "1a2b3c4"}');
|
|
170
|
+
if (!body)
|
|
171
|
+
return;
|
|
172
|
+
if (typeof body.sha !== "string" || !/^[0-9a-f]{4,64}$/i.test(body.sha))
|
|
173
|
+
return badRequest(res, `"sha" must be a hex commit id of 4-64 digits${gotSuffix(body.sha)}`);
|
|
174
|
+
if (body.dryRun !== undefined && typeof body.dryRun !== "boolean")
|
|
175
|
+
return badRequest(res, `"dryRun" must be true or false${gotSuffix(body.dryRun)}`);
|
|
176
|
+
if (body.dryRun === true) {
|
|
177
|
+
const preview = await previewRevert(root, body.sha);
|
|
178
|
+
if (!preview.ok)
|
|
179
|
+
return sendJson(res, 409, { error: preview.error });
|
|
180
|
+
return sendJson(res, 200, { ok: true, preview });
|
|
181
|
+
}
|
|
182
|
+
const result = await revertMergedCommit(root, body.sha);
|
|
183
|
+
if (!result.ok)
|
|
184
|
+
return sendJson(res, 409, { error: result.error });
|
|
185
|
+
const followUp = revertFollowUp(result.subject);
|
|
186
|
+
sendJson(res, 200, {
|
|
187
|
+
ok: true,
|
|
188
|
+
headSha: result.headSha,
|
|
189
|
+
beforeSha: result.beforeSha,
|
|
190
|
+
subject: result.subject,
|
|
191
|
+
message: `main is now ${shortSha(result.headSha)}${followUp === null ? "" : ` — ${followUp}`}`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** Handle POST /api/queue-cancel: the Merge queue card row's two-click Cancel — the browser
|
|
195
|
+
* twin of `tumwater queue --cancel <n>`, resolving by pinned sha (the same removal
|
|
196
|
+
* `cancelQueuedMergeBySha` the CLI's position variant shares). A `sha` that is not 4-64 hex
|
|
197
|
+
* digits is a 400 and runs nothing. A sha a live merge marker names mid-merge is a 409 with the
|
|
198
|
+
* wait-then-revert wording and changes nothing; a sha no longer queued (a concurrent drain
|
|
199
|
+
* raced the click) is a 200 `{ status: "gone" }` data, not an error. Otherwise it answers the
|
|
200
|
+
* cancelled entry's role/commit/summary and removes the queue file and pin. */
|
|
201
|
+
export async function handleQueueCancel(req, res, root) {
|
|
202
|
+
const body = await readPostBody(req, res, '{"sha": "1a2b3c4"}');
|
|
203
|
+
if (!body)
|
|
204
|
+
return;
|
|
205
|
+
if (typeof body.sha !== "string" || !/^[0-9a-f]{4,64}$/i.test(body.sha))
|
|
206
|
+
return badRequest(res, `"sha" must be a hex commit id of 4-64 digits${gotSuffix(body.sha)}`);
|
|
207
|
+
const view = readMergeQueue(root);
|
|
208
|
+
const sha = body.sha.toLowerCase();
|
|
209
|
+
const inFlight = queueInFlight(root, view.entries, orchestratorAlive(root));
|
|
210
|
+
if (inFlight && ((typeof inFlight.sha === "string" && inFlight.sha.toLowerCase() === sha) || mergeChanges(inFlight).some((c) => c.sha.toLowerCase() === sha)))
|
|
211
|
+
return sendJson(res, 409, { error: `this merge is being merged right now — wait for it to land, then \`tumwater revert ${shortSha(body.sha)}\`` });
|
|
212
|
+
const outcome = await cancelQueuedMergeBySha(root, body.sha);
|
|
213
|
+
if (outcome.status === "gone")
|
|
214
|
+
return sendJson(res, 200, { ok: true, status: "gone" });
|
|
215
|
+
if (outcome.status !== "cancelled")
|
|
216
|
+
return;
|
|
217
|
+
sendJson(res, 200, {
|
|
218
|
+
ok: true,
|
|
219
|
+
status: "cancelled",
|
|
220
|
+
role: outcome.role,
|
|
221
|
+
commit: outcome.sha,
|
|
222
|
+
summary: outcome.summary,
|
|
223
|
+
message: `cancelled the merge of ${shortSha(outcome.sha)} — ${outcome.summary}`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/** Handle POST /api/brief: replace the brief text through editBriefText — the same
|
|
133
227
|
* checks, locked write, and commit as `tumwater brief --file`. A non-string prompt or any
|
|
134
228
|
* refusal is a 400 and writes nothing. */
|
|
135
229
|
export async function handleBriefEdit(req, res, root) {
|
|
136
230
|
const body = await readPostBody(req, res, '{"prompt": "…"}');
|
|
137
231
|
if (!body)
|
|
138
232
|
return;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
233
|
+
const prompt = requireStringField(res, body, "prompt", "the full replacement brief");
|
|
234
|
+
if (prompt === null)
|
|
235
|
+
return;
|
|
236
|
+
const outcome = await editBriefText(root, prompt);
|
|
142
237
|
if (!outcome.ok)
|
|
143
238
|
return badRequest(res, outcome.error);
|
|
144
239
|
sendJson(res, 200, { file: outcome.file, changed: outcome.changed, committed: outcome.committed });
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/**
|
|
3
2
|
* The dashboard's POST prompt-queueing command handlers (barrelled into
|
|
4
3
|
* gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts): prompt,
|
|
@@ -19,7 +18,7 @@ import { PAUSE_FOR_MAX_MS, submitRolePromptAndWake } from "../operator/operator-
|
|
|
19
18
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
20
19
|
import { rejectBadQueueFileName, rejectBadRole, requirePromptText } from "./gui-args.js";
|
|
21
20
|
import { badRequest, OK_GONE, sendJson } from "./http-body.js";
|
|
22
|
-
import { errorMessage, gotSuffix } from "../text/text.js";
|
|
21
|
+
import { errorMessage, gotSuffix, joinParts } from "../text/text.js";
|
|
23
22
|
import { readPostBody, readRoleBody } from "./gui-endpoint-common.js";
|
|
24
23
|
/** Handle POST /api/prompt: queue a director prompt. An over-long prompt
|
|
25
24
|
* (DIRECTOR_PROMPT_MAX_CHARS) is a user-input error, not a server fault: the shared length
|
|
@@ -93,10 +92,10 @@ function checkedPromptFields(res, body, role = DIRECTOR_ROLE) {
|
|
|
93
92
|
return null;
|
|
94
93
|
return { text, images, notBeforeMs };
|
|
95
94
|
}
|
|
96
|
-
/** Handle POST /api/prompt-role: queue a prompt for one
|
|
95
|
+
/** Handle POST /api/prompt-role: queue a prompt for one role — the dashboard's per-row
|
|
97
96
|
* prompt affordance submits here, calling the same path `tumwater prompt --role <id>` uses
|
|
98
|
-
* (submitRolePrompt enqueues into that
|
|
99
|
-
* wake brings the live
|
|
97
|
+
* (submitRolePrompt enqueues into that role's own queue and logs under it, then a single-role
|
|
98
|
+
* wake brings the live role in within one poll; the wake message rides back for the flash).
|
|
100
99
|
* The role validates exactly like /api/transcript (the shared rejectBadRole wording), and the
|
|
101
100
|
* body discipline is /api/prompt's: readJsonObject → 400 malformed/non-object, 413 oversized,
|
|
102
101
|
* text a non-empty string within the shared length rule → 400 otherwise — plus the same
|
|
@@ -110,9 +109,9 @@ export async function handlePromptRole(req, res, root) {
|
|
|
110
109
|
if (!fields)
|
|
111
110
|
return;
|
|
112
111
|
// Image-input submit gate (plans/image-input.md "Attachments", part 5/5): an attachment the
|
|
113
|
-
// target
|
|
112
|
+
// target role cannot read (and that no tier reroutes) answers 400 naming the fix before
|
|
114
113
|
// anything is queued — the same gate submitRolePrompt enforces, surfaced as a user-input
|
|
115
|
-
// error rather than the server's 500. A
|
|
114
|
+
// error rather than the server's 500. A role that reroutes a blind read is not refused; its
|
|
116
115
|
// notice rides in the confirmation the composer flashes.
|
|
117
116
|
let notice = "";
|
|
118
117
|
if (fields.images && fields.images.length > 0) {
|
|
@@ -123,12 +122,10 @@ export async function handlePromptRole(req, res, root) {
|
|
|
123
122
|
}
|
|
124
123
|
notice = guidance.notice ?? "";
|
|
125
124
|
}
|
|
126
|
-
const message = [
|
|
125
|
+
const message = joinParts([
|
|
127
126
|
submitRolePromptAndWake(root, role, fields.text, fields.images, fields.notBeforeMs),
|
|
128
127
|
notice,
|
|
129
|
-
]
|
|
130
|
-
.filter(Boolean)
|
|
131
|
-
.join("\n");
|
|
128
|
+
], "\n");
|
|
132
129
|
sendJson(res, 200, { ok: true, message });
|
|
133
130
|
}
|
|
134
131
|
/** Handle POST /api/prompt-cancel: retract one queued prompt addressed by its queue file —
|
|
@@ -136,7 +133,7 @@ export async function handlePromptRole(req, res, root) {
|
|
|
136
133
|
* position-addressed `prompt --cancel`. The role is optional (absent cancels from the
|
|
137
134
|
* director's queue) and validates exactly like /api/transcript; a file name failing the
|
|
138
135
|
* shared rejectBadQueueFileName guard is a user-input error answered 400 with
|
|
139
|
-
* nothing touched on disk. A vanished file answers 200 { status: "gone" } — the
|
|
136
|
+
* nothing touched on disk. A vanished file answers 200 { status: "gone" } — the role already
|
|
140
137
|
* dequeued the prompt, which is data for the flash line, not a server fault. Same body
|
|
141
138
|
* discipline as /api/prompt (readJsonObject → 400 malformed/non-object, 413 oversized). */
|
|
142
139
|
export async function handlePromptCancel(req, res, root) {
|
|
@@ -161,7 +158,7 @@ export async function handlePromptCancel(req, res, root) {
|
|
|
161
158
|
* and images validate through the same checkedPromptFields /api/prompt and /api/prompt-role
|
|
162
159
|
* use; editQueuedFile throws for a bad name or an over-cap text — both user-input errors
|
|
163
160
|
* answered 400 (a filesystem failure reads as 400 like handleConfigSet's failed write). A
|
|
164
|
-
* vanished file answers 200 { ok: true, status: "gone" } — the
|
|
161
|
+
* vanished file answers 200 { ok: true, status: "gone" } — the role already dequeued the
|
|
165
162
|
* prompt, which is data for the flash line, not a server error; an edit answers 200 with the
|
|
166
163
|
* old and new operator-facing text the client flashes. Same body discipline as /api/prompt
|
|
167
164
|
* (readJsonObject → 400 malformed/non-object, 413 oversized). */
|
|
@@ -179,7 +176,7 @@ export async function handlePromptEdit(req, res, root) {
|
|
|
179
176
|
return;
|
|
180
177
|
// Image-input gate (plans/image-input.md "Attachments", part 5/5), the edit twin of
|
|
181
178
|
// handlePromptRole's: an image edited into the queued prompt answers 400 naming the fix
|
|
182
|
-
// when the target
|
|
179
|
+
// when the target role cannot read it (and no tier reroutes it) — the same gate
|
|
183
180
|
// submitRolePrompt enforces — before anything is written.
|
|
184
181
|
if (fields.images && fields.images.length > 0) {
|
|
185
182
|
const guidance = imagePromptGuidance(root, role);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dashboard's GET report and failures handlers (src/gui/gui-server.ts routes to them):
|
|
3
|
+
* report and failures. Each answers its request and touches no socket beyond its own `res`;
|
|
4
|
+
* the domain work lives one layer down (report/report-data.ts, failure/failure-data.ts), so
|
|
5
|
+
* this module only adapts HTTP onto it. Barrelled into gui/gui-endpoints.ts beside the other
|
|
6
|
+
* per-family GET endpoint modules.
|
|
7
|
+
*/
|
|
8
|
+
import { collectReport, collectReportSince } from "../report/report-data.js";
|
|
9
|
+
import { collectFailureReport, collectFailureReportSince } from "../failure/failure-data.js";
|
|
10
|
+
import { renderFailureMarkdown } from "../failure/failure-render.js";
|
|
11
|
+
import { renderSinceReportMarkdown } from "../report/report-render.js";
|
|
12
|
+
import { REPORT_SINCE_MAX_MS, reportSinceMsg } from "../events/event-window.js";
|
|
13
|
+
import { roleListQuery, sinceMsQuery, untilMsQuery, windowDays } from "./gui-args.js";
|
|
14
|
+
import { badRequest, sendJson } from "./http-body.js";
|
|
15
|
+
/** Parse and validate the role-scoped, sinceMs-or-days query the report and failures endpoints
|
|
16
|
+
* both take: the `?role=` filter, a comma-separated list (each id trimmed, deduplicated, and
|
|
17
|
+
* validated by rejectBadRole — an unknown id or an empty list → 400 naming it) and, when
|
|
18
|
+
* `?sinceMs=` is present, the trailing-window length (reportSinceMs, which 400s a simultaneous
|
|
19
|
+
* `?days=`). Returns null once a 400 is sent. `sinceMs` is defined exactly when the caller
|
|
20
|
+
* should use the since shape; otherwise it falls through to the days shape with `windowDays(q)`.
|
|
21
|
+
* The one home of that shared scaffolding, so the two endpoints cannot drift on the role rule or
|
|
22
|
+
* the days-vs-sinceMs mutual exclusion (the server twin of reportEndpoint on the GUI client). */
|
|
23
|
+
function reportScope(q, res, root) {
|
|
24
|
+
const roles = roleListQuery(q, res, root);
|
|
25
|
+
if (roles === null)
|
|
26
|
+
return null;
|
|
27
|
+
if (q.get("sinceMs") !== null) {
|
|
28
|
+
const sinceMs = reportSinceMs(q, res);
|
|
29
|
+
if (sinceMs === null)
|
|
30
|
+
return null;
|
|
31
|
+
return { roles, sinceMs };
|
|
32
|
+
}
|
|
33
|
+
return { roles };
|
|
34
|
+
}
|
|
35
|
+
/** Parse and bound the report endpoints' `?sinceMs=` trailing-window length: a simultaneous
|
|
36
|
+
* `?days=` is the CLI's rival shape (whole local days vs a trailing window) → 400 naming both;
|
|
37
|
+
* otherwise the length rides sinceMsQuery (gui-args.ts) — positive-integer parse, then a 400
|
|
38
|
+
* naming the cap rather than a silent clamp (an explicit window typo must not shrink to some
|
|
39
|
+
* other window) — with the "must be between 1 and …" wording. Returns the parsed length, or
|
|
40
|
+
* null once the 400 is sent. */
|
|
41
|
+
function reportSinceMs(q, res) {
|
|
42
|
+
if (q.get("days") !== null) {
|
|
43
|
+
badRequest(res, "days and sinceMs are mutually exclusive; days counts whole local days, sinceMs totals a trailing window");
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return sinceMsQuery(q, res, REPORT_SINCE_MAX_MS, reportSinceMsg);
|
|
47
|
+
}
|
|
48
|
+
/** Handle GET /api/report?days=N[&role=<id>[,<id>...]] or
|
|
49
|
+
* /api/report?sinceMs=<ms>[&untilMs=<ms>][&role=<id>[,<id>...]]: the
|
|
50
|
+
* usage report — the day series (collectReport's ReportData) as JSON the dashboard's report tab
|
|
51
|
+
* charts, or, when sinceMs is set, the trailing-window totals rendered through
|
|
52
|
+
* renderSinceReportMarkdown as { markdown } (the CLI's `report --since` shape). The day window
|
|
53
|
+
* follows windowDays. An absent role is the team-wide report; the listed roles, summed
|
|
54
|
+
* (each id validated by reportScope). Reads files directly, so it works whether or not the team
|
|
55
|
+
* is running. */
|
|
56
|
+
export function handleReport(q, res, root) {
|
|
57
|
+
const scope = reportScope(q, res, root);
|
|
58
|
+
if (scope === null)
|
|
59
|
+
return;
|
|
60
|
+
const untilMs = untilMsQuery(q, res, scope.sinceMs);
|
|
61
|
+
if (untilMs === null)
|
|
62
|
+
return;
|
|
63
|
+
if (scope.sinceMs !== undefined) {
|
|
64
|
+
sendJson(res, 200, { markdown: renderSinceReportMarkdown(collectReportSince(root, scope.sinceMs, scope.roles, untilMs)) });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
sendJson(res, 200, collectReport(root, windowDays(q), scope.roles));
|
|
68
|
+
}
|
|
69
|
+
/** Handle GET /api/failures?days=N[&role=<id>[,<id>...]] or
|
|
70
|
+
* /api/failures?sinceMs=<ms>[&untilMs=<ms>][&role=<id>[,<id>...]]: the
|
|
71
|
+
* same bounded Markdown failure digest the selfcheck role feeds on and `tumwater report
|
|
72
|
+
* --failures` prints, as JSON ({ markdown }) — the dashboard's failures tab renders it; with
|
|
73
|
+
* sinceMs it is the trailing-window digest (collectFailureReportSince) through the same render,
|
|
74
|
+
* the CLI's `report --failures --since` shape. The days window follows windowDays (so it can
|
|
75
|
+
* never drift from /api/report's). role rides the same rule as
|
|
76
|
+
* handleReport: absent → the team digest, present → the validated listed roles, summed. Reads files
|
|
77
|
+
* directly, so it works with no team running. */
|
|
78
|
+
export function handleFailures(q, res, root) {
|
|
79
|
+
const scope = reportScope(q, res, root);
|
|
80
|
+
if (scope === null)
|
|
81
|
+
return;
|
|
82
|
+
const untilMs = untilMsQuery(q, res, scope.sinceMs);
|
|
83
|
+
if (untilMs === null)
|
|
84
|
+
return;
|
|
85
|
+
if (scope.sinceMs !== undefined) {
|
|
86
|
+
sendJson(res, 200, { markdown: renderFailureMarkdown(collectFailureReportSince(root, scope.sinceMs, scope.roles, untilMs)) });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
sendJson(res, 200, { markdown: renderFailureMarkdown(collectFailureReport(root, windowDays(q), scope.roles)) });
|
|
90
|
+
}
|