tumwater 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +42 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +4 -3
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +402 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +246 -55
- package/dist/src/backlog/backlog-cli-view.js +280 -0
- package/dist/src/backlog/backlog-cli.js +85 -274
- package/dist/src/backlog/backlog-commit.js +4 -5
- package/dist/src/backlog/backlog-eligibility.js +175 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +254 -0
- package/dist/src/backlog/backlog-release.js +117 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-culprit.js +94 -0
- package/dist/src/baseline/main-guardrails.js +209 -0
- package/dist/src/baseline/main-red.js +139 -32
- package/dist/src/baseline/main-revert.js +133 -0
- package/dist/src/baseline/main-watch.js +184 -0
- package/dist/src/brief/brief-edit.js +9 -11
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget.js +30 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +118 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +150 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +308 -590
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-role-command.js +420 -0
- package/dist/src/cli/cli-start-summary.js +160 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
- package/dist/src/cli/completion.js +149 -22
- package/dist/src/cli/config-commands.js +41 -18
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +189 -73
- package/dist/src/cli/log-commands.js +148 -82
- package/dist/src/cli/question-commands.js +205 -10
- package/dist/src/cli.js +106 -43
- package/dist/src/collections.js +26 -10
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +13 -1
- package/dist/src/config/config-fallbacks.js +93 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +141 -199
- package/dist/src/config/config-views.js +127 -53
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +131 -264
- package/dist/src/config/config.js +48 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
- package/dist/src/doctor/doctor-backlog-plans.js +263 -0
- package/dist/src/doctor/doctor-backlog.js +100 -496
- package/dist/src/doctor/doctor-check-names.js +81 -0
- package/dist/src/doctor/doctor-checks.js +52 -579
- package/dist/src/doctor/doctor-claude-checks.js +145 -0
- package/dist/src/doctor/doctor-codex-checks.js +279 -0
- package/dist/src/doctor/doctor-contention-checks.js +380 -0
- package/dist/src/doctor/doctor-holds.js +82 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +236 -402
- package/dist/src/doctor/doctor-notify.js +3 -4
- package/dist/src/doctor/doctor-opencode-checks.js +162 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-project-checks.js +303 -0
- package/dist/src/doctor/doctor.js +121 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +7 -11
- package/dist/src/events/event-format.js +181 -86
- package/dist/src/events/event-labels.js +12 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +39 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +60 -24
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +56 -56
- package/dist/src/failure/failure-render.js +31 -23
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +99 -35
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +177 -84
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +108 -58
- package/dist/src/gates/maintenance-quota.js +10 -13
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +77 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +36 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +42 -18
- package/dist/src/gui/gui-backlog-endpoints.js +155 -0
- package/dist/src/gui/gui-command.js +8 -13
- package/dist/src/gui/gui-config-commands.js +94 -72
- package/dist/src/gui/gui-endpoint-commands.js +4 -3
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +108 -13
- package/dist/src/gui/gui-prompt-commands.js +11 -14
- package/dist/src/gui/gui-report-endpoints.js +90 -0
- package/dist/src/gui/gui-server.js +11 -4
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +147 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +193 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +25 -9
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +81 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +150 -11
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +90 -0
- package/dist/src/merge/merge-queue-render.js +49 -11
- package/dist/src/merge/merge-queue.js +31 -15
- package/dist/src/merge/merge-requeue.js +102 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +33 -11
- package/dist/src/merge/merge-to-main.js +86 -27
- package/dist/src/merge/merge-top.js +69 -0
- package/dist/src/merge/merge-verify.js +12 -10
- package/dist/src/notes-cli.js +30 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +320 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
- package/dist/src/orchestrator/orchestrator.js +128 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +201 -0
- package/dist/src/orchestrator/scheduling-skip.js +185 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +116 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +18 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +121 -211
- package/dist/src/prompt/notes-backlog.js +181 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +129 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +161 -77
- package/dist/src/report/report-fold.js +129 -52
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +133 -64
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +26 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/known-flakes.js +100 -1
- package/dist/src/review/post-merge-review.js +106 -0
- package/dist/src/review/protected-edits.js +4 -5
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +155 -360
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +69 -12
- package/dist/src/roles/role-guidance.js +55 -20
- package/dist/src/roles/role-labels.js +1 -2
- package/dist/src/roles/role-render.js +67 -50
- package/dist/src/roles/role-view.js +59 -47
- package/dist/src/roles/roles.js +31 -31
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/{tick → run}/qa-coverage.js +9 -27
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +179 -0
- package/dist/src/run/run-detail-render.js +98 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +172 -0
- package/dist/src/run/stage-path-checks.js +730 -0
- package/dist/src/{loop → runner}/leftover.js +80 -25
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +202 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +107 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +102 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +43 -16
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +28 -28
- package/dist/src/status/status-roles.js +39 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +5 -6
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +63 -17
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +57 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +67 -13
- package/dist/src/ui/badges.js +65 -37
- package/dist/src/ui/gui/gui-client-backlog.js +292 -0
- package/dist/src/ui/gui/gui-client-boot.js +32 -26
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
- package/dist/src/ui/gui/gui-client-drawer.js +126 -285
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +434 -67
- package/dist/src/ui/gui/gui-client-labels.js +1 -2
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +55 -51
- package/dist/src/ui/gui/gui-client-pending.js +227 -23
- package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
- package/dist/src/ui/gui/gui-client-report.js +87 -427
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +56 -4
- package/dist/src/ui/gui/gui-client-team.js +25 -465
- package/dist/src/ui/gui/gui-client.js +27 -25
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +9 -7
- package/dist/src/ui/gui/gui-styles.js +34 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +136 -49
- package/dist/src/ui/status-render.js +101 -82
- package/dist/src/ui/team-alerts.js +248 -57
- package/dist/src/ui/tone.js +5 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +51 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +3 -5
- package/dist/src/ui/tui/tui-editor-backends.js +132 -0
- package/dist/src/ui/tui/tui-editor-brief.js +10 -12
- package/dist/src/ui/tui/tui-editor-budget.js +35 -31
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
- package/dist/src/ui/tui/tui-editor-config.js +86 -0
- package/dist/src/ui/tui/tui-editor-entry.js +36 -47
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +37 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
- package/dist/src/ui/tui/tui-editor-interval.js +11 -9
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +3 -5
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
- package/dist/src/ui/tui/tui-editor-retire.js +4 -6
- package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +60 -34
- package/dist/src/ui/tui/tui-editor-slots.js +23 -0
- package/dist/src/ui/tui/tui-editors.js +35 -2
- package/dist/src/ui/tui/tui-frame.js +56 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +24 -9
- package/dist/src/ui/tui/tui-pane-activity.js +14 -11
- package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +58 -8
- package/dist/src/ui/tui/tui-pane-history.js +402 -82
- package/dist/src/ui/tui/tui-pane-pending.js +308 -13
- package/dist/src/ui/tui/tui-pane-queued.js +71 -55
- package/dist/src/ui/tui/tui-pane-report.js +46 -14
- package/dist/src/ui/tui/tui-pane-session.js +25 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +80 -23
- package/dist/src/ui/tui/tui-panes.js +100 -30
- package/dist/src/ui/tui/tui.js +14 -14
- package/dist/src/verdict/fix-claim.js +82 -32
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +42 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
2
3
|
import { branchExists, deleteRef, commitMessage, changeBaseRev, headOf } from "../git/git.js";
|
|
3
4
|
import { git, gitTry } from "../git/git-run.js";
|
|
@@ -5,32 +6,32 @@ import { abortSync, ensureWorktree, resetWorktreeToMain } from "../git/worktree.
|
|
|
5
6
|
import { leaseSlot } from "../git/worktree-pool.js";
|
|
6
7
|
import { slotCount } from "../config/config.js";
|
|
7
8
|
import { useWorktree } from "../git/worktree-use.js";
|
|
9
|
+
import { readPiListedModels } from "../pi/pi-listed-models.js";
|
|
8
10
|
import { branchName, worktreePath, mergeRefName, rejectedRefName } from "../paths.js";
|
|
9
11
|
import { logEventBestEffort } from "../events/events.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
+
import { buildConflictHandbackNote, buildSalvageRestoreNote } from "../prompt/gate-prompts.js";
|
|
13
|
+
import { buildConflictDiscardNote, buildMainKnownRedNote, buildRevisionNote, buildRejectedReviewNote, } from "../prompt/gate-notes.js";
|
|
14
|
+
import { modelSelectorField, resolvedBackendConfig, reviewRunConfig, scheduleConfigForRole, seamHoldKey, withModelOverrides, } from "../config/config-views.js";
|
|
12
15
|
import { fallbackRoleConfig } from "../config/config-fallbacks.js";
|
|
13
16
|
import { imageReroute, seamImageInput } from "../agent/image-input.js";
|
|
14
17
|
import { piModelsPath, readPiProviders } from "../pi/pi-models.js";
|
|
15
18
|
import { peekPrompt, peekRolePrompt } from "../inbox/inbox.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { briefStatusIncomplete } from "../brief/brief.js";
|
|
19
|
+
import { planRunStart, planWithSalvagedWork, takesSalvagedWork } from "../run/run-resume.js";
|
|
20
|
+
import { restoreSalvagedWork } from "../git/slot-salvage.js";
|
|
21
|
+
import { DEFAULT_BACKEND } from "../agent/agent-backend.js";
|
|
22
|
+
import { foldAgentCommitsSince } from "../run/run-stage.js";
|
|
23
|
+
import { planBacklogFull } from "../scheduling/claims.js";
|
|
22
24
|
import { modelFallbackActive, modelFallbackProbe } from "./model-fallback.js";
|
|
23
|
-
import {
|
|
25
|
+
import { finalizeRun } from "../run/run-finalize.js";
|
|
24
26
|
import { errorMessage } from "../text/text.js";
|
|
25
|
-
import { appendPromptNote } from "../text/format.js";
|
|
26
|
-
import { recoverLeftover } from "./leftover.js";
|
|
27
|
+
import { appendPromptNote, prependPromptNote } from "../text/format.js";
|
|
28
|
+
import { hasUnmergedPin, recoverLeftover } from "./leftover.js";
|
|
27
29
|
import { applyRevision, applyWithConflicts } from "./revision.js";
|
|
28
30
|
import { conflictedFileHunks, hasConflictMarkers, mainCommitsTouching } from "../merge/merge-git.js";
|
|
29
31
|
import { changedFiles } from "../git/git-diff.js";
|
|
30
|
-
import {
|
|
32
|
+
import { handlePiResultPhase } from "./runner-outcome.js";
|
|
31
33
|
import { bugfixMainRedNote, mainRedGate } from "../baseline/main-red.js";
|
|
32
|
-
import {
|
|
33
|
-
import { extractFlow } from "../verdict/reply-contract.js";
|
|
34
|
+
import { extractFlow, extractFiled } from "../verdict/reply-contract.js";
|
|
34
35
|
/** The intent the author needs to resolve a handed-back change: the change's own commit message
|
|
35
36
|
* and the main commits that touched its conflicted files since its merge-base. Mirrors what
|
|
36
37
|
* resolveConflict gathers for the merge resolver (src/merge/merge-to-main.ts) and feeds the
|
|
@@ -59,68 +60,69 @@ async function markerBearingFiles(wt) {
|
|
|
59
60
|
const files = await changedFiles(wt);
|
|
60
61
|
return files.filter((f) => hasConflictMarkers(wt, [f]));
|
|
61
62
|
}
|
|
62
|
-
/** Run one full
|
|
63
|
-
* log the start, run the pipeline, then let
|
|
64
|
-
* failed
|
|
65
|
-
* and observable. */
|
|
66
|
-
export async function
|
|
63
|
+
/** Run one full run of a role runner: resolve the config (including the model-fallback probe),
|
|
64
|
+
* log the start, run the pipeline, then let finalizeRun schedule the next run. Never throws: a
|
|
65
|
+
* failed run is an "error" result, saved and logged like any other so the runner stays
|
|
66
|
+
* resumable and observable. */
|
|
67
|
+
export async function runPhase(ctx) {
|
|
67
68
|
const s = ctx.state;
|
|
68
69
|
// This role's view of the config (per-role provider/model/thinking + minRunIntervalSeconds
|
|
69
70
|
// overrides): resolved once so every interval-based scheduling branch honors a slow
|
|
70
|
-
// clock (e.g. the steward's ~6 h) and a live-reloaded config applies from this
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
// Model-fallback episode (src/loop/model-fallback.ts): while one is active the tick runs
|
|
71
|
+
// clock (e.g. the steward's ~6 h) and a live-reloaded config applies from this run on.
|
|
72
|
+
// While PLANS.md still has planning room (planBacklogFull false), plan schedules with a zero
|
|
73
|
+
// gap so a productive plan run reruns on the next poll; an unproductive run drops to the
|
|
74
|
+
// idle backoff, and once the target is met the plans-full deferral holds the run. The shared
|
|
75
|
+
// verdict below is the same one the gate and isEligible read, and scheduleConfigForRole is
|
|
76
|
+
// the one home of the rule, so the scheduler that admits plan and the nextRunAt this run
|
|
77
|
+
// writes cannot disagree.
|
|
78
|
+
const planBacklogOpen = !planBacklogFull(ctx.root, ctx.config);
|
|
79
|
+
const primaryCfg = scheduleConfigForRole(ctx.config, ctx.role, planBacklogOpen);
|
|
80
|
+
// Model-fallback episode (src/runner/model-fallback.ts): while one is active the run runs
|
|
81
81
|
// on the role's tier fallback pair, and once the cooldown elapses it runs on the primary
|
|
82
|
-
// as the probe. Resolved at
|
|
82
|
+
// as the probe. Resolved at run start so the authoring run below and the end-of-run fold
|
|
83
83
|
// agree on which pair ran. Null fallback (the tier resolves to pause) keeps the feature off.
|
|
84
84
|
const now = Date.now();
|
|
85
85
|
const fallback = fallbackRoleConfig(ctx.config, ctx.role);
|
|
86
86
|
const probing = fallback !== null && modelFallbackProbe(s.modelFallback, now);
|
|
87
87
|
const onFallback = fallback !== null && modelFallbackActive(s.modelFallback, now);
|
|
88
|
-
// The fallback pair keeps primaryCfg's gap (they resolve the same one outside
|
|
89
|
-
// so a plan on its model fallback
|
|
88
|
+
// The fallback pair keeps primaryCfg's gap (they resolve the same one outside the plan
|
|
89
|
+
// schedule's zero-gap window), so a plan on its model fallback still schedules the same gap.
|
|
90
90
|
let cfg = onFallback && fallback !== null
|
|
91
91
|
? { ...fallback, minRunIntervalSeconds: primaryCfg.minRunIntervalSeconds }
|
|
92
92
|
: primaryCfg;
|
|
93
93
|
// Image-input reroute (plans/image-input.md part 4/5). A pending one-shot marker means
|
|
94
|
-
// THIS
|
|
95
|
-
// prior
|
|
96
|
-
// (cleared) here so it does not route a later
|
|
97
|
-
// whole
|
|
94
|
+
// THIS run is the blind-read reroute: start directly on the image-capable target the
|
|
95
|
+
// prior run recorded, over the kept worktree, in a fresh session. The marker is consumed
|
|
96
|
+
// (cleared) here so it does not route a later run; imageRerouteActive stays true for the
|
|
97
|
+
// whole run so the verdict's one-hop guard fails a second blind read instead of
|
|
98
98
|
// rerouting again. A preflight (below) sets the same active flag without a marker.
|
|
99
99
|
s.imageRerouteActive = false;
|
|
100
100
|
let preflightBlocked = false;
|
|
101
101
|
if (s.imageReroute) {
|
|
102
102
|
const r = s.imageReroute;
|
|
103
103
|
cfg = withModelOverrides(ctx.config, r.tier, {
|
|
104
|
+
backend: r.backend,
|
|
104
105
|
provider: r.provider,
|
|
105
106
|
model: r.model,
|
|
106
107
|
thinking: r.thinking,
|
|
107
108
|
});
|
|
108
|
-
// The marker is NOT cleared here:
|
|
109
|
-
// the handoff note (and clears it then), so a
|
|
110
|
-
// inbox) leaves the reroute pending for the next eligible
|
|
109
|
+
// The marker is NOT cleared here: planRunStart reads its `from`/`path` when it builds
|
|
110
|
+
// the handoff note (and clears it then), so a run skipped before the plan (an empty
|
|
111
|
+
// inbox) leaves the reroute pending for the next eligible run.
|
|
111
112
|
s.imageRerouteActive = true;
|
|
112
113
|
}
|
|
113
114
|
else {
|
|
114
115
|
// Preflight (plans/image-input.md "Attachments"): a queued prompt carrying an
|
|
115
116
|
// `[image attached: …]` line needs a model that can read images BEFORE any run starts,
|
|
116
|
-
// so a text-only
|
|
117
|
-
// learning it; with no target the
|
|
117
|
+
// so a text-only run hops straight to the target instead of spending a failed run
|
|
118
|
+
// learning it; with no target the run errors without spawning pi.
|
|
118
119
|
const nextPrompt = ctx.role === DIRECTOR_ROLE ? peekPrompt(ctx.root) : peekRolePrompt(ctx.root, ctx.role);
|
|
119
120
|
const carriesImages = nextPrompt !== null && /^\[image attached: .+\]$/m.test(nextPrompt);
|
|
120
121
|
if (carriesImages) {
|
|
121
122
|
const providers = readPiProviders(piModelsPath());
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
const listed = readPiListedModels(ctx.root);
|
|
124
|
+
if (seamImageInput(cfg, providers, undefined, listed) !== true) {
|
|
125
|
+
const target = imageReroute(ctx.config, ctx.role, ctx.rerouteRegime(now), undefined, null, undefined, null, listed);
|
|
124
126
|
if (target !== null) {
|
|
125
127
|
cfg = target.config;
|
|
126
128
|
s.imageRerouteActive = true;
|
|
@@ -142,142 +144,143 @@ export async function tickPhase(ctx) {
|
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
|
-
// Capture what this
|
|
147
|
+
// Capture what this run runs on (the orchestrator's budget handback matches it against
|
|
146
148
|
// the fallback pair) and clear any stale handback flag: an abort request that lands while
|
|
147
|
-
// the
|
|
149
|
+
// the runner is idle must not name a later run's resume.
|
|
148
150
|
// The pair is keyed by the seam that ran (seamHoldKey), not the pi provider: a claude
|
|
149
|
-
// fallback
|
|
150
|
-
// (src/gates/budget-gates.ts) compares against, so a
|
|
151
|
+
// fallback run carries `claude`, matching the configured fallback pair that runOnPair
|
|
152
|
+
// (src/gates/budget-gates.ts) compares against, so a run on a claude fallback is handed
|
|
151
153
|
// back when the budget gate reopens (PLANS.md "Mixed claude + pi fleets, part 2/4").
|
|
152
|
-
ctx.
|
|
154
|
+
ctx.setRunPair({ provider: seamHoldKey(cfg), model: cfg.model });
|
|
153
155
|
ctx.setHandedBack(false);
|
|
154
156
|
s.runs += 1;
|
|
155
|
-
// gen / peak ctx are per-
|
|
156
|
-
// reset before the start-of-
|
|
157
|
-
// an idle
|
|
158
|
-
// run of this
|
|
159
|
-
// end-of-
|
|
157
|
+
// gen / peak ctx are per-run windows, not lifetime totals (user decision 2026-08-25):
|
|
158
|
+
// reset before the start-of-run save so a working runner's columns grow live from 0 and
|
|
159
|
+
// an idle runner's show its last completed run. The runner's pi plumbing accumulates every pi
|
|
160
|
+
// run of this run (main + transient-timeout retry + conflict resolution) into them, and the
|
|
161
|
+
// end-of-run save persists the finished run's totals.
|
|
160
162
|
s.generatedTokens = 0;
|
|
161
163
|
s.peakContextTokens = 0;
|
|
162
164
|
ctx.usage.reset();
|
|
163
165
|
ctx.setRecoveryFailure(undefined);
|
|
164
|
-
// A fresh per-
|
|
165
|
-
// lands while the
|
|
166
|
-
ctx.
|
|
166
|
+
// A fresh per-run abort controller and a cleared user-abort flag: an abort request that
|
|
167
|
+
// lands while the runner is idle must not leak into the next run.
|
|
168
|
+
ctx.resetRunAbort();
|
|
167
169
|
s.running = true;
|
|
168
170
|
s.lastRunStartedAt = Date.now();
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
+
const runNumber = s.runs;
|
|
172
|
+
const runStartedAt = s.lastRunStartedAt;
|
|
171
173
|
ctx.save();
|
|
172
|
-
// The selector this
|
|
174
|
+
// The selector this run's runs start on (plans/model-tiers.md "Observability"): with
|
|
173
175
|
// the budget fallback active the config already names the fallback pair, so the logged
|
|
174
176
|
// string is what the runs actually use. Omitted when no model is configured (pi's own
|
|
175
177
|
// default), so old logs and old configs render unchanged.
|
|
176
178
|
logEventBestEffort(ctx.root, {
|
|
177
179
|
source: ctx.role,
|
|
178
180
|
type: "run_start",
|
|
179
|
-
run:
|
|
180
|
-
// A
|
|
181
|
+
run: runNumber,
|
|
182
|
+
// A run routed onto the image-capable target — a blind-read handoff or an attachment
|
|
181
183
|
// preflight — is stamped so the feed shows the reroute and the model it landed on.
|
|
182
184
|
...(s.imageRerouteActive ? { reroute: "image" } : {}),
|
|
183
185
|
...modelSelectorField(cfg),
|
|
184
186
|
});
|
|
185
187
|
let outcome;
|
|
186
188
|
try {
|
|
187
|
-
// The preflight can end the
|
|
189
|
+
// The preflight can end the run before any pi spawns: a text-only runner with an image
|
|
188
190
|
// attachment and no image-capable target has nothing to run, so it errors without
|
|
189
191
|
// spending a model call. Everything else runs the pipeline as usual.
|
|
190
192
|
outcome = preflightBlocked
|
|
191
193
|
? { result: "error" }
|
|
192
|
-
: await ctx.
|
|
194
|
+
: await ctx.runOnce(cfg, { fallback, probe: probing, primary: primaryCfg });
|
|
193
195
|
}
|
|
194
196
|
catch (err) {
|
|
195
197
|
outcome = { result: "error" };
|
|
196
198
|
s.lastError = errorMessage(err);
|
|
197
199
|
// An exception between the dequeue/reclaim and the pi run leaves the request only in
|
|
198
|
-
// the pending field — memory this failed
|
|
199
|
-
// left to re-queue it (the handlers run inside
|
|
200
|
+
// the pending field — memory this failed run is about to drop, with no outcome handler
|
|
201
|
+
// left to re-queue it (the handlers run inside runOnce, after the pi run). The queue is
|
|
200
202
|
// the durable store, so the catch re-queues whatever is still pending, like every
|
|
201
203
|
// unfulfilled outcome does; it is always null once a pi run has been accounted for.
|
|
202
204
|
ctx.pending.requeuePendingUnfulfilled();
|
|
203
205
|
}
|
|
204
|
-
// The
|
|
206
|
+
// The run that owned the dequeued request is over: remove its image files unless an
|
|
205
207
|
// unfulfilled outcome handed the request back to the queue (settleImages). Runs after the
|
|
206
208
|
// catch above, which is the last point a requeue can happen.
|
|
207
209
|
ctx.pending.settleImages();
|
|
208
210
|
// Everything after the pi run — the outcome folds, the main-head read, next-run
|
|
209
|
-
// scheduling, the episode warnings, and the run_end event — is the
|
|
210
|
-
// bookkeeping policy, not runner lifecycle: it lives in src/
|
|
211
|
+
// scheduling, the episode warnings, and the run_end event — is the run's per-result
|
|
212
|
+
// bookkeeping policy, not runner lifecycle: it lives in src/run/run-finalize.ts, which owns
|
|
211
213
|
// the ordering (the folds land on state before the schedule, the head read happens while
|
|
212
|
-
// `running` is still true) and the per-episode warning crossings. The
|
|
213
|
-
// start time are passed through as captured above: both must describe the
|
|
214
|
-
// announced, not whatever resetCounters (a documented mid-
|
|
214
|
+
// `running` is still true) and the per-episode warning crossings. The run number and
|
|
215
|
+
// start time are passed through as captured above: both must describe the run run_start
|
|
216
|
+
// announced, not whatever resetCounters (a documented mid-run operation) may have left
|
|
215
217
|
// on the shared state by finalize time.
|
|
216
|
-
const finalizeResult = await
|
|
218
|
+
const finalizeResult = await finalizeRun({
|
|
217
219
|
root: ctx.root,
|
|
218
220
|
role: ctx.role,
|
|
219
221
|
mainBranch: ctx.mainBranch,
|
|
220
222
|
config: cfg,
|
|
221
223
|
state: s,
|
|
222
224
|
outcome,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
+
runNumber,
|
|
226
|
+
runStartedAt: runStartedAt,
|
|
225
227
|
usage: ctx.usage,
|
|
226
228
|
recoveryFailure: ctx.recoveryFailure,
|
|
227
229
|
});
|
|
228
|
-
// The
|
|
229
|
-
// an idle
|
|
230
|
-
ctx.
|
|
230
|
+
// The run is over: the captured pair must not outlive it (a later poll must never match
|
|
231
|
+
// an idle runner's stale pair against a resumed fallback).
|
|
232
|
+
ctx.setRunPair(undefined);
|
|
231
233
|
return finalizeResult;
|
|
232
234
|
}
|
|
233
|
-
/** The
|
|
235
|
+
/** The run pipeline's start half: resume-or-fresh policy, then run in the director's own
|
|
234
236
|
* worktree or on a leased pool slot. Returns the setup's outcome; a null plan means nothing to
|
|
235
237
|
* run. */
|
|
236
|
-
export async function
|
|
238
|
+
export async function runOncePhase(ctx, cfg, fallbackCtx) {
|
|
237
239
|
const s = ctx.state;
|
|
238
|
-
// How this
|
|
239
|
-
// is the resume policy, not runner mechanics: it lives in src/
|
|
240
|
-
// consumes the resume flags and reclaims the interrupted
|
|
241
|
-
// plan means the
|
|
242
|
-
const plan =
|
|
240
|
+
// How this run starts — resume the interrupted session or run fresh, and which prompt —
|
|
241
|
+
// is the resume policy, not runner mechanics: it lives in src/run/run-resume.ts, which also
|
|
242
|
+
// consumes the resume flags and reclaims the interrupted run's re-queued prompt. A null
|
|
243
|
+
// plan means the runner has nothing to run.
|
|
244
|
+
const plan = planRunStart({
|
|
243
245
|
root: ctx.root,
|
|
244
246
|
role: ctx.role,
|
|
245
247
|
state: s,
|
|
246
248
|
pending: ctx.pending,
|
|
247
|
-
|
|
248
|
-
agent: cfg.agent ??
|
|
249
|
-
// The team's configured claude config dir, so
|
|
249
|
+
runPrompt: (backend) => ctx.runPrompt(backend),
|
|
250
|
+
agent: cfg.agent ?? DEFAULT_BACKEND,
|
|
251
|
+
// The team's configured claude config dir, so planRunStart's claude resume guard looks
|
|
250
252
|
// under the same dir the run's env writes its transcript to (note 4/4). Unset → undefined.
|
|
251
|
-
claudeConfigDir:
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
compact: true,
|
|
253
|
+
claudeConfigDir: resolvedBackendConfig(cfg, "claude").configDir,
|
|
254
|
+
// A director with no prompt but a kept merge pin still goes through the run, for its recovery.
|
|
255
|
+
recoveryOnly: ctx.role === DIRECTOR_ROLE && ctx.pending.get() === null && (await hasUnmergedPin(ctx.root, ctx.role)),
|
|
255
256
|
});
|
|
256
257
|
if (plan === null)
|
|
257
258
|
return { result: "skipped" };
|
|
258
259
|
if (ctx.role === DIRECTOR_ROLE) {
|
|
259
|
-
// The image-reroute one-shot marker routes exactly ONE
|
|
260
|
-
// target (plans/image-input.md part 4/5, INV-J2): for a role
|
|
261
|
-
// it after reading its handoff-note facts. The director never resumes, so
|
|
260
|
+
// The image-reroute one-shot marker routes exactly ONE run onto the image-capable
|
|
261
|
+
// target (plans/image-input.md part 4/5, INV-J2): for a role runner, planRunStart consumes
|
|
262
|
+
// it after reading its handoff-note facts. The director never resumes, so planRunStart's
|
|
262
263
|
// image-reroute branch never runs for it and the marker would otherwise linger and
|
|
263
|
-
// re-route every later director
|
|
264
|
-
//
|
|
265
|
-
// reroute pending for the next eligible
|
|
264
|
+
// re-route every later director run off its own tier. It is cleared here — after this
|
|
265
|
+
// run is confirmed to run (an empty-inbox skip above returns before this, leaving the
|
|
266
|
+
// reroute pending for the next eligible run) — so the rerouted director prompt is
|
|
266
267
|
// delivered exactly once and the director is back on its own tier next.
|
|
267
|
-
|
|
268
|
+
// A promptless recovery run delivers no prompt, so it leaves a pending reroute for the next.
|
|
269
|
+
if (plan.prompt !== null)
|
|
270
|
+
s.imageReroute = undefined;
|
|
268
271
|
// The director keeps its dedicated worktree (it runs outside the permit, consumes its
|
|
269
272
|
// config-request file at that exact path, and never resumes). Hold it from before
|
|
270
|
-
// ensureWorktree — creating or resetting it is part of the use — to the
|
|
273
|
+
// ensureWorktree — creating or resetting it is part of the use — to the run's end
|
|
271
274
|
// (plans/disk-floor.md, part 2/4), so a concurrent pressure reclaim never cleans a tree
|
|
272
|
-
// this
|
|
273
|
-
return useWorktree(ctx.root, worktreePath(ctx.root, ctx.role), () =>
|
|
275
|
+
// this run is creating or working in.
|
|
276
|
+
return useWorktree(ctx.root, worktreePath(ctx.root, ctx.role), () => runWithWorktreePhase(ctx, plan, cfg, fallbackCtx));
|
|
274
277
|
}
|
|
275
|
-
return
|
|
278
|
+
return runOnceInLeasePhase(ctx, plan, cfg, fallbackCtx);
|
|
276
279
|
}
|
|
277
|
-
/** Whether a finished role
|
|
278
|
-
* the resume flag a verdict armed inside the
|
|
280
|
+
/** Whether a finished role run's leased slot must stay pinned for the next run's resume —
|
|
281
|
+
* the resume flag a verdict armed inside the run (`resumePending`), an abort or quiet kill,
|
|
279
282
|
* or a context cut-off. Exported so a test can evaluate the exact condition the lease release
|
|
280
|
-
* uses: a resume flag set only later, by
|
|
283
|
+
* uses: a resume flag set only later, by applyRunOutcome, is too late to keep the worktree. A
|
|
281
284
|
* user abort discards its work and is never pinned. */
|
|
282
285
|
export function slotPinnedForResume(state, outcome) {
|
|
283
286
|
return (state.resumePending === true ||
|
|
@@ -285,13 +288,15 @@ export function slotPinnedForResume(state, outcome) {
|
|
|
285
288
|
outcome?.result === "quiet_killed" ||
|
|
286
289
|
outcome?.cutOff === true);
|
|
287
290
|
}
|
|
288
|
-
/** A non-director
|
|
291
|
+
/** A non-director run's whole life on a pooled slot (plans/worktree-pool.md, "Role runs
|
|
289
292
|
* lease slots"): lease, author on the role's branch, release. The release pins the slot for
|
|
290
|
-
* a
|
|
293
|
+
* a run whose next run resumes (aborted, quiet-killed, cut off, or a failed dirty run),
|
|
291
294
|
* keeping the branch checked out and the uncommitted edits in the same cwd pi's `--continue`
|
|
292
295
|
* needs; every other release detaches first so the branch is free for whichever slot the role
|
|
293
|
-
* leases next.
|
|
294
|
-
|
|
296
|
+
* leases next. A run an exception ends releases its slot unsettled, so edits it leaves there
|
|
297
|
+
* are saved for the role's next run, and a run on a fresh checkout first takes back any such
|
|
298
|
+
* work an earlier run of the role left (restoreSalvagedPhase). */
|
|
299
|
+
async function runOnceInLeasePhase(ctx, plan, cfg, fallbackCtx) {
|
|
295
300
|
const lease = await leaseSlot(ctx.root, {
|
|
296
301
|
role: ctx.role,
|
|
297
302
|
purpose: "run",
|
|
@@ -299,30 +304,50 @@ async function runTickInLeasePhase(ctx, plan, cfg, fallbackCtx) {
|
|
|
299
304
|
keep: plan.resuming || plan.handoff,
|
|
300
305
|
signal: ctx.runSignal(),
|
|
301
306
|
// The live view's pool budget: this read must not depend on tumwater.json being
|
|
302
|
-
// readable right now (a broken file must not error the
|
|
307
|
+
// readable right now (a broken file must not error the run before its pi run).
|
|
303
308
|
slotBudget: slotCount(ctx.config),
|
|
304
309
|
});
|
|
305
310
|
let outcome;
|
|
306
311
|
try {
|
|
307
|
-
|
|
312
|
+
let run = plan;
|
|
313
|
+
if (!lease.preserved) {
|
|
308
314
|
await checkoutRoleBranchPhase(ctx, lease.dir);
|
|
309
|
-
|
|
315
|
+
run = await restoreSalvagedPhase(ctx, plan, lease.dir);
|
|
316
|
+
}
|
|
317
|
+
outcome = await runWithWorktreePhase(ctx, run, cfg, fallbackCtx, lease.dir);
|
|
310
318
|
return outcome;
|
|
311
319
|
}
|
|
312
320
|
finally {
|
|
313
|
-
// The slot is pinned for the next
|
|
314
|
-
// inside the
|
|
315
|
-
// when the outcome is an abort, quiet kill, or context cut-off.
|
|
316
|
-
// `resumePending` for the other outcomes only after
|
|
321
|
+
// The slot is pinned for the next run's resume when the verdict armed `resumePending`
|
|
322
|
+
// inside the run (the error-dirty and rate-limit arms in src/run/run-verdict.ts), or
|
|
323
|
+
// when the outcome is an abort, quiet kill, or context cut-off. applyRunOutcome sets
|
|
324
|
+
// `resumePending` for the other outcomes only after runOnce returns, so those are read off
|
|
317
325
|
// the outcome here instead.
|
|
318
326
|
const pin = slotPinnedForResume(ctx.state, outcome);
|
|
319
|
-
// Detach is best-effort so it can never mask the
|
|
327
|
+
// Detach is best-effort so it can never mask the run's own outcome or error; the release
|
|
320
328
|
// still runs, and the next lease's checkout recovers an undetached branch either way.
|
|
321
329
|
if (!pin)
|
|
322
330
|
await gitTry(lease.dir, "checkout", "--detach");
|
|
323
|
-
|
|
331
|
+
// An exception escaped the run (no outcome), so nothing settled the worktree — not even a
|
|
332
|
+
// dirty check, when the exception was git itself failing (2026-10-21's spawn wedge): any
|
|
333
|
+
// edits left in the slot are this role's unfinished work, saved for its next run.
|
|
334
|
+
lease.release({ pin, unsettled: outcome === undefined });
|
|
324
335
|
}
|
|
325
336
|
}
|
|
337
|
+
/** Give a run on a fresh checkout back the work an earlier run of its role left unsettled
|
|
338
|
+
* (src/git/slot-salvage.ts): restore the newest waiting patch into `dir` and run over it as
|
|
339
|
+
* planWithSalvagedWork says, told which files carry conflict markers and which hunks it must
|
|
340
|
+
* place by hand when the branch moved under the patch. A run that does not take such work, or
|
|
341
|
+
* has none waiting or cannot restore it now, keeps its plan. */
|
|
342
|
+
async function restoreSalvagedPhase(ctx, plan, dir) {
|
|
343
|
+
if (!takesSalvagedWork(plan))
|
|
344
|
+
return plan;
|
|
345
|
+
const restored = await restoreSalvagedWork(ctx.root, ctx.role, dir);
|
|
346
|
+
if (restored === null)
|
|
347
|
+
return plan;
|
|
348
|
+
const note = buildSalvageRestoreNote(conflictedFileHunks(dir, restored.conflicted), restored.unplaced, restored.patch);
|
|
349
|
+
return planWithSalvagedWork(ctx.role, plan, path.relative(ctx.root, restored.patch), note);
|
|
350
|
+
}
|
|
326
351
|
/** Put `tumwater/<role>` on the leased slot before authoring. An existing branch is checked
|
|
327
352
|
* out, never reset — `-B` would discard the unmerged commit it durably holds; an absent one
|
|
328
353
|
* is created from main. `--ignore-other-worktrees` lets the slot take the branch while a
|
|
@@ -339,26 +364,32 @@ async function checkoutRoleBranchPhase(ctx, dir) {
|
|
|
339
364
|
}
|
|
340
365
|
await git(dir, "clean", "-fd");
|
|
341
366
|
}
|
|
342
|
-
async function
|
|
343
|
-
/** The leased slot the
|
|
367
|
+
async function runWithWorktreePhase(ctx, plan, cfg, fallbackCtx,
|
|
368
|
+
/** The leased slot the run runs in; omitted by the director, which ensures its own. */
|
|
344
369
|
leasedDir) {
|
|
345
370
|
const s = ctx.state;
|
|
346
371
|
const { priorMergeFailure, resuming, handoff, resumeCause, userPrompt } = plan;
|
|
347
|
-
|
|
372
|
+
// Empty only for a promptless recovery run, which ends before any use of it (see below).
|
|
373
|
+
let prompt = plan.prompt ?? "";
|
|
348
374
|
const wt = leasedDir ?? (await ensureWorktree(ctx.root, ctx.role, ctx.mainBranch));
|
|
349
375
|
if (resuming || handoff) {
|
|
350
376
|
// Keep the interrupted run's uncommitted edits; clear only stray merge/rebase state. A
|
|
351
377
|
// handoff resumes a FRESH session over those same edits (the newest author session belongs
|
|
352
378
|
// to another agent), so it preserves the worktree exactly as a resume does.
|
|
353
379
|
await abortSync(wt);
|
|
354
|
-
logEventBestEffort(ctx.root, {
|
|
380
|
+
logEventBestEffort(ctx.root, {
|
|
381
|
+
source: ctx.role,
|
|
382
|
+
type: "resume",
|
|
383
|
+
cause: resumeCause,
|
|
384
|
+
...(plan.restored !== undefined ? { restored: plan.restored } : {}),
|
|
385
|
+
});
|
|
355
386
|
}
|
|
356
387
|
else {
|
|
357
|
-
// Salvage a commit a previous
|
|
388
|
+
// Salvage a commit a previous run left unmerged (src/runner/leftover.ts): recovery puts it
|
|
358
389
|
// back on the merge queue, so it merges through the orchestrator's merge pipeline — the
|
|
359
|
-
// same gate as a fresh
|
|
390
|
+
// same gate as a fresh run's change, and main keeps exactly one writer (an in-run
|
|
360
391
|
// recovery merge raced the slot's batches into `merge_blocked`). A salvaged leftover
|
|
361
|
-
// ENDS the
|
|
392
|
+
// ENDS the run: the role holds one merge ref, and the leftover owns it until its
|
|
362
393
|
// merge resolves. With nothing to salvage the branch holds nothing either, so the reset
|
|
363
394
|
// below leaves pristine main for the red-main gate.
|
|
364
395
|
// A permanent reviewer config error — held from re-queueing — clears once the resolved
|
|
@@ -371,12 +402,20 @@ leasedDir) {
|
|
|
371
402
|
root: ctx.root,
|
|
372
403
|
role: ctx.role,
|
|
373
404
|
mainBranch: ctx.mainBranch,
|
|
374
|
-
|
|
405
|
+
runNumber: s.runs,
|
|
375
406
|
wt,
|
|
376
407
|
mergeConflicts: s.mergeConflicts,
|
|
377
408
|
conflictHandback: s.conflictHandback,
|
|
378
409
|
mergeReviewError: s.mergeReviewError,
|
|
410
|
+
mainRedRetry: s.mainRedRetry,
|
|
379
411
|
});
|
|
412
|
+
if (recovered?.kind === "handback" && plan.prompt === null) {
|
|
413
|
+
// A promptless recovery run has no authoring run to hand the pin back to: leave the pin and
|
|
414
|
+
// the conflict state exactly as they are, so the one hand-back the lineage is owed happens on
|
|
415
|
+
// the director's next prompt-driven run.
|
|
416
|
+
await resetWorktreeToMain(wt, ctx.mainBranch);
|
|
417
|
+
return { result: "skipped" };
|
|
418
|
+
}
|
|
380
419
|
if (recovered?.kind === "handback") {
|
|
381
420
|
// The pin hit the conflict cap and its lineage has not been handed back yet: keep the
|
|
382
421
|
// pin, record the hand-back, and fall through to author the resolution on current main.
|
|
@@ -392,22 +431,26 @@ leasedDir) {
|
|
|
392
431
|
}
|
|
393
432
|
else if (recovered?.kind === "discarded") {
|
|
394
433
|
// The pin hit the conflict cap and is gone: nothing holds the role any more, so this
|
|
395
|
-
//
|
|
434
|
+
// run authors on a fresh main like any other — told what was dropped and why.
|
|
396
435
|
s.mergeConflicts = undefined;
|
|
397
436
|
s.conflictHandback = undefined;
|
|
398
437
|
s.conflictDiscard = { sha: recovered.sha, summary: recovered.summary, attempts: recovered.attempts, at: Date.now() };
|
|
399
438
|
prompt = appendPromptNote(prompt, buildConflictDiscardNote(recovered.summary, recovered.attempts));
|
|
400
439
|
}
|
|
401
440
|
else if (recovered)
|
|
402
|
-
return ctx.
|
|
441
|
+
return ctx.finishRecoveryRun(recovered, userPrompt, wt, priorMergeFailure);
|
|
403
442
|
await resetWorktreeToMain(wt, ctx.mainBranch);
|
|
443
|
+
// A promptless recovery run found nothing to salvage (or deleted a stale pin): it authors
|
|
444
|
+
// nothing, so it ends here rather than reach the authoring run with a null prompt.
|
|
445
|
+
if (plan.prompt === null)
|
|
446
|
+
return { result: "skipped" };
|
|
404
447
|
// Red-main baseline gate (src/baseline/main-red.ts): the worktree is pristine main right
|
|
405
448
|
// now — verify main's own suite before spending an authoring run on top of it. Only roles
|
|
406
|
-
// whose diff can carry code changes are blocked; resume
|
|
449
|
+
// whose diff can carry code changes are blocked; resume runs skip this by construction
|
|
407
450
|
// (their worktree is not pristine main). The `bugfix` healer is exempt because its fix is
|
|
408
451
|
// the team's only way back to green, so instead of blocking it we hand it the failure
|
|
409
452
|
//the same check runs, but a red main appends a <main-red>
|
|
410
|
-
// note to this
|
|
453
|
+
// note to this run's prompt so it reproduces and fixes that failure rather than hunting
|
|
411
454
|
// blind.
|
|
412
455
|
if (ctx.baseRole === "bugfix") {
|
|
413
456
|
const note = await bugfixMainRedNote(ctx.root, ctx.role, wt);
|
|
@@ -415,21 +458,24 @@ leasedDir) {
|
|
|
415
458
|
prompt = appendPromptNote(prompt, note);
|
|
416
459
|
}
|
|
417
460
|
else {
|
|
418
|
-
const
|
|
461
|
+
const knownRed = [];
|
|
462
|
+
const blocked = await mainRedGate(ctx.root, ctx.role, wt, undefined, (tests) => knownRed.push(tests));
|
|
419
463
|
if (blocked) {
|
|
420
464
|
// The dequeued prompt never ran: put it back in its queue before returning, so it is
|
|
421
465
|
// not lost to a restart (the pending field is memory-only) or dropped by the next
|
|
422
|
-
//
|
|
423
|
-
// the next
|
|
466
|
+
// run's outcome handling — the queue is the durable store, and once main is green
|
|
467
|
+
// the next run dequeues it again (criterion b).
|
|
424
468
|
ctx.pending.requeueUnfulfilled(userPrompt);
|
|
425
469
|
ctx.pending.clear();
|
|
426
470
|
return blocked;
|
|
427
471
|
}
|
|
472
|
+
if (knownRed[0])
|
|
473
|
+
prompt = appendPromptNote(prompt, buildMainKnownRedNote(knownRed[0]));
|
|
428
474
|
}
|
|
429
475
|
}
|
|
430
476
|
// A change handed back after its merges kept conflicting — or a persisted hand-back the
|
|
431
|
-
// author's
|
|
432
|
-
// markers left as ordinary uncommitted edits so this
|
|
477
|
+
// author's run has not applied yet — is re-applied over current main with the conflict
|
|
478
|
+
// markers left as ordinary uncommitted edits so this run resolves them (PLANS.md "Robust
|
|
433
479
|
// conflict landing, part 2/2"). The merge ref is deleted once the diff is in the worktree.
|
|
434
480
|
// Runs before the revision branch, which applies a revision hand-back itself. A resume or
|
|
435
481
|
// handoff keeps the interrupted worktree as-is, so re-applying here would cherry-pick onto
|
|
@@ -457,7 +503,7 @@ leasedDir) {
|
|
|
457
503
|
}
|
|
458
504
|
else {
|
|
459
505
|
// The object is gone or the apply failed for a non-conflict reason: drop the hand-back
|
|
460
|
-
// so the author starts fresh rather than retrying it every
|
|
506
|
+
// so the author starts fresh rather than retrying it every run.
|
|
461
507
|
s.conflictHandback = undefined;
|
|
462
508
|
await deleteRef(ctx.root, mergeRefName(ctx.role));
|
|
463
509
|
logEventBestEffort(ctx.root, { source: ctx.role, type: "conflict_handback", action: "failed", sha: hb.sha, reason: hb.reason });
|
|
@@ -465,40 +511,40 @@ leasedDir) {
|
|
|
465
511
|
}
|
|
466
512
|
const piStartedAt = Date.now();
|
|
467
513
|
// A rejected change owes a revision (plans/revise-rejected.md): re-apply its diff to current
|
|
468
|
-
// main as uncommitted edits so this
|
|
469
|
-
// append the revision note the prompt skipped. `revisionRound` records that THIS
|
|
514
|
+
// main as uncommitted edits so this run revises it instead of authoring from scratch, and
|
|
515
|
+
// append the revision note the prompt skipped. `revisionRound` records that THIS run holds
|
|
470
516
|
// the rejected diff — set only when the revision is applied here, or a resume carries the
|
|
471
|
-
// prior attempt's already-applied edits — so the post-run handlers read the
|
|
472
|
-
// `state.revision`, which a per-role user-request
|
|
517
|
+
// prior attempt's already-applied edits — so the post-run handlers read the run rather than
|
|
518
|
+
// `state.revision`, which a per-role user-request run leaves set by design. A conflict (or a
|
|
473
519
|
// gone object) clears the revision, deletes the rejected ref, and falls back to the plain
|
|
474
520
|
// rejection note.
|
|
475
521
|
let revisionRound;
|
|
476
522
|
if (s.revision && ctx.role !== DIRECTOR_ROLE && userPrompt === null) {
|
|
477
523
|
const { sha, round } = s.revision;
|
|
478
|
-
// The prompt's rejected/revision notes read the
|
|
479
|
-
// one (a fresh
|
|
524
|
+
// The prompt's rejected/revision notes read the runner's most recent rejection; an absent
|
|
525
|
+
// one (a fresh runner or a hand-edited state) reads the empty reasons list. Computed once
|
|
480
526
|
// for the three revision-append sites and the rejected-note fallback below, so the
|
|
481
527
|
// get-or-default idiom cannot drift between them.
|
|
482
528
|
const lastReviewOrEmpty = s.lastReview ?? { reasons: [] };
|
|
483
529
|
const revisionNote = buildRevisionNote(lastReviewOrEmpty, round);
|
|
484
530
|
if (resuming || handoff) {
|
|
485
531
|
// The interrupted revision run's applied edits are still in the worktree (both the
|
|
486
|
-
// resume and the cross-agent handoff keep uncommitted work), so this
|
|
532
|
+
// resume and the cross-agent handoff keep uncommitted work), so this run still owns
|
|
487
533
|
// the revision. A handoff runs the revision in a fresh session rather than
|
|
488
534
|
// re-cherry-picking it onto the already-dirty worktree.
|
|
489
535
|
revisionRound = round;
|
|
490
536
|
if (handoff) {
|
|
491
537
|
// A handoff is a FRESH session: it has none of the interrupted run's context, and
|
|
492
|
-
//
|
|
538
|
+
// run-prompt.ts suppresses the rejected-review note whenever state.revision is set,
|
|
493
539
|
// so without this the fresh run would see neither the objections nor the revision
|
|
494
540
|
// instructions. A resume keeps its session context and must not see the note twice.
|
|
495
|
-
prompt =
|
|
541
|
+
prompt = prependPromptNote(prompt, revisionNote);
|
|
496
542
|
}
|
|
497
543
|
}
|
|
498
544
|
else if (await applyRevision(wt, ctx.mainBranch, sha)) {
|
|
499
545
|
revisionRound = round;
|
|
500
546
|
logEventBestEffort(ctx.root, { source: ctx.role, type: "revision", action: "applied", round, sha });
|
|
501
|
-
prompt =
|
|
547
|
+
prompt = prependPromptNote(prompt, revisionNote);
|
|
502
548
|
}
|
|
503
549
|
else {
|
|
504
550
|
// The clean re-apply conflicted: re-apply with the markers left in place so the author
|
|
@@ -523,19 +569,19 @@ leasedDir) {
|
|
|
523
569
|
reason: "revision",
|
|
524
570
|
conflicted: withConflicts.conflicted,
|
|
525
571
|
});
|
|
526
|
-
prompt =
|
|
572
|
+
prompt = prependPromptNote(prompt, revisionNote);
|
|
527
573
|
prompt = await appendHandbackNote(prompt, wt, ctx.mainBranch, sha, "revision", withConflicts.conflicted);
|
|
528
574
|
}
|
|
529
575
|
else {
|
|
530
576
|
s.revision = undefined;
|
|
531
577
|
await deleteRef(ctx.root, rejectedRefName(ctx.role));
|
|
532
578
|
logEventBestEffort(ctx.root, { source: ctx.role, type: "revision", action: "conflict", round, sha });
|
|
533
|
-
prompt =
|
|
579
|
+
prompt = prependPromptNote(prompt, buildRejectedReviewNote(lastReviewOrEmpty, ctx.role));
|
|
534
580
|
prompt = appendPromptNote(prompt, "The rejected diff no longer applies to current main.");
|
|
535
581
|
}
|
|
536
582
|
}
|
|
537
583
|
}
|
|
538
|
-
// A handoff runs a FRESH session over the interrupted
|
|
584
|
+
// A handoff runs a FRESH session over the interrupted run's kept worktree. When that run had
|
|
539
585
|
// already applied a conflict hand-back, the marker-bearing files are still there but the fresh
|
|
540
586
|
// run has none of the interrupted session's context explaining them, so re-send the hand-back
|
|
541
587
|
// note (a resume keeps its session context and must not see the note twice). The revision
|
|
@@ -547,101 +593,33 @@ leasedDir) {
|
|
|
547
593
|
const conflicted = await markerBearingFiles(wt);
|
|
548
594
|
prompt = await appendHandbackNote(prompt, wt, ctx.mainBranch, hb.sha, hb.reason, conflicted);
|
|
549
595
|
}
|
|
550
|
-
const
|
|
596
|
+
const runBase = await headOf(wt, "HEAD");
|
|
597
|
+
// The stray-write warning only reports, so a `git status` that fails on either side skips it.
|
|
598
|
+
const primaryBefore = await changedFiles(ctx.root).catch(() => null);
|
|
551
599
|
const pi = await ctx.pi.runAuthoringPi(wt, prompt, `tumwater-${ctx.role}-${s.runs}`, resuming, cfg);
|
|
552
|
-
// The harness owns every commit (INV-H1): fold any the agent made into the
|
|
553
|
-
await foldAgentCommitsSince(ctx, wt,
|
|
600
|
+
// The harness owns every commit (INV-H1): fold any the agent made into the run's one commit.
|
|
601
|
+
await foldAgentCommitsSince(ctx, wt, runBase);
|
|
602
|
+
// A write that landed in the primary checkout never reaches the worktree's commit, so the run
|
|
603
|
+
// can read as no_change with its work orphaned (BUGS.md, 2026-10-20). The before/after diff
|
|
604
|
+
// cannot attribute paths to this run when roles overlap, so only warn: the verdict, resume
|
|
605
|
+
// handling and model-fallback fold below stay as they were.
|
|
606
|
+
const primaryAfter = primaryBefore === null ? null : await changedFiles(ctx.root).catch(() => null);
|
|
607
|
+
const strayed = primaryAfter === null ? [] : primaryAfter.filter((f) => !primaryBefore.includes(f));
|
|
608
|
+
if (strayed.length > 0 && !pi.aborted) {
|
|
609
|
+
const shown = strayed.slice(0, 10).join(", ") + (strayed.length > 10 ? `, … (${strayed.length} paths)` : "");
|
|
610
|
+
ctx.warn(`wrote outside the worktree (possibly another writer's): ${shown}`);
|
|
611
|
+
}
|
|
554
612
|
// Fold the authoring run's verdict into the role's model-fallback episode NOW, before the
|
|
555
613
|
// post-run handlers spend any more pi runs: only this run is evidence about the primary.
|
|
556
614
|
ctx.foldModelFallback(fallbackCtx, pi);
|
|
557
615
|
// The `qa` observer's reply ends with a result-carrying `FLOW:` line (plans/observer-roles.md
|
|
558
616
|
// 2/2). Extracted once here, recorded only at the success returns below — an interrupted or
|
|
559
|
-
// failed
|
|
617
|
+
// failed run must not advance the rotation. The harness records it, never pi.
|
|
560
618
|
const flow = ctx.baseRole === "qa" ? extractFlow(pi.finalText) : null;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
* reviewed changes merge through the merge pipeline's leased verification slot instead; md-only
|
|
568
|
-
* notes are review-exempt by construction, so they keep this branch path (plans/merge-queue.md
|
|
569
|
-
* 2/5). */
|
|
570
|
-
async function mergePhase(ctx, wt, summary) {
|
|
571
|
-
return mergeToMain({
|
|
572
|
-
root: ctx.root,
|
|
573
|
-
role: ctx.role,
|
|
574
|
-
mainBranch: ctx.mainBranch,
|
|
575
|
-
exemptPaths: ctx.config.review.exemptPaths,
|
|
576
|
-
config: ctx.config,
|
|
577
|
-
tick: ctx.state.runs,
|
|
578
|
-
runPi: (w, prompt, sessionName, config) => ctx.pi.runRolePi(w, prompt, sessionName, false, config),
|
|
579
|
-
}, wt, summary);
|
|
580
|
-
}
|
|
581
|
-
/** Turn a finished pi run into its TickOutcome: the post-run half of runTick, split off so each
|
|
582
|
-
* half reads on its own screen — the setup above ends at the pi return. The verdict
|
|
583
|
-
* classification (abort, config request, quiet kill, timeout, refusal, failure, no-change) lives
|
|
584
|
-
* in resolveTickVerdict (src/tick/tick-verdict.ts), and the fulfillable path — staging —
|
|
585
|
-
* stays here. `userPrompt` is the raw director prompt this tick is executing (null for role
|
|
586
|
-
* loops) so unfulfilled outcomes can re-queue it; `revisionRound` is the revision this tick holds
|
|
587
|
-
* (undefined when it is not a revision); `flow` is the qa observer's FLOW line (null for every
|
|
588
|
-
* other role). */
|
|
589
|
-
export async function handlePiResultPhase(ctx, pi, userPrompt, revisionRound, wt, flow, piStartedAt, cfg) {
|
|
590
|
-
// Everything that decides the run left nothing mergeable — abort, config request,
|
|
591
|
-
// quiet kill, timeout, refusal, failure without changes, no change — lives in
|
|
592
|
-
// resolveTickVerdict (src/tick/tick-verdict.ts); null means the run IS fulfillable.
|
|
593
|
-
const verdict = await resolveTickVerdict({
|
|
594
|
-
root: ctx.root,
|
|
595
|
-
role: ctx.role,
|
|
596
|
-
mainBranch: ctx.mainBranch,
|
|
597
|
-
state: ctx.state,
|
|
598
|
-
pending: ctx.pending,
|
|
599
|
-
turns: ctx.usage.turns,
|
|
600
|
-
userPrompt,
|
|
601
|
-
revisionRound,
|
|
602
|
-
wt,
|
|
603
|
-
pi,
|
|
604
|
-
flow,
|
|
605
|
-
// The image-reroute arm resolves its target from the live config and the current
|
|
606
|
-
// regime, and names the model this tick ran on as the handoff's `from`.
|
|
607
|
-
config: ctx.config,
|
|
608
|
-
rerouteRegime: ctx.rerouteRegime(piStartedAt),
|
|
609
|
-
runModel: cfg.model,
|
|
610
|
-
warn: (message) => ctx.warn(message),
|
|
611
|
-
merge: (w, sum) => mergePhase(ctx, w, sum),
|
|
612
|
-
finishAbortedTick: () => ctx.finishAbortedTick(userPrompt, wt),
|
|
613
|
-
});
|
|
614
|
-
if (verdict)
|
|
615
|
-
return verdict;
|
|
616
|
-
// The commit is pinned and the worktree is free (src/tick/tick-stage.ts): stage it for the
|
|
617
|
-
// merge queue and END the tick — the orchestrator drains the queue on its single merge
|
|
618
|
-
// slot, outside the author semaphore, so this slot is free the moment the work is
|
|
619
|
-
// committed (plans/merge-queue.md 3/5). Staging derives the commit message from the reply's
|
|
620
|
-
// SUMMARY block (one bounded follow-up turn when the run left none), flags high-friction
|
|
621
|
-
// ticks, pins the commit by the role's merge ref BEFORE freeing the worktree (invariant
|
|
622
|
-
// 4 — a failed pin defers to next-tick recovery), and enqueues the merge, which runs the
|
|
623
|
-
// same gate + merge flow through runMergePi/foldMergeUsage on this same state object
|
|
624
|
-
// — recording the commit count, the outcome, and the reviewer spend into it — and logs
|
|
625
|
-
// merge_done/merge_failed; a non-terminal outcome keeps the pin for next-tick leftover recovery.
|
|
626
|
-
// `commits` was NOT incremented above: it counts merged changes only.
|
|
627
|
-
return stageTickMerge({
|
|
628
|
-
root: ctx.root,
|
|
629
|
-
role: ctx.role,
|
|
630
|
-
state: ctx.state,
|
|
631
|
-
config: ctx.config,
|
|
632
|
-
tickTurns: ctx.usage.turns,
|
|
633
|
-
userPrompt,
|
|
634
|
-
revisionRound,
|
|
635
|
-
mainBranch: ctx.mainBranch,
|
|
636
|
-
wt,
|
|
637
|
-
finalText: pi.finalText,
|
|
638
|
-
flow,
|
|
639
|
-
piStartedAt,
|
|
640
|
-
warn: (message) => ctx.warn(message),
|
|
641
|
-
requestSummary: (w, n) => ctx.pi.requestSummary(w, cfg, n),
|
|
642
|
-
stageCheck: (w) => stageCheckFindings(w, ctx.mainBranch, ctx.config.review.exemptPaths, ctx.role),
|
|
643
|
-
requestStageFix: (w, findings) => ctx.pi.requestStageFix(w, findings, cfg),
|
|
644
|
-
pinAndReset: (w, sha) => ctx.pinAndReset(w, sha),
|
|
645
|
-
finishAbortedTick: () => ctx.finishAbortedTick(userPrompt, wt),
|
|
646
|
-
});
|
|
619
|
+
// The `feedback` observer's reply ends with a result-carrying `FILED: #<n> — <title>` line
|
|
620
|
+
// (plans/feedback-role.md 2/2). Extracted once here, recorded only at the success returns
|
|
621
|
+
// below — an interrupted or failed run must not record a filing it never declared. The
|
|
622
|
+
// harness records it, never pi.
|
|
623
|
+
const filed = ctx.baseRole === "feedback" ? extractFiled(pi.finalText) : null;
|
|
624
|
+
return handlePiResultPhase(ctx, pi, userPrompt, revisionRound, wt, flow, filed, piStartedAt, cfg);
|
|
647
625
|
}
|