tumwater 0.2.0 → 0.4.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 +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +55 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +6 -4
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +404 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +281 -55
- package/dist/src/backlog/backlog-cli-view.js +255 -0
- package/dist/src/backlog/backlog-cli.js +91 -335
- package/dist/src/backlog/backlog-commit.js +20 -13
- package/dist/src/backlog/backlog-eligibility.js +193 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +264 -0
- package/dist/src/backlog/backlog-release.js +134 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-guardrails.js +136 -0
- package/dist/src/baseline/main-red.js +153 -36
- package/dist/src/baseline/main-watch.js +232 -0
- package/dist/src/brief/brief-edit.js +12 -12
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +31 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +65 -3
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +181 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +183 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +291 -598
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +525 -0
- package/dist/src/cli/cli-start-summary.js +167 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
- package/dist/src/cli/completion.js +169 -22
- package/dist/src/cli/config-commands.js +58 -26
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +258 -82
- package/dist/src/cli/log-commands.js +164 -85
- package/dist/src/cli/question-commands.js +211 -10
- package/dist/src/cli.js +173 -45
- package/dist/src/collections.js +56 -13
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +16 -1
- package/dist/src/config/config-fallbacks.js +94 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +169 -222
- package/dist/src/config/config-views.js +136 -54
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +164 -265
- package/dist/src/config/config.js +53 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
- package/dist/src/doctor/doctor-backlog-plans.js +398 -0
- package/dist/src/doctor/doctor-backlog.js +249 -512
- package/dist/src/doctor/doctor-check-names.js +85 -0
- package/dist/src/doctor/doctor-checks.js +61 -593
- package/dist/src/doctor/doctor-claude-checks.js +142 -0
- package/dist/src/doctor/doctor-codex-checks.js +276 -0
- package/dist/src/doctor/doctor-contention-checks.js +506 -0
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +80 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +216 -537
- package/dist/src/doctor/doctor-notify.js +9 -5
- package/dist/src/doctor/doctor-opencode-checks.js +164 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +397 -0
- package/dist/src/doctor/doctor.js +126 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +21 -12
- package/dist/src/events/event-format.js +185 -89
- package/dist/src/events/event-labels.js +11 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +52 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +80 -26
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +65 -60
- package/dist/src/failure/failure-render.js +29 -21
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +35 -35
- package/dist/src/files/file-queue.js +17 -0
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +88 -225
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +101 -143
- package/dist/src/gates/maintenance-quota.js +37 -30
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +89 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +54 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +43 -19
- package/dist/src/gui/gui-backlog-endpoints.js +156 -0
- package/dist/src/gui/gui-command.js +26 -13
- package/dist/src/gui/gui-config-commands.js +168 -80
- package/dist/src/gui/gui-endpoint-commands.js +5 -4
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +235 -13
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +97 -0
- package/dist/src/gui/gui-server.js +26 -5
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +155 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +190 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +28 -17
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +58 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +45 -19
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +214 -0
- package/dist/src/merge/merge-queue-render.js +48 -11
- package/dist/src/merge/merge-queue.js +52 -15
- package/dist/src/merge/merge-requeue.js +103 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +27 -11
- package/dist/src/merge/merge-to-main.js +77 -26
- package/dist/src/merge/merge-top.js +83 -0
- package/dist/src/merge/merge-verify.js +75 -18
- package/dist/src/notes-cli.js +35 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +352 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
- package/dist/src/orchestrator/orchestrator.js +129 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +245 -0
- package/dist/src/orchestrator/scheduling-skip.js +195 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +126 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +26 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +150 -217
- package/dist/src/prompt/notes-backlog.js +184 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +131 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +188 -124
- package/dist/src/report/report-fold.js +130 -54
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +136 -65
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +23 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +112 -1
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +239 -0
- package/dist/src/review/protected-edits.js +24 -10
- package/dist/src/review/quarantine-render.js +65 -0
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +180 -401
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +127 -12
- package/dist/src/roles/role-guidance.js +65 -20
- package/dist/src/roles/role-labels.js +7 -5
- package/dist/src/roles/role-render.js +108 -56
- package/dist/src/roles/role-view.js +95 -56
- package/dist/src/roles/roles.js +33 -33
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/run/qa-coverage.js +125 -0
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +175 -0
- package/dist/src/run/run-detail-render.js +91 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +178 -0
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +231 -0
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +216 -0
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +191 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +109 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +103 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claim-entries.js +49 -0
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +49 -17
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +43 -28
- package/dist/src/status/status-roles.js +50 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +7 -8
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +65 -18
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +60 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +78 -13
- package/dist/src/ui/badges.js +99 -38
- package/dist/src/ui/gui/gui-client-backlog.js +314 -0
- package/dist/src/ui/gui/gui-client-boot.js +89 -61
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
- package/dist/src/ui/gui/gui-client-drawer.js +155 -289
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +437 -68
- package/dist/src/ui/gui/gui-client-labels.js +6 -3
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +60 -52
- package/dist/src/ui/gui/gui-client-pending.js +405 -24
- package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
- package/dist/src/ui/gui/gui-client-report.js +105 -430
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +163 -5
- package/dist/src/ui/gui/gui-client-team.js +36 -465
- package/dist/src/ui/gui/gui-client.js +38 -26
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +10 -7
- package/dist/src/ui/gui/gui-styles.js +38 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +364 -49
- package/dist/src/ui/status-render.js +109 -82
- package/dist/src/ui/team-alerts.js +728 -62
- package/dist/src/ui/tone.js +12 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +53 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +4 -12
- package/dist/src/ui/tui/tui-editor-backends.js +127 -0
- package/dist/src/ui/tui/tui-editor-brief.js +14 -36
- package/dist/src/ui/tui/tui-editor-budget.js +36 -39
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
- package/dist/src/ui/tui/tui-editor-config.js +99 -0
- package/dist/src/ui/tui/tui-editor-entry.js +39 -64
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +46 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
- package/dist/src/ui/tui/tui-editor-interval.js +29 -16
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +9 -29
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
- package/dist/src/ui/tui/tui-editor-retire.js +5 -13
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +101 -34
- package/dist/src/ui/tui/tui-editor-slots.js +24 -0
- package/dist/src/ui/tui/tui-editors.js +44 -2
- package/dist/src/ui/tui/tui-frame.js +68 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +33 -9
- package/dist/src/ui/tui/tui-pane-activity.js +35 -19
- package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +168 -8
- package/dist/src/ui/tui/tui-pane-history.js +446 -82
- package/dist/src/ui/tui/tui-pane-pending.js +539 -15
- package/dist/src/ui/tui/tui-pane-queued.js +78 -57
- package/dist/src/ui/tui/tui-pane-report.js +54 -16
- package/dist/src/ui/tui/tui-pane-session.js +34 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +91 -28
- package/dist/src/ui/tui/tui-panes.js +153 -34
- package/dist/src/ui/tui/tui.js +18 -15
- package/dist/src/verdict/fix-claim.js +129 -33
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +77 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/leftover.js +0 -130
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/qa-coverage.js +0 -108
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { writeBytesAtomic } from "../files/files.js";
|
|
3
|
+
import { GitError, committerIdentArgs, git, gitCouldNotStart, gitTry, runGit } from "../git/git-run.js";
|
|
4
|
+
import { readTextOrNull, writeBytesAtomic } from "../files/files.js";
|
|
5
5
|
import { currentBranch, gitLines, headOf } from "../git/git.js";
|
|
6
|
-
import { unquotePorcelainPath } from "../git/git-diff.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { treeDiffFiles, unquotePorcelainPath } from "../git/git-diff.js";
|
|
7
|
+
import { errCode } from "../errno.js";
|
|
8
|
+
import { CONFIG_BASENAME, configPath } from "../paths.js";
|
|
9
|
+
import { withPrimaryWorktreeLockAsync } from "../concurrency/lock.js";
|
|
10
|
+
import { TUMWATER_SOURCE, warnEventBestEffort } from "../events/events.js";
|
|
11
|
+
import { errorFirstLine, errorMessage, joinWithMore } from "../text/text.js";
|
|
10
12
|
/** Git plumbing for the merge flow: rebasing a worktree branch onto main, inspecting and
|
|
11
13
|
* finishing a conflicted rebase, and fast-forwarding main — the mechanics, with no merge policy
|
|
12
14
|
* of their own. Merge policy (lock → rebase → verify → ff, conflict resolution, batch ff)
|
|
13
15
|
* lives in merge-to-main.ts on top of these. Moved here from merge-to-main.ts: the cluster started
|
|
14
|
-
* as merge-to-main.ts-only helpers (organize
|
|
16
|
+
* as merge-to-main.ts-only helpers (organize run 78, moving them out of git/git.ts), but the
|
|
15
17
|
* merger and the batch merger grew to call them directly, leaving merge-to-main.ts exporting
|
|
16
18
|
* generic rebase/ff plumbing that had nothing to do with merging. */
|
|
17
19
|
/** Paths currently in conflict (unmerged) in the worktree. C-quoted names are decoded to
|
|
18
20
|
* real paths — a non-ASCII conflicted file arrives as `"h\303\251llo.ts"` (core.quotePath is on
|
|
19
21
|
* by default), and undecoded it does not exist on disk: hasConflictMarkers could never read
|
|
20
22
|
* it, so an unresolved conflict in such a file passed the marker check and continueRebase
|
|
21
|
-
* committed its markers to main.
|
|
23
|
+
* committed its markers to main. A failed `git diff` throws: read as "no conflicts", it sent
|
|
24
|
+
* the resolver's caller straight to `continueRebase`, which stages every marker, and a
|
|
25
|
+
* stopped cherry-pick down the path that deletes a hand-back's ref. */
|
|
22
26
|
export async function conflictedFiles(wt) {
|
|
23
|
-
const out = await
|
|
27
|
+
const out = await git(wt, "diff", "--name-only", "--diff-filter=U");
|
|
24
28
|
return gitLines(out).map(unquotePorcelainPath);
|
|
25
29
|
}
|
|
26
30
|
/** How many main commits the resolver prompt lists, and how many body lines it keeps per
|
|
@@ -61,22 +65,26 @@ export async function mainCommitsTouching(wt, since, mainBranch, files) {
|
|
|
61
65
|
* cleaning up: "rebased" (including already up to date), "conflict" (the rebase stopped on
|
|
62
66
|
* unmerged paths, which remain in the worktree for a resolver), or "other" (any other
|
|
63
67
|
* failure). Shared by the two rebase wrappers below, which differ only in cleanup policy.
|
|
64
|
-
* Rebase — not merge — keeps main's history linear: each
|
|
65
|
-
* of whatever main holds.
|
|
68
|
+
* Rebase — not merge — keeps main's history linear: each run merges as its own commit on top
|
|
69
|
+
* of whatever main holds. A rebase git could not start throws (gitCouldNotStart), as does a
|
|
70
|
+
* failed conflict listing: neither says anything about the change, so neither may count as a
|
|
71
|
+
* merge conflict. */
|
|
66
72
|
async function attemptRebase(wt, mainBranch) {
|
|
67
73
|
try {
|
|
68
74
|
// The -c ident is needed for the rewritten committer identity. diff3 carries the base
|
|
69
75
|
// section of every conflict, which the deterministic backlog resolver reads
|
|
70
76
|
// (plans/parallel-work-instances.md, part 3/7); rebaseOntoMain aborts on conflict, so the
|
|
71
77
|
// marker style only matters to its leave-conflicts sibling.
|
|
72
|
-
await runGit(wt, [...
|
|
78
|
+
await runGit(wt, [...(await committerIdentArgs(wt)), "-c", "merge.conflictStyle=diff3", "rebase", mainBranch]);
|
|
73
79
|
return "rebased";
|
|
74
80
|
}
|
|
75
|
-
catch {
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (gitCouldNotStart(err))
|
|
83
|
+
throw err;
|
|
76
84
|
return (await conflictedFiles(wt)).length > 0 ? "conflict" : "other";
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
|
-
/** Rebase the worktree branch onto main (main may have advanced during the
|
|
87
|
+
/** Rebase the worktree branch onto main (main may have advanced during the run).
|
|
80
88
|
* Returns false and aborts the rebase on conflict. */
|
|
81
89
|
export async function rebaseOntoMain(wt, mainBranch) {
|
|
82
90
|
const state = await attemptRebase(wt, mainBranch);
|
|
@@ -98,23 +106,44 @@ export async function rebaseOntoMainLeaveConflicts(wt, mainBranch) {
|
|
|
98
106
|
* The one marker definition hasConflictMarkers and conflictHunkRanges share, so the ranges a
|
|
99
107
|
* conflict prompt names are exactly the blocks the marker check later rejects. */
|
|
100
108
|
const MARKER_EDGE = /^(<{7}|>{7})( |$)/m;
|
|
109
|
+
/** One file's marker check: "clean", "marked" (holds a start/end marker) or "unreadable". Only
|
|
110
|
+
* a path with no file (deleted, a parent replaced, or a directory/gitlink) is clean without
|
|
111
|
+
* being read; any other read error cannot be vouched for. */
|
|
112
|
+
function markerState(wt, f) {
|
|
113
|
+
// lstat, not stat: a symlink is never followed out of the worktree. A symlink (or a
|
|
114
|
+
// directory/gitlink) holds no text of its own to check, so it counts as clean — the same
|
|
115
|
+
// policy secret-leaks (INV-G4) already pins: a change that adds a symlink writes no
|
|
116
|
+
// content, and its target is not what the worktree put there.
|
|
117
|
+
let st;
|
|
118
|
+
try {
|
|
119
|
+
st = fs.lstatSync(path.join(wt, f));
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
const code = errCode(err);
|
|
123
|
+
return code === "ENOENT" || code === "ENOTDIR" ? "clean" : "unreadable";
|
|
124
|
+
}
|
|
125
|
+
if (!st.isFile())
|
|
126
|
+
return "clean";
|
|
127
|
+
let text;
|
|
128
|
+
try {
|
|
129
|
+
text = fs.readFileSync(path.join(wt, f), "utf8");
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const code = errCode(err);
|
|
133
|
+
return code === "ENOENT" || code === "ENOTDIR" || code === "EISDIR" ? "clean" : "unreadable";
|
|
134
|
+
}
|
|
135
|
+
return MARKER_EDGE.test(text) ? "marked" : "clean";
|
|
136
|
+
}
|
|
101
137
|
/** True if any of the given files still contains a git conflict marker.
|
|
102
|
-
* A
|
|
138
|
+
* A missing path counts as resolved (the resolver chose the deletion), but an unreadable one
|
|
139
|
+
* counts as conflicted: a check that cannot read a file cannot vouch for it. */
|
|
103
140
|
export function hasConflictMarkers(wt, files) {
|
|
104
141
|
// Only start/end markers are checked: every real conflict block carries them, while a bare
|
|
105
142
|
// `=======` line is legitimate content (a markdown setext or RST underline of exactly seven
|
|
106
143
|
// characters), and flagging it would reject clean resolutions forever. A resolver that
|
|
107
144
|
// leaves only a separator line behind is treated as resolved; its stray line is content the
|
|
108
145
|
// project's own tests can catch.
|
|
109
|
-
return files.some((f) =>
|
|
110
|
-
const p = path.join(wt, f);
|
|
111
|
-
try {
|
|
112
|
-
return MARKER_EDGE.test(fs.readFileSync(p, "utf8"));
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
});
|
|
146
|
+
return files.some((f) => markerState(wt, f) !== "clean");
|
|
118
147
|
}
|
|
119
148
|
/** How many conflict hunks a conflict prompt lists per file: every hunk of a typical backlog or
|
|
120
149
|
* code conflict, and a bound on a file pasted over wholesale. */
|
|
@@ -150,13 +179,17 @@ export function conflictHunkRanges(text) {
|
|
|
150
179
|
* file counts, backlog or code; a deleted, unreadable or binary (NUL-bearing) file lists none. */
|
|
151
180
|
export function conflictedFileHunks(wt, files) {
|
|
152
181
|
return files.map((f) => {
|
|
153
|
-
|
|
182
|
+
// Deleted or unreadable reads as empty: no ranges to name. A symlink is never followed
|
|
183
|
+
// out of the worktree (it holds no text of its own), so it too reads as empty.
|
|
184
|
+
let full = path.join(wt, f);
|
|
154
185
|
try {
|
|
155
|
-
|
|
186
|
+
if (!fs.lstatSync(full).isFile())
|
|
187
|
+
return { path: f, hunks: [], more: 0 };
|
|
156
188
|
}
|
|
157
189
|
catch {
|
|
158
|
-
|
|
190
|
+
return { path: f, hunks: [], more: 0 };
|
|
159
191
|
}
|
|
192
|
+
const text = readTextOrNull(full) ?? "";
|
|
160
193
|
const all = text.includes("\0") ? [] : conflictHunkRanges(text);
|
|
161
194
|
return { path: f, hunks: all.slice(0, MAX_LISTED_HUNKS), more: Math.max(0, all.length - MAX_LISTED_HUNKS) };
|
|
162
195
|
});
|
|
@@ -168,8 +201,17 @@ export function conflictedFileHunks(wt, files) {
|
|
|
168
201
|
* lines on main (PLANS.md "Robust conflict landing, part 2/2"). Builds on hasConflictMarkers
|
|
169
202
|
* above, the shared definition of what a marker is. */
|
|
170
203
|
export function conflictMarkerReason(wt, files) {
|
|
171
|
-
const
|
|
172
|
-
|
|
204
|
+
const states = files.map((f) => [f, markerState(wt, f)]);
|
|
205
|
+
const named = (state) => states.filter(([, s]) => s === state).map(([f]) => f);
|
|
206
|
+
const marked = named("marked");
|
|
207
|
+
const unreadable = named("unreadable");
|
|
208
|
+
const clauses = [
|
|
209
|
+
...(marked.length > 0 ? [`conflict markers remain in: ${marked.join(", ")}`] : []),
|
|
210
|
+
...(unreadable.length > 0
|
|
211
|
+
? [`could not be read to check for conflict markers: ${unreadable.join(", ")}`]
|
|
212
|
+
: []),
|
|
213
|
+
];
|
|
214
|
+
return clauses.length > 0 ? clauses.join("; ") : undefined;
|
|
173
215
|
}
|
|
174
216
|
/** Conclude an in-progress rebase with everything in the worktree as the resolution.
|
|
175
217
|
* GIT_EDITOR=true so `rebase --continue` can never block on a commit-message prompt. A
|
|
@@ -179,24 +221,40 @@ export function conflictMarkerReason(wt, files) {
|
|
|
179
221
|
* added; the caller aborts and reports merge_conflict. */
|
|
180
222
|
export async function continueRebase(wt) {
|
|
181
223
|
await git(wt, "add", "-A");
|
|
182
|
-
await runGit(wt, [...
|
|
224
|
+
await runGit(wt, [...(await committerIdentArgs(wt)), "rebase", "--continue"], { GIT_EDITOR: "true" });
|
|
183
225
|
return headOf(wt, "HEAD");
|
|
184
226
|
}
|
|
185
227
|
/** The live config's bytes, when the merge about to fast-forward `ref` would delete it
|
|
186
228
|
* (plans/portability.md §4b/7): the config file exists, is tracked in the current index (and
|
|
187
229
|
* HEAD — a staged-or-dirty config makes `git merge --ff-only` refuse below, so the two agree
|
|
188
230
|
* on every merge that can succeed), and is absent from the incoming tree. Anything else —
|
|
189
|
-
* already untracked, already absent, present in `ref` — needs no preserve step.
|
|
231
|
+
* already untracked, already absent, present in `ref` — needs no preserve step. A failed
|
|
232
|
+
* `cat-file` reads as absent, which only preserves bytes the write-back then leaves alone. */
|
|
190
233
|
export async function configBytesToPreserve(root, ref) {
|
|
191
234
|
const cfg = configPath(root);
|
|
192
235
|
if (!fs.existsSync(cfg))
|
|
193
236
|
return null;
|
|
194
|
-
if ((await
|
|
237
|
+
if (!(await configTracked(root)))
|
|
195
238
|
return null;
|
|
196
239
|
if ((await gitTry(root, "cat-file", "-e", `${ref}:${CONFIG_BASENAME}`)) !== null)
|
|
197
240
|
return null;
|
|
198
241
|
return fs.readFileSync(cfg);
|
|
199
242
|
}
|
|
243
|
+
/** Whether the live config is tracked in `root`'s index: `ls-files --error-unmatch` exits 1 for
|
|
244
|
+
* an untracked path, and any other failure throws. Read as "untracked", an unreadable index or
|
|
245
|
+
* a git that could not start skipped the preserve step, so a merge that untracks the config
|
|
246
|
+
* deleted the live one. */
|
|
247
|
+
async function configTracked(root) {
|
|
248
|
+
try {
|
|
249
|
+
await git(root, "ls-files", "--error-unmatch", CONFIG_BASENAME);
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
if (err instanceof GitError && err.code === 1)
|
|
254
|
+
return false;
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
200
258
|
/** The preserve step's write-back, restore-only-when-absent: write the saved bytes only when
|
|
201
259
|
* the live config is absent at write-back time. The merge deletes the file mid-window, so a
|
|
202
260
|
* config request applied in that window (3/7's applyConfigRequest runs outside the merge lock)
|
|
@@ -219,7 +277,9 @@ export function restoreConfigBytes(root, saved) {
|
|
|
219
277
|
* files update), otherwise a local ref push. Returns true on success. The working-tree arm
|
|
220
278
|
* preserves the live config across a merge that untracks it (plans/portability.md §4b/7):
|
|
221
279
|
* without the write-back, the commit that removes the tracked config would delete the team's
|
|
222
|
-
* running config out from under it and the next config poll would fall back to defaults.
|
|
280
|
+
* running config out from under it and the next config poll would fall back to defaults. The
|
|
281
|
+
* merge has already happened by the time of the write-back, so a failed write-back is a warning
|
|
282
|
+
* event and the function still returns null. */
|
|
223
283
|
export async function ffMainToOrWhy(root, ref, mainBranch) {
|
|
224
284
|
const primaryBranch = await currentBranch(root);
|
|
225
285
|
if (primaryBranch === mainBranch) {
|
|
@@ -228,7 +288,7 @@ export async function ffMainToOrWhy(root, ref, mainBranch) {
|
|
|
228
288
|
// that read before this merge can rename its stale whole-file write over the merged change.
|
|
229
289
|
// The ref-push arm below touches no working tree and needs no lock; the save/merge/restore
|
|
230
290
|
// stays one short section, never held across a build re-check.
|
|
231
|
-
return
|
|
291
|
+
return withPrimaryWorktreeLockAsync(root, async () => {
|
|
232
292
|
const saved = await configBytesToPreserve(root, ref);
|
|
233
293
|
try {
|
|
234
294
|
await git(root, "merge", "--ff-only", ref);
|
|
@@ -236,7 +296,13 @@ export async function ffMainToOrWhy(root, ref, mainBranch) {
|
|
|
236
296
|
catch (err) {
|
|
237
297
|
return ffFailureReason(root, ref, mainBranch, err);
|
|
238
298
|
}
|
|
239
|
-
|
|
299
|
+
try {
|
|
300
|
+
restoreConfigBytes(root, saved);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `main fast-forwarded, but writing the live ${CONFIG_BASENAME} back failed: ` +
|
|
304
|
+
`${errorMessage(err)}; restore it by hand (it is no longer tracked on main)`);
|
|
305
|
+
}
|
|
240
306
|
return null;
|
|
241
307
|
});
|
|
242
308
|
}
|
|
@@ -249,14 +315,87 @@ export async function ffMainToOrWhy(root, ref, mainBranch) {
|
|
|
249
315
|
}
|
|
250
316
|
}
|
|
251
317
|
/** Why a fast-forward to `ref` failed: main having moved off `ref`'s base (a commit that did not
|
|
252
|
-
* take the merge lock) is named with its new head and subject;
|
|
253
|
-
*
|
|
318
|
+
* take the merge lock) is named with its new head and subject; a dirty primary checkout is
|
|
319
|
+
* reported with the files it would overwrite; any other cause reports git's first error line. */
|
|
254
320
|
async function ffFailureReason(root, ref, mainBranch, err) {
|
|
255
321
|
if ((await gitTry(root, "merge-base", "--is-ancestor", mainBranch, ref)) === null) {
|
|
256
322
|
const now = await gitTry(root, "log", "-1", "--format=%h %s", mainBranch);
|
|
257
323
|
return `main moved outside the merge lock (now ${now?.trim() ?? mainBranch})`;
|
|
258
324
|
}
|
|
259
|
-
|
|
325
|
+
const blockers = overwriteBlockers(errorMessage(err));
|
|
326
|
+
if (blockers.uncommitted.length > 0 || blockers.untracked.length > 0) {
|
|
327
|
+
return `fast-forward failed: ${overwriteBlockedReason(blockers)}`;
|
|
328
|
+
}
|
|
329
|
+
return `fast-forward failed: ${errorFirstLine(err)}`;
|
|
330
|
+
}
|
|
331
|
+
/** The files git lists under its two "would be overwritten by merge:" headers. */
|
|
332
|
+
export function overwriteBlockers(gitError) {
|
|
333
|
+
const out = { uncommitted: [], untracked: [] };
|
|
334
|
+
let group = null;
|
|
335
|
+
for (const line of gitError.split("\n")) {
|
|
336
|
+
if (line.includes("Your local changes to the following files would be overwritten by merge:")) {
|
|
337
|
+
group = out.uncommitted;
|
|
338
|
+
}
|
|
339
|
+
else if (line.includes("untracked working tree files would be overwritten by merge:")) {
|
|
340
|
+
group = out.untracked;
|
|
341
|
+
}
|
|
342
|
+
else if (group && line.startsWith("\t")) {
|
|
343
|
+
group.push(line.trim());
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
group = null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
const MAX_BLOCKERS_LISTED = 5;
|
|
352
|
+
/** Why a dirty primary checkout blocks a merge: up to 5 files per group. Callers prefix it with
|
|
353
|
+
* their own stage (`fast-forward failed: `, or none from the pre-flight). */
|
|
354
|
+
export function overwriteBlockedReason(blockers) {
|
|
355
|
+
const list = (names) => joinWithMore(names, MAX_BLOCKERS_LISTED);
|
|
356
|
+
const groups = [];
|
|
357
|
+
if (blockers.uncommitted.length > 0)
|
|
358
|
+
groups.push(`uncommitted: ${list(blockers.uncommitted)}`);
|
|
359
|
+
if (blockers.untracked.length > 0)
|
|
360
|
+
groups.push(`untracked: ${list(blockers.untracked)}`);
|
|
361
|
+
const where = `the primary checkout (${groups.join("; ")})`;
|
|
362
|
+
return `the merge would overwrite files in ${where} — commit, stash or remove them there`;
|
|
363
|
+
}
|
|
364
|
+
/** The primary checkout's dirty paths a merge to `tip` would overwrite, read before any check
|
|
365
|
+
* runs: null when the checkout is not on main (the ref push touches no working tree), when
|
|
366
|
+
* nothing the merge changes is dirty, or when a git read fails — the fast-forward still decides. */
|
|
367
|
+
export async function primaryCheckoutBlockers(root, mainBranch, tip) {
|
|
368
|
+
if ((await currentBranch(root)) !== mainBranch)
|
|
369
|
+
return null;
|
|
370
|
+
const changed = await treeDiffFiles(root, mainBranch, tip);
|
|
371
|
+
const status = await gitTry(root, "--no-optional-locks", "status", "--porcelain=v1", "-z", "--untracked-files=all");
|
|
372
|
+
if (changed === null || status === null)
|
|
373
|
+
return null;
|
|
374
|
+
const untrackedSet = new Set();
|
|
375
|
+
const dirtySet = new Set();
|
|
376
|
+
const records = status.split("\0");
|
|
377
|
+
for (let i = 0; i < records.length; i++) {
|
|
378
|
+
const rec = records[i];
|
|
379
|
+
if (rec.length < 4)
|
|
380
|
+
continue;
|
|
381
|
+
const xy = rec.slice(0, 2);
|
|
382
|
+
const file = rec.slice(3);
|
|
383
|
+
if (xy === "??") {
|
|
384
|
+
untrackedSet.add(file);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
dirtySet.add(file);
|
|
388
|
+
if (xy.startsWith("R") || xy.startsWith("C")) {
|
|
389
|
+
const source = records[++i];
|
|
390
|
+
if (source)
|
|
391
|
+
dirtySet.add(source);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const out = {
|
|
395
|
+
uncommitted: changed.filter((f) => dirtySet.has(f)),
|
|
396
|
+
untracked: changed.filter((f) => untrackedSet.has(f)),
|
|
397
|
+
};
|
|
398
|
+
return out.uncommitted.length > 0 || out.untracked.length > 0 ? out : null;
|
|
260
399
|
}
|
|
261
400
|
/** Boolean form of ffMainToOrWhy for callers that need no reason. */
|
|
262
401
|
export async function ffMainTo(root, ref, mainBranch) {
|
|
@@ -3,17 +3,18 @@ import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
|
3
3
|
import { errorMessage } from "../text/text.js";
|
|
4
4
|
import { deleteRef } from "../git/git.js";
|
|
5
5
|
import { removeMergeChange, writeMergeOutcome } from "./merge-slot.js";
|
|
6
|
+
import { requeueKeptChange } from "./merge-requeue.js";
|
|
6
7
|
import { mergeRefName, rejectedRefName } from "../paths.js";
|
|
7
8
|
/** A fresh pipeline: nothing in flight anywhere — no verification running, no approved change
|
|
8
9
|
* waiting, no merge holding main. The orchestrator builds one per team start (orchestrator.ts). */
|
|
9
10
|
export function newMergePipeline() {
|
|
10
|
-
return { verifying: new Map(), approved: new Map(), merge: null };
|
|
11
|
+
return { verifying: new Map(), approved: new Map(), merge: null, postReview: null };
|
|
11
12
|
}
|
|
12
13
|
/** Every merge task in flight — each verification holding its permit, and the merge — for the
|
|
13
14
|
* shutdown and restart waits. A parked verification is not one of them: it has started nothing, and
|
|
14
15
|
* a shutdown (its controller) or a closed start gate (at its grant) settles it without a run. */
|
|
15
16
|
export function mergeTasks(p) {
|
|
16
|
-
return [...[...p.verifying.values()].filter((v) => !v.parked), ...(p.merge ? [p.merge] : [])];
|
|
17
|
+
return [...[...p.verifying.values()].filter((v) => !v.parked), ...(p.merge ? [p.merge] : []), ...(p.postReview && !p.postReview.parked ? [p.postReview] : [])];
|
|
17
18
|
}
|
|
18
19
|
/** Everything `tumwater abort --role` can reach: every task in flight, plus each approved change
|
|
19
20
|
* waiting with no task of its own. A parked verification is out of reach, as a plainly queued entry
|
|
@@ -85,7 +86,7 @@ export async function discardPinnedRefs(root, roles) {
|
|
|
85
86
|
* write-back, so no settled entry can keep a stale marker or lose its outcome write. The pinned-ref
|
|
86
87
|
* discard stays with the callers: only a user abort throws work away, and the merge discards its
|
|
87
88
|
* whole stack at once rather than change by change. */
|
|
88
|
-
export async function settleMergeOutcome(root, entry, state, result, durationMs, usage, file) {
|
|
89
|
+
export async function settleMergeOutcome(root, entry, state, result, durationMs, usage, file, mainBranch) {
|
|
89
90
|
writeMergeOutcome(root, entry, state, result, durationMs, usage, file);
|
|
90
91
|
// A revision that merges ends the rejected change's life: drop the rejected ref (plans/revise-
|
|
91
92
|
// rejected.md). A rejection repoints or deletes it in merge-core.ts, so this is the only
|
|
@@ -96,4 +97,15 @@ export async function settleMergeOutcome(root, entry, state, result, durationMs,
|
|
|
96
97
|
await deleteRef(root, rejectedRefName(entry.role));
|
|
97
98
|
}
|
|
98
99
|
removeMergeChange(root, entry.role);
|
|
100
|
+
// A kept change goes straight back on the queue (the callers pass the main branch; a
|
|
101
|
+
// direct caller without one leaves the retry to the author's leftover recovery). The write-back
|
|
102
|
+
// above already happened, so a failure here must not lose the settled outcome.
|
|
103
|
+
if (mainBranch !== undefined) {
|
|
104
|
+
try {
|
|
105
|
+
await requeueKeptChange(root, mainBranch, entry, state, result);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
warnEventBestEffort(root, entry.role, `could not re-queue the kept change: ${errorMessage(err)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
99
111
|
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { withProviderLease } from "../concurrency/provider-lease.js";
|
|
2
|
+
import { reviewRunConfig, seamConcurrencyKey } from "../config/config-views.js";
|
|
3
|
+
import { subscribeEvents, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
4
|
+
import { deleteRef, headOf, subjectOfCommit } from "../git/git.js";
|
|
5
|
+
import { ensureDetachedWorktree } from "../git/worktree.js";
|
|
6
|
+
import { useWorktree } from "../git/worktree-use.js";
|
|
7
|
+
import { postReviewWorktreePath, reviewFixRef } from "../paths.js";
|
|
8
|
+
import { POST_REVIEW_FAILURE_LIMIT, addPostReview, readPostReviews, reviewMergedCommit, settlePostReview, } from "../review/post-merge-review.js";
|
|
9
|
+
import { shortSha } from "../text/format.js";
|
|
10
|
+
import { formatDate } from "../text/datetime.js";
|
|
11
|
+
import { collapseWhitespace, errorMessage } from "../text/text.js";
|
|
12
|
+
import { fileBugsOnMain } from "../backlog/backlog-archive.js";
|
|
13
|
+
import { abortOnShutdown, acquireUnlessAborted, guardMergeTask } from "./merge-pipeline.js";
|
|
14
|
+
import { resolveWaitStage } from "./merge-slot.js";
|
|
15
|
+
import { mergeToMain } from "./merge-to-main.js";
|
|
16
|
+
import { MERGE_TIER, resolveAuthor, VERIFY_TIER } from "./merge-verify.js";
|
|
17
|
+
/** One poll of the post-merge reviews (review.afterMerge): a rejected-and-not-fixed commit is
|
|
18
|
+
* answered by the reviewer's own landed fix (Forward, never back 1/6) — never by a revert — and
|
|
19
|
+
* the next unreviewed commit starts its review when a permit is free right now. One review at a
|
|
20
|
+
* time; starts a task and returns. */
|
|
21
|
+
export function drainPostReviews(ctx, p) {
|
|
22
|
+
const pending = readPostReviews(ctx.root);
|
|
23
|
+
if (pending.length === 0)
|
|
24
|
+
return;
|
|
25
|
+
if (p.postReview !== null || ctx.startHeld())
|
|
26
|
+
return;
|
|
27
|
+
// A `fixed` entry is a settled record naming both shas (the reviewed commit and its landed fix)
|
|
28
|
+
// — it is not a pending unreviewed commit, so it is never re-reviewed (Forward never back 1a/6).
|
|
29
|
+
const next = pending.find((e) => e.rejected === undefined && e.fixed === undefined);
|
|
30
|
+
if (next)
|
|
31
|
+
p.postReview = startPostReview(ctx, p, next);
|
|
32
|
+
}
|
|
33
|
+
function startPostReview(ctx, p, entry) {
|
|
34
|
+
const { root, semaphore } = ctx;
|
|
35
|
+
const task = { promise: Promise.resolve(), controller: new AbortController(), roles: [], userAborted: false, parked: true };
|
|
36
|
+
const detach = abortOnShutdown(ctx.signal, task.controller);
|
|
37
|
+
const author = resolveAuthor(ctx, entry.role);
|
|
38
|
+
const key = seamConcurrencyKey(reviewRunConfig(ctx.liveConfig), ctx.piDefault);
|
|
39
|
+
const run = (async () => {
|
|
40
|
+
try {
|
|
41
|
+
if (!semaphore.canLeaseAtOnce(VERIFY_TIER, key))
|
|
42
|
+
return; // permits are busy; try again next poll
|
|
43
|
+
const lease = await acquireUnlessAborted(semaphore, VERIFY_TIER, task.controller.signal, key);
|
|
44
|
+
task.parked = false;
|
|
45
|
+
if (lease === null)
|
|
46
|
+
return;
|
|
47
|
+
let outcome;
|
|
48
|
+
try {
|
|
49
|
+
outcome = await reviewMergedCommit(root, ctx.liveConfig, entry, (opts) => author.runGatePi(opts), task.controller.signal);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
lease.release();
|
|
53
|
+
}
|
|
54
|
+
if (task.controller.signal.aborted)
|
|
55
|
+
return; // a shutdown: the entry stays for the next generation
|
|
56
|
+
if (outcome.kind === "approve")
|
|
57
|
+
settlePostReview(root, entry.commit, null);
|
|
58
|
+
else if (outcome.kind === "reject")
|
|
59
|
+
settlePostReview(root, entry.commit, { ...entry, rejected: outcome.reasons });
|
|
60
|
+
else if (outcome.kind === "fixed")
|
|
61
|
+
await mergeReviewerFix(ctx, entry, outcome.fix);
|
|
62
|
+
else if (outcome.kind === "exhausted") {
|
|
63
|
+
// A self-fix chain at the backstop: reviewMergedCommit has already recorded the surviving
|
|
64
|
+
// findings; the entry is cleared so nothing re-reviews it (Forward never back 2/6).
|
|
65
|
+
settlePostReview(root, entry.commit, null);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const failures = (entry.failures ?? 0) + 1;
|
|
69
|
+
warnEventBestEffort(root, entry.role, `the post-merge review of ${shortSha(entry.commit)} failed (${outcome.detail})`);
|
|
70
|
+
if (failures >= POST_REVIEW_FAILURE_LIMIT) {
|
|
71
|
+
warnEventBestEffort(root, entry.role, `dropping the post-merge review of ${shortSha(entry.commit)} after ${failures} failed runs; it stays on main unreviewed`);
|
|
72
|
+
await fileDroppedReview(ctx, entry, outcome.detail);
|
|
73
|
+
settlePostReview(root, entry.commit, null);
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
settlePostReview(root, entry.commit, { ...entry, failures, lastFailure: outcome.detail });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `the post-merge review of ${shortSha(entry.commit)} could not run: ${errorMessage(err)}`);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
detach();
|
|
84
|
+
p.postReview = null;
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
task.promise = guardMergeTask(root, `post-merge review of ${shortSha(entry.commit)}`, run);
|
|
88
|
+
return task;
|
|
89
|
+
}
|
|
90
|
+
/** File a post-merge review that is being dropped at POST_REVIEW_FAILURE_LIMIT (its commit stays
|
|
91
|
+
* on main, so it becomes ordinary backlog work a role can pick up) as one BUGS.md `## Open`
|
|
92
|
+
* entry in a best-effort `tumwater(operator):` commit. A skipped filing (off-base checkout or a
|
|
93
|
+
* BUGS.md with uncommitted changes) or a throw logs one warning and returns, so a filing problem
|
|
94
|
+
* never prevents the entry from being dropped and never throws out of the drain. */
|
|
95
|
+
async function fileDroppedReview(ctx, entry, failure) {
|
|
96
|
+
const root = ctx.root;
|
|
97
|
+
try {
|
|
98
|
+
const outcome = await fileBugsOnMain(root, [
|
|
99
|
+
{
|
|
100
|
+
title: `post-merge review of ${shortSha(entry.commit)} never reached a verdict (found by the review role ${formatDate(new Date())})`,
|
|
101
|
+
// The failure text is untrusted and can carry newlines (a backend/stderr trace) or a
|
|
102
|
+
// `## `/code-fence line that would make appendBugEntryLocked call fail() and exit the
|
|
103
|
+
// whole drain; folding it to one line keeps the body structurally sound, so a hostile
|
|
104
|
+
// failure skips the filing via the ordinary throw path instead of killing the process.
|
|
105
|
+
body: collapseWhitespace(`The post-merge review of commit \`${entry.commit}\` (${entry.summary}) never reached a verdict: ${failure}. It stays on main as ordinary unreviewed backlog work.`),
|
|
106
|
+
},
|
|
107
|
+
], () => "File an unreviewed post-merge commit in BUGS.md");
|
|
108
|
+
if (outcome.status === "skipped") {
|
|
109
|
+
warnEventBestEffort(root, entry.role, `could not file the dropped post-merge review of ${shortSha(entry.commit)} in BUGS.md (${outcome.reason})`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
warnEventBestEffort(root, entry.role, `could not file the dropped post-merge review of ${shortSha(entry.commit)} in BUGS.md: ${errorMessage(err)}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Land a reviewer's fix commit on main through the ordinary change path — mergeToMain, whose
|
|
117
|
+
* gate runs over the fix tree and whose mergeLockDir lock serializes it with every other write
|
|
118
|
+
* to main — then settle the entry `fixed` naming both shas. A landing that fails (a conflict, a
|
|
119
|
+
* red gate, a refused fast-forward) leaves main at the reviewed commit and keeps the entry with
|
|
120
|
+
* its failure recorded for the next poll's retry, exactly as a failed run is kept today — up to
|
|
121
|
+
* the same POST_REVIEW_FAILURE_LIMIT the verdict-less-review path obeys, so the at-risk doctor
|
|
122
|
+
* row/alert's "one more failed run drops it" stays true and the reviewer is not re-run forever;
|
|
123
|
+
* the review-fix ref survives so leftover recovery can still reach the commit.
|
|
124
|
+
* Throws when git could not start; a failed merge itself returns a non-terminal RunResult and
|
|
125
|
+
* is never a throw. */
|
|
126
|
+
async function mergeReviewerFix(ctx, entry, fix) {
|
|
127
|
+
const author = resolveAuthor(ctx, entry.role);
|
|
128
|
+
// The fix merge's conflict resolver rides the same provider lease the merge slot's resolver
|
|
129
|
+
// does (merge-drain.ts startMerge): the MERGE_TIER lease is keyed on the resolver config's
|
|
130
|
+
// provider and rides in the provider-lease store, so a conflict resolution counts against the
|
|
131
|
+
// strong tier's cap like any other strong-tier run instead of acquiring a provider unchecked.
|
|
132
|
+
const runPi = async (wt, prompt, sessionName, config) => {
|
|
133
|
+
const key = config ? seamConcurrencyKey(config, ctx.piDefault) : undefined;
|
|
134
|
+
const hooks = resolveWaitStage(ctx.root, entry.role);
|
|
135
|
+
if (!ctx.semaphore.canLeaseAtOnce(MERGE_TIER, key))
|
|
136
|
+
hooks.waiting();
|
|
137
|
+
const lease = await ctx.semaphore.acquireLease(MERGE_TIER, key);
|
|
138
|
+
hooks.granted();
|
|
139
|
+
try {
|
|
140
|
+
return await withProviderLease(lease, ctx.semaphore, () => author.runMergePi(wt, prompt, sessionName, config), { piDefault: ctx.piDefault });
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
lease.release();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
// Land through the merge's own write path — mergeToMain fast-forwards main under the shared
|
|
147
|
+
// mergeLockDir mutex, so a fix landing and the ordinary merge slot serialize on main's one
|
|
148
|
+
// writer either way — but from the DEDICATED post-review worktree (which already holds the fix
|
|
149
|
+
// commit), NOT the shared `_merge` checkout that belongs to the merge slot. That removes the
|
|
150
|
+
// race the merge slot's mergeApprovedChangeIn has: each resets and merges its own worktree, so
|
|
151
|
+
// a fix landing can never reset or merge the shared `_merge` worktree concurrently with the
|
|
152
|
+
// merge slot (both could start in one poll), while main's writes stay serialized by the real
|
|
153
|
+
// mergeLockDir mutex. Hold the worktree from before ensureDetachedWorktree through the merge
|
|
154
|
+
// (plans/disk-floor.md part 2/4); this task is the only user of the post-review worktree while
|
|
155
|
+
// p.postReview is held, so the reset-and-merge cannot collide with a fresh review.
|
|
156
|
+
let result;
|
|
157
|
+
// The commit that actually merges, from our own `merged` event (not the pre-rebase `fix`):
|
|
158
|
+
// mergeToMain rebases `fix` onto main's tip, so when main moved under the review the landed
|
|
159
|
+
// commit is a different sha — and `fixed` must name a commit that is provably on main.
|
|
160
|
+
// Subscribing mirrors the merge slot's startMerge, so a concurrent merge of another role
|
|
161
|
+
// landing after ours cannot overwrite the sha we record.
|
|
162
|
+
const mergedMine = new Map();
|
|
163
|
+
const unsubscribe = subscribeEvents((e) => {
|
|
164
|
+
if (e.type === "merged" && typeof e.commit === "string")
|
|
165
|
+
mergedMine.set(e.source, e.commit);
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
result = await useWorktree(ctx.root, postReviewWorktreePath(ctx.root), async () => {
|
|
169
|
+
const wt = await ensureDetachedWorktree(ctx.root, postReviewWorktreePath(ctx.root), fix);
|
|
170
|
+
return mergeToMain({
|
|
171
|
+
root: ctx.root,
|
|
172
|
+
role: entry.role,
|
|
173
|
+
mainBranch: ctx.mainBranch,
|
|
174
|
+
config: ctx.liveConfig,
|
|
175
|
+
exemptPaths: ctx.liveConfig.review.exemptPaths,
|
|
176
|
+
runNumber: author.state.runs,
|
|
177
|
+
runPi,
|
|
178
|
+
// A conflict resolution of the reviewer's fix is resolver-authored bytes the reviewer
|
|
179
|
+
// never judged; unlike the merge slot's mergeApprovedChangeIn there is no review gate
|
|
180
|
+
// here to re-run, so a diverging resolution must not merge on the in-lock build check
|
|
181
|
+
// alone (BUGS.md 2026-10-01). Withhold it: the landing reports the conflict, the fix
|
|
182
|
+
// ref and the entry stay for the next poll's retry, and main never carries resolver
|
|
183
|
+
// bytes it was never judged against.
|
|
184
|
+
recheckResolved: async () => ({ verdict: "retry" }),
|
|
185
|
+
}, wt, (await subjectOfCommit(wt, fix)) ?? `Fix ${shortSha(entry.commit)}`);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
unsubscribe();
|
|
190
|
+
}
|
|
191
|
+
if (result === "changed") {
|
|
192
|
+
const merged = mergedMine.get(entry.role) ?? (await headOf(ctx.root, ctx.mainBranch));
|
|
193
|
+
await deleteRef(ctx.root, reviewFixRef(entry.commit)); // the fix is on main; the ref has done its job
|
|
194
|
+
settlePostReview(ctx.root, entry.commit, { ...entry, fixed: merged }); // the entry names the landed fix
|
|
195
|
+
// A landed fix is a new post-review entry (Forward never back 2/6): re-queue it so the next
|
|
196
|
+
// iteration reviews it like any unreviewed commit, counting its round toward the same
|
|
197
|
+
// REVISION_BACKSTOP — the round lives on the fix commit's `Revision:` trailer.
|
|
198
|
+
addPostReview(ctx.root, { commit: merged, role: entry.role, summary: entry.summary });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// A failed merge: main is unmoved, the fix ref survives. Count it against the drop limit -
|
|
202
|
+
// keeping the entry (with the failure recorded) so the next poll retries the review, but
|
|
203
|
+
// dropping it once the limit is reached, so the reviewer is not re-run unboundedly.
|
|
204
|
+
const failures = (entry.failures ?? 0) + 1;
|
|
205
|
+
if (failures >= POST_REVIEW_FAILURE_LIMIT) {
|
|
206
|
+
warnEventBestEffort(ctx.root, entry.role, `dropping the post-merge review of ${shortSha(entry.commit)} after ${failures} failed fix merges; the fix stays unreviewed`);
|
|
207
|
+
await deleteRef(ctx.root, reviewFixRef(entry.commit)); // the merge gave up on the fix; the ref must not pin it forever
|
|
208
|
+
await fileDroppedReview(ctx, entry, `fix did not merge: ${result}`);
|
|
209
|
+
settlePostReview(ctx.root, entry.commit, null);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
warnEventBestEffort(ctx.root, entry.role, `the post-merge fix of ${shortSha(entry.commit)} could not merge (${result}); it stays queued for the next poll`);
|
|
213
|
+
settlePostReview(ctx.root, entry.commit, { ...entry, failures, lastFailure: `fix did not merge: ${result}` });
|
|
214
|
+
}
|