@pmelab/gtd 41.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 (73) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +107 -53
  3. package/claude/hooks/sketch.ts +1 -1
  4. package/dist/chunks/{reserved-Ch4LLrEj.mjs → reserved-COEMFEpy.mjs} +84 -51
  5. package/dist/chunks/{steering-DVILanVf.mjs → steering-D4LUQ-av.mjs} +95 -12
  6. package/dist/chunks/tail-C_xn8U7U.mjs +246 -0
  7. package/dist/flows.mjs +11 -1
  8. package/dist/gtd.bundle.mjs +498 -30
  9. package/dist/steering.mjs +2 -2
  10. package/dist/testkit.mjs +460 -28
  11. package/dist/web/main.js +42 -13
  12. package/dist/workflow-kit.mjs +61 -25
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +66 -57
  15. package/dist/workflows/fix.mjs +3 -3
  16. package/dist/workflows/review.mjs +2 -2
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +446 -141
  19. package/package.json +1 -1
  20. package/src/cli/Cli.test.ts +33 -1
  21. package/src/cli/Cli.ts +70 -6
  22. package/src/cli/History.ts +15 -0
  23. package/src/cli/Judges.test.ts +373 -0
  24. package/src/cli/Judges.ts +624 -0
  25. package/src/cli/Metrics.ts +3 -16
  26. package/src/cli/Program.test.ts +3 -1
  27. package/src/cli/Program.ts +9 -0
  28. package/src/flows/scripts.ts +15 -0
  29. package/src/steering/index.ts +12 -1
  30. package/src/steering/review.test.ts +109 -20
  31. package/src/steering/review.ts +160 -9
  32. package/src/testkit/EmittedScriptRecognizer.ts +0 -14
  33. package/src/testkit/FlowScripts.test.ts +17 -0
  34. package/src/testkit/FlowScripts.ts +15 -0
  35. package/src/testkit/steps.ts +31 -0
  36. package/src/workflow-kit/review.test.ts +170 -26
  37. package/src/workflow-kit/review.ts +95 -19
  38. package/src/workflows/feature/coverage.feature +3 -3
  39. package/src/workflows/feature/fast-path.feature +2 -2
  40. package/src/workflows/feature/feature.test.ts +2 -37
  41. package/src/workflows/feature/feature.ts +12 -14
  42. package/src/workflows/feature/flag-mode.feature +2 -2
  43. package/src/workflows/feature/lens-review.feature +246 -0
  44. package/src/workflows/feature/one-step-land.feature +2 -2
  45. package/src/workflows/feature/quality-unresolved.feature +47 -32
  46. package/src/workflows/feature/refactor-absorbs.feature +3 -3
  47. package/src/workflows/feature/refactor-first.feature +5 -5
  48. package/src/workflows/feature/refactor-rebuild.feature +7 -7
  49. package/src/workflows/feature/refactor-review.feature +19 -100
  50. package/src/workflows/feature/refactor.test.ts +12 -27
  51. package/src/workflows/feature/refactor.ts +0 -1
  52. package/src/workflows/feature/review-answers.feature +179 -0
  53. package/src/workflows/feature/review-gate.feature +6 -6
  54. package/src/workflows/feature/review.test.ts +33 -77
  55. package/src/workflows/feature/review.ts +79 -47
  56. package/src/workflows/feature/seam-rejection.feature +2 -2
  57. package/src/workflows/feature/seam.feature +2 -2
  58. package/src/workflows/feature/seam.test.ts +1 -1
  59. package/src/workflows/feature/slicing.feature +6 -6
  60. package/src/workflows/feature/steps.test.ts +0 -12
  61. package/src/workflows/feature/steps.ts +3 -10
  62. package/src/workflows/index.ts +1 -0
  63. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +6 -5
  64. package/src/workflows/shared/access.test.ts +2 -11
  65. package/src/workflows/shared/access.ts +0 -2
  66. package/src/workflows/shared/lenses.test.ts +39 -0
  67. package/src/workflows/shared/lenses.ts +176 -0
  68. package/src/workflows/shared/options.ts +1 -0
  69. package/src/workflows/shared/tail.ts +19 -4
  70. package/src/workflows/shared/text.test.ts +106 -88
  71. package/src/workflows/shared/text.ts +164 -53
  72. package/src/workflows/shared/vars.ts +1 -0
  73. package/dist/chunks/tail-lY1UmJrU.mjs +0 -112
package/dist/web/main.js CHANGED
@@ -29844,8 +29844,8 @@ const eolOf = (content) => content.includes("\r\n") ? "\r\n" : "\n";
29844
29844
  const PREFIX_RE = /^(H|A):\s*/;
29845
29845
  const lineOf = (node, which) => toLspPosition(node.position[which]).line;
29846
29846
  /** 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. */
29847
- const itemText = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
29848
- const prefixOf = (content, item) => PREFIX_RE.exec(itemText(content, item));
29847
+ const itemText$1 = (content, item) => item.children.map((child) => sourceText(content, child)).join(" ").trim();
29848
+ const prefixOf = (content, item) => PREFIX_RE.exec(itemText$1(content, item));
29849
29849
  /** A thread is a definition whose body OPENS with a list whose first item carries a prefix — anything else stays a one-shot footnote. */
29850
29850
  const isThreadBody = (content, children) => {
29851
29851
  const first = children[0];
@@ -29865,7 +29865,7 @@ const entryOf = (content, item, previous, report) => {
29865
29865
  return;
29866
29866
  }
29867
29867
  const author = match[1] === "H" ? "me" : "agent";
29868
- const text = itemText(content, item).slice(match[0].length).trim();
29868
+ const text = itemText$1(content, item).slice(match[0].length).trim();
29869
29869
  if (text === "") report(`empty "${match[1]}:" entry — write your own words there`, start, end);
29870
29870
  if (previous === void 0 && author === "agent") report(`the agent never starts a thread — the first entry must be "H:"`, start, end);
29871
29871
  else if (previous === author) report(`two consecutive "${match[1]}:" entries — entries must alternate`, start, end);
@@ -31565,8 +31565,6 @@ const qaDescriptor = {
31565
31565
  unansweredQuestions,
31566
31566
  openThreads: openThreadsOf
31567
31567
  };
31568
- //#endregion
31569
- //#region src/steering/review.ts
31570
31568
  /**
31571
31569
  * The canonical sample. Every detail here is load-bearing for the formatter
31572
31570
  * round-trip: both footnote bodies exceed 80 characters so a reflow actually
@@ -31833,16 +31831,42 @@ const parseHunk = (content, lines, title, item) => {
31833
31831
  errors: [hunkSecondPointerFinding(content, lines, item, token, sourceLine, title, file), hunkLineWithoutRangeFinding(content, item, token, sourceLine, title, file)].filter((e) => e !== void 0)
31834
31832
  };
31835
31833
  };
31834
+ const itemText = (content, item) => item.children.map((c) => sourceText(content, c)).join(" ").replace(/^\[[ xX]\]\s*/, "").trim();
31835
+ /**
31836
+ * The task list right after the `Tick exactly one:` paragraph, with that
31837
+ * paragraph's index. Read by position, never by label, so an edited label or
31838
+ * a free text opening with `./path` is still an answer, not a pointer. Any
31839
+ * count but three boxes is kept, and reads as unanswered.
31840
+ */
31841
+ const answerListOf = (content, body) => {
31842
+ 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));
31843
+ if (lead === -1) return void 0;
31844
+ const list = body[lead + 1];
31845
+ const last = list.children.length - 1;
31846
+ return {
31847
+ lead,
31848
+ list,
31849
+ options: list.children.map((item, i) => {
31850
+ const text = itemText(content, item);
31851
+ return {
31852
+ checked: item.checked === true,
31853
+ text: i === last && text.toLowerCase() === "_your answer_".toLowerCase() ? "" : text
31854
+ };
31855
+ })
31856
+ };
31857
+ };
31836
31858
  /** 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). */
31837
31859
  const parseChunkBody = (content, lines, title, body) => {
31838
- const firstPointerIndex = body.findIndex((n) => taskItems(n).some((item) => hasPointerToken(content, item)));
31839
- const leadingRun = firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex);
31860
+ const answer = answerListOf(content, body);
31861
+ const isAnswer = (n) => answer !== void 0 && (n === answer.list || n === body[answer.lead]);
31862
+ const firstPointerIndex = body.findIndex((n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)));
31863
+ const leadingRun = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter((n) => !isAnswer(n));
31840
31864
  const descriptionNodes = blockNodesOfRun(content, leadingRun);
31841
31865
  const descriptionInline = collapseImages(blockRunInline(content, leadingRun));
31842
31866
  const description = leadingRun.filter((n) => n.type !== "footnoteDefinition").map((n) => sourceText(content, n)).join(" ").trim();
31843
31867
  const files = [];
31844
31868
  const errors = [];
31845
- for (const item of body.flatMap((n) => taskItems(n))) {
31869
+ for (const item of body.filter((n) => !isAnswer(n)).flatMap((n) => taskItems(n))) {
31846
31870
  const parsed = parseHunk(content, lines, title, item);
31847
31871
  if (!parsed) continue;
31848
31872
  files.push(parsed.file);
@@ -31853,7 +31877,9 @@ const parseChunkBody = (content, lines, title, body) => {
31853
31877
  descriptionNodes,
31854
31878
  descriptionInline,
31855
31879
  files,
31856
- errors
31880
+ errors,
31881
+ ...answer === void 0 ? {} : { answers: answer.options },
31882
+ boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0)
31857
31883
  };
31858
31884
  };
31859
31885
  /** 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. */
@@ -31889,9 +31915,9 @@ const parseChangesets = (tree, content, lines) => {
31889
31915
  const errors = [];
31890
31916
  for (const { heading, headingLine, body } of splitChunks(tree)) {
31891
31917
  const title = headingText$1(content, heading);
31892
- const { description, descriptionNodes, descriptionInline, files, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
31918
+ const { description, descriptionNodes, descriptionInline, files, answers, boxed, errors: bodyErrors } = parseChunkBody(content, lines, title, body);
31893
31919
  errors.push(...bodyErrors);
31894
- if (files.length === 0) errors.push({
31920
+ if (files.length === 0 && boxed) errors.push({
31895
31921
  message: `Chunk "${title}" has no file pointers`,
31896
31922
  line: headingLine,
31897
31923
  range: nodeRange(heading)
@@ -31902,7 +31928,8 @@ const parseChangesets = (tree, content, lines) => {
31902
31928
  descriptionNodes,
31903
31929
  descriptionInline,
31904
31930
  files,
31905
- headingLine
31931
+ headingLine,
31932
+ ...answers === void 0 ? {} : { answers }
31906
31933
  });
31907
31934
  }
31908
31935
  if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" });
@@ -31983,7 +32010,9 @@ const isHunkPointerItem = (content, item) => {
31983
32010
  * clear, so a caller can skip the write and leave the mtime untouched.
31984
32011
  */
31985
32012
  const reviewClearTicks = (content) => {
31986
- 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);
32013
+ const tree = parseMarkdown(content);
32014
+ const answers = new Set(splitChunks(tree).flatMap(({ body }) => answerListOf(content, body)?.list.children ?? []));
32015
+ 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);
31987
32016
  if (offsets.length === 0) return content;
31988
32017
  let result = "";
31989
32018
  let cursor = 0;
@@ -1,5 +1,5 @@
1
- import { changes, changesSince, hasThreadFor, head, read, refuse, requireReplies, requireThreadsClosed, scope } from "./flows.mjs";
2
- import { c as stripCodeThreads, d as reviewNotes, f as reviewRisks, y as parseThreads } from "./chunks/steering-DVILanVf.mjs";
1
+ import { changes, changesSince, hasThreadFor, head, mark, read, refuse, requireReplies, requireThreadsClosed, scope } from "./flows.mjs";
2
+ import { _ as unansweredNotes, c as stripCodeThreads, g as reviewRisks, h as reviewNotes, m as reviewDecisions, w as parseThreads } from "./chunks/steering-D4LUQ-av.mjs";
3
3
  //#region src/workflow-kit/health.ts
4
4
  /** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
5
5
  const escalation = async (count, steps) => {
@@ -172,6 +172,13 @@ const collect = async (p, capture, round, requested) => {
172
172
  }
173
173
  return refuse(p.refusals.unmatched);
174
174
  };
175
+ /** Refuse while a note's answer boxes hold anything but exactly one tick, or a note's boxes were deleted. */
176
+ const requireDecisions = (file, reviewed) => {
177
+ const open = unansweredNotes(read(file) ?? "", changesSince(reviewed).get(file)?.before);
178
+ if (open.length === 0) return;
179
+ const list = open.map((n) => ` - ${file}${n.line > 0 ? `:${n.line}` : ""}: ${n.title} — ${n.reason}`).join("\n");
180
+ 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}`);
181
+ };
175
182
  const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
176
183
  /**
177
184
  * A review with ticks and THREAD footnotes (definition plus markers) removed:
@@ -191,7 +198,7 @@ const notesOf = (text) => {
191
198
  * commit of the last collecting turn, `"missing"` when the round left no
192
199
  * review, or `undefined` when no such turn ran.
193
200
  */
194
- const converse = async (p, base, collectedBefore) => {
201
+ const converse = async (p, base, reviewed, collectedBefore) => {
195
202
  let collectedAt = collectedBefore;
196
203
  for (;;) {
197
204
  await p.awaitReview(base);
@@ -201,24 +208,47 @@ const converse = async (p, base, collectedBefore) => {
201
208
  return "missing";
202
209
  }
203
210
  requireThreadsClosed(p.file);
204
- if (!hasThreadFor("agent", p.file)) return collectedAt;
211
+ if (!hasThreadFor("agent", p.file)) {
212
+ requireDecisions(p.file, reviewed);
213
+ return collectedAt;
214
+ }
205
215
  await p.collecting(p.captures.edits(head()), "reply");
206
216
  collectedAt = head();
207
217
  requireReplies(p.file);
208
- if (!hasThreadFor("human", p.file)) return collectedAt;
218
+ if (!hasThreadFor("human", p.file) && unansweredNotes(read(p.file) ?? "", changesSince(reviewed).get(p.file)?.before).length === 0) return collectedAt;
209
219
  }
210
220
  };
211
- /** Judge every note, then run each verdict's route: answers, then nit fixes, then deferrals, then the edits' lap. */
212
- const routeNotes = async (p, notes, r) => {
213
- const verdicts = await p.triage(notes);
214
- const of = (v) => notes.filter((n) => (verdicts.get(n.id) ?? "edit") === v);
215
- const [edits, questions, nits, deferred] = [
216
- of("edit"),
217
- of("question"),
218
- of("nit"),
219
- of("defer")
220
- ];
221
- if (edits.length + questions.length + nits.length + deferred.length === 0) return r.unfolded();
221
+ const decisionNote = (d, id, text) => ({
222
+ id,
223
+ kind: "chunk",
224
+ anchor: d.anchor,
225
+ before: d.body,
226
+ text,
227
+ forced: /^!(\s|$)/.test(text)
228
+ });
229
+ /** One `Gtd-Mark` value per line: a control character would make mark() throw on every replay. */
230
+ const oneLine = (text) => text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim() || "untitled";
231
+ /** Tell the policy, and mark each won't-fix on the gate's commit. */
232
+ const record = (p, decisions) => {
233
+ p.decided?.(decisions);
234
+ for (const d of decisions) if (d.choice === "wontfix") mark("wontfix", oneLine(d.anchor));
235
+ };
236
+ /**
237
+ * Judge every note, then run each verdict's route: answers, then nit fixes,
238
+ * then the edits' lap. Of the human's `decisions`, a fix joins the nit batch
239
+ * unjudged, an own answer is judged like a note, and a won't-fix is dropped
240
+ * with a `Gtd-Mark` on the gate's commit.
241
+ */
242
+ const routeNotes = async (p, notes, r, decisions = []) => {
243
+ record(p, decisions);
244
+ const of = (c) => decisions.filter((d) => d.choice === c);
245
+ const fixes = of("fix").map((d, i) => decisionNote(d, `fix-${i + 1}`, d.body));
246
+ const judged = [...notes, ...of("answer").map((d, i) => decisionNote(d, `answer-${i + 1}`, d.text))];
247
+ const verdicts = judged.length === 0 ? /* @__PURE__ */ new Map() : await p.triage(judged);
248
+ const ofVerdict = (v) => judged.filter((n) => (verdicts.get(n.id) ?? "edit") === v);
249
+ const [edits, questions] = [ofVerdict("edit"), ofVerdict("question")];
250
+ const nits = [...ofVerdict("nit"), ...fixes];
251
+ if (edits.length + questions.length + nits.length === 0) return r.unfolded();
222
252
  let answeredAt;
223
253
  if (questions.length > 0) {
224
254
  await p.answerQuestions(questions);
@@ -228,7 +258,6 @@ const routeNotes = async (p, notes, r) => {
228
258
  await p.fixNits(nits);
229
259
  await p.keepGreen();
230
260
  }
231
- if (deferred.length > 0) await p.defer(deferred);
232
261
  if (edits.length > 0) {
233
262
  await r.close();
234
263
  return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt), "triaged", {
@@ -250,8 +279,8 @@ const routeNotes = async (p, notes, r) => {
250
279
  };
251
280
  /** A code edit, not a thread-only change: lines that only add, answer or remove code threads don't count. */
252
281
  const isCodeEdit = (p, c) => p.isCode(c.path) && stripCodeThreads(c.path, c.before ?? "") !== stripCodeThreads(c.path, c.after ?? "");
253
- /** Whether anything but thread edits and ticks landed in the review file since `commit`. */
254
- const notedSince = (p, commit) => changesSince(commit).some((c) => c.path === p.file && notesOf(c.before ?? "") !== notesOf(read(p.file) ?? ""));
282
+ /** Whether a note or an answer, not just thread edits and pointer ticks, landed in the review file since `commit`. */
283
+ 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));
255
284
  /**
256
285
  * What the in-loop collecting turn left: `folded` when it wrote `feedback`
257
286
  * (those folds need their lap whatever is judged later), `settled` when only
@@ -265,9 +294,9 @@ const settledAfter = (p, collectedAt) => ({
265
294
  folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
266
295
  settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
267
296
  });
268
- const finishNotes = async (p, before, reopened, ctx) => {
269
- const notes = reviewNotes(before, read(p.file) ?? "");
270
- if (notes.length > 0) return routeNotes(p, notes, ctx);
297
+ const finishNotes = async (p, before, decisions, reopened, ctx) => {
298
+ const notes = reviewNotes(before ?? "", read(p.file) ?? "");
299
+ if (notes.length + decisions.length > 0) return routeNotes(p, notes, ctx, decisions);
271
300
  if (reopened) return ctx.unfolded();
272
301
  await ctx.close();
273
302
  return ctx.outcome(await collect(p, p.captures.notes(ctx.round), "notes"));
@@ -277,6 +306,11 @@ const toOutcome = (verdict, rest) => verdict === "signoff" ? { verdict } : {
277
306
  ...rest
278
307
  };
279
308
  const isReopened = (p, r) => r.settled && !r.folded && p.refusals.dropped !== void 0;
309
+ /** A carried answer is unchanged, never routed twice; one ticked beside a thread still routes once the thread has settled. */
310
+ const decisionsSince = (p, since) => {
311
+ const after = read(p.file) ?? "";
312
+ return reviewDecisions(since.get(p.file)?.before ?? after, after);
313
+ };
280
314
  const finish = async (p, reviewed, collectedAt) => {
281
315
  const round = head();
282
316
  const since = changesSince(reviewed);
@@ -297,15 +331,17 @@ const finish = async (p, reviewed, collectedAt) => {
297
331
  settled,
298
332
  folded
299
333
  });
300
- if (settled && !reopened || edited.length === 0 && !noted) return unfolded();
334
+ const decisions = decisionsSince(p, since);
335
+ if (settled && !reopened && decisions.length === 0 || edited.length === 0 && !noted) return unfolded();
301
336
  if (edited.length > 0) {
337
+ record(p, decisions);
302
338
  await close();
303
339
  return outcome(await collect(p, p.captures.edits(round, reopened ? reviewed : void 0), "edits", {
304
340
  notes: [],
305
341
  paths: edited.map((c) => c.path)
306
342
  }));
307
343
  }
308
- return finishNotes(p, since.get(p.file)?.before ?? "", reopened, {
344
+ return finishNotes(p, since.get(p.file)?.before, decisions, reopened, {
309
345
  round,
310
346
  close,
311
347
  outcome,
@@ -335,7 +371,7 @@ const review = async (base, p) => {
335
371
  let reviewed = head();
336
372
  let collectedAt;
337
373
  for (;;) {
338
- collectedAt = await converse(p, base, collectedAt);
374
+ collectedAt = await converse(p, base, reviewed, collectedAt);
339
375
  if (collectedAt === "missing") break;
340
376
  const result = await finish(p, reviewed, collectedAt);
341
377
  if (!("next" in result)) return result;
@@ -1,6 +1,6 @@
1
1
  import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
2
- import { Dt as registryEntry, Ot as requireFlag, S as coder, St as envDefaults, Tt as flagReferences, i as skills, n as summary, o as flagShipped, r as access, t as steering, xt as defaults } from "../chunks/reserved-Ch4LLrEj.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-lY1UmJrU.mjs";
2
+ import { Ct as envDefaults, Et as flagReferences, Ot as registryEntry, S as coder, St as defaults, i as skills, kt as requireFlag, n as summary, o as flagShipped, r as access, t as steering } from "../chunks/reserved-COEMFEpy.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C_xn8U7U.mjs";
4
4
  //#region src/workflows/cleanup/cleanup.ts
5
5
  const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
6
6
  const cleanupFlow = async () => {
@@ -1,7 +1,7 @@
1
- import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars, wrote } from "../flows.mjs";
2
- import { lensEntriesOf, review as review$1 } from "../workflow-kit.mjs";
3
- import { $ as naiveApplyFailure, A as agentWithSkills, B as coverageQuestion, C as fixCheck, Ct as testFileGlobs, D as reviewMissing, E as planner, F as buildReviewTriageMessage, G as designSystem, H as criterionLine, I as builderSystem, K as designTriagePrompt, L as clearCriteriaMessage, M as buildReviewAwaitAfterDeferMessage, N as buildReviewCollectingPrompt, P as buildReviewFixNitsPrompt, Q as implementFixPrompt, R as clearCriteriaQuestion, S as coder, St as envDefaults, T as fixRisks, U as deferNotesScript, V as coveringPrompt, W as designGateAnswerMessage, X as idleMessage, Z as implementBuildingPrompt, _ as REFACTOR_NOTE, _t as startGateBlockedMessage, a as flagModeOn, at as refactorDroppedRefusal, b as SLICES, bt as unwindFailure, ct as refactorRules, d as guarded, et as naivePassPrompt, f as healthy, ft as reviewNotesCapture, g as REFACTOR, gt as slicingPrompt, h as QUALITY, ht as seamSteerMessage, i as skills, it as refactorBuildingPrompt, j as buildReviewAnswerQuestionsPrompt, k as reviewing, lt as reviewEditNotesCapture, m as HANDOFF, mt as seamByHandMessage, n as summary, nt as oversizedMessage, ot as refactorFixPrompt, p as FEEDBACK, pt as reviewerSystem, r as access, rt as qualityUnresolvedMessage, s as shipped, st as refactorNotePrompt, t as steering, tt as normaliseCriteriaScript, u as gate, ut as reviewEditsCapture, v as REQUIREMENTS, vt as uncoveredMessage, x as awaitReview, xt as defaults, y as REVIEW, yt as unwindFailedMessage, z as coverageMessage } from "../chunks/reserved-Ch4LLrEj.mjs";
4
- import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-lY1UmJrU.mjs";
1
+ import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, showScript, slice, start, vars, writeScript, wrote } from "../flows.mjs";
2
+ import { lensEntriesOf, removedAssertions, review as review$1 } from "../workflow-kit.mjs";
3
+ import { $ as naivePassPrompt, A as agentWithSkills, B as coverageQuestion, C as fixCheck, Ct as envDefaults, D as reviewMissing, E as planner, F as builderSystem, G as designTriagePrompt, H as criterionLine, I as clearCriteriaMessage, L as clearCriteriaQuestion, M as buildReviewCollectingPrompt, N as buildReviewFixNitsPrompt, P as buildReviewTriageMessage, Q as naiveApplyFailure, R as composedReview, S as coder, St as defaults, T as fixRisks, U as designGateAnswerMessage, V as coveringPrompt, W as designSystem, X as implementBuildingPrompt, Y as idleMessage, Z as implementFixPrompt, _ as REFACTOR_NOTE, _t as slicingPrompt, a as flagModeOn, at as refactorBuildingPrompt, b as SLICES, bt as unwindFailedMessage, ct as refactorNotePrompt, d as guarded, dt as reviewEditsCapture, et as normaliseCriteriaScript, f as healthy, g as REFACTOR, gt as seamSteerMessage, h as QUALITY, ht as seamByHandMessage, i as skills, it as qualityUnresolvedMessage, j as buildReviewAnswerQuestionsPrompt, lt as refactorRules, m as HANDOFF, mt as reviewerSystem, n as summary, ot as refactorDroppedRefusal, p as FEEDBACK, pt as reviewNotesCapture, r as access, s as shipped, st as refactorFixPrompt, t as steering, tt as oversizedMessage, u as gate, ut as reviewEditNotesCapture, v as REQUIREMENTS, vt as startGateBlockedMessage, wt as testFileGlobs, x as awaitReview, xt as unwindFailure, y as REVIEW, yt as uncoveredMessage, z as coverageMessage } from "../chunks/reserved-COEMFEpy.mjs";
4
+ import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail, s as NO_FINDINGS } from "../chunks/tail-C_xn8U7U.mjs";
5
5
  import { posix } from "node:path";
6
6
  //#region src/workflows/shared/fences.ts
7
7
  const markFenced = (text) => {
@@ -241,7 +241,7 @@ const fixNits = (notes, access, rules) => {
241
241
  }, () => fixing("fixing"));
242
242
  };
243
243
  /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
244
- const collecting = (capture) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture), {
244
+ const collecting = (capture, round) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture, void 0, round), {
245
245
  label: "Collecting your feedback",
246
246
  file: REQUIREMENTS,
247
247
  mode: "qa",
@@ -260,11 +260,6 @@ const refactorCollecting = (capture, round, strays) => agentWithSkills("review.c
260
260
  system: reviewerSystem(),
261
261
  allowEmpty: true
262
262
  });
263
- /** `build.review.defer`: append `notes` to `.gtd/TODO.md` as one section; the step's landing is the commit. */
264
- const deferNotes = (notes, round) => run("review.defer", deferNotesScript(notes, round), {
265
- label: "Deferring could-notes to TODO.md",
266
- file: ".gtd/TODO.md"
267
- });
268
263
  //#endregion
269
264
  //#region src/workflows/feature/planning.ts
270
265
  /**
@@ -305,6 +300,13 @@ const verdictQuestion = (note) => ({
305
300
  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.`,
306
301
  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."
307
302
  });
303
+ const TIER_ORDER = {
304
+ critical: 0,
305
+ should: 1,
306
+ could: 2
307
+ };
308
+ /** Capture order: critical, should, could; within a tier one-way doors, then new concepts. */
309
+ const captureKey = (s) => s === void 0 ? 2 : TIER_ORDER[s.tier] * 3 + s.rank;
308
310
  const SEVERITY_ANSWERS = {
309
311
  security: ["yes", "no"],
310
312
  reversibility: ["one-way", "two-way"],
@@ -315,10 +317,7 @@ const SEVERITY_ANSWERS = {
315
317
  ],
316
318
  newConcept: ["yes", "no"]
317
319
  };
318
- /**
319
- * Deferring is the risky direction: anything unanswered, unrecognised, cut or
320
- * below `minP` is `critical`, never `could`.
321
- */
320
+ /** Anything unanswered, unrecognised, cut or below `minP` is `critical`, never `could`. */
322
321
  const severityOf = (note, judgment, minP) => {
323
322
  const ans = (q) => judgment.answers[`${note.id}.${q}`];
324
323
  const a = (q) => ans(q)?.answer;
@@ -382,7 +381,7 @@ const severityQuestions = (note) => [
382
381
  * nothing) plus four severity answers. Dismissing a note is the risky
383
382
  * direction, so only a confident non-`edit` verdict on uncut evidence counts;
384
383
  * a cut, unanswered or below-floor note is an `edit`. Severity answers on a
385
- * non-`edit` note are ignored. A confident `could` edit becomes `defer`.
384
+ * non-`edit` note are ignored; on an `edit` they order the collecting turn.
386
385
  */
387
386
  const triage$1 = (severities) => async (notes) => {
388
387
  const asked = notes.filter((n) => !n.forced);
@@ -402,17 +401,33 @@ const triage$1 = (severities) => async (notes) => {
402
401
  return new Map(notes.map((n) => {
403
402
  const verdict = n.forced ? void 0 : VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP));
404
403
  if (verdict !== void 0 && !truncated.includes(n.id)) return [n.id, verdict];
405
- const severity = severityOf(n, judgment, severityMinP);
406
- severities.set(n.id, severity);
407
- return [n.id, severity.tier === "could" ? "defer" : "edit"];
404
+ severities.set(n.id, severityOf(n, judgment, severityMinP));
405
+ return [n.id, "edit"];
408
406
  }));
409
407
  };
410
408
  const isCode$1 = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
411
409
  /**
412
- * A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
413
- * reviews and signs off or comments, and each note is judged: edits go to
414
- * another planning lap, questions are answered in place, nits fixed in one
415
- * batch, praise dropped.
410
+ * gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
411
+ * turn — and removes what the fixer left in `.gtd/QUALITY.md`. A re-review
412
+ * after fixed nits restores the doc as committed at `carry`, answers kept;
413
+ * a fresh one leaves out the notes the human already answered.
414
+ */
415
+ const composing = (base, report, carry, nitFix, answered = /* @__PURE__ */ new Set()) => run("review.composing", carry === void 0 ? writeScript(REVIEW, composedReview(base, {
416
+ ...report,
417
+ weakened: removedAssertions(changesSince(base)),
418
+ nitFix,
419
+ answered
420
+ }), [QUALITY]) : showScript(carry, REVIEW, REVIEW), {
421
+ label: "Composing the review",
422
+ file: REVIEW,
423
+ base
424
+ });
425
+ /**
426
+ * The quality lap's notes reach a human over everything since `base`, who
427
+ * answers each one — fix as suggested, won't fix, or their own answer — and
428
+ * may comment. A fix joins the nit batch, a won't-fix is dropped, and an own
429
+ * answer or comment is judged: edits go to another planning lap, questions
430
+ * are answered in place, nits fixed in one batch, praise dropped.
416
431
  */
417
432
  const review = (base, escalations = { rounds: 0 }, options = {}) => {
418
433
  const { guard, guardRests, fixAccess, rules } = options;
@@ -422,14 +437,16 @@ const review = (base, escalations = { rounds: 0 }, options = {}) => {
422
437
  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"
423
438
  };
424
439
  const severities = /* @__PURE__ */ new Map();
425
- let deferred = [];
440
+ let nitFix;
441
+ const answered = /* @__PURE__ */ new Set();
426
442
  return review$1(base, {
427
443
  file: REVIEW,
428
444
  feedback: feedback.file,
429
445
  isCode: isCode$1,
430
446
  reviewing: (b, carry) => {
431
- deferred = [];
432
- return reviewing(b, carry, options.lap);
447
+ const fixedAt = nitFix;
448
+ nitFix = void 0;
449
+ return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt, answered);
433
450
  },
434
451
  fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
435
452
  keepGreen: () => healthy(() => fixCheck(rules), {
@@ -438,25 +455,16 @@ const review = (base, escalations = { rounds: 0 }, options = {}) => {
438
455
  guardRests,
439
456
  rules
440
457
  }),
441
- awaitReview: (b) => {
442
- const notes = deferred;
443
- deferred = [];
444
- return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes)) : awaitReview(b);
445
- },
458
+ awaitReview: (b) => awaitReview(b),
446
459
  reviewMissing,
447
460
  collecting: feedback.collecting,
448
461
  triage: triage$1(severities),
449
- defer: (notes) => {
450
- deferred = notes.map((n) => ({
451
- id: n.id,
452
- anchor: n.anchor,
453
- text: n.text,
454
- label: severities.get(n.id).reason
455
- }));
456
- return deferNotes(deferred, head());
457
- },
462
+ decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
458
463
  answerQuestions: answerReviewQuestions,
459
- fixNits: guarded((notes) => fixNits(notes, fixAccess, rules), guard),
464
+ fixNits: async (notes) => {
465
+ await guarded((n) => fixNits(n, fixAccess, rules), guard)(notes);
466
+ nitFix = head();
467
+ },
460
468
  close: closeReview,
461
469
  captures: {
462
470
  edits: reviewEditsCapture,
@@ -464,8 +472,9 @@ const review = (base, escalations = { rounds: 0 }, options = {}) => {
464
472
  editNotes: (round, edits, answeredAt) => reviewEditNotesCapture(round, edits.map((n, i) => ({
465
473
  n,
466
474
  i,
467
- s: severities.get(n.id)
468
- })).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 }) => ({
475
+ s: severities.get(n.id),
476
+ key: captureKey(severities.get(n.id))
477
+ })).sort((x, y) => x.key - y.key || x.i - y.i).map(({ n, s }) => ({
469
478
  id: n.id,
470
479
  anchor: n.anchor,
471
480
  text: n.forced ? n.text.replace(/^!\s*/, "") : n.text,
@@ -483,7 +492,13 @@ function buildTail(first, second, ...rest) {
483
492
  const options = callForm(first, second, rest);
484
493
  if (options === void 0) return Promise.reject(/* @__PURE__ */ new Error("buildTail takes buildTail(base, options?) or the deprecated buildTail(fixFirst, base, green?, options?); this call fits neither"));
485
494
  const [base, form] = options;
486
- return tail((escalations) => review(base, escalations, form), form);
495
+ return tail((escalations, report) => review(base, escalations, {
496
+ ...form,
497
+ report
498
+ }), {
499
+ ...form,
500
+ triage: true
501
+ });
487
502
  }
488
503
  const isOptionalBoolean = (value) => value == null || typeof value === "boolean";
489
504
  const legacyForm = (fixFirst, base, green, options) => isOptionalBoolean(fixFirst) && isOptionalBoolean(green) && isOptionalObject(options) ? [base, {
@@ -818,8 +833,7 @@ const reviewedRefactor = async (shape, options = {}) => {
818
833
  feedback,
819
834
  fixAccess: strayAccess(p.strays),
820
835
  lensBase: entry,
821
- rules: refactorRules(p.strays, p.pattern),
822
- lap: "refactor"
836
+ rules: refactorRules(p.strays, p.pattern)
823
837
  });
824
838
  if (outcome.verdict === "signoff") return true;
825
839
  await reUnwind(outcome);
@@ -1430,13 +1444,11 @@ const settleQuality = async () => {
1430
1444
  }
1431
1445
  if (rested) await healthy(() => fixCheck());
1432
1446
  };
1433
- /** The quality lap, then the human review since `base`. */
1434
- const reviewedTail = (base, lensBase, lap) => tail(async (escalations) => {
1435
- await settleQuality();
1436
- return review(base, escalations, { lap });
1437
- }, {
1447
+ /** The triaged quality lap, then the human review since `base`. */
1448
+ const reviewedTail = (base, lensBase) => tail((escalations, report) => review(base, escalations, { report }), {
1438
1449
  green: true,
1439
- lensBase
1450
+ lensBase,
1451
+ triage: true
1440
1452
  });
1441
1453
  const featureFeedback = async (first, lensBase, ids) => {
1442
1454
  let outcome = first;
@@ -1456,7 +1468,7 @@ const featureFeedback = async (first, lensBase, ids) => {
1456
1468
  tier: riskTierOn() ? recordRisk() : void 0,
1457
1469
  amend: true
1458
1470
  });
1459
- outcome = await reviewedTail(outcome.base, lensBase, "feedback");
1471
+ outcome = await reviewedTail(outcome.base, lensBase);
1460
1472
  }
1461
1473
  };
1462
1474
  /** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
@@ -1474,10 +1486,7 @@ const afterTail = async (first) => {
1474
1486
  tier: riskTierOn() ? recordRisk() : void 0,
1475
1487
  amend: true
1476
1488
  });
1477
- outcome = await buildTail(outcome.base, {
1478
- green: true,
1479
- lap: "feedback"
1480
- });
1489
+ outcome = await buildTail(outcome.base, { green: true });
1481
1490
  }
1482
1491
  };
1483
1492
  /** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
@@ -1505,4 +1514,4 @@ const feature = shipped(ordinaryStart);
1505
1514
  //#region src/workflows/feature/index.ts
1506
1515
  var feature_default = feature;
1507
1516
  //#endregion
1508
- export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, closeOut, collecting, criteriaOf, criterionIds, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, triage, unstableIds, unwind };
1517
+ export { COMMENT, REFACTOR_PACKAGE, acceptanceFault, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, closeOut, collecting, criteriaOf, criterionIds, feature_default as default, defaults, design, envDefaults, feature, fixNits, holdsCallSites, implement, implementing, isTestFile, judgeSeam, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireCriteriaIfEdited, requireNoNewFlags, requireNoTestFiles, requireSlices, requireStableIds, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, sliceCap, sliceCriteria, sliceIds, slicesOf, slicing, statedCriteria, steering, summary, triage, unstableIds, unwind };
@@ -1,7 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
2
- import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
3
- import { A as agentWithSkills, J as fixReviewNotesPrompt, S as coder, St as envDefaults, c as baseline, i as skills, n as summary, q as finisherSystem, r as access, s as shipped, t as steering, xt as defaults, y as REVIEW } from "../chunks/reserved-Ch4LLrEj.mjs";
4
- import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-lY1UmJrU.mjs";
2
+ import { g as reviewRisks, h as reviewNotes } from "../chunks/steering-D4LUQ-av.mjs";
3
+ import { A as agentWithSkills, Ct as envDefaults, K as finisherSystem, S as coder, St as defaults, c as baseline, i as skills, n as summary, q as fixReviewNotesPrompt, r as access, s as shipped, t as steering, y as REVIEW } from "../chunks/reserved-COEMFEpy.mjs";
4
+ import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-C_xn8U7U.mjs";
5
5
  //#region src/workflows/fix/fix.ts
6
6
  /**
7
7
  * Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
@@ -1,6 +1,6 @@
1
1
  import { start } from "../flows.mjs";
2
- import { St as envDefaults, dt as reviewGateBlockedMessage, i as skills, n as summary, r as access, s as shipped, t as steering, u as gate, xt as defaults } from "../chunks/reserved-Ch4LLrEj.mjs";
3
- import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-lY1UmJrU.mjs";
2
+ import { Ct as envDefaults, St as defaults, ft as reviewGateBlockedMessage, i as skills, n as summary, r as access, s as shipped, t as steering, u as gate } from "../chunks/reserved-COEMFEpy.mjs";
3
+ import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-C_xn8U7U.mjs";
4
4
  //#region src/workflows/review/review.ts
5
5
  /**
6
6
  * Pure review of everything since `reviewBase`: a green gate, the quality
@@ -1,5 +1,5 @@
1
1
  import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
2
- import { Et as flipProblems, Ot as requireFlag, S as coder, St as envDefaults, i as skills, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, wt as flagMentions, xt as defaults } from "../chunks/reserved-Ch4LLrEj.mjs";
2
+ import { Ct as envDefaults, Dt as flipProblems, S as coder, St as defaults, Tt as flagMentions, i as skills, kt as requireFlag, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate } from "../chunks/reserved-COEMFEpy.mjs";
3
3
  //#region src/workflows/rollout/rollout.ts
4
4
  const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
5
5
  const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";