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 { runBuildCheck, } from "../build/build-check.js";
|
|
3
2
|
import { SLEEP_SPAN_TOLERANCE_MS, runWithCleanRetry } from "../build/build-check-events.js";
|
|
4
3
|
import { sampleSleepClock } from "../build/host-sleep.js";
|
|
@@ -10,7 +9,7 @@ import { treeDiffFiles } from "../git/git-diff.js";
|
|
|
10
9
|
import { isExemptDiff } from "../review/exemptions.js";
|
|
11
10
|
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
12
11
|
import { isJsonObject } from "../files/json-object.js";
|
|
13
|
-
import { baselineVerdictsPath } from "../paths.js";
|
|
12
|
+
import { baselineVerdictsPath, mirrorWorktreePath } from "../paths.js";
|
|
14
13
|
import { memoSetLru } from "../collections.js";
|
|
15
14
|
/** Team-shared verdict cache, keyed by main SHA. In-memory only: after a restart the cache is cold
|
|
16
15
|
* and one re-check per red SHA happens — cheap and deterministic, mirroring the budget gate's
|
|
@@ -50,14 +49,18 @@ function rememberBaseline(sha, baseline) {
|
|
|
50
49
|
memoSetLru(baselineCache, sha, baseline, BASELINE_CACHE_MAX);
|
|
51
50
|
saveGreens();
|
|
52
51
|
}
|
|
53
|
-
/**
|
|
52
|
+
/** How many reds the verdict file keeps (the newest). */
|
|
53
|
+
const SAVED_REDS_MAX = 64;
|
|
54
|
+
/** The file the cache's verdicts are mirrored to (persistBaselineVerdicts), or undefined while no
|
|
54
55
|
* team process has asked for it: the CLI and the tests keep the cache in memory only. */
|
|
55
56
|
let greensFile;
|
|
56
|
-
/** Mirror the
|
|
57
|
-
* used to empty the cache, and the first
|
|
57
|
+
/** Mirror the verdicts to disk, so the team's next generation starts with them. A restart
|
|
58
|
+
* used to empty the cache, and the first run after every restart re-ran the suite on a main the
|
|
58
59
|
* previous generation had already verified (41 of 319 baseline runs over 2026-10-02..12 started
|
|
59
|
-
* within 10 minutes of an orchestrator start).
|
|
60
|
-
* immutable tree
|
|
60
|
+
* within 10 minutes of an orchestrator start). A green is a fact about an
|
|
61
|
+
* immutable tree. A red is provisional (shouldRerunRed), so it is saved only as the main watch's
|
|
62
|
+
* record — sha, script and failing tests, the newest SAVED_REDS_MAX — and a loaded red is
|
|
63
|
+
* re-run by the next worktree that consults it, like any red seen in one worktree. Best
|
|
61
64
|
* effort: a failed write costs at most a re-run after the next restart. */
|
|
62
65
|
function saveGreens() {
|
|
63
66
|
if (greensFile === undefined)
|
|
@@ -65,15 +68,19 @@ function saveGreens() {
|
|
|
65
68
|
const greens = [...baselineCache.values()]
|
|
66
69
|
.filter((b) => b.status === "green")
|
|
67
70
|
.map((b) => (b.inheritedFrom === undefined ? { sha: b.sha } : { sha: b.sha, inheritedFrom: b.inheritedFrom }));
|
|
71
|
+
const reds = [...baselineCache.values()]
|
|
72
|
+
.filter((b) => b.status === "red")
|
|
73
|
+
.slice(-SAVED_REDS_MAX)
|
|
74
|
+
.map((b) => ({ sha: b.sha, script: b.script, failingTests: b.failingTests ?? [] }));
|
|
68
75
|
try {
|
|
69
|
-
writeJsonAtomic(greensFile, { greens });
|
|
76
|
+
writeJsonAtomic(greensFile, { greens, reds });
|
|
70
77
|
}
|
|
71
78
|
catch {
|
|
72
79
|
// A read-only or full disk: the in-memory cache still answers this generation.
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
|
-
/** Load the
|
|
76
|
-
* there (saveGreens). Called once at team startup (cli/cli-
|
|
82
|
+
/** Load the verdicts a previous generation saved under `root` and keep mirroring them
|
|
83
|
+
* there (saveGreens). Called once at team startup (cli/cli-start.ts). An entry the cache already
|
|
77
84
|
* holds is left as it is, and a malformed file or entry reads as no data. */
|
|
78
85
|
export function persistBaselineVerdicts(root) {
|
|
79
86
|
const file = baselineVerdictsPath(root);
|
|
@@ -88,9 +95,21 @@ export function persistBaselineVerdicts(root) {
|
|
|
88
95
|
const from = typeof entry.inheritedFrom === "string" ? { inheritedFrom: entry.inheritedFrom } : {};
|
|
89
96
|
rememberBaseline(entry.sha, { status: "green", sha: entry.sha, ...from });
|
|
90
97
|
}
|
|
98
|
+
const reds = Array.isArray(stored?.reds) ? stored.reds : [];
|
|
99
|
+
for (const entry of reds) {
|
|
100
|
+
if (!isJsonObject(entry) || typeof entry.sha !== "string" || !/^[0-9a-f]{40,64}$/.test(entry.sha))
|
|
101
|
+
continue;
|
|
102
|
+
if (baselineCache.has(entry.sha))
|
|
103
|
+
continue;
|
|
104
|
+
const failingTests = Array.isArray(entry.failingTests)
|
|
105
|
+
? entry.failingTests.filter((t) => typeof t === "string")
|
|
106
|
+
: [];
|
|
107
|
+
const script = typeof entry.script === "string" ? { script: entry.script } : {};
|
|
108
|
+
rememberBaseline(entry.sha, { status: "red", sha: entry.sha, ...script, failingTests });
|
|
109
|
+
}
|
|
91
110
|
greensFile = file;
|
|
92
111
|
}
|
|
93
|
-
/** In-flight dedup: concurrent
|
|
112
|
+
/** In-flight dedup: concurrent runs on the same not-yet-cached SHA (a fresh main move wakes
|
|
94
113
|
* every blocked role at once) share one check run instead of racing N npm invocations. Keyed by
|
|
95
114
|
* SHA for ordinary checks; a re-verification adds its worktree, because joining another
|
|
96
115
|
* worktree's run would observe the wrong environment — the one thing it exists to re-test. */
|
|
@@ -98,7 +117,7 @@ const baselineInFlight = new Map();
|
|
|
98
117
|
/** Should a cached RED be re-run here instead of trusted? A red says as much about the
|
|
99
118
|
* environment the check ran in as about the tree — a worktree mid-install, or a machine under
|
|
100
119
|
* the load the team deliberately creates starving a timing-sensitive test (BUGS.md's
|
|
101
|
-
* load-sensitive tests entry). The role
|
|
120
|
+
* load-sensitive tests entry). The role runners consult this team-shared cache and, before
|
|
102
121
|
* 2026-09-18, trusted a red forever: one environmental red blocked every code role until main
|
|
103
122
|
* moved, and main could not move, because blocking the code roles is exactly what stops it —
|
|
104
123
|
* a deadlock broken only by restarting the orchestrator, since the cache is per-process.
|
|
@@ -126,9 +145,9 @@ function shouldRerunRed(cached, wt, reverifyRed) {
|
|
|
126
145
|
* scope-`batch` check ran green on exactly it (a skipped check seeds nothing, and neither does a
|
|
127
146
|
* merge_blocked stack or a doc-only re-stack that merged without a check of its own). Seeding
|
|
128
147
|
* here means that once the merge completes — main now points at this very SHA — the next fresh
|
|
129
|
-
*
|
|
148
|
+
* run's checkMainBaseline is a cache hit instead of re-running the full suite on an
|
|
130
149
|
* already-verified tree: for tumwater itself that saves one redundant `npm test` (~1 min) per
|
|
131
|
-
* merged code
|
|
150
|
+
* merged code run, plus every other role waking on "main moved" stalling behind that in-flight
|
|
132
151
|
* run. Never seed a pre-rebase head: whenever main moved under the review, that SHA never
|
|
133
152
|
* becomes main and the entry would silently miss (BUGS.md 2026-09-08). Safe because git trees
|
|
134
153
|
* are immutable — a SHA's content cannot change under a cached verdict, the same staleness
|
|
@@ -197,8 +216,55 @@ function exemptPathsOf(config) {
|
|
|
197
216
|
export function cachedBaselineVerdict(sha) {
|
|
198
217
|
return baselineCache.get(sha)?.status;
|
|
199
218
|
}
|
|
219
|
+
/** The main watch's newest verdict for `sha` (default: `wt`'s HEAD) or its nearest first-parent
|
|
220
|
+
* ancestor the cache holds one for — looking at most INHERIT_WALK_MAX commits down — WITHOUT
|
|
221
|
+
* running anything; null when there is none. With `check.mergeCommand` set no merge seeds the
|
|
222
|
+
* cache, so every verdict in it is a full check the watch (main-watch.ts) or another consumer
|
|
223
|
+
* ran, or a doc-only inheritance of one, and the red-main reads (main-red.ts) answer from here
|
|
224
|
+
* rather than start a full check. Never throws: a git failure reads as no verdict. */
|
|
225
|
+
export async function watchedBaseline(wt, sha) {
|
|
226
|
+
const tip = sha ?? (await refSha(wt, "HEAD"));
|
|
227
|
+
if (!tip)
|
|
228
|
+
return null;
|
|
229
|
+
const own = baselineCache.get(tip);
|
|
230
|
+
if (own)
|
|
231
|
+
return own;
|
|
232
|
+
const ancestors = gitLines(await gitTry(wt, "rev-list", "--first-parent", `--max-count=${INHERIT_WALK_MAX}`, `${tip}^`));
|
|
233
|
+
for (const ancestor of ancestors) {
|
|
234
|
+
const verdict = baselineCache.get(ancestor);
|
|
235
|
+
if (verdict)
|
|
236
|
+
return verdict;
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
/** How many first-parent commits below main's tip redeployTarget looks at. */
|
|
241
|
+
const REDEPLOY_WALK_MAX = 64;
|
|
242
|
+
/** The commit self-redeploy should run: main's newest first-parent commit with a green full-check
|
|
243
|
+
* verdict in the cache (observed or inherited) that is newer than the running build `buildSha`,
|
|
244
|
+
* looking at most REDEPLOY_WALK_MAX commits down from `tip`. A tip the cache has no verdict for
|
|
245
|
+
* is the target as is — the redeployer checks it. A tip whose cached red was observed in the
|
|
246
|
+
* redeployer's mirror worktree (mirrorWorktreePath of `wt`) falls back to the newest older green
|
|
247
|
+
* above the build. Any other red may be another worktree's environmental one, which the redeployer
|
|
248
|
+
* re-verifies; a build that is not a first-parent ancestor of `tip`, or no green, also leaves the
|
|
249
|
+
* tip the target, and the redeployer blocks that commit alone. The cache holds only full-check
|
|
250
|
+
* verdicts, so a cheaper command's green never makes a commit deployable.
|
|
251
|
+
* Never throws: a git failure leaves the tip as the target. */
|
|
252
|
+
export async function redeployTarget(wt, tip, buildSha) {
|
|
253
|
+
const red = baselineCache.get(tip);
|
|
254
|
+
if (tip === "" || red?.status !== "red" || !(red.redFrom ?? []).includes(mirrorWorktreePath(wt)))
|
|
255
|
+
return tip;
|
|
256
|
+
const walk = gitLines(await gitTry(wt, "rev-list", "--first-parent", `--max-count=${REDEPLOY_WALK_MAX}`, tip));
|
|
257
|
+
const buildAt = walk.indexOf(buildSha);
|
|
258
|
+
if (buildAt === -1)
|
|
259
|
+
return tip;
|
|
260
|
+
for (const sha of walk.slice(0, buildAt)) {
|
|
261
|
+
if (baselineCache.get(sha)?.status === "green")
|
|
262
|
+
return sha;
|
|
263
|
+
}
|
|
264
|
+
return tip;
|
|
265
|
+
}
|
|
200
266
|
/** Verify main's own build/test suite at `wt`'s HEAD — which must be pristine main (the caller
|
|
201
|
-
* is the fresh-
|
|
267
|
+
* is the fresh-run path right after resetWorktreeToMain; a dirty or ahead worktree would
|
|
202
268
|
* measure the wrong thing). Cache hit returns immediately; on miss runs detectBuildCheck +
|
|
203
269
|
* runBuildCheck once per SHA (in-flight deduped) and caches green/red. Never throws: git,
|
|
204
270
|
* detection, and execution failures all resolve to "nothing blocks authoring". */
|
|
@@ -215,7 +281,7 @@ onRun,
|
|
|
215
281
|
* circuits — see baselineCache). For the caller whose false block is expensive enough to pay
|
|
216
282
|
* one extra run: the redeploy gate, where trusting another worktree's environmental red
|
|
217
283
|
* strands the whole team on a stale build until main moves. A green from the re-run promotes
|
|
218
|
-
* the SHA team-wide, which unblocks the role
|
|
284
|
+
* the SHA team-wide, which unblocks the role runners too. */
|
|
219
285
|
reverifyRed = false,
|
|
220
286
|
/** Ignore the cache entirely and run the suite here, even over a GREEN verdict. For the one
|
|
221
287
|
* caller that cannot trust a green (BUGS.md 2026-10-06: the review gate re-attributing a
|
|
@@ -233,7 +299,9 @@ sampleSleep = sampleSleepClock,
|
|
|
233
299
|
* doc edit main gained since its last observed green can break a test that reads the docs,
|
|
234
300
|
* so only a real run may answer. A green from that run upgrades the SHA's verdict, and a red
|
|
235
301
|
* replaces the inherited green for every consumer. */
|
|
236
|
-
observedOnly = false
|
|
302
|
+
observedOnly = false,
|
|
303
|
+
/** The check permit tier the run queues at; the main watch asks for the merge tier. */
|
|
304
|
+
tier = CHECK_TIER.other) {
|
|
237
305
|
const sha = await refSha(wt, "HEAD");
|
|
238
306
|
if (!sha)
|
|
239
307
|
return { baseline: null }; // No HEAD (unborn branch) — nothing to key on.
|
|
@@ -268,11 +336,11 @@ observedOnly = false) {
|
|
|
268
336
|
// withCheckPermit): this runs the suite directly, so it must count against the cap too.
|
|
269
337
|
// Only the one run per key holds a permit; deduped waiters above await `pending` holding
|
|
270
338
|
// none.
|
|
271
|
-
const { outcome, durationMs } = await withCheckPermit(config,
|
|
339
|
+
const { outcome, durationMs } = await withCheckPermit(config, tier, () => runWithCleanRetry(() => runBuildCheck(wt, check, undefined, undefined, sampleSleep),
|
|
272
340
|
// Weather (a late deadline, a foreign kill, a host sleep) made no verdict about the
|
|
273
341
|
// tree: the first attempt is priced as its own run, and the retry's outcome stands —
|
|
274
342
|
// the same rule runScopedBuildCheck applies at every other scope.
|
|
275
|
-
(first, dur) => onRun?.({ outcome: first, durationMs: dur })));
|
|
343
|
+
(first, dur) => onRun?.({ outcome: first, durationMs: dur })), undefined, "main baseline");
|
|
276
344
|
onRun?.({ outcome, durationMs });
|
|
277
345
|
if (outcome.status === "skipped") {
|
|
278
346
|
// Environmental (timeout/no-npm), after the clean retry above for weather shapes:
|
|
@@ -296,6 +364,7 @@ observedOnly = false) {
|
|
|
296
364
|
sha,
|
|
297
365
|
script: outcome.script,
|
|
298
366
|
outputTail: outcome.outputTail,
|
|
367
|
+
failingTests: outcome.failingTests ?? [],
|
|
299
368
|
redFrom: priorRedFrom.includes(wt) ? priorRedFrom : [...priorRedFrom, wt],
|
|
300
369
|
};
|
|
301
370
|
// A green is always recorded (promoting a provisional red); a red never overwrites an
|
|
@@ -337,7 +406,7 @@ export async function mainIsGreen(mirrorWt,
|
|
|
337
406
|
* maxConcurrentChecks sizes the check permit this run takes. */
|
|
338
407
|
config,
|
|
339
408
|
/** Hook for the build_check event — this run is a minute of the team's time and belongs in
|
|
340
|
-
* the feed like the role
|
|
409
|
+
* the feed like the role runners' own baseline checks. */
|
|
341
410
|
onRun) {
|
|
342
411
|
const check = await checkMainBaseline(mirrorWt, config, onRun, true);
|
|
343
412
|
return check.baseline ? check.baseline.status === "green" : true;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { cachedBaselineVerdict, checkMainBaseline } from "./main-baseline.js";
|
|
2
|
+
import { baselineCheckLogger } from "./main-red.js";
|
|
3
|
+
import { CHECK_TIER } from "../concurrency/check-permit.js";
|
|
4
|
+
import { gitTry } from "../git/git-run.js";
|
|
5
|
+
import { treeDiffFiles } from "../git/git-diff.js";
|
|
6
|
+
import { ensureDetachedWorktree } from "../git/worktree.js";
|
|
7
|
+
import { gateMainWorktreePath } from "../paths.js";
|
|
8
|
+
import { isExemptDiff } from "../review/exemptions.js";
|
|
9
|
+
import { onlyQuarantined } from "../review/known-flakes.js";
|
|
10
|
+
import { readEvents } from "../events/event-read.js";
|
|
11
|
+
import { TUMWATER_SOURCE } from "../events/events.js";
|
|
12
|
+
import { shortSha } from "../text/format.js";
|
|
13
|
+
/** How many first-parent commits below the red tip the search for a last green looks at. */
|
|
14
|
+
const CULPRIT_SEARCH_MAX = 50;
|
|
15
|
+
/** How many recent events the owner lookup reads; an older `merged` event counts as none. */
|
|
16
|
+
const OWNER_EVENT_TAIL = 2000;
|
|
17
|
+
/** Name the first-parent commit that turned main red at `red.sha` (plans/fast-merge.md, Lever 2,
|
|
18
|
+
* part 4a). Runs inside the main watch's `_gate-main` checkout `wt`, re-pointing it per candidate
|
|
19
|
+
* and back at the tip before returning. Finds the newest first-parent commit with a cached green
|
|
20
|
+
* verdict, re-checks it fresh (a green that is red now means the environment broke, not a
|
|
21
|
+
* change), then bisects the commits since it — skipping doc-only ones — for the first red. A
|
|
22
|
+
* search that cannot name a commit says why instead of guessing. */
|
|
23
|
+
export async function findRedCulprit(root, wt, config, red) {
|
|
24
|
+
const tip = red.sha;
|
|
25
|
+
const onRun = baselineCheckLogger(root, TUMWATER_SOURCE);
|
|
26
|
+
const mirror = gateMainWorktreePath(root);
|
|
27
|
+
const at = async (sha) => {
|
|
28
|
+
await ensureDetachedWorktree(root, mirror, sha);
|
|
29
|
+
};
|
|
30
|
+
const check = async (sha, reverifyRed, forceFresh, observedOnly) => {
|
|
31
|
+
await at(sha);
|
|
32
|
+
return (await checkMainBaseline(wt, config, onRun, reverifyRed, forceFresh, undefined, observedOnly, CHECK_TIER.merge))
|
|
33
|
+
.baseline;
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
const walk = ((await gitTry(wt, "rev-list", "--first-parent", `--max-count=${CULPRIT_SEARCH_MAX + 1}`, tip)) ?? "")
|
|
37
|
+
.split("\n")
|
|
38
|
+
.filter((s) => s !== "")
|
|
39
|
+
.slice(1);
|
|
40
|
+
const green = walk.find((sha) => cachedBaselineVerdict(sha) === "green");
|
|
41
|
+
if (!green) {
|
|
42
|
+
return { kind: "unattributed", reason: `no green commit within ${CULPRIT_SEARCH_MAX} first-parent commits` };
|
|
43
|
+
}
|
|
44
|
+
const recheck = await check(green, false, true, false);
|
|
45
|
+
if (!recheck)
|
|
46
|
+
return { kind: "unattributed", reason: `the check made no verdict at ${shortSha(green)}` };
|
|
47
|
+
if (recheck.status === "red")
|
|
48
|
+
return { kind: "environment", green };
|
|
49
|
+
const exempt = config.review.exemptPaths;
|
|
50
|
+
const since = ((await gitTry(wt, "rev-list", "--first-parent", "--reverse", `${green}..${tip}`)) ?? "")
|
|
51
|
+
.split("\n")
|
|
52
|
+
.filter((s) => s !== "");
|
|
53
|
+
const candidates = [];
|
|
54
|
+
for (const sha of since) {
|
|
55
|
+
const files = await treeDiffFiles(wt, `${sha}^`, sha);
|
|
56
|
+
if (files === null || !isExemptDiff(files, exempt))
|
|
57
|
+
candidates.push(sha);
|
|
58
|
+
}
|
|
59
|
+
if (candidates.length === 0)
|
|
60
|
+
return { kind: "unattributed", reason: "no non-doc commit since the last green" };
|
|
61
|
+
// The last candidate is taken as red unrun: it is the tip, or only doc commits follow it.
|
|
62
|
+
let lo = 0;
|
|
63
|
+
let hi = candidates.length - 1;
|
|
64
|
+
while (lo < hi) {
|
|
65
|
+
const mid = (lo + hi) >> 1;
|
|
66
|
+
const verdict = await check(candidates[mid], false, false, true);
|
|
67
|
+
if (!verdict)
|
|
68
|
+
return { kind: "unattributed", reason: `the check made no verdict at ${shortSha(candidates[mid])}` };
|
|
69
|
+
if (verdict.status === "red" && !onlyQuarantined(root, verdict.failingTests))
|
|
70
|
+
hi = mid;
|
|
71
|
+
else
|
|
72
|
+
lo = mid + 1;
|
|
73
|
+
}
|
|
74
|
+
const commit = candidates[lo];
|
|
75
|
+
const confirmed = await check(commit, true, false, true);
|
|
76
|
+
if (confirmed?.status !== "red" || onlyQuarantined(root, confirmed.failingTests)) {
|
|
77
|
+
return { kind: "unattributed", reason: `red did not reproduce at ${shortSha(commit)}` };
|
|
78
|
+
}
|
|
79
|
+
const failingTests = red.failingTests ?? []; // G was just verified green, so it carries none to subtract
|
|
80
|
+
const merged = readEvents(root, OWNER_EVENT_TAIL)
|
|
81
|
+
.reverse()
|
|
82
|
+
.find((ev) => ev.type === "merged" && ev.commit === commit);
|
|
83
|
+
return {
|
|
84
|
+
kind: "culprit",
|
|
85
|
+
commit,
|
|
86
|
+
...(merged ? { runner: merged.source } : {}),
|
|
87
|
+
failingTests,
|
|
88
|
+
green,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await at(tip);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/** Post-merge machinery that can fail quietly while merges go on — surfaced as warnings so an
|
|
2
|
+
* operator or the selfcheck role sees them (plans/fast-merge.md "Guardrails"). Pure and
|
|
3
|
+
* synchronous over the event log and quarantine.json, so every surface (doctor's rows and the
|
|
4
|
+
* selfcheck digest) words them the same. Read-only: it never writes an event, state or file.
|
|
5
|
+
*
|
|
6
|
+
* G1 lives on the `main_check` / `main_red_hold` event trail (src/baseline/main-watch.ts,
|
|
7
|
+
* src/baseline/main-red.ts): only `main_check` events that actually ran the check
|
|
8
|
+
* (`durationMs` > 0; a cached or inherited verdict is ignored) and `main_red_hold` events
|
|
9
|
+
* count, and a `main_check` that passed (a red naming only quarantined tests is recorded
|
|
10
|
+
* passed) ends a red. G3 reads the quarantine list (src/review/known-flakes.ts) directly.
|
|
11
|
+
* Both take an injected `now` so tests never read the clock. */
|
|
12
|
+
import { readEventsSince } from "../events/event-window.js";
|
|
13
|
+
import { loadQuarantine } from "../review/known-flakes.js";
|
|
14
|
+
import { DAY_MS, clockHm, humanAgo } from "../text/datetime.js";
|
|
15
|
+
import { shortSha } from "../text/format.js";
|
|
16
|
+
import { squash } from "../text/text.js";
|
|
17
|
+
/** How long a red main must last before the doctor and the selfcheck digest warn (G1). */
|
|
18
|
+
export const RED_MAIN_WARN_MS = 30 * 60_000;
|
|
19
|
+
/** True when the event is a `main_check` whose verdict actually ran the full check (G1's
|
|
20
|
+
* `relevant` rule): `durationMs` > 0, so a cached or inherited verdict (durationMs 0) is
|
|
21
|
+
* ignored. Shared by the report's main-red walk (src/report/report-main-red.ts) and
|
|
22
|
+
* `redMainWarning` below, so the two cannot disagree about whether a check ran. */
|
|
23
|
+
function ranFullMainCheck(ev) {
|
|
24
|
+
return ev.type === "main_check" && typeof ev.durationMs === "number" && ev.durationMs > 0;
|
|
25
|
+
}
|
|
26
|
+
/** True when `ev` opens or extends a red spell on main (G1's rule): a `main_red_hold`, or a
|
|
27
|
+
* `main_check` that ran the full check and failed. The report's per-day main-red walk
|
|
28
|
+
* (src/report/report-main-red.ts) gates its spell opener on this, so it and G1 share the one
|
|
29
|
+
* rule for when main is red. */
|
|
30
|
+
export function isRedMain(ev) {
|
|
31
|
+
return ev.type === "main_red_hold" || (ranFullMainCheck(ev) && ev.status === "failed");
|
|
32
|
+
}
|
|
33
|
+
/** True when `ev` is a `main_check` that ran the full check and passed — the event that ends a
|
|
34
|
+
* red spell (G1's rule: a red naming only quarantined tests is recorded passed). Shared with
|
|
35
|
+
* the report's main-red walk (src/report/report-main-red.ts), so a pass closes a spell the
|
|
36
|
+
* same way G1 ends its red. */
|
|
37
|
+
export function isPassingMainCheck(ev) {
|
|
38
|
+
return ranFullMainCheck(ev) && ev.status === "passed";
|
|
39
|
+
}
|
|
40
|
+
/** A quarantined test's `firstSeen` age past which the doctor and digest warn it is being
|
|
41
|
+
* hidden (G3). */
|
|
42
|
+
export const QUARANTINE_AGE_MS = 3 * DAY_MS;
|
|
43
|
+
/** A quarantine list longer than this many tests warns on size (G3). */
|
|
44
|
+
export const QUARANTINE_WARN_COUNT = 10;
|
|
45
|
+
/** How many of the oldest quarantined tests a warning names before folding the rest into a
|
|
46
|
+
* remainder count. */
|
|
47
|
+
const QUARANTINE_DETAIL_MAX = 3;
|
|
48
|
+
/** G1 over `events`: warns when main has been red for more than RED_MAIN_WARN_MS, over the
|
|
49
|
+
* trailing 24 h of `events` from `now`. Only `main_check` events with `durationMs` > 0 and
|
|
50
|
+
* `main_red_hold` events count; a `main_check` with `durationMs` 0 (cached or inherited) is
|
|
51
|
+
* ignored; a pass (including one naming only quarantined tests, recorded passed) ends the red.
|
|
52
|
+
* The phrase names how long main has had no passing full check, the sha it has been red since,
|
|
53
|
+
* its clock time, and the newest red's first failing test when one is named. Returns null when
|
|
54
|
+
* main is green, or red for RED_MAIN_WARN_MS or less. */
|
|
55
|
+
export function redMainWarning(events, now) {
|
|
56
|
+
const cutoff = now - DAY_MS;
|
|
57
|
+
const relevant = [];
|
|
58
|
+
for (const e of events) {
|
|
59
|
+
if (typeof e.ts !== "number" || e.ts < cutoff)
|
|
60
|
+
continue;
|
|
61
|
+
if (isRedMain(e) || isPassingMainCheck(e))
|
|
62
|
+
relevant.push(e);
|
|
63
|
+
}
|
|
64
|
+
if (relevant.length === 0)
|
|
65
|
+
return null;
|
|
66
|
+
let newest = relevant[0];
|
|
67
|
+
for (const e of relevant)
|
|
68
|
+
if (e.ts > newest.ts)
|
|
69
|
+
newest = e;
|
|
70
|
+
if (!isRedMain(newest))
|
|
71
|
+
return null;
|
|
72
|
+
const passes = relevant.filter(isPassingMainCheck);
|
|
73
|
+
const red = isRedMain;
|
|
74
|
+
const lastPass = passes.length > 0 ? passes[passes.length - 1] : undefined;
|
|
75
|
+
const reds = relevant.filter(red);
|
|
76
|
+
// No pass bounds the red spell, so the first red in the window opens it: a window with reds
|
|
77
|
+
// and no pass has been red for the whole window's coverage, so it reads "more than 24 h" (G1)
|
|
78
|
+
// no matter where the reds sit, not only when one happens to fall on the window's edge. A
|
|
79
|
+
// single fresh red (the only span recorded, e.g. a hold 47 min ago) reads its own age.
|
|
80
|
+
const firstRed = relevant.find((e) => red(e) && (lastPass === undefined || e.ts >= lastPass.ts));
|
|
81
|
+
// The red is "since" the event the duration is counted from, which for a red spell with more
|
|
82
|
+
// than one red is not the newest red: the design names the first red after the newest pass.
|
|
83
|
+
const startEv = firstRed ?? newest;
|
|
84
|
+
const durationMs = now - startEv.ts;
|
|
85
|
+
if (durationMs <= RED_MAIN_WARN_MS)
|
|
86
|
+
return null;
|
|
87
|
+
const unbroken = lastPass === undefined && reds.length > 1;
|
|
88
|
+
const duration = unbroken || durationMs >= DAY_MS ? "more than 24 h" : redDurationPhrase(durationMs);
|
|
89
|
+
const failing = Array.isArray(newest.failingTests) && typeof newest.failingTests[0] === "string" ? newest.failingTests[0] : undefined;
|
|
90
|
+
const head = `main has had no passing full check for ${duration}: red since ${shortSha(startEv.sha)} at ${clockHm(startEv.ts)}`;
|
|
91
|
+
return failing ? `${head} — ${squash(failing, 120)}` : head;
|
|
92
|
+
}
|
|
93
|
+
/** G3 over `entries` (the loadQuarantine list): warns when the list holds more than
|
|
94
|
+
* QUARANTINE_WARN_COUNT tests, or when any test's `firstSeen` is more than QUARANTINE_AGE_MS
|
|
95
|
+
* ago — an old entry is a failure the team has stopped seeing, and an unfiled one (`bug` empty)
|
|
96
|
+
* never leaves quarantine. The detail names the oldest tests with their age and BUGS.md heading
|
|
97
|
+
* (or "not filed"). Returns null when neither fires. */
|
|
98
|
+
export function quarantineWarning(entries, now) {
|
|
99
|
+
const phrases = [];
|
|
100
|
+
if (entries.length > QUARANTINE_WARN_COUNT) {
|
|
101
|
+
phrases.push(`quarantine holds ${entries.length} tests — over the ${QUARANTINE_WARN_COUNT}-test ceiling`);
|
|
102
|
+
}
|
|
103
|
+
const old = entries
|
|
104
|
+
.filter((e) => e.firstSeen < now - QUARANTINE_AGE_MS)
|
|
105
|
+
.sort((a, b) => a.firstSeen - b.firstSeen);
|
|
106
|
+
if (old.length > 0) {
|
|
107
|
+
const named = old.slice(0, QUARANTINE_DETAIL_MAX).map((e) => {
|
|
108
|
+
const filed = e.bug ? squash(e.bug, 40) : "not filed";
|
|
109
|
+
return `"${squash(e.test, 40)}" (${filed}, ${humanAgo(e.firstSeen, now)})`;
|
|
110
|
+
});
|
|
111
|
+
const more = old.length > QUARANTINE_DETAIL_MAX ? ` (and ${old.length - QUARANTINE_DETAIL_MAX} more)` : "";
|
|
112
|
+
phrases.push(`quarantine holds ${old.length} test${old.length === 1 ? "" : "s"} over 3 days: ${named.join(", ")}${more}`);
|
|
113
|
+
}
|
|
114
|
+
return phrases.length > 0 ? phrases.join("; ") : null;
|
|
115
|
+
}
|
|
116
|
+
/** How many `main_revert` events in the trailing 24 h the doctor and digest warn once the
|
|
117
|
+
* daily ceiling is passed (G2's rate test): a burst of reverts reads like a flaky-revert
|
|
118
|
+
* habit, distinct from a single false revert. */
|
|
119
|
+
export const REVERT_RATE_LIMIT = 5;
|
|
120
|
+
/** One `main_revert` in the last 24 h looks false when either G2 test holds — its failure did
|
|
121
|
+
* not reproduce (a test it names has been quarantined at or since the revert, or a later
|
|
122
|
+
* `main_check`'s `quarantined` list names it), or its change came back unchanged and main
|
|
123
|
+
* stayed green (a later `merged` event carries the culprit's patch-id, and the first
|
|
124
|
+
* `main_check` after that merge which actually ran the check passed). */
|
|
125
|
+
function revertLooksFalse(r, events, quarantine) {
|
|
126
|
+
const rts = typeof r.ts === "number" ? r.ts : 0;
|
|
127
|
+
const tests = Array.isArray(r.failingTests)
|
|
128
|
+
? r.failingTests.filter((t) => typeof t === "string")
|
|
129
|
+
: [];
|
|
130
|
+
const firstTest = tests[0];
|
|
131
|
+
// Did not reproduce: the named test is quarantined at or since the revert, or a later
|
|
132
|
+
// main_check's `quarantined` list names it (src/baseline/main-watch.ts records a red naming
|
|
133
|
+
// only quarantined flaky tests as passed there).
|
|
134
|
+
if (firstTest !== undefined &&
|
|
135
|
+
(quarantine.some((q) => q.test === firstTest && q.lastSeen >= rts) ||
|
|
136
|
+
events.some((e) => e.type === "main_check" &&
|
|
137
|
+
typeof e.ts === "number" &&
|
|
138
|
+
e.ts >= rts &&
|
|
139
|
+
Array.isArray(e.quarantined) &&
|
|
140
|
+
e.quarantined.includes(firstTest))))
|
|
141
|
+
return "quarantined";
|
|
142
|
+
// Came back unchanged and main stayed green: a later `merged` carries this revert's
|
|
143
|
+
// patch-id, and the first `main_check` after that merge which ran the check passed.
|
|
144
|
+
const patch = typeof r.patchId === "string" ? r.patchId : undefined;
|
|
145
|
+
if (patch === undefined)
|
|
146
|
+
return null;
|
|
147
|
+
const remerged = events.find((e) => e.type === "merged" && e.patchId === patch && typeof e.ts === "number" && e.ts >= rts);
|
|
148
|
+
if (remerged === undefined)
|
|
149
|
+
return null;
|
|
150
|
+
const rmTs = typeof remerged.ts === "number" ? remerged.ts : 0;
|
|
151
|
+
const after = events
|
|
152
|
+
.filter((e) => e.type === "main_check" &&
|
|
153
|
+
typeof e.ts === "number" &&
|
|
154
|
+
e.ts >= rmTs &&
|
|
155
|
+
typeof e.durationMs === "number" &&
|
|
156
|
+
e.durationMs > 0)
|
|
157
|
+
.sort((a, b) => a.ts - b.ts)[0];
|
|
158
|
+
return after?.status === "passed" ? "remerged" : null;
|
|
159
|
+
}
|
|
160
|
+
/** G2 over `events` and the quarantine list: warns on a `main_revert` in the trailing 24 h that
|
|
161
|
+
* looks false, and on more than REVERT_RATE_LIMIT reverts a day. The detail names each
|
|
162
|
+
* false-looking revert (culprit sha and which test failed / that it landed again unchanged)
|
|
163
|
+
* and the count when over the rate limit. Returns the phrases that fire (empty when none do). */
|
|
164
|
+
export function revertWarnings(events, quarantine, now) {
|
|
165
|
+
const cutoff = now - DAY_MS;
|
|
166
|
+
const reverts = events.filter((e) => e.type === "main_revert" && typeof e.ts === "number" && e.ts >= cutoff);
|
|
167
|
+
if (reverts.length === 0)
|
|
168
|
+
return [];
|
|
169
|
+
const phrases = [];
|
|
170
|
+
if (reverts.length > REVERT_RATE_LIMIT) {
|
|
171
|
+
phrases.push(`${reverts.length} reverts in the last 24 h — over the ${REVERT_RATE_LIMIT}-revert ceiling`);
|
|
172
|
+
}
|
|
173
|
+
for (const r of reverts) {
|
|
174
|
+
const culprit = typeof r.commit === "string" ? r.commit : typeof r.sha === "string" ? r.sha : "";
|
|
175
|
+
const reason = revertLooksFalse(r, events, quarantine);
|
|
176
|
+
if (reason === "quarantined") {
|
|
177
|
+
const tests = Array.isArray(r.failingTests)
|
|
178
|
+
? r.failingTests.filter((t) => typeof t === "string")
|
|
179
|
+
: [];
|
|
180
|
+
phrases.push(`revert ${shortSha(culprit)} blamed ${squash(tests[0] ?? "", 60)}, which has been quarantined since — the failure did not reproduce`);
|
|
181
|
+
}
|
|
182
|
+
else if (reason === "remerged") {
|
|
183
|
+
phrases.push(`revert ${shortSha(culprit)}'s change merged again unchanged and main stayed green — the revert looks false`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return phrases;
|
|
187
|
+
}
|
|
188
|
+
/** Both guardrail phrases that fire for `root`, reading the trailing 24 h of the event log and
|
|
189
|
+
* quarantine.json, oldest-test text bounded as the doctor rows are. */
|
|
190
|
+
export function guardrailWarnings(root, now = Date.now()) {
|
|
191
|
+
const warnings = [];
|
|
192
|
+
const events = readEventsSince(root, DAY_MS).events;
|
|
193
|
+
const red = redMainWarning(events, now);
|
|
194
|
+
if (red !== null)
|
|
195
|
+
warnings.push(red);
|
|
196
|
+
const q = loadQuarantine(root);
|
|
197
|
+
const qw = quarantineWarning(q, now);
|
|
198
|
+
if (qw !== null)
|
|
199
|
+
warnings.push(qw);
|
|
200
|
+
warnings.push(...revertWarnings(events, q, now));
|
|
201
|
+
return warnings;
|
|
202
|
+
}
|
|
203
|
+
/** "40 min", "3 h" — the duration a red main's warning names. */
|
|
204
|
+
function redDurationPhrase(ms) {
|
|
205
|
+
const minutes = Math.round(ms / 60_000);
|
|
206
|
+
if (minutes < 24 * 60)
|
|
207
|
+
return `${minutes} min`;
|
|
208
|
+
return `${Math.round(ms / 3_600_000)} h`;
|
|
209
|
+
}
|