tumwater 0.4.5 → 0.4.6
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/backlog/backlog-md.js +32 -17
- package/dist/src/baseline/main-red.js +72 -8
- package/dist/src/baseline/main-watch.js +5 -4
- package/dist/src/cli/cli-args.js +14 -1
- package/dist/src/cli/cli-flag-specs.js +5 -3
- package/dist/src/cli/cli-log-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +21 -9
- package/dist/src/cli/help.js +5 -2
- package/dist/src/cli.js +1 -1
- package/dist/src/config/config-write.js +76 -16
- package/dist/src/doctor/doctor-claude-checks.js +4 -11
- package/dist/src/doctor/doctor-codex-checks.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +9 -1
- package/dist/src/doctor/doctor-project-checks.js +6 -6
- package/dist/src/events/event-format.js +50 -38
- package/dist/src/events/event-labels.js +1 -0
- package/dist/src/failure/failure-render.js +1 -0
- package/dist/src/failure/failure-time-spend.js +1 -0
- package/dist/src/git/slot-salvage.js +6 -0
- package/dist/src/gui/gui-history-endpoints.js +4 -4
- package/dist/src/history/history.js +2 -2
- package/dist/src/{run → history}/run-detail-data.js +2 -2
- package/dist/src/{run → history}/run-detail.js +2 -2
- package/dist/src/inbox/inbox-top.js +2 -1
- package/dist/src/merge/merge-batch.js +9 -0
- package/dist/src/merge/merge-core.js +13 -0
- package/dist/src/merge/merge-drain.js +13 -2
- package/dist/src/merge/merge-pipeline.js +5 -4
- package/dist/src/merge/merge-queue.js +8 -1
- package/dist/src/merge/merge-requeue.js +16 -18
- package/dist/src/merge/merge-to-main.js +4 -0
- package/dist/src/merge/merge-verify.js +13 -2
- package/dist/src/merge/merge-withdrawn.js +71 -0
- package/dist/src/operator/operator-commands.js +2 -2
- package/dist/src/operator/operator-revert.js +1 -1
- package/dist/src/orchestrator/orchestrator.js +63 -15
- package/dist/src/paths.js +19 -3
- package/dist/src/prompt/gate-prompts.js +6 -1
- package/dist/src/prompt/prompt.js +11 -0
- package/dist/src/redeploy/redeployer.js +41 -30
- package/dist/src/report/report.js +2 -2
- package/dist/src/review/known-flakes.js +101 -40
- package/dist/src/review/post-merge-review.js +40 -33
- package/dist/src/review/review-verdict.js +25 -17
- package/dist/src/roles/role-catalog.js +10 -4
- package/dist/src/roles/role-view.js +33 -12
- package/dist/src/run/run-apply.js +3 -2
- package/dist/src/run/run-dismiss.js +2 -2
- package/dist/src/run/run-prompt.js +4 -4
- package/dist/src/run/run-stage.js +7 -1
- package/dist/src/run/run-verdict.js +1 -1
- package/dist/src/runner/runner-leftover.js +48 -1
- package/dist/src/runner/runner-outcome.js +1 -1
- package/dist/src/runner/runner-run.js +11 -2
- package/dist/src/runner/runner-state-load.js +29 -13
- package/dist/src/scheduling/claims.js +98 -19
- package/dist/src/scheduling/scope.js +16 -2
- package/dist/src/{run → stage}/stage-path-checks.js +3 -3
- package/dist/src/status/status-polls.js +1 -1
- package/dist/src/text/phrases.js +2 -2
- package/dist/src/text/text.js +1 -1
- package/dist/src/ui/gui/gui-client-model.js +1 -0
- package/dist/src/ui/gui/gui-client-settings.js +20 -10
- package/dist/src/ui/tone.js +2 -1
- package/dist/src/ui/tui/tui-editor-attach.js +2 -6
- package/dist/src/ui/tui/tui-editor-brief.js +2 -6
- package/dist/src/ui/tui/tui-editor-release.js +2 -6
- package/dist/src/ui/tui/tui-editor-retire.js +2 -6
- package/dist/src/ui/tui/tui-editor-role-enabled.js +2 -6
- package/dist/src/ui/tui/tui-editor-session.js +14 -0
- package/dist/src/ui/tui/tui-guard.js +16 -0
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +4 -14
- package/dist/src/ui/tui/tui-pane-history.js +2 -8
- package/dist/src/ui/tui/tui-pane-pending.js +8 -22
- package/dist/src/ui/tui/tui-pane-trail.js +4 -9
- package/dist/src/ui/tui/tui-pane-transcript.js +2 -7
- package/dist/src/verdict/reply-contract.js +12 -1
- package/package.json +1 -1
- /package/dist/src/{run → history}/run-detail-render.js +0 -0
- /package/dist/src/{run → observer}/ci-parity.js +0 -0
- /package/dist/src/{run → observer}/feedback-filings.js +0 -0
- /package/dist/src/{run → observer}/qa-coverage.js +0 -0
- /package/dist/src/{run → observer}/selfcheck-digest.js +0 -0
- /package/dist/src/{run → stage}/acceptance-greps.js +0 -0
- /package/dist/src/{run → stage}/stage-backlog-checks.js +0 -0
- /package/dist/src/{run → stage}/stage-check.js +0 -0
- /package/dist/src/{run → stage}/stage-checks-shared.js +0 -0
- /package/dist/src/{run → stage}/stage-name-checks.js +0 -0
- /package/dist/src/{run → stage}/stage-prose-checks.js +0 -0
|
@@ -31,8 +31,8 @@ import { KNOWN_FLAKE_WINDOW_MS, loadQuarantine, repeatedFlakeCounts } from "../r
|
|
|
31
31
|
import { POST_MERGE_REJECT_WINDOW_MS, postMergeChurn } from "../review/post-merge-review.js";
|
|
32
32
|
import { roleLabel } from "../roles/role-labels.js";
|
|
33
33
|
import { quarantineWarning } from "../baseline/main-guardrails.js";
|
|
34
|
-
import { readCiParity, currentCiParityState, parityIsCurrent, ciParityStalled, CI_PARITY_STALE_MS } from "../
|
|
35
|
-
import { readQaCoverage, qaCoverageStalled, QA_COVERAGE_STALE_MS } from "../
|
|
34
|
+
import { readCiParity, currentCiParityState, parityIsCurrent, ciParityStalled, CI_PARITY_STALE_MS } from "../observer/ci-parity.js";
|
|
35
|
+
import { readQaCoverage, qaCoverageStalled, QA_COVERAGE_STALE_MS } from "../observer/qa-coverage.js";
|
|
36
36
|
import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
|
|
37
37
|
/** .tumwater writable — absent is fine (created on first run); present, prove it by writing
|
|
38
38
|
* and deleting a temp file inside. The probe leaves the dir's listing unchanged after the run.
|
|
@@ -352,9 +352,9 @@ function tipLockPhrase(tip, lockHash) {
|
|
|
352
352
|
return `${shortSha(tip)} @ lock ${shortSha(lockHash)}`;
|
|
353
353
|
}
|
|
354
354
|
/** The doctor's `ci parity` row: does the full suite cover main's current tip? Reads the
|
|
355
|
-
* CI-parity ledger (src/
|
|
356
|
-
* CI
|
|
357
|
-
* reads "no CI parity recorded yet" (ok); a failed record is a `warn` naming the
|
|
355
|
+
* CI-parity ledger (src/observer/ci-parity.ts) — the operator surface answering whether main
|
|
356
|
+
* is verified by CI — and reports whether the last run covered the current main tip @ lock.
|
|
357
|
+
* Missing ledger reads "no CI parity recorded yet" (ok); a failed record is a `warn` naming the
|
|
358
358
|
* tip/lock (the ci role's contract is to fix or file what it finds, so a lingering failed record
|
|
359
359
|
* is actionable until the next green run); the `current`/`stale` split is `parityIsCurrent`'s
|
|
360
360
|
* one home, so the row and the ci prompt block cannot drift. Stale is normally `ok` — main moves
|
|
@@ -387,7 +387,7 @@ export function checkCiParity(root, config, now = Date.now()) {
|
|
|
387
387
|
}
|
|
388
388
|
/** The doctor's `qa coverage` row: has the `qa` observer — the team's only flow-level
|
|
389
389
|
* observer — stopped exercising any of the product's own flows? Reads the flow-coverage ledger
|
|
390
|
-
* (src/
|
|
390
|
+
* (src/observer/qa-coverage.ts), the operator surface answering "is qa still recording what it
|
|
391
391
|
* exercises?". A missing ledger reads "no qa flow coverage recorded yet" (ok — no record yet
|
|
392
392
|
* is not a stall); a fresh ledger reads ok naming the newest exercised flow and its age. A
|
|
393
393
|
* ledger whose newest exercise is older than `QA_COVERAGE_STALE_MS` (qaCoverageStalled) is a
|
|
@@ -258,19 +258,52 @@ function runEventMessage(e) {
|
|
|
258
258
|
default: return textOr(e.type);
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
|
+
function mergeQueuedMessage(e) {
|
|
262
|
+
// Merge queue 3/5: routine state change (the run committed and the merge slot is
|
|
263
|
+
// ahead) — no warning prefix. The merge's own events follow it. A retry-bearing event
|
|
264
|
+
// (a re-queued entry) appends its why-it-is-back suffix; a
|
|
265
|
+
// fresh enqueue's line stays byte-identical.
|
|
266
|
+
const base = `waiting to merge ${shortSha(e.commit)} — ${textOr(e.summary)}`;
|
|
267
|
+
return `${base}${typeof e.retry === "string" && e.retry ? ` (re-queued after ${textOr(e.retry)})` : ""}`;
|
|
268
|
+
}
|
|
269
|
+
function mergeFailedMessage(e) {
|
|
270
|
+
// The event names its reason itself (writeMergeOutcome); the review/check events still
|
|
271
|
+
// carry the full text. An old event without `reason` renders the bare outcome.
|
|
272
|
+
return `merge of ${shortSha(e.commit)} failed (${eventTypeLabel(outcomeText(e))}${isNonBlankString(e.reason) ? `: ${truncate(e.reason.trim(), 120)}` : ""})${elapsed(e.durationMs)}${isNonBlankString(e.summary) ? ` — ${truncate(e.summary, 80)}` : ""}`;
|
|
273
|
+
}
|
|
274
|
+
function conflictHandbackMessage(e) {
|
|
275
|
+
// A change whose merges kept conflicting was handed back to its author with the markers
|
|
276
|
+
// in place (PLANS.md "Robust conflict landing, part 2/2"): queued, applied, or failed.
|
|
277
|
+
const files = stringList(e.conflicted);
|
|
278
|
+
const where = files.length > 0 ? `: ${files.join(", ")}` : "";
|
|
279
|
+
const why = e.reason === "revision" ? " (revision)" : "";
|
|
280
|
+
return `conflict hand-back ${textOr(e.action)} ${shortSha(e.sha)}${why}${where}`;
|
|
281
|
+
}
|
|
282
|
+
function operatorRevertMessage(e) {
|
|
283
|
+
const undid = typeof e.runner === "string" && e.runner !== "" ? `, undid ${e.runner}` : "";
|
|
284
|
+
return `reverted ${shortSha(e.commit)} (by operator) as ${shortSha(e.revert)}${undid}`; // vocabulary-ok: the deliberate-revert writer is the operator role
|
|
285
|
+
}
|
|
286
|
+
function mainCheckMessage(e) {
|
|
287
|
+
const failing = Array.isArray(e.failingTests) ? e.failingTests.map(String) : [];
|
|
288
|
+
// The commit count renders only when it is a real number over 1 — the corrupt-field rule
|
|
289
|
+
// this file applies to every event number (a hand-edited line's quoted "3" or "0x10" reads
|
|
290
|
+
// as no covered count, not as a coerced parenthetical).
|
|
291
|
+
const coveredN = finiteNumber(e.covered, null);
|
|
292
|
+
const covered = coveredN !== null && coveredN > 1 ? ` (${coveredN} commits)` : "";
|
|
293
|
+
const verdict = e.status === "failed" ? "failed" : "passed";
|
|
294
|
+
const names = failing.length > 0 ? `: ${joinWithMore(failing, 3, "; ")}` : "";
|
|
295
|
+
return `main ${shortSha(e.sha)}${covered} full check ${verdict}${names}`;
|
|
296
|
+
}
|
|
297
|
+
function mainCulpritMessage(e) {
|
|
298
|
+
const by = typeof e.runner === "string" && e.runner !== "" ? ` (merged by ${e.runner})` : "";
|
|
299
|
+
return `main turned red at ${shortSha(e.commit)}${by}`;
|
|
300
|
+
}
|
|
261
301
|
function mergeEventMessage(e) {
|
|
262
302
|
switch (e.type) {
|
|
263
303
|
case "merged":
|
|
264
304
|
return `merged ${shortSha(e.commit)} to main — ${textOr(e.summary)}`;
|
|
265
305
|
case "merge_queued":
|
|
266
|
-
|
|
267
|
-
// ahead) — no warning prefix. The merge's own events follow it. A retry-bearing event
|
|
268
|
-
// (a re-queued entry) appends its why-it-is-back suffix; a
|
|
269
|
-
// fresh enqueue's line stays byte-identical.
|
|
270
|
-
{
|
|
271
|
-
const base = `waiting to merge ${shortSha(e.commit)} — ${textOr(e.summary)}`;
|
|
272
|
-
return `${base}${typeof e.retry === "string" && e.retry ? ` (re-queued after ${textOr(e.retry)})` : ""}`;
|
|
273
|
-
}
|
|
306
|
+
return mergeQueuedMessage(e);
|
|
274
307
|
case "merge_cancelled":
|
|
275
308
|
// Routine operation (the user removed a waiting merge), not a warning — like
|
|
276
309
|
// prompt_cancelled. The merge_cancelled event name carries the payload (commit + summary).
|
|
@@ -285,36 +318,15 @@ function mergeEventMessage(e) {
|
|
|
285
318
|
// render bare — the same phrase run_end carries for its run.
|
|
286
319
|
return `merge of ${shortSha(e.commit)} done${elapsed(e.durationMs)}${usagePhrase(e)}`;
|
|
287
320
|
case "merge_failed":
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return
|
|
291
|
-
case "
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return `conflict hand-back ${textOr(e.action)} ${shortSha(e.sha)}${why}${where}`;
|
|
298
|
-
}
|
|
299
|
-
case "operator_revert": {
|
|
300
|
-
const undid = typeof e.runner === "string" && e.runner !== "" ? `, undid ${e.runner}` : "";
|
|
301
|
-
return `reverted ${shortSha(String(e.commit ?? ""))} (by operator) as ${shortSha(String(e.revert ?? ""))}${undid}`; // vocabulary-ok: the deliberate-revert writer is the operator role
|
|
302
|
-
}
|
|
303
|
-
case "main_check": {
|
|
304
|
-
const failing = Array.isArray(e.failingTests) ? e.failingTests.map(String) : [];
|
|
305
|
-
// The commit count renders only when it is a real number over 1 — the corrupt-field rule
|
|
306
|
-
// this file applies to every event number (a hand-edited line's quoted "3" or "0x10" reads
|
|
307
|
-
// as no covered count, not as a coerced parenthetical).
|
|
308
|
-
const coveredN = finiteNumber(e.covered, null);
|
|
309
|
-
const covered = coveredN !== null && coveredN > 1 ? ` (${coveredN} commits)` : "";
|
|
310
|
-
const verdict = e.status === "failed" ? "failed" : "passed";
|
|
311
|
-
const names = failing.length > 0 ? `: ${joinWithMore(failing, 3, "; ")}` : "";
|
|
312
|
-
return `main ${shortSha(String(e.sha ?? ""))}${covered} full check ${verdict}${names}`;
|
|
313
|
-
}
|
|
314
|
-
case "main_culprit": {
|
|
315
|
-
const by = typeof e.runner === "string" && e.runner !== "" ? ` (merged by ${e.runner})` : "";
|
|
316
|
-
return `main turned red at ${shortSha(String(e.commit ?? ""))}${by}`;
|
|
317
|
-
}
|
|
321
|
+
return mergeFailedMessage(e);
|
|
322
|
+
case "conflict_handback":
|
|
323
|
+
return conflictHandbackMessage(e);
|
|
324
|
+
case "operator_revert":
|
|
325
|
+
return operatorRevertMessage(e);
|
|
326
|
+
case "main_check":
|
|
327
|
+
return mainCheckMessage(e);
|
|
328
|
+
case "main_culprit":
|
|
329
|
+
return mainCulpritMessage(e);
|
|
318
330
|
case "main_red_hold":
|
|
319
331
|
// The message already names the SHA, script, and consequence; the problem tone comes
|
|
320
332
|
// from PROBLEM_EVENTS.
|
|
@@ -106,6 +106,7 @@ export const RUN_RESULT_LABELS = {
|
|
|
106
106
|
quiet_killed: "stalled, resumed",
|
|
107
107
|
user_aborted: "stopped by you",
|
|
108
108
|
main_red: "main's check is failing",
|
|
109
|
+
withdrawn: "entry withdrawn, dropped",
|
|
109
110
|
};
|
|
110
111
|
/** The words for an event type or run result code: its own-property label, else the code with
|
|
111
112
|
* underscores as spaces. The two code families are disjoint, so one lookup serves both. */
|
|
@@ -26,6 +26,7 @@ const RESULT_ORDER = {
|
|
|
26
26
|
quiet_killed: 11,
|
|
27
27
|
aborted: 12,
|
|
28
28
|
user_aborted: 13,
|
|
29
|
+
withdrawn: 14,
|
|
29
30
|
};
|
|
30
31
|
/** The digest table's columns: every RunResult that at least one role logged in the window, ranked
|
|
31
32
|
* by RESULT_ORDER (ties alphabetical), so the table's shape follows the window's evidence instead
|
|
@@ -24,6 +24,7 @@ export const OUTCOME_CLASS = {
|
|
|
24
24
|
quiet_killed: "error",
|
|
25
25
|
aborted: "error",
|
|
26
26
|
user_aborted: "error",
|
|
27
|
+
withdrawn: "no_change",
|
|
27
28
|
};
|
|
28
29
|
/** A row's summed wall-clock span across its outcome classes — the sort key that puts the
|
|
29
30
|
* burning role first, the way the usage report ranks its cost lines. */
|
|
@@ -131,6 +131,12 @@ function settlePatch(root, file) {
|
|
|
131
131
|
return file;
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
/** Settle every patch waiting for `role`'s next run under salvageDir for the operator instead
|
|
135
|
+
* of restoring it: the work it saved belongs to a withdrawn backlog entry (the runner's
|
|
136
|
+
* `withdrawn` marker). Returns the kept paths, newest first. */
|
|
137
|
+
export function keepSalvagedWorkForOperator(root, role) {
|
|
138
|
+
return pendingPatches(root, role).map((file) => settlePatch(root, file));
|
|
139
|
+
}
|
|
134
140
|
/** Restore the newest patch of `role`'s unfinished work waiting under salvagePendingDir into `dir`,
|
|
135
141
|
* the fresh checkout of the role's branch its run is about to work in, as uncommitted edits. The
|
|
136
142
|
* patch applies exactly when the checkout is still at the patch's base. When the branch has moved,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The dashboard's GET run/event-log drill-down handlers (src/gui/gui-server.ts routes to them):
|
|
3
3
|
* transcript, history, logs, run, and role. Each answers its request and touches no socket
|
|
4
4
|
* beyond its own `res`; the domain work lives one layer down (ui/transcript.ts,
|
|
5
|
-
* history/history-data.ts,
|
|
5
|
+
* history/history-data.ts, history/run-detail-data.ts, events/event-read.ts), so this module only
|
|
6
6
|
* adapts HTTP onto it. Barrelled into gui/gui-endpoints.ts beside the other per-family GET
|
|
7
7
|
* endpoint modules.
|
|
8
8
|
*/
|
|
@@ -12,15 +12,15 @@ import { HISTORY_DEFAULT_RUNS, HISTORY_MAX_RUNS, HISTORY_RESULT_FLAG_NAMES, hist
|
|
|
12
12
|
import { LOGS_SINCE_MAX_MS, SPARSE_WINDOW_NOTE, readEventsSince } from "../events/event-window.js";
|
|
13
13
|
import { eventRole, readEvents } from "../events/event-read.js";
|
|
14
14
|
import { eventFeedItem, eventMatchesGrep } from "../events/event-format.js";
|
|
15
|
-
import { readRunDetail, runCommitSha, runMergedSha } from "../
|
|
16
|
-
import { renderRunDetail, runDetailPatch, runDetailStat, runTranscriptLines } from "../
|
|
15
|
+
import { readRunDetail, runCommitSha, runMergedSha } from "../history/run-detail-data.js";
|
|
16
|
+
import { renderRunDetail, runDetailPatch, runDetailStat, runTranscriptLines } from "../history/run-detail-render.js";
|
|
17
17
|
import { resolveRevertTarget } from "../operator/operator-revert.js";
|
|
18
18
|
import { rolePayload } from "../roles/role-view.js";
|
|
19
19
|
import { clampCount, intQuery, rejectBadRole, sinceMsQuery, untilMsQuery } from "./gui-args.js";
|
|
20
20
|
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
21
21
|
import { oneOfMessage } from "../text/suggest.js";
|
|
22
22
|
import { splitCommaList, splitCommaListUnique } from "../text/text.js";
|
|
23
|
-
import { newestRunRow, runNotFoundLastMessage, runNotFoundMessage } from "../
|
|
23
|
+
import { newestRunRow, runNotFoundLastMessage, runNotFoundMessage } from "../history/run-detail.js";
|
|
24
24
|
/** Handle GET /api/transcript?role=<id>&n=N[&grep=<text>]: rendered transcript lines for a role's
|
|
25
25
|
* pi log (same rendering as `tumwater logs --transcript <id>`). A present `prompt` flag also
|
|
26
26
|
* renders each agent run's prompt, the text `logs --transcript <id> --prompt` prints.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fail, say, sayJson, sayJsonLine } from "../cli/cli-output.js";
|
|
2
|
-
import { betweenWindow, durationLabel, failRivalShapes, failUntilShorterThanSince, flagValue, parseCountFlag, parseGrepFlag, parseRoleListScope, parseSinceFlag, rejectFlagLookingValue } from "../cli/cli-args.js";
|
|
2
|
+
import { betweenWindow, durationLabel, failFollowSinceRival, failRivalShapes, failUntilShorterThanSince, flagValue, parseCountFlag, parseGrepFlag, parseRoleListScope, parseSinceFlag, rejectFlagLookingValue } from "../cli/cli-args.js";
|
|
3
3
|
import { HISTORY_DEFAULT_RUNS, HISTORY_MAX_RUNS, HISTORY_RESULT_FLAG_NAMES, HISTORY_SUMMARY_GROUPS, historyClass, parseHistoryResultClass, readRunRows, readRunRowsSince, runRows, summarizeRunRows, summarizeRunRowsByModel, runRowsMatchingGrep, runRowsOnModels } from "./history-data.js";
|
|
4
4
|
import { renderModelSummary, renderRow, renderSummary, widenWidths, widestWidths } from "./history-render.js";
|
|
5
5
|
import { LOGS_SINCE_MAX_MS, SPARSE_WINDOW_NOTE } from "../events/event-window.js";
|
|
@@ -93,7 +93,7 @@ function parseHistoryQuery(root, args) {
|
|
|
93
93
|
const follow = hasFollowFlag(rest);
|
|
94
94
|
if (follow) {
|
|
95
95
|
if (rest.includes("--since"))
|
|
96
|
-
|
|
96
|
+
failFollowSinceRival("history");
|
|
97
97
|
if (rest.includes("--summary"))
|
|
98
98
|
failRivalShapes("history --summary", "-f/--follow", "a summary tallies a bounded set; -f follows from now on");
|
|
99
99
|
}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* runMergedSha) live here beside the payload they describe; the rendering half — the summary
|
|
10
10
|
* header, formatEvent, the stat/patch and transcript text — lives in run-detail-render.ts,
|
|
11
11
|
* and the CLI command in run-detail.ts. */
|
|
12
|
-
import { HISTORY_SCAN_MAX_EVENTS } from "
|
|
13
|
-
import { bucketMergeEvents, resolveQueuedResult } from "
|
|
12
|
+
import { HISTORY_SCAN_MAX_EVENTS } from "./history-data.js";
|
|
13
|
+
import { bucketMergeEvents, resolveQueuedResult } from "./history-queued.js";
|
|
14
14
|
import { eventUsage, readEvents, runSpanMs, runStartMap } from "../events/event-read.js";
|
|
15
15
|
import { outcomeText, usageText } from "../events/event-format.js";
|
|
16
16
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
import { fail, say, sayJson } from "../cli/cli-output.js";
|
|
8
8
|
import { parseCountFlag } from "../cli/cli-args.js";
|
|
9
9
|
import { knownRoleIdsCached } from "../config/config.js";
|
|
10
|
-
import { HISTORY_MAX_RUNS, HISTORY_RESULT_FLAG_NAMES, historyClass, readRunRows, runRowsOnModels } from "
|
|
11
|
-
import { parseHistoryModelList, parseHistoryResultList } from "
|
|
10
|
+
import { HISTORY_MAX_RUNS, HISTORY_RESULT_FLAG_NAMES, historyClass, readRunRows, runRowsOnModels } from "./history-data.js";
|
|
11
|
+
import { parseHistoryModelList, parseHistoryResultList } from "./history.js";
|
|
12
12
|
import { readRunDetail } from "./run-detail-data.js";
|
|
13
13
|
import { runDetailPatch, runDetailStat, renderRunDetail, runTranscriptLines } from "./run-detail-render.js";
|
|
14
14
|
import { unknownRoleMessage } from "../roles/roles.js";
|
|
@@ -4,6 +4,7 @@ import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
|
4
4
|
import { lowestStampName, queueFileStamp, removeQueueFile } from "../files/file-queue.js";
|
|
5
5
|
import { readTextIfExists, writeTextAtomic } from "../files/files.js";
|
|
6
6
|
import { roleInboxDir } from "../paths.js";
|
|
7
|
+
import { roleLabel } from "../roles/role-labels.js";
|
|
7
8
|
import { assertQueueFileName } from "./inbox-cancel.js";
|
|
8
9
|
import { promptPreview, queuedFileAtPosition, queuedFiles } from "./inbox.js";
|
|
9
10
|
import { movePromptImages, stripVanishedImageReferences } from "./inbox-attachments.js";
|
|
@@ -72,7 +73,7 @@ function moveFileToTop(root, role, file) {
|
|
|
72
73
|
export function movePromptToTop(root, role, position) {
|
|
73
74
|
const file = queuedFileAtPosition(root, role, position);
|
|
74
75
|
if (position === 1)
|
|
75
|
-
throw new Error(`prompt ${position} is already first`);
|
|
76
|
+
throw new Error(`prompt ${position} for ${roleLabel(role)} is already first`);
|
|
76
77
|
return moveFileToTop(root, role, file);
|
|
77
78
|
}
|
|
78
79
|
/** Move one queued prompt addressed by its queue-file basename to the head of its role's
|
|
@@ -9,6 +9,7 @@ import { leaseSlot } from "../git/worktree-pool.js";
|
|
|
9
9
|
import { slotCount } from "../config/config.js";
|
|
10
10
|
import { reviewPinnedChange, syncPinToMain, } from "./merge-core.js";
|
|
11
11
|
import { errorMessage } from "../text/text.js";
|
|
12
|
+
import { dropWithdrawnPin, withdrawnPinReason } from "./merge-withdrawn.js";
|
|
12
13
|
/** Verify one request — the whole of a verify-stage task: lease a pooled slot for the role, check
|
|
13
14
|
* its pin out detached there, rebase it onto main's current tip (syncPinToMain — so the head
|
|
14
15
|
* approved here is the head its merge starts from, and a conflict leaves the pin for the gate
|
|
@@ -20,6 +21,14 @@ import { errorMessage } from "../text/text.js";
|
|
|
20
21
|
* a commit. leaseSlot holds the slot through the disk-floor registry and resets it to the pin, so
|
|
21
22
|
* two verifies rebasing at once — or one rebasing while a merge moves main — cannot interfere. */
|
|
22
23
|
export async function verifyRequest(ctx, req, w) {
|
|
24
|
+
// A change whose backlog entry main no longer lists never merges (merge-withdrawn.ts). Asked
|
|
25
|
+
// before the slot lease and syncPinToMain's rebase, while the pin's merge-base with main still
|
|
26
|
+
// lists every entry the change moved: the rebase would carry its base past the removal.
|
|
27
|
+
const withdrawn = await withdrawnPinReason(ctx.root, ctx.mainBranch, req, req.sha);
|
|
28
|
+
if (withdrawn !== null) {
|
|
29
|
+
await dropWithdrawnPin(ctx.root, req.role, w.state, withdrawn);
|
|
30
|
+
return { kind: "result", result: "withdrawn" };
|
|
31
|
+
}
|
|
23
32
|
let lease;
|
|
24
33
|
try {
|
|
25
34
|
lease = await leaseSlot(ctx.root, {
|
|
@@ -14,6 +14,7 @@ import { priorObjectionNumber } from "../review/review-verdict.js";
|
|
|
14
14
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
15
15
|
import { shortSha } from "../text/format.js";
|
|
16
16
|
import { setMergeStage } from "./merge-slot.js";
|
|
17
|
+
import { dropWithdrawnPin, withdrawnPinReason } from "./merge-withdrawn.js";
|
|
17
18
|
/** Rebase a pinned change in its leased pool slot `wt` (checked out detached at `req.sha`) onto
|
|
18
19
|
* main's CURRENT head before its review gate, and return the request that gate must judge.
|
|
19
20
|
* The gate must review main's current tree, not the main the author started from: when main
|
|
@@ -277,6 +278,15 @@ async function mergeApprovedChangeIn(ctx, req) {
|
|
|
277
278
|
mainBranch: ctx.mainBranch,
|
|
278
279
|
config: ctx.config,
|
|
279
280
|
runNumber: req.runNumber,
|
|
281
|
+
// Main may have dropped the change's backlog entry since its verify: checked again in the
|
|
282
|
+
// lock, where a withdrawn change deletes its pin and the merge settles `withdrawn`.
|
|
283
|
+
withdrawn: async (mergeWt) => {
|
|
284
|
+
const reason = await withdrawnPinReason(mergeWt, ctx.mainBranch, req, "HEAD");
|
|
285
|
+
if (reason === null)
|
|
286
|
+
return false;
|
|
287
|
+
await dropWithdrawnPin(ctx.root, req.role, ctx.state, reason);
|
|
288
|
+
return true;
|
|
289
|
+
},
|
|
280
290
|
runPi: ctx.runPi,
|
|
281
291
|
recheckResolved: async (resolvedWt) => {
|
|
282
292
|
if (ctx.signal().aborted)
|
|
@@ -326,6 +336,9 @@ async function mergeApprovedChangeIn(ctx, req) {
|
|
|
326
336
|
await deleteRef(ctx.root, mergeRefName(req.role)); // merged: the pin has done its job
|
|
327
337
|
return result;
|
|
328
338
|
}
|
|
339
|
+
// Withdrawn in the lock: the closure above already deleted the pin and wrote the lastError.
|
|
340
|
+
if (result === "withdrawn")
|
|
341
|
+
return result;
|
|
329
342
|
// merge_conflict / merge_blocked: keep the ref — the next run's recovery re-merges it.
|
|
330
343
|
// (The deterministic cross-check blocks — fix-claim and backlog structure — no longer come
|
|
331
344
|
// through here: since part 3m/3 they are repairable and land for the main watch.)
|
|
@@ -59,8 +59,18 @@ export async function settleAbortedApproved(root, p) {
|
|
|
59
59
|
}
|
|
60
60
|
catch (err) {
|
|
61
61
|
warnEventBestEffort(root, TUMWATER_SOURCE, `the stopped merge for ${role} could not be settled: ${errorMessage(err)}`);
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// The abort is honored by hand — drop the queue file and marker. That cleanup can itself
|
|
63
|
+
// throw (a queue file that is a directory fails fs.rmSync's non-recursive delete, the
|
|
64
|
+
// shape an ENOSPC or immutable queue file takes): report it and still discard the pin,
|
|
65
|
+
// so this settle never throws out of the poll body and the next poll does not re-verify
|
|
66
|
+
// and merge the change the operator stopped.
|
|
67
|
+
try {
|
|
68
|
+
dropMerge(v.file);
|
|
69
|
+
removeMergeChange(root, role);
|
|
70
|
+
}
|
|
71
|
+
catch (cleanupErr) {
|
|
72
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `the stopped merge for ${role} could not be dropped: ${errorMessage(cleanupErr)}`);
|
|
73
|
+
}
|
|
64
74
|
}
|
|
65
75
|
await discardPinnedRefs(root, [role]);
|
|
66
76
|
}
|
|
@@ -187,6 +197,7 @@ function startMerge(ctx, p, picks) {
|
|
|
187
197
|
sha: v.sha,
|
|
188
198
|
runNumber: v.entry.run,
|
|
189
199
|
summary: v.entry.summary,
|
|
200
|
+
...(v.entry.claim !== undefined ? { claim: v.entry.claim } : {}),
|
|
190
201
|
body: v.entry.body,
|
|
191
202
|
highFriction: v.entry.highFriction,
|
|
192
203
|
...entryRevisionFields(v.entry),
|
|
@@ -91,10 +91,11 @@ awaitingWatch) {
|
|
|
91
91
|
writeMergeOutcome(root, entry, state, result, durationMs, usage, file);
|
|
92
92
|
// A revision that merges ends the rejected change's life: drop the rejected ref (plans/revise-
|
|
93
93
|
// rejected.md). A rejection repoints or deletes it in merge-core.ts, so this is the only
|
|
94
|
-
// ref cleanup a merged revision needs here
|
|
95
|
-
// `changed` result: a fresh change can merge
|
|
96
|
-
// pending, and its merge must leave that
|
|
97
|
-
|
|
94
|
+
// ref cleanup a merged revision needs here; a withdrawn revision ends it the same way.
|
|
95
|
+
// Guarded on the entry's revisionRound, not just a `changed` result: a fresh change can merge
|
|
96
|
+
// while a different rejection's revision is still pending, and its merge must leave that
|
|
97
|
+
// pending revision's ref alone.
|
|
98
|
+
if ((result === "changed" || result === "withdrawn") && entry.revisionRound !== undefined) {
|
|
98
99
|
await deleteRef(root, rejectedRefName(entry.role));
|
|
99
100
|
}
|
|
100
101
|
removeMergeChange(root, entry.role);
|
|
@@ -120,7 +120,14 @@ function isMergeEntry(v) {
|
|
|
120
120
|
(e.notBefore === undefined || nonNegativeNumber(e.notBefore, null) !== null) &&
|
|
121
121
|
(e.retry === undefined || typeof e.retry === "string") &&
|
|
122
122
|
(e.retryCount === undefined || nonNegativeNumber(e.retryCount, null) !== null) &&
|
|
123
|
-
(e.watchWait === undefined || e.watchWait === "covered" || e.watchWait === "timed_out")
|
|
123
|
+
(e.watchWait === undefined || e.watchWait === "covered" || e.watchWait === "timed_out") &&
|
|
124
|
+
(e.claim === undefined || isMergeClaim(e.claim)));
|
|
125
|
+
}
|
|
126
|
+
function isMergeClaim(v) {
|
|
127
|
+
return (isJsonObject(v) &&
|
|
128
|
+
(v.file === "PLANS.md" || v.file === "BUGS.md") &&
|
|
129
|
+
typeof v.key === "string" &&
|
|
130
|
+
typeof v.title === "string");
|
|
124
131
|
}
|
|
125
132
|
/** Read one queue file through the stat-keyed cache: null when it vanishes mid-listing (a
|
|
126
133
|
* concurrent drop — the inbox.ts race policy), is unreadable, or fails to parse or to carry
|
|
@@ -72,6 +72,18 @@ export async function requeueKeptChange(root, mainBranch, entry, state, result,
|
|
|
72
72
|
* watch check that timed out or a restart's cold cache. A pin its gate kept for the watch waits
|
|
73
73
|
* at most this long from the moment it was kept. */
|
|
74
74
|
export const MAIN_RED_VERDICT_WAIT_MS = 15 * 60_000;
|
|
75
|
+
/** The wait-start/timeout gate shared by requeueMainRedPin and requeueAwaitingWatch: if the kept
|
|
76
|
+
* record has no wait start yet, record `now` as one (and save) and hold this poll; otherwise hold
|
|
77
|
+
* while MAIN_RED_VERDICT_WAIT_MS has not passed since the start. Returns true only when the pin may
|
|
78
|
+
* proceed to re-queue. */
|
|
79
|
+
function mainRedWaitElapsed(kept, root, state, now) {
|
|
80
|
+
if (kept.waitingSince === undefined) {
|
|
81
|
+
state.mainRedRetry = { ...kept, waitingSince: now };
|
|
82
|
+
saveRunnerStateBestEffort(root, state);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return now - kept.waitingSince >= MAIN_RED_VERDICT_WAIT_MS;
|
|
86
|
+
}
|
|
75
87
|
/** Re-queue a `main_red` pin once main's head differs from the one recorded when its attempt
|
|
76
88
|
* settled (the orchestrator poll calls this for every runner), by the head's cached verdict: a
|
|
77
89
|
* verdict — green or red — re-queues at once; no verdict starts (and saves) a wait at `now`, and
|
|
@@ -92,15 +104,8 @@ export async function requeueMainRedPin(root, mainBranch, state, mainHead, now =
|
|
|
92
104
|
}
|
|
93
105
|
if (mainHead === kept.head)
|
|
94
106
|
return;
|
|
95
|
-
if (cachedBaselineVerdict(mainHead) === undefined)
|
|
96
|
-
|
|
97
|
-
state.mainRedRetry = { ...kept, waitingSince: now };
|
|
98
|
-
saveRunnerStateBestEffort(root, state);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (now - kept.waitingSince < MAIN_RED_VERDICT_WAIT_MS)
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
107
|
+
if (cachedBaselineVerdict(mainHead) === undefined && !mainRedWaitElapsed(kept, root, state, now))
|
|
108
|
+
return;
|
|
104
109
|
await requeueKeptPin(root, mainBranch, state);
|
|
105
110
|
}
|
|
106
111
|
/** requeueMainRedPin for a change its gate kept for the main watch (`awaitingWatch`): re-queue
|
|
@@ -114,15 +119,8 @@ async function requeueAwaitingWatch(root, mainBranch, state, mainHead, now) {
|
|
|
114
119
|
if (kept?.awaitingWatch === undefined)
|
|
115
120
|
return;
|
|
116
121
|
const { covers } = await watchVerdictCovering(root, mainHead, kept.awaitingWatch);
|
|
117
|
-
if (!covers)
|
|
118
|
-
|
|
119
|
-
state.mainRedRetry = { ...kept, waitingSince: now };
|
|
120
|
-
saveRunnerStateBestEffort(root, state);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (now - kept.waitingSince < MAIN_RED_VERDICT_WAIT_MS)
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
122
|
+
if (!covers && !mainRedWaitElapsed(kept, root, state, now))
|
|
123
|
+
return;
|
|
126
124
|
await requeueKeptPin(root, mainBranch, state, covers ? "covered" : "timed_out");
|
|
127
125
|
}
|
|
128
126
|
/** Clear the kept `main_red` record and put its pin back on the queue — unless the pin is gone,
|
|
@@ -55,6 +55,10 @@ async function tryMerge(ctx, wt, summary) {
|
|
|
55
55
|
// and compare, so the diff is exact; on the conflict path only the second tryMerge call ever
|
|
56
56
|
// reaches the post-ff code, so nothing double-emits.
|
|
57
57
|
const before = openQuestions(ctx.root);
|
|
58
|
+
// In the lock, so no withdrawal can land between this check and the fast-forward; before the
|
|
59
|
+
// rebase, so the change's merge-base still lists the entries it moved.
|
|
60
|
+
if (ctx.withdrawn && (await ctx.withdrawn(wt)))
|
|
61
|
+
return "withdrawn";
|
|
58
62
|
if (!(await rebaseOntoMain(wt, ctx.mainBranch)))
|
|
59
63
|
return "merge_conflict";
|
|
60
64
|
// The worktree's POST-REBASE HEAD: a pinned bare sha does not move when git
|
|
@@ -60,7 +60,11 @@ export function resolveAuthor(ctx, role) {
|
|
|
60
60
|
* dropped (the pin survives and leftover recovery re-queues the full shape on the role's next
|
|
61
61
|
* run), and the drain's poll never aborts. A torn head with no recoverable role or no pin drops
|
|
62
62
|
* with the warning alone; the crashed entry's commit, if any, still rides its merge ref into
|
|
63
|
-
* next-run leftover recovery.
|
|
63
|
+
* next-run leftover recovery. The drop itself can throw (a queue file that is a directory fails
|
|
64
|
+
* fs.rmSync's non-recursive delete — the shape an ENOSPC or immutable queue file takes), and this
|
|
65
|
+
* runs in the poll body with nothing to guard it: report it and leave the head in place rather
|
|
66
|
+
* than end the team, exactly as settleAbortedApproved guards its own drop. Exported for
|
|
67
|
+
* test/merge-queue.test.ts's torn-head step. */
|
|
64
68
|
export async function dropTornHead(root) {
|
|
65
69
|
const stale = staleHeadFile(root);
|
|
66
70
|
if (!stale)
|
|
@@ -89,7 +93,13 @@ export async function dropTornHead(root) {
|
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
|
-
|
|
96
|
+
try {
|
|
97
|
+
dropMerge(stale);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `merge queue head ${path.basename(stale)} could not be dropped: ${errorMessage(err)}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
93
103
|
const roleNote = !role
|
|
94
104
|
? ""
|
|
95
105
|
: requeued
|
|
@@ -221,6 +231,7 @@ export function startVerify(ctx, p, entry, file) {
|
|
|
221
231
|
highFriction: entry.highFriction,
|
|
222
232
|
...entryRevisionFields(entry),
|
|
223
233
|
...(entry.watchWait !== undefined ? { watchWait: entry.watchWait } : {}),
|
|
234
|
+
...(entry.claim !== undefined ? { claim: entry.claim } : {}),
|
|
224
235
|
}, { state: author.state, foldUsage, runPi: (w, prompt, s, cfg) => author.runMergePi(w, prompt, s, cfg), runGatePi: (opts) => author.runGatePi(opts) }));
|
|
225
236
|
}
|
|
226
237
|
catch (err) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Withdrawn work: a queued change whose backlog entry main no longer lists never merges.
|
|
2
|
+
* On 2026-11-01 the operator withdrew "Subtract, part 3b/7" by deleting its PLANS.md entry ten
|
|
3
|
+
* minutes after feature-2 had claimed it. The claim was released, but the interrupted run kept
|
|
4
|
+
* resuming with the plan in its session and its kept worktree, and on 2026-11-02 its change
|
|
5
|
+
* merged (8ed0d3eec), deleting the pre-merge build check the operator had chosen to keep.
|
|
6
|
+
*
|
|
7
|
+
* The scheduling poll drops a withdrawn claim's resume or revision (claims.ts's `withdrawn`
|
|
8
|
+
* release). This module is the backstop at the merge, where every path to main passes: the
|
|
9
|
+
* verify asks it before it rebases a pin onto main, and the merge asks it again inside the merge
|
|
10
|
+
* lock before its own rebase. An entry is withdrawn from a change when the change works on it —
|
|
11
|
+
* its run's recorded claim, or an entry its diff moved out of the role's actionable section
|
|
12
|
+
* since its merge-base with main — and main's tip no longer lists it in that section. Each check
|
|
13
|
+
* runs before the rebase that would move the change's base past the removal, so the base still
|
|
14
|
+
* lists what the change moved. Only the work roles (feature, bugfix) take backlog entries; every
|
|
15
|
+
* other role's changes are never judged here. */
|
|
16
|
+
import { actionableEntryRanges } from "../backlog/backlog-index-data.js";
|
|
17
|
+
import { entryKey } from "../backlog/backlog-parts.js";
|
|
18
|
+
import { roleSection } from "../backlog/backlog-eligibility.js";
|
|
19
|
+
import { stripEntryStamp } from "../backlog/backlog-structure.js";
|
|
20
|
+
import { warnEventBestEffort } from "../events/events.js";
|
|
21
|
+
import { changeBaseRev, deleteRef, fileContentAt } from "../git/git.js";
|
|
22
|
+
import { gitTryStarted } from "../git/git-run.js";
|
|
23
|
+
import { mergeRefName } from "../paths.js";
|
|
24
|
+
import { INSTANCE_ROLES, baseRoleOf } from "../roles/runner-ids.js";
|
|
25
|
+
import { movedOutEntries } from "../scheduling/claims.js";
|
|
26
|
+
/** The entries of `role`'s backlog section that commit `rev` works on and `mainBranch`'s tip no
|
|
27
|
+
* longer lists there, read through git in `dir` (the repo or any of its worktrees). A change works
|
|
28
|
+
* on `claim` (when it names the role's own backlog file) and on every entry its diff moved out of
|
|
29
|
+
* the section since its merge-base with `mainBranch`. Empty for a role that takes no backlog
|
|
30
|
+
* entries, and when the section cannot be read on main or at `rev`: an unreadable file never
|
|
31
|
+
* drops work. Throws only when git cannot start. */
|
|
32
|
+
export async function withdrawnEntries(dir, mainBranch, role, rev, claim) {
|
|
33
|
+
if (!INSTANCE_ROLES.has(baseRoleOf(role)))
|
|
34
|
+
return [];
|
|
35
|
+
const { file, section } = roleSection(role);
|
|
36
|
+
const mainMd = await gitTryStarted(dir, "show", `${mainBranch}:${file}`);
|
|
37
|
+
const headMd = await gitTryStarted(dir, "show", `${rev}:${file}`);
|
|
38
|
+
if (mainMd === null || headMd === null)
|
|
39
|
+
return [];
|
|
40
|
+
const listed = new Set(actionableEntryRanges(mainMd, section).map((e) => entryKey(e.title)));
|
|
41
|
+
const baseMd = await fileContentAt(dir, await changeBaseRev(dir, mainBranch, rev), file);
|
|
42
|
+
const worked = new Map();
|
|
43
|
+
if (claim?.file === file)
|
|
44
|
+
worked.set(claim.key, claim.title);
|
|
45
|
+
for (const m of movedOutEntries(baseMd, headMd, section))
|
|
46
|
+
worked.set(m.key, m.title);
|
|
47
|
+
return [...worked].filter(([key]) => !listed.has(key)).map(([key, title]) => ({ key, title }));
|
|
48
|
+
}
|
|
49
|
+
/** The one-line reason a withdrawn change was dropped, naming the first entry (stamp-free) and
|
|
50
|
+
* the section main no longer lists it under. It is the warning and, behind the `withdrawn — `
|
|
51
|
+
* prefix the merge_failed reason strips, the role's lastError, so both say the same thing. */
|
|
52
|
+
function withdrawnReason(role, entries) {
|
|
53
|
+
const { file, section } = roleSection(role);
|
|
54
|
+
const more = entries.length > 1 ? ` (and ${entries.length - 1} more)` : "";
|
|
55
|
+
const title = stripEntryStamp(entries[0].title);
|
|
56
|
+
return `dropped the change for "${title}"${more} unmerged: ${file} on main no longer lists it under ## ${section}`;
|
|
57
|
+
}
|
|
58
|
+
/** The reason to drop the pinned change `req` names at commit `rev` as withdrawn, or null to let
|
|
59
|
+
* it go on: withdrawnEntries over its role and recorded claim, worded by withdrawnReason. */
|
|
60
|
+
export async function withdrawnPinReason(dir, mainBranch, req, rev) {
|
|
61
|
+
const entries = await withdrawnEntries(dir, mainBranch, req.role, rev, req.claim);
|
|
62
|
+
return entries.length === 0 ? null : withdrawnReason(req.role, entries);
|
|
63
|
+
}
|
|
64
|
+
/** Drop a pinned change found withdrawn: delete its merge ref, so neither the pipeline's re-queue
|
|
65
|
+
* nor leftover recovery can bring it back, record the reason as the role's lastError (the
|
|
66
|
+
* merge_failed event's reason) and warn with it. The caller settles the change `withdrawn`. */
|
|
67
|
+
export async function dropWithdrawnPin(root, role, state, reason) {
|
|
68
|
+
await deleteRef(root, mergeRefName(role));
|
|
69
|
+
state.lastError = `withdrawn — ${reason}`;
|
|
70
|
+
warnEventBestEffort(root, role, reason);
|
|
71
|
+
}
|
|
@@ -6,8 +6,8 @@ import { PAUSE_FOR_OVERFLOW_HINT, REASON_VALUE_ERROR, ROLE_VALUE_ERROR } from ".
|
|
|
6
6
|
import { artifactPhrase, hasRetirableLeftovers, previewRetire, retireRole } from "./operator-retire.js";
|
|
7
7
|
import { previewRevert, revertFollowUp, revertMergedCommit, runRevertSha } from "./operator-revert.js";
|
|
8
8
|
import { distinctOrdered } from "../collections.js";
|
|
9
|
-
import { runNotFoundMessage } from "../
|
|
10
|
-
import { readRunDetail } from "../
|
|
9
|
+
import { runNotFoundMessage } from "../history/run-detail.js";
|
|
10
|
+
import { readRunDetail } from "../history/run-detail-data.js";
|
|
11
11
|
import { readRunRows } from "../history/history-data.js";
|
|
12
12
|
import { canonicalRoleId } from "../roles/legacy-role-ids.js";
|
|
13
13
|
import { agree, plural, teamResumePhrase, pauseReasonSuffix } from "../text/phrases.js";
|
|
@@ -5,7 +5,7 @@ import { GitError, commitAuthorArgs, committerIdentArgs, gitTry, runGit } from "
|
|
|
5
5
|
import { stampedRole, stampedSubject } from "../git/commit-message.js";
|
|
6
6
|
import { shortSha } from "../text/format.js";
|
|
7
7
|
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
8
|
-
import { runMergedSha } from "../
|
|
8
|
+
import { runMergedSha } from "../history/run-detail-data.js";
|
|
9
9
|
/** `tumwater revert <sha>`: undo one merged change on main, committing the inverse as
|
|
10
10
|
* `tumwater(operator): Revert <short sha>` on the primary checkout. Mirrors
|
|
11
11
|
* `commitOperatorBacklogEdit`'s lock-and-branch discipline (src/backlog/backlog-commit.ts): the
|