@pmelab/gtd 27.6.0 → 27.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +25 -14
- package/claude/hooks/assist.ts +35 -2
- package/claude/hooks/delegate.ts +22 -0
- package/claude/hooks/handoff.ts +13 -4
- package/claude/hooks/jobs.ts +67 -0
- package/claude/hooks/paste.ts +17 -0
- package/claude/hooks/register.tsx +126 -65
- package/claude/hooks/ship.ts +7 -2
- package/claude/hooks/tools.ts +32 -0
- package/claude/hooks/wording.ts +11 -0
- package/dist/chunks/{reserved-Df8GeSHw.mjs → reserved-UdSuHDUJ.mjs} +2 -2
- package/dist/chunks/{tail-DmIfEAI4.mjs → tail-fj60rQ3y.mjs} +1 -1
- package/dist/gtd.bundle.mjs +3 -3
- package/dist/testkit.mjs +30 -18
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +3 -3
- 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 +3 -3
- package/package.json +1 -1
- package/src/cli/Cli.ts +2 -2
- package/src/testkit/EmittedScriptRecognizer.test.ts +41 -0
- package/src/testkit/EmittedScriptRecognizer.ts +16 -7
- package/src/testkit/TestRepo.test.ts +79 -1
- package/src/testkit/TestRepo.ts +17 -6
- package/src/testkit/steps.ts +44 -1
- package/src/testkit/testkit.feature +35 -0
- package/src/workflows/cleanup/cleanup-path.feature +94 -0
- package/src/workflows/cleanup/cleanup.feature +0 -29
- package/src/workflows/feature/fast-path.feature +114 -0
- package/src/workflows/feature/one-step-land.feature +108 -0
- package/src/workflows/feature/refactor-first.feature +137 -0
- package/src/workflows/feature/review.test.ts +9 -3
- package/src/workflows/feature/review.ts +3 -3
- package/src/workflows/feature/risk-tier.feature +0 -63
- package/src/workflows/feature/seam-rejection.feature +146 -0
- package/src/workflows/feature/seam.feature +0 -67
- package/src/workflows/shared/steps.ts +1 -2
- package/src/workflows/shared/tail.test.ts +18 -9
- package/src/workflows/shared/workflows.steps.ts +5 -4
package/src/testkit/steps.ts
CHANGED
|
@@ -70,7 +70,15 @@ export class TestkitWorld extends QuickPickleWorld {
|
|
|
70
70
|
for (const action of actions) await action(beat, repo)
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
for (const [step, verdict] of this.verdicts)
|
|
73
|
+
for (const [step, verdict] of this.verdicts) {
|
|
74
|
+
// A verdict would overwrite the queued actions, silently skipping their checks.
|
|
75
|
+
if (this.actions.has(step)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`step "${step}" has a judge verdict and queued actions; the actions would never run`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
actors[step] = () => verdict
|
|
81
|
+
}
|
|
74
82
|
await this.repo().drive(actors)
|
|
75
83
|
}
|
|
76
84
|
}
|
|
@@ -120,6 +128,19 @@ Given(
|
|
|
120
128
|
},
|
|
121
129
|
)
|
|
122
130
|
|
|
131
|
+
// A standing assertion like any queued action: it applies on every later visit
|
|
132
|
+
// to the step until `from now on` clears the list.
|
|
133
|
+
Given(
|
|
134
|
+
"step {string} is prompted with {string}",
|
|
135
|
+
(world: TestkitWorld, step: string, text: string) => {
|
|
136
|
+
world.act(step, (beat) => {
|
|
137
|
+
if (!beat.content.includes(text)) {
|
|
138
|
+
throw new Error(`step "${step}" was not prompted with "${text}":\n${beat.content}`)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
123
144
|
Given("step {string} runs its script", (world: TestkitWorld, step: string) => {
|
|
124
145
|
world.act(step, runsItsScript)
|
|
125
146
|
})
|
|
@@ -207,6 +228,28 @@ Then("the commit subjects end with:", (world: TestkitWorld, list: string) => {
|
|
|
207
228
|
assert.deepStrictEqual(world.repo().subjects().slice(-expected.length), expected)
|
|
208
229
|
})
|
|
209
230
|
|
|
231
|
+
const outputOf = async (world: TestkitWorld, list: string): Promise<string> => {
|
|
232
|
+
const result = await world.repo().gtd(...args(list))
|
|
233
|
+
assert.strictEqual(result.exitCode, 0, result.stderr)
|
|
234
|
+
return result.stdout
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
Then(
|
|
238
|
+
"the output of gtd {string} contains {string}",
|
|
239
|
+
async (world: TestkitWorld, list: string, text: string) => {
|
|
240
|
+
const out = await outputOf(world, list)
|
|
241
|
+
assert.ok(out.includes(text), out)
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
Then(
|
|
246
|
+
"the output of gtd {string} does not contain {string}",
|
|
247
|
+
async (world: TestkitWorld, list: string, text: string) => {
|
|
248
|
+
const out = await outputOf(world, list)
|
|
249
|
+
assert.ok(!out.includes(text), out)
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
|
|
210
253
|
Then("{string} contains {string}", (world: TestkitWorld, path: string, text: string) => {
|
|
211
254
|
assert.ok(world.repo().read(path)?.includes(text), `${path}: ${world.repo().read(path)}`)
|
|
212
255
|
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
@inmem
|
|
1
2
|
Feature: testkit drives any workflow against the real engine
|
|
2
3
|
|
|
3
4
|
A workflow the scenario writes itself, never a shipped one: scripted actors
|
|
@@ -47,6 +48,8 @@ Feature: testkit drives any workflow against the real engine
|
|
|
47
48
|
gtd(judge): ready → ship
|
|
48
49
|
"""
|
|
49
50
|
And "PLAN.md" contains "add two numbers"
|
|
51
|
+
And the output of gtd "next" contains "ship"
|
|
52
|
+
And the output of gtd "next" does not contain "rework"
|
|
50
53
|
|
|
51
54
|
Scenario: a red check sends the process to repair, and an unscripted step is where it rests
|
|
52
55
|
Given step "plan" writes "PLAN.md" with:
|
|
@@ -63,6 +66,38 @@ Feature: testkit drives any workflow against the real engine
|
|
|
63
66
|
And the resting step's content contains "Make the suite pass."
|
|
64
67
|
And the last commit subject is "gtd(check): check → repair"
|
|
65
68
|
|
|
69
|
+
Scenario: a step's beat content is asserted as a queued actor
|
|
70
|
+
Given step "plan" is prompted with "Write PLAN.md."
|
|
71
|
+
And step "plan" writes "PLAN.md" with:
|
|
72
|
+
"""
|
|
73
|
+
add two numbers
|
|
74
|
+
"""
|
|
75
|
+
And step "check" changes nothing
|
|
76
|
+
And judge step "ready" answers:
|
|
77
|
+
"""
|
|
78
|
+
[{ "id": "ready", "answer": true, "p": 0.9 }]
|
|
79
|
+
"""
|
|
80
|
+
When gtd starts workflow "tiny"
|
|
81
|
+
And the process is driven
|
|
82
|
+
Then the process rests at "ship"
|
|
83
|
+
And "PLAN.md" contains "add two numbers"
|
|
84
|
+
|
|
85
|
+
Scenario: a judge step with queued actions refuses the drive
|
|
86
|
+
Given step "plan" changes nothing
|
|
87
|
+
And step "check" changes nothing
|
|
88
|
+
And step "ready" is prompted with "never seen"
|
|
89
|
+
And judge step "ready" answers:
|
|
90
|
+
"""
|
|
91
|
+
[{ "id": "ready", "answer": true, "p": 0.9 }]
|
|
92
|
+
"""
|
|
93
|
+
When gtd starts workflow "tiny"
|
|
94
|
+
Then driving the process is refused with "queued actions"
|
|
95
|
+
|
|
96
|
+
Scenario: a beat that lacks the expected text refuses the drive
|
|
97
|
+
Given step "plan" is prompted with "Write a poem"
|
|
98
|
+
When gtd starts workflow "tiny"
|
|
99
|
+
Then driving the process is refused with 'was not prompted with "Write a poem"'
|
|
100
|
+
|
|
66
101
|
Scenario: a start the workflow refuses is reported with its message
|
|
67
102
|
Then starting workflow "picky" with "--var mode=bad" is refused with "picky refuses every start"
|
|
68
103
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# One SDLC path per file, so a changed path reds exactly one fixture — do not merge.
|
|
2
|
+
@inmem
|
|
3
|
+
Feature: cleanup walks one fixed path from the opening commit to idle
|
|
4
|
+
|
|
5
|
+
A default-on release flag is cleaned up by an agent that removes the registry
|
|
6
|
+
entry and the off path; health, review and closing follow and the process
|
|
7
|
+
rests at idle. Cleanup reads no release flag, so no flag is pinned here and
|
|
8
|
+
every flag state takes the same path.
|
|
9
|
+
|
|
10
|
+
Background:
|
|
11
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
12
|
+
And a gtd config with:
|
|
13
|
+
"""
|
|
14
|
+
export {
|
|
15
|
+
feature as default,
|
|
16
|
+
feature,
|
|
17
|
+
defaults,
|
|
18
|
+
envDefaults,
|
|
19
|
+
summary,
|
|
20
|
+
steering,
|
|
21
|
+
skills,
|
|
22
|
+
access,
|
|
23
|
+
} from "@pmelab/gtd/workflows/feature"
|
|
24
|
+
import { cleanup } from "@pmelab/gtd/workflows"
|
|
25
|
+
export { cleanup }
|
|
26
|
+
"""
|
|
27
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
28
|
+
And a file "src/flags/index.ts" with:
|
|
29
|
+
"""
|
|
30
|
+
export const FLAGS = [
|
|
31
|
+
{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release" },
|
|
32
|
+
{ name: "live", owner: "d", expiry: "2027-04-01", kind: "release", defaultOn: true },
|
|
33
|
+
] as const
|
|
34
|
+
"""
|
|
35
|
+
And a file "src/calc.ts" with:
|
|
36
|
+
"""
|
|
37
|
+
export const on = process.env.GTD_FLAG_LIVE !== "0"
|
|
38
|
+
"""
|
|
39
|
+
And the working tree is committed as "feat: registry"
|
|
40
|
+
And step "cleanup.removing" writes "src/flags/index.ts" with:
|
|
41
|
+
"""
|
|
42
|
+
export const FLAGS = [
|
|
43
|
+
{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release" },
|
|
44
|
+
] as const
|
|
45
|
+
"""
|
|
46
|
+
And step "cleanup.removing" writes "src/calc.ts" with:
|
|
47
|
+
"""
|
|
48
|
+
export const on = true
|
|
49
|
+
"""
|
|
50
|
+
And step "build.health.check" changes nothing
|
|
51
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
52
|
+
"""
|
|
53
|
+
# Review: abc1234
|
|
54
|
+
|
|
55
|
+
<!-- base: 0000000 -->
|
|
56
|
+
|
|
57
|
+
## calc
|
|
58
|
+
|
|
59
|
+
- [ ] ./src/calc.ts#1-1
|
|
60
|
+
flag removed
|
|
61
|
+
"""
|
|
62
|
+
And step "build.review.closing" runs its script
|
|
63
|
+
|
|
64
|
+
Scenario: removing the entry and the off path runs health, review and closing to idle
|
|
65
|
+
When gtd starts workflow "cleanup" with "--var flag=live"
|
|
66
|
+
And the process is driven
|
|
67
|
+
Then the process rests at "build.review.await-review"
|
|
68
|
+
Given a file ".gtd/REVIEW.md" with:
|
|
69
|
+
"""
|
|
70
|
+
# Review: abc1234
|
|
71
|
+
|
|
72
|
+
<!-- base: 0000000 -->
|
|
73
|
+
|
|
74
|
+
## calc
|
|
75
|
+
|
|
76
|
+
- [x] ./src/calc.ts#1-1
|
|
77
|
+
flag removed
|
|
78
|
+
"""
|
|
79
|
+
When gtd lands
|
|
80
|
+
And the process is driven
|
|
81
|
+
Then the process rests at "idle"
|
|
82
|
+
And "src/flags/index.ts" does not contain "live"
|
|
83
|
+
And "src/calc.ts" does not contain "GTD_FLAG_LIVE"
|
|
84
|
+
And the commit subjects end with:
|
|
85
|
+
"""
|
|
86
|
+
gtd(human): cleanup.removing
|
|
87
|
+
gtd(agent): cleanup.removing → build.health.check
|
|
88
|
+
gtd(check): build.health.check → build.review.reviewing
|
|
89
|
+
gtd(agent): build.review.reviewing → build.review.await-review
|
|
90
|
+
gtd(human): build.review.await-review → build.review.closing
|
|
91
|
+
gtd(check): build.review.closing → idle
|
|
92
|
+
"""
|
|
93
|
+
And the commit "gtd(human): cleanup.removing" has a body containing "Gtd-Workflow: cleanup"
|
|
94
|
+
And the commit "gtd(human): cleanup.removing" has a body containing "Gtd-Var: flag=live"
|
|
@@ -36,35 +36,6 @@ Feature: cleanup removes a default-on release flag and keeps its flag-on path
|
|
|
36
36
|
"""
|
|
37
37
|
And the working tree is committed as "feat: registry"
|
|
38
38
|
|
|
39
|
-
Scenario: removing the entry and the off path lands, then reaches the review gate
|
|
40
|
-
Given step "cleanup.removing" writes "src/flags/index.ts" with:
|
|
41
|
-
"""
|
|
42
|
-
export const FLAGS = [
|
|
43
|
-
{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release" },
|
|
44
|
-
{ name: "ops1", owner: "c", expiry: "2027-03-01", kind: "ops" },
|
|
45
|
-
] as const
|
|
46
|
-
"""
|
|
47
|
-
And step "cleanup.removing" writes "src/calc.ts" with:
|
|
48
|
-
"""
|
|
49
|
-
export const on = true
|
|
50
|
-
"""
|
|
51
|
-
And step "build.health.check" changes nothing
|
|
52
|
-
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
53
|
-
"""
|
|
54
|
-
# Review: abc1234
|
|
55
|
-
|
|
56
|
-
<!-- base: 0000000 -->
|
|
57
|
-
|
|
58
|
-
## calc
|
|
59
|
-
|
|
60
|
-
- [ ] ./src/calc.ts#1-1
|
|
61
|
-
flag removed
|
|
62
|
-
"""
|
|
63
|
-
When gtd starts workflow "cleanup" with "--var flag=live"
|
|
64
|
-
And the process is driven
|
|
65
|
-
Then the process rests at "build.review.await-review"
|
|
66
|
-
And "src/flags/index.ts" does not contain "live"
|
|
67
|
-
|
|
68
39
|
Scenario: a turn leaving the registry entry is refused
|
|
69
40
|
Given step "cleanup.removing" writes "src/calc.ts" with:
|
|
70
41
|
"""
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a trivial change takes the same few beats at either risk tier, flag-free at low
|
|
3
|
+
|
|
4
|
+
With GTD_FLAG_NAIVE_PASS and GTD_FLAG_RISK_TIER on, a one-line copy fix goes
|
|
5
|
+
from idle through the naive pass, the seam judge's land, implement and human
|
|
6
|
+
review back to idle. Low tier walks exactly the steps high does; only the
|
|
7
|
+
implementation prompt and the summary differ.
|
|
8
|
+
|
|
9
|
+
Background:
|
|
10
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
11
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
|
|
12
|
+
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
13
|
+
And an environment variable "GTD_FLAG_RISK_TIER" set to "1"
|
|
14
|
+
And a file "src/greeting.ts" with:
|
|
15
|
+
"""
|
|
16
|
+
export const greeting = "Helo, world"
|
|
17
|
+
"""
|
|
18
|
+
And the working tree is committed as "feat: add greeting"
|
|
19
|
+
And a file "TODO.md" with:
|
|
20
|
+
"""
|
|
21
|
+
# Fix the typo
|
|
22
|
+
|
|
23
|
+
greeting says "Helo"; make it "Hello".
|
|
24
|
+
"""
|
|
25
|
+
And step "unwind" changes nothing
|
|
26
|
+
And step "start-gate.check" changes nothing
|
|
27
|
+
And step "design.gate.answer" changes nothing
|
|
28
|
+
And step "implement.health.check" changes nothing
|
|
29
|
+
And step "implement.closing" runs its script
|
|
30
|
+
And step "build.quality.correctness.reviewing" changes nothing
|
|
31
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
32
|
+
"""
|
|
33
|
+
# Review: abc1234
|
|
34
|
+
|
|
35
|
+
<!-- base: 0000000 -->
|
|
36
|
+
|
|
37
|
+
## greeting
|
|
38
|
+
|
|
39
|
+
- [ ] ./src/greeting.ts#1-1
|
|
40
|
+
typo fixed
|
|
41
|
+
"""
|
|
42
|
+
And step "build.review.closing" runs its script
|
|
43
|
+
|
|
44
|
+
Scenario Outline: a one-line fix runs idle to idle with the same beats at either tier
|
|
45
|
+
Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
46
|
+
"""
|
|
47
|
+
# Requirements
|
|
48
|
+
|
|
49
|
+
## Risk Tier
|
|
50
|
+
|
|
51
|
+
<tier> — one-line copy fix
|
|
52
|
+
"""
|
|
53
|
+
When the process is driven
|
|
54
|
+
Then the process rests at "seam.naive"
|
|
55
|
+
And the last commit subject is "gtd(human): design.gate.answer → seam.naive"
|
|
56
|
+
|
|
57
|
+
Given step "seam.naive" writes "src/greeting.ts" with:
|
|
58
|
+
"""
|
|
59
|
+
export const greeting = "Hello, world"
|
|
60
|
+
"""
|
|
61
|
+
And judge step "seam.judge" answers:
|
|
62
|
+
"""
|
|
63
|
+
[{"id":"shape","answer":"land","p":0.9}]
|
|
64
|
+
"""
|
|
65
|
+
When the process is driven
|
|
66
|
+
Then the process rests at "implement.building"
|
|
67
|
+
And the resting step's content contains "<demand>"
|
|
68
|
+
And the resting step's content does not contain "<absent>"
|
|
69
|
+
|
|
70
|
+
Given step "implement.building" writes "src/greeting.ts" with:
|
|
71
|
+
"""
|
|
72
|
+
export const greeting = "Hello, world!"
|
|
73
|
+
"""
|
|
74
|
+
When the process is driven
|
|
75
|
+
Then the process rests at "build.review.await-review"
|
|
76
|
+
|
|
77
|
+
Given a file ".gtd/REVIEW.md" with:
|
|
78
|
+
"""
|
|
79
|
+
# Review: abc1234
|
|
80
|
+
|
|
81
|
+
<!-- base: 0000000 -->
|
|
82
|
+
|
|
83
|
+
## greeting
|
|
84
|
+
|
|
85
|
+
- [x] ./src/greeting.ts#1-1
|
|
86
|
+
typo fixed
|
|
87
|
+
"""
|
|
88
|
+
When gtd lands
|
|
89
|
+
And the process is driven
|
|
90
|
+
Then the process rests at "idle"
|
|
91
|
+
And "src/greeting.ts" contains "Hello, world!"
|
|
92
|
+
And the commit subjects end with:
|
|
93
|
+
"""
|
|
94
|
+
gtd(human): idle → unwind
|
|
95
|
+
gtd(check): unwind → start-gate.check
|
|
96
|
+
gtd(check): start-gate.check → design.triage
|
|
97
|
+
gtd(agent): design.triage → design.gate.answer
|
|
98
|
+
gtd(human): design.gate.answer → seam.naive
|
|
99
|
+
gtd(agent): seam.naive → seam.judge
|
|
100
|
+
gtd(judge): seam.judge → implement.building
|
|
101
|
+
gtd(agent): implement.building → implement.health.check
|
|
102
|
+
gtd(check): implement.health.check → implement.closing
|
|
103
|
+
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
104
|
+
gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
|
|
105
|
+
gtd(agent): build.review.reviewing → build.review.await-review
|
|
106
|
+
gtd(human): build.review.await-review → build.review.closing
|
|
107
|
+
gtd(check): build.review.closing → idle
|
|
108
|
+
"""
|
|
109
|
+
And the output of gtd "summary" <summary> "needs a human reviewer"
|
|
110
|
+
|
|
111
|
+
Examples:
|
|
112
|
+
| tier | demand | absent | summary |
|
|
113
|
+
| low | Add no release flag | new release flag | does not contain |
|
|
114
|
+
| high | Put the new behaviour behind a new release flag | Add no release flag | contains |
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# One SDLC path per file, so a changed path reds exactly one fixture — do not merge.
|
|
2
|
+
@inmem
|
|
3
|
+
Feature: a naive pass the judge rules land goes straight to implement with no seam gate
|
|
4
|
+
|
|
5
|
+
With GTD_FLAG_NAIVE_PASS on and GTD_FLAG_RISK_TIER off, a naive pass that
|
|
6
|
+
edits an existing source file is judged land: the process skips the approval
|
|
7
|
+
gate, any refactor and any architecture, and runs implement, quality and
|
|
8
|
+
review back to idle. The flags are pinned here so the per-flag test runs never
|
|
9
|
+
move it off this path.
|
|
10
|
+
|
|
11
|
+
Background:
|
|
12
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
13
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
|
|
14
|
+
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
15
|
+
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
16
|
+
And a file "src/greeting.ts" with:
|
|
17
|
+
"""
|
|
18
|
+
export const greeting = "Helo, world"
|
|
19
|
+
"""
|
|
20
|
+
And the working tree is committed as "feat: add greeting"
|
|
21
|
+
And a file "TODO.md" with:
|
|
22
|
+
"""
|
|
23
|
+
# Fix the typo
|
|
24
|
+
|
|
25
|
+
greeting says "Helo"; make it "Hello".
|
|
26
|
+
"""
|
|
27
|
+
And step "unwind" changes nothing
|
|
28
|
+
And step "start-gate.check" changes nothing
|
|
29
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
30
|
+
"""
|
|
31
|
+
# Requirements
|
|
32
|
+
|
|
33
|
+
## Concerns
|
|
34
|
+
|
|
35
|
+
### Typo
|
|
36
|
+
|
|
37
|
+
- greeting reads "Hello, world".
|
|
38
|
+
"""
|
|
39
|
+
And step "design.gate.answer" changes nothing
|
|
40
|
+
And step "seam.naive" writes "src/greeting.ts" with:
|
|
41
|
+
"""
|
|
42
|
+
export const greeting = "Hello, world"
|
|
43
|
+
"""
|
|
44
|
+
And judge step "seam.judge" answers:
|
|
45
|
+
"""
|
|
46
|
+
[{"id":"shape","answer":"land","p":0.9}]
|
|
47
|
+
"""
|
|
48
|
+
And step "implement.building" writes "src/greeting.ts" with:
|
|
49
|
+
"""
|
|
50
|
+
export const greeting = "Hello, world!"
|
|
51
|
+
"""
|
|
52
|
+
And step "implement.health.check" changes nothing
|
|
53
|
+
And step "implement.closing" runs its script
|
|
54
|
+
And step "build.quality.correctness.reviewing" changes nothing
|
|
55
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
56
|
+
"""
|
|
57
|
+
# Review: abc1234
|
|
58
|
+
|
|
59
|
+
<!-- base: 0000000 -->
|
|
60
|
+
|
|
61
|
+
## greeting
|
|
62
|
+
|
|
63
|
+
- [ ] ./src/greeting.ts#1-1
|
|
64
|
+
typo fixed
|
|
65
|
+
"""
|
|
66
|
+
And step "build.review.closing" runs its script
|
|
67
|
+
|
|
68
|
+
Scenario: a land judgment runs idle to idle with no seam gate, refactor or architecture
|
|
69
|
+
When the process is driven
|
|
70
|
+
Then the process rests at "build.review.await-review"
|
|
71
|
+
Given a file ".gtd/REVIEW.md" with:
|
|
72
|
+
"""
|
|
73
|
+
# Review: abc1234
|
|
74
|
+
|
|
75
|
+
<!-- base: 0000000 -->
|
|
76
|
+
|
|
77
|
+
## greeting
|
|
78
|
+
|
|
79
|
+
- [x] ./src/greeting.ts#1-1
|
|
80
|
+
typo fixed
|
|
81
|
+
"""
|
|
82
|
+
When gtd lands
|
|
83
|
+
And the process is driven
|
|
84
|
+
Then the process rests at "idle"
|
|
85
|
+
And "src/greeting.ts" contains "Hello, world!"
|
|
86
|
+
And the commit subjects end with:
|
|
87
|
+
"""
|
|
88
|
+
gtd(human): idle → unwind
|
|
89
|
+
gtd(check): unwind → start-gate.check
|
|
90
|
+
gtd(check): start-gate.check → design.triage
|
|
91
|
+
gtd(agent): design.triage → design.gate.answer
|
|
92
|
+
gtd(human): design.gate.answer → seam.naive
|
|
93
|
+
gtd(agent): seam.naive → seam.judge
|
|
94
|
+
gtd(judge): seam.judge → implement.building
|
|
95
|
+
gtd(agent): implement.building → implement.health.check
|
|
96
|
+
gtd(check): implement.health.check → implement.closing
|
|
97
|
+
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
98
|
+
gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
|
|
99
|
+
gtd(agent): build.review.reviewing → build.review.await-review
|
|
100
|
+
gtd(human): build.review.await-review → build.review.closing
|
|
101
|
+
gtd(check): build.review.closing → idle
|
|
102
|
+
"""
|
|
103
|
+
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_naive_pass=1"
|
|
104
|
+
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
|
|
105
|
+
And the commit "gtd(agent): seam.naive → seam.judge" has a body containing "Gtd-Kept:"
|
|
106
|
+
And the commit "gtd(judge): seam.judge → implement.building" has a body containing "\"answer\":\"land\""
|
|
107
|
+
And no commit body contains "Gtd-Approved:"
|
|
108
|
+
And no commit body contains "Gtd-Workflow:"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# One SDLC path per file, so a changed path reds exactly one fixture — do not merge.
|
|
2
|
+
@inmem
|
|
3
|
+
Feature: a refactor-first judgment walks one fixed path to idle
|
|
4
|
+
|
|
5
|
+
With GTD_FLAG_NAIVE_PASS on and GTD_FLAG_RISK_TIER off, a naive pass touching
|
|
6
|
+
an existing non-allowlisted file is ruled refactor-first, rests at the
|
|
7
|
+
approval gate, and is refactored before it is implemented. The flags are
|
|
8
|
+
pinned so the per-flag test runs never move it off its path.
|
|
9
|
+
|
|
10
|
+
Background:
|
|
11
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
12
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
13
|
+
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
14
|
+
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
15
|
+
And a file "src/calc.ts" with:
|
|
16
|
+
"""
|
|
17
|
+
export const add = (a: number, b: number) => a + b
|
|
18
|
+
"""
|
|
19
|
+
And the working tree is committed as "feat: add calculator"
|
|
20
|
+
And a file "TODO.md" with:
|
|
21
|
+
"""
|
|
22
|
+
# Add subtract
|
|
23
|
+
|
|
24
|
+
subtract(a, b) returns the difference.
|
|
25
|
+
"""
|
|
26
|
+
And step "unwind" changes nothing
|
|
27
|
+
And step "start-gate.check" changes nothing
|
|
28
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
29
|
+
"""
|
|
30
|
+
# Requirements
|
|
31
|
+
|
|
32
|
+
## Concerns
|
|
33
|
+
|
|
34
|
+
### Subtract
|
|
35
|
+
|
|
36
|
+
- `subtract(a, b)` returns `a - b`.
|
|
37
|
+
"""
|
|
38
|
+
And step "design.gate.answer" changes nothing
|
|
39
|
+
|
|
40
|
+
Scenario: a refactor-first judgment runs approval, refactor, implement and review to idle
|
|
41
|
+
Given step "seam.naive" writes "src/calc.ts" with:
|
|
42
|
+
"""
|
|
43
|
+
export const add = (a: number, b: number) => a + b
|
|
44
|
+
export const subtract = (a: number, b: number) => a - b
|
|
45
|
+
"""
|
|
46
|
+
And judge step "seam.judge" answers:
|
|
47
|
+
"""
|
|
48
|
+
[{"id":"shape","answer":"refactor-first","p":0.9}]
|
|
49
|
+
"""
|
|
50
|
+
And step "refactor.deriving" runs its script
|
|
51
|
+
And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
|
|
52
|
+
"""
|
|
53
|
+
# Review: abc1234
|
|
54
|
+
|
|
55
|
+
<!-- base: 0000000 -->
|
|
56
|
+
|
|
57
|
+
## calc
|
|
58
|
+
|
|
59
|
+
- [ ] ./src/calc.ts#1-3
|
|
60
|
+
subtract built on the shared op
|
|
61
|
+
"""
|
|
62
|
+
And step "build.review.closing" runs its script
|
|
63
|
+
When the process is driven
|
|
64
|
+
Then the process rests at "seam.approve"
|
|
65
|
+
And the resting step's content contains "src/calc.ts"
|
|
66
|
+
|
|
67
|
+
When gtd lands
|
|
68
|
+
And the process is driven
|
|
69
|
+
Then the process rests at "refactor.building"
|
|
70
|
+
And ".gtd/REFACTOR.md" contains "calc.ts`"
|
|
71
|
+
|
|
72
|
+
Given step "refactor.building" writes "src/calc.ts" with:
|
|
73
|
+
"""
|
|
74
|
+
export const op = (a: number, b: number, sign: number) => a + sign * b
|
|
75
|
+
export const add = (a: number, b: number) => op(a, b, 1)
|
|
76
|
+
"""
|
|
77
|
+
And step "refactor.health.check" changes nothing
|
|
78
|
+
And step "refactor.closing" runs its script
|
|
79
|
+
When the process is driven
|
|
80
|
+
Then the process rests at "implement.building"
|
|
81
|
+
And ".gtd/REFACTOR.md" does not exist
|
|
82
|
+
And "src/calc.ts" contains "op(a, b, 1)"
|
|
83
|
+
|
|
84
|
+
Given step "implement.building" writes "src/calc.ts" with:
|
|
85
|
+
"""
|
|
86
|
+
export const op = (a: number, b: number, sign: number) => a + sign * b
|
|
87
|
+
export const add = (a: number, b: number) => op(a, b, 1)
|
|
88
|
+
export const subtract = (a: number, b: number) => op(a, b, -1)
|
|
89
|
+
"""
|
|
90
|
+
And step "implement.health.check" changes nothing
|
|
91
|
+
And step "implement.closing" runs its script
|
|
92
|
+
When the process is driven
|
|
93
|
+
Then the process rests at "build.review.await-review"
|
|
94
|
+
|
|
95
|
+
Given a file ".gtd/REVIEW.md" with:
|
|
96
|
+
"""
|
|
97
|
+
# Review: abc1234
|
|
98
|
+
|
|
99
|
+
<!-- base: 0000000 -->
|
|
100
|
+
|
|
101
|
+
## calc
|
|
102
|
+
|
|
103
|
+
- [x] ./src/calc.ts#1-3
|
|
104
|
+
subtract built on the shared op
|
|
105
|
+
"""
|
|
106
|
+
When gtd lands
|
|
107
|
+
And the process is driven
|
|
108
|
+
Then the process rests at "idle"
|
|
109
|
+
And "src/calc.ts" contains "op(a, b, 1)"
|
|
110
|
+
And "src/calc.ts" contains "subtract"
|
|
111
|
+
And the commit subjects end with:
|
|
112
|
+
"""
|
|
113
|
+
gtd(human): idle → unwind
|
|
114
|
+
gtd(check): unwind → start-gate.check
|
|
115
|
+
gtd(check): start-gate.check → design.triage
|
|
116
|
+
gtd(agent): design.triage → design.gate.answer
|
|
117
|
+
gtd(human): design.gate.answer → seam.naive
|
|
118
|
+
gtd(agent): seam.naive → seam.judge
|
|
119
|
+
gtd(judge): seam.judge → seam.approve
|
|
120
|
+
gtd(human): seam.approve → refactor.deriving
|
|
121
|
+
gtd(check): refactor.deriving → refactor.building
|
|
122
|
+
gtd(agent): refactor.building → refactor.health.check
|
|
123
|
+
gtd(check): refactor.health.check → refactor.closing
|
|
124
|
+
gtd(check): refactor.closing → implement.building
|
|
125
|
+
gtd(agent): implement.building → implement.health.check
|
|
126
|
+
gtd(check): implement.health.check → implement.closing
|
|
127
|
+
gtd(check): implement.closing → build.review.reviewing
|
|
128
|
+
gtd(agent): build.review.reviewing → build.review.await-review
|
|
129
|
+
gtd(human): build.review.await-review → build.review.closing
|
|
130
|
+
gtd(check): build.review.closing → idle
|
|
131
|
+
"""
|
|
132
|
+
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_naive_pass=1"
|
|
133
|
+
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
|
|
134
|
+
And the commit "gtd(agent): seam.naive → seam.judge" has a body containing "Gtd-Kept:"
|
|
135
|
+
And the commit "gtd(judge): seam.judge → seam.approve" has a body containing "\"answer\":\"refactor-first\""
|
|
136
|
+
And the commit "gtd(human): seam.approve → refactor.deriving" has a body containing "Gtd-Approved: {\"step\":\"seam.approve#1\",\"name\":\"Test\",\"email\":\"test@test.com\"}"
|
|
137
|
+
And no commit body contains "Gtd-Workflow:"
|
|
@@ -509,13 +509,19 @@ describe("severity routing", () => {
|
|
|
509
509
|
notes: ["add — quote", "sub", "mul"],
|
|
510
510
|
answers: { "note-1": verdict("edit"), ...sev("note-1", could) },
|
|
511
511
|
})
|
|
512
|
-
expect(run.gates).toEqual([
|
|
512
|
+
expect(run.gates).toEqual([true, true])
|
|
513
513
|
expect(run.gateMessages[1]).toContain("NOT acted on")
|
|
514
514
|
expect(run.gateMessages[1]).toContain("note-1")
|
|
515
515
|
expect(run.gateMessages[0]).not.toContain("NOT acted on")
|
|
516
516
|
})
|
|
517
517
|
|
|
518
|
-
it("
|
|
518
|
+
it("the first round's gate accepts a clean land", async () => {
|
|
519
|
+
const run = await drive({ notes: ["add", "sub", "mul"] })
|
|
520
|
+
expect(run.gates.length).toBeGreaterThan(0)
|
|
521
|
+
expect(run.gates.every((g) => g)).toBe(true)
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
it("a later gate still accepts a clean land after a deferral round with a nit", async () => {
|
|
519
525
|
const run = await drive({
|
|
520
526
|
notes: ["add — quote", "sub — typo", "mul"],
|
|
521
527
|
answers: {
|
|
@@ -527,7 +533,7 @@ describe("severity routing", () => {
|
|
|
527
533
|
})
|
|
528
534
|
expect(run.log).toContain("review.defer")
|
|
529
535
|
expect(run.log.at(-1)).toBe("review.await-review")
|
|
530
|
-
expect(run.gates).toEqual([
|
|
536
|
+
expect(run.gates).toEqual([true, true])
|
|
531
537
|
})
|
|
532
538
|
|
|
533
539
|
it("a question and a could note in one round: the gate's base is the answering commit", async () => {
|
|
@@ -167,8 +167,8 @@ export const review = (
|
|
|
167
167
|
escalations: EscalationCount = { rounds: 0 },
|
|
168
168
|
): Promise<kit.ReviewOutcome> => {
|
|
169
169
|
const severities = new Map<string, Severity>()
|
|
170
|
-
// Armed by a defer-only round
|
|
171
|
-
// review (`reviewing`) disarms it
|
|
170
|
+
// Armed by a defer-only round to pick the next gate's message; a fresh
|
|
171
|
+
// review (`reviewing`) disarms it.
|
|
172
172
|
let deferred: readonly t.NoteInput[] = []
|
|
173
173
|
return kit.review(base, {
|
|
174
174
|
file: REVIEW,
|
|
@@ -184,7 +184,7 @@ export const review = (
|
|
|
184
184
|
const notes = deferred
|
|
185
185
|
deferred = []
|
|
186
186
|
return notes.length > 0
|
|
187
|
-
? awaitReview(b, t.buildReviewAwaitAfterDeferMessage(b, notes)
|
|
187
|
+
? awaitReview(b, t.buildReviewAwaitAfterDeferMessage(b, notes))
|
|
188
188
|
: awaitReview(b)
|
|
189
189
|
},
|
|
190
190
|
reviewMissing,
|