@pmelab/gtd 27.6.0 → 27.7.1

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 (42) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +25 -14
  3. package/claude/hooks/assist.ts +35 -2
  4. package/claude/hooks/delegate.ts +22 -0
  5. package/claude/hooks/handoff.ts +13 -4
  6. package/claude/hooks/jobs.ts +67 -0
  7. package/claude/hooks/paste.ts +17 -0
  8. package/claude/hooks/register.tsx +126 -65
  9. package/claude/hooks/ship.ts +7 -2
  10. package/claude/hooks/tools.ts +32 -0
  11. package/claude/hooks/wording.ts +11 -0
  12. package/dist/chunks/{reserved-Df8GeSHw.mjs → reserved-UdSuHDUJ.mjs} +2 -2
  13. package/dist/chunks/{tail-DmIfEAI4.mjs → tail-fj60rQ3y.mjs} +1 -1
  14. package/dist/gtd.bundle.mjs +3 -3
  15. package/dist/testkit.mjs +30 -18
  16. package/dist/workflows/cleanup.mjs +2 -2
  17. package/dist/workflows/feature.mjs +3 -3
  18. package/dist/workflows/fix.mjs +2 -2
  19. package/dist/workflows/review.mjs +2 -2
  20. package/dist/workflows/rollout.mjs +1 -1
  21. package/dist/workflows.mjs +3 -3
  22. package/package.json +1 -1
  23. package/src/cli/Cli.ts +2 -2
  24. package/src/testkit/EmittedScriptRecognizer.test.ts +41 -0
  25. package/src/testkit/EmittedScriptRecognizer.ts +16 -7
  26. package/src/testkit/TestRepo.test.ts +79 -1
  27. package/src/testkit/TestRepo.ts +17 -6
  28. package/src/testkit/steps.ts +44 -1
  29. package/src/testkit/testkit.feature +35 -0
  30. package/src/workflows/cleanup/cleanup-path.feature +94 -0
  31. package/src/workflows/cleanup/cleanup.feature +0 -29
  32. package/src/workflows/feature/fast-path.feature +114 -0
  33. package/src/workflows/feature/one-step-land.feature +108 -0
  34. package/src/workflows/feature/refactor-first.feature +137 -0
  35. package/src/workflows/feature/review.test.ts +9 -3
  36. package/src/workflows/feature/review.ts +3 -3
  37. package/src/workflows/feature/risk-tier.feature +0 -63
  38. package/src/workflows/feature/seam-rejection.feature +146 -0
  39. package/src/workflows/feature/seam.feature +0 -67
  40. package/src/workflows/shared/steps.ts +1 -2
  41. package/src/workflows/shared/tail.test.ts +18 -9
  42. package/src/workflows/shared/workflows.steps.ts +5 -4
@@ -118,69 +118,6 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
118
118
  | low |
119
119
  | high |
120
120
 
121
- Scenario Outline: low and high processes produce the same commit subjects to implement.building
122
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
123
- """
124
- # Requirements
125
-
126
- ## Risk Tier
127
-
128
- <tier> — reason
129
- """
130
- And step "design.gate.answer" changes nothing
131
- And step "seam.naive" writes "src/calc.ts" with:
132
- """
133
- export const add = (a: number, b: number) => a + b
134
- export const subtract = (a: number, b: number) => a - b
135
- """
136
- And judge step "seam.judge" answers:
137
- """
138
- [{"id":"shape","answer":"land","p":0.9}]
139
- """
140
- When the process is driven
141
- Then the process rests at "implement.building"
142
- And the commit subjects end with:
143
- """
144
- gtd(agent): design.triage → design.gate.answer
145
- gtd(human): design.gate.answer → seam.naive
146
- gtd(agent): seam.naive → seam.judge
147
- gtd(judge): seam.judge → implement.building
148
- """
149
-
150
- Examples:
151
- | tier |
152
- | low |
153
- | high |
154
-
155
- Scenario Outline: the implementation prompt demands a flag for high and none for low
156
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
157
- """
158
- # Requirements
159
-
160
- ## Risk Tier
161
-
162
- <tier> — reason
163
- """
164
- And step "design.gate.answer" changes nothing
165
- And step "seam.naive" writes "src/calc.ts" with:
166
- """
167
- export const add = (a: number, b: number) => a + b
168
- export const subtract = (a: number, b: number) => a - b
169
- """
170
- And judge step "seam.judge" answers:
171
- """
172
- [{"id":"shape","answer":"land","p":0.9}]
173
- """
174
- When the process is driven
175
- Then the process rests at "implement.building"
176
- And the resting step's content contains "<demand>"
177
- And the resting step's content does not contain "<absent>"
178
-
179
- Examples:
180
- | tier | demand | absent |
181
- | high | Put the new behaviour behind a new release flag | Add no release flag |
182
- | low | Add no release flag — this change is low risk tier | new release flag |
183
-
184
121
  Scenario: the tier comes from the requirements the final design gate lap accepted
185
122
  Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
186
123
  """
@@ -0,0 +1,146 @@
1
+ # One SDLC path per file, so a changed path reds exactly one fixture — do not merge.
2
+ @inmem
3
+ Feature: rejected naive passes walk one fixed path through the steer gate to idle
4
+
5
+ With GTD_FLAG_NAIVE_PASS on and GTD_FLAG_RISK_TIER off, three rejected naive
6
+ passes rest at the steer gate, where a human edit restarts the seam and the
7
+ steered rerun lands. The flags are pinned so the per-flag test runs never
8
+ move it off its path.
9
+
10
+ Background:
11
+ Given the shipped workflows, as gtd install scaffolds them
12
+ And an environment variable "GTD_QUALITYREVIEWS" set to ""
13
+ And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
14
+ And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
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
+ And step "design.gate.answer" changes nothing
39
+
40
+ Scenario: three rejects rest at the steer gate, a steered rerun lands and runs to idle
41
+ Given step "seam.naive" writes "src/calc.ts" with:
42
+ """
43
+ export const add = (a: number, b: number) => b + a
44
+ """
45
+ And step "implement.health.check" changes nothing
46
+ And step "implement.closing" runs its script
47
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
48
+ """
49
+ # Review: abc1234
50
+
51
+ <!-- base: 0000000 -->
52
+
53
+ ## calc
54
+
55
+ - [ ] ./src/calc.ts#1-2
56
+ subtract added
57
+ """
58
+ And step "build.review.closing" runs its script
59
+ When the process is driven
60
+ Then the process rests at "seam.judge"
61
+
62
+ Given step "seam.naive" is prompted with "Your previous attempt was rejected"
63
+ And judge step "seam.judge" answers:
64
+ """
65
+ [{"id":"shape","answer":"reject","p":0.9}]
66
+ """
67
+ When the process is driven
68
+ Then the process rests at "seam.steer"
69
+ And the resting step's content contains "Overrule"
70
+ And the resting step's content contains "src/calc.ts"
71
+
72
+ Given a file ".gtd/REQUIREMENTS.md" with:
73
+ """
74
+ # Requirements
75
+
76
+ ## Concerns
77
+
78
+ ### Subtract
79
+
80
+ - `subtract(a, b)` returns `a - b`, in the calculator module.
81
+ """
82
+ And step "seam.naive" from now on changes nothing
83
+ And step "seam.naive" writes "src/calc.ts" with:
84
+ """
85
+ export const add = (a: number, b: number) => a + b
86
+ export const subtract = (a: number, b: number) => a - b
87
+ """
88
+ And judge step "seam.judge" answers:
89
+ """
90
+ [{"id":"shape","answer":"land","p":0.9}]
91
+ """
92
+ And step "implement.building" writes "src/calc.ts" with:
93
+ """
94
+ export const add = (a: number, b: number) => a + b
95
+ export const subtract = (a: number, b: number) => a - b
96
+ export const SUBTRACT = true
97
+ """
98
+ When gtd lands
99
+ And the process is driven
100
+ Then the process rests at "build.review.await-review"
101
+
102
+ Given a file ".gtd/REVIEW.md" with:
103
+ """
104
+ # Review: abc1234
105
+
106
+ <!-- base: 0000000 -->
107
+
108
+ ## calc
109
+
110
+ - [x] ./src/calc.ts#1-2
111
+ subtract added
112
+ """
113
+ When gtd lands
114
+ And the process is driven
115
+ Then the process rests at "idle"
116
+ And the commit subjects end with:
117
+ """
118
+ gtd(human): idle → unwind
119
+ gtd(check): unwind → start-gate.check
120
+ gtd(check): start-gate.check → design.triage
121
+ gtd(agent): design.triage → design.gate.answer
122
+ gtd(human): design.gate.answer → seam.naive
123
+ gtd(agent): seam.naive → seam.judge
124
+ gtd(judge): seam.judge → seam.naive
125
+ gtd(agent): seam.naive → seam.judge
126
+ gtd(judge): seam.judge → seam.naive
127
+ gtd(agent): seam.naive → seam.judge
128
+ gtd(judge): seam.judge → seam.steer
129
+ gtd(human): seam.steer → seam.naive
130
+ gtd(agent): seam.naive → seam.judge
131
+ gtd(judge): seam.judge → implement.building
132
+ gtd(agent): implement.building → implement.health.check
133
+ gtd(check): implement.health.check → implement.closing
134
+ gtd(check): implement.closing → build.review.reviewing
135
+ gtd(agent): build.review.reviewing → build.review.await-review
136
+ gtd(human): build.review.await-review → build.review.closing
137
+ gtd(check): build.review.closing → idle
138
+ """
139
+ And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_naive_pass=1"
140
+ And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
141
+ And the commit "gtd(agent): seam.naive → seam.judge" has a body containing "Gtd-Kept:"
142
+ And the commit "gtd(judge): seam.judge → seam.naive" has a body containing "\"answer\":\"reject\""
143
+ And the commit "gtd(judge): seam.judge → seam.steer" has a body containing "\"answer\":\"reject\""
144
+ And the commit "gtd(judge): seam.judge → implement.building" has a body containing "\"answer\":\"land\""
145
+ And no commit body contains "Gtd-Approved:"
146
+ And no commit body contains "Gtd-Workflow:"
@@ -166,73 +166,6 @@ Feature: the naive pass and seam judge replace architecture behind GTD_FLAG_NAIV
166
166
  And the resting step's content contains "src/calc.ts"
167
167
  And the resting step's content does not contain "src/calc*.ts"
168
168
 
169
- Scenario: a refactor-first judgment rests at the approval gate, then refactors, then implements
170
- Given step "seam.naive" writes "src/calc.ts" with:
171
- """
172
- export const add = (a: number, b: number) => a + b
173
- export const subtract = (a: number, b: number) => a - b
174
- """
175
- And judge step "seam.judge" answers:
176
- """
177
- [{"id":"shape","answer":"refactor-first","p":0.9}]
178
- """
179
- And step "refactor.deriving" writes ".gtd/REFACTOR.md" with:
180
- """
181
- # Behaviour-neutral refactor
182
-
183
- ## Scope (read-only)
184
-
185
- - `src/calc.ts` # gtd-path-exempt: scenario fixture, not a repo file
186
- """
187
- And step "refactor.building" writes "src/calc.ts" with:
188
- """
189
- export const add = (a: number, b: number) => a + b
190
- export const op = (a: number, b: number, sign: number) => a + sign * b
191
- """
192
- And step "refactor.health.check" changes nothing
193
- And step "refactor.closing" runs its script
194
- When the process is driven
195
- Then the process rests at "seam.approve"
196
- And the resting step's content contains "src/calc.ts"
197
- When gtd lands
198
- Then the last commit subject is "gtd(human): seam.approve → refactor.deriving"
199
- And the last commit body contains "Gtd-Approved:"
200
- When the process is driven
201
- Then the process rests at "implement.building"
202
- And the commit subjects end with:
203
- """
204
- gtd(judge): seam.judge → seam.approve
205
- gtd(human): seam.approve → refactor.deriving
206
- gtd(check): refactor.deriving → refactor.building
207
- gtd(agent): refactor.building → refactor.health.check
208
- gtd(check): refactor.health.check → refactor.closing
209
- gtd(check): refactor.closing → implement.building
210
- """
211
-
212
- Scenario: three rejects retry the naive pass twice, then rest at the steer gate
213
- Given step "seam.naive" writes "src/calc.ts" with:
214
- """
215
- export const add = (a: number, b: number) => b + a
216
- """
217
- And judge step "seam.judge" answers:
218
- """
219
- [{"id":"shape","answer":"reject","p":0.9}]
220
- """
221
- When the process is driven
222
- Then the process rests at "seam.steer"
223
- And the resting step's content contains "Overrule"
224
- And the resting step's content contains "src/calc.ts"
225
- And the commit subjects end with:
226
- """
227
- gtd(human): design.gate.answer → seam.naive
228
- gtd(agent): seam.naive → seam.judge
229
- gtd(judge): seam.judge → seam.naive
230
- gtd(agent): seam.naive → seam.judge
231
- gtd(judge): seam.judge → seam.naive
232
- gtd(agent): seam.naive → seam.judge
233
- gtd(judge): seam.judge → seam.steer
234
- """
235
-
236
169
  Scenario: an edit at the steer gate runs the naive pass again with a fresh retry budget
237
170
  Given step "seam.naive" writes "src/calc.ts" with:
238
171
  """
@@ -117,7 +117,6 @@ export const fixRisks = (notes: readonly t.NoteInput[]): Promise<void> =>
117
117
  export const awaitReview = (
118
118
  base: string,
119
119
  message = t.buildReviewAwaitReviewMessage(base),
120
- acceptClean = false,
121
120
  ): Promise<void> =>
122
121
  human("review.await-review", {
123
122
  message,
@@ -125,7 +124,7 @@ export const awaitReview = (
125
124
  file: REVIEW,
126
125
  mode: "review",
127
126
  base,
128
- ...(acceptClean ? { acceptClean } : {}),
127
+ acceptClean: true,
129
128
  })
130
129
 
131
130
  /** The round at commit `round` left no `.gtd/REVIEW.md`. */
@@ -38,8 +38,18 @@ const handOff = async (
38
38
  } = {},
39
39
  ) => {
40
40
  const log: string[] = []
41
+ const gates: boolean[] = []
41
42
  const scripts = new Map<string, string>()
42
43
  const files = new Map<string, string>()
44
+ const onReviewing = () => {
45
+ if (log.filter((n) => n === "review.reviewing").length > 1) throw new Error("re-review")
46
+ if (after !== "unwritten") files.set(REVIEW, reviewDoc("new add function"))
47
+ }
48
+ const onGate = (request: StepRequest) => {
49
+ if (request.kind === "human") gates.push(request.options.acceptClean === true)
50
+ if (after === "deleted") files.delete(REVIEW)
51
+ else if (after !== "unwritten") files.set(REVIEW, after)
52
+ }
43
53
  installContext(
44
54
  fixtureContext(
45
55
  { codeThreads: extra.codeThreads ?? [] },
@@ -48,14 +58,8 @@ const handOff = async (
48
58
  if (request.kind === "restart") return Promise.resolve()
49
59
  log.push(request.name)
50
60
  if (request.kind === "run") scripts.set(request.name, request.body)
51
- if (request.name === "review.reviewing") {
52
- if (log.filter((n) => n === "review.reviewing").length > 1) throw new Error("re-review")
53
- if (after !== "unwritten") files.set(REVIEW, reviewDoc("new add function"))
54
- }
55
- if (request.name === "review.await-review") {
56
- if (after === "deleted") files.delete(REVIEW)
57
- else if (after !== "unwritten") files.set(REVIEW, after)
58
- }
61
+ if (request.name === "review.reviewing") onReviewing()
62
+ if (request.name === "review.await-review") onGate(request)
59
63
  return Promise.resolve()
60
64
  },
61
65
  refuse: (message: string): never => {
@@ -96,7 +100,7 @@ const handOff = async (
96
100
  () => undefined,
97
101
  (e: unknown) => (e as Error).message,
98
102
  )
99
- return { log, scripts, refusal }
103
+ return { log, scripts, refusal, gates }
100
104
  }
101
105
 
102
106
  describe("the hand-off review", () => {
@@ -105,6 +109,11 @@ describe("the hand-off review", () => {
105
109
  expect(run.log).toEqual(["review.reviewing", "review.await-review", "review.closing"])
106
110
  })
107
111
 
112
+ it("the review gate accepts a clean landing", async () => {
113
+ const run = await handOff(reviewDoc("new add function"))
114
+ expect(run.gates).toEqual([true])
115
+ })
116
+
108
117
  it("a note ends the round with the review kept and the reviewer's version copied beside it", async () => {
109
118
  const run = await handOff(reviewDoc("new add function — rename it to sum"))
110
119
  expect(run.log).toEqual(["review.reviewing", "review.await-review", "review.handing-off"])
@@ -35,12 +35,13 @@ Then("the last commit body contains {string}", (world: TestkitWorld, text: strin
35
35
  Then(
36
36
  "the commit {string} has a body containing {string}",
37
37
  (world: TestkitWorld, subject: string, text: string) => {
38
- const message = world
38
+ // Every commit with the subject, so a repeated step is not judged by its first match.
39
+ const matching = world
39
40
  .repo()
40
41
  .messages()
41
- .find((m) => m.split("\n")[0] === subject)
42
- assert.ok(message !== undefined, `no commit "${subject}"`)
43
- assert.ok(message.includes(text), message)
42
+ .filter((m) => m.split("\n")[0] === subject)
43
+ assert.ok(matching.length > 0, `no commit "${subject}"`)
44
+ for (const message of matching) assert.ok(message.includes(text), message)
44
45
  },
45
46
  )
46
47