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,34 +1,50 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
import { baselineBlocked, BUGFIX_ROLE } from "../roles/roles.js";
|
|
2
|
+
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
3
|
+
import { enqueueRolePrompt } from "../inbox/inbox.js";
|
|
3
4
|
import { isCustomRole, liveConfig } from "../config/config.js";
|
|
4
|
-
import { BUILD_CHECK_TIMEOUT_MS } from "../build/build-check-detect.js";
|
|
5
|
-
import { failureHeadline } from "../build/build-check-report.js";
|
|
5
|
+
import { BUILD_CHECK_TIMEOUT_MS, mergeCommandOf } from "../build/build-check-detect.js";
|
|
6
|
+
import { failureHeadline, FAILING_TESTS_MAX } from "../build/build-check-report.js";
|
|
6
7
|
import { baselineCheckEventLogger, buildCheckSkipWarning, sleptPhrase } from "../build/build-check-events.js";
|
|
7
|
-
import { checkMainBaseline } from "./main-baseline.js";
|
|
8
|
-
import { buildMainRedNote } from "../prompt/gate-
|
|
8
|
+
import { checkMainBaseline, watchedBaseline } from "./main-baseline.js";
|
|
9
|
+
import { buildMainRedNote } from "../prompt/gate-notes.js";
|
|
9
10
|
import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
10
11
|
import { errorMessage } from "../text/text.js";
|
|
11
|
-
import { mainRedPhrase } from "../text/phrases.js";
|
|
12
|
+
import { mainRedPhrase, plural } from "../text/phrases.js";
|
|
13
|
+
import { clipToWidth } from "../text/text-width.js";
|
|
14
|
+
import { shortSha } from "../text/format.js";
|
|
12
15
|
import { branchHead } from "../git/git.js";
|
|
13
16
|
import { gateMainWorktreePath } from "../paths.js";
|
|
14
17
|
import { useWorktree } from "../git/worktree-use.js";
|
|
15
18
|
import { ensureDetachedWorktree } from "../git/worktree.js";
|
|
16
|
-
/** Red-main baseline gate for fresh authoring
|
|
19
|
+
/** Red-main baseline gate for fresh authoring runs: before an
|
|
17
20
|
* authoring run is spent on top of pristine main, verify that MAIN ITSELF is green — a red main
|
|
18
21
|
* rejects every code diff deterministically at the review gate (BUGS.md's "Build broken on main"
|
|
19
22
|
* and "Tests red on main" entries), so while a SHA is known red the blocked roles skip authoring
|
|
20
|
-
* instead of burning runs that are guaranteed to fail. Split out of
|
|
21
|
-
*
|
|
22
|
-
* state (one harness-level `main_red_hold` event
|
|
23
|
-
*
|
|
23
|
+
* instead of burning runs that are guaranteed to fail. Split out of runner.ts — which keeps the
|
|
24
|
+
* run lifecycle around it — because this is a self-contained policy with its own per-process
|
|
25
|
+
* state (one harness-level `main_red_hold` event AND one queued healer prompt per
|
|
26
|
+
* newly-discovered red SHA) and its own
|
|
27
|
+
* event contract; what it borrows from the role is identity and the role's worktree, and it
|
|
24
28
|
* consumes
|
|
25
29
|
* build/build-check.ts's checkMainBaseline machinery (detection + execution + per-SHA cache),
|
|
26
|
-
* which stays unaware of
|
|
30
|
+
* which stays unaware of runs. */
|
|
27
31
|
/** The last main SHA for which this process logged a red-main hold event (the baseline check): one
|
|
28
|
-
* harness-level warning per newly-discovered red SHA, not one per blocked role's
|
|
32
|
+
* harness-level warning per newly-discovered red SHA, not one per blocked role's run —
|
|
29
33
|
* module-level so every runner in the team shares it. A restart re-logs once: the cache is cold
|
|
30
34
|
* then too, and an operator restarting into a still-red main should see why nothing merges. */
|
|
31
35
|
let lastMainRedSha = null;
|
|
36
|
+
/** The key of the last failing set the healer was sent a prompt for: the sorted failing test
|
|
37
|
+
* names, or the red SHA when the red names none. Two red commits with the same failing set send
|
|
38
|
+
* one heal prompt, not one per commit; a green main observed by the gate or the bugfix handoff
|
|
39
|
+
* clears it, so a set that returns after green is healed again. */
|
|
40
|
+
let lastHealKey = null;
|
|
41
|
+
/** The failing tests a red verdict names, when the list is complete — non-empty and short of
|
|
42
|
+
* FAILING_TESTS_MAX, which may have been cut short (the same rule as onlyKnownMainFailures).
|
|
43
|
+
* Empty when it names none: main's red is then not a set of known failures, and the block stays. */
|
|
44
|
+
function namedFailures(red) {
|
|
45
|
+
const tests = red.failingTests ?? [];
|
|
46
|
+
return tests.length > 0 && tests.length < FAILING_TESTS_MAX ? tests : [];
|
|
47
|
+
}
|
|
32
48
|
/** The red-main message's shared spine — the SHA, script, and failure headline a red baseline
|
|
33
49
|
* carries, phrased once so the once-per-SHA warning and the main_red outcome cannot drift apart
|
|
34
50
|
* over what main looks like. `action` is the consequence each caller names. */
|
|
@@ -37,19 +53,74 @@ function redMainMessage(red, action) {
|
|
|
37
53
|
return `${mainRedPhrase(red.sha)} (${red.script}${firstLine ? `: ${firstLine}` : ""}) — ${action}`;
|
|
38
54
|
}
|
|
39
55
|
/** Log the team-wide `main_red_hold` event (the notify hook pages it) for `red`'s SHA at
|
|
40
|
-
* most once per process
|
|
41
|
-
* handoff share one guard: whichever observes the
|
|
42
|
-
|
|
56
|
+
* most once per process, and hand the red suite to the healer. Module-level (with
|
|
57
|
+
* lastMainRedSha) so the gate and the bugfix handoff share one guard: whichever observes the
|
|
58
|
+
* red first logs it, the other stays silent. `foundBy` is the role whose path verified the
|
|
59
|
+
* red — when that role is not bugfix itself, one prompt is enqueued to bugfix's own inbox so
|
|
60
|
+
* the healer runs on the orchestrator's next poll instead of waiting out its own interval or
|
|
61
|
+
* deferral; bugfix finding its own red skips the enqueue because its running run already
|
|
62
|
+
* carries the note and IS the heal (and enqueuing from inside it would queue a second run). A
|
|
63
|
+
* queued per-role prompt makes the role due by itself (`isEligible`, scheduling.ts, returns
|
|
64
|
+
* `{ run: true, reason: "inbox" }` on a non-empty inbox before any interval, backoff, or
|
|
65
|
+
* deferral check), and `pollRunnerReasons` skips the need-based deferral block for reason
|
|
66
|
+
* `inbox`, so no scheduler change is needed. A heal prompt that cannot be queued is warned under
|
|
67
|
+
* `foundBy` and clears the heal key, never thrown. */
|
|
68
|
+
function warnMainRedOnce(root, red, foundBy) {
|
|
43
69
|
if (lastMainRedSha === red.sha)
|
|
44
70
|
return;
|
|
45
71
|
lastMainRedSha = red.sha;
|
|
72
|
+
const named = namedFailures(red);
|
|
46
73
|
logEventBestEffort(root, {
|
|
47
74
|
source: TUMWATER_SOURCE,
|
|
48
75
|
type: "main_red_hold",
|
|
49
76
|
sha: red.sha,
|
|
50
77
|
script: red.script,
|
|
51
|
-
|
|
78
|
+
...(named.length > 0 ? { failingTests: named } : {}),
|
|
79
|
+
message: named.length > 0
|
|
80
|
+
? `${mainRedPhrase(red.sha)} (${plural(named.length, "failing test")}: ${clipToWidth(named[0] ?? "", 80)}${named.length > 1 ? `, +${named.length - 1} more` : ""}); changes that add no new failures still merge`
|
|
81
|
+
: redMainMessage(red, "code merges blocked until main is green"),
|
|
52
82
|
});
|
|
83
|
+
// Bugfix is the only repairer, and it is exempt from mainRedGate — but it is a work role
|
|
84
|
+
// that with no open bugs defers like a maintenance role, so a red main discovered by any
|
|
85
|
+
// other path could sit unrepaired until bugfix's own interval or a coincidental wake.
|
|
86
|
+
// Queue the heal: one prompt per newly-verified red SHA per process (a restart re-queues
|
|
87
|
+
// once, desirable because the cache is cold then too). With named failures the key is the
|
|
88
|
+
// failing set, so a second red commit with the same failures queues nothing.
|
|
89
|
+
const healKey = named.length > 0 ? [...named].sort().join("\n") : red.sha;
|
|
90
|
+
if (lastHealKey === healKey)
|
|
91
|
+
return;
|
|
92
|
+
lastHealKey = healKey;
|
|
93
|
+
if (baseRoleOf(foundBy) !== BUGFIX_ROLE) {
|
|
94
|
+
try {
|
|
95
|
+
enqueueRolePrompt(root, BUGFIX_ROLE, mainRedHealPrompt(red.sha));
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
// Clear the key so a later verification retries the heal. lastMainRedSha stays set, so a
|
|
99
|
+
// second call for this same SHA still returns early: the retry comes with the next red
|
|
100
|
+
// SHA or a fresh process.
|
|
101
|
+
lastHealKey = null;
|
|
102
|
+
warnEventBestEffort(root, foundBy, `could not queue the heal prompt for bugfix: ${errorMessage(err)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** The one-line, self-limiting directive queued for the bugfix healer when a non-bugfix path
|
|
107
|
+
* newly verifies a red main. It names the short SHA the check was reported red at, tells the
|
|
108
|
+
* healer to verify main is green now, and to reproduce and land the fix only if it is still
|
|
109
|
+
* red. The self-limiting wording is the green race's whole handling: if main turned green
|
|
110
|
+
* before the healer consumes the prompt, bugfixMainRedNote returns undefined (no `<main-red>`
|
|
111
|
+
* note is appended) and the "verify green" clause makes the run a no-op. */
|
|
112
|
+
export function mainRedHealPrompt(sha, detail) {
|
|
113
|
+
const base = `Main was reported red at commit ${shortSha(sha)}. Verify main is green now; if it is still red, reproduce the failure and land the fix.`;
|
|
114
|
+
if (!detail)
|
|
115
|
+
return base;
|
|
116
|
+
const tests = detail.failingTests.length > 0 ? ` Failing: ${detail.failingTests.join("; ")}.` : "";
|
|
117
|
+
const tail = detail.outputTail && detail.outputTail.length > 0 ? ` Output tail: ${detail.outputTail.join(" | ")}` : "";
|
|
118
|
+
return `${base} The watch named ${shortSha(detail.culprit)} as the commit that turned it red.${tests}${tail}`;
|
|
119
|
+
}
|
|
120
|
+
/** Queue the heal prompt for bugfix with the culprit's detail: the fallback when the automatic
|
|
121
|
+
* revert (src/baseline/main-revert.ts) is skipped or fails. */
|
|
122
|
+
export function queueCulpritHeal(root, tip, detail) {
|
|
123
|
+
enqueueRolePrompt(root, BUGFIX_ROLE, mainRedHealPrompt(tip, detail));
|
|
53
124
|
}
|
|
54
125
|
/** checkMainBaseline's per-run hook: log the one run per SHA (cache misses only) under the role
|
|
55
126
|
* that paid for it, with its duration — the gate's build_check sibling, so both halves of the
|
|
@@ -57,7 +128,7 @@ function warnMainRedOnce(root, red) {
|
|
|
57
128
|
* check this module runs — the bugfix handoff, the authoring gate, and mainTipVerdict's tip
|
|
58
129
|
* check (which review.ts's gate and merge-check-failures.ts's attributeRedCheck reach
|
|
59
130
|
* through it) — so the event's shape cannot drift between them. */
|
|
60
|
-
function baselineCheckLogger(root, role) {
|
|
131
|
+
export function baselineCheckLogger(root, role) {
|
|
61
132
|
return baselineCheckEventLogger(role, (event) => logEventBestEffort(root, event));
|
|
62
133
|
}
|
|
63
134
|
/** Red-main handoff for the `bugfix` healer: mainRedGate exempts
|
|
@@ -66,19 +137,23 @@ function baselineCheckLogger(root, role) {
|
|
|
66
137
|
* (cache and provisional-red re-verification reused unchanged, so it costs at most the one run
|
|
67
138
|
* the gate would have paid for) and, when main is red, returns the note that points the healer
|
|
68
139
|
* at the failure. Returns undefined on green, no declared check, or an environmental skip — the
|
|
69
|
-
* healer's
|
|
140
|
+
* healer's run then proceeds exactly as it did before this existed. Never throws. */
|
|
70
141
|
export async function bugfixMainRedNote(root, role, wt) {
|
|
71
142
|
// The declared check is detected through the live config (plans/portability.md §6/7); a
|
|
72
|
-
// broken file degrades to defaults, which declare none — the
|
|
143
|
+
// broken file degrades to defaults, which declare none — the run then proceeds as before.
|
|
73
144
|
const config = liveConfig(root);
|
|
74
|
-
const baseline =
|
|
145
|
+
const baseline = mergeCommandOf(config)
|
|
146
|
+
? { baseline: await watchedBaseline(wt) }
|
|
147
|
+
: await checkMainBaseline(wt, config, baselineCheckLogger(root, role));
|
|
75
148
|
const red = baseline.baseline;
|
|
149
|
+
if (red?.status === "green")
|
|
150
|
+
lastHealKey = null;
|
|
76
151
|
if (red?.status !== "red")
|
|
77
152
|
return undefined;
|
|
78
|
-
warnMainRedOnce(root, red);
|
|
153
|
+
warnMainRedOnce(root, red, role);
|
|
79
154
|
return buildMainRedNote(red.sha, red.script, failureHeadline(red.outputTail), red.outputTail);
|
|
80
155
|
}
|
|
81
|
-
/** Gate one fresh
|
|
156
|
+
/** Gate one fresh run on main's baseline. Returns null when authoring may proceed — role not
|
|
82
157
|
* blocked, no declared check, green, or an environmental skip (warned under the role and
|
|
83
158
|
* proceeded with, exactly like the review gate's pre-check) — otherwise the terminal
|
|
84
159
|
* `main_red` outcome for the caller to return as-is. Never throws: git, detection, and
|
|
@@ -86,8 +161,11 @@ export async function bugfixMainRedNote(root, role, wt) {
|
|
|
86
161
|
export async function mainRedGate(root, role, wt,
|
|
87
162
|
/** The sleep clock the baseline run measures host suspension with; defaults to the real
|
|
88
163
|
* sampleSleepClock. A test seam — production callers leave it unset. */
|
|
89
|
-
sampleSleep
|
|
90
|
-
|
|
164
|
+
sampleSleep,
|
|
165
|
+
/** Called with main's failing tests when the red names them: the role then runs on the red
|
|
166
|
+
* main instead of ending `main_red`, and the caller puts them in its prompt. */
|
|
167
|
+
onKnownRed) {
|
|
168
|
+
// User-defined roles are blocked alongside the built-in code roles (plans/user-defined-loops.md):
|
|
91
169
|
// an unknown charter may produce code, and on red main such diffs are rejected deterministically
|
|
92
170
|
// at the gate's pre-check — an authoring run would be pure waste. Customs come from
|
|
93
171
|
// tumwater.json, not the catalog, so read the live config (stat-cached; a broken file
|
|
@@ -95,7 +173,12 @@ sampleSleep) {
|
|
|
95
173
|
const cfg = liveConfig(root);
|
|
96
174
|
if (!baselineBlocked(role) && !isCustomRole(cfg, role))
|
|
97
175
|
return null;
|
|
98
|
-
|
|
176
|
+
// With check.mergeCommand set the lock no longer runs the full check, and neither does this
|
|
177
|
+
// gate: it reads the main watch's newest verdict for main's tip (or its nearest checked
|
|
178
|
+
// ancestor). No verdict yet reads as green.
|
|
179
|
+
const baseline = mergeCommandOf(cfg)
|
|
180
|
+
? { baseline: await watchedBaseline(wt) }
|
|
181
|
+
: await checkMainBaseline(wt, cfg, baselineCheckLogger(root, role), false, false, sampleSleep);
|
|
99
182
|
if (baseline.unverified) {
|
|
100
183
|
// The baseline run spanned a host sleep: no verdict about main, nothing cached (BUGS.md
|
|
101
184
|
// 2026-09-30). Warn-and-proceed, like a skip — the sleep is named, not a test failure.
|
|
@@ -108,17 +191,24 @@ sampleSleep) {
|
|
|
108
191
|
}
|
|
109
192
|
if (baseline.baseline?.status === "red") {
|
|
110
193
|
const red = baseline.baseline;
|
|
111
|
-
warnMainRedOnce(root, red);
|
|
194
|
+
warnMainRedOnce(root, red, role);
|
|
195
|
+
const named = namedFailures(red);
|
|
196
|
+
if (named.length > 0) {
|
|
197
|
+
onKnownRed?.(named);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
112
200
|
return {
|
|
113
201
|
result: "main_red",
|
|
114
202
|
summary: "code merges blocked until main is green",
|
|
115
|
-
// The cause rides the outcome so the
|
|
116
|
-
// (
|
|
203
|
+
// The cause rides the outcome so the run's `run_end` carries it
|
|
204
|
+
// (run-finalize.ts folds it into state.lastError before logging): the digest itemizes
|
|
117
205
|
// the outcome that blocks every merge, instead of leaving it a bare count behind a
|
|
118
206
|
// once-per-SHA warning (BUGS.md 2026-09-28).
|
|
119
207
|
error: redMainMessage(red, "authoring skipped until main is green"),
|
|
120
208
|
};
|
|
121
209
|
}
|
|
210
|
+
if (baseline.baseline?.status === "green")
|
|
211
|
+
lastHealKey = null;
|
|
122
212
|
return null;
|
|
123
213
|
}
|
|
124
214
|
/** Serializes mainTipVerdict's use of its one worktree: the merge pipeline's verifies run their
|
|
@@ -141,10 +231,27 @@ let gateMainQueue = Promise.resolve();
|
|
|
141
231
|
* failure on a clock- or load-sensitive test whose cached green has gone stale (BUGS.md
|
|
142
232
|
* 2026-10-06). The merge pipeline's attribution reads the cached verdict instead. */
|
|
143
233
|
export function mainTipVerdict(root, role, mainBranch, config, forceFresh = false) {
|
|
144
|
-
|
|
234
|
+
return queueBehindMainGates(() => verdictAtMainTip(root, role, mainBranch, config, forceFresh));
|
|
235
|
+
}
|
|
236
|
+
/** Run `task` after every earlier gate-main task has settled, and make it the queue's new tail. A
|
|
237
|
+
* task's rejection reaches only its own caller; the next task still runs. */
|
|
238
|
+
function queueBehindMainGates(task) {
|
|
239
|
+
const run = gateMainQueue.then(task);
|
|
145
240
|
gateMainQueue = run.catch(() => undefined);
|
|
146
241
|
return run;
|
|
147
242
|
}
|
|
243
|
+
/** Run `fn` in the `_gate-main` mirror re-pointed at `mainBranch`'s tip, queued behind the gates
|
|
244
|
+
* mainTipVerdict serves (gateMainQueue) and holding the mirror as they do. The main watch's way in
|
|
245
|
+
* (main-watch.ts): it hands `fn` the checkout and the tip it holds. Rejects when main is
|
|
246
|
+
* unreadable or the checkout fails — the caller is a background task that must catch. */
|
|
247
|
+
export function withMainTipWorktree(root, mainBranch, fn) {
|
|
248
|
+
return queueBehindMainGates(() => useWorktree(root, gateMainWorktreePath(root), async () => {
|
|
249
|
+
const tip = await branchHead(root, mainBranch);
|
|
250
|
+
if (!tip)
|
|
251
|
+
throw new Error(`${mainBranch} is unreadable`);
|
|
252
|
+
return fn(await ensureDetachedWorktree(root, gateMainWorktreePath(root), tip));
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
148
255
|
async function verdictAtMainTip(root, role, mainBranch, config, forceFresh) {
|
|
149
256
|
// Hold the `_gate-main` mirror from before ensureDetachedWorktree (its reset is part of the
|
|
150
257
|
// use) through the baseline check (plans/disk-floor.md, part 2/4).
|
|
@@ -164,7 +271,7 @@ async function verdictAtMainTipIn(root, role, mainBranch, config, forceFresh) {
|
|
|
164
271
|
if (baseline?.status === "green")
|
|
165
272
|
return { status: "green", sha: baseline.sha };
|
|
166
273
|
if (baseline?.status === "red") {
|
|
167
|
-
warnMainRedOnce(root, baseline);
|
|
274
|
+
warnMainRedOnce(root, baseline, role);
|
|
168
275
|
return { status: "red", sha: baseline.sha };
|
|
169
276
|
}
|
|
170
277
|
return {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { queueCulpritHeal } from "./main-red.js";
|
|
2
|
+
import { gateCommandOf } from "../build/build-check-detect.js";
|
|
3
|
+
import { runScopedBuildCheck } from "../build/build-check-scoped.js";
|
|
4
|
+
import { withLock } from "../concurrency/lock.js";
|
|
5
|
+
import { logEventBestEffort, TUMWATER_SOURCE, warnEventBestEffort } from "../events/events.js";
|
|
6
|
+
import { branchHead, patchId } from "../git/git.js";
|
|
7
|
+
import { gitTry } from "../git/git-run.js";
|
|
8
|
+
import { stampedSubject } from "../git/commit-message.js";
|
|
9
|
+
import { ensureDetachedWorktree } from "../git/worktree.js";
|
|
10
|
+
import { useWorktree } from "../git/worktree-use.js";
|
|
11
|
+
import { ffMainToOrWhy } from "../merge/merge-git.js";
|
|
12
|
+
import { revertCommitIn } from "../operator/revert.js";
|
|
13
|
+
import { mergeLockDir, mergeWorktreePath } from "../paths.js";
|
|
14
|
+
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
15
|
+
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
16
|
+
import { errorMessage } from "../text/text.js";
|
|
17
|
+
import { shortSha } from "../text/format.js";
|
|
18
|
+
const REVERT_PREFIXES = ["tumwater(operator): Revert", "tumwater(tumwater): Revert"]; // vocabulary-ok: commit prefixes are machine strings
|
|
19
|
+
/** Why the culprit is not reverted automatically, or null when it should be: no runner (an
|
|
20
|
+
* operator or hand commit), a director's merge, a revert itself, or a commit whose revert
|
|
21
|
+
* already merged. */
|
|
22
|
+
export async function revertSkipReason(root, mainBranch, c) {
|
|
23
|
+
if (c.runner === undefined)
|
|
24
|
+
return "no role runner merged it";
|
|
25
|
+
if (baseRoleOf(c.runner) === DIRECTOR_ROLE)
|
|
26
|
+
return "the director merged it";
|
|
27
|
+
const subject = ((await gitTry(root, "log", "-1", "--format=%s", c.commit)) ?? "").trim();
|
|
28
|
+
if (REVERT_PREFIXES.some((p) => subject.startsWith(p)))
|
|
29
|
+
return "it is itself a revert";
|
|
30
|
+
const merged = (await gitTry(root, "log", mainBranch, "--format=%s", "--fixed-strings", `--grep=Revert ${shortSha(c.commit)}`)) ?? "";
|
|
31
|
+
if (merged.split("\n").some((s) => REVERT_PREFIXES.some((p) => s === `${p} ${shortSha(c.commit)}`)))
|
|
32
|
+
return "a revert of it already merged";
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/** The main watch's culprit hand-off: starts the automatic revert, or falls back to bugfix's heal
|
|
36
|
+
* prompt when the revert is skipped. Never rejects, because its caller discards the promise and an
|
|
37
|
+
* unhandled rejection would end the orchestrator: a heal prompt that cannot be queued is warned. */
|
|
38
|
+
export async function routeMainCulprit(root, mainBranch, c, startRevert) {
|
|
39
|
+
const skip = await revertSkipReason(root, mainBranch, c);
|
|
40
|
+
if (skip === null) {
|
|
41
|
+
startRevert(c);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
queueCulpritHeal(root, c.tip, { culprit: c.commit, failingTests: c.failingTests });
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `not reverting ${shortSha(c.commit)} (${skip}) and could not queue the heal prompt for bugfix: ${errorMessage(err)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Run one revert task on main's current tip in the shared `_merge` checkout: revert the culprit,
|
|
52
|
+
* run `check.gateCommand` on the result when one is set (red aborts), and fast-forward main under
|
|
53
|
+
* the merge lock. Logs `main_revert` on success and calls `onReverted` (the hand-back to a role);
|
|
54
|
+
* every failure queues the heal prompt instead. Never throws: a hand-off that cannot be queued is
|
|
55
|
+
* warned, not thrown. */
|
|
56
|
+
export async function runMainRevert(root, mainBranch, config, c, onReverted, onFailed) {
|
|
57
|
+
/** Hand the failure to a role and return the warning's suffix saying truthfully what happened. */
|
|
58
|
+
const heal = (reason, outputTail) => {
|
|
59
|
+
const toReviewer = c.postReview && onFailed;
|
|
60
|
+
try {
|
|
61
|
+
if (toReviewer)
|
|
62
|
+
onFailed(c, reason);
|
|
63
|
+
else
|
|
64
|
+
queueCulpritHeal(root, c.tip, { culprit: c.commit, failingTests: c.failingTests, outputTail });
|
|
65
|
+
return toReviewer ? "sent the reviewer's reasons to its role" : "queued the heal prompt for bugfix";
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return toReviewer
|
|
69
|
+
? `could not send the reviewer's reasons to its role: ${errorMessage(err)}`
|
|
70
|
+
: `could not queue the heal prompt for bugfix: ${errorMessage(err)}`;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
const failure = await useWorktree(root, mergeWorktreePath(root), async () => {
|
|
75
|
+
const tip = await branchHead(root, mainBranch);
|
|
76
|
+
if (tip === null)
|
|
77
|
+
return { reason: `${mainBranch} is unreadable` };
|
|
78
|
+
const wt = await ensureDetachedWorktree(root, mergeWorktreePath(root), tip);
|
|
79
|
+
const subject = ((await gitTry(wt, "log", "-1", "--format=%s", c.commit)) ?? "").trim();
|
|
80
|
+
const tests = c.failingTests.length > 0 ? `\n\nFailing tests: ${c.failingTests.join("; ")}` : "";
|
|
81
|
+
const why = c.postReview
|
|
82
|
+
? `which a post-merge review rejected.\n\nReasons: ${c.postReview.reasons.join("; ")}`
|
|
83
|
+
: `which turned main red.${tests}`;
|
|
84
|
+
const message = `${stampedSubject("tumwater", `Revert ${shortSha(c.commit)}`)}\n\nReverts ${c.commit} (${subject}), ${why}`;
|
|
85
|
+
const reverted = await revertCommitIn(wt, c.commit, message);
|
|
86
|
+
if (!reverted.ok)
|
|
87
|
+
return { reason: reverted.error };
|
|
88
|
+
if (gateCommandOf(config) !== undefined) {
|
|
89
|
+
const gate = await runScopedBuildCheck(root, TUMWATER_SOURCE, "gate", wt, config);
|
|
90
|
+
if (gate && gate.outcome.status !== "passed")
|
|
91
|
+
return { reason: `the gate command was ${gate.outcome.status} on the revert`, tail: gate.outcome.outputTail };
|
|
92
|
+
}
|
|
93
|
+
const ffFailure = await withLock(mergeLockDir(root), () => ffMainToOrWhy(root, reverted.sha, mainBranch));
|
|
94
|
+
if (ffFailure !== null)
|
|
95
|
+
return { reason: ffFailure };
|
|
96
|
+
// The culprit's patch-id (`C^...C`, src/git/git.ts patchId, -U0 so a re-landing onto a
|
|
97
|
+
// moved main keeps the id): the fast-merge guardrail G2 matches a later `merged`
|
|
98
|
+
// event against it to spot a change that came back unchanged (plans/fast-merge.md).
|
|
99
|
+
const culpritPatch = await patchId(wt, `${c.commit}^`, c.commit).catch(() => null);
|
|
100
|
+
logEventBestEffort(root, {
|
|
101
|
+
source: TUMWATER_SOURCE,
|
|
102
|
+
type: "main_revert",
|
|
103
|
+
commit: c.commit,
|
|
104
|
+
revert: reverted.sha,
|
|
105
|
+
runner: c.runner,
|
|
106
|
+
failingTests: c.failingTests,
|
|
107
|
+
...(culpritPatch ? { patchId: culpritPatch } : {}),
|
|
108
|
+
});
|
|
109
|
+
await onReverted?.(c).catch((err) => logEventBestEffort(root, {
|
|
110
|
+
source: TUMWATER_SOURCE,
|
|
111
|
+
type: "warning",
|
|
112
|
+
message: `could not hand ${shortSha(c.commit)} back to its role: ${errorMessage(err)}`,
|
|
113
|
+
}));
|
|
114
|
+
return null;
|
|
115
|
+
});
|
|
116
|
+
if (failure !== null) {
|
|
117
|
+
const suffix = heal(failure.reason, failure.tail);
|
|
118
|
+
logEventBestEffort(root, {
|
|
119
|
+
source: TUMWATER_SOURCE,
|
|
120
|
+
type: "warning",
|
|
121
|
+
message: `could not revert ${shortSha(c.commit)}: ${failure.reason} — ${suffix}`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const suffix = heal(errorMessage(err));
|
|
127
|
+
logEventBestEffort(root, {
|
|
128
|
+
source: TUMWATER_SOURCE,
|
|
129
|
+
type: "warning",
|
|
130
|
+
message: `revert of ${shortSha(c.commit)} failed: ${errorMessage(err)} — ${suffix}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { checkMainBaseline } from "./main-baseline.js";
|
|
2
|
+
import { findRedCulprit } from "./main-culprit.js";
|
|
3
|
+
import { baselineCheckLogger, withMainTipWorktree } from "./main-red.js";
|
|
4
|
+
import { CHECK_TIER } from "../concurrency/check-permit.js";
|
|
5
|
+
import { FAILING_TESTS_MAX } from "../build/build-check-report.js";
|
|
6
|
+
import { detectBuildCheck } from "../build/build-check-detect.js";
|
|
7
|
+
import { gitTry } from "../git/git-run.js";
|
|
8
|
+
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
9
|
+
import { errorMessage } from "../text/text.js";
|
|
10
|
+
import { fileQuarantineBugsOnMain } from "../backlog/backlog-archive.js";
|
|
11
|
+
import { loadQuarantine, markQuarantineFiled, onlyQuarantined, quarantinedTests, quarantineTests, releaseQuarantine, } from "../review/known-flakes.js";
|
|
12
|
+
import { shortSha } from "../text/format.js";
|
|
13
|
+
/** The failing tests of each root's newest watch verdict, keyed by root. */
|
|
14
|
+
const knownFailures = new Map();
|
|
15
|
+
/** Main's known failures: the failing tests of the watch's newest verdict — main's tip, or the
|
|
16
|
+
* nearest tip it checked while the newest head waits — and every quarantined flaky test
|
|
17
|
+
* (review/known-flakes.ts). Empty when the verdict is green, red with no parsed test, or none has
|
|
18
|
+
* been recorded yet in this process, and nothing is quarantined. */
|
|
19
|
+
export function knownMainFailures(root) {
|
|
20
|
+
return [...new Set([...(knownFailures.get(root) ?? []), ...quarantinedTests(root)])];
|
|
21
|
+
}
|
|
22
|
+
/** The failing tests of a failed check when every one of them is a known main failure; null when
|
|
23
|
+
* the output names no test, any test is outside the known set, or none is known. A list at
|
|
24
|
+
* FAILING_TESTS_MAX may have been cut short, so it never counts as complete. */
|
|
25
|
+
export function onlyKnownMainFailures(root, failingTests) {
|
|
26
|
+
if (!failingTests || failingTests.length === 0 || failingTests.length >= FAILING_TESTS_MAX)
|
|
27
|
+
return null;
|
|
28
|
+
const known = new Set(knownMainFailures(root));
|
|
29
|
+
return failingTests.every((t) => known.has(t)) ? [...failingTests] : null;
|
|
30
|
+
}
|
|
31
|
+
export function createMainWatch(root, mainBranch, onCulprit) {
|
|
32
|
+
let lastWatched;
|
|
33
|
+
let inFlight = null;
|
|
34
|
+
let housekeeping = null;
|
|
35
|
+
let lastFilingWarning = "";
|
|
36
|
+
/** Release quarantined tests whose bug left `## Open`, and file the bugs of newly quarantined
|
|
37
|
+
* ones on main. A filing the primary checkout cannot take warns (once per reason) and is
|
|
38
|
+
* retried on a later poll. Never throws. */
|
|
39
|
+
async function settleQuarantine() {
|
|
40
|
+
try {
|
|
41
|
+
const released = releaseQuarantine(root);
|
|
42
|
+
if (released.length > 0) {
|
|
43
|
+
logEventBestEffort(root, {
|
|
44
|
+
source: TUMWATER_SOURCE,
|
|
45
|
+
type: "warning",
|
|
46
|
+
message: `quarantine released ${released.join(", ")}: its BUGS.md entry is no longer open`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const pending = loadQuarantine(root).filter((e) => e.bug === "");
|
|
50
|
+
if (pending.length === 0)
|
|
51
|
+
return;
|
|
52
|
+
const outcome = await fileQuarantineBugsOnMain(root, pending);
|
|
53
|
+
if (outcome.status === "filed") {
|
|
54
|
+
markQuarantineFiled(root, outcome.bugs);
|
|
55
|
+
lastFilingWarning = "";
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (outcome.reason !== lastFilingWarning) {
|
|
59
|
+
lastFilingWarning = outcome.reason;
|
|
60
|
+
logEventBestEffort(root, {
|
|
61
|
+
source: TUMWATER_SOURCE,
|
|
62
|
+
type: "warning",
|
|
63
|
+
message: `quarantined flaky tests are not filed in BUGS.md yet: ${outcome.reason}`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
logEventBestEffort(root, {
|
|
69
|
+
source: TUMWATER_SOURCE,
|
|
70
|
+
type: "warning",
|
|
71
|
+
message: `quarantine housekeeping failed: ${errorMessage(err)}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function watchOnce(mainHead, config) {
|
|
76
|
+
if (!detectBuildCheck(root, config)) {
|
|
77
|
+
lastWatched = mainHead; // no declared check: nothing to watch until main moves
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await withMainTipWorktree(root, mainBranch, async (wt) => {
|
|
81
|
+
const started = Date.now();
|
|
82
|
+
let ran = false;
|
|
83
|
+
const onRun = baselineCheckLogger(root, TUMWATER_SOURCE);
|
|
84
|
+
const hook = (run) => {
|
|
85
|
+
ran = true;
|
|
86
|
+
onRun(run);
|
|
87
|
+
};
|
|
88
|
+
let check = await checkMainBaseline(wt, config, hook, false, false, undefined, false, CHECK_TIER.merge);
|
|
89
|
+
if (check.baseline?.status === "red") {
|
|
90
|
+
const first = check.baseline;
|
|
91
|
+
check = await checkMainBaseline(wt, config, hook, true, false, undefined, false, CHECK_TIER.merge);
|
|
92
|
+
// A red the same tree's confirming re-run turns green is a flake: quarantine its tests.
|
|
93
|
+
if (check.baseline?.status === "green")
|
|
94
|
+
quarantineTests(root, first.failingTests, (first.failingTests ?? []).join("\n"));
|
|
95
|
+
}
|
|
96
|
+
const baseline = check.baseline;
|
|
97
|
+
const previous = lastWatched;
|
|
98
|
+
// A skipped or sleep-spanned run made no verdict: the head still counts as watched, so the
|
|
99
|
+
// next poll does not start another run for it.
|
|
100
|
+
if (!baseline)
|
|
101
|
+
return void (lastWatched = await headOf(wt));
|
|
102
|
+
lastWatched = baseline.sha;
|
|
103
|
+
// A red naming only quarantined tests counts as green: no bisect, no redeploy hold.
|
|
104
|
+
const quarantined = baseline.status === "red" ? onlyQuarantined(root, baseline.failingTests) : null;
|
|
105
|
+
const red = baseline.status === "red" && !quarantined;
|
|
106
|
+
knownFailures.set(root, red ? (baseline.failingTests ?? []) : []);
|
|
107
|
+
const counted = previous === undefined
|
|
108
|
+
? 1
|
|
109
|
+
: Number(((await gitTry(wt, "rev-list", "--first-parent", "--count", `${previous}..${baseline.sha}`)) ?? "").trim());
|
|
110
|
+
logEventBestEffort(root, {
|
|
111
|
+
source: TUMWATER_SOURCE,
|
|
112
|
+
type: "main_check",
|
|
113
|
+
sha: baseline.sha,
|
|
114
|
+
status: red ? "failed" : "passed",
|
|
115
|
+
durationMs: ran ? Date.now() - started : 0,
|
|
116
|
+
covered: Number.isFinite(counted) && counted > 0 ? counted : 1,
|
|
117
|
+
...(red ? { failingTests: baseline.failingTests ?? [] } : {}),
|
|
118
|
+
...(quarantined ? { quarantined } : {}),
|
|
119
|
+
...(baseline.inheritedFrom !== undefined ? { inherited: true } : {}),
|
|
120
|
+
});
|
|
121
|
+
if (!red)
|
|
122
|
+
return;
|
|
123
|
+
// Once per red head: lastWatched already holds it, so a later poll does not search again.
|
|
124
|
+
const found = await findRedCulprit(root, wt, config, baseline);
|
|
125
|
+
if (found.kind === "culprit") {
|
|
126
|
+
logEventBestEffort(root, {
|
|
127
|
+
source: TUMWATER_SOURCE,
|
|
128
|
+
type: "main_culprit",
|
|
129
|
+
commit: found.commit,
|
|
130
|
+
...(found.runner !== undefined ? { runner: found.runner } : {}),
|
|
131
|
+
failingTests: found.failingTests,
|
|
132
|
+
green: found.green,
|
|
133
|
+
});
|
|
134
|
+
onCulprit?.({
|
|
135
|
+
commit: found.commit,
|
|
136
|
+
...(found.runner !== undefined ? { runner: found.runner } : {}),
|
|
137
|
+
failingTests: found.failingTests,
|
|
138
|
+
tip: baseline.sha,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
logEventBestEffort(root, {
|
|
143
|
+
source: TUMWATER_SOURCE,
|
|
144
|
+
type: "warning",
|
|
145
|
+
message: found.kind === "environment"
|
|
146
|
+
? `main's last green commit ${shortSha(found.green)} is red now too — not attributing`
|
|
147
|
+
: `main watch could not name the commit that turned main red: ${found.reason}`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
poll(mainHead, held, config) {
|
|
154
|
+
if (!held && !housekeeping) {
|
|
155
|
+
housekeeping = settleQuarantine().finally(() => {
|
|
156
|
+
housekeeping = null;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (held || mainHead === "" || inFlight || mainHead === lastWatched)
|
|
160
|
+
return;
|
|
161
|
+
inFlight = watchOnce(mainHead, config)
|
|
162
|
+
.catch((err) => {
|
|
163
|
+
// A failed watch (main unreadable, checkout refused) records nothing and is not
|
|
164
|
+
// retried until main moves, so a persistent failure warns once per head.
|
|
165
|
+
lastWatched = mainHead;
|
|
166
|
+
logEventBestEffort(root, {
|
|
167
|
+
source: TUMWATER_SOURCE,
|
|
168
|
+
type: "warning",
|
|
169
|
+
message: `main watch could not check main: ${errorMessage(err)}`,
|
|
170
|
+
});
|
|
171
|
+
})
|
|
172
|
+
.finally(() => {
|
|
173
|
+
inFlight = null;
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
idle: async () => {
|
|
177
|
+
while (inFlight || housekeeping)
|
|
178
|
+
await (inFlight ?? housekeeping);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function headOf(wt) {
|
|
183
|
+
return ((await gitTry(wt, "rev-parse", "HEAD")) ?? "").trim();
|
|
184
|
+
}
|
|
@@ -1,34 +1,32 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import {
|
|
3
|
+
import { BRIEF_TEXT_MAX_CHARS, PROMPT_END, PROMPT_START, briefPath, replaceBriefText } from "./brief.js";
|
|
5
4
|
import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
|
|
6
|
-
import {
|
|
5
|
+
import { withPrimaryWorktreeLock } from "../concurrency/lock.js";
|
|
7
6
|
import { writeTextAtomic } from "../files/files.js";
|
|
8
7
|
import { stampedSubject } from "../git/commit-message.js";
|
|
9
|
-
import { primaryWorktreeLockPath } from "../paths.js";
|
|
10
8
|
import { PREFILL_REASON } from "../text/phrases.js";
|
|
11
9
|
import { tooLongMessage } from "../text/text.js";
|
|
12
10
|
const NO_BRIEF = "no project brief with a prompt block found — run `tumwater init` first";
|
|
13
|
-
/** Replace the
|
|
14
|
-
*
|
|
11
|
+
/** Replace the brief text with `text` under the checks init applies (non-empty, at most
|
|
12
|
+
* BRIEF_TEXT_MAX_CHARS, no prompt marker), write it under the primary-worktree lock, and
|
|
15
13
|
* commit that one file on main. The one writer behind `tumwater brief --file` and the
|
|
16
14
|
* dashboard's Settings card, so the two cannot drift. */
|
|
17
|
-
export async function
|
|
15
|
+
export async function editBriefText(root, text) {
|
|
18
16
|
const prompt = text.trim();
|
|
19
17
|
if (prompt === "")
|
|
20
18
|
return { ok: false, error: "the new brief is empty" };
|
|
21
|
-
if (prompt.length >
|
|
22
|
-
return { ok: false, error: tooLongMessage("the new brief", prompt.length,
|
|
19
|
+
if (prompt.length > BRIEF_TEXT_MAX_CHARS)
|
|
20
|
+
return { ok: false, error: tooLongMessage("the new brief", prompt.length, BRIEF_TEXT_MAX_CHARS, PREFILL_REASON) };
|
|
23
21
|
if (prompt.includes(PROMPT_START) || prompt.includes(PROMPT_END))
|
|
24
22
|
return { ok: false, error: "the new brief contains a tumwater:prompt marker, which would break the block for every reader" };
|
|
25
23
|
const file = briefPath(root);
|
|
26
24
|
if (file === null)
|
|
27
25
|
return { ok: false, error: NO_BRIEF };
|
|
28
26
|
const name = path.basename(file);
|
|
29
|
-
const changed =
|
|
27
|
+
const changed = withPrimaryWorktreeLock(root, () => {
|
|
30
28
|
const current = fs.readFileSync(file, "utf8");
|
|
31
|
-
const next =
|
|
29
|
+
const next = replaceBriefText(current, prompt);
|
|
32
30
|
if (next === null)
|
|
33
31
|
return `${name} no longer carries a prompt block`;
|
|
34
32
|
if (next === current)
|