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
|
@@ -7,35 +7,67 @@
|
|
|
7
7
|
* for that shape has an empty base section and both sides non-empty; keeping both inserted sides
|
|
8
8
|
* adds no authored bytes, so no model run is needed.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
10
|
+
* Two more shapes settle the same way:
|
|
11
|
+
* - Entries above a deleted entry, in any `## ` section: one side added whole `### ` entries
|
|
12
|
+
* right above an entry the other side deleted. Typically the plan role prepends a plan on
|
|
13
|
+
* main while the change finishes the plan below it into PLANS-DONE.md, or the other way
|
|
14
|
+
* round. The inserting side is its new entries followed by exactly the base lines and the
|
|
15
|
+
* deleting side is empty, so the new entries alone carry both sides' edits.
|
|
16
|
+
* - Entries above a rewritten history: inside a live `## Done`/`## Fixed` section, the change
|
|
17
|
+
* added whole `### ` entries right above lines that main has since rewritten — a change
|
|
18
|
+
* committed before its repository's history moved to the archive, whose entries sat right
|
|
19
|
+
* below the insertion point. The change's side is then its new entries followed by exactly
|
|
20
|
+
* the base lines, and keeping those entries on top of main's side again adds no authored
|
|
21
|
+
* bytes.
|
|
22
|
+
* A fourth shape settles entry by entry instead of hunk by hunk (mergeBacklogEntries): each side
|
|
23
|
+
* finished or added different whole entries, so git's hunks cut through entries and match none
|
|
24
|
+
* of the shapes above. It reads the three index stages of PLANS.md, BUGS.md and QUESTIONS.md,
|
|
25
|
+
* not the hunks.
|
|
26
|
+
* Anything else — the same entry edited on both sides, a code file, a file one side deleted —
|
|
27
|
+
* goes to the pi resolver untouched. */
|
|
17
28
|
import fs from "node:fs";
|
|
18
29
|
import path from "node:path";
|
|
19
|
-
import { BACKLOG_FILES, FINISHED_ARCHIVES } from "../backlog/backlog-md.js";
|
|
20
|
-
import { writeTextAtomic } from "../files/files.js";
|
|
21
|
-
import { git } from "../git/git-run.js";
|
|
22
|
-
/** The lines
|
|
23
|
-
* or null. Insert-only: an empty base section and both sides carrying lines —
|
|
24
|
-
* change's side. Entries above a
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
import { BACKLOG_FILES, FINISHED_ARCHIVES, fenceTracker } from "../backlog/backlog-md.js";
|
|
31
|
+
import { readTextOrNull, writeTextAtomic } from "../files/files.js";
|
|
32
|
+
import { git, gitTry } from "../git/git-run.js";
|
|
33
|
+
/** The lines that replace `hunk`, when it is one of the shapes this pass settles (see the
|
|
34
|
+
* module comment), or null. Insert-only: an empty base section and both sides carrying lines —
|
|
35
|
+
* the change's side stacked on main's. Entries above a deleted entry: one side empty, the other
|
|
36
|
+
* whole `### ` entries followed by exactly the base lines — those entries alone. Entries above
|
|
37
|
+
* a rewritten history: the hunk sits in a finished section, and the change's side is whole
|
|
38
|
+
* `### ` entries followed by exactly the base lines — those entries stacked on main's side. */
|
|
39
|
+
function settledLines(hunk, lines) {
|
|
40
|
+
if (hunk.base.length === 0) {
|
|
41
|
+
return hunk.ours.length > 0 && hunk.theirs.length > 0 ? stacked(hunk.theirs, hunk.ours) : null;
|
|
42
|
+
}
|
|
43
|
+
if (hunk.theirs.length === 0)
|
|
44
|
+
return entriesAboveBase(hunk.ours, hunk.base);
|
|
45
|
+
if (hunk.ours.length === 0)
|
|
46
|
+
return entriesAboveBase(hunk.theirs, hunk.base);
|
|
47
|
+
if (!inFinishedSection(lines, hunk.start))
|
|
48
|
+
return null;
|
|
49
|
+
const inserted = entriesAboveBase(hunk.theirs, hunk.base);
|
|
50
|
+
return inserted === null ? null : stacked(inserted, hunk.ours);
|
|
51
|
+
}
|
|
52
|
+
/** The lines `side` adds above the `base` lines, when `side` is whole `### ` entries followed
|
|
53
|
+
* by exactly those base lines, or null — a side no longer than the base, a base line changed or
|
|
54
|
+
* missing, or added lines that do not open at a `### ` heading. */
|
|
55
|
+
function entriesAboveBase(side, base) {
|
|
56
|
+
if (side.length <= base.length)
|
|
31
57
|
return null;
|
|
32
|
-
const cut =
|
|
33
|
-
if (
|
|
58
|
+
const cut = side.length - base.length;
|
|
59
|
+
if (side.slice(cut).some((line, i) => line !== base[i]))
|
|
34
60
|
return null;
|
|
35
|
-
const inserted =
|
|
61
|
+
const inserted = side.slice(0, cut);
|
|
36
62
|
const first = inserted.find((line) => line.trim() !== "");
|
|
37
63
|
return first?.startsWith("### ") ? inserted : null;
|
|
38
64
|
}
|
|
65
|
+
/** `upper` then `lower`, with a blank separator when neither already supplies one, so the
|
|
66
|
+
* later merge (the change, `upper`) stays first. */
|
|
67
|
+
function stacked(upper, lower) {
|
|
68
|
+
const separated = lower.length === 0 || upper[upper.length - 1] === "" || lower[0] === "";
|
|
69
|
+
return separated ? [...upper, ...lower] : [...upper, "", ...lower];
|
|
70
|
+
}
|
|
39
71
|
/** True when the nearest `## ` heading above line `at` is a finished section (`## Done` or
|
|
40
72
|
* `## Fixed`), with no conflict marker in between — an earlier hunk's sides leave the section
|
|
41
73
|
* unknown, so the answer is then no. */
|
|
@@ -52,10 +84,8 @@ function inFinishedSection(lines, at) {
|
|
|
52
84
|
/** The finished sections' titles, from FINISHED_ARCHIVES. */
|
|
53
85
|
const FINISHED_SECTIONS = new Set(FINISHED_ARCHIVES.map((a) => a.section));
|
|
54
86
|
/** Rewrite every settleable conflict hunk in `text` and return the result, or null when any
|
|
55
|
-
* hunk is
|
|
56
|
-
* leaves the file for the model resolver. Each hunk is replaced by
|
|
57
|
-
* inserted, a blank separator when neither side already supplies one, then main's (ours)
|
|
58
|
-
* lines, so the later merge stays first. */
|
|
87
|
+
* hunk is no shape settledLines accepts (or the markers are malformed) — the caller then
|
|
88
|
+
* leaves the file for the model resolver. Each hunk is replaced by settledLines' lines. */
|
|
59
89
|
export function resolveInsertOnlyText(text) {
|
|
60
90
|
const lines = text.split("\n");
|
|
61
91
|
const hunks = parseHunks(lines);
|
|
@@ -63,16 +93,13 @@ export function resolveInsertOnlyText(text) {
|
|
|
63
93
|
return null;
|
|
64
94
|
if (hunks.length === 0)
|
|
65
95
|
return null;
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
96
|
+
const settled = hunks.map((hunk) => settledLines(hunk, lines));
|
|
97
|
+
if (settled.some((replacement) => replacement === null))
|
|
68
98
|
return null;
|
|
69
99
|
// Replace from the end so earlier indices stay valid.
|
|
70
100
|
for (let h = hunks.length - 1; h >= 0; h--) {
|
|
71
101
|
const hunk = hunks[h];
|
|
72
|
-
|
|
73
|
-
const separated = hunk.ours.length === 0 || theirs[theirs.length - 1] === "" || hunk.ours[0] === "";
|
|
74
|
-
const replacement = separated ? [...theirs, ...hunk.ours] : [...theirs, "", ...hunk.ours];
|
|
75
|
-
lines.splice(hunk.start, hunk.end - hunk.start + 1, ...replacement);
|
|
102
|
+
lines.splice(hunk.start, hunk.end - hunk.start + 1, ...settled[h]);
|
|
76
103
|
}
|
|
77
104
|
return lines.join("\n");
|
|
78
105
|
}
|
|
@@ -117,6 +144,133 @@ function collectUntilMarker(lines, start, marker, into) {
|
|
|
117
144
|
into.push(lines[i++]);
|
|
118
145
|
return i >= lines.length ? null : i + 1;
|
|
119
146
|
}
|
|
147
|
+
/** Split `text` into ParsedBacklog blocks, or null when a fence is still open at EOF or a
|
|
148
|
+
* section holds two entries with the same heading line. */
|
|
149
|
+
function parseBacklogBlocks(text) {
|
|
150
|
+
const fenced = fenceTracker();
|
|
151
|
+
const parsed = { preamble: "", sections: [] };
|
|
152
|
+
let current = [];
|
|
153
|
+
let flush = (block) => {
|
|
154
|
+
parsed.preamble = block;
|
|
155
|
+
};
|
|
156
|
+
const close = () => {
|
|
157
|
+
while (current.length > 0 && current[current.length - 1].trim() === "")
|
|
158
|
+
current.pop();
|
|
159
|
+
flush(current.join("\n"));
|
|
160
|
+
current = [];
|
|
161
|
+
};
|
|
162
|
+
for (const line of text.split("\n")) {
|
|
163
|
+
const structural = !fenced.inside(line);
|
|
164
|
+
if (structural && line.startsWith("## ")) {
|
|
165
|
+
close();
|
|
166
|
+
const section = { header: "", entries: [] };
|
|
167
|
+
parsed.sections.push(section);
|
|
168
|
+
flush = (block) => {
|
|
169
|
+
section.header = block;
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
else if (structural && line.startsWith("### ") && parsed.sections.length > 0) {
|
|
173
|
+
close();
|
|
174
|
+
const section = parsed.sections[parsed.sections.length - 1];
|
|
175
|
+
flush = (block) => section.entries.push({ key: line, text: block });
|
|
176
|
+
// The entry's heading line is `line` itself; the closure above captured it.
|
|
177
|
+
}
|
|
178
|
+
current.push(line);
|
|
179
|
+
}
|
|
180
|
+
if (fenced.open())
|
|
181
|
+
return null;
|
|
182
|
+
close();
|
|
183
|
+
for (const section of parsed.sections) {
|
|
184
|
+
if (new Set(section.entries.map((e) => e.key)).size !== section.entries.length)
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return parsed;
|
|
188
|
+
}
|
|
189
|
+
/** The one text all three versions agree on after at most one side's edit: the changed side
|
|
190
|
+
* when only one changed, else the shared text, or null when both changed differently. */
|
|
191
|
+
function settleBlock(base, ours, theirs) {
|
|
192
|
+
if (ours === theirs || theirs === base)
|
|
193
|
+
return ours;
|
|
194
|
+
return ours === base ? theirs : null;
|
|
195
|
+
}
|
|
196
|
+
/** Merge one section's entries (see mergeBacklogEntries), or null on a same-entry conflict. */
|
|
197
|
+
function mergeSectionEntries(base, ours, theirs) {
|
|
198
|
+
const baseBy = new Map(base.map((e) => [e.key, e.text]));
|
|
199
|
+
const theirsBy = new Map(theirs.map((e) => [e.key, e.text]));
|
|
200
|
+
const oursKeys = new Set(ours.map((e) => e.key));
|
|
201
|
+
const result = [];
|
|
202
|
+
for (const { key, text: o } of ours) {
|
|
203
|
+
const b = baseBy.get(key);
|
|
204
|
+
const t = theirsBy.get(key);
|
|
205
|
+
if (b === undefined) {
|
|
206
|
+
if (t !== undefined && t !== o)
|
|
207
|
+
return null;
|
|
208
|
+
result.push({ key, text: o });
|
|
209
|
+
}
|
|
210
|
+
else if (t === undefined) {
|
|
211
|
+
if (o !== b)
|
|
212
|
+
return null; // main edited what the change deleted
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const settled = settleBlock(b, o, t);
|
|
216
|
+
if (settled === null)
|
|
217
|
+
return null;
|
|
218
|
+
result.push({ key, text: settled });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// An entry main deleted stays deleted, unless the change edited it.
|
|
222
|
+
for (const { key, text: b } of base) {
|
|
223
|
+
const t = theirsBy.get(key);
|
|
224
|
+
if (!oursKeys.has(key) && t !== undefined && t !== b)
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
for (let i = 0; i < theirs.length; i++) {
|
|
228
|
+
const added = theirs[i];
|
|
229
|
+
if (baseBy.has(added.key) || oursKeys.has(added.key))
|
|
230
|
+
continue;
|
|
231
|
+
let at = 0;
|
|
232
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
233
|
+
const found = result.findIndex((e) => e.key === theirs[j].key);
|
|
234
|
+
if (found >= 0) {
|
|
235
|
+
at = found + 1;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
result.splice(at, 0, added);
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
/** Merge three versions of a backlog file entry by entry — `ours` is main, `theirs` the change —
|
|
244
|
+
* or null when that cannot be done without judgment: an open fence, a duplicate entry heading,
|
|
245
|
+
* differing `## ` heading sequences, a preamble or section header both sides changed, or the
|
|
246
|
+
* same entry edited on both sides (or edited by one side and deleted by the other). */
|
|
247
|
+
export function mergeBacklogEntries(base, ours, theirs) {
|
|
248
|
+
const [b, o, t] = [base, ours, theirs].map(parseBacklogBlocks);
|
|
249
|
+
if (!b || !o || !t)
|
|
250
|
+
return null;
|
|
251
|
+
const count = b.sections.length;
|
|
252
|
+
if (o.sections.length !== count || t.sections.length !== count)
|
|
253
|
+
return null;
|
|
254
|
+
const blocks = [];
|
|
255
|
+
const preamble = settleBlock(b.preamble, o.preamble, t.preamble);
|
|
256
|
+
if (preamble === null)
|
|
257
|
+
return null;
|
|
258
|
+
if (preamble !== "")
|
|
259
|
+
blocks.push(preamble);
|
|
260
|
+
for (let i = 0; i < count; i++) {
|
|
261
|
+
const [bs, os, ts] = [b.sections[i], o.sections[i], t.sections[i]];
|
|
262
|
+
const heading = (header) => header.split("\n")[0];
|
|
263
|
+
if (heading(bs.header) !== heading(os.header) || heading(bs.header) !== heading(ts.header)) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
const header = settleBlock(bs.header, os.header, ts.header);
|
|
267
|
+
const entries = mergeSectionEntries(bs.entries, os.entries, ts.entries);
|
|
268
|
+
if (header === null || entries === null)
|
|
269
|
+
return null;
|
|
270
|
+
blocks.push(header, ...entries.map((e) => e.text));
|
|
271
|
+
}
|
|
272
|
+
return `${blocks.join("\n\n")}\n`;
|
|
273
|
+
}
|
|
120
274
|
/** Settle the insert-only conflicts among `files` in `wt` (plans/parallel-work-instances.md,
|
|
121
275
|
* part 3/7): every conflicted repo-root backlog file (BACKLOG_FILES: PLANS.md, BUGS.md,
|
|
122
276
|
* QUESTIONS.md and the two archive files) whose hunks all settle is rewritten without markers and
|
|
@@ -131,15 +285,29 @@ export async function resolveBacklogInsertConflicts(wt, files) {
|
|
|
131
285
|
continue;
|
|
132
286
|
}
|
|
133
287
|
const p = path.join(wt, file);
|
|
134
|
-
|
|
288
|
+
// lstat, not stat: a symlink among the conflicted files is never followed out of the
|
|
289
|
+
// worktree (INV-G3 — a symlink holds no text of its own and its target could point out
|
|
290
|
+
// of it, so what a change that adds one put there is the link, not the target). Leaving
|
|
291
|
+
// it to the pi resolver keeps the same policy the conflict-marker and secret-leak gates
|
|
292
|
+
// pin: the resolver, like those gates, must not read an arbitrary host file.
|
|
293
|
+
let st;
|
|
135
294
|
try {
|
|
136
|
-
|
|
295
|
+
st = fs.lstatSync(p);
|
|
137
296
|
}
|
|
138
297
|
catch {
|
|
139
298
|
remaining.push(file); // deleted side or unreadable path — leave it to the resolver
|
|
140
299
|
continue;
|
|
141
300
|
}
|
|
142
|
-
|
|
301
|
+
if (!st.isFile()) {
|
|
302
|
+
remaining.push(file); // symlink, directory or gitlink — leave it to the resolver
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const text = readTextOrNull(p);
|
|
306
|
+
if (text === null) {
|
|
307
|
+
remaining.push(file); // deleted side or unreadable path — leave it to the resolver
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const resolved = resolveInsertOnlyText(text) ?? (await mergeStagedEntries(wt, file));
|
|
143
311
|
if (resolved === null) {
|
|
144
312
|
remaining.push(file);
|
|
145
313
|
continue;
|
|
@@ -153,3 +321,15 @@ export async function resolveBacklogInsertConflicts(wt, files) {
|
|
|
153
321
|
}
|
|
154
322
|
return remaining;
|
|
155
323
|
}
|
|
324
|
+
/** The entry-by-entry merge of `file`'s three index stages (1 base, 2 main, 3 the change), or
|
|
325
|
+
* null for an archive file, a stage missing on one side, or a merge needing judgment. */
|
|
326
|
+
async function mergeStagedEntries(wt, file) {
|
|
327
|
+
if (FINISHED_ARCHIVES.some((a) => a.archive === file))
|
|
328
|
+
return null;
|
|
329
|
+
const base = await gitTry(wt, "show", `:1:${file}`);
|
|
330
|
+
const ours = await gitTry(wt, "show", `:2:${file}`);
|
|
331
|
+
const theirs = await gitTry(wt, "show", `:3:${file}`);
|
|
332
|
+
if (base === null || ours === null || theirs === null)
|
|
333
|
+
return null;
|
|
334
|
+
return mergeBacklogEntries(base, ours, theirs);
|
|
335
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** Two of the three halves of a merge (plans/merge-queue.md 5/5, PLANS.md land-queue speed
|
|
3
2
|
* 2c): the per-change verify (verifyRequest) that merge-verify.ts's verify stage runs for every
|
|
4
3
|
* queued change, and the merge (mergeApproved) its one merge slot runs over the approved ones — a
|
|
@@ -31,7 +30,7 @@ export async function verifyRequest(ctx, req, w) {
|
|
|
31
30
|
purpose: "verify",
|
|
32
31
|
ref: req.sha,
|
|
33
32
|
signal: ctx.signal(),
|
|
34
|
-
// The live view's pool budget, same rule as the
|
|
33
|
+
// The live view's pool budget, same rule as the run path: a broken tumwater.json
|
|
35
34
|
// must not error the verify before its reviewer runs.
|
|
36
35
|
slotBudget: slotCount(ctx.config),
|
|
37
36
|
});
|
|
@@ -108,8 +107,10 @@ async function verifyRequestIn(ctx, req, w, wt) {
|
|
|
108
107
|
* Un-assemblable (a cherry-pick conflict, on the first stack or any prefix) → ABANDON the changes
|
|
109
108
|
* not yet merged to one-at-a-time, in queue order, stopping at the first non-terminal outcome, each
|
|
110
109
|
* through mergeApprovedChange — no second gate and no model run but mergeToMain's conflict
|
|
111
|
-
* resolver. main is
|
|
112
|
-
* merges re-verified in-lock whenever they differ from what their verify judged
|
|
110
|
+
* resolver. main is not left red unchecked: the only bytes this path ff's are a checked tip or
|
|
111
|
+
* per-change merges re-verified in-lock whenever they differ from what their verify judged (the
|
|
112
|
+
* in-lock check is `check.mergeCommand` when set, and the main watch then runs the full check
|
|
113
|
+
* after the merge).
|
|
113
114
|
*
|
|
114
115
|
* An abort is observed between steps, not only by the pi runs it kills: before each
|
|
115
116
|
* assembly-and-check attempt and before each one-change or fallback merge, so a stopping merge
|
|
@@ -136,6 +137,9 @@ export async function mergeApproved(ctx, approved, wiringFor) {
|
|
|
136
137
|
runGatePi: (opts) => w.runGatePi(opts),
|
|
137
138
|
foldUsage: w.foldUsage,
|
|
138
139
|
signal: ctx.signal,
|
|
140
|
+
reviewHeld: ctx.reviewHeld,
|
|
141
|
+
reviewRetries: ctx.reviewRetries,
|
|
142
|
+
reviewRetryDelayMs: ctx.reviewRetryDelayMs,
|
|
139
143
|
});
|
|
140
144
|
let aborted = false;
|
|
141
145
|
// Every finalized result goes through here: record it, then hand it to the caller's
|
|
@@ -230,7 +234,8 @@ export async function mergeApproved(ctx, approved, wiringFor) {
|
|
|
230
234
|
break;
|
|
231
235
|
}
|
|
232
236
|
else if (outcome.kind === "blocked") {
|
|
233
|
-
// The race was lost on every attempt
|
|
237
|
+
// The race was lost on every attempt (main is moving faster than a check completes), or
|
|
238
|
+
// the primary checkout holds files the stack changes (found before the check).
|
|
234
239
|
// Every change not yet merged keeps its ref with "merge_blocked": leftover recovery
|
|
235
240
|
// re-merges each through its own gate + tryMerge, whose in-lock check cannot lose it.
|
|
236
241
|
for (let s = front; s < entries.length; s++)
|
|
@@ -247,10 +252,11 @@ export async function mergeApproved(ctx, approved, wiringFor) {
|
|
|
247
252
|
// outcome (the rest keep entry + ref for the next merge). Each request merges its approved
|
|
248
253
|
// head (the synced pin its verify judged, not the bare pin) — no second gate, so no model run:
|
|
249
254
|
// re-gating would rebase onto the main the earlier entries just moved, and the rewritten
|
|
250
|
-
// sha misses the approved short-circuit. main is
|
|
255
|
+
// sha misses the approved short-circuit. main is not left red unchecked — mergeToMain's in-lock
|
|
251
256
|
// rebase + verifyMerge re-check every change whose tree differs from the one its verify
|
|
252
|
-
// judged
|
|
253
|
-
//
|
|
257
|
+
// judged (with check.mergeCommand set, by that cheaper command; the main watch runs the full
|
|
258
|
+
// check after the merge). Only the change being merged reads `merging`; the rest are back to
|
|
259
|
+
// awaiting their turn, and each one merged is done.
|
|
254
260
|
for (let s = mergeDoneCount; s < approved.length; s++)
|
|
255
261
|
report(s, "approved");
|
|
256
262
|
for (let s = mergeDoneCount; s < approved.length; s++) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Operator cancellation of a waiting merge (PLANS.md `tumwater queue --cancel <n>`), split
|
|
2
|
+
* out of merge-queue.ts so the queue stays the pure reader/writer it is: this module owns the
|
|
3
|
+
* destructive half of an unattempted entry — removing the queue file and deleting the role's
|
|
4
|
+
* merge pin so next-run leftover recovery cannot silently re-enqueue the change. It mirrors
|
|
5
|
+
* merge-requeue.ts (the other settled-entry editor living outside merge-queue.ts) and takes no
|
|
6
|
+
* lock: the queue is the inbox idiom (one file per entry, dropMerge tolerant of a concurrent
|
|
7
|
+
* drop) and the pin delete is value-guarded. */
|
|
8
|
+
import { queuedMergeFiles, dropMerge } from "./merge-queue.js";
|
|
9
|
+
import { mergeRefName } from "../paths.js";
|
|
10
|
+
import { deleteRefIfAt } from "../git/git.js";
|
|
11
|
+
import { logEventBestEffort } from "../events/events.js";
|
|
12
|
+
/** Remove one readable queue entry: delete that role's merge pin first (deleteRefIfAt, a no-op
|
|
13
|
+
* when the ref is gone or names a different sha, so a newer pin is never removed), then drop
|
|
14
|
+
* the queue file (ENOENT already a no-op), logging one `merge_cancelled` event under the role,
|
|
15
|
+
* and return the `cancelled` outcome naming what was removed. The single removal both
|
|
16
|
+
* `cancelQueuedMerge` (by position) and `cancelQueuedMergeBySha` (by sha) share, so the CLI and
|
|
17
|
+
* the dashboard's queue-cancel cannot drift on what cancelling means. */
|
|
18
|
+
async function removeQueuedEntry(root, target) {
|
|
19
|
+
await deleteRefIfAt(root, mergeRefName(target.entry.role), target.entry.sha);
|
|
20
|
+
dropMerge(target.file);
|
|
21
|
+
logEventBestEffort(root, {
|
|
22
|
+
source: target.entry.role,
|
|
23
|
+
type: "merge_cancelled",
|
|
24
|
+
commit: target.entry.sha,
|
|
25
|
+
summary: target.entry.summary,
|
|
26
|
+
});
|
|
27
|
+
return { status: "cancelled", role: target.entry.role, sha: target.entry.sha, summary: target.entry.summary };
|
|
28
|
+
}
|
|
29
|
+
/** Remove the Nth queued merge — 1-based, as `tumwater queue` numbers it — and delete that
|
|
30
|
+
* entry's role's merge pin, so next-run leftover recovery finds no pin and no branch-ahead
|
|
31
|
+
* commit and does not re-enqueue the change. `position` is resolved against
|
|
32
|
+
* `queuedMergeFiles` (the same readable, filename-ordered list the queue renderer shows), so
|
|
33
|
+
* the position matches the line `tumwater queue` printed. The pin is deleted first with
|
|
34
|
+
* deleteRefIfAt (a no-op when the ref is gone or names a different sha, so a newer pin is
|
|
35
|
+
* never removed), then the queue file is dropped (ENOENT already a no-op). Logs one
|
|
36
|
+
* `merge_cancelled` event under the role. Out-of-range changes nothing and returns the
|
|
37
|
+
* current readable depth. */
|
|
38
|
+
export async function cancelQueuedMerge(root, position) {
|
|
39
|
+
const files = queuedMergeFiles(root);
|
|
40
|
+
if (position < 1 || position > files.length)
|
|
41
|
+
return { status: "out-of-range", depth: files.length };
|
|
42
|
+
return removeQueuedEntry(root, files[position - 1]);
|
|
43
|
+
}
|
|
44
|
+
/** Remove the queued merge whose entry's pinned sha is `sha` (the browser twin of
|
|
45
|
+
* `tumwater queue --cancel <n>`, resolving by sha because the dashboard rows carry one). An
|
|
46
|
+
* entry whose sha matches none is `{ status: "gone" }` and changes nothing; otherwise it runs
|
|
47
|
+
* the same `removeQueuedEntry` the position variant uses. */
|
|
48
|
+
export async function cancelQueuedMergeBySha(root, sha) {
|
|
49
|
+
const target = queuedMergeFiles(root).find((q) => q.entry.sha.toLowerCase() === sha.toLowerCase());
|
|
50
|
+
if (!target)
|
|
51
|
+
return { status: "gone" };
|
|
52
|
+
return removeQueuedEntry(root, target);
|
|
53
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import { deleteRef, headOf, patchId } from "../git/git.js";
|
|
3
2
|
import { mergeRefName } from "../paths.js";
|
|
4
|
-
import { recordReview } from "../
|
|
5
|
-
import {
|
|
3
|
+
import { recordReview } from "../run/run-apply.js";
|
|
4
|
+
import { saveRunnerStateBestEffort } from "../runner/runner-state.js";
|
|
6
5
|
import { unverifiedTreeOutcome } from "../build/build-check-events.js";
|
|
7
6
|
import { checkFailureReasons, describeCheck, failureHeadline } from "../build/build-check-report.js";
|
|
7
|
+
import { onlyKnownMainFailures } from "../baseline/main-watch.js";
|
|
8
8
|
import { mainTipVerdict } from "../baseline/main-red.js";
|
|
9
9
|
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
10
10
|
import { mainRedNotMine } from "../text/phrases.js";
|
|
11
11
|
import { normalizeClusterKey } from "../failure/failure-cluster.js";
|
|
12
12
|
import { MERGE_FLAKY_WARNING_PREFIX, isKnownFlake } from "../review/known-flakes.js";
|
|
13
|
-
/** Consecutive red in-lock merge checks of one patch (
|
|
13
|
+
/** Consecutive red in-lock merge checks of one patch (RunnerState.mergeCheckFailures) before
|
|
14
14
|
* the merge is attributed instead of retried: the first red keeps the pin for one more
|
|
15
15
|
* attempt — a load flake gets its retry, as the gate's pre-check gets one (PLANS.md land-queue
|
|
16
16
|
* 1/3) — and the second is judged by main's own verdict at its tip (attributeRedCheck). Without
|
|
@@ -28,7 +28,7 @@ function pinKept(patch, count) {
|
|
|
28
28
|
return patch === null || count < MERGE_CHECK_FAILURE_LIMIT;
|
|
29
29
|
}
|
|
30
30
|
/** Tally one deterministic failure of the same patch toward attribution: read the worktree's
|
|
31
|
-
* head and its patch-id against main, carry
|
|
31
|
+
* head and its patch-id against main, carry RunnerState.mergeCheckFailures's count across
|
|
32
32
|
* re-merges of the identical patch (a clean rebase onto a moved main keeps the patch-id, so the
|
|
33
33
|
* retries of one change add up while a different change starts fresh), and store the strike.
|
|
34
34
|
* An unreadable patch-id cannot be matched, so pinKept never counts it toward the limit.
|
|
@@ -60,8 +60,9 @@ async function keepPinOrAttribute(ctx, wt, reason, attribute) {
|
|
|
60
60
|
return attribute(head);
|
|
61
61
|
}
|
|
62
62
|
/** The terminal deterministic-reject sink shared by mergeBlocked and attributeRedCheck:
|
|
63
|
-
* record the reject review, reset the unreviewed-failure streak, persist state
|
|
64
|
-
*
|
|
63
|
+
* record the reject review, reset the unreviewed-failure streak, persist state best-effort
|
|
64
|
+
* (a failed save warns; the merge write-back saves the same state later), delete the merge
|
|
65
|
+
* ref, and log review_rejected, even on a full disk. The shared `_merge` checkout is left in place
|
|
65
66
|
* (plans/worktree-pool.md, part 2c): it persists across merges like `_gate-main`, and a
|
|
66
67
|
* rejection discards the pinned ref, not the checkout. Returns "rejected". review.ts's reject
|
|
67
68
|
* path is a different sink — it resets the branch instead of deleting the ref — and keeps its
|
|
@@ -69,7 +70,7 @@ async function keepPinOrAttribute(ctx, wt, reason, attribute) {
|
|
|
69
70
|
async function rejectChange(ctx, role, head, reasons, state) {
|
|
70
71
|
recordReview(state, "reject", reasons, head);
|
|
71
72
|
state.unreviewFailures = 0;
|
|
72
|
-
|
|
73
|
+
saveRunnerStateBestEffort(ctx.root, state);
|
|
73
74
|
await deleteRef(ctx.root, mergeRefName(role));
|
|
74
75
|
logEventBestEffort(ctx.root, { source: role, type: "review_rejected", head, reasons });
|
|
75
76
|
return "rejected";
|
|
@@ -82,6 +83,18 @@ async function rejectChange(ctx, role, head, reasons, state) {
|
|
|
82
83
|
function firstCheckReason(red) {
|
|
83
84
|
return checkFailureReasons(red.check, red.outcome)[0];
|
|
84
85
|
}
|
|
86
|
+
/** Keep the pin when a red check fails only main's known failures (onlyKnownMainFailures): not
|
|
87
|
+
* the change's failure and not a strike, so recovery's re-merge — whose verifyMerge lets such a
|
|
88
|
+
* check through — lands it. Null when the red names a test outside the known set. */
|
|
89
|
+
function keepForKnownFailures(ctx, role, label, red, state) {
|
|
90
|
+
const known = onlyKnownMainFailures(ctx.root, red.outcome.failingTests);
|
|
91
|
+
if (!known)
|
|
92
|
+
return null;
|
|
93
|
+
state.lastError = `${label} failed only main's known failures, merge kept: ${known.join(", ")}`;
|
|
94
|
+
warnEventBestEffort(ctx.root, role, `${label} fails only main's known failures: ${known.join(", ")}`);
|
|
95
|
+
saveRunnerStateBestEffort(ctx.root, state);
|
|
96
|
+
return "merge_blocked";
|
|
97
|
+
}
|
|
85
98
|
/** A merge blocked because its in-lock check went red on the rebased tree (merge-to-main.ts's
|
|
86
99
|
* verifyMerge). The count is keyed by the patch-id; under MERGE_CHECK_FAILURE_LIMIT the
|
|
87
100
|
* pin is kept (merge_blocked) for recovery's re-merge; at the limit the red is attributed like
|
|
@@ -95,6 +108,9 @@ export async function mergeCheckRed(ctx, role, wt, red) {
|
|
|
95
108
|
ctx.state.lastError = `merge failed: ${firstCheckReason(red)}`;
|
|
96
109
|
return "merge_blocked";
|
|
97
110
|
}
|
|
111
|
+
const kept = keepForKnownFailures(ctx, role, "merge check", red, ctx.state);
|
|
112
|
+
if (kept)
|
|
113
|
+
return kept;
|
|
98
114
|
const reason = firstCheckReason(red);
|
|
99
115
|
let attributed = false;
|
|
100
116
|
const result = await keepPinOrAttribute(ctx, wt, reason, (head) => {
|
|
@@ -103,7 +119,7 @@ export async function mergeCheckRed(ctx, role, wt, red) {
|
|
|
103
119
|
});
|
|
104
120
|
// The first red of a patch's in-lock check is exactly the retry that exists for load flakes,
|
|
105
121
|
// so it is the ONE signal that could name a flaky test after the fact — and lastError, the
|
|
106
|
-
// only place this reason is written, is overwritten by the leftover-recovery
|
|
122
|
+
// only place this reason is written, is overwritten by the leftover-recovery run a second
|
|
107
123
|
// later (BUGS.md 2026-10-14). Log it as a warning so the failure digest and a later audit can
|
|
108
124
|
// name the flaky test; the second red at MERGE_CHECK_FAILURE_LIMIT is attributed through
|
|
109
125
|
// review_rejected, whose reasons are already durable.
|
|
@@ -117,7 +133,7 @@ export async function mergeCheckRed(ctx, role, wt, red) {
|
|
|
117
133
|
}
|
|
118
134
|
/** Record a merge check that failed and then passed on its kept-pin retry as a flake, in the
|
|
119
135
|
* form the gate's pre-check uses (MERGE_FLAKY_WARNING_PREFIX), so the gate's known-flake memory,
|
|
120
|
-
* doctor's `flaky checks` and the
|
|
136
|
+
* doctor's `flaky checks` and the run prompt's flake note count it (PLANS.md 2026-10-16). Reads
|
|
121
137
|
* the headline mergeCheckRed stored on the strike record only when its patch-id is the merged
|
|
122
138
|
* pin's (a stale strike of a discarded pin must not be logged); a pin that was never red (or
|
|
123
139
|
* only cross-check blocked) has none and logs nothing. Call before the strike is cleared. */
|
|
@@ -134,7 +150,7 @@ export function noteMergeRetryPassed(ctx, role, pinPatch) {
|
|
|
134
150
|
* under the rebase, and a differently-rebased tree can pass the cross-check — but at the
|
|
135
151
|
* limit the block is attributed to the change: rejected deterministically with the block
|
|
136
152
|
* reason, the pin deleted, no model run. Without this a resolution that trips the same
|
|
137
|
-
* cross-check on every re-resolve pays a fresh conflict-resolver run each
|
|
153
|
+
* cross-check on every re-resolve pays a fresh conflict-resolver run each run, forever
|
|
138
154
|
* (BUGS.md 2026-10-02: five resolver sessions for one fix-claim-blocked pin). */
|
|
139
155
|
export async function mergeBlocked(ctx, role, wt, blocked) {
|
|
140
156
|
return keepPinOrAttribute(ctx, wt, blocked, (head) => rejectChange(ctx, role, head, [`merge blocked: ${blocked}`], ctx.state));
|
|
@@ -144,15 +160,18 @@ export async function mergeBlocked(ctx, role, wt, blocked) {
|
|
|
144
160
|
* MERGE_CHECK_FAILURE_LIMIT — by the gate's rule: ask main's own
|
|
145
161
|
* verdict at its current tip (mainTipVerdict — usually a cache hit, since every merge seeds
|
|
146
162
|
* the SHA it moved main to). Main green → the change broke the check: rejected deterministically
|
|
147
|
-
* — reasons in lastReview for the author's next
|
|
163
|
+
* — reasons in lastReview for the author's next run, the strike count reset, ref deleted,
|
|
148
164
|
* review_rejected logged, no pi run. Main red → not this change's failure: "main_red" with the
|
|
149
|
-
* ref kept and unreviewFailures untouched, so
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
165
|
+
* ref kept and unreviewFailures untouched, so the orchestrator poll (requeueMainRedPin) re-queues
|
|
166
|
+
* it once main's head moves to one the watch has not found red, and leftover recovery holds it
|
|
167
|
+
* until then. No verdict → reject, the
|
|
168
|
+
* safe default, and the reasons say so. The verdict
|
|
169
|
+
* is persisted best-effort before it returns. A red whose headline matches a flake the gate or
|
|
170
|
+
* a merge retry recorded (isKnownFlake) is excused once per headline per pin
|
|
171
|
+
* (RunnerState.mergeFlakeExcused): "merge_blocked" with the ref kept, before main's verdict,
|
|
172
|
+
* because that verdict is usually a cached green that would reject a change over a flaky test
|
|
173
|
+
* (PLANS.md 2026-10-16, BUGS.md 2026-10-06). A repeat of the same headline is attributed as
|
|
174
|
+
* above. */
|
|
156
175
|
export async function attributeRedCheck(ctx, role, head, label, red, state) {
|
|
157
176
|
// An unverified red — a run that spanned a host sleep, the tree never judged — is not the
|
|
158
177
|
// change's failure and not a strike: keep the ref for recovery's re-merge and name the sleep
|
|
@@ -160,22 +179,25 @@ export async function attributeRedCheck(ctx, role, head, label, red, state) {
|
|
|
160
179
|
// only live when the check produced a verdict.
|
|
161
180
|
if (unverifiedTreeOutcome(red.outcome)) {
|
|
162
181
|
state.lastError = `${label}: ${firstCheckReason(red)}`;
|
|
163
|
-
|
|
182
|
+
saveRunnerStateBestEffort(ctx.root, state);
|
|
164
183
|
return "merge_blocked";
|
|
165
184
|
}
|
|
185
|
+
const kept = keepForKnownFailures(ctx, role, label, red, state);
|
|
186
|
+
if (kept)
|
|
187
|
+
return kept;
|
|
166
188
|
const headline = failureHeadline(red.outcome.outputTail) ?? describeCheck(red.check);
|
|
167
189
|
const key = normalizeClusterKey(headline);
|
|
168
190
|
if (isKnownFlake(ctx.root, headline) && state.mergeFlakeExcused !== key) {
|
|
169
191
|
state.mergeFlakeExcused = key;
|
|
170
192
|
state.lastError = `${label} failed: known-flaky failure, merge kept: ${headline}`;
|
|
171
193
|
warnEventBestEffort(ctx.root, role, `${label}: known-flaky failure, merge kept: ${headline}`);
|
|
172
|
-
|
|
194
|
+
saveRunnerStateBestEffort(ctx.root, state);
|
|
173
195
|
return "merge_blocked";
|
|
174
196
|
}
|
|
175
197
|
const main = await mainTipVerdict(ctx.root, role, ctx.mainBranch, ctx.config);
|
|
176
198
|
if (main.status === "red") {
|
|
177
199
|
state.lastError = `${label} failed: ${mainRedNotMine(main.sha)}`;
|
|
178
|
-
|
|
200
|
+
saveRunnerStateBestEffort(ctx.root, state);
|
|
179
201
|
return "main_red";
|
|
180
202
|
}
|
|
181
203
|
const reasons = checkFailureReasons(red.check, red.outcome);
|