tumwater 0.4.2 → 0.4.4
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 +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
* "Billing" and §8 "The subscription usage window"). Persisted at
|
|
4
4
|
* `.tumwater/state/<backend>-account.json` (plan design grounds/codex-backend.md "Groundwork:
|
|
5
5
|
* the subscription account is per backend", item 1), written only by the orchestrator process,
|
|
6
|
-
* so the sibling parts
|
|
7
|
-
* of re-deriving it per surface.
|
|
6
|
+
* so the sibling parts read one answer instead of re-deriving it per surface.
|
|
8
7
|
*
|
|
9
8
|
* The file is a cache, never a source of truth: a run that reports billing overwrites it, and a
|
|
10
9
|
* missing or unreadable file reads as "no subscription". `resetsAt` is stored as epoch
|
|
@@ -28,85 +27,6 @@ export function subscriptionWindowRejected(run) {
|
|
|
28
27
|
run.billing === "subscription" &&
|
|
29
28
|
(run.rateWindows ?? []).some((w) => w.status === "rejected" && w.isUsingOverage !== true));
|
|
30
29
|
}
|
|
31
|
-
/** The subscription account's used-up usage window, or null: a window reading `rejected` with
|
|
32
|
-
* no overage, or (with `includeOverage`, the default) a window whose current reading shows
|
|
33
|
-
* `isUsingOverage`, and a `resetsAt` still ahead of `now`; among several, the one that resets
|
|
34
|
-
* last. With a `reserve` (config `backends.<id>.windowReserve`), a window whose utilization
|
|
35
|
-
* has reached its type's fraction counts too, and comes back with `reserve: true`; a rejected
|
|
36
|
-
* or overage window outranks a reserve one, so the flag is set only when no window is used up
|
|
37
|
-
* for real. The budget gate's window move (pollBudgetGate) reads the default; the team hold
|
|
38
|
-
* (windowObservation) passes `includeOverage: false`, because overage never holds a
|
|
39
|
-
* seam. Only subscription billing produces one; a missing account reads null. */
|
|
40
|
-
export function windowUsedUp(account, now, includeOverage = true, reserve) {
|
|
41
|
-
if (account?.billing !== "subscription")
|
|
42
|
-
return null;
|
|
43
|
-
let furthest = null;
|
|
44
|
-
let reserved = null;
|
|
45
|
-
for (const [type, w] of Object.entries(account.windows)) {
|
|
46
|
-
if (typeof w.resetsAt !== "number" || w.resetsAt <= now)
|
|
47
|
-
continue;
|
|
48
|
-
const used = w.isUsingOverage === true ? includeOverage : w.status === "rejected";
|
|
49
|
-
if (used) {
|
|
50
|
-
if (furthest === null || w.resetsAt > furthest.resetsAt)
|
|
51
|
-
furthest = { type, resetsAt: w.resetsAt };
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (windowReserved(w, reserve?.[type]) && (reserved === null || w.resetsAt > reserved.resetsAt))
|
|
55
|
-
reserved = { type, resetsAt: w.resetsAt, reserve: true };
|
|
56
|
-
}
|
|
57
|
-
return furthest ?? reserved;
|
|
58
|
-
}
|
|
59
|
-
/** Has this window's utilization reached its reserve fraction? False without a fraction or a
|
|
60
|
-
* reading. */
|
|
61
|
-
export function windowReserved(w, fraction) {
|
|
62
|
-
return fraction !== undefined && typeof w.utilization === "number" && w.utilization >= fraction;
|
|
63
|
-
}
|
|
64
|
-
/** Status severity for ordering: a rejected window limits, an allowed_warning nears its limit,
|
|
65
|
-
* every other reading (allowed/ok) rides at the bottom, and a reset one no longer limits. */
|
|
66
|
-
const WINDOW_RANK = { rejected: 3, allowed_warning: 2, reset: 0 };
|
|
67
|
-
/** The window type as a compact label: `five_hour` → `5h`, `seven_day` → `7d`; an unknown
|
|
68
|
-
* type falls back to its words with underscores as spaces. The one home of the window spelling
|
|
69
|
-
* the status badge and the role view share. */
|
|
70
|
-
export function windowLabel(type) {
|
|
71
|
-
const m = /^(\w+)_(hour|day|week)$/.exec(type);
|
|
72
|
-
if (!m)
|
|
73
|
-
return type.replace(/_/g, " ");
|
|
74
|
-
const words = { one: "1", two: "2", three: "3", four: "4", five: "5", six: "6", seven: "7", eight: "8", nine: "9", ten: "10" };
|
|
75
|
-
const unit = m[2] === "hour" ? "h" : m[2] === "day" ? "d" : "w";
|
|
76
|
-
const word = m[1] ?? "";
|
|
77
|
-
return `${words[word] ?? word}${unit}`;
|
|
78
|
-
}
|
|
79
|
-
/** The fraction of a subscription usage window consumed (0–1) as a whole-number percent: a
|
|
80
|
-
* `0.64` utilization reads `64`. The one home of the fraction→percent conversion, shared by
|
|
81
|
-
* the status badge, the doctor's hold reason and the event formatter so they render a window's
|
|
82
|
-
* fill identically (the browser dashboard's own window line is embedded client-side and keeps
|
|
83
|
-
* its inline copy). */
|
|
84
|
-
export function utilizationPct(utilization) {
|
|
85
|
-
return Math.round(utilization * 100);
|
|
86
|
-
}
|
|
87
|
-
/** Order two window entries by the windowEntries rule: status severity, then the reported
|
|
88
|
-
* utilization (a missing reading sorts below any number), then the earlier reset, then the
|
|
89
|
-
* type name. Exported so a list of per-backend blocks can be ordered by their primary entry
|
|
90
|
-
* the same way windowEntries orders one account's windows (part 3a/9). */
|
|
91
|
-
export function compareWindowEntries(a, b) {
|
|
92
|
-
const rank = (WINDOW_RANK[b.status] ?? 1) - (WINDOW_RANK[a.status] ?? 1);
|
|
93
|
-
if (rank !== 0)
|
|
94
|
-
return rank;
|
|
95
|
-
const util = (b.utilization ?? -1) - (a.utilization ?? -1);
|
|
96
|
-
if (util !== 0)
|
|
97
|
-
return util;
|
|
98
|
-
const reset = (a.resetsAt ?? Number.POSITIVE_INFINITY) - (b.resetsAt ?? Number.POSITIVE_INFINITY);
|
|
99
|
-
if (reset !== 0)
|
|
100
|
-
return reset;
|
|
101
|
-
return a.type.localeCompare(b.type);
|
|
102
|
-
}
|
|
103
|
-
/** The account's windows as a list, most constrained first (by compareWindowEntries). Empty
|
|
104
|
-
* when the account reported none. */
|
|
105
|
-
export function windowEntries(account) {
|
|
106
|
-
return Object.entries(account.windows)
|
|
107
|
-
.map(([type, w]) => ({ type, ...w }))
|
|
108
|
-
.sort((a, b) => compareWindowEntries(a, b));
|
|
109
|
-
}
|
|
110
30
|
/** Read the persisted account state for a backend, or null when it is absent or unreadable. A
|
|
111
31
|
* file without a usable top-level `observedAt` reads as absent (the plan's torn-file rule),
|
|
112
32
|
* and a window whose `resetsAt` is at or before `now` reads back with `status: "reset"` — the
|
|
@@ -144,8 +64,7 @@ function readWindow(value, fileObservedAt, now) {
|
|
|
144
64
|
const resetsAt = finiteNumber(value.resetsAt, Number.NaN);
|
|
145
65
|
const resets = Number.isFinite(resetsAt);
|
|
146
66
|
// utilization heals like the two stamps above: JSON's `1e999` parses to Infinity, and a
|
|
147
|
-
// non-finite fraction would
|
|
148
|
-
// windowEntries comparator (Infinity - Infinity is NaN).
|
|
67
|
+
// non-finite fraction would poison any consumer's utilization read.
|
|
149
68
|
const utilization = finiteNumber(value.utilization, undefined);
|
|
150
69
|
const reset = resets && resetsAt <= now;
|
|
151
70
|
const window = {
|
|
@@ -319,10 +319,9 @@ function archiveCommitWhat(plan) {
|
|
|
319
319
|
}
|
|
320
320
|
/** One live backlog file's finished-history rows as liveFinishedHistory reports them, phrased
|
|
321
321
|
* and joined: `file ## <section> holds N finished entries (X KB)` rows separated by "; " — the
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
* explanation, so the two texts stay byte-identical up to that suffix. */
|
|
322
|
+
* detail head the doctor's `backlog archive` check (checkLiveFinished) renders, so the row
|
|
323
|
+
* shape, the plural, and the KB rounding stay stable. Each caller appends its own trailing
|
|
324
|
+
* explanation. */
|
|
326
325
|
export function liveFinishedRowList(rows) {
|
|
327
326
|
return rows.map((l) => `${l.file} ## ${l.section} holds ${plural(l.entries, "finished entry", "finished entries")} (${Math.round(l.bytes / 1024)} KB)`).join("; ");
|
|
328
327
|
}
|
|
@@ -10,6 +10,12 @@ import { groupBlockedSeries, partsLabel } from "./backlog-index-data.js";
|
|
|
10
10
|
import { rejectUnknownArgs } from "../cli/cli-flag-specs.js";
|
|
11
11
|
import { refuseTeamWorkspace } from "../cli/cli-repo-gate.js";
|
|
12
12
|
import { sectionNoun, showEntry } from "./backlog-write.js";
|
|
13
|
+
/** `bug --list --done -n` / `plan --list --done -n` ceiling: bounds how many finished
|
|
14
|
+
* entries one list shows, so a typo'd huge count fails fast with the shared `must be between
|
|
15
|
+
* 1 and <max>` wording (the logs/history/report pattern) instead of silently meaning
|
|
16
|
+
* "everything". The finished lists are small (PLANS.md/BUGS.md), so the cap never meaningfully
|
|
17
|
+
* binds — it exists for the uniform count-flag contract and the clear over-range rebuke. */
|
|
18
|
+
const BACKLOG_MAX_LISTED = 100_000;
|
|
13
19
|
/** Ascending positions as runs: consecutive ones join with an en dash (`13–17`), runs separate
|
|
14
20
|
* with commas (`13–15, 18`). */
|
|
15
21
|
function positionRuns(positions) {
|
|
@@ -59,7 +65,7 @@ function parseListArgs(args, command) {
|
|
|
59
65
|
}
|
|
60
66
|
if (arg === "-n") {
|
|
61
67
|
rejectRepeatFlag(count !== null, "-n");
|
|
62
|
-
count = parseCountFlag("-n", args[++i]);
|
|
68
|
+
count = parseCountFlag("-n", args[++i], BACKLOG_MAX_LISTED);
|
|
63
69
|
continue;
|
|
64
70
|
}
|
|
65
71
|
if (arg === "--list") {
|
|
@@ -114,10 +120,10 @@ function announceFinishedList(root, fileName, bugSection, grep, count, json) {
|
|
|
114
120
|
const header = pattern === null ? `${what} in ${fileName}, newest first:` : `${what} in ${fileName} matching "${pattern}", newest first:`;
|
|
115
121
|
say(`${header}\n${lines.join("\n")}`);
|
|
116
122
|
}
|
|
117
|
-
/** The active
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
function
|
|
123
|
+
/** The active list's read-and-filter phase: reads the section's entries and each position's
|
|
124
|
+
* hold and live claimer, numbers them unfiltered, keeps the `--grep` matches under those
|
|
125
|
+
* positions, drops to the `--ready` survivors, counts what `--ready` hid, and names the noun. */
|
|
126
|
+
function activeListView(root, bugSection, sectionTitle, grep, ready) {
|
|
121
127
|
const entries = bugSection ? openBugEntries(root) : plannedPlanEntries(root);
|
|
122
128
|
const holds = backlogHolds(root);
|
|
123
129
|
const entryHolds = bugSection ? holds.bugs : holds.plans;
|
|
@@ -128,47 +134,62 @@ function announceActiveList(root, fileName, command, bugSection, sectionTitle, g
|
|
|
128
134
|
const numbered = entries.map((entry, i) => ({ entry, position: i + 1 }));
|
|
129
135
|
const matches = grep === null ? numbered : numbered.filter((m) => entryMatches(m.entry, grep));
|
|
130
136
|
const kept = ready ? matches.filter((m) => (entryHolds[m.position - 1] ?? null) === null) : matches;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
137
|
+
return {
|
|
138
|
+
entries,
|
|
139
|
+
entryHolds,
|
|
140
|
+
entryRunners,
|
|
141
|
+
matches,
|
|
142
|
+
kept,
|
|
143
|
+
hidden: matches.length - kept.length,
|
|
144
|
+
noun: sectionNoun(sectionTitle),
|
|
145
|
+
grep,
|
|
146
|
+
ready,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** The `--json` body: each kept entry as its position, title, verbatim body, hold,
|
|
150
|
+
* in-progress claim and — only when the heading names a filing stamp — its `since`/`ageDays`. */
|
|
151
|
+
function activeListJson(view) {
|
|
152
|
+
const { kept, entryHolds, entryRunners } = view;
|
|
153
|
+
return {
|
|
154
|
+
entries: kept.map(({ entry, position }) => {
|
|
155
|
+
const since = filingDate(entry.title);
|
|
156
|
+
return {
|
|
157
|
+
position,
|
|
158
|
+
title: entry.title,
|
|
159
|
+
body: entry.body,
|
|
160
|
+
hold: entryHolds[position - 1] ?? null,
|
|
161
|
+
inProgress: entryRunners[position - 1] ?? null,
|
|
162
|
+
// The filing stamp the entry's own heading carries, and how many days it has waited
|
|
163
|
+
// since then (both present only when the heading names a stamp — an undated entry
|
|
164
|
+
// keeps the pre-existing shape with neither key).
|
|
165
|
+
...(since === null ? {} : { since, ageDays: ageInDays(since) }),
|
|
166
|
+
};
|
|
167
|
+
}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** The prose `no …` line for an empty list, a `--grep` with no match, or a `--ready` list
|
|
171
|
+
* whose every match is held; null when `view.kept` has rows to print. */
|
|
172
|
+
function activeListEmptyLine(view, command) {
|
|
173
|
+
const { entries, matches, kept, hidden, noun, grep, ready } = view;
|
|
174
|
+
if (grep === null && entries.length === 0)
|
|
175
|
+
return `no ${noun}s`;
|
|
176
|
+
if (matches.length === 0)
|
|
177
|
+
return `no ${noun}s match "${grep}"`;
|
|
178
|
+
if (ready && kept.length === 0)
|
|
179
|
+
return `no ${noun}s ready (${hidden} held; run \`tumwater ${command} --list\` to see them)`;
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
/** The prose body of a non-empty active list: the bug list's numbered `line` rows, or the
|
|
183
|
+
* plan list's `groupBlockedSeries` rows, each with the hidden-held notice when `hidden > 0`. */
|
|
184
|
+
function activeListProse(view, fileName, command, bugSection) {
|
|
185
|
+
const { kept, entryHolds, entryRunners, hidden, noun } = view;
|
|
164
186
|
const hiddenNote = `(${hidden} held entries hidden; run \`tumwater ${command} --list\` to see them)`;
|
|
165
187
|
const line = (entry, i) => `${entry.title}${holdMark(entryHolds[i] ?? null)}${inProgressMark(entryRunners[i] ?? null)}${entryAgeSuffix(entry.title)}`;
|
|
166
188
|
if (bugSection) {
|
|
167
189
|
const bugLines = kept.map(({ entry, position }) => `${position}. ${line(entry, position - 1)}`);
|
|
168
190
|
if (hidden > 0)
|
|
169
191
|
bugLines.push(hiddenNote);
|
|
170
|
-
|
|
171
|
-
return;
|
|
192
|
+
return `${noun}s in ${fileName}:\n${bugLines.join("\n")}`;
|
|
172
193
|
}
|
|
173
194
|
// Plans: a series' blocked parts show as one line naming their positions, as the prompt's
|
|
174
195
|
// backlog index does; numbering is unchanged so --show/--edit/--cancel <n> reach every part.
|
|
@@ -189,7 +210,26 @@ function announceActiveList(root, fileName, command, bugSection, sectionTitle, g
|
|
|
189
210
|
lines.push("(each blocked series is one line; --show <n> prints a part, --json lists every entry)");
|
|
190
211
|
if (hidden > 0)
|
|
191
212
|
lines.push(hiddenNote);
|
|
192
|
-
|
|
213
|
+
return `${noun}s in ${fileName}:\n${lines.join("\n")}`;
|
|
214
|
+
}
|
|
215
|
+
/** The active half of the list shell: the open entries with their holds, in-progress claim
|
|
216
|
+
* marks and age suffixes, optionally reduced to the ready ones, as `--json` or prose, grouping
|
|
217
|
+
* a plan's blocked parts into one line (groupBlockedSeries). Orchestrates the four named
|
|
218
|
+
* phases below — the read-and-filter view (activeListView), the `--json` body (activeListJson),
|
|
219
|
+
* the empty-list prose line (activeListEmptyLine) and the bug/plan prose body
|
|
220
|
+
* (activeListProse). */
|
|
221
|
+
function announceActiveList(root, fileName, command, bugSection, sectionTitle, grep, ready, json) {
|
|
222
|
+
const view = activeListView(root, bugSection, sectionTitle, grep, ready);
|
|
223
|
+
if (json) {
|
|
224
|
+
sayJson(activeListJson(view));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const empty = activeListEmptyLine(view, command);
|
|
228
|
+
if (empty !== null) {
|
|
229
|
+
say(empty);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
say(activeListProse(view, fileName, command, bugSection));
|
|
193
233
|
}
|
|
194
234
|
/** The shared shell of the `bug --list` and `plan --list` CLI cases — the read half of the
|
|
195
235
|
* operator queue whose `--edit`/`--cancel` flags address entries by position. Reads the same
|
|
@@ -238,8 +238,35 @@ export async function topAndAnnounce(root, args, command, top) {
|
|
|
238
238
|
* $VISUAL/$EDITOR and the saved buffer goes through `editText`. A saved buffer, positional or
|
|
239
239
|
* `--file` text that rebuilds the entry byte-for-byte writes and commits nothing (`unchanged
|
|
240
240
|
* <command> <n>: <title>`), and an out-of-range position fails
|
|
241
|
-
* before the editor launches.
|
|
241
|
+
* before the editor launches. The flag vocabulary, editor round-trip, unchanged report and
|
|
242
|
+
* commit-plus-hold announcement each live in their own named helper below. */
|
|
242
243
|
export async function editAndAnnounce(root, args, command, edit, editText) {
|
|
244
|
+
const { path, json, positionals, position } = parseEditArgs(args, command);
|
|
245
|
+
await requireReadyRepo(root);
|
|
246
|
+
let text = null;
|
|
247
|
+
if (path === null && positionals.length === 0) {
|
|
248
|
+
const opened = openEditInEditor(root, command, position);
|
|
249
|
+
if (!opened.changed) {
|
|
250
|
+
sayUnchangedEdit(command, position, json, opened.file, opened.title);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
text = opened.text;
|
|
254
|
+
}
|
|
255
|
+
else if (path !== null)
|
|
256
|
+
text = readPromptFileArg(path);
|
|
257
|
+
const edited = text === null ? edit(position, positionals) : editText(position, text);
|
|
258
|
+
if (edited.unchanged) {
|
|
259
|
+
sayUnchangedEdit(command, position, json, edited.file, edited.title);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
await announceEdited(root, command, position, json, edited);
|
|
263
|
+
}
|
|
264
|
+
/** The flag vocabulary of the `--edit` shell: `--file <path>` is extracted and the repeat
|
|
265
|
+
* of the flag refused; the remaining tokens go through scanProseFlags (the `--json` boolean
|
|
266
|
+
* at most once, an `--edit <n>` position required here through scanProseFlags' one valued
|
|
267
|
+
* flag), and a stray positional alongside `--file` is refused. `fail` returns never, so on
|
|
268
|
+
* return `position` is a `number`. */
|
|
269
|
+
function parseEditArgs(args, command) {
|
|
243
270
|
const fileFlag = args.indexOf("--file");
|
|
244
271
|
const path = fileFlag >= 0 ? promptFileValue(args[fileFlag + 1]) : null;
|
|
245
272
|
const rest = path === null ? args : args.filter((_, i) => i !== fileFlag && i !== fileFlag + 1);
|
|
@@ -253,32 +280,36 @@ export async function editAndAnnounce(root, args, command, edit, editText) {
|
|
|
253
280
|
rejectStrayWithFile(positionals[0]);
|
|
254
281
|
if (position === null)
|
|
255
282
|
fail(`--edit needs a position (usage: tumwater ${command} --edit <n>)`);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const edited = text === null ? edit(position, positionals) : editText(position, text);
|
|
278
|
-
if (edited.unchanged) {
|
|
279
|
-
sayUnchanged(edited.file, edited.title);
|
|
280
|
-
return;
|
|
283
|
+
return { path, json, positionals, position };
|
|
284
|
+
}
|
|
285
|
+
/** The no-op wording when an edit changes nothing: a `--json` `{ file, title, changed: false }`
|
|
286
|
+
* payload, else a single `unchanged <command> <n>: <title>` line. */
|
|
287
|
+
function sayUnchangedEdit(command, position, json, file, title) {
|
|
288
|
+
if (json)
|
|
289
|
+
sayJson({ file, title, changed: false });
|
|
290
|
+
else
|
|
291
|
+
say(`unchanged ${command} ${position}: ${title}`);
|
|
292
|
+
}
|
|
293
|
+
/** The editor round-trip taken when the `--edit` shell has neither `--file` nor positionals:
|
|
294
|
+
* the entry rendered as `--show` prints it (showEntry/renderShownEntry) opens in
|
|
295
|
+
* $VISUAL/$EDITOR, and a saved buffer that trims to the same text reports `{ changed: false }`
|
|
296
|
+
* with the backing file's name and the shown title; otherwise the saved text is returned as
|
|
297
|
+
* `{ changed: true, text }`. */
|
|
298
|
+
function openEditInEditor(root, command, position) {
|
|
299
|
+
const shown = showEntry(root, command, position);
|
|
300
|
+
const before = renderShownEntry(shown);
|
|
301
|
+
const after = editInEditor(before, `${command}-${position}.md`);
|
|
302
|
+
if (after.trim() === before.trim()) {
|
|
303
|
+
return { changed: false, file: command === "bug" ? "BUGS.md" : "PLANS.md", title: shown.title };
|
|
281
304
|
}
|
|
305
|
+
return { changed: true, text: after };
|
|
306
|
+
}
|
|
307
|
+
/** The commit-and-announce tail of the `--edit` shell: the changed entry is committed through
|
|
308
|
+
* commitOperatorBacklogEdit with the `Edit <command>: <title>` subject (warned on an
|
|
309
|
+
* uncommitted worktree), the still-held role from the one before `position` is picked for the
|
|
310
|
+
* hold report, and the `--json` payload (with `hold` when held) or the prose `edited …` line
|
|
311
|
+
* plus stillHeldNote is printed. */
|
|
312
|
+
async function announceEdited(root, command, position, json, edited) {
|
|
282
313
|
warnIfUncommitted(await commitOperatorBacklogEdit(root, edited.file, stampedSubject("operator", `Edit ${command}: ${edited.title}`)), edited.file); // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
|
|
283
314
|
const holds = backlogHolds(root);
|
|
284
315
|
const hold = (command === "bug" ? holds.bugs : holds.plans)[position - 1] ?? null;
|
|
@@ -10,53 +10,21 @@
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { statOrNull } from "../files/files.js";
|
|
12
12
|
import { allLiveClaims } from "../runner/runner-state-load.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { firstTrimmedLineStartingWith } from "../text/text.js";
|
|
14
|
+
import { NEEDS_REVIEW_PREFIX } from "../roles/role-guidance.js";
|
|
15
15
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
16
16
|
import { roleLabel } from "../roles/role-labels.js";
|
|
17
17
|
import { actionableEntryRangesFor } from "./backlog-index-data.js";
|
|
18
18
|
import { entryKey, partKey, plannedPartKeys, plansWaitingOn, requiredParts, seriesPart } from "./backlog-parts.js";
|
|
19
19
|
import { finishedFiles, openBugEntries, plannedPlanEntries } from "./backlog.js";
|
|
20
|
-
/** The number of holds an entry's count must reach before a Needs-review note
|
|
21
|
-
* becomes a `held-for-operator` hold (the operator's decision, surfaced by parts 2/3 and 3/3). */
|
|
22
|
-
export const OPERATOR_HOLD_THRESHOLD = 3;
|
|
23
|
-
/** The held count a body records: the integer in the first Needs-review note
|
|
24
|
-
* line's `(held N)` tag, else 0. A present but non-numeric `(held …)` tag is 0 (and stops the
|
|
25
|
-
* scan), so a malformed tag never throws. Pure over the body text — the data-layer read half of
|
|
26
|
-
* the convergence backstop. No writer emits a `(held N)` tag anymore and the `**Previously held
|
|
27
|
-
* N**` markers left on disk are never read, so a freshly written flat note always reads 0 and a
|
|
28
|
-
* new hold never escalates to the operator's desk. */
|
|
29
|
-
export function heldCount(body) {
|
|
30
|
-
const tagRe = new RegExp(`${escapeRegExp(HELD_TAG_PREFIX)}(\\d+)\\)`);
|
|
31
|
-
for (const raw of body.split("\n")) {
|
|
32
|
-
const line = raw.trimStart();
|
|
33
|
-
const note = line.startsWith(NEEDS_REVIEW_PREFIX);
|
|
34
|
-
if (!note)
|
|
35
|
-
continue;
|
|
36
|
-
const tag = tagRe.exec(line);
|
|
37
|
-
if (tag !== null)
|
|
38
|
-
return Number(tag[1]);
|
|
39
|
-
if (line.includes(HELD_TAG_PREFIX))
|
|
40
|
-
return 0;
|
|
41
|
-
}
|
|
42
|
-
return 0;
|
|
43
|
-
}
|
|
44
|
-
/** The `(held N)` count on a single Needs-review note line, or undefined when the
|
|
45
|
-
* line is not such a note or carries no numeric tag. The per-line form of heldCount, so a writer
|
|
46
|
-
* that drops notes (withoutHeldForOperatorNotes) can gate on the same threshold without scanning
|
|
47
|
-
* the whole body. */
|
|
48
|
-
function lineHeldCount(line) {
|
|
49
|
-
const trimmed = line.trimStart();
|
|
50
|
-
const isNote = trimmed.startsWith(NEEDS_REVIEW_PREFIX);
|
|
51
|
-
if (!isNote)
|
|
52
|
-
return undefined;
|
|
53
|
-
const tag = new RegExp(`${escapeRegExp(HELD_TAG_PREFIX)}(\\d+)\\)`).exec(trimmed);
|
|
54
|
-
return tag === null ? undefined : Number(tag[1]);
|
|
55
|
-
}
|
|
56
20
|
/** The `**Refused …` note prefix a refusing run writes. Every hold note — this one and the
|
|
57
21
|
* imported Needs-review prefix — is matched as a line prefix (hasNoteLine)
|
|
58
22
|
* so a mention in prose never holds an entry. */
|
|
59
23
|
const REFUSED_PREFIX = "**Refused ";
|
|
24
|
+
/** The legacy marker the old increment rule left beside a Needs-review note's prior held count
|
|
25
|
+
* (`**Previously held N−1**` beside the live `(held N)`), dropped by withoutNeedsReviewNotes so
|
|
26
|
+
* a release leaves no orphan. No writer emits it anymore; only survives on live notes. */
|
|
27
|
+
const PRIOR_HELD_PREFIX = "**Previously held ";
|
|
60
28
|
/** True when `body` carries a line (ignoring leading indentation) that begins with `prefix`.
|
|
61
29
|
* The one home of the hold-note rule: a note holds its entry only when written as its own
|
|
62
30
|
* line, never when its prefix is quoted inside prose. */
|
|
@@ -80,6 +48,24 @@ export function refusalNote(body) {
|
|
|
80
48
|
export function needsReviewNote(body) {
|
|
81
49
|
return noteText(body, NEEDS_REVIEW_PREFIX);
|
|
82
50
|
}
|
|
51
|
+
/** True when `entry` is parked with the plan role — its Needs-review note is the feature
|
|
52
|
+
* role's too-large note, the automatic convergence hold, or a final-round rejection, all of
|
|
53
|
+
* which the plan role clears by splitting or replanning (role-catalog.ts `plan` find). False
|
|
54
|
+
* for the bugfix role's wait-for-the-next-failure note ("by bugfix: …"), which parks the
|
|
55
|
+
* entry for selfcheck to lift when the cluster fails again (selfcheck step 4) and which no
|
|
56
|
+
* role can act on until then. entryHold returns "needs-review" for every such note (all of
|
|
57
|
+
* them park the entry), so the doctor's `stalled entries` row (checkStalledEntries) — the
|
|
58
|
+
* stalled-with-the-plan-role surface — classifies by this agreed subset, keeping the bugfix
|
|
59
|
+
* parks out of the plan-role blame. Reads the note's text after the NEEDS_REVIEW_PREFIX once,
|
|
60
|
+
* so the row cannot disagree on which notes the plan role clears; the eligibility mark
|
|
61
|
+
* (holdMark) continues to hold every needs-review note, a by-bugfix park included. */
|
|
62
|
+
export function isPlanStalled(entry) {
|
|
63
|
+
const note = needsReviewNote(entry.body);
|
|
64
|
+
if (note === null)
|
|
65
|
+
return false;
|
|
66
|
+
// A "by bugfix:" note is a wait for evidence that selfcheck clears, not plan-role work.
|
|
67
|
+
return !note.includes("by bugfix");
|
|
68
|
+
}
|
|
83
69
|
/** `lines` without any line starting with `prefix` (the same line-prefix rule hasNoteLine
|
|
84
70
|
* applies). When a dropped note sat between two blank lines, the blank line after it goes too,
|
|
85
71
|
* so releasing leaves one blank line where the note was. Every other line is kept. */
|
|
@@ -101,33 +87,25 @@ function withoutNoteLines(lines, prefix) {
|
|
|
101
87
|
export function withoutRefusalNotes(lines) {
|
|
102
88
|
return withoutNoteLines(lines, REFUSED_PREFIX);
|
|
103
89
|
}
|
|
104
|
-
/** `lines`
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (c !== undefined && c >= OPERATOR_HOLD_THRESHOLD) {
|
|
117
|
-
priorHeld.add(c - 1);
|
|
118
|
-
drop.add(i);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
if (priorHeld.size === 0)
|
|
90
|
+
/** `lines` without its Needs-review note lines and the legacy `**Previously held N**`
|
|
91
|
+
* markers the old increment rule left beside them, the release's lift when the entry has no
|
|
92
|
+
* Refused note: a `--release` that finds no Refused note drops every Needs-review note line so
|
|
93
|
+
* the entry becomes eligible, and takes each `**Previously held N**` marker in the slice with
|
|
94
|
+
* it, so the lift leaves no orphan whether or not the note still carries a `(held N)` tag.
|
|
95
|
+
* Nothing changes when the slice carries no Needs-review note, so a marker that sits on an
|
|
96
|
+
* eligible entry stays put. Every other line, Refused notes included, is kept, so the
|
|
97
|
+
* Needs-review lift never lifts a refusal. */
|
|
98
|
+
export function withoutNeedsReviewNotes(lines) {
|
|
99
|
+
// A lift only happens when the slice carries a Needs-review note; without one, the slice
|
|
100
|
+
// (and any legacy prior-held marker on it) is left byte for byte.
|
|
101
|
+
if (!lines.some((raw) => raw.trimStart().startsWith(NEEDS_REVIEW_PREFIX)))
|
|
122
102
|
return lines;
|
|
103
|
+
const drop = new Set();
|
|
123
104
|
lines.forEach((raw, i) => {
|
|
124
|
-
if (drop.has(i))
|
|
125
|
-
return;
|
|
126
105
|
const trimmed = raw.trimStart();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (count !== null && priorHeld.has(Number(count[0])))
|
|
106
|
+
// Every Needs-review note goes, and so does each `**Previously held N**` marker the old
|
|
107
|
+
// increment rule wrote with it.
|
|
108
|
+
if (trimmed.startsWith(NEEDS_REVIEW_PREFIX) || trimmed.startsWith(PRIOR_HELD_PREFIX))
|
|
131
109
|
drop.add(i);
|
|
132
110
|
});
|
|
133
111
|
const out = [];
|
|
@@ -140,7 +118,7 @@ export function withoutHeldForOperatorNotes(lines) {
|
|
|
140
118
|
while (j < lines.length && drop.has(j))
|
|
141
119
|
j++;
|
|
142
120
|
// A dropped run between two blank lines drops the blank after it too, so the lift leaves
|
|
143
|
-
// one blank line where the note was (the same rule withoutNoteLines applies to one).
|
|
121
|
+
// one blank line where the note was (the same rule withoutNoteLines applies to one note).
|
|
144
122
|
if (j < lines.length && (lines[i - 1] ?? "x").trim() === "" && (lines[j] ?? "x").trim() === "")
|
|
145
123
|
j++;
|
|
146
124
|
i = j - 1;
|
|
@@ -209,13 +187,10 @@ export function entryHold(entry, planned) {
|
|
|
209
187
|
export function entryHoldAgainst(entry, plannedParts) {
|
|
210
188
|
if (hasNoteLine(entry.body, REFUSED_PREFIX))
|
|
211
189
|
return "refused";
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return
|
|
216
|
-
? "held-for-operator" // vocabulary-ok: the operator-held string names the operator hold decision
|
|
217
|
-
: "needs-review";
|
|
218
|
-
}
|
|
190
|
+
// A Needs-review note — the feature role's too-large note or the convergence hold's — parks the
|
|
191
|
+
// entry for the plan role to split or replan, at any count: nothing escalates to the operator.
|
|
192
|
+
if (hasNoteLine(entry.body, NEEDS_REVIEW_PREFIX))
|
|
193
|
+
return "needs-review";
|
|
219
194
|
const refs = requiredParts(entry.title);
|
|
220
195
|
if (refs.length === 0)
|
|
221
196
|
return null;
|
|
@@ -235,8 +210,6 @@ export function holdMark(hold) {
|
|
|
235
210
|
return " [blocked: refused]";
|
|
236
211
|
if (hold === "needs-review")
|
|
237
212
|
return " [blocked: needs review]";
|
|
238
|
-
if (hold === "held-for-operator")
|
|
239
|
-
return " [blocked: held for operator]"; // vocabulary-ok: this hold names the operator-held wording for the operator decision
|
|
240
213
|
return ` [blocked: requires ${hold.blockedBy.join(", ")}]`;
|
|
241
214
|
}
|
|
242
215
|
/** The line `plan|bug --release` and `--top` print when the entry they touched is still held:
|
|
@@ -305,67 +278,28 @@ export function backlogHolds(root) {
|
|
|
305
278
|
* `tumwater backlog` prints. A surface derives its marks from the one hold decision and the
|
|
306
279
|
* one claim lookup rather than re-walking the entries. An eligible entry nobody is working on
|
|
307
280
|
* renders as the empty string, so a client can append the mark without a separate check.
|
|
308
|
-
* Questions carry no holds. backlogHolds alone still drives the allocator.
|
|
309
|
-
|
|
281
|
+
* Questions carry no holds. backlogHolds alone still drives the allocator. A caller that
|
|
282
|
+
* already resolved the claim lookup (for the same poll's marks AND its own parallel checks,
|
|
283
|
+
* e.g. statusPayload's `working` or the TUI's `runners`) passes it as `working` so the claims
|
|
284
|
+
* walk is done once, not re-run inside both calls — the same shared single read the module's
|
|
285
|
+
* other single-home polls observe. */
|
|
286
|
+
export function backlogMarks(root, working) {
|
|
310
287
|
const holds = backlogHolds(root);
|
|
311
|
-
const
|
|
288
|
+
const inProgress = working ?? backlogInProgress(root);
|
|
312
289
|
const mark = (hold, runner) => holdMark(hold) + inProgressMark(runner ?? null);
|
|
313
290
|
return {
|
|
314
|
-
plans: holds.plans.map((h, i) => mark(h,
|
|
315
|
-
bugs: holds.bugs.map((h, i) => mark(h,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
/** A planned plan or open bug whose hold is `"held-for-operator"`, in file order: the
|
|
319
|
-
* entries parked until the operator lifts them with `tumwater plan --release` or `tumwater
|
|
320
|
-
* bug --release` (the `operator-held` team alert's input), each carrying the backlog file
|
|
321
|
-
* and its 0-based position so a surface opening the row can land on it, the same RefusedEntry
|
|
322
|
-
* shape refusedEntries returns. The convergence hold writes only on plans, but a role's own
|
|
323
|
-
* too-large note can sit on a bug and reach this threshold, so a bug can be held this way too.
|
|
324
|
-
* A missing file yields `[]`. */
|
|
325
|
-
export function operatorHeldEntries(root) {
|
|
326
|
-
const holds = backlogHolds(root);
|
|
327
|
-
const out = [];
|
|
328
|
-
const entriesOf = (entries, held, file) => {
|
|
329
|
-
entries.forEach((e, i) => {
|
|
330
|
-
if (held[i] === "held-for-operator")
|
|
331
|
-
out.push({ file, index: i, title: e.title }); // vocabulary-ok: the held-for-operator hold's key
|
|
332
|
-
});
|
|
291
|
+
plans: holds.plans.map((h, i) => mark(h, inProgress.plans[i])),
|
|
292
|
+
bugs: holds.bugs.map((h, i) => mark(h, inProgress.bugs[i])),
|
|
333
293
|
};
|
|
334
|
-
entriesOf(plannedPlanEntries(root), holds.plans, "plans");
|
|
335
|
-
entriesOf(openBugEntries(root), holds.bugs, "bugs");
|
|
336
|
-
return out;
|
|
337
294
|
}
|
|
338
295
|
/** The display noun for a backlog-file discriminator — the `"plans" | "bugs"` `file` on
|
|
339
|
-
* RefusedEntry
|
|
296
|
+
* RefusedEntry (and the same discriminator the prompt's named-hold wording
|
|
340
297
|
* and the doctor's Stalled row take): BUGS.md is a "Bug" and PLANS.md a "Plan", and the
|
|
341
298
|
* lowercase form is the `Refused plan|bug:` label and the `tumwater plan|bug --release`
|
|
342
|
-
* command noun
|
|
299
|
+
* command noun. */
|
|
343
300
|
export function entryKindLabel(file) {
|
|
344
301
|
return file === "bugs" ? "Bug" : "Plan";
|
|
345
302
|
}
|
|
346
|
-
/** The planned plans and open bugs whose hold is exactly `"needs-review"`, in file order: the
|
|
347
|
-
* entries parked by a Needs-review note until the plan role splits or replans them (the
|
|
348
|
-
* `stalled-entries` team alert's input), each carrying its backlog file, 0-based index and the
|
|
349
|
-
* hold. Same `backlogHolds(root)` + the planned/`openBugEntries` readers `operatorHeldEntries`
|
|
350
|
-
* uses, so the alert and the doctor's `stalled entries` row classify through the same
|
|
351
|
-
* `entryHold` and cannot disagree on which holds are role-resolvable. A held-for-operator
|
|
352
|
-
* entry (a note at the held threshold) is
|
|
353
|
-
* excluded — it feeds `operatorHeldEntries` and the `operator-held` alert instead. A missing
|
|
354
|
-
* file yields `[]`. */
|
|
355
|
-
export function stalledEntries(root) {
|
|
356
|
-
const holds = backlogHolds(root);
|
|
357
|
-
const out = [];
|
|
358
|
-
const entriesOf = (entries, held, file) => {
|
|
359
|
-
entries.forEach((e, i) => {
|
|
360
|
-
const hold = held[i];
|
|
361
|
-
if (hold === "needs-review")
|
|
362
|
-
out.push({ file, index: i, title: e.title, hold });
|
|
363
|
-
});
|
|
364
|
-
};
|
|
365
|
-
entriesOf(plannedPlanEntries(root), holds.plans, "plans");
|
|
366
|
-
entriesOf(openBugEntries(root), holds.bugs, "bugs");
|
|
367
|
-
return out;
|
|
368
|
-
}
|
|
369
303
|
/** The planned plans, then the open bugs, whose hold is `"refused"`, in file order: the
|
|
370
304
|
* entries that wait on an operator decision (the `refused` team alert's input), each carrying
|
|
371
305
|
* the backlog file and its 0-based position so a surface opening the row can land on it.
|