@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.
Files changed (83) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +72 -80
  3. package/dist/chunks/{reserved-DHafcW7t.mjs → reserved-X2RkNHin.mjs} +110 -318
  4. package/dist/chunks/{review-BJGKwlYv.mjs → review-v7sh5GF4.mjs} +30 -125
  5. package/dist/chunks/{steering-CLhu8C7q.mjs → steering-CCNvngSH.mjs} +256 -65
  6. package/dist/chunks/{tail-B9Hw2AFB.mjs → tail-S04KEewi.mjs} +144 -56
  7. package/dist/gtd.bundle.mjs +259 -22
  8. package/dist/steering.mjs +2 -2
  9. package/dist/testkit.mjs +6 -6
  10. package/dist/web/main.css +1 -1
  11. package/dist/web/main.js +551 -66
  12. package/dist/workflow-kit.mjs +1 -1
  13. package/dist/workflows/cleanup.mjs +2 -2
  14. package/dist/workflows/feature.mjs +63 -175
  15. package/dist/workflows/fix.mjs +3 -3
  16. package/dist/workflows/review.mjs +3 -3
  17. package/dist/workflows/rollout.mjs +1 -1
  18. package/dist/workflows.mjs +603 -727
  19. package/package.json +1 -1
  20. package/skills/authoring/SKILL.md +3 -3
  21. package/src/cli/Cli.ts +5 -5
  22. package/src/edge/Edge.test.ts +1 -1
  23. package/src/steering/FORMATS.md +5 -1
  24. package/src/steering/SteeringFormats.test.ts +2 -2
  25. package/src/steering/contract.test.ts +1 -0
  26. package/src/steering/findings.test.ts +47 -0
  27. package/src/steering/findings.ts +350 -0
  28. package/src/steering/fixtures/findings/gate.md +34 -0
  29. package/src/steering/fixtures/findings/gate.read.json +87 -0
  30. package/src/steering/index.test.ts +2 -2
  31. package/src/steering/index.ts +4 -3
  32. package/src/steering/review.test.ts +1 -75
  33. package/src/steering/review.ts +1 -74
  34. package/src/workflow/compile.test.ts +1 -0
  35. package/src/workflows/feature/criteria-coverage.feature +1 -7
  36. package/src/workflows/feature/criteria.test.ts +1 -41
  37. package/src/workflows/feature/criteria.ts +2 -14
  38. package/src/workflows/feature/fast-path.feature +0 -1
  39. package/src/workflows/feature/feature.test.ts +1 -1
  40. package/src/workflows/feature/feature.ts +16 -25
  41. package/src/workflows/feature/flag-mode.feature +1 -1
  42. package/src/workflows/feature/lens-models.feature +0 -1
  43. package/src/workflows/feature/lens-review.feature +323 -172
  44. package/src/workflows/feature/naive-test.feature +258 -0
  45. package/src/workflows/feature/one-step-land.feature +0 -1
  46. package/src/workflows/feature/refactor-absorbs.feature +1 -8
  47. package/src/workflows/feature/refactor-first.feature +2 -11
  48. package/src/workflows/feature/refactor-rebuild.feature +4 -25
  49. package/src/workflows/feature/refactor-review.feature +33 -118
  50. package/src/workflows/feature/refactor.test.ts +0 -2
  51. package/src/workflows/feature/refactor.ts +0 -1
  52. package/src/workflows/feature/review-gate.feature +7 -24
  53. package/src/workflows/feature/reviewer-walkthrough.feature +16 -104
  54. package/src/workflows/feature/seam-rejection.feature +0 -1
  55. package/src/workflows/feature/seam.feature +0 -1
  56. package/src/workflows/feature/seam.test.ts +1 -2
  57. package/src/workflows/feature/seam.ts +56 -10
  58. package/src/workflows/feature/standards.feature +75 -35
  59. package/src/workflows/fix/fix-lens-origin.feature +20 -7
  60. package/src/workflows/fix/fix-quality.feature +102 -23
  61. package/src/workflows/review/review-door.feature +62 -59
  62. package/src/workflows/shared/learn.test.ts +0 -14
  63. package/src/workflows/shared/learn.ts +1 -6
  64. package/src/workflows/shared/lenses.ts +149 -43
  65. package/src/workflows/shared/options.ts +9 -1
  66. package/src/workflows/shared/prose.ts +0 -6
  67. package/src/workflows/shared/review.test.ts +27 -26
  68. package/src/workflows/shared/review.ts +29 -196
  69. package/src/workflows/shared/skills.test.ts +0 -4
  70. package/src/workflows/shared/skills.ts +0 -5
  71. package/src/workflows/shared/steps.ts +1 -1
  72. package/src/workflows/shared/tail.ts +18 -9
  73. package/src/workflows/shared/text.test.ts +125 -329
  74. package/src/workflows/shared/text.ts +173 -471
  75. package/src/workflows/shared/vars.ts +2 -3
  76. package/src/workflows/feature/hands-on-skip.feature +0 -316
  77. package/src/workflows/feature/prototype-rounds.feature +0 -156
  78. package/src/workflows/feature/prototype.feature +0 -428
  79. package/src/workflows/feature/prototype.test.ts +0 -43
  80. package/src/workflows/feature/prototype.ts +0 -198
  81. package/src/workflows/feature/review-answers.feature +0 -324
  82. package/src/workflows/feature/severity-budget.feature +0 -116
  83. 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
- const composedOf = (
292
- base: string,
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 && (codeChanged || noted.has(c.title)))
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]} — gtd sorts the chunks into sections; write \`## \` chunks only`,
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, and every quality finding its own chunk, headed exactly as the prompt gave it:\n${problems.join("\n")}`,
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 and each of the lap's notes a chunk of its own; gtd
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
- report: t.ComposedReview,
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
- const notes = t.titledNotes(report)
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 layout.
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 = sectionedReview(
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: "Ordering the review and adding the quality lap's findings",
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
- * The quality lap's notes reach a human over everything since `base`, who
414
- * answers each one — fix as suggested, won't fix, or their own answer — and
415
- * may comment. A fix joins the nit batch, a won't-fix is dropped, and an own
416
- * answer or comment is judged: edits end the review as `feedback` for the
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 report = options.report ?? NO_FINDINGS
448
- return options.walkthrough === true
449
- ? guarded(
450
- () =>
451
- walkthrough(
452
- b,
453
- composedOf(b, report, fixedAt, answered),
454
- options.lap ?? "first",
455
- carry,
456
- afterRiskFix,
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 a `walkthrough` reviewer writes; absent: `first`. */
364
+ /** Which round the reviewer writes; absent: `first`. */
530
365
  readonly lap?: t.ReviewRange | undefined
531
- /** A reviewer turn writes the review as a walkthrough of the code, the lap's notes among its chunks; absent: gtd composes it from the notes alone. */
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 PROTOTYPE = ".gtd/PROTOTYPE.md"
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, the mechanical ones are fixed, noise dropped —
112
- * then `finish` with the rest as notes: a human review, or carrying them on.
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
- kit.reviewToHandOff(base, {
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
- if (review === undefined) return
225
- const full = t.withLapFindings(review, report)
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 findings",
233
+ label: "Adding the quality lap's summary",
229
234
  })
230
235
  }
231
236
  }, guard),
232
- fixRisks: guarded(fixRisks, guard),
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> =>