tumwater 0.4.2 → 0.4.4
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 +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -86,29 +86,44 @@ export function persistBaselineVerdicts(root) {
|
|
|
86
86
|
const file = baselineVerdictsPath(root);
|
|
87
87
|
greensFile = undefined; // the load below re-writes nothing it just read
|
|
88
88
|
const stored = readJsonFile(file);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (baselineCache.has(entry.sha))
|
|
94
|
-
continue;
|
|
95
|
-
const from = typeof entry.inheritedFrom === "string" ? { inheritedFrom: entry.inheritedFrom } : {};
|
|
96
|
-
rememberBaseline(entry.sha, { status: "green", sha: entry.sha, ...from });
|
|
97
|
-
}
|
|
98
|
-
const reds = Array.isArray(stored?.reds) ? stored.reds : [];
|
|
99
|
-
for (const entry of reds) {
|
|
100
|
-
if (!isJsonObject(entry) || typeof entry.sha !== "string" || !/^[0-9a-f]{40,64}$/.test(entry.sha))
|
|
101
|
-
continue;
|
|
102
|
-
if (baselineCache.has(entry.sha))
|
|
103
|
-
continue;
|
|
104
|
-
const failingTests = Array.isArray(entry.failingTests)
|
|
105
|
-
? entry.failingTests.filter((t) => typeof t === "string")
|
|
106
|
-
: [];
|
|
107
|
-
const script = typeof entry.script === "string" ? { script: entry.script } : {};
|
|
108
|
-
rememberBaseline(entry.sha, { status: "red", sha: entry.sha, ...script, failingTests });
|
|
109
|
-
}
|
|
89
|
+
for (const entry of storedVerdictList(stored, "greens"))
|
|
90
|
+
restoreGreenVerdict(entry);
|
|
91
|
+
for (const entry of storedVerdictList(stored, "reds"))
|
|
92
|
+
restoreRedVerdict(entry);
|
|
110
93
|
greensFile = file;
|
|
111
94
|
}
|
|
95
|
+
/** The saved array for one kind of verdict, or an empty list when absent or not an array. */
|
|
96
|
+
function storedVerdictList(stored, key) {
|
|
97
|
+
const value = stored?.[key];
|
|
98
|
+
return Array.isArray(value) ? value : [];
|
|
99
|
+
}
|
|
100
|
+
/** A saved entry that is an object with a 40-64 hex `sha`, or null when it is not. */
|
|
101
|
+
function storedVerdictEntry(entry) {
|
|
102
|
+
if (!isJsonObject(entry))
|
|
103
|
+
return null;
|
|
104
|
+
if (typeof entry.sha !== "string" || !/^[0-9a-f]{40,64}$/.test(entry.sha))
|
|
105
|
+
return null;
|
|
106
|
+
return entry;
|
|
107
|
+
}
|
|
108
|
+
/** Restore one saved green, carrying an optional `inheritedFrom`, unless the cache holds it. */
|
|
109
|
+
function restoreGreenVerdict(entry) {
|
|
110
|
+
const parsed = storedVerdictEntry(entry);
|
|
111
|
+
if (parsed === null || baselineCache.has(parsed.sha))
|
|
112
|
+
return;
|
|
113
|
+
const from = typeof parsed.inheritedFrom === "string" ? { inheritedFrom: parsed.inheritedFrom } : {};
|
|
114
|
+
rememberBaseline(parsed.sha, { status: "green", sha: parsed.sha, ...from });
|
|
115
|
+
}
|
|
116
|
+
/** Restore one saved red: optional `script` and failing tests, unless the cache holds it. */
|
|
117
|
+
function restoreRedVerdict(entry) {
|
|
118
|
+
const parsed = storedVerdictEntry(entry);
|
|
119
|
+
if (parsed === null || baselineCache.has(parsed.sha))
|
|
120
|
+
return;
|
|
121
|
+
const failingTests = Array.isArray(parsed.failingTests)
|
|
122
|
+
? parsed.failingTests.filter((t) => typeof t === "string")
|
|
123
|
+
: [];
|
|
124
|
+
const script = typeof parsed.script === "string" ? { script: parsed.script } : {};
|
|
125
|
+
rememberBaseline(parsed.sha, { status: "red", sha: parsed.sha, ...script, failingTests });
|
|
126
|
+
}
|
|
112
127
|
/** In-flight dedup: concurrent runs on the same not-yet-cached SHA (a fresh main move wakes
|
|
113
128
|
* every role's verifier at once) share one check run instead of racing N npm invocations. Keyed by
|
|
114
129
|
* SHA for ordinary checks; a re-verification adds its worktree, because joining another
|
|
@@ -350,76 +365,22 @@ tier = CHECK_TIER.other) {
|
|
|
350
365
|
const sha = await refSha(wt, "HEAD");
|
|
351
366
|
if (!sha)
|
|
352
367
|
return { baseline: null }; // No HEAD (unborn branch) — nothing to key on.
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
if (cached && !forceFresh && !shouldRerunRed(cached, wt, reverifyRed))
|
|
368
|
+
const { cached, key } = cachedBaselineFor(sha, wt, { reverifyRed, observedOnly, forceFresh });
|
|
369
|
+
if (cached)
|
|
356
370
|
return { baseline: cached };
|
|
357
|
-
|
|
371
|
+
// A re-run of a red keys the in-flight map by worktree (key !== sha): joining another
|
|
372
|
+
// worktree's run would observe the environment this run exists to re-test, and it is not a
|
|
373
|
+
// carry-over candidate either — only a SHA with no cached verdict may inherit a green.
|
|
374
|
+
if (!forceFresh && !observedOnly && key === sha) {
|
|
358
375
|
// Outside the in-flight dedup below on purpose: a forceFresh or observedOnly caller joining
|
|
359
376
|
// a pending promise must get a run, never a carried-over verdict.
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
if (inherited) {
|
|
363
|
-
// Re-read after the await: a run that settled meanwhile knows more than the carry-over.
|
|
364
|
-
const settled = baselineCache.get(sha);
|
|
365
|
-
if (settled)
|
|
366
|
-
return { baseline: settled };
|
|
367
|
-
rememberBaseline(sha, inherited);
|
|
377
|
+
const inherited = await inheritedBaselineFor(wt, sha, config);
|
|
378
|
+
if (inherited)
|
|
368
379
|
return { baseline: inherited };
|
|
369
|
-
}
|
|
370
380
|
}
|
|
371
|
-
// A re-run of a red keys the in-flight map by worktree: joining another worktree's run would
|
|
372
|
-
// observe the environment this run exists to re-test.
|
|
373
|
-
const key = cached?.status === "red" ? `${sha}\u0000${wt}` : sha;
|
|
374
381
|
let pending = baselineInFlight.get(key);
|
|
375
382
|
if (!pending) {
|
|
376
|
-
pending = (
|
|
377
|
-
const check = detectBuildCheck(wt, config);
|
|
378
|
-
if (!check)
|
|
379
|
-
return { baseline: null }; // No declared check — nothing to verify, nothing to block on.
|
|
380
|
-
// The same process-wide check permit the scoped checks take (build/build-check.ts's
|
|
381
|
-
// withCheckPermit): this runs the suite directly, so it must count against the cap too.
|
|
382
|
-
// Only the one run per key holds a permit; deduped waiters above await `pending` holding
|
|
383
|
-
// none.
|
|
384
|
-
const { outcome, durationMs } = await withCheckPermit(config, tier, () => runWithCleanRetry(() => runBuildCheck(wt, check, undefined, undefined, sampleSleep),
|
|
385
|
-
// Weather (a late deadline, a foreign kill, a host sleep) made no verdict about the
|
|
386
|
-
// tree: the first attempt is priced as its own run, and the retry's outcome stands —
|
|
387
|
-
// the same rule runScopedBuildCheck applies at every other scope.
|
|
388
|
-
(first, dur) => onRun?.({ outcome: first, durationMs: dur })), undefined, "main baseline");
|
|
389
|
-
onRun?.({ outcome, durationMs });
|
|
390
|
-
if (outcome.status === "skipped") {
|
|
391
|
-
// Environmental (timeout/no-npm), after the clean retry above for weather shapes:
|
|
392
|
-
// warn-and-proceed semantics like the gate's pre-check; never cache red for a skip.
|
|
393
|
-
return { baseline: null, skipReason: outcome.skipReason, run: outcome.run };
|
|
394
|
-
}
|
|
395
|
-
if (outcome.status === "failed" && (outcome.run?.sleptMs ?? 0) > SLEEP_SPAN_TOLERANCE_MS) {
|
|
396
|
-
// A failure the host slept through made no verdict about the tree either (BUGS.md
|
|
397
|
-
// 2026-09-30): the sleep expired a test's own wall-clock wait at the wake and the suite
|
|
398
|
-
// exited 1 inside the deadline. Caching it red would make sleep weather the team's
|
|
399
|
-
// authoritative red main, so nothing is cached — the next consult re-runs, and a clean
|
|
400
|
-
// verdict then settles the SHA exactly as before.
|
|
401
|
-
return { baseline: null, run: outcome.run, unverified: true };
|
|
402
|
-
}
|
|
403
|
-
const prior = baselineCache.get(sha);
|
|
404
|
-
const priorRedFrom = prior?.status === "red" ? (prior.redFrom ?? []) : [];
|
|
405
|
-
const baseline = outcome.status === "passed"
|
|
406
|
-
? { status: "green", sha }
|
|
407
|
-
: {
|
|
408
|
-
status: "red",
|
|
409
|
-
sha,
|
|
410
|
-
script: outcome.script,
|
|
411
|
-
outputTail: outcome.outputTail,
|
|
412
|
-
failingTests: outcome.failingTests ?? [],
|
|
413
|
-
redFrom: priorRedFrom.includes(wt) ? priorRedFrom : [...priorRedFrom, wt],
|
|
414
|
-
};
|
|
415
|
-
// A green is always recorded (promoting a provisional red); a red never overwrites an
|
|
416
|
-
// observed green — a concurrent run may have promoted this SHA while this one was still
|
|
417
|
-
// going. An inherited green is no observation, so this run's verdict replaces it either way.
|
|
418
|
-
const current = baselineCache.get(sha);
|
|
419
|
-
if (current?.status !== "green" || current.inheritedFrom !== undefined)
|
|
420
|
-
rememberBaseline(sha, baseline);
|
|
421
|
-
return { baseline };
|
|
422
|
-
})();
|
|
383
|
+
pending = runBaselineOnce(sha, wt, config, onRun, sampleSleep, tier);
|
|
423
384
|
baselineInFlight.set(key, pending);
|
|
424
385
|
}
|
|
425
386
|
try {
|
|
@@ -430,6 +391,109 @@ tier = CHECK_TIER.other) {
|
|
|
430
391
|
baselineInFlight.delete(key);
|
|
431
392
|
}
|
|
432
393
|
}
|
|
394
|
+
/** The usable cached verdict for `sha` — or null when the suite must run — together with the
|
|
395
|
+
* in-flight key its run dedups on. Applies the observedOnly inherited-green filter, the
|
|
396
|
+
* forceFresh / red re-run short-circuit, and the worktree-qualified key a red re-run keys by.
|
|
397
|
+
* `key === sha` tells the caller the SHA had no verdict to re-run (a possible carry-over
|
|
398
|
+
* candidate); a worktree-qualified key is a pending red re-run, which never inherits. */
|
|
399
|
+
function cachedBaselineFor(sha, wt, opts) {
|
|
400
|
+
const stored = baselineCache.get(sha);
|
|
401
|
+
const cached = opts.observedOnly && stored?.inheritedFrom !== undefined ? undefined : stored;
|
|
402
|
+
const usable = cached && !opts.forceFresh && !shouldRerunRed(cached, wt, opts.reverifyRed) ? cached : null;
|
|
403
|
+
const key = cached?.status === "red" ? `${sha}\u0000${wt}` : sha;
|
|
404
|
+
return { cached: usable, key };
|
|
405
|
+
}
|
|
406
|
+
/** The inherited green for `sha` across review-exempt ancestors, or null when there is none to
|
|
407
|
+
* carry over. The exemptPathsOf(config) + inheritedGreen(wt, sha, exempt) walk, the post-await
|
|
408
|
+
* cache re-read (a run that settled meanwhile knows more than the carry-over), and the
|
|
409
|
+
* rememberBaseline re-seed of the carried-over verdict. */
|
|
410
|
+
async function inheritedBaselineFor(wt, sha, config) {
|
|
411
|
+
const exempt = exemptPathsOf(config);
|
|
412
|
+
const inherited = exempt.length > 0 ? await inheritedGreen(wt, sha, exempt) : null;
|
|
413
|
+
if (inherited) {
|
|
414
|
+
// Re-read after the await: a run that settled meanwhile knows more than the carry-over.
|
|
415
|
+
const settled = baselineCache.get(sha);
|
|
416
|
+
if (settled)
|
|
417
|
+
return settled;
|
|
418
|
+
rememberBaseline(sha, inherited);
|
|
419
|
+
return inherited;
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
/** Run the declared check once under the same process-wide check permit the scoped checks take,
|
|
424
|
+
* retrying once when the first attempt made no verdict about the tree (a late deadline, a
|
|
425
|
+
* foreign kill, a host sleep). Returns the outcome and its duration, or null when no check is
|
|
426
|
+
* declared (nothing to verify, nothing to block on). */
|
|
427
|
+
async function detectedBaselineOutcome(wt, config, onRun, sampleSleep, tier) {
|
|
428
|
+
const check = detectBuildCheck(wt, config);
|
|
429
|
+
if (!check)
|
|
430
|
+
return null; // No declared check — nothing to verify, nothing to block on.
|
|
431
|
+
// The same process-wide check permit the scoped checks take (build/build-check.ts's
|
|
432
|
+
// withCheckPermit): this runs the suite directly, so it must count against the cap too.
|
|
433
|
+
// Only the one run per key holds a permit; deduped waiters above await `pending` holding
|
|
434
|
+
// none.
|
|
435
|
+
return withCheckPermit(config, tier, () => runWithCleanRetry(() => runBuildCheck(wt, check, undefined, undefined, sampleSleep),
|
|
436
|
+
// Weather (a late deadline, a foreign kill, a host sleep) made no verdict about the
|
|
437
|
+
// tree: the first attempt is priced as its own run, and the retry's outcome stands —
|
|
438
|
+
// the same rule runScopedBuildCheck applies at every other scope.
|
|
439
|
+
(first, dur) => onRun?.({ outcome: first, durationMs: dur })), undefined, "main baseline");
|
|
440
|
+
}
|
|
441
|
+
/** Build the green/red verdict from a run's outcome, folding the prior red's observed worktrees
|
|
442
|
+
* (redFrom) into a fresh red. Does not write the cache — the caller decides whether to promote. */
|
|
443
|
+
function baselineFromOutcome(sha, wt, outcome) {
|
|
444
|
+
const prior = baselineCache.get(sha);
|
|
445
|
+
const priorRedFrom = prior?.status === "red" ? (prior.redFrom ?? []) : [];
|
|
446
|
+
return outcome.status === "passed"
|
|
447
|
+
? { status: "green", sha }
|
|
448
|
+
: {
|
|
449
|
+
status: "red",
|
|
450
|
+
sha,
|
|
451
|
+
script: outcome.script,
|
|
452
|
+
outputTail: outcome.outputTail,
|
|
453
|
+
failingTests: outcome.failingTests ?? [],
|
|
454
|
+
redFrom: priorRedFrom.includes(wt) ? priorRedFrom : [...priorRedFrom, wt],
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/** Persist a freshly-run verdict unless a concurrent run already promoted the SHA with an
|
|
458
|
+
* observed green. A green is always recorded (promoting a provisional red); a red never
|
|
459
|
+
* overwrites an observed green — a concurrent run may have promoted this SHA while this one was
|
|
460
|
+
* still going. An inherited green is no observation, so this run's verdict replaces it either
|
|
461
|
+
* way. */
|
|
462
|
+
function rememberPromotableBaseline(sha, baseline) {
|
|
463
|
+
// A green is always recorded (promoting a provisional red); a red never overwrites an
|
|
464
|
+
// observed green — a concurrent run may have promoted this SHA while this one was still
|
|
465
|
+
// going. An inherited green is no observation, so this run's verdict replaces it either way.
|
|
466
|
+
const current = baselineCache.get(sha);
|
|
467
|
+
if (current?.status !== "green" || current.inheritedFrom !== undefined)
|
|
468
|
+
rememberBaseline(sha, baseline);
|
|
469
|
+
}
|
|
470
|
+
/** Run the suite once for `sha` at `wt`: detect the check, run it under the permit with a clean
|
|
471
|
+
* retry and its two onRun calls, fold the outcome into a verdict — skips and slept-through
|
|
472
|
+
* failures return no verdict and write nothing — and promote it unless an observed green
|
|
473
|
+
* outranks it. This replaces the old `(async () => …)()` closure stored in the in-flight map. */
|
|
474
|
+
async function runBaselineOnce(sha, wt, config, onRun, sampleSleep, tier) {
|
|
475
|
+
const detected = await detectedBaselineOutcome(wt, config, onRun, sampleSleep, tier);
|
|
476
|
+
if (!detected)
|
|
477
|
+
return { baseline: null };
|
|
478
|
+
const { outcome, durationMs } = detected;
|
|
479
|
+
onRun?.({ outcome, durationMs });
|
|
480
|
+
if (outcome.status === "skipped") {
|
|
481
|
+
// Environmental (timeout/no-npm), after the clean retry above for weather shapes:
|
|
482
|
+
// warn-and-proceed semantics like the gate's pre-check; never cache red for a skip.
|
|
483
|
+
return { baseline: null, skipReason: outcome.skipReason, run: outcome.run };
|
|
484
|
+
}
|
|
485
|
+
if (outcome.status === "failed" && (outcome.run?.sleptMs ?? 0) > SLEEP_SPAN_TOLERANCE_MS) {
|
|
486
|
+
// A failure the host slept through made no verdict about the tree either (BUGS.md
|
|
487
|
+
// 2026-09-30): the sleep expired a test's own wall-clock wait at the wake and the suite
|
|
488
|
+
// exited 1 inside the deadline. Caching it red would make sleep weather the team's
|
|
489
|
+
// authoritative red main, so nothing is cached — the next consult re-runs, and a clean
|
|
490
|
+
// verdict then settles the SHA exactly as before.
|
|
491
|
+
return { baseline: null, run: outcome.run, unverified: true };
|
|
492
|
+
}
|
|
493
|
+
const baseline = baselineFromOutcome(sha, wt, outcome);
|
|
494
|
+
rememberPromotableBaseline(sha, baseline);
|
|
495
|
+
return { baseline };
|
|
496
|
+
}
|
|
433
497
|
/** Is main green at the mirror worktree's HEAD? The merge path seeds a green verdict for every
|
|
434
498
|
* merged code SHA (noteGreenBaseline — its in-lock post-rebase re-check, or the gate's pre-check
|
|
435
499
|
* when the rebase was a no-op), so the common case is a cache hit. A head that only added doc
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Both take an injected `now` so tests never read the clock. */
|
|
14
14
|
import { readEventsSince } from "../events/event-window.js";
|
|
15
15
|
import { loadQuarantine } from "../review/known-flakes.js";
|
|
16
|
-
import { DAY_MS, clockHm, humanAgo } from "../text/datetime.js";
|
|
16
|
+
import { DAY_MS, HOUR_MS, clockHm, humanAgo } from "../text/datetime.js";
|
|
17
17
|
import { shortSha } from "../text/format.js";
|
|
18
18
|
import { roleLabel } from "../roles/role-labels.js";
|
|
19
19
|
import { plural } from "../text/phrases.js";
|
|
@@ -193,5 +193,5 @@ function redDurationPhrase(ms) {
|
|
|
193
193
|
const minutes = Math.round(ms / 60_000);
|
|
194
194
|
if (minutes < 24 * 60)
|
|
195
195
|
return `${minutes} min`;
|
|
196
|
-
return `${Math.round(ms /
|
|
196
|
+
return `${Math.round(ms / HOUR_MS)} h`;
|
|
197
197
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { distinctOrdered } from "../collections.js";
|
|
1
2
|
import { checkMainBaseline } from "./main-baseline.js";
|
|
2
3
|
import { baselineCheckLogger, clearHealKeyOnGreen, withMainTipWorktree } from "./main-red.js";
|
|
3
4
|
import { CHECK_TIER } from "../concurrency/check-permit.js";
|
|
@@ -13,6 +14,22 @@ import { treeDiffFiles } from "../git/git-diff.js";
|
|
|
13
14
|
import { mergingRoleOf } from "../events/event-read.js";
|
|
14
15
|
import { isExemptDiff } from "../review/exemptions.js";
|
|
15
16
|
import { diffRuleReasons } from "../review/diff-rules.js";
|
|
17
|
+
/** The main_check event payload for a settled verdict: the baseline sha and status, the run
|
|
18
|
+
* duration (0 when nothing ran), the covered-commit count, and, on a red, the failing tests and
|
|
19
|
+
* the quarantined-only marker. */
|
|
20
|
+
function mainCheckEvent(baseline, red, quarantined, covered, durationMs) {
|
|
21
|
+
return {
|
|
22
|
+
source: TUMWATER_SOURCE,
|
|
23
|
+
type: "main_check",
|
|
24
|
+
sha: baseline.sha,
|
|
25
|
+
status: red ? "failed" : "passed",
|
|
26
|
+
durationMs,
|
|
27
|
+
covered,
|
|
28
|
+
...(red ? { failingTests: baseline.failingTests ?? [] } : {}),
|
|
29
|
+
...(quarantined ? { quarantined } : {}),
|
|
30
|
+
...(baseline.inheritedFrom !== undefined ? { inherited: true } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
16
33
|
/** The failing tests of each root's newest watch verdict, keyed by root. */
|
|
17
34
|
const knownFailures = new Map();
|
|
18
35
|
/** Main's known failures: the failing tests of the watch's newest verdict — main's tip, or the
|
|
@@ -20,7 +37,7 @@ const knownFailures = new Map();
|
|
|
20
37
|
* (review/known-flakes.ts). Empty when the verdict is green, red with no parsed test, or none has
|
|
21
38
|
* been recorded yet in this process, and nothing is quarantined. */
|
|
22
39
|
export function knownMainFailures(root) {
|
|
23
|
-
return
|
|
40
|
+
return distinctOrdered([...(knownFailures.get(root) ?? []), ...quarantinedTests(root)]);
|
|
24
41
|
}
|
|
25
42
|
/** The failing tests of a failed check when every one of them is a known main failure; null when
|
|
26
43
|
* the output names no test, any test is outside the known set, or none is known. A list at
|
|
@@ -76,8 +93,9 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
76
93
|
}
|
|
77
94
|
}
|
|
78
95
|
async function watchOnce(mainHead, config) {
|
|
79
|
-
// The deterministic diff rules ("Every commit is verified on main, part 1/3"): run the
|
|
80
|
-
// five
|
|
96
|
+
// The deterministic diff rules ("Every commit is verified on main, part 1/3"): run the full
|
|
97
|
+
// five-check set the watch and gate share — the gate now runs only the three a later commit
|
|
98
|
+
// cannot repair — against main's new tip. A rule failure is a
|
|
81
99
|
// deterministic red — it takes no confirming re-run and never quarantines — reported as a
|
|
82
100
|
// warning naming the rule and the head (and each committing role, when a recent merged event
|
|
83
101
|
// names one). It runs on every tip the watch covers in a poll — a build green, a build
|
|
@@ -106,7 +124,7 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
106
124
|
await git(wt, "checkout", "-q", "--detach", "--force", commit);
|
|
107
125
|
const parent = `${commit}^`;
|
|
108
126
|
const files = (await treeDiffFiles(wt, parent, commit)) ?? [];
|
|
109
|
-
const [ruleReason] = await diffRuleReasons(wt, parent, files, mergingRoleOf(root, commit) ?? "", isExemptDiff(files, config.review.exemptPaths));
|
|
127
|
+
const [ruleReason] = await diffRuleReasons(wt, parent, files, mergingRoleOf(root, commit) ?? "", isExemptDiff(files, config.review.exemptPaths), "watch");
|
|
110
128
|
if (ruleReason !== undefined) {
|
|
111
129
|
logEventBestEffort(root, {
|
|
112
130
|
source: TUMWATER_SOURCE,
|
|
@@ -120,17 +138,11 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
120
138
|
}
|
|
121
139
|
}
|
|
122
140
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
129
|
-
lastWatched = mainHead; // no declared check: nothing else to watch until main moves
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
await withMainTipWorktree(root, mainBranch, async (wt) => {
|
|
133
|
-
const started = Date.now();
|
|
141
|
+
/** Run the declared build check on main's tip, re-running a red once and quarantining a flake
|
|
142
|
+
* whose re-run turns green. Returns the check's result and whether any run actually executed
|
|
143
|
+
* (false for a cache hit, a skip, or a sleep-spanned run). `config` and `root` are captured
|
|
144
|
+
* from `watchOnce`. */
|
|
145
|
+
async function runBaselineCheck(wt) {
|
|
134
146
|
let ran = false;
|
|
135
147
|
const onRun = baselineCheckLogger(root, TUMWATER_SOURCE);
|
|
136
148
|
const hook = (run) => {
|
|
@@ -145,45 +157,31 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
145
157
|
if (check.baseline?.status === "green")
|
|
146
158
|
quarantineTests(root, first.failingTests, (first.failingTests ?? []).join("\n"));
|
|
147
159
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
return { check, ran };
|
|
161
|
+
}
|
|
162
|
+
/** The covered-commit count for a main_check event: the first-parent commits between the
|
|
163
|
+
* previous and new watched heads, or 1 when there is no previous head. */
|
|
164
|
+
async function countCovered(wt, previous, sha) {
|
|
165
|
+
if (previous === undefined)
|
|
166
|
+
return 1;
|
|
167
|
+
const counted = Number(((await gitTry(wt, "rev-list", "--first-parent", "--count", `${previous}..${sha}`)) ?? "").trim());
|
|
168
|
+
return Number.isFinite(counted) && counted > 0 ? counted : 1;
|
|
169
|
+
}
|
|
170
|
+
/** Settle the verdict for a checked baseline: distinguish quarantined-only reds, record
|
|
171
|
+
* main's known failures, count the covered commits, and log the main_check event. Returns
|
|
172
|
+
* whether the tip settled red (a red naming tests that are not all quarantined). */
|
|
173
|
+
async function recordMainCheck(wt, baseline, previous, ran, started) {
|
|
158
174
|
// A red naming only quarantined tests counts as green: no redeploy hold.
|
|
159
175
|
const quarantined = baseline.status === "red" ? onlyQuarantined(root, baseline.failingTests) : null;
|
|
160
176
|
const red = baseline.status === "red" && !quarantined;
|
|
161
177
|
knownFailures.set(root, red ? (baseline.failingTests ?? []) : []);
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
status: red ? "failed" : "passed",
|
|
170
|
-
durationMs: ran ? Date.now() - started : 0,
|
|
171
|
-
covered: Number.isFinite(counted) && counted > 0 ? counted : 1,
|
|
172
|
-
...(red ? { failingTests: baseline.failingTests ?? [] } : {}),
|
|
173
|
-
...(quarantined ? { quarantined } : {}),
|
|
174
|
-
...(baseline.inheritedFrom !== undefined ? { inherited: true } : {}),
|
|
175
|
-
});
|
|
176
|
-
// The diff rules judge the tip whether the build is green or red; the build-red path below
|
|
177
|
-
// names the whole tip as the culprit, so the two red kinds are judged independently.
|
|
178
|
-
await watchRules(wt, previous, baseline.sha);
|
|
179
|
-
// A settled non-red verdict (a green, or a red naming only quarantined tests) clears the
|
|
180
|
-
// heal-dedup key, so a failing set that returns after a green main is healed again. A
|
|
181
|
-
// settled red verdict below does NOT clear it: the heal-dedup key stays set while main
|
|
182
|
-
// is red.
|
|
183
|
-
if (!red) {
|
|
184
|
-
clearHealKeyOnGreen();
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
178
|
+
const covered = await countCovered(wt, previous, baseline.sha);
|
|
179
|
+
logEventBestEffort(root, mainCheckEvent(baseline, red, quarantined, covered, ran ? Date.now() - started : 0));
|
|
180
|
+
return red;
|
|
181
|
+
}
|
|
182
|
+
/** Name a settled red tip as the culprit: its merging runner, the main_culprit event, and
|
|
183
|
+
* the onCulprit callback with the tip and its failing tests. */
|
|
184
|
+
function reportCulprit(baseline) {
|
|
187
185
|
// The newest head is the culprit ("Every commit is verified on main, part 2/3"): the watch
|
|
188
186
|
// checks each new red tip, so whatever it found belongs to the head it checked. Once per red
|
|
189
187
|
// head — lastWatched already holds it, so a later poll does not attribute it again — name
|
|
@@ -201,6 +199,42 @@ export function createMainWatch(root, mainBranch, onCulprit) {
|
|
|
201
199
|
failingTests: baseline.failingTests ?? [],
|
|
202
200
|
tip: baseline.sha,
|
|
203
201
|
});
|
|
202
|
+
}
|
|
203
|
+
if (!detectBuildCheck(root, config)) {
|
|
204
|
+
// No declared build check: nothing to watch for a build red, but the diff rules still run on
|
|
205
|
+
// the tip. The build-red path below is what never runs here.
|
|
206
|
+
await withMainTipWorktree(root, mainBranch, async (wt) => {
|
|
207
|
+
await watchRules(wt, lastWatched, await headOf(wt));
|
|
208
|
+
});
|
|
209
|
+
lastWatched = mainHead; // no declared check: nothing else to watch until main moves
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
await withMainTipWorktree(root, mainBranch, async (wt) => {
|
|
213
|
+
const started = Date.now();
|
|
214
|
+
const { check, ran } = await runBaselineCheck(wt);
|
|
215
|
+
const baseline = check.baseline;
|
|
216
|
+
const previous = lastWatched;
|
|
217
|
+
// A skipped or sleep-spanned run made no verdict: the head still counts as watched, so the
|
|
218
|
+
// next poll does not start another run for it — but the diff rules still run on this tip.
|
|
219
|
+
if (!baseline) {
|
|
220
|
+
const tip = await headOf(wt);
|
|
221
|
+
await watchRules(wt, previous, tip);
|
|
222
|
+
return void (lastWatched = tip);
|
|
223
|
+
}
|
|
224
|
+
lastWatched = baseline.sha;
|
|
225
|
+
const red = await recordMainCheck(wt, baseline, previous, ran, started);
|
|
226
|
+
// The diff rules judge the tip whether the build is green or red; the build-red path below
|
|
227
|
+
// names the whole tip as the culprit, so the two red kinds are judged independently.
|
|
228
|
+
await watchRules(wt, previous, baseline.sha);
|
|
229
|
+
// A settled non-red verdict (a green, or a red naming only quarantined tests) clears the
|
|
230
|
+
// heal-dedup key, so a failing set that returns after a green main is healed again. A
|
|
231
|
+
// settled red verdict below does NOT clear it: the heal-dedup key stays set while main
|
|
232
|
+
// is red.
|
|
233
|
+
if (!red) {
|
|
234
|
+
clearHealKeyOnGreen();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
reportCulprit(baseline);
|
|
204
238
|
});
|
|
205
239
|
}
|
|
206
240
|
return {
|
package/dist/src/brief/brief.js
CHANGED
|
@@ -106,16 +106,10 @@ export function briefFile(root) {
|
|
|
106
106
|
* on-purpose bare repo. An unreadable candidate counts: a brief that cannot be read is one
|
|
107
107
|
* the roles cannot use, and the underlying firstBriefText throw is caught and reported as
|
|
108
108
|
* true here — same subset doctor's `brief` warn covers, minus the bare-repo state that
|
|
109
|
-
* doctor deems fine.
|
|
109
|
+
* doctor deems fine. Thin accessor over briefStatus, so this predicate and the snapshot's
|
|
110
|
+
* shared flags share one implementation. */
|
|
110
111
|
export function briefBlockMissing(root) {
|
|
111
|
-
|
|
112
|
-
return false;
|
|
113
|
-
try {
|
|
114
|
-
return (readBrief(root)?.prompt ?? "") === "";
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
112
|
+
return briefStatus(root).missing;
|
|
119
113
|
}
|
|
120
114
|
/** True when the resolved brief block exceeds the read cap (BRIEF_TEXT_MAX_CHARS) — the
|
|
121
115
|
* always-on amber twin of the doctor `brief` row's over-cap warn. extractPrompt truncates
|
|
@@ -127,15 +121,34 @@ export function briefBlockMissing(root) {
|
|
|
127
121
|
* a surrogate pair, while an at-cap block stays equal to it. A repo with no candidate
|
|
128
122
|
* resolves null (0, false), and an unreadable candidate's readBrief throw is caught to false,
|
|
129
123
|
* because briefBlockMissing already raises `no-brief` for that state; an empty block stays
|
|
130
|
-
* briefBlockMissing's (its resolved length is 0).
|
|
131
|
-
* snapshot. */
|
|
124
|
+
* briefBlockMissing's (its resolved length is 0). Thin accessor over briefStatus, so this
|
|
125
|
+
* predicate and the snapshot's shared flags share one implementation. */
|
|
132
126
|
export function briefBlockOverCap(root) {
|
|
127
|
+
return briefStatus(root).overCap;
|
|
128
|
+
}
|
|
129
|
+
/** The always-on brief degraded-state flags, derived from ONE candidate scan and ONE file
|
|
130
|
+
* read — the per-second status snapshot's `noBrief`/`briefTruncated` fields, and the single
|
|
131
|
+
* implementation of both predicates: the standalone briefBlockMissing/briefBlockOverCap
|
|
132
|
+
* accessors delegate here, so the shared and standalone paths cannot disagree. The snapshot
|
|
133
|
+
* used to call those two accessors, each calling readBrief — which re-lists both candidates
|
|
134
|
+
* and re-reads the whole brief file — twice per poll, on a file that changes only when a role
|
|
135
|
+
* edits it. briefStatus is the degraded-state twin of readBrief's promise (briefFile +
|
|
136
|
+
* readBriefText derive from one scan): the two flags share one read, and each flag applies the
|
|
137
|
+
* value's own predicate. `missing` is true when a candidate exists but none resolves a
|
|
138
|
+
* non-empty block (or the resolving read threw); `overCap` true when the resolved block
|
|
139
|
+
* exceeds the cap. A repo with no candidate at all is neither (both false). */
|
|
140
|
+
export function briefStatus(root) {
|
|
141
|
+
if (briefCandidates(root).every((candidate) => !fs.existsSync(candidate))) {
|
|
142
|
+
return { missing: false, overCap: false };
|
|
143
|
+
}
|
|
144
|
+
let prompt = "";
|
|
133
145
|
try {
|
|
134
|
-
|
|
146
|
+
prompt = readBrief(root)?.prompt ?? "";
|
|
135
147
|
}
|
|
136
148
|
catch {
|
|
137
|
-
return false;
|
|
149
|
+
return { missing: true, overCap: false }; // Unreadable — the briefBlockMissing subset.
|
|
138
150
|
}
|
|
151
|
+
return { missing: prompt === "", overCap: prompt.length > BRIEF_TEXT_MAX_CHARS };
|
|
139
152
|
}
|
|
140
153
|
/** The project's brief text, extracted from the resolved brief file's managed section —
|
|
141
154
|
* TUMWATER.md when it exists with markers, else README.md (plans/portability.md §7a/7). The
|