@pmelab/gtd 35.0.0 → 37.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 (69) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +45 -29
  3. package/claude/hooks/context.ts +66 -0
  4. package/claude/hooks/drive.ts +3 -0
  5. package/claude/hooks/register.tsx +69 -6
  6. package/dist/chunks/{reserved-C8Fm3s72.mjs → reserved-zAfoJtR7.mjs} +62 -24
  7. package/dist/chunks/{tail-oaYewBN_.mjs → tail-BRC4DGdV.mjs} +1 -1
  8. package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
  9. package/dist/flows.mjs +15 -10
  10. package/dist/gtd.bundle.mjs +511 -64
  11. package/dist/testkit.mjs +158 -26
  12. package/dist/wire.mjs +2 -2
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +138 -38
  15. package/dist/workflows/fix.mjs +2 -2
  16. package/dist/workflows/review.mjs +2 -2
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +208 -66
  19. package/package.json +1 -1
  20. package/skills/authoring/SKILL.md +3 -3
  21. package/src/cli/Cli.test.ts +10 -3
  22. package/src/cli/Cli.ts +76 -1
  23. package/src/cli/Program.test.ts +1 -0
  24. package/src/cli/Program.ts +28 -0
  25. package/src/cli/ToolCommands.ts +3 -0
  26. package/src/edge/Edge.ts +11 -0
  27. package/src/flows/runtime.ts +11 -0
  28. package/src/flows/scripts.ts +31 -9
  29. package/src/replay/Replay.ts +51 -2
  30. package/src/testkit/FlowScripts.test.ts +58 -0
  31. package/src/testkit/FlowScripts.ts +61 -24
  32. package/src/testkit/Layers.ts +1 -0
  33. package/src/wire/BeatDocument.ts +12 -2
  34. package/src/wire/BeatStatus.ts +3 -0
  35. package/src/wire/ContextLimit.fixture.ts +30 -0
  36. package/src/wire/ContextLimit.test.ts +96 -0
  37. package/src/wire/ContextLimit.ts +169 -0
  38. package/src/wire/index.ts +1 -0
  39. package/src/wire/types.ts +7 -0
  40. package/src/workflow/Workflow.ts +9 -1
  41. package/src/workflows/feature/acceptance-criteria.feature +5 -1
  42. package/src/workflows/feature/context-limit.feature +37 -0
  43. package/src/workflows/feature/coverage.feature +6 -3
  44. package/src/workflows/feature/coverage.ts +8 -2
  45. package/src/workflows/feature/fast-path.feature +11 -19
  46. package/src/workflows/feature/feature.test.ts +24 -4
  47. package/src/workflows/feature/feature.ts +14 -10
  48. package/src/workflows/feature/flag-mode.feature +17 -9
  49. package/src/workflows/feature/lens-models.feature +7 -1
  50. package/src/workflows/feature/naive-acceptance.feature +2 -2
  51. package/src/workflows/feature/one-step-land.feature +10 -12
  52. package/src/workflows/feature/quality-unresolved.feature +7 -1
  53. package/src/workflows/feature/refactor-absorbs.feature +12 -5
  54. package/src/workflows/feature/refactor-first.feature +11 -2
  55. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  56. package/src/workflows/feature/refactor-rebuild.feature +269 -0
  57. package/src/workflows/feature/refactor-review.feature +45 -1
  58. package/src/workflows/feature/refactor.ts +9 -3
  59. package/src/workflows/feature/risk-tier.feature +12 -4
  60. package/src/workflows/feature/seam-rejection.feature +4 -9
  61. package/src/workflows/feature/seam.feature +29 -26
  62. package/src/workflows/feature/seam.test.ts +117 -6
  63. package/src/workflows/feature/seam.ts +158 -39
  64. package/src/workflows/review/review-door.feature +12 -4
  65. package/src/workflows/shared/options.test.ts +11 -2
  66. package/src/workflows/shared/options.ts +18 -4
  67. package/src/workflows/shared/steps.ts +1 -0
  68. package/src/workflows/shared/text.test.ts +8 -0
  69. package/src/workflows/shared/text.ts +91 -22
@@ -66,6 +66,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
66
66
  """
67
67
  [{"id":"shape","answer":"refactor-first","p":0.9}]
68
68
  """
69
+ And step "seam.note" writes ".gtd/REFACTOR-NOTE.md" with:
70
+ """
71
+ Seam: the existing code the strays touch.
72
+ """
69
73
  And step "refactor.deriving" runs its script
70
74
  And step "refactor.building" writes "src/calc.ts" with:
71
75
  """
@@ -155,6 +159,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
155
159
  """
156
160
  [{"id":"shape","answer":"refactor-first","p":0.9}]
157
161
  """
162
+ And step "seam.note" writes ".gtd/REFACTOR-NOTE.md" with:
163
+ """
164
+ Seam: the existing code the strays touch.
165
+ """
158
166
  And step "refactor.deriving" runs its script
159
167
  And step "refactor.building" writes "src/calc.ts" with:
160
168
  """
@@ -243,6 +251,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
243
251
  """
244
252
  [{"id":"shape","answer":"refactor-first","p":0.9}]
245
253
  """
254
+ And step "seam.note" writes ".gtd/REFACTOR-NOTE.md" with:
255
+ """
256
+ Seam: the existing code the strays touch.
257
+ """
246
258
  And step "refactor.deriving" runs its script
247
259
  And step "refactor.building" writes "src/calc.ts" with:
248
260
  """
@@ -345,6 +357,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
345
357
  export const add = (a: number, b: number) => op(a, b, 1)
346
358
  export const subtract = (a: number, b: number) => op(a, b, -1)
347
359
  """
360
+ And step "implement.building" writes "src/acceptance.test.ts" with:
361
+ """
362
+ it("[AC-1] holds", () => {})
363
+ """
348
364
  And step "implement.health.check" changes nothing
349
365
  And step "implement.closing" runs its script
350
366
  And judge step "implement.coverage" answers:
@@ -638,6 +654,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
638
654
  export const add = (a: number, b: number) => op(a, b, 1)
639
655
  export const subtract = (a: number, b: number) => op(a, b, -1)
640
656
  """
657
+ And step "implement.building" writes "src/acceptance.test.ts" with:
658
+ """
659
+ it("[AC-1] holds", () => {})
660
+ """
641
661
  And step "implement.health.check" changes nothing
642
662
  And step "implement.closing" runs its script
643
663
  And judge step "implement.coverage" answers:
@@ -717,6 +737,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
717
737
  export const add = (a: number, b: number) => op(a, b, 1)
718
738
  export const subtract = (a: number, b: number) => op(a, b, -1)
719
739
  """
740
+ And step "implement.building" writes "src/acceptance.test.ts" with:
741
+ """
742
+ it("[AC-1] holds", () => {})
743
+ """
720
744
  And step "implement.health.check" changes nothing
721
745
  And step "implement.closing" runs its script
722
746
  And judge step "implement.coverage" answers:
@@ -983,6 +1007,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
983
1007
  export const add = (a: number, b: number) => op(a, b, 1)
984
1008
  export const subtract = (a: number, b: number) => op(a, b, -1)
985
1009
  """
1010
+ And step "implement.building" writes "src/acceptance.test.ts" with:
1011
+ """
1012
+ it("[AC-1] holds", () => {})
1013
+ """
986
1014
  And step "implement.health.check" changes nothing
987
1015
  And step "implement.closing" runs its script
988
1016
  And judge step "implement.coverage" answers:
@@ -1066,6 +1094,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1066
1094
  export const add = (a: number, b: number) => op(a, b, 1)
1067
1095
  export const subtract = (a: number, b: number) => op(a, b, -1)
1068
1096
  """
1097
+ And step "implement.building" writes "src/acceptance.test.ts" with:
1098
+ """
1099
+ it("[AC-1] holds", () => {})
1100
+ """
1069
1101
  And step "implement.health.check" changes nothing
1070
1102
  And step "implement.closing" runs its script
1071
1103
  And judge step "implement.coverage" answers:
@@ -1212,6 +1244,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1212
1244
  """
1213
1245
  [{"id":"shape","answer":"refactor-first","p":0.9}]
1214
1246
  """
1247
+ And step "seam.note" writes ".gtd/REFACTOR-NOTE.md" with:
1248
+ """
1249
+ Seam: the existing code the strays touch.
1250
+ """
1215
1251
  And step "refactor.deriving" runs its script
1216
1252
  And step "refactor.building" writes "src/calc.ts" with:
1217
1253
  """
@@ -1236,7 +1272,7 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1236
1272
  Scenario: each slice's quality lap reviews from that slice's base
1237
1273
  When the process is driven
1238
1274
  Then the process rests at "refactor.build.quality.correctness.reviewing"
1239
- And the resting step's content contains the hash of the commit "gtd(judge): seam.judge → refactor.deriving"
1275
+ And the resting step's content contains the hash of the commit "gtd(agent): seam.note → refactor.deriving"
1240
1276
  Given step "refactor.build.quality.correctness.reviewing" changes nothing
1241
1277
  And step "implement.building" writes "src/calc.ts" with:
1242
1278
  """
@@ -1244,6 +1280,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1244
1280
  export const add = (a: number, b: number) => op(a, b, 1)
1245
1281
  export const subtract = (a: number, b: number) => op(a, b, -1)
1246
1282
  """
1283
+ And step "implement.building" writes "src/acceptance.test.ts" with:
1284
+ """
1285
+ it("[AC-1] holds", () => {})
1286
+ """
1247
1287
  And step "implement.health.check" changes nothing
1248
1288
  And step "implement.closing" runs its script
1249
1289
  And judge step "implement.coverage" answers:
@@ -1318,6 +1358,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1318
1358
  """
1319
1359
  [{"id":"shape","answer":"refactor-first","p":0.9}]
1320
1360
  """
1361
+ And step "seam.note" writes ".gtd/REFACTOR-NOTE.md" with:
1362
+ """
1363
+ Seam: the existing code the strays touch.
1364
+ """
1321
1365
 
1322
1366
  Scenario: a test-file stray is dropped from the derived refactor package
1323
1367
  Given step "seam.check" changes nothing
@@ -27,6 +27,8 @@ export const REFACTOR_PACKAGE = REFACTOR
27
27
  export interface RefactorOptions {
28
28
  /** Blank counts as absent: `kept()` reports `""` while the keeping turn is pending. */
29
29
  readonly reference?: string | undefined
30
+ /** The refactor note `seam.note` wrote; rides the package as `## Refactor note`. */
31
+ readonly note?: string | undefined
30
32
  }
31
33
 
32
34
  const isCode = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
@@ -203,7 +205,7 @@ const packageLines = (
203
205
  "",
204
206
  ]
205
207
 
206
- // Nested under `## Review feedback`, so every heading of the collected text drops a level.
208
+ // Nested under a `##` section (review feedback, the refactor note), so every heading drops a level.
207
209
  const demoteHeadings = (text: string): string[] => {
208
210
  const marked = markFenced(text.trimEnd())
209
211
  const lines = marked.map((m) => m.line)
@@ -230,6 +232,7 @@ interface Plan {
230
232
  readonly pattern: string
231
233
  readonly strays: readonly string[]
232
234
  readonly reference: string | undefined
235
+ readonly note: string | undefined
233
236
  }
234
237
 
235
238
  /** Undefined for every shape that is not a readable `refactor-first`; throws on a bad flag pattern. */
@@ -245,7 +248,8 @@ const plan = (shape: DiffShapeResult, options: RefactorOptions): Plan | undefine
245
248
  `gtd: refactorFlagPattern (${pattern}) produces a zero-length match in:\n${listPaths(zeroLength)}`,
246
249
  )
247
250
  const reference = options.reference?.trim() ? options.reference : undefined
248
- return { pattern, strays, reference }
251
+ const note = options.note?.trim() ? options.note : undefined
252
+ return { pattern, strays, reference, note }
249
253
  }
250
254
 
251
255
  // The in-memory `strays`, never the package file: a hand edit of its list changes nothing.
@@ -260,11 +264,12 @@ const guardOf =
260
264
 
261
265
  /** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
262
266
  const refactorLap = async (p: Plan, guard: Guard, feedback?: string): Promise<void> => {
263
- const { pattern, strays, reference } = p
267
+ const { pattern, strays, reference, note } = p
264
268
  await run(
265
269
  "deriving",
266
270
  deriveScript([
267
271
  ...packageLines(strays, pattern, reference),
272
+ ...(note === undefined ? [] : ["## Refactor note", "", ...demoteHeadings(note), ""]),
268
273
  ...(feedback === undefined
269
274
  ? []
270
275
  : ["## Review feedback", "", ...demoteHeadings(feedback), ""]),
@@ -279,6 +284,7 @@ const refactorLap = async (p: Plan, guard: Guard, feedback?: string): Promise<vo
279
284
  pattern,
280
285
  reference,
281
286
  feedback: feedback !== undefined,
287
+ note: note !== undefined,
282
288
  }),
283
289
  { label: "Refactoring", model: coder(), system: t.builderSystem() },
284
290
  )
@@ -133,7 +133,7 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
133
133
  | low |
134
134
  | high |
135
135
 
136
- Scenario: the tier comes from the requirements the final design gate lap accepted
136
+ Scenario: the tier a rebuilt pass is told comes from the requirements the final design gate lap accepted
137
137
  Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
138
138
  """
139
139
  # Requirements
@@ -146,7 +146,11 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
146
146
 
147
147
  - AC-1: the change behaves as described.
148
148
  """
149
- And step "seam.check" changes nothing
149
+ And step "seam.check" writes "src/acceptance.test.ts" with:
150
+ """
151
+ it("[AC-1] holds elsewhere", () => {})
152
+ """
153
+ And step "implement.applying" runs its script
150
154
  And step "seam.naive" writes "src/acceptance.test.ts" with:
151
155
  """
152
156
  it("[AC-1] holds", () => {})
@@ -245,7 +249,7 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
245
249
  And the resting step's content does not contain "## Production Signals"
246
250
 
247
251
 
248
- Scenario: the flag-off twin keeps today's flag demand
252
+ Scenario: the flag-off twin keeps today's flag demand on a rebuilt pass
249
253
  Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
250
254
  """
251
255
  # Requirements
@@ -259,7 +263,11 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
259
263
  - AC-1: the change behaves as described.
260
264
  """
261
265
  And step "design.gate.answer" changes nothing
262
- And step "seam.check" changes nothing
266
+ And step "seam.check" writes "src/acceptance.test.ts" with:
267
+ """
268
+ it("[AC-1] holds elsewhere", () => {})
269
+ """
270
+ And step "implement.applying" runs its script
263
271
  And step "seam.naive" writes "src/acceptance.test.ts" with:
264
272
  """
265
273
  it("[AC-1] holds", () => {})
@@ -97,12 +97,7 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
97
97
  """
98
98
  [{"id":"shape","answer":"land","p":0.9}]
99
99
  """
100
- And step "implement.building" writes "src/calc.ts" with:
101
- """
102
- export const add = (a: number, b: number) => a + b
103
- export const subtract = (a: number, b: number) => a - b
104
- export const SUBTRACT = true
105
- """
100
+ And step "implement.applying" runs its script
106
101
  When gtd lands
107
102
  And the process is driven
108
103
  Then the process rests at "idle"
@@ -125,8 +120,8 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
125
120
  gtd(human): seam.steer → seam.naive
126
121
  gtd(agent): seam.naive → seam.check
127
122
  gtd(check): seam.check → seam.judge
128
- gtd(judge): seam.judge → implement.building
129
- gtd(agent): implement.building → implement.health.check
123
+ gtd(judge): seam.judge → implement.applying
124
+ gtd(check): implement.applying → implement.health.check
130
125
  gtd(check): implement.health.check → implement.coverage
131
126
  gtd(judge): implement.coverage → implement.closing
132
127
  gtd(check): implement.closing → idle
@@ -135,7 +130,7 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
135
130
  And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
136
131
  And the commit "gtd(judge): seam.judge → seam.naive" has a body containing "\"answer\":\"reject\""
137
132
  And the commit "gtd(judge): seam.judge → seam.steer" has a body containing "\"answer\":\"reject\""
138
- And the commit "gtd(judge): seam.judge → implement.building" has a body containing "\"answer\":\"land\""
133
+ And the commit "gtd(judge): seam.judge → implement.applying" has a body containing "\"answer\":\"land\""
139
134
  And no commit body contains "Gtd-Approved:"
140
135
  And the output lines of gtd "slices" start with:
141
136
  """
@@ -2,8 +2,8 @@
2
2
  Feature: the naive pass and seam judge decide how a feature is built
3
3
 
4
4
  After design a naive pass is set aside under a kept ref, the judge reads its
5
- diff, and the process either implements straight away, refactors the seam
6
- first behind a human approval, or retries the naive pass.
5
+ diff, and the process either applies the pass as the implementation,
6
+ refactors the seam first behind a human approval, or retries the naive pass.
7
7
 
8
8
  Background:
9
9
  Given the shipped workflows, as gtd install scaffolds them
@@ -50,7 +50,7 @@ Feature: the naive pass and seam judge decide how a feature is built
50
50
  """
51
51
  And step "design.gate.answer" changes nothing
52
52
 
53
- Scenario: a land judgment goes straight to the implementation with no seam gate or refactor
53
+ Scenario: a land judgment applies the naive pass with no seam gate, refactor or build turn
54
54
  Given step "seam.check" changes nothing
55
55
  And step "seam.naive" writes "src/acceptance.test.ts" with:
56
56
  """
@@ -66,7 +66,7 @@ Feature: the naive pass and seam judge decide how a feature is built
66
66
  [{"id":"shape","answer":"land","p":0.9}]
67
67
  """
68
68
  When the process is driven
69
- Then the process rests at "implement.building"
69
+ Then the process rests at "implement.applying"
70
70
  And the last commit body contains "Gtd-Judge:"
71
71
  And the commit subjects end with:
72
72
  """
@@ -79,27 +79,34 @@ Feature: the naive pass and seam judge decide how a feature is built
79
79
  gtd(human): design.gate.answer → seam.naive
80
80
  gtd(agent): seam.naive → seam.check
81
81
  gtd(check): seam.check → seam.judge
82
- gtd(judge): seam.judge → implement.building
82
+ gtd(judge): seam.judge → implement.applying
83
83
  """
84
84
 
85
- Scenario: config skills for the implement scope reach the implement step
86
- Given step "seam.check" changes nothing
87
- And step "seam.naive" writes "src/acceptance.test.ts" with:
85
+ Scenario: a naive pass that no longer applies is built fresh, with the implement scope's skills
86
+ Given step "seam.naive" writes "src/acceptance.test.ts" with:
88
87
  """
89
88
  it("[AC-1] holds", () => {})
90
89
  """
91
- And step "seam.naive" writes "src/calc.ts" with:
90
+ And step "seam.naive" writes "src/subtract.ts" with:
92
91
  """
93
- export const add = (a: number, b: number) => a + b
94
92
  export const subtract = (a: number, b: number) => a - b
95
93
  """
96
- And judge step "seam.judge" answers:
94
+ And step "seam.check" writes "src/subtract.ts" with:
97
95
  """
98
- [{"id":"shape","answer":"land","p":0.9}]
96
+ export const subtract = (a: number, b: number) => b - a
99
97
  """
98
+ And step "implement.applying" runs its script
100
99
  When the process is driven
101
100
  Then the process rests at "implement.building"
102
101
  And the resting step's content contains "my-tdd, my-runbook"
102
+ And the resting step's content contains "git show"
103
+ And ".gtd/FEEDBACK.md" contains "could not apply the naive pass"
104
+ And "src/subtract.ts" contains "b - a"
105
+ And the commit subjects end with:
106
+ """
107
+ gtd(check): seam.check → implement.applying
108
+ gtd(check): implement.applying → implement.building
109
+ """
103
110
 
104
111
  Scenario: the naive pass lands as a marker commit and its turn stays off the branch
105
112
  Given step "seam.check" changes nothing
@@ -122,7 +129,7 @@ Feature: the naive pass and seam judge decide how a feature is built
122
129
  And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
123
130
  And "src/calc.ts" does not contain "subtract"
124
131
 
125
- Scenario: the implementation is one build turn, a green check, then the requirements are closed out
132
+ Scenario: the implementation is the applied naive pass, a green check, then the requirements are closed out
126
133
  Given step "seam.check" changes nothing
127
134
  And step "seam.naive" writes "src/acceptance.test.ts" with:
128
135
  """
@@ -137,12 +144,7 @@ Feature: the naive pass and seam judge decide how a feature is built
137
144
  """
138
145
  [{"id":"shape","answer":"land","p":0.9}]
139
146
  """
140
- And step "implement.building" writes "src/calc.ts" with:
141
- """
142
- export const add = (a: number, b: number) => a + b
143
- export const subtract = (a: number, b: number) => a - b
144
- export const SUBTRACT_ENABLED = process.env.GTD_FLAG_SUBTRACT === "1"
145
- """
147
+ And step "implement.applying" runs its script
146
148
  And step "implement.health.check" changes nothing
147
149
  And step "implement.closing" runs its script
148
150
  And judge step "implement.coverage" answers:
@@ -151,10 +153,11 @@ Feature: the naive pass and seam judge decide how a feature is built
151
153
  """
152
154
  When the process is driven
153
155
  Then the process rests at "idle"
156
+ And "src/calc.ts" contains "subtract"
154
157
  And the commit subjects end with:
155
158
  """
156
- gtd(judge): seam.judge → implement.building
157
- gtd(agent): implement.building → implement.health.check
159
+ gtd(judge): seam.judge → implement.applying
160
+ gtd(check): implement.applying → implement.health.check
158
161
  gtd(check): implement.health.check → implement.coverage
159
162
  gtd(judge): implement.coverage → implement.closing
160
163
  gtd(check): implement.closing → idle
@@ -177,12 +180,12 @@ Feature: the naive pass and seam judge decide how a feature is built
177
180
  it("adds", () => expect(2 + 2).toBe(4))
178
181
  """
179
182
  When the process is driven
180
- Then the process rests at "implement.building"
183
+ Then the process rests at "implement.applying"
181
184
  And the commit subjects end with:
182
185
  """
183
186
  gtd(human): design.gate.answer → seam.naive
184
187
  gtd(agent): seam.naive → seam.check
185
- gtd(check): seam.check → implement.building
188
+ gtd(check): seam.check → implement.applying
186
189
  """
187
190
 
188
191
  Scenario: a naive pass adding a glob-named file does not hide a modified sibling
@@ -258,7 +261,7 @@ Feature: the naive pass and seam judge decide how a feature is built
258
261
  gtd(judge): seam.judge → seam.steer
259
262
  """
260
263
 
261
- Scenario: a clean landing at the steer gate overrules, with no seam gate and no refactor
264
+ Scenario: a clean landing at the steer gate overrules and applies the pass, with no seam gate and no refactor
262
265
  Given step "seam.check" changes nothing
263
266
  And step "seam.naive" writes "src/acceptance.test.ts" with:
264
267
  """
@@ -275,9 +278,9 @@ Feature: the naive pass and seam judge decide how a feature is built
275
278
  When the process is driven
276
279
  Then the process rests at "seam.steer"
277
280
  When gtd lands
278
- Then the process rests at "implement.building"
281
+ Then the process rests at "implement.applying"
279
282
  And the commit subjects end with:
280
283
  """
281
284
  gtd(judge): seam.judge → seam.steer
282
- gtd(human): seam.steer → implement.building
285
+ gtd(human): seam.steer → implement.applying
283
286
  """
@@ -13,6 +13,9 @@ import {
13
13
  judgeSeam,
14
14
  naivePass,
15
15
  acceptanceFault,
16
+ naiveAcceptance,
17
+ requireAcceptance,
18
+ requireAcceptanceKept,
16
19
  seamBuild,
17
20
  type SeamOutcome,
18
21
  } from "./seam.js"
@@ -105,6 +108,67 @@ describe("naivePass", () => {
105
108
  })
106
109
  })
107
110
 
111
+ describe("naiveAcceptance and requireAcceptance", () => {
112
+ const requirements = "# R\n\n## Acceptance Criteria\n\n- AC-1: adds.\n- AC-2: subtracts.\n"
113
+ const file = (path: string, before: string | undefined, after: string): Change => ({
114
+ path,
115
+ status: before === undefined ? "added" : "modified",
116
+ before,
117
+ after,
118
+ })
119
+ const install = (since: Change[] = []) =>
120
+ installContext({
121
+ ...fixtureContext({}),
122
+ matches: globMatches,
123
+ read: (path: string) => (path === ".gtd/REQUIREMENTS.md" ? requirements : undefined),
124
+ changesSince: () => since,
125
+ changes: () => since,
126
+ refuse: (message: string) => {
127
+ throw new Error(message)
128
+ },
129
+ } as unknown as FlowContext)
130
+
131
+ it("takes the IDs the pass's tests name on an added line, and only those test files", () => {
132
+ install()
133
+ expect(
134
+ naiveAcceptance(
135
+ tip(
136
+ file("src/a.test.ts", 'it("[AC-2] old")', 'it("[AC-2] old")\nit("[AC-1] adds")'),
137
+ file("src/b.test.ts", undefined, 'it("helper")'),
138
+ file("src/calc.ts", "x", "AC-2"),
139
+ ),
140
+ ),
141
+ ).toEqual({ hash: "abc", tests: ["src/a.test.ts"], ids: ["AC-1"] })
142
+ })
143
+
144
+ it("refuses a rebuild whose tests leave out an ID, naming the naive tests to carry over", () => {
145
+ install([file("src/calc.test.ts", undefined, 'it("[AC-1] adds")')])
146
+ expect(() =>
147
+ requireAcceptance({ hash: "abc", tests: ["src/a.test.ts"], ids: ["AC-1", "AC-2"] }, "c0"),
148
+ ).toThrow(
149
+ /^gtd land: rebuild-acceptance: .*leave out AC-2 — .*git show abc -- 'src\/a\.test\.ts'/,
150
+ )
151
+ })
152
+
153
+ it("refuses a fix turn that itself drops a carried ID, never one that leaves it alone", () => {
154
+ const acceptance = { hash: "abc", tests: [], ids: ["AC-1"] }
155
+ install([file("src/calc.test.ts", 'it("[AC-1] adds")', 'it("adds")')])
156
+ expect(() => requireAcceptanceKept(acceptance)).toThrow(/drops AC-1/)
157
+ install([file("src/calc.ts", "a", "b")])
158
+ expect(() => requireAcceptanceKept(acceptance)).not.toThrow()
159
+ })
160
+
161
+ it("passes a rebuild whose tests name every ID, wherever they live", () => {
162
+ install([
163
+ file("tests/x.test.ts", undefined, 'it("[AC-1] adds")'),
164
+ file("tests/y.feature", "", "Scenario: [AC-2] subtracts"),
165
+ ])
166
+ expect(() =>
167
+ requireAcceptance({ hash: "abc", tests: ["src/a.test.ts"], ids: ["AC-1", "AC-2"] }, "c0"),
168
+ ).not.toThrow()
169
+ })
170
+ })
171
+
108
172
  describe("acceptanceFault", () => {
109
173
  const requirements = "# R\n\n## Acceptance Criteria\n\n- AC-1: adds.\n- AC-2: subtracts.\n"
110
174
  const file = (path: string, status: Change["status"], after: string): Change => ({
@@ -231,6 +295,7 @@ describe("acceptanceFault", () => {
231
295
 
232
296
  const REVIEW = ".gtd/REVIEW.md"
233
297
  const REFACTOR = ".gtd/REFACTOR.md"
298
+ const NOTE = ".gtd/REFACTOR-NOTE.md"
234
299
  const reviewDoc = [
235
300
  "# Review: abc1234",
236
301
  "",
@@ -267,6 +332,7 @@ const driveSeam = async (
267
332
  const files = new Map<string, string>([["src/a.ts", "a"]])
268
333
  let current = "c0"
269
334
  const effects: Record<string, () => void> = {
335
+ note: () => void files.set(NOTE, "Seam: a shared op"),
270
336
  deriving: () => void files.set(REFACTOR, "package"),
271
337
  closing: () => void files.delete(REFACTOR),
272
338
  "review.reviewing": () => void files.set(REVIEW, reviewDoc),
@@ -321,13 +387,19 @@ const driveSeam = async (
321
387
  }
322
388
 
323
389
  describe("seamBuild", () => {
324
- it("land resolves the kept tip with the caller's base and enters no slice", async () => {
390
+ it("land resolves the kept tip to land, with the caller's base, and enters no slice", async () => {
325
391
  const h = await driveSeam("land")
326
- expect(h.outcome).toEqual({ reference: "abc", base: "c-base" })
392
+ expect(h.outcome).toEqual({ reference: "abc", base: "c-base", land: true })
327
393
  expect(h.slices).toEqual([])
328
394
  expect(h.names).toEqual(["seam.naive", "seam.check", "seam.judge"])
329
395
  })
330
396
 
397
+ it("with flagMode off resolves a vetted pass to land with no judge", async () => {
398
+ const h = await driveSeam("reject", undefined, { flagMode: "false" })
399
+ expect(h.outcome).toEqual({ reference: "abc", base: "c-base", land: true })
400
+ expect(h.names).toEqual(["seam.naive", "seam.check"])
401
+ })
402
+
331
403
  it("a red check retries the pass, and the judge sees it only once green", async () => {
332
404
  const h = await driveSeam("land", undefined, {}, 1)
333
405
  expect(h.names).toEqual(["seam.naive", "seam.check", "seam.naive", "seam.check", "seam.judge"])
@@ -355,13 +427,20 @@ describe("seamBuild", () => {
355
427
  "seam.clear",
356
428
  "seam.judge",
357
429
  ])
358
- expect(h.outcome).toEqual({ reference: "abc", base: "c-base" })
430
+ expect(h.outcome).toEqual({ reference: "abc", base: "c-base", land: true })
359
431
  })
360
432
 
361
433
  it("refactor-first runs the reviewed refactor in slice refactor and resolves HEAD after it", async () => {
362
434
  const h = await driveSeam("refactor-first")
363
- expect(h.outcome).toEqual({ reference: "abc", base: "after-refactor", refactored: true })
435
+ expect(h.outcome).toEqual({
436
+ reference: "abc",
437
+ base: "after-refactor",
438
+ refactored: true,
439
+ rebuild: { note: "Seam: a shared op", acceptance: { hash: "abc", tests: [], ids: [] } },
440
+ })
364
441
  expect(h.slices).toEqual(["refactor"])
442
+ expect(h.names.indexOf("seam.note")).toBe(h.names.indexOf("seam.judge") + 1)
443
+ expect(h.inside.refactor).not.toContain("seam.note")
365
444
  expect(h.inside.refactor![0]).toBe("refactor.deriving")
366
445
  expect(h.inside.refactor).toContain("refactor.build.review.closing")
367
446
  expect(h.names.some((n) => n.includes("approve"))).toBe(false)
@@ -372,7 +451,11 @@ describe("seamBuild", () => {
372
451
  const h = await driveSeam("refactor-first", change("src/a.test.ts", "modified"), {
373
452
  seamAllow: ".gtd/**",
374
453
  })
375
- expect(h.outcome).toEqual({ reference: "abc", base: "c-base" })
454
+ expect(h.outcome).toEqual({
455
+ reference: "abc",
456
+ base: "c-base",
457
+ rebuild: { acceptance: { hash: "abc", tests: [], ids: [] } },
458
+ })
376
459
  expect(h.slices).toEqual([])
377
460
  expect(h.names).not.toContain("seam.by-hand")
378
461
  expect(h.names.some((n) => n.startsWith("refactor"))).toBe(false)
@@ -397,7 +480,11 @@ describe("seamBuild", () => {
397
480
  })
398
481
  expect(h.slices).toEqual([])
399
482
  expect(h.names).toContain("seam.by-hand")
400
- expect(h.outcome).toEqual({ reference: "abc", base: "c-base" })
483
+ expect(h.outcome).toEqual({
484
+ reference: "abc",
485
+ base: "c-base",
486
+ rebuild: { acceptance: { hash: "abc", tests: [], ids: [] } },
487
+ })
401
488
  })
402
489
  })
403
490
 
@@ -466,6 +553,30 @@ describe("implement", () => {
466
553
  expect(bare).toBe(await building(undefined))
467
554
  })
468
555
 
556
+ it("a rebuild quotes the refactor note and names the naive acceptance tests to carry over", async () => {
557
+ const prompt = await building("abc", undefined, undefined, () =>
558
+ implement({
559
+ reference: "abc",
560
+ rebuild: {
561
+ note: "# Note\n\nSeam: a shared op",
562
+ acceptance: { hash: "abc", tests: ["src/a.test.ts"], ids: ["AC-1"] },
563
+ },
564
+ // The fixture's turn carries no test, so the acceptance guard refuses it after the prompt.
565
+ }).catch(() => undefined),
566
+ )
567
+ expect(prompt).toContain(" > Seam: a shared op")
568
+ expect(prompt).toContain("acceptance tests for AC-1")
569
+ expect(prompt).toContain("git show abc -- 'src/a.test.ts'")
570
+ })
571
+
572
+ it("a rebuild whose naive tests named no ID asks to carry nothing over", async () => {
573
+ const prompt = await building("abc", undefined, undefined, () =>
574
+ implement({ reference: "abc", rebuild: { acceptance: { hash: "abc", tests: [], ids: [] } } }),
575
+ )
576
+ expect(prompt).not.toContain("Carry over")
577
+ expect(prompt).not.toContain("refactor note")
578
+ })
579
+
469
580
  it("names the reference with a git show line", async () => {
470
581
  expect(await building("abc")).toContain("git show abc")
471
582
  })