tumwater 0.1.2 → 0.2.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 +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** The CLI shells of `tumwater bug` and `tumwater plan` — flag classification, the preflights,
|
|
3
|
+
* and the confirmation each case prints — over the entry-writing primitives in
|
|
4
|
+
* backlog-write.ts (the read half's mirror, backlog.ts). cli.ts reaches the file, cancel,
|
|
5
|
+
* release, edit, list, and show cases only through the `*AndAnnounce` shells here; the markdown
|
|
6
|
+
* surgery and the locked read-modify-write live in backlog-write.ts. The wake of the loop that
|
|
7
|
+
* should act stays in cli.ts beside the other command bodies. */
|
|
8
|
+
import { fail, say, sayJson, warnIfUncommitted } from "../cli/cli-output.js";
|
|
9
|
+
import { parseCountFlag, parsePositionFlag, rejectRepeatFlag } from "../cli/cli-args.js";
|
|
10
|
+
import { promptFileValue, readPromptFileArg } from "../cli/cli-command-args.js";
|
|
11
|
+
import { DONE_DATE_RE, FIXED_DATE_RE, completionDate, donePlanEntries, fixedBugEntries, openBugEntries, plannedPlanEntries, ageInDays, entryAgeSuffix, entryMatches, filingDate, } from "./backlog.js";
|
|
12
|
+
import { backlogHolds, backlogInProgress, holdMark, inProgressMark } from "./backlog-eligibility.js";
|
|
13
|
+
import { groupBlockedSeries, partsLabel } from "./backlog-structure.js";
|
|
14
|
+
import { numberedList } from "../text/markdown.js";
|
|
15
|
+
import { collapseWhitespace } from "../text/text.js";
|
|
16
|
+
import { JSON_FLAG, rejectUnknownArgs } from "../cli/cli-flag-specs.js";
|
|
17
|
+
import { requireReadyRepo } from "../cli/cli-repo-gate.js";
|
|
18
|
+
import { editInEditor } from "../cli/cli-editor.js";
|
|
19
|
+
import { requestWake, submitRolePromptAndWake } from "../operator/operator-intent.js";
|
|
20
|
+
import { promptLengthProblem } from "../inbox/inbox-submit.js";
|
|
21
|
+
import { commitOperatorBacklogEdit } from "./backlog-commit.js";
|
|
22
|
+
import { stampedSubject } from "../git/commit-message.js";
|
|
23
|
+
import { sectionNoun, showEntry, splitEntryFile } from "./backlog-write.js";
|
|
24
|
+
import { archiveFinishedOnMain } from "./backlog-archive.js";
|
|
25
|
+
import { plural } from "../text/phrases.js";
|
|
26
|
+
/** The title each file command derives from the CLI positionals, exported so the CLI case
|
|
27
|
+
* can hand fileAndAnnounce the same derivation the write will carry: fileAndAnnounce
|
|
28
|
+
* preflights the composed wake prompt (wakeText(title)) against the submission cap BEFORE
|
|
29
|
+
* the write, and a titleOf that drifted from file()'s own derivation would preflight the
|
|
30
|
+
* wrong text. bugTitleOf(positionals) is exactly fileBug's symptom when the CLI case passes
|
|
31
|
+
* positionals.join(" ") as its raw text; planTitleOf(positionals) is exactly filePlan's
|
|
32
|
+
* title when it passes positionals[0]. */
|
|
33
|
+
export function bugTitleOf(positionals) {
|
|
34
|
+
return collapseWhitespace(positionals.join(" "));
|
|
35
|
+
}
|
|
36
|
+
export function planTitleOf(positionals) {
|
|
37
|
+
return collapseWhitespace(positionals[0] ?? "");
|
|
38
|
+
}
|
|
39
|
+
/** A positional alongside `--file` on a `plan|bug` command is refused: the entry's text comes
|
|
40
|
+
* from the file, so a stray positional almost always means the operator pasted prose or
|
|
41
|
+
* forgot `--file`. The one home of that check, shared by fileAndAnnounce and editAndAnnounce
|
|
42
|
+
* so the two `--file` preflights cannot drift on the message. */
|
|
43
|
+
function rejectStrayWithFile(stray) {
|
|
44
|
+
if (stray !== undefined)
|
|
45
|
+
fail(`unexpected argument ${JSON.stringify(stray)} — with --file the entry comes from the file`);
|
|
46
|
+
}
|
|
47
|
+
/** The one home of the prose-carrying file commands' flag classification (the `bug`/`plan`
|
|
48
|
+
* file forms and their `--edit` forms): `--json` is the one boolean, refused on a repeat
|
|
49
|
+
* (the "each flag at most once" rule every spec-parsed command and the questions parser
|
|
50
|
+
* apply); when a position flag is supplied it is the one valued flag, likewise refused on a
|
|
51
|
+
* repeat and parsed through the shared plain-decimal rule (parsePositionFlag); `--help`/`-h`
|
|
52
|
+
* rides into the prose positionals rather than being skipped, because the dispatcher's help
|
|
53
|
+
* interception stands down for text-carrying invocations (cli.ts) so a help token here is
|
|
54
|
+
* always content; every other flag-shaped token is named by rejectUnknownArgs, and every
|
|
55
|
+
* bare token is prose. Tokens keep their original order. */
|
|
56
|
+
function scanProseFlags(args, command, positionFlag) {
|
|
57
|
+
let json = false;
|
|
58
|
+
let position = null;
|
|
59
|
+
const positionals = [];
|
|
60
|
+
for (let i = 0; i < args.length; i++) {
|
|
61
|
+
const arg = args[i] ?? "";
|
|
62
|
+
if (arg === "--json") {
|
|
63
|
+
rejectRepeatFlag(json, "--json");
|
|
64
|
+
json = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (arg === positionFlag?.name) {
|
|
68
|
+
rejectRepeatFlag(position !== null, positionFlag.name);
|
|
69
|
+
position = parsePositionFlag(positionFlag.name, args[i + 1], positionFlag.example);
|
|
70
|
+
i++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (arg === "--help" || arg === "-h") {
|
|
74
|
+
positionals.push(arg);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (arg.startsWith("-"))
|
|
78
|
+
rejectUnknownArgs(command, [arg], [JSON_FLAG]);
|
|
79
|
+
else
|
|
80
|
+
positionals.push(arg);
|
|
81
|
+
}
|
|
82
|
+
return { json, positionals, position };
|
|
83
|
+
}
|
|
84
|
+
/** The shared shell of the `bug` and `plan` CLI cases — the one home of the five-step
|
|
85
|
+
* sequence both commands run in cli.ts: peel the free-form positionals (the questions
|
|
86
|
+
* command's prose-token pattern), reject anything flag-shaped that is not `--json`, run the
|
|
87
|
+
* ready-repo gate the other backlog commands run, write the entry via `file`, wake the
|
|
88
|
+
* `role` loop that should act so it picks the entry up within one poll, then announce — the
|
|
89
|
+
* wake always fires, its confirmation line rides the prose output only, in `--json` mode
|
|
90
|
+
* stdout is the payload document alone (the prompt --json precedent) so a script can parse
|
|
91
|
+
* it. The command name and usage string ride in for the reject/confirmation/empty-arg
|
|
92
|
+
* wordings (fileBug/filePlan's empty-arg usage lines); `wakeText` builds the loop prompt
|
|
93
|
+
* from the filed entry's title and `titleOf` derives that title from the positionals (the
|
|
94
|
+
* exported bugTitleOf/planTitleOf, so the preflight and the write see one derivation).
|
|
95
|
+
* The wake prompt rides into the target tick's prefill under the same submission cap every
|
|
96
|
+
* prompt answers to, so fileAndAnnounce checks it (promptLengthProblem) BEFORE calling
|
|
97
|
+
* `file` — otherwise an over-long operator text would write its entry and only then throw
|
|
98
|
+
* inside submitRolePromptAndWake, half-applying the command with the backlog file already
|
|
99
|
+
* grown. With `--file <path>` (`-` reads stdin) the entry comes from a markdown file instead
|
|
100
|
+
* of positionals: `fileText` receives its text (splitEntryFile: first line the title, the rest
|
|
101
|
+
* the body kept line for line), the same preflight runs on that title, and any positional
|
|
102
|
+
* alongside --file is refused. */
|
|
103
|
+
export async function fileAndAnnounce(root, args, command, file, role, wakeText, titleOf, fileText) {
|
|
104
|
+
const fileFlag = args.indexOf("--file");
|
|
105
|
+
if (fileFlag >= 0) {
|
|
106
|
+
const file = promptFileValue(args[fileFlag + 1]);
|
|
107
|
+
const rest = args.filter((_, i) => i !== fileFlag && i !== fileFlag + 1);
|
|
108
|
+
rejectRepeatFlag(rest.includes("--file"), "--file");
|
|
109
|
+
const { json, positionals } = scanProseFlags(rest, command);
|
|
110
|
+
rejectStrayWithFile(positionals[0]);
|
|
111
|
+
await requireReadyRepo(root);
|
|
112
|
+
const text = readPromptFileArg(file);
|
|
113
|
+
const wakeProblem = promptLengthProblem(wakeText(splitEntryFile(text).title), role);
|
|
114
|
+
if (wakeProblem)
|
|
115
|
+
fail(wakeProblem);
|
|
116
|
+
await announceFiled(root, command, fileText(text), role, wakeText, json);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// peelPositionals cannot serve this command: bug/plan's text is operator prose, and a
|
|
120
|
+
// standalone --help/-h token inside it ("the TUI mishandles --help output") is content —
|
|
121
|
+
// the dispatcher's help interception stands down for text-carrying invocations (cli.ts),
|
|
122
|
+
// so a help token that reaches here is always prose, and scanProseFlags keeps it in the
|
|
123
|
+
// positionals rather than flag-shaping it into rest for rejectUnknownArgs to refuse.
|
|
124
|
+
const { json, positionals } = scanProseFlags(args, command);
|
|
125
|
+
await requireReadyRepo(root);
|
|
126
|
+
const wakeProblem = promptLengthProblem(wakeText(titleOf(positionals)), role);
|
|
127
|
+
if (wakeProblem)
|
|
128
|
+
fail(wakeProblem);
|
|
129
|
+
await announceFiled(root, command, file(positionals), role, wakeText, json);
|
|
130
|
+
}
|
|
131
|
+
/** The tail both fileAndAnnounce forms share: commit the entry on main, wake the `role`
|
|
132
|
+
* loop with the filed entry's title, then print the confirmation (the wake line rides the
|
|
133
|
+
* prose output only). */
|
|
134
|
+
async function announceFiled(root, command, filed, role, wakeText, json) {
|
|
135
|
+
warnIfUncommitted(await commitOperatorBacklogEdit(root, filed.file, stampedSubject("operator", `File ${command}: ${filed.title}`)), filed.file); // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
|
|
136
|
+
const wake = submitRolePromptAndWake(root, role, wakeText(filed.title));
|
|
137
|
+
sayFiled(command, filed, json);
|
|
138
|
+
if (!json)
|
|
139
|
+
say(wake);
|
|
140
|
+
}
|
|
141
|
+
/** The shared shell of the `bug --cancel` and `plan --cancel` CLI cases, mirroring
|
|
142
|
+
* fileAndAnnounce's flag handling: `--cancel <n>` is the one valued flag, `--json` the one
|
|
143
|
+
* boolean (at most once), `--help`/`-h` tolerated; any positional or other flag is refused
|
|
144
|
+
* through rejectUnknownArgs. The position parses through the plain-decimal rule every position
|
|
145
|
+
* flag shares (parsePositionFlag, cli-args.ts), not Number(). After requireReadyRepo it prints
|
|
146
|
+
* `cancelled <command> in <file>: <title>` or the `--json` payload {file, title}. Unlike
|
|
147
|
+
* fileAndAnnounce there is no wake: removing work must not wake a loop. */
|
|
148
|
+
export async function cancelAndAnnounce(root, args, command, cancel) {
|
|
149
|
+
let position = null;
|
|
150
|
+
let json = false;
|
|
151
|
+
const rest = [];
|
|
152
|
+
for (let i = 0; i < args.length; i++) {
|
|
153
|
+
const arg = args[i];
|
|
154
|
+
if (arg === "--json") {
|
|
155
|
+
rejectRepeatFlag(json, "--json");
|
|
156
|
+
json = true;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (arg === "--cancel") {
|
|
160
|
+
rejectRepeatFlag(position !== null, "--cancel");
|
|
161
|
+
position = parsePositionFlag("--cancel", args[i + 1], "--cancel 2");
|
|
162
|
+
i++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (arg === "--help" || arg === "-h")
|
|
166
|
+
continue;
|
|
167
|
+
rest.push(arg);
|
|
168
|
+
}
|
|
169
|
+
// rejectUnknownArgs names both a stray positional and an unknown flag, with this command's
|
|
170
|
+
// (empty) valid-flag list — the vocabulary a cancel invocation accepts is exactly --cancel
|
|
171
|
+
// and --json, both consumed above.
|
|
172
|
+
rejectUnknownArgs(command, rest, []);
|
|
173
|
+
if (position === null)
|
|
174
|
+
fail(`--cancel needs a position (usage: tumwater ${command} --cancel <n>)`);
|
|
175
|
+
await requireReadyRepo(root);
|
|
176
|
+
const removed = cancel(position);
|
|
177
|
+
warnIfUncommitted(await commitOperatorBacklogEdit(root, removed.file, stampedSubject("operator", `Cancel ${command}: ${removed.title}`)), removed.file); // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
|
|
178
|
+
if (json)
|
|
179
|
+
sayJson({ file: removed.file, title: removed.title });
|
|
180
|
+
else
|
|
181
|
+
say(`cancelled ${command} in ${removed.file}: ${removed.title}`);
|
|
182
|
+
}
|
|
183
|
+
/** The shell of the `bug --release` and `plan --release` CLI cases, parsed as cancelAndAnnounce
|
|
184
|
+
* parses `--cancel`: `--release <n>`, an optional `--json`, anything else refused. After
|
|
185
|
+
* requireReadyRepo it releases the entry, commits the file on main, and wakes `role` (the
|
|
186
|
+
* released entry is eligible now, and that loop may be backed off) with no queued prompt. Prints
|
|
187
|
+
* `released <command> in <file>: <title>` plus the wake line, or only the `--json` payload. */
|
|
188
|
+
export async function releaseAndAnnounce(root, args, command, release, role) {
|
|
189
|
+
let position = null;
|
|
190
|
+
let json = false;
|
|
191
|
+
const rest = [];
|
|
192
|
+
for (let i = 0; i < args.length; i++) {
|
|
193
|
+
const arg = args[i];
|
|
194
|
+
if (arg === "--json") {
|
|
195
|
+
rejectRepeatFlag(json, "--json");
|
|
196
|
+
json = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (arg === "--release") {
|
|
200
|
+
rejectRepeatFlag(position !== null, "--release");
|
|
201
|
+
position = parsePositionFlag("--release", args[i + 1], "--release 2");
|
|
202
|
+
i++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (arg === "--help" || arg === "-h")
|
|
206
|
+
continue;
|
|
207
|
+
rest.push(arg);
|
|
208
|
+
}
|
|
209
|
+
rejectUnknownArgs(command, rest, []);
|
|
210
|
+
if (position === null)
|
|
211
|
+
fail(`--release needs a position (usage: tumwater ${command} --release <n>)`);
|
|
212
|
+
await requireReadyRepo(root);
|
|
213
|
+
const released = release(position);
|
|
214
|
+
warnIfUncommitted(await commitOperatorBacklogEdit(root, released.file, stampedSubject("operator", `Release ${command}: ${released.title}`)), released.file); // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
|
|
215
|
+
const wake = requestWake(root, [role]);
|
|
216
|
+
if (json)
|
|
217
|
+
sayJson({ file: released.file, title: released.title });
|
|
218
|
+
else {
|
|
219
|
+
say(`released ${command} in ${released.file}: ${released.title}`);
|
|
220
|
+
say(wake);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** The shell of the `bug --top` and `plan --top` CLI cases, parsed as releaseAndAnnounce parses
|
|
224
|
+
* `--release`: `--top <n>`, an optional `--json`, anything else refused. After requireReadyRepo
|
|
225
|
+
* it moves the entry to the head of its section and commits the file on main. No wake: a
|
|
226
|
+
* reorder makes no entry newly eligible. Prints `moved <command> to the top of <file>: <title>`
|
|
227
|
+
* or only the `--json` payload. */
|
|
228
|
+
export async function topAndAnnounce(root, args, command, top) {
|
|
229
|
+
let position = null;
|
|
230
|
+
let json = false;
|
|
231
|
+
const rest = [];
|
|
232
|
+
for (let i = 0; i < args.length; i++) {
|
|
233
|
+
const arg = args[i];
|
|
234
|
+
if (arg === "--json") {
|
|
235
|
+
rejectRepeatFlag(json, "--json");
|
|
236
|
+
json = true;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (arg === "--top") {
|
|
240
|
+
rejectRepeatFlag(position !== null, "--top");
|
|
241
|
+
position = parsePositionFlag("--top", args[i + 1], "--top 2");
|
|
242
|
+
i++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (arg === "--help" || arg === "-h")
|
|
246
|
+
continue;
|
|
247
|
+
rest.push(arg);
|
|
248
|
+
}
|
|
249
|
+
rejectUnknownArgs(command, rest, []);
|
|
250
|
+
if (position === null)
|
|
251
|
+
fail(`--top needs a position (usage: tumwater ${command} --top <n>)`);
|
|
252
|
+
await requireReadyRepo(root);
|
|
253
|
+
const moved = top(position);
|
|
254
|
+
warnIfUncommitted(await commitOperatorBacklogEdit(root, moved.file, stampedSubject("operator", `Prioritize ${command}: ${moved.title}`)), moved.file); // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
|
|
255
|
+
if (json)
|
|
256
|
+
sayJson({ file: moved.file, title: moved.title });
|
|
257
|
+
else
|
|
258
|
+
say(`moved ${command} to the top of ${moved.file}: ${moved.title}`);
|
|
259
|
+
}
|
|
260
|
+
/** Ascending positions as runs: consecutive ones join with an en dash (`13–17`), runs separate
|
|
261
|
+
* with commas (`13–15, 18`). */
|
|
262
|
+
function positionRuns(positions) {
|
|
263
|
+
const runs = [];
|
|
264
|
+
for (let i = 0; i < positions.length;) {
|
|
265
|
+
let j = i;
|
|
266
|
+
while (j + 1 < positions.length && positions[j + 1] === positions[j] + 1)
|
|
267
|
+
j++;
|
|
268
|
+
runs.push(j > i ? `${positions[i]}–${positions[j]}` : `${positions[i]}`);
|
|
269
|
+
i = j + 1;
|
|
270
|
+
}
|
|
271
|
+
return runs.join(", ");
|
|
272
|
+
}
|
|
273
|
+
/** The shared shell of the `bug --list` and `plan --list` CLI cases — the read half of the
|
|
274
|
+
* operator queue whose `--edit`/`--cancel` flags address entries by position. Reads the same
|
|
275
|
+
* entries, in the same file order, that locateEntry numbers for `--edit`/`--cancel`, pairs
|
|
276
|
+
* each with its eligibility hold (backlogHolds), and prints them numbered from 1, or the
|
|
277
|
+
* command's `no …` line on an empty or missing file. Like `tumwater backlog` and `tumwater
|
|
278
|
+
* questions` it is read-only: no requireReadyRepo gate and no wake. `--list` and `--json` are
|
|
279
|
+
* the two booleans this shell consumes (repeats refused), `--help`/`-h` is tolerated, and any
|
|
280
|
+
* positional or other flag is refused through rejectUnknownArgs.
|
|
281
|
+
*
|
|
282
|
+
* `--done [-n <count>] [--grep <text>]` (each once; `-n` and `--grep` need `--done`) lists the
|
|
283
|
+
* newest finished entries instead (BUGS.md ## Fixed or PLANS.md ## Done, numbered, default 10)
|
|
284
|
+
* with `older` counting those left out; --json carries each entry's `position` and
|
|
285
|
+
* `completedOn` heading date. `--grep` keeps the entries whose heading or body contains the
|
|
286
|
+
* text (case-insensitive); each keeps its unfiltered number, so it is the `--show <n> --done`
|
|
287
|
+
* number, and `older` counts matches left out. `plan --list` prose
|
|
288
|
+
* groups a series' blocked parts into one line (groupBlockedSeries). */
|
|
289
|
+
export function listAndAnnounce(root, args, command, fileName, sectionTitle) {
|
|
290
|
+
let json = false;
|
|
291
|
+
let list = false;
|
|
292
|
+
let done = false;
|
|
293
|
+
let count = null;
|
|
294
|
+
let grep = null;
|
|
295
|
+
const rest = [];
|
|
296
|
+
for (let i = 0; i < args.length; i++) {
|
|
297
|
+
const arg = args[i];
|
|
298
|
+
if (arg === "--grep") {
|
|
299
|
+
rejectRepeatFlag(grep !== null, "--grep");
|
|
300
|
+
const text = args[++i];
|
|
301
|
+
if (text === undefined || text === "") {
|
|
302
|
+
fail(`--grep needs text (usage: tumwater ${command} --list --done --grep <text>)`);
|
|
303
|
+
}
|
|
304
|
+
grep = text;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (arg === "--done") {
|
|
308
|
+
rejectRepeatFlag(done, "--done");
|
|
309
|
+
done = true;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (arg === "-n") {
|
|
313
|
+
rejectRepeatFlag(count !== null, "-n");
|
|
314
|
+
count = parseCountFlag("-n", args[++i]);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (arg === "--list") {
|
|
318
|
+
rejectRepeatFlag(list, "--list");
|
|
319
|
+
list = true;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (arg === "--json") {
|
|
323
|
+
rejectRepeatFlag(json, "--json");
|
|
324
|
+
json = true;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (arg === "--help" || arg === "-h")
|
|
328
|
+
continue;
|
|
329
|
+
rest.push(arg);
|
|
330
|
+
}
|
|
331
|
+
// rejectUnknownArgs names a stray positional and an unknown flag with this command's (empty)
|
|
332
|
+
// valid-flag list: the vocabulary a list invocation accepts is exactly --list and --json,
|
|
333
|
+
// both consumed above.
|
|
334
|
+
rejectUnknownArgs(command, rest, []);
|
|
335
|
+
if (count !== null && !done)
|
|
336
|
+
fail("-n needs --done (it limits the finished entries listed)");
|
|
337
|
+
if (grep !== null && !done)
|
|
338
|
+
fail("--grep needs --done (it searches the finished entries)");
|
|
339
|
+
// BUGS.md's `## Open` is the one section whose noun is "bug"; every other caller is a plan.
|
|
340
|
+
const bugSection = sectionTitle === "Open";
|
|
341
|
+
if (done) {
|
|
342
|
+
const numbered = (bugSection ? fixedBugEntries(root) : donePlanEntries(root)).map((entry, i) => ({
|
|
343
|
+
entry,
|
|
344
|
+
position: i + 1,
|
|
345
|
+
}));
|
|
346
|
+
const pattern = grep;
|
|
347
|
+
const matches = pattern === null ? numbered : numbered.filter((m) => entryMatches(m.entry, pattern));
|
|
348
|
+
const limit = count ?? 10;
|
|
349
|
+
const shown = matches.slice(0, limit);
|
|
350
|
+
const older = matches.length - shown.length;
|
|
351
|
+
if (json) {
|
|
352
|
+
const dateRe = bugSection ? FIXED_DATE_RE : DONE_DATE_RE;
|
|
353
|
+
sayJson({
|
|
354
|
+
entries: shown.map(({ entry: e, position }) => ({
|
|
355
|
+
position,
|
|
356
|
+
title: e.title,
|
|
357
|
+
body: e.body,
|
|
358
|
+
completedOn: completionDate(e.title, dateRe),
|
|
359
|
+
})),
|
|
360
|
+
older,
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const what = bugSection ? "fixed bugs" : "done plans";
|
|
365
|
+
if (shown.length === 0) {
|
|
366
|
+
say(pattern === null ? `no ${what}` : `no ${what} match "${pattern}"`);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const lines = shown.map((m) => `${m.position}. ${m.entry.title}`);
|
|
370
|
+
if (older > 0)
|
|
371
|
+
lines.push(`(${older} older; -n ${matches.length} shows more)`);
|
|
372
|
+
const header = pattern === null ? `${what} in ${fileName}, newest first:` : `${what} in ${fileName} matching "${pattern}", newest first:`;
|
|
373
|
+
say(`${header}\n${lines.join("\n")}`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const entries = bugSection ? openBugEntries(root) : plannedPlanEntries(root);
|
|
377
|
+
const holds = backlogHolds(root);
|
|
378
|
+
const entryHolds = bugSection ? holds.bugs : holds.plans;
|
|
379
|
+
const inProgress = backlogInProgress(root);
|
|
380
|
+
const entryRunners = bugSection ? inProgress.bugs : inProgress.plans;
|
|
381
|
+
if (json) {
|
|
382
|
+
sayJson({
|
|
383
|
+
entries: entries.map((entry, i) => {
|
|
384
|
+
const since = filingDate(entry.title);
|
|
385
|
+
return {
|
|
386
|
+
position: i + 1,
|
|
387
|
+
title: entry.title,
|
|
388
|
+
body: entry.body,
|
|
389
|
+
hold: entryHolds[i] ?? null,
|
|
390
|
+
inProgress: entryRunners[i] ?? null,
|
|
391
|
+
// The filing stamp the entry's own heading carries, and how many days it has waited
|
|
392
|
+
// since then (both present only when the heading names a stamp — an undated entry
|
|
393
|
+
// keeps the pre-existing shape with neither key).
|
|
394
|
+
...(since === null ? {} : { since, ageDays: ageInDays(since) }),
|
|
395
|
+
};
|
|
396
|
+
}),
|
|
397
|
+
});
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const noun = sectionNoun(sectionTitle);
|
|
401
|
+
if (entries.length === 0) {
|
|
402
|
+
say(`no ${noun}s`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const line = (entry, i) => `${entry.title}${holdMark(entryHolds[i] ?? null)}${inProgressMark(entryRunners[i] ?? null)}${entryAgeSuffix(entry.title)}`;
|
|
406
|
+
if (bugSection) {
|
|
407
|
+
say(`${noun}s in ${fileName}:\n${numberedList(entries.map(line))}`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// Plans: a series' blocked parts show as one line naming their positions, as the prompt's
|
|
411
|
+
// backlog index does; numbering is unchanged so --show/--edit/--cancel <n> reach every part.
|
|
412
|
+
const rows = entries.map((entry, i) => ({
|
|
413
|
+
position: i + 1,
|
|
414
|
+
title: entry.title,
|
|
415
|
+
hold: entryHolds[i] ?? null,
|
|
416
|
+
claimer: entryRunners[i] ?? undefined,
|
|
417
|
+
}));
|
|
418
|
+
let grouped = false;
|
|
419
|
+
const lines = groupBlockedSeries(rows).map((g) => {
|
|
420
|
+
if (g.kind === "row")
|
|
421
|
+
return `${g.row.position}. ${line(g.row, g.row.position - 1)}`;
|
|
422
|
+
grouped = true;
|
|
423
|
+
return `${positionRuns(g.rows.map((r) => r.position))}. ${g.series}, ${g.rows.length} blocked parts (${partsLabel(g.rows)}) [blocked: each waits on an earlier part]`;
|
|
424
|
+
});
|
|
425
|
+
if (grouped)
|
|
426
|
+
lines.push("(each blocked series is one line; --show <n> prints a part, --json lists every entry)");
|
|
427
|
+
say(`${noun}s in ${fileName}:\n${lines.join("\n")}`);
|
|
428
|
+
}
|
|
429
|
+
/** The shell of `bug --show` and `plan --show`: `--show <n>` (once), an optional `--json`
|
|
430
|
+
* and `--done` (each once; `--done` shows the Nth newest finished entry, as `--list --done`
|
|
431
|
+
* numbers them, with its heading stamp kept and `completedOn` in --json),
|
|
432
|
+
* `--help`/`-h` tolerated, anything else refused through rejectUnknownArgs. Read-only like
|
|
433
|
+
* `--list`: no repo gate, commit, or wake. Prose is the title, a blank line, then the body, so
|
|
434
|
+
* stdout is the file `--edit <n> --file` takes back. */
|
|
435
|
+
export function showAndAnnounce(root, args, command) {
|
|
436
|
+
let position = null;
|
|
437
|
+
let json = false;
|
|
438
|
+
let done = false;
|
|
439
|
+
const rest = [];
|
|
440
|
+
for (let i = 0; i < args.length; i++) {
|
|
441
|
+
const arg = args[i];
|
|
442
|
+
if (arg === "--done") {
|
|
443
|
+
rejectRepeatFlag(done, "--done");
|
|
444
|
+
done = true;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (arg === "--json") {
|
|
448
|
+
rejectRepeatFlag(json, "--json");
|
|
449
|
+
json = true;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (arg === "--show") {
|
|
453
|
+
rejectRepeatFlag(position !== null, "--show");
|
|
454
|
+
position = parsePositionFlag("--show", args[i + 1], "--show 2");
|
|
455
|
+
i++;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (arg === "--help" || arg === "-h")
|
|
459
|
+
continue;
|
|
460
|
+
rest.push(arg);
|
|
461
|
+
}
|
|
462
|
+
rejectUnknownArgs(command, rest, []);
|
|
463
|
+
if (position === null)
|
|
464
|
+
fail(`--show needs a position (usage: tumwater ${command} --show <n>)`);
|
|
465
|
+
if (done) {
|
|
466
|
+
const bug = command === "bug";
|
|
467
|
+
const all = bug ? fixedBugEntries(root) : donePlanEntries(root);
|
|
468
|
+
const entry = all[position - 1];
|
|
469
|
+
if (entry === undefined) {
|
|
470
|
+
const range = all.length > 0 ? ` — valid: 1-${all.length}` : "";
|
|
471
|
+
fail(`no ${bug ? "fixed bug" : "done plan"} at position ${position} (${all.length} in ${bug ? "BUGS.md" : "PLANS.md"})${range}`);
|
|
472
|
+
}
|
|
473
|
+
if (json) {
|
|
474
|
+
const dateRe = bug ? FIXED_DATE_RE : DONE_DATE_RE;
|
|
475
|
+
sayJson({ position, title: entry.title, body: entry.body, completedOn: completionDate(entry.title, dateRe) });
|
|
476
|
+
}
|
|
477
|
+
else
|
|
478
|
+
say(renderShownEntry({ title: entry.title, body: entry.body }));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const shown = showEntry(root, command, position);
|
|
482
|
+
if (json)
|
|
483
|
+
sayJson(shown);
|
|
484
|
+
else
|
|
485
|
+
say(renderShownEntry(shown));
|
|
486
|
+
}
|
|
487
|
+
/** The prose `--show` prints: the title, a blank line, then the body (the title alone when the
|
|
488
|
+
* body is empty). */
|
|
489
|
+
function renderShownEntry(shown) {
|
|
490
|
+
return shown.body === "" ? shown.title : `${shown.title}\n\n${shown.body}`;
|
|
491
|
+
}
|
|
492
|
+
/** The shared shell of the `bug --edit` and `plan --edit` CLI cases, mirroring
|
|
493
|
+
* fileAndAnnounce's prose classification and cancelAndAnnounce's flag handling: `--edit <n>` is
|
|
494
|
+
* the one valued flag, `--json` the one boolean (at most once), `--help`/`-h` ride into the new
|
|
495
|
+
* text, and any other flag-shaped token is refused through rejectUnknownArgs. Unlike cancel,
|
|
496
|
+
* what remains after the flags is the new text the callback folds to one line (so a `## `/`### `
|
|
497
|
+
* line quoted in it can never grow a phantom section). After requireReadyRepo it prints
|
|
498
|
+
* `edited <command> in <file>: <title>` or the `--json` payload {file, title}. Unlike
|
|
499
|
+
* fileAndAnnounce there is no wake: an edit adds no work for a loop to pick up. With
|
|
500
|
+
* `--file <path>` (`-` reads stdin) the new entry comes from a markdown file instead of
|
|
501
|
+
* positionals: `editText` receives its text, and any positional alongside --file is refused.
|
|
502
|
+
* With neither positionals nor --file the entry, rendered as `--show` prints it, opens in
|
|
503
|
+
* $VISUAL/$EDITOR and the saved buffer goes through `editText`; an unchanged buffer writes and
|
|
504
|
+
* commits nothing (`unchanged <command> <n>: <title>`), and an out-of-range position fails
|
|
505
|
+
* before the editor launches. */
|
|
506
|
+
export async function editAndAnnounce(root, args, command, edit, editText) {
|
|
507
|
+
const fileFlag = args.indexOf("--file");
|
|
508
|
+
const path = fileFlag >= 0 ? promptFileValue(args[fileFlag + 1]) : null;
|
|
509
|
+
const rest = path === null ? args : args.filter((_, i) => i !== fileFlag && i !== fileFlag + 1);
|
|
510
|
+
if (path !== null)
|
|
511
|
+
rejectRepeatFlag(rest.includes("--file"), "--file");
|
|
512
|
+
const { json, positionals, position } = scanProseFlags(rest, command, {
|
|
513
|
+
name: "--edit",
|
|
514
|
+
example: "--edit 2",
|
|
515
|
+
});
|
|
516
|
+
if (path !== null)
|
|
517
|
+
rejectStrayWithFile(positionals[0]);
|
|
518
|
+
if (position === null)
|
|
519
|
+
fail(`--edit needs a position (usage: tumwater ${command} --edit <n>)`);
|
|
520
|
+
await requireReadyRepo(root);
|
|
521
|
+
let text = null;
|
|
522
|
+
if (path === null && positionals.length === 0) {
|
|
523
|
+
const shown = showEntry(root, command, position);
|
|
524
|
+
const before = renderShownEntry(shown);
|
|
525
|
+
const after = editInEditor(before, `${command}-${position}.md`);
|
|
526
|
+
if (after.trim() === before.trim()) {
|
|
527
|
+
const file = command === "bug" ? "BUGS.md" : "PLANS.md";
|
|
528
|
+
if (json)
|
|
529
|
+
sayJson({ file, title: shown.title, changed: false });
|
|
530
|
+
else
|
|
531
|
+
say(`unchanged ${command} ${position}: ${shown.title}`);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
text = after;
|
|
535
|
+
}
|
|
536
|
+
else if (path !== null)
|
|
537
|
+
text = readPromptFileArg(path);
|
|
538
|
+
const edited = text === null ? edit(position, positionals) : editText(position, text);
|
|
539
|
+
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
|
|
540
|
+
if (json)
|
|
541
|
+
sayJson({ file: edited.file, title: edited.title });
|
|
542
|
+
else
|
|
543
|
+
say(`edited ${command} in ${edited.file}: ${edited.title}`);
|
|
544
|
+
}
|
|
545
|
+
/** One confirmation for both file commands, prose or `--json`: the JSON branch prints the
|
|
546
|
+
* {file, title, stamp} payload as data, the prose branch one line naming the command, the
|
|
547
|
+
* file, and the entry title. Private to this module — cli.ts reaches it through
|
|
548
|
+
* fileAndAnnounce, the shared shell both file commands' CLI cases run through. */
|
|
549
|
+
function sayFiled(command, filed, json) {
|
|
550
|
+
if (json)
|
|
551
|
+
sayJson({ file: filed.file, title: filed.title, stamp: filed.stamp });
|
|
552
|
+
else
|
|
553
|
+
say(`filed ${command} in ${filed.file}: ${filed.title}`);
|
|
554
|
+
}
|
|
555
|
+
/** How long `tumwater archive` waits for a merge in progress to release the merge lock: a merge
|
|
556
|
+
* holds it through its in-lock check, which can run several minutes. */
|
|
557
|
+
const ARCHIVE_LOCK_WAIT_MS = 15 * 60_000;
|
|
558
|
+
/** `tumwater archive`: move the finished entries still under PLANS.md's `## Done` and BUGS.md's
|
|
559
|
+
* `## Fixed` to PLANS-DONE.md and BUGS-FIXED.md and commit them on the base branch
|
|
560
|
+
* (plans/backlog-archive.md) — the hand-run twin of the archive a starting team makes. Prints
|
|
561
|
+
* what moved and the commit, or why nothing was written (a refusal fails the command). */
|
|
562
|
+
export async function cmdArchive(root) {
|
|
563
|
+
const outcome = await archiveFinishedOnMain(root, { lockTimeoutMs: ARCHIVE_LOCK_WAIT_MS });
|
|
564
|
+
if (outcome.status === "nothing") {
|
|
565
|
+
say("nothing to archive: PLANS.md ## Done and BUGS.md ## Fixed hold no finished entries");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (outcome.status === "skipped")
|
|
569
|
+
fail(`nothing archived: ${outcome.reason}`);
|
|
570
|
+
for (const f of outcome.moved)
|
|
571
|
+
say(`${f.file} → ${f.archive}: ${plural(f.titles.length, "entry", "entries")}`);
|
|
572
|
+
say(`committed ${outcome.commit.slice(0, 8)}`);
|
|
573
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { loadConfigSafe } from "../config/config.js";
|
|
2
|
+
import { withStateLockAsync } from "../concurrency/lock.js";
|
|
3
|
+
import { currentBranch, targetBranch } from "../git/git.js";
|
|
4
|
+
import { COMMIT_IDENT, gitTry } from "../git/git-run.js";
|
|
5
|
+
import { primaryWorktreeLockPath } from "../paths.js";
|
|
6
|
+
/** Commit one operator-written backlog file on main, so the entry reaches the worktrees that
|
|
7
|
+
* branch from main and the primary checkout stays clean for the next fast-forward. Runs under
|
|
8
|
+
* the primary-worktree lock `ffMainTo`'s working-tree arm takes, so the commit and a
|
|
9
|
+
* fast-forward cannot interleave. Returns false, writing nothing, when the primary is not on
|
|
10
|
+
* the base branch (the ref-push arm: the working tree is not main's) or when git fails; it
|
|
11
|
+
* never throws, and the entry stays written either way. `--only` with a pathspec commits just
|
|
12
|
+
* that file's working-tree content and leaves other staged or dirty paths alone. */
|
|
13
|
+
export async function commitOperatorBacklogEdit(root, file, subject) {
|
|
14
|
+
try {
|
|
15
|
+
// Yield first: a synchronous caller (the TUI key handler) may write again right after this
|
|
16
|
+
// call, and that write takes the same lock synchronously — a lock already held across our
|
|
17
|
+
// awaits would block its event loop until the lock times out.
|
|
18
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
19
|
+
return await withStateLockAsync(primaryWorktreeLockPath(root), async () => {
|
|
20
|
+
const primary = await currentBranch(root);
|
|
21
|
+
if (primary === null)
|
|
22
|
+
return false;
|
|
23
|
+
if (targetBranch(loadConfigSafe(root).config?.baseBranch, primary) !== primary)
|
|
24
|
+
return false;
|
|
25
|
+
return (await gitTry(root, ...COMMIT_IDENT, "commit", "--only", "-m", subject, "--", file)) !== null;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|