tumwater 0.1.2 → 0.2.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 +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** The review gate's deterministic build pre-check — split out of review.ts so the gate keeps
|
|
2
3
|
* one responsibility (running the model reviewer and acting on its verdict) while this module
|
|
3
4
|
* owns the pre-review verification: the one scoped build-check run, its flake re-run, the
|
|
@@ -13,7 +14,7 @@ import { sampleSleepClock } from "../build/host-sleep.js";
|
|
|
13
14
|
import { recordReview } from "../tick/tick-apply.js";
|
|
14
15
|
import { mainRedNotMine } from "../text/phrases.js";
|
|
15
16
|
import { shortSha } from "../text/format.js";
|
|
16
|
-
import { checkWaitStage,
|
|
17
|
+
import { checkWaitStage, setMergeStage } from "../merge/merge-slot.js";
|
|
17
18
|
import { mainTipVerdict } from "../baseline/main-red.js";
|
|
18
19
|
import { FLAKY_WARNING_PREFIX, isKnownFlake } from "./known-flakes.js";
|
|
19
20
|
/** Run the gate's deterministic build pre-check. `reject` is the gate's one shared rejection
|
|
@@ -22,10 +23,10 @@ import { FLAKY_WARNING_PREFIX, isKnownFlake } from "./known-flakes.js";
|
|
|
22
23
|
* model-verdict path. */
|
|
23
24
|
export async function gateBuildPrecheck(ctx, state, head, reject) {
|
|
24
25
|
const { root, role, wt, mainBranch, config } = ctx;
|
|
25
|
-
// The
|
|
26
|
+
// The merge cell's stage (a no-op outside a queued merge — see setMergeStage): the
|
|
26
27
|
// pre-check, its one re-run, and any attribution check behind them are the gate's first
|
|
27
28
|
// long phase.
|
|
28
|
-
|
|
29
|
+
setMergeStage(root, role, "build-check");
|
|
29
30
|
// The gate's one check invocation — the pre-check and its one flake re-run below are the
|
|
30
31
|
// same run in the same scope, worktree, and config with the same resolved timeout, so the
|
|
31
32
|
// scope and timeout resolution live here once and cannot drift between the two calls.
|
|
@@ -65,9 +66,9 @@ export async function gateBuildPrecheck(ctx, state, head, reject) {
|
|
|
65
66
|
// The run spanned a host sleep past the tolerance and no clean attempt judged the tree
|
|
66
67
|
// (runScopedBuildCheck already retried once): nothing here is the change's failure, so
|
|
67
68
|
// the gate attributes nothing — it keeps the commit and reports the sleep, and the
|
|
68
|
-
//
|
|
69
|
+
// merge path keeps the pin for a re-merge (BUGS.md 2026-09-30).
|
|
69
70
|
const detail = sleptPhrase("gate check", outcome.run.sleptMs, UNVERIFIED_TREE);
|
|
70
|
-
warnEventBestEffort(root, role, `${detail}; keeping the commit for
|
|
71
|
+
warnEventBestEffort(root, role, `${detail}; keeping the commit for another merge attempt`);
|
|
71
72
|
return { resolved: { decision: "failed", detail, unverified: true } };
|
|
72
73
|
}
|
|
73
74
|
if (outcome.status === "failed") {
|
|
@@ -75,23 +76,23 @@ export async function gateBuildPrecheck(ctx, state, head, reject) {
|
|
|
75
76
|
// re-run passed on this exact headline) says nothing about this change: attribute nothing
|
|
76
77
|
// — keep the commit and the pin, count no strike — and name the flake, exactly as the
|
|
77
78
|
// host-sleep path above does. Before this, the repeat was attributed through main's
|
|
78
|
-
// per-SHA baseline cache (usually a green seeded by an earlier
|
|
79
|
+
// per-SHA baseline cache (usually a green seeded by an earlier merge), so a load- or
|
|
79
80
|
// clock-sensitive test rejected whichever unrelated change hit it (BUGS.md 2026-10-06).
|
|
80
81
|
const flaky = failureHeadline(outcome.outputTail) ?? describeCheck(check);
|
|
81
82
|
if (isKnownFlake(root, flaky)) {
|
|
82
|
-
const detail = `known-flaky failure,
|
|
83
|
+
const detail = `known-flaky failure, merge kept: ${flaky}`;
|
|
83
84
|
recordReview(state, "failed", [detail], head);
|
|
84
85
|
warnEventBestEffort(root, role, detail);
|
|
85
86
|
return { resolved: { decision: "failed", detail, unverified: true } };
|
|
86
87
|
}
|
|
87
88
|
const reasons = checkFailureReasons(check, outcome);
|
|
88
|
-
// A failure that reproduced is attributed, never fixed here: the one
|
|
89
|
+
// A failure that reproduced is attributed, never fixed here: the one merge slot the
|
|
89
90
|
// whole queue waits on is no place for a model run (the in-slot build-fix run this
|
|
90
|
-
// replaces held it for hours and never once led to a
|
|
91
|
+
// replaces held it for hours and never once led to a merge — PLANS.md "Land-queue
|
|
91
92
|
// speed 1/3"). The question is whose failure it is, and main's own verdict at its tip
|
|
92
93
|
// answers it. A failure matching no recorded flake bypasses the per-SHA cache and runs
|
|
93
94
|
// main's check now (forceFresh): a clock- or load-sensitive test can pass on an immutable
|
|
94
|
-
// SHA early and fail later, so a cached green — usually seeded by an earlier
|
|
95
|
+
// SHA early and fail later, so a cached green — usually seeded by an earlier merge —
|
|
95
96
|
// may no longer be true, and trusting it rejected an unrelated change (BUGS.md
|
|
96
97
|
// 2026-10-06). A shutdown already under way skips the question and fails closed like any
|
|
97
98
|
// abort.
|
|
@@ -105,10 +106,10 @@ export async function gateBuildPrecheck(ctx, state, head, reject) {
|
|
|
105
106
|
// Main fails too: not this change's failure. Leave the commit (and with it the pin) and
|
|
106
107
|
// do not advance the discard counter — the transport-failure rule (BUGS.md 2026-09-20):
|
|
107
108
|
// nothing judged this diff. main-red.ts's gate and the bugfix handoff own the repair,
|
|
108
|
-
// and the author's next tick re-
|
|
109
|
+
// and the author's next tick re-merges the change once main is green again.
|
|
109
110
|
const detail = mainRedNotMine(main.sha);
|
|
110
111
|
recordReview(state, "failed", [detail], head);
|
|
111
|
-
warnEventBestEffort(root, role, `gate check failed on ${shortSha(head)}, but ${detail};
|
|
112
|
+
warnEventBestEffort(root, role, `gate check failed on ${shortSha(head)}, but ${detail}; merge kept`);
|
|
112
113
|
return { resolved: { decision: "failed", detail, mainRed: true } };
|
|
113
114
|
}
|
|
114
115
|
// Main green: the change broke the check — rejected deterministically through the shared
|
|
@@ -123,7 +124,7 @@ export async function gateBuildPrecheck(ctx, state, head, reject) {
|
|
|
123
124
|
}
|
|
124
125
|
if (outcome.status === "passed") {
|
|
125
126
|
// Passed: this exact tree just went green under the project's own declared check. Hand
|
|
126
|
-
// the verdict to the
|
|
127
|
+
// the verdict to the merge path via verifiedHead — it seeds the red-main baseline with
|
|
127
128
|
// the SHA that actually becomes main (the rebased head, which may differ from `head`),
|
|
128
129
|
// so every role's next fresh tick is a cache hit instead of one redundant full-suite
|
|
129
130
|
// re-run on an already-verified tree.
|
|
@@ -7,6 +7,20 @@ import { verdictLines } from "../verdict/reply-contract.js";
|
|
|
7
7
|
import { clipReason } from "../build/build-check-report.js";
|
|
8
8
|
/** Cap on recorded reasons, so a chatty reviewer cannot bloat persisted state. */
|
|
9
9
|
const MAX_REASONS = 10;
|
|
10
|
+
/** The prior-objection number a reviewer reason is tagged with at its start — `(prior N)`, N
|
|
11
|
+
* the objection's 1-based number in the prior list the re-review prompt showed — or null when
|
|
12
|
+
* the reason is a newly raised (untagged) objection. The repeat-finality policy (BUGS.md
|
|
13
|
+
* 2026-10-14) asks a re-review to restate a still-open prior objection with this tag so the
|
|
14
|
+
* harness can tell a repeat from a new finding and end a chain whose objection has stopped
|
|
15
|
+
* converging. Anything else that merely begins with the word "prior" (e.g. "prior to the
|
|
16
|
+
* change…") is not a tag and reads as an untagged reason. */
|
|
17
|
+
export function priorObjectionNumber(reason) {
|
|
18
|
+
const m = /^\(prior\s+(\d+)\)/i.exec(reason.trim());
|
|
19
|
+
if (!m)
|
|
20
|
+
return null;
|
|
21
|
+
const n = Number(m[1]);
|
|
22
|
+
return Number.isInteger(n) && n >= 1 ? n : null;
|
|
23
|
+
}
|
|
10
24
|
/** Parse the reviewer's reply: the LAST VERDICT line wins (the prompt asks for exactly one),
|
|
11
25
|
* followed by its reasons — numbered/bulleted lines first, any other non-empty prose as a
|
|
12
26
|
* fallback. Null when no parseable verdict exists: that is a FAILED review, never an approval
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { REVIEW_RESUME_LIMIT, saveLoopState } from "../loop/loop-state.js";
|
|
2
|
-
import { modelSelectorField, reviewRunConfig } from "../config/config-views.js";
|
|
2
|
+
import { modelSelectorField, resolvedAgentConfig, reviewRunConfig } from "../config/config-views.js";
|
|
3
3
|
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
4
4
|
import { git } from "../git/git-run.js";
|
|
5
5
|
import { changeBaseRev, headOf, patchId } from "../git/git.js";
|
|
@@ -16,16 +16,18 @@ import { shortSha } from "../text/format.js";
|
|
|
16
16
|
import { firstReason } from "../text/phrases.js";
|
|
17
17
|
import { isExemptDiff } from "./exemptions.js";
|
|
18
18
|
import { falseFixReason } from "../verdict/fix-claim.js";
|
|
19
|
-
import { conflictMarkerReason } from "../
|
|
19
|
+
import { conflictMarkerReason } from "../merge/merge-git.js";
|
|
20
|
+
import { protectedEditReason } from "./protected-edits.js";
|
|
21
|
+
import { secretLeakReason } from "./secret-leaks.js";
|
|
20
22
|
import { backlogStructureReason } from "../backlog/backlog-structure.js";
|
|
21
23
|
import { suiteRerunWarning } from "./suite-rerun.js";
|
|
22
|
-
import {
|
|
24
|
+
import { setMergeStage } from "../merge/merge-slot.js";
|
|
23
25
|
import { gateBuildPrecheck } from "./review-precheck.js";
|
|
24
26
|
/** Attach the review gate's pi runs onto a partial GateResult — `run` plus the optional
|
|
25
27
|
* verdict-recovery follow-up (`followUpRun`) and no-re-run nudge (`nudgeRun`) fields, each
|
|
26
28
|
* present only when its turn actually ran. Spelled once so every return site of
|
|
27
29
|
* reviewAheadOfMain that carries runs shares one shape for the usage-folding caller
|
|
28
|
-
* (
|
|
30
|
+
* (merge-core.ts folds run, then followUpRun, then nudgeRun), instead of one conditional
|
|
29
31
|
* spread per site that a new run kind would have to update everywhere. */
|
|
30
32
|
function withRuns(r, run, followUp, nudge) {
|
|
31
33
|
return {
|
|
@@ -35,6 +37,15 @@ function withRuns(r, run, followUp, nudge) {
|
|
|
35
37
|
...(nudge ? { nudgeRun: nudge } : {}),
|
|
36
38
|
};
|
|
37
39
|
}
|
|
40
|
+
/** Put the worktree back on the exact head the gate reviewed: the reviewer's only output is its
|
|
41
|
+
* verdict, so a commit it made must never reach the merge pin or the next re-review. */
|
|
42
|
+
async function restoreReviewedHead(wt, head, root, role) {
|
|
43
|
+
const now = await headOf(wt, "HEAD").catch(() => null);
|
|
44
|
+
if (now && now !== head) {
|
|
45
|
+
warnEventBestEffort(root, role, `the reviewer moved HEAD to ${shortSha(now)} during review; restored the reviewed head ${shortSha(head)}`);
|
|
46
|
+
}
|
|
47
|
+
await git(wt, "reset", "--hard", head);
|
|
48
|
+
}
|
|
38
49
|
/** Consecutive failed reviews of one branch HEAD after which the leftover is discarded with
|
|
39
50
|
* a warning: a misconfigured reviewer model must not be able to wedge a loop into re-reviewing
|
|
40
51
|
* the same commit forever. */
|
|
@@ -84,7 +95,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
84
95
|
state.unreviewFailures = 0;
|
|
85
96
|
await resetWorktreeToMain(wt, mainBranch);
|
|
86
97
|
logEventBestEffort(root, {
|
|
87
|
-
|
|
98
|
+
source: role,
|
|
88
99
|
type: "review_rejected",
|
|
89
100
|
head,
|
|
90
101
|
reasons,
|
|
@@ -99,23 +110,32 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
99
110
|
}
|
|
100
111
|
const files = await aheadOfMainFiles(wt, mainBranch);
|
|
101
112
|
// A change handed back to its author with the conflict markers left in place (PLANS.md
|
|
102
|
-
// "Robust conflict landing, part 2/2") must not
|
|
113
|
+
// "Robust conflict landing, part 2/2") must not merge raw `<<<<<<<` lines. The stage check
|
|
103
114
|
// flags them for one fix-up turn, but only this deterministic gate rejection makes it a
|
|
104
115
|
// hard stop — an md-only diff skips the reviewer entirely, so nothing else would catch it.
|
|
105
116
|
const markerReason = conflictMarkerReason(wt, files);
|
|
106
117
|
if (markerReason !== undefined)
|
|
107
118
|
return reject([markerReason]);
|
|
119
|
+
// The prompt block and PRINCIPLES.md are markdown, so the exempt fast path below would merge
|
|
120
|
+
// an edit to either unreviewed; reject it here, for md-only and code diffs alike.
|
|
121
|
+
const protectedReason = await protectedEditReason(wt, mainBranch, files, role);
|
|
122
|
+
if (protectedReason !== undefined)
|
|
123
|
+
return reject([protectedReason]);
|
|
124
|
+
// A leaked credential is just as invisible to the exempt fast path: reject it here too.
|
|
125
|
+
const secretReason = await secretLeakReason(wt, mainBranch, files);
|
|
126
|
+
if (secretReason !== undefined)
|
|
127
|
+
return reject([secretReason]);
|
|
108
128
|
if (isExemptDiff(files, config.review.exemptPaths)) {
|
|
109
129
|
// The exemption is a fast path, not a blind eye: an md-only diff that moves a bug to
|
|
110
130
|
// Fixed must have its fix's symbols on this tree, or it records code that does not
|
|
111
|
-
// exist (BUGS.md 2026-09-22 — 9cea8c3
|
|
131
|
+
// exist (BUGS.md 2026-09-22 — 9cea8c3 merged a Fix paragraph naming runScriptGroup
|
|
112
132
|
// and signalTree with no source behind it). A deterministic rejection, like the
|
|
113
133
|
// pre-check below: no pi run consumed, reasons injected into the author's next tick.
|
|
114
134
|
const falseFix = await falseFixReason(wt, mainBranch, files);
|
|
115
135
|
if (falseFix)
|
|
116
136
|
return reject([falseFix]);
|
|
117
|
-
// Same cross-check as the
|
|
118
|
-
// duplicates or drops a `## ` section heading would
|
|
137
|
+
// Same cross-check as the merge path (backlog-structure.ts): an md-only diff that
|
|
138
|
+
// duplicates or drops a `## ` section heading would merge unreviewed — md-only edits skip
|
|
119
139
|
// the reviewer by design, and a malformed backlog stays invisible until an entry ends up
|
|
120
140
|
// on the wrong side of a heading (PLANS.md 2026-09-25, 52cbadd1).
|
|
121
141
|
const structure = await backlogStructureReason(wt, mainBranch, files);
|
|
@@ -126,7 +146,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
126
146
|
}
|
|
127
147
|
// The backlog-structure check for code diffs too, before the build pre-check: a change that
|
|
128
148
|
// duplicates or drops a `## ` section heading in PLANS.md, BUGS.md, or QUESTIONS.md cannot
|
|
129
|
-
//
|
|
149
|
+
// merge, and the rejection is deterministic — no pi run and no check run spent on a tree the
|
|
130
150
|
// heading set already condemns.
|
|
131
151
|
const structure = await backlogStructureReason(wt, mainBranch, files);
|
|
132
152
|
if (structure)
|
|
@@ -136,9 +156,9 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
136
156
|
// "reviewing": a deterministic rejection never shows as reviewing on the dashboards. Both
|
|
137
157
|
// gate callers (the tick path and leftover.ts's recoverLeftover) get it for free. Its run,
|
|
138
158
|
// flake re-run, and attribution live in review-precheck.ts (the check run itself lives in
|
|
139
|
-
// runScopedBuildCheck, shared with the
|
|
140
|
-
// here is returned verbatim and a pass hands the verified head to the
|
|
141
|
-
// The pre-check sets the
|
|
159
|
+
// runScopedBuildCheck, shared with the merge path's in-lock re-check); a resolved result
|
|
160
|
+
// here is returned verbatim and a pass hands the verified head to the merge path below.
|
|
161
|
+
// The pre-check sets the merge cell's stage (a no-op outside a queued merge) —
|
|
142
162
|
// "build-check" while it runs, "reviewing" when the reviewer run starts below.
|
|
143
163
|
const pre = await gateBuildPrecheck(ctx, state, head, reject);
|
|
144
164
|
if (pre.resolved) {
|
|
@@ -163,6 +183,10 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
163
183
|
// same config the run below gets, so the event names what the reviewer actually runs on —
|
|
164
184
|
// the budget fallback included. Omitted when no model is configured (pi's own default).
|
|
165
185
|
const reviewCfg = reviewRunConfig(config);
|
|
186
|
+
// The team claude config dir (plans/notes-command.md "Keeping funes out of team runs"), so
|
|
187
|
+
// a claude review's resume guard looks under the same dir the review run writes its transcript
|
|
188
|
+
// to — a team that set one must resume under it, not the default ~/.claude.
|
|
189
|
+
const claudeConfigDir = resolvedAgentConfig(config, "claude").configDir;
|
|
166
190
|
// A progressing timeout left this run's session for the next attempt (LoopState.reviewResume):
|
|
167
191
|
// resume it only while the patch is unchanged (the same diff), the resume budget lasts, and
|
|
168
192
|
// the directory still holds a gate session to `--continue`. Otherwise the stale marker is
|
|
@@ -174,7 +198,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
174
198
|
agentSessionResumable(priorResume.sessionDir, {
|
|
175
199
|
kind: "gate",
|
|
176
200
|
agent: reviewCfg.agent ?? "pi",
|
|
177
|
-
});
|
|
201
|
+
}, claudeConfigDir);
|
|
178
202
|
if (resuming) {
|
|
179
203
|
// Count the resume before the run: a crash must not let the same session be resumed
|
|
180
204
|
// forever without ever spending the budget.
|
|
@@ -194,7 +218,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
194
218
|
// needs the override.
|
|
195
219
|
const followupCtx = reviewSessionDir === ctx.sessionDir ? ctx : { ...ctx, sessionDir: reviewSessionDir };
|
|
196
220
|
logEventBestEffort(root, {
|
|
197
|
-
|
|
221
|
+
source: role,
|
|
198
222
|
type: "review_start",
|
|
199
223
|
head,
|
|
200
224
|
...(ctx.revisionRound !== undefined ? { revision: ctx.revisionRound } : {}),
|
|
@@ -207,8 +231,8 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
207
231
|
state.phase = "review";
|
|
208
232
|
saveLoopState(root, state);
|
|
209
233
|
// Freeze the revision the diff is measured from once, before the run: main moves while a
|
|
210
|
-
//
|
|
211
|
-
// just-
|
|
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
|
|
212
236
|
// and tells the reviewer to compare against it instead.
|
|
213
237
|
const base = await changeBaseRev(wt, mainBranch);
|
|
214
238
|
const diff = await aheadOfMainDiff(wt, mainBranch);
|
|
@@ -224,17 +248,17 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
224
248
|
}
|
|
225
249
|
: undefined;
|
|
226
250
|
// The reviewer run's wall time rides on its verdict event: a reviewer that takes an hour per
|
|
227
|
-
// merge on local hardware is a
|
|
251
|
+
// merge on local hardware is a team-level cost an operator must be able to see.
|
|
228
252
|
const reviewStartedAt = Date.now();
|
|
229
253
|
// The reviewer's started tool calls, collected only when the no-re-run rule stands (a verified
|
|
230
254
|
// pre-check) — the one case the suite-rerun tripwire below can fire.
|
|
231
255
|
const reviewerCalls = [];
|
|
232
|
-
// The
|
|
233
|
-
//
|
|
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
|
|
234
258
|
// line resets the gate progress accumulator, so the cell never shows a previous run's counts.
|
|
235
|
-
|
|
259
|
+
setMergeStage(root, role, "reviewing");
|
|
236
260
|
// The author's claimed WHY/RISK/VERIFIED ride along when present — checking those claims
|
|
237
|
-
// against the actual diff is exactly the adversarial angle (recovery
|
|
261
|
+
// against the actual diff is exactly the adversarial angle (recovery merges reconstruct
|
|
238
262
|
// them from the pinned commit's message).
|
|
239
263
|
const pi = await ctx.runGatePi({
|
|
240
264
|
cwd: wt,
|
|
@@ -244,8 +268,8 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
244
268
|
: buildReviewPrompt(diff, summary, commitBody, readPrinciples(root), highFriction, verifiedByHarness, base, undefined, priorReview),
|
|
245
269
|
// The reviewer runs on its own time budget (review.timeoutSeconds), never longer than a
|
|
246
270
|
// tick's: a timed-out review is a FAILED run (pi.ok false), so it takes the dead-backend
|
|
247
|
-
// path below — commit kept, no strike — and re-
|
|
248
|
-
// instead of holding the
|
|
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.
|
|
249
273
|
config: reviewCfg,
|
|
250
274
|
// This run's own session directory (per-run, keyed by tick and pinned sha): a follow-up
|
|
251
275
|
// turn, or a later attempt that resumes this review, continues exactly this run instead
|
|
@@ -269,21 +293,38 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
269
293
|
reviewerCalls.push({ toolName, args });
|
|
270
294
|
}
|
|
271
295
|
: undefined,
|
|
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
|
+
}),
|
|
272
308
|
});
|
|
273
|
-
// A reviewer told the pre-check passed that re-ran the full suite anyway held the
|
|
309
|
+
// A reviewer told the pre-check passed that re-ran the full suite anyway held the merge
|
|
274
310
|
// slot and loaded the shared host for a result the harness already had (BUGS.md 2026-09-23:
|
|
275
311
|
// scratch copies under /tmp, `npm ci` and `npm test` there). Warned on every outcome, abort
|
|
276
312
|
// included — the run happened either way; the verdict itself is not touched.
|
|
277
313
|
const rerun = suiteRerunWarning(reviewerCalls);
|
|
278
314
|
if (rerun)
|
|
279
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
|
+
};
|
|
280
321
|
if (pi.aborted) {
|
|
281
322
|
// Shutdown mid-review: fail closed — the commit stays on the branch and the
|
|
282
323
|
// resumed/following tick re-reviews it via the combined ahead-of-main diff. Any resume
|
|
283
324
|
// marker from an earlier attempt is dropped: an abort says nothing about the diff, and the
|
|
284
325
|
// next attempt starts clean rather than inheriting a half-counted session.
|
|
285
326
|
state.reviewResume = undefined;
|
|
286
|
-
return withRuns({ decision: "failed", aborted: true }, pi, null, null);
|
|
327
|
+
return keep(withRuns({ decision: "failed", aborted: true }, pi, null, null));
|
|
287
328
|
}
|
|
288
329
|
// The bounded nudge (BUGS.md 2026-10-02): the reviewer broke the no-re-run rule despite a
|
|
289
330
|
// green pre-check, so one turn on its own session names the call and asks it to finish
|
|
@@ -295,11 +336,11 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
295
336
|
const nudgeCalls = [];
|
|
296
337
|
nudge = await requestNoRerun(followupCtx, rerun, nudgeCalls);
|
|
297
338
|
if (nudge?.aborted) {
|
|
298
|
-
return withRuns({ decision: "failed", aborted: true }, pi, null, nudge);
|
|
339
|
+
return keep(withRuns({ decision: "failed", aborted: true }, pi, null, nudge));
|
|
299
340
|
}
|
|
300
341
|
nudgeRepeat = nudge ? (suiteRerunWarning(nudgeCalls) ?? null) : null;
|
|
301
342
|
if (nudgeRepeat) {
|
|
302
|
-
warnEventBestEffort(root, role, `${nudgeRepeat} the repeat fails this review: the commit stays, and the
|
|
343
|
+
warnEventBestEffort(root, role, `${nudgeRepeat} the repeat fails this review: the commit stays, and the merge re-reviews it without a third suite run`);
|
|
303
344
|
}
|
|
304
345
|
}
|
|
305
346
|
let verdict = parseVerdict(pi.verdictText ?? "");
|
|
@@ -324,7 +365,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
324
365
|
if (!verdict && pi.ok && !nudge) {
|
|
325
366
|
followUp = await requestVerdict(followupCtx);
|
|
326
367
|
if (followUp?.aborted)
|
|
327
|
-
return withRuns({ decision: "failed", aborted: true }, pi, followUp, nudge);
|
|
368
|
+
return keep(withRuns({ decision: "failed", aborted: true }, pi, followUp, nudge));
|
|
328
369
|
const recovered = followUp ? parseVerdict(followUp.verdictText ?? "") : null;
|
|
329
370
|
if (recovered) {
|
|
330
371
|
verdict = recovered;
|
|
@@ -333,14 +374,14 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
333
374
|
}
|
|
334
375
|
// A repeat after the nudge (BUGS.md 2026-10-02): the reviewer re-ran the suite on the nudge
|
|
335
376
|
// turn too. Fail the review — strike-free like a dead backend (BUGS.md 2026-09-20): the
|
|
336
|
-
// commit stays for the next
|
|
377
|
+
// commit stays for the next merge's re-review, the discard counter never advances, and no
|
|
337
378
|
// rejection is recorded against the author's diff.
|
|
338
379
|
if (nudgeRepeat) {
|
|
339
|
-
const message = `${nudgeRepeat} The review is failed on this repeat: the commit is kept, and the
|
|
340
|
-
logEventBestEffort(root, {
|
|
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 });
|
|
341
382
|
state.reviewResume = undefined;
|
|
342
383
|
recordReview(state, "failed", [message], head);
|
|
343
|
-
return withRuns({ decision: "failed", detail: message }, pi, followUp, nudge);
|
|
384
|
+
return keep(withRuns({ decision: "failed", detail: message }, pi, followUp, nudge));
|
|
344
385
|
}
|
|
345
386
|
if (!verdict) {
|
|
346
387
|
const followUpError = followUp && !followUp.ok ? followUp.errorMessage : undefined;
|
|
@@ -369,7 +410,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
369
410
|
agentSessionResumable(reviewSessionDir, {
|
|
370
411
|
kind: "gate",
|
|
371
412
|
agent: reviewCfg.agent ?? "pi",
|
|
372
|
-
});
|
|
413
|
+
}, claudeConfigDir);
|
|
373
414
|
const reviewTimeoutRewrite = (message) => message.replace(" — session and worktree edits preserved for resume", resumeRemains
|
|
374
415
|
? " — the commit is kept; the next attempt continues this review"
|
|
375
416
|
: " — the commit is kept; the next attempt reviews it from scratch");
|
|
@@ -381,7 +422,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
381
422
|
: followUp
|
|
382
423
|
? ", even after a follow-up turn on its session"
|
|
383
424
|
: ""}`);
|
|
384
|
-
logEventBestEffort(root, {
|
|
425
|
+
logEventBestEffort(root, { source: role, type: "review_failed", head, message, durationMs: Date.now() - reviewStartedAt });
|
|
385
426
|
// A dead reviewer must never destroy committed work (BUGS.md 2026-09-20): leave the commit
|
|
386
427
|
// for the next tick's re-review and do not advance the per-HEAD discard counter. That
|
|
387
428
|
// holds for the review run itself (BUGS.md 2026-09-20), for the verdict-recovery
|
|
@@ -405,7 +446,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
405
446
|
}
|
|
406
447
|
recordReview(state, "failed", [message], head);
|
|
407
448
|
const configError = pi.configError || Boolean(followUp?.configError) || Boolean(nudge?.configError);
|
|
408
|
-
return withRuns({ decision: "failed", detail: message, ...(configError ? { configError: true } : {}) }, pi, followUp, nudge);
|
|
449
|
+
return keep(withRuns({ decision: "failed", detail: message, ...(configError ? { configError: true } : {}) }, pi, followUp, nudge));
|
|
409
450
|
}
|
|
410
451
|
// Consecutive failures of THIS HEAD only: a new commit (new HEAD) starts fresh. Read
|
|
411
452
|
// *before* overwriting lastReview with this failure.
|
|
@@ -421,7 +462,8 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
421
462
|
discarded = true;
|
|
422
463
|
warnEventBestEffort(root, role, `discarding unreviewed leftover after ${REVIEW_FAILURE_LIMIT} failed reviews (${shortSha(head)})`);
|
|
423
464
|
}
|
|
424
|
-
|
|
465
|
+
const failed = { ...withRuns({ decision: "failed", detail: message }, pi, followUp, nudge), ...(discarded ? { discarded: true } : {}) };
|
|
466
|
+
return discarded ? failed : keep(failed);
|
|
425
467
|
}
|
|
426
468
|
if (verdict.verdict === "reject") {
|
|
427
469
|
const rejected = await reject(verdict.reasons, Date.now() - reviewStartedAt);
|
|
@@ -435,9 +477,9 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
435
477
|
state.reviewResume = undefined;
|
|
436
478
|
recordReview(state, "approve", verdict.reasons, head);
|
|
437
479
|
state.unreviewFailures = 0;
|
|
438
|
-
await
|
|
480
|
+
await restoreReviewedHead(wt, head, root, role);
|
|
439
481
|
logEventBestEffort(root, {
|
|
440
|
-
|
|
482
|
+
source: role,
|
|
441
483
|
type: "review_verdict",
|
|
442
484
|
head,
|
|
443
485
|
reason: verdict.reasons[0],
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readTextOrNull } from "../files/files.js";
|
|
5
|
+
import { readJsonFile } from "../files/json-files.js";
|
|
6
|
+
import { changeBaseRev, fileContentAt } from "../git/git.js";
|
|
7
|
+
import { piModelsPath } from "../pi/pi-models.js";
|
|
8
|
+
import { moreSuffix } from "../text/phrases.js";
|
|
9
|
+
/** Environment variable names that hold credentials. */
|
|
10
|
+
const SECRET_NAME = /(TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|API_?KEY|ACCESS_?KEY|PRIVATE_?KEY)/i;
|
|
11
|
+
/** A value shorter than this is a setting (a token limit), not a credential. */
|
|
12
|
+
const MIN_SECRET_LENGTH = 16;
|
|
13
|
+
/** Files larger than this are not scanned. */
|
|
14
|
+
const MAX_SCAN_BYTES = 8 * 1024 * 1024;
|
|
15
|
+
/** Most files named in one reason. */
|
|
16
|
+
const MAX_NAMED = 10;
|
|
17
|
+
/** A credential-shaped value: at least 16 characters, no whitespace, not a path. */
|
|
18
|
+
function looksLikeSecret(value) {
|
|
19
|
+
return value.length >= MIN_SECRET_LENGTH && !/\s/.test(value) && !value.startsWith("/");
|
|
20
|
+
}
|
|
21
|
+
/** The environment entries that look like credentials: a secret-like name and a value of at
|
|
22
|
+
* least 16 characters with no whitespace that is not a path (so `GOOGLE_APPLICATION_CREDENTIALS`
|
|
23
|
+
* pointing at a key file is not a secret). */
|
|
24
|
+
export function secretEnvValues(env) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const [name, value] of Object.entries(env)) {
|
|
27
|
+
if (value === undefined || !SECRET_NAME.test(name))
|
|
28
|
+
continue;
|
|
29
|
+
if (!looksLikeSecret(value))
|
|
30
|
+
continue;
|
|
31
|
+
out.push({ name, value });
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function isRecord(v) {
|
|
36
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
37
|
+
}
|
|
38
|
+
/** The credentials pi stores: literal `apiKey`s and header values in `models.json`, and the
|
|
39
|
+
* `key`/`access`/`refresh` fields (plus secret-named `env` values) of the sibling `auth.json`.
|
|
40
|
+
* `$NAME` interpolations (secretEnvValues covers those) and `!command` values are skipped. A
|
|
41
|
+
* missing or torn file yields nothing; never throws. */
|
|
42
|
+
export function piCredentialValues(modelsPath = piModelsPath()) {
|
|
43
|
+
const out = [];
|
|
44
|
+
const add = (name, value) => {
|
|
45
|
+
if (typeof value !== "string" || value.startsWith("$") || value.startsWith("!"))
|
|
46
|
+
return;
|
|
47
|
+
if (looksLikeSecret(value))
|
|
48
|
+
out.push({ name, value });
|
|
49
|
+
};
|
|
50
|
+
const models = readJsonFile(modelsPath);
|
|
51
|
+
if (models && isRecord(models.providers)) {
|
|
52
|
+
for (const [p, cfg] of Object.entries(models.providers)) {
|
|
53
|
+
if (!isRecord(cfg))
|
|
54
|
+
continue;
|
|
55
|
+
add(`models.json ${p}.apiKey`, cfg.apiKey);
|
|
56
|
+
if (isRecord(cfg.headers)) {
|
|
57
|
+
for (const [h, v] of Object.entries(cfg.headers))
|
|
58
|
+
add(`models.json ${p}.headers.${h}`, v);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const auth = readJsonFile(path.join(path.dirname(modelsPath), "auth.json"));
|
|
63
|
+
if (auth) {
|
|
64
|
+
for (const [p, cred] of Object.entries(auth)) {
|
|
65
|
+
if (!isRecord(cred))
|
|
66
|
+
continue;
|
|
67
|
+
for (const f of ["key", "access", "refresh"])
|
|
68
|
+
add(`auth.json ${p}.${f}`, cred[f]);
|
|
69
|
+
if (isRecord(cred.env)) {
|
|
70
|
+
for (const [n, v] of Object.entries(cred.env)) {
|
|
71
|
+
if (SECRET_NAME.test(n))
|
|
72
|
+
add(`auth.json ${p}.env.${n}`, v);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/** Every secret the gate knows: secret-named environment values (named `$NAME`) then pi's stored
|
|
80
|
+
* credentials (named `pi <file> <path>`), the first entry winning when two share a value. */
|
|
81
|
+
export function knownSecrets(env = process.env, modelsPath = piModelsPath()) {
|
|
82
|
+
const all = [
|
|
83
|
+
...secretEnvValues(env).map((s) => ({ name: `$${s.name}`, value: s.value })),
|
|
84
|
+
...piCredentialValues(modelsPath).map((s) => ({ name: `pi ${s.name}`, value: s.value })),
|
|
85
|
+
];
|
|
86
|
+
return all.filter((s, i) => all.findIndex((o) => o.value === s.value) === i);
|
|
87
|
+
}
|
|
88
|
+
/** The deterministic gate/stage finding for a change that writes the value of a secret
|
|
89
|
+
* environment variable or stored pi credential into a file (a Node diagnostic report dumps
|
|
90
|
+
* the whole environment). A value already present in the file at the merge-base is not this
|
|
91
|
+
* change's leak. The reason
|
|
92
|
+
* names files and variables, never the value. undefined when nothing leaked; never throws on
|
|
93
|
+
* an unreadable file. */
|
|
94
|
+
export async function secretLeakReason(wt, mainBranch, files, secrets = knownSecrets()) {
|
|
95
|
+
if (secrets.length === 0)
|
|
96
|
+
return undefined;
|
|
97
|
+
let baseRev;
|
|
98
|
+
const leaks = [];
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const full = path.join(wt, file);
|
|
101
|
+
let stat;
|
|
102
|
+
try {
|
|
103
|
+
stat = fs.statSync(full);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!stat.isFile() || stat.size > MAX_SCAN_BYTES)
|
|
109
|
+
continue;
|
|
110
|
+
const head = readTextOrNull(full);
|
|
111
|
+
if (head === null)
|
|
112
|
+
continue;
|
|
113
|
+
const hits = secrets.filter((s) => head.includes(s.value));
|
|
114
|
+
if (hits.length === 0)
|
|
115
|
+
continue;
|
|
116
|
+
baseRev ??= await changeBaseRev(wt, mainBranch);
|
|
117
|
+
const base = await fileContentAt(wt, baseRev, file);
|
|
118
|
+
for (const s of hits) {
|
|
119
|
+
if (!base.includes(s.value))
|
|
120
|
+
leaks.push(`\`${file}\` contains the value of ${s.name}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (leaks.length === 0)
|
|
124
|
+
return undefined;
|
|
125
|
+
const shown = leaks.slice(0, MAX_NAMED).join("; ");
|
|
126
|
+
return `${shown}${moreSuffix(leaks.length - MAX_NAMED)} — delete the file or remove the value; never commit credentials (a Node diagnostic report, report.*.json, dumps the whole environment)`;
|
|
127
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** Detecting a reviewer run that re-ran the full suite on a tree the gate's pre-check already
|
|
2
3
|
* verified — pure string logic over the bash command lines a pi run started, no subprocess or
|
|
3
4
|
* file I/O. The review prompt tells such a reviewer not to (src/prompt/gate-prompts.ts), but on
|
|
4
|
-
* 2026-09-23 two reviewers that got the instruction copied their
|
|
5
|
-
* `npm ci` and `npm test` there anyway, holding the
|
|
5
|
+
* 2026-09-23 two reviewers that got the instruction copied their merger worktree to /tmp and ran
|
|
6
|
+
* `npm ci` and `npm test` there anyway, holding the merge slot and loading the shared host;
|
|
6
7
|
* 102 of 429 retained review sessions ran a suite or `npm ci` themselves (BUGS.md 2026-09-23).
|
|
7
8
|
* The prompt is the fix, this is the tripwire: review.ts feeds the reviewer's tool calls through
|
|
8
9
|
* suiteRerunWarning and logs a warning when the rule was broken, so the digest can count it.
|
|
@@ -174,5 +175,5 @@ export function suiteRerunWarning(calls) {
|
|
|
174
175
|
if (first === undefined)
|
|
175
176
|
return undefined;
|
|
176
177
|
const more = moreSuffix(reruns.length - 1);
|
|
177
|
-
return `reviewer re-ran the suite
|
|
178
|
+
return `reviewer re-ran the suite tumwater's pre-check already verified: ${squash(first, 160)}${more}`;
|
|
178
179
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** The one table of retired role ids, old id → new id. The switch parts of the plain-terms role
|
|
2
|
+
* rename add a row each; a row is never removed, because old events, state files and typed
|
|
3
|
+
* flags keep naming the old id. */
|
|
4
|
+
export const LEGACY_ROLE_IDS = {
|
|
5
|
+
dry: "dedupe",
|
|
6
|
+
telemetry: "selfcheck",
|
|
7
|
+
};
|
|
8
|
+
/** Today's id for a role id a person typed or an old file holds; any other id comes back
|
|
9
|
+
* unchanged. `legacy` is a test seam. */
|
|
10
|
+
export function canonicalRoleId(id, legacy = LEGACY_ROLE_IDS) {
|
|
11
|
+
return Object.hasOwn(legacy, id) ? legacy[id] : id;
|
|
12
|
+
}
|
|
13
|
+
/** The retired ids that now map to `id`. */
|
|
14
|
+
export function legacyRoleIdsOf(id, legacy = LEGACY_ROLE_IDS) {
|
|
15
|
+
return Object.entries(legacy).filter(([, next]) => next === id).map(([old]) => old);
|
|
16
|
+
}
|
|
17
|
+
/** `--grep` arguments matching a role's commit subjects under its current id and every retired
|
|
18
|
+
* one. git ORs repeated `--grep`s, so a history search sees both. */
|
|
19
|
+
export function roleCommitGrep(id, legacy = LEGACY_ROLE_IDS) {
|
|
20
|
+
return [id, ...legacyRoleIdsOf(id, legacy)].map((r) => `--grep="tumwater(${r})"`).join(" ");
|
|
21
|
+
}
|