@pmelab/gtd 64.0.0 → 64.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtd",
3
- "version": "64.0.0",
3
+ "version": "64.1.1",
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
@@ -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 };
@@ -1,6 +1,6 @@
1
1
  import { C as moveScript, D as revertScript, E as restoreScript, H as installContext, K as markedOptions, O as showScript, S as checkScript, T as removeScript, Y as protocolMismatch, i as configure, k as writeScript$1, n as configProblems, o as isFactoryWorkflow, p as numeric, s as answered, t as flows_exports, x as applyScript } from "./flows-B7ZRCe-B.mjs";
2
2
  import { M as renderFormat, O as ACCESS_REFUSAL, a as renderScriptJson, d as renderLandPlain, f as DEMAND_BRIEFING, g as statusOf, h as renderBeatJson, i as judgeJson, k as PIN_PATH, l as noopText, m as beatDocument, n as resolveSession, o as scriptFields, p as renderBeatPlain, r as selectPath, s as landFields, t as globMatches, u as renderLandJson, v as beatKindOf, y as demandOf } from "./wire-CPNSm_9N.mjs";
3
- import { M as headingSectionBodies, N as formatFinding, a as sections, c as openThreads, i as sectionBodies, l as steeringFormatFor, o as threads, r as openQuestions, s as BUILT_IN_MODE_NAMES, t as parseCodeThreads, u as unansweredQuestions } from "./steering-DkA51qDa.mjs";
3
+ import { F as headingSectionBodies, I as formatFinding, a as sections, c as openThreads, d as formatFromSpec, i as sectionBodies, l as steeringFormatFor, o as threads, r as openQuestions, s as BUILT_IN_MODE_NAMES, t as parseCodeThreads, u as unansweredQuestions } from "./steering-Cem5i6Xl.mjs";
4
4
  import { createRequire, isBuiltin } from "node:module";
5
5
  import * as Crypto from "node:crypto";
6
6
  import { createHash } from "node:crypto";
@@ -24289,6 +24289,8 @@ const replay = async (raw) => {
24289
24289
  const seededValidateCommand = (mode) => `gtd check ${mode} "$GTD_FILE"`;
24290
24290
  /** True when `command` is exactly `mode`'s seeded template (compared literally, not rendered) — distinguishes gtd's own seeding from a user override. */
24291
24291
  const isSeededValidateCommand = (mode, command) => command === seededValidateCommand(mode);
24292
+ /** The `validate:` command for a declared format: its spec travels on the command line, so `gtd check` loads no workflow to find it. */
24293
+ const declaredValidateCommand = (mode, spec) => `gtd check ${mode} "$GTD_FILE" --spec '${spec.replace(/'/g, "'\\''")}'`;
24292
24294
  //#endregion
24293
24295
  //#region src/workflow/compile.ts
24294
24296
  /**
@@ -25723,8 +25725,27 @@ const capabilitiesFor = (mode, format, validate) => {
25723
25725
  * The `"unknown"` arm carries the whole refusal message, naming what IS known
25724
25726
  * when `def` is given — this is the one place that message is built, so it
25725
25727
  * can never drift from what actually resolved.
25728
+ *
25729
+ * `spec` is a declared format's: it alone decides, validating through
25730
+ * `gtd check --spec` with no `format:` half.
25726
25731
  */
25727
- const resolveMode = (def, state, mode) => {
25732
+ const resolveMode = (def, state, mode, spec) => {
25733
+ if (spec !== void 0) {
25734
+ const format = formatFromSpec(spec);
25735
+ return {
25736
+ kind: "resolved",
25737
+ mode,
25738
+ format,
25739
+ validate: {
25740
+ kind: "command",
25741
+ command: declaredValidateCommand(mode, spec)
25742
+ },
25743
+ capabilities: {
25744
+ format,
25745
+ liveValidate: format.validate
25746
+ }
25747
+ };
25748
+ }
25728
25749
  const declared = def?.modes?.[mode];
25729
25750
  const format = steeringFormatFor(mode);
25730
25751
  if (declared === void 0 && format === void 0) return {
@@ -26392,7 +26413,10 @@ const commonOf = (step) => {
26392
26413
  actor: step.actor,
26393
26414
  ...optional("label", options.label),
26394
26415
  ...optional("file", options.file),
26395
- ...optional("mode", options.mode)
26416
+ ...typeof options.mode === "object" ? {
26417
+ mode: options.mode.name,
26418
+ modeSpec: options.mode.spec
26419
+ } : optional("mode", options.mode)
26396
26420
  };
26397
26421
  };
26398
26422
  const promptDef = (common, request, skills, access) => ({
@@ -26468,6 +26492,13 @@ const hintsOf = (def) => ({
26468
26492
  ...optional("skills", def.skills),
26469
26493
  ...optional("access", def.access)
26470
26494
  });
26495
+ /** A step's mode is a name the workflow knows, or a declared format named apart from all of them. */
26496
+ const modeFaultOf = (def, stepName, stepDef) => {
26497
+ if (stepDef.mode === void 0) return void 0;
26498
+ const known = def.modes[stepDef.mode] !== void 0;
26499
+ if (stepDef.modeSpec !== void 0) return known ? `gtd: config: step "${stepName}": declared format "${stepDef.mode}" has the name of a mode this workflow already knows — rename the format` : void 0;
26500
+ return known ? void 0 : `gtd: config: step "${stepName}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`;
26501
+ };
26471
26502
  const normalizeStatus = (raw) => raw === "A" ? "A" : raw === "D" ? "D" : "M";
26472
26503
  /** Per-model token totals, highest-cost first (ties broken by model name). */
26473
26504
  const costByModel = (entries) => {
@@ -26559,7 +26590,8 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
26559
26590
  const { reached, step } = resting;
26560
26591
  const { stepDef, state, memory } = restFacts(pin, step, reached.trace, run);
26561
26592
  yield* (yield* Narrator).narrate(`rest resolved: ${state} (awaits ${stepDef.actor})`);
26562
- if (stepDef.mode !== void 0 && def.modes[stepDef.mode] === void 0) return yield* fail$4(/* @__PURE__ */ new Error(`gtd: config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`));
26593
+ const modeFault = modeFaultOf(def, step.name, stepDef);
26594
+ if (modeFault !== void 0) return yield* fail$4(new Error(modeFault));
26563
26595
  const changes = ref === void 0 ? (yield* pendingChanges(git, stepDef.approve === true)).map((e) => ({
26564
26596
  status: normalizeStatus(e.status),
26565
26597
  path: e.path
@@ -28168,8 +28200,8 @@ const runRepinCommand = (json, out) => gen(function* () {
28168
28200
  * `gtd check <mode>` invocation instead, so there's always something concrete
28169
28201
  * to name.
28170
28202
  */
28171
- const selfValidateCommand = (def, state, mode, file) => {
28172
- const resolved = resolveMode(def, state, mode);
28203
+ const selfValidateCommand = (def, state, mode, file, spec) => {
28204
+ const resolved = resolveMode(def, state, mode, spec);
28173
28205
  const validate = resolved.kind === "resolved" ? resolved.validate : void 0;
28174
28206
  return commandForFile(validate?.kind === "command" ? validate.command : seededValidateCommand(mode), file);
28175
28207
  };
@@ -28187,7 +28219,7 @@ const resolveValidateScript = (rest) => gen(function* () {
28187
28219
  const file = rest.hints.file;
28188
28220
  const mode = rest.stepDef.mode;
28189
28221
  if (file === void 0 || mode === void 0) return void 0;
28190
- const resolved = resolveMode(rest.def, rest.state, mode);
28222
+ const resolved = resolveMode(rest.def, rest.state, mode, rest.stepDef.modeSpec);
28191
28223
  if (resolved.kind === "unknown") return yield* fail$4(new Error(resolved.message));
28192
28224
  const { script } = yield* validateScriptFor(resolved, file);
28193
28225
  return {
@@ -28218,7 +28250,7 @@ const runNextCommand = (json, out) => gen(function* () {
28218
28250
  if (json.kind === "document") out.write(renderBeatJson(fields));
28219
28251
  else if (json.kind === "select") yield* writeSelection(out, fields, json.path);
28220
28252
  else {
28221
- const validateCommand = emitsValidatablePrompt(rendered) ? selfValidateCommand(rest.def, rest.state, rendered.mode, rendered.file) : void 0;
28253
+ const validateCommand = emitsValidatablePrompt(rendered) ? selfValidateCommand(rest.def, rest.state, rendered.mode, rendered.file, rest.stepDef.modeSpec) : void 0;
28222
28254
  if (DEMAND_BRIEFING[fields.kind].suppressHeader) {
28223
28255
  const shown = renderBeatPlain({
28224
28256
  ...fields,
@@ -28245,8 +28277,8 @@ const runValidateCommand = (json, out) => gen(function* () {
28245
28277
  * Findings ride the failing Effect's message, so the shared refusal path
28246
28278
  * reports them on stderr.
28247
28279
  */
28248
- const runCheckCommand = (mode, file, openQuestions, openThreadsFlag) => gen(function* () {
28249
- const format = steeringFormatFor(mode);
28280
+ const runCheckCommand = (mode, file, openQuestions, openThreadsFlag, spec) => gen(function* () {
28281
+ const format = spec !== void 0 ? yield* declaredFormat(spec) : steeringFormatFor(mode);
28250
28282
  if (format === void 0) return yield* fail$4(new GtdUsageError(`gtd check: unknown mode "${mode}" — known modes: ${BUILT_IN_MODE_NAMES.join(", ")}`));
28251
28283
  if (openThreadsFlag) {
28252
28284
  if (openQuestions) return yield* fail$4(/* @__PURE__ */ new Error("gtd check: --open-threads and --open-questions are mutually exclusive"));
@@ -28265,6 +28297,10 @@ const runCheckCommand = (mode, file, openQuestions, openThreadsFlag) => gen(func
28265
28297
  if (errors.length === 0) return;
28266
28298
  return yield* fail$4(/* @__PURE__ */ new Error(`gtd check: ${file} is not valid under mode "${mode}" (${errors.length} finding(s)):\n` + errors.join("\n")));
28267
28299
  });
28300
+ const declaredFormat = (spec) => try_({
28301
+ try: () => formatFromSpec(spec),
28302
+ catch: (error) => new GtdUsageError(`gtd check: --spec is not a declared format: ${error.message}`)
28303
+ });
28268
28304
  const existingFile = (file) => flatMap(Workspace, (workspace) => {
28269
28305
  const content = workspace.atPath(file);
28270
28306
  return content === void 0 ? fail$4(/* @__PURE__ */ new Error(`gtd check: no such file: ${file}`)) : succeed$1(content);
@@ -28434,7 +28470,7 @@ const dispatchVoidCommand = (command, json, out) => {
28434
28470
  case "validate": return runValidateCommand(json, out);
28435
28471
  case "check":
28436
28472
  if (command.mode === void 0 || command.file === void 0) return runCodeThreadsCheckCommand();
28437
- return runCheckCommand(command.mode, command.file, command.openQuestions ?? false, command.openThreads ?? false);
28473
+ return runCheckCommand(command.mode, command.file, command.openQuestions ?? false, command.openThreads ?? false, command.spec);
28438
28474
  case "uncheck": return runUncheckCommand(command.file);
28439
28475
  case "guide": return runGuideCommand(out);
28440
28476
  case "bundle": return runBundleCommand(out);
@@ -28729,6 +28765,16 @@ const FLAGS = [
28729
28765
  valueHint: "",
28730
28766
  help: "Instead of <mode>'s findings, list the open threads in <file>; exits 1 when any are open. Alone, lists the open code-comment threads in the files the current process changed"
28731
28767
  },
28768
+ {
28769
+ name: "--spec",
28770
+ arity: 1,
28771
+ repeatable: false,
28772
+ scope: (kind) => kind === "check",
28773
+ decode: ([raw]) => succeed$5(raw ?? ""),
28774
+ scopeError: "gtd: --spec is only valid for `gtd check`",
28775
+ valueHint: "<json>",
28776
+ help: "Check against the declared format this spec describes, named <mode>, instead of a built-in one; validation scripts pass it for a step whose mode is a declared format"
28777
+ },
28732
28778
  {
28733
28779
  name: "--verbose",
28734
28780
  arity: 0,
@@ -28974,7 +29020,7 @@ The fixed answerer reads --answers and leaves out the questions it does not cove
28974
29020
  arity: { names: ["mode", "file"] },
28975
29021
  group: "Driver plumbing",
28976
29022
  summary: "Check a steering file's format",
28977
- details: `Run the steering format <mode> (qa, review or findings) over <file> and print each finding on a line of its own; exits 1 when there are any, or when <file> does not exist. An unknown <mode> is a usage error (exit 2). Reads no workflow and runs from any directory — emitted validation scripts call it.`
29023
+ details: `Run the steering format <mode> (qa, review or findings, or the declared format --spec describes) over <file> and print each finding on a line of its own; exits 1 when there are any, or when <file> does not exist. An unknown <mode> is a usage error (exit 2). Reads no workflow and runs from any directory — emitted validation scripts call it.`
28978
29024
  },
28979
29025
  {
28980
29026
  token: "uncheck",
@@ -29348,7 +29394,8 @@ const parseArgv = (argv) => {
29348
29394
  ...restPositionals[0] !== void 0 ? { mode: restPositionals[0] } : {},
29349
29395
  ...restPositionals[1] !== void 0 ? { file: restPositionals[1] } : {},
29350
29396
  ...bag["--open-questions"] !== void 0 ? { openQuestions: bag["--open-questions"] } : {},
29351
- ...bag["--open-threads"] !== void 0 ? { openThreads: bag["--open-threads"] } : {}
29397
+ ...bag["--open-threads"] !== void 0 ? { openThreads: bag["--open-threads"] } : {},
29398
+ ...bag["--spec"] !== void 0 ? { spec: bag["--spec"] } : {}
29352
29399
  },
29353
29400
  json,
29354
29401
  verbose
@@ -30971,13 +31018,13 @@ const recognizeModeContradictionCheck = (repo, commands, block) => {
30971
31018
  return parsed === void 0 ? void 0 : simulateModeContradictionCheck(repo, commands, parsed);
30972
31019
  };
30973
31020
  const GTD_CHECK_RE = /^gtd check (\S+) (.+)$/;
30974
- /** `gtd check <mode> <file>` — a non-empty findings array fails the script, mirroring a real invocation's non-zero exit under `set -e`. */
31021
+ /** `gtd check <mode> <file> [--spec <json>]` — a non-empty findings array fails the script, mirroring a real invocation's non-zero exit under `set -e`. */
30975
31022
  const recognizeGtdCheck = (repo, block) => {
30976
31023
  const match = GTD_CHECK_RE.exec(block);
30977
31024
  if (!match) return void 0;
30978
31025
  const mode = match[1];
30979
- const [file] = extractQuotedTokens(match[2]);
30980
- const format = steeringFormatFor(mode);
31026
+ const [file, spec] = extractQuotedTokens(match[2]);
31027
+ const format = / --spec '/.test(match[2]) && spec !== void 0 ? formatFromSpec(spec) : steeringFormatFor(mode);
30981
31028
  if (format === void 0 || file === void 0) return {
30982
31029
  kind: "failed",
30983
31030
  error: `gtd check: unknown mode "${mode}" — known modes: ${BUILT_IN_MODE_NAMES.join(", ")}`