@pmelab/gtd 40.0.0 → 42.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 +41 -30
- package/dist/chunks/{reserved-Qt5FM6Ia.mjs → reserved-D7MNY9QJ.mjs} +66 -18
- package/dist/chunks/{steering-DVILanVf.mjs → steering-oCDdQlpP.mjs} +1 -1
- package/dist/chunks/tail-D2o2h5cc.mjs +246 -0
- package/dist/flows.mjs +11 -1
- package/dist/gtd.bundle.mjs +12 -2
- package/dist/steering.mjs +1 -1
- package/dist/testkit.mjs +15 -3
- package/dist/web/main.js +1 -1
- package/dist/workflow-kit.mjs +30 -12
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +44 -24
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +282 -51
- package/package.json +1 -1
- package/src/flows/scripts.ts +15 -0
- package/src/steering/review.test.ts +2 -20
- package/src/steering/review.ts +2 -1
- package/src/testkit/FlowScripts.test.ts +17 -0
- package/src/testkit/FlowScripts.ts +15 -0
- package/src/testkit/steps.ts +27 -0
- package/src/workflow-kit/health.ts +42 -13
- package/src/workflows/feature/coverage.feature +3 -3
- package/src/workflows/feature/fast-path.feature +2 -2
- package/src/workflows/feature/feature.test.ts +2 -37
- package/src/workflows/feature/feature.ts +12 -14
- package/src/workflows/feature/flag-mode.feature +2 -2
- package/src/workflows/feature/lens-review.feature +234 -0
- package/src/workflows/feature/one-step-land.feature +2 -2
- package/src/workflows/feature/quality-unresolved.feature +47 -32
- 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 +7 -7
- package/src/workflows/feature/refactor-review.feature +19 -100
- package/src/workflows/feature/refactor.test.ts +12 -27
- package/src/workflows/feature/refactor.ts +0 -1
- package/src/workflows/feature/review-gate.feature +6 -6
- package/src/workflows/feature/review.test.ts +29 -25
- package/src/workflows/feature/review.ts +53 -10
- package/src/workflows/feature/seam-rejection.feature +2 -2
- package/src/workflows/feature/seam.feature +2 -2
- package/src/workflows/feature/seam.test.ts +1 -1
- package/src/workflows/feature/slicing.feature +6 -6
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/health.test.ts +30 -0
- package/src/workflows/shared/health.ts +12 -9
- package/src/workflows/shared/lenses.test.ts +39 -0
- package/src/workflows/shared/lenses.ts +176 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/progress.feature +124 -0
- package/src/workflows/shared/tail.ts +19 -4
- package/src/workflows/shared/text.test.ts +68 -1
- package/src/workflows/shared/text.ts +126 -13
- package/src/workflows/shared/vars.ts +1 -0
- package/dist/chunks/tail-tAHx_AHe.mjs +0 -112
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
restoreScript,
|
|
13
13
|
revertScript,
|
|
14
14
|
showScript,
|
|
15
|
+
writeScript,
|
|
15
16
|
} from "../flows/index.js"
|
|
16
17
|
import type { InMemRepo } from "./InMemRepo.js"
|
|
17
18
|
import type { ScriptedCommand } from "./Layers.js"
|
|
@@ -60,6 +61,19 @@ const recognizeMove = (repo: InMemRepo, script: string): FlowScriptResult | unde
|
|
|
60
61
|
return ok()
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
const recognizeWrite = (repo: InMemRepo, script: string): FlowScriptResult | undefined => {
|
|
65
|
+
const lines = shellLines(script.split("\n"))
|
|
66
|
+
if (!lines[3]?.startsWith("printf '%s' ")) return undefined
|
|
67
|
+
const [path] = words(lines[2] ?? "")
|
|
68
|
+
const [, content, to] = words(lines[3])
|
|
69
|
+
const remove = words(lines.find((l) => l.startsWith("rm -rf -- ")) ?? "")
|
|
70
|
+
if (path === undefined || content === undefined || to !== path) return undefined
|
|
71
|
+
if (writeScript(path, content, remove) !== script) return undefined
|
|
72
|
+
repo.writeFile(path, content)
|
|
73
|
+
removeAll(repo, remove)
|
|
74
|
+
return ok()
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
const recognizeShow = (repo: InMemRepo, script: string): FlowScriptResult | undefined => {
|
|
64
78
|
const line = script.split("\n").find((l) => l.startsWith("git show ")) ?? ""
|
|
65
79
|
const [source, to] = words(line)
|
|
@@ -356,6 +370,7 @@ export const applyFlowScript = (
|
|
|
356
370
|
recognizeRemove(repo, script) ??
|
|
357
371
|
recognizeMove(repo, script) ??
|
|
358
372
|
recognizeShow(repo, script) ??
|
|
373
|
+
recognizeWrite(repo, script) ??
|
|
359
374
|
recognizePatch(repo, script) ??
|
|
360
375
|
recognizeRestore(repo, script) ??
|
|
361
376
|
recognizeCheck(repo, commands, script)
|
package/src/testkit/steps.ts
CHANGED
|
@@ -127,6 +127,19 @@ Given("the file {string} is deleted", (world: TestkitWorld, path: string) => {
|
|
|
127
127
|
world.repo().remove(path)
|
|
128
128
|
})
|
|
129
129
|
|
|
130
|
+
// A human's edit in place, so a scenario need not restate a file gtd wrote.
|
|
131
|
+
Given(
|
|
132
|
+
"the text {string} in {string} is replaced with {string}",
|
|
133
|
+
(world: TestkitWorld, text: string, path: string, replacement: string) => {
|
|
134
|
+
const content = world.repo().read(path) ?? ""
|
|
135
|
+
assert.ok(content.includes(text), `${path}: ${content}`)
|
|
136
|
+
world.repo().write(
|
|
137
|
+
path,
|
|
138
|
+
content.replace(text, () => replacement),
|
|
139
|
+
)
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
|
|
130
143
|
Given("the working tree is committed as {string}", (world: TestkitWorld, message: string) => {
|
|
131
144
|
world.repo().commit(message)
|
|
132
145
|
})
|
|
@@ -341,6 +354,20 @@ Then("{string} contains {string}", (world: TestkitWorld, path: string, text: str
|
|
|
341
354
|
assert.ok(world.repo().read(path)?.includes(text), `${path}: ${world.repo().read(path)}`)
|
|
342
355
|
})
|
|
343
356
|
|
|
357
|
+
Then("{string} contains:", (world: TestkitWorld, path: string, text: string) => {
|
|
358
|
+
assert.ok(world.repo().read(path)?.includes(text), `${path}: ${world.repo().read(path)}`)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
Then(
|
|
362
|
+
"{string} contains the short hash of the commit {string}",
|
|
363
|
+
(world: TestkitWorld, path: string, subject: string) => {
|
|
364
|
+
const repo = world.repo()
|
|
365
|
+
const hash = repo.hashes()[repo.subjects().lastIndexOf(subject)]
|
|
366
|
+
assert.ok(hash !== undefined, `no commit "${subject}"`)
|
|
367
|
+
assert.ok(repo.read(path)?.includes(hash.slice(0, 7)), `${path}: ${repo.read(path)}`)
|
|
368
|
+
},
|
|
369
|
+
)
|
|
370
|
+
|
|
344
371
|
Then("{string} does not contain {string}", (world: TestkitWorld, path: string, text: string) => {
|
|
345
372
|
const content = world.repo().read(path)
|
|
346
373
|
assert.ok(content !== undefined && !content.includes(text), `${path}: ${content}`)
|
|
@@ -32,6 +32,8 @@ export const untilSettled = async (
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export type RetryVerdict = "identical" | "progress"
|
|
36
|
+
|
|
35
37
|
export interface Health {
|
|
36
38
|
/** One counted run of the suite; resolves `true` when green. */
|
|
37
39
|
readonly check: () => Promise<boolean>
|
|
@@ -39,8 +41,8 @@ export interface Health {
|
|
|
39
41
|
readonly report: string
|
|
40
42
|
readonly fixCap: number
|
|
41
43
|
readonly fixesSoFar?: number
|
|
42
|
-
/**
|
|
43
|
-
readonly
|
|
44
|
+
/** How a red report compares to the previous round's: `identical` escalates at once, `progress` at the cap earns one extra fix. */
|
|
45
|
+
readonly compare: (previous: string, current: string) => Promise<RetryVerdict | undefined>
|
|
44
46
|
/** Shared with other callers in the same run of red checks. */
|
|
45
47
|
readonly escalations: EscalationCount
|
|
46
48
|
readonly escalation: EscalationSteps
|
|
@@ -58,13 +60,42 @@ const recheckReport = async (health: Health): Promise<true | string | undefined>
|
|
|
58
60
|
return (await health.recheck()) || stripStamp(read(health.report) ?? "")
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/** Escalate unless the re-check passes: `true` then, else the report the next round compares against. */
|
|
64
|
+
const escalated = async (health: Health, current: string): Promise<true | string> => {
|
|
65
|
+
const rechecked = await recheckReport(health)
|
|
66
|
+
if (rechecked === true) {
|
|
67
|
+
health.escalations.rounds = 0
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
await escalation(health.escalations, health.escalation)
|
|
71
|
+
return rechecked ?? current
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const nextMove = async (
|
|
75
|
+
health: Health,
|
|
76
|
+
fixes: number,
|
|
77
|
+
cap: number,
|
|
78
|
+
previous: string | undefined,
|
|
79
|
+
current: string,
|
|
80
|
+
): Promise<"extend" | "escalate" | undefined> => {
|
|
81
|
+
const capped = fixes >= cap
|
|
82
|
+
// Past a raised cap the loop escalates whatever the judge would answer.
|
|
83
|
+
if (capped && cap > health.fixCap) return "escalate"
|
|
84
|
+
const verdict = previous === undefined ? undefined : await health.compare(previous, current)
|
|
85
|
+
if (capped && verdict === "progress") return "extend"
|
|
86
|
+
return verdict === "identical" || capped ? "escalate" : undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
/**
|
|
62
90
|
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
63
|
-
* first once `fixCap` fixes are spent, or `
|
|
64
|
-
* identical to the previous round's, unless `recheck` passes.
|
|
91
|
+
* first once `fixCap` fixes are spent, or `compare` calls the failure
|
|
92
|
+
* identical to the previous round's, unless `recheck` passes. A round that
|
|
93
|
+
* reaches the cap while `compare` calls it progress raises the cap by one fix,
|
|
94
|
+
* once per run of fixes between escalations.
|
|
65
95
|
*/
|
|
66
96
|
export const healthy = async (fix: () => Promise<void>, health: Health): Promise<void> => {
|
|
67
97
|
let fixes = health.fixesSoFar ?? 0
|
|
98
|
+
let cap = health.fixCap
|
|
68
99
|
let previous: string | undefined
|
|
69
100
|
for (;;) {
|
|
70
101
|
if (await health.check()) {
|
|
@@ -72,17 +103,15 @@ export const healthy = async (fix: () => Promise<void>, health: Health): Promise
|
|
|
72
103
|
return
|
|
73
104
|
}
|
|
74
105
|
const current = stripStamp(read(health.report) ?? "")
|
|
75
|
-
const
|
|
106
|
+
const move = await nextMove(health, fixes, cap, previous, current)
|
|
76
107
|
previous = current
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
previous = rechecked ?? previous
|
|
84
|
-
await escalation(health.escalations, health.escalation)
|
|
108
|
+
if (move === "extend") cap++
|
|
109
|
+
else if (move === "escalate") {
|
|
110
|
+
const after = await escalated(health, current)
|
|
111
|
+
if (after === true) return
|
|
112
|
+
previous = after
|
|
85
113
|
fixes = 0
|
|
114
|
+
cap = health.fixCap
|
|
86
115
|
}
|
|
87
116
|
fixes++
|
|
88
117
|
await fix()
|
|
@@ -61,13 +61,13 @@ Feature: a feature is done only when every acceptance criterion has a passing te
|
|
|
61
61
|
[{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
|
|
62
62
|
"""
|
|
63
63
|
When the process is driven
|
|
64
|
-
Then the process rests at "build.review.
|
|
64
|
+
Then the process rests at "build.review.composing"
|
|
65
65
|
And the commit subjects end with:
|
|
66
66
|
"""
|
|
67
67
|
gtd(check): implement.applying → implement.health.check
|
|
68
68
|
gtd(check): implement.health.check → implement.coverage
|
|
69
69
|
gtd(judge): implement.coverage → implement.closing
|
|
70
|
-
gtd(check): implement.closing → build.review.
|
|
70
|
+
gtd(check): implement.closing → build.review.composing
|
|
71
71
|
"""
|
|
72
72
|
And the commit "gtd(judge): implement.coverage → implement.closing" has a body containing "Gtd-Mark: covers=AC-1"
|
|
73
73
|
And the commit "gtd(judge): implement.coverage → implement.closing" has a body containing "Gtd-Mark: covers=AC-2"
|
|
@@ -144,7 +144,7 @@ Feature: a feature is done only when every acceptance criterion has a passing te
|
|
|
144
144
|
"""
|
|
145
145
|
And step "implement.uncovered" changes nothing
|
|
146
146
|
When the process is driven
|
|
147
|
-
Then the process rests at "build.review.
|
|
147
|
+
Then the process rests at "build.review.composing"
|
|
148
148
|
And the commit "gtd(human): implement.uncovered → implement.closing" has a body containing "Gtd-Mark: covers=AC-1"
|
|
149
149
|
And the commit "gtd(human): implement.uncovered → implement.closing" has a body containing "Gtd-Mark: uncovered=AC-2"
|
|
150
150
|
|
|
@@ -64,7 +64,7 @@ Feature: a trivial change takes the same few beats at either risk tier
|
|
|
64
64
|
"""
|
|
65
65
|
And step "implement.applying" runs its script
|
|
66
66
|
When the process is driven
|
|
67
|
-
Then the process rests at "build.review.
|
|
67
|
+
Then the process rests at "build.review.composing"
|
|
68
68
|
And "src/greeting.ts" contains "Hello, world"
|
|
69
69
|
And the commit subjects end with:
|
|
70
70
|
"""
|
|
@@ -80,7 +80,7 @@ Feature: a trivial change takes the same few beats at either risk tier
|
|
|
80
80
|
gtd(check): implement.health.check → implement.coverage
|
|
81
81
|
gtd(judge): implement.coverage → implement.closing
|
|
82
82
|
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
83
|
-
gtd(agent): build.quality.correctness.reviewing → build.review.
|
|
83
|
+
gtd(agent): build.quality.correctness.reviewing → build.review.composing
|
|
84
84
|
"""
|
|
85
85
|
And the output of gtd "summary" <summary> "needs a human reviewer"
|
|
86
86
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { runsItsScript, testRepo
|
|
2
|
+
import { runsItsScript, testRepo } from "../../testkit/index.js"
|
|
3
3
|
import { CONFIG_SCAFFOLD } from "../../install/index.js"
|
|
4
4
|
import { reviewDoc } from "../shared/config.fixture.js"
|
|
5
5
|
|
|
@@ -75,10 +75,7 @@ describe("afterTail feedback lap", () => {
|
|
|
75
75
|
`}`,
|
|
76
76
|
].join("\n")
|
|
77
77
|
|
|
78
|
-
const prompts: string[] = []
|
|
79
|
-
|
|
80
78
|
const feedbackLap = async () => {
|
|
81
|
-
prompts.length = 0
|
|
82
79
|
const repo = testRepo({
|
|
83
80
|
workflow: TAIL,
|
|
84
81
|
env: {
|
|
@@ -93,10 +90,7 @@ describe("afterTail feedback lap", () => {
|
|
|
93
90
|
await repo.drive({
|
|
94
91
|
"review-gate.check": () => undefined,
|
|
95
92
|
"build.health.check": () => undefined,
|
|
96
|
-
"build.review.
|
|
97
|
-
prompts.push(beat.content)
|
|
98
|
-
r.write(".gtd/REVIEW.md", review())
|
|
99
|
-
},
|
|
93
|
+
"build.review.composing": (_, r) => r.write(".gtd/REVIEW.md", review()),
|
|
100
94
|
})
|
|
101
95
|
repo.write(".gtd/REVIEW.md", review("also export multiply"))
|
|
102
96
|
await repo.land()
|
|
@@ -137,35 +131,6 @@ describe("afterTail feedback lap", () => {
|
|
|
137
131
|
expect(rest.content).not.toContain("write the change fresh")
|
|
138
132
|
expect(repo.subjects().some((subject) => subject.includes("seam."))).toBe(false)
|
|
139
133
|
})
|
|
140
|
-
|
|
141
|
-
it("the first review says everything since base, the one after feedback only what is new", async () => {
|
|
142
|
-
const repo = await feedbackLap()
|
|
143
|
-
expect(prompts[0]).toContain("it covers everything since the base")
|
|
144
|
-
expect(prompts[0]).not.toContain("only what is new since the previous review")
|
|
145
|
-
const writes: Record<string, () => [string, string]> = {
|
|
146
|
-
"design.triage": () => [
|
|
147
|
-
".gtd/REQUIREMENTS.md",
|
|
148
|
-
"# Requirements\n\n- multiply, designed\n\n## Acceptance Criteria\n\n- AC-1: done\n",
|
|
149
|
-
],
|
|
150
|
-
"implement.building": () => ["src/calc.ts", `${CALC}// again\n`],
|
|
151
|
-
}
|
|
152
|
-
const actors = new Proxy({} as Record<string, Actor>, {
|
|
153
|
-
get: (_, state: string): Actor | undefined =>
|
|
154
|
-
state === "build.review.await-review" || state === "idle"
|
|
155
|
-
? undefined
|
|
156
|
-
: (beat, r) => {
|
|
157
|
-
if (state === "implement.coverage") return '[{"id":"AC-1","answer":"yes","p":0.9}]'
|
|
158
|
-
if (state === "build.review.reviewing") {
|
|
159
|
-
prompts.push(beat.content)
|
|
160
|
-
r.write(".gtd/REVIEW.md", review())
|
|
161
|
-
} else if (writes[state]) r.write(...writes[state]())
|
|
162
|
-
else if (state.endsWith("closing") || state === "re-unwind") r.runScript(beat.content)
|
|
163
|
-
},
|
|
164
|
-
})
|
|
165
|
-
await repo.drive(actors, { maxBeats: 80 })
|
|
166
|
-
expect(prompts[1]).toContain("it covers only what is new since the previous review")
|
|
167
|
-
expect(prompts[1]).not.toContain("everything since the base")
|
|
168
|
-
})
|
|
169
134
|
})
|
|
170
135
|
|
|
171
136
|
describe("an in-flight 28.0.0 naive-pass process", () => {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
slice,
|
|
10
10
|
start,
|
|
11
11
|
} from "../../flows/index.js"
|
|
12
|
-
import { gate, healthy
|
|
12
|
+
import { gate, healthy } from "../shared/health.js"
|
|
13
13
|
import { flagModeOn, shipped } from "../shared/options.js"
|
|
14
14
|
import { FEEDBACK, fixCheck, QUALITY, REQUIREMENTS } from "../shared/steps.js"
|
|
15
15
|
import { refusePendingHandOff, tail } from "../shared/tail.js"
|
|
@@ -135,8 +135,8 @@ const buildSlice = async (sliceBase: string, build: SliceBuild): Promise<void> =
|
|
|
135
135
|
})
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
//
|
|
139
|
-
//
|
|
138
|
+
// Between slices nothing reviews the lap's leftovers, and the next lap appends to the file.
|
|
139
|
+
// Empty counts as resolved, as the lap reads it.
|
|
140
140
|
const settleQuality = async (): Promise<void> => {
|
|
141
141
|
let rested = false
|
|
142
142
|
while ((read(QUALITY) ?? "").length > 0) {
|
|
@@ -151,15 +151,13 @@ const settleQuality = async (): Promise<void> => {
|
|
|
151
151
|
if (rested) await healthy(() => fixCheck())
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
/** The quality lap, then the human review since `base`. */
|
|
155
|
-
const reviewedTail = (base: string, lensBase: string
|
|
156
|
-
tail(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
{ green: true, lensBase },
|
|
162
|
-
)
|
|
154
|
+
/** The triaged quality lap, then the human review since `base`. */
|
|
155
|
+
const reviewedTail = (base: string, lensBase: string): Promise<ReviewOutcome> =>
|
|
156
|
+
tail((escalations, report) => review(base, escalations, { report }), {
|
|
157
|
+
green: true,
|
|
158
|
+
lensBase,
|
|
159
|
+
triage: true,
|
|
160
|
+
})
|
|
163
161
|
|
|
164
162
|
// seam.naive / seam.judge never run on a feedback lap: its design → implement is built unjudged
|
|
165
163
|
// and unrefactored, held to every criterion's coverage since the process start.
|
|
@@ -181,7 +179,7 @@ const featureFeedback = async (
|
|
|
181
179
|
})
|
|
182
180
|
const tier = riskTierOn() ? recordRisk() : undefined
|
|
183
181
|
closed = await implementing({ tier, amend: true })
|
|
184
|
-
outcome = await reviewedTail(outcome.base, lensBase
|
|
182
|
+
outcome = await reviewedTail(outcome.base, lensBase)
|
|
185
183
|
}
|
|
186
184
|
}
|
|
187
185
|
|
|
@@ -201,7 +199,7 @@ export const afterTail = async (first: ReviewOutcome): Promise<void> => {
|
|
|
201
199
|
await design(outcome.base)
|
|
202
200
|
const tier = riskTierOn() ? recordRisk() : undefined
|
|
203
201
|
await implement({ tier, amend: true })
|
|
204
|
-
outcome = await buildTail(outcome.base, { green: true
|
|
202
|
+
outcome = await buildTail(outcome.base, { green: true })
|
|
205
203
|
}
|
|
206
204
|
}
|
|
207
205
|
|
|
@@ -67,7 +67,7 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
|
|
|
67
67
|
"""
|
|
68
68
|
And step "build.quality.correctness.reviewing" changes nothing
|
|
69
69
|
When the process is driven
|
|
70
|
-
Then the process rests at "build.review.
|
|
70
|
+
Then the process rests at "build.review.composing"
|
|
71
71
|
And "src/calc.ts" contains "subtract"
|
|
72
72
|
And "src/calc.test.ts" contains "[AC-1] subtracts"
|
|
73
73
|
And the commit subjects end with:
|
|
@@ -85,5 +85,5 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
|
|
|
85
85
|
gtd(check): implement.health.check → implement.coverage
|
|
86
86
|
gtd(judge): implement.coverage → implement.closing
|
|
87
87
|
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
88
|
-
gtd(agent): build.quality.correctness.reviewing → build.review.
|
|
88
|
+
gtd(agent): build.quality.correctness.reviewing → build.review.composing
|
|
89
89
|
"""
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: the quality lenses are the only agentic review
|
|
3
|
+
|
|
4
|
+
A judge classifies each `## ` finding the lenses wrote to `.gtd/QUALITY.md`:
|
|
5
|
+
a mechanical one is fixed by the fixer, noise is dropped with its `p` kept in
|
|
6
|
+
a trailer, and a design one becomes a note in `.gtd/REVIEW.md`. gtd writes
|
|
7
|
+
that file itself — no reviewer turn runs — and lists, without tick boxes,
|
|
8
|
+
what was fixed automatically and what was dropped as noise.
|
|
9
|
+
|
|
10
|
+
Background:
|
|
11
|
+
Given the shipped workflows, as gtd install scaffolds them
|
|
12
|
+
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
|
|
13
|
+
And an environment variable "GTD_FLAGMODE" set to "false"
|
|
14
|
+
And a file "src/calc.ts" with:
|
|
15
|
+
"""
|
|
16
|
+
export const add = (a: number, b: number) => a + b
|
|
17
|
+
"""
|
|
18
|
+
And the working tree is committed as "feat: add calculator"
|
|
19
|
+
And a file "TODO.md" with:
|
|
20
|
+
"""
|
|
21
|
+
# Add subtract
|
|
22
|
+
|
|
23
|
+
subtract(a, b) returns the difference.
|
|
24
|
+
"""
|
|
25
|
+
And step "unwind" changes nothing
|
|
26
|
+
And step "start-gate.check" changes nothing
|
|
27
|
+
And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
|
|
28
|
+
"""
|
|
29
|
+
# Requirements
|
|
30
|
+
|
|
31
|
+
## Concerns
|
|
32
|
+
|
|
33
|
+
### Subtract
|
|
34
|
+
|
|
35
|
+
- `subtract(a, b)` returns `a - b`.
|
|
36
|
+
|
|
37
|
+
## Acceptance Criteria
|
|
38
|
+
|
|
39
|
+
- AC-1: subtract(a, b) returns a - b.
|
|
40
|
+
"""
|
|
41
|
+
And step "design.gate.answer" changes nothing
|
|
42
|
+
And step "seam.naive" writes "src/calc.test.ts" with:
|
|
43
|
+
"""
|
|
44
|
+
it("[AC-1] subtracts", () => {})
|
|
45
|
+
"""
|
|
46
|
+
And step "seam.naive" writes "src/calc.ts" with:
|
|
47
|
+
"""
|
|
48
|
+
export const add = (a: number, b: number) => a + b
|
|
49
|
+
export const subtract = (a: number, b: number) => a - b
|
|
50
|
+
"""
|
|
51
|
+
And step "seam.check" changes nothing
|
|
52
|
+
And step "implement.applying" runs its script
|
|
53
|
+
And step "implement.health.check" changes nothing
|
|
54
|
+
And step "implement.closing" runs its script
|
|
55
|
+
And judge step "implement.coverage" answers:
|
|
56
|
+
"""
|
|
57
|
+
[{"id":"AC-1","answer":"yes","p":0.9}]
|
|
58
|
+
"""
|
|
59
|
+
And step "build.quality.correctness.reviewing" writes ".gtd/QUALITY.md" with:
|
|
60
|
+
"""
|
|
61
|
+
# Quality
|
|
62
|
+
|
|
63
|
+
## subtract has no negative test
|
|
64
|
+
|
|
65
|
+
Add a test subtracting a larger number.
|
|
66
|
+
|
|
67
|
+
## add could be a method
|
|
68
|
+
|
|
69
|
+
A class would read better.
|
|
70
|
+
|
|
71
|
+
## AC-1 ignores floating point
|
|
72
|
+
|
|
73
|
+
Should subtract round? The criterion does not say.
|
|
74
|
+
"""
|
|
75
|
+
And step "build.quality.sorting" runs its script
|
|
76
|
+
And step "build.review.composing" runs its script
|
|
77
|
+
|
|
78
|
+
Scenario: a mechanical finding is fixed and listed, noise dropped and listed, a design one noted
|
|
79
|
+
Given judge step "build.quality.triage" answers:
|
|
80
|
+
"""
|
|
81
|
+
[{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"noise","p":0.85},{"id":"Q3","answer":"design","p":0.8}]
|
|
82
|
+
"""
|
|
83
|
+
And step "build.fix.quality.fixing" writes "src/calc.test.ts" with:
|
|
84
|
+
"""
|
|
85
|
+
it("[AC-1] subtracts", () => {})
|
|
86
|
+
it("subtracts a larger number", () => {})
|
|
87
|
+
"""
|
|
88
|
+
And step "build.fix.quality.fixing" deletes ".gtd/QUALITY.md"
|
|
89
|
+
And step "build.health.check" changes nothing
|
|
90
|
+
When the process is driven
|
|
91
|
+
Then the process rests at "build.review.await-review"
|
|
92
|
+
And ".gtd/QUALITY.md" does not exist
|
|
93
|
+
And the commit "gtd(judge): build.quality.triage → build.quality.sorting" has a body containing "Gtd-Mark: noise=Q2 p=0.85 add could be a method"
|
|
94
|
+
And ".gtd/REVIEW.md" contains:
|
|
95
|
+
"""
|
|
96
|
+
## AC-1 ignores floating point
|
|
97
|
+
|
|
98
|
+
Should subtract round? The criterion does not say.
|
|
99
|
+
|
|
100
|
+
## Fixed automatically
|
|
101
|
+
|
|
102
|
+
- subtract has no negative test (
|
|
103
|
+
"""
|
|
104
|
+
And ".gtd/REVIEW.md" contains the short hash of the commit "gtd(agent): build.fix.quality.fixing → build.health.check"
|
|
105
|
+
And ".gtd/REVIEW.md" contains:
|
|
106
|
+
"""
|
|
107
|
+
## Dropped as noise
|
|
108
|
+
|
|
109
|
+
- add could be a method (p=0.85)
|
|
110
|
+
"""
|
|
111
|
+
And ".gtd/REVIEW.md" does not contain "[ ]"
|
|
112
|
+
And the commit subjects end with:
|
|
113
|
+
"""
|
|
114
|
+
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
115
|
+
gtd(agent): build.quality.correctness.reviewing → build.quality.triage
|
|
116
|
+
gtd(judge): build.quality.triage → build.quality.sorting
|
|
117
|
+
gtd(check): build.quality.sorting → build.fix.quality.fixing
|
|
118
|
+
gtd(agent): build.fix.quality.fixing → build.health.check
|
|
119
|
+
gtd(check): build.health.check → build.review.composing
|
|
120
|
+
gtd(check): build.review.composing → build.review.await-review
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
Scenario: an unsure verdict makes a finding a note, never a fix or a drop
|
|
124
|
+
Given judge step "build.quality.triage" answers:
|
|
125
|
+
"""
|
|
126
|
+
[{"id":"Q1","answer":"mechanical","p":0.5},{"id":"Q2","answer":"noise","p":0.6}]
|
|
127
|
+
"""
|
|
128
|
+
When the process is driven
|
|
129
|
+
Then the process rests at "build.review.await-review"
|
|
130
|
+
And ".gtd/REVIEW.md" contains "## subtract has no negative test"
|
|
131
|
+
And ".gtd/REVIEW.md" contains "## add could be a method"
|
|
132
|
+
And ".gtd/REVIEW.md" contains "## AC-1 ignores floating point"
|
|
133
|
+
And ".gtd/REVIEW.md" does not contain "Fixed automatically"
|
|
134
|
+
And no commit body contains "Gtd-Mark: noise="
|
|
135
|
+
And the commit subjects end with:
|
|
136
|
+
"""
|
|
137
|
+
gtd(judge): build.quality.triage → build.quality.sorting
|
|
138
|
+
gtd(check): build.quality.sorting → build.review.composing
|
|
139
|
+
gtd(check): build.review.composing → build.review.await-review
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
Scenario: a mechanical finding the fix cap leaves becomes a note
|
|
143
|
+
Given judge step "build.quality.triage" answers:
|
|
144
|
+
"""
|
|
145
|
+
[{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"mechanical","p":0.9},{"id":"Q3","answer":"mechanical","p":0.9}]
|
|
146
|
+
"""
|
|
147
|
+
And step "build.fix.quality.fixing" writes ".gtd/QUALITY.md" with:
|
|
148
|
+
"""
|
|
149
|
+
# Quality
|
|
150
|
+
|
|
151
|
+
## add could be a method
|
|
152
|
+
|
|
153
|
+
A class would read better.
|
|
154
|
+
"""
|
|
155
|
+
And step "build.health.check" changes nothing
|
|
156
|
+
When the process is driven
|
|
157
|
+
Then the process rests at "build.review.await-review"
|
|
158
|
+
And ".gtd/QUALITY.md" does not exist
|
|
159
|
+
And ".gtd/REVIEW.md" contains:
|
|
160
|
+
"""
|
|
161
|
+
## add could be a method
|
|
162
|
+
|
|
163
|
+
_The fixer left this unresolved._
|
|
164
|
+
|
|
165
|
+
A class would read better.
|
|
166
|
+
"""
|
|
167
|
+
And ".gtd/REVIEW.md" contains "- subtract has no negative test ("
|
|
168
|
+
And ".gtd/REVIEW.md" contains "- AC-1 ignores floating point ("
|
|
169
|
+
And the commit subjects end with:
|
|
170
|
+
"""
|
|
171
|
+
gtd(judge): build.quality.triage → build.fix.quality.fixing
|
|
172
|
+
gtd(agent): build.fix.quality.fixing
|
|
173
|
+
gtd(agent): build.fix.quality.fixing
|
|
174
|
+
gtd(agent): build.fix.quality.fixing → build.health.check
|
|
175
|
+
gtd(check): build.health.check → build.review.composing
|
|
176
|
+
gtd(check): build.review.composing → build.review.await-review
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
Scenario: a fixer that renames a chunk it leaves counts nothing fixed
|
|
180
|
+
Given judge step "build.quality.triage" answers:
|
|
181
|
+
"""
|
|
182
|
+
[{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"mechanical","p":0.9},{"id":"Q3","answer":"mechanical","p":0.9}]
|
|
183
|
+
"""
|
|
184
|
+
And step "build.fix.quality.fixing" writes ".gtd/QUALITY.md" with:
|
|
185
|
+
"""
|
|
186
|
+
# Quality
|
|
187
|
+
|
|
188
|
+
## null check in parse()
|
|
189
|
+
|
|
190
|
+
Still open.
|
|
191
|
+
"""
|
|
192
|
+
And step "build.health.check" changes nothing
|
|
193
|
+
When the process is driven
|
|
194
|
+
Then the process rests at "build.review.await-review"
|
|
195
|
+
And ".gtd/REVIEW.md" contains "## null check in parse()"
|
|
196
|
+
And ".gtd/REVIEW.md" contains "## subtract has no negative test"
|
|
197
|
+
And ".gtd/REVIEW.md" contains "check whether it was fixed"
|
|
198
|
+
And ".gtd/REVIEW.md" does not contain "Fixed automatically"
|
|
199
|
+
|
|
200
|
+
Scenario: prose the fixer leaves outside every chunk does not hide its fixes
|
|
201
|
+
Given judge step "build.quality.triage" answers:
|
|
202
|
+
"""
|
|
203
|
+
[{"id":"Q1","answer":"mechanical","p":0.9},{"id":"Q2","answer":"mechanical","p":0.9},{"id":"Q3","answer":"mechanical","p":0.9}]
|
|
204
|
+
"""
|
|
205
|
+
And step "build.fix.quality.fixing" writes ".gtd/QUALITY.md" with:
|
|
206
|
+
"""
|
|
207
|
+
# Quality
|
|
208
|
+
|
|
209
|
+
All fixed.
|
|
210
|
+
"""
|
|
211
|
+
And step "build.health.check" changes nothing
|
|
212
|
+
When the process is driven
|
|
213
|
+
Then the process rests at "build.review.await-review"
|
|
214
|
+
And ".gtd/REVIEW.md" contains "- subtract has no negative test ("
|
|
215
|
+
And ".gtd/REVIEW.md" does not contain "check whether it was fixed"
|
|
216
|
+
|
|
217
|
+
Scenario: signing off a composed review ends the process
|
|
218
|
+
Given judge step "build.quality.triage" answers:
|
|
219
|
+
"""
|
|
220
|
+
[{"id":"Q1","answer":"noise","p":0.9},{"id":"Q2","answer":"noise","p":0.9},{"id":"Q3","answer":"noise","p":0.9}]
|
|
221
|
+
"""
|
|
222
|
+
And step "build.review.closing" runs its script
|
|
223
|
+
When the process is driven
|
|
224
|
+
Then the process rests at "build.review.await-review"
|
|
225
|
+
When gtd lands
|
|
226
|
+
And the process is driven
|
|
227
|
+
Then the process rests at "idle"
|
|
228
|
+
And ".gtd/REVIEW.md" does not exist
|
|
229
|
+
And the commit subjects end with:
|
|
230
|
+
"""
|
|
231
|
+
gtd(check): build.review.composing → build.review.await-review
|
|
232
|
+
gtd(human): build.review.await-review → build.review.closing
|
|
233
|
+
gtd(check): build.review.closing → idle
|
|
234
|
+
"""
|
|
@@ -64,7 +64,7 @@ Feature: a naive pass the judge rules land is itself the implementation
|
|
|
64
64
|
|
|
65
65
|
Scenario: a land judgment applies the naive pass and runs from idle to the review gate with no rebuild
|
|
66
66
|
When the process is driven
|
|
67
|
-
Then the process rests at "build.review.
|
|
67
|
+
Then the process rests at "build.review.composing"
|
|
68
68
|
And "src/greeting.ts" contains "Hello, world"
|
|
69
69
|
And "src/acceptance.test.ts" contains "[AC-1] holds"
|
|
70
70
|
And the commit subjects end with:
|
|
@@ -81,7 +81,7 @@ Feature: a naive pass the judge rules land is itself the implementation
|
|
|
81
81
|
gtd(check): implement.health.check → implement.coverage
|
|
82
82
|
gtd(judge): implement.coverage → implement.closing
|
|
83
83
|
gtd(check): implement.closing → build.quality.correctness.reviewing
|
|
84
|
-
gtd(agent): build.quality.correctness.reviewing → build.review.
|
|
84
|
+
gtd(agent): build.quality.correctness.reviewing → build.review.composing
|
|
85
85
|
"""
|
|
86
86
|
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
|
|
87
87
|
And the commit "gtd(agent): seam.naive → seam.check" has a body containing "Gtd-Kept:"
|