@pmelab/gtd 54.0.0 → 56.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 +109 -104
- package/dist/chunks/{reserved-CQ7amcKG.mjs → reserved-B8EC-WE8.mjs} +124 -230
- package/dist/chunks/{review-CXzUXJHL.mjs → review-HdrX0FJ7.mjs} +30 -125
- package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
- package/dist/chunks/{tail-JZ9PAOwp.mjs → tail-BSLah2mA.mjs} +155 -63
- package/dist/gtd.bundle.mjs +259 -22
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +6 -6
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +551 -66
- package/dist/workflow-kit.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +47 -102
- package/dist/workflows/fix.mjs +3 -3
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +614 -574
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +3 -3
- package/src/cli/Cli.ts +5 -5
- package/src/edge/Edge.test.ts +1 -1
- package/src/steering/FORMATS.md +5 -1
- package/src/steering/SteeringFormats.test.ts +2 -2
- package/src/steering/contract.test.ts +1 -0
- package/src/steering/findings.test.ts +47 -0
- package/src/steering/findings.ts +350 -0
- package/src/steering/fixtures/findings/gate.md +34 -0
- package/src/steering/fixtures/findings/gate.read.json +87 -0
- package/src/steering/index.test.ts +2 -2
- package/src/steering/index.ts +4 -3
- package/src/steering/review.test.ts +1 -75
- package/src/steering/review.ts +1 -74
- package/src/workflow/compile.test.ts +1 -0
- package/src/workflows/feature/criteria-coverage.feature +104 -0
- package/src/workflows/feature/feature.test.ts +1 -1
- package/src/workflows/feature/feature.ts +17 -20
- package/src/workflows/feature/green.ts +2 -5
- package/src/workflows/feature/lens-review.feature +323 -171
- package/src/workflows/feature/naive-acceptance.feature +31 -32
- package/src/workflows/feature/prototype.ts +5 -8
- package/src/workflows/feature/refactor-absorbs.feature +1 -7
- package/src/workflows/feature/refactor-first.feature +2 -10
- package/src/workflows/feature/refactor-rebuild.feature +20 -36
- package/src/workflows/feature/refactor-review.feature +33 -117
- package/src/workflows/feature/refactor.test.ts +0 -2
- package/src/workflows/feature/refactor.ts +0 -1
- package/src/workflows/feature/review-gate.feature +6 -22
- package/src/workflows/feature/reviewer-walkthrough.feature +16 -103
- package/src/workflows/feature/seam.test.ts +19 -101
- package/src/workflows/feature/seam.ts +13 -89
- package/src/workflows/feature/standards.feature +75 -34
- package/src/workflows/fix/fix-lens-origin.feature +20 -7
- package/src/workflows/fix/fix-quality.feature +104 -23
- package/src/workflows/review/review-door.feature +62 -59
- package/src/workflows/shared/access.test.ts +1 -0
- package/src/workflows/shared/learn.test.ts +0 -14
- package/src/workflows/shared/learn.ts +1 -6
- package/src/workflows/shared/lenses.ts +149 -43
- package/src/workflows/shared/review.test.ts +30 -27
- package/src/workflows/shared/review.ts +29 -196
- package/src/workflows/shared/skills.test.ts +3 -1
- package/src/workflows/shared/steps.test.ts +9 -6
- package/src/workflows/shared/steps.ts +12 -3
- package/src/workflows/shared/tail.test.ts +20 -2
- package/src/workflows/shared/tail.ts +38 -16
- package/src/workflows/shared/text.test.ts +122 -294
- package/src/workflows/shared/text.ts +181 -358
- package/src/workflows/shared/vars.ts +1 -1
- package/src/workflows/feature/review-answers.feature +0 -324
- package/src/workflows/feature/severity-budget.feature +0 -116
- package/src/workflows/feature/severity-sorted-review.feature +0 -198
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
answered,
|
|
3
3
|
head,
|
|
4
|
+
human,
|
|
4
5
|
judge,
|
|
5
6
|
mark,
|
|
6
7
|
numeric,
|
|
7
8
|
read,
|
|
8
|
-
|
|
9
|
+
refuse,
|
|
9
10
|
run,
|
|
10
11
|
sectionBodies,
|
|
11
12
|
vars,
|
|
12
13
|
writeScript,
|
|
13
14
|
type JudgeQuestion,
|
|
14
15
|
} from "../../flows/index.js"
|
|
16
|
+
import {
|
|
17
|
+
findingEntries,
|
|
18
|
+
formatFinding,
|
|
19
|
+
steeringFormatFor,
|
|
20
|
+
type FindingEntry,
|
|
21
|
+
} from "../../steering/index.js"
|
|
15
22
|
import { FIX_CAP, guarded, healthy, type EscalationCount, type Guard } from "./health.js"
|
|
16
23
|
import { fixCheck, fixQuality, QUALITY } from "./steps.js"
|
|
17
24
|
import * as t from "./text.js"
|
|
@@ -27,19 +34,27 @@ export interface Finding {
|
|
|
27
34
|
|
|
28
35
|
type FindingKind = "mechanical" | "noise" | "design"
|
|
29
36
|
|
|
30
|
-
/** What the triaged quality lap did with
|
|
37
|
+
/** What the triaged quality lap and its finding gate did with the findings, for the review to list. */
|
|
31
38
|
export interface LensReport {
|
|
32
|
-
/**
|
|
33
|
-
readonly notes: readonly (Finding &
|
|
34
|
-
Origin & {
|
|
35
|
-
readonly unresolved?: true
|
|
36
|
-
readonly unclear?: true
|
|
37
|
-
})[]
|
|
39
|
+
/** Fixed by the fix turns the judge sent them to, with the fixing commit. */
|
|
38
40
|
readonly fixed: readonly (Finding & Origin & { readonly commit: string })[]
|
|
39
|
-
|
|
41
|
+
/** Ticked at the finding gate and fixed, with the fixing commit. */
|
|
42
|
+
readonly gateFixed: readonly (Finding & Origin & { readonly commit: string })[]
|
|
43
|
+
/** Left unticked at the finding gate. */
|
|
44
|
+
readonly dropped: readonly (Finding & Origin)[]
|
|
45
|
+
/** Called noise by the judge and left unticked at the finding gate; `p` absent on one the human added there. */
|
|
46
|
+
readonly noise: readonly (Finding & Origin & { readonly p?: number })[]
|
|
47
|
+
/** Ticked at the finding gate, still open after the fix turns. */
|
|
48
|
+
readonly unresolved: readonly (Finding & Origin & { readonly unclear?: true })[]
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
export const NO_FINDINGS: LensReport = {
|
|
51
|
+
export const NO_FINDINGS: LensReport = {
|
|
52
|
+
fixed: [],
|
|
53
|
+
gateFixed: [],
|
|
54
|
+
dropped: [],
|
|
55
|
+
noise: [],
|
|
56
|
+
unresolved: [],
|
|
57
|
+
}
|
|
43
58
|
|
|
44
59
|
const UNTITLED = "Quality findings"
|
|
45
60
|
|
|
@@ -138,14 +153,117 @@ export interface TriageOptions {
|
|
|
138
153
|
readonly lensOf?: ((f: Finding) => string | undefined) | undefined
|
|
139
154
|
}
|
|
140
155
|
|
|
156
|
+
type Note = Finding & Origin & { readonly unresolved?: true; readonly unclear?: true }
|
|
157
|
+
|
|
158
|
+
interface Fixing {
|
|
159
|
+
readonly fixed: readonly (Finding & Origin & { readonly commit: string })[]
|
|
160
|
+
readonly open: readonly (Finding & Origin)[]
|
|
161
|
+
readonly unclear: readonly (Finding & Origin)[]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const NOTHING_FIXED: Fixing = { fixed: [], open: [], unclear: [] }
|
|
165
|
+
|
|
166
|
+
/** Up to `FIX_CAP` fix turns over `.gtd/QUALITY.md`, which holds one chunk per finding in `findings`. */
|
|
167
|
+
const fixTurns = async (
|
|
168
|
+
findings: readonly (Finding & Origin)[],
|
|
169
|
+
options: TriageOptions,
|
|
170
|
+
): Promise<Fixing> => {
|
|
171
|
+
const fixed: (Finding & Origin & { commit: string })[] = []
|
|
172
|
+
const unclear: (Finding & Origin)[] = []
|
|
173
|
+
let open: readonly (Finding & Origin)[] = findings
|
|
174
|
+
for (let turns = 0; turns < FIX_CAP && open.length > 0; turns++) {
|
|
175
|
+
await guarded(() => fixQuality(options.rules), options.guard)()
|
|
176
|
+
const turn = afterFix(open, findingsOf(read(QUALITY) ?? ""))
|
|
177
|
+
const commit = head()
|
|
178
|
+
fixed.push(...turn.fixed.map((f) => ({ ...f, commit })))
|
|
179
|
+
unclear.push(...turn.unclear)
|
|
180
|
+
// A turn that renamed a chunk hands back fresh parses: keep the lens a same-titled finding had.
|
|
181
|
+
open = turn.open.map((f) => ({ ...findings.find((m) => m.title === f.title), ...f }))
|
|
182
|
+
}
|
|
183
|
+
const { escalations, guard, guardRests, rules } = options
|
|
184
|
+
await healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules })
|
|
185
|
+
return { fixed, open, unclear }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Matches each entry the human left to the finding it was written from, by title, first unused first; `left` is what no entry claimed. */
|
|
189
|
+
const matcher = <F extends Finding>(findings: readonly F[]) => {
|
|
190
|
+
const left = [...findings]
|
|
191
|
+
const of = (e: FindingEntry): F | undefined => {
|
|
192
|
+
const at = left.findIndex((f) => f.title === e.title)
|
|
193
|
+
return at < 0 ? undefined : left.splice(at, 1)[0]
|
|
194
|
+
}
|
|
195
|
+
return { of, left }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const gateProblems = (text: string): readonly string[] =>
|
|
199
|
+
steeringFormatFor("findings")!
|
|
200
|
+
.validate(text)
|
|
201
|
+
.map((f) => ` - ${formatFinding(QUALITY, f)}`)
|
|
202
|
+
|
|
141
203
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
|
|
145
|
-
|
|
204
|
+
* The human gate over the open notes and the noise: a ticked finding is
|
|
205
|
+
* fixed — never re-planned — and the rest dropped. No notes and no noise, no gate.
|
|
206
|
+
*/
|
|
207
|
+
const findingGate = async (
|
|
208
|
+
notes: readonly Note[],
|
|
209
|
+
noise: readonly (Finding & Origin & { readonly p: number })[],
|
|
210
|
+
options: TriageOptions,
|
|
211
|
+
): Promise<Omit<LensReport, "fixed">> => {
|
|
212
|
+
if (notes.length + noise.length === 0) return NO_FINDINGS
|
|
213
|
+
const written = t.findingGateFile(notes, noise)
|
|
214
|
+
await run("quality.gating", writeScript(QUALITY, written), {
|
|
215
|
+
label: "Listing the findings for you",
|
|
216
|
+
})
|
|
217
|
+
await human("quality.gate", {
|
|
218
|
+
message: t.findingGateMessage(),
|
|
219
|
+
label: "Deciding on the quality findings",
|
|
220
|
+
file: QUALITY,
|
|
221
|
+
mode: "findings",
|
|
222
|
+
acceptClean: true,
|
|
223
|
+
})
|
|
224
|
+
// A deleted file decides nothing: every finding is dropped, as if untouched.
|
|
225
|
+
const answered = read(QUALITY) ?? written
|
|
226
|
+
const problems = gateProblems(answered)
|
|
227
|
+
if (problems.length > 0)
|
|
228
|
+
refuse(
|
|
229
|
+
`gtd land: finding-gate: ${problems.length} problem(s) in ${QUALITY} — keep one \`- [ ] fix\` box per finding, ticked to fix it or unticked to drop it, and write guidance only under a ticked one:\n${problems.join("\n")}`,
|
|
230
|
+
)
|
|
231
|
+
const noteOf = matcher(notes)
|
|
232
|
+
const noiseOf = matcher(noise)
|
|
233
|
+
// An entry the human added is a finding of its own.
|
|
234
|
+
const decided = findingEntries(answered).map((e) => ({
|
|
235
|
+
e,
|
|
236
|
+
finding: (e.noise ? noiseOf.of(e) : noteOf.of(e)) ?? { id: "", title: e.title, body: e.body },
|
|
237
|
+
}))
|
|
238
|
+
// The finding as gtd wrote it: a human steers with guidance, not by editing its text.
|
|
239
|
+
const ticked = decided
|
|
240
|
+
.filter(({ e }) => e.fix)
|
|
241
|
+
.map(({ e, finding }) => ({ ...finding, body: t.withGuidance(finding.body, e.guidance) }))
|
|
242
|
+
let fixing = NOTHING_FIXED
|
|
243
|
+
if (ticked.length > 0) {
|
|
244
|
+
await run("quality.picking", writeScript(QUALITY, t.qualityFindingsFile(ticked)), {
|
|
245
|
+
label: "Keeping the findings you ticked",
|
|
246
|
+
})
|
|
247
|
+
fixing = await fixTurns(ticked, options)
|
|
248
|
+
}
|
|
249
|
+
const unticked = (noisy: boolean) =>
|
|
250
|
+
decided.filter(({ e }) => !e.fix && e.noise === noisy).map(({ finding }) => finding)
|
|
251
|
+
// A finding whose entry the human deleted is dropped too.
|
|
252
|
+
return {
|
|
253
|
+
gateFixed: fixing.fixed,
|
|
254
|
+
dropped: [...unticked(false), ...noteOf.left],
|
|
255
|
+
noise: [...unticked(true), ...noiseOf.left],
|
|
256
|
+
unresolved: [...fixing.open, ...fixing.unclear.map((f) => ({ ...f, unclear: true as const }))],
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Judge each finding in `.gtd/QUALITY.md` and fix the mechanical ones for up
|
|
262
|
+
* to `FIX_CAP` turns; the rest — design, unresolved and noise — go to the
|
|
263
|
+
* finding gate. Leaves `.gtd/QUALITY.md` holding only what the last fixer
|
|
264
|
+
* left: the caller must remove it, or the next lap appends to stale findings.
|
|
146
265
|
*/
|
|
147
266
|
export const triageFindings = async (options: TriageOptions): Promise<LensReport> => {
|
|
148
|
-
const { escalations, guard, guardRests, rules } = options
|
|
149
267
|
const withLens = (f: Finding): Finding & Origin => {
|
|
150
268
|
const lens = options.lensOf?.(f)
|
|
151
269
|
return lens === undefined ? f : { ...f, lens }
|
|
@@ -153,36 +271,24 @@ export const triageFindings = async (options: TriageOptions): Promise<LensReport
|
|
|
153
271
|
const findings = findingsOf(read(QUALITY) ?? "").map(withLens)
|
|
154
272
|
if (findings.length === 0) return NO_FINDINGS
|
|
155
273
|
const { mechanical, noise, design } = await classify(findings)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
274
|
+
let auto = NOTHING_FIXED
|
|
275
|
+
if (mechanical.length > 0) {
|
|
276
|
+
// Rewritten whenever it is not one chunk per mechanical finding: the fixer deletes chunks.
|
|
277
|
+
const sorted = t.qualityFindingsFile(mechanical)
|
|
278
|
+
if (read(QUALITY) !== sorted)
|
|
279
|
+
await run("quality.sorting", writeScript(QUALITY, sorted), {
|
|
280
|
+
label: "Setting findings aside for the finding gate",
|
|
281
|
+
})
|
|
282
|
+
auto = await fixTurns(mechanical, options)
|
|
164
283
|
}
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
let open: readonly Finding[] = mechanical
|
|
168
|
-
for (let turns = 0; turns < FIX_CAP && open.length > 0; turns++) {
|
|
169
|
-
await guarded(() => fixQuality(rules), guard)()
|
|
170
|
-
const turn = afterFix(open, findingsOf(read(QUALITY) ?? ""))
|
|
171
|
-
const commit = head()
|
|
172
|
-
fixed.push(...turn.fixed.map((f) => ({ ...f, commit })))
|
|
173
|
-
unclear.push(...turn.unclear)
|
|
174
|
-
// A turn that renamed a chunk hands back fresh parses: keep the lens a same-titled finding had.
|
|
175
|
-
open = turn.open.map((f) => ({ ...mechanical.find((m) => m.title === f.title), ...f }))
|
|
176
|
-
}
|
|
177
|
-
if (mechanical.length > 0)
|
|
178
|
-
await healthy(() => fixCheck(rules), { escalations, guard, guardRests, rules })
|
|
179
|
-
return {
|
|
180
|
-
notes: [
|
|
284
|
+
const gated = await findingGate(
|
|
285
|
+
[
|
|
181
286
|
...design,
|
|
182
|
-
...open.map((f) => ({ ...f, unresolved: true as const })),
|
|
183
|
-
...unclear.map((f) => ({ ...f, unclear: true as const })),
|
|
287
|
+
...auto.open.map((f) => ({ ...f, unresolved: true as const })),
|
|
288
|
+
...auto.unclear.map((f) => ({ ...f, unclear: true as const })),
|
|
184
289
|
],
|
|
185
|
-
fixed,
|
|
186
290
|
noise,
|
|
187
|
-
|
|
291
|
+
options,
|
|
292
|
+
)
|
|
293
|
+
return { ...gated, fixed: auto.fixed }
|
|
188
294
|
}
|
|
@@ -47,7 +47,7 @@ interface Drive {
|
|
|
47
47
|
readonly vars?: Readonly<Record<string, string>>
|
|
48
48
|
/** The REVIEW.md the human leaves; defaults to `notes` laid over the baseline. */
|
|
49
49
|
readonly after?: string
|
|
50
|
-
/** The REVIEW.md each `review.
|
|
50
|
+
/** The REVIEW.md each `review.reviewing` turn writes, in order; also raises the review-turn stop to one past the last. */
|
|
51
51
|
readonly reviewDocs?: readonly (string | undefined)[]
|
|
52
52
|
/** Let the run pass the second gate, stopping at the first range read after it. */
|
|
53
53
|
readonly passSecondGate?: boolean
|
|
@@ -94,7 +94,7 @@ const drive = async (d: Drive): Promise<Run> => {
|
|
|
94
94
|
let awaits = 0
|
|
95
95
|
let reviews = 0
|
|
96
96
|
const effects: Record<string, () => void> = {
|
|
97
|
-
"review.
|
|
97
|
+
"review.reviewing": () => {
|
|
98
98
|
const docs = d.reviewDocs
|
|
99
99
|
if (++reviews > (docs?.length ?? 0) + (docs === undefined ? 1 : 0)) throw new Stop()
|
|
100
100
|
const written = docs?.[reviews - 1]
|
|
@@ -184,10 +184,10 @@ describe("the risk-fix pass", () => {
|
|
|
184
184
|
it("a marked risk is fixed, kept green, re-reviewed, then rests at the gate", async () => {
|
|
185
185
|
const run = await drive({ notes: [], reviewDocs: [risky, clean] })
|
|
186
186
|
expect(run.log.slice(0, 5)).toEqual([
|
|
187
|
-
"review.
|
|
187
|
+
"review.reviewing",
|
|
188
188
|
"review.fix.risks.fixing",
|
|
189
189
|
"health.check",
|
|
190
|
-
"review.
|
|
190
|
+
"review.reviewing",
|
|
191
191
|
"review.await-review",
|
|
192
192
|
])
|
|
193
193
|
expect(run.prompts.get("review.fix.risks.fixing")).toContain("risk-1")
|
|
@@ -199,10 +199,10 @@ describe("the risk-fix pass", () => {
|
|
|
199
199
|
const run = await drive({ notes: [], reviewDocs: [risky, risky] })
|
|
200
200
|
expect(run.log.filter((n) => n === "review.fix.risks.fixing")).toHaveLength(1)
|
|
201
201
|
expect(run.log.slice(0, 5)).toEqual([
|
|
202
|
-
"review.
|
|
202
|
+
"review.reviewing",
|
|
203
203
|
"review.fix.risks.fixing",
|
|
204
204
|
"health.check",
|
|
205
|
-
"review.
|
|
205
|
+
"review.reviewing",
|
|
206
206
|
"review.await-review",
|
|
207
207
|
])
|
|
208
208
|
})
|
|
@@ -232,7 +232,7 @@ describe("the risk-fix pass", () => {
|
|
|
232
232
|
it("no marker means no fix-risks step", async () => {
|
|
233
233
|
const run = await drive({ notes: [], reviewDocs: [clean] })
|
|
234
234
|
expect(run.log).not.toContain("review.fix.risks.fixing")
|
|
235
|
-
expect(run.log[0]).toBe("review.
|
|
235
|
+
expect(run.log[0]).toBe("review.reviewing")
|
|
236
236
|
expect(run.log[1]).toBe("review.await-review")
|
|
237
237
|
})
|
|
238
238
|
|
|
@@ -243,16 +243,16 @@ describe("the risk-fix pass", () => {
|
|
|
243
243
|
reviewDocs: [undefined, risky, clean],
|
|
244
244
|
})
|
|
245
245
|
expect(run.log).toEqual([
|
|
246
|
-
"review.
|
|
246
|
+
"review.reviewing",
|
|
247
247
|
"review.await-review",
|
|
248
248
|
"review.triage",
|
|
249
249
|
"review.fix.nits.fixing",
|
|
250
250
|
"health.check",
|
|
251
251
|
"review.closing",
|
|
252
|
-
"review.
|
|
252
|
+
"review.reviewing",
|
|
253
253
|
"review.fix.risks.fixing",
|
|
254
254
|
"health.check",
|
|
255
|
-
"review.
|
|
255
|
+
"review.reviewing",
|
|
256
256
|
"review.await-review",
|
|
257
257
|
])
|
|
258
258
|
})
|
|
@@ -264,8 +264,9 @@ describe("the guard", () => {
|
|
|
264
264
|
|
|
265
265
|
it("runs right after the risk-fix turn", async () => {
|
|
266
266
|
const run = await drive({ notes: [], reviewDocs: [risky, clean], guarded: true })
|
|
267
|
-
expect(run.log.slice(0,
|
|
268
|
-
"review.
|
|
267
|
+
expect(run.log.slice(0, 5)).toEqual([
|
|
268
|
+
"review.reviewing",
|
|
269
|
+
"guard",
|
|
269
270
|
"review.fix.risks.fixing",
|
|
270
271
|
"guard",
|
|
271
272
|
"health.check",
|
|
@@ -278,7 +279,7 @@ describe("the guard", () => {
|
|
|
278
279
|
answers: { "note-1": verdict("nit") },
|
|
279
280
|
guarded: true,
|
|
280
281
|
})
|
|
281
|
-
expect(run.log.slice(
|
|
282
|
+
expect(run.log.slice(3, 7)).toEqual([
|
|
282
283
|
"review.triage",
|
|
283
284
|
"review.fix.nits.fixing",
|
|
284
285
|
"guard",
|
|
@@ -443,7 +444,7 @@ describe("review verdict routing", () => {
|
|
|
443
444
|
it("an unanswered note counts as edit", async () => {
|
|
444
445
|
const run = await drive({ notes: ["add — typo", "sub", "mul"], answers: {} })
|
|
445
446
|
expect(run.log).toEqual([
|
|
446
|
-
"review.
|
|
447
|
+
"review.reviewing",
|
|
447
448
|
"review.await-review",
|
|
448
449
|
"review.triage",
|
|
449
450
|
"review.closing",
|
|
@@ -478,7 +479,7 @@ describe("review verdict routing", () => {
|
|
|
478
479
|
it("only praise closes and signs off with no agent turn", async () => {
|
|
479
480
|
const run = await drive({ notes, answers: { "note-1": verdict("praise") } })
|
|
480
481
|
expect(run.log).toEqual([
|
|
481
|
-
"review.
|
|
482
|
+
"review.reviewing",
|
|
482
483
|
"review.await-review",
|
|
483
484
|
"review.triage",
|
|
484
485
|
"review.closing",
|
|
@@ -492,7 +493,7 @@ describe("review verdict routing", () => {
|
|
|
492
493
|
answers: { "note-1": verdict("question"), "note-2": verdict("question") },
|
|
493
494
|
})
|
|
494
495
|
expect(run.log).toEqual([
|
|
495
|
-
"review.
|
|
496
|
+
"review.reviewing",
|
|
496
497
|
"review.await-review",
|
|
497
498
|
"review.triage",
|
|
498
499
|
"review.answer-review-questions",
|
|
@@ -508,17 +509,17 @@ describe("review verdict routing", () => {
|
|
|
508
509
|
answers: { "note-1": verdict("question"), "note-2": verdict("nit") },
|
|
509
510
|
})
|
|
510
511
|
expect(run.log).toEqual([
|
|
511
|
-
"review.
|
|
512
|
+
"review.reviewing",
|
|
512
513
|
"review.await-review",
|
|
513
514
|
"review.triage",
|
|
514
515
|
"review.answer-review-questions",
|
|
515
516
|
"review.fix.nits.fixing",
|
|
516
517
|
"health.check",
|
|
517
518
|
"review.closing",
|
|
518
|
-
"review.
|
|
519
|
+
"review.reviewing",
|
|
519
520
|
])
|
|
520
521
|
expect(run.prompts.get("review.fix.nits.fixing")).not.toContain("why?")
|
|
521
|
-
expect(run.prompts.get("review.
|
|
522
|
+
expect(run.prompts.get("review.reviewing")).toContain("Run: git show c4")
|
|
522
523
|
expect(run.result).toBe("stopped")
|
|
523
524
|
})
|
|
524
525
|
|
|
@@ -527,8 +528,8 @@ describe("review verdict routing", () => {
|
|
|
527
528
|
notes: ["add — typo", "sub", "mul"],
|
|
528
529
|
answers: { "note-1": verdict("nit") },
|
|
529
530
|
})
|
|
530
|
-
expect(run.prompts.get("review.
|
|
531
|
-
expect(run.prompts.get("review.
|
|
531
|
+
expect(run.prompts.get("review.reviewing")).not.toContain("git show")
|
|
532
|
+
expect(run.prompts.get("review.reviewing")).toContain("worked on the human's nits")
|
|
532
533
|
})
|
|
533
534
|
|
|
534
535
|
it("edits run after questions and nits, and the capture names only the edits and the answering commit", async () => {
|
|
@@ -537,7 +538,7 @@ describe("review verdict routing", () => {
|
|
|
537
538
|
answers: { "note-2": verdict("question"), "note-3": verdict("nit") },
|
|
538
539
|
})
|
|
539
540
|
expect(run.log).toEqual([
|
|
540
|
-
"review.
|
|
541
|
+
"review.reviewing",
|
|
541
542
|
"review.await-review",
|
|
542
543
|
"review.triage",
|
|
543
544
|
"review.answer-review-questions",
|
|
@@ -557,7 +558,7 @@ describe("review verdict routing", () => {
|
|
|
557
558
|
it("a noted round with no extractable note collects as one edit, without judging", async () => {
|
|
558
559
|
const run = await drive({ notes: [], after: BEFORE + "\nSome stray prose.\n" })
|
|
559
560
|
expect(run.log).toEqual([
|
|
560
|
-
"review.
|
|
561
|
+
"review.reviewing",
|
|
561
562
|
"review.await-review",
|
|
562
563
|
"review.closing",
|
|
563
564
|
"review.collecting",
|
|
@@ -575,7 +576,7 @@ describe("review verdict routing", () => {
|
|
|
575
576
|
})
|
|
576
577
|
|
|
577
578
|
describe("the default quality lenses", () => {
|
|
578
|
-
it("are the
|
|
579
|
+
it("are the seven lenses in the settled order", () => {
|
|
579
580
|
expect(bundled.defaults.qualityReviews!.split(",").map((l) => l.trim())).toEqual([
|
|
580
581
|
"correctness",
|
|
581
582
|
"owasp-security",
|
|
@@ -583,6 +584,7 @@ describe("the default quality lenses", () => {
|
|
|
583
584
|
"test-audit",
|
|
584
585
|
"conventions",
|
|
585
586
|
"spec-challenge",
|
|
587
|
+
"criteria-coverage",
|
|
586
588
|
])
|
|
587
589
|
})
|
|
588
590
|
|
|
@@ -590,6 +592,7 @@ describe("the default quality lenses", () => {
|
|
|
590
592
|
expect(Object.keys(bundled.builtInLenses).sort()).toEqual([
|
|
591
593
|
"conventions",
|
|
592
594
|
"correctness",
|
|
595
|
+
"criteria-coverage",
|
|
593
596
|
"spec-challenge",
|
|
594
597
|
])
|
|
595
598
|
})
|
|
@@ -680,7 +683,7 @@ describe("severity routing", () => {
|
|
|
680
683
|
answers: { "note-1": verdict("edit"), ...sev("note-1", could) },
|
|
681
684
|
})
|
|
682
685
|
expect(run.log).toEqual([
|
|
683
|
-
"review.
|
|
686
|
+
"review.reviewing",
|
|
684
687
|
"review.await-review",
|
|
685
688
|
"review.triage",
|
|
686
689
|
"review.closing",
|
|
@@ -760,7 +763,7 @@ describe("BuildTailOptions.lensBase", () => {
|
|
|
760
763
|
if (request.kind === "restart") return Promise.resolve()
|
|
761
764
|
if (request.kind === "agent" && request.name.startsWith("quality."))
|
|
762
765
|
prompts.push(request.prompt)
|
|
763
|
-
if (request.name === "review.
|
|
766
|
+
if (request.name === "review.reviewing") throw new Stop()
|
|
764
767
|
return Promise.resolve()
|
|
765
768
|
},
|
|
766
769
|
pushScope: () => undefined,
|
|
@@ -998,7 +1001,7 @@ describe("buildTail without rules", () => {
|
|
|
998
1001
|
if (request.kind === "agent") prompts.set(request.name, request.prompt)
|
|
999
1002
|
if (request.name === "health.check") checks++
|
|
1000
1003
|
if (request.name === "fix.quality.fixing") files.delete(".gtd/QUALITY.md")
|
|
1001
|
-
if (request.name === "review.
|
|
1004
|
+
if (request.name === "review.reviewing" || request.name === "health.stop")
|
|
1002
1005
|
throw new Stop()
|
|
1003
1006
|
return Promise.resolve(
|
|
1004
1007
|
request.kind === "judge"
|