@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +66 -58
- package/dist/chunks/{reserved-C3PgP7XB.mjs → reserved-Qt5FM6Ia.mjs} +29 -10
- package/dist/chunks/{tail-Bkxki2nK.mjs → tail-tAHx_AHe.mjs} +1 -1
- package/dist/gtd.bundle.mjs +14 -5
- package/dist/testkit.mjs +14 -5
- package/dist/workflow-kit.mjs +12 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +98 -37
- 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 +136 -45
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +3 -2
- package/src/cli/Cli.ts +4 -3
- package/src/cli/Metrics.test.ts +31 -0
- package/src/cli/Metrics.ts +8 -0
- package/src/workflow-kit/health.ts +15 -1
- package/src/workflows/cleanup/cleanup.test.ts +2 -0
- package/src/workflows/feature/coverage.feature +5 -5
- package/src/workflows/feature/criteria.test.ts +20 -1
- package/src/workflows/feature/criteria.ts +24 -0
- package/src/workflows/feature/fast-path.feature +4 -4
- package/src/workflows/feature/feature.ts +82 -22
- package/src/workflows/feature/flag-mode.feature +2 -2
- package/src/workflows/feature/one-step-land.feature +4 -4
- package/src/workflows/feature/planning.ts +3 -2
- package/src/workflows/feature/quality-unresolved.feature +6 -6
- package/src/workflows/feature/refactor-absorbs.feature +3 -3
- package/src/workflows/feature/refactor-first.feature +5 -5
- package/src/workflows/feature/refactor-rebuild.feature +2 -2
- package/src/workflows/feature/refactor-review.feature +8 -8
- package/src/workflows/feature/refactor.test.ts +4 -0
- package/src/workflows/feature/review-gate.feature +271 -0
- package/src/workflows/feature/seam-rejection.feature +4 -4
- package/src/workflows/feature/seam.feature +2 -2
- package/src/workflows/feature/seam.ts +17 -7
- package/src/workflows/feature/slicing.feature +63 -4
- package/src/workflows/rollout/rollout.feature +16 -0
- package/src/workflows/rollout/rollout.test.ts +2 -0
- package/src/workflows/shared/health.test.ts +2 -0
- package/src/workflows/shared/health.ts +34 -5
- package/src/workflows/shared/recheck.feature +60 -0
- package/src/workflows/shared/text.ts +7 -5
- 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
|
-
|
|
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
|
|
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
|
|
96
|
-
|
|
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
|
|
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
|
|
148
|
-
|
|
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
|
-
"""
|