@pmelab/gtd 55.0.0 → 57.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 +72 -80
- package/dist/chunks/{reserved-DHafcW7t.mjs → reserved-X2RkNHin.mjs} +110 -318
- package/dist/chunks/{review-BJGKwlYv.mjs → review-v7sh5GF4.mjs} +30 -125
- package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
- package/dist/chunks/{tail-B9Hw2AFB.mjs → tail-S04KEewi.mjs} +144 -56
- 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 +63 -175
- 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 +603 -727
- 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 +1 -7
- package/src/workflows/feature/criteria.test.ts +1 -41
- package/src/workflows/feature/criteria.ts +2 -14
- package/src/workflows/feature/fast-path.feature +0 -1
- package/src/workflows/feature/feature.test.ts +1 -1
- package/src/workflows/feature/feature.ts +16 -25
- package/src/workflows/feature/flag-mode.feature +1 -1
- package/src/workflows/feature/lens-models.feature +0 -1
- package/src/workflows/feature/lens-review.feature +323 -172
- package/src/workflows/feature/naive-test.feature +258 -0
- package/src/workflows/feature/one-step-land.feature +0 -1
- package/src/workflows/feature/refactor-absorbs.feature +1 -8
- package/src/workflows/feature/refactor-first.feature +2 -11
- package/src/workflows/feature/refactor-rebuild.feature +4 -25
- package/src/workflows/feature/refactor-review.feature +33 -118
- package/src/workflows/feature/refactor.test.ts +0 -2
- package/src/workflows/feature/refactor.ts +0 -1
- package/src/workflows/feature/review-gate.feature +7 -24
- package/src/workflows/feature/reviewer-walkthrough.feature +16 -104
- package/src/workflows/feature/seam-rejection.feature +0 -1
- package/src/workflows/feature/seam.feature +0 -1
- package/src/workflows/feature/seam.test.ts +1 -2
- package/src/workflows/feature/seam.ts +56 -10
- package/src/workflows/feature/standards.feature +75 -35
- package/src/workflows/fix/fix-lens-origin.feature +20 -7
- package/src/workflows/fix/fix-quality.feature +102 -23
- package/src/workflows/review/review-door.feature +62 -59
- 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/options.ts +9 -1
- package/src/workflows/shared/prose.ts +0 -6
- package/src/workflows/shared/review.test.ts +27 -26
- package/src/workflows/shared/review.ts +29 -196
- package/src/workflows/shared/skills.test.ts +0 -4
- package/src/workflows/shared/skills.ts +0 -5
- package/src/workflows/shared/steps.ts +1 -1
- package/src/workflows/shared/tail.ts +18 -9
- package/src/workflows/shared/text.test.ts +125 -329
- package/src/workflows/shared/text.ts +173 -471
- package/src/workflows/shared/vars.ts +2 -3
- package/src/workflows/feature/hands-on-skip.feature +0 -316
- package/src/workflows/feature/prototype-rounds.feature +0 -156
- package/src/workflows/feature/prototype.feature +0 -428
- package/src/workflows/feature/prototype.test.ts +0 -43
- package/src/workflows/feature/prototype.ts +0 -198
- 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
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
requireRevert,
|
|
10
10
|
restoreScript,
|
|
11
11
|
run,
|
|
12
|
-
showScript,
|
|
13
12
|
vars,
|
|
14
13
|
writeScript,
|
|
15
14
|
type JudgeQuestion,
|
|
@@ -21,7 +20,6 @@ import {
|
|
|
21
20
|
reviewChunks,
|
|
22
21
|
reviewRisks,
|
|
23
22
|
reviewSections,
|
|
24
|
-
sectionedReview,
|
|
25
23
|
type ReviewNote,
|
|
26
24
|
} from "../../steering/index.js"
|
|
27
25
|
import * as kit from "../../workflow-kit/index.js"
|
|
@@ -195,214 +193,59 @@ const triage =
|
|
|
195
193
|
)
|
|
196
194
|
}
|
|
197
195
|
|
|
198
|
-
/** Review order: security, one-way doors, the judge unsure, then should, then could. */
|
|
199
|
-
const chunkKey = (s: Severity): number =>
|
|
200
|
-
s.tier === "could" ? 4 : s.tier === "should" ? 3 : s.security === true ? 0 : s.rank === 0 ? 1 : 2
|
|
201
|
-
|
|
202
|
-
type Pointer = ReturnType<typeof reviewChunks>[number]["files"][number]
|
|
203
|
-
|
|
204
|
-
/** A chunk's text, then the code each of its pointers covers: a bare path the whole file. */
|
|
205
|
-
const chunkEvidence = (text: string, files: readonly Pointer[]): string =>
|
|
206
|
-
[
|
|
207
|
-
text,
|
|
208
|
-
...files.map(({ path, line, rangeEnd }) => {
|
|
209
|
-
const where = `${path}${line === undefined ? "" : `#${line}${rangeEnd === undefined ? "" : `-${rangeEnd}`}`}`
|
|
210
|
-
const lines = read(path.replace(/^\.\//, ""))?.split("\n")
|
|
211
|
-
if (lines === undefined) return `${where}: no longer exists`
|
|
212
|
-
const code = line === undefined ? lines : lines.slice(line - 1, rangeEnd ?? line)
|
|
213
|
-
return `${where}:\n${code.join("\n")}`
|
|
214
|
-
}),
|
|
215
|
-
].join("\n\n")
|
|
216
|
-
|
|
217
|
-
const bytes = (text: string): number => new TextEncoder().encode(text).length
|
|
218
|
-
|
|
219
|
-
/** Fills each batch while the budget's even share still holds its widest evidence, so nothing is cut that fits alone. */
|
|
220
|
-
const batched = <T extends { readonly evidence: string }>(
|
|
221
|
-
items: readonly T[],
|
|
222
|
-
budget: number,
|
|
223
|
-
): T[][] => {
|
|
224
|
-
const batches: T[][] = []
|
|
225
|
-
let batch: T[] = []
|
|
226
|
-
let widest = 0
|
|
227
|
-
for (const item of items) {
|
|
228
|
-
const size = bytes(item.evidence)
|
|
229
|
-
if (batch.length > 0 && Math.floor(budget / (batch.length + 1)) < Math.max(widest, size)) {
|
|
230
|
-
batches.push(batch)
|
|
231
|
-
batch = []
|
|
232
|
-
widest = 0
|
|
233
|
-
}
|
|
234
|
-
batch.push(item)
|
|
235
|
-
widest = Math.max(widest, size)
|
|
236
|
-
}
|
|
237
|
-
return batch.length === 0 ? batches : [...batches, batch]
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Rates every chunk the reviewer wrote and sorts the \`findings\` among them
|
|
242
|
-
* first in \`full\`, each led by its tier and reason; a walkthrough chunk
|
|
243
|
-
* keeps its place and carries no tier. It is still rated, so a review with no
|
|
244
|
-
* findings still reaches \`review.ranking\` and older histories replay.
|
|
245
|
-
*/
|
|
246
|
-
const ranked = async (
|
|
247
|
-
review: string,
|
|
248
|
-
full: string,
|
|
249
|
-
findings: ReadonlySet<string>,
|
|
250
|
-
): Promise<string> => {
|
|
251
|
-
const texts = t.chunkTexts(full)
|
|
252
|
-
const chunks = reviewChunks(review).map((c, i) => ({
|
|
253
|
-
id: `C${i + 1}`,
|
|
254
|
-
title: c.title,
|
|
255
|
-
evidence: chunkEvidence(texts.get(c.title) ?? "", c.files),
|
|
256
|
-
}))
|
|
257
|
-
if (chunks.length === 0) return full
|
|
258
|
-
const minP = numeric(vars.reviewSeverityMinP, Infinity)
|
|
259
|
-
const batches = batched(chunks, numeric(vars.judgeBudgetBytes, 32768))
|
|
260
|
-
const severities = new Map<string, Severity>()
|
|
261
|
-
for (const [i, batch] of batches.entries()) {
|
|
262
|
-
const judgment = await judge("review.ranking", {
|
|
263
|
-
questions: batch.flatMap((c) =>
|
|
264
|
-
severityQuestions(
|
|
265
|
-
c.id,
|
|
266
|
-
`the change review chunk "${c.id}" ("${c.title}") walks through`,
|
|
267
|
-
`the change in review chunk "${c.id}"`,
|
|
268
|
-
),
|
|
269
|
-
),
|
|
270
|
-
evidence: Object.fromEntries(batch.map((c) => [c.id, c.evidence])),
|
|
271
|
-
message: t.reviewRankingMessage(),
|
|
272
|
-
label:
|
|
273
|
-
batches.length === 1
|
|
274
|
-
? "Ranking the review"
|
|
275
|
-
: `Ranking the review (${i + 1}/${batches.length})`,
|
|
276
|
-
})
|
|
277
|
-
for (const c of batch) severities.set(c.id, severityOf(c, judgment, minP))
|
|
278
|
-
}
|
|
279
|
-
return t.rankedReview(
|
|
280
|
-
full,
|
|
281
|
-
chunks
|
|
282
|
-
.map((c, i) => ({ c, i, s: severities.get(c.id)! }))
|
|
283
|
-
.filter(({ c }) => findings.has(c.title))
|
|
284
|
-
.sort((x, y) => chunkKey(x.s) - chunkKey(y.s) || x.i - y.i)
|
|
285
|
-
.map(({ c, s }) => ({ title: c.title, label: s.reason })),
|
|
286
|
-
)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
196
|
export const isCode = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
|
|
290
197
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
report: LensReport,
|
|
294
|
-
nitFix: string | undefined,
|
|
295
|
-
answered: ReadonlySet<string>,
|
|
296
|
-
): t.ComposedReview => ({
|
|
297
|
-
...report,
|
|
298
|
-
nitFix,
|
|
299
|
-
answered,
|
|
300
|
-
standards: {
|
|
301
|
-
added: standardsAddedSince(base),
|
|
302
|
-
undecided: parseStandards(read(STANDARDS) ?? "").undecided,
|
|
303
|
-
},
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* gtd writes `.gtd/REVIEW.md` from the quality lap's `report` — no reviewer
|
|
308
|
-
* turn. A re-review
|
|
309
|
-
* after fixed nits restores the doc as committed at `carry`, answers kept;
|
|
310
|
-
* a fresh one leaves out the notes the human already answered.
|
|
311
|
-
*/
|
|
312
|
-
const composing = (
|
|
313
|
-
base: string,
|
|
314
|
-
report: LensReport,
|
|
315
|
-
carry?: string,
|
|
316
|
-
nitFix?: string,
|
|
317
|
-
answered: ReadonlySet<string> = new Set(),
|
|
318
|
-
): Promise<void> =>
|
|
319
|
-
run(
|
|
320
|
-
"review.composing",
|
|
321
|
-
carry === undefined
|
|
322
|
-
? writeScript(
|
|
323
|
-
REVIEW,
|
|
324
|
-
t.composedReview(base, {
|
|
325
|
-
...composedOf(base, report, nitFix, answered),
|
|
326
|
-
weakened: kit.removedAssertions(changesSince(base)),
|
|
327
|
-
}),
|
|
328
|
-
)
|
|
329
|
-
: showScript(carry, REVIEW, REVIEW),
|
|
330
|
-
{ label: "Composing the review", file: REVIEW, base },
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Refuses a review with a chunk pointing at no code, or a lap note without a
|
|
335
|
-
* chunk of its own. A range with no code change has nothing to point at, so
|
|
336
|
-
* there only a note's chunk needs a pointer.
|
|
337
|
-
*/
|
|
338
|
-
const requirePointers = (
|
|
339
|
-
review: string,
|
|
340
|
-
notes: readonly { title: string }[],
|
|
341
|
-
codeChanged: boolean,
|
|
342
|
-
): void => {
|
|
198
|
+
/** Refuses a review with a chunk pointing at no code; a range with no code change has nothing to point at. */
|
|
199
|
+
const requirePointers = (review: string, codeChanged: boolean): void => {
|
|
343
200
|
const chunks = reviewChunks(review)
|
|
344
|
-
const titles = new Set(chunks.map((c) => c.title))
|
|
345
|
-
const noted = new Set(notes.map((n) => n.title))
|
|
346
201
|
const problems = [
|
|
347
202
|
...chunks
|
|
348
|
-
.filter((c) => c.pointers === 0 &&
|
|
203
|
+
.filter((c) => c.pointers === 0 && codeChanged)
|
|
349
204
|
.map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — points at no code`),
|
|
350
205
|
...chunks
|
|
351
206
|
.filter((c) => isConflictTitle(c.title))
|
|
352
207
|
.map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — gtd writes conflicts; leave it out`),
|
|
353
|
-
...notes.filter((n) => !titles.has(n.title)).map((n) => ` - ${n.title} — no chunk of its own`),
|
|
354
208
|
// Notes and answers are matched to their chunk by title.
|
|
355
209
|
...chunks
|
|
356
210
|
.filter((c, i) => chunks.findIndex((d) => d.title === c.title) !== i)
|
|
357
211
|
.map((c) => ` - ${REVIEW}:${c.line + 1}: ${c.title} — a second chunk with this title`),
|
|
358
212
|
...reviewSections(review).map(
|
|
359
213
|
(s) =>
|
|
360
|
-
` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} —
|
|
214
|
+
` - ${REVIEW}:${s.line + 1}: ${REVIEW_SECTIONS[s.section]} — write \`## \` chunks only`,
|
|
361
215
|
),
|
|
362
|
-
// A risk is fixed before the human sees it; a note is the human's to decide.
|
|
363
|
-
...reviewRisks(review)
|
|
364
|
-
.filter((r) => [...noted].some((title) => r.anchor.startsWith(`${title} ./`)))
|
|
365
|
-
.map((r) => ` - ${r.anchor} — no \`Risk:\` in a quality finding's chunk`),
|
|
366
216
|
]
|
|
367
217
|
if (problems.length === 0) return
|
|
368
218
|
refuse(
|
|
369
|
-
`gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code
|
|
219
|
+
`gtd land: review-pointers: ${problems.length} problem(s) in ${REVIEW} — every chunk needs a pointer to its code:\n${problems.join("\n")}`,
|
|
370
220
|
)
|
|
371
221
|
}
|
|
372
222
|
|
|
373
223
|
/**
|
|
374
224
|
* A reviewer walks the human through everything since `base`, every chunk
|
|
375
|
-
* pointing at its code
|
|
376
|
-
* then adds each note's text and answer boxes, and the summary chunks.
|
|
225
|
+
* pointing at its code; gtd then appends the summary chunks.
|
|
377
226
|
*/
|
|
378
227
|
const walkthrough = async (
|
|
379
228
|
base: string,
|
|
380
|
-
|
|
229
|
+
summary: t.ReviewSummary,
|
|
381
230
|
lap: t.ReviewRange,
|
|
382
231
|
carry: string | undefined,
|
|
232
|
+
nitFix: string | undefined,
|
|
383
233
|
afterRiskFix: boolean,
|
|
384
234
|
): Promise<void> => {
|
|
385
|
-
|
|
386
|
-
await reviewing(base, carry, lap, t.walkthroughBlock(notes, report.nitFix))
|
|
235
|
+
await reviewing(base, carry, lap, t.nitFixBlock(nitFix))
|
|
387
236
|
const written = read(REVIEW)
|
|
388
237
|
// A review the reviewer did not write is the round's own refusal or rest.
|
|
389
238
|
if (written === undefined) return
|
|
390
|
-
const review = t.unranked(written)
|
|
391
|
-
// Against the file as written, so a refusal's line numbers match it.
|
|
392
239
|
requirePointers(
|
|
393
240
|
written,
|
|
394
|
-
notes,
|
|
395
241
|
changesSince(base).some((c) => isCode(c.path)),
|
|
396
242
|
)
|
|
397
|
-
// Its risks are fixed and the review written again, so the re-reviewer edits this draft, never gtd's
|
|
243
|
+
// Its risks are fixed and the review written again, so the re-reviewer edits this draft, never gtd's additions.
|
|
398
244
|
if (!afterRiskFix && reviewRisks(written).length > 0) return
|
|
399
|
-
const full =
|
|
400
|
-
await ranked(review, t.withWalkthrough(review, report), new Set(notes.map((n) => n.title))),
|
|
401
|
-
new Set(reviewChunks(review).map((c) => c.title)),
|
|
402
|
-
)
|
|
245
|
+
const full = t.withSummary(written, summary)
|
|
403
246
|
if (full !== written) {
|
|
404
247
|
await run("review.carrying", writeScript(REVIEW, full), {
|
|
405
|
-
label: "
|
|
248
|
+
label: "Adding the quality lap's summary",
|
|
406
249
|
file: REVIEW,
|
|
407
250
|
base,
|
|
408
251
|
})
|
|
@@ -410,12 +253,10 @@ const walkthrough = async (
|
|
|
410
253
|
}
|
|
411
254
|
|
|
412
255
|
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* caller to build, questions are answered in place, nits fixed in one batch,
|
|
418
|
-
* praise dropped.
|
|
256
|
+
* A reviewer's walkthrough of everything since `base` reaches a human, who
|
|
257
|
+
* may comment: each note is judged — edits end the review as `feedback` for
|
|
258
|
+
* the caller to build, questions are answered in place, nits fixed in one
|
|
259
|
+
* batch, praise dropped.
|
|
419
260
|
*/
|
|
420
261
|
export const humanReview = (
|
|
421
262
|
base: string,
|
|
@@ -432,8 +273,6 @@ export const humanReview = (
|
|
|
432
273
|
const severities = new Map<string, Severity>()
|
|
433
274
|
// Set by a nit fix, so the fresh review that follows names its commit.
|
|
434
275
|
let nitFix: string | undefined
|
|
435
|
-
// Every note the human answered in this review, left out of a fresh one.
|
|
436
|
-
const answered = new Set<string>()
|
|
437
276
|
// Set by a risk fix, so the review written next reaches the human whatever it marks.
|
|
438
277
|
let riskFixed = false
|
|
439
278
|
return kit.review(base, {
|
|
@@ -444,20 +283,17 @@ export const humanReview = (
|
|
|
444
283
|
const fixedAt = nitFix
|
|
445
284
|
const afterRiskFix = riskFixed
|
|
446
285
|
riskFixed = false
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
guard,
|
|
459
|
-
)()
|
|
460
|
-
: composing(b, report, carry, fixedAt, answered)
|
|
286
|
+
const summary: t.ReviewSummary = {
|
|
287
|
+
report: options.report ?? NO_FINDINGS,
|
|
288
|
+
standards: {
|
|
289
|
+
added: standardsAddedSince(b),
|
|
290
|
+
undecided: parseStandards(read(STANDARDS) ?? "").undecided,
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
return guarded(
|
|
294
|
+
() => walkthrough(b, summary, options.lap ?? "first", carry, fixedAt, afterRiskFix),
|
|
295
|
+
guard,
|
|
296
|
+
)()
|
|
461
297
|
},
|
|
462
298
|
fixRisks: guarded((notes) => {
|
|
463
299
|
riskFixed = true
|
|
@@ -472,7 +308,6 @@ export const humanReview = (
|
|
|
472
308
|
reviewMissing,
|
|
473
309
|
collecting: feedback.collecting,
|
|
474
310
|
triage: triage(severities),
|
|
475
|
-
decided: (decisions) => decisions.forEach((d) => answered.add(d.anchor)),
|
|
476
311
|
settles: settlesStandard,
|
|
477
312
|
learn: learnStandards,
|
|
478
313
|
pending: standardsPending,
|
|
@@ -526,11 +361,9 @@ export interface BuildTailOptions extends TailOptions {
|
|
|
526
361
|
readonly feedback?: FeedbackTarget | undefined
|
|
527
362
|
/** Write access for the nit and risk fix turns; absent: the bundled `access` export decides. */
|
|
528
363
|
readonly fixAccess?: ScopeAccess | undefined
|
|
529
|
-
/** Which round
|
|
364
|
+
/** Which round the reviewer writes; absent: `first`. */
|
|
530
365
|
readonly lap?: t.ReviewRange | undefined
|
|
531
|
-
/**
|
|
532
|
-
readonly walkthrough?: boolean | undefined
|
|
533
|
-
/** What the triaged quality lap left for the review; absent: nothing. */
|
|
366
|
+
/** What the quality lap and its finding gate did, for the review's summary; absent: nothing. */
|
|
534
367
|
readonly report?: LensReport | undefined
|
|
535
368
|
}
|
|
536
369
|
|
|
@@ -29,10 +29,6 @@ describe("the bundled workflow's skills export", () => {
|
|
|
29
29
|
"implement",
|
|
30
30
|
"implement.fix.suite",
|
|
31
31
|
"implement.health",
|
|
32
|
-
"implement.prototype",
|
|
33
|
-
"implement.prototype.fix",
|
|
34
|
-
"implement.prototype.fix.suite",
|
|
35
|
-
"implement.prototype.health",
|
|
36
32
|
"build.quality.correctness",
|
|
37
33
|
"build.quality.owasp-security",
|
|
38
34
|
"build.quality.ponytail-review",
|
|
@@ -33,11 +33,6 @@ export const skills = (
|
|
|
33
33
|
seam: ["incremental-implementation"],
|
|
34
34
|
implement: ["test-driven-development", "incremental-implementation"],
|
|
35
35
|
"implement.fix.suite": ["debugging-and-error-recovery"],
|
|
36
|
-
// The hands-on tester runs the program, never the suite: no TDD.
|
|
37
|
-
"implement.prototype": [],
|
|
38
|
-
"implement.prototype.fix": ["test-driven-development", "incremental-implementation"],
|
|
39
|
-
"implement.prototype.fix.suite": ["debugging-and-error-recovery"],
|
|
40
|
-
"implement.prototype.health": ["debugging-and-error-recovery"],
|
|
41
36
|
// `health` is shared code reached from both `implement` and `build`.
|
|
42
37
|
"implement.health": ["debugging-and-error-recovery"],
|
|
43
38
|
...lensScopes(
|
|
@@ -14,7 +14,7 @@ export const REVIEW = ".gtd/REVIEW.md"
|
|
|
14
14
|
export const REFACTOR = ".gtd/REFACTOR.md"
|
|
15
15
|
export const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md"
|
|
16
16
|
export const QUALITY = ".gtd/QUALITY.md"
|
|
17
|
-
export const
|
|
17
|
+
export const NAIVE_TEST = ".gtd/NAIVE-TEST.md"
|
|
18
18
|
|
|
19
19
|
export const planner = (): string => env.plannerModel ?? ""
|
|
20
20
|
export const coder = (): string => env.coderModel ?? ""
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
vars,
|
|
9
9
|
writeScript,
|
|
10
10
|
} from "../../flows/index.js"
|
|
11
|
+
import { reviewRisks } from "../../steering/index.js"
|
|
11
12
|
import * as kit from "../../workflow-kit/index.js"
|
|
12
13
|
import { guarded, healthy, type EscalationCount, type Guard } from "./health.js"
|
|
13
14
|
import {
|
|
@@ -108,8 +109,8 @@ export interface TailOptions {
|
|
|
108
109
|
|
|
109
110
|
/**
|
|
110
111
|
* The build tail: fix (when entered red), keep green, the quality lap — a
|
|
111
|
-
* judge sorts its findings
|
|
112
|
-
*
|
|
112
|
+
* judge sorts its findings and the mechanical ones are fixed — then the
|
|
113
|
+
* finding gate over the rest, then `finish` with what became of them all.
|
|
113
114
|
*/
|
|
114
115
|
export function tail<T>(
|
|
115
116
|
finish: (escalations: EscalationCount, report: LensReport) => Promise<T>,
|
|
@@ -214,22 +215,29 @@ export const handOffReview = (
|
|
|
214
215
|
escalations: EscalationCount,
|
|
215
216
|
guard?: Guard,
|
|
216
217
|
report: LensReport = NO_FINDINGS,
|
|
217
|
-
): Promise<void> =>
|
|
218
|
-
|
|
218
|
+
): Promise<void> => {
|
|
219
|
+
// Set by a risk fix, so the re-review written next is summarised whatever it marks.
|
|
220
|
+
let riskFixed = false
|
|
221
|
+
return kit.reviewToHandOff(base, {
|
|
219
222
|
file: REVIEW,
|
|
220
223
|
reviewing: guarded(async (b, carry) => {
|
|
224
|
+
const afterRiskFix = riskFixed
|
|
225
|
+
riskFixed = false
|
|
221
226
|
await reviewing(b, carry)
|
|
222
|
-
// gtd adds the lap's chunks itself: .gtd/QUALITY.md is gone, so a reviewer leaving one out would lose it.
|
|
223
227
|
const review = read(REVIEW)
|
|
224
|
-
|
|
225
|
-
|
|
228
|
+
// A risk-marked draft is fixed and written again first: the re-reviewer edits it, never gtd's summary.
|
|
229
|
+
if (review === undefined || (!afterRiskFix && reviewRisks(review).length > 0)) return
|
|
230
|
+
const full = t.withSummary(review, { report })
|
|
226
231
|
if (full !== review) {
|
|
227
232
|
await run("review.carrying", writeScript(REVIEW, full), {
|
|
228
|
-
label: "Adding the quality lap's
|
|
233
|
+
label: "Adding the quality lap's summary",
|
|
229
234
|
})
|
|
230
235
|
}
|
|
231
236
|
}, guard),
|
|
232
|
-
fixRisks: guarded(
|
|
237
|
+
fixRisks: guarded((risks) => {
|
|
238
|
+
riskFixed = true
|
|
239
|
+
return fixRisks(risks)
|
|
240
|
+
}, guard),
|
|
233
241
|
keepGreen: () => healthy(fixCheck, { escalations, guard }),
|
|
234
242
|
awaitReview: (b) => awaitReview(b, t.handOffAwaitReviewMessage(b)),
|
|
235
243
|
reviewMissing,
|
|
@@ -246,6 +254,7 @@ export const handOffReview = (
|
|
|
246
254
|
deleted:
|
|
247
255
|
"gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix.",
|
|
248
256
|
})
|
|
257
|
+
}
|
|
249
258
|
|
|
250
259
|
/** The tail the fix workflow runs: ends at the hand-off review since `base`, which gets what the lap left. */
|
|
251
260
|
export const handOffTail = (fixFirst: boolean, base: string, guard?: Guard): Promise<void> =>
|