@pmelab/gtd 48.0.0 → 49.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 (41) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +38 -32
  3. package/dist/chunks/{reserved-DF1HD6jL.mjs → reserved-CYZ1bSBy.mjs} +93 -24
  4. package/dist/chunks/{review-Bv40kZu9.mjs → review-BewyJflS.mjs} +58 -14
  5. package/dist/chunks/{steering-D7hWeMgE.mjs → steering-BXwjmocB.mjs} +7 -1
  6. package/dist/chunks/{tail-D89fU_wf.mjs → tail-C1Oi2YEy.mjs} +1 -1
  7. package/dist/steering.mjs +2 -2
  8. package/dist/testkit.mjs +1 -1
  9. package/dist/workflow-kit.mjs +1 -1
  10. package/dist/workflows/cleanup.mjs +2 -2
  11. package/dist/workflows/feature.mjs +16 -9
  12. package/dist/workflows/fix.mjs +3 -3
  13. package/dist/workflows/review.mjs +3 -3
  14. package/dist/workflows/rollout.mjs +1 -1
  15. package/dist/workflows.mjs +164 -39
  16. package/package.json +1 -1
  17. package/src/steering/index.ts +1 -0
  18. package/src/steering/review.test.ts +29 -0
  19. package/src/steering/review.ts +10 -0
  20. package/src/workflows/feature/coverage.feature +3 -3
  21. package/src/workflows/feature/fast-path.feature +2 -2
  22. package/src/workflows/feature/feature.ts +10 -3
  23. package/src/workflows/feature/flag-mode.feature +2 -2
  24. package/src/workflows/feature/lens-review.feature +116 -13
  25. package/src/workflows/feature/one-step-land.feature +2 -2
  26. package/src/workflows/feature/refactor-absorbs.feature +2 -2
  27. package/src/workflows/feature/refactor-first.feature +2 -2
  28. package/src/workflows/feature/refactor-rebuild.feature +2 -2
  29. package/src/workflows/feature/refactor-review.feature +6 -6
  30. package/src/workflows/feature/review-answers.feature +44 -5
  31. package/src/workflows/feature/review-gate.feature +6 -6
  32. package/src/workflows/feature/reviewer-walkthrough.feature +229 -0
  33. package/src/workflows/feature/seam-rejection.feature +2 -2
  34. package/src/workflows/feature/seam.feature +2 -2
  35. package/src/workflows/feature/slice-findings.feature +56 -5
  36. package/src/workflows/feature/slicing.feature +6 -6
  37. package/src/workflows/feature/standards.feature +25 -1
  38. package/src/workflows/shared/review.ts +108 -13
  39. package/src/workflows/shared/steps.ts +3 -2
  40. package/src/workflows/shared/text.test.ts +126 -0
  41. package/src/workflows/shared/text.ts +125 -19
@@ -0,0 +1,229 @@
1
+ @inmem
2
+ Feature: a reviewer walks the human through the feature's code
3
+
4
+ The feature's final review is written by a reviewer turn
5
+ (`build.review.reviewing`): chunks grouped by concern, each pointing at its
6
+ code, so the phone shows it. Every finding the quality laps left gets a chunk
7
+ of its own, headed exactly as the prompt gives it, holding the pointers to the
8
+ code it cites; gtd then adds the finding's text and the answer boxes
9
+ (`build.review.carrying`). A chunk pointing at no code, or a finding left out,
10
+ is refused.
11
+
12
+ Background:
13
+ Given the shipped workflows, as gtd install scaffolds them
14
+ And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
15
+ And an environment variable "GTD_FLAGMODE" set to "false"
16
+ And a file "src/calc.ts" with:
17
+ """
18
+ export const add = (a: number, b: number) => a + b
19
+ """
20
+ And the working tree is committed as "feat: add calculator"
21
+ And a file "TODO.md" with:
22
+ """
23
+ # Add subtract
24
+
25
+ subtract(a, b) returns the difference.
26
+ """
27
+ And step "unwind" changes nothing
28
+ And step "start-gate.check" changes nothing
29
+ And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
30
+ """
31
+ # Requirements
32
+
33
+ ## Acceptance Criteria
34
+
35
+ - AC-1: subtract(a, b) returns a - b.
36
+ """
37
+ And step "design.gate.answer" changes nothing
38
+ And step "seam.naive" writes "src/calc.test.ts" with:
39
+ """
40
+ it("[AC-1] subtracts", () => {})
41
+ """
42
+ And step "seam.naive" writes "src/calc.ts" with:
43
+ """
44
+ export const add = (a: number, b: number) => a + b
45
+ export const subtract = (a: number, b: number) => a - b
46
+ """
47
+ And step "seam.check" changes nothing
48
+ And step "implement.applying" runs its script
49
+ And step "implement.health.check" changes nothing
50
+ And step "implement.closing" runs its script
51
+ And judge step "implement.coverage" answers:
52
+ """
53
+ [{"id":"AC-1","answer":"yes","p":0.9}]
54
+ """
55
+ And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
56
+ """
57
+ # Quality
58
+
59
+ ## AC-1 ignores floating point
60
+
61
+ Should subtract round? The criterion does not say.
62
+ """
63
+ And judge step "build.quality.triage" answers:
64
+ """
65
+ [{"id":"Q1","answer":"design","p":0.9}]
66
+ """
67
+ And step "build.quality.sorting" runs its script
68
+ And step "build.quality.closing" runs its script
69
+ And step "build.review.carrying" runs its script
70
+
71
+ Scenario: the finding gets its text and answer boxes under the reviewer's pointers
72
+ Given step "build.review.reviewing" is prompted with "- `## AC-1 ignores floating point`:"
73
+ And step "build.review.reviewing" is prompted with "> Should subtract round? The criterion does not say."
74
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
75
+ """
76
+ # Review: abc1234
77
+
78
+ <!-- base: 0000000 -->
79
+
80
+ ## Subtract
81
+
82
+ The new export.
83
+
84
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
85
+
86
+ ## AC-1 ignores floating point
87
+
88
+ - [ ] ./src/calc.ts#2-2 — returns the raw difference
89
+ """
90
+ When the process is driven
91
+ Then the process rests at "build.review.await-review"
92
+ And ".gtd/REVIEW.md" contains:
93
+ """
94
+ ## Subtract
95
+
96
+ The new export.
97
+
98
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
99
+
100
+ ## AC-1 ignores floating point
101
+
102
+ Should subtract round? The criterion does not say.
103
+
104
+ In the code:
105
+
106
+ - [ ] ./src/calc.ts#2-2 — returns the raw difference
107
+
108
+ Tick exactly one:
109
+
110
+ - [ ] fix as suggested
111
+ - [ ] won't fix
112
+ - [ ] _your answer_
113
+ """
114
+ And no commit body contains "build.review.composing"
115
+ And the commit subjects end with:
116
+ """
117
+ gtd(agent): build.review.reviewing → build.review.carrying
118
+ gtd(check): build.review.carrying → build.review.await-review
119
+ """
120
+
121
+ Scenario: a chunk pointing at no code is refused
122
+ Given step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
123
+ """
124
+ # Review: abc1234
125
+
126
+ <!-- base: 0000000 -->
127
+
128
+ ## Subtract
129
+
130
+ The new export, with no pointer.
131
+
132
+ ## AC-1 ignores floating point
133
+
134
+ - [ ] ./src/calc.ts#2-2 — returns the raw difference
135
+ """
136
+ Then driving the process is refused with "Subtract — points at no code"
137
+
138
+ Scenario: a finding the reviewer left out is refused
139
+ Given step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
140
+ """
141
+ # Review: abc1234
142
+
143
+ <!-- base: 0000000 -->
144
+
145
+ ## Subtract
146
+
147
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
148
+ """
149
+ Then driving the process is refused with "AC-1 ignores floating point — no chunk of its own"
150
+
151
+ Scenario: a risk the reviewer marks is fixed before the human sees the review
152
+ Given step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
153
+ """
154
+ # Review: abc1234
155
+
156
+ <!-- base: 0000000 -->
157
+
158
+ ## Subtract
159
+
160
+ - [ ] ./src/calc.ts#2-2 — Risk: overflows on large numbers
161
+
162
+ ## AC-1 ignores floating point
163
+
164
+ - [ ] ./src/calc.ts#2-2 — returns the raw difference
165
+ """
166
+ And step "build.review.fix.risks.fixing" changes nothing
167
+ And step "build.health.check" changes nothing
168
+ When the process is driven
169
+ Then the process rests at "build.review.await-review"
170
+ And the commit subjects end with:
171
+ """
172
+ gtd(agent): build.review.reviewing → build.review.carrying
173
+ gtd(check): build.review.carrying → build.review.fix.risks.fixing
174
+ gtd(agent): build.review.fix.risks.fixing → build.health.check
175
+ gtd(check): build.health.check → build.review.reviewing
176
+ gtd(agent): build.review.reviewing → build.review.carrying
177
+ gtd(check): build.review.carrying → build.review.await-review
178
+ """
179
+
180
+ Scenario: a standard conflict the reviewer writes itself is refused
181
+ Given step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
182
+ """
183
+ # Review: abc1234
184
+
185
+ <!-- base: 0000000 -->
186
+
187
+ ## Standard conflict 0123abcd
188
+
189
+ - [ ] ./src/calc.ts#2-2 — copied over
190
+
191
+ ## AC-1 ignores floating point
192
+
193
+ - [ ] ./src/calc.ts#2-2 — returns the raw difference
194
+ """
195
+ Then driving the process is refused with "Standard conflict 0123abcd — gtd writes conflicts"
196
+
197
+ Scenario: two chunks with one title are refused
198
+ Given step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
199
+ """
200
+ # Review: abc1234
201
+
202
+ <!-- base: 0000000 -->
203
+
204
+ ## AC-1 ignores floating point
205
+
206
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
207
+
208
+ ## AC-1 ignores floating point
209
+
210
+ - [ ] ./src/calc.ts#2-2 — returns the raw difference
211
+ """
212
+ Then driving the process is refused with "AC-1 ignores floating point — a second chunk with this title"
213
+
214
+ Scenario: a risk marked in a finding's chunk is refused, never fixed
215
+ Given step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
216
+ """
217
+ # Review: abc1234
218
+
219
+ <!-- base: 0000000 -->
220
+
221
+ ## Subtract
222
+
223
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
224
+
225
+ ## AC-1 ignores floating point
226
+
227
+ - [ ] ./src/calc.ts#2-2 — Risk: no rounding
228
+ """
229
+ Then driving the process is refused with "no `Risk:` in a quality finding's chunk"
@@ -100,7 +100,7 @@ Feature: rejected naive passes walk one fixed path through the steer gate to the
100
100
  And step "implement.applying" runs its script
101
101
  When gtd lands
102
102
  And the process is driven
103
- Then the process rests at "build.review.composing"
103
+ Then the process rests at "build.review.reviewing"
104
104
  And the commit subjects end with:
105
105
  """
106
106
  gtd(human): idle → unwind
@@ -124,7 +124,7 @@ Feature: rejected naive passes walk one fixed path through the steer gate to the
124
124
  gtd(check): implement.applying → implement.health.check
125
125
  gtd(check): implement.health.check → implement.coverage
126
126
  gtd(judge): implement.coverage → implement.closing
127
- gtd(check): implement.closing → build.review.composing
127
+ gtd(check): implement.closing → build.review.reviewing
128
128
  """
129
129
  And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
130
130
  And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
@@ -152,7 +152,7 @@ Feature: the naive pass and seam judge decide how a feature is built
152
152
  [{"id":"AC-1","answer":"yes","p":0.9}]
153
153
  """
154
154
  When the process is driven
155
- Then the process rests at "build.review.composing"
155
+ Then the process rests at "build.review.reviewing"
156
156
  And "src/calc.ts" contains "subtract"
157
157
  And the commit subjects end with:
158
158
  """
@@ -160,7 +160,7 @@ Feature: the naive pass and seam judge decide how a feature is built
160
160
  gtd(check): implement.applying → implement.health.check
161
161
  gtd(check): implement.health.check → implement.coverage
162
162
  gtd(judge): implement.coverage → implement.closing
163
- gtd(check): implement.closing → build.review.composing
163
+ gtd(check): implement.closing → build.review.reviewing
164
164
  """
165
165
  And ".gtd/REQUIREMENTS.md" does not exist
166
166
 
@@ -70,7 +70,7 @@ Feature: every slice's quality lap is triaged, and its leftovers wait for the re
70
70
  And step "build.quality.sorting" runs its script
71
71
  And step "build.quality.closing" runs its script
72
72
  And step "build.health.check" changes nothing
73
- And step "build.review.composing" runs its script
73
+ And step "build.review.carrying" runs its script
74
74
 
75
75
  Scenario: a finding questioning a criterion reaches the final review, never the fixer
76
76
  Given step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
@@ -85,6 +85,25 @@ Feature: every slice's quality lap is triaged, and its leftovers wait for the re
85
85
  """
86
86
  [{"id":"Q1","answer":"design","p":0.9}]
87
87
  """
88
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
89
+ """
90
+ # Review: abc1234
91
+
92
+ <!-- base: 0000000 -->
93
+
94
+ ## Subtract and multiply
95
+
96
+ - [ ] ./src/sub.test.ts#1-1 — subtract
97
+ - [ ] ./src/mul.test.ts#1-1 — multiply
98
+
99
+ ## AC-1 ignores floating point
100
+
101
+ - [ ] ./src/sub.test.ts#1-1 — where it applies
102
+
103
+ ## AC-1 ignores floating point (2)
104
+
105
+ - [ ] ./src/sub.test.ts#1-1 — where it applies
106
+ """
88
107
  When the process is driven
89
108
  Then the process rests at "build.review.await-review"
90
109
  And no commit body contains "build.fix.quality.fixing"
@@ -117,8 +136,9 @@ Feature: every slice's quality lap is triaged, and its leftovers wait for the re
117
136
  gtd(check): implement.closing → build.quality.correctness.reviewing
118
137
  gtd(agent): build.quality.correctness.reviewing → build.quality.triage
119
138
  gtd(judge): build.quality.triage → build.quality.sorting
120
- gtd(check): build.quality.sorting → build.review.composing
121
- gtd(check): build.review.composing → build.review.await-review
139
+ gtd(check): build.quality.sorting → build.review.reviewing
140
+ gtd(agent): build.review.reviewing → build.review.carrying
141
+ gtd(check): build.review.carrying → build.review.await-review
122
142
  """
123
143
 
124
144
  Scenario: a mechanical finding the fix cap leaves in an early slice is carried, never escalated
@@ -135,6 +155,25 @@ Feature: every slice's quality lap is triaged, and its leftovers wait for the re
135
155
  [{"id":"Q1","answer":"mechanical","p":0.9}]
136
156
  """
137
157
  And step "build.fix.quality.fixing" changes nothing
158
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
159
+ """
160
+ # Review: abc1234
161
+
162
+ <!-- base: 0000000 -->
163
+
164
+ ## Subtract and multiply
165
+
166
+ - [ ] ./src/sub.test.ts#1-1 — subtract
167
+ - [ ] ./src/mul.test.ts#1-1 — multiply
168
+
169
+ ## subtract has no negative test
170
+
171
+ - [ ] ./src/sub.test.ts#1-1 — where it applies
172
+
173
+ ## subtract has no negative test (2)
174
+
175
+ - [ ] ./src/sub.test.ts#1-1 — where it applies
176
+ """
138
177
  When the process is driven
139
178
  Then the process rests at "build.review.await-review"
140
179
  And no commit body contains "build.health.describe"
@@ -168,8 +207,9 @@ Feature: every slice's quality lap is triaged, and its leftovers wait for the re
168
207
  gtd(agent): build.fix.quality.fixing
169
208
  gtd(agent): build.fix.quality.fixing → build.health.check
170
209
  gtd(check): build.health.check → build.quality.closing
171
- gtd(check): build.quality.closing → build.review.composing
172
- gtd(check): build.review.composing → build.review.await-review
210
+ gtd(check): build.quality.closing → build.review.reviewing
211
+ gtd(agent): build.review.reviewing → build.review.carrying
212
+ gtd(check): build.review.carrying → build.review.await-review
173
213
  """
174
214
 
175
215
  Scenario: the final review lists every slice's fixes with their commits
@@ -190,6 +230,17 @@ Feature: every slice's quality lap is triaged, and its leftovers wait for the re
190
230
  it("subtracts a larger number", () => {})
191
231
  """
192
232
  And step "build.fix.quality.fixing" deletes ".gtd/QUALITY.md"
233
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
234
+ """
235
+ # Review: abc1234
236
+
237
+ <!-- base: 0000000 -->
238
+
239
+ ## Subtract and multiply
240
+
241
+ - [ ] ./src/sub.test.ts#1-1 — subtract
242
+ - [ ] ./src/mul.test.ts#1-1 — multiply
243
+ """
193
244
  When the process is driven
194
245
  Then the process rests at "build.review.await-review"
195
246
  And ".gtd/REVIEW.md" contains ", slice 1, lens correctness)"
@@ -51,7 +51,7 @@ Feature: a change with more criteria than the cap is built in coarse slices
51
51
  [{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
52
52
  """
53
53
  When the process is driven
54
- Then the process rests at "build.review.composing"
54
+ Then the process rests at "build.review.reviewing"
55
55
  And the commit subjects end with:
56
56
  """
57
57
  gtd(human): design.gate.answer → seam.naive
@@ -60,7 +60,7 @@ Feature: a change with more criteria than the cap is built in coarse slices
60
60
  gtd(check): implement.applying → implement.health.check
61
61
  gtd(check): implement.health.check → implement.coverage
62
62
  gtd(judge): implement.coverage → implement.closing
63
- gtd(check): implement.closing → build.review.composing
63
+ gtd(check): implement.closing → build.review.reviewing
64
64
  """
65
65
  And the output lines of gtd "slices" start with:
66
66
  """
@@ -112,7 +112,7 @@ Feature: a change with more criteria than the cap is built in coarse slices
112
112
  [{"id":"AC-2","answer":"yes","p":0.9}]
113
113
  """
114
114
  When the process is driven
115
- Then the process rests at "build.review.composing"
115
+ Then the process rests at "build.review.reviewing"
116
116
  And the commit subjects end with:
117
117
  """
118
118
  gtd(agent): design.triage → design.gate.answer
@@ -129,7 +129,7 @@ Feature: a change with more criteria than the cap is built in coarse slices
129
129
  gtd(check): implement.applying → implement.health.check
130
130
  gtd(check): implement.health.check → implement.coverage
131
131
  gtd(judge): implement.coverage → implement.closing
132
- gtd(check): implement.closing → build.review.composing
132
+ gtd(check): implement.closing → build.review.reviewing
133
133
  """
134
134
  And the output lines of gtd "slices" start with:
135
135
  """
@@ -268,7 +268,7 @@ Feature: a change with more criteria than the cap is built in coarse slices
268
268
  """
269
269
  When gtd lands
270
270
  And the process is driven
271
- Then the process rests at "build.review.composing"
271
+ Then the process rests at "build.review.reviewing"
272
272
  And the commit subjects end with:
273
273
  """
274
274
  gtd(human): seam.steer → seam.naive
@@ -277,7 +277,7 @@ Feature: a change with more criteria than the cap is built in coarse slices
277
277
  gtd(check): implement.applying → implement.health.check
278
278
  gtd(check): implement.health.check → implement.coverage
279
279
  gtd(judge): implement.coverage → implement.closing
280
- gtd(check): implement.closing → build.review.composing
280
+ gtd(check): implement.closing → build.review.reviewing
281
281
  """
282
282
  And ".gtd/REQUIREMENTS.md" does not exist
283
283
  And ".gtd/SLICES.md" does not exist
@@ -67,7 +67,31 @@ Feature: the review learns standards from the human's answers
67
67
  Calculators read better as classes.
68
68
  """
69
69
  And step "build.quality.sorting" runs its script
70
- And step "build.review.composing" runs its script
70
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
71
+ """
72
+ # Review: abc1234
73
+
74
+ <!-- base: 0000000 -->
75
+
76
+ ## Subtract
77
+
78
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
79
+ """
80
+ And step "build.review.reviewing", when prompted with "- `## subtract has no class`", writes ".gtd/REVIEW.md" with:
81
+ """
82
+ # Review: abc1234
83
+
84
+ <!-- base: 0000000 -->
85
+
86
+ ## Subtract
87
+
88
+ - [ ] ./src/calc.ts#2-2 — subtract(a, b)
89
+
90
+ ## subtract has no class
91
+
92
+ - [ ] ./src/calc.ts#2-2 — a plain function
93
+ """
94
+ And step "build.review.carrying" runs its script
71
95
  And judge step "build.quality.triage" answers:
72
96
  """
73
97
  [{"id":"Q1","answer":"design","p":0.9}]
@@ -5,6 +5,7 @@ import {
5
5
  judge,
6
6
  numeric,
7
7
  read,
8
+ refuse,
8
9
  requireRevert,
9
10
  restoreScript,
10
11
  run,
@@ -15,7 +16,7 @@ import {
15
16
  type Judgment,
16
17
  type ScopeAccess,
17
18
  } from "../../flows/index.js"
18
- import type { ReviewNote } from "../../steering/index.js"
19
+ import { reviewChunks, reviewRisks, type ReviewNote } from "../../steering/index.js"
19
20
  import * as kit from "../../workflow-kit/index.js"
20
21
  import { guarded, healthy, type EscalationCount } from "./health.js"
21
22
  import { NO_FINDINGS, type LensReport } from "./lenses.js"
@@ -28,10 +29,11 @@ import {
28
29
  fixRisks,
29
30
  REQUIREMENTS,
30
31
  REVIEW,
32
+ reviewing,
31
33
  reviewMissing,
32
34
  } from "./steps.js"
33
35
  import { learnStandards, settlesStandard, standardsAddedSince, standardsPending } from "./learn.js"
34
- import { parseStandards, STANDARDS } from "./standards.js"
36
+ import { isConflictTitle, parseStandards, STANDARDS } from "./standards.js"
35
37
  import { closeReview, isOptionalObject, tail, type TailOptions } from "./tail.js"
36
38
  import * as t from "./text.js"
37
39
 
@@ -170,6 +172,21 @@ const triage =
170
172
 
171
173
  export const isCode = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
172
174
 
175
+ const composedOf = (
176
+ base: string,
177
+ report: LensReport,
178
+ nitFix: string | undefined,
179
+ answered: ReadonlySet<string>,
180
+ ): t.ComposedReview => ({
181
+ ...report,
182
+ nitFix,
183
+ answered,
184
+ standards: {
185
+ added: standardsAddedSince(base),
186
+ undecided: parseStandards(read(STANDARDS) ?? "").undecided,
187
+ },
188
+ })
189
+
173
190
  /**
174
191
  * gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
175
192
  * turn. A re-review
@@ -189,20 +206,81 @@ const composing = (
189
206
  ? writeScript(
190
207
  REVIEW,
191
208
  t.composedReview(base, {
192
- ...report,
209
+ ...composedOf(base, report, nitFix, answered),
193
210
  weakened: kit.removedAssertions(changesSince(base)),
194
- nitFix,
195
- answered,
196
- standards: {
197
- added: standardsAddedSince(base),
198
- undecided: parseStandards(read(STANDARDS) ?? "").undecided,
199
- },
200
211
  }),
201
212
  )
202
213
  : showScript(carry, REVIEW, REVIEW),
203
214
  { label: "Composing the review", file: REVIEW, base },
204
215
  )
205
216
 
217
+ /**
218
+ * Refuses a review with a chunk pointing at no code, or a lap note without a
219
+ * chunk of its own. A range with no code change has nothing to point at, so
220
+ * there only a note's chunk needs a pointer.
221
+ */
222
+ const requirePointers = (
223
+ review: string,
224
+ notes: readonly { title: string }[],
225
+ codeChanged: boolean,
226
+ ): void => {
227
+ const chunks = reviewChunks(review)
228
+ const titles = new Set(chunks.map((c) => c.title))
229
+ const noted = new Set(notes.map((n) => n.title))
230
+ const problems = [
231
+ ...chunks
232
+ .filter((c) => c.pointers === 0 && (codeChanged || noted.has(c.title)))
233
+ .map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — points at no code`),
234
+ ...chunks
235
+ .filter((c) => isConflictTitle(c.title))
236
+ .map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — gtd writes conflicts; leave it out`),
237
+ ...notes.filter((n) => !titles.has(n.title)).map((n) => ` - ${n.title} — no chunk of its own`),
238
+ // Notes and answers are matched to their chunk by title.
239
+ ...chunks
240
+ .filter((c, i) => chunks.findIndex((d) => d.title === c.title) !== i)
241
+ .map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — a second chunk with this title`),
242
+ // A risk is fixed before the human sees it; a note is the human's to decide.
243
+ ...reviewRisks(review)
244
+ .filter((r) => [...noted].some((title) => r.anchor.startsWith(`${title} ./`)))
245
+ .map((r) => ` - ${r.anchor} — no \`Risk:\` in a quality finding's chunk`),
246
+ ]
247
+ if (problems.length === 0) return
248
+ refuse(
249
+ `gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code, and every quality finding its own chunk, headed exactly as the prompt gave it:\n${problems.join("\n")}`,
250
+ )
251
+ }
252
+
253
+ /**
254
+ * A reviewer walks the human through everything since `base`, every chunk
255
+ * pointing at its code and each of the lap's notes a chunk of its own; gtd
256
+ * then adds each note's text and answer boxes, and the summary chunks.
257
+ */
258
+ const walkthrough = async (
259
+ base: string,
260
+ report: t.ComposedReview,
261
+ lap: t.ReviewRange,
262
+ carry: string | undefined,
263
+ ): Promise<void> => {
264
+ const notes = t.titledNotes(report)
265
+ await reviewing(base, carry, lap, t.walkthroughBlock(notes, report.nitFix))
266
+ const review = read(REVIEW)
267
+ // A review the reviewer did not write is the round's own refusal or rest.
268
+ if (review === undefined) return
269
+ requirePointers(
270
+ review,
271
+ notes,
272
+ changesSince(base).some((c) => isCode(c.path)),
273
+ )
274
+ const full = t.withWalkthrough(review, report)
275
+ if (full !== review) {
276
+ await run("review.carrying", writeScript(REVIEW, full), {
277
+ label: "Adding the quality lap's findings",
278
+ file: REVIEW,
279
+ base,
280
+ })
281
+ }
282
+ }
283
+
206
284
  /**
207
285
  * The quality lap's notes reach a human over everything since `base`, who
208
286
  * answers each one — fix as suggested, won't fix, or their own answer — and
@@ -234,12 +312,27 @@ export const humanReview = (
234
312
  isCode,
235
313
  reviewing: (b, carry) => {
236
314
  const fixedAt = nitFix
237
- nitFix = undefined
238
- return composing(b, options.report ?? NO_FINDINGS, carry, fixedAt, answered)
315
+ const report = options.report ?? NO_FINDINGS
316
+ return options.walkthrough === true
317
+ ? guarded(
318
+ () =>
319
+ walkthrough(
320
+ b,
321
+ composedOf(b, report, fixedAt, answered),
322
+ options.lap ?? "first",
323
+ carry,
324
+ ),
325
+ guard,
326
+ )()
327
+ : composing(b, report, carry, fixedAt, answered)
239
328
  },
240
329
  fixRisks: guarded((notes) => fixRisks(notes, fixAccess, rules), guard),
241
330
  keepGreen: () => healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules }),
242
- awaitReview,
331
+ // Kept until the human sees a review, so a re-review after a risk fix still names it.
332
+ awaitReview: (b) => {
333
+ nitFix = undefined
334
+ return awaitReview(b)
335
+ },
243
336
  reviewMissing,
244
337
  collecting: feedback.collecting,
245
338
  triage: triage(severities),
@@ -297,8 +390,10 @@ export interface BuildTailOptions extends TailOptions {
297
390
  readonly feedback?: FeedbackTarget | undefined
298
391
  /** Write access for the nit and risk fix turns; absent: the bundled `access` export decides. */
299
392
  readonly fixAccess?: ScopeAccess | undefined
300
- /** @deprecated No reviewer turn reads it since the review is composed. */
393
+ /** Which round a `walkthrough` reviewer writes; absent: `first`. */
301
394
  readonly lap?: t.ReviewRange | undefined
395
+ /** A reviewer turn writes the review as a walkthrough of the code, the lap's notes among its chunks; absent: gtd composes it from the notes alone. */
396
+ readonly walkthrough?: boolean | undefined
302
397
  /** What the triaged quality lap left for the review; absent: nothing. */
303
398
  readonly report?: LensReport | undefined
304
399
  }
@@ -94,15 +94,16 @@ export const fixQuality = (rules?: string): Promise<void> =>
94
94
  allowEmpty: true,
95
95
  })
96
96
 
97
- /** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given. */
97
+ /** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given; `extra` joins the prompt. */
98
98
  export const reviewing = (
99
99
  base: string,
100
100
  carry?: string,
101
101
  lap: t.ReviewRange = "first",
102
+ extra = "",
102
103
  ): Promise<void> =>
103
104
  t.agentWithSkills(
104
105
  "review.reviewing",
105
- t.buildReviewReviewingPrompt(base, lap, carry, removedAssertions(changesSince(base))),
106
+ t.buildReviewReviewingPrompt(base, lap, carry, removedAssertions(changesSince(base)), extra),
106
107
  {
107
108
  label: "Reviewing",
108
109
  file: REVIEW,