@pmelab/gtd 58.2.0 → 59.0.1

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 (150) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +120 -395
  3. package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-07Z_ENch.mjs} +13503 -3460
  4. package/dist/chunks/{wire-C12dahrH.mjs → wire-BTUhRGXj.mjs} +1 -1
  5. package/dist/flows.mjs +1 -1
  6. package/dist/gtd.bundle.mjs +484 -497
  7. package/dist/testkit/gherkin.mjs +145 -65
  8. package/dist/testkit.mjs +1 -1
  9. package/dist/wire.mjs +1 -1
  10. package/package.json +3 -42
  11. package/skills/authoring/SKILL.md +34 -27
  12. package/src/cli/Cli.test.ts +148 -71
  13. package/src/cli/Cli.ts +384 -463
  14. package/src/cli/Help.test.ts +47 -0
  15. package/src/cli/Help.ts +70 -0
  16. package/src/cli/Program.test.ts +127 -33
  17. package/src/cli/Program.ts +70 -25
  18. package/src/edge/Edge.ts +4 -4
  19. package/src/emit/Emit.test.ts +1 -1
  20. package/src/emit/Emit.ts +1 -1
  21. package/src/flags/index.test.ts +0 -13
  22. package/src/flags/index.ts +1 -3
  23. package/src/flows/factory.ts +3 -1
  24. package/src/gherkin/index.ts +135 -71
  25. package/src/gherkin/messages.test.ts +90 -0
  26. package/src/gherkin/messages.ts +84 -0
  27. package/src/install/Install.test.ts +14 -11
  28. package/src/install/Install.ts +38 -15
  29. package/src/install/index.ts +1 -2
  30. package/src/replay/Replay.test.ts +1 -1
  31. package/src/replay/index.ts +1 -1
  32. package/src/step/planStart.ts +6 -1
  33. package/src/testkit/TestRepo.test.ts +47 -2
  34. package/src/testkit/TestRepo.ts +51 -6
  35. package/src/twins/EmittedScriptRecognizer.ts +5 -8
  36. package/src/twins/FlowScripts.ts +1 -1
  37. package/src/wire/Land.ts +1 -1
  38. package/src/workflow/index.ts +1 -0
  39. package/src/workflow/load.test.ts +138 -113
  40. package/src/workflow/load.ts +7 -7
  41. package/src/workflow/skills.test.ts +5 -5
  42. package/src/workflow/skills.ts +1 -1
  43. package/src/workflow/vars.test.ts +30 -3
  44. package/src/workflow/vars.ts +34 -2
  45. package/src/workflow-kit/index.ts +1 -1
  46. package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
  47. package/dist/chunks/review-Bfaror38.mjs +0 -427
  48. package/dist/chunks/tail-Bd7L940p.mjs +0 -355
  49. package/dist/chunks/testkit-JDtpNKyi.mjs +0 -10410
  50. package/dist/workflows/cleanup.mjs +0 -36
  51. package/dist/workflows/feature.mjs +0 -1018
  52. package/dist/workflows/fix.mjs +0 -41
  53. package/dist/workflows/review.mjs +0 -45
  54. package/dist/workflows/rollout.mjs +0 -48
  55. package/dist/workflows.mjs +0 -40651
  56. package/src/install/ConfigScaffold.ts +0 -22
  57. package/src/workflows/cleanup/cleanup-path.feature +0 -94
  58. package/src/workflows/cleanup/cleanup.feature +0 -315
  59. package/src/workflows/cleanup/cleanup.test.ts +0 -276
  60. package/src/workflows/cleanup/cleanup.ts +0 -52
  61. package/src/workflows/cleanup/index.ts +0 -6
  62. package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
  63. package/src/workflows/feature/acceptance-criteria.feature +0 -389
  64. package/src/workflows/feature/criteria-coverage.feature +0 -102
  65. package/src/workflows/feature/criteria.test.ts +0 -147
  66. package/src/workflows/feature/criteria.ts +0 -267
  67. package/src/workflows/feature/fast-path.feature +0 -93
  68. package/src/workflows/feature/feature.test.ts +0 -338
  69. package/src/workflows/feature/feature.ts +0 -156
  70. package/src/workflows/feature/green.ts +0 -19
  71. package/src/workflows/feature/index.ts +0 -18
  72. package/src/workflows/feature/lens-models.feature +0 -69
  73. package/src/workflows/feature/lens-review.feature +0 -566
  74. package/src/workflows/feature/naive-acceptance.feature +0 -147
  75. package/src/workflows/feature/naive-test.feature +0 -263
  76. package/src/workflows/feature/one-step-land.feature +0 -93
  77. package/src/workflows/feature/planning.ts +0 -41
  78. package/src/workflows/feature/refactor-absorbs.feature +0 -131
  79. package/src/workflows/feature/refactor-first.feature +0 -158
  80. package/src/workflows/feature/refactor-lens-models.feature +0 -79
  81. package/src/workflows/feature/refactor-rebuild.feature +0 -273
  82. package/src/workflows/feature/refactor-review.feature +0 -1528
  83. package/src/workflows/feature/refactor.test.ts +0 -1220
  84. package/src/workflows/feature/refactor.ts +0 -368
  85. package/src/workflows/feature/release-flag.feature +0 -167
  86. package/src/workflows/feature/release-flag.test.ts +0 -49
  87. package/src/workflows/feature/release-flag.ts +0 -46
  88. package/src/workflows/feature/review-gate.feature +0 -241
  89. package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
  90. package/src/workflows/feature/risk-tier.feature +0 -351
  91. package/src/workflows/feature/seam-rejection.feature +0 -146
  92. package/src/workflows/feature/seam.feature +0 -297
  93. package/src/workflows/feature/seam.test.ts +0 -527
  94. package/src/workflows/feature/seam.ts +0 -426
  95. package/src/workflows/feature/standards.feature +0 -354
  96. package/src/workflows/feature/steps.test.ts +0 -65
  97. package/src/workflows/feature/steps.ts +0 -45
  98. package/src/workflows/feature/tier-pin.feature +0 -66
  99. package/src/workflows/feature/tier.test.ts +0 -166
  100. package/src/workflows/feature/tier.ts +0 -56
  101. package/src/workflows/fix/fix-lens-origin.feature +0 -114
  102. package/src/workflows/fix/fix-quality.feature +0 -212
  103. package/src/workflows/fix/fix.feature +0 -79
  104. package/src/workflows/fix/fix.test.ts +0 -74
  105. package/src/workflows/fix/fix.ts +0 -52
  106. package/src/workflows/fix/index.ts +0 -6
  107. package/src/workflows/index.ts +0 -47
  108. package/src/workflows/review/index.ts +0 -6
  109. package/src/workflows/review/review-door.feature +0 -199
  110. package/src/workflows/review/review.test.ts +0 -27
  111. package/src/workflows/review/review.ts +0 -60
  112. package/src/workflows/rollout/index.ts +0 -6
  113. package/src/workflows/rollout/rollout.feature +0 -510
  114. package/src/workflows/rollout/rollout.test.ts +0 -263
  115. package/src/workflows/rollout/rollout.ts +0 -82
  116. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
  117. package/src/workflows/shared/access.test.ts +0 -62
  118. package/src/workflows/shared/access.ts +0 -24
  119. package/src/workflows/shared/config.fixture.ts +0 -12
  120. package/src/workflows/shared/fences.ts +0 -24
  121. package/src/workflows/shared/health.test.ts +0 -148
  122. package/src/workflows/shared/health.ts +0 -191
  123. package/src/workflows/shared/learn.test.ts +0 -140
  124. package/src/workflows/shared/learn.ts +0 -201
  125. package/src/workflows/shared/lenses.test.ts +0 -39
  126. package/src/workflows/shared/lenses.ts +0 -294
  127. package/src/workflows/shared/options.test.ts +0 -157
  128. package/src/workflows/shared/options.ts +0 -92
  129. package/src/workflows/shared/progress.feature +0 -124
  130. package/src/workflows/shared/prose.ts +0 -263
  131. package/src/workflows/shared/recheck.feature +0 -60
  132. package/src/workflows/shared/registry.test.ts +0 -345
  133. package/src/workflows/shared/registry.ts +0 -183
  134. package/src/workflows/shared/reserved.ts +0 -11
  135. package/src/workflows/shared/review.test.ts +0 -1062
  136. package/src/workflows/shared/review.ts +0 -446
  137. package/src/workflows/shared/skills.test.ts +0 -121
  138. package/src/workflows/shared/skills.ts +0 -49
  139. package/src/workflows/shared/standards.test.ts +0 -90
  140. package/src/workflows/shared/standards.ts +0 -156
  141. package/src/workflows/shared/steps.test.ts +0 -219
  142. package/src/workflows/shared/steps.ts +0 -201
  143. package/src/workflows/shared/tail.test.ts +0 -454
  144. package/src/workflows/shared/tail.ts +0 -264
  145. package/src/workflows/shared/text.fixture.ts +0 -128
  146. package/src/workflows/shared/text.test.ts +0 -1430
  147. package/src/workflows/shared/text.ts +0 -1700
  148. package/src/workflows/shared/threads.test.ts +0 -129
  149. package/src/workflows/shared/vars.ts +0 -47
  150. package/src/workflows/shared/workflows.steps.ts +0 -54
@@ -1,263 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
- import { CONFIG_SCAFFOLD } from "../../install/index.js"
3
- import { runsItsScript, testRepo } from "../../testkit/index.js"
4
-
5
- const CONFIG = `${CONFIG_SCAFFOLD}export { rollout } from "@pmelab/gtd/workflows"\n`
6
-
7
- const REGISTRY = `export const FLAGS = [
8
- { name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release" },
9
- { name: "beta", owner: "b", expiry: "2027-02-01", kind: "release" },
10
- { name: "ops1", owner: "c", expiry: "2027-03-01", kind: "ops" },
11
- { name: "live", owner: "d", expiry: "2027-04-01", kind: "release", defaultOn: true },
12
- ] as const
13
- `
14
-
15
- const FLIPPED = REGISTRY.replace(
16
- '{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release" }',
17
- '{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release", defaultOn: true }',
18
- )
19
-
20
- const repoWith = () =>
21
- testRepo({ workflow: CONFIG, files: { "src/flags/index.ts": REGISTRY, "src/a.ts": "a\n" } })
22
-
23
- describe("the rollout workflow", () => {
24
- it("lands a flip, checks the suite and ends with no review", async () => {
25
- const repo = repoWith()
26
- await repo.start("rollout", "--var", "flag=alpha")
27
- const rest = await repo.drive({
28
- "rollout.flipping": (beat, r) => {
29
- expect(beat.content).toContain("alpha")
30
- r.write("src/flags/index.ts", FLIPPED)
31
- },
32
- "rollout.suite.check": () => undefined,
33
- "rollout.mentions": runsItsScript,
34
- })
35
- expect(rest.state).toBe("idle")
36
- expect(repo.subjects().slice(-2)).toEqual([
37
- "gtd(check): rollout.suite.check → rollout.mentions",
38
- "gtd(check): rollout.mentions → idle",
39
- ])
40
- })
41
-
42
- it("lands a flip when the tree was already dirty at the start", async () => {
43
- const repo = repoWith()
44
- repo.write("src/other.ts", "dirty\n")
45
- await repo.start("rollout", "--var", "flag=alpha")
46
- const rest = await repo.drive({
47
- "rollout.flipping": (_, r) => r.write("src/flags/index.ts", FLIPPED),
48
- "rollout.suite.check": () => undefined,
49
- "rollout.mentions": runsItsScript,
50
- })
51
- expect(rest.state).toBe("idle")
52
- })
53
-
54
- it("refuses a turn that also changes a source file, naming it", async () => {
55
- const repo = repoWith()
56
- await repo.start("rollout", "--var", "flag=alpha")
57
- await expect(
58
- repo.drive({
59
- "rollout.flipping": (_, r) => {
60
- r.write("src/flags/index.ts", FLIPPED)
61
- r.write("src/a.ts", "changed\n")
62
- },
63
- }),
64
- ).rejects.toThrow(/rollout-only: src\/a\.ts changed/)
65
- })
66
-
67
- it("refuses an edit to a second entry", async () => {
68
- const repo = repoWith()
69
- await repo.start("rollout", "--var", "flag=alpha")
70
- await expect(
71
- repo.drive({
72
- "rollout.flipping": (_, r) =>
73
- r.write("src/flags/index.ts", FLIPPED.replace('owner: "b"', 'owner: "z"')),
74
- }),
75
- ).rejects.toThrow("rollout-only: other edits in src/flags/index.ts")
76
- })
77
-
78
- it("refuses a turn that writes defaultOn: false, naming the fault", async () => {
79
- const repo = repoWith()
80
- await repo.start("rollout", "--var", "flag=alpha")
81
- await expect(
82
- repo.drive({
83
- "rollout.flipping": (_, r) =>
84
- r.write("src/flags/index.ts", FLIPPED.replace("defaultOn: true }", "defaultOn: false }")),
85
- }),
86
- ).rejects.toThrow('"alpha" is not set to defaultOn: true')
87
- })
88
-
89
- it("refuses a turn that also writes under .gtd/", async () => {
90
- const repo = repoWith()
91
- await repo.start("rollout", "--var", "flag=alpha")
92
- await expect(
93
- repo.drive({
94
- "rollout.flipping": (_, r) => {
95
- r.write("src/flags/index.ts", FLIPPED)
96
- r.write(".gtd/NOTES.md", "planted\n")
97
- },
98
- }),
99
- ).rejects.toThrow(/rollout-only: \.gtd\/NOTES\.md changed/)
100
- })
101
-
102
- it.each([
103
- [[], "gtd: rollout needs --var flag=<name>"],
104
- [["--var", "flag=nope"], 'gtd: rollout: no flag "nope" in src/flags/index.ts'],
105
- [["--var", "flag=ops1"], 'gtd: rollout: flag "ops1" is ops, not release'],
106
- [["--var", "flag=live"], 'gtd: rollout: flag "live" is already default-on'],
107
- ])("refuses %j at start", async (args, message) => {
108
- await expect(repoWith().start("rollout", ...args)).rejects.toThrow(message)
109
- })
110
-
111
- it("rests at rollout.suite.blocked on a red suite, with no fix turn", async () => {
112
- const repo = repoWith()
113
- await repo.start("rollout", "--var", "flag=alpha")
114
- const rest = await repo.drive({
115
- "rollout.flipping": (_, r) => r.write("src/flags/index.ts", FLIPPED),
116
- "rollout.suite.check": (_, r) => r.write(".gtd/FEEDBACK.md", "1 failing\n"),
117
- "rollout.suite.recheck": (_, r) => r.write(".gtd/FEEDBACK.md", "still 1 failing\n"),
118
- })
119
- expect(rest).toMatchObject({ state: "rollout.suite.blocked", actor: "human" })
120
- })
121
-
122
- describe("the human's fix at rollout.suite.blocked", () => {
123
- const blocked = async () => {
124
- const repo = repoWith()
125
- await repo.start("rollout", "--var", "flag=alpha")
126
- await repo.drive({
127
- "rollout.flipping": (_, r) => r.write("src/flags/index.ts", FLIPPED),
128
- "rollout.suite.check": (_, r) => r.write(".gtd/FEEDBACK.md", "1 failing\n"),
129
- "rollout.suite.recheck": (_, r) => r.write(".gtd/FEEDBACK.md", "still 1 failing\n"),
130
- })
131
- return repo
132
- }
133
-
134
- it("refuses a fix that edits a source file, naming it", async () => {
135
- const repo = await blocked()
136
- await expect(
137
- repo.drive({ "rollout.suite.blocked": (_, r) => r.write("src/a.ts", "fixed\n") }),
138
- ).rejects.toThrow(/rollout-only: src\/a\.ts changed/)
139
- })
140
-
141
- it("refuses a fix that edits a second registry entry", async () => {
142
- const repo = await blocked()
143
- await expect(
144
- repo.drive({
145
- "rollout.suite.blocked": (_, r) =>
146
- r.write("src/flags/index.ts", FLIPPED.replace('owner: "b"', 'owner: "z"')),
147
- }),
148
- ).rejects.toThrow("rollout-only: other edits in src/flags/index.ts")
149
- })
150
-
151
- it("refuses a fix that rewrites the flipped entry back to defaultOn: false", async () => {
152
- const repo = await blocked()
153
- await expect(
154
- repo.drive({ "rollout.suite.blocked": (_, r) => r.write("src/flags/index.ts", REGISTRY) }),
155
- ).rejects.toThrow('"alpha" is not set to defaultOn: true')
156
- })
157
-
158
- it("lands a fix that re-wraps the flag's entry and ends with no review", async () => {
159
- const repo = await blocked()
160
- const rest = await repo.drive({
161
- "rollout.suite.blocked": (_, r) =>
162
- r.write(
163
- "src/flags/index.ts",
164
- FLIPPED.replace(
165
- '{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release", defaultOn: true }',
166
- '{\n name: "alpha",\n owner: "a",\n expiry: "2027-01-01",\n kind: "release",\n defaultOn: true,\n }',
167
- ),
168
- ),
169
- "rollout.suite.check": () => undefined,
170
- "rollout.mentions": runsItsScript,
171
- })
172
- expect(rest.state).toBe("idle")
173
- })
174
- })
175
- })
176
-
177
- describe("rollout.mentions", () => {
178
- const flip = {
179
- "rollout.flipping": (_: unknown, r: { write: (p: string, c: string) => void }) =>
180
- r.write("src/flags/index.ts", FLIPPED),
181
- }
182
- const finish = async (
183
- files: Record<string, string>,
184
- mentions: typeof runsItsScript = runsItsScript,
185
- ) => {
186
- const repo = testRepo({
187
- workflow: CONFIG,
188
- files: { "src/flags/index.ts": REGISTRY, "src/a.ts": "a\n", ...files },
189
- })
190
- await repo.start("rollout", "--var", "flag=alpha")
191
- const rest = await repo.drive({
192
- ...flip,
193
- "rollout.suite.check": () => undefined,
194
- "rollout.mentions": mentions,
195
- })
196
- expect(rest.state).toBe("idle")
197
- const marks = (repo.messages().at(-1) ?? "")
198
- .split("\n")
199
- .filter((l) => l.startsWith("Gtd-Mark: "))
200
- .map((l) => l.slice("Gtd-Mark: ".length))
201
- return { repo, marks }
202
- }
203
-
204
- it("lists mentions outside the registry and under no .gtd/ path", async () => {
205
- const { marks } = await finish({
206
- "README.md": "intro\non under GTD_FLAG_ALPHA=1\n",
207
- "src/help.ts": 'export const h = "alpha help"\n',
208
- ".gtd/NOTES.md": "alpha\n",
209
- })
210
- expect(marks).toEqual([
211
- "flag-mentions=2",
212
- "flag-mention=README.md:2",
213
- "flag-mention=src/help.ts:1",
214
- ])
215
- })
216
-
217
- it("the mentions script removes a feedback file present at the step; marks list the tree", async () => {
218
- const { repo, marks } = await finish({ "README.md": "alpha\n" }, (beat, r) => {
219
- r.write(".gtd/FEEDBACK.md", "alpha\n")
220
- runsItsScript(beat, r)
221
- })
222
- expect(repo.read(".gtd/FEEDBACK.md")).toBeUndefined()
223
- expect(marks).toEqual(["flag-mentions=1", "flag-mention=README.md:1"])
224
- })
225
-
226
- it("refuses a stray file written by the mentions script", async () => {
227
- await expect(
228
- finish({}, (beat, r) => {
229
- runsItsScript(beat, r)
230
- r.write("src/stray.ts", "x\n")
231
- }),
232
- ).rejects.toThrow("gtd land: rollout-only: src/stray.ts changed")
233
- })
234
-
235
- it("refuses a registry edit beyond the flip made by the mentions script", async () => {
236
- await expect(
237
- finish({}, (beat, r) => {
238
- runsItsScript(beat, r)
239
- r.write("src/flags/index.ts", FLIPPED.replace('owner: "b"', 'owner: "z"'))
240
- }),
241
- ).rejects.toThrow("rollout-only: other edits in src/flags/index.ts")
242
- })
243
-
244
- it("marks 0 when nothing names the flag", async () => {
245
- const { marks } = await finish({})
246
- expect(marks).toEqual(["flag-mentions=0"])
247
- })
248
-
249
- it("marks the total for 60 mentions but only the first 50 lines", async () => {
250
- const text = Array.from({ length: 60 }, (_, i) => `alpha ${i}`).join("\n")
251
- const { marks } = await finish({ "docs/many.md": `${text}\n` })
252
- expect(marks[0]).toBe("flag-mentions=60")
253
- expect(marks.filter((m) => m.startsWith("flag-mention="))).toHaveLength(50)
254
- })
255
-
256
- it("counts a path the mark grammar rejects without marking it, and never throws", async () => {
257
- const { marks } = await finish({
258
- "docs/bad\u0001name.md": "alpha\n",
259
- "README.md": "alpha\n",
260
- })
261
- expect(marks).toEqual(["flag-mentions=2", "flag-mention=README.md:1"])
262
- })
263
- })
@@ -1,82 +0,0 @@
1
- import {
2
- agent,
3
- changesSince,
4
- glob,
5
- head,
6
- mark,
7
- read,
8
- refuse,
9
- removeScript,
10
- run,
11
- scope,
12
- vars,
13
- } from "../../flows/index.js"
14
- import { gate } from "../shared/health.js"
15
- import { flagShipped } from "../shared/options.js"
16
- import { flagMentions, flipProblems, requireFlag } from "../shared/registry.js"
17
- import { coder, FEEDBACK } from "../shared/steps.js"
18
-
19
- const flippingPrompt = (name: string, registry: string): string =>
20
- `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`
21
-
22
- const suiteBlockedMessage = (): string =>
23
- "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land."
24
-
25
- // Marks past this many are dropped; the summary points at grep for the rest.
26
- const MARKED_MENTIONS = 50
27
-
28
- // Lists, never edits: the human fixes the now-stale prose, or cleanup does. The
29
- // list rides in marks only: a state file left at idle would be published by
30
- // ship and read as a failed unwind by the next process.
31
- const listMentions = async (registry: string, name: string, guard: () => void): Promise<void> => {
32
- await run("mentions", removeScript([FEEDBACK]), { label: "Listing flag mentions" })
33
- guard()
34
- const files = glob("**")
35
- .filter((path) => path !== registry && path !== ".gtd" && !path.startsWith(".gtd/"))
36
- .sort()
37
- .map((path) => ({ path, text: read(path) ?? "" }))
38
- const mentions = flagMentions(files, name)
39
- mark("flag-mentions", String(mentions.length))
40
- for (const m of mentions.slice(0, MARKED_MENTIONS)) {
41
- // A path the mark grammar rejects stays counted above, just unlisted here.
42
- try {
43
- mark("flag-mention", `${m.path}:${m.line}`)
44
- } catch {
45
- continue
46
- }
47
- }
48
- }
49
-
50
- // No review gate — a one-field change needs no review lap.
51
- export const rolloutFlow = async (): Promise<void> => {
52
- const registry = vars.flagRegistry ?? ""
53
- const name = vars.flag ?? ""
54
- // Before the turn, never after: replay re-runs every pre-turn check.
55
- requireFlag("rollout", registry, name, false)
56
- // Strays count from the same point `before` is read at, not `start()`: the opening commit sweeps in whatever the human left uncommitted.
57
- const base = head()
58
- const before = read(registry) ?? ""
59
- const guard = (): void => {
60
- const strays = changesSince(base).paths.filter((path) => path !== registry && path !== FEEDBACK)
61
- if (strays.length > 0) {
62
- refuse(
63
- `gtd land: rollout-only: ${strays.join(", ")} changed — rollout may only set defaultOn: true on "${name}" in ${registry}`,
64
- )
65
- }
66
- const problems = flipProblems(before, read(registry) ?? "", name)
67
- if (problems.length > 0) {
68
- refuse(`gtd land: rollout-only: other edits in ${registry}: ${problems.join("; ")}`)
69
- }
70
- }
71
- await scope("rollout", async () => {
72
- await agent("flipping", flippingPrompt(name, registry), {
73
- label: `Rolling out ${name}`,
74
- model: coder(),
75
- })
76
- guard()
77
- await gate("suite", suiteBlockedMessage(), guard)
78
- await listMentions(registry, name, guard)
79
- })
80
- }
81
-
82
- export const rollout = flagShipped(rolloutFlow, "rollout")
@@ -1,98 +0,0 @@
1
- - A deliverable, not a chat reply — size follows the work; cut padding
2
- - Lead with the answer; never circle back to restate it
3
- - Flat, commanding sentences — commit to the claim, never hedge
4
- - Everyday words; define an unavoidable term in five words or fewer, on
5
- first use
6
- - Bold carries the load: bold the claim, not the sentence around it —
7
- the bold text alone must yield the full point and every risk
8
- - **Never trim a risk, a number, a threshold, or a scoped condition to
9
- save space — this outranks every other rule here**
10
- - Ship the artifact bare — no lead-in, no sign-off
11
- - Compressing is not dropping: three load-bearing parts ship as three,
12
- each shorter, never as two
13
- - One idea per block; break when the idea shifts
14
- - Flag risk in one blunt line, never hedged prose; never narrate — do it
15
-
16
- - Machine-read: its format contract outranks every style rule above —
17
- keep every `##`/`###` heading, checkbox row, and marker line exactly
18
- as specified. Never renumber or rename a heading; a parser reads
19
- these literally and a violation refuses the turn
20
- - Voice rules above govern only the prose between these elements
21
-
22
- You are judging and classifying a round of review feedback.
23
-
24
- - You are an autonomous coding agent
25
- - This workflow's own state files are its private scratchpad, never
26
- project code or documentation
27
- - A footnote (`[^name]` plus its `[^name]:` definition) is a comment on
28
- its exact anchor — fold it in as a mandatory concern described
29
- against that anchor's hunk or paragraph, never flattened into a
30
- whole-file remark
31
- - A one-shot footnote needs no reply: fold it in and DELETE it in this
32
- same turn — marker and definition together. Never re-read a footnote
33
- already acted on. A one-shot footnote that asks a question becomes a
34
- thread instead: rewrite its definition as `- H: <its body>` then
35
- `- A: <your reply>`, keeping the marker
36
- - A thread is a definition whose body is a list of `- H:`/`- A:`
37
- entries. Handle each by its last entry:
38
- - last is `- H:` and asks something — reply with exactly one
39
- `- A: <reply>` entry and change nothing else for it
40
- - last concludes or advises — fold it in like a one-shot footnote,
41
- then delete marker and definition
42
- - last is `- A:` — it waits on the human; leave it alone
43
- - Never start a thread yourself, and never leave a thread whose last
44
- entry is `- H:` — a turn that does is refused
45
- - This turn writes `.gtd/REQUIREMENTS.md` (the folded concerns) and
46
- replies inside `.gtd/REVIEW.md` (thread replies only; the file stays
47
- in the tree) — you classify, you do not build
48
- - Fold every concluded thread and hand-edit — and each `edit` note
49
- the capture lists, no other note — into `.gtd/REQUIREMENTS.md` and
50
- delete the folded threads from `.gtd/REVIEW.md`; append one `- A:`
51
- reply to each thread whose last entry is a `- H:` question
52
- - Never fold a note's ticked answer box the capture does not list: gtd
53
- routes those itself
54
- - When the capture names an answering commit, carry each answered
55
- question into `.gtd/REQUIREMENTS.md`'s `## Answered Questions` as
56
- `### <the note>` plus its answer — the lap replaces `.gtd/REVIEW.md`
57
- - Keep capture order, and carry each note's `(tier: reason)`
58
- label into the concern folded from it
59
- - Nit notes were already fixed: never re-raise a fixed nit as a concern
60
- - Finish by running `gtd check review .gtd/REVIEW.md` and fix
61
- what it reports
62
-
63
- The raw review material is:
64
-
65
- capture-text
66
- It names a commit. Work from what you already reviewed if you
67
- wrote today's review earlier this conversation; otherwise read
68
- that commit's diff yourself first.
69
-
70
- The round is actionable if any of these hold:
71
-
72
- - The human left a note on `.gtd/REVIEW.md`. A note is a mandatory
73
- concern below
74
- - The human added a code comment this round. A comment run whose
75
- first line starts `H:` is a THREAD: when its last entry is an
76
- `H:` question, write exactly one `A:` comment line, same token
77
- and indentation, directly below it — no fold-in; a concluded
78
- thread is folded into the requirements and its comment lines
79
- deleted. Any other comment, even a plain-prose one, is a one-shot
80
- concern — describe it, and note the comment line itself is
81
- transient: it must not survive the lap that satisfies it
82
- - The human hand-edited non-comment code this round — no longer
83
- a committed intent to build on, but a sketch like the entry
84
- commit's own diff. Describe what it was reaching for; expect
85
- the next lap to re-derive it from scratch, never call it final
86
-
87
- Not actionable only when none of the above holds — nothing but an
88
- approving remark, no code edit, no substantive note. Never invent
89
- actionability, and never dismiss a real note or edit as approval.
90
-
91
- - If actionable: write `.gtd/REQUIREMENTS.md` with an ordered
92
- list of concerns, each PRODUCT or TECHNICAL — the shape
93
- `design.triage` builds, one `## <heading>` per concern in
94
- build order. Fold every note, comment, and hand-edit in under
95
- its concern. Raise no open questions here — `design.triage`
96
- owns that later. Then finish
97
- - If not: finish without writing anything — changing nothing is
98
- the sign-off
@@ -1,62 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
- import { access } from "./access.js"
3
- import * as workflow from "../index.js"
4
-
5
- const { defaults } = workflow
6
-
7
- const bundled = access(defaults)
8
-
9
- describe("the bundled workflow's access export", () => {
10
- it("restricts writes on planning, review and lens scopes only", () => {
11
- expect(bundled).toEqual({
12
- design: { write: [] },
13
- "build.review": { write: [".gtd/REVIEW.md"] },
14
- "build.review.fix.nits": {},
15
- "build.review.fix.risks": {},
16
- "build.quality.correctness": { write: [] },
17
- "build.quality.owasp-security": { write: [] },
18
- "build.quality.ponytail-review": { write: [] },
19
- "build.quality.test-audit": { write: [] },
20
- "build.quality.conventions": { write: [] },
21
- "build.quality.spec-challenge": { write: [] },
22
- "build.quality.criteria-coverage": { write: [] },
23
- "refactor.build.review": { write: [".gtd/REVIEW.md"] },
24
- "refactor.build.quality.correctness": { write: [] },
25
- "refactor.build.quality.ponytail-review": { write: [] },
26
- "refactor.build.quality.conventions": { write: [] },
27
- })
28
- })
29
-
30
- it("leaves the refactor review's fix scopes to the refactor's own scope option", () => {
31
- expect(bundled).not.toHaveProperty(["refactor.build.review.fix.nits"])
32
- expect(bundled).not.toHaveProperty(["refactor.build.review.fix.risks"])
33
- expect(bundled["refactor.build.review"]).toEqual({ write: [".gtd/REVIEW.md"] })
34
- })
35
-
36
- it("keys one lens scope per qualityReviews entry", () => {
37
- const custom = access({ ...defaults, qualityReviews: " my-lens ,, correctness" })
38
- expect(
39
- Object.keys(custom)
40
- .filter((k) => k.startsWith("build.quality."))
41
- .sort(),
42
- ).toEqual(["build.quality.correctness", "build.quality.my-lens"])
43
- })
44
-
45
- it("keys one refactor lens scope per refactorQualityReviews entry beside the refactor review's own", () => {
46
- const custom = access({ ...defaults, refactorQualityReviews: " my-lens ,, correctness" })
47
- expect(
48
- Object.keys(custom)
49
- .filter((k) => k.startsWith("refactor."))
50
- .sort(),
51
- ).toEqual([
52
- "refactor.build.quality.correctness",
53
- "refactor.build.quality.my-lens",
54
- "refactor.build.review",
55
- ])
56
- })
57
-
58
- it("keys only turn scopes", () => {
59
- const known = Object.keys(workflow.skills(defaults))
60
- for (const key of Object.keys(bundled)) expect(known).toContain(key)
61
- })
62
- })
@@ -1,24 +0,0 @@
1
- import type { ScopeAccess } from "../../flows/index.js"
2
- import { lensesOf, lensScopes } from "../../workflow-kit/index.js"
3
-
4
- // The bundled workflow's default file access, keyed by scope full name — the
5
- // name a `config.access` entry addresses. Only planning and review scopes
6
- // are restricted, and only on writes; no scope restricts reads. `refactor`'s
7
- // write restriction lives in its own `scope()` option, not here. A step's own
8
- // steering file is folded in per step, so it is not listed here. Build and fix
9
- // scopes carry no entry: no project layout is assumed. `refactor.build.review`'s
10
- // fix scopes carry none either: `reviewedRefactor` sets their write limit at run time.
11
- export const access = (
12
- vars: Readonly<Record<string, string>>,
13
- ): Readonly<Record<string, ScopeAccess>> => ({
14
- design: { write: [] },
15
- "build.review": { write: [".gtd/REVIEW.md"] },
16
- // `{}` reopens what `build.review` restricted: the fixes edit code.
17
- "build.review.fix.nits": {},
18
- "build.review.fix.risks": {},
19
- "refactor.build.review": { write: [".gtd/REVIEW.md"] },
20
- ...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), () => ({ write: [] })),
21
- ...lensScopes("refactor.build.quality", lensesOf(vars.refactorQualityReviews ?? ""), () => ({
22
- write: [],
23
- })),
24
- })
@@ -1,12 +0,0 @@
1
- /** A `.gtd/REVIEW.md` on the fixture calculator, one hunk per note. */
2
- export const reviewDoc = (...notes: string[]): string =>
3
- [
4
- "# Review: abc1234",
5
- "",
6
- "<!-- base: 0000000 -->",
7
- "",
8
- "## calc",
9
- "",
10
- ...notes.flatMap((note, i) => [`- [ ] ./src/calc.ts#${i + 1}-${i + 1}`, ` ${note}`]),
11
- "",
12
- ].join("\n")
@@ -1,24 +0,0 @@
1
- // An unclosed fence runs to end of file. A backtick
2
- // opener with a backtick in its info string still counts as a fence: deviates from CommonMark,
3
- // worst case is `high`.
4
- export const markFenced = (text: string): readonly { line: string; fenced: boolean }[] => {
5
- let fence: string | undefined // the opener run, e.g. "````"
6
- return text.split(/\r?\n/).map((line) => {
7
- const run = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1]
8
- const fenced = fence !== undefined || run !== undefined
9
- if (fence === undefined) fence = run
10
- else if (
11
- run !== undefined &&
12
- run[0] === fence[0] &&
13
- run.length >= fence.length &&
14
- /^ {0,3}(`+|~+)[ \t]*$/.test(line)
15
- )
16
- fence = undefined
17
- return { line, fenced }
18
- })
19
- }
20
-
21
- /** A markdown heading: its `#` run, then its text. */
22
- export const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/
23
- /** A list item, its checkbox dropped: its text. */
24
- export const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/