tumwater 0.4.0 → 0.4.2
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 +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -59,6 +59,7 @@ function scanMainCheck(root) {
|
|
|
59
59
|
// The final window's merged events, in scan order — collected in the same pass that
|
|
60
60
|
// finds the check instead of a second full scan of the window below.
|
|
61
61
|
let mergeDone = [];
|
|
62
|
+
let culprit;
|
|
62
63
|
const grewFull = mainCheckGrewFull.has(root);
|
|
63
64
|
// A set note means the previous scan reached the cap without finding a check, so start
|
|
64
65
|
// there: starting at DEFAULT_EVENT_TAIL would only re-read (and re-parse on a cache miss)
|
|
@@ -68,9 +69,11 @@ function scanMainCheck(root) {
|
|
|
68
69
|
const events = readEvents(root, window);
|
|
69
70
|
check = undefined;
|
|
70
71
|
mergeDone = [];
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
//
|
|
72
|
+
culprit = undefined;
|
|
73
|
+
// One pass collects all three facts the derivation needs: the newest merge-scope check,
|
|
74
|
+
// every merged event, and the newest main_culprit blame. The merges are re-read against
|
|
75
|
+
// the check's ts below — a pass over the merges alone, not a second pass over every
|
|
76
|
+
// event in the window.
|
|
74
77
|
for (const e of events) {
|
|
75
78
|
if (e.type === "build_check" && isFullCheckScope(e.scope, e.full)) {
|
|
76
79
|
check = e;
|
|
@@ -78,6 +81,9 @@ function scanMainCheck(root) {
|
|
|
78
81
|
else if (e.type === "merge_done" && typeof e.ts === "number") {
|
|
79
82
|
mergeDone.push(e);
|
|
80
83
|
}
|
|
84
|
+
else if (e.type === "main_culprit") {
|
|
85
|
+
culprit = e;
|
|
86
|
+
}
|
|
81
87
|
}
|
|
82
88
|
// Found, the log is shorter than the window (nothing older exists to find), or the cap
|
|
83
89
|
// is reached: the tail is final. A stale `check` from a previous, smaller window cannot
|
|
@@ -93,7 +99,7 @@ function scanMainCheck(root) {
|
|
|
93
99
|
else
|
|
94
100
|
mainCheckGrewFull.add(root);
|
|
95
101
|
if (!check || typeof check.ts !== "number" || !isMainCheckStatus(check.status)) {
|
|
96
|
-
return {
|
|
102
|
+
return { ...(culprit ? { culprit } : {}) };
|
|
97
103
|
}
|
|
98
104
|
let mergeDoneAfter;
|
|
99
105
|
let mergeDoneBefore;
|
|
@@ -105,6 +111,7 @@ function scanMainCheck(root) {
|
|
|
105
111
|
}
|
|
106
112
|
return {
|
|
107
113
|
check,
|
|
114
|
+
...(culprit ? { culprit } : {}),
|
|
108
115
|
...(typeof mergeDoneAfter?.commit === "string" ? { mergeDoneAfterCommit: mergeDoneAfter.commit } : {}),
|
|
109
116
|
...(typeof mergeDoneBefore?.commit === "string" ? { mergeDoneBeforeCommit: mergeDoneBefore.commit } : {}),
|
|
110
117
|
};
|
|
@@ -113,7 +120,11 @@ function scanMainCheck(root) {
|
|
|
113
120
|
* copies the one check event and the record, so a caller never receives the cached object
|
|
114
121
|
* itself. */
|
|
115
122
|
function mainCheckScanFor(root) {
|
|
116
|
-
return (cachedByStat(mainCheckScanCache, root, eventsLogPath(root), () => scanMainCheck(root), (v) => ({
|
|
123
|
+
return (cachedByStat(mainCheckScanCache, root, eventsLogPath(root), () => scanMainCheck(root), (v) => ({
|
|
124
|
+
...v,
|
|
125
|
+
...(v.check ? { check: { ...v.check } } : {}),
|
|
126
|
+
...(v.culprit ? { culprit: { ...v.culprit } } : {}),
|
|
127
|
+
})) ?? { check: undefined });
|
|
117
128
|
}
|
|
118
129
|
/** Has main's newest merge-scope check retired a role's stale `main_red` verdict? Only a
|
|
119
130
|
* PASSED check stamped strictly after the role's red run ended counts — the team-level
|
|
@@ -143,11 +154,36 @@ export function mainCheckForPoll(root, cfg) {
|
|
|
143
154
|
const branch = targetBranch(cfg.baseBranch, currentBranchFromHeadFile(root));
|
|
144
155
|
const sha = scan.mergeDoneAfterCommit ?? readBranchHead(root, branch) ?? scan.mergeDoneBeforeCommit;
|
|
145
156
|
const counts = asTestCounts(check.counts);
|
|
157
|
+
// The red evidence names the failing test and the merge the main watch blamed — and that
|
|
158
|
+
// data lives only on the main_culprit event (the main watch's verdict on the red tip; the
|
|
159
|
+
// build_check event itself carries no failing tests), attributed only when the verdict's
|
|
160
|
+
// commit is exactly the sha this poll derived, so a newer build_check failure can't borrow
|
|
161
|
+
// a blame the watch wrote for a different head. A passing/skipped check, or a failing one
|
|
162
|
+
// whose tip the watch hasn't blamed, keeps today's exact MainCheckStatus shape (no `red`).
|
|
163
|
+
let red;
|
|
164
|
+
if (check.status === "failed" && sha !== undefined && scan.culprit && typeof scan.culprit.commit === "string") {
|
|
165
|
+
const c = scan.culprit;
|
|
166
|
+
if (c.commit === sha) {
|
|
167
|
+
const failing = Array.isArray(c.failingTests) && typeof c.failingTests[0] === "string"
|
|
168
|
+
? c.failingTests.filter((f) => typeof f === "string")
|
|
169
|
+
: undefined;
|
|
170
|
+
if (failing) {
|
|
171
|
+
red = {
|
|
172
|
+
failingTests: failing,
|
|
173
|
+
culprit: {
|
|
174
|
+
commit: c.commit,
|
|
175
|
+
...(typeof c.runner === "string" ? { runner: c.runner } : {}),
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
146
181
|
return {
|
|
147
182
|
...(sha ? { sha } : {}),
|
|
148
183
|
status: check.status,
|
|
149
184
|
...(counts ? { counts } : {}),
|
|
150
185
|
at: check.ts,
|
|
186
|
+
...(red ? { red } : {}),
|
|
151
187
|
};
|
|
152
188
|
}
|
|
153
189
|
/** The parity ledger's glance verdict for one status poll, or undefined when there is no
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readTextOrNull } from "../files/files.js";
|
|
4
|
+
import { cachedByStat } from "../files/stat-cache.js";
|
|
5
|
+
import { roleNotesPath } from "../paths.js";
|
|
6
|
+
import { ROLE_NOTES_MAX_BYTES } from "../agent/role-notes-write.js";
|
|
7
|
+
/** The per-file lengths, keyed by absolute path. One stat per unchanged file per poll (the
|
|
8
|
+
* same stat-keyed contract status-inbox.ts relies on for the inbox's prompt cache), so the
|
|
9
|
+
* dashboards' once-a-second poll never re-reads an unchanged notes file. */
|
|
10
|
+
const noteLengthCache = new Map();
|
|
11
|
+
/** Every role whose notes file runs past the read bound, in descending-char order — the
|
|
12
|
+
* single home of the role-notes over-cap walk, shared by the status snapshot's
|
|
13
|
+
* `collectRoleNotesOverCap` and the doctor `role notes` row
|
|
14
|
+
* (src/doctor/doctor-role-notes.ts's checkRoleNotes), so the alert and the doctor row cannot
|
|
15
|
+
* disagree about which notes are over the bound. `readRoleNote` (src/run/run-prompt.ts) cuts
|
|
16
|
+
* every notes file at ROLE_NOTES_MAX_BYTES *characters* on read, but every operator surface
|
|
17
|
+
* reads the file whole — so a hand-edited or old-build note past the bound reaches that role's
|
|
18
|
+
* run prompt as only its head plus a truncation marker while `tumwater status` looks green.
|
|
19
|
+
* Returns null when the notes dir is missing or unreadable (the write path creates it on first
|
|
20
|
+
* write), whose callers each map to their own "nothing recorded yet" reading. Never throws. */
|
|
21
|
+
export function readRoleNotesOverCap(root) {
|
|
22
|
+
const notesDir = path.dirname(roleNotesPath(root, "role"));
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
entries = fs.readdirSync(notesDir);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Missing or unreadable notes dir: nothing recorded yet (the write path creates it).
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const over = [];
|
|
32
|
+
for (const name of entries) {
|
|
33
|
+
if (!name.endsWith(".md"))
|
|
34
|
+
continue;
|
|
35
|
+
const file = path.join(notesDir, name);
|
|
36
|
+
const chars = cachedByStat(noteLengthCache, file, file, () => {
|
|
37
|
+
const text = readTextOrNull(file);
|
|
38
|
+
return text === null ? null : text.length;
|
|
39
|
+
}, (n) => n);
|
|
40
|
+
if (chars === null)
|
|
41
|
+
continue;
|
|
42
|
+
if (chars > ROLE_NOTES_MAX_BYTES)
|
|
43
|
+
over.push({ role: name.slice(0, -3), chars });
|
|
44
|
+
}
|
|
45
|
+
over.sort((a, b) => b.chars - a.chars);
|
|
46
|
+
return over;
|
|
47
|
+
}
|
|
48
|
+
/** The role-notes slice of a status snapshot as a plain array for the dashboard payloads:
|
|
49
|
+
* every over-cap note in descending-char order, or [] when the notes dir is missing (the same
|
|
50
|
+
* "nothing recorded yet" degrade the doctor's checkRoleNotes treats as a pass). */
|
|
51
|
+
export function collectRoleNotesOverCap(root) {
|
|
52
|
+
return readRoleNotesOverCap(root) ?? [];
|
|
53
|
+
}
|
|
@@ -3,17 +3,17 @@ import { configForRole, modelSelectorField, roleSeamTier, } from "../config/conf
|
|
|
3
3
|
import { modelFallbackView } from "../config/config-fallbacks.js";
|
|
4
4
|
import { isCustomRole } from "../config/config.js";
|
|
5
5
|
import { baseRoleOf, configuredInstances, instanceIndex, runnerIds, runnerPaused } from "../roles/runner-ids.js";
|
|
6
|
-
import {
|
|
6
|
+
import { errorStreakAtWarn, QUIET_KILL_RESUME_LIMIT } from "../run/run-apply.js";
|
|
7
7
|
import { queuedMergeRoles } from "../merge/merge-queue.js";
|
|
8
8
|
import { hasOutstandingWork } from "../runner/runner-state.js";
|
|
9
9
|
import { formatModelSelector } from "../config/model-selector.js";
|
|
10
10
|
import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
|
|
11
11
|
import { isJsonObject } from "../files/json-object.js";
|
|
12
12
|
import { runnerStateForPoll } from "./status-polls.js";
|
|
13
|
-
import { roleImageInfo
|
|
13
|
+
import { roleImageInfo } from "../agent/image-input.js";
|
|
14
14
|
import { backendDefaultLabel } from "../agent/backend-traits.js";
|
|
15
15
|
import { readPiListedModels } from "../pi/pi-listed-models.js";
|
|
16
|
-
import {
|
|
16
|
+
import { seamContextWindowForRole } from "../pi/pi-models.js";
|
|
17
17
|
/** One row per enabled role id (part 5b/7). `configForRole`, `modelFallbackView`, the leased /
|
|
18
18
|
* pinned slot and the pi-default seam all resolve against the values `snapshot()` already
|
|
19
19
|
* read once for the poll — the info and slots pins (threaded as `slots`) keep rows, `running`
|
|
@@ -42,13 +42,10 @@ export function collectRoleRows(root, cfg, now, slots, modelsPath, paused) {
|
|
|
42
42
|
queued.has(r) ||
|
|
43
43
|
s.claim !== undefined ||
|
|
44
44
|
hasOutstandingWork(s) ||
|
|
45
|
-
(s
|
|
45
|
+
errorStreakAtWarn(s) ||
|
|
46
46
|
(s.lastResult === "quiet_killed" && (s.quietKillStreak ?? 0) >= QUIET_KILL_RESUME_LIMIT));
|
|
47
47
|
});
|
|
48
48
|
const piDefault = readPiDefaultModel(piSettingsPath(modelsPath), root);
|
|
49
|
-
// One probe-cache read per poll serves every row's image-input surface (same no-data policy
|
|
50
|
-
// as every other degraded read: a missing cache is an empty one).
|
|
51
|
-
const probes = readImageProbeCache(root);
|
|
52
49
|
const listed = readPiListedModels(root);
|
|
53
50
|
return roles.map((r) => {
|
|
54
51
|
const base = { ...runnerStateForPoll(root, r), custom: isCustomRole(cfg, r) };
|
|
@@ -65,7 +62,11 @@ export function collectRoleRows(root, cfg, now, slots, modelsPath, paused) {
|
|
|
65
62
|
// capability and its reroute target, one shared resolver so the status rows, the role
|
|
66
63
|
// page, and the submit gate cannot disagree about who reads images or where a blind
|
|
67
64
|
// prompt would run.
|
|
68
|
-
const image = roleImageInfo(cfg, r, modelsPath,
|
|
65
|
+
const image = roleImageInfo(cfg, r, modelsPath, listed);
|
|
66
|
+
// The peak-ctx share denominator (the role's context window, part 3c): resolved once per
|
|
67
|
+
// poll here, beside `eff` (the same seam the role page resolves and names) — the same seam
|
|
68
|
+
// `seamContextWindow` run against, so row, page and drawer cannot disagree.
|
|
69
|
+
const contextWindow = seamContextWindowForRole(cfg, r, root, modelsPath);
|
|
69
70
|
// Model failure fallback, part 2/2: while the role's episode is active its agent runs use
|
|
70
71
|
// the tier-resolved fallback pair, so the row names that pair, the episode's start, and
|
|
71
72
|
// the failure that tripped it. `now` is the poll's own clock (the snapshot seam), so
|
|
@@ -106,37 +107,9 @@ export function collectRoleRows(root, cfg, now, slots, modelsPath, paused) {
|
|
|
106
107
|
: { model: backendDefaultLabel(eff.agent) }),
|
|
107
108
|
...(isJsonObject(cfg.model) ? { modelTier: roleSeamTier(cfg, r) } : {}),
|
|
108
109
|
...(episode !== null ? { fallback: episode } : {}),
|
|
110
|
+
...(contextWindow !== null ? { contextWindow } : {}),
|
|
109
111
|
...(slot ? { slot: path.basename(slot.dir), slotPinned: pinned !== undefined } : {}),
|
|
110
112
|
...(baseRoleOf(r) === r ? {} : { instanceOf: baseRoleOf(r) }),
|
|
111
113
|
};
|
|
112
114
|
});
|
|
113
115
|
}
|
|
114
|
-
/** The doctor's live-probe failures (plans/image-input.md "Surfaces", part 5/5): every cached
|
|
115
|
-
* probe verdict of `fail` that is still fresh (the claim is unchanged and within the 7-day
|
|
116
|
-
* TTL), labelled as `<provider>/<model>`. A claimed model that failed the probe reads `false`
|
|
117
|
-
* everywhere (seamImageInput honors a fresh failure), but the observability value is naming
|
|
118
|
-
* it — a claim the doctor contradicted is exactly what the amber probe alert and the role/status
|
|
119
|
-
* rows should surface, not just "this role is text-only". A torn or missing cache answers
|
|
120
|
-
* empty (the same no-data policy as the per-role read). */
|
|
121
|
-
export function collectImageProbeFailures(root, now = Date.now()) {
|
|
122
|
-
const probes = readImageProbeCache(root);
|
|
123
|
-
const out = [];
|
|
124
|
-
for (const [key, entry] of Object.entries(probes)) {
|
|
125
|
-
if (entry.verdict !== "fail" || !imageProbeEntryFresh(entry, now))
|
|
126
|
-
continue;
|
|
127
|
-
// The cache key is agent/provider/model, and a pi model id is itself fully qualified (the
|
|
128
|
-
// key's third segment can carry embedded '/' — parseModelSelector keeps the id whole), so
|
|
129
|
-
// split only the first two separators and keep the rest as the model. A naive split("/")
|
|
130
|
-
// destructured to three parts would truncate a qualified id to its org, mislabelling the
|
|
131
|
-
// model the doctor flagged.
|
|
132
|
-
const first = key.indexOf("/");
|
|
133
|
-
const second = key.indexOf("/", first + 1);
|
|
134
|
-
if (first === -1 || second === -1)
|
|
135
|
-
continue;
|
|
136
|
-
const agent = key.slice(0, first) || undefined;
|
|
137
|
-
const provider = key.slice(first + 1, second) || undefined;
|
|
138
|
-
const model = key.slice(second + 1) || undefined;
|
|
139
|
-
out.push(seamImageLabel(agent, provider, model));
|
|
140
|
-
}
|
|
141
|
-
return out.sort();
|
|
142
|
-
}
|
|
@@ -16,7 +16,7 @@ import { EVENTS_LOG_BASENAME } from "../paths.js";
|
|
|
16
16
|
* (or `N * DAY_MS`) below. */
|
|
17
17
|
export const HOUR_MS = 60 * 60 * 1000;
|
|
18
18
|
/** Milliseconds in a day — the sibling of `HOUR_MS`: the shared day unit for the
|
|
19
|
-
* day-sized retention and lookback constants (the opencode-catalog
|
|
19
|
+
* day-sized retention and lookback constants (the opencode-catalog TTL,
|
|
20
20
|
* the doctor's stale-pin/merge/prompt/wait checks, the known-flake and selfcheck
|
|
21
21
|
* windows, the dashboard-history since bound, the retention prune, the claude-window
|
|
22
22
|
* lookback) so one day cannot mean slightly different spans per consumer. */
|
|
@@ -52,6 +52,13 @@ export function secondsSince(sinceMs, now) {
|
|
|
52
52
|
export function secondsUntil(atMs, now) {
|
|
53
53
|
return Math.max(0, Math.round((atMs - now) / 1000));
|
|
54
54
|
}
|
|
55
|
+
/** Whole seconds from `now` to a unix-seconds instant (stamps providers emit as whole seconds,
|
|
56
|
+
* not millis), clamped at zero — the secondsUntil ×1000 fold shared by the claude and codex
|
|
57
|
+
* stream translators' rate-limit retry-after computations (both carry a `resetsAt`/`resets_at`
|
|
58
|
+
* unix-seconds stamp), so the unix-seconds-to-epoch-ms conversion lives in one place. */
|
|
59
|
+
export function secondsUntilUnixStamp(seconds, now) {
|
|
60
|
+
return secondsUntil(seconds * 1000, now);
|
|
61
|
+
}
|
|
55
62
|
/** Calendar date as `YYYY-MM-DD` in local time — shared by the transcript run separators,
|
|
56
63
|
* the daily-budget day stamp (todayStamp), and the usage report's per-day buckets, which must
|
|
57
64
|
* all agree on what counts as one day. */
|
package/dist/src/text/format.js
CHANGED
|
@@ -36,6 +36,18 @@ export function compactTokens(n) {
|
|
|
36
36
|
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
37
37
|
return n >= 10_000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
38
38
|
}
|
|
39
|
+
/** A peak context reading its share of the role's declared context window (`110.0k (84%)`) —
|
|
40
|
+
* the single home of the share format so the status table's `peak ctx` cell, the GUI/`status
|
|
41
|
+
* --json` role payload, the dashboard role drawer, and the `history <role> <n>` summary header
|
|
42
|
+
* all render a peak against a window identically. When the window is unknown
|
|
43
|
+
* (null/undefined/0) or the peak is 0 there is no meaningful share, so it returns exactly
|
|
44
|
+
* `compactTokens(peakTokens)` — the bare peak today's surfaces print. The percentage is
|
|
45
|
+
* floor-free rounded: `Math.round((100 * peak) / window)`. */
|
|
46
|
+
export function contextShareLabel(peakTokens, windowTokens) {
|
|
47
|
+
if (!windowTokens || !peakTokens)
|
|
48
|
+
return compactTokens(peakTokens);
|
|
49
|
+
return `${compactTokens(peakTokens)} (${Math.round((100 * peakTokens) / windowTokens)}%)`;
|
|
50
|
+
}
|
|
39
51
|
/** The abbreviated form of a commit hash for human-facing text — its first 8 characters.
|
|
40
52
|
* The single home of this format: the event feed's merged/review lines and the review gate's
|
|
41
53
|
* discard warning all render hashes through it, so the abbreviation length cannot drift per
|
|
@@ -68,7 +80,7 @@ export function usdCap(n) {
|
|
|
68
80
|
}
|
|
69
81
|
/** A disk size in GB for display: one decimal and the unit (`12.3 GB`) — the single home of
|
|
70
82
|
* the disk-size format shared by the event feed's disk lines (event-format.ts), the status
|
|
71
|
-
* header's disk badge (badges.ts's `diskBadge`), the team's low-disk alert (team-
|
|
83
|
+
* header's disk badge (badges.ts's `diskBadge`), the team's low-disk alert (team-alert-status.ts),
|
|
72
84
|
* the doctor's disk check detail (doctor-project-checks.ts), and `tumwater reclaim`'s
|
|
73
85
|
* freed-size line (operator-commands.ts), so the decimal width and unit cannot drift per
|
|
74
86
|
* consumer. A caller
|
package/dist/src/text/phrases.js
CHANGED
|
@@ -80,8 +80,9 @@ export function mainRedNotMine(sha) {
|
|
|
80
80
|
}
|
|
81
81
|
/** The ` — "<reason>"` suffix the operator's pause reason (`pause --reason <text>`) rides on —
|
|
82
82
|
* one home so the CLI's confirmation line (operator/operator-commands.ts), the status header's
|
|
83
|
-
* pause badge (ui/badges.ts), and the paused alert's title (ui/team-
|
|
84
|
-
* quoting. No reason, no suffix: every caller's reasonless phrasing keeps
|
|
83
|
+
* pause badge (ui/badges.ts), and the paused alert's title (ui/team-alert-tail.ts) cannot
|
|
84
|
+
* drift on the quoting. No reason, no suffix: every caller's reasonless phrasing keeps
|
|
85
|
+
* today's byte-exact form.
|
|
85
86
|
* (The GUI keeps its own JS copies in gui-client.ts: a separate runtime that cannot import
|
|
86
87
|
* TypeScript.) */
|
|
87
88
|
export function pauseReasonSuffix(reason) {
|
|
@@ -220,6 +221,19 @@ export function backendKindPhrase(kind) {
|
|
|
220
221
|
return "backend failure";
|
|
221
222
|
}
|
|
222
223
|
}
|
|
224
|
+
/** True when a hold/resume event's kind names an actual backend failure rather than a
|
|
225
|
+
* rate-limit (or an unreadable, torn-line kind). The one home of the "not a 429"
|
|
226
|
+
* discriminator: an absent, blank, or `"rate-limit"` kind keeps the 429 wording every
|
|
227
|
+
* historical event already has, and only a real non-empty backend kind names itself
|
|
228
|
+
* (through backendKindPhrase). Shared by the event feed (event-format.ts's
|
|
229
|
+
* rate_limit_hold/recovered lines) and the failure digest's Team state-change lines
|
|
230
|
+
* (src/failure/failure-state-change.ts's rate_limit_hold/resumed), so the two cannot
|
|
231
|
+
* disagree. Not to be confused with isBackendFailureKind in pi-failure.ts, which is a
|
|
232
|
+
* strict enum-membership guard for the 5 BACKEND_FAILURE_KINDS: this predicate instead
|
|
233
|
+
* accepts any non-empty kind that isn't "rate-limit". */
|
|
234
|
+
export function rendersAsBackendFailure(kind) {
|
|
235
|
+
return !!kind && kind !== "rate-limit";
|
|
236
|
+
}
|
|
223
237
|
const LEADING_CD = /^\s*cd\s+(?:"[^"]*"|'[^']*'|\S+)\s*(?:&&|;)\s*/;
|
|
224
238
|
function withoutLeadingCd(command) {
|
|
225
239
|
let rest = command;
|
package/dist/src/text/text.js
CHANGED
|
@@ -243,6 +243,18 @@ export function parsePositiveInt(raw) {
|
|
|
243
243
|
export function parseNonNegativeInt(raw) {
|
|
244
244
|
return parseDecimalInt(raw);
|
|
245
245
|
}
|
|
246
|
+
/** Whether a string is an exponent spelling of a number ("1e2", "2.5E-3", "4e1") rather
|
|
247
|
+
* than a plain decimal: a run of digits Number() represents exactly. `Number()` would read an
|
|
248
|
+
* exponent spelling as a real number ("1e2" → 100), which is why every integer-writing
|
|
249
|
+
* surface refuses it — JSON.parse would silently read it past an integer validator — while
|
|
250
|
+
* the plain-decimal rule above admits only the literal spelling. The two shapes are mutually
|
|
251
|
+
* exclusive (a plain decimal has no `e`), so the plain-decimal exclusion is kept here as the
|
|
252
|
+
* shape's honest statement: one home for the predicate the config writer, the TUI team-key
|
|
253
|
+
* editor, the per-role instances editor, and the CLI role writes' local helper each
|
|
254
|
+
* hand-rolled. */
|
|
255
|
+
export function isExponentSpelling(raw) {
|
|
256
|
+
return !/^\d*\.?\d+$/.test(raw) && /^\d+(\.\d+)?[eE][+-]?\d+$/.test(raw);
|
|
257
|
+
}
|
|
246
258
|
/** The shared over-long-text message: `<subject> is <n> chars — shorten it to at most <max>`,
|
|
247
259
|
* with `tail` naming where the text rides once accepted. The one home of that phrasing, used
|
|
248
260
|
* by init's seed check (init.ts, "the brief text … every run's prefill"), inbox-submit's
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { openQuestionEntries, ageInDays } from "../backlog/backlog.js";
|
|
2
|
+
import { stripEntryStamp } from "../backlog/backlog-structure.js";
|
|
3
|
+
import { questionFilingDate } from "../backlog/question-answer.js";
|
|
4
|
+
import { byDaysThenIndex } from "../collections.js";
|
|
5
|
+
import { eventsSince } from "../events/event-window.js";
|
|
6
|
+
import { STALE_QUESTION_DAYS } from "../doctor/doctor-backlog.js";
|
|
7
|
+
import { regressedFixCandidates, matchedRegressedFixes, regressedFixesPhrase } from "../doctor/doctor-backlog-bugs.js";
|
|
8
|
+
import { planDependencyProblem } from "../doctor/doctor-backlog-plans.js";
|
|
9
|
+
import { checkWaitContention, providerConcurrencyProblems, providerWaitContention, slotWaitContention, smallPoolProblems, staleWorkspacePins, CHECK_WAIT_CHECK_WINDOW_MS, PROVIDER_WAIT_CHECK_WINDOW_MS, SLOT_WAIT_CHECK_WINDOW_MS } from "../doctor/doctor-contention-checks.js";
|
|
10
|
+
import { readSlotsState } from "../git/slots-state.js";
|
|
11
|
+
import { configForStatus } from "../status/status-polls.js";
|
|
12
|
+
import { piModelsPath } from "../pi/pi-models.js";
|
|
13
|
+
import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
|
|
14
|
+
/** This module is the alert-feed half of src/ui/status-payload.ts: each always-on team alert's
|
|
15
|
+
* data-sourcing function (capacityContention, …) lives here, a
|
|
16
|
+
* sibling of the src/ui/team-alert-*.ts builders that turn them into banners. status-payload.ts
|
|
17
|
+
* stays the one status-document builder, importing these feeds and folding each feed's result
|
|
18
|
+
* into the `alerts` field; status-render.ts reads the feeds directly for the TUI. */
|
|
19
|
+
/** The open questions the doctor's 7-day rule (src/doctor/doctor-backlog-bugs.ts
|
|
20
|
+
* checkStaleQuestions, STALE_QUESTION_DAYS) calls stale: an open QUESTIONS.md entry whose
|
|
21
|
+
* `(asked by …)` stamp is at least 7 days old. This is the always-on feed for the stale-questions
|
|
22
|
+
* team alert — the shared rule's twin on every observer surface, computed from the same threshold
|
|
23
|
+
* and the same questionFilingDate parser the doctor's check uses, so the two cannot drift on which
|
|
24
|
+
* questions are stale. An undated heading is skipped (like the doctor's check); questions carry no
|
|
25
|
+
* refused note to filter. The returned objects carry the 1-based position (for
|
|
26
|
+
* `tumwater questions answer <n>`), the 0-based index (kept only as the file-order tiebreak), the
|
|
27
|
+
* stamp-stripped title, and the whole-day age. Returned oldest-first — most days waited, ties in
|
|
28
|
+
* file order — so a caller naming the first element names the most-stale question. */
|
|
29
|
+
export function staleOpenQuestions(root, now = Date.now()) {
|
|
30
|
+
return openQuestionEntries(root)
|
|
31
|
+
.flatMap((entry, index) => {
|
|
32
|
+
const stamp = questionFilingDate(entry.title);
|
|
33
|
+
if (stamp === null)
|
|
34
|
+
return [];
|
|
35
|
+
const days = ageInDays(stamp, now);
|
|
36
|
+
return days >= STALE_QUESTION_DAYS ? [{ index, position: index + 1, title: stripEntryStamp(entry.title), days }] : [];
|
|
37
|
+
})
|
|
38
|
+
.sort(byDaysThenIndex);
|
|
39
|
+
}
|
|
40
|
+
/** A broken plan-dependency clause, the always-on twin of the doctor's `plan dependencies` row
|
|
41
|
+
* (src/doctor/doctor-backlog-plans.ts checkPlanDependencies): the same rule's problem set,
|
|
42
|
+
* severity order, and exact message, so the alert and the doctor cannot drift. The reader owns
|
|
43
|
+
* the rule — planDependencyAlert passes the doctor's planDependencyProblem through unchanged,
|
|
44
|
+
* like staleOpenQuestions and the other stale feeds do their doctor's rules. null when PLANS.md's
|
|
45
|
+
* planned entries have no cycle, dangling ref, unparsed clause, or misworded dependency. */
|
|
46
|
+
export function planDependencyAlert(root) {
|
|
47
|
+
return planDependencyProblem(root);
|
|
48
|
+
}
|
|
49
|
+
/** @see CapacityBottleneck */
|
|
50
|
+
export function capacityContention(root, now = Date.now()) {
|
|
51
|
+
const out = [];
|
|
52
|
+
/** Resolve one capacity verdict and push a "capacity bottleneck" row when it warns. Shared
|
|
53
|
+
* by the three capacity checks (workspace, provider, check slots) so the try/catch that keeps
|
|
54
|
+
* each one from crying wolf on an unreadable event log lives in one place, and the row's
|
|
55
|
+
* `name`/`detail` shape cannot drift between them. */
|
|
56
|
+
const maybeWarn = (name, windowMs, keep, resolve) => {
|
|
57
|
+
try {
|
|
58
|
+
const verdict = resolve(eventsSince(root, windowMs, now, keep));
|
|
59
|
+
if (verdict.warn)
|
|
60
|
+
out.push({ name, detail: verdict.detail });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// A read error skips this row, so the alert never cries wolf on an unreadable log.
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
maybeWarn("workspace slots", SLOT_WAIT_CHECK_WINDOW_MS, (ev) => ev.type === "slot_wait", (waits) => slotWaitContention(waits, readSlotsState(root).slots.length));
|
|
67
|
+
maybeWarn("provider slots", PROVIDER_WAIT_CHECK_WINDOW_MS, (ev) => ev.type === "provider_wait", (waits) => providerWaitContention(waits, configForStatus(root)));
|
|
68
|
+
maybeWarn("check slots", CHECK_WAIT_CHECK_WINDOW_MS, (e) => e.type === "check_wait", (waits) => checkWaitContention(waits, configForStatus(root)));
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** A `maxConcurrentPerProvider` map that cannot do its job — the always-on twin of the doctor's
|
|
72
|
+
* `provider concurrency` row (src/doctor/doctor-contention-checks.ts checkProviderConcurrency):
|
|
73
|
+
* a cap key no seam resolves to (a typo, or a provider the team never runs on), a cap at or
|
|
74
|
+
* above `maxConcurrent` (it never binds), or a keyless seam in a capped team. The `contention`
|
|
75
|
+
* and `provider-hold` alerts fire only once a too-small cap has already parked runs or a storm
|
|
76
|
+
* has struck; this standing alert names the static misconfiguration itself, which the doctor
|
|
77
|
+
* row is the only other surface that otherwise exposes. Reads the same last-known-good config
|
|
78
|
+
* the snapshot uses (configForStatus) and audits it through the row's own
|
|
79
|
+
* providerConcurrencyProblems, so the alert and the row cannot drift on the trigger or the
|
|
80
|
+
* wording; `[]` when no cap is configured or the map is well-formed. */
|
|
81
|
+
export function providerCapProblems(root) {
|
|
82
|
+
return providerConcurrencyProblems(configForStatus(root), readPiDefaultModel(piSettingsPath(piModelsPath()), root));
|
|
83
|
+
}
|
|
84
|
+
/** The always-on twin of the doctor `parallel instances` row's pool-mismatch clause
|
|
85
|
+
* (doctor-contention-checks.ts smallPoolProblems): an enabled `feature`/`bugfix` running
|
|
86
|
+
* more than one instance while slotCount(config) sits below maxConcurrent — the shared pool
|
|
87
|
+
* is smaller than the run cap, so those instances contend for the same checkouts. Built from
|
|
88
|
+
* the same smallPoolProblems selector the doctor row uses, so the alert and the row cannot
|
|
89
|
+
* drift on the trigger, the role list, or the wording. configForStatus never returns
|
|
90
|
+
* undefined (an unreadable tumwater.json falls back to defaultConfig(), whose pool is the
|
|
91
|
+
* default maxConcurrent + 1 — never below maxConcurrent), so the [] is the only silent
|
|
92
|
+
* outcome. */
|
|
93
|
+
export function instancePoolProblems(root) {
|
|
94
|
+
return smallPoolProblems(configForStatus(root));
|
|
95
|
+
}
|
|
96
|
+
/** How far back the regressed-fixes alert looks — one day, so a recently Fixed test that failed
|
|
97
|
+
* again is a standing amber alert until a day passes (or the failure is filed). The doctor's
|
|
98
|
+
* own `regressed fixes` row keeps its since-the-fix read; this is the alert's daily trigger, the
|
|
99
|
+
* same trailing day the regressed-fixes alert reads. */
|
|
100
|
+
export const REGRESSED_FIX_WINDOW_MS = 24 * 60 * 60_000;
|
|
101
|
+
/** The always-on twin of the doctor's `regressed fixes` row (src/doctor/doctor-backlog-bugs.ts
|
|
102
|
+
* checkRegressedFixes): the same candidate selection (regressedFixCandidates) and the same
|
|
103
|
+
* failureMentions match (matchedRegressedFixes) and sentence (regressedFixesPhrase), read off the
|
|
104
|
+
* trailing REGRESSED_FIX_WINDOW_MS of events (the doctor, whose window is since each fix, still
|
|
105
|
+
* warns on an older recurrence). An unreadable event log yields none. */
|
|
106
|
+
export function regressedFixesAlert(root, now = Date.now()) {
|
|
107
|
+
const candidates = regressedFixCandidates(root);
|
|
108
|
+
const rows = matchedRegressedFixes(candidates, eventsSince(root, REGRESSED_FIX_WINDOW_MS, now));
|
|
109
|
+
const detail = regressedFixesPhrase(rows);
|
|
110
|
+
return detail === null ? null : { count: rows.length, detail };
|
|
111
|
+
}
|
|
112
|
+
/** Re-export so the always-on alert's stale-pin selector stays in its one home
|
|
113
|
+
* (src/doctor/doctor-contention-checks.ts) while tui.tsx keeps importing every alert feed from
|
|
114
|
+
* this one module. */
|
|
115
|
+
export { staleWorkspacePins };
|
package/dist/src/ui/badges.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sortEntriesByKey } from "../collections.js";
|
|
1
2
|
import { utilizationPct, windowLabel } from "../agent/subscription-account.js";
|
|
2
3
|
import { MODEL_TIERS } from "../config/config-schema.js";
|
|
3
4
|
import { budgetGate, budgetReached, fallbackTierEntries } from "../budget/budget.js";
|
|
@@ -338,7 +339,7 @@ export function pauseBadge(pausedUntil, now, reason) {
|
|
|
338
339
|
/** The countdown a timed team pause has left, as humanSeconds — `null` when there is none
|
|
339
340
|
* (no deadline, or an already-expired one: the read side treats an expired marker as
|
|
340
341
|
* unpaused). The one home of the guard + ms→seconds rounding behind the team's pause
|
|
341
|
-
* countdowns, shared by pauseBadge's header fragment and team-
|
|
342
|
+
* countdowns, shared by pauseBadge's header fragment and team-alert-tail.ts's paused alert
|
|
342
343
|
* title, so the two surfaces cannot disagree about how much time remains. The guard is this
|
|
343
344
|
* function's own; the ms→seconds rounding rides datetime.ts's secondsUntil. The GUI's pause
|
|
344
345
|
* control recomputes the counting-down number client-side from the payload's raw `pausedUntil`
|
|
@@ -391,9 +392,7 @@ export function diskBadge(disk) {
|
|
|
391
392
|
export function permitsBadge(permits) {
|
|
392
393
|
if (!permits)
|
|
393
394
|
return "";
|
|
394
|
-
const providers = Object.entries(permits.byKey)
|
|
395
|
-
.filter(([, e]) => e.limit !== undefined)
|
|
396
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
395
|
+
const providers = sortEntriesByKey(Object.entries(permits.byKey).filter(([, e]) => e.limit !== undefined))
|
|
397
396
|
.map(([k, e]) => `${k} ${e.inUse}/${e.limit}`)
|
|
398
397
|
.join(" · ");
|
|
399
398
|
if (providers.length === 0)
|