@pmelab/gtd 48.0.0 → 50.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 (45) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +31 -20
  3. package/dist/chunks/{reserved-DF1HD6jL.mjs → reserved-CdOzKFVX.mjs} +150 -24
  4. package/dist/chunks/{review-Bv40kZu9.mjs → review-BChXEl2Y.mjs} +132 -24
  5. package/dist/chunks/{steering-D7hWeMgE.mjs → steering-DtUVUz9l.mjs} +49 -13
  6. package/dist/chunks/{tail-D89fU_wf.mjs → tail-DRM2eC1z.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +22 -5
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +1 -1
  10. package/dist/web/main.js +27 -3
  11. package/dist/workflow-kit.mjs +1 -1
  12. package/dist/workflows/cleanup.mjs +2 -2
  13. package/dist/workflows/feature.mjs +16 -9
  14. package/dist/workflows/fix.mjs +3 -3
  15. package/dist/workflows/review.mjs +3 -3
  16. package/dist/workflows/rollout.mjs +1 -1
  17. package/dist/workflows.mjs +338 -62
  18. package/package.json +1 -1
  19. package/src/steering/index.ts +2 -0
  20. package/src/steering/review.test.ts +87 -0
  21. package/src/steering/review.ts +75 -8
  22. package/src/workflows/feature/coverage.feature +3 -3
  23. package/src/workflows/feature/fast-path.feature +2 -2
  24. package/src/workflows/feature/feature.ts +10 -3
  25. package/src/workflows/feature/flag-mode.feature +2 -2
  26. package/src/workflows/feature/lens-review.feature +128 -13
  27. package/src/workflows/feature/one-step-land.feature +2 -2
  28. package/src/workflows/feature/refactor-absorbs.feature +2 -2
  29. package/src/workflows/feature/refactor-first.feature +2 -2
  30. package/src/workflows/feature/refactor-rebuild.feature +2 -2
  31. package/src/workflows/feature/refactor-review.feature +6 -6
  32. package/src/workflows/feature/review-answers.feature +51 -5
  33. package/src/workflows/feature/review-gate.feature +23 -6
  34. package/src/workflows/feature/reviewer-walkthrough.feature +240 -0
  35. package/src/workflows/feature/seam-rejection.feature +2 -2
  36. package/src/workflows/feature/seam.feature +2 -2
  37. package/src/workflows/feature/severity-budget.feature +111 -0
  38. package/src/workflows/feature/severity-sorted-review.feature +197 -0
  39. package/src/workflows/feature/slice-findings.feature +68 -5
  40. package/src/workflows/feature/slicing.feature +6 -6
  41. package/src/workflows/feature/standards.feature +29 -1
  42. package/src/workflows/shared/review.ts +226 -25
  43. package/src/workflows/shared/steps.ts +3 -2
  44. package/src/workflows/shared/text.test.ts +216 -0
  45. package/src/workflows/shared/text.ts +201 -19
@@ -1276,6 +1276,33 @@ describe("review.view", () => {
1276
1276
  expect(view.nodes[1]!.detail).toBe("")
1277
1277
  })
1278
1278
 
1279
+ it("shows a chunk's leading severity line as its status, not its prose", () => {
1280
+ const view = review.view(
1281
+ [
1282
+ "# Review: abc1234",
1283
+ "",
1284
+ "## Ranked",
1285
+ "",
1286
+ "_critical: security, one-way door_",
1287
+ "",
1288
+ "The new export.",
1289
+ "",
1290
+ "- [ ] ./a.ts#1-1",
1291
+ "",
1292
+ "## Unranked",
1293
+ "",
1294
+ "_emphasis, not a tier_",
1295
+ "",
1296
+ "- [ ] ./a.ts#2-2",
1297
+ "",
1298
+ ].join("\n"),
1299
+ )
1300
+ expect(view.nodes.map((c) => [c.status, c.detail])).toEqual([
1301
+ ["critical: security, one-way door", "The new export."],
1302
+ [undefined, "_emphasis, not a tier_"],
1303
+ ])
1304
+ })
1305
+
1279
1306
  it("is built from one parse of the document, not one per element", () => {
1280
1307
  const uniqueContent = [
1281
1308
  "# Review: def4567",
@@ -1706,6 +1733,29 @@ describe("reviewNotes", async () => {
1706
1733
  expect(reviewNotes(base, base)).toEqual([])
1707
1734
  })
1708
1735
 
1736
+ it("an edited tier line is a note", () => {
1737
+ const was = doc("_could_\n\nProse.")
1738
+ const after = doc("_critical: this leaks the token_\n\nProse.")
1739
+ expect(reviewNotes(was, after)).toMatchObject([
1740
+ {
1741
+ kind: "chunk",
1742
+ before: "Prose.\n\n_could_",
1743
+ text: "Prose.\n\n_critical: this leaks the token_",
1744
+ },
1745
+ ])
1746
+ })
1747
+
1748
+ it("prose added under a tier is the human's alone, and a leading ! still forces", () => {
1749
+ const was = doc("_could_\n\nProse.")
1750
+ expect(reviewNotes(was, doc("_could_\n\n! Not here.\n\nProse."))).toMatchObject([
1751
+ { kind: "chunk", text: "! Not here. Prose.", forced: true },
1752
+ ])
1753
+ })
1754
+
1755
+ it("a deleted tier line asks nothing", () => {
1756
+ expect(reviewNotes(doc("_could_\n\nProse."), base)).toEqual([])
1757
+ })
1758
+
1709
1759
  it("a pointer note added", () => {
1710
1760
  const after = doc("Prose.").replace("#1-3", "#1-3 rename this")
1711
1761
  expect(reviewNotes(base, after)).toEqual([
@@ -1840,6 +1890,43 @@ describe("reviewRisks", async () => {
1840
1890
  })
1841
1891
  })
1842
1892
 
1893
+ describe("reviewChunks", async () => {
1894
+ const { reviewChunks } = await import("./review.js")
1895
+
1896
+ it("counts each chunk's pointers, a chunk of prose and answer boxes holding none", () => {
1897
+ const content = [
1898
+ "# Review: abc1234",
1899
+ "",
1900
+ "<!-- base: abc1234def5678901234567890123456789abcd -->",
1901
+ "",
1902
+ "## Walkthrough",
1903
+ "",
1904
+ "- [ ] ./src/a.ts#1-3 — first",
1905
+ "- [ ] ./src/b.ts#4-6 — second",
1906
+ "",
1907
+ "## Fixed automatically",
1908
+ "",
1909
+ "Tick exactly one:",
1910
+ "",
1911
+ "- [ ] fix as suggested",
1912
+ "- [ ] won't fix",
1913
+ "- [ ] _your answer_",
1914
+ ].join("\n")
1915
+ expect(reviewChunks(content)).toEqual([
1916
+ {
1917
+ title: "Walkthrough",
1918
+ line: 4,
1919
+ pointers: 2,
1920
+ files: [
1921
+ { path: "./src/a.ts", line: 1, rangeEnd: 3 },
1922
+ { path: "./src/b.ts", line: 4, rangeEnd: 6 },
1923
+ ],
1924
+ },
1925
+ { title: "Fixed automatically", line: 9, pointers: 0, files: [] },
1926
+ ])
1927
+ })
1928
+ })
1929
+
1843
1930
  describe("answer boxes", async () => {
1844
1931
  const { reviewDecisions, reviewNotes, unansweredNotes } = await import("./review.js")
1845
1932
  const note = (title: string, boxes: readonly [string, string, string]): string =>
@@ -77,6 +77,8 @@ interface Changeset {
77
77
  readonly headingLine: number
78
78
  /** The chunk's answer boxes, when gtd gave it some. */
79
79
  readonly answers?: readonly AnswerOption[]
80
+ /** The tier and reason gtd ranked the chunk with, from its leading severity line. */
81
+ readonly severity?: { readonly label: string; readonly line: number }
80
82
  }
81
83
 
82
84
  interface AnswerOption {
@@ -89,6 +91,10 @@ export const ANSWER_LEAD = "Tick exactly one:"
89
91
  export const ANSWER_FIX = "fix as suggested"
90
92
  export const ANSWER_WONT_FIX = "won't fix"
91
93
 
94
+ /** A chunk's leading `_critical: security_` paragraph: the tier gtd ranked it with, and why. */
95
+ const SEVERITY_LINE_RE = /^_((?:critical|should|could)(?::[^_\n]*)?)_$/
96
+ export const severityLine = (label: string): string => `_${label}_`
97
+
92
98
  interface ReviewDoc {
93
99
  readonly shortHash?: string
94
100
  readonly fullHash?: string
@@ -521,6 +527,26 @@ const answerListOf = (
521
527
  return { lead, list, options }
522
528
  }
523
529
 
530
+ /** A leading severity line is the chunk's tier, never its prose. */
531
+ const splitSeverity = (
532
+ content: string,
533
+ run: readonly RootContent[],
534
+ ): {
535
+ readonly severity?: { readonly label: string; readonly line: number } | undefined
536
+ readonly leadingRun: readonly RootContent[]
537
+ } => {
538
+ const first = run[0]
539
+ const label =
540
+ first?.type === "paragraph"
541
+ ? SEVERITY_LINE_RE.exec(sourceText(content, first).trim())?.[1]
542
+ : undefined
543
+ if (label === undefined) return { leadingRun: run }
544
+ return {
545
+ severity: { label, line: toLspPosition(first!.position!.start).line },
546
+ leadingRun: run.slice(1),
547
+ }
548
+ }
549
+
524
550
  /** 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). */
525
551
  const parseChunkBody = (
526
552
  content: string,
@@ -534,6 +560,7 @@ const parseChunkBody = (
534
560
  readonly files: readonly ReviewFile[]
535
561
  readonly errors: readonly SteeringFinding[]
536
562
  readonly answers?: readonly AnswerOption[]
563
+ readonly severity?: { readonly label: string; readonly line: number } | undefined
537
564
  /** Whether a task box outside the answer list exists. */
538
565
  readonly boxed: boolean
539
566
  } => {
@@ -543,9 +570,10 @@ const parseChunkBody = (
543
570
  const firstPointerIndex = body.findIndex(
544
571
  (n) => !isAnswer(n) && taskItems(n).some((item) => hasPointerToken(content, item)),
545
572
  )
546
- const leadingRun = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter(
573
+ const run = (firstPointerIndex === -1 ? body : body.slice(0, firstPointerIndex)).filter(
547
574
  (n) => !isAnswer(n),
548
575
  )
576
+ const { severity, leadingRun } = splitSeverity(content, run)
549
577
  const descriptionNodes = blockNodesOfRun(content, leadingRun)
550
578
  const descriptionInline = collapseImages(blockRunInline(content, leadingRun))
551
579
  const description = leadingRun
@@ -569,6 +597,7 @@ const parseChunkBody = (
569
597
  files,
570
598
  errors,
571
599
  ...(answer === undefined ? {} : { answers: answer.options }),
600
+ ...(severity === undefined ? {} : { severity }),
572
601
  boxed: body.some((n) => !isAnswer(n) && taskItems(n).length > 0),
573
602
  }
574
603
  }
@@ -618,6 +647,7 @@ const parseChangesets = (
618
647
  descriptionInline,
619
648
  files,
620
649
  answers,
650
+ severity,
621
651
  boxed,
622
652
  errors: bodyErrors,
623
653
  } = parseChunkBody(content, lines, title, body)
@@ -638,6 +668,7 @@ const parseChangesets = (
638
668
  files,
639
669
  headingLine,
640
670
  ...(answers === undefined ? {} : { answers }),
671
+ ...(severity === undefined ? {} : { severity }),
641
672
  })
642
673
  }
643
674
  if (changesets.length === 0) errors.push({ message: "REVIEW.md has no '##' chunks" })
@@ -997,6 +1028,7 @@ const reviewView = (content: string): SteeringView => {
997
1028
  detail: chunk.description,
998
1029
  detailInline: chunk.descriptionInline,
999
1030
  body: chunk.descriptionNodes,
1031
+ ...(chunk.severity === undefined ? {} : { status: chunk.severity.label }),
1000
1032
  anchor: { kind: "chunk", index: chunkIndex },
1001
1033
  ...noteAt(chunk.headingLine),
1002
1034
  children: chunk.files.map((file, index) => ({
@@ -1177,11 +1209,22 @@ const pointerLabel = (f: ReviewFile): string =>
1177
1209
 
1178
1210
  type Found = { line: number; note: Omit<ReviewNote, "id" | "forced"> }
1179
1211
 
1180
- // A chunk added with its own answer boxes (a driver's imported note) is
1181
- // answered by its tick, never judged as prose as well.
1182
- const chunkNote = (chunk: Changeset, prior: Changeset | undefined): Found[] =>
1183
- chunk.description === (prior?.description ?? "") ||
1184
- (prior === undefined && chunk.answers !== undefined)
1212
+ // Rewording a tier disputes it, so a changed tier joins the note — after the
1213
+ // prose, so a leading `!` still forces; deleting one asks nothing.
1214
+ const chunkProse = (chunk: Changeset | undefined, retiered: boolean): string =>
1215
+ [
1216
+ chunk?.description ?? "",
1217
+ retiered && chunk?.severity !== undefined ? severityLine(chunk.severity.label) : "",
1218
+ ]
1219
+ .filter((p) => p !== "")
1220
+ .join("\n\n")
1221
+
1222
+ const chunkNote = (chunk: Changeset, prior: Changeset | undefined): Found[] => {
1223
+ const retiered = chunk.severity !== undefined && chunk.severity.label !== prior?.severity?.label
1224
+ // A chunk added with its own answer boxes (a driver's imported note) is
1225
+ // answered by its tick, never judged as prose as well.
1226
+ return chunkProse(chunk, retiered) === chunkProse(prior, retiered) ||
1227
+ (prior === undefined && chunk.answers !== undefined)
1185
1228
  ? []
1186
1229
  : [
1187
1230
  {
@@ -1189,11 +1232,12 @@ const chunkNote = (chunk: Changeset, prior: Changeset | undefined): Found[] =>
1189
1232
  note: {
1190
1233
  kind: "chunk",
1191
1234
  anchor: chunk.title,
1192
- before: prior?.description ?? "",
1193
- text: chunk.description,
1235
+ before: chunkProse(prior, retiered),
1236
+ text: chunkProse(chunk, retiered),
1194
1237
  },
1195
1238
  },
1196
1239
  ]
1240
+ }
1197
1241
 
1198
1242
  const pointerNotes = (chunk: Changeset, prior: Changeset | undefined): Found[] =>
1199
1243
  chunk.files.flatMap((file): Found[] => {
@@ -1338,3 +1382,26 @@ export const reviewDecisions = (before: string, after: string): readonly ReviewD
1338
1382
  return old?.choice === decision.choice && old.text === decision.text ? [] : [decision]
1339
1383
  })
1340
1384
  }
1385
+
1386
+ /** Each `##` chunk in document order: its title, 0-based heading line and how many hunk pointers it holds. */
1387
+ export const reviewChunks = (
1388
+ content: string,
1389
+ ): readonly {
1390
+ title: string
1391
+ line: number
1392
+ pointers: number
1393
+ /** 0-based line of the chunk's severity line, when it has one. */
1394
+ severityLine?: number
1395
+ files: readonly { path: string; line?: number; rangeEnd?: number }[]
1396
+ }[] =>
1397
+ parseReviewDoc(content).changesets.map((c) => ({
1398
+ title: c.title,
1399
+ line: c.headingLine,
1400
+ pointers: c.files.length,
1401
+ ...(c.severity === undefined ? {} : { severityLine: c.severity.line }),
1402
+ files: c.files.map((f) => ({
1403
+ path: f.path,
1404
+ ...(f.line === undefined ? {} : { line: f.line }),
1405
+ ...(f.rangeEnd === undefined ? {} : { rangeEnd: f.rangeEnd }),
1406
+ })),
1407
+ }))
@@ -61,13 +61,13 @@ Feature: a feature is done only when every acceptance criterion has a passing te
61
61
  [{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
62
62
  """
63
63
  When the process is driven
64
- Then the process rests at "build.review.composing"
64
+ Then the process rests at "build.review.reviewing"
65
65
  And the commit subjects end with:
66
66
  """
67
67
  gtd(check): implement.applying → implement.health.check
68
68
  gtd(check): implement.health.check → implement.coverage
69
69
  gtd(judge): implement.coverage → implement.closing
70
- gtd(check): implement.closing → build.review.composing
70
+ gtd(check): implement.closing → build.review.reviewing
71
71
  """
72
72
  And the commit "gtd(judge): implement.coverage → implement.closing" has a body containing "Gtd-Mark: covers=AC-1"
73
73
  And the commit "gtd(judge): implement.coverage → implement.closing" has a body containing "Gtd-Mark: covers=AC-2"
@@ -144,7 +144,7 @@ Feature: a feature is done only when every acceptance criterion has a passing te
144
144
  """
145
145
  And step "implement.uncovered" changes nothing
146
146
  When the process is driven
147
- Then the process rests at "build.review.composing"
147
+ Then the process rests at "build.review.reviewing"
148
148
  And the commit "gtd(human): implement.uncovered → implement.closing" has a body containing "Gtd-Mark: covers=AC-1"
149
149
  And the commit "gtd(human): implement.uncovered → implement.closing" has a body containing "Gtd-Mark: uncovered=AC-2"
150
150
 
@@ -64,7 +64,7 @@ Feature: a trivial change takes the same few beats at either risk tier
64
64
  """
65
65
  And step "implement.applying" runs its script
66
66
  When the process is driven
67
- Then the process rests at "build.review.composing"
67
+ Then the process rests at "build.review.reviewing"
68
68
  And "src/greeting.ts" contains "Hello, world"
69
69
  And the commit subjects end with:
70
70
  """
@@ -80,7 +80,7 @@ Feature: a trivial change takes the same few beats at either risk tier
80
80
  gtd(check): implement.health.check → implement.coverage
81
81
  gtd(judge): implement.coverage → implement.closing
82
82
  gtd(check): implement.closing → build.quality.correctness.reviewing
83
- gtd(agent): build.quality.correctness.reviewing → build.review.composing
83
+ gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
84
84
  """
85
85
  And the output of gtd "summary" <summary> "needs a human reviewer"
86
86
 
@@ -38,7 +38,8 @@ import { recordRisk, riskTierOn, type RiskTier } from "./tier.js"
38
38
  // judged with `flagMode` on: it lands as the implementation, or (refactoring
39
39
  // first when the judge asks) is built fresh from → the health loop → the
40
40
  // triaged quality lap, whose leftovers are carried on → after the last slice,
41
- // human review of the whole change and every slice's findings, which
41
+ // a reviewer's walkthrough of the whole change, every slice's findings
42
+ // among its chunks, for the human, which
42
43
  // signs off (the episode ends back at `idle`) or sends a design → implement
43
44
  // lap.
44
45
 
@@ -152,11 +153,15 @@ const buildSlice = async (sliceBase: string, build: SliceBuild): Promise<void> =
152
153
  })
153
154
  }
154
155
 
155
- /** The quality lap of slice `last.slice`, then the human review since `base`, listing what earlier slices' `laps` left too. */
156
+ /**
157
+ * The quality lap of slice `last.slice`, then a reviewer's walkthrough since
158
+ * `base` for the human, carrying what earlier slices' `laps` left too.
159
+ */
156
160
  const reviewedTail = (
157
161
  base: string,
158
162
  lensBase: string,
159
163
  last: { readonly slice: string; readonly laps: SliceBuild["laps"] } = { slice: "", laps: [] },
164
+ lap: "first" | "feedback" = "first",
160
165
  ): Promise<ReviewOutcome> =>
161
166
  tail(
162
167
  (escalations, report) =>
@@ -165,6 +170,8 @@ const reviewedTail = (
165
170
  last.laps.length === 0
166
171
  ? report
167
172
  : joinReports([...last.laps, { slice: last.slice, report }]),
173
+ walkthrough: true,
174
+ lap,
168
175
  }),
169
176
  { green: true, lensBase },
170
177
  )
@@ -189,7 +196,7 @@ const featureFeedback = async (
189
196
  })
190
197
  const tier = riskTierOn() ? recordRisk() : undefined
191
198
  closed = await implementing({ tier, amend: true })
192
- outcome = await reviewedTail(outcome.base, lensBase)
199
+ outcome = await reviewedTail(outcome.base, lensBase, undefined, "feedback")
193
200
  }
194
201
  }
195
202
 
@@ -67,7 +67,7 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
67
67
  """
68
68
  And step "build.quality.correctness.reviewing" changes nothing
69
69
  When the process is driven
70
- Then the process rests at "build.review.composing"
70
+ Then the process rests at "build.review.reviewing"
71
71
  And "src/calc.ts" contains "subtract"
72
72
  And "src/calc.test.ts" contains "[AC-1] subtracts"
73
73
  And the commit subjects end with:
@@ -85,5 +85,5 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
85
85
  gtd(check): implement.health.check → implement.coverage
86
86
  gtd(judge): implement.coverage → implement.closing
87
87
  gtd(check): implement.closing → build.quality.correctness.reviewing
88
- gtd(agent): build.quality.correctness.reviewing → build.review.composing
88
+ gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
89
89
  """
@@ -1,12 +1,11 @@
1
1
  @inmem
2
- Feature: the quality lenses are the only agentic review
2
+ Feature: the quality lap's findings are triaged before the review
3
3
 
4
4
  A judge classifies each `## ` finding the lenses wrote to `.gtd/QUALITY.md`:
5
5
  a mechanical one is fixed by the fixer, noise is dropped with its `p` kept in
6
6
  a trailer, and a design one becomes a note in `.gtd/REVIEW.md`, with answer
7
- boxes. gtd writes that file itself — no reviewer turn runs — and lists,
8
- without tick boxes, what was fixed automatically and what was dropped as
9
- noise.
7
+ boxes, in the chunk the reviewer gave it. gtd lists, without tick boxes,
8
+ what was fixed automatically and what was dropped as noise.
10
9
 
11
10
  Background:
12
11
  Given the shipped workflows, as gtd install scaffolds them
@@ -75,7 +74,11 @@ Feature: the quality lenses are the only agentic review
75
74
  """
76
75
  And step "build.quality.sorting" runs its script
77
76
  And step "build.quality.closing" runs its script
78
- And step "build.review.composing" runs its script
77
+ And step "build.review.carrying" runs its script
78
+ And judge step "build.review.ranking" answers:
79
+ """
80
+ []
81
+ """
79
82
 
80
83
  Scenario: a mechanical finding is fixed and listed, noise dropped and listed, a design one noted
81
84
  Given judge step "build.quality.triage" answers:
@@ -89,6 +92,20 @@ Feature: the quality lenses are the only agentic review
89
92
  """
90
93
  And step "build.fix.quality.fixing" deletes ".gtd/QUALITY.md"
91
94
  And step "build.health.check" changes nothing
95
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
96
+ """
97
+ # Review: abc1234
98
+
99
+ <!-- base: 0000000 -->
100
+
101
+ ## Subtract
102
+
103
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
104
+
105
+ ## AC-1 ignores floating point
106
+
107
+ - [ ] ./src/calc.ts#2-2 — where it applies
108
+ """
92
109
  When the process is driven
93
110
  Then the process rests at "build.review.await-review"
94
111
  And ".gtd/QUALITY.md" does not exist
@@ -97,8 +114,14 @@ Feature: the quality lenses are the only agentic review
97
114
  """
98
115
  ## AC-1 ignores floating point
99
116
 
117
+ _critical: judge unsure_
118
+
100
119
  Should subtract round? The criterion does not say.
101
120
 
121
+ In the code:
122
+
123
+ - [ ] ./src/calc.ts#2-2 — where it applies
124
+
102
125
  Tick exactly one:
103
126
 
104
127
  - [ ] fix as suggested
@@ -129,8 +152,10 @@ Feature: the quality lenses are the only agentic review
129
152
  gtd(judge): build.quality.triage → build.quality.sorting
130
153
  gtd(check): build.quality.sorting → build.fix.quality.fixing
131
154
  gtd(agent): build.fix.quality.fixing → build.health.check
132
- gtd(check): build.health.check → build.review.composing
133
- gtd(check): build.review.composing → build.review.await-review
155
+ gtd(check): build.health.check → build.review.reviewing
156
+ gtd(agent): build.review.reviewing → build.review.ranking
157
+ gtd(judge): build.review.ranking → build.review.carrying
158
+ gtd(check): build.review.carrying → build.review.await-review
134
159
  """
135
160
 
136
161
  Scenario: an unsure verdict makes a finding a note, never a fix or a drop
@@ -138,6 +163,28 @@ Feature: the quality lenses are the only agentic review
138
163
  """
139
164
  [{"id":"Q1","answer":"mechanical","p":0.5},{"id":"Q2","answer":"noise","p":0.6}]
140
165
  """
166
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
167
+ """
168
+ # Review: abc1234
169
+
170
+ <!-- base: 0000000 -->
171
+
172
+ ## Subtract
173
+
174
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
175
+
176
+ ## subtract has no negative test
177
+
178
+ - [ ] ./src/calc.ts#2-2 — where it applies
179
+
180
+ ## add could be a method
181
+
182
+ - [ ] ./src/calc.ts#2-2 — where it applies
183
+
184
+ ## AC-1 ignores floating point
185
+
186
+ - [ ] ./src/calc.ts#2-2 — where it applies
187
+ """
141
188
  When the process is driven
142
189
  Then the process rests at "build.review.await-review"
143
190
  And ".gtd/REVIEW.md" contains "## subtract has no negative test"
@@ -148,8 +195,10 @@ Feature: the quality lenses are the only agentic review
148
195
  And the commit subjects end with:
149
196
  """
150
197
  gtd(judge): build.quality.triage → build.quality.sorting
151
- gtd(check): build.quality.sorting → build.review.composing
152
- gtd(check): build.review.composing → build.review.await-review
198
+ gtd(check): build.quality.sorting → build.review.reviewing
199
+ gtd(agent): build.review.reviewing → build.review.ranking
200
+ gtd(judge): build.review.ranking → build.review.carrying
201
+ gtd(check): build.review.carrying → build.review.await-review
153
202
  """
154
203
 
155
204
  Scenario: a mechanical finding the fix cap leaves becomes a note
@@ -166,6 +215,20 @@ Feature: the quality lenses are the only agentic review
166
215
  A class would read better.
167
216
  """
168
217
  And step "build.health.check" changes nothing
218
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
219
+ """
220
+ # Review: abc1234
221
+
222
+ <!-- base: 0000000 -->
223
+
224
+ ## Subtract
225
+
226
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
227
+
228
+ ## add could be a method
229
+
230
+ - [ ] ./src/calc.ts#2-2 — where it applies
231
+ """
169
232
  When the process is driven
170
233
  Then the process rests at "build.review.await-review"
171
234
  And ".gtd/QUALITY.md" does not exist
@@ -173,6 +236,8 @@ Feature: the quality lenses are the only agentic review
173
236
  """
174
237
  ## add could be a method
175
238
 
239
+ _critical: judge unsure_
240
+
176
241
  _The fixer left this unresolved._
177
242
 
178
243
  A class would read better.
@@ -186,8 +251,10 @@ Feature: the quality lenses are the only agentic review
186
251
  gtd(agent): build.fix.quality.fixing
187
252
  gtd(agent): build.fix.quality.fixing → build.health.check
188
253
  gtd(check): build.health.check → build.quality.closing
189
- gtd(check): build.quality.closing → build.review.composing
190
- gtd(check): build.review.composing → build.review.await-review
254
+ gtd(check): build.quality.closing → build.review.reviewing
255
+ gtd(agent): build.review.reviewing → build.review.ranking
256
+ gtd(judge): build.review.ranking → build.review.carrying
257
+ gtd(check): build.review.carrying → build.review.await-review
191
258
  """
192
259
 
193
260
  Scenario: a fixer that renames a chunk it leaves counts nothing fixed
@@ -204,6 +271,32 @@ Feature: the quality lenses are the only agentic review
204
271
  Still open.
205
272
  """
206
273
  And step "build.health.check" changes nothing
274
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
275
+ """
276
+ # Review: abc1234
277
+
278
+ <!-- base: 0000000 -->
279
+
280
+ ## Subtract
281
+
282
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
283
+
284
+ ## subtract has no negative test
285
+
286
+ - [ ] ./src/calc.ts#2-2 — where it applies
287
+
288
+ ## add could be a method
289
+
290
+ - [ ] ./src/calc.ts#2-2 — where it applies
291
+
292
+ ## AC-1 ignores floating point
293
+
294
+ - [ ] ./src/calc.ts#2-2 — where it applies
295
+
296
+ ## null check in parse()
297
+
298
+ - [ ] ./src/calc.ts#2-2 — where it applies
299
+ """
207
300
  When the process is driven
208
301
  Then the process rests at "build.review.await-review"
209
302
  And ".gtd/REVIEW.md" contains "## null check in parse()"
@@ -223,17 +316,37 @@ Feature: the quality lenses are the only agentic review
223
316
  All fixed.
224
317
  """
225
318
  And step "build.health.check" changes nothing
319
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
320
+ """
321
+ # Review: abc1234
322
+
323
+ <!-- base: 0000000 -->
324
+
325
+ ## Subtract
326
+
327
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
328
+ """
226
329
  When the process is driven
227
330
  Then the process rests at "build.review.await-review"
228
331
  And ".gtd/REVIEW.md" contains "- subtract has no negative test ("
229
332
  And ".gtd/REVIEW.md" does not contain "check whether it was fixed"
230
333
 
231
- Scenario: signing off a composed review ends the process
334
+ Scenario: signing off a review with no notes ends the process
232
335
  Given judge step "build.quality.triage" answers:
233
336
  """
234
337
  [{"id":"Q1","answer":"noise","p":0.9},{"id":"Q2","answer":"noise","p":0.9},{"id":"Q3","answer":"noise","p":0.9}]
235
338
  """
236
339
  And step "build.review.closing" runs its script
340
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
341
+ """
342
+ # Review: abc1234
343
+
344
+ <!-- base: 0000000 -->
345
+
346
+ ## Subtract
347
+
348
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
349
+ """
237
350
  When the process is driven
238
351
  Then the process rests at "build.review.await-review"
239
352
  When gtd lands
@@ -242,7 +355,9 @@ Feature: the quality lenses are the only agentic review
242
355
  And ".gtd/REVIEW.md" does not exist
243
356
  And the commit subjects end with:
244
357
  """
245
- gtd(check): build.review.composing → build.review.await-review
358
+ gtd(agent): build.review.reviewing → build.review.ranking
359
+ gtd(judge): build.review.ranking → build.review.carrying
360
+ gtd(check): build.review.carrying → build.review.await-review
246
361
  gtd(human): build.review.await-review → build.review.closing
247
362
  gtd(check): build.review.closing → idle
248
363
  """
@@ -64,7 +64,7 @@ Feature: a naive pass the judge rules land is itself the implementation
64
64
 
65
65
  Scenario: a land judgment applies the naive pass and runs from idle to the review gate with no rebuild
66
66
  When the process is driven
67
- Then the process rests at "build.review.composing"
67
+ Then the process rests at "build.review.reviewing"
68
68
  And "src/greeting.ts" contains "Hello, world"
69
69
  And "src/acceptance.test.ts" contains "[AC-1] holds"
70
70
  And the commit subjects end with:
@@ -81,7 +81,7 @@ Feature: a naive pass the judge rules land is itself the implementation
81
81
  gtd(check): implement.health.check → implement.coverage
82
82
  gtd(judge): implement.coverage → implement.closing
83
83
  gtd(check): implement.closing → build.quality.correctness.reviewing
84
- gtd(agent): build.quality.correctness.reviewing → build.review.composing
84
+ gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
85
85
  """
86
86
  And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
87
87
  And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
@@ -113,7 +113,7 @@ Feature: a refactor that carried the whole feature still gets a quality lap over
113
113
 
114
114
  Given step "build.quality.correctness.reviewing" changes nothing
115
115
  When the process is driven
116
- Then the process rests at "build.review.composing"
116
+ Then the process rests at "build.review.reviewing"
117
117
  And "src/calc.ts" contains "subtract"
118
118
  And the commit subjects end with:
119
119
  """
@@ -123,7 +123,7 @@ Feature: a refactor that carried the whole feature still gets a quality lap over
123
123
  gtd(check): implement.health.check → implement.coverage
124
124
  gtd(judge): implement.coverage → implement.closing
125
125
  gtd(check): implement.closing → build.quality.correctness.reviewing
126
- gtd(agent): build.quality.correctness.reviewing → build.review.composing
126
+ gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
127
127
  """
128
128
  And the output lines of gtd "slices" start with:
129
129
  """