tumwater 0.4.2 → 0.4.3
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} +25 -12
- 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 +1 -8
- 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 +9 -0
- 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 +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +50 -27
- 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 +1 -1
- package/dist/src/agent/subscription-account.js +3 -3
- 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 +58 -102
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +1 -1
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- 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-scoped.js +40 -16
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +34 -12
- package/dist/src/cli/cli-query-commands.js +1 -1
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-start.js +91 -58
- package/dist/src/cli/completion.js +82 -46
- package/dist/src/cli/config-commands.js +82 -71
- package/dist/src/cli/help.js +4 -6
- package/dist/src/cli/log-commands.js +43 -23
- package/dist/src/cli/notify-command.js +2 -2
- package/dist/src/cli/question-commands.js +86 -60
- package/dist/src/cli.js +181 -135
- package/dist/src/collections.js +31 -16
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +22 -0
- 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 +5 -20
- 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 +153 -102
- package/dist/src/config/config-schema.js +9 -11
- 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 +4 -52
- package/dist/src/config/config-validation.js +4 -38
- package/dist/src/config/config-views.js +9 -34
- package/dist/src/config/config-write.js +6 -23
- package/dist/src/config/config.js +4 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +24 -24
- package/dist/src/doctor/doctor-backlog.js +7 -5
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +5 -5
- package/dist/src/doctor/doctor-claude-checks.js +44 -28
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +44 -112
- package/dist/src/doctor/doctor-fallback-checks.js +2 -2
- package/dist/src/doctor/doctor-holds.js +6 -45
- 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 +120 -20
- 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 +3 -3
- 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 +30 -54
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +1 -3
- package/dist/src/events/{notify.js → event-notify.js} +2 -2
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/failure/failure-cluster.js +6 -6
- package/dist/src/failure/failure-data.js +8 -4
- package/dist/src/failure/failure-render.js +44 -34
- package/dist/src/failure/failure-state-change.js +4 -34
- 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 +27 -68
- 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} +7 -7
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- 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-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-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +22 -23
- package/dist/src/gui/gui-prompt-commands.js +23 -23
- package/dist/src/gui/gui-report-endpoints.js +1 -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 +51 -33
- 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-core.js +1 -1
- package/dist/src/merge/merge-drain.js +8 -11
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +10 -13
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-verify.js +8 -18
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +76 -44
- 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 +27 -32
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +1 -1
- package/dist/src/orchestrator/scheduling-skip.js +1 -1
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +8 -7
- 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 +1 -1
- 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 +32 -6
- package/dist/src/pi/pi-watchdogs.js +12 -5
- 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 +49 -5
- package/dist/src/process/process.js +8 -8
- package/dist/src/process/run-marker.js +31 -13
- 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/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/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/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +1 -1
- package/dist/src/review/review-precheck.js +11 -1
- package/dist/src/review/review.js +40 -153
- package/dist/src/review/secret-leaks.js +26 -16
- 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 +1 -14
- package/dist/src/roles/role-view.js +9 -21
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/run-apply.js +12 -6
- 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 +135 -84
- 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 +119 -50
- package/dist/src/run/stage-check.js +49 -32
- 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/runner-outcome.js +6 -1
- package/dist/src/runner/runner-pi.js +3 -10
- package/dist/src/runner/runner-recovery.js +1 -1
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
- package/dist/src/runner/runner-run.js +97 -35
- package/dist/src/runner/runner-state-load.js +12 -9
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-data.js +2 -2
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -17
- 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 +98 -65
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
- package/dist/src/team/team-polls.js +28 -126
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +20 -15
- package/dist/src/text/text.js +3 -3
- package/dist/src/ui/alert-feeds.js +49 -73
- package/dist/src/ui/badges.js +3 -13
- package/dist/src/ui/gui/gui-client-boot.js +2 -2
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
- package/dist/src/ui/gui/gui-client-drawer.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-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 +4 -4
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +53 -40
- package/dist/src/ui/run-progress-model.js +9 -2
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +3 -7
- package/dist/src/ui/status-render.js +5 -9
- package/dist/src/ui/team-alert-backlog.js +6 -6
- 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 +7 -8
- package/dist/src/ui/transcript-run.js +27 -16
- 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 +1 -7
- 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 +159 -92
- 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 +3 -3
- 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 -228
- package/dist/src/ui/tui/tui-pane-history.js +61 -70
- package/dist/src/ui/tui/tui-pane-pending.js +284 -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 +11 -7
- 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 +29 -32
- package/dist/src/ui/tui/tui.js +3 -4
- package/dist/src/verdict/fix-claim.js +76 -54
- package/dist/src/verdict/refusal.js +1 -1
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- 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/backlog/{question-answer.js → backlog-question-answer.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
- /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
|
@@ -86,7 +86,7 @@ function freshProgress(quietMs, agent) {
|
|
|
86
86
|
/** The event types feedLine acts on — everything else (streaming deltas, turn/agent
|
|
87
87
|
* bookkeeping) is ignored. Also passed as parsePiEventLine's pre-filter to skip JSON.parse for
|
|
88
88
|
* pi lines whose type is verifiably not one of these; a new case in the switch must be added
|
|
89
|
-
* here too. `tumwater_run` is the harness's own marker (src/agent/
|
|
89
|
+
* here too. `tumwater_run` is the harness's own marker (src/agent/agent-spawn.ts), read for its
|
|
90
90
|
* run kind: a kind-bearing marker flips the demux and starts that accumulator fresh before
|
|
91
91
|
* its `session` event lands, and a legacy label-only marker ("review") still flips the demux
|
|
92
92
|
* to gate. */
|
|
@@ -98,54 +98,67 @@ const PROGRESS_TYPES = new Set([
|
|
|
98
98
|
"tool_execution_end",
|
|
99
99
|
"message_end",
|
|
100
100
|
]);
|
|
101
|
+
/** A new run starts: everything before it was a previous run — restore the at-time-zero state. */
|
|
102
|
+
function applySessionEvent(progress) {
|
|
103
|
+
Object.assign(progress, freshProgress(progress.quietMs));
|
|
104
|
+
}
|
|
105
|
+
/** A tool execution starts: count the call, record its label as the last tool, and seed the
|
|
106
|
+
* open-call entry the stall flag reads. Track the open call for the stall flag — pi runs a
|
|
107
|
+
* message's tool calls concurrently by default, so several can be open at once and end in
|
|
108
|
+
* completion order. The state machine is shared with runPi's stall warning
|
|
109
|
+
* (src/pi/pi-watchdogs.ts); only the fallback label differs here. The full raw command rides
|
|
110
|
+
* along because the flag classifies the command's shape, and describeToolCall's label
|
|
111
|
+
* truncates at 32 chars — an operator past that point would be invisible there. */
|
|
112
|
+
function applyToolStart(progress, event) {
|
|
113
|
+
progress.toolCalls += 1;
|
|
114
|
+
const label = event.toolName ? describeToolCall(event.toolName, event.args) : undefined;
|
|
115
|
+
if (label)
|
|
116
|
+
progress.lastTool = label;
|
|
117
|
+
applyToolExecutionEvent(progress.openToolCalls ??= [], event.type, event.toolCallId, event.partialResult, label ?? "tool", toolCallCommand(event.args));
|
|
118
|
+
}
|
|
119
|
+
/** A tool update or end folds through the same call: `applyToolExecutionEvent` reads `event.type`
|
|
120
|
+
* to decide what to do (a content-bearing update moves the activity clock; end closes the
|
|
121
|
+
* call), and both no-op when no call is tracked. Only content-bearing updates prove the
|
|
122
|
+
* command is alive — bash emits one empty-content update right after start, and a
|
|
123
|
+
* content-free keepalive must not mask a hang — a rule applyToolExecutionEvent owns. */
|
|
124
|
+
function applyToolProgress(progress, event) {
|
|
125
|
+
if (progress.openToolCalls)
|
|
126
|
+
applyToolExecutionEvent(progress.openToolCalls, event.type, event.toolCallId, event.partialResult);
|
|
127
|
+
}
|
|
128
|
+
/** An assistant message ends: count the turn and fold usage/peak/context and the first work item.
|
|
129
|
+
* Guard each count like pi-stream's usageNumber does: a string-serialized or corrupt wire value
|
|
130
|
+
* would otherwise poison the live cell — `+=` would concatenate output into a string and
|
|
131
|
+
* Math.max would yield NaN. An unusable total keeps the previous context (this cell is "latest
|
|
132
|
+
* value"), while output and the peak floor treat it as absent. The first text the role speaks
|
|
133
|
+
* in this run is its work item ("I'll implement plan X"); later messages never replace it. */
|
|
134
|
+
function applyAssistantTurn(progress, event) {
|
|
135
|
+
if (event.message?.role !== "assistant")
|
|
136
|
+
return;
|
|
137
|
+
progress.turns += 1;
|
|
138
|
+
const usage = event.message.usage;
|
|
139
|
+
if (usage) {
|
|
140
|
+
progress.contextTokens = nonNegativeNumber(usage.totalTokens, progress.contextTokens);
|
|
141
|
+
progress.outputTokens += nonNegativeNumber(usage.output, 0);
|
|
142
|
+
progress.peakContextTokens = Math.max(progress.peakContextTokens, nonNegativeNumber(usage.totalTokens, 0));
|
|
143
|
+
}
|
|
144
|
+
if (!progress.currentWork)
|
|
145
|
+
progress.currentWork = workItemFromContent(event.message.content);
|
|
146
|
+
}
|
|
101
147
|
/** Apply one parsed progress event to a progress object (mutates it). */
|
|
102
148
|
function feedLine(progress, event) {
|
|
103
149
|
switch (event.type) {
|
|
104
150
|
case "session": // A new run starts: everything before it was a previous run — restore the at-time-zero state.
|
|
105
|
-
|
|
151
|
+
applySessionEvent(progress);
|
|
106
152
|
break;
|
|
107
|
-
case "tool_execution_start":
|
|
108
|
-
progress
|
|
109
|
-
const label = event.toolName ? describeToolCall(event.toolName, event.args) : undefined;
|
|
110
|
-
if (label)
|
|
111
|
-
progress.lastTool = label;
|
|
112
|
-
// Track the open call for the stall flag — pi runs a message's tool calls concurrently
|
|
113
|
-
// by default, so several can be open at once and end in completion order. The state
|
|
114
|
-
// machine is shared with runPi's stall warning (src/pi/pi-watchdogs.ts); only the
|
|
115
|
-
// fallback label differs here. The full raw command rides along because the flag
|
|
116
|
-
// classifies the command's shape, and describeToolCall's label truncates at 32 chars —
|
|
117
|
-
// an operator past that point would be invisible there.
|
|
118
|
-
applyToolExecutionEvent(progress.openToolCalls ??= [], event.type, event.toolCallId, event.partialResult, label ?? "tool", toolCallCommand(event.args));
|
|
153
|
+
case "tool_execution_start":
|
|
154
|
+
applyToolStart(progress, event);
|
|
119
155
|
break;
|
|
120
|
-
}
|
|
121
|
-
// Updates and ends fold through the same call: `applyToolExecutionEvent` reads `event.type`
|
|
122
|
-
// to decide what to do (a content-bearing update moves the activity clock; end closes the
|
|
123
|
-
// call), and both no-op when no call is tracked. Only content-bearing updates prove the
|
|
124
|
-
// command is alive — bash emits one empty-content update right after start, and a
|
|
125
|
-
// content-free keepalive must not mask a hang — a rule applyToolExecutionEvent owns.
|
|
126
156
|
case "tool_execution_update":
|
|
127
157
|
case "tool_execution_end":
|
|
128
|
-
|
|
129
|
-
applyToolExecutionEvent(progress.openToolCalls, event.type, event.toolCallId, event.partialResult);
|
|
158
|
+
applyToolProgress(progress, event);
|
|
130
159
|
break;
|
|
131
160
|
case "message_end":
|
|
132
|
-
|
|
133
|
-
progress.turns += 1;
|
|
134
|
-
const usage = event.message.usage;
|
|
135
|
-
if (usage) {
|
|
136
|
-
// Guard each count like pi-stream's usageNumber does: a string-serialized or corrupt
|
|
137
|
-
// wire value would otherwise poison the live cell — `+=` would concatenate output into
|
|
138
|
-
// a string and Math.max would yield NaN. An unusable total keeps the previous context
|
|
139
|
-
// (this cell is "latest value"), while output and the peak floor treat it as absent.
|
|
140
|
-
progress.contextTokens = nonNegativeNumber(usage.totalTokens, progress.contextTokens);
|
|
141
|
-
progress.outputTokens += nonNegativeNumber(usage.output, 0);
|
|
142
|
-
progress.peakContextTokens = Math.max(progress.peakContextTokens, nonNegativeNumber(usage.totalTokens, 0));
|
|
143
|
-
}
|
|
144
|
-
// The first text the role speaks in this run is its work item ("I'll implement plan X");
|
|
145
|
-
// later messages never replace it.
|
|
146
|
-
if (!progress.currentWork)
|
|
147
|
-
progress.currentWork = workItemFromContent(event.message.content);
|
|
148
|
-
}
|
|
161
|
+
applyAssistantTurn(progress, event);
|
|
149
162
|
break;
|
|
150
163
|
}
|
|
151
164
|
}
|
|
@@ -251,7 +264,7 @@ function feedDemuxed(tail, line, gateCwd) {
|
|
|
251
264
|
* run in the role's own worktree, the default; `gate` — the review gate's runs in a leased pool
|
|
252
265
|
* slot, what the reviewing cell shows), or null when there is no log yet. The raw log is
|
|
253
266
|
* append-only while pi runs and every run starts with a `tumwater_run` marker
|
|
254
|
-
* (src/agent/
|
|
267
|
+
* (src/agent/agent-spawn.ts), so after seeding from the tail window once we only read and parse
|
|
255
268
|
* bytes appended since the last poll — observers that call this every second (TUI, GUI) stop
|
|
256
269
|
* rescanning up to TAIL_BYTES
|
|
257
270
|
* of JSON per role per poll. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { callBuffersOutput } from "../pi/command-shape.js";
|
|
1
2
|
import { readLiveProgress } from "./progress-data.js";
|
|
2
3
|
import { compactTokens } from "../text/format.js";
|
|
3
4
|
import { pad2 } from "../text/datetime.js";
|
|
@@ -83,8 +84,14 @@ export function inFlightDetail(head, p) {
|
|
|
83
84
|
else if (p.lastTool)
|
|
84
85
|
parts.push(p.lastTool);
|
|
85
86
|
// Silence under five minutes is normal (slow local-model prefills, long tool calls);
|
|
86
|
-
// only flag a stall once at least five minutes have passed without any pi output.
|
|
87
|
-
|
|
87
|
+
// only flag a stall once at least five minutes have passed without any pi output. While a
|
|
88
|
+
// tool call that buffers its output (a redirect or pipe — the run prompt's prescribed
|
|
89
|
+
// `> check.log` verify shape) is open, that flat threshold would cry wolf inside the call's
|
|
90
|
+
// own generous wall-clock stall window: the silence is the prescribed shape, not a hang,
|
|
91
|
+
// and a real stall past that window has already named itself as `tool call stalled: <tool>`
|
|
92
|
+
// above. Defer to the call so a slow suite stops being read as a hang (BUGS.md 2026-10-28).
|
|
93
|
+
const hasOpenBuffered = p.openToolCalls?.some((call) => callBuffersOutput(p.agent, call.command || call.label)) ?? false;
|
|
94
|
+
if (p.quietMs >= 300_000 && !hasOpenBuffered)
|
|
88
95
|
parts.push(`no agent output for ${duration(p.quietMs)}`);
|
|
89
96
|
return parts.join(" · ");
|
|
90
97
|
}
|
|
@@ -84,7 +84,7 @@ export function onErrorStreak(s) {
|
|
|
84
84
|
return (errorStreakAtWarn(s) ||
|
|
85
85
|
(s.lastResult === "quiet_killed" && (s.quietKillStreak ?? 0) >= QUIET_KILL_RESUME_LIMIT));
|
|
86
86
|
}
|
|
87
|
-
export function rolePhase(s, orchestratorRunning, root, budgetPaused = false, live, userPaused = false, merge, mainCheck, capPaused = false, quietHold, diskHeld = false, bootstrapHeld = false, maintenanceQuotaHold,
|
|
87
|
+
export function rolePhase(s, orchestratorRunning, root, budgetPaused = false, live, userPaused = false, merge, mainCheck, capPaused = false, quietHold, diskHeld = false, bootstrapHeld = false, maintenanceQuotaHold, pausedUntilMs) {
|
|
88
88
|
if (!orchestratorRunning)
|
|
89
89
|
return "stopped";
|
|
90
90
|
// Merge queue 4/5 — the marker-driven merge label: only a role whose change record was
|
|
@@ -114,16 +114,6 @@ export function rolePhase(s, orchestratorRunning, root, budgetPaused = false, li
|
|
|
114
114
|
// parked waiter visibly does not (BUGS.md 2026-09-24).
|
|
115
115
|
if (s.parkedSince) {
|
|
116
116
|
const parkedTime = duration(Date.now() - s.parkedSince);
|
|
117
|
-
// The parked waiter's provider key, named only while that key is the binding limit in
|
|
118
|
-
// the published permit snapshot (plans/provider-concurrency.md §8): the key is at its
|
|
119
|
-
// cap, so THIS waiter is queued on its provider, not on the team total — `awaiting
|
|
120
|
-
// omlx slot` tells the operator exactly which cap is full. A role parked on the total
|
|
121
|
-
// (no key, or an uncapped one) reads today's `awaiting slot` as before.
|
|
122
|
-
if (s.parkedOn !== undefined &&
|
|
123
|
-
permits !== undefined &&
|
|
124
|
-
isBindingLimit(permits, s.parkedOn)) {
|
|
125
|
-
return `waiting for ${s.parkedOn} ${parkedTime}`;
|
|
126
|
-
}
|
|
127
117
|
return `waiting to start ${parkedTime}`;
|
|
128
118
|
}
|
|
129
119
|
// The director bypasses maxConcurrent (`usesSlot` is false for it), so its in-flight run
|
|
@@ -322,9 +312,6 @@ export function roleRowCells(snap, root, s) {
|
|
|
322
312
|
snap.maintenanceQuota.held.includes(s.role)
|
|
323
313
|
? `waiting: maintenance allowance used ${snap.maintenanceQuota.used}/${snap.maintenanceQuota.allowance}`
|
|
324
314
|
: undefined,
|
|
325
|
-
// The permit pool's live state (plans/provider-concurrency.md §8): names the parked
|
|
326
|
-
// waiter's provider key in `awaiting <key> slot` when that key is at its cap.
|
|
327
|
-
snap.permits,
|
|
328
315
|
// A timed per-role pause's deadline — only when the role itself is individually paused
|
|
329
316
|
// (in `pausedRoles`) and not under a team-wide pause, matching the GUI's phaseDetail
|
|
330
317
|
// guard, so the team pause (whose countdown rides the header badge) never double-appends.
|
|
@@ -336,15 +323,6 @@ export function roleRowCells(snap, root, s) {
|
|
|
336
323
|
* gui-client-model.ts interpolates ROLE_RANK_RULES, so the two dashboards cannot disagree
|
|
337
324
|
* about which phases are in flight. */
|
|
338
325
|
export const ACTIVE_PHASE_PREFIXES = ["working", "reviewing", "merging"];
|
|
339
|
-
/** Whether `key` is the binding limit in the published permit snapshot: its cap is set and
|
|
340
|
-
* its in-use already meets or exceeds it, so the pool is granting nothing more on that key and
|
|
341
|
-
* a waiter carrying it is queued on the key, not on the team total. A key without a cap in
|
|
342
|
-
* the snapshot (uncapped) is never binding. One home for the rule, shared by status-model's
|
|
343
|
-
* `awaiting <key> slot` and the GUI chip's provider detail. */
|
|
344
|
-
function isBindingLimit(permits, key) {
|
|
345
|
-
const entry = permits.byKey[key];
|
|
346
|
-
return entry?.limit !== undefined && entry.inUse >= entry.limit;
|
|
347
|
-
}
|
|
348
326
|
/** Is this phase one of the PERMIT-HOLDER in-flight states — the rows an operator counts
|
|
349
327
|
* against maxConcurrent? The three labels come from rolePhase: `working …`, `reviewing …`,
|
|
350
328
|
* and the marker-driven `merge …` (merge queue 4/5) — an approved change waiting for the merge
|
|
@@ -375,7 +353,7 @@ export function isActivePhase(phase) {
|
|
|
375
353
|
export const ROLE_RANK_RULES = [
|
|
376
354
|
{ rank: -1, prefixes: ["director working"] },
|
|
377
355
|
{ rank: 0, active: true },
|
|
378
|
-
{ rank: 1, prefixes: ["approved", "waiting to start"
|
|
356
|
+
{ rank: 1, prefixes: ["approved", "waiting to start"] },
|
|
379
357
|
{ rank: 2, phases: ["failing", "main's check is failing"] },
|
|
380
358
|
{ rank: 3, phases: ["paused"] },
|
|
381
359
|
{ rank: 3, prefixes: ["paused", "waiting:"] },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { answeredQuestionSummaries } from "../backlog/question-answer.js";
|
|
1
|
+
import { answeredQuestionSummaries } from "../backlog/backlog-question-answer.js";
|
|
2
2
|
import { completionDate, donePlanEntries, DONE_DATE_RE, FIXED_DATE_RE, fixedBugEntries, openBugs, openQuestions, plannedPlans } from "../backlog/backlog.js";
|
|
3
3
|
import { backlogHolds, backlogInProgress, backlogMarks, refusedEntries, stalledEntries } from "../backlog/backlog-eligibility.js";
|
|
4
4
|
import { finishedTitle, strandedPlanEntriesFor } from "../backlog/backlog-structure.js";
|
|
@@ -15,11 +15,10 @@ import { scopeSnapshotToRoles, snapshot } from "../status/status-data.js";
|
|
|
15
15
|
import { bootstrapBadge, buildBadge, budgetBadge, diskBadge, mergeBadge, mainCheckBadge, mainCountsFragment, ciParityBadge, ciParityTone, permitsBadge, checksBadge, quietBadge, runWindowBadge, workspacesBadge } from "./badges.js";
|
|
16
16
|
import { isActivePhase, roleRowCells, onErrorStreak } from "./status-model.js";
|
|
17
17
|
import { yieldMultiplierFor } from "../scheduling/backoff.js";
|
|
18
|
-
import { staleClaimEntries } from "../scheduling/claim-entries.js";
|
|
19
18
|
import { teamAlerts } from "./team-alerts.js";
|
|
20
19
|
import { duplicatedBacklogHeadings, liveFinishedEntriesFor } from "../doctor/doctor-backlog-plans.js";
|
|
21
20
|
import { readPostReviews } from "../review/post-merge-review.js";
|
|
22
|
-
import {
|
|
21
|
+
import { getOrphanAlert, planDependencyAlert, staleOpenQuestions } from "./alert-feeds.js";
|
|
23
22
|
/** How many recent events the payload carries — enough for the dashboard's activity feed to
|
|
24
23
|
* still show a screenful of notable events (merges, failures, questions) after it filters
|
|
25
24
|
* out the routine ones (run starts, wakes, passing checks). */
|
|
@@ -61,9 +60,6 @@ function roleRows(snap, root) {
|
|
|
61
60
|
// the dashboard can mark a row whose model tumwater.json did not name. Same conditional
|
|
62
61
|
// spread as `model`, present on exactly the rows that carry the flag.
|
|
63
62
|
...(s.modelFromPi ? { modelFromPi: true } : {}),
|
|
64
|
-
// Model failure fallback (PLANS.md "Model failure fallback, part 2/2"): present only
|
|
65
|
-
// while the role's runs use another model, so the common row shape is unchanged.
|
|
66
|
-
...(s.fallback ? { fallback: s.fallback } : {}),
|
|
67
63
|
...(s.slot ? { slot: s.slot, slotPinned: s.slotPinned === true } : {}),
|
|
68
64
|
// Parallel work instances (plans/parallel-work-instances.md "Observability", part
|
|
69
65
|
// 6/7): the backlog entry this instance claimed, shown by the dashboards while it is
|
|
@@ -419,7 +415,7 @@ function alertFields(root, snap, questions, roles, now) {
|
|
|
419
415
|
return {
|
|
420
416
|
// What needs the operator (team-alerts.ts's teamAlerts): the dashboard's alert banners and
|
|
421
417
|
// the TUI's attention lines, phrased once.
|
|
422
|
-
alerts: teamAlerts(snap, questions, roles, now, refusedEntries(root), planDependencyAlert(root), stalledEntries(root), strandedPlanEntriesFor(root), duplicatedBacklogHeadings(root), validationGapProblems(root),
|
|
418
|
+
alerts: teamAlerts(snap, questions, roles, now, refusedEntries(root), planDependencyAlert(root), stalledEntries(root), strandedPlanEntriesFor(root), duplicatedBacklogHeadings(root), validationGapProblems(root), staleOpenQuestions(root, now), fixClaimProblems(root), liveFinishedEntriesFor(root), getOrphanAlert(root)),
|
|
423
419
|
};
|
|
424
420
|
}
|
|
425
421
|
/** The one team-state document both observer surfaces carry: `GET /api/status` (gui/gui-server.ts)
|
|
@@ -9,12 +9,11 @@ import { runProgress } from "./run-progress-model.js";
|
|
|
9
9
|
import { eventTypeLabel } from "../events/event-labels.js";
|
|
10
10
|
import { roleLabel } from "../roles/role-labels.js";
|
|
11
11
|
import { yieldMultiplierFor, yieldMultiplierSuffix } from "../scheduling/backoff.js";
|
|
12
|
-
import { staleClaimEntries } from "../scheduling/claim-entries.js";
|
|
13
12
|
import { phaseTone, resultTone } from "./tone.js";
|
|
14
13
|
import { teamAlerts } from "./team-alerts.js";
|
|
15
14
|
import { openQuestions } from "../backlog/backlog.js";
|
|
16
15
|
import { refusedEntries, stalledEntries } from "../backlog/backlog-eligibility.js";
|
|
17
|
-
import {
|
|
16
|
+
import { getOrphanAlert, planDependencyAlert, staleOpenQuestions } from "./alert-feeds.js";
|
|
18
17
|
import { strandedPlanEntriesFor } from "../backlog/backlog-structure.js";
|
|
19
18
|
import { duplicatedBacklogHeadings, fixClaimProblems, liveFinishedEntriesFor, validationGapProblems } from "../doctor/doctor-backlog-plans.js";
|
|
20
19
|
/** The status RENDER layer: time/token cell formatters and the width-aware table shared by
|
|
@@ -238,8 +237,8 @@ function statusHeaderLine(root, snap, now) {
|
|
|
238
237
|
// comfortable or unmeasured state, so an unaffected header stays byte-identical.
|
|
239
238
|
...badgeSpans(diskBadge(snap.disk), snap.disk?.held ? "yellow" : "blue"),
|
|
240
239
|
// The permit pool's live state (plans/provider-concurrency.md §8): the team line reads
|
|
241
|
-
// `
|
|
242
|
-
//
|
|
240
|
+
// `running 5 of 8` — the team total's in-use/cap — for any team with a published permit
|
|
241
|
+
// snapshot; empty until one has been published.
|
|
243
242
|
...badgeSpans(permitsBadge(snap.permits), "blue"),
|
|
244
243
|
// Checks running/waiting for a check permit; empty while the check pool is idle.
|
|
245
244
|
...badgeSpans(checksBadge(snap.checkPermits), snap.checkPermits && snap.checkPermits.waiting > 0 ? "yellow" : "blue"),
|
|
@@ -306,14 +305,11 @@ function statusRoleRows(root, snap, now, withMetrics, sharedModel) {
|
|
|
306
305
|
// today's.
|
|
307
306
|
const inner = roleModelLabel(s);
|
|
308
307
|
const modelSuffix = inner && !sharedModel ? ` (${inner})` : "";
|
|
309
|
-
// Model failure fallback, part 2/2: while the role runs off-model its name cell says so,
|
|
310
|
-
// so the operator can see which roles the fallback pair is carrying without reading logs.
|
|
311
|
-
const fallbackSuffix = s.fallback ? " (over-cap model)" : "";
|
|
312
308
|
// User-defined roles (snapshot's `custom` flag) get an asterisk beside their name — the
|
|
313
309
|
// dashboards' at-a-glance marker. The name column (index 0) is not in FLEXIBLE_COLUMNS;
|
|
314
310
|
// its width derives from row content and it shrinks only after those,
|
|
315
311
|
// so the extra character widens it.
|
|
316
|
-
const name = (s.custom ? `${roleLabel(s.role)}*` : roleLabel(s.role)) + modelSuffix
|
|
312
|
+
const name = (s.custom ? `${roleLabel(s.role)}*` : roleLabel(s.role)) + modelSuffix;
|
|
317
313
|
const state = stateCell(root, s, phase, live);
|
|
318
314
|
const tone = phaseTone(phase);
|
|
319
315
|
// The whole state cell carries the phase's tone: a work item leads the cell (so it
|
|
@@ -509,7 +505,7 @@ export function renderStatus(root, snap, maxWidth, scoped = false) {
|
|
|
509
505
|
// Composed here, NOT inside renderStatusSpans: the TUI re-slices that function's shared
|
|
510
506
|
// `lines` (lines[0], its own attention block, then lines.slice(1)), so a banner embedded
|
|
511
507
|
// there would render twice in the TUI.
|
|
512
|
-
const alerts = teamAlerts(snap, openQuestions(root), roles, Date.now(), refusedEntries(root), planDependencyAlert(root), stalledEntries(root), strandedPlanEntriesFor(root), duplicatedBacklogHeadings(root), validationGapProblems(root),
|
|
508
|
+
const alerts = teamAlerts(snap, openQuestions(root), roles, Date.now(), refusedEntries(root), planDependencyAlert(root), stalledEntries(root), strandedPlanEntriesFor(root), duplicatedBacklogHeadings(root), validationGapProblems(root), staleOpenQuestions(root), fixClaimProblems(root), liveFinishedEntriesFor(root), getOrphanAlert(root)).filter((a) => a.key !== "stopped");
|
|
513
509
|
const banner = alertLines(alerts, maxWidth ?? Number.MAX_SAFE_INTEGER, alerts.length).map(plainText);
|
|
514
510
|
const text = lines.map(plainText);
|
|
515
511
|
return [...text.slice(0, 2), ...banner, ...text.slice(2)].join("\n");
|
|
@@ -7,7 +7,7 @@ import { entryListDetail, firstAndMore, openBacklogAction } from "./team-alert-s
|
|
|
7
7
|
import { entryKindLabel } from "../backlog/backlog-eligibility.js";
|
|
8
8
|
import { missingSymbolNames } from "../verdict/fix-claim.js";
|
|
9
9
|
import { joinWithMore } from "../text/text.js";
|
|
10
|
-
import { plural } from "../text/phrases.js";
|
|
10
|
+
import { agree, plural } from "../text/phrases.js";
|
|
11
11
|
import { liveFinishedRowList } from "../backlog/backlog-archive.js";
|
|
12
12
|
function pushQuestionsAlert(out, questions) {
|
|
13
13
|
if (questions.length) {
|
|
@@ -112,7 +112,7 @@ function pushStrandedPlansAlert(out, strandedPlans) {
|
|
|
112
112
|
out.push({
|
|
113
113
|
key: "stranded-plans",
|
|
114
114
|
tone: "amber",
|
|
115
|
-
title: `${strandedPlans.length
|
|
115
|
+
title: `${agree(strandedPlans.length, "A plan sits", "Plans sit")} under the wrong backlog section`,
|
|
116
116
|
detail: `${first.file ?? "PLANS.md"} has a plan stranded under ## ${first.section}: "${first.title}"${more} — move it under ## ${first.section === "Done" ? "Planned" : "Done"}${first.file === undefined || first.file === "PLANS.md" ? "" : " in PLANS.md"} (the clean role repairs these)`,
|
|
117
117
|
// The fix is editing a backlog file, and a PLANS-DONE.md entry has no PLANS.md drawer index.
|
|
118
118
|
actions: [],
|
|
@@ -140,10 +140,10 @@ function pushBacklogHeadingsAlert(out, duplicatedHeadings) {
|
|
|
140
140
|
}
|
|
141
141
|
// Finished history still in the live backlog files (the doctor's `backlog archive` rule,
|
|
142
142
|
// checkLiveFinished, src/doctor/doctor-backlog-plans.ts, via the shared liveFinishedEntriesFor)
|
|
143
|
-
// is
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
143
|
+
// is a backlog-hygiene rule the doctor warned but no always-on alert surfaced — stranded
|
|
144
|
+
// plans, duplicated headings, validation gaps, plan dependencies and questions each have one,
|
|
145
|
+
// so an operator watching `tumwater status` or the dashboard never sees the growing finished
|
|
146
|
+
// sections until they run `tumwater doctor`. It asks (amber) the way
|
|
147
147
|
// the duplicated-heading alert does, naming each live file, its finished section, its entry
|
|
148
148
|
// count, and its size in KB; the fix is the `tumwater archive` command, so there are no actions.
|
|
149
149
|
function pushBacklogArchiveAlert(out, liveFinished) {
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
/** The status/capacity and role-phase team alerts, in emission order: a spent budget
|
|
2
|
-
* (chain/fallback/budget),
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
import {
|
|
6
|
-
import { openRoleActions, seeFailures, listRoles, listRoleIds, stuckDetail, localizeInstants, } from "./team-alert-shared.js";
|
|
2
|
+
* (chain/fallback/budget), disk, install, and main-red holds, roles failing run after run or
|
|
3
|
+
* stuck. One of the per-domain builders split out of src/ui/team-alerts.ts; the shared types
|
|
4
|
+
* and wording helpers live in team-alert-shared.ts. */
|
|
5
|
+
import { openRoleActions, seeFailures, listRoles, listRoleIds, stuckDetail, } from "./team-alert-shared.js";
|
|
7
6
|
import { chainFragments } from "./badges.js";
|
|
8
7
|
import { fallbackTierEntries } from "../budget/budget.js";
|
|
9
8
|
import { fallbackDisplayPair } from "../config/config-views.js";
|
|
10
9
|
import { agree, plural } from "../text/phrases.js";
|
|
11
|
-
import { BRIEF_TEXT_MAX_CHARS } from "../brief/brief.js";
|
|
12
10
|
import { formatGB, shortSha, usd, usdCap } from "../text/format.js";
|
|
13
11
|
import { joinWithMore, squash } from "../text/text.js";
|
|
14
12
|
import { roleLabel } from "../roles/role-labels.js";
|
|
15
13
|
import { STALL_RE } from "./run-progress-model.js";
|
|
16
|
-
import { ROLE_NOTES_MAX_BYTES } from "../agent/role-notes-write.js";
|
|
17
|
-
import { PRINCIPLES_MAX_CHARS } from "../prompt/principles.js";
|
|
18
14
|
function pushChainAlert(out, snap) {
|
|
19
15
|
const onFallback = chainFragments(snap.chain);
|
|
20
16
|
if (onFallback) {
|
|
@@ -60,23 +56,6 @@ function pushSpentBudgetAlert(out, snap) {
|
|
|
60
56
|
});
|
|
61
57
|
}
|
|
62
58
|
}
|
|
63
|
-
// A role over its maxDailyCostUsdPerRole cap (status-model.ts "waiting: daily cap spent") is
|
|
64
|
-
// a per-role hold — one role sits idle while the rest of the team keeps working — so the team
|
|
65
|
-
// banner names it the way the budget, disk, and quiet banners name theirs, instead of leaving
|
|
66
|
-
// an operator to read every row. An amber ask (it needs a raise to resume); clears on its own
|
|
67
|
-
// at local midnight when the spend window rolls over.
|
|
68
|
-
function pushRoleCapAlert(out, snap, roles) {
|
|
69
|
-
const capPausedRoles = roles.filter((l) => snap.capPaused.includes(l.role));
|
|
70
|
-
if (snap.capPaused.length > 0) {
|
|
71
|
-
out.push({
|
|
72
|
-
key: "role-cap",
|
|
73
|
-
tone: "amber",
|
|
74
|
-
title: `${listRoleIds(snap.capPaused)} ${agree(snap.capPaused.length, "is over its daily cap", "are over their daily caps")}`,
|
|
75
|
-
detail: `The daily cap (maxDailyCostUsdPerRole) holds ${listRoleIds(snap.capPaused)}: no new runs until local midnight. Raise the cap to resume ${agree(snap.capPaused.length, "it", "them")}.`,
|
|
76
|
-
actions: openRoleActions(capPausedRoles),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
59
|
// The disk floor holding new work (plans/disk-floor.md, part 4/4): the header badge and the
|
|
81
60
|
// role rows say what is happening, but the banner names the threshold and the way out, so an
|
|
82
61
|
// operator hitting a full volume does not have to read events. An amber ask — the team is
|
|
@@ -109,86 +88,6 @@ function pushInstallDriftAlert(out, snap) {
|
|
|
109
88
|
});
|
|
110
89
|
}
|
|
111
90
|
}
|
|
112
|
-
// No deterministic build check is declared (detectBuildCheck returned null): the review
|
|
113
|
-
// gate's build pre-check, the red-main baseline and redeploy's green check have nothing to
|
|
114
|
-
// run, and the header badge renders nothing (mainCheckBadge needs a build_check event).
|
|
115
|
-
// The same state the doctor `check` row warns on, raised as a standing alert on the always-on
|
|
116
|
-
// surfaces. Independent of team state (running or stopped), like the install alert beside it.
|
|
117
|
-
function pushNoCheckAlert(out, snap) {
|
|
118
|
-
if (snap.noDeclaredCheck) {
|
|
119
|
-
out.push({
|
|
120
|
-
key: "no-check",
|
|
121
|
-
tone: "amber",
|
|
122
|
-
title: "No project check is declared — the safety gates are off",
|
|
123
|
-
detail: "The review gate's build pre-check, the red-main baseline and redeploy's green check have nothing to run. The plan role will plan this project's check and the feature role declare it — a test/typecheck/build npm script, or check.command. To skip the wait, declare it yourself; a project with nothing to verify yet stays as it is.",
|
|
124
|
-
actions: [],
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
// A brief candidate exists but none resolves to a usable prompt block (briefBlockMissing):
|
|
129
|
-
// every run reaches its model with no project prompt, so the team burns tokens producing
|
|
130
|
-
// work unrelated to the project. The same marker-less/unreadable/empty-block state the doctor
|
|
131
|
-
// `brief` row warns on, raised as a standing alert on the always-on surfaces (the bare
|
|
132
|
-
// pre-init repo stays silent). Independent of team state, like the no-check alert beside it.
|
|
133
|
-
function pushNoBriefAlert(out, snap) {
|
|
134
|
-
if (snap.noBrief) {
|
|
135
|
-
out.push({
|
|
136
|
-
key: "no-brief",
|
|
137
|
-
tone: "amber",
|
|
138
|
-
title: "The project brief's prompt block is missing or empty — every role runs without its project prompt",
|
|
139
|
-
detail: "A brief file exists but carries no usable tumwater:prompt block (no markers, or no text between them), so each run's prompt reaches the model with no project brief. Restore the block in the file the doctor `brief` row names, or re-run `tumwater init`.",
|
|
140
|
-
actions: [],
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// A brief block past the read cap (briefBlockOverCap): every run's prompt silently carries
|
|
145
|
-
// only the head plus a truncation marker, so each role plans and codes against a cut brief.
|
|
146
|
-
// The same over-cap state the doctor `brief` row warns on, raised as a standing alert on the
|
|
147
|
-
// always-on surfaces; mutually exclusive with `no-brief` (an over-cap block is non-empty).
|
|
148
|
-
function pushBriefTruncatedAlert(out, snap) {
|
|
149
|
-
if (snap.briefTruncated) {
|
|
150
|
-
out.push({
|
|
151
|
-
key: "brief-truncated",
|
|
152
|
-
tone: "amber",
|
|
153
|
-
title: "The project brief is over its prompt cap — every role runs on a truncated brief",
|
|
154
|
-
detail: "The brief block exceeds the read cap, so each run's prompt carries only its head plus a truncation marker. Shorten the block to at most " + BRIEF_TEXT_MAX_CHARS + " characters in the file the doctor `brief` row names (a hand edit, or the dashboard's Brief card), then save it.",
|
|
155
|
-
actions: [],
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
// A role note past the read bound (collectRoleNotesOverCap's predicate, the doctor `role
|
|
160
|
-
// notes` row's own): `readRoleNote` truncates that role's note on read, so the role's next
|
|
161
|
-
// prompt carries only the note's head plus a truncation marker while the operator sees the
|
|
162
|
-
// file whole — a silently degraded next run on a green-looking status. The standing twin of
|
|
163
|
-
// the doctor `role notes` row's warn, raised as an amber alert on the always-on surfaces.
|
|
164
|
-
// Independent of team state, like the no-brief alert beside it.
|
|
165
|
-
function pushRoleNotesTruncatedAlert(out, snap) {
|
|
166
|
-
const notesOver = snap.roleNotesOverCap ?? [];
|
|
167
|
-
if (notesOver.length > 0) {
|
|
168
|
-
out.push({
|
|
169
|
-
key: "role-notes-truncated",
|
|
170
|
-
tone: "amber",
|
|
171
|
-
title: `${plural(notesOver.length, "role note is", "role notes are")} over the read bound — those roles run on truncated notes`,
|
|
172
|
-
detail: `${joinWithMore(notesOver.map((o) => `${o.role} (${o.chars})`), 3)} exceed the ${ROLE_NOTES_MAX_BYTES}-character read bound, so each of those roles' prompt carries only the note's head plus a truncation marker. Rewrite them under the cap (the run's \`tumwater-notes write\` shim, \`tumwater role <id> --notes-file <path>\`, the dashboard drawer, or the TUI notes editor).`,
|
|
173
|
-
actions: [],
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
// PRINCIPLES.md is over its prompt cap (principlesBlockOverCap): every role's <principles>
|
|
178
|
-
// block carries only its head plus a truncation marker, so the team runs on truncated design
|
|
179
|
-
// guidance. The same over-cap state the doctor `principles` row warns on, raised as a standing
|
|
180
|
-
// alert on the always-on surfaces. Independent of team state, like the no-brief alert beside it.
|
|
181
|
-
function pushPrinciplesTruncatedAlert(out, snap) {
|
|
182
|
-
if (snap.principlesTruncated) {
|
|
183
|
-
out.push({
|
|
184
|
-
key: "principles-truncated",
|
|
185
|
-
tone: "amber",
|
|
186
|
-
title: "PRINCIPLES.md is over its prompt cap — every role runs on truncated design guidance",
|
|
187
|
-
detail: "The file exceeds the " + PRINCIPLES_MAX_CHARS + "-character prompt cap, so every run's <principles> block carries only its head plus a truncation marker and only the leading bullets survive. Keep PRINCIPLES.md to its list bullets and move long-form rationale into docs/, which is not injected.",
|
|
188
|
-
actions: [],
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
91
|
function pushMainRedAlert(out, snap, roles) {
|
|
193
92
|
const red = roles.filter((l) => l.phase === "main's check is failing");
|
|
194
93
|
if (red.length) {
|
|
@@ -246,81 +145,21 @@ function pushStuckRoleAlert(out, roles) {
|
|
|
246
145
|
});
|
|
247
146
|
}
|
|
248
147
|
}
|
|
249
|
-
function pushBuildAlert(out, snap) {
|
|
250
|
-
if (snap.build?.stale) {
|
|
251
|
-
const behind = plural(snap.build.aheadCommits ?? 0, "commit");
|
|
252
|
-
// The cooldown scopes the deferral to the redeploy, not the team (PLANS.md 2026-10-23): a
|
|
253
|
-
// restart held by the cooldown leaves the roles running on the stale build, so its alert
|
|
254
|
-
// reads blue "work continues" with the deadline, while a genuinely blocked restart (red main,
|
|
255
|
-
// a failed build, a boot refusal) stays the amber "restart blocked", and the momentary drain
|
|
256
|
-
// of an in-flight restart — the one stale state that actually holds new runs and merges —
|
|
257
|
-
// keeps the plain wording.
|
|
258
|
-
const deferred = snap.build.restartBlocked?.startsWith("cooldown until ");
|
|
259
|
-
out.push(deferred
|
|
260
|
-
? {
|
|
261
|
-
key: "build",
|
|
262
|
-
tone: "blue",
|
|
263
|
-
title: `Update available (${behind} behind) — restarts after cooldown; work continues`,
|
|
264
|
-
detail: `Work continues on the running build; the restart lands when the cooldown lapses or you force it (${localizeInstants(snap.build.restartBlocked ?? "")}).`,
|
|
265
|
-
actions: [],
|
|
266
|
-
}
|
|
267
|
-
: snap.build.restartBlocked
|
|
268
|
-
? {
|
|
269
|
-
key: "build",
|
|
270
|
-
tone: "amber",
|
|
271
|
-
title: `Update available (${behind} behind) — restart blocked`,
|
|
272
|
-
detail: localizeInstants(snap.build.restartBlocked),
|
|
273
|
-
actions: [],
|
|
274
|
-
}
|
|
275
|
-
: {
|
|
276
|
-
key: "build",
|
|
277
|
-
tone: "blue",
|
|
278
|
-
title: `Update available (${behind} behind)${snap.build.restartPending ? " — restarts when idle" : ""}`,
|
|
279
|
-
detail: snap.build.restartPending ? "The team restarts onto it once running work finishes." : "Restart tumwater start to pick it up.",
|
|
280
|
-
actions: [],
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
// A work-instance claim the operator should act on without running `tumwater doctor`
|
|
285
|
-
// (src/scheduling/claim-entries.ts, the doctor's `parallel instances` row): a stopped
|
|
286
|
-
// instance holding an entry that left its role's section, or idle past the scheduler's
|
|
287
|
-
// release window, is an amber ask. One detail line per claim, worded like the doctor row so
|
|
288
|
-
// the two cannot drift; actions open the first three claimed roles. Nothing when empty.
|
|
289
|
-
function pushStaleClaimsAlert(out, staleClaims) {
|
|
290
|
-
if (staleClaims.length > 0) {
|
|
291
|
-
out.push({
|
|
292
|
-
key: "stale-claims",
|
|
293
|
-
tone: "amber",
|
|
294
|
-
title: `${plural(staleClaims.length, "work-instance claim is", "work-instance claims are")} stale`,
|
|
295
|
-
detail: staleClaims.map(staleClaimDetail).join("; "),
|
|
296
|
-
actions: openRoleActions(staleClaims.map((c) => ({ role: c.id }))),
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
148
|
/** The status/capacity and role-phase alerts, in emission order: a spent budget
|
|
301
|
-
* (chain/fallback/budget),
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* alerts at the position this run used to occupy, so the emitted key order is
|
|
149
|
+
* (chain/fallback/budget), disk, install, and main-red holds, roles failing run after run or
|
|
150
|
+
* stuck. Extracted from teamAlerts (part 1/5 of the teamAlerts split) so the shared module
|
|
151
|
+
* stays small; the input names exactly the fields this run reads. The caller pushes the
|
|
152
|
+
* returned alerts at the position this run used to occupy, so the emitted key order is
|
|
306
153
|
* unchanged. */
|
|
307
154
|
export function buildStatusAlerts(input) {
|
|
308
|
-
const { snap, roles
|
|
155
|
+
const { snap, roles } = input;
|
|
309
156
|
const out = [];
|
|
310
157
|
pushChainAlert(out, snap);
|
|
311
158
|
pushSpentBudgetAlert(out, snap);
|
|
312
|
-
pushRoleCapAlert(out, snap, roles);
|
|
313
159
|
pushDiskAlert(out, snap);
|
|
314
160
|
pushInstallDriftAlert(out, snap);
|
|
315
|
-
pushNoCheckAlert(out, snap);
|
|
316
|
-
pushNoBriefAlert(out, snap);
|
|
317
|
-
pushBriefTruncatedAlert(out, snap);
|
|
318
|
-
pushRoleNotesTruncatedAlert(out, snap);
|
|
319
|
-
pushPrinciplesTruncatedAlert(out, snap);
|
|
320
161
|
pushMainRedAlert(out, snap, roles);
|
|
321
162
|
pushFailingStreakAlert(out, roles);
|
|
322
163
|
pushStuckRoleAlert(out, roles);
|
|
323
|
-
pushBuildAlert(out, snap);
|
|
324
|
-
pushStaleClaimsAlert(out, staleClaims);
|
|
325
164
|
return out;
|
|
326
165
|
}
|