@pmelab/gtd 56.0.0 → 58.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 (56) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +29 -35
  3. package/dist/chunks/{reserved-B8EC-WE8.mjs → reserved-BzAFXuz_.mjs} +56 -131
  4. package/dist/chunks/{review-HdrX0FJ7.mjs → review-CVqRS0mc.mjs} +2 -2
  5. package/dist/chunks/{tail-BSLah2mA.mjs → tail-JQzB21Q3.mjs} +1 -1
  6. package/dist/workflows/cleanup.mjs +2 -2
  7. package/dist/workflows/feature.mjs +99 -170
  8. package/dist/workflows/fix.mjs +2 -2
  9. package/dist/workflows/review.mjs +3 -3
  10. package/dist/workflows/rollout.mjs +1 -1
  11. package/dist/workflows.mjs +150 -297
  12. package/package.json +1 -1
  13. package/src/workflows/feature/acceptance-criteria.feature +32 -0
  14. package/src/workflows/feature/criteria-coverage.feature +0 -2
  15. package/src/workflows/feature/criteria.test.ts +1 -41
  16. package/src/workflows/feature/criteria.ts +12 -18
  17. package/src/workflows/feature/fast-path.feature +4 -1
  18. package/src/workflows/feature/feature.test.ts +3 -39
  19. package/src/workflows/feature/feature.ts +6 -8
  20. package/src/workflows/feature/lens-models.feature +0 -2
  21. package/src/workflows/feature/lens-review.feature +0 -2
  22. package/src/workflows/feature/naive-acceptance.feature +4 -0
  23. package/src/workflows/feature/naive-test.feature +263 -0
  24. package/src/workflows/feature/one-step-land.feature +4 -1
  25. package/src/workflows/feature/planning.ts +2 -0
  26. package/src/workflows/feature/refactor-absorbs.feature +4 -1
  27. package/src/workflows/feature/refactor-first.feature +4 -1
  28. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  29. package/src/workflows/feature/refactor-rebuild.feature +4 -1
  30. package/src/workflows/feature/refactor-review.feature +20 -1
  31. package/src/workflows/feature/release-flag.feature +167 -0
  32. package/src/workflows/feature/release-flag.test.ts +49 -0
  33. package/src/workflows/feature/release-flag.ts +46 -0
  34. package/src/workflows/feature/review-gate.feature +1 -3
  35. package/src/workflows/feature/reviewer-walkthrough.feature +0 -2
  36. package/src/workflows/feature/risk-tier.feature +32 -0
  37. package/src/workflows/feature/seam-rejection.feature +8 -1
  38. package/src/workflows/feature/seam.feature +8 -1
  39. package/src/workflows/feature/seam.test.ts +21 -9
  40. package/src/workflows/feature/seam.ts +60 -14
  41. package/src/workflows/feature/standards.feature +0 -2
  42. package/src/workflows/shared/options.test.ts +3 -10
  43. package/src/workflows/shared/options.ts +5 -6
  44. package/src/workflows/shared/prose.ts +0 -6
  45. package/src/workflows/shared/skills.test.ts +0 -4
  46. package/src/workflows/shared/skills.ts +0 -5
  47. package/src/workflows/shared/steps.ts +1 -1
  48. package/src/workflows/shared/text.test.ts +58 -69
  49. package/src/workflows/shared/text.ts +79 -162
  50. package/src/workflows/shared/vars.ts +2 -4
  51. package/src/workflows/feature/flag-mode.feature +0 -88
  52. package/src/workflows/feature/hands-on-skip.feature +0 -316
  53. package/src/workflows/feature/prototype-rounds.feature +0 -156
  54. package/src/workflows/feature/prototype.feature +0 -428
  55. package/src/workflows/feature/prototype.test.ts +0 -43
  56. package/src/workflows/feature/prototype.ts +0 -198
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "56.0.0",
3
+ "version": "58.0.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
@@ -39,6 +39,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
39
39
 
40
40
  - the greeting module
41
41
 
42
+ ## Release Flag
43
+
44
+ new
45
+
42
46
  ## Acceptance Criteria
43
47
 
44
48
  - AC-1: `greeting` reads "Hello, world".
@@ -67,6 +71,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
67
71
 
68
72
  - the greeting module
69
73
 
74
+ ## Release Flag
75
+
76
+ new
77
+
70
78
  ## Acceptance Criteria
71
79
 
72
80
  - `greeting` reads "Hello, world".
@@ -82,6 +90,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
82
90
 
83
91
  - the greeting module
84
92
 
93
+ ## Release Flag
94
+
95
+ new
96
+
85
97
  ## Acceptance Criteria
86
98
 
87
99
  - AC-1: `greeting` reads "Hello, world".
@@ -98,6 +110,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
98
110
 
99
111
  - the greeting module
100
112
 
113
+ ## Release Flag
114
+
115
+ new
116
+
101
117
  ## Acceptance Criteria
102
118
 
103
119
  - AC-1: `greeting` reads "Hello, world".
@@ -131,6 +147,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
131
147
 
132
148
  - the greeting module
133
149
 
150
+ ## Release Flag
151
+
152
+ new
153
+
134
154
  ## Acceptance Criteria
135
155
 
136
156
  - AC-1: `greeting` reads "Hi".
@@ -150,6 +170,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
150
170
  """
151
171
  # Requirements
152
172
 
173
+ ## Release Flag
174
+
175
+ new
176
+
153
177
  ## Acceptance Criteria
154
178
 
155
179
  - AC-1: `greeting` reads "Hello, world".
@@ -178,6 +202,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
178
202
 
179
203
  - the greeting module
180
204
 
205
+ ## Release Flag
206
+
207
+ new
208
+
181
209
  ## Acceptance Criteria
182
210
 
183
211
  - AC-1: `greeting` reads "Hi".
@@ -226,6 +254,10 @@ Feature: acceptance criteria carry stable IDs, and a sketch that states clear on
226
254
 
227
255
  - the greeting module
228
256
 
257
+ ## Release Flag
258
+
259
+ new
260
+
229
261
  ## Acceptance Criteria
230
262
 
231
263
  - AC-1: `greeting` reads "Hello, world".
@@ -7,9 +7,7 @@ Feature: the criteria-coverage lens checks the change's tests against its criter
7
7
 
8
8
  Background:
9
9
  Given the shipped workflows, as gtd install scaffolds them
10
- And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
11
10
  And an environment variable "GTD_QUALITYREVIEWS" set to "criteria-coverage"
12
- And an environment variable "GTD_FLAGMODE" set to "false"
13
11
  And a file "src/calc.ts" with:
14
12
  """
15
13
  export const add = (a: number, b: number) => a + b
@@ -1,13 +1,6 @@
1
1
  import { describe, expect, it } from "vitest"
2
2
  import type { Change } from "../../flows/index.js"
3
- import {
4
- criteriaOf,
5
- criterionIds,
6
- handsOnIds,
7
- hasImpact,
8
- statedCriteria,
9
- unstableIds,
10
- } from "./criteria.js"
3
+ import { criteriaOf, criterionIds, hasImpact, statedCriteria, unstableIds } from "./criteria.js"
11
4
 
12
5
  const added = (path: string, after: string): Change => ({
13
6
  path,
@@ -55,17 +48,6 @@ describe("criteriaOf", () => {
55
48
  ).toEqual([{ id: "AC-1", text: "exits 2 when config is bad - missing file" }])
56
49
  })
57
50
 
58
- it("reads the (no hands-on) tag between an ID and its colon", () => {
59
- expect(
60
- criteriaOf(
61
- "## Acceptance Criteria\n\n- AC-1: runs\n- AC-2 (no hands-on): the prompt says so\n",
62
- ),
63
- ).toEqual([
64
- { id: "AC-1", text: "runs" },
65
- { id: "AC-2", text: "the prompt says so", noHandsOn: true },
66
- ])
67
- })
68
-
69
51
  it("is undefined without the section", () => {
70
52
  expect(criteriaOf("# R\n\n- AC-1: x\n")).toBeUndefined()
71
53
  })
@@ -84,19 +66,6 @@ describe("statedCriteria", () => {
84
66
  })
85
67
  })
86
68
 
87
- it("keeps a stated (no hands-on) tag, renumbered or not", () => {
88
- const tagged = "## Acceptance criteria\n\n- AC-1: a\n- AC-2 (no hands-on): b\n"
89
- expect(statedCriteria([added("TODO.md", tagged)], "abc")?.criteria).toEqual([
90
- { id: "AC-1", text: "a" },
91
- { id: "AC-2", text: "b", noHandsOn: true },
92
- ])
93
- const partial = "## Acceptance criteria\n\n- a\n- AC-7 (no hands-on): b\n"
94
- expect(statedCriteria([added("TODO.md", partial)], "abc")?.criteria).toEqual([
95
- { id: "AC-1", text: "a" },
96
- { id: "AC-2", text: "b", noHandsOn: true },
97
- ])
98
- })
99
-
100
69
  it("renumbers when the stated IDs are partial or clash", () => {
101
70
  const sketch = [added("TODO.md", "## Acceptance criteria\n\n- AC-4: a\n- b\n")]
102
71
  expect(statedCriteria(sketch, "abc")?.criteria).toEqual([
@@ -144,15 +113,6 @@ describe("criterionIds", () => {
144
113
  })
145
114
  })
146
115
 
147
- describe("handsOnIds", () => {
148
- const requirements =
149
- "## Acceptance Criteria\n\n- AC-1: a\n- AC-2 (no hands-on): b\n- AC-3: c\n- AC-4 (no hands-on): d\n"
150
-
151
- it("leaves out every criterion tagged (no hands-on)", () => {
152
- expect(handsOnIds(requirements)).toEqual(["AC-1", "AC-3"])
153
- })
154
- })
155
-
156
116
  describe("unstableIds", () => {
157
117
  const ids = { closed: ["AC-1", "AC-3"], used: ["AC-1", "AC-2", "AC-3"] }
158
118
 
@@ -13,12 +13,11 @@ import {
13
13
  import { HEADING, ITEM, markFenced } from "../shared/fences.js"
14
14
  import { planner, REQUIREMENTS } from "../shared/steps.js"
15
15
  import * as t from "../shared/text.js"
16
+ import { requireReleaseFlag } from "./release-flag.js"
16
17
 
17
18
  export interface Criterion {
18
19
  readonly id: string
19
20
  readonly text: string
20
- /** Tagged `(no hands-on)`: its tests prove it, so the hands-on test skips it. */
21
- readonly noHandsOn?: true
22
21
  }
23
22
 
24
23
  /** Acceptance criteria the human's sketch stated outright, ready to normalise. */
@@ -33,13 +32,9 @@ export interface StatedCriteria {
33
32
  /** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
34
33
  const CLEAR_MIN_P = 0.8
35
34
 
36
- const ID = /^(AC-\d+)(\s*\(no hands-on\))?:\s*(.*)$/
35
+ const ID = /^(AC-\d+):\s*(.*)$/
37
36
 
38
- const criterion = (id: RegExpExecArray, as = id[1]!): Criterion => ({
39
- id: as,
40
- text: id[3]!,
41
- ...(id[2] === undefined ? {} : { noHandsOn: true }),
42
- })
37
+ const criterion = (id: RegExpExecArray, as = id[1]!): Criterion => ({ id: as, text: id[2]! })
43
38
 
44
39
  /** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
45
40
  const listUnder = (text: string, title: RegExp, level?: number): string[] | undefined => {
@@ -83,12 +78,6 @@ export const criteriaOf = (text: string): readonly Criterion[] | undefined =>
83
78
  export const criterionIds = (text: string): string[] =>
84
79
  (criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "")
85
80
 
86
- /** The ID'd criteria of `text` a person can try by hand: none tagged `(no hands-on)`. */
87
- export const handsOnIds = (text: string): string[] => {
88
- const tagged = (criteriaOf(text) ?? []).filter((c) => c.noHandsOn).map((c) => c.id)
89
- return criterionIds(text).filter((id) => !tagged.includes(id))
90
- }
91
-
92
81
  /** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
93
82
  const numbered = (items: readonly string[]): Criterion[] => {
94
83
  const ids = items.map((item) => ID.exec(item))
@@ -149,16 +138,20 @@ export const requireCriteria = (file: string): void => {
149
138
  const isImpactHeading = (h: RegExpExecArray | null): boolean =>
150
139
  h !== null && h[1]!.length === 2 && /^impact$/i.test(h[2]!.trim())
151
140
 
141
+ // The skip path's impact turn may write both: a sketch can ask for a flag.
142
+ const isImpactTurnHeading = (h: RegExpExecArray | null): boolean =>
143
+ h !== null && h[1]!.length === 2 && /^(impact|release flag)$/i.test(h[2]!.trim())
144
+
152
145
  /** Whether `text` has an unfenced, level-2 `## Impact` heading. */
153
146
  export const hasImpact = (text: string): boolean =>
154
147
  markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)))
155
148
 
156
- /** `text` without its `## Impact` section, whitespace collapsed. */
149
+ /** `text` without its `## Impact` and `## Release Flag` sections, whitespace collapsed. */
157
150
  const withoutImpact = (text: string): string => {
158
151
  let inImpact = false
159
152
  const kept = markFenced(text).filter((l) => {
160
153
  const h = l.fenced ? null : HEADING.exec(l.line)
161
- if (h !== null && h[1]!.length <= 2) inImpact = isImpactHeading(h)
154
+ if (h !== null && h[1]!.length <= 2) inImpact = isImpactTurnHeading(h)
162
155
  return !inImpact
163
156
  })
164
157
  return kept
@@ -184,13 +177,13 @@ export const requireImpact = (file: string): void => {
184
177
  )
185
178
  }
186
179
 
187
- /** Refuse a turn that changed `file` beyond its `## Impact` section. */
180
+ /** Refuse a turn that changed `file` beyond its `## Impact` and `## Release Flag` sections. */
188
181
  export const requireImpactOnly = (file: string): void => {
189
182
  const change = changes().get(file)
190
183
  if (change === undefined) return
191
184
  if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return
192
185
  refuse(
193
- `gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`,
186
+ `gtd land: impact-only: this turn may only add "## Impact" (and "## Release Flag") to ${file} — restore everything else`,
194
187
  )
195
188
  }
196
189
 
@@ -252,6 +245,7 @@ const impact = async (): Promise<void> => {
252
245
  requireCriteriaFrozen(REQUIREMENTS)
253
246
  requireImpact(REQUIREMENTS)
254
247
  requireImpactOnly(REQUIREMENTS)
248
+ requireReleaseFlag(REQUIREMENTS)
255
249
  }
256
250
 
257
251
  /**
@@ -8,7 +8,6 @@ Feature: a trivial change takes the same few beats at either risk tier
8
8
 
9
9
  Background:
10
10
  Given the shipped workflows, as gtd install scaffolds them
11
- And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
12
11
  And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
13
12
  And an environment variable "GTD_FLAG_RISK_TIER" set to "1"
14
13
  And a file "src/greeting.ts" with:
@@ -42,6 +41,10 @@ Feature: a trivial change takes the same few beats at either risk tier
42
41
 
43
42
  - the greeting module
44
43
 
44
+ ## Release Flag
45
+
46
+ new
47
+
45
48
  ## Acceptance Criteria
46
49
 
47
50
  - AC-1: the change behaves as described.
@@ -302,38 +302,9 @@ describe("an in-flight process from before the architecture path was removed", (
302
302
  })
303
303
  })
304
304
 
305
- describe("flagMode", () => {
306
- const config = (workflow: string): string =>
307
- [
308
- 'import { feature } from "@pmelab/gtd/workflows/feature"',
309
- 'export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"',
310
- `export default ${workflow}`,
311
- "",
312
- ].join("\n")
313
-
314
- const designed = async (workflow: string, env: Record<string, string> = {}) => {
315
- const repo = testRepo({ workflow, env: { GTD_QUALITYREVIEWS: "", ...env } })
316
- repo.write("src/calc.ts", CALC)
317
- repo.commit("feat: add calculator")
318
- repo.write("TODO.md", "# Add subtract\n")
319
- return repo.drive({
320
- unwind: () => undefined,
321
- "start-gate.check": () => undefined,
322
- "design.triage": (_, r) =>
323
- r.write(
324
- ".gtd/REQUIREMENTS.md",
325
- "# Requirements\n\n## Concerns\n\n### S\n\n- `s`\n\n## Impact\n\n- the module\n\n## Acceptance Criteria\n\n- AC-1: done\n",
326
- ),
327
- "design.gate.answer": () => undefined,
328
- })
329
- }
330
-
331
- it("set on explicitly, runs the naive pass like the default", async () => {
332
- expect((await designed(config("feature({ flagMode: true })"))).state).toBe("seam.naive")
333
- })
334
-
335
- it("off through GTD_FLAGMODE=false, lands the naive pass with no seam judge", async () => {
336
- const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_FLAGMODE: "false" } })
305
+ describe("release flag", () => {
306
+ it("with no Release Flag section, lands the naive pass with no seam judge", async () => {
307
+ const repo = testRepo({ workflow: CONFIG_SCAFFOLD })
337
308
  repo.write("src/calc.ts", CALC)
338
309
  repo.commit("feat: add calculator")
339
310
  repo.write("TODO.md", "# Add subtract\n")
@@ -357,13 +328,6 @@ describe("flagMode", () => {
357
328
  expect(repo.read("src/calc.ts")).toBe(`${CALC}// naive\n`)
358
329
  expect(repo.subjects().some((subject) => subject.includes("seam.judge"))).toBe(false)
359
330
  })
360
-
361
- it("refuses a value other than true or false before the process starts", async () => {
362
- const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_FLAGMODE: "off" } })
363
- repo.write("TODO.md", "# Add subtract\n")
364
- await expect(repo.drive({})).rejects.toThrow('flagMode must be "true" or "false", got "off"')
365
- expect(repo.subjects()).toEqual(["chore: initial commit"])
366
- })
367
331
  })
368
332
 
369
333
  // pins the public re-exports from the feature entry point
@@ -9,7 +9,7 @@ import {
9
9
  start,
10
10
  } from "../../flows/index.js"
11
11
  import { gate } from "../shared/health.js"
12
- import { flagModeOn, shipped } from "../shared/options.js"
12
+ import { naiveTestOn, shipped } from "../shared/options.js"
13
13
  import { FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
14
14
  import { refusePendingHandOff } from "../shared/tail.js"
15
15
  import * as t from "../shared/text.js"
@@ -23,16 +23,15 @@ import {
23
23
  } from "./criteria.js"
24
24
  import { design } from "./planning.js"
25
25
  import { buildTail, isCode, reUnwind, type ReviewOutcome } from "../shared/review.js"
26
- import { prototypeRounds } from "./prototype.js"
27
26
  import { implement, implementing, seamBuild } from "./seam.js"
28
27
  import { recordRisk, riskTierOn } from "./tier.js"
29
28
 
30
29
  // The ordinary start. Any change to the tree starts a process: `idle` →
31
30
  // `unwind` reverts the sketch (its intent survives in history) → a
32
- // green-baseline gate → design → a naive pass, judged with `flagMode` on: it
31
+ // green-baseline gate → design → a naive pass, tried by a person with
32
+ // `naiveTest` on and judged when the requirements ask for a release flag: it
33
33
  // lands as the implementation, or (refactoring first when the judge asks) is
34
- // built fresh from → the health loop → a hands-on test of every criterion,
35
- // fixed and re-tested → the triaged quality lap and its finding gate → a
34
+ // built fresh from → the health loop → the triaged quality lap and its finding gate → a
36
35
  // reviewer's walkthrough of the whole change for the human, which signs off
37
36
  // (the episode ends back at `idle`) or sends a design → implement lap.
38
37
 
@@ -47,7 +46,7 @@ export const unwind = (): Promise<void> => {
47
46
  export { reUnwind }
48
47
 
49
48
  /**
50
- * A naive pass (judged only with `flagMode` on), then implement and the
49
+ * A naive pass (judged only when a release flag is asked for), then implement and the
51
50
  * quality lap, then the review; design must have run since `firstBase`.
52
51
  */
53
52
  export const buildFeature = async (firstBase: string): Promise<void> => {
@@ -140,8 +139,7 @@ export const ordinaryStart = async (): Promise<void> => {
140
139
  await human("idle", { message: t.idleMessage(), label: "Idle", file: ".gtd/TODO.md" })
141
140
  refusePendingHandOff()
142
141
  // Read right after idle, so a bad value refuses the landing that would pin it.
143
- flagModeOn()
144
- prototypeRounds()
142
+ naiveTestOn()
145
143
  const stated = statedCriteria(changes(), head())
146
144
  await unwind()
147
145
  if (changes(FEEDBACK).some((c) => c.status !== "deleted")) {
@@ -11,8 +11,6 @@ Feature: each quality lens runs on its own model hint
11
11
  import { feature } from "@pmelab/gtd/workflows/feature"
12
12
  export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
13
13
  export default feature({
14
- flagMode: false,
15
- prototypeRounds: "0",
16
14
  qualityReviews: "correctness, conventions, test-audit=opus",
17
15
  })
18
16
  """
@@ -11,9 +11,7 @@ Feature: the quality lap's findings are triaged, then decided at the finding gat
11
11
 
12
12
  Background:
13
13
  Given the shipped workflows, as gtd install scaffolds them
14
- And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
15
14
  And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
16
- And an environment variable "GTD_FLAGMODE" set to "false"
17
15
  And a file "src/calc.ts" with:
18
16
  """
19
17
  export const add = (a: number, b: number) => a + b
@@ -31,6 +31,10 @@ Feature: a naive pass ends only when its tests reach no file it added and the su
31
31
 
32
32
  - the greeting module
33
33
 
34
+ ## Release Flag
35
+
36
+ new
37
+
34
38
  ## Acceptance Criteria
35
39
 
36
40
  - AC-1: `subtract(5, 3)` returns 2.