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,107 +1,108 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
2
|
-
import { baseRoleOf
|
|
3
|
+
import { baseRoleOf } from "../roles/loop-ids.js";
|
|
4
|
+
import { heldByMainRed } from "../status/status-polls.js";
|
|
3
5
|
import { ERROR_STREAK_WARN, QUIET_KILL_RESUME_LIMIT } from "../tick/tick-apply.js";
|
|
4
6
|
import { readLiveProgress } from "./progress-data.js";
|
|
5
7
|
import { quietWindowEnd } from "../scheduling/quiet-hours.js";
|
|
6
8
|
import { humanSeconds, secondsUntil } from "../text/datetime.js";
|
|
7
9
|
import { duration, inFlightDetail, inFlightLabel, progressKind, tickProgress, workingDetail, } from "./tick-progress-model.js";
|
|
8
|
-
import {
|
|
9
|
-
/** The snapshot's in-flight
|
|
10
|
-
* role's change record while the pipeline holds it —
|
|
11
|
-
* keeping it for the merge slot (`
|
|
10
|
+
import { mergeChanges, MERGE_STAGE_LABELS } from "../merge/merge-slot.js";
|
|
11
|
+
/** The snapshot's in-flight merge record (merge queue 4/5) filtered to one role: this
|
|
12
|
+
* role's change record while the pipeline holds it — verifying or merging it (`merge`), or
|
|
13
|
+
* keeping it for the merge slot (`approved`) — and null otherwise, including once the merge is
|
|
12
14
|
* `done` with it (a change that reached its outcome must not keep a live label while the rest of
|
|
13
|
-
* its stack
|
|
14
|
-
* `
|
|
15
|
-
* every phase call site derives its `
|
|
15
|
+
* its stack merges — BUGS.md 2026-09-23). An older generation's one-change marker reads as one
|
|
16
|
+
* `merge` record (mergeChanges). The single home of the "is this role merge" filter —
|
|
17
|
+
* every phase call site derives its `merge` argument through this so the filtering cannot
|
|
16
18
|
* drift between them. */
|
|
17
|
-
export function
|
|
18
|
-
if (!
|
|
19
|
+
export function mergeForRole(mergeQueue, role) {
|
|
20
|
+
if (!mergeQueue.inFlight)
|
|
19
21
|
return null;
|
|
20
|
-
const change =
|
|
22
|
+
const change = mergeChanges(mergeQueue.inFlight).find((c) => c.role === role);
|
|
21
23
|
return change && change.status !== "done"
|
|
22
|
-
? { status: change.status, startedAt: change.startedAt, stage: change.stage }
|
|
24
|
+
? { status: change.status, startedAt: change.startedAt, stage: change.stage, stageSince: change.stageSince }
|
|
23
25
|
: null;
|
|
24
26
|
}
|
|
25
|
-
/**
|
|
26
|
-
const LANDING_STAGE_LABELS = {
|
|
27
|
-
rebasing: "rebasing",
|
|
28
|
-
"check-wait": "waiting for a check slot",
|
|
29
|
-
"build-check": "build check",
|
|
30
|
-
reviewing: "reviewing",
|
|
31
|
-
"resolve-wait": "waiting to resolve conflicts",
|
|
32
|
-
resolving: "resolving conflicts",
|
|
33
|
-
merging: "merging",
|
|
34
|
-
};
|
|
35
|
-
/** The work text a row shows in place of live progress while it is idle or landing
|
|
27
|
+
/** The work text a row shows in place of live progress while it is idle or merge
|
|
36
28
|
* (plans/parallel-work-instances.md "Observability", part 6/7): the claim's heading when the
|
|
37
|
-
* instance holds one
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export function idleClaimText(s, phase) {
|
|
42
|
-
if (s.claim)
|
|
43
|
-
return s.claim.title;
|
|
44
|
-
if (phase === "queued" && instanceIndex(s.role) >= 2)
|
|
45
|
-
return `idle — no unclaimed ${baseRoleOf(s.role) === "bugfix" ? "bugs" : "plans"}`;
|
|
46
|
-
return null;
|
|
29
|
+
* instance holds one. Returns null otherwise, so the state cell keeps its plain phase — an
|
|
30
|
+
* idle instance's phase label (`idle: no unclaimed plans`) carries its own reason. */
|
|
31
|
+
export function idleClaimText(s, _phase) {
|
|
32
|
+
return s.claim ? s.claim.title : null;
|
|
47
33
|
}
|
|
48
34
|
/** The loop's state label — the one precedence ladder shared by the status table and both
|
|
49
35
|
* dashboards (renderStatus, statusPayload). First match wins: a stopped harness; an in-flight
|
|
50
|
-
*
|
|
36
|
+
* merge (the role's tick already ended "queued" while its change merges, so the marker
|
|
51
37
|
* supplies the label); a running tick ("working", or "reviewing" while the review gate holds
|
|
52
|
-
* it — in-flight work finishes through both
|
|
38
|
+
* it — in-flight work finishes through both team gates); the director's "waiting for
|
|
53
39
|
* prompts" (exempt from those gates); user pause; budget pause; a red main ("main red"); an
|
|
54
|
-
* error or quiet-kill streak at its threshold ("failing"); sleep; then
|
|
40
|
+
* error or quiet-kill streak at its threshold ("failing"); sleep; then the scheduling
|
|
41
|
+
* poll's recorded skip (`idle: …` / `deferred (…)`, from LoopState.skipReason), else
|
|
42
|
+
* "queued".
|
|
55
43
|
* Pass `root` so an in-flight tick expands into live detail (elapsed · turn · ctx · tool);
|
|
56
44
|
* without it a working loop shows plain "working". `live`, when given, is the frame's
|
|
57
45
|
* precomputed tail (see workingDetail) — it skips the log read; without it an in-flight tick
|
|
58
|
-
* reads on its own. `budgetPaused` marks the
|
|
46
|
+
* reads on its own. `budgetPaused` marks the team's daily cost budget as reached: idle role
|
|
59
47
|
* loops show `budget paused` instead of their sleep/queue state — that is why they are not
|
|
60
48
|
* ticking. `userPaused` marks an operator pause (`tumwater pause` marker present): idle role
|
|
61
49
|
* loops show `paused`, checked before the budget gate because user intent is more specific
|
|
62
50
|
* than spend state — while both hold, "paused" tells the operator what to do (`resume`).
|
|
63
|
-
* `
|
|
64
|
-
*
|
|
65
|
-
* change the pipeline holds but is not working on reads `
|
|
66
|
-
* elapsed since nothing of its own is running: its
|
|
67
|
-
* slot. (A queued change whose
|
|
51
|
+
* `merge`, when given for this role, is its change's record in the snapshot's in-flight
|
|
52
|
+
* merge — build it with mergeForRole so the marker's role filter lives in one place. A
|
|
53
|
+
* change the pipeline holds but is not working on reads `approved, awaiting merge`, with no
|
|
54
|
+
* elapsed since nothing of its own is running: its verification approved it and it waits for the
|
|
55
|
+
* merge slot. (A queued change whose verification is still parked for a permit has no record:
|
|
56
|
+
* it reads
|
|
68
57
|
* plainly queued.)
|
|
69
58
|
* `mainCheck`, when given (loopRowCells passes the snapshot's), lets the main-red label
|
|
70
59
|
* retire itself between ticks: main's newest merge-scope check passing AFTER this loop's red
|
|
71
60
|
* tick ended means main is green now — the loop's `main_red` verdict is stale until its next
|
|
72
61
|
* tick, which sleep, a full slot pool, a pause, or a spent budget can defer for hours
|
|
73
62
|
* (BUGS.md 2026-09-30), so the label falls through to the ordinary idle state and the
|
|
74
|
-
*
|
|
63
|
+
* teamAlerts banner follows. A check that is newer but failed keeps the label (main is
|
|
75
64
|
* genuinely red); so does one older than the red tick or with no known pass at all — the
|
|
76
65
|
* blockage stands until the loop's own next tick re-verdicts it.
|
|
77
|
-
* Otherwise its elapsed is the CHANGE's own
|
|
66
|
+
* Otherwise its elapsed is the CHANGE's own merge (its record's startedAt — from its verification's
|
|
78
67
|
* start, never another change's), never the authoring tick's,
|
|
79
|
-
* and its stage scopes the cell to the phase the
|
|
80
|
-
* the reviewer run's live detail exactly as a reviewing tick does (`
|
|
68
|
+
* and its stage scopes the cell to the phase the merge is actually in: `reviewing` carries
|
|
69
|
+
* the reviewer run's live detail exactly as a reviewing tick does (`merge 3m · reviewing ·
|
|
81
70
|
* turn 2 · ctx 18.0k · bash npm test`), and `resolving` the conflict resolver's the same way;
|
|
82
|
-
* `build-check`, `resolve-wait` and `merging` render just the stage (`
|
|
71
|
+
* `build-check`, `resolve-wait` and `merging` render just the stage (`merge 1m · build
|
|
83
72
|
* check`) and never read the log — no reviewer or resolver is running then, and the log's
|
|
84
73
|
* newest run is typically a finished one (the author's, a previous review) whose turns are
|
|
85
74
|
* stale and whose silence would read as a false stall; a record with no stage (an older
|
|
86
75
|
* writer mid-upgrade) keeps the bare elapsed label. */
|
|
87
|
-
export function
|
|
76
|
+
export function onErrorStreak(s) {
|
|
77
|
+
// An error streak at or past the warning threshold (tick-apply.ts's ERROR_STREAK_WARN), or
|
|
78
|
+
// a quiet-kill streak at the give-up threshold (BUGS.md 2026-09-18): the exact condition
|
|
79
|
+
// that makes loopPhase read "failing", split out so the team alert (team-alerts.ts) can
|
|
80
|
+
// key its red "failing tick after tick" banner on the streak rather than on the rendered
|
|
81
|
+
// phase — an in-flight tick hides the streak behind a `working`/`merge` label (loopPhase
|
|
82
|
+
// ranks running work above failing), but the alert must stay up while the loop's next tick
|
|
83
|
+
// runs and clear only when the streak resets (BUGS.md 2026-10-14).
|
|
84
|
+
return ((s.consecutiveErrors ?? 0) >= ERROR_STREAK_WARN ||
|
|
85
|
+
(s.lastResult === "quiet_killed" && (s.quietKillStreak ?? 0) >= QUIET_KILL_RESUME_LIMIT));
|
|
86
|
+
}
|
|
87
|
+
export function loopPhase(s, orchestratorRunning, root, budgetPaused = false, live, userPaused = false, merge, mainCheck, capPaused = false, quietHold, diskHeld = false, bootstrapHeld = false, maintenanceQuotaHold, permits, pausedUntilMs) {
|
|
88
88
|
if (!orchestratorRunning)
|
|
89
89
|
return "stopped";
|
|
90
|
-
// Merge queue 4/5 — the marker-driven
|
|
91
|
-
// passed gets it (callers derive it via
|
|
92
|
-
// shows it — a dead
|
|
93
|
-
if (
|
|
94
|
-
if (
|
|
95
|
-
return "
|
|
96
|
-
const head =
|
|
97
|
-
if (!
|
|
90
|
+
// Merge queue 4/5 — the marker-driven merge label: only a role whose change record was
|
|
91
|
+
// passed gets it (callers derive it via mergeForRole), and a stopped harness never
|
|
92
|
+
// shows it — a dead team's marker is stale by definition.
|
|
93
|
+
if (merge) {
|
|
94
|
+
if (merge.status === "approved")
|
|
95
|
+
return "approved, waiting to merge";
|
|
96
|
+
const head = merge.startedAt === undefined ? "merging" : `merging ${duration(Date.now() - merge.startedAt)}`;
|
|
97
|
+
if (!merge.stage)
|
|
98
98
|
return head;
|
|
99
|
-
const
|
|
100
|
-
|
|
99
|
+
const base = `${head} · ${MERGE_STAGE_LABELS[merge.stage]}`;
|
|
100
|
+
const staged = merge.stageSince === undefined ? base : `${base} ${duration(Date.now() - merge.stageSince)}`;
|
|
101
|
+
if (merge.stage !== "reviewing" && merge.stage !== "resolving")
|
|
101
102
|
return staged;
|
|
102
103
|
// The reviewer writes the role's own log from its leased pooled slot, and the conflict
|
|
103
104
|
// resolver from the merge worktree, both as gate runs (loop-pi.ts): the GATE accumulator,
|
|
104
|
-
// read here rather than taken from `live` — the
|
|
105
|
+
// read here rather than taken from `live` — the merge role's tick has ended, so the
|
|
105
106
|
// frame's per-running-loop tail is not this run's (callers pass null for it). Each run's
|
|
106
107
|
// `tumwater_run` marker resets that accumulator, so neither shows the other's counts.
|
|
107
108
|
return inFlightDetail(staged, root ? readLiveProgress(root, s.role, "gate") : null);
|
|
@@ -109,10 +110,22 @@ export function loopPhase(s, orchestratorRunning, root, budgetPaused = false, li
|
|
|
109
110
|
if (s.running) {
|
|
110
111
|
// A parked waiter is reserved against double-scheduling but holds no maxConcurrent permit
|
|
111
112
|
// and runs no pi yet: render its true state and keep it OUT of the active set (isActivePhase)
|
|
112
|
-
// so the operator can count active rows against the cap — a
|
|
113
|
+
// so the operator can count active rows against the cap — a merge visibly counts, a
|
|
113
114
|
// parked waiter visibly does not (BUGS.md 2026-09-24).
|
|
114
|
-
if (s.parkedSince)
|
|
115
|
-
|
|
115
|
+
if (s.parkedSince) {
|
|
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
|
+
return `waiting to start ${parkedTime}`;
|
|
128
|
+
}
|
|
116
129
|
// The director bypasses maxConcurrent (`usesSlot` is false for it), so its in-flight tick
|
|
117
130
|
// is a state of its own: a distinct label that `isActivePhase`'s cap-mirroring set
|
|
118
131
|
// excludes while still carrying the live detail, so its work stays visible without
|
|
@@ -135,32 +148,43 @@ export function loopPhase(s, orchestratorRunning, root, budgetPaused = false, li
|
|
|
135
148
|
return root ? workingDetail(root, s, live) : "working";
|
|
136
149
|
}
|
|
137
150
|
if (s.role === DIRECTOR_ROLE)
|
|
138
|
-
return "
|
|
139
|
-
if (userPaused)
|
|
151
|
+
return "ready for messages"; // exempt from both gates
|
|
152
|
+
if (userPaused) {
|
|
153
|
+
// A timed per-role pause (`pause --role <id> --for`): the marker's `until` gives the
|
|
154
|
+
// operator a countdown, mirroring the team header badge and the GUI loop-list's
|
|
155
|
+
// `· resumes in <s>` — an indefinite per-role pause or a team-wide pause (deadline
|
|
156
|
+
// absent) keeps today's bare `paused`, so the surfaces cannot drift.
|
|
157
|
+
if (pausedUntilMs !== undefined && pausedUntilMs > Date.now()) {
|
|
158
|
+
return `paused · resumes in ${humanSeconds(Math.max(0, Math.round((pausedUntilMs - Date.now()) / 1000)))}`;
|
|
159
|
+
}
|
|
160
|
+
// An absent deadline (indefinite per-role pause, team-wide pause) or an expired one
|
|
161
|
+
// (the marker team-state already reads as unpaused) keeps today's bare `paused` — never
|
|
162
|
+
// a stale `resumes in 0s`.
|
|
140
163
|
return "paused";
|
|
164
|
+
}
|
|
141
165
|
// The loop's OWN daily cap: the more specific spend
|
|
142
|
-
// state, so it names itself ahead of the
|
|
166
|
+
// state, so it names itself ahead of the team's budget pause — the ladder's existing
|
|
143
167
|
// "user intent is more specific than spend state" argument extended one level down.
|
|
144
168
|
if (capPaused)
|
|
145
|
-
return "cap
|
|
146
|
-
// The loop's OWN quiet-hours window (PLANS.md, quietHoursPerRole): the
|
|
169
|
+
return "waiting: daily cap spent";
|
|
170
|
+
// The loop's OWN quiet-hours window (PLANS.md, quietHoursPerRole): the team gate's own
|
|
147
171
|
// wording — `quiet until <end>` — scoped to the one loop the operator scheduled. Shown
|
|
148
|
-
// after the cap and before the
|
|
172
|
+
// after the cap and before the team's budget pause: the loop's own schedule is more
|
|
149
173
|
// specific than either.
|
|
150
174
|
if (quietHold)
|
|
151
175
|
return `quiet until ${quietWindowEnd(quietHold)}`;
|
|
152
176
|
if (budgetPaused)
|
|
153
|
-
return "budget
|
|
154
|
-
// The disk floor (plans/disk-floor.md, part 4/4): the
|
|
155
|
-
// floor, so an idle loop reads "disk hold" — the
|
|
177
|
+
return "waiting: budget spent";
|
|
178
|
+
// The disk floor (plans/disk-floor.md, part 4/4): the team starts no new work below the
|
|
179
|
+
// floor, so an idle loop reads "disk hold" — the team-wide gate's own wording, ranked with
|
|
156
180
|
// the budget pause (LOOP_RANK_RULES). In-flight ticks keep their live detail above, exactly
|
|
157
181
|
// as under every autonomous gate.
|
|
158
182
|
if (diskHeld)
|
|
159
|
-
return "disk
|
|
183
|
+
return "waiting: low disk";
|
|
160
184
|
// New-project bootstrap (plans/work-ratio.md, part 2/2): the idle loop is held until enough
|
|
161
185
|
// plans are Done. The literal label names the reason, like the gates above.
|
|
162
186
|
if (bootstrapHeld)
|
|
163
|
-
return "
|
|
187
|
+
return "waiting: bootstrap";
|
|
164
188
|
// The maintenance allowance's own hold (plans/work-ratio.md, part 1b/4): the label carries the
|
|
165
189
|
// figures so the operator sees how far over the window is; the string is preformatted by the
|
|
166
190
|
// snapshot's holder, like roleQuietPaused's window.
|
|
@@ -168,18 +192,18 @@ export function loopPhase(s, orchestratorRunning, root, budgetPaused = false, li
|
|
|
168
192
|
return maintenanceQuotaHold;
|
|
169
193
|
// Main's own suite is known red at this loop's last tick: code-producing loops are blocked
|
|
170
194
|
// from authoring until main is green (the red-main baseline check). Shown before sleep/queue
|
|
171
|
-
// because it explains why the loop keeps waking and
|
|
195
|
+
// because it explains why the loop keeps waking and merge nothing; self-correcting, since
|
|
172
196
|
// each blocked tick re-records main_red while red and a green wake overwrites lastResult.
|
|
173
197
|
// Except a green wake that queues a change: `queued` is in flight, not a completed result,
|
|
174
|
-
// so it leaves the main_red pair in place until its
|
|
198
|
+
// so it leaves the main_red pair in place until its merge resolves (tick-apply.ts's
|
|
175
199
|
// applyTickOutcome) — its stashed summary is the tell that the loop's latest tick got past
|
|
176
200
|
// the red-main gate, so main was green then and "main red" would be stale.
|
|
177
201
|
// Except too when main's newest merge-scope check PASSED after this loop's red tick ended:
|
|
178
|
-
// the
|
|
202
|
+
// the team-level verdict is fresher than the loop's own, so the label is stale until the
|
|
179
203
|
// loop's next tick — which can be hours away (BUGS.md 2026-09-30). A check older than the
|
|
180
204
|
// red tick, a failed or skipped one, or an unknown tick end all keep the label.
|
|
181
|
-
if (s
|
|
182
|
-
return "main
|
|
205
|
+
if (heldByMainRed(s, mainCheck))
|
|
206
|
+
return "main's check is failing";
|
|
183
207
|
// An error streak at or past the warning threshold (tick-apply.ts's ERROR_STREAK_WARN): the
|
|
184
208
|
// loop is retrying the same failure on the error ladder, and the operator must see
|
|
185
209
|
// "failing" — not a sleepy label — while it is stuck (BUGS.md 2026-09-15). The streak
|
|
@@ -188,40 +212,62 @@ export function loopPhase(s, orchestratorRunning, root, budgetPaused = false, li
|
|
|
188
212
|
// backend must still read "failing" (BUGS.md 2026-09-21). Self-clearing: the first
|
|
189
213
|
// healthy tick resets the streak, and each retry re-arms it while the environment stays
|
|
190
214
|
// broken.
|
|
191
|
-
if ((s
|
|
192
|
-
return "failing";
|
|
193
|
-
// A quiet-kill streak at the give-up threshold reads "failing" too (BUGS.md 2026-09-18):
|
|
194
|
-
// the loop is retrying a session the backend will not schedule, and before this it looked
|
|
195
|
-
// exactly like a sleeping loop while it burned an hour of slot time per tick.
|
|
196
|
-
if (s.lastResult === "quiet_killed" && (s.quietKillStreak ?? 0) >= QUIET_KILL_RESUME_LIMIT)
|
|
215
|
+
if (onErrorStreak(s))
|
|
197
216
|
return "failing";
|
|
217
|
+
// The resting state cell (sleeping / idle / deferred / queued): it is the only label that
|
|
218
|
+
// names what a loop has been RETURNING, so the consecutive no-change streak rides on it
|
|
219
|
+
// (tick-apply.ts applyTickOutcome's noChangeStreak, persisted in LoopState). A loop that
|
|
220
|
+
// keeps waking and returning nothing to do reads " · no change ×N" instead of an ordinary
|
|
221
|
+
// idle — the harness premise's silent no-op failure mode, otherwise indistinguishable from a
|
|
222
|
+
// healthy idle loop. Observational only: a legitimately idle maintenance loop with an empty
|
|
223
|
+
// backlog can hold a long streak and that is expected, so it never warns (unlike
|
|
224
|
+
// consecutiveErrors/quietKillStreak). N=1 and an absent field read 0 and render the label
|
|
225
|
+
// byte-identically; the error/quiet-kill/merge/paused cells return earlier and never carry
|
|
226
|
+
// the suffix. `noChangeStreak` is on the state accumulator, so its type is on the full
|
|
227
|
+
// LoopState.
|
|
228
|
+
let cell;
|
|
198
229
|
if (s.nextRunAt > Date.now()) {
|
|
199
230
|
// The loop is sleeping *now* until nextRunAt: show the remaining sleep duration
|
|
200
231
|
// ("for 30m"), not a future start ("in 30m"). Floor at 1s so a sub-second remainder
|
|
201
232
|
// never renders as "sleeping (for now)".
|
|
202
233
|
const remain = Math.max(1, secondsUntil(s.nextRunAt, Date.now()));
|
|
203
|
-
|
|
234
|
+
cell = `sleeping (for ${humanSeconds(remain)})`;
|
|
204
235
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
236
|
+
else {
|
|
237
|
+
switch (s.skipReason) {
|
|
238
|
+
case "no-free-entry":
|
|
239
|
+
cell = `idle: no unclaimed ${baseRoleOf(s.role) === "bugfix" ? "bugs" : "plans"}`;
|
|
240
|
+
break;
|
|
241
|
+
case "claim-hold":
|
|
242
|
+
cell = `idle: another ${baseRoleOf(s.role)} instance is working on it`;
|
|
243
|
+
break;
|
|
244
|
+
case "deferred-backlog":
|
|
245
|
+
cell = "deferred (backlog open)";
|
|
246
|
+
break;
|
|
247
|
+
case "deferred-no-work":
|
|
248
|
+
cell = "deferred (no new work)";
|
|
249
|
+
break;
|
|
250
|
+
case "deferred-plans-full":
|
|
251
|
+
cell = "deferred (plans full)";
|
|
252
|
+
break;
|
|
253
|
+
default:
|
|
254
|
+
cell = "due";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Two or more consecutive no-change ticks earn the suffix (N=1 is a single quiet tick —
|
|
258
|
+
// ordinary, not yet a pattern).
|
|
259
|
+
const streak = s.noChangeStreak ?? 0;
|
|
260
|
+
if (streak >= 2)
|
|
261
|
+
cell += ` · no change ×${streak}`;
|
|
262
|
+
return cell;
|
|
217
263
|
}
|
|
218
264
|
/** One loop row's display derivation — the block both observer surfaces re-ran per loop
|
|
219
265
|
* (renderStatus's withMetrics map and statusPayload's loops map): a single live tail read
|
|
220
266
|
* (the running-and-not-parked gate, read at the phase's progressKind), the token metrics
|
|
221
267
|
* over that tail, and the rendered phase string. The phase's snapshot-level gates are
|
|
222
268
|
* derived here too — budget paused (only `paused` stops a loop; `fallback` keeps it ticking
|
|
223
|
-
* on the free model, which the header badge names), the
|
|
224
|
-
* (one `paused` flag to loopPhase covers both), and the role's
|
|
269
|
+
* on the free model, which the header badge names), the team pause plus the per-role one
|
|
270
|
+
* (one `paused` flag to loopPhase covers both), and the role's merge cell (merge queue
|
|
225
271
|
* 4/5) — so the two dashboards' per-loop cells cannot drift apart. */
|
|
226
272
|
export function loopRowCells(snap, root, s) {
|
|
227
273
|
const live = s.running && !s.parkedSince
|
|
@@ -230,44 +276,60 @@ export function loopRowCells(snap, root, s) {
|
|
|
230
276
|
const m = displayTokenMetrics(root, s, live);
|
|
231
277
|
const phase = loopPhase(s, snap.running, root,
|
|
232
278
|
// The per-tier budget pause (part 5c/8): a loop whose model tier resolved to no usable
|
|
233
|
-
// free pair reads `budget paused` — per role, not the
|
|
234
|
-
// strong pair no longer paints the whole
|
|
279
|
+
// free pair reads `budget paused` — per role, not the team-wide verdict, so a demoted
|
|
280
|
+
// strong pair no longer paints the whole team paused while small and default keep
|
|
235
281
|
// ticking on their own pairs.
|
|
236
282
|
snap.budgetPausedRoles.includes(s.role), live,
|
|
237
|
-
// The per-role pause reads the same `paused` cell as the
|
|
283
|
+
// The per-role pause reads the same `paused` cell as the team pause: one flag to
|
|
238
284
|
// loopPhase covers both gates (status-model has no per-role branch of its own).
|
|
239
|
-
snap.paused || snap.
|
|
285
|
+
snap.paused || snap.pausedLoops.includes(s.role), mergeForRole(snap.mergeQueue, s.role),
|
|
240
286
|
// The snapshot's newest merge-scope check lets a stale main-red phase retire itself
|
|
241
|
-
// between ticks (BUGS.md 2026-09-30) — the banner (
|
|
287
|
+
// between ticks (BUGS.md 2026-09-30) — the banner (teamAlerts) reads this same phase.
|
|
242
288
|
snap.mainCheck,
|
|
243
289
|
// The per-role cap's verdict, computed once per poll in the snapshot (status/status-data.ts
|
|
244
290
|
// roleCapPaused): an idle loop over its own cap reads `cap paused` on every surface.
|
|
245
291
|
snap.capPaused.includes(s.role),
|
|
246
292
|
// The per-role quiet window's verdict (status/status-data.ts roleQuietPaused): an idle loop
|
|
247
|
-
// inside its own window reads `quiet until <end>` — the
|
|
293
|
+
// inside its own window reads `quiet until <end>` — the team badge's wording, scoped.
|
|
248
294
|
snap.roleQuietPaused?.[s.role],
|
|
249
295
|
// The disk floor's verdict (status/status-data.ts disk): an idle loop while the hold is on
|
|
250
|
-
// reads `disk hold` — the
|
|
296
|
+
// reads `disk hold` — the team-wide gate, ranked with the budget pause.
|
|
251
297
|
snap.disk?.held === true,
|
|
252
298
|
// The new-project bootstrap's verdict (status/status-data.ts bootstrapHeld): an idle loop
|
|
253
|
-
// it holds reads `held: bootstrap` — the
|
|
299
|
+
// it holds reads `held: bootstrap` — the team gate's own wording.
|
|
254
300
|
snap.bootstrapHeld.includes(s.role),
|
|
255
301
|
// The maintenance allowance's verdict (status/status-data.ts maintenanceQuota): an idle loop
|
|
256
|
-
// it holds reads `held: maintenance quota <used>/<allowance>` — the
|
|
302
|
+
// it holds reads `held: maintenance quota <used>/<allowance>` — the team gate's figures.
|
|
257
303
|
snap.maintenanceQuota.held.includes(s.role)
|
|
258
|
-
? `
|
|
259
|
-
: undefined
|
|
304
|
+
? `waiting: maintenance allowance used ${snap.maintenanceQuota.used}/${snap.maintenanceQuota.allowance}`
|
|
305
|
+
: undefined,
|
|
306
|
+
// The permit pool's live state (plans/provider-concurrency.md §8): names the parked
|
|
307
|
+
// waiter's provider key in `awaiting <key> slot` when that key is at its cap.
|
|
308
|
+
snap.permits,
|
|
309
|
+
// A timed per-role pause's deadline — only when the loop itself is individually paused
|
|
310
|
+
// (in `pausedRoles`) and not under a team-wide pause, matching the GUI's phaseDetail
|
|
311
|
+
// guard, so the team pause (whose countdown rides the header badge) never double-appends.
|
|
312
|
+
snap.paused || !snap.pausedLoops.includes(s.role) ? undefined : snap.pausedRolesUntil);
|
|
260
313
|
return { live, generated: m.generated, peakCtx: m.peakCtx, phase };
|
|
261
314
|
}
|
|
262
|
-
/** The phase prefixes that make a loop a permit holder — working, reviewing,
|
|
315
|
+
/** The phase prefixes that make a loop a permit holder — working, reviewing, merge. The
|
|
263
316
|
* browser dashboard (gui-client.ts) interpolates this list into its isActivePhase, exactly as
|
|
264
317
|
* gui-client-model.ts interpolates LOOP_RANK_RULES, so the two dashboards cannot disagree
|
|
265
318
|
* about which phases are in flight. */
|
|
266
|
-
export const ACTIVE_PHASE_PREFIXES = ["working", "reviewing", "
|
|
319
|
+
export const ACTIVE_PHASE_PREFIXES = ["working", "reviewing", "merging"];
|
|
320
|
+
/** Whether `key` is the binding limit in the published permit snapshot: its cap is set and
|
|
321
|
+
* its in-use already meets or exceeds it, so the pool is granting nothing more on that key and
|
|
322
|
+
* a waiter carrying it is queued on the key, not on the team total. A key without a cap in
|
|
323
|
+
* the snapshot (uncapped) is never binding. One home for the rule, shared by status-model's
|
|
324
|
+
* `awaiting <key> slot` and the GUI chip's provider detail. */
|
|
325
|
+
function isBindingLimit(permits, key) {
|
|
326
|
+
const entry = permits.byKey[key];
|
|
327
|
+
return entry?.limit !== undefined && entry.inUse >= entry.limit;
|
|
328
|
+
}
|
|
267
329
|
/** Is this phase one of the PERMIT-HOLDER in-flight states — the rows an operator counts
|
|
268
330
|
* against maxConcurrent? The three labels come from loopPhase: `working …`, `reviewing …`,
|
|
269
|
-
* and the marker-driven `
|
|
270
|
-
* slot (`
|
|
331
|
+
* and the marker-driven `merge …` (merge queue 4/5) — a approved change waiting for the merge
|
|
332
|
+
* slot (`approved, awaiting merge`) runs nothing, so it is not in flight. The running director's
|
|
271
333
|
* `director working …` label is deliberately outside the set: the director never calls
|
|
272
334
|
* `semaphore.acquire`, so the cap-mirroring rows are exactly the permit holders (BUGS.md
|
|
273
335
|
* 2026-10-06). Named once so the two rendered tables and their lockstep test share the rule
|
|
@@ -281,11 +343,11 @@ export function isActivePhase(phase) {
|
|
|
281
343
|
/** A loop's rank in the rendered tables, from its phase label (loopPhase): -1 a RUNNING
|
|
282
344
|
* DIRECTOR — live work of its own, outside the maxConcurrent cap it bypasses, so it groups
|
|
283
345
|
* apart and never inflates the permit-holder count (BUGS.md 2026-10-06); 0 live work —
|
|
284
|
-
* working, reviewing,
|
|
346
|
+
* working, reviewing, merge (isActivePhase); 1 work waiting in the pipeline — a approved change
|
|
285
347
|
* awaiting the merge slot, or a tick parked awaiting a permit; 2 needs attention — failing, or
|
|
286
348
|
* blocked by a red main; 3 paused — by the operator, by the budget, or by the loop's own
|
|
287
349
|
* per-role cap; 4 everything else (idle: sleeping, queued, the director waiting for prompts, a
|
|
288
|
-
* stopped
|
|
350
|
+
* stopped team). The dashboard groups its loop table by these ranks; the TUI/status table
|
|
289
351
|
* orders by them.
|
|
290
352
|
*
|
|
291
353
|
* The rank → phase mapping is LOOP_RANK_RULES, one home for the phase literals so the
|
|
@@ -295,10 +357,10 @@ export function isActivePhase(phase) {
|
|
|
295
357
|
export const LOOP_RANK_RULES = [
|
|
296
358
|
{ rank: -1, prefixes: ["director working"] },
|
|
297
359
|
{ rank: 0, active: true },
|
|
298
|
-
{ rank: 1, prefixes: ["
|
|
299
|
-
{ rank: 2, phases: ["failing", "main
|
|
300
|
-
{ rank: 3, phases: ["paused"
|
|
301
|
-
{ rank: 3, prefixes: ["
|
|
360
|
+
{ rank: 1, prefixes: ["approved", "waiting to start", "waiting for "] },
|
|
361
|
+
{ rank: 2, phases: ["failing", "main's check is failing"] },
|
|
362
|
+
{ rank: 3, phases: ["paused"] },
|
|
363
|
+
{ rank: 3, prefixes: ["paused", "waiting:"] },
|
|
302
364
|
];
|
|
303
365
|
/** The rank LOOP_RANK_RULES assigns `phase`: -1 for director working, 4 for idle, otherwise the
|
|
304
366
|
* first matching rule's rank. */
|
|
@@ -324,8 +386,8 @@ export function sortLoopsByState(loops) {
|
|
|
324
386
|
const rb = loopRank(b.phase);
|
|
325
387
|
if (ra !== rb)
|
|
326
388
|
return ra - rb;
|
|
327
|
-
const ta = a.
|
|
328
|
-
const tb = b.
|
|
389
|
+
const ta = a.lastRunEndedAt ?? 0;
|
|
390
|
+
const tb = b.lastRunEndedAt ?? 0;
|
|
329
391
|
if (ta !== tb)
|
|
330
392
|
return tb - ta;
|
|
331
393
|
return a.role.localeCompare(b.role);
|