@pmelab/gtd 64.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +8 -8
- package/dist/chunks/{steering-DkA51qDa.mjs → steering-Cem5i6Xl.mjs} +421 -1
- package/dist/chunks/{testkit-zGjNeQNL.mjs → testkit-DfWZQCWQ.mjs} +63 -16
- package/dist/gtd.bundle.mjs +484 -18
- package/dist/gtd.core.mjs +117 -110
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/dist/workflow-kit.mjs +1 -1
- package/package.json +1 -1
- package/src/cli/Cli.ts +13 -1
- package/src/cli/Program.ts +23 -4
- package/src/edge/Edge.ts +23 -9
- package/src/emit/SteeringMode.ts +21 -1
- package/src/flows/index.ts +1 -0
- package/src/flows/runtime.test.ts +2 -1
- package/src/flows/runtime.ts +8 -2
- package/src/steering/Declared.test.ts +218 -0
- package/src/steering/Declared.ts +607 -0
- package/src/steering/FORMATS.md +3 -0
- package/src/steering/index.ts +12 -0
- package/src/steering/internal.ts +1 -0
- package/src/twins/EmittedScriptRecognizer.ts +5 -3
- package/src/twins/EmittedScripts.test.ts +19 -0
- package/src/workflow/SteeringFormats.ts +4 -0
- package/src/workflow/Workflow.ts +2 -0
- package/src/workflow/index.ts +5 -1
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 {
|
|
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 {
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
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(", ")}`
|