@pmelab/gtd 38.2.0 → 40.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +66 -58
  3. package/dist/chunks/{reserved-C3PgP7XB.mjs → reserved-Qt5FM6Ia.mjs} +29 -10
  4. package/dist/chunks/{tail-Bkxki2nK.mjs → tail-tAHx_AHe.mjs} +1 -1
  5. package/dist/gtd.bundle.mjs +14 -5
  6. package/dist/testkit.mjs +14 -5
  7. package/dist/workflow-kit.mjs +12 -1
  8. package/dist/workflows/cleanup.mjs +2 -2
  9. package/dist/workflows/feature.mjs +98 -37
  10. package/dist/workflows/fix.mjs +2 -2
  11. package/dist/workflows/review.mjs +2 -2
  12. package/dist/workflows/rollout.mjs +1 -1
  13. package/dist/workflows.mjs +136 -45
  14. package/package.json +1 -1
  15. package/skills/authoring/SKILL.md +3 -2
  16. package/src/cli/Cli.ts +4 -3
  17. package/src/cli/Metrics.test.ts +31 -0
  18. package/src/cli/Metrics.ts +8 -0
  19. package/src/workflow-kit/health.ts +15 -1
  20. package/src/workflows/cleanup/cleanup.test.ts +2 -0
  21. package/src/workflows/feature/coverage.feature +5 -5
  22. package/src/workflows/feature/criteria.test.ts +20 -1
  23. package/src/workflows/feature/criteria.ts +24 -0
  24. package/src/workflows/feature/fast-path.feature +4 -4
  25. package/src/workflows/feature/feature.ts +82 -22
  26. package/src/workflows/feature/flag-mode.feature +2 -2
  27. package/src/workflows/feature/one-step-land.feature +4 -4
  28. package/src/workflows/feature/planning.ts +3 -2
  29. package/src/workflows/feature/quality-unresolved.feature +6 -6
  30. package/src/workflows/feature/refactor-absorbs.feature +3 -3
  31. package/src/workflows/feature/refactor-first.feature +5 -5
  32. package/src/workflows/feature/refactor-rebuild.feature +2 -2
  33. package/src/workflows/feature/refactor-review.feature +8 -8
  34. package/src/workflows/feature/refactor.test.ts +4 -0
  35. package/src/workflows/feature/review-gate.feature +271 -0
  36. package/src/workflows/feature/seam-rejection.feature +4 -4
  37. package/src/workflows/feature/seam.feature +2 -2
  38. package/src/workflows/feature/seam.ts +17 -7
  39. package/src/workflows/feature/slicing.feature +63 -4
  40. package/src/workflows/rollout/rollout.feature +16 -0
  41. package/src/workflows/rollout/rollout.test.ts +2 -0
  42. package/src/workflows/shared/health.test.ts +2 -0
  43. package/src/workflows/shared/health.ts +34 -5
  44. package/src/workflows/shared/recheck.feature +60 -0
  45. package/src/workflows/shared/text.ts +7 -5
  46. package/src/workflows/review/review-door.feature +0 -121
@@ -4,8 +4,10 @@ import {
4
4
  env,
5
5
  human,
6
6
  judge,
7
+ mark,
7
8
  numeric,
8
9
  vars,
10
+ type CheckOptions,
9
11
  type JudgeQuestion,
10
12
  } from "../../flows/index.js"
11
13
  import * as kit from "../../workflow-kit/index.js"
@@ -19,6 +21,24 @@ export const FIX_CAP = 3
19
21
  export const baseline = (name: string, label = "Checking the baseline"): Promise<boolean> =>
20
22
  check(name, env.testCommand ?? "", { report: FEEDBACK, label })
21
23
 
24
+ /**
25
+ * Re-run a red check as step `name` before a person sees it. Green records
26
+ * `Gtd-Mark: flaky=<red>` on the re-check's landing and resolves `true`.
27
+ */
28
+ export const recheck = async (
29
+ name: string,
30
+ red: string,
31
+ options: Pick<CheckOptions, "sweepOnGreen"> = {},
32
+ ): Promise<boolean> => {
33
+ const green = await check(name, env.testCommand ?? "", {
34
+ ...options,
35
+ report: FEEDBACK,
36
+ label: "Re-running checks",
37
+ })
38
+ if (green) mark("flaky", red)
39
+ return green
40
+ }
41
+
22
42
  export type { EscalationCount } from "../../workflow-kit/index.js"
23
43
 
24
44
  const escalationSteps = (rules?: string): kit.EscalationSteps => ({
@@ -117,18 +137,24 @@ export interface HealthOptions {
117
137
  readonly rules?: string | undefined
118
138
  }
119
139
 
140
+ // Swept only on green: an unresolved analysis survives every retry.
141
+ const healthSweep = (options: HealthOptions): string[] => [
142
+ ESCALATION,
143
+ ...(options.sweepOnGreen ?? []),
144
+ ]
145
+
120
146
  const runSuite = (options: HealthOptions): Promise<boolean> =>
121
147
  check("health.check", env.testCommand ?? "", {
122
148
  report: FEEDBACK,
123
149
  label: "Running checks",
124
- // Swept only on green: an unresolved analysis survives every retry.
125
- sweepOnGreen: [ESCALATION, ...(options.sweepOnGreen ?? [])],
150
+ sweepOnGreen: healthSweep(options),
126
151
  })
127
152
 
128
153
  /**
129
154
  * Loop until the suite is green: check, and on red run `fix` — escalating
130
155
  * first once `FIX_CAP` fixes are spent, or a judge calls the failure
131
- * identical to the previous round's.
156
+ * identical to the previous round's. An escalation re-runs the suite once
157
+ * first; a green re-run ends the loop.
132
158
  */
133
159
  export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}): Promise<void> =>
134
160
  kit.healthy(guarded(fix, options.guard), {
@@ -138,6 +164,8 @@ export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}):
138
164
  fixesSoFar: options.fixesSoFar ?? 0,
139
165
  sameFailure,
140
166
  escalations: options.escalations ?? { rounds: 0 },
167
+ recheck: () =>
168
+ recheck("health.recheck", "health.check", { sweepOnGreen: healthSweep(options) }),
141
169
  escalation: guardedSteps(
142
170
  options.guard,
143
171
  options.describeGuard,
@@ -147,13 +175,14 @@ export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}):
147
175
  })
148
176
 
149
177
  /**
150
- * Hold the process at `<name>.blocked` until the suite is green. `guard` runs
178
+ * Hold the process at `<name>.blocked` until the suite is green, re-running a
179
+ * red check once as `<name>.recheck` before resting. `guard` runs
151
180
  * after each `blocked` landing only — never after a check, whose refusal would
152
181
  * re-run the same script forever.
153
182
  */
154
183
  export const gate = (name: string, message: string, guard?: Guard): Promise<void> =>
155
184
  kit.gate(
156
185
  name,
157
- () => baseline("check"),
186
+ async () => (await baseline("check")) || recheck("recheck", `${name}.check`),
158
187
  guarded(() => human("blocked", { message, label: "Baseline is red", file: FEEDBACK }), guard),
159
188
  )
@@ -0,0 +1,60 @@
1
+ @inmem
2
+ Feature: a red check is re-run once before it reaches a person
3
+
4
+ Before the fix loop escalates, `healthy` re-runs the suite as
5
+ `health.recheck`. Green there ends the loop as if the check had passed, and
6
+ the re-check's landing records `Gtd-Mark: flaky=health.check`. Red again
7
+ escalates as before.
8
+
9
+ Background:
10
+ Given a gtd config with:
11
+ """
12
+ import { healthy } from "@pmelab/gtd/workflows"
13
+ import { agent, human } from "@pmelab/gtd/flows"
14
+
15
+ export const held = async () => {
16
+ await agent("first", "go")
17
+ await healthy(() => agent("fix", "go"))
18
+ await human("done")
19
+ }
20
+ export default held
21
+ """
22
+ And step "first" writes "src/a.ts" with:
23
+ """
24
+ export const a = 1
25
+ """
26
+ And step "health.check" writes ".gtd/FEEDBACK.md" with:
27
+ """
28
+ 1 test failing
29
+ """
30
+ And step "fix" deletes ".gtd/FEEDBACK.md"
31
+ And judge step "health.judge" answers:
32
+ """
33
+ [{"id":"verdict","answer":"progress","p":0.9}]
34
+ """
35
+
36
+ Scenario: a re-check that passes ends the loop without a person, recorded as flaky
37
+ Given step "health.recheck" deletes ".gtd/FEEDBACK.md"
38
+ When gtd starts workflow "held"
39
+ And the process is driven
40
+ Then the process rests at "done"
41
+ And the commit subjects end with:
42
+ """
43
+ gtd(agent): fix → health.check
44
+ gtd(check): health.check → health.judge
45
+ gtd(judge): health.judge → health.recheck
46
+ gtd(check): health.recheck → done
47
+ """
48
+ And the last commit body contains "Gtd-Mark: flaky=health.check"
49
+ And ".gtd/FEEDBACK.md" does not exist
50
+
51
+ Scenario: a re-check that stays red escalates, with nothing recorded as flaky
52
+ Given step "health.recheck" writes ".gtd/FEEDBACK.md" with:
53
+ """
54
+ 1 test still failing
55
+ """
56
+ When gtd starts workflow "held"
57
+ And the process is driven
58
+ Then the process rests at "health.describe"
59
+ And the last commit subject is "gtd(check): health.recheck → health.describe"
60
+ And no commit body contains "Gtd-Mark: flaky="
@@ -92,14 +92,14 @@ What each change does next (then run \`gtd land\`):
92
92
 
93
93
  export const qualityUnresolvedMessage = (): string =>
94
94
  `The quality lap's fix turns hit their cap and \`.gtd/QUALITY.md\` still
95
- holds findings. Nothing reviews the change after this, so the process
96
- will not end with them open.
95
+ holds findings. Nothing after this fixes them, so the process will not
96
+ go on with them open.
97
97
 
98
98
  Fix what matters by hand, then delete or empty \`.gtd/QUALITY.md\` —
99
99
  dropping a finding is your call to make.
100
100
 
101
101
  What each change does next (then run \`gtd land\`):
102
- - **Delete or empty \`.gtd/QUALITY.md\`** — the suite runs on your edits, then the process ends (**idle**).
102
+ - **Delete or empty \`.gtd/QUALITY.md\`** — the suite runs on your edits, then the next slice is built, or after the last one you review the change (**build.review.reviewing**).
103
103
  - **Anything else** — rests here again.
104
104
  `
105
105
 
@@ -144,8 +144,10 @@ reword a criterion in place, append a new one with the next unused
144
144
  number, delete a dropped one — never renumber, never reuse an ID. When
145
145
  the file holds no list (a review loop-back rewrites it), recover the
146
146
  last settled one from history (\`git log -p -- .gtd/REQUIREMENTS.md\`)
147
- and carry its IDs over. Landing is refused while a criterion has no ID or
148
- two share one.
147
+ and carry its IDs over: after a review a built criterion keeps its ID,
148
+ reworded in place when the review changes its behaviour, and new
149
+ behaviour gets a new ID. Landing is refused while a criterion has no ID
150
+ or two share one.
149
151
  `
150
152
 
151
153
  const riskTierBlock = `
@@ -1,121 +0,0 @@
1
- @inmem
2
- Feature: human review is the review door's, never the feature workflow's
3
-
4
- A feature process ends once its last slice lands: the quality lap is its
5
- last look at the change, and no human review rest follows it. Human review
6
- runs only when someone opens the `review` door.
7
-
8
- Background:
9
- Given the shipped workflows, as gtd install scaffolds them
10
- And an environment variable "GTD_QUALITYREVIEWS" set to ""
11
- And an environment variable "GTD_FLAGMODE" set to "false"
12
- And a file "src/calc.ts" with:
13
- """
14
- export const add = (a: number, b: number) => a + b
15
- """
16
- And the working tree is committed as "feat: add calculator"
17
- And a file "TODO.md" with:
18
- """
19
- # Add subtract
20
-
21
- subtract(a, b) returns the difference.
22
- """
23
- And step "unwind" changes nothing
24
- And step "start-gate.check" changes nothing
25
- And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
26
- """
27
- # Requirements
28
-
29
- ## Concerns
30
-
31
- ### Subtract
32
-
33
- - `subtract(a, b)` returns `a - b`.
34
-
35
- ## Acceptance Criteria
36
-
37
- - AC-1: subtract(a, b) returns a - b.
38
- """
39
- And step "design.gate.answer" changes nothing
40
- And step "seam.naive" writes "src/calc.test.ts" with:
41
- """
42
- it("[AC-1] subtracts", () => {})
43
- """
44
- And step "seam.naive" writes "src/calc.ts" with:
45
- """
46
- export const add = (a: number, b: number) => a + b
47
- export const subtract = (a: number, b: number) => a - b
48
- """
49
- And step "seam.check" changes nothing
50
- And step "implement.applying" runs its script
51
- And step "implement.health.check" changes nothing
52
- And step "implement.closing" runs its script
53
- And judge step "implement.coverage" answers:
54
- """
55
- [{"id":"AC-1","answer":"yes","p":0.9}]
56
- """
57
-
58
- Scenario: a feature process ends at idle with no review rest
59
- When the process is driven
60
- Then the process rests at "idle"
61
- And "src/calc.ts" contains "subtract"
62
- And ".gtd/REVIEW.md" does not exist
63
- And the commit subjects end with:
64
- """
65
- gtd(human): design.gate.answer → seam.naive
66
- gtd(agent): seam.naive → seam.check
67
- gtd(check): seam.check → implement.applying
68
- gtd(check): implement.applying → implement.health.check
69
- gtd(check): implement.health.check → implement.coverage
70
- gtd(judge): implement.coverage → implement.closing
71
- gtd(check): implement.closing → idle
72
- """
73
-
74
- Scenario: the review door still runs human review over the finished feature
75
- When the process is driven
76
- Then the process rests at "idle"
77
-
78
- Given step "review-gate.check" changes nothing
79
- And step "build.health.check" changes nothing
80
- And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
81
- """
82
- # Review: abc1234
83
-
84
- <!-- base: 0000000 -->
85
-
86
- ## calc
87
-
88
- - [ ] ./src/calc.ts#2-2
89
- new subtract function
90
- """
91
- When gtd opens door "review" with "HEAD~11"
92
- And the process is driven
93
- Then the process rests at "build.review.await-review"
94
- And the resting step's content contains the hash of the commit "feat: add calculator"
95
-
96
- Given a file ".gtd/REVIEW.md" with:
97
- """
98
- # Review: abc1234
99
-
100
- <!-- base: 0000000 -->
101
-
102
- ## calc
103
-
104
- - [x] ./src/calc.ts#2-2
105
- new subtract function
106
- """
107
- And step "build.review.closing" runs its script
108
- When gtd lands
109
- And the process is driven
110
- Then the process rests at "idle"
111
- And ".gtd/REVIEW.md" does not exist
112
- And the commit subjects end with:
113
- """
114
- gtd(check): implement.closing → idle
115
- gtd(human): review-gate.check
116
- gtd(check): review-gate.check → build.health.check
117
- gtd(check): build.health.check → build.review.reviewing
118
- gtd(agent): build.review.reviewing → build.review.await-review
119
- gtd(human): build.review.await-review → build.review.closing
120
- gtd(check): build.review.closing → idle
121
- """