@pmelab/gtd 44.0.0 → 45.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +40 -9
  3. package/dist/chunks/{reserved-DJk_6-ju.mjs → reserved-ulcSkND6.mjs} +142 -5
  4. package/dist/chunks/{review-X7Jfz0dY.mjs → review-Da2ndnB1.mjs} +177 -7
  5. package/dist/chunks/{tail-5w5mxGTq.mjs → tail-BW030xz_.mjs} +1 -1
  6. package/dist/gtd.bundle.mjs +7 -5
  7. package/dist/testkit.mjs +7 -5
  8. package/dist/workflow-kit.mjs +22 -10
  9. package/dist/workflows/cleanup.mjs +2 -2
  10. package/dist/workflows/feature.mjs +3 -3
  11. package/dist/workflows/fix.mjs +2 -2
  12. package/dist/workflows/review.mjs +4 -15
  13. package/dist/workflows/rollout.mjs +1 -1
  14. package/dist/workflows/tests.mjs +193 -0
  15. package/dist/workflows.mjs +533 -40
  16. package/package.json +6 -1
  17. package/src/install/ConfigScaffold.ts +2 -1
  18. package/src/install/Install.ts +5 -4
  19. package/src/workflow-kit/review.test.ts +80 -2
  20. package/src/workflow-kit/review.ts +45 -11
  21. package/src/workflows/feature/review-answers.feature +12 -3
  22. package/src/workflows/feature/standards.feature +286 -0
  23. package/src/workflows/index.ts +2 -1
  24. package/src/workflows/review/review-door.feature +11 -2
  25. package/src/workflows/review/review.ts +3 -16
  26. package/src/workflows/shared/learn.test.ts +154 -0
  27. package/src/workflows/shared/learn.ts +206 -0
  28. package/src/workflows/shared/lenses.ts +5 -1
  29. package/src/workflows/shared/options.ts +22 -1
  30. package/src/workflows/shared/review.ts +29 -2
  31. package/src/workflows/shared/skills.test.ts +2 -0
  32. package/src/workflows/shared/skills.ts +3 -0
  33. package/src/workflows/shared/standards.test.ts +90 -0
  34. package/src/workflows/shared/standards.ts +156 -0
  35. package/src/workflows/shared/text.test.ts +58 -1
  36. package/src/workflows/shared/text.ts +33 -3
  37. package/src/workflows/shared/vars.ts +10 -0
  38. package/src/workflows/tests/index.ts +6 -0
  39. package/src/workflows/tests/tests-options.feature +52 -0
  40. package/src/workflows/tests/tests.feature +266 -0
  41. package/src/workflows/tests/tests.test.ts +51 -0
  42. package/src/workflows/tests/tests.ts +270 -0
@@ -16773,11 +16773,17 @@ const decisionNote = (d, id, text) => ({
16773
16773
  forced: /^!(\s|$)/.test(text)
16774
16774
  });
16775
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";
16776
+ const oneLine$1 = (text) => text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim() || "untitled";
16777
16777
  /** Tell the policy, and mark each won't-fix on the gate's commit. */
16778
16778
  const record$2 = (p, decisions) => {
16779
16779
  p.decided?.(decisions);
16780
- for (const d of decisions) if (d.choice === "wontfix") mark("wontfix", oneLine(d.anchor));
16780
+ for (const d of decisions) if (d.choice === "wontfix") mark("wontfix", oneLine$1(d.anchor));
16781
+ };
16782
+ const routed = (p, answers) => answers.filter((d) => p.settles?.(d) !== true);
16783
+ /** Record the routed answers on the gate's commit, then hand every answer to `learn`. */
16784
+ const recordAndLearn = async (p, decisions, answers) => {
16785
+ record$2(p, decisions);
16786
+ if (answers.length > 0 && p.learn !== void 0) await p.learn(answers);
16781
16787
  };
16782
16788
  /**
16783
16789
  * Judge every note, then run each verdict's route: answers, then nit fixes,
@@ -16785,8 +16791,8 @@ const record$2 = (p, decisions) => {
16785
16791
  * unjudged, an own answer is judged like a note, and a won't-fix is dropped
16786
16792
  * with a `Gtd-Mark` on the gate's commit.
16787
16793
  */
16788
- const routeNotes = async (p, notes, r, decisions = []) => {
16789
- record$2(p, decisions);
16794
+ const routeNotes = async (p, notes, r, decisions = [], answers = decisions) => {
16795
+ await recordAndLearn(p, decisions, answers);
16790
16796
  const of = (c) => decisions.filter((d) => d.choice === c);
16791
16797
  const fixes = of("fix").map((d, i) => decisionNote(d, `fix-${i + 1}`, d.body));
16792
16798
  const judged = [...notes, ...of("answer").map((d, i) => decisionNote(d, `answer-${i + 1}`, d.text))];
@@ -16840,9 +16846,8 @@ const settledAfter = (p, collectedAt) => ({
16840
16846
  folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
16841
16847
  settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
16842
16848
  });
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);
16849
+ const finishNotes = async (p, notes, answers, reopened, ctx) => {
16850
+ if (notes.length + answers.length > 0) return routeNotes(p, notes, ctx, routed(p, answers), answers);
16846
16851
  if (reopened) return ctx.unfolded();
16847
16852
  await ctx.close();
16848
16853
  return ctx.outcome(await collect(p, p.captures.notes(ctx.round), "notes"));
@@ -16857,6 +16862,7 @@ const decisionsSince = (p, since) => {
16857
16862
  const after = read(p.file) ?? "";
16858
16863
  return reviewDecisions(since.get(p.file)?.before ?? after, after);
16859
16864
  };
16865
+ const notesSince = (p, since) => reviewNotes(since.get(p.file)?.before ?? "", read(p.file) ?? "");
16860
16866
  const finish = async (p, reviewed, collectedAt) => {
16861
16867
  const round = head$1();
16862
16868
  const since = changesSince(reviewed);
@@ -16877,23 +16883,29 @@ const finish = async (p, reviewed, collectedAt) => {
16877
16883
  settled,
16878
16884
  folded
16879
16885
  });
16880
- const decisions = decisionsSince(p, since);
16881
- if (settled && !reopened && decisions.length === 0 || edited.length === 0 && !noted) return unfolded();
16886
+ const answers = decisionsSince(p, since);
16887
+ if (settled && !reopened && answers.length === 0 || edited.length === 0 && !noted) return unfolded();
16888
+ const notes = notesSince(p, since);
16882
16889
  if (edited.length > 0) {
16883
- record$2(p, decisions);
16890
+ await recordAndLearn(p, routed(p, answers), answers);
16884
16891
  await close();
16885
16892
  return outcome(await collect(p, p.captures.edits(round, reopened ? reviewed : void 0), "edits", {
16886
16893
  notes: [],
16887
16894
  paths: edited.map((c) => c.path)
16888
16895
  }));
16889
16896
  }
16890
- return finishNotes(p, since.get(p.file)?.before, decisions, reopened, {
16897
+ return finishNotes(p, notes, answers, reopened, {
16891
16898
  round,
16892
16899
  close,
16893
16900
  outcome,
16894
16901
  unfolded
16895
16902
  });
16896
16903
  };
16904
+ /** A sign-off while something waits on the human writes the review again instead. */
16905
+ const unlessPending = (p, result) => "verdict" in result && result.verdict === "signoff" && p.pending?.() === true ? {
16906
+ next: "rereview",
16907
+ carry: void 0
16908
+ } : result;
16897
16909
  /** Write the review; if it marks risks, fix them, keep green, and write it again — once, so the re-review's own risks reach the human unfixed. */
16898
16910
  const reviewOnce = async (p, base, carry) => {
16899
16911
  await p.reviewing(base, carry);
@@ -16919,7 +16931,7 @@ const review$1 = async (base, p) => {
16919
16931
  for (;;) {
16920
16932
  collectedAt = await converse(p, base, reviewed, collectedAt);
16921
16933
  if (collectedAt === "missing") break;
16922
- const result = await finish(p, reviewed, collectedAt);
16934
+ const result = unlessPending(p, await finish(p, reviewed, collectedAt));
16923
16935
  if (!("next" in result)) return result;
16924
16936
  if (result.next === "rereview") {
16925
16937
  carry = result.carry;
@@ -17072,6 +17084,108 @@ const requireFlag = (workflow, registryPath, name, defaultOn) => {
17072
17084
  if (entry.defaultOn !== defaultOn) return refuse(defaultOn ? `gtd: ${workflow}: flag "${name}" is not default-on — roll it out first` : `gtd: ${workflow}: flag "${name}" is already default-on`);
17073
17085
  };
17074
17086
  //#endregion
17087
+ //#region src/workflows/shared/standards.ts
17088
+ const STANDARDS = ".gtd/STANDARDS.md";
17089
+ const UNDECIDED = "Undecided";
17090
+ const oneLine = (text) => text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim();
17091
+ const ITEM$1 = /^(?:[-*+]|\d+[.)])\s+(.*)$/;
17092
+ const CONTRADICTS = /^\s+[-*+]\s+contradicts:\s*(.*)$/i;
17093
+ const wrapped = (item, line) => {
17094
+ const olds = item.olds;
17095
+ if (olds !== void 0 && olds.length > 0) olds[olds.length - 1] += ` ${line.trim()}`;
17096
+ else item.text += ` ${line.trim()}`;
17097
+ };
17098
+ /** What tells two standards apart: not the emphasis or spacing a formatter may rewrite. */
17099
+ const standardKey = (text) => oneLine(text.replace(/[*_`\\]/g, "")).toLowerCase();
17100
+ const toStandards = (items) => {
17101
+ const seen = /* @__PURE__ */ new Set();
17102
+ const fresh = (text) => {
17103
+ const key = standardKey(text);
17104
+ if (key === "" || seen.has(key)) return false;
17105
+ seen.add(key);
17106
+ return true;
17107
+ };
17108
+ const decided = items.filter((i) => i.olds === void 0).map((i) => oneLine(i.text));
17109
+ const undecided = items.flatMap((i) => i.olds === void 0 ? [] : [{
17110
+ text: oneLine(i.text),
17111
+ olds: i.olds.map(oneLine).filter((o) => o !== "")
17112
+ }]);
17113
+ return {
17114
+ decided: decided.filter(fresh),
17115
+ undecided: undecided.filter((c) => fresh(c.text))
17116
+ };
17117
+ };
17118
+ /**
17119
+ * Every top-level bullet is one standard, those under `## Undecided` waiting
17120
+ * on the human; a hand-edited file may group the rest under any heading.
17121
+ */
17122
+ const parseStandards = (text) => {
17123
+ const items = [];
17124
+ let pending = false;
17125
+ let last;
17126
+ for (const line of text.split("\n")) {
17127
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
17128
+ const item = ITEM$1.exec(line);
17129
+ const old = CONTRADICTS.exec(line);
17130
+ if (heading !== null) {
17131
+ pending = heading[1].length > 1 && heading[2].trim() === UNDECIDED;
17132
+ last = void 0;
17133
+ } else if (item !== null) {
17134
+ last = pending ? {
17135
+ text: item[1],
17136
+ olds: []
17137
+ } : { text: item[1] };
17138
+ items.push(last);
17139
+ } else if (old !== null && last?.olds !== void 0) last.olds.push(old[1]);
17140
+ else if (/^\s+\S/.test(line) && last !== void 0) wrapped(last, line);
17141
+ else if (line.trim() !== "") last = void 0;
17142
+ }
17143
+ return toStandards(items);
17144
+ };
17145
+ const renderStandards = ({ decided, undecided }) => {
17146
+ return `${[
17147
+ "# Standards",
17148
+ "Kept from your review answers for every agent turn. One standard per bullet.",
17149
+ ...decided.length === 0 ? [] : [decided.map((s) => `- ${s}`).join("\n")],
17150
+ ...undecided.length === 0 ? [] : [`## ${UNDECIDED}`, undecided.map((c) => [`- ${c.text}`, ...c.olds.map((o) => ` - contradicts: ${o}`)].join("\n")).join("\n")]
17151
+ ].join("\n\n")}\n`;
17152
+ };
17153
+ const hash$1 = (raw) => {
17154
+ const text = standardKey(raw);
17155
+ let h = 2166136261;
17156
+ for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 16777619);
17157
+ return (h >>> 0).toString(16).padStart(8, "0");
17158
+ };
17159
+ const CONFLICT_TITLE = /^Standard conflict ([0-9a-f]{8})$/;
17160
+ const conflictTitle = (c) => `Standard conflict ${hash$1(c.text)}`;
17161
+ const conflictFor = (standards, title) => {
17162
+ const id = CONFLICT_TITLE.exec(title)?.[1];
17163
+ return id === void 0 ? void 0 : standards.undecided.find((c) => hash$1(c.text) === id);
17164
+ };
17165
+ const isConflictTitle = (title) => CONFLICT_TITLE.test(title);
17166
+ const KEEP_NEW = "keep the new one (drop the old)";
17167
+ const KEEP_OLD = "keep the old one (discard the new)";
17168
+ /** A conflict's chunk body; its three boxes read as fix / won't fix / own answer by position. */
17169
+ const conflictBody = (c) => [...c.olds.length === 0 ? [`A standard learned from your answers contradicted ones another answer since replaced in \`${STANDARDS}\`. Keep it, discard it, or reword it.`, `New: ${c.text}`] : [
17170
+ `A standard learned from your answers contradicts ${c.olds.length === 1 ? "one" : "some"} already in \`${STANDARDS}\`. Your own answer replaces both with its wording.`,
17171
+ `New: ${c.text}`,
17172
+ `Old:\n\n${c.olds.map((o) => `- ${o}`).join("\n")}`
17173
+ ], `${ANSWER_LEAD}\n\n- [ ] ${KEEP_NEW}\n- [ ] ${KEEP_OLD}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`].join("\n\n");
17174
+ /** What every agent turn is told while `.gtd/STANDARDS.md` holds decided standards. */
17175
+ const standardsBlock = (text) => {
17176
+ const { decided } = parseStandards(text ?? "");
17177
+ if (decided.length === 0) return "";
17178
+ return `## Standards
17179
+
17180
+ \`${STANDARDS}\` holds the standards the human set in earlier reviews.
17181
+ Follow every one in what you write. Reviewing, never raise a finding a
17182
+ standard rules out — a \`Won't fix:\` standard is a finding the human
17183
+ rejected, so never raise it or one like it again — and raise breaking a
17184
+ standard as a finding.
17185
+
17186
+ ${decided.map((s) => `- ${s}`).join("\n")}`;
17187
+ };
17188
+ //#endregion
17075
17189
  //#region src/workflows/shared/vars.ts
17076
17190
  /** The refactor guard's fixed list, and `seamAllow`'s default minus `.gtd/**`. */
17077
17191
  const testFileGlobs = [
@@ -17083,6 +17197,11 @@ const testFileGlobs = [
17083
17197
  "**/__tests__/**"
17084
17198
  ];
17085
17199
  const defaultFlagRegistry = "src/flags/index.ts";
17200
+ /** The `tests` workflow's own process settings: where it looks for skipped tests, and how a skip reads. */
17201
+ const testsDefaults = {
17202
+ testFiles: testFileGlobs.join(", "),
17203
+ skipPattern: "^\\s*(?:(?:describe|context|suite|it|test|specify)\\.(?:skip|todo)\\b|x(?:describe|context|it|test|specify)\\s*\\(|(?:@\\S+\\s+)*@(?:skip|ignore)(?:\\s|$))"
17204
+ };
17086
17205
  /** Process settings. */
17087
17206
  const defaults = {
17088
17207
  reviewBase: "",
@@ -17091,6 +17210,7 @@ const defaults = {
17091
17210
  reviewNoteActionable: "0.7",
17092
17211
  reviewSeverityMinP: "0.7",
17093
17212
  qualityTriageMinP: "0.7",
17213
+ standardMinP: "0.8",
17094
17214
  refactorFlagPattern: "\\bGTD_FLAG_\\w+",
17095
17215
  seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
17096
17216
  flagMode: "true",
@@ -17350,8 +17470,11 @@ const withSkills = (skills, prompt) => {
17350
17470
  };
17351
17471
  /** An `agent()` step whose prompt opens with the preamble naming its scope's skills, then its restricted file access. */
17352
17472
  const agentWithSkills = (name, prompt, options = {}) => {
17353
- const access = accessPreamble(accessFor(name, options.file));
17354
- const body = access === "" ? prompt : `${access}\n\n${prompt}`;
17473
+ const body = [
17474
+ accessPreamble(accessFor(name, options.file)),
17475
+ prompt,
17476
+ standardsBlock(read(STANDARDS))
17477
+ ].filter((part) => part !== "").join("\n\n");
17355
17478
  return agent(name, withSkills(skillsFor(name).join(", "), body), options);
17356
17479
  };
17357
17480
  const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
@@ -17391,6 +17514,7 @@ const SUMMARY_TITLES = [
17391
17514
  "Weakened scenarios",
17392
17515
  "Fixed from your notes",
17393
17516
  "Fixed automatically",
17517
+ "Standards added",
17394
17518
  "Dropped as noise",
17395
17519
  "Nothing to note"
17396
17520
  ];
@@ -17410,7 +17534,9 @@ const composedReview = (base, report) => {
17410
17534
  return name;
17411
17535
  };
17412
17536
  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}`;
17537
+ const answerBoxes = ([fix, wontFix] = [ANSWER_FIX, ANSWER_WONT_FIX]) => `${ANSWER_LEAD}\n\n- [ ] ${fix}\n- [ ] ${wontFix}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`;
17538
+ const conflicts = (report.standards?.undecided ?? []).map((c) => chunk(unique(conflictTitle(c)), conflictBody(c)));
17539
+ const added = report.standards?.added ?? [];
17414
17540
  const notes = report.notes.map((n) => ({
17415
17541
  ...n,
17416
17542
  title: unique(n.title)
@@ -17419,8 +17545,10 @@ const composedReview = (base, report) => {
17419
17545
  const chunks = [
17420
17546
  ...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.`)],
17421
17547
  ...weakened.length === 0 ? [] : [chunk("Weakened scenarios", `These scenarios lost an assertion the review base had:\n\n${weakened.join("\n")}`)],
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}`)),
17548
+ ...conflicts,
17549
+ ...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(n.answers)}`)),
17423
17550
  ...report.fixed.length === 0 ? [] : [chunk("Fixed automatically", report.fixed.map((f) => `- ${f.title} (${short(f.commit)})`).join("\n"))],
17551
+ ...added.length === 0 ? [] : [chunk("Standards added", `Learned from your answers and kept in \`${STANDARDS}\`:\n\n${added.map((s) => `- ${s}`).join("\n")}`)],
17424
17552
  ...report.noise.length === 0 ? [] : [chunk("Dropped as noise", report.noise.map((f) => `- ${f.title} (p=${f.p})`).join("\n"))]
17425
17553
  ];
17426
17554
  const body = chunks.length > 0 ? chunks : [chunk("Nothing to note", "No quality lens raised anything for you — or none ran. Review the diff yourself.")];
@@ -25619,9 +25747,9 @@ const HashMapProto = {
25619
25747
  return new HashMapIterator(this, (k, v) => [k, v]);
25620
25748
  },
25621
25749
  [symbol$1]() {
25622
- let hash$1 = hash(HashMapSymbolKey);
25623
- for (const item of this) hash$1 ^= pipe(hash(item[0]), combine$5(hash(item[1])));
25624
- return cached(this, hash$1);
25750
+ let hash$2 = hash(HashMapSymbolKey);
25751
+ for (const item of this) hash$2 ^= pipe(hash(item[0]), combine$5(hash(item[1])));
25752
+ return cached(this, hash$2);
25625
25753
  },
25626
25754
  [symbol](that) {
25627
25755
  if (isHashMap(that)) {
@@ -31627,7 +31755,7 @@ const gauge$1 = (count) => new GaugeState(count);
31627
31755
  /** @internal */
31628
31756
  const histogram$2 = (options) => new HistogramState(options.buckets, options.count, options.min, options.max, options.sum);
31629
31757
  /** @internal */
31630
- const summary$2 = (options) => new SummaryState(options.error, options.quantiles, options.count, options.min, options.max, options.sum);
31758
+ const summary$3 = (options) => new SummaryState(options.error, options.quantiles, options.count, options.min, options.max, options.sum);
31631
31759
  /** @internal */
31632
31760
  const isCounterState = (u) => hasProperty(u, CounterStateTypeId);
31633
31761
  /**
@@ -31760,7 +31888,7 @@ const histogram$1 = (key) => {
31760
31888
  });
31761
31889
  };
31762
31890
  /** @internal */
31763
- const summary$1 = (key) => {
31891
+ const summary$2 = (key) => {
31764
31892
  const { error, maxAge, maxSize, quantiles } = key.keyType;
31765
31893
  const sortedQuantiles = pipe(quantiles, sort(Order$1));
31766
31894
  const values = allocate(maxSize);
@@ -31795,7 +31923,7 @@ const summary$1 = (key) => {
31795
31923
  sum = sum + value;
31796
31924
  };
31797
31925
  return make$5({
31798
- get: () => summary$2({
31926
+ get: () => summary$3({
31799
31927
  error,
31800
31928
  quantiles: snapshot(Date.now()),
31801
31929
  count,
@@ -31995,7 +32123,7 @@ var MetricRegistryImpl = class {
31995
32123
  getSummary(key) {
31996
32124
  let value = pipe(this.map, get$1(key), getOrUndefined);
31997
32125
  if (value == null) {
31998
- const summary = summary$1(key);
32126
+ const summary = summary$2(key);
31999
32127
  if (!pipe(this.map, has(key))) pipe(this.map, set(key, summary));
32000
32128
  value = summary;
32001
32129
  }
@@ -38042,6 +38170,7 @@ const ShippedOptions = Struct({
38042
38170
  reviewNoteActionable: setting$1,
38043
38171
  reviewSeverityMinP: setting$1,
38044
38172
  qualityTriageMinP: setting$1,
38173
+ standardMinP: setting$1,
38045
38174
  refactorFlagPattern: setting$1,
38046
38175
  seamAllow: setting$1,
38047
38176
  sliceMaxCriteria: setting$1,
@@ -38104,6 +38233,24 @@ const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions,
38104
38233
  base: void 0
38105
38234
  };
38106
38235
  });
38236
+ const TestsOptions = Struct({
38237
+ ...ShippedOptions.fields,
38238
+ testFiles: setting$1,
38239
+ skipPattern: setting$1
38240
+ });
38241
+ const testsShipped = (flow) => defineWorkflow(standardSchemaV1(TestsOptions, {
38242
+ onExcessProperty: "error",
38243
+ errors: "all"
38244
+ }), ({ contextLimit, ...options }) => ({
38245
+ flow: limited(flow, contextLimit),
38246
+ defaults: over({
38247
+ ...defaults,
38248
+ ...testsDefaults
38249
+ }, options),
38250
+ envDefaults: over(envDefaults, options),
38251
+ doors: { tests: {} },
38252
+ base: void 0
38253
+ }));
38107
38254
  //#endregion
38108
38255
  //#region src/workflows/shared/lenses.ts
38109
38256
  const NO_FINDINGS = {
@@ -38112,7 +38259,7 @@ const NO_FINDINGS = {
38112
38259
  noise: []
38113
38260
  };
38114
38261
  const UNTITLED = "Quality findings";
38115
- const below = (section) => section.split("\n").slice(1).join("\n").trim();
38262
+ const below$1 = (section) => section.split("\n").slice(1).join("\n").trim();
38116
38263
  const strayProse = (text, sections) => sections.reduce((rest, body) => rest.replace(body, "\n"), text).split("\n").filter((line) => !/^#\s/.test(line)).join("\n").trim();
38117
38264
  /** The findings in `text`, one per `## ` section, in file order, numbered `Q<n>`. */
38118
38265
  const findingsOf = (text) => {
@@ -38123,7 +38270,7 @@ const findingsOf = (text) => {
38123
38270
  body: stray
38124
38271
  }], ...sections.map((s) => ({
38125
38272
  title: s.title,
38126
- body: below(s.body)
38273
+ body: below$1(s.body)
38127
38274
  }))].map((f, i) => ({
38128
38275
  id: `Q${i + 1}`,
38129
38276
  ...f
@@ -38600,6 +38747,158 @@ const design = (base, guard) => scope("design", async () => {
38600
38747
  } while (await noteGate(REQUIREMENTS, answerProductQuestions));
38601
38748
  });
38602
38749
  //#endregion
38750
+ //#region src/workflows/shared/learn.ts
38751
+ const SUMMARY_MAX = 200;
38752
+ const BANNER = /^_(?:The fixer left this unresolved\.|A fix turn removed this while renaming another finding — check whether it was fixed\.|No detail given\.)_\s*/;
38753
+ const summary$1 = (body) => {
38754
+ const text = oneLine(body).replace(BANNER, "");
38755
+ return text.length > SUMMARY_MAX ? `${text.slice(0, SUMMARY_MAX - 1)}…` : text;
38756
+ };
38757
+ /** The standard a won't-fix or an own answer would make; a fix states none. */
38758
+ const candidateOf = (d) => {
38759
+ const anchor = oneLine(d.anchor);
38760
+ if (d.choice === "wontfix") {
38761
+ const why = summary$1(d.body);
38762
+ return [{
38763
+ decision: d,
38764
+ text: `Won't fix: ${anchor}${why === "" ? "" : ` — ${why}`}`
38765
+ }];
38766
+ }
38767
+ if (d.choice === "answer") return [{
38768
+ decision: d,
38769
+ text: `${oneLine(d.text)} (re: ${anchor})`
38770
+ }];
38771
+ return [];
38772
+ };
38773
+ const without = (list, drop) => {
38774
+ const keys = new Set(drop.map(standardKey));
38775
+ return list.filter((s) => !keys.has(standardKey(s)));
38776
+ };
38777
+ const adding = (list, text) => list.some((s) => standardKey(s) === standardKey(text)) ? [...list] : [...list, text];
38778
+ /**
38779
+ * Keep the new one, keep the old one, or replace both with the human's own
38780
+ * wording. Another open conflict stops naming a replaced standard: the human
38781
+ * never judged it against the replacement, so keeping it must not drop that.
38782
+ */
38783
+ const resolve = (standards, d) => {
38784
+ const c = conflictFor(standards, d.anchor);
38785
+ if (c === void 0) return standards;
38786
+ const others = standards.undecided.filter((u) => u !== c);
38787
+ if (d.choice === "wontfix") return {
38788
+ decided: standards.decided,
38789
+ undecided: others
38790
+ };
38791
+ const kept = d.choice === "fix" ? c.text : oneLine(d.text);
38792
+ const undecided = others.map((u) => ({
38793
+ text: u.text,
38794
+ olds: without(u.olds, c.olds)
38795
+ }));
38796
+ return {
38797
+ decided: adding(without(standards.decided, c.olds), kept),
38798
+ undecided
38799
+ };
38800
+ };
38801
+ const contradicts = (i, id, other) => ({
38802
+ id: `standard-${i}.contradicts-${id}`,
38803
+ primitive: "noul",
38804
+ instructions: `Does the "Standard:" line of evidence "standard-${i}" contradict ${other}?`,
38805
+ criteria: "yes: following one breaks the other. no: both can be followed — they are compatible or unrelated."
38806
+ });
38807
+ const questionsFor = (i, anchor, decided) => [
38808
+ {
38809
+ id: `standard-${i}`,
38810
+ primitive: "noul",
38811
+ instructions: `Is the human's answer to review note "${anchor}" (evidence "standard-${i}") a reusable standard for this codebase — a rule later changes and reviews should follow — rather than a call about this one note? Its "Standard:" line is the wording gtd would keep.`,
38812
+ criteria: "yes: it states a rule or preference that applies beyond this note. no: it is specific to this note, a one-off call, or unclear."
38813
+ },
38814
+ ...decided.map((_, k) => contradicts(i, `${k + 1}`, `kept standard ${k + 1} (evidence "kept-${k + 1}")`)),
38815
+ ...Array.from({ length: i - 1 }, (_, j) => contradicts(i, `new-${j + 1}`, `the "Standard:" line of evidence "standard-${j + 1}"`))
38816
+ ];
38817
+ const evidenceOf = (c) => [
38818
+ `Note: ${c.decision.anchor}`,
38819
+ `Reviewer's text: ${c.decision.body}`,
38820
+ `Human's answer: ${c.decision.choice === "wontfix" ? "won't fix" : c.decision.text}`,
38821
+ `Standard: ${c.text}`
38822
+ ].join("\n");
38823
+ /**
38824
+ * One judge rest over every candidate. Only a confident `yes` on uncut
38825
+ * evidence makes a standard; a contradiction the judge does not confidently
38826
+ * rule out goes to the human instead of overwriting what they decided before.
38827
+ * A cut kept standard leaves the round unlearned: raise `judgeBudgetBytes`.
38828
+ */
38829
+ const judged = async (candidates, decided, minP) => {
38830
+ const { answers, truncated } = await judge("review.standards.judge", {
38831
+ questions: candidates.flatMap((c, i) => questionsFor(i + 1, c.decision.anchor, decided)),
38832
+ evidence: Object.fromEntries([...candidates.map((c, i) => [`standard-${i + 1}`, evidenceOf(c)]), ...decided.map((s, k) => [`kept-${k + 1}`, s])]),
38833
+ message: "Judging whether your answers are standards to keep: run `gtd judge run`, or land without a verdict, which keeps none.",
38834
+ label: "Learning your standards"
38835
+ });
38836
+ const ruledOut = (i, id) => answered(answers[`standard-${i}.contradicts-${id}`], "no", minP);
38837
+ const added = [];
38838
+ const raised = [];
38839
+ candidates.forEach((c, n) => {
38840
+ const i = n + 1;
38841
+ if (truncated.includes(`standard-${i}`)) return;
38842
+ if (decided.some((_, k) => truncated.includes(`kept-${k + 1}`))) return;
38843
+ if (!answered(answers[`standard-${i}`], "yes", minP)) return;
38844
+ const olds = [...decided.filter((_, k) => !ruledOut(i, `${k + 1}`)), ...added.filter((a) => !ruledOut(i, `new-${a.i}`)).map((a) => a.text)];
38845
+ if (olds.length === 0) added.push({
38846
+ i,
38847
+ text: c.text
38848
+ });
38849
+ else raised.push({
38850
+ text: c.text,
38851
+ olds
38852
+ });
38853
+ });
38854
+ return {
38855
+ added: added.map((a) => a.text),
38856
+ raised
38857
+ };
38858
+ };
38859
+ /**
38860
+ * Settles every answer to a standard conflict, then learns standards from
38861
+ * the won't-fix ticks and own answers: kept in `.gtd/STANDARDS.md`, a
38862
+ * contradicting one as a new conflict note in `.gtd/REVIEW.md`.
38863
+ */
38864
+ const learnStandards = async (decisions) => {
38865
+ const before = parseStandards(read(".gtd/STANDARDS.md") ?? "");
38866
+ const rest = decisions.filter((d) => !isConflictTitle(d.anchor));
38867
+ const settled = decisions.filter((d) => isConflictTitle(d.anchor)).reduce(resolve, before);
38868
+ const known = new Set([...settled.decided, ...settled.undecided.map((c) => c.text)].map(standardKey));
38869
+ const candidates = rest.flatMap(candidateOf).filter((c) => {
38870
+ const key = standardKey(c.text);
38871
+ if (known.has(key)) return false;
38872
+ known.add(key);
38873
+ return true;
38874
+ });
38875
+ const minP = numeric(vars.standardMinP, Infinity);
38876
+ const { added, raised } = candidates.length === 0 || !Number.isFinite(minP) ? {
38877
+ added: [],
38878
+ raised: []
38879
+ } : await judged(candidates, settled.decided, minP);
38880
+ const after = {
38881
+ decided: [...settled.decided, ...added],
38882
+ undecided: [...settled.undecided, ...raised]
38883
+ };
38884
+ if (renderStandards(after) !== renderStandards(before)) await run("review.standards.recording", writeScript(STANDARDS, renderStandards(after)), { label: "Recording your standards" });
38885
+ if (raised.length > 0) {
38886
+ const chunks = raised.map((c) => `## ${conflictTitle(c)}\n\n${conflictBody(c)}`);
38887
+ await run("review.standards.raising", writeScript(REVIEW, `${(read(".gtd/REVIEW.md") ?? "").trimEnd()}\n\n${chunks.join("\n\n")}\n`), {
38888
+ label: "Asking about a contradicting standard",
38889
+ file: REVIEW
38890
+ });
38891
+ }
38892
+ };
38893
+ const settlesStandard = (d) => isConflictTitle(d.anchor);
38894
+ const standardsPending = () => parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided.length > 0;
38895
+ const standardsAddedSince = (base) => {
38896
+ const change = changesSince(base).get(STANDARDS);
38897
+ if (change === void 0) return [];
38898
+ const was = new Set(parseStandards(change.before ?? "").decided);
38899
+ return parseStandards(change.after ?? "").decided.filter((s) => !was.has(s));
38900
+ };
38901
+ //#endregion
38603
38902
  //#region src/workflows/shared/review.ts
38604
38903
  const VERDICTS = [
38605
38904
  "edit",
@@ -38730,7 +39029,11 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
38730
39029
  ...report,
38731
39030
  weakened: removedAssertions(changesSince(base)),
38732
39031
  nitFix,
38733
- answered
39032
+ answered,
39033
+ standards: {
39034
+ added: standardsAddedSince(base),
39035
+ undecided: parseStandards(read(".gtd/STANDARDS.md") ?? "").undecided
39036
+ }
38734
39037
  }), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
38735
39038
  label: "Composing the review",
38736
39039
  file: REVIEW,
@@ -38745,7 +39048,7 @@ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new S
38745
39048
  * praise dropped.
38746
39049
  */
38747
39050
  const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
38748
- const { guard, guardRests, fixAccess, rules } = options;
39051
+ const { guard, guardRests, fixAccess, rules, awaitMessage } = options;
38749
39052
  const feedback = options.feedback ?? {
38750
39053
  file: ".gtd/REQUIREMENTS.md",
38751
39054
  collecting,
@@ -38770,11 +39073,14 @@ const humanReview = (base, escalations = { rounds: 0 }, options = {}) => {
38770
39073
  guardRests,
38771
39074
  rules
38772
39075
  }),
38773
- awaitReview: (b) => awaitReview(b),
39076
+ awaitReview: (b) => awaitReview(b, awaitMessage?.(b)),
38774
39077
  reviewMissing,
38775
39078
  collecting: feedback.collecting,
38776
39079
  triage: triage$1(severities),
38777
39080
  decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
39081
+ settles: settlesStandard,
39082
+ learn: learnStandards,
39083
+ pending: standardsPending,
38778
39084
  answerQuestions: answerReviewQuestions,
38779
39085
  fixNits: async (notes) => {
38780
39086
  await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
@@ -38827,6 +39133,17 @@ const callForm = (first, second, [third, fourth, ...extra]) => {
38827
39133
  if (typeof second === "string") return legacyForm(first, second, third, fourth);
38828
39134
  if (typeof first === "string") return currentForm(first, second, third, fourth);
38829
39135
  };
39136
+ /** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
39137
+ const fixFeedback = async () => {
39138
+ await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
39139
+ label: "Fixing your review feedback",
39140
+ file: REQUIREMENTS,
39141
+ model: coder(),
39142
+ system: finisherSystem(),
39143
+ allowEmpty: true
39144
+ });
39145
+ await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
39146
+ };
38830
39147
  /**
38831
39148
  * Undo the human's review-round code edits. A path changed since the human
38832
39149
  * left it is not overwritten; requireRevert names it instead.
@@ -39840,6 +40157,8 @@ const skills = (vars) => {
39840
40157
  design: ["spec-driven-development", "planning-and-task-breakdown"],
39841
40158
  notes: ["incremental-implementation", "code-simplification"],
39842
40159
  feedback: ["test-driven-development", "incremental-implementation"],
40160
+ tests: ["test-audit", "test-driven-development"],
40161
+ "tests.health": ["debugging-and-error-recovery"],
39843
40162
  ...build,
39844
40163
  ...Object.fromEntries(Object.entries(build).map(([name, list]) => [`refactor.${name}`, list])),
39845
40164
  refactor: ["ponytail"],
@@ -39873,17 +40192,6 @@ const steering = {
39873
40192
  };
39874
40193
  //#endregion
39875
40194
  //#region src/workflows/review/review.ts
39876
- /** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
39877
- const fixFeedback = async () => {
39878
- await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
39879
- label: "Fixing your review feedback",
39880
- file: REQUIREMENTS,
39881
- model: coder(),
39882
- system: finisherSystem(),
39883
- allowEmpty: true
39884
- });
39885
- await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" });
39886
- };
39887
40195
  /**
39888
40196
  * The feature's review of everything since `reviewBase` — a green gate, the
39889
40197
  * triaged quality lap, then the human review — with no design phase: a
@@ -40017,6 +40325,191 @@ const cleanupFlow = async () => {
40017
40325
  };
40018
40326
  const cleanup = flagShipped(cleanupFlow, "cleanup");
40019
40327
  //#endregion
40328
+ //#region src/workflows/tests/tests.ts
40329
+ const TESTS = ".gtd/TESTS.md";
40330
+ const RETIRE = ".gtd/RETIRE.md";
40331
+ /** The first two answer boxes of a deletion proposal; the third stays the human's own answer. */
40332
+ const RETIRE_ANSWERS = ["delete it", "keep it skipped"];
40333
+ testsDefaults.skipPattern;
40334
+ /** `skipPattern` as a per-line expression; refuses one that is blank, invalid, or matches every line. */
40335
+ const skipExpression = (pattern) => {
40336
+ if (pattern.trim() === "") return refuse("gtd: skipPattern must not be blank");
40337
+ let expression;
40338
+ try {
40339
+ expression = new RegExp(pattern);
40340
+ } catch (error) {
40341
+ return refuse(`gtd: skipPattern is not a valid regular expression: ${String(error)}`);
40342
+ }
40343
+ if (expression.test("")) return refuse("gtd: skipPattern must not match the empty string");
40344
+ return expression;
40345
+ };
40346
+ /** Every line of `files` the expression calls skipped, in file then line order. */
40347
+ const skipsIn = (files, expression) => files.flatMap(({ path, text }) => text.split("\n").flatMap((line, i) => expression.test(line) ? [{
40348
+ path,
40349
+ line: i + 1,
40350
+ text: line.trim()
40351
+ }] : []));
40352
+ const testPaths = () => {
40353
+ const globs = (vars.testFiles ?? testsDefaults.testFiles).split(",").map((g) => g.trim()).filter((g) => g !== "");
40354
+ return [...new Set(globs.flatMap((g) => glob(g)))].filter(isCode$1).sort();
40355
+ };
40356
+ const scan = (paths, expression) => skipsIn(paths.map((path) => ({
40357
+ path,
40358
+ text: read(path) ?? ""
40359
+ })), expression);
40360
+ /** A section title as a repository path: backticks and a leading `./` dropped. */
40361
+ const pathOf = (title) => title.replace(/`/g, "").trim().replace(/^\.\//, "");
40362
+ const below = (section) => section.split("\n").slice(1).join("\n").trim();
40363
+ const sweepingPrompt = (skips) => `Sweep this repository's tests for ones to revive.
40364
+
40365
+ Skipped tests gtd found:
40366
+
40367
+ ${skips.length === 0 ? "- none" : skips.map((s) => `- ${s.path}:${s.line} — ${s.text}`).join("\n")}
40368
+
40369
+ - Audit every test in the repository with the test-audit skill — the whole
40370
+ suite, not a recent change
40371
+ - Write \`${TESTS}\`: \`# Tests\`, then one \`## <path>\` section per test file
40372
+ that needs work — every file listed above, and each file the audit found
40373
+ worth rewriting — saying which tests are skipped and what the audit found
40374
+ - Change nothing else; write nothing when there is nothing to do
40375
+ - Leave everything uncommitted and finish your turn`;
40376
+ const rewritingPrompt = (path, found) => `Rewrite the tests in \`${path}\` until none is skipped and the suite is green.
40377
+
40378
+ What the sweep found:
40379
+
40380
+ ${found || "_Nothing more than the skipped tests._"}
40381
+
40382
+ - Unskip every skipped test in the file and make it pass, rewriting it where
40383
+ it rotted, and fix what the audit found
40384
+ - A test that is obsolete — what it tests is gone, or another test already
40385
+ covers it — stays skipped: never delete it. Propose its deletion in
40386
+ \`${RETIRE}\` instead: a \`## <title>\` section naming \`${path}\`, the test,
40387
+ and why it should go. A person decides. Propose a line gtd misread as
40388
+ skipped (fixture text, a comment) the same way, saying so
40389
+ - Delete no test file
40390
+ - Leave everything uncommitted and finish your turn`;
40391
+ const gateMessage = () => `The test baseline is red — gtd will not revive skipped tests on a broken suite.
40392
+ \`.gtd/FEEDBACK.md\` holds the failing output.
40393
+
40394
+ What each change does next (then run \`gtd land\`):
40395
+ - **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**tests-gate.check**).
40396
+ `;
40397
+ const awaitMessage = (base) => `\`.gtd/REVIEW.md\` holds one chunk per test the sweep would delete. Each ends in
40398
+ three boxes — tick exactly one: \`delete it\` (the test is deleted, then the
40399
+ suite runs), \`keep it skipped\` (dropped, recorded in the commit), or write your
40400
+ own answer over \`_your answer_\` and tick it (judged like a note). Every
40401
+ landing here is refused until each proposal holds exactly one tick. An agent
40402
+ turn that deleted a test file was refused; check the diff for tests removed
40403
+ inside a file.
40404
+
40405
+ Review the revived tests yourself:
40406
+
40407
+ git diff ${base}
40408
+
40409
+ Then run \`gtd land\`. A note or code edit of your own is read as feedback and
40410
+ built before you review again.
40411
+ `;
40412
+ /** A proposal's own text, made a request the nit fixer acts on once the human ticks `delete it`. */
40413
+ const proposals = (text) => sectionBodies(text).map((s, i) => ({
40414
+ id: `R${i + 1}`,
40415
+ title: s.title,
40416
+ body: `Delete this skipped test.\n\n${below(s.body)}`,
40417
+ answers: RETIRE_ANSWERS
40418
+ }));
40419
+ /** Sections of `text` that name `path` as a whole token — a trailing full stop still counts. */
40420
+ const naming = (text, path) => {
40421
+ const token = new RegExp(`(?<![\\w./-])${path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w/-])`);
40422
+ return sectionBodies(text).filter((s) => token.test(s.body.replace(/(^|[^\w.])\.\//g, "$1"))).length;
40423
+ };
40424
+ /** Refuses a test file a turn deleted, and a skip left in the file being rewritten that `.gtd/RETIRE.md` does not name. */
40425
+ const guardFor = (known, expression, path) => () => {
40426
+ const deleted = changes().filter((c) => c.status === "deleted" && known.has(c.path)).map((c) => c.path);
40427
+ if (deleted.length > 0) refuse(`gtd land: tests: ${deleted.join(", ")} deleted — a test goes only once a person ticks "delete it" at the review`);
40428
+ if (path === void 0) return;
40429
+ const left = scan([path], expression).slice(naming(read(RETIRE) ?? "", path));
40430
+ if (left.length > 0) refuse(`gtd land: tests: ${left.map((s) => `${s.path}:${s.line}`).join(", ")} still skipped — revive it, or propose its deletion in ${RETIRE}`);
40431
+ };
40432
+ /** The sweep's packages, refused when it changed more than its plan or the plan misses a file. */
40433
+ const planned = (skips, known) => {
40434
+ const strays = changes().filter((c) => c.path !== TESTS);
40435
+ if (strays.length > 0) refuse(`gtd land: tests: the sweep changed ${strays.map((c) => c.path).join(", ")} — it writes only ${TESTS}`);
40436
+ const packages = sectionBodies(read(TESTS) ?? "").map((s) => ({
40437
+ path: pathOf(s.title),
40438
+ found: below(s.body)
40439
+ }));
40440
+ const missing = [...packages.filter((p) => !known.has(p.path)).map((p) => `"${p.path}" is no test file`), ...[...new Set(skips.map((s) => s.path))].filter((path) => !packages.some((p) => p.path === path)).map((path) => `${path} has no section`)];
40441
+ if (missing.length > 0) refuse(`gtd land: tests: in ${TESTS}, ${missing.join("; ")}`);
40442
+ return packages;
40443
+ };
40444
+ /**
40445
+ * Revive or retire skipped tests: a sweep plans one package per test file, each
40446
+ * is rewritten until unskipped and green, and what an agent calls obsolete
40447
+ * reaches the human review as a deletion proposal only a person can tick.
40448
+ */
40449
+ const testsFlow = async () => {
40450
+ refusePendingHandOff();
40451
+ for (const file of [
40452
+ REQUIREMENTS,
40453
+ TESTS,
40454
+ RETIRE
40455
+ ]) if (read(file) !== void 0) refuse(`gtd: ${file} is left over — finish or remove it first.`);
40456
+ const expression = skipExpression(vars.skipPattern ?? testsDefaults.skipPattern);
40457
+ await gate("tests-gate", gateMessage());
40458
+ const base = start();
40459
+ const known = new Set(testPaths());
40460
+ const guard = guardFor(known, expression);
40461
+ const report = await scope("tests", async () => {
40462
+ const skips = scan([...known], expression);
40463
+ await agentWithSkills("sweeping", sweepingPrompt(skips), {
40464
+ label: "Sweeping the tests",
40465
+ file: TESTS,
40466
+ model: coder(),
40467
+ system: finisherSystem(),
40468
+ allowEmpty: true
40469
+ });
40470
+ guard();
40471
+ const packages = planned(skips, known);
40472
+ for (const { path, found } of packages) {
40473
+ const guarded = guardFor(known, expression, path);
40474
+ await agentWithSkills("rewriting", rewritingPrompt(path, found), {
40475
+ label: `Rewriting ${path}`,
40476
+ file: RETIRE,
40477
+ model: coder(),
40478
+ system: finisherSystem(),
40479
+ allowEmpty: true
40480
+ });
40481
+ guarded();
40482
+ if (!changes().some((c) => isCode$1(c.path))) continue;
40483
+ await healthy(() => fixCheck(), {
40484
+ guard: guarded,
40485
+ guardRests: false
40486
+ });
40487
+ }
40488
+ const retired = proposals(read(RETIRE) ?? "");
40489
+ if (read(TESTS) !== void 0 || read(RETIRE) !== void 0) await run("closing", removeScript([TESTS, RETIRE]), { label: "Closing the sweep" });
40490
+ const changed = changesSince(base).paths.some(isCode$1);
40491
+ return retired.length === 0 && !changed ? void 0 : {
40492
+ notes: retired,
40493
+ fixed: [],
40494
+ noise: []
40495
+ };
40496
+ });
40497
+ if (report === void 0) return;
40498
+ let outcome = await tail((escalations) => humanReview(base, escalations, {
40499
+ report,
40500
+ awaitMessage: report.notes.length > 0 ? awaitMessage : void 0
40501
+ }), {
40502
+ lenses: [],
40503
+ green: true
40504
+ });
40505
+ while (outcome.verdict === "feedback") {
40506
+ await reUnwind(outcome);
40507
+ await fixFeedback();
40508
+ outcome = await tail((escalations) => humanReview(base, escalations), { lenses: [] });
40509
+ }
40510
+ };
40511
+ const tests = testsShipped(testsFlow);
40512
+ //#endregion
40020
40513
  //#region src/workflows/index.ts
40021
40514
  var workflows_default = feature;
40022
40515
  /**
@@ -40026,4 +40519,4 @@ var workflows_default = feature;
40026
40519
  */
40027
40520
  const doors = {};
40028
40521
  //#endregion
40029
- 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 };
40522
+ export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, HANDOFF, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, SLICES, ShippedOptions, TestsOptions, 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, tests, testsFlow, threads, triage, unstableIds, unwind };