@pmelab/gtd 42.0.0 → 43.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +79 -35
  3. package/claude/hooks/sketch.ts +1 -1
  4. package/dist/chunks/{reserved-D7MNY9QJ.mjs → reserved-COEMFEpy.mjs} +35 -46
  5. package/dist/chunks/{steering-oCDdQlpP.mjs → steering-D4LUQ-av.mjs} +95 -12
  6. package/dist/chunks/{tail-D2o2h5cc.mjs → tail-C_xn8U7U.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +488 -30
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +447 -27
  10. package/dist/web/main.js +42 -13
  11. package/dist/workflow-kit.mjs +61 -25
  12. package/dist/workflows/cleanup.mjs +2 -2
  13. package/dist/workflows/feature.mjs +31 -42
  14. package/dist/workflows/fix.mjs +3 -3
  15. package/dist/workflows/review.mjs +2 -2
  16. package/dist/workflows/rollout.mjs +1 -1
  17. package/dist/workflows.mjs +217 -121
  18. package/package.json +1 -1
  19. package/src/cli/Cli.test.ts +33 -1
  20. package/src/cli/Cli.ts +70 -6
  21. package/src/cli/History.ts +15 -0
  22. package/src/cli/Judges.test.ts +373 -0
  23. package/src/cli/Judges.ts +624 -0
  24. package/src/cli/Metrics.ts +3 -16
  25. package/src/cli/Program.test.ts +3 -1
  26. package/src/cli/Program.ts +9 -0
  27. package/src/steering/index.ts +12 -1
  28. package/src/steering/review.test.ts +107 -0
  29. package/src/steering/review.ts +159 -9
  30. package/src/testkit/EmittedScriptRecognizer.ts +0 -14
  31. package/src/testkit/steps.ts +4 -0
  32. package/src/workflow-kit/review.test.ts +170 -26
  33. package/src/workflow-kit/review.ts +95 -19
  34. package/src/workflows/feature/lens-review.feature +16 -4
  35. package/src/workflows/feature/review-answers.feature +179 -0
  36. package/src/workflows/feature/review.test.ts +5 -53
  37. package/src/workflows/feature/review.ts +31 -42
  38. package/src/workflows/feature/steps.test.ts +0 -12
  39. package/src/workflows/feature/steps.ts +3 -10
  40. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +6 -5
  41. package/src/workflows/shared/access.test.ts +2 -11
  42. package/src/workflows/shared/access.ts +0 -2
  43. package/src/workflows/shared/text.test.ts +40 -89
  44. package/src/workflows/shared/text.ts +52 -54
@@ -0,0 +1,179 @@
1
+ @inmem
2
+ Feature: every note in the review is answered with one tick
3
+
4
+ Each note gtd writes to `.gtd/REVIEW.md` ends in three boxes — fix as
5
+ suggested, won't fix, or the human's own answer written over the
6
+ placeholder. The gate refuses to land until every note holds exactly one
7
+ tick. A fix goes to the nit batch, a won't-fix is dropped and recorded in
8
+ the gate's commit, an own answer is judged like any other note. Nothing is
9
+ deferred to `.gtd/TODO.md`.
10
+
11
+ Background:
12
+ Given the shipped workflows, as gtd install scaffolds them
13
+ And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
14
+ And an environment variable "GTD_FLAGMODE" set to "false"
15
+ And a file "src/calc.ts" with:
16
+ """
17
+ export const add = (a: number, b: number) => a + b
18
+ """
19
+ And the working tree is committed as "feat: add calculator"
20
+ And a file "TODO.md" with:
21
+ """
22
+ # Add subtract
23
+
24
+ subtract(a, b) returns the difference.
25
+ """
26
+ And step "unwind" changes nothing
27
+ And step "start-gate.check" changes nothing
28
+ And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
29
+ """
30
+ # Requirements
31
+
32
+ ## Concerns
33
+
34
+ ### Subtract
35
+
36
+ - `subtract(a, b)` returns `a - b`.
37
+
38
+ ## Acceptance Criteria
39
+
40
+ - AC-1: subtract(a, b) returns a - b.
41
+ """
42
+ And step "design.gate.answer" changes nothing
43
+ And step "seam.naive" writes "src/calc.test.ts" with:
44
+ """
45
+ it("[AC-1] subtracts", () => {})
46
+ """
47
+ And step "seam.naive" writes "src/calc.ts" with:
48
+ """
49
+ export const add = (a: number, b: number) => a + b
50
+ export const subtract = (a: number, b: number) => a - b
51
+ """
52
+ And step "seam.check" changes nothing
53
+ And step "implement.applying" runs its script
54
+ And step "implement.health.check" changes nothing
55
+ And step "implement.closing" runs its script
56
+ And judge step "implement.coverage" answers:
57
+ """
58
+ [{"id":"AC-1","answer":"yes","p":0.9}]
59
+ """
60
+ And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
61
+ """
62
+ # Quality
63
+
64
+ ## subtract has no negative test
65
+
66
+ Add a test subtracting a larger number.
67
+
68
+ ## add could be a method
69
+
70
+ A class would read better.
71
+
72
+ ## AC-1 ignores floating point
73
+
74
+ Should subtract round? The criterion does not say.
75
+ """
76
+ And step "build.quality.sorting" runs its script
77
+ And step "build.review.composing" runs its script
78
+ And judge step "build.quality.triage" answers:
79
+ """
80
+ [{"id":"Q1","answer":"noise","p":0.9},{"id":"Q2","answer":"noise","p":0.9},{"id":"Q3","answer":"design","p":0.9}]
81
+ """
82
+ And step "build.review.closing" runs its script
83
+
84
+ Scenario: the note ends in its three answer boxes
85
+ When the process is driven
86
+ Then the process rests at "build.review.await-review"
87
+ And ".gtd/REVIEW.md" contains:
88
+ """
89
+ ## AC-1 ignores floating point
90
+
91
+ Should subtract round? The criterion does not say.
92
+
93
+ Tick exactly one:
94
+
95
+ - [ ] fix as suggested
96
+ - [ ] won't fix
97
+ - [ ] _your answer_
98
+ """
99
+
100
+ Scenario: a sign-off with the note unanswered is refused
101
+ When the process is driven
102
+ Then landing is refused with "answer-completeness: 1 note(s) in .gtd/REVIEW.md not answered"
103
+ And the process rests at "build.review.await-review"
104
+
105
+ Scenario: two ticks on one note are refused
106
+ When the process is driven
107
+ And the text "- [ ] fix as suggested" in ".gtd/REVIEW.md" is replaced with "- [x] fix as suggested"
108
+ And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
109
+ Then landing is refused with "tick exactly one box per note"
110
+
111
+ Scenario: a ticked answer still holding the placeholder is refused
112
+ When the process is driven
113
+ And the text "- [ ] _your answer_" in ".gtd/REVIEW.md" is replaced with "- [x] _your answer_"
114
+ Then landing is refused with "answer-completeness"
115
+
116
+ Scenario: won't fix drops the note, records it, and signs off
117
+ When the process is driven
118
+ And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
119
+ And gtd lands
120
+ And the process is driven
121
+ Then the process rests at "idle"
122
+ And the commit "gtd(human): build.review.await-review → build.review.closing" has a body containing "Gtd-Mark: wontfix=AC-1 ignores floating point"
123
+ And ".gtd/TODO.md" does not exist
124
+ And no commit body contains "build.review.triage"
125
+
126
+ Scenario: fix as suggested goes to the nit batch unjudged, and the note is not asked again
127
+ Given step "build.review.fix.nits.fixing" writes "src/calc.ts" with:
128
+ """
129
+ export const add = (a: number, b: number) => a + b
130
+ export const subtract = (a: number, b: number) => Math.round(a - b)
131
+ """
132
+ And step "build.review.fix.nits.fixing" is prompted with "Should subtract round?"
133
+ And step "build.health.check" changes nothing
134
+ When the process is driven
135
+ And the text "- [ ] fix as suggested" in ".gtd/REVIEW.md" is replaced with "- [x] fix as suggested"
136
+ And gtd lands
137
+ And the process is driven
138
+ Then the process rests at "build.review.await-review"
139
+ And no commit body contains "build.review.triage"
140
+ And ".gtd/REVIEW.md" contains "## Fixed from your notes"
141
+ And ".gtd/REVIEW.md" does not contain "AC-1 ignores floating point"
142
+ And ".gtd/TODO.md" does not exist
143
+ When gtd lands
144
+ And the process is driven
145
+ Then the process rests at "idle"
146
+
147
+ Scenario: an own answer is judged like a note
148
+ Given judge step "build.review.triage" answers:
149
+ """
150
+ [{"id":"answer-1","answer":"nit","p":0.9}]
151
+ """
152
+ And step "build.review.fix.nits.fixing" is prompted with "round half up"
153
+ And step "build.health.check" changes nothing
154
+ And step "build.review.fix.nits.fixing" writes "src/calc.ts" with:
155
+ """
156
+ export const add = (a: number, b: number) => a + b
157
+ export const subtract = (a: number, b: number) => Math.round(a - b)
158
+ """
159
+ When the process is driven
160
+ And the text "- [ ] _your answer_" in ".gtd/REVIEW.md" is replaced with "- [x] round half up"
161
+ And gtd lands
162
+ And the process is driven
163
+ Then the process rests at "build.review.await-review"
164
+ And the commit subjects end with:
165
+ """
166
+ gtd(human): build.review.await-review → build.review.triage
167
+ gtd(judge): build.review.triage → build.review.fix.nits.fixing
168
+ gtd(agent): build.review.fix.nits.fixing → build.health.check
169
+ gtd(check): build.health.check → build.review.closing
170
+ gtd(check): build.review.closing → build.review.composing
171
+ gtd(check): build.review.composing → build.review.await-review
172
+ """
173
+ And ".gtd/REVIEW.md" does not contain "AC-1 ignores floating point"
174
+
175
+ Scenario: deleting a note's boxes instead of answering is refused
176
+ When the process is driven
177
+ And the text "- [ ] fix as suggested" in ".gtd/REVIEW.md" is replaced with ""
178
+ And the text "Tick exactly one:" in ".gtd/REVIEW.md" is replaced with ""
179
+ Then landing is refused with "its answer boxes were deleted"
@@ -675,7 +675,7 @@ describe("severity routing", () => {
675
675
  expect(run.prompts.get("review.collecting")).toContain("(critical: forced by `!`)")
676
676
  })
677
677
 
678
- it("a confident could edit defers and, alone, returns to the review gate", async () => {
678
+ it("a confident could edit is still re-planned, labelled could", async () => {
679
679
  const run = await drive({
680
680
  notes: ["add — quote", "sub", "mul"],
681
681
  answers: { "note-1": verdict("edit"), ...sev("note-1", could) },
@@ -684,9 +684,10 @@ describe("severity routing", () => {
684
684
  "review.composing",
685
685
  "review.await-review",
686
686
  "review.triage",
687
- "review.defer",
688
- "review.await-review",
687
+ "review.closing",
688
+ "review.collecting",
689
689
  ])
690
+ expect(run.prompts.get("review.collecting")).toContain("(could)")
690
691
  })
691
692
 
692
693
  it("severity answers on a nit leave it a nit", async () => {
@@ -730,68 +731,19 @@ describe("severity routing", () => {
730
731
  expect(run.judged).toContain("note-1.security")
731
732
  })
732
733
 
733
- it("the gate after a deferral accepts a clean land and lists the deferred notes", async () => {
734
- const run = await drive({
735
- notes: ["add — quote", "sub", "mul"],
736
- answers: { "note-1": verdict("edit"), ...sev("note-1", could) },
737
- })
738
- expect(run.gates).toEqual([true, true])
739
- expect(run.gateMessages[1]).toContain("NOT acted on")
740
- expect(run.gateMessages[1]).toContain("note-1")
741
- expect(run.gateMessages[0]).not.toContain("NOT acted on")
742
- })
743
-
744
734
  it("the first round's gate accepts a clean land", async () => {
745
735
  const run = await drive({ notes: ["add", "sub", "mul"] })
746
736
  expect(run.gates.length).toBeGreaterThan(0)
747
737
  expect(run.gates.every((g) => g)).toBe(true)
748
738
  })
749
739
 
750
- it("a later gate still accepts a clean land after a deferral round with a nit", async () => {
751
- const run = await drive({
752
- notes: ["add — quote", "sub — typo", "mul"],
753
- answers: {
754
- "note-1": verdict("edit"),
755
- ...sev("note-1", could),
756
- "note-2": verdict("nit"),
757
- },
758
- reviewDocs: [undefined, doc(["add", "sub"])],
759
- })
760
- expect(run.log).toContain("review.defer")
761
- expect(run.log.at(-1)).toBe("review.await-review")
762
- expect(run.gates).toEqual([true, true])
763
- })
764
-
765
- it("a question and a could note in one round: the gate's base is the answering commit", async () => {
766
- const run = await drive({
767
- notes: ["add — why?", "sub — quote", "mul"],
768
- answers: {
769
- "note-1": verdict("question"),
770
- "note-2": verdict("edit"),
771
- ...sev("note-2", could),
772
- },
773
- passSecondGate: true,
774
- })
775
- expect(run.log).toEqual([
776
- "review.composing",
777
- "review.await-review",
778
- "review.triage",
779
- "review.answer-review-questions",
780
- "review.defer",
781
- "review.await-review",
782
- ])
783
- // c4 is the answering commit, not the defer commit (c5) or the round (c2)
784
- expect(run.since.at(-1)).toBe("c4")
785
- })
786
-
787
740
  it("a blank reviewSeverityMinP makes every confident could critical", async () => {
788
741
  const run = await drive({
789
742
  notes: ["add — quote", "sub", "mul"],
790
743
  answers: { "note-1": verdict("edit"), ...sev("note-1", could) },
791
744
  vars: { reviewSeverityMinP: "" },
792
745
  })
793
- expect(run.log).toContain("review.collecting")
794
- expect(run.log).not.toContain("review.defer")
746
+ expect(run.prompts.get("review.collecting")).toContain("(critical: judge unsure)")
795
747
  })
796
748
  })
797
749
 
@@ -29,7 +29,7 @@ import {
29
29
  } from "../shared/steps.js"
30
30
  import { closeReview, isOptionalObject, tail, type TailOptions } from "../shared/tail.js"
31
31
  import * as t from "../shared/text.js"
32
- import { answerReviewQuestions, collecting, deferNotes, fixNits } from "./steps.js"
32
+ import { answerReviewQuestions, collecting, fixNits } from "./steps.js"
33
33
 
34
34
  export type { ReviewOutcome } from "../../workflow-kit/index.js"
35
35
 
@@ -46,6 +46,12 @@ const verdictQuestion = (note: ReviewNote): JudgeQuestion => ({
46
46
 
47
47
  type Tier = "critical" | "should" | "could"
48
48
 
49
+ const TIER_ORDER: Readonly<Record<Tier, number>> = { critical: 0, should: 1, could: 2 }
50
+
51
+ /** Capture order: critical, should, could; within a tier one-way doors, then new concepts. */
52
+ const captureKey = (s: Severity | undefined): number =>
53
+ s === undefined ? 2 : TIER_ORDER[s.tier] * 3 + s.rank
54
+
49
55
  const SEVERITY_ANSWERS: Readonly<Record<string, readonly string[]>> = {
50
56
  security: ["yes", "no"],
51
57
  reversibility: ["one-way", "two-way"],
@@ -60,10 +66,7 @@ interface Severity {
60
66
  readonly rank: 0 | 1 | 2
61
67
  }
62
68
 
63
- /**
64
- * Deferring is the risky direction: anything unanswered, unrecognised, cut or
65
- * below `minP` is `critical`, never `could`.
66
- */
69
+ /** Anything unanswered, unrecognised, cut or below `minP` is `critical`, never `could`. */
67
70
  export const severityOf = (note: ReviewNote, judgment: Judgment, minP: number): Severity => {
68
71
  const ans = (q: string) => judgment.answers[`${note.id}.${q}`]
69
72
  const a = (q: string): string | undefined => ans(q)?.answer
@@ -125,7 +128,7 @@ const severityQuestions = (note: ReviewNote): JudgeQuestion[] => [
125
128
  * nothing) plus four severity answers. Dismissing a note is the risky
126
129
  * direction, so only a confident non-`edit` verdict on uncut evidence counts;
127
130
  * a cut, unanswered or below-floor note is an `edit`. Severity answers on a
128
- * non-`edit` note are ignored. A confident `could` edit becomes `defer`.
131
+ * non-`edit` note are ignored; on an `edit` they order the collecting turn.
129
132
  */
130
133
  const triage =
131
134
  (severities: Map<string, Severity>) =>
@@ -155,9 +158,8 @@ const triage =
155
158
  ? undefined
156
159
  : VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP))
157
160
  if (verdict !== undefined && !truncated.includes(n.id)) return [n.id, verdict]
158
- const severity = severityOf(n, judgment, severityMinP)
159
- severities.set(n.id, severity)
160
- return [n.id, severity.tier === "could" ? "defer" : "edit"]
161
+ severities.set(n.id, severityOf(n, judgment, severityMinP))
162
+ return [n.id, "edit"]
161
163
  }),
162
164
  )
163
165
  }
@@ -167,13 +169,15 @@ export const isCode = (path: string): boolean => path !== ".gtd" && !path.starts
167
169
  /**
168
170
  * gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
169
171
  * turn — and removes what the fixer left in `.gtd/QUALITY.md`. A re-review
170
- * after fixed nits restores the doc as committed at `carry`, answers kept.
172
+ * after fixed nits restores the doc as committed at `carry`, answers kept;
173
+ * a fresh one leaves out the notes the human already answered.
171
174
  */
172
175
  const composing = (
173
176
  base: string,
174
177
  report: LensReport,
175
178
  carry?: string,
176
179
  nitFix?: string,
180
+ answered: ReadonlySet<string> = new Set(),
177
181
  ): Promise<void> =>
178
182
  run(
179
183
  "review.composing",
@@ -184,6 +188,7 @@ const composing = (
184
188
  ...report,
185
189
  weakened: kit.removedAssertions(changesSince(base)),
186
190
  nitFix,
191
+ answered,
187
192
  }),
188
193
  [QUALITY],
189
194
  )
@@ -193,9 +198,10 @@ const composing = (
193
198
 
194
199
  /**
195
200
  * The quality lap's notes reach a human over everything since `base`, who
196
- * signs off or comments, and each note is judged: edits go to another
197
- * planning lap, questions are answered in place, nits fixed in one batch,
198
- * praise dropped.
201
+ * answers each one — fix as suggested, won't fix, or their own answer — and
202
+ * may comment. A fix joins the nit batch, a won't-fix is dropped, and an own
203
+ * answer or comment is judged: edits go to another planning lap, questions
204
+ * are answered in place, nits fixed in one batch, praise dropped.
199
205
  */
200
206
  export const review = (
201
207
  base: string,
@@ -210,42 +216,26 @@ export const review = (
210
216
  "gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing",
211
217
  }
212
218
  const severities = new Map<string, Severity>()
213
- // Armed by a defer-only round to pick the next gate's message; a fresh
214
- // review (`reviewing`) disarms it.
215
- let deferred: readonly t.NoteInput[] = []
216
219
  // Set by a nit fix, so the fresh review that follows names its commit.
217
220
  let nitFix: string | undefined
221
+ // Every note the human answered in this review, left out of a fresh one.
222
+ const answered = new Set<string>()
218
223
  return kit.review(base, {
219
224
  file: REVIEW,
220
225
  feedback: feedback.file,
221
226
  isCode,
222
227
  reviewing: (b, carry) => {
223
- deferred = []
224
228
  const fixedAt = nitFix
225
229
  nitFix = undefined
226
- return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt)
230
+ return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt, answered)
227
231
  },
228
232
  fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
229
233
  keepGreen: () => healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules }),
230
- awaitReview: (b) => {
231
- const notes = deferred
232
- deferred = []
233
- return notes.length > 0
234
- ? awaitReview(b, t.buildReviewAwaitAfterDeferMessage(b, notes))
235
- : awaitReview(b)
236
- },
234
+ awaitReview: (b) => awaitReview(b),
237
235
  reviewMissing,
238
236
  collecting: feedback.collecting,
239
237
  triage: triage(severities),
240
- defer: (notes) => {
241
- deferred = notes.map((n) => ({
242
- id: n.id,
243
- anchor: n.anchor,
244
- text: n.text,
245
- label: severities.get(n.id)!.reason,
246
- }))
247
- return deferNotes(deferred, head())
248
- },
238
+ decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
249
239
  answerQuestions: answerReviewQuestions,
250
240
  fixNits: async (notes) => {
251
241
  await guarded((n: typeof notes) => fixNits(n, fixAccess, rules), guard)(notes)
@@ -259,14 +249,13 @@ export const review = (
259
249
  t.reviewEditNotesCapture(
260
250
  round,
261
251
  edits
262
- .map((n, i) => ({ n, i, s: severities.get(n.id) }))
263
- // critical before should, then one-way doors, then new concepts
264
- .sort(
265
- (x, y) =>
266
- Number(x.s?.tier === "should") - Number(y.s?.tier === "should") ||
267
- (x.s?.rank ?? 2) - (y.s?.rank ?? 2) ||
268
- x.i - y.i,
269
- )
252
+ .map((n, i) => ({
253
+ n,
254
+ i,
255
+ s: severities.get(n.id),
256
+ key: captureKey(severities.get(n.id)),
257
+ }))
258
+ .sort((x, y) => x.key - y.key || x.i - y.i)
270
259
  .map(({ n, s }) => ({
271
260
  id: n.id,
272
261
  anchor: n.anchor,
@@ -111,18 +111,6 @@ describe("the feature workflow's own steps declare skills — a step's rendered
111
111
  expect(scopes).toEqual([{ name: "review.fix.nits", access }])
112
112
  expect(steps_.map((r) => ("name" in r ? r.name : ""))).toEqual(["fixing"])
113
113
  })
114
-
115
- it("deferNotes rests at build.review.defer as a script writing .gtd/TODO.md", async () => {
116
- const request = await capture(
117
- () =>
118
- steps.deferNotes([{ id: "note-1", anchor: "a", text: "x", label: "could" }], "abcdef123"),
119
- "build",
120
- )
121
- if (request.kind !== "run") throw new Error("unreachable")
122
- expect(request.name).toBe("review.defer")
123
- expect(request.body).toContain("## Deferred review notes (abcdef1)")
124
- expect(request.options).toMatchObject({ file: ".gtd/TODO.md" })
125
- })
126
114
  })
127
115
 
128
116
  describe("the feature workflow's planning steps run on the planner model", () => {
@@ -1,4 +1,4 @@
1
- import { human, run, scope, type ScopeAccess } from "../../flows/index.js"
1
+ import { human, scope, type ScopeAccess } from "../../flows/index.js"
2
2
  import type { CollectRound } from "../../workflow-kit/index.js"
3
3
  import * as t from "../shared/text.js"
4
4
  import { riskTierOn } from "./tier.js"
@@ -52,8 +52,8 @@ export const fixNits = (
52
52
  }
53
53
 
54
54
  /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
55
- export const collecting = (capture: string): Promise<void> =>
56
- t.agentWithSkills("review.collecting", t.buildReviewCollectingPrompt(capture), {
55
+ export const collecting = (capture: string, round?: CollectRound): Promise<void> =>
56
+ t.agentWithSkills("review.collecting", t.buildReviewCollectingPrompt(capture, undefined, round), {
57
57
  label: "Collecting your feedback",
58
58
  file: REQUIREMENTS,
59
59
  mode: "qa",
@@ -79,10 +79,3 @@ export const refactorCollecting = (
79
79
  allowEmpty: true,
80
80
  },
81
81
  )
82
-
83
- /** `build.review.defer`: append `notes` to `.gtd/TODO.md` as one section; the step's landing is the commit. */
84
- export const deferNotes = (notes: readonly t.NoteInput[], round: string): Promise<void> =>
85
- run("review.defer", t.deferNotesScript(notes, round), {
86
- label: "Deferring could-notes to TODO.md",
87
- file: ".gtd/TODO.md",
88
- })
@@ -45,11 +45,12 @@ You are judging and classifying a round of review feedback.
45
45
  - This turn writes `.gtd/REQUIREMENTS.md` (the folded concerns) and
46
46
  replies inside `.gtd/REVIEW.md` (thread replies only; the file stays
47
47
  in the tree) — you classify, you do not build
48
- - Fold every concluded thread, ticked answer and hand-edit — and each
49
- `edit` note the capture lists, no other note — into
50
- `.gtd/REQUIREMENTS.md` and delete the folded threads from
51
- `.gtd/REVIEW.md`; append one `- A:` reply to each thread
52
- whose last entry is a `- H:` question
48
+ - Fold every concluded thread and hand-edit — and each `edit` note
49
+ the capture lists, no other note — into `.gtd/REQUIREMENTS.md` and
50
+ delete the folded threads from `.gtd/REVIEW.md`; append one `- A:`
51
+ reply to each thread whose last entry is a `- H:` question
52
+ - Never fold a note's ticked answer box the capture does not list: gtd
53
+ routes those itself
53
54
  - When the capture names an answering commit, carry each answered
54
55
  question into `.gtd/REQUIREMENTS.md`'s `## Answered Questions` as
55
56
  `### <the note>` plus its answer — the lap replaces `.gtd/REVIEW.md`
@@ -11,7 +11,6 @@ describe("the bundled workflow's access export", () => {
11
11
  expect(bundled).toEqual({
12
12
  design: { write: [] },
13
13
  "build.review": { write: [".gtd/REVIEW.md"] },
14
- "build.review.defer": { write: [".gtd/TODO.md"] },
15
14
  "build.review.fix.nits": {},
16
15
  "build.review.fix.risks": {},
17
16
  "build.quality.correctness": { write: [] },
@@ -21,7 +20,6 @@ describe("the bundled workflow's access export", () => {
21
20
  "build.quality.conventions": { write: [] },
22
21
  "build.quality.spec-challenge": { write: [] },
23
22
  "refactor.build.review": { write: [".gtd/REVIEW.md"] },
24
- "refactor.build.review.defer": { write: [".gtd/TODO.md"] },
25
23
  "refactor.build.quality.correctness": { write: [] },
26
24
  "refactor.build.quality.ponytail-review": { write: [] },
27
25
  "refactor.build.quality.conventions": { write: [] },
@@ -32,7 +30,6 @@ describe("the bundled workflow's access export", () => {
32
30
  expect(bundled).not.toHaveProperty(["refactor.build.review.fix.nits"])
33
31
  expect(bundled).not.toHaveProperty(["refactor.build.review.fix.risks"])
34
32
  expect(bundled["refactor.build.review"]).toEqual({ write: [".gtd/REVIEW.md"] })
35
- expect(bundled["refactor.build.review.defer"]).toEqual({ write: [".gtd/TODO.md"] })
36
33
  })
37
34
 
38
35
  it("keys one lens scope per qualityReviews entry", () => {
@@ -54,17 +51,11 @@ describe("the bundled workflow's access export", () => {
54
51
  "refactor.build.quality.correctness",
55
52
  "refactor.build.quality.my-lens",
56
53
  "refactor.build.review",
57
- "refactor.build.review.defer",
58
54
  ])
59
55
  })
60
56
 
61
- it("keys only turn or script scopes", () => {
62
- // `build.review.defer` is a script scope: no turn, so no skills entry.
63
- const known = [
64
- ...Object.keys(workflow.skills(defaults)),
65
- "build.review.defer",
66
- "refactor.build.review.defer",
67
- ]
57
+ it("keys only turn scopes", () => {
58
+ const known = Object.keys(workflow.skills(defaults))
68
59
  for (const key of Object.keys(bundled)) expect(known).toContain(key)
69
60
  })
70
61
  })
@@ -13,12 +13,10 @@ export const access = (
13
13
  ): Readonly<Record<string, ScopeAccess>> => ({
14
14
  design: { write: [] },
15
15
  "build.review": { write: [".gtd/REVIEW.md"] },
16
- "build.review.defer": { write: [".gtd/TODO.md"] },
17
16
  // `{}` reopens what `build.review` restricted: the fixes edit code.
18
17
  "build.review.fix.nits": {},
19
18
  "build.review.fix.risks": {},
20
19
  "refactor.build.review": { write: [".gtd/REVIEW.md"] },
21
- "refactor.build.review.defer": { write: [".gtd/TODO.md"] },
22
20
  ...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), () => ({ write: [] })),
23
21
  ...lensScopes("refactor.build.quality", lensesOf(vars.refactorQualityReviews ?? ""), () => ({
24
22
  write: [],