tumwater 0.2.0 → 0.3.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 +42 -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 +4 -3
- 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 +402 -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 +246 -55
- package/dist/src/backlog/backlog-cli-view.js +280 -0
- package/dist/src/backlog/backlog-cli.js +85 -274
- package/dist/src/backlog/backlog-commit.js +4 -5
- package/dist/src/backlog/backlog-eligibility.js +175 -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 +254 -0
- package/dist/src/backlog/backlog-release.js +117 -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-culprit.js +94 -0
- package/dist/src/baseline/main-guardrails.js +209 -0
- package/dist/src/baseline/main-red.js +139 -32
- package/dist/src/baseline/main-revert.js +133 -0
- package/dist/src/baseline/main-watch.js +184 -0
- package/dist/src/brief/brief-edit.js +9 -11
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget.js +30 -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 +1 -2
- 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 +118 -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 +150 -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 +308 -590
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-role-command.js +420 -0
- package/dist/src/cli/cli-start-summary.js +160 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
- package/dist/src/cli/completion.js +149 -22
- package/dist/src/cli/config-commands.js +41 -18
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +189 -73
- package/dist/src/cli/log-commands.js +148 -82
- package/dist/src/cli/question-commands.js +205 -10
- package/dist/src/cli.js +106 -43
- package/dist/src/collections.js +26 -10
- 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 +13 -1
- package/dist/src/config/config-fallbacks.js +93 -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 +141 -199
- package/dist/src/config/config-views.js +127 -53
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +131 -264
- package/dist/src/config/config.js +48 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
- package/dist/src/doctor/doctor-backlog-plans.js +263 -0
- package/dist/src/doctor/doctor-backlog.js +100 -496
- package/dist/src/doctor/doctor-check-names.js +81 -0
- package/dist/src/doctor/doctor-checks.js +52 -579
- package/dist/src/doctor/doctor-claude-checks.js +145 -0
- package/dist/src/doctor/doctor-codex-checks.js +279 -0
- package/dist/src/doctor/doctor-contention-checks.js +380 -0
- package/dist/src/doctor/doctor-holds.js +82 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +236 -402
- package/dist/src/doctor/doctor-notify.js +3 -4
- package/dist/src/doctor/doctor-opencode-checks.js +162 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-project-checks.js +303 -0
- package/dist/src/doctor/doctor.js +121 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +7 -11
- package/dist/src/events/event-format.js +181 -86
- package/dist/src/events/event-labels.js +12 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +39 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +60 -24
- 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 +56 -56
- package/dist/src/failure/failure-render.js +31 -23
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +99 -35
- 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 +177 -84
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +108 -58
- package/dist/src/gates/maintenance-quota.js +10 -13
- 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 +77 -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 +36 -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 +42 -18
- package/dist/src/gui/gui-backlog-endpoints.js +155 -0
- package/dist/src/gui/gui-command.js +8 -13
- package/dist/src/gui/gui-config-commands.js +94 -72
- package/dist/src/gui/gui-endpoint-commands.js +4 -3
- 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 +108 -13
- package/dist/src/gui/gui-prompt-commands.js +11 -14
- package/dist/src/gui/gui-report-endpoints.js +90 -0
- package/dist/src/gui/gui-server.js +11 -4
- 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 +147 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +193 -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 +25 -9
- 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 +81 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +150 -11
- 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 +90 -0
- package/dist/src/merge/merge-queue-render.js +49 -11
- package/dist/src/merge/merge-queue.js +31 -15
- package/dist/src/merge/merge-requeue.js +102 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +33 -11
- package/dist/src/merge/merge-to-main.js +86 -27
- package/dist/src/merge/merge-top.js +69 -0
- package/dist/src/merge/merge-verify.js +12 -10
- package/dist/src/notes-cli.js +30 -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 +320 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
- package/dist/src/orchestrator/orchestrator.js +128 -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 +201 -0
- package/dist/src/orchestrator/scheduling-skip.js +185 -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 +116 -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 +18 -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 +121 -211
- package/dist/src/prompt/notes-backlog.js +181 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +129 -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 +161 -77
- package/dist/src/report/report-fold.js +129 -52
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +133 -64
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +26 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/known-flakes.js +100 -1
- package/dist/src/review/post-merge-review.js +106 -0
- package/dist/src/review/protected-edits.js +4 -5
- 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 +155 -360
- 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 +69 -12
- package/dist/src/roles/role-guidance.js +55 -20
- package/dist/src/roles/role-labels.js +1 -2
- package/dist/src/roles/role-render.js +67 -50
- package/dist/src/roles/role-view.js +59 -47
- package/dist/src/roles/roles.js +31 -31
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/{tick → run}/qa-coverage.js +9 -27
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +179 -0
- package/dist/src/run/run-detail-render.js +98 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -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} +108 -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} +108 -50
- 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} +91 -56
- 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 +172 -0
- package/dist/src/run/stage-path-checks.js +730 -0
- package/dist/src/{loop → runner}/leftover.js +80 -25
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +202 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +107 -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} +21 -15
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +102 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- 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 +43 -16
- 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 +28 -28
- package/dist/src/status/status-roles.js +39 -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 +5 -6
- 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 +63 -17
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +57 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +67 -13
- package/dist/src/ui/badges.js +65 -37
- package/dist/src/ui/gui/gui-client-backlog.js +292 -0
- package/dist/src/ui/gui/gui-client-boot.js +32 -26
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
- package/dist/src/ui/gui/gui-client-drawer.js +126 -285
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +434 -67
- package/dist/src/ui/gui/gui-client-labels.js +1 -2
- 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 +55 -51
- package/dist/src/ui/gui/gui-client-pending.js +227 -23
- package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
- package/dist/src/ui/gui/gui-client-report.js +87 -427
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +56 -4
- package/dist/src/ui/gui/gui-client-team.js +25 -465
- package/dist/src/ui/gui/gui-client.js +27 -25
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +9 -7
- package/dist/src/ui/gui/gui-styles.js +34 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +136 -49
- package/dist/src/ui/status-render.js +101 -82
- package/dist/src/ui/team-alerts.js +248 -57
- package/dist/src/ui/tone.js +5 -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 +51 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +3 -5
- package/dist/src/ui/tui/tui-editor-backends.js +132 -0
- package/dist/src/ui/tui/tui-editor-brief.js +10 -12
- package/dist/src/ui/tui/tui-editor-budget.js +35 -31
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
- package/dist/src/ui/tui/tui-editor-config.js +86 -0
- package/dist/src/ui/tui/tui-editor-entry.js +36 -47
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +37 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
- package/dist/src/ui/tui/tui-editor-interval.js +11 -9
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +3 -5
- 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 +99 -35
- package/dist/src/ui/tui/tui-editor-retire.js +4 -6
- package/dist/src/ui/tui/tui-editor-role-key.js +76 -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 +60 -34
- package/dist/src/ui/tui/tui-editor-slots.js +23 -0
- package/dist/src/ui/tui/tui-editors.js +35 -2
- package/dist/src/ui/tui/tui-frame.js +56 -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 +24 -9
- package/dist/src/ui/tui/tui-pane-activity.js +14 -11
- package/dist/src/ui/tui/tui-pane-backlog.js +171 -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 +58 -8
- package/dist/src/ui/tui/tui-pane-history.js +402 -82
- package/dist/src/ui/tui/tui-pane-pending.js +308 -13
- package/dist/src/ui/tui/tui-pane-queued.js +71 -55
- package/dist/src/ui/tui/tui-pane-report.js +46 -14
- package/dist/src/ui/tui/tui-pane-session.js +25 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +80 -23
- package/dist/src/ui/tui/tui-panes.js +100 -30
- package/dist/src/ui/tui/tui.js +14 -14
- package/dist/src/verdict/fix-claim.js +82 -32
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +42 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { writeBytesAtomic } from "../files/files.js";
|
|
3
|
+
import { GitError, committerIdentArgs, git, gitCouldNotStart, gitTry, runGit } from "../git/git-run.js";
|
|
4
|
+
import { readTextOrNull, writeBytesAtomic } from "../files/files.js";
|
|
5
5
|
import { currentBranch, gitLines, headOf } from "../git/git.js";
|
|
6
|
-
import { unquotePorcelainPath } from "../git/git-diff.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { treeDiffFiles, unquotePorcelainPath } from "../git/git-diff.js";
|
|
7
|
+
import { errCode } from "../errno.js";
|
|
8
|
+
import { CONFIG_BASENAME, configPath } from "../paths.js";
|
|
9
|
+
import { withPrimaryWorktreeLockAsync } from "../concurrency/lock.js";
|
|
10
|
+
import { TUMWATER_SOURCE, warnEventBestEffort } from "../events/events.js";
|
|
11
|
+
import { errorFirstLine, errorMessage, joinWithMore } from "../text/text.js";
|
|
10
12
|
/** Git plumbing for the merge flow: rebasing a worktree branch onto main, inspecting and
|
|
11
13
|
* finishing a conflicted rebase, and fast-forwarding main — the mechanics, with no merge policy
|
|
12
14
|
* of their own. Merge policy (lock → rebase → verify → ff, conflict resolution, batch ff)
|
|
13
15
|
* lives in merge-to-main.ts on top of these. Moved here from merge-to-main.ts: the cluster started
|
|
14
|
-
* as merge-to-main.ts-only helpers (organize
|
|
16
|
+
* as merge-to-main.ts-only helpers (organize run 78, moving them out of git/git.ts), but the
|
|
15
17
|
* merger and the batch merger grew to call them directly, leaving merge-to-main.ts exporting
|
|
16
18
|
* generic rebase/ff plumbing that had nothing to do with merging. */
|
|
17
19
|
/** Paths currently in conflict (unmerged) in the worktree. C-quoted names are decoded to
|
|
18
20
|
* real paths — a non-ASCII conflicted file arrives as `"h\303\251llo.ts"` (core.quotePath is on
|
|
19
21
|
* by default), and undecoded it does not exist on disk: hasConflictMarkers could never read
|
|
20
22
|
* it, so an unresolved conflict in such a file passed the marker check and continueRebase
|
|
21
|
-
* committed its markers to main.
|
|
23
|
+
* committed its markers to main. A failed `git diff` throws: read as "no conflicts", it sent
|
|
24
|
+
* the resolver's caller straight to `continueRebase`, which stages every marker, and a
|
|
25
|
+
* stopped cherry-pick down the path that deletes a hand-back's ref. */
|
|
22
26
|
export async function conflictedFiles(wt) {
|
|
23
|
-
const out = await
|
|
27
|
+
const out = await git(wt, "diff", "--name-only", "--diff-filter=U");
|
|
24
28
|
return gitLines(out).map(unquotePorcelainPath);
|
|
25
29
|
}
|
|
26
30
|
/** How many main commits the resolver prompt lists, and how many body lines it keeps per
|
|
@@ -61,22 +65,26 @@ export async function mainCommitsTouching(wt, since, mainBranch, files) {
|
|
|
61
65
|
* cleaning up: "rebased" (including already up to date), "conflict" (the rebase stopped on
|
|
62
66
|
* unmerged paths, which remain in the worktree for a resolver), or "other" (any other
|
|
63
67
|
* failure). Shared by the two rebase wrappers below, which differ only in cleanup policy.
|
|
64
|
-
* Rebase — not merge — keeps main's history linear: each
|
|
65
|
-
* of whatever main holds.
|
|
68
|
+
* Rebase — not merge — keeps main's history linear: each run merges as its own commit on top
|
|
69
|
+
* of whatever main holds. A rebase git could not start throws (gitCouldNotStart), as does a
|
|
70
|
+
* failed conflict listing: neither says anything about the change, so neither may count as a
|
|
71
|
+
* merge conflict. */
|
|
66
72
|
async function attemptRebase(wt, mainBranch) {
|
|
67
73
|
try {
|
|
68
74
|
// The -c ident is needed for the rewritten committer identity. diff3 carries the base
|
|
69
75
|
// section of every conflict, which the deterministic backlog resolver reads
|
|
70
76
|
// (plans/parallel-work-instances.md, part 3/7); rebaseOntoMain aborts on conflict, so the
|
|
71
77
|
// marker style only matters to its leave-conflicts sibling.
|
|
72
|
-
await runGit(wt, [...
|
|
78
|
+
await runGit(wt, [...(await committerIdentArgs(wt)), "-c", "merge.conflictStyle=diff3", "rebase", mainBranch]);
|
|
73
79
|
return "rebased";
|
|
74
80
|
}
|
|
75
|
-
catch {
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (gitCouldNotStart(err))
|
|
83
|
+
throw err;
|
|
76
84
|
return (await conflictedFiles(wt)).length > 0 ? "conflict" : "other";
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
|
-
/** Rebase the worktree branch onto main (main may have advanced during the
|
|
87
|
+
/** Rebase the worktree branch onto main (main may have advanced during the run).
|
|
80
88
|
* Returns false and aborts the rebase on conflict. */
|
|
81
89
|
export async function rebaseOntoMain(wt, mainBranch) {
|
|
82
90
|
const state = await attemptRebase(wt, mainBranch);
|
|
@@ -98,23 +106,44 @@ export async function rebaseOntoMainLeaveConflicts(wt, mainBranch) {
|
|
|
98
106
|
* The one marker definition hasConflictMarkers and conflictHunkRanges share, so the ranges a
|
|
99
107
|
* conflict prompt names are exactly the blocks the marker check later rejects. */
|
|
100
108
|
const MARKER_EDGE = /^(<{7}|>{7})( |$)/m;
|
|
109
|
+
/** One file's marker check: "clean", "marked" (holds a start/end marker) or "unreadable". Only
|
|
110
|
+
* a path with no file (deleted, a parent replaced, or a directory/gitlink) is clean without
|
|
111
|
+
* being read; any other read error cannot be vouched for. */
|
|
112
|
+
function markerState(wt, f) {
|
|
113
|
+
// lstat, not stat: a symlink is never followed out of the worktree. A symlink (or a
|
|
114
|
+
// directory/gitlink) holds no text of its own to check, so it counts as clean — the same
|
|
115
|
+
// policy secret-leaks (INV-G4) already pins: a change that adds a symlink writes no
|
|
116
|
+
// content, and its target is not what the worktree put there.
|
|
117
|
+
let st;
|
|
118
|
+
try {
|
|
119
|
+
st = fs.lstatSync(path.join(wt, f));
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
const code = errCode(err);
|
|
123
|
+
return code === "ENOENT" || code === "ENOTDIR" ? "clean" : "unreadable";
|
|
124
|
+
}
|
|
125
|
+
if (!st.isFile())
|
|
126
|
+
return "clean";
|
|
127
|
+
let text;
|
|
128
|
+
try {
|
|
129
|
+
text = fs.readFileSync(path.join(wt, f), "utf8");
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const code = errCode(err);
|
|
133
|
+
return code === "ENOENT" || code === "ENOTDIR" || code === "EISDIR" ? "clean" : "unreadable";
|
|
134
|
+
}
|
|
135
|
+
return MARKER_EDGE.test(text) ? "marked" : "clean";
|
|
136
|
+
}
|
|
101
137
|
/** True if any of the given files still contains a git conflict marker.
|
|
102
|
-
* A
|
|
138
|
+
* A missing path counts as resolved (the resolver chose the deletion), but an unreadable one
|
|
139
|
+
* counts as conflicted: a check that cannot read a file cannot vouch for it. */
|
|
103
140
|
export function hasConflictMarkers(wt, files) {
|
|
104
141
|
// Only start/end markers are checked: every real conflict block carries them, while a bare
|
|
105
142
|
// `=======` line is legitimate content (a markdown setext or RST underline of exactly seven
|
|
106
143
|
// characters), and flagging it would reject clean resolutions forever. A resolver that
|
|
107
144
|
// leaves only a separator line behind is treated as resolved; its stray line is content the
|
|
108
145
|
// project's own tests can catch.
|
|
109
|
-
return files.some((f) =>
|
|
110
|
-
const p = path.join(wt, f);
|
|
111
|
-
try {
|
|
112
|
-
return MARKER_EDGE.test(fs.readFileSync(p, "utf8"));
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
});
|
|
146
|
+
return files.some((f) => markerState(wt, f) !== "clean");
|
|
118
147
|
}
|
|
119
148
|
/** How many conflict hunks a conflict prompt lists per file: every hunk of a typical backlog or
|
|
120
149
|
* code conflict, and a bound on a file pasted over wholesale. */
|
|
@@ -150,13 +179,17 @@ export function conflictHunkRanges(text) {
|
|
|
150
179
|
* file counts, backlog or code; a deleted, unreadable or binary (NUL-bearing) file lists none. */
|
|
151
180
|
export function conflictedFileHunks(wt, files) {
|
|
152
181
|
return files.map((f) => {
|
|
153
|
-
|
|
182
|
+
// Deleted or unreadable reads as empty: no ranges to name. A symlink is never followed
|
|
183
|
+
// out of the worktree (it holds no text of its own), so it too reads as empty.
|
|
184
|
+
let full = path.join(wt, f);
|
|
154
185
|
try {
|
|
155
|
-
|
|
186
|
+
if (!fs.lstatSync(full).isFile())
|
|
187
|
+
return { path: f, hunks: [], more: 0 };
|
|
156
188
|
}
|
|
157
189
|
catch {
|
|
158
|
-
|
|
190
|
+
return { path: f, hunks: [], more: 0 };
|
|
159
191
|
}
|
|
192
|
+
const text = readTextOrNull(full) ?? "";
|
|
160
193
|
const all = text.includes("\0") ? [] : conflictHunkRanges(text);
|
|
161
194
|
return { path: f, hunks: all.slice(0, MAX_LISTED_HUNKS), more: Math.max(0, all.length - MAX_LISTED_HUNKS) };
|
|
162
195
|
});
|
|
@@ -168,8 +201,17 @@ export function conflictedFileHunks(wt, files) {
|
|
|
168
201
|
* lines on main (PLANS.md "Robust conflict landing, part 2/2"). Builds on hasConflictMarkers
|
|
169
202
|
* above, the shared definition of what a marker is. */
|
|
170
203
|
export function conflictMarkerReason(wt, files) {
|
|
171
|
-
const
|
|
172
|
-
|
|
204
|
+
const states = files.map((f) => [f, markerState(wt, f)]);
|
|
205
|
+
const named = (state) => states.filter(([, s]) => s === state).map(([f]) => f);
|
|
206
|
+
const marked = named("marked");
|
|
207
|
+
const unreadable = named("unreadable");
|
|
208
|
+
const clauses = [
|
|
209
|
+
...(marked.length > 0 ? [`conflict markers remain in: ${marked.join(", ")}`] : []),
|
|
210
|
+
...(unreadable.length > 0
|
|
211
|
+
? [`could not be read to check for conflict markers: ${unreadable.join(", ")}`]
|
|
212
|
+
: []),
|
|
213
|
+
];
|
|
214
|
+
return clauses.length > 0 ? clauses.join("; ") : undefined;
|
|
173
215
|
}
|
|
174
216
|
/** Conclude an in-progress rebase with everything in the worktree as the resolution.
|
|
175
217
|
* GIT_EDITOR=true so `rebase --continue` can never block on a commit-message prompt. A
|
|
@@ -179,24 +221,40 @@ export function conflictMarkerReason(wt, files) {
|
|
|
179
221
|
* added; the caller aborts and reports merge_conflict. */
|
|
180
222
|
export async function continueRebase(wt) {
|
|
181
223
|
await git(wt, "add", "-A");
|
|
182
|
-
await runGit(wt, [...
|
|
224
|
+
await runGit(wt, [...(await committerIdentArgs(wt)), "rebase", "--continue"], { GIT_EDITOR: "true" });
|
|
183
225
|
return headOf(wt, "HEAD");
|
|
184
226
|
}
|
|
185
227
|
/** The live config's bytes, when the merge about to fast-forward `ref` would delete it
|
|
186
228
|
* (plans/portability.md §4b/7): the config file exists, is tracked in the current index (and
|
|
187
229
|
* HEAD — a staged-or-dirty config makes `git merge --ff-only` refuse below, so the two agree
|
|
188
230
|
* on every merge that can succeed), and is absent from the incoming tree. Anything else —
|
|
189
|
-
* already untracked, already absent, present in `ref` — needs no preserve step.
|
|
231
|
+
* already untracked, already absent, present in `ref` — needs no preserve step. A failed
|
|
232
|
+
* `cat-file` reads as absent, which only preserves bytes the write-back then leaves alone. */
|
|
190
233
|
export async function configBytesToPreserve(root, ref) {
|
|
191
234
|
const cfg = configPath(root);
|
|
192
235
|
if (!fs.existsSync(cfg))
|
|
193
236
|
return null;
|
|
194
|
-
if ((await
|
|
237
|
+
if (!(await configTracked(root)))
|
|
195
238
|
return null;
|
|
196
239
|
if ((await gitTry(root, "cat-file", "-e", `${ref}:${CONFIG_BASENAME}`)) !== null)
|
|
197
240
|
return null;
|
|
198
241
|
return fs.readFileSync(cfg);
|
|
199
242
|
}
|
|
243
|
+
/** Whether the live config is tracked in `root`'s index: `ls-files --error-unmatch` exits 1 for
|
|
244
|
+
* an untracked path, and any other failure throws. Read as "untracked", an unreadable index or
|
|
245
|
+
* a git that could not start skipped the preserve step, so a merge that untracks the config
|
|
246
|
+
* deleted the live one. */
|
|
247
|
+
async function configTracked(root) {
|
|
248
|
+
try {
|
|
249
|
+
await git(root, "ls-files", "--error-unmatch", CONFIG_BASENAME);
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
if (err instanceof GitError && err.code === 1)
|
|
254
|
+
return false;
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
200
258
|
/** The preserve step's write-back, restore-only-when-absent: write the saved bytes only when
|
|
201
259
|
* the live config is absent at write-back time. The merge deletes the file mid-window, so a
|
|
202
260
|
* config request applied in that window (3/7's applyConfigRequest runs outside the merge lock)
|
|
@@ -219,7 +277,9 @@ export function restoreConfigBytes(root, saved) {
|
|
|
219
277
|
* files update), otherwise a local ref push. Returns true on success. The working-tree arm
|
|
220
278
|
* preserves the live config across a merge that untracks it (plans/portability.md §4b/7):
|
|
221
279
|
* without the write-back, the commit that removes the tracked config would delete the team's
|
|
222
|
-
* running config out from under it and the next config poll would fall back to defaults.
|
|
280
|
+
* running config out from under it and the next config poll would fall back to defaults. The
|
|
281
|
+
* merge has already happened by the time of the write-back, so a failed write-back is a warning
|
|
282
|
+
* event and the function still returns null. */
|
|
223
283
|
export async function ffMainToOrWhy(root, ref, mainBranch) {
|
|
224
284
|
const primaryBranch = await currentBranch(root);
|
|
225
285
|
if (primaryBranch === mainBranch) {
|
|
@@ -228,7 +288,7 @@ export async function ffMainToOrWhy(root, ref, mainBranch) {
|
|
|
228
288
|
// that read before this merge can rename its stale whole-file write over the merged change.
|
|
229
289
|
// The ref-push arm below touches no working tree and needs no lock; the save/merge/restore
|
|
230
290
|
// stays one short section, never held across a build re-check.
|
|
231
|
-
return
|
|
291
|
+
return withPrimaryWorktreeLockAsync(root, async () => {
|
|
232
292
|
const saved = await configBytesToPreserve(root, ref);
|
|
233
293
|
try {
|
|
234
294
|
await git(root, "merge", "--ff-only", ref);
|
|
@@ -236,7 +296,13 @@ export async function ffMainToOrWhy(root, ref, mainBranch) {
|
|
|
236
296
|
catch (err) {
|
|
237
297
|
return ffFailureReason(root, ref, mainBranch, err);
|
|
238
298
|
}
|
|
239
|
-
|
|
299
|
+
try {
|
|
300
|
+
restoreConfigBytes(root, saved);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `main fast-forwarded, but writing the live ${CONFIG_BASENAME} back failed: ` +
|
|
304
|
+
`${errorMessage(err)}; restore it by hand (it is no longer tracked on main)`);
|
|
305
|
+
}
|
|
240
306
|
return null;
|
|
241
307
|
});
|
|
242
308
|
}
|
|
@@ -249,14 +315,87 @@ export async function ffMainToOrWhy(root, ref, mainBranch) {
|
|
|
249
315
|
}
|
|
250
316
|
}
|
|
251
317
|
/** Why a fast-forward to `ref` failed: main having moved off `ref`'s base (a commit that did not
|
|
252
|
-
* take the merge lock) is named with its new head and subject;
|
|
253
|
-
*
|
|
318
|
+
* take the merge lock) is named with its new head and subject; a dirty primary checkout is
|
|
319
|
+
* reported with the files it would overwrite; any other cause reports git's first error line. */
|
|
254
320
|
async function ffFailureReason(root, ref, mainBranch, err) {
|
|
255
321
|
if ((await gitTry(root, "merge-base", "--is-ancestor", mainBranch, ref)) === null) {
|
|
256
322
|
const now = await gitTry(root, "log", "-1", "--format=%h %s", mainBranch);
|
|
257
323
|
return `main moved outside the merge lock (now ${now?.trim() ?? mainBranch})`;
|
|
258
324
|
}
|
|
259
|
-
|
|
325
|
+
const blockers = overwriteBlockers(errorMessage(err));
|
|
326
|
+
if (blockers.uncommitted.length > 0 || blockers.untracked.length > 0) {
|
|
327
|
+
return `fast-forward failed: ${overwriteBlockedReason(blockers)}`;
|
|
328
|
+
}
|
|
329
|
+
return `fast-forward failed: ${errorFirstLine(err)}`;
|
|
330
|
+
}
|
|
331
|
+
/** The files git lists under its two "would be overwritten by merge:" headers. */
|
|
332
|
+
export function overwriteBlockers(gitError) {
|
|
333
|
+
const out = { uncommitted: [], untracked: [] };
|
|
334
|
+
let group = null;
|
|
335
|
+
for (const line of gitError.split("\n")) {
|
|
336
|
+
if (line.includes("Your local changes to the following files would be overwritten by merge:")) {
|
|
337
|
+
group = out.uncommitted;
|
|
338
|
+
}
|
|
339
|
+
else if (line.includes("untracked working tree files would be overwritten by merge:")) {
|
|
340
|
+
group = out.untracked;
|
|
341
|
+
}
|
|
342
|
+
else if (group && line.startsWith("\t")) {
|
|
343
|
+
group.push(line.trim());
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
group = null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
const MAX_BLOCKERS_LISTED = 5;
|
|
352
|
+
/** Why a dirty primary checkout blocks a merge: up to 5 files per group. Callers prefix it with
|
|
353
|
+
* their own stage (`fast-forward failed: `, or none from the pre-flight). */
|
|
354
|
+
export function overwriteBlockedReason(blockers) {
|
|
355
|
+
const list = (names) => joinWithMore(names, MAX_BLOCKERS_LISTED);
|
|
356
|
+
const groups = [];
|
|
357
|
+
if (blockers.uncommitted.length > 0)
|
|
358
|
+
groups.push(`uncommitted: ${list(blockers.uncommitted)}`);
|
|
359
|
+
if (blockers.untracked.length > 0)
|
|
360
|
+
groups.push(`untracked: ${list(blockers.untracked)}`);
|
|
361
|
+
const where = `the primary checkout (${groups.join("; ")})`;
|
|
362
|
+
return `the merge would overwrite files in ${where} — commit, stash or remove them there`;
|
|
363
|
+
}
|
|
364
|
+
/** The primary checkout's dirty paths a merge to `tip` would overwrite, read before any check
|
|
365
|
+
* runs: null when the checkout is not on main (the ref push touches no working tree), when
|
|
366
|
+
* nothing the merge changes is dirty, or when a git read fails — the fast-forward still decides. */
|
|
367
|
+
export async function primaryCheckoutBlockers(root, mainBranch, tip) {
|
|
368
|
+
if ((await currentBranch(root)) !== mainBranch)
|
|
369
|
+
return null;
|
|
370
|
+
const changed = await treeDiffFiles(root, mainBranch, tip);
|
|
371
|
+
const status = await gitTry(root, "--no-optional-locks", "status", "--porcelain=v1", "-z", "--untracked-files=all");
|
|
372
|
+
if (changed === null || status === null)
|
|
373
|
+
return null;
|
|
374
|
+
const untrackedSet = new Set();
|
|
375
|
+
const dirtySet = new Set();
|
|
376
|
+
const records = status.split("\0");
|
|
377
|
+
for (let i = 0; i < records.length; i++) {
|
|
378
|
+
const rec = records[i];
|
|
379
|
+
if (rec.length < 4)
|
|
380
|
+
continue;
|
|
381
|
+
const xy = rec.slice(0, 2);
|
|
382
|
+
const file = rec.slice(3);
|
|
383
|
+
if (xy === "??") {
|
|
384
|
+
untrackedSet.add(file);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
dirtySet.add(file);
|
|
388
|
+
if (xy.startsWith("R") || xy.startsWith("C")) {
|
|
389
|
+
const source = records[++i];
|
|
390
|
+
if (source)
|
|
391
|
+
dirtySet.add(source);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const out = {
|
|
395
|
+
uncommitted: changed.filter((f) => dirtySet.has(f)),
|
|
396
|
+
untracked: changed.filter((f) => untrackedSet.has(f)),
|
|
397
|
+
};
|
|
398
|
+
return out.uncommitted.length > 0 || out.untracked.length > 0 ? out : null;
|
|
260
399
|
}
|
|
261
400
|
/** Boolean form of ffMainToOrWhy for callers that need no reason. */
|
|
262
401
|
export async function ffMainTo(root, ref, mainBranch) {
|
|
@@ -3,17 +3,18 @@ import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
|
3
3
|
import { errorMessage } from "../text/text.js";
|
|
4
4
|
import { deleteRef } from "../git/git.js";
|
|
5
5
|
import { removeMergeChange, writeMergeOutcome } from "./merge-slot.js";
|
|
6
|
+
import { requeueKeptChange } from "./merge-requeue.js";
|
|
6
7
|
import { mergeRefName, rejectedRefName } from "../paths.js";
|
|
7
8
|
/** A fresh pipeline: nothing in flight anywhere — no verification running, no approved change
|
|
8
9
|
* waiting, no merge holding main. The orchestrator builds one per team start (orchestrator.ts). */
|
|
9
10
|
export function newMergePipeline() {
|
|
10
|
-
return { verifying: new Map(), approved: new Map(), merge: null };
|
|
11
|
+
return { verifying: new Map(), approved: new Map(), merge: null, revert: null, postReview: null };
|
|
11
12
|
}
|
|
12
13
|
/** Every merge task in flight — each verification holding its permit, and the merge — for the
|
|
13
14
|
* shutdown and restart waits. A parked verification is not one of them: it has started nothing, and
|
|
14
15
|
* a shutdown (its controller) or a closed start gate (at its grant) settles it without a run. */
|
|
15
16
|
export function mergeTasks(p) {
|
|
16
|
-
return [...[...p.verifying.values()].filter((v) => !v.parked), ...(p.merge ? [p.merge] : [])];
|
|
17
|
+
return [...[...p.verifying.values()].filter((v) => !v.parked), ...(p.merge ? [p.merge] : []), ...(p.postReview && !p.postReview.parked ? [p.postReview] : [])];
|
|
17
18
|
}
|
|
18
19
|
/** Everything `tumwater abort --role` can reach: every task in flight, plus each approved change
|
|
19
20
|
* waiting with no task of its own. A parked verification is out of reach, as a plainly queued entry
|
|
@@ -85,7 +86,7 @@ export async function discardPinnedRefs(root, roles) {
|
|
|
85
86
|
* write-back, so no settled entry can keep a stale marker or lose its outcome write. The pinned-ref
|
|
86
87
|
* discard stays with the callers: only a user abort throws work away, and the merge discards its
|
|
87
88
|
* whole stack at once rather than change by change. */
|
|
88
|
-
export async function settleMergeOutcome(root, entry, state, result, durationMs, usage, file) {
|
|
89
|
+
export async function settleMergeOutcome(root, entry, state, result, durationMs, usage, file, mainBranch) {
|
|
89
90
|
writeMergeOutcome(root, entry, state, result, durationMs, usage, file);
|
|
90
91
|
// A revision that merges ends the rejected change's life: drop the rejected ref (plans/revise-
|
|
91
92
|
// rejected.md). A rejection repoints or deletes it in merge-core.ts, so this is the only
|
|
@@ -96,4 +97,15 @@ export async function settleMergeOutcome(root, entry, state, result, durationMs,
|
|
|
96
97
|
await deleteRef(root, rejectedRefName(entry.role));
|
|
97
98
|
}
|
|
98
99
|
removeMergeChange(root, entry.role);
|
|
100
|
+
// A kept change goes straight back on the queue (the callers pass the main branch; a
|
|
101
|
+
// direct caller without one leaves the retry to the author's leftover recovery). The write-back
|
|
102
|
+
// above already happened, so a failure here must not lose the settled outcome.
|
|
103
|
+
if (mainBranch !== undefined) {
|
|
104
|
+
try {
|
|
105
|
+
await requeueKeptChange(root, mainBranch, entry, state, result);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
warnEventBestEffort(root, entry.role, `could not re-queue the kept change: ${errorMessage(err)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
99
111
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { revertSkipReason } from "../baseline/main-revert.js";
|
|
2
|
+
import { reviewRunConfig, seamConcurrencyKey } from "../config/config-views.js";
|
|
3
|
+
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
4
|
+
import { POST_REVIEW_FAILURE_LIMIT, readPostReviews, reviewMergedCommit, settlePostReview, } from "../review/post-merge-review.js";
|
|
5
|
+
import { shortSha } from "../text/format.js";
|
|
6
|
+
import { errorMessage } from "../text/text.js";
|
|
7
|
+
import { abortOnShutdown, acquireUnlessAborted, guardMergeTask } from "./merge-pipeline.js";
|
|
8
|
+
import { resolveAuthor, VERIFY_TIER } from "./merge-verify.js";
|
|
9
|
+
/** Pipelines with a revert skip check in flight, so a poll does not start a second one. */
|
|
10
|
+
const skipChecks = new WeakSet();
|
|
11
|
+
/** One poll of the post-merge reviews (review.afterMerge): a reviewed-and-rejected commit goes to
|
|
12
|
+
* the revert slot when it and the merge slot are free, and the next unreviewed commit starts its
|
|
13
|
+
* review when a permit is free right now. One review at a time; starts a task and returns. */
|
|
14
|
+
export function drainPostReviews(ctx, p) {
|
|
15
|
+
const pending = readPostReviews(ctx.root);
|
|
16
|
+
if (pending.length === 0)
|
|
17
|
+
return;
|
|
18
|
+
const rejected = pending.find((e) => e.rejected !== undefined);
|
|
19
|
+
// The entry stays in the file until its revert has run (startRevert settles it), so a shutdown,
|
|
20
|
+
// or the main-red watch replacing the slot, only delays the revert to a later poll.
|
|
21
|
+
if (rejected && p.revert === null && p.merge === null && !skipChecks.has(p)) {
|
|
22
|
+
// Check the skip reason before filling the slot: drainMerge starts the revert in the same poll.
|
|
23
|
+
skipChecks.add(p);
|
|
24
|
+
const culprit = { commit: rejected.commit, runner: rejected.role, failingTests: [], tip: rejected.commit, postReview: { reasons: rejected.rejected } };
|
|
25
|
+
void revertSkipReason(ctx.root, ctx.mainBranch, culprit)
|
|
26
|
+
.then((skip) => {
|
|
27
|
+
if (skip !== null) {
|
|
28
|
+
settlePostReview(ctx.root, rejected.commit, null);
|
|
29
|
+
warnEventBestEffort(ctx.root, rejected.role, `not reverting ${shortSha(rejected.commit)} after its post-merge rejection: ${skip}`);
|
|
30
|
+
}
|
|
31
|
+
else if (p.revert === null && p.merge === null)
|
|
32
|
+
p.revert = culprit;
|
|
33
|
+
})
|
|
34
|
+
.finally(() => skipChecks.delete(p));
|
|
35
|
+
}
|
|
36
|
+
if (p.postReview !== null || ctx.startHeld())
|
|
37
|
+
return;
|
|
38
|
+
const next = pending.find((e) => e.rejected === undefined);
|
|
39
|
+
if (next)
|
|
40
|
+
p.postReview = startPostReview(ctx, p, next);
|
|
41
|
+
}
|
|
42
|
+
function startPostReview(ctx, p, entry) {
|
|
43
|
+
const { root, semaphore } = ctx;
|
|
44
|
+
const task = { promise: Promise.resolve(), controller: new AbortController(), roles: [], userAborted: false, parked: true };
|
|
45
|
+
const detach = abortOnShutdown(ctx.signal, task.controller);
|
|
46
|
+
const author = resolveAuthor(ctx, entry.role);
|
|
47
|
+
const key = seamConcurrencyKey(reviewRunConfig(ctx.liveConfig), ctx.piDefault);
|
|
48
|
+
const run = (async () => {
|
|
49
|
+
try {
|
|
50
|
+
if (!semaphore.canLeaseAtOnce(VERIFY_TIER, key))
|
|
51
|
+
return; // permits are busy; try again next poll
|
|
52
|
+
const lease = await acquireUnlessAborted(semaphore, VERIFY_TIER, task.controller.signal, key);
|
|
53
|
+
task.parked = false;
|
|
54
|
+
if (lease === null)
|
|
55
|
+
return;
|
|
56
|
+
let outcome;
|
|
57
|
+
try {
|
|
58
|
+
outcome = await reviewMergedCommit(root, ctx.liveConfig, entry, (opts) => author.runGatePi(opts), task.controller.signal);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
lease.release();
|
|
62
|
+
}
|
|
63
|
+
if (task.controller.signal.aborted)
|
|
64
|
+
return; // a shutdown: the entry stays for the next generation
|
|
65
|
+
if (outcome.kind === "approve")
|
|
66
|
+
settlePostReview(root, entry.commit, null);
|
|
67
|
+
else if (outcome.kind === "reject")
|
|
68
|
+
settlePostReview(root, entry.commit, { ...entry, rejected: outcome.reasons });
|
|
69
|
+
else {
|
|
70
|
+
const failures = (entry.failures ?? 0) + 1;
|
|
71
|
+
warnEventBestEffort(root, entry.role, `the post-merge review of ${shortSha(entry.commit)} failed (${outcome.detail})`);
|
|
72
|
+
if (failures >= POST_REVIEW_FAILURE_LIMIT) {
|
|
73
|
+
warnEventBestEffort(root, entry.role, `dropping the post-merge review of ${shortSha(entry.commit)} after ${failures} failed runs; it stays on main unreviewed`);
|
|
74
|
+
settlePostReview(root, entry.commit, null);
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
settlePostReview(root, entry.commit, { ...entry, failures });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `the post-merge review of ${shortSha(entry.commit)} could not run: ${errorMessage(err)}`);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
detach();
|
|
85
|
+
p.postReview = null;
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
task.promise = guardMergeTask(root, `post-merge review of ${shortSha(entry.commit)}`, run);
|
|
89
|
+
return task;
|
|
90
|
+
}
|
|
@@ -1,16 +1,15 @@
|
|
|
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";
|
|
10
9
|
import { plural } from "../text/phrases.js";
|
|
11
10
|
import { mergeChanges, MERGE_STAGE_LABELS } from "./merge-slot.js";
|
|
12
|
-
/** The bracketed-label text for one queue entry, in the wording the
|
|
13
|
-
*
|
|
11
|
+
/** The bracketed-label text for one queue entry, in the wording the role rows use (status-model's
|
|
12
|
+
* rolePhase): `merge <elapsed> · <stage>` while a verify or the merge holds the change,
|
|
14
13
|
* `approved, awaiting merge` once approved, and "" when the marker is absent, names no record for
|
|
15
14
|
* `sha`, or the record is `done`. Reads the marker through mergeChanges, so an older-generation
|
|
16
15
|
* single-change marker counts as one `merge` record. */
|
|
@@ -44,28 +43,67 @@ export function revisionNote(entry) {
|
|
|
44
43
|
note += " · " + plural(reasons.length, "prior objection");
|
|
45
44
|
return note;
|
|
46
45
|
}
|
|
46
|
+
/** One re-queued entry's what-brought-it-back note:
|
|
47
|
+
* `re-queued after <result>`, plus ` · eligible in <duration>` while a `review_error` 60 s
|
|
48
|
+
* deferral still holds (its `notBefore` in the future). "" for a fresh entry with no `retry`
|
|
49
|
+
* field, so a first submission's queue line is byte-identical. */
|
|
50
|
+
export function retryNote(entry, now) {
|
|
51
|
+
if (entry.retry === undefined)
|
|
52
|
+
return "";
|
|
53
|
+
let note = `re-queued after ${entry.retry}`;
|
|
54
|
+
if (entry.notBefore !== undefined && entry.notBefore > now) {
|
|
55
|
+
note += ` · eligible in ${humanSeconds(secondsUntil(entry.notBefore, now))}`;
|
|
56
|
+
}
|
|
57
|
+
return note;
|
|
58
|
+
}
|
|
59
|
+
/** Narrows a whole-queue view to the entries whose role `keep` accepts, and returns the
|
|
60
|
+
* `scope` renderMergeQueue takes: the kept roles, each kept entry's 1-based place in the whole
|
|
61
|
+
* queue, and the whole queue's entry count. `unreadable` stays whole because an unreadable file
|
|
62
|
+
* has no role to attribute it to. */
|
|
63
|
+
export function scopeMergeQueue(whole, roles, keep) {
|
|
64
|
+
const positions = [];
|
|
65
|
+
const entries = whole.entries.filter((e, i) => {
|
|
66
|
+
const kept = keep(e.role);
|
|
67
|
+
if (kept)
|
|
68
|
+
positions.push(i + 1);
|
|
69
|
+
return kept;
|
|
70
|
+
});
|
|
71
|
+
return { view: { ...whole, entries }, scope: { roles, positions, total: whole.entries.length } };
|
|
72
|
+
}
|
|
47
73
|
/** 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
|
|
74
|
+
* one 1-based line per readable entry, oldest first, carrying the role, run number, short
|
|
49
75
|
* pinned sha, age, and summary. An empty queue (no entries and nothing skipped) prints
|
|
50
76
|
* `merge queue empty`; a non-zero skip count appends one `(+N unreadable queue file(s) skipped)`
|
|
51
77
|
* line so a torn or foreign file the reader dropped is visible rather than silent. An entry the
|
|
52
78
|
* optional in-flight marker has a live record for carries ` · [<label>]` after its age
|
|
53
|
-
* (queueEntryMergeLabel).
|
|
54
|
-
|
|
79
|
+
* (queueEntryMergeLabel). With the optional `scope` (`queue --role`: `view` holds only the
|
|
80
|
+
* listed roles' entries, `positions` their 1-based places in the whole queue, `total` the whole
|
|
81
|
+
* queue's entry count) each line keeps its whole-queue number, the header reads
|
|
82
|
+
* `merge queue (<kept> of <whole depth>, --role <ids>):`, and a scope that keeps nothing of a
|
|
83
|
+
* non-empty queue prints `no queued merges for <ids> (merge queue holds <depth>)`. Without
|
|
84
|
+
* `scope` the output is the whole queue's. */
|
|
85
|
+
export function renderMergeQueue(view, now, inFlight, scope) {
|
|
55
86
|
const depth = view.entries.length + view.unreadable;
|
|
87
|
+
const wholeDepth = scope ? scope.total + view.unreadable : depth;
|
|
88
|
+
const ids = scope ? scope.roles.join(",") : "";
|
|
56
89
|
const lines = [];
|
|
57
|
-
if (
|
|
90
|
+
if (wholeDepth === 0) {
|
|
58
91
|
lines.push("merge queue empty");
|
|
59
92
|
}
|
|
93
|
+
else if (scope && view.entries.length === 0) {
|
|
94
|
+
lines.push(`no queued merges for ${ids} (merge queue holds ${wholeDepth})`);
|
|
95
|
+
}
|
|
60
96
|
else {
|
|
61
|
-
lines.push(`merge queue (${depth}):`);
|
|
97
|
+
lines.push(scope ? `merge queue (${view.entries.length} of ${wholeDepth}, --role ${ids}):` : `merge queue (${depth}):`);
|
|
62
98
|
view.entries.forEach((e, i) => {
|
|
63
|
-
const age =
|
|
99
|
+
const age = humanAgo(e.enqueuedAt, now);
|
|
64
100
|
const label = queueEntryMergeLabel(e.sha, inFlight, now);
|
|
65
101
|
const tag = label ? ` · [${label}]` : "";
|
|
66
102
|
const note = revisionNote(e);
|
|
67
103
|
const noteTag = note ? ` · ${note}` : "";
|
|
68
|
-
|
|
104
|
+
const retry = retryNote(e, now);
|
|
105
|
+
const retryTag = retry ? ` · ${retry}` : "";
|
|
106
|
+
lines.push(` ${scope ? scope.positions[i] : i + 1}. ${roleLabel(e.role)} run #${e.run} · ${shortSha(e.sha)} · ${age}${tag}${retryTag}${noteTag} · ${e.summary}`);
|
|
69
107
|
});
|
|
70
108
|
}
|
|
71
109
|
if (view.unreadable > 0) {
|