@pmelab/gtd 63.0.0 → 64.1.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.
Files changed (80) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +12 -11
  3. package/dist/chunks/{flows-DENt0uWV.mjs → flows-B7ZRCe-B.mjs} +1 -1
  4. package/dist/chunks/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -1
  5. package/dist/chunks/{steering-DF_BMadH.mjs → steering-Cem5i6Xl.mjs} +422 -2
  6. package/dist/chunks/{testkit-DwypA7cH.mjs → testkit-DfWZQCWQ.mjs} +1073 -328
  7. package/dist/flows.mjs +1 -1
  8. package/dist/gtd.bundle.mjs +4372 -1310
  9. package/dist/gtd.core.mjs +126 -118
  10. package/dist/steering.mjs +2 -2
  11. package/dist/testkit/gherkin.mjs +2 -2
  12. package/dist/testkit.mjs +2 -2
  13. package/dist/workflow-kit.mjs +2 -2
  14. package/package.json +1 -1
  15. package/src/cli/Cli.test.ts +5 -8
  16. package/src/cli/Cli.ts +22 -28
  17. package/src/cli/Output.ts +2 -1
  18. package/src/cli/Program.test.ts +55 -53
  19. package/src/cli/Program.ts +34 -34
  20. package/src/cli/ToolCommands.ts +3 -4
  21. package/src/edge/Edge.test.ts +38 -12
  22. package/src/edge/Edge.ts +53 -19
  23. package/src/emit/SteeringMode.test.ts +4 -2
  24. package/src/emit/SteeringMode.ts +31 -10
  25. package/src/flows/index.ts +1 -0
  26. package/src/flows/runtime.test.ts +2 -1
  27. package/src/flows/runtime.ts +8 -2
  28. package/src/gherkin/messages.test.ts +1 -1
  29. package/src/gherkin/module-config.feature +1 -1
  30. package/src/gherkin/testkit.feature +2 -2
  31. package/src/install/Version.ts +14 -13
  32. package/src/platform/Host.ts +75 -22
  33. package/src/platform/NodePinFile.ts +44 -0
  34. package/src/platform/NodeRuntime.ts +19 -0
  35. package/src/platform/NodeSync.ts +64 -0
  36. package/src/platform/PinFile.ts +0 -50
  37. package/src/platform/Sha.ts +121 -0
  38. package/src/platform/SyncIo.ts +35 -0
  39. package/src/platform/Workspace.test.ts +8 -3
  40. package/src/platform/Workspace.ts +82 -123
  41. package/src/platform/WorktreeState.test.ts +8 -4
  42. package/src/platform/WorktreeState.ts +21 -19
  43. package/src/platform/index.ts +18 -5
  44. package/src/replay/Format.test.ts +16 -13
  45. package/src/replay/Format.ts +4 -2
  46. package/src/replay/Replay.test.ts +25 -5
  47. package/src/replay/Replay.ts +10 -6
  48. package/src/replay/Trailers.test.ts +10 -10
  49. package/src/replay/Trailers.ts +23 -1
  50. package/src/replay/index.ts +1 -0
  51. package/src/steering/Declared.test.ts +218 -0
  52. package/src/steering/Declared.ts +607 -0
  53. package/src/steering/FORMATS.md +3 -0
  54. package/src/steering/index.ts +12 -0
  55. package/src/steering/internal.ts +1 -0
  56. package/src/step/planStart.test.ts +2 -2
  57. package/src/step/planStep.test.ts +4 -4
  58. package/src/testkit/TestRepo.test.ts +1 -1
  59. package/src/testkit/TestRepo.ts +4 -18
  60. package/src/twins/EmittedScriptRecognizer.ts +5 -3
  61. package/src/twins/EmittedScripts.test.ts +19 -0
  62. package/src/twins/Host.test.ts +151 -0
  63. package/src/twins/Layers.test.ts +2 -2
  64. package/src/twins/Layers.ts +44 -18
  65. package/src/twins/Path.test.ts +52 -0
  66. package/src/twins/SyncIo.test.ts +153 -0
  67. package/src/twins/SyncIo.ts +159 -0
  68. package/src/twins/cliIo.ts +8 -1
  69. package/src/twins/index.ts +5 -0
  70. package/src/workflow/SteeringFormats.ts +4 -0
  71. package/src/workflow/Workflow.ts +2 -0
  72. package/src/workflow/discovery.test.ts +6 -5
  73. package/src/workflow/discovery.ts +43 -32
  74. package/src/workflow/fingerprint.test.ts +23 -3
  75. package/src/workflow/fingerprint.ts +9 -11
  76. package/src/workflow/index.ts +5 -1
  77. package/src/workflow/load.test.ts +16 -6
  78. package/src/workflow/load.ts +107 -69
  79. package/src/workflow/pin.test.ts +3 -2
  80. package/src/workflow/pin.ts +17 -11
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "63.0.0",
3
+ "version": "64.1.0",
4
4
  "description": "Drive the gtd loop inside Claude Code: agent turns run as subagents, gtd decides every beat",
5
5
  "author": {
6
6
  "name": "Philipp Melab"
package/README.md CHANGED
@@ -610,14 +610,14 @@ approvals as `approvals` (`[]` when none), and `gtd history show` prints each as
610
610
  gtd is one package, `@pmelab/gtd`, and every entrypoint shares its version and
611
611
  [SemVer promise](https://github.com/pmelab/gtd/blob/main/COMPATIBILITY.md).
612
612
 
613
- | Import | What it is |
614
- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
615
- | `@pmelab/gtd/flows` | The flow API a workflow is written in: the steps (`agent`, `human`, `run`, …), `scope`, `workflow()`, workflow factories, `defineConfig` and `FLOWS_PROTOCOL`. |
616
- | `@pmelab/gtd/wire` | The driver protocol: `WIRE_SCHEMA`, the exit codes and the types of every `--json` document. |
617
- | `@pmelab/gtd/steering` | The steering-file formats (`qa`, `review`, `findings`), the review-note readers, and `sections`, `threads` and `openQuestions` as plain functions. |
618
- | `@pmelab/gtd/workflow-kit` | Policy-free building blocks to compose a workflow from: health checks, quality laps, scenario freezing, review tails. See [Workflow kit](https://github.com/pmelab/gtd/blob/main/docs/workflow-kit.md). |
619
- | `@pmelab/gtd/testkit` | Runs any workflow against the real engine in an in-memory repository, under any test runner, plus a flow-context harness for rendering prompts. See [Testing a workflow](https://github.com/pmelab/gtd/blob/main/docs/testing.md). |
620
- | `@pmelab/gtd/testkit/gherkin` | The testkit as quickpickle steps for vitest. Needs `vitest` and `quickpickle`, optional peer dependencies. |
613
+ | Import | What it is |
614
+ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
615
+ | `@pmelab/gtd/flows` | The flow API a workflow is written in: the steps (`agent`, `human`, `run`, …), `scope`, `workflow()`, workflow factories, `defineConfig` and `FLOWS_PROTOCOL`. |
616
+ | `@pmelab/gtd/wire` | The driver protocol: `WIRE_SCHEMA`, the exit codes and the types of every `--json` document. |
617
+ | `@pmelab/gtd/steering` | The steering-file formats (`qa`, `review`, `findings`), `listFormat`/`sectionFormat` to [declare a format](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#declared-formats) a step's `mode` validates against, the review-note readers, and `sections`, `threads` and `openQuestions` as plain functions. |
618
+ | `@pmelab/gtd/workflow-kit` | Policy-free building blocks to compose a workflow from: health checks, quality laps, scenario freezing, review tails. See [Workflow kit](https://github.com/pmelab/gtd/blob/main/docs/workflow-kit.md). |
619
+ | `@pmelab/gtd/testkit` | Runs any workflow against the real engine in an in-memory repository, under any test runner, plus a flow-context harness for rendering prompts. See [Testing a workflow](https://github.com/pmelab/gtd/blob/main/docs/testing.md). |
620
+ | `@pmelab/gtd/testkit/gherkin` | The testkit as quickpickle steps for vitest. Needs `vitest` and `quickpickle`, optional peer dependencies. |
621
621
 
622
622
  Types come from the TypeScript sources the package ships. Configs never need an
623
623
  install for any of these but the testkit's: gtd resolves a `gtd.config.ts`'s
@@ -642,7 +642,8 @@ flow has ended, an unpin beat removing it — the commit that closes the process
642
642
  Both are ordinary `script` beats a driver runs unattended; see
643
643
  [Pin and unpin beats](https://github.com/pmelab/gtd/blob/main/docs/driver.md#pin-and-unpin-beats).
644
644
  Histories with pin commits are history format 2 or later, which an older gtd
645
- refuses.
645
+ refuses. The beats rest at `gtd:pin` and `gtd:unpin`; a workflow step may not
646
+ take a name starting with `gtd:`.
646
647
 
647
648
  While a process is pinned, the `gtd` on your `PATH` hands every command over to
648
649
  the pin, so editing the workflow or upgrading gtd mid-process changes nothing;
@@ -654,8 +655,8 @@ and run `gtd repin`: its script commits a fresh pin from the live engine and
654
655
  config, and the process continues on it. Where the edited workflow no longer
655
656
  replays the process, the repin continues from the divergence: the commits it
656
657
  cannot replay stay as files, and the process rests at the step the edited
657
- workflow reached (history format 3). A pin in a format your gtd does not know is
658
- refused: upgrade gtd. See
658
+ workflow reached (history format 3 or later). A pin in a format your gtd does
659
+ not know is refused: upgrade gtd. See
659
660
  [Pinned processes](https://github.com/pmelab/gtd/blob/main/docs/cli.md#pinned-processes).
660
661
 
661
662
  ## License
@@ -1,4 +1,4 @@
1
- import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  //#region src/flows/runtime.ts
3
3
  /**
4
4
  * Bump only when the installed `FlowContext` object changes shape or semantics;
@@ -1,4 +1,3 @@
1
- import "node:module";
2
1
  //#region \0rolldown/runtime.js
3
2
  var __defProp = Object.defineProperty;
4
3
  var __exportAll = (all, no_symbols) => {
@@ -1,4 +1,4 @@
1
- import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  //#region src/steering/SteeringFormat.ts
3
3
  /** `file:line:col: message` — the shape editors and grep already jump on. Stored 0-based, printed 1-based. A finding with a `line` but no `range`, or none at all, degrades to the shorter forms. */
4
4
  const formatFinding = (file, finding) => {
@@ -16600,6 +16600,426 @@ const freeFormFormat = {
16600
16600
  clearTicks: (content) => content
16601
16601
  };
16602
16602
  //#endregion
16603
+ //#region src/steering/Declared.ts
16604
+ const FENCE_RE = /^( *)(`{3,}|~{3,})/;
16605
+ const HEADING_RE = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/;
16606
+ const ITEM_RE = /^( *)([-*+]|\d{1,9}[.)])(?:[ \t]+(.*))?$/;
16607
+ const CHECKBOX_RE = /^\[([ xX])\](?:[ \t]+|$)/;
16608
+ /** Marks fenced lines; an unclosed fence is a problem rather than swallowing the rest of the document. */
16609
+ const walk = (content) => {
16610
+ const raw = content.split(/\r?\n/);
16611
+ const lines = [];
16612
+ const problems = [];
16613
+ let open;
16614
+ for (const [index, text] of raw.entries()) {
16615
+ const fence = FENCE_RE.exec(text);
16616
+ if (open === void 0) {
16617
+ if (fence !== null) {
16618
+ open = {
16619
+ char: fence[2][0],
16620
+ length: fence[2].length,
16621
+ line: index,
16622
+ indent: fence[1].length
16623
+ };
16624
+ lines.push({
16625
+ index,
16626
+ text,
16627
+ fenced: true,
16628
+ fenceIndent: open.indent,
16629
+ opens: true
16630
+ });
16631
+ } else lines.push({
16632
+ index,
16633
+ text,
16634
+ fenced: false
16635
+ });
16636
+ continue;
16637
+ }
16638
+ lines.push({
16639
+ index,
16640
+ text,
16641
+ fenced: true,
16642
+ fenceIndent: open.indent
16643
+ });
16644
+ if (fence !== null && fence[2][0] === open.char && fence[2].length >= open.length && text.slice(fence[0].length).trim() === "") open = void 0;
16645
+ }
16646
+ if (open !== void 0) problems.push({
16647
+ line: open.line,
16648
+ message: "code fence is never closed"
16649
+ });
16650
+ return {
16651
+ lines,
16652
+ problems
16653
+ };
16654
+ };
16655
+ const headingOf = (line) => {
16656
+ if (line.fenced) return void 0;
16657
+ const match = HEADING_RE.exec(line.text);
16658
+ return match === null ? void 0 : {
16659
+ line: line.index,
16660
+ level: match[1].length,
16661
+ title: (match[2] ?? "").trim()
16662
+ };
16663
+ };
16664
+ const indentOf = (text) => /^ */.exec(text)[0].length;
16665
+ const markerKind = (marker) => marker === "-" || marker === "*" || marker === "+" ? marker : "1.";
16666
+ const markerList = (markers) => markers.map((m) => `\`${m}\``).join(" or ");
16667
+ const titleMatches = (spec, title) => typeof spec.title === "string" ? spec.title.trim().toLowerCase() === title.toLowerCase() : spec.title.test(title);
16668
+ const titleShown = (spec) => typeof spec.title === "string" ? spec.title : spec.example;
16669
+ /** The lines the list lives on, with the section-heading problems. */
16670
+ const regionOf = (spec, lines) => {
16671
+ const section = spec.section;
16672
+ if (section === void 0) return {
16673
+ region: {
16674
+ start: 0,
16675
+ end: lines.length
16676
+ },
16677
+ problems: []
16678
+ };
16679
+ const problems = [];
16680
+ const headings = lines.flatMap((line) => headingOf(line) ?? []);
16681
+ const matching = headings.filter((h) => titleMatches(section, h.title));
16682
+ const wanted = "#".repeat(section.level);
16683
+ for (const h of matching.filter((h) => h.level !== section.level)) problems.push({
16684
+ line: h.line,
16685
+ message: `"${h.title}" is a level-${h.level} heading — write it as \`${wanted} ${h.title}\``
16686
+ });
16687
+ const atLevel = matching.filter((h) => h.level === section.level);
16688
+ for (const h of atLevel.slice(1)) problems.push({
16689
+ line: h.line,
16690
+ message: `"${h.title}" repeats the section on line ${atLevel[0].line + 1} — keep one`
16691
+ });
16692
+ const first = atLevel[0];
16693
+ if (first === void 0) {
16694
+ if (section.required !== false && matching.length === 0) problems.push({ message: `missing the \`${wanted} ${titleShown(section)}\` section` });
16695
+ return { problems };
16696
+ }
16697
+ const next = headings.find((h) => h.line > first.line && h.level <= section.level);
16698
+ return {
16699
+ region: {
16700
+ start: first.line + 1,
16701
+ end: next?.line ?? lines.length
16702
+ },
16703
+ problems
16704
+ };
16705
+ };
16706
+ /** One pass over the region: items, their continuations, and every lint the format implies. */
16707
+ const readItems = (spec, lines, region) => {
16708
+ const markers = spec.markers ?? ["-"];
16709
+ const wrap = spec.wrap ?? "join";
16710
+ const problems = [];
16711
+ const drafts = [];
16712
+ let firstKind;
16713
+ let current;
16714
+ let blank = false;
16715
+ let prose = [];
16716
+ const continueWith = (line, text) => {
16717
+ if (wrap === "forbid") {
16718
+ problems.push({
16719
+ line: line.index,
16720
+ message: "one line per item — this line continues the one above"
16721
+ });
16722
+ return;
16723
+ }
16724
+ current.parts.push(text.trim());
16725
+ };
16726
+ for (const line of lines.slice(region.start, region.end)) {
16727
+ const text = line.text;
16728
+ if (line.fenced) {
16729
+ if (current !== void 0 && (line.fenceIndent ?? 0) >= 2) {
16730
+ blank = false;
16731
+ continue;
16732
+ }
16733
+ if (line.opens === true && current !== void 0) {
16734
+ prose.push(line.index);
16735
+ current = void 0;
16736
+ } else if (line.opens !== true && ITEM_RE.test(text) && indentOf(text) < 2) problems.push({
16737
+ line: line.index,
16738
+ message: "a list item inside a code fence is not read as an item"
16739
+ });
16740
+ blank = false;
16741
+ continue;
16742
+ }
16743
+ if (text.trim() === "") {
16744
+ blank = true;
16745
+ continue;
16746
+ }
16747
+ const item = ITEM_RE.exec(text);
16748
+ const indent = indentOf(text);
16749
+ if (item !== null && indent < 2) {
16750
+ for (const p of prose) problems.push({
16751
+ line: p,
16752
+ message: "text between items — make it part of an item, or move it out of the list"
16753
+ });
16754
+ prose = [];
16755
+ const kind = markerKind(item[2]);
16756
+ if (!markers.includes(kind)) problems.push({
16757
+ line: line.index,
16758
+ message: `\`${item[2]}\` is not an allowed marker — use ${markerList(markers)}`
16759
+ });
16760
+ else if (firstKind !== void 0 && kind !== firstKind) problems.push({
16761
+ line: line.index,
16762
+ message: `\`${item[2]}\` mixes markers in one list — the list started with \`${firstKind}\``
16763
+ });
16764
+ firstKind ??= kind;
16765
+ current = {
16766
+ line: line.index,
16767
+ parts: [(item[3] ?? "").trim()]
16768
+ };
16769
+ drafts.push(current);
16770
+ blank = false;
16771
+ continue;
16772
+ }
16773
+ if (current === void 0) {
16774
+ blank = false;
16775
+ continue;
16776
+ }
16777
+ if (item !== null) problems.push({
16778
+ line: line.index,
16779
+ message: "nested list — this format is flat, one level of items"
16780
+ });
16781
+ else if (indent >= 2) continueWith(line, text);
16782
+ else if (!blank) {
16783
+ problems.push({
16784
+ line: line.index,
16785
+ message: "continuation line is not indented — indent it under its item, or separate it with a blank line"
16786
+ });
16787
+ continueWith(line, text);
16788
+ } else {
16789
+ prose.push(line.index);
16790
+ current = void 0;
16791
+ }
16792
+ blank = false;
16793
+ }
16794
+ return {
16795
+ drafts,
16796
+ problems
16797
+ };
16798
+ };
16799
+ /** Strips a leading `[ ]`/`[x]`, reporting it where the format forbids or requires one. */
16800
+ const readCheckbox = (spec, draft, problems) => {
16801
+ const checkbox = spec.checkbox ?? "forbid";
16802
+ const text = draft.parts.join(" ").trim();
16803
+ const box = CHECKBOX_RE.exec(text);
16804
+ if (box === null) {
16805
+ if (checkbox === "require") problems.push({
16806
+ line: draft.line,
16807
+ message: "every item starts with a checkbox — write `- [ ] …`"
16808
+ });
16809
+ return checkbox === "forbid" ? { text } : {
16810
+ text,
16811
+ checked: false
16812
+ };
16813
+ }
16814
+ if (checkbox === "forbid") problems.push({
16815
+ line: draft.line,
16816
+ message: "no checkboxes in this list — drop the `[ ]`"
16817
+ });
16818
+ const rest = text.slice(box[0].length).trim();
16819
+ return checkbox === "forbid" ? { text: rest } : {
16820
+ text: rest,
16821
+ checked: box[1] !== " "
16822
+ };
16823
+ };
16824
+ const finishItem = (spec, draft, problems) => {
16825
+ const { text, ...checked } = readCheckbox(spec, draft, problems);
16826
+ const item = spec.item;
16827
+ if (text === "") {
16828
+ problems.push({
16829
+ line: draft.line,
16830
+ message: "empty item"
16831
+ });
16832
+ return;
16833
+ }
16834
+ if (item === void 0) return {
16835
+ line: draft.line,
16836
+ text,
16837
+ ...checked
16838
+ };
16839
+ const at = text.indexOf(item.separator);
16840
+ const id = at < 0 ? "" : text.slice(0, at).trim();
16841
+ if (at < 0 || !item.id.test(id)) {
16842
+ problems.push({
16843
+ line: draft.line,
16844
+ message: `item has no ID — start it \`${item.example}${item.separator} …\``
16845
+ });
16846
+ return;
16847
+ }
16848
+ const body = text.slice(at + item.separator.length).trim();
16849
+ if (body === "") problems.push({
16850
+ line: draft.line,
16851
+ message: `empty item — ${id} has no text`
16852
+ });
16853
+ return {
16854
+ line: draft.line,
16855
+ text: body,
16856
+ id,
16857
+ ...checked
16858
+ };
16859
+ };
16860
+ const parseList = (spec, content) => {
16861
+ const walked = walk(content);
16862
+ const { region, problems: sectionProblems } = regionOf(spec, walked.lines);
16863
+ const problems = [...walked.problems, ...sectionProblems];
16864
+ if (region === void 0) return {
16865
+ items: [],
16866
+ problems
16867
+ };
16868
+ const read = readItems(spec, walked.lines, region);
16869
+ problems.push(...read.problems);
16870
+ const items = read.drafts.flatMap((draft) => finishItem(spec, draft, problems) ?? []);
16871
+ if (spec.ids?.unique === true) {
16872
+ const seen = /* @__PURE__ */ new Map();
16873
+ for (const item of items) {
16874
+ if (item.id === void 0) continue;
16875
+ const first = seen.get(item.id);
16876
+ if (first === void 0) seen.set(item.id, item.line);
16877
+ else problems.push({
16878
+ line: item.line,
16879
+ message: `duplicate ID ${item.id} — first on line ${first + 1}`
16880
+ });
16881
+ }
16882
+ }
16883
+ problems.sort((a, b) => (a.line ?? -1) - (b.line ?? -1));
16884
+ return {
16885
+ items,
16886
+ problems
16887
+ };
16888
+ };
16889
+ const sectionSentence = (spec) => {
16890
+ if (spec.section === void 0) return "The whole document is the list.";
16891
+ return `Write the list under one \`${`${"#".repeat(spec.section.level)} ${titleShown(spec.section)}`}\` heading; it ends at the next heading of the same or a higher level.`;
16892
+ };
16893
+ const CHECKBOX_SENTENCE = {
16894
+ forbid: "Do not use checkboxes.",
16895
+ strip: "",
16896
+ require: "Start every item with a checkbox: `[ ]`, or `[x]` when done."
16897
+ };
16898
+ const idSentence = (spec) => {
16899
+ if (spec.item === void 0) return "";
16900
+ const unique = spec.ids?.unique === true ? ", unique within the list" : "";
16901
+ return `Start every item with its ID (like \`${spec.item.example}\`${unique}), then \`${spec.item.separator}\`, then the text.`;
16902
+ };
16903
+ const listInstructions = (spec) => [
16904
+ sectionSentence(spec),
16905
+ `Write each item as a ${markerList(spec.markers ?? ["-"])} list item, all with the same marker.`,
16906
+ CHECKBOX_SENTENCE[spec.checkbox ?? "forbid"],
16907
+ idSentence(spec),
16908
+ (spec.wrap ?? "join") === "join" ? "Continue a long item on lines indented two spaces under it." : "Keep every item on one line.",
16909
+ "Keep the list flat: no nested items, and no text between items. Items inside a code fence are not read."
16910
+ ].filter((sentence) => sentence !== "").join(" ");
16911
+ const listSample = (spec) => {
16912
+ const item = `${(spec.markers ?? ["-"])[0] === "1." ? "1." : (spec.markers ?? ["-"])[0]} ${spec.checkbox === "require" ? "[ ] " : ""}${spec.item !== void 0 ? `${spec.item.example}${spec.item.separator} ` : ""}Describe the item.\n`;
16913
+ return spec.section === void 0 ? item : `${"#".repeat(spec.section.level)} ${titleShown(spec.section)}\n\n${item}`;
16914
+ };
16915
+ const bodyOf = (lines, start, end) => lines.slice(start, end).map((l) => l.text).join("\n").trim();
16916
+ /** The problem with heading `i` as a section, if any; `null` when it is not a section heading at all. */
16917
+ const sectionHeadingProblem = (spec, headings, i, firstLine) => {
16918
+ const h = headings[i];
16919
+ if (h.level < spec.level && headings.slice(0, i).some((p) => p.level === spec.level)) return `"${h.title}" is a level-${h.level} heading — sections are \`${"#".repeat(spec.level)}\``;
16920
+ if (h.level !== spec.level) return null;
16921
+ if (h.title === "") return "section heading has no title";
16922
+ const first = firstLine.get(h.title.toLowerCase());
16923
+ return spec.title === "unique" && first !== void 0 ? `"${h.title}" repeats the section on line ${first + 1} — titles are unique` : void 0;
16924
+ };
16925
+ const parseSections = (spec, content) => {
16926
+ const walked = walk(content);
16927
+ const problems = [...walked.problems];
16928
+ const headings = walked.lines.flatMap((line) => headingOf(line) ?? []);
16929
+ const sections = [];
16930
+ const firstLine = /* @__PURE__ */ new Map();
16931
+ for (const [i, h] of headings.entries()) {
16932
+ const problem = sectionHeadingProblem(spec, headings, i, firstLine);
16933
+ if (problem === null) continue;
16934
+ if (problem !== void 0) problems.push({
16935
+ line: h.line,
16936
+ message: problem
16937
+ });
16938
+ if (h.level !== spec.level || h.title === "") continue;
16939
+ if (!firstLine.has(h.title.toLowerCase())) firstLine.set(h.title.toLowerCase(), h.line);
16940
+ const end = headings.slice(i + 1).find((n) => n.level <= spec.level)?.line ?? walked.lines.length;
16941
+ sections.push({
16942
+ line: h.line,
16943
+ title: h.title,
16944
+ body: bodyOf(walked.lines, h.line + 1, end)
16945
+ });
16946
+ }
16947
+ problems.sort((a, b) => (a.line ?? -1) - (b.line ?? -1));
16948
+ return {
16949
+ sections,
16950
+ problems
16951
+ };
16952
+ };
16953
+ const sectionInstructions = (spec) => {
16954
+ const hashes = "#".repeat(spec.level);
16955
+ return [
16956
+ `Write each section under a \`${hashes} Title\` heading.`,
16957
+ spec.title === "unique" ? "No two sections share a title." : "",
16958
+ spec.level > 1 ? `Use deeper headings inside a section only; no heading above \`${hashes}\` after the first section.` : "",
16959
+ "Close every code fence."
16960
+ ].filter((s) => s !== "").join(" ");
16961
+ };
16962
+ const NAME_RE = /^[a-z][a-z0-9-]*$/;
16963
+ const pattern = (re) => new RegExp(re.source, re.flags.replace(/[gy]/g, ""));
16964
+ const encode = (value) => value instanceof RegExp ? {
16965
+ source: value.source,
16966
+ flags: value.flags
16967
+ } : Array.isArray(value) ? value.map(encode) : typeof value === "object" && value !== null ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, encode(v)])) : value;
16968
+ const isPattern = (value) => typeof value === "object" && value !== null && typeof value.source === "string" && typeof value.flags === "string";
16969
+ const checkName = (name) => {
16970
+ if (typeof name !== "string" || !NAME_RE.test(name)) throw new Error(`format name must be lowercase letters, digits and "-" — got ${JSON.stringify(name)}`);
16971
+ return name;
16972
+ };
16973
+ const withThreads = (name, spec, kind, parse, instructions, sample) => ({
16974
+ ...freeFormFormat,
16975
+ openThreads: openThreadsOf,
16976
+ name,
16977
+ spec: JSON.stringify({
16978
+ kind,
16979
+ ...encode(spec)
16980
+ }),
16981
+ instructions,
16982
+ sample,
16983
+ parse,
16984
+ validate: (content) => [...parse(content).problems, ...threadFindings(content)]
16985
+ });
16986
+ /** Declares a list format: `parse`, `validate`, `instructions` and `sample` all derive from `spec`. Usable as a step's `mode`. */
16987
+ const listFormat = (spec) => {
16988
+ const name = checkName(spec.name);
16989
+ const normalized = {
16990
+ ...spec,
16991
+ ...spec.section !== void 0 && typeof spec.section.title !== "string" ? { section: {
16992
+ ...spec.section,
16993
+ title: pattern(spec.section.title)
16994
+ } } : {},
16995
+ ...spec.item !== void 0 ? { item: {
16996
+ ...spec.item,
16997
+ id: pattern(spec.item.id)
16998
+ } } : {}
16999
+ };
17000
+ if ((normalized.markers ?? ["-"]).length === 0) throw new Error(`format "${name}": markers is empty`);
17001
+ if (normalized.item !== void 0 && !normalized.item.id.test(normalized.item.example)) throw new Error(`format "${name}": item.example "${normalized.item.example}" does not match item.id`);
17002
+ const section = normalized.section;
17003
+ if (section !== void 0 && typeof section.title !== "string" && (section.example === void 0 || !section.title.test(section.example))) throw new Error(`format "${name}": a RegExp section.title needs a section.example it matches`);
17004
+ const format = withThreads(name, normalized, "list", (content) => parseList(normalized, content), listInstructions(normalized), listSample(normalized));
17005
+ if (format.parse(format.sample).problems.length > 0) throw new Error(`format "${name}": its own sample does not validate`);
17006
+ return format;
17007
+ };
17008
+ /** Declares a document of headed sections. Usable as a step's `mode`. */
17009
+ const sectionFormat = (spec) => {
17010
+ const name = checkName(spec.name);
17011
+ const hashes = "#".repeat(spec.level);
17012
+ return withThreads(name, spec, "section", (content) => parseSections(spec, content), sectionInstructions(spec), `${hashes} First section\n\nWhat this section says.\n`);
17013
+ };
17014
+ const decodePatterns = (value, key) => (key === "title" || key === "id") && isPattern(value) ? new RegExp(value.source, value.flags) : Array.isArray(value) ? value.map((v) => decodePatterns(v)) : typeof value === "object" && value !== null ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, decodePatterns(v, k)])) : value;
17015
+ /** The format a declared format's `spec` describes — what `gtd check --spec` validates with. Throws on a spec no declaration produces. */
17016
+ const formatFromSpec = (spec) => {
17017
+ const { kind, ...rest } = decodePatterns(JSON.parse(spec));
17018
+ if (kind === "list") return listFormat(rest);
17019
+ if (kind === "section") return sectionFormat(rest);
17020
+ throw new Error(`not a declared format spec: ${JSON.stringify(kind)}`);
17021
+ };
17022
+ //#endregion
16603
17023
  //#region src/steering/registry.ts
16604
17024
  /** The built-in registry: mode name → its format, in registry order. The single place the built-in formats are wired into names — everything else in this module dispatches over a resolved `SteeringFormat`, never a mode-name string. */
16605
17025
  const REGISTRY = /* @__PURE__ */ new Map([
@@ -16827,4 +17247,4 @@ const stripCodeThreads = (path, text) => {
16827
17247
  return lines.filter((_, i) => !drop.has(i)).join("\n");
16828
17248
  };
16829
17249
  //#endregion
16830
- export { parseThreads as A, FREE_TEXT_PLACEHOLDER as C, findingEntries as D, NOISE_HEADING as E, headingSectionBodies as M, formatFinding as N, openThreadFindings as O, unansweredNotes as S, FIX_BOX as T, reviewChunks as _, sections as a, reviewRisks as b, openThreads as c, freeFormFormat as d, applySteeringEdits as f, REVIEW_SECTIONS as g, ANSWER_WONT_FIX as h, sectionBodies as i, parseThreadsWithFindings as j, parseFootnotes as k, steeringFormatFor as l, ANSWER_LEAD as m, stripCodeThreads as n, threads as o, ANSWER_FIX as p, openQuestions as r, BUILT_IN_MODE_NAMES as s, parseCodeThreads as t, unansweredQuestions as u, reviewDecisions as v, isAnswered as w, reviewSections as x, reviewNotes as y };
17250
+ export { findingEntries as A, reviewRisks as C, isAnswered as D, FREE_TEXT_PLACEHOLDER as E, headingSectionBodies as F, formatFinding as I, parseFootnotes as M, parseThreads as N, FIX_BOX as O, parseThreadsWithFindings as P, reviewNotes as S, unansweredNotes as T, ANSWER_LEAD as _, sections as a, reviewChunks as b, openThreads as c, formatFromSpec as d, listFormat as f, ANSWER_FIX as g, applySteeringEdits as h, sectionBodies as i, openThreadFindings as j, NOISE_HEADING as k, steeringFormatFor as l, freeFormFormat as m, stripCodeThreads as n, threads as o, sectionFormat as p, openQuestions as r, BUILT_IN_MODE_NAMES as s, parseCodeThreads as t, unansweredQuestions as u, ANSWER_WONT_FIX as v, reviewSections as w, reviewDecisions as x, REVIEW_SECTIONS as y };