tumwater 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,19 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { groupBlockedSeries, partsLabel } from "./backlog-structure.js";
|
|
2
|
+
import { holdMark, inProgressMark } from "./backlog-eligibility.js";
|
|
2
3
|
/** One `## ` section: its entries verbatim, or the single `_(none)_` placeholder line. When
|
|
3
4
|
* `holds` is given, entry `i`'s hold mark rides on its `### ` heading. */
|
|
4
|
-
function renderSection(title, entries, holds) {
|
|
5
|
+
function renderSection(title, entries, holds, inProgress) {
|
|
5
6
|
const lines = [`## ${title}`, ""];
|
|
6
7
|
if (entries.length === 0) {
|
|
7
8
|
lines.push("_(none)_", "");
|
|
8
9
|
return lines;
|
|
9
10
|
}
|
|
10
|
-
for (let i = 0; i < entries.length; i++)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
for (let i = 0; i < entries.length; i++)
|
|
12
|
+
lines.push(...renderEntry(entries[i], holds?.[i] ?? null, inProgress?.[i] ?? null));
|
|
13
|
+
return lines;
|
|
14
|
+
}
|
|
15
|
+
/** One entry: its heading with hold and in-progress marks, then its body indented two spaces. */
|
|
16
|
+
function renderEntry(entry, hold, runner) {
|
|
17
|
+
const lines = [`### ${entry.title}${holdMark(hold)}${inProgressMark(runner)}`, ""];
|
|
18
|
+
// The body is already trimmed by parseEntryDetails; only the indent is added here.
|
|
19
|
+
for (const line of entry.body.split("\n"))
|
|
20
|
+
lines.push(line === "" ? "" : ` ${line}`);
|
|
21
|
+
lines.push("");
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
/** The Planned section. With holds, each series with two or more blocked parts prints as one
|
|
25
|
+
* body-less heading at its first part's place (the grouping `plan --list` uses); every other
|
|
26
|
+
* entry prints as renderSection would. Without holds it is renderSection itself. */
|
|
27
|
+
function renderPlannedSection(entries, holds, inProgress) {
|
|
28
|
+
if (!holds || entries.length === 0)
|
|
29
|
+
return renderSection("Planned features", entries, holds, inProgress);
|
|
30
|
+
const rows = entries.map((entry, index) => ({
|
|
31
|
+
index, title: entry.title, hold: holds[index] ?? null, claimer: inProgress?.[index] ?? undefined,
|
|
32
|
+
}));
|
|
33
|
+
const lines = ["## Planned features", ""];
|
|
34
|
+
let grouped = false;
|
|
35
|
+
for (const g of groupBlockedSeries(rows)) {
|
|
36
|
+
if (g.kind === "row") {
|
|
37
|
+
lines.push(...renderEntry(entries[g.row.index], g.row.hold, inProgress?.[g.row.index] ?? null));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
grouped = true;
|
|
41
|
+
lines.push(`### ${g.series}, ${g.rows.length} blocked parts (${partsLabel(g.rows)}) [blocked: each waits on an earlier part]`, "");
|
|
42
|
+
}
|
|
43
|
+
if (grouped) {
|
|
44
|
+
lines.push("_(each blocked series is one heading; `tumwater plan --show <n>` prints a part, `tumwater backlog --json` lists every entry)_", "");
|
|
17
45
|
}
|
|
18
46
|
return lines;
|
|
19
47
|
}
|
|
@@ -21,8 +49,31 @@ function renderSection(title, entries, holds) {
|
|
|
21
49
|
* Markdown. */
|
|
22
50
|
export function renderBacklogMarkdown(payload) {
|
|
23
51
|
const lines = ["# tumwater backlog", ""];
|
|
24
|
-
lines.push(...
|
|
25
|
-
lines.push(...renderSection("Open bugs", payload.bugs, payload.holds?.bugs));
|
|
52
|
+
lines.push(...renderPlannedSection(payload.plans, payload.holds?.plans, payload.inProgress?.plans));
|
|
53
|
+
lines.push(...renderSection("Open bugs", payload.bugs, payload.holds?.bugs, payload.inProgress?.bugs));
|
|
26
54
|
lines.push(...renderSection("Open questions", payload.questions));
|
|
27
55
|
return lines.join("\n").trimEnd();
|
|
28
56
|
}
|
|
57
|
+
/** The view with only the plans and bugs no hold keeps from being picked: entry `i` stays when
|
|
58
|
+
* `holds.plans[i]` / `holds.bugs[i]` is null, and `holds` and `inProgress` are filtered by the
|
|
59
|
+
* same indexes so the arrays stay parallel. An entry a role is working on has no hold, so it
|
|
60
|
+
* stays with its in-progress mark. Questions carry no holds and pass through; a view without
|
|
61
|
+
* `holds` is returned unchanged. */
|
|
62
|
+
export function readyBacklog(view) {
|
|
63
|
+
const holds = view.holds;
|
|
64
|
+
if (!holds)
|
|
65
|
+
return view;
|
|
66
|
+
const keep = (hs, n) => Array.from({ length: n }, (_, i) => i).filter((i) => (hs[i] ?? null) === null);
|
|
67
|
+
const plan = keep(holds.plans, view.plans.length);
|
|
68
|
+
const bug = keep(holds.bugs, view.bugs.length);
|
|
69
|
+
const pick = (xs, idx) => idx.map((i) => xs?.[i]);
|
|
70
|
+
return {
|
|
71
|
+
...view,
|
|
72
|
+
plans: pick(view.plans, plan),
|
|
73
|
+
bugs: pick(view.bugs, bug),
|
|
74
|
+
holds: { plans: pick(holds.plans, plan), bugs: pick(holds.bugs, bug) },
|
|
75
|
+
...(view.inProgress
|
|
76
|
+
? { inProgress: { plans: pick(view.inProgress.plans, plan), bugs: pick(view.inProgress.bugs, bug) } }
|
|
77
|
+
: {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { increment } from "../collections.js";
|
|
3
4
|
import { readTextOrNull } from "../files/files.js";
|
|
4
5
|
import { cachedTextByStat } from "../files/stat-cache.js";
|
|
5
|
-
import { BACKLOG_FILES, fenceTracker, headingMetadata, parseEntryDetails, sectionBodyLines, fenceAwareHeadingLines, headingTitle, } from "./backlog-md.js";
|
|
6
|
+
import { BACKLOG_FILES, FINISHED_ARCHIVES, fenceTracker, headingMetadata, parseEntryDetails, sectionBodyLines, fenceAwareHeadingLines, headingTitle, } from "./backlog-md.js";
|
|
6
7
|
import { changeBaseRev, fileContentAt } from "../git/git.js";
|
|
8
|
+
import { allLiveClaims } from "../loop/loop-state.js";
|
|
7
9
|
import { collapseWhitespace, truncate } from "../text/text.js";
|
|
8
|
-
import { entryHoldAgainst, holdMark, plannedPartKeys } from "./backlog-eligibility.js";
|
|
10
|
+
import { entryHoldAgainst, holdMark, plannedPartKeys, seriesPart, } from "./backlog-eligibility.js";
|
|
9
11
|
import { plannedPlanEntries } from "./backlog.js";
|
|
12
|
+
/** The archive files of PLANS.md and BUGS.md (FINISHED_ARCHIVES). */
|
|
13
|
+
const PLANS_ARCHIVE = FINISHED_ARCHIVES[0].archive;
|
|
14
|
+
const BUGS_ARCHIVE = FINISHED_ARCHIVES[1].archive;
|
|
10
15
|
/** The stamp verbs a backlog heading's trailing `(planned 2026-09-29)`-style parenthetical
|
|
11
16
|
* opens with — the same list the backlog writers stamp and the dashboards parse. One home
|
|
12
|
-
* for the verb list: the TUI's
|
|
17
|
+
* for the verb list: the TUI's team alerts strip the suffix (stripEntryStamp) and the
|
|
13
18
|
* dashboard's browser view model splits it off as metadata (splitTitle), and a new stamp
|
|
14
19
|
* verb must reach both renders.
|
|
15
20
|
* Kept as pattern SOURCE rather than a RegExp so the browser twin (a String.raw template
|
|
@@ -20,16 +25,44 @@ export const ENTRY_STAMP_META_SOURCE = String.raw `\s*\(((?:planned|reported|fou
|
|
|
20
25
|
const ENTRY_STAMP_META_RE = new RegExp(ENTRY_STAMP_META_SOURCE, "i");
|
|
21
26
|
/** A heading or title with its trailing `(planned 2026-09-29)`-style stamp suffix removed,
|
|
22
27
|
* whitespace otherwise untouched — the shared span of this module's indexTitle, eligibility's
|
|
23
|
-
* entryKey and seriesPart, and the TUI
|
|
28
|
+
* entryKey and seriesPart, and the TUI team alerts, which all read a heading stamp-free. */
|
|
24
29
|
export function stripEntryStamp(title) {
|
|
25
30
|
return title.replace(ENTRY_STAMP_META_RE, "");
|
|
26
31
|
}
|
|
32
|
+
/** The heading of a `## Done`/`## Fixed` entry with its completion stamp removed. Cuts at the
|
|
33
|
+
* first `dateRe` match, drops a parenthetical the cut left open (`(found by … 2026-10-01; `,
|
|
34
|
+
* `(planned …; `), ignores a stray `)` (`… now under ## Fixed); fixed 2026-10-04 …)`), trims
|
|
35
|
+
* trailing separators, then strips a closed `(planned …)`/`(found …)` stamp suffix. `dateRe`
|
|
36
|
+
* matches nothing in the result. */
|
|
37
|
+
export function finishedTitle(title, dateRe) {
|
|
38
|
+
const match = dateRe.exec(title);
|
|
39
|
+
let t = match ? title.slice(0, match.index) : title;
|
|
40
|
+
const open = [];
|
|
41
|
+
for (let i = 0; i < t.length; i++) {
|
|
42
|
+
if (t[i] === "(")
|
|
43
|
+
open.push(i);
|
|
44
|
+
else if (t[i] === ")" && open.length)
|
|
45
|
+
open.pop();
|
|
46
|
+
}
|
|
47
|
+
if (open.length)
|
|
48
|
+
t = t.slice(0, open[0]);
|
|
49
|
+
t = t.replace(/[\s,;:—–-]+$/, "");
|
|
50
|
+
return stripEntryStamp(t).trimEnd();
|
|
51
|
+
}
|
|
27
52
|
/** A heading's parenthetical dates: `(planned YYYY-MM-DD` opens a plan entry; `done
|
|
28
53
|
* YYYY-MM-DD` records its completion. Matched against the JOINED heading metadata
|
|
29
54
|
* (headingMetadata), never the bare first line — many headings wrap their dates onto a
|
|
30
55
|
* second line (`(planned 2026-09-02, done` / `2026-09-03)`). */
|
|
31
56
|
const PLANNED_DATE = /\(planned \d{4}-\d{2}-\d{2}/;
|
|
32
57
|
const DONE_DATE = /\bdone \d{4}-\d{2}-\d{2}/;
|
|
58
|
+
/** A completion stamp: `done YYYY-MM-DD` introduced by a structural delimiter (`— done`,
|
|
59
|
+
* `, done`, `; done`, `(done`) rather than by an English subject in prose. Every entry-own done
|
|
60
|
+
* record in PLANS.md is delimiter-introduced (`…planned …, done …`, `…planned …; done …`,
|
|
61
|
+
* `…) — done …`, `…) (done …)`); a "done DATE" that merely names PRIOR merged work is a
|
|
62
|
+
* participle ("…part 2/2 … both done 2026-10-16, …") whose `done` follows a word, so this
|
|
63
|
+
* narrower regex does not match it and a mention inside a heading's own `(planned …)` prose
|
|
64
|
+
* cannot strand the entry. */
|
|
65
|
+
const DONE_STAMP = /[—,(;]\s*done \d{4}-\d{2}-\d{2}/;
|
|
33
66
|
/** The `### ` headings of `md`'s PLANS.md that sit under the wrong section, in file order:
|
|
34
67
|
* (a) entries under `## Done` whose heading carries a `(planned YYYY-MM-DD…)` parenthetical
|
|
35
68
|
* but no `done YYYY-MM-DD` — written as plans, never stamped done, and invisible to every
|
|
@@ -58,7 +91,16 @@ export function strandedPlanEntries(md) {
|
|
|
58
91
|
if (!line.startsWith("### "))
|
|
59
92
|
continue;
|
|
60
93
|
const { text } = headingMetadata(lines, i);
|
|
61
|
-
|
|
94
|
+
// A `## Done` entry is stranded when it carries the `(planned …)` opener but no done
|
|
95
|
+
// stamp of its own. An entry still under `## Planned` is stranded only when its heading
|
|
96
|
+
// carries a completion stamp — a `done YYYY-MM-DD` introduced by a structural delimiter
|
|
97
|
+
// (`— done`, `, done`, `; done`, `(done`), the repo's completion shapes. A "done DATE" that
|
|
98
|
+
// merely describes PRIOR merged work ("…both done 2026-10-16, …") follows an English word,
|
|
99
|
+
// not a delimiter, so DONE_STAMP does not match it and the entry is not stranded.
|
|
100
|
+
const misplaced = section === "Done"
|
|
101
|
+
? PLANNED_DATE.test(text) && !DONE_DATE.test(text)
|
|
102
|
+
: DONE_STAMP.test(text);
|
|
103
|
+
if (misplaced)
|
|
62
104
|
stranded.push({ title: headingTitle(line, "### "), section });
|
|
63
105
|
}
|
|
64
106
|
}
|
|
@@ -97,8 +139,8 @@ function sectionTitleCounts(md) {
|
|
|
97
139
|
return counts;
|
|
98
140
|
}
|
|
99
141
|
/** The `## ` titles of `md` that appear more than once, in first-appearance order — the
|
|
100
|
-
* duplicate-heading signal `checkBacklogHeadings` reports for main and
|
|
101
|
-
*
|
|
142
|
+
* duplicate-heading signal `checkBacklogHeadings` reports for main and `backlogStructureReason`
|
|
143
|
+
* checks on a tree being merged. */
|
|
102
144
|
export function duplicateHeadings(md) {
|
|
103
145
|
return [...sectionTitleCounts(md)].filter(([, n]) => n > 1).map(([title]) => title);
|
|
104
146
|
}
|
|
@@ -129,30 +171,29 @@ export function doubleBlockFixedEntries(md) {
|
|
|
129
171
|
}
|
|
130
172
|
return found;
|
|
131
173
|
}
|
|
132
|
-
/** The first structural fault a diff
|
|
133
|
-
* undefined when none: for each touched backlog file it compares the `## ` heading set of the
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* re-check can afford it on every landing (plans: "Backlog structure check", part 2/4). */
|
|
174
|
+
/** The first structural fault a diff merging into `mainBranch` leaves in a backlog file, or
|
|
175
|
+
* undefined when none: for each touched backlog file it compares the `## ` heading set of the tree
|
|
176
|
+
* being merged (`wt`) against the diff's merge-base — the same base falseFixReason measures
|
|
177
|
+
* against, so a stacked batch is judged change by change — and rejects when (a) the head carries
|
|
178
|
+
* MORE of a title than the base carried (a duplicate `## Done` added, or a second one where the
|
|
179
|
+
* base had none), or (b) a title present on the base is gone from the head (a whole section
|
|
180
|
+
* dropped). The rule is deliberately about heading sets, not section names: a project whose BUGS.md
|
|
181
|
+
* adds `## Verified` (this repo's does) or a fresh repo seeded from src/init/init.ts's templates
|
|
182
|
+
* passes unchanged. A base that already carries a duplicate never blocks unrelated edits — rule (a)
|
|
183
|
+
* fires only when the head's count EXCEEDS the base's, so a change that removes a duplicate always
|
|
184
|
+
* passes. For PLANS.md there is a third rule (part 4/4): a head that ADDS a `### ` entry directly
|
|
185
|
+
* under `## Done` whose joined heading metadata carries `(planned YYYY-MM-DD` but no `done
|
|
186
|
+
* YYYY-MM-DD`, and whose key the base's PLANS.md never carried in ANY section, is a plan filed into
|
|
187
|
+
* the wrong section — rejected with a reason naming the entry and the one-line fix. Judged against
|
|
188
|
+
* the merge-base (the same base the heading rules use), so a Planned → Done move of an entry the
|
|
189
|
+
* base already had passes, a stacked batch is measured change by change, and a pre-existing
|
|
190
|
+
* stranded entry (whose key the base has) never blocks unrelated merges — the clean loop's repair
|
|
191
|
+
* (part 3/4) owns those. BUGS.md has one more: a head `## Fixed` entry that holds more than one
|
|
192
|
+
* `**Symptom:**` or `**Fix:**` block (doubleBlockFixedEntries) absorbed a headingless record into
|
|
193
|
+
* its body; a base body that already had the shape is never re-flagged, so only a newly merged
|
|
194
|
+
* record is rejected (BUGS.md 2026-10-08). Detection is deterministic markdown reading — no pi — so
|
|
195
|
+
* both the review gate (exempt and code diffs alike) and the in-lock merge re-check can afford it
|
|
196
|
+
* on every merge (plans: "Backlog structure check", part 2/4). */
|
|
156
197
|
export async function backlogStructureReason(wt, mainBranch, files) {
|
|
157
198
|
const touched = files.filter((f) => BACKLOG_FILES.has(f));
|
|
158
199
|
if (touched.length === 0)
|
|
@@ -172,45 +213,52 @@ export async function backlogStructureReason(wt, mainBranch, files) {
|
|
|
172
213
|
if (!headCounts.has(title))
|
|
173
214
|
return (`${file} drops the "## ${title}" section the base had — entries filed under it would ` +
|
|
174
215
|
`be invisible to every section reader; restore "## ${title}"`);
|
|
175
|
-
// Part 4/4's rule, PLANS.md
|
|
176
|
-
// directly under ## Done with no done date. strandedPlanEntries already does the
|
|
177
|
-
// fence-aware, joined-metadata reading the first two conditions need; the base's key set
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
216
|
+
// Part 4/4's rule, PLANS.md and its archive (see this function's doc comment): a NEW plan
|
|
217
|
+
// filed directly under ## Done with no done date. strandedPlanEntries already does the
|
|
218
|
+
// fence-aware, joined-metadata reading the first two conditions need; the base's key set —
|
|
219
|
+
// over both files, since tumwater moves a finished plan from one to the other
|
|
220
|
+
// (plans/backlog-archive.md) — supplies the third.
|
|
221
|
+
if (file === "PLANS.md" || file === PLANS_ARCHIVE) {
|
|
222
|
+
const baseKeys = new Set([
|
|
223
|
+
...planHeadingKeys(file === "PLANS.md" ? base : await fileContentAt(wt, baseRev, "PLANS.md")),
|
|
224
|
+
...planHeadingKeys(file === PLANS_ARCHIVE ? base : await fileContentAt(wt, baseRev, PLANS_ARCHIVE)),
|
|
225
|
+
]);
|
|
181
226
|
const newcomer = strandedPlanEntries(head).find((e) => e.section === "Done" && !baseKeys.has(planHeadingKey(e.title)));
|
|
182
227
|
if (newcomer)
|
|
183
|
-
return (
|
|
228
|
+
return (`${file} files "${newcomer.title}" directly under "## Done" with no done date — a plan ` +
|
|
184
229
|
`written into the done section is invisible to every Planned reader; file it under ` +
|
|
185
|
-
`"## Planned" instead`);
|
|
230
|
+
`"## Planned"${file === "PLANS.md" ? "" : " in PLANS.md"} instead`);
|
|
186
231
|
}
|
|
187
232
|
// A new headingless record cannot hide in a Fixed entry's body: reject a head whose
|
|
188
233
|
// multi-block entries the base did not carry (an existing one never blocks unrelated
|
|
189
234
|
// edits, matching the heading-set rules above). The Fix paragraph is the guard's unit, so
|
|
190
235
|
// a merged second block would otherwise sit inside the neighbor it was symbol-checked
|
|
191
|
-
// against.
|
|
192
|
-
if (file === "BUGS.md") {
|
|
236
|
+
// against. BUGS-FIXED.md holds the same entries once archived.
|
|
237
|
+
if (file === "BUGS.md" || file === BUGS_ARCHIVE) {
|
|
193
238
|
const baseTitles = new Set(doubleBlockFixedEntries(base));
|
|
194
239
|
const newcomer = doubleBlockFixedEntries(head).find((title) => !baseTitles.has(title));
|
|
195
240
|
if (newcomer)
|
|
196
|
-
return (
|
|
241
|
+
return (`${file}'s Fixed entry "${newcomer}" holds more than one **Symptom:**/**Fix:** block — ` +
|
|
197
242
|
`a record with no "### " heading has merged into it, so the hidden record is invisible ` +
|
|
198
243
|
`to every section reader; give it its own "### " heading`);
|
|
199
244
|
}
|
|
200
245
|
}
|
|
201
246
|
return undefined;
|
|
202
247
|
}
|
|
203
|
-
/** Per-file cache of PLANS.md's stranded entries (strandedPlanEntriesFor). */
|
|
248
|
+
/** Per-file cache of PLANS.md's and PLANS-DONE.md's stranded entries (strandedPlanEntriesFor). */
|
|
204
249
|
const strandedPlanCache = new Map();
|
|
205
|
-
/** `strandedPlanEntries` for the primary checkout's PLANS.md
|
|
206
|
-
*
|
|
207
|
-
*
|
|
250
|
+
/** `strandedPlanEntries` for the primary checkout's PLANS.md and PLANS-DONE.md (where finished
|
|
251
|
+
* plans are archived, plans/backlog-archive.md), each entry tagged with its file, stat-cached per
|
|
252
|
+
* file path (stat-cache.cachedTextByStat): the clean loop asks for it every tick, `tumwater role`
|
|
253
|
+
* previews the same block and doctor reports it, and the archive grows without bound, so an
|
|
208
254
|
* unchanged file costs one stat instead of a full read and section walk. A missing or
|
|
209
255
|
* unreadable file reads as no entries. The result is copied out, so each caller owns its
|
|
210
256
|
* data. */
|
|
211
|
-
function strandedPlanEntriesFor(root) {
|
|
212
|
-
|
|
213
|
-
|
|
257
|
+
export function strandedPlanEntriesFor(root) {
|
|
258
|
+
return ["PLANS.md", PLANS_ARCHIVE].flatMap((file) => {
|
|
259
|
+
const full = path.join(root, file);
|
|
260
|
+
return (cachedTextByStat(strandedPlanCache, full, full, (md) => strandedPlanEntries(md).map((e) => ({ ...e, file })), (entries) => entries.slice()) ?? []);
|
|
261
|
+
});
|
|
214
262
|
}
|
|
215
263
|
/** The clean loop's `<backlog-structure>` prompt block for the primary checkout at `root`
|
|
216
264
|
* (the same root the telemetry digest and qa coverage blocks read): a rendered block listing
|
|
@@ -223,12 +271,12 @@ export function renderBacklogStructureBlock(root) {
|
|
|
223
271
|
if (stranded.length === 0)
|
|
224
272
|
return undefined;
|
|
225
273
|
const listed = stranded
|
|
226
|
-
.map((e) => `- (now under ## ${e.section}) ${e.title}`)
|
|
274
|
+
.map((e) => `- (now under ${e.file !== undefined && e.file !== "PLANS.md" ? `${e.file} ` : ""}## ${e.section}) ${e.title}`)
|
|
227
275
|
.join("\n");
|
|
228
276
|
// The task wording lives in the clean role's find text (src/roles/role-catalog.ts); the block
|
|
229
277
|
// only carries the evidence, like the digest and coverage blocks do.
|
|
230
|
-
return `Plan headings filed under the wrong section of PLANS.md — invisible to
|
|
231
|
-
that scan one section, so no
|
|
278
|
+
return `Plan headings filed under the wrong section of PLANS.md or PLANS-DONE.md — invisible to
|
|
279
|
+
the readers that scan one section, so no role sees them as backlog work:
|
|
232
280
|
<backlog-structure>
|
|
233
281
|
${listed}
|
|
234
282
|
</backlog-structure>`;
|
|
@@ -301,8 +349,128 @@ const INDEX_SECTIONS = [
|
|
|
301
349
|
{ file: "BUGS.md", section: "Open" },
|
|
302
350
|
{ file: "QUESTIONS.md", section: "Open" },
|
|
303
351
|
];
|
|
352
|
+
/** How many closed entries per closed section the index lists. */
|
|
353
|
+
const RECENT_CLOSED_MAX = 5;
|
|
354
|
+
/** The closed sections whose newest entries the index also lists, each read from the live file
|
|
355
|
+
* and then its archive (FINISHED_ARCHIVES, plans/backlog-archive.md); every one keeps its entries
|
|
356
|
+
* newest first, so the first entries in file order are the recent ones. */
|
|
357
|
+
const CLOSED_SECTIONS = FINISHED_ARCHIVES.map((a) => ({
|
|
358
|
+
files: [a.file, a.archive],
|
|
359
|
+
section: a.section,
|
|
360
|
+
}));
|
|
304
361
|
/** Per-file cache of the parsed actionable entries (indexSectionCache). */
|
|
305
362
|
const indexSectionCache = new Map();
|
|
363
|
+
/** Per-file cache of the newest closed entries' ranges; separate from indexSectionCache, which is
|
|
364
|
+
* keyed by the file path alone and PLANS.md would collide. */
|
|
365
|
+
const closedSectionCache = new Map();
|
|
366
|
+
/** One `IndexRow` as an ordinary index line: range, truncated stamp-free title, hold mark, and
|
|
367
|
+
* the in-flight suffix when a live claim names it. The collapsed-series line is built by
|
|
368
|
+
* collapseBlockedSeries, returned alongside these. */
|
|
369
|
+
function renderIndexRow(row) {
|
|
370
|
+
return `- ${row.start}-${row.end}: ${indexTitle(row.title)}${row.mark}${row.claimer ? ` [in flight by ${row.claimer}]` : ""}`;
|
|
371
|
+
}
|
|
372
|
+
/** True when `row` is a candidate for series-collapsing: it is a series part held only by an
|
|
373
|
+
* unmerged prerequisite (`{ blockedBy }`, not a Refused/needs-review/needs-replan note) and no live
|
|
374
|
+
* claim names it. Returns the row's series part when it collapses, else null. */
|
|
375
|
+
function collapsibleSeriesPart(row) {
|
|
376
|
+
if (row.claimer !== undefined)
|
|
377
|
+
return null;
|
|
378
|
+
if (typeof row.hold !== "object" || row.hold === null)
|
|
379
|
+
return null;
|
|
380
|
+
return seriesPart(row.title);
|
|
381
|
+
}
|
|
382
|
+
/** The collapsed part list for a series' blocked parts: each part as `i/n`, with a run of
|
|
383
|
+
* consecutive numeric parts (same total, adjacent values) joined into one en-dash range
|
|
384
|
+
* (`2/5–5/5`) and each lettered part (`2a`) listed singly. */
|
|
385
|
+
export function partsLabel(rows) {
|
|
386
|
+
const refs = rows.map((r) => seriesPart(r.title));
|
|
387
|
+
const pieces = [];
|
|
388
|
+
let start = null;
|
|
389
|
+
let prev = null;
|
|
390
|
+
const numeric = (p) => /^\d+$/.test(p);
|
|
391
|
+
const flush = () => {
|
|
392
|
+
if (start === null || prev === null)
|
|
393
|
+
return;
|
|
394
|
+
pieces.push(start.part === prev.part
|
|
395
|
+
? `${start.part}/${start.of}`
|
|
396
|
+
: `${start.part}/${start.of}–${prev.part}/${prev.of}`);
|
|
397
|
+
start = null;
|
|
398
|
+
prev = null;
|
|
399
|
+
};
|
|
400
|
+
for (const ref of refs) {
|
|
401
|
+
if (prev !== null &&
|
|
402
|
+
numeric(ref.part) &&
|
|
403
|
+
numeric(prev.part) &&
|
|
404
|
+
Number(ref.part) === Number(prev.part) + 1 &&
|
|
405
|
+
ref.of === prev.of) {
|
|
406
|
+
prev = ref;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
flush();
|
|
410
|
+
start = ref;
|
|
411
|
+
prev = ref;
|
|
412
|
+
}
|
|
413
|
+
flush();
|
|
414
|
+
return pieces.join(", ");
|
|
415
|
+
}
|
|
416
|
+
/** Renders `rows` to index lines, collapsing a multi-part series' blocked parts to one line per
|
|
417
|
+
* series (this plan, "Compact backlog index, part 1/1"): a long series' chain of held parts would
|
|
418
|
+
* otherwise fill every loop prompt with lines no role can act on. An entry collapses when it is a
|
|
419
|
+
* series part held only by an unmerged prerequisite and no live claim names it; a series with two
|
|
420
|
+
* or more such parts renders as one line at the position of its first collapsed part, spanning from
|
|
421
|
+
* that part's start to the last's end, with the part list (`2/4–4/4`) and a `[blocked: each waits
|
|
422
|
+
* on an earlier part]` mark. A series with a single blocked part, an eligible part, a note-held
|
|
423
|
+
* (Refused/needs-review/needs-replan) part and a claimed part keep their ordinary lines, so the
|
|
424
|
+
* eligibility rules the feature role's charter teaches are unchanged; the collapsed mark keeps the
|
|
425
|
+
* `[blocked: …]` prefix it already skips. */
|
|
426
|
+
/** The grouping half of collapseBlockedSeries, on any rows carrying a title, hold and claimer:
|
|
427
|
+
* each series with two or more collapsible blocked parts becomes one `series` group at its first
|
|
428
|
+
* part's place; every other row stays a `row` group in order. */
|
|
429
|
+
export function groupBlockedSeries(rows) {
|
|
430
|
+
// Group the collapsible rows by series, keyed on the lowercased series name.
|
|
431
|
+
const bySeries = new Map();
|
|
432
|
+
for (const row of rows) {
|
|
433
|
+
const part = collapsibleSeriesPart(row);
|
|
434
|
+
if (part === null)
|
|
435
|
+
continue;
|
|
436
|
+
const key = part.series.toLowerCase();
|
|
437
|
+
const list = bySeries.get(key) ?? [];
|
|
438
|
+
list.push(row);
|
|
439
|
+
bySeries.set(key, list);
|
|
440
|
+
}
|
|
441
|
+
// Only a series with two or more blocked parts collapses; a lone blocked part keeps its
|
|
442
|
+
// exact `requires` line.
|
|
443
|
+
const collapse = new Set();
|
|
444
|
+
for (const [key, list] of bySeries)
|
|
445
|
+
if (list.length >= 2)
|
|
446
|
+
collapse.add(key);
|
|
447
|
+
const groups = [];
|
|
448
|
+
const skipped = new Set();
|
|
449
|
+
for (const row of rows) {
|
|
450
|
+
if (skipped.has(row))
|
|
451
|
+
continue;
|
|
452
|
+
const part = collapsibleSeriesPart(row);
|
|
453
|
+
if (part !== null && collapse.has(part.series.toLowerCase())) {
|
|
454
|
+
const group = bySeries.get(part.series.toLowerCase());
|
|
455
|
+
groups.push({ kind: "series", series: part.series, rows: group });
|
|
456
|
+
for (const g of group)
|
|
457
|
+
skipped.add(g);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
groups.push({ kind: "row", row });
|
|
461
|
+
}
|
|
462
|
+
return groups;
|
|
463
|
+
}
|
|
464
|
+
function collapseBlockedSeries(rows) {
|
|
465
|
+
return groupBlockedSeries(rows).map((g) => {
|
|
466
|
+
if (g.kind === "row")
|
|
467
|
+
return renderIndexRow(g.row);
|
|
468
|
+
const first = g.rows[0];
|
|
469
|
+
const last = g.rows[g.rows.length - 1];
|
|
470
|
+
return (`- ${first.start}-${last.end}: ${indexTitle(g.series)}, ${g.rows.length} blocked parts ` +
|
|
471
|
+
`(${partsLabel(g.rows)}) [blocked: each waits on an earlier part]`);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
306
474
|
/** The `<backlog-index>` prompt block: each actionable entry (PLANS.md ## Planned, BUGS.md
|
|
307
475
|
* ## Open, QUESTIONS.md ## Open) with its 1-based line range, rendered from the primary
|
|
308
476
|
* checkout `root`. Replaces the per-tick `grep -n '^##'` heading map, whose pattern also matched
|
|
@@ -319,6 +487,12 @@ export function renderBacklogIndexBlock(root) {
|
|
|
319
487
|
// PLANS.md costs one stat per call.
|
|
320
488
|
const planned = plannedPlanEntries(root);
|
|
321
489
|
const plannedParts = plannedPartKeys(planned);
|
|
490
|
+
// The cross-role in-flight hold (BUGS.md 2026-10-09): every role's prompts share this one
|
|
491
|
+
// section-agnostic index, and the role it is assembled for is not available here, so an entry
|
|
492
|
+
// claimed by ANY role is marked in flight — the owner's own index marks it too, which is harmless
|
|
493
|
+
// (its assignment note is the instruction), and every other role reads it as taken instead of
|
|
494
|
+
// re-implementing a change someone's merge is already carrying to main.
|
|
495
|
+
const claimed = allLiveClaims(root);
|
|
322
496
|
const listed = [];
|
|
323
497
|
for (const { file, section } of INDEX_SECTIONS) {
|
|
324
498
|
const full = path.join(root, file);
|
|
@@ -339,16 +513,45 @@ export function renderBacklogIndexBlock(root) {
|
|
|
339
513
|
if (entries === null || entries.length === 0)
|
|
340
514
|
continue;
|
|
341
515
|
listed.push(`${file} ## ${section}`);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
516
|
+
// Build the rows once per section, then collapse a PLANS.md series' blocked parts (this
|
|
517
|
+
// plan) — BUGS.md and QUESTIONS.md sections keep every entry on its own line.
|
|
518
|
+
const rows = entries.map((e) => {
|
|
519
|
+
const hold = entryHoldAgainst({ title: e.title, body: e.body }, plannedParts);
|
|
520
|
+
const key = collapseWhitespace(stripEntryStamp(e.title)).toLowerCase();
|
|
521
|
+
return {
|
|
522
|
+
start: e.start,
|
|
523
|
+
end: e.end,
|
|
524
|
+
title: e.title,
|
|
525
|
+
hold,
|
|
526
|
+
mark: holdMark(hold),
|
|
527
|
+
claimer: claimed.get(key),
|
|
528
|
+
};
|
|
529
|
+
});
|
|
530
|
+
const isPlans = file === "PLANS.md" && section === "Planned";
|
|
531
|
+
listed.push(...(isPlans ? collapseBlockedSeries(rows) : rows.map(renderIndexRow)));
|
|
532
|
+
}
|
|
533
|
+
for (const { files, section } of CLOSED_SECTIONS) {
|
|
534
|
+
// The newest RECENT_CLOSED_MAX across the live section (normally empty) and the archive.
|
|
535
|
+
let room = RECENT_CLOSED_MAX;
|
|
536
|
+
for (const file of files) {
|
|
537
|
+
if (room <= 0)
|
|
538
|
+
break;
|
|
539
|
+
const full = path.join(root, file);
|
|
540
|
+
const entries = cachedTextByStat(closedSectionCache, full, full, (md) => actionableEntryRanges(md, section).slice(0, RECENT_CLOSED_MAX), (v) => v.slice())?.slice(0, room);
|
|
541
|
+
if (entries === undefined || entries.length === 0)
|
|
542
|
+
continue;
|
|
543
|
+
room -= entries.length;
|
|
544
|
+
listed.push(`${file} ## ${section} (newest ${entries.length})`);
|
|
545
|
+
for (const e of entries)
|
|
546
|
+
listed.push(`- ${e.start}-${e.end}: ${indexTitle(e.title)}`);
|
|
345
547
|
}
|
|
346
548
|
}
|
|
347
549
|
if (listed.length === 0)
|
|
348
550
|
return undefined;
|
|
349
|
-
return `
|
|
350
|
-
Read the entries you need by those ranges; the files
|
|
351
|
-
|
|
551
|
+
return `Backlog index — each actionable entry, then the newest closed ones, with the 1-based line range it occupies in its file.
|
|
552
|
+
Read the entries you need by those ranges; the archive files (PLANS-DONE.md, BUGS-FIXED.md) grow
|
|
553
|
+
with every merge, so search them rather than reading them. Blocked parts of a multi-part series are
|
|
554
|
+
shown as one line per series.
|
|
352
555
|
<backlog-index>
|
|
353
556
|
${listed.join("\n")}
|
|
354
557
|
</backlog-index>`;
|