@pmelab/gtd 30.1.0 → 31.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 (32) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +6 -1
  3. package/dist/chunks/{reserved-DDiiAZ8U.mjs → reserved-CHTJTzpA.mjs} +52 -3
  4. package/dist/chunks/{tail-ILPlELuy.mjs → tail-CCsDBbZe.mjs} +1 -1
  5. package/dist/workflows/cleanup.mjs +2 -2
  6. package/dist/workflows/feature.mjs +137 -8
  7. package/dist/workflows/fix.mjs +2 -2
  8. package/dist/workflows/review.mjs +2 -2
  9. package/dist/workflows/rollout.mjs +1 -1
  10. package/dist/workflows.mjs +188 -10
  11. package/package.json +1 -1
  12. package/src/workflows/feature/acceptance-criteria-tier.feature +25 -0
  13. package/src/workflows/feature/acceptance-criteria.feature +189 -0
  14. package/src/workflows/feature/criteria.test.ts +106 -0
  15. package/src/workflows/feature/criteria.ts +168 -0
  16. package/src/workflows/feature/fast-path.feature +4 -0
  17. package/src/workflows/feature/feature.test.ts +41 -11
  18. package/src/workflows/feature/feature.ts +7 -4
  19. package/src/workflows/feature/flag-mode.feature +8 -0
  20. package/src/workflows/feature/index.ts +1 -0
  21. package/src/workflows/feature/one-step-land.feature +4 -0
  22. package/src/workflows/feature/planning.ts +2 -0
  23. package/src/workflows/feature/refactor-absorbs.feature +4 -0
  24. package/src/workflows/feature/refactor-first.feature +4 -0
  25. package/src/workflows/feature/refactor-review.feature +24 -0
  26. package/src/workflows/feature/risk-tier.feature +32 -0
  27. package/src/workflows/feature/seam-rejection.feature +8 -0
  28. package/src/workflows/feature/seam.feature +8 -0
  29. package/src/workflows/feature/seam.ts +12 -7
  30. package/src/workflows/feature/tier-pin.feature +4 -0
  31. package/src/workflows/index.ts +1 -0
  32. package/src/workflows/shared/text.ts +77 -2
@@ -0,0 +1,168 @@
1
+ import {
2
+ answered,
3
+ changes,
4
+ judge,
5
+ read,
6
+ refuse,
7
+ run,
8
+ scope,
9
+ type Change,
10
+ } from "../../flows/index.js"
11
+ import { markFenced } from "../shared/fences.js"
12
+ import { REQUIREMENTS } from "../shared/steps.js"
13
+ import * as t from "../shared/text.js"
14
+
15
+ export interface Criterion {
16
+ readonly id: string
17
+ readonly text: string
18
+ }
19
+
20
+ /** Acceptance criteria the human's sketch stated outright, ready to normalise. */
21
+ export interface StatedCriteria {
22
+ readonly path: string
23
+ readonly commit: string
24
+ /** The file as the sketch left it: `unwind` reverts it out of the tree. */
25
+ readonly text: string
26
+ readonly criteria: readonly Criterion[]
27
+ }
28
+
29
+ /** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
30
+ const CLEAR_MIN_P = 0.8
31
+
32
+ const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/
33
+ const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/
34
+ const ID = /^(AC-\d+):\s*(.*)$/
35
+
36
+ /** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
37
+ const listUnder = (text: string, title: RegExp, level?: number): string[] | undefined => {
38
+ const lines = markFenced(text)
39
+ const at = lines.findIndex((l) => {
40
+ const heading = l.fenced ? null : HEADING.exec(l.line)
41
+ return (
42
+ heading !== null &&
43
+ (level === undefined || heading[1]!.length === level) &&
44
+ title.test(heading[2]!)
45
+ )
46
+ })
47
+ if (at === -1) return undefined
48
+ const items: string[] = []
49
+ let open = false
50
+ for (const { line, fenced } of lines.slice(at + 1)) {
51
+ if (fenced) {
52
+ open = false
53
+ continue
54
+ }
55
+ if (HEADING.test(line)) break
56
+ const item = ITEM.exec(line)
57
+ if (item !== null) {
58
+ items.push(item[1]!.trim())
59
+ open = true
60
+ } else if (open && line.trim() !== "") {
61
+ items[items.length - 1] = `${items.at(-1)!} ${line.trim()}`.trim()
62
+ } else open = false
63
+ }
64
+ return items
65
+ }
66
+
67
+ /** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
68
+ export const criteriaOf = (text: string): readonly Criterion[] | undefined =>
69
+ listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
70
+ const id = ID.exec(item)
71
+ return id === null ? { id: "", text: item } : { id: id[1]!, text: id[2]! }
72
+ })
73
+
74
+ /** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
75
+ const numbered = (items: readonly string[]): Criterion[] => {
76
+ const ids = items.map((item) => ID.exec(item))
77
+ const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id![1])).size === ids.length
78
+ return items.map((item, i) =>
79
+ own
80
+ ? { id: ids[i]![1]!, text: ids[i]![2]! }
81
+ : { id: `AC-${i + 1}`, text: item.replace(ID, "$2") },
82
+ )
83
+ }
84
+
85
+ const STATED = /^acceptance criteria:?$/i
86
+
87
+ const statedItems = (text: string | undefined): string[] =>
88
+ text === undefined ? [] : (listUnder(text, STATED) ?? []).filter((item) => item !== "")
89
+
90
+ /**
91
+ * The criteria a `TODO.md` in the sketch `sketch` (the idle turn's changes)
92
+ * added or changed under an `Acceptance criteria` heading. More than one such
93
+ * file is ambiguous, and any other change in the sketch is input only triage
94
+ * folds in, so none.
95
+ */
96
+ export const statedCriteria = (
97
+ sketch: readonly Change[],
98
+ commit: string,
99
+ ): StatedCriteria | undefined => {
100
+ const found = sketch.flatMap((change) => {
101
+ if (!/(^|\/)TODO\.md$/.test(change.path) || change.after === undefined) return []
102
+ const items = statedItems(change.after)
103
+ if (items.length === 0 || items.join("\n") === statedItems(change.before).join("\n")) return []
104
+ return [{ path: change.path, commit, text: change.after, criteria: numbered(items) }]
105
+ })
106
+ return found.length === 1 && sketch.length === 1 ? found[0] : undefined
107
+ }
108
+
109
+ const listing = (criteria: readonly Criterion[]): string => criteria.map(t.criterionLine).join("\n")
110
+
111
+ /** Refuse a turn that leaves `file` without ID'd acceptance criteria. A deleted file is `requireProgress`'s call. */
112
+ export const requireCriteria = (file: string): void => {
113
+ const text = read(file)
114
+ if (text === undefined) return
115
+ const criteria = criteriaOf(text) ?? []
116
+ const unnamed = criteria.filter((c) => c.id === "")
117
+ const ids = criteria.map((c) => c.id).filter((id) => id !== "")
118
+ const twice = ids.filter((id, i) => ids.indexOf(id) !== i)
119
+ if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return
120
+ const why =
121
+ criteria.length === 0
122
+ ? `no "## Acceptance Criteria" list`
123
+ : unnamed.length > 0
124
+ ? `${unnamed.length} criterion without an ID:\n${unnamed.map((c) => ` - ${c.text}`).join("\n")}`
125
+ : `duplicate IDs: ${[...new Set(twice)].join(", ")}`
126
+ refuse(
127
+ `gtd land: acceptance-criteria: ${file} has ${why} — list every criterion under "## Acceptance Criteria" as "- AC-<n>: <checkable criterion>", each ID unique and never reused`,
128
+ )
129
+ }
130
+
131
+ /** Refuse a turn that changed the acceptance criteria in `file`: after the design gate only another design lap may. */
132
+ export const requireCriteriaFrozen = (file: string): void => {
133
+ const change = changes().get(file)
134
+ if (change === undefined) return
135
+ const before = listing(criteriaOf(change.before ?? "") ?? [])
136
+ if (before === listing(criteriaOf(change.after ?? "") ?? [])) return
137
+ refuse(
138
+ `gtd land: criteria-frozen: the acceptance criteria in ${file} are settled — restore them; only a design lap changes them`,
139
+ )
140
+ }
141
+
142
+ /** Whether the judge trusts `stated` as clear and checkable, every criterion of it. Truncated evidence never is. */
143
+ const clear = async (stated: StatedCriteria): Promise<boolean> => {
144
+ const { answers, truncated } = await judge("clear", {
145
+ questions: [t.clearCriteriaQuestion],
146
+ evidence: { criteria: listing(stated.criteria), sketch: stated.text },
147
+ message: t.clearCriteriaMessage(),
148
+ label: "Judging the stated acceptance criteria",
149
+ })
150
+ return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P)
151
+ }
152
+
153
+ /**
154
+ * The skip path: when the sketch stated clear, checkable criteria, write them
155
+ * to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
156
+ * design gate. `false` when design must run.
157
+ */
158
+ export const skipDesign = (stated: StatedCriteria | undefined): Promise<boolean> =>
159
+ stated === undefined
160
+ ? Promise.resolve(false)
161
+ : scope("design", async () => {
162
+ if (!(await clear(stated))) return false
163
+ await run("normalise", t.normaliseCriteriaScript(stated), {
164
+ label: "Recording the stated acceptance criteria",
165
+ file: REQUIREMENTS,
166
+ })
167
+ return true
168
+ })
@@ -48,6 +48,10 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
48
48
  ## Risk Tier
49
49
 
50
50
  <tier> — one-line copy fix
51
+
52
+ ## Acceptance Criteria
53
+
54
+ - AC-1: the change behaves as described.
51
55
  """
52
56
  When the process is driven
53
57
  Then the process rests at "seam.naive"
@@ -72,7 +72,10 @@ describe("planAndBuild", () => {
72
72
  unwind: () => undefined,
73
73
  "start-gate.check": () => undefined,
74
74
  "design.triage": (_, r) =>
75
- r.write(".gtd/REQUIREMENTS.md", "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n"),
75
+ r.write(
76
+ ".gtd/REQUIREMENTS.md",
77
+ "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
78
+ ),
76
79
  "design.gate.answer": () => undefined,
77
80
  "seam.naive": (_, r) => r.write("src/calc.ts", SUBTRACT),
78
81
  "seam.judge": () => '[{"id":"shape","answer":"refactor-first","p":0.9}]',
@@ -102,7 +105,7 @@ describe("planAndBuild", () => {
102
105
  const requirements = (_: unknown, r: typeof repo) =>
103
106
  r.write(
104
107
  ".gtd/REQUIREMENTS.md",
105
- "# Requirements\n\n## Concerns\n\n### Subtract\n\n- `subtract`\n",
108
+ "# Requirements\n\n## Concerns\n\n### Subtract\n\n- `subtract`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
106
109
  )
107
110
  const lap = {
108
111
  unwind: () => undefined,
@@ -134,7 +137,11 @@ describe("planAndBuild", () => {
134
137
  const rest = await repo.drive({
135
138
  "build.review.closing": runsItsScript,
136
139
  "re-unwind": runsItsScript,
137
- "design.triage": (_, r) => r.write(".gtd/REQUIREMENTS.md", "# Requirements\n\n- multiply\n"),
140
+ "design.triage": (_, r) =>
141
+ r.write(
142
+ ".gtd/REQUIREMENTS.md",
143
+ "# Requirements\n\n- multiply\n\n## Acceptance Criteria\n\n- AC-1: done\n",
144
+ ),
138
145
  "design.gate.answer": () => undefined,
139
146
  })
140
147
  expect(rest.state).toBe("implement.building")
@@ -186,7 +193,10 @@ describe("afterTail feedback lap", () => {
186
193
  await repo.land()
187
194
  await repo.drive({
188
195
  "build.review.collecting": (_, r) => {
189
- r.write(".gtd/REQUIREMENTS.md", "# Requirements\n\n- multiply\n")
196
+ r.write(
197
+ ".gtd/REQUIREMENTS.md",
198
+ "# Requirements\n\n- multiply\n\n## Acceptance Criteria\n\n- AC-1: done\n",
199
+ )
190
200
  r.write(".gtd/REVIEW.md", `${review("also export multiply")} - A: noted.\n`)
191
201
  },
192
202
  })
@@ -199,7 +209,10 @@ describe("afterTail feedback lap", () => {
199
209
  "build.review.closing": runsItsScript,
200
210
  "re-unwind": runsItsScript,
201
211
  "design.triage": (_: unknown, r: ReturnType<typeof testRepo>) =>
202
- r.write(".gtd/REQUIREMENTS.md", "# Requirements\n\n- multiply, designed\n"),
212
+ r.write(
213
+ ".gtd/REQUIREMENTS.md",
214
+ "# Requirements\n\n- multiply, designed\n\n## Acceptance Criteria\n\n- AC-1: done\n",
215
+ ),
203
216
  "design.gate.answer": () => undefined,
204
217
  }
205
218
 
@@ -221,7 +234,10 @@ describe("afterTail feedback lap", () => {
221
234
  expect(prompts[0]).toContain("it covers everything since the base")
222
235
  expect(prompts[0]).not.toContain("only what is new since the previous review")
223
236
  const writes: Record<string, () => [string, string]> = {
224
- "design.triage": () => [".gtd/REQUIREMENTS.md", "# Requirements\n\n- multiply, designed\n"],
237
+ "design.triage": () => [
238
+ ".gtd/REQUIREMENTS.md",
239
+ "# Requirements\n\n- multiply, designed\n\n## Acceptance Criteria\n\n- AC-1: done\n",
240
+ ],
225
241
  "implement.building": () => ["src/calc.ts", `${CALC}// again\n`],
226
242
  }
227
243
  const actors = new Proxy({} as Record<string, Actor>, {
@@ -295,13 +311,20 @@ describe("an in-flight 28.0.0 naive-pass process", () => {
295
311
  from: string,
296
312
  to: string,
297
313
  ) => {
298
- if (from === "seam.steer") repo.write(".gtd/REQUIREMENTS.md", "# Requirements\n\n- steered\n")
314
+ if (from === "seam.steer")
315
+ repo.write(
316
+ ".gtd/REQUIREMENTS.md",
317
+ "# Requirements\n\n- steered\n\n## Acceptance Criteria\n\n- AC-1: done\n",
318
+ )
299
319
  if (from === "build.review.reviewing") repo.write(".gtd/REVIEW.md", review())
300
320
  if (to === "build.review.collecting")
301
321
  repo.write(".gtd/REVIEW.md", review("also export multiply"))
302
322
  if (from === "build.review.collecting") {
303
323
  repo.write(".gtd/REVIEW.md", `${review("also export multiply")} - A: noted.\n`)
304
- repo.write(".gtd/REQUIREMENTS.md", "# Requirements\n\n- multiply\n")
324
+ repo.write(
325
+ ".gtd/REQUIREMENTS.md",
326
+ "# Requirements\n\n- multiply\n\n## Acceptance Criteria\n\n- AC-1: done\n",
327
+ )
305
328
  }
306
329
  if (to === "build.review.closing") repo.write(".gtd/REVIEW.md", review())
307
330
  }
@@ -313,7 +336,10 @@ describe("an in-flight 28.0.0 naive-pass process", () => {
313
336
  unwind: () => undefined,
314
337
  "start-gate.check": () => undefined,
315
338
  "design.triage": (_, r) =>
316
- r.write(".gtd/REQUIREMENTS.md", "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n"),
339
+ r.write(
340
+ ".gtd/REQUIREMENTS.md",
341
+ "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
342
+ ),
317
343
  "design.gate.answer": () => undefined,
318
344
  "seam.naive": (_, r) => r.write("src/calc.ts", `${CALC}// naive\n`),
319
345
  })
@@ -356,7 +382,8 @@ describe("an in-flight 28.0.0 naive-pass process", () => {
356
382
  })
357
383
 
358
384
  describe("an in-flight process from before the architecture path was removed", () => {
359
- const REQUIREMENTS = "# Requirements\n\n## Concerns\n\n### Subtract\n\n- `subtract`\n"
385
+ const REQUIREMENTS =
386
+ "# Requirements\n\n## Concerns\n\n### Subtract\n\n- `subtract`\n\n## Acceptance Criteria\n\n- AC-1: done\n"
360
387
 
361
388
  /** Its history up to the design gate, forged with the old opening commit's `flag_naive_pass=0`. */
362
389
  const atDesignGate = async () => {
@@ -416,7 +443,10 @@ describe("flagMode", () => {
416
443
  unwind: () => undefined,
417
444
  "start-gate.check": () => undefined,
418
445
  "design.triage": (_, r) =>
419
- r.write(".gtd/REQUIREMENTS.md", "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n"),
446
+ r.write(
447
+ ".gtd/REQUIREMENTS.md",
448
+ "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Acceptance Criteria\n\n- AC-1: done\n",
449
+ ),
420
450
  "design.gate.answer": () => undefined,
421
451
  })
422
452
  }
@@ -13,6 +13,7 @@ import { flagModeOn, shipped } from "../shared/options.js"
13
13
  import { FEEDBACK } from "../shared/steps.js"
14
14
  import { refusePendingHandOff } from "../shared/tail.js"
15
15
  import * as t from "../shared/text.js"
16
+ import { skipDesign, statedCriteria, type StatedCriteria } from "./criteria.js"
16
17
  import { design } from "./planning.js"
17
18
  import { buildTail, isCode, reUnwind, type ReviewOutcome } from "./review.js"
18
19
  import { implement, seamBuild, type SeamOutcome } from "./seam.js"
@@ -54,9 +55,10 @@ export const buildFeature = async (firstBase: string): Promise<void> => {
54
55
  })
55
56
  }
56
57
 
57
- /** Design, then `buildFeature`. */
58
- export const planAndBuild = async (firstBase: string): Promise<void> => {
59
- await design(firstBase)
58
+ /** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
59
+ export const planAndBuild = async (firstBase: string, stated?: StatedCriteria): Promise<void> => {
60
+ // GTD_FLAG_RISK_TIER: the tier is proposed at triage and set at the design gate, so no skip.
61
+ if (!(await skipDesign(riskTierOn() ? undefined : stated))) await design(firstBase)
60
62
  await buildFeature(firstBase)
61
63
  }
62
64
 
@@ -80,6 +82,7 @@ export const ordinaryStart = async (): Promise<void> => {
80
82
  refusePendingHandOff()
81
83
  // Read right after idle, so a bad value refuses the landing that would pin it.
82
84
  flagModeOn()
85
+ const stated = statedCriteria(changes(), head())
83
86
  await unwind()
84
87
  if (changes(FEEDBACK).some((c) => c.status !== "deleted")) {
85
88
  await human("unwind-failed", {
@@ -89,7 +92,7 @@ export const ordinaryStart = async (): Promise<void> => {
89
92
  })
90
93
  }
91
94
  await gate("start-gate", t.startGateBlockedMessage())
92
- await planAndBuild(start())
95
+ await planAndBuild(start(), stated)
93
96
  }
94
97
 
95
98
  export const feature = shipped(ordinaryStart)
@@ -36,6 +36,10 @@ Feature: flag mode off builds a feature with no release flag, seam judge or refa
36
36
  ### Subtract
37
37
 
38
38
  - `subtract(a, b)` returns `a - b`.
39
+
40
+ ## Acceptance Criteria
41
+
42
+ - AC-1: the change behaves as described.
39
43
  """
40
44
  And step "design.gate.answer" changes nothing
41
45
 
@@ -116,6 +120,10 @@ Feature: flag mode off builds a feature with no release flag, seam judge or refa
116
120
  ### Multiply
117
121
 
118
122
  - Export `multiply()`.
123
+
124
+ ## Acceptance Criteria
125
+
126
+ - AC-1: the change behaves as described.
119
127
  """
120
128
  And step "build.review.collecting" writes ".gtd/REVIEW.md" with:
121
129
  """
@@ -1,6 +1,7 @@
1
1
  import { feature } from "./feature.js"
2
2
 
3
3
  export * from "./steps.js"
4
+ export * from "./criteria.js"
4
5
  export * from "./planning.js"
5
6
  export * from "./refactor.js"
6
7
  export * from "./seam.js"
@@ -34,6 +34,10 @@ Feature: a naive pass the judge rules land goes straight to implement with no se
34
34
  ### Typo
35
35
 
36
36
  - greeting reads "Hello, world".
37
+
38
+ ## Acceptance Criteria
39
+
40
+ - AC-1: the change behaves as described.
37
41
  """
38
42
  And step "design.gate.answer" changes nothing
39
43
  And step "seam.naive" writes "src/greeting.ts" with:
@@ -8,6 +8,7 @@ import {
8
8
  scope,
9
9
  } from "../../flows/index.js"
10
10
  import { REQUIREMENTS } from "../shared/steps.js"
11
+ import { requireCriteria } from "./criteria.js"
11
12
  import { answerProductQuestions, triage } from "./steps.js"
12
13
 
13
14
  /**
@@ -31,5 +32,6 @@ export const design = (base: string): Promise<void> =>
31
32
  await triage(base)
32
33
  requireProgress(REQUIREMENTS)
33
34
  requireReplies(REQUIREMENTS)
35
+ requireCriteria(REQUIREMENTS)
34
36
  } while (await noteGate(REQUIREMENTS, answerProductQuestions))
35
37
  })
@@ -35,6 +35,10 @@ Feature: a refactor that carried the whole feature still gets a feature review
35
35
  ### Subtract
36
36
 
37
37
  - `subtract(a, b)` returns `a - b`.
38
+
39
+ ## Acceptance Criteria
40
+
41
+ - AC-1: the change behaves as described.
38
42
  """
39
43
  And step "design.gate.answer" changes nothing
40
44
 
@@ -34,6 +34,10 @@ Feature: a refactor-first judgment walks one fixed path to idle
34
34
  ### Subtract
35
35
 
36
36
  - `subtract(a, b)` returns `a - b`.
37
+
38
+ ## Acceptance Criteria
39
+
40
+ - AC-1: the change behaves as described.
37
41
  """
38
42
  And step "design.gate.answer" changes nothing
39
43
 
@@ -46,6 +46,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
46
46
  ### Subtract
47
47
 
48
48
  - `subtract(a, b)` returns `a - b`.
49
+
50
+ ## Acceptance Criteria
51
+
52
+ - AC-1: the change behaves as described.
49
53
  """
50
54
  And step "design.gate.answer" changes nothing
51
55
  And step "seam.naive" writes "src/calc.ts" with:
@@ -126,6 +130,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
126
130
  ### Subtract
127
131
 
128
132
  - `subtract(a, b)` returns `a - b`.
133
+
134
+ ## Acceptance Criteria
135
+
136
+ - AC-1: the change behaves as described.
129
137
  """
130
138
  And step "design.gate.answer" changes nothing
131
139
  And step "seam.naive" writes "src/calc.ts" with:
@@ -205,6 +213,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
205
213
  ### Subtract
206
214
 
207
215
  - `subtract(a, b)` returns `a - b`.
216
+
217
+ ## Acceptance Criteria
218
+
219
+ - AC-1: the change behaves as described.
208
220
  """
209
221
  And step "design.gate.answer" changes nothing
210
222
  And step "seam.naive" writes "src/calc.ts" with:
@@ -449,6 +461,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
449
461
  # Requirements
450
462
 
451
463
  - multiply, designed
464
+
465
+ ## Acceptance Criteria
466
+
467
+ - AC-1: the change behaves as described.
452
468
  """
453
469
  And step "implement.building" writes "src/calc.ts" with:
454
470
  """
@@ -1404,6 +1420,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1404
1420
  ### Subtract
1405
1421
 
1406
1422
  - `subtract(a, b)` returns `a - b`.
1423
+
1424
+ ## Acceptance Criteria
1425
+
1426
+ - AC-1: the change behaves as described.
1407
1427
  """
1408
1428
  And step "design.gate.answer" changes nothing
1409
1429
  And step "seam.naive" writes "src/calc.ts" with:
@@ -1582,6 +1602,10 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
1582
1602
  ### Subtract
1583
1603
 
1584
1604
  - `subtract(a, b)` returns `a - b`.
1605
+
1606
+ ## Acceptance Criteria
1607
+
1608
+ - AC-1: the change behaves as described.
1585
1609
  """
1586
1610
  And step "design.gate.answer" changes nothing
1587
1611
  And judge step "seam.judge" answers:
@@ -49,6 +49,10 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
49
49
  ## Risk Tier
50
50
 
51
51
  low — trivial copy change
52
+
53
+ ## Acceptance Criteria
54
+
55
+ - AC-1: the change behaves as described.
52
56
  """
53
57
  And step "design.gate.answer" changes nothing
54
58
  When the process is driven
@@ -64,6 +68,10 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
64
68
  ## Risk Tier
65
69
 
66
70
  high — touches payments
71
+
72
+ ## Acceptance Criteria
73
+
74
+ - AC-1: the change behaves as described.
67
75
  """
68
76
  And step "design.gate.answer" changes nothing
69
77
  When the process is driven
@@ -80,6 +88,10 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
80
88
  ## Concerns
81
89
 
82
90
  ### Subtract
91
+
92
+ ## Acceptance Criteria
93
+
94
+ - AC-1: the change behaves as described.
83
95
  """
84
96
  And step "design.gate.answer" changes nothing
85
97
  When the process is driven
@@ -105,6 +117,10 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
105
117
 
106
118
  - error rate on /subtract stays flat
107
119
  - p95 latency
120
+
121
+ ## Acceptance Criteria
122
+
123
+ - AC-1: the change behaves as described.
108
124
  """
109
125
  And step "design.gate.answer" changes nothing
110
126
  When the process is driven
@@ -125,6 +141,10 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
125
141
  ## Risk Tier
126
142
 
127
143
  low — trivial copy change
144
+
145
+ ## Acceptance Criteria
146
+
147
+ - AC-1: the change behaves as described.
128
148
  """
129
149
  And step "seam.naive" writes "src/calc.ts" with:
130
150
  """
@@ -157,6 +177,10 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
157
177
  ## Concerns
158
178
 
159
179
  ### Subtract
180
+
181
+ ## Acceptance Criteria
182
+
183
+ - AC-1: the change behaves as described.
160
184
  """
161
185
  And step "design.gate.answer" changes nothing
162
186
  When the process is driven
@@ -199,6 +223,10 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
199
223
  ## Production Signals
200
224
 
201
225
  - error rate stays flat
226
+
227
+ ## Acceptance Criteria
228
+
229
+ - AC-1: the change behaves as described.
202
230
  """
203
231
  And step "design.gate.answer" changes nothing
204
232
  When the process is driven
@@ -220,6 +248,10 @@ Feature: the risk tier is proposed at triage and recorded behind GTD_FLAG_RISK_T
220
248
  ## Risk Tier
221
249
 
222
250
  low — trivial
251
+
252
+ ## Acceptance Criteria
253
+
254
+ - AC-1: the change behaves as described.
223
255
  """
224
256
  And step "design.gate.answer" changes nothing
225
257
  And step "seam.naive" writes "src/calc.ts" with:
@@ -33,6 +33,10 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
33
33
  ### Subtract
34
34
 
35
35
  - `subtract(a, b)` returns `a - b`.
36
+
37
+ ## Acceptance Criteria
38
+
39
+ - AC-1: the change behaves as described.
36
40
  """
37
41
  And step "design.gate.answer" changes nothing
38
42
 
@@ -77,6 +81,10 @@ Feature: rejected naive passes walk one fixed path through the steer gate to idl
77
81
  ### Subtract
78
82
 
79
83
  - `subtract(a, b)` returns `a - b`, in the calculator module.
84
+
85
+ ## Acceptance Criteria
86
+
87
+ - AC-1: the change behaves as described.
80
88
  """
81
89
  And step "seam.naive" from now on changes nothing
82
90
  And step "seam.naive" writes "src/calc.ts" with:
@@ -43,6 +43,10 @@ Feature: the naive pass and seam judge decide how a feature is built
43
43
  ### Subtract
44
44
 
45
45
  - `subtract(a, b)` returns `a - b`.
46
+
47
+ ## Acceptance Criteria
48
+
49
+ - AC-1: the change behaves as described.
46
50
  """
47
51
  And step "design.gate.answer" changes nothing
48
52
 
@@ -184,6 +188,10 @@ Feature: the naive pass and seam judge decide how a feature is built
184
188
  ### Subtract
185
189
 
186
190
  - `subtract(a, b)` returns `a - b`, in a new module.
191
+
192
+ ## Acceptance Criteria
193
+
194
+ - AC-1: the change behaves as described.
187
195
  """
188
196
  When gtd lands
189
197
  Then the process rests at "seam.naive"
@@ -16,6 +16,7 @@ import { healthy } from "../shared/health.js"
16
16
  import { coder, FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
17
17
  import * as t from "../shared/text.js"
18
18
  import { defaults } from "../shared/vars.js"
19
+ import { requireCriteria, requireCriteriaFrozen } from "./criteria.js"
19
20
  import { refactorStrays, reviewedRefactor } from "./refactor.js"
20
21
  import { flagModeOn } from "../shared/options.js"
21
22
  import type { RiskTier } from "./tier.js"
@@ -81,13 +82,16 @@ export function implement(
81
82
  ...(options.afterRefactor === true ? { allowEmpty: true } : {}),
82
83
  },
83
84
  )
84
- await healthy(() =>
85
- t.agentWithSkills("fix.suite.fixing", t.implementFixPrompt(), {
86
- label: "Fixing the check",
87
- file: FEEDBACK,
88
- model: coder(),
89
- system: t.builderSystem(),
90
- }),
85
+ requireCriteriaFrozen(REQUIREMENTS)
86
+ await healthy(
87
+ () =>
88
+ t.agentWithSkills("fix.suite.fixing", t.implementFixPrompt(), {
89
+ label: "Fixing the check",
90
+ file: FEEDBACK,
91
+ model: coder(),
92
+ system: t.builderSystem(),
93
+ }),
94
+ { guard: () => requireCriteriaFrozen(REQUIREMENTS), guardRests: false },
91
95
  )
92
96
  await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" })
93
97
  })
@@ -124,6 +128,7 @@ const decide = (): Promise<Decision> =>
124
128
  })
125
129
  // Any edit steers, with a fresh budget and no stale rejection; clean overrules.
126
130
  if (changes().length === 0) return { reference: tip.hash }
131
+ requireCriteria(REQUIREMENTS)
127
132
  rejection = undefined
128
133
  retries = 0
129
134
  }
@@ -33,6 +33,10 @@ Feature: a process keeps the risk tier setting it started with when GTD_FLAG_RIS
33
33
  ## Risk Tier
34
34
 
35
35
  low — a new pure function
36
+
37
+ ## Acceptance Criteria
38
+
39
+ - AC-1: the change behaves as described.
36
40
  """
37
41
 
38
42
  Scenario: a process started with the flag off records no tier after the flag turns on