@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.
Files changed (42) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +25 -14
  3. package/claude/hooks/assist.ts +35 -2
  4. package/claude/hooks/delegate.ts +22 -0
  5. package/claude/hooks/handoff.ts +13 -4
  6. package/claude/hooks/jobs.ts +67 -0
  7. package/claude/hooks/paste.ts +17 -0
  8. package/claude/hooks/register.tsx +126 -65
  9. package/claude/hooks/ship.ts +7 -2
  10. package/claude/hooks/tools.ts +32 -0
  11. package/claude/hooks/wording.ts +11 -0
  12. package/dist/chunks/{reserved-Df8GeSHw.mjs → reserved-UdSuHDUJ.mjs} +2 -2
  13. package/dist/chunks/{tail-DmIfEAI4.mjs → tail-fj60rQ3y.mjs} +1 -1
  14. package/dist/gtd.bundle.mjs +3 -3
  15. package/dist/testkit.mjs +30 -18
  16. package/dist/workflows/cleanup.mjs +2 -2
  17. package/dist/workflows/feature.mjs +3 -3
  18. package/dist/workflows/fix.mjs +2 -2
  19. package/dist/workflows/review.mjs +2 -2
  20. package/dist/workflows/rollout.mjs +1 -1
  21. package/dist/workflows.mjs +3 -3
  22. package/package.json +1 -1
  23. package/src/cli/Cli.ts +2 -2
  24. package/src/testkit/EmittedScriptRecognizer.test.ts +41 -0
  25. package/src/testkit/EmittedScriptRecognizer.ts +16 -7
  26. package/src/testkit/TestRepo.test.ts +79 -1
  27. package/src/testkit/TestRepo.ts +17 -6
  28. package/src/testkit/steps.ts +44 -1
  29. package/src/testkit/testkit.feature +35 -0
  30. package/src/workflows/cleanup/cleanup-path.feature +94 -0
  31. package/src/workflows/cleanup/cleanup.feature +0 -29
  32. package/src/workflows/feature/fast-path.feature +114 -0
  33. package/src/workflows/feature/one-step-land.feature +108 -0
  34. package/src/workflows/feature/refactor-first.feature +137 -0
  35. package/src/workflows/feature/review.test.ts +9 -3
  36. package/src/workflows/feature/review.ts +3 -3
  37. package/src/workflows/feature/risk-tier.feature +0 -63
  38. package/src/workflows/feature/seam-rejection.feature +146 -0
  39. package/src/workflows/feature/seam.feature +0 -67
  40. package/src/workflows/shared/steps.ts +1 -2
  41. package/src/workflows/shared/tail.test.ts +18 -9
  42. package/src/workflows/shared/workflows.steps.ts +5 -4
@@ -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) actors[step] = () => verdict
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([false, true])
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("a later gate does not accept a clean land after a deferral round with a nit", async () => {
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([false, false])
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 and spent at the very next gate; a fresh
171
- // review (`reviewing`) disarms it, so a later gate never accepts a clean land.
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), true)
187
+ ? awaitReview(b, t.buildReviewAwaitAfterDeferMessage(b, notes))
188
188
  : awaitReview(b)
189
189
  },
190
190
  reviewMissing,