tumwater 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
import { andMore, readBacklogChecked, FIX_CLAIMS_CHECKED,
|
|
1
|
+
import { andMore, quoteHeading, readBacklogChecked, FIX_CLAIMS_CHECKED, REFUSAL_NOTE_MAX, STALE_QUESTION_DAYS, STALE_BUG_DAYS } from "./doctor-backlog.js";
|
|
2
2
|
import { truncate } from "../text/text.js";
|
|
3
|
+
import { plural, agree } from "../text/phrases.js";
|
|
3
4
|
import { ageInDays, filingDate, fixedBugEntries, openBugEntries, openQuestionEntries, plannedPlanEntries } from "../backlog/backlog.js";
|
|
4
|
-
import { stripEntryStamp } from "../backlog/backlog-structure.js";
|
|
5
5
|
import { questionFilingDate } from "../backlog/question-answer.js";
|
|
6
|
-
import {
|
|
6
|
+
import { needsReviewNote, refusalNote } from "../backlog/backlog-eligibility.js";
|
|
7
7
|
import { plannedPartKeys, plansWaitingOn, prerequisitesMerged } from "../backlog/backlog-parts.js";
|
|
8
8
|
import { lastDate } from "../backlog/backlog-md.js";
|
|
9
9
|
import { formatTimestamp } from "../text/datetime.js";
|
|
10
10
|
import { stringList } from "../files/json-object.js";
|
|
11
|
-
import { byDaysThenIndex } from "../collections.js";
|
|
12
11
|
import { readEventsSince } from "../events/event-window.js";
|
|
13
12
|
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
14
13
|
/** The doctor's open-bug and question checks, split out of doctor-backlog.ts: questions that
|
|
15
14
|
* have waited too long for an answer (checkStaleQuestions), bugs left unfixed (checkStaleBugs),
|
|
16
|
-
*
|
|
17
|
-
* failureMentions helpers), fixed records whose quoted test regressed (checkRegressedFixes), and
|
|
15
|
+
* fixed records whose quoted test regressed (checkRegressedFixes), and
|
|
18
16
|
* entries parked by a Refused note (checkRefusedEntries). They import the shared helpers
|
|
19
17
|
* (readBacklogChecked, andMore) from doctor-backlog.ts, plus the age/trim bounds, and
|
|
20
18
|
* all warn instead of failing: a stale or
|
|
@@ -44,21 +42,22 @@ export function checkStaleQuestions(root) {
|
|
|
44
42
|
const [first, ...rest] = stale;
|
|
45
43
|
if (!first)
|
|
46
44
|
return okOutcome("no open question has waited more than 7 days for an answer");
|
|
47
|
-
const more = andMore(rest, (q) =>
|
|
48
|
-
return warnOutcome(`QUESTIONS.md question
|
|
45
|
+
const more = andMore(rest, (q) => quoteHeading(q.title));
|
|
46
|
+
return warnOutcome(`QUESTIONS.md question ${quoteHeading(first.title)} has waited ${first.days} d for an answer${more} — ` +
|
|
49
47
|
`a question that waits too long for a human answer is worth your attention`);
|
|
50
48
|
}
|
|
51
|
-
/** Stale bugs — unrefused open BUGS.md bugs whose `(found by … YYYY-MM-DD)` or
|
|
52
|
-
* stamp is at least 3 days old. The bugfix role fixes a bug within hours, so one
|
|
53
|
-
* no role is fixing it. Positions are the 1-based `bug --list` numbers; a refused
|
|
54
|
-
* (checkRefusedEntries names it), a
|
|
55
|
-
*
|
|
49
|
+
/** Stale bugs — unrefused, unheld open BUGS.md bugs whose `(found by … YYYY-MM-DD)` or
|
|
50
|
+
* `(reported …)` stamp is at least 3 days old. The bugfix role fixes a bug within hours, so one
|
|
51
|
+
* that sits means no role is fixing it. Positions are the 1-based `bug --list` numbers; a refused
|
|
52
|
+
* bug is skipped (checkRefusedEntries names it), a bug parked by a Needs-review note is skipped
|
|
53
|
+
* (the note names what would make it fail, so it is not unfixed), and so is an undated heading.
|
|
54
|
+
* A warn, never a fail. */
|
|
56
55
|
export function checkStaleBugs(root) {
|
|
57
56
|
const doc = readBacklogChecked(root, "BUGS.md");
|
|
58
57
|
if (typeof doc !== "string")
|
|
59
58
|
return doc;
|
|
60
59
|
const stale = openBugEntries(root).flatMap((entry, index) => {
|
|
61
|
-
if (refusalNote(entry.body) !== null ||
|
|
60
|
+
if (refusalNote(entry.body) !== null || needsReviewNote(entry.body) !== null)
|
|
62
61
|
return [];
|
|
63
62
|
const stamp = filingDate(entry.title);
|
|
64
63
|
if (stamp === null)
|
|
@@ -69,8 +68,8 @@ export function checkStaleBugs(root) {
|
|
|
69
68
|
const [first, ...rest] = stale;
|
|
70
69
|
if (!first)
|
|
71
70
|
return okOutcome("no open bug has waited more than 3 days for a fix");
|
|
72
|
-
const more = andMore(rest, (b) => `#${b.position}
|
|
73
|
-
return warnOutcome(`BUGS.md bug #${first.position}
|
|
71
|
+
const more = andMore(rest, (b) => `#${b.position} ${quoteHeading(b.title)}`);
|
|
72
|
+
return warnOutcome(`BUGS.md bug #${first.position} ${quoteHeading(first.title)} has waited ${first.days} d for a fix${more} — ` +
|
|
74
73
|
`no role is fixing it: check that the bugfix role is enabled and not failing, or retract it with \`tumwater bug --cancel ${first.position}\``);
|
|
75
74
|
}
|
|
76
75
|
/** The first span between straight double quotes in a bug heading that is at least 12
|
|
@@ -85,10 +84,6 @@ export function quotedTestName(title) {
|
|
|
85
84
|
function dayAfterMs(date) {
|
|
86
85
|
return new Date(Number(date.slice(0, 4)), Number(date.slice(5, 7)) - 1, Number(date.slice(8, 10)) + 1).getTime();
|
|
87
86
|
}
|
|
88
|
-
/** The first `YYYY-MM-DD` in a Waiting for the next failure note's text, or null. */
|
|
89
|
-
export function waitingNoteDate(note) {
|
|
90
|
-
return /\d{4}-\d{2}-\d{2}/.exec(note)?.[0] ?? null;
|
|
91
|
-
}
|
|
92
87
|
/** True when `text` is a check-failure report that names `name` as a test that failed — the
|
|
93
88
|
* doctors' recurrence/regression evidence. The recurrence scans must not count any line that
|
|
94
89
|
* merely *contains* the quoted test name as the flake's return, or a warning that only quotes
|
|
@@ -127,8 +122,8 @@ export function failureMentions(events, name, sinceMs) {
|
|
|
127
122
|
}
|
|
128
123
|
/** The newest failure report at or after `since` naming `name` — `failureMentions`'s last
|
|
129
124
|
* mention — or undefined when none. The single home of the "did this flake come back after
|
|
130
|
-
* its boundary?" step, shared by the
|
|
131
|
-
*
|
|
125
|
+
* its boundary?" step, shared by the regressed-fix match (matchedRegressedFixes), so both
|
|
126
|
+
* keep the same last-mention rule. */
|
|
132
127
|
function lastFailureMention(events, name, sinceMs) {
|
|
133
128
|
const mentions = failureMentions(events, name, sinceMs);
|
|
134
129
|
return mentions[mentions.length - 1];
|
|
@@ -136,104 +131,11 @@ function lastFailureMention(events, name, sinceMs) {
|
|
|
136
131
|
/** The read-events window (ms) that reaches the oldest of `sinces` — `Date.now()` minus the
|
|
137
132
|
* oldest timestamp, floored at 0 — so a recurrence check reads events past every candidate's
|
|
138
133
|
* `since`. The single home of the "oldest of a set of since-times" step, shared by
|
|
139
|
-
*
|
|
134
|
+
* checkRegressedFixes, so those sites cannot drift on the clamp or the
|
|
140
135
|
* time base. */
|
|
141
136
|
function sinceWindowMs(sinces) {
|
|
142
137
|
return Math.max(0, Date.now() - Math.min(...sinces));
|
|
143
138
|
}
|
|
144
|
-
/** Open BUGS.md bugs held by a **Waiting for the next failure …** note, in file order: the
|
|
145
|
-
* doctor's `checkWaitingBugs` selection and anchor, factored out so the always-on stale-wait
|
|
146
|
-
* feed (staleWaitingBugRows) reads the same held set and cannot drift from the doctor's rule.
|
|
147
|
-
* A bug that is also Refused is left to checkRefusedEntries. `since` is the day-after the
|
|
148
|
-
* note's date — the recurrence scan's boundary; `date` the note's own `YYYY-MM-DD` or null. */
|
|
149
|
-
function heldWaitingBugs(root) {
|
|
150
|
-
return openBugEntries(root).flatMap((entry, index) => {
|
|
151
|
-
const note = waitingNote(entry.body);
|
|
152
|
-
if (note === null || refusalNote(entry.body) !== null)
|
|
153
|
-
return [];
|
|
154
|
-
const name = quotedTestName(entry.title);
|
|
155
|
-
const date = waitingNoteDate(note);
|
|
156
|
-
const since = name !== null && date !== null ? dayAfterMs(date) : null;
|
|
157
|
-
return [{ index, position: index + 1, title: entry.title, note, name, since, date }];
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
/** Waiting bugs — open BUGS.md bugs held by a **Waiting for the next failure …** note, which
|
|
161
|
-
* parks a bug until it fails again. Positions are the 1-based `bug --list` numbers; a bug that
|
|
162
|
-
* is also Refused is left to checkRefusedEntries. Ok when the hold is undisturbed — doctor
|
|
163
|
-
* then only names the bugs and how to release one — and a warn when a held bug's quoted test
|
|
164
|
-
* has failed in a gate or merge check since the day after its note (the note carries only a
|
|
165
|
-
* date, so a same-day failure cannot be told from the one it was written about). A warn is
|
|
166
|
-
* never a fail, and doctor writes nothing to BUGS.md. */
|
|
167
|
-
export function checkWaitingBugs(root) {
|
|
168
|
-
const doc = readBacklogChecked(root, "BUGS.md");
|
|
169
|
-
if (typeof doc !== "string")
|
|
170
|
-
return doc;
|
|
171
|
-
const held = heldWaitingBugs(root);
|
|
172
|
-
const [first, ...rest] = held;
|
|
173
|
-
if (!first)
|
|
174
|
-
return okOutcome("no open bug is waiting for its next failure");
|
|
175
|
-
const sinces = held.flatMap((b) => (b.since !== null && !Number.isNaN(b.since) ? [b.since] : []));
|
|
176
|
-
if (sinces.length > 0) {
|
|
177
|
-
const { events } = readEventsSince(root, sinceWindowMs(sinces));
|
|
178
|
-
const recurred = held.flatMap((b) => {
|
|
179
|
-
if (b.name === null || b.since === null)
|
|
180
|
-
return [];
|
|
181
|
-
const newest = lastFailureMention(events, b.name, b.since);
|
|
182
|
-
return newest ? [{ ...b, newest }] : [];
|
|
183
|
-
});
|
|
184
|
-
const [again, ...others] = recurred;
|
|
185
|
-
if (again) {
|
|
186
|
-
const more = andMore(others, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
187
|
-
return warnOutcome(`BUGS.md bug #${again.position} "${truncate(again.title, FIX_CLAIM_HEADING_MAX)}" failed again since its Waiting for the next failure note ` +
|
|
188
|
-
`(${formatTimestamp(again.newest.ts)}: ${truncate(again.newest.text, REFUSAL_NOTE_MAX)})${more} — ` +
|
|
189
|
-
`record the occurrence under the entry and release it with \`tumwater bug --release ${again.position}\``);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
/* A Waiting hold has no expiry of its own, so a bug whose flake never comes back parks
|
|
193
|
-
* indefinitely; a hold that has outlived STALE_WAIT_DAYS without a recurrence is escalated
|
|
194
|
-
* from the ok below to a warn so the operator is nudged to release or re-hold it. */
|
|
195
|
-
const lapsed = held.flatMap((b) => {
|
|
196
|
-
/* An undated note (no YYYY-MM-DD in its text) has no hold-start marker, and checkStaleBugs
|
|
197
|
-
* skips any bug carrying a waiting note, so without a fallback it would park indefinitely
|
|
198
|
-
* too; the filing stamp anchors it — the hold cannot be younger than the bug it dates from. */
|
|
199
|
-
const anchor = b.date ?? filingDate(b.title);
|
|
200
|
-
if (anchor === null)
|
|
201
|
-
return [];
|
|
202
|
-
const days = ageInDays(anchor);
|
|
203
|
-
return days >= STALE_WAIT_DAYS ? [{ ...b, days }] : [];
|
|
204
|
-
});
|
|
205
|
-
const [firstLapsed, ...restLapsed] = lapsed;
|
|
206
|
-
if (firstLapsed) {
|
|
207
|
-
const more = andMore(restLapsed, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
208
|
-
return warnOutcome(`BUGS.md bug #${firstLapsed.position} "${truncate(firstLapsed.title, FIX_CLAIM_HEADING_MAX)}" has waited ${firstLapsed.days} d for its next failure without one ` +
|
|
209
|
-
`— its flake likely never comes back, so it parks indefinitely; release it with \`tumwater bug --release ${firstLapsed.position}\`${more}`);
|
|
210
|
-
}
|
|
211
|
-
const more = andMore(rest, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
212
|
-
return okOutcome(`BUGS.md bug #${first.position} "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" is held until it fails again ` +
|
|
213
|
-
`(${truncate(first.note, REFUSAL_NOTE_MAX)})${more} — selfcheck lifts the note when it records a new occurrence; ` +
|
|
214
|
-
`release it now with \`tumwater bug --release ${first.position}\``);
|
|
215
|
-
}
|
|
216
|
-
/** The always-on rows behind the dashboard/TUI/`tumwater status` waiting-bug alert: open BUGS.md
|
|
217
|
-
* bugs held by a **Waiting for the next failure …** note whose anchor (the note's `YYYY-MM-DD`,
|
|
218
|
-
* else the heading's filing stamp) is at least STALE_WAIT_DAYS old. Shares the doctor's
|
|
219
|
-
* STALE_WAIT_DAYS threshold and the heldWaitingBugs selection/anchor, so the alert and the
|
|
220
|
-
* doctor's `waiting bugs` row cannot drift on the rule; an undated note-and-heading is skipped.
|
|
221
|
-
* The returned objects carry the 0-based index (drawer address `bugs:<index>`), the 1-based
|
|
222
|
-
* position (`bug --release <position>`), the stamp-stripped title, and the whole-day age.
|
|
223
|
-
* Returned most-days-waited-first, ties in file order, like staleOpenBugs. This deliberately
|
|
224
|
-
* omits `checkWaitingBugs`'s recurrence scan, because that scan reads events back to the oldest
|
|
225
|
-
* hold and is too costly on the status poll path; the doctor still reports a recurrence first. */
|
|
226
|
-
export function staleWaitingBugRows(root, now = Date.now()) {
|
|
227
|
-
return heldWaitingBugs(root)
|
|
228
|
-
.flatMap((b) => {
|
|
229
|
-
const anchor = b.date ?? filingDate(b.title);
|
|
230
|
-
if (anchor === null)
|
|
231
|
-
return [];
|
|
232
|
-
const days = ageInDays(anchor, now);
|
|
233
|
-
return days >= STALE_WAIT_DAYS ? [{ index: b.index, position: b.position, title: stripEntryStamp(b.title), days }] : [];
|
|
234
|
-
})
|
|
235
|
-
.sort(byDaysThenIndex);
|
|
236
|
-
}
|
|
237
139
|
/** The regression candidates for the `regressed fixes` feed: a newest-FIX_CLAIMS_CHECKED BUGS.md
|
|
238
140
|
* Fixed record whose heading quotes a test name and carries a `YYYY-MM-DD` date, unless an Open
|
|
239
141
|
* bug already quotes the same test (which means the regression is filed). Shared by the doctor's
|
|
@@ -268,8 +170,8 @@ export function regressedFixesPhrase(regressed) {
|
|
|
268
170
|
const [first, ...rest] = regressed;
|
|
269
171
|
if (!first)
|
|
270
172
|
return null;
|
|
271
|
-
const more = andMore(rest, (c) =>
|
|
272
|
-
return `BUGS.md Fixed record
|
|
173
|
+
const more = andMore(rest, (c) => quoteHeading(c.title));
|
|
174
|
+
return `BUGS.md Fixed record ${quoteHeading(first.title)} quotes a test that failed again after its ${first.date} fix ` +
|
|
273
175
|
`(${formatTimestamp(first.newest.ts)}: ${truncate(first.newest.text, REFUSAL_NOTE_MAX)})${more} — ` +
|
|
274
176
|
"file it as a new bug with `tumwater bug`, quoting the test name and citing the Fixed record";
|
|
275
177
|
}
|
|
@@ -314,14 +216,14 @@ export function checkRefusedEntries(root) {
|
|
|
314
216
|
const [first, ...rest] = held;
|
|
315
217
|
if (!first)
|
|
316
218
|
return okOutcome("no planned plan or open bug is held by a Refused note");
|
|
317
|
-
const more = andMore(rest, (e) => `${e.file} ${e.command} #${e.position}
|
|
219
|
+
const more = andMore(rest, (e) => `${e.file} ${e.command} #${e.position} ${quoteHeading(e.title)}` +
|
|
318
220
|
(e.mergeDone.length > 0 ? " [prerequisites no longer planned]" : "") +
|
|
319
|
-
(e.waiting > 0 ? ` [${e.waiting
|
|
221
|
+
(e.waiting > 0 ? ` [${plural(e.waiting, "part waits on it", "parts wait on it")}]` : ""));
|
|
320
222
|
const stale = first.mergeDone.length > 0
|
|
321
|
-
? `; its prerequisite
|
|
223
|
+
? `; its ${agree(first.mergeDone.length, "prerequisite", "prerequisites")} ${first.mergeDone.map((r) => `${r.series} ${r.part}/${r.of}`).join(", ")} ${agree(first.mergeDone.length, "is", "are")} no longer planned, so the refusal may be stale`
|
|
322
224
|
: "";
|
|
323
225
|
const waiting = first.waiting > 0
|
|
324
|
-
? `; ${first.waiting
|
|
226
|
+
? `; ${plural(first.waiting, "planned part waits on it", "planned parts wait on it")}, directly or through an earlier part`
|
|
325
227
|
: "";
|
|
326
228
|
return warnOutcome(`${first.file} ${first.command} #${first.position} "${first.title}" is held by a Refused note ` +
|
|
327
229
|
`(${truncate(first.note, REFUSAL_NOTE_MAX)})${stale}${waiting}${more} — no role takes a refused entry until it is edited: ` +
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { andMore, readDoc, readBacklogChecked, FIX_CLAIMS_CHECKED,
|
|
1
|
+
import { andMore, quoteHeading, readDoc, readBacklogChecked, FIX_CLAIMS_CHECKED, REFUSAL_NOTE_MAX } from "./doctor-backlog.js";
|
|
2
2
|
import { cachedByStat } from "../files/stat-cache.js";
|
|
3
3
|
import { VALIDATION_GAP_TAGS } from "../roles/role-guidance.js";
|
|
4
4
|
import path from "node:path";
|
|
@@ -9,9 +9,8 @@ import { planDependencyProblems } from "../backlog/backlog-parts.js";
|
|
|
9
9
|
import { BACKLOG_FILES, FINISHED_ARCHIVES, validationGapTag } from "../backlog/backlog-md.js";
|
|
10
10
|
import { statOrNull } from "../files/files.js";
|
|
11
11
|
import { duplicateHeadings, strandedPlanEntriesFor } from "../backlog/backlog-structure.js";
|
|
12
|
-
import { liveFinishedHistory } from "../backlog/backlog-archive.js";
|
|
12
|
+
import { liveFinishedHistory, liveFinishedRowList } from "../backlog/backlog-archive.js";
|
|
13
13
|
import { fixSymbols, missingSymbolNames, cachedSourceHaystack, unbackedSymbols } from "../verdict/fix-claim.js";
|
|
14
|
-
import { plural } from "../text/phrases.js";
|
|
15
14
|
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
16
15
|
/** The doctor's PLANS.md-structure and fix-claim checks, split out of doctor-backlog.ts: the
|
|
17
16
|
* checks that re-verify BUGS.md's newest Fixed records against the tree (checkFixClaims,
|
|
@@ -45,7 +44,7 @@ export function checkFixClaims(root) {
|
|
|
45
44
|
const { heading, missing } = first;
|
|
46
45
|
const names = missingSymbolNames(missing);
|
|
47
46
|
// Every other suspect is still named, shortened: a doctor check is one line.
|
|
48
|
-
const more = andMore(rest, (p) =>
|
|
47
|
+
const more = andMore(rest, (p) => quoteHeading(p.heading), "record(s)");
|
|
49
48
|
return warnOutcome(`BUGS.md records "${heading}" as Fixed, but none of the symbols its Fix paragraph names ` +
|
|
50
49
|
`exist on this tree: ${names}${more} — land the fix or keep the bug Open / refresh a stale record`);
|
|
51
50
|
}
|
|
@@ -148,14 +147,14 @@ export function checkValidationGaps(root) {
|
|
|
148
147
|
if (!first) {
|
|
149
148
|
if (unpinned.length > 0) {
|
|
150
149
|
const up = unpinned[0];
|
|
151
|
-
const more = andMore(unpinned.slice(1), (s) =>
|
|
150
|
+
const more = andMore(unpinned.slice(1), (s) => quoteHeading(s.title), "record(s)");
|
|
152
151
|
return warnOutcome(`BUGS.md record "${up.title}" is an \`unclear-invariant\` fix but names no ` +
|
|
153
152
|
`docs/invariants.md section — record the reconstructed guarantee there (a "Pinned by" line) in the ` +
|
|
154
153
|
`same commit so the next bug in that area does not reconstruct it again${more}`);
|
|
155
154
|
}
|
|
156
155
|
return okOutcome(`newest ${validationGapRecords(root).length} Fixed record(s) carry a known Validation gap tag`);
|
|
157
156
|
}
|
|
158
|
-
const more = andMore(rest, (s) =>
|
|
157
|
+
const more = andMore(rest, (s) => quoteHeading(s.title), "record(s)");
|
|
159
158
|
const what = first.kind === "missing"
|
|
160
159
|
? `BUGS.md record "${first.title}" has no **Validation gap:** line`
|
|
161
160
|
: `BUGS.md record "${first.title}" names unrecognized Validation gap tag "${first.tag}"`;
|
|
@@ -183,7 +182,7 @@ export function checkStrandedPlans(root) {
|
|
|
183
182
|
const [first, ...rest] = stranded;
|
|
184
183
|
if (!first)
|
|
185
184
|
return okOutcome(clean);
|
|
186
|
-
const more = andMore(rest, (e) =>
|
|
185
|
+
const more = andMore(rest, (e) => quoteHeading(e.title));
|
|
187
186
|
const file = first.file ?? "PLANS.md";
|
|
188
187
|
return warnOutcome(`${file} has a plan stranded under ## ${first.section}: "${first.title}"${more} — ` +
|
|
189
188
|
`move it under ## ${first.section === "Done" ? "Planned" : "Done"}${file === "PLANS.md" ? "" : " in PLANS.md"} ` +
|
|
@@ -199,8 +198,7 @@ export function checkLiveFinished(root) {
|
|
|
199
198
|
const live = liveFinishedHistory(root);
|
|
200
199
|
if (live.length === 0)
|
|
201
200
|
return okOutcome("PLANS.md and BUGS.md hold no finished entries — they are archived");
|
|
202
|
-
|
|
203
|
-
return warnOutcome(`${parts.join("; ")} — every run that reads the file pays for them; ` +
|
|
201
|
+
return warnOutcome(`${liveFinishedRowList(live)} — every run that reads the file pays for them; ` +
|
|
204
202
|
"`tumwater archive` moves them to the archive files and commits them (a starting team does too)");
|
|
205
203
|
}
|
|
206
204
|
/** The always-on `backlog-headings` alert polls this on every observer surface and every
|
|
@@ -245,8 +243,8 @@ function scanBacklogHeadings(root) {
|
|
|
245
243
|
* always-on alert so the poll path never re-reads an unchanged backlog or archive file. */
|
|
246
244
|
const backlogHeadingFileCache = new Map();
|
|
247
245
|
/** The duplicated `## ` headings already on main, exported so the always-on `backlog-headings`
|
|
248
|
-
* team alert (src/ui/team-
|
|
249
|
-
* warns about. */
|
|
246
|
+
* team alert (src/ui/team-alert-backlog.ts) renders the same set the doctor's `backlog
|
|
247
|
+
* headings` row warns about. */
|
|
250
248
|
export function duplicatedBacklogHeadings(root) {
|
|
251
249
|
return scanBacklogHeadings(root).duplicates;
|
|
252
250
|
}
|
|
@@ -258,9 +256,10 @@ export function duplicatedBacklogHeadings(root) {
|
|
|
258
256
|
* stat-keyed cache entry can serve it. */
|
|
259
257
|
const liveFinishedCache = new Map();
|
|
260
258
|
/** The finished-history rows already on main, exported so the always-on `backlog-archive` team
|
|
261
|
-
* alert (src/ui/team-
|
|
262
|
-
* about — stat-gated so a steady-state poll pays four stats, not a full re-parse.
|
|
263
|
-
* is the stat identity (`dev:ino:mtimeMs:size`, `absent` when the stat is
|
|
259
|
+
* alert (src/ui/team-alert-backlog.ts) renders the same set the doctor's `backlog archive`
|
|
260
|
+
* row warns about — stat-gated so a steady-state poll pays four stats, not a full re-parse.
|
|
261
|
+
* The cache key is the stat identity (`dev:ino:mtimeMs:size`, `absent` when the stat is
|
|
262
|
+
* null) of every
|
|
264
263
|
* FINISHED_ARCHIVES live file and archive under `root`, so any write that changes finished
|
|
265
264
|
* history or the archive refreshes the rows on the next call without a process restart. Returns
|
|
266
265
|
* a copy on a hit; on a miss it recomputes via liveFinishedHistory, and returns `[]` (uncached)
|
|
@@ -310,24 +309,24 @@ export function checkBacklogHeadings(root) {
|
|
|
310
309
|
`later entries are invisible; keep one heading per name (the gate blocks only a merge ` +
|
|
311
310
|
`that adds another copy — remove the duplicate in a deliberate edit)`);
|
|
312
311
|
}
|
|
313
|
-
/** Stalled entries — plans or bugs held by a **Needs review …**
|
|
312
|
+
/** Stalled entries — plans or bugs held by a **Needs review …** note,
|
|
314
313
|
* parked until the plan role splits or replans them, plus the held-for-operator entries
|
|
315
314
|
* parked until the operator releases them. No role takes a held entry (the claim gate refuses
|
|
316
315
|
* one) and nothing clears the note on its own, so the state is visible only as the
|
|
317
|
-
* `[needs review]
|
|
316
|
+
* `[needs review]` marks in the backlog listings; this names them so an
|
|
318
317
|
* operator gets a single-line health signal from `tumwater doctor`. Reads the tree at `root`
|
|
319
318
|
* — the primary checkout IS main's tree — like checkRefusedEntries, which is also where
|
|
320
319
|
* refusal notes (a different hold) are surfaced. entryHold (src/backlog/backlog-eligibility.ts)
|
|
321
320
|
* already classifies the notes, so this check adds no parsing: only the entries whose hold is
|
|
322
|
-
* exactly needs-review
|
|
321
|
+
* exactly needs-review or held-for-operator are collected, from both PLANS.md
|
|
323
322
|
* and BUGS.md. A warn, never a fail — the checkRefusedEntries precedent: a parked entry is
|
|
324
|
-
* operator signal, not a broken environment; the plan role resolves the first
|
|
323
|
+
* operator signal, not a broken environment; the plan role resolves the first itself, and
|
|
325
324
|
* a held-for-operator entry is the operator's to release (`tumwater plan|bug --release`). */
|
|
326
325
|
export function checkStalledEntries(root) {
|
|
327
326
|
const planned = plannedPlanEntries(root);
|
|
328
327
|
const collect = (entry, file) => {
|
|
329
328
|
const hold = entryHold(entry, planned);
|
|
330
|
-
return hold === "needs-review" || hold === "
|
|
329
|
+
return hold === "needs-review" || hold === "held-for-operator" // vocabulary-ok: the held-for-operator hold's key
|
|
331
330
|
? { file, entry }
|
|
332
331
|
: null;
|
|
333
332
|
};
|
|
@@ -337,16 +336,16 @@ export function checkStalledEntries(root) {
|
|
|
337
336
|
].filter((x) => x !== null);
|
|
338
337
|
const [first, ...rest] = stalled;
|
|
339
338
|
if (!first)
|
|
340
|
-
return okOutcome("no planned plan or open bug is held by a Needs-review
|
|
341
|
-
const more = andMore(rest, (s) =>
|
|
339
|
+
return okOutcome("no planned plan or open bug is held by a Needs-review or held-for-operator note"); // vocabulary-ok: names the held-for-operator hold
|
|
340
|
+
const more = andMore(rest, (s) => quoteHeading(s.entry.title));
|
|
342
341
|
const kind = entryKindLabel(first.file); // "Bug" | "Plan"
|
|
343
342
|
const head = `${first.file === "bugs" ? "BUGS.md" : "PLANS.md"} ${kind.toLowerCase()}`;
|
|
344
343
|
const hold = entryHold(first.entry, planned);
|
|
345
|
-
const note = (first.entry.body.split(/\n/).find((line) => line.trimStart().startsWith("**Needs review ")
|
|
344
|
+
const note = (first.entry.body.split(/\n/).find((line) => line.trimStart().startsWith("**Needs review ")) ?? "").replace(/^\s*\*\*/, "").replace("**", "");
|
|
346
345
|
if (hold === "held-for-operator") { // vocabulary-ok: the held-for-operator hold's key
|
|
347
|
-
return warnOutcome(`${head}
|
|
346
|
+
return warnOutcome(`${head} ${quoteHeading(first.entry.title)} is held for the operator ${heldCount(first.entry.body)} times${more} — release it with tumwater ${kind.toLowerCase()} --release`); // vocabulary-ok: a held-for-operator entry is the operator's to release
|
|
348
347
|
}
|
|
349
|
-
return warnOutcome(`${head}
|
|
348
|
+
return warnOutcome(`${head} ${quoteHeading(first.entry.title)} is held by a Needs review note ` +
|
|
350
349
|
`(${truncate(note, REFUSAL_NOTE_MAX)})${more} — the plan role resolves it by splitting or replanning`);
|
|
351
350
|
}
|
|
352
351
|
/** Plan dependencies — mistakes in the planned plans' `requires … landed` clauses that the claim
|
|
@@ -361,7 +360,7 @@ export function planDependencyProblem(root) {
|
|
|
361
360
|
const [firstDangling, ...restDangling] = dangling;
|
|
362
361
|
const [firstUnparsed, ...restUnparsed] = unparsed;
|
|
363
362
|
const [firstMisworded, ...restMisworded] = misworded;
|
|
364
|
-
const quote =
|
|
363
|
+
const quote = quoteHeading;
|
|
365
364
|
const others = (parts) => parts.some(([n]) => n > 0) ? ` (also ${parts.map(([n, noun]) => `${n} ${noun}`).join(", ")})` : "";
|
|
366
365
|
const danglingMsg = (d) => `PLANS.md plan ${quote(d.title)} requires ${d.ref}, which no PLANS.md heading carries — the hold is void, ` +
|
|
367
366
|
"so the plan is claimable now; fix the series name or part number";
|
|
@@ -37,9 +37,9 @@ import { okOutcome, warnOutcome } from "./outcome.js";
|
|
|
37
37
|
* and pre-convention Fixed entries predate the validation-gap line — so a whole-section scan
|
|
38
38
|
* would warn forever on records nobody should rewrite. */
|
|
39
39
|
export const FIX_CLAIMS_CHECKED = 10;
|
|
40
|
-
/** A
|
|
41
|
-
*
|
|
42
|
-
|
|
40
|
+
/** A cited record's heading is trimmed to this many characters in a doctor warn — enough to
|
|
41
|
+
* find it back in its file; quoteHeading applies it to every citation. */
|
|
42
|
+
const FIX_CLAIM_HEADING_MAX = 60;
|
|
43
43
|
/** A refused entry's note is trimmed to this many characters in the refusals warn. */
|
|
44
44
|
export const REFUSAL_NOTE_MAX = 120;
|
|
45
45
|
/** The whole days an open question may wait before the stale-questions check warns about it:
|
|
@@ -50,13 +50,6 @@ export const STALE_QUESTION_DAYS = 7;
|
|
|
50
50
|
* opinionated default with no knob, like STALE_QUESTION_DAYS. Three days is long enough that a
|
|
51
51
|
* bug filed late one evening and fixed the next day never warns. */
|
|
52
52
|
export const STALE_BUG_DAYS = 3;
|
|
53
|
-
/** The whole days a bug may stay held by a Waiting for the next failure note before the
|
|
54
|
-
* waiting-bugs check warns that its flake has not come back. A note parks a bug until it fails
|
|
55
|
-
* again, but the failure may never come, so a hold has to lapse or the entry parks indefinitely;
|
|
56
|
-
* one week without a recurrence is long enough that a recurring flake would almost surely have
|
|
57
|
-
* shown up, and short enough that a bug which never returns stops squatting in the backlog as
|
|
58
|
-
* an ok. An opinionated default with no knob, like STALE_BUG_DAYS. */
|
|
59
|
-
export const STALE_WAIT_DAYS = 7;
|
|
60
53
|
/** The `(and N more …)` suffix three one-line doctor details append after naming their first
|
|
61
54
|
* suspect (fix claims, stranded plans, duplicated headings): the rest stay visible but
|
|
62
55
|
* shortened, since a doctor check renders one line. `render` makes each extra item's short
|
|
@@ -69,6 +62,12 @@ export function andMore(rest, render, noun = "") {
|
|
|
69
62
|
? ` (and ${rest.length} more${noun ? ` ${noun}` : ""}: ${rest.map(render).join(", ")})`
|
|
70
63
|
: "";
|
|
71
64
|
}
|
|
65
|
+
/** Quote a heading/summary in a doctor sentence, truncated to FIX_CLAIM_HEADING_MAX so a check
|
|
66
|
+
* stays one line: `"…"`. The doctor's citations all quote-then-truncate the same way; this is
|
|
67
|
+
* the one home for that shape. */
|
|
68
|
+
export function quoteHeading(title) {
|
|
69
|
+
return `"${truncate(title, FIX_CLAIM_HEADING_MAX)}"`;
|
|
70
|
+
}
|
|
72
71
|
/** Read one backlog file for a check, or the failure message explaining why it could not be
|
|
73
72
|
* read (text.ts's errorMessage). Callers shape the failure themselves — a returned warn for a
|
|
74
73
|
* single-file check, an "(unreadable)" list entry for the multi-file heading sweep. */
|
|
@@ -94,7 +93,7 @@ export function readDocChecked(p, file, missingDetail) {
|
|
|
94
93
|
}
|
|
95
94
|
/** The standard single-file check's read prologue for a backlog document at `root`: the
|
|
96
95
|
* readDocChecked call every per-file doctor check (checkFixClaims, checkValidationGaps,
|
|
97
|
-
* checkStrandedPlans, checkStaleQuestions, and the stale
|
|
96
|
+
* checkStrandedPlans, checkStaleQuestions, and the stale bug checks) repeats, with the
|
|
98
97
|
* `no <file> — nothing to verify` missing-detail derived from the file name so the file triple
|
|
99
98
|
* (path, display name, message) and the message template cannot drift across the seven call
|
|
100
99
|
* sites. Returns the document, or the outcome the caller should return as-is. */
|
|
@@ -105,11 +104,9 @@ export function readBacklogChecked(root, file) {
|
|
|
105
104
|
* warns: 6 hours, fixed. A verify plus a merge completes in minutes (plans/merge-queue.md), so a
|
|
106
105
|
* full six-hour silence while the team is healthy — not on a hold, which doctor already reports
|
|
107
106
|
* separately — is never a merely busy pipeline: it reads as a drain that has stalled. The one
|
|
108
|
-
* opinionated default, like STALE_QUESTION_DAYS, so no knob
|
|
109
|
-
* alert (src/ui/status-payload.ts staleMerges) reuses this same window through
|
|
110
|
-
* staleQueuedMergeRows, so the doctor's row and the alert cannot drift on the threshold (the
|
|
107
|
+
* opinionated default, like STALE_QUESTION_DAYS, so no knob (the dead-alert feed is gone; the
|
|
111
108
|
* STALE_PROMPT_MS/STALE_BUG_DAYS precedent). */
|
|
112
|
-
|
|
109
|
+
const MERGE_QUEUE_STALE_MS = 6 * HOUR_MS;
|
|
113
110
|
/** The window of the merge-wait phrase on the stale merge-queue row. */
|
|
114
111
|
const MERGE_WAIT_WINDOW_MS = DAY_MS;
|
|
115
112
|
/** Stale queued merges — a change that has sat in the durable merge queue too long without starting
|
|
@@ -141,19 +138,18 @@ export function checkStaleQueuedMerges(root, now) {
|
|
|
141
138
|
const [first, ...rest] = stale;
|
|
142
139
|
if (!first)
|
|
143
140
|
return withWait(okOutcome(`no merge-queue entry has waited more than ${humanSeconds(MERGE_QUEUE_STALE_MS / 1000)} to start`));
|
|
144
|
-
const more = andMore(rest, (e) => `${e.role} —
|
|
145
|
-
return withWait(warnOutcome(`merge-queue entry of ${roleLabel(first.role)} —
|
|
141
|
+
const more = andMore(rest, (e) => `${e.role} — ${quoteHeading(e.summary)}`);
|
|
142
|
+
return withWait(warnOutcome(`merge-queue entry of ${roleLabel(first.role)} — ${quoteHeading(first.summary)} — has waited ` +
|
|
146
143
|
`${first.age} to start${more}; a queued change the pipeline neither approves nor flags reads as healthy, so the merge queue has stopped moving`));
|
|
147
144
|
}
|
|
148
|
-
/** The one traversal
|
|
149
|
-
* (checkStaleQueuedMerges)
|
|
150
|
-
* (src/ui/status-payload.ts staleMerges): every queued merge whose role is not in the in-flight
|
|
145
|
+
/** The one traversal for the doctor's `stale merge-queue entries` row
|
|
146
|
+
* (checkStaleQueuedMerges): every queued merge whose role is not in the in-flight
|
|
151
147
|
* marker and whose wait strictly exceeds MERGE_QUEUE_STALE_MS (6 h), in queue-file order (oldest
|
|
152
148
|
* first). The in-flight set comes from readMergeMarker via mergeChanges (an absent or
|
|
153
149
|
* never-throwing unreadable marker contributes the empty set) — a slow review is not a stalled
|
|
154
|
-
* drain. Each row carries { role, summary, waitedMs }
|
|
155
|
-
*
|
|
156
|
-
|
|
150
|
+
* drain. Each row carries { role, summary, waitedMs }, which the row renders with its age
|
|
151
|
+
* phrase, so the rule and its threshold live in one place and cannot drift. */
|
|
152
|
+
function staleQueuedMergeRows(root, now) {
|
|
157
153
|
const marker = readMergeMarker(root);
|
|
158
154
|
const inFlight = new Set(marker ? mergeChanges(marker).map((c) => c.role) : []);
|
|
159
155
|
return queuedMerges(root)
|
|
@@ -161,9 +157,8 @@ export function staleQueuedMergeRows(root, now) {
|
|
|
161
157
|
.map((e) => ({ role: e.role, summary: e.summary, waitedMs: now - e.enqueuedAt }));
|
|
162
158
|
}
|
|
163
159
|
/** The merge-queue files `readMergeQueue` skips — torn, foreign, or vanished mid-listing — as
|
|
164
|
-
* checkout-relative paths.
|
|
165
|
-
* the
|
|
166
|
-
* the two surfaces select the same files and name the same paths and cannot drift. */
|
|
160
|
+
* checkout-relative paths. The one traversal behind the doctor's `checkMergeQueueFiles` row, so
|
|
161
|
+
* the row names the same paths the reader actually skips and cannot drift. */
|
|
167
162
|
export function skippedMergeQueueFiles(root) {
|
|
168
163
|
return unreadableMergeQueueFiles(root).map((f) => path.relative(root, f));
|
|
169
164
|
}
|
|
@@ -197,9 +192,8 @@ export const POST_REVIEW_AT_RISK_FAILURES = POST_REVIEW_FAILURE_LIMIT - 1;
|
|
|
197
192
|
* is not waiting on a verdict and must not read as one failed run from being dropped.
|
|
198
193
|
* A review that has already landed its fix (`fixed` set) is left out too — it is a settled
|
|
199
194
|
* record naming both shas, not a verdict-less entry the next run could drop.
|
|
200
|
-
* One traversal
|
|
201
|
-
*
|
|
202
|
-
* two surfaces select the same entries and cannot drift. Reads .tumwater/state/post-review.json
|
|
195
|
+
* One traversal used by the doctor's `post-merge reviews` row (checkPostReviews), so the row's
|
|
196
|
+
* selection lives in one place. Reads .tumwater/state/post-review.json
|
|
203
197
|
* through readPostReviews, which degrades a missing or torn file to the well-formed subset or
|
|
204
198
|
* `[]` and never throws. */
|
|
205
199
|
export function atRiskPostReviews(root) {
|
|
@@ -209,22 +203,22 @@ export function atRiskPostReviews(root) {
|
|
|
209
203
|
}
|
|
210
204
|
/** The doctor's one sentence naming the post-merge reviews one failed run from being dropped
|
|
211
205
|
* (the checkStaleQueuedMerges phrase precedent): the first in full — role, short commit, summary
|
|
212
|
-
* and the failures count — the rest folded through andMore,
|
|
213
|
-
*
|
|
206
|
+
* and the failures count — the rest folded through andMore, the one sentence the row shows, so
|
|
207
|
+
* the wording lives in one place. null for an empty list. */
|
|
214
208
|
export function atRiskPostReviewsPhrase(rows) {
|
|
215
209
|
const [first, ...rest] = rows;
|
|
216
210
|
if (!first)
|
|
217
211
|
return null;
|
|
218
212
|
const more = andMore(rest, (r) => `${roleLabel(r.role)} ${shortSha(r.commit)}`);
|
|
219
|
-
return `the post-merge review of ${roleLabel(first.role)} ${shortSha(first.commit)} —
|
|
213
|
+
return `the post-merge review of ${roleLabel(first.role)} ${shortSha(first.commit)} — ${quoteHeading(first.summary)} — has ended without a verdict ${first.failures} times${more}; ` +
|
|
220
214
|
`one more failed run drops it and the commit stays on main unreviewed — fix the reviewer backend or accept the commit unreviewed deliberately`;
|
|
221
215
|
}
|
|
222
216
|
/** A post-merge review one failed run from being dropped (src/review/post-merge-review.ts, the
|
|
223
217
|
* drain drops an entry at POST_REVIEW_FAILURE_LIMIT failed runs) — the last moment an operator
|
|
224
218
|
* can fix the reviewer backend or deliberately accept the commit unreviewed before the drain
|
|
225
219
|
* drops it from the queue and it stays on main unreviewed. Reads .tumwater/state/post-review.json
|
|
226
|
-
* through the shared atRiskPostReviews traversal the
|
|
227
|
-
*
|
|
220
|
+
* through the shared atRiskPostReviews traversal, so the selection and wording live in one
|
|
221
|
+
* place. A warn,
|
|
228
222
|
* never a fail — the checkStaleQueuedMerges precedent: a failing reviewer is operator signal,
|
|
229
223
|
* not a broken environment; readPostReviews never throws (a missing or torn file degrades to the
|
|
230
224
|
* well-formed subset or `[]`), so the check needs no read-error arm. */
|
|
@@ -236,16 +230,14 @@ export function checkPostReviews(root) {
|
|
|
236
230
|
/** How long a queued prompt may wait for its role to dequeue it before the stale-prompt check
|
|
237
231
|
* warns: a day, fixed. An opinionated default with no knob, like MERGE_QUEUE_STALE_MS: a day is
|
|
238
232
|
* long enough that a healthy role's next run would have dequeued the prompt, and short enough
|
|
239
|
-
* that a paused or disabled role's stranded prompt shows up. The
|
|
240
|
-
*
|
|
241
|
-
|
|
242
|
-
export const STALE_PROMPT_MS = DAY_MS;
|
|
233
|
+
* that a paused or disabled role's stranded prompt shows up. The stale-prompt check reads this
|
|
234
|
+
* same threshold, so it cannot drift — the STALE_BUG_DAYS precedent. */
|
|
235
|
+
const STALE_PROMPT_MS = DAY_MS;
|
|
243
236
|
/** The queued inbox prompts the doctor's stale-prompt rule calls stale — deliverable now (a
|
|
244
237
|
* `--at` prompt still counting down is never stale) and queued more than STALE_PROMPT_MS,
|
|
245
238
|
* oldest-first, `queuedAtMs` narrowed to a number. One traversal shared by the doctor's
|
|
246
|
-
* `checkStalePrompts` check
|
|
247
|
-
|
|
248
|
-
export function staleQueuedPromptRows(root, now) {
|
|
239
|
+
* `checkStalePrompts` check, so the rule lives in one place at one threshold. */
|
|
240
|
+
function staleQueuedPromptRows(root, now) {
|
|
249
241
|
return queuedListRows(root, null, listWideScope(root))
|
|
250
242
|
.filter((p) => p.queuedAtMs !== null && deliverableAt(p.notBeforeMs, now) && now - p.queuedAtMs > STALE_PROMPT_MS)
|
|
251
243
|
.sort((a, b) => a.queuedAtMs - b.queuedAtMs)
|
|
@@ -258,7 +250,7 @@ export function staleQueuedPromptRows(root, now) {
|
|
|
258
250
|
* views (`tumwater prompt --list`, the dashboard Inbox tab, the TUI Inbox pane) show its age —
|
|
259
251
|
* and only when the operator looks. This names the oldest due-but-undelivered prompt, its role,
|
|
260
252
|
* and the deliver-or-drop commands. Reads the queue through the shared `staleQueuedPromptRows`
|
|
261
|
-
* traversal
|
|
253
|
+
* traversal (no new reader in src/inbox/), keeping only
|
|
262
254
|
* rows that are deliverable now — a `--at 7d` prompt still counting down is never stale — and
|
|
263
255
|
* stamped more than STALE_PROMPT_MS ago. A warn, never a fail — the checkStaleQueuedMerges
|
|
264
256
|
* precedent: a stranded prompt is operator signal, not a broken environment. */
|
|
@@ -12,6 +12,7 @@ export const DOCTOR_CHECK_NAMES = [
|
|
|
12
12
|
"init",
|
|
13
13
|
"config key names",
|
|
14
14
|
"brief",
|
|
15
|
+
"principles",
|
|
15
16
|
"bootstrap",
|
|
16
17
|
"pause",
|
|
17
18
|
"quiet hours",
|
|
@@ -28,7 +29,6 @@ export const DOCTOR_CHECK_NAMES = [
|
|
|
28
29
|
"over-cap model",
|
|
29
30
|
"fallback chain",
|
|
30
31
|
"models for each tier",
|
|
31
|
-
"image input",
|
|
32
32
|
"claude auth",
|
|
33
33
|
"claude api key",
|
|
34
34
|
"claude version",
|
|
@@ -59,6 +59,7 @@ export const DOCTOR_CHECK_NAMES = [
|
|
|
59
59
|
"check",
|
|
60
60
|
"ci parity",
|
|
61
61
|
"qa coverage",
|
|
62
|
+
"role notes",
|
|
62
63
|
"root install",
|
|
63
64
|
"flaky checks",
|
|
64
65
|
"post-merge churn",
|
|
@@ -72,11 +73,12 @@ export const DOCTOR_CHECK_NAMES = [
|
|
|
72
73
|
"plan dependencies",
|
|
73
74
|
"stale questions",
|
|
74
75
|
"stale bugs",
|
|
75
|
-
"waiting bugs",
|
|
76
76
|
"regressed fixes",
|
|
77
77
|
"stale merge-queue entries",
|
|
78
78
|
"merge-queue files",
|
|
79
79
|
"post-merge reviews",
|
|
80
|
+
"main diff rules",
|
|
81
|
+
"dropped post-merge reviews",
|
|
80
82
|
"stale prompts",
|
|
81
83
|
"uncommitted backlog",
|
|
82
84
|
"update",
|