tumwater 0.1.2 → 0.2.0
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 +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import { runBuildCheck, } from "../build/build-check.js";
|
|
2
|
-
import { SLEEP_SPAN_TOLERANCE_MS,
|
|
3
|
+
import { SLEEP_SPAN_TOLERANCE_MS, runWithCleanRetry } from "../build/build-check-events.js";
|
|
3
4
|
import { sampleSleepClock } from "../build/host-sleep.js";
|
|
4
5
|
import { CHECK_TIER, withCheckPermit } from "../concurrency/check-permit.js";
|
|
5
6
|
import { detectBuildCheck } from "../build/build-check-detect.js";
|
|
@@ -10,11 +11,12 @@ import { isExemptDiff } from "../review/exemptions.js";
|
|
|
10
11
|
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
11
12
|
import { isJsonObject } from "../files/json-object.js";
|
|
12
13
|
import { baselineVerdictsPath } from "../paths.js";
|
|
13
|
-
|
|
14
|
+
import { memoSetLru } from "../collections.js";
|
|
15
|
+
/** Team-shared verdict cache, keyed by main SHA. In-memory only: after a restart the cache is cold
|
|
14
16
|
* and one re-check per red SHA happens — cheap and deterministic, mirroring the budget gate's
|
|
15
17
|
* stateless resume. Entries come from two sources: checkMainBaseline's own runs, and
|
|
16
18
|
* noteGreenBaseline seeding a green verdict for a SHA that just became main
|
|
17
|
-
* (src/
|
|
19
|
+
* (src/merge/merge-to-main.ts: either its in-lock post-rebase re-check passed on exactly that
|
|
18
20
|
* tree, or the rebase was a no-op so the review gate's pre-check had already run green on it).
|
|
19
21
|
* A third kind of entry is an inherited green (MainBaseline.inheritedFrom), which no check
|
|
20
22
|
* observed. It is carried over across doc-only differences (inheritedGreen, noteInheritedGreen)
|
|
@@ -26,7 +28,7 @@ import { baselineVerdictsPath } from "../paths.js";
|
|
|
26
28
|
* tree: the same SHA can fail in one worktree and pass in another when the failure is really
|
|
27
29
|
* about the environment the check ran in (a worktree with no install, a half-written
|
|
28
30
|
* node_modules). On 2026-09-08 exactly that happened — a role worktree's environmental red
|
|
29
|
-
* became the
|
|
31
|
+
* became the team-wide verdict that blocked the harness's own restart (BUGS.md) — so a caller
|
|
30
32
|
* whose false block is expensive may re-verify a red in its own worktree (`reverifyRed`), and a
|
|
31
33
|
* green from that run promotes the SHA for everyone. Since 2026-09-18 that re-verification is
|
|
32
34
|
* not opt-in only: a red carries the worktrees that observed it (`redFrom`) and is re-run by the
|
|
@@ -34,29 +36,24 @@ import { baselineVerdictsPath } from "../paths.js";
|
|
|
34
36
|
* role until main moves — see shouldRerunRed. */
|
|
35
37
|
const baselineCache = new Map();
|
|
36
38
|
/** Safety cap so the per-process verdict cache can never grow unbounded: it is keyed by every
|
|
37
|
-
* merged code SHA and a red entry carries the failed run's output tail, so a
|
|
38
|
-
* weeks would accumulate one entry per
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* merged code SHA and a red entry carries the failed run's output tail, so a team running for
|
|
40
|
+
* weeks would accumulate one entry per merge forever. Evicting the oldest entry (Map's insertion
|
|
41
|
+
* order) costs at most one redundant suite run if that SHA is consulted again: a green for a SHA
|
|
42
|
+
* that becomes main is re-seeded by the next merge (or its own checkMainBaseline run), and a red
|
|
43
|
+
* is provisional by shouldRerunRed anyway. In-place updates never grow the map and are never
|
|
44
|
+
* evicted. */
|
|
43
45
|
const BASELINE_CACHE_MAX = 256;
|
|
44
46
|
/** Insert or update `sha`'s verdict, evicting the oldest entry when a NEW SHA would exceed the
|
|
45
47
|
* cap. Both write sites route through here so the cap cannot be bypassed, and so does the
|
|
46
48
|
* mirror on disk (saveGreens). */
|
|
47
49
|
function rememberBaseline(sha, baseline) {
|
|
48
|
-
|
|
49
|
-
const oldest = baselineCache.keys().next().value;
|
|
50
|
-
if (oldest !== undefined)
|
|
51
|
-
baselineCache.delete(oldest);
|
|
52
|
-
}
|
|
53
|
-
baselineCache.set(sha, baseline);
|
|
50
|
+
memoSetLru(baselineCache, sha, baseline, BASELINE_CACHE_MAX);
|
|
54
51
|
saveGreens();
|
|
55
52
|
}
|
|
56
53
|
/** The file the cache's greens are mirrored to (persistBaselineVerdicts), or undefined while no
|
|
57
|
-
*
|
|
54
|
+
* team process has asked for it: the CLI and the tests keep the cache in memory only. */
|
|
58
55
|
let greensFile;
|
|
59
|
-
/** Mirror the green verdicts to disk, so the
|
|
56
|
+
/** Mirror the green verdicts to disk, so the team's next generation starts with them. A restart
|
|
60
57
|
* used to empty the cache, and the first tick after every restart re-ran the suite on a main the
|
|
61
58
|
* previous generation had already verified (41 of 319 baseline runs over 2026-10-02..12 started
|
|
62
59
|
* within 10 minutes of an orchestrator start). Only greens are kept: a green is a fact about an
|
|
@@ -76,7 +73,7 @@ function saveGreens() {
|
|
|
76
73
|
}
|
|
77
74
|
}
|
|
78
75
|
/** Load the green verdicts a previous generation saved under `root` and keep mirroring them
|
|
79
|
-
* there (saveGreens). Called once at
|
|
76
|
+
* there (saveGreens). Called once at team startup (cli/cli-run.ts). An entry the cache already
|
|
80
77
|
* holds is left as it is, and a malformed file or entry reads as no data. */
|
|
81
78
|
export function persistBaselineVerdicts(root) {
|
|
82
79
|
const file = baselineVerdictsPath(root);
|
|
@@ -100,14 +97,14 @@ export function persistBaselineVerdicts(root) {
|
|
|
100
97
|
const baselineInFlight = new Map();
|
|
101
98
|
/** Should a cached RED be re-run here instead of trusted? A red says as much about the
|
|
102
99
|
* environment the check ran in as about the tree — a worktree mid-install, or a machine under
|
|
103
|
-
* the load the
|
|
104
|
-
* load-sensitive tests entry). The role loops consult this
|
|
100
|
+
* the load the team deliberately creates starving a timing-sensitive test (BUGS.md's
|
|
101
|
+
* load-sensitive tests entry). The role loops consult this team-shared cache and, before
|
|
105
102
|
* 2026-09-18, trusted a red forever: one environmental red blocked every code role until main
|
|
106
103
|
* moved, and main could not move, because blocking the code roles is exactly what stops it —
|
|
107
104
|
* a deadlock broken only by restarting the orchestrator, since the cache is per-process.
|
|
108
105
|
*
|
|
109
106
|
* So a red is provisional until two DIFFERENT worktrees have seen it. The second observer pays
|
|
110
|
-
* one extra suite run and a green from it promotes the SHA
|
|
107
|
+
* one extra suite run and a green from it promotes the SHA team-wide; a second red makes the
|
|
111
108
|
* verdict authoritative for everyone. The cost is bounded at ONE confirmation run per SHA, not
|
|
112
109
|
* one per role. `reverifyRed` still forces a run unconditionally for the redeploy gate, whose
|
|
113
110
|
* false block is expensive enough to always pay for its own opinion. */
|
|
@@ -119,32 +116,32 @@ function shouldRerunRed(cached, wt, reverifyRed) {
|
|
|
119
116
|
const seen = cached.redFrom ?? [];
|
|
120
117
|
return seen.length < 2 && !seen.includes(wt);
|
|
121
118
|
}
|
|
122
|
-
/** Record a green baseline verdict for `sha` WITHOUT running anything. The callers are the
|
|
123
|
-
* path (src/
|
|
124
|
-
* (src/
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* scope-`batch` check ran green on exactly it (a skipped check seeds nothing, and neither does
|
|
130
|
-
*
|
|
131
|
-
* here means that once the merge
|
|
119
|
+
/** Record a green baseline verdict for `sha` WITHOUT running anything. The callers are the merge
|
|
120
|
+
* path (src/merge/merge-to-main.ts's verifyMerge) and the stack lander
|
|
121
|
+
* (src/merge/merge-stack.ts' mergeStack) — both call it only after their fast-forward SUCCEEDED,
|
|
122
|
+
* with the exact SHA that just became main: verifyMerge with the POST-rebase head in two cases —
|
|
123
|
+
* its own in-lock re-check just ran this project's declared check green on that tree, or the
|
|
124
|
+
* rebase was a no-op so the review gate's pre-check (which runs outside the merge lock) had
|
|
125
|
+
* already run green on exactly this tree — and mergeStack with the stacked tip when a
|
|
126
|
+
* scope-`batch` check ran green on exactly it (a skipped check seeds nothing, and neither does a
|
|
127
|
+
* merge_blocked stack or a doc-only re-stack that merged without a check of its own). Seeding
|
|
128
|
+
* here means that once the merge completes — main now points at this very SHA — the next fresh
|
|
132
129
|
* tick's checkMainBaseline is a cache hit instead of re-running the full suite on an
|
|
133
130
|
* already-verified tree: for tumwater itself that saves one redundant `npm test` (~1 min) per
|
|
134
131
|
* merged code tick, plus every other role waking on "main moved" stalling behind that in-flight
|
|
135
132
|
* run. Never seed a pre-rebase head: whenever main moved under the review, that SHA never
|
|
136
133
|
* becomes main and the entry would silently miss (BUGS.md 2026-09-08). Safe because git trees
|
|
137
134
|
* are immutable — a SHA's content cannot change under a cached verdict, the same staleness
|
|
138
|
-
* semantics checkMainBaseline already has for its own entries. Only a directly observed pass
|
|
139
|
-
*
|
|
135
|
+
* semantics checkMainBaseline already has for its own entries. Only a directly observed pass may
|
|
136
|
+
* seed this; skips and failures leave the baseline unknown (the caller decides). */
|
|
140
137
|
export function noteGreenBaseline(sha) {
|
|
141
138
|
rememberBaseline(sha, { status: "green", sha });
|
|
142
139
|
}
|
|
143
140
|
/** Record an INHERITED green for `sha` (MainBaseline.inheritedFrom): a tree no check ran on, whose
|
|
144
141
|
* only difference from the green-verified `from` is in review-exempt paths. The caller is
|
|
145
|
-
*
|
|
142
|
+
* merge-to-main.ts's verifyMerge, after it skipped the in-lock check because main moved
|
|
146
143
|
* under the gate-verified head only by doc commits. It seeds the head about to become main, as
|
|
147
|
-
*
|
|
144
|
+
* verifyMerge's own noteGreenBaseline calls do. If the fast-forward then fails, the entry is
|
|
148
145
|
* still true of that tree and simply never consulted. It never displaces an observed verdict: a
|
|
149
146
|
* check that actually ran on this tree knows more than the carry-over. */
|
|
150
147
|
export function noteInheritedGreen(sha, from) {
|
|
@@ -153,22 +150,21 @@ export function noteInheritedGreen(sha, from) {
|
|
|
153
150
|
return;
|
|
154
151
|
rememberBaseline(sha, { status: "green", sha, inheritedFrom: from });
|
|
155
152
|
}
|
|
156
|
-
/** How far down main's first-parent history inheritedGreen looks for a verdict to carry over.
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
153
|
+
/** How far down main's first-parent history inheritedGreen looks for a verdict to carry over. main
|
|
154
|
+
* gains about two dozen commits an hour under a full team, and the walk stops at the first cached
|
|
155
|
+
* verdict anyway (every code merge seeds one), so this bound only caps the rev-list on a cold
|
|
156
|
+
* cache. */
|
|
160
157
|
const INHERIT_WALK_MAX = 64;
|
|
161
|
-
/** A green for `sha` carried over from its nearest verdict-bearing first-parent ancestor, or
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* and the caller runs the suite as before. */
|
|
158
|
+
/** A green for `sha` carried over from its nearest verdict-bearing first-parent ancestor, or null
|
|
159
|
+
* when there is none to carry. The nearest ancestor with a cached verdict must be GREEN, and the
|
|
160
|
+
* tree diff from it to `sha` must touch only `exemptPaths` (config.review.exemptPaths, the same
|
|
161
|
+
* patterns that let a doc-only change merge with no build check at all). Under that policy a doc
|
|
162
|
+
* commit cannot change the suite's verdict, so re-running the suite on every doc-only move of main
|
|
163
|
+
* buys nothing. Before this, 82% of the baseline runs (260 of 319 over 2026-10-02..12) ran on a
|
|
164
|
+
* main whose tip was doc commits on top of a code merge that had already seeded green. The nearest
|
|
165
|
+
* RED stops the walk: a doc commit after a red may be the fix, so it is re-checked, never inherited
|
|
166
|
+
* past. An uncached ancestor is walked through, since the single diff from the green covers it.
|
|
167
|
+
* Never throws: any git failure means no inheritance, and the caller runs the suite as before. */
|
|
172
168
|
async function inheritedGreen(wt, sha, exemptPaths) {
|
|
173
169
|
const ancestors = gitLines(await gitTry(wt, "rev-list", "--first-parent", `--max-count=${INHERIT_WALK_MAX}`, `${sha}^`));
|
|
174
170
|
for (const ancestor of ancestors) {
|
|
@@ -191,14 +187,13 @@ function exemptPathsOf(config) {
|
|
|
191
187
|
const patterns = config.review?.exemptPaths;
|
|
192
188
|
return Array.isArray(patterns) ? patterns.filter((p) => typeof p === "string" && p !== "") : [];
|
|
193
189
|
}
|
|
194
|
-
/** The cached verdict for `sha` WITHOUT running anything — "green", "red", or undefined when
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* checkMainBaseline's alone. */
|
|
190
|
+
/** The cached verdict for `sha` WITHOUT running anything — "green", "red", or undefined when this
|
|
191
|
+
* process holds none. The redeployer's urgency carve-out consults it first (BUGS.md 2026-09-30): a
|
|
192
|
+
* verdict already in hand answers the RUNNING build's redness for free — the warm path, a red
|
|
193
|
+
* observed here while that SHA was still main's tip. Undefined — a cold cache, e.g. a fresh
|
|
194
|
+
* orchestrator that inherited a stale build — sends the caller to run the check itself
|
|
195
|
+
* (redeploy.ts's witness worktree at the build SHA), which is recorded in this same cache and
|
|
196
|
+
* answers every later consult. Read-only: deciding a SHA's verdict is checkMainBaseline's alone. */
|
|
202
197
|
export function cachedBaselineVerdict(sha) {
|
|
203
198
|
return baselineCache.get(sha)?.status;
|
|
204
199
|
}
|
|
@@ -219,8 +214,8 @@ onRun,
|
|
|
219
214
|
/** Ignore a cached RED verdict and run the suite here instead (a cached green still short-
|
|
220
215
|
* circuits — see baselineCache). For the caller whose false block is expensive enough to pay
|
|
221
216
|
* one extra run: the redeploy gate, where trusting another worktree's environmental red
|
|
222
|
-
* strands the whole
|
|
223
|
-
* the SHA
|
|
217
|
+
* strands the whole team on a stale build until main moves. A green from the re-run promotes
|
|
218
|
+
* the SHA team-wide, which unblocks the role loops too. */
|
|
224
219
|
reverifyRed = false,
|
|
225
220
|
/** Ignore the cache entirely and run the suite here, even over a GREEN verdict. For the one
|
|
226
221
|
* caller that cannot trust a green (BUGS.md 2026-10-06: the review gate re-attributing a
|
|
@@ -273,19 +268,11 @@ observedOnly = false) {
|
|
|
273
268
|
// withCheckPermit): this runs the suite directly, so it must count against the cap too.
|
|
274
269
|
// Only the one run per key holds a permit; deduped waiters above await `pending` holding
|
|
275
270
|
// none.
|
|
276
|
-
const { outcome, durationMs } = await withCheckPermit(config, CHECK_TIER.other,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
// Weather (a late deadline, a foreign kill, a host sleep) made no verdict about the tree:
|
|
282
|
-
// the first attempt is priced as its own run, and the retry's outcome stands — the same
|
|
283
|
-
// rule runScopedBuildCheck applies at every other scope.
|
|
284
|
-
onRun?.({ outcome: first, durationMs: Date.now() - startedAt });
|
|
285
|
-
const retryStart = Date.now();
|
|
286
|
-
const retry = await runBuildCheck(wt, check, undefined, undefined, sampleSleep);
|
|
287
|
-
return { outcome: retry, durationMs: Date.now() - retryStart };
|
|
288
|
-
});
|
|
271
|
+
const { outcome, durationMs } = await withCheckPermit(config, CHECK_TIER.other, () => runWithCleanRetry(() => runBuildCheck(wt, check, undefined, undefined, sampleSleep),
|
|
272
|
+
// Weather (a late deadline, a foreign kill, a host sleep) made no verdict about the
|
|
273
|
+
// tree: the first attempt is priced as its own run, and the retry's outcome stands —
|
|
274
|
+
// the same rule runScopedBuildCheck applies at every other scope.
|
|
275
|
+
(first, dur) => onRun?.({ outcome: first, durationMs: dur })));
|
|
289
276
|
onRun?.({ outcome, durationMs });
|
|
290
277
|
if (outcome.status === "skipped") {
|
|
291
278
|
// Environmental (timeout/no-npm), after the clean retry above for weather shapes:
|
|
@@ -295,7 +282,7 @@ observedOnly = false) {
|
|
|
295
282
|
if (outcome.status === "failed" && (outcome.run?.sleptMs ?? 0) > SLEEP_SPAN_TOLERANCE_MS) {
|
|
296
283
|
// A failure the host slept through made no verdict about the tree either (BUGS.md
|
|
297
284
|
// 2026-09-30): the sleep expired a test's own wall-clock wait at the wake and the suite
|
|
298
|
-
// exited 1 inside the deadline. Caching it red would make sleep weather the
|
|
285
|
+
// exited 1 inside the deadline. Caching it red would make sleep weather the team's
|
|
299
286
|
// authoritative red main, so nothing is cached — the next consult re-runs, and a clean
|
|
300
287
|
// verdict then settles the SHA exactly as before.
|
|
301
288
|
return { baseline: null, run: outcome.run, unverified: true };
|
|
@@ -311,9 +298,9 @@ observedOnly = false) {
|
|
|
311
298
|
outputTail: outcome.outputTail,
|
|
312
299
|
redFrom: priorRedFrom.includes(wt) ? priorRedFrom : [...priorRedFrom, wt],
|
|
313
300
|
};
|
|
314
|
-
// A green always
|
|
315
|
-
// green — a concurrent run may have promoted this SHA while this one was still
|
|
316
|
-
// inherited green is no observation, so this run's verdict replaces it either way.
|
|
301
|
+
// A green is always recorded (promoting a provisional red); a red never overwrites an
|
|
302
|
+
// observed green — a concurrent run may have promoted this SHA while this one was still
|
|
303
|
+
// going. An inherited green is no observation, so this run's verdict replaces it either way.
|
|
317
304
|
const current = baselineCache.get(sha);
|
|
318
305
|
if (current?.status !== "green" || current.inheritedFrom !== undefined)
|
|
319
306
|
rememberBaseline(sha, baseline);
|
|
@@ -329,19 +316,19 @@ observedOnly = false) {
|
|
|
329
316
|
baselineInFlight.delete(key);
|
|
330
317
|
}
|
|
331
318
|
}
|
|
332
|
-
/** Is main green at the mirror worktree's HEAD? The
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
319
|
+
/** Is main green at the mirror worktree's HEAD? The merge path seeds a green verdict for every
|
|
320
|
+
* merged code SHA (noteGreenBaseline — its in-lock post-rebase re-check, or the gate's pre-check
|
|
321
|
+
* when the rebase was a no-op), so the common case is a cache hit. A head that only added doc
|
|
322
|
+
* commits on top of such a SHA inherits its green (checkMainBaseline's inheritedGreen). The
|
|
323
|
+
* redeployer's cooldown pre-warm asks this for every new head, and those doc heads were most of the
|
|
324
|
+
* harness's baseline runs (247 of 319 over 2026-10-02..12). Otherwise the project's declared check
|
|
325
|
+
* runs once in the mirror. No declared check or an environmental skip reads as green — the gates'
|
|
326
|
+
* warn-and-proceed policy.
|
|
340
327
|
*
|
|
341
328
|
* A cached RED, though, is re-verified here (checkMainBaseline's `reverifyRed`) instead of
|
|
342
329
|
* being taken as given. A red verdict can belong to the worktree that produced it rather than
|
|
343
330
|
* to the tree, and the redeploy gate that asks this is the one place where believing a wrong
|
|
344
|
-
* red is expensive: it strands the whole
|
|
331
|
+
* red is expensive: it strands the whole team on a stale build with no retry until main
|
|
345
332
|
* moves. One extra suite run per red head buys that, and a green from it promotes the SHA for
|
|
346
333
|
* every other gate too. */
|
|
347
334
|
export async function mainIsGreen(mirrorWt,
|
|
@@ -349,7 +336,7 @@ export async function mainIsGreen(mirrorWt,
|
|
|
349
336
|
* §6/7), so a configured command makes the green check run on a non-npm repo too; its
|
|
350
337
|
* maxConcurrentChecks sizes the check permit this run takes. */
|
|
351
338
|
config,
|
|
352
|
-
/** Hook for the build_check event — this run is a minute of the
|
|
339
|
+
/** Hook for the build_check event — this run is a minute of the team's time and belongs in
|
|
353
340
|
* the feed like the role loops' own baseline checks. */
|
|
354
341
|
onRun) {
|
|
355
342
|
const check = await checkMainBaseline(mirrorWt, config, onRun, true);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import { baselineBlocked } from "../roles/roles.js";
|
|
2
3
|
import { isCustomRole, liveConfig } from "../config/config.js";
|
|
3
4
|
import { BUILD_CHECK_TIMEOUT_MS } from "../build/build-check-detect.js";
|
|
@@ -5,7 +6,7 @@ import { failureHeadline } from "../build/build-check-report.js";
|
|
|
5
6
|
import { baselineCheckEventLogger, buildCheckSkipWarning, sleptPhrase } from "../build/build-check-events.js";
|
|
6
7
|
import { checkMainBaseline } from "./main-baseline.js";
|
|
7
8
|
import { buildMainRedNote } from "../prompt/gate-prompts.js";
|
|
8
|
-
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
9
|
+
import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
9
10
|
import { errorMessage } from "../text/text.js";
|
|
10
11
|
import { mainRedPhrase } from "../text/phrases.js";
|
|
11
12
|
import { branchHead } from "../git/git.js";
|
|
@@ -18,14 +19,15 @@ import { ensureDetachedWorktree } from "../git/worktree.js";
|
|
|
18
19
|
* and "Tests red on main" entries), so while a SHA is known red the blocked roles skip authoring
|
|
19
20
|
* instead of burning runs that are guaranteed to fail. Split out of loop.ts — which keeps the
|
|
20
21
|
* tick lifecycle around it — because this is a self-contained policy with its own per-process
|
|
21
|
-
* state (one harness-level
|
|
22
|
-
* it borrows from the loop is identity and the role's worktree, and it
|
|
22
|
+
* state (one harness-level `main_red_hold` event per newly-discovered red SHA) and its own
|
|
23
|
+
* event contract; what it borrows from the loop is identity and the role's worktree, and it
|
|
24
|
+
* consumes
|
|
23
25
|
* build/build-check.ts's checkMainBaseline machinery (detection + execution + per-SHA cache),
|
|
24
26
|
* which stays unaware of ticks. */
|
|
25
|
-
/** The last main SHA for which this process logged a red-main
|
|
26
|
-
*
|
|
27
|
-
* module-level so every runner in the
|
|
28
|
-
*
|
|
27
|
+
/** The last main SHA for which this process logged a red-main hold event (the baseline check): one
|
|
28
|
+
* harness-level warning per newly-discovered red SHA, not one per blocked role's tick —
|
|
29
|
+
* module-level so every runner in the team shares it. A restart re-logs once: the cache is cold
|
|
30
|
+
* then too, and an operator restarting into a still-red main should see why nothing merges. */
|
|
29
31
|
let lastMainRedSha = null;
|
|
30
32
|
/** The red-main message's shared spine — the SHA, script, and failure headline a red baseline
|
|
31
33
|
* carries, phrased once so the once-per-SHA warning and the main_red outcome cannot drift apart
|
|
@@ -34,26 +36,32 @@ function redMainMessage(red, action) {
|
|
|
34
36
|
const firstLine = failureHeadline(red.outputTail);
|
|
35
37
|
return `${mainRedPhrase(red.sha)} (${red.script}${firstLine ? `: ${firstLine}` : ""}) — ${action}`;
|
|
36
38
|
}
|
|
37
|
-
/** Log the
|
|
38
|
-
* (with lastMainRedSha) so the gate and the bugfix
|
|
39
|
-
* the red first logs it, the other stays silent. */
|
|
39
|
+
/** Log the team-wide `main_red_hold` event (the notify hook pages it) for `red`'s SHA at
|
|
40
|
+
* most once per process. Module-level (with lastMainRedSha) so the gate and the bugfix
|
|
41
|
+
* handoff share one guard: whichever observes the red first logs it, the other stays silent. */
|
|
40
42
|
function warnMainRedOnce(root, red) {
|
|
41
43
|
if (lastMainRedSha === red.sha)
|
|
42
44
|
return;
|
|
43
45
|
lastMainRedSha = red.sha;
|
|
44
|
-
|
|
46
|
+
logEventBestEffort(root, {
|
|
47
|
+
source: TUMWATER_SOURCE,
|
|
48
|
+
type: "main_red_hold",
|
|
49
|
+
sha: red.sha,
|
|
50
|
+
script: red.script,
|
|
51
|
+
message: redMainMessage(red, "code merges blocked until main is green"),
|
|
52
|
+
});
|
|
45
53
|
}
|
|
46
54
|
/** checkMainBaseline's per-run hook: log the one run per SHA (cache misses only) under the role
|
|
47
55
|
* that paid for it, with its duration — the gate's build_check sibling, so both halves of the
|
|
48
|
-
*
|
|
56
|
+
* team's deterministic verification are priced in the feed. The one logger for every baseline
|
|
49
57
|
* check this module runs — the bugfix handoff, the authoring gate, and mainTipVerdict's tip
|
|
50
|
-
* check (which review.ts's gate and
|
|
58
|
+
* check (which review.ts's gate and merge-check-failures.ts's attributeRedCheck reach
|
|
51
59
|
* through it) — so the event's shape cannot drift between them. */
|
|
52
60
|
function baselineCheckLogger(root, role) {
|
|
53
61
|
return baselineCheckEventLogger(role, (event) => logEventBestEffort(root, event));
|
|
54
62
|
}
|
|
55
63
|
/** Red-main handoff for the `bugfix` healer: mainRedGate exempts
|
|
56
|
-
* bugfix so the only role that can unblock the
|
|
64
|
+
* bugfix so the only role that can unblock the team may author, but its prompt then starts from
|
|
57
65
|
* BUGS.md and knows nothing about the red suite. This runs the same per-SHA baseline check
|
|
58
66
|
* (cache and provisional-red re-verification reused unchanged, so it costs at most the one run
|
|
59
67
|
* the gate would have paid for) and, when main is red, returns the note that points the healer
|
|
@@ -68,7 +76,7 @@ export async function bugfixMainRedNote(root, role, wt) {
|
|
|
68
76
|
if (red?.status !== "red")
|
|
69
77
|
return undefined;
|
|
70
78
|
warnMainRedOnce(root, red);
|
|
71
|
-
return buildMainRedNote(red.sha, red.script, failureHeadline(red.outputTail));
|
|
79
|
+
return buildMainRedNote(red.sha, red.script, failureHeadline(red.outputTail), red.outputTail);
|
|
72
80
|
}
|
|
73
81
|
/** Gate one fresh tick on main's baseline. Returns null when authoring may proceed — role not
|
|
74
82
|
* blocked, no declared check, green, or an environmental skip (warned under the role and
|
|
@@ -104,7 +112,7 @@ sampleSleep) {
|
|
|
104
112
|
return {
|
|
105
113
|
result: "main_red",
|
|
106
114
|
summary: "code merges blocked until main is green",
|
|
107
|
-
// The cause rides the outcome so the tick's `
|
|
115
|
+
// The cause rides the outcome so the tick's `run_end` carries it
|
|
108
116
|
// (tick-finalize.ts folds it into state.lastError before logging): the digest itemizes
|
|
109
117
|
// the outcome that blocks every merge, instead of leaving it a bare count behind a
|
|
110
118
|
// once-per-SHA warning (BUGS.md 2026-09-28).
|
|
@@ -113,25 +121,25 @@ sampleSleep) {
|
|
|
113
121
|
}
|
|
114
122
|
return null;
|
|
115
123
|
}
|
|
116
|
-
/** Serializes mainTipVerdict's use of its one worktree: the
|
|
124
|
+
/** Serializes mainTipVerdict's use of its one worktree: the merge pipeline's verifies run their
|
|
117
125
|
* gates concurrently, and a second gate re-pointing the checkout at a newer main while the first's
|
|
118
|
-
* check ran in it would measure a tree that is neither. Each link is bounded — a few git
|
|
119
|
-
*
|
|
120
|
-
*
|
|
126
|
+
* check ran in it would measure a tree that is neither. Each link is bounded — a few git commands
|
|
127
|
+
* plus at most one check run, which runBuildCheck kills at its timeout — and takes no other lock,
|
|
128
|
+
* so a waiter waits at most for the gates queued ahead of it. */
|
|
121
129
|
let gateMainQueue = Promise.resolve();
|
|
122
130
|
/** Main's baseline verdict at its current tip, for attributing a gate check that failed twice
|
|
123
131
|
* (src/review/review.ts): gateMainWorktreePath is re-pointed at `mainBranch`'s tip and asked
|
|
124
|
-
* through checkMainBaseline — the same per-SHA,
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* throws: an unreadable main, a checkout that fails, no
|
|
129
|
-
* run all read as `unavailable`.
|
|
132
|
+
* through checkMainBaseline — the same per-SHA, team-wide cache mainRedGate reads, which every
|
|
133
|
+
* merge seeds green for the SHA it moved main to, so the common case is a cache hit. A miss (or a
|
|
134
|
+
* provisional red from another worktree, or a green that was only inherited across doc commits)
|
|
135
|
+
* runs the declared check once here, bounded by its timeout. A red is warned team-wide once per
|
|
136
|
+
* SHA, exactly as mainRedGate warns it. Never throws: an unreadable main, a checkout that fails, no
|
|
137
|
+
* declared check on main, or a skipped run all read as `unavailable`.
|
|
130
138
|
*
|
|
131
|
-
* `forceFresh` bypasses that per-SHA cache and runs main's declared check now, even over a
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
139
|
+
* `forceFresh` bypasses that per-SHA cache and runs main's declared check now, even over a cached
|
|
140
|
+
* green (checkMainBaseline's forceFresh): the review gate needs it to re-attribute a repeated gate
|
|
141
|
+
* failure on a clock- or load-sensitive test whose cached green has gone stale (BUGS.md
|
|
142
|
+
* 2026-10-06). The merge pipeline's attribution reads the cached verdict instead. */
|
|
135
143
|
export function mainTipVerdict(root, role, mainBranch, config, forceFresh = false) {
|
|
136
144
|
const run = gateMainQueue.then(() => verdictAtMainTip(root, role, mainBranch, config, forceFresh));
|
|
137
145
|
gateMainQueue = run.catch(() => undefined);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { INITIAL_PROMPT_MAX_CHARS, PROMPT_END, PROMPT_START, briefPath, replaceInitialPrompt } from "./brief.js";
|
|
5
|
+
import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
|
|
6
|
+
import { withStateLock } from "../concurrency/lock.js";
|
|
7
|
+
import { writeTextAtomic } from "../files/files.js";
|
|
8
|
+
import { stampedSubject } from "../git/commit-message.js";
|
|
9
|
+
import { primaryWorktreeLockPath } from "../paths.js";
|
|
10
|
+
import { PREFILL_REASON } from "../text/phrases.js";
|
|
11
|
+
import { tooLongMessage } from "../text/text.js";
|
|
12
|
+
const NO_BRIEF = "no project brief with a prompt block found — run `tumwater init` first";
|
|
13
|
+
/** Replace the initial prompt with `text` under the checks init applies (non-empty, at most
|
|
14
|
+
* INITIAL_PROMPT_MAX_CHARS, no prompt marker), write it under the primary-worktree lock, and
|
|
15
|
+
* commit that one file on main. The one writer behind `tumwater brief --file` and the
|
|
16
|
+
* dashboard's Settings card, so the two cannot drift. */
|
|
17
|
+
export async function editInitialPrompt(root, text) {
|
|
18
|
+
const prompt = text.trim();
|
|
19
|
+
if (prompt === "")
|
|
20
|
+
return { ok: false, error: "the new brief is empty" };
|
|
21
|
+
if (prompt.length > INITIAL_PROMPT_MAX_CHARS)
|
|
22
|
+
return { ok: false, error: tooLongMessage("the new brief", prompt.length, INITIAL_PROMPT_MAX_CHARS, PREFILL_REASON) };
|
|
23
|
+
if (prompt.includes(PROMPT_START) || prompt.includes(PROMPT_END))
|
|
24
|
+
return { ok: false, error: "the new brief contains a tumwater:prompt marker, which would break the block for every reader" };
|
|
25
|
+
const file = briefPath(root);
|
|
26
|
+
if (file === null)
|
|
27
|
+
return { ok: false, error: NO_BRIEF };
|
|
28
|
+
const name = path.basename(file);
|
|
29
|
+
const changed = withStateLock(primaryWorktreeLockPath(root), () => {
|
|
30
|
+
const current = fs.readFileSync(file, "utf8");
|
|
31
|
+
const next = replaceInitialPrompt(current, prompt);
|
|
32
|
+
if (next === null)
|
|
33
|
+
return `${name} no longer carries a prompt block`;
|
|
34
|
+
if (next === current)
|
|
35
|
+
return false;
|
|
36
|
+
writeTextAtomic(file, next);
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
if (typeof changed === "string")
|
|
40
|
+
return { ok: false, error: changed };
|
|
41
|
+
const committed = changed && (await commitOperatorBacklogEdit(root, name, stampedSubject("operator", "Edit brief"))); // vocabulary-ok: commit prefix stays
|
|
42
|
+
return { ok: true, file: name, changed, committed };
|
|
43
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
|
-
import { briefCandidates } from "
|
|
4
|
-
import { errorMessage, truncateWithNote } from "
|
|
4
|
+
import { briefCandidates } from "../paths.js";
|
|
5
|
+
import { errorMessage, truncateWithNote } from "../text/text.js";
|
|
5
6
|
/** The managed sections of the project brief (TUMWATER.md, with README.md as the
|
|
6
7
|
* compatibility path — plans/portability.md §7a/7): marker constants, the templates a fresh
|
|
7
8
|
* repo gets, and reading the project's initial prompt back out. */
|
|
@@ -9,7 +10,7 @@ export const PROMPT_START = "<!-- tumwater:prompt:start -->";
|
|
|
9
10
|
export const PROMPT_END = "<!-- tumwater:prompt:end -->";
|
|
10
11
|
/** Cap on the initial prompt (see readInitialPrompt). The prompt is the project's reason to
|
|
11
12
|
* exist and rides into EVERY tick and director prompt, so unbounded text would be a standing
|
|
12
|
-
* per-tick prefill cost — the same budget
|
|
13
|
+
* per-tick prefill cost — the same budget customRoles.task and roles.<id>.instructions are
|
|
13
14
|
* held to. `tumwater init` rejects an over-long prompt before it is committed (init.ts); this
|
|
14
15
|
* constant is the shared bound so the read path and the init check cannot drift. */
|
|
15
16
|
export const INITIAL_PROMPT_MAX_CHARS = 4096;
|
|
@@ -18,6 +19,11 @@ export const INITIAL_PROMPT_MAX_CHARS = 4096;
|
|
|
18
19
|
// prompt markers above stay exported because init.ts names them in its error message.
|
|
19
20
|
export const STATUS_START = "<!-- tumwater:status:start -->";
|
|
20
21
|
export const STATUS_END = "<!-- tumwater:status:end -->";
|
|
22
|
+
/** The placeholder every fresh template seeds between the status markers. A brief still
|
|
23
|
+
* carrying exactly this body (or an empty one) is "description incomplete"; once the readme
|
|
24
|
+
* role replaces it with a real status the tight-loop rule turns itself off. Hoisted so the
|
|
25
|
+
* template and briefStatusIncomplete cannot drift on the wording. */
|
|
26
|
+
export const EMPTY_STATUS = "_No status yet. The readme role keeps this section up to date._";
|
|
21
27
|
/** The two managed sections a fresh brief carries — the initial prompt in its prompt
|
|
22
28
|
* markers, an empty status in its status markers — under the given H1. Both homes render
|
|
23
29
|
* the identical body (README.md for a fresh repo, TUMWATER.md for an adopted one), so the
|
|
@@ -26,7 +32,7 @@ export const STATUS_END = "<!-- tumwater:status:end -->";
|
|
|
26
32
|
function managedSections(heading, initialPrompt) {
|
|
27
33
|
return `# ${heading}
|
|
28
34
|
|
|
29
|
-
##
|
|
35
|
+
## Brief
|
|
30
36
|
|
|
31
37
|
${PROMPT_START}
|
|
32
38
|
${initialPrompt.trim()}
|
|
@@ -35,7 +41,7 @@ ${PROMPT_END}
|
|
|
35
41
|
## Status
|
|
36
42
|
|
|
37
43
|
${STATUS_START}
|
|
38
|
-
|
|
44
|
+
${EMPTY_STATUS}
|
|
39
45
|
${STATUS_END}
|
|
40
46
|
`;
|
|
41
47
|
}
|
|
@@ -67,7 +73,7 @@ function ownsBrief(text) {
|
|
|
67
73
|
* A candidate that exists but cannot be read (README.md created as a directory, a
|
|
68
74
|
* permission-lost file) is an operator-visible break, not a silent skip: the brief is
|
|
69
75
|
* every loop's reason to exist, read each tick, so falling through to the next candidate
|
|
70
|
-
* could run the
|
|
76
|
+
* could run the team on the wrong brief and throwing raw would crash every tick with an
|
|
71
77
|
* errno that names neither the file nor the fix — the message below names both. */
|
|
72
78
|
function firstBriefText(root, accepts) {
|
|
73
79
|
for (const candidate of briefCandidates(root)) {
|
|
@@ -78,7 +84,7 @@ function firstBriefText(root, accepts) {
|
|
|
78
84
|
text = fs.readFileSync(candidate, "utf8");
|
|
79
85
|
}
|
|
80
86
|
catch (err) {
|
|
81
|
-
throw new Error(`cannot read the project brief ${JSON.stringify(path.basename(candidate))}: ${errorMessage(err)} — every
|
|
87
|
+
throw new Error(`cannot read the project brief ${JSON.stringify(path.basename(candidate))}: ${errorMessage(err)} — every role reads this file's brief on each run, so make it a readable file carrying the ${PROMPT_START}/${PROMPT_END} markers (or move it aside)`);
|
|
82
88
|
}
|
|
83
89
|
if (accepts(text))
|
|
84
90
|
return { file: path.basename(candidate), text };
|
|
@@ -92,6 +98,25 @@ function firstBriefText(root, accepts) {
|
|
|
92
98
|
export function briefFile(root) {
|
|
93
99
|
return readBrief(root)?.file ?? null;
|
|
94
100
|
}
|
|
101
|
+
/** False when there is no resolved brief, or the resolved brief carries no well-formed
|
|
102
|
+
* STATUS_START/STATUS_END pair (a malformed brief is doctor's warning, not a tight loop);
|
|
103
|
+
* otherwise true when the status body between the markers is empty or still the EMPTY_STATUS
|
|
104
|
+
* placeholder. The one machine-detectable "description not yet complete" signal: the initial
|
|
105
|
+
* prompt half of the description is always present (init rejects an empty one). One small
|
|
106
|
+
* single-file read, like readInitialPrompt. */
|
|
107
|
+
export function briefStatusIncomplete(root) {
|
|
108
|
+
const found = firstBriefText(root, ownsBrief);
|
|
109
|
+
if (!found)
|
|
110
|
+
return false;
|
|
111
|
+
const bodyStart = found.text.indexOf(STATUS_START);
|
|
112
|
+
if (bodyStart < 0)
|
|
113
|
+
return false;
|
|
114
|
+
const bodyEnd = found.text.indexOf(STATUS_END, bodyStart + STATUS_START.length);
|
|
115
|
+
if (bodyEnd < 0)
|
|
116
|
+
return false;
|
|
117
|
+
const body = found.text.slice(bodyStart + STATUS_START.length, bodyEnd).trim();
|
|
118
|
+
return body === "" || body === EMPTY_STATUS;
|
|
119
|
+
}
|
|
95
120
|
/** The project's initial prompt, extracted from the resolved brief file's managed section —
|
|
96
121
|
* TUMWATER.md when it exists with markers, else README.md (plans/portability.md §7a/7). The
|
|
97
122
|
* closing marker is searched for only AFTER the opening one: an end marker mentioned in prose
|
|
@@ -101,19 +126,45 @@ export function briefFile(root) {
|
|
|
101
126
|
export function readInitialPrompt(root) {
|
|
102
127
|
return readBrief(root)?.prompt ?? "";
|
|
103
128
|
}
|
|
104
|
-
|
|
129
|
+
/** The full path of the file that owns the prompt block (the same candidate readBrief
|
|
130
|
+
* resolves), or null when none does — so a writer edits the file every reader reads. */
|
|
131
|
+
export function briefPath(root) {
|
|
132
|
+
const found = firstBriefText(root, ownsBrief);
|
|
133
|
+
return found ? path.join(root, found.file) : null;
|
|
134
|
+
}
|
|
135
|
+
/** `text` with the prompt block's content replaced by `prompt` (trimmed), every byte outside
|
|
136
|
+
* the two markers kept; null when the text has no well-formed block. Finds the block by
|
|
137
|
+
* extractPrompt's rule (first start marker, then the first end marker after it). */
|
|
138
|
+
export function replaceInitialPrompt(text, prompt) {
|
|
139
|
+
const start = text.indexOf(PROMPT_START);
|
|
140
|
+
if (start < 0)
|
|
141
|
+
return null;
|
|
142
|
+
const bodyStart = start + PROMPT_START.length;
|
|
143
|
+
const end = text.indexOf(PROMPT_END, bodyStart);
|
|
144
|
+
if (end < 0)
|
|
145
|
+
return null;
|
|
146
|
+
return `${text.slice(0, bodyStart)}\n${prompt.trim()}\n${text.slice(end)}`;
|
|
147
|
+
}
|
|
148
|
+
/** The trimmed, untruncated text between the first start marker and the first end marker after
|
|
149
|
+
* it; null when `text` has no well-formed block. */
|
|
150
|
+
export function promptBlockOf(text) {
|
|
105
151
|
const start = text.indexOf(PROMPT_START);
|
|
106
152
|
if (start < 0)
|
|
107
153
|
return null;
|
|
108
154
|
const end = text.indexOf(PROMPT_END, start + PROMPT_START.length);
|
|
109
155
|
if (end < 0)
|
|
110
156
|
return null;
|
|
111
|
-
|
|
157
|
+
return text.slice(start + PROMPT_START.length, end).trim();
|
|
158
|
+
}
|
|
159
|
+
function extractPrompt(text) {
|
|
160
|
+
const prompt = promptBlockOf(text);
|
|
161
|
+
if (prompt === null)
|
|
162
|
+
return null;
|
|
112
163
|
// A hand-edited brief can carry an over-long prompt past init's length check. Truncate it
|
|
113
164
|
// for every tick instead of injecting the whole thing (the same defensive cap readPrinciples
|
|
114
165
|
// applies, through text.ts's truncateWithNote), with a visible note so the loss is not
|
|
115
166
|
// silent. init rejects the normal path before it is committed, so this is the backstop.
|
|
116
|
-
return truncateWithNote(prompt, INITIAL_PROMPT_MAX_CHARS, "
|
|
167
|
+
return truncateWithNote(prompt, INITIAL_PROMPT_MAX_CHARS, "brief");
|
|
117
168
|
}
|
|
118
169
|
/** The resolved brief as one value: the owning file's basename (briefFile) and its extracted
|
|
119
170
|
* initial prompt (readInitialPrompt) from a SINGLE firstBriefText scan. Tick prompt assembly
|
|
@@ -125,3 +176,8 @@ export function readBrief(root) {
|
|
|
125
176
|
const found = firstBriefText(root, ownsBrief);
|
|
126
177
|
return found ? { file: found.file, prompt: extractPrompt(found.text) ?? "" } : null;
|
|
127
178
|
}
|
|
179
|
+
/** The brief's plain-text form: its file name, then the initial prompt — what `tumwater brief`
|
|
180
|
+
* prints and the TUI's Brief tab shows, so the two cannot drift. */
|
|
181
|
+
export function renderBrief(brief) {
|
|
182
|
+
return `${brief.file}\n${brief.prompt}`;
|
|
183
|
+
}
|