@pmelab/gtd 42.0.0 → 43.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +79 -35
  3. package/claude/hooks/sketch.ts +1 -1
  4. package/dist/chunks/{reserved-D7MNY9QJ.mjs → reserved-COEMFEpy.mjs} +35 -46
  5. package/dist/chunks/{steering-oCDdQlpP.mjs → steering-D4LUQ-av.mjs} +95 -12
  6. package/dist/chunks/{tail-D2o2h5cc.mjs → tail-C_xn8U7U.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +488 -30
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +447 -27
  10. package/dist/web/main.js +42 -13
  11. package/dist/workflow-kit.mjs +61 -25
  12. package/dist/workflows/cleanup.mjs +2 -2
  13. package/dist/workflows/feature.mjs +31 -42
  14. package/dist/workflows/fix.mjs +3 -3
  15. package/dist/workflows/review.mjs +2 -2
  16. package/dist/workflows/rollout.mjs +1 -1
  17. package/dist/workflows.mjs +217 -121
  18. package/package.json +1 -1
  19. package/src/cli/Cli.test.ts +33 -1
  20. package/src/cli/Cli.ts +70 -6
  21. package/src/cli/History.ts +15 -0
  22. package/src/cli/Judges.test.ts +373 -0
  23. package/src/cli/Judges.ts +624 -0
  24. package/src/cli/Metrics.ts +3 -16
  25. package/src/cli/Program.test.ts +3 -1
  26. package/src/cli/Program.ts +9 -0
  27. package/src/steering/index.ts +12 -1
  28. package/src/steering/review.test.ts +107 -0
  29. package/src/steering/review.ts +159 -9
  30. package/src/testkit/EmittedScriptRecognizer.ts +0 -14
  31. package/src/testkit/steps.ts +4 -0
  32. package/src/workflow-kit/review.test.ts +170 -26
  33. package/src/workflow-kit/review.ts +95 -19
  34. package/src/workflows/feature/lens-review.feature +16 -4
  35. package/src/workflows/feature/review-answers.feature +179 -0
  36. package/src/workflows/feature/review.test.ts +5 -53
  37. package/src/workflows/feature/review.ts +31 -42
  38. package/src/workflows/feature/steps.test.ts +0 -12
  39. package/src/workflows/feature/steps.ts +3 -10
  40. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +6 -5
  41. package/src/workflows/shared/access.test.ts +2 -11
  42. package/src/workflows/shared/access.ts +0 -2
  43. package/src/workflows/shared/text.test.ts +40 -89
  44. package/src/workflows/shared/text.ts +52 -54
@@ -13931,8 +13931,8 @@ const eolOf = (content) => content.includes("\r\n") ? "\r\n" : "\n";
13931
13931
  const PREFIX_RE$1 = /^(H|A):\s*/;
13932
13932
  const lineOf = (node, which) => toLspPosition(node.position[which]).line;
13933
13933
  /** The item's own blocks joined — never the item node's span, which can run past its last line into the next bullet's marker. */
13934
- const itemText = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
13935
- const prefixOf = (content, item) => PREFIX_RE$1.exec(itemText(content, item));
13934
+ const itemText$1 = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
13935
+ const prefixOf = (content, item) => PREFIX_RE$1.exec(itemText$1(content, item));
13936
13936
  /** A thread is a definition whose body OPENS with a list whose first item carries a prefix — anything else stays a one-shot footnote. */
13937
13937
  const isThreadBody = (content, children) => {
13938
13938
  const first = children[0];
@@ -13952,7 +13952,7 @@ const entryOf = (content, item, previous, report) => {
13952
13952
  return;
13953
13953
  }
13954
13954
  const author = match[1] === "H" ? "me" : "agent";
13955
- const text = itemText(content, item).slice(match[0].length).trim();
13955
+ const text = itemText$1(content, item).slice(match[0].length).trim();
13956
13956
  if (text === "") report(`empty "${match[1]}:" entry — write your own words there`, start, end);
13957
13957
  if (previous === void 0 && author === "agent") report(`the agent never starts a thread — the first entry must be "H:"`, start, end);
13958
13958
  else if (previous === author) report(`two consecutive "${match[1]}:" entries — entries must alternate`, start, end);
@@ -15654,6 +15654,9 @@ const qaDescriptor = {
15654
15654
  };
15655
15655
  //#endregion
15656
15656
  //#region src/steering/review.ts
15657
+ const ANSWER_LEAD = "Tick exactly one:";
15658
+ const ANSWER_FIX = "fix as suggested";
15659
+ const ANSWER_WONT_FIX = "won't fix";
15657
15660
  /**
15658
15661
  * The canonical sample. Every detail here is load-bearing for the formatter
15659
15662
  * round-trip: both footnote bodies exceed 80 characters so a reflow actually
@@ -15920,16 +15923,42 @@ const parseHunk = (content, lines, title, item) => {
15920
15923
  errors: [hunkSecondPointerFinding(content, lines, item, token, sourceLine, title, file), hunkLineWithoutRangeFinding(content, item, token, sourceLine, title, file)].filter((e) => e !== void 0)
15921
15924
  };
15922
15925
  };
15926
+ const itemText = (content, item) => item.children.map((c) => sourceText(content, c)).join(" ").replace(/^\[[ xX]\]\s*/, "").trim();
15927
+ /**
15928
+ * The task list right after the `Tick exactly one:` paragraph, with that
15929
+ * paragraph's index. Read by position, never by label, so an edited label or
15930
+ * a free text opening with `./path` is still an answer, not a pointer. Any
15931
+ * count but three boxes is kept, and reads as unanswered.
15932
+ */
15933
+ const answerListOf = (content, body) => {
15934
+ const lead = body.findIndex((n, i) => n.type === "paragraph" && sourceText(content, n) === "Tick exactly one:" && body[i + 1]?.type === "list" && body[i + 1].children.every((item) => item.checked != null));
15935
+ if (lead === -1) return void 0;
15936
+ const list = body[lead + 1];
15937
+ const last = list.children.length - 1;
15938
+ return {
15939
+ lead,
15940
+ list,
15941
+ options: list.children.map((item, i) => {
15942
+ const text = itemText(content, item);
15943
+ return {
15944
+ checked: item.checked === true,
15945
+ text: i === last && text.toLowerCase() === "_your answer_".toLowerCase() ? "" : text
15946
+ };
15947
+ })
15948
+ };
15949
+ };
15923
15950
  /** Splits one chunk's body nodes into its file pointers (hunk pointers are task items, collected recursively at ANY nesting depth via `taskItems` — a nested hunk is the same kind of hunk as a top-level one) and description prose. The description is the chunk's own leading run and never a node that contains a hunk pointer: it stops at the first node whose `taskItems` include a real pointer (at any depth — a blockquote wrapping a pointer list stops it just as a top-level list would). Every node kind in what's left before that point contributes — a `footnoteDefinition` is the one exception, excluded by `blockNodesOfRun` itself (it's a note, never document content). */
15924
15951
  const parseChunkBody = (content, lines, title, body) => {
15925
- const firstPointerIndex = body.findIndex((n) => taskItems(n).some((item) => hasPointerToken(content, item)));
15926
- const leadingRun = firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex);
15952
+ const answer = answerListOf(content, body);
15953
+ const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
15954
+ const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)));
15955
+ const leadingRun = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
15927
15956
  const descriptionNodes = blockNodesOfRun(content, leadingRun);
15928
15957
  const descriptionInline = collapseImages(blockRunInline(content, leadingRun));
15929
15958
  const description = leadingRun.filter((n) => n.type !== "footnoteDefinition").map((n) => sourceText(content, n)).join(" ").trim();
15930
15959
  const files = [];
15931
15960
  const errors = [];
15932
- for (const item of body.flatMap((n) => taskItems(n))) {
15961
+ for (const item of body.filter((n) => !isAnswer(n)).flatMap((n) => taskItems(n))) {
15933
15962
  const parsed = parseHunk(content, lines, title, item);
15934
15963
  if (!parsed) continue;
15935
15964
  files.push(parsed.file);
@@ -15940,7 +15969,9 @@ const parseChunkBody = (content, lines, title, body) => {
15940
15969
  descriptionNodes,
15941
15970
  descriptionInline,
15942
15971
  files,
15943
- errors
15972
+ errors,
15973
+ ...answer === void 0 ? {} : { answers: answer.options },
15974
+ boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0)
15944
15975
  };
15945
15976
  };
15946
15977
  /** Splits the tree's top-level nodes into consecutive `##` chunk blocks — a `## ` line found only inside a fence never becomes a heading node at all, so it's never mistaken for a chunk. */
@@ -15976,9 +16007,9 @@ const parseChangesets = (tree, content, lines) => {
15976
16007
  const errors = [];
15977
16008
  for (const { heading, headingLine, body } of splitChunks(tree)) {
15978
16009
  const title = headingText$1(content, heading);
15979
- const { description, descriptionNodes, descriptionInline, files, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
16010
+ const { description, descriptionNodes, descriptionInline, files, answers, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
15980
16011
  errors.push(...bodyErrors);
15981
- if (files.length === 0 && body.some((n) => taskItems(n).length > 0)) errors.push({
16012
+ if (files.length === 0 && boxed) errors.push({
15982
16013
  message: `Chunk "${title}" has no file pointers`,
15983
16014
  line: headingLine,
15984
16015
  range: nodeRange(heading)
@@ -15989,7 +16020,8 @@ const parseChangesets = (tree, content, lines) => {
15989
16020
  descriptionNodes,
15990
16021
  descriptionInline,
15991
16022
  files,
15992
- headingLine
16023
+ headingLine,
16024
+ ...answers === void 0 ? {} : { answers }
15993
16025
  });
15994
16026
  }
15995
16027
  if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" });
@@ -16070,7 +16102,9 @@ const isHunkPointerItem = (content, item) => {
16070
16102
  * clear, so a caller can skip the write and leave the mtime untouched.
16071
16103
  */
16072
16104
  const reviewClearTicks = (content) => {
16073
- const offsets = taskItems(parseMarkdown(content)).filter((item) => item.checked === true && isHunkPointerItem(content, item)).map((item) => checkboxOffset(content, item)).filter((o) => o !== void 0).sort((a, b) => a - b);
16105
+ const tree = parseMarkdown(content);
16106
+ const answers = new Set(splitChunks(tree).flatMap(({ body }) => answerListOf(content, body)?.list.children ?? []));
16107
+ const offsets = taskItems(tree).filter((item) => item.checked === true && !answers.has(item) && isHunkPointerItem(content, item)).map((item) => checkboxOffset(content, item)).filter((o) => o !== void 0).sort((a, b) => a - b);
16074
16108
  if (offsets.length === 0) return content;
16075
16109
  let result = "";
16076
16110
  let cursor = 0;
@@ -16504,6 +16538,55 @@ const reviewRisks = (content) => parseReviewDoc(content).changesets.flatMap((chu
16504
16538
  id: `risk-${i + 1}`,
16505
16539
  ...r
16506
16540
  }));
16541
+ const CHOICES = [
16542
+ "fix",
16543
+ "wontfix",
16544
+ "answer"
16545
+ ];
16546
+ const decisionOf = (chunk) => {
16547
+ if (chunk.answers?.length !== 3) return void 0;
16548
+ const ticked = chunk.answers.flatMap((o, i) => o.checked ? [i] : []);
16549
+ if (ticked.length !== 1) return void 0;
16550
+ const i = ticked[0];
16551
+ const text = i === 2 ? chunk.answers[2].text : "";
16552
+ if (i === 2 && text === "") return void 0;
16553
+ return {
16554
+ anchor: chunk.title,
16555
+ body: chunk.description,
16556
+ choice: CHOICES[i],
16557
+ text
16558
+ };
16559
+ };
16560
+ /**
16561
+ * Notes whose answer boxes do not hold exactly one tick (a ticked free-text
16562
+ * slot filled in), with their 1-based heading line and why; and, given the
16563
+ * file `before` the edit, every note whose boxes were deleted.
16564
+ */
16565
+ const unansweredNotes = (content, before) => {
16566
+ const now = parseReviewDoc(content).changesets;
16567
+ const boxed = new Set(now.filter((c) => c.answers !== void 0).map((c) => c.title));
16568
+ const removed = (before === void 0 ? [] : parseReviewDoc(before).changesets).filter((c) => c.answers !== void 0 && !boxed.has(c.title)).map((c) => ({
16569
+ title: c.title,
16570
+ line: 0,
16571
+ reason: "its answer boxes were deleted — restore them"
16572
+ }));
16573
+ return [...now.filter((c) => c.answers !== void 0 && decisionOf(c) === void 0).map((c) => ({
16574
+ title: c.title,
16575
+ line: c.headingLine + 1,
16576
+ reason: c.answers.length === 3 ? "tick exactly one box" : `it has ${c.answers.length} boxes — restore the three gtd wrote`
16577
+ })), ...removed];
16578
+ };
16579
+ /** Every answer in `after` that `before` did not already hold for the same note, in document order. */
16580
+ const reviewDecisions = (before, after) => {
16581
+ const prior = new Map(parseReviewDoc(before).changesets.map((c) => [c.title, c]));
16582
+ return parseReviewDoc(after).changesets.flatMap((chunk) => {
16583
+ const decision = decisionOf(chunk);
16584
+ if (decision === void 0) return [];
16585
+ const was = prior.get(chunk.title);
16586
+ const old = was === void 0 ? void 0 : decisionOf(was);
16587
+ return old?.choice === decision.choice && old.text === decision.text ? [] : [decision];
16588
+ });
16589
+ };
16507
16590
  //#endregion
16508
16591
  //#region src/steering/CodeThreads.ts
16509
16592
  const TOKENS = [
@@ -16635,6 +16718,13 @@ const collect = async (p, capture, round, requested) => {
16635
16718
  }
16636
16719
  return refuse(p.refusals.unmatched);
16637
16720
  };
16721
+ /** Refuse while a note's answer boxes hold anything but exactly one tick, or a note's boxes were deleted. */
16722
+ const requireDecisions = (file, reviewed) => {
16723
+ const open = unansweredNotes(read(file) ?? "", changesSince(reviewed).get(file)?.before);
16724
+ if (open.length === 0) return;
16725
+ const list = open.map((n) => ` - ${file}${n.line > 0 ? `:${n.line}` : ""}: ${n.title} — ${n.reason}`).join("\n");
16726
+ refuse(`gtd land: answer-completeness: ${open.length} note(s) in ${file} not answered — tick exactly one box per note: fix as suggested, won't fix, or your own answer written over _your answer_:\n${list}`);
16727
+ };
16638
16728
  const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16639
16729
  /**
16640
16730
  * A review with ticks and THREAD footnotes (definition plus markers) removed:
@@ -16654,7 +16744,7 @@ const notesOf = (text) => {
16654
16744
  * commit of the last collecting turn, `"missing"` when the round left no
16655
16745
  * review, or `undefined` when no such turn ran.
16656
16746
  */
16657
- const converse = async (p, base, collectedBefore) => {
16747
+ const converse = async (p, base, reviewed, collectedBefore) => {
16658
16748
  let collectedAt = collectedBefore;
16659
16749
  for (;;) {
16660
16750
  await p.awaitReview(base);
@@ -16664,24 +16754,47 @@ const converse = async (p, base, collectedBefore) => {
16664
16754
  return "missing";
16665
16755
  }
16666
16756
  requireThreadsClosed(p.file);
16667
- if (!hasThreadFor("agent", p.file)) return collectedAt;
16757
+ if (!hasThreadFor("agent", p.file)) {
16758
+ requireDecisions(p.file, reviewed);
16759
+ return collectedAt;
16760
+ }
16668
16761
  await p.collecting(p.captures.edits(head$1()), "reply");
16669
16762
  collectedAt = head$1();
16670
16763
  requireReplies(p.file);
16671
- if (!hasThreadFor("human", p.file)) return collectedAt;
16672
- }
16673
- };
16674
- /** Judge every note, then run each verdict's route: answers, then nit fixes, then deferrals, then the edits' lap. */
16675
- const routeNotes = async (p, notes, r) => {
16676
- const verdicts = await p.triage(notes);
16677
- const of = (v) => notes.filter((n) => (verdicts.get(n.id) ?? "edit") === v);
16678
- const [edits, questions, nits, deferred] = [
16679
- of("edit"),
16680
- of("question"),
16681
- of("nit"),
16682
- of("defer")
16683
- ];
16684
- if (edits.length + questions.length + nits.length + deferred.length === 0) return r.unfolded();
16764
+ if (!hasThreadFor("human", p.file) && unansweredNotes(read(p.file) ?? "", changesSince(reviewed).get(p.file)?.before).length === 0) return collectedAt;
16765
+ }
16766
+ };
16767
+ const decisionNote = (d, id, text) => ({
16768
+ id,
16769
+ kind: "chunk",
16770
+ anchor: d.anchor,
16771
+ before: d.body,
16772
+ text,
16773
+ forced: /^!(\s|$)/.test(text)
16774
+ });
16775
+ /** One `Gtd-Mark` value per line: a control character would make mark() throw on every replay. */
16776
+ const oneLine = (text) => text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim() || "untitled";
16777
+ /** Tell the policy, and mark each won't-fix on the gate's commit. */
16778
+ const record$2 = (p, decisions) => {
16779
+ p.decided?.(decisions);
16780
+ for (const d of decisions) if (d.choice === "wontfix") mark("wontfix", oneLine(d.anchor));
16781
+ };
16782
+ /**
16783
+ * Judge every note, then run each verdict's route: answers, then nit fixes,
16784
+ * then the edits' lap. Of the human's `decisions`, a fix joins the nit batch
16785
+ * unjudged, an own answer is judged like a note, and a won't-fix is dropped
16786
+ * with a `Gtd-Mark` on the gate's commit.
16787
+ */
16788
+ const routeNotes = async (p, notes, r, decisions = []) => {
16789
+ record$2(p, decisions);
16790
+ const of = (c) => decisions.filter((d) => d.choice === c);
16791
+ const fixes = of("fix").map((d, i) => decisionNote(d, `fix-${i + 1}`, d.body));
16792
+ const judged = [...notes, ...of("answer").map((d, i) => decisionNote(d, `answer-${i + 1}`, d.text))];
16793
+ const verdicts = judged.length === 0 ? /* @__PURE__ */ new Map() : await p.triage(judged);
16794
+ const ofVerdict = (v) => judged.filter((n) => (verdicts.get(n.id) ?? "edit") === v);
16795
+ const [edits, questions] = [ofVerdict("edit"), ofVerdict("question")];
16796
+ const nits = [...ofVerdict("nit"), ...fixes];
16797
+ if (edits.length + questions.length + nits.length === 0) return r.unfolded();
16685
16798
  let answeredAt;
16686
16799
  if (questions.length > 0) {
16687
16800
  await p.answerQuestions(questions);
@@ -16691,7 +16804,6 @@ const routeNotes = async (p, notes, r) => {
16691
16804
  await p.fixNits(nits);
16692
16805
  await p.keepGreen();
16693
16806
  }
16694
- if (deferred.length > 0) await p.defer(deferred);
16695
16807
  if (edits.length > 0) {
16696
16808
  await r.close();
16697
16809
  return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt), "triaged", {
@@ -16713,8 +16825,8 @@ const routeNotes = async (p, notes, r) => {
16713
16825
  };
16714
16826
  /** A code edit, not a thread-only change: lines that only add, answer or remove code threads don't count. */
16715
16827
  const isCodeEdit = (p, c) => p.isCode(c.path) && stripCodeThreads(c.path, c.before ?? "") !== stripCodeThreads(c.path, c.after ?? "");
16716
- /** Whether anything but thread edits and ticks landed in the review file since `commit`. */
16717
- const notedSince = (p, commit) => changesSince(commit).some((c) => c.path === p.file && notesOf(c.before ?? "") !== notesOf(read(p.file) ?? ""));
16828
+ /** Whether a note or an answer, not just thread edits and pointer ticks, landed in the review file since `commit`. */
16829
+ const notedSince = (p, commit) => changesSince(commit).some((c) => c.path === p.file && (notesOf(c.before ?? "") !== notesOf(read(p.file) ?? "") || reviewDecisions(c.before ?? "", read(p.file) ?? "").length > 0));
16718
16830
  /**
16719
16831
  * What the in-loop collecting turn left: `folded` when it wrote `feedback`
16720
16832
  * (those folds need their lap whatever is judged later), `settled` when only
@@ -16728,9 +16840,9 @@ const settledAfter = (p, collectedAt) => ({
16728
16840
  folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
16729
16841
  settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
16730
16842
  });
16731
- const finishNotes = async (p, before, reopened, ctx) => {
16732
- const notes = reviewNotes(before, read(p.file) ?? "");
16733
- if (notes.length > 0) return routeNotes(p, notes, ctx);
16843
+ const finishNotes = async (p, before, decisions, reopened, ctx) => {
16844
+ const notes = reviewNotes(before ?? "", read(p.file) ?? "");
16845
+ if (notes.length + decisions.length > 0) return routeNotes(p, notes, ctx, decisions);
16734
16846
  if (reopened) return ctx.unfolded();
16735
16847
  await ctx.close();
16736
16848
  return ctx.outcome(await collect(p, p.captures.notes(ctx.round), "notes"));
@@ -16740,6 +16852,11 @@ const toOutcome = (verdict, rest) => verdict === "signoff" ? { verdict } : {
16740
16852
  ...rest
16741
16853
  };
16742
16854
  const isReopened = (p, r) => r.settled && !r.folded && p.refusals.dropped !== void 0;
16855
+ /** A carried answer is unchanged, never routed twice; one ticked beside a thread still routes once the thread has settled. */
16856
+ const decisionsSince = (p, since) => {
16857
+ const after = read(p.file) ?? "";
16858
+ return reviewDecisions(since.get(p.file)?.before ?? after, after);
16859
+ };
16743
16860
  const finish = async (p, reviewed, collectedAt) => {
16744
16861
  const round = head$1();
16745
16862
  const since = changesSince(reviewed);
@@ -16760,15 +16877,17 @@ const finish = async (p, reviewed, collectedAt) => {
16760
16877
  settled,
16761
16878
  folded
16762
16879
  });
16763
- if (settled && !reopened || edited.length === 0 && !noted) return unfolded();
16880
+ const decisions = decisionsSince(p, since);
16881
+ if (settled && !reopened && decisions.length === 0 || edited.length === 0 && !noted) return unfolded();
16764
16882
  if (edited.length > 0) {
16883
+ record$2(p, decisions);
16765
16884
  await close();
16766
16885
  return outcome(await collect(p, p.captures.edits(round, reopened ? reviewed : void 0), "edits", {
16767
16886
  notes: [],
16768
16887
  paths: edited.map((c) => c.path)
16769
16888
  }));
16770
16889
  }
16771
- return finishNotes(p, since.get(p.file)?.before ?? "", reopened, {
16890
+ return finishNotes(p, since.get(p.file)?.before, decisions, reopened, {
16772
16891
  round,
16773
16892
  close,
16774
16893
  outcome,
@@ -16798,7 +16917,7 @@ const review$2 = async (base, p) => {
16798
16917
  let reviewed = head$1();
16799
16918
  let collectedAt;
16800
16919
  for (;;) {
16801
- collectedAt = await converse(p, base, collectedAt);
16920
+ collectedAt = await converse(p, base, reviewed, collectedAt);
16802
16921
  if (collectedAt === "missing") break;
16803
16922
  const result = await finish(p, reviewed, collectedAt);
16804
16923
  if (!("next" in result)) return result;
@@ -17278,9 +17397,9 @@ const SUMMARY_TITLES = [
17278
17397
  const unboxed = (body) => body.replace(/^(\s*(?:>\s*)*(?:[-*+]|\d+[.)])\s+)\[[ xX]\]\s?/gm, "$1");
17279
17398
  const short = (hash) => hash.slice(0, 7);
17280
17399
  /**
17281
- * \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, then the fixed and
17282
- * noise lists, with no tick boxes. Chunk titles are made unique — notes are
17283
- * matched to their chunk by title.
17400
+ * \`.gtd/REVIEW.md\` as gtd writes it: one chunk per note, each ending in its
17401
+ * answer boxes, then the fixed and noise lists. Chunk titles are made unique —
17402
+ * notes and answers are matched to their chunk by title.
17284
17403
  */
17285
17404
  const composedReview = (base, report) => {
17286
17405
  const used = new Set(SUMMARY_TITLES);
@@ -17291,11 +17410,16 @@ const composedReview = (base, report) => {
17291
17410
  return name;
17292
17411
  };
17293
17412
  const chunk = (title, body) => `## ${title}\n\n${body.trim()}`;
17413
+ const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${ANSWER_FIX}\n- [ ] ${ANSWER_WONT_FIX}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
17414
+ const notes = report.notes.map((n) => ({
17415
+ ...n,
17416
+ title: unique(n.title)
17417
+ })).filter((n) => report.answered?.has(n.title) !== true);
17294
17418
  const weakened = (report.weakened ?? []).flatMap(({ path, lines }) => lines.map((a) => `- ${path} — ${a.scenario} — ${a.line}`));
17295
17419
  const chunks = [
17296
- ...report.nitFix === void 0 ? [] : [chunk("Fixed from your notes", `Your nits were fixed in ${short(report.nitFix)}; the list below is from before.`)],
17420
+ ...report.nitFix === void 0 ? [] : [chunk("Fixed from your notes", `Your nits and fix-as-suggested notes were worked on in ${short(report.nitFix)}; the notes you answered are left out — check the diff.`)],
17297
17421
  ...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
17298
- ...report.notes.map((n) => chunk(unique(n.title), n.unresolved === true ? `_The fixer left this unresolved._\n\n${unboxed(n.body)}` : n.unclear === true ? `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${unboxed(n.body)}` : unboxed(n.body) || "_No detail given._")),
17422
+ ...notes.map((n) => chunk(n.title, `${n.unresolved === true ? `_The fixer left this unresolved._\n\n${unboxed(n.body)}` : n.unclear === true ? `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${unboxed(n.body)}` : unboxed(n.body) || "_No detail given._"}\n\n${answerBoxes}`)),
17299
17423
  ...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
17300
17424
  ...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
17301
17425
  ];
@@ -17378,10 +17502,6 @@ ${stateFileRules}
17378
17502
  ${footnoteFoldIn}${codeThreadReplies()}
17379
17503
  - The only state file this turn touches is \`.gtd/REQUIREMENTS.md\`
17380
17504
  — no other files for notes or output
17381
- - Lines of a \`## Deferred review notes\` section of \`.gtd/TODO.md\`
17382
- that the start diff does not touch are parked by review, never folded;
17383
- the human's edits to such a line (or its removal) are input like any
17384
- other part of the start diff
17385
17505
  - \`.gtd/TODO.md\` is the likely home of the sketch that started
17386
17506
  this process — the human's input, folded into the concerns
17387
17507
  below like any other part of the start diff; never a state
@@ -17674,8 +17794,12 @@ const buildReviewAwaitReviewMessage = (base) => `\`.gtd/REVIEW.md\` holds what t
17674
17794
  per finding that needs your judgement — it questions a criterion or the
17675
17795
  approach, or the fixer could not resolve it — then, for reference only, what
17676
17796
  was **fixed automatically** (with the fixing commit) and what was **dropped
17677
- as noise**. Nothing there needs an answer; disagreeing with a fix or a drop
17678
- means writing a note of your own.
17797
+ as noise**. Each finding chunk ends in three boxes — tick exactly one:
17798
+ \`fix as suggested\` (fixed in the nit batch below), \`won't fix\` (dropped,
17799
+ recorded in the commit), or write your own answer over \`_your answer_\` and
17800
+ tick it (judged like a note). Every landing here is refused until each
17801
+ finding holds exactly one tick. The fixed and noise lists need no answer;
17802
+ disagreeing with a fix or a drop means writing a note of your own.
17679
17803
 
17680
17804
  Review the diff yourself, with whatever tool you like — gtd checks
17681
17805
  nothing out and touches no ref; \`gtd base\` prints this same hash any
@@ -17685,8 +17809,8 @@ working tree:
17685
17809
  git diff ${base}
17686
17810
  When you've been through the whole diff, run \`gtd land\`:
17687
17811
 
17688
- - **Sign off** — leave no comment — no note in
17689
- \`.gtd/REVIEW.md\`, no code edit — to close the process.
17812
+ - **Sign off** — answer every finding, leave no other comment — no note
17813
+ in \`.gtd/REVIEW.md\`, no code edit — to close the process.
17690
17814
  Every turn commit stays on the
17691
17815
  branch; run \`gtd summary\` afterward for a closing-message
17692
17816
  prompt.
@@ -17833,27 +17957,6 @@ const wrapItem = (item) => {
17833
17957
  } else line = line === "" ? word : `${line} ${word}`;
17834
17958
  return [...out, line];
17835
17959
  };
17836
- /** Appends one section of `notes` to `.gtd/TODO.md`, wrapped the way oxfmt writes it; an existing file is only ever appended to. */
17837
- const deferNotesScript = (notes, round) => {
17838
- return `mkdir -p .gtd
17839
- if [ -s .gtd/TODO.md ]; then
17840
- [ -z "$(tail -c1 .gtd/TODO.md)" ] || printf '\\n' >> .gtd/TODO.md
17841
- printf '\\n' >> .gtd/TODO.md
17842
- fi
17843
- printf '%s\\n' ${[
17844
- `## Deferred review notes (${round.slice(0, 7)})`,
17845
- "",
17846
- ...notes.flatMap((n) => wrapItem(`- ${noteHead(n)}: ${n.text.replace(/\s+/g, " ")}`))
17847
- ].map(quote).join(" ")} >> .gtd/TODO.md
17848
- `;
17849
- };
17850
- /** The gate right after a deferral: landing clean signs off without these notes. */
17851
- const buildReviewAwaitAfterDeferMessage = (base, deferred) => `${buildReviewAwaitReviewMessage(base)}
17852
- Deferred to \`.gtd/TODO.md\` just now, NOT acted on in this process — landing
17853
- clean signs off without them:
17854
-
17855
- ${deferred.map((n) => `- ${noteHead(n)}: ${n.text.replace(/\s*\n\s*/g, " ")}`).join("\n")}
17856
- `;
17857
17960
  const buildReviewTriageMessage = () => `Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
17858
17961
  footnote, or prose under a chunk) with one verdict: \`edit\` (a change
17859
17962
  request), \`question\`, \`nit\` (a small fix needing no re-plan) or
@@ -17863,16 +17966,18 @@ default; a skipped judgment never dismisses a note).
17863
17966
 
17864
17967
  Also judge each note's severity, one id per note: \`note-<n>.security\`,
17865
17968
  \`note-<n>.reversibility\`, \`note-<n>.blastRadius\` and
17866
- \`note-<n>.newConcept\`.
17969
+ \`note-<n>.newConcept\`. An own answer under a finding's boxes is judged
17970
+ the same way, keyed \`answer-<n>\`.
17867
17971
  `;
17868
17972
  const closedFold = (round, target) => round === "edits" ? `- Fold every hand-edit and every note the human left that round into
17869
- \`${target}\`
17973
+ \`${target}\` — a note ticked \`fix as suggested\` or answered in its
17974
+ own words included; one ticked \`won't fix\` is dropped
17870
17975
  ` : round === "notes" ? `- Fold each note that asks for a change into \`${target}\`
17871
17976
  ` : `- Fold each \`edit\` note the capture lists, no other note, and every
17872
17977
  hand-edit, into \`${target}\`
17873
17978
  `;
17874
17979
  /** `refactor` retargets the prompt at `.gtd/REFACTOR.md` and fences it to the strays. A concern outside the fence is not folded; if nothing at all is written the landing is refused, but a partly buildable round drops the rest silently. */
17875
- const buildReviewCollectingPrompt = (capture, refactor) => {
17980
+ const buildReviewCollectingPrompt = (capture, refactor, round) => {
17876
17981
  const target = refactor ? ".gtd/REFACTOR.md" : ".gtd/REQUIREMENTS.md";
17877
17982
  const fence = refactor ? `- The next refactor may change only these paths, plus an existing test
17878
17983
  file's call sites, and never a flag read:
@@ -17947,12 +18052,16 @@ ${refactor ? refactor.round === "reply" ? `- This turn replies inside \`.gtd/REV
17947
18052
  ${closedFold(refactor.round, target)}` : `- This turn writes \`${target}\` (the folded concerns) and
17948
18053
  replies inside \`.gtd/REVIEW.md\` (thread replies only; the file stays
17949
18054
  in the tree) — you classify, you do not build
17950
- - Fold every concluded thread, ticked answer and hand-edit — and each
17951
- \`edit\` note the capture lists, no other note — into
17952
- \`${target}\` and delete the folded threads from
17953
- \`.gtd/REVIEW.md\`; append one \`- A:\` reply to each thread
17954
- whose last entry is a \`- H:\` question
17955
- `}- When the capture names an answering commit, carry each answered
18055
+ - Fold every concluded thread and hand-edit — and each \`edit\` note
18056
+ the capture lists, no other note — into \`${target}\` and
18057
+ delete the folded threads from \`.gtd/REVIEW.md\`; append one \`- A:\`
18058
+ reply to each thread whose last entry is a \`- H:\` question
18059
+ ${round === "edits" ? `- A hand-edit round routes no answer box itself: fold each note
18060
+ ticked \`fix as suggested\` or answered in its own words, too; drop one
18061
+ ticked \`won't fix\`
18062
+ ` : `- Never fold a note's ticked answer box the capture does not list: gtd
18063
+ routes those itself
18064
+ `}`}- When the capture names an answering commit, carry each answered
17956
18065
  question into \`${target}\`'s \`## Answered Questions\` as
17957
18066
  \`### <the note>\` plus its answer — the lap replaces \`.gtd/REVIEW.md\`
17958
18067
  - Keep capture order, and carry each note's \`(tier: reason)\`
@@ -38437,7 +38546,7 @@ const fixNits = (notes, access, rules) => {
38437
38546
  }, () => fixing("fixing"));
38438
38547
  };
38439
38548
  /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
38440
- const collecting = (capture) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture), {
38549
+ const collecting = (capture, round) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, void 0, round), {
38441
38550
  label: "Collecting your feedback",
38442
38551
  file: REQUIREMENTS,
38443
38552
  mode: "qa",
@@ -38456,11 +38565,6 @@ const refactorCollecting = (capture, round, strays) => agentWithSkills("review.c
38456
38565
  system: reviewerSystem(),
38457
38566
  allowEmpty: true
38458
38567
  });
38459
- /** `build.review.defer`: append `notes` to `.gtd/TODO.md` as one section; the step's landing is the commit. */
38460
- const deferNotes = (notes, round) => run("review.defer", deferNotesScript(notes, round), {
38461
- label: "Deferring could-notes to TODO.md",
38462
- file: ".gtd/TODO.md"
38463
- });
38464
38568
  //#endregion
38465
38569
  //#region src/workflows/feature/planning.ts
38466
38570
  /**
@@ -38501,6 +38605,13 @@ const verdictQuestion = (note) => ({
38501
38605
  instructions: `Classify the human's note on review item "${note.anchor}" (in .gtd/REVIEW.md). Its evidence holds the anchor, the reviewer's text and the human's text.`,
38502
38606
  criteria: "edit: a request to change behaviour, design or scope — anything needing a plan, or any note you are unsure about. question: asks something and wants an answer, with no change requested. nit: a small, local, unambiguous fix (naming, typo, formatting, comment wording) needing no re-plan. praise: an approving remark with nothing to do."
38503
38607
  });
38608
+ const TIER_ORDER = {
38609
+ critical: 0,
38610
+ should: 1,
38611
+ could: 2
38612
+ };
38613
+ /** Capture order: critical, should, could; within a tier one-way doors, then new concepts. */
38614
+ const captureKey = (s) => s === void 0 ? 2 : TIER_ORDER[s.tier] * 3 + s.rank;
38504
38615
  const SEVERITY_ANSWERS = {
38505
38616
  security: ["yes", "no"],
38506
38617
  reversibility: ["one-way", "two-way"],
@@ -38511,10 +38622,7 @@ const SEVERITY_ANSWERS = {
38511
38622
  ],
38512
38623
  newConcept: ["yes", "no"]
38513
38624
  };
38514
- /**
38515
- * Deferring is the risky direction: anything unanswered, unrecognised, cut or
38516
- * below `minP` is `critical`, never `could`.
38517
- */
38625
+ /** Anything unanswered, unrecognised, cut or below `minP` is `critical`, never `could`. */
38518
38626
  const severityOf = (note, judgment, minP) => {
38519
38627
  const ans = (q) => judgment.answers[`${note.id}.${q}`];
38520
38628
  const a = (q) => ans(q)?.answer;
@@ -38578,7 +38686,7 @@ const severityQuestions = (note) => [
38578
38686
  * nothing) plus four severity answers. Dismissing a note is the risky
38579
38687
  * direction, so only a confident non-`edit` verdict on uncut evidence counts;
38580
38688
  * a cut, unanswered or below-floor note is an `edit`. Severity answers on a
38581
- * non-`edit` note are ignored. A confident `could` edit becomes `defer`.
38689
+ * non-`edit` note are ignored; on an `edit` they order the collecting turn.
38582
38690
  */
38583
38691
  const triage$1 = (severities) => async (notes) => {
38584
38692
  const asked = notes.filter((n) => !n.forced);
@@ -38598,21 +38706,22 @@ const triage$1 = (severities) => async (notes) => {
38598
38706
  return new Map(notes.map((n) => {
38599
38707
  const verdict = n.forced ? void 0 : VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP));
38600
38708
  if (verdict !== void 0 && !truncated.includes(n.id)) return [n.id, verdict];
38601
- const severity = severityOf(n, judgment, severityMinP);
38602
- severities.set(n.id, severity);
38603
- return [n.id, severity.tier === "could" ? "defer" : "edit"];
38709
+ severities.set(n.id, severityOf(n, judgment, severityMinP));
38710
+ return [n.id, "edit"];
38604
38711
  }));
38605
38712
  };
38606
38713
  const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
38607
38714
  /**
38608
38715
  * gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
38609
38716
  * turn — and removes what the fixer left in `.gtd/QUALITY.md`. A re-review
38610
- * after fixed nits restores the doc as committed at `carry`, answers kept.
38717
+ * after fixed nits restores the doc as committed at `carry`, answers kept;
38718
+ * a fresh one leaves out the notes the human already answered.
38611
38719
  */
38612
- const composing = (base, report, carry, nitFix) => run("review.composing", carry === void 0 ? writeScript(REVIEW, composedReview(base, {
38720
+ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new Set()) => run("review.composing", carry === void 0 ? writeScript(REVIEW, composedReview(base, {
38613
38721
  ...report,
38614
38722
  weakened: removedAssertions(changesSince(base)),
38615
- nitFix
38723
+ nitFix,
38724
+ answered
38616
38725
  }), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
38617
38726
  label: "Composing the review",
38618
38727
  file: REVIEW,
@@ -38620,9 +38729,10 @@ const composing = (base, report, carry, nitFix) => run("review.composing", carry
38620
38729
  });
38621
38730
  /**
38622
38731
  * The quality lap's notes reach a human over everything since `base`, who
38623
- * signs off or comments, and each note is judged: edits go to another
38624
- * planning lap, questions are answered in place, nits fixed in one batch,
38625
- * praise dropped.
38732
+ * answers each one — fix as suggested, won't fix, or their own answer — and
38733
+ * may comment. A fix joins the nit batch, a won't-fix is dropped, and an own
38734
+ * answer or comment is judged: edits go to another planning lap, questions
38735
+ * are answered in place, nits fixed in one batch, praise dropped.
38626
38736
  */
38627
38737
  const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
38628
38738
  const { guard, guardRests, fixAccess, rules } = options;
@@ -38632,17 +38742,16 @@ const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
38632
38742
  unmatched: "gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing"
38633
38743
  };
38634
38744
  const severities = /* @__PURE__ */ new Map();
38635
- let deferred = [];
38636
38745
  let nitFix;
38746
+ const answered = /* @__PURE__ */ new Set();
38637
38747
  return review$2(base, {
38638
38748
  file: REVIEW,
38639
38749
  feedback: feedback.file,
38640
38750
  isCode: isCode$1,
38641
38751
  reviewing: (b, carry) => {
38642
- deferred = [];
38643
38752
  const fixedAt = nitFix;
38644
38753
  nitFix = void 0;
38645
- return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt);
38754
+ return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt, answered);
38646
38755
  },
38647
38756
  fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
38648
38757
  keepGreen: () => healthy(() => fixCheck(rules), {
@@ -38651,23 +38760,11 @@ const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
38651
38760
  guardRests,
38652
38761
  rules
38653
38762
  }),
38654
- awaitReview: (b) => {
38655
- const notes = deferred;
38656
- deferred = [];
38657
- return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes)) : awaitReview(b);
38658
- },
38763
+ awaitReview: (b) => awaitReview(b),
38659
38764
  reviewMissing,
38660
38765
  collecting: feedback.collecting,
38661
38766
  triage: triage$1(severities),
38662
- defer: (notes) => {
38663
- deferred = notes.map((n) => ({
38664
- id: n.id,
38665
- anchor: n.anchor,
38666
- text: n.text,
38667
- label: severities.get(n.id).reason
38668
- }));
38669
- return deferNotes(deferred, head$1());
38670
- },
38767
+ decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
38671
38768
  answerQuestions: answerReviewQuestions,
38672
38769
  fixNits: async (notes) => {
38673
38770
  await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
@@ -38680,8 +38777,9 @@ const review$1 = (base, escalations = { rounds: 0 }, options = {}) => {
38680
38777
  editNotes: (round, edits, answeredAt) => reviewEditNotesCapture(round, edits.map((n, i) => ({
38681
38778
  n,
38682
38779
  i,
38683
- s: severities.get(n.id)
38684
- })).sort((x, y) => Number(x.s?.tier === "should") - Number(y.s?.tier === "should") || (x.s?.rank ?? 2) - (y.s?.rank ?? 2) || x.i - y.i).map(({ n, s }) => ({
38780
+ s: severities.get(n.id),
38781
+ key: captureKey(severities.get(n.id))
38782
+ })).sort((x, y) => x.key - y.key || x.i - y.i).map(({ n, s }) => ({
38685
38783
  id: n.id,
38686
38784
  anchor: n.anchor,
38687
38785
  text: n.forced ? n.text.replace(/^!\s*/, "") : n.text,
@@ -39749,11 +39847,9 @@ const skills = (vars) => {
39749
39847
  const access = (vars) => ({
39750
39848
  design: { write: [] },
39751
39849
  "build.review": { write: [".gtd/REVIEW.md"] },
39752
- "build.review.defer": { write: [".gtd/TODO.md"] },
39753
39850
  "build.review.fix.nits": {},
39754
39851
  "build.review.fix.risks": {},
39755
39852
  "refactor.build.review": { write: [".gtd/REVIEW.md"] },
39756
- "refactor.build.review.defer": { write: [".gtd/TODO.md"] },
39757
39853
  ...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), () => ({ write: [] })),
39758
39854
  ...lensScopes("refactor.build.quality", lensesOf(vars.refactorQualityReviews ?? ""), () => ({ write: [] }))
39759
39855
  });
@@ -39900,4 +39996,4 @@ var workflows_default = feature;
39900
39996
  */
39901
39997
  const doors = {};
39902
39998
  //#endregion
39903
- export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, implementing, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unstableIds, unwind };
39999
+ export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeOut, closeReview, coder, collecting, criteriaOf, criterionIds, workflows_default as default, defaults, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, implementing, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, recheck, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, tail, threads, triage, unstableIds, unwind };