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,16 +1,16 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** The renderer behind `tumwater queue` — the operator-facing text for the merge queue view the
|
|
3
2
|
* reader gathers (merge-queue.ts's readMergeQueue, beside it): one line per queued merge
|
|
4
3
|
* in execution order, plus the count of queue files the reader skipped. Pure function of the
|
|
5
4
|
* collected view and a `now` instant — no I/O, no clock reads — so the age it prints can be
|
|
6
5
|
* pinned by a test. */
|
|
7
|
-
import { humanSeconds, secondsSince } from "../text/datetime.js";
|
|
6
|
+
import { humanAgo, humanSeconds, secondsSince, secondsUntil } from "../text/datetime.js";
|
|
8
7
|
import { roleLabel } from "../roles/role-labels.js";
|
|
9
8
|
import { shortSha } from "../text/format.js";
|
|
9
|
+
import { filterKeepPositions } from "../collections.js";
|
|
10
10
|
import { plural } from "../text/phrases.js";
|
|
11
11
|
import { mergeChanges, MERGE_STAGE_LABELS } from "./merge-slot.js";
|
|
12
|
-
/** The bracketed-label text for one queue entry, in the wording the
|
|
13
|
-
*
|
|
12
|
+
/** The bracketed-label text for one queue entry, in the wording the role rows use (status-model's
|
|
13
|
+
* rolePhase): `merge <elapsed> · <stage>` while a verify or the merge holds the change,
|
|
14
14
|
* `approved, awaiting merge` once approved, and "" when the marker is absent, names no record for
|
|
15
15
|
* `sha`, or the record is `done`. Reads the marker through mergeChanges, so an older-generation
|
|
16
16
|
* single-change marker counts as one `merge` record. */
|
|
@@ -44,28 +44,65 @@ export function revisionNote(entry) {
|
|
|
44
44
|
note += " · " + plural(reasons.length, "prior objection");
|
|
45
45
|
return note;
|
|
46
46
|
}
|
|
47
|
+
/** One re-queued entry's what-brought-it-back note:
|
|
48
|
+
* `re-queued after <result>`, plus ` · ×<n>` while it has bounced more than once, plus
|
|
49
|
+
* ` · eligible in <duration>` while a `review_error` 60 s deferral still holds (its `notBefore`
|
|
50
|
+
* in the future). "" for a fresh entry with no `retry` field, so a first submission's queue
|
|
51
|
+
* line is byte-identical. */
|
|
52
|
+
export function retryNote(entry, now) {
|
|
53
|
+
if (entry.retry === undefined)
|
|
54
|
+
return "";
|
|
55
|
+
let note = `re-queued after ${entry.retry}`;
|
|
56
|
+
if ((entry.retryCount ?? 0) > 1)
|
|
57
|
+
note += ` · ×${entry.retryCount}`;
|
|
58
|
+
if (entry.notBefore !== undefined && entry.notBefore > now) {
|
|
59
|
+
note += ` · eligible in ${humanSeconds(secondsUntil(entry.notBefore, now))}`;
|
|
60
|
+
}
|
|
61
|
+
return note;
|
|
62
|
+
}
|
|
63
|
+
/** Narrows a whole-queue view to the entries whose role `keep` accepts, and returns the
|
|
64
|
+
* `scope` renderMergeQueue takes: the kept roles, each kept entry's 1-based place in the whole
|
|
65
|
+
* queue, and the whole queue's entry count. `unreadable` stays whole because an unreadable file
|
|
66
|
+
* has no role to attribute it to. */
|
|
67
|
+
export function scopeMergeQueue(whole, roles, keep) {
|
|
68
|
+
const { kept, positions } = filterKeepPositions(whole.entries, (e) => keep(e.role));
|
|
69
|
+
return { view: { ...whole, entries: kept }, scope: { roles, positions, total: whole.entries.length } };
|
|
70
|
+
}
|
|
47
71
|
/** Render the queue as the text `tumwater queue` prints: a `merge queue (<depth>):` header then
|
|
48
|
-
* one 1-based line per readable entry, oldest first, carrying the
|
|
72
|
+
* one 1-based line per readable entry, oldest first, carrying the role, run number, short
|
|
49
73
|
* pinned sha, age, and summary. An empty queue (no entries and nothing skipped) prints
|
|
50
74
|
* `merge queue empty`; a non-zero skip count appends one `(+N unreadable queue file(s) skipped)`
|
|
51
75
|
* line so a torn or foreign file the reader dropped is visible rather than silent. An entry the
|
|
52
76
|
* optional in-flight marker has a live record for carries ` · [<label>]` after its age
|
|
53
|
-
* (queueEntryMergeLabel).
|
|
54
|
-
|
|
77
|
+
* (queueEntryMergeLabel). With the optional `scope` (`queue --role`: `view` holds only the
|
|
78
|
+
* listed roles' entries, `positions` their 1-based places in the whole queue, `total` the whole
|
|
79
|
+
* queue's entry count) each line keeps its whole-queue number, the header reads
|
|
80
|
+
* `merge queue (<kept> of <whole depth>, --role <ids>):`, and a scope that keeps nothing of a
|
|
81
|
+
* non-empty queue prints `no queued merges for <ids> (merge queue holds <depth>)`. Without
|
|
82
|
+
* `scope` the output is the whole queue's. */
|
|
83
|
+
export function renderMergeQueue(view, now, inFlight, scope) {
|
|
55
84
|
const depth = view.entries.length + view.unreadable;
|
|
85
|
+
const wholeDepth = scope ? scope.total + view.unreadable : depth;
|
|
86
|
+
const ids = scope ? scope.roles.join(",") : "";
|
|
56
87
|
const lines = [];
|
|
57
|
-
if (
|
|
88
|
+
if (wholeDepth === 0) {
|
|
58
89
|
lines.push("merge queue empty");
|
|
59
90
|
}
|
|
91
|
+
else if (scope && view.entries.length === 0) {
|
|
92
|
+
lines.push(`no queued merges for ${ids} (merge queue holds ${wholeDepth})`);
|
|
93
|
+
}
|
|
60
94
|
else {
|
|
61
|
-
lines.push(`merge queue (${depth}):`);
|
|
95
|
+
lines.push(scope ? `merge queue (${view.entries.length} of ${wholeDepth}, --role ${ids}):` : `merge queue (${depth}):`);
|
|
62
96
|
view.entries.forEach((e, i) => {
|
|
63
|
-
const age =
|
|
97
|
+
const age = humanAgo(e.enqueuedAt, now);
|
|
64
98
|
const label = queueEntryMergeLabel(e.sha, inFlight, now);
|
|
65
99
|
const tag = label ? ` · [${label}]` : "";
|
|
66
100
|
const note = revisionNote(e);
|
|
67
101
|
const noteTag = note ? ` · ${note}` : "";
|
|
68
|
-
|
|
102
|
+
const retry = retryNote(e, now);
|
|
103
|
+
const retryTag = retry ? ` · ${retry}` : "";
|
|
104
|
+
const frictionTag = e.highFriction === true ? " · high friction" : "";
|
|
105
|
+
lines.push(` ${scope ? scope.positions[i] : i + 1}. ${roleLabel(e.role)} run #${e.run} · ${shortSha(e.sha)} · ${age}${tag}${frictionTag}${retryTag}${noteTag} · ${e.summary}`);
|
|
69
106
|
});
|
|
70
107
|
}
|
|
71
108
|
if (view.unreadable > 0) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errCode } from "../errno.js";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { listQueueFiles, queueFileName, removeQueueFile } from "../files/file-queue.js";
|
|
@@ -10,7 +11,7 @@ let seq = 0;
|
|
|
10
11
|
/** Append one merge to the queue as a single timestamped file (creating the queue dir if
|
|
11
12
|
* needed). The filename orders entries across processes by wall-clock time; the per-process
|
|
12
13
|
* counter and pid break ties within one process. Written atomically (tmp + rename): the
|
|
13
|
-
* enqueue runs while the process can be hard-killed mid-write (the
|
|
14
|
+
* enqueue runs while the process can be hard-killed mid-write (the run deadline's
|
|
14
15
|
* process-group kill, a shutdown), and a torn entry the crash left behind is not always
|
|
15
16
|
* recoverable — the drain repairs only an unreadable HEAD (staleHeadFile), so a torn file
|
|
16
17
|
* that sorts after a healthy entry would sit in the queue dir unread forever. */
|
|
@@ -20,7 +21,7 @@ export function enqueueMerge(root, entry) {
|
|
|
20
21
|
writeJsonAtomic(path.join(dir, name), entry);
|
|
21
22
|
}
|
|
22
23
|
/** The revision fields of a queue entry, serialized only when present — the shape merge
|
|
23
|
-
* surfaces (drain, verify) and the
|
|
24
|
+
* surfaces (drain, verify) and the run-stage enqueue re-emit when they build their own
|
|
24
25
|
* entry/request objects, so the `revisionRound`/`priorReview` names live in one place and
|
|
25
26
|
* cannot drift. */
|
|
26
27
|
export function entryRevisionFields(e) {
|
|
@@ -45,7 +46,7 @@ function queueFiles(root) {
|
|
|
45
46
|
export function legacyMigrateLandQueueDir(root) {
|
|
46
47
|
const from = legacyLandQueueDir(root);
|
|
47
48
|
const to = mergeQueueDir(root);
|
|
48
|
-
const notes = [];
|
|
49
|
+
const notes = { moved: [], kept: [], failed: [] };
|
|
49
50
|
const files = listQueueFiles(from, ".json");
|
|
50
51
|
if (files.length > 0)
|
|
51
52
|
fs.mkdirSync(to, { recursive: true });
|
|
@@ -53,15 +54,15 @@ export function legacyMigrateLandQueueDir(root) {
|
|
|
53
54
|
const name = path.basename(file);
|
|
54
55
|
const dest = path.join(to, name);
|
|
55
56
|
if (fs.existsSync(dest)) {
|
|
56
|
-
notes.push(`${name} already in merge-queue, left in place`);
|
|
57
|
+
notes.kept.push(`${name} already in merge-queue, left in place`);
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
59
60
|
try {
|
|
60
61
|
fs.renameSync(file, dest);
|
|
61
|
-
notes.push(`moved ${name}`);
|
|
62
|
+
notes.moved.push(`moved ${name}`);
|
|
62
63
|
}
|
|
63
64
|
catch (err) {
|
|
64
|
-
if (err
|
|
65
|
+
if (errCode(err) !== "ENOENT")
|
|
65
66
|
throw err;
|
|
66
67
|
}
|
|
67
68
|
}
|
|
@@ -86,12 +87,12 @@ const entryCache = new Map();
|
|
|
86
87
|
* queue is one JSON object per file, so a file that parses to anything else — a scalar, an
|
|
87
88
|
* array, an object missing a required field — is foreign or torn and reads as no entry.
|
|
88
89
|
* Checking only `role` and `sha` left the `as MergeEntry` cast unsound: a foreign file that
|
|
89
|
-
* happened to carry those two fields was handed downstream with `
|
|
90
|
+
* happened to carry those two fields was handed downstream with `run` and `summary`
|
|
90
91
|
* undefined, which surface as bogus session names (`tumwater-<role>-undefined-review`) and
|
|
91
92
|
* `undefined` in the reviewer's prompt. Optional fields are checked only when present.
|
|
92
93
|
*
|
|
93
94
|
* The three numeric fields go through nonNegativeNumber, not a bare `typeof === "number"`:
|
|
94
|
-
* JSON's `1e999` parses to Infinity, so a hand-edited file could carry a non-finite
|
|
95
|
+
* JSON's `1e999` parses to Infinity, so a hand-edited file could carry a non-finite run or
|
|
95
96
|
* enqueuedAt past a typeof check, and both feed plumbing downstream (session names, queue
|
|
96
97
|
* ordering, the status payload). */
|
|
97
98
|
function isPriorReview(v) {
|
|
@@ -114,7 +115,10 @@ function isMergeEntry(v) {
|
|
|
114
115
|
(e.body === undefined || typeof e.body === "string") &&
|
|
115
116
|
(e.highFriction === undefined || typeof e.highFriction === "boolean") &&
|
|
116
117
|
(e.revisionRound === undefined || nonNegativeNumber(e.revisionRound, null) !== null) &&
|
|
117
|
-
(e.priorReview === undefined || isPriorReview(e.priorReview))
|
|
118
|
+
(e.priorReview === undefined || isPriorReview(e.priorReview)) &&
|
|
119
|
+
(e.notBefore === undefined || nonNegativeNumber(e.notBefore, null) !== null) &&
|
|
120
|
+
(e.retry === undefined || typeof e.retry === "string") &&
|
|
121
|
+
(e.retryCount === undefined || nonNegativeNumber(e.retryCount, null) !== null));
|
|
118
122
|
}
|
|
119
123
|
/** Read one queue file through the stat-keyed cache: null when it vanishes mid-listing (a
|
|
120
124
|
* concurrent drop — the inbox.ts race policy), is unreadable, or fails to parse or to carry
|
|
@@ -127,9 +131,9 @@ function readEntry(file) {
|
|
|
127
131
|
// is still rejected by isMergeEntry.
|
|
128
132
|
let parsed = readJsonFile(file);
|
|
129
133
|
// An entry an old build wrote carries `tick` where the current shape has `run`.
|
|
130
|
-
if (isJsonObject(parsed) && parsed.run === undefined && parsed.tick !== undefined) {
|
|
131
|
-
const { tick, ...rest } = parsed;
|
|
132
|
-
parsed = { ...rest, run: tick };
|
|
134
|
+
if (isJsonObject(parsed) && parsed.run === undefined && parsed.tick !== undefined) { // retired-name-ok: legacy name
|
|
135
|
+
const { tick, ...rest } = parsed; // retired-name-ok: legacy name
|
|
136
|
+
parsed = { ...rest, run: tick }; // retired-name-ok: legacy name
|
|
133
137
|
}
|
|
134
138
|
if (!isMergeEntry(parsed))
|
|
135
139
|
return null;
|
|
@@ -176,6 +180,19 @@ export function readMergeQueue(root) {
|
|
|
176
180
|
}
|
|
177
181
|
return { entries, unreadable };
|
|
178
182
|
}
|
|
183
|
+
/** The full path of every queue file `readMergeQueue` skips — torn, foreign, or vanished
|
|
184
|
+
* mid-listing — in filename order. This names what `readMergeQueue.unreadable` counts, through
|
|
185
|
+
* the same skip policy, so the count and the list cannot drift: nothing else ages a skipped
|
|
186
|
+
* file (the drain repairs only an unreadable head), so each here is a stranded entry the
|
|
187
|
+
* queue reader silently passes over. A missing queue directory reads as an empty list. */
|
|
188
|
+
export function unreadableMergeQueueFiles(root) {
|
|
189
|
+
const out = [];
|
|
190
|
+
for (const file of queueFiles(root)) {
|
|
191
|
+
if (readEntry(file) === null)
|
|
192
|
+
out.push(file);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
179
196
|
/** The head of the queue — its oldest entry — plus the file to drop on completion, or null when
|
|
180
197
|
* the queue is empty or its oldest file is a vanished/torn entry. */
|
|
181
198
|
export function headMerge(root) {
|
|
@@ -190,8 +207,8 @@ export function headMerge(root) {
|
|
|
190
207
|
* reads null while the queue holds files; null when the queue is empty or its head is healthy.
|
|
191
208
|
* The drain drops this file with a warning so the queue can drain: headMerge reads null for
|
|
192
209
|
* it forever and nothing else will remove it, stranding every live entry behind it (each
|
|
193
|
-
* author's interlock, the per-poll queued-roles set, pins its
|
|
194
|
-
* crashed entry's commit, if any, still lives in its merge ref — next-
|
|
210
|
+
* author's interlock, the per-poll queued-roles set, pins its runs along with them). The
|
|
211
|
+
* crashed entry's commit, if any, still lives in its merge ref — next-run leftover recovery
|
|
195
212
|
* re-merges it — so the drop loses nothing recoverable from the file (BUGS.md 2026-09-16). */
|
|
196
213
|
export function staleHeadFile(root) {
|
|
197
214
|
const file = queueFiles(root)[0];
|
|
@@ -199,8 +216,28 @@ export function staleHeadFile(root) {
|
|
|
199
216
|
return null;
|
|
200
217
|
return readEntry(file) === null ? file : null;
|
|
201
218
|
}
|
|
219
|
+
/** The role a torn queue file was being written for, recovered from its raw bytes: MergeEntry
|
|
220
|
+
* serializes `role` first, so even a head a crash cut mid-write keeps a `"role":"<id>"`
|
|
221
|
+
* substring the drain can read back without a JSON parse. Tolerant by design — the regex, not
|
|
222
|
+
* a JSON.parse — because the file's tail is exactly the part the tear may have lost. Null when
|
|
223
|
+
* the file has no readable role: a foreign file, or a tear that cut the write before the role
|
|
224
|
+
* landed. Best-effort: an unreadable file reads as null, never a throw. The drain uses it to
|
|
225
|
+
* name the torn head's owner and re-queue its pinned change before dropping the file. */
|
|
226
|
+
export function tornHeadRole(file) {
|
|
227
|
+
let raw;
|
|
228
|
+
try {
|
|
229
|
+
raw = fs.readFileSync(file, "utf8");
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const m = /"role"\s*:\s*"([^"]+)"/.exec(raw);
|
|
235
|
+
if (!m)
|
|
236
|
+
return null;
|
|
237
|
+
return canonicalRoleId(m[1]);
|
|
238
|
+
}
|
|
202
239
|
/** Remove one queued merge's file after its merge outcome — terminal or not (a
|
|
203
|
-
* non-terminal outcome keeps the merge ref; the retry is the role's next fresh
|
|
240
|
+
* non-terminal outcome keeps the merge ref; the retry is the role's next fresh run, not a
|
|
204
241
|
* queue re-drain). ENOENT is a no-op: a concurrent drop between listing and removal is a
|
|
205
242
|
* normal race (the inbox.ts policy), not an error. */
|
|
206
243
|
export function dropMerge(file) {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Re-queuing a settled merge entry (plans/fast-merge.md "Lever 1"), split out of
|
|
2
|
+
* merge-slot.ts so the kept-change policy reads on its own screen: merge-slot.ts owns the
|
|
3
|
+
* in-flight marker, the per-merge usage accounting, and the write-back that folds an outcome
|
|
4
|
+
* into the authoring role's state and drops its queue entry; this module owns the other half
|
|
5
|
+
* of a settled entry — putting a kept change back on the queue itself, either at once (a
|
|
6
|
+
* retriable result) or once main's head stops being red (a `main_red` pin). */
|
|
7
|
+
import { enqueueMerge, queuedMerges } from "./merge-queue.js";
|
|
8
|
+
import { RETRIABLE_MERGE_RESULTS } from "./merge-core.js";
|
|
9
|
+
import { pinnedRetrySha } from "../runner/leftover.js";
|
|
10
|
+
import { branchHead } from "../git/git.js";
|
|
11
|
+
import { cachedBaselineVerdict } from "../baseline/main-baseline.js";
|
|
12
|
+
import { saveRunnerStateBestEffort } from "../runner/runner-state.js";
|
|
13
|
+
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
14
|
+
import { errorMessage } from "../text/text.js";
|
|
15
|
+
/** How long a re-queued `review_error` change waits before its verification may start. */
|
|
16
|
+
const REVIEW_ERROR_RETRY_MS = 60_000;
|
|
17
|
+
/** The queue entry that re-queues a settled one: its run, summary, body, friction flag and
|
|
18
|
+
* revision fields unchanged (so the summary is not a recovery label), the pin's current `sha`,
|
|
19
|
+
* and `retry` naming the result it retries. */
|
|
20
|
+
function retryEntry(from, sha, result) {
|
|
21
|
+
const { notBefore: _notBefore, retry: _retry, ...rest } = from;
|
|
22
|
+
return {
|
|
23
|
+
...rest,
|
|
24
|
+
sha,
|
|
25
|
+
retry: result,
|
|
26
|
+
retryCount: (from.retryCount ?? 0) + 1,
|
|
27
|
+
enqueuedAt: Date.now(),
|
|
28
|
+
...(result === "review_error" ? { notBefore: Date.now() + REVIEW_ERROR_RETRY_MS } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function enqueueRetry(root, entry) {
|
|
32
|
+
enqueueMerge(root, entry);
|
|
33
|
+
logEventBestEffort(root, {
|
|
34
|
+
source: entry.role,
|
|
35
|
+
type: "merge_queued",
|
|
36
|
+
commit: entry.sha,
|
|
37
|
+
summary: entry.summary,
|
|
38
|
+
retry: entry.retry,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** After a settled merge dropped its entry, put a kept change back on the queue itself, so a
|
|
42
|
+
* retry needs no author run (plans/fast-merge.md "Lever 1"). A `merge_blocked`, `review_error`
|
|
43
|
+
* or `merge_conflict` result re-queues at once when its pin survives and pinnedRetrySha allows
|
|
44
|
+
* it (not stale, not held, under MERGE_CONFLICT_LIMIT); a `main_red` pin is recorded on the
|
|
45
|
+
* state with main's head, for requeueMainRedPin to re-queue once main moves to a head whose
|
|
46
|
+
* verdict is not red. Anything else, or a pin that is gone, changes nothing — leftover recovery
|
|
47
|
+
* stays the backstop. */
|
|
48
|
+
export async function requeueKeptChange(root, mainBranch, entry, state, result) {
|
|
49
|
+
const isMainRed = result === "main_red";
|
|
50
|
+
if (!isMainRed && !RETRIABLE_MERGE_RESULTS.has(result))
|
|
51
|
+
return;
|
|
52
|
+
const sha = await pinnedRetrySha(root, entry.role, mainBranch, state);
|
|
53
|
+
if (!sha)
|
|
54
|
+
return;
|
|
55
|
+
if (isMainRed) {
|
|
56
|
+
const { sha: _sha, notBefore: _nb, retry: _r, ...rest } = entry;
|
|
57
|
+
state.mainRedRetry = { head: (await branchHead(root, mainBranch)) ?? "", entry: rest };
|
|
58
|
+
saveRunnerStateBestEffort(root, state);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (queuedMerges(root).some((e) => e.role === entry.role))
|
|
62
|
+
return;
|
|
63
|
+
enqueueRetry(root, retryEntry(entry, sha, result));
|
|
64
|
+
}
|
|
65
|
+
/** The longest a kept `main_red` pin waits for the main watch's verdict on a moved head before it
|
|
66
|
+
* is re-queued anyway. This bounds the wait when no verdict ever arrives, for example after a
|
|
67
|
+
* watch check that timed out or a restart's cold cache. */
|
|
68
|
+
export const MAIN_RED_VERDICT_WAIT_MS = 15 * 60_000;
|
|
69
|
+
/** Re-queue a `main_red` pin once main's head differs from the one recorded when its attempt
|
|
70
|
+
* settled (the orchestrator poll calls this for every runner), by the head's cached verdict: red
|
|
71
|
+
* leaves the record untouched, so a later poll asks again; no verdict starts (and saves) a wait
|
|
72
|
+
* at `now`, and the pin re-queues once MAIN_RED_VERDICT_WAIT_MS has passed; green re-queues at
|
|
73
|
+
* once. Re-queuing clears the record; no entry is queued when the pin is gone or held back by
|
|
74
|
+
* pinnedRetrySha, or the role already has one queued. It never throws: the poll
|
|
75
|
+
* loop calls it with no catch. */
|
|
76
|
+
export async function requeueMainRedPin(root, mainBranch, state, mainHead, now = Date.now()) {
|
|
77
|
+
const kept = state.mainRedRetry;
|
|
78
|
+
if (!kept || mainHead === "" || mainHead === kept.head)
|
|
79
|
+
return;
|
|
80
|
+
const verdict = cachedBaselineVerdict(mainHead);
|
|
81
|
+
if (verdict === "red")
|
|
82
|
+
return;
|
|
83
|
+
if (verdict === undefined) {
|
|
84
|
+
if (kept.waitingSince === undefined) {
|
|
85
|
+
state.mainRedRetry = { ...kept, waitingSince: now };
|
|
86
|
+
saveRunnerStateBestEffort(root, state);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (now - kept.waitingSince < MAIN_RED_VERDICT_WAIT_MS)
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
state.mainRedRetry = undefined;
|
|
93
|
+
saveRunnerStateBestEffort(root, state);
|
|
94
|
+
const sha = await pinnedRetrySha(root, kept.entry.role, mainBranch, state);
|
|
95
|
+
if (!sha || queuedMerges(root).some((e) => e.role === kept.entry.role))
|
|
96
|
+
return;
|
|
97
|
+
try {
|
|
98
|
+
enqueueRetry(root, retryEntry(kept.entry, sha, "main_red"));
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
warnEventBestEffort(root, kept.entry.role, `the kept main_red change could not be re-queued: ${errorMessage(err)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import { dropMerge } from "./merge-queue.js";
|
|
3
2
|
import { runCostIsReal } from "../pi/pi-run-result.js";
|
|
4
3
|
import { reviewRunConfig, seamHoldKey, seamPairName } from "../config/config-views.js";
|
|
5
|
-
import {
|
|
6
|
-
import { applyMergeOutcome, ERROR_STREAK_WARN } from "../
|
|
7
|
-
import {
|
|
4
|
+
import { saveRunnerStateBestEffort } from "../runner/runner-state.js";
|
|
5
|
+
import { applyMergeOutcome, ERROR_STREAK_WARN } from "../run/run-apply.js";
|
|
6
|
+
import { observeAccount } from "../agent/subscription-account.js";
|
|
8
7
|
import { logEventBestEffort, usageFragment, warnEventBestEffort } from "../events/events.js";
|
|
9
8
|
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
10
9
|
import { isJsonObject, nonNegativeNumber } from "../files/json-object.js";
|
|
11
10
|
import { removeQuiet } from "../files/files.js";
|
|
12
11
|
import { mergeStatePath } from "../paths.js";
|
|
13
12
|
import { consecutiveFailuresWarning } from "../text/phrases.js";
|
|
13
|
+
import { truncate } from "../text/text.js";
|
|
14
14
|
import { canonicalRoleId } from "../roles/legacy-role-ids.js";
|
|
15
15
|
/** How each merge stage names itself in the merge cell. */
|
|
16
16
|
export const MERGE_STAGE_LABELS = {
|
|
@@ -18,6 +18,7 @@ export const MERGE_STAGE_LABELS = {
|
|
|
18
18
|
"check-wait": "waiting to check",
|
|
19
19
|
"build-check": "checking",
|
|
20
20
|
reviewing: "reviewing",
|
|
21
|
+
"reviewing-check": "reviewing, checking",
|
|
21
22
|
"resolve-wait": "waiting to resolve conflicts",
|
|
22
23
|
resolving: "resolving conflicts",
|
|
23
24
|
"provider-wait": "waiting for the provider",
|
|
@@ -232,22 +233,48 @@ export function isMergeChange(value) {
|
|
|
232
233
|
(value.status === "merging" || value.status === "approved" || value.status === "done") &&
|
|
233
234
|
(value.stageSince === undefined || nonNegativeNumber(value.stageSince, null) !== null));
|
|
234
235
|
}
|
|
236
|
+
/** Why a merge ended without landing, for the `merge_failed` event, or undefined when the
|
|
237
|
+
* state names none. `rejected` reads the review's first reason, only when that review was
|
|
238
|
+
* recorded during this merge (`at >= startedAt`; an older one is stale). Every other failure
|
|
239
|
+
* reads `lastError`, minus its `merge failed: ` and `<result> — ` prefixes; a bare result
|
|
240
|
+
* (the catch-all `merge failed: merge_conflict`) names no reason. */
|
|
241
|
+
export function mergeFailureReason(state, result, startedAt) {
|
|
242
|
+
if (result === "changed" || result === "aborted")
|
|
243
|
+
return undefined;
|
|
244
|
+
if (result === "rejected") {
|
|
245
|
+
const review = state.lastReview;
|
|
246
|
+
if (!review || review.at < startedAt)
|
|
247
|
+
return undefined;
|
|
248
|
+
return review.reasons[0]?.trim() || undefined;
|
|
249
|
+
}
|
|
250
|
+
let reason = (state.lastError ?? "").trim();
|
|
251
|
+
if (reason.startsWith("merge failed: "))
|
|
252
|
+
reason = reason.slice("merge failed: ".length);
|
|
253
|
+
if (reason.startsWith(`${result} — `))
|
|
254
|
+
reason = reason.slice(`${result} — `.length);
|
|
255
|
+
reason = reason.trim();
|
|
256
|
+
return reason === "" || reason === result ? undefined : reason;
|
|
257
|
+
}
|
|
235
258
|
/** Fold one merge's outcome into its role's state and drop its queue entry — the 3/5
|
|
236
259
|
* write-back every pipeline outcome goes through (a verification's final verdict, a merged change,
|
|
237
260
|
* an aborted approved change): apply the result to the live state object (paired with the entry's
|
|
238
261
|
* change for the last-result cell — applyMergeOutcome), persist it, log merge_done/merge_failed
|
|
262
|
+
* (the failed event carries the reason mergeFailureReason derives)
|
|
239
263
|
* with the merge's own duration (from its verification's start) and usage (omitted when zero — the
|
|
240
264
|
* 4/5 idiom, so review-exempt merges render bare), and drop the entry: EVERY defined result
|
|
241
265
|
* drops. Both event writes are best-effort: an unwritable feed (ENOSPC, EACCES) must not
|
|
242
266
|
* skip the drop, or the settled entry would stay queued and be re-verified and re-merged on
|
|
243
|
-
* the next drain — the queue's "drop after EVERY outcome" contract (merge-queue.ts).
|
|
244
|
-
*
|
|
267
|
+
* the next drain — the queue's "drop after EVERY outcome" contract (merge-queue.ts). A failed
|
|
268
|
+
* state save warns and does not skip the drop either: the in-memory state already holds the
|
|
269
|
+
* outcome and the runner's next save writes it. The change's marker record is the caller's
|
|
270
|
+
* concern (removeMergeChange). */
|
|
245
271
|
export function writeMergeOutcome(root, entry, state, result, durationMs, usage, file) {
|
|
246
272
|
applyMergeOutcome(state, result, entry);
|
|
247
|
-
|
|
273
|
+
saveRunnerStateBestEffort(root, state);
|
|
274
|
+
const reason = result === "changed" ? undefined : mergeFailureReason(state, result, Date.now() - durationMs);
|
|
248
275
|
// One warning per error episode, merge side (BUGS.md 2026-09-30): a review rejection
|
|
249
|
-
// feeds the same streak the
|
|
250
|
-
// AFTER the
|
|
276
|
+
// feeds the same streak the run-side warn reads (src/run/run-finalize.ts), but it resolves
|
|
277
|
+
// AFTER the run's end-save, so the crossing warn belongs here — same shape, same bar.
|
|
251
278
|
if (result === "rejected" && (state.consecutiveErrors ?? 0) === ERROR_STREAK_WARN) {
|
|
252
279
|
warnEventBestEffort(root, entry.role, consecutiveFailuresWarning(state.consecutiveErrors ?? 0, state.lastError ?? "review rejected"));
|
|
253
280
|
}
|
|
@@ -260,6 +287,7 @@ export function writeMergeOutcome(root, entry, state, result, durationMs, usage,
|
|
|
260
287
|
commit: entry.sha,
|
|
261
288
|
result,
|
|
262
289
|
summary: entry.summary,
|
|
290
|
+
...(reason !== undefined ? { reason: truncate(reason, 300) } : {}),
|
|
263
291
|
durationMs,
|
|
264
292
|
// Enqueue-to-outcome span, including the slot's own durationMs. A leftover-recovery
|
|
265
293
|
// re-enqueue writes a new entry with its own enqueuedAt, so a retried change counts from
|
|
@@ -271,6 +299,13 @@ export function writeMergeOutcome(root, entry, state, result, durationMs, usage,
|
|
|
271
299
|
// sees a merge's spend under its reviewer's provider. Omitted when empty (a merge
|
|
272
300
|
// whose runs carried no provider), the same omit-when-empty idiom the usage fields use.
|
|
273
301
|
...(Object.keys(usage.providerCost).length > 0 ? { providerCost: usage.providerCost } : {}),
|
|
302
|
+
// The merge's notional spend per backend (plans/codex-backend.md part 3d/9): the
|
|
303
|
+
// subscription seam whose runs made the merge's API-equivalent cost, so a codex merge's
|
|
304
|
+
// subscription spend names codex, not a generic claude line. Omitted when the merge spent
|
|
305
|
+
// no notional, the same omit-when-empty idiom the usage fields use.
|
|
306
|
+
...(Object.keys(usage.notionalByBackend ?? {}).length > 0
|
|
307
|
+
? { notionalByBackend: usage.notionalByBackend }
|
|
308
|
+
: {}),
|
|
274
309
|
});
|
|
275
310
|
dropMerge(file);
|
|
276
311
|
}
|
|
@@ -283,7 +318,7 @@ export function writeMergeOutcome(root, entry, state, result, durationMs, usage,
|
|
|
283
318
|
* folded onto the rate-limit and backend-failure stamps this fold writes: a 429 on a claude
|
|
284
319
|
* reviewer must hold `claude`, not the pi author's provider (PLANS.md "Mixed claude + pi
|
|
285
320
|
* fleets, part 2/4"). The reviewer's final runs are folded HERE (merge-core.ts's
|
|
286
|
-
* reviewPinnedChange), not inside
|
|
321
|
+
* reviewPinnedChange), not inside RunnerPi — runner-pi.ts stamps the retried first attempt; this
|
|
287
322
|
* stamps the final run, which ends on a different seam when the reviewer runs on a
|
|
288
323
|
* non-pi backend. */
|
|
289
324
|
/** Fetch-or-create a provider's accumulator entry in `usage.providerCost` and add `amount`
|
|
@@ -298,8 +333,16 @@ function addProviderCost(usage, provider, amount, notional) {
|
|
|
298
333
|
e.costUsd += amount;
|
|
299
334
|
usage.providerCost[provider] = e;
|
|
300
335
|
}
|
|
336
|
+
/** Add a subscription run's notional spend to the merge's per-backend tally, keyed by the
|
|
337
|
+
* run's agent (falling back to the claude default, like the run-end fold), so the merge event
|
|
338
|
+
* names the backend whose runs made its API-equivalent cost (plans/codex-backend.md part 3d/9). */
|
|
339
|
+
function addBackendNotional(usage, agent, amount) {
|
|
340
|
+
const backend = agent && agent !== "" ? agent : "claude";
|
|
341
|
+
usage.notionalByBackend = usage.notionalByBackend ?? {};
|
|
342
|
+
usage.notionalByBackend[backend] = (usage.notionalByBackend[backend] ?? 0) + amount;
|
|
343
|
+
}
|
|
301
344
|
export function mergeUsage(author, root) {
|
|
302
|
-
const usage = { tokens: 0, cost: 0, providerCost: {} };
|
|
345
|
+
const usage = { tokens: 0, cost: 0, providerCost: {}, notionalByBackend: {} };
|
|
303
346
|
const reviewConfig = reviewRunConfig(author.config);
|
|
304
347
|
const reviewProvider = seamHoldKey(reviewConfig);
|
|
305
348
|
const reviewPair = seamPairName(reviewConfig);
|
|
@@ -308,13 +351,14 @@ export function mergeUsage(author, root) {
|
|
|
308
351
|
foldUsage: (run) => {
|
|
309
352
|
author.foldMergeUsage(run, reviewProvider, reviewPair);
|
|
310
353
|
// A claude reviewer's or resolver's billing/window readings reach the account file the
|
|
311
|
-
// moment this run folds, not at the author's next
|
|
312
|
-
// claude reviewer still updates the five-hour window before the author's next
|
|
313
|
-
// starts (PLANS.md "Mixed claude + pi fleets, part 3/4").
|
|
314
|
-
// call; the file is a cache and
|
|
354
|
+
// moment this run folds, not at the author's next run end — so a pi-author team's
|
|
355
|
+
// claude reviewer still updates the five-hour window before the author's next run
|
|
356
|
+
// starts (PLANS.md "Mixed claude + pi fleets, part 3/4"). The run's agent names the
|
|
357
|
+
// backend's own file. Best-effort like run-finalize.ts's call; the file is a cache and
|
|
358
|
+
// an unwritable state dir must not fail the merge.
|
|
315
359
|
if (run.billing !== undefined || run.rateWindows !== undefined) {
|
|
316
360
|
try {
|
|
317
|
-
|
|
361
|
+
observeAccount(root, run.agent ?? "pi", run);
|
|
318
362
|
}
|
|
319
363
|
catch {
|
|
320
364
|
// The next reporting run rewrites it; nothing this merge depends on is lost.
|
|
@@ -331,6 +375,7 @@ export function mergeUsage(author, root) {
|
|
|
331
375
|
}
|
|
332
376
|
else {
|
|
333
377
|
usage.notionalCost = (usage.notionalCost ?? 0) + run.costUsd;
|
|
378
|
+
addBackendNotional(usage, run.agent, run.costUsd);
|
|
334
379
|
if (reviewProvider !== undefined) {
|
|
335
380
|
addProviderCost(usage, reviewProvider, run.costUsd, true);
|
|
336
381
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** The stack merger — the merge-half machinery of a multi-change batch (extracted from
|
|
3
2
|
* merge-batch.ts, whose header scopes that file to the verify and merge halves): assemble the
|
|
4
3
|
* approved changes as a cherry-picked stack on main's current tip, run ONE scope-`batch` build
|
|
@@ -6,8 +5,9 @@
|
|
|
6
5
|
* re-stack loop for a main that moves mid-check, the doc-only exemption and its cross-checks,
|
|
7
6
|
* and the per-attempt abort observation. The bisect over a red stack (mergeApproved, in
|
|
8
7
|
* merge-batch.ts) drives this file's mergeStack one prefix at a time. */
|
|
9
|
-
import {
|
|
10
|
-
import { branchHead, deleteRef,
|
|
8
|
+
import { committerIdentArgs, gitTry } from "../git/git-run.js";
|
|
9
|
+
import { branchHead, deleteRef, headOf } from "../git/git.js";
|
|
10
|
+
import { treeDiffFiles } from "../git/git-diff.js";
|
|
11
11
|
import { mergeRefName, mergeLockDir } from "../paths.js";
|
|
12
12
|
import { ensureDetachedWorktree } from "../git/worktree.js";
|
|
13
13
|
import { useWorktree } from "../git/worktree-use.js";
|
|
@@ -15,11 +15,14 @@ import { exemptSkipBlockReason } from "./merge-to-main.js";
|
|
|
15
15
|
import { logNewQuestions } from "./merge-questions.js";
|
|
16
16
|
import { openQuestions } from "../backlog/backlog.js";
|
|
17
17
|
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
18
|
-
import { ffMainToOrWhy } from "./merge-git.js";
|
|
18
|
+
import { ffMainToOrWhy, overwriteBlockedReason, primaryCheckoutBlockers } from "./merge-git.js";
|
|
19
19
|
import { syncRootInstall } from "../build/dep-install.js";
|
|
20
20
|
import { withLock } from "../concurrency/lock.js";
|
|
21
21
|
import { runScopedBuildCheck } from "../build/build-check-scoped.js";
|
|
22
|
+
import { mergeCommandOf } from "../build/build-check-detect.js";
|
|
22
23
|
import { noteGreenBaseline } from "../baseline/main-baseline.js";
|
|
24
|
+
import { onlyKnownMainFailures } from "../baseline/main-watch.js";
|
|
25
|
+
import { unverifiedTreeOutcome } from "../build/build-check-events.js";
|
|
23
26
|
import { isExemptDiff } from "../review/exemptions.js";
|
|
24
27
|
import { checkWaitStage, setMergeStage } from "./merge-slot.js";
|
|
25
28
|
/** How many times a batch whose fast-forward lost the race to a moved main re-stacks onto the
|
|
@@ -62,7 +65,7 @@ async function assembleStack(root, mainBranch, wtPath, entries) {
|
|
|
62
65
|
// from merging only its last diff and orphaning the work beneath it. (Not a
|
|
63
66
|
// rebase: after 2/5 the role branches sit at main and each merge lives only in its
|
|
64
67
|
// pinned ref — there is nothing to rebase.)
|
|
65
|
-
const pick = await gitTry(wt, ...
|
|
68
|
+
const pick = await gitTry(wt, ...(await committerIdentArgs(wt)), "cherry-pick", `${base}..${entry.sha}`);
|
|
66
69
|
if (pick === null) {
|
|
67
70
|
// A conflict (or an already-applied patch — the crash-window re-drain): abort the
|
|
68
71
|
// pick and abandon to one-at-a-time. The worktree may be left mid-state; its next
|
|
@@ -84,8 +87,8 @@ async function assembleStack(root, mainBranch, wtPath, entries) {
|
|
|
84
87
|
* --no-renames so a rename out of a code path lists the deleted source, not only its exempt
|
|
85
88
|
* destination. An unreadable diff is not exempt: the caller runs the check. */
|
|
86
89
|
async function exemptTreeDelta(root, from, to, exemptPaths) {
|
|
87
|
-
const
|
|
88
|
-
return
|
|
90
|
+
const files = await treeDiffFiles(root, from, to);
|
|
91
|
+
return files !== null && isExemptDiff(files, exemptPaths);
|
|
89
92
|
}
|
|
90
93
|
/** Merge `entries` — the whole stack, or one bisect prefix of it — as ONE fast-forward on its
|
|
91
94
|
* own green check: assemble them on main's current tip in `wtPath`, run ONE scope-`batch`
|
|
@@ -97,6 +100,8 @@ async function exemptTreeDelta(root, from, to, exemptPaths) {
|
|
|
97
100
|
* build (the reasoning verifyMerge's exempt arm applies) — and runs the same cross-checks
|
|
98
101
|
* that arm runs, the backlog heading structure and the false-fix claim; one that fails either
|
|
99
102
|
* returns `conflict`, so the per-change fallback applies the gate's own verdict per change.
|
|
103
|
+
* A primary checkout on main that holds uncommitted or untracked files the stack changes
|
|
104
|
+
* returns `blocked` before any check runs, with no re-stack.
|
|
100
105
|
* On `landed` every entry's ref is deleted and the red-main baseline is seeded with the tip
|
|
101
106
|
* when a check PASSED on exactly it — a skipped check seeds nothing, like verifyMerge's
|
|
102
107
|
* exempt arm. The check's events log under the first entry's role. */
|
|
@@ -121,6 +126,12 @@ async function mergeStackIn(ctx, wtPath, entries) {
|
|
|
121
126
|
return { kind: "conflict" }; // main unreadable or a pick conflicted
|
|
122
127
|
const { base, mergeDone } = assembled;
|
|
123
128
|
const tip = mergeDone.at(-1).sha;
|
|
129
|
+
// A dirty primary checkout is one `git status` away: block before the check, not after it.
|
|
130
|
+
const dirty = await primaryCheckoutBlockers(ctx.root, ctx.mainBranch, tip);
|
|
131
|
+
if (dirty !== null) {
|
|
132
|
+
warnEventBestEffort(ctx.root, mergeDone.at(-1).role, `merge blocked: ${overwriteBlockedReason(dirty)}`);
|
|
133
|
+
return { kind: "blocked" };
|
|
134
|
+
}
|
|
124
135
|
// The expensive deterministic half, shared: ONE run over the combined tree. Outcome
|
|
125
136
|
// routing — null: no declared check, merge directly; "failed": red or a merge-scope
|
|
126
137
|
// timeout, the caller bisects; "skipped": no npm / broken toolchain (the helper warned),
|
|
@@ -133,8 +144,7 @@ async function mergeStackIn(ctx, wtPath, entries) {
|
|
|
133
144
|
// stacked on — decides like verifyMerge does: doc-only cannot break the build, so no
|
|
134
145
|
// check runs (a doc-only stack, or a bisect's size-1 doc-only remainder, merges on the
|
|
135
146
|
// exemption instead of paying a suite run no code change asked for).
|
|
136
|
-
const
|
|
137
|
-
const stackFiles = stackDiff === null ? null : gitLines(stackDiff);
|
|
147
|
+
const stackFiles = await treeDiffFiles(ctx.root, base, tip);
|
|
138
148
|
if (stackFiles !== null && isExemptDiff(stackFiles, exemptPaths)) {
|
|
139
149
|
// The same cross-checks verifyMerge's exempt arm runs (merge-to-main.ts's
|
|
140
150
|
// exemptSkipBlockReason): the skip must not wave through an md-only edit the gate would
|
|
@@ -153,10 +163,16 @@ async function mergeStackIn(ctx, wtPath, entries) {
|
|
|
153
163
|
const check = await runScopedBuildCheck(ctx.root, roles[0], "batch", wtPath, ctx.config, undefined, undefined, undefined, checkWaitStage(ctx.root, roles));
|
|
154
164
|
for (const e of entries)
|
|
155
165
|
setMergeStage(ctx.root, e.role, "merging");
|
|
156
|
-
|
|
166
|
+
// Failing only main's known failures is not a red stack (verifyMerge's rule): the ff goes
|
|
167
|
+
// ahead, and no green seeds for a tree that did not pass.
|
|
168
|
+
const knownOnly = check !== null && check.outcome.status === "failed" &&
|
|
169
|
+
!unverifiedTreeOutcome(check.outcome) &&
|
|
170
|
+
onlyKnownMainFailures(ctx.root, check.outcome.failingTests) !== null;
|
|
171
|
+
if (check !== null && check.outcome.status === "failed" && !knownOnly)
|
|
157
172
|
return { kind: "red", ...check };
|
|
158
173
|
checkedTip = tip;
|
|
159
|
-
|
|
174
|
+
// A mergeCommand green seeds nothing: main's verdict comes from the main watch.
|
|
175
|
+
if (check !== null && check.outcome.status === "passed" && mergeCommandOf(ctx.config) === undefined)
|
|
160
176
|
seed = tip;
|
|
161
177
|
}
|
|
162
178
|
}
|