tumwater 0.2.0 → 0.4.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 +55 -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 +6 -4
- 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 +404 -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 +281 -55
- package/dist/src/backlog/backlog-cli-view.js +255 -0
- package/dist/src/backlog/backlog-cli.js +91 -335
- package/dist/src/backlog/backlog-commit.js +20 -13
- package/dist/src/backlog/backlog-eligibility.js +193 -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 +264 -0
- package/dist/src/backlog/backlog-release.js +134 -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-guardrails.js +136 -0
- package/dist/src/baseline/main-red.js +153 -36
- package/dist/src/baseline/main-watch.js +232 -0
- package/dist/src/brief/brief-edit.js +12 -12
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +31 -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 +65 -3
- 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 +181 -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 +183 -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 +291 -598
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +525 -0
- package/dist/src/cli/cli-start-summary.js +167 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
- package/dist/src/cli/completion.js +169 -22
- package/dist/src/cli/config-commands.js +58 -26
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +258 -82
- package/dist/src/cli/log-commands.js +164 -85
- package/dist/src/cli/question-commands.js +211 -10
- package/dist/src/cli.js +173 -45
- package/dist/src/collections.js +56 -13
- 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 +16 -1
- package/dist/src/config/config-fallbacks.js +94 -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 +169 -222
- package/dist/src/config/config-views.js +136 -54
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +164 -265
- package/dist/src/config/config.js +53 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
- package/dist/src/doctor/doctor-backlog-plans.js +398 -0
- package/dist/src/doctor/doctor-backlog.js +249 -512
- package/dist/src/doctor/doctor-check-names.js +85 -0
- package/dist/src/doctor/doctor-checks.js +61 -593
- package/dist/src/doctor/doctor-claude-checks.js +142 -0
- package/dist/src/doctor/doctor-codex-checks.js +276 -0
- package/dist/src/doctor/doctor-contention-checks.js +506 -0
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +80 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +216 -537
- package/dist/src/doctor/doctor-notify.js +9 -5
- package/dist/src/doctor/doctor-opencode-checks.js +164 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +397 -0
- package/dist/src/doctor/doctor.js +126 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +21 -12
- package/dist/src/events/event-format.js +185 -89
- package/dist/src/events/event-labels.js +11 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +52 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +80 -26
- 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 +65 -60
- package/dist/src/failure/failure-render.js +29 -21
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +35 -35
- package/dist/src/files/file-queue.js +17 -0
- 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 +88 -225
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +101 -143
- package/dist/src/gates/maintenance-quota.js +37 -30
- 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 +89 -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 +54 -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 +43 -19
- package/dist/src/gui/gui-backlog-endpoints.js +156 -0
- package/dist/src/gui/gui-command.js +26 -13
- package/dist/src/gui/gui-config-commands.js +168 -80
- package/dist/src/gui/gui-endpoint-commands.js +5 -4
- 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 +235 -13
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +97 -0
- package/dist/src/gui/gui-server.js +26 -5
- 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 +155 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +190 -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 +28 -17
- 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 +58 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +45 -19
- 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 +214 -0
- package/dist/src/merge/merge-queue-render.js +48 -11
- package/dist/src/merge/merge-queue.js +52 -15
- package/dist/src/merge/merge-requeue.js +103 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +27 -11
- package/dist/src/merge/merge-to-main.js +77 -26
- package/dist/src/merge/merge-top.js +83 -0
- package/dist/src/merge/merge-verify.js +75 -18
- package/dist/src/notes-cli.js +35 -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 +352 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
- package/dist/src/orchestrator/orchestrator.js +129 -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 +245 -0
- package/dist/src/orchestrator/scheduling-skip.js +195 -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 +126 -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 +26 -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 +150 -217
- package/dist/src/prompt/notes-backlog.js +184 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +131 -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 +188 -124
- package/dist/src/report/report-fold.js +130 -54
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +136 -65
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +23 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +112 -1
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +239 -0
- package/dist/src/review/protected-edits.js +24 -10
- package/dist/src/review/quarantine-render.js +65 -0
- 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 +180 -401
- 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 +127 -12
- package/dist/src/roles/role-guidance.js +65 -20
- package/dist/src/roles/role-labels.js +7 -5
- package/dist/src/roles/role-render.js +108 -56
- package/dist/src/roles/role-view.js +95 -56
- package/dist/src/roles/roles.js +33 -33
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/run/qa-coverage.js +125 -0
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +175 -0
- package/dist/src/run/run-detail-render.js +91 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -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} +122 -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} +170 -83
- 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} +105 -57
- 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 +178 -0
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +231 -0
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +216 -0
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +191 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +109 -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} +39 -18
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +103 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claim-entries.js +49 -0
- 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 +49 -17
- 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 +43 -28
- package/dist/src/status/status-roles.js +50 -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 +7 -8
- 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 +65 -18
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +60 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +78 -13
- package/dist/src/ui/badges.js +99 -38
- package/dist/src/ui/gui/gui-client-backlog.js +314 -0
- package/dist/src/ui/gui/gui-client-boot.js +89 -61
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
- package/dist/src/ui/gui/gui-client-drawer.js +155 -289
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +437 -68
- package/dist/src/ui/gui/gui-client-labels.js +6 -3
- 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 +60 -52
- package/dist/src/ui/gui/gui-client-pending.js +405 -24
- package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
- package/dist/src/ui/gui/gui-client-report.js +105 -430
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +163 -5
- package/dist/src/ui/gui/gui-client-team.js +36 -465
- package/dist/src/ui/gui/gui-client.js +38 -26
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +10 -7
- package/dist/src/ui/gui/gui-styles.js +38 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +364 -49
- package/dist/src/ui/status-render.js +109 -82
- package/dist/src/ui/team-alerts.js +728 -62
- package/dist/src/ui/tone.js +12 -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 +53 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +4 -12
- package/dist/src/ui/tui/tui-editor-backends.js +127 -0
- package/dist/src/ui/tui/tui-editor-brief.js +14 -36
- package/dist/src/ui/tui/tui-editor-budget.js +36 -39
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
- package/dist/src/ui/tui/tui-editor-config.js +99 -0
- package/dist/src/ui/tui/tui-editor-entry.js +39 -64
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +46 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
- package/dist/src/ui/tui/tui-editor-interval.js +29 -16
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +9 -29
- 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 +95 -41
- package/dist/src/ui/tui/tui-editor-retire.js +5 -13
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +71 -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 +101 -34
- package/dist/src/ui/tui/tui-editor-slots.js +24 -0
- package/dist/src/ui/tui/tui-editors.js +44 -2
- package/dist/src/ui/tui/tui-frame.js +68 -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 +33 -9
- package/dist/src/ui/tui/tui-pane-activity.js +35 -19
- package/dist/src/ui/tui/tui-pane-backlog.js +198 -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 +168 -8
- package/dist/src/ui/tui/tui-pane-history.js +446 -82
- package/dist/src/ui/tui/tui-pane-pending.js +539 -15
- package/dist/src/ui/tui/tui-pane-queued.js +78 -57
- package/dist/src/ui/tui/tui-pane-report.js +54 -16
- package/dist/src/ui/tui/tui-pane-session.js +34 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +91 -28
- package/dist/src/ui/tui/tui-panes.js +153 -34
- package/dist/src/ui/tui/tui.js +18 -15
- package/dist/src/verdict/fix-claim.js +129 -33
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +77 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/leftover.js +0 -130
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/qa-coverage.js +0 -108
- 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
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/** The doctor's checks that inspect the project's own on-disk state, disk and build,
|
|
2
|
+
* split out of doctor-checks.ts (whose environment/repo file still holds the toolchain and
|
|
3
|
+
* setup checks — the node/git/exec binaries, the repo/init/config/brief/bootstrap setup, the
|
|
4
|
+
* agent backends' CLI versions and the operator pi-extension/funes coordination). This module
|
|
5
|
+
* holds the checks that look at the project itself: a writable .tumwater (checkStateDir), a
|
|
6
|
+
* readable event feed (checkEventLog), enough free disk (checkDiskSpace), a declared project
|
|
7
|
+
* check (checkBuildCheck), the review gate's flake memory (checkFlakyChecks) and build
|
|
8
|
+
* provenance (checkBuild). Read-only like every doctor check; the private helpers each cluster
|
|
9
|
+
* uses (sweepStaleProbes, eventLogHistoryPhrase, firstEventLine, diskReclaimPhrase,
|
|
10
|
+
* checkDurationPhrase, currentHead) live here beside the checks that use them. doctor.ts stays
|
|
11
|
+
* the composition layer that calls each check in fixed order. */
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { detectBuildCheck, mergeCommandOf } from "../build/build-check-detect.js";
|
|
15
|
+
import { isPassingFullCheck } from "../build/build-check-events.js";
|
|
16
|
+
import { buildStaleness, isSelfHosted, readBuildInfo, STALE_INPUTS_LABEL } from "../build/build-info.js";
|
|
17
|
+
import { dirEntryNames, statOrNull } from "../files/files.js";
|
|
18
|
+
import { refSha } from "../git/git.js";
|
|
19
|
+
import { STATE_DIR, eventsArchivePath, eventsLogPath } from "../paths.js";
|
|
20
|
+
import { BYTES_PER_GB, diskVolumePath, sampleFreeBytes } from "../gates/disk-gate.js";
|
|
21
|
+
import { DAY_MS, dayKey } from "../text/datetime.js";
|
|
22
|
+
import { errorMessage } from "../text/text.js";
|
|
23
|
+
import { lowerMedian } from "../collections.js";
|
|
24
|
+
import { formatGB, shortSha } from "../text/format.js";
|
|
25
|
+
import { eventLogState, readEventsSince } from "../events/event-window.js";
|
|
26
|
+
import { EVENTS_KEEP } from "../events/events.js";
|
|
27
|
+
import { finiteNumber } from "../files/json-object.js";
|
|
28
|
+
import { agree, plural, shortSpanPhrase } from "../text/phrases.js";
|
|
29
|
+
import { KNOWN_FLAKE_WINDOW_MS, loadQuarantine, repeatedFlakeCounts } from "../review/known-flakes.js";
|
|
30
|
+
import { POST_MERGE_REJECT_WINDOW_MS, postMergeChurn } from "../review/post-merge-review.js";
|
|
31
|
+
import { roleLabel } from "../roles/role-labels.js";
|
|
32
|
+
import { quarantineWarning } from "../baseline/main-guardrails.js";
|
|
33
|
+
import { readCiParity, currentCiParityState, parityIsCurrent, ciParityStalled, CI_PARITY_STALE_MS } from "../run/ci-parity.js";
|
|
34
|
+
import { readQaCoverage, qaCoverageStalled, QA_COVERAGE_STALE_MS } from "../run/qa-coverage.js";
|
|
35
|
+
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
36
|
+
/** .tumwater writable — absent is fine (created on first run); present, prove it by writing
|
|
37
|
+
* and deleting a temp file inside. The probe leaves the dir's listing unchanged after the run.
|
|
38
|
+
* The write is the property under test: only a write that fails is "not writable", so the
|
|
39
|
+
* cleanup below — deleting the probe, sweeping stales — never vetoes a dir the write just
|
|
40
|
+
* proved writable, and a probe left over is swept rather than accumulated. */
|
|
41
|
+
export function checkStateDir(root) {
|
|
42
|
+
const dir = path.join(root, STATE_DIR);
|
|
43
|
+
if (!fs.existsSync(dir))
|
|
44
|
+
return okOutcome("absent — created on first run");
|
|
45
|
+
const probe = path.join(dir, `.doctor-probe-${process.pid}`);
|
|
46
|
+
try {
|
|
47
|
+
fs.writeFileSync(probe, "");
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
// A partial write may already have created the file — sweep it like any other leftover.
|
|
51
|
+
sweepStaleProbes(dir);
|
|
52
|
+
return failOutcome(`not writable: ${errorMessage(err)}`);
|
|
53
|
+
}
|
|
54
|
+
// The write proved writability. Deleting the probe is cleanup, not measurement: a removal
|
|
55
|
+
// failure must not turn a succeeded write into a false alarm, and the stray is collected by
|
|
56
|
+
// the sweep below (or a later pass's sweep).
|
|
57
|
+
try {
|
|
58
|
+
fs.rmSync(probe);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Best-effort — the stray is swept in a moment.
|
|
62
|
+
}
|
|
63
|
+
sweepStaleProbes(dir);
|
|
64
|
+
return okOutcome("writable");
|
|
65
|
+
}
|
|
66
|
+
/** Best-effort removal of every `.doctor-probe-*` file in `dir` — the only artifact
|
|
67
|
+
* checkStateDir creates, so any present probe is a leftover: a prior run killed between its
|
|
68
|
+
* write and delete, a partial write above, or a removal that failed. Removed one at a time so
|
|
69
|
+
* a single unremovable probe cannot stop the sweep. */
|
|
70
|
+
function sweepStaleProbes(dir) {
|
|
71
|
+
for (const name of dirEntryNames(dir)) {
|
|
72
|
+
if (!name.startsWith(".doctor-probe-"))
|
|
73
|
+
continue;
|
|
74
|
+
try {
|
|
75
|
+
fs.rmSync(path.join(dir, name));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Best-effort — leave it for the next pass.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Event log readable — open and parse events.jsonl (and its rotated archive) so the one
|
|
83
|
+
* failure the selfcheck role is chartered to catch — a feed that hid itself — is named by the
|
|
84
|
+
* pre-flight too. An absent or empty log is fine: a young project writes its first event on the
|
|
85
|
+
* first run. A present-but-unparseable log (a directory at the path, a permission failure, or
|
|
86
|
+
* every complete line corrupt) fails, because every observer — selfcheck, the TUI/GUI feeds,
|
|
87
|
+
* `logs`, `report` — reads that one feed. Read-only. */
|
|
88
|
+
export function checkEventLog(root) {
|
|
89
|
+
let state;
|
|
90
|
+
try {
|
|
91
|
+
state = eventLogState(root);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
return failOutcome(`could not read ${eventsLogPath(root)}: ${errorMessage(err)}`);
|
|
95
|
+
}
|
|
96
|
+
if (state === "unreadable")
|
|
97
|
+
return failOutcome(`unreadable: ${eventsLogPath(root)} — the team's event feed cannot be read`);
|
|
98
|
+
if (state === "empty")
|
|
99
|
+
return okOutcome("no events yet");
|
|
100
|
+
const history = eventLogHistoryPhrase(root);
|
|
101
|
+
return okOutcome(history ? `readable: ${eventsLogPath(root)} — ${history}` : `readable: ${eventsLogPath(root)}`);
|
|
102
|
+
}
|
|
103
|
+
/** The `history from <date>, <n> files` phrase for checkEventLog's ok detail (G4): the first
|
|
104
|
+
* complete line of the oldest event-log generation present names how far back the kept history
|
|
105
|
+
* reaches, and the count is how many generations (live file plus archives) exist. Undefined
|
|
106
|
+
* when no generation parses an event, so a readable-but-shapeless log still reads as readable. */
|
|
107
|
+
function eventLogHistoryPhrase(root) {
|
|
108
|
+
const present = [];
|
|
109
|
+
const livePath = eventsLogPath(root);
|
|
110
|
+
if (statOrNull(livePath))
|
|
111
|
+
present.push(livePath);
|
|
112
|
+
for (let gen = 1; gen <= EVENTS_KEEP; gen++) {
|
|
113
|
+
const archive = eventsArchivePath(root, gen);
|
|
114
|
+
if (statOrNull(archive))
|
|
115
|
+
present.push(archive);
|
|
116
|
+
}
|
|
117
|
+
if (present.length === 0)
|
|
118
|
+
return undefined;
|
|
119
|
+
const oldest = present[present.length - 1];
|
|
120
|
+
const ev = firstEventLine(oldest);
|
|
121
|
+
if (!ev || typeof ev.ts !== "number")
|
|
122
|
+
return undefined;
|
|
123
|
+
const noun = plural(present.length, "file", "files");
|
|
124
|
+
return `history from ${dayKey(ev.ts)}, ${noun}`;
|
|
125
|
+
}
|
|
126
|
+
/** Parse the first complete line of an append-only JSONL `file`, or null when it is unreadable
|
|
127
|
+
* or does not parse. Reads only the first chunk — the event log is up to 16 MB a generation,
|
|
128
|
+
* and a doctor pre-flight should not slurp the oldest archive just to name its first day. */
|
|
129
|
+
function firstEventLine(file) {
|
|
130
|
+
try {
|
|
131
|
+
const fd = fs.openSync(file, "r");
|
|
132
|
+
try {
|
|
133
|
+
const buf = Buffer.alloc(4096);
|
|
134
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
135
|
+
const line = buf.toString("utf8", 0, n).split("\n")[0] ?? "";
|
|
136
|
+
if (!line)
|
|
137
|
+
return null;
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(line);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
fs.closeSync(fd);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Disk floor (plans/disk-floor.md, parts 1/4 and 2/4) — is there room on the volume the
|
|
154
|
+
* worktrees live on? Fail below `diskHoldGB`, the same floor the running team holds new work
|
|
155
|
+
* at, so doctor and the gate agree on when the disk is a problem. Warn in the band between
|
|
156
|
+
* `diskHoldGB` and `diskReclaimGB`: pressure reclaim will delete ignored build outputs from
|
|
157
|
+
* idle worktrees but the team still runs. Ok at or above `diskReclaimGB` (or when reclaim is
|
|
158
|
+
* disabled and free space is above the hold). A statfs failure is a warning, not a failure: an
|
|
159
|
+
* unmeasurable volume never holds the team, so it must not fail a scripted pre-flight. The
|
|
160
|
+
* detail names the measured path, the free GB to one decimal, and the floor. `holdGB` 0
|
|
161
|
+
* disables the hold, so a free space above `reclaimGB`'s own band (or `reclaimGB` 0) passes.
|
|
162
|
+
* After a successful sample the detail also carries `diskReclaimPhrase` (what `disk_reclaim`
|
|
163
|
+
* passes freed over the last 24 h); it is informational and never changes the level. */
|
|
164
|
+
export function checkDiskSpace(root, holdGB, reclaimGB, sample = sampleFreeBytes) {
|
|
165
|
+
const where = diskVolumePath(root);
|
|
166
|
+
const free = sample(root);
|
|
167
|
+
if (free === null)
|
|
168
|
+
return warnOutcome(`cannot measure free space at ${where} (statfs failed) — the disk hold is off for this process`);
|
|
169
|
+
const freeGB = free / BYTES_PER_GB;
|
|
170
|
+
const freeText = `${formatGB(freeGB)} free at ${where}`;
|
|
171
|
+
const phrase = diskReclaimPhrase(readEventsSince(root, DAY_MS).events);
|
|
172
|
+
const withReclaim = (detail) => (phrase ? `${detail} · ${phrase}` : detail);
|
|
173
|
+
if (holdGB > 0 && freeGB < holdGB)
|
|
174
|
+
return failOutcome(withReclaim(`${freeText} — below the ${holdGB} GB diskHoldGB floor; the team waits to start new work until space recovers`));
|
|
175
|
+
if (reclaimGB > 0 && freeGB < reclaimGB)
|
|
176
|
+
return warnOutcome(withReclaim(`${freeText} — below the ${reclaimGB} GB diskReclaimGB threshold; a pressure reclaim pass deletes ignored build outputs from idle worktrees`));
|
|
177
|
+
const floor = holdGB > 0 ? `above the ${holdGB} GB floor` : "diskHoldGB is 0 (hold disabled)";
|
|
178
|
+
return okOutcome(withReclaim(`${freeText}, ${floor}`));
|
|
179
|
+
}
|
|
180
|
+
/** The `reclaim in the last 24h freed …` phrase for the `disk space` row: the summed
|
|
181
|
+
* `freedGB` of the `disk_reclaim` passes, their count, and how many ran under pressure (an
|
|
182
|
+
* event without a `mode` counts as pressure, like its feed line). Each pass is a point event,
|
|
183
|
+
* so summing cannot straddle the window edge. Undefined when no pass ran. */
|
|
184
|
+
export function diskReclaimPhrase(events) {
|
|
185
|
+
const passes = events.filter((e) => e.type === "disk_reclaim");
|
|
186
|
+
if (passes.length === 0)
|
|
187
|
+
return undefined;
|
|
188
|
+
const freed = passes.reduce((sum, e) => sum + finiteNumber(e.freedGB, 0), 0);
|
|
189
|
+
const pressure = passes.filter((e) => (typeof e.mode === "string" && e.mode ? e.mode : "pressure") === "pressure").length;
|
|
190
|
+
const base = `reclaim in the last 24h freed ${formatGB(freed)} over ${plural(passes.length, "pass", "passes")}`;
|
|
191
|
+
return pressure > 0 ? `${base}, ${pressure} under pressure` : base;
|
|
192
|
+
}
|
|
193
|
+
/** The `passing merge checks …` phrase for the `check` row: median and slowest wall clock of
|
|
194
|
+
* the passed full-suite runs (`merge`, `batch`, `baseline` scopes — a `gate` check can run a
|
|
195
|
+
* subset, and failed or skipped runs do not measure a full pass). The lower median keeps the
|
|
196
|
+
* figure a run that happened. Undefined when no event qualifies. */
|
|
197
|
+
export function checkDurationPhrase(events) {
|
|
198
|
+
const durations = events
|
|
199
|
+
.filter(isPassingFullCheck)
|
|
200
|
+
.map((e) => finiteNumber(e.durationMs, 0))
|
|
201
|
+
.filter((ms) => ms > 0);
|
|
202
|
+
if (durations.length === 0)
|
|
203
|
+
return undefined;
|
|
204
|
+
const median = lowerMedian(durations);
|
|
205
|
+
const slowest = Math.max(...durations);
|
|
206
|
+
return `passing merge checks in the last 24h took ${shortSpanPhrase(median)} median, ${shortSpanPhrase(slowest)} slowest (${plural(durations.length, "run")})`;
|
|
207
|
+
}
|
|
208
|
+
/** Declared project check — names what the review gate's deterministic pre-check, the
|
|
209
|
+
* red-main baseline, and redeploy's green check will run, without running it: a configured
|
|
210
|
+
* `check.command` (plans/portability.md §6/7), else the npm auto-detection. None declared is
|
|
211
|
+
* a warn, not informational (the 2026-09-05 stance predates non-npm targets being supported,
|
|
212
|
+
* when a warn would have been noise no operator could act on — with `check.command` available
|
|
213
|
+
* the warning is actionable): the three gates degrade to "no check" and a silently absent
|
|
214
|
+
* safety layer is the failure this check exists to surface. Warn does not fail the exit code
|
|
215
|
+
* (the fallback check's precedent, doctor-fallback-checks.ts's checkFallbackModel). */
|
|
216
|
+
export function checkBuildCheck(root, config) {
|
|
217
|
+
const check = detectBuildCheck(root, config ?? undefined);
|
|
218
|
+
if (!check)
|
|
219
|
+
return warnOutcome("none declared — the review gate's build pre-check, the red-main baseline, and redeploy's green check are all off (set `check.command` in tumwater.json for a non-npm repo)");
|
|
220
|
+
// The in-lock command from mergeCommandOf (absent mergeCommand falls back to the gate
|
|
221
|
+
// command): the row names which command the merge lock runs and where the full check lands,
|
|
222
|
+
// facts a reader cannot get from detectBuildCheck alone. The after-merge roles are not named
|
|
223
|
+
// here — they live under `review`, which this slice does not hold, and the `review` row in
|
|
224
|
+
// docs/config.md documents them.
|
|
225
|
+
const lockPhrase = (() => {
|
|
226
|
+
const mc = mergeCommandOf(config ?? undefined);
|
|
227
|
+
return mc !== undefined
|
|
228
|
+
? `merge lock runs ${mc}, full check after merging`
|
|
229
|
+
: "full check in the merge lock";
|
|
230
|
+
})();
|
|
231
|
+
const phrase = checkDurationPhrase(readEventsSince(root, DAY_MS).events);
|
|
232
|
+
const withTiming = (detail) => okOutcome(`${detail} · ${lockPhrase}${phrase ? ` · ${phrase}` : ""}`);
|
|
233
|
+
if (check.kind === "command") {
|
|
234
|
+
const where = path.relative(root, check.cwd);
|
|
235
|
+
return withTiming(where ? `${check.command} (cwd ${where})` : check.command);
|
|
236
|
+
}
|
|
237
|
+
const where = path.relative(root, check.rootDir);
|
|
238
|
+
return withTiming(where ? `npm ${check.script} in ${where}` : `npm ${check.script}`);
|
|
239
|
+
}
|
|
240
|
+
/** The review gate's flake memory surfaced read-only: names which checks the gate kept excusing as
|
|
241
|
+
* flakes over its own KNOWN_FLAKE_WINDOW_MS window. The gate logs `gate check failed then passed on
|
|
242
|
+
* retry — flaky: <headline>` for each failure that passed on its one immediate re-run, and the
|
|
243
|
+
* in-lock merge check logs the same under MERGE_FLAKY_WARNING_PREFIX when its kept-pin retry passes
|
|
244
|
+
* (src/review/known-flakes.ts); that record is internal to the gate's matching, so a genuinely
|
|
245
|
+
* flaky check silently burns a re-run per merge and hides real instability — exactly the
|
|
246
|
+
* information that explains intermittent rejections. This check reads the same window and folds the
|
|
247
|
+
* same normalized headline the gate matches with (repeatedFlakeCounts over flakeCounts, so the
|
|
248
|
+
* prefix slice-offset and
|
|
249
|
+
* cluster normalization cannot drift), and warns only when a normalized check flapped more than
|
|
250
|
+
* once, so a routine one-off flake stays quiet. ok/warn only — never a fail — so a flaky suite
|
|
251
|
+
* cannot fail a scripted doctor pre-flight (the checkStalledEntries precedent; the exit-code
|
|
252
|
+
* rule is
|
|
253
|
+
* outcome.ts's). */
|
|
254
|
+
export function checkFlakyChecks(root, now = Date.now()) {
|
|
255
|
+
const { events } = readEventsSince(root, KNOWN_FLAKE_WINDOW_MS);
|
|
256
|
+
const flapped = repeatedFlakeCounts(events);
|
|
257
|
+
const phrases = [];
|
|
258
|
+
const flappedPhrase = flappedChecksPhrase(flapped);
|
|
259
|
+
if (flappedPhrase !== null)
|
|
260
|
+
phrases.push(flappedPhrase);
|
|
261
|
+
const q = quarantineWarning(loadQuarantine(root), now);
|
|
262
|
+
if (q !== null)
|
|
263
|
+
phrases.push(`${q} — see \`tumwater quarantine\``);
|
|
264
|
+
if (phrases.length === 0)
|
|
265
|
+
return okOutcome("no check flapped more than once in the last 24h");
|
|
266
|
+
return warnOutcome(phrases.join("; "));
|
|
267
|
+
}
|
|
268
|
+
/** A trailing day of at least `POST_MERGE_REJECT_WARN` post-merge `review_rejected` events — the
|
|
269
|
+
* audit twin of the always-on `post-merge-churn` amber alert (src/ui/status-payload.ts
|
|
270
|
+
* `postMergeChurnAlert`). Each rejection is an already-landed commit the reviewer turned back, so
|
|
271
|
+
* it stays on main with the reviewer's reasons, so a churn day must not read as a clean
|
|
272
|
+
* main. Shares the alert's
|
|
273
|
+
* window, threshold and
|
|
274
|
+
* event selection through `postMergeChurn`, so the doctor row and the alert cannot disagree on
|
|
275
|
+
* when churn fires. ok/warn only — a churn burst does not stop the team. */
|
|
276
|
+
export function checkPostMergeChurn(root) {
|
|
277
|
+
const { events } = readEventsSince(root, POST_MERGE_REJECT_WINDOW_MS);
|
|
278
|
+
const churn = postMergeChurn(events);
|
|
279
|
+
if (churn === null)
|
|
280
|
+
return okOutcome("no post-merge rejection burst in the last day");
|
|
281
|
+
return warnOutcome(`${plural(churn.count, "post-merge review rejection")} in the last day (${churn.roles
|
|
282
|
+
.map((r) => `${roleLabel(r.role)} ×${r.count}`)
|
|
283
|
+
.join(", ")}) — each commit had already reached main and stays there with the reviewer's reasons; narrow \`review.afterMerge\` for the noisy role or raise the authoring bar`);
|
|
284
|
+
}
|
|
285
|
+
/** The one home of the doctor's `flaky checks` sentence: names each check that flapped more than
|
|
286
|
+
* once, most frequent first, with how many times it flapped — the same wording the always-on
|
|
287
|
+
* flapped-checks alert carries, so the doctor row and the alert cannot drift. null when `flapped`
|
|
288
|
+
* is empty (nothing crossed the bar). */
|
|
289
|
+
export function flappedChecksPhrase(flapped) {
|
|
290
|
+
if (flapped.length === 0)
|
|
291
|
+
return null;
|
|
292
|
+
const render = ([k, n]) => `"${k}" ×${n}`;
|
|
293
|
+
const [first, ...rest] = flapped;
|
|
294
|
+
const more = rest.length > 0 ? ` (and ${rest.length} more: ${rest.map(render).join(", ")})` : "";
|
|
295
|
+
return `${agree(flapped.length, "check", "checks")} flapped more than once in the last 24h: ${render(first)}${more}`;
|
|
296
|
+
}
|
|
297
|
+
/** Build provenance — is the harness about to run (this process's dist/) the code main
|
|
298
|
+
* describes? Only meaningful when this project IS the harness (isSelfHosted); elsewhere the
|
|
299
|
+
* stamp is reported as-is. A stale build is a warning, not a failure: the team runs, just not
|
|
300
|
+
* the newest code, and auto-restart (or a rebuild + restart) resolves it. `info`, `head` and
|
|
301
|
+
* `running` are injectable so tests can exercise every branch without compiling anything. */
|
|
302
|
+
export async function checkBuild(root, info = readBuildInfo(), head = undefined,
|
|
303
|
+
/** What the running orchestrator published about this build (orchestrator.json), when one is
|
|
304
|
+
* up. A stale build whose restart was REFUSED must not be reported with the boilerplate
|
|
305
|
+
* "auto-restart does this for a running team": nothing will happen until main moves, and
|
|
306
|
+
* doctor is where an operator goes to find out why. */
|
|
307
|
+
running = null) {
|
|
308
|
+
if (!info)
|
|
309
|
+
return okOutcome("no build stamp — dist/ compiled without `npm run build`");
|
|
310
|
+
const sha = shortSha(info.sha);
|
|
311
|
+
if (!(await isSelfHosted(root, info)))
|
|
312
|
+
// Not "dist/ from …": in someone else's project that names a dist/ directory the project
|
|
313
|
+
// does not have — the stamp read here is the running harness install's own, so say that.
|
|
314
|
+
return okOutcome(`tumwater version ${sha} (this project is not tumwater itself)`);
|
|
315
|
+
const mainHead = head === undefined ? await currentHead(root) : head;
|
|
316
|
+
if (!mainHead)
|
|
317
|
+
return okOutcome(`dist/ from ${sha}`);
|
|
318
|
+
const stale = await buildStaleness(root, info.sha, mainHead);
|
|
319
|
+
if (!stale)
|
|
320
|
+
return okOutcome(`dist/ from ${sha}`);
|
|
321
|
+
if (stale.stale) {
|
|
322
|
+
const next = running?.restartBlocked
|
|
323
|
+
? `; auto-restart is BLOCKED (${running.restartBlocked}) and will not retry until main moves or the block clears`
|
|
324
|
+
: running?.restartPending
|
|
325
|
+
? "; auto-restart is under way"
|
|
326
|
+
: "; run `npm run build` and restart `tumwater start` (auto-restart does this for a running team)";
|
|
327
|
+
return warnOutcome(`dist/ from ${sha} is stale — main has ${stale.aheadCommits} later commit(s) touching ${STALE_INPUTS_LABEL}${next}`);
|
|
328
|
+
}
|
|
329
|
+
return okOutcome(`dist/ from ${sha}, matches main`);
|
|
330
|
+
}
|
|
331
|
+
/** The primary checkout's HEAD sha, or null when it cannot be resolved (no repo) —
|
|
332
|
+
* git/git.ts's refSha, the one home of the rev-parse probe. */
|
|
333
|
+
function currentHead(root) {
|
|
334
|
+
return refSha(root, "HEAD");
|
|
335
|
+
}
|
|
336
|
+
/** The doctor's `ci parity` row: does the full suite cover main's current tip? Reads the
|
|
337
|
+
* CI-parity ledger (src/run/ci-parity.ts) — the operator surface answering "is main verified by
|
|
338
|
+
* CI?" — and reports whether the last run covered the current main tip @ lock. Missing ledger
|
|
339
|
+
* reads "no CI parity recorded yet" (ok); a failed record is a `warn` naming the covered
|
|
340
|
+
* tip/lock (the ci role's contract is to fix or file what it finds, so a lingering failed record
|
|
341
|
+
* is actionable until the next green run); the `current`/`stale` split is `parityIsCurrent`'s
|
|
342
|
+
* one home, so the row and the ci prompt block cannot drift. Stale is normally `ok` — main moves
|
|
343
|
+
* on every merge and the ci role runs on its own clock, so a post-merge gap is normal and must
|
|
344
|
+
* not turn the Health card or the exit code red. A stale verdict that stays stale past
|
|
345
|
+
* `CI_PARITY_STALE_MS` (ciParityStalled) is a stall — the full suite has silently stopped
|
|
346
|
+
* covering new tips — and warns, alongside the standing `ci-stale` alert. Read-only against
|
|
347
|
+
* .tumwater/. */
|
|
348
|
+
export function checkCiParity(root, config, now = Date.now()) {
|
|
349
|
+
const entry = readCiParity(root);
|
|
350
|
+
if (!entry)
|
|
351
|
+
return okOutcome("no CI parity recorded yet — the ci role has not completed a workflow run");
|
|
352
|
+
const { tip, lockHash } = currentCiParityState(root, config ?? undefined);
|
|
353
|
+
const age = shortSpanPhrase(Math.max(0, now - entry.lastRunAt));
|
|
354
|
+
if (entry.result === "failed") {
|
|
355
|
+
return warnOutcome(`the last CI run failed covering ${shortSha(entry.tip)} @ lock ${shortSha(entry.lockHash)} ${age} ago — the ci role must fix or file what it found before the next green run`);
|
|
356
|
+
}
|
|
357
|
+
if (parityIsCurrent(entry, tip, lockHash)) {
|
|
358
|
+
return okOutcome(`current — CI covered main at ${shortSha(tip)} @ lock ${shortSha(lockHash)}, passed ${age} ago`);
|
|
359
|
+
}
|
|
360
|
+
// A stale verdict is normal right after a merge (main moves and the ci role runs on its own
|
|
361
|
+
// clock), but one that stays stale past CI_PARITY_STALE_MS is a stall: the full suite has
|
|
362
|
+
// silently stopped covering new tips. The doctor warn and the standing `ci-stale` alert share
|
|
363
|
+
// this one predicate (ciParityStalled), so the row and the alert cannot disagree on when a
|
|
364
|
+
// gap is a problem. Inside the window the existing friendly stale wording stands.
|
|
365
|
+
if (ciParityStalled({ state: "stale", lastRunAt: entry.lastRunAt }, now)) {
|
|
366
|
+
return warnOutcome(`stale and stalled — the last run covered ${shortSha(entry.tip)} @ lock ${shortSha(entry.lockHash)} ${age} ago (${entry.result}); main is now ${shortSha(tip)} @ lock ${shortSha(lockHash)} — no full-suite run has covered a tip in over ${CI_PARITY_STALE_MS / DAY_MS} days, so the ci role is disabled, paused, or unable to record`);
|
|
367
|
+
}
|
|
368
|
+
return okOutcome(`stale — the last run covered ${shortSha(entry.tip)} @ lock ${shortSha(entry.lockHash)} (${entry.result}, ${age} ago); main is now ${shortSha(tip)} @ lock ${shortSha(lockHash)}, so the ci role has not yet covered this tip`);
|
|
369
|
+
}
|
|
370
|
+
/** The doctor's `qa coverage` row: has the `qa` observer — the team's only flow-level
|
|
371
|
+
* observer — stopped exercising any of the product's own flows? Reads the flow-coverage ledger
|
|
372
|
+
* (src/run/qa-coverage.ts), the operator surface answering "is qa still recording what it
|
|
373
|
+
* exercises?". A missing ledger reads "no qa flow coverage recorded yet" (ok — no record yet
|
|
374
|
+
* is not a stall); a fresh ledger reads ok naming the newest exercised flow and its age. A
|
|
375
|
+
* ledger whose newest exercise is older than `QA_COVERAGE_STALE_MS` (qaCoverageStalled) is a
|
|
376
|
+
* `warn` naming that flow and its age — the role is disabled, paused, or unable to record —
|
|
377
|
+
* alongside the standing `qa-stale` alert, both through the one `qaCoverageStalled` predicate
|
|
378
|
+
* so the row and the alert cannot disagree. Read-only against .tumwater/. */
|
|
379
|
+
export function checkQaCoverage(root, now = Date.now()) {
|
|
380
|
+
const coverage = readQaCoverage(root);
|
|
381
|
+
const entries = Object.entries(coverage);
|
|
382
|
+
if (entries.length === 0)
|
|
383
|
+
return okOutcome("no qa flow coverage recorded yet — the qa role has not completed a flow");
|
|
384
|
+
let newestFlow = entries[0][0];
|
|
385
|
+
let newestAt = entries[0][1].lastRunAt;
|
|
386
|
+
for (const [name, entry] of entries) {
|
|
387
|
+
if (entry.lastRunAt > newestAt) {
|
|
388
|
+
newestAt = entry.lastRunAt;
|
|
389
|
+
newestFlow = name;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const age = shortSpanPhrase(Math.max(0, now - newestAt));
|
|
393
|
+
if (qaCoverageStalled(coverage, now)) {
|
|
394
|
+
return warnOutcome(`the newest exercised flow ${newestFlow} ran ${age} ago — no qa flow has been exercised in over ${QA_COVERAGE_STALE_MS / DAY_MS} days, so the qa role is disabled, paused, or unable to record`);
|
|
395
|
+
}
|
|
396
|
+
return okOutcome(`newest flow coverage: ${newestFlow} exercised ${age} ago`);
|
|
397
|
+
}
|