tumwater 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +42 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +4 -3
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +402 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +246 -55
- package/dist/src/backlog/backlog-cli-view.js +280 -0
- package/dist/src/backlog/backlog-cli.js +85 -274
- package/dist/src/backlog/backlog-commit.js +4 -5
- package/dist/src/backlog/backlog-eligibility.js +175 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +254 -0
- package/dist/src/backlog/backlog-release.js +117 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-culprit.js +94 -0
- package/dist/src/baseline/main-guardrails.js +209 -0
- package/dist/src/baseline/main-red.js +139 -32
- package/dist/src/baseline/main-revert.js +133 -0
- package/dist/src/baseline/main-watch.js +184 -0
- package/dist/src/brief/brief-edit.js +9 -11
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget.js +30 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +118 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +150 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +308 -590
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-role-command.js +420 -0
- package/dist/src/cli/cli-start-summary.js +160 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
- package/dist/src/cli/completion.js +149 -22
- package/dist/src/cli/config-commands.js +41 -18
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +189 -73
- package/dist/src/cli/log-commands.js +148 -82
- package/dist/src/cli/question-commands.js +205 -10
- package/dist/src/cli.js +106 -43
- package/dist/src/collections.js +26 -10
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +13 -1
- package/dist/src/config/config-fallbacks.js +93 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +141 -199
- package/dist/src/config/config-views.js +127 -53
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +131 -264
- package/dist/src/config/config.js +48 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
- package/dist/src/doctor/doctor-backlog-plans.js +263 -0
- package/dist/src/doctor/doctor-backlog.js +100 -496
- package/dist/src/doctor/doctor-check-names.js +81 -0
- package/dist/src/doctor/doctor-checks.js +52 -579
- package/dist/src/doctor/doctor-claude-checks.js +145 -0
- package/dist/src/doctor/doctor-codex-checks.js +279 -0
- package/dist/src/doctor/doctor-contention-checks.js +380 -0
- package/dist/src/doctor/doctor-holds.js +82 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +236 -402
- package/dist/src/doctor/doctor-notify.js +3 -4
- package/dist/src/doctor/doctor-opencode-checks.js +162 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-project-checks.js +303 -0
- package/dist/src/doctor/doctor.js +121 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +7 -11
- package/dist/src/events/event-format.js +181 -86
- package/dist/src/events/event-labels.js +12 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +39 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +60 -24
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +56 -56
- package/dist/src/failure/failure-render.js +31 -23
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +99 -35
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +177 -84
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +108 -58
- package/dist/src/gates/maintenance-quota.js +10 -13
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +77 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +36 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +42 -18
- package/dist/src/gui/gui-backlog-endpoints.js +155 -0
- package/dist/src/gui/gui-command.js +8 -13
- package/dist/src/gui/gui-config-commands.js +94 -72
- package/dist/src/gui/gui-endpoint-commands.js +4 -3
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +108 -13
- package/dist/src/gui/gui-prompt-commands.js +11 -14
- package/dist/src/gui/gui-report-endpoints.js +90 -0
- package/dist/src/gui/gui-server.js +11 -4
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +147 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +193 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +25 -9
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +81 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +150 -11
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +90 -0
- package/dist/src/merge/merge-queue-render.js +49 -11
- package/dist/src/merge/merge-queue.js +31 -15
- package/dist/src/merge/merge-requeue.js +102 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +33 -11
- package/dist/src/merge/merge-to-main.js +86 -27
- package/dist/src/merge/merge-top.js +69 -0
- package/dist/src/merge/merge-verify.js +12 -10
- package/dist/src/notes-cli.js +30 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +320 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
- package/dist/src/orchestrator/orchestrator.js +128 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +201 -0
- package/dist/src/orchestrator/scheduling-skip.js +185 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +116 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +18 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +121 -211
- package/dist/src/prompt/notes-backlog.js +181 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +129 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +161 -77
- package/dist/src/report/report-fold.js +129 -52
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +133 -64
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +26 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/known-flakes.js +100 -1
- package/dist/src/review/post-merge-review.js +106 -0
- package/dist/src/review/protected-edits.js +4 -5
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +155 -360
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +69 -12
- package/dist/src/roles/role-guidance.js +55 -20
- package/dist/src/roles/role-labels.js +1 -2
- package/dist/src/roles/role-render.js +67 -50
- package/dist/src/roles/role-view.js +59 -47
- package/dist/src/roles/roles.js +31 -31
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/{tick → run}/qa-coverage.js +9 -27
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +179 -0
- package/dist/src/run/run-detail-render.js +98 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +172 -0
- package/dist/src/run/stage-path-checks.js +730 -0
- package/dist/src/{loop → runner}/leftover.js +80 -25
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +202 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +107 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +102 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +43 -16
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +28 -28
- package/dist/src/status/status-roles.js +39 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +5 -6
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +63 -17
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +57 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +67 -13
- package/dist/src/ui/badges.js +65 -37
- package/dist/src/ui/gui/gui-client-backlog.js +292 -0
- package/dist/src/ui/gui/gui-client-boot.js +32 -26
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
- package/dist/src/ui/gui/gui-client-drawer.js +126 -285
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +434 -67
- package/dist/src/ui/gui/gui-client-labels.js +1 -2
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +55 -51
- package/dist/src/ui/gui/gui-client-pending.js +227 -23
- package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
- package/dist/src/ui/gui/gui-client-report.js +87 -427
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +56 -4
- package/dist/src/ui/gui/gui-client-team.js +25 -465
- package/dist/src/ui/gui/gui-client.js +27 -25
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +9 -7
- package/dist/src/ui/gui/gui-styles.js +34 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +136 -49
- package/dist/src/ui/status-render.js +101 -82
- package/dist/src/ui/team-alerts.js +248 -57
- package/dist/src/ui/tone.js +5 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +51 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +3 -5
- package/dist/src/ui/tui/tui-editor-backends.js +132 -0
- package/dist/src/ui/tui/tui-editor-brief.js +10 -12
- package/dist/src/ui/tui/tui-editor-budget.js +35 -31
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
- package/dist/src/ui/tui/tui-editor-config.js +86 -0
- package/dist/src/ui/tui/tui-editor-entry.js +36 -47
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +37 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
- package/dist/src/ui/tui/tui-editor-interval.js +11 -9
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +3 -5
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
- package/dist/src/ui/tui/tui-editor-retire.js +4 -6
- package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +60 -34
- package/dist/src/ui/tui/tui-editor-slots.js +23 -0
- package/dist/src/ui/tui/tui-editors.js +35 -2
- package/dist/src/ui/tui/tui-frame.js +56 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +24 -9
- package/dist/src/ui/tui/tui-pane-activity.js +14 -11
- package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +58 -8
- package/dist/src/ui/tui/tui-pane-history.js +402 -82
- package/dist/src/ui/tui/tui-pane-pending.js +308 -13
- package/dist/src/ui/tui/tui-pane-queued.js +71 -55
- package/dist/src/ui/tui/tui-pane-report.js +46 -14
- package/dist/src/ui/tui/tui-pane-session.js +25 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +80 -23
- package/dist/src/ui/tui/tui-panes.js +100 -30
- package/dist/src/ui/tui/tui.js +14 -14
- package/dist/src/verdict/fix-claim.js +82 -32
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +42 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,57 +1,57 @@
|
|
|
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 { fixSymbols, fixedEntriesAt, missingSymbolNames, sourceHaystack, unbackedSymbols } from "../verdict/fix-claim.js";
|
|
14
|
-
import { briefFile } from "../brief/brief.js";
|
|
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 { humanSeconds, secondsSince, HOUR_MS, DAY_MS } from "../text/datetime.js";
|
|
6
|
+
import { eventsSince } from "../events/event-window.js";
|
|
7
|
+
import { finiteNumber } from "../files/json-object.js";
|
|
8
|
+
import { lowerMedian } from "../collections.js";
|
|
9
|
+
import { plural, shortSpanPhrase } from "../text/phrases.js";
|
|
10
|
+
import { queuedMerges, unreadableMergeQueueFiles } from "../merge/merge-queue.js";
|
|
18
11
|
import { mergeChanges, readMergeMarker } from "../merge/merge-slot.js";
|
|
19
12
|
import { listWideScope, queuedListRows } from "../inbox/inbox.js";
|
|
20
13
|
import { deliverableAt } from "../inbox/prompt-not-before.js";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { plural, shortSpanPhrase } from "../text/phrases.js";
|
|
14
|
+
import { currentBranch, targetBranch } from "../git/git.js";
|
|
15
|
+
import { gitTry } from "../git/git-run.js";
|
|
16
|
+
import { briefFile } from "../brief/brief.js";
|
|
17
|
+
import { BACKLOG_FILES } from "../backlog/backlog-md.js";
|
|
26
18
|
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
27
|
-
/** The doctor's backlog
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
19
|
+
/** The doctor's backlog modules' shared scaffolding and the check groups that do not read a
|
|
20
|
+
* single backlog document: the merge-queue and queued-prompt checks (checkStaleQueuedMerges,
|
|
21
|
+
* checkMergeQueueFiles, checkStalePrompts and the mergeWaitPhrase helper) and the uncommitted-
|
|
22
|
+
* edits-in-the-primary-checkout check. This module also hosts the helpers and constants the
|
|
23
|
+
* per-document checks import — readDoc and its checked wrapper readDocChecked, andMore, and the
|
|
24
|
+
* age/heading-trim bounds — so the per-document reads cannot drift. The per-document
|
|
25
|
+
* checks live in doctor-backlog-plans.ts (the plan-document structure and fix-claim checks) and
|
|
26
|
+
* doctor-backlog-bugs.ts (open bugs, questions and holds). All of them warn instead of failing:
|
|
27
|
+
* damaged documentation is operator signal, not a broken environment. doctor.ts composes them
|
|
28
|
+
* beside the environment checks; the shared CheckOutcome shape is imported from outcome.ts. */
|
|
36
29
|
/** How many Fixed records the two newest-first Fixed-record checks (fix claims and validation
|
|
37
30
|
* gaps) verify: the newest, since the section is newest-first by template convention. Older
|
|
38
31
|
* records drift as the code evolves — a symbol legitimately fixed long ago gets renamed later,
|
|
39
32
|
* and pre-convention Fixed entries predate the validation-gap line — so a whole-section scan
|
|
40
33
|
* would warn forever on records nobody should rewrite. */
|
|
41
|
-
const FIX_CLAIMS_CHECKED = 10;
|
|
34
|
+
export const FIX_CLAIMS_CHECKED = 10;
|
|
42
35
|
/** A second or later suspect record's heading is trimmed to this many characters in the warn —
|
|
43
36
|
* the first is named in full, the rest only enough to find them in BUGS.md. */
|
|
44
|
-
const FIX_CLAIM_HEADING_MAX = 60;
|
|
37
|
+
export const FIX_CLAIM_HEADING_MAX = 60;
|
|
45
38
|
/** A refused entry's note is trimmed to this many characters in the refusals warn. */
|
|
46
|
-
const REFUSAL_NOTE_MAX = 120;
|
|
39
|
+
export const REFUSAL_NOTE_MAX = 120;
|
|
47
40
|
/** The whole days an open question may wait before the stale-questions check warns about it:
|
|
48
41
|
* an opinionated default, matching the 7-day window the `--list` age is meant to surface.
|
|
49
42
|
* A question awaiting a human answer is operator signal, so the value needs no knob. */
|
|
50
|
-
const STALE_QUESTION_DAYS = 7;
|
|
43
|
+
export const STALE_QUESTION_DAYS = 7;
|
|
51
44
|
/** The whole days an open bug may wait before the stale-bugs check warns about it: an
|
|
52
45
|
* opinionated default with no knob, like STALE_QUESTION_DAYS. Three days is long enough that a
|
|
53
46
|
* bug filed late one evening and fixed the next day never warns. */
|
|
54
|
-
const STALE_BUG_DAYS = 3;
|
|
47
|
+
export const STALE_BUG_DAYS = 3;
|
|
48
|
+
/** The whole days a bug may stay held by a Waiting for the next failure note before the
|
|
49
|
+
* waiting-bugs check warns that its flake has not come back. A note parks a bug until it fails
|
|
50
|
+
* again, but the failure may never come, so a hold has to lapse or the entry parks indefinitely;
|
|
51
|
+
* one week without a recurrence is long enough that a recurring flake would almost surely have
|
|
52
|
+
* shown up, and short enough that a bug which never returns stops squatting in the backlog as
|
|
53
|
+
* an ok. An opinionated default with no knob, like STALE_BUG_DAYS. */
|
|
54
|
+
export const STALE_WAIT_DAYS = 7;
|
|
55
55
|
/** The `(and N more …)` suffix three one-line doctor details append after naming their first
|
|
56
56
|
* suspect (fix claims, stranded plans, duplicated headings): the rest stay visible but
|
|
57
57
|
* shortened, since a doctor check renders one line. `render` makes each extra item's short
|
|
@@ -59,7 +59,7 @@ const STALE_BUG_DAYS = 3;
|
|
|
59
59
|
* FIX_CLAIM_HEADING_MAX, the duplicate-headings site passes its items through as built;
|
|
60
60
|
* `noun` names what the extras are ("record(s)" for fix claims, empty for the other two).
|
|
61
61
|
* Empty string when there are no extras. */
|
|
62
|
-
function andMore(rest, render, noun = "") {
|
|
62
|
+
export function andMore(rest, render, noun = "") {
|
|
63
63
|
return rest.length > 0
|
|
64
64
|
? ` (and ${rest.length} more${noun ? ` ${noun}` : ""}: ${rest.map(render).join(", ")})`
|
|
65
65
|
: "";
|
|
@@ -67,7 +67,7 @@ function andMore(rest, render, noun = "") {
|
|
|
67
67
|
/** Read one backlog file for a check, or the failure message explaining why it could not be
|
|
68
68
|
* read (text.ts's errorMessage). Callers shape the failure themselves — a returned warn for a
|
|
69
69
|
* single-file check, an "(unreadable)" list entry for the multi-file heading sweep. */
|
|
70
|
-
function readDoc(p) {
|
|
70
|
+
export function readDoc(p) {
|
|
71
71
|
try {
|
|
72
72
|
return { doc: fs.readFileSync(p, "utf8") };
|
|
73
73
|
}
|
|
@@ -78,7 +78,7 @@ function readDoc(p) {
|
|
|
78
78
|
/** The single-file check's read prologue, shared by checkFixClaims and checkStrandedPlans:
|
|
79
79
|
* an absent file is that check's own "nothing to verify" ok outcome, an unreadable one a warn
|
|
80
80
|
* naming the file. Returns the document, or the outcome the caller should return as-is. */
|
|
81
|
-
function readDocChecked(p, file, missingDetail) {
|
|
81
|
+
export function readDocChecked(p, file, missingDetail) {
|
|
82
82
|
if (!fs.existsSync(p))
|
|
83
83
|
return okOutcome(missingDetail);
|
|
84
84
|
const read = readDoc(p);
|
|
@@ -86,242 +86,17 @@ function readDocChecked(p, file, missingDetail) {
|
|
|
86
86
|
return warnOutcome(`cannot read ${file} — ${read.error}`);
|
|
87
87
|
return read.doc;
|
|
88
88
|
}
|
|
89
|
-
/** Fix claims — the standalone half of the merge gate's false-fix check (src/verdict/fix-claim.ts):
|
|
90
|
-
* that gate fires only when an md-only diff moves a BUGS.md entry to Fixed, so a phantom fix that
|
|
91
|
-
* reached main any other way (merged before the gate existed, or through a path it never sees) was
|
|
92
|
-
* visible only to a human reading raw history. This re-verifies the newest Fixed records against
|
|
93
|
-
* the tree at `root` — the primary checkout IS main's tree — with the gate's own parsing and
|
|
94
|
-
* existence rules. Deliberately looser than the gate: a record warns only when EVERY symbol its Fix
|
|
95
|
-
* paragraph names is absent (one live symbol passes — the phantom signature, not a half-stale
|
|
96
|
-
* narrative), and a record naming no symbols is skipped, since pure-documentation fixes are
|
|
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");
|
|
316
|
-
}
|
|
317
89
|
/** How long a queued merge may wait without its change starting before the stale-queued-merge check
|
|
318
90
|
* warns: 6 hours, fixed. A verify plus a merge completes in minutes (plans/merge-queue.md), so a
|
|
319
91
|
* full six-hour silence while the team is healthy — not on a hold, which doctor already reports
|
|
320
92
|
* 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
|
-
|
|
93
|
+
* opinionated default, like STALE_QUESTION_DAYS, so no knob. The always-on `stale-merges` team
|
|
94
|
+
* alert (src/ui/status-payload.ts staleMerges) reuses this same window through
|
|
95
|
+
* staleQueuedMergeRows, so the doctor's row and the alert cannot drift on the threshold (the
|
|
96
|
+
* STALE_PROMPT_MS/STALE_BUG_DAYS precedent). */
|
|
97
|
+
export const MERGE_QUEUE_STALE_MS = 6 * HOUR_MS;
|
|
323
98
|
/** The window of the merge-wait phrase on the stale merge-queue row. */
|
|
324
|
-
const MERGE_WAIT_WINDOW_MS =
|
|
99
|
+
const MERGE_WAIT_WINDOW_MS = DAY_MS;
|
|
325
100
|
/** Stale queued merges — a change that has sat in the durable merge queue too long without starting
|
|
326
101
|
* (plans/merge-queue.md). Every queued-change health fact is otherwise a hook into its own role or
|
|
327
102
|
* stage: a verify under way reads in the team row, a team hold gets its own banner, and
|
|
@@ -347,11 +122,7 @@ export function checkStaleQueuedMerges(root, now) {
|
|
|
347
122
|
}
|
|
348
123
|
return phrase ? { ...out, detail: `${out.detail} · ${phrase}` } : out;
|
|
349
124
|
};
|
|
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)) }));
|
|
125
|
+
const stale = staleQueuedMergeRows(root, now).map((e) => ({ ...e, age: humanSeconds(secondsSince(now - e.waitedMs, now)) }));
|
|
355
126
|
const [first, ...rest] = stale;
|
|
356
127
|
if (!first)
|
|
357
128
|
return withWait(okOutcome(`no merge-queue entry has waited more than ${humanSeconds(MERGE_QUEUE_STALE_MS / 1000)} to start`));
|
|
@@ -359,27 +130,76 @@ export function checkStaleQueuedMerges(root, now) {
|
|
|
359
130
|
return withWait(warnOutcome(`merge-queue entry of ${roleLabel(first.role)} — "${truncate(first.summary, FIX_CLAIM_HEADING_MAX)}" — has waited ` +
|
|
360
131
|
`${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
132
|
}
|
|
133
|
+
/** The one traversal shared by the doctor's `stale merge-queue entries` row
|
|
134
|
+
* (checkStaleQueuedMerges) and the always-on `stale-merges` team alert
|
|
135
|
+
* (src/ui/status-payload.ts staleMerges): every queued merge whose role is not in the in-flight
|
|
136
|
+
* marker and whose wait strictly exceeds MERGE_QUEUE_STALE_MS (6 h), in queue-file order (oldest
|
|
137
|
+
* first). The in-flight set comes from readMergeMarker via mergeChanges (an absent or
|
|
138
|
+
* never-throwing unreadable marker contributes the empty set) — a slow review is not a stalled
|
|
139
|
+
* drain. Each row carries { role, summary, waitedMs }; the doctor's row adds its age phrase and
|
|
140
|
+
* the alert names the wait, so the rule and its threshold live in one place and cannot drift. */
|
|
141
|
+
export function staleQueuedMergeRows(root, now) {
|
|
142
|
+
const marker = readMergeMarker(root);
|
|
143
|
+
const inFlight = new Set(marker ? mergeChanges(marker).map((c) => c.role) : []);
|
|
144
|
+
return queuedMerges(root)
|
|
145
|
+
.filter((e) => !inFlight.has(e.role) && now - e.enqueuedAt > MERGE_QUEUE_STALE_MS)
|
|
146
|
+
.map((e) => ({ role: e.role, summary: e.summary, waitedMs: now - e.enqueuedAt }));
|
|
147
|
+
}
|
|
148
|
+
/** The merge-queue files `readMergeQueue` skips — torn, foreign, or vanished mid-listing — as
|
|
149
|
+
* checkout-relative paths. One traversal shared by the doctor's `checkMergeQueueFiles` row and
|
|
150
|
+
* the always-on `skipped-merges` team alert (src/ui/status-payload.ts skippedMergeFiles), so
|
|
151
|
+
* the two surfaces select the same files and name the same paths and cannot drift. */
|
|
152
|
+
export function skippedMergeQueueFiles(root) {
|
|
153
|
+
return unreadableMergeQueueFiles(root).map((f) => path.relative(root, f));
|
|
154
|
+
}
|
|
155
|
+
/** The merge-queue files `readMergeQueue` skips — torn, foreign, or vanished mid-listing —
|
|
156
|
+
* named so a stranded queue entry is a health signal instead of only a number on `tumwater
|
|
157
|
+
* queue`'s output. It exists because `tumwater queue` counts the skips but does not name them,
|
|
158
|
+
* and the drain drops only an unreadable head, so a non-head skip is otherwise silent: it sits
|
|
159
|
+
* in `.tumwater/merge-queue/` forever, never drained, and neither the queue reader nor the
|
|
160
|
+
* stale-queued-merges doctor row (which reads only readable entries) surfaces it. Warn, never
|
|
161
|
+
* fail, like the module's other document checks: a foreign file is operator signal, not a
|
|
162
|
+
* broken environment. */
|
|
163
|
+
export function checkMergeQueueFiles(root) {
|
|
164
|
+
const files = skippedMergeQueueFiles(root);
|
|
165
|
+
if (files.length === 0)
|
|
166
|
+
return okOutcome("the queue reader skips no merge-queue file");
|
|
167
|
+
const [first, ...rest] = files;
|
|
168
|
+
const more = andMore(rest, (f) => f, "file(s)");
|
|
169
|
+
return warnOutcome(`the merge queue holds ${plural(files.length, "file")} the reader skips — ${first}${more}; ` +
|
|
170
|
+
`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)`);
|
|
171
|
+
}
|
|
362
172
|
/** How long a queued prompt may wait for its role to dequeue it before the stale-prompt check
|
|
363
173
|
* warns: a day, fixed. An opinionated default with no knob, like MERGE_QUEUE_STALE_MS: a day is
|
|
364
174
|
* 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
|
-
|
|
175
|
+
* that a paused or disabled role's stranded prompt shows up. The always-on stale-prompt team
|
|
176
|
+
* alert (src/ui/status-payload.ts stalePrompts) reuses this same threshold, so it cannot drift
|
|
177
|
+
* from the doctor's check — the STALE_BUG_DAYS precedent. */
|
|
178
|
+
export const STALE_PROMPT_MS = DAY_MS;
|
|
179
|
+
/** The queued inbox prompts the doctor's stale-prompt rule calls stale — deliverable now (a
|
|
180
|
+
* `--at` prompt still counting down is never stale) and queued more than STALE_PROMPT_MS,
|
|
181
|
+
* oldest-first, `queuedAtMs` narrowed to a number. One traversal shared by the doctor's
|
|
182
|
+
* `checkStalePrompts` check and the always-on `stalePrompts` team alert (src/ui/status-payload.ts),
|
|
183
|
+
* so the two surfaces read the same rule at the same threshold and cannot drift. */
|
|
184
|
+
export function staleQueuedPromptRows(root, now) {
|
|
185
|
+
return queuedListRows(root, null, listWideScope(root))
|
|
186
|
+
.filter((p) => p.queuedAtMs !== null && deliverableAt(p.notBeforeMs, now) && now - p.queuedAtMs > STALE_PROMPT_MS)
|
|
187
|
+
.sort((a, b) => a.queuedAtMs - b.queuedAtMs)
|
|
188
|
+
.map((p) => ({ ...p, queuedAtMs: p.queuedAtMs }));
|
|
189
|
+
}
|
|
367
190
|
/** Stale queued prompts — a prompt queued for a role (`tumwater prompt --role <id>`, the
|
|
368
191
|
* dashboard composer, the TUI, a `--at` delay whose time has now passed) that has waited more
|
|
369
192
|
* than a day for that role to dequeue it. A role that is paused, disabled, or failing never
|
|
370
193
|
* reaches the run that would dequeue the prompt, so it waits indefinitely and only the Inbox
|
|
371
194
|
* views (`tumwater prompt --list`, the dashboard Inbox tab, the TUI Inbox pane) show its age —
|
|
372
195
|
* 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
|
-
*
|
|
196
|
+
* and the deliver-or-drop commands. Reads the queue through the shared `staleQueuedPromptRows`
|
|
197
|
+
* traversal the always-on stale-prompts alert uses (no new reader in src/inbox/), keeping only
|
|
375
198
|
* rows that are deliverable now — a `--at 7d` prompt still counting down is never stale — and
|
|
376
199
|
* stamped more than STALE_PROMPT_MS ago. A warn, never a fail — the checkStaleQueuedMerges
|
|
377
200
|
* precedent: a stranded prompt is operator signal, not a broken environment. */
|
|
378
201
|
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 }));
|
|
202
|
+
const stale = staleQueuedPromptRows(root, now);
|
|
383
203
|
const [first, ...rest] = stale;
|
|
384
204
|
if (!first)
|
|
385
205
|
return okOutcome("no queued prompt has waited more than 1 day for a role to dequeue it");
|
|
@@ -403,225 +223,9 @@ export function mergeWaitPhrase(events) {
|
|
|
403
223
|
const longest = Math.max(...waits);
|
|
404
224
|
return `merges in the last 24h waited ${shortSpanPhrase(median)} median, ${shortSpanPhrase(longest)} longest (${plural(waits.length, "merge")})`;
|
|
405
225
|
}
|
|
406
|
-
/** The read-events window (ms) that reaches the oldest of `sinces` — `Date.now()` minus the
|
|
407
|
-
* oldest timestamp, floored at 0 — so a recurrence check reads events past every candidate's
|
|
408
|
-
* `since`. The single home of the "oldest of a set of since-times" step, shared by
|
|
409
|
-
* checkWaitingBugs and checkRegressedFixes, so those sites cannot drift on the clamp or the
|
|
410
|
-
* time base. */
|
|
411
|
-
function sinceWindowMs(sinces) {
|
|
412
|
-
return Math.max(0, Date.now() - Math.min(...sinces));
|
|
413
|
-
}
|
|
414
|
-
/** Stale questions — open QUESTIONS.md questions that have waited more than 7 days for an
|
|
415
|
-
* answer. A team can stay parked on a question for days while `tumwater doctor` reports
|
|
416
|
-
* nothing; the `--list` age (`questionAgeSuffix` on `tumwater questions`) is the only other
|
|
417
|
-
* place the wait shows, and only when the operator asks. Reads the tree at `root` — the
|
|
418
|
-
* primary checkout IS main's tree — like checkRefusedEntries, reusing
|
|
419
|
-
* `questionFilingDate`/`openQuestionEntries` (src/backlog) unchanged. The age is the whole
|
|
420
|
-
* days between the heading's `(asked by …)` stamp and now through backlog.js's ageInDays,
|
|
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
|
-
}
|
|
541
|
-
}
|
|
542
|
-
const more = andMore(rest, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
543
|
-
return okOutcome(`BUGS.md bug #${first.position} "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" is held until it fails again ` +
|
|
544
|
-
`(${truncate(first.note, REFUSAL_NOTE_MAX)})${more} — selfcheck lifts the note when it records a new occurrence; ` +
|
|
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
|
-
}
|
|
580
|
-
}
|
|
581
|
-
return okOutcome("no recently Fixed bug's quoted test has failed since its fix");
|
|
582
|
-
}
|
|
583
|
-
/** Refused entries — planned plans and open bugs held by a **Refused …** note. No loop clears
|
|
584
|
-
* a Refused note, so the entry stays parked until a human or the director edits it, or an
|
|
585
|
-
* operator releases it with `--release <n>`, and the
|
|
586
|
-
* only other signs are the `[refused]` marks in the backlog listings. Reads the tree at `root`
|
|
587
|
-
* — the primary checkout IS main's tree — like checkFixClaims, in `--list` order (PLANS.md
|
|
588
|
-
* Planned, then BUGS.md Open). A warn, never a fail: a refusal is operator signal, not a broken
|
|
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}\``);
|
|
621
|
-
}
|
|
622
226
|
/** Names the backlog files (and the brief file) with uncommitted edits in the primary checkout. An
|
|
623
227
|
* 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
|
|
228
|
+
* that commit does not happen the edit is invisible to every role worktree (they branch from main)
|
|
625
229
|
* and a merge that touches the file cannot fast-forward the checkout (merge_blocked). A primary off
|
|
626
230
|
* the base branch is skipped: the ref-push merge arm leaves its working tree alone. Untracked files
|
|
627
231
|
* are not edits of a tracked file and are ignored. */
|