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,7 +1,6 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import { allRoleIds } from "../roles/roles.js";
|
|
4
|
-
import {
|
|
3
|
+
import { runnerIds } from "../roles/runner-ids.js";
|
|
5
4
|
import { cachedByStat } from "../files/stat-cache.js";
|
|
6
5
|
import { CONFIG_BASENAME, configPath } from "../paths.js";
|
|
7
6
|
import { errorMessage } from "../text/text.js";
|
|
@@ -31,7 +30,7 @@ export function parseJsonConfig(file, basename) {
|
|
|
31
30
|
return { raw, renamed };
|
|
32
31
|
}
|
|
33
32
|
/** Build the default TumwaterConfig: every role enabled except the opt-in selfcheck (steward on its
|
|
34
|
-
* slow ~6 h
|
|
33
|
+
* slow ~6 h run, qa and selfcheck on ~2 h, readme on 30 min, plan and organize on 1 h), with
|
|
35
34
|
* defaults for concurrency, timeouts, log size, retention, thrash detection, idle backoff,
|
|
36
35
|
* self-redeploy, and review settings. The return type is the resolved view (config-views.ts): a
|
|
37
36
|
* freshly defaulted config has no model map to resolve — only a loaded file can carry one — so
|
|
@@ -47,18 +46,22 @@ export function defaultConfig() {
|
|
|
47
46
|
roles.steward = { enabled: true, minRunIntervalSeconds: 21600 };
|
|
48
47
|
// QA exercises the product like a user on a ~2 h clock: user flows change slower than code.
|
|
49
48
|
roles.qa = { enabled: true, minRunIntervalSeconds: 7200 };
|
|
50
|
-
// Selfcheck
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
49
|
+
// Selfcheck and feedback are the two opt-in roles, both on the same ~2 h clock: the digest is
|
|
50
|
+
// a windowed view, so a run a couple of hours apart reads a new window rather than re-filing
|
|
51
|
+
// the same cluster. Selfcheck's evidence is this harness's own event log and its charter files
|
|
52
|
+
// only harness misbehavior, so on any other project every finding is a tumwater bug filed into
|
|
53
|
+
// the wrong backlog, which that project's bugfix role rightly rejects — that is why it is
|
|
54
|
+
// opt-in. Feedback files the same digest's findings as GitHub issues on znation/tumwater, so
|
|
55
|
+
// nothing leaves the machine until the operator opts in (plans/feedback-role.md). Tumwater's
|
|
56
|
+
// own tumwater.example.json keeps selfcheck (dogfood) and not feedback, so this repo's
|
|
57
|
+
// checkouts still get their findings at `init` and file nothing upstream.
|
|
56
58
|
roles.selfcheck = { enabled: false, minRunIntervalSeconds: 7200 };
|
|
59
|
+
roles.feedback = { enabled: false, minRunIntervalSeconds: 7200 };
|
|
57
60
|
// The bookkeeping roles run on slower clocks too. In dogfood readme (95 commits, 82 of them
|
|
58
61
|
// status syncs) and plan (80, 44 of them refine/re-audit notes) were 30% of every commit and
|
|
59
|
-
// 20% of every
|
|
62
|
+
// 20% of every run: readme woke on every merge to restamp one line, and plan re-audited plans
|
|
60
63
|
// nobody had touched. A 30 min readme clock batches a burst of merges into one sync; a 1 h
|
|
61
|
-
// plan clock leaves the slots to the
|
|
64
|
+
// plan clock leaves the slots to the roles that ship. Both still wake early when main moves —
|
|
62
65
|
// the clock only bounds how often.
|
|
63
66
|
roles.readme = { enabled: true, minRunIntervalSeconds: 1800 };
|
|
64
67
|
roles.plan = { enabled: true, minRunIntervalSeconds: 3600 };
|
|
@@ -74,7 +77,7 @@ export function defaultConfig() {
|
|
|
74
77
|
piArgs: [],
|
|
75
78
|
// Agent backends (plans/agent-backends.md, part 4a/7): empty until a file names one.
|
|
76
79
|
// Keeping the map always present means read sites need no `?? {}`; the legacy
|
|
77
|
-
// agentBin/piArgs are read as pi's entry through
|
|
80
|
+
// agentBin/piArgs are read as pi's entry through resolvedBackendConfig (config-views.ts).
|
|
78
81
|
backends: {},
|
|
79
82
|
maxConcurrent: 6,
|
|
80
83
|
// Three is the team's realistic concurrent-role count: a busy queue coalesces the
|
|
@@ -84,7 +87,7 @@ export function defaultConfig() {
|
|
|
84
87
|
// without a burst stacking every check on the host beside the authors' own test runs.
|
|
85
88
|
maxConcurrentChecks: 2,
|
|
86
89
|
minRunIntervalSeconds: 20,
|
|
87
|
-
// Four hours: the largest
|
|
90
|
+
// Four hours: the largest run a hosted model legitimately took and still merged work
|
|
88
91
|
// measured 185 minutes (BUGS.md 2026-09-29), so 30 minutes discarded real work and burned
|
|
89
92
|
// every resume before the task could finish. A run that stops making progress is still
|
|
90
93
|
// bounded earlier by the quiet watchdog, not by this cap.
|
|
@@ -103,12 +106,12 @@ export function defaultConfig() {
|
|
|
103
106
|
maxDailyCostUsd: 50,
|
|
104
107
|
// Work ratio 1a/4 and 1b/4 (plans/work-ratio.md): the maintenance allowance is
|
|
105
108
|
// maintenancePerWorkMerge × work merges + 12 per rolling 24 h, and the scheduler holds
|
|
106
|
-
// maintenance
|
|
109
|
+
// maintenance roles once the window reaches it. Two keeps roughly two upkeep merges per
|
|
107
110
|
// shipped change while the fixed floor lets hygiene run on an empty backlog.
|
|
108
111
|
maintenancePerWorkMerge: 2,
|
|
109
112
|
// Disk floor (plans/disk-floor.md, part 1/4): when the volume the worktrees live on drops
|
|
110
113
|
// below this many GB free, the team holds new work until it climbs 5 GB back above it.
|
|
111
|
-
// 10 GB leaves room for an in-flight
|
|
114
|
+
// 10 GB leaves room for an in-flight run to finish and a state write to land. 0 disables.
|
|
112
115
|
diskHoldGB: 10,
|
|
113
116
|
// Pressure reclaim (plans/disk-floor.md, part 2/4): below this many GB free, a background
|
|
114
117
|
// pass deletes gitignored build outputs from idle worktrees before the 10 GB hold engages.
|
|
@@ -119,9 +122,9 @@ export function defaultConfig() {
|
|
|
119
122
|
// whatever the free space. A day catches paused, retired and rarely due roles without
|
|
120
123
|
// sweeping a worktree that merely slept through the night. 0 disables idle reclaim.
|
|
121
124
|
worktreeIdleReclaimHours: 24,
|
|
122
|
-
// Friction is flagged only when a changed
|
|
123
|
-
// (src/
|
|
124
|
-
// model's ordinary 40+ turn / few-minute
|
|
125
|
+
// Friction is flagged only when a changed run burns BOTH thresholds
|
|
126
|
+
// (src/run/run-stage.ts): the absolute turn count alone measures model speed, so a fast
|
|
127
|
+
// model's ordinary 40+ turn / few-minute run stays unflagged, while a genuinely hard run
|
|
125
128
|
// that burned 40+ turns over half an hour or more still is (BUGS.md 2026-09-19). Tuned for
|
|
126
129
|
// the ~27B local model at 24-35 tok/s, where 40 turns is half an hour of work.
|
|
127
130
|
thrashTurns: 40,
|
|
@@ -134,9 +137,16 @@ export function defaultConfig() {
|
|
|
134
137
|
// writes a request file the harness applies to the live config before any commit, so no
|
|
135
138
|
// diff can ever touch tumwater.json and the exemption — once the only thing keeping an
|
|
136
139
|
// explicit user instruction from being discarded by a rejected review — has nothing left
|
|
137
|
-
// to exempt. Removing it also closes the path where a director
|
|
140
|
+
// to exempt. Removing it also closes the path where a director run could merge a mixed
|
|
138
141
|
// doc-plus-config diff unreviewed.
|
|
139
|
-
|
|
142
|
+
// Reviewer as a regular role (part 1/4): the high-volume, low-blast-radius roles — bugfix,
|
|
143
|
+
// readme, organize, plus clean, dedupe, coverage — are reviewed AFTER they merge, on their
|
|
144
|
+
// merged commit (src/review/post-merge-review.ts), instead of holding the merge queue for a
|
|
145
|
+
// reviewer run on their (usually trivial) diff; a rejection reverts the commit back to the
|
|
146
|
+
// author as a revision. feature, director, perf, improve and custom roles stay pre-merge. A
|
|
147
|
+
// config that omits `review.afterMerge` inherits this, and `"afterMerge": []` opts out (back
|
|
148
|
+
// to reviewing before merging).
|
|
149
|
+
review: { enabled: true, exemptPaths: ["*.md", "docs/**"], afterMerge: ["bugfix", "clean", "coverage", "dedupe", "organize", "readme"] },
|
|
140
150
|
customRoles: [],
|
|
141
151
|
roles,
|
|
142
152
|
};
|
|
@@ -161,7 +171,7 @@ export function overlayDefaults(base, cfg) {
|
|
|
161
171
|
review: { ...base.review, ...(cfg.review ?? {}) },
|
|
162
172
|
piArgs: cfg.piArgs ?? base.piArgs,
|
|
163
173
|
// Clone each backend entry (and its args array) so the result owns its data like every
|
|
164
|
-
// other section. The legacy agentBin/piArgs stay beside it;
|
|
174
|
+
// other section. The legacy agentBin/piArgs stay beside it; resolvedBackendConfig picks
|
|
165
175
|
// between them.
|
|
166
176
|
backends: Object.fromEntries(Object.entries(cfg.backends ?? base.backends ?? {}).map(([id, a]) => [
|
|
167
177
|
id,
|
|
@@ -172,16 +182,16 @@ export function overlayDefaults(base, cfg) {
|
|
|
172
182
|
customRoles: (cfg.customRoles ?? []).map((c) => ({ ...c })),
|
|
173
183
|
roles: { ...base.roles },
|
|
174
184
|
};
|
|
175
|
-
// Seed each user-defined
|
|
185
|
+
// Seed each user-defined role into roles BEFORE the overlay loop so a file's `roles` section
|
|
176
186
|
// overrides per-key for a custom exactly like for built-ins (enabled, instructions,
|
|
177
|
-
//
|
|
187
|
+
// runInterval, provider/model); seeding after would silently ignore those overrides.
|
|
178
188
|
// A custom absent from `roles` stays enabled. Insertion order = built-ins then customs in
|
|
179
189
|
// array order — the display and startup tie-break order.
|
|
180
190
|
for (const c of merged.customRoles) {
|
|
181
191
|
// Own-property test, not a bare truthiness read: the custom name `constructor` passes the
|
|
182
192
|
// lowercase charset check and collides with an Object.prototype key, whose inherited value
|
|
183
|
-
// is truthy — a bare `!merged.roles[c.name]` would skip the seed and the
|
|
184
|
-
// become an own key, so
|
|
193
|
+
// is truthy — a bare `!merged.roles[c.name]` would skip the seed and the role would never
|
|
194
|
+
// become an own key, so runnerIds silently dropped it.
|
|
185
195
|
if (!Object.hasOwn(merged.roles, c.name))
|
|
186
196
|
merged.roles[c.name] = { enabled: true };
|
|
187
197
|
}
|
|
@@ -295,7 +305,7 @@ export function liveConfig(root) {
|
|
|
295
305
|
* dropped from the serialized candidate. A merged config always carries the default `piArgs`,
|
|
296
306
|
* so without this a whole-config write would put both spellings on disk and the next load's
|
|
297
307
|
* raw pass would reject the file; a config that uses only the legacy spellings is unchanged. */
|
|
298
|
-
export function
|
|
308
|
+
export function collapseBackendAliases(config) {
|
|
299
309
|
const pi = config.backends?.pi;
|
|
300
310
|
if (pi === undefined)
|
|
301
311
|
return config;
|
|
@@ -312,11 +322,11 @@ export function collapseAgentAliases(config) {
|
|
|
312
322
|
return next;
|
|
313
323
|
}
|
|
314
324
|
/** Persist a config after validating it, so an invalid tumwater.json can never be written.
|
|
315
|
-
* Written atomically (writeJsonAtomic, trailing newline — the POSIX-newline convention
|
|
316
|
-
*
|
|
325
|
+
* Written atomically (writeJsonAtomic, trailing newline — the POSIX-newline convention shared
|
|
326
|
+
* with config-write.ts and config-request.ts): the config-live poller re-reads the file
|
|
317
327
|
* every ~2 s, so a plain overwrite could hand it a torn read mid-write. */
|
|
318
328
|
export function saveConfig(root, config) {
|
|
319
|
-
const collapsed =
|
|
329
|
+
const collapsed = collapseBackendAliases(config);
|
|
320
330
|
validateConfig(collapsed);
|
|
321
331
|
writeJsonAtomic(configPath(root), collapsed, true);
|
|
322
332
|
}
|
|
@@ -359,36 +369,36 @@ export function changedConfigKeys(prev, next) {
|
|
|
359
369
|
}
|
|
360
370
|
return keys.sort();
|
|
361
371
|
}
|
|
362
|
-
/** The names of the user-defined
|
|
372
|
+
/** The names of the user-defined roles, in array order (plans/user-defined-loops.md).
|
|
363
373
|
* One source of truth for "which ids are user-defined" so consumers cannot drift. */
|
|
364
|
-
export function
|
|
374
|
+
export function customRoleNames(config) {
|
|
365
375
|
return config.customRoles.map((c) => c.name);
|
|
366
376
|
}
|
|
367
|
-
/** True when `id` is a user-defined
|
|
377
|
+
/** True when `id` is a user-defined role rather than a catalog role. */
|
|
368
378
|
export function isCustomRole(config, id) {
|
|
369
379
|
return config.customRoles.some((c) => c.name === id);
|
|
370
380
|
}
|
|
371
|
-
/** Every valid role id — the catalog plus the user-defined
|
|
381
|
+
/** Every valid role id — the catalog plus the user-defined roles (the one answer for "which
|
|
372
382
|
* ids exist", so consumers cannot drift from each other). */
|
|
373
383
|
export function knownRoleIds(config) {
|
|
374
|
-
const ids = [...allRoleIds(), ...
|
|
384
|
+
const ids = [...allRoleIds(), ...customRoleNames(config)];
|
|
375
385
|
const seen = new Set(ids);
|
|
376
|
-
// Parallel work instances (part 5b/7): an enabled role's instance
|
|
386
|
+
// Parallel work instances (part 5b/7): an enabled role's instance role ids (`feature-2`) are
|
|
377
387
|
// valid CLI and GUI targets even though they are not catalog roles. The bare ids are already
|
|
378
388
|
// present, so this only appends the extras; deduping keeps the catalog+customs order intact.
|
|
379
|
-
for (const id of
|
|
389
|
+
for (const id of runnerIds(config))
|
|
380
390
|
if (!seen.has(id)) {
|
|
381
391
|
ids.push(id);
|
|
382
392
|
seen.add(id);
|
|
383
393
|
}
|
|
384
394
|
return ids;
|
|
385
395
|
}
|
|
386
|
-
/** Every valid role id a READ-ONLY surface accepts: the catalog plus the user-defined
|
|
396
|
+
/** Every valid role id a READ-ONLY surface accepts: the catalog plus the user-defined roles,
|
|
387
397
|
* read through loadConfigCached (which never throws), so a transiently broken tumwater.json
|
|
388
398
|
* falls back to the built-in catalog instead of taking the view down or refusing every id.
|
|
389
399
|
* The one home of that fallback rule — every read-only role-id consumer resolves through it:
|
|
390
400
|
* parseRoleScope (cmdLogs/cmdHistory), validRoleIds (the GUI endpoints), cmdDiff's role flag,
|
|
391
|
-
*
|
|
401
|
+
* cmdRunDetail's positional, cmdPrompt's --list and list-wide --cancel/--edit scope, and
|
|
392
402
|
* collectTeamChanges' roster — so they cannot drift. (Read-only deliberately: state-changing
|
|
393
403
|
* commands resolve ids through loadConfig and fail loudly, because the operator is owed the
|
|
394
404
|
* config error before a write.) */
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** Parsing model selector strings — `provider/id[:thinking]`, the form pi's own `--model`
|
|
3
2
|
* accepts (plans/model-tiers.md "Selectors"). Tumwater parses them itself because pricing
|
|
4
3
|
* (src/pi/pi-models.ts) needs the provider and id apart, while the argv builder
|
|
5
4
|
* (src/pi/pi-args.ts) still passes `--provider` / `--model` / `--thinking` separately. Pure
|
|
6
5
|
* string algebra — no I/O, no config knowledge beyond THINKING_LEVELS. */
|
|
7
|
-
import {
|
|
6
|
+
import { BACKEND_IDS, THINKING_LEVELS } from "./config-schema.js";
|
|
8
7
|
/** Parse one selector string. Rules:
|
|
9
8
|
* 1. A trailing `:x` is a thinking level only when `x` is in THINKING_LEVELS, so
|
|
10
9
|
* `…:together` (a provider pin) survives on the id while `…:together:low` strips to
|
|
@@ -36,27 +35,25 @@ export function parseModelSelector(s, legacyProvider) {
|
|
|
36
35
|
})();
|
|
37
36
|
return thinking ? { ...base, thinking } : base;
|
|
38
37
|
}
|
|
39
|
-
/** True when `s` names a registered agent backend
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return s !== undefined && AGENT_IDS.includes(s);
|
|
38
|
+
/** True when `s` names a registered agent backend. Validation restricts the config's `backend`
|
|
39
|
+
* key to these ids; a model id never names one. */
|
|
40
|
+
export function isBackendId(s) {
|
|
41
|
+
return s !== undefined && BACKEND_IDS.includes(s);
|
|
44
42
|
}
|
|
45
43
|
/** Resolve a seam's backend identity: its own `agent` when it carries one (configForRole,
|
|
46
|
-
* reviewConfig and
|
|
47
|
-
*
|
|
48
|
-
* default a selector routes to when neither an agent nor a reserved provider prefix names one.
|
|
44
|
+
* reviewConfig, resolverConfig and a fallback pair's `backend` all do), else pi — the one
|
|
45
|
+
* backend with providers.
|
|
49
46
|
* The single home of the rule: the budget gate's claude check, pi's free-ness and
|
|
50
47
|
* context-window lookups, and the image-input seam all read a seam's backend through it, so
|
|
51
48
|
* no two sites can disagree on which backend a selector runs. */
|
|
52
49
|
export function seamAgent(seam) {
|
|
53
|
-
return seam.agent ??
|
|
50
|
+
return seam.agent ?? "pi";
|
|
54
51
|
}
|
|
55
52
|
/** Each agent backend's per-tier default model, applied only when the config resolves no model
|
|
56
53
|
* id for a seam (plans/agent-backends.md "Config" decision 5). pi has none — its own default
|
|
57
54
|
* applies. It lives here, in the pure config layer, because `routeModel` runs at config-
|
|
58
55
|
* resolution time and must not import the runtime backend registry. */
|
|
59
|
-
const
|
|
56
|
+
const BACKEND_DEFAULT_MODELS = {
|
|
60
57
|
pi: {},
|
|
61
58
|
claude: {
|
|
62
59
|
small: "claude-haiku-5-5",
|
|
@@ -67,30 +64,33 @@ const AGENT_DEFAULT_MODELS = {
|
|
|
67
64
|
// applies, exactly as pi's does. The free model is a time-limited promotion, so its id
|
|
68
65
|
// belongs in tumwater.json, not in code.
|
|
69
66
|
opencode: {},
|
|
67
|
+
// codex's tier defaults (plans/codex-backend.md decision 4), from the catalog's own
|
|
68
|
+
// descriptions (design finding 14): strong is `gpt-6-astra`, default is `gpt-6.1-sol` (Codex's
|
|
69
|
+
// own default), small is `gpt-6-luna`. The director runs the strong tier, as on claude.
|
|
70
|
+
codex: {
|
|
71
|
+
small: "gpt-6-luna",
|
|
72
|
+
default: "gpt-6.1-sol",
|
|
73
|
+
strong: "gpt-6-astra",
|
|
74
|
+
},
|
|
70
75
|
};
|
|
71
76
|
/** The backend and concrete model a resolved seam's provider/model pair routes to
|
|
72
|
-
* (plans/agent-backends.md "Config"). The
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
: "pi";
|
|
90
|
-
if (model !== undefined)
|
|
91
|
-
return { agent, model };
|
|
92
|
-
const def = AGENT_DEFAULT_MODELS[agent]?.[tier];
|
|
93
|
-
return def !== undefined ? { agent, model: def } : { agent };
|
|
77
|
+
* (plans/agent-backends.md "Config"). The backend is the `explicitBackend` (a tier entry's,
|
|
78
|
+
* role's or review's `backend` key), else the config's `backend`, else pi — a model id never
|
|
79
|
+
* names one. pi keeps the parsed provider (the caller's); claude and opencode take the whole
|
|
80
|
+
* `provider/model` id with no provider, verbatim; codex takes the bare model as written (there
|
|
81
|
+
* is no `codex/<id>` spelling — a model id never names a backend, and a whole `codex/<id>`
|
|
82
|
+
* parses to provider `codex` which codex, like pi, drops, leaving the model half). When no
|
|
83
|
+
* model id survives the resolution, the
|
|
84
|
+
* backend's tier default fills in (pi and opencode supply none). */
|
|
85
|
+
export function routeModel(provider, model, configBackend, tier, explicitBackend) {
|
|
86
|
+
const backend = explicitBackend ?? configBackend;
|
|
87
|
+
const whole = model !== undefined && provider !== undefined && backend !== "pi" && backend !== "codex"
|
|
88
|
+
? `${provider}/${model}`
|
|
89
|
+
: model;
|
|
90
|
+
if (whole !== undefined)
|
|
91
|
+
return { agent: backend, model: whole };
|
|
92
|
+
const def = BACKEND_DEFAULT_MODELS[backend]?.[tier];
|
|
93
|
+
return def !== undefined ? { agent: backend, model: def } : { agent: backend };
|
|
94
94
|
}
|
|
95
95
|
/** The inverse of parseModelSelector (in its no-legacyProvider form): the selector string a
|
|
96
96
|
* triple round-trips through, for messages and future writers. */
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { andMore, readDocChecked, FIX_CLAIMS_CHECKED, FIX_CLAIM_HEADING_MAX, REFUSAL_NOTE_MAX, STALE_QUESTION_DAYS, STALE_BUG_DAYS, STALE_WAIT_DAYS } from "./doctor-backlog.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { truncate } from "../text/text.js";
|
|
4
|
+
import { ageInDays, filingDate, openBugEntries, openQuestionEntries, plannedPlanEntries } from "../backlog/backlog.js";
|
|
5
|
+
import { stripEntryStamp } from "../backlog/backlog-structure.js";
|
|
6
|
+
import { questionFilingDate } from "../backlog/question-answer.js";
|
|
7
|
+
import { refusalNote, waitingNote } from "../backlog/backlog-eligibility.js";
|
|
8
|
+
import { plannedPartKeys, plansWaitingOn, prerequisitesMerged } from "../backlog/backlog-parts.js";
|
|
9
|
+
import { fixedEntriesAt } from "../verdict/fix-claim.js";
|
|
10
|
+
import { lastDate } from "../backlog/backlog-md.js";
|
|
11
|
+
import { formatTimestamp } from "../text/datetime.js";
|
|
12
|
+
import { stringList } from "../files/json-object.js";
|
|
13
|
+
import { readEventsSince } from "../events/event-window.js";
|
|
14
|
+
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
15
|
+
/** The doctor's open-bug and question checks, split out of doctor-backlog.ts: questions that
|
|
16
|
+
* have waited too long for an answer (checkStaleQuestions), bugs left unfixed (checkStaleBugs),
|
|
17
|
+
* bugs held for their next failure (checkWaitingBugs, with its quotedTestName / waitingNoteDate /
|
|
18
|
+
* failureMentions helpers), fixed records whose quoted test regressed (checkRegressedFixes), and
|
|
19
|
+
* entries parked by a Refused note (checkRefusedEntries). They import the shared helpers
|
|
20
|
+
* (readDocChecked, andMore) from doctor-backlog.ts, plus the age/trim bounds, and
|
|
21
|
+
* all warn instead of failing: a stale or
|
|
22
|
+
* parked entry is operator signal, not a broken environment. doctor.ts composes them beside the
|
|
23
|
+
* plan-structure and queue checks; the shared CheckOutcome shape is imported from outcome.ts. */
|
|
24
|
+
/** Stale questions — open QUESTIONS.md questions that have waited more than 7 days for an
|
|
25
|
+
* answer. A team can stay parked on a question for days while `tumwater doctor` reports
|
|
26
|
+
* nothing; the `--list` age (`questionAgeSuffix` on `tumwater questions`) is the only other
|
|
27
|
+
* place the wait shows, and only when the operator asks. Reads the tree at `root` — the
|
|
28
|
+
* primary checkout IS main's tree — like checkRefusedEntries, reusing
|
|
29
|
+
* `questionFilingDate`/`openQuestionEntries` (src/backlog) unchanged. The age is the whole
|
|
30
|
+
* days between the heading's `(asked by …)` stamp and now through backlog.js's ageInDays,
|
|
31
|
+
* the one age formula, floored at 0; an undated heading is skipped. A warn, never a fail —
|
|
32
|
+
* the checkStalledPlans/checkRefusedEntries precedent: a question awaiting a human is
|
|
33
|
+
* operator signal, not a broken environment. */
|
|
34
|
+
export function checkStaleQuestions(root) {
|
|
35
|
+
const doc = readDocChecked(path.join(root, "QUESTIONS.md"), "QUESTIONS.md", "no QUESTIONS.md — nothing to verify");
|
|
36
|
+
if (typeof doc !== "string")
|
|
37
|
+
return doc;
|
|
38
|
+
const stale = openQuestionEntries(root).flatMap((q) => {
|
|
39
|
+
const stamp = questionFilingDate(q.title);
|
|
40
|
+
if (stamp === null)
|
|
41
|
+
return [];
|
|
42
|
+
const days = ageInDays(stamp);
|
|
43
|
+
return days >= STALE_QUESTION_DAYS ? [{ title: q.title, days }] : [];
|
|
44
|
+
});
|
|
45
|
+
const [first, ...rest] = stale;
|
|
46
|
+
if (!first)
|
|
47
|
+
return okOutcome("no open question has waited more than 7 days for an answer");
|
|
48
|
+
const more = andMore(rest, (q) => `"${truncate(q.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
49
|
+
return warnOutcome(`QUESTIONS.md question "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" has waited ${first.days} d for an answer${more} — ` +
|
|
50
|
+
`a question that waits too long for a human answer is worth your attention`);
|
|
51
|
+
}
|
|
52
|
+
/** Stale bugs — unrefused open BUGS.md bugs whose `(found by … YYYY-MM-DD)` or `(reported …)`
|
|
53
|
+
* stamp is at least 3 days old. The bugfix role fixes a bug within hours, so one that sits means
|
|
54
|
+
* no role is fixing it. Positions are the 1-based `bug --list` numbers; a refused bug is skipped
|
|
55
|
+
* (checkRefusedEntries names it), a waiting bug is skipped (checkWaitingBugs names it) and so is
|
|
56
|
+
* an undated heading. A warn, never a fail. */
|
|
57
|
+
export function checkStaleBugs(root) {
|
|
58
|
+
const doc = readDocChecked(path.join(root, "BUGS.md"), "BUGS.md", "no BUGS.md — nothing to verify");
|
|
59
|
+
if (typeof doc !== "string")
|
|
60
|
+
return doc;
|
|
61
|
+
const stale = openBugEntries(root).flatMap((entry, index) => {
|
|
62
|
+
if (refusalNote(entry.body) !== null || waitingNote(entry.body) !== null)
|
|
63
|
+
return [];
|
|
64
|
+
const stamp = filingDate(entry.title);
|
|
65
|
+
if (stamp === null)
|
|
66
|
+
return [];
|
|
67
|
+
const days = ageInDays(stamp);
|
|
68
|
+
return days >= STALE_BUG_DAYS ? [{ position: index + 1, title: entry.title, days }] : [];
|
|
69
|
+
});
|
|
70
|
+
const [first, ...rest] = stale;
|
|
71
|
+
if (!first)
|
|
72
|
+
return okOutcome("no open bug has waited more than 3 days for a fix");
|
|
73
|
+
const more = andMore(rest, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
74
|
+
return warnOutcome(`BUGS.md bug #${first.position} "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" has waited ${first.days} d for a fix${more} — ` +
|
|
75
|
+
`no role is fixing it: check that the bugfix role is enabled and not failing, or retract it with \`tumwater bug --cancel ${first.position}\``);
|
|
76
|
+
}
|
|
77
|
+
/** The first span between straight double quotes in a bug heading that is at least 12
|
|
78
|
+
* characters long — the test name a flake bug's heading quotes — or null. The floor stops a
|
|
79
|
+
* short quoted word from matching unrelated failures. */
|
|
80
|
+
export function quotedTestName(title) {
|
|
81
|
+
return /"([^"]{12,})"/.exec(title)?.[1] ?? null;
|
|
82
|
+
}
|
|
83
|
+
/** Local midnight at the end of a `YYYY-MM-DD` day, in ms: the earliest instant a failure can
|
|
84
|
+
* be told apart from the one a same-day note or fix record was written about. NaN for a
|
|
85
|
+
* malformed date. */
|
|
86
|
+
function dayAfterMs(date) {
|
|
87
|
+
return new Date(Number(date.slice(0, 4)), Number(date.slice(5, 7)) - 1, Number(date.slice(8, 10)) + 1).getTime();
|
|
88
|
+
}
|
|
89
|
+
/** The first `YYYY-MM-DD` in a Waiting for the next failure note's text, or null. */
|
|
90
|
+
export function waitingNoteDate(note) {
|
|
91
|
+
return /\d{4}-\d{2}-\d{2}/.exec(note)?.[0] ?? null;
|
|
92
|
+
}
|
|
93
|
+
/** Every event at or after `sinceMs` that records a failure naming `name`: a `warning` whose
|
|
94
|
+
* message includes it (gate and merge flake warnings, a merge check kept for one retry), or a
|
|
95
|
+
* `review_rejected` with a reason that includes it (its text is the first such reason). In
|
|
96
|
+
* event order. */
|
|
97
|
+
export function failureMentions(events, name, sinceMs) {
|
|
98
|
+
return events.flatMap((e) => {
|
|
99
|
+
if (typeof e.ts !== "number" || e.ts < sinceMs)
|
|
100
|
+
return [];
|
|
101
|
+
if (e.type === "warning" && typeof e.message === "string" && e.message.includes(name))
|
|
102
|
+
return [{ ts: e.ts, text: e.message }];
|
|
103
|
+
if (e.type === "review_rejected") {
|
|
104
|
+
const reason = stringList(e.reasons).find((r) => r.includes(name));
|
|
105
|
+
if (reason !== undefined)
|
|
106
|
+
return [{ ts: e.ts, text: reason }];
|
|
107
|
+
}
|
|
108
|
+
return [];
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** The read-events window (ms) that reaches the oldest of `sinces` — `Date.now()` minus the
|
|
112
|
+
* oldest timestamp, floored at 0 — so a recurrence check reads events past every candidate's
|
|
113
|
+
* `since`. The single home of the "oldest of a set of since-times" step, shared by
|
|
114
|
+
* checkWaitingBugs and checkRegressedFixes, so those sites cannot drift on the clamp or the
|
|
115
|
+
* time base. */
|
|
116
|
+
function sinceWindowMs(sinces) {
|
|
117
|
+
return Math.max(0, Date.now() - Math.min(...sinces));
|
|
118
|
+
}
|
|
119
|
+
/** Open BUGS.md bugs held by a **Waiting for the next failure …** note, in file order: the
|
|
120
|
+
* doctor's `checkWaitingBugs` selection and anchor, factored out so the always-on stale-wait
|
|
121
|
+
* feed (staleWaitingBugRows) reads the same held set and cannot drift from the doctor's rule.
|
|
122
|
+
* A bug that is also Refused is left to checkRefusedEntries. `since` is the day-after the
|
|
123
|
+
* note's date — the recurrence scan's boundary; `date` the note's own `YYYY-MM-DD` or null. */
|
|
124
|
+
function heldWaitingBugs(root) {
|
|
125
|
+
return openBugEntries(root).flatMap((entry, index) => {
|
|
126
|
+
const note = waitingNote(entry.body);
|
|
127
|
+
if (note === null || refusalNote(entry.body) !== null)
|
|
128
|
+
return [];
|
|
129
|
+
const name = quotedTestName(entry.title);
|
|
130
|
+
const date = waitingNoteDate(note);
|
|
131
|
+
const since = name !== null && date !== null ? dayAfterMs(date) : null;
|
|
132
|
+
return [{ index, position: index + 1, title: entry.title, note, name, since, date }];
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Waiting bugs — open BUGS.md bugs held by a **Waiting for the next failure …** note, which
|
|
136
|
+
* parks a bug until it fails again. Positions are the 1-based `bug --list` numbers; a bug that
|
|
137
|
+
* is also Refused is left to checkRefusedEntries. Ok when the hold is undisturbed — doctor
|
|
138
|
+
* then only names the bugs and how to release one — and a warn when a held bug's quoted test
|
|
139
|
+
* has failed in a gate or merge check since the day after its note (the note carries only a
|
|
140
|
+
* date, so a same-day failure cannot be told from the one it was written about). A warn is
|
|
141
|
+
* never a fail, and doctor writes nothing to BUGS.md. */
|
|
142
|
+
export function checkWaitingBugs(root) {
|
|
143
|
+
const doc = readDocChecked(path.join(root, "BUGS.md"), "BUGS.md", "no BUGS.md — nothing to verify");
|
|
144
|
+
if (typeof doc !== "string")
|
|
145
|
+
return doc;
|
|
146
|
+
const held = heldWaitingBugs(root);
|
|
147
|
+
const [first, ...rest] = held;
|
|
148
|
+
if (!first)
|
|
149
|
+
return okOutcome("no open bug is waiting for its next failure");
|
|
150
|
+
const sinces = held.flatMap((b) => (b.since !== null && !Number.isNaN(b.since) ? [b.since] : []));
|
|
151
|
+
if (sinces.length > 0) {
|
|
152
|
+
const { events } = readEventsSince(root, sinceWindowMs(sinces));
|
|
153
|
+
const recurred = held.flatMap((b) => {
|
|
154
|
+
if (b.name === null || b.since === null)
|
|
155
|
+
return [];
|
|
156
|
+
const mentions = failureMentions(events, b.name, b.since);
|
|
157
|
+
const newest = mentions[mentions.length - 1];
|
|
158
|
+
return newest ? [{ ...b, newest }] : [];
|
|
159
|
+
});
|
|
160
|
+
const [again, ...others] = recurred;
|
|
161
|
+
if (again) {
|
|
162
|
+
const more = andMore(others, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
163
|
+
return warnOutcome(`BUGS.md bug #${again.position} "${truncate(again.title, FIX_CLAIM_HEADING_MAX)}" failed again since its Waiting for the next failure note ` +
|
|
164
|
+
`(${formatTimestamp(again.newest.ts)}: ${truncate(again.newest.text, REFUSAL_NOTE_MAX)})${more} — ` +
|
|
165
|
+
`record the occurrence under the entry and release it with \`tumwater bug --release ${again.position}\``);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/* A Waiting hold has no expiry of its own, so a bug whose flake never comes back parks
|
|
169
|
+
* indefinitely; a hold that has outlived STALE_WAIT_DAYS without a recurrence is escalated
|
|
170
|
+
* from the ok below to a warn so the operator is nudged to release or re-hold it. */
|
|
171
|
+
const lapsed = held.flatMap((b) => {
|
|
172
|
+
/* An undated note (no YYYY-MM-DD in its text) has no hold-start marker, and checkStaleBugs
|
|
173
|
+
* skips any bug carrying a waiting note, so without a fallback it would park indefinitely
|
|
174
|
+
* too; the filing stamp anchors it — the hold cannot be younger than the bug it dates from. */
|
|
175
|
+
const anchor = b.date ?? filingDate(b.title);
|
|
176
|
+
if (anchor === null)
|
|
177
|
+
return [];
|
|
178
|
+
const days = ageInDays(anchor);
|
|
179
|
+
return days >= STALE_WAIT_DAYS ? [{ ...b, days }] : [];
|
|
180
|
+
});
|
|
181
|
+
const [firstLapsed, ...restLapsed] = lapsed;
|
|
182
|
+
if (firstLapsed) {
|
|
183
|
+
const more = andMore(restLapsed, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
184
|
+
return warnOutcome(`BUGS.md bug #${firstLapsed.position} "${truncate(firstLapsed.title, FIX_CLAIM_HEADING_MAX)}" has waited ${firstLapsed.days} d for its next failure without one ` +
|
|
185
|
+
`— its flake likely never comes back, so it parks indefinitely; release it with \`tumwater bug --release ${firstLapsed.position}\`${more}`);
|
|
186
|
+
}
|
|
187
|
+
const more = andMore(rest, (b) => `#${b.position} "${truncate(b.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
188
|
+
return okOutcome(`BUGS.md bug #${first.position} "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" is held until it fails again ` +
|
|
189
|
+
`(${truncate(first.note, REFUSAL_NOTE_MAX)})${more} — selfcheck lifts the note when it records a new occurrence; ` +
|
|
190
|
+
`release it now with \`tumwater bug --release ${first.position}\``);
|
|
191
|
+
}
|
|
192
|
+
/** The always-on rows behind the dashboard/TUI/`tumwater status` waiting-bug alert: open BUGS.md
|
|
193
|
+
* bugs held by a **Waiting for the next failure …** note whose anchor (the note's `YYYY-MM-DD`,
|
|
194
|
+
* else the heading's filing stamp) is at least STALE_WAIT_DAYS old. Shares the doctor's
|
|
195
|
+
* STALE_WAIT_DAYS threshold and the heldWaitingBugs selection/anchor, so the alert and the
|
|
196
|
+
* doctor's `waiting bugs` row cannot drift on the rule; an undated note-and-heading is skipped.
|
|
197
|
+
* The returned objects carry the 0-based index (drawer address `bugs:<index>`), the 1-based
|
|
198
|
+
* position (`bug --release <position>`), the stamp-stripped title, and the whole-day age.
|
|
199
|
+
* Returned most-days-waited-first, ties in file order, like staleOpenBugs. This deliberately
|
|
200
|
+
* omits `checkWaitingBugs`'s recurrence scan, because that scan reads events back to the oldest
|
|
201
|
+
* hold and is too costly on the status poll path; the doctor still reports a recurrence first. */
|
|
202
|
+
export function staleWaitingBugRows(root, now = Date.now()) {
|
|
203
|
+
return heldWaitingBugs(root)
|
|
204
|
+
.flatMap((b) => {
|
|
205
|
+
const anchor = b.date ?? filingDate(b.title);
|
|
206
|
+
if (anchor === null)
|
|
207
|
+
return [];
|
|
208
|
+
const days = ageInDays(anchor, now);
|
|
209
|
+
return days >= STALE_WAIT_DAYS ? [{ index: b.index, position: b.position, title: stripEntryStamp(b.title), days }] : [];
|
|
210
|
+
})
|
|
211
|
+
.sort((a, b) => b.days - a.days || a.index - b.index);
|
|
212
|
+
}
|
|
213
|
+
/** Regressed fixes — a newest-10 BUGS.md Fixed record whose heading quotes a test name that a
|
|
214
|
+
* gate or merge check has failed on since the day after its fix (the heading carries only a
|
|
215
|
+
* date, so a same-day failure cannot be told from the one the fix answered), unless an Open bug
|
|
216
|
+
* already quotes the same test, which means the regression is filed. A warn, never a fail, and
|
|
217
|
+
* doctor writes nothing to BUGS.md. */
|
|
218
|
+
export function checkRegressedFixes(root) {
|
|
219
|
+
const doc = readDocChecked(path.join(root, "BUGS.md"), "BUGS.md", "no BUGS.md — nothing to verify");
|
|
220
|
+
if (typeof doc !== "string")
|
|
221
|
+
return doc;
|
|
222
|
+
const candidates = fixedEntriesAt(root, doc).slice(0, FIX_CLAIMS_CHECKED).flatMap((entry) => {
|
|
223
|
+
const name = quotedTestName(entry.title);
|
|
224
|
+
const date = lastDate(entry.title, /(\d{4}-\d{2}-\d{2})/);
|
|
225
|
+
if (name === null || date === null)
|
|
226
|
+
return [];
|
|
227
|
+
const since = dayAfterMs(date);
|
|
228
|
+
return Number.isNaN(since) ? [] : [{ title: entry.title, name, date, since }];
|
|
229
|
+
});
|
|
230
|
+
const open = candidates.length > 0 ? openBugEntries(root).map((e) => quotedTestName(e.title)) : [];
|
|
231
|
+
const unfiled = candidates.filter((c) => !open.includes(c.name));
|
|
232
|
+
if (unfiled.length > 0) {
|
|
233
|
+
const { events } = readEventsSince(root, sinceWindowMs(unfiled.map((c) => c.since)));
|
|
234
|
+
const regressed = unfiled.flatMap((c) => {
|
|
235
|
+
const mentions = failureMentions(events, c.name, c.since);
|
|
236
|
+
const newest = mentions[mentions.length - 1];
|
|
237
|
+
return newest ? [{ ...c, newest }] : [];
|
|
238
|
+
});
|
|
239
|
+
const [first, ...rest] = regressed;
|
|
240
|
+
if (first) {
|
|
241
|
+
const more = andMore(rest, (c) => `"${truncate(c.title, FIX_CLAIM_HEADING_MAX)}"`);
|
|
242
|
+
return warnOutcome(`BUGS.md Fixed record "${truncate(first.title, FIX_CLAIM_HEADING_MAX)}" quotes a test that failed again after its ${first.date} fix ` +
|
|
243
|
+
`(${formatTimestamp(first.newest.ts)}: ${truncate(first.newest.text, REFUSAL_NOTE_MAX)})${more} — ` +
|
|
244
|
+
"file it as a new bug with `tumwater bug`, quoting the test name and citing the Fixed record");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return okOutcome("no recently Fixed bug's quoted test has failed since its fix");
|
|
248
|
+
}
|
|
249
|
+
/** Refused entries — planned plans and open bugs held by a **Refused …** note. No role clears
|
|
250
|
+
* a Refused note, so the entry stays parked until a human or the director edits it, or an
|
|
251
|
+
* operator releases it with `--release <n>`, and the
|
|
252
|
+
* only other signs are the `[refused]` marks in the backlog listings. Reads the tree at `root`
|
|
253
|
+
* — the primary checkout IS main's tree — like checkFixClaims, in `--list` order (PLANS.md
|
|
254
|
+
* Planned, then BUGS.md Open). A warn, never a fail: a refusal is operator signal, not a broken
|
|
255
|
+
* environment. The warning also counts the planned parts a refused series part holds. */
|
|
256
|
+
export function checkRefusedEntries(root) {
|
|
257
|
+
const planned = plannedPlanEntries(root);
|
|
258
|
+
const parts = plannedPartKeys(planned);
|
|
259
|
+
const sources = [
|
|
260
|
+
{ file: "PLANS.md", command: "plan", entries: planned },
|
|
261
|
+
{ file: "BUGS.md", command: "bug", entries: openBugEntries(root) },
|
|
262
|
+
];
|
|
263
|
+
const held = sources.flatMap(({ file, command, entries }) => entries.flatMap((entry, index) => {
|
|
264
|
+
const note = refusalNote(entry.body);
|
|
265
|
+
if (note === null)
|
|
266
|
+
return [];
|
|
267
|
+
const mergeDone = prerequisitesMerged(entry.title, parts);
|
|
268
|
+
const waiting = file === "PLANS.md" ? plansWaitingOn(planned, index).length : 0;
|
|
269
|
+
return [{ file, command, position: index + 1, title: entry.title, note, mergeDone, waiting }];
|
|
270
|
+
}));
|
|
271
|
+
const [first, ...rest] = held;
|
|
272
|
+
if (!first)
|
|
273
|
+
return okOutcome("no planned plan or open bug is held by a Refused note");
|
|
274
|
+
const more = andMore(rest, (e) => `${e.file} ${e.command} #${e.position} "${truncate(e.title, FIX_CLAIM_HEADING_MAX)}"` +
|
|
275
|
+
(e.mergeDone.length > 0 ? " [prerequisites no longer planned]" : "") +
|
|
276
|
+
(e.waiting > 0 ? ` [${e.waiting} part(s) wait on it]` : ""));
|
|
277
|
+
const stale = first.mergeDone.length > 0
|
|
278
|
+
? `; its prerequisite(s) ${first.mergeDone.map((r) => `${r.series} ${r.part}/${r.of}`).join(", ")} are no longer planned, so the refusal may be stale`
|
|
279
|
+
: "";
|
|
280
|
+
const waiting = first.waiting > 0
|
|
281
|
+
? `; ${first.waiting} planned part(s) wait on it, directly or through an earlier part`
|
|
282
|
+
: "";
|
|
283
|
+
return warnOutcome(`${first.file} ${first.command} #${first.position} "${first.title}" is held by a Refused note ` +
|
|
284
|
+
`(${truncate(first.note, REFUSAL_NOTE_MAX)})${stale}${waiting}${more} — no role takes a refused entry until it is edited: ` +
|
|
285
|
+
`answer the objection and release the entry with \`tumwater ${first.command} --release ${first.position}\`, ` +
|
|
286
|
+
`or retract it with \`tumwater ${first.command} --cancel ${first.position}\``);
|
|
287
|
+
}
|