tumwater 0.4.1 → 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/backend-traits.js +14 -0
- package/dist/src/agent/claude/claude-stream.js +27 -31
- package/dist/src/agent/codex/codex-rollout.js +38 -24
- package/dist/src/agent/codex/codex-stream.js +71 -38
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +4 -4
- package/dist/src/agent/run-agent.js +11 -2
- package/dist/src/agent/run-settle.js +22 -13
- package/dist/src/agent/run-spawn.js +3 -12
- package/dist/src/agent/run-stream.js +2 -10
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/backlog/backlog-archive.js +64 -165
- package/dist/src/backlog/backlog-eligibility.js +50 -58
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +2 -2
- package/dist/src/backlog/backlog-md.js +38 -27
- package/dist/src/backlog/backlog-release.js +11 -19
- package/dist/src/backlog/backlog-structure.js +14 -1
- package/dist/src/backlog/backlog-write.js +1 -1
- package/dist/src/backlog/backlog.js +5 -4
- package/dist/src/backlog/question-answer.js +30 -14
- package/dist/src/baseline/main-baseline.js +30 -20
- package/dist/src/baseline/main-guardrails.js +54 -17
- package/dist/src/baseline/main-watch.js +1 -1
- package/dist/src/budget/budget-wiring.js +0 -5
- package/dist/src/budget/budget.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-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +1 -4
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +27 -50
- package/dist/src/cli/cli-start.js +62 -12
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +6 -7
- package/dist/src/cli/log-commands.js +3 -1
- package/dist/src/cli.js +3 -7
- package/dist/src/collections.js +21 -7
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +19 -26
- package/dist/src/doctor/doctor-backlog-bugs.js +17 -115
- package/dist/src/doctor/doctor-backlog-plans.js +8 -8
- package/dist/src/doctor/doctor-backlog.js +22 -36
- package/dist/src/doctor/doctor-check-names.js +2 -2
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-contention-checks.js +1 -1
- package/dist/src/doctor/doctor-holds.js +13 -11
- 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 +85 -48
- package/dist/src/doctor/doctor-project-checks.js +66 -17
- package/dist/src/doctor/doctor-role-notes.js +6 -24
- package/dist/src/doctor/doctor.js +6 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +5 -15
- 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/failure/failure-state-change.js +183 -68
- package/dist/src/failure/time-spend.js +1 -1
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/git/slot-salvage.js +5 -4
- package/dist/src/gui/gui-admin-endpoints.js +2 -3
- package/dist/src/gui/gui-backlog-commands.js +1 -1
- package/dist/src/gui/gui-backlog-endpoints.js +1 -7
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-operator-commands.js +0 -12
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +2 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/history/history.js +102 -67
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/prompt-commands.js +24 -19
- package/dist/src/init/init.js +68 -15
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-post-review-drain.js +1 -1
- package/dist/src/operator/operator-commands.js +68 -47
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +0 -6
- package/dist/src/pi/pi-watchdogs.js +20 -1
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/process/process.js +23 -0
- package/dist/src/process/supervisor.js +17 -1
- package/dist/src/prompt/gate-notes.js +9 -9
- package/dist/src/prompt/notes-backlog.js +11 -24
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +4 -5
- package/dist/src/report/report-data.js +10 -225
- package/dist/src/report/report-fold.js +20 -295
- package/dist/src/report/report-main-red.js +45 -42
- package/dist/src/report/report-render.js +66 -39
- 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 +3 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +9 -9
- package/dist/src/review/quarantine-render.js +2 -1
- package/dist/src/roles/role-catalog.js +18 -30
- package/dist/src/roles/role-guidance.js +18 -43
- package/dist/src/roles/role-view.js +1 -2
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-detail-data.js +23 -7
- package/dist/src/run/run-detail-render.js +6 -0
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-prompt.js +100 -56
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +4 -21
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +8 -73
- package/dist/src/run/stage-backlog-checks.js +2 -29
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-run.js +2 -79
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +3 -10
- package/dist/src/status/status-data.js +5 -9
- package/dist/src/status/status-role-notes.js +18 -14
- package/dist/src/status/status-roles.js +2 -35
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +7 -212
- package/dist/src/ui/badges.js +2 -3
- package/dist/src/ui/gui/gui-client-boot.js +19 -4
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-report-chart.js +10 -8
- package/dist/src/ui/gui/gui-client-report.js +1 -2
- package/dist/src/ui/gui/gui-client-settings.js +0 -23
- package/dist/src/ui/gui/gui-client-team.js +26 -0
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/status-model.js +2 -2
- package/dist/src/ui/status-payload.js +3 -3
- package/dist/src/ui/status-render.js +3 -3
- package/dist/src/ui/team-alert-backlog.js +23 -54
- package/dist/src/ui/team-alert-shared.js +23 -10
- package/dist/src/ui/team-alert-status.js +8 -3
- package/dist/src/ui/team-alert-tail.js +1 -14
- package/dist/src/ui/team-alerts.js +6 -17
- package/dist/src/ui/transcript-tail.js +137 -94
- 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 +105 -93
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- 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-pause.js +209 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +44 -34
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +121 -108
- package/dist/src/ui/tui/tui-editor-release.js +50 -42
- 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 +58 -46
- package/dist/src/ui/tui/tui-editor-role-key.js +49 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-frame.js +1 -1
- package/dist/src/ui/tui/tui-keys.js +1 -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-health.js +3 -24
- package/dist/src/ui/tui/tui-pane-history.js +20 -24
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +3 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +151 -128
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +2 -3
- package/dist/src/ui/tui/tui.js +3 -3
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +30 -6
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- package/dist/src/ui/team-alert-backlog-feed.js +0 -181
- package/dist/src/ui/team-alert-merge.js +0 -183
|
@@ -12,7 +12,7 @@ import { escapeRegExp } from "../text/text.js";
|
|
|
12
12
|
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
13
13
|
import { loadConfigForCheck, opencodeSeamsInUse, seamedModels, } from "./doctor-model-checks.js";
|
|
14
14
|
/** The doctor's opencode credential and leak readiness check, split out of doctor-model-checks.ts
|
|
15
|
-
* (whose shared seam/model framework and the pi
|
|
15
|
+
* (whose shared seam/model framework and the pi and tier-models checks stay there):
|
|
16
16
|
* checkOpencode and its helpers (opencodeProviderOf, opencodeAuthLists, jsonKeyPresent,
|
|
17
17
|
* opencodeConfigLeaks). This is the doctor's only check that spawns the `opencode` CLI and reads
|
|
18
18
|
* its catalog and the operator's global opencode config — a distinct concern from the
|
|
@@ -108,6 +108,80 @@ function opencodeConfigLeaks(configHome) {
|
|
|
108
108
|
warns.push(`${agentsMd} still loads into team runs`);
|
|
109
109
|
return warns;
|
|
110
110
|
}
|
|
111
|
+
/** A catalog model's price text — "free" when free, the billed-demotion phrase when `billedAt`
|
|
112
|
+
* is set, else the per-M prices — so opencodeModelRow can name the price without the nested
|
|
113
|
+
* ternary and its two `?? 0` fallbacks. */
|
|
114
|
+
function opencodePriceText(free, billedAt, inputPerM, outputPerM) {
|
|
115
|
+
if (free)
|
|
116
|
+
return "free";
|
|
117
|
+
if (billedAt !== undefined)
|
|
118
|
+
return `no longer free (billed ${formatDate(new Date(billedAt))})`;
|
|
119
|
+
return `$${inputPerM ?? 0}/$${outputPerM ?? 0} per M`;
|
|
120
|
+
}
|
|
121
|
+
/** One opencode catalog model's row: today's per-model body from the cache key through the
|
|
122
|
+
* dropped-effort warning, returning the catalog note and any warnings, or undefined when the
|
|
123
|
+
* model is missing from the cache (the caller owns that failure message). `warnings` holds the
|
|
124
|
+
* billed-at demotion line and the dropped-effort line in today's order. */
|
|
125
|
+
function opencodeModelRow(cache, model, thinking) {
|
|
126
|
+
const key = opencodeModelKey(model);
|
|
127
|
+
const entry = cache.models[key];
|
|
128
|
+
if (entry === undefined)
|
|
129
|
+
return undefined;
|
|
130
|
+
const warnings = [];
|
|
131
|
+
const free = opencodeModelFree(cache, key);
|
|
132
|
+
if (entry.billedAt !== undefined) {
|
|
133
|
+
warnings.push(`${model} was free in the catalog but billed once on ${formatDate(new Date(entry.billedAt))} — the budget gate now prices it`);
|
|
134
|
+
}
|
|
135
|
+
const window = entry.context !== undefined ? `, ${tokenWindowLabel(entry.context)}` : "";
|
|
136
|
+
const note = `${model} — ${opencodePriceText(free, entry.billedAt, entry.inputPerM, entry.outputPerM)}${window}`;
|
|
137
|
+
// A model with no effort list at all supports no thinking level, so a requested level is
|
|
138
|
+
// dropped just the same as one the list omits.
|
|
139
|
+
if (thinking !== undefined && thinking !== "" && !entry.efforts?.includes(thinking)) {
|
|
140
|
+
warnings.push(`${model} has no ${thinking} effort — that thinking level is dropped`);
|
|
141
|
+
}
|
|
142
|
+
return { note, warnings };
|
|
143
|
+
}
|
|
144
|
+
/** The team's opencode database and spill directory sizes as one information line (`db …,
|
|
145
|
+
* spill …`), reading the db, the spill dir and their byte formatting — today's storage tail. */
|
|
146
|
+
function opencodeStorageNote(root) {
|
|
147
|
+
const db = statOrNull(opencodeDbPath(root));
|
|
148
|
+
const spillDir = opencodeSpillDir(root);
|
|
149
|
+
const spill = spillDir !== "" && isDirectory(spillDir) ? treeBytes(spillDir) : null;
|
|
150
|
+
return `db ${formatBytes(db?.size ?? 0)}${spill !== null ? `, spill ${formatBytes(spill)}` : ", no spill dir"}`;
|
|
151
|
+
}
|
|
152
|
+
/** Walk the opencode-routed model pairs into today's per-model fails, warns and notes: the
|
|
153
|
+
* catalog-miss failure, the catalog note and its warnings from opencodeModelRow, and the
|
|
154
|
+
* non-gateway credential probe each provider. Returns the three arrays checkOpencode assembles. */
|
|
155
|
+
async function collectOpencodeModelRows(cache, models, config, pathEnv, timeoutMs) {
|
|
156
|
+
const fails = [];
|
|
157
|
+
const warns = [];
|
|
158
|
+
const notes = [];
|
|
159
|
+
for (const pair of models) {
|
|
160
|
+
const model = pair.model;
|
|
161
|
+
const row = opencodeModelRow(cache, model, pair.thinking);
|
|
162
|
+
if (row === undefined) {
|
|
163
|
+
fails.push(`${model} is not in the opencode catalog cache`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
notes.push(row.note);
|
|
167
|
+
warns.push(...row.warnings);
|
|
168
|
+
const provider = opencodeProviderOf(model);
|
|
169
|
+
if (provider !== undefined && !(await opencodeAuthLists(config, provider, pathEnv, timeoutMs))) {
|
|
170
|
+
warns.push(`${model} is a non-gateway provider opencode does not list a credential for`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { fails, warns, notes };
|
|
174
|
+
}
|
|
175
|
+
/** The check's outcome from the accumulated fails/warns/notes and the storage tail: the joined
|
|
176
|
+
* base line ("opencode ready — …" when nothing is wrong, else the found lines) and the
|
|
177
|
+
* fail/warn/ok verdict by severity. */
|
|
178
|
+
function opencodeOutcome(fails, warns, notes, sizeNotes) {
|
|
179
|
+
const found = [...fails, ...warns];
|
|
180
|
+
const base = found.length === 0 ? `opencode ready — ${notes.join("; ")}; ${sizeNotes}` : found.join("; ");
|
|
181
|
+
if (fails.length > 0)
|
|
182
|
+
return failOutcome(base);
|
|
183
|
+
return (warns.length > 0 ? warnOutcome : okOutcome)(base);
|
|
184
|
+
}
|
|
111
185
|
/** OpenCode model, credential and leak readiness (plans/opencode-backend.md §7, part 7/7):
|
|
112
186
|
* every model routed to opencode must be in the catalog cache — an unknown model fails, a
|
|
113
187
|
* cached one reads "free" or its per-M prices and context window, and a thinking level with no
|
|
@@ -120,57 +194,20 @@ function opencodeConfigLeaks(configHome) {
|
|
|
120
194
|
* PATH or HOME. A model's freeness is read from opencodeModelFree — the same call the budget
|
|
121
195
|
* gate and status use — so a catalog-free model that an observed bill demoted to billed reads
|
|
122
196
|
* as billed here, not free, keeping the row from drifting from what the team is billed. The
|
|
123
|
-
*
|
|
124
|
-
*
|
|
197
|
+
* per-model catalog row lives in opencodeModelRow, the database-and-spill size line in
|
|
198
|
+
* opencodeStorageNote; both are the two extracted jobs the reader names by. The version half of
|
|
199
|
+
* the opencode pre-flight is the sibling checkOpencodeVersion in doctor-checks.ts. */
|
|
125
200
|
export async function checkOpencode(root, pathEnv = envPath(), home = os.homedir(), xdgConfigHome = process.env.XDG_CONFIG_HOME ?? "", timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
126
201
|
const config = loadConfigForCheck(root);
|
|
127
202
|
if ("level" in config)
|
|
128
203
|
return config;
|
|
129
204
|
if (!opencodeSeamsInUse(config))
|
|
130
205
|
return okOutcome("no model jobs run on opencode");
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const entry = cache.models[key];
|
|
139
|
-
if (entry === undefined) {
|
|
140
|
-
fails.push(`${pair.model} is not in the opencode catalog cache`);
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
const free = opencodeModelFree(cache, key);
|
|
144
|
-
if (entry.billedAt !== undefined) {
|
|
145
|
-
warns.push(`${pair.model} was free in the catalog but billed once on ${formatDate(new Date(entry.billedAt))} — the budget gate now prices it`);
|
|
146
|
-
}
|
|
147
|
-
const price = free
|
|
148
|
-
? "free"
|
|
149
|
-
: entry.billedAt !== undefined
|
|
150
|
-
? `no longer free (billed ${formatDate(new Date(entry.billedAt))})`
|
|
151
|
-
: `$${entry.inputPerM ?? 0}/$${entry.outputPerM ?? 0} per M`;
|
|
152
|
-
const window = entry.context !== undefined ? `, ${tokenWindowLabel(entry.context)}` : "";
|
|
153
|
-
notes.push(`${pair.model} — ${price}${window}`);
|
|
154
|
-
// A model with no effort list at all supports no thinking level, so a requested level is
|
|
155
|
-
// dropped just the same as one the list omits.
|
|
156
|
-
if (pair.thinking !== undefined &&
|
|
157
|
-
pair.thinking !== "" &&
|
|
158
|
-
!entry.efforts?.includes(pair.thinking)) {
|
|
159
|
-
warns.push(`${pair.model} has no ${pair.thinking} effort — that thinking level is dropped`);
|
|
160
|
-
}
|
|
161
|
-
const provider = opencodeProviderOf(pair.model);
|
|
162
|
-
if (provider !== undefined && !(await opencodeAuthLists(config, provider, pathEnv, timeoutMs))) {
|
|
163
|
-
warns.push(`${pair.model} is a non-gateway provider opencode does not list a credential for`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
warns.push(...opencodeConfigLeaks(xdgConfigHome !== "" ? xdgConfigHome : path.join(home, ".config")));
|
|
167
|
-
const db = statOrNull(opencodeDbPath(root));
|
|
168
|
-
const spillDir = opencodeSpillDir(root);
|
|
169
|
-
const spill = spillDir !== "" && isDirectory(spillDir) ? treeBytes(spillDir) : null;
|
|
170
|
-
const sizeNotes = `db ${formatBytes(db?.size ?? 0)}${spill !== null ? `, spill ${formatBytes(spill)}` : ", no spill dir"}`;
|
|
171
|
-
const found = [...fails, ...warns];
|
|
172
|
-
const base = found.length === 0 ? `opencode ready — ${notes.join("; ")}; ${sizeNotes}` : found.join("; ");
|
|
173
|
-
if (fails.length > 0)
|
|
174
|
-
return failOutcome(base);
|
|
175
|
-
return (warns.length > 0 ? warnOutcome : okOutcome)(base);
|
|
206
|
+
const rows = await collectOpencodeModelRows(readOpencodeCache(root), seamedModels(config, "opencode"), config, pathEnv, timeoutMs);
|
|
207
|
+
const warns = [
|
|
208
|
+
...rows.warns,
|
|
209
|
+
...opencodeConfigLeaks(xdgConfigHome !== "" ? xdgConfigHome : path.join(home, ".config")),
|
|
210
|
+
];
|
|
211
|
+
const sizeNotes = opencodeStorageNote(root);
|
|
212
|
+
return opencodeOutcome(rows.fails, warns, rows.notes, sizeNotes);
|
|
176
213
|
}
|
|
@@ -267,13 +267,10 @@ export function checkFlakyChecks(root, now = Date.now()) {
|
|
|
267
267
|
return warnOutcome(phrases.join("; "));
|
|
268
268
|
}
|
|
269
269
|
/** A trailing day of at least `POST_MERGE_REJECT_WARN` post-merge `review_rejected` events — the
|
|
270
|
-
* audit
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* window, threshold and
|
|
275
|
-
* event selection through `postMergeChurn`, so the doctor row and the alert cannot disagree on
|
|
276
|
-
* when churn fires. ok/warn only — a churn burst does not stop the team. */
|
|
270
|
+
* audit of a day that must not read as a clean main: each rejection is an already-landed commit
|
|
271
|
+
* the reviewer turned back, so it stays on main with the reviewer's reasons. Reads the
|
|
272
|
+
* `POST_MERGE_REJECT_WINDOW_MS` window and
|
|
273
|
+
* threshold through `postMergeChurn`. ok/warn only — a churn burst does not stop the team. */
|
|
277
274
|
export function checkPostMergeChurn(root) {
|
|
278
275
|
const { events } = readEventsSince(root, POST_MERGE_REJECT_WINDOW_MS);
|
|
279
276
|
const churn = postMergeChurn(events);
|
|
@@ -284,10 +281,10 @@ export function checkPostMergeChurn(root) {
|
|
|
284
281
|
.join(", ")}) — each commit had already reached main and stays there with the reviewer's reasons; narrow \`review.afterMerge\` for the noisy role or raise the authoring bar`);
|
|
285
282
|
}
|
|
286
283
|
/** The one home of the doctor's `flaky checks` sentence: names each check that flapped more than
|
|
287
|
-
* once, most frequent first, with how many times it flapped — the
|
|
288
|
-
*
|
|
284
|
+
* once, most frequent first, with how many times it flapped — the one sentence the doctor's
|
|
285
|
+
* `flaky checks` row shows. null when `flapped`
|
|
289
286
|
* is empty (nothing crossed the bar). */
|
|
290
|
-
|
|
287
|
+
function flappedChecksPhrase(flapped) {
|
|
291
288
|
if (flapped.length === 0)
|
|
292
289
|
return null;
|
|
293
290
|
const render = ([k, n]) => `"${k}" ×${n}`;
|
|
@@ -325,7 +322,7 @@ running = null) {
|
|
|
325
322
|
: running?.restartPending
|
|
326
323
|
? "; auto-restart is under way"
|
|
327
324
|
: "; run `npm run build` and restart `tumwater start` (auto-restart does this for a running team)";
|
|
328
|
-
return warnOutcome(`dist/ from ${sha} is stale — main has ${stale.aheadCommits
|
|
325
|
+
return warnOutcome(`dist/ from ${sha} is stale — main has ${plural(stale.aheadCommits, "later commit")} touching ${STALE_INPUTS_LABEL}${next}`);
|
|
329
326
|
}
|
|
330
327
|
return okOutcome(`dist/ from ${sha}, matches main`);
|
|
331
328
|
}
|
|
@@ -344,7 +341,7 @@ function currentHead(root) {
|
|
|
344
341
|
* on every merge and the ci role runs on its own clock, so a post-merge gap is normal and must
|
|
345
342
|
* not turn the Health card or the exit code red. A stale verdict that stays stale past
|
|
346
343
|
* `CI_PARITY_STALE_MS` (ciParityStalled) is a stall — the full suite has silently stopped
|
|
347
|
-
* covering new tips — and warns
|
|
344
|
+
* covering new tips — and warns. Read-only against
|
|
348
345
|
* .tumwater/. */
|
|
349
346
|
export function checkCiParity(root, config, now = Date.now()) {
|
|
350
347
|
const entry = readCiParity(root);
|
|
@@ -360,9 +357,9 @@ export function checkCiParity(root, config, now = Date.now()) {
|
|
|
360
357
|
}
|
|
361
358
|
// A stale verdict is normal right after a merge (main moves and the ci role runs on its own
|
|
362
359
|
// clock), but one that stays stale past CI_PARITY_STALE_MS is a stall: the full suite has
|
|
363
|
-
// silently stopped covering new tips.
|
|
364
|
-
//
|
|
365
|
-
//
|
|
360
|
+
// silently stopped covering new tips. This warn is the one place ciParityStalled is applied —
|
|
361
|
+
// the row is the standing signal. Inside the window the existing friendly stale wording
|
|
362
|
+
// stands.
|
|
366
363
|
if (ciParityStalled({ state: "stale", lastRunAt: entry.lastRunAt }, now)) {
|
|
367
364
|
return warnOutcome(`stale and stalled — the last run covered ${shortSha(entry.tip)} @ lock ${shortSha(entry.lockHash)} ${age} ago (${entry.result}); main is now ${shortSha(tip)} @ lock ${shortSha(lockHash)} — no full-suite run has covered a tip in over ${CI_PARITY_STALE_MS / DAY_MS} days, so the ci role is disabled, paused, or unable to record`);
|
|
368
365
|
}
|
|
@@ -375,8 +372,8 @@ export function checkCiParity(root, config, now = Date.now()) {
|
|
|
375
372
|
* is not a stall); a fresh ledger reads ok naming the newest exercised flow and its age. A
|
|
376
373
|
* ledger whose newest exercise is older than `QA_COVERAGE_STALE_MS` (qaCoverageStalled) is a
|
|
377
374
|
* `warn` naming that flow and its age — the role is disabled, paused, or unable to record —
|
|
378
|
-
*
|
|
379
|
-
*
|
|
375
|
+
* via the one `qaCoverageStalled` predicate, this row the standing signal. Read-only against
|
|
376
|
+
* .tumwater/. */
|
|
380
377
|
export function checkQaCoverage(root, now = Date.now()) {
|
|
381
378
|
const coverage = readQaCoverage(root);
|
|
382
379
|
const entries = Object.entries(coverage);
|
|
@@ -396,3 +393,55 @@ export function checkQaCoverage(root, now = Date.now()) {
|
|
|
396
393
|
}
|
|
397
394
|
return okOutcome(`newest flow coverage: ${newestFlow} exercised ${age} ago`);
|
|
398
395
|
}
|
|
396
|
+
/** How far back the `main diff rules` row looks — one day, the same 24 h trailing-day convention
|
|
397
|
+
* every event-reading doctor row uses, so a diff-rule failure the main watch logged stays a
|
|
398
|
+
* standing warn until a day passes. Owned here (the check that reads it). */
|
|
399
|
+
const MAIN_RULE_FAILURE_WINDOW_MS = 24 * 60 * 60_000;
|
|
400
|
+
/** The exact substring src/baseline/main-watch.ts's `watchRules` logs in its warning, so this
|
|
401
|
+
* check matches a real rule failure and cannot drift from the wording the watch writes. */
|
|
402
|
+
const MAIN_RULE_FAILURE_MARKER = "fails the diff rules";
|
|
403
|
+
/** The doctor's `main diff rules` row: a `warning` the main watch logged when the review gate's
|
|
404
|
+
* diff rules rejected main's tip in the trailing day. The gate judges one change; the watch
|
|
405
|
+
* judges each first-parent commit's own diff, so a conflict marker, a protected edit, a leaked
|
|
406
|
+
* secret, a phantom fix claim or a broken backlog structure that reached main through a gate
|
|
407
|
+
* blind spot sits on main with only this event log to say so. Reads only the `warning` event
|
|
408
|
+
* the watch already writes, so the row cannot drift from the watch's wording; the most recent
|
|
409
|
+
* hit names the tip and the rule. ok/warn only — it never fails a scripted pre-flight. */
|
|
410
|
+
export function checkMainDiffRules(root) {
|
|
411
|
+
const { events } = readEventsSince(root, MAIN_RULE_FAILURE_WINDOW_MS);
|
|
412
|
+
const hits = events.filter((e) => e.type === "warning" && typeof e.message === "string" && e.message.includes(MAIN_RULE_FAILURE_MARKER));
|
|
413
|
+
if (hits.length === 0)
|
|
414
|
+
return okOutcome("no diff-rule failure on main in the last day");
|
|
415
|
+
const latest = hits[hits.length - 1];
|
|
416
|
+
return warnOutcome(`${plural(hits.length, "diff-rule failure on main", "diff-rule failures on main")} in the last day — most recently ${String(latest.message)} — the gate approved a diff the watch's rules reject; repair the tip or revert it with \`tumwater revert <sha>\``);
|
|
417
|
+
}
|
|
418
|
+
/** The exact prefix src/merge/merge-post-review-drain.ts logs when it drops a post-merge
|
|
419
|
+
* review at POST_REVIEW_FAILURE_LIMIT. The drain drops in two ways and logs each with its own
|
|
420
|
+
* tail — "it stays on main unreviewed" after failed runs (the commit never got a review) and
|
|
421
|
+
* "the fix stays unreviewed" after failed fix merges (the fix ref is deleted, main unmoved) —
|
|
422
|
+
* so this check matches the shared "dropping the post-merge review of" prefix both share, not
|
|
423
|
+
* either tail. The drain also files the drop in BUGS.md as best-effort ordinary backlog work
|
|
424
|
+
* (fileDroppedReview), but a skipped filing (off-base checkout or a dirty BUGS.md) leaves only
|
|
425
|
+
* this event log standing, so this check matches the drain's own wording. */
|
|
426
|
+
const DROPPED_REVIEW_MARKER = "dropping the post-merge review of";
|
|
427
|
+
/** How far back the `dropped post-merge reviews` row looks — one day, the same 24 h trailing-day
|
|
428
|
+
* convention every event-reading doctor row uses. Owned here (the check that reads it). */
|
|
429
|
+
const DROPPED_REVIEW_WINDOW_MS = 24 * 60 * 60_000;
|
|
430
|
+
/** The doctor's `dropped post-merge reviews` row: a `warning` the post-merge-review drain
|
|
431
|
+
* (src/merge/merge-post-review-drain.ts) wrote when it dropped a review at
|
|
432
|
+
* POST_REVIEW_FAILURE_LIMIT — after failed runs its commit stays on main unreviewed, and
|
|
433
|
+
* after failed fix merges the fix ref is deleted (main unmoved) so the commit stays
|
|
434
|
+
* unreviewed; either way the review is gone and no longer in the queue the `post-merge
|
|
435
|
+
* reviews` row reads. The drain files the dropped review in BUGS.md as best-effort ordinary
|
|
436
|
+
* backlog work, so when that filing is skipped this row is the standing signal naming the
|
|
437
|
+
* dropped review. Reads only the `warning` event the drain already writes, so it cannot drift
|
|
438
|
+
* from the drain's wording; the most recent hit names the commit. ok/warn only — an
|
|
439
|
+
* unreviewed commit is operator signal, not a broken environment. */
|
|
440
|
+
export function checkDroppedPostMergeReviews(root) {
|
|
441
|
+
const { events } = readEventsSince(root, DROPPED_REVIEW_WINDOW_MS);
|
|
442
|
+
const hits = events.filter((e) => e.type === "warning" && typeof e.message === "string" && e.message.includes(DROPPED_REVIEW_MARKER));
|
|
443
|
+
if (hits.length === 0)
|
|
444
|
+
return okOutcome("no post-merge review was dropped in the last day");
|
|
445
|
+
const latest = hits[hits.length - 1];
|
|
446
|
+
return warnOutcome(`${plural(hits.length, "post-merge review was dropped", "post-merge reviews were dropped")} in the last day — the dropped review is gone and its commit stays unreviewed; most recently ${String(latest.message)}`);
|
|
447
|
+
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { readTextOrNull } from "../files/files.js";
|
|
4
|
-
import { roleNotesPath } from "../paths.js";
|
|
1
|
+
import { readRoleNotesOverCap } from "../status/status-role-notes.js";
|
|
5
2
|
import { ROLE_NOTES_MAX_BYTES } from "../agent/role-notes-write.js";
|
|
6
3
|
import { agree, plural } from "../text/phrases.js";
|
|
7
4
|
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
@@ -14,34 +11,19 @@ const ROLE_NOTES_NAMED_CAP = 3;
|
|
|
14
11
|
* ROLE_NOTES_MAX_BYTES *characters* on read (truncateWithNote's text.length bound), but every
|
|
15
12
|
* operator surface reads the file whole — so a hand edit or old build can leave a note the
|
|
16
13
|
* operator sees and the role never does. This row warns on that gap without touching the file.
|
|
14
|
+
* The over-cap walk is shared with the status snapshot's collectRoleNotesOverCap
|
|
15
|
+
* (src/status/status-role-notes.ts), so the row and the status alert cannot disagree about
|
|
16
|
+
* which notes are over the bound; null means the notes dir is missing or unreadable.
|
|
17
17
|
* Read-only against .tumwater/, like every check. Never throws. */
|
|
18
18
|
export function checkRoleNotes(root) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
entries = fs.readdirSync(notesDir);
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
19
|
+
const over = readRoleNotesOverCap(root);
|
|
20
|
+
if (over === null) {
|
|
25
21
|
// Missing or unreadable notes dir: nothing recorded yet (the write path creates it).
|
|
26
22
|
return okOutcome("no role notes recorded yet");
|
|
27
23
|
}
|
|
28
|
-
// The bound is the read's own unit — truncateWithNote cuts on text.length (UTF-16
|
|
29
|
-
// characters), not the write path's Buffer.byteLength, so compare characters too or a
|
|
30
|
-
// multibyte note the prompt reads in full would false-positive; never statSync().size.
|
|
31
|
-
const over = [];
|
|
32
|
-
for (const name of entries) {
|
|
33
|
-
if (!name.endsWith(".md"))
|
|
34
|
-
continue;
|
|
35
|
-
const text = readTextOrNull(path.join(notesDir, name));
|
|
36
|
-
if (text === null)
|
|
37
|
-
continue;
|
|
38
|
-
if (text.length > ROLE_NOTES_MAX_BYTES)
|
|
39
|
-
over.push({ role: name.slice(0, -3), chars: text.length });
|
|
40
|
-
}
|
|
41
24
|
if (over.length === 0) {
|
|
42
25
|
return okOutcome(`every role note is within the ${ROLE_NOTES_MAX_BYTES}-character read bound`);
|
|
43
26
|
}
|
|
44
|
-
over.sort((a, b) => b.chars - a.chars);
|
|
45
27
|
const first = over
|
|
46
28
|
.slice(0, ROLE_NOTES_NAMED_CAP)
|
|
47
29
|
.map((o) => `${o.role} (${o.chars})`)
|
|
@@ -14,10 +14,10 @@ import { failOutcome } from "./outcome.js";
|
|
|
14
14
|
import { errorMessage } from "../text/text.js";
|
|
15
15
|
import { checkBackendBinary, checkBrief, checkBootstrap, checkClaudeTranscripts, checkExecPath, checkFunes, checkGitBinary, checkConfigKeyNames, checkInit, checkOpencodeVersion, checkNodeVersion, checkPiExtensions, checkRepo, } from "./doctor-checks.js";
|
|
16
16
|
import { checkPrinciples } from "./doctor-principles.js";
|
|
17
|
-
import { checkBuild, checkBuildCheck, checkCiParity, checkDiskSpace, checkEventLog, checkFlakyChecks, checkPostMergeChurn, checkQaCoverage, checkStateDir, } from "./doctor-project-checks.js";
|
|
17
|
+
import { checkBuild, checkBuildCheck, checkCiParity, checkDiskSpace, checkDroppedPostMergeReviews, checkEventLog, checkFlakyChecks, checkMainDiffRules, checkPostMergeChurn, checkQaCoverage, checkStateDir, } from "./doctor-project-checks.js";
|
|
18
18
|
import { sampleFreeBytes } from "../gates/disk-gate.js";
|
|
19
19
|
import { checkCheckContention, checkLeftoverRoleBranches, checkMergeLock, checkProviderConcurrency, checkProviderContention, checkTumwaterPins, checkWorkInstances, checkWorktreePool, } from "./doctor-contention-checks.js";
|
|
20
|
-
import { checkFeedbackFiling,
|
|
20
|
+
import { checkFeedbackFiling, checkTierModels } from "./doctor-model-checks.js";
|
|
21
21
|
import { checkFallbackChain, checkFallbackModel } from "./doctor-fallback-checks.js";
|
|
22
22
|
import { checkAnthroApiKey, checkClaudeAuth, checkClaudeVersion } from "./doctor-claude-checks.js";
|
|
23
23
|
import { checkOpencode } from "./doctor-opencode-checks.js";
|
|
@@ -25,7 +25,7 @@ import { checkCodexAccount, checkCodexAuth, checkCodexHome, checkCodexLeaks, che
|
|
|
25
25
|
import { backendQualifiedSelector } from "../config/config-views.js";
|
|
26
26
|
import { seamsInUse } from "../config/config-fallbacks.js";
|
|
27
27
|
import { checkBacklogHeadings, checkFixClaims, checkLiveFinished, checkPlanDependencies, checkStalledEntries, checkStrandedPlans, checkValidationGaps } from "./doctor-backlog-plans.js";
|
|
28
|
-
import { checkRefusedEntries, checkRegressedFixes, checkStaleBugs, checkStaleQuestions
|
|
28
|
+
import { checkRefusedEntries, checkRegressedFixes, checkStaleBugs, checkStaleQuestions } from "./doctor-backlog-bugs.js";
|
|
29
29
|
import { checkMergeQueueFiles, checkPostReviews, checkStaleQueuedMerges, checkStalePrompts, checkUncommittedBacklog } from "./doctor-backlog.js";
|
|
30
30
|
/** Pre-flight environment check (`tumwater doctor`). The harness's preconditions are
|
|
31
31
|
* scattered across fail-fast checks that each command re-runs on its own (requireReadyRepo in
|
|
@@ -61,7 +61,7 @@ export async function runDoctorCheck(run) {
|
|
|
61
61
|
* just the named checks, in report order; a skipped check is never evaluated, and a named
|
|
62
62
|
* conditional check that returns null leaves no row. A check that throws becomes its own fail
|
|
63
63
|
* row, and every other check still runs. */
|
|
64
|
-
export async function runDoctor(root, pathEnv = envPath(), probe = systemProcessProbe,
|
|
64
|
+
export async function runDoctor(root, pathEnv = envPath(), probe = systemProcessProbe, only, diskSample = sampleFreeBytes) {
|
|
65
65
|
// Loaded once for the checks that read config (repo's baseBranch); a broken file stays
|
|
66
66
|
// undefined — checkInit reports it verbatim — so doctor still runs every other check.
|
|
67
67
|
const { config } = loadConfigSafe(root);
|
|
@@ -107,7 +107,6 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
|
|
|
107
107
|
{ name: "over-cap model", run: () => checkFallbackModel(root, undefined, (orchestratorAlive(root, info) && info?.fallbackDemoted) || null) },
|
|
108
108
|
{ name: "fallback chain", run: () => checkFallbackChain(root) },
|
|
109
109
|
{ name: "models for each tier", run: () => checkTierModels(root) },
|
|
110
|
-
{ name: "image input", run: () => checkImageInput(root, undefined, undefined, undefined, probeImages, pathEnv) },
|
|
111
110
|
{ name: "claude auth", run: () => checkClaudeAuth(root, pathEnv) },
|
|
112
111
|
{ name: "claude api key", run: () => checkAnthroApiKey(root, undefined, pathEnv) },
|
|
113
112
|
{ name: "claude version", run: () => checkClaudeVersion(root, pathEnv) },
|
|
@@ -165,11 +164,12 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
|
|
|
165
164
|
{ name: "plan dependencies", run: () => checkPlanDependencies(root) },
|
|
166
165
|
{ name: "stale questions", run: () => checkStaleQuestions(root) },
|
|
167
166
|
{ name: "stale bugs", run: () => checkStaleBugs(root) },
|
|
168
|
-
{ name: "waiting bugs", run: () => checkWaitingBugs(root) },
|
|
169
167
|
{ name: "regressed fixes", run: () => checkRegressedFixes(root) },
|
|
170
168
|
{ name: "stale merge-queue entries", run: () => checkStaleQueuedMerges(root, Date.now()) },
|
|
171
169
|
{ name: "merge-queue files", run: () => checkMergeQueueFiles(root) },
|
|
172
170
|
{ name: "post-merge reviews", run: () => checkPostReviews(root) },
|
|
171
|
+
{ name: "main diff rules", run: () => checkMainDiffRules(root) },
|
|
172
|
+
{ name: "dropped post-merge reviews", run: () => checkDroppedPostMergeReviews(root) },
|
|
173
173
|
{ name: "stale prompts", run: () => checkStalePrompts(root, Date.now()) },
|
|
174
174
|
{ name: "uncommitted backlog", run: () => checkUncommittedBacklog(root, config) },
|
|
175
175
|
// The running team's own view of its build (staleness and what auto-restart made of it)
|
package/dist/src/doctor/probe.js
CHANGED
|
@@ -20,7 +20,7 @@ export const AUTH_CHECK_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
|
20
20
|
* spawn closes for git). stderr is ignored rather than piped, since nothing reads it and a
|
|
21
21
|
* full stderr pipe would block the child. `timeoutMs` and `maxOutputBytes` are test seams;
|
|
22
22
|
* production callers pass their own named constants. One home of the shape, shared by the
|
|
23
|
-
* model checks' credential/version probes, the tier-model probes
|
|
23
|
+
* model checks' credential/version probes, and the tier-model probes —
|
|
24
24
|
* before the split each doctor module carried its own byte-for-byte copy. The credential probes'
|
|
25
25
|
* named budgets (AUTH_CHECK_TIMEOUT_MS, AUTH_CHECK_MAX_OUTPUT_BYTES) live beside it here, so the
|
|
26
26
|
* backend check modules share them without importing them from one another. */
|
|
@@ -213,11 +213,9 @@ function runEventMessage(e) {
|
|
|
213
213
|
? "resuming the run cut off at the context ceiling (compacted pi session, same workspace)"
|
|
214
214
|
: e.cause === "handoff"
|
|
215
215
|
? "handing the interrupted run off to a fresh session on another agent (same workspace, uncommitted edits kept)"
|
|
216
|
-
: e.cause === "
|
|
217
|
-
? "handing the
|
|
218
|
-
:
|
|
219
|
-
? "handing the work an earlier run left unsaved to a fresh session"
|
|
220
|
-
: "resuming the run a shutdown interrupted (same agent session and workspace)") + restored);
|
|
216
|
+
: e.cause === "salvaged"
|
|
217
|
+
? "handing the work an earlier run left unsaved to a fresh session"
|
|
218
|
+
: "resuming the run a shutdown interrupted (same agent session and workspace)") + restored);
|
|
221
219
|
}
|
|
222
220
|
case "wake":
|
|
223
221
|
return `woke (${textOr(e.reason)})`;
|
|
@@ -238,7 +236,7 @@ function runEventMessage(e) {
|
|
|
238
236
|
// Priority headroom (part 7/7): a reserve appearing or vanishing is the scheduler working
|
|
239
237
|
// as designed; it names the permits withheld for the work tier so an operator can see why
|
|
240
238
|
// maintenance concurrency dipped.
|
|
241
|
-
return `${finiteNumber(e.reserve, 0)
|
|
239
|
+
return `${plural(finiteNumber(e.reserve, 0), "permit")} reserved for work roles that have work to take`;
|
|
242
240
|
default: return textOr(e.type);
|
|
243
241
|
}
|
|
244
242
|
}
|
|
@@ -616,13 +614,6 @@ function imageRestartEventMessage(e) {
|
|
|
616
614
|
: "";
|
|
617
615
|
return `blind image read: ${textOr(e.path)} on ${textOr(e.model, "the model")}${action}`;
|
|
618
616
|
}
|
|
619
|
-
case "image_reroute":
|
|
620
|
-
// The run's work moves to a model that can read the image (plans/image-input.md part
|
|
621
|
-
// 4/5). Routine-with-explanation — no warning prefix: names the hop the way the
|
|
622
|
-
// handoff reads, and whether the traits-of-attachment preflight avoided a failed run.
|
|
623
|
-
return e.preflight === true
|
|
624
|
-
? `image attachment routed to ${textOr(e.to, "the target")} (${textOr(e.tier, "a peer tier")}) to read its image — no failed run`
|
|
625
|
-
: `image rerouted to ${textOr(e.to, "the target")} (${textOr(e.tier, "a peer tier")}) — next run goes there after a blind read on ${textOr(e.from, "the prior model")}`;
|
|
626
617
|
case "build_stale":
|
|
627
618
|
// Self-hosting teams only (src/redeploy/redeploy.ts): the code main describes is not the
|
|
628
619
|
// code running. Not a warning prefix — a stale build is a state, and auto-restart resolves
|
|
@@ -632,7 +623,7 @@ function imageRestartEventMessage(e) {
|
|
|
632
623
|
return `restart pending — main ${shortSha(e.head)} is green; finishing current runs (no new runs start)`;
|
|
633
624
|
case "restart": {
|
|
634
625
|
const aborted = finiteNumber(e.abortedRuns, 0);
|
|
635
|
-
return `restarting onto update ${shortSha(e.to)} (finished current work in ${minutes(e.finishedMs)}m${aborted > 0 ? `, ${aborted
|
|
626
|
+
return `restarting onto update ${shortSha(e.to)} (finished current work in ${minutes(e.finishedMs)}m${aborted > 0 ? `, ${plural(aborted, "run")} resume after the restart` : ""})`;
|
|
636
627
|
}
|
|
637
628
|
case "restart_refused":
|
|
638
629
|
// The restart is refused, not failed: the running build stays and the gate is re-asked
|
|
@@ -798,7 +789,6 @@ export function eventMessage(e) {
|
|
|
798
789
|
case "orchestrator_start":
|
|
799
790
|
case "orchestrator_stop": return configEventMessage(e);
|
|
800
791
|
case "image_blind":
|
|
801
|
-
case "image_reroute":
|
|
802
792
|
case "build_stale":
|
|
803
793
|
case "restart_pending":
|
|
804
794
|
case "restart":
|
|
@@ -84,7 +84,6 @@ export const EVENT_TYPE_LABELS = {
|
|
|
84
84
|
model_fallback_ended: "fallback model ended",
|
|
85
85
|
opencode_free_model_billed: "free model billed",
|
|
86
86
|
image_blind: "image not seen",
|
|
87
|
-
image_reroute: "image sent to another model",
|
|
88
87
|
build_stale: "update available",
|
|
89
88
|
restart_pending: "update waits for idle",
|
|
90
89
|
restart: "restarted onto update",
|
|
@@ -34,7 +34,7 @@ const LEGACY_EVENT_TYPES = {
|
|
|
34
34
|
/** The ordered field rewrites; later parts of the series append here, and they run in order. */
|
|
35
35
|
const LEGACY_EVENT_REWRITES = [
|
|
36
36
|
{
|
|
37
|
-
// The run number rode `tick` on run_start, run_end, prompt_taken, image_blind
|
|
37
|
+
// The run number rode `tick` on run_start, run_end, prompt_taken, image_blind.
|
|
38
38
|
name: "tick-to-run", // retired-name-ok: legacy name
|
|
39
39
|
apply(ev) {
|
|
40
40
|
const e = ev;
|
|
@@ -349,7 +349,7 @@ export function eventsSince(root, sinceMs, now, keep = () => true) {
|
|
|
349
349
|
return readEventsSince(root, sinceMs).events.filter((ev) => keep(ev) && ev.ts >= now - sinceMs);
|
|
350
350
|
}
|
|
351
351
|
/** The `--since` / `?sinceMs=` trailing window's bounds and identity slice, shared by the two
|
|
352
|
-
* trailing-window collectors (`collectReportSince` in report-data.ts and
|
|
352
|
+
* trailing-window collectors (`collectReportSince` in report-since-data.ts and
|
|
353
353
|
* `collectFailureReportSince` in failure-data.ts): an open-ended window (cutoff = now − sinceMs,
|
|
354
354
|
* end = now) when `untilMs` is absent, else the bounded [now − sinceMs, now − untilMs). Both
|
|
355
355
|
* collectors budget events with the same open/closed comparisons against `cutoff`/`end` (and
|
|
@@ -58,8 +58,21 @@ export function logEvent(root, event) {
|
|
|
58
58
|
ensureParentDir(file);
|
|
59
59
|
rotateIfLarge(file, EVENTS_MAX_BYTES, EVENTS_KEEP);
|
|
60
60
|
appendEventLine(file, JSON.stringify(full) + "\n");
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// The event is committed to the log before dispatch, so a subscriber's own failure must not
|
|
62
|
+
// make the write look like it failed to the caller, starve the other subscribers of this or
|
|
63
|
+
// any later event, or crash an unwrapped writer (the orchestrator's poll-loop warn site)
|
|
64
|
+
// for one listener's bug. Each listener is isolated: its throw is surfaced loudly on stderr
|
|
65
|
+
// (a terminal boundary, so the text is sanitized) and the dispatch moves on. The set is
|
|
66
|
+
// snapshotted so a listener that unsubscribes itself mid-dispatch cannot skip or double-run
|
|
67
|
+
// a sibling.
|
|
68
|
+
for (const listener of [...listeners]) {
|
|
69
|
+
try {
|
|
70
|
+
listener(full);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
process.stderr.write(`tumwater: event subscriber failed: ${stripTerminalControls(errorMessage(err))}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
63
76
|
return full;
|
|
64
77
|
}
|
|
65
78
|
/** Log a warning event: the harness's "something is off but the loop continues" signal. The
|