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
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
* CLI command package for domain logic.
|
|
15
15
|
*/
|
|
16
16
|
import path from "node:path";
|
|
17
|
-
import { fencedHeadingTitle, fenceTracker, nextSectionHeading } from "./backlog-md.js";
|
|
17
|
+
import { fencedHeadingTitle, fenceTracker, firstDate, nextSectionHeading } from "./backlog-md.js";
|
|
18
18
|
import { answeredQuestionEntries, entryMatches, openQuestionEntries, ageInDays, ageSuffix } from "./backlog.js";
|
|
19
19
|
import { normalizeEntryBody } from "./backlog-write.js";
|
|
20
20
|
import { collapseWhitespace, firstTrimmedLineStartingWith, trimLeadingBlankLines, trimTrailingBlankLines, truncate } from "../text/text.js";
|
|
21
21
|
import { readTextOrNull, writeTextAtomic } from "../files/files.js";
|
|
22
22
|
import { formatDate } from "../text/datetime.js";
|
|
23
23
|
import { numberedList } from "../text/markdown.js";
|
|
24
|
+
import { topAndHidden } from "../collections.js";
|
|
24
25
|
import { withPrimaryWorktreeLock } from "../concurrency/lock.js";
|
|
25
26
|
/** Cap on a question's one-line body preview, so one very long first line cannot dominate
|
|
26
27
|
* the `questions` list. */
|
|
@@ -42,10 +43,11 @@ function firstBodySuffix(body) {
|
|
|
42
43
|
/** The open question's filing stamp, `YYYY-MM-DD`, read from the heading's `(asked by <who>
|
|
43
44
|
* YYYY-MM-DD)` clause — the first `YYYY-MM-DD` after an `asked by` word in the title text — or
|
|
44
45
|
* null when the heading carries no such clause. Taken from the title text only, not the body:
|
|
45
|
-
* the stamp is the entry's own, and the body may name other dates.
|
|
46
|
+
* the stamp is the entry's own, and the body may name other dates. Routed through
|
|
47
|
+
* backlog-md.ts's firstDate - the one first-match mechanism filingDate (backlog.ts) uses too -
|
|
48
|
+
* with the `asked by` verb in place of the plans/bugs stamp verbs. */
|
|
46
49
|
export function questionFilingDate(title) {
|
|
47
|
-
|
|
48
|
-
return m === null ? null : m[1];
|
|
50
|
+
return firstDate(title, /(?:asked by)\b[^)]*?\b(\d{4}-\d{2}-\d{2})/);
|
|
49
51
|
}
|
|
50
52
|
/** The role id named in the heading's `(asked by <role> …)` clause — the first word after
|
|
51
53
|
* `asked by` — or null when the heading has no such clause. Taken from the title only, like
|
|
@@ -133,7 +135,8 @@ function answerPreview(body) {
|
|
|
133
135
|
function answeredQuestionsNewestFirst(root, count, pattern) {
|
|
134
136
|
const numbered = answeredQuestionEntries(root).reverse().map((e, i) => ({ ...e, position: i + 1 }));
|
|
135
137
|
const all = pattern === undefined ? numbered : numbered.filter((e) => entryMatches(e, pattern));
|
|
136
|
-
|
|
138
|
+
const { visible: entries, hidden: older } = topAndHidden(all, count);
|
|
139
|
+
return { entries, older };
|
|
137
140
|
}
|
|
138
141
|
/** `tumwater questions --answered`: lines numbered by `--show <n> --answered` position, newest
|
|
139
142
|
* first — a numbering separate from the `## Open` positions `questions answer <n>` takes. */
|
|
@@ -271,40 +274,56 @@ function answerQuestionLocked(root, n, rawDecision, expectedTitle, keepLines) {
|
|
|
271
274
|
if (expectedTitle !== undefined && block.title !== expectedTitle)
|
|
272
275
|
return { status: "moved", title: block.title };
|
|
273
276
|
const moved = lines.slice(block.start, block.end);
|
|
274
|
-
|
|
277
|
+
const rebuilt = cutOpenBlock(lines, scanned, block);
|
|
278
|
+
const text = appendAnsweredEntry(rebuilt, moved, decision, scanned.answeredIdx !== -1);
|
|
279
|
+
writeTextAtomic(file, text);
|
|
280
|
+
return { status: "answered", title: block.title, decision };
|
|
281
|
+
}
|
|
282
|
+
/** Cut `block` out of `## Open`, returning the rebuilt file text (as lines). When the section
|
|
283
|
+
* is left with no content at all, restores the `_None yet._` placeholder the skeleton carries,
|
|
284
|
+
* so an empty outbox still reads as an intentionally empty section. */
|
|
285
|
+
function cutOpenBlock(lines, scan, block) {
|
|
275
286
|
// Cut the block out of ## Open: everything before it, everything from its end on. When the
|
|
276
287
|
// section is left with no content at all, restore the `_None yet._` placeholder the skeleton
|
|
277
288
|
// carries, so an empty outbox still reads as an intentionally empty section.
|
|
278
289
|
// Emptiness is judged against the Open section only (openIdx+1 .. openEnd): with an
|
|
279
290
|
// intermediate section between Open and Answered, judging against answeredIdx would keep
|
|
280
291
|
// the placeholder from returning and, worse, the empty rebuild would drop that section.
|
|
281
|
-
const openContent = lines.slice(
|
|
282
|
-
const remaining = openContent.filter((_, i) => i < block.start -
|
|
292
|
+
const openContent = lines.slice(scan.openIdx + 1, scan.openEnd);
|
|
293
|
+
const remaining = openContent.filter((_, i) => i < block.start - scan.openIdx - 1 || i >= block.end - scan.openIdx - 1);
|
|
283
294
|
const openEmpty = remaining.every((l) => l.trim() === "");
|
|
284
295
|
const rebuilt = [];
|
|
285
296
|
if (openEmpty) {
|
|
286
|
-
rebuilt.push(...lines.slice(0,
|
|
297
|
+
rebuilt.push(...lines.slice(0, scan.openIdx + 1), "", "_None yet._",
|
|
287
298
|
// Resume at the section that followed Open (openEnd, not answeredIdx — an intermediate
|
|
288
299
|
// section between the two must survive) with the blank separator line the dropped Open
|
|
289
300
|
// content used to carry, so `_None yet._` never reads as a paragraph glued to a heading.
|
|
290
|
-
"", ...lines.slice(
|
|
301
|
+
"", ...lines.slice(scan.openEnd));
|
|
291
302
|
}
|
|
292
303
|
else {
|
|
293
304
|
rebuilt.push(...lines.slice(0, block.start), ...lines.slice(block.end));
|
|
294
305
|
}
|
|
306
|
+
return rebuilt;
|
|
307
|
+
}
|
|
308
|
+
/** Append the moved block at the end of `## Answered` (growing the section when the file has
|
|
309
|
+
* none, stripping a real `_None yet._` placeholder from it), returning the full text to write.
|
|
310
|
+
* `hasAnswered` is whether the file already held a `## Answered` section. */
|
|
311
|
+
function appendAnsweredEntry(rebuilt, moved, decision, hasAnswered) {
|
|
312
|
+
// The moved block's trailing blank lines are dropped, so the appended entry never reads
|
|
313
|
+
// as a spill of blank lines before the dated answer stamp.
|
|
314
|
+
trimTrailingBlankLines(moved);
|
|
295
315
|
// Append the block at the end of ## Answered: before the next `## ` heading when one
|
|
296
316
|
// follows, at the file's end otherwise — the section is last in the documented skeleton.
|
|
297
317
|
const stamp = `**Answered ${formatDate(new Date())} by the user:**`;
|
|
298
318
|
const stamped = decision.includes("\n") ? [stamp, "", ...decision.split("\n")] : [`${stamp} ${decision}`];
|
|
299
319
|
const answerLines = moved.concat("", ...stamped, "");
|
|
300
|
-
if (
|
|
320
|
+
if (!hasAnswered) {
|
|
301
321
|
// No ## Answered section: grow the file's end with the skeleton section plus the block.
|
|
302
322
|
let out = rebuilt.join("\n");
|
|
303
323
|
if (out !== "" && !out.endsWith("\n"))
|
|
304
324
|
out += "\n";
|
|
305
325
|
out += `\n## Answered\n\n${answerLines.join("\n")}`;
|
|
306
|
-
|
|
307
|
-
return { status: "answered", title: block.title, decision };
|
|
326
|
+
return out;
|
|
308
327
|
}
|
|
309
328
|
const fenced2 = fenceTracker();
|
|
310
329
|
let answeredAt = -1;
|
|
@@ -340,6 +359,5 @@ function answerQuestionLocked(root, n, rawDecision, expectedTitle, keepLines) {
|
|
|
340
359
|
trimTrailingBlankLines(head);
|
|
341
360
|
trimLeadingBlankLines(tail);
|
|
342
361
|
const result = head.concat("", answerLines, tail);
|
|
343
|
-
|
|
344
|
-
return { status: "answered", title: block.title, decision };
|
|
362
|
+
return result.join("\n");
|
|
345
363
|
}
|
|
@@ -2,7 +2,7 @@ import { runBuildCheck, } from "../build/build-check.js";
|
|
|
2
2
|
import { SLEEP_SPAN_TOLERANCE_MS, runWithCleanRetry } from "../build/build-check-events.js";
|
|
3
3
|
import { sampleSleepClock } from "../build/host-sleep.js";
|
|
4
4
|
import { CHECK_TIER, withCheckPermit } from "../concurrency/check-permit.js";
|
|
5
|
-
import { detectBuildCheck } from "../build/build-check-detect.js";
|
|
5
|
+
import { detectBuildCheck, mergeCommandOf } from "../build/build-check-detect.js";
|
|
6
6
|
import { gitLines, refSha } from "../git/git.js";
|
|
7
7
|
import { gitTry } from "../git/git-run.js";
|
|
8
8
|
import { treeDiffFiles } from "../git/git-diff.js";
|
|
@@ -110,7 +110,7 @@ export function persistBaselineVerdicts(root) {
|
|
|
110
110
|
greensFile = file;
|
|
111
111
|
}
|
|
112
112
|
/** In-flight dedup: concurrent runs on the same not-yet-cached SHA (a fresh main move wakes
|
|
113
|
-
* every
|
|
113
|
+
* every role's verifier at once) share one check run instead of racing N npm invocations. Keyed by
|
|
114
114
|
* SHA for ordinary checks; a re-verification adds its worktree, because joining another
|
|
115
115
|
* worktree's run would observe the wrong environment — the one thing it exists to re-test. */
|
|
116
116
|
const baselineInFlight = new Map();
|
|
@@ -174,6 +174,15 @@ export function noteInheritedGreen(sha, from) {
|
|
|
174
174
|
* verdict anyway (every code merge seeds one), so this bound only caps the rev-list on a cold
|
|
175
175
|
* cache. */
|
|
176
176
|
const INHERIT_WALK_MAX = 64;
|
|
177
|
+
/** The body of a `git rev-list --first-parent --max-count=<max> <ref>` read — the walk the three
|
|
178
|
+
* first-parent scans in this module share, spelled the same on purpose so a change to the walk
|
|
179
|
+
* shape (the bound, the `--first-parent` flag) lands once. `inheritedGreen` and `watchedBaseline`
|
|
180
|
+
* walk a commit's ancestors (`${sha}^` / `${tip}^`, the commit itself excluded);
|
|
181
|
+
* `newestGreenAboveBuild` walks `tip` itself and down. Never throws: a git failure is an empty
|
|
182
|
+
* list, and every caller already treats that as a no-inheritance / no-verdict answer. */
|
|
183
|
+
async function firstParentWalk(wt, ref, max) {
|
|
184
|
+
return gitLines(await gitTry(wt, "rev-list", "--first-parent", `--max-count=${max}`, ref));
|
|
185
|
+
}
|
|
177
186
|
/** A green for `sha` carried over from its nearest verdict-bearing first-parent ancestor, or null
|
|
178
187
|
* when there is none to carry. The nearest ancestor with a cached verdict must be GREEN, and the
|
|
179
188
|
* tree diff from it to `sha` must touch only `exemptPaths` (config.review.exemptPaths, the same
|
|
@@ -185,7 +194,7 @@ const INHERIT_WALK_MAX = 64;
|
|
|
185
194
|
* past. An uncached ancestor is walked through, since the single diff from the green covers it.
|
|
186
195
|
* Never throws: any git failure means no inheritance, and the caller runs the suite as before. */
|
|
187
196
|
async function inheritedGreen(wt, sha, exemptPaths) {
|
|
188
|
-
const ancestors =
|
|
197
|
+
const ancestors = await firstParentWalk(wt, `${sha}^`, INHERIT_WALK_MAX);
|
|
189
198
|
for (const ancestor of ancestors) {
|
|
190
199
|
const verdict = baselineCache.get(ancestor);
|
|
191
200
|
if (verdict === undefined)
|
|
@@ -229,7 +238,7 @@ export async function watchedBaseline(wt, sha) {
|
|
|
229
238
|
const own = baselineCache.get(tip);
|
|
230
239
|
if (own)
|
|
231
240
|
return own;
|
|
232
|
-
const ancestors =
|
|
241
|
+
const ancestors = await firstParentWalk(wt, `${tip}^`, INHERIT_WALK_MAX);
|
|
233
242
|
for (const ancestor of ancestors) {
|
|
234
243
|
const verdict = baselineCache.get(ancestor);
|
|
235
244
|
if (verdict)
|
|
@@ -239,49 +248,85 @@ export async function watchedBaseline(wt, sha) {
|
|
|
239
248
|
}
|
|
240
249
|
/** How many first-parent commits below main's tip redeployTarget looks at. */
|
|
241
250
|
const REDEPLOY_WALK_MAX = 64;
|
|
242
|
-
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
251
|
+
/** True when the main watch (src/baseline/main-watch.ts) verifies main — the configs where no
|
|
252
|
+
* merge seeds a green verdict (`watchedBaseline`'s doc) and the watch has a check to run: a
|
|
253
|
+
* declared check and a `check.mergeCommand` (or its gate-command fallback) in effect. In this
|
|
254
|
+
* mode the redeployer reads the watch's verdicts and starts no full check of its own
|
|
255
|
+
* (redeployTarget's `watched`). False when the project declares no check, or when a blank
|
|
256
|
+
* `check.mergeCommand` keeps the full check in the merge lock. Never throws: detection resolves
|
|
257
|
+
* to null, which is also false. */
|
|
258
|
+
export function watchVerifiesMain(root, config) {
|
|
259
|
+
return detectBuildCheck(root, config) !== null && mergeCommandOf(config) !== undefined;
|
|
260
|
+
}
|
|
261
|
+
/** The newest first-parent commit below `tip`, down to and through `buildSha`, with a green
|
|
262
|
+
* cached verdict — the walk+scan redeployTarget's watched and un-watched branches share. Only
|
|
263
|
+
* their fallback differs: `notOnHistory` when `buildSha` is not on the first-parent walk at all
|
|
264
|
+
* (the caller keeps today's answer, `tip`), `onHistoryNone` when it is but nothing above it is
|
|
265
|
+
* green (watched keeps the build; un-watched keeps `tip`). Never throws: a git failure leaves
|
|
266
|
+
* the walk empty, so the fallback answers. */
|
|
267
|
+
async function newestGreenAboveBuild(wt, tip, buildSha, notOnHistory, onHistoryNone) {
|
|
268
|
+
const walk = await firstParentWalk(wt, tip, REDEPLOY_WALK_MAX);
|
|
257
269
|
const buildAt = walk.indexOf(buildSha);
|
|
258
270
|
if (buildAt === -1)
|
|
259
|
-
return
|
|
271
|
+
return notOnHistory;
|
|
260
272
|
for (const sha of walk.slice(0, buildAt)) {
|
|
261
273
|
if (baselineCache.get(sha)?.status === "green")
|
|
262
274
|
return sha;
|
|
263
275
|
}
|
|
264
|
-
return
|
|
276
|
+
return onHistoryNone;
|
|
277
|
+
}
|
|
278
|
+
/** The commit self-redeploy should run: main's newest first-parent commit with a green full-check
|
|
279
|
+
* verdict in the cache (observed or inherited) that is newer than the running build `buildSha`,
|
|
280
|
+
* looking at most REDEPLOY_WALK_MAX commits down from `tip`.
|
|
281
|
+
*
|
|
282
|
+
* With `watched` false (the default, a project whose main the watch does not verify), a tip the
|
|
283
|
+
* cache has no verdict for is the target as is — the redeployer checks it. A tip whose cached red
|
|
284
|
+
* was observed in the redeployer's mirror worktree (mirrorWorktreePath of `wt`) falls back to the
|
|
285
|
+
* newest older green above the build. Any other red may be another worktree's environmental one,
|
|
286
|
+
* which the redeployer re-verifies; a build that is not a first-parent ancestor of `tip`, or no
|
|
287
|
+
* green, also leaves the tip the target, and the redeployer blocks that commit alone.
|
|
288
|
+
*
|
|
289
|
+
* With `watched` true (watchVerifiesMain), the watch supplies the verdicts and the redeployer only
|
|
290
|
+
* reads them: walk `tip`'s first-parent history down to `buildSha` and return the first commit
|
|
291
|
+
* whose cached verdict is green — a cached red is walked past, never targeted. With no green
|
|
292
|
+
* above the build, target `buildSha` itself, so buildStaleness reads it as not stale and no
|
|
293
|
+
* episode, hold or pre-warm starts until the watch calls a newer commit green. A build that is
|
|
294
|
+
* not a first-parent ancestor of `tip` keeps today's answer (`tip`, which the redeployer
|
|
295
|
+
* checks), as does `watched` false. Every target `watched` picks has a cached green, so
|
|
296
|
+
* mainIsGreen on it is a cache hit and the redeployer starts no suite.
|
|
297
|
+
* The cache holds only full-check verdicts, so a cheaper command's green never makes a commit
|
|
298
|
+
* deployable.
|
|
299
|
+
* Never throws: a git failure leaves the tip as the target. */
|
|
300
|
+
export async function redeployTarget(wt, tip, buildSha, watched = false) {
|
|
301
|
+
if (watched) {
|
|
302
|
+
if (tip === "")
|
|
303
|
+
return tip;
|
|
304
|
+
return newestGreenAboveBuild(wt, tip, buildSha, tip, buildSha);
|
|
305
|
+
}
|
|
306
|
+
const red = baselineCache.get(tip);
|
|
307
|
+
if (tip === "" || red?.status !== "red" || !(red.redFrom ?? []).includes(mirrorWorktreePath(wt)))
|
|
308
|
+
return tip;
|
|
309
|
+
return newestGreenAboveBuild(wt, tip, buildSha, tip, tip);
|
|
265
310
|
}
|
|
266
311
|
/** Verify main's own build/test suite at `wt`'s HEAD — which must be pristine main (the caller
|
|
267
312
|
* is the fresh-run path right after resetWorktreeToMain; a dirty or ahead worktree would
|
|
268
313
|
* measure the wrong thing). Cache hit returns immediately; on miss runs detectBuildCheck +
|
|
269
314
|
* runBuildCheck once per SHA (in-flight deduped) and caches green/red. Never throws: git,
|
|
270
|
-
* detection, and execution failures all resolve to
|
|
315
|
+
* detection, and execution failures all resolve to no verdict (nothing to verify). */
|
|
271
316
|
export async function checkMainBaseline(wt,
|
|
272
317
|
/** The live config — the declared check is detected through it (plans/portability.md
|
|
273
318
|
* §6/7), so a configured command verifies main on a non-npm repo too. Required in this
|
|
274
319
|
* position: detection needs it, and an optional trailing parameter would let future
|
|
275
|
-
* callers skip the baseline
|
|
320
|
+
* callers skip the baseline check entirely. */
|
|
276
321
|
config,
|
|
277
322
|
/** Called once per actual script run (never for cache hits or deduped waiters) with what ran
|
|
278
323
|
* and how long it took — the caller's hook for a build_check event. */
|
|
279
324
|
onRun,
|
|
280
325
|
/** Ignore a cached RED verdict and run the suite here instead (a cached green still short-
|
|
281
326
|
* circuits — see baselineCache). For the caller whose false block is expensive enough to pay
|
|
282
|
-
* one extra run: the redeploy
|
|
283
|
-
* strands the
|
|
284
|
-
* the SHA team-wide,
|
|
327
|
+
* one extra run: the redeploy path, where trusting another worktree's environmental red
|
|
328
|
+
* strands the team on a stale build until main moves. A green from the re-run promotes
|
|
329
|
+
* the SHA team-wide, so every consumer reads main green. */
|
|
285
330
|
reverifyRed = false,
|
|
286
331
|
/** Ignore the cache entirely and run the suite here, even over a GREEN verdict. For the one
|
|
287
332
|
* caller that cannot trust a green (BUGS.md 2026-10-06: the review gate re-attributing a
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* selfcheck digest) words them the same. Read-only: it never writes an event, state or file.
|
|
5
5
|
*
|
|
6
6
|
* G1 lives on the `main_check` / `main_red_hold` event trail (src/baseline/main-watch.ts,
|
|
7
|
-
* src/baseline/main-red.ts)
|
|
7
|
+
* src/baseline/main-red.ts), and names the merge the watch blamed (`main_culprit`, logged by
|
|
8
|
+
* src/baseline/main-watch.ts) in the same clause the dashboard alert uses. Only `main_check`
|
|
9
|
+
* events that actually ran the check
|
|
8
10
|
* (`durationMs` > 0; a cached or inherited verdict is ignored) and `main_red_hold` events
|
|
9
11
|
* count, and a `main_check` that passed (a red naming only quarantined tests is recorded
|
|
10
12
|
* passed) ends a red. G3 reads the quarantine list (src/review/known-flakes.ts) directly.
|
|
@@ -13,6 +15,8 @@ import { readEventsSince } from "../events/event-window.js";
|
|
|
13
15
|
import { loadQuarantine } from "../review/known-flakes.js";
|
|
14
16
|
import { DAY_MS, clockHm, humanAgo } from "../text/datetime.js";
|
|
15
17
|
import { shortSha } from "../text/format.js";
|
|
18
|
+
import { roleLabel } from "../roles/role-labels.js";
|
|
19
|
+
import { plural } from "../text/phrases.js";
|
|
16
20
|
import { squash } from "../text/text.js";
|
|
17
21
|
/** How long a red main must last before the doctor and the selfcheck digest warn (G1). */
|
|
18
22
|
export const RED_MAIN_WARN_MS = 30 * 60_000;
|
|
@@ -45,22 +49,36 @@ export const QUARANTINE_WARN_COUNT = 10;
|
|
|
45
49
|
/** How many of the oldest quarantined tests a warning names before folding the rest into a
|
|
46
50
|
* remainder count. */
|
|
47
51
|
const QUARANTINE_DETAIL_MAX = 3;
|
|
48
|
-
/**
|
|
49
|
-
*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
/** True when `e` is the newest `main_culprit` seen so far: no contender yet, or a later `ts`.
|
|
53
|
+
* The newest-in-window pick that `scanRedWindow` folds into its loop. */
|
|
54
|
+
function isNewerCulprit(c, e) {
|
|
55
|
+
return c === undefined || e.ts > c.ts;
|
|
56
|
+
}
|
|
57
|
+
/** Scans the trailing 24 h of `events` (cutoff `now - DAY_MS`), collecting the events that open,
|
|
58
|
+
* extend or end a red and picking the newest in-window `main_culprit` with a non-empty string
|
|
59
|
+
* `commit`. Owns the window scan and the newest-culprit pick. */
|
|
60
|
+
function scanRedWindow(events, cutoff) {
|
|
57
61
|
const relevant = [];
|
|
62
|
+
let culprit;
|
|
58
63
|
for (const e of events) {
|
|
59
64
|
if (typeof e.ts !== "number" || e.ts < cutoff)
|
|
60
65
|
continue;
|
|
66
|
+
if (e.type === "main_culprit") {
|
|
67
|
+
if (typeof e.commit === "string" && e.commit.length > 0 && isNewerCulprit(culprit, e)) {
|
|
68
|
+
culprit = e;
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
61
72
|
if (isRedMain(e) || isPassingMainCheck(e))
|
|
62
73
|
relevant.push(e);
|
|
63
74
|
}
|
|
75
|
+
return { relevant, culprit };
|
|
76
|
+
}
|
|
77
|
+
/** Derives the red spell over `window`'s `relevant` events: the start event and duration from
|
|
78
|
+
* `now`, whether the spell is unbroken, and the 30-minute threshold (null when main is green,
|
|
79
|
+
* or red for RED_MAIN_WARN_MS or less). Owns the red-spell derivation and the 30-minute bail. */
|
|
80
|
+
function redSpell(window, now) {
|
|
81
|
+
const relevant = window.relevant;
|
|
64
82
|
if (relevant.length === 0)
|
|
65
83
|
return null;
|
|
66
84
|
let newest = relevant[0];
|
|
@@ -85,10 +103,53 @@ export function redMainWarning(events, now) {
|
|
|
85
103
|
if (durationMs <= RED_MAIN_WARN_MS)
|
|
86
104
|
return null;
|
|
87
105
|
const unbroken = lastPass === undefined && reds.length > 1;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
106
|
+
return { startEv, newest, durationMs, unbroken };
|
|
107
|
+
}
|
|
108
|
+
/** "more than 24 h" for an unbroken or full-window red, else the short duration phrase. Owns
|
|
109
|
+
* the duration wording. */
|
|
110
|
+
function redSpellDuration(spell) {
|
|
111
|
+
return spell.unbroken || spell.durationMs >= DAY_MS ? "more than 24 h" : redDurationPhrase(spell.durationMs);
|
|
112
|
+
}
|
|
113
|
+
/** The newest red's first failing test, when one is named. Owns the first-failing-test read. */
|
|
114
|
+
function redFailingTest(newest) {
|
|
115
|
+
return Array.isArray(newest.failingTests) && typeof newest.failingTests[0] === "string" ? newest.failingTests[0] : undefined;
|
|
116
|
+
}
|
|
117
|
+
/** The blame clause naming the merge the main watch blamed, when the newest `culprit` in the
|
|
118
|
+
* window blames the same sha the phrase names. Owns the blame clause. */
|
|
119
|
+
function redBlameClause(culprit, newest) {
|
|
120
|
+
// The blame names the merge the main watch blamed for the red — the same `main_culprit`
|
|
121
|
+
// commit-equals-tip rule the dashboard alert uses (status-polls only publishes a blame whose
|
|
122
|
+
// commit is the derived tip sha), so a red the watch did not blame never borrows another
|
|
123
|
+
// head's culprit. A culprit naming another commit, or none, leaves the phrase as today.
|
|
124
|
+
const shouldBlame = culprit !== undefined && culprit.commit === newest.sha;
|
|
125
|
+
return shouldBlame
|
|
126
|
+
? typeof culprit.runner === "string" && culprit.runner.length > 0
|
|
127
|
+
? `; the main watch blames ${roleLabel(culprit.runner)}'s merge ${shortSha(culprit.commit)}`
|
|
128
|
+
: `; the main watch blames merge ${shortSha(culprit.commit)}`
|
|
129
|
+
: "";
|
|
130
|
+
}
|
|
131
|
+
/** G1 over `events`: warns when main has been red for more than RED_MAIN_WARN_MS, over the
|
|
132
|
+
* trailing 24 h of `events` from `now`. Only `main_check` events with `durationMs` > 0 and
|
|
133
|
+
* `main_red_hold` events count; a `main_check` with `durationMs` 0 (cached or inherited) is
|
|
134
|
+
* ignored; a pass (including one naming only quarantined tests, recorded passed) ends the red.
|
|
135
|
+
* The phrase names how long main has had no passing full check, the sha it has been red since,
|
|
136
|
+
* its clock time, the newest red's first failing test when one is named, and — when the newest
|
|
137
|
+
* `main_culprit` in the window blames the same sha the phrase names — the merge the main watch
|
|
138
|
+
* blamed (`; the main watch blames <role>'s merge <sha>`, or `; the main watch blames merge
|
|
139
|
+
* <sha>` when no runner is named), the one rule the dashboard alert and this phrase share.
|
|
140
|
+
* Returns null when main is green, or red for RED_MAIN_WARN_MS or less. The per-job work is
|
|
141
|
+
* split into the module-private helpers `scanRedWindow`, `redSpell`, `redSpellDuration`,
|
|
142
|
+
* `redFailingTest` and `redBlameClause`, one per job, so each is found by name. */
|
|
143
|
+
export function redMainWarning(events, now) {
|
|
144
|
+
const window = scanRedWindow(events, now - DAY_MS);
|
|
145
|
+
const spell = redSpell(window, now);
|
|
146
|
+
if (spell === null)
|
|
147
|
+
return null;
|
|
148
|
+
const duration = redSpellDuration(spell);
|
|
149
|
+
const head = `main has had no passing full check for ${duration}: red since ${shortSha(spell.startEv.sha)} at ${clockHm(spell.startEv.ts)}`;
|
|
150
|
+
const failing = redFailingTest(spell.newest);
|
|
151
|
+
const blame = redBlameClause(window.culprit, spell.newest);
|
|
152
|
+
return failing ? `${head} — ${squash(failing, 120)}${blame}` : `${head}${blame}`;
|
|
92
153
|
}
|
|
93
154
|
/** G3 over `entries` (the loadQuarantine list): warns when the list holds more than
|
|
94
155
|
* QUARANTINE_WARN_COUNT tests, or when any test's `firstSeen` is more than QUARANTINE_AGE_MS
|
|
@@ -109,7 +170,7 @@ export function quarantineWarning(entries, now) {
|
|
|
109
170
|
return `"${squash(e.test, 40)}" (${filed}, ${humanAgo(e.firstSeen, now)})`;
|
|
110
171
|
});
|
|
111
172
|
const more = old.length > QUARANTINE_DETAIL_MAX ? ` (and ${old.length - QUARANTINE_DETAIL_MAX} more)` : "";
|
|
112
|
-
phrases.push(`quarantine holds ${old.length
|
|
173
|
+
phrases.push(`quarantine holds ${plural(old.length, "test")} over 3 days: ${named.join(", ")}${more}`);
|
|
113
174
|
}
|
|
114
175
|
return phrases.length > 0 ? phrases.join("; ") : null;
|
|
115
176
|
}
|