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
|
@@ -1,41 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* entry and before the next `## ` heading,
|
|
7
|
-
* entry's code fence is body text, never a boundary —
|
|
8
|
-
* reader consults), and a file tumwater init never seeded is
|
|
9
|
-
* rather than failing.
|
|
10
|
-
*
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** The entry-writing half of `tumwater bug` and `tumwater plan` — the operator-authored backlog
|
|
3
|
+
* writes, mirroring the read half in backlog.ts: file a bug into BUGS.md's `## Open` or a plan
|
|
4
|
+
* request into PLANS.md's `## Planned`, stamped the way loops write those entries, and cancel,
|
|
5
|
+
* edit, release, or show an entry by position. The markdown surgery is more than a dispatch case:
|
|
6
|
+
* the entry must be placed after the section's last entry and before the next `## ` heading,
|
|
7
|
+
* fenced-safe (a `## ` line quoted inside an entry's code fence is body text, never a boundary —
|
|
8
|
+
* the same fenceTracker every backlog reader consults), and a file tumwater init never seeded is
|
|
9
|
+
* created with init's template rather than failing. Flag classification, preflights, and the
|
|
10
|
+
* printed confirmations live in backlog-cli.ts; the wake of the loop that should act stays in
|
|
11
|
+
* cli.ts beside the other command bodies. */
|
|
11
12
|
import path from "node:path";
|
|
12
13
|
import { fencedHeadingTitle, fenceTracker, headingMetadata, nextSectionHeading } from "./backlog-md.js";
|
|
13
14
|
import { actionableEntryRanges, stripEntryStamp } from "./backlog-structure.js";
|
|
14
15
|
import { readTextIfExists, writeTextAtomic } from "../files/files.js";
|
|
15
16
|
import { formatDate } from "../text/datetime.js";
|
|
16
|
-
import { fail
|
|
17
|
-
import {
|
|
18
|
-
import { promptFileValue, readPromptFileArg } from "../cli/cli-command-args.js";
|
|
19
|
-
import { plannedPlanEntries, openBugEntries } from "./backlog.js";
|
|
20
|
-
import { backlogHolds, holdMark } from "./backlog-eligibility.js";
|
|
21
|
-
import { numberedList } from "../text/markdown.js";
|
|
17
|
+
import { fail } from "../cli/cli-output.js";
|
|
18
|
+
import { backlogHolds, withoutRefusalNotes, withoutWaitingNotes } from "./backlog-eligibility.js";
|
|
22
19
|
import { collapseWhitespace, trimLeadingBlankLines, trimTrailingBlankLines } from "../text/text.js";
|
|
23
20
|
import { BUGS_TEMPLATE, PLANS_TEMPLATE } from "../init/init.js";
|
|
24
|
-
import { JSON_FLAG, rejectUnknownArgs } from "../cli/cli-flag-specs.js";
|
|
25
|
-
import { requireReadyRepo } from "../cli/cli-repo-gate.js";
|
|
26
|
-
import { submitRolePromptAndWake } from "../operator/operator-intent.js";
|
|
27
|
-
import { promptLengthProblem } from "../inbox/inbox-submit.js";
|
|
28
21
|
import { withStateLock } from "../concurrency/lock.js";
|
|
29
22
|
import { primaryWorktreeLockPath } from "../paths.js";
|
|
30
23
|
/** Today's stamp body every operator-filed entry carries, parenthesized by the caller that
|
|
31
24
|
* builds the heading. Computed once per call (not module load) so a long-lived process
|
|
32
25
|
* stamps with the date it filed on. */
|
|
33
|
-
const operatorStamp = () => `reported by the
|
|
26
|
+
const operatorStamp = () => `reported by the user ${formatDate(new Date())}`;
|
|
27
|
+
/** The kind's open-section markdown file and its `## ` heading: bugs live in BUGS.md's `Open`
|
|
28
|
+
* section, plan requests in PLANS.md's `Planned`. Every position-based writer (cancel, edit,
|
|
29
|
+
* release, show) resolves the same pair. */
|
|
30
|
+
function backlogTarget(kind) {
|
|
31
|
+
return kind === "bug"
|
|
32
|
+
? { fileName: "BUGS.md", sectionTitle: "Open" }
|
|
33
|
+
: { fileName: "PLANS.md", sectionTitle: "Planned" };
|
|
34
|
+
}
|
|
34
35
|
/** The section-append entry point: run the whole read-modify-write under the cross-process
|
|
35
|
-
* primary-worktree lock (paths.ts), which a
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* primary-worktree lock (paths.ts), which a merge's working-tree fast-forward (`ffMainTo`) takes
|
|
37
|
+
* too, so a filed entry and a merged change to the same backlog file cannot each clobber the other.
|
|
38
|
+
* The section is one read plus one atomic rename — short, never held across a build check — and
|
|
39
|
+
* withStateLock creates the state dir the lock lives under on first use. */
|
|
39
40
|
function appendEntry(root, fileName, template, sectionTitle, heading, body) {
|
|
40
41
|
withStateLock(primaryWorktreeLockPath(root), () => appendEntryLocked(root, fileName, template, sectionTitle, heading, body));
|
|
41
42
|
}
|
|
@@ -102,7 +103,7 @@ function appendEntryLocked(root, fileName, template, sectionTitle, heading, body
|
|
|
102
103
|
}
|
|
103
104
|
/** The noun a cancel failure names the section by: the plan example's `no open bug at
|
|
104
105
|
* position N`, and the parallel planned-feature wording for PLANS.md. */
|
|
105
|
-
function sectionNoun(sectionTitle) {
|
|
106
|
+
export function sectionNoun(sectionTitle) {
|
|
106
107
|
if (sectionTitle === "Open")
|
|
107
108
|
return "open bug";
|
|
108
109
|
if (sectionTitle === "Planned")
|
|
@@ -116,7 +117,10 @@ function locateEntry(md, fileName, sectionTitle, n) {
|
|
|
116
117
|
const entry = findEntry(md, sectionTitle, n);
|
|
117
118
|
if (entry === undefined) {
|
|
118
119
|
const count = actionableEntryRanges(md, sectionTitle).length;
|
|
119
|
-
|
|
120
|
+
// Name the valid range when the section is non-empty, so the operator sees the fix in the
|
|
121
|
+
// same line instead of deriving `1..N` from the count; an empty section needs no range.
|
|
122
|
+
const range = count > 0 ? ` — valid: 1-${count}` : "";
|
|
123
|
+
fail(`no ${sectionNoun(sectionTitle)} at position ${n} (${count} in ${fileName})${range}`);
|
|
120
124
|
}
|
|
121
125
|
return entry;
|
|
122
126
|
}
|
|
@@ -136,7 +140,7 @@ function readEntryLines(root, fileName, sectionTitle, n) {
|
|
|
136
140
|
return { file, lines: md.split("\n"), entry };
|
|
137
141
|
}
|
|
138
142
|
/** The section-remove entry point, the appendEntry analogue: the same cross-process lock, so a
|
|
139
|
-
* cancel and a
|
|
143
|
+
* cancel and a merge's working-tree fast-forward cannot each clobber the other's
|
|
140
144
|
* read-modify-write. */
|
|
141
145
|
function removeEntry(root, fileName, sectionTitle, n) {
|
|
142
146
|
return withStateLock(primaryWorktreeLockPath(root), () => removeEntryLocked(root, fileName, sectionTitle, n));
|
|
@@ -166,8 +170,7 @@ function spliceOutEntry(file, lines, entry) {
|
|
|
166
170
|
* decided inside the lock. `expectedTitle` is the heading the caller saw (the status payload's
|
|
167
171
|
* `plans`/`bugs` strings, the same `### ` text as ActionableEntryRange.title). */
|
|
168
172
|
export function tryCancelEntry(root, kind, n, expectedTitle) {
|
|
169
|
-
const fileName = kind
|
|
170
|
-
const sectionTitle = kind === "bug" ? "Open" : "Planned";
|
|
173
|
+
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
171
174
|
return withStateLock(primaryWorktreeLockPath(root), () => {
|
|
172
175
|
const file = path.join(root, fileName);
|
|
173
176
|
const md = readTextIfExists(file) ?? "";
|
|
@@ -180,9 +183,9 @@ export function tryCancelEntry(root, kind, n, expectedTitle) {
|
|
|
180
183
|
return { status: "cancelled", file: fileName, title: entry.title };
|
|
181
184
|
});
|
|
182
185
|
}
|
|
183
|
-
/** The section-edit entry point, the appendEntry/removeEntry analogue: the same
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
+
/** The section-edit entry point, the appendEntry/removeEntry analogue: the same cross-process lock,
|
|
187
|
+
* so an edit and a merge's working-tree fast-forward cannot each clobber the other's
|
|
188
|
+
* read-modify-write. */
|
|
186
189
|
function editEntry(root, fileName, sectionTitle, n, title, body) {
|
|
187
190
|
return withStateLock(primaryWorktreeLockPath(root), () => editEntryLocked(root, fileName, sectionTitle, n, title, body));
|
|
188
191
|
}
|
|
@@ -237,8 +240,7 @@ export function bodyStructureProblem(body) {
|
|
|
237
240
|
* body's line structure kept (no whitespace fold). The caller validates (bodyStructureProblem,
|
|
238
241
|
* non-empty title) first, since fail() exits the process. */
|
|
239
242
|
export function tryEditEntry(root, kind, n, expectedTitle, rawTitle, rawBody) {
|
|
240
|
-
const fileName = kind
|
|
241
|
-
const sectionTitle = kind === "bug" ? "Open" : "Planned";
|
|
243
|
+
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
242
244
|
const title = stripEntryStamp(collapseWhitespace(rawTitle));
|
|
243
245
|
const body = rawBody.replace(/\r\n?/g, "\n").trim();
|
|
244
246
|
return withStateLock(primaryWorktreeLockPath(root), () => {
|
|
@@ -253,113 +255,6 @@ export function tryEditEntry(root, kind, n, expectedTitle, rawTitle, rawBody) {
|
|
|
253
255
|
return { status: "edited", file: fileName, title };
|
|
254
256
|
});
|
|
255
257
|
}
|
|
256
|
-
/** The title each file command derives from the CLI positionals, exported so the CLI case
|
|
257
|
-
* can hand fileAndAnnounce the same derivation the write will carry: fileAndAnnounce
|
|
258
|
-
* preflights the composed wake prompt (wakeText(title)) against the submission cap BEFORE
|
|
259
|
-
* the write, and a titleOf that drifted from file()'s own derivation would preflight the
|
|
260
|
-
* wrong text. bugTitleOf(positionals) is exactly fileBug's symptom when the CLI case passes
|
|
261
|
-
* positionals.join(" ") as its raw text; planTitleOf(positionals) is exactly filePlan's
|
|
262
|
-
* title when it passes positionals[0]. */
|
|
263
|
-
export function bugTitleOf(positionals) {
|
|
264
|
-
return collapseWhitespace(positionals.join(" "));
|
|
265
|
-
}
|
|
266
|
-
export function planTitleOf(positionals) {
|
|
267
|
-
return collapseWhitespace(positionals[0] ?? "");
|
|
268
|
-
}
|
|
269
|
-
/** The one home of the prose-carrying file commands' flag classification (the `bug`/`plan`
|
|
270
|
-
* file forms and their `--edit` forms): `--json` is the one boolean, refused on a repeat
|
|
271
|
-
* (the "each flag at most once" rule every spec-parsed command and the questions parser
|
|
272
|
-
* apply); when a position flag is supplied it is the one valued flag, likewise refused on a
|
|
273
|
-
* repeat and parsed through the shared plain-decimal rule (parsePositionFlag); `--help`/`-h`
|
|
274
|
-
* rides into the prose positionals rather than being skipped, because the dispatcher's help
|
|
275
|
-
* interception stands down for text-carrying invocations (cli.ts) so a help token here is
|
|
276
|
-
* always content; every other flag-shaped token is named by rejectUnknownArgs, and every
|
|
277
|
-
* bare token is prose. Tokens keep their original order. */
|
|
278
|
-
function scanProseFlags(args, command, positionFlag) {
|
|
279
|
-
let json = false;
|
|
280
|
-
let position = null;
|
|
281
|
-
const positionals = [];
|
|
282
|
-
for (let i = 0; i < args.length; i++) {
|
|
283
|
-
const arg = args[i] ?? "";
|
|
284
|
-
if (arg === "--json") {
|
|
285
|
-
rejectRepeatFlag(json, "--json");
|
|
286
|
-
json = true;
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
if (arg === positionFlag?.name) {
|
|
290
|
-
rejectRepeatFlag(position !== null, positionFlag.name);
|
|
291
|
-
position = parsePositionFlag(positionFlag.name, args[i + 1], positionFlag.example);
|
|
292
|
-
i++;
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
if (arg === "--help" || arg === "-h") {
|
|
296
|
-
positionals.push(arg);
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
if (arg.startsWith("-"))
|
|
300
|
-
rejectUnknownArgs(command, [arg], [JSON_FLAG]);
|
|
301
|
-
else
|
|
302
|
-
positionals.push(arg);
|
|
303
|
-
}
|
|
304
|
-
return { json, positionals, position };
|
|
305
|
-
}
|
|
306
|
-
/** The shared shell of the `bug` and `plan` CLI cases — the one home of the five-step
|
|
307
|
-
* sequence both commands run in cli.ts: peel the free-form positionals (the questions
|
|
308
|
-
* command's prose-token pattern), reject anything flag-shaped that is not `--json`, run the
|
|
309
|
-
* ready-repo gate the other backlog commands run, write the entry via `file`, wake the
|
|
310
|
-
* `role` loop that should act so it picks the entry up within one poll, then announce — the
|
|
311
|
-
* wake always fires, its confirmation line rides the prose output only, in `--json` mode
|
|
312
|
-
* stdout is the payload document alone (the prompt --json precedent) so a script can parse
|
|
313
|
-
* it. The command name and usage string ride in for the reject/confirmation/empty-arg
|
|
314
|
-
* wordings (fileBug/filePlan's empty-arg usage lines); `wakeText` builds the loop prompt
|
|
315
|
-
* from the filed entry's title and `titleOf` derives that title from the positionals (the
|
|
316
|
-
* exported bugTitleOf/planTitleOf, so the preflight and the write see one derivation).
|
|
317
|
-
* The wake prompt rides into the target tick's prefill under the same submission cap every
|
|
318
|
-
* prompt answers to, so fileAndAnnounce checks it (promptLengthProblem) BEFORE calling
|
|
319
|
-
* `file` — otherwise an over-long operator text would write its entry and only then throw
|
|
320
|
-
* inside submitRolePromptAndWake, half-applying the command with the backlog file already
|
|
321
|
-
* grown. With `--file <path>` (`-` reads stdin) the entry comes from a markdown file instead
|
|
322
|
-
* of positionals: `fileText` receives its text (splitEntryFile: first line the title, the rest
|
|
323
|
-
* the body kept line for line), the same preflight runs on that title, and any positional
|
|
324
|
-
* alongside --file is refused. */
|
|
325
|
-
export async function fileAndAnnounce(root, args, command, file, role, wakeText, titleOf, fileText) {
|
|
326
|
-
const fileFlag = args.indexOf("--file");
|
|
327
|
-
if (fileFlag >= 0) {
|
|
328
|
-
const file = promptFileValue(args[fileFlag + 1]);
|
|
329
|
-
const rest = args.filter((_, i) => i !== fileFlag && i !== fileFlag + 1);
|
|
330
|
-
rejectRepeatFlag(rest.includes("--file"), "--file");
|
|
331
|
-
const { json, positionals } = scanProseFlags(rest, command);
|
|
332
|
-
const stray = positionals[0];
|
|
333
|
-
if (stray !== undefined)
|
|
334
|
-
fail(`unexpected argument ${JSON.stringify(stray)} — with --file the entry comes from the file`);
|
|
335
|
-
await requireReadyRepo(root);
|
|
336
|
-
const text = readPromptFileArg(file);
|
|
337
|
-
const wakeProblem = promptLengthProblem(wakeText(splitEntryFile(text).title), role);
|
|
338
|
-
if (wakeProblem)
|
|
339
|
-
fail(wakeProblem);
|
|
340
|
-
announceFiled(root, command, fileText(text), role, wakeText, json);
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
// peelPositionals cannot serve this command: bug/plan's text is operator prose, and a
|
|
344
|
-
// standalone --help/-h token inside it ("the TUI mishandles --help output") is content —
|
|
345
|
-
// the dispatcher's help interception stands down for text-carrying invocations (cli.ts),
|
|
346
|
-
// so a help token that reaches here is always prose, and scanProseFlags keeps it in the
|
|
347
|
-
// positionals rather than flag-shaping it into rest for rejectUnknownArgs to refuse.
|
|
348
|
-
const { json, positionals } = scanProseFlags(args, command);
|
|
349
|
-
await requireReadyRepo(root);
|
|
350
|
-
const wakeProblem = promptLengthProblem(wakeText(titleOf(positionals)), role);
|
|
351
|
-
if (wakeProblem)
|
|
352
|
-
fail(wakeProblem);
|
|
353
|
-
announceFiled(root, command, file(positionals), role, wakeText, json);
|
|
354
|
-
}
|
|
355
|
-
/** The tail both fileAndAnnounce forms share: wake the `role` loop with the filed entry's
|
|
356
|
-
* title, then print the confirmation (the wake line rides the prose output only). */
|
|
357
|
-
function announceFiled(root, command, filed, role, wakeText, json) {
|
|
358
|
-
const wake = submitRolePromptAndWake(root, role, wakeText(filed.title));
|
|
359
|
-
sayFiled(command, filed, json);
|
|
360
|
-
if (!json)
|
|
361
|
-
say(wake);
|
|
362
|
-
}
|
|
363
258
|
/** Split a markdown entry file into its title and body: the title is the first non-blank line
|
|
364
259
|
* (a leading `#`-marker stripped, folded through collapseWhitespace), the body every later
|
|
365
260
|
* line kept as written with trailing whitespace stripped and edge blank lines dropped. */
|
|
@@ -379,6 +274,21 @@ export function normalizeEntryBody(body) {
|
|
|
379
274
|
trimTrailingBlankLines(lines);
|
|
380
275
|
return lines.join("\n");
|
|
381
276
|
}
|
|
277
|
+
/** Fail with the kind's empty-field usage line when the title/symptom is empty: the plan
|
|
278
|
+
* form names its missing field a "title", the bug form a "symptom". The single home of those
|
|
279
|
+
* two strings, shared by the inline, `--file`, and `--edit` forms so the CLI and dashboard
|
|
280
|
+
* can't drift. */
|
|
281
|
+
function requireEntryTitle(kind, title, usage) {
|
|
282
|
+
if (title === "")
|
|
283
|
+
fail(kind === "plan" ? `plan needs a title: ${usage}` : `bug needs a symptom: ${usage}`);
|
|
284
|
+
}
|
|
285
|
+
/** Fail when a split `--file` body would grow a phantom section (bodyStructureProblem). verb is
|
|
286
|
+
* the CLI verb in the message: "filed" for `--file`, "written" for `--edit <n> --file`. */
|
|
287
|
+
function requireBodyStructurallySound(body, verb) {
|
|
288
|
+
const problem = bodyStructureProblem(body);
|
|
289
|
+
if (problem)
|
|
290
|
+
fail(`the --file body cannot be ${verb}: ${problem}`);
|
|
291
|
+
}
|
|
382
292
|
/** File an already-validated title and body under PLANS.md's `## Planned` (plan) or BUGS.md's
|
|
383
293
|
* `## Open` (bug). Shared by the `--file` forms and the dashboard file endpoint; the caller has
|
|
384
294
|
* checked the title is non-empty and the body passes bodyStructureProblem. */
|
|
@@ -388,35 +298,32 @@ export function fileEntryWithBody(root, kind, title, body) {
|
|
|
388
298
|
? appendOperatorEntry(root, "PLANS.md", PLANS_TEMPLATE, "Planned", t, body)
|
|
389
299
|
: appendOperatorEntry(root, "BUGS.md", BUGS_TEMPLATE, "Open", t, body);
|
|
390
300
|
}
|
|
301
|
+
/** The `--file` filing form shared by plan and bug: split the file's title and body, require a
|
|
302
|
+
* title and a structurally-sound body, and file under the kind's section (fileEntryWithBody).
|
|
303
|
+
* filePlanFromText and fileBugFromText are the public plan/bug twins routing through it. */
|
|
304
|
+
function fileFromText(root, kind, text, usage) {
|
|
305
|
+
const { title, body } = splitEntryFile(text);
|
|
306
|
+
requireEntryTitle(kind, title, usage);
|
|
307
|
+
requireBodyStructurallySound(body, "filed");
|
|
308
|
+
return fileEntryWithBody(root, kind, title, body);
|
|
309
|
+
}
|
|
391
310
|
/** `tumwater plan --file`: file the markdown file's title and line-for-line body under
|
|
392
311
|
* PLANS.md's `## Planned`. A body that would grow a phantom section writes nothing. */
|
|
393
312
|
export function filePlanFromText(root, text, usage) {
|
|
394
|
-
|
|
395
|
-
if (title === "")
|
|
396
|
-
fail(`plan needs a title: ${usage}`);
|
|
397
|
-
const problem = bodyStructureProblem(body);
|
|
398
|
-
if (problem)
|
|
399
|
-
fail(`the --file body cannot be filed: ${problem}`);
|
|
400
|
-
return fileEntryWithBody(root, "plan", title, body);
|
|
313
|
+
return fileFromText(root, "plan", text, usage);
|
|
401
314
|
}
|
|
402
315
|
/** `tumwater bug --file`: the bug twin of filePlanFromText, under BUGS.md's `## Open`. */
|
|
403
316
|
export function fileBugFromText(root, text, usage) {
|
|
404
|
-
|
|
405
|
-
if (title === "")
|
|
406
|
-
fail(`bug needs a symptom: ${usage}`);
|
|
407
|
-
const problem = bodyStructureProblem(body);
|
|
408
|
-
if (problem)
|
|
409
|
-
fail(`the --file body cannot be filed: ${problem}`);
|
|
410
|
-
return fileEntryWithBody(root, "bug", title, body);
|
|
317
|
+
return fileFromText(root, "bug", text, usage);
|
|
411
318
|
}
|
|
412
319
|
/** The loop prompt a filed bug wakes the `bugfix` loop with — one wording for the CLI's
|
|
413
320
|
* `tumwater bug` and the dashboard's backlog-file endpoint. */
|
|
414
321
|
export function bugWakeText(title) {
|
|
415
|
-
return `
|
|
322
|
+
return `The user filed a new bug with \`tumwater bug\`: "${title}" — it is under BUGS.md ## Open; fix it.`;
|
|
416
323
|
}
|
|
417
324
|
/** The loop prompt a requested plan wakes the `feature` loop with, shared like bugWakeText. */
|
|
418
325
|
export function planWakeText(title) {
|
|
419
|
-
return `
|
|
326
|
+
return `The user requested a plan with \`tumwater plan\`: "${title}" — it is under PLANS.md ## Planned; flesh out the plan stub.`;
|
|
420
327
|
}
|
|
421
328
|
/** The loop each operator-filed kind wakes and the wording it wakes it with, keyed by the
|
|
422
329
|
* kind the dashboard posts. */
|
|
@@ -434,26 +341,24 @@ function appendOperatorEntry(root, fileName, template, sectionTitle, title, body
|
|
|
434
341
|
appendEntry(root, fileName, template, sectionTitle, `### ${title} (${stamp})`, body);
|
|
435
342
|
return { file: fileName, title, stamp };
|
|
436
343
|
}
|
|
437
|
-
/** `tumwater bug "<symptom>"`: append one `### <symptom> (reported by the
|
|
344
|
+
/** `tumwater bug "<symptom>"`: append one `### <symptom> (reported by the user <date>)`
|
|
438
345
|
* entry to BUGS.md's `## Open`. The symptom is operator prose, so it folds to one line
|
|
439
346
|
* (collapseWhitespace, the questions answer precedent) — a multi-line raw text could carry
|
|
440
347
|
* a `## `/`### ` line and grow a phantom section no reader wrote. An empty symptom fails
|
|
441
348
|
* with the usage line and writes nothing. */
|
|
442
349
|
export function fileBug(root, rawText, usage) {
|
|
443
350
|
const symptom = collapseWhitespace(rawText);
|
|
444
|
-
|
|
445
|
-
fail(`bug needs a symptom: ${usage}`);
|
|
351
|
+
requireEntryTitle("bug", symptom, usage);
|
|
446
352
|
return appendOperatorEntry(root, "BUGS.md", BUGS_TEMPLATE, "Open", symptom, "");
|
|
447
353
|
}
|
|
448
|
-
/** `tumwater plan "<title>" [body...]`: append one `### <title> (reported by the
|
|
354
|
+
/** `tumwater plan "<title>" [body...]`: append one `### <title> (reported by the user
|
|
449
355
|
* <date>)` entry to PLANS.md's `## Planned`, with the optional body as the entry text. No
|
|
450
356
|
* goal/approach/acceptance-criteria scaffolding is invented — the feature loop's
|
|
451
357
|
* plan-refinement pass fleshes the stub out on pickup. An empty title fails with the usage
|
|
452
358
|
* line and writes nothing. */
|
|
453
359
|
export function filePlan(root, rawTitle, rawBody, usage) {
|
|
454
360
|
const title = collapseWhitespace(rawTitle);
|
|
455
|
-
|
|
456
|
-
fail(`plan needs a title: ${usage}`);
|
|
361
|
+
requireEntryTitle("plan", title, usage);
|
|
457
362
|
return appendOperatorEntry(root, "PLANS.md", PLANS_TEMPLATE, "Planned", title, collapseWhitespace(rawBody));
|
|
458
363
|
}
|
|
459
364
|
/** `tumwater bug --cancel <n>`: remove the nth entry from BUGS.md's `## Open`, as
|
|
@@ -466,28 +371,29 @@ export function cancelBug(root, n) {
|
|
|
466
371
|
export function cancelPlan(root, n) {
|
|
467
372
|
return removeEntry(root, "PLANS.md", "Planned", n);
|
|
468
373
|
}
|
|
374
|
+
/** The `--edit <n> --file` form shared by plan and bug: split the file, strip a pasted-back
|
|
375
|
+
* heading's own stamp, and require a title and structurally-sound body before splicing into the
|
|
376
|
+
* kind's section (editEntry). editPlanFromText and editBugFromText are the public plan/bug twins
|
|
377
|
+
* routing through it, so the two cannot drift. */
|
|
378
|
+
function editFromText(root, kind, n, text, usage) {
|
|
379
|
+
const { title: rawTitle, body } = splitEntryFile(text);
|
|
380
|
+
// A first line pasted back as the full heading carries the entry's stamp; the splice re-adds it.
|
|
381
|
+
const title = stripEntryStamp(rawTitle);
|
|
382
|
+
requireEntryTitle(kind, title, usage);
|
|
383
|
+
requireBodyStructurallySound(body, "written");
|
|
384
|
+
const target = backlogTarget(kind);
|
|
385
|
+
return editEntry(root, target.fileName, target.sectionTitle, n, title, body);
|
|
386
|
+
}
|
|
469
387
|
/** `tumwater plan --edit <n> --file`: rewrite the nth `## Planned` entry from a markdown file,
|
|
470
388
|
* title and body kept line for line (splitEntryFile), position and original stamp kept. A body
|
|
471
389
|
* that would grow a phantom section writes nothing. */
|
|
472
390
|
export function editPlanFromText(root, n, text, usage) {
|
|
473
|
-
|
|
474
|
-
if (title === "")
|
|
475
|
-
fail(`plan needs a title: ${usage}`);
|
|
476
|
-
const problem = bodyStructureProblem(body);
|
|
477
|
-
if (problem)
|
|
478
|
-
fail(`the --file body cannot be written: ${problem}`);
|
|
479
|
-
return editEntry(root, "PLANS.md", "Planned", n, title, body);
|
|
391
|
+
return editFromText(root, "plan", n, text, usage);
|
|
480
392
|
}
|
|
481
393
|
/** `tumwater bug --edit <n> --file`: the bug twin of editPlanFromText over `## Open`. The body is
|
|
482
394
|
* written as the file gives it, so a bug filed with `--file` can be revised with its body. */
|
|
483
395
|
export function editBugFromText(root, n, text, usage) {
|
|
484
|
-
|
|
485
|
-
if (title === "")
|
|
486
|
-
fail(`bug needs a symptom: ${usage}`);
|
|
487
|
-
const problem = bodyStructureProblem(body);
|
|
488
|
-
if (problem)
|
|
489
|
-
fail(`the --file body cannot be written: ${problem}`);
|
|
490
|
-
return editEntry(root, "BUGS.md", "Open", n, title, body);
|
|
396
|
+
return editFromText(root, "bug", n, text, usage);
|
|
491
397
|
}
|
|
492
398
|
/** `tumwater bug --edit <n> "<symptom>"`: replace the nth BUGS.md `## Open` entry's heading
|
|
493
399
|
* text with the new symptom, keeping its position and its original `(reported by …)` stamp.
|
|
@@ -496,8 +402,7 @@ export function editBugFromText(root, n, text, usage) {
|
|
|
496
402
|
* body, so the rebuilt entry is its heading alone. */
|
|
497
403
|
export function editBug(root, n, rawText, usage) {
|
|
498
404
|
const symptom = collapseWhitespace(rawText);
|
|
499
|
-
|
|
500
|
-
fail(`bug needs a symptom: ${usage}`);
|
|
405
|
+
requireEntryTitle("bug", symptom, usage);
|
|
501
406
|
return editEntry(root, "BUGS.md", "Open", n, symptom, "");
|
|
502
407
|
}
|
|
503
408
|
/** `tumwater plan --edit <n> "<title>" [body...]`: replace the nth PLANS.md `## Planned`
|
|
@@ -506,148 +411,121 @@ export function editBug(root, n, rawText, usage) {
|
|
|
506
411
|
* phantom section; an empty title fails with the usage line and writes nothing. */
|
|
507
412
|
export function editPlan(root, n, rawTitle, rawBody, usage) {
|
|
508
413
|
const title = collapseWhitespace(rawTitle);
|
|
509
|
-
|
|
510
|
-
fail(`plan needs a title: ${usage}`);
|
|
414
|
+
requireEntryTitle("plan", title, usage);
|
|
511
415
|
const body = collapseWhitespace(rawBody);
|
|
512
416
|
return editEntry(root, "PLANS.md", "Planned", n, title, body);
|
|
513
417
|
}
|
|
514
|
-
/**
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
rejectUnknownArgs(command, rest, []);
|
|
546
|
-
if (position === null)
|
|
547
|
-
fail(`--cancel needs a position (usage: tumwater ${command} --cancel <n>)`);
|
|
548
|
-
await requireReadyRepo(root);
|
|
549
|
-
const removed = cancel(position);
|
|
550
|
-
if (json)
|
|
551
|
-
sayJson({ file: removed.file, title: removed.title });
|
|
552
|
-
else
|
|
553
|
-
say(`cancelled ${command} in ${removed.file}: ${removed.title}`);
|
|
554
|
-
}
|
|
555
|
-
/** The shared shell of the `bug --list` and `plan --list` CLI cases — the read half of the
|
|
556
|
-
* operator queue whose `--edit`/`--cancel` flags address entries by position. Reads the same
|
|
557
|
-
* entries, in the same file order, that locateEntry numbers for `--edit`/`--cancel`, pairs
|
|
558
|
-
* each with its eligibility hold (backlogHolds), and prints them numbered from 1, or the
|
|
559
|
-
* command's `no …` line on an empty or missing file. Like `tumwater backlog` and `tumwater
|
|
560
|
-
* questions` it is read-only: no requireReadyRepo gate and no wake. `--list` and `--json` are
|
|
561
|
-
* the two booleans this shell consumes (repeats refused), `--help`/`-h` is tolerated, and any
|
|
562
|
-
* positional or other flag is refused through rejectUnknownArgs. */
|
|
563
|
-
export function listAndAnnounce(root, args, command, fileName, sectionTitle) {
|
|
564
|
-
let json = false;
|
|
565
|
-
let list = false;
|
|
566
|
-
const rest = [];
|
|
567
|
-
for (const arg of args) {
|
|
568
|
-
if (arg === "--list") {
|
|
569
|
-
rejectRepeatFlag(list, "--list");
|
|
570
|
-
list = true;
|
|
571
|
-
continue;
|
|
572
|
-
}
|
|
573
|
-
if (arg === "--json") {
|
|
574
|
-
rejectRepeatFlag(json, "--json");
|
|
575
|
-
json = true;
|
|
576
|
-
continue;
|
|
577
|
-
}
|
|
578
|
-
if (arg === "--help" || arg === "-h")
|
|
579
|
-
continue;
|
|
580
|
-
rest.push(arg);
|
|
581
|
-
}
|
|
582
|
-
// rejectUnknownArgs names a stray positional and an unknown flag with this command's (empty)
|
|
583
|
-
// valid-flag list: the vocabulary a list invocation accepts is exactly --list and --json,
|
|
584
|
-
// both consumed above.
|
|
585
|
-
rejectUnknownArgs(command, rest, []);
|
|
586
|
-
// BUGS.md's `## Open` is the one section whose noun is "bug"; every other caller is a plan.
|
|
587
|
-
const bugSection = sectionTitle === "Open";
|
|
588
|
-
const entries = bugSection ? openBugEntries(root) : plannedPlanEntries(root);
|
|
589
|
-
const holds = backlogHolds(root);
|
|
590
|
-
const entryHolds = bugSection ? holds.bugs : holds.plans;
|
|
591
|
-
if (json) {
|
|
592
|
-
sayJson({
|
|
593
|
-
entries: entries.map((entry, i) => ({
|
|
594
|
-
position: i + 1,
|
|
595
|
-
title: entry.title,
|
|
596
|
-
body: entry.body,
|
|
597
|
-
hold: entryHolds[i] ?? null,
|
|
598
|
-
})),
|
|
599
|
-
});
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
const noun = sectionNoun(sectionTitle);
|
|
603
|
-
if (entries.length === 0) {
|
|
604
|
-
say(`no ${noun}s`);
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
const numbered = entries.map((entry, i) => `${entry.title}${holdMark(entryHolds[i] ?? null)}`);
|
|
608
|
-
say(`${noun}s in ${fileName}:\n${numberedList(numbered)}`);
|
|
609
|
-
}
|
|
610
|
-
/** The shared shell of the `bug --edit` and `plan --edit` CLI cases, mirroring
|
|
611
|
-
* fileAndAnnounce's prose classification and cancelAndAnnounce's flag handling: `--edit <n>` is
|
|
612
|
-
* the one valued flag, `--json` the one boolean (at most once), `--help`/`-h` ride into the new
|
|
613
|
-
* text, and any other flag-shaped token is refused through rejectUnknownArgs. Unlike cancel,
|
|
614
|
-
* what remains after the flags is the new text the callback folds to one line (so a `## `/`### `
|
|
615
|
-
* line quoted in it can never grow a phantom section). After requireReadyRepo it prints
|
|
616
|
-
* `edited <command> in <file>: <title>` or the `--json` payload {file, title}. Unlike
|
|
617
|
-
* fileAndAnnounce there is no wake: an edit adds no work for a loop to pick up. With
|
|
618
|
-
* `--file <path>` (`-` reads stdin) the new entry comes from a markdown file instead of
|
|
619
|
-
* positionals: `editText` receives its text, and any positional alongside --file is refused. */
|
|
620
|
-
export async function editAndAnnounce(root, args, command, edit, editText) {
|
|
621
|
-
const fileFlag = args.indexOf("--file");
|
|
622
|
-
const path = fileFlag >= 0 ? promptFileValue(args[fileFlag + 1]) : null;
|
|
623
|
-
const rest = path === null ? args : args.filter((_, i) => i !== fileFlag && i !== fileFlag + 1);
|
|
624
|
-
if (path !== null)
|
|
625
|
-
rejectRepeatFlag(rest.includes("--file"), "--file");
|
|
626
|
-
const { json, positionals, position } = scanProseFlags(rest, command, {
|
|
627
|
-
name: "--edit",
|
|
628
|
-
example: "--edit 2",
|
|
418
|
+
/** Drop the Refused note from `entry`'s slice of `lines`, in place; an entry with none loses its
|
|
419
|
+
* Waiting-for-the-next-failure note instead. False (and `lines` untouched) when the entry
|
|
420
|
+
* carries neither; shared by releaseEntry and tryReleaseEntry so they cannot
|
|
421
|
+
* drift. */
|
|
422
|
+
function liftRefusal(lines, entry) {
|
|
423
|
+
const slice = lines.slice(entry.start - 1, entry.end);
|
|
424
|
+
let kept = withoutRefusalNotes(slice);
|
|
425
|
+
if (kept.length === slice.length)
|
|
426
|
+
kept = withoutWaitingNotes(slice);
|
|
427
|
+
if (kept.length === slice.length)
|
|
428
|
+
return false;
|
|
429
|
+
lines.splice(entry.start - 1, slice.length, ...kept);
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
/** The non-exiting release the dashboard calls: releaseEntry's splice with tryCancelEntry's
|
|
433
|
+
* title check, all decided inside the lock. */
|
|
434
|
+
export function tryReleaseEntry(root, kind, n, expectedTitle) {
|
|
435
|
+
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
436
|
+
return withStateLock(primaryWorktreeLockPath(root), () => {
|
|
437
|
+
const file = path.join(root, fileName);
|
|
438
|
+
const md = readTextIfExists(file) ?? "";
|
|
439
|
+
const entry = findEntry(md, sectionTitle, n);
|
|
440
|
+
if (entry === undefined)
|
|
441
|
+
return { status: "gone" };
|
|
442
|
+
if (entry.title !== expectedTitle)
|
|
443
|
+
return { status: "moved", title: entry.title };
|
|
444
|
+
const lines = md.split("\n");
|
|
445
|
+
if (!liftRefusal(lines, entry))
|
|
446
|
+
return { status: "not_refused", title: entry.title };
|
|
447
|
+
writeTextAtomic(file, lines.join("\n"));
|
|
448
|
+
return { status: "released", file: fileName, title: entry.title };
|
|
629
449
|
});
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
450
|
+
}
|
|
451
|
+
/** Lift the Refused or Waiting for the next failure note from the
|
|
452
|
+
* `n`th entry (1-based, as `--list` numbers them) of the kind's open section and write the
|
|
453
|
+
* file, under the same lock the cancel and edit writers hold.
|
|
454
|
+
* An out-of-range position fails with locateEntry's wording; an entry with neither note
|
|
455
|
+
* fails too, writing nothing. */
|
|
456
|
+
export function releaseEntry(root, kind, n) {
|
|
457
|
+
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
458
|
+
return withStateLock(primaryWorktreeLockPath(root), () => {
|
|
459
|
+
const { file, lines, entry } = readEntryLines(root, fileName, sectionTitle, n);
|
|
460
|
+
if (!liftRefusal(lines, entry))
|
|
461
|
+
fail(`${kind} #${n} "${entry.title}" carries no Refused or Waiting for the next failure note`);
|
|
462
|
+
writeTextAtomic(file, lines.join("\n"));
|
|
463
|
+
return { file: fileName, title: entry.title };
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/** Move the `n`th entry (1-based, as `--list` numbers them) of the kind's open section to the
|
|
467
|
+
* head of that section and write the file, under the same lock as the other writers. Position
|
|
468
|
+
* 1 fails ("is already first") and an out-of-range one fails with locateEntry's wording, both
|
|
469
|
+
* writing nothing. The entry's lines move byte for byte; only the blank lines around the cut
|
|
470
|
+
* and the splice are normalized, and an entry that was the file's last content leaves the file
|
|
471
|
+
* ending as it did before the move (a trailing newline iff it had one). */
|
|
472
|
+
export function moveEntryToTop(root, kind, n) {
|
|
473
|
+
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
474
|
+
return withStateLock(primaryWorktreeLockPath(root), () => {
|
|
475
|
+
const { file, lines, entry } = readEntryLines(root, fileName, sectionTitle, n);
|
|
476
|
+
if (n === 1)
|
|
477
|
+
fail(`${kind} #1 "${entry.title}" is already first`);
|
|
478
|
+
spliceEntryToTop(lines, sectionTitle, entry);
|
|
479
|
+
writeTextAtomic(file, lines.join("\n"));
|
|
480
|
+
return { file: fileName, title: entry.title };
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
/** Cut `entry` out of `lines` and put it at the head of the section, in place; shared by
|
|
484
|
+
* moveEntryToTop and tryMoveEntryToTop so their bytes cannot drift. */
|
|
485
|
+
function spliceEntryToTop(lines, sectionTitle, entry) {
|
|
486
|
+
const first = findEntry(lines.join("\n"), sectionTitle, 1);
|
|
487
|
+
const endsWithNewline = lines[lines.length - 1] === "";
|
|
488
|
+
const atEof = lines.slice(entry.end).every((l) => l.trim() === "");
|
|
489
|
+
const cut = lines.slice(entry.start - 1, entry.end);
|
|
490
|
+
trimTrailingBlankLines(cut);
|
|
491
|
+
lines.splice(entry.start - 1, entry.end - entry.start + 1);
|
|
492
|
+
if (atEof) {
|
|
493
|
+
trimTrailingBlankLines(lines);
|
|
494
|
+
if (endsWithNewline)
|
|
495
|
+
lines.push("");
|
|
634
496
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
497
|
+
lines.splice(first.start - 1, 0, ...cut, "");
|
|
498
|
+
}
|
|
499
|
+
/** The non-exiting reorder the dashboard calls: moveEntryToTop's splice with tryReleaseEntry's
|
|
500
|
+
* title check, all decided inside the lock. */
|
|
501
|
+
export function tryMoveEntryToTop(root, kind, n, expectedTitle) {
|
|
502
|
+
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
503
|
+
return withStateLock(primaryWorktreeLockPath(root), () => {
|
|
504
|
+
const file = path.join(root, fileName);
|
|
505
|
+
const md = readTextIfExists(file) ?? "";
|
|
506
|
+
const entry = findEntry(md, sectionTitle, n);
|
|
507
|
+
if (entry === undefined)
|
|
508
|
+
return { status: "gone" };
|
|
509
|
+
if (entry.title !== expectedTitle)
|
|
510
|
+
return { status: "moved", title: entry.title };
|
|
511
|
+
if (n === 1)
|
|
512
|
+
return { status: "already_first", title: entry.title };
|
|
513
|
+
const lines = md.split("\n");
|
|
514
|
+
spliceEntryToTop(lines, sectionTitle, entry);
|
|
515
|
+
writeTextAtomic(file, lines.join("\n"));
|
|
516
|
+
return { status: "moved_to_top", file: fileName, title: entry.title };
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
/** The nth entry as the markdown file `--edit <n> --file` reads back: the stamp-stripped title
|
|
520
|
+
* and the body with its indentation kept (read from the raw lines, not the trimmed
|
|
521
|
+
* BacklogEntry.body), plus the entry's eligibility hold. Read-only. */
|
|
522
|
+
export function showEntry(root, kind, n) {
|
|
523
|
+
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
524
|
+
const { lines, entry } = readEntryLines(root, fileName, sectionTitle, n);
|
|
525
|
+
const meta = headingMetadata(lines, entry.start - 1);
|
|
526
|
+
const title = collapseWhitespace(stripEntryStamp(meta.text.replace(/^### /, "")));
|
|
527
|
+
const body = normalizeEntryBody(lines.slice(meta.next, entry.end).join("\n"));
|
|
528
|
+
const holds = backlogHolds(root);
|
|
529
|
+
const hold = (kind === "bug" ? holds.bugs : holds.plans)[n - 1] ?? null;
|
|
530
|
+
return { position: n, title, body, hold };
|
|
653
531
|
}
|