tumwater 0.4.2 → 0.4.4
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 +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
import { APPROVED_PATCH_IDS_MAX, REVIEW_RESUME_LIMIT, saveRunnerStateBestEffort } from "../runner/runner-state.js";
|
|
2
|
-
import { modelSelectorField, resolvedBackendConfig, reviewRunConfig } from "../config/config-views.js";
|
|
3
|
-
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
4
|
-
import { git } from "../git/git-run.js";
|
|
5
|
-
import { changeBaseRev, headOf } from "../git/git.js";
|
|
6
|
-
import { aheadOfMainDiff, revisionInterdiff } from "../git/git-diff.js";
|
|
7
|
-
import { resetWorktreeToMain } from "../git/worktree.js";
|
|
8
|
-
import { nativeLogPath, piLogPath } from "../paths.js";
|
|
9
|
-
import { readPrinciples } from "../prompt/principles.js";
|
|
10
|
-
import { buildReviewPrompt, buildReviewResumePrompt } from "../prompt/gate-prompts.js";
|
|
11
|
-
import { backendSessionResumable } from "../agent/agent-backend.js";
|
|
12
|
-
import { requestNoRerun, requestVerdict } from "./review-followup.js";
|
|
13
|
-
import { parseVerdict } from "./review-verdict.js";
|
|
14
|
-
import { recordReview } from "../run/run-apply.js";
|
|
15
|
-
import { shortSha } from "../text/format.js";
|
|
16
|
-
import { suiteRerunWarning } from "./suite-rerun.js";
|
|
17
|
-
import { setMergeStage } from "../merge/merge-slot.js";
|
|
18
|
-
/** Attach the review gate's pi runs onto a partial GateResult — `run` plus the optional
|
|
19
|
-
* verdict-recovery follow-up (`followUpRun`) and no-re-run nudge (`nudgeRun`) fields, each
|
|
20
|
-
* present only when its turn actually ran. Spelled once so every return site of
|
|
21
|
-
* reviewAheadOfMain that carries runs shares one shape for the usage-folding caller
|
|
22
|
-
* (merge-core.ts folds run, then followUpRun, then nudgeRun), instead of one conditional
|
|
23
|
-
* spread per site that a new run kind would have to update everywhere. */
|
|
24
|
-
export function withRuns(r, run, followUp, nudge) {
|
|
25
|
-
return {
|
|
26
|
-
...r,
|
|
27
|
-
run,
|
|
28
|
-
...(followUp ? { followUpRun: followUp } : {}),
|
|
29
|
-
...(nudge ? { nudgeRun: nudge } : {}),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
/** Put the worktree back on the exact head the gate reviewed: the reviewer's only output is its
|
|
33
|
-
* verdict, so a commit it made must never reach the merge pin or the next re-review. */
|
|
34
|
-
export async function restoreReviewedHead(wt, head, root, role) {
|
|
35
|
-
const now = await headOf(wt, "HEAD").catch(() => null);
|
|
36
|
-
if (now && now !== head) {
|
|
37
|
-
warnEventBestEffort(root, role, `the reviewer moved HEAD to ${shortSha(now)} during review; restored the reviewed head ${shortSha(head)}`);
|
|
38
|
-
}
|
|
39
|
-
await git(wt, "reset", "--hard", head);
|
|
40
|
-
}
|
|
41
|
-
/** Consecutive failed reviews of one branch HEAD after which the leftover is discarded with
|
|
42
|
-
* a warning: a misconfigured reviewer model must not be able to wedge a role into re-reviewing
|
|
43
|
-
* the same commit forever. */
|
|
44
|
-
export const REVIEW_FAILURE_LIMIT = 3;
|
|
45
|
-
function combineSignals(a, b) {
|
|
46
|
-
return a ? AbortSignal.any([a, b]) : b;
|
|
47
|
-
}
|
|
48
|
-
/** The reviewer run: resume-vs-fresh session choice, prompt, run, follow-up turns, verdict parse
|
|
49
|
-
* and the approve/reject bookkeeping. Called by reviewAttempt once the deterministic gate has
|
|
50
|
-
* passed the change on to a reviewer. */
|
|
51
|
-
export async function runReviewer(ctx, state, input) {
|
|
52
|
-
const { root, role, wt, mainBranch, config } = ctx;
|
|
53
|
-
const { head, reviewedPatchId, overlap, knownMainFailures, retry, summary, commitBody, highFriction } = input;
|
|
54
|
-
const { reject, keep, settleFailedCheck } = input;
|
|
55
|
-
// The reviewer's model selector (plans/model-tiers.md "Observability"): resolved from the
|
|
56
|
-
// same config the run below gets, so the event names what the reviewer actually runs on —
|
|
57
|
-
// the budget fallback included. Omitted when no model is configured (pi's own default).
|
|
58
|
-
const reviewCfg = reviewRunConfig(config);
|
|
59
|
-
// The team claude config dir (plans/notes-command.md "Keeping funes out of team runs"), so
|
|
60
|
-
// a claude review's resume guard looks under the same dir the review run writes its transcript
|
|
61
|
-
// to — a team that set one must resume under it, not the default ~/.claude.
|
|
62
|
-
const claudeConfigDir = resolvedBackendConfig(config, "claude").configDir;
|
|
63
|
-
// A progressing timeout left this run's session for the next attempt (RunnerState.reviewResume):
|
|
64
|
-
// resume it only while the patch is unchanged (the same diff), the resume budget lasts, and
|
|
65
|
-
// the directory still holds a gate session to `--continue`. Otherwise the stale marker is
|
|
66
|
-
// cleared and this attempt reviews the change from scratch.
|
|
67
|
-
const priorResume = state.reviewResume;
|
|
68
|
-
const resuming = priorResume !== undefined &&
|
|
69
|
-
priorResume.patchId === reviewedPatchId &&
|
|
70
|
-
priorResume.attempts < REVIEW_RESUME_LIMIT &&
|
|
71
|
-
backendSessionResumable(priorResume.sessionDir, {
|
|
72
|
-
kind: "gate",
|
|
73
|
-
agent: reviewCfg.agent ?? "pi",
|
|
74
|
-
}, claudeConfigDir);
|
|
75
|
-
if (resuming) {
|
|
76
|
-
// Count the resume before the run: a crash must not let the same session be resumed
|
|
77
|
-
// forever without ever spending the budget.
|
|
78
|
-
priorResume.attempts += 1;
|
|
79
|
-
saveRunnerStateBestEffort(root, state);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
state.reviewResume = undefined;
|
|
83
|
-
}
|
|
84
|
-
// The directory this run actually uses: a resumed attempt continues the timed-out run's
|
|
85
|
-
// session, a fresh one its own. Both the run and its follow-up turns must target this dir —
|
|
86
|
-
// a follow-up that used ctx.sessionDir while the review resumed another dir would
|
|
87
|
-
// `--continue` a session that never held this review.
|
|
88
|
-
const reviewSessionDir = resuming ? state.reviewResume.sessionDir : ctx.sessionDir;
|
|
89
|
-
// The follow-up turns (verdict recovery, no-re-run nudge) continue the just-finished review,
|
|
90
|
-
// so they must read the same directory. A fresh run's dir is already ctx's, so only a resume
|
|
91
|
-
// needs the override.
|
|
92
|
-
const followupCtx = reviewSessionDir === ctx.sessionDir ? ctx : { ...ctx, sessionDir: reviewSessionDir };
|
|
93
|
-
logEventBestEffort(root, {
|
|
94
|
-
source: role,
|
|
95
|
-
type: "review_start",
|
|
96
|
-
head,
|
|
97
|
-
...(ctx.revisionRound !== undefined ? { revision: ctx.revisionRound } : {}),
|
|
98
|
-
...(resuming ? { resumed: true } : {}),
|
|
99
|
-
...modelSelectorField(reviewCfg),
|
|
100
|
-
});
|
|
101
|
-
// Persist the phase BEFORE the run so a dashboard mid-review shows "reviewing" and a crash
|
|
102
|
-
// mid-review is distinguishable from a crash mid-author-run on resume (stray edits are the
|
|
103
|
-
// reviewer's then, not the author's). Cleared at run end alongside `running`.
|
|
104
|
-
state.phase = "review";
|
|
105
|
-
saveRunnerStateBestEffort(root, state);
|
|
106
|
-
// Freeze the revision the diff is measured from once, before the run: main moves while a
|
|
107
|
-
// merge is approved, and a reviewer comparing against the live `main` ref reads another role's
|
|
108
|
-
// just-merged work as this change reverting it (BUGS.md 2026-10-06). The prompt names this SHA
|
|
109
|
-
// and tells the reviewer to compare against it instead.
|
|
110
|
-
const base = await changeBaseRev(wt, mainBranch);
|
|
111
|
-
const diff = await aheadOfMainDiff(wt, mainBranch);
|
|
112
|
-
// A revision's re-review gets the prior review's objections and the interdiff between the two
|
|
113
|
-
// versions (plans/revise-rejected.md part 2/2), so it checks what the revision had to resolve
|
|
114
|
-
// instead of starting from no memory. A missing prior object yields an empty interdiff, never
|
|
115
|
-
// a throw (revisionInterdiff).
|
|
116
|
-
const priorReview = ctx.priorReview
|
|
117
|
-
? {
|
|
118
|
-
round: ctx.revisionRound ?? 1,
|
|
119
|
-
reasons: ctx.priorReview.reasons,
|
|
120
|
-
interdiff: await revisionInterdiff(wt, mainBranch, ctx.priorReview.sha, head),
|
|
121
|
-
}
|
|
122
|
-
: undefined;
|
|
123
|
-
// The reviewer run's wall time rides on its verdict event: a reviewer that takes an hour per
|
|
124
|
-
// merge on local hardware is a team-level cost an operator must be able to see.
|
|
125
|
-
const reviewStartedAt = Date.now();
|
|
126
|
-
// The reviewer's started tool calls: the no-re-run rule always stands (tumwater runs the
|
|
127
|
-
// check), so the suite-rerun tripwire below always applies.
|
|
128
|
-
const reviewerCalls = [];
|
|
129
|
-
// The merge cell switches to the reviewer run's live detail (no-op outside a queued
|
|
130
|
-
// merge). runPi writes the run's `tumwater_run` label line before spawning pi, and that
|
|
131
|
-
// line resets the gate progress accumulator, so the cell never shows a previous run's counts.
|
|
132
|
-
setMergeStage(root, role, overlap ? "reviewing-check" : "reviewing");
|
|
133
|
-
// A check that already went red has aborted the overlap: no reviewer run starts, the verdict
|
|
134
|
-
// below is the gate's alone.
|
|
135
|
-
if (overlap?.abort.signal.aborted)
|
|
136
|
-
return settleFailedCheck(overlap, await overlap.gate);
|
|
137
|
-
// The author's claimed WHY/RISK/VERIFIED ride along when present — checking those claims
|
|
138
|
-
// against the actual diff is exactly the adversarial angle (recovery merges reconstruct
|
|
139
|
-
// them from the pinned commit's message).
|
|
140
|
-
const pi = await ctx.runGatePi({
|
|
141
|
-
cwd: wt,
|
|
142
|
-
kind: "gate",
|
|
143
|
-
prompt: resuming
|
|
144
|
-
? buildReviewResumePrompt(base, state.reviewResume.patchId)
|
|
145
|
-
: buildReviewPrompt(diff, summary, commitBody, readPrinciples(root), highFriction, base, undefined, priorReview, knownMainFailures),
|
|
146
|
-
// The reviewer runs on its own time budget (review.timeoutSeconds), never longer than a
|
|
147
|
-
// run's: a timed-out review is a FAILED run (pi.ok false), so it takes the dead-backend
|
|
148
|
-
// path below — commit kept, no strike — and re-merges through the author's next run
|
|
149
|
-
// instead of holding the merge queue for a whole authoring run.
|
|
150
|
-
config: reviewCfg,
|
|
151
|
-
// This run's own session directory (per-run, keyed by run and pinned sha): a follow-up
|
|
152
|
-
// turn, or a later attempt that resumes this review, continues exactly this run instead
|
|
153
|
-
// of whichever review wrote last. A fresh review carries no --continue — the reviewer
|
|
154
|
-
// must not inherit the author's context — and old files are collected by the age-based
|
|
155
|
-
// prune at orchestrator start.
|
|
156
|
-
sessionDir: reviewSessionDir,
|
|
157
|
-
...(resuming ? { continueSession: true } : {}),
|
|
158
|
-
sessionName: `tumwater-review-${role}-${ctx.runNumber}`,
|
|
159
|
-
rawLogFile: piLogPath(root, role),
|
|
160
|
-
nativeLogFile: nativeLogPath(root, role, reviewCfg.agent ?? "pi"),
|
|
161
|
-
// Label this run in the shared transcript (src/ui/transcript.ts renders it as
|
|
162
|
-
// `── review @ <ts> ──`) so an operator can tell reviewer runs from author runs.
|
|
163
|
-
label: "review",
|
|
164
|
-
signal: overlap ? combineSignals(ctx.signal, overlap.abort.signal) : ctx.signal,
|
|
165
|
-
// A stalled tool call during review hangs the gate just like one during authoring —
|
|
166
|
-
// name it in the event feed while the quiet watchdog still counts down.
|
|
167
|
-
onToolCallStalled: (message) => warnEventBestEffort(root, role, message),
|
|
168
|
-
onToolCallStart: (toolName, args) => {
|
|
169
|
-
reviewerCalls.push({ toolName, args });
|
|
170
|
-
},
|
|
171
|
-
// A gate run that read an image its model cannot see is logged, not killed (plans/
|
|
172
|
-
// image-input.md "Detecting a blind read"): the reviewer keeps working from what it CAN
|
|
173
|
-
// see, and the image_blind line names the file, the model, and this run so the operator
|
|
174
|
-
// knows the diff was judged under a blind read.
|
|
175
|
-
onBlindRead: (path, action) => logEventBestEffort(root, {
|
|
176
|
-
source: role,
|
|
177
|
-
type: "image_blind",
|
|
178
|
-
run: ctx.runNumber,
|
|
179
|
-
path,
|
|
180
|
-
model: reviewCfg.model,
|
|
181
|
-
action,
|
|
182
|
-
}),
|
|
183
|
-
});
|
|
184
|
-
// A reviewer told the pre-check passed that re-ran the full suite anyway held the merge
|
|
185
|
-
// slot and loaded the shared host for a result the harness already had (BUGS.md 2026-09-23:
|
|
186
|
-
// scratch copies under /tmp, `npm ci` and `npm test` there). Warned on every outcome, abort
|
|
187
|
-
// included — the run happened either way; the verdict itself is not touched.
|
|
188
|
-
const rerun = suiteRerunWarning(reviewerCalls);
|
|
189
|
-
if (rerun)
|
|
190
|
-
warnEventBestEffort(root, role, rerun);
|
|
191
|
-
// Join the overlapped check (Fast merge part 12): a gate that did not pass settles the attempt;
|
|
192
|
-
// a green gate lets the reviewer's outcome stand, and a reviewer that finished first waited
|
|
193
|
-
// here for the gate.
|
|
194
|
-
if (overlap) {
|
|
195
|
-
if (!overlap.done)
|
|
196
|
-
setMergeStage(root, role, "build-check");
|
|
197
|
-
const pre = await overlap.gate;
|
|
198
|
-
if (pre.resolved)
|
|
199
|
-
return settleFailedCheck(overlap, pre, pi);
|
|
200
|
-
retry.pre = { verifiedHead: pre.verifiedHead, knownMainFailures: pre.knownMainFailures };
|
|
201
|
-
}
|
|
202
|
-
const verifiedHead = retry.pre?.verifiedHead;
|
|
203
|
-
if (pi.aborted) {
|
|
204
|
-
// Shutdown mid-review: fail closed — the commit stays on the branch and the
|
|
205
|
-
// resumed/following run re-reviews it via the combined ahead-of-main diff. Any resume
|
|
206
|
-
// marker from an earlier attempt is dropped: an abort says nothing about the diff, and the
|
|
207
|
-
// next attempt starts clean rather than inheriting a half-counted session.
|
|
208
|
-
state.reviewResume = undefined;
|
|
209
|
-
return keep(withRuns({ decision: "failed", aborted: true }, pi, null, null));
|
|
210
|
-
}
|
|
211
|
-
// The bounded nudge (BUGS.md 2026-10-02): the reviewer broke the no-re-run rule despite a
|
|
212
|
-
// green pre-check, so one turn on its own session names the call and asks it to finish
|
|
213
|
-
// without re-running. A repeat on the nudge's own tool calls fails the review — strike-free,
|
|
214
|
-
// commit kept, re-reviewed under the normal path — instead of paying a third suite run.
|
|
215
|
-
let nudge = null;
|
|
216
|
-
let nudgeRepeat = null;
|
|
217
|
-
if (rerun && pi.ok) {
|
|
218
|
-
const nudgeCalls = [];
|
|
219
|
-
nudge = await requestNoRerun(followupCtx, rerun, nudgeCalls);
|
|
220
|
-
if (nudge?.aborted) {
|
|
221
|
-
return keep(withRuns({ decision: "failed", aborted: true }, pi, null, nudge));
|
|
222
|
-
}
|
|
223
|
-
nudgeRepeat = nudge ? (suiteRerunWarning(nudgeCalls) ?? null) : null;
|
|
224
|
-
if (nudgeRepeat) {
|
|
225
|
-
warnEventBestEffort(root, role, `${nudgeRepeat} the repeat fails this review: the commit stays, and the merge re-reviews it without a third suite run`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
let verdict = parseVerdict(pi.verdictText ?? "");
|
|
229
|
-
// The nudge turn's reply can carry the verdict the review run lost — use it before spending
|
|
230
|
-
// any other recovery turn, but never on a repeat (the repeat fails the review regardless).
|
|
231
|
-
if (!verdict && nudge && !nudgeRepeat) {
|
|
232
|
-
const recovered = parseVerdict(nudge.verdictText ?? "");
|
|
233
|
-
if (recovered) {
|
|
234
|
-
verdict = recovered;
|
|
235
|
-
warnEventBestEffort(root, role, "the reviewer's reply had no VERDICT line — recovered it with the no-re-run nudge turn on its session");
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
// A run that FAILED (`ok` false: transport error, failed spawn, timeout) produced no reply,
|
|
239
|
-
// so no follow-up is attempted — it is evidence about the backend, and the strike-free
|
|
240
|
-
// branch below keeps it that way (BUGS.md 2026-09-20). Only a run that completed and
|
|
241
|
-
// replied without a parseable VERDICT earns the recovery turn: that is evidence about the
|
|
242
|
-
// reviewer's output format, not about the diff, and it is as recoverable as the author
|
|
243
|
-
// side's missing SUMMARY (BUGS.md 2026-09-29). The nudge turn is the bounded follow-up when
|
|
244
|
-
// the reviewer broke the no-re-run rule — one turn total, so a spent nudge never earns the
|
|
245
|
-
// verdict-recovery turn on top.
|
|
246
|
-
let followUp = null;
|
|
247
|
-
if (!verdict && pi.ok && !nudge) {
|
|
248
|
-
followUp = await requestVerdict(followupCtx);
|
|
249
|
-
if (followUp?.aborted)
|
|
250
|
-
return keep(withRuns({ decision: "failed", aborted: true }, pi, followUp, nudge));
|
|
251
|
-
const recovered = followUp ? parseVerdict(followUp.verdictText ?? "") : null;
|
|
252
|
-
if (recovered) {
|
|
253
|
-
verdict = recovered;
|
|
254
|
-
warnEventBestEffort(root, role, "the reviewer's reply had no VERDICT line — recovered it with a follow-up turn on its own session");
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// A repeat after the nudge (BUGS.md 2026-10-02): the reviewer re-ran the suite on the nudge
|
|
258
|
-
// turn too. Fail the review — strike-free like a dead backend (BUGS.md 2026-09-20): the
|
|
259
|
-
// commit stays for the next merge's re-review, the discard counter never advances, and no
|
|
260
|
-
// rejection is recorded against the author's diff.
|
|
261
|
-
if (nudgeRepeat) {
|
|
262
|
-
const message = `${nudgeRepeat} The review is failed on this repeat: the commit is kept, and the merge re-reviews it under the normal path.`;
|
|
263
|
-
logEventBestEffort(root, { source: role, type: "review_failed", head, message, attempt: retry.attempt, durationMs: Date.now() - reviewStartedAt });
|
|
264
|
-
state.reviewResume = undefined;
|
|
265
|
-
recordReview(state, "failed", [message], head);
|
|
266
|
-
return keep(withRuns({ decision: "failed", detail: message }, pi, followUp, nudge));
|
|
267
|
-
}
|
|
268
|
-
if (!verdict) {
|
|
269
|
-
return settleUnparsedReview(ctx, state, input, {
|
|
270
|
-
pi, followUp, nudge, resuming, reviewSessionDir, reviewStartedAt, reviewCfg, claudeConfigDir,
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
if (verdict.verdict === "reject") {
|
|
274
|
-
const rejected = await reject(verdict.reasons, Date.now() - reviewStartedAt);
|
|
275
|
-
return withRuns({ ...rejected }, pi, followUp, nudge);
|
|
276
|
-
}
|
|
277
|
-
// Approve: record the reviewed HEAD and discard any stray working-tree edits the reviewer
|
|
278
|
-
// made while reading around — its only output channel is the verdict. The patch-id keys the
|
|
279
|
-
// approval to the diff it judged; unreadable, it is not remembered.
|
|
280
|
-
state.lastApprovedHead = head;
|
|
281
|
-
if (reviewedPatchId !== undefined) {
|
|
282
|
-
const kept = (state.approvedPatchIds ?? []).filter((id) => id !== reviewedPatchId);
|
|
283
|
-
state.approvedPatchIds = [...kept, reviewedPatchId].slice(-APPROVED_PATCH_IDS_MAX);
|
|
284
|
-
}
|
|
285
|
-
state.reviewResume = undefined;
|
|
286
|
-
recordReview(state, "approve", verdict.reasons, head);
|
|
287
|
-
state.unreviewFailures = 0;
|
|
288
|
-
await restoreReviewedHead(wt, head, root, role);
|
|
289
|
-
logEventBestEffort(root, {
|
|
290
|
-
source: role,
|
|
291
|
-
type: "review_verdict",
|
|
292
|
-
head,
|
|
293
|
-
reason: verdict.reasons[0],
|
|
294
|
-
durationMs: Date.now() - reviewStartedAt,
|
|
295
|
-
});
|
|
296
|
-
return withRuns({ decision: "approved", verifiedHead }, pi, followUp, nudge);
|
|
297
|
-
}
|
|
298
|
-
/** Settle a review that produced no verdict: a dead backend keeps the commit (strike-free, retried
|
|
299
|
-
* in place while retries last), a reply that stayed unparseable counts a strike per HEAD and
|
|
300
|
-
* discards the leftover at REVIEW_FAILURE_LIMIT. */
|
|
301
|
-
async function settleUnparsedReview(ctx, state, input, outcome) {
|
|
302
|
-
const { root, role, wt, mainBranch } = ctx;
|
|
303
|
-
const { head, retry, reviewedPatchId, keep } = input;
|
|
304
|
-
const { pi, followUp, nudge, resuming, reviewSessionDir, reviewStartedAt, reviewCfg, claudeConfigDir } = outcome;
|
|
305
|
-
const followUpError = followUp && !followUp.ok ? followUp.errorMessage : undefined;
|
|
306
|
-
// The nudge turn's own death is backend evidence, exactly like the verdict-recovery
|
|
307
|
-
// follow-up's: a nudge that died with a transport error is the backend failing after the
|
|
308
|
-
// reviewer judged nothing — about the world, never about the diff — so the strike-free
|
|
309
|
-
// branch below must see it, and its error names the failure.
|
|
310
|
-
const nudgeError = nudge && !nudge.ok ? nudge.errorMessage : undefined;
|
|
311
|
-
// A progressing timeout of the REVIEW run itself (never a follow-up or nudge turn, both of
|
|
312
|
-
// which run only after pi ok) leaves the session for the next attempt while the patch is
|
|
313
|
-
// identifiable and the resume budget lasts. That is what makes the recorded failure honest:
|
|
314
|
-
// while a resume remains the next attempt continues this review, and once the budget is
|
|
315
|
-
// spent or the failure cannot resume, it reviews the change from scratch (BUGS.md
|
|
316
|
-
// 2026-09-30). A string rewrite of runPi's exact suffix, so any other cause — and the plain
|
|
317
|
-
// no-progress timeout, which promises nothing — passes through untouched.
|
|
318
|
-
const resumeAttempts = resuming ? state.reviewResume?.attempts ?? 0 : 0;
|
|
319
|
-
const resumeRemains = !pi.ok &&
|
|
320
|
-
!pi.aborted &&
|
|
321
|
-
pi.timedOutProgressing &&
|
|
322
|
-
reviewedPatchId !== undefined &&
|
|
323
|
-
resumeAttempts < REVIEW_RESUME_LIMIT &&
|
|
324
|
-
// The promise must be one the next attempt can keep: only a dir whose backend still
|
|
325
|
-
// holds a gate session to resume earns the marker (and the "continues this review"
|
|
326
|
-
// wording). Without this a backend that leaves no session file would record a resume the
|
|
327
|
-
// next attempt silently declines, reviewing from scratch anyway.
|
|
328
|
-
backendSessionResumable(reviewSessionDir, {
|
|
329
|
-
kind: "gate",
|
|
330
|
-
agent: reviewCfg.agent ?? "pi",
|
|
331
|
-
}, claudeConfigDir);
|
|
332
|
-
const reviewTimeoutRewrite = (message) => message.replace(" — session and worktree edits preserved for resume", resumeRemains
|
|
333
|
-
? " — the commit is kept; the next attempt continues this review"
|
|
334
|
-
: " — the commit is kept; the next attempt reviews it from scratch");
|
|
335
|
-
const message = reviewTimeoutRewrite(followUpError ??
|
|
336
|
-
nudgeError ??
|
|
337
|
-
pi.errorMessage ??
|
|
338
|
-
`no parseable VERDICT line in the reviewer's reply${nudge
|
|
339
|
-
? ", even after the no-re-run nudge turn on its session"
|
|
340
|
-
: followUp
|
|
341
|
-
? ", even after a follow-up turn on its session"
|
|
342
|
-
: ""}`);
|
|
343
|
-
logEventBestEffort(root, { source: role, type: "review_failed", head, message, attempt: retry.attempt, durationMs: Date.now() - reviewStartedAt });
|
|
344
|
-
// A dead reviewer must never destroy committed work (BUGS.md 2026-09-20): leave the commit
|
|
345
|
-
// for the next run's re-review and do not advance the per-HEAD discard counter. That
|
|
346
|
-
// holds for the review run itself (BUGS.md 2026-09-20), for the verdict-recovery
|
|
347
|
-
// follow-up (BUGS.md 2026-09-29), and for the no-re-run nudge turn (BUGS.md 2026-10-02):
|
|
348
|
-
// each one's death is the backend failing after the reviewer had already judged nothing —
|
|
349
|
-
// evidence about the world, never about the diff.
|
|
350
|
-
if (!pi.ok || (followUp && !followUp.ok) || (nudge && !nudge.ok)) {
|
|
351
|
-
if (resumeRemains) {
|
|
352
|
-
// Record the session this run used (the resumed directory, or this fresh run's own),
|
|
353
|
-
// so the next attempt `--continue`s exactly it.
|
|
354
|
-
state.reviewResume = {
|
|
355
|
-
patchId: reviewedPatchId,
|
|
356
|
-
sessionDir: reviewSessionDir,
|
|
357
|
-
attempts: resumeAttempts,
|
|
358
|
-
at: Date.now(),
|
|
359
|
-
};
|
|
360
|
-
saveRunnerStateBestEffort(root, state);
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
state.reviewResume = undefined;
|
|
364
|
-
}
|
|
365
|
-
const configError = pi.configError || Boolean(followUp?.configError) || Boolean(nudge?.configError);
|
|
366
|
-
// Retried in place (reviewAheadOfMain): the failure is about the world, so the verify tries
|
|
367
|
-
// the reviewer again before the attempt ends. A configuration error can never succeed.
|
|
368
|
-
if (retry.retriesLeft > 0 && !configError) {
|
|
369
|
-
return keep(withRuns({ decision: "failed", detail: message, retryable: true }, pi, followUp, nudge));
|
|
370
|
-
}
|
|
371
|
-
recordReview(state, "failed", [message], head);
|
|
372
|
-
return keep(withRuns({ decision: "failed", detail: message, ...(configError ? { configError: true } : {}) }, pi, followUp, nudge));
|
|
373
|
-
}
|
|
374
|
-
// Consecutive failures of THIS HEAD only: a new commit (new HEAD) starts fresh. Read
|
|
375
|
-
// *before* overwriting lastReview with this failure.
|
|
376
|
-
// Retry before counting: the strike below is per verify's final outcome, not per run.
|
|
377
|
-
if (retry.retriesLeft > 0) {
|
|
378
|
-
state.reviewResume = undefined;
|
|
379
|
-
return keep(withRuns({ decision: "failed", detail: message, retryable: true }, pi, followUp, nudge));
|
|
380
|
-
}
|
|
381
|
-
const prev = state.lastReview;
|
|
382
|
-
const sameHead = prev?.verdict === "failed" && prev.head === head;
|
|
383
|
-
state.reviewResume = undefined;
|
|
384
|
-
state.unreviewFailures = (sameHead ? (state.unreviewFailures ?? 0) : 0) + 1;
|
|
385
|
-
recordReview(state, "failed", [message], head);
|
|
386
|
-
let discarded = false;
|
|
387
|
-
if ((state.unreviewFailures ?? 0) >= REVIEW_FAILURE_LIMIT) {
|
|
388
|
-
await resetWorktreeToMain(wt, mainBranch);
|
|
389
|
-
state.unreviewFailures = 0; // The HEAD is gone; nothing left to count against.
|
|
390
|
-
discarded = true;
|
|
391
|
-
warnEventBestEffort(root, role, `discarding unreviewed leftover after ${REVIEW_FAILURE_LIMIT} failed reviews (${shortSha(head)})`);
|
|
392
|
-
}
|
|
393
|
-
const failed = { ...withRuns({ decision: "failed", detail: message }, pi, followUp, nudge), ...(discarded ? { discarded: true } : {}) };
|
|
394
|
-
return discarded ? failed : keep(failed);
|
|
395
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/** Consecutive provider-class failures that trip a fallback episode. */
|
|
2
|
-
export const MODEL_FALLBACK_FAILURES = 3;
|
|
3
|
-
/** The first probe cooldown after a trip; a failed probe doubles it. */
|
|
4
|
-
export const MODEL_FALLBACK_COOLDOWN_MS = 5 * 60_000;
|
|
5
|
-
/** The ceiling a repeatedly-failing probe's cooldown doubles toward. */
|
|
6
|
-
export const MODEL_FALLBACK_MAX_COOLDOWN_MS = 30 * 60_000;
|
|
7
|
-
/** True only while the role is in a fallback episode whose probe is still in the future — the
|
|
8
|
-
* runs that use the fallback pair. */
|
|
9
|
-
export function modelFallbackActive(state, now) {
|
|
10
|
-
return state !== undefined && state.since > 0 && now < state.probeAt;
|
|
11
|
-
}
|
|
12
|
-
/** True only while the role is in a fallback episode whose cooldown has elapsed: the next
|
|
13
|
-
* run uses the primary as the probe. */
|
|
14
|
-
export function modelFallbackProbe(state, now) {
|
|
15
|
-
return state !== undefined && state.since > 0 && now >= state.probeAt;
|
|
16
|
-
}
|
|
17
|
-
/** Classify one authoring agent run's verdict about the primary (src/run/run-usage.ts records the
|
|
18
|
-
* same flags for the team-wide hold). An agent run that was aborted, quiet-killed, or timed out
|
|
19
|
-
* — or that read an image its model cannot see (the runner killed it, plans/image-input.md) — is
|
|
20
|
-
* inconclusive even when it also reports provider evidence: the harness killed it, so it is
|
|
21
|
-
* no verdict about the primary's health. */
|
|
22
|
-
export function modelFallbackVerdict(pi) {
|
|
23
|
-
if (pi.aborted || pi.quietKilled || pi.timedOut || pi.imageBlind)
|
|
24
|
-
return "inconclusive";
|
|
25
|
-
if (!pi.ok && (pi.transientRateLimit || pi.transientBackend))
|
|
26
|
-
return "provider-failure";
|
|
27
|
-
return "answered";
|
|
28
|
-
}
|
|
29
|
-
/** Fold one authoring agent run's verdict into the role's fallback state, returning the next state
|
|
30
|
-
* or undefined for "running the primary" (no episode).
|
|
31
|
-
*
|
|
32
|
-
* - Not in fallback, provider failure: increment the consecutive count; the
|
|
33
|
-
* MODEL_FALLBACK_FAILURES-th trips the episode.
|
|
34
|
-
* - Not in fallback, an answering agent run: clear the running count (undefined).
|
|
35
|
-
* - Not in fallback, an inconclusive agent run: unchanged — a harness kill is not evidence the
|
|
36
|
-
* primary recovered, so the running count survives it.
|
|
37
|
-
* - In fallback, a fallback run (`probe` false): unchanged — its outcome neither returns
|
|
38
|
-
* the role to primary nor re-trips it.
|
|
39
|
-
* - In fallback, a probe run: an inconclusive probe is unchanged (retried on the next run);
|
|
40
|
-
* an answering probe clears the episode; a provider-class failed probe doubles the cooldown
|
|
41
|
-
* (capped) and schedules the next one.
|
|
42
|
-
*/
|
|
43
|
-
export function recordModelFallback(state, outcome) {
|
|
44
|
-
const { verdict, probe, now, reason } = outcome;
|
|
45
|
-
if (state !== undefined && state.since > 0) {
|
|
46
|
-
if (!probe)
|
|
47
|
-
return state;
|
|
48
|
-
if (verdict === "inconclusive")
|
|
49
|
-
return state;
|
|
50
|
-
if (verdict === "answered")
|
|
51
|
-
return undefined;
|
|
52
|
-
const cooldownMs = Math.min(state.cooldownMs * 2, MODEL_FALLBACK_MAX_COOLDOWN_MS);
|
|
53
|
-
return { ...state, cooldownMs, probeAt: now + cooldownMs, reason: reason || state.reason };
|
|
54
|
-
}
|
|
55
|
-
if (verdict === "inconclusive")
|
|
56
|
-
return state;
|
|
57
|
-
if (verdict === "answered")
|
|
58
|
-
return undefined;
|
|
59
|
-
const failures = (state?.failures ?? 0) + 1;
|
|
60
|
-
if (failures >= MODEL_FALLBACK_FAILURES) {
|
|
61
|
-
return {
|
|
62
|
-
failures: 0,
|
|
63
|
-
since: now,
|
|
64
|
-
probeAt: now + MODEL_FALLBACK_COOLDOWN_MS,
|
|
65
|
-
cooldownMs: MODEL_FALLBACK_COOLDOWN_MS,
|
|
66
|
-
reason: reason || "backend failure",
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
return { failures, since: 0, probeAt: 0, cooldownMs: 0, reason };
|
|
70
|
-
}
|
|
71
|
-
/** The reason label one agent run's provider evidence yields: "rate-limit" for a 429, else the
|
|
72
|
-
* backend-failure kind. Exported so the runner's evidence assembly and tests share one
|
|
73
|
-
* spelling. */
|
|
74
|
-
export function providerFailureReason(rateLimit, kind) {
|
|
75
|
-
return rateLimit ? "rate-limit" : (kind ?? "backend failure");
|
|
76
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { logEventBestEffort } from "../events/events.js";
|
|
2
|
-
import { modelFallbackVerdict, providerFailureReason, recordModelFallback } from "./model-fallback.js";
|
|
3
|
-
/** Fold one AUTHORING agent run's verdict into the role's model-fallback state
|
|
4
|
-
* (src/runner/model-fallback.ts) and emit the episode's started/ended events. Only the agent run
|
|
5
|
-
* that actually happened is evidence: a run that never invoked pi (a skipped director
|
|
6
|
-
* inbox, a recovered leftover, a merge-side `main_red` pin) returns from runOnce before this is
|
|
7
|
-
* reached, and modelFallbackVerdict reports an aborted/killed/timed-out agent run as
|
|
8
|
-
* inconclusive — so neither can end an episode without the primary having answered.
|
|
9
|
-
* A role whose tier resolves to no fallback pair never trips, and an episode persisted from a
|
|
10
|
-
* config that HAD a pair is cleared when a live edit removes it (or sets it to "pause"), so a
|
|
11
|
-
* stale "on fallback" mark cannot outlive the config that created it. Persists on any state
|
|
12
|
-
* change; the run's own finalize save carries it too. */
|
|
13
|
-
export function foldModelFallbackPhase(ctx, fallbackCtx, pi) {
|
|
14
|
-
const s = ctx.state;
|
|
15
|
-
const prior = s.modelFallback;
|
|
16
|
-
const now = Date.now();
|
|
17
|
-
const verdict = modelFallbackVerdict(pi);
|
|
18
|
-
const reason = providerFailureReason(pi.transientRateLimit, pi.backendKind);
|
|
19
|
-
const wasIn = prior !== undefined && prior.since > 0;
|
|
20
|
-
if (fallbackCtx.fallback === null) {
|
|
21
|
-
// No fallback pair resolves any more: the feature is off for this role. End a persisted
|
|
22
|
-
// episode (the primary just ran, so this run IS the return), and drop any pre-trip count
|
|
23
|
-
// so a later config starts from zero. A killed/aborted/timed-out agent run is no verdict, so it
|
|
24
|
-
// leaves the episode in place exactly as it does on a configured fallback.
|
|
25
|
-
if (prior === undefined)
|
|
26
|
-
return;
|
|
27
|
-
if (wasIn && verdict === "inconclusive")
|
|
28
|
-
return;
|
|
29
|
-
emitFallbackEnded(ctx, prior, fallbackCtx.primary, now, wasIn);
|
|
30
|
-
s.modelFallback = undefined;
|
|
31
|
-
ctx.save();
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const next = recordModelFallback(prior, { verdict, probe: fallbackCtx.probe, now, reason });
|
|
35
|
-
const willBeIn = next !== undefined && next.since > 0;
|
|
36
|
-
if (!wasIn && willBeIn) {
|
|
37
|
-
logEventBestEffort(ctx.root, {
|
|
38
|
-
source: ctx.role,
|
|
39
|
-
type: "model_fallback_started",
|
|
40
|
-
provider: fallbackCtx.fallback.provider,
|
|
41
|
-
model: fallbackCtx.fallback.model,
|
|
42
|
-
reason: next?.reason,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
else if (wasIn && !willBeIn && prior !== undefined) {
|
|
46
|
-
emitFallbackEnded(ctx, prior, fallbackCtx.primary, now, wasIn);
|
|
47
|
-
}
|
|
48
|
-
if (next !== prior) {
|
|
49
|
-
s.modelFallback = next;
|
|
50
|
-
ctx.save();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/** Log the model_fallback_ended event for an episode that was active. Split out so the two
|
|
54
|
-
* ending paths — a successful probe and a config that dropped the pair — name the primary
|
|
55
|
-
* pair and duration the same way. A pre-trip state (no episode) has no ended event. */
|
|
56
|
-
function emitFallbackEnded(ctx, prior, primary, now, wasIn) {
|
|
57
|
-
if (!wasIn)
|
|
58
|
-
return;
|
|
59
|
-
logEventBestEffort(ctx.root, {
|
|
60
|
-
source: ctx.role,
|
|
61
|
-
type: "model_fallback_ended",
|
|
62
|
-
provider: primary.provider,
|
|
63
|
-
model: primary.model,
|
|
64
|
-
durationMs: now - (prior.since || now),
|
|
65
|
-
});
|
|
66
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { loadConfigSafe } from "../../config/config.js";
|
|
2
|
-
import { createTeamConfigKeyEditor } from "./tui-editor-config.js";
|
|
3
|
-
/** The Ctrl+Y claude subscription-window reserve editor on the Activity view's shared prompt
|
|
4
|
-
* line — the terminal twin of the dashboard Settings `Claude window reserve
|
|
5
|
-
* (backends.claude.windowReserve)` field and `tumwater config set claudeWindowReserve`. Enter
|
|
6
|
-
* writes the top-level `claudeWindowReserve`
|
|
7
|
-
* through the shared setConfigKey/unsetConfigKey family, so validateConfig's claudeWindowReserve
|
|
8
|
-
* block owns the value's truth. The team scope is the only scope, so the editor takes the line
|
|
9
|
-
* on the Activity view (view 0) alone; a History pane keeps Ctrl+Y for its per-role Summary
|
|
10
|
-
* toggle, which runs after this arm. */
|
|
11
|
-
export function createClaudeReserveEditor(session) {
|
|
12
|
-
return createTeamConfigKeyEditor(session, {
|
|
13
|
-
mode: "claudeReserve",
|
|
14
|
-
openCtrl: "y",
|
|
15
|
-
configKey: "claudeWindowReserve",
|
|
16
|
-
prefill: (s) => {
|
|
17
|
-
const existing = loadConfigSafe(s.root).config?.claudeWindowReserve;
|
|
18
|
-
return existing !== undefined ? JSON.stringify(existing) : "";
|
|
19
|
-
},
|
|
20
|
-
openFlash: "edit the team's claude subscription-window reserve (backends.claude.windowReserve, a JSON object like {\"seven_day\":0.8}; empty clears): Enter to save, Esc to cancel",
|
|
21
|
-
clearedFlash: "claude window reserve cleared",
|
|
22
|
-
setFlash: (text) => `claude window reserve set to ${text}`,
|
|
23
|
-
});
|
|
24
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { loadConfigSafe } from "../../config/config.js";
|
|
2
|
-
import { createTeamConfigKeyEditor } from "./tui-editor-config.js";
|
|
3
|
-
/** The Ctrl+P provider-concurrency-caps editor on the Activity view's shared prompt line — the
|
|
4
|
-
* terminal twin of the dashboard Settings `Provider concurrency caps (JSON)` field and
|
|
5
|
-
* `tumwater config set maxConcurrentPerProvider`, completing the run-concurrency knob set
|
|
6
|
-
* `tumwater doctor`'s checkProviderContention points at. Enter writes the top-level
|
|
7
|
-
* `maxConcurrentPerProvider` through the shared setConfigKey/unsetConfigKey family the CLI and
|
|
8
|
-
* the dashboard use — so setConfigKey JSON-parses the typed text and validateConfig's map block
|
|
9
|
-
* owns the value's truth — and Esc, Ctrl+P again, and Ctrl+T cancel by restoring the prompt
|
|
10
|
-
* draft. Mutually exclusive with the other editors on the one prompt line via the session's
|
|
11
|
-
* held registry. The Team scope is the only scope, so the editor takes the line on the Activity
|
|
12
|
-
* view (view 0) alone; a role's Transcript keeps Ctrl+P for its pause editor, which runs after
|
|
13
|
-
* this arm. */
|
|
14
|
-
export function createProviderCapsEditor(session) {
|
|
15
|
-
return createTeamConfigKeyEditor(session, {
|
|
16
|
-
mode: "providerCaps",
|
|
17
|
-
openCtrl: "p",
|
|
18
|
-
configKey: "maxConcurrentPerProvider",
|
|
19
|
-
prefill: (s) => {
|
|
20
|
-
const existing = loadConfigSafe(s.root).config?.maxConcurrentPerProvider;
|
|
21
|
-
return existing !== undefined ? JSON.stringify(existing) : "";
|
|
22
|
-
},
|
|
23
|
-
openFlash: "edit the team's provider concurrency caps (maxConcurrentPerProvider, a JSON object like {\"anthropic\":2}; empty clears): Enter to save, Esc to cancel",
|
|
24
|
-
clearedFlash: "provider concurrency caps cleared",
|
|
25
|
-
setFlash: (text) => `provider concurrency caps set to ${text}`,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|