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,9 +1,10 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import { openQuestions } from "../backlog/backlog.js";
|
|
2
3
|
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
3
4
|
import { changeBaseRev, commitMessage, headOf } from "../git/git.js";
|
|
4
5
|
import { aheadOfMainFiles, treeDiffFiles } from "../git/git-diff.js";
|
|
5
|
-
import { resolvedDiffDiverges } from "./
|
|
6
|
-
import { conflictedFiles, continueRebase,
|
|
6
|
+
import { resolvedDiffDiverges } from "./merge-diff.js";
|
|
7
|
+
import { conflictedFileHunks, conflictedFiles, continueRebase, ffMainToOrWhy, hasConflictMarkers, mainCommitsTouching, rebaseOntoMain, rebaseOntoMainLeaveConflicts, } from "./merge-git.js";
|
|
7
8
|
import { resolveBacklogInsertConflicts } from "./backlog-conflicts.js";
|
|
8
9
|
import { abortSync } from "../git/worktree.js";
|
|
9
10
|
import { runScopedBuildCheck } from "../build/build-check-scoped.js";
|
|
@@ -15,25 +16,25 @@ import { backlogStructureReason } from "../backlog/backlog-structure.js";
|
|
|
15
16
|
import { withLock } from "../concurrency/lock.js";
|
|
16
17
|
import { buildConflictPrompt } from "../prompt/gate-prompts.js";
|
|
17
18
|
import { mergeLockDir } from "../paths.js";
|
|
18
|
-
import { logNewQuestions } from "./
|
|
19
|
-
import { checkWaitStage,
|
|
19
|
+
import { logNewQuestions } from "./merge-questions.js";
|
|
20
|
+
import { checkWaitStage, setMergeStage } from "./merge-slot.js";
|
|
20
21
|
import { syncRootInstall } from "../build/dep-install.js";
|
|
21
22
|
import { resolverConfig } from "../config/config-views.js";
|
|
22
|
-
/**
|
|
23
|
+
/** Merge the worktree branch on main under the shared merge lock: rebase it onto main (keeping
|
|
23
24
|
* history linear), verify the rebased tree when it differs from what was reviewed, and
|
|
24
25
|
* fast-forward. On conflict, makes one pi-driven resolution attempt
|
|
25
26
|
* (outside the lock) before giving up. A routine conflict is normal operation, not a warning:
|
|
26
|
-
* success
|
|
27
|
+
* success merges as an ordinary `merged` event and failure surfaces via the tick's merge_conflict
|
|
27
28
|
* result — no separate log line for the hand-off itself. A merged diff that adds entries under
|
|
28
29
|
* QUESTIONS.md's ## Open also emits one `question_posted` per new heading alongside the `merged`
|
|
29
|
-
* event, so `tumwater logs` shows what the
|
|
30
|
+
* event, so `tumwater logs` shows what the team is asking for (plans/questions-outbox.md).
|
|
30
31
|
* `verifiedHead` is the head the review gate's pre-check just ran green on
|
|
31
32
|
* (GateResult.verifiedHead) — when the rebase turns out to be a no-op it names the exact tree
|
|
32
|
-
* about to
|
|
33
|
+
* about to merge, so the in-lock re-check can both skip and seed the baseline from it; pass
|
|
33
34
|
* undefined when no fresh green observation was made (exempt diff, review disabled,
|
|
34
35
|
* already-approved early return). */
|
|
35
36
|
export async function mergeToMain(ctx, wt, summary, verifiedHead) {
|
|
36
|
-
// The branch tip before ANY rebase of this
|
|
37
|
+
// The branch tip before ANY rebase of this merge. Captured once here — not per tryMerge —
|
|
37
38
|
// because the conflict-retry path rebases twice: after pi resolves, the second attempt's
|
|
38
39
|
// rebase is a no-op even though its tree (the resolution) was never checked.
|
|
39
40
|
const preMergeHead = await headOf(wt, "HEAD");
|
|
@@ -46,8 +47,8 @@ export async function mergeToMain(ctx, wt, summary, verifiedHead) {
|
|
|
46
47
|
// removes lines the reviewed change never removed, has left the reviewer's scope: those bytes
|
|
47
48
|
// were never judged, and the in-lock build check cannot judge intent (BUGS.md 2026-10-01: a
|
|
48
49
|
// resolver restored 158 lines main had deliberately reverted). Re-run the gate over the
|
|
49
|
-
// resolved tree before anything
|
|
50
|
-
// lines — including one that drops branch edits main superseded — re-
|
|
50
|
+
// resolved tree before anything merges; a resolution that stays inside the reviewed change's
|
|
51
|
+
// lines — including one that drops branch edits main superseded — re-merges with no extra run.
|
|
51
52
|
if (ctx.recheckResolved && (await resolvedDiffDiverges(ctx, wt, preMergeHead))) {
|
|
52
53
|
const recheck = await ctx.recheckResolved(wt);
|
|
53
54
|
if (recheck.verdict === "rejected")
|
|
@@ -59,66 +60,69 @@ export async function mergeToMain(ctx, wt, summary, verifiedHead) {
|
|
|
59
60
|
return tryMerge(ctx, wt, summary, verifiedHead);
|
|
60
61
|
}
|
|
61
62
|
/** Emit one `question_posted` event per entry QUESTIONS.md's ## Open gained since `before`,
|
|
62
|
-
* recorded by
|
|
63
|
+
* recorded by merge-questions.ts's logNewQuestions alongside this module's `merged` event. */
|
|
63
64
|
async function tryMerge(ctx, wt, summary, verifiedHead) {
|
|
64
65
|
return withLock(mergeLockDir(ctx.root), async () => {
|
|
65
66
|
// Capture the Open questions before the rebase so a merged diff that posts new ones can
|
|
66
|
-
// emit one question_posted per entry. The lock keeps no other merge
|
|
67
|
+
// emit one question_posted per entry. The lock keeps no other merge between capture
|
|
67
68
|
// and compare, so the diff is exact; on the conflict path only the second tryMerge call ever
|
|
68
69
|
// reaches the post-ff code, so nothing double-emits.
|
|
69
70
|
const before = openQuestions(ctx.root);
|
|
70
71
|
if (!(await rebaseOntoMain(wt, ctx.mainBranch)))
|
|
71
72
|
return "merge_conflict";
|
|
72
73
|
// The gate's pre-check ran OUTSIDE this lock against the head as it stood then; a rebase
|
|
73
|
-
// that rewrote anything means the tree about to
|
|
74
|
+
// that rewrote anything means the tree about to merge is new bytes (BUGS.md 2026-09-08).
|
|
74
75
|
// Re-verify exactly what will become main before fast-forwarding.
|
|
75
76
|
// The worktree's POST-REBASE HEAD: fetched once and used for both the re-check and the
|
|
76
77
|
// fast-forward. A branch ref tracks its own tip through the rebase (so this is
|
|
77
78
|
// behavior-preserving for role branches), but a pinned bare sha does not move when git
|
|
78
79
|
// rebase rewrites it — ff'ing main to the original pin would fail as merge_blocked whenever
|
|
79
|
-
// main moved between commit and
|
|
80
|
+
// main moved between commit and merge, which under concurrency is the common case (review
|
|
80
81
|
// runs outside this lock).
|
|
81
82
|
const target = await headOf(wt, "HEAD");
|
|
82
|
-
if (!(await
|
|
83
|
+
if (!(await verifyMerge(ctx, wt, target, verifiedHead)))
|
|
83
84
|
return "merge_blocked";
|
|
84
85
|
const mainBefore = await headOf(ctx.root, ctx.mainBranch);
|
|
85
|
-
|
|
86
|
+
const ffFailure = await ffMainToOrWhy(ctx.root, target, ctx.mainBranch);
|
|
87
|
+
if (ffFailure !== null) {
|
|
88
|
+
warnEventBestEffort(ctx.root, ctx.role, `merge blocked: ${ffFailure}`);
|
|
86
89
|
return "merge_blocked";
|
|
90
|
+
}
|
|
87
91
|
const commit = await headOf(ctx.root, ctx.mainBranch);
|
|
88
92
|
// A fast-forward to a commit main already contains is a no-op ("Already up to date"): the
|
|
89
|
-
// change is already an ancestor of main (a leftover-recovery re-
|
|
93
|
+
// change is already an ancestor of main (a leftover-recovery re-merge of an entry main
|
|
90
94
|
// holds), so main's head is some later commit that did not produce this summary. Logging a
|
|
91
|
-
// `merged` event there would count the already-counted
|
|
95
|
+
// `merged` event there would count the already-counted merge a second time and pair the
|
|
92
96
|
// change's summary with the wrong commit (BUGS.md 2026-10-10). Only a fast-forward that
|
|
93
|
-
// moved main records a
|
|
97
|
+
// moved main records a merge.
|
|
94
98
|
// Best-effort: main has already fast-forwarded, so an unwritable events feed must not turn
|
|
95
|
-
// a completed
|
|
99
|
+
// a completed merge into a rejected one and skip syncRootInstall below — the same
|
|
96
100
|
// no-torn-feed policy as the orchestrator's other post-effect logging.
|
|
97
101
|
if (commit !== mainBefore)
|
|
98
|
-
logEventBestEffort(ctx.root, {
|
|
102
|
+
logEventBestEffort(ctx.root, { source: ctx.role, type: "merged", commit, summary });
|
|
99
103
|
logNewQuestions(ctx.root, before, ctx.role);
|
|
100
|
-
// Still under the lock: a
|
|
101
|
-
// every worktree resolves through before the next
|
|
104
|
+
// Still under the lock: a merge that moved main's lockfile re-syncs the root install
|
|
105
|
+
// every worktree resolves through before the next merge's check runs (BUGS.md 2026-10-01).
|
|
102
106
|
await syncRootInstall(ctx.root, ctx.role);
|
|
103
107
|
return "changed";
|
|
104
108
|
});
|
|
105
109
|
}
|
|
106
|
-
/** The backlog-structure cross-check both
|
|
110
|
+
/** The backlog-structure cross-check both verifyMerge paths run (exempt and code diffs
|
|
107
111
|
* alike): when the tree ahead of main duplicates or drops a `## ` section heading, warn on the
|
|
108
|
-
* role's feed with the one "
|
|
112
|
+
* role's feed with the one "merge blocked:" phrasing and return the reason (null when the
|
|
109
113
|
* headings are intact). Shared so the wording and the block action cannot drift between the
|
|
110
114
|
* paths. */
|
|
111
115
|
async function structureBlocked(ctx, wt, files) {
|
|
112
116
|
const structure = await backlogStructureReason(wt, ctx.mainBranch, files);
|
|
113
117
|
if (!structure)
|
|
114
118
|
return null;
|
|
115
|
-
warnEventBestEffort(ctx.root, ctx.role, `
|
|
119
|
+
warnEventBestEffort(ctx.root, ctx.role, `merge blocked: ${structure}`);
|
|
116
120
|
return structure;
|
|
117
121
|
}
|
|
118
122
|
/** The exempt arm's full cross-check — backlog structure, then fix-claim — shared by
|
|
119
|
-
*
|
|
123
|
+
* verifyMerge and merge-batch.ts's stack skip: an md-only tree delta that skips the build
|
|
120
124
|
* check must not wave through an edit the gate would have rejected. Returns the first blocking
|
|
121
|
-
* reason (a structure reason already warned as "
|
|
125
|
+
* reason (a structure reason already warned as "merge blocked: ..."), or null when the skip
|
|
122
126
|
* may proceed. */
|
|
123
127
|
export async function exemptSkipBlockReason(ctx, wt, files) {
|
|
124
128
|
const structure = await structureBlocked(ctx, wt, files);
|
|
@@ -130,43 +134,43 @@ export async function exemptSkipBlockReason(ctx, wt, files) {
|
|
|
130
134
|
// caller may warn twice for one change (here, then again when the fallback's exempt arm
|
|
131
135
|
// blocks it) — a duplicated line is harmless next to a silent block.
|
|
132
136
|
if (fix)
|
|
133
|
-
warnEventBestEffort(ctx.root, ctx.role, `
|
|
137
|
+
warnEventBestEffort(ctx.root, ctx.role, `merge blocked: ${fix}`);
|
|
134
138
|
return fix ?? null;
|
|
135
139
|
}
|
|
136
|
-
/** Verify the exact tree about to
|
|
140
|
+
/** Verify the exact tree about to reach main — the post-rebase head (BUGS.md 2026-09-08: the
|
|
137
141
|
* gate's pre-check ran outside this lock against a head the rebase may have rewritten, so the
|
|
138
142
|
* bytes that become main were never run through a check). Returns false when the tree is
|
|
139
143
|
* structurally unsound (backlogStructureReason: a duplicated or dropped `## ` section heading,
|
|
140
144
|
* with a warning event so a broken conflict resolution shows on the dashboards) or when the
|
|
141
|
-
* project's declared check FAILS on the rebased tree; every other outcome
|
|
145
|
+
* project's declared check FAILS on the rebased tree; every other outcome merges. Skips:
|
|
142
146
|
* - a no-op rebase whose exact tree the gate verified (`rebasedHead === verifiedHead`): main
|
|
143
|
-
* did not move under us, so the
|
|
147
|
+
* did not move under us, so the merge tree is byte-identical to what the gate's pre-check
|
|
144
148
|
* already ran green. Seed the red-main baseline with the SHA that becomes main: every role's
|
|
145
149
|
* next fresh tick then hits the cache instead of re-running the full suite on an
|
|
146
150
|
* already-verified tree. A no-op rebase the gate did NOT verify — its pre-check was skipped
|
|
147
|
-
* (a timeout, a killed run) — falls through and runs the
|
|
151
|
+
* (a timeout, a killed run) — falls through and runs the merge-scope check, which fails
|
|
148
152
|
* closed on the same timeout; an exempt diff or a project with no declared check falls
|
|
149
153
|
* through to its own arm below. Not taken when a `check.gateCommand` is configured: the gate
|
|
150
154
|
* then ran only that, not the full check.
|
|
151
155
|
* - doc-only delta ahead of main (the gate's own exemption test): cannot break the build.
|
|
152
156
|
* - a code change whose rebased tree differs from the gate-verified one only in exempt paths
|
|
153
157
|
* (onlyExemptDriftFromVerified): main moved under the review by doc commits alone, so the
|
|
154
|
-
* bytes about to
|
|
158
|
+
* bytes about to merge are the verified ones plus a delta that same rule exempts.
|
|
155
159
|
* - no declared check at all: nothing to run, exactly like the gate skipping its pre-check.
|
|
156
160
|
* An environmental skip (no npm / broken toolchain) warns and proceeds — deliberately NOT
|
|
157
161
|
* fail-closed, so a broken toolchain that would fail every check regardless of the tree
|
|
158
|
-
* (BUGS.md 2026-09-15) cannot wedge every
|
|
162
|
+
* (BUGS.md 2026-09-15) cannot wedge every merge behind the merge lock. A TIMEOUT is not
|
|
159
163
|
* environmental here: the tree is unverified, so runScopedBuildCheck remaps it to a failed
|
|
160
|
-
* check and the
|
|
161
|
-
async function
|
|
164
|
+
* check and the merge rejects (BUGS.md: an unverified merge must not reach main). */
|
|
165
|
+
async function verifyMerge(ctx, wt, rebasedHead, verifiedHead) {
|
|
162
166
|
// Trust only the tree the gate actually verified. The identity means the in-lock rebase was
|
|
163
167
|
// a no-op on exactly the head the pre-check ran green on (a real conflict resolution's
|
|
164
168
|
// re-review sets verifiedHead to the resolved head, and that second rebase is then a no-op).
|
|
165
169
|
// A no-op rebase the gate never verified — its pre-check timed out or was killed and left
|
|
166
|
-
// GateResult.verifiedHead undefined — falls through and runs the
|
|
170
|
+
// GateResult.verifiedHead undefined — falls through and runs the merge-scope check, which
|
|
167
171
|
// fails closed on the same timeout. With a check.gateCommand configured the gate ran only
|
|
168
172
|
// that cheaper check, so even a verified no-op rebase must run the full check here: the
|
|
169
|
-
//
|
|
173
|
+
// merge scope is what verifies a single change (and each change of an abandoned stack),
|
|
170
174
|
// and a gateCommand green must never seed the baseline.
|
|
171
175
|
if (rebasedHead === verifiedHead && gateCommandOf(ctx.config) === undefined) {
|
|
172
176
|
noteGreenBaseline(rebasedHead);
|
|
@@ -178,34 +182,34 @@ async function verifyLanding(ctx, wt, rebasedHead, verifiedHead) {
|
|
|
178
182
|
// must not wave through an md-only edit the gate would have rejected — and this is the
|
|
179
183
|
// site that catches what the gate cannot see: a conflict resolution happens AFTER the
|
|
180
184
|
// gate, inside this lock, so a resolution that kept both sides of a `## Done` conflict and
|
|
181
|
-
// duplicated the heading
|
|
185
|
+
// duplicated the heading merges here or not at all (PLANS.md 2026-09-25, 9eaae5ac).
|
|
182
186
|
const reason = await exemptSkipBlockReason(ctx, wt, files);
|
|
183
187
|
if (reason !== null) {
|
|
184
|
-
ctx.
|
|
188
|
+
ctx.onMergeBlocked?.(reason);
|
|
185
189
|
return false;
|
|
186
190
|
}
|
|
187
191
|
return true;
|
|
188
192
|
}
|
|
189
193
|
// The heading check for code diffs too, before the build check: a conflict resolution that
|
|
190
194
|
// broke backlog structure reads as an explained block on the dashboards, not as an
|
|
191
|
-
// unexplained red check run on a tree that could never
|
|
195
|
+
// unexplained red check run on a tree that could never merge.
|
|
192
196
|
const structure = await structureBlocked(ctx, wt, files);
|
|
193
197
|
if (structure) {
|
|
194
|
-
ctx.
|
|
198
|
+
ctx.onMergeBlocked?.(structure);
|
|
195
199
|
return false;
|
|
196
200
|
}
|
|
197
201
|
if (await onlyExemptDriftFromVerified(ctx, wt, rebasedHead, verifiedHead))
|
|
198
202
|
return true;
|
|
199
203
|
// The run itself (build_check event, environmental-skip warning) lives in
|
|
200
|
-
// runScopedBuildCheck, shared with the review gate's pre-check. The
|
|
204
|
+
// runScopedBuildCheck, shared with the review gate's pre-check. The merge cell names the
|
|
201
205
|
// check (or its wait for a permit) while it runs, then the merge again for the ff.
|
|
202
|
-
|
|
203
|
-
const check = await runScopedBuildCheck(ctx.root, ctx.role, "
|
|
204
|
-
|
|
206
|
+
setMergeStage(ctx.root, ctx.role, "build-check");
|
|
207
|
+
const check = await runScopedBuildCheck(ctx.root, ctx.role, "merge", wt, ctx.config, undefined, undefined, undefined, checkWaitStage(ctx.root, [ctx.role]));
|
|
208
|
+
setMergeStage(ctx.root, ctx.role, "merging");
|
|
205
209
|
if (!check)
|
|
206
210
|
return true; // No declared check: nothing to run, exactly like the gate skipping its pre-check.
|
|
207
211
|
if (check.outcome.status === "failed") {
|
|
208
|
-
ctx.
|
|
212
|
+
ctx.onMergeCheckRed?.(check.check, check.outcome);
|
|
209
213
|
return false;
|
|
210
214
|
}
|
|
211
215
|
if (check.outcome.status === "skipped")
|
|
@@ -215,12 +219,12 @@ async function verifyLanding(ctx, wt, rebasedHead, verifiedHead) {
|
|
|
215
219
|
noteGreenBaseline(rebasedHead);
|
|
216
220
|
return true;
|
|
217
221
|
}
|
|
218
|
-
/** True, after seeding the red-main baseline, when the tree about to
|
|
219
|
-
* gate's pre-check ran green on (`verifiedHead`) only in review-exempt paths. That is the usual
|
|
222
|
+
/** True, after seeding the red-main baseline, when the tree about to merge differs from the tree
|
|
223
|
+
* the gate's pre-check ran green on (`verifiedHead`) only in review-exempt paths. That is the usual
|
|
220
224
|
* shape of "main moved under the review" here: 43% of main's commits are doc-only (plan, readme
|
|
221
|
-
* and qa
|
|
225
|
+
* and qa merge them all day), and 165 of 750 in-lock checks over 2026-10-02..12 re-ran the full
|
|
222
226
|
* suite, holding the merge lock, for a tree that differed from its verified one by markdown
|
|
223
|
-
* alone. The exempt-diff rule above already lets such a delta
|
|
227
|
+
* alone. The exempt-diff rule above already lets such a delta merge with no check at all, so the
|
|
224
228
|
* same rule covers drift on top of a verified tree. An identical tree (empty drift, a rebase
|
|
225
229
|
* that only rewrote SHAs) is the verified bytes themselves and seeds an observed green. Any
|
|
226
230
|
* other drift seeds an inherited one, which attribution re-checks before trusting
|
|
@@ -264,7 +268,7 @@ async function resolveConflict(ctx, wt, preMergeHead) {
|
|
|
264
268
|
return false;
|
|
265
269
|
const files = await conflictedFiles(wt);
|
|
266
270
|
// Deterministic first pass (plans/parallel-work-instances.md, part 3/7): insert-only
|
|
267
|
-
// conflicts in the backlog markdown — two
|
|
271
|
+
// conflicts in the backlog markdown — two merges pasting different entries as the first
|
|
268
272
|
// under ## Done — carry no authored bytes, so they are settled without a model run. Only the
|
|
269
273
|
// files it could not resolve (code, or a same-entry edit) reach the resolver below.
|
|
270
274
|
const remaining = await resolveBacklogInsertConflicts(wt, files);
|
|
@@ -274,7 +278,7 @@ async function resolveConflict(ctx, wt, preMergeHead) {
|
|
|
274
278
|
return continueOrAbortRebase(wt);
|
|
275
279
|
}
|
|
276
280
|
// The prompt names the project's own check, detected the way the in-lock re-check detects it,
|
|
277
|
-
// as the one
|
|
281
|
+
// as the one verifyMerge runs on the resolution — so the resolver neither guesses a runner
|
|
278
282
|
// (BUGS.md 2026-10-05) nor re-runs the check itself while the merge slot waits on it.
|
|
279
283
|
const check = detectBuildCheck(wt, ctx.config) ?? undefined;
|
|
280
284
|
// Show both sides' intent: the change's own commit message and the main commits that touched
|
|
@@ -283,20 +287,26 @@ async function resolveConflict(ctx, wt, preMergeHead) {
|
|
|
283
287
|
const since = await changeBaseRev(wt, ctx.mainBranch, preMergeHead);
|
|
284
288
|
const change = (await commitMessage(wt, preMergeHead)) ?? "";
|
|
285
289
|
const { commits, omitted } = await mainCommitsTouching(wt, since, ctx.mainBranch, remaining);
|
|
286
|
-
// The
|
|
287
|
-
// the run holds the merge slot, and every queued
|
|
288
|
-
// ends, whatever happened — the merge's git steps come next, or the
|
|
289
|
-
|
|
290
|
+
// The merge cell names the resolver run, not "merging" (a no-op outside a queued merge):
|
|
291
|
+
// the run holds the merge slot, and every queued merge waits on it. Back to `merging` once it
|
|
292
|
+
// ends, whatever happened — the merge's git steps come next, or the merge ends.
|
|
293
|
+
setMergeStage(ctx.root, ctx.role, "resolving");
|
|
290
294
|
let pi;
|
|
291
295
|
try {
|
|
292
|
-
pi = await ctx.runPi(wt,
|
|
296
|
+
pi = await ctx.runPi(wt,
|
|
297
|
+
// Each file's hunk ranges, so the resolver reads the hunks rather than the file from line 1.
|
|
298
|
+
buildConflictPrompt(ctx.role, conflictedFileHunks(wt, remaining), check, {
|
|
299
|
+
change,
|
|
300
|
+
main: commits,
|
|
301
|
+
mainOmitted: omitted,
|
|
302
|
+
}), `tumwater-${ctx.role}-${ctx.tick}-conflict`,
|
|
293
303
|
// The resolver rides the strong tier (plans/model-tiers.md part 4/8): resolution is rare,
|
|
294
|
-
// tolerant of latency, and edits code inside
|
|
304
|
+
// tolerant of latency, and edits code inside merge. Its spend still folds into the
|
|
295
305
|
// authoring role's usage — the fold belongs to the loop's wiring, not to the config.
|
|
296
306
|
resolverConfig(ctx.config));
|
|
297
307
|
}
|
|
298
308
|
finally {
|
|
299
|
-
|
|
309
|
+
setMergeStage(ctx.root, ctx.role, "merging");
|
|
300
310
|
}
|
|
301
311
|
if (!pi.ok || hasConflictMarkers(wt, remaining)) {
|
|
302
312
|
await abortSync(wt);
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { gateRoleConfig } from "../gates/budget-gates.js";
|
|
5
|
+
import { reviewRunConfig, seamConcurrencyKey } from "../config/config-views.js";
|
|
6
|
+
import { providerWaitStage } from "./merge-slot.js";
|
|
7
|
+
import { withProviderLease } from "../concurrency/provider-lease.js";
|
|
8
|
+
import { LoopRunner } from "../loop/loop.js";
|
|
9
|
+
import { branchHead, isMergedInto } from "../git/git.js";
|
|
10
|
+
import { addMergeChange, mergeUsage, removeMergeChange, setMergeChangeStatus } from "./merge-slot.js";
|
|
11
|
+
import { dropMerge, entryRevisionFields, queuedMergeFiles, staleHeadFile } from "./merge-queue.js";
|
|
12
|
+
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
13
|
+
import { errorMessage } from "../text/text.js";
|
|
14
|
+
import { verifyRequest } from "./merge-batch.js";
|
|
15
|
+
import { abortOnShutdown, acquireUnlessAborted, discardPinnedRefs, guardMergeTask, liveRoles, settleMergeOutcome, } from "./merge-pipeline.js";
|
|
16
|
+
/** The semaphore tier a verification waits at, below every roleTier (0/1): committed work whose
|
|
17
|
+
* author the interlock has already blocked jumps ahead of parked role waiters rather than starving
|
|
18
|
+
* behind them (BUGS.md 2026-09-18). Exported for merge-drain.ts, whose merge tier sits one
|
|
19
|
+
* below it. */
|
|
20
|
+
export const VERIFY_TIER = -1;
|
|
21
|
+
/** How many verifications may be in flight (parked or running) at once: every `maxConcurrent`
|
|
22
|
+
* permit but one, so a deep merge queue never takes the last slot from the roles that could
|
|
23
|
+
* author — at least one, so `maxConcurrent` 1 still merges. The merge's short conflict-resolution
|
|
24
|
+
* run is not counted: it is the one serial step every queued change waits on. */
|
|
25
|
+
export function verifyLimit(maxConcurrent) {
|
|
26
|
+
return Math.max(1, maxConcurrent - 1);
|
|
27
|
+
}
|
|
28
|
+
/** Resolve a merge entry's authoring runner: the live runner when the role is enabled
|
|
29
|
+
* (runners are never removed from the array on disable — only a warning event fires); a role
|
|
30
|
+
* disabled before this process started has no runner, so a throwaway one supplies the same
|
|
31
|
+
* wiring (loop-pi.ts, runMergePi, foldMergeUsage) and a disk-loaded state to fold and save
|
|
32
|
+
* on. Both share the live config, like every runner — the director takes directorConfig (the live
|
|
33
|
+
* config, its budget-gate exemption, unless a used-up window moved it), every other role takes
|
|
34
|
+
* roleConfig (gateRoleConfig's single home for the rule; the scheduler assigns the live runners
|
|
35
|
+
* by it every poll). */
|
|
36
|
+
function resolveAuthor(ctx, role) {
|
|
37
|
+
const { root, mainBranch, signal, runners, liveConfig, roleConfig, directorConfig } = ctx;
|
|
38
|
+
return (runners.find((r) => r.role === role) ??
|
|
39
|
+
new LoopRunner(root, role, gateRoleConfig(role, liveConfig, roleConfig, directorConfig), mainBranch, signal));
|
|
40
|
+
}
|
|
41
|
+
/** Drop a torn queue head. A torn head (torn or foreign — a partial write an older build left,
|
|
42
|
+
* a hand-placed file) makes headMerge read null forever — nothing else drops it, stranding
|
|
43
|
+
* every live entry behind it and pinning their authors' ticks via the interlock. Drop it with
|
|
44
|
+
* one warning; the healthy entries behind it are verified in the same poll. The crashed
|
|
45
|
+
* entry's commit, if any, still rides its merge ref into next-tick leftover recovery
|
|
46
|
+
* (BUGS.md 2026-09-17). */
|
|
47
|
+
function dropTornHead(root) {
|
|
48
|
+
const stale = staleHeadFile(root);
|
|
49
|
+
if (!stale)
|
|
50
|
+
return;
|
|
51
|
+
dropMerge(stale);
|
|
52
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `merge queue head ${path.basename(stale)} is unreadable (torn or foreign) — dropped so the queue keeps moving`);
|
|
53
|
+
}
|
|
54
|
+
/** Already-merged verdicts cached per (root, sha) against the main head they were computed
|
|
55
|
+
* against. drainVerify re-runs its dedupe against main every poll, but a queued entry's sha is
|
|
56
|
+
* fixed and merge-base --is-ancestor against the same head always returns the same answer — the
|
|
57
|
+
* verdict can only change when main itself moves (a merge's fast-forward). Serving the verdict
|
|
58
|
+
* from this cache makes a poll whose queue is waiting on busy authors or the merge slot cost one
|
|
59
|
+
* branchHead ref-file read instead of one git spawn per entry, and a moved main recomputes each
|
|
60
|
+
* entry's verdict once (the new head misses the cache). Bounded: verdicts computed against an
|
|
61
|
+
* older head are stale falses no future poll can reuse, so they are the first pruned. */
|
|
62
|
+
const mergedIntoMainCache = new Map();
|
|
63
|
+
const MERGED_INTO_MAIN_CACHE_MAX = 128;
|
|
64
|
+
/** The cached dedupe verdict for `sha` against main at `head`, computing it through
|
|
65
|
+
* isMergedInto on a cache miss. `head` is the caller's already-resolved main head ("" when main
|
|
66
|
+
* does not exist yet — isMergedInto then fails and reads false, a verdict worth caching like any
|
|
67
|
+
* other since the key matches). */
|
|
68
|
+
async function mergedIntoMain(root, sha, mainBranch, head) {
|
|
69
|
+
const key = `${root}\u0000${sha}`;
|
|
70
|
+
const hit = mergedIntoMainCache.get(key);
|
|
71
|
+
if (hit?.head === head)
|
|
72
|
+
return hit.merged;
|
|
73
|
+
const merged = await isMergedInto(root, sha, mainBranch);
|
|
74
|
+
if (mergedIntoMainCache.size >= MERGED_INTO_MAIN_CACHE_MAX && !mergedIntoMainCache.has(key)) {
|
|
75
|
+
for (const [k, v] of mergedIntoMainCache)
|
|
76
|
+
if (v.head !== head)
|
|
77
|
+
mergedIntoMainCache.delete(k);
|
|
78
|
+
if (mergedIntoMainCache.size >= MERGED_INTO_MAIN_CACHE_MAX)
|
|
79
|
+
mergedIntoMainCache.clear();
|
|
80
|
+
}
|
|
81
|
+
mergedIntoMainCache.set(key, { head, merged });
|
|
82
|
+
return merged;
|
|
83
|
+
}
|
|
84
|
+
/** Start a verification, in queue order, for every queued entry whose role the pipeline does not
|
|
85
|
+
* already hold, up to verifyLimit — the shared semaphore bounds how many of them run. The torn-head
|
|
86
|
+
* drop comes first, and each entry is deduped against main before its verification starts: an entry
|
|
87
|
+
* whose sha main already holds (a crash between the fast-forward and the drop) is dropped without a
|
|
88
|
+
* run, with its marker record; a crash mid-verification leaves both entry and ref, so the entry is
|
|
89
|
+
* verified again — the established crash semantics. */
|
|
90
|
+
export async function drainVerify(ctx, p) {
|
|
91
|
+
dropTornHead(ctx.root);
|
|
92
|
+
const queue = queuedMergeFiles(ctx.root);
|
|
93
|
+
// One main-head read per poll with a non-empty queue (branchHead's ref-file fast path — no
|
|
94
|
+
// spawn), serving every entry's cached dedupe verdict below.
|
|
95
|
+
const mainHead = queue.length === 0 ? "" : ((await branchHead(ctx.root, ctx.mainBranch)) ?? "");
|
|
96
|
+
for (const { entry, file } of queue) {
|
|
97
|
+
if (p.verifying.size >= verifyLimit(ctx.semaphore.limit))
|
|
98
|
+
return;
|
|
99
|
+
if (liveRoles(p).has(entry.role))
|
|
100
|
+
continue;
|
|
101
|
+
if (await mergedIntoMain(ctx.root, entry.sha, ctx.mainBranch, mainHead)) {
|
|
102
|
+
dropMerge(file);
|
|
103
|
+
removeMergeChange(ctx.root, entry.role);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// The dedupe awaited git: a task that settled meanwhile may have dropped this very entry
|
|
107
|
+
// (its role was busy when the queue was listed) or claimed its role. Start nothing for it.
|
|
108
|
+
if (!fs.existsSync(file) || liveRoles(p).has(entry.role))
|
|
109
|
+
continue;
|
|
110
|
+
startVerify(ctx, p, entry, file);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Start one verification task and record it in `p.verifying`: parked until a shared permit comes,
|
|
114
|
+
* at VERIFY_TIER (a shutdown settles a parked verification at once, and one granted while the start
|
|
115
|
+
* gate is closed hands its permit back — either way nothing ran, and its entry and pin stay queued
|
|
116
|
+
* with no outcome written), then, holding the permit for its whole length: open the change's
|
|
117
|
+
* marker record and verify it — checkout at the pin, rebase onto main, gate check, review
|
|
118
|
+
* (verifyRequest), the verdict persisted by the gate. An approval moves it to `approved` (record
|
|
119
|
+
* `approved, awaiting merge`). Any other verdict — rejected, a review_error of either kind,
|
|
120
|
+
* main_red, a lost pin's "error", aborted — is final for this queue entry, so its outcome is
|
|
121
|
+
* written at once and its entry dropped, which frees its author on the next poll while the
|
|
122
|
+
* other verifications run on (BUGS.md 2026-09-23's early-rejection rule). A user-aborted
|
|
123
|
+
* verification discards its pin; a shutdown keeps it. Never rejects for a failed merge: an
|
|
124
|
+
* unexpected throw becomes an "error" outcome. Exported for
|
|
125
|
+
* test/fixtures/orchestrator-fixtures.ts's mergeHead, which merges one queue entry through the
|
|
126
|
+
* pipeline without draining the rest of the queue. */
|
|
127
|
+
export function startVerify(ctx, p, entry, file) {
|
|
128
|
+
const { root, mainBranch, signal, semaphore } = ctx;
|
|
129
|
+
const { role } = entry;
|
|
130
|
+
const author = resolveAuthor(ctx, role);
|
|
131
|
+
// The verification's provider key: the review seam's key for the author's role, so its reviewer
|
|
132
|
+
// run counts toward the same provider cap as the roles that share it
|
|
133
|
+
// (plans/provider-concurrency.md §4). The gate off runs no model, so then there is no key — the
|
|
134
|
+
// verification is bounded by the team total alone.
|
|
135
|
+
const reviewKey = ctx.liveConfig.review.enabled
|
|
136
|
+
? seamConcurrencyKey(reviewRunConfig(author.config), ctx.piDefault)
|
|
137
|
+
: undefined;
|
|
138
|
+
const verify = {
|
|
139
|
+
promise: Promise.resolve(), // Replaced below; the placeholder satisfies the type.
|
|
140
|
+
controller: new AbortController(),
|
|
141
|
+
roles: [role],
|
|
142
|
+
userAborted: false,
|
|
143
|
+
parked: true,
|
|
144
|
+
};
|
|
145
|
+
const detachShutdown = abortOnShutdown(signal, verify.controller);
|
|
146
|
+
p.verifying.set(role, verify);
|
|
147
|
+
const task = (async () => {
|
|
148
|
+
try {
|
|
149
|
+
const lease = await acquireUnlessAborted(semaphore, VERIFY_TIER, verify.controller.signal, reviewKey);
|
|
150
|
+
verify.parked = false;
|
|
151
|
+
if (lease === null)
|
|
152
|
+
return; // a shutdown while parked
|
|
153
|
+
if (verify.controller.signal.aborted || ctx.startHeld()) {
|
|
154
|
+
lease.release(); // stopping, or a restart / team hold closed the gate while it waited
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const { usage, foldUsage } = mergeUsage(author, root);
|
|
158
|
+
let verdict;
|
|
159
|
+
const startedAt = Date.now();
|
|
160
|
+
try {
|
|
161
|
+
addMergeChange(root, entry, liveRoles(p));
|
|
162
|
+
// The verification's lease rides in the provider-lease store for the verification's body,
|
|
163
|
+
// so a review run that lands on a different provider than the verification's review seam
|
|
164
|
+
// re-keys it there, and a re-key park is shown by providerWaitStage; its slot wait drops
|
|
165
|
+
// the key (pool-slot rule).
|
|
166
|
+
verdict = await withProviderLease(lease, semaphore, () => verifyRequest({ root, mainBranch, config: author.config, signal: () => verify.controller.signal, runGatePi: (opts) => author.runGatePi(opts) }, {
|
|
167
|
+
role,
|
|
168
|
+
sha: entry.sha,
|
|
169
|
+
tick: entry.run,
|
|
170
|
+
summary: entry.summary,
|
|
171
|
+
body: entry.body,
|
|
172
|
+
highFriction: entry.highFriction,
|
|
173
|
+
...entryRevisionFields(entry),
|
|
174
|
+
}, { state: author.state, foldUsage, runPi: (w, prompt, s, cfg) => author.runMergePi(w, prompt, s, cfg), runGatePi: (opts) => author.runGatePi(opts) }), { piDefault: ctx.piDefault, hooks: providerWaitStage(root, role) });
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
verdict = { kind: "result", result: "error" };
|
|
178
|
+
author.state.lastError = errorMessage(err);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
lease.release();
|
|
182
|
+
}
|
|
183
|
+
if (verdict.kind === "stack" && !verify.userAborted) {
|
|
184
|
+
p.approved.set(role, {
|
|
185
|
+
entry,
|
|
186
|
+
file,
|
|
187
|
+
sha: verdict.sha,
|
|
188
|
+
...(verdict.verifiedHead !== undefined ? { verifiedHead: verdict.verifiedHead } : {}),
|
|
189
|
+
startedAt,
|
|
190
|
+
author,
|
|
191
|
+
usage,
|
|
192
|
+
foldUsage,
|
|
193
|
+
roles: [role],
|
|
194
|
+
controller: new AbortController(),
|
|
195
|
+
userAborted: false,
|
|
196
|
+
});
|
|
197
|
+
setMergeChangeStatus(root, role, "approved");
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const result = verdict.kind === "stack" ? "aborted" : verdict.result;
|
|
201
|
+
await settleMergeOutcome(root, entry, author.state, result, Date.now() - startedAt, usage, file);
|
|
202
|
+
if (verify.userAborted)
|
|
203
|
+
await discardPinnedRefs(root, [role]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
detachShutdown();
|
|
208
|
+
p.verifying.delete(role);
|
|
209
|
+
}
|
|
210
|
+
})();
|
|
211
|
+
verify.promise = guardMergeTask(root, role, task);
|
|
212
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// vocabulary: plain
|
|
3
|
+
/** The `tumwater-notes` command — the second bin entry (dist/src/notes-cli.js), the one
|
|
4
|
+
* carrier for writing a role's note on every backend (plans/notes-command.md "The
|
|
5
|
+
* `tumwater-notes` command"). A run finds it on its PATH (src/agent/notes-shim.ts) and hands
|
|
6
|
+
* it only the note `TUMWATER_NOTES_PATH` names — the path comes from the environment tumwater
|
|
7
|
+
* set, never from an argument. It replaces the three legacy carriers (the pi extension, and
|
|
8
|
+
* the MCP server for Claude Code and OpenCode), validating and writing through the same
|
|
9
|
+
* validateRoleNote / writeRoleNote as those, so the 4 KB cap and atomic write cannot drift.
|
|
10
|
+
*
|
|
11
|
+
* Subcommands:
|
|
12
|
+
* write replace the note with stdin (refuses empty input)
|
|
13
|
+
* write --file <p> replace the note with the file at <p>
|
|
14
|
+
* show print the note (nothing when there is none)
|
|
15
|
+
* clear empty the note
|
|
16
|
+
*
|
|
17
|
+
* Exits 0 on success, 1 on a refusal (empty input, over-cap note, or no
|
|
18
|
+
* TUMWATER_NOTES_PATH), and 2 with usage for anything else. */
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import { validateRoleNote, writeRoleNote } from "./agent/role-notes-write.js";
|
|
21
|
+
/** The note's path, read ONLY from the environment — never from an argument. */
|
|
22
|
+
const NOTES_PATH = process.env.TUMWATER_NOTES_PATH;
|
|
23
|
+
const USAGE = [
|
|
24
|
+
"usage: tumwater-notes <write [--file <path>] | show | clear>",
|
|
25
|
+
" write replaces the note with stdin, clear empties it, show prints it (--file reads <path> instead of stdin)",
|
|
26
|
+
" the note is the file TUMWATER_NOTES_PATH names",
|
|
27
|
+
].join("\n");
|
|
28
|
+
/** Read the child's stdin to EOF as UTF-8. */
|
|
29
|
+
function readStdin() {
|
|
30
|
+
return fs.readFileSync(0, "utf8");
|
|
31
|
+
}
|
|
32
|
+
/** Read the note's current text; a missing note reads as empty (show prints nothing). */
|
|
33
|
+
function readNote() {
|
|
34
|
+
try {
|
|
35
|
+
return fs.readFileSync(NOTES_PATH, "utf8");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Refuse with `message` on stderr and exit 1 — the exit code for every expected refusal. */
|
|
42
|
+
function refuse(message) {
|
|
43
|
+
process.stderr.write(`${message}\n`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
/** Write a non-empty note and announce it; assumes NOTES_PATH is set. Returns the exit code. */
|
|
47
|
+
function writeNote(text) {
|
|
48
|
+
if (text === "") {
|
|
49
|
+
return refuse("nothing to write — use `tumwater-notes clear` to empty the note");
|
|
50
|
+
}
|
|
51
|
+
const error = validateRoleNote(text);
|
|
52
|
+
if (error)
|
|
53
|
+
return refuse(error);
|
|
54
|
+
writeRoleNote(NOTES_PATH, text);
|
|
55
|
+
process.stdout.write(`notes saved (${Buffer.byteLength(text, "utf8")} bytes)\n`);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
function main() {
|
|
59
|
+
if (NOTES_PATH === undefined || NOTES_PATH === "") {
|
|
60
|
+
return refuse("this run keeps no notes");
|
|
61
|
+
}
|
|
62
|
+
const args = process.argv.slice(2);
|
|
63
|
+
const [command, flag, flagArg] = args;
|
|
64
|
+
switch (command) {
|
|
65
|
+
case "write":
|
|
66
|
+
if (flag === undefined)
|
|
67
|
+
return writeNote(readStdin());
|
|
68
|
+
if (flag === "--file") {
|
|
69
|
+
if (flagArg === undefined || args.length > 3)
|
|
70
|
+
return refuse(USAGE);
|
|
71
|
+
let text;
|
|
72
|
+
try {
|
|
73
|
+
text = fs.readFileSync(flagArg, "utf8");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return refuse(`cannot read ${flagArg}`);
|
|
77
|
+
}
|
|
78
|
+
return writeNote(text);
|
|
79
|
+
}
|
|
80
|
+
return refuse(USAGE);
|
|
81
|
+
case "show":
|
|
82
|
+
if (flag !== undefined)
|
|
83
|
+
return refuse(USAGE);
|
|
84
|
+
process.stdout.write(readNote());
|
|
85
|
+
return 0;
|
|
86
|
+
case "clear":
|
|
87
|
+
if (flag !== undefined)
|
|
88
|
+
return refuse(USAGE);
|
|
89
|
+
writeRoleNote(NOTES_PATH, "");
|
|
90
|
+
process.stdout.write("notes cleared\n");
|
|
91
|
+
return 0;
|
|
92
|
+
default:
|
|
93
|
+
process.stderr.write(`${USAGE}\n`);
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
process.exit(main());
|