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,18 +1,10 @@
|
|
|
1
|
-
import { REVIEW_RESUME_LIMIT, saveLoopState } from "../loop/loop-state.js";
|
|
2
|
-
import { modelSelectorField, resolvedAgentConfig, reviewRunConfig } from "../config/config-views.js";
|
|
3
1
|
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { aheadOfMainDiff, aheadOfMainFiles, revisionInterdiff } from "../git/git-diff.js";
|
|
2
|
+
import { headOf, patchId } from "../git/git.js";
|
|
3
|
+
import { aheadOfMainFiles } from "../git/git-diff.js";
|
|
7
4
|
import { resetWorktreeToMain } from "../git/worktree.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { agentSessionResumable } from "../agent/agent-backend.js";
|
|
12
|
-
import { requestNoRerun, requestVerdict } from "./review-followup.js";
|
|
13
|
-
import { parseVerdict } from "./review-verdict.js";
|
|
14
|
-
import { recordReview } from "../tick/tick-apply.js";
|
|
15
|
-
import { shortSha } from "../text/format.js";
|
|
5
|
+
import { recordReview } from "../run/run-apply.js";
|
|
6
|
+
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
7
|
+
import { errorMessage } from "../text/text.js";
|
|
16
8
|
import { firstReason } from "../text/phrases.js";
|
|
17
9
|
import { isExemptDiff } from "./exemptions.js";
|
|
18
10
|
import { falseFixReason } from "../verdict/fix-claim.js";
|
|
@@ -20,52 +12,72 @@ import { conflictMarkerReason } from "../merge/merge-git.js";
|
|
|
20
12
|
import { protectedEditReason } from "./protected-edits.js";
|
|
21
13
|
import { secretLeakReason } from "./secret-leaks.js";
|
|
22
14
|
import { backlogStructureReason } from "../backlog/backlog-structure.js";
|
|
23
|
-
import {
|
|
15
|
+
import { idleWithoutProviderLease } from "../concurrency/provider-lease.js";
|
|
24
16
|
import { setMergeStage } from "../merge/merge-slot.js";
|
|
25
17
|
import { gateBuildPrecheck } from "./review-precheck.js";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
18
|
+
export { REVIEW_FAILURE_LIMIT } from "./review-run.js";
|
|
19
|
+
import { restoreReviewedHead, runReviewer, withRuns } from "./review-run.js";
|
|
20
|
+
/** In-place retries of a failed reviewer run within one verify, after the first run. */
|
|
21
|
+
const REVIEW_RETRIES = 2;
|
|
22
|
+
/** The pause before each retry (ReviewContext.retryDelayMs overrides it in tests). */
|
|
23
|
+
const REVIEW_RETRY_DELAY_MS = 30_000;
|
|
24
|
+
/** Overrides the pause for tests that drive a whole runner or orchestrator, where no context
|
|
25
|
+
* reaches the review. */
|
|
26
|
+
const REVIEW_RETRY_DELAY_ENV = "TUMWATER_REVIEW_RETRY_DELAY_MS";
|
|
27
|
+
/** A second pool slot for the gate check, or null when the context cannot lease one, none is free
|
|
28
|
+
* right now, or the lease failed (the check then runs first, in the change's own slot). */
|
|
29
|
+
async function leaseCheckSlot(ctx, head) {
|
|
30
|
+
if (!ctx.leaseCheckSlot)
|
|
31
|
+
return null;
|
|
32
|
+
try {
|
|
33
|
+
return await ctx.leaseCheckSlot(head);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
warnEventBestEffort(ctx.root, ctx.role, `no second workspace for the gate check (${errorMessage(err)}); running it before the review`);
|
|
37
|
+
return null;
|
|
46
38
|
}
|
|
47
|
-
await git(wt, "reset", "--hard", head);
|
|
48
39
|
}
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
/** Start the gate check in `lease`'s checkout of `head`. A rejection is recorded, not applied:
|
|
41
|
+
* resetting the change's branch is left to reviewAttempt, after the reviewer has stopped. */
|
|
42
|
+
function startOverlappedCheck(ctx, state, head, lease) {
|
|
43
|
+
const overlap = { abort: new AbortController(), done: false };
|
|
44
|
+
const reject = async (reasons) => {
|
|
45
|
+
overlap.rejectedReasons = reasons;
|
|
46
|
+
return { decision: "rejected", detail: firstReason(reasons) };
|
|
47
|
+
};
|
|
48
|
+
overlap.gate = gateBuildPrecheck({ ...ctx, wt: lease.dir }, state, head, reject)
|
|
49
|
+
.then((pre) => {
|
|
50
|
+
overlap.done = true;
|
|
51
|
+
if (pre.resolved)
|
|
52
|
+
overlap.abort.abort();
|
|
53
|
+
else
|
|
54
|
+
setMergeStage(ctx.root, ctx.role, "reviewing");
|
|
55
|
+
return pre;
|
|
56
|
+
}, (err) => {
|
|
57
|
+
overlap.done = true;
|
|
58
|
+
overlap.abort.abort();
|
|
59
|
+
throw err;
|
|
60
|
+
})
|
|
61
|
+
.finally(() => lease.release());
|
|
62
|
+
overlap.gate.catch(() => { }); // surfaced where reviewAttempt awaits it
|
|
63
|
+
return overlap;
|
|
64
|
+
}
|
|
53
65
|
/** Run the adversarial review gate over everything ahead of main in `wt` and update `state`
|
|
54
|
-
* accordingly. Shared by the
|
|
66
|
+
* accordingly. Shared by the run path (after commitAll) and leftover recovery
|
|
55
67
|
* (leftover.ts's recoverLeftover) — every path
|
|
56
68
|
* that can move a commit into main routes through here, so no crash or abort path smuggles
|
|
57
69
|
* unreviewed work in:
|
|
58
70
|
* - gate disabled, or an already-approved HEAD, or an exempt (doc-only) diff → merge as-is;
|
|
59
|
-
* - a new HEAD carrying
|
|
71
|
+
* - a new HEAD carrying an approved patch (approvedPatchIds) → approved after the
|
|
60
72
|
* build pre-check, with no reviewer run;
|
|
61
73
|
* - approve → record lastApprovedHead and its patch-id, and discard the reviewer's stray
|
|
62
74
|
* working-tree edits (its only output channel is the verdict);
|
|
63
75
|
* - reject → reset the branch to main, record reasons in state.lastReview (injected into the
|
|
64
|
-
* role's next
|
|
76
|
+
* role's next run prompt), log review_rejected;
|
|
65
77
|
* - fail with an unparseable reply (the reviewer ran to completion but emitted no VERDICT) →
|
|
66
78
|
* one capped follow-up turn on the reviewer's own session asks for the missing line
|
|
67
79
|
* (mirroring requestSummary — BUGS.md 2026-09-29); only a follow-up that also yields
|
|
68
|
-
* nothing counts the strike. Leave the commit on the branch for the next
|
|
80
|
+
* nothing counts the strike. Leave the commit on the branch for the next run's re-review,
|
|
69
81
|
* counting consecutive failures per HEAD; past REVIEW_FAILURE_LIMIT discard the leftover
|
|
70
82
|
* with a warning.
|
|
71
83
|
* - fail because the run itself failed (transport/spawn/timeout, pi.ok false) → leave the
|
|
@@ -77,7 +89,7 @@ export const REVIEW_FAILURE_LIMIT = 3;
|
|
|
77
89
|
* `highFriction` marks a change whose authoring run burned more than the configured turn/time
|
|
78
90
|
* thresholds (plans/refusal-and-thrash.md): the flag rides along in the review prompt so the
|
|
79
91
|
* reviewer applies extra scrutiny to whether the work should exist at all. */
|
|
80
|
-
|
|
92
|
+
async function reviewAttempt(ctx, state, summary, commitBody, highFriction, retry) {
|
|
81
93
|
const { root, role, wt, mainBranch, config } = ctx;
|
|
82
94
|
if (!config.review.enabled) {
|
|
83
95
|
state.reviewResume = undefined;
|
|
@@ -86,7 +98,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
86
98
|
const head = await headOf(wt, "HEAD");
|
|
87
99
|
// The one reject path: every rejection of this HEAD — deterministic build-check failure or
|
|
88
100
|
// model verdict — shares this bookkeeping. Record lastReview (injected into the role's next
|
|
89
|
-
//
|
|
101
|
+
// run prompt), reset the failure count (a verdict about this HEAD is a successful review
|
|
90
102
|
// either way), discard the branch by resetting it to main, and log review_rejected.
|
|
91
103
|
// Callers attach `run` when a pi run was consumed.
|
|
92
104
|
const reject = async (reasons, durationMs) => {
|
|
@@ -103,6 +115,22 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
103
115
|
});
|
|
104
116
|
return { decision: "rejected", detail: firstReason(reasons) };
|
|
105
117
|
};
|
|
118
|
+
// Every exit below that keeps the change leaves the worktree at the head this gate reviewed.
|
|
119
|
+
const keep = async (r) => {
|
|
120
|
+
await restoreReviewedHead(wt, head, root, role);
|
|
121
|
+
return r;
|
|
122
|
+
};
|
|
123
|
+
// Settle the attempt on an overlapped check that did not pass: the sequential pre-check's own
|
|
124
|
+
// outcome, with the reviewer's run (when one started) dropped and no verdict recorded. A
|
|
125
|
+
// rejection is applied now, after the reviewer stopped touching the tree; every other outcome
|
|
126
|
+
// keeps the change at the head the gate reviewed.
|
|
127
|
+
const settleFailedCheck = async (o, pre, pi) => {
|
|
128
|
+
state.reviewResume = undefined;
|
|
129
|
+
const settled = o.rejectedReasons
|
|
130
|
+
? await reject(o.rejectedReasons)
|
|
131
|
+
: await keep(pre.resolved ?? { decision: "failed", detail: "the gate check did not complete" });
|
|
132
|
+
return pi ? withRuns(settled, pi, null, null) : settled;
|
|
133
|
+
};
|
|
106
134
|
// Already reviewed this exact HEAD (e.g. a merge_blocked retry): do not burn another run.
|
|
107
135
|
if (state.lastApprovedHead === head) {
|
|
108
136
|
state.reviewResume = undefined;
|
|
@@ -130,7 +158,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
130
158
|
// Fixed must have its fix's symbols on this tree, or it records code that does not
|
|
131
159
|
// exist (BUGS.md 2026-09-22 — 9cea8c3 merged a Fix paragraph naming runScriptGroup
|
|
132
160
|
// and signalTree with no source behind it). A deterministic rejection, like the
|
|
133
|
-
// pre-check below: no pi run consumed, reasons injected into the author's next
|
|
161
|
+
// pre-check below: no pi run consumed, reasons injected into the author's next run.
|
|
134
162
|
const falseFix = await falseFixReason(wt, mainBranch, files);
|
|
135
163
|
if (falseFix)
|
|
136
164
|
return reject([falseFix]);
|
|
@@ -154,336 +182,103 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
154
182
|
// Deterministic build pre-check — after BOTH early returns above (an md-only diff cannot
|
|
155
183
|
// break the build) and before any reviewer run or the phase/event that would show
|
|
156
184
|
// "reviewing": a deterministic rejection never shows as reviewing on the dashboards. Both
|
|
157
|
-
// gate callers (the
|
|
185
|
+
// gate callers (the run path and leftover.ts's recoverLeftover) get it for free. Its run,
|
|
158
186
|
// flake re-run, and attribution live in review-precheck.ts (the check run itself lives in
|
|
159
187
|
// runScopedBuildCheck, shared with the merge path's in-lock re-check); a resolved result
|
|
160
188
|
// here is returned verbatim and a pass hands the verified head to the merge path below.
|
|
161
189
|
// The pre-check sets the merge cell's stage (a no-op outside a queued merge) —
|
|
162
190
|
// "build-check" while it runs, "reviewing" when the reviewer run starts below.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
state.reviewResume = undefined;
|
|
166
|
-
return pre.resolved;
|
|
167
|
-
}
|
|
168
|
-
const { verifiedHead, verifiedByHarness } = pre;
|
|
191
|
+
// A retried attempt keeps the first attempt's passing pre-check (a green, or a red naming
|
|
192
|
+
// only main's known failures): the tree is the same head.
|
|
169
193
|
// A review judges a diff, not a sha: a head carrying exactly the patch this role's last
|
|
170
194
|
// approval judged (the approved head cleanly rebased onto a moved main) reuses that verdict
|
|
171
195
|
// instead of paying a second reviewer run. Only the model review is reused — the pre-check
|
|
172
|
-
//
|
|
173
|
-
//
|
|
196
|
+
// has just run on this tree (a failure rejected it there), and its verifiedHead rides on the
|
|
197
|
+
// result as for any approval.
|
|
174
198
|
const reviewedPatchId = (await patchId(wt, mainBranch, head)) ?? undefined;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// resume it only while the patch is unchanged (the same diff), the resume budget lasts, and
|
|
192
|
-
// the directory still holds a gate session to `--continue`. Otherwise the stale marker is
|
|
193
|
-
// cleared and this attempt reviews the change from scratch.
|
|
194
|
-
const priorResume = state.reviewResume;
|
|
195
|
-
const resuming = priorResume !== undefined &&
|
|
196
|
-
priorResume.patchId === reviewedPatchId &&
|
|
197
|
-
priorResume.attempts < REVIEW_RESUME_LIMIT &&
|
|
198
|
-
agentSessionResumable(priorResume.sessionDir, {
|
|
199
|
-
kind: "gate",
|
|
200
|
-
agent: reviewCfg.agent ?? "pi",
|
|
201
|
-
}, claudeConfigDir);
|
|
202
|
-
if (resuming) {
|
|
203
|
-
// Count the resume before the run: a crash must not let the same session be resumed
|
|
204
|
-
// forever without ever spending the budget.
|
|
205
|
-
priorResume.attempts += 1;
|
|
206
|
-
saveLoopState(root, state);
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
state.reviewResume = undefined;
|
|
210
|
-
}
|
|
211
|
-
// The directory this run actually uses: a resumed attempt continues the timed-out run's
|
|
212
|
-
// session, a fresh one its own. Both the run and its follow-up turns must target this dir —
|
|
213
|
-
// a follow-up that used ctx.sessionDir while the review resumed another dir would
|
|
214
|
-
// `--continue` a session that never held this review.
|
|
215
|
-
const reviewSessionDir = resuming ? state.reviewResume.sessionDir : ctx.sessionDir;
|
|
216
|
-
// The follow-up turns (verdict recovery, no-re-run nudge) continue the just-finished review,
|
|
217
|
-
// so they must read the same directory. A fresh run's dir is already ctx's, so only a resume
|
|
218
|
-
// needs the override.
|
|
219
|
-
const followupCtx = reviewSessionDir === ctx.sessionDir ? ctx : { ...ctx, sessionDir: reviewSessionDir };
|
|
220
|
-
logEventBestEffort(root, {
|
|
221
|
-
source: role,
|
|
222
|
-
type: "review_start",
|
|
223
|
-
head,
|
|
224
|
-
...(ctx.revisionRound !== undefined ? { revision: ctx.revisionRound } : {}),
|
|
225
|
-
...(resuming ? { resumed: true } : {}),
|
|
226
|
-
...modelSelectorField(reviewCfg),
|
|
227
|
-
});
|
|
228
|
-
// Persist the phase BEFORE the run so a dashboard mid-review shows "reviewing" and a crash
|
|
229
|
-
// mid-review is distinguishable from a crash mid-author-run on resume (stray edits are the
|
|
230
|
-
// reviewer's then, not the author's). Cleared at tick end alongside `running`.
|
|
231
|
-
state.phase = "review";
|
|
232
|
-
saveLoopState(root, state);
|
|
233
|
-
// Freeze the revision the diff is measured from once, before the run: main moves while a
|
|
234
|
-
// merge is approved, and a reviewer comparing against the live `main` ref reads another loop's
|
|
235
|
-
// just-merged work as this change reverting it (BUGS.md 2026-10-06). The prompt names this SHA
|
|
236
|
-
// and tells the reviewer to compare against it instead.
|
|
237
|
-
const base = await changeBaseRev(wt, mainBranch);
|
|
238
|
-
const diff = await aheadOfMainDiff(wt, mainBranch);
|
|
239
|
-
// A revision's re-review gets the prior review's objections and the interdiff between the two
|
|
240
|
-
// versions (plans/revise-rejected.md part 2/2), so it checks what the revision had to resolve
|
|
241
|
-
// instead of starting from no memory. A missing prior object yields an empty interdiff, never
|
|
242
|
-
// a throw (revisionInterdiff).
|
|
243
|
-
const priorReview = ctx.priorReview
|
|
244
|
-
? {
|
|
245
|
-
round: ctx.revisionRound ?? 1,
|
|
246
|
-
reasons: ctx.priorReview.reasons,
|
|
247
|
-
interdiff: await revisionInterdiff(wt, mainBranch, ctx.priorReview.sha, head),
|
|
248
|
-
}
|
|
249
|
-
: undefined;
|
|
250
|
-
// The reviewer run's wall time rides on its verdict event: a reviewer that takes an hour per
|
|
251
|
-
// merge on local hardware is a team-level cost an operator must be able to see.
|
|
252
|
-
const reviewStartedAt = Date.now();
|
|
253
|
-
// The reviewer's started tool calls, collected only when the no-re-run rule stands (a verified
|
|
254
|
-
// pre-check) — the one case the suite-rerun tripwire below can fire.
|
|
255
|
-
const reviewerCalls = [];
|
|
256
|
-
// The merge cell switches to the reviewer run's live detail (no-op outside a queued
|
|
257
|
-
// merge). runPi writes the run's `tumwater_run` label line before spawning pi, and that
|
|
258
|
-
// line resets the gate progress accumulator, so the cell never shows a previous run's counts.
|
|
259
|
-
setMergeStage(root, role, "reviewing");
|
|
260
|
-
// The author's claimed WHY/RISK/VERIFIED ride along when present — checking those claims
|
|
261
|
-
// against the actual diff is exactly the adversarial angle (recovery merges reconstruct
|
|
262
|
-
// them from the pinned commit's message).
|
|
263
|
-
const pi = await ctx.runGatePi({
|
|
264
|
-
cwd: wt,
|
|
265
|
-
kind: "gate",
|
|
266
|
-
prompt: resuming
|
|
267
|
-
? buildReviewResumePrompt(base, state.reviewResume.patchId)
|
|
268
|
-
: buildReviewPrompt(diff, summary, commitBody, readPrinciples(root), highFriction, verifiedByHarness, base, undefined, priorReview),
|
|
269
|
-
// The reviewer runs on its own time budget (review.timeoutSeconds), never longer than a
|
|
270
|
-
// tick's: a timed-out review is a FAILED run (pi.ok false), so it takes the dead-backend
|
|
271
|
-
// path below — commit kept, no strike — and re-merges through the author's next tick
|
|
272
|
-
// instead of holding the merge queue for a whole authoring tick.
|
|
273
|
-
config: reviewCfg,
|
|
274
|
-
// This run's own session directory (per-run, keyed by tick and pinned sha): a follow-up
|
|
275
|
-
// turn, or a later attempt that resumes this review, continues exactly this run instead
|
|
276
|
-
// of whichever review wrote last. A fresh review carries no --continue — the reviewer
|
|
277
|
-
// must not inherit the author's context — and old files are collected by the age-based
|
|
278
|
-
// prune at orchestrator start.
|
|
279
|
-
sessionDir: reviewSessionDir,
|
|
280
|
-
...(resuming ? { continueSession: true } : {}),
|
|
281
|
-
sessionName: `tumwater-review-${role}-${ctx.tick}`,
|
|
282
|
-
rawLogFile: piLogPath(root, role),
|
|
283
|
-
nativeLogFile: nativeLogPath(root, role, reviewCfg.agent ?? "pi"),
|
|
284
|
-
// Label this run in the shared transcript (src/ui/transcript.ts renders it as
|
|
285
|
-
// `── review @ <ts> ──`) so an operator can tell reviewer runs from author ticks.
|
|
286
|
-
label: "review",
|
|
287
|
-
signal: ctx.signal,
|
|
288
|
-
// A stalled tool call during review hangs the gate just like one during authoring —
|
|
289
|
-
// name it in the event feed while the quiet watchdog still counts down.
|
|
290
|
-
onToolCallStalled: (message) => warnEventBestEffort(root, role, message),
|
|
291
|
-
onToolCallStart: verifiedByHarness
|
|
292
|
-
? (toolName, args) => {
|
|
293
|
-
reviewerCalls.push({ toolName, args });
|
|
199
|
+
const patchApproved = reviewedPatchId !== undefined && state.approvedPatchIds?.includes(reviewedPatchId) === true;
|
|
200
|
+
// The check runs alongside the reviewer when a second pool slot is free right now (Fast merge
|
|
201
|
+
// part 12): it starts here, in its own checkout of this head, while the reviewer run below
|
|
202
|
+
// starts in the change's own slot. An already-approved patch has no reviewer to overlap, and
|
|
203
|
+
// with no free slot (or no way to lease one) the check runs first, as it always did.
|
|
204
|
+
const afterMerge = (config.review.afterMerge ?? []).includes(baseRoleOf(role));
|
|
205
|
+
let overlap;
|
|
206
|
+
if (!retry.pre) {
|
|
207
|
+
const lease = patchApproved || afterMerge ? null : await leaseCheckSlot(ctx, head);
|
|
208
|
+
if (lease)
|
|
209
|
+
overlap = startOverlappedCheck(ctx, state, head, lease);
|
|
210
|
+
else {
|
|
211
|
+
const pre = await gateBuildPrecheck(ctx, state, head, reject);
|
|
212
|
+
if (pre.resolved) {
|
|
213
|
+
state.reviewResume = undefined;
|
|
214
|
+
return pre.resolved;
|
|
294
215
|
}
|
|
295
|
-
:
|
|
296
|
-
// A gate run that read an image its model cannot see is logged, not killed (plans/
|
|
297
|
-
// image-input.md "Detecting a blind read"): the reviewer keeps working from what it CAN
|
|
298
|
-
// see, and the image_blind line names the file, the model, and this tick so the operator
|
|
299
|
-
// knows the diff was judged under a blind read.
|
|
300
|
-
onBlindRead: (path, action) => logEventBestEffort(root, {
|
|
301
|
-
source: role,
|
|
302
|
-
type: "image_blind",
|
|
303
|
-
run: ctx.tick,
|
|
304
|
-
path,
|
|
305
|
-
model: reviewCfg.model,
|
|
306
|
-
action,
|
|
307
|
-
}),
|
|
308
|
-
});
|
|
309
|
-
// A reviewer told the pre-check passed that re-ran the full suite anyway held the merge
|
|
310
|
-
// slot and loaded the shared host for a result the harness already had (BUGS.md 2026-09-23:
|
|
311
|
-
// scratch copies under /tmp, `npm ci` and `npm test` there). Warned on every outcome, abort
|
|
312
|
-
// included — the run happened either way; the verdict itself is not touched.
|
|
313
|
-
const rerun = suiteRerunWarning(reviewerCalls);
|
|
314
|
-
if (rerun)
|
|
315
|
-
warnEventBestEffort(root, role, rerun);
|
|
316
|
-
// Every exit below that keeps the change leaves the worktree at the head this gate reviewed.
|
|
317
|
-
const keep = async (r) => {
|
|
318
|
-
await restoreReviewedHead(wt, head, root, role);
|
|
319
|
-
return r;
|
|
320
|
-
};
|
|
321
|
-
if (pi.aborted) {
|
|
322
|
-
// Shutdown mid-review: fail closed — the commit stays on the branch and the
|
|
323
|
-
// resumed/following tick re-reviews it via the combined ahead-of-main diff. Any resume
|
|
324
|
-
// marker from an earlier attempt is dropped: an abort says nothing about the diff, and the
|
|
325
|
-
// next attempt starts clean rather than inheriting a half-counted session.
|
|
326
|
-
state.reviewResume = undefined;
|
|
327
|
-
return keep(withRuns({ decision: "failed", aborted: true }, pi, null, null));
|
|
328
|
-
}
|
|
329
|
-
// The bounded nudge (BUGS.md 2026-10-02): the reviewer broke the no-re-run rule despite a
|
|
330
|
-
// green pre-check, so one turn on its own session names the call and asks it to finish
|
|
331
|
-
// without re-running. A repeat on the nudge's own tool calls fails the review — strike-free,
|
|
332
|
-
// commit kept, re-reviewed under the normal path — instead of paying a third suite run.
|
|
333
|
-
let nudge = null;
|
|
334
|
-
let nudgeRepeat = null;
|
|
335
|
-
if (rerun && pi.ok) {
|
|
336
|
-
const nudgeCalls = [];
|
|
337
|
-
nudge = await requestNoRerun(followupCtx, rerun, nudgeCalls);
|
|
338
|
-
if (nudge?.aborted) {
|
|
339
|
-
return keep(withRuns({ decision: "failed", aborted: true }, pi, null, nudge));
|
|
340
|
-
}
|
|
341
|
-
nudgeRepeat = nudge ? (suiteRerunWarning(nudgeCalls) ?? null) : null;
|
|
342
|
-
if (nudgeRepeat) {
|
|
343
|
-
warnEventBestEffort(root, role, `${nudgeRepeat} the repeat fails this review: the commit stays, and the merge re-reviews it without a third suite run`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
let verdict = parseVerdict(pi.verdictText ?? "");
|
|
347
|
-
// The nudge turn's reply can carry the verdict the review run lost — use it before spending
|
|
348
|
-
// any other recovery turn, but never on a repeat (the repeat fails the review regardless).
|
|
349
|
-
if (!verdict && nudge && !nudgeRepeat) {
|
|
350
|
-
const recovered = parseVerdict(nudge.verdictText ?? "");
|
|
351
|
-
if (recovered) {
|
|
352
|
-
verdict = recovered;
|
|
353
|
-
warnEventBestEffort(root, role, "the reviewer's reply had no VERDICT line — recovered it with the no-re-run nudge turn on its session");
|
|
216
|
+
retry.pre = { verifiedHead: pre.verifiedHead, knownMainFailures: pre.knownMainFailures };
|
|
354
217
|
}
|
|
355
218
|
}
|
|
356
|
-
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
// side's missing SUMMARY (BUGS.md 2026-09-29). The nudge turn is the bounded follow-up when
|
|
362
|
-
// the reviewer broke the no-re-run rule — one turn total, so a spent nudge never earns the
|
|
363
|
-
// verdict-recovery turn on top.
|
|
364
|
-
let followUp = null;
|
|
365
|
-
if (!verdict && pi.ok && !nudge) {
|
|
366
|
-
followUp = await requestVerdict(followupCtx);
|
|
367
|
-
if (followUp?.aborted)
|
|
368
|
-
return keep(withRuns({ decision: "failed", aborted: true }, pi, followUp, nudge));
|
|
369
|
-
const recovered = followUp ? parseVerdict(followUp.verdictText ?? "") : null;
|
|
370
|
-
if (recovered) {
|
|
371
|
-
verdict = recovered;
|
|
372
|
-
warnEventBestEffort(root, role, "the reviewer's reply had no VERDICT line — recovered it with a follow-up turn on its own session");
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
// A repeat after the nudge (BUGS.md 2026-10-02): the reviewer re-ran the suite on the nudge
|
|
376
|
-
// turn too. Fail the review — strike-free like a dead backend (BUGS.md 2026-09-20): the
|
|
377
|
-
// commit stays for the next merge's re-review, the discard counter never advances, and no
|
|
378
|
-
// rejection is recorded against the author's diff.
|
|
379
|
-
if (nudgeRepeat) {
|
|
380
|
-
const message = `${nudgeRepeat} The review is failed on this repeat: the commit is kept, and the merge re-reviews it under the normal path.`;
|
|
381
|
-
logEventBestEffort(root, { source: role, type: "review_failed", head, message, durationMs: Date.now() - reviewStartedAt });
|
|
219
|
+
const knownMainFailures = retry.pre?.knownMainFailures;
|
|
220
|
+
// A role listed in review.afterMerge merges on its gate alone: the deterministic rejects and the
|
|
221
|
+
// pre-check above still ran, and the merged commit is reviewed after it lands
|
|
222
|
+
// (reviewMergedCommit, run by the merge pipeline's post-review drain).
|
|
223
|
+
if (afterMerge && retry.pre) {
|
|
382
224
|
state.reviewResume = undefined;
|
|
383
|
-
|
|
384
|
-
return
|
|
225
|
+
state.lastApprovedHead = head;
|
|
226
|
+
return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
|
|
385
227
|
}
|
|
386
|
-
if (
|
|
387
|
-
const followUpError = followUp && !followUp.ok ? followUp.errorMessage : undefined;
|
|
388
|
-
// The nudge turn's own death is backend evidence, exactly like the verdict-recovery
|
|
389
|
-
// follow-up's: a nudge that died with a transport error is the backend failing after the
|
|
390
|
-
// reviewer judged nothing — about the world, never about the diff — so the strike-free
|
|
391
|
-
// branch below must see it, and its error names the failure.
|
|
392
|
-
const nudgeError = nudge && !nudge.ok ? nudge.errorMessage : undefined;
|
|
393
|
-
// A progressing timeout of the REVIEW run itself (never a follow-up or nudge turn, both of
|
|
394
|
-
// which run only after pi ok) leaves the session for the next attempt while the patch is
|
|
395
|
-
// identifiable and the resume budget lasts. That is what makes the recorded failure honest:
|
|
396
|
-
// while a resume remains the next attempt continues this review, and once the budget is
|
|
397
|
-
// spent or the failure cannot resume, it reviews the change from scratch (BUGS.md
|
|
398
|
-
// 2026-09-30). A string rewrite of runPi's exact suffix, so any other cause — and the plain
|
|
399
|
-
// no-progress timeout, which promises nothing — passes through untouched.
|
|
400
|
-
const resumeAttempts = resuming ? state.reviewResume?.attempts ?? 0 : 0;
|
|
401
|
-
const resumeRemains = !pi.ok &&
|
|
402
|
-
!pi.aborted &&
|
|
403
|
-
pi.timedOutProgressing &&
|
|
404
|
-
reviewedPatchId !== undefined &&
|
|
405
|
-
resumeAttempts < REVIEW_RESUME_LIMIT &&
|
|
406
|
-
// The promise must be one the next attempt can keep: only a dir whose backend still
|
|
407
|
-
// holds a gate session to resume earns the marker (and the "continues this review"
|
|
408
|
-
// wording). Without this a backend that leaves no session file would record a resume the
|
|
409
|
-
// next attempt silently declines, reviewing from scratch anyway.
|
|
410
|
-
agentSessionResumable(reviewSessionDir, {
|
|
411
|
-
kind: "gate",
|
|
412
|
-
agent: reviewCfg.agent ?? "pi",
|
|
413
|
-
}, claudeConfigDir);
|
|
414
|
-
const reviewTimeoutRewrite = (message) => message.replace(" — session and worktree edits preserved for resume", resumeRemains
|
|
415
|
-
? " — the commit is kept; the next attempt continues this review"
|
|
416
|
-
: " — the commit is kept; the next attempt reviews it from scratch");
|
|
417
|
-
const message = reviewTimeoutRewrite(followUpError ??
|
|
418
|
-
nudgeError ??
|
|
419
|
-
pi.errorMessage ??
|
|
420
|
-
`no parseable VERDICT line in the reviewer's reply${nudge
|
|
421
|
-
? ", even after the no-re-run nudge turn on its session"
|
|
422
|
-
: followUp
|
|
423
|
-
? ", even after a follow-up turn on its session"
|
|
424
|
-
: ""}`);
|
|
425
|
-
logEventBestEffort(root, { source: role, type: "review_failed", head, message, durationMs: Date.now() - reviewStartedAt });
|
|
426
|
-
// A dead reviewer must never destroy committed work (BUGS.md 2026-09-20): leave the commit
|
|
427
|
-
// for the next tick's re-review and do not advance the per-HEAD discard counter. That
|
|
428
|
-
// holds for the review run itself (BUGS.md 2026-09-20), for the verdict-recovery
|
|
429
|
-
// follow-up (BUGS.md 2026-09-29), and for the no-re-run nudge turn (BUGS.md 2026-10-02):
|
|
430
|
-
// each one's death is the backend failing after the reviewer had already judged nothing —
|
|
431
|
-
// evidence about the world, never about the diff.
|
|
432
|
-
if (!pi.ok || (followUp && !followUp.ok) || (nudge && !nudge.ok)) {
|
|
433
|
-
if (resumeRemains) {
|
|
434
|
-
// Record the session this run used (the resumed directory, or this fresh run's own),
|
|
435
|
-
// so the next attempt `--continue`s exactly it.
|
|
436
|
-
state.reviewResume = {
|
|
437
|
-
patchId: reviewedPatchId,
|
|
438
|
-
sessionDir: reviewSessionDir,
|
|
439
|
-
attempts: resumeAttempts,
|
|
440
|
-
at: Date.now(),
|
|
441
|
-
};
|
|
442
|
-
saveLoopState(root, state);
|
|
443
|
-
}
|
|
444
|
-
else {
|
|
445
|
-
state.reviewResume = undefined;
|
|
446
|
-
}
|
|
447
|
-
recordReview(state, "failed", [message], head);
|
|
448
|
-
const configError = pi.configError || Boolean(followUp?.configError) || Boolean(nudge?.configError);
|
|
449
|
-
return keep(withRuns({ decision: "failed", detail: message, ...(configError ? { configError: true } : {}) }, pi, followUp, nudge));
|
|
450
|
-
}
|
|
451
|
-
// Consecutive failures of THIS HEAD only: a new commit (new HEAD) starts fresh. Read
|
|
452
|
-
// *before* overwriting lastReview with this failure.
|
|
453
|
-
const prev = state.lastReview;
|
|
454
|
-
const sameHead = prev?.verdict === "failed" && prev.head === head;
|
|
228
|
+
if (patchApproved && retry.pre) {
|
|
455
229
|
state.reviewResume = undefined;
|
|
456
|
-
state.
|
|
457
|
-
|
|
458
|
-
let discarded = false;
|
|
459
|
-
if ((state.unreviewFailures ?? 0) >= REVIEW_FAILURE_LIMIT) {
|
|
460
|
-
await resetWorktreeToMain(wt, mainBranch);
|
|
461
|
-
state.unreviewFailures = 0; // The HEAD is gone; nothing left to count against.
|
|
462
|
-
discarded = true;
|
|
463
|
-
warnEventBestEffort(root, role, `discarding unreviewed leftover after ${REVIEW_FAILURE_LIMIT} failed reviews (${shortSha(head)})`);
|
|
464
|
-
}
|
|
465
|
-
const failed = { ...withRuns({ decision: "failed", detail: message }, pi, followUp, nudge), ...(discarded ? { discarded: true } : {}) };
|
|
466
|
-
return discarded ? failed : keep(failed);
|
|
230
|
+
state.lastApprovedHead = head;
|
|
231
|
+
return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
|
|
467
232
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
233
|
+
return runReviewer(ctx, state, {
|
|
234
|
+
head, overlap, knownMainFailures, retry, summary, commitBody, highFriction,
|
|
235
|
+
reviewedPatchId, reject, keep, settleFailedCheck,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** Review the change ahead of main, retrying a failed reviewer run in place (twice, 30 s apart)
|
|
239
|
+
* before the attempt ends `failed`: a run that timed out, was killed, died in transport or at
|
|
240
|
+
* the provider, or stated no VERDICT after its recovery turn. A resumable session is resumed,
|
|
241
|
+
* otherwise the retry starts fresh (reviewAttempt). A configuration error and an abort are not
|
|
242
|
+
* retried. While the reviewer's provider is held the retry waits for the hold to lift and
|
|
243
|
+
* costs nothing. Each pause runs without the caller's team permit (idleWithoutProviderLease):
|
|
244
|
+
* a verification hands its lease back while it waits and takes it back before the next attempt.
|
|
245
|
+
* See reviewAttempt for the rest of the gate's contract. */
|
|
246
|
+
export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFriction) {
|
|
247
|
+
const retry = { attempt: 1, retriesLeft: ctx.retries ?? REVIEW_RETRIES };
|
|
248
|
+
const delayMs = ctx.retryDelayMs ?? Number(process.env[REVIEW_RETRY_DELAY_ENV] ?? REVIEW_RETRY_DELAY_MS);
|
|
249
|
+
for (;;) {
|
|
250
|
+
const result = await reviewAttempt(ctx, state, summary, commitBody, highFriction, retry);
|
|
251
|
+
if (!result.retryable)
|
|
252
|
+
return result;
|
|
253
|
+
for (const run of [result.run, result.followUpRun, result.nudgeRun])
|
|
254
|
+
if (run)
|
|
255
|
+
ctx.foldUsage?.(run);
|
|
256
|
+
const held = ctx.reviewHeld?.() ?? false;
|
|
257
|
+
warnEventBestEffort(ctx.root, ctx.role, `the reviewer run failed (${result.detail ?? "no detail"}) — ${held ? "waiting for the provider hold to lift, then " : ""}retrying the review in place`);
|
|
258
|
+
// The pause holds no team permit: a verification's lease is handed back while it waits and
|
|
259
|
+
// re-taken before the next attempt, so a reviewer that keeps failing does not starve the roles.
|
|
260
|
+
await idleWithoutProviderLease(async () => {
|
|
261
|
+
await sleepUnlessAborted(delayMs, ctx.signal);
|
|
262
|
+
while (ctx.reviewHeld?.() && !ctx.signal?.aborted)
|
|
263
|
+
await sleepUnlessAborted(Math.min(delayMs, 5000), ctx.signal);
|
|
264
|
+
}, ctx.signal);
|
|
265
|
+
if (ctx.signal?.aborted)
|
|
266
|
+
return { decision: "failed", aborted: true };
|
|
267
|
+
if (!held)
|
|
268
|
+
retry.retriesLeft -= 1;
|
|
269
|
+
retry.attempt += 1;
|
|
471
270
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
head,
|
|
485
|
-
reason: verdict.reasons[0],
|
|
486
|
-
durationMs: Date.now() - reviewStartedAt,
|
|
271
|
+
}
|
|
272
|
+
function sleepUnlessAborted(ms, signal) {
|
|
273
|
+
return new Promise((resolve) => {
|
|
274
|
+
if (signal?.aborted)
|
|
275
|
+
return resolve();
|
|
276
|
+
const done = () => {
|
|
277
|
+
clearTimeout(timer);
|
|
278
|
+
signal?.removeEventListener("abort", done);
|
|
279
|
+
resolve();
|
|
280
|
+
};
|
|
281
|
+
const timer = setTimeout(done, ms);
|
|
282
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
487
283
|
});
|
|
488
|
-
return withRuns({ decision: "approved", verifiedHead }, pi, followUp, nudge);
|
|
489
284
|
}
|