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
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/** The doctor's checks that inspect the project's own on-disk state, disk and build,
|
|
2
|
+
* split out of doctor-checks.ts (whose environment/repo file still holds the toolchain and
|
|
3
|
+
* setup checks — the node/git/exec binaries, the repo/init/config/brief/bootstrap setup, the
|
|
4
|
+
* agent backends' CLI versions and the operator pi-extension/funes coordination). This module
|
|
5
|
+
* holds the checks that look at the project itself: a writable .tumwater (checkStateDir), a
|
|
6
|
+
* readable event feed (checkEventLog), enough free disk (checkDiskSpace), a declared project
|
|
7
|
+
* check (checkBuildCheck), the review gate's flake memory (checkFlakyChecks) and build
|
|
8
|
+
* provenance (checkBuild). Read-only like every doctor check; the private helpers each cluster
|
|
9
|
+
* uses (sweepStaleProbes, eventLogHistoryPhrase, firstEventLine, diskReclaimPhrase,
|
|
10
|
+
* checkDurationPhrase, currentHead) live here beside the checks that use them. doctor.ts stays
|
|
11
|
+
* the composition layer that calls each check in fixed order. */
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { detectBuildCheck, mergeCommandOf } from "../build/build-check-detect.js";
|
|
15
|
+
import { isPassingFullCheck } from "../build/build-check-events.js";
|
|
16
|
+
import { buildStaleness, isSelfHosted, readBuildInfo, STALE_INPUTS_LABEL } from "../build/build-info.js";
|
|
17
|
+
import { dirEntryNames, statOrNull } from "../files/files.js";
|
|
18
|
+
import { refSha } from "../git/git.js";
|
|
19
|
+
import { STATE_DIR, eventsArchivePath, eventsLogPath } from "../paths.js";
|
|
20
|
+
import { BYTES_PER_GB, diskVolumePath, sampleFreeBytes } from "../gates/disk-gate.js";
|
|
21
|
+
import { DAY_MS, dayKey } from "../text/datetime.js";
|
|
22
|
+
import { errorMessage } from "../text/text.js";
|
|
23
|
+
import { lowerMedian } from "../collections.js";
|
|
24
|
+
import { formatGB, shortSha } from "../text/format.js";
|
|
25
|
+
import { eventLogState, readEventsSince } from "../events/event-window.js";
|
|
26
|
+
import { EVENTS_KEEP } from "../events/events.js";
|
|
27
|
+
import { finiteNumber } from "../files/json-object.js";
|
|
28
|
+
import { agree, plural, shortSpanPhrase } from "../text/phrases.js";
|
|
29
|
+
import { flakeCounts, KNOWN_FLAKE_WINDOW_MS, loadQuarantine } from "../review/known-flakes.js";
|
|
30
|
+
import { quarantineWarning } from "../baseline/main-guardrails.js";
|
|
31
|
+
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
32
|
+
/** .tumwater writable — absent is fine (created on first run); present, prove it by writing
|
|
33
|
+
* and deleting a temp file inside. The probe leaves the dir's listing unchanged after the run.
|
|
34
|
+
* The write is the property under test: only a write that fails is "not writable", so the
|
|
35
|
+
* cleanup below — deleting the probe, sweeping stales — never vetoes a dir the write just
|
|
36
|
+
* proved writable, and a probe left over is swept rather than accumulated. */
|
|
37
|
+
export function checkStateDir(root) {
|
|
38
|
+
const dir = path.join(root, STATE_DIR);
|
|
39
|
+
if (!fs.existsSync(dir))
|
|
40
|
+
return okOutcome("absent — created on first run");
|
|
41
|
+
const probe = path.join(dir, `.doctor-probe-${process.pid}`);
|
|
42
|
+
try {
|
|
43
|
+
fs.writeFileSync(probe, "");
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
// A partial write may already have created the file — sweep it like any other leftover.
|
|
47
|
+
sweepStaleProbes(dir);
|
|
48
|
+
return failOutcome(`not writable: ${errorMessage(err)}`);
|
|
49
|
+
}
|
|
50
|
+
// The write proved writability. Deleting the probe is cleanup, not measurement: a removal
|
|
51
|
+
// failure must not turn a succeeded write into a false alarm, and the stray is collected by
|
|
52
|
+
// the sweep below (or a later pass's sweep).
|
|
53
|
+
try {
|
|
54
|
+
fs.rmSync(probe);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Best-effort — the stray is swept in a moment.
|
|
58
|
+
}
|
|
59
|
+
sweepStaleProbes(dir);
|
|
60
|
+
return okOutcome("writable");
|
|
61
|
+
}
|
|
62
|
+
/** Best-effort removal of every `.doctor-probe-*` file in `dir` — the only artifact
|
|
63
|
+
* checkStateDir creates, so any present probe is a leftover: a prior run killed between its
|
|
64
|
+
* write and delete, a partial write above, or a removal that failed. Removed one at a time so
|
|
65
|
+
* a single unremovable probe cannot stop the sweep. */
|
|
66
|
+
function sweepStaleProbes(dir) {
|
|
67
|
+
for (const name of dirEntryNames(dir)) {
|
|
68
|
+
if (!name.startsWith(".doctor-probe-"))
|
|
69
|
+
continue;
|
|
70
|
+
try {
|
|
71
|
+
fs.rmSync(path.join(dir, name));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Best-effort — leave it for the next pass.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Event log readable — open and parse events.jsonl (and its rotated archive) so the one
|
|
79
|
+
* failure the selfcheck role is chartered to catch — a feed that hid itself — is named by the
|
|
80
|
+
* pre-flight too. An absent or empty log is fine: a young project writes its first event on the
|
|
81
|
+
* first run. A present-but-unparseable log (a directory at the path, a permission failure, or
|
|
82
|
+
* every complete line corrupt) fails, because every observer — selfcheck, the TUI/GUI feeds,
|
|
83
|
+
* `logs`, `report` — reads that one feed. Read-only. */
|
|
84
|
+
export function checkEventLog(root) {
|
|
85
|
+
let state;
|
|
86
|
+
try {
|
|
87
|
+
state = eventLogState(root);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return failOutcome(`could not read ${eventsLogPath(root)}: ${errorMessage(err)}`);
|
|
91
|
+
}
|
|
92
|
+
if (state === "unreadable")
|
|
93
|
+
return failOutcome(`unreadable: ${eventsLogPath(root)} — the team's event feed cannot be read`);
|
|
94
|
+
if (state === "empty")
|
|
95
|
+
return okOutcome("no events yet");
|
|
96
|
+
const history = eventLogHistoryPhrase(root);
|
|
97
|
+
return okOutcome(history ? `readable: ${eventsLogPath(root)} — ${history}` : `readable: ${eventsLogPath(root)}`);
|
|
98
|
+
}
|
|
99
|
+
/** The `history from <date>, <n> files` phrase for checkEventLog's ok detail (G4): the first
|
|
100
|
+
* complete line of the oldest event-log generation present names how far back the kept history
|
|
101
|
+
* reaches, and the count is how many generations (live file plus archives) exist. Undefined
|
|
102
|
+
* when no generation parses an event, so a readable-but-shapeless log still reads as readable. */
|
|
103
|
+
function eventLogHistoryPhrase(root) {
|
|
104
|
+
const present = [];
|
|
105
|
+
const livePath = eventsLogPath(root);
|
|
106
|
+
if (statOrNull(livePath))
|
|
107
|
+
present.push(livePath);
|
|
108
|
+
for (let gen = 1; gen <= EVENTS_KEEP; gen++) {
|
|
109
|
+
const archive = eventsArchivePath(root, gen);
|
|
110
|
+
if (statOrNull(archive))
|
|
111
|
+
present.push(archive);
|
|
112
|
+
}
|
|
113
|
+
if (present.length === 0)
|
|
114
|
+
return undefined;
|
|
115
|
+
const oldest = present[present.length - 1];
|
|
116
|
+
const ev = firstEventLine(oldest);
|
|
117
|
+
if (!ev || typeof ev.ts !== "number")
|
|
118
|
+
return undefined;
|
|
119
|
+
const noun = plural(present.length, "file", "files");
|
|
120
|
+
return `history from ${dayKey(ev.ts)}, ${noun}`;
|
|
121
|
+
}
|
|
122
|
+
/** Parse the first complete line of an append-only JSONL `file`, or null when it is unreadable
|
|
123
|
+
* or does not parse. Reads only the first chunk — the event log is up to 16 MB a generation,
|
|
124
|
+
* and a doctor pre-flight should not slurp the oldest archive just to name its first day. */
|
|
125
|
+
function firstEventLine(file) {
|
|
126
|
+
try {
|
|
127
|
+
const fd = fs.openSync(file, "r");
|
|
128
|
+
try {
|
|
129
|
+
const buf = Buffer.alloc(4096);
|
|
130
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
131
|
+
const line = buf.toString("utf8", 0, n).split("\n")[0] ?? "";
|
|
132
|
+
if (!line)
|
|
133
|
+
return null;
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(line);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
fs.closeSync(fd);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Disk floor (plans/disk-floor.md, parts 1/4 and 2/4) — is there room on the volume the
|
|
150
|
+
* worktrees live on? Fail below `diskHoldGB`, the same floor the running team holds new work
|
|
151
|
+
* at, so doctor and the gate agree on when the disk is a problem. Warn in the band between
|
|
152
|
+
* `diskHoldGB` and `diskReclaimGB`: pressure reclaim will delete ignored build outputs from
|
|
153
|
+
* idle worktrees but the team still runs. Ok at or above `diskReclaimGB` (or when reclaim is
|
|
154
|
+
* disabled and free space is above the hold). A statfs failure is a warning, not a failure: an
|
|
155
|
+
* unmeasurable volume never holds the team, so it must not fail a scripted pre-flight. The
|
|
156
|
+
* detail names the measured path, the free GB to one decimal, and the floor. `holdGB` 0
|
|
157
|
+
* disables the hold, so a free space above `reclaimGB`'s own band (or `reclaimGB` 0) passes.
|
|
158
|
+
* After a successful sample the detail also carries `diskReclaimPhrase` (what `disk_reclaim`
|
|
159
|
+
* passes freed over the last 24 h); it is informational and never changes the level. */
|
|
160
|
+
export function checkDiskSpace(root, holdGB, reclaimGB, sample = sampleFreeBytes) {
|
|
161
|
+
const where = diskVolumePath(root);
|
|
162
|
+
const free = sample(root);
|
|
163
|
+
if (free === null)
|
|
164
|
+
return warnOutcome(`cannot measure free space at ${where} (statfs failed) — the disk hold is off for this process`);
|
|
165
|
+
const freeGB = free / BYTES_PER_GB;
|
|
166
|
+
const freeText = `${formatGB(freeGB)} free at ${where}`;
|
|
167
|
+
const phrase = diskReclaimPhrase(readEventsSince(root, DAY_MS).events);
|
|
168
|
+
const withReclaim = (detail) => (phrase ? `${detail} · ${phrase}` : detail);
|
|
169
|
+
if (holdGB > 0 && freeGB < holdGB)
|
|
170
|
+
return failOutcome(withReclaim(`${freeText} — below the ${holdGB} GB diskHoldGB floor; the team waits to start new work until space recovers`));
|
|
171
|
+
if (reclaimGB > 0 && freeGB < reclaimGB)
|
|
172
|
+
return warnOutcome(withReclaim(`${freeText} — below the ${reclaimGB} GB diskReclaimGB threshold; a pressure reclaim pass deletes ignored build outputs from idle worktrees`));
|
|
173
|
+
const floor = holdGB > 0 ? `above the ${holdGB} GB floor` : "diskHoldGB is 0 (hold disabled)";
|
|
174
|
+
return okOutcome(withReclaim(`${freeText}, ${floor}`));
|
|
175
|
+
}
|
|
176
|
+
/** The `reclaim in the last 24h freed …` phrase for the `disk space` row: the summed
|
|
177
|
+
* `freedGB` of the `disk_reclaim` passes, their count, and how many ran under pressure (an
|
|
178
|
+
* event without a `mode` counts as pressure, like its feed line). Each pass is a point event,
|
|
179
|
+
* so summing cannot straddle the window edge. Undefined when no pass ran. */
|
|
180
|
+
export function diskReclaimPhrase(events) {
|
|
181
|
+
const passes = events.filter((e) => e.type === "disk_reclaim");
|
|
182
|
+
if (passes.length === 0)
|
|
183
|
+
return undefined;
|
|
184
|
+
const freed = passes.reduce((sum, e) => sum + finiteNumber(e.freedGB, 0), 0);
|
|
185
|
+
const pressure = passes.filter((e) => (typeof e.mode === "string" && e.mode ? e.mode : "pressure") === "pressure").length;
|
|
186
|
+
const base = `reclaim in the last 24h freed ${formatGB(freed)} over ${plural(passes.length, "pass", "passes")}`;
|
|
187
|
+
return pressure > 0 ? `${base}, ${pressure} under pressure` : base;
|
|
188
|
+
}
|
|
189
|
+
/** The `passing merge checks …` phrase for the `check` row: median and slowest wall clock of
|
|
190
|
+
* the passed full-suite runs (`merge`, `batch`, `baseline` scopes — a `gate` check can run a
|
|
191
|
+
* subset, and failed or skipped runs do not measure a full pass). The lower median keeps the
|
|
192
|
+
* figure a run that happened. Undefined when no event qualifies. */
|
|
193
|
+
export function checkDurationPhrase(events) {
|
|
194
|
+
const durations = events
|
|
195
|
+
.filter(isPassingFullCheck)
|
|
196
|
+
.map((e) => finiteNumber(e.durationMs, 0))
|
|
197
|
+
.filter((ms) => ms > 0);
|
|
198
|
+
if (durations.length === 0)
|
|
199
|
+
return undefined;
|
|
200
|
+
const median = lowerMedian(durations);
|
|
201
|
+
const slowest = Math.max(...durations);
|
|
202
|
+
return `passing merge checks in the last 24h took ${shortSpanPhrase(median)} median, ${shortSpanPhrase(slowest)} slowest (${plural(durations.length, "run")})`;
|
|
203
|
+
}
|
|
204
|
+
/** Declared project check — names what the review gate's deterministic pre-check, the
|
|
205
|
+
* red-main baseline, and redeploy's green check will run, without running it: a configured
|
|
206
|
+
* `check.command` (plans/portability.md §6/7), else the npm auto-detection. None declared is
|
|
207
|
+
* a warn, not informational (the 2026-09-05 stance predates non-npm targets being supported,
|
|
208
|
+
* when a warn would have been noise no operator could act on — with `check.command` available
|
|
209
|
+
* the warning is actionable): the three gates degrade to "no check" and a silently absent
|
|
210
|
+
* safety layer is the failure this check exists to surface. Warn does not fail the exit code
|
|
211
|
+
* (the fallback check's precedent, doctor-model-checks.ts's checkFallbackModel). */
|
|
212
|
+
export function checkBuildCheck(root, config) {
|
|
213
|
+
const check = detectBuildCheck(root, config ?? undefined);
|
|
214
|
+
if (!check)
|
|
215
|
+
return warnOutcome("none declared — the review gate's build pre-check, the red-main baseline, and redeploy's green check are all off (set `check.command` in tumwater.json for a non-npm repo)");
|
|
216
|
+
// The in-lock command from mergeCommandOf (absent mergeCommand falls back to the gate
|
|
217
|
+
// command): the row names which command the merge lock runs and where the full check lands,
|
|
218
|
+
// facts a reader cannot get from detectBuildCheck alone. The after-merge roles are not named
|
|
219
|
+
// here — they live under `review`, which this slice does not hold, and the `review` row in
|
|
220
|
+
// docs/config.md documents them.
|
|
221
|
+
const lockPhrase = (() => {
|
|
222
|
+
const mc = mergeCommandOf(config ?? undefined);
|
|
223
|
+
return mc !== undefined
|
|
224
|
+
? `merge lock runs ${mc}, full check after merging`
|
|
225
|
+
: "full check in the merge lock";
|
|
226
|
+
})();
|
|
227
|
+
const phrase = checkDurationPhrase(readEventsSince(root, DAY_MS).events);
|
|
228
|
+
const withTiming = (detail) => okOutcome(`${detail} · ${lockPhrase}${phrase ? ` · ${phrase}` : ""}`);
|
|
229
|
+
if (check.kind === "command") {
|
|
230
|
+
const where = path.relative(root, check.cwd);
|
|
231
|
+
return withTiming(where ? `${check.command} (cwd ${where})` : check.command);
|
|
232
|
+
}
|
|
233
|
+
const where = path.relative(root, check.rootDir);
|
|
234
|
+
return withTiming(where ? `npm ${check.script} in ${where}` : `npm ${check.script}`);
|
|
235
|
+
}
|
|
236
|
+
/** The review gate's flake memory surfaced read-only: names which checks the gate kept excusing as
|
|
237
|
+
* flakes over its own KNOWN_FLAKE_WINDOW_MS window. The gate logs `gate check failed then passed on
|
|
238
|
+
* retry — flaky: <headline>` for each failure that passed on its one immediate re-run, and the
|
|
239
|
+
* in-lock merge check logs the same under MERGE_FLAKY_WARNING_PREFIX when its kept-pin retry passes
|
|
240
|
+
* (src/review/known-flakes.ts); that record is internal to the gate's matching, so a genuinely
|
|
241
|
+
* flaky check silently burns a re-run per merge and hides real instability — exactly the
|
|
242
|
+
* information that explains intermittent rejections. This check reads the same window and folds the
|
|
243
|
+
* same normalized headline the gate matches with (flakeCounts, so the prefix slice-offset and
|
|
244
|
+
* cluster normalization cannot drift), and warns only when a normalized check flapped more than
|
|
245
|
+
* once, so a routine one-off flake stays quiet. ok/warn only — never a fail — so a flaky suite
|
|
246
|
+
* cannot fail a scripted doctor pre-flight (the checkStalledPlans precedent; the exit-code rule is
|
|
247
|
+
* outcome.ts's). */
|
|
248
|
+
export function checkFlakyChecks(root, now = Date.now()) {
|
|
249
|
+
const { events } = readEventsSince(root, KNOWN_FLAKE_WINDOW_MS);
|
|
250
|
+
const flapped = flakeCounts(events).filter(([, n]) => n >= 2);
|
|
251
|
+
const phrases = [];
|
|
252
|
+
if (flapped.length > 0) {
|
|
253
|
+
const render = ([k, n]) => `"${k}" ×${n}`;
|
|
254
|
+
const [first, ...rest] = flapped;
|
|
255
|
+
const more = rest.length > 0 ? ` (and ${rest.length} more: ${rest.map(render).join(", ")})` : "";
|
|
256
|
+
phrases.push(`${agree(flapped.length, "check", "checks")} flapped more than once in the last 24h: ${render(first)}${more}`);
|
|
257
|
+
}
|
|
258
|
+
const q = quarantineWarning(loadQuarantine(root), now);
|
|
259
|
+
if (q !== null)
|
|
260
|
+
phrases.push(q);
|
|
261
|
+
if (phrases.length === 0)
|
|
262
|
+
return okOutcome("no check flapped more than once in the last 24h");
|
|
263
|
+
return warnOutcome(phrases.join("; "));
|
|
264
|
+
}
|
|
265
|
+
/** Build provenance — is the harness about to run (this process's dist/) the code main
|
|
266
|
+
* describes? Only meaningful when this project IS the harness (isSelfHosted); elsewhere the
|
|
267
|
+
* stamp is reported as-is. A stale build is a warning, not a failure: the team runs, just not
|
|
268
|
+
* the newest code, and auto-restart (or a rebuild + restart) resolves it. `info`, `head` and
|
|
269
|
+
* `running` are injectable so tests can exercise every branch without compiling anything. */
|
|
270
|
+
export async function checkBuild(root, info = readBuildInfo(), head = undefined,
|
|
271
|
+
/** What the running orchestrator published about this build (orchestrator.json), when one is
|
|
272
|
+
* up. A stale build whose restart was REFUSED must not be reported with the boilerplate
|
|
273
|
+
* "auto-restart does this for a running team": nothing will happen until main moves, and
|
|
274
|
+
* doctor is where an operator goes to find out why. */
|
|
275
|
+
running = null) {
|
|
276
|
+
if (!info)
|
|
277
|
+
return okOutcome("no build stamp — dist/ compiled without `npm run build`");
|
|
278
|
+
const sha = shortSha(info.sha);
|
|
279
|
+
if (!(await isSelfHosted(root, info)))
|
|
280
|
+
// Not "dist/ from …": in someone else's project that names a dist/ directory the project
|
|
281
|
+
// does not have — the stamp read here is the running harness install's own, so say that.
|
|
282
|
+
return okOutcome(`tumwater version ${sha} (this project is not tumwater itself)`);
|
|
283
|
+
const mainHead = head === undefined ? await currentHead(root) : head;
|
|
284
|
+
if (!mainHead)
|
|
285
|
+
return okOutcome(`dist/ from ${sha}`);
|
|
286
|
+
const stale = await buildStaleness(root, info.sha, mainHead);
|
|
287
|
+
if (!stale)
|
|
288
|
+
return okOutcome(`dist/ from ${sha}`);
|
|
289
|
+
if (stale.stale) {
|
|
290
|
+
const next = running?.restartBlocked
|
|
291
|
+
? `; auto-restart is BLOCKED (${running.restartBlocked}) and will not retry until main moves or the block clears`
|
|
292
|
+
: running?.restartPending
|
|
293
|
+
? "; auto-restart is under way"
|
|
294
|
+
: "; run `npm run build` and restart `tumwater start` (auto-restart does this for a running team)";
|
|
295
|
+
return warnOutcome(`dist/ from ${sha} is stale — main has ${stale.aheadCommits} later commit(s) touching ${STALE_INPUTS_LABEL}${next}`);
|
|
296
|
+
}
|
|
297
|
+
return okOutcome(`dist/ from ${sha}, matches main`);
|
|
298
|
+
}
|
|
299
|
+
/** The primary checkout's HEAD sha, or null when it cannot be resolved (no repo) —
|
|
300
|
+
* git/git.ts's refSha, the one home of the rev-parse probe. */
|
|
301
|
+
function currentHead(root) {
|
|
302
|
+
return refSha(root, "HEAD");
|
|
303
|
+
}
|
|
@@ -1,43 +1,64 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import { loadConfigSafe } from "../config/config.js";
|
|
3
2
|
import { envPath } from "../files/files.js";
|
|
4
3
|
import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
|
|
5
4
|
import { systemProcessProbe } from "../process/process-table.js";
|
|
6
5
|
import { checkOrphans } from "./doctor-orphans.js";
|
|
7
|
-
import { checkBudget,
|
|
6
|
+
import { checkBudget, checkErrorStreaks, checkMainRed, checkMaintenanceQuota, checkPause, checkProviderHolds, checkQuietHours, checkSubscriptionWindow } from "./doctor-holds.js";
|
|
8
7
|
import { checkRootInstall } from "./doctor-install.js";
|
|
9
8
|
import { checkNotify } from "./doctor-notify.js";
|
|
10
9
|
import { checkLaunchServicesPorts } from "./doctor-launch-services.js";
|
|
11
10
|
import { plural } from "../text/phrases.js";
|
|
12
11
|
import { shortSha } from "../text/format.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
12
|
+
import { failOutcome } from "./outcome.js";
|
|
13
|
+
import { errorMessage } from "../text/text.js";
|
|
14
|
+
import { checkBackendBinary, checkBrief, checkBootstrap, checkClaudeTranscripts, checkExecPath, checkFunes, checkGitBinary, checkConfigKeyNames, checkInit, checkOpencodeVersion, checkNodeVersion, checkPiExtensions, checkRepo, } from "./doctor-checks.js";
|
|
15
|
+
import { checkBuild, checkBuildCheck, checkDiskSpace, checkEventLog, checkFlakyChecks, checkStateDir, } from "./doctor-project-checks.js";
|
|
16
|
+
import { sampleFreeBytes } from "../gates/disk-gate.js";
|
|
17
|
+
import { checkCheckContention, checkLeftoverRoleBranches, checkMergeLock, checkProviderConcurrency, checkProviderContention, checkTumwaterPins, checkWorkInstances, checkWorktreePool, } from "./doctor-contention-checks.js";
|
|
18
|
+
import { checkFallbackChain, checkFallbackModel, checkFeedbackFiling, checkImageInput, checkTierModels } from "./doctor-model-checks.js";
|
|
19
|
+
import { checkAnthroApiKey, checkClaudeAuth, checkClaudeVersion } from "./doctor-claude-checks.js";
|
|
20
|
+
import { checkOpencode } from "./doctor-opencode-checks.js";
|
|
21
|
+
import { checkCodexAccount, checkCodexAuth, checkCodexHome, checkCodexLeaks, checkCodexModels, checkCodexSessions, checkCodexWriteGuard, checkCodexVersion, } from "./doctor-codex-checks.js";
|
|
22
|
+
import { backendQualifiedSelector } from "../config/config-views.js";
|
|
16
23
|
import { seamsInUse } from "../config/config-fallbacks.js";
|
|
17
|
-
import { checkBacklogHeadings, checkFixClaims, checkLiveFinished, checkPlanDependencies,
|
|
24
|
+
import { checkBacklogHeadings, checkFixClaims, checkLiveFinished, checkPlanDependencies, checkStalledPlans, checkStrandedPlans, checkValidationGaps } from "./doctor-backlog-plans.js";
|
|
25
|
+
import { checkRefusedEntries, checkRegressedFixes, checkStaleBugs, checkStaleQuestions, checkWaitingBugs } from "./doctor-backlog-bugs.js";
|
|
26
|
+
import { checkMergeQueueFiles, checkStaleQueuedMerges, checkStalePrompts, checkUncommittedBacklog } from "./doctor-backlog.js";
|
|
18
27
|
/** Pre-flight environment check (`tumwater doctor`). The harness's preconditions are
|
|
19
28
|
* scattered across fail-fast checks that each command re-runs on its own (requireReadyRepo in
|
|
20
29
|
* cli.ts walks git-binary → repo → tumwater.json → commits and stops at the first failure;
|
|
21
|
-
*
|
|
30
|
+
* cmdStart adds pi-on-PATH and orchestrator-alive), so a user facing a partially initialized or
|
|
22
31
|
* drifted environment gets one error at a time. doctor runs every check, reports each result
|
|
23
32
|
* individually instead of stopping at the first failure, and exits 0/1 so it can be scripted —
|
|
24
33
|
* the pre-flight sibling of `status --json`, which queries live team state while doctor
|
|
25
34
|
* verdicts on the environment. Every check is read-only against .tumwater/ (works with or
|
|
26
35
|
* without a running harness) and none runs the project's build/test: that can take minutes and
|
|
27
36
|
* belongs in the review gate / red-main check, not a pre-flight. The individual checks live in
|
|
28
|
-
* doctor-checks.ts
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
* doctor-checks.ts, doctor-project-checks.ts, doctor-contention-checks.ts, doctor-model-checks.ts,
|
|
38
|
+
* doctor-claude-checks.ts, doctor-opencode-checks.ts, doctor-backlog.ts,
|
|
39
|
+
* doctor-backlog-plans.ts and doctor-backlog-bugs.ts;
|
|
40
|
+
* this module composes them into the report. */
|
|
41
|
+
/** Run one doctor check, turning a throw or rejection into a fail row: a check that crashed
|
|
42
|
+
* verified nothing, so it must not read as a pass or abort the report. */
|
|
43
|
+
export async function runDoctorCheck(run) {
|
|
44
|
+
try {
|
|
45
|
+
return await run();
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return failOutcome(`the check threw: ${errorMessage(err)}`);
|
|
49
|
+
}
|
|
34
50
|
}
|
|
35
51
|
/** Run every check in order and compose the report. Read-only against .tumwater/ by
|
|
36
52
|
* construction — no check removes or repairs anything (the state-dir probe writes a temp file
|
|
37
53
|
* and deletes it again; the orphan check reports processes, never signals them) — so doctor
|
|
38
54
|
* works identically with or without a running harness. `probe` feeds the orphan and port
|
|
39
|
-
* checks, so tests can pin the report without reading the host's process table.
|
|
40
|
-
|
|
55
|
+
* checks, so tests can pin the report without reading the host's process table. `diskSample`
|
|
56
|
+
* feeds the disk check the same way, so tests can pin the report without reading the host's
|
|
57
|
+
* real free space (a hosted runner under the 10 GB floor would fail its row). `only` runs
|
|
58
|
+
* just the named checks, in report order; a skipped check is never evaluated, and a named
|
|
59
|
+
* conditional check that returns null leaves no row. A check that throws becomes its own fail
|
|
60
|
+
* row, and every other check still runs. */
|
|
61
|
+
export async function runDoctor(root, pathEnv = envPath(), probe = systemProcessProbe, probeImages = "never", only, diskSample = sampleFreeBytes) {
|
|
41
62
|
// Loaded once for the checks that read config (repo's baseBranch); a broken file stays
|
|
42
63
|
// undefined — checkInit reports it verbatim — so doctor still runs every other check.
|
|
43
64
|
const { config } = loadConfigSafe(root);
|
|
@@ -48,7 +69,7 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
|
|
|
48
69
|
const routing = config
|
|
49
70
|
? seamsInUse(config).map((s) => ({
|
|
50
71
|
label: s.label,
|
|
51
|
-
selector: s.model !== undefined ?
|
|
72
|
+
selector: s.model !== undefined ? backendQualifiedSelector({ ...s, model: s.model }) : "no model declared",
|
|
52
73
|
backend: s.agent ?? "pi",
|
|
53
74
|
}))
|
|
54
75
|
: [];
|
|
@@ -56,80 +77,103 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
|
|
|
56
77
|
const header = orchestratorAlive(root, info) && info
|
|
57
78
|
? `tumwater doctor — running (pid ${info.pid}${info.build ? `, version ${shortSha(info.build.sha)}${info.build.stale ? " — update available" : ""}${info.build.restartBlocked ? " (restart blocked)" : ""}` : ""})`
|
|
58
79
|
: "tumwater doctor — not running";
|
|
59
|
-
const
|
|
60
|
-
{ name: "node",
|
|
61
|
-
{ name: "
|
|
62
|
-
{ name: "
|
|
63
|
-
{ name: "
|
|
64
|
-
{ name: "
|
|
65
|
-
{ name: "
|
|
80
|
+
const steps = [
|
|
81
|
+
{ name: "node", run: () => checkNodeVersion() },
|
|
82
|
+
{ name: "node binary", run: () => checkExecPath() },
|
|
83
|
+
{ name: "git binary", run: () => checkGitBinary(pathEnv) },
|
|
84
|
+
{ name: "repo", run: () => checkRepo(root, config) },
|
|
85
|
+
{ name: "init", run: () => checkInit(root) },
|
|
86
|
+
{ name: "config key names", run: () => checkConfigKeyNames(root) },
|
|
87
|
+
{ name: "brief", run: () => checkBrief(root) },
|
|
66
88
|
// New-project bootstrap (plans/work-ratio.md, part 2/2): an active hold reports as ok
|
|
67
89
|
// information, never a warning — a young project is not misconfigured.
|
|
68
|
-
{ name: "bootstrap",
|
|
69
|
-
{ name: "pause",
|
|
70
|
-
{ name: "quiet hours",
|
|
71
|
-
{ name: "budget",
|
|
72
|
-
{ name: "claude window",
|
|
73
|
-
{ name: "
|
|
74
|
-
{ name: "
|
|
75
|
-
{ name: "
|
|
76
|
-
{ name: "provider
|
|
77
|
-
{ name: "
|
|
78
|
-
{ name: "
|
|
79
|
-
{ name: "
|
|
80
|
-
{ name: "
|
|
81
|
-
{ name: "
|
|
82
|
-
{ name: "
|
|
83
|
-
{ name: "
|
|
84
|
-
{ name: "
|
|
85
|
-
{ name: "claude
|
|
86
|
-
{ name: "
|
|
87
|
-
{ name: "
|
|
88
|
-
{ name: "
|
|
90
|
+
{ name: "bootstrap", run: () => checkBootstrap(root, config) },
|
|
91
|
+
{ name: "pause", run: () => checkPause(root) },
|
|
92
|
+
{ name: "quiet hours", run: () => checkQuietHours(config) },
|
|
93
|
+
{ name: "budget", run: () => checkBudget(root, config, (orchestratorAlive(root, info) && info?.budget) || null) },
|
|
94
|
+
{ name: "claude window", run: () => checkSubscriptionWindow("claude", root, undefined, (orchestratorAlive(root, info) && info?.chain) || undefined, config) },
|
|
95
|
+
{ name: "codex window", run: () => checkSubscriptionWindow("codex", root, undefined, (orchestratorAlive(root, info) && info?.chain) || undefined, config) },
|
|
96
|
+
{ name: "provider waits", run: () => checkProviderHolds(root) },
|
|
97
|
+
{ name: "maintenance allowance", run: () => checkMaintenanceQuota(root, config, orchestratorAlive(root, info)) },
|
|
98
|
+
{ name: "provider concurrency", run: () => checkProviderConcurrency(config, root) },
|
|
99
|
+
{ name: "provider contention", run: () => checkProviderContention(root, config) },
|
|
100
|
+
{ name: "check contention", run: () => checkCheckContention(root, config) },
|
|
101
|
+
{ name: "repeated failures", run: () => checkErrorStreaks(root, config) },
|
|
102
|
+
{ name: "main's check", run: () => checkMainRed(root, config) },
|
|
103
|
+
{ name: "over-cap model", run: () => checkFallbackModel(root, undefined, (orchestratorAlive(root, info) && info?.fallbackDemoted) || null) },
|
|
104
|
+
{ name: "fallback chain", run: () => checkFallbackChain(root) },
|
|
105
|
+
{ name: "models for each tier", run: () => checkTierModels(root) },
|
|
106
|
+
{ name: "image input", run: () => checkImageInput(root, undefined, undefined, undefined, probeImages, pathEnv) },
|
|
107
|
+
{ name: "claude auth", run: () => checkClaudeAuth(root, pathEnv) },
|
|
108
|
+
{ name: "claude api key", run: () => checkAnthroApiKey(root, undefined, pathEnv) },
|
|
109
|
+
{ name: "claude version", run: () => checkClaudeVersion(root, pathEnv) },
|
|
110
|
+
{ name: "opencode version", run: () => checkOpencodeVersion(root, pathEnv) },
|
|
111
|
+
{ name: "opencode", run: () => checkOpencode(root, pathEnv) },
|
|
112
|
+
{ name: "codex version", run: () => checkCodexVersion(root, pathEnv) },
|
|
113
|
+
{ name: "codex login", run: () => checkCodexAuth(root, pathEnv) },
|
|
114
|
+
{ name: "codex home", run: () => checkCodexHome(root) },
|
|
115
|
+
{ name: "codex models", run: () => checkCodexModels(root) },
|
|
116
|
+
{ name: "codex account", run: () => checkCodexAccount(root) },
|
|
117
|
+
{ name: "codex leaks", run: () => checkCodexLeaks(root) },
|
|
118
|
+
{ name: "codex sessions", run: () => checkCodexSessions(root) },
|
|
119
|
+
{ name: "codex write guard", run: () => checkCodexWriteGuard(root, pathEnv) },
|
|
120
|
+
{ name: "backend binary", run: () => checkBackendBinary(root, pathEnv) },
|
|
121
|
+
{ name: "feedback filing", run: () => checkFeedbackFiling(root, pathEnv) },
|
|
89
122
|
// The transcript total is informational and only read when a seam routes to claude.
|
|
90
|
-
{ name: "claude transcripts",
|
|
123
|
+
{ name: "claude transcripts", run: () => checkClaudeTranscripts(config) },
|
|
91
124
|
// The receive-side guards name what a running team skips, so an operator tool meant for the
|
|
92
125
|
// team (a pi package, an extension file, an mcp.json server, or funes) does not vanish
|
|
93
126
|
// silently. Each check returns null to omit its row: the pi row for a team with no pi seam,
|
|
94
|
-
// the funes row when funes is not on PATH
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
{ name: "
|
|
98
|
-
{ name: "
|
|
99
|
-
{ name: "
|
|
100
|
-
{ name: "
|
|
101
|
-
{ name: "
|
|
102
|
-
{ name: "
|
|
103
|
-
{ name: "
|
|
127
|
+
// the funes row when funes is not on PATH, and the claude/codex window rows when that backend
|
|
128
|
+
// has no subscription account.
|
|
129
|
+
{ name: "pi extensions", run: () => checkPiExtensions(config, undefined, pathEnv) },
|
|
130
|
+
{ name: "funes", run: () => checkFunes(config, pathEnv) },
|
|
131
|
+
{ name: "state dir", run: () => checkStateDir(root) },
|
|
132
|
+
{ name: "event log", run: () => checkEventLog(root) },
|
|
133
|
+
{ name: "notify", run: () => checkNotify(root, config) },
|
|
134
|
+
{ name: "merge lock", run: () => checkMergeLock(root) },
|
|
135
|
+
{ name: "workspaces", run: () => checkWorktreePool(root) },
|
|
136
|
+
{ name: "parallel instances", run: () => checkWorkInstances(root, config) },
|
|
137
|
+
{ name: "leftover role branches", run: () => checkLeftoverRoleBranches(root, config) },
|
|
138
|
+
{ name: "tumwater pins", run: () => checkTumwaterPins(root) },
|
|
104
139
|
// The disk floor (plans/disk-floor.md, part 1/4): a broken config stays undefined (checkInit
|
|
105
140
|
// reports it), so the check falls back to the 10 GB default rather than skipping itself.
|
|
106
|
-
{ name: "disk space",
|
|
107
|
-
{ name: "check",
|
|
108
|
-
{ name: "root install",
|
|
141
|
+
{ name: "disk space", run: () => checkDiskSpace(root, config?.diskHoldGB ?? 10, config?.diskReclaimGB ?? 40, diskSample) },
|
|
142
|
+
{ name: "check", run: () => checkBuildCheck(root, config) },
|
|
143
|
+
{ name: "root install", run: () => checkRootInstall(root) },
|
|
109
144
|
// The review gate's flake memory, surfaced read-only: naming which checks the gate kept
|
|
110
145
|
// excusing as flakes (ok/warn only, so a flaky suite never fails a scripted pre-flight).
|
|
111
|
-
{ name: "flaky checks",
|
|
112
|
-
{ name: "claimed bugs",
|
|
113
|
-
{ name: "validation gaps",
|
|
114
|
-
{ name: "plans stuck in Planned",
|
|
115
|
-
{ name: "backlog headings",
|
|
116
|
-
{ name: "backlog archive",
|
|
117
|
-
{ name: "refusals",
|
|
118
|
-
{ name: "stalled plans",
|
|
119
|
-
{ name: "plan dependencies",
|
|
120
|
-
{ name: "stale questions",
|
|
121
|
-
{ name: "stale bugs",
|
|
122
|
-
{ name: "waiting bugs",
|
|
123
|
-
{ name: "regressed fixes",
|
|
124
|
-
{ name: "stale merge-queue entries",
|
|
125
|
-
{ name: "
|
|
126
|
-
{ name: "
|
|
146
|
+
{ name: "flaky checks", run: () => checkFlakyChecks(root) },
|
|
147
|
+
{ name: "claimed bugs", run: () => checkFixClaims(root) },
|
|
148
|
+
{ name: "validation gaps", run: () => checkValidationGaps(root) },
|
|
149
|
+
{ name: "plans stuck in Planned", run: () => checkStrandedPlans(root) },
|
|
150
|
+
{ name: "backlog headings", run: () => checkBacklogHeadings(root) },
|
|
151
|
+
{ name: "backlog archive", run: () => checkLiveFinished(root) },
|
|
152
|
+
{ name: "refusals", run: () => checkRefusedEntries(root) },
|
|
153
|
+
{ name: "stalled plans", run: () => checkStalledPlans(root) },
|
|
154
|
+
{ name: "plan dependencies", run: () => checkPlanDependencies(root) },
|
|
155
|
+
{ name: "stale questions", run: () => checkStaleQuestions(root) },
|
|
156
|
+
{ name: "stale bugs", run: () => checkStaleBugs(root) },
|
|
157
|
+
{ name: "waiting bugs", run: () => checkWaitingBugs(root) },
|
|
158
|
+
{ name: "regressed fixes", run: () => checkRegressedFixes(root) },
|
|
159
|
+
{ name: "stale merge-queue entries", run: () => checkStaleQueuedMerges(root, Date.now()) },
|
|
160
|
+
{ name: "merge-queue files", run: () => checkMergeQueueFiles(root) },
|
|
161
|
+
{ name: "stale prompts", run: () => checkStalePrompts(root, Date.now()) },
|
|
162
|
+
{ name: "uncommitted backlog", run: () => checkUncommittedBacklog(root, config) },
|
|
127
163
|
// The running team's own view of its build (staleness and what auto-restart made of it)
|
|
128
164
|
// when there is one; without it the check still stands on its own git comparison.
|
|
129
|
-
{ name: "update",
|
|
130
|
-
{ name: "orphans",
|
|
131
|
-
{ name: "macOS port leak",
|
|
165
|
+
{ name: "update", run: () => checkBuild(root, undefined, undefined, (orchestratorAlive(root, info) && info?.build) || null) },
|
|
166
|
+
{ name: "orphans", run: () => checkOrphans(root, probe) },
|
|
167
|
+
{ name: "macOS port leak", run: () => checkLaunchServicesPorts(probe) },
|
|
132
168
|
];
|
|
169
|
+
const checks = [];
|
|
170
|
+
for (const step of steps) {
|
|
171
|
+
if (only && !only.includes(step.name))
|
|
172
|
+
continue;
|
|
173
|
+
const outcome = await runDoctorCheck(step.run);
|
|
174
|
+
if (outcome !== null)
|
|
175
|
+
checks.push({ name: step.name, ...outcome });
|
|
176
|
+
}
|
|
133
177
|
const problems = checks.filter((c) => c.level === "fail").length;
|
|
134
178
|
return { header, checks, routing, verdict: problems === 0 ? "ready to run" : plural(problems, "problem") };
|
|
135
179
|
}
|