tumwater 0.4.0 → 0.4.2
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 +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -23,7 +23,7 @@ export function finishedArchiveOf(file) {
|
|
|
23
23
|
}
|
|
24
24
|
/** The repo-root backlog markdown files shared by every layer that knows them by name: the
|
|
25
25
|
* tracked files roles edit and readers parse by `## ` section — read by backlog-structure.ts's
|
|
26
|
-
* structural checks, merge/backlog-conflicts.ts's mechanical insert-conflict resolver, and
|
|
26
|
+
* structural checks, merge/merge-backlog-conflicts.ts's mechanical insert-conflict resolver, and
|
|
27
27
|
* doctor-backlog-plans.ts's duplicate-heading check. One definition, so the three cannot disagree
|
|
28
28
|
* about which files count. The archive files count too: merges insert at their top, and their
|
|
29
29
|
* `## ` heading is as load-bearing as the live files'. */
|
|
@@ -134,7 +134,7 @@ export function nextSectionHeading(lines, from, fenced) {
|
|
|
134
134
|
/** The body lines of one `## <sectionTitle>` section with fenced content stripped: the
|
|
135
135
|
* sectionLines walk, then a fresh fenceTracker's filter — the shape every reader that
|
|
136
136
|
* classifies a section's lines as markdown structure (entry headings, bullets, dates) walks.
|
|
137
|
-
* Exactly
|
|
137
|
+
* Exactly four call sites today: entryDates and entryLags (both below), backlog-structure.ts's
|
|
138
138
|
* strandedPlanEntries, and stage-backlog-checks.ts's placeholderSections. The fresh
|
|
139
139
|
* tracker is in sync with the document here: sectionLines
|
|
140
140
|
* only recognizes a `## ` boundary outside a fence, so the section's heading line is
|
|
@@ -248,30 +248,44 @@ export function entryDates(md, sectionTitle, dateRe, first = false) {
|
|
|
248
248
|
const dates = [];
|
|
249
249
|
const pick = first ? firstDate : lastDate;
|
|
250
250
|
const lines = sectionBodyLines(md, sectionTitle);
|
|
251
|
-
|
|
252
|
-
const line = lines[i] ?? "";
|
|
253
|
-
if (!line.startsWith("### ") && !line.startsWith("- "))
|
|
254
|
-
continue;
|
|
255
|
-
const meta = headingMetadata(lines, i);
|
|
251
|
+
eachEntryStart(lines, ({ isBullet, meta, tail }) => {
|
|
256
252
|
let date = null;
|
|
257
|
-
if (
|
|
253
|
+
if (isBullet) {
|
|
258
254
|
// Epitaph guard (see the doc comment): the date must live in the line's trailing
|
|
259
255
|
// parenthetical beside a commit reference, or the bullet is body text, not an entry.
|
|
260
256
|
// Within the parenthetical the completion is the LAST dated verb, matching the heading
|
|
261
257
|
// branch: a decomposition cross-reference ("decomposed from the sibling bug fixed
|
|
262
258
|
// <date>, fixed <date>") precedes the entry's own completion record.
|
|
263
|
-
const
|
|
264
|
-
if (/\bcommits?\b/.test(
|
|
265
|
-
date = pick(
|
|
259
|
+
const bulletTail = tail ?? "";
|
|
260
|
+
if (/\bcommits?\b/.test(bulletTail))
|
|
261
|
+
date = pick(bulletTail, dateRe);
|
|
266
262
|
}
|
|
267
263
|
else {
|
|
268
264
|
date = pick(meta.text, dateRe);
|
|
269
265
|
}
|
|
270
266
|
if (date)
|
|
271
267
|
dates.push(date);
|
|
268
|
+
});
|
|
269
|
+
return dates;
|
|
270
|
+
}
|
|
271
|
+
/** One walk over the entry starts inside a `## ` section's body lines, visiting each `### `
|
|
272
|
+
* heading or `- ` bullet line in order. An entry start is a line that begins `### ` or `- `
|
|
273
|
+
* (any other line is body content, skipped); `meta` is headingMetadata's join of the entry's
|
|
274
|
+
* metadata, `tail` is a bullet's trailing `(…)` parenthetical (null for a heading), and
|
|
275
|
+
* `isBullet` tells which shape it is. Advances past each entry's metadata by
|
|
276
|
+
* headingMetadata.next, so a later entry cannot scan the earlier one's continuation as body.
|
|
277
|
+
* Shared by entryDates and entryLags, whose date readings branch on the same `### `/`- `
|
|
278
|
+
* shape, so the two walks stay in lockstep. */
|
|
279
|
+
function eachEntryStart(lines, visit) {
|
|
280
|
+
for (let i = 0; i < lines.length; i++) {
|
|
281
|
+
const line = lines[i] ?? "";
|
|
282
|
+
if (!line.startsWith("### ") && !line.startsWith("- "))
|
|
283
|
+
continue;
|
|
284
|
+
const meta = headingMetadata(lines, i);
|
|
285
|
+
const isBullet = line.startsWith("- ");
|
|
286
|
+
visit({ isBullet, meta, tail: isBullet ? trailingParenthetical(line) : null });
|
|
272
287
|
i = meta.next - 1; // The loop's ++ resumes at the first line not consumed as metadata.
|
|
273
288
|
}
|
|
274
|
-
return dates;
|
|
275
289
|
}
|
|
276
290
|
/** The filing-to-done span of each finished entry inside one `## <sectionTitle>` section,
|
|
277
291
|
* paired with the day it completed on: `{ done, lagDays }` for every entry `entryDates` counts
|
|
@@ -287,22 +301,18 @@ export function entryDates(md, sectionTitle, dateRe, first = false) {
|
|
|
287
301
|
export function entryLags(md, sectionTitle, dateRe, filingRe) {
|
|
288
302
|
const lags = [];
|
|
289
303
|
const lines = sectionBodyLines(md, sectionTitle);
|
|
290
|
-
|
|
291
|
-
const line = lines[i] ?? "";
|
|
292
|
-
if (!line.startsWith("### ") && !line.startsWith("- "))
|
|
293
|
-
continue;
|
|
294
|
-
const meta = headingMetadata(lines, i);
|
|
304
|
+
eachEntryStart(lines, ({ isBullet, meta, tail }) => {
|
|
295
305
|
let done = null;
|
|
296
306
|
let filed = null;
|
|
297
|
-
if (
|
|
307
|
+
if (isBullet) {
|
|
298
308
|
// Same epitaph guard as entryDates: the completion must live in the trailing
|
|
299
309
|
// parenthetical beside a commit reference, and the filing read follows the same
|
|
300
310
|
// rule — sectionFilingDates matches the first stamp inside that same parenthetical
|
|
301
311
|
// for a `- ` bullet, so a description-date mention before it must not pair.
|
|
302
|
-
const
|
|
303
|
-
if (/\bcommits?\b/.test(
|
|
304
|
-
done = lastDate(
|
|
305
|
-
filed = firstDate(
|
|
312
|
+
const bulletTail = tail ?? "";
|
|
313
|
+
if (/\bcommits?\b/.test(bulletTail)) {
|
|
314
|
+
done = lastDate(bulletTail, dateRe);
|
|
315
|
+
filed = firstDate(bulletTail, filingRe);
|
|
306
316
|
}
|
|
307
317
|
}
|
|
308
318
|
else {
|
|
@@ -314,8 +324,7 @@ export function entryLags(md, sectionTitle, dateRe, filingRe) {
|
|
|
314
324
|
if (lagDays >= 0)
|
|
315
325
|
lags.push({ done, lagDays });
|
|
316
326
|
}
|
|
317
|
-
|
|
318
|
-
}
|
|
327
|
+
});
|
|
319
328
|
return lags;
|
|
320
329
|
}
|
|
321
330
|
/** A `### `/`- ` entry start's METADATA, joined for matching: the start line plus, for `### `
|
|
@@ -407,10 +416,12 @@ export function lastDate(text, dateRe) {
|
|
|
407
416
|
* the filing onto a later day. This is the same first-match the canonical `filingDate`
|
|
408
417
|
* (backlog.ts) applies to a heading line, so the filing read and `plan|bug --list`'s age agree
|
|
409
418
|
* about when an entry opened. dateRe is cloned with the g flag so matchAll sees every date; a
|
|
410
|
-
* text with no date yields null.
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
|
|
419
|
+
* text with no date yields null. Exported so the heading stamp parsers that apply the same
|
|
420
|
+
* filing rule to a single title line — `filingDate` (backlog.ts) and `questionFilingDate`
|
|
421
|
+
* (question-answer.ts) — share the one first-match mechanism instead of re-stating it;
|
|
422
|
+
* entryDates selects it with `first`, so the walker stays single-homed like lastDate's
|
|
423
|
+
* completion rule. */
|
|
424
|
+
export function firstDate(text, dateRe) {
|
|
414
425
|
const global = new RegExp(dateRe.source, dateRe.flags.includes("g") ? dateRe.flags : `${dateRe.flags}g`);
|
|
415
426
|
const match = global.exec(text);
|
|
416
427
|
return match?.[1] ?? null;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/** The hold-lifting and reordering writes of `tumwater bug|plan --release <n>` and `--top <n>`:
|
|
2
|
-
* lift an entry's Refused
|
|
2
|
+
* lift an entry's Refused or held-for-operator note, or move
|
|
3
3
|
* it to the head of its open
|
|
4
4
|
* section. Each comes as an exiting writer (the CLI) and a non-exiting `try…` core that refuses as
|
|
5
5
|
* data (the TUI and dashboard); entry lookup and the section's file target are
|
|
6
6
|
* backlog-write.ts's. */
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { readTextIfExists, writeTextAtomic } from "../files/files.js";
|
|
7
|
+
import { backlogTarget, findEntry, locateDashboardEntry, readEntryLines } from "./backlog-write.js";
|
|
8
|
+
import { withoutHeldForOperatorNotes, withoutRefusalNotes } from "./backlog-eligibility.js";
|
|
9
|
+
import { writeTextAtomic } from "../files/files.js";
|
|
11
10
|
import { fail } from "../cli/cli-output.js";
|
|
12
11
|
import { trimTrailingBlankLines } from "../text/text.js";
|
|
13
12
|
import { withPrimaryWorktreeLock } from "../concurrency/lock.js";
|
|
@@ -16,29 +15,21 @@ import { withPrimaryWorktreeLock } from "../concurrency/lock.js";
|
|
|
16
15
|
export function liftedNotePhrase(lifted) {
|
|
17
16
|
return lifted === "refused"
|
|
18
17
|
? "the Refused note"
|
|
19
|
-
:
|
|
20
|
-
? "the Waiting-for-the-next-failure note"
|
|
21
|
-
: "the held-for-operator note"; // vocabulary-ok: the held-for-operator note's release wording
|
|
18
|
+
: "the held-for-operator note"; // vocabulary-ok: the held-for-operator note's release wording
|
|
22
19
|
}
|
|
23
20
|
/** Drop the Refused note from `entry`'s slice of `lines`, in place, and return its kind; an
|
|
24
|
-
* entry with none loses its
|
|
25
|
-
* loses its held-for-operator Needs-review/Needs-replan note (the `(held N)` count at/above
|
|
21
|
+
* entry with none loses its held-for-operator Needs-review note (the `(held N)` count at/above
|
|
26
22
|
* threshold, plus its **Previously held N** line). Null (and `lines` untouched) when the entry
|
|
27
|
-
* carries
|
|
23
|
+
* carries neither, shared by releaseEntry and tryReleaseEntry so they cannot drift. */
|
|
28
24
|
function liftRefusal(lines, entry) {
|
|
29
25
|
const slice = lines.slice(entry.start - 1, entry.end);
|
|
30
26
|
let kept = withoutRefusalNotes(slice);
|
|
31
27
|
if (kept.length === slice.length) {
|
|
32
|
-
kept =
|
|
33
|
-
if (kept.length === slice.length)
|
|
34
|
-
|
|
35
|
-
if (kept.length === slice.length)
|
|
36
|
-
return null;
|
|
37
|
-
lines.splice(entry.start - 1, slice.length, ...kept);
|
|
38
|
-
return "held-for-operator"; // vocabulary-ok: the held-for-operator note's release wording
|
|
39
|
-
}
|
|
28
|
+
kept = withoutHeldForOperatorNotes(slice);
|
|
29
|
+
if (kept.length === slice.length)
|
|
30
|
+
return null;
|
|
40
31
|
lines.splice(entry.start - 1, slice.length, ...kept);
|
|
41
|
-
return "
|
|
32
|
+
return "held-for-operator"; // vocabulary-ok: the held-for-operator note's release wording
|
|
42
33
|
}
|
|
43
34
|
lines.splice(entry.start - 1, slice.length, ...kept);
|
|
44
35
|
return "refused";
|
|
@@ -48,13 +39,10 @@ function liftRefusal(lines, entry) {
|
|
|
48
39
|
export function tryReleaseEntry(root, kind, n, expectedTitle) {
|
|
49
40
|
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
50
41
|
return withPrimaryWorktreeLock(root, () => {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return { status: "gone" };
|
|
56
|
-
if (entry.title !== expectedTitle)
|
|
57
|
-
return { status: "moved", title: entry.title };
|
|
42
|
+
const located = locateDashboardEntry(root, fileName, sectionTitle, n, expectedTitle);
|
|
43
|
+
if (located.status !== "ok")
|
|
44
|
+
return located;
|
|
45
|
+
const { file, md, entry } = located;
|
|
58
46
|
const lines = md.split("\n");
|
|
59
47
|
const lifted = liftRefusal(lines, entry);
|
|
60
48
|
if (lifted === null)
|
|
@@ -63,7 +51,7 @@ export function tryReleaseEntry(root, kind, n, expectedTitle) {
|
|
|
63
51
|
return { status: "released", file: fileName, title: entry.title, lifted };
|
|
64
52
|
});
|
|
65
53
|
}
|
|
66
|
-
/** Lift the Refused
|
|
54
|
+
/** Lift the Refused or held-for-operator note from the
|
|
67
55
|
* `n`th entry (1-based, as `--list` numbers them) of the kind's open section and write the
|
|
68
56
|
* file, under the same lock the cancel and edit writers hold, returning which hold it lifted.
|
|
69
57
|
* An out-of-range position fails with locateEntry's wording; an entry with neither note
|
|
@@ -74,7 +62,7 @@ export function releaseEntry(root, kind, n) {
|
|
|
74
62
|
const { file, lines, entry } = readEntryLines(root, fileName, sectionTitle, n);
|
|
75
63
|
const lifted = liftRefusal(lines, entry);
|
|
76
64
|
if (lifted === null)
|
|
77
|
-
fail(`${kind} #${n} "${entry.title}" carries no Refused
|
|
65
|
+
fail(`${kind} #${n} "${entry.title}" carries no Refused or held-for-operator note`); // vocabulary-ok: the held-for-operator note's release wording
|
|
78
66
|
writeTextAtomic(file, lines.join("\n"));
|
|
79
67
|
return { file: fileName, title: entry.title, lifted };
|
|
80
68
|
});
|
|
@@ -117,13 +105,10 @@ function spliceEntryToTop(lines, sectionTitle, entry) {
|
|
|
117
105
|
export function tryMoveEntryToTop(root, kind, n, expectedTitle) {
|
|
118
106
|
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
119
107
|
return withPrimaryWorktreeLock(root, () => {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return { status: "gone" };
|
|
125
|
-
if (entry.title !== expectedTitle)
|
|
126
|
-
return { status: "moved", title: entry.title };
|
|
108
|
+
const located = locateDashboardEntry(root, fileName, sectionTitle, n, expectedTitle);
|
|
109
|
+
if (located.status !== "ok")
|
|
110
|
+
return located;
|
|
111
|
+
const { file, md, entry } = located;
|
|
127
112
|
if (n === 1)
|
|
128
113
|
return { status: "already_first", title: entry.title };
|
|
129
114
|
const lines = md.split("\n");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { entryMatches } from "./backlog.js";
|
|
2
|
-
import { groupBlockedSeries, partsLabel } from "./backlog-
|
|
2
|
+
import { groupBlockedSeries, partsLabel } from "./backlog-index-data.js";
|
|
3
3
|
import { holdMark, inProgressMark } from "./backlog-eligibility.js";
|
|
4
4
|
/** One `## ` section: its entries verbatim, or the single `_(none)_` placeholder line. When
|
|
5
5
|
* `holds` is given, entry `i`'s hold mark rides on its `### ` heading. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { increment } from "../collections.js";
|
|
3
4
|
import { readTextOrNull } from "../files/files.js";
|
|
@@ -6,7 +7,6 @@ import { BACKLOG_FILES, FINISHED_ARCHIVES, fenceTracker, headingMetadata, parseE
|
|
|
6
7
|
import { changeBaseRev, fileContentAt } from "../git/git.js";
|
|
7
8
|
import { plural } from "../text/phrases.js";
|
|
8
9
|
import { collapseWhitespace, truncate } from "../text/text.js";
|
|
9
|
-
import { seriesPart } from "./backlog-parts.js";
|
|
10
10
|
/** The archive files of PLANS.md and BUGS.md (FINISHED_ARCHIVES). */
|
|
11
11
|
const PLANS_ARCHIVE = FINISHED_ARCHIVES[0].archive;
|
|
12
12
|
const BUGS_ARCHIVE = FINISHED_ARCHIVES[1].archive;
|
|
@@ -252,7 +252,19 @@ export async function backlogStructureReason(wt, mainBranch, files) {
|
|
|
252
252
|
const baseRev = await changeBaseRev(wt, mainBranch);
|
|
253
253
|
for (const file of touched) {
|
|
254
254
|
// A deleted file reads as empty: every heading the base had is gone — rule (b).
|
|
255
|
-
const
|
|
255
|
+
const full = path.join(wt, file);
|
|
256
|
+
// A symlink is never followed out of the worktree (it holds no heading of its own — the
|
|
257
|
+
// same lstat policy the backlog-conflict resolver and the false-fix guard pin, INV-G3), so
|
|
258
|
+
// it too reads as empty rather than bringing an arbitrary host file's headings into this
|
|
259
|
+
// gate's verdict.
|
|
260
|
+
let head = "";
|
|
261
|
+
try {
|
|
262
|
+
if (fs.lstatSync(full).isFile())
|
|
263
|
+
head = readTextOrNull(full) ?? "";
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
head = "";
|
|
267
|
+
}
|
|
256
268
|
const base = await fileContentAt(wt, baseRev, file);
|
|
257
269
|
const headCounts = sectionTitleCounts(head);
|
|
258
270
|
const baseCounts = sectionTitleCounts(base);
|
|
@@ -356,146 +368,3 @@ the readers that scan one section, so no role sees them as backlog work:
|
|
|
356
368
|
${listed}
|
|
357
369
|
</backlog-structure>`;
|
|
358
370
|
}
|
|
359
|
-
/** The entries under one `## <sectionTitle>` section with their 1-based line ranges, in file
|
|
360
|
-
* order: the fence-aware walk sectionLines and parseEntryDetails share, carrying the line
|
|
361
|
-
* numbers neither returns. An entry starts at a `### ` line and ends at the line before the
|
|
362
|
-
* next `### ` or `## ` line (or EOF) — the same boundary parseEntryDetails closes bodies on —
|
|
363
|
-
* so a role reading lines `start`..`end` sees exactly the entry's heading and body. A `## ` or
|
|
364
|
-
* `### ` line inside a fenced code block is body content, never a boundary, through
|
|
365
|
-
* backlog-md.ts's shared fenceTracker. */
|
|
366
|
-
export function actionableEntryRanges(md, sectionTitle) {
|
|
367
|
-
const lines = md.split("\n");
|
|
368
|
-
const fenced = fenceTracker();
|
|
369
|
-
const entries = [];
|
|
370
|
-
let inSection = false;
|
|
371
|
-
let open = null;
|
|
372
|
-
// `end` is the 1-based number of the entry's last body line, i.e. the 0-based index of the
|
|
373
|
-
// next heading; at EOF it is the line count (the last 1-based line number).
|
|
374
|
-
const close = (end) => {
|
|
375
|
-
if (open !== null)
|
|
376
|
-
entries.push({ title: open.title, start: open.start, end });
|
|
377
|
-
open = null;
|
|
378
|
-
};
|
|
379
|
-
for (let i = 0; i < lines.length; i++) {
|
|
380
|
-
const line = lines[i] ?? "";
|
|
381
|
-
if (fenced.inside(line))
|
|
382
|
-
continue;
|
|
383
|
-
if (line.startsWith("## ")) {
|
|
384
|
-
close(i);
|
|
385
|
-
inSection = headingTitle(line, "## ") === sectionTitle;
|
|
386
|
-
}
|
|
387
|
-
else if (inSection && line.startsWith("### ")) {
|
|
388
|
-
close(i);
|
|
389
|
-
open = { title: headingTitle(line, "### "), start: i + 1 };
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
// A document ending in a newline splits to a trailing "" element; the last real line is one
|
|
393
|
-
// less, so an entry running to EOF ends there instead of at the phantom line.
|
|
394
|
-
close(md.endsWith("\n") ? lines.length - 1 : lines.length);
|
|
395
|
-
return entries;
|
|
396
|
-
}
|
|
397
|
-
/** Per-file cache of a section's actionable ranges (actionableEntryRangesFor). */
|
|
398
|
-
const actionableRangesCache = new Map();
|
|
399
|
-
/** `actionableEntryRanges` for one backlog file, stat-cached per (file path, section title):
|
|
400
|
-
* the per-poll and per-run readers (eligibility's eligibleEntries, claims' listedKeys) ask for
|
|
401
|
-
* the same section of the same file every couple of seconds, and BUGS.md's closed history —
|
|
402
|
-
* which the whole-file walk reads — grows without bound, so an unchanged file costs one stat
|
|
403
|
-
* instead of a full read, split, and section walk. Keyed by file path so distinct roots never
|
|
404
|
-
* collide, bounded inside cachedTextByStat. A missing or unreadable file reads as no ranges. The
|
|
405
|
-
* result is copied out, so each caller owns its data. */
|
|
406
|
-
export function actionableEntryRangesFor(root, file, sectionTitle) {
|
|
407
|
-
const full = path.join(root, file);
|
|
408
|
-
return (cachedTextByStat(actionableRangesCache, `${full}\u0000${sectionTitle}`, full, (md) => actionableEntryRanges(md, sectionTitle), (ranges) => ranges.slice()) ?? []);
|
|
409
|
-
}
|
|
410
|
-
/** True when `row` is a candidate for series-collapsing: it is a series part held only by an
|
|
411
|
-
* unmerged prerequisite (`{ blockedBy }`, not a Refused/needs-review/needs-replan note) and no live
|
|
412
|
-
* claim names it. Returns the row's series part when it collapses, else null. */
|
|
413
|
-
function collapsibleSeriesPart(row) {
|
|
414
|
-
if (row.claimer !== undefined)
|
|
415
|
-
return null;
|
|
416
|
-
if (typeof row.hold !== "object" || row.hold === null)
|
|
417
|
-
return null;
|
|
418
|
-
return seriesPart(row.title);
|
|
419
|
-
}
|
|
420
|
-
/** The collapsed part list for a series' blocked parts: each part as `i/n`, with a run of
|
|
421
|
-
* consecutive numeric parts (same total, adjacent values) joined into one en-dash range
|
|
422
|
-
* (`2/5–5/5`) and each lettered part (`2a`) listed singly. */
|
|
423
|
-
export function partsLabel(rows) {
|
|
424
|
-
const refs = rows.map((r) => seriesPart(r.title));
|
|
425
|
-
const pieces = [];
|
|
426
|
-
let start = null;
|
|
427
|
-
let prev = null;
|
|
428
|
-
const numeric = (p) => /^\d+$/.test(p);
|
|
429
|
-
const flush = () => {
|
|
430
|
-
if (start === null || prev === null)
|
|
431
|
-
return;
|
|
432
|
-
pieces.push(start.part === prev.part
|
|
433
|
-
? `${start.part}/${start.of}`
|
|
434
|
-
: `${start.part}/${start.of}–${prev.part}/${prev.of}`);
|
|
435
|
-
start = null;
|
|
436
|
-
prev = null;
|
|
437
|
-
};
|
|
438
|
-
for (const ref of refs) {
|
|
439
|
-
if (prev !== null &&
|
|
440
|
-
numeric(ref.part) &&
|
|
441
|
-
numeric(prev.part) &&
|
|
442
|
-
Number(ref.part) === Number(prev.part) + 1 &&
|
|
443
|
-
ref.of === prev.of) {
|
|
444
|
-
prev = ref;
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
flush();
|
|
448
|
-
start = ref;
|
|
449
|
-
prev = ref;
|
|
450
|
-
}
|
|
451
|
-
flush();
|
|
452
|
-
return pieces.join(", ");
|
|
453
|
-
}
|
|
454
|
-
/** Renders `rows` to index lines, collapsing a multi-part series' blocked parts to one line per
|
|
455
|
-
* series (this plan, "Compact backlog index, part 1/1"): a long series' chain of held parts would
|
|
456
|
-
* otherwise fill every role prompt with lines no role can act on. An entry collapses when it is a
|
|
457
|
-
* series part held only by an unmerged prerequisite and no live claim names it; a series with two
|
|
458
|
-
* or more such parts renders as one line at the position of its first collapsed part, spanning from
|
|
459
|
-
* that part's start to the last's end, with the part list (`2/4–4/4`) and a `[blocked: each waits
|
|
460
|
-
* on an earlier part]` mark. A series with a single blocked part, an eligible part, a note-held
|
|
461
|
-
* (Refused/needs-review/needs-replan) part and a claimed part keep their ordinary lines, so the
|
|
462
|
-
* eligibility rules the feature role's charter teaches are unchanged; the collapsed mark keeps the
|
|
463
|
-
* `[blocked: …]` prefix it already skips. */
|
|
464
|
-
/** The grouping half of backlog-index's collapseBlockedSeries, on any rows carrying a title,
|
|
465
|
-
* hold and claimer: each series with two or more collapsible blocked parts becomes one `series`
|
|
466
|
-
* group at its first part's place; every other row stays a `row` group in order. */
|
|
467
|
-
export function groupBlockedSeries(rows) {
|
|
468
|
-
// Group the collapsible rows by series, keyed on the lowercased series name.
|
|
469
|
-
const bySeries = new Map();
|
|
470
|
-
for (const row of rows) {
|
|
471
|
-
const part = collapsibleSeriesPart(row);
|
|
472
|
-
if (part === null)
|
|
473
|
-
continue;
|
|
474
|
-
const key = part.series.toLowerCase();
|
|
475
|
-
const list = bySeries.get(key) ?? [];
|
|
476
|
-
list.push(row);
|
|
477
|
-
bySeries.set(key, list);
|
|
478
|
-
}
|
|
479
|
-
// Only a series with two or more blocked parts collapses; a lone blocked part keeps its
|
|
480
|
-
// exact `requires` line.
|
|
481
|
-
const collapse = new Set();
|
|
482
|
-
for (const [key, list] of bySeries)
|
|
483
|
-
if (list.length >= 2)
|
|
484
|
-
collapse.add(key);
|
|
485
|
-
const groups = [];
|
|
486
|
-
const skipped = new Set();
|
|
487
|
-
for (const row of rows) {
|
|
488
|
-
if (skipped.has(row))
|
|
489
|
-
continue;
|
|
490
|
-
const part = collapsibleSeriesPart(row);
|
|
491
|
-
if (part !== null && collapse.has(part.series.toLowerCase())) {
|
|
492
|
-
const group = bySeries.get(part.series.toLowerCase());
|
|
493
|
-
groups.push({ kind: "series", series: part.series, rows: group });
|
|
494
|
-
for (const g of group)
|
|
495
|
-
skipped.add(g);
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
groups.push({ kind: "row", row });
|
|
499
|
-
}
|
|
500
|
-
return groups;
|
|
501
|
-
}
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* cli.ts beside the other command bodies. */
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { fencedHeadingTitle, fenceTracker, headingMetadata, nextSectionHeading } from "./backlog-md.js";
|
|
14
|
-
import { actionableEntryRanges
|
|
14
|
+
import { actionableEntryRanges } from "./backlog-index-data.js";
|
|
15
|
+
import { stripEntryStamp } from "./backlog-structure.js";
|
|
15
16
|
import { readTextIfExists, writeTextAtomic } from "../files/files.js";
|
|
16
17
|
import { formatDate } from "../text/datetime.js";
|
|
17
18
|
import { fail } from "../cli/cli-output.js";
|
|
@@ -138,6 +139,24 @@ export function readEntryLines(root, fileName, sectionTitle, n) {
|
|
|
138
139
|
const entry = locateEntry(md, fileName, sectionTitle, n);
|
|
139
140
|
return { file, lines: md.split("\n"), entry };
|
|
140
141
|
}
|
|
142
|
+
/** The title-checked entry lookup the four non-exiting dashboard writers share
|
|
143
|
+
* (tryCancelEntry, tryEditEntry here; tryReleaseEntry, tryMoveEntryToTop in backlog-release.ts):
|
|
144
|
+
* resolve `<fileName>`, read it — an absent file is an empty section, never a seed — locate the
|
|
145
|
+
* `n`th actionable entry, and compare its heading to `expectedTitle`. Returns `gone` when there
|
|
146
|
+
* is no entry at the position and `moved` when a different heading sits there, so the caller can
|
|
147
|
+
* refuse as data without writing; on success returns the `ok` file path, raw markdown and entry
|
|
148
|
+
* for the caller to splice and write its own outcome. A pure lookup, decided inside the caller's
|
|
149
|
+
* lock. */
|
|
150
|
+
export function locateDashboardEntry(root, fileName, sectionTitle, n, expectedTitle) {
|
|
151
|
+
const file = path.join(root, fileName);
|
|
152
|
+
const md = readTextIfExists(file) ?? "";
|
|
153
|
+
const entry = findEntry(md, sectionTitle, n);
|
|
154
|
+
if (entry === undefined)
|
|
155
|
+
return { status: "gone" };
|
|
156
|
+
if (entry.title !== expectedTitle)
|
|
157
|
+
return { status: "moved", title: entry.title };
|
|
158
|
+
return { status: "ok", file, md, entry };
|
|
159
|
+
}
|
|
141
160
|
/** The section-remove entry point, the appendEntry analogue: the same cross-process lock, so a
|
|
142
161
|
* cancel and a merge's working-tree fast-forward cannot each clobber the other's
|
|
143
162
|
* read-modify-write. */
|
|
@@ -145,7 +164,7 @@ function removeEntry(root, fileName, sectionTitle, n) {
|
|
|
145
164
|
return withPrimaryWorktreeLock(root, () => removeEntryLocked(root, fileName, sectionTitle, n));
|
|
146
165
|
}
|
|
147
166
|
/** Remove the `n`th entry (1-based, as `tumwater backlog` lists it) from `<root>/<fileName>`'s
|
|
148
|
-
* `## <sectionTitle>` section. The ranges come from actionableEntryRanges (backlog-
|
|
167
|
+
* `## <sectionTitle>` section. The ranges come from actionableEntryRanges (backlog-index-data.ts),
|
|
149
168
|
* whose walk is fence-aware, so a `## `/`### ` line quoted inside an entry's code fence is body
|
|
150
169
|
* text: slicing the range removes the heading and its body whole without mistaking a quoted line
|
|
151
170
|
* for the boundary. An out-of-range position (an empty section, or one holding only the
|
|
@@ -171,13 +190,10 @@ function spliceOutEntry(file, lines, entry) {
|
|
|
171
190
|
export function tryCancelEntry(root, kind, n, expectedTitle) {
|
|
172
191
|
const { fileName, sectionTitle } = backlogTarget(kind);
|
|
173
192
|
return withPrimaryWorktreeLock(root, () => {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return { status: "gone" };
|
|
179
|
-
if (entry.title !== expectedTitle)
|
|
180
|
-
return { status: "moved", title: entry.title };
|
|
193
|
+
const located = locateDashboardEntry(root, fileName, sectionTitle, n, expectedTitle);
|
|
194
|
+
if (located.status !== "ok")
|
|
195
|
+
return located;
|
|
196
|
+
const { file, md, entry } = located;
|
|
181
197
|
spliceOutEntry(file, md.split("\n"), entry);
|
|
182
198
|
return { status: "cancelled", file: fileName, title: entry.title };
|
|
183
199
|
});
|
|
@@ -191,7 +207,7 @@ function editEntry(root, fileName, sectionTitle, n, title, body) {
|
|
|
191
207
|
/** Replace the `n`th entry (1-based, as `tumwater backlog` lists it) of `<root>/<fileName>`'s
|
|
192
208
|
* `## <sectionTitle>` section with `### <title><stamp>` plus `body`, in place: the entry keeps
|
|
193
209
|
* its position and its original trailing stamp parenthetical. The ranges come from
|
|
194
|
-
* actionableEntryRanges (backlog-
|
|
210
|
+
* actionableEntryRanges (backlog-index-data.ts), whose fence-aware walk means a `## `/`### `
|
|
195
211
|
* line quoted inside an entry's code fence is body text, so the spliced range covers exactly
|
|
196
212
|
* the heading and body without mistaking a quoted line for the boundary. The stamp is the
|
|
197
213
|
* trailing suffix of the JOINED heading metadata (headingMetadata folds a wrapped heading onto
|
|
@@ -249,13 +265,10 @@ export function tryEditEntry(root, kind, n, expectedTitle, rawTitle, rawBody) {
|
|
|
249
265
|
const title = stripEntryStamp(collapseWhitespace(rawTitle));
|
|
250
266
|
const body = rawBody.replace(/\r\n?/g, "\n").trim();
|
|
251
267
|
return withPrimaryWorktreeLock(root, () => {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return { status: "gone" };
|
|
257
|
-
if (entry.title !== expectedTitle)
|
|
258
|
-
return { status: "moved", title: entry.title };
|
|
268
|
+
const located = locateDashboardEntry(root, fileName, sectionTitle, n, expectedTitle);
|
|
269
|
+
if (located.status !== "ok")
|
|
270
|
+
return located;
|
|
271
|
+
const { file, md, entry } = located;
|
|
259
272
|
const wrote = spliceInEditedEntry(file, md.split("\n"), entry, title, body);
|
|
260
273
|
return { status: wrote ? "edited" : "unchanged", file: fileName, title };
|
|
261
274
|
});
|
|
@@ -348,7 +361,7 @@ function appendOperatorEntry(root, fileName, template, sectionTitle, title, body
|
|
|
348
361
|
}
|
|
349
362
|
/** Append one tumwater-filed bug under BUGS.md's `## Open` with the heading as given (no operator
|
|
350
363
|
* stamp), for a caller that already holds the primary-worktree lock and commits the file itself
|
|
351
|
-
* (backlog-
|
|
364
|
+
* (backlog-file-bugs.ts's quarantine filing). Returns the heading line's title. */
|
|
352
365
|
export function appendBugEntryLocked(root, title, body) {
|
|
353
366
|
const t = collapseWhitespace(title);
|
|
354
367
|
requireBodyStructurallySound(body, "filed");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { cachedTextByStat } from "../files/stat-cache.js";
|
|
3
|
-
import { entryDates, entryLags, finishedArchiveOf, lastDate, parseEntryDetails } from "./backlog-md.js";
|
|
3
|
+
import { entryDates, entryLags, finishedArchiveOf, firstDate, lastDate, parseEntryDetails } from "./backlog-md.js";
|
|
4
|
+
import { CHECK_DECLARATION_MARKER } from "../roles/role-guidance.js";
|
|
4
5
|
/** The stat-cached file readers of the project backlog data shown on both dashboards: planned
|
|
5
6
|
* features (PLANS.md), open bugs (BUGS.md), and open questions (QUESTIONS.md). These are tracked
|
|
6
7
|
* markdown that roles edit, so readers must never show a stale entry — but the dashboards poll them
|
|
@@ -78,11 +79,12 @@ export function sectionCompletionLags(root, fileName, sectionTitle, dateRe) {
|
|
|
78
79
|
* bugs with `(found by …)` or `(reported …)`), parsed so `plan|bug --list` shows how long an entry
|
|
79
80
|
* has waited, without opening the file. Read only the title — the whole heading, never the body,
|
|
80
81
|
* because the stamp is the entry's own and the body may name other dates.
|
|
81
|
-
* null when the heading carries no such clause.
|
|
82
|
+
* null when the heading carries no such clause. Routed through backlog-md.ts's firstDate -
|
|
83
|
+
* the one first-match mechanism - with this module's own stamp regex.
|
|
84
|
+
* questionFilingDate (question-answer.ts) is the same recipe for the `asked by` verb. */
|
|
82
85
|
const STAMP_DATE_RE = /(?:planned|reported|found by)\b[^)]*?\b(\d{4}-\d{2}-\d{2})/;
|
|
83
86
|
export function filingDate(title) {
|
|
84
|
-
|
|
85
|
-
return m ? m[1] : null;
|
|
87
|
+
return firstDate(title, STAMP_DATE_RE);
|
|
86
88
|
}
|
|
87
89
|
/** The completion stamp of a `## Done` plan heading. */
|
|
88
90
|
export const DONE_DATE_RE = /done (\d{4}-\d{2}-\d{2})/;
|
|
@@ -168,6 +170,14 @@ export function openQuestions(root) {
|
|
|
168
170
|
export function plannedPlanEntries(root) {
|
|
169
171
|
return sectionEntries(root, "PLANS.md", "Planned");
|
|
170
172
|
}
|
|
173
|
+
/** The PLANS.md `## Planned` entry that declares this project's deterministic check — the one
|
|
174
|
+
* whose body carries CHECK_DECLARATION_MARKER (the plan role writes it under the no-check note's
|
|
175
|
+
* direction, src/prompt/gate-notes.ts buildNoCheckNote; the feature role reads it as its task).
|
|
176
|
+
* Null when no planned entry declares one, so a declaration already waiting is never
|
|
177
|
+
* double-planned and a feature run implements only a declaration the plan role actually wrote. */
|
|
178
|
+
export function declarationPlanEntry(root) {
|
|
179
|
+
return plannedPlanEntries(root).find((e) => e.body.includes(CHECK_DECLARATION_MARKER)) ?? null;
|
|
180
|
+
}
|
|
171
181
|
/** Open bugs as full entries (title + body), in file order. Missing or unreadable → []. */
|
|
172
182
|
export function openBugEntries(root) {
|
|
173
183
|
return sectionEntries(root, "BUGS.md", "Open");
|