tumwater 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
2
|
-
import { changeBaseRev, headOf
|
|
2
|
+
import { changeBaseRev, headOf } from "../git/git.js";
|
|
3
3
|
import { aheadOfMainFiles } from "../git/git-diff.js";
|
|
4
4
|
import { resetWorktreeToMain } from "../git/worktree.js";
|
|
5
5
|
import { recordReview } from "../run/run-apply.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { sleepInterruptible } from "../run/run-timing.js";
|
|
7
|
+
import { parseNonNegativeInt } from "../text/text.js";
|
|
8
8
|
import { firstReason } from "../text/phrases.js";
|
|
9
9
|
import { isExemptDiff } from "./exemptions.js";
|
|
10
|
-
import {
|
|
11
|
-
import { setMergeStage } from "../merge/merge-slot.js";
|
|
10
|
+
import { idleWithoutPermit } from "../concurrency/run-permit.js";
|
|
12
11
|
import { gateBuildPrecheck } from "./review-precheck.js";
|
|
13
12
|
import { diffRuleReasons } from "./diff-rules.js";
|
|
14
|
-
export { REVIEW_FAILURE_LIMIT } from "./review-run.js";
|
|
15
|
-
import { restoreReviewedHead, runReviewer, withRuns } from "./review-run.js";
|
|
16
13
|
/** In-place retries of a failed reviewer run within one verify, after the first run. */
|
|
17
14
|
const REVIEW_RETRIES = 2;
|
|
18
15
|
/** The pause before each retry (ReviewContext.retryDelayMs overrides it in tests). */
|
|
@@ -23,92 +20,40 @@ const REVIEW_RETRY_DELAY_ENV = "TUMWATER_REVIEW_RETRY_DELAY_MS";
|
|
|
23
20
|
/** The pause before each retry: the context override wins; else the tunable env knob read through
|
|
24
21
|
* the shared plain-decimal rule (parseNonNegativeInt — a 0 ms delay, the test-suite's setting, is
|
|
25
22
|
* valid). A malformed spelling falls back to REVIEW_RETRY_DELAY_MS rather than silently becoming
|
|
26
|
-
* NaN: `
|
|
23
|
+
* NaN: `sleepInterruptible(NaN)` resolves at ~0 ms and the retries would fire back-to-back instead
|
|
27
24
|
* of 30 s apart. */
|
|
28
25
|
export function reviewRetryDelayMs(ctxRetryDelayMs) {
|
|
29
26
|
const raw = process.env[REVIEW_RETRY_DELAY_ENV];
|
|
30
27
|
return ctxRetryDelayMs ?? (raw === undefined ? REVIEW_RETRY_DELAY_MS : (parseNonNegativeInt(raw) ?? REVIEW_RETRY_DELAY_MS));
|
|
31
28
|
}
|
|
32
|
-
/**
|
|
33
|
-
* right now, or the lease failed (the check then runs first, in the change's own slot). */
|
|
34
|
-
async function leaseCheckSlot(ctx, head) {
|
|
35
|
-
if (!ctx.leaseCheckSlot)
|
|
36
|
-
return null;
|
|
37
|
-
try {
|
|
38
|
-
return await ctx.leaseCheckSlot(head);
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
warnEventBestEffort(ctx.root, ctx.role, `no second workspace for the gate check (${errorMessage(err)}); running it before the review`);
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
/** Start the gate check in `lease`'s checkout of `head`. A rejection is recorded, not applied:
|
|
46
|
-
* resetting the change's branch is left to reviewAttempt, after the reviewer has stopped. */
|
|
47
|
-
function startOverlappedCheck(ctx, state, head, lease) {
|
|
48
|
-
const overlap = { abort: new AbortController(), done: false };
|
|
49
|
-
const reject = async (reasons) => {
|
|
50
|
-
overlap.rejectedReasons = reasons;
|
|
51
|
-
return { decision: "rejected", detail: firstReason(reasons) };
|
|
52
|
-
};
|
|
53
|
-
overlap.gate = gateBuildPrecheck({ ...ctx, wt: lease.dir }, state, head, reject)
|
|
54
|
-
.then((pre) => {
|
|
55
|
-
overlap.done = true;
|
|
56
|
-
if (pre.resolved)
|
|
57
|
-
overlap.abort.abort();
|
|
58
|
-
else
|
|
59
|
-
setMergeStage(ctx.root, ctx.role, "reviewing");
|
|
60
|
-
return pre;
|
|
61
|
-
}, (err) => {
|
|
62
|
-
overlap.done = true;
|
|
63
|
-
overlap.abort.abort();
|
|
64
|
-
throw err;
|
|
65
|
-
})
|
|
66
|
-
.finally(() => lease.release());
|
|
67
|
-
overlap.gate.catch(() => { }); // surfaced where reviewAttempt awaits it
|
|
68
|
-
return overlap;
|
|
69
|
-
}
|
|
70
|
-
/** Run the adversarial review gate over everything ahead of main in `wt` and update `state`
|
|
29
|
+
/** Run the deterministic review gate over everything ahead of main in `wt` and update `state`
|
|
71
30
|
* accordingly. Shared by the run path (after commitAll) and leftover recovery
|
|
72
|
-
* (leftover.ts's recoverLeftover) — every path
|
|
31
|
+
* (runner-leftover.ts's recoverLeftover) — every path
|
|
73
32
|
* that can move a commit into main routes through here, so no crash or abort path smuggles
|
|
74
33
|
* unreviewed work in:
|
|
75
34
|
* - gate disabled, or an already-approved HEAD, or an exempt (doc-only) diff → merge as-is;
|
|
76
|
-
* -
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* working-tree edits (its only output channel is the verdict);
|
|
35
|
+
* - approve → a passing build pre-check records lastApprovedHead and approves, with no
|
|
36
|
+
* reviewer run; every merged change is judged once, after it lands, by the post-landing
|
|
37
|
+
* review (part 3g/3, reviewMergedCommit, run by the merge pipeline's post-review drain);
|
|
80
38
|
* - reject → reset the branch to main, record reasons in state.lastReview (injected into the
|
|
81
39
|
* role's next run prompt), log review_rejected;
|
|
82
|
-
* - fail with an unparseable reply (the reviewer ran to completion but emitted no VERDICT) →
|
|
83
|
-
* one capped follow-up turn on the reviewer's own session asks for the missing line
|
|
84
|
-
* (mirroring requestSummary — BUGS.md 2026-09-29); only a follow-up that also yields
|
|
85
|
-
* nothing counts the strike. Leave the commit on the branch for the next run's re-review,
|
|
86
|
-
* counting consecutive failures per HEAD; past REVIEW_FAILURE_LIMIT discard the leftover
|
|
87
|
-
* with a warning.
|
|
88
|
-
* - fail because the run itself failed (transport/spawn/timeout, pi.ok false) → leave the
|
|
89
|
-
* commit and do NOT advance the count: the reviewer never judged the diff, so the failure
|
|
90
|
-
* is evidence about the backend, never about the commit (BUGS.md 2026-09-20).
|
|
91
40
|
* - the declared check fails, and fails again on its one re-run → reject with the check's
|
|
92
|
-
* reasons
|
|
93
|
-
*
|
|
41
|
+
* reasons when main's own tip is green (or has no verdict); when main is red too, fail
|
|
42
|
+
* without advancing the count and leave the commit — the failure is main's. Never throws.
|
|
94
43
|
* `highFriction` marks a change whose authoring run burned more than the configured turn/time
|
|
95
|
-
* thresholds (plans/refusal-and-thrash.md):
|
|
96
|
-
|
|
97
|
-
async function reviewAttempt(ctx, state, summary, commitBody, highFriction, retry) {
|
|
44
|
+
* thresholds (plans/refusal-and-thrash.md): kept for the caller's convenience. */
|
|
45
|
+
async function reviewAttempt(ctx, state, _summary, _commitBody, _highFriction, retry) {
|
|
98
46
|
const { root, role, wt, mainBranch, config } = ctx;
|
|
99
47
|
if (!config.review.enabled) {
|
|
100
|
-
state.reviewResume = undefined;
|
|
101
48
|
return { decision: "exempt" };
|
|
102
49
|
}
|
|
103
50
|
const head = await headOf(wt, "HEAD");
|
|
104
|
-
// The one reject path: every rejection of this HEAD — deterministic build-check
|
|
105
|
-
//
|
|
51
|
+
// The one reject path: every rejection of this HEAD — the deterministic build-check
|
|
52
|
+
// outcome — shares this bookkeeping. Record lastReview (injected into the role's next
|
|
106
53
|
// run prompt), reset the failure count (a verdict about this HEAD is a successful review
|
|
107
54
|
// either way), discard the branch by resetting it to main, and log review_rejected.
|
|
108
|
-
// Callers attach `run` when a pi run was consumed.
|
|
109
55
|
const reject = async (reasons, durationMs) => {
|
|
110
56
|
recordReview(state, "reject", reasons, head);
|
|
111
|
-
state.reviewResume = undefined;
|
|
112
57
|
state.unreviewFailures = 0;
|
|
113
58
|
await resetWorktreeToMain(wt, mainBranch);
|
|
114
59
|
logEventBestEffort(root, {
|
|
@@ -120,111 +65,57 @@ async function reviewAttempt(ctx, state, summary, commitBody, highFriction, retr
|
|
|
120
65
|
});
|
|
121
66
|
return { decision: "rejected", detail: firstReason(reasons) };
|
|
122
67
|
};
|
|
123
|
-
// Every exit below that keeps the change leaves the worktree at the head this gate reviewed.
|
|
124
|
-
const keep = async (r) => {
|
|
125
|
-
await restoreReviewedHead(wt, head, root, role);
|
|
126
|
-
return r;
|
|
127
|
-
};
|
|
128
|
-
// Settle the attempt on an overlapped check that did not pass: the sequential pre-check's own
|
|
129
|
-
// outcome, with the reviewer's run (when one started) dropped and no verdict recorded. A
|
|
130
|
-
// rejection is applied now, after the reviewer stopped touching the tree; every other outcome
|
|
131
|
-
// keeps the change at the head the gate reviewed.
|
|
132
|
-
const settleFailedCheck = async (o, pre, pi) => {
|
|
133
|
-
state.reviewResume = undefined;
|
|
134
|
-
const settled = o.rejectedReasons
|
|
135
|
-
? await reject(o.rejectedReasons)
|
|
136
|
-
: await keep(pre.resolved ?? { decision: "failed", detail: "the gate check did not complete" });
|
|
137
|
-
return pi ? withRuns(settled, pi, null, null) : settled;
|
|
138
|
-
};
|
|
139
68
|
// Already reviewed this exact HEAD (e.g. a merge_blocked retry): do not burn another run.
|
|
140
69
|
if (state.lastApprovedHead === head) {
|
|
141
|
-
state.reviewResume = undefined;
|
|
142
70
|
return { decision: "approved" };
|
|
143
71
|
}
|
|
144
72
|
const files = await aheadOfMainFiles(wt, mainBranch);
|
|
145
|
-
// The
|
|
146
|
-
//
|
|
147
|
-
// the
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
// PLANS.md 2026-09-25).
|
|
73
|
+
// The three deterministic diff rules the gate rejects on (a brief-block edit, a protected
|
|
74
|
+
// edit and a secret leak) are judged by diffRuleReasons in the "gate" scope — the same rules
|
|
75
|
+
// the main watch runs on main's tip, but a narrower set: a conflict marker, a false fix claim
|
|
76
|
+
// on an exempt md-only diff, and a broken backlog structure are all repairable by a later
|
|
77
|
+
// commit, so they stop refusing a diff pre-merge and the watch reports them on main's tip
|
|
78
|
+
// instead ("Every commit is verified on main, part 3m/3"). A rejection is a hard stop and
|
|
79
|
+
// deterministic, like the pre-check below: no pi run consumed, reasons injected into the
|
|
80
|
+
// author's next run. A brief edit or a leaked credential must not merge raw (PLANS.md
|
|
81
|
+
// "Robust conflict landing, part 2/2"), and an md-only diff skips the reviewer, so each rule
|
|
82
|
+
// below is what catches a change the reviewer would never see: an edit to the brief prompt
|
|
83
|
+
// block or PRINCIPLES.md (protectedEditReason), and a leaked credential (secretLeakReason).
|
|
157
84
|
const baseRev = await changeBaseRev(wt, mainBranch);
|
|
158
85
|
const exempt = isExemptDiff(files, config.review.exemptPaths);
|
|
159
|
-
const [firstFinding] = await diffRuleReasons(wt, baseRev, files, role, exempt);
|
|
86
|
+
const [firstFinding] = await diffRuleReasons(wt, baseRev, files, role, exempt, "gate");
|
|
160
87
|
if (firstFinding !== undefined)
|
|
161
88
|
return reject([firstFinding]);
|
|
162
89
|
if (exempt) {
|
|
163
|
-
state.reviewResume = undefined;
|
|
164
90
|
return { decision: "exempt" };
|
|
165
91
|
}
|
|
166
92
|
// Deterministic build pre-check — after BOTH early returns above (an md-only diff cannot
|
|
167
|
-
// break the build)
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
// A retried attempt keeps the first attempt's passing pre-check (a green, or a red naming
|
|
176
|
-
// only main's known failures): the tree is the same head.
|
|
177
|
-
// A review judges a diff, not a sha: a head carrying exactly the patch this role's last
|
|
178
|
-
// approval judged (the approved head cleanly rebased onto a moved main) reuses that verdict
|
|
179
|
-
// instead of paying a second reviewer run. Only the model review is reused — the pre-check
|
|
180
|
-
// has just run on this tree (a failure rejected it there), and its verifiedHead rides on the
|
|
181
|
-
// result as for any approval.
|
|
182
|
-
const reviewedPatchId = (await patchId(wt, mainBranch, head)) ?? undefined;
|
|
183
|
-
const patchApproved = reviewedPatchId !== undefined && state.approvedPatchIds?.includes(reviewedPatchId) === true;
|
|
184
|
-
// The check runs alongside the reviewer when a second pool slot is free right now (Fast merge
|
|
185
|
-
// part 12): it starts here, in its own checkout of this head, while the reviewer run below
|
|
186
|
-
// starts in the change's own slot. An already-approved patch has no reviewer to overlap, and
|
|
187
|
-
// with no free slot (or no way to lease one) the check runs first, as it always did.
|
|
188
|
-
const afterMerge = reviewedAfterMerge(config, role);
|
|
189
|
-
let overlap;
|
|
93
|
+
// break the build): a deterministic rejection never shows as reviewing on the dashboards.
|
|
94
|
+
// Both gate callers (the run path and runner-leftover.ts's recoverLeftover) get it for free.
|
|
95
|
+
// Its run, flake re-run, and attribution live in review-precheck.ts (the check run itself
|
|
96
|
+
// lives in runScopedBuildCheck, shared with the merge path's in-lock re-check); a resolved
|
|
97
|
+
// result here is returned verbatim and a pass hands the verified head to the merge path below.
|
|
98
|
+
// The pre-check always runs in the change's own slot. A retried attempt keeps the first
|
|
99
|
+
// attempt's passing pre-check (a green, or a red naming only main's known failures): the
|
|
100
|
+
// tree is the same head.
|
|
190
101
|
if (!retry.pre) {
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
else {
|
|
195
|
-
const pre = await gateBuildPrecheck(ctx, state, head, reject);
|
|
196
|
-
if (pre.resolved) {
|
|
197
|
-
state.reviewResume = undefined;
|
|
198
|
-
return pre.resolved;
|
|
199
|
-
}
|
|
200
|
-
retry.pre = { verifiedHead: pre.verifiedHead, knownMainFailures: pre.knownMainFailures };
|
|
102
|
+
const pre = await gateBuildPrecheck(ctx, state, head, reject);
|
|
103
|
+
if (pre.resolved) {
|
|
104
|
+
return pre.resolved;
|
|
201
105
|
}
|
|
106
|
+
retry.pre = { verifiedHead: pre.verifiedHead, knownMainFailures: pre.knownMainFailures };
|
|
202
107
|
}
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (afterMerge && retry.pre) {
|
|
208
|
-
state.reviewResume = undefined;
|
|
209
|
-
state.lastApprovedHead = head;
|
|
210
|
-
return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
|
|
211
|
-
}
|
|
212
|
-
if (patchApproved && retry.pre) {
|
|
213
|
-
state.reviewResume = undefined;
|
|
214
|
-
state.lastApprovedHead = head;
|
|
215
|
-
return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
|
|
216
|
-
}
|
|
217
|
-
return runReviewer(ctx, state, {
|
|
218
|
-
head, overlap, knownMainFailures, retry, summary, commitBody, highFriction,
|
|
219
|
-
reviewedPatchId, reject, keep, settleFailedCheck,
|
|
220
|
-
});
|
|
108
|
+
// A passing pre-check approves: the gate runs no model reviewer, and every merge is judged
|
|
109
|
+
// once, after it lands, by the post-landing review (part 3g/3, reviewMergedCommit).
|
|
110
|
+
state.lastApprovedHead = head;
|
|
111
|
+
return { decision: "approved", verifiedHead: retry.pre.verifiedHead };
|
|
221
112
|
}
|
|
222
113
|
/** Review the change ahead of main, retrying a failed reviewer run in place (twice, 30 s apart)
|
|
223
114
|
* before the attempt ends `failed`: a run that timed out, was killed, died in transport or at
|
|
224
115
|
* the provider, or stated no VERDICT after its recovery turn. A resumable session is resumed,
|
|
225
116
|
* otherwise the retry starts fresh (reviewAttempt). A configuration error and an abort are not
|
|
226
117
|
* retried. While the reviewer's provider is held the retry waits for the hold to lift and
|
|
227
|
-
* costs nothing. Each pause runs without the caller's team permit (
|
|
118
|
+
* costs nothing. Each pause runs without the caller's team permit (idleWithoutPermit):
|
|
228
119
|
* a verification hands its lease back while it waits and takes it back before the next attempt.
|
|
229
120
|
* See reviewAttempt for the rest of the gate's contract. */
|
|
230
121
|
export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFriction) {
|
|
@@ -241,10 +132,10 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
241
132
|
warnEventBestEffort(ctx.root, ctx.role, `the reviewer run failed (${result.detail ?? "no detail"}) — ${held ? "waiting for the provider hold to lift, then " : ""}retrying the review in place`);
|
|
242
133
|
// The pause holds no team permit: a verification's lease is handed back while it waits and
|
|
243
134
|
// re-taken before the next attempt, so a reviewer that keeps failing does not starve the roles.
|
|
244
|
-
await
|
|
245
|
-
await
|
|
135
|
+
await idleWithoutPermit(async () => {
|
|
136
|
+
await sleepInterruptible(delayMs, ctx.signal);
|
|
246
137
|
while (ctx.reviewHeld?.() && !ctx.signal?.aborted)
|
|
247
|
-
await
|
|
138
|
+
await sleepInterruptible(Math.min(delayMs, 5000), ctx.signal);
|
|
248
139
|
}, ctx.signal);
|
|
249
140
|
if (ctx.signal?.aborted)
|
|
250
141
|
return { decision: "failed", aborted: true };
|
|
@@ -253,16 +144,3 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
|
|
|
253
144
|
retry.attempt += 1;
|
|
254
145
|
}
|
|
255
146
|
}
|
|
256
|
-
function sleepUnlessAborted(ms, signal) {
|
|
257
|
-
return new Promise((resolve) => {
|
|
258
|
-
if (signal?.aborted)
|
|
259
|
-
return resolve();
|
|
260
|
-
const done = () => {
|
|
261
|
-
clearTimeout(timer);
|
|
262
|
-
signal?.removeEventListener("abort", done);
|
|
263
|
-
resolve();
|
|
264
|
-
};
|
|
265
|
-
const timer = setTimeout(done, ms);
|
|
266
|
-
signal?.addEventListener("abort", done, { once: true });
|
|
267
|
-
});
|
|
268
|
-
}
|
|
@@ -32,47 +32,57 @@ export function secretEnvValues(env) {
|
|
|
32
32
|
}
|
|
33
33
|
return out;
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
/** Push `value` into `out` as a credential named `name`, unless it is not a string, is a `$NAME`
|
|
36
|
+
* interpolation or `!command`, or fails `looksLikeSecret`. */
|
|
37
|
+
function addPiSecret(out, name, value) {
|
|
38
|
+
if (typeof value !== "string" || value.startsWith("$") || value.startsWith("!"))
|
|
39
|
+
return;
|
|
40
|
+
if (looksLikeSecret(value))
|
|
41
|
+
out.push({ name, value });
|
|
42
|
+
}
|
|
43
|
+
function modelsJsonCredentials(modelsPath) {
|
|
40
44
|
const out = [];
|
|
41
|
-
const add = (name, value) => {
|
|
42
|
-
if (typeof value !== "string" || value.startsWith("$") || value.startsWith("!"))
|
|
43
|
-
return;
|
|
44
|
-
if (looksLikeSecret(value))
|
|
45
|
-
out.push({ name, value });
|
|
46
|
-
};
|
|
47
45
|
const models = readJsonFile(modelsPath);
|
|
48
46
|
if (models && isJsonObject(models.providers)) {
|
|
49
47
|
for (const [p, cfg] of Object.entries(models.providers)) {
|
|
50
48
|
if (!isJsonObject(cfg))
|
|
51
49
|
continue;
|
|
52
|
-
|
|
50
|
+
addPiSecret(out, `models.json ${p}.apiKey`, cfg.apiKey);
|
|
53
51
|
if (isJsonObject(cfg.headers)) {
|
|
54
|
-
for (const [h, v] of Object.entries(cfg.headers))
|
|
55
|
-
|
|
52
|
+
for (const [h, v] of Object.entries(cfg.headers)) {
|
|
53
|
+
addPiSecret(out, `models.json ${p}.headers.${h}`, v);
|
|
54
|
+
}
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function authJsonCredentials(modelsPath) {
|
|
61
|
+
const out = [];
|
|
59
62
|
const auth = readJsonFile(path.join(path.dirname(modelsPath), "auth.json"));
|
|
60
63
|
if (auth) {
|
|
61
64
|
for (const [p, cred] of Object.entries(auth)) {
|
|
62
65
|
if (!isJsonObject(cred))
|
|
63
66
|
continue;
|
|
64
67
|
for (const f of ["key", "access", "refresh"])
|
|
65
|
-
|
|
68
|
+
addPiSecret(out, `auth.json ${p}.${f}`, cred[f]);
|
|
66
69
|
if (isJsonObject(cred.env)) {
|
|
67
70
|
for (const [n, v] of Object.entries(cred.env)) {
|
|
68
71
|
if (SECRET_NAME.test(n))
|
|
69
|
-
|
|
72
|
+
addPiSecret(out, `auth.json ${p}.env.${n}`, v);
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
77
|
return out;
|
|
75
78
|
}
|
|
79
|
+
/** The credentials pi stores: literal `apiKey`s and header values in `models.json`, and the
|
|
80
|
+
* `key`/`access`/`refresh` fields (plus secret-named `env` values) of the sibling `auth.json`.
|
|
81
|
+
* `$NAME` interpolations (secretEnvValues covers those) and `!command` values are skipped. A
|
|
82
|
+
* missing or torn file yields nothing; never throws. */
|
|
83
|
+
export function piCredentialValues(modelsPath = piModelsPath()) {
|
|
84
|
+
return [...modelsJsonCredentials(modelsPath), ...authJsonCredentials(modelsPath)];
|
|
85
|
+
}
|
|
76
86
|
/** Every secret the gate knows: secret-named environment values (named `$NAME`) then pi's stored
|
|
77
87
|
* credentials (named `pi <file> <path>`), the first entry winning when two share a value. */
|
|
78
88
|
export function knownSecrets(env = process.env, modelsPath = piModelsPath()) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
3
3
|
import { deleteRefIfAt, refSha } from "../git/git.js";
|
|
4
|
-
import {
|
|
4
|
+
import { runGit } from "../git/git-run.js";
|
|
5
5
|
import { branchName, mergeRefName, rejectedRefName, reviewSessionDir, roleInboxDir, roleNotesPath, sessionDir, statePath } from "../paths.js";
|
|
6
6
|
import { errorMessage } from "../text/text.js";
|
|
7
7
|
import { LEGACY_ROLE_IDS } from "./legacy-role-ids.js";
|
|
@@ -72,8 +72,11 @@ export async function migrateLegacyRoleIds(root, legacy = LEGACY_ROLE_IDS) {
|
|
|
72
72
|
return null;
|
|
73
73
|
if ((await refSha(root, to)) !== null)
|
|
74
74
|
return `kept ${from} and ${to}: both exist`;
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// runGit, not gitTry: a failure here is reported verbatim into notes.failed, and gitTry
|
|
76
|
+
// would swallow the cause (git's stderr and exit code), leaving a bare `update-ref ${to}
|
|
77
|
+
// failed` with no reason to act on — the sibling branch move above propagates the same
|
|
78
|
+
// GitError through runGit, so this stays in family.
|
|
79
|
+
await runGit(root, ["update-ref", to, sha]);
|
|
77
80
|
await deleteRefIfAt(root, from, sha);
|
|
78
81
|
return `moved ${from} to ${to}`;
|
|
79
82
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* in roles.ts. Pure catalog data — the shared instruction fragments role prompts embed live in
|
|
5
5
|
* role-guidance.ts (import-only downward, so prompt assembly depends on both without a cycle).
|
|
6
6
|
* roles.ts re-exports ROLES and Role, so every existing importer keeps its import path. */
|
|
7
|
-
import { backlogMoveGuidance, CHECK_DECLARATION_MARKER, CONVERGENCE_HOLD_MARKER, DECOMPOSITION_GUIDANCE, NEEDS_REVIEW_NOTE, PLAN_QUEUE_GUIDANCE, PLAN_SIZING, searchGuidance, VALIDATION_GAP_GUIDANCE, VALIDATION_GAP_QUERY, VALIDATION_GAP_TALLY, } from "./role-guidance.js";
|
|
7
|
+
import { backlogMoveGuidance, CHECK_DECLARATION_MARKER, CONVERGENCE_HOLD_MARKER, DECOMPOSITION_GUIDANCE, NEEDS_REVIEW_NOTE, PLAN_DEFECT_MARKER, PLAN_DEFECT_NOTE, PLAN_QUEUE_GUIDANCE, PLAN_SIZING, searchGuidance, VALIDATION_GAP_GUIDANCE, VALIDATION_GAP_QUERY, VALIDATION_GAP_TALLY, } from "./role-guidance.js";
|
|
8
8
|
import { roleCommitGrep } from "./legacy-role-ids.js";
|
|
9
9
|
import { NOTHING_TO_DO } from "../verdict/reply-contract.js";
|
|
10
10
|
/** The trailer the readme role's commits carry and its `git log --grep` reads back; git parses
|
|
@@ -50,8 +50,10 @@ export const ROLES = [
|
|
|
50
50
|
6. Mark the plan done in PLANS.md — ${backlogMoveGuidance("PLANS.md", "## Planned", "## Done", "PLANS-DONE.md", "done")}
|
|
51
51
|
7. Verify last, after the PLANS.md edit, so one run of the project's check covers everything
|
|
52
52
|
you changed (see "Leave the project working" below).
|
|
53
|
-
A plan
|
|
54
|
-
|
|
53
|
+
A plan whose approach or criteria are wrong is a finding the plan role owns, not a refusal:
|
|
54
|
+
append ${PLAN_DEFECT_NOTE} under its heading with the objection recorded, skip it, and implement
|
|
55
|
+
the next available plan that fits — do not force it. Reserve **Refused …** for a plan that
|
|
56
|
+
should not be done at all, which is the user's call to decide.`,
|
|
55
57
|
},
|
|
56
58
|
{
|
|
57
59
|
id: "bugfix",
|
|
@@ -113,6 +115,12 @@ within that budget, there is nothing to do.`,
|
|
|
113
115
|
the quoted objections and the code they name and rewrite the plan so an implementation
|
|
114
116
|
following it would answer each — correcting the approach or the anchors, tightening the
|
|
115
117
|
acceptance criteria, or splitting it per PLAN_SIZING — then remove the note.
|
|
118
|
+
- A plan carrying a ${PLAN_DEFECT_MARKER} note (the feature implementer found the plan's
|
|
119
|
+
approach or criteria wrong) also outranks adding another plan: read the objection after the
|
|
120
|
+
marker and the code it names and rewrite the plan so an implementation following it would
|
|
121
|
+
answer it — correcting the approach or the anchors, tightening the acceptance criteria,
|
|
122
|
+
splitting it per PLAN_SIZING, or refusing it with the objection recorded when it should not
|
|
123
|
+
be done at all — then remove the note. That replan is this run's task.
|
|
116
124
|
- A plan carrying a **Needs review …** note that quotes the reviewer's surviving objections
|
|
117
125
|
(a final-round rejection — \`rejected after <N> review rounds\`) also outranks adding another
|
|
118
126
|
plan: read the quoted objections and the code they name, then rewrite the plan so an
|
|
@@ -125,11 +125,6 @@ export const VALIDATION_GAP_QUERY = `grep -snE '\\*\\*Validation gap:\\*\\* (${V
|
|
|
125
125
|
* feature role appends (gate-notes.ts) also begins with this prefix, so it reads as a Needs-review
|
|
126
126
|
* note and is classified by entryHold accordingly. */
|
|
127
127
|
export const NEEDS_REVIEW_PREFIX = `**Needs review `;
|
|
128
|
-
/** The tag text before a held count, read by the backlog (backlog-eligibility's heldCount) so
|
|
129
|
-
* the note form and the parser cannot drift. No writer emits `(held N)` anymore — the feature
|
|
130
|
-
* note is flat — but heldCount still parses the tag from a Needs-review note line already on
|
|
131
|
-
* disk until part 2f/6 removes the reader. */
|
|
132
|
-
export const HELD_TAG_PREFIX = `(held `;
|
|
133
128
|
/** The markdown note the feature role appends to a plan it cannot finish in one run, so the plan
|
|
134
129
|
* role can split it. Mirrors the **Refused …** note convention: a note the next fresh run reads,
|
|
135
130
|
* with no code parsing it. Defined once so the feature and plan texts and the director's routing
|
|
@@ -137,7 +132,7 @@ export const HELD_TAG_PREFIX = `(held `;
|
|
|
137
132
|
* flat, so a new hold never escalates to the operator's desk. */
|
|
138
133
|
export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: too large for one run**`;
|
|
139
134
|
/** The literal reason the automatic convergence hold writes into a held plan's note
|
|
140
|
-
* (holdEntryAfterExhaustion, src/runner/revision.ts, builds the note as
|
|
135
|
+
* (holdEntryAfterExhaustion, src/runner/runner-revision.ts, builds the note as
|
|
141
136
|
* `**Needs review … by <role>: could not converge — <surviving objections>**`, INV-BH4).
|
|
142
137
|
* Exported so the writer and the reader share one literal: it sits between holdEntryAfterExhaustion
|
|
143
138
|
* (which writes the note) and the plan role's charter (role-catalog.ts `find`) which reads it, so
|
|
@@ -146,12 +141,21 @@ export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature:
|
|
|
146
141
|
* each hold to its own action without matching on a second literal. Add no second spelling of
|
|
147
142
|
* this text. */
|
|
148
143
|
export const CONVERGENCE_HOLD_MARKER = "could not converge";
|
|
149
|
-
/** The marker
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
|
|
144
|
+
/** The literal marker the feature role writes inside a Needs-review note when a planned entry's
|
|
145
|
+
* approach or acceptance criteria resists implementation, so the plan role re-specifies the plan
|
|
146
|
+
* instead of the refusal landing on the human user's desk. Mirrors the convergence hold: the note
|
|
147
|
+
* reads `**Needs review <YYYY-MM-DD> by feature: plan defect — <objection>**`, carrying this
|
|
148
|
+
* text only in the note body after the prefix, so entryHold classes it as a plain needs-review
|
|
149
|
+
* hold and the feature role (which reads a Needs-review note as a park) skips it. Exported so the
|
|
150
|
+
* feature charter (which tells the feature role to write it) and the plan charter (which reads it
|
|
151
|
+
* to replan) share one literal like the convergence marker does. Add no second spelling of this
|
|
152
|
+
* text. */
|
|
153
|
+
export const PLAN_DEFECT_MARKER = "plan defect";
|
|
154
|
+
/** The template for the replan hand-back note the feature role appends to a planned entry whose
|
|
155
|
+
* approach or criteria are wrong, routing it to the plan role instead of a Refused note. The
|
|
156
|
+
* objection the feature role recorded goes after the marker's dash, so the plan charter can name
|
|
157
|
+
* it (`the objection after the marker`) without parsing the note body itself. */
|
|
158
|
+
export const PLAN_DEFECT_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: ${PLAN_DEFECT_MARKER} — <objection>**`;
|
|
155
159
|
/** The literal marker the plan role writes as its own line under the PLANS.md `## Planned` entry
|
|
156
160
|
* that declares this project's deterministic check, and the feature role reads as that entry's
|
|
157
161
|
* identity. It is the one decision point between the two no-check notes (gate-notes.ts
|
|
@@ -2,9 +2,7 @@ import { joinParts } from "../text/text.js";
|
|
|
2
2
|
import { agree, shortSpanPhrase } from "../text/phrases.js";
|
|
3
3
|
import { compactTokens, usd } from "../text/format.js";
|
|
4
4
|
import { clockHm, formatAge, formatTimestamp, humanAgo, nextRunPhrase, secondsUntil } from "../text/datetime.js";
|
|
5
|
-
import {
|
|
6
|
-
import { windowLabel } from "../agent/subscription-account.js";
|
|
7
|
-
import { backendDefaultLabel, backendDisplayName } from "../agent/backend-traits.js";
|
|
5
|
+
import { backendDefaultLabel } from "../agent/backend-traits.js";
|
|
8
6
|
import { yieldMultiplierSuffix } from "../scheduling/backoff.js";
|
|
9
7
|
import { quietWindowEnd } from "../scheduling/quiet-hours.js";
|
|
10
8
|
/** The terminal's view of one role — what `tumwater role <id>` prints, rendered as Markdown
|
|
@@ -133,7 +131,7 @@ function modelLine(p) {
|
|
|
133
131
|
/** A fallback pair as one readable token, agent-qualified exactly like modelLine — a non-pi
|
|
134
132
|
* backend (its `agent`, or a provider that names one, `claude`) is named as the selector's
|
|
135
133
|
* namespace; a pi pair keeps `provider/model`; neither half present falls back to the
|
|
136
|
-
* backend-default label. One home for the
|
|
134
|
+
* backend-default label. One home for the Budget fallback line, so a
|
|
137
135
|
* claude fallback reads `claude/claude-sonnet-5-5`, not a bare id (PLANS.md "Mixed claude +
|
|
138
136
|
* pi fleets, part 3/4" gap 7). */
|
|
139
137
|
function fallbackPairLabel(fb, backendDefault) {
|
|
@@ -147,26 +145,11 @@ function fallbackPairLabel(fb, backendDefault) {
|
|
|
147
145
|
return fb.provider;
|
|
148
146
|
return backendDefault;
|
|
149
147
|
}
|
|
150
|
-
/**
|
|
151
|
-
|
|
152
|
-
* codex subscription (plans/codex-backend.md part 3c/9), so a reader knows whose window it
|
|
153
|
-
* is. Claude keeps today's bare window type ("5h") so its role view stays byte-identical. */
|
|
154
|
-
function billingWindowLabel(billingBackend, windowType) {
|
|
155
|
-
const type = windowType !== undefined ? windowLabel(windowType) : "";
|
|
156
|
-
const backend = billingBackend !== undefined && billingBackend !== "claude" ? backendDisplayName(billingBackend) : "";
|
|
157
|
-
return [backend, type].filter(Boolean).join(" ");
|
|
158
|
-
}
|
|
159
|
-
/** Why a tier moved down its fallback chain, as the role view words it. The window phrase
|
|
160
|
-
* names the backend in play ("Codex 7d window used up") when a non-claude backend moved the
|
|
161
|
-
* role; claude keeps today's wording byte-identical, and an unknown backend falls back to
|
|
162
|
-
* today's hardcoded "Claude". */
|
|
163
|
-
function chainReasonPhrase(reason, billingBackend, windowType) {
|
|
148
|
+
/** Why a tier moved down its fallback chain, as the role view words it. */
|
|
149
|
+
function chainReasonPhrase(reason) {
|
|
164
150
|
switch (reason) {
|
|
165
151
|
case "window":
|
|
166
|
-
|
|
167
|
-
return `${billingWindowLabel(billingBackend, windowType)} window used up`;
|
|
168
|
-
}
|
|
169
|
-
return `${windowType !== undefined ? claudeWindowLabel(windowType) : "Claude"} window used up`;
|
|
152
|
+
return "window used up";
|
|
170
153
|
case "reserve":
|
|
171
154
|
return "window reserve reached";
|
|
172
155
|
case "cap":
|
|
@@ -186,7 +169,7 @@ function chainStepLine(p) {
|
|
|
186
169
|
if (!c)
|
|
187
170
|
return null;
|
|
188
171
|
const resets = c.returnsAt !== undefined ? `, resets ${clockHm(c.returnsAt)}` : "";
|
|
189
|
-
return `- Running on fallback ${c.step} of ${c.of} (${chainReasonPhrase(c.reason
|
|
172
|
+
return `- Running on fallback ${c.step} of ${c.of} (${chainReasonPhrase(c.reason)}${resets})`;
|
|
190
173
|
}
|
|
191
174
|
/** One recent run outcome as a Markdown bullet line — `<time> · <result> · <span> —
|
|
192
175
|
* <summary>` — so `tumwater role <id>` and the TUI notes view read a role's discrete
|
|
@@ -254,13 +237,6 @@ export function renderRoleMarkdown(p, now = Date.now()) {
|
|
|
254
237
|
// fallback's own documented state). `unknown` when it cannot be known (a non-pi seam, an
|
|
255
238
|
// unresolvable pair, a model with no declared window).
|
|
256
239
|
lines.push(`- Context window: ${p.contextWindow === null ? "unknown" : `${compactTokens(p.contextWindow)} tokens`}`);
|
|
257
|
-
// Per-provider concurrency cap (plans/provider-concurrency.md §8): name the key whose cap
|
|
258
|
-
// bounds the role's concurrent runs, plus the cap itself. Present only while
|
|
259
|
-
// `maxConcurrentPerProvider` maps this seam's key — a cap-less default team has no
|
|
260
|
-
// resolvable key entry, so its role view renders no new line and stays byte-identical.
|
|
261
|
-
if (p.providerKey !== undefined && p.providerCap !== undefined) {
|
|
262
|
-
lines.push(`- Provider: ${p.providerKey} (cap ${p.providerCap})`);
|
|
263
|
-
}
|
|
264
240
|
// Image input (plans/image-input.md "Surfaces", part 5/5): whether this role's own model
|
|
265
241
|
// reads images, and — when it cannot — where a run that reads an image reroutes. Not just
|
|
266
242
|
// `yes`/`no`: a text-only role that falls back names the target, so `tumwater role <id>`
|
|
@@ -274,18 +250,6 @@ export function renderRoleMarkdown(p, now = Date.now()) {
|
|
|
274
250
|
: "no"
|
|
275
251
|
: "unknown";
|
|
276
252
|
lines.push(`- Images: ${images}`);
|
|
277
|
-
// Whether the review gate reviews this role's commits after they merge (role-view.ts's
|
|
278
|
-
// reviewedAfterMerge). Present only while review.enabled is on and review.afterMerge lists
|
|
279
|
-
// the role's base id — a pre-merge-reviewed role or a gate-off config omits the line.
|
|
280
|
-
if (p.reviewedAfterMerge) {
|
|
281
|
-
lines.push(`- Review: after merge (a rejected change stays on main with the reviewer's recorded reasons)`);
|
|
282
|
-
}
|
|
283
|
-
// Model failure fallback, part 2/2: name the off-model pair, when the episode began, and
|
|
284
|
-
// the provider-class failure that tripped it, so `tumwater role <id>` explains the state.
|
|
285
|
-
if (p.modelFallback) {
|
|
286
|
-
const pair = fallbackPairLabel(p.modelFallback, backendDefault(p));
|
|
287
|
-
lines.push(`- Model fallback: ${pair} — on fallback since ${formatTimestamp(p.modelFallback.since)} (primary failing: ${p.modelFallback.reason})`);
|
|
288
|
-
}
|
|
289
253
|
if (p.fallback) {
|
|
290
254
|
const pair = fallbackPairLabel(p.fallback, backendDefault(p));
|
|
291
255
|
lines.push(`- Over-cap model: ${pair} (${p.fallbackFree ? "free" : "priced"})`);
|
|
@@ -296,10 +260,7 @@ export function renderRoleMarkdown(p, now = Date.now()) {
|
|
|
296
260
|
// The claude seam's billing (plans/agent-backends.md §7/§8, PLANS.md part 6d/7): absent on
|
|
297
261
|
// a pi seam, so its role view is byte-identical to before.
|
|
298
262
|
if (p.billing) {
|
|
299
|
-
|
|
300
|
-
? ` (${billingWindowLabel(p.billingBackend, p.billingWindow)} window)`
|
|
301
|
-
: "";
|
|
302
|
-
lines.push(`- Billing: ${p.billing === "api" ? "API" : p.billing}${window}`);
|
|
263
|
+
lines.push(`- Billing: ${p.billing === "api" ? "API" : p.billing}`);
|
|
303
264
|
}
|
|
304
265
|
lines.push(`- Min run interval: ${p.minRunIntervalSeconds}s`);
|
|
305
266
|
if (p.instances !== undefined)
|