@pmelab/gtd 24.4.0 → 26.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 (39) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +15 -18
  3. package/dist/chunks/{reserved-B9pG93Fc.mjs → reserved-6pAgFGgB.mjs} +69 -125
  4. package/dist/gtd.bundle.mjs +28 -3
  5. package/dist/testkit.mjs +30 -5
  6. package/dist/workflow-kit.mjs +47 -90
  7. package/dist/workflows/feature.mjs +25 -47
  8. package/dist/workflows/fix.mjs +1 -1
  9. package/dist/workflows/review.mjs +1 -1
  10. package/dist/workflows.mjs +135 -261
  11. package/package.json +2 -2
  12. package/src/cli/Cli.test.ts +25 -0
  13. package/src/cli/Cli.ts +12 -1
  14. package/src/flags/index.test.ts +67 -0
  15. package/src/flags/index.ts +39 -0
  16. package/src/replay/Replay.ts +2 -1
  17. package/src/workflow/vars.test.ts +16 -0
  18. package/src/workflow/vars.ts +4 -1
  19. package/src/workflow-kit/scenarios.test.ts +116 -0
  20. package/src/workflow-kit/scenarios.ts +53 -113
  21. package/src/workflows/feature/feature.ts +2 -5
  22. package/src/workflows/feature/packages.test.ts +17 -34
  23. package/src/workflows/feature/packages.ts +13 -52
  24. package/src/workflows/feature/planning.ts +1 -1
  25. package/src/workflows/feature/review.test.ts +0 -47
  26. package/src/workflows/feature/review.ts +6 -10
  27. package/src/workflows/index.ts +0 -2
  28. package/src/workflows/shared/health.test.ts +1 -21
  29. package/src/workflows/shared/health.ts +4 -39
  30. package/src/workflows/shared/options.ts +0 -1
  31. package/src/workflows/shared/steps.ts +16 -11
  32. package/src/workflows/shared/tail.test.ts +1 -41
  33. package/src/workflows/shared/tail.ts +14 -30
  34. package/src/workflows/shared/text.fixture.ts +1 -0
  35. package/src/workflows/shared/text.test.ts +23 -41
  36. package/src/workflows/shared/text.ts +54 -74
  37. package/src/workflows/shared/vars.ts +0 -1
  38. package/src/workflows/shared/scenarios.test.ts +0 -256
  39. package/src/workflows/shared/scenarios.ts +0 -35
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  changesSince,
3
- type Change,
4
3
  glob,
5
4
  head,
6
5
  read,
@@ -10,12 +9,9 @@ import {
10
9
  scope,
11
10
  } from "../../flows/index.js"
12
11
  import { healthy } from "../shared/health.js"
13
- import { freezeScenarios, guarded, type FrozenScenarios } from "../shared/scenarios.js"
12
+ import { ARCHITECTURE } from "../shared/steps.js"
14
13
  import { build, fixSuite } from "./steps.js"
15
14
 
16
- /** Package 0: the e2e scenarios, written red. */
17
- export const SCENARIO_PACKAGE = ".gtd/packages/00-e2e-scenarios.md"
18
-
19
15
  export interface DeclaredTest {
20
16
  readonly level: "unit" | "e2e"
21
17
  readonly path: string
@@ -62,68 +58,33 @@ export const requireDeclaredTests = (pkg: string, since: string): void => {
62
58
  )
63
59
  }
64
60
 
65
- export interface PackageRange {
66
- readonly pkg: string
67
- readonly from: string
68
- readonly to: string
69
- }
70
-
71
- export interface BuiltPlan {
72
- readonly ranges: readonly PackageRange[]
73
- /** Paths package 0 added / modified outside `.gtd/`. */
74
- readonly scenarios: { readonly added: readonly string[]; readonly changed: readonly string[] }
75
- /** The wording snapshot after this run, to guard the tail and later laps. */
76
- readonly frozen: FrozenScenarios | undefined
77
- }
78
-
79
- /** Build `pkg`, check its declared tests, keep the fast suite green, and close it out, which removes it. */
80
- export const packageItem = async (pkg: string, frozen?: FrozenScenarios): Promise<PackageRange> => {
61
+ /**
62
+ * Build `pkg`, check its declared tests, keep the full suite green, and close
63
+ * it out, which removes it — and `.gtd/ARCHITECTURE.md` with the last one.
64
+ */
65
+ export const packageItem = async (pkg: string): Promise<void> => {
81
66
  const from = head()
82
67
  // Rechecked after every fix too: a fix deleting a red declared test would
83
- // otherwise green the suite. Before the wording gate, so it refuses the turn.
68
+ // otherwise green the suite.
84
69
  const declared = (turn: () => Promise<void>) => async () => {
85
70
  await turn()
86
71
  requireDeclaredTests(pkg, from)
87
72
  }
88
- await guarded(
89
- frozen,
90
- declared(() => build(pkg)),
91
- )()
92
- await healthy(guarded(frozen, declared(fixSuite)), { suite: "fast" })
93
- await run("closing", removeScript([pkg, ".gtd/SATISFIED.md"]), {
73
+ await declared(() => build(pkg))()
74
+ await healthy(declared(fixSuite))
75
+ const last = glob(".gtd/packages/*.md").length === 1
76
+ await run("closing", removeScript([pkg, ".gtd/SATISFIED.md", ...(last ? [ARCHITECTURE] : [])]), {
94
77
  label: "Closing out the package",
95
78
  })
96
- return { pkg, from, to: head() }
97
79
  }
98
80
 
99
81
  /** The first queued package — the one a package step works on. */
100
82
  export const nextPackage = (): string | undefined => [...glob(".gtd/packages/*.md")].sort()[0]
101
83
 
102
- const outsideGtd = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
103
-
104
84
  /** Build every package file under `.gtd/packages/`, in name order. */
105
- export const packages = (carried?: FrozenScenarios): Promise<BuiltPlan> =>
85
+ export const packages = (): Promise<void> =>
106
86
  scope("packages", async () => {
107
- let frozen = carried
108
- const ranges: PackageRange[] = []
109
- // Captured right after package 0: a later diff to the tree would also hold what 01…N touch.
110
- let touched: readonly Change[] = []
111
87
  for (let pkg = nextPackage(); pkg !== undefined; pkg = nextPackage()) {
112
- const range = await scope("item", () =>
113
- packageItem(pkg, pkg === SCENARIO_PACKAGE ? undefined : frozen),
114
- )
115
- ranges.push(range)
116
- if (pkg === SCENARIO_PACKAGE) {
117
- touched = changesSince(range.from).filter(
118
- (c) => outsideGtd(c.path) && c.status !== "deleted",
119
- )
120
- frozen = freezeScenarios(
121
- touched.filter((c) => c.path.endsWith(".feature")).map((c) => c.path),
122
- frozen,
123
- )
124
- }
88
+ await scope("item", () => packageItem(pkg))
125
89
  }
126
- const paths = (status: string): string[] =>
127
- touched.filter((c) => c.status === status).map((c) => c.path)
128
- return { ranges, scenarios: { added: paths("added"), changed: paths("modified") }, frozen }
129
90
  })
@@ -54,7 +54,7 @@ export const architecture = (): Promise<void> =>
54
54
  }
55
55
  if (changes(ARCHITECTURE).some((c) => c.status === "deleted")) {
56
56
  refuse(
57
- "gtd land: decompose: keep .gtd/ARCHITECTURE.md — the build tail's full run needs it; it is removed only once that run is green",
57
+ "gtd land: decompose: keep .gtd/ARCHITECTURE.md — the packages are built from it; it is removed with the last package",
58
58
  )
59
59
  }
60
60
  })
@@ -370,53 +370,6 @@ describe("the default quality lenses", () => {
370
370
  })
371
371
  })
372
372
 
373
- describe("scenario wording in review's fix turns", () => {
374
- const frozen = { at: "h1", texts: { "e2e/a.feature": "Given a" } }
375
- const risky = doc(["Risk: drops the carry", "sub"])
376
-
377
- it("a green-keeping fix after a risk fix that rewrites a scenario stops at review.scenario-wording", async () => {
378
- const log: string[] = []
379
- let red = false
380
- let drifted = false
381
- installContext(
382
- fixtureContext(
383
- {},
384
- {
385
- step: (request: StepRequest) => {
386
- if (request.kind === "restart") return Promise.resolve()
387
- log.push(request.name)
388
- if (request.name === "health.check") red = !red && !drifted
389
- if (request.name === "fix") drifted = true
390
- if (request.name === "scenario-wording") throw new Stop()
391
- return Promise.resolve()
392
- },
393
- pushScope: () => undefined,
394
- popScope: () => undefined,
395
- read: (path) =>
396
- path === REVIEW
397
- ? risky
398
- : path === "e2e/a.feature"
399
- ? drifted
400
- ? "Given b"
401
- : "Given a"
402
- : undefined,
403
- changes: (): readonly Change[] =>
404
- red
405
- ? [{ path: ".gtd/FEEDBACK.md", status: "added", before: undefined, after: "red" }]
406
- : [],
407
- matches: (path, pattern) => path === pattern,
408
- head: () => "h2",
409
- start: () => "h0",
410
- },
411
- ),
412
- )
413
- await review("base", undefined, frozen).catch((e: unknown) => {
414
- if (!(e instanceof Stop)) throw e
415
- })
416
- expect(log.slice(-2)).toEqual(["fix", "scenario-wording"])
417
- })
418
- })
419
-
420
373
  describe("severityOf", () => {
421
374
  const note = (forced = false): ReviewNote => ({
422
375
  id: "n",
@@ -10,7 +10,6 @@ import {
10
10
  import type { ReviewNote } from "../../steering/index.js"
11
11
  import * as kit from "../../workflow-kit/index.js"
12
12
  import { healthy, type EscalationCount } from "../shared/health.js"
13
- import { guarded, type FrozenScenarios } from "../shared/scenarios.js"
14
13
  import {
15
14
  awaitReview,
16
15
  fixCheck,
@@ -22,7 +21,6 @@ import {
22
21
  } from "../shared/steps.js"
23
22
  import { closeReview, tail } from "../shared/tail.js"
24
23
  import * as t from "../shared/text.js"
25
- import type { BuiltPlan } from "./packages.js"
26
24
  import { answerReviewQuestions, collecting, deferNotes, fixNits } from "./steps.js"
27
25
 
28
26
  export type { ReviewOutcome } from "../../workflow-kit/index.js"
@@ -167,7 +165,6 @@ const isCode = (path: string): boolean => path !== ".gtd" && !path.startsWith(".
167
165
  export const review = (
168
166
  base: string,
169
167
  escalations: EscalationCount = { rounds: 0 },
170
- frozen?: FrozenScenarios,
171
168
  ): Promise<kit.ReviewOutcome> => {
172
169
  const severities = new Map<string, Severity>()
173
170
  // Armed by a defer-only round and spent at the very next gate; a fresh
@@ -181,8 +178,8 @@ export const review = (
181
178
  deferred = []
182
179
  return reviewing(b, carry)
183
180
  },
184
- fixRisks: (risks) => guarded(frozen, () => fixRisks(risks), "review")(),
185
- keepGreen: () => healthy(guarded(frozen, fixCheck, "review"), { escalations }),
181
+ fixRisks,
182
+ keepGreen: () => healthy(fixCheck, { escalations }),
186
183
  awaitReview: (b) => {
187
184
  const notes = deferred
188
185
  deferred = []
@@ -203,7 +200,7 @@ export const review = (
203
200
  return deferNotes(deferred, head())
204
201
  },
205
202
  answerQuestions: answerReviewQuestions,
206
- fixNits: (notes) => guarded(frozen, () => fixNits(notes), "review")(),
203
+ fixNits,
207
204
  close: closeReview,
208
205
  captures: {
209
206
  edits: t.reviewEditsCapture,
@@ -238,10 +235,9 @@ export const review = (
238
235
  })
239
236
  }
240
237
 
241
- /** The build tail: fix (when entered red), keep green, the quality lap, then human review since `base`. */
238
+ /** The build tail: fix (when entered red), keep green unless `green`, the quality lap, then human review since `base`. */
242
239
  export const buildTail = (
243
240
  fixFirst: boolean,
244
241
  base: string,
245
- built?: BuiltPlan,
246
- ): Promise<kit.ReviewOutcome> =>
247
- tail(fixFirst, (escalations, frozen) => review(base, escalations, frozen), built)
242
+ green = false,
243
+ ): Promise<kit.ReviewOutcome> => tail(fixFirst, (escalations) => review(base, escalations), green)
@@ -9,7 +9,6 @@ export { ShippedOptions } from "./shared/options.js"
9
9
  export { agentWithSkills } from "./shared/text.js"
10
10
  export * from "./shared/steps.js"
11
11
  export * from "./shared/health.js"
12
- export * from "./shared/scenarios.js"
13
12
  export {
14
13
  closeReview,
15
14
  fixQualityFindings,
@@ -18,7 +17,6 @@ export {
18
17
  qualityLap,
19
18
  REVIEW_ORIG,
20
19
  tail,
21
- type TailPlan,
22
20
  } from "./shared/tail.js"
23
21
  export { lensesOf } from "../workflow-kit/index.js"
24
22
  export * from "./feature/steps.js"
@@ -1,7 +1,6 @@
1
1
  import { describe, expect, it } from "vitest"
2
2
  import { toRequest } from "../../judges/index.js"
3
- import { fastSuiteGuard, retryQuestion } from "./health.js"
4
- import { renderText } from "./text.fixture.js"
3
+ import { retryQuestion } from "./health.js"
5
4
 
6
5
  describe("retryQuestion", () => {
7
6
  it.each([true, false])("is sendable to jev (comparable=%s)", (comparable) => {
@@ -12,22 +11,3 @@ describe("retryQuestion", () => {
12
11
  ).toBeGreaterThanOrEqual(2)
13
12
  })
14
13
  })
15
-
16
- describe("fastSuiteGuard", () => {
17
- it("unset: names the setting and exits 0", () => {
18
- const lines = renderText(fastSuiteGuard).join("\n")
19
- expect(lines).toContain("fastTestCommand")
20
- expect(lines).toContain("GTD_FASTTESTCOMMAND")
21
- expect(lines).toContain("exit 0")
22
- })
23
-
24
- it("blank after trim counts as unset", () => {
25
- expect(renderText(fastSuiteGuard, { env: { fastTestCommand: " \t " } })).toContain("exit 0")
26
- })
27
-
28
- it("set: only removes SETUP", () => {
29
- const lines = renderText(fastSuiteGuard, { env: { fastTestCommand: "true" } })
30
- expect(lines.join("\n")).not.toContain("exit 0")
31
- expect(lines.join("\n")).toContain("rm -f")
32
- })
33
- })
@@ -1,4 +1,3 @@
1
- import { dirname } from "node:path"
2
1
  import {
3
2
  answered,
4
3
  check,
@@ -6,8 +5,6 @@ import {
6
5
  human,
7
6
  judge,
8
7
  numeric,
9
- quote,
10
- read,
11
8
  vars,
12
9
  type JudgeQuestion,
13
10
  } from "../../flows/index.js"
@@ -15,35 +12,12 @@ import * as kit from "../../workflow-kit/index.js"
15
12
  import { describeEscalation, escalate, escalationExhausted, ESCALATION, FEEDBACK } from "./steps.js"
16
13
  import * as t from "./text.js"
17
14
 
18
- /** Written while `fastTestCommand` is unset; its presence rests the process at the check. */
19
- export const SETUP = ".gtd/SETUP.md"
20
-
21
- const SETUP_TEXT =
22
- "The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it as a workflow option in `gtd.config.ts` or as GTD_FASTTESTCOMMAND."
23
-
24
- /** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
25
- export const fastSuiteGuard = (): string[] =>
26
- (env.fastTestCommand ?? "").trim() === ""
27
- ? [
28
- `mkdir -p ${quote(dirname(SETUP))}`,
29
- `printf '%s\\n' ${quote(SETUP_TEXT)} > ${quote(SETUP)}`,
30
- `printf '%s\\n' ${quote(SETUP_TEXT)} >&2`,
31
- "exit 0",
32
- ]
33
- : [`rm -f ${quote(SETUP)}`]
34
-
35
15
  /** Fix turns a run of red checks gets before it escalates. */
36
16
  export const FIX_CAP = 3
37
17
 
38
- const unconfigured = (): boolean => read(SETUP) !== undefined
39
-
40
18
  /** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
41
19
  export const baseline = (name: string, label = "Checking the baseline"): Promise<boolean> =>
42
- kit.untilSettled(
43
- () =>
44
- check(name, env.testCommand ?? "", { report: FEEDBACK, label, preamble: fastSuiteGuard() }),
45
- unconfigured,
46
- )
20
+ check(name, env.testCommand ?? "", { report: FEEDBACK, label })
47
21
 
48
22
  export type { EscalationCount } from "../../workflow-kit/index.js"
49
23
 
@@ -103,22 +77,17 @@ export interface HealthOptions {
103
77
  readonly fixesSoFar?: number
104
78
  /** Escalation rounds shared with other callers in the same run of red checks. */
105
79
  readonly escalations?: kit.EscalationCount
106
- /** `fast` runs `fastTestCommand` (default `full`: `testCommand`). */
107
- readonly suite?: "full" | "fast"
108
80
  /** Paths removed on a green run. */
109
81
  readonly sweepOnGreen?: readonly string[]
110
82
  }
111
83
 
112
- const runSuite = (options: HealthOptions): Promise<boolean> => {
113
- const fast = options.suite === "fast"
114
- return check("health.check", (fast ? env.fastTestCommand : env.testCommand) ?? "", {
84
+ const runSuite = (options: HealthOptions): Promise<boolean> =>
85
+ check("health.check", env.testCommand ?? "", {
115
86
  report: FEEDBACK,
116
87
  label: "Running checks",
117
88
  // Swept only on green: an unresolved analysis survives every retry.
118
89
  sweepOnGreen: [ESCALATION, ...(options.sweepOnGreen ?? [])],
119
- ...(fast ? { preamble: fastSuiteGuard() } : {}),
120
90
  })
121
- }
122
91
 
123
92
  /**
124
93
  * Loop until the suite is green: check, and on red run `fix` — escalating
@@ -127,11 +96,7 @@ const runSuite = (options: HealthOptions): Promise<boolean> => {
127
96
  */
128
97
  export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}): Promise<void> =>
129
98
  kit.healthy(fix, {
130
- // Unconfigured: straight back to the check, no fix turn or count.
131
- check:
132
- options.suite === "fast"
133
- ? () => kit.untilSettled(() => runSuite(options), unconfigured)
134
- : () => runSuite(options),
99
+ check: () => runSuite(options),
135
100
  report: FEEDBACK,
136
101
  fixCap: FIX_CAP,
137
102
  fixesSoFar: options.fixesSoFar ?? 0,
@@ -18,7 +18,6 @@ export const ShippedOptions = Schema.Struct({
18
18
  reviewSeverityMinP: setting,
19
19
  qualityReviews: setting,
20
20
  testCommand: setting,
21
- fastTestCommand: setting,
22
21
  plannerModel: setting,
23
22
  coderModel: setting,
24
23
  })
@@ -1,4 +1,5 @@
1
- import { env, human } from "../../flows/index.js"
1
+ import { changesSince, env, human } from "../../flows/index.js"
2
+ import { removedAssertions } from "../../workflow-kit/index.js"
2
3
  import * as t from "./text.js"
3
4
 
4
5
  // The bundled workflow's single steps. A step's name is relative to the
@@ -18,8 +19,8 @@ export const coder = (): string => env.coderModel ?? ""
18
19
 
19
20
  // ── Keeping the suite green ─────────────────────────────────────────────────
20
21
 
21
- export const fixCheck = (built?: t.BuildContext): Promise<void> =>
22
- t.agentWithSkills("fix", t.buildFixPrompt(built), {
22
+ export const fixCheck = (): Promise<void> =>
23
+ t.agentWithSkills("fix", t.buildFixPrompt(), {
23
24
  label: "Fixing the check",
24
25
  file: FEEDBACK,
25
26
  model: coder(),
@@ -90,14 +91,18 @@ export const fixQuality = (): Promise<void> =>
90
91
 
91
92
  /** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given. */
92
93
  export const reviewing = (base: string, carry?: string): Promise<void> =>
93
- t.agentWithSkills("review.reviewing", t.buildReviewReviewingPrompt(base, carry), {
94
- label: "Reviewing",
95
- file: REVIEW,
96
- mode: "review",
97
- model: planner(),
98
- system: t.reviewerSystem(),
99
- base,
100
- })
94
+ t.agentWithSkills(
95
+ "review.reviewing",
96
+ t.buildReviewReviewingPrompt(base, carry, removedAssertions(changesSince(base))),
97
+ {
98
+ label: "Reviewing",
99
+ file: REVIEW,
100
+ mode: "review",
101
+ model: planner(),
102
+ system: t.reviewerSystem(),
103
+ base,
104
+ },
105
+ )
101
106
 
102
107
  /** Fix the `Risk:`-marked notes the reviewer named; an empty turn means the risk was judged false. */
103
108
  export const fixRisks = (notes: readonly t.NoteInput[]): Promise<void> =>
@@ -5,51 +5,11 @@ import {
5
5
  type CodeThreadInfo,
6
6
  type StepRequest,
7
7
  } from "../../flows/index.js"
8
- import { fixQualityFindings, handOffReview, REVIEW_ORIG } from "./tail.js"
8
+ import { handOffReview, REVIEW_ORIG } from "./tail.js"
9
9
  import { fixtureContext } from "./text.fixture.js"
10
10
 
11
11
  afterEach(() => installContext(undefined))
12
12
 
13
- describe("the quality fix loop", () => {
14
- it("a fix that resolves QUALITY.md and rewrites a scenario ends after the wording gate", async () => {
15
- const QUALITY = ".gtd/QUALITY.md"
16
- const log: string[] = []
17
- const files = new Map([
18
- [QUALITY, "- finding"],
19
- ["e2e/a.feature", "Given a"],
20
- ])
21
- let last: readonly Change[] = []
22
- installContext(
23
- fixtureContext(
24
- {},
25
- {
26
- step: (request: StepRequest) => {
27
- if (request.kind === "restart") return Promise.resolve()
28
- log.push(request.name)
29
- last = []
30
- if (request.name === "fix.quality.fixing") {
31
- files.delete(QUALITY)
32
- files.set("e2e/a.feature", "Given b")
33
- last = [{ path: QUALITY, status: "deleted", before: "- finding", after: undefined }]
34
- }
35
- return Promise.resolve()
36
- },
37
- pushScope: () => undefined,
38
- popScope: () => undefined,
39
- read: (path) => files.get(path),
40
- changes: () => last,
41
- matches: (path, pattern) => path === pattern,
42
- head: () => "h2",
43
- start: () => "h0",
44
- },
45
- ),
46
- )
47
- const frozen = { at: "h1", texts: { "e2e/a.feature": "Given a" } }
48
- expect(await fixQualityFindings({ rounds: 0 }, frozen)).toBe(true)
49
- expect(log).toEqual(["fix.quality.fixing", "scenario-wording"])
50
- })
51
- })
52
-
53
13
  const REVIEW = ".gtd/REVIEW.md"
54
14
  const reviewDoc = (note: string): string =>
55
15
  [
@@ -1,9 +1,7 @@
1
1
  import { read, refuse, removeScript, run, scope, showScript, vars } from "../../flows/index.js"
2
2
  import * as kit from "../../workflow-kit/index.js"
3
3
  import { escalation, FIX_CAP, healthy, type EscalationCount } from "./health.js"
4
- import { guarded, type FrozenScenarios } from "./scenarios.js"
5
4
  import {
6
- ARCHITECTURE,
7
5
  awaitReview,
8
6
  fixCheck,
9
7
  fixQuality,
@@ -15,7 +13,6 @@ import {
15
13
  reviewQuality,
16
14
  } from "./steps.js"
17
15
  import * as t from "./text.js"
18
- import type { BuildContext } from "./text.js"
19
16
 
20
17
  /** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
21
18
  export const REVIEW_ORIG = ".gtd/REVIEW.orig.md"
@@ -29,22 +26,14 @@ export const qualityLap = (): Promise<"clean" | "findings"> =>
29
26
  kit.qualityLap(kit.lensesOf(vars.qualityReviews ?? ""), reviewQuality, QUALITY)
30
27
 
31
28
  /** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
32
- export const fixQualityFindings = (
33
- escalations: EscalationCount,
34
- frozen?: FrozenScenarios,
35
- ): Promise<boolean> =>
29
+ export const fixQualityFindings = (escalations: EscalationCount): Promise<boolean> =>
36
30
  kit.fixFindings({
37
- fix: guarded(frozen, fixQuality),
31
+ fix: fixQuality,
38
32
  findings: QUALITY,
39
33
  cap: FIX_CAP,
40
34
  escalate: () => escalation(escalations),
41
35
  })
42
36
 
43
- /** What the tail's full run knows of a plan the process built. */
44
- export interface TailPlan extends BuildContext {
45
- readonly frozen: FrozenScenarios | undefined
46
- }
47
-
48
37
  /** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
49
38
  export const closeReview = (round: string): Promise<void> =>
50
39
  run("review.closing", removeScript([REVIEW, REVIEW_ORIG]), {
@@ -63,36 +52,31 @@ export const refusePendingHandOff = (): void => {
63
52
 
64
53
  /**
65
54
  * The build tail: fix (when entered red), keep green, the quality lap, then
66
- * `finish` — the human review, as the calling workflow runs it.
55
+ * `finish` — the human review, as the calling workflow runs it. `green`: the
56
+ * caller just left the full suite green (the package loop), so no run opens it.
67
57
  */
68
58
  export const tail = <T>(
69
59
  fixFirst: boolean,
70
- finish: (escalations: EscalationCount, frozen: FrozenScenarios | undefined) => Promise<T>,
71
- built?: TailPlan,
60
+ finish: (escalations: EscalationCount) => Promise<T>,
61
+ green = false,
72
62
  ): Promise<T> =>
73
63
  scope("build", async () => {
74
- const frozen = built?.frozen
75
64
  const escalations: EscalationCount = { rounds: 0 }
76
- const guardedFix = guarded(frozen, () => fixCheck())
65
+ const fix = fixCheck
77
66
  const repair = async () => {
78
- await guardedFix()
79
- await healthy(guardedFix, { fixesSoFar: 1, escalations })
67
+ await fix()
68
+ await healthy(fix, { fixesSoFar: 1, escalations })
80
69
  }
81
70
  // fixFirst's own fix + healthy below already is the full run.
82
71
  if (fixFirst) await repair()
83
- else {
84
- await healthy(
85
- guarded(frozen, () => fixCheck(built)),
86
- { escalations, sweepOnGreen: [ARCHITECTURE] },
87
- )
88
- }
72
+ else if (!green) await healthy(fix, { escalations })
89
73
  // The lap runs once a tail: after its findings are fixed, review follows.
90
74
  if ((await qualityLap()) === "findings") {
91
- if (await fixQualityFindings(escalations, frozen)) {
92
- await healthy(guardedFix, { escalations })
75
+ if (await fixQualityFindings(escalations)) {
76
+ await healthy(fix, { escalations })
93
77
  } else await repair()
94
78
  }
95
- return finish(escalations, frozen)
79
+ return finish(escalations)
96
80
  })
97
81
 
98
82
  /**
@@ -105,7 +89,7 @@ export const handOffReview = (base: string, escalations: EscalationCount): Promi
105
89
  file: REVIEW,
106
90
  reviewing,
107
91
  fixRisks,
108
- keepGreen: () => healthy(() => fixCheck(), { escalations }),
92
+ keepGreen: () => healthy(fixCheck, { escalations }),
109
93
  awaitReview: (b) => awaitReview(b, t.handOffAwaitReviewMessage(b)),
110
94
  reviewMissing,
111
95
  close: closeReview,
@@ -114,6 +114,7 @@ export const captureStep = async (
114
114
  captured = request
115
115
  return Promise.resolve()
116
116
  },
117
+ changesSince: () => [],
117
118
  }),
118
119
  )
119
120
  try {
@@ -182,50 +182,11 @@ describe("architectureAuthorPrompt", () => {
182
182
  })
183
183
  })
184
184
 
185
- describe("buildFixPrompt with build context", () => {
186
- const built = {
187
- ranges: [
188
- { pkg: "00-scenarios", from: "aaa1111", to: "bbb2222" },
189
- { pkg: "01-engine", from: "bbb2222", to: "ccc3333" },
190
- ],
191
- scenarios: { added: ["tests/new.feature"], changed: ["tests/old.feature"] },
192
- }
193
-
194
- it("points at the architecture document", () => {
195
- expect(renderText(() => buildFixPrompt(built))).toContain(".gtd/ARCHITECTURE.md")
196
- })
197
-
198
- it("lists new scenario paths as likely missing wiring", () => {
199
- const prompt = renderText(() => buildFixPrompt(built))
200
- expect(prompt).toContain("tests/new.feature")
201
- expect(prompt).toContain("missing wiring")
202
- })
203
-
204
- it("lists changed or existing scenarios as regressions", () => {
205
- const prompt = renderText(() => buildFixPrompt(built))
206
- expect(prompt).toContain("regression")
207
- expect(prompt).toContain("tests/old.feature")
208
- })
209
-
210
- it("lists one range line per package", () => {
211
- const prompt = renderText(() => buildFixPrompt(built))
212
- expect(prompt).toContain("git log --oneline aaa1111..bbb2222 # 00-scenarios")
213
- expect(prompt).toContain("git log --oneline bbb2222..ccc3333 # 01-engine")
214
- })
215
-
216
- it("is today's prompt without a build context", () => {
185
+ describe("buildFixPrompt", () => {
186
+ it("starts each bullet on its own line", () => {
217
187
  const prompt = renderText(() => buildFixPrompt())
218
- expect(renderText(() => buildFixPrompt(undefined))).toBe(prompt)
219
188
  expect(prompt).toMatch(/\n- Leave everything uncommitted — do not commit\n$/)
220
189
  expect(prompt).toContain("writes its own\n- Leave")
221
- expect(prompt).not.toContain("ARCHITECTURE")
222
- expect(prompt).not.toContain("git log --oneline")
223
- })
224
-
225
- it("starts each bullet on its own line", () => {
226
- const prompt = renderText(() => buildFixPrompt(built))
227
- expect(prompt).toMatch(/^- Read `\.gtd\/ARCHITECTURE\.md`/m)
228
- expect(prompt).toMatch(/^- Leave everything uncommitted/m)
229
190
  })
230
191
  })
231
192
 
@@ -459,3 +420,24 @@ describe("deferNotesScript — the in-memory recognizer agrees with the real sh
459
420
  expect(repo.readFile(".gtd/TODO.md")).toBe(real)
460
421
  })
461
422
  })
423
+
424
+ describe("buildReviewReviewingPrompt scenarios", () => {
425
+ it("names each removed assertion and asks for a Weakened: note", () => {
426
+ const prompt = renderText(() =>
427
+ buildReviewReviewingPrompt("base", undefined, [
428
+ {
429
+ path: "e2e/a.feature",
430
+ lines: [{ scenario: "Scenario: shown", line: "Then it is shown" }],
431
+ },
432
+ ]),
433
+ )
434
+ expect(prompt).toContain("e2e/a.feature — Scenario: shown — Then it is shown")
435
+ expect(prompt).toContain("`Weakened:`")
436
+ })
437
+
438
+ it("asks for no Weakened: note when nothing was removed", () => {
439
+ const prompt = renderText(() => buildReviewReviewingPrompt("base"))
440
+ expect(prompt).toContain("## Scenarios")
441
+ expect(prompt).not.toContain("Weakened:")
442
+ })
443
+ })