tumwater 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/agent/backend-traits.js +13 -0
- package/dist/src/agent/claude/claude-backend.js +2 -2
- package/dist/src/agent/claude/claude-stream.js +3 -2
- package/dist/src/agent/codex/codex-backend.js +3 -3
- package/dist/src/agent/codex/codex-stream.js +3 -1
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/backlog/backlog-archive.js +60 -25
- package/dist/src/backlog/backlog-cli-view.js +3 -28
- package/dist/src/backlog/backlog-cli.js +9 -64
- package/dist/src/backlog/backlog-commit.js +17 -9
- package/dist/src/backlog/backlog-eligibility.js +44 -26
- package/dist/src/backlog/backlog-parts.js +23 -13
- package/dist/src/backlog/backlog-release.js +33 -16
- package/dist/src/baseline/main-guardrails.js +0 -73
- package/dist/src/baseline/main-red.js +31 -21
- package/dist/src/baseline/main-watch.js +82 -34
- package/dist/src/brief/brief-edit.js +3 -1
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +2 -1
- package/dist/src/build/dep-install.js +64 -1
- package/dist/src/cli/cli-args.js +63 -0
- package/dist/src/cli/cli-flag-specs.js +33 -0
- package/dist/src/cli/cli-query-commands.js +16 -41
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +118 -13
- package/dist/src/cli/cli-start-summary.js +10 -3
- package/dist/src/cli/cli-start.js +33 -5
- package/dist/src/cli/completion.js +21 -1
- package/dist/src/cli/config-commands.js +17 -8
- package/dist/src/cli/help.js +74 -14
- package/dist/src/cli/log-commands.js +26 -13
- package/dist/src/cli/question-commands.js +7 -1
- package/dist/src/cli.js +71 -6
- package/dist/src/collections.js +31 -4
- package/dist/src/config/config-editable-keys.js +3 -0
- package/dist/src/config/config-fallbacks.js +3 -2
- package/dist/src/config/config-validation.js +47 -42
- package/dist/src/config/config-views.js +10 -2
- package/dist/src/config/config-write.js +33 -1
- package/dist/src/config/config.js +7 -2
- package/dist/src/doctor/doctor-backlog-bugs.js +85 -42
- package/dist/src/doctor/doctor-backlog-plans.js +214 -79
- package/dist/src/doctor/doctor-backlog.js +155 -22
- package/dist/src/doctor/doctor-check-names.js +5 -1
- package/dist/src/doctor/doctor-checks.js +10 -15
- package/dist/src/doctor/doctor-claude-checks.js +6 -9
- package/dist/src/doctor/doctor-codex-checks.js +6 -9
- package/dist/src/doctor/doctor-contention-checks.js +244 -118
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +4 -6
- package/dist/src/doctor/doctor-model-checks.js +45 -200
- package/dist/src/doctor/doctor-notify.js +7 -2
- package/dist/src/doctor/doctor-opencode-checks.js +7 -5
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +106 -12
- package/dist/src/doctor/doctor.js +10 -5
- package/dist/src/doctor/probe.js +14 -1
- package/dist/src/events/event-format.js +7 -6
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-read.js +13 -0
- package/dist/src/events/event-window.js +20 -2
- package/dist/src/failure/failure-data.js +9 -4
- package/dist/src/failure/failure-render.js +8 -8
- package/dist/src/failure/time-spend.js +0 -64
- package/dist/src/files/file-queue.js +17 -0
- package/dist/src/gates/budget-gates.js +27 -257
- package/dist/src/gates/gate-polls.js +49 -141
- package/dist/src/gates/maintenance-quota.js +27 -17
- package/dist/src/git/commit-message.js +12 -0
- package/dist/src/git/git.js +18 -0
- package/dist/src/git/slot-salvage.js +3 -3
- package/dist/src/gui/gui-backlog-commands.js +3 -3
- package/dist/src/gui/gui-backlog-endpoints.js +5 -4
- package/dist/src/gui/gui-command.js +19 -1
- package/dist/src/gui/gui-config-commands.js +75 -9
- package/dist/src/gui/gui-endpoint-commands.js +2 -2
- package/dist/src/gui/gui-operator-commands.js +134 -7
- package/dist/src/gui/gui-prompt-commands.js +15 -6
- package/dist/src/gui/gui-report-endpoints.js +10 -3
- package/dist/src/gui/gui-server.js +16 -2
- package/dist/src/history/history-queued.js +8 -0
- package/dist/src/history/history.js +2 -5
- package/dist/src/inbox/inbox-top.js +4 -9
- package/dist/src/merge/merge-core.js +2 -25
- package/dist/src/merge/merge-drain.js +22 -135
- package/dist/src/merge/merge-pipeline.js +1 -1
- package/dist/src/merge/merge-post-review.js +154 -30
- package/dist/src/merge/merge-queue-render.js +11 -12
- package/dist/src/merge/merge-queue.js +22 -1
- package/dist/src/merge/merge-requeue.js +1 -0
- package/dist/src/merge/merge-stack.js +1 -7
- package/dist/src/merge/merge-to-main.js +1 -9
- package/dist/src/merge/merge-top.js +33 -19
- package/dist/src/merge/merge-verify.js +64 -9
- package/dist/src/notes-cli.js +8 -3
- package/dist/src/orchestrator/orchestrator-poll.js +47 -15
- package/dist/src/orchestrator/orchestrator-scheduling.js +4 -2
- package/dist/src/orchestrator/orchestrator.js +5 -4
- package/dist/src/orchestrator/scheduling-scope.js +51 -7
- package/dist/src/orchestrator/scheduling-skip.js +15 -5
- package/dist/src/paths.js +16 -6
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/process/run-marker.js +8 -0
- package/dist/src/prompt/gate-notes.js +1 -1
- package/dist/src/prompt/gate-prompts.js +30 -7
- package/dist/src/prompt/notes-backlog.js +13 -10
- package/dist/src/prompt/prompt.js +3 -1
- package/dist/src/report/report-data.js +49 -69
- package/dist/src/report/report-fold.js +11 -12
- package/dist/src/report/report-render.js +6 -4
- package/dist/src/report/report.js +2 -5
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +12 -0
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +157 -24
- package/dist/src/review/protected-edits.js +22 -7
- package/dist/src/review/quarantine-render.js +65 -0
- package/dist/src/review/review.js +33 -49
- package/dist/src/roles/role-catalog.js +60 -2
- package/dist/src/roles/role-guidance.js +10 -0
- package/dist/src/roles/role-labels.js +6 -3
- package/dist/src/roles/role-render.js +43 -8
- package/dist/src/roles/role-view.js +36 -9
- package/dist/src/roles/roles.js +3 -3
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/qa-coverage.js +50 -15
- package/dist/src/run/run-detail-data.js +9 -13
- package/dist/src/run/run-detail-render.js +1 -8
- package/dist/src/run/run-finalize.js +1 -0
- package/dist/src/run/run-prompt.js +14 -0
- package/dist/src/run/run-stage.js +66 -37
- package/dist/src/run/run-verdict.js +14 -1
- package/dist/src/run/stage-check.js +17 -11
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +16 -515
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +50 -19
- package/dist/src/runner/revision.js +12 -23
- package/dist/src/runner/runner-outcome.js +4 -2
- package/dist/src/runner/runner-recovery.js +18 -3
- package/dist/src/runner/runner-run.js +21 -8
- package/dist/src/runner/runner-state.js +4 -3
- package/dist/src/scheduling/claim-entries.js +49 -0
- package/dist/src/status/status-data.js +6 -1
- package/dist/src/status/status-polls.js +15 -0
- package/dist/src/status/status-roles.js +11 -0
- package/dist/src/team/reclaim.js +2 -2
- package/dist/src/text/datetime.js +3 -2
- package/dist/src/text/phrases.js +6 -3
- package/dist/src/text/text.js +11 -0
- package/dist/src/ui/badges.js +34 -1
- package/dist/src/ui/gui/gui-client-backlog.js +25 -3
- package/dist/src/ui/gui/gui-client-boot.js +63 -41
- package/dist/src/ui/gui/gui-client-drawer-config.js +56 -14
- package/dist/src/ui/gui/gui-client-drawer.js +29 -4
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +74 -72
- package/dist/src/ui/gui/gui-client-labels.js +5 -1
- package/dist/src/ui/gui/gui-client-operator.js +6 -2
- package/dist/src/ui/gui/gui-client-pending.js +188 -11
- package/dist/src/ui/gui/gui-client-report-chart.js +36 -8
- package/dist/src/ui/gui/gui-client-report.js +20 -5
- package/dist/src/ui/gui/gui-client-settings.js +109 -3
- package/dist/src/ui/gui/gui-client-team.js +11 -0
- package/dist/src/ui/gui/gui-client.js +11 -1
- package/dist/src/ui/gui/gui-page.js +1 -0
- package/dist/src/ui/gui/gui-styles.js +4 -0
- package/dist/src/ui/run-progress-model.js +2 -1
- package/dist/src/ui/status-payload.js +237 -9
- package/dist/src/ui/status-render.js +12 -4
- package/dist/src/ui/team-alerts.js +512 -37
- package/dist/src/ui/tone.js +7 -0
- package/dist/src/ui/tui/tui-backlog.js +7 -5
- package/dist/src/ui/tui/tui-editor-attach.js +1 -7
- package/dist/src/ui/tui/tui-editor-backends.js +1 -6
- package/dist/src/ui/tui/tui-editor-brief.js +4 -24
- package/dist/src/ui/tui/tui-editor-budget.js +1 -8
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +1 -0
- package/dist/src/ui/tui/tui-editor-config.js +19 -6
- package/dist/src/ui/tui/tui-editor-entry.js +3 -17
- package/dist/src/ui/tui/tui-editor-instances.js +9 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +7 -26
- package/dist/src/ui/tui/tui-editor-interval.js +18 -7
- package/dist/src/ui/tui/tui-editor-notes.js +6 -24
- package/dist/src/ui/tui/tui-editor-quiet.js +2 -12
- package/dist/src/ui/tui/tui-editor-retire.js +1 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +1 -6
- package/dist/src/ui/tui/tui-editor-session.js +41 -0
- package/dist/src/ui/tui/tui-editor-slots.js +1 -0
- package/dist/src/ui/tui/tui-editors.js +10 -1
- package/dist/src/ui/tui/tui-frame.js +18 -6
- package/dist/src/ui/tui/tui-keys.js +9 -0
- package/dist/src/ui/tui/tui-pane-activity.js +21 -8
- package/dist/src/ui/tui/tui-pane-backlog.js +36 -9
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +113 -3
- package/dist/src/ui/tui/tui-pane-history.js +44 -0
- package/dist/src/ui/tui/tui-pane-pending.js +285 -56
- package/dist/src/ui/tui/tui-pane-queued.js +7 -2
- package/dist/src/ui/tui/tui-pane-report.js +9 -3
- package/dist/src/ui/tui/tui-pane-session.js +9 -0
- package/dist/src/ui/tui/tui-pane.js +16 -10
- package/dist/src/ui/tui/tui-panes.js +56 -7
- package/dist/src/ui/tui/tui.js +8 -5
- package/dist/src/verdict/fix-claim.js +47 -1
- package/dist/src/verdict/reply-contract.js +35 -0
- package/package.json +1 -1
- package/dist/src/baseline/main-culprit.js +0 -94
- package/dist/src/baseline/main-revert.js +0 -133
|
@@ -43,10 +43,16 @@ export function createTuiPanes(deps) {
|
|
|
43
43
|
session.notesClearArmed = null;
|
|
44
44
|
session.armedKillOrphans = session.killOrphansArmed;
|
|
45
45
|
session.killOrphansArmed = false;
|
|
46
|
+
session.armedDropBrokenPins = session.dropBrokenPinsArmed;
|
|
47
|
+
session.dropBrokenPinsArmed = false;
|
|
48
|
+
session.armedRetireLeftovers = session.retireLeftoversArmed;
|
|
49
|
+
session.retireLeftoversArmed = false;
|
|
46
50
|
session.armedRevert = session.revertArmed;
|
|
47
51
|
session.revertArmed = null;
|
|
48
52
|
session.armedQueue = session.queueArmed;
|
|
49
53
|
session.queueArmed = null;
|
|
54
|
+
session.armedReview = session.reviewArmed;
|
|
55
|
+
session.reviewArmed = null;
|
|
50
56
|
};
|
|
51
57
|
/** The per-key pane snapshot the editors' open arms read. */
|
|
52
58
|
const editorCtx = () => {
|
|
@@ -159,6 +165,16 @@ export function createTuiPanes(deps) {
|
|
|
159
165
|
return health.handleKey?.(key, ctx) ?? false;
|
|
160
166
|
if (key.ctrl && key.name === "w")
|
|
161
167
|
return history.handleKey?.(key, ctx) ?? false;
|
|
168
|
+
// Health owns Ctrl+S (drop broken pins) ahead of the pane router's fallthrough; off the
|
|
169
|
+
// Health view it falls through so the quiet-hours (Activity), trailing-window (History)
|
|
170
|
+
// and min-run-interval (Transcript) arms keep their current meanings.
|
|
171
|
+
if (key.ctrl && key.name === "s" && session.view === session.roleIds.length + 7) {
|
|
172
|
+
// On the Health view the drop-pins arm consumes Ctrl+S only on an empty prompt line; a
|
|
173
|
+
// non-empty prompt or a held editor falls through so History's trailing-window cycle still
|
|
174
|
+
// takes the key, exactly as it did before the drop-pins arm existed.
|
|
175
|
+
if (health.handleKey?.(key, ctx))
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
162
178
|
if (key.ctrl && key.name === "s")
|
|
163
179
|
return history.handleKey?.(key, ctx) ?? false;
|
|
164
180
|
// Ctrl+P on Usage/Failures steps the report window back; every other view is History's.
|
|
@@ -170,6 +186,29 @@ export function createTuiPanes(deps) {
|
|
|
170
186
|
return health.handleKey?.(key, ctx) ?? false;
|
|
171
187
|
if (key.ctrl && key.name === "p")
|
|
172
188
|
return history.handleKey?.(key, ctx) ?? false;
|
|
189
|
+
// Health owns Ctrl+A (commit-backlog) ahead of the pane router's fallthrough; the Backlog
|
|
190
|
+
// view's Ctrl+A (the answer editor) is handled by tui-editor-answer.ts, which the key
|
|
191
|
+
// dispatcher consults before this router, so this case never shadows it.
|
|
192
|
+
if (key.ctrl && key.name === "a" && session.view === session.roleIds.length + 7)
|
|
193
|
+
return health.handleKey?.(key, ctx) ?? false;
|
|
194
|
+
// Pending owns Ctrl+A on its own view (the two-step post-merge-review accept), routed after
|
|
195
|
+
// Health's so the Health commit-backlog arm keeps its view's slot.
|
|
196
|
+
if (key.ctrl && key.name === "a" && session.view === session.roleIds.length + 5)
|
|
197
|
+
return pending.handleKey?.(key, ctx) ?? false;
|
|
198
|
+
// Health owns Ctrl+U (re-sync install) ahead of the pane router's fallthrough; off the
|
|
199
|
+
// Health view it falls through so the prompt-line editor keeps Ctrl+U (kill-to-start).
|
|
200
|
+
if (key.ctrl && key.name === "u" && session.view === session.roleIds.length + 7)
|
|
201
|
+
return health.handleKey?.(key, ctx) ?? false;
|
|
202
|
+
// Health owns Ctrl+L (retire leftovers) ahead of the pane router's fallthrough; off the
|
|
203
|
+
// Health view it falls through so the Activity fallback shift (view 0) and the
|
|
204
|
+
// instructions editor on a role's Transcript keep their current meanings.
|
|
205
|
+
if (key.ctrl && key.name === "l" && session.view === session.roleIds.length + 7)
|
|
206
|
+
return health.handleKey?.(key, ctx) ?? false;
|
|
207
|
+
// History owns Ctrl+L (newest failure) after Health's; off the History view it falls
|
|
208
|
+
// through so the Activity fallback editor (view 0) and a Transcript's instructions editor
|
|
209
|
+
// (handled in the editors ahead of this router) keep their current meanings.
|
|
210
|
+
if (key.ctrl && key.name === "l")
|
|
211
|
+
return history.handleKey?.(key, ctx) ?? false;
|
|
173
212
|
if (key.ctrl && key.name === "y") {
|
|
174
213
|
// Ctrl+Y's transcript change-step arm (the diffstat toggle) is routed first and falls
|
|
175
214
|
// through off it, so History keeps its Summary cycle on every other view.
|
|
@@ -179,12 +218,18 @@ export function createTuiPanes(deps) {
|
|
|
179
218
|
}
|
|
180
219
|
if (key.ctrl && key.name === "o") {
|
|
181
220
|
// The Activity type cycle is routed first on its own view (0); on the Health view the
|
|
182
|
-
// problems-only toggle precedes the History outcome cycle
|
|
183
|
-
//
|
|
221
|
+
// problems-only toggle precedes the History outcome cycle; on the Pending view the
|
|
222
|
+
// post-merge review state filter is routed ahead of History's outcome cycle (its own arm
|
|
223
|
+
// consumes the key on Pending); on the Backlog view the Ready-only filter is routed
|
|
224
|
+
// ahead of History's outcome cycle, and every other view keeps the History outcome cycle.
|
|
184
225
|
if (session.view === 0)
|
|
185
226
|
return activity.handleKey?.(key, ctx) ?? false;
|
|
186
227
|
if (session.view === session.roleIds.length + 7)
|
|
187
228
|
return health.handleKey?.(key, ctx) ?? false;
|
|
229
|
+
if (session.view === session.roleIds.length + 5)
|
|
230
|
+
return pending.handleKey?.(key, ctx) ?? false;
|
|
231
|
+
if (session.view === session.roleIds.length + 1)
|
|
232
|
+
return backlog.handleKey?.(key, ctx) ?? false;
|
|
188
233
|
return history.handleKey?.(key, ctx) ?? false;
|
|
189
234
|
}
|
|
190
235
|
if (key.ctrl && key.name === "x") {
|
|
@@ -214,15 +259,17 @@ export function createTuiPanes(deps) {
|
|
|
214
259
|
return backlog.handleKey?.(key, ctx) ?? false;
|
|
215
260
|
}
|
|
216
261
|
if (key.ctrl && key.name === "v") {
|
|
217
|
-
// The Ctrl+V arms, routed like Ctrl+X's: the History trail's revert,
|
|
218
|
-
// prompt's
|
|
219
|
-
// view/selection does not
|
|
220
|
-
// Activity attach editor's own Ctrl+V arm is guarded to view 0, so it
|
|
221
|
-
// there ahead of this router.
|
|
262
|
+
// The Ctrl+V arms, routed like Ctrl+X's: the History trail's revert, the queued
|
|
263
|
+
// prompt's move-to-top, then the Pending pane's merge move-to-top, and the backlog
|
|
264
|
+
// entry's move-to-top last — each returns false when its view/selection does not
|
|
265
|
+
// apply. The Activity attach editor's own Ctrl+V arm is guarded to view 0, so it
|
|
266
|
+
// still fires there ahead of this router.
|
|
222
267
|
if (history.handleKey?.(key, ctx))
|
|
223
268
|
return true;
|
|
224
269
|
if (queued.handleKey?.(key, ctx))
|
|
225
270
|
return true;
|
|
271
|
+
if (pending.handleKey?.(key, ctx))
|
|
272
|
+
return true;
|
|
226
273
|
return backlog.handleKey?.(key, ctx) ?? false;
|
|
227
274
|
}
|
|
228
275
|
const arrow = arrowDir(key.name);
|
|
@@ -310,6 +357,7 @@ export function createTuiPanes(deps) {
|
|
|
310
357
|
activityRole: act.activityRole,
|
|
311
358
|
activityGrep: act.activityGrep,
|
|
312
359
|
activityType: act.activityType,
|
|
360
|
+
activityNotable: act.activityNotable,
|
|
313
361
|
noteMode: tra.noteMode,
|
|
314
362
|
noteCache: tra.noteCache,
|
|
315
363
|
changeMode: tra.changeMode,
|
|
@@ -319,6 +367,7 @@ export function createTuiPanes(deps) {
|
|
|
319
367
|
selectedEntry: back.selectedEntry,
|
|
320
368
|
entryScroll: back.entryScroll,
|
|
321
369
|
backlogGrep: back.backlogGrep,
|
|
370
|
+
backlogReadyOnly: back.backlogReadyOnly,
|
|
322
371
|
scopeRole: rep.scopeRole,
|
|
323
372
|
reportSince: rep.reportSince,
|
|
324
373
|
reportStep: rep.reportStep,
|
package/dist/src/ui/tui/tui.js
CHANGED
|
@@ -2,10 +2,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { render as inkRender } from "ink";
|
|
3
3
|
import { TuiApp } from "./tui-app.js";
|
|
4
4
|
import { openQuestions } from "../../backlog/backlog.js";
|
|
5
|
-
import { operatorHeldEntries, refusedEntries,
|
|
6
|
-
import { planDependencyAlert, skippedMergeFiles, staleMerges, staleOpenBugs, stalePrompts, staleWaitingBugs } from "../status-payload.js";
|
|
5
|
+
import { operatorHeldEntries, refusedEntries, stalledEntries } from "../../backlog/backlog-eligibility.js";
|
|
6
|
+
import { capacityContention, droppedPostReviewsAlert, flappedChecksAlert, mainRuleFailures, notifyFailures, planDependencyAlert, postMergeChurnAlert, postReviewRiskAlert, primaryOverwriteBlocks, qaCoverageAlert, quarantineAlert, regressedFixesAlert, skippedMergeFiles, staleMerges, staleOpenBugs, staleOpenQuestions, stalePrompts, staleWaitingBugs, staleWorkspacePins } from "../status-payload.js";
|
|
7
|
+
import { strandedPlanEntriesFor } from "../../backlog/backlog-structure.js";
|
|
8
|
+
import { duplicatedBacklogHeadings, fixClaimProblems, liveFinishedEntriesFor, validationGapProblems } from "../../doctor/doctor-backlog-plans.js";
|
|
7
9
|
import { snapshot } from "../../status/status-data.js";
|
|
8
10
|
import { renderStatusSpans } from "../status-render.js";
|
|
11
|
+
import { staleClaimEntries } from "../../scheduling/claim-entries.js";
|
|
9
12
|
import { teamAlerts } from "../team-alerts.js";
|
|
10
13
|
import { captureStartupBuild, createReloadWatch, reexecSelf, watchReloadSupervisor, } from "../../redeploy/self-reload.js";
|
|
11
14
|
import { renderInputView, tuiTerminalError, } from "./tui-input.js";
|
|
@@ -90,7 +93,7 @@ export async function runTui(root, seams = {}) {
|
|
|
90
93
|
const view = s.view;
|
|
91
94
|
// What needs the operator — the dashboard's alert banners (team-alerts.ts's teamAlerts), as
|
|
92
95
|
// attention lines under the header. Each consumes one line of the height budget.
|
|
93
|
-
const attention = alertLines(teamAlerts(snap, openQuestions(root), status.roles, Date.now(), refusedEntries(root), operatorHeldEntries(root), staleOpenBugs(root), stalePrompts(root), staleMerges(root), planDependencyAlert(root), staleWaitingBugs(root), skippedMergeFiles(root),
|
|
96
|
+
const attention = alertLines(teamAlerts(snap, openQuestions(root), status.roles, Date.now(), refusedEntries(root), operatorHeldEntries(root), staleOpenBugs(root), stalePrompts(root), staleMerges(root), planDependencyAlert(root), staleWaitingBugs(root), skippedMergeFiles(root), stalledEntries(root), quarantineAlert(root), strandedPlanEntriesFor(root), duplicatedBacklogHeadings(root), validationGapProblems(root), notifyFailures(root), primaryOverwriteBlocks(root), capacityContention(root), flappedChecksAlert(root), regressedFixesAlert(root, Date.now()), postMergeChurnAlert(root), postReviewRiskAlert(root), droppedPostReviewsAlert(root), mainRuleFailures(root, Date.now()), staleClaimEntries(root, status.roles.map((r) => r.role), Date.now()), staleOpenQuestions(root), fixClaimProblems(root), qaCoverageAlert(root, Date.now()), staleWorkspacePins(root, Date.now()), liveFinishedEntriesFor(root)), width);
|
|
94
97
|
const statusLines = [status.lines[0] ?? [], ...attention, ...status.lines.slice(1)];
|
|
95
98
|
// Numbered previews of prompts queued for the director, between the table and the
|
|
96
99
|
// activity pane: what will run next, in execution order. Each line consumes exactly
|
|
@@ -112,7 +115,7 @@ export async function runTui(root, seams = {}) {
|
|
|
112
115
|
: view === roleIds.length + 6 ? { kind: "queued", grep: s.queuedGrep }
|
|
113
116
|
: view === roleIds.length + 7 ? { kind: "health", problems: s.healthProblems }
|
|
114
117
|
: view === roleIds.length + 8 ? { kind: "brief" }
|
|
115
|
-
: { kind: "activity", role: s.activityRole, grep: s.activityGrep, type: s.activityType };
|
|
118
|
+
: { kind: "activity", role: s.activityRole, grep: s.activityGrep, type: s.activityType, notable: s.activityNotable };
|
|
116
119
|
// The pane's body lines (tui-pane.ts, extracted from this closure): transcripts, the
|
|
117
120
|
// backlog views, usage/failures, or activity, composed at this frame's budgets.
|
|
118
121
|
const paneResult = paneBody(pane, { root, width, eventBudget, entryBudget, state: s });
|
|
@@ -121,7 +124,7 @@ export async function runTui(root, seams = {}) {
|
|
|
121
124
|
// The frame as the ink tree draws it (tui-app.tsx): the same composed lines the
|
|
122
125
|
// hand-painted string frame joined, as clipped StatusLines — one column row per line,
|
|
123
126
|
// one styled Text per span, colors through tui-app.tsx's toneColor.
|
|
124
|
-
const mode = { budget: s.budgetMode, concurrencyMode: s.concurrencyMode, fallbackMode: s.fallbackMode, modelMode: s.modelMode, rolePromptFor: s.rolePromptFor, attachMode: s.attachMode, pauseEditMode: s.pauseEditMode, pauseEditRole: s.pauseEditRole, pauseEditWake: s.pauseEditWake, pauseEditStop: s.pauseEditStop, queuedEditFor: s.queuedEditFor, answerFor: s.answerFor, entryEditFor: s.entryEditFor, notesEditFor: s.notesEditFor, briefEditMode: s.briefEditMode, retireMode: s.retireMode, quietMode: s.quietMode, quietTeam: s.quietTeam, providerCapsMode: s.providerCapsMode, claudeReserveMode: s.claudeReserveMode, intervalMode: s.intervalMode, roleModelMode: s.roleModelMode, instructionsEditFor: s.instructionsEditFor, slotsMode: s.slotsMode, images: s.images, notifyMode: s.notifyMode, backendsMode: s.backendsMode, instancesMode: s.instancesMode };
|
|
127
|
+
const mode = { budget: s.budgetMode, concurrencyMode: s.concurrencyMode, checksMode: s.checksMode, checkMode: s.checkMode, fallbackMode: s.fallbackMode, modelMode: s.modelMode, rolePromptFor: s.rolePromptFor, attachMode: s.attachMode, pauseEditMode: s.pauseEditMode, pauseEditRole: s.pauseEditRole, pauseEditWake: s.pauseEditWake, pauseEditStop: s.pauseEditStop, queuedEditFor: s.queuedEditFor, answerFor: s.answerFor, entryEditFor: s.entryEditFor, notesEditFor: s.notesEditFor, briefEditMode: s.briefEditMode, retireMode: s.retireMode, roleEnabledMode: s.roleEnabledMode, quietMode: s.quietMode, quietTeam: s.quietTeam, providerCapsMode: s.providerCapsMode, claudeReserveMode: s.claudeReserveMode, intervalMode: s.intervalMode, roleModelMode: s.roleModelMode, instructionsEditFor: s.instructionsEditFor, slotsMode: s.slotsMode, images: s.images, notifyMode: s.notifyMode, backendsMode: s.backendsMode, instancesMode: s.instancesMode };
|
|
125
128
|
const lines = [...statusLines, []]; // a blank row below the header
|
|
126
129
|
for (const [i, preview] of queued.entries())
|
|
127
130
|
lines.push(toneLine(`${i + 1}. ${preview}`, width));
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* bare file name is backed by a file of that name anywhere in the walked source dirs. */
|
|
46
46
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
47
47
|
import { extname, join } from "node:path";
|
|
48
|
-
import { readTextOrNull, walkFiles } from "../files/files.js";
|
|
48
|
+
import { readTextOrNull, statOrNull, walkFiles } from "../files/files.js";
|
|
49
49
|
import { fenceTracker, finishedArchiveOf, parseEntryDetails, sectionLines, trailingParenthetical, } from "../backlog/backlog-md.js";
|
|
50
50
|
import { changeBaseRev, fileContentAt } from "../git/git.js";
|
|
51
51
|
import { collapseWhitespace, truncate } from "../text/text.js";
|
|
@@ -217,6 +217,52 @@ export function sourceHaystack(root) {
|
|
|
217
217
|
}
|
|
218
218
|
return parts.join("\n");
|
|
219
219
|
}
|
|
220
|
+
/** The module-level cache backing cachedSourceHaystack, keyed by root. Cleared wholesale past a
|
|
221
|
+
* small cap, like the other stat caches (src/files/stat-cache.ts) — evicting only costs one
|
|
222
|
+
* re-walk and re-read per tree on the next call. */
|
|
223
|
+
const sourceHaystackCache = new Map();
|
|
224
|
+
const HAYSTACK_CACHE_MAX = 64;
|
|
225
|
+
/** sourceHaystack with a stat-keyed cache so the always-on observer poll reads the tree only
|
|
226
|
+
* when a file changed: the first call per root walks the tree once and, on every later call,
|
|
227
|
+
* builds a signature from each file's path + mtimeMs + size (all stat-only, sorted so the walk
|
|
228
|
+
* order cannot matter) and returns the cached text unchanged when the signature matches, or
|
|
229
|
+
* re-reads on a change. Transparent — its return equals sourceHaystack(root) for the same tree —
|
|
230
|
+
* so the merge gate keeps calling sourceHaystack and never notices the cache. */
|
|
231
|
+
export function cachedSourceHaystack(root) {
|
|
232
|
+
const signature = sourceHaystackSignature(root);
|
|
233
|
+
const cached = sourceHaystackCache.get(root);
|
|
234
|
+
if (cached?.signature === signature)
|
|
235
|
+
return cached.text;
|
|
236
|
+
const text = sourceHaystack(root);
|
|
237
|
+
if (sourceHaystackCache.size >= HAYSTACK_CACHE_MAX)
|
|
238
|
+
sourceHaystackCache.clear();
|
|
239
|
+
sourceHaystackCache.set(root, { signature, text });
|
|
240
|
+
return text;
|
|
241
|
+
}
|
|
242
|
+
/** The stat-only signature a cachedSourceHaystack call compares: every source file the haystack
|
|
243
|
+
* reads (docs excluded, exactly as sourceHaystack does) plus package.json/tsconfig.json, each as
|
|
244
|
+
* `path:mtimeMs:size`. Sorted so the walk order cannot matter; an unreadable or missing file
|
|
245
|
+
* contributes nothing, the same way sourceHaystack skips it — except a file that is stat-able
|
|
246
|
+
* but unreadable, which contributes its stat but read nothing: the cache then keys on a byte-for-
|
|
247
|
+
* byte consistent shape, never stale text. */
|
|
248
|
+
function sourceHaystackSignature(root) {
|
|
249
|
+
const sigs = [];
|
|
250
|
+
const docExts = new Set([".md", ".markdown"]);
|
|
251
|
+
const add = (p) => {
|
|
252
|
+
const st = statOrNull(p);
|
|
253
|
+
if (st?.isFile())
|
|
254
|
+
sigs.push(`${p}:${st.mtimeMs}:${st.size}`);
|
|
255
|
+
};
|
|
256
|
+
walkSourceFiles(root, (p, entry) => {
|
|
257
|
+
if (docExts.has(extname(entry.name).toLowerCase()))
|
|
258
|
+
return;
|
|
259
|
+
add(p);
|
|
260
|
+
});
|
|
261
|
+
for (const file of ["package.json", "tsconfig.json"])
|
|
262
|
+
add(join(root, file));
|
|
263
|
+
sigs.sort();
|
|
264
|
+
return sigs.join("|");
|
|
265
|
+
}
|
|
220
266
|
/** Every file under the source dirs the haystack reads (src/, test/, scripts/), node_modules and
|
|
221
267
|
* dist trees skipped at any depth. */
|
|
222
268
|
function walkSourceFiles(root, visit) {
|
|
@@ -185,6 +185,41 @@ export function extractFlow(text) {
|
|
|
185
185
|
return null;
|
|
186
186
|
return { flow: match[1].trim(), result: match[2].toLowerCase() };
|
|
187
187
|
}
|
|
188
|
+
/** Extract the `ci` run's result-carrying `CI: <sha> — <passed|failed>` line (plans, part
|
|
189
|
+
* 3/3); null when absent. Built on `labeledLine`, so the label is anchored at line start and
|
|
190
|
+
* a mid-sentence mention is ignored. The verdict is required and the sha must be a hex id: a
|
|
191
|
+
* bare `CI: <sha>` — or a reply truncated mid-verdict (`CI: abc123 — pa`) — is not a result,
|
|
192
|
+
* and returning null leaves the parity ledger unadvanced rather than latching a coverage the
|
|
193
|
+
* run never declared (the same lesson as qa's FLOW line, BUGS.md 2026-09-23). Splits on the
|
|
194
|
+
* dash/em-dash token before the verdict. */
|
|
195
|
+
export function extractCiResult(text) {
|
|
196
|
+
const value = labeledLine(text, "CI");
|
|
197
|
+
if (!value)
|
|
198
|
+
return null;
|
|
199
|
+
const match = /^([0-9a-f]{7,64})\s*[—-]\s*(passed|failed)\s*$/i.exec(value);
|
|
200
|
+
if (!match?.[1])
|
|
201
|
+
return null;
|
|
202
|
+
return { sha: match[1].toLowerCase(), result: match[2].toLowerCase() };
|
|
203
|
+
}
|
|
204
|
+
/** The paths a post-merge review's rejecting reply pairs with its verdict to declare a repair
|
|
205
|
+
* (Forward never back 1a/6): `FIX: <comma-separated paths>` naming the files the reviewer edited
|
|
206
|
+
* in the review worktree. Unlike `labeledLine` — whose `\s` can span the newline, so
|
|
207
|
+
* a bare `FIX:` with no content of its own would borrow the next line and read as paths — this
|
|
208
|
+
* regex anchors its content to ONE line: the label is anchored at line start (a mid-sentence
|
|
209
|
+
* mention is ignored), the content is captured on the same line, and each comma-separated path is
|
|
210
|
+
* whitespace-trimmed. The paths are required — a bare `FIX:` (or a reply truncated so the line
|
|
211
|
+
* carries none) records nothing and the review stays a plain reject, the same fail-closed lesson
|
|
212
|
+
* as qa's FLOW line (BUGS.md 2026-09-23). null when no usable one-line list exists. */
|
|
213
|
+
export function fixLine(text) {
|
|
214
|
+
const match = /^[ \t]*FIX:[ \t]*(.+?)[ \t]*$/m.exec(text);
|
|
215
|
+
if (!match)
|
|
216
|
+
return null;
|
|
217
|
+
const paths = match[1]
|
|
218
|
+
.split(",")
|
|
219
|
+
.map((p) => p.trim())
|
|
220
|
+
.filter((p) => p.length > 0);
|
|
221
|
+
return paths.length > 0 ? paths : null;
|
|
222
|
+
}
|
|
188
223
|
/** The shortest a filed title may be to count as a declared result. A truncated `FILED:` line
|
|
189
224
|
* cuts the title off mid-word (`FILED: #12 — tit` where the run meant `title`); requiring a
|
|
190
225
|
* title of at least this many characters keeps a torn line from being latched as a filing the
|
package/package.json
CHANGED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { cachedBaselineVerdict, checkMainBaseline } from "./main-baseline.js";
|
|
2
|
-
import { baselineCheckLogger } from "./main-red.js";
|
|
3
|
-
import { CHECK_TIER } from "../concurrency/check-permit.js";
|
|
4
|
-
import { gitTry } from "../git/git-run.js";
|
|
5
|
-
import { treeDiffFiles } from "../git/git-diff.js";
|
|
6
|
-
import { ensureDetachedWorktree } from "../git/worktree.js";
|
|
7
|
-
import { gateMainWorktreePath } from "../paths.js";
|
|
8
|
-
import { isExemptDiff } from "../review/exemptions.js";
|
|
9
|
-
import { onlyQuarantined } from "../review/known-flakes.js";
|
|
10
|
-
import { readEvents } from "../events/event-read.js";
|
|
11
|
-
import { TUMWATER_SOURCE } from "../events/events.js";
|
|
12
|
-
import { shortSha } from "../text/format.js";
|
|
13
|
-
/** How many first-parent commits below the red tip the search for a last green looks at. */
|
|
14
|
-
const CULPRIT_SEARCH_MAX = 50;
|
|
15
|
-
/** How many recent events the owner lookup reads; an older `merged` event counts as none. */
|
|
16
|
-
const OWNER_EVENT_TAIL = 2000;
|
|
17
|
-
/** Name the first-parent commit that turned main red at `red.sha` (plans/fast-merge.md, Lever 2,
|
|
18
|
-
* part 4a). Runs inside the main watch's `_gate-main` checkout `wt`, re-pointing it per candidate
|
|
19
|
-
* and back at the tip before returning. Finds the newest first-parent commit with a cached green
|
|
20
|
-
* verdict, re-checks it fresh (a green that is red now means the environment broke, not a
|
|
21
|
-
* change), then bisects the commits since it — skipping doc-only ones — for the first red. A
|
|
22
|
-
* search that cannot name a commit says why instead of guessing. */
|
|
23
|
-
export async function findRedCulprit(root, wt, config, red) {
|
|
24
|
-
const tip = red.sha;
|
|
25
|
-
const onRun = baselineCheckLogger(root, TUMWATER_SOURCE);
|
|
26
|
-
const mirror = gateMainWorktreePath(root);
|
|
27
|
-
const at = async (sha) => {
|
|
28
|
-
await ensureDetachedWorktree(root, mirror, sha);
|
|
29
|
-
};
|
|
30
|
-
const check = async (sha, reverifyRed, forceFresh, observedOnly) => {
|
|
31
|
-
await at(sha);
|
|
32
|
-
return (await checkMainBaseline(wt, config, onRun, reverifyRed, forceFresh, undefined, observedOnly, CHECK_TIER.merge))
|
|
33
|
-
.baseline;
|
|
34
|
-
};
|
|
35
|
-
try {
|
|
36
|
-
const walk = ((await gitTry(wt, "rev-list", "--first-parent", `--max-count=${CULPRIT_SEARCH_MAX + 1}`, tip)) ?? "")
|
|
37
|
-
.split("\n")
|
|
38
|
-
.filter((s) => s !== "")
|
|
39
|
-
.slice(1);
|
|
40
|
-
const green = walk.find((sha) => cachedBaselineVerdict(sha) === "green");
|
|
41
|
-
if (!green) {
|
|
42
|
-
return { kind: "unattributed", reason: `no green commit within ${CULPRIT_SEARCH_MAX} first-parent commits` };
|
|
43
|
-
}
|
|
44
|
-
const recheck = await check(green, false, true, false);
|
|
45
|
-
if (!recheck)
|
|
46
|
-
return { kind: "unattributed", reason: `the check made no verdict at ${shortSha(green)}` };
|
|
47
|
-
if (recheck.status === "red")
|
|
48
|
-
return { kind: "environment", green };
|
|
49
|
-
const exempt = config.review.exemptPaths;
|
|
50
|
-
const since = ((await gitTry(wt, "rev-list", "--first-parent", "--reverse", `${green}..${tip}`)) ?? "")
|
|
51
|
-
.split("\n")
|
|
52
|
-
.filter((s) => s !== "");
|
|
53
|
-
const candidates = [];
|
|
54
|
-
for (const sha of since) {
|
|
55
|
-
const files = await treeDiffFiles(wt, `${sha}^`, sha);
|
|
56
|
-
if (files === null || !isExemptDiff(files, exempt))
|
|
57
|
-
candidates.push(sha);
|
|
58
|
-
}
|
|
59
|
-
if (candidates.length === 0)
|
|
60
|
-
return { kind: "unattributed", reason: "no non-doc commit since the last green" };
|
|
61
|
-
// The last candidate is taken as red unrun: it is the tip, or only doc commits follow it.
|
|
62
|
-
let lo = 0;
|
|
63
|
-
let hi = candidates.length - 1;
|
|
64
|
-
while (lo < hi) {
|
|
65
|
-
const mid = (lo + hi) >> 1;
|
|
66
|
-
const verdict = await check(candidates[mid], false, false, true);
|
|
67
|
-
if (!verdict)
|
|
68
|
-
return { kind: "unattributed", reason: `the check made no verdict at ${shortSha(candidates[mid])}` };
|
|
69
|
-
if (verdict.status === "red" && !onlyQuarantined(root, verdict.failingTests))
|
|
70
|
-
hi = mid;
|
|
71
|
-
else
|
|
72
|
-
lo = mid + 1;
|
|
73
|
-
}
|
|
74
|
-
const commit = candidates[lo];
|
|
75
|
-
const confirmed = await check(commit, true, false, true);
|
|
76
|
-
if (confirmed?.status !== "red" || onlyQuarantined(root, confirmed.failingTests)) {
|
|
77
|
-
return { kind: "unattributed", reason: `red did not reproduce at ${shortSha(commit)}` };
|
|
78
|
-
}
|
|
79
|
-
const failingTests = red.failingTests ?? []; // G was just verified green, so it carries none to subtract
|
|
80
|
-
const merged = readEvents(root, OWNER_EVENT_TAIL)
|
|
81
|
-
.reverse()
|
|
82
|
-
.find((ev) => ev.type === "merged" && ev.commit === commit);
|
|
83
|
-
return {
|
|
84
|
-
kind: "culprit",
|
|
85
|
-
commit,
|
|
86
|
-
...(merged ? { runner: merged.source } : {}),
|
|
87
|
-
failingTests,
|
|
88
|
-
green,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
finally {
|
|
92
|
-
await at(tip);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { queueCulpritHeal } from "./main-red.js";
|
|
2
|
-
import { gateCommandOf } from "../build/build-check-detect.js";
|
|
3
|
-
import { runScopedBuildCheck } from "../build/build-check-scoped.js";
|
|
4
|
-
import { withLock } from "../concurrency/lock.js";
|
|
5
|
-
import { logEventBestEffort, TUMWATER_SOURCE, warnEventBestEffort } from "../events/events.js";
|
|
6
|
-
import { branchHead, patchId } from "../git/git.js";
|
|
7
|
-
import { gitTry } from "../git/git-run.js";
|
|
8
|
-
import { stampedSubject } from "../git/commit-message.js";
|
|
9
|
-
import { ensureDetachedWorktree } from "../git/worktree.js";
|
|
10
|
-
import { useWorktree } from "../git/worktree-use.js";
|
|
11
|
-
import { ffMainToOrWhy } from "../merge/merge-git.js";
|
|
12
|
-
import { revertCommitIn } from "../operator/revert.js";
|
|
13
|
-
import { mergeLockDir, mergeWorktreePath } from "../paths.js";
|
|
14
|
-
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
15
|
-
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
16
|
-
import { errorMessage } from "../text/text.js";
|
|
17
|
-
import { shortSha } from "../text/format.js";
|
|
18
|
-
const REVERT_PREFIXES = ["tumwater(operator): Revert", "tumwater(tumwater): Revert"]; // vocabulary-ok: commit prefixes are machine strings
|
|
19
|
-
/** Why the culprit is not reverted automatically, or null when it should be: no runner (an
|
|
20
|
-
* operator or hand commit), a director's merge, a revert itself, or a commit whose revert
|
|
21
|
-
* already merged. */
|
|
22
|
-
export async function revertSkipReason(root, mainBranch, c) {
|
|
23
|
-
if (c.runner === undefined)
|
|
24
|
-
return "no role runner merged it";
|
|
25
|
-
if (baseRoleOf(c.runner) === DIRECTOR_ROLE)
|
|
26
|
-
return "the director merged it";
|
|
27
|
-
const subject = ((await gitTry(root, "log", "-1", "--format=%s", c.commit)) ?? "").trim();
|
|
28
|
-
if (REVERT_PREFIXES.some((p) => subject.startsWith(p)))
|
|
29
|
-
return "it is itself a revert";
|
|
30
|
-
const merged = (await gitTry(root, "log", mainBranch, "--format=%s", "--fixed-strings", `--grep=Revert ${shortSha(c.commit)}`)) ?? "";
|
|
31
|
-
if (merged.split("\n").some((s) => REVERT_PREFIXES.some((p) => s === `${p} ${shortSha(c.commit)}`)))
|
|
32
|
-
return "a revert of it already merged";
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
/** The main watch's culprit hand-off: starts the automatic revert, or falls back to bugfix's heal
|
|
36
|
-
* prompt when the revert is skipped. Never rejects, because its caller discards the promise and an
|
|
37
|
-
* unhandled rejection would end the orchestrator: a heal prompt that cannot be queued is warned. */
|
|
38
|
-
export async function routeMainCulprit(root, mainBranch, c, startRevert) {
|
|
39
|
-
const skip = await revertSkipReason(root, mainBranch, c);
|
|
40
|
-
if (skip === null) {
|
|
41
|
-
startRevert(c);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
try {
|
|
45
|
-
queueCulpritHeal(root, c.tip, { culprit: c.commit, failingTests: c.failingTests });
|
|
46
|
-
}
|
|
47
|
-
catch (err) {
|
|
48
|
-
warnEventBestEffort(root, TUMWATER_SOURCE, `not reverting ${shortSha(c.commit)} (${skip}) and could not queue the heal prompt for bugfix: ${errorMessage(err)}`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
/** Run one revert task on main's current tip in the shared `_merge` checkout: revert the culprit,
|
|
52
|
-
* run `check.gateCommand` on the result when one is set (red aborts), and fast-forward main under
|
|
53
|
-
* the merge lock. Logs `main_revert` on success and calls `onReverted` (the hand-back to a role);
|
|
54
|
-
* every failure queues the heal prompt instead. Never throws: a hand-off that cannot be queued is
|
|
55
|
-
* warned, not thrown. */
|
|
56
|
-
export async function runMainRevert(root, mainBranch, config, c, onReverted, onFailed) {
|
|
57
|
-
/** Hand the failure to a role and return the warning's suffix saying truthfully what happened. */
|
|
58
|
-
const heal = (reason, outputTail) => {
|
|
59
|
-
const toReviewer = c.postReview && onFailed;
|
|
60
|
-
try {
|
|
61
|
-
if (toReviewer)
|
|
62
|
-
onFailed(c, reason);
|
|
63
|
-
else
|
|
64
|
-
queueCulpritHeal(root, c.tip, { culprit: c.commit, failingTests: c.failingTests, outputTail });
|
|
65
|
-
return toReviewer ? "sent the reviewer's reasons to its role" : "queued the heal prompt for bugfix";
|
|
66
|
-
}
|
|
67
|
-
catch (err) {
|
|
68
|
-
return toReviewer
|
|
69
|
-
? `could not send the reviewer's reasons to its role: ${errorMessage(err)}`
|
|
70
|
-
: `could not queue the heal prompt for bugfix: ${errorMessage(err)}`;
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
try {
|
|
74
|
-
const failure = await useWorktree(root, mergeWorktreePath(root), async () => {
|
|
75
|
-
const tip = await branchHead(root, mainBranch);
|
|
76
|
-
if (tip === null)
|
|
77
|
-
return { reason: `${mainBranch} is unreadable` };
|
|
78
|
-
const wt = await ensureDetachedWorktree(root, mergeWorktreePath(root), tip);
|
|
79
|
-
const subject = ((await gitTry(wt, "log", "-1", "--format=%s", c.commit)) ?? "").trim();
|
|
80
|
-
const tests = c.failingTests.length > 0 ? `\n\nFailing tests: ${c.failingTests.join("; ")}` : "";
|
|
81
|
-
const why = c.postReview
|
|
82
|
-
? `which a post-merge review rejected.\n\nReasons: ${c.postReview.reasons.join("; ")}`
|
|
83
|
-
: `which turned main red.${tests}`;
|
|
84
|
-
const message = `${stampedSubject("tumwater", `Revert ${shortSha(c.commit)}`)}\n\nReverts ${c.commit} (${subject}), ${why}`;
|
|
85
|
-
const reverted = await revertCommitIn(wt, c.commit, message);
|
|
86
|
-
if (!reverted.ok)
|
|
87
|
-
return { reason: reverted.error };
|
|
88
|
-
if (gateCommandOf(config) !== undefined) {
|
|
89
|
-
const gate = await runScopedBuildCheck(root, TUMWATER_SOURCE, "gate", wt, config);
|
|
90
|
-
if (gate && gate.outcome.status !== "passed")
|
|
91
|
-
return { reason: `the gate command was ${gate.outcome.status} on the revert`, tail: gate.outcome.outputTail };
|
|
92
|
-
}
|
|
93
|
-
const ffFailure = await withLock(mergeLockDir(root), () => ffMainToOrWhy(root, reverted.sha, mainBranch));
|
|
94
|
-
if (ffFailure !== null)
|
|
95
|
-
return { reason: ffFailure };
|
|
96
|
-
// The culprit's patch-id (`C^...C`, src/git/git.ts patchId, -U0 so a re-landing onto a
|
|
97
|
-
// moved main keeps the id): the fast-merge guardrail G2 matches a later `merged`
|
|
98
|
-
// event against it to spot a change that came back unchanged (plans/fast-merge.md).
|
|
99
|
-
const culpritPatch = await patchId(wt, `${c.commit}^`, c.commit).catch(() => null);
|
|
100
|
-
logEventBestEffort(root, {
|
|
101
|
-
source: TUMWATER_SOURCE,
|
|
102
|
-
type: "main_revert",
|
|
103
|
-
commit: c.commit,
|
|
104
|
-
revert: reverted.sha,
|
|
105
|
-
runner: c.runner,
|
|
106
|
-
failingTests: c.failingTests,
|
|
107
|
-
...(culpritPatch ? { patchId: culpritPatch } : {}),
|
|
108
|
-
});
|
|
109
|
-
await onReverted?.(c).catch((err) => logEventBestEffort(root, {
|
|
110
|
-
source: TUMWATER_SOURCE,
|
|
111
|
-
type: "warning",
|
|
112
|
-
message: `could not hand ${shortSha(c.commit)} back to its role: ${errorMessage(err)}`,
|
|
113
|
-
}));
|
|
114
|
-
return null;
|
|
115
|
-
});
|
|
116
|
-
if (failure !== null) {
|
|
117
|
-
const suffix = heal(failure.reason, failure.tail);
|
|
118
|
-
logEventBestEffort(root, {
|
|
119
|
-
source: TUMWATER_SOURCE,
|
|
120
|
-
type: "warning",
|
|
121
|
-
message: `could not revert ${shortSha(c.commit)}: ${failure.reason} — ${suffix}`,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
const suffix = heal(errorMessage(err));
|
|
127
|
-
logEventBestEffort(root, {
|
|
128
|
-
source: TUMWATER_SOURCE,
|
|
129
|
-
type: "warning",
|
|
130
|
-
message: `revert of ${shortSha(c.commit)} failed: ${errorMessage(err)} — ${suffix}`,
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|