@pmelab/gtd 52.0.0 → 53.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 (108) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +96 -131
  3. package/claude/hooks/drive.ts +0 -3
  4. package/claude/hooks/register.tsx +3 -68
  5. package/claude/hooks/ship.ts +85 -1595
  6. package/dist/chunks/{reserved-D0MKVFFa.mjs → reserved-CQ7amcKG.mjs} +87 -133
  7. package/dist/chunks/{review-ZzUQH1KZ.mjs → review-CNgN_V8T.mjs} +2 -2
  8. package/dist/chunks/{tail-I1-vVq0P.mjs → tail-JZ9PAOwp.mjs} +2 -14
  9. package/dist/chunks/{wire-BJuZWR3C.mjs → wire-C12dahrH.mjs} +2 -130
  10. package/dist/flows.mjs +1 -16
  11. package/dist/gtd.bundle.mjs +476 -982
  12. package/dist/testkit.mjs +58 -417
  13. package/dist/wire.mjs +2 -2
  14. package/dist/workflows/cleanup.mjs +2 -2
  15. package/dist/workflows/feature.mjs +159 -363
  16. package/dist/workflows/fix.mjs +2 -2
  17. package/dist/workflows/review.mjs +3 -3
  18. package/dist/workflows/rollout.mjs +1 -1
  19. package/dist/workflows.mjs +400 -675
  20. package/package.json +5 -4
  21. package/src/cli/Cli.test.ts +1 -86
  22. package/src/cli/Cli.ts +13 -133
  23. package/src/cli/Metrics.test.ts +21 -31
  24. package/src/cli/Metrics.ts +25 -36
  25. package/src/cli/Program.test.ts +1 -35
  26. package/src/cli/Program.ts +5 -64
  27. package/src/cli/ToolCommands.ts +0 -3
  28. package/src/edge/Edge.test.ts +20 -322
  29. package/src/edge/Edge.ts +10 -181
  30. package/src/edge/index.ts +0 -1
  31. package/src/flows/runtime.test.ts +0 -27
  32. package/src/flows/runtime.ts +1 -49
  33. package/src/install/Install.ts +15 -24
  34. package/src/replay/Replay.test.ts +0 -169
  35. package/src/replay/Replay.ts +4 -93
  36. package/src/replay/index.ts +0 -1
  37. package/src/testkit/Layers.ts +0 -1
  38. package/src/testkit/steps.ts +13 -0
  39. package/src/wire/BeatDocument.ts +2 -12
  40. package/src/wire/BeatStatus.ts +0 -3
  41. package/src/wire/WireSchema.test.ts +0 -18
  42. package/src/wire/index.ts +0 -2
  43. package/src/wire/types.ts +0 -7
  44. package/src/workflow/Workflow.ts +1 -9
  45. package/src/workflows/feature/acceptance-criteria.feature +159 -3
  46. package/src/workflows/feature/criteria.test.ts +23 -15
  47. package/src/workflows/feature/criteria.ts +72 -39
  48. package/src/workflows/feature/fast-path.feature +4 -0
  49. package/src/workflows/feature/feature.test.ts +6 -5
  50. package/src/workflows/feature/feature.ts +34 -118
  51. package/src/workflows/feature/flag-mode.feature +4 -0
  52. package/src/workflows/feature/hands-on-skip.feature +37 -65
  53. package/src/workflows/feature/index.ts +0 -1
  54. package/src/workflows/feature/lens-models.feature +4 -0
  55. package/src/workflows/feature/lens-review.feature +4 -0
  56. package/src/workflows/feature/naive-acceptance.feature +4 -0
  57. package/src/workflows/feature/one-step-land.feature +4 -4
  58. package/src/workflows/feature/planning.ts +2 -1
  59. package/src/workflows/feature/prototype-rounds.feature +5 -1
  60. package/src/workflows/feature/prototype.feature +11 -3
  61. package/src/workflows/feature/prototype.ts +18 -29
  62. package/src/workflows/feature/refactor-absorbs.feature +7 -8
  63. package/src/workflows/feature/refactor-first.feature +4 -5
  64. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  65. package/src/workflows/feature/refactor-rebuild.feature +4 -6
  66. package/src/workflows/feature/refactor-review.feature +31 -11
  67. package/src/workflows/feature/refactor.ts +30 -64
  68. package/src/workflows/feature/review-answers.feature +4 -0
  69. package/src/workflows/feature/review-gate.feature +13 -1
  70. package/src/workflows/feature/reviewer-walkthrough.feature +4 -0
  71. package/src/workflows/feature/risk-tier.feature +32 -0
  72. package/src/workflows/feature/seam-rejection.feature +8 -4
  73. package/src/workflows/feature/seam.feature +8 -0
  74. package/src/workflows/feature/seam.test.ts +11 -28
  75. package/src/workflows/feature/seam.ts +52 -165
  76. package/src/workflows/feature/severity-budget.feature +4 -0
  77. package/src/workflows/feature/severity-sorted-review.feature +4 -0
  78. package/src/workflows/feature/standards.feature +4 -0
  79. package/src/workflows/feature/tier-pin.feature +4 -0
  80. package/src/workflows/index.ts +0 -1
  81. package/src/workflows/shared/lenses.ts +0 -13
  82. package/src/workflows/shared/options.test.ts +2 -11
  83. package/src/workflows/shared/options.ts +4 -21
  84. package/src/workflows/shared/review.test.ts +3 -3
  85. package/src/workflows/shared/steps.ts +0 -2
  86. package/src/workflows/shared/text.test.ts +41 -71
  87. package/src/workflows/shared/text.ts +82 -212
  88. package/src/workflows/shared/vars.ts +2 -8
  89. package/src/workflows/shared/workflows.steps.ts +0 -13
  90. package/claude/hooks/context.ts +0 -66
  91. package/src/replay/Slices.test.ts +0 -73
  92. package/src/replay/Slices.ts +0 -45
  93. package/src/wire/ContextLimit.fixture.ts +0 -30
  94. package/src/wire/ContextLimit.test.ts +0 -96
  95. package/src/wire/ContextLimit.ts +0 -169
  96. package/src/wire/Slices.ts +0 -25
  97. package/src/wire/golden/1/slices.json +0 -11
  98. package/src/workflows/feature/context-limit.feature +0 -37
  99. package/src/workflows/feature/continuation-cap.feature +0 -65
  100. package/src/workflows/feature/continuation-rebuild.feature +0 -88
  101. package/src/workflows/feature/continuation-slices.feature +0 -64
  102. package/src/workflows/feature/continuation.feature +0 -205
  103. package/src/workflows/feature/slice-findings.feature +0 -252
  104. package/src/workflows/feature/slicing-cap.feature +0 -23
  105. package/src/workflows/feature/slicing.feature +0 -265
  106. package/src/workflows/feature/slicing.test.ts +0 -49
  107. package/src/workflows/feature/slicing.ts +0 -105
  108. package/src/workflows/shared/continuation.ts +0 -92
@@ -5,7 +5,6 @@ import type {
5
5
  RenderedDemandSource,
6
6
  StateMode,
7
7
  StateName,
8
- StepContextLimit,
9
8
  } from "./types.js"
10
9
 
11
10
  /** One pending change — `gtd next --json`'s `changes` entries. */
@@ -33,7 +32,6 @@ export interface BeatStatus {
33
32
  readonly initial: boolean
34
33
  readonly model: string | undefined
35
34
  readonly system: string | undefined
36
- readonly context: StepContextLimit | undefined
37
35
  readonly log: string
38
36
  readonly state: StateName
39
37
  readonly actor: Actor
@@ -76,7 +74,6 @@ export const statusOf = (input: {
76
74
  initial,
77
75
  model: rendered.model,
78
76
  system: rendered.system !== undefined && rendered.system !== "" ? rendered.system : undefined,
79
- context: rendered.context,
80
77
  log,
81
78
  state: rendered.state,
82
79
  actor: rendered.actor,
@@ -9,8 +9,6 @@ import {
9
9
  landFields,
10
10
  renderBeatJson,
11
11
  renderLandJson,
12
- renderSlicesJson,
13
- slicesDocument,
14
12
  statusOf,
15
13
  WIRE_SCHEMA,
16
14
  type BeatKind,
@@ -118,9 +116,6 @@ const documents: Readonly<Record<string, string>> = {
118
116
  }),
119
117
  ),
120
118
  "judge.json": JUDGE,
121
- "slices.json": renderSlicesJson(
122
- slicesDocument([{ name: "build", base: "a".repeat(40), tip: "b".repeat(40), closed: true }]),
123
- ),
124
119
  }
125
120
 
126
121
  describe(`wire schema ${WIRE_SCHEMA}`, () => {
@@ -144,19 +139,6 @@ describe(`wire schema ${WIRE_SCHEMA}`, () => {
144
139
  }
145
140
  })
146
141
 
147
- describe("slicesDocument", () => {
148
- it("stamps WIRE_SCHEMA and drops any field beyond the four", () => {
149
- const doc = slicesDocument([
150
- { name: "n", base: "b", tip: "t", closed: false, extra: 1 } as never,
151
- ])
152
- expect(doc).toEqual({
153
- schema: WIRE_SCHEMA,
154
- slices: [{ name: "n", base: "b", tip: "t", closed: false }],
155
- })
156
- expect(Object.keys(doc.slices[0]!)).toEqual(["name", "base", "tip", "closed"])
157
- })
158
- })
159
-
160
142
  describe("the shape check", () => {
161
143
  const land = JSON.parse(documents["land.json"]!) as Record<string, unknown>
162
144
  const { subject, ...rest } = land
package/src/wire/index.ts CHANGED
@@ -10,5 +10,3 @@ export * from "./Judge.js"
10
10
  export { selectPath } from "./Select.js"
11
11
  export { resolveSession } from "./Sessions.js"
12
12
  export { globMatches } from "./Glob.js"
13
- export * from "./Slices.js"
14
- export * from "./ContextLimit.js"
package/src/wire/types.ts CHANGED
@@ -38,12 +38,6 @@ export interface ApprovalEntry {
38
38
  readonly commit: string
39
39
  }
40
40
 
41
- /** The context-limit shares a step sets; a driver fills an absent one from its defaults. */
42
- export interface StepContextLimit {
43
- readonly soft?: number
44
- readonly hard?: number
45
- }
46
-
47
41
  export interface RenderedDemandSource {
48
42
  readonly state: StateName
49
43
  readonly actor: Actor
@@ -57,5 +51,4 @@ export interface RenderedDemandSource {
57
51
  readonly judge?: string
58
52
  readonly skills?: readonly string[]
59
53
  readonly access?: StepAccess
60
- readonly context?: StepContextLimit
61
54
  }
@@ -1,12 +1,5 @@
1
1
  import type { WorkflowBase, Flow, ScopeAccess, Summary } from "../flows/index.js"
2
- import type {
3
- Actor,
4
- ContentKind,
5
- StateMode,
6
- StateName,
7
- StepAccess,
8
- StepContextLimit,
9
- } from "../wire/index.js"
2
+ import type { Actor, ContentKind, StateMode, StateName, StepAccess } from "../wire/index.js"
10
3
 
11
4
  export type { Actor, ContentKind, StateMode, StateName }
12
5
 
@@ -51,7 +44,6 @@ export interface StepDef {
51
44
  readonly approve?: boolean
52
45
  readonly skills?: readonly string[]
53
46
  readonly access?: StepAccess
54
- readonly context?: StepContextLimit
55
47
  }
56
48
 
57
49
  /**
@@ -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\"."
@@ -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, handsOnIds, 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,
@@ -132,17 +139,9 @@ describe("statedCriteria", () => {
132
139
  describe("criterionIds", () => {
133
140
  const requirements = "## Acceptance Criteria\n\n- AC-1: a\n- AC-2: b\n- AC-4: added\n- unnamed\n"
134
141
 
135
- it("lists every ID'd criterion of an unsliced change", () => {
142
+ it("lists every ID'd criterion", () => {
136
143
  expect(criterionIds(requirements)).toEqual(["AC-1", "AC-2", "AC-4"])
137
144
  })
138
-
139
- it("keeps `only` to the IDs the file still lists", () => {
140
- expect(criterionIds(requirements, { only: ["AC-1", "AC-3"] })).toEqual(["AC-1"])
141
- })
142
-
143
- it("gives `except` every other ID, one added since included", () => {
144
- expect(criterionIds(requirements, { except: ["AC-1", "AC-3"] })).toEqual(["AC-2", "AC-4"])
145
- })
146
145
  })
147
146
 
148
147
  describe("handsOnIds", () => {
@@ -152,11 +151,6 @@ describe("handsOnIds", () => {
152
151
  it("leaves out every criterion tagged (no hands-on)", () => {
153
152
  expect(handsOnIds(requirements)).toEqual(["AC-1", "AC-3"])
154
153
  })
155
-
156
- it("keeps to the slice", () => {
157
- expect(handsOnIds(requirements, { only: ["AC-2", "AC-3"] })).toEqual(["AC-3"])
158
- expect(handsOnIds(requirements, { only: ["AC-2", "AC-4"] })).toEqual([])
159
- })
160
154
  })
161
155
 
162
156
  describe("unstableIds", () => {
@@ -177,3 +171,17 @@ describe("unstableIds", () => {
177
171
  expect(unstableIds(["AC-0", "AC-2"], ids)).toMatch(/^numbers AC-0, AC-2 at or below AC-3/)
178
172
  })
179
173
  })
174
+
175
+ describe("hasImpact", () => {
176
+ it.each([
177
+ ["a level-2 heading", "# R\n\n## Impact\n\nx\n", true],
178
+ ["a mixed-case heading", "## impact\n", true],
179
+ ["a level-3 heading", "### Impact\n", false],
180
+ ["a level-1 heading", "# Impact\n", false],
181
+ ["a longer heading", "## Impactful\n", false],
182
+ ["a fenced heading", "```md\n## Impact\n```\n", false],
183
+ ["no heading", "# R\n\nImpact\n", false],
184
+ ])("%s", (_name, text, expected) => {
185
+ expect(hasImpact(text)).toBe(expected)
186
+ })
187
+ })
@@ -1,15 +1,17 @@
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 {
@@ -77,37 +79,16 @@ export const criteriaOf = (text: string): readonly Criterion[] | undefined =>
77
79
  return id === null ? { id: "", text: item } : criterion(id)
78
80
  })
79
81
 
80
- /**
81
- * Which criteria one slice builds, resolved against the file whenever read:
82
- * `only` drops an ID a steer deleted, `except` (the last slice) takes one a
83
- * steer added.
84
- */
85
- export interface CriteriaSlice {
86
- readonly only?: readonly string[]
87
- readonly except?: readonly string[]
88
- }
89
-
90
- /** The ID'd criteria of `text`, only `slice`'s when given. */
91
- export const criterionIds = (text: string, slice?: CriteriaSlice): string[] =>
92
- (criteriaOf(text) ?? [])
93
- .map((c) => c.id)
94
- .filter(
95
- (id) =>
96
- id !== "" &&
97
- (slice?.only === undefined || slice.only.includes(id)) &&
98
- !(slice?.except ?? []).includes(id),
99
- )
82
+ /** The ID'd criteria of `text`. */
83
+ export const criterionIds = (text: string): string[] =>
84
+ (criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "")
100
85
 
101
- /** The ID'd criteria of `text` (`slice`'s when given) a person can try by hand: none tagged `(no hands-on)`. */
102
- export const handsOnIds = (text: string, slice?: CriteriaSlice): string[] => {
86
+ /** The ID'd criteria of `text` a person can try by hand: none tagged `(no hands-on)`. */
87
+ export const handsOnIds = (text: string): string[] => {
103
88
  const tagged = (criteriaOf(text) ?? []).filter((c) => c.noHandsOn).map((c) => c.id)
104
- return criterionIds(text, slice).filter((id) => !tagged.includes(id))
89
+ return criterionIds(text).filter((id) => !tagged.includes(id))
105
90
  }
106
91
 
107
- /** `slice`'s IDs in `.gtd/REQUIREMENTS.md` as it is now; `undefined` for an unsliced change. */
108
- export const sliceIds = (slice: CriteriaSlice | undefined): string[] | undefined =>
109
- slice === undefined ? undefined : criterionIds(read(REQUIREMENTS) ?? "", slice)
110
-
111
92
  /** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
112
93
  const numbered = (items: readonly string[]): Criterion[] => {
113
94
  const ids = items.map((item) => ID.exec(item))
@@ -165,6 +146,54 @@ export const requireCriteria = (file: string): void => {
165
146
  )
166
147
  }
167
148
 
149
+ const isImpactHeading = (h: RegExpExecArray | null): boolean =>
150
+ h !== null && h[1]!.length === 2 && /^impact$/i.test(h[2]!.trim())
151
+
152
+ /** Whether `text` has an unfenced, level-2 `## Impact` heading. */
153
+ export const hasImpact = (text: string): boolean =>
154
+ markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)))
155
+
156
+ /** `text` without its `## Impact` section, whitespace collapsed. */
157
+ const withoutImpact = (text: string): string => {
158
+ let inImpact = false
159
+ const kept = markFenced(text).filter((l) => {
160
+ const h = l.fenced ? null : HEADING.exec(l.line)
161
+ if (h !== null && h[1]!.length <= 2) inImpact = isImpactHeading(h)
162
+ return !inImpact
163
+ })
164
+ return kept
165
+ .map((l) => l.line)
166
+ .join(" ")
167
+ .replace(/\s+/g, " ")
168
+ .trim()
169
+ }
170
+
171
+ /**
172
+ * Refuse a turn that leaves `file` without a `## Impact` section. Only the
173
+ * pending turn (`head()` is `""`): a triage lap recorded before Impact existed
174
+ * replays without one, and refusing there would fail every in-flight process.
175
+ * A human's edit at a later gate is not checked — the human owns that file then.
176
+ */
177
+ export const requireImpact = (file: string): void => {
178
+ if (head() !== "") return
179
+ const text = read(file)
180
+ if (text === undefined) return
181
+ if (hasImpact(text)) return
182
+ refuse(
183
+ `gtd land: impact: ${file} has no "## Impact" section — explore the codebase and add "## Impact" naming the units, public surfaces and surprises this change touches`,
184
+ )
185
+ }
186
+
187
+ /** Refuse a turn that changed `file` beyond its `## Impact` section. */
188
+ export const requireImpactOnly = (file: string): void => {
189
+ const change = changes().get(file)
190
+ if (change === undefined) return
191
+ if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return
192
+ refuse(
193
+ `gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`,
194
+ )
195
+ }
196
+
168
197
  /** The IDs a review's feedback lap must respect. */
169
198
  export interface StableIds {
170
199
  /** Listed when the build closed: each may stay, reworded or not, or go. */
@@ -211,6 +240,20 @@ const clear = async (stated: StatedCriteria): Promise<boolean> => {
211
240
  return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P)
212
241
  }
213
242
 
243
+ /** The fast path's impact-only turn: add `## Impact`, leave the criteria alone. */
244
+ const impact = async (): Promise<void> => {
245
+ await t.agentWithSkills("impact", t.designImpactPrompt(), {
246
+ label: "Mapping the change's impact",
247
+ file: REQUIREMENTS,
248
+ model: planner(),
249
+ system: t.designSystem(),
250
+ })
251
+ requireProgress(REQUIREMENTS)
252
+ requireCriteriaFrozen(REQUIREMENTS)
253
+ requireImpact(REQUIREMENTS)
254
+ requireImpactOnly(REQUIREMENTS)
255
+ }
256
+
214
257
  /**
215
258
  * The skip path: when the sketch stated clear, checkable criteria, write them
216
259
  * to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
@@ -225,16 +268,6 @@ export const skipDesign = (stated: StatedCriteria | undefined): Promise<boolean>
225
268
  label: "Recording the stated acceptance criteria",
226
269
  file: REQUIREMENTS,
227
270
  })
271
+ await impact()
228
272
  return true
229
273
  })
230
-
231
- /** After a gate that invites narrowing the requirements: an edit must leave valid criteria. */
232
- export const requireCriteriaIfEdited = (): void => {
233
- const change = changes().get(REQUIREMENTS)
234
- if (change === undefined) return
235
- if (change.status === "deleted")
236
- refuse(
237
- `gtd land: criteria-missing: ${REQUIREMENTS} must stay — narrow its criteria, never delete it`,
238
- )
239
- requireCriteria(REQUIREMENTS)
240
- }
@@ -38,6 +38,10 @@ Feature: a trivial change takes the same few beats at either risk tier
38
38
 
39
39
  <tier> — one-line copy fix
40
40
 
41
+ ## Impact
42
+
43
+ - the greeting module
44
+
41
45
  ## Acceptance Criteria
42
46
 
43
47
  - AC-1: the change behaves as described.
@@ -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) => {