@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +120 -395
- package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-07Z_ENch.mjs} +13503 -3460
- package/dist/chunks/{wire-C12dahrH.mjs → wire-BTUhRGXj.mjs} +1 -1
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +484 -497
- package/dist/testkit/gherkin.mjs +145 -65
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +1 -1
- package/package.json +3 -42
- package/skills/authoring/SKILL.md +34 -27
- package/src/cli/Cli.test.ts +148 -71
- package/src/cli/Cli.ts +384 -463
- package/src/cli/Help.test.ts +47 -0
- package/src/cli/Help.ts +70 -0
- package/src/cli/Program.test.ts +127 -33
- package/src/cli/Program.ts +70 -25
- package/src/edge/Edge.ts +4 -4
- package/src/emit/Emit.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/flags/index.test.ts +0 -13
- package/src/flags/index.ts +1 -3
- package/src/flows/factory.ts +3 -1
- package/src/gherkin/index.ts +135 -71
- package/src/gherkin/messages.test.ts +90 -0
- package/src/gherkin/messages.ts +84 -0
- package/src/install/Install.test.ts +14 -11
- package/src/install/Install.ts +38 -15
- package/src/install/index.ts +1 -2
- package/src/replay/Replay.test.ts +1 -1
- package/src/replay/index.ts +1 -1
- package/src/step/planStart.ts +6 -1
- package/src/testkit/TestRepo.test.ts +47 -2
- package/src/testkit/TestRepo.ts +51 -6
- package/src/twins/EmittedScriptRecognizer.ts +5 -8
- package/src/twins/FlowScripts.ts +1 -1
- package/src/wire/Land.ts +1 -1
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +138 -113
- package/src/workflow/load.ts +7 -7
- package/src/workflow/skills.test.ts +5 -5
- package/src/workflow/skills.ts +1 -1
- package/src/workflow/vars.test.ts +30 -3
- package/src/workflow/vars.ts +34 -2
- package/src/workflow-kit/index.ts +1 -1
- package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
- package/dist/chunks/review-Bfaror38.mjs +0 -427
- package/dist/chunks/tail-Bd7L940p.mjs +0 -355
- package/dist/chunks/testkit-JDtpNKyi.mjs +0 -10410
- package/dist/workflows/cleanup.mjs +0 -36
- package/dist/workflows/feature.mjs +0 -1018
- package/dist/workflows/fix.mjs +0 -41
- package/dist/workflows/review.mjs +0 -45
- package/dist/workflows/rollout.mjs +0 -48
- package/dist/workflows.mjs +0 -40651
- package/src/install/ConfigScaffold.ts +0 -22
- package/src/workflows/cleanup/cleanup-path.feature +0 -94
- package/src/workflows/cleanup/cleanup.feature +0 -315
- package/src/workflows/cleanup/cleanup.test.ts +0 -276
- package/src/workflows/cleanup/cleanup.ts +0 -52
- package/src/workflows/cleanup/index.ts +0 -6
- package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
- package/src/workflows/feature/acceptance-criteria.feature +0 -389
- package/src/workflows/feature/criteria-coverage.feature +0 -102
- package/src/workflows/feature/criteria.test.ts +0 -147
- package/src/workflows/feature/criteria.ts +0 -267
- package/src/workflows/feature/fast-path.feature +0 -93
- package/src/workflows/feature/feature.test.ts +0 -338
- package/src/workflows/feature/feature.ts +0 -156
- package/src/workflows/feature/green.ts +0 -19
- package/src/workflows/feature/index.ts +0 -18
- package/src/workflows/feature/lens-models.feature +0 -69
- package/src/workflows/feature/lens-review.feature +0 -566
- package/src/workflows/feature/naive-acceptance.feature +0 -147
- package/src/workflows/feature/naive-test.feature +0 -263
- package/src/workflows/feature/one-step-land.feature +0 -93
- package/src/workflows/feature/planning.ts +0 -41
- package/src/workflows/feature/refactor-absorbs.feature +0 -131
- package/src/workflows/feature/refactor-first.feature +0 -158
- package/src/workflows/feature/refactor-lens-models.feature +0 -79
- package/src/workflows/feature/refactor-rebuild.feature +0 -273
- package/src/workflows/feature/refactor-review.feature +0 -1528
- package/src/workflows/feature/refactor.test.ts +0 -1220
- package/src/workflows/feature/refactor.ts +0 -368
- package/src/workflows/feature/release-flag.feature +0 -167
- package/src/workflows/feature/release-flag.test.ts +0 -49
- package/src/workflows/feature/release-flag.ts +0 -46
- package/src/workflows/feature/review-gate.feature +0 -241
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
- package/src/workflows/feature/risk-tier.feature +0 -351
- package/src/workflows/feature/seam-rejection.feature +0 -146
- package/src/workflows/feature/seam.feature +0 -297
- package/src/workflows/feature/seam.test.ts +0 -527
- package/src/workflows/feature/seam.ts +0 -426
- package/src/workflows/feature/standards.feature +0 -354
- package/src/workflows/feature/steps.test.ts +0 -65
- package/src/workflows/feature/steps.ts +0 -45
- package/src/workflows/feature/tier-pin.feature +0 -66
- package/src/workflows/feature/tier.test.ts +0 -166
- package/src/workflows/feature/tier.ts +0 -56
- package/src/workflows/fix/fix-lens-origin.feature +0 -114
- package/src/workflows/fix/fix-quality.feature +0 -212
- package/src/workflows/fix/fix.feature +0 -79
- package/src/workflows/fix/fix.test.ts +0 -74
- package/src/workflows/fix/fix.ts +0 -52
- package/src/workflows/fix/index.ts +0 -6
- package/src/workflows/index.ts +0 -47
- package/src/workflows/review/index.ts +0 -6
- package/src/workflows/review/review-door.feature +0 -199
- package/src/workflows/review/review.test.ts +0 -27
- package/src/workflows/review/review.ts +0 -60
- package/src/workflows/rollout/index.ts +0 -6
- package/src/workflows/rollout/rollout.feature +0 -510
- package/src/workflows/rollout/rollout.test.ts +0 -263
- package/src/workflows/rollout/rollout.ts +0 -82
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
- package/src/workflows/shared/access.test.ts +0 -62
- package/src/workflows/shared/access.ts +0 -24
- package/src/workflows/shared/config.fixture.ts +0 -12
- package/src/workflows/shared/fences.ts +0 -24
- package/src/workflows/shared/health.test.ts +0 -148
- package/src/workflows/shared/health.ts +0 -191
- package/src/workflows/shared/learn.test.ts +0 -140
- package/src/workflows/shared/learn.ts +0 -201
- package/src/workflows/shared/lenses.test.ts +0 -39
- package/src/workflows/shared/lenses.ts +0 -294
- package/src/workflows/shared/options.test.ts +0 -157
- package/src/workflows/shared/options.ts +0 -92
- package/src/workflows/shared/progress.feature +0 -124
- package/src/workflows/shared/prose.ts +0 -263
- package/src/workflows/shared/recheck.feature +0 -60
- package/src/workflows/shared/registry.test.ts +0 -345
- package/src/workflows/shared/registry.ts +0 -183
- package/src/workflows/shared/reserved.ts +0 -11
- package/src/workflows/shared/review.test.ts +0 -1062
- package/src/workflows/shared/review.ts +0 -446
- package/src/workflows/shared/skills.test.ts +0 -121
- package/src/workflows/shared/skills.ts +0 -49
- package/src/workflows/shared/standards.test.ts +0 -90
- package/src/workflows/shared/standards.ts +0 -156
- package/src/workflows/shared/steps.test.ts +0 -219
- package/src/workflows/shared/steps.ts +0 -201
- package/src/workflows/shared/tail.test.ts +0 -454
- package/src/workflows/shared/tail.ts +0 -264
- package/src/workflows/shared/text.fixture.ts +0 -128
- package/src/workflows/shared/text.test.ts +0 -1430
- package/src/workflows/shared/text.ts +0 -1700
- package/src/workflows/shared/threads.test.ts +0 -129
- package/src/workflows/shared/vars.ts +0 -47
- 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+)?(.*)$/
|