@pmelab/gtd 58.1.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 (142) 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 +30 -2
  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/gherkin/git-control.feature +78 -0
  17. package/src/gherkin/index.ts +35 -1
  18. package/src/install/Install.test.ts +14 -11
  19. package/src/install/Install.ts +38 -15
  20. package/src/install/index.ts +1 -2
  21. package/src/replay/Replay.test.ts +1 -1
  22. package/src/replay/index.ts +1 -1
  23. package/src/step/planStart.ts +6 -1
  24. package/src/testkit/TestRepo.test.ts +118 -2
  25. package/src/testkit/TestRepo.ts +57 -6
  26. package/src/testkit/index.ts +1 -0
  27. package/src/twins/EmittedScriptRecognizer.ts +12 -14
  28. package/src/twins/FlowScripts.ts +10 -9
  29. package/src/twins/InMemRepo.ts +26 -1
  30. package/src/twins/Layers.ts +12 -1
  31. package/src/twins/index.ts +6 -1
  32. package/src/workflow/index.ts +1 -0
  33. package/src/workflow/load.test.ts +138 -113
  34. package/src/workflow/load.ts +1 -1
  35. package/src/workflow/vars.test.ts +30 -3
  36. package/src/workflow/vars.ts +34 -2
  37. package/src/workflow-kit/index.ts +1 -1
  38. package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
  39. package/dist/chunks/review-Bfaror38.mjs +0 -427
  40. package/dist/chunks/tail-Bd7L940p.mjs +0 -355
  41. package/dist/chunks/testkit-D1-auYau.mjs +0 -10362
  42. package/dist/workflows/cleanup.mjs +0 -36
  43. package/dist/workflows/feature.mjs +0 -1018
  44. package/dist/workflows/fix.mjs +0 -41
  45. package/dist/workflows/review.mjs +0 -45
  46. package/dist/workflows/rollout.mjs +0 -48
  47. package/dist/workflows.mjs +0 -40651
  48. package/src/install/ConfigScaffold.ts +0 -22
  49. package/src/workflows/cleanup/cleanup-path.feature +0 -94
  50. package/src/workflows/cleanup/cleanup.feature +0 -315
  51. package/src/workflows/cleanup/cleanup.test.ts +0 -276
  52. package/src/workflows/cleanup/cleanup.ts +0 -52
  53. package/src/workflows/cleanup/index.ts +0 -6
  54. package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
  55. package/src/workflows/feature/acceptance-criteria.feature +0 -389
  56. package/src/workflows/feature/criteria-coverage.feature +0 -102
  57. package/src/workflows/feature/criteria.test.ts +0 -147
  58. package/src/workflows/feature/criteria.ts +0 -267
  59. package/src/workflows/feature/fast-path.feature +0 -93
  60. package/src/workflows/feature/feature.test.ts +0 -338
  61. package/src/workflows/feature/feature.ts +0 -156
  62. package/src/workflows/feature/green.ts +0 -19
  63. package/src/workflows/feature/index.ts +0 -18
  64. package/src/workflows/feature/lens-models.feature +0 -69
  65. package/src/workflows/feature/lens-review.feature +0 -566
  66. package/src/workflows/feature/naive-acceptance.feature +0 -147
  67. package/src/workflows/feature/naive-test.feature +0 -263
  68. package/src/workflows/feature/one-step-land.feature +0 -93
  69. package/src/workflows/feature/planning.ts +0 -41
  70. package/src/workflows/feature/refactor-absorbs.feature +0 -131
  71. package/src/workflows/feature/refactor-first.feature +0 -158
  72. package/src/workflows/feature/refactor-lens-models.feature +0 -79
  73. package/src/workflows/feature/refactor-rebuild.feature +0 -273
  74. package/src/workflows/feature/refactor-review.feature +0 -1528
  75. package/src/workflows/feature/refactor.test.ts +0 -1220
  76. package/src/workflows/feature/refactor.ts +0 -368
  77. package/src/workflows/feature/release-flag.feature +0 -167
  78. package/src/workflows/feature/release-flag.test.ts +0 -49
  79. package/src/workflows/feature/release-flag.ts +0 -46
  80. package/src/workflows/feature/review-gate.feature +0 -241
  81. package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
  82. package/src/workflows/feature/risk-tier.feature +0 -351
  83. package/src/workflows/feature/seam-rejection.feature +0 -146
  84. package/src/workflows/feature/seam.feature +0 -297
  85. package/src/workflows/feature/seam.test.ts +0 -527
  86. package/src/workflows/feature/seam.ts +0 -426
  87. package/src/workflows/feature/standards.feature +0 -354
  88. package/src/workflows/feature/steps.test.ts +0 -65
  89. package/src/workflows/feature/steps.ts +0 -45
  90. package/src/workflows/feature/tier-pin.feature +0 -66
  91. package/src/workflows/feature/tier.test.ts +0 -166
  92. package/src/workflows/feature/tier.ts +0 -56
  93. package/src/workflows/fix/fix-lens-origin.feature +0 -114
  94. package/src/workflows/fix/fix-quality.feature +0 -212
  95. package/src/workflows/fix/fix.feature +0 -79
  96. package/src/workflows/fix/fix.test.ts +0 -74
  97. package/src/workflows/fix/fix.ts +0 -52
  98. package/src/workflows/fix/index.ts +0 -6
  99. package/src/workflows/index.ts +0 -47
  100. package/src/workflows/review/index.ts +0 -6
  101. package/src/workflows/review/review-door.feature +0 -199
  102. package/src/workflows/review/review.test.ts +0 -27
  103. package/src/workflows/review/review.ts +0 -60
  104. package/src/workflows/rollout/index.ts +0 -6
  105. package/src/workflows/rollout/rollout.feature +0 -510
  106. package/src/workflows/rollout/rollout.test.ts +0 -263
  107. package/src/workflows/rollout/rollout.ts +0 -82
  108. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
  109. package/src/workflows/shared/access.test.ts +0 -62
  110. package/src/workflows/shared/access.ts +0 -24
  111. package/src/workflows/shared/config.fixture.ts +0 -12
  112. package/src/workflows/shared/fences.ts +0 -24
  113. package/src/workflows/shared/health.test.ts +0 -148
  114. package/src/workflows/shared/health.ts +0 -191
  115. package/src/workflows/shared/learn.test.ts +0 -140
  116. package/src/workflows/shared/learn.ts +0 -201
  117. package/src/workflows/shared/lenses.test.ts +0 -39
  118. package/src/workflows/shared/lenses.ts +0 -294
  119. package/src/workflows/shared/options.test.ts +0 -157
  120. package/src/workflows/shared/options.ts +0 -92
  121. package/src/workflows/shared/progress.feature +0 -124
  122. package/src/workflows/shared/prose.ts +0 -263
  123. package/src/workflows/shared/recheck.feature +0 -60
  124. package/src/workflows/shared/registry.test.ts +0 -345
  125. package/src/workflows/shared/registry.ts +0 -183
  126. package/src/workflows/shared/reserved.ts +0 -11
  127. package/src/workflows/shared/review.test.ts +0 -1062
  128. package/src/workflows/shared/review.ts +0 -446
  129. package/src/workflows/shared/skills.test.ts +0 -121
  130. package/src/workflows/shared/skills.ts +0 -49
  131. package/src/workflows/shared/standards.test.ts +0 -90
  132. package/src/workflows/shared/standards.ts +0 -156
  133. package/src/workflows/shared/steps.test.ts +0 -219
  134. package/src/workflows/shared/steps.ts +0 -201
  135. package/src/workflows/shared/tail.test.ts +0 -454
  136. package/src/workflows/shared/tail.ts +0 -264
  137. package/src/workflows/shared/text.fixture.ts +0 -128
  138. package/src/workflows/shared/text.test.ts +0 -1430
  139. package/src/workflows/shared/text.ts +0 -1700
  140. package/src/workflows/shared/threads.test.ts +0 -129
  141. package/src/workflows/shared/vars.ts +0 -47
  142. 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 |