tumwater 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { imageReroute } from "../agent/image-input.js";
|
|
1
2
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
2
3
|
import { isDirty, deleteRef } from "../git/git.js";
|
|
3
4
|
import { applyConfigRequest } from "../config/config-write.js";
|
|
4
|
-
import { diagnoseNoChange } from "../verdict/no-change.js";
|
|
5
|
+
import { diagnoseNoChange, extractNoChangeAnswer } from "../verdict/no-change.js";
|
|
5
6
|
import { handleRefusal, refusalContradiction } from "../verdict/refusal.js";
|
|
6
7
|
import { extractSummary } from "../git/commit-message.js";
|
|
7
8
|
import { recordFlow } from "./qa-coverage.js";
|
|
@@ -41,7 +42,7 @@ export async function resolveTickVerdict(ctx) {
|
|
|
41
42
|
if (request.error)
|
|
42
43
|
ctx.warn(`config request rejected: ${request.error}`);
|
|
43
44
|
if (request.ignored.length)
|
|
44
|
-
ctx.warn(`config request ignored key(s): ${request.ignored.join(", ")} — only
|
|
45
|
+
ctx.warn(`config request ignored key(s): ${request.ignored.join(", ")} — only customRoles is accepted`);
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
if (pi.quietKilled) {
|
|
@@ -68,13 +69,13 @@ export async function resolveTickVerdict(ctx) {
|
|
|
68
69
|
}
|
|
69
70
|
if (pi.timedOut) {
|
|
70
71
|
s.lastError = pi.errorMessage ?? "timed out";
|
|
71
|
-
// The request never ran to completion and no work
|
|
72
|
+
// The request never ran to completion and no work merged: put it back so the next
|
|
72
73
|
// tick retries it. (A killed run's half-done edits are discarded by the reset.)
|
|
73
74
|
ctx.pending.requeueUnfulfilled(ctx.userPrompt);
|
|
74
75
|
return { result: "error" };
|
|
75
76
|
}
|
|
76
77
|
// A subscription claude run the usage window rejected is a hold, not a run failure: the
|
|
77
|
-
//
|
|
78
|
+
// team hold keeps every subscription claude seam from starting until `resetsAt`
|
|
78
79
|
// (plans/agent-backends.md §8, part 6c), and this run's session and worktree edits must
|
|
79
80
|
// survive the wait — the next eligible tick resumes them (part 6e) instead of committing
|
|
80
81
|
// partial work or resetting the worktree to main. The rejected-window reading is the
|
|
@@ -105,6 +106,62 @@ export async function resolveTickVerdict(ctx) {
|
|
|
105
106
|
ctx.pending.requeueForResume(s, ctx.userPrompt);
|
|
106
107
|
return { result: "aborted", resumeCause: "budget-capped" };
|
|
107
108
|
}
|
|
109
|
+
// A run the runner killed the moment its agent read an image its model cannot see
|
|
110
|
+
// (plans/image-input.md "Failing the tick"): pi strips such an image from the request, so
|
|
111
|
+
// the agent was about to answer about a picture it never saw. The run was deliberately
|
|
112
|
+
// stopped before it could work on, so its half-done edits are discarded exactly as a
|
|
113
|
+
// timed-out run's are — nothing to resume, and the prompt re-queues to run fresh on a
|
|
114
|
+
// model that CAN see (part 4 adds the reroute arm here). The error backoff and streak apply
|
|
115
|
+
// as for any error.
|
|
116
|
+
if (pi.imageBlind) {
|
|
117
|
+
// One hop only (plans/image-input.md "Rerunning on a model that can read images"): a
|
|
118
|
+
// tick that ALREADY started on the image-capable target and still read blind is not
|
|
119
|
+
// rerouted a second time — it fails, so a misreadable image cannot bounce the loop
|
|
120
|
+
// between tiers forever. Set at tick start by tickPhase when it routes a tick onto the
|
|
121
|
+
// target (blind-read handoff or attachment preflight).
|
|
122
|
+
if (s.imageRerouteActive) {
|
|
123
|
+
s.lastError = pi.errorMessage ?? `read ${pi.imageBlind.path}, but the model cannot read images`;
|
|
124
|
+
// No work was done (the run was killed at the first read): nothing to resume, and the
|
|
125
|
+
// prompt re-queues to run fresh later like any other error. The error backoff/streak
|
|
126
|
+
// apply as usual.
|
|
127
|
+
ctx.pending.requeueUnfulfilled(ctx.userPrompt);
|
|
128
|
+
return { result: "error" };
|
|
129
|
+
}
|
|
130
|
+
// Otherwise, when the role's tier has an image-capable reroute target, the failed work
|
|
131
|
+
// is not discarded: the next tick starts directly on that target over the kept worktree.
|
|
132
|
+
// The one-shot field carries the target seam plus the `from`/`path` facts the handoff
|
|
133
|
+
// note needs; the tick ends `aborted` with the `image-reroute` resume cause, and
|
|
134
|
+
// applyTickOutcome schedules it promptly (nextRunAt = now) while planTickStart hands the
|
|
135
|
+
// kept worktree off to a FRESH session on the target.
|
|
136
|
+
const regime = ctx.rerouteRegime ?? "primary";
|
|
137
|
+
const target = ctx.config !== undefined ? imageReroute(ctx.config, ctx.role, regime) : null;
|
|
138
|
+
if (target) {
|
|
139
|
+
const from = ctx.runModel ?? target.config.model ?? "the model";
|
|
140
|
+
s.imageReroute = {
|
|
141
|
+
tier: target.tier,
|
|
142
|
+
provider: target.config.provider,
|
|
143
|
+
model: target.config.model,
|
|
144
|
+
thinking: target.config.thinking,
|
|
145
|
+
from,
|
|
146
|
+
path: pi.imageBlind.path,
|
|
147
|
+
};
|
|
148
|
+
s.lastError = `read ${pi.imageBlind.path}, but ${from} cannot read images — rerouting to ${target.tier}`;
|
|
149
|
+
ctx.pending.requeueForResume(s, ctx.userPrompt);
|
|
150
|
+
logEventBestEffort(ctx.root, {
|
|
151
|
+
source: ctx.role,
|
|
152
|
+
type: "image_reroute",
|
|
153
|
+
run: s.runs,
|
|
154
|
+
from,
|
|
155
|
+
to: target.config.model,
|
|
156
|
+
tier: target.tier,
|
|
157
|
+
preflight: false,
|
|
158
|
+
});
|
|
159
|
+
return { result: "aborted", resumeCause: "image-reroute" };
|
|
160
|
+
}
|
|
161
|
+
s.lastError = pi.errorMessage ?? `read ${pi.imageBlind.path}, but the model cannot read images`;
|
|
162
|
+
ctx.pending.requeueUnfulfilled(ctx.userPrompt);
|
|
163
|
+
return { result: "error" };
|
|
164
|
+
}
|
|
108
165
|
// A refusal is a decision, not a failure: even when pi's exit was abnormal, the sentinel
|
|
109
166
|
// and any note it left are the run's verdict — classify what it left behind
|
|
110
167
|
// (src/verdict/refusal.ts).
|
|
@@ -120,6 +177,7 @@ export async function resolveTickVerdict(ctx) {
|
|
|
120
177
|
else {
|
|
121
178
|
return handleRefusal({
|
|
122
179
|
role: ctx.role,
|
|
180
|
+
root: ctx.root,
|
|
123
181
|
mainBranch: ctx.mainBranch,
|
|
124
182
|
turns: ctx.turns,
|
|
125
183
|
merge: (w, sum) => ctx.merge(w, sum),
|
|
@@ -136,7 +194,7 @@ export async function resolveTickVerdict(ctx) {
|
|
|
136
194
|
// would ship unfinished work under a diff-derived subject and drop the resumable session
|
|
137
195
|
// (BUGS.md 2026-10-06). Keep the worktree and session for the next resume.
|
|
138
196
|
if (!pi.ok && (!changed || !pi.producedAssistantContent)) {
|
|
139
|
-
s.lastError = pi.errorMessage ?? "
|
|
197
|
+
s.lastError = pi.errorMessage ?? "the agent failed";
|
|
140
198
|
// A dirty worktree on a run that added nothing of its own is a prior tick's work: arm the
|
|
141
199
|
// resume so the next tick continues that session instead of resetting the edits. A role
|
|
142
200
|
// loop re-queues through requeueForResume, so the resume reclaims exactly this prompt
|
|
@@ -148,7 +206,7 @@ export async function resolveTickVerdict(ctx) {
|
|
|
148
206
|
ctx.pending.requeueForResume(s, ctx.userPrompt);
|
|
149
207
|
}
|
|
150
208
|
else {
|
|
151
|
-
// No work
|
|
209
|
+
// No work merged, so the request was not fulfilled: re-queue it. A no_change outcome
|
|
152
210
|
// IS fulfillment (a question-type prompt answered without file changes) — never
|
|
153
211
|
// re-queue that, or such prompts would loop forever.
|
|
154
212
|
ctx.pending.requeueUnfulfilled(ctx.userPrompt);
|
|
@@ -170,9 +228,9 @@ export async function resolveTickVerdict(ctx) {
|
|
|
170
228
|
await resetWorktreeToMain(ctx.wt, ctx.mainBranch);
|
|
171
229
|
await deleteRef(ctx.root, rejectedRefName(ctx.role));
|
|
172
230
|
// The drop's durable effects — the worktree reset, the rejected ref's deletion, and the
|
|
173
|
-
// cleared state — have all
|
|
231
|
+
// cleared state — have all taken effect; the event is observability, so an unwritable feed must
|
|
174
232
|
// not end the tick as an error and mask a change that really was dropped.
|
|
175
|
-
logEventBestEffort(ctx.root, {
|
|
233
|
+
logEventBestEffort(ctx.root, { source: ctx.role, type: "revision", action: "dropped", round: ctx.revisionRound, sha });
|
|
176
234
|
return { result: "no_change" };
|
|
177
235
|
}
|
|
178
236
|
if (!changed) {
|
|
@@ -181,7 +239,7 @@ export async function resolveTickVerdict(ctx) {
|
|
|
181
239
|
// diagnosable on its own.
|
|
182
240
|
const diagnosis = diagnoseNoChange(pi);
|
|
183
241
|
if (!pi.nothingToDo) {
|
|
184
|
-
ctx.warn(`
|
|
242
|
+
ctx.warn(`the agent finished without changes and without declaring nothing-to-do` +
|
|
185
243
|
(diagnosis.notes.length ? ` (${diagnosis.notes.join(", ")})` : ""));
|
|
186
244
|
}
|
|
187
245
|
// A cut-off run did real work and was NOT fulfilled: a director prompt goes back
|
|
@@ -194,7 +252,15 @@ export async function resolveTickVerdict(ctx) {
|
|
|
194
252
|
// rotation past a check that did not complete. Only a run that was not cut off records.
|
|
195
253
|
if (ctx.flow && !diagnosis.cutOff)
|
|
196
254
|
recordFlow(ctx.root, ctx.flow.flow, ctx.flow.result, ctx.flow.result === "bug" ? extractSummary(pi.finalText) ?? undefined : undefined);
|
|
197
|
-
|
|
255
|
+
// A prompt-driven (director) tick that answers a question in prose ends `no_change` with
|
|
256
|
+
// no SUMMARY block, so its reply would be lost — the dashboard reads a bare "No change"
|
|
257
|
+
// and the feed carries nothing (BUGS.md 2026-10-14). Keep the prose above the sentinel as
|
|
258
|
+
// the tick's summary so `lastSummary` and the run_end event show the answer where the
|
|
259
|
+
// operator asked the question. A role loop's bare nothing-to-do stays summary-less.
|
|
260
|
+
const summary = ctx.userPrompt !== null && !diagnosis.cutOff
|
|
261
|
+
? (extractNoChangeAnswer(pi.finalText) ?? undefined)
|
|
262
|
+
: undefined;
|
|
263
|
+
return { result: "no_change", cutOff: diagnosis.cutOff || undefined, summary };
|
|
198
264
|
}
|
|
199
265
|
return null;
|
|
200
266
|
}
|
package/dist/src/ui/badges.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import { claudeWindowLabel } from "../agent/claude/claude-account.js";
|
|
1
|
+
import { claudeUtilizationPct, claudeWindowLabel } from "../agent/claude/claude-account.js";
|
|
2
|
+
import { MODEL_TIERS } from "../config/config-schema.js";
|
|
2
3
|
import { budgetGate, budgetReached, fallbackTierEntries } from "../budget/budget.js";
|
|
4
|
+
import { fallbackDisplayPair } from "../config/config-views.js";
|
|
3
5
|
import { quietWindowEnd } from "../scheduling/quiet-hours.js";
|
|
4
|
-
import {
|
|
5
|
-
import { pauseReasonSuffix } from "../text/phrases.js";
|
|
6
|
+
import { clockHm, humanUntil } from "../text/datetime.js";
|
|
7
|
+
import { pauseReasonSuffix, plural } from "../text/phrases.js";
|
|
6
8
|
import { formatGB, shortSha, usd, usdCap } from "../text/format.js";
|
|
7
9
|
/** The status header's BADGE fragments, phrased once for both observer surfaces: the terminal
|
|
8
10
|
* table (status-render.ts's renderStatus header) and the JSON/GUI payload
|
|
9
11
|
* (status-payload.ts's preformatted badge fields). Split from status-model.ts, which keeps the
|
|
10
|
-
* per-loop display model (phase, rows, token metrics) — the badges are header-scale
|
|
11
|
-
* facts (build, main check, budget,
|
|
12
|
+
* per-loop display model (phase, rows, token metrics) — the badges are header-scale team
|
|
13
|
+
* facts (build, main check, budget, merge queue, timed pause) rather than per-loop state, and
|
|
12
14
|
* their one-home rule ("the page cannot re-derive and drift from these strings") is easier to
|
|
13
15
|
* see when the module is exactly the badges. The duration phrasing they share (humanSeconds)
|
|
14
16
|
* lives in datetime.ts — core modules need it too, so it sits below the ui layer. */
|
|
15
17
|
/** The header's build fragment: which commit the running harness was compiled from and, when
|
|
16
|
-
* main's build inputs have moved past it, how far — the
|
|
18
|
+
* main's build inputs have moved past it, how far — the team is then executing code main no
|
|
17
19
|
* longer describes (auto-restart lands the new build; off, restart `tumwater run` by hand).
|
|
18
20
|
* A stale build also says what auto-restart is doing about it: `restart pending` resolves
|
|
19
21
|
* itself, `restart BLOCKED` never will until main moves, and telling them apart at a glance is
|
|
@@ -22,11 +24,11 @@ import { formatGB, shortSha, usd, usdCap } from "../text/format.js";
|
|
|
22
24
|
export function buildBadge(build) {
|
|
23
25
|
if (!build)
|
|
24
26
|
return "";
|
|
25
|
-
const stale = build.stale ? ` —
|
|
27
|
+
const stale = build.stale ? ` — update available (${plural(build.aheadCommits ?? 0, "commit")} behind)` : "";
|
|
26
28
|
const restart = build.restartBlocked
|
|
27
|
-
? `; restart
|
|
29
|
+
? `; restart blocked: ${build.restartBlocked}`
|
|
28
30
|
: build.restartPending
|
|
29
|
-
? "
|
|
31
|
+
? " — restarts when idle"
|
|
30
32
|
: "";
|
|
31
33
|
return `, build ${shortSha(build.sha)}${stale}${restart}`;
|
|
32
34
|
}
|
|
@@ -52,7 +54,7 @@ export function mainCountsFragment(counts) {
|
|
|
52
54
|
* its raw status word with `fallbackTone` for color — callers pick the tone their palette
|
|
53
55
|
* uses (the TUI's status header renders yellow, the GUI sidebar amber). The verdict word
|
|
54
56
|
* never reads green for an unverified tree. Readers: mainCheckBadge (below) and the header
|
|
55
|
-
* badge tone in status-render.ts. The GUI sidebar's status row (gui-client-
|
|
57
|
+
* badge tone in status-render.ts. The GUI sidebar's status row (gui-client-team.ts) ships
|
|
56
58
|
* its own copy of the mapping — it is browser-side JS spliced into gui-client.ts and cannot
|
|
57
59
|
* import this module; its three-line ternary block is deliberately left as is. */
|
|
58
60
|
export function mainCheckVerdict(status, fallbackTone = status) {
|
|
@@ -67,7 +69,7 @@ export function mainCheckVerdict(status, fallbackTone = status) {
|
|
|
67
69
|
/** Main's newest merge-scope check as a header badge (PLANS.md "Retire the README freshness
|
|
68
70
|
* stamp"): `· main <sha>: green · N/N (N skipped)` — the live replacement for the committed
|
|
69
71
|
* README stamp the readme role used to maintain. Empty when the snapshot carries no check
|
|
70
|
-
* (none has run yet), so a quiet header stays byte-identical to a pre-check
|
|
72
|
+
* (none has run yet), so a quiet header stays byte-identical to a pre-check team. The
|
|
71
73
|
* verdict maps passed→green, failed→red; anything else (a skipped scope check) renders its
|
|
72
74
|
* raw word — an unverified tree must not read green. The counts fragment (mainCountsFragment)
|
|
73
75
|
* omits parentheticals for zero counts, matching the stamp wording the README carried. */
|
|
@@ -80,11 +82,11 @@ export function mainCheckBadge(mainCheck) {
|
|
|
80
82
|
const sha = mainCheck.sha ? `${shortSha(mainCheck.sha)}: ` : "";
|
|
81
83
|
return ` · main ${sha}${verdict}${counts}`;
|
|
82
84
|
}
|
|
83
|
-
/** The
|
|
84
|
-
* badge's default-tier story (part 5c/8): the badge names the
|
|
85
|
+
/** The team's current budget-gate state, derived from a snapshot's budget block — the header
|
|
86
|
+
* badge's default-tier story (part 5c/8): the badge names the team's bulk fallback, so it
|
|
85
87
|
* reads the `default` tier's verdict alone (the per-tier pause lives on the loop rows, via
|
|
86
88
|
* status-data.ts's budgetPausedRoles). */
|
|
87
|
-
function
|
|
89
|
+
function teamBudgetGate(budget) {
|
|
88
90
|
return budgetGate(budgetReached(budget), { default: budget.fallback !== null, strong: budget.fallback !== null }, false);
|
|
89
91
|
}
|
|
90
92
|
/** The new-project bootstrap's progress fragment (plans/work-ratio.md, part 2/2):
|
|
@@ -103,32 +105,63 @@ function claudeWindowStatusWord(status) {
|
|
|
103
105
|
return "limited";
|
|
104
106
|
return status === "reset" ? "reset" : "ok";
|
|
105
107
|
}
|
|
106
|
-
/**
|
|
107
|
-
*
|
|
108
|
-
function
|
|
109
|
-
const
|
|
110
|
-
|
|
108
|
+
/** How far off a window's reset is, two units wide: `4h 12m`, `5d 3h`, `37m`. A clock time
|
|
109
|
+
* alone cannot tell a 7d window resetting Monday from one resetting tonight. */
|
|
110
|
+
function untilReset(resetsAt, now) {
|
|
111
|
+
const minutes = Math.floor((resetsAt - now) / 60_000);
|
|
112
|
+
if (minutes < 60)
|
|
113
|
+
return `${minutes}m`;
|
|
114
|
+
const hours = Math.floor(minutes / 60);
|
|
115
|
+
if (hours < 24)
|
|
116
|
+
return `${hours}h ${minutes % 60}m`;
|
|
117
|
+
return `${Math.floor(hours / 24)}d ${hours % 24}h`;
|
|
111
118
|
}
|
|
112
119
|
/** The subscription usage-window fragment (plans/agent-backends.md §8, PLANS.md part 6d/7):
|
|
113
|
-
* `· Claude 5h · 64% · resets
|
|
114
|
-
* and an `overage` marker while the
|
|
115
|
-
*
|
|
120
|
+
* `· Claude 5h · 64% · resets in 4h 12m` (`resets now` once the deadline has passed), with
|
|
121
|
+
* the status word when no `utilization` was reported and an `overage` marker while the
|
|
122
|
+
* account spends into overage. A rejected window reads
|
|
123
|
+
* `claude model jobs wait until 14:30` in place of `resets` while a future deadline holds the
|
|
116
124
|
* seams; a reset reading names neither. The one home of the string, so the TUI header and the
|
|
117
125
|
* payload's preformatted badge cannot drift. */
|
|
118
|
-
function claudeWindowBadge(claudeWindow) {
|
|
126
|
+
function claudeWindowBadge(claudeWindow, now, moved = false) {
|
|
119
127
|
const w = claudeWindow.primary;
|
|
120
128
|
const parts = [`Claude ${claudeWindowLabel(w.type)}`];
|
|
121
|
-
parts.push(typeof w.utilization === "number" ? `${
|
|
129
|
+
parts.push(typeof w.utilization === "number" ? `${claudeUtilizationPct(w.utilization)}%` : claudeWindowStatusWord(w.status));
|
|
122
130
|
if (w.isUsingOverage === true)
|
|
123
131
|
parts.push("overage");
|
|
124
132
|
if (w.status === "rejected" && claudeWindow.heldUntil !== null) {
|
|
125
|
-
parts.push(
|
|
133
|
+
parts.push(`${moved ? "on fallback" : "claude model jobs wait"} until ${clockHm(claudeWindow.heldUntil)}`);
|
|
126
134
|
}
|
|
127
135
|
else if (w.status !== "reset" && typeof w.resetsAt === "number") {
|
|
128
|
-
parts.push(`resets ${
|
|
136
|
+
parts.push(w.resetsAt - now < 60_000 ? "resets now" : `resets in ${untilReset(w.resetsAt, now)}`);
|
|
129
137
|
}
|
|
130
138
|
return ` · ${parts.join(" · ")}`;
|
|
131
139
|
}
|
|
140
|
+
/** Whether any tier of a chain block runs on a fallback because the Claude window (or its
|
|
141
|
+
* reserve) is used up. */
|
|
142
|
+
function chainMovedByWindow(chain) {
|
|
143
|
+
return Object.values(chain ?? {}).some((t) => t.reason === "window" || t.reason === "reserve");
|
|
144
|
+
}
|
|
145
|
+
/** The tiers' fallback steps as badge text, ` · `-joined without a leading separator:
|
|
146
|
+
* `default → DeepSeek-V4-Flash $3.10/$5 until 17:40`, one fragment per tier in MODEL_TIERS
|
|
147
|
+
* order. Empty when the block is absent or empty. */
|
|
148
|
+
export function chainFragments(chain) {
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const tier of MODEL_TIERS) {
|
|
151
|
+
const t = chain?.[tier];
|
|
152
|
+
if (!t)
|
|
153
|
+
continue;
|
|
154
|
+
const money = t.spentUsd !== undefined && t.capUsd !== undefined ? ` ${usd(t.spentUsd)}/${usdCap(t.capUsd)}` : "";
|
|
155
|
+
const until = t.returnsAt !== undefined ? ` until ${clockHm(t.returnsAt)}` : "";
|
|
156
|
+
out.push(`${tier} → ${t.pair}${money}${until}`);
|
|
157
|
+
}
|
|
158
|
+
return out.join(" · ");
|
|
159
|
+
}
|
|
160
|
+
/** The chain block as a header fragment: ` · ` plus chainFragments, empty without a chain. */
|
|
161
|
+
export function chainBadge(chain) {
|
|
162
|
+
const text = chainFragments(chain);
|
|
163
|
+
return text ? ` · ${text}` : "";
|
|
164
|
+
}
|
|
132
165
|
/** The window fragment's tone rank, for the header badge and the GUI meter: a rejected window
|
|
133
166
|
* is red, an allowed_warning amber, anything else no tone. */
|
|
134
167
|
export function claudeWindowTone(claudeWindow) {
|
|
@@ -141,8 +174,8 @@ export function claudeWindowTone(claudeWindow) {
|
|
|
141
174
|
return undefined;
|
|
142
175
|
}
|
|
143
176
|
/** The header's daily-cost-budget fragment, standing in every cap state (the badge is also
|
|
144
|
-
* the affordance for editing the cap, so a disabled
|
|
145
|
-
* for a
|
|
177
|
+
* the affordance for editing the cap, so a disabled team needs it too): `· budget: n/a`
|
|
178
|
+
* for a team whose models are all free (spend can never accumulate against a cap that
|
|
146
179
|
* cannot be reached — and no priced model means no daily spend to count, hence no "today"
|
|
147
180
|
* — checked first, in every cap state), `· budget: $X.XX/$Y today` while enabled with
|
|
148
181
|
* priced models, and `· budget: $X today · no cap` when disabled. One
|
|
@@ -160,16 +193,16 @@ export function claudeWindowTone(claudeWindow) {
|
|
|
160
193
|
* needs no forecast slot: a disabled cap never carries a capHitAt (projectCapHit returns null
|
|
161
194
|
* for capUsd <= 0), and the fallback fragment only stands once the cap IS reached — the one
|
|
162
195
|
* state where the forecast is itself null — so the fragments can never compete for the line. */
|
|
163
|
-
export function budgetBadge(budget, claudeWindow) {
|
|
164
|
-
const window = claudeWindow ? claudeWindowBadge(claudeWindow) : "";
|
|
196
|
+
export function budgetBadge(budget, claudeWindow, now = Date.now(), chain) {
|
|
197
|
+
const window = (claudeWindow ? claudeWindowBadge(claudeWindow, now, chainMovedByWindow(chain)) : "") + chainBadge(chain);
|
|
165
198
|
// Subscription-only: the window stands IN PLACE of the daily spend — there is no priced
|
|
166
199
|
// spend for a dollar figure to report. Mixed and none fall through to the daily spend.
|
|
167
200
|
if (claudeWindow?.only)
|
|
168
201
|
return window;
|
|
169
202
|
if (budget.free)
|
|
170
203
|
return ` · budget: n/a${window}`;
|
|
171
|
-
// Only while the fallback is actually carrying the
|
|
172
|
-
// is spent, so the dollar figure alone would read like a stopped
|
|
204
|
+
// Only while the fallback is actually carrying the team (plans/fallback-model.md): the cap
|
|
205
|
+
// is spent, so the dollar figure alone would read like a stopped team. Naming the model
|
|
173
206
|
// answers the operator's next question — what is it running on now? Its cost is n/a by
|
|
174
207
|
// construction (the gate engages nothing else), so no second figure is shown. When the
|
|
175
208
|
// snapshot carries the per-tier map (tiers, part 7b/8 — non-null exactly when the tiers
|
|
@@ -179,12 +212,12 @@ export function budgetBadge(budget, claudeWindow) {
|
|
|
179
212
|
// identical to before either way.
|
|
180
213
|
const tierEntries = fallbackTierEntries(budget.tiers);
|
|
181
214
|
// The backend-default label when the engaged pair names neither half: `<agent> default`
|
|
182
|
-
// (plans/agent-backends.md part 4f/7), `pi default` for a pi
|
|
183
|
-
const agentDefault = `${budget.
|
|
184
|
-
const fallback =
|
|
215
|
+
// (plans/agent-backends.md part 4f/7), `pi default` for a pi team as before.
|
|
216
|
+
const agentDefault = `${budget.backend && budget.backend !== "pi" ? budget.backend : "pi"} default`;
|
|
217
|
+
const fallback = teamBudgetGate(budget) === "fallback"
|
|
185
218
|
? tierEntries.length >= 2
|
|
186
|
-
? ` ·
|
|
187
|
-
: ` ·
|
|
219
|
+
? ` · over-cap model: ${tierEntries.join(", ")} (cost n/a)`
|
|
220
|
+
: ` · over-cap model: ${fallbackDisplayPair(budget.fallback, agentDefault)} (cost n/a)`
|
|
188
221
|
: "";
|
|
189
222
|
const forecast = budget.capHitAt !== null
|
|
190
223
|
? ` · ~cap at ${clockHm(budget.capHitAt)}`
|
|
@@ -193,13 +226,20 @@ export function budgetBadge(budget, claudeWindow) {
|
|
|
193
226
|
return ` · budget: ${usd(budget.spentUsd)}/${usdCap(budget.capUsd)} today${forecast}${fallback}${window}`;
|
|
194
227
|
return ` · budget: ${usd(budget.spentUsd)} today · no cap${window}`;
|
|
195
228
|
}
|
|
196
|
-
/** The header's
|
|
197
|
-
*
|
|
229
|
+
/** The header's merge queue fragment (plans/merge-queue.md 4/5): `· merge queue: N` while any
|
|
230
|
+
* merge is queued or in flight, empty when the queue is idle — so an idle team keeps
|
|
198
231
|
* every existing header byte intact. One home for the rule, like buildBadge and budgetBadge:
|
|
199
232
|
* renderStatus renders it in the TUI/status header and status-payload.ts ships its output
|
|
200
233
|
* preformatted, so the GUI page cannot drift from this string. */
|
|
201
|
-
export function
|
|
202
|
-
return
|
|
234
|
+
export function mergeBadge(mergeQueue) {
|
|
235
|
+
return mergeQueue.depth > 0 ? ` · merge queue: ${mergeQueue.depth}` : "";
|
|
236
|
+
}
|
|
237
|
+
/** The header's `run --for` window fragment: ` · stops in <duration>` while the deadline is
|
|
238
|
+
* ahead, ` · stopping` once it has passed (the drain), empty for an unwindowed team. */
|
|
239
|
+
export function runWindowBadge(stopsAt, now) {
|
|
240
|
+
if (stopsAt === undefined)
|
|
241
|
+
return "";
|
|
242
|
+
return stopsAt > now ? ` · stops in ${humanUntil(stopsAt, now)}` : " · stopping";
|
|
203
243
|
}
|
|
204
244
|
/** The header's scheduled-wake fragment: `· wake scheduled in <duration> (<roles>)` while a
|
|
205
245
|
* `wake --in` marker stands, empty otherwise. It exists because a scheduled wake is otherwise
|
|
@@ -207,7 +247,7 @@ export function landingBadge(landQueue) {
|
|
|
207
247
|
export function wakeBadge(wake, now) {
|
|
208
248
|
if (!wake || wake.atMs <= now)
|
|
209
249
|
return "";
|
|
210
|
-
return ` · wake scheduled in ${
|
|
250
|
+
return ` · wake scheduled in ${humanUntil(wake.atMs, now)} (${wake.roles.join(", ")})`;
|
|
211
251
|
}
|
|
212
252
|
/** One loop's own pending-wake marker for its state cell: ` · wake in <duration>` for the
|
|
213
253
|
* earliest future wake naming `role` (or `instanceOf`, the base role an extra instance runs),
|
|
@@ -221,13 +261,13 @@ export function loopWakeMarker(wakes, role, instanceOf, now) {
|
|
|
221
261
|
if (at === undefined || w.atMs < at)
|
|
222
262
|
at = w.atMs;
|
|
223
263
|
}
|
|
224
|
-
return at === undefined ? "" : ` · wake in ${
|
|
264
|
+
return at === undefined ? "" : ` · wake in ${humanUntil(at, now)}`;
|
|
225
265
|
}
|
|
226
|
-
/** The header's
|
|
266
|
+
/** The header's team timed-pause fragment: `· paused — auto-resumes in <duration>` while a
|
|
227
267
|
* timed pause stands, empty otherwise — so an operator can tell at a glance whether a paused
|
|
228
|
-
*
|
|
229
|
-
* display out on purpose). The snapshot's `pausedUntil` is the
|
|
230
|
-
* epoch, plans/daily-cost-budget.md item 5), so the badge stands exactly when the
|
|
268
|
+
* team will come back on its own or needs a manual `resume` (the Timed pause plan left the
|
|
269
|
+
* display out on purpose). The snapshot's `pausedUntil` is the TEAM marker's deadline (ms
|
|
270
|
+
* epoch, plans/daily-cost-budget.md item 5), so the badge stands exactly when the team
|
|
231
271
|
* itself is timed-paused: a role-only timed pause leaves the header unchanged, and an absent
|
|
232
272
|
* or already-expired deadline renders nothing — the read side treats an expired marker as
|
|
233
273
|
* unpaused, so the badge never claims a countdown that is over. The duration goes through
|
|
@@ -249,10 +289,10 @@ export function pauseBadge(pausedUntil, now, reason) {
|
|
|
249
289
|
return "";
|
|
250
290
|
return left ? ` · paused — auto-resumes in ${left}${why}` : ` · paused${why}`;
|
|
251
291
|
}
|
|
252
|
-
/** The countdown a timed
|
|
292
|
+
/** The countdown a timed team pause has left, as humanSeconds — `null` when there is none
|
|
253
293
|
* (no deadline, or an already-expired one: the read side treats an expired marker as
|
|
254
|
-
* unpaused). The one home of the guard + ms→seconds rounding behind the
|
|
255
|
-
* countdowns, shared by pauseBadge's header fragment and
|
|
294
|
+
* unpaused). The one home of the guard + ms→seconds rounding behind the team's pause
|
|
295
|
+
* countdowns, shared by pauseBadge's header fragment and team-alerts.ts's paused alert
|
|
256
296
|
* title, so the two surfaces cannot disagree about how much time remains. The guard is this
|
|
257
297
|
* function's own; the ms→seconds rounding rides datetime.ts's secondsUntil. The GUI's pause
|
|
258
298
|
* control recomputes the ticking number client-side from the payload's raw `pausedUntil`
|
|
@@ -260,19 +300,19 @@ export function pauseBadge(pausedUntil, now, reason) {
|
|
|
260
300
|
export function pauseCountdown(pausedUntil, now) {
|
|
261
301
|
if (pausedUntil === undefined || pausedUntil <= now)
|
|
262
302
|
return null;
|
|
263
|
-
return
|
|
303
|
+
return humanUntil(pausedUntil, now);
|
|
264
304
|
}
|
|
265
305
|
/** The header's quiet-hours fragment (plans: "Quiet hours … part 2/2, observability"): the
|
|
266
306
|
* schedule as standing information in every configured state — `· quiet until 07:00` while
|
|
267
307
|
* the local clock sits inside the window (the active reading answers "why are the loops
|
|
268
308
|
* idle?" with when they start again) and `· quiet 23:00-07:00` otherwise — and empty when
|
|
269
|
-
* the snapshot carries no window, so a
|
|
309
|
+
* the snapshot carries no window, so a team without the schedule keeps its header
|
|
270
310
|
* byte-identical. The window end comes from quiet-hours.ts's quietWindowEnd — read back out of
|
|
271
311
|
* the window string, not re-parsed into minutes — so the badge renders the operator's own
|
|
272
312
|
* spelling.
|
|
273
313
|
* renderStatus renders it in the TUI/status header after the pause badge; the GUI page
|
|
274
314
|
* renders its sidebar chip from the payload's raw fields and its active indicator from
|
|
275
|
-
*
|
|
315
|
+
* team-alerts' quiet alert, so both dashboards say the same thing. */
|
|
276
316
|
export function quietBadge(quietHours, inQuietHours) {
|
|
277
317
|
if (!quietHours)
|
|
278
318
|
return "";
|
|
@@ -281,25 +321,36 @@ export function quietBadge(quietHours, inQuietHours) {
|
|
|
281
321
|
}
|
|
282
322
|
/** The header's disk-floor fragment (plans/disk-floor.md, part 4/4): shown while the disk hold
|
|
283
323
|
* is on (`— holding new work`) or free space sits below the reclaim threshold, so an operator
|
|
284
|
-
* sees why the
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
|
|
290
|
-
* the TUI/status header and status-payload.ts ships it preformatted. `now` pins the reclaim
|
|
291
|
-
* age (the same elapsed-seconds rule status-render.ts's own `ago` uses). */
|
|
292
|
-
export function diskBadge(disk, now = Date.now()) {
|
|
324
|
+
* sees why the team stopped without reading events. A finished reclaim is not standing
|
|
325
|
+
* information here; it surfaces as a `disk_reclaim` item in the Activity log's Notable feed.
|
|
326
|
+
* Empty when the snapshot carries no disk block (an older orchestrator, or an unmeasurable
|
|
327
|
+
* volume) and space is comfortable. One home for the rule, like budgetBadge: renderStatus
|
|
328
|
+
* renders it in the TUI/status header and status-payload.ts ships it preformatted. */
|
|
329
|
+
export function diskBadge(disk) {
|
|
293
330
|
if (!disk)
|
|
294
331
|
return "";
|
|
295
332
|
const free = `${formatGB(disk.freeGB)} free`;
|
|
296
|
-
|
|
333
|
+
return disk.held
|
|
297
334
|
? ` · disk ${free} — holding new work`
|
|
298
335
|
: disk.reclaimGB > 0 && disk.freeGB < disk.reclaimGB
|
|
299
336
|
? ` · disk ${free}`
|
|
300
337
|
: "";
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
338
|
+
}
|
|
339
|
+
/** The permit pool's header fragment (plans/provider-concurrency.md §8): `slots 5/8 · omlx
|
|
340
|
+
* 3/3 · claude 2/6` — the team total's in-use/cap followed by each capped provider key's own
|
|
341
|
+
* in-use/cap. The WHOLE fragment appears only while at least one key carries a cap (the
|
|
342
|
+
* `maxConcurrentPerProvider` map is set), so a team without it renders the header exactly
|
|
343
|
+
* as before; uncapped keys that happen to be in the snapshot are dropped rather than shown
|
|
344
|
+
* with a bare in-use that would read like a cap. */
|
|
345
|
+
export function permitsBadge(permits) {
|
|
346
|
+
if (!permits)
|
|
347
|
+
return "";
|
|
348
|
+
const providers = Object.entries(permits.byKey)
|
|
349
|
+
.filter(([, e]) => e.limit !== undefined)
|
|
350
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
351
|
+
.map(([k, e]) => `${k} ${e.inUse}/${e.limit}`)
|
|
352
|
+
.join(" · ");
|
|
353
|
+
if (providers.length === 0)
|
|
354
|
+
return "";
|
|
355
|
+
return ` · running ${permits.inUse} of ${permits.limit} · ${providers}`;
|
|
305
356
|
}
|