tumwater 0.4.2 → 0.4.4
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 +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -87,6 +87,62 @@ export function handleBacklogMatch(q, res, root) {
|
|
|
87
87
|
.map((row) => ({ role: row.role, file: row.file }));
|
|
88
88
|
sendJson(res, 200, out);
|
|
89
89
|
}
|
|
90
|
+
/** Normalise an optional `?path=` query arg to `{ path }` (absent → `{ path: undefined }`),
|
|
91
|
+
* answering the shared 400 when the raw value does not name a repo-relative path. Shared by
|
|
92
|
+
* the `?overlaps` and `?role` arms; `null` means the 400 was sent, so the caller returns. */
|
|
93
|
+
function diffPathParam(res, rawPath) {
|
|
94
|
+
if (rawPath === null)
|
|
95
|
+
return { path: undefined };
|
|
96
|
+
const path = normalizeDiffPath(rawPath);
|
|
97
|
+
if (path === null) {
|
|
98
|
+
badRequest(res, "path takes a repo-relative path such as src/cli.ts (as overlaps lists it)");
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return { path };
|
|
102
|
+
}
|
|
103
|
+
/** The `?overlaps` view: refuse stat, normalise the path, resolve the comma-separated role
|
|
104
|
+
* list, collect the overlaps and scope them by roles then by path, then send. */
|
|
105
|
+
async function handleDiffOverlaps(q, res, root, rawPath, stat) {
|
|
106
|
+
if (stat) {
|
|
107
|
+
badRequest(res, "overlaps lists paths, not patches; drop stat");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const opath = diffPathParam(res, rawPath);
|
|
111
|
+
if (opath === null)
|
|
112
|
+
return;
|
|
113
|
+
const roles = roleListQuery(q, res, root);
|
|
114
|
+
if (roles === null)
|
|
115
|
+
return;
|
|
116
|
+
let view = await collectChangeOverlaps(root);
|
|
117
|
+
if (roles !== undefined)
|
|
118
|
+
view = scopeChangeOverlaps(view, roles);
|
|
119
|
+
if (opath.path !== undefined)
|
|
120
|
+
view = scopeChangeOverlapsToPath(view, opath.path);
|
|
121
|
+
sendJson(res, 200, view);
|
|
122
|
+
}
|
|
123
|
+
/** The role-less team roster: refuse stat and path with their own 400s, then send
|
|
124
|
+
* collectTeamChanges's document. */
|
|
125
|
+
async function handleDiffTeam(res, root, rawPath, stat) {
|
|
126
|
+
if (stat) {
|
|
127
|
+
badRequest(res, "stat needs role (the team roster carries no patch)");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (rawPath !== null) {
|
|
131
|
+
badRequest(res, "path needs role (the team roster carries no patch)");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
sendJson(res, 200, await collectTeamChanges(root));
|
|
135
|
+
}
|
|
136
|
+
/** The `?role=<id>` per-role view: validate through rejectBadRole, normalise the shared path,
|
|
137
|
+
* then send collectRoleChange with its path and stat options. */
|
|
138
|
+
async function handleDiffRole(res, root, role, rawPath, stat) {
|
|
139
|
+
if (rejectBadRole(root, res, role))
|
|
140
|
+
return;
|
|
141
|
+
const path = diffPathParam(res, rawPath);
|
|
142
|
+
if (path === null)
|
|
143
|
+
return;
|
|
144
|
+
sendJson(res, 200, await collectRoleChange(root, role, { ...(path.path === undefined ? {} : { path: path.path }), ...(stat ? { stat: true } : {}) }));
|
|
145
|
+
}
|
|
90
146
|
/** Handle GET /api/diff[?role=<id>]: the unmerged-change view the CLI prints from the same
|
|
91
147
|
* collectors — no `role` → collectTeamChanges (exactly `tumwater diff --json`'s document:
|
|
92
148
|
* `{ mainBranch, roles }`, no per-role patch halves); `?role=<id>` → collectRoleChange (the
|
|
@@ -105,46 +161,10 @@ export function handleBacklogMatch(q, res, root) {
|
|
|
105
161
|
export async function handleDiff(q, res, root) {
|
|
106
162
|
const rawPath = q.get("path");
|
|
107
163
|
const stat = q.get("stat") !== null;
|
|
108
|
-
if (q.get("overlaps") !== null)
|
|
109
|
-
|
|
110
|
-
badRequest(res, "overlaps lists paths, not patches; drop stat");
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const opath = rawPath === null ? undefined : normalizeDiffPath(rawPath);
|
|
114
|
-
if (opath === null) {
|
|
115
|
-
badRequest(res, "path takes a repo-relative path such as src/cli.ts (as overlaps lists it)");
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const roles = roleListQuery(q, res, root);
|
|
119
|
-
if (roles === null)
|
|
120
|
-
return;
|
|
121
|
-
let view = await collectChangeOverlaps(root);
|
|
122
|
-
if (roles !== undefined)
|
|
123
|
-
view = scopeChangeOverlaps(view, roles);
|
|
124
|
-
if (opath !== undefined)
|
|
125
|
-
view = scopeChangeOverlapsToPath(view, opath);
|
|
126
|
-
sendJson(res, 200, view);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
164
|
+
if (q.get("overlaps") !== null)
|
|
165
|
+
return handleDiffOverlaps(q, res, root, rawPath, stat);
|
|
129
166
|
const role = q.get("role");
|
|
130
|
-
if (role === null)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (rawPath !== null) {
|
|
136
|
-
badRequest(res, "path needs role (the team roster carries no patch)");
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
sendJson(res, 200, await collectTeamChanges(root));
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (rejectBadRole(root, res, role))
|
|
143
|
-
return;
|
|
144
|
-
const path = rawPath === null ? undefined : normalizeDiffPath(rawPath);
|
|
145
|
-
if (path === null) {
|
|
146
|
-
badRequest(res, "path takes a repo-relative path such as src/cli.ts (as overlaps lists it)");
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
sendJson(res, 200, await collectRoleChange(root, role, { ...(path === undefined ? {} : { path }), ...(stat ? { stat: true } : {}) }));
|
|
167
|
+
if (role === null)
|
|
168
|
+
return handleDiffTeam(res, root, rawPath, stat);
|
|
169
|
+
return handleDiffRole(res, root, role, rawPath, stat);
|
|
150
170
|
}
|
|
@@ -32,29 +32,19 @@ export function lanAddresses(interfaces = os.networkInterfaces()) {
|
|
|
32
32
|
* /proc/<pid>/cmdline exposes to every local user and what a reload re-runs verbatim — so the
|
|
33
33
|
* secret must travel by env, never argv. */
|
|
34
34
|
export const GUI_TOKEN_ENV = "TUMWATER_GUI_TOKEN";
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const port = portRaw !== null ? parsePortFlag(portRaw) : 7180;
|
|
49
|
-
const allInterfaces = args.includes("--all-interfaces");
|
|
50
|
-
// A valueless or empty --token is a CLI error, not an open server: an operator who
|
|
51
|
-
// asked for protection must never silently get none. The validation and its wording
|
|
52
|
-
// live in tokenValueProblem, shared with `start --gui` so the two callers cannot
|
|
53
|
-
// drift; its flag-looking-value case names `tumwater gui` here.
|
|
54
|
-
const problem = tokenValueProblem(args, "tumwater gui");
|
|
55
|
-
if (problem)
|
|
56
|
-
fail(problem);
|
|
57
|
-
const flagToken = flagValue(args, "--token");
|
|
35
|
+
/** Resolve cmdGui's four injectable seams to their runnable forms: the server it starts, the
|
|
36
|
+
* LAN address source the banner names, the printer for the banner and warnings, and the
|
|
37
|
+
* serve-until-Ctrl+C wait. Production callers omit every seam and get the real server,
|
|
38
|
+
* interfaces, stdout, and wait. */
|
|
39
|
+
function resolveGuiSeams(seams) {
|
|
40
|
+
return {
|
|
41
|
+
start: seams.startGui ?? startGui,
|
|
42
|
+
lan: seams.lanAddresses ?? lanAddresses,
|
|
43
|
+
print: seams.say ?? say,
|
|
44
|
+
serve: seams.serve ?? (() => new Promise(() => { })),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function adoptGuiToken(flagToken, print) {
|
|
58
48
|
// A direct `tumwater gui --token` puts the secret on its own argv — readable by any local
|
|
59
49
|
// user and re-run verbatim by reexecSelf onto every reloaded generation — so fold it into
|
|
60
50
|
// the env and take it off the command line before a redeploy can re-expose it. A child
|
|
@@ -66,7 +56,10 @@ export async function cmdGui(root, args, seams = {}) {
|
|
|
66
56
|
process.argv.splice(argvToken, 2);
|
|
67
57
|
print(`--token is visible to every local user in this process's command line (\`ps\`, /proc/<pid>/cmdline) and is re-run onto each reloaded generation — prefer the ${GUI_TOKEN_ENV} env var`);
|
|
68
58
|
}
|
|
69
|
-
|
|
59
|
+
return flagToken ?? process.env[GUI_TOKEN_ENV] ?? "";
|
|
60
|
+
}
|
|
61
|
+
/** Start the server and translate the two common listen failures into hints naming their fix. */
|
|
62
|
+
async function startGuiOrExplain(start, root, port, allInterfaces, token) {
|
|
70
63
|
try {
|
|
71
64
|
await start(root, port, allInterfaces, token);
|
|
72
65
|
}
|
|
@@ -83,6 +76,9 @@ export async function cmdGui(root, args, seams = {}) {
|
|
|
83
76
|
throw new Error(`port ${port} could not be opened (permission denied) — ports below 1024 need root; pick an unprivileged port with \`tumwater gui --port <n>\``);
|
|
84
77
|
throw err;
|
|
85
78
|
}
|
|
79
|
+
}
|
|
80
|
+
/** Print the localhost banner and, under --all-interfaces, the LAN URLs and exposure wording. */
|
|
81
|
+
function printGuiBanner(print, lan, port, allInterfaces, token) {
|
|
86
82
|
const tokenSuffix = token ? `/?token=${encodeURIComponent(token)}` : "";
|
|
87
83
|
print(`tumwater gui at http://127.0.0.1:${port}${tokenSuffix} — Ctrl+C to stop`);
|
|
88
84
|
if (allInterfaces) {
|
|
@@ -95,5 +91,28 @@ export async function cmdGui(root, args, seams = {}) {
|
|
|
95
91
|
? "listening on ALL interfaces — token-protected; prompting the director requires the token"
|
|
96
92
|
: "listening on ALL interfaces — no auth; anyone reaching it can prompt the director");
|
|
97
93
|
}
|
|
94
|
+
}
|
|
95
|
+
/** The `tumwater gui` command: parse the --port/--all-interfaces/--token flags, start the
|
|
96
|
+
* server, print the banner, and serve until Ctrl+C. Lives beside startGui (gui/gui-server.ts) so
|
|
97
|
+
* the CLI's gui-specific policy (token validation, the listen-failure messages, the LAN
|
|
98
|
+
* announcement) cannot drift from the server it drives. Flag-vocabulary rejection stays in
|
|
99
|
+
* cli.ts with the other cases; everything gui-specific after that gate is this function's job.
|
|
100
|
+
* A failed listen throws (cli.ts's main catch renders it as the same `tumwater: …` line and
|
|
101
|
+
* exit 1 fail() would) so the listen-failure wordings stay assertable in-process. */
|
|
102
|
+
export async function cmdGui(root, args, seams = {}) {
|
|
103
|
+
const { start, lan, print, serve } = resolveGuiSeams(seams);
|
|
104
|
+
const portRaw = flagValue(args, "--port");
|
|
105
|
+
const port = portRaw !== null ? parsePortFlag(portRaw) : 7180;
|
|
106
|
+
const allInterfaces = args.includes("--all-interfaces");
|
|
107
|
+
// A valueless or empty --token is a CLI error, not an open server: an operator who
|
|
108
|
+
// asked for protection must never silently get none. The validation and its wording
|
|
109
|
+
// live in tokenValueProblem, shared with `start --gui` so the two callers cannot
|
|
110
|
+
// drift; its flag-looking-value case names `tumwater gui` here.
|
|
111
|
+
const problem = tokenValueProblem(args, "tumwater gui");
|
|
112
|
+
if (problem)
|
|
113
|
+
fail(problem);
|
|
114
|
+
const token = adoptGuiToken(flagValue(args, "--token") ?? null, print);
|
|
115
|
+
await startGuiOrExplain(start, root, port, allInterfaces, token);
|
|
116
|
+
printGuiBanner(print, lan, port, allInterfaces, token);
|
|
98
117
|
await serve(); // Serve until Ctrl+C.
|
|
99
118
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The dashboard's POST config/budget command handlers (barrelled into
|
|
3
3
|
* gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts):
|
|
4
|
-
* config-set, config-unset, budget, role-cap, role-
|
|
4
|
+
* config-set, config-unset, budget, role-cap, role-enable,
|
|
5
5
|
* role-disable, and
|
|
6
6
|
* notify-test — the
|
|
7
7
|
* post-handler layer onto
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
* own `res`.
|
|
10
10
|
*/
|
|
11
11
|
import { setConfigKey, setDailyBudgetUsd, setRoleModelFields, unsetConfigKey } from "../config/config-write.js";
|
|
12
|
-
import { loadConfigSafe } from "../config/config.js";
|
|
13
12
|
import { checkDailyBudgetUsd, checkMinRunInterval } from "../config/config-keys.js";
|
|
14
|
-
import { testNotifyHook } from "../cli/notify-command.js";
|
|
13
|
+
import { testNotifyHook } from "../cli/cli-notify-command.js";
|
|
15
14
|
import { rejectBadRole, requireConfigKey, requireRoleField, requireStringField } from "./gui-args.js";
|
|
16
15
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
17
16
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
@@ -88,67 +87,6 @@ export async function handleRoleCap(req, res, root) {
|
|
|
88
87
|
return configKeyError(res, "maxDailyCostUsdPerRole." + baseRole, result);
|
|
89
88
|
sendJson(res, 200, { ok: true, role: baseRole, maxDailyCostUsd: value, ...unchangedFlag(result) });
|
|
90
89
|
}
|
|
91
|
-
/** Handle POST /api/role-review-after-merge: the role drawer's Review row button moves one role
|
|
92
|
-
* in or out of review.afterMerge — the browser twin of `tumwater role <id>
|
|
93
|
-
* --review-after-merge`/`--clear-review-after-merge` (the CLI's just-landed write, the amber
|
|
94
|
-
* role-cap alert's remedy on the surface the alert appears on). Same body discipline as
|
|
95
|
-
* /api/role-cap (readPostBody → 400 malformed/non-object, 413 oversized); the body carries the
|
|
96
|
-
* desired membership `afterMerge: true|false` (400 `afterMerge must be true or false`
|
|
97
|
-
* otherwise), and the role id maps to its base role's membership (baseRoleOf) exactly as the
|
|
98
|
-
* CLI's `--review-after-merge` branch does. An unknown id is refused up front (rejectBadRole
|
|
99
|
-
* on the base id, the mirror of the CLI's requireKnownRole) because a no-op or clear write
|
|
100
|
-
* skips validateConfig's known-role refusal — setConfigKey's section branch returns unchanged
|
|
101
|
-
* and writes nothing when the candidate equals the resolved list, so without it a bogus id
|
|
102
|
-
* would answer 200 on a clear. The whole-list value rides the same setConfigKey core `config
|
|
103
|
-
* set review.afterMerge <json>` uses, so validateConfig owns the string-array shape and the
|
|
104
|
-
* `review.afterMerge must not be "director"` refusal — no explicit director branch, matching
|
|
105
|
-
* the CLI. A no-op write (already in the asked-for state) answers unchanged and leaves
|
|
106
|
-
* tumwater.json byte-identical. */
|
|
107
|
-
export async function handleRoleReviewAfterMerge(req, res, root) {
|
|
108
|
-
const body = await readPostBody(req, res, '{"role": "feature", "afterMerge": true}');
|
|
109
|
-
if (!body)
|
|
110
|
-
return;
|
|
111
|
-
const role = requireRoleField(res, body);
|
|
112
|
-
if (role === null)
|
|
113
|
-
return;
|
|
114
|
-
const want = body.afterMerge;
|
|
115
|
-
if (typeof want !== "boolean") {
|
|
116
|
-
badRequest(res, "afterMerge must be true or false");
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
const base = baseRoleOf(role);
|
|
120
|
-
// An unknown base id is refused up front — the mirror of the CLI's requireKnownRole. A
|
|
121
|
-
// no-op or clear write returns the list unchanged and setConfigKey skips validateConfig
|
|
122
|
-
// (the known-role check a real write gets for free), so without this guard a bogus id
|
|
123
|
-
// would answer 200 on the clear path (same reason handleRoleQuietHours's clear arm carries
|
|
124
|
-
// rejectBadRole). The stored list holds base ids, so the base id is screened — as
|
|
125
|
-
// validateConfig's checkKnownRoleId screens the written list — letting an instance id
|
|
126
|
-
// like feature-2 map to its known base and pass.
|
|
127
|
-
if (rejectBadRole(root, res, base))
|
|
128
|
-
return;
|
|
129
|
-
// The CLI's own computation, list-for-list: the whole value lands through the same
|
|
130
|
-
// setConfigKey core so validateConfig owns the string-array shape and the director
|
|
131
|
-
// refusal, and the write is atomic.
|
|
132
|
-
const list = loadConfigSafe(root).config?.review.afterMerge ?? [];
|
|
133
|
-
const has = list.includes(base);
|
|
134
|
-
const next = has === want ? list : want ? [...list, base] : list.filter((r) => r !== base);
|
|
135
|
-
const result = setConfigKey(root, "review.afterMerge", JSON.stringify(next));
|
|
136
|
-
if (!result.ok)
|
|
137
|
-
return configKeyError(res, "review.afterMerge", result);
|
|
138
|
-
sendJson(res, 200, {
|
|
139
|
-
ok: true,
|
|
140
|
-
role: base,
|
|
141
|
-
afterMerge: want,
|
|
142
|
-
message: result.unchanged
|
|
143
|
-
? want
|
|
144
|
-
? `${base}: already in review.afterMerge, so tumwater.json was not written`
|
|
145
|
-
: `${base}: not in review.afterMerge, so nothing was cleared`
|
|
146
|
-
: want
|
|
147
|
-
? `${base}: added to review.afterMerge`
|
|
148
|
-
: `${base}: removed from review.afterMerge`,
|
|
149
|
-
...unchangedFlag(result),
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
90
|
/** Handle POST /api/role-interval: the role drawer's min run interval editor sets or clears
|
|
153
91
|
* one role's override of the team-wide minRunIntervalSeconds — the knob that makes one role
|
|
154
92
|
* act rarely, distinct from the team-wide value. Same body discipline as /api/role-cap
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export { handlePrompt, handlePromptRole, handlePromptCancel, handlePromptEdit, handlePromptTop } from "./gui-prompt-commands.js";
|
|
17
17
|
export { handleQuestionAnswer, handleBacklogFile, handleBacklogCancel, handleBacklogRelease, handleBacklogTop, handleBacklogEdit } from "./gui-backlog-commands.js";
|
|
18
|
-
export { handleConfigSet, handleConfigUnset, handleBudget, handleRoleCap,
|
|
18
|
+
export { handleConfigSet, handleConfigUnset, handleBudget, handleRoleCap, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest } from "./gui-config-commands.js";
|
|
19
19
|
export { handlePause, handlePauseRole, handleWake, handleWakeCancel } from "./gui-team-commands.js";
|
|
20
20
|
export { handleResetCounters, handleRestart, handleReclaim, handleKillOrphans, handleDropBrokenPins, handleCommitBacklog, handleResyncInstall, handleRetireLeftovers, handleArchiveBacklog, handleAbort, handleAbortAll, handleStopSchedule, handleRoleNotes, handleRetire, handleRevert, handleQueueCancel, handleQueueTop, handlePostReviewAccept, handleBriefEdit } from "./gui-operator-commands.js";
|
|
21
21
|
export { handleQuarantineRelease } from "./gui-quarantine-commands.js";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* inputs with — and sendRequestResult, the shared boolean-claiming marker reply. The
|
|
8
8
|
* per-domain `checked*` validators live with their domain's module, not here.
|
|
9
9
|
*/
|
|
10
|
-
import { rejectBadRole } from "./gui-args.js";
|
|
10
|
+
import { rejectBadRole, requireSha } from "./gui-args.js";
|
|
11
11
|
import { readJsonObject, sendJson } from "./gui-http-body.js";
|
|
12
12
|
/** Every POST handler's shared opening: readJsonObject reads the body and — when it is
|
|
13
13
|
* oversized, malformed, or not a JSON object — has already written the 413/400 itself. Its
|
|
@@ -38,6 +38,24 @@ export async function readRoleBody(req, res, example, root, allowMissing = false
|
|
|
38
38
|
return null; // 400 already sent
|
|
39
39
|
return body;
|
|
40
40
|
}
|
|
41
|
+
/** The sha-addressed POST handlers' shared prologue: readPostBody (400/413 sent on a null
|
|
42
|
+
* return) plus requireSha's hex-commit-id validation (400 sent on a null return), so the
|
|
43
|
+
* queue-cancel/queue-top/post-review-accept/revert family states the body-discipline-and-sha
|
|
44
|
+
* check pair once instead of two guard lines per handler. Its null return means the response
|
|
45
|
+
* is already written and the handler must stop; callers keep their own `if (!read) return`
|
|
46
|
+
* bail. Exactly four call sites — handleRevert, handleQueueCancel, handleQueueTop, and
|
|
47
|
+
* handlePostReviewAccept — returning the body too so handleRevert can read its optional
|
|
48
|
+
* dryRun; the other three callers use only the sha. A caller that needs neither the body nor
|
|
49
|
+
* the example can read just `{ sha }` from the result. */
|
|
50
|
+
export async function readShaBody(req, res) {
|
|
51
|
+
const body = await readPostBody(req, res, '{"sha": "1a2b3c4"}');
|
|
52
|
+
if (body === null)
|
|
53
|
+
return null; // 4xx already sent — oversized, malformed, or not an object
|
|
54
|
+
const sha = requireSha(res, body.sha);
|
|
55
|
+
if (sha === null)
|
|
56
|
+
return null; // 400 already sent
|
|
57
|
+
return { body, sha };
|
|
58
|
+
}
|
|
41
59
|
/** Reply for a boolean-claiming marker request (RequestResult): success maps to 200 with its
|
|
42
60
|
* message, refusal to 409 — the request is valid but nothing can act on it, the same conflict
|
|
43
61
|
* reading requestAbort's not-live error uses. /api/restart and /api/abort share it. */
|
|
@@ -14,7 +14,7 @@ import { eventRole, readEvents } from "../events/event-read.js";
|
|
|
14
14
|
import { eventFeedItem, eventMatchesGrep } from "../events/event-format.js";
|
|
15
15
|
import { readRunDetail, runCommitSha, runMergedSha } from "../run/run-detail-data.js";
|
|
16
16
|
import { renderRunDetail, runDetailPatch, runDetailStat, runTranscriptLines } from "../run/run-detail-render.js";
|
|
17
|
-
import { resolveRevertTarget } from "../operator/revert.js";
|
|
17
|
+
import { resolveRevertTarget } from "../operator/operator-revert.js";
|
|
18
18
|
import { rolePayload } from "../roles/role-view.js";
|
|
19
19
|
import { clampCount, intQuery, rejectBadRole, sinceMsQuery, untilMsQuery } from "./gui-args.js";
|
|
20
20
|
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
@@ -124,36 +124,67 @@ function windowOrCount(q, res) {
|
|
|
124
124
|
return { kind: "count" };
|
|
125
125
|
}
|
|
126
126
|
export function handleHistory(q, res, root) {
|
|
127
|
+
const req = historyRequest(q, res);
|
|
128
|
+
if (req === null)
|
|
129
|
+
return;
|
|
130
|
+
const ctl = req.windowCtl;
|
|
131
|
+
if (ctl.kind === "window")
|
|
132
|
+
serveHistoryWindow(res, root, req, ctl.sinceMs, ctl.untilMs);
|
|
133
|
+
else
|
|
134
|
+
serveHistoryCount(q, res, root, req);
|
|
135
|
+
}
|
|
136
|
+
/** Parse the request prefix `GET /api/history` shares across both shapes: the `role`/`grep`
|
|
137
|
+
* filters, the `result` class list (resultClassesQuery, a 400 on an unknown class), the `model`
|
|
138
|
+
* selector list, the `summary` flag's mere presence, and the rival `sinceMs`-vs-`n` shape
|
|
139
|
+
* (windowOrCount, a 400 on both or a bad cap). Any 400 is already sent and yields null. */
|
|
140
|
+
function historyRequest(q, res) {
|
|
127
141
|
const role = q.get("role") || null; // "" and absent both read all roles
|
|
128
142
|
const grep = q.get("grep") || null; // "" and absent both filter nothing
|
|
129
143
|
// `model` is a comma list of exact selectors (trimmed, deduplicated); "", absent and a list
|
|
130
144
|
// with no non-empty piece all filter nothing, and an unknown selector keeps no rows.
|
|
131
145
|
const classes = resultClassesQuery(q, res);
|
|
132
146
|
if (classes === null)
|
|
133
|
-
return;
|
|
134
|
-
const inClasses = (rows) => (classes.length ? rows.filter((r) => classes.includes(historyClass(r.result))) : rows);
|
|
147
|
+
return null;
|
|
135
148
|
const modelPieces = splitCommaListUnique(q.get("model") ?? "");
|
|
136
|
-
const onModels = (rows) => (modelPieces.length ? runRowsOnModels(rows, modelPieces) : rows);
|
|
137
149
|
const summary = q.get("summary") !== null; // the flag's presence, whatever its value
|
|
138
150
|
const windowCtl = windowOrCount(q, res);
|
|
139
151
|
if (windowCtl === null)
|
|
140
|
-
return;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
return null;
|
|
153
|
+
return { role, grep, classes, modelPieces, summary, windowCtl };
|
|
154
|
+
}
|
|
155
|
+
/** The shared filter chain both shapes apply to the served rows: grep first, then the `result`
|
|
156
|
+
* outcome classes, then the `model` selectors — the CLI's `history --grep`, `--result` and
|
|
157
|
+
* `--model` rules, in that order. */
|
|
158
|
+
function historyRows(rows, req) {
|
|
159
|
+
const grepped = req.grep === null ? rows : runRowsMatchingGrep(rows, req.grep);
|
|
160
|
+
const classed = req.classes.length ? grepped.filter((r) => req.classes.includes(historyClass(r.result))) : grepped;
|
|
161
|
+
return req.modelPieces.length ? runRowsOnModels(classed, req.modelPieces) : classed;
|
|
162
|
+
}
|
|
163
|
+
/** The optional additive fold both answers share: when `summary` is present, `{ summary,
|
|
164
|
+
* modelSummary }` added to the 200 payload; absent, nothing (so the response is byte-for-byte
|
|
165
|
+
* today's). */
|
|
166
|
+
function historySummaryFields(rows, summary) {
|
|
167
|
+
return summary ? { summary: summarizeRunRows(rows), modelSummary: summarizeRunRowsByModel(rows) } : {};
|
|
168
|
+
}
|
|
169
|
+
/** Answer the trailing-window shape ({ rows, covered, note }): rows read through readRunRowsSince
|
|
170
|
+
* from `sinceMs` (untilMs drops the rows newer than now − untilMs when present, exactly as the
|
|
171
|
+
* CLI's `history --since … --until` window reads), then the shared historyRows filter, then the
|
|
172
|
+
* optional summary fold — key order rows, covered, note, summary, modelSummary. */
|
|
173
|
+
function serveHistoryWindow(res, root, req, sinceMs, untilMs) {
|
|
174
|
+
const { rows, covered } = readRunRowsSince(root, sinceMs, req.role, untilMs ?? null);
|
|
175
|
+
const matched = historyRows(rows, req);
|
|
176
|
+
sendJson(res, 200, { rows: matched, covered, note: covered ? "" : SPARSE_WINDOW_NOTE, ...historySummaryFields(matched, req.summary) });
|
|
177
|
+
}
|
|
178
|
+
/** Answer the newest-`n` count shape: `n` parses through intQuery (non-negative, default
|
|
179
|
+
* HISTORY_DEFAULT_RUNS) and is clamped to [1, HISTORY_MAX_RUNS] — the GUI clamps where the CLI
|
|
180
|
+
* fails fast — then rows read through readRunRows, filtered and answered through the shared
|
|
181
|
+
* historyRows/historySummaryFields, key order rows, summary, modelSummary. */
|
|
182
|
+
function serveHistoryCount(q, res, root, req) {
|
|
148
183
|
const parsed = intQuery(q, res, "n", "non-negative", HISTORY_DEFAULT_RUNS);
|
|
149
184
|
if (parsed === null)
|
|
150
185
|
return;
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
if (grep !== null)
|
|
154
|
-
rows = runRowsMatchingGrep(rows, grep);
|
|
155
|
-
rows = onModels(inClasses(rows));
|
|
156
|
-
sendJson(res, 200, summary ? { rows, summary: summarizeRunRows(rows), modelSummary: summarizeRunRowsByModel(rows) } : { rows });
|
|
186
|
+
const rows = historyRows(readRunRows(root, clampCount(parsed, HISTORY_MAX_RUNS), req.role), req);
|
|
187
|
+
sendJson(res, 200, { rows, ...historySummaryFields(rows, req.summary) });
|
|
157
188
|
}
|
|
158
189
|
/** How many of the log's newest events `GET /api/logs` scans by default, and the most it will
|
|
159
190
|
* scan however large `n` asks: the CLI's `-n` shape (n bounds the scanned tail, not the printed
|
|
@@ -245,47 +276,64 @@ function transcriptText(root, role, run, detail, includePrompts, grep) {
|
|
|
245
276
|
* payload's `role` is that run's; `result`/`model` narrow it the same way, and a miss is a 404
|
|
246
277
|
* worded for any role. */
|
|
247
278
|
export async function handleRun(q, res, root) {
|
|
248
|
-
|
|
279
|
+
const target = runSelector(q, res, root);
|
|
280
|
+
if (target === null)
|
|
281
|
+
return;
|
|
282
|
+
const detail = readRunDetail(root, target.role, target.run);
|
|
283
|
+
if (detail === null) {
|
|
284
|
+
sendJson(res, 404, { error: runNotFoundMessage(target.role, target.run) });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
sendJson(res, 200, await runPayload(root, target.role, target.run, detail, q));
|
|
288
|
+
}
|
|
289
|
+
/** Resolve which run the request names — validating the role, then the numbered or `last` form. */
|
|
290
|
+
function runSelector(q, res, root) {
|
|
291
|
+
const role = q.get("role");
|
|
249
292
|
const bareLast = role === null && q.get("last") !== null;
|
|
250
293
|
if (!bareLast && rejectBadRole(root, res, role))
|
|
251
|
-
return;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (run === null)
|
|
260
|
-
return;
|
|
294
|
+
return null;
|
|
295
|
+
return q.get("last") !== null ? lastRunSelector(q, res, root, role) : numberedRunSelector(q, res, role);
|
|
296
|
+
}
|
|
297
|
+
/** Resolve the explicit `run=N` selector of /api/run (three 400s, the miss worded by intQuery). */
|
|
298
|
+
function numberedRunSelector(q, res, role) {
|
|
299
|
+
if (q.get("result") !== null) {
|
|
300
|
+
badRequest(res, "result needs last");
|
|
301
|
+
return null;
|
|
261
302
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const classes = resultClassesQuery(q, res);
|
|
266
|
-
if (classes === null)
|
|
267
|
-
return;
|
|
268
|
-
const models = splitCommaListUnique(q.get("model") ?? "");
|
|
269
|
-
const classFilter = classes.length ? classes : null;
|
|
270
|
-
const modelFilter = models.length ? models : null;
|
|
271
|
-
const row = newestRunRow(root, role, classFilter, modelFilter);
|
|
272
|
-
if (row !== null)
|
|
273
|
-
role = row.role;
|
|
274
|
-
run = row?.run ?? null;
|
|
275
|
-
if (run === null) {
|
|
276
|
-
sendJson(res, 404, { error: runNotFoundLastMessage(role, classFilter, modelFilter) });
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
303
|
+
if (q.get("model") !== null) {
|
|
304
|
+
badRequest(res, "model needs last");
|
|
305
|
+
return null;
|
|
279
306
|
}
|
|
280
|
-
const
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
307
|
+
const run = intQuery(q, res, "run", "positive");
|
|
308
|
+
if (run === null)
|
|
309
|
+
return null;
|
|
310
|
+
return { role: role, run };
|
|
311
|
+
}
|
|
312
|
+
/** Resolve the `last` selector, narrowed by `result`/`model`, to the role's newest run. */
|
|
313
|
+
function lastRunSelector(q, res, root, role) {
|
|
314
|
+
if (q.get("run") !== null) {
|
|
315
|
+
badRequest(res, "last cannot be combined with run (a number and the newest run are rival selectors)");
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const classes = resultClassesQuery(q, res);
|
|
319
|
+
if (classes === null)
|
|
320
|
+
return null;
|
|
321
|
+
const models = splitCommaListUnique(q.get("model") ?? "");
|
|
322
|
+
const classFilter = classes.length ? classes : null;
|
|
323
|
+
const modelFilter = models.length ? models : null;
|
|
324
|
+
const row = newestRunRow(root, role, classFilter, modelFilter);
|
|
325
|
+
if (row === null) {
|
|
326
|
+
sendJson(res, 404, { error: runNotFoundLastMessage(role, classFilter, modelFilter) });
|
|
327
|
+
return null;
|
|
284
328
|
}
|
|
329
|
+
return { role: row.role, run: row.run };
|
|
330
|
+
}
|
|
331
|
+
/** Assemble the 200 payload — the collector plus its rendered, stat and flag-gated fields. */
|
|
332
|
+
async function runPayload(root, role, run, detail, q) {
|
|
285
333
|
const grep = (q.get("grep") ?? "").trim();
|
|
286
334
|
const mergedAs = runMergedSha(detail);
|
|
287
335
|
const target = mergedAs === null ? null : await resolveRevertTarget(root, mergedAs);
|
|
288
|
-
|
|
336
|
+
return {
|
|
289
337
|
...detail,
|
|
290
338
|
text: renderRunDetail(detail),
|
|
291
339
|
stat: await runDetailStat(root, detail),
|
|
@@ -293,7 +341,7 @@ export async function handleRun(q, res, root) {
|
|
|
293
341
|
hasCommit: runCommitSha(detail) !== null,
|
|
294
342
|
...(q.get("patch") !== null ? { patch: await runDetailPatch(root, detail) } : {}),
|
|
295
343
|
...(q.get("transcript") !== null ? { transcript: transcriptText(root, role, run, detail, q.get("prompt") !== null, grep === "" ? undefined : grep) } : {}),
|
|
296
|
-
}
|
|
344
|
+
};
|
|
297
345
|
}
|
|
298
346
|
/** Handle GET /api/role?role=<id>: one role's inspection payload — rolePayload's object,
|
|
299
347
|
* the same collection behind `tumwater role <id>` — so the drawer's Notes and Next run
|