@pmelab/gtd 10.3.0 → 10.5.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.
@@ -190484,16 +190484,336 @@ const flattenMachines = (raw, errors) => {
190484
190484
  scopes
190485
190485
  };
190486
190486
  };
190487
- /** `QA_FORMAT`'s canonical sample: one open question with two options plus the unfilled free-text slot. Not authored to survive any particular formatter. */
190487
+ //#endregion
190488
+ //#region src/Footnotes.ts
190489
+ /** The seeded placeholder a hand-authored definition starts as — still present means the human never filled it in. `footnoteAdditionEdits` (below) seeds new definitions with this exact text, so `computeFindings`'s placeholder check fires until a human replaces it. */
190490
+ const PLACEHOLDER_BODY = "your comment";
190491
+ /** A definition's start line: `[^name]:` at COLUMN 0, nothing before it. */
190492
+ const DEFINITION_START_RE = /^\[\^([^\s\]]+)\]:\s?(.*)$/;
190493
+ /** A marker anywhere on a line: `[^name]`, name has no whitespace and no `]`. */
190494
+ const MARKER_RE = /\[\^([^\s\]]+)\]/g;
190495
+ /** A fenced-code-block delimiter line (any indent, ``` or more backticks). */
190496
+ const FENCE_RE = /^\s*```/;
190497
+ /** Blanks out inline-code spans (\`...\`) while preserving line length, so marker column offsets stay accurate. */
190498
+ const maskInlineCode = (line) => line.replace(/`[^`]*`/g, (m) => " ".repeat(m.length));
190499
+ /** A non-blank line that starts with leading whitespace — the shape of a definition's continuation line. */
190500
+ const isContinuationLine = (line) => line.trim().length > 0 && /^\s+\S/.test(line);
190501
+ /**
190502
+ * For each line in `lines`, whether it's inside (or is the delimiter of) a
190503
+ * fenced code block — content that "scanning skips code" excludes from both
190504
+ * marker and definition recognition. The one shared source of truth for
190505
+ * fence state: `parseFootnotes` and `isFootnoteDefinitionLine` both consult
190506
+ * this instead of tracking fences independently, so the two can never
190507
+ * disagree about what a definition is.
190508
+ */
190509
+ const computeFenceSkip = (lines) => {
190510
+ const skip = [];
190511
+ let inFence = false;
190512
+ for (const line of lines) if (FENCE_RE.test(line)) {
190513
+ skip.push(true);
190514
+ inFence = !inFence;
190515
+ } else skip.push(inFence);
190516
+ return skip;
190517
+ };
190518
+ /**
190519
+ * True when `lines[index]` is part of a footnote definition — either the
190520
+ * `[^name]:` start line itself, or one of its indented continuation lines —
190521
+ * and is not itself fenced-code content. Walks backward through the
190522
+ * contiguous run of indented non-blank lines above `index` to find the
190523
+ * start; a blank line, an unindented line, fenced-code content, or the top
190524
+ * of `lines` ends the search with `false`. Exported so `OpenQuestions.ts`
190525
+ * and `ReviewDoc.ts` can break their own item/pointer spans on a definition
190526
+ * without re-parsing the whole document.
190527
+ */
190528
+ const isFootnoteDefinitionLine = (lines, index) => {
190529
+ const line = lines[index];
190530
+ if (line === void 0) return false;
190531
+ const fenceSkip = computeFenceSkip(lines);
190532
+ if (fenceSkip[index]) return false;
190533
+ if (DEFINITION_START_RE.test(line)) return true;
190534
+ if (!isContinuationLine(line)) return false;
190535
+ let i = index - 1;
190536
+ while (i >= 0) {
190537
+ if (fenceSkip[i]) return false;
190538
+ const prev = lines[i];
190539
+ if (DEFINITION_START_RE.test(prev)) return true;
190540
+ if (!isContinuationLine(prev)) return false;
190541
+ i -= 1;
190542
+ }
190543
+ return false;
190544
+ };
190545
+ /** Strips every `[^name]` marker out of `text` — applied to every extracted text field so a marker never leaks into an option's or note's text. */
190546
+ const stripFootnoteMarkers = (text) => text.replace(MARKER_RE, "");
190547
+ const computeFindings = (markers, definitions) => {
190548
+ const findings = [];
190549
+ const definedNames = new Set(definitions.map((d) => d.name));
190550
+ const referencedNames = new Set(markers.map((m) => m.name));
190551
+ const seenDefinitionNames = /* @__PURE__ */ new Set();
190552
+ for (const marker of markers) if (!definedNames.has(marker.name)) findings.push({
190553
+ message: `Footnote marker "[^${marker.name}]" has no matching definition`,
190554
+ line: marker.line
190555
+ });
190556
+ for (const def of definitions) {
190557
+ if (seenDefinitionNames.has(def.name)) findings.push({
190558
+ message: `Duplicate footnote definition "[^${def.name}]"`,
190559
+ line: def.line
190560
+ });
190561
+ else seenDefinitionNames.add(def.name);
190562
+ if (!referencedNames.has(def.name)) findings.push({
190563
+ message: `Footnote definition "[^${def.name}]" has no marker referencing it`,
190564
+ line: def.line
190565
+ });
190566
+ if (def.body.trim().toLowerCase() === PLACEHOLDER_BODY) findings.push({
190567
+ message: `Footnote definition "[^${def.name}]" still has its seeded placeholder body`,
190568
+ line: def.line
190569
+ });
190570
+ }
190571
+ return findings.sort((a, b) => (a.line ?? 0) - (b.line ?? 0));
190572
+ };
190573
+ /** Parses the `[^name]:` definition starting at `lines[index]`, plus its indented continuation run. Returns the parsed definition and the index of the first line past it. */
190574
+ const parseDefinitionAt = (lines, index, defMatch) => {
190575
+ const name = defMatch[1];
190576
+ const sameLineBody = (defMatch[2] ?? "").trim();
190577
+ const bodyParts = sameLineBody.length > 0 ? [sameLineBody] : [];
190578
+ let endLine = index;
190579
+ let j = index + 1;
190580
+ while (j < lines.length && isContinuationLine(lines[j])) {
190581
+ bodyParts.push(lines[j].trim());
190582
+ endLine = j;
190583
+ j += 1;
190584
+ }
190585
+ return {
190586
+ definition: {
190587
+ name,
190588
+ line: index,
190589
+ endLine,
190590
+ body: bodyParts.join(" ")
190591
+ },
190592
+ nextIndex: j
190593
+ };
190594
+ };
190595
+ /** Every marker on `line` (after masking inline-code spans), anchored at `lineIndex`. */
190596
+ const scanMarkers = (line, lineIndex) => {
190597
+ const masked = maskInlineCode(line);
190598
+ const re = new RegExp(MARKER_RE);
190599
+ const found = [];
190600
+ let match;
190601
+ while ((match = re.exec(masked)) !== null) found.push({
190602
+ name: match[1],
190603
+ line: lineIndex,
190604
+ character: match.index
190605
+ });
190606
+ return found;
190607
+ };
190608
+ /**
190609
+ * Parses every footnote marker and definition out of `content`. Total and
190610
+ * side-effect-free: always returns a result, never throws. Scanning skips
190611
+ * fenced code blocks entirely and blanks inline-code spans before looking for
190612
+ * markers, so `[^x]` inside either never counts. A definition's own body
190613
+ * (same-line plus indented continuation lines) is excluded from marker
190614
+ * scanning — a `[^y]` written there is ordinary text.
190615
+ */
190616
+ const parseFootnotes = (content) => {
190617
+ const lines = content.split(/\r?\n/);
190618
+ const fenceSkip = computeFenceSkip(lines);
190619
+ const markers = [];
190620
+ const definitions = [];
190621
+ let i = 0;
190622
+ while (i < lines.length) {
190623
+ const line = lines[i];
190624
+ if (fenceSkip[i]) {
190625
+ i += 1;
190626
+ continue;
190627
+ }
190628
+ const defMatch = DEFINITION_START_RE.exec(line);
190629
+ if (defMatch) {
190630
+ const { definition, nextIndex } = parseDefinitionAt(lines, i, defMatch);
190631
+ definitions.push(definition);
190632
+ i = nextIndex;
190633
+ continue;
190634
+ }
190635
+ markers.push(...scanMarkers(line, i));
190636
+ i += 1;
190637
+ }
190638
+ return {
190639
+ markers,
190640
+ definitions,
190641
+ findings: computeFindings(markers, definitions)
190642
+ };
190643
+ };
190644
+ /** The first integer unused by any `fnN` marker or definition already in the document — deterministic (no clock, no randomness), so "add a footnote" is testable and idempotent under re-run: applying it twice yields `fn1` then `fn2`, never a collision. */
190645
+ const nextFootnoteName = (content) => {
190646
+ const { markers, definitions } = parseFootnotes(content);
190647
+ const NAME_RE = /^fn(\d+)$/;
190648
+ const used = /* @__PURE__ */ new Set();
190649
+ for (const name of [...markers.map((m) => m.name), ...definitions.map((d) => d.name)]) {
190650
+ const match = NAME_RE.exec(name);
190651
+ if (match) used.add(Number(match[1]));
190652
+ }
190653
+ let n = 1;
190654
+ while (used.has(n)) n += 1;
190655
+ return `fn${n}`;
190656
+ };
190657
+ /**
190658
+ * Where "add a footnote" plants the marker: scan right from `character` while
190659
+ * the character at that position is a word character, and stop there. One
190660
+ * rule, no branching — a cursor inside a word lands at the word's end, a
190661
+ * cursor already just past a word (or on whitespace/punctuation) doesn't
190662
+ * move at all, landing right at the cursor.
190663
+ */
190664
+ const footnoteMarkerColumn = (line, character) => {
190665
+ let i = character;
190666
+ while (i < line.length && /\w/.test(line[i])) i += 1;
190667
+ return i;
190668
+ };
190669
+ /** The first line index at or after `from` that is non-blank, or `lines.length` when none remain (EOF). */
190670
+ const firstNonBlankFrom = (lines, from) => {
190671
+ let i = from;
190672
+ while (i < lines.length && lines[i].trim().length === 0) i += 1;
190673
+ return i;
190674
+ };
190675
+ /**
190676
+ * The last non-blank line of the current prose block starting at `index` —
190677
+ * the "otherwise" placement rule for "add a footnote": the next blank line,
190678
+ * or end of file. Shared by both formats' fallback case (a hunk's own span
190679
+ * and a question's option-list span are format-specific and computed by the
190680
+ * caller instead).
190681
+ */
190682
+ const proseBlockEnd = (lines, index) => {
190683
+ let end = index;
190684
+ for (let i = index + 1; i < lines.length; i += 1) {
190685
+ if (lines[i].trim().length === 0) break;
190686
+ end = i;
190687
+ }
190688
+ return end;
190689
+ };
190690
+ /** The marker whose `[^name]` span (inclusive of both brackets) contains `position`, or `undefined`. Shared by `footnotePointerAt` and `isOnExistingFootnote` so the two can never disagree about what counts as "inside a marker". */
190691
+ const markerAt = (markers, position) => markers.find((m) => {
190692
+ if (m.line !== position.line) return false;
190693
+ const end = m.character + m.name.length + 3;
190694
+ return position.character >= m.character && position.character <= end;
190695
+ });
190696
+ /**
190697
+ * True when `position` sits inside an EXISTING marker's `[^name]` span, or on
190698
+ * an existing definition's own line (its `[^name]:` label line or an indented
190699
+ * continuation) — the guard "gtd: add a footnote" uses to refuse itself
190700
+ * rather than plant a new marker/definition into already-written footnote
190701
+ * syntax (which would corrupt it: a marker inserted inside another marker's
190702
+ * name, or a definition inserted between an existing definition's label and
190703
+ * its own name).
190704
+ */
190705
+ const isOnExistingFootnote = (content, position) => {
190706
+ const { markers } = parseFootnotes(content);
190707
+ if (markerAt(markers, position)) return true;
190708
+ return isFootnoteDefinitionLine(content.split(/\r?\n/), position.line);
190709
+ };
190710
+ /**
190711
+ * The two edits behind "gtd: add a footnote": a marker inserted at the
190712
+ * cursor (via `footnoteMarkerColumn`) and a definition seeded with
190713
+ * `PLACEHOLDER_BODY`, planted right after `blockEndLine` — the caller's own
190714
+ * notion of "the current block's last line" (a hunk's span in `ReviewDoc.ts`,
190715
+ * an option-list's span in `OpenQuestions.ts`, or `proseBlockEnd` otherwise).
190716
+ * The definition edit REPLACES any existing blank-line run between
190717
+ * `blockEndLine` and the next non-blank content (or EOF) with exactly one
190718
+ * blank line, the definition, and — unless at EOF — one more blank line, so
190719
+ * the result is deterministic regardless of the surrounding whitespace.
190720
+ *
190721
+ * The definition edit's range starts at `(blockEndLine + 1, 0)` — the line
190722
+ * AFTER `blockEndLine` — never at the end of `blockEndLine` itself, even when
190723
+ * that line doesn't literally exist yet (a document with no trailing
190724
+ * newline): a `line` past the document's last index is a legal LSP position,
190725
+ * clamped to end-of-file, per the protocol. The marker edit's own position is
190726
+ * always on or before `blockEndLine` (callers only ever pass a `blockEndLine`
190727
+ * at or after the cursor's line), so anchoring one line later guarantees the
190728
+ * two edits' ranges never touch: two edits that share a start position have
190729
+ * no defined application order (LSP forbids overlapping — including
190730
+ * coincident-zero-length — ranges in one action), and a naive apply corrupts
190731
+ * whichever text sits at the shared offset.
190732
+ */
190733
+ const footnoteAdditionEdits = (content, position, blockEndLine) => {
190734
+ const lines = content.split(/\r?\n/);
190735
+ const cursorLine = lines[position.line] ?? "";
190736
+ const markerColumn = footnoteMarkerColumn(cursorLine, position.character);
190737
+ const name = nextFootnoteName(content);
190738
+ const markerEdit = {
190739
+ range: {
190740
+ start: {
190741
+ line: position.line,
190742
+ character: markerColumn
190743
+ },
190744
+ end: {
190745
+ line: position.line,
190746
+ character: markerColumn
190747
+ }
190748
+ },
190749
+ newText: `[^${name}]`
190750
+ };
190751
+ const insertLine = blockEndLine + 1;
190752
+ const start = {
190753
+ line: insertLine,
190754
+ character: 0
190755
+ };
190756
+ const nextContentLine = firstNonBlankFrom(lines, insertLine);
190757
+ const atEof = nextContentLine >= lines.length;
190758
+ return [markerEdit, {
190759
+ range: {
190760
+ start,
190761
+ end: atEof ? start : {
190762
+ line: nextContentLine,
190763
+ character: 0
190764
+ }
190765
+ },
190766
+ newText: atEof ? `\n[^${name}]: ${PLACEHOLDER_BODY}\n` : `\n[^${name}]: ${PLACEHOLDER_BODY}\n\n`
190767
+ }];
190768
+ };
190769
+ /**
190770
+ * The footnote half of `pointerAt`, shared by `qa` (which serves footnote
190771
+ * jumps ONLY) and `review` (which tries this FIRST — footnotes are
190772
+ * column-scoped and the hunk jump is line-scoped, so a marker sitting in a
190773
+ * hunk's inline note would otherwise be shadowed by it). Returns `undefined`
190774
+ * when `position` isn't on a footnote at all (the caller should try its own
190775
+ * next resolver); returns `{ pointer: undefined }` for an orphan marker or
190776
+ * definition — resolved, but to nothing, so the caller must NOT fall through
190777
+ * to another resolver (an orphan marker inside a review hunk's note must not
190778
+ * jump to the hunk).
190779
+ */
190780
+ const footnotePointerAt = (content, position) => {
190781
+ const { markers, definitions } = parseFootnotes(content);
190782
+ const marker = markerAt(markers, position);
190783
+ if (marker) {
190784
+ const definition = definitions.find((d) => d.name === marker.name);
190785
+ return { pointer: definition ? { line: definition.line } : void 0 };
190786
+ }
190787
+ const lines = content.split(/\r?\n/);
190788
+ if (isFootnoteDefinitionLine(lines, position.line)) {
190789
+ const definition = definitions.find((d) => position.line >= d.line && position.line <= d.endLine);
190790
+ const firstMarker = definition ? markers.find((m) => m.name === definition.name) : void 0;
190791
+ return { pointer: firstMarker ? {
190792
+ line: firstMarker.line,
190793
+ character: firstMarker.character
190794
+ } : void 0 };
190795
+ }
190796
+ };
190797
+ /**
190798
+ * `QA_FORMAT`'s canonical sample: one open question with two options plus the
190799
+ * unfilled free-text slot, and one anchored footnote on an option with a body
190800
+ * over 80 characters — pinned already in oxfmt's own wrapped four-space form
190801
+ * (see `src/SteeringFormats.test.ts`'s formatter round-trip). Not authored to
190802
+ * survive any particular formatter.
190803
+ */
190488
190804
  const QA_SAMPLE = `Sample plan. Add a thing.
190489
190805
 
190490
190806
  ## Open Questions
190491
190807
 
190492
190808
  ### Which option?
190493
190809
 
190494
- - [ ] Option A
190810
+ - [ ] Option A[^fn1]
190495
190811
  - [ ] Option B
190496
190812
  - [ ] _your answer_
190813
+
190814
+ [^fn1]:
190815
+ This option keeps the current behavior exactly as it is today, which is the
190816
+ safer default for most reviewers.
190497
190817
  `;
190498
190818
  const OPEN_QUESTIONS_HEADING = "## Open Questions";
190499
190819
  const ANSWERED_QUESTIONS_HEADING = "## Answered Questions";
@@ -190521,7 +190841,7 @@ const splitQuestionBlocks = (lines, start) => {
190521
190841
  i += 1;
190522
190842
  continue;
190523
190843
  }
190524
- const question = heading.text;
190844
+ const question = stripFootnoteMarkers(heading.text);
190525
190845
  const headingLine = i;
190526
190846
  i += 1;
190527
190847
  const body = [];
@@ -190549,7 +190869,7 @@ const itemEndIndex = (body, index) => {
190549
190869
  let end = index;
190550
190870
  for (let i = index + 1; i < body.length; i += 1) {
190551
190871
  const line = body[i];
190552
- if (line.trim().length === 0 || CHECKBOX_RE.test(line)) break;
190872
+ if (line.trim().length === 0 || CHECKBOX_RE.test(line) || isFootnoteDefinitionLine(body, i)) break;
190553
190873
  end = i;
190554
190874
  }
190555
190875
  return end;
@@ -190566,7 +190886,7 @@ const parseOptions = (body, bodyStart) => {
190566
190886
  if (!match) return;
190567
190887
  raw.push({
190568
190888
  checked: match[1] !== " ",
190569
- text: match[2].trim(),
190889
+ text: stripFootnoteMarkers(match[2]).trim(),
190570
190890
  sourceLine: bodyStart + i,
190571
190891
  bodyIndex: i
190572
190892
  });
@@ -190598,7 +190918,8 @@ const isAnswered = (options) => {
190598
190918
  };
190599
190919
  const parseQuestionBlock = (block, status) => {
190600
190920
  if (block.question.length === 0) return { error: "An '### ' question heading under '## Open Questions' or '## Answered Questions' has no question text" };
190601
- const firstNonBlank = block.body.map((line) => line.trim()).find((line) => line.length > 0) ?? "";
190921
+ const firstNonBlankLine = block.body.find((line, i) => line.trim().length > 0 && !isFootnoteDefinitionLine(block.body, i));
190922
+ const firstNonBlank = firstNonBlankLine ? stripFootnoteMarkers(firstNonBlankLine.trim()) : "";
190602
190923
  const options = status === "open" ? parseOptions(block.body, block.headingLine + 1) : [];
190603
190924
  return {
190604
190925
  question: block.question,
@@ -190701,19 +191022,45 @@ const statusMarker = (question) => {
190701
191022
  if (question.status === "answered") return "[answered]";
190702
191023
  return question.answered ? "[answered]" : "[unanswered]";
190703
191024
  };
190704
- /** The outline tree for a `qa`-mode file's open/answered questions, each option a `leaf: true` child of its open question. */
191025
+ /** One footnote marker rendered as an outline leaf: `[^name] <body>`. */
191026
+ const footnoteLeaf$1 = (lines, definitionByName, marker) => ({
191027
+ name: `[^${marker.name}] ${definitionByName.get(marker.name) ?? ""}`.trim(),
191028
+ range: lineRange$1(lines, marker.line),
191029
+ selectionRange: lineRange$1(lines, marker.line),
191030
+ leaf: true
191031
+ });
191032
+ /**
191033
+ * The outline tree for a `qa`-mode file's open/answered questions, each
191034
+ * option a `leaf: true` child of its open question — unless it carries a
191035
+ * footnote of its own, in which case it's a container instead (`leaf` and
191036
+ * `children` are never both set; see `SteeringFormat.ts`'s `leaf` doc). A
191037
+ * footnote is itself always a `leaf: true` child of whichever node's span
191038
+ * contains its marker — an option when the marker sits inside that option's
191039
+ * span, otherwise the question itself.
191040
+ */
190705
191041
  const questionsOutline = (content) => {
190706
191042
  const { questions } = parseOpenQuestions(content);
191043
+ const { markers, definitions } = parseFootnotes(content);
191044
+ const definitionByName = new Map(definitions.map((d) => [d.name, d.body]));
190707
191045
  const lines = content.split(/\r?\n/);
190708
191046
  return questions.map((question, i) => {
190709
191047
  const start = question.headingLine;
190710
191048
  const end = Math.max(start, (questions[i + 1]?.headingLine ?? lines.length) - 1);
190711
- const children = question.options.map((option) => ({
190712
- name: `${option.checked ? "[x]" : "[ ]"} ${option.text || "your answer"}`,
190713
- range: spanRange$2(lines, option.sourceLine, option.endLine),
190714
- selectionRange: lineRange$1(lines, option.sourceLine),
190715
- leaf: true
190716
- }));
191049
+ const questionMarkers = markers.filter((m) => m.line >= start && m.line <= end);
191050
+ const assigned = /* @__PURE__ */ new Set();
191051
+ const optionChildren = question.options.map((option) => {
191052
+ const optionMarkers = questionMarkers.filter((m) => m.line >= option.sourceLine && m.line <= option.endLine);
191053
+ optionMarkers.forEach((m) => assigned.add(m));
191054
+ const footnotes = optionMarkers.map((m) => footnoteLeaf$1(lines, definitionByName, m));
191055
+ return {
191056
+ name: `${option.checked ? "[x]" : "[ ]"} ${option.text || "your answer"}`,
191057
+ range: spanRange$2(lines, option.sourceLine, option.endLine),
191058
+ selectionRange: lineRange$1(lines, option.sourceLine),
191059
+ ...footnotes.length > 0 ? { children: footnotes } : { leaf: true }
191060
+ };
191061
+ });
191062
+ const questionFootnotes = questionMarkers.filter((m) => !assigned.has(m)).map((m) => footnoteLeaf$1(lines, definitionByName, m));
191063
+ const children = [...optionChildren, ...questionFootnotes];
190717
191064
  return {
190718
191065
  name: `${statusMarker(question)} ${question.question}`,
190719
191066
  detail: question.text,
@@ -190743,15 +191090,42 @@ const optionAction = (content, question, option) => {
190743
191090
  };
190744
191091
  };
190745
191092
  /**
191093
+ * The block a footnote lands after when "add a footnote" fires with the
191094
+ * cursor at `cursorLine`: the whole contiguous option list's own span (its
191095
+ * first option's `sourceLine` through its last option's `endLine`, never
191096
+ * split between two items) when the cursor sits inside one, otherwise the
191097
+ * surrounding prose block (the next blank line or EOF) — covers question-body
191098
+ * prose ABOVE a list, a question with no options at all, and any cursor
191099
+ * position outside every question.
191100
+ */
191101
+ const footnoteBlockEnd$1 = (content, lines, cursorLine) => {
191102
+ const { questions } = parseOpenQuestions(content);
191103
+ for (const question of questions) {
191104
+ if (question.options.length === 0) continue;
191105
+ const first = question.options[0].sourceLine;
191106
+ const last = question.options[question.options.length - 1].endLine;
191107
+ if (cursorLine >= first && cursorLine <= last) return last;
191108
+ }
191109
+ return proseBlockEnd(lines, cursorLine);
191110
+ };
191111
+ /**
190746
191112
  * Actions for a `qa`-mode file: anywhere on an open question's option's list
190747
191113
  * item, "pick this option" (radio semantics) or "uncheck this option" when
190748
- * it's already chosen. No action off an option's span, or on an
190749
- * answered-section (prose) question.
191114
+ * it's already chosen; "add a footnote" everywhere EXCEPT inside an existing
191115
+ * marker's span or on an existing definition's own line — planting a new
191116
+ * marker/definition there would corrupt the footnote already written. No
191117
+ * pick/uncheck action off an option's span, or on an answered-section
191118
+ * (prose) question.
190750
191119
  */
190751
191120
  const questionActions = (content, range) => {
190752
191121
  const { questions } = parseOpenQuestions(content);
191122
+ const lines = content.split(/\r?\n/);
190753
191123
  const cursorLine = range.start.line;
190754
191124
  const actions = [];
191125
+ if (!isOnExistingFootnote(content, range.start)) actions.push({
191126
+ title: "gtd: add a footnote",
191127
+ edits: footnoteAdditionEdits(content, range.start, footnoteBlockEnd$1(content, lines, cursorLine))
191128
+ });
190755
191129
  for (const question of questions) {
190756
191130
  if (question.status !== "open") continue;
190757
191131
  const option = question.options.find((o) => cursorLine >= o.sourceLine && cursorLine <= o.endLine);
@@ -190761,33 +191135,64 @@ const questionActions = (content, range) => {
190761
191135
  }
190762
191136
  return actions;
190763
191137
  };
190764
- /** The `qa` steering format: gtd's own in-process open-questions checkbox format — validation, outline, and code actions, no `pointerAt` (an open question's options have nothing to jump to). Its one finding is always positionless — this format has no notion of a per-line problem. */
191138
+ /**
191139
+ * `qa`'s `pointerAt`: footnote jumps only (an open question's options have
191140
+ * nothing else to jump to) — marker → definition, definition → first marker,
191141
+ * both within the same document.
191142
+ */
191143
+ const questionsPointerAt = (content, position) => footnotePointerAt(content, position)?.pointer;
191144
+ /** The `qa` steering format: gtd's own in-process open-questions checkbox format — validation, outline, code actions, and a footnote-only `pointerAt`. Its structural findings are positionless, but a footnote finding carries the offending marker's or definition's `line`. */
190765
191145
  const QA_FORMAT = {
190766
191146
  sample: QA_SAMPLE,
190767
- validate: (content) => parseOpenQuestions(content).errors.map((message) => ({ message })),
191147
+ validate: (content) => [...parseOpenQuestions(content).errors.map((message) => ({ message })), ...parseFootnotes(content).findings],
190768
191148
  outline: questionsOutline,
190769
- actions: questionActions
191149
+ actions: questionActions,
191150
+ pointerAt: questionsPointerAt
190770
191151
  };
190771
191152
  //#endregion
190772
191153
  //#region src/ReviewDoc.ts
190773
191154
  /**
190774
191155
  * `REVIEW_FORMAT`'s canonical sample — a minimal, valid `review`-mode
190775
- * document: the header, the base comment, and one chunk with one file
190776
- * pointer. Deliberately not authored to survive any particular formatter.
191156
+ * document: the header, the base comment, one chunk with one file pointer,
191157
+ * and one anchored footnote on that hunk's note with a body over 80
191158
+ * characters — pinned already in oxfmt's own wrapped four-space form (see
191159
+ * `src/SteeringFormats.test.ts`'s formatter round-trip). Deliberately not
191160
+ * authored to survive any particular formatter beyond that.
190777
191161
  */
190778
191162
  const REVIEW_SAMPLE = `# Review: sample123
191163
+
190779
191164
  <!-- base: 0000000000000000000000000000000000000000 -->
190780
191165
 
190781
191166
  ## Sample chunk
190782
191167
 
190783
- - [ ] ./sample.ts#1
190784
- what this hunk does
191168
+ - [ ] ./sample.ts#1 what this hunk does[^fn1]
191169
+
191170
+ [^fn1]:
191171
+ This note explains why the hunk exists in more detail than fits on one line
191172
+ for a reviewer.
190785
191173
  `;
190786
191174
  const HEADER_RE = /^#\s+Review:\s*(\S+)\s*$/;
190787
191175
  const BASE_COMMENT_RE = /^<!--\s*base:\s*(\S+)\s*-->$/;
190788
191176
  const CHUNK_HEADING_RE = /^##\s+(.+)$/;
190789
191177
  /** One `- [ ]`/`- [x]` pointer line: the box, the whitespace-delimited pointer token, and the inline note segment that trails it. */
190790
191178
  const FILE_POINTER_RE = /^-\s*\[([ xX])\]\s*(\S+)(?:\s+(.*))?$/;
191179
+ /**
191180
+ * Matches only the `x`/`X` box character of a `- [x]`/`- [X]` pointer line —
191181
+ * mirrors `FILE_POINTER_RE`'s shape (box then a whitespace-delimited token)
191182
+ * via a lookahead, so nothing but that one character is ever part of the
191183
+ * match. `[ \t]`, never `\s`, everywhere this mirrors `FILE_POINTER_RE`'s own
191184
+ * `\s`: `FILE_POINTER_RE` is applied per already-split line, where `\s` can
191185
+ * only ever mean horizontal whitespace, but a `\s` here (over the whole
191186
+ * multiline document, `gm`) also matches `\n` — letting `-` anchor past a
191187
+ * blank line, or the lookahead's token search cross onto a LATER line
191188
+ * entirely. Both silently widen the match past what `FILE_POINTER_RE` itself
191189
+ * accepts: an indented line (which is a chunk's own continuation/note text,
191190
+ * never a pointer — `FILE_POINTER_RE` requires `-` at column 0, with no
191191
+ * leading-indentation allowance of its own) and a `- [x]` with no token on
191192
+ * its OWN line. A prose line, a chunk heading, or a `- [x]` with no token
191193
+ * after the box never matches at all.
191194
+ */
191195
+ const FILE_POINTER_TICK_RE = /^(-[ \t]*\[)[xX](?=\][ \t]*\S)/gm;
190791
191196
  /** A pointer token's trailing `#<line>` (greedy, so a `#` inside the path stays in it). */
190792
191197
  const POINTER_LINE_RE = /^(.*)#(\d+)$/;
190793
191198
  /** The optional dash that may lead the inline note segment or a continuation line — em dash, en dash, or hyphens. */
@@ -190810,11 +191215,18 @@ const isPointerToken = (token) => {
190810
191215
  const lineMatch = POINTER_LINE_RE.exec(token);
190811
191216
  return (lineMatch ? lineMatch[1] : token).length > 2;
190812
191217
  };
190813
- /** One `- [ ]` / `- [x]` file-pointer line, or `undefined` if `line` isn't one. Text trailing the pointer token on its own line still parses as the note's inline segment, not an error. */
191218
+ /**
191219
+ * One `- [ ]` / `- [x]` file-pointer line, or `undefined` if `line` isn't
191220
+ * one. Text trailing the pointer token on its own line still parses as the
191221
+ * note's inline segment, not an error. A marker written directly against the
191222
+ * token (`./a.ts#1[^fn1]`, the most natural way to comment on a hunk) is
191223
+ * stripped before path/line parsing — otherwise it's swallowed into the
191224
+ * `\S+` token capture and corrupts the path.
191225
+ */
190814
191226
  const parseFilePointer = (line, sourceLine) => {
190815
191227
  const match = FILE_POINTER_RE.exec(line);
190816
191228
  if (!match) return void 0;
190817
- const token = match[2];
191229
+ const token = stripFootnoteMarkers(match[2]);
190818
191230
  if (!isPointerToken(token)) return void 0;
190819
191231
  const lineMatch = POINTER_LINE_RE.exec(token);
190820
191232
  const path = lineMatch ? lineMatch[1] : token;
@@ -190838,19 +191250,29 @@ const parseFilePointer = (line, sourceLine) => {
190838
191250
  * next pointer/heading isn't absorbed.
190839
191251
  */
190840
191252
  const pointerEndIndex = (body, index) => {
191253
+ const texts = body.map((b) => b.text);
190841
191254
  let end = index;
190842
191255
  for (let i = index + 1; i < body.length; i += 1) {
190843
191256
  const trimmed = body[i].text.trim();
190844
191257
  if (trimmed.length === 0) continue;
190845
- if (parseFilePointer(trimmed, body[i].line)) break;
191258
+ if (parseFilePointer(trimmed, body[i].line) || isFootnoteDefinitionLine(texts, i)) break;
190846
191259
  end = i;
190847
191260
  }
190848
191261
  return end;
190849
191262
  };
190850
- /** Joins a pointer's gathered explanation: every non-blank line strictly between `startIndex` and `endIndex` (inclusive), each stripped of a leading dash, joined with a single space. */
191263
+ /**
191264
+ * Joins a pointer's gathered explanation: every non-blank line strictly
191265
+ * between `startIndex` and `endIndex` (inclusive), each stripped of a
191266
+ * leading dash, joined with a single space. A footnote definition line (and
191267
+ * its continuations) is never gathered — `pointerEndIndex` already stops the
191268
+ * span before reaching one, so this skip only matters for a definition that
191269
+ * begins exactly at `endIndex` itself.
191270
+ */
190851
191271
  const gatherNote = (body, startIndex, endIndex) => {
191272
+ const texts = body.map((b) => b.text);
190852
191273
  const noteLines = [];
190853
191274
  for (let j = startIndex + 1; j <= endIndex; j += 1) {
191275
+ if (isFootnoteDefinitionLine(texts, j)) continue;
190854
191276
  const t = body[j].text.trim();
190855
191277
  if (t.length > 0) noteLines.push(t.replace(NOTE_SEPARATOR_RE, ""));
190856
191278
  }
@@ -190886,7 +191308,7 @@ const secondPointerError = (title, pointer, secondToken) => {
190886
191308
  const parsePointerSpan = (title, body, index, pointer) => {
190887
191309
  const endIndex = pointerEndIndex(body, index);
190888
191310
  const inlineSegment = (pointer.inlineNote ?? "").replace(NOTE_SEPARATOR_RE, "");
190889
- const note = [inlineSegment, gatherNote(body, index, endIndex)].filter((segment) => segment.length > 0).join(" ");
191311
+ const note = stripFootnoteMarkers([inlineSegment, gatherNote(body, index, endIndex)].filter((segment) => segment.length > 0).join(" "));
190890
191312
  const { inlineNote: _inlineNote, ...pointerFields } = pointer;
190891
191313
  const file = {
190892
191314
  ...pointerFields,
@@ -190906,10 +191328,11 @@ const parseChunkBody = (title, body) => {
190906
191328
  const files = [];
190907
191329
  const errors = [];
190908
191330
  const descriptionLines = [];
191331
+ const texts = body.map((b) => b.text);
190909
191332
  let i = 0;
190910
191333
  while (i < body.length) {
190911
191334
  const trimmed = body[i].text.trim();
190912
- if (trimmed.length === 0) {
191335
+ if (trimmed.length === 0 || isFootnoteDefinitionLine(texts, i)) {
190913
191336
  i += 1;
190914
191337
  continue;
190915
191338
  }
@@ -190925,7 +191348,7 @@ const parseChunkBody = (title, body) => {
190925
191348
  i = nextIndex;
190926
191349
  }
190927
191350
  return {
190928
- description: descriptionLines.join(" "),
191351
+ description: stripFootnoteMarkers(descriptionLines.join(" ")),
190929
191352
  files,
190930
191353
  errors
190931
191354
  };
@@ -190939,7 +191362,7 @@ const splitChunks = (lines) => {
190939
191362
  i += 1;
190940
191363
  continue;
190941
191364
  }
190942
- const title = chunkMatch[1].trim();
191365
+ const title = stripFootnoteMarkers(chunkMatch[1].trim());
190943
191366
  const headingLine = i;
190944
191367
  i += 1;
190945
191368
  const body = [];
@@ -191012,6 +191435,19 @@ const parseReviewDoc = (content) => {
191012
191435
  errors: findings.map((f) => f.message)
191013
191436
  };
191014
191437
  };
191438
+ /**
191439
+ * Resets every `- [x]`/`- [X]` pointer box in `content` back to `- [ ]` —
191440
+ * ticks are read-progress, never sign-off, and are cleared on every land at
191441
+ * the human review gate (see `src/Edge.ts#renderDecision`). An anchored
191442
+ * multiline regex replace, NOT `split(/\r?\n/)` + `join("\n")`: a split/join
191443
+ * would normalize CRLF and rewrite every line of a CRLF checkout, turning a
191444
+ * tick-only round into a whole-file diff (`src/Git.ts`'s `hashObjects` warns
191445
+ * of the same failure mode). Line-wise, not document-wise: it never parses
191446
+ * the document as a structure, so a structurally broken file still gets its
191447
+ * ticks cleared. Total — never throws, and idempotent since the result has no
191448
+ * `[xX]` left for a second pass to find.
191449
+ */
191450
+ const clearFilePointerTicks = (content) => content.replace(FILE_POINTER_TICK_RE, "$1 ");
191015
191451
  /** Flips the `[ ]`/`[x]` box of the hunk line at `line`, preserving path/note text exactly. */
191016
191452
  const toggleFilePointer = (content, line) => {
191017
191453
  const raw = content.split(/\r?\n/)[line];
@@ -191055,19 +191491,39 @@ const spanRange$1 = (lines, startLine, endLine) => ({
191055
191491
  character: (lines[endLine] ?? "").length
191056
191492
  }
191057
191493
  });
191058
- /** Document outline for `.gtd/REVIEW.md`: only the headlines of chunks (the user-facing "work packages") that still carry at least one unchecked hunk — the outline is the list of packages left to review, nothing else. No children — a chunk headline is the whole node. */
191494
+ /** One footnote marker rendered as an outline leaf: `[^name] <body>`. */
191495
+ const footnoteLeaf = (lines, definitionByName, marker) => ({
191496
+ name: `[^${marker.name}] ${definitionByName.get(marker.name) ?? ""}`.trim(),
191497
+ range: lineRange(lines, marker.line),
191498
+ selectionRange: lineRange(lines, marker.line),
191499
+ leaf: true
191500
+ });
191501
+ /**
191502
+ * Document outline for `.gtd/REVIEW.md`: the headlines of chunks (the
191503
+ * user-facing "work packages") that still carry at least one unchecked hunk
191504
+ * — the outline is the list of packages left to review — PLUS any chunk
191505
+ * that carries at least one footnote, even when every hunk in it is ticked:
191506
+ * `reviewOutline` otherwise drops a fully-checked chunk, which would make a
191507
+ * human's own comment vanish the moment they tick the last box. A chunk's
191508
+ * footnotes are `leaf: true` children; a chunk with none has no children.
191509
+ */
191059
191510
  const reviewOutline = (content) => {
191060
191511
  const { changesets } = parseReviewDoc(content);
191512
+ const { markers, definitions } = parseFootnotes(content);
191513
+ const definitionByName = new Map(definitions.map((d) => [d.name, d.body]));
191061
191514
  const lines = content.split(/\r?\n/);
191062
191515
  return changesets.map((chunk, i) => {
191063
191516
  const start = chunk.headingLine;
191064
191517
  const end = Math.max(start, (changesets[i + 1]?.headingLine ?? lines.length) - 1);
191065
191518
  const checkedCount = chunk.files.filter((file) => file.checked).length;
191519
+ const chunkMarkers = markers.filter((m) => m.line >= start && m.line <= end);
191520
+ const children = chunkMarkers.map((m) => footnoteLeaf(lines, definitionByName, m));
191066
191521
  return {
191067
191522
  name: `${chunk.title} (${checkedCount}/${chunk.files.length})`,
191068
191523
  range: spanRange$1(lines, start, end),
191069
191524
  selectionRange: lineRange(lines, start),
191070
- unchecked: checkedCount < chunk.files.length
191525
+ ...children.length > 0 ? { children } : {},
191526
+ unchecked: checkedCount < chunk.files.length || chunkMarkers.length > 0
191071
191527
  };
191072
191528
  }).filter((node) => node.unchecked).map(({ unchecked: _unchecked, ...node }) => node);
191073
191529
  };
@@ -191094,12 +191550,33 @@ const toggleChunkEdits = (content, headingLine) => {
191094
191550
  }
191095
191551
  return edits;
191096
191552
  };
191097
- /** Actions for `.gtd/REVIEW.md`: "check/uncheck this hunk" when `range` sits on a hunk line, "check/uncheck all hunks" when `range` sits anywhere in a chunk (heading or body). */
191553
+ /**
191554
+ * The block a footnote lands after when "add a footnote" fires with the
191555
+ * cursor at `cursorLine`: the containing hunk's own span (never split, even
191556
+ * across a multi-paragraph note) when the cursor sits in one, otherwise the
191557
+ * surrounding prose block (the next blank line or EOF).
191558
+ */
191559
+ const footnoteBlockEnd = (content, lines, cursorLine) => {
191560
+ const { changesets } = parseReviewDoc(content);
191561
+ const hunk = changesets.flatMap((chunk) => chunk.files).find((file) => cursorLine >= file.sourceLine && cursorLine <= file.endLine);
191562
+ return hunk ? hunk.endLine : proseBlockEnd(lines, cursorLine);
191563
+ };
191564
+ /**
191565
+ * Actions for `.gtd/REVIEW.md`: "check/uncheck this hunk" when `range` sits
191566
+ * on a hunk line, "check/uncheck all hunks" when `range` sits anywhere in a
191567
+ * chunk (heading or body), and "add a footnote" everywhere EXCEPT inside an
191568
+ * existing marker's span or on an existing definition's own line — planting
191569
+ * a new marker/definition there would corrupt the footnote already written.
191570
+ */
191098
191571
  const reviewActions = (content, range) => {
191099
191572
  const { changesets } = parseReviewDoc(content);
191100
191573
  const lines = content.split(/\r?\n/);
191101
191574
  const cursorLine = range.start.line;
191102
191575
  const actions = [];
191576
+ if (!isOnExistingFootnote(content, range.start)) actions.push({
191577
+ title: "gtd: add a footnote",
191578
+ edits: footnoteAdditionEdits(content, range.start, footnoteBlockEnd(content, lines, cursorLine))
191579
+ });
191103
191580
  changesets.forEach((chunk, i) => {
191104
191581
  const hunk = chunk.files.find((file) => cursorLine >= file.sourceLine && cursorLine <= file.endLine);
191105
191582
  if (hunk) {
@@ -191134,7 +191611,7 @@ const reviewActions = (content, range) => {
191134
191611
  * concern, not this module's — the path returned here is the pointer's raw
191135
191612
  * `./`-relative text.
191136
191613
  */
191137
- const reviewPointerAt = (content, line) => {
191614
+ const hunkPointerAt = (content, line) => {
191138
191615
  const { changesets } = parseReviewDoc(content);
191139
191616
  const hunk = changesets.flatMap((chunk) => chunk.files).find((file) => file.sourceLine === line);
191140
191617
  if (!hunk) return void 0;
@@ -191143,11 +191620,24 @@ const reviewPointerAt = (content, line) => {
191143
191620
  line: hunk.line !== void 0 ? hunk.line - 1 : 0
191144
191621
  };
191145
191622
  };
191623
+ /**
191624
+ * `.gtd/REVIEW.md`'s `pointerAt`: footnotes resolve FIRST (they're
191625
+ * column-scoped, the hunk jump is line-scoped — a marker sitting in a hunk's
191626
+ * inline note would otherwise be shadowed by it), the hunk-pointer jump to
191627
+ * another file second. An orphan marker/definition resolves (via
191628
+ * `footnotePointerAt`) to "handled, but no pointer" — so it returns
191629
+ * `undefined` here too, rather than falling through to the hunk jump.
191630
+ */
191631
+ const reviewPointerAt = (content, position) => {
191632
+ const footnote = footnotePointerAt(content, position);
191633
+ if (footnote) return footnote.pointer;
191634
+ return hunkPointerAt(content, position.line);
191635
+ };
191146
191636
  //#endregion
191147
191637
  //#region src/SteeringFormats.ts
191148
191638
  const STEERING_FORMATS = /* @__PURE__ */ new Map([["qa", QA_FORMAT], ["review", {
191149
191639
  sample: REVIEW_SAMPLE,
191150
- validate: (content) => parseReviewFindings(content).findings,
191640
+ validate: (content) => [...parseReviewFindings(content).findings, ...parseFootnotes(content).findings],
191151
191641
  outline: reviewOutline,
191152
191642
  actions: reviewActions,
191153
191643
  pointerAt: reviewPointerAt
@@ -191774,7 +192264,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
191774
192264
  };
191775
192265
  //#endregion
191776
192266
  //#region src/workflows/unified.yaml
191777
- var unified_default = "# gtd's BUILT-IN DEFAULT workflow — used when no `workflow:` key is\n# configured anywhere in the cwd→home config chain; `gtd init` seeds only\n# vars.testCommand + a modes: suggestion, never this file. Compiled through\n# the same `compileWorkflowConfig` a user's own `workflow:` goes through — no\n# privileged path. See skills/authoring/SKILL.md for how to read or edit it\n# (machines, patterns, templates) and docs/driver.md for how a driver runs it.\n#\n# One flow: any change to the tree starts the process. `idle` -> `unwind`\n# reverts that diff (intent survives only in history) -> a green-baseline\n# gate -> `design.triage` groups the reverted diff into ordered, classified\n# concerns and resolves the PRODUCT ones' open questions (`design.gate`) ->\n# `architecture.author` works out the *how* and resolves the TECHNICAL ones\n# (`architecture.gate`) -> `architecture.decompose` writes one package file\n# per concern -> the per-package queue (`packages.*`) builds and reviews each\n# -> the shared tail (`build.review.*`) decides sign-off (-> `idle`, landing\n# an ordinary commit that closes the process — every per-turn commit stays on\n# the branch; run `gtd summary` afterward for a closing-message prompt) vs.\n# actionable feedback (-> `re-unwind` -> back to `design.triage`, a full\n# re-plan lap that never builds on the human's hand-edit). `--entry\n# review-gate.check --var reviewBase=<commitish>` and `--entry fix-precheck`\n# are the two other entries into this same flow.\nvars:\n testCommand: npm test\n # plannerModel: the heavier tier for one-shot triage/design/review turns.\n # coderModel: the tier for build/fix turns. Both repointable via a\n # top-level `vars:` key or a `GTD_<NAME>` env var, like any other var.\n plannerModel: smart\n coderModel: base\n # Declared (not left undeclared) so `--var reviewBase=...` passes the CLI's\n # declared-name check; blank renders empty, which the review entry refuses.\n reviewBase: \"\"\n # gtd's own voice for generated files — adapted from the \"Spartan\" output\n # style (https://github.com/alexgreensh/attention-span, AGPL-3.0, version 0.6),\n # rewritten in gtd's own words for deliverables rather than chat replies;\n # no upstream text ships. A point-in-time derivation with no refresh\n # mechanism.\n styleBlock: |-\n This file is a deliverable, not a chat reply — size follows the work, and\n padding is the target of any cut. Lead with the answer immediately; never\n circle back to restate it once it has landed.\n\n Write flat, commanding sentences — commit to the claim, do not hedge it.\n Favor everyday vocabulary; where a term is unavoidable, define it in five\n words or fewer on first use.\n\n In markdown, bold carries the load: bold the claim, not the sentence around\n it — reading only the bold text must yield the full point and every risk.\n\n Never trim a risk, a number, a threshold, or a scoped condition to save\n space. This outranks every other rule here.\n\n Ship the artifact bare — no lead-in, no sign-off wrapped around it.\n Compressing is not dropping: a task with three load-bearing parts ships all\n three, each shorter, never two of three.\n\n One idea per block; break when the idea shifts. Flag risk in one blunt\n line rather than hedged prose. Never narrate the work — do it.\n styleFormatContract: |-\n This file is machine-read; its format contract outranks every style rule\n above it: keep every `##`/`###` heading, every `- [ ]` checkbox row, and\n every marker line exactly as specified — do not renumber or rename a\n heading or replace a checkbox list with prose. A parser reads these\n literally; a violation refuses the turn outright.\n\n Voice rules above govern the prose between these elements, never the\n elements themselves.\n\n # One persona per prompt-bearing machine below, stamped as its `system:`\n # (see src/StateFields.ts's `system`). `--system-prompt` REPLACES a\n # harness's default system prompt — including its injected cwd/env/\n # git-status block — so each persona is written self-contained, restating\n # that context explicitly rather than relying on it.\n\n # Shared conduct tail, appended after every persona's own role paragraph:\n # use tools without asking, orient yourself with git, and go inspect what\n # the turn's message names. Deliberately NOT named `*Persona`:\n # `templates.test.ts` derives its six-name persona set from\n # `it\\.vars\\.(\\w+Persona)`, so that suffix would silently grow the set.\n agentConduct: |-\n You have shell and file tools — bash, read, write, edit — and you are\n expected to use them without asking first. This runs unattended: there is\n no one to grant permission, confirm a command, or approve a plan before\n you act. Investigate with real commands rather than assuming what a file,\n a commit, or a decision's status actually is, then act on what you find.\n\n You are already inside a git repository, at the working directory you\n should treat as your whole world for this turn. There is no injected\n status block — no cwd, no branch, no recent history, no tree state. Run\n `git status`, `git log`, `ls` and the like yourself before you do\n anything else; that is the normal first move of this turn, not an extra\n step.\n\n The message you receive each turn names a commit, a range, or a file for\n you to go inspect yourself — `git show`, `git diff`, reading the file\n directly — never a diff or a summary rendered into the conversation. Go\n look before you act. Across the several turns one conversation may span,\n work from what you already read and settled earlier rather than\n re-deriving it from a cold read every time, unless the turn's own\n instructions tell you this is a fresh, cold pass.\n designPersona: |-\n You are the product-facing planning voice in an autonomous build pipeline\n called gtd. Your job is to turn a raw, half-formed sketch — a hand-edit to\n real code, a scratch note, or both — into an ordered, classified list of\n concerns a build can execute, and to carry a running conversation with a\n human about the product decisions that list depends on. You are read by\n that human, so write plainly and decisively; you are not read by a parser\n except where a state's own instructions say otherwise.\n architectPersona: |-\n You are the technical planning voice in an autonomous build pipeline\n called gtd. You take over once the product-facing concerns are settled,\n reading them cold — you do not carry over the product conversation, only\n its written, settled output. Your job is to work out the technical *how*\n for each settled concern — file and module structure, data models,\n library and tech-stack choices, error-handling strategy — raise the\n technical open questions a human still needs to decide, and once every\n question is answered, write one package spec file per concern with no\n further judgement call: the grouping and ordering were already decided\n upstream, and this final step is a mechanical write-out of a plan you\n already converged on.\n reviewerPersona: |-\n You are the independent reviewing mind in an autonomous build pipeline\n called gtd — deliberately a separate identity from whichever agent wrote\n the code under review, so you bring no attachment to how it got built.\n Your job splits into two distinct turns: first, writing a structured\n review document that groups a diff into reviewable chunks for a human to\n read through and comment on; later, reading back a round of that human's\n feedback — a note, a hand-edit — and classifying whether it is genuinely\n actionable or just an approving remark, without fixing anything yourself\n either way. You judge and you classify; you never build.\n specReviewerPersona: |-\n You are the adversarial spec-conformance checker in an autonomous build\n pipeline called gtd, reviewing one freshly-built work package against the\n written spec it was built from. Your only job is to verify: are the\n package's tasks actually done, its acceptance criteria actually met, and\n the code sound and consistent with the rest of the codebase? You write\n concrete, actionable feedback only when something is genuinely wrong —\n and when nothing is, you write nothing at all, because a clean turn from\n you IS the approval. You never fix a problem you find; naming it\n precisely enough for someone else's fix turn to act on is the whole job.\n builderPersona: |-\n You are the TDD implementer in an autonomous build pipeline called gtd.\n Your job is to build exactly one package's declared scope end to end,\n tests first, and to come back and fix things when redirected: a failing\n check's output, or a reviewer's concrete feedback. Stay strictly inside\n the package in front of you, touching neither another package's files nor\n code outside the current task. Treat feedback you're given as the\n diagnosis to act on; discard it, with reason, only when it is simply\n wrong.\n finisherPersona: |-\n You are the closing identity in an autonomous build pipeline called gtd —\n the last coder this process hands work to before it closes. You are\n handed a late-breaking failing check after sign-off; fix it, focused and\n minimal, before the process can close. Every turn you land stays its own\n commit on the branch — nothing here gets squashed away.\n\n # The scratchpad-opener sentence shared by every prompt-content state (see\n # STATE_FIELDS's `prompt`) — this workflow's own state files are its private\n # working notes, never project code or documentation a human reads. Injected\n # as the leading line of every prompt body that touches a `.gtd/` file\n # directly; each state's own \"the only state file this turn touches is X\"\n # sentence stays local, immediately after the tag. Some states splice a\n # role clause into this same opening sentence (`build.review.collecting`,\n # `packages.item.spec.review`) — those hoist the clause to its own leading\n # sentence ahead of the tag instead, keeping every word.\n stateFileRules: |-\n You are an autonomous coding agent. This workflow steers itself\n through its own state files — treat them as its private scratchpad,\n never as project code or documentation.\n # The open-questions warrant test, decide-it-yourself sink, and `## Open\n # Questions` checkbox shape shared by `design.triage` and\n # `architecture.author` — written phase-agnostic (no PRODUCT/TECHNICAL\n # bake-in) so both sites can inject it verbatim. Each site's own\n # phase-scope sentence (triage: product-only, technical waits; architecture:\n # every point here is TECHNICAL) stays local around the tag — the two\n # genuinely disagree on scope, so that sentence is never shared.\n # `questionBarReturn` is the return-lap half — split out so each site can\n # inject it under its own `## Return lap` heading instead of burying\n # return-lap behaviour inside the first lap's own section.\n questionBar: |-\n For every open point, a question is warranted only when all three hold:\n the answer is a genuine fork with divergent outcomes — user-visible for a\n product question, materially different implementations for a technical\n one; the sketch, the entry diff, and history do not already settle it\n (treat anything the entry diff states explicitly — a directive in\n `.gtd/TODO.md`, a committed choice in a hand-edit — as already settled,\n and on a review loop-back lap treat whatever the human's own review-round\n edit or note states explicitly the same way); and getting it wrong would\n survive all the way to the human review tail.\n\n Anything below that bar, decide yourself: record the decision under\n `## Answered Questions` — the same section a human-answered question\n lands in — as `### <the point phrased as a question>` followed by the\n decision and a one-line rationale in prose, no checkboxes.\n `## Answered Questions` must come after every other `##` section in the\n document — it is always the last section.\n\n A question that clears the bar is written under a `## Open Questions`\n heading that must come before every other `##` section in the document —\n not just near the top, but ahead of every other `##` section — and takes\n this shape: one `### <question>` sub-heading each, followed by a checkbox\n list of candidate answers — two concrete options plus a final free-text\n slot, all left unticked:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n Never tick a box yourself — the human ticks exactly one per question.\n questionBarReturn: |-\n The human has ticked one option per question (or replaced\n `_your answer_` with their own text): fold each chosen answer into its\n concern's prose, then move the question into a `## Answered Questions`\n section as `### <question>` followed by the resolved answer in plain\n prose (drop the checkboxes). `## Answered Questions` must come after\n every other `##` section in the document — it is always the last\n section, so a moved question lands there, not wherever `## Open\n Questions` used to sit. Never re-raise a deleted question; treat a\n deleted `## Open Questions` section as acceptance. Omit `## Open\n Questions` entirely once there are none left.\n # The body `packages.item.fix-suite` and `build.fix` share byte for byte —\n # both fix a red `.gtd/FEEDBACK.md`. `fix-suite` appends its own\n # cross-package paragraph right after this tag (before the shared\n # \"Leave everything uncommitted\" close, which stays local at both sites so\n # the appended paragraph lands in the same position it renders in today);\n # `build.fix` renders the tag alone.\n fixFeedbackPrompt: |-\n The only state file this turn touches is `.gtd/FEEDBACK.md`; address it,\n then delete it. Do not create other files to hold notes or output.\n\n Read `.gtd/FEEDBACK.md` (the failing test output) and fix the\n code so the suite passes. Keep the change focused — do not refactor\n unrelated code. If the feedback is wrong, fix nothing rather than\n \"fixing\" a non-issue.\n\n Delete `.gtd/FEEDBACK.md` either way — once the suite passes, or once\n you have established the feedback was wrong. Leaving it in place is\n never the right end state: the next check writes its own.\n\n# `gtd summary`'s prompt — printed cold, no session identity, no diff inlined.\n# Names the hashes/range for the agent to inspect itself; renders the same\n# it.processCost/it.processCostByModel the old squash finale used to.\nsummary: |\n <%~ it.vars.styleBlock %>\n\n\n Write the closing message for the process HEAD closes or currently sits\n inside — the text a driver may use for a squash, an amend, or a pull\n request body. You are starting cold: read every decision back out of the\n commits named below rather than assuming any prior context.\n\n Cover the initial motivation, the important decisions, and the trade-offs\n made, and the high-level architectural changes — never which files\n changed; that is what `git diff --stat` is for, not this message.\n\n The process's entry commit is `<%= it.entryCommit %>`. <% if\n (it.humanCommits.length > 0) { %>The human contributed at these commits,\n oldest to newest:\n <% it.humanCommits.forEach(function (c) { %>\n - `<%= c.hash %>` (entering `<%= c.state %>`)\n <% }) %><% } else { %>The human left no comment or edit this process —\n every commit is machine-authored.<% } %>\n\n Inspect the range yourself: `git log <%= it.processBase %>..<%= it.processTip %>`\n and `git diff <%= it.processBase %> <%= it.processTip %>`. That range is\n exactly what a squash or PR body should describe.\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n\n Print the closing message and stop — this command writes nothing itself.\n\nentry:\n default: unified\n\nmachines:\n # Shared green-baseline gate — start-gate/review-gate/fix-precheck all\n # alias this script via &suiteCheck.\n entryGate:\n params: [onGreen, blockedMessage, blockedDescribe, checkLabel, blockedLabel, reviewBase]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: &suiteCheck |\n #!/usr/bin/env sh\n set +e\n mkdir -p .gtd\n # Sweep a raw review capture an earlier, abandoned process may have\n # left behind — no ordinary path from deciding/collecting reaches\n # this check.\n rm -f .gtd/REVIEW_RAW.md\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output .gtd/FEEDBACK.md\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > .gtd/FEEDBACK.md\n fi\n # Stamp with HEAD so a repeat identical failure still re-registers\n # as an M/A edit instead of looking byte-identical (GREEN).\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> .gtd/FEEDBACK.md\n else\n rm -f .gtd/.check-output\n rm -f .gtd/FEEDBACK.md\n fi\n # Marks BOTH entryGate instances (start-gate/review-gate); only\n # review-gate strictly needs it, but the shared dedup makes marking\n # one mark both — both ARE entry gates.\n entry: true\n # Only review-gate binds a real reviewBase (via --var); start-gate\n # binds \"\" (no base), since it's reached from idle, never entered.\n reviewBase: $reviewBase\n on:\n \"A .gtd/FEEDBACK.md\": blocked\n \"M .gtd/FEEDBACK.md\": blocked\n \"D .gtd/REVIEW_RAW.md\": $onGreen\n \"D .gtd/FEEDBACK.md\": $onGreen\n \"C\": $onGreen\n blocked:\n actor: human\n label: $blockedLabel\n file: FEEDBACK.md\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $blockedDescribe\n\n # Shared check/escalate pair for build.health.*/packages.item.health.* —\n # the caller's own coder fix-state ($onRed), not this machine, handles red.\n healthGate:\n params: [onGreen, onRed, escalateDescribe, checkLabel, escalateLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # See the shared suite check on `entryGate.check` above.\n script: *suiteCheck\n on:\n \"A .gtd/FEEDBACK.md\": $onRed\n \"M .gtd/FEEDBACK.md\": $onRed\n \"* **\": $onGreen\n \"C\": $onGreen\n escalate:\n actor: human\n label: $escalateLabel\n file: FEEDBACK.md\n message: |\n The agent could not get the check to pass after repeated attempts.\n `.gtd/FEEDBACK.md` holds the last failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $escalateDescribe\n\n # Shared check/answer pair for design.gate/architecture.gate. No `file:`\n # PARAM for the probe script itself: machine `$param` substitution is\n # whole-value only, so a path can't be spliced into a shared script body —\n # `file` here is instead the `answer` param's own `file:` (the human gate).\n questionGate:\n params: [file, message, onNone, onRevise, checkLabel, answerLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: |\n #!/usr/bin/env sh\n # Exactly one of REQUIREMENTS.md/ARCHITECTURE.md exists on disk at\n # a time — architecture.author deletes the former in the same turn\n # it writes the latter — so this probe is unambiguous either way.\n set +e\n mkdir -p .gtd\n file=.gtd/REQUIREMENTS.md\n [ -f \"$file\" ] || file=.gtd/ARCHITECTURE.md\n gtd check qa \"$file\" --open-questions > /dev/null 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n printf 'open questions remain in %s\\n' \"$file\" > .gtd/QUESTIONS.md\n # See the shared suite check's cache-buster rationale on\n # `entryGate.check` above.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> .gtd/QUESTIONS.md\n else\n rm -f .gtd/QUESTIONS.md\n fi\n on:\n \"A .gtd/QUESTIONS.md\": answer\n \"M .gtd/QUESTIONS.md\": answer\n \"D .gtd/QUESTIONS.md\": $onNone\n \"C\": $onNone\n answer:\n actor: human\n label: $answerLabel\n file: $file\n mode: qa\n answerGate: true\n message: $message\n on:\n \"* **\":\n to: $onRevise\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace\n `_your answer_` for your own) to send it back for the agent to\n fold your answers in. Delete a question to skip it, or delete\n the whole `## Open Questions` section to accept the plan as-is.\n\n # The triage phase — one conversation across the whole back-and-forth (the\n # human's answer rationale survives every return lap). A review loop-back\n # lap runs the suite itself and files any breakage as its own FIRST\n # concern, rather than spending one of packages.item.fix-suite's\n # (per-episode) retries on breakage the package didn't cause.\n # ▸ planner identity.\n designPlan:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.designPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onArchitecture]\n entry: triage\n states:\n triage:\n actor: agent\n label: Triaging the change\n file: REQUIREMENTS.md\n mode: qa\n # Guards against discarding assembled review input without folding\n # it in (the original bug this guard was added for).\n requireProgress: true\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this turn touches is `.gtd/REQUIREMENTS.md`; develop it. Do\n not create other files to hold notes or output.\n\n `.gtd/TODO.md` is the likely home of the sketch that\n started this process. Despite living beside this workflow's own\n state files, it is the human's input — the sketch to be folded into\n the concerns below like any other part of the start diff. It is\n never a state file to preserve, and never gtd bookkeeping to ignore.\n\n This process started at commit `<%= it.startCommit %>`. The change\n that started it was reverted out of the working tree by the\n `unwind` beat right after it landed, so `git diff <%= it.startCommit %>`\n (against the working tree) is now empty — its content survives only\n in history. Find the entry commit yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\n (the process's very first turn, landed immediately after\n `<%= it.startCommit %>`, before any later baseline-repair commits) —\n then `git show` that commit to see everything that started this\n process: a hand-edit to real code, a scratch note in some file, or\n both.\n\n Whichever lap this is, leave `.gtd/REQUIREMENTS.md` uncommitted and\n finish your turn once it reflects this lap's own work below.\n\n ## First lap\n\n `.gtd/REQUIREMENTS.md` does not exist yet (or holds whatever the\n human's own change happened to leave there). Build it from that\n start diff:\n\n ### Group into concerns\n\n Group the diff into an ordered list of concerns — each one a\n coherent piece of work that can leave the test suite green on\n its own, in the order you'd want them built. If a concern would\n red-light tests that a later concern is meant to fix, that is\n not a valid split: merge the two into one concern.\n\n Two more tests sit beside that green floor, not instead of it.\n Vertical, not horizontal: slice by capability, never by layer. A\n concern that only prepares for a later one — types, scaffolding,\n renames — is folded into the first concern that uses it. Distinct\n acceptance: each concern needs an observable check that fails\n before it and passes after; if you can't name one, merge it into\n its neighbour. Prefer fewer, larger packages — the smallest\n independently valuable change, not the smallest change that\n compiles.\n\n ### Classify each concern\n\n Classify each concern as PRODUCT (a user-facing/requirements\n decision) or TECHNICAL (an implementation decision).\n\n <%~ it.vars.questionBar %>\n\n\n Raise questions only for product concerns here — a technical\n concern's open points wait for the next phase. When every concern\n in this lap is TECHNICAL, write no `## Open Questions` section at\n all.\n\n ### Fold in the sketch\n\n Fold everything the entry commit added into the concerns above —\n a scratch note and a real code edit are both just a sketch of\n the intent, a spec to be finished, never work to be preserved;\n `unwind` already reverted both out of the tree, so there is\n nothing left to delete. Describe each piece under the concern it\n belongs to.\n\n ## Return lap\n\n <%~ it.vars.questionBarReturn %>\n\n ## Review loop-back\n\n `.gtd/REQUIREMENTS.md` already holds concerns and none of them\n have a ticked-but-unfolded answer waiting — that shape means a\n completed REVIEW round put this material here, not a question you\n asked. Develop those existing concerns further with whatever the\n round raised; never rediscover or regroup them from a cold read\n the way the first lap does.\n\n The human's own edit from that review round has been reverted out\n of the working tree the same way the entry commit is — read it\n from history, never the working tree: run\n `git show <%= it.reviewBase %>` to see it. (On the first lap that\n same hash is the process's own diff base, `<%= it.startCommit %>`,\n so this branch does not apply: `.gtd/REQUIREMENTS.md`\n does not exist yet for a review round to have happened against.)\n\n Whatever this round touches, every decision already settled under\n `## Answered Questions` stays settled on this lap too — never\n re-open one. A genuinely open PRODUCT point in the round's own\n material may still raise a fresh question under\n `## Open Questions`: the product gate sits on this path exactly as\n it does on the first lap, and will ask it.\n\n Before grouping anything on this lap, run\n `<%= it.vars.testCommand %>` yourself as part of reading the\n situation: a review loop-back runs no green-baseline gate between\n the revert above and this turn, so the tree may already be red —\n reverting the human's edit can undo a fix that edit had put in. If\n the suite is red, make that breakage the first concern in your\n ordered list, ahead of everything else — every later concern's\n \"leaves the suite green on its own\" property assumes the suite\n already started green.\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: REQUIREMENTS.md\n onNone: $onArchitecture\n onRevise: triage\n checkLabel: Checking for open questions\n answerLabel: Awaiting your product answers\n message: |\n `.gtd/REQUIREMENTS.md` holds the concerns under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n # A separate machine from designPlan (its own memory scope — a COLD read,\n # not a resumed conversation). Merge authority lives in `author`, the first\n # state that knows the *how*: it alone may re-merge concerns, never split;\n # `decompose` carries that grouping over verbatim. ▸ planner identity.\n archPlan:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.architectPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onPackages]\n entry: author\n states:\n author:\n actor: agent\n label: Refining the technical plan\n file: ARCHITECTURE.md\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state files this\n turn touches are `.gtd/ARCHITECTURE.md` (write it) and\n `.gtd/REQUIREMENTS.md` (delete it once its content is\n folded in). Do not create other files to hold notes or output.\n\n You do not resume the design conversation — this is a separate\n machine with its own memory, a cold read every time. Read\n `.gtd/REQUIREMENTS.md` (the settled, ordered, classified\n concerns) in full. Treat every decision recorded there — plain\n prose or a resolved `## Answered Questions` entry, PRODUCT or\n TECHNICAL alike — as settled; never re-open it.\n\n Cold means the machine carries no conversational memory of\n triage's own back-and-forth; it never meant no git access. This\n process started at commit `<%= it.startCommit %>`. Find the\n process's first turn yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\n then `git show` that commit to see everything that started this\n process: a hand-edit to real code, a scratch note in some file,\n or both.\n\n Whichever lap this is, once `.gtd/ARCHITECTURE.md` is written,\n delete `.gtd/REQUIREMENTS.md` — its content has been folded in and\n it must not linger. Leave everything uncommitted and finish your\n turn.\n\n ## First lap\n\n Develop `.gtd/ARCHITECTURE.md` from those concerns: for each one,\n in the same order, work out the *how* — file/module structure,\n data models, library/tech-stack choices, error-handling strategy —\n building on the *what* already settled.\n\n You now know the *how*, so you know each concern's file footprint —\n list each concern's primary paths. Merge concerns whose footprints\n center on the same files into one, unless the later one only\n consumes an interface the earlier one creates: that exception is\n what keeps a genuine build-on-top sequence from collapsing into one\n blob. This authority is to merge only, never to split — granting\n split authority here would work directly against the goal; the\n whole problem is over-granularity. Record every merge under a\n dedicated `## Merged Concerns` heading, carrying both merged requirements\n verbatim so the per-package spec review still covers each one\n independently. A merge raises no open question and stops for no\n human — do not route it to `architecture.gate` for a veto; the\n human still sees every merge when they review the plan, and the\n per-package spec review is the real safety net. Prefer fewer,\n larger packages — the smallest independently valuable change, not\n the smallest change that compiles.\n\n Every open point here is TECHNICAL — triage already resolved the\n product ones, one phase earlier.\n\n <%~ it.vars.questionBar %>\n\n ## Return lap\n\n <%~ it.vars.questionBarReturn %>\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: ARCHITECTURE.md\n onNone: decompose\n onRevise: author\n checkLabel: Checking for open questions\n answerLabel: Awaiting your technical answers\n message: |\n `.gtd/ARCHITECTURE.md` holds the technical plan under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n decompose:\n actor: agent\n label: Decomposing into packages\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state files this turn touches\n are the package files it writes under `.gtd/packages/` and\n `.gtd/ARCHITECTURE.md`, which it deletes. Do not create other\n files to hold notes or output.\n\n If you wrote `.gtd/ARCHITECTURE.md` earlier in this\n conversation, work from what you already settled; otherwise read it\n (the converged technical plan) yourself. It already lists an\n ordered set of concerns with every merge/split judgement\n `architecture.author` made — this turn is a mechanical write-out,\n not a planning one. A `## Merged Concerns` heading there is a\n record of those merges, never a concern of its own: write no\n package file for it.\n\n Write one package file per concern, in the settled order, under\n `.gtd/packages/` (e.g.\n `.gtd/packages/01-name.md`,\n `.gtd/packages/02-name.md`, ...). Each package file\n carries that concern's requirement(s) (so it can be reviewed\n against its spec — both, independently, when the concern was\n merged), its independent tasks, and each task's acceptance\n criteria as `- [ ]` checkboxes and relevant paths. Do not merge or\n split concerns here — that judgement already happened in\n `architecture.author`; carry the settled grouping over verbatim.\n No package file may reference any other `.gtd/` file.\n\n Once the package files are written, delete\n `.gtd/ARCHITECTURE.md`. Leave everything uncommitted and\n finish your turn.\n on:\n \"* .gtd/packages/**\": $onPackages\n\n # The shared review tail. ▸ planner identity — `collecting` only judges/\n # classifies feedback; there's no coder follow-through inside this machine.\n #\n # Nested inside buildTail (build.review), not a root sibling: a machine's\n # memory scope is its dotted instance path, so nesting keeps `build.fix ->\n # build.health.check -> build.review.*` (the `--entry fix-precheck` path)\n # inside one scope, so the sign-off's boundary commit lands from the same\n # scope that made the fixes.\n humanReview:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.reviewerPersona %>\n\n\n <%~ it.vars.agentConduct %>\n entry: reviewing\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: REVIEW.md\n mode: review\n # `gtd --entry review-gate.check` enters via review-gate.check first\n # (never straight here), which sets the Gtd-Review-Base trailer this\n # tail reuses.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this turn touches is `.gtd/REVIEW.md`; write it at exactly\n that path. Do not create other files to hold notes or output.\n\n Write `.gtd/REVIEW.md` to help a human review the changes in\n this exact format:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.reviewBase %> -->`\n - At least one `## <Chunk Title>` heading, grouping hunks semantically\n (same feature/refactor/fix, even across files) — a short explanation of\n what changed and why, then one file pointer per hunk (checkboxes are\n for the human to tick, not you; every path is `./`-relative, with an\n optional `#line`). Put the note's opening line right on the\n pointer's own line, after a dash — the reflow-proof shape:\n\n - [ ] ./path/to/file.ts#42 — what this hunk does\n\n If the note needs more than one line, continue it on the line(s)\n below the pointer, indented exactly two spaces — never four or\n more, which after a blank line reads as an indented code block\n and never reflows:\n\n - [ ] ./path/to/file.ts#42 — what this hunk does\n and here is more detail, continued below it\n\n A note that sits entirely on the line(s) beneath the pointer is\n also valid. Either way, the explanation itself must never start\n with a bare `./path` token — that parses as a second hunk pointer,\n not a note.\n\n This prompt carries no diff — read the changes yourself. The range runs\n from `<%= it.reviewBase %>` to the working tree (committed turns plus\n anything still pending); on a feedback round that base is the previous\n review's boundary, so it covers only what changed since then, not items\n already signed off.\n\n Leave `.gtd/REVIEW.md` uncommitted and finish your turn.\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n label: Awaiting your review\n file: REVIEW.md\n mode: review\n message: |\n `.gtd/REVIEW.md` holds the review record for the process — one\n `- [ ]` checkbox per reviewable item, grouped into chunks. Tick a box\n (`- [x]`) as you review each hunk; ticking only records that you've read\n it, it is not sign-off.\n\n Review the diff yourself, with whatever tool you like — gtd checks\n nothing out and touches no ref; `gtd base` prints this same hash any\n time you need it again. The range runs from the review base to the\n working tree:\n\n git diff <%= it.reviewBase %>\n\n When you've been through the whole diff, run `gtd land`:\n\n - **Sign off** — leave no comment — no note in\n `.gtd/REVIEW.md`, no code edit — to close the process,\n whatever the boxes say. Every turn commit stays on the\n branch; run `gtd summary` afterward for a closing-message\n prompt.\n - **Request changes** — leave a comment: a note on a\n `.gtd/REVIEW.md` line, or a direct code edit — to send a FULL\n development lap (**review.deciding** → **review.collecting** →\n re-triage). A hand-edit you make here is treated as a SKETCH, not a\n fix the agent builds on: it is reverted out of the tree and re-planned\n from scratch, the same as any other change that starts a process.\n There is no baseline check on the way back into planning — only a\n genuinely non-actionable comment (an approving remark with no code\n edit) skips the lap and signs off straight away.\n\n Deleting `.gtd/REVIEW.md` is refused.\n on:\n \"* **\": deciding\n\n deciding:\n actor: check\n label: Reviewing\n file: REVIEW.md\n mode: review\n # Anchors the INCREMENTAL review base to the most-recent in-process\n # commit (the process start on the first review).\n reviewBase: true\n script: |\n #!/usr/bin/env sh\n # FEEDBACK iff the human left a REVIEW.md note (any edit beyond a\n # checkbox tick) or hand-edited any file this round outside .gtd/;\n # otherwise a clean sign-off. This turn only CAPTURES the raw\n # material into REVIEW_RAW.md — collecting judges actionability.\n # A/M REVIEW_RAW.md rows are declared before D REVIEW.md so a\n # feedback round (which also deletes REVIEW.md) isn't mistaken for\n # sign-off.\n set +e\n mkdir -p .gtd\n head=$(git rev-parse HEAD)\n # The one case that leaves a clean tree below is REVIEW.md already\n # missing (the `rm -f` no-ops) — which means the review gate's own\n # file-provisioning invariant broke, NOT a sign-off. Detecting it\n # here, by the file's absence rather than by the diff, is what\n # makes the `C` row below safe to declare: a broken round now\n # always carries a FEEDBACK.md diff and routes to a human.\n if ! git cat-file -e \"HEAD:.gtd/REVIEW.md\" 2>/dev/null; then\n printf 'there is no `.gtd/REVIEW.md` at %s — nothing was reviewed this round.\\n' \"$head\" > .gtd/FEEDBACK.md\n elif git diff-tree --no-commit-id --name-only -r HEAD -- . \":(exclude).gtd\" \\\n | grep -q . \\\n || [ \"$(git show \"HEAD^:.gtd/REVIEW.md\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\" \\\n != \"$(git show \"HEAD:.gtd/REVIEW.md\" 2>/dev/null | sed -E 's/\\[[ xX]\\]/[_]/g')\" ]; then\n # Reached only with REVIEW.md present. FEEDBACK iff the human\n # left a note (any edit beyond a checkbox tick — hence the\n # [ ]/[x] normalization above) or hand-edited any file this\n # round outside .gtd/; otherwise a clean sign-off. This turn\n # only CAPTURES the raw material into REVIEW_RAW.md —\n # collecting judges actionability.\n {\n echo \"This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.\"\n echo\n echo \"Commit: $head\"\n echo \"The human's notes are in .gtd/REVIEW.md at this commit. Any hand edits are\"\n echo \"in that commit's other paths. Run: git show $head\"\n } > .gtd/REVIEW_RAW.md\n rm -f .gtd/REVIEW.md\n else\n rm -f .gtd/REVIEW.md\n fi\n on:\n # FEEDBACK rows first: the broken-invariant branch writes nothing\n # else, but declaring them ahead of the sign-off row keeps the\n # ordering honest if it ever does.\n \"A .gtd/FEEDBACK.md\": review-missing\n \"M .gtd/FEEDBACK.md\": review-missing\n # A/M REVIEW_RAW.md before D REVIEW.md so a feedback round (which\n # also deletes REVIEW.md) isn't mistaken for sign-off.\n \"A .gtd/REVIEW_RAW.md\": collecting\n \"M .gtd/REVIEW_RAW.md\": collecting\n \"D .gtd/REVIEW.md\": $onSignoff\n # Unreachable now that the missing-REVIEW.md case writes\n # FEEDBACK.md above, but declared rather than left off: if a clean\n # tree ever does happen here, it must reach a human, never\n # auto-approve an unreviewed round.\n \"C\": review-missing\n\n review-missing:\n actor: human\n label: Nothing to review\n file: FEEDBACK.md\n message: |\n The review round committed no `.gtd/REVIEW.md`, so there is nothing\n to sign off on. `.gtd/FEEDBACK.md` holds the detail.\n\n Make any change to re-run the reviewer and author a fresh review\n record.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: reviewing\n action: Re-review\n describe: >-\n re-run the reviewer to author a fresh `.gtd/REVIEW.md`\n (**build.review.reviewing**).\n\n # Outcome is by which paths THIS diff touches: writing REQUIREMENTS.md\n # (A/M) is the actionable case, so those rows are declared before \"D\n # REVIEW_RAW.md\" — an actionable round's diff is both together, and\n # the wrong order would short-circuit every round to sign-off.\n # Consuming REVIEW_RAW.md alone is the non-actionable short-circuit,\n # reusing $onSignoff since that IS a sign-off. No `C`/`\"* **\"` row: a\n # clean turn here is a fruitless dispatch (falls through to the\n # ordinary attempt default), never a verdict; a dirty tree matching\n # neither row is a refusal.\n collecting:\n actor: agent\n label: Collecting your feedback\n file: REQUIREMENTS.md\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are an autonomous coding agent judging and classifying a round\n of review feedback.\n\n <%~ it.vars.stateFileRules %>\n\n The only state files this turn\n touches are `.gtd/REQUIREMENTS.md` and\n `.gtd/REVIEW_RAW.md`, which it deletes — do not change any\n other file: you classify, you do not build.\n\n The raw review material is: <%~ it.read(\".gtd/REVIEW_RAW.md\") %>\n\n It names a commit. If you wrote today's review earlier in this\n conversation, work from what you already reviewed; otherwise read that\n commit's diff yourself before classifying anything.\n\n The round is actionable if any of the following is true:\n\n 1. The human left a note on `.gtd/REVIEW.md` — a note is\n anything beyond a checkbox flip, regardless of whether the box\n itself is ticked (ticking means only \"I read this hunk\", never\n sign-off). A note is a mandatory concern below, whether or not\n its box is ticked.\n 2. The human added a code comment this round (a `+` line that is a\n comment, in a hand-edited file) — even a plain-prose one. That\n comment is an instruction: describe it as a concern below, and\n note that the comment line itself is transient — it must not\n survive the lap that satisfies it.\n 3. The human hand-edited any non-comment code this round. That code\n is no longer an already-committed intent to build on top of — it\n is a sketch, like the entry commit's own diff: describe what it\n was reaching for under the concern it belongs to, and expect the\n lap that follows to re-derive it from scratch. Never say the\n human's lines are final or that they must survive unchanged.\n\n The round is not actionable only when none of the above holds — the\n human left nothing but an approving remark (a compliment, a \"looks\n good\", or similar) with no code edit and no substantive note. Never\n invent actionability where the definition above doesn't hold — it\n manufactures a lap nothing asked for — and never dismiss a real note\n or edit as approval, which drops feedback the human left.\n\n If the round is actionable: write `.gtd/REQUIREMENTS.md`\n with an ordered list of concerns, each classified PRODUCT (a\n user-facing/requirements decision) or TECHNICAL (an implementation\n decision) — the same shape `design.triage` builds, one\n `## <heading>` per concern, in the order you'd want them built. Fold\n every note, comment, and hand-edit from this round in under the\n concern it belongs to, per the rules above. Raise no open\n questions here — write no `## Open Questions` section;\n `design.triage` owns that, one phase later. Then delete\n `.gtd/REVIEW_RAW.md` and finish your turn.\n\n If the round is not actionable: delete\n `.gtd/REVIEW_RAW.md` and finish your turn, writing nothing\n to `.gtd/REQUIREMENTS.md` — consuming the capture with no\n other change is the sign-off.\n on:\n \"A .gtd/REQUIREMENTS.md\": $onFeedback\n \"M .gtd/REQUIREMENTS.md\": $onFeedback\n \"D .gtd/REVIEW_RAW.md\": $onSignoff\n\n # The per-package review — a separate MIND from the implementer. ▸ planner\n # identity; fixing findings is a coder action (packages.item.fix-spec).\n specReview:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.specReviewerPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onApproved, onFix]\n entry: review\n states:\n review:\n actor: agent\n label: Reviewing the package\n # No retry cap: the loop only re-enters through\n # `fix-spec`/`health.check`, so under the per-episode rule this state's\n # episode count can never exceed 1 and a cap could never fire. The\n # spec-review loop is genuinely unbounded — see docs/configuration.md's\n # `retry` documentation.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are an autonomous coding agent reviewing a freshly-built work package\n against its own spec.\n\n <%~ it.vars.stateFileRules %>\n\n The only state file this turn touches is\n `.gtd/SPEC_FEEDBACK.md`; write it only when — and only\n when — you find problems. Do not create other files to hold notes or\n output.\n\n The package spec is: <%~ it.read(\".gtd/NEXT.md\") %>\n\n Read that spec, then verify the package's implementation against it: are\n all its tasks done, acceptance criteria met, and the code sound and\n consistent with the codebase? This prompt carries no diff — read it\n yourself: the range runs from `<%= it.startCommit %>` to the working\n tree.\n\n Note the range is process-wide (from the start of the whole build, not\n just this package), so it can span earlier packages too.\n\n - If it fully satisfies the spec, write nothing and finish your turn — a\n clean turn is your approval.\n - If it does not, write `.gtd/SPEC_FEEDBACK.md` listing the\n concrete problems for a fix turn to address. Be specific.\n\n Do not fix anything yourself and do not delete the package file — a later\n step owns that. Finish your turn.\n on:\n \"A .gtd/SPEC_FEEDBACK.md\": $onFix\n \"M .gtd/SPEC_FEEDBACK.md\": $onFix\n \"D .gtd/SPEC_FEEDBACK.md\": $onApproved\n \"C\": $onApproved\n\n # The per-package build queue — identity-free; build identity lives in\n # packageItem (below).\n packageLoop:\n params: [onDrained]\n entry: picking\n states:\n picking:\n actor: check\n label: Picking the next package\n script: |\n #!/usr/bin/env sh\n # Mechanics only — NEXT.md's presence/absence is interpreted by\n # the `on` rows below, never here.\n set +e\n mkdir -p .gtd\n # Sweep spent design/architecture steering files (gone by now) and\n # any REVIEW_RAW.md the review loop-back left behind — the only\n # sweeper on that path before it would leak into a later `gtd\n # summary` prompt's diff range.\n rm -f .gtd/REQUIREMENTS.md .gtd/ARCHITECTURE.md .gtd/QUESTIONS.md .gtd/REVIEW_RAW.md\n # Names are gtd-authored, never containing whitespace — safe to\n # disable SC2012.\n # shellcheck disable=SC2012\n next=$(ls .gtd/packages/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n printf '%s' \"$next\" > .gtd/NEXT.md\n else\n rm -f .gtd/NEXT.md\n fi\n on:\n \"D .gtd/NEXT.md\": $onDrained\n \"* .gtd/NEXT.md\": item.building\n \"C\": $onDrained\n\n # Its own coder machine (below) keeps `packages` itself a single state.\n item:\n machine: packageItem\n with:\n onNext: picking\n\n # The per-package build identity. ▸ coder identity. packages.item.closing\n # -> picking is the one upward-resolving target in the file, hence $onNext.\n packageItem:\n model: <%= it.vars.coderModel %>\n system: |-\n <%~ it.vars.builderPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onNext]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n The only state file this\n turn may write is `.gtd/SATISFIED.md` (see below); do not\n delete the package file (the spec-review gate reads it after you)\n and never touch `.gtd/NEXT.md` — the `picking` state owns\n it.\n\n The package to implement is: <%~ it.read(\".gtd/NEXT.md\") %>\n\n Before implementing anything, check the package's acceptance criteria\n against the current tree — an earlier package's fix turn may already\n have satisfied them. If **every** criterion is already met, implement\n nothing: write `.gtd/SATISFIED.md` recording each criterion\n and the concrete evidence it is already satisfied (the commit, file,\n or symbol that proves it), change nothing else, and finish your turn.\n If any criterion is outstanding, implement the package normally and\n do not write that file.\n\n That file describes a set of independent tasks. Implement all of them in\n this one turn, fanning the independent tasks out to parallel subagents\n where your harness supports it — each task is self-contained and can be\n built concurrently. Implement exactly the tasks this package describes — no\n more, no less; leave every other package file untouched. Use TDD\n discipline within each task.\n\n Leave the package file in place and everything else uncommitted, then\n finish your turn.\n on:\n \"A .gtd/SATISFIED.md\": &satisfied\n to: health.check\n action: Close as already satisfied\n describe: >-\n record per-criterion evidence in .gtd/SATISFIED.md when this\n package's work already landed (an earlier package's fix turn\n pulled it in) — the package still runs the checks and the spec\n review, then closes out.\n \"M .gtd/SATISFIED.md\": *satisfied\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.fixFeedbackPrompt %>\n\n If the\n only way to green the suite is to implement work that another\n package file describes, make the smallest change that gets there and\n do it — that later package will then legitimately report itself\n already satisfied when its turn comes.\n\n Leave everything uncommitted and finish your turn — do not commit.\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n fix-spec:\n actor: agent\n label: Fixing review feedback\n file: SPEC_FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n The only state file this\n turn touches is `.gtd/SPEC_FEEDBACK.md`; address it, then\n delete it. Do not create other files to hold notes or output.\n\n Read `.gtd/SPEC_FEEDBACK.md` (the reviewer's concerns about the\n current package) and the package spec (`<%= it.read(\".gtd/NEXT.md\") %>`),\n then fix the code to resolve every concern. Keep the change focused.\n\n Delete `.gtd/SPEC_FEEDBACK.md` once every concern is addressed.\n Leave everything else uncommitted and finish your turn.\n on:\n \"* **\": health.check\n\n closing:\n actor: check\n label: Closing out the package\n script: |\n #!/usr/bin/env sh\n # Removes the just-reviewed package file (path in NEXT.md) plus\n # leftover spec feedback/evidence, so picking selects the next.\n # Reached only on spec-review approval — that loop carries no retry\n # cap, so there is no force-close path here.\n set +e\n pkg=$(cat .gtd/NEXT.md 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f .gtd/SPEC_FEEDBACK.md .gtd/NEXT.md .gtd/SATISFIED.md\n on:\n \"* **\": $onNext\n # Nothing left to sweep (an already-clean NEXT.md/package) still\n # needs to proceed to the next package, not stall here.\n \"C\": $onNext\n\n health:\n machine: healthGate\n with:\n onGreen: spec\n onRed: fix-suite\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**packages.item.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n spec:\n machine: specReview\n with:\n onApproved: closing\n onFix: fix-spec\n\n # The process's build identity. ▸ coder identity — model/system stamped\n # onto fix.\n buildTail:\n model: <%= it.vars.coderModel %>\n system: |-\n <%~ it.vars.finisherPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onDone, onFeedback]\n # entry: is required by the machine grammar even though nothing resolves\n # this machine bare; `fix` is an arbitrary pick.\n entry: fix\n states:\n fix:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.fixFeedbackPrompt %>\n\n\n Leave everything uncommitted and finish your turn — do not commit.\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n health:\n machine: healthGate\n with:\n onGreen: review\n onRed: fix\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**build.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # Nested here (not root) so the sign-off's boundary commit stays inside\n # the builder's own scope — see humanReview above.\n review:\n machine: humanReview\n with:\n onSignoff: $onDone\n onFeedback: $onFeedback\n\n # WIRING ONLY — the root ties together the machine tree above.\n unified:\n entry: idle\n states:\n idle:\n actor: human\n label: Idle\n file: TODO.md\n message: |\n No active gtd process.\n\n To start one, make ANY change — a hand-edit to real code, a scratch\n note, anything at all. .gtd/TODO.md is a good default\n place to start sketching. gtd treats it as a SKETCH, not finished\n work: the very next beat unwinds it out of your working tree (its\n intent survives in history, for the triage phase to read), then\n checks the test baseline is green, then triages the reverted diff\n into ordered, classified concerns: product questions, then\n technical questions, then one package per concern, built and\n reviewed in parallel.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: unwind\n action: Start\n describe: >-\n make any change — gtd unwinds it out of your working tree (its\n intent survives in history) before checking the test baseline is\n green, then triages the reverted diff into ordered, classified\n concerns and resolves product questions, then technical ones,\n before building each concern's package (**unwind** ->\n **start-gate.check**).\n\n # --entry fix-precheck: a red suite drops into build.fix (-> health ->\n # review tail); a green suite is a no-op back to idle.\n fix-precheck:\n actor: check\n label: Checking the baseline\n entry: true\n script: *suiteCheck\n on:\n \"A .gtd/FEEDBACK.md\": build.fix\n \"M .gtd/FEEDBACK.md\": build.fix\n \"D .gtd/REVIEW_RAW.md\": idle\n \"D .gtd/FEEDBACK.md\": idle\n \"C\": idle\n\n # Separate from start-gate.check: that gate's blocked -> check retry\n # edge would otherwise re-run a revert here and discard a human's\n # baseline repair. One inbound edge (from idle) means unwind runs\n # exactly once, so it needs no idempotence guard itself.\n unwind:\n actor: check\n label: Unwinding your input\n script: |\n #!/usr/bin/env sh\n set +e\n mkdir -p .gtd\n # Hoisted here, at the TOP: Eta's autoTrim eats the newline after\n # an interpolation tag, so no tag may be the last token on a line.\n # Uses it.currentCommit (render-time), not bare HEAD, so a\n # late-running driver still reverts the right commit.\n commit=\"<%~ it.currentCommit %>\"\n git revert --no-commit \"$commit\" 2> .gtd/.unwind-error\n code=$?\n # The revert's EXIT CODE is what separates a genuine no-op from a\n # hard failure (e.g. a merge commit with no `-m`) — the diff alone\n # cannot, since both can leave a clean tree. Turning the failure\n # into a FEEDBACK.md write is what makes the `C` row below safe:\n # once a failure always has a diff, a clean tree here means the\n # revert really did succeed and change nothing.\n if [ \"$code\" -ne 0 ]; then\n printf 'gtd could not unwind %s out of your working tree.\\n\\n' \"$commit\" > .gtd/FEEDBACK.md\n if [ -s .gtd/.unwind-error ]; then\n cat .gtd/.unwind-error >> .gtd/FEEDBACK.md\n else\n printf '`git revert --no-commit` exited %s and produced no output.\\n' \"$code\" >> .gtd/FEEDBACK.md\n fi\n fi\n rm -f .gtd/.unwind-error\n on:\n \"A .gtd/FEEDBACK.md\": unwind-failed\n \"M .gtd/FEEDBACK.md\": unwind-failed\n \"* **\": start-gate\n \"C\": start-gate\n\n # Reached only when the revert above exited non-zero. A human repairs\n # the tree by hand; start-gate.check clears FEEDBACK.md on its way\n # green, so nothing has to sweep it here.\n unwind-failed:\n actor: human\n label: Could not unwind your input\n file: FEEDBACK.md\n message: |\n gtd could not revert your sketch out of the working tree.\n `.gtd/FEEDBACK.md` holds the error.\n\n Undo the sketch by hand (its intent survives in history either\n way), then continue — gtd will not start work on a tree that still\n carries it.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: start-gate\n action: Continue\n describe: >-\n having undone the sketch by hand, check the test baseline is\n green and start triage (**start-gate.check**).\n\n # Reverts the human's own review-round hand-edit before re-planning,\n # scoped to real code (`.gtd/` excluded — REVIEW.md is already gone by\n # now). Names the commit via it.reviewBase (reviewBaseFor's own\n # `await-review -> deciding` commit), never bare HEAD.\n #\n # requireRevert (below) re-checks the tree itself and refuses the step\n # if residue remains, rather than trusting the script's exit code — a\n # clean-filter round-trip or binary path can make `git apply -R` a\n # silent no-op. Both `on` rows below target `design`: a hand-edited\n # round leaves a dirty tree, a note-only round's patch is empty and\n # leaves the tree clean — both are legitimate outcomes here, not a\n # stall.\n #\n # `file: REVIEW.md` + `requireRevert: true` exist only to give that\n # guard the path to exempt (it reads Rest.hints.file, never this\n # literal) — REVIEW.md is already gone two commits earlier, so this\n # state never renders it.\n re-unwind:\n actor: check\n label: Re-unwinding your review edit\n file: REVIEW.md\n requireRevert: true\n script: |\n #!/usr/bin/env sh\n # Scoped revert of the human's review-round edit — .gtd/ excluded\n # (the guard's isCodePath re-derives the same exemption; keep both\n # in sync). Expected to succeed; requireRevert catches a silent\n # apply failure.\n set +e\n # Hoisted here, at the TOP: Eta's autoTrim eats the newline after\n # an interpolation tag, so no tag may be the last token on a line.\n commit=\"<%~ it.reviewBase %>\"\n patch=.gtd/.re-unwind.patch\n mkdir -p .gtd\n git diff --binary \"$commit^\" \"$commit\" -- . \":(exclude).gtd\" > \"$patch\"\n if [ -s \"$patch\" ]; then\n git apply -R \"$patch\" || echo \"re-unwind: could not revert $commit\" >&2\n fi\n rm -f \"$patch\"\n on:\n \"* **\": design # a code hand-edit was reverted\n \"C\": design # note-only round: nothing to revert\n\n start-gate:\n machine: entryGate\n with:\n onGreen: design\n reviewBase: \"\"\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `.gtd/FEEDBACK.md` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**start-gate.check**). To repair the baseline as its own separate\n reviewed commit instead, abandon this start and run\n `gtd --entry fix-precheck` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n review-gate:\n machine: entryGate\n with:\n onGreen: build.review\n reviewBase: <%= it.vars.reviewBase %>\n blockedMessage: |\n The test baseline is red — gtd will not start a review on a broken suite.\n `.gtd/FEEDBACK.md` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**review-gate.check**).\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n design:\n machine: designPlan\n with:\n onArchitecture: architecture\n\n architecture:\n machine: archPlan\n with:\n onPackages: packages\n\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n build:\n machine: buildTail\n with:\n onDone: idle\n onFeedback: re-unwind\n";
192267
+ var unified_default = "# gtd's BUILT-IN DEFAULT workflow — used when no `workflow:` key is\n# configured anywhere in the cwd→home config chain; `gtd init` seeds only\n# vars.testCommand + a modes: suggestion, never this file. Compiled through\n# the same `compileWorkflowConfig` a user's own `workflow:` goes through — no\n# privileged path. See skills/authoring/SKILL.md for how to read or edit it\n# (machines, patterns, templates) and docs/driver.md for how a driver runs it.\n#\n# One flow: any change to the tree starts the process. `idle` -> `unwind`\n# reverts that diff (intent survives only in history) -> a green-baseline\n# gate -> `design.triage` groups the reverted diff into ordered, classified\n# concerns and resolves the PRODUCT ones' open questions (`design.gate`) ->\n# `architecture.author` works out the *how* and resolves the TECHNICAL ones\n# (`architecture.gate`) -> `architecture.decompose` writes one package file\n# per concern -> the per-package queue (`packages.*`) builds and reviews each\n# -> the shared tail (`build.review.*`) decides sign-off (-> `idle`, landing\n# an ordinary commit that closes the process — every per-turn commit stays on\n# the branch; run `gtd summary` afterward for a closing-message prompt) vs.\n# actionable feedback (-> `re-unwind` -> back to `design.triage`, a full\n# re-plan lap that never builds on the human's hand-edit). `--entry\n# review-gate.check --var reviewBase=<commitish>` and `--entry fix-precheck`\n# are the two other entries into this same flow.\nvars:\n testCommand: npm test\n # plannerModel: the heavier tier for one-shot triage/design/review turns.\n # coderModel: the tier for build/fix turns. Both repointable via a\n # top-level `vars:` key or a `GTD_<NAME>` env var, like any other var.\n plannerModel: smart\n coderModel: base\n # Declared (not left undeclared) so `--var reviewBase=...` passes the CLI's\n # declared-name check; blank renders empty, which the review entry refuses.\n reviewBase: \"\"\n # gtd's own voice for generated files — adapted from the \"Spartan\" output\n # style (https://github.com/alexgreensh/attention-span, AGPL-3.0, version 0.6),\n # rewritten in gtd's own words for deliverables rather than chat replies;\n # no upstream text ships. A point-in-time derivation with no refresh\n # mechanism.\n styleBlock: |-\n - A deliverable, not a chat reply — size follows the work; cut padding\n - Lead with the answer; never circle back to restate it\n - Flat, commanding sentences — commit to the claim, never hedge\n - Everyday words; define an unavoidable term in five words or fewer, on\n first use\n - Bold carries the load: bold the claim, not the sentence around it —\n the bold text alone must yield the full point and every risk\n - **Never trim a risk, a number, a threshold, or a scoped condition to\n save space — this outranks every other rule here**\n - Ship the artifact bare — no lead-in, no sign-off\n - Compressing is not dropping: three load-bearing parts ship as three,\n each shorter, never as two\n - One idea per block; break when the idea shifts\n - Flag risk in one blunt line, never hedged prose; never narrate — do it\n styleFormatContract: |-\n - Machine-read: its format contract outranks every style rule above —\n keep every `##`/`###` heading, checkbox row, and marker line exactly\n as specified. Never renumber or rename a heading; a parser reads\n these literally and a violation refuses the turn\n - Voice rules above govern only the prose between these elements\n\n # One persona per prompt-bearing machine below, stamped as its `system:`\n # (see src/StateFields.ts's `system`). `--system-prompt` REPLACES a\n # harness's default system prompt — including its injected cwd/env/\n # git-status block — so each persona is written self-contained, restating\n # that context explicitly rather than relying on it.\n\n # Shared conduct tail, appended after every persona's own role paragraph:\n # use tools without asking, orient yourself with git, and go inspect what\n # the turn's message names. Deliberately NOT named `*Persona`:\n # `templates.test.ts` derives its six-name persona set from\n # `it\\.vars\\.(\\w+Persona)`, so that suffix would silently grow the set.\n agentConduct: |-\n - You have shell and file tools — bash, read, write, edit — use them\n without asking first; this runs unattended, no one grants permission\n - Investigate with real commands rather than assuming a file's, a\n commit's, or a decision's status — then act on what you find\n - No injected status block — no cwd, no branch, no history, no tree\n state — orient yourself first with `git status`, `git log`, `ls`\n - The turn's message names a commit, a range, or a file to go inspect\n yourself — never a diff or summary inlined into the conversation\n - Across turns one conversation may span, work from what you already\n read and settled, unless told this is a fresh, cold pass\n designPersona: |-\n You are the product-facing planning voice in gtd's build pipeline: turn\n a raw sketch — a hand-edit, a scratch note, or both — into an ordered,\n classified list of concerns, and hold the running product conversation\n with the human it depends on. You are read by that human — write\n plainly — not by a parser, except where a state says otherwise.\n architectPersona: |-\n You are the technical planning voice in gtd's build pipeline: take over\n once product concerns are settled, reading them cold, no carried\n conversation. Work out the *how* per concern — structure, data models,\n tech-stack choices, error handling — raise the technical open\n questions, then write one package spec per concern with no further\n judgement call: the grouping is already decided.\n reviewerPersona: |-\n You are the independent reviewing mind in gtd's build pipeline —\n deliberately separate from whoever wrote the code, with no attachment\n to it. Two turns: write a structured review document grouping a diff\n into chunks; later, classify a round of the human's feedback as\n actionable or just approving, never fixing anything yourself. You judge\n and classify; you never build.\n specReviewerPersona: |-\n You are the adversarial spec-conformance checker in gtd's build\n pipeline, checking one freshly-built package against its spec. Verify\n only: tasks done, criteria met, code sound and consistent with the\n codebase. Write feedback only when something is genuinely wrong —\n otherwise write nothing; a clean turn IS the approval. Never fix what\n you find — naming it precisely enough for a fix turn is the whole job.\n builderPersona: |-\n You are the TDD implementer in gtd's build pipeline: build one\n package's declared scope end to end, tests first, and come back to fix\n things when redirected — a failing check, or reviewer feedback. Stay\n strictly inside the package in front of you; never touch another\n package's files or code outside the task. Treat feedback as the\n diagnosis to act on; discard it, with reason, only when simply wrong.\n finisherPersona: |-\n You are the closing identity in gtd's build pipeline — the last coder\n before it closes. Fix a late-breaking failing check after sign-off,\n focused and minimal. Every turn lands its own commit — nothing here\n gets squashed away.\n\n # The scratchpad-opener sentence shared by every prompt-content state (see\n # STATE_FIELDS's `prompt`) — this workflow's own state files are its private\n # working notes, never project code or documentation a human reads. Injected\n # as the leading line of every prompt body that touches a `.gtd/` file\n # directly; each state's own \"the only state file this turn touches is X\"\n # sentence stays local, immediately after the tag. Some states splice a\n # role clause into this same opening sentence (`build.review.collecting`,\n # `packages.item.spec.review`) — those hoist the clause to its own leading\n # sentence ahead of the tag instead, keeping every word.\n stateFileRules: |-\n - You are an autonomous coding agent\n - This workflow's own state files are its private scratchpad, never\n project code or documentation\n # The open-questions warrant test, decide-it-yourself sink, and `## Open\n # Questions` checkbox shape shared by `design.triage` and\n # `architecture.author` — written phase-agnostic (no PRODUCT/TECHNICAL\n # bake-in) so both sites can inject it verbatim. Each site's own\n # phase-scope sentence (triage: product-only, technical waits; architecture:\n # every point here is TECHNICAL) stays local around the tag — the two\n # genuinely disagree on scope, so that sentence is never shared.\n # `questionBarReturn` is the return-lap half — split out so each site can\n # inject it under its own `## Return lap` heading instead of burying\n # return-lap behaviour inside the first lap's own section.\n questionBar: |-\n - Ask only when all three hold: it's a genuine fork with divergent\n outcomes (user-visible for product, materially different builds for\n technical); the diff and history don't already settle it (treat an\n explicit `.gtd/TODO.md` directive, a committed hand-edit, or — on a\n loop-back lap — the human's own review-round edit/note as settled);\n and getting it wrong would survive to the human review tail\n - Below that bar, decide it yourself: record it under\n `## Answered Questions` as `### <the point phrased as a question>`\n plus a one-line rationale in prose, no checkboxes. That heading\n always comes last, after every other `##` section\n - Above the bar, write it under `## Open Questions` — always the first\n `##` section — as `### <question>` plus a checkbox list: two\n concrete options and a free-text slot, all unticked:\n\n ### <the question>\n\n - [ ] <first option — a concrete answer, a few words of rationale>\n - [ ] <second option>\n - [ ] _your answer_\n\n - Never tick a box yourself — the human ticks exactly one per question\n questionBarReturn: |-\n - The human ticked one option per question (or wrote their own in\n place of `_your answer_`): fold each answer into its concern's\n prose, then move the question into `## Answered Questions` as\n `### <question>` with the resolved answer in plain prose — no\n checkboxes\n - `## Answered Questions` is always the last `##` section — a moved\n question lands there, never wherever `## Open Questions` used to sit\n - Never re-raise a deleted question — treat a deleted `## Open\n Questions` section as acceptance; omit it entirely once none are left\n # The body `packages.item.fix-suite` and `build.fix` share byte for byte —\n # both fix a red `.gtd/FEEDBACK.md`. `fix-suite` appends its own\n # cross-package paragraph right after this tag (before the shared\n # \"Leave everything uncommitted\" close, which stays local at both sites so\n # the appended paragraph lands in the same position it renders in today);\n # `build.fix` renders the tag alone.\n fixFeedbackPrompt: |-\n - The only state file this turn touches is `.gtd/FEEDBACK.md` — no\n other files for notes or output\n - Read it (the failing test output) and fix the code so the suite\n passes; keep the change focused, never refactor unrelated code\n - If the feedback is wrong, fix nothing rather than \"fixing\" a non-issue\n - Delete `.gtd/FEEDBACK.md` either way — once the suite passes, or once\n you have established the feedback was wrong; leaving it in place is\n never the right end state, the next check writes its own\n\n # The human-facing half of footnotes — how to type one, injected into the\n # three human-gate messages (`design.gate.answer`, `architecture.gate.answer`,\n # `build.review.await-review`). `footnoteFoldIn` below is the agent-facing\n # half; the two never share text, different audience and content.\n footnoteRules: |-\n - Leave a footnote anywhere: mark the exact spot with `[^name]` (any\n name, no whitespace or `]`), then define it below as `[^name]:\n explain what you mean` — indent a longer comment's continuation\n lines. (The literal words \"your comment\" are this format's seeded\n placeholder body — a definition still holding them exactly is\n flagged as unfilled, so write your own words there)\n - A footnote is a comment on that exact spot — the hunk, line, or\n paragraph it marks — never a whole-file remark\n - `name` is yours to pick; only a definition's name must be unique in\n this document — the same name may mark more than one spot\n # The agent-facing half — injected into the three prompts that fold a\n # footnote in (`design.triage`, `architecture.author`,\n # `build.review.collecting`). `build.review.reviewing`, the one state that\n # WRITES a review file, references neither tag — the agent never authors a\n # footnote.\n footnoteFoldIn: |-\n - A footnote (`[^name]` plus its `[^name]:` definition) is a comment on\n its exact anchor — fold it in as a mandatory concern described\n against that anchor's hunk or paragraph, never flattened into a\n whole-file remark\n - DELETE it in this same turn — marker and definition together — the\n way a transient hand-written code comment is already treated. Never\n re-read a footnote already acted on\n - A footnote is human input only — you reply in prose; never write one\n yourself\n\n# `gtd summary`'s prompt — printed cold, no session identity, no diff inlined.\n# Names the hashes/range for the agent to inspect itself; renders the same\n# it.processCost/it.processCostByModel the old squash finale used to.\nsummary: |\n <%~ it.vars.styleBlock %>\n\n\n - Write the closing message for the process HEAD closes or sits inside —\n for a squash, an amend, or a PR body. Starting cold: read every\n decision out of the commits below, not assumed context\n - Cover the motivation, the decisions, the trade-offs, and the\n high-level architectural changes — never which files changed; `git\n diff --stat` is for that, not this message\n\n The process's entry commit is `<%= it.entryCommit %>`. <% if\n (it.humanCommits.length > 0) { %>The human contributed at these commits,\n oldest to newest:\n <% it.humanCommits.forEach(function (c) { %>\n - `<%= c.hash %>` (entering `<%= c.state %>`)\n <% }) %><% } else { %>The human left no comment or edit this process —\n every commit is machine-authored.<% } %>\n\n Inspect the range: `git log <%= it.processBase %>..<%= it.processTip %>`\n and `git diff <%= it.processBase %> <%= it.processTip %>` — exactly what\n a squash or PR body should describe.\n\n Token cost: <%= it.processCost %>\n <% it.processCostByModel.forEach(function(m){ %>\n - <%= m.model %>: <%= m.cost %>\n <% }) %>\n\n Print the closing message and stop — this writes nothing itself.\n\nentry:\n default: unified\n\nmachines:\n # Shared green-baseline gate — start-gate/review-gate/fix-precheck all\n # alias this script via &suiteCheck.\n entryGate:\n params: [onGreen, blockedMessage, blockedDescribe, checkLabel, blockedLabel, reviewBase]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: &suiteCheck |\n #!/usr/bin/env sh\n set +e\n mkdir -p .gtd\n # Sweep a raw review capture an earlier, abandoned process may have\n # left behind — no ordinary path from deciding/collecting reaches\n # this check.\n rm -f .gtd/REVIEW_RAW.md\n <%~ it.vars.testCommand %> > .gtd/.check-output 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n if [ -s .gtd/.check-output ]; then\n mv .gtd/.check-output .gtd/FEEDBACK.md\n else\n rm -f .gtd/.check-output\n printf 'the test command failed with exit code %s and produced no output.' \"$code\" > .gtd/FEEDBACK.md\n fi\n # Stamp with HEAD so a repeat identical failure still re-registers\n # as an M/A edit instead of looking byte-identical (GREEN).\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> .gtd/FEEDBACK.md\n else\n rm -f .gtd/.check-output\n rm -f .gtd/FEEDBACK.md\n fi\n # Marks BOTH entryGate instances (start-gate/review-gate); only\n # review-gate strictly needs it, but the shared dedup makes marking\n # one mark both — both ARE entry gates.\n entry: true\n # Only review-gate binds a real reviewBase (via --var); start-gate\n # binds \"\" (no base), since it's reached from idle, never entered.\n reviewBase: $reviewBase\n on:\n \"A .gtd/FEEDBACK.md\": blocked\n \"M .gtd/FEEDBACK.md\": blocked\n \"D .gtd/REVIEW_RAW.md\": $onGreen\n \"D .gtd/FEEDBACK.md\": $onGreen\n \"C\": $onGreen\n blocked:\n actor: human\n label: $blockedLabel\n file: FEEDBACK.md\n message: $blockedMessage\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $blockedDescribe\n\n # Shared check/escalate pair for build.health.*/packages.item.health.* —\n # the caller's own coder fix-state ($onRed), not this machine, handles red.\n healthGate:\n params: [onGreen, onRed, escalateDescribe, checkLabel, escalateLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n # See the shared suite check on `entryGate.check` above.\n script: *suiteCheck\n on:\n \"A .gtd/FEEDBACK.md\": $onRed\n \"M .gtd/FEEDBACK.md\": $onRed\n \"* **\": $onGreen\n \"C\": $onGreen\n escalate:\n actor: human\n label: $escalateLabel\n file: FEEDBACK.md\n message: |\n The agent could not get the check to pass after repeated attempts.\n `.gtd/FEEDBACK.md` holds the last failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: check\n action: Retry check\n describe: $escalateDescribe\n\n # Shared check/answer pair for design.gate/architecture.gate. No `file:`\n # PARAM for the probe script itself: machine `$param` substitution is\n # whole-value only, so a path can't be spliced into a shared script body —\n # `file` here is instead the `answer` param's own `file:` (the human gate).\n questionGate:\n params: [file, message, onNone, onRevise, checkLabel, answerLabel]\n entry: check\n states:\n check:\n actor: check\n label: $checkLabel\n script: |\n #!/usr/bin/env sh\n # Exactly one of REQUIREMENTS.md/ARCHITECTURE.md exists on disk at\n # a time — architecture.author deletes the former in the same turn\n # it writes the latter — so this probe is unambiguous either way.\n set +e\n mkdir -p .gtd\n file=.gtd/REQUIREMENTS.md\n [ -f \"$file\" ] || file=.gtd/ARCHITECTURE.md\n gtd check qa \"$file\" --open-questions > /dev/null 2>&1\n code=$?\n if [ \"$code\" -ne 0 ]; then\n printf 'open questions remain in %s\\n' \"$file\" > .gtd/QUESTIONS.md\n # See the shared suite check's cache-buster rationale on\n # `entryGate.check` above.\n printf '\\n<!-- gtd check %s -->\\n' \"$(git rev-parse --short HEAD 2>/dev/null || echo pending)\" >> .gtd/QUESTIONS.md\n else\n rm -f .gtd/QUESTIONS.md\n fi\n on:\n \"A .gtd/QUESTIONS.md\": answer\n \"M .gtd/QUESTIONS.md\": answer\n \"D .gtd/QUESTIONS.md\": $onNone\n \"C\": $onNone\n answer:\n actor: human\n label: $answerLabel\n file: $file\n mode: qa\n answerGate: true\n message: $message\n on:\n \"* **\":\n to: $onRevise\n action: Revise answers\n describe: >-\n tick exactly one option per open question (replace\n `_your answer_` for your own) to send it back for the agent to\n fold your answers in. Delete a question to skip it, or delete\n the whole `## Open Questions` section to accept the plan as-is.\n\n # The triage phase — one conversation across the whole back-and-forth (the\n # human's answer rationale survives every return lap). A review loop-back\n # lap runs the suite itself and files any breakage as its own FIRST\n # concern, rather than spending one of packages.item.fix-suite's\n # (per-episode) retries on breakage the package didn't cause.\n # ▸ planner identity.\n designPlan:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.designPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onArchitecture]\n entry: triage\n states:\n triage:\n actor: agent\n label: Triaging the change\n file: REQUIREMENTS.md\n mode: qa\n # Guards against discarding assembled review input without folding\n # it in (the original bug this guard was added for).\n requireProgress: true\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.footnoteFoldIn %>\n\n - The only state file this turn touches is `.gtd/REQUIREMENTS.md`\n — no other files for notes or output\n - `.gtd/TODO.md` is the likely home of the sketch that started\n this process — the human's input, folded into the concerns\n below like any other part of the start diff; never a state\n file to preserve, never gtd bookkeeping to ignore\n - This process started at commit `<%= it.startCommit %>`,\n reverted out of the tree by `unwind` right after landing, so\n `git diff <%= it.startCommit %>` is now empty — its content\n survives only in history. Find the entry commit yourself:\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\n (the process's first turn, before any baseline-repair\n commits), then `git show` it — a hand-edit, a scratch note,\n or both\n - Whichever lap this is, leave `.gtd/REQUIREMENTS.md`\n uncommitted and finish once it reflects this lap's own work\n\n ## First lap\n\n `.gtd/REQUIREMENTS.md` does not exist yet (or holds whatever the\n human's change left there). Build it from that start diff:\n\n ### Group into concerns\n\n - An ordered list of concerns, each a coherent piece of work\n that leaves the suite green on its own, in build order — a\n concern that would red-light a later fix is not a valid\n split; merge the two\n - Vertical, not horizontal: slice by capability, never by layer\n — a concern that only prepares for a later one (types,\n scaffolding, renames) folds into the first concern that uses it\n - Distinct acceptance: each concern needs a check that fails\n before it and passes after; if you can't name one, merge it into\n its neighbour\n - Prefer fewer, larger packages — the smallest independently\n valuable change, not the smallest change that compiles\n\n ### Classify each concern\n\n Classify each as PRODUCT (user-facing/requirements) or TECHNICAL\n (implementation).\n\n <%~ it.vars.questionBar %>\n\n\n Raise questions only for product concerns here — technical ones\n wait for the next phase. When every concern is TECHNICAL, write\n no `## Open Questions` section.\n\n ### Fold in the sketch\n\n - Fold everything the entry commit added under the concern it\n belongs to — a scratch note and a real code edit are both\n just a sketch to finish, never work to preserve; `unwind`\n already reverted both, so there is nothing left to delete\n\n ## Return lap\n\n <%~ it.vars.questionBarReturn %>\n\n ## Review loop-back\n\n - `.gtd/REQUIREMENTS.md` already holds concerns with no\n ticked-but-unfolded answer waiting — a completed REVIEW round\n put this here, not a question you asked. Develop those\n concerns further with whatever the round raised; never\n rediscover or regroup them cold, the way the first lap does\n - The human's review-round edit was reverted the same way the\n entry commit was — read it from history:\n `git show <%= it.reviewBase %>`. (On the first lap that hash\n is the process's own diff base, `<%= it.startCommit %>` — this\n branch doesn't apply)\n - Every decision under `## Answered Questions` stays settled —\n never re-open one. A genuinely open PRODUCT point may still\n raise a fresh `## Open Questions` entry: the product gate sits\n on this path exactly as on the first lap\n - Before grouping anything, run `<%= it.vars.testCommand %>`\n yourself — a loop-back runs no green-baseline gate, so the\n tree may already be red (reverting the edit can undo a fix it\n made). If red, make that breakage the first concern, ahead of\n everything else — every later concern's green-on-its-own\n property assumes the suite started green\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: REQUIREMENTS.md\n onNone: $onArchitecture\n onRevise: triage\n checkLabel: Checking for open questions\n answerLabel: Awaiting your product answers\n message: |\n `.gtd/REQUIREMENTS.md` holds the concerns under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n You can also leave a footnote alongside an answer — it never\n substitutes for ticking a box, which is still required before\n stepping is allowed:\n\n <%~ it.vars.footnoteRules %>\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n # A separate machine from designPlan (its own memory scope — a COLD read,\n # not a resumed conversation). Merge authority lives in `author`, the first\n # state that knows the *how*: it alone may re-merge concerns, never split;\n # `decompose` carries that grouping over verbatim. ▸ planner identity.\n archPlan:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.architectPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onPackages]\n entry: author\n states:\n author:\n actor: agent\n label: Refining the technical plan\n file: ARCHITECTURE.md\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.footnoteFoldIn %>\n\n - The only state files this turn touches are\n `.gtd/ARCHITECTURE.md` (write it) and `.gtd/REQUIREMENTS.md`\n (delete once folded in) — no other files for notes or output\n - You do not resume the design conversation — a separate\n machine, its own memory, a cold read every time. Read\n `.gtd/REQUIREMENTS.md` (the settled, ordered, classified\n concerns) in full; treat every decision there, PRODUCT or\n TECHNICAL alike, as settled — never re-open it\n - Cold means no memory of triage's own back-and-forth, not no\n git access. This process started at commit\n `<%= it.startCommit %>`. Find the first turn yourself: run\n `git rev-list --ancestry-path <%= it.startCommit %>..HEAD | tail -1`\n then `git show` it to see what started this process — a\n hand-edit, a scratch note, or both\n - Once `.gtd/ARCHITECTURE.md` is written, delete\n `.gtd/REQUIREMENTS.md` — folded in, it must not linger. Leave\n everything uncommitted and finish\n\n ## First lap\n\n - Develop `.gtd/ARCHITECTURE.md` from those concerns: for each,\n in order, work out the *how* — structure, data models,\n tech-stack choices, error handling — building on the settled\n *what*\n - You now know the *how*, so you know each concern's file footprint\n — list each concern's primary paths. Merge concerns whose\n footprints center on the same files into one, unless the later\n one only consumes an interface the earlier one creates (keeps a\n build-on-top sequence from collapsing into one blob). This\n authority is to merge only, never to split — the whole problem\n is over-granularity\n - Record every merge under `## Merged Concerns`,\n carrying both merged requirements verbatim so spec review\n still covers each independently\n - A merge raises no open question and stops for no\n human — do not route it to `architecture.gate` for a veto; the\n human sees it when reviewing the plan, and spec review is the\n real safety net\n - Prefer fewer, larger packages — the smallest independently\n valuable change, not the smallest change that compiles\n - Every open point here is TECHNICAL — triage already resolved\n the product ones, one phase earlier\n\n <%~ it.vars.questionBar %>\n\n ## Return lap\n\n <%~ it.vars.questionBarReturn %>\n on:\n \"* **\": gate.check\n\n gate:\n machine: questionGate\n with:\n file: ARCHITECTURE.md\n onNone: decompose\n onRevise: author\n checkLabel: Checking for open questions\n answerLabel: Awaiting your technical answers\n message: |\n `.gtd/ARCHITECTURE.md` holds the technical plan under\n development. Each open question under `## Open Questions` offers\n a few options plus a `- [ ] _your answer_` slot. Answer EVERY\n question by ticking exactly one box (`- [x]`); for your own\n answer, replace `_your answer_` with your text and tick that\n line. Stepping is refused while any question is unanswered.\n\n You can also leave a footnote alongside an answer — it never\n substitutes for ticking a box, which is still required before\n stepping is allowed:\n\n <%~ it.vars.footnoteRules %>\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n\n decompose:\n actor: agent\n label: Decomposing into packages\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.stateFileRules %>\n\n - The only state files this turn touches are the package files\n under `.gtd/packages/` and `.gtd/ARCHITECTURE.md` (deleted) —\n no other files for notes or output\n - Work from `.gtd/ARCHITECTURE.md` if you wrote it earlier this\n conversation, otherwise read it (the converged technical\n plan). It already lists an ordered set of concerns with every\n merge/split judgement made — this turn is a mechanical\n write-out, not a planning one. A `## Merged Concerns` heading\n there records those merges, never a concern of its own: write\n no package file for it\n - Write one package file per concern, in the settled order,\n under `.gtd/packages/` (e.g. `.gtd/packages/01-name.md`,\n `02-name.md`, ...), each carrying that concern's\n requirement(s) — both, independently, if merged — its\n independent tasks, and each task's acceptance criteria as\n `- [ ]` checkboxes and relevant paths\n - Do not merge or split concerns here — that judgement already\n happened; carry the settled grouping over verbatim. No\n package file may reference any other `.gtd/` file\n - Once written, delete `.gtd/ARCHITECTURE.md`. Leave everything\n uncommitted and finish\n on:\n \"* .gtd/packages/**\": $onPackages\n\n # The shared review tail. ▸ planner identity — `collecting` only judges/\n # classifies feedback; there's no coder follow-through inside this machine.\n #\n # Nested inside buildTail (build.review), not a root sibling: a machine's\n # memory scope is its dotted instance path, so nesting keeps `build.fix ->\n # build.health.check -> build.review.*` (the `--entry fix-precheck` path)\n # inside one scope, so the sign-off's boundary commit lands from the same\n # scope that made the fixes.\n humanReview:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.reviewerPersona %>\n\n\n <%~ it.vars.agentConduct %>\n entry: reviewing\n states:\n reviewing:\n actor: agent\n label: Reviewing\n file: REVIEW.md\n mode: review\n # `gtd --entry review-gate.check` enters via review-gate.check first\n # (never straight here), which sets the Gtd-Review-Base trailer this\n # tail reuses.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n <%~ it.vars.stateFileRules %>\n\n - The only state file this turn touches is `.gtd/REVIEW.md`,\n written at exactly that path — no other files for notes or\n output\n\n Write `.gtd/REVIEW.md` in this exact format, to help a human\n review the changes:\n\n - First non-blank line: `# Review: <%= it.currentCommit.slice(0, 7) %>`\n - Somewhere in the document: `<!-- base: <%= it.reviewBase %> -->`\n - At least one `## <Chunk Title>` heading grouping hunks\n semantically (same feature/refactor/fix, across files), each\n with a short explanation of what changed and why, then one\n pointer per hunk (`./`-relative path, optional `#line`;\n checkboxes are for the human, not you). Put the note's\n opening line right on the pointer's line:\n\n - [ ] ./path/to/file.ts#42 — what this hunk does\n\n Continue a longer note below the pointer, indented exactly two spaces\n — never four or more, which reads as a code block and never\n reflows:\n\n - [ ] ./path/to/file.ts#42 — what this hunk does\n and here is more detail, continued below it\n\n A note sitting entirely on the line(s) beneath the pointer is\n also valid. Either way, the note must never start with a bare `./path` token\n — that parses as a second pointer, not a note\n\n No diff is given — read the changes yourself. The range runs\n from `<%= it.reviewBase %>` to the working tree (committed turns\n plus anything pending); on a feedback round that's the previous\n review's boundary, so it covers only what's new.\n\n Leave `.gtd/REVIEW.md` uncommitted and finish.\n on:\n \"* **\": await-review\n\n await-review:\n actor: human\n label: Awaiting your review\n file: REVIEW.md\n mode: review\n message: |\n `.gtd/REVIEW.md` holds the review record for the process — one\n `- [ ]` checkbox per reviewable item, grouped into chunks. Tick a box\n (`- [x]`) as you review each hunk; ticking only records that you've read\n it, it is not sign-off. Ticks are read-progress only: landing clears\n every box back to `- [ ]` on disk and nothing records which hunks\n you read — there is no persisted trail of it.\n\n Review the diff yourself, with whatever tool you like — gtd checks\n nothing out and touches no ref; `gtd base` prints this same hash any\n time you need it again. The range runs from the review base to the\n working tree:\n\n git diff <%= it.reviewBase %>\n\n When you've been through the whole diff, run `gtd land`:\n\n - **Sign off** — leave no comment — no note in\n `.gtd/REVIEW.md`, no code edit — to close the process,\n whatever the boxes say. Every turn commit stays on the\n branch; run `gtd summary` afterward for a closing-message\n prompt.\n - **Request changes** — leave a comment: a note on a\n `.gtd/REVIEW.md` line, a footnote anchored to a hunk, or a\n direct code edit — to send a FULL development lap\n (**review.deciding** → **review.collecting** →\n re-triage). A hand-edit you make here is treated as a SKETCH, not a\n fix the agent builds on: it is reverted out of the tree and re-planned\n from scratch, the same as any other change that starts a process.\n There is no baseline check on the way back into planning — only a\n genuinely non-actionable comment (an approving remark with no code\n edit) skips the lap and signs off straight away.\n\n A footnote works the same way here as a line note:\n\n <%~ it.vars.footnoteRules %>\n\n Deleting `.gtd/REVIEW.md` is refused.\n on:\n \"* **\": deciding\n\n deciding:\n actor: check\n label: Reviewing\n file: REVIEW.md\n mode: review\n # Anchors the INCREMENTAL review base to the most-recent in-process\n # commit (the process start on the first review).\n reviewBase: true\n script: |\n #!/usr/bin/env sh\n # FEEDBACK iff the human left a REVIEW.md note or hand-edited any\n # file this round outside .gtd/; otherwise a clean sign-off. No\n # [ ]/[x] normalization is needed here: `gtd uncheck` (emitted\n # ahead of every human-review-gate commit) already resets every\n # tick before this commit is made, so no `[x]` can ever reach it —\n # a byte-for-byte comparison is enough. This turn only\n # CAPTURES the raw material into REVIEW_RAW.md — collecting judges\n # actionability. A/M REVIEW_RAW.md rows are declared before D\n # REVIEW.md so a feedback round (which also deletes REVIEW.md)\n # isn't mistaken for sign-off.\n set +e\n mkdir -p .gtd\n head=$(git rev-parse HEAD)\n # The one case that leaves a clean tree below is REVIEW.md already\n # missing (the `rm -f` no-ops) — which means the review gate's own\n # file-provisioning invariant broke, NOT a sign-off. Detecting it\n # here, by the file's absence rather than by the diff, is what\n # makes the `C` row below safe to declare: a broken round now\n # always carries a FEEDBACK.md diff and routes to a human.\n if ! git cat-file -e \"HEAD:.gtd/REVIEW.md\" 2>/dev/null; then\n printf 'there is no `.gtd/REVIEW.md` at %s — nothing was reviewed this round.\\n' \"$head\" > .gtd/FEEDBACK.md\n elif git diff-tree --no-commit-id --name-only -r HEAD -- . \":(exclude).gtd\" \\\n | grep -q . \\\n || [ \"$(git show \"HEAD^:.gtd/REVIEW.md\" 2>/dev/null)\" \\\n != \"$(git show \"HEAD:.gtd/REVIEW.md\" 2>/dev/null)\" ]; then\n # Reached only with REVIEW.md present. FEEDBACK iff the human\n # left a note (any edit — no tick can reach this commit at all)\n # or hand-edited any file this round outside .gtd/; otherwise a\n # clean sign-off. This turn only CAPTURES the raw material into\n # REVIEW_RAW.md — collecting judges actionability.\n {\n echo \"This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.\"\n echo\n echo \"Commit: $head\"\n echo \"The human's notes are in .gtd/REVIEW.md at this commit. Any hand edits are\"\n echo \"in that commit's other paths. Run: git show $head\"\n } > .gtd/REVIEW_RAW.md\n rm -f .gtd/REVIEW.md\n else\n rm -f .gtd/REVIEW.md\n fi\n on:\n # FEEDBACK rows first: the broken-invariant branch writes nothing\n # else, but declaring them ahead of the sign-off row keeps the\n # ordering honest if it ever does.\n \"A .gtd/FEEDBACK.md\": review-missing\n \"M .gtd/FEEDBACK.md\": review-missing\n # A/M REVIEW_RAW.md before D REVIEW.md so a feedback round (which\n # also deletes REVIEW.md) isn't mistaken for sign-off.\n \"A .gtd/REVIEW_RAW.md\": collecting\n \"M .gtd/REVIEW_RAW.md\": collecting\n \"D .gtd/REVIEW.md\": $onSignoff\n # Unreachable now that the missing-REVIEW.md case writes\n # FEEDBACK.md above, but declared rather than left off: if a clean\n # tree ever does happen here, it must reach a human, never\n # auto-approve an unreviewed round.\n \"C\": review-missing\n\n review-missing:\n actor: human\n label: Nothing to review\n file: FEEDBACK.md\n message: |\n The review round committed no `.gtd/REVIEW.md`, so there is nothing\n to sign off on. `.gtd/FEEDBACK.md` holds the detail.\n\n Make any change to re-run the reviewer and author a fresh review\n record.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: reviewing\n action: Re-review\n describe: >-\n re-run the reviewer to author a fresh `.gtd/REVIEW.md`\n (**build.review.reviewing**).\n\n # Outcome is by which paths THIS diff touches: writing REQUIREMENTS.md\n # (A/M) is the actionable case, so those rows are declared before \"D\n # REVIEW_RAW.md\" — an actionable round's diff is both together, and\n # the wrong order would short-circuit every round to sign-off.\n # Consuming REVIEW_RAW.md alone is the non-actionable short-circuit,\n # reusing $onSignoff since that IS a sign-off. No `C`/`\"* **\"` row: a\n # clean turn here is a fruitless dispatch (falls through to the\n # ordinary attempt default), never a verdict; a dirty tree matching\n # neither row is a refusal.\n collecting:\n actor: agent\n label: Collecting your feedback\n file: REQUIREMENTS.md\n mode: qa\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n <%~ it.vars.styleFormatContract %>\n\n\n You are judging and classifying a round of review feedback.\n\n <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.footnoteFoldIn %>\n\n - The only state files this turn touches are\n `.gtd/REQUIREMENTS.md` and `.gtd/REVIEW_RAW.md` (deleted) —\n you classify, you do not build\n\n The raw review material is: <%~ it.read(\".gtd/REVIEW_RAW.md\") %>\n\n It names a commit. Work from what you already reviewed if you\n wrote today's review earlier this conversation; otherwise read\n that commit's diff yourself first.\n\n The round is actionable if any of these hold:\n\n - The human left a note on `.gtd/REVIEW.md`. A note is a mandatory\n concern below\n - The human added a code comment this round, even a plain-prose\n one — describe it as a concern, and note the comment line\n itself is transient: it must not survive the lap that\n satisfies it\n - The human hand-edited non-comment code this round — no longer\n a committed intent to build on, but a sketch like the entry\n commit's own diff. Describe what it was reaching for; expect\n the next lap to re-derive it from scratch, never call it final\n\n Not actionable only when none of the above holds — nothing but an\n approving remark, no code edit, no substantive note. Never invent\n actionability, and never dismiss a real note or edit as approval.\n\n - If actionable: write `.gtd/REQUIREMENTS.md` with an ordered\n list of concerns, each PRODUCT or TECHNICAL — the shape\n `design.triage` builds, one `## <heading>` per concern in\n build order. Fold every note, comment, and hand-edit in under\n its concern. Raise no open questions here — `design.triage`\n owns that later. Then delete `.gtd/REVIEW_RAW.md` and finish\n - If not: delete `.gtd/REVIEW_RAW.md` and finish, writing\n nothing to `.gtd/REQUIREMENTS.md` — that alone is the sign-off\n on:\n \"A .gtd/REQUIREMENTS.md\": $onFeedback\n \"M .gtd/REQUIREMENTS.md\": $onFeedback\n \"D .gtd/REVIEW_RAW.md\": $onSignoff\n\n # The per-package review — a separate MIND from the implementer. ▸ planner\n # identity; fixing findings is a coder action (packages.item.fix-spec).\n specReview:\n model: <%= it.vars.plannerModel %>\n system: |-\n <%~ it.vars.specReviewerPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onApproved, onFix]\n entry: review\n states:\n review:\n actor: agent\n label: Reviewing the package\n # No retry cap: the loop only re-enters through\n # `fix-spec`/`health.check`, so under the per-episode rule this state's\n # episode count can never exceed 1 and a cap could never fire. The\n # spec-review loop is genuinely unbounded — see docs/configuration.md's\n # `retry` documentation.\n prompt: |\n <%~ it.vars.styleBlock %>\n\n\n You are reviewing a freshly-built work package against its own\n spec.\n\n <%~ it.vars.stateFileRules %>\n\n - The only state file this turn touches is\n `.gtd/SPEC_FEEDBACK.md` — write it only when you find problems\n - The package spec is: <%~ it.read(\".gtd/NEXT.md\") %>\n - Verify the implementation against it: tasks done, criteria\n met, code sound and consistent with the codebase. No diff is\n given — read the range yourself, from `<%= it.startCommit %>`\n to the working tree, process-wide (it can span earlier\n packages)\n - If it fully satisfies the spec, write nothing — silence is\n your approval. Otherwise write `.gtd/SPEC_FEEDBACK.md` listing\n concrete problems for a fix turn, specific enough to act on\n - Never fix anything yourself and never delete the package\n file — a later step owns that\n on:\n \"A .gtd/SPEC_FEEDBACK.md\": $onFix\n \"M .gtd/SPEC_FEEDBACK.md\": $onFix\n \"D .gtd/SPEC_FEEDBACK.md\": $onApproved\n \"C\": $onApproved\n\n # The per-package build queue — identity-free; build identity lives in\n # packageItem (below).\n packageLoop:\n params: [onDrained]\n entry: picking\n states:\n picking:\n actor: check\n label: Picking the next package\n script: |\n #!/usr/bin/env sh\n # Mechanics only — NEXT.md's presence/absence is interpreted by\n # the `on` rows below, never here.\n set +e\n mkdir -p .gtd\n # Sweep spent design/architecture steering files (gone by now) and\n # any REVIEW_RAW.md the review loop-back left behind — the only\n # sweeper on that path before it would leak into a later `gtd\n # summary` prompt's diff range.\n rm -f .gtd/REQUIREMENTS.md .gtd/ARCHITECTURE.md .gtd/QUESTIONS.md .gtd/REVIEW_RAW.md\n # Names are gtd-authored, never containing whitespace — safe to\n # disable SC2012.\n # shellcheck disable=SC2012\n next=$(ls .gtd/packages/*.md 2>/dev/null | head -n 1)\n if [ -n \"$next\" ]; then\n printf '%s' \"$next\" > .gtd/NEXT.md\n else\n rm -f .gtd/NEXT.md\n fi\n on:\n \"D .gtd/NEXT.md\": $onDrained\n \"* .gtd/NEXT.md\": item.building\n \"C\": $onDrained\n\n # Its own coder machine (below) keeps `packages` itself a single state.\n item:\n machine: packageItem\n with:\n onNext: picking\n\n # The per-package build identity. ▸ coder identity. packages.item.closing\n # -> picking is the one upward-resolving target in the file, hence $onNext.\n packageItem:\n model: <%= it.vars.coderModel %>\n system: |-\n <%~ it.vars.builderPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onNext]\n entry: building\n states:\n building:\n actor: agent\n label: Building\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n - The only state file this turn may write is `.gtd/SATISFIED.md`;\n never delete the package file (the spec-review gate reads it\n after you) or touch `.gtd/NEXT.md` — `picking` owns it\n - The package to implement is: <%~ it.read(\".gtd/NEXT.md\") %>\n - First check its acceptance criteria against the current tree —\n an earlier fix turn may already satisfy them. If **every**\n criterion is met, implement nothing: write `.gtd/SATISFIED.md`\n with each criterion's concrete evidence (commit, file, or\n symbol), change nothing else, and finish. Otherwise implement\n normally and skip that file\n - Implement every task the package describes, no more, no less,\n fanning independent ones out to parallel subagents where your\n harness supports it. TDD throughout; leave other package\n files untouched\n - Leave the package file in place, everything uncommitted, then\n finish your turn\n on:\n \"A .gtd/SATISFIED.md\": &satisfied\n to: health.check\n action: Close as already satisfied\n describe: >-\n record per-criterion evidence in .gtd/SATISFIED.md when this\n package's work already landed (an earlier package's fix turn\n pulled it in) — the package still runs the checks and the spec\n review, then closes out.\n \"M .gtd/SATISFIED.md\": *satisfied\n \"* **\": health.check\n\n fix-suite:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.fixFeedbackPrompt %>\n\n - If the only way to green the suite is another package's work,\n make the smallest change that gets there — that package can\n then legitimately report itself already satisfied later\n - Leave everything uncommitted and finish your turn — do not commit\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n fix-spec:\n actor: agent\n label: Fixing review feedback\n file: SPEC_FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n - The only state file this turn touches is\n `.gtd/SPEC_FEEDBACK.md` — address it, then delete it\n - Read it (the reviewer's concerns) and the package spec\n (`<%= it.read(\".gtd/NEXT.md\") %>`), then fix the code to\n resolve every concern, focused\n - Delete `.gtd/SPEC_FEEDBACK.md` once resolved; leave everything\n else uncommitted and finish your turn\n on:\n \"* **\": health.check\n\n closing:\n actor: check\n label: Closing out the package\n script: |\n #!/usr/bin/env sh\n # Removes the just-reviewed package file (path in NEXT.md) plus\n # leftover spec feedback/evidence, so picking selects the next.\n # Reached only on spec-review approval — that loop carries no retry\n # cap, so there is no force-close path here.\n set +e\n pkg=$(cat .gtd/NEXT.md 2>/dev/null)\n [ -n \"$pkg\" ] && rm -f \"$pkg\"\n rm -f .gtd/SPEC_FEEDBACK.md .gtd/NEXT.md .gtd/SATISFIED.md\n on:\n \"* **\": $onNext\n # Nothing left to sweep (an already-clean NEXT.md/package) still\n # needs to proceed to the next package, not stall here.\n \"C\": $onNext\n\n health:\n machine: healthGate\n with:\n onGreen: spec\n onRed: fix-suite\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**packages.item.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n spec:\n machine: specReview\n with:\n onApproved: closing\n onFix: fix-spec\n\n # The process's build identity. ▸ coder identity — model/system stamped\n # onto fix.\n buildTail:\n model: <%= it.vars.coderModel %>\n system: |-\n <%~ it.vars.finisherPersona %>\n\n\n <%~ it.vars.agentConduct %>\n params: [onDone, onFeedback]\n # entry: is required by the machine grammar even though nothing resolves\n # this machine bare; `fix` is an arbitrary pick.\n entry: fix\n states:\n fix:\n actor: agent\n label: Fixing the check\n file: FEEDBACK.md\n prompt: |\n <%~ it.vars.stateFileRules %>\n\n <%~ it.vars.fixFeedbackPrompt %>\n\n - Leave everything uncommitted — do not commit\n retry:\n max: 3\n otherwise: health.escalate\n on:\n \"* **\": health.check\n\n health:\n machine: healthGate\n with:\n onGreen: review\n onRed: fix\n escalateDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` yourself\n (**build.health.check**).\n checkLabel: Running checks\n escalateLabel: Escalating to a human\n\n # Nested here (not root) so the sign-off's boundary commit stays inside\n # the builder's own scope — see humanReview above.\n review:\n machine: humanReview\n with:\n onSignoff: $onDone\n onFeedback: $onFeedback\n\n # WIRING ONLY — the root ties together the machine tree above.\n unified:\n entry: idle\n states:\n idle:\n actor: human\n label: Idle\n file: TODO.md\n message: |\n No active gtd process.\n\n To start one, make ANY change — a hand-edit to real code, a scratch\n note, anything at all. .gtd/TODO.md is a good default\n place to start sketching. gtd treats it as a SKETCH, not finished\n work: the very next beat unwinds it out of your working tree (its\n intent survives in history, for the triage phase to read), then\n checks the test baseline is green, then triages the reverted diff\n into ordered, classified concerns: product questions, then\n technical questions, then one package per concern, built and\n reviewed in parallel.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: unwind\n action: Start\n describe: >-\n make any change — gtd unwinds it out of your working tree (its\n intent survives in history) before checking the test baseline is\n green, then triages the reverted diff into ordered, classified\n concerns and resolves product questions, then technical ones,\n before building each concern's package (**unwind** ->\n **start-gate.check**).\n\n # --entry fix-precheck: a red suite drops into build.fix (-> health ->\n # review tail); a green suite is a no-op back to idle.\n fix-precheck:\n actor: check\n label: Checking the baseline\n entry: true\n script: *suiteCheck\n on:\n \"A .gtd/FEEDBACK.md\": build.fix\n \"M .gtd/FEEDBACK.md\": build.fix\n \"D .gtd/REVIEW_RAW.md\": idle\n \"D .gtd/FEEDBACK.md\": idle\n \"C\": idle\n\n # Separate from start-gate.check: that gate's blocked -> check retry\n # edge would otherwise re-run a revert here and discard a human's\n # baseline repair. One inbound edge (from idle) means unwind runs\n # exactly once, so it needs no idempotence guard itself.\n unwind:\n actor: check\n label: Unwinding your input\n script: |\n #!/usr/bin/env sh\n set +e\n mkdir -p .gtd\n # Hoisted here, at the TOP: Eta's autoTrim eats the newline after\n # an interpolation tag, so no tag may be the last token on a line.\n # Uses it.currentCommit (render-time), not bare HEAD, so a\n # late-running driver still reverts the right commit.\n commit=\"<%~ it.currentCommit %>\"\n git revert --no-commit \"$commit\" 2> .gtd/.unwind-error\n code=$?\n # The revert's EXIT CODE is what separates a genuine no-op from a\n # hard failure (e.g. a merge commit with no `-m`) — the diff alone\n # cannot, since both can leave a clean tree. Turning the failure\n # into a FEEDBACK.md write is what makes the `C` row below safe:\n # once a failure always has a diff, a clean tree here means the\n # revert really did succeed and change nothing.\n if [ \"$code\" -ne 0 ]; then\n printf 'gtd could not unwind %s out of your working tree.\\n\\n' \"$commit\" > .gtd/FEEDBACK.md\n if [ -s .gtd/.unwind-error ]; then\n cat .gtd/.unwind-error >> .gtd/FEEDBACK.md\n else\n printf '`git revert --no-commit` exited %s and produced no output.\\n' \"$code\" >> .gtd/FEEDBACK.md\n fi\n fi\n rm -f .gtd/.unwind-error\n on:\n \"A .gtd/FEEDBACK.md\": unwind-failed\n \"M .gtd/FEEDBACK.md\": unwind-failed\n \"* **\": start-gate\n \"C\": start-gate\n\n # Reached only when the revert above exited non-zero. A human repairs\n # the tree by hand; start-gate.check clears FEEDBACK.md on its way\n # green, so nothing has to sweep it here.\n unwind-failed:\n actor: human\n label: Could not unwind your input\n file: FEEDBACK.md\n message: |\n gtd could not revert your sketch out of the working tree.\n `.gtd/FEEDBACK.md` holds the error.\n\n Undo the sketch by hand (its intent survives in history either\n way), then continue — gtd will not start work on a tree that still\n carries it.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n on:\n \"* **\":\n to: start-gate\n action: Continue\n describe: >-\n having undone the sketch by hand, check the test baseline is\n green and start triage (**start-gate.check**).\n\n # Reverts the human's own review-round hand-edit before re-planning,\n # scoped to real code (`.gtd/` excluded — REVIEW.md is already gone by\n # now). Names the commit via it.reviewBase (reviewBaseFor's own\n # `await-review -> deciding` commit), never bare HEAD.\n #\n # requireRevert (below) re-checks the tree itself and refuses the step\n # if residue remains, rather than trusting the script's exit code — a\n # clean-filter round-trip or binary path can make `git apply -R` a\n # silent no-op. Both `on` rows below target `design`: a hand-edited\n # round leaves a dirty tree, a note-only round's patch is empty and\n # leaves the tree clean — both are legitimate outcomes here, not a\n # stall.\n #\n # `file: REVIEW.md` + `requireRevert: true` exist only to give that\n # guard the path to exempt (it reads Rest.hints.file, never this\n # literal) — REVIEW.md is already gone two commits earlier, so this\n # state never renders it.\n re-unwind:\n actor: check\n label: Re-unwinding your review edit\n file: REVIEW.md\n requireRevert: true\n script: |\n #!/usr/bin/env sh\n # Scoped revert of the human's review-round edit — .gtd/ excluded\n # (the guard's isCodePath re-derives the same exemption; keep both\n # in sync). Expected to succeed; requireRevert catches a silent\n # apply failure.\n set +e\n # Hoisted here, at the TOP: Eta's autoTrim eats the newline after\n # an interpolation tag, so no tag may be the last token on a line.\n commit=\"<%~ it.reviewBase %>\"\n patch=.gtd/.re-unwind.patch\n mkdir -p .gtd\n git diff --binary \"$commit^\" \"$commit\" -- . \":(exclude).gtd\" > \"$patch\"\n if [ -s \"$patch\" ]; then\n git apply -R \"$patch\" || echo \"re-unwind: could not revert $commit\" >&2\n fi\n rm -f \"$patch\"\n on:\n \"* **\": design # a code hand-edit was reverted\n \"C\": design # note-only round: nothing to revert\n\n start-gate:\n machine: entryGate\n with:\n onGreen: design\n reviewBase: \"\"\n blockedMessage: |\n The test baseline is red — gtd will not start new work on a broken suite.\n `.gtd/FEEDBACK.md` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**start-gate.check**). To repair the baseline as its own separate\n reviewed commit instead, abandon this start and run\n `gtd --entry fix-precheck` from a clean `idle`.\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n review-gate:\n machine: entryGate\n with:\n onGreen: build.review\n reviewBase: <%= it.vars.reviewBase %>\n blockedMessage: |\n The test baseline is red — gtd will not start a review on a broken suite.\n `.gtd/FEEDBACK.md` holds the failing output.\n\n What each change does next (then run `gtd land`):\n <% it.edges.forEach(function (e) { if (e.describe) { %>\n <%~ \"- \" + (e.action ? \"**\" + e.action + \"** — \" : \"\") + e.describe + \"\\n\" %>\n <% } }) %>\n blockedDescribe: >-\n edit the code and/or `.gtd/FEEDBACK.md` to fix the failing tests\n (**review-gate.check**).\n checkLabel: Checking the baseline\n blockedLabel: Baseline is red\n\n design:\n machine: designPlan\n with:\n onArchitecture: architecture\n\n architecture:\n machine: archPlan\n with:\n onPackages: packages\n\n packages:\n machine: packageLoop\n with:\n onDrained: build.review\n\n build:\n machine: buildTail\n with:\n onDone: idle\n onFeedback: re-unwind\n";
191778
192268
  //#endregion
191779
192269
  //#region src/workflows/templates.ts
191780
192270
  const SCHEMA_URL = "https://cdn.jsdelivr.net/npm/@pmelab/gtd/schema.json";
@@ -193150,6 +193640,116 @@ const restoredOutcome = (to, state) => printfLine(FMT_RESTORED, [
193150
193640
  shellQuote(state)
193151
193641
  ]);
193152
193642
  //#endregion
193643
+ //#region src/StepGuards.ts
193644
+ /** True when `changes` deletes `file` — the require-revert guard's own `GuardContext.fileDeleted` check. */
193645
+ const deletesFile = (changes, file) => changes.some((c) => c.path === file && c.status === "D");
193646
+ /**
193647
+ * True when `path` sits inside gtd's plumbing directory. Exact match or
193648
+ * `STATE_DIR + "/"` prefix — a naive `path.startsWith(STATE_DIR)` would also
193649
+ * swallow a sibling `.gtd-backup/`.
193650
+ */
193651
+ const isPlumbingPath = (path) => path === ".gtd" || path.startsWith(`.gtd/`);
193652
+ /** True for a real code path (outside gtd's plumbing directory) — every state's `file:` compiles under `STATE_DIR` too, so this already excludes a state's own steering file. */
193653
+ const isCodePath = (path) => !isPlumbingPath(path);
193654
+ /** The `mode:` name of gtd's built-in REVIEW.md checkbox format — the only mode the review-doc guard understands. */
193655
+ const REVIEW_MODE = "review";
193656
+ /**
193657
+ * Selects exactly the human review gate (`await-review` in the bundled
193658
+ * template) — a human-actor state declaring `mode: review`. Exported so
193659
+ * `src/Edge.ts#renderDecision` can emit the `gtd uncheck` reset step at
193660
+ * precisely the same state this guard applies to — one predicate, so the
193661
+ * guard and the reset can't drift apart.
193662
+ */
193663
+ const isHumanReviewGate = (rest) => rest.stateDef.actor === "human" && rest.stateDef.mode === REVIEW_MODE;
193664
+ /** Selects exactly the human review gate (`await-review` in the bundled template) — a human-actor state declaring `mode: review`. */
193665
+ const reviewDocGuard = {
193666
+ name: "review-doc",
193667
+ appliesTo: isHumanReviewGate,
193668
+ check: (ctx) => succeed$3(ctx.fileDeleted ? `${ctx.file} was deleted at "${ctx.rest.state}" — restore it, or leave a note (or edit code) to request changes.` : void 0)
193669
+ };
193670
+ /** The one-line marker `feedback-collecting` writes when a review round left nothing actionable — the ONLY content that lets a `requireProgress` state's instructions file be deleted without a code change. */
193671
+ const NOTHING_ACTIONABLE_SENTINEL = "NOTHING ACTIONABLE";
193672
+ const feedbackProgressGuard = {
193673
+ name: "feedback-progress",
193674
+ appliesTo: (rest) => isRequireProgressState(rest.def, rest.state),
193675
+ check: (ctx) => gen(function* () {
193676
+ if (!ctx.fileDeleted) return void 0;
193677
+ if (ctx.hasCodeChange) return void 0;
193678
+ if (((yield* ctx.head) ?? "").trim().startsWith(NOTHING_ACTIONABLE_SENTINEL)) return void 0;
193679
+ return `${ctx.file} was deleted at "${ctx.rest.state}" without addressing its instructions — implement the changes it lists (then delete it), don't just remove the file.`;
193680
+ })
193681
+ };
193682
+ /** The `mode:` name of gtd's built-in open-questions checkbox format — the only mode the answer-completeness guard acts on. */
193683
+ const QA_MODE = "qa";
193684
+ /** The four step-capture guards, in EVALUATION order — message precedence when two would fire is observable, so this order is the contract (matches the old hand-copied call order, minus the now-removed steering-file guard). */
193685
+ const stepGuards = [
193686
+ reviewDocGuard,
193687
+ feedbackProgressGuard,
193688
+ {
193689
+ name: "answer-completeness",
193690
+ appliesTo: (rest) => isAnswerGateState(rest.def, rest.state) && rest.stateDef.mode === QA_MODE,
193691
+ check: (ctx) => gen(function* () {
193692
+ const unanswered = unansweredQuestions((yield* ctx.worktree) ?? "");
193693
+ if (unanswered.length === 0) return void 0;
193694
+ const list = unanswered.map((q) => ` - ${q.question}`).join("\n");
193695
+ return `${unanswered.length} open question(s) in ${ctx.file} not answered at "${ctx.rest.state}" — tick exactly one option per question (or delete a question you don't want to answer, or delete the whole "## Open Questions" section to accept the plan as-is):\n${list}`;
193696
+ })
193697
+ },
193698
+ {
193699
+ name: "require-revert",
193700
+ appliesTo: (rest) => isRequireRevertState(rest.def, rest.state),
193701
+ check: (ctx) => gen(function* () {
193702
+ const rb = ctx.template.reviewBase;
193703
+ if (rb === "" || rb === ctx.template.startCommit) return `"${ctx.rest.state}" has no identifiable review round to check (reviewBase is unset) — the revert cannot be established.`;
193704
+ const git = yield* GitService;
193705
+ const base = `${rb}~1`;
193706
+ const scoped = ((yield* git.commitHistory(base, rb))[0]?.touched ?? []).filter((path) => isCodePath(path));
193707
+ if (scoped.length === 0) return void 0;
193708
+ const residue = (yield* git.changedPaths(base)).filter((c) => scoped.includes(c.path)).map((c) => c.path);
193709
+ if (residue.length === 0) return void 0;
193710
+ return `${residue.join(", ")} still differ from ${base} at "${ctx.rest.state}" — the revert did not take. Run \`git checkout ${base} -- ${pathspec(residue)}\`, then \`gtd land\` again.`;
193711
+ })
193712
+ }
193713
+ ];
193714
+ /**
193715
+ * The `gtd land` capture gate: run every applicable guard against one sample
193716
+ * of the current committed/working bytes, and fail with the first refusal's
193717
+ * reason, prefixed `gtd land: ` (the one place that prefix, and
193718
+ * `Commentary.ts`'s `renderFailure` `/^gtd[: ]/` contract, is satisfied). A
193719
+ * no-op for a `"noop"` decision, an ATTEMPT commit (empty diff by
193720
+ * construction — nothing for any guard to check), a state with no `file:`, or
193721
+ * a state no guard applies to.
193722
+ */
193723
+ const enforceStepGuards = (input) => gen(function* () {
193724
+ if (input.kind !== "commit" || input.attempt) return;
193725
+ const file = input.file;
193726
+ if (file === void 0) return;
193727
+ const applicable = stepGuards.filter((g) => g.appliesTo(input.rest));
193728
+ if (applicable.length === 0) return;
193729
+ const fileDeleted = deletesFile(input.changes, file);
193730
+ const hasCodeChange = input.changes.some((c) => isCodePath(c.path));
193731
+ const base = {
193732
+ rest: input.rest,
193733
+ file,
193734
+ changes: input.changes,
193735
+ fileDeleted,
193736
+ hasCodeChange,
193737
+ template: input.context
193738
+ };
193739
+ const files = yield* RepoFiles;
193740
+ const head = yield* cached(files.committed(file));
193741
+ const worktree = yield* cached(try_(() => files.working(file)));
193742
+ const ctx = {
193743
+ ...base,
193744
+ head,
193745
+ worktree
193746
+ };
193747
+ for (const g of applicable) {
193748
+ const refusal = yield* g.check(ctx);
193749
+ if (refusal !== void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd land: ${refusal}`));
193750
+ }
193751
+ });
193752
+ //#endregion
193153
193753
  //#region src/Edge.ts
193154
193754
  const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
193155
193755
  const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
@@ -193670,12 +194270,31 @@ const commitDecisionOutcome = (decision) => ({
193670
194270
  * produce its git effect — the ONE place a decision becomes git commands.
193671
194271
  * Pure: no git read, no failure mode — a commit decision always becomes an
193672
194272
  * ordinary commit plus its outcome report.
194273
+ *
194274
+ * At the human review gate (`isHumanReviewGate`, the same predicate
194275
+ * `StepGuards.ts`'s `reviewDocGuard` applies) an unconditional
194276
+ * `gtd uncheck '<file>'` step runs ahead of the commit, resetting every
194277
+ * `- [x]`/`- [X]` pointer box in `rest.hints.file` before `git add -A` picks
194278
+ * it up — a tick is read-progress, never sign-off, and must never reach a
194279
+ * commit (see `src/ReviewDoc.ts#clearFilePointerTicks`). Never wrapped in
194280
+ * `fileExistsGuard`: that guard's `[ -f <file> ] || exit 0` would exit the
194281
+ * WHOLE script on a missing file, silently skipping the commit — `gtd
194282
+ * uncheck` already treats a missing file as a no-op itself.
193673
194283
  */
193674
194284
  const renderDecision = (rest, decision, cost, model) => {
193675
- return [{
193676
- kind: "gitWrite",
193677
- command: commitAll(withCostTrailer(decision.subject, cost, model))
193678
- }, commitDecisionOutcome(decision)];
194285
+ const command = commitAll(withCostTrailer(decision.subject, cost, model));
194286
+ const file = rest.hints.file;
194287
+ return [
194288
+ ...isHumanReviewGate(rest) && file !== void 0 ? [{
194289
+ kind: "command",
194290
+ command: `gtd uncheck ${shellQuote(file)}`
194291
+ }] : [],
194292
+ {
194293
+ kind: "gitWrite",
194294
+ command
194295
+ },
194296
+ commitDecisionOutcome(decision)
194297
+ ];
193679
194298
  };
193680
194299
  /** The `EmittedScripts` a `"commit"` `StepPlan` carries alongside `perform` — built from `renderDecision`'s output. */
193681
194300
  const buildStepScripts = (rest, decision, cost, model) => emitScripts(renderDecision(rest, decision, cost, model));
@@ -204958,17 +205577,23 @@ const toCodeAction = (uri) => (action) => ({
204958
205577
  kind: import_main.CodeActionKind.QuickFix,
204959
205578
  edit: { changes: { [uri]: [...action.edits] } }
204960
205579
  });
204961
- /** A `SteeringPointer` → a `Location` resolved against `root` — a collapsed range (a cursor, not a selection), the target file never stat-ed. */
204962
- const toLocation = (root) => (pointer) => ({
204963
- uri: pathToFileURL(resolve(root, pointer.path)).toString(),
205580
+ /**
205581
+ * A `SteeringPointer` → a `Location` — a collapsed range (a cursor, not a
205582
+ * selection). An absent `pointer.path` means "this same document": the
205583
+ * `Location` uses `documentUri` untouched, and `root` (needed only to
205584
+ * resolve a foreign `path` against the git working tree) goes unused. An
205585
+ * absent `pointer.character` lands at column 0.
205586
+ */
205587
+ const toLocation = (root, documentUri) => (pointer) => ({
205588
+ uri: pointer.path === void 0 ? documentUri : pathToFileURL(resolve(root, pointer.path)).toString(),
204964
205589
  range: {
204965
205590
  start: {
204966
205591
  line: pointer.line,
204967
- character: 0
205592
+ character: pointer.character ?? 0
204968
205593
  },
204969
205594
  end: {
204970
205595
  line: pointer.line,
204971
- character: 0
205596
+ character: pointer.character ?? 0
204972
205597
  }
204973
205598
  }
204974
205599
  });
@@ -205102,10 +205727,11 @@ const makeSteeringLanguageService = (env, warn) => {
205102
205727
  return ((await capabilitiesFor(uri)).format?.actions(text, range) ?? []).map(toCodeAction(uri));
205103
205728
  },
205104
205729
  definition: async (uri, text, position) => {
205105
- const pointer = (await capabilitiesFor(uri)).format?.pointerAt?.(text, position.line);
205730
+ const pointer = (await capabilitiesFor(uri)).format?.pointerAt?.(text, position);
205106
205731
  if (pointer === void 0) return [];
205732
+ if (pointer.path === void 0) return [toLocation("", uri)(pointer)];
205107
205733
  const root = await safeGitTopLevel(dirname(fileURLToPath(uri))) ?? workspaceRoot;
205108
- return root === void 0 ? [] : [toLocation(root)(pointer)];
205734
+ return root === void 0 ? [] : [toLocation(root, uri)(pointer)];
205109
205735
  },
205110
205736
  diagnostics: async (uri, text) => {
205111
205737
  const map = await safeSteeringMap(rootFor(uri));
@@ -205471,108 +206097,6 @@ const openInBrowser = (url) => {
205471
206097
  } catch {}
205472
206098
  };
205473
206099
  //#endregion
205474
- //#region src/StepGuards.ts
205475
- /** True when `changes` deletes `file` — the require-revert guard's own `GuardContext.fileDeleted` check. */
205476
- const deletesFile = (changes, file) => changes.some((c) => c.path === file && c.status === "D");
205477
- /**
205478
- * True when `path` sits inside gtd's plumbing directory. Exact match or
205479
- * `STATE_DIR + "/"` prefix — a naive `path.startsWith(STATE_DIR)` would also
205480
- * swallow a sibling `.gtd-backup/`.
205481
- */
205482
- const isPlumbingPath = (path) => path === ".gtd" || path.startsWith(`.gtd/`);
205483
- /** True for a real code path (outside gtd's plumbing directory) — every state's `file:` compiles under `STATE_DIR` too, so this already excludes a state's own steering file. */
205484
- const isCodePath = (path) => !isPlumbingPath(path);
205485
- /** The `mode:` name of gtd's built-in REVIEW.md checkbox format — the only mode the review-doc guard understands. */
205486
- const REVIEW_MODE = "review";
205487
- /** Selects exactly the human review gate (`await-review` in the bundled template) — a human-actor state declaring `mode: review`. */
205488
- const reviewDocGuard = {
205489
- name: "review-doc",
205490
- appliesTo: (rest) => rest.stateDef.actor === "human" && rest.stateDef.mode === REVIEW_MODE,
205491
- check: (ctx) => succeed$3(ctx.fileDeleted ? `${ctx.file} was deleted at "${ctx.rest.state}" — restore it, or leave a note (or edit code) to request changes.` : void 0)
205492
- };
205493
- /** The one-line marker `feedback-collecting` writes when a review round left nothing actionable — the ONLY content that lets a `requireProgress` state's instructions file be deleted without a code change. */
205494
- const NOTHING_ACTIONABLE_SENTINEL = "NOTHING ACTIONABLE";
205495
- const feedbackProgressGuard = {
205496
- name: "feedback-progress",
205497
- appliesTo: (rest) => isRequireProgressState(rest.def, rest.state),
205498
- check: (ctx) => gen(function* () {
205499
- if (!ctx.fileDeleted) return void 0;
205500
- if (ctx.hasCodeChange) return void 0;
205501
- if (((yield* ctx.head) ?? "").trim().startsWith(NOTHING_ACTIONABLE_SENTINEL)) return void 0;
205502
- return `${ctx.file} was deleted at "${ctx.rest.state}" without addressing its instructions — implement the changes it lists (then delete it), don't just remove the file.`;
205503
- })
205504
- };
205505
- /** The `mode:` name of gtd's built-in open-questions checkbox format — the only mode the answer-completeness guard acts on. */
205506
- const QA_MODE = "qa";
205507
- /** The four step-capture guards, in EVALUATION order — message precedence when two would fire is observable, so this order is the contract (matches the old hand-copied call order, minus the now-removed steering-file guard). */
205508
- const stepGuards = [
205509
- reviewDocGuard,
205510
- feedbackProgressGuard,
205511
- {
205512
- name: "answer-completeness",
205513
- appliesTo: (rest) => isAnswerGateState(rest.def, rest.state) && rest.stateDef.mode === QA_MODE,
205514
- check: (ctx) => gen(function* () {
205515
- const unanswered = unansweredQuestions((yield* ctx.worktree) ?? "");
205516
- if (unanswered.length === 0) return void 0;
205517
- const list = unanswered.map((q) => ` - ${q.question}`).join("\n");
205518
- return `${unanswered.length} open question(s) in ${ctx.file} not answered at "${ctx.rest.state}" — tick exactly one option per question (or delete a question you don't want to answer, or delete the whole "## Open Questions" section to accept the plan as-is):\n${list}`;
205519
- })
205520
- },
205521
- {
205522
- name: "require-revert",
205523
- appliesTo: (rest) => isRequireRevertState(rest.def, rest.state),
205524
- check: (ctx) => gen(function* () {
205525
- const rb = ctx.template.reviewBase;
205526
- if (rb === "" || rb === ctx.template.startCommit) return `"${ctx.rest.state}" has no identifiable review round to check (reviewBase is unset) — the revert cannot be established.`;
205527
- const git = yield* GitService;
205528
- const base = `${rb}~1`;
205529
- const scoped = ((yield* git.commitHistory(base, rb))[0]?.touched ?? []).filter((path) => isCodePath(path));
205530
- if (scoped.length === 0) return void 0;
205531
- const residue = (yield* git.changedPaths(base)).filter((c) => scoped.includes(c.path)).map((c) => c.path);
205532
- if (residue.length === 0) return void 0;
205533
- return `${residue.join(", ")} still differ from ${base} at "${ctx.rest.state}" — the revert did not take. Run \`git checkout ${base} -- ${pathspec(residue)}\`, then \`gtd land\` again.`;
205534
- })
205535
- }
205536
- ];
205537
- /**
205538
- * The `gtd land` capture gate: run every applicable guard against one sample
205539
- * of the current committed/working bytes, and fail with the first refusal's
205540
- * reason, prefixed `gtd land: ` (the one place that prefix, and
205541
- * `Commentary.ts`'s `renderFailure` `/^gtd[: ]/` contract, is satisfied). A
205542
- * no-op for a `"noop"` decision, an ATTEMPT commit (empty diff by
205543
- * construction — nothing for any guard to check), a state with no `file:`, or
205544
- * a state no guard applies to.
205545
- */
205546
- const enforceStepGuards = (input) => gen(function* () {
205547
- if (input.kind !== "commit" || input.attempt) return;
205548
- const file = input.file;
205549
- if (file === void 0) return;
205550
- const applicable = stepGuards.filter((g) => g.appliesTo(input.rest));
205551
- if (applicable.length === 0) return;
205552
- const fileDeleted = deletesFile(input.changes, file);
205553
- const hasCodeChange = input.changes.some((c) => isCodePath(c.path));
205554
- const base = {
205555
- rest: input.rest,
205556
- file,
205557
- changes: input.changes,
205558
- fileDeleted,
205559
- hasCodeChange,
205560
- template: input.context
205561
- };
205562
- const files = yield* RepoFiles;
205563
- const head = yield* cached(files.committed(file));
205564
- const worktree = yield* cached(try_(() => files.working(file)));
205565
- const ctx = {
205566
- ...base,
205567
- head,
205568
- worktree
205569
- };
205570
- for (const g of applicable) {
205571
- const refusal = yield* g.check(ctx);
205572
- if (refusal !== void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd land: ${refusal}`));
205573
- }
205574
- });
205575
- //#endregion
205576
206100
  //#region src/ModeContradiction.ts
205577
206101
  /**
205578
206102
  * Written for two readers at once, since gtd validate's exit code carries no
@@ -206941,6 +207465,28 @@ const runOpenQuestionsCheckCommand = (file) => gen(function* () {
206941
207465
  if (errors.length === 0) return;
206942
207466
  return yield* fail$2(/* @__PURE__ */ new Error(`gtd check: ${errors.length} open question(s) unanswered in ${file}:\n` + errors.join("\n")));
206943
207467
  });
207468
+ /**
207469
+ * `gtd uncheck <file>`: read `<file>`, apply `clearFilePointerTicks`, and
207470
+ * write the result back only when the bytes actually changed — an untouched
207471
+ * file is never rewritten, so its mtime never moves. Resolves no workflow
207472
+ * state and reads no config — standalone, runnable from any directory with
207473
+ * `<file>` given explicitly, shaped exactly like `gtd check <mode> <file>`.
207474
+ *
207475
+ * Takes no `<mode>` argument, and must never grow one: this command means
207476
+ * review-mode file pointers and nothing else — `gtd check <mode> <file>`
207477
+ * already handles `qa`-mode's answered-question boxes, which this command
207478
+ * must never touch. A missing file writes nothing and exits 0, mirroring
207479
+ * `gtd check`'s absent-file behavior.
207480
+ */
207481
+ const runUncheckCommand = (file) => gen(function* () {
207482
+ const fs = yield* FileSystem;
207483
+ const toError = (e) => e instanceof Error ? e : new Error(String(e));
207484
+ if (!(yield* fs.exists(file).pipe(mapError$1(toError)))) return;
207485
+ const content = yield* fs.readFileString(file).pipe(mapError$1(toError));
207486
+ const cleared = clearFilePointerTicks(content);
207487
+ if (cleared === content) return;
207488
+ yield* fs.writeFileString(file, cleared).pipe(mapError$1(toError));
207489
+ });
206944
207490
  /** Which declared `on` pattern (if any) each pending change matches. `onEdges` must already be rendered against `it.vars`, so the reported pattern is the one a real `gtd land` would match against. */
206945
207491
  const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
206946
207492
  const matchedRow = onEdges.find(([patternStr]) => {
@@ -207076,7 +207622,8 @@ const needsOf = (kind) => {
207076
207622
  case "lsp":
207077
207623
  case "install": return "none";
207078
207624
  case "init":
207079
- case "check": return "fs";
207625
+ case "check":
207626
+ case "uncheck": return "fs";
207080
207627
  case "visualize": return "config";
207081
207628
  default: return "state";
207082
207629
  }
@@ -207103,6 +207650,7 @@ const dispatchVoidCommand = (command, json, out) => {
207103
207650
  case "next": return runNextCommand(json, out);
207104
207651
  case "validate": return runValidateCommand(out);
207105
207652
  case "check": return runCheckCommand(command.mode, command.file, command.openQuestions ?? false);
207653
+ case "uncheck": return runUncheckCommand(command.file);
207106
207654
  case "install": return runInstallCommand(out);
207107
207655
  case "summary": return runSummaryCommand(out);
207108
207656
  case "base": return runBaseCommand(out);
@@ -207425,6 +207973,23 @@ const COMMAND_ROWS = [
207425
207973
  "instead (see --help)"
207426
207974
  ]
207427
207975
  },
207976
+ {
207977
+ token: "uncheck",
207978
+ kind: "uncheck",
207979
+ arity: { name: "file" },
207980
+ details: [
207981
+ "Read <file> and reset every review-mode `- [x]`/`- [X]`",
207982
+ "file-pointer box back to `- [ ]`, writing the result back",
207983
+ "only when the bytes actually changed. Resolves no workflow",
207984
+ "state and reads no config — standalone, runnable from any",
207985
+ "directory with <file> given explicitly. This is what the",
207986
+ "landing script runs ahead of the commit at the human review",
207987
+ "gate, so no tick ever reaches a commit. Takes no <mode> —",
207988
+ "it means review-mode file pointers and nothing else, never",
207989
+ "qa-mode's answered-question boxes. A missing file writes",
207990
+ "nothing and exits 0"
207991
+ ]
207992
+ },
207428
207993
  {
207429
207994
  token: "install",
207430
207995
  kind: "install",
@@ -207741,6 +208306,15 @@ const parseArgv = (argv) => {
207741
208306
  json,
207742
208307
  verbose
207743
208308
  };
208309
+ if (kind === "uncheck") return {
208310
+ kind: "command",
208311
+ command: {
208312
+ kind: "uncheck",
208313
+ file: restPositionals[0]
208314
+ },
208315
+ json,
208316
+ verbose
208317
+ };
207744
208318
  return {
207745
208319
  kind: "command",
207746
208320
  command: { kind },