@bensandee/tooling 0.84.0 → 0.84.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.
Files changed (2) hide show
  1. package/dist/bin.mjs +148 -62
  2. package/package.json +1 -1
package/dist/bin.mjs CHANGED
@@ -269,7 +269,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
269
269
  */
270
270
  const envFileLoad = loadEnvFileForBin(process.argv.slice(2), {
271
271
  name: "bst",
272
- version: "0.84.0"
272
+ version: "0.84.2"
273
273
  });
274
274
  //#endregion
275
275
  //#region ../cli-kit/src/logger.ts
@@ -5777,8 +5777,8 @@ function declaredPublishRegistries(targetDir, structure, rootPackageJson) {
5777
5777
  * shape of custom regex manager in the root `renovate.json` can rewrite both this version and this
5778
5778
  * digest (`customManagers` entries scoped to this file).
5779
5779
  */
5780
- const MISE_VERSION = "2026.9.18";
5781
- const MISE_LINUX_X64_SHA256 = "d24fe0bf7e613824ad99f7b8dac3f2b381a37b9f75f84dd250855217095a8de4";
5780
+ const MISE_VERSION = "2026.10.0";
5781
+ const MISE_LINUX_X64_SHA256 = "57ced973f968b8fbab07aa8e32bd7077d4a357e200a22356d98963c723c6de0a";
5782
5782
  /** Where generated Forgejo workflows fetch the shared mise setup action: a composite action in
5783
5783
  * `bensandee/common`, on the forge every fleet repo lives on. GitHub cannot reach it; the
5784
5784
  * generators keep inline steps there. */
@@ -7163,7 +7163,7 @@ function buildManagedDevDeps(ctx) {
7163
7163
  const devDeps = { ...rootDevDeps(ctx.config) };
7164
7164
  if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
7165
7165
  if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.14.5";
7166
- if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.84.0";
7166
+ if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.84.2";
7167
7167
  if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.15.2";
7168
7168
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
7169
7169
  "@changesets/cli": "3.0.3",
@@ -11950,6 +11950,10 @@ const GIT_GUIDE_BODY = [
11950
11950
  "",
11951
11951
  "Start a pull request's title with the stage it carries: `Spec:` for a spec, `Plan:` for a plan, and no stem once it carries the implementation. A draft keeps its `WIP:` marker in front (`WIP: Plan: Add retry to uploads`). When a pull request moves on to the next stage, move the stem with it. This is informational -- nothing checks it and nothing fails without it -- but it tells a reviewer at a glance what they are reading.",
11952
11952
  "",
11953
+ "**Drop `WIP:` and fix the stem when the work is done.** Once the pull request carries finished work you are handing over for review -- a spec or a plan ready to approve, or an implementation whose check passes -- remove the `WIP:` marker and make the stem match what it now carries: no stem for an implementation. Do it in the same step that reports the work done, without being asked. A marker left behind tells a reviewer the work is still moving.",
11954
+ "",
11955
+ "`pr:update` never changes a pull request's draft state, so it keeps `WIP:` on a retitle. On Forgejo a draft is only that title prefix: retitle the pull request without it (`bst-fj issue:update <n> --title \"<title>\"` where `bst-fj` is installed). On GitHub, run `gh pr ready` and then edit the title.",
11956
+ "",
11953
11957
  "## Merging the default branch in",
11954
11958
  "",
11955
11959
  "**A merge with no conflicts is not yet a merge that works.** Read the incoming diff for what your branch builds on -- a field made required, a shared schema, a size budget -- and run the full check before you push the merge. Fix what it finds in the merge commit itself, so that commit passes on its own.",
@@ -12692,7 +12696,7 @@ function reportFailures(failed, command) {
12692
12696
  function generateMigratePrompt(results, config, detected) {
12693
12697
  const sections = [];
12694
12698
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
12695
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.84.0 repo:sync\` on ${timestamp}_\n`);
12699
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.84.2 repo:sync\` on ${timestamp}_\n`);
12696
12700
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
12697
12701
  if (changed.length > 0) {
12698
12702
  const items = changed.map((r) => {
@@ -25935,7 +25939,7 @@ async function refusalMessage(res) {
25935
25939
  try {
25936
25940
  const parsed = ErrorBodySchema.safeParse(JSON.parse(text));
25937
25941
  if (parsed.success) return parsed.data.message;
25938
- } catch {}
25942
+ } catch (_error) {}
25939
25943
  return text;
25940
25944
  }
25941
25945
  /** Delete one branch. The name is encoded per segment so `feat/a/b` keeps its slashes. */
@@ -26179,12 +26183,15 @@ function conventionalActiveDirs(exec) {
26179
26183
  * prefix replaced by the terminal verb (`docs/plan-foo.md` ->
26180
26184
  * `docs/retired/completed-foo.md`); anything else (a `plans`/`specs` dir doc,
26181
26185
  * date-prefixed superpowers plan or not) keeps its basename and moves to the
26182
- * sibling `retired/` directory. The single place `docs:retire` reads this
26183
- * convention from, so it can never drift from what `discoverPairs` expects.
26186
+ * sibling `retired/` directory. A doc already in a retired directory stays
26187
+ * where it is -- re-retiring one to add its retire paragraph once moved it a
26188
+ * level deeper, into `retired/retired/`. The single place `docs:retire` reads
26189
+ * this convention from, so it can never drift from what `discoverPairs` expects.
26184
26190
  */
26185
26191
  function retireDestination(src, verb) {
26186
26192
  const dir = path.posix.dirname(src);
26187
26193
  const base = path.posix.basename(src);
26194
+ if (RETIRED_DIR_NAMES$1.includes(path.posix.basename(dir))) return src;
26188
26195
  if (dir === DOCS_ROOT && LEGACY_ACTIVE_PATTERN.test(base)) return `${DOCS_ROOT}/${RETIRED_DIR}/${verb}-${base.slice(5)}`;
26189
26196
  return `${dir}/${RETIRED_DIR}/${base}`;
26190
26197
  }
@@ -27107,7 +27114,7 @@ function buildRelocatePlan(exec, moves) {
27107
27114
  for (const { from, to } of moves) {
27108
27115
  if (moveMap.has(from)) throw new FatalError(`docs:relocate: duplicate source ${from}`);
27109
27116
  moveMap.set(from, to);
27110
- if (exec.exists(from)) plannedMoves.push({
27117
+ if (exec.exists(from) && from !== to) plannedMoves.push({
27111
27118
  from,
27112
27119
  to,
27113
27120
  status: "pending"
@@ -27311,14 +27318,12 @@ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
27311
27318
  function validateRetireArgs(exec, src, opts) {
27312
27319
  if (!isRetireVerb(opts.status)) throw new FatalError(`docs:retire: unrecognized --status "${opts.status}" (expected one of: ${RETIRE_VERBS.join(", ")})`);
27313
27320
  const verb = opts.status;
27314
- const date = opts.date ?? localToday(/* @__PURE__ */ new Date());
27315
- if (!DATE_RE.test(date)) throw new FatalError(`docs:retire: --date must be YYYY-MM-DD, got: ${date}`);
27321
+ if (opts.date !== void 0 && !DATE_RE.test(opts.date)) throw new FatalError(`docs:retire: --date must be YYYY-MM-DD, got: ${opts.date}`);
27316
27322
  if (!src.endsWith(".md")) throw new FatalError(`docs:retire: source must be a .md file: ${src}`);
27317
27323
  const dest = opts.dest !== void 0 ? path.posix.normalize(opts.dest) : retireDestination(src, verb);
27318
27324
  if (src !== dest && exec.exists(src) && exec.exists(dest)) throw new FatalError(`docs:retire: refusing to overwrite existing destination: ${dest}`);
27319
27325
  return {
27320
27326
  verb,
27321
- date,
27322
27327
  dest
27323
27328
  };
27324
27329
  }
@@ -27482,7 +27487,7 @@ function formatRetireLines(src, dest, relocatePlan, lifecycleSummary) {
27482
27487
  */
27483
27488
  function runDocsRetire(exec, src, opts) {
27484
27489
  const vc = { verbose: opts.verbose ?? false };
27485
- const { verb, date, dest } = validateRetireArgs(exec, src, opts);
27490
+ const { verb, dest } = validateRetireArgs(exec, src, opts);
27486
27491
  const relocatePlan = buildRelocatePlan(exec, [{
27487
27492
  from: src,
27488
27493
  to: dest
@@ -27492,6 +27497,7 @@ function runDocsRetire(exec, src, opts) {
27492
27497
  const relinkRewrite = relocatePlan.rewrites.find((r) => r.file === dest);
27493
27498
  const baseContent = relinkRewrite?.content ?? exec.readFile(move.status === "pending" ? src : dest);
27494
27499
  if (baseContent === void 0) throw new FatalError(`docs:retire: cannot read ${move.status === "pending" ? src : dest}`);
27500
+ const date = opts.date ?? datedStatusValue(baseContent) ?? localToday(/* @__PURE__ */ new Date());
27495
27501
  const { content: lifecycleContent, summary, edits: lifecycle } = applyLifecycleEdits(baseContent, dest, verb, date, opts.shipped);
27496
27502
  const delinked = delinkDocument(lifecycleContent, path.posix.dirname(dest));
27497
27503
  const finalContent = delinked.content;
@@ -27561,7 +27567,7 @@ const docsRetireCommand = defineCommand({
27561
27567
  },
27562
27568
  date: {
27563
27569
  type: "string",
27564
- description: "Retirement date, YYYY-MM-DD (defaults to today, local time)"
27570
+ description: "Retirement date, YYYY-MM-DD (defaults to the date of an existing terminal Status: line, else today, local time)"
27565
27571
  },
27566
27572
  shipped: {
27567
27573
  type: "string",
@@ -27877,13 +27883,39 @@ function normalizeIssue(raw) {
27877
27883
  };
27878
27884
  }
27879
27885
  /**
27886
+ * Parse the backing issue number out of a PR body's `Closes #<n>` line. Returns null when the
27887
+ * body carries no such line (a hand-written PR, or a body that could not be read). Lives beside
27888
+ * {@link toIssueSummaries}, which keeps it for every pull request a search returns.
27889
+ *
27890
+ * `Closes` only, deliberately -- narrower than the `Closes`/`Fixes`/`Resolves` vocabulary the
27891
+ * closing-ref machinery in `closing-refs.ts` recognizes, and it must stay
27892
+ * that way. A body whose *first* closing reference names some other issue is what a caller
27893
+ * resolves the backing issue against, and a branch name derived from that number follows it, so
27894
+ * every keyword added here widens the set of bodies that can be misrouted. The
27895
+ * cost of the narrow set is the opposite failure and the cheaper one: an adopted body already
27896
+ * saying `Fixes #<n>` gets no `Closes` line appended (it already closes the issue), so this
27897
+ * returns null and the PR is treated as having no backing issue -- which still closes its issue
27898
+ * on merge and still runs on the PR's own head ref, losing only the issue mapping.
27899
+ */
27900
+ function backingIssueNumber(prBody) {
27901
+ const match = /Closes #(\d+)/i.exec(prBody);
27902
+ if (match === null) return null;
27903
+ const parsed = Number(match[1]);
27904
+ return Number.isNaN(parsed) ? null : parsed;
27905
+ }
27906
+ /**
27880
27907
  * Normalize a raw issue list, drop pull requests (both platforms serve them
27881
27908
  * through the issues endpoint), and strip bodies down to summaries. Shared by
27882
27909
  * the Forgejo and GitHub list transports so the post-processing chain lives
27883
27910
  * in exactly one place.
27884
27911
  */
27885
27912
  function toIssueSummaries(raw, type = "issues") {
27886
- const summaries = raw.map(normalizeIssue).map(({ body: _body, ...summary }) => summary);
27913
+ const summaries = raw.map(normalizeIssue).map(({ body, ...summary }) => {
27914
+ const result = summary;
27915
+ const backingIssue = summary.isPullRequest ? backingIssueNumber(body) : null;
27916
+ if (backingIssue !== null) result.backingIssue = backingIssue;
27917
+ return result;
27918
+ });
27887
27919
  return type === "issues" ? summaries.filter((s) => !s.isPullRequest) : summaries;
27888
27920
  }
27889
27921
  /** Fetch a single issue by number. */
@@ -30533,6 +30565,71 @@ function writeReviewPackage(planPath, base, head, cwd, out) {
30533
30565
  };
30534
30566
  }
30535
30567
  //#endregion
30568
+ //#region src/plan-workspace/task-label.ts
30569
+ /** The line shapes `plan:brief` and `plan:status` both read a task's number from. */
30570
+ const HEADING = /^(#{1,6})[ \t]+(.*)$/;
30571
+ const BOX = /^([ \t]*)[-*] \[[ xX]\][ \t]*(.*)$/;
30572
+ const LABEL = /^Task[ \t]+([0-9]+)(?![0-9])/;
30573
+ /** A line that opens a fence: three or more of one character, after any amount of whitespace. */
30574
+ const FENCE_OPEN = /^[ \t]*(`{3,}|~{3,})/;
30575
+ /** The same shape with nothing but whitespace after the run, which is what closes a fence. */
30576
+ const FENCE_CLOSE = /^[ \t]*(`{3,}|~{3,})[ \t]*$/;
30577
+ /** Whether a line closes the fence opened by `fenceChar` repeated `fenceLength` times. */
30578
+ function closesFence(text, fenceChar, fenceLength) {
30579
+ const run = FENCE_CLOSE.exec(text)?.[1];
30580
+ if (run === void 0) return false;
30581
+ return run[0] === fenceChar && run.length >= fenceLength;
30582
+ }
30583
+ /**
30584
+ * Which lines of a document a rendered markdown viewer would show as code rather than parse as
30585
+ * markdown: true at every fence line and every line inside one, false elsewhere. The rules are
30586
+ * CommonMark's, with two choices that belong to these readers:
30587
+ *
30588
+ * - Any indentation opens a fence, not the 0-3 spaces CommonMark allows, because plans nest
30589
+ * example fences under list items and none of the readers tracks list nesting.
30590
+ * - An unclosed fence runs to the end of the document, which is how a renderer shows one.
30591
+ *
30592
+ * A closer is the opener's own character repeated at least as many times, with nothing after it
30593
+ * but whitespace; a run of the other character, a shorter run, or a run carrying an info string is
30594
+ * code content and keeps the fence open.
30595
+ *
30596
+ * `stripFencedCodeBlocks` in `docs-check/links.ts` walks fences by the same rule but is a
30597
+ * different reader with different rules (it limits the opener's indent and tracks blockquote
30598
+ * depth), which is why this walk is not shared with it.
30599
+ */
30600
+ function fencedLines(lines) {
30601
+ const fenced = lines.map(() => false);
30602
+ let fenceChar;
30603
+ let fenceLength = 0;
30604
+ for (let index = 0; index < lines.length; index++) {
30605
+ const text = lines[index] ?? "";
30606
+ if (fenceChar === void 0) {
30607
+ const run = FENCE_OPEN.exec(text)?.[1];
30608
+ if (run === void 0) continue;
30609
+ fenceChar = run[0];
30610
+ fenceLength = run.length;
30611
+ fenced[index] = true;
30612
+ continue;
30613
+ }
30614
+ fenced[index] = true;
30615
+ if (closesFence(text, fenceChar, fenceLength)) {
30616
+ fenceChar = void 0;
30617
+ fenceLength = 0;
30618
+ }
30619
+ }
30620
+ return fenced;
30621
+ }
30622
+ /**
30623
+ * The `<n>` of a heading or checkbox text that starts `Task <n>` (after an opening `**` or `__`),
30624
+ * or null. A number with a leading zero reads as null, because `plan:brief <n>` matches the digits
30625
+ * as written and could never find `Task 05`.
30626
+ */
30627
+ function taskLabelNumber(text) {
30628
+ const digits = LABEL.exec(text.replace(/^(\*\*|__)/, ""))?.[1];
30629
+ if (digits === void 0 || String(Number(digits)) !== digits) return null;
30630
+ return Number(digits);
30631
+ }
30632
+ //#endregion
30536
30633
  //#region src/plan-workspace/plan-boxes.ts
30537
30634
  /**
30538
30635
  * Read a plan document's checkboxes and the notes committed under them. One strict parser, fail-
@@ -30545,8 +30642,14 @@ function writeReviewPackage(planPath, base, head, cwd, out) {
30545
30642
  * boundary matters because a batched plan is a sequence of `### <title>`
30546
30643
  * sections and a note must never leak across one.
30547
30644
  *
30548
- * The agent imports this module (`run-outcome.ts`, `run-gates.ts`, `prompt.ts`, `run.ts`), so a
30549
- * change here changes how the agent reads a plan as well as what `plan:status` reports.
30645
+ * A line inside a fenced code block (`fencedLines`, `task-label.ts`) is invisible in every role:
30646
+ * a fenced box is not counted, a fenced heading does not end a block, and a fenced note or
30647
+ * declination is not one. The fail-safe direction holds -- an ambiguous line degrades to "not
30648
+ * declined".
30649
+ *
30650
+ * The agent imports this module (`run-outcome.ts`, `run-gates.ts`, `run.ts`, `plan-box-diff.ts`,
30651
+ * `plan-box-watch.ts`, `plan-box-walk.ts`, `plan-progress.ts`), so a change here changes how the
30652
+ * agent reads a plan as well as what `plan:status` reports.
30550
30653
  *
30551
30654
  * An unchecked box is **declined** when its block carries a line matching
30552
30655
  * `^[ \t]*(?:[-*][ \t]+)?\*\*Declined:\*\*[ \t]*(.*)$` whose trimmed remainder is non-empty; that
@@ -30599,8 +30702,10 @@ function unwrapEmphasis(rawTitle) {
30599
30702
  /** Parse every checkbox on a plan document, each with its own block already resolved. */
30600
30703
  function parsePlanBoxes(planText) {
30601
30704
  const lines = planText.split("\n");
30705
+ const fenced = fencedLines(lines);
30602
30706
  const boxStarts = [];
30603
30707
  for (let index = 0; index < lines.length; index++) {
30708
+ if (fenced[index] === true) continue;
30604
30709
  const match = BOX_LINE_REGEX.exec(lines[index] ?? "");
30605
30710
  if (match === null) continue;
30606
30711
  boxStarts.push({
@@ -30612,13 +30717,17 @@ function parsePlanBoxes(planText) {
30612
30717
  return boxStarts.map((box, position) => {
30613
30718
  const nextBoxIndex = boxStarts[position + 1]?.index ?? lines.length;
30614
30719
  let blockEnd = nextBoxIndex;
30615
- for (let index = box.index + 1; index < nextBoxIndex; index++) if (HEADING_REGEX.test(lines[index] ?? "")) {
30616
- blockEnd = index;
30617
- break;
30720
+ for (let index = box.index + 1; index < nextBoxIndex; index++) {
30721
+ if (fenced[index] === true) continue;
30722
+ if (HEADING_REGEX.test(lines[index] ?? "")) {
30723
+ blockEnd = index;
30724
+ break;
30725
+ }
30618
30726
  }
30619
30727
  const title = unwrapEmphasis(box.rawTitle);
30620
30728
  const retireTask = RETIRE_TASK_REGEX.test(title);
30621
30729
  if (box.checked) return {
30730
+ line: box.index,
30622
30731
  checked: true,
30623
30732
  declined: false,
30624
30733
  reason: null,
@@ -30628,6 +30737,7 @@ function parsePlanBoxes(planText) {
30628
30737
  };
30629
30738
  const notes = [];
30630
30739
  for (let index = box.index + 1; index < blockEnd; index++) {
30740
+ if (fenced[index] === true) continue;
30631
30741
  const noteMatch = NOTE_LINE_REGEX.exec(lines[index] ?? "");
30632
30742
  if (noteMatch === null) continue;
30633
30743
  const text = (noteMatch[2] ?? "").trim();
@@ -30638,6 +30748,7 @@ function parsePlanBoxes(planText) {
30638
30748
  }
30639
30749
  let reason = null;
30640
30750
  for (let index = box.index + 1; index < blockEnd; index++) {
30751
+ if (fenced[index] === true) continue;
30641
30752
  const declinedMatch = DECLINED_LINE_REGEX.exec(lines[index] ?? "");
30642
30753
  if (declinedMatch === null) continue;
30643
30754
  const remainder = (declinedMatch[1] ?? "").trim();
@@ -30647,6 +30758,7 @@ function parsePlanBoxes(planText) {
30647
30758
  }
30648
30759
  }
30649
30760
  return {
30761
+ line: box.index,
30650
30762
  checked: false,
30651
30763
  declined: reason !== null,
30652
30764
  reason,
@@ -30657,45 +30769,23 @@ function parsePlanBoxes(planText) {
30657
30769
  });
30658
30770
  }
30659
30771
  //#endregion
30660
- //#region src/plan-workspace/task-label.ts
30661
- /** The line shapes `plan:brief` and `plan:status` both read a task's number from. */
30662
- const FENCE = /^[ \t]*(```|~~~)/;
30663
- const HEADING = /^(#{1,6})[ \t]+(.*)$/;
30664
- const BOX = /^([ \t]*)[-*] \[[ xX]\][ \t]*(.*)$/;
30665
- const LABEL = /^Task[ \t]+([0-9]+)(?![0-9])/;
30666
- /**
30667
- * The `<n>` of a heading or checkbox text that starts `Task <n>` (after an opening `**` or `__`),
30668
- * or null. A number with a leading zero reads as null, because `plan:brief <n>` matches the digits
30669
- * as written and could never find `Task 05`.
30670
- */
30671
- function taskLabelNumber(text) {
30672
- const digits = LABEL.exec(text.replace(/^(\*\*|__)/, ""))?.[1];
30673
- if (digits === void 0 || String(Number(digits)) !== digits) return null;
30674
- return Number(digits);
30675
- }
30676
- //#endregion
30677
30772
  //#region src/plan-workspace/status.ts
30678
30773
  const RETIRED_SEGMENT = /(^|\/)retired\//;
30679
30774
  /**
30680
- * The nearest `Task <n>` label at or before the plan's `ordinal`-th box line. Box lines are counted
30681
- * fence-blind, as `parsePlanBoxes` counts them, so the ordinal lands on the same line; only the
30682
- * label tracking skips fences, as `plan:brief` does.
30775
+ * The nearest `Task <n>` label at or before the box on `line`, the box's own title winning. The
30776
+ * walk shares `fencedLines` with `plan:brief`, so a fenced `Task <n>` line is no more a label here
30777
+ * than it is a task boundary there.
30683
30778
  */
30684
- function enclosingTaskNumber(planText, ordinal) {
30685
- let inFence = false;
30686
- let boxes = -1;
30779
+ function enclosingTaskNumber(planText, line) {
30780
+ const lines = planText.split("\n");
30781
+ const fenced = fencedLines(lines);
30687
30782
  let label = null;
30688
- for (const line of planText.split("\n")) {
30689
- const box = BOX.exec(line);
30690
- if (box !== null) boxes++;
30691
- if (box !== null && boxes === ordinal) return taskLabelNumber(box[2] ?? "") ?? label;
30692
- if (FENCE.test(line)) {
30693
- inFence = !inFence;
30694
- continue;
30695
- }
30696
- if (inFence) continue;
30697
- const text = box?.[2] ?? HEADING.exec(line)?.[2];
30698
- const n = text === void 0 ? null : taskLabelNumber(text);
30783
+ for (let index = 0; index < lines.length; index++) {
30784
+ const text = lines[index] ?? "";
30785
+ if (index === line) return taskLabelNumber(BOX.exec(text)?.[2] ?? "") ?? label;
30786
+ if (fenced[index] === true) continue;
30787
+ const title = BOX.exec(text)?.[2] ?? HEADING.exec(text)?.[2];
30788
+ const n = title === void 0 ? null : taskLabelNumber(title);
30699
30789
  if (n !== null) label = n;
30700
30790
  }
30701
30791
  return null;
@@ -30718,7 +30808,7 @@ function planStatus(planText, planPath) {
30718
30808
  outstanding: open.length,
30719
30809
  declined: boxes.filter((box) => box.declined).length,
30720
30810
  next: nextBox === void 0 || nextBox.title === "" ? null : nextBox.title,
30721
- nextTask: nextBox === void 0 ? null : enclosingTaskNumber(planText, boxes.indexOf(nextBox)),
30811
+ nextTask: nextBox === void 0 ? null : enclosingTaskNumber(planText, nextBox.line),
30722
30812
  onlyRetireLeft: open.length > 0 && open.every((box) => box.retireTask),
30723
30813
  notes
30724
30814
  };
@@ -30735,17 +30825,13 @@ function isTaskLabel(text, n) {
30735
30825
  */
30736
30826
  function extractTask(planText, n) {
30737
30827
  const lines = planText.split("\n");
30738
- let inFence = false;
30828
+ const fenced = fencedLines(lines);
30739
30829
  let start = -1;
30740
30830
  let shape = null;
30741
30831
  let end = lines.length;
30742
30832
  for (let index = 0; index < lines.length; index++) {
30833
+ if (fenced[index] === true) continue;
30743
30834
  const line = lines[index] ?? "";
30744
- if (FENCE.test(line)) {
30745
- inFence = !inFence;
30746
- continue;
30747
- }
30748
- if (inFence) continue;
30749
30835
  const heading = HEADING.exec(line);
30750
30836
  const box = BOX.exec(line);
30751
30837
  if (shape === null) {
@@ -31171,7 +31257,7 @@ setForgejoLogger(log$1);
31171
31257
  * `local <sha>[-dirty]` otherwise -- so an artifact built from the working tree
31172
31258
  * can't pass itself off as the published release whose version it carries.
31173
31259
  */
31174
- const displayVersion = "0.84.0";
31260
+ const displayVersion = "0.84.2";
31175
31261
  /**
31176
31262
  * The registered `bst` subcommand map, exported so both the JSON entry path below (resolving a
31177
31263
  * command name before citty parses anything) and the coverage test (enumerating every
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.84.0",
3
+ "version": "0.84.2",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "license": "MIT",
6
6
  "repository": {