@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +45 -29
- package/claude/hooks/context.ts +66 -0
- package/claude/hooks/drive.ts +3 -0
- package/claude/hooks/register.tsx +69 -6
- package/dist/chunks/{reserved-C8Fm3s72.mjs → reserved-zAfoJtR7.mjs} +62 -24
- package/dist/chunks/{tail-oaYewBN_.mjs → tail-BRC4DGdV.mjs} +1 -1
- package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
- package/dist/flows.mjs +15 -10
- package/dist/gtd.bundle.mjs +511 -64
- package/dist/testkit.mjs +158 -26
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +138 -38
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +208 -66
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +3 -3
- package/src/cli/Cli.test.ts +10 -3
- package/src/cli/Cli.ts +76 -1
- package/src/cli/Program.test.ts +1 -0
- package/src/cli/Program.ts +28 -0
- package/src/cli/ToolCommands.ts +3 -0
- package/src/edge/Edge.ts +11 -0
- package/src/flows/runtime.ts +11 -0
- package/src/flows/scripts.ts +31 -9
- package/src/replay/Replay.ts +51 -2
- package/src/testkit/FlowScripts.test.ts +58 -0
- package/src/testkit/FlowScripts.ts +61 -24
- package/src/testkit/Layers.ts +1 -0
- package/src/wire/BeatDocument.ts +12 -2
- package/src/wire/BeatStatus.ts +3 -0
- package/src/wire/ContextLimit.fixture.ts +30 -0
- package/src/wire/ContextLimit.test.ts +96 -0
- package/src/wire/ContextLimit.ts +169 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/types.ts +7 -0
- package/src/workflow/Workflow.ts +9 -1
- package/src/workflows/feature/acceptance-criteria.feature +5 -1
- package/src/workflows/feature/context-limit.feature +37 -0
- package/src/workflows/feature/coverage.feature +6 -3
- package/src/workflows/feature/coverage.ts +8 -2
- package/src/workflows/feature/fast-path.feature +11 -19
- package/src/workflows/feature/feature.test.ts +24 -4
- package/src/workflows/feature/feature.ts +14 -10
- package/src/workflows/feature/flag-mode.feature +17 -9
- package/src/workflows/feature/lens-models.feature +7 -1
- package/src/workflows/feature/naive-acceptance.feature +2 -2
- package/src/workflows/feature/one-step-land.feature +10 -12
- package/src/workflows/feature/quality-unresolved.feature +7 -1
- package/src/workflows/feature/refactor-absorbs.feature +12 -5
- package/src/workflows/feature/refactor-first.feature +11 -2
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +269 -0
- package/src/workflows/feature/refactor-review.feature +45 -1
- package/src/workflows/feature/refactor.ts +9 -3
- package/src/workflows/feature/risk-tier.feature +12 -4
- package/src/workflows/feature/seam-rejection.feature +4 -9
- package/src/workflows/feature/seam.feature +29 -26
- package/src/workflows/feature/seam.test.ts +117 -6
- package/src/workflows/feature/seam.ts +158 -39
- package/src/workflows/review/review-door.feature +12 -4
- package/src/workflows/shared/options.test.ts +11 -2
- package/src/workflows/shared/options.ts +18 -4
- package/src/workflows/shared/steps.ts +1 -0
- package/src/workflows/shared/text.test.ts +8 -0
- 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(
|
|
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
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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.
|
|
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.
|
|
129
|
-
gtd(
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
82
|
+
gtd(judge): seam.judge → implement.applying
|
|
83
83
|
"""
|
|
84
84
|
|
|
85
|
-
Scenario:
|
|
86
|
-
Given step "seam.
|
|
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/
|
|
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
|
|
94
|
+
And step "seam.check" writes "src/subtract.ts" with:
|
|
97
95
|
"""
|
|
98
|
-
|
|
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
|
|
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.
|
|
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.
|
|
157
|
-
gtd(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
})
|