tumwater 0.4.3 → 0.4.5
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/agent-run.js +3 -1
- package/dist/src/agent/claude/claude-account.js +6 -20
- package/dist/src/agent/codex/codex-account.js +5 -6
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-rollout.js +1 -1
- package/dist/src/agent/opencode/opencode-backend.js +24 -22
- package/dist/src/agent/opencode/opencode-catalog.js +35 -9
- package/dist/src/agent/opencode/write-guard-plugin/index.js +11 -8
- package/dist/src/agent/session-ledger.js +22 -26
- package/dist/src/agent/subscription-account.js +25 -41
- package/dist/src/agent/write-guard.js +50 -13
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-eligibility.js +21 -39
- package/dist/src/backlog/backlog-index.js +2 -2
- package/dist/src/backlog/backlog-md.js +13 -2
- package/dist/src/backlog/backlog-question-answer.js +1 -1
- package/dist/src/backlog/backlog-release.js +3 -11
- package/dist/src/backlog/backlog-write.js +19 -11
- package/dist/src/backlog/backlog.js +2 -1
- package/dist/src/baseline/main-baseline.js +47 -72
- package/dist/src/baseline/main-guardrails.js +13 -9
- package/dist/src/baseline/main-red.js +100 -12
- package/dist/src/baseline/main-watch.js +134 -54
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +96 -28
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/build/build-check-detect.js +22 -3
- package/dist/src/build/build-check-events.js +25 -5
- package/dist/src/build/build-check-report.js +47 -1
- package/dist/src/build/build-check-rerun.js +62 -0
- package/dist/src/build/build-check-scoped.js +13 -4
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +4 -0
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/build/host-sleep.js +9 -0
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/change/change-render.js +2 -1
- package/dist/src/cli/cli-args.js +34 -2
- package/dist/src/cli/cli-command-args.js +12 -4
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-init.js +7 -3
- package/dist/src/cli/cli-query-commands.js +11 -7
- package/dist/src/cli/cli-review-commands.js +3 -3
- package/dist/src/cli/cli-role-command.js +339 -284
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +14 -189
- package/dist/src/cli/completion.js +9 -5
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +3 -8
- package/dist/src/cli.js +123 -303
- package/dist/src/collections.js +31 -13
- package/dist/src/concurrency/check-permit.js +30 -3
- package/dist/src/concurrency/lock.js +74 -15
- package/dist/src/config/config-editable-keys.js +9 -0
- package/dist/src/config/config-fallbacks.js +16 -5
- package/dist/src/config/config-keys.js +61 -20
- package/dist/src/config/config-request.js +11 -1
- package/dist/src/config/config-schema.js +4 -4
- package/dist/src/config/config-validation-roles.js +6 -22
- package/dist/src/config/config-validation.js +2 -1
- package/dist/src/config/config-views.js +62 -39
- package/dist/src/config/config-write.js +122 -79
- package/dist/src/config/config.js +13 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +33 -1
- package/dist/src/doctor/doctor-backlog-plans.js +29 -96
- package/dist/src/doctor/doctor-backlog.js +14 -16
- package/dist/src/doctor/doctor-check-names.js +2 -0
- package/dist/src/doctor/doctor-checks.js +16 -9
- package/dist/src/doctor/doctor-claude-checks.js +29 -11
- package/dist/src/doctor/doctor-contention-checks.js +20 -11
- package/dist/src/doctor/doctor-fallback-checks.js +83 -26
- package/dist/src/doctor/doctor-holds.js +9 -12
- package/dist/src/doctor/doctor-model-checks.js +7 -0
- package/dist/src/doctor/doctor-opencode-checks.js +2 -2
- package/dist/src/doctor/doctor-orphans.js +106 -29
- package/dist/src/doctor/doctor-project-checks.js +52 -22
- package/dist/src/doctor/doctor.js +5 -3
- package/dist/src/events/event-format.js +78 -109
- package/dist/src/events/event-labels.js +2 -1
- package/dist/src/events/event-legacy.js +2 -2
- package/dist/src/events/event-notify.js +4 -4
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +4 -13
- package/dist/src/failure/failure-data.js +10 -8
- package/dist/src/failure/failure-render.js +2 -2
- package/dist/src/failure/failure-state-change.js +64 -90
- package/dist/src/files/file-queue.js +32 -19
- package/dist/src/files/files.js +2 -2
- package/dist/src/files/json-object.js +3 -2
- package/dist/src/gates/budget-gates.js +37 -2
- package/dist/src/gates/gate-polls.js +13 -13
- package/dist/src/gates/{maintenance-quota.js → maintenance-quota-gates.js} +1 -1
- package/dist/src/gates/startup-gates.js +20 -2
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +8 -0
- package/dist/src/git/worktree-pool.js +18 -5
- package/dist/src/gui/gui-admin-endpoints.js +11 -1
- package/dist/src/gui/gui-args.js +14 -2
- package/dist/src/gui/gui-backlog-endpoints.js +2 -1
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +3 -3
- package/dist/src/gui/gui-operator-commands.js +23 -28
- package/dist/src/gui/gui-prompt-commands.js +2 -2
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history.js +14 -10
- package/dist/src/inbox/director-request.js +92 -0
- package/dist/src/merge/merge-backlog-conflicts.js +33 -11
- package/dist/src/merge/merge-batch.js +13 -235
- package/dist/src/merge/merge-cancel.js +2 -1
- package/dist/src/merge/merge-core.js +18 -52
- package/dist/src/merge/merge-drain.js +106 -101
- package/dist/src/merge/merge-pipeline.js +6 -4
- package/dist/src/merge/merge-post-review-drain.js +9 -3
- package/dist/src/merge/merge-questions.js +3 -3
- package/dist/src/merge/merge-queue-render.js +4 -4
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-requeue.js +62 -19
- package/dist/src/merge/merge-slot.js +41 -10
- package/dist/src/merge/merge-to-main.js +16 -210
- package/dist/src/merge/merge-top.js +2 -1
- package/dist/src/merge/merge-verify.js +9 -6
- package/dist/src/model-check/model-check-gate.js +206 -0
- package/dist/src/model-check/model-check-probe.js +109 -0
- package/dist/src/model-check/model-check.js +101 -0
- package/dist/src/operator/operator-commands.js +10 -26
- package/dist/src/orchestrator/orchestrator-launch.js +6 -1
- package/dist/src/orchestrator/orchestrator-poll.js +50 -43
- package/dist/src/orchestrator/orchestrator-scheduling.js +12 -16
- package/dist/src/orchestrator/orchestrator.js +14 -1
- package/dist/src/paths.js +24 -2
- package/dist/src/pi/pi-event-line.js +27 -0
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-run-result.js +16 -0
- package/dist/src/pi/pi-stream.js +9 -2
- package/dist/src/pi/pi-watchdogs.js +42 -10
- package/dist/src/process/process-group.js +2 -21
- package/dist/src/process/process.js +36 -4
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/prompt/gate-notes.js +10 -4
- package/dist/src/prompt/gate-prompts.js +14 -58
- package/dist/src/prompt/principles.js +13 -3
- package/dist/src/prompt/prompt-followup.js +14 -5
- package/dist/src/redeploy/redeploy-probes.js +53 -18
- package/dist/src/redeploy/redeploy-target.js +100 -0
- package/dist/src/redeploy/redeployer.js +120 -46
- package/dist/src/report/report-main-red.js +2 -2
- package/dist/src/report/report-render.js +2 -2
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/known-flakes.js +4 -2
- package/dist/src/review/post-merge-review-accept.js +2 -1
- package/dist/src/review/post-merge-review.js +53 -41
- package/dist/src/review/review-precheck.js +75 -31
- package/dist/src/review/review.js +13 -22
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +13 -3
- package/dist/src/roles/role-render.js +157 -108
- package/dist/src/roles/role-view.js +271 -81
- package/dist/src/roles/roles.js +1 -1
- package/dist/src/run/ci-parity.js +34 -10
- package/dist/src/run/run-apply.js +0 -15
- package/dist/src/run/run-detail-render.js +2 -2
- package/dist/src/run/run-finalize.js +4 -0
- package/dist/src/run/run-stage.js +6 -6
- package/dist/src/run/run-timing.js +2 -2
- package/dist/src/run/run-usage.js +9 -1
- package/dist/src/run/run-verdict.js +92 -55
- package/dist/src/run/stage-check.js +17 -24
- package/dist/src/runner/runner-leftover.js +9 -26
- package/dist/src/runner/runner-outcome.js +1 -2
- package/dist/src/runner/runner-pi.js +12 -12
- package/dist/src/runner/runner-recovery.js +9 -16
- package/dist/src/runner/runner-revision.js +3 -3
- package/dist/src/runner/runner-run.js +155 -118
- package/dist/src/runner/runner-state-load.js +94 -61
- package/dist/src/runner/runner-state.js +3 -16
- package/dist/src/runner/runner.js +39 -20
- package/dist/src/{orchestrator/scheduling-admit.js → scheduling/admit.js} +2 -2
- package/dist/src/scheduling/backoff.js +14 -9
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +10 -12
- package/dist/src/{orchestrator/scheduling-scope.js → scheduling/scope.js} +15 -74
- package/dist/src/{orchestrator/scheduling-skip.js → scheduling/skip.js} +113 -83
- package/dist/src/status/status-budget.js +3 -8
- package/dist/src/status/status-data.js +46 -41
- package/dist/src/status/status-polls.js +82 -25
- package/dist/src/status/status-role-notes.js +25 -7
- package/dist/src/status/status-roles.js +57 -27
- package/dist/src/team/team-hold.js +7 -5
- package/dist/src/team/team-orchestrator-info.js +104 -21
- package/dist/src/team/team-polls.js +36 -86
- package/dist/src/team/team-state.js +25 -9
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/format.js +20 -2
- package/dist/src/text/phrases.js +66 -10
- package/dist/src/text/suggest.js +15 -2
- package/dist/src/text/text.js +33 -3
- package/dist/src/ui/alert-feeds.js +0 -41
- package/dist/src/ui/badges.js +76 -52
- package/dist/src/ui/gui/gui-client-boot.js +0 -6
- package/dist/src/ui/gui/gui-client-drawer-config.js +11 -20
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-model.js +4 -0
- package/dist/src/ui/gui/gui-client-operator.js +14 -15
- package/dist/src/ui/gui/gui-client-pending.js +10 -3
- package/dist/src/ui/gui/gui-client-report-chart.js +22 -15
- package/dist/src/ui/gui/gui-client-settings.js +5 -1
- package/dist/src/ui/gui/gui-client-team.js +15 -2
- package/dist/src/ui/progress-data.js +54 -4
- package/dist/src/ui/run-progress-model.js +12 -3
- package/dist/src/ui/status-model.js +21 -6
- package/dist/src/ui/status-payload.js +23 -24
- package/dist/src/ui/status-render.js +12 -13
- package/dist/src/ui/team-alert-backlog.js +15 -205
- package/dist/src/ui/team-alert-shared.js +8 -31
- package/dist/src/ui/team-alert-standings.js +96 -0
- package/dist/src/ui/team-alert-status.js +39 -12
- package/dist/src/ui/team-alerts.js +9 -11
- package/dist/src/ui/tui/tui-backlog.js +14 -2
- package/dist/src/ui/tui/tui-editor-interval.js +2 -2
- package/dist/src/ui/tui/tui-editor-role-model.js +5 -1
- package/dist/src/ui/tui/tui-editors.js +5 -4
- package/dist/src/ui/tui/tui-input.js +10 -8
- package/dist/src/ui/tui/tui-pane-activity.js +2 -1
- package/dist/src/ui/tui/tui-pane-backlog.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +46 -44
- package/dist/src/ui/tui/tui-pane-history.js +162 -475
- package/dist/src/ui/tui/tui-pane-pending-selection.js +346 -0
- package/dist/src/ui/tui/tui-pane-pending.js +59 -331
- package/dist/src/ui/tui/tui-pane-queued.js +2 -2
- package/dist/src/ui/tui/tui-pane-session.js +4 -3
- package/dist/src/ui/tui/tui-pane-trail.js +420 -0
- package/dist/src/ui/tui/tui-pane.js +3 -3
- package/dist/src/ui/tui/tui-panes.js +9 -1
- package/dist/src/ui/tui/tui.js +4 -5
- package/dist/src/verdict/fix-claim.js +46 -11
- package/dist/src/verdict/reply-contract.js +5 -8
- package/package.json +9 -7
- package/dist/src/merge/merge-check-failures.js +0 -209
- package/dist/src/merge/merge-stack.js +0 -236
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/cli/{config-commands.js → cli-config-commands.js} +0 -0
- /package/dist/src/cli/{log-commands.js → cli-log-commands.js} +0 -0
- /package/dist/src/cli/{notify-command.js → cli-notify-command.js} +0 -0
- /package/dist/src/cli/{question-commands.js → cli-question-commands.js} +0 -0
|
@@ -53,19 +53,22 @@ export function needsReviewNote(body) {
|
|
|
53
53
|
* which the plan role clears by splitting or replanning (role-catalog.ts `plan` find). False
|
|
54
54
|
* for the bugfix role's wait-for-the-next-failure note ("by bugfix: …"), which parks the
|
|
55
55
|
* entry for selfcheck to lift when the cluster fails again (selfcheck step 4) and which no
|
|
56
|
-
* role can act on until then
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* the
|
|
56
|
+
* role can act on until then, and for the director's user fallback ("by director: …", the
|
|
57
|
+
* director-request handoff's final park) — the director has already reviewed the entry and the
|
|
58
|
+
* park names what the user must decide, so it is not plan-role work either. entryHold returns
|
|
59
|
+
* "needs-review" for every such note (all of them park the entry), so the doctor's `stalled
|
|
60
|
+
* entries` row (checkStalledEntries) — the
|
|
61
|
+
* stalled-with-the-plan-role surface — classifies by this agreed subset, keeping the bugfix
|
|
62
|
+
* parks out of the plan-role blame. Reads the note's text after the NEEDS_REVIEW_PREFIX once,
|
|
63
|
+
* so the row cannot disagree on which notes the plan role clears; the eligibility mark
|
|
62
64
|
* (holdMark) continues to hold every needs-review note, a by-bugfix park included. */
|
|
63
65
|
export function isPlanStalled(entry) {
|
|
64
66
|
const note = needsReviewNote(entry.body);
|
|
65
67
|
if (note === null)
|
|
66
68
|
return false;
|
|
67
|
-
// A "by bugfix:" note is a wait for evidence that selfcheck clears,
|
|
68
|
-
|
|
69
|
+
// A "by bugfix:" note is a wait for evidence that selfcheck clears, and a "by director:"
|
|
70
|
+
// note is the director's reviewed user fallback (INV-B8) — neither is plan-role work.
|
|
71
|
+
return !note.includes("by bugfix") && !note.includes("by director");
|
|
69
72
|
}
|
|
70
73
|
/** `lines` without any line starting with `prefix` (the same line-prefix rule hasNoteLine
|
|
71
74
|
* applies). When a dropped note sat between two blank lines, the blank line after it goes too,
|
|
@@ -279,49 +282,28 @@ export function backlogHolds(root) {
|
|
|
279
282
|
* `tumwater backlog` prints. A surface derives its marks from the one hold decision and the
|
|
280
283
|
* one claim lookup rather than re-walking the entries. An eligible entry nobody is working on
|
|
281
284
|
* renders as the empty string, so a client can append the mark without a separate check.
|
|
282
|
-
* Questions carry no holds. backlogHolds alone still drives the allocator.
|
|
283
|
-
|
|
285
|
+
* Questions carry no holds. backlogHolds alone still drives the allocator. A caller that
|
|
286
|
+
* already resolved the claim lookup (for the same poll's marks AND its own parallel checks,
|
|
287
|
+
* e.g. statusPayload's `working` or the TUI's `runners`) passes it as `working` so the claims
|
|
288
|
+
* walk is done once, not re-run inside both calls — the same shared single read the module's
|
|
289
|
+
* other single-home polls observe. */
|
|
290
|
+
export function backlogMarks(root, working) {
|
|
284
291
|
const holds = backlogHolds(root);
|
|
285
|
-
const
|
|
292
|
+
const inProgress = working ?? backlogInProgress(root);
|
|
286
293
|
const mark = (hold, runner) => holdMark(hold) + inProgressMark(runner ?? null);
|
|
287
294
|
return {
|
|
288
|
-
plans: holds.plans.map((h, i) => mark(h,
|
|
289
|
-
bugs: holds.bugs.map((h, i) => mark(h,
|
|
295
|
+
plans: holds.plans.map((h, i) => mark(h, inProgress.plans[i])),
|
|
296
|
+
bugs: holds.bugs.map((h, i) => mark(h, inProgress.bugs[i])),
|
|
290
297
|
};
|
|
291
298
|
}
|
|
292
299
|
/** The display noun for a backlog-file discriminator — the `"plans" | "bugs"` `file` on
|
|
293
|
-
* RefusedEntry
|
|
300
|
+
* RefusedEntry (and the same discriminator the prompt's named-hold wording
|
|
294
301
|
* and the doctor's Stalled row take): BUGS.md is a "Bug" and PLANS.md a "Plan", and the
|
|
295
302
|
* lowercase form is the `Refused plan|bug:` label and the `tumwater plan|bug --release`
|
|
296
303
|
* command noun. */
|
|
297
304
|
export function entryKindLabel(file) {
|
|
298
305
|
return file === "bugs" ? "Bug" : "Plan";
|
|
299
306
|
}
|
|
300
|
-
/** The planned plans and open bugs parked with the plan role — whose Needs-review hold the plan
|
|
301
|
-
* role clears by splitting or replanning (the feature role's too-large note, the convergence hold,
|
|
302
|
-
* a final-round rejection), in file order: the `stalled-entries` team alert's input, each
|
|
303
|
-
* carrying its backlog file, 0-based index and the hold. Same `backlogHolds(root)` + the
|
|
304
|
-
* planned/`openBugEntries` readers `refusedEntries` uses, so the alert and the doctor's
|
|
305
|
-
* `stalled entries` row classify through the same `entryHold` + `isPlanStalled` and cannot
|
|
306
|
-
* disagree on which holds are role-resolvable. The bugfix role's wait-for-the-next-failure
|
|
307
|
-
* parks ("by bugfix:") are excluded: selfcheck lifts them, not the plan role. A missing
|
|
308
|
-
* file yields `[]`. */
|
|
309
|
-
export function stalledEntries(root) {
|
|
310
|
-
const holds = backlogHolds(root);
|
|
311
|
-
const out = [];
|
|
312
|
-
const entriesOf = (entries, held, file) => {
|
|
313
|
-
entries.forEach((e, i) => {
|
|
314
|
-
const hold = held[i];
|
|
315
|
-
// Only holds the plan role can clear surface as stalled: the bugfix role's
|
|
316
|
-
// wait-for-the-next-failure parks stay out of the plan-role blame.
|
|
317
|
-
if (hold === "needs-review" && isPlanStalled(e))
|
|
318
|
-
out.push({ file, index: i, title: e.title, hold });
|
|
319
|
-
});
|
|
320
|
-
};
|
|
321
|
-
entriesOf(plannedPlanEntries(root), holds.plans, "plans");
|
|
322
|
-
entriesOf(openBugEntries(root), holds.bugs, "bugs");
|
|
323
|
-
return out;
|
|
324
|
-
}
|
|
325
307
|
/** The planned plans, then the open bugs, whose hold is `"refused"`, in file order: the
|
|
326
308
|
* entries that wait on an operator decision (the `refused` team alert's input), each carrying
|
|
327
309
|
* the backlog file and its 0-based position so a surface opening the row can land on it.
|
|
@@ -15,7 +15,7 @@ import { collapseWhitespace, truncate } from "../text/text.js";
|
|
|
15
15
|
import { actionableEntryRanges, groupBlockedSeries, partsLabel, } from "./backlog-index-data.js";
|
|
16
16
|
import { stripEntryStamp } from "./backlog-structure.js";
|
|
17
17
|
import { entryHoldAgainst, holdMark } from "./backlog-eligibility.js";
|
|
18
|
-
import { plannedPartKeys } from "./backlog-parts.js";
|
|
18
|
+
import { entryKey, plannedPartKeys } from "./backlog-parts.js";
|
|
19
19
|
import { plannedPlanEntries } from "./backlog.js";
|
|
20
20
|
/** Max characters an index title keeps before a trailing ellipsis, so one very long BUGS.md
|
|
21
21
|
* heading cannot dominate the block. */
|
|
@@ -110,7 +110,7 @@ export function renderBacklogIndexBlock(root) {
|
|
|
110
110
|
// plan) — BUGS.md and QUESTIONS.md sections keep every entry on its own line.
|
|
111
111
|
const rows = entries.map((e) => {
|
|
112
112
|
const hold = entryHoldAgainst({ title: e.title, body: e.body }, plannedParts);
|
|
113
|
-
const key =
|
|
113
|
+
const key = entryKey(e.title);
|
|
114
114
|
return {
|
|
115
115
|
start: e.start,
|
|
116
116
|
end: e.end,
|
|
@@ -342,9 +342,20 @@ export function entryLags(md, sectionTitle, dateRe, filingRe) {
|
|
|
342
342
|
export function headingMetadata(lines, start) {
|
|
343
343
|
const line = lines[start] ?? "";
|
|
344
344
|
const meta = [line];
|
|
345
|
+
const j = collectHeadingContinuation(lines, start, meta, headingIsWrapped(line));
|
|
346
|
+
return { text: meta.join(" "), next: j };
|
|
347
|
+
}
|
|
348
|
+
/** Whether a `### ` heading line opens an unbalanced parenthetical: the only shape whose
|
|
349
|
+
* metadata runs beyond its first line. */
|
|
350
|
+
function headingIsWrapped(line) {
|
|
345
351
|
// A wrapped heading's first line opens a parenthetical its continuation will close; an
|
|
346
352
|
// unbalanced count is therefore the only shape that has metadata beyond the first line.
|
|
347
|
-
|
|
353
|
+
return (line.match(/\(/g) ?? []).length > (line.match(/\)/g) ?? []).length;
|
|
354
|
+
}
|
|
355
|
+
/** Append a wrapped `### ` heading's continuation lines to `meta`, stopping at the next entry
|
|
356
|
+
* start, the 3-line cap, or a line ending in `)`; returns the first line index not consumed. */
|
|
357
|
+
function collectHeadingContinuation(lines, start, meta, wrapped) {
|
|
358
|
+
const line = lines[start] ?? "";
|
|
348
359
|
let closed = line.endsWith(")");
|
|
349
360
|
let j = start + 1;
|
|
350
361
|
// Continuation is a heading-only concern (wrapped headings); bullets are single-line.
|
|
@@ -356,7 +367,7 @@ export function headingMetadata(lines, start) {
|
|
|
356
367
|
j++;
|
|
357
368
|
closed = next.endsWith(")");
|
|
358
369
|
}
|
|
359
|
-
return
|
|
370
|
+
return j;
|
|
360
371
|
}
|
|
361
372
|
/** A `- ` line's or `### ` heading's trailing parenthetical's inner text, nesting-aware: a
|
|
362
373
|
* backward scan from the text's closing ")" to its matching "(" returns the group's FULL inner
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* output helpers, so every surface can drive it without ending the process.
|
|
8
8
|
*
|
|
9
9
|
* Shared by every read/answer surface: the CLI's `tumwater questions` command body wraps it in
|
|
10
|
-
* src/cli/question-commands.ts (the say/fail/editor layer), while the TUI's answer box
|
|
10
|
+
* src/cli/cli-question-commands.ts (the say/fail/editor layer), while the TUI's answer box
|
|
11
11
|
* (src/ui/tui/tui-keys.ts) and the dashboard's POST route (src/gui/gui-endpoint-commands.ts)
|
|
12
12
|
* import the non-exiting answer core directly. It lives with the rest of the BUGS/PLANS/QUESTIONS
|
|
13
13
|
* markdown (src/backlog/), not under src/cli/, so the non-command surfaces never depend on the
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* section. Each comes as an exiting writer (the CLI) and a non-exiting `try…` core that refuses as
|
|
5
5
|
* data (the TUI and dashboard); entry lookup and the section's file target are
|
|
6
6
|
* backlog-write.ts's. */
|
|
7
|
-
import { backlogTarget, findEntry,
|
|
7
|
+
import { backlogTarget, findEntry, readEntryLines, withLocatedEntry } from "./backlog-write.js";
|
|
8
8
|
import { withoutNeedsReviewNotes, withoutRefusalNotes } from "./backlog-eligibility.js";
|
|
9
9
|
import { writeTextAtomic } from "../files/files.js";
|
|
10
10
|
import { fail } from "../cli/cli-output.js";
|
|
@@ -38,11 +38,7 @@ function liftRefusal(lines, entry) {
|
|
|
38
38
|
* title check, all decided inside the lock. */
|
|
39
39
|
export function tryReleaseEntry(root, kind, n, expectedTitle) {
|
|
40
40
|
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
41
|
-
return
|
|
42
|
-
const located = locateDashboardEntry(root, fileName, sectionTitle, n, expectedTitle);
|
|
43
|
-
if (located.status !== "ok")
|
|
44
|
-
return located;
|
|
45
|
-
const { file, md, entry } = located;
|
|
41
|
+
return withLocatedEntry(root, fileName, sectionTitle, n, expectedTitle, ({ file, md, entry }) => {
|
|
46
42
|
const lines = md.split("\n");
|
|
47
43
|
const lifted = liftRefusal(lines, entry);
|
|
48
44
|
if (lifted === null)
|
|
@@ -104,11 +100,7 @@ function spliceEntryToTop(lines, sectionTitle, entry) {
|
|
|
104
100
|
* title check, all decided inside the lock. */
|
|
105
101
|
export function tryMoveEntryToTop(root, kind, n, expectedTitle) {
|
|
106
102
|
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
107
|
-
return
|
|
108
|
-
const located = locateDashboardEntry(root, fileName, sectionTitle, n, expectedTitle);
|
|
109
|
-
if (located.status !== "ok")
|
|
110
|
-
return located;
|
|
111
|
-
const { file, md, entry } = located;
|
|
103
|
+
return withLocatedEntry(root, fileName, sectionTitle, n, expectedTitle, ({ file, md, entry }) => {
|
|
112
104
|
if (n === 1)
|
|
113
105
|
return { status: "already_first", title: entry.title };
|
|
114
106
|
const lines = md.split("\n");
|
|
@@ -147,7 +147,7 @@ export function readEntryLines(root, fileName, sectionTitle, n) {
|
|
|
147
147
|
* refuse as data without writing; on success returns the `ok` file path, raw markdown and entry
|
|
148
148
|
* for the caller to splice and write its own outcome. A pure lookup, decided inside the caller's
|
|
149
149
|
* lock. */
|
|
150
|
-
|
|
150
|
+
function locateDashboardEntry(root, fileName, sectionTitle, n, expectedTitle) {
|
|
151
151
|
const file = path.join(root, fileName);
|
|
152
152
|
const md = readTextIfExists(file) ?? "";
|
|
153
153
|
const entry = findEntry(md, sectionTitle, n);
|
|
@@ -157,6 +157,22 @@ export function locateDashboardEntry(root, fileName, sectionTitle, n, expectedTi
|
|
|
157
157
|
return { status: "moved", title: entry.title };
|
|
158
158
|
return { status: "ok", file, md, entry };
|
|
159
159
|
}
|
|
160
|
+
/** The shared lock-and-locate prologue of the four non-exiting dashboard writers (tryCancelEntry
|
|
161
|
+
* and tryEditEntry here; tryReleaseEntry and tryMoveEntryToTop in backlog-release.ts): take the
|
|
162
|
+
* cross-process lock, locate the `n`th entry by position and expected title, and refuse as data —
|
|
163
|
+
* returning `located`'s `gone`/`moved` verbatim when the lookup fails — without writing. On a
|
|
164
|
+
* match it calls `splice` with the located file, markdown and entry and returns whatever the
|
|
165
|
+
* splice returns, so each writer keeps its own `ok`-branch outcome; the return is the union of the
|
|
166
|
+
* two refusals and `splice`'s own return, which is exactly each writer's outcome type. The
|
|
167
|
+
* `splice` runs inside the lock, the same window the inline prologue held. */
|
|
168
|
+
export function withLocatedEntry(root, fileName, sectionTitle, n, expectedTitle, splice) {
|
|
169
|
+
return withPrimaryWorktreeLock(root, () => {
|
|
170
|
+
const located = locateDashboardEntry(root, fileName, sectionTitle, n, expectedTitle);
|
|
171
|
+
if (located.status !== "ok")
|
|
172
|
+
return located;
|
|
173
|
+
return splice({ file: located.file, md: located.md, entry: located.entry });
|
|
174
|
+
});
|
|
175
|
+
}
|
|
160
176
|
/** The section-remove entry point, the appendEntry analogue: the same cross-process lock, so a
|
|
161
177
|
* cancel and a merge's working-tree fast-forward cannot each clobber the other's
|
|
162
178
|
* read-modify-write. */
|
|
@@ -189,11 +205,7 @@ function spliceOutEntry(file, lines, entry) {
|
|
|
189
205
|
* `plans`/`bugs` strings, the same `### ` text as ActionableEntryRange.title). */
|
|
190
206
|
export function tryCancelEntry(root, kind, n, expectedTitle) {
|
|
191
207
|
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
192
|
-
return
|
|
193
|
-
const located = locateDashboardEntry(root, fileName, sectionTitle, n, expectedTitle);
|
|
194
|
-
if (located.status !== "ok")
|
|
195
|
-
return located;
|
|
196
|
-
const { file, md, entry } = located;
|
|
208
|
+
return withLocatedEntry(root, fileName, sectionTitle, n, expectedTitle, ({ file, md, entry }) => {
|
|
197
209
|
spliceOutEntry(file, md.split("\n"), entry);
|
|
198
210
|
return { status: "cancelled", file: fileName, title: entry.title };
|
|
199
211
|
});
|
|
@@ -264,11 +276,7 @@ export function tryEditEntry(root, kind, n, expectedTitle, rawTitle, rawBody) {
|
|
|
264
276
|
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
265
277
|
const title = stripEntryStamp(collapseWhitespace(rawTitle));
|
|
266
278
|
const body = rawBody.replace(/\r\n?/g, "\n").trim();
|
|
267
|
-
return
|
|
268
|
-
const located = locateDashboardEntry(root, fileName, sectionTitle, n, expectedTitle);
|
|
269
|
-
if (located.status !== "ok")
|
|
270
|
-
return located;
|
|
271
|
-
const { file, md, entry } = located;
|
|
279
|
+
return withLocatedEntry(root, fileName, sectionTitle, n, expectedTitle, ({ file, md, entry }) => {
|
|
272
280
|
const wrote = spliceInEditedEntry(file, md.split("\n"), entry, title, body);
|
|
273
281
|
return { status: wrote ? "edited" : "unchanged", file: fileName, title };
|
|
274
282
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { cachedTextByStat } from "../files/stat-cache.js";
|
|
3
|
+
import { DAY_MS } from "../text/datetime.js";
|
|
3
4
|
import { entryDates, entryLags, finishedArchiveOf, firstDate, lastDate, parseEntryDetails } from "./backlog-md.js";
|
|
4
5
|
import { CHECK_DECLARATION_MARKER } from "../roles/role-guidance.js";
|
|
5
6
|
/** The stat-cached file readers of the project backlog data shown on both dashboards: planned
|
|
@@ -104,7 +105,7 @@ export function completionDate(title, dateRe) {
|
|
|
104
105
|
/** Whole days between a filing stamp and `now`, floored at 0 so a future-dated entry reads as
|
|
105
106
|
* fresh (0d) rather than negative. Shared by the suffix and the `--list --json` `ageDays` field. */
|
|
106
107
|
export function ageInDays(since, now = Date.now()) {
|
|
107
|
-
return Math.max(0, Math.floor((now - Date.parse(since)) /
|
|
108
|
+
return Math.max(0, Math.floor((now - Date.parse(since)) / DAY_MS));
|
|
108
109
|
}
|
|
109
110
|
/** The ` · since YYYY-MM-DD (Nd)`/`(today)` tail a listing line gains for its entry's filing
|
|
110
111
|
* stamp — the one place the wording and the today/`Nd` choice live, so the plans/bugs list
|
|
@@ -9,10 +9,14 @@ import { treeDiffFiles } from "../git/git-diff.js";
|
|
|
9
9
|
import { isExemptDiff } from "../review/exemptions.js";
|
|
10
10
|
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
11
11
|
import { isJsonObject } from "../files/json-object.js";
|
|
12
|
-
import { baselineVerdictsPath
|
|
12
|
+
import { baselineVerdictsPath } from "../paths.js";
|
|
13
13
|
import { memoSetLru } from "../collections.js";
|
|
14
|
-
/** Team-shared verdict cache, keyed by main SHA.
|
|
15
|
-
*
|
|
14
|
+
/** Team-shared verdict cache, keyed by main SHA. Exported read-only to the one consumer outside
|
|
15
|
+
* this module that decides redeploy's target from it (src/redeploy/redeploy-target.ts's
|
|
16
|
+
* `redeployTarget`); writes stay here — `rememberBaseline`, `noteGreenBaseline` and
|
|
17
|
+
* `noteInheritedGreen` — so the cache's write policy has one home. In-memory only: after a
|
|
18
|
+
* restart the cache is cold and one re-check per red SHA happens — cheap and deterministic,
|
|
19
|
+
* mirroring the budget gate's
|
|
16
20
|
* stateless resume. Entries come from two sources: checkMainBaseline's own runs, and
|
|
17
21
|
* noteGreenBaseline seeding a green verdict for a SHA that just became main
|
|
18
22
|
* (src/merge/merge-to-main.ts: either its in-lock post-rebase re-check passed on exactly that
|
|
@@ -33,7 +37,7 @@ import { memoSetLru } from "../collections.js";
|
|
|
33
37
|
* not opt-in only: a red carries the worktrees that observed it (`redFrom`) and is re-run by the
|
|
34
38
|
* next DIFFERENT worktree to consult it, so a single environmental red can no longer block every
|
|
35
39
|
* role until main moves — see shouldRerunRed. */
|
|
36
|
-
const baselineCache = new Map();
|
|
40
|
+
export const baselineCache = new Map();
|
|
37
41
|
/** Safety cap so the per-process verdict cache can never grow unbounded: it is keyed by every
|
|
38
42
|
* merged code SHA and a red entry carries the failed run's output tail, so a team running for
|
|
39
43
|
* weeks would accumulate one entry per merge forever. Evicting the oldest entry (Map's insertion
|
|
@@ -150,15 +154,10 @@ function shouldRerunRed(cached, wt, reverifyRed) {
|
|
|
150
154
|
const seen = cached.redFrom ?? [];
|
|
151
155
|
return seen.length < 2 && !seen.includes(wt);
|
|
152
156
|
}
|
|
153
|
-
/** Record a green baseline verdict for `sha` WITHOUT running anything. The
|
|
154
|
-
* path (src/merge/merge-to-main.ts's
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* its own in-lock re-check just ran this project's declared check green on that tree, or the
|
|
158
|
-
* rebase was a no-op so the review gate's pre-check (which runs outside the merge lock) had
|
|
159
|
-
* already run green on exactly this tree — and mergeStack with the stacked tip when a
|
|
160
|
-
* scope-`batch` check ran green on exactly it (a skipped check seeds nothing, and neither does a
|
|
161
|
-
* merge_blocked stack or a doc-only re-stack that merged without a check of its own). Seeding
|
|
157
|
+
/** Record a green baseline verdict for `sha` WITHOUT running anything. The caller is the merge
|
|
158
|
+
* path (src/merge/merge-to-main.ts's mergeToMain), called only after its fast-forward SUCCEEDED,
|
|
159
|
+
* with the exact SHA that just became main.
|
|
160
|
+
* Seeding
|
|
162
161
|
* here means that once the merge completes — main now points at this very SHA — the next fresh
|
|
163
162
|
* run's checkMainBaseline is a cache hit instead of re-running the full suite on an
|
|
164
163
|
* already-verified tree: for tumwater itself that saves one redundant `npm test` (~1 min) per
|
|
@@ -171,11 +170,23 @@ function shouldRerunRed(cached, wt, reverifyRed) {
|
|
|
171
170
|
export function noteGreenBaseline(sha) {
|
|
172
171
|
rememberBaseline(sha, { status: "green", sha });
|
|
173
172
|
}
|
|
173
|
+
/** Settle `sha`'s red verdict by a re-run of only its failing tests on the same tree
|
|
174
|
+
* (build-check-rerun.ts; the main watch's confirmation, which replaced a second full run): with
|
|
175
|
+
* none failing again the SHA is promoted green, as the full re-run's green promoted it — every test
|
|
176
|
+
* has now passed on this tree — and otherwise the red narrows to the tests that failed again.
|
|
177
|
+
* Returns the settled verdict, or null when `sha` holds no red to settle. */
|
|
178
|
+
export function settleRedByRerun(sha, failing) {
|
|
179
|
+
const red = baselineCache.get(sha);
|
|
180
|
+
if (red?.status !== "red")
|
|
181
|
+
return null;
|
|
182
|
+
const settled = failing.length === 0 ? { status: "green", sha } : { ...red, failingTests: [...failing] };
|
|
183
|
+
rememberBaseline(sha, settled);
|
|
184
|
+
return settled;
|
|
185
|
+
}
|
|
174
186
|
/** Record an INHERITED green for `sha` (MainBaseline.inheritedFrom): a tree no check ran on, whose
|
|
175
|
-
* only difference from the green-verified `from` is in review-exempt paths.
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* verifyMerge's own noteGreenBaseline calls do. If the fast-forward then fails, the entry is
|
|
187
|
+
* only difference from the green-verified `from` is in review-exempt paths. It seeds the head
|
|
188
|
+
* about to become main, as
|
|
189
|
+
* noteGreenBaseline calls do. If the fast-forward then fails, the entry is
|
|
179
190
|
* still true of that tree and simply never consulted. It never displaces an observed verdict: a
|
|
180
191
|
* check that actually ran on this tree knows more than the carry-over. */
|
|
181
192
|
export function noteInheritedGreen(sha, from) {
|
|
@@ -193,7 +204,7 @@ const INHERIT_WALK_MAX = 64;
|
|
|
193
204
|
* first-parent scans in this module share, spelled the same on purpose so a change to the walk
|
|
194
205
|
* shape (the bound, the `--first-parent` flag) lands once. `inheritedGreen` and `watchedBaseline`
|
|
195
206
|
* walk a commit's ancestors (`${sha}^` / `${tip}^`, the commit itself excluded);
|
|
196
|
-
* `
|
|
207
|
+
* `newestObservedBaseline` walks `tip` itself and down. Never throws: a git failure is an empty
|
|
197
208
|
* list, and every caller already treats that as a no-inheritance / no-verdict answer. */
|
|
198
209
|
async function firstParentWalk(wt, ref, max) {
|
|
199
210
|
return gitLines(await gitTry(wt, "rev-list", "--first-parent", `--max-count=${max}`, ref));
|
|
@@ -261,8 +272,21 @@ export async function watchedBaseline(wt, sha) {
|
|
|
261
272
|
}
|
|
262
273
|
return null;
|
|
263
274
|
}
|
|
264
|
-
/**
|
|
265
|
-
|
|
275
|
+
/** The newest verdict a check OBSERVED on `tip` or its first-parent ancestors — `tip` itself
|
|
276
|
+
* first, looking at most INHERIT_WALK_MAX commits down — WITHOUT running anything; null when
|
|
277
|
+
* there is none. An inherited green is walked past: no check ran on that tree, and a doc edit can
|
|
278
|
+
* break a test that reads the docs (checkMainBaseline's `observedOnly`). The review gate's
|
|
279
|
+
* attribution reads this when the main watch verifies main (main-red.ts's watchedMainVerdict):
|
|
280
|
+
* with `check.mergeCommand` in effect every observed verdict is a full check the watch or
|
|
281
|
+
* another consumer ran. Never throws: a git failure reads as no verdict. */
|
|
282
|
+
export async function newestObservedBaseline(wt, tip) {
|
|
283
|
+
for (const sha of await firstParentWalk(wt, tip, INHERIT_WALK_MAX)) {
|
|
284
|
+
const verdict = baselineCache.get(sha);
|
|
285
|
+
if (verdict && verdict.inheritedFrom === undefined)
|
|
286
|
+
return verdict;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
266
290
|
/** True when the main watch (src/baseline/main-watch.ts) verifies main — the configs where no
|
|
267
291
|
* merge seeds a green verdict (`watchedBaseline`'s doc) and the watch has a check to run: a
|
|
268
292
|
* declared check and a `check.mergeCommand` (or its gate-command fallback) in effect. In this
|
|
@@ -273,56 +297,6 @@ const REDEPLOY_WALK_MAX = 64;
|
|
|
273
297
|
export function watchVerifiesMain(root, config) {
|
|
274
298
|
return detectBuildCheck(root, config) !== null && mergeCommandOf(config) !== undefined;
|
|
275
299
|
}
|
|
276
|
-
/** The newest first-parent commit below `tip`, down to and through `buildSha`, with a green
|
|
277
|
-
* cached verdict — the walk+scan redeployTarget's watched and un-watched branches share. Only
|
|
278
|
-
* their fallback differs: `notOnHistory` when `buildSha` is not on the first-parent walk at all
|
|
279
|
-
* (the caller keeps today's answer, `tip`), `onHistoryNone` when it is but nothing above it is
|
|
280
|
-
* green (watched keeps the build; un-watched keeps `tip`). Never throws: a git failure leaves
|
|
281
|
-
* the walk empty, so the fallback answers. */
|
|
282
|
-
async function newestGreenAboveBuild(wt, tip, buildSha, notOnHistory, onHistoryNone) {
|
|
283
|
-
const walk = await firstParentWalk(wt, tip, REDEPLOY_WALK_MAX);
|
|
284
|
-
const buildAt = walk.indexOf(buildSha);
|
|
285
|
-
if (buildAt === -1)
|
|
286
|
-
return notOnHistory;
|
|
287
|
-
for (const sha of walk.slice(0, buildAt)) {
|
|
288
|
-
if (baselineCache.get(sha)?.status === "green")
|
|
289
|
-
return sha;
|
|
290
|
-
}
|
|
291
|
-
return onHistoryNone;
|
|
292
|
-
}
|
|
293
|
-
/** The commit self-redeploy should run: main's newest first-parent commit with a green full-check
|
|
294
|
-
* verdict in the cache (observed or inherited) that is newer than the running build `buildSha`,
|
|
295
|
-
* looking at most REDEPLOY_WALK_MAX commits down from `tip`.
|
|
296
|
-
*
|
|
297
|
-
* With `watched` false (the default, a project whose main the watch does not verify), a tip the
|
|
298
|
-
* cache has no verdict for is the target as is — the redeployer checks it. A tip whose cached red
|
|
299
|
-
* was observed in the redeployer's mirror worktree (mirrorWorktreePath of `wt`) falls back to the
|
|
300
|
-
* newest older green above the build. Any other red may be another worktree's environmental one,
|
|
301
|
-
* which the redeployer re-verifies; a build that is not a first-parent ancestor of `tip`, or no
|
|
302
|
-
* green, also leaves the tip the target, and the redeployer blocks that commit alone.
|
|
303
|
-
*
|
|
304
|
-
* With `watched` true (watchVerifiesMain), the watch supplies the verdicts and the redeployer only
|
|
305
|
-
* reads them: walk `tip`'s first-parent history down to `buildSha` and return the first commit
|
|
306
|
-
* whose cached verdict is green — a cached red is walked past, never targeted. With no green
|
|
307
|
-
* above the build, target `buildSha` itself, so buildStaleness reads it as not stale and no
|
|
308
|
-
* episode, hold or pre-warm starts until the watch calls a newer commit green. A build that is
|
|
309
|
-
* not a first-parent ancestor of `tip` keeps today's answer (`tip`, which the redeployer
|
|
310
|
-
* checks), as does `watched` false. Every target `watched` picks has a cached green, so
|
|
311
|
-
* mainIsGreen on it is a cache hit and the redeployer starts no suite.
|
|
312
|
-
* The cache holds only full-check verdicts, so a cheaper command's green never makes a commit
|
|
313
|
-
* deployable.
|
|
314
|
-
* Never throws: a git failure leaves the tip as the target. */
|
|
315
|
-
export async function redeployTarget(wt, tip, buildSha, watched = false) {
|
|
316
|
-
if (watched) {
|
|
317
|
-
if (tip === "")
|
|
318
|
-
return tip;
|
|
319
|
-
return newestGreenAboveBuild(wt, tip, buildSha, tip, buildSha);
|
|
320
|
-
}
|
|
321
|
-
const red = baselineCache.get(tip);
|
|
322
|
-
if (tip === "" || red?.status !== "red" || !(red.redFrom ?? []).includes(mirrorWorktreePath(wt)))
|
|
323
|
-
return tip;
|
|
324
|
-
return newestGreenAboveBuild(wt, tip, buildSha, tip, tip);
|
|
325
|
-
}
|
|
326
300
|
/** Verify main's own build/test suite at `wt`'s HEAD — which must be pristine main (the caller
|
|
327
301
|
* is the fresh-run path right after resetWorktreeToMain; a dirty or ahead worktree would
|
|
328
302
|
* measure the wrong thing). Cache hit returns immediately; on miss runs detectBuildCheck +
|
|
@@ -451,6 +425,7 @@ function baselineFromOutcome(sha, wt, outcome) {
|
|
|
451
425
|
script: outcome.script,
|
|
452
426
|
outputTail: outcome.outputTail,
|
|
453
427
|
failingTests: outcome.failingTests ?? [],
|
|
428
|
+
...(outcome.failingTestFiles ? { failingTestFiles: outcome.failingTestFiles } : {}),
|
|
454
429
|
redFrom: priorRedFrom.includes(wt) ? priorRedFrom : [...priorRedFrom, wt],
|
|
455
430
|
};
|
|
456
431
|
}
|
|
@@ -495,8 +470,8 @@ async function runBaselineOnce(sha, wt, config, onRun, sampleSleep, tier) {
|
|
|
495
470
|
return { baseline };
|
|
496
471
|
}
|
|
497
472
|
/** Is main green at the mirror worktree's HEAD? The merge path seeds a green verdict for every
|
|
498
|
-
* merged code SHA (noteGreenBaseline —
|
|
499
|
-
*
|
|
473
|
+
* merged code SHA (noteGreenBaseline — the stack lander's scope-`batch` check on the merged tip,
|
|
474
|
+
* or a no-op gate pre-check, so the common case is a cache hit. A head that only added doc
|
|
500
475
|
* commits on top of such a SHA inherits its green (checkMainBaseline's inheritedGreen). The
|
|
501
476
|
* redeployer's cooldown pre-warm asks this for every new head, and those doc heads were most of the
|
|
502
477
|
* harness's baseline runs (247 of 319 over 2026-10-02..12). Otherwise the project's declared check
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Both take an injected `now` so tests never read the clock. */
|
|
14
14
|
import { readEventsSince } from "../events/event-window.js";
|
|
15
15
|
import { loadQuarantine } from "../review/known-flakes.js";
|
|
16
|
-
import { DAY_MS, clockHm, humanAgo } from "../text/datetime.js";
|
|
16
|
+
import { DAY_MS, HOUR_MS, clockHm, humanAgo } from "../text/datetime.js";
|
|
17
17
|
import { shortSha } from "../text/format.js";
|
|
18
18
|
import { roleLabel } from "../roles/role-labels.js";
|
|
19
19
|
import { plural } from "../text/phrases.js";
|
|
@@ -115,13 +115,17 @@ function redFailingTest(newest) {
|
|
|
115
115
|
return Array.isArray(newest.failingTests) && typeof newest.failingTests[0] === "string" ? newest.failingTests[0] : undefined;
|
|
116
116
|
}
|
|
117
117
|
/** The blame clause naming the merge the main watch blamed, when the newest `culprit` in the
|
|
118
|
-
* window
|
|
119
|
-
function redBlameClause(culprit, newest) {
|
|
118
|
+
* window names the spell's opener or its newest red. Owns the blame clause. */
|
|
119
|
+
function redBlameClause(culprit, startEv, newest) {
|
|
120
120
|
// The blame names the merge the main watch blamed for the red — the same `main_culprit`
|
|
121
121
|
// commit-equals-tip rule the dashboard alert uses (status-polls only publishes a blame whose
|
|
122
122
|
// commit is the derived tip sha), so a red the watch did not blame never borrows another
|
|
123
123
|
// head's culprit. A culprit naming another commit, or none, leaves the phrase as today.
|
|
124
|
-
|
|
124
|
+
// The watch names the commit that OPENED the red (its first red tip since the last green)
|
|
125
|
+
// even when a later tip re-checks red, so the match accepts the spell's opener as well as the
|
|
126
|
+
// newest red tip: the opener is what the watch blames for the ongoing spell, and a
|
|
127
|
+
// pre-fix log that named the newest tip instead still matches its own newest red.
|
|
128
|
+
const shouldBlame = culprit !== undefined && (culprit.commit === newest.sha || culprit.commit === startEv.sha);
|
|
125
129
|
return shouldBlame
|
|
126
130
|
? typeof culprit.runner === "string" && culprit.runner.length > 0
|
|
127
131
|
? `; the main watch blames ${roleLabel(culprit.runner)}'s merge ${shortSha(culprit.commit)}`
|
|
@@ -134,9 +138,9 @@ function redBlameClause(culprit, newest) {
|
|
|
134
138
|
* ignored; a pass (including one naming only quarantined tests, recorded passed) ends the red.
|
|
135
139
|
* The phrase names how long main has had no passing full check, the sha it has been red since,
|
|
136
140
|
* its clock time, the newest red's first failing test when one is named, and — when the newest
|
|
137
|
-
* `main_culprit` in the window
|
|
138
|
-
* blamed (`; the main watch blames <role>'s merge <sha>`, or `; the main watch blames
|
|
139
|
-
* <sha>` when no runner is named), the one rule the dashboard alert and this phrase share.
|
|
141
|
+
* `main_culprit` in the window names the spell's opener or its newest red tip — the merge the
|
|
142
|
+
* main watch blamed (`; the main watch blames <role>'s merge <sha>`, or `; the main watch blames
|
|
143
|
+
* merge <sha>` when no runner is named), the one rule the dashboard alert and this phrase share.
|
|
140
144
|
* Returns null when main is green, or red for RED_MAIN_WARN_MS or less. The per-job work is
|
|
141
145
|
* split into the module-private helpers `scanRedWindow`, `redSpell`, `redSpellDuration`,
|
|
142
146
|
* `redFailingTest` and `redBlameClause`, one per job, so each is found by name. */
|
|
@@ -148,7 +152,7 @@ export function redMainWarning(events, now) {
|
|
|
148
152
|
const duration = redSpellDuration(spell);
|
|
149
153
|
const head = `main has had no passing full check for ${duration}: red since ${shortSha(spell.startEv.sha)} at ${clockHm(spell.startEv.ts)}`;
|
|
150
154
|
const failing = redFailingTest(spell.newest);
|
|
151
|
-
const blame = redBlameClause(window.culprit, spell.newest);
|
|
155
|
+
const blame = redBlameClause(window.culprit, spell.startEv, spell.newest);
|
|
152
156
|
return failing ? `${head} — ${squash(failing, 120)}${blame}` : `${head}${blame}`;
|
|
153
157
|
}
|
|
154
158
|
/** G3 over `entries` (the loadQuarantine list): warns when the list holds more than
|
|
@@ -193,5 +197,5 @@ function redDurationPhrase(ms) {
|
|
|
193
197
|
const minutes = Math.round(ms / 60_000);
|
|
194
198
|
if (minutes < 24 * 60)
|
|
195
199
|
return `${minutes} min`;
|
|
196
|
-
return `${Math.round(ms /
|
|
200
|
+
return `${Math.round(ms / HOUR_MS)} h`;
|
|
197
201
|
}
|