tumwater 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,19 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
import { answeredQuestionSummaries } from "../backlog/question-answer.js";
|
|
3
|
+
import { completionDate, donePlanEntries, DONE_DATE_RE, FIXED_DATE_RE, fixedBugEntries, openBugs, openQuestions, plannedPlans } from "../backlog/backlog.js";
|
|
4
|
+
import { backlogHolds, backlogInProgress, backlogMarks, refusedEntryTitles } from "../backlog/backlog-eligibility.js";
|
|
5
|
+
import { finishedTitle, groupBlockedSeries, partsLabel } from "../backlog/backlog-structure.js";
|
|
3
6
|
import { readEvents } from "../events/event-read.js";
|
|
4
7
|
import { eventFeedItem, formatEvent } from "../events/event-format.js";
|
|
5
8
|
import { projectName } from "../project-name.js";
|
|
6
9
|
import { dailyCost } from "../budget/budget.js";
|
|
10
|
+
import { queueEntryMergeLabel } from "../merge/merge-queue-render.js";
|
|
7
11
|
import { snapshot } from "../status/status-data.js";
|
|
8
|
-
import { bootstrapBadge, buildBadge, budgetBadge, diskBadge,
|
|
9
|
-
import { isActivePhase, loopRowCells } from "./status-model.js";
|
|
12
|
+
import { bootstrapBadge, buildBadge, budgetBadge, diskBadge, mergeBadge, mainCheckBadge, mainCountsFragment, permitsBadge, quietBadge, runWindowBadge } from "./badges.js";
|
|
13
|
+
import { isActivePhase, loopRowCells, onErrorStreak } from "./status-model.js";
|
|
10
14
|
import { yieldMultiplierFor } from "../scheduling/backoff.js";
|
|
11
|
-
import {
|
|
15
|
+
import { teamAlerts } from "./team-alerts.js";
|
|
12
16
|
/** How many recent events the payload carries — enough for the dashboard's activity feed to
|
|
13
|
-
* still show a screenful of notable events (
|
|
17
|
+
* still show a screenful of notable events (merges, failures, questions) after it filters
|
|
14
18
|
* out the routine ones (tick starts, wakes, passing checks). */
|
|
15
19
|
const RECENT_EVENTS = 80;
|
|
16
|
-
/**
|
|
20
|
+
/** How many answered questions the dashboard lists under the open ones (a default, not a knob). */
|
|
21
|
+
export const DASHBOARD_ANSWERED_QUESTIONS = 5;
|
|
22
|
+
/** How many done plans / fixed bugs the dashboard lists under the open ones (a default). */
|
|
23
|
+
export const DASHBOARD_RECENT_FINISHED = 5;
|
|
24
|
+
/** The newest finished entries (sections are newest first), titles cleaned of their stamp. */
|
|
25
|
+
export function recentFinished(entries, dateRe) {
|
|
26
|
+
return entries.slice(0, DASHBOARD_RECENT_FINISHED).map((e) => ({ title: finishedTitle(e.title, dateRe), completedOn: completionDate(e.title, dateRe) }));
|
|
27
|
+
}
|
|
28
|
+
/** The one team-state document both observer surfaces carry: `GET /api/status` (gui/gui-server.ts)
|
|
17
29
|
* spreads it and adds the serving process's own `serverBuildSha` (the page's cue to notice a
|
|
18
30
|
* newer build and reload), while `tumwater status --json` (cli.ts) prints it verbatim — every
|
|
19
31
|
* other field is shared, so the dashboard and the CLI can never drift apart. Assembled here —
|
|
@@ -34,13 +46,31 @@ export function statusPayload(root, now = Date.now()) {
|
|
|
34
46
|
// the prompt's backlog index and `tumwater backlog` render, projected to the mark each
|
|
35
47
|
// surface appends. Read once here, consumed as the parallel planHolds/bugHolds arrays below.
|
|
36
48
|
const marks = backlogMarks(root);
|
|
37
|
-
const
|
|
49
|
+
const holds = backlogHolds(root);
|
|
50
|
+
const plans = plannedPlans(root);
|
|
51
|
+
// Which blocked plans fold into a series row: the grouping `plan --list` and the prompt's
|
|
52
|
+
// backlog index use, as a slot per plan (null for a plan that stays an ordinary row).
|
|
53
|
+
const working = backlogInProgress(root);
|
|
54
|
+
const planSeries = plans.map(() => null);
|
|
55
|
+
for (const g of groupBlockedSeries(holds.plans.map((hold, i) => ({ i, title: plans[i], hold, claimer: working.plans[i] ?? undefined })))) {
|
|
56
|
+
if (g.kind !== "series")
|
|
57
|
+
continue;
|
|
58
|
+
const slot = { name: g.series, label: `${g.series}, ${g.rows.length} blocked parts (${partsLabel(g.rows)})` };
|
|
59
|
+
for (const r of g.rows)
|
|
60
|
+
planSeries[r.i] = slot;
|
|
61
|
+
}
|
|
62
|
+
const roles = snap.loops.map((s) => {
|
|
38
63
|
// loopRowCells single-homes the per-loop tail read + metrics + phase derivation the
|
|
39
64
|
// TUI table (status-render.ts's renderStatus) repeats, so the two dashboards cannot
|
|
40
65
|
// drift apart.
|
|
41
66
|
const { live, generated, peakCtx, phase } = loopRowCells(snap, root, s);
|
|
42
67
|
return {
|
|
43
68
|
role: s.role,
|
|
69
|
+
// The loop's own failure-streak tell (onErrorStreak, status-model.ts): the red
|
|
70
|
+
// "failing tick after tick" alert keys on this rather than the rendered phase, so it
|
|
71
|
+
// stays up while the loop's next tick runs (phase reads "working") and clears only
|
|
72
|
+
// when the streak resets (BUGS.md 2026-10-14).
|
|
73
|
+
errorStreak: onErrorStreak(s),
|
|
44
74
|
// User-defined-loop marker (computed in snapshot — see StatusSnapshot.loops): the GUI
|
|
45
75
|
// renders it as an asterisk beside the loop name.
|
|
46
76
|
custom: s.custom,
|
|
@@ -50,28 +80,42 @@ export function statusPayload(root, now = Date.now()) {
|
|
|
50
80
|
// the loop name and the selector in the sub line.
|
|
51
81
|
...(s.modelTier ? { modelTier: s.modelTier } : {}),
|
|
52
82
|
...(s.model ? { model: s.model } : {}),
|
|
83
|
+
// pi's own saved default (see StatusSnapshot.loops.modelFromPi): ships beside `model` so
|
|
84
|
+
// the dashboard can mark a row whose model tumwater.json did not name. Same conditional
|
|
85
|
+
// spread as `model`, present on exactly the rows that carry the flag.
|
|
86
|
+
...(s.modelFromPi ? { modelFromPi: true } : {}),
|
|
53
87
|
// Model failure fallback (PLANS.md "Model failure fallback, part 2/2"): present only
|
|
54
88
|
// while the loop's ticks run off-model, so the common row shape is unchanged.
|
|
55
89
|
...(s.fallback ? { fallback: s.fallback } : {}),
|
|
56
90
|
...(s.slot ? { slot: s.slot, slotPinned: s.slotPinned === true } : {}),
|
|
57
91
|
// Parallel work instances (plans/parallel-work-instances.md "Observability", part
|
|
58
92
|
// 6/7): the backlog entry this instance claimed, shown by the dashboards while it is
|
|
59
|
-
// idle or
|
|
93
|
+
// idle or merge, and the base role it belongs to (`feature-2` → `feature`). `claim`
|
|
60
94
|
// is the claimed heading text, not the persisted record; both are absent on a bare,
|
|
61
95
|
// unclaimed row so the common payload shape is unchanged.
|
|
62
96
|
...(s.claim ? { claim: s.claim.title } : {}),
|
|
63
97
|
...(s.instanceOf ? { instanceOf: s.instanceOf } : {}),
|
|
98
|
+
// The loop's image-input surface (plans/image-input.md "Surfaces", part 5/5), always
|
|
99
|
+
// present: whether its own model reads images and where a blind prompt reroutes — the
|
|
100
|
+
// GUI's images alert and composer notice read them (the JSON field is `imageReroute`, the
|
|
101
|
+
// display label, not the persisted handoff state).
|
|
102
|
+
...(s.imageInput !== undefined ? { imageInput: s.imageInput } : {}),
|
|
103
|
+
...(s.imageRerouteLabel !== undefined ? { imageReroute: s.imageRerouteLabel } : {}),
|
|
64
104
|
phase,
|
|
65
105
|
// In-flight flag derived from the same rendered phase (isActivePhase's
|
|
66
|
-
// permit-holder prefixes: working/reviewing/
|
|
106
|
+
// permit-holder prefixes: working/reviewing/merge). The running director is excluded
|
|
67
107
|
// (it holds no maxConcurrent permit), so this is the cap count, not the abort set — the
|
|
68
108
|
// GUI's row actions derive their own live flag from the phase instead (BUGS.md
|
|
69
109
|
// 2026-10-06).
|
|
70
110
|
inFlight: isActivePhase(phase),
|
|
111
|
+
// The stuck alert's silence span (ms since the last pi log write, capped to the tick by
|
|
112
|
+
// progressOfTick) — the dashboards render it as "for <duration>" on a tool-call stall.
|
|
113
|
+
// Present only while the loop holds a live tail, so the common payload shape is unchanged.
|
|
114
|
+
...(live?.quietMs !== undefined ? { stuckMs: live.quietMs } : {}),
|
|
71
115
|
// What a working loop is doing right now (first assistant text of the in-flight run).
|
|
72
116
|
// Null when idle — never show a stale item from a finished tick.
|
|
73
117
|
currentWork: live?.currentWork ?? null,
|
|
74
|
-
|
|
118
|
+
runs: s.runs,
|
|
75
119
|
commits: s.commits,
|
|
76
120
|
generated,
|
|
77
121
|
peakCtx,
|
|
@@ -81,12 +125,16 @@ export function statusPayload(root, now = Date.now()) {
|
|
|
81
125
|
todayUsd: dailyCost(s),
|
|
82
126
|
lastResult: s.lastResult ?? null,
|
|
83
127
|
lastSummary: s.lastSummary ?? null,
|
|
84
|
-
// The last failure's text (a tick error, a failed
|
|
128
|
+
// The last failure's text (a tick error, a failed merge's reason); the dashboard shows it
|
|
85
129
|
// beside a problem result that carries no summary of its own.
|
|
86
130
|
lastError: s.lastError ?? null,
|
|
87
|
-
|
|
131
|
+
lastRunEndedAt: s.lastRunEndedAt ?? null,
|
|
132
|
+
// When the loop last actually merged a change on main (epoch ms; null until it ever
|
|
133
|
+
// does) — the direct answer to "how long has the dry spell been", the productivity
|
|
134
|
+
// complement to lastError/lastRunEndedAt.
|
|
135
|
+
lastMergedAt: s.lastMergedAt ?? null,
|
|
88
136
|
// The scheduling facts behind the `next run` column: raw epoch ms and seconds, formatted
|
|
89
|
-
// client-side like
|
|
137
|
+
// client-side like lastRunEndedAt/costUsd — the GUI's fmtNextRun twin applies the same
|
|
90
138
|
// rules nextRunCell does (see status-render.ts).
|
|
91
139
|
nextRunAt: s.nextRunAt,
|
|
92
140
|
backoffSeconds: s.backoffSeconds,
|
|
@@ -94,6 +142,12 @@ export function statusPayload(root, now = Date.now()) {
|
|
|
94
142
|
// clocks, PLANS.md): the payload carries the computed number, not the ring, so the
|
|
95
143
|
// GUI's JS twin applies the same display rule without re-deriving the role sets.
|
|
96
144
|
yieldMultiplier: yieldMultiplierFor(s),
|
|
145
|
+
// The consecutive no-change streak (tick-apply.ts applyTickOutcome's noChangeStreak, an
|
|
146
|
+
// observational counter): how many completed ticks in a row returned nothing to do. The
|
|
147
|
+
// TUI reads it off the rendered label; the GUI's phaseDetailBase re-derives the ` · no
|
|
148
|
+
// change ×N` suffix from this number (its row and drawer share noChangeStreak) so its
|
|
149
|
+
// JS twin applies the same display rule without re-parsing the phase text.
|
|
150
|
+
noChangeStreak: s.noChangeStreak ?? 0,
|
|
97
151
|
};
|
|
98
152
|
});
|
|
99
153
|
return {
|
|
@@ -110,21 +164,34 @@ export function statusPayload(root, now = Date.now()) {
|
|
|
110
164
|
// not be re-derived client-side where it could drift from the TUI header.
|
|
111
165
|
buildBadge: buildBadge(snap.build),
|
|
112
166
|
// The header's daily-cost-budget badge preformatted through badges.ts's budgetBadge —
|
|
113
|
-
// the same string the TUI/status table renders (n/a for an all-free
|
|
167
|
+
// the same string the TUI/status table renders (n/a for an all-free team; `· no cap`
|
|
114
168
|
// when disabled). Sent display-ready like buildBadge so the page cannot re-derive it.
|
|
115
|
-
budgetBadge: budgetBadge(snap.budget, snap.claudeWindow),
|
|
169
|
+
budgetBadge: budgetBadge(snap.budget, snap.claudeWindow, now, snap.chain),
|
|
116
170
|
// The disk floor (plans/disk-floor.md, part 4/4): the raw block is machine-readable for
|
|
117
171
|
// `status --json`, and the header badge is preformatted through badges.ts's diskBadge —
|
|
118
172
|
// the same string the TUI/status header renders. Omitted (not null) when no orchestrator
|
|
119
173
|
// has published one — the same omit-undefined idiom as pausedUntil.
|
|
120
174
|
...(snap.disk ? { disk: snap.disk } : {}),
|
|
121
|
-
diskBadge: diskBadge(snap.disk
|
|
122
|
-
// The
|
|
123
|
-
// `status --json
|
|
175
|
+
diskBadge: diskBadge(snap.disk),
|
|
176
|
+
// The permit pool (plans/provider-concurrency.md §8): raw block machine-readable for
|
|
177
|
+
// `status --json`, plus the display-ready header badge — the same `slots 5/8 · omlx 3/3`
|
|
178
|
+
// string the TUI/status header renders. Omitted when no orchestrator has published one.
|
|
179
|
+
...(snap.permits ? { permits: snap.permits } : {}),
|
|
180
|
+
permitsBadge: permitsBadge(snap.permits),
|
|
181
|
+
// The `run --for` deadline: raw epoch ms (only when windowed) plus the display-ready badge.
|
|
182
|
+
...(snap.runStopsAt !== undefined ? { runStopsAt: snap.runStopsAt } : {}),
|
|
183
|
+
...(snap.scheduledStopAt !== undefined ? { scheduledStopAt: snap.scheduledStopAt } : {}),
|
|
184
|
+
runWindowBadge: runWindowBadge(snap.runStopsAt, now),
|
|
185
|
+
// The merge queue (plans/merge-queue.md 4/5): depth always (machine-readable for
|
|
186
|
+
// `status --json`; 0 when idle) plus the in-flight merge's identity only while one is
|
|
124
187
|
// actually running. Raw data here, like budget — and the display-ready header badge
|
|
125
|
-
// preformatted through badges.ts's
|
|
126
|
-
|
|
127
|
-
|
|
188
|
+
// preformatted through badges.ts's mergeBadge, so the page cannot re-derive it.
|
|
189
|
+
mergeQueue: snap.mergeQueue,
|
|
190
|
+
mergeBadge: mergeBadge(snap.mergeQueue),
|
|
191
|
+
// One bracketed merge label per merge queue entry (queueEntryMergeLabel, "" for none),
|
|
192
|
+
// parallel to mergeQueue.entries — display-ready like mergeBadge, since browser JS cannot
|
|
193
|
+
// import the TypeScript stage labels.
|
|
194
|
+
mergeQueueLabels: (snap.mergeQueue.entries ?? []).map((e) => queueEntryMergeLabel(e.sha, snap.mergeQueue.inFlight, now)),
|
|
128
195
|
// Main's newest merge-scope check: the
|
|
129
196
|
// raw block is machine-readable for `status --json`, and the header badge is
|
|
130
197
|
// preformatted through badges.ts's mainCheckBadge — the same string the TUI/status
|
|
@@ -167,14 +234,18 @@ export function statusPayload(root, now = Date.now()) {
|
|
|
167
234
|
// the preformatted `budgetBadge`.
|
|
168
235
|
budget: snap.budget,
|
|
169
236
|
// The subscription usage-window block (part 6d/7), absent on a pi-only or API-claude
|
|
170
|
-
//
|
|
237
|
+
// team — the same raw shape `status --json` carries, which the GUI's spend card reads
|
|
171
238
|
// to render the window (the preformatted budgetBadge above already folds it into the
|
|
172
239
|
// header for the TUI/status). Raw data, like budget.
|
|
173
240
|
...(snap.claudeWindow ? { claudeWindow: snap.claudeWindow } : {}),
|
|
241
|
+
// Each tier's fallback step (fallback chain part 7c/7), raw for the GUI side card.
|
|
242
|
+
...(snap.chain ? { chain: snap.chain } : {}),
|
|
174
243
|
// Operator pause (`tumwater pause` marker present): every idle role loop's phase reads
|
|
175
244
|
// `paused` ahead of budget/main-red — one flag covers both dashboards through loopPhase.
|
|
176
245
|
paused: snap.paused,
|
|
177
246
|
pausedRoles: snap.pausedRoles,
|
|
247
|
+
// The loop ids the scheduler treats as paused by the marker; the dashboards read this.
|
|
248
|
+
pausedLoops: snap.pausedLoops,
|
|
178
249
|
// The per-role cap's held roles (part 2/2): an idle
|
|
179
250
|
// loop in this set reads `cap paused` — the loopPhase ladder names it, and `status
|
|
180
251
|
// --json` carries the raw set beside pausedRoles so scripts read the same verdict.
|
|
@@ -194,12 +265,13 @@ export function statusPayload(root, now = Date.now()) {
|
|
|
194
265
|
// window, machine-readable for `status --json` beside capPaused; the dashboards render
|
|
195
266
|
// the loop's own `quiet until <end>` from it through loopPhase.
|
|
196
267
|
roleQuietPaused: snap.roleQuietPaused,
|
|
197
|
-
// The
|
|
268
|
+
// The team marker's standing timed-pause deadline (ms epoch), absent when the team is
|
|
198
269
|
// not timed-paused (undefined drops from the JSON). Raw data, like paused/pausedRoles —
|
|
199
270
|
// machine-readable for `status --json`; the planned GUI pause-badge countdown reads it
|
|
200
271
|
// client-side.
|
|
201
272
|
pausedUntil: snap.pausedUntil,
|
|
202
|
-
// The paused-roles marker's shared `until` while a timed per-role pause stands (omitted
|
|
273
|
+
// The paused-roles marker's shared `until` while a timed per-role pause stands (omitted
|
|
274
|
+
// otherwise).
|
|
203
275
|
pausedRolesUntil: snap.pausedRolesUntil,
|
|
204
276
|
// A pending scheduled wake ({ roles, atMs }); omitted when none stands.
|
|
205
277
|
scheduledWake: snap.scheduledWake,
|
|
@@ -217,7 +289,7 @@ export function statusPayload(root, now = Date.now()) {
|
|
|
217
289
|
...(snap.quietHours ? { quietHours: snap.quietHours } : {}),
|
|
218
290
|
inQuietHours: snap.inQuietHours,
|
|
219
291
|
quietBadge: quietBadge(snap.quietHours, snap.inQuietHours),
|
|
220
|
-
|
|
292
|
+
roles,
|
|
221
293
|
// The recent event feed as the one-line text `tumwater logs` prints…
|
|
222
294
|
events: recent.map((e) => formatEvent(e)),
|
|
223
295
|
// …and the same events as data for the dashboard's activity feed: when, which loop, what
|
|
@@ -227,16 +299,31 @@ export function statusPayload(root, now = Date.now()) {
|
|
|
227
299
|
// Project status (planned features + open bugs + open questions), fresh per poll like
|
|
228
300
|
// events — loops edit these files constantly, so there is no cache to go stale. The page
|
|
229
301
|
// derives the header badge count from this list's length.
|
|
230
|
-
plans
|
|
302
|
+
plans,
|
|
231
303
|
bugs: openBugs(root),
|
|
232
|
-
// The holds beside those titles, index-for-index parallel
|
|
233
|
-
// openBugs walk the same sectionEntries file order):
|
|
234
|
-
// the empty string, so a client can append the
|
|
304
|
+
// The holds and working-on-it marks beside those titles, index-for-index parallel
|
|
305
|
+
// (backlogMarks and plannedPlans/openBugs walk the same sectionEntries file order): an
|
|
306
|
+
// eligible entry nobody is working on is the empty string, so a client can append the
|
|
307
|
+
// mark without a separate eligibility check.
|
|
235
308
|
planHolds: marks.plans,
|
|
236
309
|
bugHolds: marks.bugs,
|
|
310
|
+
// Whether each entry carries any eligibility hold (parallel to plans/bugs): the dashboard
|
|
311
|
+
// lists unblocked rows first and gathers the rest under a Blocked divider.
|
|
312
|
+
planBlocked: holds.plans.map((h) => h !== null),
|
|
313
|
+
bugBlocked: holds.bugs.map((h) => h !== null),
|
|
314
|
+
// The series each blocked plan folds into (parallel to plans): the Plans tab shows one
|
|
315
|
+
// expandable row per series instead of every waiting part; null for an ordinary row.
|
|
316
|
+
planSeries,
|
|
237
317
|
questions,
|
|
238
|
-
//
|
|
318
|
+
// The newest answered questions, newest first, read fresh each poll like the other backlog
|
|
319
|
+
// lists. Alerts and badges keep counting open questions only.
|
|
320
|
+
answeredQuestions: answeredQuestionSummaries(root, DASHBOARD_ANSWERED_QUESTIONS),
|
|
321
|
+
// The newest done plans and fixed bugs, newest first: cleaned titles only, no bodies, read
|
|
322
|
+
// fresh each poll. Badges and alerts keep counting open entries only.
|
|
323
|
+
donePlans: recentFinished(donePlanEntries(root), DONE_DATE_RE),
|
|
324
|
+
fixedBugs: recentFinished(fixedBugEntries(root), FIXED_DATE_RE),
|
|
325
|
+
// What needs the operator (team-alerts.ts's teamAlerts): the dashboard's alert banners and
|
|
239
326
|
// the TUI's attention lines, phrased once.
|
|
240
|
-
alerts:
|
|
327
|
+
alerts: teamAlerts(snap, questions, roles, now, refusedEntryTitles(root)),
|
|
241
328
|
};
|
|
242
329
|
}
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { dailyCost } from "../budget/budget.js";
|
|
2
2
|
import { clipToWidth, displayWidth } from "../text/text-width.js";
|
|
3
3
|
import { compactTokens, usd } from "../text/format.js";
|
|
4
|
-
import { elapsedSeconds, formatTime, humanSeconds, pad2, secondsUntil } from "../text/datetime.js";
|
|
4
|
+
import { elapsedSeconds, formatTime, humanSeconds, nextRunPhrase, pad2, secondsUntil } from "../text/datetime.js";
|
|
5
5
|
import { projectName } from "../project-name.js";
|
|
6
|
-
import { bootstrapBadge, buildBadge, budgetBadge, claudeWindowTone, diskBadge,
|
|
7
|
-
import { idleClaimText, isActivePhase, loopRowCells, sortLoopsByState } from "./status-model.js";
|
|
6
|
+
import { bootstrapBadge, buildBadge, budgetBadge, claudeWindowTone, diskBadge, mergeBadge, mainCheckBadge, mainCheckVerdict, pauseBadge, loopWakeMarker, permitsBadge, quietBadge, runWindowBadge, wakeBadge } from "./badges.js";
|
|
7
|
+
import { idleClaimText, isActivePhase, loopRowCells, onErrorStreak, sortLoopsByState } from "./status-model.js";
|
|
8
8
|
import { tickProgress } from "./tick-progress-model.js";
|
|
9
|
-
import {
|
|
9
|
+
import { eventTypeLabel } from "../events/event-labels.js";
|
|
10
|
+
import { roleLabel } from "../roles/role-labels.js";
|
|
11
|
+
import { yieldMultiplierFor, yieldMultiplierSuffix } from "../scheduling/backoff.js";
|
|
10
12
|
import { phaseTone, resultTone } from "./tone.js";
|
|
13
|
+
import { teamAlerts } from "./team-alerts.js";
|
|
14
|
+
import { openQuestions } from "../backlog/backlog.js";
|
|
15
|
+
import { refusedEntryTitles } from "../backlog/backlog-eligibility.js";
|
|
11
16
|
/** The status RENDER layer: time/token cell formatters and the width-aware table shared by
|
|
12
17
|
* `tumwater status` and the TUI. The labels, badges, and metrics it draws come from the shared
|
|
13
18
|
* display model (status-model.ts); this module decides column widths and layout. Depends on
|
|
14
|
-
* status/status-data.ts one way — rendering reads the snapshot; it never collects
|
|
19
|
+
* status/status-data.ts one way — rendering reads the snapshot; it never collects team state
|
|
15
20
|
* itself (live tick detail is display-only). */
|
|
16
21
|
function ago(ts) {
|
|
17
22
|
if (!ts)
|
|
@@ -33,32 +38,35 @@ export function lastTickCell(ts) {
|
|
|
33
38
|
s = `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${s}`;
|
|
34
39
|
return `${s} · ${ago(ts)}`;
|
|
35
40
|
}
|
|
41
|
+
/** The table's `last merged` cell: when the loop last merged a change on main, in the SAME
|
|
42
|
+
* absolute-time-plus-relative-age shape as the `last tick` cell (lastTickCell), so the two
|
|
43
|
+
* columns cannot drift on what "ago" means. A loop that has never merged shows the same dash
|
|
44
|
+
* lastTickCell uses for a never-ticked loop. */
|
|
45
|
+
export function lastMergedCell(ts) {
|
|
46
|
+
return lastTickCell(ts);
|
|
47
|
+
}
|
|
36
48
|
/** The table's `next run` cell: when the loop may tick again, or `-` when that is not the
|
|
37
|
-
* operator's question — a loop in flight (an active rendered phase: working/reviewing/
|
|
38
|
-
* or `s.running`) has no next run to speak of, and a stopped
|
|
49
|
+
* operator's question — a loop in flight (an active rendered phase: working/reviewing/merge,
|
|
50
|
+
* or `s.running`) has no next run to speak of, and a stopped team renders no schedule at all
|
|
39
51
|
* (its `nextRunAt`s are stale leftovers, not plans). A due idle loop reads `now`; a future one
|
|
40
52
|
* reads its remaining time through humanSeconds (the same bucketing the sleeping phase label
|
|
41
53
|
* uses, so the two cannot drift), prefixed `backoff ` while `backoffSeconds > 0` — the wake
|
|
42
54
|
* row-action clears exactly that, so the prefix is what makes waking meaningful. The GUI
|
|
43
55
|
* renders the same rules from its own JS copy in gui-client.ts (fmtNextRun). The `phase` and
|
|
44
|
-
* `
|
|
45
|
-
* active-phase check needs the rendered label (a
|
|
46
|
-
*
|
|
47
|
-
export function nextRunCell(s, phase, now,
|
|
48
|
-
if (!
|
|
56
|
+
* `teamRunning` arguments come from the caller because neither fact lives on LoopState: the
|
|
57
|
+
* active-phase check needs the rendered label (a merge is visible only there), and the
|
|
58
|
+
* team's running flag lives on the snapshot, not the loop. */
|
|
59
|
+
export function nextRunCell(s, phase, now, teamRunning) {
|
|
60
|
+
if (!teamRunning || s.running || isActivePhase(phase))
|
|
49
61
|
return "-";
|
|
50
62
|
const remain = secondsUntil(s.nextRunAt, now);
|
|
51
63
|
// The yield multiplier rides the cell as `×N` (yield-scaled clocks, PLANS.md): a scalable
|
|
52
|
-
// role whose recent ticks
|
|
64
|
+
// role whose recent ticks merged nothing keeps a longer effective gap than nextRunAt's
|
|
53
65
|
// countdown shows, and the suffix is what makes that visible. The multiplier gates the
|
|
54
66
|
// scheduled gap and the main-moved wake in isEligible, so a `now`-due cell at ×4 is a
|
|
55
67
|
// role waiting out its stretched gap, not a broken clock.
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
if (remain <= 0)
|
|
59
|
-
return `now${suffix}`;
|
|
60
|
-
const label = humanSeconds(remain);
|
|
61
|
-
return (s.backoffSeconds > 0 ? `backoff ${label}` : label) + suffix;
|
|
68
|
+
const suffix = yieldMultiplierSuffix(yieldMultiplierFor(s));
|
|
69
|
+
return nextRunPhrase(remain, s.backoffSeconds > 0, "now", false) + suffix;
|
|
62
70
|
}
|
|
63
71
|
/** The table's state cell: a loop's phase label (loopPhase, computed once per row by
|
|
64
72
|
* renderStatus so the same label drives the row order), with the current work item prepended
|
|
@@ -70,11 +78,11 @@ export function nextRunCell(s, phase, now, fleetRunning) {
|
|
|
70
78
|
function stateCell(root, s, phase, live) {
|
|
71
79
|
// While under review the log tail's "current work" is the reviewer's own output, not the
|
|
72
80
|
// author's task — don't prepend it; the phase cell already carries the reviewer's live
|
|
73
|
-
// detail. The
|
|
74
|
-
// phase ("
|
|
81
|
+
// detail. The merge label rides the same guard: the merge role is not running, so its
|
|
82
|
+
// phase ("merge <elapsed> · <stage>", with the reviewer's live detail while it reviews)
|
|
75
83
|
// is returned without a work-item prefix.
|
|
76
84
|
if (!s.running || s.parkedSince || s.phase === "review") {
|
|
77
|
-
// Parallel work instances, part 6/7: an idle or
|
|
85
|
+
// Parallel work instances, part 6/7: an idle or merge instance leads with the entry it
|
|
78
86
|
// claimed (or, for an idle extra with none, the no-unclaimed note), so the operator can
|
|
79
87
|
// see WHAT it is parked on without opening its drawer.
|
|
80
88
|
const idle = idleClaimText(s, phase);
|
|
@@ -87,11 +95,13 @@ function stateCell(root, s, phase, live) {
|
|
|
87
95
|
/** Columns allowed to shrink when the table is wider than the terminal, widest offender
|
|
88
96
|
* first: `last result` (holds the tick summary), then `state` (live working detail), then
|
|
89
97
|
* `last tick` — it shrinks last so on a narrow terminal it loses " · 3m ago" before whole
|
|
90
|
-
* lines clip; its minimum is a bare HH:MM:SS.
|
|
91
|
-
*
|
|
92
|
-
*
|
|
98
|
+
* lines clip; its minimum is a bare HH:MM:SS. `last merged` sits right of `last tick` and is
|
|
99
|
+
* not flexible: it is a short sibling time cell whose width follows lastTickCell's, and the
|
|
100
|
+
* whole-line clip that already protects the header also bounds it. Indices are positional in
|
|
101
|
+
* the `cols` array below — renumber when columns change. (`today`, like `cost`, is a short
|
|
102
|
+
* fixed-width cell: never flexible.) */
|
|
93
103
|
const FLEXIBLE_COLUMNS = [
|
|
94
|
-
{ index:
|
|
104
|
+
{ index: 10, minWidth: 12 },
|
|
95
105
|
{ index: 1, minWidth: 12 },
|
|
96
106
|
{ index: 8, minWidth: 10 },
|
|
97
107
|
];
|
|
@@ -124,7 +134,35 @@ export function clipSpans(line, width) {
|
|
|
124
134
|
}
|
|
125
135
|
return out;
|
|
126
136
|
}
|
|
127
|
-
/**
|
|
137
|
+
/** The attention line's mark and tone, per alert tone — shared by the TUI and `tumwater
|
|
138
|
+
* status` (status-render.ts alertLines), so the two surfaces draw the same banner glyphs. */
|
|
139
|
+
const ALERT_MARKS = {
|
|
140
|
+
red: { mark: "!", tone: "red" },
|
|
141
|
+
amber: { mark: "!", tone: "yellow" },
|
|
142
|
+
indigo: { mark: "?", tone: "magenta" },
|
|
143
|
+
blue: { mark: "i", tone: "blue" },
|
|
144
|
+
gray: { mark: "i", tone: "dim" },
|
|
145
|
+
};
|
|
146
|
+
/** A plain line clipped to a width, in one tone. */
|
|
147
|
+
export function toneLine(text, width, tone) {
|
|
148
|
+
return clipSpans([{ text, ...(tone ? { tone } : {}) }], width);
|
|
149
|
+
}
|
|
150
|
+
/** The attention lines under a header: one per alert — its mark and title in its tone, then
|
|
151
|
+
* the detail — at most `max` of them, the rest counted on a last line. This is the banner
|
|
152
|
+
* block the TUI draws under its header and `tumwater status` pushes under its own, so the two
|
|
153
|
+
* surfaces render the same wording from the one shared alert source (team-alerts.ts). */
|
|
154
|
+
export function alertLines(alerts, width, max = 3) {
|
|
155
|
+
const shown = alerts.length > max ? alerts.slice(0, max - 1) : alerts;
|
|
156
|
+
const lines = shown.map((a) => {
|
|
157
|
+
const { mark, tone } = ALERT_MARKS[a.tone];
|
|
158
|
+
const t = tone ? { tone } : {};
|
|
159
|
+
return clipSpans([{ text: `${mark} `, ...t }, { text: a.title, ...t }, { text: a.detail ? ` — ${a.detail}` : "" }], width);
|
|
160
|
+
});
|
|
161
|
+
if (shown.length < alerts.length)
|
|
162
|
+
lines.push(toneLine(` +${alerts.length - shown.length} more — the dashboard lists them all`, width, "dim"));
|
|
163
|
+
return lines;
|
|
164
|
+
}
|
|
165
|
+
/** A header badge (" · merge queue: 2", ", build abc…") as a plain separator plus its toned
|
|
128
166
|
* words, so the TUI colors the words and not the dots. Empty badges add nothing. */
|
|
129
167
|
function badgeSpans(badge, tone) {
|
|
130
168
|
if (!badge)
|
|
@@ -157,27 +195,32 @@ export function renderStatusSpans(root, snap, maxWidth) {
|
|
|
157
195
|
// The questions badge (like the inbox one) appears only when something needs an answer.
|
|
158
196
|
// The budget badge is standing information for a money-spending system in EVERY cap state
|
|
159
197
|
// (disabled reads `· no cap` — and it is the affordance for editing the cap); on narrow
|
|
160
|
-
// terminals the header's existing last-resort whole-line clipping applies. A
|
|
198
|
+
// terminals the header's existing last-resort whole-line clipping applies. A team whose
|
|
161
199
|
// models are all free reads n/a — spend can never accumulate against a cap that cannot be
|
|
162
200
|
// reached, so a dollar figure would mislead. The timed-pause badge (plans/pause-countdown
|
|
163
|
-
// wording) rides last so a
|
|
164
|
-
// empty unless the
|
|
201
|
+
// wording) rides last so a team under a timed pause reads how long until it comes back —
|
|
202
|
+
// empty unless the team marker's deadline stands in the future, so a role-only or
|
|
165
203
|
// indefinite pause keeps today's header byte-identical.
|
|
166
204
|
const running = snap.running
|
|
167
205
|
? [{ text: "running", tone: "green" }, { text: ` (pid ${snap.pid}` }, ...badgeSpans(buildBadge(snap.build), snap.build?.stale ? "yellow" : undefined), { text: ")" }]
|
|
168
|
-
: [{ text: "not running — start with `tumwater
|
|
206
|
+
: [{ text: "not running — start with `tumwater start`", tone: "dim" }];
|
|
169
207
|
lines.push([
|
|
170
208
|
{ text: "tumwater", tone: "brand" },
|
|
171
209
|
{ text: ` · ${name} · ` },
|
|
172
210
|
...running,
|
|
173
|
-
...badgeSpans(
|
|
211
|
+
...(snap.running ? badgeSpans(runWindowBadge(snap.runStopsAt, now), "blue") : []),
|
|
212
|
+
...badgeSpans(mergeBadge(snap.mergeQueue), "blue"),
|
|
174
213
|
...(snap.inbox ? [{ text: ` · inbox: ${snap.inbox}` }] : []),
|
|
175
214
|
...(snap.questions ? badgeSpans(` · questions: ${snap.questions}`, "magenta") : []),
|
|
176
|
-
...badgeSpans(budgetBadge(snap.budget, snap.claudeWindow), budgetTone(snap.budget, snap.claudeWindow)),
|
|
215
|
+
...badgeSpans(budgetBadge(snap.budget, snap.claudeWindow, now, snap.chain), budgetTone(snap.budget, snap.claudeWindow)),
|
|
177
216
|
// The disk floor (plans/disk-floor.md, part 4/4): `disk 8.2 GB free — holding new work`
|
|
178
217
|
// while the hold is on, or the bare reading below the reclaim threshold. Empty in every
|
|
179
218
|
// comfortable or unmeasured state, so an unaffected header stays byte-identical.
|
|
180
|
-
...badgeSpans(diskBadge(snap.disk
|
|
219
|
+
...badgeSpans(diskBadge(snap.disk), snap.disk?.held ? "yellow" : "blue"),
|
|
220
|
+
// The permit pool's live state (plans/provider-concurrency.md §8): the team line reads
|
|
221
|
+
// `slots 5/8 · omlx 3/3` only while the per-provider cap map is set (permitsBadge already
|
|
222
|
+
// omits the providers on a map-less team, so the header stays byte-identical).
|
|
223
|
+
...badgeSpans(permitsBadge(snap.permits), "blue"),
|
|
181
224
|
// New-project bootstrap (plans/work-ratio.md, part 2/2): the progress reading while the
|
|
182
225
|
// hold is active. Empty in every normal project, so the header stays byte-identical.
|
|
183
226
|
...badgeSpans(bootstrapBadge(snap.bootstrap), "blue"),
|
|
@@ -199,9 +242,10 @@ export function renderStatusSpans(root, snap, maxWidth) {
|
|
|
199
242
|
// or missing, so loops that never ticked — or last ticked yesterday — read zero without a
|
|
200
243
|
// save. It renders whether or not the cap is enabled: spend observability does not depend
|
|
201
244
|
// on it.
|
|
202
|
-
// `next run` is
|
|
203
|
-
//
|
|
204
|
-
|
|
245
|
+
// `next run` is a short fixed-width cell like `today`, never flexible: the columns that
|
|
246
|
+
// give way when the table is wider than the terminal are `last result`, then `state`, then
|
|
247
|
+
// `last tick`, in FLEXIBLE_COLUMNS' order.
|
|
248
|
+
const cols = ["role", "state", "runs", "commits", "output", "peak ctx", "cost", "today", "last run", "last merged", "last result", "next run"];
|
|
205
249
|
// One live tail read per running loop per frame, threaded through every cell that shows
|
|
206
250
|
// in-flight detail (metrics, state, current work) — each helper used to re-read the log on
|
|
207
251
|
// its own, up to three stats + reads per loop per second. The phase is computed once per
|
|
@@ -225,7 +269,7 @@ export function renderStatusSpans(root, snap, maxWidth) {
|
|
|
225
269
|
live: cells.live,
|
|
226
270
|
phase: cells.phase,
|
|
227
271
|
role: s.role,
|
|
228
|
-
|
|
272
|
+
lastRunEndedAt: s.lastRunEndedAt,
|
|
229
273
|
};
|
|
230
274
|
});
|
|
231
275
|
// User-defined loops (snapshot's `custom` flag) get an asterisk beside their name — the
|
|
@@ -244,12 +288,12 @@ export function renderStatusSpans(root, snap, maxWidth) {
|
|
|
244
288
|
const modelSuffix = s.modelTier
|
|
245
289
|
? ` (${s.modelTier}${s.model ? ` · ${s.model}` : ""})`
|
|
246
290
|
: s.model
|
|
247
|
-
? ` (${s.model})`
|
|
291
|
+
? ` (${s.model}${s.modelFromPi ? ", pi default" : ""})`
|
|
248
292
|
: "";
|
|
249
293
|
// Model failure fallback, part 2/2: while the loop runs off-model its name cell says so,
|
|
250
294
|
// so the operator can see which loops the fallback pair is carrying without reading logs.
|
|
251
|
-
const fallbackSuffix = s.fallback ? " (
|
|
252
|
-
const name = (s.custom ? `${s.role}*` : s.role) + modelSuffix + fallbackSuffix;
|
|
295
|
+
const fallbackSuffix = s.fallback ? " (over-cap model)" : "";
|
|
296
|
+
const name = (s.custom ? `${roleLabel(s.role)}*` : roleLabel(s.role)) + modelSuffix + fallbackSuffix;
|
|
253
297
|
const state = stateCell(root, s, phase, live);
|
|
254
298
|
const tone = phaseTone(phase);
|
|
255
299
|
// The whole state cell carries the phase's tone: a work item leads the cell (so it
|
|
@@ -260,20 +304,21 @@ export function renderStatusSpans(root, snap, maxWidth) {
|
|
|
260
304
|
const resultT = resultTone(s.lastResult);
|
|
261
305
|
return [
|
|
262
306
|
cell(name),
|
|
263
|
-
// Merge queue 4/5 — the
|
|
264
|
-
// this role by loopPhase's `
|
|
307
|
+
// Merge queue 4/5 — the merge role's phase label (the marker-driven record, filtered to
|
|
308
|
+
// this role by loopPhase's `merge` argument above) rides this same phase string.
|
|
265
309
|
[...stateSpans, { text: roleQueued(s.role) }, ...(wakeMark ? [{ text: wakeMark, tone: "blue" }] : [])],
|
|
266
|
-
cell(String(s.
|
|
310
|
+
cell(String(s.runs)),
|
|
267
311
|
cell(String(s.commits)),
|
|
268
312
|
cell(compactTokens(m.generated)),
|
|
269
313
|
cell(compactTokens(m.peakCtx)),
|
|
270
314
|
cell(usd(s.totalCostUsd)),
|
|
271
315
|
cell(usd(dailyCost(s))),
|
|
272
|
-
cell(lastTickCell(s.
|
|
316
|
+
cell(lastTickCell(s.lastRunEndedAt)),
|
|
317
|
+
cell(lastMergedCell(s.lastMergedAt)),
|
|
273
318
|
s.lastResult
|
|
274
|
-
? [{ text: s.lastResult, ...(resultT ? { tone: resultT } : {}) }, { text: s.lastSummary ? ` — ${s.lastSummary}` : "" }]
|
|
319
|
+
? [{ text: eventTypeLabel(s.lastResult), ...(resultT ? { tone: resultT } : {}) }, { text: s.lastSummary ? ` — ${s.lastSummary}` : "" }]
|
|
275
320
|
: cell("-"),
|
|
276
|
-
cell(next, next.startsWith("
|
|
321
|
+
cell(next, next.startsWith("sleeping") ? "yellow" : undefined),
|
|
277
322
|
];
|
|
278
323
|
});
|
|
279
324
|
const totalsRow = [
|
|
@@ -284,7 +329,7 @@ export function renderStatusSpans(root, snap, maxWidth) {
|
|
|
284
329
|
compactTokens(withMetrics.reduce((sum, { m }) => sum + m.generated, 0)),
|
|
285
330
|
compactTokens(Math.max(0, ...withMetrics.map(({ m }) => m.peakCtx))),
|
|
286
331
|
usd(snap.loops.reduce((sum, s) => sum + s.totalCostUsd, 0)),
|
|
287
|
-
// The
|
|
332
|
+
// The team's today-spend, from the snapshot's budget block — the same number the header
|
|
288
333
|
// badge reads, so table and badge cannot drift. While the orchestrator runs that figure is
|
|
289
334
|
// the scheduler's live sum over its in-memory states, which can sit ABOVE the per-loop
|
|
290
335
|
// `today` cells above (each loop's persisted copy, written only at its saves): the gap is
|
|
@@ -295,6 +340,7 @@ export function renderStatusSpans(root, snap, maxWidth) {
|
|
|
295
340
|
"",
|
|
296
341
|
"",
|
|
297
342
|
"",
|
|
343
|
+
"",
|
|
298
344
|
].map((text) => cell(text));
|
|
299
345
|
const allRows = [...rows, totalsRow];
|
|
300
346
|
// Widths are measured in terminal display columns (displayWidth), not UTF-16 code units:
|
|
@@ -344,6 +390,18 @@ export function renderStatusSpans(root, snap, maxWidth) {
|
|
|
344
390
|
}
|
|
345
391
|
return out;
|
|
346
392
|
};
|
|
393
|
+
const loops = withMetrics.map(({ s, phase, live }) => ({
|
|
394
|
+
role: s.role,
|
|
395
|
+
phase,
|
|
396
|
+
errorStreak: onErrorStreak(s),
|
|
397
|
+
inFlight: isActivePhase(phase),
|
|
398
|
+
...(live?.quietMs !== undefined ? { stuckMs: live.quietMs } : {}),
|
|
399
|
+
...(s.lastError ? { lastError: s.lastError } : {}),
|
|
400
|
+
// The loop's image-input surface (plans/image-input.md "Surfaces", part 5/5): always
|
|
401
|
+
// present, so the TUI's images attention line keys on it like the dashboard's.
|
|
402
|
+
imageInput: s.imageInput,
|
|
403
|
+
imageReroute: s.imageRerouteLabel,
|
|
404
|
+
}));
|
|
347
405
|
const separator = [{ text: widths.map((w) => "-".repeat(w)).join(" "), tone: "dim" }];
|
|
348
406
|
lines.push(fmt(cols.map((c) => cell(c)), "dim"));
|
|
349
407
|
lines.push(separator);
|
|
@@ -352,10 +410,9 @@ export function renderStatusSpans(root, snap, maxWidth) {
|
|
|
352
410
|
lines.push(separator);
|
|
353
411
|
lines.push(fmt(totalsRow, "bold"));
|
|
354
412
|
// One footnote under the table when any custom loop exists — explains the asterisk without
|
|
355
|
-
// taking a column. Absent (byte-identical table) on a
|
|
413
|
+
// taking a column. Absent (byte-identical table) on a team with no user-defined loops.
|
|
356
414
|
if (snap.loops.some((l) => l.custom))
|
|
357
|
-
lines.push([{ text: "*
|
|
358
|
-
const loops = withMetrics.map(({ s, phase }) => ({ role: s.role, phase, inFlight: isActivePhase(phase), ...(s.lastError ? { lastError: s.lastError } : {}) }));
|
|
415
|
+
lines.push([{ text: "* custom role", tone: "dim" }]);
|
|
359
416
|
// The header line (and any residual overflow past the columns' minimums) is clipped too,
|
|
360
417
|
// so no status line ever wraps in a terminal of `maxWidth` columns.
|
|
361
418
|
return { lines: maxWidth === undefined ? lines : lines.map((l) => clipSpans(l, maxWidth)), loops };
|
|
@@ -364,5 +421,18 @@ export function renderStatusSpans(root, snap, maxWidth) {
|
|
|
364
421
|
* renderStatusSpans). When `maxWidth` is given, wide cells are clipped so no line exceeds it
|
|
365
422
|
* (terminal rows never wrap). */
|
|
366
423
|
export function renderStatus(root, snap, maxWidth) {
|
|
367
|
-
|
|
424
|
+
const { lines, loops } = renderStatusSpans(root, snap, maxWidth);
|
|
425
|
+
// The team-attention banners (team-alerts.ts) drop under the header's blank line here, on
|
|
426
|
+
// the CLI-only plain-text surface — the same spelled-out banners the dashboard and TUI draw,
|
|
427
|
+
// from the one shared source, so the CLI/GUI/TUI cannot drift on wording. One QUESTIONS.md
|
|
428
|
+
// read (openQuestions) for the pending-questions alert — the dashboard's cadence. The CLI
|
|
429
|
+
// omits the not-running banner: the header's first line already spells out the team's state,
|
|
430
|
+
// so a `status` run on a stopped team does not lead with a banner that duplicates it.
|
|
431
|
+
// Composed here, NOT inside renderStatusSpans: the TUI re-slices that function's shared
|
|
432
|
+
// `lines` (lines[0], its own attention block, then lines.slice(1)), so a banner embedded
|
|
433
|
+
// there would render twice in the TUI.
|
|
434
|
+
const alerts = teamAlerts(snap, openQuestions(root), loops, Date.now(), refusedEntryTitles(root)).filter((a) => a.key !== "stopped");
|
|
435
|
+
const banner = alertLines(alerts, maxWidth ?? Number.MAX_SAFE_INTEGER, alerts.length).map(plainText);
|
|
436
|
+
const text = lines.map(plainText);
|
|
437
|
+
return [...text.slice(0, 2), ...banner, ...text.slice(2)].join("\n");
|
|
368
438
|
}
|