tumwater 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +55 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +6 -4
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +404 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +281 -55
- package/dist/src/backlog/backlog-cli-view.js +255 -0
- package/dist/src/backlog/backlog-cli.js +91 -335
- package/dist/src/backlog/backlog-commit.js +20 -13
- package/dist/src/backlog/backlog-eligibility.js +193 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +264 -0
- package/dist/src/backlog/backlog-release.js +134 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-guardrails.js +136 -0
- package/dist/src/baseline/main-red.js +153 -36
- package/dist/src/baseline/main-watch.js +232 -0
- package/dist/src/brief/brief-edit.js +12 -12
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +31 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +65 -3
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +181 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +183 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +291 -598
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +525 -0
- package/dist/src/cli/cli-start-summary.js +167 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
- package/dist/src/cli/completion.js +169 -22
- package/dist/src/cli/config-commands.js +58 -26
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +258 -82
- package/dist/src/cli/log-commands.js +164 -85
- package/dist/src/cli/question-commands.js +211 -10
- package/dist/src/cli.js +173 -45
- package/dist/src/collections.js +56 -13
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +16 -1
- package/dist/src/config/config-fallbacks.js +94 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +169 -222
- package/dist/src/config/config-views.js +136 -54
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +164 -265
- package/dist/src/config/config.js +53 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
- package/dist/src/doctor/doctor-backlog-plans.js +398 -0
- package/dist/src/doctor/doctor-backlog.js +249 -512
- package/dist/src/doctor/doctor-check-names.js +85 -0
- package/dist/src/doctor/doctor-checks.js +61 -593
- package/dist/src/doctor/doctor-claude-checks.js +142 -0
- package/dist/src/doctor/doctor-codex-checks.js +276 -0
- package/dist/src/doctor/doctor-contention-checks.js +506 -0
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +80 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +216 -537
- package/dist/src/doctor/doctor-notify.js +9 -5
- package/dist/src/doctor/doctor-opencode-checks.js +164 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +397 -0
- package/dist/src/doctor/doctor.js +126 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +21 -12
- package/dist/src/events/event-format.js +185 -89
- package/dist/src/events/event-labels.js +11 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +52 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +80 -26
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +65 -60
- package/dist/src/failure/failure-render.js +29 -21
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +35 -35
- package/dist/src/files/file-queue.js +17 -0
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +88 -225
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +101 -143
- package/dist/src/gates/maintenance-quota.js +37 -30
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +89 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +54 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +43 -19
- package/dist/src/gui/gui-backlog-endpoints.js +156 -0
- package/dist/src/gui/gui-command.js +26 -13
- package/dist/src/gui/gui-config-commands.js +168 -80
- package/dist/src/gui/gui-endpoint-commands.js +5 -4
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +235 -13
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +97 -0
- package/dist/src/gui/gui-server.js +26 -5
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +155 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +190 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +28 -17
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +58 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +45 -19
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +214 -0
- package/dist/src/merge/merge-queue-render.js +48 -11
- package/dist/src/merge/merge-queue.js +52 -15
- package/dist/src/merge/merge-requeue.js +103 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +27 -11
- package/dist/src/merge/merge-to-main.js +77 -26
- package/dist/src/merge/merge-top.js +83 -0
- package/dist/src/merge/merge-verify.js +75 -18
- package/dist/src/notes-cli.js +35 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +352 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
- package/dist/src/orchestrator/orchestrator.js +129 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +245 -0
- package/dist/src/orchestrator/scheduling-skip.js +195 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +126 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +26 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +150 -217
- package/dist/src/prompt/notes-backlog.js +184 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +131 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +188 -124
- package/dist/src/report/report-fold.js +130 -54
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +136 -65
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +23 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +112 -1
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +239 -0
- package/dist/src/review/protected-edits.js +24 -10
- package/dist/src/review/quarantine-render.js +65 -0
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +180 -401
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +127 -12
- package/dist/src/roles/role-guidance.js +65 -20
- package/dist/src/roles/role-labels.js +7 -5
- package/dist/src/roles/role-render.js +108 -56
- package/dist/src/roles/role-view.js +95 -56
- package/dist/src/roles/roles.js +33 -33
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/run/qa-coverage.js +125 -0
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +175 -0
- package/dist/src/run/run-detail-render.js +91 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +178 -0
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +231 -0
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +216 -0
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +191 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +109 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +103 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claim-entries.js +49 -0
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +49 -17
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +43 -28
- package/dist/src/status/status-roles.js +50 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +7 -8
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +65 -18
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +60 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +78 -13
- package/dist/src/ui/badges.js +99 -38
- package/dist/src/ui/gui/gui-client-backlog.js +314 -0
- package/dist/src/ui/gui/gui-client-boot.js +89 -61
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
- package/dist/src/ui/gui/gui-client-drawer.js +155 -289
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +437 -68
- package/dist/src/ui/gui/gui-client-labels.js +6 -3
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +60 -52
- package/dist/src/ui/gui/gui-client-pending.js +405 -24
- package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
- package/dist/src/ui/gui/gui-client-report.js +105 -430
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +163 -5
- package/dist/src/ui/gui/gui-client-team.js +36 -465
- package/dist/src/ui/gui/gui-client.js +38 -26
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +10 -7
- package/dist/src/ui/gui/gui-styles.js +38 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +364 -49
- package/dist/src/ui/status-render.js +109 -82
- package/dist/src/ui/team-alerts.js +728 -62
- package/dist/src/ui/tone.js +12 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +53 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +4 -12
- package/dist/src/ui/tui/tui-editor-backends.js +127 -0
- package/dist/src/ui/tui/tui-editor-brief.js +14 -36
- package/dist/src/ui/tui/tui-editor-budget.js +36 -39
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
- package/dist/src/ui/tui/tui-editor-config.js +99 -0
- package/dist/src/ui/tui/tui-editor-entry.js +39 -64
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +46 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
- package/dist/src/ui/tui/tui-editor-interval.js +29 -16
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +9 -29
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
- package/dist/src/ui/tui/tui-editor-retire.js +5 -13
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +101 -34
- package/dist/src/ui/tui/tui-editor-slots.js +24 -0
- package/dist/src/ui/tui/tui-editors.js +44 -2
- package/dist/src/ui/tui/tui-frame.js +68 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +33 -9
- package/dist/src/ui/tui/tui-pane-activity.js +35 -19
- package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +168 -8
- package/dist/src/ui/tui/tui-pane-history.js +446 -82
- package/dist/src/ui/tui/tui-pane-pending.js +539 -15
- package/dist/src/ui/tui/tui-pane-queued.js +78 -57
- package/dist/src/ui/tui/tui-pane-report.js +54 -16
- package/dist/src/ui/tui/tui-pane-session.js +34 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +91 -28
- package/dist/src/ui/tui/tui-panes.js +153 -34
- package/dist/src/ui/tui/tui.js +18 -15
- package/dist/src/verdict/fix-claim.js +129 -33
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +77 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/leftover.js +0 -130
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/qa-coverage.js +0 -108
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import { openQuestions } from "../backlog/backlog.js";
|
|
3
2
|
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
4
3
|
import { changeBaseRev, commitMessage, headOf } from "../git/git.js";
|
|
5
4
|
import { aheadOfMainFiles, treeDiffFiles } from "../git/git-diff.js";
|
|
6
5
|
import { resolvedDiffDiverges } from "./merge-diff.js";
|
|
7
|
-
import { conflictedFileHunks, conflictedFiles, continueRebase, ffMainToOrWhy, hasConflictMarkers, mainCommitsTouching, rebaseOntoMain, rebaseOntoMainLeaveConflicts, } from "./merge-git.js";
|
|
6
|
+
import { conflictedFileHunks, conflictedFiles, continueRebase, ffMainToOrWhy, overwriteBlockedReason, primaryCheckoutBlockers, hasConflictMarkers, mainCommitsTouching, rebaseOntoMain, rebaseOntoMainLeaveConflicts, } from "./merge-git.js";
|
|
8
7
|
import { resolveBacklogInsertConflicts } from "./backlog-conflicts.js";
|
|
9
8
|
import { abortSync } from "../git/worktree.js";
|
|
10
9
|
import { runScopedBuildCheck } from "../build/build-check-scoped.js";
|
|
11
|
-
import { detectBuildCheck, gateCommandOf } from "../build/build-check-detect.js";
|
|
10
|
+
import { detectBuildCheck, gateCommandOf, mergeCommandOf } from "../build/build-check-detect.js";
|
|
11
|
+
import { onlyKnownMainFailures } from "../baseline/main-watch.js";
|
|
12
|
+
import { unverifiedTreeOutcome } from "../build/build-check-events.js";
|
|
13
|
+
import { describeCheck, failureHeadline } from "../build/build-check-report.js";
|
|
14
|
+
import { MERGE_FLAKY_WARNING_PREFIX, quarantineTests } from "../review/known-flakes.js";
|
|
12
15
|
import { noteGreenBaseline, noteInheritedGreen } from "../baseline/main-baseline.js";
|
|
13
16
|
import { isExemptDiff } from "../review/exemptions.js";
|
|
14
17
|
import { falseFixReason } from "../verdict/fix-claim.js";
|
|
@@ -24,7 +27,7 @@ import { resolverConfig } from "../config/config-views.js";
|
|
|
24
27
|
* history linear), verify the rebased tree when it differs from what was reviewed, and
|
|
25
28
|
* fast-forward. On conflict, makes one pi-driven resolution attempt
|
|
26
29
|
* (outside the lock) before giving up. A routine conflict is normal operation, not a warning:
|
|
27
|
-
* success merges as an ordinary `merged` event and failure surfaces via the
|
|
30
|
+
* success merges as an ordinary `merged` event and failure surfaces via the run's merge_conflict
|
|
28
31
|
* result — no separate log line for the hand-off itself. A merged diff that adds entries under
|
|
29
32
|
* QUESTIONS.md's ## Open also emits one `question_posted` per new heading alongside the `merged`
|
|
30
33
|
* event, so `tumwater logs` shows what the team is asking for (plans/questions-outbox.md).
|
|
@@ -80,6 +83,11 @@ async function tryMerge(ctx, wt, summary, verifiedHead) {
|
|
|
80
83
|
// main moved between commit and merge, which under concurrency is the common case (review
|
|
81
84
|
// runs outside this lock).
|
|
82
85
|
const target = await headOf(wt, "HEAD");
|
|
86
|
+
const dirty = await primaryCheckoutBlockers(ctx.root, ctx.mainBranch, target);
|
|
87
|
+
if (dirty !== null) {
|
|
88
|
+
warnEventBestEffort(ctx.root, ctx.role, `merge blocked: ${overwriteBlockedReason(dirty)}`);
|
|
89
|
+
return "merge_blocked";
|
|
90
|
+
}
|
|
83
91
|
if (!(await verifyMerge(ctx, wt, target, verifiedHead)))
|
|
84
92
|
return "merge_blocked";
|
|
85
93
|
const mainBefore = await headOf(ctx.root, ctx.mainBranch);
|
|
@@ -98,14 +106,20 @@ async function tryMerge(ctx, wt, summary, verifiedHead) {
|
|
|
98
106
|
// Best-effort: main has already fast-forwarded, so an unwritable events feed must not turn
|
|
99
107
|
// a completed merge into a rejected one and skip syncRootInstall below — the same
|
|
100
108
|
// no-torn-feed policy as the orchestrator's other post-effect logging.
|
|
101
|
-
if (commit !== mainBefore)
|
|
102
|
-
logEventBestEffort(ctx.root, {
|
|
109
|
+
if (commit !== mainBefore) {
|
|
110
|
+
logEventBestEffort(ctx.root, {
|
|
111
|
+
source: ctx.role,
|
|
112
|
+
type: "merged",
|
|
113
|
+
commit,
|
|
114
|
+
summary,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
103
117
|
logNewQuestions(ctx.root, before, ctx.role);
|
|
104
118
|
// Still under the lock: a merge that moved main's lockfile re-syncs the root install
|
|
105
119
|
// every worktree resolves through before the next merge's check runs (BUGS.md 2026-10-01).
|
|
106
120
|
await syncRootInstall(ctx.root, ctx.role);
|
|
107
121
|
return "changed";
|
|
108
|
-
});
|
|
122
|
+
}, ctx.lockTimeoutMs);
|
|
109
123
|
}
|
|
110
124
|
/** The backlog-structure cross-check both verifyMerge paths run (exempt and code diffs
|
|
111
125
|
* alike): when the tree ahead of main duplicates or drops a `## ` section heading, warn on the
|
|
@@ -146,12 +160,16 @@ export async function exemptSkipBlockReason(ctx, wt, files) {
|
|
|
146
160
|
* - a no-op rebase whose exact tree the gate verified (`rebasedHead === verifiedHead`): main
|
|
147
161
|
* did not move under us, so the merge tree is byte-identical to what the gate's pre-check
|
|
148
162
|
* already ran green. Seed the red-main baseline with the SHA that becomes main: every role's
|
|
149
|
-
* next fresh
|
|
163
|
+
* next fresh run then hits the cache instead of re-running the full suite on an
|
|
150
164
|
* already-verified tree. A no-op rebase the gate did NOT verify — its pre-check was skipped
|
|
151
165
|
* (a timeout, a killed run) — falls through and runs the merge-scope check, which fails
|
|
152
166
|
* closed on the same timeout; an exempt diff or a project with no declared check falls
|
|
153
|
-
* through to its own arm below.
|
|
154
|
-
*
|
|
167
|
+
* through to its own arm below. Taken (Fast merge "Defaults") whenever a `check.mergeCommand`
|
|
168
|
+
* resolves — to its own value or, when absent, to `check.gateCommand`. In the fallback case
|
|
169
|
+
* (no separate `mergeCommand`) the in-lock command IS the gate command the gate already ran
|
|
170
|
+
* green on the rebased head, so the in-lock run would add nothing; with a separate
|
|
171
|
+
* `check.mergeCommand` the gate's green was the gate command, not the merge one. Either way
|
|
172
|
+
* it skips and seeds nothing (the main watch decides main's verdict).
|
|
155
173
|
* - doc-only delta ahead of main (the gate's own exemption test): cannot break the build.
|
|
156
174
|
* - a code change whose rebased tree differs from the gate-verified one only in exempt paths
|
|
157
175
|
* (onlyExemptDriftFromVerified): main moved under the review by doc commits alone, so the
|
|
@@ -168,10 +186,15 @@ async function verifyMerge(ctx, wt, rebasedHead, verifiedHead) {
|
|
|
168
186
|
// re-review sets verifiedHead to the resolved head, and that second rebase is then a no-op).
|
|
169
187
|
// A no-op rebase the gate never verified — its pre-check timed out or was killed and left
|
|
170
188
|
// GateResult.verifiedHead undefined — falls through and runs the merge-scope check, which
|
|
171
|
-
// fails closed on the same timeout. With a check.gateCommand
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
189
|
+
// fails closed on the same timeout. With a check.gateCommand or check.mergeCommand
|
|
190
|
+
// configured (Fast merge "Defaults": the lock runs the gate command when mergeCommand is
|
|
191
|
+
// absent): in the fallback case (no separate `mergeCommand`) the in-lock command IS the gate
|
|
192
|
+
// command the gate already ran green on the rebased head, so the verified no-op rebase adds
|
|
193
|
+
// nothing — skip it; with a separate `mergeCommand` the gate's green was the gate command.
|
|
194
|
+
// Either way it seeds nothing — a cheaper command's green never seeds the baseline: main's
|
|
195
|
+
// verdicts come from the main watch's full check on the tip.
|
|
196
|
+
if (rebasedHead === verifiedHead && mergeCommandOf(ctx.config) !== undefined)
|
|
197
|
+
return true;
|
|
175
198
|
if (rebasedHead === verifiedHead && gateCommandOf(ctx.config) === undefined) {
|
|
176
199
|
noteGreenBaseline(rebasedHead);
|
|
177
200
|
return true;
|
|
@@ -204,19 +227,40 @@ async function verifyMerge(ctx, wt, rebasedHead, verifiedHead) {
|
|
|
204
227
|
// runScopedBuildCheck, shared with the review gate's pre-check. The merge cell names the
|
|
205
228
|
// check (or its wait for a permit) while it runs, then the merge again for the ff.
|
|
206
229
|
setMergeStage(ctx.root, ctx.role, "build-check");
|
|
207
|
-
const
|
|
230
|
+
const runMergeCheck = () => runScopedBuildCheck(ctx.root, ctx.role, "merge", wt, ctx.config, undefined, undefined, undefined, checkWaitStage(ctx.root, [ctx.role]));
|
|
231
|
+
let check = await runMergeCheck();
|
|
232
|
+
if (check?.outcome.status === "failed" && !unverifiedTreeOutcome(check.outcome) && !onlyKnownMainFailures(ctx.root, check.outcome.failingTests)) {
|
|
233
|
+
// One immediate re-run of the same tree: a pass makes the failed run a flake. The change
|
|
234
|
+
// merges and each test the failed run named is quarantined.
|
|
235
|
+
const failed = check.outcome;
|
|
236
|
+
const retry = await runMergeCheck();
|
|
237
|
+
if (retry?.outcome.status === "passed") {
|
|
238
|
+
warnEventBestEffort(ctx.root, ctx.role, `${MERGE_FLAKY_WARNING_PREFIX}${failureHeadline(failed.outputTail) ?? describeCheck(check.check)}`);
|
|
239
|
+
quarantineTests(ctx.root, failed.failingTests, failed.outputTail);
|
|
240
|
+
check = retry;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
208
243
|
setMergeStage(ctx.root, ctx.role, "merging");
|
|
209
244
|
if (!check)
|
|
210
245
|
return true; // No declared check: nothing to run, exactly like the gate skipping its pre-check.
|
|
211
246
|
if (check.outcome.status === "failed") {
|
|
247
|
+
// Failing only what main already fails is not a red change: the fast-forward goes ahead, with
|
|
248
|
+
// no strike and no green seeded for a tree that did not pass.
|
|
249
|
+
const known = unverifiedTreeOutcome(check.outcome) ? null : onlyKnownMainFailures(ctx.root, check.outcome.failingTests);
|
|
250
|
+
if (known) {
|
|
251
|
+
warnEventBestEffort(ctx.root, ctx.role, `merge check fails only main's known failures: ${known.join(", ")}`);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
212
254
|
ctx.onMergeCheckRed?.(check.check, check.outcome);
|
|
213
255
|
return false;
|
|
214
256
|
}
|
|
215
257
|
if (check.outcome.status === "skipped")
|
|
216
258
|
return true; // No npm / broken toolchain — the helper already warned.
|
|
217
|
-
// Green on exactly the tree that becomes main: seed it so the next
|
|
259
|
+
// Green on exactly the tree that becomes main: seed it so the next run's red-main baseline
|
|
218
260
|
// check is a cache hit instead of one redundant full-suite run.
|
|
219
|
-
|
|
261
|
+
// A mergeCommand green is not the full check's: main's verdict comes from the main watch.
|
|
262
|
+
if (mergeCommandOf(ctx.config) === undefined)
|
|
263
|
+
noteGreenBaseline(rebasedHead);
|
|
220
264
|
return true;
|
|
221
265
|
}
|
|
222
266
|
/** True, after seeding the red-main baseline, when the tree about to merge differs from the tree
|
|
@@ -229,14 +273,20 @@ async function verifyMerge(ctx, wt, rebasedHead, verifiedHead) {
|
|
|
229
273
|
* that only rewrote SHAs) is the verified bytes themselves and seeds an observed green. Any
|
|
230
274
|
* other drift seeds an inherited one, which attribution re-checks before trusting
|
|
231
275
|
* (main-baseline.ts's observedOnly). Not taken without a verified head (the pre-check was skipped
|
|
232
|
-
* or never ran)
|
|
276
|
+
* or never ran). With an in-lock command configured (a gate or merge command — Fast merge
|
|
277
|
+
* "Defaults": the lock runs the gate command when mergeCommand is absent) the drift merges
|
|
278
|
+
* without a seed: the main watch decides main's verdict.
|
|
233
279
|
* A git failure means "not proven" and the check runs. */
|
|
234
280
|
async function onlyExemptDriftFromVerified(ctx, wt, rebasedHead, verifiedHead) {
|
|
235
|
-
if (verifiedHead === undefined
|
|
281
|
+
if (verifiedHead === undefined)
|
|
282
|
+
return false;
|
|
283
|
+
if (gateCommandOf(ctx.config) !== undefined && mergeCommandOf(ctx.config) === undefined)
|
|
236
284
|
return false;
|
|
237
285
|
const drift = await treeDiffFiles(wt, verifiedHead, rebasedHead);
|
|
238
286
|
if (drift === null || !isExemptDiff(drift, ctx.exemptPaths))
|
|
239
287
|
return false;
|
|
288
|
+
if (mergeCommandOf(ctx.config) !== undefined)
|
|
289
|
+
return true; // no seed: the watch decides main's verdict
|
|
240
290
|
if (drift.length === 0)
|
|
241
291
|
noteGreenBaseline(rebasedHead);
|
|
242
292
|
else
|
|
@@ -269,12 +319,13 @@ async function resolveConflict(ctx, wt, preMergeHead) {
|
|
|
269
319
|
const files = await conflictedFiles(wt);
|
|
270
320
|
// Deterministic first pass (plans/parallel-work-instances.md, part 3/7): insert-only
|
|
271
321
|
// conflicts in the backlog markdown — two merges pasting different entries as the first
|
|
272
|
-
// under ## Done
|
|
273
|
-
//
|
|
322
|
+
// under ## Done, or new entries above an entry the other side deleted — carry no authored
|
|
323
|
+
// bytes, so they are settled without a model run. Only the files it could not resolve (code,
|
|
324
|
+
// or a same-entry edit) reach the resolver below.
|
|
274
325
|
const remaining = await resolveBacklogInsertConflicts(wt, files);
|
|
275
326
|
if (remaining.length === 0) {
|
|
276
327
|
// A second stop is only possible when the branch holds more than the one insert-only
|
|
277
|
-
// commit. One attempt per
|
|
328
|
+
// commit. One attempt per run.
|
|
278
329
|
return continueOrAbortRebase(wt);
|
|
279
330
|
}
|
|
280
331
|
// The prompt names the project's own check, detected the way the in-lock re-check detects it,
|
|
@@ -299,10 +350,10 @@ async function resolveConflict(ctx, wt, preMergeHead) {
|
|
|
299
350
|
change,
|
|
300
351
|
main: commits,
|
|
301
352
|
mainOmitted: omitted,
|
|
302
|
-
}), `tumwater-${ctx.role}-${ctx.
|
|
353
|
+
}), `tumwater-${ctx.role}-${ctx.runNumber}-conflict`,
|
|
303
354
|
// The resolver rides the strong tier (plans/model-tiers.md part 4/8): resolution is rare,
|
|
304
355
|
// tolerant of latency, and edits code inside merge. Its spend still folds into the
|
|
305
|
-
// authoring role's usage — the fold belongs to the
|
|
356
|
+
// authoring role's usage — the fold belongs to the runner's wiring, not to the config.
|
|
306
357
|
resolverConfig(ctx.config));
|
|
307
358
|
}
|
|
308
359
|
finally {
|
|
@@ -312,7 +363,7 @@ async function resolveConflict(ctx, wt, preMergeHead) {
|
|
|
312
363
|
await abortSync(wt);
|
|
313
364
|
return false;
|
|
314
365
|
}
|
|
315
|
-
// A second stop is only possible when pi itself authored extra commits during the
|
|
316
|
-
// One resolution attempt per
|
|
366
|
+
// A second stop is only possible when pi itself authored extra commits during the run.
|
|
367
|
+
// One resolution attempt per run.
|
|
317
368
|
return continueOrAbortRebase(wt);
|
|
318
369
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Operator reordering of a waiting merge (PLANS.md `tumwater queue --top <n>`): moving the
|
|
2
|
+
* Nth queued merge to the head of the execution order. Split out of merge-queue.ts so the
|
|
3
|
+
* queue stays the pure reader/writer it is: this module owns the reorder half of an
|
|
4
|
+
* unattempted entry, renaming its queue file to a stamp one below the queue's minimum — the
|
|
5
|
+
* merge-queue twin of inbox-top.ts (which does the same for the prompt queue). It mirrors
|
|
6
|
+
* inbox-top.ts's rename-and-event pairing and merge-cancel.ts's stance: no lock, one file per
|
|
7
|
+
* entry, tolerant of a concurrent drop. */
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import { logEventBestEffort } from "../events/events.js";
|
|
11
|
+
import { errCode } from "../errno.js";
|
|
12
|
+
import { queueFileStamp, lowestStampName } from "../files/file-queue.js";
|
|
13
|
+
import { queuedMergeFiles } from "./merge-queue.js";
|
|
14
|
+
/** Move the Nth queued merge — 1-based, as `tumwater queue` numbers it and `--cancel`
|
|
15
|
+
* resolves it — to the head of the execution order, so it is the next due entry the queue
|
|
16
|
+
* takes when ready. Position 1 reports `already-first` and changes nothing; an out-of-range
|
|
17
|
+
* position (below 1 or past the readable depth) reports `out-of-range` with the current
|
|
18
|
+
* depth and changes nothing. The Nth entry is resolved against `queuedMergeFiles` and the
|
|
19
|
+
* same `topQueuedFile` the by-sha twin shares, so the CLI and the surfaces cannot drift on
|
|
20
|
+
* what topping means. */
|
|
21
|
+
export function moveQueuedMergeToTop(root, position) {
|
|
22
|
+
const files = queuedMergeFiles(root);
|
|
23
|
+
if (position < 1 || position > files.length)
|
|
24
|
+
return { status: "out-of-range", depth: files.length };
|
|
25
|
+
return topQueuedFile(root, files, position - 1);
|
|
26
|
+
}
|
|
27
|
+
/** Move the post-position body of a top move to the head of the execution order: the same
|
|
28
|
+
* rename-and-event work both the position and by-sha variants share. `index` is a 0-based
|
|
29
|
+
* index into `files`. Position 1 (index 0) reports `already-first` and changes nothing. The
|
|
30
|
+
* move is one rename: the entry JSON is never rewritten, so every field (`enqueuedAt`,
|
|
31
|
+
* `notBefore`, `retry`, `revisionRound`, `priorReview`) is unchanged; giving the file a
|
|
32
|
+
* stamp one below the queue's readable minimum sorts it first (falling back to `Date.now()`
|
|
33
|
+
* when no stamp parses), stepping one lower while the destination exists (a concurrent
|
|
34
|
+
* writer may have just taken the name we computed). One `merge_topped` event is logged under
|
|
35
|
+
* the entry's role. The rename is guarded: if the target vanished between the list and the
|
|
36
|
+
* rename (a concurrent drain took the change), the move answers `gone` and changes nothing
|
|
37
|
+
* rather than throwing. No lock and no pin write: the module mirrors merge-cancel.ts's "one
|
|
38
|
+
* file per entry, tolerant of a concurrent drop" stance. */
|
|
39
|
+
function topQueuedFile(root, files, index) {
|
|
40
|
+
const target = files[index];
|
|
41
|
+
if (index === 0) {
|
|
42
|
+
return {
|
|
43
|
+
status: "already-first",
|
|
44
|
+
role: target.entry.role,
|
|
45
|
+
sha: target.entry.sha,
|
|
46
|
+
summary: target.entry.summary,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const stamps = files
|
|
50
|
+
.map((q) => queueFileStamp(path.basename(q.file)))
|
|
51
|
+
.filter((s) => s !== null);
|
|
52
|
+
const dir = path.dirname(target.file);
|
|
53
|
+
const newFile = lowestStampName(stamps, dir, ".json");
|
|
54
|
+
try {
|
|
55
|
+
fs.renameSync(target.file, newFile);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
// The queue drained the target between our list and this rename (an orchestrator took
|
|
59
|
+
// the change): a concurrent drop is a normal race, never an error — the entry has
|
|
60
|
+
// already left the queue, so there is nothing to top. Any other rename failure is real.
|
|
61
|
+
if (errCode(err) === "ENOENT")
|
|
62
|
+
return { status: "gone" };
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
logEventBestEffort(root, {
|
|
66
|
+
source: target.entry.role,
|
|
67
|
+
type: "merge_topped",
|
|
68
|
+
commit: target.entry.sha,
|
|
69
|
+
summary: target.entry.summary,
|
|
70
|
+
});
|
|
71
|
+
return { status: "topped", role: target.entry.role, sha: target.entry.sha, summary: target.entry.summary };
|
|
72
|
+
}
|
|
73
|
+
/** Move the queued merge whose entry's pinned sha is `sha` to the head of the queue (the
|
|
74
|
+
* browser twin of `tumwater queue --top <n>`, resolving by sha because the dashboard rows
|
|
75
|
+
* carry one). An entry whose sha matches none is `{ status: "gone" }` and changes nothing;
|
|
76
|
+
* otherwise it runs the same `topQueuedFile` the position variant uses. */
|
|
77
|
+
export function moveQueuedMergeToTopBySha(root, sha) {
|
|
78
|
+
const files = queuedMergeFiles(root);
|
|
79
|
+
const i = files.findIndex((q) => q.entry.sha.toLowerCase() === sha.toLowerCase());
|
|
80
|
+
if (i < 0)
|
|
81
|
+
return { status: "gone" };
|
|
82
|
+
return topQueuedFile(root, files, i);
|
|
83
|
+
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { gateRoleConfig } from "../gates/budget-gates.js";
|
|
5
4
|
import { reviewRunConfig, seamConcurrencyKey } from "../config/config-views.js";
|
|
6
5
|
import { providerWaitStage } from "./merge-slot.js";
|
|
7
6
|
import { withProviderLease } from "../concurrency/provider-lease.js";
|
|
8
|
-
import {
|
|
9
|
-
import { branchHead, isMergedInto } from "../git/git.js";
|
|
7
|
+
import { RoleRunner } from "../runner/runner.js";
|
|
8
|
+
import { branchHead, commitMessage, isMergedInto, refSha } from "../git/git.js";
|
|
9
|
+
import { commitRunOf } from "../git/commit-message.js";
|
|
10
|
+
import { recoveredMergeEntry, recoveredMetadata } from "../runner/leftover.js";
|
|
10
11
|
import { addMergeChange, mergeUsage, removeMergeChange, setMergeChangeStatus } from "./merge-slot.js";
|
|
11
|
-
import { dropMerge, entryRevisionFields, queuedMergeFiles, staleHeadFile } from "./merge-queue.js";
|
|
12
|
+
import { dropMerge, enqueueMerge, entryRevisionFields, queuedMergeFiles, staleHeadFile, tornHeadRole } from "./merge-queue.js";
|
|
13
|
+
import { mergeRefName } from "../paths.js";
|
|
12
14
|
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
13
15
|
import { errorMessage } from "../text/text.js";
|
|
14
16
|
import { verifyRequest } from "./merge-batch.js";
|
|
@@ -18,6 +20,13 @@ import { abortOnShutdown, acquireUnlessAborted, discardPinnedRefs, guardMergeTas
|
|
|
18
20
|
* behind them (BUGS.md 2026-09-18). Exported for merge-drain.ts, whose merge tier sits one
|
|
19
21
|
* below it. */
|
|
20
22
|
export const VERIFY_TIER = -1;
|
|
23
|
+
/** The tier the merge's conflict-resolution runs wait at, one below VERIFY_TIER: ahead of any
|
|
24
|
+
* verification parked for a permit, because the merge is the one serial step every queued change
|
|
25
|
+
* waits on. (Nothing that holds a permit ever waits on the merge: no verification takes the merge
|
|
26
|
+
* lock, and the resolver runs outside it — merge-to-main.ts's mergeToMain — so this cannot
|
|
27
|
+
* deadlock.) Shared by merge-drain.ts's merge slot and merge-post-review.ts's fix landing, so the
|
|
28
|
+
* reviewer's fix-merge resolver rides the same tier and the two cannot drift. */
|
|
29
|
+
export const MERGE_TIER = VERIFY_TIER - 1;
|
|
21
30
|
/** How many verifications may be in flight (parked or running) at once: every `maxConcurrent`
|
|
22
31
|
* permit but one, so a deep merge queue never takes the last slot from the roles that could
|
|
23
32
|
* author — at least one, so `maxConcurrent` 1 still merges. The merge's short conflict-resolution
|
|
@@ -28,28 +37,68 @@ export function verifyLimit(maxConcurrent) {
|
|
|
28
37
|
/** Resolve a merge entry's authoring runner: the live runner when the role is enabled
|
|
29
38
|
* (runners are never removed from the array on disable — only a warning event fires); a role
|
|
30
39
|
* disabled before this process started has no runner, so a throwaway one supplies the same
|
|
31
|
-
* wiring (
|
|
40
|
+
* wiring (runner-pi.ts, runMergePi, foldMergeUsage) and a disk-loaded state to fold and save
|
|
32
41
|
* on. Both share the live config, like every runner — the director takes directorConfig (the live
|
|
33
42
|
* config, its budget-gate exemption, unless a used-up window moved it), every other role takes
|
|
34
43
|
* roleConfig (gateRoleConfig's single home for the rule; the scheduler assigns the live runners
|
|
35
44
|
* by it every poll). */
|
|
36
|
-
function resolveAuthor(ctx, role) {
|
|
45
|
+
export function resolveAuthor(ctx, role) {
|
|
37
46
|
const { root, mainBranch, signal, runners, liveConfig, roleConfig, directorConfig } = ctx;
|
|
38
47
|
return (runners.find((r) => r.role === role) ??
|
|
39
|
-
new
|
|
48
|
+
new RoleRunner(root, role, gateRoleConfig(role, liveConfig, roleConfig, directorConfig), mainBranch, signal));
|
|
40
49
|
}
|
|
41
50
|
/** Drop a torn queue head. A torn head (torn or foreign — a partial write an older build left,
|
|
42
51
|
* a hand-placed file) makes headMerge read null forever — nothing else drops it, stranding
|
|
43
|
-
* every live entry behind it and pinning their authors'
|
|
44
|
-
* one warning
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
52
|
+
* every live entry behind it and pinning their authors' runs via the interlock. Drop it with
|
|
53
|
+
* one warning naming the file (and, when the head's raw bytes still carry a role, that role);
|
|
54
|
+
* the healthy entries behind it are verified in the same poll. When a role is recovered, that
|
|
55
|
+
* role's merge pin is read and, if it resolves, the change is re-queued fresh — in the same
|
|
56
|
+
* shape leftover recovery builds (recoveredMergeEntry: the commit message's real summary,
|
|
57
|
+
* body, high-friction flag, and revision round, plus the authoring run from its Run trailer) —
|
|
58
|
+
* before the drop, so the same poll's queuedMergeFiles sees it and no landing waits forever on
|
|
59
|
+
* the one unreadable file (BUGS.md 2026-09-17). The re-queue is best-effort: a write failure
|
|
60
|
+
* (ENOSPC/EMFILE) or an unreadable commit must not wedge the queue, so the torn head is still
|
|
61
|
+
* dropped (the pin survives and leftover recovery re-queues the full shape on the role's next
|
|
62
|
+
* run), and the drain's poll never aborts. A torn head with no recoverable role or no pin drops
|
|
63
|
+
* with the warning alone; the crashed entry's commit, if any, still rides its merge ref into
|
|
64
|
+
* next-run leftover recovery. Exported for test/merge-queue.test.ts's torn-head step. */
|
|
65
|
+
export async function dropTornHead(root) {
|
|
48
66
|
const stale = staleHeadFile(root);
|
|
49
67
|
if (!stale)
|
|
50
68
|
return;
|
|
69
|
+
const role = tornHeadRole(stale);
|
|
70
|
+
let requeued = false;
|
|
71
|
+
let requeueFailed = false;
|
|
72
|
+
if (role) {
|
|
73
|
+
const sha = await refSha(root, mergeRefName(role)).catch(() => null);
|
|
74
|
+
if (sha) {
|
|
75
|
+
try {
|
|
76
|
+
// The shape leftover recovery builds. `run` is recovered from the commit's Run trailer
|
|
77
|
+
// (the drain has no run number of its own) so session naming keeps the authoring run;
|
|
78
|
+
// a hand-made commit with no trailer falls back to the zero slot.
|
|
79
|
+
const meta = await recoveredMetadata(root, sha);
|
|
80
|
+
const message = await commitMessage(root, sha);
|
|
81
|
+
const run = message === null ? 0 : (commitRunOf(message) ?? 0);
|
|
82
|
+
enqueueMerge(root, recoveredMergeEntry(role, sha, meta, run));
|
|
83
|
+
requeued = true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// A re-queue that cannot be built or written (ENOSPC/EMFILE on the queue write, an
|
|
87
|
+
// unreadable commit) must not abort the poll or strand the head: drop it anyway and let
|
|
88
|
+
// the still-held pin feed next-run leftover recovery, which rebuilds the full shape.
|
|
89
|
+
requeueFailed = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
51
93
|
dropMerge(stale);
|
|
52
|
-
|
|
94
|
+
const roleNote = !role
|
|
95
|
+
? ""
|
|
96
|
+
: requeued
|
|
97
|
+
? `; ${role}'s pinned change was re-queued`
|
|
98
|
+
: requeueFailed
|
|
99
|
+
? `; ${role}'s pinned change could not be re-queued — left for leftover recovery`
|
|
100
|
+
: `; ${role}'s pin is unreadable — left for leftover recovery`;
|
|
101
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `merge queue head ${path.basename(stale)} is unreadable (torn or foreign) — dropped so the queue keeps moving${roleNote}`);
|
|
53
102
|
}
|
|
54
103
|
/** Already-merged verdicts cached per (root, sha) against the main head they were computed
|
|
55
104
|
* against. drainVerify re-runs its dedupe against main every poll, but a queued entry's sha is
|
|
@@ -88,7 +137,7 @@ async function mergedIntoMain(root, sha, mainBranch, head) {
|
|
|
88
137
|
* run, with its marker record; a crash mid-verification leaves both entry and ref, so the entry is
|
|
89
138
|
* verified again — the established crash semantics. */
|
|
90
139
|
export async function drainVerify(ctx, p) {
|
|
91
|
-
dropTornHead(ctx.root);
|
|
140
|
+
await dropTornHead(ctx.root);
|
|
92
141
|
const queue = queuedMergeFiles(ctx.root);
|
|
93
142
|
// One main-head read per poll with a non-empty queue (branchHead's ref-file fast path — no
|
|
94
143
|
// spawn), serving every entry's cached dedupe verdict below.
|
|
@@ -98,6 +147,9 @@ export async function drainVerify(ctx, p) {
|
|
|
98
147
|
return;
|
|
99
148
|
if (liveRoles(p).has(entry.role))
|
|
100
149
|
continue;
|
|
150
|
+
// A re-queued review_error waits out its delay; the entry stays queued and holds its author.
|
|
151
|
+
if (entry.notBefore !== undefined && Date.now() < entry.notBefore)
|
|
152
|
+
continue;
|
|
101
153
|
if (await mergedIntoMain(ctx.root, entry.sha, ctx.mainBranch, mainHead)) {
|
|
102
154
|
dropMerge(file);
|
|
103
155
|
removeMergeChange(ctx.root, entry.role);
|
|
@@ -107,6 +159,11 @@ export async function drainVerify(ctx, p) {
|
|
|
107
159
|
// (its role was busy when the queue was listed) or claimed its role. Start nothing for it.
|
|
108
160
|
if (!fs.existsSync(file) || liveRoles(p).has(entry.role))
|
|
109
161
|
continue;
|
|
162
|
+
// A pre-merge entry during a reviewer hold stays queued instead of starting a parked verify
|
|
163
|
+
// that fills the verify limit; a review.afterMerge role's entry keeps draining (its review
|
|
164
|
+
// runs after merging, so the hold is not for it) — ctx's role-scoped start gate decides.
|
|
165
|
+
if (ctx.startHeld(entry.role))
|
|
166
|
+
continue;
|
|
110
167
|
startVerify(ctx, p, entry, file);
|
|
111
168
|
}
|
|
112
169
|
}
|
|
@@ -150,7 +207,7 @@ export function startVerify(ctx, p, entry, file) {
|
|
|
150
207
|
verify.parked = false;
|
|
151
208
|
if (lease === null)
|
|
152
209
|
return; // a shutdown while parked
|
|
153
|
-
if (verify.controller.signal.aborted || ctx.startHeld()) {
|
|
210
|
+
if (verify.controller.signal.aborted || ctx.startHeld(role)) {
|
|
154
211
|
lease.release(); // stopping, or a restart / team hold closed the gate while it waited
|
|
155
212
|
return;
|
|
156
213
|
}
|
|
@@ -163,10 +220,10 @@ export function startVerify(ctx, p, entry, file) {
|
|
|
163
220
|
// so a review run that lands on a different provider than the verification's review seam
|
|
164
221
|
// re-keys it there, and a re-key park is shown by providerWaitStage; its slot wait drops
|
|
165
222
|
// the key (pool-slot rule).
|
|
166
|
-
verdict = await withProviderLease(lease, semaphore, () => verifyRequest({ root, mainBranch, config: author.config, signal: () => verify.controller.signal, runGatePi: (opts) => author.runGatePi(opts) }, {
|
|
223
|
+
verdict = await withProviderLease(lease, semaphore, () => verifyRequest({ root, mainBranch, config: author.config, signal: () => verify.controller.signal, runGatePi: (opts) => author.runGatePi(opts), reviewHeld: () => ctx.startHeld() }, {
|
|
167
224
|
role,
|
|
168
225
|
sha: entry.sha,
|
|
169
|
-
|
|
226
|
+
runNumber: entry.run,
|
|
170
227
|
summary: entry.summary,
|
|
171
228
|
body: entry.body,
|
|
172
229
|
highFriction: entry.highFriction,
|
|
@@ -198,7 +255,7 @@ export function startVerify(ctx, p, entry, file) {
|
|
|
198
255
|
}
|
|
199
256
|
else {
|
|
200
257
|
const result = verdict.kind === "stack" ? "aborted" : verdict.result;
|
|
201
|
-
await settleMergeOutcome(root, entry, author.state, result, Date.now() - startedAt, usage, file);
|
|
258
|
+
await settleMergeOutcome(root, entry, author.state, result, Date.now() - startedAt, usage, file, mainBranch);
|
|
202
259
|
if (verify.userAborted)
|
|
203
260
|
await discardPinnedRefs(root, [role]);
|
|
204
261
|
}
|
package/dist/src/notes-cli.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// vocabulary: plain
|
|
3
2
|
/** The `tumwater-notes` command — the second bin entry (dist/src/notes-cli.js), the one
|
|
4
3
|
* carrier for writing a role's note on every backend (plans/notes-command.md "The
|
|
5
4
|
* `tumwater-notes` command"). A run finds it on its PATH (src/agent/notes-shim.ts) and hands
|
|
@@ -14,9 +13,12 @@
|
|
|
14
13
|
* show print the note (nothing when there is none)
|
|
15
14
|
* clear empty the note
|
|
16
15
|
*
|
|
17
|
-
* Exits 0 on success, 1 on a refusal (empty input, over-cap note,
|
|
18
|
-
* TUMWATER_NOTES_PATH), and 2 with usage for
|
|
16
|
+
* Exits 0 on success, 1 on a refusal (empty input, over-cap note, a note file that cannot be
|
|
17
|
+
* written or read, or no TUMWATER_NOTES_PATH), and 2 with usage for an unknown subcommand (a
|
|
18
|
+
* known subcommand with bad arguments refuses with usage and exit 1). */
|
|
19
19
|
import fs from "node:fs";
|
|
20
|
+
import { readTextIfExists } from "./files/files.js";
|
|
21
|
+
import { errorMessage, stripTerminalControls } from "./text/text.js";
|
|
20
22
|
import { validateRoleNote, writeRoleNote } from "./agent/role-notes-write.js";
|
|
21
23
|
/** The note's path, read ONLY from the environment — never from an argument. */
|
|
22
24
|
const NOTES_PATH = process.env.TUMWATER_NOTES_PATH;
|
|
@@ -29,14 +31,10 @@ const USAGE = [
|
|
|
29
31
|
function readStdin() {
|
|
30
32
|
return fs.readFileSync(0, "utf8");
|
|
31
33
|
}
|
|
32
|
-
/** Read the note's current text; a missing note reads as empty (show prints nothing).
|
|
34
|
+
/** Read the note's current text; a missing note reads as empty (show prints nothing). Any
|
|
35
|
+
* other read failure (EACCES, EISDIR) throws, so it is never mistaken for an empty note. */
|
|
33
36
|
function readNote() {
|
|
34
|
-
|
|
35
|
-
return fs.readFileSync(NOTES_PATH, "utf8");
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return "";
|
|
39
|
-
}
|
|
37
|
+
return readTextIfExists(NOTES_PATH) ?? "";
|
|
40
38
|
}
|
|
41
39
|
/** Refuse with `message` on stderr and exit 1 — the exit code for every expected refusal. */
|
|
42
40
|
function refuse(message) {
|
|
@@ -51,7 +49,12 @@ function writeNote(text) {
|
|
|
51
49
|
const error = validateRoleNote(text);
|
|
52
50
|
if (error)
|
|
53
51
|
return refuse(error);
|
|
54
|
-
|
|
52
|
+
try {
|
|
53
|
+
writeRoleNote(NOTES_PATH, text);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return refuse(`could not save the note: ${errorMessage(err)}`);
|
|
57
|
+
}
|
|
55
58
|
process.stdout.write(`notes saved (${Buffer.byteLength(text, "utf8")} bytes)\n`);
|
|
56
59
|
return 0;
|
|
57
60
|
}
|
|
@@ -72,8 +75,8 @@ function main() {
|
|
|
72
75
|
try {
|
|
73
76
|
text = fs.readFileSync(flagArg, "utf8");
|
|
74
77
|
}
|
|
75
|
-
catch {
|
|
76
|
-
return refuse(`cannot read ${flagArg}`);
|
|
78
|
+
catch (err) {
|
|
79
|
+
return refuse(`cannot read ${JSON.stringify(flagArg)}: ${errorMessage(err)}`);
|
|
77
80
|
}
|
|
78
81
|
return writeNote(text);
|
|
79
82
|
}
|
|
@@ -81,12 +84,29 @@ function main() {
|
|
|
81
84
|
case "show":
|
|
82
85
|
if (flag !== undefined)
|
|
83
86
|
return refuse(USAGE);
|
|
84
|
-
|
|
87
|
+
let note;
|
|
88
|
+
try {
|
|
89
|
+
note = readNote();
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
return refuse(`could not read the note: ${errorMessage(err)}`);
|
|
93
|
+
}
|
|
94
|
+
// The note is model-authored text, so strip terminal controls at this display
|
|
95
|
+
// boundary — the same treatment say()/fail()/the TUI frame give every untrusted
|
|
96
|
+
// string, so a prompt-injected note with an ANSI escape (cursor move, title set,
|
|
97
|
+
// OSC 52 clipboard write, carriage-return overwrite) cannot drive the operator's
|
|
98
|
+
// terminal when they run `tumwater-notes show`.
|
|
99
|
+
process.stdout.write(stripTerminalControls(note));
|
|
85
100
|
return 0;
|
|
86
101
|
case "clear":
|
|
87
102
|
if (flag !== undefined)
|
|
88
103
|
return refuse(USAGE);
|
|
89
|
-
|
|
104
|
+
try {
|
|
105
|
+
writeRoleNote(NOTES_PATH, "");
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
return refuse(`could not clear the note: ${errorMessage(err)}`);
|
|
109
|
+
}
|
|
90
110
|
process.stdout.write("notes cleared\n");
|
|
91
111
|
return 0;
|
|
92
112
|
default:
|