@pmelab/gtd 58.2.0 → 59.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 (133) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +115 -390
  3. package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
  4. package/dist/gtd.bundle.mjs +76 -59
  5. package/dist/testkit/gherkin.mjs +1 -1
  6. package/dist/testkit.mjs +1 -1
  7. package/package.json +3 -42
  8. package/skills/authoring/SKILL.md +31 -25
  9. package/src/cli/Cli.test.ts +15 -19
  10. package/src/cli/Cli.ts +2 -4
  11. package/src/cli/Program.test.ts +60 -29
  12. package/src/cli/Program.ts +5 -7
  13. package/src/edge/Edge.ts +3 -3
  14. package/src/flags/index.test.ts +0 -13
  15. package/src/flags/index.ts +1 -3
  16. package/src/install/Install.test.ts +14 -11
  17. package/src/install/Install.ts +38 -15
  18. package/src/install/index.ts +1 -2
  19. package/src/replay/Replay.test.ts +1 -1
  20. package/src/replay/index.ts +1 -1
  21. package/src/step/planStart.ts +6 -1
  22. package/src/twins/FlowScripts.ts +1 -1
  23. package/src/workflow/index.ts +1 -0
  24. package/src/workflow/load.test.ts +138 -113
  25. package/src/workflow/load.ts +1 -1
  26. package/src/workflow/vars.test.ts +30 -3
  27. package/src/workflow/vars.ts +34 -2
  28. package/src/workflow-kit/index.ts +1 -1
  29. package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
  30. package/dist/chunks/review-Bfaror38.mjs +0 -427
  31. package/dist/chunks/tail-Bd7L940p.mjs +0 -355
  32. package/dist/chunks/testkit-JDtpNKyi.mjs +0 -10410
  33. package/dist/workflows/cleanup.mjs +0 -36
  34. package/dist/workflows/feature.mjs +0 -1018
  35. package/dist/workflows/fix.mjs +0 -41
  36. package/dist/workflows/review.mjs +0 -45
  37. package/dist/workflows/rollout.mjs +0 -48
  38. package/dist/workflows.mjs +0 -40651
  39. package/src/install/ConfigScaffold.ts +0 -22
  40. package/src/workflows/cleanup/cleanup-path.feature +0 -94
  41. package/src/workflows/cleanup/cleanup.feature +0 -315
  42. package/src/workflows/cleanup/cleanup.test.ts +0 -276
  43. package/src/workflows/cleanup/cleanup.ts +0 -52
  44. package/src/workflows/cleanup/index.ts +0 -6
  45. package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
  46. package/src/workflows/feature/acceptance-criteria.feature +0 -389
  47. package/src/workflows/feature/criteria-coverage.feature +0 -102
  48. package/src/workflows/feature/criteria.test.ts +0 -147
  49. package/src/workflows/feature/criteria.ts +0 -267
  50. package/src/workflows/feature/fast-path.feature +0 -93
  51. package/src/workflows/feature/feature.test.ts +0 -338
  52. package/src/workflows/feature/feature.ts +0 -156
  53. package/src/workflows/feature/green.ts +0 -19
  54. package/src/workflows/feature/index.ts +0 -18
  55. package/src/workflows/feature/lens-models.feature +0 -69
  56. package/src/workflows/feature/lens-review.feature +0 -566
  57. package/src/workflows/feature/naive-acceptance.feature +0 -147
  58. package/src/workflows/feature/naive-test.feature +0 -263
  59. package/src/workflows/feature/one-step-land.feature +0 -93
  60. package/src/workflows/feature/planning.ts +0 -41
  61. package/src/workflows/feature/refactor-absorbs.feature +0 -131
  62. package/src/workflows/feature/refactor-first.feature +0 -158
  63. package/src/workflows/feature/refactor-lens-models.feature +0 -79
  64. package/src/workflows/feature/refactor-rebuild.feature +0 -273
  65. package/src/workflows/feature/refactor-review.feature +0 -1528
  66. package/src/workflows/feature/refactor.test.ts +0 -1220
  67. package/src/workflows/feature/refactor.ts +0 -368
  68. package/src/workflows/feature/release-flag.feature +0 -167
  69. package/src/workflows/feature/release-flag.test.ts +0 -49
  70. package/src/workflows/feature/release-flag.ts +0 -46
  71. package/src/workflows/feature/review-gate.feature +0 -241
  72. package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
  73. package/src/workflows/feature/risk-tier.feature +0 -351
  74. package/src/workflows/feature/seam-rejection.feature +0 -146
  75. package/src/workflows/feature/seam.feature +0 -297
  76. package/src/workflows/feature/seam.test.ts +0 -527
  77. package/src/workflows/feature/seam.ts +0 -426
  78. package/src/workflows/feature/standards.feature +0 -354
  79. package/src/workflows/feature/steps.test.ts +0 -65
  80. package/src/workflows/feature/steps.ts +0 -45
  81. package/src/workflows/feature/tier-pin.feature +0 -66
  82. package/src/workflows/feature/tier.test.ts +0 -166
  83. package/src/workflows/feature/tier.ts +0 -56
  84. package/src/workflows/fix/fix-lens-origin.feature +0 -114
  85. package/src/workflows/fix/fix-quality.feature +0 -212
  86. package/src/workflows/fix/fix.feature +0 -79
  87. package/src/workflows/fix/fix.test.ts +0 -74
  88. package/src/workflows/fix/fix.ts +0 -52
  89. package/src/workflows/fix/index.ts +0 -6
  90. package/src/workflows/index.ts +0 -47
  91. package/src/workflows/review/index.ts +0 -6
  92. package/src/workflows/review/review-door.feature +0 -199
  93. package/src/workflows/review/review.test.ts +0 -27
  94. package/src/workflows/review/review.ts +0 -60
  95. package/src/workflows/rollout/index.ts +0 -6
  96. package/src/workflows/rollout/rollout.feature +0 -510
  97. package/src/workflows/rollout/rollout.test.ts +0 -263
  98. package/src/workflows/rollout/rollout.ts +0 -82
  99. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
  100. package/src/workflows/shared/access.test.ts +0 -62
  101. package/src/workflows/shared/access.ts +0 -24
  102. package/src/workflows/shared/config.fixture.ts +0 -12
  103. package/src/workflows/shared/fences.ts +0 -24
  104. package/src/workflows/shared/health.test.ts +0 -148
  105. package/src/workflows/shared/health.ts +0 -191
  106. package/src/workflows/shared/learn.test.ts +0 -140
  107. package/src/workflows/shared/learn.ts +0 -201
  108. package/src/workflows/shared/lenses.test.ts +0 -39
  109. package/src/workflows/shared/lenses.ts +0 -294
  110. package/src/workflows/shared/options.test.ts +0 -157
  111. package/src/workflows/shared/options.ts +0 -92
  112. package/src/workflows/shared/progress.feature +0 -124
  113. package/src/workflows/shared/prose.ts +0 -263
  114. package/src/workflows/shared/recheck.feature +0 -60
  115. package/src/workflows/shared/registry.test.ts +0 -345
  116. package/src/workflows/shared/registry.ts +0 -183
  117. package/src/workflows/shared/reserved.ts +0 -11
  118. package/src/workflows/shared/review.test.ts +0 -1062
  119. package/src/workflows/shared/review.ts +0 -446
  120. package/src/workflows/shared/skills.test.ts +0 -121
  121. package/src/workflows/shared/skills.ts +0 -49
  122. package/src/workflows/shared/standards.test.ts +0 -90
  123. package/src/workflows/shared/standards.ts +0 -156
  124. package/src/workflows/shared/steps.test.ts +0 -219
  125. package/src/workflows/shared/steps.ts +0 -201
  126. package/src/workflows/shared/tail.test.ts +0 -454
  127. package/src/workflows/shared/tail.ts +0 -264
  128. package/src/workflows/shared/text.fixture.ts +0 -128
  129. package/src/workflows/shared/text.test.ts +0 -1430
  130. package/src/workflows/shared/text.ts +0 -1700
  131. package/src/workflows/shared/threads.test.ts +0 -129
  132. package/src/workflows/shared/vars.ts +0 -47
  133. package/src/workflows/shared/workflows.steps.ts +0 -54
@@ -1,267 +0,0 @@
1
- import {
2
- answered,
3
- changes,
4
- head,
5
- judge,
6
- read,
7
- refuse,
8
- requireProgress,
9
- run,
10
- scope,
11
- type Change,
12
- } from "../../flows/index.js"
13
- import { HEADING, ITEM, markFenced } from "../shared/fences.js"
14
- import { planner, REQUIREMENTS } from "../shared/steps.js"
15
- import * as t from "../shared/text.js"
16
- import { requireReleaseFlag } from "./release-flag.js"
17
-
18
- export interface Criterion {
19
- readonly id: string
20
- readonly text: string
21
- }
22
-
23
- /** Acceptance criteria the human's sketch stated outright, ready to normalise. */
24
- export interface StatedCriteria {
25
- readonly path: string
26
- readonly commit: string
27
- /** The file as the sketch left it: `unwind` reverts it out of the tree. */
28
- readonly text: string
29
- readonly criteria: readonly Criterion[]
30
- }
31
-
32
- /** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
33
- const CLEAR_MIN_P = 0.8
34
-
35
- const ID = /^(AC-\d+):\s*(.*)$/
36
-
37
- const criterion = (id: RegExpExecArray, as = id[1]!): Criterion => ({ id: as, text: id[2]! })
38
-
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. */
40
- const listUnder = (text: string, title: RegExp, level?: number): string[] | undefined => {
41
- const lines = markFenced(text)
42
- const at = lines.findIndex((l) => {
43
- const heading = l.fenced ? null : HEADING.exec(l.line)
44
- return (
45
- heading !== null &&
46
- (level === undefined || heading[1]!.length === level) &&
47
- title.test(heading[2]!)
48
- )
49
- })
50
- if (at === -1) return undefined
51
- const items: string[] = []
52
- let open = false
53
- for (const { line, fenced } of lines.slice(at + 1)) {
54
- if (fenced) {
55
- open = false
56
- continue
57
- }
58
- if (HEADING.test(line)) break
59
- const item = ITEM.exec(line)
60
- if (item !== null) {
61
- items.push(item[1]!.trim())
62
- open = true
63
- } else if (open && line.trim() !== "") {
64
- items[items.length - 1] = `${items.at(-1)!} ${line.trim()}`.trim()
65
- } else open = false
66
- }
67
- return items
68
- }
69
-
70
- /** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
71
- export const criteriaOf = (text: string): readonly Criterion[] | undefined =>
72
- listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
73
- const id = ID.exec(item)
74
- return id === null ? { id: "", text: item } : criterion(id)
75
- })
76
-
77
- /** The ID'd criteria of `text`. */
78
- export const criterionIds = (text: string): string[] =>
79
- (criteriaOf(text) ?? []).map((c) => c.id).filter((id) => id !== "")
80
-
81
- /** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
82
- const numbered = (items: readonly string[]): Criterion[] => {
83
- const ids = items.map((item) => ID.exec(item))
84
- const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id![1])).size === ids.length
85
- return items.map((item, i) => {
86
- const id = ids[i]
87
- if (own) return criterion(id!)
88
- return id === null ? { id: `AC-${i + 1}`, text: item } : criterion(id!, `AC-${i + 1}`)
89
- })
90
- }
91
-
92
- const STATED = /^acceptance criteria:?$/i
93
-
94
- const statedItems = (text: string | undefined): string[] =>
95
- text === undefined ? [] : (listUnder(text, STATED) ?? []).filter((item) => item !== "")
96
-
97
- /**
98
- * The criteria a `TODO.md` in the sketch `sketch` (the idle turn's changes)
99
- * added or changed under an `Acceptance criteria` heading. More than one such
100
- * file is ambiguous, and any other change in the sketch is input only triage
101
- * folds in, so none.
102
- */
103
- export const statedCriteria = (
104
- sketch: readonly Change[],
105
- commit: string,
106
- ): StatedCriteria | undefined => {
107
- const found = sketch.flatMap((change) => {
108
- if (!/(^|\/)TODO\.md$/.test(change.path) || change.after === undefined) return []
109
- const items = statedItems(change.after)
110
- if (items.length === 0 || items.join("\n") === statedItems(change.before).join("\n")) return []
111
- return [{ path: change.path, commit, text: change.after, criteria: numbered(items) }]
112
- })
113
- return found.length === 1 && sketch.length === 1 ? found[0] : undefined
114
- }
115
-
116
- const listing = (criteria: readonly Criterion[]): string => criteria.map(t.criterionLine).join("\n")
117
-
118
- /** Refuse a turn that leaves `file` without ID'd acceptance criteria. A deleted file is `requireProgress`'s call. */
119
- export const requireCriteria = (file: string): void => {
120
- const text = read(file)
121
- if (text === undefined) return
122
- const criteria = criteriaOf(text) ?? []
123
- const unnamed = criteria.filter((c) => c.id === "")
124
- const ids = criteria.map((c) => c.id).filter((id) => id !== "")
125
- const twice = ids.filter((id, i) => ids.indexOf(id) !== i)
126
- if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return
127
- const why =
128
- criteria.length === 0
129
- ? `no "## Acceptance Criteria" list`
130
- : unnamed.length > 0
131
- ? `${unnamed.length} criterion without an ID:\n${unnamed.map((c) => ` - ${c.text}`).join("\n")}`
132
- : `duplicate IDs: ${[...new Set(twice)].join(", ")}`
133
- refuse(
134
- `gtd land: acceptance-criteria: ${file} has ${why} — list every criterion under "## Acceptance Criteria" as "- AC-<n>: <checkable criterion>", each ID unique and never reused`,
135
- )
136
- }
137
-
138
- const isImpactHeading = (h: RegExpExecArray | null): boolean =>
139
- h !== null && h[1]!.length === 2 && /^impact$/i.test(h[2]!.trim())
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
-
145
- /** Whether `text` has an unfenced, level-2 `## Impact` heading. */
146
- export const hasImpact = (text: string): boolean =>
147
- markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)))
148
-
149
- /** `text` without its `## Impact` and `## Release Flag` sections, whitespace collapsed. */
150
- const withoutImpact = (text: string): string => {
151
- let inImpact = false
152
- const kept = markFenced(text).filter((l) => {
153
- const h = l.fenced ? null : HEADING.exec(l.line)
154
- if (h !== null && h[1]!.length <= 2) inImpact = isImpactTurnHeading(h)
155
- return !inImpact
156
- })
157
- return kept
158
- .map((l) => l.line)
159
- .join(" ")
160
- .replace(/\s+/g, " ")
161
- .trim()
162
- }
163
-
164
- /**
165
- * Refuse a turn that leaves `file` without a `## Impact` section. Only the
166
- * pending turn (`head()` is `""`): a triage lap recorded before Impact existed
167
- * replays without one, and refusing there would fail every in-flight process.
168
- * A human's edit at a later gate is not checked — the human owns that file then.
169
- */
170
- export const requireImpact = (file: string): void => {
171
- if (head() !== "") return
172
- const text = read(file)
173
- if (text === undefined) return
174
- if (hasImpact(text)) return
175
- refuse(
176
- `gtd land: impact: ${file} has no "## Impact" section — explore the codebase and add "## Impact" naming the units, public surfaces and surprises this change touches`,
177
- )
178
- }
179
-
180
- /** Refuse a turn that changed `file` beyond its `## Impact` and `## Release Flag` sections. */
181
- export const requireImpactOnly = (file: string): void => {
182
- const change = changes().get(file)
183
- if (change === undefined) return
184
- if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return
185
- refuse(
186
- `gtd land: impact-only: this turn may only add "## Impact" (and "## Release Flag") to ${file} — restore everything else`,
187
- )
188
- }
189
-
190
- /** The IDs a review's feedback lap must respect. */
191
- export interface StableIds {
192
- /** Listed when the build closed: each may stay, reworded or not, or go. */
193
- readonly closed: readonly string[]
194
- /** Every ID handed out so far: any other is numbered above all of them. */
195
- readonly used: readonly string[]
196
- }
197
-
198
- const idNumber = (id: string): number => Number(id.slice("AC-".length))
199
-
200
- /** Why `listed` renumbers or reuses an earlier ID; `undefined` when it does neither. */
201
- export const unstableIds = (listed: readonly string[], ids: StableIds): string | undefined => {
202
- const top = Math.max(0, ...ids.used.map(idNumber))
203
- const reused = listed.filter((id) => !ids.closed.includes(id) && idNumber(id) <= top)
204
- if (reused.length === 0) return undefined
205
- return `numbers ${reused.join(", ")} at or below AC-${top}, an ID already handed out — keep a built criterion's ID (reword it in place when the review changes it), and number new ones from AC-${top + 1}`
206
- }
207
-
208
- /** Refuse criteria in `file` that renumber or reuse an earlier ID. */
209
- export const requireStableIds = (file: string, ids: StableIds): void => {
210
- const why = unstableIds(criterionIds(read(file) ?? ""), ids)
211
- if (why !== undefined) refuse(`gtd land: criteria-ids: ${file} ${why}`)
212
- }
213
-
214
- /** Refuse a turn that changed the acceptance criteria in `file`: after the design gate only another design lap may. */
215
- export const requireCriteriaFrozen = (file: string): void => {
216
- const change = changes().get(file)
217
- if (change === undefined) return
218
- const before = listing(criteriaOf(change.before ?? "") ?? [])
219
- if (before === listing(criteriaOf(change.after ?? "") ?? [])) return
220
- refuse(
221
- `gtd land: criteria-frozen: the acceptance criteria in ${file} are settled — restore them; only a design lap changes them`,
222
- )
223
- }
224
-
225
- /** Whether the judge trusts `stated` as clear and checkable, every criterion of it. Truncated evidence never is. */
226
- const clear = async (stated: StatedCriteria): Promise<boolean> => {
227
- const { answers, truncated } = await judge("clear", {
228
- questions: [t.clearCriteriaQuestion],
229
- evidence: { criteria: listing(stated.criteria), sketch: stated.text },
230
- message: t.clearCriteriaMessage(),
231
- label: "Judging the stated acceptance criteria",
232
- })
233
- return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P)
234
- }
235
-
236
- /** The fast path's impact-only turn: add `## Impact`, leave the criteria alone. */
237
- const impact = async (): Promise<void> => {
238
- await t.agentWithSkills("impact", t.designImpactPrompt(), {
239
- label: "Mapping the change's impact",
240
- file: REQUIREMENTS,
241
- model: planner(),
242
- system: t.designSystem(),
243
- })
244
- requireProgress(REQUIREMENTS)
245
- requireCriteriaFrozen(REQUIREMENTS)
246
- requireImpact(REQUIREMENTS)
247
- requireImpactOnly(REQUIREMENTS)
248
- requireReleaseFlag(REQUIREMENTS)
249
- }
250
-
251
- /**
252
- * The skip path: when the sketch stated clear, checkable criteria, write them
253
- * to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
254
- * design gate. `false` when design must run.
255
- */
256
- export const skipDesign = (stated: StatedCriteria | undefined): Promise<boolean> =>
257
- stated === undefined
258
- ? Promise.resolve(false)
259
- : scope("design", async () => {
260
- if (!(await clear(stated))) return false
261
- await run("normalise", t.normaliseCriteriaScript(stated), {
262
- label: "Recording the stated acceptance criteria",
263
- file: REQUIREMENTS,
264
- })
265
- await impact()
266
- return true
267
- })
@@ -1,93 +0,0 @@
1
- @inmem
2
- Feature: a trivial change takes the same few beats at either risk tier
3
-
4
- With GTD_FLAG_RISK_TIER on, a one-line copy fix goes
5
- from idle through the naive pass, the seam judge's land, the applied pass and
6
- the quality lap to the review gate. Low tier walks exactly the steps high does; only
7
- the summary differs.
8
-
9
- Background:
10
- Given the shipped workflows, as gtd install scaffolds them
11
- And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
12
- And an environment variable "GTD_FLAG_RISK_TIER" set to "1"
13
- And a file "src/greeting.ts" with:
14
- """
15
- export const greeting = "Helo, world"
16
- """
17
- And the working tree is committed as "feat: add greeting"
18
- And a file "TODO.md" with:
19
- """
20
- # Fix the typo
21
-
22
- greeting says "Helo"; make it "Hello".
23
- """
24
- And step "unwind" changes nothing
25
- And step "start-gate.check" changes nothing
26
- And step "design.gate.answer" changes nothing
27
- And step "implement.health.check" changes nothing
28
- And step "implement.closing" runs its script
29
- And step "build.quality.correctness.reviewing" changes nothing
30
-
31
- Scenario Outline: a one-line fix runs from idle to the review gate with the same beats at either tier
32
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
33
- """
34
- # Requirements
35
-
36
- ## Risk Tier
37
-
38
- <tier> — one-line copy fix
39
-
40
- ## Impact
41
-
42
- - the greeting module
43
-
44
- ## Release Flag
45
-
46
- new
47
-
48
- ## Acceptance Criteria
49
-
50
- - AC-1: the change behaves as described.
51
- """
52
- When the process is driven
53
- Then the process rests at "seam.naive"
54
- And the last commit subject is "gtd(human): design.gate.answer → seam.naive"
55
-
56
- Given step "seam.check" changes nothing
57
- And step "seam.naive" writes "src/acceptance.test.ts" with:
58
- """
59
- it("[AC-1] holds", () => {})
60
- """
61
- And step "seam.naive" writes "src/greeting.ts" with:
62
- """
63
- export const greeting = "Hello, world"
64
- """
65
- And judge step "seam.judge" answers:
66
- """
67
- [{"id":"shape","answer":"land","p":0.9}]
68
- """
69
- And step "implement.applying" runs its script
70
- When the process is driven
71
- Then the process rests at "build.review.reviewing"
72
- And "src/greeting.ts" contains "Hello, world"
73
- And the commit subjects end with:
74
- """
75
- gtd(human): idle → unwind
76
- gtd(check): unwind → start-gate.check
77
- gtd(check): start-gate.check → design.triage
78
- gtd(agent): design.triage → design.gate.answer
79
- gtd(human): design.gate.answer → seam.naive
80
- gtd(agent): seam.naive → seam.check
81
- gtd(check): seam.check → seam.judge
82
- gtd(judge): seam.judge → implement.applying
83
- gtd(check): implement.applying → implement.health.check
84
- gtd(check): implement.health.check → implement.closing
85
- gtd(check): implement.closing → build.quality.correctness.reviewing
86
- gtd(agent): build.quality.correctness.reviewing → build.review.reviewing
87
- """
88
- And the output of gtd "summary" <summary> "needs a human reviewer"
89
-
90
- Examples:
91
- | tier | summary |
92
- | low | does not contain |
93
- | high | contains |