tumwater 0.1.3 → 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 +63 -28
- 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-eligibility.js +332 -43
- package/dist/src/backlog/backlog-md.js +79 -17
- 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 +178 -441
- 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 +32 -30
- package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
- package/dist/src/{brief.js → brief/brief.js} +33 -8
- 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 +13 -11
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +9 -7
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +43 -10
- package/dist/src/cli/cli-editor.js +13 -1
- package/dist/src/cli/cli-flag-specs.js +31 -6
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-query-commands.js +439 -56
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +124 -68
- package/dist/src/cli/completion.js +70 -12
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +264 -124
- 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 +13 -224
- package/dist/src/cli.js +78 -36
- 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 +107 -325
- 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 +494 -68
- package/dist/src/doctor/doctor-checks.js +509 -58
- package/dist/src/doctor/doctor-holds.js +139 -78
- 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 +2 -3
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +76 -25
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +219 -106
- 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 +80 -30
- 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 +6 -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 +79 -59
- 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 -716
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +105 -54
- 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 +25 -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 +179 -104
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +29 -70
- package/dist/src/inbox/inbox-attachments.js +66 -10
- 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 +114 -73
- 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 +104 -50
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +130 -13
- package/dist/src/loop/loop-tick.js +190 -88
- package/dist/src/loop/loop.js +68 -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 -20
- 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 +113 -45
- package/dist/src/operator/operator-intent.js +74 -53
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +47 -28
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
- package/dist/src/orchestrator/orchestrator.js +149 -96
- 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 +124 -38
- 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 +4 -3
- 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 +36 -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 +29 -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 +259 -254
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +235 -44
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +4 -3
- 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 +55 -34
- 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 +109 -131
- 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 +16 -14
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +15 -8
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +28 -25
- 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 +72 -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 +50 -39
- 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 +84 -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} +39 -38
- 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 +142 -25
- 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 +23 -22
- package/dist/src/tick/tick-detail.js +37 -24
- 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 +67 -53
- 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 +75 -10
- package/dist/src/ui/badges.js +113 -69
- package/dist/src/ui/gui/gui-client-boot.js +202 -29
- 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 +94 -37
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +90 -47
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +60 -43
- package/dist/src/ui/gui/gui-client-operator.js +73 -43
- package/dist/src/ui/gui/gui-client-pending.js +34 -31
- package/dist/src/ui/gui/gui-client-report.js +330 -47
- package/dist/src/ui/gui/gui-client-settings.js +126 -18
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +18 -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 +176 -102
- package/dist/src/ui/status-payload.js +114 -36
- package/dist/src/ui/status-render.js +121 -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 +9 -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 +61 -57
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +44 -50
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +87 -1821
- 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 +52 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +44 -26
- package/dist/src/verdict/fix-claim.js +56 -33
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +43 -11
- 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 -56
- package/dist/src/landing/landing-slot.js +0 -256
- 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,17 +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
20
|
import { protectedEditReason } from "./protected-edits.js";
|
|
21
|
+
import { secretLeakReason } from "./secret-leaks.js";
|
|
21
22
|
import { backlogStructureReason } from "../backlog/backlog-structure.js";
|
|
22
23
|
import { suiteRerunWarning } from "./suite-rerun.js";
|
|
23
|
-
import {
|
|
24
|
+
import { setMergeStage } from "../merge/merge-slot.js";
|
|
24
25
|
import { gateBuildPrecheck } from "./review-precheck.js";
|
|
25
26
|
/** Attach the review gate's pi runs onto a partial GateResult — `run` plus the optional
|
|
26
27
|
* verdict-recovery follow-up (`followUpRun`) and no-re-run nudge (`nudgeRun`) fields, each
|
|
27
28
|
* present only when its turn actually ran. Spelled once so every return site of
|
|
28
29
|
* reviewAheadOfMain that carries runs shares one shape for the usage-folding caller
|
|
29
|
-
* (
|
|
30
|
+
* (merge-core.ts folds run, then followUpRun, then nudgeRun), instead of one conditional
|
|
30
31
|
* spread per site that a new run kind would have to update everywhere. */
|
|
31
32
|
function withRuns(r, run, followUp, nudge) {
|
|
32
33
|
return {
|
|
@@ -37,7 +38,7 @@ function withRuns(r, run, followUp, nudge) {
|
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
/** Put the worktree back on the exact head the gate reviewed: the reviewer's only output is its
|
|
40
|
-
* verdict, so a commit it made must never reach the
|
|
41
|
+
* verdict, so a commit it made must never reach the merge pin or the next re-review. */
|
|
41
42
|
async function restoreReviewedHead(wt, head, root, role) {
|
|
42
43
|
const now = await headOf(wt, "HEAD").catch(() => null);
|
|
43
44
|
if (now && now !== head) {
|
|
@@ -94,7 +95,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
94
95
|
state.unreviewFailures = 0;
|
|
95
96
|
await resetWorktreeToMain(wt, mainBranch);
|
|
96
97
|
logEventBestEffort(root, {
|
|
97
|
-
|
|
98
|
+
source: role,
|
|
98
99
|
type: "review_rejected",
|
|
99
100
|
head,
|
|
100
101
|
reasons,
|
|
@@ -109,28 +110,32 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
109
110
|
}
|
|
110
111
|
const files = await aheadOfMainFiles(wt, mainBranch);
|
|
111
112
|
// A change handed back to its author with the conflict markers left in place (PLANS.md
|
|
112
|
-
// "Robust conflict landing, part 2/2") must not
|
|
113
|
+
// "Robust conflict landing, part 2/2") must not merge raw `<<<<<<<` lines. The stage check
|
|
113
114
|
// flags them for one fix-up turn, but only this deterministic gate rejection makes it a
|
|
114
115
|
// hard stop — an md-only diff skips the reviewer entirely, so nothing else would catch it.
|
|
115
116
|
const markerReason = conflictMarkerReason(wt, files);
|
|
116
117
|
if (markerReason !== undefined)
|
|
117
118
|
return reject([markerReason]);
|
|
118
|
-
// The prompt block and PRINCIPLES.md are markdown, so the exempt fast path below would
|
|
119
|
+
// The prompt block and PRINCIPLES.md are markdown, so the exempt fast path below would merge
|
|
119
120
|
// an edit to either unreviewed; reject it here, for md-only and code diffs alike.
|
|
120
121
|
const protectedReason = await protectedEditReason(wt, mainBranch, files, role);
|
|
121
122
|
if (protectedReason !== undefined)
|
|
122
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]);
|
|
123
128
|
if (isExemptDiff(files, config.review.exemptPaths)) {
|
|
124
129
|
// The exemption is a fast path, not a blind eye: an md-only diff that moves a bug to
|
|
125
130
|
// Fixed must have its fix's symbols on this tree, or it records code that does not
|
|
126
|
-
// exist (BUGS.md 2026-09-22 — 9cea8c3
|
|
131
|
+
// exist (BUGS.md 2026-09-22 — 9cea8c3 merged a Fix paragraph naming runScriptGroup
|
|
127
132
|
// and signalTree with no source behind it). A deterministic rejection, like the
|
|
128
133
|
// pre-check below: no pi run consumed, reasons injected into the author's next tick.
|
|
129
134
|
const falseFix = await falseFixReason(wt, mainBranch, files);
|
|
130
135
|
if (falseFix)
|
|
131
136
|
return reject([falseFix]);
|
|
132
|
-
// Same cross-check as the
|
|
133
|
-
// 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
|
|
134
139
|
// the reviewer by design, and a malformed backlog stays invisible until an entry ends up
|
|
135
140
|
// on the wrong side of a heading (PLANS.md 2026-09-25, 52cbadd1).
|
|
136
141
|
const structure = await backlogStructureReason(wt, mainBranch, files);
|
|
@@ -141,7 +146,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
141
146
|
}
|
|
142
147
|
// The backlog-structure check for code diffs too, before the build pre-check: a change that
|
|
143
148
|
// duplicates or drops a `## ` section heading in PLANS.md, BUGS.md, or QUESTIONS.md cannot
|
|
144
|
-
//
|
|
149
|
+
// merge, and the rejection is deterministic — no pi run and no check run spent on a tree the
|
|
145
150
|
// heading set already condemns.
|
|
146
151
|
const structure = await backlogStructureReason(wt, mainBranch, files);
|
|
147
152
|
if (structure)
|
|
@@ -151,9 +156,9 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
151
156
|
// "reviewing": a deterministic rejection never shows as reviewing on the dashboards. Both
|
|
152
157
|
// gate callers (the tick path and leftover.ts's recoverLeftover) get it for free. Its run,
|
|
153
158
|
// flake re-run, and attribution live in review-precheck.ts (the check run itself lives in
|
|
154
|
-
// runScopedBuildCheck, shared with the
|
|
155
|
-
// here is returned verbatim and a pass hands the verified head to the
|
|
156
|
-
// 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) —
|
|
157
162
|
// "build-check" while it runs, "reviewing" when the reviewer run starts below.
|
|
158
163
|
const pre = await gateBuildPrecheck(ctx, state, head, reject);
|
|
159
164
|
if (pre.resolved) {
|
|
@@ -178,6 +183,10 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
178
183
|
// same config the run below gets, so the event names what the reviewer actually runs on —
|
|
179
184
|
// the budget fallback included. Omitted when no model is configured (pi's own default).
|
|
180
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;
|
|
181
190
|
// A progressing timeout left this run's session for the next attempt (LoopState.reviewResume):
|
|
182
191
|
// resume it only while the patch is unchanged (the same diff), the resume budget lasts, and
|
|
183
192
|
// the directory still holds a gate session to `--continue`. Otherwise the stale marker is
|
|
@@ -189,7 +198,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
189
198
|
agentSessionResumable(priorResume.sessionDir, {
|
|
190
199
|
kind: "gate",
|
|
191
200
|
agent: reviewCfg.agent ?? "pi",
|
|
192
|
-
});
|
|
201
|
+
}, claudeConfigDir);
|
|
193
202
|
if (resuming) {
|
|
194
203
|
// Count the resume before the run: a crash must not let the same session be resumed
|
|
195
204
|
// forever without ever spending the budget.
|
|
@@ -209,7 +218,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
209
218
|
// needs the override.
|
|
210
219
|
const followupCtx = reviewSessionDir === ctx.sessionDir ? ctx : { ...ctx, sessionDir: reviewSessionDir };
|
|
211
220
|
logEventBestEffort(root, {
|
|
212
|
-
|
|
221
|
+
source: role,
|
|
213
222
|
type: "review_start",
|
|
214
223
|
head,
|
|
215
224
|
...(ctx.revisionRound !== undefined ? { revision: ctx.revisionRound } : {}),
|
|
@@ -222,8 +231,8 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
222
231
|
state.phase = "review";
|
|
223
232
|
saveLoopState(root, state);
|
|
224
233
|
// Freeze the revision the diff is measured from once, before the run: main moves while a
|
|
225
|
-
//
|
|
226
|
-
// 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
|
|
227
236
|
// and tells the reviewer to compare against it instead.
|
|
228
237
|
const base = await changeBaseRev(wt, mainBranch);
|
|
229
238
|
const diff = await aheadOfMainDiff(wt, mainBranch);
|
|
@@ -239,17 +248,17 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
239
248
|
}
|
|
240
249
|
: undefined;
|
|
241
250
|
// The reviewer run's wall time rides on its verdict event: a reviewer that takes an hour per
|
|
242
|
-
// merge on local hardware is a
|
|
251
|
+
// merge on local hardware is a team-level cost an operator must be able to see.
|
|
243
252
|
const reviewStartedAt = Date.now();
|
|
244
253
|
// The reviewer's started tool calls, collected only when the no-re-run rule stands (a verified
|
|
245
254
|
// pre-check) — the one case the suite-rerun tripwire below can fire.
|
|
246
255
|
const reviewerCalls = [];
|
|
247
|
-
// The
|
|
248
|
-
//
|
|
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
|
|
249
258
|
// line resets the gate progress accumulator, so the cell never shows a previous run's counts.
|
|
250
|
-
|
|
259
|
+
setMergeStage(root, role, "reviewing");
|
|
251
260
|
// The author's claimed WHY/RISK/VERIFIED ride along when present — checking those claims
|
|
252
|
-
// against the actual diff is exactly the adversarial angle (recovery
|
|
261
|
+
// against the actual diff is exactly the adversarial angle (recovery merges reconstruct
|
|
253
262
|
// them from the pinned commit's message).
|
|
254
263
|
const pi = await ctx.runGatePi({
|
|
255
264
|
cwd: wt,
|
|
@@ -259,8 +268,8 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
259
268
|
: buildReviewPrompt(diff, summary, commitBody, readPrinciples(root), highFriction, verifiedByHarness, base, undefined, priorReview),
|
|
260
269
|
// The reviewer runs on its own time budget (review.timeoutSeconds), never longer than a
|
|
261
270
|
// tick's: a timed-out review is a FAILED run (pi.ok false), so it takes the dead-backend
|
|
262
|
-
// path below — commit kept, no strike — and re-
|
|
263
|
-
// 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.
|
|
264
273
|
config: reviewCfg,
|
|
265
274
|
// This run's own session directory (per-run, keyed by tick and pinned sha): a follow-up
|
|
266
275
|
// turn, or a later attempt that resumes this review, continues exactly this run instead
|
|
@@ -284,8 +293,20 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
284
293
|
reviewerCalls.push({ toolName, args });
|
|
285
294
|
}
|
|
286
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
|
+
}),
|
|
287
308
|
});
|
|
288
|
-
// 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
|
|
289
310
|
// slot and loaded the shared host for a result the harness already had (BUGS.md 2026-09-23:
|
|
290
311
|
// scratch copies under /tmp, `npm ci` and `npm test` there). Warned on every outcome, abort
|
|
291
312
|
// included — the run happened either way; the verdict itself is not touched.
|
|
@@ -319,7 +340,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
319
340
|
}
|
|
320
341
|
nudgeRepeat = nudge ? (suiteRerunWarning(nudgeCalls) ?? null) : null;
|
|
321
342
|
if (nudgeRepeat) {
|
|
322
|
-
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`);
|
|
323
344
|
}
|
|
324
345
|
}
|
|
325
346
|
let verdict = parseVerdict(pi.verdictText ?? "");
|
|
@@ -353,11 +374,11 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
353
374
|
}
|
|
354
375
|
// A repeat after the nudge (BUGS.md 2026-10-02): the reviewer re-ran the suite on the nudge
|
|
355
376
|
// turn too. Fail the review — strike-free like a dead backend (BUGS.md 2026-09-20): the
|
|
356
|
-
// commit stays for the next
|
|
377
|
+
// commit stays for the next merge's re-review, the discard counter never advances, and no
|
|
357
378
|
// rejection is recorded against the author's diff.
|
|
358
379
|
if (nudgeRepeat) {
|
|
359
|
-
const message = `${nudgeRepeat} The review is failed on this repeat: the commit is kept, and the
|
|
360
|
-
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 });
|
|
361
382
|
state.reviewResume = undefined;
|
|
362
383
|
recordReview(state, "failed", [message], head);
|
|
363
384
|
return keep(withRuns({ decision: "failed", detail: message }, pi, followUp, nudge));
|
|
@@ -389,7 +410,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
389
410
|
agentSessionResumable(reviewSessionDir, {
|
|
390
411
|
kind: "gate",
|
|
391
412
|
agent: reviewCfg.agent ?? "pi",
|
|
392
|
-
});
|
|
413
|
+
}, claudeConfigDir);
|
|
393
414
|
const reviewTimeoutRewrite = (message) => message.replace(" — session and worktree edits preserved for resume", resumeRemains
|
|
394
415
|
? " — the commit is kept; the next attempt continues this review"
|
|
395
416
|
: " — the commit is kept; the next attempt reviews it from scratch");
|
|
@@ -401,7 +422,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
401
422
|
: followUp
|
|
402
423
|
? ", even after a follow-up turn on its session"
|
|
403
424
|
: ""}`);
|
|
404
|
-
logEventBestEffort(root, {
|
|
425
|
+
logEventBestEffort(root, { source: role, type: "review_failed", head, message, durationMs: Date.now() - reviewStartedAt });
|
|
405
426
|
// A dead reviewer must never destroy committed work (BUGS.md 2026-09-20): leave the commit
|
|
406
427
|
// for the next tick's re-review and do not advance the per-HEAD discard counter. That
|
|
407
428
|
// holds for the review run itself (BUGS.md 2026-09-20), for the verdict-recovery
|
|
@@ -458,7 +479,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
458
479
|
state.unreviewFailures = 0;
|
|
459
480
|
await restoreReviewedHead(wt, head, root, role);
|
|
460
481
|
logEventBestEffort(root, {
|
|
461
|
-
|
|
482
|
+
source: role,
|
|
462
483
|
type: "review_verdict",
|
|
463
484
|
head,
|
|
464
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
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { deleteRefIfAt, refSha } from "../git/git.js";
|
|
3
|
+
import { gitTry, runGit } from "../git/git-run.js";
|
|
4
|
+
import { branchName, mergeRefName, rejectedRefName, reviewSessionDir, roleInboxDir, roleNotesPath, sessionDir, statePath } from "../paths.js";
|
|
5
|
+
import { errorMessage } from "../text/text.js";
|
|
6
|
+
import { LEGACY_ROLE_IDS } from "./legacy-role-ids.js";
|
|
7
|
+
/** Move each retired role id's branch, refs and files to the new id (plans/terminology.md "Role
|
|
8
|
+
* names"). Returns one line per artifact moved or skipped. Nothing that exists under the new id
|
|
9
|
+
* is overwritten, each artifact fails alone, and pi and native logs stay put as history. */
|
|
10
|
+
export async function migrateLegacyRoleIds(root, legacy = LEGACY_ROLE_IDS) {
|
|
11
|
+
const notes = [];
|
|
12
|
+
for (const [oldId, newId] of Object.entries(legacy)) {
|
|
13
|
+
const attempt = async (what, move) => {
|
|
14
|
+
try {
|
|
15
|
+
const note = await move();
|
|
16
|
+
if (note !== null)
|
|
17
|
+
notes.push(note);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
notes.push(`could not move ${what}: ${errorMessage(err)}`);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
await attempt("branch", async () => {
|
|
24
|
+
const from = branchName(oldId);
|
|
25
|
+
const to = branchName(newId);
|
|
26
|
+
if ((await refSha(root, `refs/heads/${from}`)) === null)
|
|
27
|
+
return null;
|
|
28
|
+
if ((await refSha(root, `refs/heads/${to}`)) !== null)
|
|
29
|
+
return `kept branch ${from} and ${to}: both exist`;
|
|
30
|
+
await runGit(root, ["branch", "-m", from, to]);
|
|
31
|
+
return `renamed branch ${from} to ${to}`;
|
|
32
|
+
});
|
|
33
|
+
for (const refFor of [mergeRefName, rejectedRefName]) {
|
|
34
|
+
const from = refFor(oldId);
|
|
35
|
+
const to = refFor(newId);
|
|
36
|
+
await attempt(from, async () => {
|
|
37
|
+
const sha = await refSha(root, from);
|
|
38
|
+
if (sha === null)
|
|
39
|
+
return null;
|
|
40
|
+
if ((await refSha(root, to)) !== null)
|
|
41
|
+
return `kept ${from} and ${to}: both exist`;
|
|
42
|
+
if ((await gitTry(root, "update-ref", to, sha)) === null)
|
|
43
|
+
throw new Error(`update-ref ${to} failed`);
|
|
44
|
+
await deleteRefIfAt(root, from, sha);
|
|
45
|
+
return `moved ${from} to ${to}`;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
for (const pathFor of [statePath, roleNotesPath, roleInboxDir, sessionDir, reviewSessionDir]) {
|
|
49
|
+
const from = pathFor(root, oldId);
|
|
50
|
+
const to = pathFor(root, newId);
|
|
51
|
+
await attempt(from, async () => {
|
|
52
|
+
if (!fs.existsSync(from))
|
|
53
|
+
return null;
|
|
54
|
+
if (fs.existsSync(to))
|
|
55
|
+
return `kept ${from} and ${to}: both exist`;
|
|
56
|
+
fs.renameSync(from, to);
|
|
57
|
+
return `moved ${from} to ${to}`;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return notes;
|
|
62
|
+
}
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
/** The base roles that may run more than one instance. */
|
|
10
10
|
export const INSTANCE_ROLES = new Set(["feature", "bugfix"]);
|
|
11
11
|
/** The instance-id shape: a base role, a dash, and an index of 2 or more (`-1` is not an
|
|
12
|
-
* instance — the bare id already means instance 1).
|
|
13
|
-
|
|
12
|
+
* instance — the bare id already means instance 1). The source string is also interpolated into
|
|
13
|
+
* the dashboard script (gui-client-labels.ts). */
|
|
14
|
+
export const INSTANCE_ID_SOURCE = "^(feature|bugfix)-([2-9]|[1-9][0-9]+)$";
|
|
15
|
+
const INSTANCE_RE = new RegExp(INSTANCE_ID_SOURCE);
|
|
14
16
|
/** The base role a loop id belongs to: `feature-2` → `feature`, `bugfix-10` → `bugfix`; any
|
|
15
17
|
* other id — a bare role, a custom loop, `feature-1` — is returned unchanged. */
|
|
16
18
|
export function baseRoleOf(id) {
|
|
@@ -58,3 +60,18 @@ export function loopIds(config) {
|
|
|
58
60
|
}
|
|
59
61
|
return ids;
|
|
60
62
|
}
|
|
63
|
+
/** The pause-marker key for a loop id. Instance 1's loop id is the bare role id, which as a pause
|
|
64
|
+
* key would mean the whole role, so while the role runs several instances instance 1 pauses under
|
|
65
|
+
* `<role>-1`. `-1` is a pause key only, never a loop id. Every other id is its own key. */
|
|
66
|
+
export function instancePauseKey(config, loopId) {
|
|
67
|
+
return INSTANCE_ROLES.has(loopId) && configuredInstances(config, loopId) > 1
|
|
68
|
+
? `${loopId}-1`
|
|
69
|
+
: loopId;
|
|
70
|
+
}
|
|
71
|
+
/** Does the per-role pause marker stop this loop id? True for its own id, its base role's id (a
|
|
72
|
+
* whole-role pause), or — for the bare id of an instance role — the `<role>-1` pause key. */
|
|
73
|
+
export function loopPaused(paused, loopId) {
|
|
74
|
+
return (paused.has(loopId) ||
|
|
75
|
+
paused.has(baseRoleOf(loopId)) ||
|
|
76
|
+
(INSTANCE_ROLES.has(loopId) && paused.has(`${loopId}-1`)));
|
|
77
|
+
}
|