@pmelab/gtd 51.0.1 → 52.1.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 (74) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +46 -40
  3. package/dist/chunks/{reserved-CUkk9XgL.mjs → reserved-DxB5CROj.mjs} +73 -38
  4. package/dist/chunks/{review-BaCXlIT2.mjs → review-CKMSdvgW.mjs} +11 -9
  5. package/dist/chunks/{steering-DtUVUz9l.mjs → steering-CLhu8C7q.mjs} +262 -52
  6. package/dist/chunks/{tail-CX6Pdsql.mjs → tail-QZc7Uz2U.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +198 -43
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +1 -1
  10. package/dist/web/main.css +1 -1
  11. package/dist/web/main.js +510 -147
  12. package/dist/workflow-kit.mjs +1 -1
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +228 -222
  15. package/dist/workflows/fix.mjs +3 -3
  16. package/dist/workflows/review.mjs +3 -3
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +570 -317
  19. package/package.json +5 -4
  20. package/src/steering/FORMATS.md +4 -1
  21. package/src/steering/SteeringFormat.ts +7 -1
  22. package/src/steering/contract.test.ts +2 -1
  23. package/src/steering/fixtures/review/sectioned.md +31 -0
  24. package/src/steering/fixtures/review/sectioned.read.json +119 -0
  25. package/src/steering/index.ts +4 -0
  26. package/src/steering/qa.ts +1 -1
  27. package/src/steering/review.test.ts +270 -4
  28. package/src/steering/review.ts +328 -69
  29. package/src/testkit/steps.ts +13 -0
  30. package/src/workflows/feature/acceptance-criteria.feature +159 -3
  31. package/src/workflows/feature/continuation-cap.feature +4 -0
  32. package/src/workflows/feature/continuation-rebuild.feature +4 -0
  33. package/src/workflows/feature/continuation-slices.feature +4 -0
  34. package/src/workflows/feature/continuation.feature +4 -0
  35. package/src/workflows/feature/criteria.test.ts +60 -1
  36. package/src/workflows/feature/criteria.ts +87 -8
  37. package/src/workflows/feature/fast-path.feature +5 -6
  38. package/src/workflows/feature/feature.test.ts +6 -5
  39. package/src/workflows/feature/feature.ts +1 -2
  40. package/src/workflows/feature/flag-mode.feature +5 -6
  41. package/src/workflows/feature/green.ts +22 -0
  42. package/src/workflows/feature/hands-on-skip.feature +384 -0
  43. package/src/workflows/feature/lens-models.feature +4 -4
  44. package/src/workflows/feature/lens-review.feature +10 -8
  45. package/src/workflows/feature/naive-acceptance.feature +4 -0
  46. package/src/workflows/feature/one-step-land.feature +5 -6
  47. package/src/workflows/feature/planning.ts +2 -1
  48. package/src/workflows/feature/prototype-rounds.feature +5 -1
  49. package/src/workflows/feature/prototype.feature +18 -9
  50. package/src/workflows/feature/prototype.ts +48 -17
  51. package/src/workflows/feature/refactor-absorbs.feature +7 -8
  52. package/src/workflows/feature/refactor-first.feature +7 -8
  53. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  54. package/src/workflows/feature/refactor-rebuild.feature +11 -12
  55. package/src/workflows/feature/refactor-review.feature +72 -72
  56. package/src/workflows/feature/review-answers.feature +94 -5
  57. package/src/workflows/feature/review-gate.feature +23 -53
  58. package/src/workflows/feature/reviewer-walkthrough.feature +33 -12
  59. package/src/workflows/feature/risk-tier.feature +32 -0
  60. package/src/workflows/feature/seam-rejection.feature +9 -6
  61. package/src/workflows/feature/seam.feature +9 -6
  62. package/src/workflows/feature/seam.test.ts +5 -3
  63. package/src/workflows/feature/seam.ts +2 -3
  64. package/src/workflows/feature/severity-budget.feature +11 -7
  65. package/src/workflows/feature/severity-sorted-review.feature +54 -54
  66. package/src/workflows/feature/slice-findings.feature +9 -15
  67. package/src/workflows/feature/slicing.feature +36 -24
  68. package/src/workflows/feature/standards.feature +4 -4
  69. package/src/workflows/feature/tier-pin.feature +4 -0
  70. package/src/workflows/shared/review.ts +26 -6
  71. package/src/workflows/shared/text.test.ts +85 -11
  72. package/src/workflows/shared/text.ts +95 -42
  73. package/src/workflows/feature/coverage.feature +0 -165
  74. package/src/workflows/feature/coverage.ts +0 -120
@@ -30,11 +30,15 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
30
30
  greeting says "Helo"; make it "Hello".
31
31
  """
32
32
 
33
- Scenario: triage writes ID'd criteria that reach the naive pass through the design gate
33
+ Scenario: [AC-6] triage writes ID'd criteria that reach the naive pass through the design gate
34
34
  Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
35
35
  """
36
36
  # Requirements
37
37
 
38
+ ## Impact
39
+
40
+ - the greeting module
41
+
38
42
  ## Acceptance Criteria
39
43
 
40
44
  - AC-1: `greeting` reads "Hello, world".
@@ -45,6 +49,7 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
45
49
  Then the process rests at "seam.naive"
46
50
  And ".gtd/REQUIREMENTS.md" contains "- AC-1: `greeting` reads \"Hello, world\"."
47
51
  And ".gtd/REQUIREMENTS.md" contains "- AC-2: no other export changes."
52
+ And the resting step's content contains "maps where the change lands"
48
53
  And the resting step's content contains "Acceptance Criteria"
49
54
  And the commit subjects end with:
50
55
  """
@@ -58,6 +63,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
58
63
  """
59
64
  # Requirements
60
65
 
66
+ ## Impact
67
+
68
+ - the greeting module
69
+
61
70
  ## Acceptance Criteria
62
71
 
63
72
  - `greeting` reads "Hello, world".
@@ -69,6 +78,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
69
78
  """
70
79
  # Requirements
71
80
 
81
+ ## Impact
82
+
83
+ - the greeting module
84
+
72
85
  ## Acceptance Criteria
73
86
 
74
87
  - AC-1: `greeting` reads "Hello, world".
@@ -81,6 +94,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
81
94
  """
82
95
  # Requirements
83
96
 
97
+ ## Impact
98
+
99
+ - the greeting module
100
+
84
101
  ## Acceptance Criteria
85
102
 
86
103
  - AC-1: `greeting` reads "Hello, world".
@@ -110,15 +127,114 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
110
127
  """
111
128
  # Requirements
112
129
 
130
+ ## Impact
131
+
132
+ - the greeting module
133
+
134
+ ## Acceptance Criteria
135
+
136
+ - AC-1: `greeting` reads "Hi".
137
+ """
138
+ Then driving the process is refused with "criteria-frozen"
139
+
140
+ Rule: impact
141
+
142
+ Scenario: [AC-1] a triage turn with no Impact section is refused, naming it
143
+ Given a file "TODO.md" with:
144
+ """
145
+ # Fix the typo
146
+
147
+ greeting says "Helo"; make it "Hello".
148
+ """
149
+ And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
150
+ """
151
+ # Requirements
152
+
153
+ ## Acceptance Criteria
154
+
155
+ - AC-1: `greeting` reads "Hello, world".
156
+ """
157
+ Then driving the process is refused with "impact: .gtd/REQUIREMENTS.md has no \"## Impact\" section"
158
+
159
+ Scenario: [AC-9] a design.impact turn that changes the criteria is refused
160
+ Given a file "TODO.md" with:
161
+ """
162
+ # Fix the typo
163
+
164
+ ## Acceptance criteria
165
+
166
+ - `greeting` reads "Hello, world".
167
+ """
168
+ And judge step "design.clear" answers:
169
+ """
170
+ [{"id":"clear","answer":"yes","p":0.9}]
171
+ """
172
+ And step "design.normalise" runs its script
173
+ And step "design.impact" writes ".gtd/REQUIREMENTS.md" with:
174
+ """
175
+ # Requirements
176
+
177
+ ## Impact
178
+
179
+ - the greeting module
180
+
113
181
  ## Acceptance Criteria
114
182
 
115
183
  - AC-1: `greeting` reads "Hi".
116
184
  """
117
185
  Then driving the process is refused with "criteria-frozen"
118
186
 
187
+ Scenario: [AC-9] a design.impact turn that adds no Impact section is refused
188
+ Given a file "TODO.md" with:
189
+ """
190
+ # Fix the typo
191
+
192
+ ## Acceptance criteria
193
+
194
+ - `greeting` reads "Hello, world".
195
+ """
196
+ And judge step "design.clear" answers:
197
+ """
198
+ [{"id":"clear","answer":"yes","p":0.9}]
199
+ """
200
+ And step "design.normalise" runs its script
201
+ And step "design.impact" appends to ".gtd/REQUIREMENTS.md":
202
+ """
203
+ Touches the greeting module.
204
+ """
205
+ Then driving the process is refused with "impact: .gtd/REQUIREMENTS.md has no \"## Impact\" section"
206
+
207
+ Scenario: [AC-9] a design.impact turn that drops the sketch pointer is refused
208
+ Given a file "TODO.md" with:
209
+ """
210
+ # Fix the typo
211
+
212
+ ## Acceptance criteria
213
+
214
+ - `greeting` reads "Hello, world".
215
+ """
216
+ And judge step "design.clear" answers:
217
+ """
218
+ [{"id":"clear","answer":"yes","p":0.9}]
219
+ """
220
+ And step "design.normalise" runs its script
221
+ And step "design.impact" writes ".gtd/REQUIREMENTS.md" with:
222
+ """
223
+ # Requirements
224
+
225
+ ## Impact
226
+
227
+ - the greeting module
228
+
229
+ ## Acceptance Criteria
230
+
231
+ - AC-1: `greeting` reads "Hello, world".
232
+ """
233
+ Then driving the process is refused with "impact-only"
234
+
119
235
  Rule: the skip path
120
236
 
121
- Scenario: clear stated criteria are normalised into ID'd requirements with no triage and no design gate
237
+ Scenario: [AC-9] [AC-10] clear stated criteria are normalised into ID'd requirements with no triage and no design gate
122
238
  Given a file "TODO.md" with:
123
239
  """
124
240
  # Fix the typo
@@ -133,18 +249,52 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
133
249
  [{"id":"clear","answer":"yes","p":0.9}]
134
250
  """
135
251
  And step "design.normalise" runs its script
252
+ And step "design.impact" appends to ".gtd/REQUIREMENTS.md":
253
+ """
254
+ ## Impact
255
+
256
+ - the greeting module
257
+ """
136
258
  When the process is driven
137
259
  Then the process rests at "seam.naive"
138
260
  And ".gtd/REQUIREMENTS.md" contains "- AC-1: `greeting` reads \"Hello, world\"."
139
261
  And ".gtd/REQUIREMENTS.md" contains "- AC-2: no other export changes."
262
+ And ".gtd/REQUIREMENTS.md" contains "Stated in `TODO.md` by the sketch"
263
+ And the resting step's content contains "maps where the change lands"
140
264
  And the commit subjects end with:
141
265
  """
142
266
  gtd(human): idle → unwind
143
267
  gtd(check): unwind → start-gate.check
144
268
  gtd(check): start-gate.check → design.clear
145
269
  gtd(judge): design.clear → design.normalise
146
- gtd(check): design.normalise → seam.naive
270
+ gtd(check): design.normalise → design.impact
271
+ gtd(agent): design.impact → seam.naive
272
+ """
273
+
274
+ Scenario: a fast-path process recorded before design.impact existed fails replay
275
+ Given a file "TODO.md" with:
276
+ """
277
+ # Fix the typo
278
+
279
+ ## Acceptance criteria
280
+
281
+ - `greeting` reads "Hello, world".
282
+ """
283
+ And judge step "design.clear" answers:
284
+ """
285
+ [{"id":"clear","answer":"yes","p":0.9}]
286
+ """
287
+ And step "design.normalise" runs its script
288
+ When the process is driven
289
+ Then the process rests at "design.impact"
290
+ Given the working tree is committed with the message:
291
+ """
292
+ gtd(agent): seam.naive → seam.check
293
+
294
+ Gtd-Step: seam.naive#1
295
+ Gtd-Format: 1
147
296
  """
297
+ Then driving the process is refused with "the workflow changed under this process"
148
298
 
149
299
  Scenario: IDs the sketch already gave its criteria are kept
150
300
  Given a file "TODO.md" with:
@@ -159,6 +309,12 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
159
309
  [{"id":"clear","answer":"yes","p":0.9}]
160
310
  """
161
311
  And step "design.normalise" runs its script
312
+ And step "design.impact" appends to ".gtd/REQUIREMENTS.md":
313
+ """
314
+ ## Impact
315
+
316
+ - the greeting module
317
+ """
162
318
  When the process is driven
163
319
  Then the process rests at "seam.naive"
164
320
  And ".gtd/REQUIREMENTS.md" contains "- AC-3: `greeting` reads \"Hello, world\"."
@@ -26,6 +26,10 @@ Feature: past its continuation cap a phase rests for a person
26
26
  """
27
27
  # Requirements
28
28
 
29
+ ## Impact
30
+
31
+ - the greeting module
32
+
29
33
  ## Acceptance Criteria
30
34
 
31
35
  - AC-1: `subtract(5, 3)` returns 2.
@@ -31,6 +31,10 @@ Feature: a rebuild stopped at its context limit is continued on the branch
31
31
  """
32
32
  # Requirements
33
33
 
34
+ ## Impact
35
+
36
+ - the greeting module
37
+
34
38
  ## Acceptance Criteria
35
39
 
36
40
  - AC-1: `subtract(5, 3)` returns 2.
@@ -30,6 +30,10 @@ Feature: a continuation stays inside its coarse slice
30
30
  """
31
31
  # Requirements
32
32
 
33
+ ## Impact
34
+
35
+ - the greeting module
36
+
33
37
  ## Acceptance Criteria
34
38
 
35
39
  - AC-1: subtract(a, b) returns a - b.
@@ -29,6 +29,10 @@ Feature: a turn stopped at its context limit is snapshotted and continued
29
29
  """
30
30
  # Requirements
31
31
 
32
+ ## Impact
33
+
34
+ - the greeting module
35
+
32
36
  ## Acceptance Criteria
33
37
 
34
38
  - AC-1: `subtract(5, 3)` returns 2.
@@ -1,6 +1,13 @@
1
1
  import { describe, expect, it } from "vitest"
2
2
  import type { Change } from "../../flows/index.js"
3
- import { criteriaOf, criterionIds, statedCriteria, unstableIds } from "./criteria.js"
3
+ import {
4
+ criteriaOf,
5
+ criterionIds,
6
+ handsOnIds,
7
+ hasImpact,
8
+ statedCriteria,
9
+ unstableIds,
10
+ } from "./criteria.js"
4
11
 
5
12
  const added = (path: string, after: string): Change => ({
6
13
  path,
@@ -48,6 +55,17 @@ describe("criteriaOf", () => {
48
55
  ).toEqual([{ id: "AC-1", text: "exits 2 when config is bad - missing file" }])
49
56
  })
50
57
 
58
+ it("reads the (no hands-on) tag between an ID and its colon", () => {
59
+ expect(
60
+ criteriaOf(
61
+ "## Acceptance Criteria\n\n- AC-1: runs\n- AC-2 (no hands-on): the prompt says so\n",
62
+ ),
63
+ ).toEqual([
64
+ { id: "AC-1", text: "runs" },
65
+ { id: "AC-2", text: "the prompt says so", noHandsOn: true },
66
+ ])
67
+ })
68
+
51
69
  it("is undefined without the section", () => {
52
70
  expect(criteriaOf("# R\n\n- AC-1: x\n")).toBeUndefined()
53
71
  })
@@ -66,6 +84,19 @@ describe("statedCriteria", () => {
66
84
  })
67
85
  })
68
86
 
87
+ it("keeps a stated (no hands-on) tag, renumbered or not", () => {
88
+ const tagged = "## Acceptance criteria\n\n- AC-1: a\n- AC-2 (no hands-on): b\n"
89
+ expect(statedCriteria([added("TODO.md", tagged)], "abc")?.criteria).toEqual([
90
+ { id: "AC-1", text: "a" },
91
+ { id: "AC-2", text: "b", noHandsOn: true },
92
+ ])
93
+ const partial = "## Acceptance criteria\n\n- a\n- AC-7 (no hands-on): b\n"
94
+ expect(statedCriteria([added("TODO.md", partial)], "abc")?.criteria).toEqual([
95
+ { id: "AC-1", text: "a" },
96
+ { id: "AC-2", text: "b", noHandsOn: true },
97
+ ])
98
+ })
99
+
69
100
  it("renumbers when the stated IDs are partial or clash", () => {
70
101
  const sketch = [added("TODO.md", "## Acceptance criteria\n\n- AC-4: a\n- b\n")]
71
102
  expect(statedCriteria(sketch, "abc")?.criteria).toEqual([
@@ -121,6 +152,20 @@ describe("criterionIds", () => {
121
152
  })
122
153
  })
123
154
 
155
+ describe("handsOnIds", () => {
156
+ const requirements =
157
+ "## Acceptance Criteria\n\n- AC-1: a\n- AC-2 (no hands-on): b\n- AC-3: c\n- AC-4 (no hands-on): d\n"
158
+
159
+ it("leaves out every criterion tagged (no hands-on)", () => {
160
+ expect(handsOnIds(requirements)).toEqual(["AC-1", "AC-3"])
161
+ })
162
+
163
+ it("keeps to the slice", () => {
164
+ expect(handsOnIds(requirements, { only: ["AC-2", "AC-3"] })).toEqual(["AC-3"])
165
+ expect(handsOnIds(requirements, { only: ["AC-2", "AC-4"] })).toEqual([])
166
+ })
167
+ })
168
+
124
169
  describe("unstableIds", () => {
125
170
  const ids = { closed: ["AC-1", "AC-3"], used: ["AC-1", "AC-2", "AC-3"] }
126
171
 
@@ -139,3 +184,17 @@ describe("unstableIds", () => {
139
184
  expect(unstableIds(["AC-0", "AC-2"], ids)).toMatch(/^numbers AC-0, AC-2 at or below AC-3/)
140
185
  })
141
186
  })
187
+
188
+ describe("hasImpact", () => {
189
+ it.each([
190
+ ["a level-2 heading", "# R\n\n## Impact\n\nx\n", true],
191
+ ["a mixed-case heading", "## impact\n", true],
192
+ ["a level-3 heading", "### Impact\n", false],
193
+ ["a level-1 heading", "# Impact\n", false],
194
+ ["a longer heading", "## Impactful\n", false],
195
+ ["a fenced heading", "```md\n## Impact\n```\n", false],
196
+ ["no heading", "# R\n\nImpact\n", false],
197
+ ])("%s", (_name, text, expected) => {
198
+ expect(hasImpact(text)).toBe(expected)
199
+ })
200
+ })
@@ -1,20 +1,24 @@
1
1
  import {
2
2
  answered,
3
3
  changes,
4
+ head,
4
5
  judge,
5
6
  read,
6
7
  refuse,
8
+ requireProgress,
7
9
  run,
8
10
  scope,
9
11
  type Change,
10
12
  } from "../../flows/index.js"
11
13
  import { HEADING, ITEM, markFenced } from "../shared/fences.js"
12
- import { REQUIREMENTS } from "../shared/steps.js"
14
+ import { planner, REQUIREMENTS } from "../shared/steps.js"
13
15
  import * as t from "../shared/text.js"
14
16
 
15
17
  export interface Criterion {
16
18
  readonly id: string
17
19
  readonly text: string
20
+ /** Tagged `(no hands-on)`: its tests prove it, so the hands-on test skips it. */
21
+ readonly noHandsOn?: true
18
22
  }
19
23
 
20
24
  /** Acceptance criteria the human's sketch stated outright, ready to normalise. */
@@ -29,7 +33,13 @@ export interface StatedCriteria {
29
33
  /** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
30
34
  const CLEAR_MIN_P = 0.8
31
35
 
32
- const ID = /^(AC-\d+):\s*(.*)$/
36
+ const ID = /^(AC-\d+)(\s*\(no hands-on\))?:\s*(.*)$/
37
+
38
+ const criterion = (id: RegExpExecArray, as = id[1]!): Criterion => ({
39
+ id: as,
40
+ text: id[3]!,
41
+ ...(id[2] === undefined ? {} : { noHandsOn: true }),
42
+ })
33
43
 
34
44
  /** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
35
45
  const listUnder = (text: string, title: RegExp, level?: number): string[] | undefined => {
@@ -66,7 +76,7 @@ const listUnder = (text: string, title: RegExp, level?: number): string[] | unde
66
76
  export const criteriaOf = (text: string): readonly Criterion[] | undefined =>
67
77
  listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
68
78
  const id = ID.exec(item)
69
- return id === null ? { id: "", text: item } : { id: id[1]!, text: id[2]! }
79
+ return id === null ? { id: "", text: item } : criterion(id)
70
80
  })
71
81
 
72
82
  /**
@@ -90,6 +100,12 @@ export const criterionIds = (text: string, slice?: CriteriaSlice): string[] =>
90
100
  !(slice?.except ?? []).includes(id),
91
101
  )
92
102
 
103
+ /** The ID'd criteria of `text` (`slice`'s when given) a person can try by hand: none tagged `(no hands-on)`. */
104
+ export const handsOnIds = (text: string, slice?: CriteriaSlice): string[] => {
105
+ const tagged = (criteriaOf(text) ?? []).filter((c) => c.noHandsOn).map((c) => c.id)
106
+ return criterionIds(text, slice).filter((id) => !tagged.includes(id))
107
+ }
108
+
93
109
  /** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
94
110
  export const sliceIds = (slice: CriteriaSlice | undefined): string[] | undefined =>
95
111
  slice === undefined ? undefined : criterionIds(read(REQUIREMENTS) ?? "", slice)
@@ -98,11 +114,11 @@ export const sliceIds = (slice: CriteriaSlice | undefined): string[] | undefined
98
114
  const numbered = (items: readonly string[]): Criterion[] => {
99
115
  const ids = items.map((item) => ID.exec(item))
100
116
  const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id![1])).size === ids.length
101
- return items.map((item, i) =>
102
- own
103
- ? { id: ids[i]![1]!, text: ids[i]![2]! }
104
- : { id: `AC-${i + 1}`, text: item.replace(ID, "$2") },
105
- )
117
+ return items.map((item, i) => {
118
+ const id = ids[i]
119
+ if (own) return criterion(id!)
120
+ return id === null ? { id: `AC-${i + 1}`, text: item } : criterion(id!, `AC-${i + 1}`)
121
+ })
106
122
  }
107
123
 
108
124
  const STATED = /^acceptance criteria:?$/i
@@ -151,6 +167,54 @@ export const requireCriteria = (file: string): void => {
151
167
  )
152
168
  }
153
169
 
170
+ const isImpactHeading = (h: RegExpExecArray | null): boolean =>
171
+ h !== null && h[1]!.length === 2 && /^impact$/i.test(h[2]!.trim())
172
+
173
+ /** Whether `text` has an unfenced, level-2 `## Impact` heading. */
174
+ export const hasImpact = (text: string): boolean =>
175
+ markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)))
176
+
177
+ /** `text` without its `## Impact` section, whitespace collapsed. */
178
+ const withoutImpact = (text: string): string => {
179
+ let inImpact = false
180
+ const kept = markFenced(text).filter((l) => {
181
+ const h = l.fenced ? null : HEADING.exec(l.line)
182
+ if (h !== null && h[1]!.length <= 2) inImpact = isImpactHeading(h)
183
+ return !inImpact
184
+ })
185
+ return kept
186
+ .map((l) => l.line)
187
+ .join(" ")
188
+ .replace(/\s+/g, " ")
189
+ .trim()
190
+ }
191
+
192
+ /**
193
+ * Refuse a turn that leaves `file` without a `## Impact` section. Only the
194
+ * pending turn (`head()` is `""`): a triage lap recorded before Impact existed
195
+ * replays without one, and refusing there would fail every in-flight process.
196
+ * A human's edit at a later gate is not checked — the human owns that file then.
197
+ */
198
+ export const requireImpact = (file: string): void => {
199
+ if (head() !== "") return
200
+ const text = read(file)
201
+ if (text === undefined) return
202
+ if (hasImpact(text)) return
203
+ refuse(
204
+ `gtd land: impact: ${file} has no "## Impact" section — explore the codebase and add "## Impact" naming the units, public surfaces and surprises this change touches`,
205
+ )
206
+ }
207
+
208
+ /** Refuse a turn that changed `file` beyond its `## Impact` section. */
209
+ export const requireImpactOnly = (file: string): void => {
210
+ const change = changes().get(file)
211
+ if (change === undefined) return
212
+ if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return
213
+ refuse(
214
+ `gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`,
215
+ )
216
+ }
217
+
154
218
  /** The IDs a review's feedback lap must respect. */
155
219
  export interface StableIds {
156
220
  /** Listed when the build closed: each may stay, reworded or not, or go. */
@@ -197,6 +261,20 @@ const clear = async (stated: StatedCriteria): Promise<boolean> => {
197
261
  return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P)
198
262
  }
199
263
 
264
+ /** The fast path's impact-only turn: add `## Impact`, leave the criteria alone. */
265
+ const impact = async (): Promise<void> => {
266
+ await t.agentWithSkills("impact", t.designImpactPrompt(), {
267
+ label: "Mapping the change's impact",
268
+ file: REQUIREMENTS,
269
+ model: planner(),
270
+ system: t.designSystem(),
271
+ })
272
+ requireProgress(REQUIREMENTS)
273
+ requireCriteriaFrozen(REQUIREMENTS)
274
+ requireImpact(REQUIREMENTS)
275
+ requireImpactOnly(REQUIREMENTS)
276
+ }
277
+
200
278
  /**
201
279
  * The skip path: when the sketch stated clear, checkable criteria, write them
202
280
  * to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
@@ -211,6 +289,7 @@ export const skipDesign = (stated: StatedCriteria | undefined): Promise<boolean>
211
289
  label: "Recording the stated acceptance criteria",
212
290
  file: REQUIREMENTS,
213
291
  })
292
+ await impact()
214
293
  return true
215
294
  })
216
295
 
@@ -27,10 +27,6 @@ Feature: a trivial change takes the same few beats at either risk tier
27
27
  And step "design.gate.answer" changes nothing
28
28
  And step "implement.health.check" changes nothing
29
29
  And step "implement.closing" runs its script
30
- And judge step "implement.coverage" answers:
31
- """
32
- [{"id":"AC-1","answer":"yes","p":0.9}]
33
- """
34
30
  And step "build.quality.correctness.reviewing" changes nothing
35
31
 
36
32
  Scenario Outline: a one-line fix runs from idle to the review gate with the same beats at either tier
@@ -42,6 +38,10 @@ Feature: a trivial change takes the same few beats at either risk tier
42
38
 
43
39
  <tier> — one-line copy fix
44
40
 
41
+ ## Impact
42
+
43
+ - the greeting module
44
+
45
45
  ## Acceptance Criteria
46
46
 
47
47
  - AC-1: the change behaves as described.
@@ -78,8 +78,7 @@ Feature: a trivial change takes the same few beats at either risk tier
78
78
  gtd(check): seam.check → seam.judge
79
79
  gtd(judge): seam.judge → implement.applying
80
80
  gtd(check): implement.applying → implement.health.check
81
- gtd(check): implement.health.check → implement.coverage
82
- gtd(judge): implement.coverage → implement.closing
81
+ gtd(check): implement.health.check → implement.closing
83
82
  gtd(check): implement.closing → build.quality.correctness.reviewing
84
83
  gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
85
84
  """
@@ -99,7 +99,7 @@ describe("afterTail feedback lap", () => {
99
99
  "build.review.collecting": (_, r) => {
100
100
  r.write(
101
101
  ".gtd/REQUIREMENTS.md",
102
- "# Requirements\n\n- multiply\n\n## Acceptance Criteria\n\n- AC-1: done\n",
102
+ "# Requirements\n\n- multiply\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
103
103
  )
104
104
  r.write(".gtd/REVIEW.md", `${review("also export multiply")} - A: noted.\n`)
105
105
  },
@@ -115,7 +115,7 @@ describe("afterTail feedback lap", () => {
115
115
  "design.triage": (_: unknown, r: ReturnType<typeof testRepo>) =>
116
116
  r.write(
117
117
  ".gtd/REQUIREMENTS.md",
118
- "# Requirements\n\n- multiply, designed\n\n## Acceptance Criteria\n\n- AC-1: done\n",
118
+ "# Requirements\n\n- multiply, designed\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
119
119
  ),
120
120
  "design.gate.answer": () => undefined,
121
121
  }
@@ -214,7 +214,7 @@ describe("an in-flight 28.0.0 naive-pass process", () => {
214
214
  "design.triage": (_, r) =>
215
215
  r.write(
216
216
  ".gtd/REQUIREMENTS.md",
217
- "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
217
+ "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
218
218
  ),
219
219
  "design.gate.answer": () => undefined,
220
220
  "seam.naive": (_, r) => {
@@ -258,6 +258,7 @@ describe("an in-flight 28.0.0 naive-pass process", () => {
258
258
  })
259
259
 
260
260
  describe("an in-flight process from before the architecture path was removed", () => {
261
+ // No `## Impact`: no version before it wrote one, and replaying its triage must not refuse.
261
262
  const REQUIREMENTS =
262
263
  "# Requirements\n\n## Concerns\n\n### Subtract\n\n- `subtract`\n\n## Acceptance Criteria\n\n- AC-1: done\n"
263
264
 
@@ -321,7 +322,7 @@ describe("flagMode", () => {
321
322
  "design.triage": (_, r) =>
322
323
  r.write(
323
324
  ".gtd/REQUIREMENTS.md",
324
- "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
325
+ "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
325
326
  ),
326
327
  "design.gate.answer": () => undefined,
327
328
  })
@@ -342,7 +343,7 @@ describe("flagMode", () => {
342
343
  "design.triage": (_, r) =>
343
344
  r.write(
344
345
  ".gtd/REQUIREMENTS.md",
345
- "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
346
+ "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
346
347
  ),
347
348
  "design.gate.answer": () => undefined,
348
349
  "seam.naive": (_, r) => {
@@ -123,7 +123,6 @@ const buildSlice = async (sliceBase: string, build: SliceBuild): Promise<void> =
123
123
  naive: seam.land === true ? seam.reference : undefined,
124
124
  rebuild: seam.rebuild,
125
125
  slice: build.criteria,
126
- base: build.criteria === undefined ? undefined : sliceBase,
127
126
  more: build.more,
128
127
  })
129
128
  // Closing kept the criteria exactly when there is more to build, so this reads as it did.
@@ -179,7 +178,7 @@ const reviewedTail = (
179
178
  )
180
179
 
181
180
  // seam.naive / seam.judge never run on a feedback lap: its design → implement is built unjudged
182
- // and unrefactored, held to every criterion's coverage since the process start.
181
+ // and unrefactored.
183
182
  const featureFeedback = async (
184
183
  first: ReviewOutcome,
185
184
  lensBase: string,
@@ -37,6 +37,10 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
37
37
 
38
38
  - `subtract(a, b)` returns `a - b`.
39
39
 
40
+ ## Impact
41
+
42
+ - the greeting module
43
+
40
44
  ## Acceptance Criteria
41
45
 
42
46
  - AC-1: the change behaves as described.
@@ -61,10 +65,6 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
61
65
  And step "implement.applying" runs its script
62
66
  And step "implement.health.check" changes nothing
63
67
  And step "implement.closing" runs its script
64
- And judge step "implement.coverage" answers:
65
- """
66
- [{"id":"AC-1","answer":"yes","p":0.9}]
67
- """
68
68
  And step "build.quality.correctness.reviewing" changes nothing
69
69
  When the process is driven
70
70
  Then the process rests at "build.review.reviewing"
@@ -82,8 +82,7 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
82
82
  gtd(agent): seam.naive → seam.check
83
83
  gtd(check): seam.check → implement.applying
84
84
  gtd(check): implement.applying → implement.health.check
85
- gtd(check): implement.health.check → implement.coverage
86
- gtd(judge): implement.coverage → implement.closing
85
+ gtd(check): implement.health.check → implement.closing
87
86
  gtd(check): implement.closing → build.quality.correctness.reviewing
88
87
  gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
89
88
  """