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
package/dist/src/collections.js
CHANGED
|
@@ -65,17 +65,28 @@ export function increment(map, key) {
|
|
|
65
65
|
* step (the seed matters because `noUncheckedIndexedAccess` reads every entry as possibly
|
|
66
66
|
* undefined), shared by the failure digest's outcome tally per role (failure-data.ts), the
|
|
67
67
|
* usage report's window role totals (report-render.ts), and its per-day fold's run and cost
|
|
68
|
-
* tallies (report-fold.ts), so those sites cannot drift on how a missing key is seeded. The
|
|
68
|
+
* tallies (report-usage-fold.ts), so those sites cannot drift on how a missing key is seeded. The
|
|
69
69
|
* test duration reporter (test/test-durations-reporter.ts) keeps its own copy: it imports no
|
|
70
70
|
* tumwater module by design, since node --test loads it into its own parent process.
|
|
71
71
|
*
|
|
72
72
|
* The seed reads an OWN property (Object.hasOwn), not a bare `record[key] ?? 0`: the
|
|
73
73
|
* custom-role name charset admits "constructor", an Object.prototype key, whose inherited
|
|
74
74
|
* function would otherwise be the seed — storing `function Object() …5` and turning every
|
|
75
|
-
* later `roleSum`/report total into string concatenation (BUGS.md 2026-10-10).
|
|
75
|
+
* later `roleSum`/report total into string concatenation (BUGS.md 2026-10-10). The write is
|
|
76
|
+
* an `Object.defineProperty`, not a plain `record[key] = …`: the latter assignment to
|
|
77
|
+
* `__proto__` (another Object.prototype key, reachable through a failing-test name) would
|
|
78
|
+
* trigger the inherited setter and create no own key, silently dropping the count. A
|
|
79
|
+
* defineProperty with the default attribute flags (writable, enumerable, configurable)
|
|
80
|
+
* writes every other key exactly as plain assignment would, so an own `__proto__` key is
|
|
81
|
+
* the only behavior this changes. */
|
|
76
82
|
export function addTo(record, key, amount) {
|
|
77
83
|
const current = Object.hasOwn(record, key) ? record[key] : 0;
|
|
78
|
-
record
|
|
84
|
+
Object.defineProperty(record, key, {
|
|
85
|
+
value: (current ?? 0) + amount,
|
|
86
|
+
writable: true,
|
|
87
|
+
enumerable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
});
|
|
79
90
|
}
|
|
80
91
|
/** Set `key` to `value` in a bounded memo `cache`, moving an existing key to the LRU tail
|
|
81
92
|
* (delete + re-set, so a re-read at the cap does not evict itself) and, when the map would
|
|
@@ -127,6 +138,18 @@ export function trimToLast(arr, max) {
|
|
|
127
138
|
export function rankByCount(items, count, key) {
|
|
128
139
|
return [...items].sort((a, b) => count(b) - count(a) || key(a).localeCompare(key(b)));
|
|
129
140
|
}
|
|
141
|
+
/** Split a list into its first `top` entries and how many were cut off — the pairing five call
|
|
142
|
+
* sites built by hand as `items.slice(0, N)` beside `Math.max(0, items.length - N)`. `visible`
|
|
143
|
+
* aliases `items` when it already fits; `hidden` is `max(0, items.length - top)`, which the
|
|
144
|
+
* slice-length arithmetic yields without the explicit clamp (a slice never returns more than
|
|
145
|
+
* `top` even when the list is shorter). Shared by the merge-intent and conflict-hunk caps
|
|
146
|
+
* (merge-git.ts), the failure digest's loss-cause and message-cluster tops
|
|
147
|
+
* (time-spend.ts, failure-cluster.ts), and the answered-questions cap (question-answer.ts),
|
|
148
|
+
* so those cuts cannot drift on how the remainder is counted. */
|
|
149
|
+
export function topAndHidden(items, top) {
|
|
150
|
+
const visible = items.slice(0, top);
|
|
151
|
+
return { visible, hidden: items.length - visible.length };
|
|
152
|
+
}
|
|
130
153
|
/** The lower median of a non-empty numeric list: the middle value, or the smaller of the
|
|
131
154
|
* middle two for an even count. Returns a fresh sorted copy; the input is never mutated.
|
|
132
155
|
* Shared by report-data.ts's per-day medians, doctor-project-checks.ts's and doctor-backlog.ts's
|
|
@@ -137,18 +160,18 @@ export function lowerMedian(values) {
|
|
|
137
160
|
return sorted[Math.floor((sorted.length - 1) / 2)];
|
|
138
161
|
}
|
|
139
162
|
/** Most-days-waited-first comparator, ties in ascending index (file order) — `b.days - a.days ||
|
|
140
|
-
* a.index - b.index`, the single home of that one-line ordering. Shared by the stale-
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* rows off the same backlog scan, so the two cannot drift on how ties order. The generic holds
|
|
163
|
+
* a.index - b.index`, the single home of that one-line ordering. Shared by the stale-questions
|
|
164
|
+
* feed (src/ui/alert-feeds.ts's staleOpenQuestions), which builds `{index, days}`
|
|
165
|
+
* rows off the same backlog scan, so its tie order cannot drift. The generic holds
|
|
144
166
|
* any row type carrying both fields; a row without a natural index passes its first-seen order. */
|
|
145
167
|
export function byDaysThenIndex(a, b) {
|
|
146
168
|
return b.days - a.days || a.index - b.index;
|
|
147
169
|
}
|
|
148
170
|
/** Ascending string comparator by UTF-16 code unit — the locale-independent order a bare `.sort()`
|
|
149
171
|
* uses, unlike `localeCompare` (which `rankByCount` uses for its tiebreak). Used directly by
|
|
150
|
-
* src/cli/cli-start-summary.ts, src/cli/
|
|
151
|
-
*
|
|
172
|
+
* src/cli/cli-start-summary.ts, src/cli/completion.ts, src/cli/log-commands.ts,
|
|
173
|
+
* src/history/history-data.ts, src/review/post-merge-review.ts, src/review/quarantine-render.ts,
|
|
174
|
+
* src/budget/budget.ts and src/change/change-data.ts, and by `sortByValueDesc` for its tiebreak. */
|
|
152
175
|
export function compareCodeUnits(a, b) {
|
|
153
176
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
154
177
|
}
|
|
@@ -162,6 +185,20 @@ export function compareCodeUnits(a, b) {
|
|
|
162
185
|
export function sortByValueDesc(entries) {
|
|
163
186
|
return entries.sort((a, b) => b[1] - a[1] || compareCodeUnits(a[0], b[0]));
|
|
164
187
|
}
|
|
188
|
+
/** Sort `[key, …]` entry pairs by their string key ascending — the single home of the
|
|
189
|
+
* `entries.sort(([a], [b]) => a.localeCompare(b))` step, shared by the budget check's
|
|
190
|
+
* capped-role rows (doctor-holds.ts's checkBudget), the permit pool's provider fragment
|
|
191
|
+
* (ui/badges.ts's permitsBadge), and the doctor's rate-limit standing-holds rows
|
|
192
|
+
* (doctor-holds.ts's checkRateLimitHolds), so those sites cannot drift on how a possibly
|
|
193
|
+
* `undefined` key (a rate-limit hold recorded with no provider) orders. A missing key sorts
|
|
194
|
+
* first, matching the `(a ?? "")` guard the hold site alone needed; on a defined string the
|
|
195
|
+
* guard is a no-op, so the comparator is identical for the other two. Returns a fresh sorted
|
|
196
|
+
* array; the input is never mutated. (Deliberately unlike the value-descending `sortByValueDesc`
|
|
197
|
+
* and the count-then-key `rankCountEntries` above, which sort by a different axis. Also used to
|
|
198
|
+
* stabilize a Map's insertion order before an assertion in test/once-round.test.ts.) */
|
|
199
|
+
export function sortEntriesByKey(entries) {
|
|
200
|
+
return [...entries].sort(([a], [b]) => (a ?? "").localeCompare(b ?? ""));
|
|
201
|
+
}
|
|
165
202
|
/** rankByCount for `[key, count]` entry pairs (count first): src/report/report-render.ts's
|
|
166
203
|
* rankedRoleMap, src/team/failure-spread.ts's strongest-kind pick, and
|
|
167
204
|
* src/doctor/doctor-contention-checks.ts's check-wait and provider-wait rankings. */
|
|
@@ -80,10 +80,6 @@ const BORROW_ORDER = {
|
|
|
80
80
|
default: ["default", "strong", "small"],
|
|
81
81
|
strong: ["strong", "default"],
|
|
82
82
|
};
|
|
83
|
-
/** Exported for imageRerouteTarget (src/agent/image-input.ts), which walks a rerouting role's
|
|
84
|
-
* tier through the same borrow order the budget fallback uses (skipping the tier itself), and
|
|
85
|
-
* for the doctor's `image input` check naming the tier a text-only role reroutes to. */
|
|
86
|
-
export { BORROW_ORDER };
|
|
87
83
|
/** A tier's OWN fallback chain, first step first — what the `fallback` key declares at that
|
|
88
84
|
* tier's map entry (a selector or a list of them), or (default only) the string/list form of
|
|
89
85
|
* `fallback` or the legacy `fallbackModel` object (a one-element chain). Empty when the tier
|
|
@@ -47,163 +47,195 @@ export function newLiveConfigReload(deps) {
|
|
|
47
47
|
// the config application that follows the emission. bestEffortEventLogger (events.ts) holds
|
|
48
48
|
// the shared "logging must not throw" policy.
|
|
49
49
|
const { emit, warn } = bestEffortEventLogger(deps.root);
|
|
50
|
+
// Warn once per vanish and once per reappearance of tumwater.json.
|
|
51
|
+
function noteConfigPresence(reloaded) {
|
|
52
|
+
// A missing file keeps the last-known-good config too, and never reloads as defaults: the
|
|
53
|
+
// team was started on a real file (startup refuses to run without one), so its vanishing
|
|
54
|
+
// mid-run is an incident — a merge's fast-forward deleted it on 2026-09-22 and the team
|
|
55
|
+
// silently ran 8.6 h on defaults (BUGS.md 2026-09-23). One warning per vanish, not per
|
|
56
|
+
// poll; its return logs one line, then the normal reload below diffs it against the
|
|
57
|
+
// retained config — so config_changed names only what the returned file really changed.
|
|
58
|
+
if (reloaded.missing) {
|
|
59
|
+
if (!configMissing) {
|
|
60
|
+
warn(`tumwater.json missing — keeping current config until it returns (${configPath(deps.root)})`);
|
|
61
|
+
configMissing = true;
|
|
62
|
+
lastConfigError = null; // Whatever state it returns in is stated afresh.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (configMissing) {
|
|
66
|
+
warn("tumwater.json reappeared — reloading it");
|
|
67
|
+
configMissing = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Refuse a live edit that routes a seam to an API-key-logged-in codex, keeping prevLive.
|
|
71
|
+
function refuseApiKeyCodex(nextConfig, prevBackends) {
|
|
72
|
+
// A live edit that routes a seam to a codex logged in with an API key is refused before
|
|
73
|
+
// the reload is applied: startup refuses the same codex seam (decision 5), and here a
|
|
74
|
+
// warn-only would let the running team spend per-token Codex the budget cap cannot see
|
|
75
|
+
// (review objection 1). Refusing keeps the last-known-good config, like a missing or
|
|
76
|
+
// broken file. The seed runs once per root (`codex login status` is a real probe); the
|
|
77
|
+
// persisted account file is the voice of the refusal on later polls, re-probing a stale
|
|
78
|
+
// api answer (codex-account.ts) so the operator's `codex login --device-auth` remedy
|
|
79
|
+
// clears it at the next seed. A subscription codex passes through to the backend loop.
|
|
80
|
+
const newlyRoutedCodex = !prevBackends.has("codex") && backendsInUse(nextConfig).includes("codex");
|
|
81
|
+
if (newlyRoutedCodex && !liveCodexSeedAttempted.has(deps.root)) {
|
|
82
|
+
liveCodexSeedAttempted.add(deps.root);
|
|
83
|
+
seedCodexAccount(deps.root, nextConfig);
|
|
84
|
+
}
|
|
85
|
+
if (newlyRoutedCodex && readCodexAccount(deps.root)?.billing === "api") {
|
|
86
|
+
if (codexLiveRefused)
|
|
87
|
+
return true;
|
|
88
|
+
codexLiveRefused = true;
|
|
89
|
+
warn("refusing live edit: Codex is logged in with an API key; tumwater runs Codex on a ChatGPT subscription only — run `CODEX_HOME=<home> codex login --device-auth`");
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
// A poll that did not refuse clears the warn-once marker so a later re-entry into the
|
|
93
|
+
// refusal (the operator reverts, then re-adds the API-billed codex seam) warns again.
|
|
94
|
+
codexLiveRefused = false;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
// Emit config_changed and model_changed for what this reload actually changed.
|
|
98
|
+
function emitConfigChangeEvents(nextConfig) {
|
|
99
|
+
// A live edit that changes behavior elsewhere logs one event naming the settings that
|
|
100
|
+
// changed (maxConcurrent and sessionRetentionDays have their own events elsewhere).
|
|
101
|
+
const changedKeys = changedConfigKeys(prevLive, nextConfig);
|
|
102
|
+
if (changedKeys.length > 0) {
|
|
103
|
+
emit({ source: TUMWATER_SOURCE, type: "config_changed", keys: changedKeys });
|
|
104
|
+
// A real model-wiring change gets its own event naming the new selector(s), which the
|
|
105
|
+
// key list cannot show; a non-model edit (instructions, an interval) emits nothing.
|
|
106
|
+
const fromModel = teamModelLabel(prevLive);
|
|
107
|
+
const toModel = teamModelLabel(nextConfig);
|
|
108
|
+
const roleDiffs = changedKeys
|
|
109
|
+
.filter((key) => key.startsWith("roles."))
|
|
110
|
+
.map((key) => {
|
|
111
|
+
const role = key.slice("roles.".length);
|
|
112
|
+
const from = modelSelectorField(configForRole(prevLive, role)).model ?? null;
|
|
113
|
+
const to = modelSelectorField(configForRole(nextConfig, role)).model ?? null;
|
|
114
|
+
return { role, from, to };
|
|
115
|
+
})
|
|
116
|
+
.filter((d) => d.from !== d.to);
|
|
117
|
+
if (fromModel !== toModel || roleDiffs.length > 0)
|
|
118
|
+
emit({
|
|
119
|
+
source: TUMWATER_SOURCE,
|
|
120
|
+
type: "model_changed",
|
|
121
|
+
from: fromModel,
|
|
122
|
+
to: toModel,
|
|
123
|
+
...(roleDiffs.length > 0 ? { roles: roleDiffs } : {}),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Verify and seed each backend a live edit newly routes to.
|
|
128
|
+
function seedNewlyRoutedBackends(nextConfig, prevBackends) {
|
|
129
|
+
// A live edit that routes a seam to a backend the previous config did not use: the
|
|
130
|
+
// team keeps running, but that backend must be verified and its account (claude) or
|
|
131
|
+
// models catalog (opencode) seeded, exactly as startup would — so a claude reviewer
|
|
132
|
+
// added to a pi team does not start uncheckable. A missing binary is a warning event,
|
|
133
|
+
// never a crash; a present one logs its verdict as a backend_binary event. The seed is
|
|
134
|
+
// attempted once per root. (The codex seed moved to the refusal check at the top of
|
|
135
|
+
// this block, which also refuses an API-key-logged-in codex seam.)
|
|
136
|
+
for (const backend of backendsInUse(nextConfig)) {
|
|
137
|
+
if (prevBackends.has(backend))
|
|
138
|
+
continue;
|
|
139
|
+
const resolved = resolveBackendBin(nextConfig, backend);
|
|
140
|
+
if (findBackendBinary(resolved)) {
|
|
141
|
+
emit({ source: TUMWATER_SOURCE, type: "backend_binary", agent: backend, found: true });
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
warn(`newly routed ${backend} backend: ${backendMissingMessage(resolved, backend)}`);
|
|
145
|
+
}
|
|
146
|
+
if (backend === "claude" && !liveClaudeSeedAttempted.has(deps.root)) {
|
|
147
|
+
liveClaudeSeedAttempted.add(deps.root);
|
|
148
|
+
seedClaudeAccount(deps.root, nextConfig);
|
|
149
|
+
}
|
|
150
|
+
if (backend === "opencode" && !liveOpencodeSeedAttempted.has(deps.root)) {
|
|
151
|
+
liveOpencodeSeedAttempted.add(deps.root);
|
|
152
|
+
// Not awaited: poll() is synchronous and a models.dev fetch must not stall the loop.
|
|
153
|
+
void seedOpencodeCatalog(deps.root, nextConfig, { ...deps.opencodeSeed, warn }).catch((err) => warn(`open code model catalog seed failed: ${errorMessage(err)}`));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Live-resize the concurrency cap and the per-provider key caps.
|
|
158
|
+
function resizeConcurrency(nextConfig) {
|
|
159
|
+
// Live-resize the concurrency cap: a mid-run edit changes how many pi runs execute
|
|
160
|
+
// concurrently within this poll — no restart. Growing admits already-queued runs;
|
|
161
|
+
// shrinking never preempts in-flight work, it only caps future grants.
|
|
162
|
+
const newMaxConcurrent = Math.max(1, nextConfig.maxConcurrent);
|
|
163
|
+
if (newMaxConcurrent !== lastMaxConcurrent) {
|
|
164
|
+
deps.semaphore.setCapacity(newMaxConcurrent);
|
|
165
|
+
emit({ source: TUMWATER_SOURCE, type: "max_concurrent_changed", from: lastMaxConcurrent, to: newMaxConcurrent });
|
|
166
|
+
lastMaxConcurrent = newMaxConcurrent;
|
|
167
|
+
}
|
|
168
|
+
// Live-resize the per-provider caps (plans/provider-concurrency.md §7): an edit
|
|
169
|
+
// re-applies the whole map to the pool, and each changed provider fires its own
|
|
170
|
+
// `provider_concurrency_changed` event. setKeyCaps never preempts — shrinking only
|
|
171
|
+
// withholds future grants on that key until releases drain it under the new cap.
|
|
172
|
+
const nextKeyCaps = nextConfig.maxConcurrentPerProvider ?? {};
|
|
173
|
+
deps.semaphore.setKeyCaps(nextKeyCaps);
|
|
174
|
+
for (const provider of new Set([...Object.keys(lastKeyCaps), ...Object.keys(nextKeyCaps)])) {
|
|
175
|
+
const from = lastKeyCaps[provider];
|
|
176
|
+
const to = nextKeyCaps[provider];
|
|
177
|
+
if (from !== to) {
|
|
178
|
+
emit({
|
|
179
|
+
source: TUMWATER_SOURCE,
|
|
180
|
+
type: "provider_concurrency_changed",
|
|
181
|
+
provider,
|
|
182
|
+
from: from ?? 0,
|
|
183
|
+
to: to ?? 0,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
lastKeyCaps = nextKeyCaps;
|
|
188
|
+
}
|
|
189
|
+
// Sync the runner list to newly enabled roles and warn on enable/disable.
|
|
190
|
+
function syncRunners(nextConfig) {
|
|
191
|
+
const nowEnabled = runnerIds(nextConfig);
|
|
192
|
+
// Enabling a role — or raising `roles.<id>.instances` — mid-run starts it: create the
|
|
193
|
+
// runner for each newly present role id (its persisted state survives). A lowered
|
|
194
|
+
// instance count keeps its runner; `runnerEnabled` skips its future runs. A scoped round
|
|
195
|
+
// skips every other id: the enabling is logged (the warn loop below), but its runner
|
|
196
|
+
// waits for the next unscoped round.
|
|
197
|
+
for (const role of nowEnabled) {
|
|
198
|
+
if (deps.roleFilter !== undefined && !deps.roleFilter.includes(role))
|
|
199
|
+
continue;
|
|
200
|
+
if (!deps.runners.some((r) => r.role === role))
|
|
201
|
+
deps.runners.push(new RoleRunner(deps.root, role, nextConfig, deps.mainBranch, deps.runnerSignal));
|
|
202
|
+
}
|
|
203
|
+
for (const role of prevEnabled)
|
|
204
|
+
if (!nowEnabled.includes(role))
|
|
205
|
+
warn(`role ${role} disabled — no new runs`);
|
|
206
|
+
for (const role of nowEnabled)
|
|
207
|
+
if (!prevEnabled.has(role))
|
|
208
|
+
warn(`role ${role} enabled — starting runs`);
|
|
209
|
+
prevEnabled = new Set(nowEnabled);
|
|
210
|
+
}
|
|
211
|
+
// Warn once per distinct broken-file error text.
|
|
212
|
+
function noteInvalidConfig(reloaded) {
|
|
213
|
+
if (reloaded.error && reloaded.error !== lastConfigError) {
|
|
214
|
+
warn(`tumwater.json invalid — keeping current config: ${reloaded.error}`);
|
|
215
|
+
lastConfigError = reloaded.error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
50
218
|
return {
|
|
51
219
|
poll() {
|
|
52
220
|
const reloaded = loadConfigCached(deps.root);
|
|
53
|
-
|
|
54
|
-
// team was started on a real file (startup refuses to run without one), so its vanishing
|
|
55
|
-
// mid-run is an incident — a merge's fast-forward deleted it on 2026-09-22 and the team
|
|
56
|
-
// silently ran 8.6 h on defaults (BUGS.md 2026-09-23). One warning per vanish, not per
|
|
57
|
-
// poll; its return logs one line, then the normal reload below diffs it against the
|
|
58
|
-
// retained config — so config_changed names only what the returned file really changed.
|
|
59
|
-
if (reloaded.missing) {
|
|
60
|
-
if (!configMissing) {
|
|
61
|
-
warn(`tumwater.json missing — keeping current config until it returns (${configPath(deps.root)})`);
|
|
62
|
-
configMissing = true;
|
|
63
|
-
lastConfigError = null; // Whatever state it returns in is stated afresh.
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else if (configMissing) {
|
|
67
|
-
warn("tumwater.json reappeared — reloading it");
|
|
68
|
-
configMissing = false;
|
|
69
|
-
}
|
|
221
|
+
noteConfigPresence(reloaded);
|
|
70
222
|
if (reloaded.config) {
|
|
71
223
|
const nextConfig = reloaded.config;
|
|
72
|
-
// A live edit that routes a seam to a codex logged in with an API key is refused before
|
|
73
|
-
// the reload is applied: startup refuses the same codex seam (decision 5), and here a
|
|
74
|
-
// warn-only would let the running team spend per-token Codex the budget cap cannot see
|
|
75
|
-
// (review objection 1). Refusing keeps the last-known-good config, like a missing or
|
|
76
|
-
// broken file. The seed runs once per root (`codex login status` is a real probe); the
|
|
77
|
-
// persisted account file is the voice of the refusal on later polls, re-probing a stale
|
|
78
|
-
// api answer (codex-account.ts) so the operator's `codex login --device-auth` remedy
|
|
79
|
-
// clears it at the next seed. A subscription codex passes through to the backend loop.
|
|
80
224
|
const prevBackends = new Set(backendsInUse(prevLive));
|
|
81
|
-
|
|
82
|
-
if (newlyRoutedCodex && !liveCodexSeedAttempted.has(deps.root)) {
|
|
83
|
-
liveCodexSeedAttempted.add(deps.root);
|
|
84
|
-
seedCodexAccount(deps.root, nextConfig);
|
|
85
|
-
}
|
|
86
|
-
if (newlyRoutedCodex && readCodexAccount(deps.root)?.billing === "api") {
|
|
87
|
-
if (codexLiveRefused)
|
|
88
|
-
return prevLive;
|
|
89
|
-
codexLiveRefused = true;
|
|
90
|
-
warn("refusing live edit: Codex is logged in with an API key; tumwater runs Codex on a ChatGPT subscription only — run `CODEX_HOME=<home> codex login --device-auth`");
|
|
225
|
+
if (refuseApiKeyCodex(nextConfig, prevBackends))
|
|
91
226
|
return prevLive;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
live = reloaded.config;
|
|
97
|
-
// A live edit that changes behavior elsewhere logs one event naming the settings that
|
|
98
|
-
// changed (maxConcurrent and sessionRetentionDays have their own events elsewhere).
|
|
99
|
-
const changedKeys = changedConfigKeys(prevLive, reloaded.config);
|
|
100
|
-
if (changedKeys.length > 0) {
|
|
101
|
-
emit({ source: TUMWATER_SOURCE, type: "config_changed", keys: changedKeys });
|
|
102
|
-
// A real model-wiring change gets its own event naming the new selector(s), which the
|
|
103
|
-
// key list cannot show; a non-model edit (instructions, an interval) emits nothing.
|
|
104
|
-
const fromModel = teamModelLabel(prevLive);
|
|
105
|
-
const toModel = teamModelLabel(reloaded.config);
|
|
106
|
-
const roleDiffs = changedKeys
|
|
107
|
-
.filter((key) => key.startsWith("roles."))
|
|
108
|
-
.map((key) => {
|
|
109
|
-
const role = key.slice("roles.".length);
|
|
110
|
-
const from = modelSelectorField(configForRole(prevLive, role)).model ?? null;
|
|
111
|
-
const to = modelSelectorField(configForRole(nextConfig, role)).model ?? null;
|
|
112
|
-
return { role, from, to };
|
|
113
|
-
})
|
|
114
|
-
.filter((d) => d.from !== d.to);
|
|
115
|
-
if (fromModel !== toModel || roleDiffs.length > 0)
|
|
116
|
-
emit({
|
|
117
|
-
source: TUMWATER_SOURCE,
|
|
118
|
-
type: "model_changed",
|
|
119
|
-
from: fromModel,
|
|
120
|
-
to: toModel,
|
|
121
|
-
...(roleDiffs.length > 0 ? { roles: roleDiffs } : {}),
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
// A live edit that routes a seam to a backend the previous config did not use: the
|
|
125
|
-
// team keeps running, but that backend must be verified and its account (claude) or
|
|
126
|
-
// models catalog (opencode) seeded, exactly as startup would — so a claude reviewer
|
|
127
|
-
// added to a pi team does not start uncheckable. A missing binary is a warning event,
|
|
128
|
-
// never a crash; a present one logs its verdict as a backend_binary event. The seed is
|
|
129
|
-
// attempted once per root. (The codex seed moved to the refusal check at the top of
|
|
130
|
-
// this block, which also refuses an API-key-logged-in codex seam.)
|
|
131
|
-
for (const backend of backendsInUse(reloaded.config)) {
|
|
132
|
-
if (prevBackends.has(backend))
|
|
133
|
-
continue;
|
|
134
|
-
const resolved = resolveBackendBin(reloaded.config, backend);
|
|
135
|
-
if (findBackendBinary(resolved)) {
|
|
136
|
-
emit({ source: TUMWATER_SOURCE, type: "backend_binary", agent: backend, found: true });
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
warn(`newly routed ${backend} backend: ${backendMissingMessage(resolved, backend)}`);
|
|
140
|
-
}
|
|
141
|
-
if (backend === "claude" && !liveClaudeSeedAttempted.has(deps.root)) {
|
|
142
|
-
liveClaudeSeedAttempted.add(deps.root);
|
|
143
|
-
seedClaudeAccount(deps.root, reloaded.config);
|
|
144
|
-
}
|
|
145
|
-
if (backend === "opencode" && !liveOpencodeSeedAttempted.has(deps.root)) {
|
|
146
|
-
liveOpencodeSeedAttempted.add(deps.root);
|
|
147
|
-
// Not awaited: poll() is synchronous and a models.dev fetch must not stall the loop.
|
|
148
|
-
void seedOpencodeCatalog(deps.root, reloaded.config, { ...deps.opencodeSeed, warn }).catch((err) => warn(`open code model catalog seed failed: ${errorMessage(err)}`));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
prevLive = reloaded.config;
|
|
227
|
+
live = nextConfig;
|
|
228
|
+
emitConfigChangeEvents(nextConfig);
|
|
229
|
+
seedNewlyRoutedBackends(nextConfig, prevBackends);
|
|
230
|
+
prevLive = nextConfig;
|
|
152
231
|
for (const r of deps.runners)
|
|
153
|
-
r.config =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// shrinking never preempts in-flight work, it only caps future grants.
|
|
157
|
-
const newMaxConcurrent = Math.max(1, reloaded.config.maxConcurrent);
|
|
158
|
-
if (newMaxConcurrent !== lastMaxConcurrent) {
|
|
159
|
-
deps.semaphore.setCapacity(newMaxConcurrent);
|
|
160
|
-
emit({ source: TUMWATER_SOURCE, type: "max_concurrent_changed", from: lastMaxConcurrent, to: newMaxConcurrent });
|
|
161
|
-
lastMaxConcurrent = newMaxConcurrent;
|
|
162
|
-
}
|
|
163
|
-
// Live-resize the per-provider caps (plans/provider-concurrency.md §7): an edit
|
|
164
|
-
// re-applies the whole map to the pool, and each changed provider fires its own
|
|
165
|
-
// `provider_concurrency_changed` event. setKeyCaps never preempts — shrinking only
|
|
166
|
-
// withholds future grants on that key until releases drain it under the new cap.
|
|
167
|
-
const nextKeyCaps = reloaded.config.maxConcurrentPerProvider ?? {};
|
|
168
|
-
deps.semaphore.setKeyCaps(nextKeyCaps);
|
|
169
|
-
for (const provider of new Set([...Object.keys(lastKeyCaps), ...Object.keys(nextKeyCaps)])) {
|
|
170
|
-
const from = lastKeyCaps[provider];
|
|
171
|
-
const to = nextKeyCaps[provider];
|
|
172
|
-
if (from !== to) {
|
|
173
|
-
emit({
|
|
174
|
-
source: TUMWATER_SOURCE,
|
|
175
|
-
type: "provider_concurrency_changed",
|
|
176
|
-
provider,
|
|
177
|
-
from: from ?? 0,
|
|
178
|
-
to: to ?? 0,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
lastKeyCaps = nextKeyCaps;
|
|
183
|
-
const nowEnabled = runnerIds(reloaded.config);
|
|
184
|
-
// Enabling a role — or raising `roles.<id>.instances` — mid-run starts it: create the
|
|
185
|
-
// runner for each newly present role id (its persisted state survives). A lowered
|
|
186
|
-
// instance count keeps its runner; `runnerEnabled` skips its future runs. A scoped round
|
|
187
|
-
// skips every other id: the enabling is logged (the warn loop below), but its runner
|
|
188
|
-
// waits for the next unscoped round.
|
|
189
|
-
for (const role of nowEnabled) {
|
|
190
|
-
if (deps.roleFilter !== undefined && !deps.roleFilter.includes(role))
|
|
191
|
-
continue;
|
|
192
|
-
if (!deps.runners.some((r) => r.role === role))
|
|
193
|
-
deps.runners.push(new RoleRunner(deps.root, role, reloaded.config, deps.mainBranch, deps.runnerSignal));
|
|
194
|
-
}
|
|
195
|
-
for (const role of prevEnabled)
|
|
196
|
-
if (!nowEnabled.includes(role))
|
|
197
|
-
warn(`role ${role} disabled — no new runs`);
|
|
198
|
-
for (const role of nowEnabled)
|
|
199
|
-
if (!prevEnabled.has(role))
|
|
200
|
-
warn(`role ${role} enabled — starting runs`);
|
|
201
|
-
prevEnabled = new Set(nowEnabled);
|
|
232
|
+
r.config = nextConfig;
|
|
233
|
+
resizeConcurrency(nextConfig);
|
|
234
|
+
syncRunners(nextConfig);
|
|
202
235
|
lastConfigError = null;
|
|
203
236
|
}
|
|
204
|
-
else
|
|
205
|
-
|
|
206
|
-
lastConfigError = reloaded.error;
|
|
237
|
+
else {
|
|
238
|
+
noteInvalidConfig(reloaded);
|
|
207
239
|
}
|
|
208
240
|
return live;
|
|
209
241
|
},
|