tumwater 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -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";
|
|
@@ -56,13 +57,14 @@ function progressOfRun(s, p) {
|
|
|
56
57
|
return { ...p, quietMs: Math.min(p.quietMs, elapsed) };
|
|
57
58
|
}
|
|
58
59
|
/** The stall phrases a phase label can carry — inFlightDetail appends "tool call stalled:
|
|
59
|
-
* <tool>" and "no agent output for <duration>" to the head — as
|
|
60
|
+
* <tool>", "no model reply for <duration>", and "no agent output for <duration>" to the head — as
|
|
61
|
+
* a regex SOURCE string plus the
|
|
60
62
|
* compiled matcher, single-homed beside the code that renders those phrases. Every consumer
|
|
61
63
|
* that must recognize a stall inside a rendered phase label goes through these two, so the
|
|
62
64
|
* server-side alert (team-alert-status.ts) and the dashboard's browser twin (gui-client-model.ts,
|
|
63
65
|
* interpolating the source into its String.raw script) cannot drift from the wording the cell
|
|
64
66
|
* actually renders. */
|
|
65
|
-
export const STALL_SOURCE = String.raw `tool call stalled[^·]*|no agent output for [^·]*`;
|
|
67
|
+
export const STALL_SOURCE = String.raw `tool call stalled[^·]*|no model reply for [^·]*|no agent output for [^·]*`;
|
|
66
68
|
/** STALL_SOURCE compiled — the server-side matcher (team-alert-status.ts's stuck alert). */
|
|
67
69
|
export const STALL_RE = new RegExp(STALL_SOURCE);
|
|
68
70
|
/** The shared parts assembly for an in-flight state cell: `head` (the phase label with its
|
|
@@ -83,9 +85,23 @@ export function inFlightDetail(head, p) {
|
|
|
83
85
|
else if (p.lastTool)
|
|
84
86
|
parts.push(p.lastTool);
|
|
85
87
|
// 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
|
-
|
|
88
|
+
// only flag a stall once at least five minutes have passed without any pi output. While a
|
|
89
|
+
// tool call that buffers its output (a redirect or pipe — the run prompt's prescribed
|
|
90
|
+
// `> check.log` verify shape) is open, that flat threshold would cry wolf inside the call's
|
|
91
|
+
// own generous wall-clock stall window: the silence is the prescribed shape, not a hang,
|
|
92
|
+
// and a real stall past that window has already named itself as `tool call stalled: <tool>`
|
|
93
|
+
// above. Defer to the call so a slow suite stops being read as a hang (BUGS.md 2026-10-28).
|
|
94
|
+
const hasOpenBuffered = p.openToolCalls?.some((call) => callBuffersOutput(p.agent, call.command || call.label)) ?? false;
|
|
95
|
+
// A model request left unanswered past the stall threshold names the cause instead: the
|
|
96
|
+
// provider has not replied (BUGS.md 2026-10-29). pi's retries write to the log, so the bare
|
|
97
|
+
// silence measure restarts with each attempt, while this one spans the whole wait.
|
|
98
|
+
if (p.unansweredReply) {
|
|
99
|
+
const { waitedMs, attempts } = p.unansweredReply;
|
|
100
|
+
parts.push(`no model reply for ${duration(waitedMs)}${attempts > 1 ? ` (${attempts} attempts)` : ""}`);
|
|
101
|
+
}
|
|
102
|
+
else if (p.quietMs >= 5 * 60_000 && !hasOpenBuffered) {
|
|
88
103
|
parts.push(`no agent output for ${duration(p.quietMs)}`);
|
|
104
|
+
}
|
|
89
105
|
return parts.join(" · ");
|
|
90
106
|
}
|
|
91
107
|
/** Which pi run kind (progress-data.ts's ProgressRunKind) a role's in-flight cells should read:
|
|
@@ -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,9 +1,8 @@
|
|
|
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
|
-
import { backlogHolds, backlogInProgress, backlogMarks, refusedEntries
|
|
4
|
-
import { finishedTitle
|
|
3
|
+
import { backlogHolds, backlogInProgress, backlogMarks, refusedEntries } from "../backlog/backlog-eligibility.js";
|
|
4
|
+
import { finishedTitle } from "../backlog/backlog-structure.js";
|
|
5
5
|
import { groupBlockedSeries, partsLabel } from "../backlog/backlog-index-data.js";
|
|
6
|
-
import { fixClaimProblems, validationGapProblems } from "../doctor/doctor-backlog-plans.js";
|
|
7
6
|
import { readEvents } from "../events/event-read.js";
|
|
8
7
|
import { eventFeedItem, formatEvent } from "../events/event-format.js";
|
|
9
8
|
import { projectName } from "../project-name.js";
|
|
@@ -15,11 +14,9 @@ import { scopeSnapshotToRoles, snapshot } from "../status/status-data.js";
|
|
|
15
14
|
import { bootstrapBadge, buildBadge, budgetBadge, diskBadge, mergeBadge, mainCheckBadge, mainCountsFragment, ciParityBadge, ciParityTone, permitsBadge, checksBadge, quietBadge, runWindowBadge, workspacesBadge } from "./badges.js";
|
|
16
15
|
import { isActivePhase, roleRowCells, onErrorStreak } from "./status-model.js";
|
|
17
16
|
import { yieldMultiplierFor } from "../scheduling/backoff.js";
|
|
18
|
-
import { staleClaimEntries } from "../scheduling/claim-entries.js";
|
|
19
17
|
import { teamAlerts } from "./team-alerts.js";
|
|
20
|
-
import { duplicatedBacklogHeadings, liveFinishedEntriesFor } from "../doctor/doctor-backlog-plans.js";
|
|
21
18
|
import { readPostReviews } from "../review/post-merge-review.js";
|
|
22
|
-
import {
|
|
19
|
+
import { getOrphanAlert } from "./alert-feeds.js";
|
|
23
20
|
/** How many recent events the payload carries — enough for the dashboard's activity feed to
|
|
24
21
|
* still show a screenful of notable events (merges, failures, questions) after it filters
|
|
25
22
|
* out the routine ones (run starts, wakes, passing checks). */
|
|
@@ -61,9 +58,6 @@ function roleRows(snap, root) {
|
|
|
61
58
|
// the dashboard can mark a row whose model tumwater.json did not name. Same conditional
|
|
62
59
|
// spread as `model`, present on exactly the rows that carry the flag.
|
|
63
60
|
...(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
61
|
...(s.slot ? { slot: s.slot, slotPinned: s.slotPinned === true } : {}),
|
|
68
62
|
// Parallel work instances (plans/parallel-work-instances.md "Observability", part
|
|
69
63
|
// 6/7): the backlog entry this instance claimed, shown by the dashboards while it is
|
|
@@ -173,7 +167,7 @@ function headerFields(root, snap, now) {
|
|
|
173
167
|
// The header's daily-cost-budget badge preformatted through badges.ts's budgetBadge —
|
|
174
168
|
// the same string the TUI/status table renders (n/a for an all-free team; `· no cap`
|
|
175
169
|
// when disabled). Sent display-ready like buildBadge so the page cannot re-derive it.
|
|
176
|
-
budgetBadge: budgetBadge(snap.budget, snap.
|
|
170
|
+
budgetBadge: budgetBadge(snap.budget, snap.chain),
|
|
177
171
|
// The disk floor (plans/disk-floor.md, part 4/4): the raw block is machine-readable for
|
|
178
172
|
// `status --json`, and the header badge is preformatted through badges.ts's diskBadge —
|
|
179
173
|
// the same string the TUI/status header renders. Omitted (not null) when no orchestrator
|
|
@@ -294,7 +288,7 @@ function inboxFields(snap) {
|
|
|
294
288
|
};
|
|
295
289
|
}
|
|
296
290
|
/** Budget and pause/scheduling state (status-payload.ts's field-group split): the daily
|
|
297
|
-
* budget,
|
|
291
|
+
* budget, the fallback chain, operator/per-role pauses, the
|
|
298
292
|
* cap/maintenance holds, scheduled wakes, quiet hours, and the quiet badge. */
|
|
299
293
|
function budgetAndPauseFields(snap) {
|
|
300
294
|
return {
|
|
@@ -302,19 +296,6 @@ function budgetAndPauseFields(snap) {
|
|
|
302
296
|
// reported) — machine-readable for `status --json`; the display-ready header badge is
|
|
303
297
|
// the preformatted `budgetBadge`.
|
|
304
298
|
budget: snap.budget,
|
|
305
|
-
// The subscription usage-window block (part 6d/7), absent on a pi-only or API-claude
|
|
306
|
-
// team — the same raw shape `status --json` carries, which the GUI's spend card reads
|
|
307
|
-
// to render the window (the preformatted budgetBadge above already folds it into the
|
|
308
|
-
// header for the TUI/status). Raw data, like budget.
|
|
309
|
-
...(snap.claudeWindow ? { claudeWindow: snap.claudeWindow } : {}),
|
|
310
|
-
// The per-backend window blocks (part 3a/9): one entry per subscription backend in use,
|
|
311
|
-
// machine-readable for `status --json` beside claudeWindow. As of part 3b/9 the preformatted
|
|
312
|
-
// budgetBadge above folds the per-backend blocks — the most-constrained first — into the
|
|
313
|
-
// header, naming each backend's own window.
|
|
314
|
-
...(snap.subscriptionWindows && snap.subscriptionWindows.length > 0 ? { subscriptionWindows: snap.subscriptionWindows } : {}),
|
|
315
|
-
// The team-global free-ness answer (part 3a/9); budgetBadge reads it to decide whether the
|
|
316
|
-
// window lines stand alone (subscription-only) beside the daily spend.
|
|
317
|
-
subscriptionOnly: snap.subscriptionOnly,
|
|
318
299
|
// Each tier's fallback step (fallback chain part 7c/7), raw for the GUI side card.
|
|
319
300
|
...(snap.chain ? { chain: snap.chain } : {}),
|
|
320
301
|
// Operator pause (`tumwater pause` marker present): every idle role's phase reads
|
|
@@ -419,7 +400,7 @@ function alertFields(root, snap, questions, roles, now) {
|
|
|
419
400
|
return {
|
|
420
401
|
// What needs the operator (team-alerts.ts's teamAlerts): the dashboard's alert banners and
|
|
421
402
|
// the TUI's attention lines, phrased once.
|
|
422
|
-
alerts: teamAlerts(snap, questions, roles, now, refusedEntries(root),
|
|
403
|
+
alerts: teamAlerts(snap, questions, roles, now, refusedEntries(root), getOrphanAlert(root)),
|
|
423
404
|
};
|
|
424
405
|
}
|
|
425
406
|
/** The one team-state document both observer surfaces carry: `GET /api/status` (gui/gui-server.ts)
|
|
@@ -446,12 +427,12 @@ export function statusPayload(root, now = Date.now(), scope = null, modelsPath =
|
|
|
446
427
|
// Why each plan/bug is held (src/backlog/backlog-eligibility.ts): the same hold decision
|
|
447
428
|
// the prompt's backlog index and `tumwater backlog` render, projected to the mark each
|
|
448
429
|
// surface appends. Read once here, consumed as the parallel planHolds/bugHolds arrays below.
|
|
449
|
-
|
|
430
|
+
// Which blocked plans fold into a series row (computed before marks so credit, the same
|
|
431
|
+
// claims walk feeding both, is done once per poll — see backlogMarks' optional working arg).
|
|
432
|
+
const working = backlogInProgress(root);
|
|
433
|
+
const marks = backlogMarks(root, working);
|
|
450
434
|
const holds = backlogHolds(root);
|
|
451
435
|
const plans = plannedPlans(root);
|
|
452
|
-
// Which blocked plans fold into a series row: the grouping `plan --list` and the prompt's
|
|
453
|
-
// backlog index use, as a slot per plan (null for a plan that stays an ordinary row).
|
|
454
|
-
const working = backlogInProgress(root);
|
|
455
436
|
const planSeries = planSeriesSlots(plans, holds, working);
|
|
456
437
|
const roles = roleRows(snap, root);
|
|
457
438
|
return {
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { dailyCost } from "../budget/budget.js";
|
|
2
2
|
import { clipToWidth, displayWidth } from "../text/text-width.js";
|
|
3
3
|
import { compactTokens, contextShareLabel, usd } from "../text/format.js";
|
|
4
|
-
import { formatTime, humanAgo, nextRunPhrase, pad2, secondsUntil } from "../text/datetime.js";
|
|
4
|
+
import { DAY_MS, formatTime, humanAgo, nextRunPhrase, pad2, secondsUntil } from "../text/datetime.js";
|
|
5
5
|
import { projectName } from "../project-name.js";
|
|
6
|
-
import { bootstrapBadge, buildBadge, budgetBadge,
|
|
6
|
+
import { bootstrapBadge, buildBadge, budgetBadge, diskBadge, mergeBadge, mainCheckBadge, mainCheckVerdict, ciParityBadge, ciParityTone, pauseBadge, roleWakeMarker, permitsBadge, checksBadge, quietBadge, runWindowBadge, wakeBadge, workspacesBadge } from "./badges.js";
|
|
7
7
|
import { idleClaimText, isActivePhase, roleRowCells, onErrorStreak, sortRolesByState } from "./status-model.js";
|
|
8
8
|
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
|
-
import { refusedEntries
|
|
17
|
-
import {
|
|
18
|
-
import { strandedPlanEntriesFor } from "../backlog/backlog-structure.js";
|
|
19
|
-
import { duplicatedBacklogHeadings, fixClaimProblems, liveFinishedEntriesFor, validationGapProblems } from "../doctor/doctor-backlog-plans.js";
|
|
15
|
+
import { refusedEntries } from "../backlog/backlog-eligibility.js";
|
|
16
|
+
import { getOrphanAlert } from "./alert-feeds.js";
|
|
20
17
|
/** The status RENDER layer: time/token cell formatters and the width-aware table shared by
|
|
21
18
|
* `tumwater status` and the TUI. The labels, badges, and metrics it draws come from the shared
|
|
22
19
|
* display model (status-model.ts); this module decides column widths and layout. Depends on
|
|
@@ -37,7 +34,7 @@ export function lastRunCell(ts) {
|
|
|
37
34
|
return "-";
|
|
38
35
|
const d = new Date(ts);
|
|
39
36
|
let s = formatTime(d);
|
|
40
|
-
if (Date.now() - ts >
|
|
37
|
+
if (Date.now() - ts > DAY_MS)
|
|
41
38
|
s = `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${s}`;
|
|
42
39
|
return `${s} · ${ago(ts)}`;
|
|
43
40
|
}
|
|
@@ -174,13 +171,8 @@ function badgeSpans(badge, tone) {
|
|
|
174
171
|
const sep = /^(?: · |, )/.exec(badge)?.[0] ?? "";
|
|
175
172
|
return [{ text: sep }, { text: badge.slice(sep.length), ...(tone ? { tone } : {}) }];
|
|
176
173
|
}
|
|
177
|
-
/** The daily budget badge's tone: yellow from 85% of the cap, red once it is spent
|
|
178
|
-
|
|
179
|
-
* the seams) wins when it stands beside the daily spend. */
|
|
180
|
-
function budgetTone(budget, windows) {
|
|
181
|
-
const window = subscriptionWindowTone(windows);
|
|
182
|
-
if (window !== undefined)
|
|
183
|
-
return window;
|
|
174
|
+
/** The daily budget badge's tone: yellow from 85% of the cap, red once it is spent. */
|
|
175
|
+
function budgetTone(budget) {
|
|
184
176
|
if (budget.free || !(budget.capUsd > 0))
|
|
185
177
|
return undefined;
|
|
186
178
|
const share = budget.spentUsd / budget.capUsd;
|
|
@@ -222,7 +214,7 @@ function statusHeaderLine(root, snap, now) {
|
|
|
222
214
|
// empty unless the team marker's deadline stands in the future, so a role-only or
|
|
223
215
|
// indefinite pause keeps today's header byte-identical.
|
|
224
216
|
const running = snap.running
|
|
225
|
-
? [{ text: "running", tone: "green" }, { text: ` (pid ${snap.pid}` }, ...badgeSpans(buildBadge(snap.build), snap.build?.stale ? "yellow" : undefined), { text: ")" }]
|
|
217
|
+
? [{ text: "running", tone: "green" }, { text: ` (pid ${snap.pid}` }, ...badgeSpans(buildBadge(snap.build), snap.build?.stale || snap.build?.waitingForCheck ? "yellow" : undefined), { text: ")" }]
|
|
226
218
|
: [{ text: "not running — start with `tumwater start`", tone: "dim" }];
|
|
227
219
|
return [
|
|
228
220
|
{ text: "tumwater", tone: "brand" },
|
|
@@ -232,14 +224,14 @@ function statusHeaderLine(root, snap, now) {
|
|
|
232
224
|
...badgeSpans(mergeBadge(snap.mergeQueue), "blue"),
|
|
233
225
|
...(snap.inbox ? [{ text: ` · inbox: ${snap.inbox}` }] : []),
|
|
234
226
|
...(snap.questions ? badgeSpans(` · questions: ${snap.questions}`, "magenta") : []),
|
|
235
|
-
...badgeSpans(budgetBadge(snap.budget, snap.
|
|
227
|
+
...badgeSpans(budgetBadge(snap.budget, snap.chain), budgetTone(snap.budget)),
|
|
236
228
|
// The disk floor (plans/disk-floor.md, part 4/4): `disk 8.2 GB free — holding new work`
|
|
237
229
|
// while the hold is on, or the bare reading below the reclaim threshold. Empty in every
|
|
238
230
|
// comfortable or unmeasured state, so an unaffected header stays byte-identical.
|
|
239
231
|
...badgeSpans(diskBadge(snap.disk), snap.disk?.held ? "yellow" : "blue"),
|
|
240
232
|
// The permit pool's live state (plans/provider-concurrency.md §8): the team line reads
|
|
241
|
-
// `
|
|
242
|
-
//
|
|
233
|
+
// `running 5 of 8` — the team total's in-use/cap — for any team with a published permit
|
|
234
|
+
// snapshot; empty until one has been published.
|
|
243
235
|
...badgeSpans(permitsBadge(snap.permits), "blue"),
|
|
244
236
|
// Checks running/waiting for a check permit; empty while the check pool is idle.
|
|
245
237
|
...badgeSpans(checksBadge(snap.checkPermits), snap.checkPermits && snap.checkPermits.waiting > 0 ? "yellow" : "blue"),
|
|
@@ -306,14 +298,11 @@ function statusRoleRows(root, snap, now, withMetrics, sharedModel) {
|
|
|
306
298
|
// today's.
|
|
307
299
|
const inner = roleModelLabel(s);
|
|
308
300
|
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
301
|
// User-defined roles (snapshot's `custom` flag) get an asterisk beside their name — the
|
|
313
302
|
// dashboards' at-a-glance marker. The name column (index 0) is not in FLEXIBLE_COLUMNS;
|
|
314
303
|
// its width derives from row content and it shrinks only after those,
|
|
315
304
|
// so the extra character widens it.
|
|
316
|
-
const name = (s.custom ? `${roleLabel(s.role)}*` : roleLabel(s.role)) + modelSuffix
|
|
305
|
+
const name = (s.custom ? `${roleLabel(s.role)}*` : roleLabel(s.role)) + modelSuffix;
|
|
317
306
|
const state = stateCell(root, s, phase, live);
|
|
318
307
|
const tone = phaseTone(phase);
|
|
319
308
|
// The whole state cell carries the phase's tone: a work item leads the cell (so it
|
|
@@ -509,7 +498,7 @@ export function renderStatus(root, snap, maxWidth, scoped = false) {
|
|
|
509
498
|
// Composed here, NOT inside renderStatusSpans: the TUI re-slices that function's shared
|
|
510
499
|
// `lines` (lines[0], its own attention block, then lines.slice(1)), so a banner embedded
|
|
511
500
|
// there would render twice in the TUI.
|
|
512
|
-
const alerts = teamAlerts(snap, openQuestions(root), roles, Date.now(), refusedEntries(root),
|
|
501
|
+
const alerts = teamAlerts(snap, openQuestions(root), roles, Date.now(), refusedEntries(root), getOrphanAlert(root)).filter((a) => a.key !== "stopped");
|
|
513
502
|
const banner = alertLines(alerts, maxWidth ?? Number.MAX_SAFE_INTEGER, alerts.length).map(plainText);
|
|
514
503
|
const text = lines.map(plainText);
|
|
515
504
|
return [...text.slice(0, 2), ...banner, ...text.slice(2)].join("\n");
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
/** The backlog team alerts, in emission order: open questions, refused entries
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
import { entryListDetail,
|
|
1
|
+
/** The backlog team alerts, in emission order: open questions, refused entries. The two
|
|
2
|
+
* per-domain builders split out of src/ui/team-alerts.ts; the shared types and wording helpers
|
|
3
|
+
* live in team-alert-shared.ts. The stale questions, stalled entries, plan dependencies,
|
|
4
|
+
* stranded plans, duplicated headings, live finished history, validation gaps and false fixes
|
|
5
|
+
* banners moved to `tumwater doctor` rows (Subtract part 1e/6), each printing its row. */
|
|
6
|
+
import { entryListDetail, openBacklogAction } from "./team-alert-shared.js";
|
|
7
7
|
import { entryKindLabel } from "../backlog/backlog-eligibility.js";
|
|
8
|
-
import { missingSymbolNames } from "../verdict/fix-claim.js";
|
|
9
|
-
import { joinWithMore } from "../text/text.js";
|
|
10
8
|
import { plural } from "../text/phrases.js";
|
|
11
|
-
import { liveFinishedRowList } from "../backlog/backlog-archive.js";
|
|
12
9
|
function pushQuestionsAlert(out, questions) {
|
|
13
10
|
if (questions.length) {
|
|
14
11
|
out.push({
|
|
@@ -20,29 +17,6 @@ function pushQuestionsAlert(out, questions) {
|
|
|
20
17
|
});
|
|
21
18
|
}
|
|
22
19
|
}
|
|
23
|
-
// A stale question (doctor's STALE_QUESTION_DAYS rule, src/doctor/doctor-backlog-bugs.ts
|
|
24
|
-
// checkStaleQuestions) has waited a week without an answer — a role is stuck behind a decision,
|
|
25
|
-
// the exact class every other backlog row raises as an amber alert — so it asks (amber), naming
|
|
26
|
-
// the oldest question, its age, and the `tumwater questions
|
|
27
|
-
// answer` fix. Its callers pass it oldest-first (alert-feeds.ts staleOpenQuestions sorts
|
|
28
|
-
// most-days-waited first), so the first element is the most-stale question and is named with its
|
|
29
|
-
// position and days; the rest are folded through entryListDetail like the questions and refused
|
|
30
|
-
// alerts, so the wording and counts cannot drift. The `questions` action opens the Questions tab,
|
|
31
|
-
// so the alert carries no row address.
|
|
32
|
-
function pushStaleQuestionsAlert(out, staleQuestions) {
|
|
33
|
-
if (staleQuestions.length) {
|
|
34
|
-
const { first, more } = firstAndMore(staleQuestions, (q) => q.title);
|
|
35
|
-
out.push({
|
|
36
|
-
key: "stale-questions", // vocabulary-ok: the stale-questions amber alert's key
|
|
37
|
-
tone: "amber",
|
|
38
|
-
title: `${plural(staleQuestions.length, "open question waited", "open questions waited")} 7+ days for an answer`,
|
|
39
|
-
detail: `#${first.position} "${first.title}" has waited ${first.days}d for an answer` +
|
|
40
|
-
more +
|
|
41
|
-
` · answer it with \`tumwater questions answer ${first.position} "<decision>"\`, or close it in QUESTIONS.md`,
|
|
42
|
-
actions: [{ label: "Answer", act: "questions" }],
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
20
|
// Refused entries hold until a human decides; amber because the operator has to act.
|
|
47
21
|
function pushRefusedAlert(out, refused) {
|
|
48
22
|
if (refused.length) {
|
|
@@ -56,181 +30,17 @@ function pushRefusedAlert(out, refused) {
|
|
|
56
30
|
});
|
|
57
31
|
}
|
|
58
32
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (stalledEntries.length) {
|
|
67
|
-
const first = stalledEntries[0];
|
|
68
|
-
out.push({
|
|
69
|
-
key: "stalled-entries",
|
|
70
|
-
tone: "amber",
|
|
71
|
-
title: `${plural(stalledEntries.length, "backlog entry is stalled", "backlog entries are stalled")} in review`,
|
|
72
|
-
detail: `${entryKindLabel(first.file)} ${entryListDetail(stalledEntries.map((p) => p.title))} — a Needs review note parks it; the plan role splits or replans the entry, so one that keeps sitting here means the plan role is not running or keeps deferring`,
|
|
73
|
-
actions: openBacklogAction(first),
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// A broken plan-dependency clause (the doctor's `plan dependencies` rule,
|
|
78
|
-
// src/doctor/doctor-backlog-plans.ts planDependencyProblem, via alert-feeds.ts's
|
|
79
|
-
// planDependencyAlert) is invisible in the backlog listings — the claim allocator keeps
|
|
80
|
-
// acting on the misread clause — so it asks (amber), named by type
|
|
81
|
-
// and carrying the doctor's exact message. One alert at most (the problem set is the single
|
|
82
|
-
// most serious rule), and the fix is editing PLANS.md's heading, so there are no actions.
|
|
83
|
-
function pushPlanDependencyAlert(out, planDependency) {
|
|
84
|
-
if (planDependency !== null) {
|
|
85
|
-
const titles = {
|
|
86
|
-
cycle: "Plan dependency cycle blocks a whole series",
|
|
87
|
-
dangling: "A plan requires a part no heading carries",
|
|
88
|
-
unparsed: "A plan's requires clause names no readable part",
|
|
89
|
-
misworded: "A plan's dependency wording holds nothing",
|
|
90
|
-
};
|
|
91
|
-
out.push({
|
|
92
|
-
key: "plan-dependencies",
|
|
93
|
-
tone: "amber",
|
|
94
|
-
title: titles[planDependency.kind],
|
|
95
|
-
detail: planDependency.message,
|
|
96
|
-
// The fix is editing PLANS.md, and the message names the heading and ref.
|
|
97
|
-
actions: [],
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// A plan heading filed under the wrong ## section (the doctor's `plans stuck in Planned`
|
|
102
|
-
// rule, checkStrandedPlans, src/doctor/doctor-backlog-plans.ts, via the shared
|
|
103
|
-
// strandedPlanEntriesFor) sits invisible to every reader that scans the right section: a
|
|
104
|
-
// `(planned …)` heading under ## Done never enters a Planned reader, and a done-dated heading
|
|
105
|
-
// still under ## Planned invites a second implementation. The clean role repairs it only when
|
|
106
|
-
// it happens to run and the doctor warns only on request, so it asks (amber) the way the
|
|
107
|
-
// plan-dependency alert does, naming the first stranded heading, its file, its section, and
|
|
108
|
-
// the section it belongs under; the rest are folded through joinWithMore.
|
|
109
|
-
function pushStrandedPlansAlert(out, strandedPlans) {
|
|
110
|
-
if (strandedPlans.length) {
|
|
111
|
-
const { first, more } = firstAndMore(strandedPlans, (e) => `"${e.title}" in ${e.file ?? "PLANS.md"} ## ${e.section}`, (labels) => joinWithMore(labels, 3));
|
|
112
|
-
out.push({
|
|
113
|
-
key: "stranded-plans",
|
|
114
|
-
tone: "amber",
|
|
115
|
-
title: `${strandedPlans.length === 1 ? "A plan sits" : "Plans sit"} under the wrong backlog section`,
|
|
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
|
-
// The fix is editing a backlog file, and a PLANS-DONE.md entry has no PLANS.md drawer index.
|
|
118
|
-
actions: [],
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
// A duplicated `## ` section heading already on main (the doctor's `backlog headings` row,
|
|
123
|
-
// checkBacklogHeadings, src/doctor/doctor-backlog-plans.ts, via the shared
|
|
124
|
-
// duplicatedBacklogHeadings) sits until a deliberate removal edit — the merge gate blocks
|
|
125
|
-
// only a merge that ADDS another copy, so a duplicate already on main persists and hides
|
|
126
|
-
// every later entry under its name from the `## ` section readers. It asks (amber) the way
|
|
127
|
-
// the stranded-plan alert does, naming the first file-heading pair and folding the rest
|
|
128
|
-
// through joinWithMore; the fix is editing a backlog file, so there are no actions.
|
|
129
|
-
function pushBacklogHeadingsAlert(out, duplicatedHeadings) {
|
|
130
|
-
if (duplicatedHeadings.length) {
|
|
131
|
-
const { first, more } = firstAndMore(duplicatedHeadings, (d) => `"## ${d.heading}" in ${d.file}`, (labels) => joinWithMore(labels, 3));
|
|
132
|
-
out.push({
|
|
133
|
-
key: "backlog-headings",
|
|
134
|
-
tone: "amber",
|
|
135
|
-
title: plural(duplicatedHeadings.length, "duplicated backlog heading", "duplicated backlog headings"),
|
|
136
|
-
detail: `${first.file} "## ${first.heading}" appears more than once${more} — readers take the first section of a name, so later entries are invisible; remove the duplicate in a deliberate edit`,
|
|
137
|
-
actions: [],
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
// Finished history still in the live backlog files (the doctor's `backlog archive` rule,
|
|
142
|
-
// checkLiveFinished, src/doctor/doctor-backlog-plans.ts, via the shared liveFinishedEntriesFor)
|
|
143
|
-
// is the one backlog-hygiene rule the doctor warned but no always-on alert surfaced — every
|
|
144
|
-
// other rule (stranded plans, duplicated headings, validation gaps, plan dependencies, stale
|
|
145
|
-
// bugs and questions) has one, so an operator watching `tumwater status` or the dashboard never
|
|
146
|
-
// sees the growing finished sections until they run `tumwater doctor`. It asks (amber) the way
|
|
147
|
-
// the duplicated-heading alert does, naming each live file, its finished section, its entry
|
|
148
|
-
// count, and its size in KB; the fix is the `tumwater archive` command, so there are no actions.
|
|
149
|
-
function pushBacklogArchiveAlert(out, liveFinished) {
|
|
150
|
-
if (liveFinished.length) {
|
|
151
|
-
out.push({
|
|
152
|
-
key: "backlog-archive",
|
|
153
|
-
tone: "amber",
|
|
154
|
-
title: `${plural(liveFinished.length, "live backlog file still holds", "live backlog files still hold")} finished history`,
|
|
155
|
-
detail: liveFinishedRowList(liveFinished) +
|
|
156
|
-
" — every run that reads the file pays for them; `tumwater archive` moves them to the archive files",
|
|
157
|
-
// The fix is a command, not a drawer row.
|
|
158
|
-
actions: [],
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// A Fixed record with a bad Validation gap trace (the doctor's `validation gaps` check,
|
|
163
|
-
// src/doctor/doctor-backlog-plans.ts checkValidationGaps) is the always-on twin of that
|
|
164
|
-
// doctor row: a newest Fixed record that omits its **Validation gap:** line, names a tag
|
|
165
|
-
// outside the closed vocabulary, or declares `unclear-invariant` without pinning a
|
|
166
|
-
// docs/invariants.md section is invisible or misleading to every closed-vocabulary consumer,
|
|
167
|
-
// so it asks (amber) the way the stranded-plan alert does, naming the first offending record
|
|
168
|
-
// and the fix. It is fed from the same scan (validationGapProblems over validationGapRecords)
|
|
169
|
-
// the doctor's check uses, so the alert and the doctor cannot drift on the problem set, and
|
|
170
|
-
// that scan runs no sourceHaystack walk, so it adds only the same two backlog-file reads the
|
|
171
|
-
// doctor's check already makes to the poll path. The fix is editing BUGS.md, so it has no
|
|
172
|
-
// buttons.
|
|
173
|
-
function pushValidationGapsAlert(out, validationGaps) {
|
|
174
|
-
if (validationGaps.length) {
|
|
175
|
-
const { first, more } = firstAndMore(validationGaps, (v) => `"${v.title}"`, (labels) => joinWithMore(labels, 3));
|
|
176
|
-
const reason = first.kind === "missing"
|
|
177
|
-
? "has no **Validation gap:** line"
|
|
178
|
-
: first.kind === "unrecognized"
|
|
179
|
-
? `names unrecognized Validation gap tag "${first.tag}"`
|
|
180
|
-
: "is an unclear-invariant fix but names no docs/invariants.md section";
|
|
181
|
-
out.push({
|
|
182
|
-
key: "validation-gaps",
|
|
183
|
-
tone: "amber",
|
|
184
|
-
title: plural(validationGaps.length, "Fixed record has a validation-gap problem", "Fixed records have validation-gap problems"),
|
|
185
|
-
detail: `BUGS.md "${first.title}" ${reason}${more} — every Fixed entry writes exactly one known **Validation gap:** tag (write \`none\` rather than omitting it), and an unclear-invariant fix pins its reconstructed guarantee in docs/invariants.md`,
|
|
186
|
-
// The fix is editing BUGS.md.
|
|
187
|
-
actions: [],
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
// A newest Fixed record whose Fix paragraph names only code absent from the tree (the
|
|
192
|
-
// doctor's `claimed bugs` check, src/doctor/doctor-backlog-plans.ts checkFixClaims) is the
|
|
193
|
-
// always-on twin of that doctor row: a fix claim that names no live symbol is invisible to
|
|
194
|
-
// every consumer that reads BUGS.md (BUGS-FIXED.md's long fight over an already-fixed entry),
|
|
195
|
-
// so it asks (amber) the way the validation-gaps alert does, naming the first record and the
|
|
196
|
-
// missing symbols. It is fed from the same scan (fixClaimProblems over fixClaimRecords) the
|
|
197
|
-
// doctor's check uses, so the alert and the doctor cannot drift on the problem set, and that
|
|
198
|
-
// scan reads the tree only when a file changed (cachedSourceHaystack), so it adds no re-read
|
|
199
|
-
// to the 1 Hz poll path. The fix is editing BUGS.md, so it has no buttons.
|
|
200
|
-
function pushFalseFixesAlert(out, fixClaims) {
|
|
201
|
-
if (fixClaims.length) {
|
|
202
|
-
const { first, more } = firstAndMore(fixClaims, (p) => `"${p.heading}"`, (labels) => joinWithMore(labels, 3));
|
|
203
|
-
out.push({
|
|
204
|
-
key: "false-fixes",
|
|
205
|
-
tone: "amber",
|
|
206
|
-
title: plural(fixClaims.length, "Fixed record names code that is not on this tree", "Fixed records name code that is not on this tree"),
|
|
207
|
-
detail: `BUGS.md records "${first.heading}" as Fixed, but none of the symbols its Fix paragraph names exist on this tree: ${missingSymbolNames(first.missing)}${more} — land the fix or keep the bug Open / refresh a stale record`,
|
|
208
|
-
// The fix is editing BUGS.md.
|
|
209
|
-
actions: [],
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
/** The backlog alerts, in emission order: open questions, refused entries, stalled
|
|
214
|
-
* entries, a broken plan-dependency clause, stranded plans, duplicated backlog
|
|
215
|
-
* headings, finished history still in the live backlog files, and Fixed records with a
|
|
216
|
-
* bad Validation gap trace. Extracted from
|
|
217
|
-
* teamAlerts (part 2/5 of the teamAlerts split) so the shared module stays small; the input
|
|
218
|
-
* names exactly the fields this run reads. The caller pushes the returned alerts at the
|
|
219
|
-
* position this run used to occupy, so the emitted key order is unchanged. Each alert arm is a
|
|
220
|
-
* module-private builder named for its alert, in this same emission order, so a backlog alert
|
|
221
|
-
* is found by name. */
|
|
33
|
+
/** The backlog alerts, in emission order: open questions, refused entries. The conditions that
|
|
34
|
+
* used to raise the removed backlog banners each print their `tumwater doctor` row instead
|
|
35
|
+
* (Subtract part 1e/6). Extracted from teamAlerts (part 2/5 of the teamAlerts split) so the
|
|
36
|
+
* shared module stays small; the input names exactly the fields this run reads. The caller
|
|
37
|
+
* pushes the returned alerts at the position this run used to occupy, so the emitted key order
|
|
38
|
+
* is unchanged. Each alert arm is a module-private builder named for its alert, in this same
|
|
39
|
+
* emission order, so a backlog alert is found by name. */
|
|
222
40
|
export function buildBacklogAlerts(input) {
|
|
223
41
|
const out = [];
|
|
224
|
-
const { questions,
|
|
42
|
+
const { questions, refused } = input;
|
|
225
43
|
pushQuestionsAlert(out, questions);
|
|
226
|
-
pushStaleQuestionsAlert(out, staleQuestions);
|
|
227
44
|
pushRefusedAlert(out, refused);
|
|
228
|
-
pushStalledEntriesAlert(out, stalledEntries);
|
|
229
|
-
pushPlanDependencyAlert(out, planDependency);
|
|
230
|
-
pushStrandedPlansAlert(out, strandedPlans);
|
|
231
|
-
pushBacklogHeadingsAlert(out, duplicatedHeadings);
|
|
232
|
-
pushBacklogArchiveAlert(out, liveFinished);
|
|
233
|
-
pushValidationGapsAlert(out, validationGaps);
|
|
234
|
-
pushFalseFixesAlert(out, fixClaims);
|
|
235
45
|
return out;
|
|
236
46
|
}
|
|
@@ -15,30 +15,6 @@ export function entryListDetail(titles) {
|
|
|
15
15
|
const first = titles[0] === undefined ? "" : stripEntryStamp(titles[0]);
|
|
16
16
|
return titles.length > 1 ? `${first} — and ${titles.length - 1} more` : first;
|
|
17
17
|
}
|
|
18
|
-
/** Append the folded names of the entries past an alert's first one, after a ` · ` separator,
|
|
19
|
-
* when there are any, and the empty string otherwise — the shared idiom every amber
|
|
20
|
-
* "name the oldest, fold the rest" alert (stale-questions, stranded-plans,
|
|
21
|
-
* backlog-headings, validation-gaps, false-fixes) uses to compose its
|
|
22
|
-
* `detail`, so the separator, the empty case, and the default fold (entryListDetail;
|
|
23
|
-
* joinWithMore for the alerts that fold their rest labels with a count) cannot
|
|
24
|
-
* drift. Callers pre-map each
|
|
25
|
-
* rest element to the label the first clause already named. */
|
|
26
|
-
function moreDetail(restLabels, fold = entryListDetail) {
|
|
27
|
-
return restLabels.length > 0 ? ` · ${fold(restLabels)}` : "";
|
|
28
|
-
}
|
|
29
|
-
/** Split a bounded entry list into its first (oldest, named) item and the " · rest" suffix
|
|
30
|
-
* moreDetail builds for the entries past it — the shared shape every amber "name the oldest,
|
|
31
|
-
* fold the rest" alert (stale-questions, stranded-plans,
|
|
32
|
-
* backlog-headings, validation-gaps, false-fixes) composes, so
|
|
33
|
-
* the split and the separator/empty case have one home beside moreDetail. `label` maps each
|
|
34
|
-
* rest element to the string its clause already named; `fold` defaults to entryListDetail
|
|
35
|
-
* (moreDetail's own default), with joinWithMore(labels, 3) for the alerts that fold their rest
|
|
36
|
-
* labels with a count. */
|
|
37
|
-
export function firstAndMore(items, label, fold = entryListDetail) {
|
|
38
|
-
const first = items[0];
|
|
39
|
-
const rest = items.slice(1);
|
|
40
|
-
return { first, more: moreDetail(rest.map(label), fold) };
|
|
41
|
-
}
|
|
42
18
|
/** The dashboard's "Open <role>" buttons for an alert about a set of roles: one button per
|
|
43
19
|
* role (capped to the first three so the banner does not overflow), each opening that role's
|
|
44
20
|
* pane. Shared by the failing, stuck, and no-image-target alerts, so the button shape and its
|
|
@@ -54,8 +30,8 @@ export function seeFailures() {
|
|
|
54
30
|
}
|
|
55
31
|
/** The dashboard's "Open backlog" button for an alert naming one backlog entry: opens the
|
|
56
32
|
* entry's backlog-card tab and that row (the dashboard's backlog arm reads `file:index`).
|
|
57
|
-
*
|
|
58
|
-
* so the button shape and the `file:index` contract cannot drift
|
|
33
|
+
* Used by the refused alert,
|
|
34
|
+
* so the button shape and the `file:index` contract cannot drift. */
|
|
59
35
|
export function openBacklogAction(entry) {
|
|
60
36
|
return [{ label: "Open backlog", act: "backlog", arg: `${entry.file}:${entry.index}` }];
|
|
61
37
|
}
|
|
@@ -77,11 +53,12 @@ export function listRoleIds(ids) {
|
|
|
77
53
|
/** The stuck alert's per-role detail: the stall phrase the phase rendered, plus — when that
|
|
78
54
|
* phrase does not already carry one — the honest silence span from the live tail (stuckMs,
|
|
79
55
|
* ms since the role's last pi log write, capped to the run by progressOfRun). A
|
|
80
|
-
* "no agent output for <d>" phrase already carries its duration, so
|
|
81
|
-
* a " for <duration>" suffix, and only when the caller holds a live
|
|
56
|
+
* "no agent output for <d>" or "no model reply for <d>" phrase already carries its duration, so
|
|
57
|
+
* only a tool-call stall gets a " for <duration>" suffix, and only when the caller holds a live
|
|
58
|
+
* tail (stuckMs present). */
|
|
82
59
|
export function stuckDetail(l) {
|
|
83
60
|
const cause = (STALL_RE.exec(l.phase)?.[0] ?? "").trim();
|
|
84
|
-
if (cause
|
|
61
|
+
if (cause.startsWith("tool call stalled") && l.stuckMs !== undefined) {
|
|
85
62
|
return `${roleLabel(l.role)}: ${cause} for ${duration(l.stuckMs)}`;
|
|
86
63
|
}
|
|
87
64
|
return `${roleLabel(l.role)}: ${cause}`;
|