@pmelab/gtd 45.0.0 → 45.2.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 (41) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +33 -14
  3. package/claude/hooks/pr.ts +342 -0
  4. package/claude/hooks/register.tsx +148 -5
  5. package/claude/types/index.d.ts +18 -0
  6. package/dist/chunks/{reserved-ty1mtlnS.mjs → reserved-FWrjy7zo.mjs} +29 -4
  7. package/dist/chunks/{review-BmoH-9im.mjs → review-Dhl4vvDe.mjs} +17 -6
  8. package/dist/chunks/{steering-D4LUQ-av.mjs → steering-D7hWeMgE.mjs} +1 -1
  9. package/dist/chunks/{tail-CM3sVoAb.mjs → tail-B-Es6Hwj.mjs} +1 -1
  10. package/dist/gtd.bundle.mjs +7 -5
  11. package/dist/steering.mjs +1 -1
  12. package/dist/testkit.mjs +8 -6
  13. package/dist/workflow-kit.mjs +1 -1
  14. package/dist/workflows/cleanup.mjs +2 -2
  15. package/dist/workflows/feature.mjs +3 -3
  16. package/dist/workflows/fix.mjs +3 -3
  17. package/dist/workflows/review.mjs +4 -15
  18. package/dist/workflows/rollout.mjs +1 -1
  19. package/dist/workflows/tests.mjs +193 -0
  20. package/dist/workflows.mjs +229 -19
  21. package/package.json +6 -1
  22. package/src/install/ConfigScaffold.ts +2 -1
  23. package/src/install/Install.ts +5 -4
  24. package/src/steering/review.test.ts +6 -0
  25. package/src/steering/review.ts +4 -1
  26. package/src/testkit/steps.ts +5 -0
  27. package/src/workflows/index.ts +2 -1
  28. package/src/workflows/review/review-door.feature +38 -0
  29. package/src/workflows/review/review.ts +3 -16
  30. package/src/workflows/shared/lenses.ts +5 -1
  31. package/src/workflows/shared/options.ts +21 -1
  32. package/src/workflows/shared/review.ts +19 -2
  33. package/src/workflows/shared/skills.test.ts +2 -0
  34. package/src/workflows/shared/skills.ts +3 -0
  35. package/src/workflows/shared/text.ts +5 -2
  36. package/src/workflows/shared/vars.ts +8 -0
  37. package/src/workflows/tests/index.ts +6 -0
  38. package/src/workflows/tests/tests-options.feature +52 -0
  39. package/src/workflows/tests/tests.feature +266 -0
  40. package/src/workflows/tests/tests.test.ts +51 -0
  41. package/src/workflows/tests/tests.ts +270 -0
@@ -124,6 +124,8 @@ export interface ComposedReview {
124
124
  readonly unresolved?: true
125
125
  /** A fix turn removed its chunk while renaming another. */
126
126
  readonly unclear?: true
127
+ /** Labels for the first two boxes; absent: fix as suggested, won't fix. Read by position, so only the wording changes. */
128
+ readonly answers?: readonly [string, string]
127
129
  }[]
128
130
  readonly fixed: readonly { readonly title: string; readonly commit: string }[]
129
131
  readonly noise: readonly { readonly title: string; readonly p: number }[]
@@ -170,7 +172,8 @@ export const composedReview = (base: string, report: ComposedReview): string =>
170
172
  }
171
173
  const chunk = (title: string, body: string): string => `## ${title}\n\n${body.trim()}`
172
174
  // The lead-in line keeps a body that ends in a list from absorbing the boxes.
173
- const answerBoxes = `${ANSWER_LEAD}\n\n- [ ] ${ANSWER_FIX}\n- [ ] ${ANSWER_WONT_FIX}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`
175
+ const answerBoxes = ([fix, wontFix]: readonly [string, string] = [ANSWER_FIX, ANSWER_WONT_FIX]) =>
176
+ `${ANSWER_LEAD}\n\n- [ ] ${fix}\n- [ ] ${wontFix}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`
174
177
  const conflicts = (report.standards?.undecided ?? []).map((c) =>
175
178
  chunk(unique(conflictTitle(c)), conflictBody(c)),
176
179
  )
@@ -208,7 +211,7 @@ export const composedReview = (base: string, report: ComposedReview): string =>
208
211
  : n.unclear === true
209
212
  ? `_A fix turn removed this while renaming another finding — check whether it was fixed._\n\n${unboxed(n.body)}`
210
213
  : unboxed(n.body) || "_No detail given._"
211
- }\n\n${answerBoxes}`,
214
+ }\n\n${answerBoxes(n.answers)}`,
212
215
  ),
213
216
  ),
214
217
  ...(report.fixed.length === 0
@@ -16,6 +16,14 @@ export const testFileGlobs: readonly string[] = [
16
16
 
17
17
  export const defaultFlagRegistry = "src/flags/index.ts"
18
18
 
19
+ /** The `tests` workflow's own process settings: where it looks for skipped tests, and how a skip reads. */
20
+ export const testsDefaults: Readonly<Record<string, string>> = {
21
+ testFiles: testFileGlobs.join(", "),
22
+ // JS/TS runners' `.skip`/`.todo` and `x`-prefixed blocks, and cucumber's skip tags.
23
+ skipPattern:
24
+ "^\\s*(?:(?:describe|context|suite|it|test|specify)\\.(?:skip|todo)\\b|x(?:describe|context|it|test|specify)\\s*\\(|(?:@\\S+\\s+)*@(?:skip|ignore)(?:\\s|$))",
25
+ }
26
+
19
27
  /** Process settings. */
20
28
  export const defaults: Readonly<Record<string, string>> = {
21
29
  reviewBase: "",
@@ -0,0 +1,6 @@
1
+ import { tests } from "./tests.js"
2
+
3
+ export { tests, testsFlow } from "./tests.js"
4
+ export * from "../shared/reserved.js"
5
+
6
+ export default tests
@@ -0,0 +1,52 @@
1
+ @inmem
2
+ Feature: the tests workflow's skip detection is a workflow option
3
+
4
+ `testFiles` (globs) and `skipPattern` (a per-line regular expression) default
5
+ to JavaScript/TypeScript runners and cucumber tags; another stack sets its
6
+ own on the factory.
7
+
8
+ Background:
9
+ Given a gtd config with:
10
+ """
11
+ export {
12
+ feature as default,
13
+ feature,
14
+ defaults,
15
+ envDefaults,
16
+ summary,
17
+ steering,
18
+ skills,
19
+ access,
20
+ } from "@pmelab/gtd/workflows/feature"
21
+ import { tests as shippedTests } from "@pmelab/gtd/workflows/tests"
22
+ export const tests = shippedTests({
23
+ testFiles: "tests/test_*.py",
24
+ skipPattern: "@pytest\\.mark\\.skip\\b",
25
+ })
26
+ """
27
+ And a file "tests/test_calc.py" with:
28
+ """
29
+ @pytest.mark.skip
30
+ def test_add(): pass
31
+ """
32
+ And a file "src/calc.test.ts" with:
33
+ """
34
+ it.skip("adds", () => {})
35
+ """
36
+ And the working tree is committed as "test: python"
37
+ And step "tests-gate.check" changes nothing
38
+
39
+ Scenario: only the configured files and pattern count as skipped
40
+ Given step "tests.sweeping" is prompted with "tests/test_calc.py:1 — @pytest.mark.skip"
41
+ And step "tests.sweeping" changes nothing
42
+ When gtd starts workflow "tests"
43
+ Then driving the process is refused with "tests/test_calc.py has no section"
44
+ And driving the process is refused without "src/calc.test.ts"
45
+
46
+ Scenario Outline: an unusable skipPattern is refused before any step
47
+ Then starting workflow "tests" with "--var skipPattern=<pattern>" is refused with "<refusal>"
48
+
49
+ Examples:
50
+ | pattern | refusal |
51
+ | .* | skipPattern must not match the empty string |
52
+ | ( | skipPattern is not a valid regular expression |
@@ -0,0 +1,266 @@
1
+ @inmem
2
+ Feature: the tests workflow revives skipped tests or proposes their deletion
3
+
4
+ `gtd --workflow tests` sweeps the repository for skipped tests and test-audit
5
+ findings into `.gtd/TESTS.md`, one section per test file. Each file is
6
+ rewritten in its own turn until nothing in it is skipped and the suite is
7
+ green. A test the agent calls obsolete stays skipped and becomes a proposal in
8
+ `.gtd/RETIRE.md`; the human review asks for one tick on each — delete it, keep
9
+ it skipped, or an own answer. Only a person's tick deletes a test.
10
+
11
+ Background:
12
+ Given 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
+ export { tests } from "@pmelab/gtd/workflows/tests"
25
+ """
26
+ And a file "src/calc.ts" with:
27
+ """
28
+ export const add = (a: number, b: number) => a + b
29
+ """
30
+ And a file "src/calc.test.ts" with:
31
+ """
32
+ it.skip("adds", () => expect(add(1, 2)).toBe(3))
33
+ """
34
+ And a file "src/legacy.test.ts" with:
35
+ """
36
+ it.skip("calls the v1 api", () => {})
37
+ """
38
+ And the working tree is committed as "feat: calculator"
39
+ And step "tests-gate.check" changes nothing
40
+ And step "tests.sweeping" writes ".gtd/TESTS.md" with:
41
+ """
42
+ # Tests
43
+
44
+ ## src/calc.test.ts
45
+
46
+ "adds" is skipped.
47
+
48
+ ## src/legacy.test.ts
49
+
50
+ "calls the v1 api" is skipped.
51
+ """
52
+ And step "tests.rewriting", when prompted with "\"adds\" is skipped", writes "src/calc.test.ts" with:
53
+ """
54
+ it("adds", () => expect(add(1, 2)).toBe(3))
55
+ """
56
+ And step "tests.rewriting", when prompted with "\"calls the v1 api\" is skipped", writes ".gtd/RETIRE.md" with:
57
+ """
58
+ # Retire
59
+
60
+ ## calls the v1 api
61
+
62
+ src/legacy.test.ts tests the v1 api, which is gone.
63
+ """
64
+ And step "tests.health.check" changes nothing
65
+ And step "tests.closing" runs its script
66
+ And step "build.review.composing" runs its script
67
+ And step "build.review.closing" runs its script
68
+
69
+ Scenario: the revivable test is unskipped and the obsolete one is proposed for deletion
70
+ When gtd starts workflow "tests"
71
+ And the process is driven
72
+ Then the process rests at "build.review.await-review"
73
+ And the commit subjects end with:
74
+ """
75
+ gtd(check): tests-gate.check → tests.sweeping
76
+ gtd(agent): tests.sweeping → tests.rewriting
77
+ gtd(agent): tests.rewriting → tests.health.check
78
+ gtd(check): tests.health.check → tests.rewriting
79
+ gtd(agent): tests.rewriting → tests.closing
80
+ gtd(check): tests.closing → build.review.composing
81
+ gtd(check): build.review.composing → build.review.await-review
82
+ """
83
+ And "src/calc.test.ts" contains "it(\"adds\""
84
+ And "src/legacy.test.ts" contains "it.skip"
85
+ And ".gtd/TESTS.md" does not exist
86
+ And ".gtd/RETIRE.md" does not exist
87
+ And ".gtd/REVIEW.md" contains:
88
+ """
89
+ ## calls the v1 api
90
+
91
+ Delete this skipped test.
92
+
93
+ src/legacy.test.ts tests the v1 api, which is gone.
94
+
95
+ Tick exactly one:
96
+
97
+ - [ ] delete it
98
+ - [ ] keep it skipped
99
+ - [ ] _your answer_
100
+ """
101
+ And the resting step's content contains "tests removed"
102
+
103
+ Scenario: signing off with an unticked proposal is refused
104
+ When gtd starts workflow "tests"
105
+ And the process is driven
106
+ Then landing is refused with "tick exactly one box"
107
+
108
+ Scenario: a ticked delete it has the test deleted, then the review comes back
109
+ Given step "build.review.fix.nits.fixing" deletes "src/legacy.test.ts"
110
+ And step "build.health.check" changes nothing
111
+ When gtd starts workflow "tests"
112
+ And the process is driven
113
+ And the text "- [ ] delete it" in ".gtd/REVIEW.md" is replaced with "- [x] delete it"
114
+ And gtd lands
115
+ And the process is driven
116
+ Then the process rests at "build.review.await-review"
117
+ And "src/legacy.test.ts" does not exist
118
+ When gtd lands
119
+ And the process is driven
120
+ Then the process rests at "idle"
121
+
122
+ Scenario: a ticked keep it skipped leaves the test skipped and ends the process
123
+ Given judge step "build.review.standards.judge" answers:
124
+ """
125
+ [{"id":"standard-1","answer":"no","p":0.9}]
126
+ """
127
+ When gtd starts workflow "tests"
128
+ And the process is driven
129
+ And the text "- [ ] keep it skipped" in ".gtd/REVIEW.md" is replaced with "- [x] keep it skipped"
130
+ And gtd lands
131
+ And the process is driven
132
+ Then the process rests at "idle"
133
+ And "src/legacy.test.ts" contains "it.skip"
134
+ And the commit "gtd(human): build.review.await-review → build.review.standards.judge" has a body containing "Gtd-Mark: wontfix=calls the v1 api"
135
+
136
+ Scenario: a rewrite leaving a test skipped without a proposal is refused
137
+ Given step "tests.rewriting", when prompted with "\"calls the v1 api\" is skipped", writes ".gtd/RETIRE.md" with:
138
+ """
139
+ # Retire
140
+ """
141
+ When gtd starts workflow "tests"
142
+ Then driving the process is refused with "src/legacy.test.ts:1 still skipped"
143
+
144
+ Scenario: a rewrite deleting a test file is refused
145
+ Given step "tests.rewriting" deletes "src/legacy.test.ts"
146
+ When gtd starts workflow "tests"
147
+ Then driving the process is refused with "src/legacy.test.ts deleted"
148
+
149
+ Scenario: a sweep deleting a test file is refused
150
+ Given step "tests.sweeping" deletes "src/legacy.test.ts"
151
+ When gtd starts workflow "tests"
152
+ Then driving the process is refused with "src/legacy.test.ts deleted"
153
+
154
+ Scenario: a sweep changing anything but its plan is refused
155
+ Given step "tests.sweeping" writes "src/calc.ts" with:
156
+ """
157
+ export const add = (a: number, b: number) => b + a
158
+ """
159
+ When gtd starts workflow "tests"
160
+ Then driving the process is refused with "the sweep changed src/calc.ts"
161
+
162
+ Scenario: a proposal naming its file at the end of a sentence counts
163
+ Given step "tests.rewriting", when prompted with "\"calls the v1 api\" is skipped", writes ".gtd/RETIRE.md" with:
164
+ """
165
+ # Retire
166
+
167
+ ## calls the v1 api
168
+
169
+ The v1 api is gone, so drop it from src/legacy.test.ts.
170
+ """
171
+ When gtd starts workflow "tests"
172
+ And the process is driven
173
+ Then the process rests at "build.review.await-review"
174
+
175
+ Scenario: a sweep planning a file that is no test file is refused
176
+ Given step "tests.sweeping" writes ".gtd/TESTS.md" with:
177
+ """
178
+ # Tests
179
+
180
+ ## src/calc.ts
181
+
182
+ Nothing is skipped.
183
+
184
+ ## src/calc.test.ts
185
+
186
+ "adds" is skipped.
187
+
188
+ ## src/legacy.test.ts
189
+
190
+ "calls the v1 api" is skipped.
191
+ """
192
+ When gtd starts workflow "tests"
193
+ Then driving the process is refused with "is no test file"
194
+
195
+ Scenario: a proposal naming its file as ./path counts
196
+ Given step "tests.rewriting", when prompted with "\"calls the v1 api\" is skipped", writes ".gtd/RETIRE.md" with:
197
+ """
198
+ # Retire
199
+
200
+ ## calls the v1 api
201
+
202
+ ./src/legacy.test.ts tests the v1 api, which is gone.
203
+ """
204
+ When gtd starts workflow "tests"
205
+ And the process is driven
206
+ Then the process rests at "build.review.await-review"
207
+
208
+ Scenario: rewrites that change no code and propose nothing end the process
209
+ Given a file "src/legacy.test.ts" with:
210
+ """
211
+ it("calls the v2 api", () => {})
212
+ """
213
+ And a file "src/calc.test.ts" with:
214
+ """
215
+ it("adds", () => expect(add(1, 2)).toBe(3))
216
+ """
217
+ And the working tree is committed as "test: nothing skipped"
218
+ And step "tests.rewriting" from now on changes nothing
219
+ When gtd starts workflow "tests"
220
+ And the process is driven
221
+ Then the process rests at "idle"
222
+
223
+ Scenario: one proposal covers one skip, not every skip in its file
224
+ Given a file "src/legacy.test.ts" with:
225
+ """
226
+ it.skip("calls the v1 api", () => {})
227
+ it.skip("calls the v1 batch api", () => {})
228
+ """
229
+ And the working tree is committed as "test: another legacy test"
230
+ When gtd starts workflow "tests"
231
+ Then driving the process is refused with "src/legacy.test.ts:2 still skipped"
232
+
233
+ Scenario: a sweep leaving out a file with a skipped test is refused
234
+ Given step "tests.sweeping" writes ".gtd/TESTS.md" with:
235
+ """
236
+ # Tests
237
+
238
+ ## src/calc.test.ts
239
+
240
+ "adds" is skipped.
241
+ """
242
+ When gtd starts workflow "tests"
243
+ Then driving the process is refused with "src/legacy.test.ts has no section"
244
+
245
+ Scenario: the sweep is told every skipped test, path and line
246
+ Given step "tests.sweeping" is prompted with "src/legacy.test.ts:1 — it.skip(\"calls the v1 api\", () => {})"
247
+ When gtd starts workflow "tests"
248
+ And the process is driven
249
+ Then the process rests at "build.review.await-review"
250
+
251
+ Scenario: a sweep with nothing to do ends the process
252
+ Given a file "src/calc.test.ts" with:
253
+ """
254
+ it("adds", () => expect(add(1, 2)).toBe(3))
255
+ """
256
+ And the file "src/legacy.test.ts" is deleted
257
+ And the working tree is committed as "test: revive by hand"
258
+ And step "tests.sweeping" from now on changes nothing
259
+ When gtd starts workflow "tests"
260
+ And the process is driven
261
+ Then the process rests at "idle"
262
+
263
+ Scenario: gtd door tests starts the workflow
264
+ When gtd opens door "tests"
265
+ And the process is driven
266
+ Then the process rests at "build.review.await-review"
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { DEFAULT_SKIP_PATTERN, skipsIn } from "./tests.js"
3
+
4
+ const skipped = (text: string): readonly number[] =>
5
+ skipsIn([{ path: "a", text }], new RegExp(DEFAULT_SKIP_PATTERN)).map((s) => s.line)
6
+
7
+ describe("the default skipPattern", () => {
8
+ it.each([
9
+ 'it.skip("x", () => {})',
10
+ 'test.skip("x", () => {})',
11
+ 'describe.skip("x", () => {})',
12
+ 'it.todo("x")',
13
+ 'xit("x", () => {})',
14
+ 'xdescribe("x", () => {})',
15
+ " @inmem @skip",
16
+ "@ignore",
17
+ ])("reads %s as skipped", (line) => {
18
+ expect(skipped(line)).toEqual([1])
19
+ })
20
+
21
+ it.each([
22
+ 'it("x", () => {})',
23
+ 'it.skipIf(windows)("x", () => {})',
24
+ 'it.only("x", () => {})',
25
+ "@skipped-by-nobody",
26
+ "fixit()",
27
+ "user@skip.example",
28
+ ' "it.skip(x)",',
29
+ "// xit( is how jasmine skips",
30
+ "@skip-ci",
31
+ ])("does not read %s as skipped", (line) => {
32
+ expect(skipped(line)).toEqual([])
33
+ })
34
+ })
35
+
36
+ describe("skipsIn", () => {
37
+ it("names every skipped line by path and 1-based line, trimmed", () => {
38
+ expect(
39
+ skipsIn(
40
+ [
41
+ { path: "a.test.ts", text: 'it("a", f)\n it.skip("b", f)\n' },
42
+ { path: "b.feature", text: "@skip\nScenario: c" },
43
+ ],
44
+ new RegExp(DEFAULT_SKIP_PATTERN),
45
+ ),
46
+ ).toEqual([
47
+ { path: "a.test.ts", line: 2, text: 'it.skip("b", f)' },
48
+ { path: "b.feature", line: 1, text: "@skip" },
49
+ ])
50
+ })
51
+ })