@pmelab/gtd 43.0.0 → 44.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 (34) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +17 -13
  3. package/dist/chunks/{reserved-COEMFEpy.mjs → reserved-DJk_6-ju.mjs} +41 -1
  4. package/dist/chunks/review-X7Jfz0dY.mjs +249 -0
  5. package/dist/chunks/{tail-C_xn8U7U.mjs → tail-5w5mxGTq.mjs} +3 -3
  6. package/dist/workflows/cleanup.mjs +2 -2
  7. package/dist/workflows/feature.mjs +6 -278
  8. package/dist/workflows/fix.mjs +2 -2
  9. package/dist/workflows/review.mjs +27 -7
  10. package/dist/workflows/rollout.mjs +1 -1
  11. package/dist/workflows.mjs +74 -44
  12. package/package.json +1 -1
  13. package/src/steering/FORMATS.md +4 -4
  14. package/src/workflows/feature/coverage.ts +1 -1
  15. package/src/workflows/feature/feature.test.ts +8 -0
  16. package/src/workflows/feature/feature.ts +2 -2
  17. package/src/workflows/feature/index.ts +1 -1
  18. package/src/workflows/feature/refactor.ts +1 -1
  19. package/src/workflows/feature/seam.ts +1 -1
  20. package/src/workflows/feature/steps.test.ts +2 -64
  21. package/src/workflows/feature/steps.ts +5 -41
  22. package/src/workflows/fix/fix.feature +20 -21
  23. package/src/workflows/index.ts +1 -1
  24. package/src/workflows/review/review-door.feature +149 -0
  25. package/src/workflows/review/review.test.ts +1 -73
  26. package/src/workflows/review/review.ts +34 -6
  27. package/src/workflows/{feature → shared}/review.test.ts +4 -8
  28. package/src/workflows/{feature → shared}/review.ts +13 -10
  29. package/src/workflows/shared/skills.test.ts +2 -0
  30. package/src/workflows/shared/skills.ts +2 -0
  31. package/src/workflows/shared/steps.test.ts +64 -2
  32. package/src/workflows/shared/steps.ts +40 -1
  33. package/src/workflows/shared/tail.ts +2 -2
  34. package/src/workflows/shared/text.ts +11 -0
@@ -26,7 +26,7 @@ import {
26
26
  type StatedCriteria,
27
27
  } from "./criteria.js"
28
28
  import { design } from "./planning.js"
29
- import { buildTail, isCode, reUnwind, review, type ReviewOutcome } from "./review.js"
29
+ import { buildTail, humanReview, isCode, reUnwind, type ReviewOutcome } from "../shared/review.js"
30
30
  import { implement, implementing, seamBuild } from "./seam.js"
31
31
  import { sliceCap, sliceCriteria, slicing } from "./slicing.js"
32
32
  import { recordRisk, riskTierOn, type RiskTier } from "./tier.js"
@@ -153,7 +153,7 @@ const settleQuality = async (): Promise<void> => {
153
153
 
154
154
  /** The triaged quality lap, then the human review since `base`. */
155
155
  const reviewedTail = (base: string, lensBase: string): Promise<ReviewOutcome> =>
156
- tail((escalations, report) => review(base, escalations, { report }), {
156
+ tail((escalations, report) => humanReview(base, escalations, { report }), {
157
157
  green: true,
158
158
  lensBase,
159
159
  triage: true,
@@ -11,7 +11,7 @@ export {
11
11
  type BuildTailOptions,
12
12
  type FeedbackTarget,
13
13
  type ReviewOutcome,
14
- } from "./review.js"
14
+ } from "../shared/review.js"
15
15
  export type { ReviewRange } from "../shared/text.js"
16
16
  export * from "./feature.js"
17
17
  export * from "../shared/reserved.js"
@@ -20,7 +20,7 @@ import { healthy, type Guard } from "../shared/health.js"
20
20
  import { coder, FEEDBACK, HANDOFF, REFACTOR } from "../shared/steps.js"
21
21
  import * as t from "../shared/text.js"
22
22
  import { defaults, testFileGlobs } from "../shared/vars.js"
23
- import { buildTail, reUnwind, type FeedbackTarget } from "./review.js"
23
+ import { buildTail, reUnwind, type FeedbackTarget } from "../shared/review.js"
24
24
  import { refactorCollecting } from "./steps.js"
25
25
 
26
26
  export const REFACTOR_PACKAGE = REFACTOR
@@ -45,7 +45,7 @@ import {
45
45
  refactorStrays,
46
46
  reviewedRefactor,
47
47
  } from "./refactor.js"
48
- import { isCode } from "./review.js"
48
+ import { isCode } from "../shared/review.js"
49
49
  import { flagModeOn } from "../shared/options.js"
50
50
  import type { RiskTier } from "./tier.js"
51
51
 
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { describe, expect, it } from "vitest"
3
- import { installContext, type ScopeOptions, type StepRequest } from "../../flows/index.js"
4
- import { captureStep, fixtureContext } from "../shared/text.fixture.js"
3
+ import type { StepRequest } from "../../flows/index.js"
4
+ import { captureStep } from "../shared/text.fixture.js"
5
5
  import * as steps from "./steps.js"
6
6
 
7
7
  const capture = (
@@ -49,68 +49,6 @@ describe("the feature workflow's own steps declare skills — a step's rendered
49
49
  expect(closed).toContain("already closed")
50
50
  expect(closed).not.toContain("exactly one `- A:` reply")
51
51
  })
52
-
53
- it("answerReviewQuestions answers inline in .gtd/REVIEW.md as the reviewer, with its bundled skills", async () => {
54
- const request = await capture(
55
- () =>
56
- steps.answerReviewQuestions([{ id: "note-1", anchor: "calc ./a.ts#1-1", text: "why?" }]),
57
- "build",
58
- )
59
- if (request.kind !== "agent") throw new Error("unreachable")
60
- expect(request.name).toBe("review.answer-review-questions")
61
- expect(request.options).toMatchObject({
62
- file: ".gtd/REVIEW.md",
63
- mode: "review",
64
- })
65
- expect(request.prompt).toContain("code-review-and-quality")
66
- expect(request.prompt).toContain("note-1 — calc ./a.ts#1-1")
67
- expect(request.prompt).toContain("`A: ` line")
68
- expect(request.prompt).toContain("gtd check review .gtd/REVIEW.md")
69
- })
70
-
71
- it("fixNits fixes every nit in one turn and leaves .gtd/REVIEW.md alone", async () => {
72
- const request = await capture(
73
- () =>
74
- steps.fixNits([
75
- { id: "note-1", anchor: "calc ./a.ts#1-1", text: "typo" },
76
- { id: "note-2", anchor: "calc ./a.ts#2-2", text: "semicolon" },
77
- ]),
78
- "build",
79
- )
80
- if (request.kind !== "agent") throw new Error("unreachable")
81
- expect(request.name).toBe("review.fix.nits.fixing")
82
- expect(request.prompt).toContain("incremental-implementation, code-simplification")
83
- expect(request.prompt).toContain("typo")
84
- expect(request.prompt).toContain("semicolon")
85
- expect(request.prompt).toContain("Leave `.gtd/REVIEW.md` untouched")
86
- })
87
-
88
- it("fixNits with access runs in a review.fix.nits scope, the step named fixing inside it", async () => {
89
- const scopes: ScopeOptions[] = []
90
- const steps_: StepRequest[] = []
91
- const access = { write: ["src/a.ts", ".gtd/**"] }
92
- installContext(
93
- fixtureContext(
94
- { scope: "refactor.build" },
95
- {
96
- step: (request) => {
97
- steps_.push(request)
98
- return Promise.resolve()
99
- },
100
- pushScope: (s) => void scopes.push(s),
101
- popScope: () => undefined,
102
- changesSince: () => [],
103
- },
104
- ),
105
- )
106
- try {
107
- await steps.fixNits([], access)
108
- } finally {
109
- installContext(undefined)
110
- }
111
- expect(scopes).toEqual([{ name: "review.fix.nits", access }])
112
- expect(steps_.map((r) => ("name" in r ? r.name : ""))).toEqual(["fixing"])
113
- })
114
52
  })
115
53
 
116
54
  describe("the feature workflow's planning steps run on the planner model", () => {
@@ -1,8 +1,11 @@
1
- import { human, scope, type ScopeAccess } from "../../flows/index.js"
1
+ import { human } 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"
5
- import { planner, REFACTOR, REQUIREMENTS, REVIEW } from "../shared/steps.js"
5
+ import { planner, REFACTOR, REQUIREMENTS } from "../shared/steps.js"
6
+
7
+ // Kept on the published feature entry.
8
+ export { answerReviewQuestions, collecting, fixNits } from "../shared/steps.js"
6
9
 
7
10
  /** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
8
11
  export const triage = (base: string): Promise<void> =>
@@ -23,45 +26,6 @@ export const answerProductQuestions = (): Promise<void> =>
23
26
  acceptClean: true,
24
27
  })
25
28
 
26
- /** Answer every `question` note inline in `.gtd/REVIEW.md`. */
27
- export const answerReviewQuestions = (notes: readonly t.NoteInput[]): Promise<void> =>
28
- t.agentWithSkills("review.answer-review-questions", t.buildReviewAnswerQuestionsPrompt(notes), {
29
- label: "Answering your questions",
30
- file: REVIEW,
31
- mode: "review",
32
- model: planner(),
33
- system: t.reviewerSystem(),
34
- })
35
-
36
- /** Fix every `nit` note in one turn. */
37
- export const fixNits = (
38
- notes: readonly t.NoteInput[],
39
- access?: ScopeAccess,
40
- rules?: string,
41
- ): Promise<void> => {
42
- const fixing = (name: string): Promise<void> =>
43
- t.agentWithSkills(name, t.buildReviewFixNitsPrompt(notes, rules), {
44
- label: "Fixing your nits",
45
- file: REVIEW,
46
- model: planner(),
47
- system: t.reviewerSystem(),
48
- })
49
- return access === undefined
50
- ? fixing("review.fix.nits.fixing")
51
- : scope({ name: "review.fix.nits", access }, () => fixing("fixing"))
52
- }
53
-
54
- /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
55
- export const collecting = (capture: string, round?: CollectRound): Promise<void> =>
56
- t.agentWithSkills("review.collecting", t.buildReviewCollectingPrompt(capture, undefined, round), {
57
- label: "Collecting your feedback",
58
- file: REQUIREMENTS,
59
- mode: "qa",
60
- model: planner(),
61
- system: t.reviewerSystem(),
62
- allowEmpty: true,
63
- })
64
-
65
29
  /** Turn the refactor review round `capture` describes into `.gtd/REFACTOR.md`, or change nothing. */
66
30
  export const refactorCollecting = (
67
31
  capture: string,
@@ -1,8 +1,7 @@
1
- Feature: fix fixes the notes review hands it
1
+ Feature: fix fixes the notes a hand-off leaves it
2
2
 
3
- `review` and `fix` meet only in `.gtd/REVIEW.md`: a review the human leaves
4
- a note in ends with the reviewer's copy kept beside it, and `gtd door fix`
5
- picks both up.
3
+ A hand-off is `.gtd/REVIEW.md` with the human's notes beside the reviewer's
4
+ own `.gtd/REVIEW.orig.md`; `gtd door fix` picks both up.
6
5
 
7
6
  Background:
8
7
  Given the shipped workflows, as gtd install scaffolds them
@@ -13,10 +12,8 @@ Feature: fix fixes the notes review hands it
13
12
  """
14
13
  And the working tree is committed as "feat: add calculator"
15
14
 
16
- Scenario: a note left in review is fixed, and both review files are removed
17
- Given step "review-gate.check" changes nothing
18
- And step "build.health.check" changes nothing
19
- And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
15
+ Scenario: a handed-off note is fixed, and both review files are removed
16
+ Given a file ".gtd/REVIEW.orig.md" with:
20
17
  """
21
18
  # Review: abc1234
22
19
 
@@ -27,11 +24,7 @@ Feature: fix fixes the notes review hands it
27
24
  - [ ] ./src/calc.ts#1-1
28
25
  new add function
29
26
  """
30
- When gtd opens door "review" with "HEAD~1"
31
- And the process is driven
32
- Then the process rests at "build.review.await-review"
33
-
34
- Given a file ".gtd/REVIEW.md" with:
27
+ And a file ".gtd/REVIEW.md" with:
35
28
  """
36
29
  # Review: abc1234
37
30
 
@@ -42,21 +35,27 @@ Feature: fix fixes the notes review hands it
42
35
  - [ ] ./src/calc.ts#1-1
43
36
  new add function — rename it to sum
44
37
  """
45
- And step "build.review.handing-off" runs its script
46
- When gtd lands
47
- And the process is driven
48
- Then the process rests at "idle"
49
- And ".gtd/REVIEW.orig.md" contains "new add function"
38
+ And the working tree is committed as "chore: hand off a review"
39
+ And step "build.health.check" changes nothing
40
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
41
+ """
42
+ # Review: abc1234
43
+
44
+ <!-- base: 0000000 -->
45
+
46
+ ## calc
50
47
 
51
- Given step "notes.fixing" writes "src/calc.ts" with:
48
+ - [ ] ./src/calc.ts#1-1
49
+ new add function
50
+ """
51
+ And step "notes.fixing" writes "src/calc.ts" with:
52
52
  """
53
53
  export const sum = (a: number, b: number) => a + b
54
54
  """
55
55
  And step "notes.closing" runs its script
56
56
  When gtd opens door "fix"
57
57
  Then the resting step's content contains "rename it to sum"
58
- # The fix goes to its own review, which the same scripted reviewer writes
59
- # afresh — the handed-off review itself is gone, as the reviewer's copy shows.
58
+ # The fix goes to its own review, which the scripted reviewer writes afresh.
60
59
  When the process is driven
61
60
  Then the process rests at "build.review.await-review"
62
61
  And the commit subjects end with:
@@ -32,7 +32,7 @@ export {
32
32
  type BuildTailOptions,
33
33
  type FeedbackTarget,
34
34
  type ReviewOutcome,
35
- } from "./feature/review.js"
35
+ } from "./shared/review.js"
36
36
  export * from "./feature/feature.js"
37
37
  export { base, review, reviewFlow } from "./review/review.js"
38
38
  export { fix, fixFlow, fixReviewNotes } from "./fix/fix.js"
@@ -0,0 +1,149 @@
1
+ @inmem
2
+ Feature: the review door fixes its notes in the same process
3
+
4
+ `gtd door review` runs the feature's review over everything since its base:
5
+ the triaged quality lap writes `.gtd/REVIEW.md`, the human answers each note,
6
+ and what they ask for is fixed in place under the health loop before the
7
+ review comes back. A sign-off ends the process; nothing is left for `fix`.
8
+
9
+ Background:
10
+ Given the shipped workflows, as gtd install scaffolds them
11
+ And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
12
+ And a file "src/calc.ts" with:
13
+ """
14
+ export const add = (a: number, b: number) => a + b
15
+ """
16
+ And the working tree is committed as "feat: add calculator"
17
+ And step "review-gate.check" changes nothing
18
+ And step "build.health.check" changes nothing
19
+ And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
20
+ """
21
+ # Quality
22
+
23
+ ## add has no test
24
+
25
+ Add a test for add.
26
+ """
27
+ And step "build.quality.sorting" runs its script
28
+ And judge step "build.quality.triage" answers:
29
+ """
30
+ [{"id":"Q1","answer":"design","p":0.9}]
31
+ """
32
+ And step "build.review.composing" runs its script
33
+ And step "build.review.closing" runs its script
34
+
35
+ Scenario: the review is composed from the lap's notes, with answer boxes
36
+ When gtd opens door "review" with "HEAD~1"
37
+ And the process is driven
38
+ Then the process rests at "build.review.await-review"
39
+ And the commit subjects end with:
40
+ """
41
+ gtd(human): review-gate.check
42
+ gtd(check): review-gate.check → build.quality.correctness.reviewing
43
+ gtd(agent): build.quality.correctness.reviewing → build.quality.triage
44
+ gtd(judge): build.quality.triage → build.quality.sorting
45
+ gtd(check): build.quality.sorting → build.review.composing
46
+ gtd(check): build.review.composing → build.review.await-review
47
+ """
48
+ And ".gtd/REVIEW.md" contains:
49
+ """
50
+ ## add has no test
51
+
52
+ Add a test for add.
53
+
54
+ Tick exactly one:
55
+
56
+ - [ ] fix as suggested
57
+ - [ ] won't fix
58
+ - [ ] _your answer_
59
+ """
60
+
61
+ Scenario: a won't-fix signs off, and no review is left for fix
62
+ When gtd opens door "review" with "HEAD~1"
63
+ And the process is driven
64
+ And the text "- [ ] won't fix" in ".gtd/REVIEW.md" is replaced with "- [x] won't fix"
65
+ And gtd lands
66
+ And the process is driven
67
+ Then the process rests at "idle"
68
+ And ".gtd/REVIEW.md" does not exist
69
+ And ".gtd/REVIEW.orig.md" does not exist
70
+
71
+ Scenario: a note ticked fix as suggested is fixed in place, then reviewed again
72
+ Given step "build.review.fix.nits.fixing" writes "src/calc.test.ts" with:
73
+ """
74
+ it("adds", () => {})
75
+ """
76
+ When gtd opens door "review" with "HEAD~1"
77
+ And the process is driven
78
+ And the text "- [ ] fix as suggested" in ".gtd/REVIEW.md" is replaced with "- [x] fix as suggested"
79
+ And gtd lands
80
+ And the process is driven
81
+ Then the process rests at "build.review.await-review"
82
+ And the commit subjects end with:
83
+ """
84
+ gtd(human): build.review.await-review → build.review.fix.nits.fixing
85
+ gtd(agent): build.review.fix.nits.fixing → build.health.check
86
+ gtd(check): build.health.check → build.review.closing
87
+ gtd(check): build.review.closing → build.review.composing
88
+ gtd(check): build.review.composing → build.review.await-review
89
+ """
90
+ And ".gtd/REVIEW.md" contains "## Fixed from your notes"
91
+ And "src/calc.test.ts" contains "adds"
92
+ When gtd lands
93
+ And the process is driven
94
+ Then the process rests at "idle"
95
+ And ".gtd/REVIEW.md" does not exist
96
+ And ".gtd/REVIEW.orig.md" does not exist
97
+
98
+ Scenario: an own answer asking for a change is fixed in place, then reviewed again
99
+ Given judge step "build.review.triage" answers:
100
+ """
101
+ [{"id":"answer-1","answer":"edit","p":0.9}]
102
+ """
103
+ And step "build.review.collecting" writes ".gtd/REQUIREMENTS.md" with:
104
+ """
105
+ # Requirements
106
+
107
+ ## Test add with negatives
108
+
109
+ add(-1, -2) returns -3.
110
+ """
111
+ And step "feedback.fixing" is prompted with ".gtd/REQUIREMENTS.md"
112
+ And step "feedback.fixing" writes "src/calc.test.ts" with:
113
+ """
114
+ it("adds negatives", () => {})
115
+ """
116
+ And step "re-unwind" runs its script
117
+ And step "feedback.closing" runs its script
118
+ When gtd opens door "review" with "HEAD~1"
119
+ And the process is driven
120
+ And the text "- [ ] _your answer_" in ".gtd/REVIEW.md" is replaced with "- [x] test negatives too"
121
+ And gtd lands
122
+ And the process is driven
123
+ Then the process rests at "build.review.await-review"
124
+ And the commit subjects end with:
125
+ """
126
+ gtd(human): build.review.await-review → build.review.triage
127
+ gtd(judge): build.review.triage → build.review.closing
128
+ gtd(check): build.review.closing → build.review.collecting
129
+ gtd(agent): build.review.collecting → re-unwind
130
+ gtd(check): re-unwind → feedback.fixing
131
+ gtd(agent): feedback.fixing → feedback.closing
132
+ gtd(check): feedback.closing → build.health.check
133
+ gtd(check): build.health.check → build.quality.correctness.reviewing
134
+ gtd(agent): build.quality.correctness.reviewing → build.quality.triage
135
+ gtd(judge): build.quality.triage → build.quality.sorting
136
+ gtd(check): build.quality.sorting → build.review.composing
137
+ gtd(check): build.review.composing → build.review.await-review
138
+ """
139
+ And ".gtd/REQUIREMENTS.md" does not exist
140
+ And "src/calc.test.ts" contains "adds negatives"
141
+ And ".gtd/REVIEW.md" does not contain "test negatives too"
142
+
143
+ Scenario: a leftover requirements file is refused before any step
144
+ Given a file ".gtd/REQUIREMENTS.md" with:
145
+ """
146
+ # Requirements
147
+ """
148
+ And the working tree is committed as "chore: leftover requirements"
149
+ Then opening door "review" is refused with ".gtd/REQUIREMENTS.md is left over"
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import { runsItsScript, testRepo, type TestRepo } from "../../testkit/index.js"
2
+ import { testRepo } from "../../testkit/index.js"
3
3
  import { CONFIG_SCAFFOLD } from "../../install/index.js"
4
4
  import { reviewDoc } from "../shared/config.fixture.js"
5
5
  import { base } from "./review.js"
@@ -7,79 +7,7 @@ import { base } from "./review.js"
7
7
  const REVIEW = ".gtd/REVIEW.md"
8
8
  const ORIG = ".gtd/REVIEW.orig.md"
9
9
 
10
- /** A repository with one commit to review, the review run until the human holds `.gtd/REVIEW.md`. */
11
- const reviewed = async (): Promise<TestRepo> => {
12
- const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_QUALITYREVIEWS: "" } })
13
- repo.write("src/calc.ts", "export const add = (a: number, b: number) => a + b\n")
14
- repo.commit("feat: add calculator")
15
- await repo.door("review", "HEAD~1")
16
- const rest = await repo.drive({
17
- "review-gate.check": () => undefined,
18
- "build.health.check": () => undefined,
19
- "build.review.reviewing": (beat, r) => {
20
- expect(beat.file).toBe(REVIEW)
21
- r.write(REVIEW, reviewDoc("new add function"))
22
- },
23
- })
24
- expect(rest).toMatchObject({ state: "build.review.await-review", actor: "human" })
25
- return repo
26
- }
27
-
28
10
  describe("the review workflow", () => {
29
- it("checks, reviews and hands the review to the human", async () => {
30
- const repo = await reviewed()
31
- expect(repo.subjects().slice(2)).toEqual([
32
- "gtd(human): review-gate.check",
33
- "gtd(check): review-gate.check → build.health.check",
34
- "gtd(check): build.health.check → build.review.reviewing",
35
- "gtd(agent): build.review.reviewing → build.review.await-review",
36
- ])
37
- })
38
-
39
- it("closes a review the human signs off", async () => {
40
- const repo = await reviewed()
41
- repo.write(REVIEW, reviewDoc("new add function").replace("- [ ]", "- [x]"))
42
- await repo.land()
43
- const rest = await repo.drive({ "build.review.closing": runsItsScript })
44
- expect(rest.state).toBe("idle")
45
- expect(repo.subjects().slice(-2)).toEqual([
46
- "gtd(human): build.review.await-review → build.review.closing",
47
- "gtd(check): build.review.closing → idle",
48
- ])
49
- expect(repo.read(REVIEW)).toBeUndefined()
50
- })
51
-
52
- it("never lands a review the review format rejects", async () => {
53
- const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_QUALITYREVIEWS: "" } })
54
- repo.write("src/calc.ts", "export const add = (a: number, b: number) => a + b\n")
55
- repo.commit("feat: add calculator")
56
- await repo.door("review", "HEAD~1")
57
- await expect(
58
- repo.drive({
59
- "review-gate.check": () => undefined,
60
- "build.health.check": () => undefined,
61
- "build.review.reviewing": (_, r) => r.write(REVIEW, "not a review\n"),
62
- }),
63
- ).rejects.toThrow(/validating \.gtd\/REVIEW\.md/)
64
- expect(repo.subjects().at(-1)).toBe("gtd(check): build.health.check → build.review.reviewing")
65
- })
66
-
67
- it("ends with the review and the reviewer's copy kept when the human leaves a note", async () => {
68
- const repo = await reviewed()
69
- repo.write(REVIEW, reviewDoc("new add function — rename it to sum"))
70
- await repo.land()
71
- const rest = await repo.drive({
72
- "build.review.handing-off": runsItsScript,
73
- })
74
- expect(rest.state).toBe("idle")
75
- expect(repo.subjects().slice(-2)).toEqual([
76
- "gtd(human): build.review.await-review → build.review.handing-off",
77
- "gtd(check): build.review.handing-off → idle",
78
- ])
79
- expect(repo.read(REVIEW)).toContain("rename it to sum")
80
- expect(repo.read(ORIG)).toBe(reviewDoc("new add function"))
81
- })
82
-
83
11
  it("refuses a pending hand-off before requesting any step, the gate's check included", async () => {
84
12
  const repo = testRepo({
85
13
  workflow: CONFIG_SCAFFOLD,
@@ -1,18 +1,46 @@
1
- import { start, type WorkflowBase } from "../../flows/index.js"
1
+ import { read, refuse, removeScript, run, start, type WorkflowBase } from "../../flows/index.js"
2
+ import { buildTail, reUnwind } from "../shared/review.js"
2
3
  import { gate } from "../shared/health.js"
3
4
  import { shipped } from "../shared/options.js"
4
- import { handOffTail, refusePendingHandOff } from "../shared/tail.js"
5
+ import { coder, REQUIREMENTS } from "../shared/steps.js"
6
+ import { refusePendingHandOff } from "../shared/tail.js"
5
7
  import * as t from "../shared/text.js"
6
8
 
9
+ /** Build the concerns a review round collected into `.gtd/REQUIREMENTS.md`, then remove it. */
10
+ const fixFeedback = async (): Promise<void> => {
11
+ await t.agentWithSkills("feedback.fixing", t.reviewFeedbackFixPrompt(), {
12
+ label: "Fixing your review feedback",
13
+ file: REQUIREMENTS,
14
+ model: coder(),
15
+ system: t.finisherSystem(),
16
+ // An earlier nit fix may already have built the concern.
17
+ allowEmpty: true,
18
+ })
19
+ await run("feedback.closing", removeScript([REQUIREMENTS]), { label: "Closing the feedback" })
20
+ }
21
+
7
22
  /**
8
- * Pure review of everything since `reviewBase`: a green gate, the quality
9
- * lap, then the human review. A note ends the process with `.gtd/REVIEW.md`
10
- * left for `fix`; it never re-plans.
23
+ * The feature's review of everything since `reviewBase` — a green gate, the
24
+ * triaged quality lap, then the human review — with no design phase: a
25
+ * review that asks for changes is fixed in place and reviewed again, until
26
+ * sign-off.
11
27
  */
12
28
  export const reviewFlow = async (): Promise<void> => {
13
29
  refusePendingHandOff()
30
+ // A review's feedback is whatever sits in this file, so a leftover one would be built, then deleted.
31
+ if (read(REQUIREMENTS) !== undefined) {
32
+ refuse(`gtd: ${REQUIREMENTS} is left over — finish or remove it before a review.`)
33
+ }
14
34
  await gate("review-gate", t.reviewGateBlockedMessage())
15
- await handOffTail(false, start())
35
+ const base = start()
36
+ // The gate just proved the tree green.
37
+ let outcome = await buildTail(base, { green: true })
38
+ while (outcome.verdict === "feedback") {
39
+ await reUnwind(outcome)
40
+ await fixFeedback()
41
+ // The human reviews the whole change again; the lenses only what the fix changed.
42
+ outcome = await buildTail(base, { lensBase: outcome.base })
43
+ }
16
44
  }
17
45
 
18
46
  export const review = shipped(
@@ -9,7 +9,7 @@ import {
9
9
  } from "../../flows/index.js"
10
10
  import {
11
11
  buildTail,
12
- review,
12
+ humanReview,
13
13
  reUnwind,
14
14
  severityOf,
15
15
  type BuildTailOptions,
@@ -17,8 +17,7 @@ import {
17
17
  } from "./review.js"
18
18
  import type { ReviewNote } from "../../steering/index.js"
19
19
  import * as bundled from "../index.js"
20
- import type * as featureEntry from "./index.js"
21
- import { fixtureContext } from "../shared/text.fixture.js"
20
+ import { fixtureContext } from "./text.fixture.js"
22
21
  import { toRequest } from "../../judges/index.js"
23
22
 
24
23
  afterEach(() => installContext(undefined))
@@ -155,7 +154,7 @@ const drive = async (d: Drive): Promise<Run> => {
155
154
  installContext(context)
156
155
  let result: ReviewOutcome | "stopped"
157
156
  try {
158
- result = await review("base", undefined, {
157
+ result = await humanReview("base", undefined, {
159
158
  ...d.options,
160
159
  ...(d.guarded ? { guard: () => void log.push("guard") } : {}),
161
160
  })
@@ -975,11 +974,8 @@ describe("BuildTailOptions.lensBase", () => {
975
974
  })
976
975
  })
977
976
 
978
- // pins the public re-exports from the feature entry point and the published workflows entry
977
+ // pins the public re-exports from the published workflows entry
979
978
  export type _PublicBuildTailOptions = [
980
- featureEntry.BuildTailOptions,
981
- featureEntry.FeedbackTarget,
982
- featureEntry.ReviewRange,
983
979
  bundled.BuildTailOptions,
984
980
  bundled.FeedbackTarget,
985
981
  bundled.ReviewRange,
@@ -16,20 +16,22 @@ import {
16
16
  } from "../../flows/index.js"
17
17
  import type { ReviewNote } from "../../steering/index.js"
18
18
  import * as kit from "../../workflow-kit/index.js"
19
- import { guarded, healthy, type EscalationCount } from "../shared/health.js"
20
- import { NO_FINDINGS, type LensReport } from "../shared/lenses.js"
19
+ import { guarded, healthy, type EscalationCount } from "./health.js"
20
+ import { NO_FINDINGS, type LensReport } from "./lenses.js"
21
21
  import {
22
+ answerReviewQuestions,
22
23
  awaitReview,
24
+ collecting,
23
25
  fixCheck,
26
+ fixNits,
24
27
  fixRisks,
25
28
  QUALITY,
26
29
  REQUIREMENTS,
27
30
  REVIEW,
28
31
  reviewMissing,
29
- } from "../shared/steps.js"
30
- import { closeReview, isOptionalObject, tail, type TailOptions } from "../shared/tail.js"
31
- import * as t from "../shared/text.js"
32
- import { answerReviewQuestions, collecting, fixNits } from "./steps.js"
32
+ } from "./steps.js"
33
+ import { closeReview, isOptionalObject, tail, type TailOptions } from "./tail.js"
34
+ import * as t from "./text.js"
33
35
 
34
36
  export type { ReviewOutcome } from "../../workflow-kit/index.js"
35
37
 
@@ -200,10 +202,11 @@ const composing = (
200
202
  * The quality lap's notes reach a human over everything since `base`, who
201
203
  * answers each one — fix as suggested, won't fix, or their own answer — and
202
204
  * 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.
205
+ * answer or comment is judged: edits end the review as `feedback` for the
206
+ * caller to build, questions are answered in place, nits fixed in one batch,
207
+ * praise dropped.
205
208
  */
206
- export const review = (
209
+ export const humanReview = (
207
210
  base: string,
208
211
  escalations: EscalationCount = { rounds: 0 },
209
212
  options: Omit<BuildTailOptions, "fixFirst" | "green"> = {},
@@ -316,7 +319,7 @@ export function buildTail(
316
319
  )
317
320
  }
318
321
  const [base, form] = options
319
- return tail((escalations, report) => review(base, escalations, { ...form, report }), {
322
+ return tail((escalations, report) => humanReview(base, escalations, { ...form, report }), {
320
323
  ...form,
321
324
  triage: true,
322
325
  })