tumwater 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +143 -2
- package/dist/build-info.json +5 -0
- package/dist/src/backlog/backlog-eligibility.js +141 -0
- package/dist/src/backlog/backlog-md.js +270 -0
- package/dist/src/backlog/backlog-render.js +37 -0
- 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/baseline/main-baseline.js +220 -0
- package/dist/src/baseline/main-red.js +171 -0
- package/dist/src/brief.js +127 -0
- package/dist/src/budget/budget.js +152 -0
- package/dist/src/budget/fallback-breaker.js +175 -0
- package/dist/src/build/build-check-counts.js +46 -0
- package/dist/src/build/build-check-detect.js +117 -0
- package/dist/src/build/build-check-events.js +178 -0
- package/dist/src/build/build-check-report.js +120 -0
- package/dist/src/build/build-check-scoped.js +130 -0
- package/dist/src/build/build-check.js +175 -0
- package/dist/src/build/build-info.js +72 -0
- package/dist/src/build/build-stage.js +251 -0
- package/dist/src/build/dep-install.js +114 -0
- package/dist/src/build/host-sleep.js +86 -0
- package/dist/src/change/change-data.js +93 -0
- package/dist/src/change/change-render.js +49 -0
- package/dist/src/cli/cli-args.js +235 -0
- package/dist/src/cli/cli-command-args.js +366 -0
- 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/cli-run.js +355 -0
- package/dist/src/cli/config-commands.js +97 -0
- package/dist/src/cli/help.js +261 -0
- package/dist/src/cli/log-commands.js +195 -0
- package/dist/src/cli/question-commands.js +232 -0
- package/dist/src/cli.js +290 -0
- package/dist/src/collections.js +71 -0
- package/dist/src/concurrency/check-permit.js +55 -0
- package/dist/src/concurrency/lock.js +236 -0
- package/dist/src/concurrency/semaphore.js +85 -0
- package/dist/src/config/config-editable-keys.js +15 -0
- package/dist/src/config/config-example.js +84 -0
- package/dist/src/config/config-field-checks.js +199 -0
- package/dist/src/config/config-live.js +130 -0
- package/dist/src/config/config-schema.js +135 -0
- 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/config.js +352 -0
- package/dist/src/config/model-selector.js +43 -0
- package/dist/src/doctor/doctor-backlog.js +159 -0
- package/dist/src/doctor/doctor-checks.js +393 -0
- package/dist/src/doctor/doctor-launch-services.js +24 -0
- package/dist/src/doctor/doctor-model-checks.js +254 -0
- package/dist/src/doctor/doctor-orphans.js +217 -0
- package/dist/src/doctor/doctor-render.js +13 -0
- package/dist/src/doctor/doctor.js +67 -0
- package/dist/src/errno.js +14 -0
- package/dist/src/events/event-format.js +392 -0
- package/dist/src/events/event-read.js +178 -0
- package/dist/src/events/event-window.js +221 -0
- package/dist/src/events/events.js +154 -0
- package/dist/src/events/notify.js +84 -0
- package/dist/src/failure/failure-cluster.js +143 -0
- package/dist/src/failure/failure-data.js +250 -0
- package/dist/src/failure/failure-render.js +280 -0
- package/dist/src/failure/failure-state-change.js +177 -0
- package/dist/src/failure/time-spend.js +201 -0
- package/dist/src/files/file-queue.js +48 -0
- package/dist/src/files/files.js +270 -0
- package/dist/src/files/json-files.js +51 -0
- package/dist/src/files/json-object.js +77 -0
- package/dist/src/files/stat-cache.js +33 -0
- package/dist/src/files/tail.js +282 -0
- package/dist/src/fleet/error-storm.js +63 -0
- package/dist/src/fleet/failure-spread.js +61 -0
- package/dist/src/fleet/fleet-hold.js +139 -0
- package/dist/src/fleet/fleet-polls.js +133 -0
- package/dist/src/fleet/fleet-state.js +251 -0
- 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/gates/pause-gates.js +41 -0
- package/dist/src/gates/readiness.js +11 -0
- package/dist/src/gates/role-cap-gates.js +92 -0
- package/dist/src/gates/startup-gate.js +91 -0
- package/dist/src/gates/streak-gate.js +86 -0
- package/dist/src/git/commit-message.js +155 -0
- package/dist/src/git/git-diff.js +202 -0
- package/dist/src/git/git-run.js +235 -0
- package/dist/src/git/git.js +286 -0
- 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/gui/gui-args.js +124 -0
- package/dist/src/gui/gui-command.js +86 -0
- package/dist/src/gui/gui-endpoint-commands.js +309 -0
- package/dist/src/gui/gui-endpoints.js +167 -0
- package/dist/src/gui/gui-server.js +256 -0
- package/dist/src/gui/http-body.js +137 -0
- package/dist/src/history/history-data.js +217 -0
- package/dist/src/history/history.js +156 -0
- 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/inbox-submit.js +100 -0
- package/dist/src/inbox/inbox.js +288 -0
- package/dist/src/inbox/pending-prompt.js +96 -0
- package/dist/src/inbox/prompt-commands.js +278 -0
- package/dist/src/inbox/prompt-not-before.js +61 -0
- package/dist/src/init/init-templates.js +152 -0
- package/dist/src/init/init.js +270 -0
- package/dist/src/landing/backlog-conflicts.js +116 -0
- package/dist/src/landing/landing-batch.js +266 -0
- package/dist/src/landing/landing-check-failures.js +141 -0
- package/dist/src/landing/landing-core.js +316 -0
- package/dist/src/landing/landing-diff.js +57 -0
- package/dist/src/landing/landing-drain.js +159 -0
- package/dist/src/landing/landing-git.js +191 -0
- package/dist/src/landing/landing-merge.js +255 -0
- package/dist/src/landing/landing-pipeline.js +82 -0
- package/dist/src/landing/landing-questions.js +16 -0
- package/dist/src/landing/landing-queue.js +123 -0
- package/dist/src/landing/landing-slot.js +229 -0
- package/dist/src/landing/landing-stack.js +216 -0
- package/dist/src/landing/landing-vetting.js +195 -0
- package/dist/src/loop/leftover.js +128 -0
- package/dist/src/loop/loop-pi.js +282 -0
- 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/operator-commands.js +301 -0
- package/dist/src/operator/operator-intent.js +230 -0
- 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/orchestrator.js +448 -0
- package/dist/src/orchestrator/retention.js +66 -0
- package/dist/src/paths.js +319 -0
- package/dist/src/pi/command-shape.js +52 -0
- package/dist/src/pi/pi-args.js +41 -0
- package/dist/src/pi/pi-bin.js +78 -0
- package/dist/src/pi/pi-event-line.js +100 -0
- package/dist/src/pi/pi-models.js +153 -0
- package/dist/src/pi/pi-run-result.js +1 -0
- package/dist/src/pi/pi-stream.js +373 -0
- package/dist/src/pi/pi-watchdogs.js +176 -0
- package/dist/src/pi/pi.js +304 -0
- package/dist/src/pi-extension/bounded-output.js +158 -0
- package/dist/src/pi-extension/context-budget.js +79 -0
- 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/process/launch-services.js +85 -0
- package/dist/src/process/process-group.js +180 -0
- package/dist/src/process/process-table.js +167 -0
- package/dist/src/process/process.js +164 -0
- package/dist/src/process/run-marker.js +176 -0
- package/dist/src/process/supervisor.js +148 -0
- package/dist/src/project-name.js +10 -0
- package/dist/src/prompt/principles.js +14 -0
- package/dist/src/prompt/prompt-followup.js +77 -0
- package/dist/src/prompt/prompt.js +332 -0
- package/dist/src/redeploy/redeploy-policy.js +71 -0
- package/dist/src/redeploy/redeploy-probes.js +75 -0
- package/dist/src/redeploy/redeploy.js +88 -0
- package/dist/src/redeploy/redeployer.js +570 -0
- package/dist/src/redeploy/self-reload.js +153 -0
- package/dist/src/report/report-data.js +325 -0
- package/dist/src/report/report-render.js +137 -0
- package/dist/src/report/report.js +55 -0
- package/dist/src/request-timeouts.js +6 -0
- package/dist/src/review/exemptions.js +59 -0
- package/dist/src/review/known-flakes.js +45 -0
- package/dist/src/review/review-followup.js +70 -0
- package/dist/src/review/review-precheck.js +142 -0
- package/dist/src/review/review-verdict.js +80 -0
- package/dist/src/review/review.js +362 -0
- package/dist/src/review/suite-rerun.js +178 -0
- package/dist/src/roles/loop-ids.js +61 -0
- package/dist/src/roles/role-catalog.js +485 -0
- package/dist/src/roles/role-guidance.js +126 -0
- package/dist/src/roles/role-render.js +74 -0
- package/dist/src/roles/role-view.js +84 -0
- package/dist/src/roles/roles.js +135 -0
- package/dist/src/scheduling/backoff.js +123 -0
- package/dist/src/scheduling/claims.js +104 -0
- package/dist/src/scheduling/once-round.js +92 -0
- package/dist/src/scheduling/quiet-hours.js +135 -0
- package/dist/src/scheduling/scheduling.js +158 -0
- package/dist/src/scheduling/work-landed-cache.js +51 -0
- package/dist/src/status/status-data.js +225 -0
- package/dist/src/status/status-polls.js +202 -0
- package/dist/src/text/datetime.js +137 -0
- 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/text-width.js +77 -0
- package/dist/src/text/text.js +199 -0
- package/dist/src/tick/qa-coverage.js +99 -0
- package/dist/src/tick/stage-check.js +276 -0
- package/dist/src/tick/telemetry-digest.js +24 -0
- package/dist/src/tick/tick-apply.js +328 -0
- package/dist/src/tick/tick-detail-data.js +94 -0
- package/dist/src/tick/tick-detail.js +122 -0
- package/dist/src/tick/tick-finalize.js +119 -0
- package/dist/src/tick/tick-outcome.js +13 -0
- package/dist/src/tick/tick-prompt.js +189 -0
- package/dist/src/tick/tick-resume.js +54 -0
- package/dist/src/tick/tick-stage.js +181 -0
- package/dist/src/tick/tick-timing.js +185 -0
- package/dist/src/tick/tick-usage.js +112 -0
- package/dist/src/tick/tick-verdict.js +155 -0
- package/dist/src/ui/badges.js +229 -0
- package/dist/src/ui/fleet-alerts.js +174 -0
- package/dist/src/ui/gui/gui-client-boot.js +196 -0
- package/dist/src/ui/gui/gui-client-composer.js +227 -0
- package/dist/src/ui/gui/gui-client-drawer.js +271 -0
- package/dist/src/ui/gui/gui-client-fleet.js +323 -0
- package/dist/src/ui/gui/gui-client-history.js +196 -0
- package/dist/src/ui/gui/gui-client-loops.js +153 -0
- package/dist/src/ui/gui/gui-client-markdown.js +159 -0
- package/dist/src/ui/gui/gui-client-model.js +167 -0
- package/dist/src/ui/gui/gui-client-operator.js +224 -0
- package/dist/src/ui/gui/gui-client-pending.js +77 -0
- package/dist/src/ui/gui/gui-client-report.js +280 -0
- package/dist/src/ui/gui/gui-client-settings.js +72 -0
- package/dist/src/ui/gui/gui-client-sound.js +52 -0
- package/dist/src/ui/gui/gui-client.js +314 -0
- package/dist/src/ui/gui/gui-icons.js +49 -0
- package/dist/src/ui/gui/gui-page.js +126 -0
- package/dist/src/ui/gui/gui-styles.js +523 -0
- package/dist/src/ui/progress-data.js +273 -0
- package/dist/src/ui/status-model.js +341 -0
- package/dist/src/ui/status-payload.js +229 -0
- package/dist/src/ui/status-render.js +360 -0
- package/dist/src/ui/tick-progress-model.js +117 -0
- package/dist/src/ui/tone.js +72 -0
- package/dist/src/ui/transcript-tail.js +208 -0
- package/dist/src/ui/transcript.js +229 -0
- package/dist/src/ui/tui/tui-app.js +44 -0
- package/dist/src/ui/tui/tui-backlog.js +81 -0
- package/dist/src/ui/tui/tui-frame.js +122 -0
- package/dist/src/ui/tui/tui-input.js +250 -0
- package/dist/src/ui/tui/tui-keymap.js +69 -0
- package/dist/src/ui/tui/tui-keys.js +451 -0
- 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/tui.js +164 -0
- package/dist/src/verdict/fix-claim.js +200 -0
- package/dist/src/verdict/no-change.js +22 -0
- package/dist/src/verdict/refusal.js +59 -0
- package/dist/src/verdict/reply-contract.js +221 -0
- package/dist/src/version.js +70 -0
- package/package.json +53 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the tumwater authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,144 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tumwater
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
tumwater is an opinionated autonomous development harness built on
|
|
4
|
+
[pi](https://github.com/badlogic/pi-mono). You write a short project brief, and a fleet of
|
|
5
|
+
role-driven loops (feature, bugfix, planning, tests, cleanup, docs, and more) builds the project
|
|
6
|
+
one small, reviewed commit at a time. It came from wanting to write only the brief and let a team
|
|
7
|
+
of always-on specialists do the rest: each loop owns one concern, lands one change per tick,
|
|
8
|
+
sleeps when it has nothing to do, and wakes when main moves. All project state lives in the
|
|
9
|
+
local git repo, and no remote is ever touched.
|
|
10
|
+
|
|
11
|
+

|
|
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
|
+
|
|
24
|
+
## Status
|
|
25
|
+
|
|
26
|
+
<!-- tumwater:status:start -->
|
|
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`).
|
|
29
|
+
|
|
30
|
+
Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
|
|
31
|
+
[QUESTIONS.md](QUESTIONS.md) (open questions).
|
|
32
|
+
<!-- tumwater:status:end -->
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd your-project # a new or existing directory
|
|
38
|
+
tumwater init "Build a tiny markdown-to-html converter CLI in Python."
|
|
39
|
+
# add --template <id> to seed from a bundled starting point
|
|
40
|
+
# (blank, python-cli, node-cli, static-site); --list-templates
|
|
41
|
+
# prints the catalog
|
|
42
|
+
# add --file <path> to read the brief from a file
|
|
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
|
|
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
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then, from another terminal:
|
|
54
|
+
|
|
55
|
+
| To | Run |
|
|
56
|
+
| --- | --- |
|
|
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)) |
|
|
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 |
|
|
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`) |
|
|
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 |
|
|
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) |
|
|
65
|
+
|
|
66
|
+
`tumwater help` lists every command and flag; `tumwater help <command>` shows one command's usage. `gui --all-interfaces` exposes the dashboard, and
|
|
67
|
+
with it the director prompt, to your whole network, so pair it with `--token <secret>`.
|
|
68
|
+
|
|
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
|
|
74
|
+
(`maxDailyCostUsd`, with optional per-role caps `maxDailyCostUsdPerRole` — a loop over its own
|
|
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
|
|
80
|
+
(e.g. `"23:00-07:00"` local time) during
|
|
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
|
|
92
|
+
optional `notify` shell command run when the fleet needs a human (a budget pause, a budget warning
|
|
93
|
+
at 80% of the cap while the gate is still open, an error-streak
|
|
94
|
+
breaker trip, a failed landing, a blocked restart — the command gets `TUMWATER_EVENT_TYPE`,
|
|
95
|
+
`TUMWATER_EVENT_LOOP`, and `TUMWATER_EVENT_MESSAGE` in its environment).
|
|
96
|
+
Edits apply live while the fleet runs.
|
|
97
|
+
From the terminal, `tumwater config` prints the effective config as JSON, `tumwater config get
|
|
98
|
+
<key>` reads one resolved value, and `tumwater config set <key> <value>` writes one top-level
|
|
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.
|
|
110
|
+
|
|
111
|
+
For how the loops, review gate, scheduling, and self-redeploy work, see
|
|
112
|
+
[docs/how-it-works.md](docs/how-it-works.md). For a measured comparison of tumwater's own
|
|
113
|
+
code against human-written open source, see [docs/code-metrics.md](docs/code-metrics.md).
|
|
114
|
+
|
|
115
|
+
## Appendix: initial prompt
|
|
116
|
+
|
|
117
|
+
The brief this repository was started from, kept for history. The harness still reads it from
|
|
118
|
+
between the markers below on every tick.
|
|
119
|
+
|
|
120
|
+
<!-- tumwater:prompt:start -->
|
|
121
|
+
Idea: agentic harness
|
|
122
|
+
|
|
123
|
+
Opinionated. Built on pi. Lots of autonomous loops. You only write the markdown/initial prompt.
|
|
124
|
+
It builds the project with immense effort. First puts the initial prompt and project status into
|
|
125
|
+
README.md. Background loops are observable by gui/tui/log. GUI/TUI also gives user a main prompt.
|
|
126
|
+
Loop sleeps a while when the prompt results in no further changes. Starts again after a while to
|
|
127
|
+
see if the answer has changed due to the new state of the world. Each run attempts to find
|
|
128
|
+
something to do, do one thing, commit, merge to main. The find-something-to-do part is role
|
|
129
|
+
specific. Each loop has a role:
|
|
130
|
+
|
|
131
|
+
- Make the code more organized
|
|
132
|
+
- Increase unit test code coverage
|
|
133
|
+
- Make the code cleaner
|
|
134
|
+
- Make the code less repetitive
|
|
135
|
+
- Implement a planned feature (tracked in PLANS.md)
|
|
136
|
+
- Fix a bug (tracked in BUGS.md in repo)
|
|
137
|
+
- Plan a feature (write markdown plan, add to PLANS.md)
|
|
138
|
+
- Keep the README up to date
|
|
139
|
+
- Make an improvement to the code
|
|
140
|
+
|
|
141
|
+
Assumptions: run within a git repo dir. Each loop uses a persistent git workspace and branch.
|
|
142
|
+
Each loop keeps itself synced up with git main. Don't involve git remotes at all; do everything
|
|
143
|
+
locally and keep all project state within the git repo.
|
|
144
|
+
<!-- tumwater:prompt:end -->
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
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"]);
|
|
16
|
+
/** A per-line CommonMark fenced-code state machine, shared by every line-level parser of
|
|
17
|
+
* backlog markdown. `inside(line)` feeds one line and returns whether it is fence syntax or
|
|
18
|
+
* fenced content — never markdown structure: a fence opens at a ```` ``` ````/`~~~` line (an
|
|
19
|
+
* info string is allowed, except that a backtick fence's info string may not contain a
|
|
20
|
+
* backtick — such a line is paragraph text that opens nothing), closes only at a bare fence
|
|
21
|
+
* line of the same character at least as long, and an unclosed fence runs to EOF. Every reader
|
|
22
|
+
* that classifies backlog lines as markdown structure (section boundaries, entry headings,
|
|
23
|
+
* bullets) must consult this, so two readers can never disagree about what is body content.
|
|
24
|
+
* `open()` reports whether the tracker is inside a fence where the walk stopped — true when a
|
|
25
|
+
* fence ran unclosed to the walk's end, so a caller that walked a bounded region can tell its
|
|
26
|
+
* read is fence-degraded (the region's tail quoted real structure). */
|
|
27
|
+
export function fenceTracker() {
|
|
28
|
+
// The open fence's marker (null = none): only a matching bare fence line closes it.
|
|
29
|
+
let fence = null;
|
|
30
|
+
return {
|
|
31
|
+
open() {
|
|
32
|
+
return fence !== null;
|
|
33
|
+
},
|
|
34
|
+
inside(line) {
|
|
35
|
+
const fenceLine = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
36
|
+
if (fenceLine) {
|
|
37
|
+
const marker = fenceLine[1] ?? ""; // The group always participates; "" keeps types honest.
|
|
38
|
+
if (fence === null) {
|
|
39
|
+
// CommonMark: an info string for a backtick fence cannot contain a backtick, so a
|
|
40
|
+
// line like ````md / ## Done / ````` quoted in prose is paragraph text, not a fence
|
|
41
|
+
// opener — taken as one, no later bare fence line can close it and the fence runs to
|
|
42
|
+
// EOF, swallowing the rest of the document as fenced content.
|
|
43
|
+
const info = line.replace(/^ {0,3}/, "").slice(marker.length);
|
|
44
|
+
if (marker.charAt(0) === "`" && info.includes("`"))
|
|
45
|
+
return false;
|
|
46
|
+
fence = { char: marker.charAt(0), length: marker.length };
|
|
47
|
+
}
|
|
48
|
+
else if (marker.charAt(0) === fence.char && marker.length >= fence.length && line.trim() === marker)
|
|
49
|
+
fence = null;
|
|
50
|
+
return true; // The fence line itself is fence syntax, never structure.
|
|
51
|
+
}
|
|
52
|
+
return fence !== null;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
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
|
+
}
|
|
69
|
+
/** The body lines of the `## <sectionTitle>` section of a markdown document: everything
|
|
70
|
+
* between that heading line and the next `## ` line (or EOF), neither boundary included. A
|
|
71
|
+
* `## ` line inside a fenced code block (entries quote markdown templates and shell traces) is
|
|
72
|
+
* body content, never a boundary. The single home of "where a section starts and ends" — every
|
|
73
|
+
* reader of a `## ` section (backlog entry parsing here, the usage report's Done/Fixed date
|
|
74
|
+
* scan in src/report/report-data.ts, and backlog-structure.ts's strandedPlanEntries) walks its
|
|
75
|
+
* section through this, so independent readers can never disagree about the boundary. */
|
|
76
|
+
export function sectionLines(md, sectionTitle) {
|
|
77
|
+
const lines = [];
|
|
78
|
+
let inSection = false;
|
|
79
|
+
const fenced = fenceTracker();
|
|
80
|
+
for (const line of md.split("\n")) {
|
|
81
|
+
if (fencedHeadingTitle(line, fenced, "## ") !== null) {
|
|
82
|
+
inSection = line.slice(3).trim() === sectionTitle;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (inSection)
|
|
86
|
+
lines.push(line);
|
|
87
|
+
}
|
|
88
|
+
return lines;
|
|
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
|
+
}
|
|
104
|
+
/** The body lines of one `## <sectionTitle>` section with fenced content stripped: the
|
|
105
|
+
* sectionLines walk, then a fresh fenceTracker's filter — the shape every reader that
|
|
106
|
+
* classifies a section's lines as markdown structure (entry headings, bullets, dates) walks.
|
|
107
|
+
* Exactly two call sites today: entryDates (below) and backlog-structure.ts's
|
|
108
|
+
* strandedPlanEntries. The fresh tracker is in sync with the document here: sectionLines
|
|
109
|
+
* only recognizes a `## ` boundary outside a fence, so the section's heading line is
|
|
110
|
+
* non-fenced and the fence state at the section's first line is closed — a tracker started
|
|
111
|
+
* there sees exactly what a document-wide tracker sees inside the section. Readers that
|
|
112
|
+
* must KEEP fenced lines as body content (parseEntryDetails) do not use this. */
|
|
113
|
+
export function sectionBodyLines(md, sectionTitle) {
|
|
114
|
+
const fenced = fenceTracker();
|
|
115
|
+
return sectionLines(md, sectionTitle).filter((line) => !fenced.inside(line));
|
|
116
|
+
}
|
|
117
|
+
/** The heading lines of `md` starting with `prefix` (a `"## "` section heading or a `"### "`
|
|
118
|
+
* entry heading), in file order, fence-aware (fenceTracker): a heading line quoted inside a
|
|
119
|
+
* fenced code block is body text, never structure. The single home of the whole-document
|
|
120
|
+
* prefix-heading walk — backlog-structure.ts's sectionTitles ("## ") and planHeadingKeys
|
|
121
|
+
* ("### ") each carried their own tracker before, so their fence handling could drift from
|
|
122
|
+
* sectionLines'. Callers slice and trim the prefix themselves. (sectionLines does not use
|
|
123
|
+
* this: its walk must track which section it is inside, not just collect headings; readers
|
|
124
|
+
* inside one section go through sectionLines/sectionBodyLines instead.) */
|
|
125
|
+
export function fenceAwareHeadingLines(md, prefix) {
|
|
126
|
+
const fenced = fenceTracker();
|
|
127
|
+
const lines = [];
|
|
128
|
+
for (const line of md.split("\n")) {
|
|
129
|
+
if (fenced.inside(line))
|
|
130
|
+
continue;
|
|
131
|
+
if (line.startsWith(prefix))
|
|
132
|
+
lines.push(line);
|
|
133
|
+
}
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
/** The entries inside one `## <sectionTitle>` section of a markdown document, each with its
|
|
137
|
+
* full body: stops at the next `## ` line (so Done/Fixed entries never leak in), skips
|
|
138
|
+
* non-heading placeholders like `_None yet._` and any prose before the first heading, keeps
|
|
139
|
+
* interior blank lines within a body while trimming leading/trailing ones, and ends an open
|
|
140
|
+
* entry's body at EOF as well as at the next heading. */
|
|
141
|
+
export function parseEntryDetails(md, sectionTitle) {
|
|
142
|
+
const entries = [];
|
|
143
|
+
let title = null; // The open entry's heading (null = no entry open yet).
|
|
144
|
+
let bodyLines = [];
|
|
145
|
+
const close = () => {
|
|
146
|
+
if (title !== null)
|
|
147
|
+
entries.push({ title, body: bodyLines.join("\n").trim() });
|
|
148
|
+
title = null;
|
|
149
|
+
bodyLines = []; // Prose before the first heading never becomes a body.
|
|
150
|
+
};
|
|
151
|
+
// A `### ` line inside a fenced code block is quoted content, never an entry boundary: an
|
|
152
|
+
// entry quoting a markdown template keeps its whole body instead of splitting into a phantom
|
|
153
|
+
// entry at the quoted heading.
|
|
154
|
+
const fenced = fenceTracker();
|
|
155
|
+
for (const line of sectionLines(md, sectionTitle)) {
|
|
156
|
+
if (fenced.inside(line))
|
|
157
|
+
bodyLines.push(line);
|
|
158
|
+
else if (fencedHeadingTitle(line, fenced, "### ") !== null) {
|
|
159
|
+
close();
|
|
160
|
+
title = line.slice(4).trim();
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
bodyLines.push(line);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
close(); // An entry at the end of file ends with EOF, not a heading.
|
|
167
|
+
return entries;
|
|
168
|
+
}
|
|
169
|
+
/** The completion dates ("YYYY-MM-DD") of the entries inside one `## <sectionTitle>` section.
|
|
170
|
+
* An entry starts at a `### ` heading or `- ` bullet line and ends at the next such line; only
|
|
171
|
+
* its METADATA is matched for dates — never its body, so a body's "**Done 2026-…**" recap line
|
|
172
|
+
* (or a prose cross-reference like "(done 2026-…)") cannot double-count. Fenced lines are
|
|
173
|
+
* body content, never entry starts — an entry quoting a markdown template with a
|
|
174
|
+
* `### … (fixed DATE)` heading inside must not count as a completion of its own. Metadata is
|
|
175
|
+
* headingMetadata's join (below). Entries without a parseable date are skipped.
|
|
176
|
+
*
|
|
177
|
+
* A `- ` line needs more than a date to be an entry: the sections also hold body bullets (an
|
|
178
|
+
* entry's repro steps, a plan's task breakdown), and a body bullet that merely mentions a
|
|
179
|
+
* completion — "- same shape as the sibling bug (fixed 2026-09-24)" — is not one. The epitaph
|
|
180
|
+
* shape separates them: an epitaph always closes its line with a parenthetical that records
|
|
181
|
+
* both the completion date and the landing commit ("(planned …, done …; commit abc1234)"),
|
|
182
|
+
* so a bullet counts only when its trailing `(…)` group carries the date AND a `commit`
|
|
183
|
+
* reference; a heading entry keeps the plain metadata match (headings are the primary entry
|
|
184
|
+
* format and always close their metadata parenthetical by convention). */
|
|
185
|
+
export function entryDates(md, sectionTitle, dateRe) {
|
|
186
|
+
const dates = [];
|
|
187
|
+
const lines = sectionBodyLines(md, sectionTitle);
|
|
188
|
+
for (let i = 0; i < lines.length; i++) {
|
|
189
|
+
const line = lines[i] ?? "";
|
|
190
|
+
if (!line.startsWith("### ") && !line.startsWith("- "))
|
|
191
|
+
continue;
|
|
192
|
+
const meta = headingMetadata(lines, i);
|
|
193
|
+
let date = null;
|
|
194
|
+
if (line.startsWith("- ")) {
|
|
195
|
+
// Epitaph guard (see the doc comment): the date must live in the line's trailing
|
|
196
|
+
// parenthetical beside a commit reference, or the bullet is body text, not an entry.
|
|
197
|
+
// Within the parenthetical the completion is the LAST dated verb, matching the heading
|
|
198
|
+
// branch: a decomposition cross-reference ("decomposed from the sibling bug fixed
|
|
199
|
+
// <date>, fixed <date>") precedes the entry's own completion record.
|
|
200
|
+
const tail = trailingParenthetical(line);
|
|
201
|
+
if (/\bcommits?\b/.test(tail))
|
|
202
|
+
date = lastDate(tail, dateRe);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
date = lastDate(meta.text, dateRe);
|
|
206
|
+
}
|
|
207
|
+
if (date)
|
|
208
|
+
dates.push(date);
|
|
209
|
+
i = meta.next - 1; // The loop's ++ resumes at the first line not consumed as metadata.
|
|
210
|
+
}
|
|
211
|
+
return dates;
|
|
212
|
+
}
|
|
213
|
+
/** A `### `/`- ` entry start's METADATA, joined for matching: the start line plus, for `### `
|
|
214
|
+
* headings only, continuation lines up to and including the first line ending in `)` (capped
|
|
215
|
+
* at 3 lines) — wrapped headings carry their date on the second line, while `- ` epitaphs are
|
|
216
|
+
* single-line by construction, so a bullet's own line is its whole metadata (a following prose
|
|
217
|
+
* paragraph is body, never matched). Joining with a space keeps "done\n2026-…" matchable.
|
|
218
|
+
* Returns the joined text and the index of the first line NOT consumed as metadata, so a
|
|
219
|
+
* walker can resume its scan there. Extracted from entryDates (its only original caller) so
|
|
220
|
+
* the stranded-plan detector (src/backlog/backlog-structure.ts) matches dates against exactly the
|
|
221
|
+
* same joined text instead of growing a second, drifting copy of the join rule. */
|
|
222
|
+
export function headingMetadata(lines, start) {
|
|
223
|
+
const line = lines[start] ?? "";
|
|
224
|
+
const meta = [line];
|
|
225
|
+
let closed = line.endsWith(")");
|
|
226
|
+
let j = start + 1;
|
|
227
|
+
// Continuation is a heading-only concern (wrapped headings); bullets are single-line.
|
|
228
|
+
while (line.startsWith("### ") && meta.length < 3 && j < lines.length && !closed) {
|
|
229
|
+
const next = lines[j] ?? "";
|
|
230
|
+
if (next.startsWith("### ") || next.startsWith("- "))
|
|
231
|
+
break; // The entry ends at the next start.
|
|
232
|
+
meta.push(next);
|
|
233
|
+
j++;
|
|
234
|
+
closed = next.endsWith(")");
|
|
235
|
+
}
|
|
236
|
+
return { text: meta.join(" "), next: j };
|
|
237
|
+
}
|
|
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) {
|
|
248
|
+
if (!line.endsWith(")"))
|
|
249
|
+
return "";
|
|
250
|
+
let depth = 0;
|
|
251
|
+
for (let i = line.length - 1; i >= 0; i--) {
|
|
252
|
+
const ch = line[i];
|
|
253
|
+
if (ch === ")")
|
|
254
|
+
depth++;
|
|
255
|
+
else if (ch === "(" && --depth === 0)
|
|
256
|
+
return line.slice(i + 1, -1);
|
|
257
|
+
}
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
/** A text's LAST `<dateRe>` capture — the completion rule both branches of entryDates share:
|
|
261
|
+
* an entry's completion is its LAST dated verb, not the first, because found-by,
|
|
262
|
+
* decomposition, and sibling mentions ("decomposed from the X bug fixed <date>") all precede
|
|
263
|
+
* the completion record, so first-match let a sibling's date steal the entry's count onto the
|
|
264
|
+
* wrong day (BUGS.md 2026-09-29). dateRe is cloned with the g flag so matchAll sees every
|
|
265
|
+
* date; a text with no date yields null. */
|
|
266
|
+
function lastDate(text, dateRe) {
|
|
267
|
+
const global = new RegExp(dateRe.source, dateRe.flags.includes("g") ? dateRe.flags : `${dateRe.flags}g`);
|
|
268
|
+
const all = [...text.matchAll(global)];
|
|
269
|
+
return all[all.length - 1]?.[1] ?? null;
|
|
270
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** The terminal's view of the project backlog — the same three open sections the GUI's
|
|
2
|
+
* /api/backlog endpoint and the TUI's project-status browse show, rendered as Markdown for
|
|
3
|
+
* `tumwater backlog`. The renderer consumes the payload backlogPayload collects — the three
|
|
4
|
+
* backlog.ts entry readers with their cache behavior, placeholder skipping, and Done/Fixed
|
|
5
|
+
* exclusion the dashboards rely on — so the terminal view cannot drift from the dashboard
|
|
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
|
+
* verbatim — heading text with its `(planned …)`/`(reported …)` suffix, body lines
|
|
9
|
+
* indented two spaces under it — because these are markdown the loops
|
|
10
|
+
* wrote, including their Goal/Approach/Acceptance-criteria structure; reflowing them here
|
|
11
|
+
* would make this command a worse reader of its own backlog than the files it summarizes. An
|
|
12
|
+
* empty section renders a single `_(none)_` line rather than disappearing, so an all-clear
|
|
13
|
+
* backlog reads as three explicit empties, not a suspiciously short document. */
|
|
14
|
+
/** One `## ` section: its entries verbatim, or the single `_(none)_` placeholder line. */
|
|
15
|
+
function renderSection(title, entries) {
|
|
16
|
+
const lines = [`## ${title}`, ""];
|
|
17
|
+
if (entries.length === 0) {
|
|
18
|
+
lines.push("_(none)_", "");
|
|
19
|
+
return lines;
|
|
20
|
+
}
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
lines.push(`### ${entry.title}`, "");
|
|
23
|
+
// The body is already trimmed by parseEntryDetails; only the indent is added here.
|
|
24
|
+
for (const line of entry.body.split("\n"))
|
|
25
|
+
lines.push(line === "" ? "" : ` ${line}`);
|
|
26
|
+
lines.push("");
|
|
27
|
+
}
|
|
28
|
+
return lines;
|
|
29
|
+
}
|
|
30
|
+
/** Render the backlog payload (planned plans, open bugs, open questions) as Markdown. */
|
|
31
|
+
export function renderBacklogMarkdown(payload) {
|
|
32
|
+
const lines = ["# tumwater backlog", ""];
|
|
33
|
+
lines.push(...renderSection("Planned features", payload.plans));
|
|
34
|
+
lines.push(...renderSection("Open bugs", payload.bugs));
|
|
35
|
+
lines.push(...renderSection("Open questions", payload.questions));
|
|
36
|
+
return lines.join("\n").trimEnd();
|
|
37
|
+
}
|