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,4 +1,3 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { loadConfigSafe } from "../config/config.js";
|
|
@@ -6,11 +5,16 @@ import { seamsInUse } from "../config/config-fallbacks.js";
|
|
|
6
5
|
import { findOnPath } from "../files/files.js";
|
|
7
6
|
import { GIT_MISSING_MESSAGE } from "../git/git-run.js";
|
|
8
7
|
import { branchesPhrase, branchExists, currentBranch, hasCommits, isGitRepo } from "../git/git.js";
|
|
9
|
-
import { DETACHED_HEAD_MESSAGE, NOT_A_REPO_MESSAGE, NOT_INITIALIZED_MESSAGE, NO_COMMITS_MESSAGE, } from "./readiness.js";
|
|
10
|
-
import {
|
|
8
|
+
import { DETACHED_HEAD_MESSAGE, NOT_A_REPO_MESSAGE, NOT_INITIALIZED_MESSAGE, NO_COMMITS_MESSAGE, teamWorkspaceMessage, } from "./readiness.js";
|
|
9
|
+
import { owningTeamRoot } from "../paths.js";
|
|
10
|
+
import { backendMissingMessage, findBackendBinary, resolveBackendBin } from "../pi/pi-bin.js";
|
|
11
11
|
import { seedClaudeAccount } from "../agent/claude/claude-account.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { seedCodexAccount } from "../agent/codex/codex-account.js";
|
|
13
|
+
import { opencodeCatalogStale, readOpencodeCache, refreshOpencodeCatalog, } from "../agent/opencode/opencode-catalog.js";
|
|
14
|
+
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
15
|
+
import { HOUR_MS } from "../text/datetime.js";
|
|
16
|
+
import { errorMessage } from "../text/text.js";
|
|
17
|
+
import { backendById } from "../agent/agent-backend.js";
|
|
14
18
|
// Register the built-in pi and claude adapters into the registry this module reads (see
|
|
15
19
|
// unregisteredBackendInUse) — importing run-agent.ts is the one place they register.
|
|
16
20
|
import "../agent/run-agent.js";
|
|
@@ -26,13 +30,15 @@ export async function repoNotReady(root) {
|
|
|
26
30
|
return GIT_MISSING_MESSAGE;
|
|
27
31
|
if (!(await isGitRepo(root)))
|
|
28
32
|
return NOT_A_REPO_MESSAGE;
|
|
29
|
-
if (!fs.existsSync(path.join(root, "tumwater.json")))
|
|
30
|
-
|
|
33
|
+
if (!fs.existsSync(path.join(root, "tumwater.json"))) {
|
|
34
|
+
const owner = owningTeamRoot(root);
|
|
35
|
+
return owner === undefined ? NOT_INITIALIZED_MESSAGE : teamWorkspaceMessage(owner);
|
|
36
|
+
}
|
|
31
37
|
if (!(await hasCommits(root)))
|
|
32
38
|
return NO_COMMITS_MESSAGE;
|
|
33
39
|
return null;
|
|
34
40
|
}
|
|
35
|
-
/** Every precondition a `tumwater
|
|
41
|
+
/** Every precondition a `tumwater start` generation checks before its orchestrator starts, in the
|
|
36
42
|
* order it checks them: the repo gate, a tumwater.json that loads and validates, a resolvable
|
|
37
43
|
* agent binary, and a main branch it can target. Either the values the generation starts with,
|
|
38
44
|
* or the first unmet precondition's message — never a throw, so a poll can ask it safely. */
|
|
@@ -57,18 +63,18 @@ opencodeSeed = {}) {
|
|
|
57
63
|
if (unregistered !== undefined) {
|
|
58
64
|
return { problem: `backend ${unregistered} is configured but this build has no adapter for it` };
|
|
59
65
|
}
|
|
60
|
-
// Fail fast instead of starting
|
|
61
|
-
// enabled seam's agent — the top-level `agent` plus each enabled
|
|
62
|
-
// reviewer — resolves through the same
|
|
63
|
-
// a missing one names its own agent's binary, not pi's.
|
|
66
|
+
// Fail fast instead of starting roles whose every run dies with "spawn <bin> ENOENT". Every
|
|
67
|
+
// enabled seam's agent — the top-level `agent` plus each enabled role's resolved view and the
|
|
68
|
+
// reviewer — resolves through the same resolveBackendBin the spawn uses (and doctor's pi check);
|
|
69
|
+
// a missing one names its own agent's binary, not pi's. resolveBackendBin normalizes path-shaped
|
|
64
70
|
// values against THIS process's cwd, which every generation inherits, so what is checked here
|
|
65
|
-
// is exactly what the
|
|
66
|
-
for (const agent of
|
|
67
|
-
const resolved =
|
|
68
|
-
// The shared executability rule (pi/pi-bin.ts's
|
|
71
|
+
// is exactly what the runs spawn.
|
|
72
|
+
for (const agent of backendsInUse(config)) {
|
|
73
|
+
const resolved = resolveBackendBin(config, agent);
|
|
74
|
+
// The shared executability rule (pi/pi-bin.ts's findBackendBinary — the one doctor's pi check
|
|
69
75
|
// asks too), so a seam's missing binary reads the same way on both surfaces.
|
|
70
|
-
if (!
|
|
71
|
-
return { problem:
|
|
76
|
+
if (!findBackendBinary(resolved))
|
|
77
|
+
return { problem: backendMissingMessage(resolved, agent) };
|
|
72
78
|
}
|
|
73
79
|
// Seed the claude account state before the first run (plans/agent-backends.md part 6a):
|
|
74
80
|
// `claude auth status` decides subscription vs API key, and an existing file is left alone.
|
|
@@ -76,24 +82,36 @@ opencodeSeed = {}) {
|
|
|
76
82
|
// process: runStartupCheck is also the redeploy/supervisor poll's ask (runStartupProblem),
|
|
77
83
|
// and a logged-out claude would otherwise spawn a bounded probe on every poll. If the seed
|
|
78
84
|
// fails, the first claude run's observation fills the file in.
|
|
79
|
-
if (!claudeSeedAttempted.has(root) &&
|
|
85
|
+
if (!claudeSeedAttempted.has(root) && backendsInUse(config).includes("claude")) {
|
|
80
86
|
claudeSeedAttempted.add(root);
|
|
81
87
|
seedClaudeAccount(root, config);
|
|
82
88
|
}
|
|
89
|
+
// Seed the codex account state before the first codex run (plans/codex-backend.md §7):
|
|
90
|
+
// `codex login status` decides subscription vs API key, and an existing file is left alone.
|
|
91
|
+
// Gated on a codex seam like the claude seed above, and attempted once per root per process.
|
|
92
|
+
// API-key billing is refused (decision 5): Codex reports no cost, so per-token billing would
|
|
93
|
+
// spend money the cap cannot see. If the seed fails or reads something else, the first codex
|
|
94
|
+
// run's observation fills the file in.
|
|
95
|
+
if (!codexSeedAttempted.has(root) && backendsInUse(config).includes("codex")) {
|
|
96
|
+
codexSeedAttempted.add(root);
|
|
97
|
+
const codexAccount = seedCodexAccount(root, config);
|
|
98
|
+
if (codexAccount?.billing === "api") {
|
|
99
|
+
return {
|
|
100
|
+
problem: "Codex is logged in with an API key; tumwater runs Codex on a ChatGPT subscription only — run `CODEX_HOME=<home> codex login --device-auth`",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
83
104
|
// Seed the opencode models catalog (part 6b/7) before the first run: the poll surfaces price
|
|
84
105
|
// opencode seams from the cached `.tumwater/state/opencode-models.json`, and an absent cache
|
|
85
106
|
// reads every opencode seam NOT free (the safe direction for the budget gate). Mirroring the
|
|
86
107
|
// claude seed above, it runs once per root per process when a model job routes to opencode;
|
|
87
|
-
// a failed fetch keeps the last good cache (or an empty one) and never blocks startup
|
|
88
|
-
|
|
108
|
+
// a failed fetch keeps the last good cache (or an empty one) and never blocks startup; a
|
|
109
|
+
// failed fetch or cache write logs one warning event.
|
|
110
|
+
if (!opencodeSeedAttempted.has(root) && backendsInUse(config).includes("opencode")) {
|
|
89
111
|
opencodeSeedAttempted.add(root);
|
|
90
|
-
await
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// the freshness check compares against the binary the team actually runs.
|
|
94
|
-
cliBin: resolveAgentBin(config, "opencode").bin,
|
|
95
|
-
fetch: opencodeSeed.fetch,
|
|
96
|
-
now: opencodeSeed.now,
|
|
112
|
+
await seedOpencodeCatalog(root, config, {
|
|
113
|
+
...opencodeSeed,
|
|
114
|
+
warn: (message) => warnEventBestEffort(root, TUMWATER_SOURCE, message),
|
|
97
115
|
});
|
|
98
116
|
}
|
|
99
117
|
const branch = await resolveMainBranch(root, config, branchArg);
|
|
@@ -101,10 +119,54 @@ opencodeSeed = {}) {
|
|
|
101
119
|
return branch;
|
|
102
120
|
return { config, mainBranch: branch.mainBranch };
|
|
103
121
|
}
|
|
122
|
+
/** Refresh the opencode models catalog for the config's opencode seams: the one seed both the
|
|
123
|
+
* startup check and a live edit that first routes a seam to opencode run. `warn` receives the
|
|
124
|
+
* refresh's own warnings; every caller passes one (startup logs through warnEventBestEffort). */
|
|
125
|
+
export function seedOpencodeCatalog(root, config, opts = {}) {
|
|
126
|
+
return refreshOpencodeCatalog(root, {
|
|
127
|
+
providers: opencodeProviders(config),
|
|
128
|
+
// Probe the configured opencode binary (not a different one on PATH) for the version, so
|
|
129
|
+
// the freshness check compares against the binary the team actually runs.
|
|
130
|
+
cliBin: resolveBackendBin(config, "opencode").bin,
|
|
131
|
+
fetch: opts.fetch,
|
|
132
|
+
now: opts.now,
|
|
133
|
+
warn: opts.warn,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/** How long a running team waits between attempts to refetch a missing or stale opencode
|
|
137
|
+
* catalog. */
|
|
138
|
+
export const OPENCODE_CATALOG_RETRY_MS = HOUR_MS;
|
|
139
|
+
/** The poll-driven catalog refresh: a running team refetches a missing or stale catalog, so a
|
|
140
|
+
* failed startup fetch or a days-long run does not price by nothing or by an old copy. The poll
|
|
141
|
+
* path checks only the cache's age. It skips the `opencode --version` comparison, so no binary is
|
|
142
|
+
* spawned on every poll; the version check still runs inside refreshOpencodeCatalog once a
|
|
143
|
+
* refresh is due. The throttle starts at construction (the startup check has just tried) and
|
|
144
|
+
* moves on every attempt, so a fetch that keeps failing warns at most once per
|
|
145
|
+
* OPENCODE_CATALOG_RETRY_MS, through refreshOpencodeCatalog's own fetch-failed warning. */
|
|
146
|
+
export function newOpencodeCatalogRefresh(root, seed, warn) {
|
|
147
|
+
const clock = seed.now ?? Date.now;
|
|
148
|
+
let lastAttempt = clock();
|
|
149
|
+
return {
|
|
150
|
+
poll(config) {
|
|
151
|
+
if (!backendsInUse(config).includes("opencode"))
|
|
152
|
+
return;
|
|
153
|
+
if (clock() - lastAttempt < OPENCODE_CATALOG_RETRY_MS)
|
|
154
|
+
return;
|
|
155
|
+
if (!opencodeCatalogStale(readOpencodeCache(root), clock()))
|
|
156
|
+
return;
|
|
157
|
+
lastAttempt = clock();
|
|
158
|
+
void seedOpencodeCatalog(root, config, { ...seed, warn }).catch((err) => warn(`open code model catalog refresh failed: ${errorMessage(err)}`));
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
104
162
|
/** Roots this process has already attempted a claude-account seed for (see runStartupCheck),
|
|
105
163
|
* so the supervisor's and redeploy's repeated startup checks do not respawn `claude auth
|
|
106
164
|
* status` on every poll when the probe fails. */
|
|
107
165
|
const claudeSeedAttempted = new Set();
|
|
166
|
+
/** Roots this process has already attempted a codex-account seed for (see runStartupCheck),
|
|
167
|
+
* so the supervisor's and redeploy's repeated startup checks do not respawn `codex login
|
|
168
|
+
* status` on every poll when the probe fails. */
|
|
169
|
+
const codexSeedAttempted = new Set();
|
|
108
170
|
/** Roots this process has already attempted an opencode-catalog seed for (see runStartupCheck),
|
|
109
171
|
* so the supervisor's and redeploy's repeated startup checks do not respawn the models.dev
|
|
110
172
|
* fetch on every poll when it fails. */
|
|
@@ -131,7 +193,7 @@ export function opencodeProviders(config) {
|
|
|
131
193
|
* as a fallback, the resolver, or the director is checked exactly like a primary seam.
|
|
132
194
|
* One list the startup gate and doctor's binary check share, so a route the harness would
|
|
133
195
|
* spawn is checked on both surfaces. */
|
|
134
|
-
export function
|
|
196
|
+
export function backendsInUse(config) {
|
|
135
197
|
return [...new Set(seamsInUse(config).map((s) => s.agent ?? "pi"))];
|
|
136
198
|
}
|
|
137
199
|
/** The first backend an enabled seam routes to that this build has no registered adapter for,
|
|
@@ -139,10 +201,10 @@ export function agentsInUse(config) {
|
|
|
139
201
|
* are registered by importing src/agent/run-agent.ts (see the import above); a seam backend
|
|
140
202
|
* that is not in the registry is one this build cannot run. Doctor's binary check asks the
|
|
141
203
|
* same question, so the startup gate and doctor cannot disagree about which backend a build
|
|
142
|
-
* can serve.
|
|
143
|
-
* future id. */
|
|
204
|
+
* can serve. opencode's and codex's adapters now register like pi's and claude's, so only a
|
|
205
|
+
* future id without an adapter trips this rule. */
|
|
144
206
|
export function unregisteredBackendInUse(config) {
|
|
145
|
-
return
|
|
207
|
+
return backendsInUse(config).find((agent) => backendById(agent) === undefined);
|
|
146
208
|
}
|
|
147
209
|
/** runStartupCheck reduced to its verdict: the message a generation would fail on right now, or
|
|
148
210
|
* null when one would boot — what the redeploy and the supervisor ask. */
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/** The error-streak circuit breaker (PLANS.md 2026-09-30): a role whose
|
|
2
|
-
* consecutively is warned at ERROR_STREAK_WARN (src/
|
|
1
|
+
/** The error-streak circuit breaker (PLANS.md 2026-09-30): a role whose runs keep failing
|
|
2
|
+
* consecutively is warned at ERROR_STREAK_WARN (src/run/run-apply.ts) and named by the team-wide
|
|
3
3
|
* storm alarms (src/team/error-storm.ts, src/team/failure-spread.ts), but both only talk — the
|
|
4
|
-
* role keeps
|
|
5
|
-
* spend on a
|
|
4
|
+
* role keeps running on the error ladder's 600 s max backoff forever, burning a model slot and
|
|
5
|
+
* spend on a role that cannot succeed. Past ERROR_STREAK_BREAKER consecutive failed runs this gate
|
|
6
6
|
* pauses the role through the same per-role pause marker the operator's `tumwater pause --role
|
|
7
7
|
* <id>` writes (src/team/team-state.ts's pauseRole — the marker, lock, and idempotence come
|
|
8
|
-
* free), so the scheduler's existing pausedRolesSet skip blocks new
|
|
8
|
+
* free), so the scheduler's existing pausedRolesSet skip blocks new runs with zero scheduler
|
|
9
9
|
* changes. The director is NOT exempt: a failing director cannot process prompts anyway, and one
|
|
10
10
|
* uniform rule needs no carve-out. `tumwater resume --role <id>` (or the dashboard's per-row
|
|
11
11
|
* toggle) lifts it, as today.
|
|
@@ -14,27 +14,33 @@
|
|
|
14
14
|
* here so it is unit-testable without a team, and the module owns the only event emission — the
|
|
15
15
|
* orchestrator's poll owns only the wiring.
|
|
16
16
|
*
|
|
17
|
-
* Ack bookkeeping. The marker
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
17
|
+
* Ack bookkeeping. The marker is the ack carrier within a run, and the durable copy of the
|
|
18
|
+
* ack lives in each runner's persisted `RunnerState.streakAcked`: every poll, each role present
|
|
19
|
+
* in the paused set has its streak recorded in the in-memory `acked` cache (and flushed to the
|
|
20
|
+
* runner's state), so after the operator resumes a breaker-paused role, re-tripping
|
|
21
|
+
* requires ERROR_STREAK_BREAKER *more* consecutive failures, not one — the same grace an
|
|
22
|
+
* operator-paused failing role gets (its streak is acked while the pause stands, so resuming
|
|
23
|
+
* it does not instantly re-trip on the pre-pause streak). Because the ack is
|
|
24
|
+
* persisted, a harness restart after a resume does NOT re-trip a role whose frozen streak
|
|
25
|
+
* never ran down — the durable cause's first poll finds the persisted ack and leaves the role
|
|
26
|
+
* alone (BUGS.md 2026-10-23: the old in-memory-only ack re-paused a resumed role on every
|
|
27
|
+
* auto-redeploy). A streak that resets to 0 through the role's own success clears the ack
|
|
28
|
+
* (from the cache and the runner's state): the next episode trips on its own fresh
|
|
29
|
+
* ERROR_STREAK_BREAKER failures.
|
|
27
30
|
*
|
|
28
|
-
* Accepted behavior, stated here and not fixed in this change: the dashboards' "failing
|
|
29
|
-
*
|
|
30
|
-
* listing a breaker-paused role until the streak clears; the
|
|
31
|
-
* it is not
|
|
32
|
-
import { ERROR_STREAK_BREAKER } from "../
|
|
33
|
-
import {
|
|
31
|
+
* Accepted behavior, stated here and not fixed in this change: the dashboards' "failing run after
|
|
32
|
+
* run" alert (src/ui/team-alerts.ts) reads consecutiveErrors regardless of pause, so it keeps
|
|
33
|
+
* listing a breaker-paused role until the streak clears; the role cell's paused badge explains why
|
|
34
|
+
* it is not running, and the role_streak_paused event tells the operator why. */
|
|
35
|
+
import { ERROR_STREAK_BREAKER } from "../run/run-apply.js";
|
|
36
|
+
import { saveRunnerStateBestEffort } from "../runner/runner-state.js";
|
|
37
|
+
import { runnerPaused } from "../roles/runner-ids.js";
|
|
34
38
|
import { pauseRole } from "../team/team-state.js";
|
|
35
39
|
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
36
|
-
/** A fresh gate state: nothing
|
|
37
|
-
*
|
|
40
|
+
/** A fresh gate state: nothing cached yet, so the first poll of a runner whose persisted
|
|
41
|
+
* `streakAcked` is set seeds it from there — a restart does NOT re-trip a role the breaker
|
|
42
|
+
* already excused (the bug this state exists to fix). A runner with no persisted ack that
|
|
43
|
+
* is at or past the breaker trips on the first poll, as before. */
|
|
38
44
|
export function newStreakGateState() {
|
|
39
45
|
return { acked: new Map() };
|
|
40
46
|
}
|
|
@@ -42,7 +48,7 @@ export function newStreakGateState() {
|
|
|
42
48
|
* has reached ERROR_STREAK_BREAKER plus what its ack already excused, and whose role is not
|
|
43
49
|
* in `pausedRoles`: write the per-role pause marker and log one `role_streak_paused` event
|
|
44
50
|
* carrying the role, the streak, and the last error. Returns the pause keys this poll wrote
|
|
45
|
-
* (`pauseKey` maps a
|
|
51
|
+
* (`pauseKey` maps a role id to its marker key; instance 1 of a multi-instance role pauses as
|
|
46
52
|
* `<role>-1`), so the wiring can fold them into the poll's paused-roles view and the scheduler
|
|
47
53
|
* blocks them on this very poll, not the next one.
|
|
48
54
|
*
|
|
@@ -54,16 +60,30 @@ export function pollStreakGate(root, state, runners, pausedRoles, pauseKey = (r)
|
|
|
54
60
|
const tripped = [];
|
|
55
61
|
for (const { role, state: s } of runners) {
|
|
56
62
|
const streak = s.consecutiveErrors ?? 0;
|
|
63
|
+
// Seed a fresh in-run ack cache from the persisted excused streak, so a restart keeps the
|
|
64
|
+
// grace the breaker (and a later operator resume) relies on instead of re-pausing a role
|
|
65
|
+
// whose frozen streak never ran down (BUGS.md 2026-10-23).
|
|
66
|
+
if (!state.acked.has(role) && s.streakAcked !== undefined) {
|
|
67
|
+
state.acked.set(role, s.streakAcked);
|
|
68
|
+
}
|
|
57
69
|
// Ack while paused: the marker is the ack carrier. The streak is frozen while a role is
|
|
58
|
-
// paused (no
|
|
59
|
-
if (
|
|
60
|
-
state.acked.
|
|
70
|
+
// paused (no new runs start), so recording it here is exactly the level the resume excuses.
|
|
71
|
+
if (runnerPaused(pausedRoles, role)) {
|
|
72
|
+
if ((state.acked.get(role) ?? 0) !== streak) {
|
|
73
|
+
state.acked.set(role, streak);
|
|
74
|
+
s.streakAcked = streak;
|
|
75
|
+
saveRunnerStateBestEffort(root, s);
|
|
76
|
+
}
|
|
61
77
|
continue;
|
|
62
78
|
}
|
|
63
79
|
if (streak === 0) {
|
|
64
80
|
// A streak that reset through the role's own success ends the excused episode: the
|
|
65
|
-
// next one must climb a fresh ERROR_STREAK_BREAKER of its own.
|
|
66
|
-
state.acked.
|
|
81
|
+
// next one must climb a fresh ERROR_STREAK_BREAKER of its own, so drop the durable ack too.
|
|
82
|
+
if (state.acked.get(role) !== undefined) {
|
|
83
|
+
state.acked.delete(role);
|
|
84
|
+
delete s.streakAcked;
|
|
85
|
+
saveRunnerStateBestEffort(root, s);
|
|
86
|
+
}
|
|
67
87
|
continue;
|
|
68
88
|
}
|
|
69
89
|
const ack = state.acked.get(role) ?? 0;
|
|
@@ -71,9 +91,13 @@ export function pollStreakGate(root, state, runners, pausedRoles, pauseKey = (r)
|
|
|
71
91
|
continue;
|
|
72
92
|
// Ack at trip time, before the pause is observed: the bar for the next trip is
|
|
73
93
|
// ERROR_STREAK_BREAKER more failures from this level, whether or not any poll sees the
|
|
74
|
-
// role paused in between (an operator resume racing the next poll must not re-trip).
|
|
94
|
+
// role paused in between (an operator resume racing the next poll must not re-trip). The
|
|
95
|
+
// durable copy is written only once the pause is confirmed below — a failed or crashed
|
|
96
|
+
// pauseRole must not leave a permanent excused ack with no marker behind it.
|
|
75
97
|
state.acked.set(role, streak);
|
|
76
98
|
if (pauseRole(root, pauseKey(role))) {
|
|
99
|
+
s.streakAcked = streak;
|
|
100
|
+
saveRunnerStateBestEffort(root, s);
|
|
77
101
|
logEventBestEffort(root, {
|
|
78
102
|
source: TUMWATER_SOURCE,
|
|
79
103
|
type: "role_streak_paused",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
import { truncate } from "../text/text.js";
|
|
1
|
+
import { truncate, joinParts } from "../text/text.js";
|
|
3
2
|
import { compactTokens } from "../text/format.js";
|
|
3
|
+
import { modelLabel } from "../agent/backend-traits.js";
|
|
4
4
|
import { labeledLine } from "../verdict/reply-contract.js";
|
|
5
|
-
/** Assembling
|
|
5
|
+
/** Assembling run commit messages from pi's final reply: the SUMMARY line becomes the
|
|
6
6
|
* subject, the WHY/RISK/VERIFIED block becomes the body, and the harness stamps a trailer
|
|
7
|
-
* with
|
|
7
|
+
* with run metadata. Split out of prompt.ts — which keeps building the prompts that declare
|
|
8
8
|
* this contract (the machine-detectable half — sentinel and verdict-line detection — lives in
|
|
9
9
|
* reply-contract.ts) — because turning a reply into what `git commit` receives is the git side
|
|
10
|
-
* of the contract, consumed by the
|
|
10
|
+
* of the contract, consumed by the run pipeline (run-stage.ts, run-verdict.ts) and
|
|
11
11
|
* refusal.ts. */
|
|
12
12
|
/** Cap on the commit subject's summary portion (the SUMMARY line), so a verbose model cannot
|
|
13
13
|
* bloat every commit subject. Truncated with an ellipsis — like the body fields below — rather
|
|
@@ -25,23 +25,39 @@ export function extractSummary(finalText) {
|
|
|
25
25
|
const summary = labeledLine(finalText, "SUMMARY");
|
|
26
26
|
return summary === null ? null : truncate(summary, COMMIT_SUMMARY_MAX);
|
|
27
27
|
}
|
|
28
|
-
/** The subject for a changed
|
|
28
|
+
/** The subject for a changed run whose reply — and whose follow-up request — carried no
|
|
29
29
|
* SUMMARY line: named from what actually changed, so the log still says what the commit touched
|
|
30
|
-
* instead of only which
|
|
30
|
+
* instead of only which role made it. Up to three repo-relative paths (worktree order), then a
|
|
31
31
|
* count; capped like a real summary. Falls back to the bare "<role> run N" only when nothing is
|
|
32
32
|
* known. */
|
|
33
|
-
export function fallbackSummary(files, role,
|
|
33
|
+
export function fallbackSummary(files, role, runNumber) {
|
|
34
34
|
if (files.length === 0)
|
|
35
|
-
return `${role} run ${
|
|
35
|
+
return `${role} run ${runNumber}`;
|
|
36
36
|
const shown = files.slice(0, 3).join(", ");
|
|
37
37
|
const rest = files.length > 3 ? ` and ${files.length - 3} more` : "";
|
|
38
38
|
return truncate(`Update ${shown}${rest}`, COMMIT_SUMMARY_MAX);
|
|
39
39
|
}
|
|
40
|
-
/** Cap on each commit-body field
|
|
41
|
-
|
|
40
|
+
/** Cap on each commit-body field. It only bounds a runaway reply: at 200 it cut almost half of
|
|
41
|
+
* all body lines, which reviewers rejected as incomplete claims. */
|
|
42
|
+
const COMMIT_BODY_FIELD_MAX = 1000;
|
|
43
|
+
/** A numbered `Objection N: <disposition>` line a revision run's reply carries (the closing
|
|
44
|
+
* contract buildRevisionNote teaches, gate-notes.ts) — the author's answer to the prior
|
|
45
|
+
* review's Nth numbered objection. Anchored at line start so prose that merely mentions an
|
|
46
|
+
* objection mid-sentence cannot be read as one. */
|
|
47
|
+
const OBJECTION_LINE = /^\s*Objection (\d+):\s*(.+)\s*$/gm;
|
|
48
|
+
/** Pull the numbered Objection lines out of a reply, each capped like a body field. */
|
|
49
|
+
function extractObjections(finalText) {
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const m of finalText.matchAll(OBJECTION_LINE)) {
|
|
52
|
+
out.push({ n: Number(m[1]), text: truncate(m[2] ?? "", COMMIT_BODY_FIELD_MAX) });
|
|
53
|
+
}
|
|
54
|
+
// Preserve the numbered order regardless of where the author placed each line.
|
|
55
|
+
out.sort((a, b) => a.n - b.n);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
42
58
|
/** Pull the WHY:/RISK:/VERIFIED: lines out of a pi final reply via the shared labeled-line
|
|
43
59
|
* parser, tolerant of any subset being absent (a non-compliant reply still commits), each field
|
|
44
|
-
* capped at
|
|
60
|
+
* capped at COMMIT_BODY_FIELD_MAX (truncate + ellipsis). Null when none were found. */
|
|
45
61
|
export function extractCommitBody(finalText) {
|
|
46
62
|
const pick = (label) => {
|
|
47
63
|
const v = labeledLine(finalText, label);
|
|
@@ -52,50 +68,59 @@ export function extractCommitBody(finalText) {
|
|
|
52
68
|
risk: pick("RISK"),
|
|
53
69
|
verified: pick("VERIFIED"),
|
|
54
70
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
const objections = extractObjections(finalText);
|
|
72
|
+
if (objections.length > 0)
|
|
73
|
+
body.objections = objections;
|
|
74
|
+
return body.why || body.risk || body.verified || objections.length > 0 ? body : null;
|
|
75
|
+
}
|
|
76
|
+
/** Add the pre-queue self-check fix-up turn's RISK and VERIFIED lines to the run's body, each
|
|
77
|
+
* after the authoring run's own line as `(self-check: …)`. The fix-up turn answers the
|
|
78
|
+
* self-check's findings, not the change. When its lines replaced the authoring run's, the commit
|
|
79
|
+
* described that answer over the whole diff ("No code changed" on a full feature), and review
|
|
80
|
+
* rejected it as claims-vs-diff (BUGS.md 2026-10-21). Its WHY is dropped for the same reason.
|
|
81
|
+
* Returns `base` unchanged when the reply has neither line. */
|
|
82
|
+
export function appendSelfCheckNotes(base, fixUp) {
|
|
83
|
+
if (!fixUp?.risk && !fixUp?.verified)
|
|
63
84
|
return base;
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
risk: update.risk ?? base?.risk,
|
|
67
|
-
verified: update.verified ?? base?.verified,
|
|
68
|
-
};
|
|
69
|
-
return merged.why || merged.risk || merged.verified ? merged : null;
|
|
85
|
+
const note = (own, added) => added === undefined ? own : own === undefined ? `self-check: ${added}` : `${own} (self-check: ${added})`;
|
|
86
|
+
return { ...base, risk: note(base?.risk, fixUp.risk), verified: note(base?.verified, fixUp.verified) };
|
|
70
87
|
}
|
|
71
|
-
/** The body's lines in commit order ("WHY: …\nRISK: …\nVERIFIED: …"); "" when
|
|
72
|
-
*
|
|
88
|
+
/** The body's lines in commit order ("WHY: …\nRISK: …\nVERIFIED: …\nObjection N: …"); "" when
|
|
89
|
+
* the body has no fields. Objection lines follow the VERIFIED line, numbered as the author
|
|
90
|
+
* wrote them. Empty when the body is empty. */
|
|
73
91
|
export function formatCommitBody(body) {
|
|
74
|
-
return [
|
|
92
|
+
return joinParts([
|
|
75
93
|
body.why && `WHY: ${body.why}`,
|
|
76
94
|
body.risk && `RISK: ${body.risk}`,
|
|
77
95
|
body.verified && `VERIFIED: ${body.verified}`,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.join("\n");
|
|
96
|
+
...(body.objections ?? []).map((o) => `Objection ${o.n}: ${o.text}`),
|
|
97
|
+
], "\n");
|
|
81
98
|
}
|
|
82
|
-
/** The harness-stamped trailer line of every
|
|
83
|
-
* model claims. `turns` sums this
|
|
84
|
-
* is their largest single-request context. A changed
|
|
99
|
+
/** The harness-stamped trailer line of every run commit — truth from the run's counters, not
|
|
100
|
+
* model claims. `turns` sums this run's pre-commit pi runs (main + transient retry); `peakCtx`
|
|
101
|
+
* is their largest single-request context. A changed run flagged high-friction (plans/
|
|
85
102
|
* refusal-and-thrash.md) appends a sibling Friction line after the Run line — minutes rounded,
|
|
86
|
-
* e.g. `Friction: high (41 turns / 62m)`; only changed
|
|
87
|
-
* never passes the argument and the Run line's asserted format stays stable.
|
|
88
|
-
|
|
89
|
-
|
|
103
|
+
* e.g. `Friction: high (41 turns / 62m)`; only changed runs carry it, so the refusal path
|
|
104
|
+
* never passes the argument and the Run line's asserted format stays stable. A `modelLine` (see
|
|
105
|
+
* modelTrailerLine) goes on its own line directly after the Run line, before Friction and
|
|
106
|
+
* Revision, so Revision stays last. */
|
|
107
|
+
export function commitTrailer(role, runNumber, turns, peakCtx, highFrictionMinutes, revisionRound, modelLine) {
|
|
108
|
+
const base = `Run: ${role} #${runNumber} · turns ${turns} · ctx ${compactTokens(peakCtx)}`;
|
|
109
|
+
const model = modelLine === undefined ? "" : `\n${modelLine}`;
|
|
90
110
|
const friction = highFrictionMinutes === undefined
|
|
91
111
|
? ""
|
|
92
112
|
: `\nFriction: high (${turns} turns / ${Math.round(highFrictionMinutes)}m)`;
|
|
93
113
|
// The revision line is last and absent on a fresh change: leftover recovery reads it back to
|
|
94
114
|
// keep the rejected change's round when the queue marker is lost (plans/revise-rejected.md).
|
|
95
115
|
const revision = revisionRound === undefined ? "" : `\nRevision: ${revisionRound}`;
|
|
96
|
-
return `${base}${friction}${revision}`;
|
|
116
|
+
return `${base}${model}${friction}${revision}`;
|
|
97
117
|
}
|
|
98
|
-
/** The
|
|
118
|
+
/** The trailer's Model line: the model and backend a run's seam started on, e.g.
|
|
119
|
+
* `Model: claude-opus-5-5 · backend claude`; `default` when the seam names no model. */
|
|
120
|
+
export function modelTrailerLine(agent, provider, model) {
|
|
121
|
+
return `Model: ${model === undefined ? "default" : modelLabel(agent, provider, model)} · backend ${agent}`;
|
|
122
|
+
}
|
|
123
|
+
/** The high-friction trailer line a flagged run's commit carries (see commitTrailer) — the
|
|
99
124
|
* durable record recovery reads back. Anchored to the whole line so prose that merely mentions
|
|
100
125
|
* friction cannot set the flag. */
|
|
101
126
|
const FRICTION_TRAILER = /^Friction: high \(\d+ turns \/ \d+m\)$/m;
|
|
@@ -110,15 +135,15 @@ const REVISION_TRAILER = /^Revision: (\d+)$/m;
|
|
|
110
135
|
* undefined for a fresh change. Leftover recovery reads this off the pinned commit so a
|
|
111
136
|
* retriable revision keeps its round (and its rejected ref's cleanup) across a lost queue
|
|
112
137
|
* marker (plans/revise-rejected.md). */
|
|
113
|
-
function revisionRoundOf(message) {
|
|
138
|
+
export function revisionRoundOf(message) {
|
|
114
139
|
const m = REVISION_TRAILER.exec(message);
|
|
115
140
|
return m ? Number(m[1]) : undefined;
|
|
116
141
|
}
|
|
117
|
-
/** The harness-stamped commit-subject prefix — the `tumwater(<role>):` part
|
|
142
|
+
/** The harness-stamped commit-subject prefix — the `tumwater(<role>):` part run-stage.ts (and
|
|
118
143
|
* refusal.ts) adds before the SUMMARY line, space included when a summary follows. */
|
|
119
|
-
const SUBJECT_PREFIX = /^tumwater\([^)]
|
|
144
|
+
const SUBJECT_PREFIX = /^tumwater\(([^)]+)\):(?:\s|$)/;
|
|
120
145
|
/** Build a harness-stamped commit subject: `tumwater(<role>): <summary>` — the exact shape
|
|
121
|
-
* SUBJECT_PREFIX below matches, so the stamp's writers (
|
|
146
|
+
* SUBJECT_PREFIX below matches, so the stamp's writers (run-stage.ts's normal run commit and
|
|
122
147
|
* refusal.ts's refusal note) and its parser live in one file and cannot drift apart. */
|
|
123
148
|
export function stampedSubject(role, summary) {
|
|
124
149
|
return `tumwater(${role}): ${summary}`;
|
|
@@ -137,6 +162,12 @@ export function commitSubject(message) {
|
|
|
137
162
|
const rest = line.slice(m[0].length).trim();
|
|
138
163
|
return rest === "" ? undefined : rest;
|
|
139
164
|
}
|
|
165
|
+
/** The `<role>` of a commit subject the harness stamped (`tumwater(<role>): …`); undefined for an
|
|
166
|
+
* unstamped line. */
|
|
167
|
+
export function stampedRole(message) {
|
|
168
|
+
const line = (message.split("\n", 1)[0] ?? "").trim();
|
|
169
|
+
return SUBJECT_PREFIX.exec(line)?.[1];
|
|
170
|
+
}
|
|
140
171
|
/** Reconstruct the review-gate metadata from a commit message the harness stamped itself.
|
|
141
172
|
* Leftover recovery has no authoring run to read from, so the pinned commit's message is the
|
|
142
173
|
* only source of truth: without this a recovered high-friction change loses both its flag and
|
|
@@ -153,7 +184,7 @@ export function parseCommitMetadata(message) {
|
|
|
153
184
|
...(revisionRound !== undefined ? { revisionRound } : {}),
|
|
154
185
|
};
|
|
155
186
|
}
|
|
156
|
-
/** Assemble a
|
|
187
|
+
/** Assemble a run's full commit message — the single place that builds one. The subject is
|
|
157
188
|
* the existing "tumwater(<role>): <summary>" line; the author's body (omitted when absent)
|
|
158
189
|
* and the harness-stamped trailer follow as separate paragraphs. Refusal commits route
|
|
159
190
|
* through here too (subject + trailer only). */
|
package/dist/src/git/git-diff.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* ahead-of-main diff extraction (including its oversized-diff truncation). Everything here
|
|
4
4
|
* interprets git output; spawning git stays in git.ts. */
|
|
5
5
|
import { changeBaseRev, gitLines } from "./git.js";
|
|
6
|
-
import { gitTry } from "./git-run.js";
|
|
6
|
+
import { git, gitTry } from "./git-run.js";
|
|
7
7
|
import { cutSplitsSurrogatePair } from "../text/text.js";
|
|
8
8
|
/** Git's C-quoting short escapes (its `sq_lookup` table) and the byte each decodes to. The
|
|
9
9
|
* mapping was a nine-case switch that repeated `bytes += …; break;` per entry; keeping it as
|
|
@@ -68,7 +68,10 @@ export function unquotePorcelainPath(p) {
|
|
|
68
68
|
}
|
|
69
69
|
/** Repo-relative paths of every change in the worktree — modified, untracked, and deleted, parsed
|
|
70
70
|
* from `git status --porcelain -z` (paths only). The refusal path uses this to classify what a
|
|
71
|
-
* refusing run left behind: markdown notes may be kept, everything else is discarded.
|
|
71
|
+
* refusing run left behind: markdown notes may be kept, everything else is discarded. A failed
|
|
72
|
+
* `git status` throws: read as "no changes", it once sent a refusal's or a self-check's
|
|
73
|
+
* decision down the path that drops the run's work (2026-10-21's spawn wedge), so a caller that
|
|
74
|
+
* only reports (a view, a warning) catches it itself.
|
|
72
75
|
*
|
|
73
76
|
* The `-z` format is what git recommends for machine parsing: records are NUL-terminated and
|
|
74
77
|
* paths are emitted verbatim, never C-quoted, so a path with whitespace, a quote, a control
|
|
@@ -79,7 +82,7 @@ export function unquotePorcelainPath(p) {
|
|
|
79
82
|
* `-z` a rename/copy is two records — `XY <to>\0<from>\0`, destination first — so this reads
|
|
80
83
|
* the destination (the path that exists now) and skips the extra origin record. */
|
|
81
84
|
export async function changedFiles(wt) {
|
|
82
|
-
const out = await
|
|
85
|
+
const out = await git(wt, "status", "--porcelain", "-z");
|
|
83
86
|
if (!out)
|
|
84
87
|
return [];
|
|
85
88
|
const files = [];
|
|
@@ -101,9 +104,11 @@ export async function changedFiles(wt) {
|
|
|
101
104
|
}
|
|
102
105
|
/** Repo-relative paths changed between the branch's fork point from main and its HEAD —
|
|
103
106
|
* everything a merge of this branch would bring in (the three-dot range diffs against the
|
|
104
|
-
* merge-base, so commits main gained during the
|
|
107
|
+
* merge-base, so commits main gained during the run are not included). A failed `git diff`
|
|
108
|
+
* throws: an empty list is vacuously review-exempt (isExemptDiff), so the gate skipped its
|
|
109
|
+
* reviewer and the in-lock re-check its build check. A view catches it itself. */
|
|
105
110
|
export async function aheadOfMainFiles(wt, mainBranch) {
|
|
106
|
-
const out = await
|
|
111
|
+
const out = await git(wt, "diff", "--name-only", `${mainBranch}...HEAD`);
|
|
107
112
|
// Decode git's C-quoted names (same convention as `git status --porcelain`): every caller
|
|
108
113
|
// uses these as real worktree paths or matches them against plain HEAD-provided names, and
|
|
109
114
|
// an undecoded `"h\303\251llo.md"` reads as a nonexistent file (hasConflictMarkers) and
|
|
@@ -143,19 +148,20 @@ function splitDiffByFile(diff) {
|
|
|
143
148
|
* oversized diff is itself reviewable information, and the reviewer can read any file in the
|
|
144
149
|
* worktree directly). The per-file sections are split out of the one full diff already fetched —
|
|
145
150
|
* spawning a diff per file would re-diff the entire range once per file for text we already hold in
|
|
146
|
-
* memory (measured ~0.5 s of redundant git work on a 530 KB / 39-file
|
|
151
|
+
* memory (measured ~0.5 s of redundant git work on a 530 KB / 39-file run). Section byte length
|
|
147
152
|
* ranks files at least as well as numstat's added+deleted: it is exactly what the reviewer will
|
|
148
153
|
* see, and a binary file's short "Binary files differ" stub naturally ranks below any real text
|
|
149
|
-
* change.
|
|
150
|
-
|
|
154
|
+
* change. A failed `git diff` throws: the reviewer handed an empty diff judged no change at
|
|
155
|
+
* all. A view catches it itself. */
|
|
156
|
+
export async function aheadOfMainDiff(wt, mainBranch, maxBytes = 200_000, pathspec = []) {
|
|
151
157
|
const range = `${mainBranch}...HEAD`;
|
|
152
|
-
const full =
|
|
158
|
+
const full = await git(wt, "diff", range, ...pathspec);
|
|
153
159
|
if (full.length <= maxBytes)
|
|
154
160
|
return full;
|
|
155
161
|
// Over the cap: rank files by change size and include the largest while budget allows.
|
|
156
162
|
const ranked = splitDiffByFile(full).sort((a, b) => b.length - a.length);
|
|
157
163
|
let out = `[diff truncated: the full ahead-of-main diff is ${full.length} bytes; ` +
|
|
158
|
-
`showing --stat plus the largest files]\n\n` + (
|
|
164
|
+
`showing --stat plus the largest files]\n\n` + (await git(wt, "diff", "--stat", range, ...pathspec)) + "\n";
|
|
159
165
|
for (const d of ranked) {
|
|
160
166
|
// Account for the surrounding newlines in the budget check so the output never exceeds maxBytes
|
|
161
167
|
// even when a section ends exactly on the boundary.
|