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
|
@@ -1,57 +1,62 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import { roleLabel } from "../roles/role-labels.js";
|
|
3
|
-
import { VALIDATION_GAP_TAGS } from "../roles/role-guidance.js";
|
|
4
2
|
import fs from "node:fs";
|
|
5
3
|
import path from "node:path";
|
|
6
4
|
import { errorMessage, truncate } from "../text/text.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { currentBranch, targetBranch } from "../git/git.js";
|
|
16
|
-
import { gitTry } from "../git/git-run.js";
|
|
17
|
-
import { queuedMerges } from "../merge/merge-queue.js";
|
|
5
|
+
import { shortSha } from "../text/format.js";
|
|
6
|
+
import { readPostReviews, POST_REVIEW_FAILURE_LIMIT } from "../review/post-merge-review.js";
|
|
7
|
+
import { humanSeconds, secondsSince, HOUR_MS, DAY_MS } from "../text/datetime.js";
|
|
8
|
+
import { eventsSince } from "../events/event-window.js";
|
|
9
|
+
import { finiteNumber } from "../files/json-object.js";
|
|
10
|
+
import { lowerMedian } from "../collections.js";
|
|
11
|
+
import { plural, shortSpanPhrase } from "../text/phrases.js";
|
|
12
|
+
import { queuedMerges, unreadableMergeQueueFiles } from "../merge/merge-queue.js";
|
|
18
13
|
import { mergeChanges, readMergeMarker } from "../merge/merge-slot.js";
|
|
19
14
|
import { listWideScope, queuedListRows } from "../inbox/inbox.js";
|
|
20
15
|
import { deliverableAt } from "../inbox/prompt-not-before.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
16
|
+
import { currentBranch, targetBranch } from "../git/git.js";
|
|
17
|
+
import { gitTry } from "../git/git-run.js";
|
|
18
|
+
import { stampedSubject } from "../git/commit-message.js";
|
|
19
|
+
import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
|
|
20
|
+
import { briefFile } from "../brief/brief.js";
|
|
21
|
+
import { BACKLOG_FILES } from "../backlog/backlog-md.js";
|
|
26
22
|
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
27
|
-
/** The doctor's backlog
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
23
|
+
/** The doctor's backlog modules' shared scaffolding and the check groups that do not read a
|
|
24
|
+
* single backlog document: the merge-queue, post-merge-review and queued-prompt checks
|
|
25
|
+
* (checkStaleQueuedMerges, checkMergeQueueFiles, checkPostReviews, checkStalePrompts and the
|
|
26
|
+
* mergeWaitPhrase helper) and the uncommitted-
|
|
27
|
+
* edits-in-the-primary-checkout check. This module also hosts the helpers and constants the
|
|
28
|
+
* per-document checks import — readDoc and its checked wrapper readDocChecked, andMore, and the
|
|
29
|
+
* age/heading-trim bounds — so the per-document reads cannot drift. The per-document
|
|
30
|
+
* checks live in doctor-backlog-plans.ts (the plan-document structure and fix-claim checks) and
|
|
31
|
+
* doctor-backlog-bugs.ts (open bugs, questions and holds). All of them warn instead of failing:
|
|
32
|
+
* damaged documentation is operator signal, not a broken environment. doctor.ts composes them
|
|
33
|
+
* beside the environment checks; the shared CheckOutcome shape is imported from outcome.ts. */
|
|
36
34
|
/** How many Fixed records the two newest-first Fixed-record checks (fix claims and validation
|
|
37
35
|
* gaps) verify: the newest, since the section is newest-first by template convention. Older
|
|
38
36
|
* records drift as the code evolves — a symbol legitimately fixed long ago gets renamed later,
|
|
39
37
|
* and pre-convention Fixed entries predate the validation-gap line — so a whole-section scan
|
|
40
38
|
* would warn forever on records nobody should rewrite. */
|
|
41
|
-
const FIX_CLAIMS_CHECKED = 10;
|
|
39
|
+
export const FIX_CLAIMS_CHECKED = 10;
|
|
42
40
|
/** A second or later suspect record's heading is trimmed to this many characters in the warn —
|
|
43
41
|
* the first is named in full, the rest only enough to find them in BUGS.md. */
|
|
44
|
-
const FIX_CLAIM_HEADING_MAX = 60;
|
|
42
|
+
export const FIX_CLAIM_HEADING_MAX = 60;
|
|
45
43
|
/** A refused entry's note is trimmed to this many characters in the refusals warn. */
|
|
46
|
-
const REFUSAL_NOTE_MAX = 120;
|
|
44
|
+
export const REFUSAL_NOTE_MAX = 120;
|
|
47
45
|
/** The whole days an open question may wait before the stale-questions check warns about it:
|
|
48
46
|
* an opinionated default, matching the 7-day window the `--list` age is meant to surface.
|
|
49
47
|
* A question awaiting a human answer is operator signal, so the value needs no knob. */
|
|
50
|
-
const STALE_QUESTION_DAYS = 7;
|
|
48
|
+
export const STALE_QUESTION_DAYS = 7;
|
|
51
49
|
/** The whole days an open bug may wait before the stale-bugs check warns about it: an
|
|
52
50
|
* opinionated default with no knob, like STALE_QUESTION_DAYS. Three days is long enough that a
|
|
53
51
|
* bug filed late one evening and fixed the next day never warns. */
|
|
54
|
-
const STALE_BUG_DAYS = 3;
|
|
52
|
+
export const STALE_BUG_DAYS = 3;
|
|
53
|
+
/** The whole days a bug may stay held by a Waiting for the next failure note before the
|
|
54
|
+
* waiting-bugs check warns that its flake has not come back. A note parks a bug until it fails
|
|
55
|
+
* again, but the failure may never come, so a hold has to lapse or the entry parks indefinitely;
|
|
56
|
+
* one week without a recurrence is long enough that a recurring flake would almost surely have
|
|
57
|
+
* shown up, and short enough that a bug which never returns stops squatting in the backlog as
|
|
58
|
+
* an ok. An opinionated default with no knob, like STALE_BUG_DAYS. */
|
|
59
|
+
export const STALE_WAIT_DAYS = 7;
|
|
55
60
|
/** The `(and N more …)` suffix three one-line doctor details append after naming their first
|
|
56
61
|
* suspect (fix claims, stranded plans, duplicated headings): the rest stay visible but
|
|
57
62
|
* shortened, since a doctor check renders one line. `render` makes each extra item's short
|
|
@@ -59,7 +64,7 @@ const STALE_BUG_DAYS = 3;
|
|
|
59
64
|
* FIX_CLAIM_HEADING_MAX, the duplicate-headings site passes its items through as built;
|
|
60
65
|
* `noun` names what the extras are ("record(s)" for fix claims, empty for the other two).
|
|
61
66
|
* Empty string when there are no extras. */
|
|
62
|
-
function andMore(rest, render, noun = "") {
|
|
67
|
+
export function andMore(rest, render, noun = "") {
|
|
63
68
|
return rest.length > 0
|
|
64
69
|
? ` (and ${rest.length} more${noun ? ` ${noun}` : ""}: ${rest.map(render).join(", ")})`
|
|
65
70
|
: "";
|
|
@@ -67,7 +72,7 @@ function andMore(rest, render, noun = "") {
|
|
|
67
72
|
/** Read one backlog file for a check, or the failure message explaining why it could not be
|
|
68
73
|
* read (text.ts's errorMessage). Callers shape the failure themselves — a returned warn for a
|
|
69
74
|
* single-file check, an "(unreadable)" list entry for the multi-file heading sweep. */
|
|
70
|
-
function readDoc(p) {
|
|
75
|
+
export function readDoc(p) {
|
|
71
76
|
try {
|
|
72
77
|
return { doc: fs.readFileSync(p, "utf8") };
|
|
73
78
|
}
|
|
@@ -75,10 +80,11 @@ function readDoc(p) {
|
|
|
75
80
|
return { error: errorMessage(err) };
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
|
-
/** The single-file check's read prologue
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
83
|
+
/** The single-file check's read prologue: an absent file is that check's own "nothing to
|
|
84
|
+
* verify" ok outcome, an unreadable one a warn naming the file. Returns the document, or the
|
|
85
|
+
* outcome the caller should return as-is. The per-file doctor checks go through readBacklogChecked.
|
|
86
|
+
* */
|
|
87
|
+
export function readDocChecked(p, file, missingDetail) {
|
|
82
88
|
if (!fs.existsSync(p))
|
|
83
89
|
return okOutcome(missingDetail);
|
|
84
90
|
const read = readDoc(p);
|
|
@@ -86,242 +92,26 @@ function readDocChecked(p, file, missingDetail) {
|
|
|
86
92
|
return warnOutcome(`cannot read ${file} — ${read.error}`);
|
|
87
93
|
return read.doc;
|
|
88
94
|
}
|
|
89
|
-
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
* legitimate. A warn, never a fail (the checkFallbackModel precedent): a suspicious record is
|
|
98
|
-
* operator signal, not a broken environment. */
|
|
99
|
-
export function checkFixClaims(root) {
|
|
100
|
-
const doc = readDocChecked(path.join(root, "BUGS.md"), "BUGS.md", "no BUGS.md — nothing to verify");
|
|
101
|
-
if (typeof doc !== "string")
|
|
102
|
-
return doc;
|
|
103
|
-
// One parse feeds every record's title and body: bugEntryBody re-parsed the whole document
|
|
104
|
-
// per heading, and BUGS.md grows without bound, so the newest-10 scan paid 1 + 10 full-document
|
|
105
|
-
// walks where one suffices.
|
|
106
|
-
const records = fixedEntriesAt(root, doc).slice(0, FIX_CLAIMS_CHECKED);
|
|
107
|
-
// The haystack walks src/, test/ and scripts/: build it only once a record names something.
|
|
108
|
-
let haystack;
|
|
109
|
-
const phantoms = [];
|
|
110
|
-
for (const { title: heading, body } of records) {
|
|
111
|
-
const symbols = fixSymbols(body);
|
|
112
|
-
if (symbols.length === 0)
|
|
113
|
-
continue;
|
|
114
|
-
haystack ??= sourceHaystack(root);
|
|
115
|
-
const missing = unbackedSymbols(root, symbols, haystack);
|
|
116
|
-
if (missing.length === symbols.length)
|
|
117
|
-
phantoms.push({ heading, missing });
|
|
118
|
-
}
|
|
119
|
-
const [first, ...rest] = phantoms;
|
|
120
|
-
if (!first)
|
|
121
|
-
return okOutcome(`newest ${records.length} Fixed record(s) name code that exists on this tree`);
|
|
122
|
-
// falseFixReason's message shape: the heading, then up to 3 missing names.
|
|
123
|
-
const { heading, missing } = first;
|
|
124
|
-
const names = missingSymbolNames(missing);
|
|
125
|
-
// Every other suspect is still named, shortened: a doctor check is one line.
|
|
126
|
-
const more = andMore(rest, (p) => `"${truncate(p.heading, FIX_CLAIM_HEADING_MAX)}"`, "record(s)");
|
|
127
|
-
return warnOutcome(`BUGS.md records "${heading}" as Fixed, but none of the symbols its Fix paragraph names ` +
|
|
128
|
-
`exist on this tree: ${names}${more} — land the fix or keep the bug Open / refresh a stale record`);
|
|
129
|
-
}
|
|
130
|
-
/** The fixed-record validation-gap twin of checkFixClaims, standing in for plans/repair-traces.md's
|
|
131
|
-
* invariant 2 ("the vocabulary is closed and lives in one place"): the bugfix guidance tells every
|
|
132
|
-
* run to write exactly one `**Validation gap:**` line with a known tag, and `none` rather than no
|
|
133
|
-
* line at all so a tally's denominator stays honest. Nothing checks that a Fixed entry actually
|
|
134
|
-
* carries such a line or that its tag is one of the closed vocabulary — a typo like `no-reprod`
|
|
135
|
-
* lands as its own key on `tumwater report`'s `Validation gaps` line and matches no
|
|
136
|
-
* closed-vocabulary consumer that enumerates the six non-`none` tags, and an entry with no line at
|
|
137
|
-
* all is invisible to every one of them. Re-reads the newest Fixed records with the same
|
|
138
|
-
* FIX_CLAIMS_CHECKED bound (pre-convention history would otherwise warn forever) and warns on a
|
|
139
|
-
* missing or unrecognized tag. An entry declaring `unclear-invariant` should also have pinned the
|
|
140
|
-
* reconstructed guarantee in docs/invariants.md in the same commit — a fix that names no such
|
|
141
|
-
* section warns with that nudge (informational: code-pinned or prose-that-skips-the-filename fixes
|
|
142
|
-
* may spuriously warn). A warn, never a fail, like its sibling: a suspect record is
|
|
143
|
-
* operator signal, not a broken environment. */
|
|
144
|
-
export function checkValidationGaps(root) {
|
|
145
|
-
const doc = readDocChecked(path.join(root, "BUGS.md"), "BUGS.md", "no BUGS.md — nothing to verify");
|
|
146
|
-
if (typeof doc !== "string")
|
|
147
|
-
return doc;
|
|
148
|
-
const records = fixedEntriesAt(root, doc).slice(0, FIX_CLAIMS_CHECKED);
|
|
149
|
-
const suspects = [];
|
|
150
|
-
const unpinned = [];
|
|
151
|
-
for (const { title, body } of records) {
|
|
152
|
-
const tag = validationGapTag(body);
|
|
153
|
-
if (tag === null)
|
|
154
|
-
suspects.push({ title, problem: "missing" });
|
|
155
|
-
else if (!VALIDATION_GAP_TAGS.includes(tag))
|
|
156
|
-
suspects.push({ title, problem: "unrecognized", tag });
|
|
157
|
-
else if (tag === "unclear-invariant" && !body.toLowerCase().includes("docs/invariants.md"))
|
|
158
|
-
unpinned.push({ title });
|
|
159
|
-
}
|
|
160
|
-
const [first, ...rest] = suspects;
|
|
161
|
-
if (!first) {
|
|
162
|
-
if (unpinned.length > 0) {
|
|
163
|
-
const up = unpinned[0];
|
|
164
|
-
const more = andMore(unpinned.slice(1), (s) => `"${truncate(s.title, FIX_CLAIM_HEADING_MAX)}"`, "record(s)");
|
|
165
|
-
return warnOutcome(`BUGS.md record "${up.title}" is an \`unclear-invariant\` fix but names no ` +
|
|
166
|
-
`docs/invariants.md section — record the reconstructed guarantee there (a "Pinned by" line) in the ` +
|
|
167
|
-
`same commit so the next bug in that area does not reconstruct it again${more}`);
|
|
168
|
-
}
|
|
169
|
-
return okOutcome(`newest ${records.length} Fixed record(s) carry a known Validation gap tag`);
|
|
170
|
-
}
|
|
171
|
-
const more = andMore(rest, (s) => `"${truncate(s.title, FIX_CLAIM_HEADING_MAX)}"`, "record(s)");
|
|
172
|
-
const what = first.problem === "missing"
|
|
173
|
-
? `BUGS.md record "${first.title}" has no **Validation gap:** line`
|
|
174
|
-
: `BUGS.md record "${first.title}" names unrecognized Validation gap tag "${first.tag}"`;
|
|
175
|
-
const valid = VALIDATION_GAP_TAGS.join(", ");
|
|
176
|
-
return warnOutcome(`${what}${more} — every Fixed entry writes exactly one of: ${valid}; ` +
|
|
177
|
-
`write \`none\` rather than omitting the line (the bugfix role records it; see plans/repair-traces.md)`);
|
|
178
|
-
}
|
|
179
|
-
/** Stranded plans — plan headings filed under the wrong PLANS.md section (the stranded-plan
|
|
180
|
-
* detector, src/backlog/backlog-structure.ts): a `(planned …)` heading sitting under `## Done` is
|
|
181
|
-
* invisible to every Planned reader, and a done-dated heading still under `## Planned` invites
|
|
182
|
-
* a second implementation. The clean loop repairs these when it happens to tick; this makes
|
|
183
|
-
* the state visible to an operator without waiting for that tick. Reads the tree at `root` —
|
|
184
|
-
* the primary checkout IS main's tree — like checkFixClaims. A warn, never a fail: a misplaced
|
|
185
|
-
* heading is operator signal, not a broken environment. */
|
|
186
|
-
export function checkStrandedPlans(root) {
|
|
187
|
-
const doc = readDocChecked(path.join(root, "PLANS.md"), "PLANS.md", "no PLANS.md — nothing to verify");
|
|
188
|
-
if (typeof doc !== "string")
|
|
189
|
-
return doc;
|
|
190
|
-
// PLANS-DONE.md too: finished plans are archived there (plans/backlog-archive.md), and a plan
|
|
191
|
-
// filed under its ## Done is as invisible as one under PLANS.md's. Each entry names its file.
|
|
192
|
-
const stranded = strandedPlanEntriesFor(root);
|
|
193
|
-
const clean = "no plan headings filed under the wrong PLANS.md section";
|
|
194
|
-
if (stranded.length === 0)
|
|
195
|
-
return okOutcome(clean);
|
|
196
|
-
const [first, ...rest] = stranded;
|
|
197
|
-
if (!first)
|
|
198
|
-
return okOutcome(clean);
|
|
199
|
-
const more = andMore(rest, (e) => `"${truncate(e.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
200
|
-
const file = first.file ?? "PLANS.md";
|
|
201
|
-
return warnOutcome(`${file} has a plan stranded under ## ${first.section}: "${first.title}"${more} — ` +
|
|
202
|
-
`move it under ## ${first.section === "Done" ? "Planned" : "Done"}${file === "PLANS.md" ? "" : " in PLANS.md"} ` +
|
|
203
|
-
"(the clean role repairs these)");
|
|
204
|
-
}
|
|
205
|
-
/** Finished history still in the live backlog files (plans/backlog-archive.md): entries under
|
|
206
|
-
* PLANS.md's `## Done` or BUGS.md's `## Fixed` that have not moved to PLANS-DONE.md or
|
|
207
|
-
* BUGS-FIXED.md. Every run that reads the live file pays for them. A starting team archives them
|
|
208
|
-
* unless that is refused (a dirty file, a checkout off the base branch), and a hand commit can put
|
|
209
|
-
* new ones there between starts, so this names how many there are and the fix. A warn, never a
|
|
210
|
-
* fail: the team works either way, only more expensively. */
|
|
211
|
-
export function checkLiveFinished(root) {
|
|
212
|
-
const live = liveFinishedHistory(root);
|
|
213
|
-
if (live.length === 0)
|
|
214
|
-
return okOutcome("PLANS.md and BUGS.md hold no finished entries — they are archived");
|
|
215
|
-
const parts = live.map((l) => `${l.file} ## ${l.section} holds ${plural(l.entries, "finished entry", "finished entries")} (${Math.round(l.bytes / 1024)} KB)`);
|
|
216
|
-
return warnOutcome(`${parts.join("; ")} — every run that reads the file pays for them; ` +
|
|
217
|
-
"`tumwater archive` moves them to the archive files and commits them (a starting team does too)");
|
|
218
|
-
}
|
|
219
|
-
/** Duplicated `## ` headings already on main (the duplicate-heading half of the backlog-structure
|
|
220
|
-
* check, src/backlog/backlog-structure.ts): a `## Done` a past merge or conflict resolution added
|
|
221
|
-
* twice leaves every later entry after the first section invisible to the section readers, and
|
|
222
|
-
* nothing repairs it on its own. Reads the tree at `root` — the primary checkout IS main's tree —
|
|
223
|
-
* like checkFixClaims. A warn, never a fail: existing damage is operator signal, not a broken
|
|
224
|
-
* environment, and a merge that adds another copy of the heading trips the gate's same rule (a) —
|
|
225
|
-
* src/backlog/backlog-structure.ts fires only when a count EXCEEDS the merge-base's, so an edit
|
|
226
|
-
* that leaves an existing duplicate alone passes and the duplicate itself needs a deliberate
|
|
227
|
-
* removal edit. */
|
|
228
|
-
export function checkBacklogHeadings(root) {
|
|
229
|
-
const duplicates = [];
|
|
230
|
-
for (const file of BACKLOG_FILES) {
|
|
231
|
-
const p = path.join(root, file);
|
|
232
|
-
if (!fs.existsSync(p))
|
|
233
|
-
continue; // Absent file (early repo): contributes nothing.
|
|
234
|
-
const read = readDoc(p);
|
|
235
|
-
if ("error" in read) {
|
|
236
|
-
duplicates.push(`${file} (unreadable)`);
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
const { doc } = read;
|
|
240
|
-
for (const title of duplicateHeadings(doc))
|
|
241
|
-
duplicates.push(`${file} "## ${title}"`);
|
|
242
|
-
}
|
|
243
|
-
if (duplicates.length === 0)
|
|
244
|
-
return okOutcome("no duplicated ## section headings in the backlog files");
|
|
245
|
-
const [first, ...rest] = duplicates;
|
|
246
|
-
const more = andMore(rest, (d) => d);
|
|
247
|
-
return warnOutcome(`${first}${more} appears more than once — readers take the first section of a name, so ` +
|
|
248
|
-
`later entries are invisible; keep one heading per name (the gate blocks only a merge ` +
|
|
249
|
-
`that adds another copy — remove the duplicate in a deliberate edit)`);
|
|
250
|
-
}
|
|
251
|
-
/** Stalled plans — planned plans held by a **Needs review …** or **Needs replan …** note,
|
|
252
|
-
* parked until the plan role splits or replans them. No loop takes a held entry (the claim
|
|
253
|
-
* gate refuses one) and nothing clears the note on its own, so the state is visible only as
|
|
254
|
-
* the `[needs review]`/`[needs replan]` marks in the backlog listings; this names them so an
|
|
255
|
-
* operator gets a single-line health signal from `tumwater doctor`. Reads the tree at `root`
|
|
256
|
-
* — the primary checkout IS main's tree — like checkRefusedEntries, which is also where
|
|
257
|
-
* refusal notes (a different hold) are surfaced. entryHold (src/backlog/backlog-eligibility.ts)
|
|
258
|
-
* already classifies the notes, so this check adds no parsing: only the planned entries whose
|
|
259
|
-
* hold is exactly needs-review or needs-replan are collected. A warn, never a fail — the
|
|
260
|
-
* checkRefusedEntries precedent: a parked plan is operator signal, not a broken environment,
|
|
261
|
-
* and the plan role resolves it itself. */
|
|
262
|
-
export function checkStalledPlans(root) {
|
|
263
|
-
const planned = plannedPlanEntries(root);
|
|
264
|
-
const stalled = planned.filter((entry) => {
|
|
265
|
-
const hold = entryHold(entry, planned);
|
|
266
|
-
return hold === "needs-review" || hold === "needs-replan";
|
|
267
|
-
});
|
|
268
|
-
const [first, ...rest] = stalled;
|
|
269
|
-
if (!first)
|
|
270
|
-
return okOutcome("no planned plan is held by a Needs-review or Needs-replan note");
|
|
271
|
-
const more = andMore(rest, (e) => `"${truncate(e.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
272
|
-
const hold = entryHold(first, planned);
|
|
273
|
-
const note = (first.body.split(/\n/).find((line) => line.trimStart().startsWith("**Needs review ") || line.trimStart().startsWith("**Needs replan ")) ?? "").replace(/^\s*\*\*/, "").replace("**", "");
|
|
274
|
-
return warnOutcome(`PLANS.md plan "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" is held by a ${hold === "needs-review" ? "Needs review" : "Needs replan"} note ` +
|
|
275
|
-
`(${truncate(note, REFUSAL_NOTE_MAX)})${more} — the plan role resolves it by splitting or replanning`);
|
|
276
|
-
}
|
|
277
|
-
/** Plan dependencies — mistakes in the planned plans' `requires … landed` clauses that the claim
|
|
278
|
-
* allocator (entryHoldAgainst) cannot see: a ref naming no heading (the hold is void, so the
|
|
279
|
-
* dependent is claimable now), a clause that parses to no refs (it holds nothing), and a
|
|
280
|
-
* dependency cycle (every part on it stays blocked forever). Reads the tree at `root` through
|
|
281
|
-
* planDependencyProblems. A warn, never a fail, like checkStalledPlans: the plan role writes the
|
|
282
|
-
* clauses by hand and fixes them itself. The most serious kind is named — cycle, then dangling,
|
|
283
|
-
* then unparsed — with a count of the other kinds. */
|
|
284
|
-
export function checkPlanDependencies(root) {
|
|
285
|
-
const { cycle, dangling, unparsed, misworded } = planDependencyProblems(plannedPlanEntries(root), donePlans(root));
|
|
286
|
-
const [firstDangling, ...restDangling] = dangling;
|
|
287
|
-
const [firstUnparsed, ...restUnparsed] = unparsed;
|
|
288
|
-
const [firstMisworded, ...restMisworded] = misworded;
|
|
289
|
-
const quote = (t) => `"${truncate(t, FIX_CLAIM_HEADING_MAX)}"`;
|
|
290
|
-
const others = (parts) => parts.some(([n]) => n > 0) ? ` (also ${parts.map(([n, noun]) => `${n} ${noun}`).join(", ")})` : "";
|
|
291
|
-
const danglingMsg = (d) => `PLANS.md plan ${quote(d.title)} requires ${d.ref}, which no PLANS.md heading carries — the hold is void, ` +
|
|
292
|
-
"so the plan is claimable now; fix the series name or part number";
|
|
293
|
-
const unparsedMsg = (t) => `PLANS.md plan ${quote(t)} has a \`requires … landed\` clause that names no part the allocator can read — ` + // vocabulary-ok: PLANS.md heading syntax
|
|
294
|
-
"it holds nothing; write each ref as `<Series> i/n` or give the heading a `<Series>, part i/n:` prefix";
|
|
295
|
-
const miswordedMsg = (w) => `PLANS.md plan ${quote(w.title)} names ${w.ref} as a prerequisite without a \`requires … landed\` clause — ` + // vocabulary-ok: PLANS.md heading syntax
|
|
296
|
-
"the claim allocator ignores that wording and hands the part out now; write it as `requires part i/n landed`"; // vocabulary-ok: PLANS.md heading syntax
|
|
297
|
-
if (cycle !== null) {
|
|
298
|
-
return warnOutcome(`plan dependency cycle: ${cycle.join(" → ")} — every part on it stays blocked forever; ` +
|
|
299
|
-
"edit one heading's `requires … landed` clause to break it" + // vocabulary-ok: PLANS.md heading syntax
|
|
300
|
-
others([[dangling.length, "dangling ref(s)"], [unparsed.length, "unparsed clause(s)"], [misworded.length, "misworded dependency(ies)"]]));
|
|
301
|
-
}
|
|
302
|
-
if (firstDangling) {
|
|
303
|
-
return warnOutcome(danglingMsg(firstDangling) +
|
|
304
|
-
andMore(restDangling, (d) => `${quote(d.title)} → ${d.ref}`, "ref(s)") +
|
|
305
|
-
others([[unparsed.length, "unparsed clause(s)"], [misworded.length, "misworded dependency(ies)"]]));
|
|
306
|
-
}
|
|
307
|
-
if (firstUnparsed) {
|
|
308
|
-
return warnOutcome(unparsedMsg(firstUnparsed) + andMore(restUnparsed, quote, "clause(s)") +
|
|
309
|
-
others([[misworded.length, "misworded dependency(ies)"]]));
|
|
310
|
-
}
|
|
311
|
-
if (firstMisworded) {
|
|
312
|
-
return warnOutcome(miswordedMsg(firstMisworded) +
|
|
313
|
-
andMore(restMisworded, (w) => `${quote(w.title)} → ${w.ref}`, "dependency(ies)"));
|
|
314
|
-
}
|
|
315
|
-
return okOutcome("every planned plan's requires clause names a known part, with no cycle or misworded dependency");
|
|
95
|
+
/** The standard single-file check's read prologue for a backlog document at `root`: the
|
|
96
|
+
* readDocChecked call every per-file doctor check (checkFixClaims, checkValidationGaps,
|
|
97
|
+
* checkStrandedPlans, checkStaleQuestions, and the stale/waiting bug checks) repeats, with the
|
|
98
|
+
* `no <file> — nothing to verify` missing-detail derived from the file name so the file triple
|
|
99
|
+
* (path, display name, message) and the message template cannot drift across the seven call
|
|
100
|
+
* sites. Returns the document, or the outcome the caller should return as-is. */
|
|
101
|
+
export function readBacklogChecked(root, file) {
|
|
102
|
+
return readDocChecked(path.join(root, file), file, `no ${file} — nothing to verify`);
|
|
316
103
|
}
|
|
317
104
|
/** How long a queued merge may wait without its change starting before the stale-queued-merge check
|
|
318
105
|
* warns: 6 hours, fixed. A verify plus a merge completes in minutes (plans/merge-queue.md), so a
|
|
319
106
|
* full six-hour silence while the team is healthy — not on a hold, which doctor already reports
|
|
320
107
|
* separately — is never a merely busy pipeline: it reads as a drain that has stalled. The one
|
|
321
|
-
* opinionated default, like STALE_QUESTION_DAYS, so no knob.
|
|
322
|
-
|
|
108
|
+
* opinionated default, like STALE_QUESTION_DAYS, so no knob. The always-on `stale-merges` team
|
|
109
|
+
* alert (src/ui/status-payload.ts staleMerges) reuses this same window through
|
|
110
|
+
* staleQueuedMergeRows, so the doctor's row and the alert cannot drift on the threshold (the
|
|
111
|
+
* STALE_PROMPT_MS/STALE_BUG_DAYS precedent). */
|
|
112
|
+
export const MERGE_QUEUE_STALE_MS = 6 * HOUR_MS;
|
|
323
113
|
/** The window of the merge-wait phrase on the stale merge-queue row. */
|
|
324
|
-
const MERGE_WAIT_WINDOW_MS =
|
|
114
|
+
const MERGE_WAIT_WINDOW_MS = DAY_MS;
|
|
325
115
|
/** Stale queued merges — a change that has sat in the durable merge queue too long without starting
|
|
326
116
|
* (plans/merge-queue.md). Every queued-change health fact is otherwise a hook into its own role or
|
|
327
117
|
* stage: a verify under way reads in the team row, a team hold gets its own banner, and
|
|
@@ -334,7 +124,7 @@ const MERGE_WAIT_WINDOW_MS = 24 * 3600_000;
|
|
|
334
124
|
* holding a change being verified or merged right now — a slow review is not a stalled drain, and
|
|
335
125
|
* an absent or never-throwing unreadable marker contributes the empty set. Reads the tree at `root`
|
|
336
126
|
* with the already-exported readers, changing nothing in src/merge/. A warn, never a fail — the
|
|
337
|
-
* checkStaleQuestions/
|
|
127
|
+
* checkStaleQuestions/checkStalledEntries precedent: a queued wait is legitimate while the team is
|
|
338
128
|
* genuinely held, and those holds already warn separately. */
|
|
339
129
|
export function checkStaleQueuedMerges(root, now) {
|
|
340
130
|
const withWait = (out) => {
|
|
@@ -347,11 +137,7 @@ export function checkStaleQueuedMerges(root, now) {
|
|
|
347
137
|
}
|
|
348
138
|
return phrase ? { ...out, detail: `${out.detail} · ${phrase}` } : out;
|
|
349
139
|
};
|
|
350
|
-
const
|
|
351
|
-
const inFlight = new Set(marker ? mergeChanges(marker).map((c) => c.role) : []);
|
|
352
|
-
const stale = queuedMerges(root)
|
|
353
|
-
.filter((e) => !inFlight.has(e.role) && now - e.enqueuedAt > MERGE_QUEUE_STALE_MS)
|
|
354
|
-
.map((e) => ({ role: e.role, summary: e.summary, age: humanSeconds(secondsSince(e.enqueuedAt, now)) }));
|
|
140
|
+
const stale = staleQueuedMergeRows(root, now).map((e) => ({ ...e, age: humanSeconds(secondsSince(now - e.waitedMs, now)) }));
|
|
355
141
|
const [first, ...rest] = stale;
|
|
356
142
|
if (!first)
|
|
357
143
|
return withWait(okOutcome(`no merge-queue entry has waited more than ${humanSeconds(MERGE_QUEUE_STALE_MS / 1000)} to start`));
|
|
@@ -359,27 +145,125 @@ export function checkStaleQueuedMerges(root, now) {
|
|
|
359
145
|
return withWait(warnOutcome(`merge-queue entry of ${roleLabel(first.role)} — "${truncate(first.summary, FIX_CLAIM_HEADING_MAX)}" — has waited ` +
|
|
360
146
|
`${first.age} to start${more}; a queued change the pipeline neither approves nor flags reads as healthy, so the merge queue has stopped moving`));
|
|
361
147
|
}
|
|
148
|
+
/** The one traversal shared by the doctor's `stale merge-queue entries` row
|
|
149
|
+
* (checkStaleQueuedMerges) and the always-on `stale-merges` team alert
|
|
150
|
+
* (src/ui/status-payload.ts staleMerges): every queued merge whose role is not in the in-flight
|
|
151
|
+
* marker and whose wait strictly exceeds MERGE_QUEUE_STALE_MS (6 h), in queue-file order (oldest
|
|
152
|
+
* first). The in-flight set comes from readMergeMarker via mergeChanges (an absent or
|
|
153
|
+
* never-throwing unreadable marker contributes the empty set) — a slow review is not a stalled
|
|
154
|
+
* drain. Each row carries { role, summary, waitedMs }; the doctor's row adds its age phrase and
|
|
155
|
+
* the alert names the wait, so the rule and its threshold live in one place and cannot drift. */
|
|
156
|
+
export function staleQueuedMergeRows(root, now) {
|
|
157
|
+
const marker = readMergeMarker(root);
|
|
158
|
+
const inFlight = new Set(marker ? mergeChanges(marker).map((c) => c.role) : []);
|
|
159
|
+
return queuedMerges(root)
|
|
160
|
+
.filter((e) => !inFlight.has(e.role) && now - e.enqueuedAt > MERGE_QUEUE_STALE_MS)
|
|
161
|
+
.map((e) => ({ role: e.role, summary: e.summary, waitedMs: now - e.enqueuedAt }));
|
|
162
|
+
}
|
|
163
|
+
/** The merge-queue files `readMergeQueue` skips — torn, foreign, or vanished mid-listing — as
|
|
164
|
+
* checkout-relative paths. One traversal shared by the doctor's `checkMergeQueueFiles` row and
|
|
165
|
+
* the always-on `skipped-merges` team alert (src/ui/status-payload.ts skippedMergeFiles), so
|
|
166
|
+
* the two surfaces select the same files and name the same paths and cannot drift. */
|
|
167
|
+
export function skippedMergeQueueFiles(root) {
|
|
168
|
+
return unreadableMergeQueueFiles(root).map((f) => path.relative(root, f));
|
|
169
|
+
}
|
|
170
|
+
/** The merge-queue files `readMergeQueue` skips — torn, foreign, or vanished mid-listing —
|
|
171
|
+
* named so a stranded queue entry is a health signal instead of only a number on `tumwater
|
|
172
|
+
* queue`'s output. It exists because `tumwater queue` counts the skips but does not name them,
|
|
173
|
+
* and the drain drops only an unreadable head, so a non-head skip is otherwise silent: it sits
|
|
174
|
+
* in `.tumwater/merge-queue/` forever, never drained, and neither the queue reader nor the
|
|
175
|
+
* stale-queued-merges doctor row (which reads only readable entries) surfaces it. Warn, never
|
|
176
|
+
* fail, like the module's other document checks: a foreign file is operator signal, not a
|
|
177
|
+
* broken environment. */
|
|
178
|
+
export function checkMergeQueueFiles(root) {
|
|
179
|
+
const files = skippedMergeQueueFiles(root);
|
|
180
|
+
if (files.length === 0)
|
|
181
|
+
return okOutcome("the queue reader skips no merge-queue file");
|
|
182
|
+
const [first, ...rest] = files;
|
|
183
|
+
const more = andMore(rest, (f) => f, "file(s)");
|
|
184
|
+
return warnOutcome(`the merge queue holds ${plural(files.length, "file")} the reader skips — ${first}${more}; ` +
|
|
185
|
+
`a torn or foreign file is never cleared, so nothing ages it or surfaces its merge — remove it (a crashed enqueue's merge ref still recovers on the next run)`);
|
|
186
|
+
}
|
|
187
|
+
/** The failures count at which a post-merge review is one failed run from being dropped: one
|
|
188
|
+
* below POST_REVIEW_FAILURE_LIMIT, so the drain's drop limit and this at-risk bar cannot drift
|
|
189
|
+
* (the STALE_BUG_DAYS precedent). A review at this bar has as many unreviewed failures as the
|
|
190
|
+
* drain allows before it drops the commit from the queue forever. */
|
|
191
|
+
export const POST_REVIEW_AT_RISK_FAILURES = POST_REVIEW_FAILURE_LIMIT - 1;
|
|
192
|
+
/** The post-merge reviews one failed run from being dropped, in file (FIFO) order: every entry
|
|
193
|
+
* whose `failures` is at least POST_REVIEW_AT_RISK_FAILURES, each with `commit`, `role`,
|
|
194
|
+
* `summary` and `failures` carried out (a missing failures reads as 0, so it is never at risk).
|
|
195
|
+
* A review that has already been rejected is left out: the drain settles a rejection as
|
|
196
|
+
* `{ ...entry, rejected }` (it preserves `failures`) and never re-runs it, so a rejected commit
|
|
197
|
+
* is not waiting on a verdict and must not read as one failed run from being dropped.
|
|
198
|
+
* A review that has already landed its fix (`fixed` set) is left out too — it is a settled
|
|
199
|
+
* record naming both shas, not a verdict-less entry the next run could drop.
|
|
200
|
+
* One traversal shared by the doctor's `post-merge reviews` row (checkPostReviews) and the
|
|
201
|
+
* always-on `post-merge-risk` team alert (src/ui/status-payload.ts postReviewRiskAlert), so the
|
|
202
|
+
* two surfaces select the same entries and cannot drift. Reads .tumwater/state/post-review.json
|
|
203
|
+
* through readPostReviews, which degrades a missing or torn file to the well-formed subset or
|
|
204
|
+
* `[]` and never throws. */
|
|
205
|
+
export function atRiskPostReviews(root) {
|
|
206
|
+
return readPostReviews(root)
|
|
207
|
+
.filter((e) => e.rejected === undefined && e.fixed === undefined && (e.failures ?? 0) >= POST_REVIEW_AT_RISK_FAILURES)
|
|
208
|
+
.map((e) => ({ commit: e.commit, role: e.role, summary: e.summary, failures: e.failures ?? 0 }));
|
|
209
|
+
}
|
|
210
|
+
/** The doctor's one sentence naming the post-merge reviews one failed run from being dropped
|
|
211
|
+
* (the checkStaleQueuedMerges phrase precedent): the first in full — role, short commit, summary
|
|
212
|
+
* and the failures count — the rest folded through andMore, and the one sentence every surface
|
|
213
|
+
* shows, so the doctor row and the alert cannot drift on the wording. null for an empty list. */
|
|
214
|
+
export function atRiskPostReviewsPhrase(rows) {
|
|
215
|
+
const [first, ...rest] = rows;
|
|
216
|
+
if (!first)
|
|
217
|
+
return null;
|
|
218
|
+
const more = andMore(rest, (r) => `${roleLabel(r.role)} ${shortSha(r.commit)}`);
|
|
219
|
+
return `the post-merge review of ${roleLabel(first.role)} ${shortSha(first.commit)} — "${truncate(first.summary, FIX_CLAIM_HEADING_MAX)}" — has ended without a verdict ${first.failures} times${more}; ` +
|
|
220
|
+
`one more failed run drops it and the commit stays on main unreviewed — fix the reviewer backend or accept the commit unreviewed deliberately`;
|
|
221
|
+
}
|
|
222
|
+
/** A post-merge review one failed run from being dropped (src/review/post-merge-review.ts, the
|
|
223
|
+
* drain drops an entry at POST_REVIEW_FAILURE_LIMIT failed runs) — the last moment an operator
|
|
224
|
+
* can fix the reviewer backend or deliberately accept the commit unreviewed before the drain
|
|
225
|
+
* drops it from the queue and it stays on main unreviewed. Reads .tumwater/state/post-review.json
|
|
226
|
+
* through the shared atRiskPostReviews traversal the always-on post-merge-risk team alert
|
|
227
|
+
* (src/ui/status-payload.ts postReviewRiskAlert) uses, so the two surfaces cannot drift. A warn,
|
|
228
|
+
* never a fail — the checkStaleQueuedMerges precedent: a failing reviewer is operator signal,
|
|
229
|
+
* not a broken environment; readPostReviews never throws (a missing or torn file degrades to the
|
|
230
|
+
* well-formed subset or `[]`), so the check needs no read-error arm. */
|
|
231
|
+
export function checkPostReviews(root) {
|
|
232
|
+
const rows = atRiskPostReviews(root);
|
|
233
|
+
const phrase = atRiskPostReviewsPhrase(rows);
|
|
234
|
+
return phrase === null ? okOutcome("no post-merge review is one failed run from being dropped") : warnOutcome(phrase);
|
|
235
|
+
}
|
|
362
236
|
/** How long a queued prompt may wait for its role to dequeue it before the stale-prompt check
|
|
363
237
|
* warns: a day, fixed. An opinionated default with no knob, like MERGE_QUEUE_STALE_MS: a day is
|
|
364
238
|
* long enough that a healthy role's next run would have dequeued the prompt, and short enough
|
|
365
|
-
* that a paused or disabled role's stranded prompt shows up.
|
|
366
|
-
|
|
239
|
+
* that a paused or disabled role's stranded prompt shows up. The always-on stale-prompt team
|
|
240
|
+
* alert (src/ui/status-payload.ts stalePrompts) reuses this same threshold, so it cannot drift
|
|
241
|
+
* from the doctor's check — the STALE_BUG_DAYS precedent. */
|
|
242
|
+
export const STALE_PROMPT_MS = DAY_MS;
|
|
243
|
+
/** The queued inbox prompts the doctor's stale-prompt rule calls stale — deliverable now (a
|
|
244
|
+
* `--at` prompt still counting down is never stale) and queued more than STALE_PROMPT_MS,
|
|
245
|
+
* oldest-first, `queuedAtMs` narrowed to a number. One traversal shared by the doctor's
|
|
246
|
+
* `checkStalePrompts` check and the always-on `stalePrompts` team alert (src/ui/status-payload.ts),
|
|
247
|
+
* so the two surfaces read the same rule at the same threshold and cannot drift. */
|
|
248
|
+
export function staleQueuedPromptRows(root, now) {
|
|
249
|
+
return queuedListRows(root, null, listWideScope(root))
|
|
250
|
+
.filter((p) => p.queuedAtMs !== null && deliverableAt(p.notBeforeMs, now) && now - p.queuedAtMs > STALE_PROMPT_MS)
|
|
251
|
+
.sort((a, b) => a.queuedAtMs - b.queuedAtMs)
|
|
252
|
+
.map((p) => ({ ...p, queuedAtMs: p.queuedAtMs }));
|
|
253
|
+
}
|
|
367
254
|
/** Stale queued prompts — a prompt queued for a role (`tumwater prompt --role <id>`, the
|
|
368
255
|
* dashboard composer, the TUI, a `--at` delay whose time has now passed) that has waited more
|
|
369
256
|
* than a day for that role to dequeue it. A role that is paused, disabled, or failing never
|
|
370
257
|
* reaches the run that would dequeue the prompt, so it waits indefinitely and only the Inbox
|
|
371
258
|
* views (`tumwater prompt --list`, the dashboard Inbox tab, the TUI Inbox pane) show its age —
|
|
372
259
|
* and only when the operator looks. This names the oldest due-but-undelivered prompt, its role,
|
|
373
|
-
* and the deliver-or-drop commands. Reads the queue
|
|
374
|
-
*
|
|
260
|
+
* and the deliver-or-drop commands. Reads the queue through the shared `staleQueuedPromptRows`
|
|
261
|
+
* traversal the always-on stale-prompts alert uses (no new reader in src/inbox/), keeping only
|
|
375
262
|
* rows that are deliverable now — a `--at 7d` prompt still counting down is never stale — and
|
|
376
263
|
* stamped more than STALE_PROMPT_MS ago. A warn, never a fail — the checkStaleQueuedMerges
|
|
377
264
|
* precedent: a stranded prompt is operator signal, not a broken environment. */
|
|
378
265
|
export function checkStalePrompts(root, now) {
|
|
379
|
-
const stale =
|
|
380
|
-
.filter((p) => p.queuedAtMs !== null && deliverableAt(p.notBeforeMs, now) && now - p.queuedAtMs > STALE_PROMPT_MS)
|
|
381
|
-
.sort((a, b) => a.queuedAtMs - b.queuedAtMs)
|
|
382
|
-
.map((p) => ({ ...p, queuedAtMs: p.queuedAtMs }));
|
|
266
|
+
const stale = staleQueuedPromptRows(root, now);
|
|
383
267
|
const [first, ...rest] = stale;
|
|
384
268
|
if (!first)
|
|
385
269
|
return okOutcome("no queued prompt has waited more than 1 day for a role to dequeue it");
|
|
@@ -403,249 +287,102 @@ export function mergeWaitPhrase(events) {
|
|
|
403
287
|
const longest = Math.max(...waits);
|
|
404
288
|
return `merges in the last 24h waited ${shortSpanPhrase(median)} median, ${shortSpanPhrase(longest)} longest (${plural(waits.length, "merge")})`;
|
|
405
289
|
}
|
|
406
|
-
/**
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
* the one age formula, floored at 0; an undated heading is skipped. A warn, never a fail —
|
|
422
|
-
* the checkStalledPlans/checkRefusedEntries precedent: a question awaiting a human is
|
|
423
|
-
* operator signal, not a broken environment. */
|
|
424
|
-
export function checkStaleQuestions(root) {
|
|
425
|
-
const doc = readDocChecked(path.join(root, "QUESTIONS.md"), "QUESTIONS.md", "no QUESTIONS.md — nothing to verify");
|
|
426
|
-
if (typeof doc !== "string")
|
|
427
|
-
return doc;
|
|
428
|
-
const stale = openQuestionEntries(root).flatMap((q) => {
|
|
429
|
-
const stamp = questionFilingDate(q.title);
|
|
430
|
-
if (stamp === null)
|
|
431
|
-
return [];
|
|
432
|
-
const days = ageInDays(stamp);
|
|
433
|
-
return days >= STALE_QUESTION_DAYS ? [{ title: q.title, days }] : [];
|
|
434
|
-
});
|
|
435
|
-
const [first, ...rest] = stale;
|
|
436
|
-
if (!first)
|
|
437
|
-
return okOutcome("no open question has waited more than 7 days for an answer");
|
|
438
|
-
const more = andMore(rest, (q) => `"${truncate(q.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
439
|
-
return warnOutcome(`QUESTIONS.md question "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" has waited ${first.days} d for an answer${more} — ` +
|
|
440
|
-
`a question that waits too long for a human answer is worth your attention`);
|
|
441
|
-
}
|
|
442
|
-
/** Stale bugs — unrefused open BUGS.md bugs whose `(found by … YYYY-MM-DD)` or `(reported …)`
|
|
443
|
-
* stamp is at least 3 days old. The bugfix role fixes a bug within hours, so one that sits means
|
|
444
|
-
* no loop is fixing it. Positions are the 1-based `bug --list` numbers; a refused bug is skipped
|
|
445
|
-
* (checkRefusedEntries names it), a waiting bug is skipped (checkWaitingBugs names it) and so is
|
|
446
|
-
* an undated heading. A warn, never a fail. */
|
|
447
|
-
export function checkStaleBugs(root) {
|
|
448
|
-
const doc = readDocChecked(path.join(root, "BUGS.md"), "BUGS.md", "no BUGS.md — nothing to verify");
|
|
449
|
-
if (typeof doc !== "string")
|
|
450
|
-
return doc;
|
|
451
|
-
const stale = openBugEntries(root).flatMap((entry, index) => {
|
|
452
|
-
if (refusalNote(entry.body) !== null || waitingNote(entry.body) !== null)
|
|
453
|
-
return [];
|
|
454
|
-
const stamp = filingDate(entry.title);
|
|
455
|
-
if (stamp === null)
|
|
456
|
-
return [];
|
|
457
|
-
const days = ageInDays(stamp);
|
|
458
|
-
return days >= STALE_BUG_DAYS ? [{ position: index + 1, title: entry.title, days }] : [];
|
|
459
|
-
});
|
|
460
|
-
const [first, ...rest] = stale;
|
|
461
|
-
if (!first)
|
|
462
|
-
return okOutcome("no open bug has waited more than 3 days for a fix");
|
|
463
|
-
const more = andMore(rest, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
464
|
-
return warnOutcome(`BUGS.md bug #${first.position} "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" has waited ${first.days} d for a fix${more} — ` +
|
|
465
|
-
`no role is fixing it: check that the bugfix role is enabled and not failing, or retract it with \`tumwater bug --cancel ${first.position}\``);
|
|
466
|
-
}
|
|
467
|
-
/** The first span between straight double quotes in a bug heading that is at least 12
|
|
468
|
-
* characters long — the test name a flake bug's heading quotes — or null. The floor stops a
|
|
469
|
-
* short quoted word from matching unrelated failures. */
|
|
470
|
-
export function quotedTestName(title) {
|
|
471
|
-
return /"([^"]{12,})"/.exec(title)?.[1] ?? null;
|
|
472
|
-
}
|
|
473
|
-
/** Local midnight at the end of a `YYYY-MM-DD` day, in ms: the earliest instant a failure can
|
|
474
|
-
* be told apart from the one a same-day note or fix record was written about. NaN for a
|
|
475
|
-
* malformed date. */
|
|
476
|
-
function dayAfterMs(date) {
|
|
477
|
-
return new Date(Number(date.slice(0, 4)), Number(date.slice(5, 7)) - 1, Number(date.slice(8, 10)) + 1).getTime();
|
|
478
|
-
}
|
|
479
|
-
/** The first `YYYY-MM-DD` in a Waiting for the next failure note's text, or null. */
|
|
480
|
-
export function waitingNoteDate(note) {
|
|
481
|
-
return /\d{4}-\d{2}-\d{2}/.exec(note)?.[0] ?? null;
|
|
482
|
-
}
|
|
483
|
-
/** Every event at or after `sinceMs` that records a failure naming `name`: a `warning` whose
|
|
484
|
-
* message includes it (gate and merge flake warnings, a merge check kept for one retry), or a
|
|
485
|
-
* `review_rejected` with a reason that includes it (its text is the first such reason). In
|
|
486
|
-
* event order. */
|
|
487
|
-
export function failureMentions(events, name, sinceMs) {
|
|
488
|
-
return events.flatMap((e) => {
|
|
489
|
-
if (typeof e.ts !== "number" || e.ts < sinceMs)
|
|
490
|
-
return [];
|
|
491
|
-
if (e.type === "warning" && typeof e.message === "string" && e.message.includes(name))
|
|
492
|
-
return [{ ts: e.ts, text: e.message }];
|
|
493
|
-
if (e.type === "review_rejected") {
|
|
494
|
-
const reason = stringList(e.reasons).find((r) => r.includes(name));
|
|
495
|
-
if (reason !== undefined)
|
|
496
|
-
return [{ ts: e.ts, text: reason }];
|
|
497
|
-
}
|
|
498
|
-
return [];
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
/** Waiting bugs — open BUGS.md bugs held by a **Waiting for the next failure …** note, which
|
|
502
|
-
* parks a bug until it fails again. Positions are the 1-based `bug --list` numbers; a bug that
|
|
503
|
-
* is also Refused is left to checkRefusedEntries. Ok when the hold is undisturbed — doctor
|
|
504
|
-
* then only names the bugs and how to release one — and a warn when a held bug's quoted test
|
|
505
|
-
* has failed in a gate or merge check since the day after its note (the note carries only a
|
|
506
|
-
* date, so a same-day failure cannot be told from the one it was written about). A warn is
|
|
507
|
-
* never a fail, and doctor writes nothing to BUGS.md. */
|
|
508
|
-
export function checkWaitingBugs(root) {
|
|
509
|
-
const doc = readDocChecked(path.join(root, "BUGS.md"), "BUGS.md", "no BUGS.md — nothing to verify");
|
|
510
|
-
if (typeof doc !== "string")
|
|
511
|
-
return doc;
|
|
512
|
-
const held = openBugEntries(root).flatMap((entry, index) => {
|
|
513
|
-
const note = waitingNote(entry.body);
|
|
514
|
-
if (note === null || refusalNote(entry.body) !== null)
|
|
515
|
-
return [];
|
|
516
|
-
const name = quotedTestName(entry.title);
|
|
517
|
-
const date = waitingNoteDate(note);
|
|
518
|
-
const since = name !== null && date !== null ? dayAfterMs(date) : null;
|
|
519
|
-
return [{ position: index + 1, title: entry.title, note, name, since }];
|
|
520
|
-
});
|
|
521
|
-
const [first, ...rest] = held;
|
|
522
|
-
if (!first)
|
|
523
|
-
return okOutcome("no open bug is waiting for its next failure");
|
|
524
|
-
const sinces = held.flatMap((b) => (b.since !== null && !Number.isNaN(b.since) ? [b.since] : []));
|
|
525
|
-
if (sinces.length > 0) {
|
|
526
|
-
const { events } = readEventsSince(root, sinceWindowMs(sinces));
|
|
527
|
-
const recurred = held.flatMap((b) => {
|
|
528
|
-
if (b.name === null || b.since === null)
|
|
529
|
-
return [];
|
|
530
|
-
const mentions = failureMentions(events, b.name, b.since);
|
|
531
|
-
const newest = mentions[mentions.length - 1];
|
|
532
|
-
return newest ? [{ ...b, newest }] : [];
|
|
533
|
-
});
|
|
534
|
-
const [again, ...others] = recurred;
|
|
535
|
-
if (again) {
|
|
536
|
-
const more = andMore(others, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
537
|
-
return warnOutcome(`BUGS.md bug #${again.position} "${truncate(again.title, FIX_CLAIM_HEADING_MAX)}" failed again since its Waiting for the next failure note ` +
|
|
538
|
-
`(${formatTimestamp(again.newest.ts)}: ${truncate(again.newest.text, REFUSAL_NOTE_MAX)})${more} — ` +
|
|
539
|
-
`record the occurrence under the entry and release it with \`tumwater bug --release ${again.position}\``);
|
|
540
|
-
}
|
|
290
|
+
/** Discover the tracked backlog/brief files with uncommitted edits in the primary checkout (the
|
|
291
|
+
* shared discovery behind checkUncommittedBacklog and commitUncommittedBacklog). A primary off
|
|
292
|
+
* the base branch is skipped by its callers: the ref-push merge arm leaves its working tree
|
|
293
|
+
* alone. Untracked files are not edits of a tracked file and are ignored. */
|
|
294
|
+
export async function dirtyBacklogFiles(root, config) {
|
|
295
|
+
const primary = await currentBranch(root);
|
|
296
|
+
const base = targetBranch(config?.baseBranch, primary);
|
|
297
|
+
// A primary off the base branch is skipped by every caller before it reads `watched`/`dirty`, so
|
|
298
|
+
// the brief is not touched here either: an unreadable brief must not turn that skip into a throw
|
|
299
|
+
// (the ref-push merge arm leaves an off-base working tree alone).
|
|
300
|
+
if (primary === null || primary !== base)
|
|
301
|
+
return { base, primary, watched: [], dirty: [] };
|
|
302
|
+
let brief = null;
|
|
303
|
+
try {
|
|
304
|
+
brief = briefFile(root);
|
|
541
305
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
`release it now with \`tumwater bug --release ${first.position}\``);
|
|
546
|
-
}
|
|
547
|
-
/** Regressed fixes — a newest-10 BUGS.md Fixed record whose heading quotes a test name that a
|
|
548
|
-
* gate or merge check has failed on since the day after its fix (the heading carries only a
|
|
549
|
-
* date, so a same-day failure cannot be told from the one the fix answered), unless an Open bug
|
|
550
|
-
* already quotes the same test, which means the regression is filed. A warn, never a fail, and
|
|
551
|
-
* doctor writes nothing to BUGS.md. */
|
|
552
|
-
export function checkRegressedFixes(root) {
|
|
553
|
-
const doc = readDocChecked(path.join(root, "BUGS.md"), "BUGS.md", "no BUGS.md — nothing to verify");
|
|
554
|
-
if (typeof doc !== "string")
|
|
555
|
-
return doc;
|
|
556
|
-
const candidates = fixedEntriesAt(root, doc).slice(0, FIX_CLAIMS_CHECKED).flatMap((entry) => {
|
|
557
|
-
const name = quotedTestName(entry.title);
|
|
558
|
-
const date = lastDate(entry.title, /(\d{4}-\d{2}-\d{2})/);
|
|
559
|
-
if (name === null || date === null)
|
|
560
|
-
return [];
|
|
561
|
-
const since = dayAfterMs(date);
|
|
562
|
-
return Number.isNaN(since) ? [] : [{ title: entry.title, name, date, since }];
|
|
563
|
-
});
|
|
564
|
-
const open = candidates.length > 0 ? openBugEntries(root).map((e) => quotedTestName(e.title)) : [];
|
|
565
|
-
const unfiled = candidates.filter((c) => !open.includes(c.name));
|
|
566
|
-
if (unfiled.length > 0) {
|
|
567
|
-
const { events } = readEventsSince(root, sinceWindowMs(unfiled.map((c) => c.since)));
|
|
568
|
-
const regressed = unfiled.flatMap((c) => {
|
|
569
|
-
const mentions = failureMentions(events, c.name, c.since);
|
|
570
|
-
const newest = mentions[mentions.length - 1];
|
|
571
|
-
return newest ? [{ ...c, newest }] : [];
|
|
572
|
-
});
|
|
573
|
-
const [first, ...rest] = regressed;
|
|
574
|
-
if (first) {
|
|
575
|
-
const more = andMore(rest, (c) => `"${truncate(c.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
576
|
-
return warnOutcome(`BUGS.md Fixed record "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" quotes a test that failed again after its ${first.date} fix ` +
|
|
577
|
-
`(${formatTimestamp(first.newest.ts)}: ${truncate(first.newest.text, REFUSAL_NOTE_MAX)})${more} — ` +
|
|
578
|
-
"file it as a new bug with `tumwater bug`, quoting the test name and citing the Fixed record");
|
|
579
|
-
}
|
|
306
|
+
catch {
|
|
307
|
+
// An unreadable brief is a doctor finding of its own; it must not abort the uncommitted-backlog
|
|
308
|
+
// check or `--commit-backlog`, so the tracked backlog set is watched without it.
|
|
580
309
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
* environment. The warning also counts the planned parts a refused series part holds. */
|
|
590
|
-
export function checkRefusedEntries(root) {
|
|
591
|
-
const planned = plannedPlanEntries(root);
|
|
592
|
-
const parts = plannedPartKeys(planned);
|
|
593
|
-
const sources = [
|
|
594
|
-
{ file: "PLANS.md", command: "plan", entries: planned },
|
|
595
|
-
{ file: "BUGS.md", command: "bug", entries: openBugEntries(root) },
|
|
596
|
-
];
|
|
597
|
-
const held = sources.flatMap(({ file, command, entries }) => entries.flatMap((entry, index) => {
|
|
598
|
-
const note = refusalNote(entry.body);
|
|
599
|
-
if (note === null)
|
|
600
|
-
return [];
|
|
601
|
-
const mergeDone = prerequisitesMerged(entry.title, parts);
|
|
602
|
-
const waiting = file === "PLANS.md" ? plansWaitingOn(planned, index).length : 0;
|
|
603
|
-
return [{ file, command, position: index + 1, title: entry.title, note, mergeDone, waiting }];
|
|
604
|
-
}));
|
|
605
|
-
const [first, ...rest] = held;
|
|
606
|
-
if (!first)
|
|
607
|
-
return okOutcome("no planned plan or open bug is held by a Refused note");
|
|
608
|
-
const more = andMore(rest, (e) => `${e.file} ${e.command} #${e.position} "${truncate(e.title, FIX_CLAIM_HEADING_MAX)}"` +
|
|
609
|
-
(e.mergeDone.length > 0 ? " [prerequisites no longer planned]" : "") +
|
|
610
|
-
(e.waiting > 0 ? ` [${e.waiting} part(s) wait on it]` : ""));
|
|
611
|
-
const stale = first.mergeDone.length > 0
|
|
612
|
-
? `; its prerequisite(s) ${first.mergeDone.map((r) => `${r.series} ${r.part}/${r.of}`).join(", ")} are no longer planned, so the refusal may be stale`
|
|
613
|
-
: "";
|
|
614
|
-
const waiting = first.waiting > 0
|
|
615
|
-
? `; ${first.waiting} planned part(s) wait on it, directly or through an earlier part`
|
|
616
|
-
: "";
|
|
617
|
-
return warnOutcome(`${first.file} ${first.command} #${first.position} "${first.title}" is held by a Refused note ` +
|
|
618
|
-
`(${truncate(first.note, REFUSAL_NOTE_MAX)})${stale}${waiting}${more} — no role takes a refused entry until it is edited: ` +
|
|
619
|
-
`answer the objection and release the entry with \`tumwater ${first.command} --release ${first.position}\`, ` +
|
|
620
|
-
`or retract it with \`tumwater ${first.command} --cancel ${first.position}\``);
|
|
310
|
+
const watched = [...new Set([...BACKLOG_FILES, ...(brief ? [brief] : [])])];
|
|
311
|
+
const out = await gitTry(root, "status", "--porcelain", "--", ...watched);
|
|
312
|
+
if (out === null)
|
|
313
|
+
return { base, primary, watched, dirty: null };
|
|
314
|
+
const dirtySet = new Set(out.split("\n")
|
|
315
|
+
.filter((line) => line.length > 3 && !line.startsWith("??"))
|
|
316
|
+
.map((line) => line.slice(3)));
|
|
317
|
+
return { base, primary, watched, dirty: watched.filter((f) => dirtySet.has(f)) };
|
|
621
318
|
}
|
|
622
319
|
/** Names the backlog files (and the brief file) with uncommitted edits in the primary checkout. An
|
|
623
320
|
* operator write is meant to be committed on the base branch by commitOperatorBacklogEdit; when
|
|
624
|
-
* that commit does not happen the edit is invisible to every
|
|
321
|
+
* that commit does not happen the edit is invisible to every role worktree (they branch from main)
|
|
625
322
|
* and a merge that touches the file cannot fast-forward the checkout (merge_blocked). A primary off
|
|
626
323
|
* the base branch is skipped: the ref-push merge arm leaves its working tree alone. Untracked files
|
|
627
324
|
* are not edits of a tracked file and are ignored. */
|
|
628
325
|
export async function checkUncommittedBacklog(root, config) {
|
|
629
|
-
const primary = await
|
|
630
|
-
const base = targetBranch(config?.baseBranch, primary);
|
|
326
|
+
const { base, primary, watched, dirty } = await dirtyBacklogFiles(root, config);
|
|
631
327
|
if (primary === null || primary !== base) {
|
|
632
328
|
return okOutcome(`your checkout is not on ${base}; backlog writes from you are not committed from it`);
|
|
633
329
|
}
|
|
634
|
-
|
|
635
|
-
const files = [...new Set([...BACKLOG_FILES, ...(brief ? [brief] : [])])];
|
|
636
|
-
const out = await gitTry(root, "status", "--porcelain", "--", ...files);
|
|
637
|
-
if (out === null)
|
|
330
|
+
if (dirty === null)
|
|
638
331
|
return okOutcome("could not read git status; skipped");
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const named = files.filter((f) => dirty.has(f));
|
|
643
|
-
if (named.length === 0) {
|
|
644
|
-
const list = files.slice(0, -1).join(", ");
|
|
645
|
-
return okOutcome(`${list} and ${files[files.length - 1]} are committed`);
|
|
332
|
+
if (dirty.length === 0) {
|
|
333
|
+
const list = watched.slice(0, -1).join(", ");
|
|
334
|
+
return okOutcome(`${list} and ${watched[watched.length - 1]} are committed`);
|
|
646
335
|
}
|
|
647
|
-
const list =
|
|
648
|
-
return warnOutcome(`${list} ${
|
|
336
|
+
const list = dirty.join(", ");
|
|
337
|
+
return warnOutcome(`${list} ${dirty.length > 1 ? "have" : "has"} uncommitted changes in your checkout — ` +
|
|
649
338
|
`roles branch from main and do not see them, and a merge that touches one cannot fast-forward the checkout ` +
|
|
650
|
-
`(merge_blocked); review them with \`git diff -- ${list}\` and commit them on ${base}`
|
|
339
|
+
`(merge_blocked); review them with \`git diff -- ${list}\` and commit them on ${base} ` +
|
|
340
|
+
`(or run \`tumwater doctor --commit-backlog\`, or the Health card's Commit backlog, or the TUI Health tab's Ctrl+A)`);
|
|
341
|
+
}
|
|
342
|
+
/** Commit every dirty tracked backlog or brief file in the primary checkout on the base branch,
|
|
343
|
+
* through the same commitOperatorBacklogEdit core every operator backlog write uses, running
|
|
344
|
+
* under the primary-worktree lock it takes. The dirtyBacklogFiles discovery and branch precedence
|
|
345
|
+
* are checkUncommittedBacklog's own, so a `tumwater doctor --commit-backlog` unblocks exactly the
|
|
346
|
+
* merges the `uncommitted backlog` row warns would be blocked. When the primary is off the base
|
|
347
|
+
* branch or the status cannot be read it commits nothing and reports a skip sentence; a file whose
|
|
348
|
+
* commit fails stays dirty and lands in `failed` while the other listed files are still attempted.
|
|
349
|
+
* Never throws. */
|
|
350
|
+
export async function commitUncommittedBacklog(root, config) {
|
|
351
|
+
const d = await dirtyBacklogFiles(root, config);
|
|
352
|
+
if (d.primary === null || d.primary !== d.base)
|
|
353
|
+
return { base: d.base, committed: [], failed: [], skipped: `your checkout is not on ${d.base}; nothing committed` };
|
|
354
|
+
if (d.dirty === null)
|
|
355
|
+
return { base: d.base, committed: [], failed: [], skipped: "could not read git status; nothing committed" };
|
|
356
|
+
const committed = [];
|
|
357
|
+
const failed = [];
|
|
358
|
+
for (const name of d.dirty) {
|
|
359
|
+
const ok = await commitOperatorBacklogEdit(root, name, stampedSubject("operator", `Commit uncommitted ${name}`)); // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
|
|
360
|
+
(ok ? committed : failed).push(name);
|
|
361
|
+
}
|
|
362
|
+
return { base: d.base, committed, failed, skipped: null };
|
|
363
|
+
}
|
|
364
|
+
/** The committed/failed/skip wording the CLI, the dashboard handler and the TUI flash all share
|
|
365
|
+
* for a BacklogCommitReport, so the three surfaces cannot drift: one `committed <file> on
|
|
366
|
+
* <base>` line per committed file, one `could not commit <file> — commit it by hand` line per
|
|
367
|
+
* failed file, then the skipped sentence when set. `[]` when the report committed nothing,
|
|
368
|
+
* failed nothing, and carried no skip sentence. `tumwater doctor --commit-backlog` (src/cli.ts),
|
|
369
|
+
* the dashboard Health card's Commit backlog (handleCommitBacklog) and the TUI Health tab's
|
|
370
|
+
* Ctrl+A all render from this one source. */
|
|
371
|
+
export function backlogCommitLines(report) {
|
|
372
|
+
const lines = [
|
|
373
|
+
...report.committed.map((file) => `committed ${file} on ${report.base}`),
|
|
374
|
+
...report.failed.map((file) => `could not commit ${file} — commit it by hand`),
|
|
375
|
+
];
|
|
376
|
+
if (report.skipped !== null)
|
|
377
|
+
lines.push(report.skipped);
|
|
378
|
+
return lines;
|
|
379
|
+
}
|
|
380
|
+
/** The single-line message a surface flashes or answers for a BacklogCommitReport: the shared
|
|
381
|
+
* backlogCommitLines joined with "; ", or the nothing-to-commit sentence when the report
|
|
382
|
+
* committed nothing, failed nothing and carried no skip sentence. Lives here beside
|
|
383
|
+
* backlogCommitLines so the dashboard Health card's Commit backlog handler and the TUI Health
|
|
384
|
+
* tab's Ctrl+A flash cannot drift from each other. */
|
|
385
|
+
export function backlogCommitMessage(report) {
|
|
386
|
+
const lines = backlogCommitLines(report);
|
|
387
|
+
return lines.length ? lines.join("; ") : "nothing to commit — every watched backlog or brief file is committed";
|
|
651
388
|
}
|