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,46 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** Two of the three halves of a merge (plans/merge-queue.md 5/5, PLANS.md land-queue speed
|
|
3
|
+
* 2c): the per-change verify (verifyRequest) that merge-verify.ts's verify stage runs for every
|
|
4
|
+
* queued change, and the merge (mergeApproved) its one merge slot runs over the approved ones — a
|
|
4
5
|
* stack of two or more sharing a single build check, with a per-change fallback. The stack
|
|
5
|
-
*
|
|
6
|
-
* beside it in
|
|
7
|
-
* one-change
|
|
6
|
+
* merger mergeApproved drives (assemble → one shared check → ff, with re-stack and bisect) lives
|
|
7
|
+
* beside it in merge-stack.ts. The shared review gate (reviewPinnedChange) and the
|
|
8
|
+
* one-change merge (mergeApprovedChange) live beside both in merge-core.ts. */
|
|
8
9
|
import { leaseSlot } from "../git/worktree-pool.js";
|
|
9
10
|
import { slotCount } from "../config/config.js";
|
|
10
11
|
import { mergeWorktreePath } from "../paths.js";
|
|
11
|
-
import {
|
|
12
|
-
import { attributeRedCheck } from "./
|
|
12
|
+
import { mergeApprovedChange, reviewPinnedChange, syncPinToMain, } from "./merge-core.js";
|
|
13
|
+
import { attributeRedCheck } from "./merge-check-failures.js";
|
|
13
14
|
import { errorMessage } from "../text/text.js";
|
|
14
|
-
import {
|
|
15
|
-
/**
|
|
15
|
+
import { mergeStack } from "./merge-stack.js";
|
|
16
|
+
/** Verify one request — the whole of a verify-stage task: lease a pooled slot for the role, check
|
|
16
17
|
* its pin out detached there, rebase it onto main's current tip (syncPinToMain — so the head
|
|
17
18
|
* approved here is the head its merge starts from, and a conflict leaves the pin for the gate
|
|
18
19
|
* and, at the merge, mergeToMain's resolver), and run the review gate, the verdict persisted
|
|
19
20
|
* immediately by reviewPinnedChange (the drain's write-back of an approved change happens only
|
|
20
|
-
* once it
|
|
21
|
-
* outcome (plans/worktree-pool.md, "Vets and merges"): the
|
|
22
|
-
*
|
|
21
|
+
* once it merges, so a crash must not lose what the verify earned). The lease is released in every
|
|
22
|
+
* outcome (plans/worktree-pool.md, "Vets and merges"): the verify passes its result on through the
|
|
23
|
+
* merge ref and ApprovedMerge, never through the slot, and every merge-side step re-ensures at
|
|
23
24
|
* a commit. leaseSlot holds the slot through the disk-floor registry and resets it to the pin, so
|
|
24
|
-
* two
|
|
25
|
-
export async function
|
|
25
|
+
* two verifies rebasing at once — or one rebasing while a merge moves main — cannot interfere. */
|
|
26
|
+
export async function verifyRequest(ctx, req, w) {
|
|
26
27
|
let lease;
|
|
27
28
|
try {
|
|
28
29
|
lease = await leaseSlot(ctx.root, {
|
|
29
30
|
role: req.role,
|
|
30
|
-
purpose: "
|
|
31
|
+
purpose: "verify",
|
|
31
32
|
ref: req.sha,
|
|
32
33
|
signal: ctx.signal(),
|
|
33
34
|
// The live view's pool budget, same rule as the tick path: a broken tumwater.json
|
|
34
|
-
// must not error the
|
|
35
|
+
// must not error the verify before its reviewer runs.
|
|
35
36
|
slotBudget: slotCount(ctx.config),
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
catch (err) {
|
|
39
|
-
// An abort while waiting for a free slot is the
|
|
40
|
+
// An abort while waiting for a free slot is the verify never starting — the same terminal
|
|
40
41
|
// "aborted" reviewPinnedChange returns when it observes an already-fired signal.
|
|
41
42
|
if (err instanceof Error && err.name === "AbortError")
|
|
42
43
|
return { kind: "result", result: "aborted" };
|
|
43
|
-
// The queue entry outlived its pinned commit — the
|
|
44
|
+
// The queue entry outlived its pinned commit — the merge queue outlives the ref by design
|
|
44
45
|
// (a crash between pin and drop, or an outside gc), so a checkout of `req.sha` can fail
|
|
45
46
|
// with the commit gone. Degrade to a terminal "error" for this request so the drain drops
|
|
46
47
|
// its entry and the queue advances; a throw would keep the entry, and a lost pin would then
|
|
@@ -50,13 +51,13 @@ export async function vetRequest(ctx, req, w) {
|
|
|
50
51
|
return { kind: "result", result: "error" };
|
|
51
52
|
}
|
|
52
53
|
try {
|
|
53
|
-
return await
|
|
54
|
+
return await verifyRequestIn(ctx, req, w, lease.dir);
|
|
54
55
|
}
|
|
55
56
|
finally {
|
|
56
57
|
lease.release();
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
async function
|
|
60
|
+
async function verifyRequestIn(ctx, req, w, wt) {
|
|
60
61
|
const synced = await syncPinToMain(ctx, wt, req);
|
|
61
62
|
const outcome = await reviewPinnedChange(ctx, synced, wt, w.state, w.foldUsage);
|
|
62
63
|
if (outcome.kind === "result")
|
|
@@ -66,67 +67,67 @@ async function vetRequestIn(ctx, req, w, wt) {
|
|
|
66
67
|
const { verifiedHead } = outcome.gate;
|
|
67
68
|
return verifiedHead === outcome.sha ? { kind: "stack", sha: outcome.sha, verifiedHead } : { kind: "stack", sha: outcome.sha };
|
|
68
69
|
}
|
|
69
|
-
/**
|
|
70
|
-
* (
|
|
71
|
-
* in queue order, each request at the head its
|
|
72
|
-
* none is gated again, so no model review runs here (an adversarial review of a stack would
|
|
73
|
-
* which change a criticism applies to, so each change was reviewed alone). The flow:
|
|
70
|
+
/** Merge changes whose own verify already approved them — the merge slot's whole task
|
|
71
|
+
* (merge-drain.ts, land-queue speed 2c), over every approved entry up to mergeBatchMax. `approved`
|
|
72
|
+
* is in queue order, each request at the head its verify approved (the synced pin its merge ref
|
|
73
|
+
* names); none is gated again, so no model review runs here (an adversarial review of a stack would
|
|
74
|
+
* blur which change a criticism applies to, so each change was reviewed alone). The flow:
|
|
74
75
|
*
|
|
75
|
-
* One change —
|
|
76
|
-
* re-check whenever main moved since its
|
|
77
|
-
* pre-check ran green on exactly that head).
|
|
76
|
+
* One change — merge it through mergeApprovedChange: git + ff, plus an in-lock scope-`merge`
|
|
77
|
+
* re-check whenever main moved since its verify (and a seeded baseline when it did not and its
|
|
78
|
+
* verify's pre-check ran green on exactly that head).
|
|
78
79
|
*
|
|
79
80
|
* Two or more — assemble the stack in the shared `_merge` checkout, checked out detached at
|
|
80
|
-
* main's CURRENT tip, then cherry-pick each change's full range from that tip to its head to
|
|
81
|
+
* main's CURRENT tip, then cherry-pick each change's full range from that tip to its head to merge,
|
|
81
82
|
* in queue order — `base..sha`, every commit ahead of main (normally one), capturing each
|
|
82
83
|
* post-pick tip — and run ONE scope-`batch` runScopedBuildCheck over the combined tree: the
|
|
83
|
-
* expensive, deterministic half the stack shares. null (no declared check) →
|
|
84
|
+
* expensive, deterministic half the stack shares. null (no declared check) → merge directly;
|
|
84
85
|
* "failed" (a red tree or a timeout remapped to a reject — the tree is unverified) → bisect;
|
|
85
86
|
* "skipped" (no npm / broken toolchain — the helper already warned) → proceed, never
|
|
86
87
|
* fail-closed; "passed" → green. Green: under the merge lock, ffStackToMain ff's main through the
|
|
87
88
|
* stack in ONE fast-forward, emits one `merged` event per change, and — only when the check
|
|
88
89
|
* PASSED on exactly that tip — seeds noteGreenBaseline with the stacked tip. ff failure (main
|
|
89
90
|
* moved while the check ran: the window is the whole check, and main still has writers outside the
|
|
90
|
-
*
|
|
91
|
+
* merge queue) → RE-STACK: assemble the same changes afresh on main's new tip and go round again,
|
|
91
92
|
* up to BATCH_RESTACK_ATTEMPTS times, stopping before any attempt on an abort. A re-stacked tree
|
|
92
93
|
* that differs from the last tree a check ran on only in review-exempt paths goes straight to the
|
|
93
94
|
* ff; any other re-stack pays one more check. Only a race lost on every attempt leaves each change
|
|
94
|
-
* not yet
|
|
95
|
+
* not yet merged its ref with "merge_blocked", nothing seeded, for leftover recovery.
|
|
95
96
|
*
|
|
96
|
-
* Red →
|
|
97
|
-
* step the same assemble → check → ff (
|
|
98
|
-
* — the first half of the ones the last red check ran over — so every prefix that
|
|
99
|
-
* on its own green check with nothing rewritten before its ff. A green prefix
|
|
100
|
-
* the red run is bisected next; a red one is halved. The one change a red check ran over alone
|
|
101
|
-
* attributed through main's own baseline (
|
|
97
|
+
* Red → MERGE THE LARGEST PASSING PREFIX: bisect in queue order, each
|
|
98
|
+
* step the same assemble → check → ff (mergeStack) over a prefix of the changes not yet merged
|
|
99
|
+
* — the first half of the ones the last red check ran over — so every prefix that merges, merges
|
|
100
|
+
* on its own green check with nothing rewritten before its ff. A green prefix merges and the rest
|
|
101
|
+
* of the red run is bisected next; a red one is halved. The one change a red check ran over alone
|
|
102
|
+
* is attributed through main's own baseline (merge-check-failures.ts's attributeRedCheck): main
|
|
102
103
|
* green → rejected with the check's reasons, no pi run; main red → "main_red", pin kept. Its
|
|
103
104
|
* red is the second one observed with it in the tree, so a single flaky run never rejects a change.
|
|
104
105
|
* The changes after it stay unattempted for the next merge. A stack of N with one broken change
|
|
105
106
|
* costs about log2(N) + 1 extra checks, never a second model review.
|
|
106
107
|
*
|
|
107
108
|
* Un-assemblable (a cherry-pick conflict, on the first stack or any prefix) → ABANDON the changes
|
|
108
|
-
* not yet
|
|
109
|
-
* through
|
|
109
|
+
* not yet merged to one-at-a-time, in queue order, stopping at the first non-terminal outcome, each
|
|
110
|
+
* through mergeApprovedChange — no second gate and no model run but mergeToMain's conflict
|
|
110
111
|
* resolver. main is never left red: the only bytes this path ff's are a checked tip or per-change
|
|
111
|
-
*
|
|
112
|
+
* merges re-verified in-lock whenever they differ from what their verify judged.
|
|
112
113
|
*
|
|
113
114
|
* An abort is observed between steps, not only by the pi runs it kills: before each
|
|
114
|
-
* assembly-and-check attempt and before each one-change or fallback
|
|
115
|
+
* assembly-and-check attempt and before each one-change or fallback merge, so a stopping merge
|
|
115
116
|
* ends at its next step boundary (BUGS.md 2026-09-23). A shared check that has already run is
|
|
116
117
|
* followed through: its fast-forward is the merge's bounded commit point.
|
|
117
118
|
*
|
|
118
119
|
* Returns one result per request in order; `undefined` means unattempted (behind a bisect's
|
|
119
120
|
* attributed change, or a fallback early stop) — entry and ref kept for the next merge. On an
|
|
120
121
|
* abort every request without a result reads "aborted" (refs kept). `wiringFor` must return the
|
|
121
|
-
* same wiring for a role on every call (its usage accumulator is the
|
|
122
|
-
* failed
|
|
122
|
+
* same wiring for a role on every call (its usage accumulator is the merge's). Never throws for a
|
|
123
|
+
* failed merge — per-change mergeApprovedChange failures degrade to "error", and so does a throw
|
|
123
124
|
* from any bisect step after the first stack attempt, on the change at the front of that step (a
|
|
124
125
|
* prefix may already be on main, and a throw would lose its "changed"); only the first stack
|
|
125
126
|
* attempt's git plumbing propagates, and the merge slot then keeps every entry. */
|
|
126
|
-
export async function
|
|
127
|
-
const results =
|
|
128
|
-
const report = (s, status) => ctx.onChangeStatus?.(
|
|
129
|
-
const
|
|
127
|
+
export async function mergeApproved(ctx, approved, wiringFor) {
|
|
128
|
+
const results = approved.map(() => undefined);
|
|
129
|
+
const report = (s, status) => ctx.onChangeStatus?.(approved[s].role, status);
|
|
130
|
+
const mergerCtx = (w) => ({
|
|
130
131
|
root: ctx.root,
|
|
131
132
|
mainBranch: ctx.mainBranch,
|
|
132
133
|
config: ctx.config,
|
|
@@ -145,62 +146,62 @@ export async function landVetted(ctx, vetted, wiringFor) {
|
|
|
145
146
|
await ctx.onResult?.(s, result);
|
|
146
147
|
};
|
|
147
148
|
const finish = () => aborted ? results.map((r) => r ?? "aborted") : results;
|
|
148
|
-
// The
|
|
149
|
-
// through
|
|
149
|
+
// The merge step the degenerate case and the abandon fallback share: merge one request
|
|
150
|
+
// through mergeApprovedChange on its role's wiring, and degrade a throw to "error" on that
|
|
150
151
|
// change (its ref kept for recovery) with the reason on the role's state — a throw means
|
|
151
152
|
// this change's plumbing failed, never that an earlier one is lost (see the catches below
|
|
152
|
-
// and test/
|
|
153
|
-
const
|
|
153
|
+
// and test/merger-errors.test.ts, which pins the degradation).
|
|
154
|
+
const mergeOne = async (s, req) => {
|
|
154
155
|
try {
|
|
155
|
-
await emit(s, await
|
|
156
|
+
await emit(s, await mergeApprovedChange(mergerCtx(wiringFor(req.role)), req));
|
|
156
157
|
}
|
|
157
158
|
catch (err) {
|
|
158
159
|
wiringFor(req.role).state.lastError = errorMessage(err);
|
|
159
160
|
await emit(s, "error");
|
|
160
161
|
}
|
|
161
162
|
};
|
|
162
|
-
// ── The degenerate case: one
|
|
163
|
-
if (
|
|
164
|
-
const req =
|
|
165
|
-
report(0, "
|
|
166
|
-
await
|
|
163
|
+
// ── The degenerate case: one approved change merges on its own ──────────────────────────
|
|
164
|
+
if (approved.length === 1) {
|
|
165
|
+
const req = approved[0];
|
|
166
|
+
report(0, "merging");
|
|
167
|
+
await mergeOne(0, req);
|
|
167
168
|
return results;
|
|
168
169
|
}
|
|
169
170
|
// ── Assemble the stack in the shared `_merge` checkout: ONE check over the whole tree, and
|
|
170
171
|
// on a red one, the largest passing prefix
|
|
171
172
|
// The shared `_merge` checkout hosts every assembly and the attribution's baseline check:
|
|
172
|
-
// the drain is serial, so one checkout is enough, and no
|
|
173
|
+
// the drain is serial, so one checkout is enough, and no verify runs for a role while its
|
|
173
174
|
// change is being merged (plans/worktree-pool.md, part 2c).
|
|
174
175
|
const wtPath = mergeWorktreePath(ctx.root);
|
|
175
|
-
const entries =
|
|
176
|
+
const entries = approved.map((req) => ({ role: req.role, sha: req.sha, summary: req.summary }));
|
|
176
177
|
let abandon = false;
|
|
177
|
-
// Every stacked change is
|
|
178
|
-
for (let s = 0; s <
|
|
179
|
-
report(s, "
|
|
180
|
-
// Stack positions [0,
|
|
178
|
+
// Every stacked change is merging now: they share the assembly, the check, and the ff.
|
|
179
|
+
for (let s = 0; s < approved.length; s++)
|
|
180
|
+
report(s, "merging");
|
|
181
|
+
// Stack positions [0, mergeDoneCount) are on main. `suspect` is how many of the rest, from the
|
|
181
182
|
// front, the last red check ran over — the prefix that still holds what broke it; null when
|
|
182
183
|
// no red check stands against the rest, which are then tried together.
|
|
183
|
-
let
|
|
184
|
+
let mergeDoneCount = 0;
|
|
184
185
|
let suspect = null;
|
|
185
|
-
while (
|
|
186
|
-
const front =
|
|
186
|
+
while (mergeDoneCount < entries.length) {
|
|
187
|
+
const front = mergeDoneCount;
|
|
187
188
|
const size = suspect === null ? entries.length - front : suspect === 1 ? 1 : Math.floor(suspect / 2);
|
|
188
189
|
const firstAttempt = front === 0 && suspect === null;
|
|
189
190
|
let outcome;
|
|
190
191
|
try {
|
|
191
|
-
outcome = await
|
|
192
|
+
outcome = await mergeStack(ctx, wtPath, entries.slice(front, front + size));
|
|
192
193
|
if (outcome.kind === "red" && size === 1) {
|
|
193
194
|
// The one change this red check ran over alone: attribute it, and leave every change
|
|
194
195
|
// after it unattempted for the next drain.
|
|
195
196
|
const { state } = wiringFor(entries[front].role);
|
|
196
197
|
const entry = entries[front];
|
|
197
|
-
await emit(front, await attributeRedCheck(ctx, entry.role, entry.sha, "
|
|
198
|
+
await emit(front, await attributeRedCheck(ctx, entry.role, entry.sha, "merge check", outcome, state));
|
|
198
199
|
report(front, "done");
|
|
199
200
|
break;
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
catch (err) {
|
|
203
|
-
// The first attempt's plumbing propagates, as it always has — nothing has
|
|
204
|
+
// The first attempt's plumbing propagates, as it always has — nothing has merged. A
|
|
204
205
|
// later step's degrades to "error" on the change at its front (ref kept for recovery),
|
|
205
206
|
// the rest unattempted, so the prefixes already on main keep their "changed".
|
|
206
207
|
if (firstAttempt)
|
|
@@ -210,13 +211,13 @@ export async function landVetted(ctx, vetted, wiringFor) {
|
|
|
210
211
|
report(front, "done");
|
|
211
212
|
break;
|
|
212
213
|
}
|
|
213
|
-
if (outcome.kind === "landed") {
|
|
214
|
+
if (outcome.kind === "landed") { // vocabulary-ok: persisted state value or event type, renamed by a later series
|
|
214
215
|
for (let s = front; s < front + size; s++) {
|
|
215
216
|
await emit(s, "changed");
|
|
216
217
|
report(s, "done");
|
|
217
218
|
}
|
|
218
|
-
|
|
219
|
-
// The shared `_merge` checkout persists across merges, so a
|
|
219
|
+
mergeDoneCount += size;
|
|
220
|
+
// The shared `_merge` checkout persists across merges, so a merged stack leaves it.
|
|
220
221
|
// What broke the red run is still in its remainder, unless this prefix WAS that run —
|
|
221
222
|
// then the red did not reproduce, and the rest are tried together.
|
|
222
223
|
suspect = suspect !== null && suspect > size ? suspect - size : null;
|
|
@@ -230,8 +231,8 @@ export async function landVetted(ctx, vetted, wiringFor) {
|
|
|
230
231
|
}
|
|
231
232
|
else if (outcome.kind === "blocked") {
|
|
232
233
|
// The race was lost on every attempt — main is moving faster than a check completes.
|
|
233
|
-
// Every change not yet
|
|
234
|
-
// re-
|
|
234
|
+
// Every change not yet merged keeps its ref with "merge_blocked": leftover recovery
|
|
235
|
+
// re-merges each through its own gate + tryMerge, whose in-lock check cannot lose it.
|
|
235
236
|
for (let s = front; s < entries.length; s++)
|
|
236
237
|
await emit(s, "merge_blocked");
|
|
237
238
|
break;
|
|
@@ -242,20 +243,20 @@ export async function landVetted(ctx, vetted, wiringFor) {
|
|
|
242
243
|
}
|
|
243
244
|
}
|
|
244
245
|
if (abandon) {
|
|
245
|
-
// One-at-a-time through
|
|
246
|
-
// outcome (the rest keep entry + ref for the next merge). Each request
|
|
247
|
-
// head (the synced pin its
|
|
246
|
+
// One-at-a-time through mergeApprovedChange, queue order, stopping at the first non-terminal
|
|
247
|
+
// outcome (the rest keep entry + ref for the next merge). Each request merges its approved
|
|
248
|
+
// head (the synced pin its verify judged, not the bare pin) — no second gate, so no model run:
|
|
248
249
|
// re-gating would rebase onto the main the earlier entries just moved, and the rewritten
|
|
249
250
|
// sha misses the approved short-circuit. main is never left red — mergeToMain's in-lock
|
|
250
|
-
// rebase +
|
|
251
|
-
// judged. Only the change being
|
|
252
|
-
// their turn, and each one
|
|
253
|
-
for (let s =
|
|
254
|
-
report(s, "
|
|
255
|
-
for (let s =
|
|
256
|
-
const req =
|
|
257
|
-
report(s, "
|
|
258
|
-
await
|
|
251
|
+
// rebase + verifyMerge re-check every change whose tree differs from the one its verify
|
|
252
|
+
// judged. Only the change being merged reads `merging`; the rest are back to awaiting
|
|
253
|
+
// their turn, and each one merged is done.
|
|
254
|
+
for (let s = mergeDoneCount; s < approved.length; s++)
|
|
255
|
+
report(s, "approved");
|
|
256
|
+
for (let s = mergeDoneCount; s < approved.length; s++) {
|
|
257
|
+
const req = approved[s];
|
|
258
|
+
report(s, "merging");
|
|
259
|
+
await mergeOne(s, req);
|
|
259
260
|
if (results[s] === "aborted")
|
|
260
261
|
aborted = true;
|
|
261
262
|
report(s, "done");
|
|
@@ -265,7 +266,7 @@ export async function landVetted(ctx, vetted, wiringFor) {
|
|
|
265
266
|
}
|
|
266
267
|
// A change the merge stopped short of is out of it (its entry and ref wait for the next
|
|
267
268
|
// merge, or an abort's write-back), so it must not keep a live row meanwhile.
|
|
268
|
-
for (let s = 0; s <
|
|
269
|
+
for (let s = 0; s < approved.length; s++) {
|
|
269
270
|
if (results[s] === undefined)
|
|
270
271
|
report(s, "done");
|
|
271
272
|
}
|
|
@@ -1,53 +1,56 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import { deleteRef, headOf, patchId } from "../git/git.js";
|
|
2
|
-
import {
|
|
3
|
+
import { mergeRefName } from "../paths.js";
|
|
3
4
|
import { recordReview } from "../tick/tick-apply.js";
|
|
4
5
|
import { saveLoopState } from "../loop/loop-state.js";
|
|
5
6
|
import { unverifiedTreeOutcome } from "../build/build-check-events.js";
|
|
6
|
-
import { checkFailureReasons } from "../build/build-check-report.js";
|
|
7
|
+
import { checkFailureReasons, describeCheck, failureHeadline } from "../build/build-check-report.js";
|
|
7
8
|
import { mainTipVerdict } from "../baseline/main-red.js";
|
|
8
|
-
import { logEventBestEffort } from "../events/events.js";
|
|
9
|
+
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
9
10
|
import { mainRedNotMine } from "../text/phrases.js";
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
import { normalizeClusterKey } from "../failure/failure-cluster.js";
|
|
12
|
+
import { MERGE_FLAKY_WARNING_PREFIX, isKnownFlake } from "../review/known-flakes.js";
|
|
13
|
+
/** Consecutive red in-lock merge checks of one patch (LoopState.mergeCheckFailures) before
|
|
14
|
+
* the merge is attributed instead of retried: the first red keeps the pin for one more
|
|
12
15
|
* attempt — a load flake gets its retry, as the gate's pre-check gets one (PLANS.md land-queue
|
|
13
16
|
* 1/3) — and the second is judged by main's own verdict at its tip (attributeRedCheck). Without
|
|
14
|
-
* a limit a change whose gate passes but whose
|
|
17
|
+
* a limit a change whose gate passes but whose merge check fails (a cheaper check.gateCommand
|
|
15
18
|
* than the full check) would re-queue as merge_blocked forever, its role never authoring. */
|
|
16
|
-
export const
|
|
17
|
-
/** Whether a deterministic failure keeps its pin for one more re-
|
|
19
|
+
export const MERGE_CHECK_FAILURE_LIMIT = 2;
|
|
20
|
+
/** Whether a deterministic failure keeps its pin for one more re-merge instead of being
|
|
18
21
|
* attributed to the change: always when the patch-id could not be read (it cannot be matched
|
|
19
|
-
* to a prior strike, so it never reaches
|
|
22
|
+
* to a prior strike, so it never reaches MERGE_CHECK_FAILURE_LIMIT — the pre-cap behavior,
|
|
20
23
|
* never a wrong attribution), otherwise while the strike count is still under the limit. The
|
|
21
24
|
* one home of that rule: tallyCheckFailure's strike store and keepPinOrAttribute's
|
|
22
25
|
* keep-or-attribute branch both read it, so the two cannot disagree on when the limit is
|
|
23
26
|
* reached. */
|
|
24
27
|
function pinKept(patch, count) {
|
|
25
|
-
return patch === null || count <
|
|
28
|
+
return patch === null || count < MERGE_CHECK_FAILURE_LIMIT;
|
|
26
29
|
}
|
|
27
30
|
/** Tally one deterministic failure of the same patch toward attribution: read the worktree's
|
|
28
|
-
* head and its patch-id against main, carry LoopState.
|
|
29
|
-
* re-
|
|
31
|
+
* head and its patch-id against main, carry LoopState.mergeCheckFailures's count across
|
|
32
|
+
* re-merges of the identical patch (a clean rebase onto a moved main keeps the patch-id, so the
|
|
30
33
|
* retries of one change add up while a different change starts fresh), and store the strike.
|
|
31
34
|
* An unreadable patch-id cannot be matched, so pinKept never counts it toward the limit.
|
|
32
35
|
* Keeping the strike only under the limit (pinKept) is what ends the streak at attribution. */
|
|
33
36
|
async function tallyCheckFailure(ctx, wt) {
|
|
34
37
|
const head = await headOf(wt, "HEAD");
|
|
35
38
|
const patch = await patchId(wt, ctx.mainBranch, head);
|
|
36
|
-
const prior = ctx.state.
|
|
39
|
+
const prior = ctx.state.mergeCheckFailures;
|
|
37
40
|
const count = (patch !== null && prior?.patchId === patch ? prior.count : 0) + 1;
|
|
38
41
|
if (pinKept(patch, count)) {
|
|
39
|
-
ctx.state.
|
|
42
|
+
ctx.state.mergeCheckFailures = patch === null ? undefined : { patchId: patch, count };
|
|
40
43
|
}
|
|
41
44
|
else {
|
|
42
|
-
ctx.state.
|
|
45
|
+
ctx.state.mergeCheckFailures = undefined;
|
|
43
46
|
}
|
|
44
47
|
return { head, patch, count };
|
|
45
48
|
}
|
|
46
|
-
/** The shared re-
|
|
47
|
-
*
|
|
49
|
+
/** The shared re-merge rule of the two deterministic-block paths below (mergeCheckRed and
|
|
50
|
+
* mergeBlocked): tally the strike on this patch (tallyCheckFailure), then either keep the
|
|
48
51
|
* pin for one more attempt — merge_blocked with `reason` named in lastError — or hand the head
|
|
49
52
|
* to the caller's terminal sink `attribute`. Both paths decide keep-vs-attribute through
|
|
50
|
-
* pinKept, so the
|
|
53
|
+
* pinKept, so the MERGE_CHECK_FAILURE_LIMIT rule has one home. */
|
|
51
54
|
async function keepPinOrAttribute(ctx, wt, reason, attribute) {
|
|
52
55
|
const { head, patch, count } = await tallyCheckFailure(ctx, wt);
|
|
53
56
|
if (pinKept(patch, count)) {
|
|
@@ -56,9 +59,9 @@ async function keepPinOrAttribute(ctx, wt, reason, attribute) {
|
|
|
56
59
|
}
|
|
57
60
|
return attribute(head);
|
|
58
61
|
}
|
|
59
|
-
/** The terminal deterministic-reject sink shared by
|
|
62
|
+
/** The terminal deterministic-reject sink shared by mergeBlocked and attributeRedCheck:
|
|
60
63
|
* record the reject review, reset the unreviewed-failure streak, persist state, delete the
|
|
61
|
-
*
|
|
64
|
+
* merge ref, and log review_rejected. The shared `_merge` checkout is left in place
|
|
62
65
|
* (plans/worktree-pool.md, part 2c): it persists across merges like `_gate-main`, and a
|
|
63
66
|
* rejection discards the pinned ref, not the checkout. Returns "rejected". review.ts's reject
|
|
64
67
|
* path is a different sink — it resets the branch instead of deleting the ref — and keeps its
|
|
@@ -67,58 +70,92 @@ async function rejectChange(ctx, role, head, reasons, state) {
|
|
|
67
70
|
recordReview(state, "reject", reasons, head);
|
|
68
71
|
state.unreviewFailures = 0;
|
|
69
72
|
saveLoopState(ctx.root, state);
|
|
70
|
-
await deleteRef(ctx.root,
|
|
71
|
-
logEventBestEffort(ctx.root, {
|
|
73
|
+
await deleteRef(ctx.root, mergeRefName(role));
|
|
74
|
+
logEventBestEffort(ctx.root, { source: role, type: "review_rejected", head, reasons });
|
|
72
75
|
return "rejected";
|
|
73
76
|
}
|
|
74
77
|
/** The one-line reason a red check's lastError carries: checkFailureReasons' first entry (it
|
|
75
|
-
* always returns at least one). Shared by the blocked-
|
|
78
|
+
* always returns at least one). Shared by the blocked-merge paths below — mergeCheckRed's
|
|
76
79
|
* two branches and attributeRedCheck's unverified branch — so the "name the red by its first
|
|
77
80
|
* reason" rule has one home while the full reason list stays where a whole review needs it
|
|
78
81
|
* (attributeRedCheck's reject reasons). */
|
|
79
82
|
function firstCheckReason(red) {
|
|
80
83
|
return checkFailureReasons(red.check, red.outcome)[0];
|
|
81
84
|
}
|
|
82
|
-
/** A
|
|
83
|
-
*
|
|
84
|
-
* pin is kept (merge_blocked) for recovery's re-
|
|
85
|
+
/** A merge blocked because its in-lock check went red on the rebased tree (merge-to-main.ts's
|
|
86
|
+
* verifyMerge). The count is keyed by the patch-id; under MERGE_CHECK_FAILURE_LIMIT the
|
|
87
|
+
* pin is kept (merge_blocked) for recovery's re-merge; at the limit the red is attributed like
|
|
85
88
|
* any single-change red. The other merge_blocked causes — a fast-forward that fails on a dirty
|
|
86
89
|
* primary checkout, or main moving under the ff — are never the change's fault and never
|
|
87
90
|
* counted. An unverified red made no verdict about the tree — the run spanned a host sleep
|
|
88
91
|
* (BUGS.md 2026-09-30) — so it is not a strike against the patch: the pin stays for the next
|
|
89
92
|
* attempt and the lastError names the sleep instead of a test failure. */
|
|
90
|
-
export async function
|
|
93
|
+
export async function mergeCheckRed(ctx, role, wt, red) {
|
|
91
94
|
if (unverifiedTreeOutcome(red.outcome)) {
|
|
92
95
|
ctx.state.lastError = `merge failed: ${firstCheckReason(red)}`;
|
|
93
96
|
return "merge_blocked";
|
|
94
97
|
}
|
|
95
|
-
|
|
98
|
+
const reason = firstCheckReason(red);
|
|
99
|
+
let attributed = false;
|
|
100
|
+
const result = await keepPinOrAttribute(ctx, wt, reason, (head) => {
|
|
101
|
+
attributed = true;
|
|
102
|
+
return attributeRedCheck(ctx, role, head, "merge check", red, ctx.state);
|
|
103
|
+
});
|
|
104
|
+
// The first red of a patch's in-lock check is exactly the retry that exists for load flakes,
|
|
105
|
+
// so it is the ONE signal that could name a flaky test after the fact — and lastError, the
|
|
106
|
+
// only place this reason is written, is overwritten by the leftover-recovery tick a second
|
|
107
|
+
// later (BUGS.md 2026-10-14). Log it as a warning so the failure digest and a later audit can
|
|
108
|
+
// name the flaky test; the second red at MERGE_CHECK_FAILURE_LIMIT is attributed through
|
|
109
|
+
// review_rejected, whose reasons are already durable.
|
|
110
|
+
if (result === "merge_blocked" && !attributed) {
|
|
111
|
+
warnEventBestEffort(ctx.root, role, `merge check failed (kept for one retry): ${reason}`);
|
|
112
|
+
if (ctx.state.mergeCheckFailures) {
|
|
113
|
+
ctx.state.mergeCheckFailures.headline = failureHeadline(red.outcome.outputTail) ?? describeCheck(red.check);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
/** Record a merge check that failed and then passed on its kept-pin retry as a flake, in the
|
|
119
|
+
* form the gate's pre-check uses (MERGE_FLAKY_WARNING_PREFIX), so the gate's known-flake memory,
|
|
120
|
+
* doctor's `flaky checks` and the tick prompt's flake note count it (PLANS.md 2026-10-16). Reads
|
|
121
|
+
* the headline mergeCheckRed stored on the strike record only when its patch-id is the merged
|
|
122
|
+
* pin's (a stale strike of a discarded pin must not be logged); a pin that was never red (or
|
|
123
|
+
* only cross-check blocked) has none and logs nothing. Call before the strike is cleared. */
|
|
124
|
+
export function noteMergeRetryPassed(ctx, role, pinPatch) {
|
|
125
|
+
const strike = ctx.state.mergeCheckFailures;
|
|
126
|
+
const headline = strike?.patchId === pinPatch ? strike?.headline : undefined;
|
|
127
|
+
if (headline)
|
|
128
|
+
warnEventBestEffort(ctx.root, role, MERGE_FLAKY_WARNING_PREFIX + headline);
|
|
96
129
|
}
|
|
97
|
-
/** A
|
|
130
|
+
/** A merge blocked by a deterministic cross-check (onMergeBlocked's fix-claim or
|
|
98
131
|
* backlog-structure reason — a property of the tree ahead of main, not a flaky run). The
|
|
99
|
-
* block is counted like a red
|
|
100
|
-
*
|
|
132
|
+
* block is counted like a red merge check, keyed by the same patch-id: under
|
|
133
|
+
* MERGE_CHECK_FAILURE_LIMIT the pin is kept for one recovery re-merge — main may have moved
|
|
101
134
|
* under the rebase, and a differently-rebased tree can pass the cross-check — but at the
|
|
102
135
|
* limit the block is attributed to the change: rejected deterministically with the block
|
|
103
136
|
* reason, the pin deleted, no model run. Without this a resolution that trips the same
|
|
104
137
|
* cross-check on every re-resolve pays a fresh conflict-resolver run each tick, forever
|
|
105
138
|
* (BUGS.md 2026-10-02: five resolver sessions for one fix-claim-blocked pin). */
|
|
106
|
-
export async function
|
|
107
|
-
return keepPinOrAttribute(ctx, wt, blocked, (head) => rejectChange(ctx, role, head, [`
|
|
139
|
+
export async function mergeBlocked(ctx, role, wt, blocked) {
|
|
140
|
+
return keepPinOrAttribute(ctx, wt, blocked, (head) => rejectChange(ctx, role, head, [`merge blocked: ${blocked}`], ctx.state));
|
|
108
141
|
}
|
|
109
|
-
/** Attribute a check that went red over ONE change's tree after its
|
|
110
|
-
* bisect's last step (
|
|
111
|
-
*
|
|
112
|
-
* verdict at its current tip (mainTipVerdict — usually a cache hit, since every
|
|
142
|
+
/** Attribute a check that went red over ONE change's tree after its verify approved it — a batch
|
|
143
|
+
* bisect's last step (merge-batch.ts), or a single merge's in-lock check at
|
|
144
|
+
* MERGE_CHECK_FAILURE_LIMIT — by the gate's rule: ask main's own
|
|
145
|
+
* verdict at its current tip (mainTipVerdict — usually a cache hit, since every merge seeds
|
|
113
146
|
* the SHA it moved main to). Main green → the change broke the check: rejected deterministically
|
|
114
147
|
* — reasons in lastReview for the author's next tick, the strike count reset, ref deleted,
|
|
115
148
|
* review_rejected logged, no pi run. Main red → not this change's failure: "main_red" with the
|
|
116
|
-
* ref kept and unreviewFailures untouched, so recovery re-
|
|
149
|
+
* ref kept and unreviewFailures untouched, so recovery re-merges it once main-red.ts's repair
|
|
117
150
|
* turns main green. No verdict → reject, the safe default, and the reasons say so. The verdict
|
|
118
|
-
* is persisted before it returns.
|
|
151
|
+
* is persisted before it returns. A red whose headline matches a flake the gate or a merge
|
|
152
|
+
* retry recorded (isKnownFlake) is excused once per headline per pin (LoopState.mergeFlakeExcused):
|
|
153
|
+
* "merge_blocked" with the ref kept, before main's verdict, because that verdict is usually a
|
|
154
|
+
* cached green that would reject a change over a flaky test (PLANS.md 2026-10-16, BUGS.md
|
|
155
|
+
* 2026-10-06). A repeat of the same headline is attributed as above. */
|
|
119
156
|
export async function attributeRedCheck(ctx, role, head, label, red, state) {
|
|
120
157
|
// An unverified red — a run that spanned a host sleep, the tree never judged — is not the
|
|
121
|
-
// change's failure and not a strike: keep the ref for recovery's re-
|
|
158
|
+
// change's failure and not a strike: keep the ref for recovery's re-merge and name the sleep
|
|
122
159
|
// (BUGS.md 2026-09-30). Main's own verdict is beside the point: the attribution question is
|
|
123
160
|
// only live when the check produced a verdict.
|
|
124
161
|
if (unverifiedTreeOutcome(red.outcome)) {
|
|
@@ -126,6 +163,15 @@ export async function attributeRedCheck(ctx, role, head, label, red, state) {
|
|
|
126
163
|
saveLoopState(ctx.root, state);
|
|
127
164
|
return "merge_blocked";
|
|
128
165
|
}
|
|
166
|
+
const headline = failureHeadline(red.outcome.outputTail) ?? describeCheck(red.check);
|
|
167
|
+
const key = normalizeClusterKey(headline);
|
|
168
|
+
if (isKnownFlake(ctx.root, headline) && state.mergeFlakeExcused !== key) {
|
|
169
|
+
state.mergeFlakeExcused = key;
|
|
170
|
+
state.lastError = `${label} failed: known-flaky failure, merge kept: ${headline}`;
|
|
171
|
+
warnEventBestEffort(ctx.root, role, `${label}: known-flaky failure, merge kept: ${headline}`);
|
|
172
|
+
saveLoopState(ctx.root, state);
|
|
173
|
+
return "merge_blocked";
|
|
174
|
+
}
|
|
129
175
|
const main = await mainTipVerdict(ctx.root, role, ctx.mainBranch, ctx.config);
|
|
130
176
|
if (main.status === "red") {
|
|
131
177
|
state.lastError = `${label} failed: ${mainRedNotMine(main.sha)}`;
|
|
@@ -134,7 +180,7 @@ export async function attributeRedCheck(ctx, role, head, label, red, state) {
|
|
|
134
180
|
}
|
|
135
181
|
const reasons = checkFailureReasons(red.check, red.outcome);
|
|
136
182
|
if (main.status === "unavailable") {
|
|
137
|
-
reasons.push(`main's own baseline was unavailable (${main.why}), so the
|
|
183
|
+
reasons.push(`main's own baseline was unavailable (${main.why}), so the failed ${label} is attributed to this change`);
|
|
138
184
|
}
|
|
139
185
|
// Terminal rejection: the sink deletes the pin; the shared `_merge` checkout persists.
|
|
140
186
|
return rejectChange(ctx, role, head, reasons, state);
|