tumwater 0.1.0 → 0.1.1
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 +57 -16
- package/dist/build-info.json +2 -2
- package/dist/src/backlog/backlog-eligibility.js +141 -0
- package/dist/src/{backlog.js → backlog/backlog-md.js} +56 -71
- package/dist/src/{ui/backlog-report.js → backlog/backlog-render.js} +3 -2
- package/dist/src/backlog/backlog-structure.js +350 -0
- package/dist/src/backlog/backlog-write.js +204 -0
- package/dist/src/backlog/backlog.js +99 -0
- package/dist/src/{main-baseline.js → baseline/main-baseline.js} +53 -28
- package/dist/src/{main-red.js → baseline/main-red.js} +50 -39
- package/dist/src/{readme.js → brief.js} +16 -6
- package/dist/src/{budget.js → budget/budget.js} +62 -28
- package/dist/src/{fallback-breaker.js → budget/fallback-breaker.js} +60 -10
- package/dist/src/{build-check-counts.js → build/build-check-counts.js} +16 -6
- package/dist/src/{build-check-detect.js → build/build-check-detect.js} +28 -44
- package/dist/src/{build-check-events.js → build/build-check-events.js} +23 -9
- package/dist/src/{build-check-report.js → build/build-check-report.js} +9 -9
- package/dist/src/{build-check-scoped.js → build/build-check-scoped.js} +15 -14
- package/dist/src/{build-check.js → build/build-check.js} +7 -5
- package/dist/src/{build-info.js → build/build-info.js} +12 -6
- package/dist/src/{build-stage.js → build/build-stage.js} +65 -29
- package/dist/src/{dep-install.js → build/dep-install.js} +9 -6
- package/dist/src/{host-sleep.js → build/host-sleep.js} +4 -4
- package/dist/src/{change-data.js → change/change-data.js} +21 -18
- package/dist/src/{ui/change-preview.js → change/change-render.js} +14 -7
- package/dist/src/{cli-args.js → cli/cli-args.js} +85 -37
- package/dist/src/{cli-command-args.js → cli/cli-command-args.js} +148 -58
- package/dist/src/cli/cli-flag-specs.js +309 -0
- package/dist/src/cli/cli-marker-commands.js +43 -0
- package/dist/src/cli/cli-output.js +63 -0
- package/dist/src/cli/cli-query-commands.js +186 -0
- package/dist/src/{cli-run.js → cli/cli-run.js} +156 -44
- package/dist/src/cli/config-commands.js +97 -0
- package/dist/src/{help.js → cli/help.js} +89 -13
- package/dist/src/{ui → cli}/log-commands.js +21 -16
- package/dist/src/{question-commands.js → cli/question-commands.js} +41 -41
- package/dist/src/cli.js +80 -262
- package/dist/src/collections.js +71 -0
- package/dist/src/{check-permit.js → concurrency/check-permit.js} +12 -13
- package/dist/src/concurrency/lock.js +236 -0
- package/dist/src/{semaphore.js → concurrency/semaphore.js} +21 -17
- package/dist/src/config/config-editable-keys.js +15 -0
- package/dist/src/{config-example.js → config/config-example.js} +32 -16
- package/dist/src/{config-field-checks.js → config/config-field-checks.js} +105 -38
- package/dist/src/{config-live.js → config/config-live.js} +60 -17
- package/dist/src/{config-schema.js → config/config-schema.js} +33 -1
- package/dist/src/config/config-validation.js +427 -0
- package/dist/src/config/config-views.js +457 -0
- package/dist/src/config/config-write.js +383 -0
- package/dist/src/{config.js → config/config.js} +74 -23
- package/dist/src/config/model-selector.js +43 -0
- package/dist/src/{doctor-backlog.js → doctor/doctor-backlog.js} +15 -13
- package/dist/src/{doctor-checks.js → doctor/doctor-checks.js} +150 -58
- package/dist/src/doctor/doctor-launch-services.js +24 -0
- package/dist/src/doctor/doctor-model-checks.js +254 -0
- package/dist/src/{doctor-orphans.js → doctor/doctor-orphans.js} +5 -5
- package/dist/src/doctor/doctor-render.js +13 -0
- package/dist/src/{doctor.js → doctor/doctor.js} +20 -9
- package/dist/src/errno.js +1 -1
- package/dist/src/events/event-format.js +392 -0
- package/dist/src/{event-read.js → events/event-read.js} +55 -23
- package/dist/src/{event-window.js → events/event-window.js} +27 -18
- package/dist/src/{events.js → events/events.js} +45 -4
- package/dist/src/{notify.js → events/notify.js} +13 -11
- package/dist/src/{failure-cluster.js → failure/failure-cluster.js} +29 -10
- package/dist/src/{failure-data.js → failure/failure-data.js} +59 -25
- package/dist/src/{failure-report.js → failure/failure-render.js} +83 -57
- package/dist/src/{failure-state-change.js → failure/failure-state-change.js} +36 -24
- package/dist/src/{time-spend.js → failure/time-spend.js} +63 -38
- package/dist/src/{file-queue.js → files/file-queue.js} +1 -1
- package/dist/src/{files.js → files/files.js} +122 -20
- package/dist/src/{json-files.js → files/json-files.js} +13 -11
- package/dist/src/files/json-object.js +77 -0
- package/dist/src/{stat-cache.js → files/stat-cache.js} +1 -1
- package/dist/src/{tail.js → files/tail.js} +12 -11
- package/dist/src/{error-storm.js → fleet/error-storm.js} +9 -10
- package/dist/src/{failure-spread.js → fleet/failure-spread.js} +7 -8
- package/dist/src/{fleet-hold.js → fleet/fleet-hold.js} +59 -21
- package/dist/src/{fleet-polls.js → fleet/fleet-polls.js} +55 -35
- package/dist/src/{fleet-state.js → fleet/fleet-state.js} +71 -53
- package/dist/src/fleet/orchestrator-info.js +132 -0
- package/dist/src/fleet/reclaim.js +299 -0
- package/dist/src/gates/bootstrap-gates.js +99 -0
- package/dist/src/gates/budget-gates.js +205 -0
- package/dist/src/gates/disk-gate.js +78 -0
- package/dist/src/gates/gate-polls.js +276 -0
- package/dist/src/gates/gate-prompts.js +364 -0
- package/dist/src/gates/maintenance-quota.js +198 -0
- package/dist/src/{pause-gates.js → gates/pause-gates.js} +10 -10
- package/dist/src/gates/readiness.js +11 -0
- package/dist/src/{role-cap-gates.js → gates/role-cap-gates.js} +44 -28
- package/dist/src/{startup-gate.js → gates/startup-gate.js} +19 -16
- package/dist/src/{streak-gate.js → gates/streak-gate.js} +31 -33
- package/dist/src/{commit-message.js → git/commit-message.js} +45 -11
- package/dist/src/{git-diff.js → git/git-diff.js} +40 -2
- package/dist/src/git/git-run.js +235 -0
- package/dist/src/{git.js → git/git.js} +45 -92
- package/dist/src/git/slots-state.js +106 -0
- package/dist/src/git/worktree-pool.js +395 -0
- package/dist/src/git/worktree-use.js +156 -0
- package/dist/src/git/worktree.js +263 -0
- package/dist/src/git/xcrun-git.js +24 -0
- package/dist/src/{ui → gui}/gui-args.js +19 -18
- package/dist/src/{ui/gui.js → gui/gui-command.js} +5 -8
- package/dist/src/{ui → gui}/gui-endpoint-commands.js +57 -44
- package/dist/src/{ui → gui}/gui-endpoints.js +53 -28
- package/dist/src/gui/gui-server.js +256 -0
- package/dist/src/{ui → gui}/http-body.js +35 -16
- package/dist/src/{history-data.js → history/history-data.js} +45 -56
- package/dist/src/{ui → history}/history.js +12 -17
- package/dist/src/inbox/inbox-attachments.js +167 -0
- package/dist/src/inbox/inbox-cancel.js +107 -0
- package/dist/src/inbox/inbox-edit.js +64 -0
- package/dist/src/{inbox-submit.js → inbox/inbox-submit.js} +34 -14
- package/dist/src/inbox/inbox.js +288 -0
- package/dist/src/{pending-prompt.js → inbox/pending-prompt.js} +4 -4
- package/dist/src/{prompt-commands.js → inbox/prompt-commands.js} +135 -45
- package/dist/src/inbox/prompt-not-before.js +61 -0
- package/dist/src/{init-templates.js → init/init-templates.js} +78 -4
- package/dist/src/{init.js → init/init.js} +84 -89
- package/dist/src/landing/backlog-conflicts.js +116 -0
- package/dist/src/{landing-batch.js → landing/landing-batch.js} +91 -68
- package/dist/src/{landing-check-failures.js → landing/landing-check-failures.js} +65 -39
- package/dist/src/{landing-core.js → landing/landing-core.js} +105 -23
- package/dist/src/landing/landing-diff.js +57 -0
- package/dist/src/{landing-drain.js → landing/landing-drain.js} +14 -10
- package/dist/src/{landing-git.js → landing/landing-git.js} +78 -18
- package/dist/src/{landing-merge.js → landing/landing-merge.js} +72 -102
- package/dist/src/{landing-pipeline.js → landing/landing-pipeline.js} +19 -9
- package/dist/src/landing/landing-questions.js +16 -0
- package/dist/src/{landing-queue.js → landing/landing-queue.js} +31 -17
- package/dist/src/{landing-slot.js → landing/landing-slot.js} +57 -34
- package/dist/src/{landing-stack.js → landing/landing-stack.js} +35 -21
- package/dist/src/{landing-vetting.js → landing/landing-vetting.js} +22 -18
- package/dist/src/{leftover.js → loop/leftover.js} +50 -18
- package/dist/src/{loop-pi.js → loop/loop-pi.js} +125 -61
- package/dist/src/loop/loop-state.js +171 -0
- package/dist/src/loop/loop.js +893 -0
- package/dist/src/loop/model-fallback.js +75 -0
- package/dist/src/loop/revision.js +61 -0
- package/dist/src/{operator-commands.js → operator/operator-commands.js} +130 -43
- package/dist/src/{operator-intent.js → operator/operator-intent.js} +88 -42
- package/dist/src/operator/operator-requests.js +226 -0
- package/dist/src/operator/retire.js +145 -0
- package/dist/src/orchestrator/orchestrator-launch.js +159 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +270 -0
- package/dist/src/{orchestrator.js → orchestrator/orchestrator.js} +178 -184
- package/dist/src/{retention.js → orchestrator/retention.js} +5 -5
- package/dist/src/paths.js +104 -20
- package/dist/src/{command-shape.js → pi/command-shape.js} +15 -3
- package/dist/src/{pi-args.js → pi/pi-args.js} +5 -3
- package/dist/src/{readiness.js → pi/pi-bin.js} +11 -11
- package/dist/src/{pi-event-line.js → pi/pi-event-line.js} +18 -19
- package/dist/src/{pi-models.js → pi/pi-models.js} +59 -20
- package/dist/src/pi/pi-run-result.js +1 -0
- package/dist/src/{pi-stream.js → pi/pi-stream.js} +99 -21
- package/dist/src/pi/pi-watchdogs.js +176 -0
- package/dist/src/{pi.js → pi/pi.js} +108 -137
- package/dist/src/pi-extension/bounded-output.js +5 -51
- package/dist/src/pi-extension/context-budget.js +44 -37
- package/dist/src/pi-extension/context-shake.js +235 -0
- package/dist/src/pi-extension/context-usage.js +20 -0
- package/dist/src/pi-extension/full-output.js +62 -0
- package/dist/src/pi-extension/role-notes.js +90 -0
- package/dist/src/pi-extension/tool-result-content.js +15 -0
- package/dist/src/{launch-services.js → process/launch-services.js} +16 -30
- package/dist/src/{process-group.js → process/process-group.js} +79 -51
- package/dist/src/{process-table.js → process/process-table.js} +23 -13
- package/dist/src/{process.js → process/process.js} +62 -20
- package/dist/src/{supervisor.js → process/supervisor.js} +31 -10
- package/dist/src/prompt/principles.js +14 -0
- package/dist/src/{prompt-followup.js → prompt/prompt-followup.js} +1 -1
- package/dist/src/{prompt.js → prompt/prompt.js} +87 -44
- package/dist/src/{redeploy-policy.js → redeploy/redeploy-policy.js} +15 -11
- package/dist/src/{redeploy-probes.js → redeploy/redeploy-probes.js} +5 -2
- package/dist/src/{redeploy.js → redeploy/redeploy.js} +23 -17
- package/dist/src/{redeployer.js → redeploy/redeployer.js} +43 -17
- package/dist/src/{self-reload.js → redeploy/self-reload.js} +30 -3
- package/dist/src/report/report-data.js +325 -0
- package/dist/src/{ui → report}/report-render.js +22 -20
- package/dist/src/{ui → report}/report.js +12 -12
- package/dist/src/request-timeouts.js +6 -0
- package/dist/src/review/known-flakes.js +45 -0
- package/dist/src/{review-followup.js → review/review-followup.js} +31 -40
- package/dist/src/{review-precheck.js → review/review-precheck.js} +37 -19
- package/dist/src/{review-verdict.js → review/review-verdict.js} +2 -2
- package/dist/src/{review.js → review/review.js} +88 -51
- package/dist/src/{suite-rerun.js → review/suite-rerun.js} +6 -5
- package/dist/src/roles/loop-ids.js +61 -0
- package/dist/src/{role-catalog.js → roles/role-catalog.js} +175 -29
- package/dist/src/{role-guidance.js → roles/role-guidance.js} +23 -10
- package/dist/src/{ui/role-report.js → roles/role-render.js} +12 -3
- package/dist/src/{role-view.js → roles/role-view.js} +28 -15
- package/dist/src/{roles.js → roles/roles.js} +47 -20
- package/dist/src/{backoff.js → scheduling/backoff.js} +12 -2
- package/dist/src/scheduling/claims.js +104 -0
- package/dist/src/{once-round.js → scheduling/once-round.js} +3 -6
- package/dist/src/{quiet-hours.js → scheduling/quiet-hours.js} +30 -11
- package/dist/src/{scheduling.js → scheduling/scheduling.js} +31 -41
- package/dist/src/{work-landed-cache.js → scheduling/work-landed-cache.js} +3 -3
- package/dist/src/status/status-data.js +225 -0
- package/dist/src/status/status-polls.js +202 -0
- package/dist/src/{datetime.js → text/datetime.js} +61 -16
- package/dist/src/text/format.js +58 -0
- package/dist/src/text/markdown.js +30 -0
- package/dist/src/text/phrases.js +228 -0
- package/dist/src/text/suggest.js +66 -0
- package/dist/src/{text-width.js → text/text-width.js} +1 -1
- package/dist/src/text/text.js +199 -0
- package/dist/src/{qa-coverage.js → tick/qa-coverage.js} +3 -3
- package/dist/src/tick/stage-check.js +276 -0
- package/dist/src/{telemetry-digest.js → tick/telemetry-digest.js} +6 -6
- package/dist/src/{tick-apply.js → tick/tick-apply.js} +51 -9
- package/dist/src/{tick-detail-data.js → tick/tick-detail-data.js} +9 -8
- package/dist/src/tick/tick-detail.js +122 -0
- package/dist/src/{tick-finalize.js → tick/tick-finalize.js} +41 -15
- package/dist/src/{tick-prompt.js → tick/tick-prompt.js} +89 -20
- package/dist/src/{tick-resume.js → tick/tick-resume.js} +5 -5
- package/dist/src/tick/tick-stage.js +181 -0
- package/dist/src/{tick-timing.js → tick/tick-timing.js} +44 -50
- package/dist/src/tick/tick-usage.js +112 -0
- package/dist/src/{tick-verdict.js → tick/tick-verdict.js} +55 -14
- package/dist/src/ui/badges.js +74 -19
- package/dist/src/ui/fleet-alerts.js +35 -16
- package/dist/src/ui/{gui-client-boot.js → gui/gui-client-boot.js} +8 -2
- package/dist/src/ui/{gui-client-composer.js → gui/gui-client-composer.js} +38 -6
- package/dist/src/ui/{gui-client-drawer.js → gui/gui-client-drawer.js} +56 -4
- package/dist/src/ui/{gui-client-fleet.js → gui/gui-client-fleet.js} +38 -23
- package/dist/src/ui/{gui-client-loops.js → gui/gui-client-loops.js} +15 -3
- package/dist/src/ui/{gui-client-model.js → gui/gui-client-model.js} +47 -20
- package/dist/src/ui/{gui-client-operator.js → gui/gui-client-operator.js} +5 -15
- package/dist/src/ui/gui/gui-client-pending.js +77 -0
- package/dist/src/ui/{gui-client-report.js → gui/gui-client-report.js} +9 -5
- package/dist/src/ui/{gui-client-settings.js → gui/gui-client-settings.js} +4 -2
- package/dist/src/ui/{gui-client.js → gui/gui-client.js} +79 -15
- package/dist/src/ui/{gui-page.js → gui/gui-page.js} +8 -6
- package/dist/src/ui/{gui-styles.js → gui/gui-styles.js} +10 -5
- package/dist/src/{progress-data.js → ui/progress-data.js} +68 -42
- package/dist/src/ui/status-model.js +122 -143
- package/dist/src/ui/status-payload.js +64 -20
- package/dist/src/ui/status-render.js +45 -15
- package/dist/src/ui/tick-progress-model.js +117 -0
- package/dist/src/ui/tone.js +28 -9
- package/dist/src/ui/transcript-tail.js +38 -14
- package/dist/src/ui/transcript.js +45 -36
- package/dist/src/ui/{tui-app.js → tui/tui-app.js} +2 -4
- package/dist/src/ui/{tui-backlog.js → tui/tui-backlog.js} +5 -4
- package/dist/src/ui/{tui-frame.js → tui/tui-frame.js} +15 -4
- package/dist/src/ui/{tui-input.js → tui/tui-input.js} +24 -84
- package/dist/src/ui/{tui-keys.js → tui/tui-keys.js} +26 -14
- package/dist/src/ui/tui/tui-pane.js +66 -0
- package/dist/src/ui/tui/tui-prompt-history.js +80 -0
- package/dist/src/ui/{tui.js → tui/tui.js} +45 -68
- package/dist/src/{fix-claim.js → verdict/fix-claim.js} +49 -21
- package/dist/src/{refusal.js → verdict/refusal.js} +11 -5
- package/dist/src/{reply-contract.js → verdict/reply-contract.js} +38 -27
- package/dist/src/version.js +3 -3
- package/package.json +1 -1
- package/dist/src/backlog-structure.js +0 -166
- package/dist/src/budget-gates.js +0 -128
- package/dist/src/cli-flag-specs.js +0 -185
- package/dist/src/cli-output.js +0 -44
- package/dist/src/config-commands.js +0 -58
- package/dist/src/config-validation.js +0 -307
- package/dist/src/config-views.js +0 -106
- package/dist/src/config-write.js +0 -217
- package/dist/src/event-format.js +0 -261
- package/dist/src/gate-polls.js +0 -150
- package/dist/src/gate-prompts.js +0 -210
- package/dist/src/inbox-attachments.js +0 -114
- package/dist/src/inbox.js +0 -392
- package/dist/src/json-object.js +0 -31
- package/dist/src/lock.js +0 -165
- package/dist/src/loop-state.js +0 -44
- package/dist/src/loop.js +0 -541
- package/dist/src/operator-requests.js +0 -114
- package/dist/src/orchestrator-launch.js +0 -119
- package/dist/src/phrases.js +0 -116
- package/dist/src/rank.js +0 -18
- package/dist/src/report-data.js +0 -186
- package/dist/src/status-data.js +0 -133
- package/dist/src/status-polls.js +0 -133
- package/dist/src/text.js +0 -183
- package/dist/src/tick-stage.js +0 -95
- package/dist/src/tick-usage.js +0 -67
- package/dist/src/ui/doctor-report.js +0 -9
- package/dist/src/ui/gui-server.js +0 -198
- package/dist/src/ui/tick-detail.js +0 -87
- package/dist/src/worktree.js +0 -154
- /package/dist/src/{run-marker.js → process/run-marker.js} +0 -0
- /package/dist/src/{exemptions.js → review/exemptions.js} +0 -0
- /package/dist/src/{tick-outcome.js → tick/tick-outcome.js} +0 -0
- /package/dist/src/ui/{gui-client-history.js → gui/gui-client-history.js} +0 -0
- /package/dist/src/ui/{gui-client-markdown.js → gui/gui-client-markdown.js} +0 -0
- /package/dist/src/ui/{gui-client-sound.js → gui/gui-client-sound.js} +0 -0
- /package/dist/src/ui/{gui-icons.js → gui/gui-icons.js} +0 -0
- /package/dist/src/ui/{tui-keymap.js → tui/tui-keymap.js} +0 -0
- /package/dist/src/{no-change.js → verdict/no-change.js} +0 -0
package/README.md
CHANGED
|
@@ -10,10 +10,22 @@ local git repo, and no remote is ever touched.
|
|
|
10
10
|
|
|
11
11
|

|
|
12
12
|
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Requires Node 20.3 or later on macOS or Linux.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g tumwater
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or run any command without installing: `npx tumwater`. To run from a checkout of this repo
|
|
22
|
+
instead: `npm install && npm run build && npm link`.
|
|
23
|
+
|
|
13
24
|
## Status
|
|
14
25
|
|
|
15
26
|
<!-- tumwater:status:start -->
|
|
16
|
-
**v0.1**: working harness.
|
|
27
|
+
**v0.1.1**: working harness. The director and 14 of the 15 roles are enabled by default; `telemetry`,
|
|
28
|
+
which files harness bugs from tumwater's own event log, is opt-in (`roles.telemetry.enabled`).
|
|
17
29
|
|
|
18
30
|
Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
|
|
19
31
|
[QUESTIONS.md](QUESTIONS.md) (open questions).
|
|
@@ -22,8 +34,6 @@ Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
|
|
|
22
34
|
## Usage
|
|
23
35
|
|
|
24
36
|
```bash
|
|
25
|
-
npm install -g tumwater # or run any command with npx tumwater
|
|
26
|
-
# from a checkout: npm install && npm run build && npm link
|
|
27
37
|
cd your-project # a new or existing directory
|
|
28
38
|
tumwater init "Build a tiny markdown-to-html converter CLI in Python."
|
|
29
39
|
# add --template <id> to seed from a bundled starting point
|
|
@@ -31,30 +41,54 @@ tumwater init "Build a tiny markdown-to-html converter CLI in Python."
|
|
|
31
41
|
# prints the catalog
|
|
32
42
|
# add --file <path> to read the brief from a file
|
|
33
43
|
# add --adopt to adopt an existing repo as-is
|
|
44
|
+
# a fresh/empty project is seeded with
|
|
45
|
+
# "bootstrap": {"untilPlansDone": 5}, holding the maintenance
|
|
46
|
+
# loops until 5 plans are done; remove "bootstrap" from
|
|
47
|
+
# tumwater.json to end that early
|
|
34
48
|
tumwater run # start the loops (Ctrl+C to stop)
|
|
49
|
+
tumwater run --gui # ... and serve the browser dashboard at http://127.0.0.1:7180 from the same process
|
|
50
|
+
tumwater run --for 2h # run for a bounded window (capped at 90d), then drain and exit like Ctrl+C would
|
|
35
51
|
```
|
|
36
52
|
|
|
37
53
|
Then, from another terminal:
|
|
38
54
|
|
|
39
55
|
| To | Run |
|
|
40
56
|
| --- | --- |
|
|
41
|
-
| Watch the fleet | `tumwater tui`, or
|
|
42
|
-
| Watch per-tick history | `tumwater history [--role <id>] [-n N] [--since <duration>] [--grep <text>]`, or `tumwater history --json` for the rows as JSON; `tumwater tick <role> <n>` for one tick's full event trail (a summary header — when it ran, how long, result, usage — followed by the tick's events, oldest first; `--json` prints the payload as JSON, `null` when the log holds no such tick) |
|
|
43
|
-
| Check state | `tumwater status`, `tumwater logs -f`, `tumwater logs --since <duration>`, `tumwater logs --grep <text>`, `tumwater logs --json` (the event feed as NDJSON, for scripts), `tumwater logs --role <id>`, `tumwater backlog` (planned features, open bugs, open questions as Markdown), `tumwater backlog --json` (the backlog as JSON, for scripts), `tumwater role <id>` (one loop's standing prompt — find text, `instructions` override, resolved model and interval, enabled/paused state — plus its next tick's assembled prompt, which shows the oldest queued prompt without consuming it (one is dequeued per tick; `--json` for scripts)) |
|
|
57
|
+
| Watch the fleet | `tumwater tui`, or the browser dashboard at http://127.0.0.1:7180 — `tumwater gui` on its own, or `tumwater run --gui` to boot the fleet and the dashboard together |
|
|
58
|
+
| Watch per-tick history | `tumwater history [--role <id>] [-n N] [--since <duration>] [--grep <text>]`, or `tumwater history --json` for the rows as JSON; `tumwater tick <role> <n>` for one tick's full event trail (a summary header — when it ran, how long, result, usage — followed by the tick's events, oldest first; `--last` shows the newest completed tick's trail instead of numbering one; `--json` prints the payload as JSON, `null` when the log holds no such tick) |
|
|
59
|
+
| Check state | `tumwater status`, `tumwater logs -f`, `tumwater logs --since <duration>`, `tumwater logs --grep <text>`, `tumwater logs --json` (the event feed as NDJSON, for scripts), `tumwater logs --role <id>`, `tumwater backlog` (planned features, open bugs, open questions as Markdown), `tumwater backlog --json` (the backlog as JSON, for scripts), `tumwater role <id>` (one loop's standing prompt — find text, `instructions` override, resolved model and interval, enabled/paused state, its notebook — plus its next tick's assembled prompt, which shows the oldest queued prompt without consuming it (one is dequeued per tick; `--json` for scripts)) |
|
|
44
60
|
| See a loop's pending change | `tumwater diff --role <id>` — that loop's branch's unlanded commits (with the patch) and its worktree's uncommitted edits (staged and unstaged); without `--role`, one line per loop holding pending work; `--json` prints the payload as data |
|
|
45
|
-
| Steer the project | `tumwater prompt "prefer no third-party deps"` queues a request for the director; add `--role <id>` to aim it at one loop's next tick, `tumwater prompt --list` shows the queued prompts numbered and grouped by loop, with how long each has waited (`--json` for scripts), and `tumwater prompt --cancel <n>` removes the Nth entry as `--list` shows them (
|
|
61
|
+
| Steer the project | `tumwater prompt "prefer no third-party deps"` queues a request for the director; add `--role <id>` to aim it at one loop's next tick, `--file <path>` (`-` for stdin) to read the text from a file, or `--at <duration>` to defer it until the duration passes (45s, 90m, 1h30m, 1d; capped at 90d), `tumwater prompt --list` shows the queued prompts numbered and grouped by loop, with how long each has waited (`--json` for scripts), and `tumwater prompt --cancel <n>` removes the Nth entry as `--list` shows them, and `tumwater prompt --edit <n> "new text"` rewrites the Nth entry in place, keeping its position and wait time (both take `--role <id>` when several loops share that number), and `--attach <path>` (repeatable, up to 4 images — png, jpg, jpeg, gif, webp, bmp, each at most 5 MiB) attaches an image the receiving loop reads at its next tick; `tumwater bug "<symptom>"` files a bug into BUGS.md's Open section and wakes the bugfix loop, `tumwater plan "<title>" [body...]` files a plan request into PLANS.md's Planned section and wakes the feature loop — both stamped as operator-reported (the other half of `tumwater backlog`) |
|
|
46
62
|
| Answer open questions | `tumwater questions` lists QUESTIONS.md's open questions numbered (as the loops wrote them); `tumwater questions answer <n> "decision text"` moves the Nth entry to ## Answered with your dated answer (loops read the file back on their next tick); `--json` prints the list as data |
|
|
47
|
-
| Control the loops | `tumwater pause [--for <duration>]` / `resume [--role <id>]` (fleet or one loop; `--for 2h` auto-resumes, capped at 90d; add `--reason <text>` on a fleet pause to state why — it shows on `status`, the TUI, and the dashboard), `tumwater wake` (skip backoff), `tumwater abort --role <id>`, `tumwater stop` (drain and exit, like Ctrl+C) |
|
|
48
|
-
| Audit | `tumwater doctor` (pre-flight; `--json` prints the report as JSON, for scripts), `tumwater report` (usage and cost; totals include landing runs — reviewer + conflict resolution), `tumwater report --since <duration>` (totals over a trailing window, capped at 7d), `tumwater report --json` (the `--days`/`--since`/`--failures` reports as JSON, for scripts), `tumwater report --failures` (the failure digest: per-role outcomes, each role's time and spend by outcome, and the top five loss causes ranked by agent-hours, with a marker naming how many were cut) |
|
|
63
|
+
| Control the loops | `tumwater pause [--for <duration>]` / `resume [--role <id>]` (fleet or one loop; `--for 2h` auto-resumes, capped at 90d; add `--reason <text>` on a fleet pause to state why — it shows on `status`, the TUI, and the dashboard), `tumwater wake` (skip backoff; `wake --in 45m` schedules the wake for 45 minutes from now — the marker is written immediately but consumed no earlier than the deadline, like `pause --for`'s auto-resume), `tumwater reclaim [--dry-run]` (drop gitignored build outputs from every harness worktree; `--dry-run` lists the candidates and cleans nothing), `tumwater retire --role <id>` (remove a disabled loop's worktree, branch, and per-role state — use after setting `enabled: false` for that role; `--force` overrides the safety rails), `tumwater abort --role <id>`, `tumwater stop` (drain and exit, like Ctrl+C), `tumwater reset-counters [--role <id>]` (zero the per-loop counters the dashboards show, starting a fresh observation window — scheduling is untouched) |
|
|
64
|
+
| Audit | `tumwater doctor` (pre-flight; `--json` prints the report as JSON, for scripts), `tumwater report` (usage and cost, with landed commits by role and the work/maintenance split; totals include landing runs — reviewer + conflict resolution), `tumwater report --since <duration>` (totals over a trailing window, capped at 7d), `tumwater report --json` (the `--days`/`--since`/`--failures` reports as JSON, for scripts), `tumwater report --failures` (the failure digest: per-role outcomes, each role's time and spend by outcome, and the top five loss causes ranked by agent-hours, with a marker naming how many were cut) |
|
|
49
65
|
|
|
50
66
|
`tumwater help` lists every command and flag; `tumwater help <command>` shows one command's usage. `gui --all-interfaces` exposes the dashboard, and
|
|
51
67
|
with it the director prompt, to your whole network, so pair it with `--token <secret>`.
|
|
52
68
|
|
|
53
|
-
Settings live in `tumwater.json`: enabled roles, model
|
|
69
|
+
Settings live in `tumwater.json`: enabled roles, model (either one selector or a map of tiers
|
|
70
|
+
`small`/`default`/`strong` — omitted tiers inherit `default`; per-tier `fallback` overrides may name a
|
|
71
|
+
model or `"pause"`, and a role's `model` may name a tier; see [plans/model-tiers.md](plans/model-tiers.md)),
|
|
72
|
+
intervals, per-role `instances` (`feature` and `bugfix` may each run 1–8 parallel runners, every one
|
|
73
|
+
with its own branch and state; default 1), the daily spend cap
|
|
54
74
|
(`maxDailyCostUsd`, with optional per-role caps `maxDailyCostUsdPerRole` — a loop over its own
|
|
55
|
-
cap starts no new ticks until the next local day or a live edit), a
|
|
75
|
+
cap starts no new ticks until the next local day or a live edit), a
|
|
76
|
+
`maintenancePerWorkLanding` ratio (default 2; a rolling 24 h allowance of that many
|
|
77
|
+
code-maintenance landings per feature/bugfix/director landing, plus a floor of 12 — when the
|
|
78
|
+
window reaches it the scheduler holds the enabled maintenance loops until it rolls under, and a
|
|
79
|
+
fresh `tumwater wake <role>` or a queued prompt for one admits a single tick anyway), a nightly `quietHours` window
|
|
56
80
|
(e.g. `"23:00-07:00"` local time) during
|
|
57
|
-
which role loops start no new ticks (the director is exempt),
|
|
81
|
+
which role loops start no new ticks (the director is exempt), with optional per-role windows
|
|
82
|
+
`quietHoursPerRole` — a loop inside its own window starts no new ticks, whether or not the
|
|
83
|
+
fleet-wide window covers now — a `diskHoldGB` free-space floor (default 10; when the volume
|
|
84
|
+
holding the worktrees drops below it, no new work starts until free space recovers 5 GB above it;
|
|
85
|
+
0 disables), a `diskReclaimGB` pressure-reclaim threshold (default 40; below it idle worktrees
|
|
86
|
+
drop their gitignored build outputs before the hold engages; 0 disables), and a
|
|
87
|
+
`worktreeIdleReclaimHours` window (default 24; an hourly pass drops a worktree's gitignored build
|
|
88
|
+
outputs once it has sat unused that long, whatever the free space; 0 disables), a
|
|
89
|
+
`worktreeSlots` count of pooled checkouts shared by role ticks and landing vets (default
|
|
90
|
+
`maxConcurrent` + 1; the director's worktree sits outside the pool) — user-defined
|
|
91
|
+
`customLoops`, and an
|
|
58
92
|
optional `notify` shell command run when the fleet needs a human (a budget pause, a budget warning
|
|
59
93
|
at 80% of the cap while the gate is still open, an error-streak
|
|
60
94
|
breaker trip, a failed landing, a blocked restart — the command gets `TUMWATER_EVENT_TYPE`,
|
|
@@ -62,10 +96,17 @@ breaker trip, a failed landing, a blocked restart — the command gets `TUMWATER
|
|
|
62
96
|
Edits apply live while the fleet runs.
|
|
63
97
|
From the terminal, `tumwater config` prints the effective config as JSON, `tumwater config get
|
|
64
98
|
<key>` reads one resolved value, and `tumwater config set <key> <value>` writes one top-level
|
|
65
|
-
key.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
99
|
+
key; dotted keys (`maxDailyCostUsdPerRole.feature 1.5`, `roles.qa.model x`) merge one entry
|
|
100
|
+
into the existing map or role entry, while bare keys replace the whole value.
|
|
101
|
+
|
|
102
|
+
**Backends:** any OpenAI-compatible model pi can reach works; set `model` in `tumwater.json` to
|
|
103
|
+
one `provider/id[:thinking]` selector (plus `fallback` to a free selector), or a map of tiers
|
|
104
|
+
`small`/`default`/`strong`. A role whose primary keeps failing with provider-class errors
|
|
105
|
+
(three consecutive 429s or backend failures) runs its next ticks on its tier's resolved
|
|
106
|
+
`fallback` pair and probes the primary once the 5-minute cooldown elapses, returning to it
|
|
107
|
+
when a probe answers; `tumwater status`, the TUI, the dashboard, and `tumwater role <id>` name
|
|
108
|
+
the off-model episode. See [docs/backends.md](docs/backends.md) for requirements and a
|
|
109
|
+
worked setup.
|
|
69
110
|
|
|
70
111
|
For how the loops, review gate, scheduling, and self-redeploy work, see
|
|
71
112
|
[docs/how-it-works.md](docs/how-it-works.md). For a measured comparison of tumwater's own
|
package/dist/build-info.json
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** Backlog eligibility: which PLANS.md / BUGS.md entries a work loop may take now (plans,
|
|
2
|
+
* parallel-work-instances, part 2/7). An entry is held when its body carries a **Refused …**,
|
|
3
|
+
* **Needs review …** or **Needs replan …** note, or when its heading's trailing parenthetical
|
|
4
|
+
* names a prerequisite (`requires parts 1/5–3/5 landed`) that is itself still listed under
|
|
5
|
+
* `## Planned`. The parsers are pure over markdown text; `eligibleEntries` is the stat-backed
|
|
6
|
+
* reader for a caller that wants the list. The backlog index marks every held entry so a loop
|
|
7
|
+
* skips it without re-deriving the rule. */
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { readTextOrNull } from "../files/files.js";
|
|
10
|
+
import { collapseWhitespace } from "../text/text.js";
|
|
11
|
+
import { NEEDS_REPLAN_PREFIX, NEEDS_REVIEW_PREFIX } from "../roles/role-guidance.js";
|
|
12
|
+
import { baseRoleOf } from "../roles/loop-ids.js";
|
|
13
|
+
import { trailingParenthetical } from "./backlog-md.js";
|
|
14
|
+
import { actionableEntryRanges, stripEntryStamp } from "./backlog-structure.js";
|
|
15
|
+
import { openBugEntries, plannedPlanEntries } from "./backlog.js";
|
|
16
|
+
/** The `**Refused …` note prefix a refusing tick writes. Every hold note — this one and the
|
|
17
|
+
* imported Needs-review and Needs-replan prefixes — is matched as a line prefix (hasNoteLine)
|
|
18
|
+
* so a mention in prose never holds an entry. */
|
|
19
|
+
const REFUSED_PREFIX = "**Refused ";
|
|
20
|
+
/** True when `body` carries a line (ignoring leading indentation) that begins with `prefix`.
|
|
21
|
+
* The one home of the hold-note rule: a note holds its entry only when written as its own
|
|
22
|
+
* line, never when its prefix is quoted inside prose. */
|
|
23
|
+
function hasNoteLine(body, prefix) {
|
|
24
|
+
return body.split("\n").some((line) => line.trimStart().startsWith(prefix));
|
|
25
|
+
}
|
|
26
|
+
/** One prerequisite ref, resolved against the entry's own series when it names none. */
|
|
27
|
+
function parseRef(chunk, ownSeries) {
|
|
28
|
+
const text = chunk.trim().replace(/^parts?\s+/i, "");
|
|
29
|
+
if (text === "")
|
|
30
|
+
return [];
|
|
31
|
+
const m = /^(?:([\s\S]+?)\s+)?([0-9]+[a-z]?)\/(\d+)(?:\s*[–-]\s*(\d+)\/(\d+))?$/i.exec(text);
|
|
32
|
+
if (m === null)
|
|
33
|
+
return [];
|
|
34
|
+
const series = (m[1]?.trim() ?? "") || ownSeries;
|
|
35
|
+
if (series === null || series === "")
|
|
36
|
+
return [];
|
|
37
|
+
const of = Number(m[3]);
|
|
38
|
+
const start = m[2].toLowerCase();
|
|
39
|
+
if (!Number.isInteger(of) || of < 1)
|
|
40
|
+
return [];
|
|
41
|
+
if (m[4] === undefined)
|
|
42
|
+
return [{ series, part: start, of }];
|
|
43
|
+
// A range is numeric on both ends (the lettered parts never range); expand it inclusively.
|
|
44
|
+
const from = Number(start);
|
|
45
|
+
const to = Number(m[4]);
|
|
46
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || to < from)
|
|
47
|
+
return [];
|
|
48
|
+
const parts = [];
|
|
49
|
+
for (let p = from; p <= to; p++)
|
|
50
|
+
parts.push({ series, part: String(p), of });
|
|
51
|
+
return parts;
|
|
52
|
+
}
|
|
53
|
+
/** The stable identity of an entry heading: its `(planned …)`/`(done …)` stamp suffix
|
|
54
|
+
* removed through the shared stripEntryStamp, whitespace collapsed, lowercased. Adding a
|
|
55
|
+
* done stamp or a Refused note therefore leaves the key unchanged. */
|
|
56
|
+
export function entryKey(title) {
|
|
57
|
+
return collapseWhitespace(stripEntryStamp(title)).toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
/** An entry's own series and part from its heading, or null when the heading is not a
|
|
60
|
+
* `<Series>, part i/n: …` title. The series is the text before `, part i/n:`. */
|
|
61
|
+
export function seriesPart(title) {
|
|
62
|
+
const m = /^(.+?),\s*part\s+([0-9]+[a-z]?)\/(\d+)\s*:/i.exec(stripEntryStamp(title));
|
|
63
|
+
if (m === null)
|
|
64
|
+
return null;
|
|
65
|
+
const series = m[1].trim();
|
|
66
|
+
if (series === "")
|
|
67
|
+
return null;
|
|
68
|
+
return { series, part: m[2].toLowerCase(), of: Number(m[3]) };
|
|
69
|
+
}
|
|
70
|
+
/** The prerequisite parts a plan heading's trailing parenthetical names through the clause
|
|
71
|
+
* `requires <ref>((, | and )<ref>)* landed`, with each range (e.g. `parts 1/5–3/5`) expanded.
|
|
72
|
+
* Returns [] when the heading has no such clause or the clause does not parse — an agent judges
|
|
73
|
+
* an unparseable clause, exactly as it does today. Only the heading is read, never the body. */
|
|
74
|
+
export function requiredParts(title) {
|
|
75
|
+
const meta = trailingParenthetical(title);
|
|
76
|
+
if (meta === "")
|
|
77
|
+
return [];
|
|
78
|
+
const m = /requires\s+(.+?)\s+landed\b/i.exec(meta);
|
|
79
|
+
if (m === null)
|
|
80
|
+
return [];
|
|
81
|
+
const ownSeries = seriesPart(title)?.series ?? null;
|
|
82
|
+
const refs = [];
|
|
83
|
+
for (const chunk of m[1].split(/\s*,\s*|\s+and\s+/i))
|
|
84
|
+
refs.push(...parseRef(chunk, ownSeries));
|
|
85
|
+
return refs;
|
|
86
|
+
}
|
|
87
|
+
/** Why `entry` is held against the still-`planned` entries, or null when it may be taken: a
|
|
88
|
+
* `**Refused …` line (refused), a Needs-review line (needs-review), a Needs-replan line
|
|
89
|
+
* (needs-replan — the plan loop owns it), or a prerequisite `(series, part)` still among
|
|
90
|
+
* `planned` ({ blockedBy }). Series compare case-insensitively; part tokens compare verbatim.
|
|
91
|
+
* Bodies mentioning "requires" are never consulted — only the heading's trailing
|
|
92
|
+
* parenthetical. */
|
|
93
|
+
export function entryHold(entry, planned) {
|
|
94
|
+
if (hasNoteLine(entry.body, REFUSED_PREFIX))
|
|
95
|
+
return "refused";
|
|
96
|
+
if (hasNoteLine(entry.body, NEEDS_REVIEW_PREFIX))
|
|
97
|
+
return "needs-review";
|
|
98
|
+
if (hasNoteLine(entry.body, NEEDS_REPLAN_PREFIX))
|
|
99
|
+
return "needs-replan";
|
|
100
|
+
const refs = requiredParts(entry.title);
|
|
101
|
+
if (refs.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
const parts = planned
|
|
104
|
+
.map((p) => seriesPart(p.title))
|
|
105
|
+
.filter((p) => p !== null);
|
|
106
|
+
const blocked = refs.filter((ref) => parts.some((p) => p.series.toLowerCase() === ref.series.toLowerCase() && p.part === ref.part));
|
|
107
|
+
return blocked.length === 0
|
|
108
|
+
? null
|
|
109
|
+
: { blockedBy: blocked.map((ref) => `${ref.series} ${ref.part}/${ref.of}`) };
|
|
110
|
+
}
|
|
111
|
+
/** The entries a `role` loop may take now, in file order: PLANS.md's `## Planned` entries for
|
|
112
|
+
* feature, BUGS.md's `## Open` entries for bugfix, minus every entry entryHold holds, each with
|
|
113
|
+
* its stamp-free key, verbatim title and 1-based line range. Reads the primary checkout through
|
|
114
|
+
* the same stat-cached readers the index uses; a missing or unreadable file yields []. */
|
|
115
|
+
export function eligibleEntries(root, role) {
|
|
116
|
+
// An instance id (`bugfix-2`) resolves through its base role, exactly as claims.ts's
|
|
117
|
+
// roleSection does, so a claim's line range is read from the right file.
|
|
118
|
+
const bugfix = baseRoleOf(role) === "bugfix";
|
|
119
|
+
const file = bugfix ? "BUGS.md" : "PLANS.md";
|
|
120
|
+
const section = bugfix ? "Open" : "Planned";
|
|
121
|
+
const md = readTextOrNull(path.join(root, file));
|
|
122
|
+
if (md === null)
|
|
123
|
+
return [];
|
|
124
|
+
const entries = bugfix ? openBugEntries(root) : plannedPlanEntries(root);
|
|
125
|
+
const ranges = actionableEntryRanges(md, section);
|
|
126
|
+
const planned = plannedPlanEntries(root);
|
|
127
|
+
const out = [];
|
|
128
|
+
for (let i = 0; i < entries.length && i < ranges.length; i++) {
|
|
129
|
+
const entry = entries[i];
|
|
130
|
+
if (entryHold(entry, planned) !== null)
|
|
131
|
+
continue;
|
|
132
|
+
const range = ranges[i];
|
|
133
|
+
out.push({
|
|
134
|
+
key: entryKey(entry.title),
|
|
135
|
+
title: entry.title,
|
|
136
|
+
start: range.start,
|
|
137
|
+
end: range.end,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/** The pure markdown layer of the backlog parsers: fence-aware reading of PLANS.md / BUGS.md /
|
|
2
|
+
* QUESTIONS.md text, with no filesystem access — every function here takes the markdown text as
|
|
3
|
+
* an argument. Split from src/backlog/backlog.ts, which keeps the stat-cached file readers
|
|
4
|
+
* (sectionEntries, the root-based plannedPlans/openBugs/openQuestions family, and the
|
|
5
|
+
* Done/Fixed date scan): consumers who only parse markdown (question-commands.ts's QUESTIONS.md
|
|
6
|
+
* walks, backlog-write.ts's section appends, backlog-structure.ts's stranding checks)
|
|
7
|
+
* import from here without reaching the file/cache layer, and a caller who parses a string never
|
|
8
|
+
* pays for a stat. Both layers share one fenceTracker state machine, so independent readers can
|
|
9
|
+
* never disagree about what is body content. */
|
|
10
|
+
/** The repo-root backlog markdown files shared by every layer that knows them by name: the
|
|
11
|
+
* tracked files loops edit and readers parse by `## ` section — read by backlog-structure.ts's
|
|
12
|
+
* structural checks, landing/backlog-conflicts.ts's mechanical insert-conflict resolver, and
|
|
13
|
+
* doctor-backlog.ts's duplicate-heading check. One definition, so the three cannot disagree
|
|
14
|
+
* about which files count. */
|
|
15
|
+
export const BACKLOG_FILES = new Set(["PLANS.md", "BUGS.md", "QUESTIONS.md"]);
|
|
4
16
|
/** A per-line CommonMark fenced-code state machine, shared by every line-level parser of
|
|
5
17
|
* backlog markdown. `inside(line)` feeds one line and returns whether it is fence syntax or
|
|
6
18
|
* fenced content — never markdown structure: a fence opens at a ```` ``` ````/`~~~` line (an
|
|
@@ -41,20 +53,32 @@ export function fenceTracker() {
|
|
|
41
53
|
},
|
|
42
54
|
};
|
|
43
55
|
}
|
|
56
|
+
/** The trimmed title of `line` when it is a markdown heading line at `prefix` level and not
|
|
57
|
+
* quoted content — null otherwise, including when fenceTracker reports the line inside a fenced
|
|
58
|
+
* code block. The single home of the fence-aware heading guard (`!fenced.inside(line) &&
|
|
59
|
+
* line.startsWith(prefix)` plus the slice/trim) that every line-level reader of the backlog
|
|
60
|
+
* docs repeats: sectionLines' section boundaries and parseEntryDetails' entry boundaries here,
|
|
61
|
+
* and question-commands.ts's walks of QUESTIONS.md (the Open/Answered scan, the `### ` block
|
|
62
|
+
* split, and the Answered section's start); that file's two pure next-`## ` boundary walks
|
|
63
|
+
* go through nextSectionHeading below instead. Readers that walk
|
|
64
|
+
* whole documents collecting heading lines go through fenceAwareHeadingLines instead; callers
|
|
65
|
+
* that only need "is this a heading" pass a tracker and compare the title or test for null. */
|
|
66
|
+
export function fencedHeadingTitle(line, fenced, prefix) {
|
|
67
|
+
return !fenced.inside(line) && line.startsWith(prefix) ? line.slice(prefix.length).trim() : null;
|
|
68
|
+
}
|
|
44
69
|
/** The body lines of the `## <sectionTitle>` section of a markdown document: everything
|
|
45
70
|
* between that heading line and the next `## ` line (or EOF), neither boundary included. A
|
|
46
71
|
* `## ` line inside a fenced code block (entries quote markdown templates and shell traces) is
|
|
47
72
|
* body content, never a boundary. The single home of "where a section starts and ends" — every
|
|
48
73
|
* reader of a `## ` section (backlog entry parsing here, the usage report's Done/Fixed date
|
|
49
|
-
* scan in src/report-data.ts, and backlog-structure.ts's strandedPlanEntries) walks its
|
|
74
|
+
* scan in src/report/report-data.ts, and backlog-structure.ts's strandedPlanEntries) walks its
|
|
50
75
|
* section through this, so independent readers can never disagree about the boundary. */
|
|
51
76
|
export function sectionLines(md, sectionTitle) {
|
|
52
77
|
const lines = [];
|
|
53
78
|
let inSection = false;
|
|
54
79
|
const fenced = fenceTracker();
|
|
55
80
|
for (const line of md.split("\n")) {
|
|
56
|
-
|
|
57
|
-
if (!inFence && line.startsWith("## ")) {
|
|
81
|
+
if (fencedHeadingTitle(line, fenced, "## ") !== null) {
|
|
58
82
|
inSection = line.slice(3).trim() === sectionTitle;
|
|
59
83
|
continue;
|
|
60
84
|
}
|
|
@@ -63,6 +87,20 @@ export function sectionLines(md, sectionTitle) {
|
|
|
63
87
|
}
|
|
64
88
|
return lines;
|
|
65
89
|
}
|
|
90
|
+
/** The index of the next `## ` section heading at or after `from`, fence-aware through the
|
|
91
|
+
* caller's `fenced` tracker — or `lines.length` when none follows. The section-end boundary
|
|
92
|
+
* rule as an index, for readers that must cut or splice at the boundary rather than collect
|
|
93
|
+
* its content (question-commands.ts cuts the Open section at its end and inserts a moved
|
|
94
|
+
* block before Answered's next `## `; backlog-write.ts cuts the section a filed entry is
|
|
95
|
+
* appended to). Shares fencedHeadingTitle's guard with sectionLines, so
|
|
96
|
+
* a boundary found here is the same boundary sectionLines would stop at. */
|
|
97
|
+
export function nextSectionHeading(lines, from, fenced) {
|
|
98
|
+
for (let i = from; i < lines.length; i++) {
|
|
99
|
+
if (fencedHeadingTitle(lines[i] ?? "", fenced, "## ") !== null)
|
|
100
|
+
return i;
|
|
101
|
+
}
|
|
102
|
+
return lines.length;
|
|
103
|
+
}
|
|
66
104
|
/** The body lines of one `## <sectionTitle>` section with fenced content stripped: the
|
|
67
105
|
* sectionLines walk, then a fresh fenceTracker's filter — the shape every reader that
|
|
68
106
|
* classifies a section's lines as markdown structure (entry headings, bullets, dates) walks.
|
|
@@ -117,7 +155,7 @@ export function parseEntryDetails(md, sectionTitle) {
|
|
|
117
155
|
for (const line of sectionLines(md, sectionTitle)) {
|
|
118
156
|
if (fenced.inside(line))
|
|
119
157
|
bodyLines.push(line);
|
|
120
|
-
else if (line
|
|
158
|
+
else if (fencedHeadingTitle(line, fenced, "### ") !== null) {
|
|
121
159
|
close();
|
|
122
160
|
title = line.slice(4).trim();
|
|
123
161
|
}
|
|
@@ -179,7 +217,7 @@ export function entryDates(md, sectionTitle, dateRe) {
|
|
|
179
217
|
* paragraph is body, never matched). Joining with a space keeps "done\n2026-…" matchable.
|
|
180
218
|
* Returns the joined text and the index of the first line NOT consumed as metadata, so a
|
|
181
219
|
* walker can resume its scan there. Extracted from entryDates (its only original caller) so
|
|
182
|
-
* the stranded-plan detector (src/backlog-structure.ts) matches dates against exactly the
|
|
220
|
+
* the stranded-plan detector (src/backlog/backlog-structure.ts) matches dates against exactly the
|
|
183
221
|
* same joined text instead of growing a second, drifting copy of the join rule. */
|
|
184
222
|
export function headingMetadata(lines, start) {
|
|
185
223
|
const line = lines[start] ?? "";
|
|
@@ -197,14 +235,16 @@ export function headingMetadata(lines, start) {
|
|
|
197
235
|
}
|
|
198
236
|
return { text: meta.join(" "), next: j };
|
|
199
237
|
}
|
|
200
|
-
/** A `- ` line's trailing parenthetical's inner text, nesting-aware: a
|
|
201
|
-
*
|
|
202
|
-
* that quotes a parenthetical of its own — "(planned …, done …; commit
|
|
203
|
-
* after review fix))" — still yields its date-bearing text. The previous
|
|
204
|
-
* match saw only the innermost group ("" when the line ended in two closes)
|
|
205
|
-
* dropped the epitaph's date from the day report (BUGS.md 2026-09-29). Unbalanced
|
|
206
|
-
* matching open paren) yields "" — the guard then treats the bullet as body text, as
|
|
207
|
-
|
|
238
|
+
/** A `- ` line's or `### ` heading's trailing parenthetical's inner text, nesting-aware: a
|
|
239
|
+
* backward scan from the text's closing ")" to its matching "(" returns the group's FULL inner
|
|
240
|
+
* text, so an epitaph that quotes a parenthetical of its own — "(planned …, done …; commit
|
|
241
|
+
* abc1234 (re-landed after review fix))" — still yields its date-bearing text. The previous
|
|
242
|
+
* flat `\([^()]*\)$` match saw only the innermost group ("" when the line ended in two closes)
|
|
243
|
+
* and silently dropped the epitaph's date from the day report (BUGS.md 2026-09-29). Unbalanced
|
|
244
|
+
* text (no matching open paren) yields "" — the guard then treats the bullet as body text, as
|
|
245
|
+
* before. The one home for this rule: entryDates reads it for `- ` epitaphs and
|
|
246
|
+
* backlog-eligibility.ts's requiredParts for a heading's prerequisite clause. */
|
|
247
|
+
export function trailingParenthetical(line) {
|
|
208
248
|
if (!line.endsWith(")"))
|
|
209
249
|
return "";
|
|
210
250
|
let depth = 0;
|
|
@@ -228,58 +268,3 @@ function lastDate(text, dateRe) {
|
|
|
228
268
|
const all = [...text.matchAll(global)];
|
|
229
269
|
return all[all.length - 1]?.[1] ?? null;
|
|
230
270
|
}
|
|
231
|
-
/** Parsed sections keyed by file + section title (a future reader of a second section from the
|
|
232
|
-
* same file must not collide with the first). Bounded inside cachedByStat: many short-lived
|
|
233
|
-
* roots in tests would otherwise accumulate. Stores the richer {title, body} shape — one parse
|
|
234
|
-
* per file change subsumes both the titles-only and the full-entry reads. */
|
|
235
|
-
const sectionCache = new Map();
|
|
236
|
-
/** The entries under `<root>/<fileName>`'s `## <sectionTitle>`: fresh when the file's identity
|
|
237
|
-
* or mtime/size changed since the last read, cached otherwise (see module docs). A missing or
|
|
238
|
-
* unreadable file yields [] — a render path must never throw on backlog state. */
|
|
239
|
-
function sectionEntries(root, fileName, sectionTitle) {
|
|
240
|
-
const file = path.join(root, fileName);
|
|
241
|
-
return (cachedByStat(sectionCache, `${file}\u0000${sectionTitle}`, file, () => {
|
|
242
|
-
const md = readTextOrNull(file); // Missing or unreadable — no data.
|
|
243
|
-
return md === null ? null : parseEntryDetails(md, sectionTitle);
|
|
244
|
-
}, (entries) => entries.map((e) => ({ ...e }))) ?? []);
|
|
245
|
-
}
|
|
246
|
-
/** Planned features: the `### ` headings under PLANS.md's `## Planned` section. Missing or
|
|
247
|
-
* unreadable → []. */
|
|
248
|
-
export function plannedPlans(root) {
|
|
249
|
-
return sectionEntries(root, "PLANS.md", "Planned").map((e) => e.title);
|
|
250
|
-
}
|
|
251
|
-
/** Open bugs: the `### ` headings under BUGS.md's `## Open` section. Missing or unreadable → []. */
|
|
252
|
-
export function openBugs(root) {
|
|
253
|
-
return sectionEntries(root, "BUGS.md", "Open").map((e) => e.title);
|
|
254
|
-
}
|
|
255
|
-
/** Open questions: the `### ` headings under QUESTIONS.md's `## Open` section — loops post
|
|
256
|
-
* them when a decision is genuinely the user's (see plans/questions-outbox.md). Missing or
|
|
257
|
-
* unreadable → []. */
|
|
258
|
-
export function openQuestions(root) {
|
|
259
|
-
return sectionEntries(root, "QUESTIONS.md", "Open").map((e) => e.title);
|
|
260
|
-
}
|
|
261
|
-
/** Planned features as full entries (title + body), in file order — the TUI's project-status
|
|
262
|
-
* browse and the GUI's /api/backlog endpoint read these. Missing or unreadable → []. */
|
|
263
|
-
export function plannedPlanEntries(root) {
|
|
264
|
-
return sectionEntries(root, "PLANS.md", "Planned");
|
|
265
|
-
}
|
|
266
|
-
/** Open bugs as full entries (title + body), in file order. Missing or unreadable → []. */
|
|
267
|
-
export function openBugEntries(root) {
|
|
268
|
-
return sectionEntries(root, "BUGS.md", "Open");
|
|
269
|
-
}
|
|
270
|
-
/** Open questions as full entries (title + body), in file order. Missing or unreadable → []. */
|
|
271
|
-
export function openQuestionEntries(root) {
|
|
272
|
-
return sectionEntries(root, "QUESTIONS.md", "Open");
|
|
273
|
-
}
|
|
274
|
-
/** The backlog as one machine-readable document: the three entry arrays the Markdown renderer
|
|
275
|
-
* and the GUI's /api/backlog endpoint serve, so `tumwater backlog --json` prints the same data
|
|
276
|
-
* every surface reads (status --json's "print the endpoint's payload" pattern). Each array keeps
|
|
277
|
-
* file order and {title, body} verbatim, and a missing or unreadable file degrades to [] like the
|
|
278
|
-
* individual readers — a bare directory yields the all-empty object, never an error. */
|
|
279
|
-
export function backlogPayload(root) {
|
|
280
|
-
return {
|
|
281
|
-
plans: plannedPlanEntries(root),
|
|
282
|
-
bugs: openBugEntries(root),
|
|
283
|
-
questions: openQuestionEntries(root),
|
|
284
|
-
};
|
|
285
|
-
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* backlog.ts entry readers with their cache behavior, placeholder skipping, and Done/Fixed
|
|
5
5
|
* exclusion the dashboards rely on — so the terminal view cannot drift from the dashboard
|
|
6
6
|
* view (one parser, two surfaces) and the CLI's --json/human branches share one collection
|
|
7
|
-
* (sayJsonOrRender's thunk) instead of each reading the entry files afresh. Entries render
|
|
8
|
-
*
|
|
7
|
+
* (sayJsonOrRender's thunk) instead of each reading the entry files afresh. Entries render
|
|
8
|
+
* verbatim — heading text with its `(planned …)`/`(reported …)` suffix, body lines
|
|
9
|
+
* indented two spaces under it — because these are markdown the loops
|
|
9
10
|
* wrote, including their Goal/Approach/Acceptance-criteria structure; reflowing them here
|
|
10
11
|
* would make this command a worse reader of its own backlog than the files it summarizes. An
|
|
11
12
|
* empty section renders a single `_(none)_` line rather than disappearing, so an all-clear
|