@pmelab/gtd 29.3.0 → 30.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +65 -71
- package/claude/hooks/ship.ts +36 -25
- package/dist/chunks/{reserved-Dmpg6dAp.mjs → reserved-iG86IR3I.mjs} +19 -233
- package/dist/chunks/{tail-CLyE5VFR.mjs → tail-DA2UuJNP.mjs} +1 -1
- package/dist/gtd.bundle.mjs +0 -5
- package/dist/testkit.mjs +0 -5
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +14 -180
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +29 -409
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +15 -27
- package/src/cli/Cli.test.ts +6 -6
- package/src/edge/Edge.test.ts +7 -7
- package/src/flags/index.test.ts +0 -4
- package/src/flags/index.ts +0 -1
- package/src/workflow/load.test.ts +1 -1
- package/src/workflows/feature/fast-path.feature +1 -2
- package/src/workflows/feature/feature.test.ts +74 -63
- package/src/workflows/feature/feature.ts +13 -35
- package/src/workflows/feature/index.ts +0 -1
- package/src/workflows/feature/one-step-land.feature +4 -6
- package/src/workflows/feature/planning.ts +2 -27
- package/src/workflows/feature/refactor-absorbs.feature +1 -2
- package/src/workflows/feature/refactor-first.feature +1 -3
- package/src/workflows/feature/refactor-review.feature +0 -1
- package/src/workflows/feature/risk-tier.feature +2 -3
- package/src/workflows/feature/seam-rejection.feature +1 -3
- package/src/workflows/feature/seam.feature +4 -6
- package/src/workflows/feature/steps.test.ts +0 -20
- package/src/workflows/feature/steps.ts +1 -49
- package/src/workflows/feature/{seam-pin.feature → tier-pin.feature} +17 -11
- package/src/workflows/feature/tier.test.ts +3 -5
- package/src/workflows/feature/tier.ts +2 -2
- package/src/workflows/index.ts +0 -1
- package/src/workflows/shared/access.test.ts +0 -2
- package/src/workflows/shared/access.ts +0 -2
- package/src/workflows/shared/options.test.ts +2 -3
- package/src/workflows/shared/prose.ts +4 -11
- package/src/workflows/shared/reserved.ts +2 -2
- package/src/workflows/shared/skills.test.ts +0 -11
- package/src/workflows/shared/skills.ts +2 -9
- package/src/workflows/shared/steps.ts +0 -1
- package/src/workflows/shared/text.test.ts +4 -104
- package/src/workflows/shared/text.ts +15 -223
- package/src/workflows/shared/vars.ts +1 -3
- package/src/workflows/feature/packages.test.ts +0 -368
- package/src/workflows/feature/packages.ts +0 -173
|
@@ -74,7 +74,7 @@ current directory is the whole workflow. If one already exists, read it and edit
|
|
|
74
74
|
it in place.
|
|
75
75
|
|
|
76
76
|
Prefer the bundled workflow's own parts over re-implementing them — `healthy`,
|
|
77
|
-
`escalation`, `gate`, `design`, `
|
|
77
|
+
`escalation`, `gate`, `design`, `seamBuild`, `implement`, `qualityLap`,
|
|
78
78
|
`buildTail`, and single steps like `triage` or `fixCheck`. Their full step names
|
|
79
79
|
are versioned API.
|
|
80
80
|
|
|
@@ -222,41 +222,29 @@ workflow.
|
|
|
222
222
|
|
|
223
223
|
## Worked example: add an approval gate before building
|
|
224
224
|
|
|
225
|
-
In the bundled default, `planAndBuild` runs the design phase,
|
|
226
|
-
pass
|
|
227
|
-
sign-off
|
|
225
|
+
In the bundled default, `planAndBuild` runs the design phase, then
|
|
226
|
+
`buildFeature`: the naive pass and its seam judge, then the implementation and
|
|
227
|
+
review. Add a human sign-off before the first build (a review feedback lap
|
|
228
|
+
re-runs design inside `buildFeature` and does not pass it again):
|
|
228
229
|
|
|
229
230
|
```ts
|
|
230
231
|
const planAndBuild = async (firstBase: string): Promise<void> => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
label: "Approve the plan",
|
|
240
|
-
acceptClean: true,
|
|
241
|
-
})
|
|
242
|
-
await packages()
|
|
243
|
-
const outcome = await buildTail(base, {
|
|
244
|
-
green: true,
|
|
245
|
-
...(lap === "first" ? {} : { lap }),
|
|
246
|
-
})
|
|
247
|
-
if (outcome.verdict === "signoff") return
|
|
248
|
-
await reUnwind(outcome)
|
|
249
|
-
base = outcome.base
|
|
250
|
-
lap = "feedback"
|
|
251
|
-
}
|
|
232
|
+
await design(firstBase)
|
|
233
|
+
await human("approve-plan", {
|
|
234
|
+
message:
|
|
235
|
+
"`.gtd/REQUIREMENTS.md` is settled. Edit it to adjust the plan, or change nothing — then run `gtd land` to start building.",
|
|
236
|
+
label: "Approve the plan",
|
|
237
|
+
acceptClean: true,
|
|
238
|
+
})
|
|
239
|
+
await buildFeature(firstBase)
|
|
252
240
|
}
|
|
253
241
|
```
|
|
254
242
|
|
|
255
243
|
`acceptClean: true` is what makes an untouched landing approve; without it the
|
|
256
244
|
gate would wait for an edit. `approve-plan` sits in the `root` scope and is a
|
|
257
245
|
new, unique name. Verify: `gtd next` loads without errors, and in a scratch
|
|
258
|
-
repository a landing at `
|
|
259
|
-
|
|
246
|
+
repository a landing at `design.gate.answer` now leads to `approve-plan`, and an
|
|
247
|
+
untouched landing there to `seam.naive`.
|
|
260
248
|
|
|
261
249
|
## No migration
|
|
262
250
|
|
package/src/cli/Cli.test.ts
CHANGED
|
@@ -1327,7 +1327,7 @@ export const side = async () => {
|
|
|
1327
1327
|
await agent("working", "w")
|
|
1328
1328
|
}
|
|
1329
1329
|
|
|
1330
|
-
export const defaults = {
|
|
1330
|
+
export const defaults = { flag_risk_tier: "0" }
|
|
1331
1331
|
`
|
|
1332
1332
|
const start = async (
|
|
1333
1333
|
flags?: CliIo["flags"],
|
|
@@ -1343,24 +1343,24 @@ export const defaults = { flag_naive_pass: "0" }
|
|
|
1343
1343
|
return result().stdout
|
|
1344
1344
|
}
|
|
1345
1345
|
|
|
1346
|
-
it("pins
|
|
1347
|
-
expect(await start(undefined, {
|
|
1346
|
+
it("pins flag_risk_tier=1 with the flag on", async () => {
|
|
1347
|
+
expect(await start(undefined, { GTD_FLAG_RISK_TIER: "1" })).toContain("flag_risk_tier=1")
|
|
1348
1348
|
})
|
|
1349
1349
|
|
|
1350
1350
|
it("honours defaultOn with GTD_FLAG_* unset", async () => {
|
|
1351
1351
|
const out = await start(
|
|
1352
1352
|
[
|
|
1353
1353
|
{
|
|
1354
|
-
name: "
|
|
1354
|
+
name: "risk_tier",
|
|
1355
1355
|
owner: "pmelab",
|
|
1356
1356
|
expiry: "2099-01-01",
|
|
1357
1357
|
kind: "release",
|
|
1358
1358
|
defaultOn: true,
|
|
1359
1359
|
},
|
|
1360
1360
|
],
|
|
1361
|
-
{
|
|
1361
|
+
{ GTD_FLAG_RISK_TIER: "" },
|
|
1362
1362
|
)
|
|
1363
|
-
expect(out).toContain("
|
|
1363
|
+
expect(out).toContain("flag_risk_tier=1")
|
|
1364
1364
|
})
|
|
1365
1365
|
})
|
|
1366
1366
|
|
package/src/edge/Edge.test.ts
CHANGED
|
@@ -1272,7 +1272,7 @@ export const side = async () => {
|
|
|
1272
1272
|
await agent("working", "w")
|
|
1273
1273
|
}
|
|
1274
1274
|
|
|
1275
|
-
export const defaults = {
|
|
1275
|
+
export const defaults = { flag_risk_tier: "0", other: "x" }
|
|
1276
1276
|
`
|
|
1277
1277
|
const withFlags = <A>(
|
|
1278
1278
|
eff: Effect.Effect<A, Error, RestRequirements>,
|
|
@@ -1285,23 +1285,23 @@ export const defaults = { flag_naive_pass: "0", other: "x" }
|
|
|
1285
1285
|
|
|
1286
1286
|
it("a starting process takes the declared flag from StartFlags, never an undeclared one", async () => {
|
|
1287
1287
|
const rest = await withFlags(currentRest, repoWith(FLAGGED), {
|
|
1288
|
-
|
|
1288
|
+
flag_risk_tier: "1",
|
|
1289
1289
|
flag_other: "1",
|
|
1290
1290
|
})
|
|
1291
|
-
expect(rest.vars).toEqual({
|
|
1291
|
+
expect(rest.vars).toEqual({ flag_risk_tier: "1", other: "x" })
|
|
1292
1292
|
})
|
|
1293
1293
|
|
|
1294
1294
|
it("a running process missing the pin resolves the declared default, never the live flag", async () => {
|
|
1295
|
-
const repo = repoWith(FLAGGED.replace('
|
|
1295
|
+
const repo = repoWith(FLAGGED.replace('flag_risk_tier: "0", ', ""))
|
|
1296
1296
|
await enter(repo, "side")
|
|
1297
1297
|
// a process started before the flag existed: its opening commit lacks the pin
|
|
1298
1298
|
repo.writeFile("gtd.config.ts", FLAGGED)
|
|
1299
|
-
const rest = await withFlags(currentRest, repo, {
|
|
1300
|
-
expect(rest.vars.
|
|
1299
|
+
const rest = await withFlags(currentRest, repo, { flag_risk_tier: "1" })
|
|
1300
|
+
expect(rest.vars.flag_risk_tier).toBe("0")
|
|
1301
1301
|
})
|
|
1302
1302
|
|
|
1303
1303
|
it("with no StartFlags provided, resolves the declared default", async () => {
|
|
1304
|
-
expect((await provide(currentRest, repoWith(FLAGGED))).vars.
|
|
1304
|
+
expect((await provide(currentRest, repoWith(FLAGGED))).vars.flag_risk_tier).toBe("0")
|
|
1305
1305
|
})
|
|
1306
1306
|
})
|
|
1307
1307
|
|
package/src/flags/index.test.ts
CHANGED
package/src/flags/index.ts
CHANGED
|
@@ -14,7 +14,6 @@ export interface FlagDeclaration {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export const FLAGS = [
|
|
17
|
-
{ name: "naive_pass", owner: "pmelab", expiry: "2027-01-15", kind: "release" },
|
|
18
17
|
{ name: "risk_tier", owner: "pmelab", expiry: "2027-01-15", kind: "release" },
|
|
19
18
|
] as const satisfies readonly FlagDeclaration[]
|
|
20
19
|
|
|
@@ -853,7 +853,7 @@ describe("ConfigService", () => {
|
|
|
853
853
|
it("names the key and lists every known scope", async () => {
|
|
854
854
|
const message = await loadError(keyed("nope"))
|
|
855
855
|
expect(message).toContain('"skills.nope" is not a scope that runs a turn — known scopes: ')
|
|
856
|
-
expect(message).toContain("
|
|
856
|
+
expect(message).toContain("known scopes: build, build.fix.quality,")
|
|
857
857
|
})
|
|
858
858
|
|
|
859
859
|
it("gives a group key the same plain error", async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@inmem
|
|
2
2
|
Feature: a trivial change takes the same few beats at either risk tier, flag-free at low
|
|
3
3
|
|
|
4
|
-
With
|
|
4
|
+
With GTD_FLAG_RISK_TIER on, a one-line copy fix goes
|
|
5
5
|
from idle through the naive pass, the seam judge's land, implement and human
|
|
6
6
|
review back to idle. Low tier walks exactly the steps high does; only the
|
|
7
7
|
implementation prompt and the summary differ.
|
|
@@ -9,7 +9,6 @@ Feature: a trivial change takes the same few beats at either risk tier, flag-fre
|
|
|
9
9
|
Background:
|
|
10
10
|
Given the shipped workflows, as gtd install scaffolds them
|
|
11
11
|
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
|
|
12
|
-
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
13
12
|
And an environment variable "GTD_FLAG_RISK_TIER" set to "1"
|
|
14
13
|
And a file "src/greeting.ts" with:
|
|
15
14
|
"""
|
|
@@ -47,13 +47,12 @@ const review = (text?: string): string =>
|
|
|
47
47
|
|
|
48
48
|
const CALC = "export const add = (a: number, b: number) => a + b\n"
|
|
49
49
|
|
|
50
|
-
const naivePlanned = (
|
|
50
|
+
const naivePlanned = () => {
|
|
51
51
|
const repo = testRepo({
|
|
52
52
|
workflow: CONFIG_SCAFFOLD,
|
|
53
53
|
env: {
|
|
54
54
|
GTD_QUALITYREVIEWS: "",
|
|
55
55
|
GTD_REFACTORQUALITYREVIEWS: "",
|
|
56
|
-
GTD_FLAG_NAIVE_PASS: flag,
|
|
57
56
|
GTD_FLAG_RISK_TIER: "0",
|
|
58
57
|
},
|
|
59
58
|
})
|
|
@@ -63,9 +62,9 @@ const naivePlanned = (flag: "0" | "1") => {
|
|
|
63
62
|
return repo
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
describe("planAndBuild
|
|
65
|
+
describe("planAndBuild", () => {
|
|
67
66
|
it("a refactor that carried the whole feature: the empty implement turn passes and the review covers the whole change", async () => {
|
|
68
|
-
const repo = naivePlanned(
|
|
67
|
+
const repo = naivePlanned()
|
|
69
68
|
const SUBTRACT = `${CALC}export const subtract = (a: number, b: number) => a - b\n`
|
|
70
69
|
expect(
|
|
71
70
|
(
|
|
@@ -99,7 +98,7 @@ describe("planAndBuild on the naive pass", () => {
|
|
|
99
98
|
})
|
|
100
99
|
|
|
101
100
|
it("after land, feedback rests next at design.triage, then implement.building, never seam.naive", async () => {
|
|
102
|
-
const repo = naivePlanned(
|
|
101
|
+
const repo = naivePlanned()
|
|
103
102
|
const requirements = (_: unknown, r: typeof repo) =>
|
|
104
103
|
r.write(
|
|
105
104
|
".gtd/REQUIREMENTS.md",
|
|
@@ -149,20 +148,6 @@ describe("planAndBuild on the naive pass", () => {
|
|
|
149
148
|
})
|
|
150
149
|
})
|
|
151
150
|
|
|
152
|
-
describe("planAndBuild without the naive pass", () => {
|
|
153
|
-
it("runs the design → architecture path: no seam, no implement", async () => {
|
|
154
|
-
const repo = naivePlanned("0")
|
|
155
|
-
const rest = await repo.drive({
|
|
156
|
-
unwind: () => undefined,
|
|
157
|
-
"start-gate.check": () => undefined,
|
|
158
|
-
"design.triage": (_, r) => r.write(".gtd/REQUIREMENTS.md", "# Requirements\n"),
|
|
159
|
-
"design.gate.answer": () => undefined,
|
|
160
|
-
})
|
|
161
|
-
expect(rest.state).toBe("architecture.author")
|
|
162
|
-
expect(repo.subjects().some((subject) => /seam|implement/.test(subject))).toBe(false)
|
|
163
|
-
})
|
|
164
|
-
})
|
|
165
|
-
|
|
166
151
|
describe("afterTail feedback lap", () => {
|
|
167
152
|
const TAIL = [
|
|
168
153
|
`import { start } from "@pmelab/gtd/flows"`,
|
|
@@ -176,14 +161,13 @@ describe("afterTail feedback lap", () => {
|
|
|
176
161
|
|
|
177
162
|
const prompts: string[] = []
|
|
178
163
|
|
|
179
|
-
const feedbackLap = async (
|
|
164
|
+
const feedbackLap = async () => {
|
|
180
165
|
prompts.length = 0
|
|
181
166
|
const repo = testRepo({
|
|
182
167
|
workflow: TAIL,
|
|
183
168
|
env: {
|
|
184
169
|
GTD_QUALITYREVIEWS: "",
|
|
185
170
|
GTD_REFACTORQUALITYREVIEWS: "",
|
|
186
|
-
GTD_FLAG_NAIVE_PASS: flag,
|
|
187
171
|
GTD_FLAG_RISK_TIER: "0",
|
|
188
172
|
},
|
|
189
173
|
})
|
|
@@ -219,8 +203,8 @@ describe("afterTail feedback lap", () => {
|
|
|
219
203
|
"design.gate.answer": () => undefined,
|
|
220
204
|
}
|
|
221
205
|
|
|
222
|
-
it("
|
|
223
|
-
const repo = await feedbackLap(
|
|
206
|
+
it("rests at design.triage, then implement.building, reaching no seam step", async () => {
|
|
207
|
+
const repo = await feedbackLap()
|
|
224
208
|
expect(
|
|
225
209
|
(await repo.drive({ "build.review.closing": runsItsScript, "re-unwind": runsItsScript }))
|
|
226
210
|
.state,
|
|
@@ -232,46 +216,30 @@ describe("afterTail feedback lap", () => {
|
|
|
232
216
|
expect(repo.subjects().some((subject) => subject.includes("seam."))).toBe(false)
|
|
233
217
|
})
|
|
234
218
|
|
|
235
|
-
it("
|
|
236
|
-
const repo = await feedbackLap(
|
|
237
|
-
|
|
238
|
-
expect(
|
|
219
|
+
it("the first review says everything since base, the one after feedback only what is new", async () => {
|
|
220
|
+
const repo = await feedbackLap()
|
|
221
|
+
expect(prompts[0]).toContain("it covers everything since the base")
|
|
222
|
+
expect(prompts[0]).not.toContain("only what is new since the previous review")
|
|
223
|
+
const writes: Record<string, () => [string, string]> = {
|
|
224
|
+
"design.triage": () => [".gtd/REQUIREMENTS.md", "# Requirements\n\n- multiply, designed\n"],
|
|
225
|
+
"implement.building": () => ["src/calc.ts", `${CALC}// again\n`],
|
|
226
|
+
}
|
|
227
|
+
const actors = new Proxy({} as Record<string, Actor>, {
|
|
228
|
+
get: (_, state: string): Actor | undefined =>
|
|
229
|
+
state === "build.review.await-review" || state === "idle"
|
|
230
|
+
? undefined
|
|
231
|
+
: (beat, r) => {
|
|
232
|
+
if (state === "build.review.reviewing") {
|
|
233
|
+
prompts.push(beat.content)
|
|
234
|
+
r.write(".gtd/REVIEW.md", review())
|
|
235
|
+
} else if (writes[state]) r.write(...writes[state]())
|
|
236
|
+
else if (state.endsWith("closing") || state === "re-unwind") r.runScript(beat.content)
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
await repo.drive(actors, { maxBeats: 80 })
|
|
240
|
+
expect(prompts[1]).toContain("it covers only what is new since the previous review")
|
|
241
|
+
expect(prompts[1]).not.toContain("everything since the base")
|
|
239
242
|
})
|
|
240
|
-
|
|
241
|
-
it.each(["0", "1"] as const)(
|
|
242
|
-
"flag_naive_pass=%s: the first review says everything since base, the one after feedback only what is new",
|
|
243
|
-
async (flag) => {
|
|
244
|
-
const repo = await feedbackLap(flag)
|
|
245
|
-
expect(prompts[0]).toContain("it covers everything since the base")
|
|
246
|
-
expect(prompts[0]).not.toContain("only what is new since the previous review")
|
|
247
|
-
const writes: Record<string, () => [string, string]> = {
|
|
248
|
-
"design.triage": () => [".gtd/REQUIREMENTS.md", "# Requirements\n\n- multiply, designed\n"],
|
|
249
|
-
"implement.building": () => ["src/calc.ts", `${CALC}// again\n`],
|
|
250
|
-
"packages.item.building": () => ["src/calc.ts", `${CALC}// package\n`],
|
|
251
|
-
"architecture.author": () => [".gtd/ARCHITECTURE.md", "# Architecture\n\nmultiply\n"],
|
|
252
|
-
"architecture.decompose.decomposing": () => [
|
|
253
|
-
".gtd/packages/01-multiply.md",
|
|
254
|
-
"# 01 multiply\n",
|
|
255
|
-
],
|
|
256
|
-
}
|
|
257
|
-
const actors = new Proxy({} as Record<string, Actor>, {
|
|
258
|
-
get: (_, state: string): Actor | undefined =>
|
|
259
|
-
state === "build.review.await-review" || state === "idle"
|
|
260
|
-
? undefined
|
|
261
|
-
: (beat, r) => {
|
|
262
|
-
if (state === "build.review.reviewing") {
|
|
263
|
-
prompts.push(beat.content)
|
|
264
|
-
r.write(".gtd/REVIEW.md", review())
|
|
265
|
-
} else if (writes[state]) r.write(...writes[state]())
|
|
266
|
-
else if (state.endsWith("closing") || state === "re-unwind")
|
|
267
|
-
r.runScript(beat.content)
|
|
268
|
-
},
|
|
269
|
-
})
|
|
270
|
-
await repo.drive(actors, { maxBeats: 80 })
|
|
271
|
-
expect(prompts[1]).toContain("it covers only what is new since the previous review")
|
|
272
|
-
expect(prompts[1]).not.toContain("everything since the base")
|
|
273
|
-
},
|
|
274
|
-
)
|
|
275
243
|
})
|
|
276
244
|
|
|
277
245
|
describe("an in-flight 28.0.0 naive-pass process", () => {
|
|
@@ -340,7 +308,7 @@ describe("an in-flight 28.0.0 naive-pass process", () => {
|
|
|
340
308
|
|
|
341
309
|
/** The repo at the judge's rest, then `trail` forged edge by edge as `Gtd-Step:` commits. */
|
|
342
310
|
const forged = async (trail: readonly Edge[], upTo: number) => {
|
|
343
|
-
const repo = naivePlanned(
|
|
311
|
+
const repo = naivePlanned()
|
|
344
312
|
await repo.drive({
|
|
345
313
|
unwind: () => undefined,
|
|
346
314
|
"start-gate.check": () => undefined,
|
|
@@ -386,3 +354,46 @@ describe("an in-flight 28.0.0 naive-pass process", () => {
|
|
|
386
354
|
})
|
|
387
355
|
}
|
|
388
356
|
})
|
|
357
|
+
|
|
358
|
+
describe("an in-flight process from before the architecture path was removed", () => {
|
|
359
|
+
const REQUIREMENTS = "# Requirements\n\n## Concerns\n\n### Subtract\n\n- `subtract`\n"
|
|
360
|
+
|
|
361
|
+
/** Its history up to the design gate, forged with the old opening commit's `flag_naive_pass=0`. */
|
|
362
|
+
const atDesignGate = async () => {
|
|
363
|
+
const live = naivePlanned()
|
|
364
|
+
await live.drive({ unwind: () => undefined, "start-gate.check": () => undefined })
|
|
365
|
+
const opening = live.messages().find((m) => m.startsWith("gtd(human): idle → unwind"))!
|
|
366
|
+
const repo = naivePlanned()
|
|
367
|
+
repo.commit(
|
|
368
|
+
opening.replace(
|
|
369
|
+
"Gtd-Var: flag_risk_tier=0",
|
|
370
|
+
"Gtd-Var: flag_naive_pass=0\nGtd-Var: flag_risk_tier=0",
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
repo.remove("TODO.md")
|
|
374
|
+
repo.commit("gtd(check): unwind → start-gate.check\n\nGtd-Step: unwind#1\nGtd-Format: 1")
|
|
375
|
+
repo.commit(
|
|
376
|
+
"gtd(check): start-gate.check → design.triage\n\nGtd-Step: start-gate.check#1\nGtd-Format: 1",
|
|
377
|
+
)
|
|
378
|
+
repo.write(".gtd/REQUIREMENTS.md", REQUIREMENTS)
|
|
379
|
+
repo.commit(
|
|
380
|
+
"gtd(agent): design.triage → design.gate.answer\n\nGtd-Step: design.triage#1\nGtd-Format: 1",
|
|
381
|
+
)
|
|
382
|
+
return repo
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
it("resting at the design gate, replays onto the naive pass", async () => {
|
|
386
|
+
const repo = await atDesignGate()
|
|
387
|
+
expect((await repo.next()).state).toBe("design.gate.answer")
|
|
388
|
+
await repo.land()
|
|
389
|
+
expect((await repo.next()).state).toBe("seam.naive")
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it("past the design gate on the architecture path, fails replay", async () => {
|
|
393
|
+
const repo = await atDesignGate()
|
|
394
|
+
repo.commit(
|
|
395
|
+
"gtd(human): design.gate.answer → architecture.author\n\nGtd-Step: design.gate.answer#1\nGtd-Format: 1",
|
|
396
|
+
)
|
|
397
|
+
await expect(repo.next()).rejects.toThrow("the workflow changed under this process")
|
|
398
|
+
})
|
|
399
|
+
})
|
|
@@ -7,24 +7,22 @@ import {
|
|
|
7
7
|
run,
|
|
8
8
|
slice,
|
|
9
9
|
start,
|
|
10
|
-
vars,
|
|
11
10
|
} from "../../flows/index.js"
|
|
12
11
|
import { gate } from "../shared/health.js"
|
|
13
12
|
import { shipped } from "../shared/options.js"
|
|
14
13
|
import { FEEDBACK } from "../shared/steps.js"
|
|
15
14
|
import { refusePendingHandOff } from "../shared/tail.js"
|
|
16
15
|
import * as t from "../shared/text.js"
|
|
17
|
-
import {
|
|
18
|
-
import { architecture, design } from "./planning.js"
|
|
16
|
+
import { design } from "./planning.js"
|
|
19
17
|
import { buildTail, isCode, reUnwind, type ReviewOutcome } from "./review.js"
|
|
20
18
|
import { implement, seamBuild } from "./seam.js"
|
|
21
19
|
import { recordRisk, riskTierOn } from "./tier.js"
|
|
22
20
|
|
|
23
21
|
// The ordinary start. Any change to the tree starts a process: `idle` →
|
|
24
22
|
// `unwind` reverts the sketch (its intent survives in history) → a
|
|
25
|
-
// green-baseline gate → design
|
|
26
|
-
// the quality lap → human review, which signs
|
|
27
|
-
// `idle`) or sends a
|
|
23
|
+
// green-baseline gate → design → a judged naive pass (refactoring first when
|
|
24
|
+
// the judge asks) → implement → the quality lap → human review, which signs
|
|
25
|
+
// off (the episode ends back at `idle`) or sends a design → implement lap.
|
|
28
26
|
|
|
29
27
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
30
28
|
export const unwind = (): Promise<void> => {
|
|
@@ -36,31 +34,10 @@ export const unwind = (): Promise<void> => {
|
|
|
36
34
|
|
|
37
35
|
export { reUnwind }
|
|
38
36
|
|
|
39
|
-
/** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
|
|
40
|
-
export const planAndBuild = async (
|
|
41
|
-
firstBase: string,
|
|
42
|
-
firstLap: t.ReviewRange = "first",
|
|
43
|
-
): Promise<void> => {
|
|
44
|
-
// GTD_FLAG_NAIVE_PASS: the record is pinned at the process start, so a mid-process flip never moves a process.
|
|
45
|
-
if (vars.flag_naive_pass === "1") return naivePlanAndBuild(firstBase)
|
|
46
|
-
let base = firstBase
|
|
47
|
-
let lap = firstLap
|
|
48
|
-
for (;;) {
|
|
49
|
-
await design(base)
|
|
50
|
-
await architecture()
|
|
51
|
-
await packages()
|
|
52
|
-
const outcome = await buildTail(base, { green: true, lap })
|
|
53
|
-
if (outcome.verdict === "signoff") return
|
|
54
|
-
await reUnwind(outcome)
|
|
55
|
-
base = outcome.base
|
|
56
|
-
lap = "feedback"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
37
|
// seam.naive / seam.judge run once: a feedback lap is design → implement on every verdict, so a
|
|
61
38
|
// seam a re-plan turns up is built unjudged and unrefactored.
|
|
62
|
-
|
|
63
|
-
|
|
39
|
+
/** A judged naive pass, then implement and review until a review round signs off; design must have run since `firstBase`. */
|
|
40
|
+
export const buildFeature = async (firstBase: string): Promise<void> => {
|
|
64
41
|
const tier = riskTierOn() ? recordRisk() : undefined
|
|
65
42
|
const seam = await seamBuild(firstBase)
|
|
66
43
|
await slice("feature", async () => {
|
|
@@ -74,6 +51,12 @@ const naivePlanAndBuild = async (firstBase: string): Promise<void> => {
|
|
|
74
51
|
})
|
|
75
52
|
}
|
|
76
53
|
|
|
54
|
+
/** Design, then `buildFeature`. */
|
|
55
|
+
export const planAndBuild = async (firstBase: string): Promise<void> => {
|
|
56
|
+
await design(firstBase)
|
|
57
|
+
await buildFeature(firstBase)
|
|
58
|
+
}
|
|
59
|
+
|
|
77
60
|
const featureFeedback = async (first: ReviewOutcome, lensBase?: string): Promise<void> => {
|
|
78
61
|
let outcome = first
|
|
79
62
|
while (outcome.verdict === "feedback") {
|
|
@@ -86,12 +69,7 @@ const featureFeedback = async (first: ReviewOutcome, lensBase?: string): Promise
|
|
|
86
69
|
}
|
|
87
70
|
|
|
88
71
|
/** After a tail that skipped planning: stop on sign-off, re-plan on feedback. Opens no slice: slices cannot nest. */
|
|
89
|
-
export const afterTail =
|
|
90
|
-
if (outcome.verdict === "signoff") return
|
|
91
|
-
if (vars.flag_naive_pass === "1") return featureFeedback(outcome)
|
|
92
|
-
await reUnwind(outcome)
|
|
93
|
-
await planAndBuild(outcome.base, "feedback")
|
|
94
|
-
}
|
|
72
|
+
export const afterTail = (outcome: ReviewOutcome): Promise<void> => featureFeedback(outcome)
|
|
95
73
|
|
|
96
74
|
/** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
|
|
97
75
|
export const ordinaryStart = async (): Promise<void> => {
|
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
@inmem
|
|
3
3
|
Feature: a naive pass the judge rules land goes straight to implement with no seam gate
|
|
4
4
|
|
|
5
|
-
With
|
|
5
|
+
With GTD_FLAG_RISK_TIER off, a naive pass that
|
|
6
6
|
edits an existing source file is judged land: the process skips the approval
|
|
7
|
-
gate
|
|
8
|
-
review back to idle. The
|
|
7
|
+
gate and any refactor, and runs implement, quality and
|
|
8
|
+
review back to idle. The flag is pinned here so the per-flag test runs never
|
|
9
9
|
move it off this path.
|
|
10
10
|
|
|
11
11
|
Background:
|
|
12
12
|
Given the shipped workflows, as gtd install scaffolds them
|
|
13
13
|
And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
|
|
14
|
-
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
15
14
|
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
16
15
|
And a file "src/greeting.ts" with:
|
|
17
16
|
"""
|
|
@@ -65,7 +64,7 @@ Feature: a naive pass the judge rules land goes straight to implement with no se
|
|
|
65
64
|
"""
|
|
66
65
|
And step "build.review.closing" runs its script
|
|
67
66
|
|
|
68
|
-
Scenario: a land judgment runs idle to idle with no seam gate
|
|
67
|
+
Scenario: a land judgment runs idle to idle with no seam gate or refactor
|
|
69
68
|
When the process is driven
|
|
70
69
|
Then the process rests at "build.review.await-review"
|
|
71
70
|
Given a file ".gtd/REVIEW.md" with:
|
|
@@ -100,7 +99,6 @@ Feature: a naive pass the judge rules land goes straight to implement with no se
|
|
|
100
99
|
gtd(human): build.review.await-review → build.review.closing
|
|
101
100
|
gtd(check): build.review.closing → idle
|
|
102
101
|
"""
|
|
103
|
-
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_naive_pass=1"
|
|
104
102
|
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
|
|
105
103
|
And the commit "gtd(agent): seam.naive → seam.judge" has a body containing "Gtd-Kept:"
|
|
106
104
|
And the commit "gtd(judge): seam.judge → implement.building" has a body containing "\"answer\":\"land\""
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
changes,
|
|
3
3
|
hasThreadFor,
|
|
4
|
-
refuse,
|
|
5
4
|
requireAnswers,
|
|
6
5
|
requireReplies,
|
|
7
6
|
requireThreadsClosed,
|
|
8
7
|
requireProgress,
|
|
9
8
|
scope,
|
|
10
9
|
} from "../../flows/index.js"
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
answerProductQuestions,
|
|
14
|
-
answerTechnicalQuestions,
|
|
15
|
-
author,
|
|
16
|
-
decompose,
|
|
17
|
-
triage,
|
|
18
|
-
} from "./steps.js"
|
|
10
|
+
import { REQUIREMENTS } from "../shared/steps.js"
|
|
11
|
+
import { answerProductQuestions, triage } from "./steps.js"
|
|
19
12
|
|
|
20
13
|
/**
|
|
21
14
|
* Always stop for the human. Resolves `true` when the round changed anything
|
|
@@ -40,21 +33,3 @@ export const design = (base: string): Promise<void> =>
|
|
|
40
33
|
requireReplies(REQUIREMENTS)
|
|
41
34
|
} while (await noteGate(REQUIREMENTS, answerProductQuestions))
|
|
42
35
|
})
|
|
43
|
-
|
|
44
|
-
/** Work out the technical plan, then decompose it into package files. */
|
|
45
|
-
export const architecture = (): Promise<void> =>
|
|
46
|
-
scope("architecture", async () => {
|
|
47
|
-
do {
|
|
48
|
-
await author()
|
|
49
|
-
requireReplies(ARCHITECTURE)
|
|
50
|
-
} while (await noteGate(ARCHITECTURE, answerTechnicalQuestions))
|
|
51
|
-
await decompose()
|
|
52
|
-
if (changes(".gtd/packages/**").length === 0) {
|
|
53
|
-
refuse("gtd land: decompose: write at least one package under .gtd/packages/")
|
|
54
|
-
}
|
|
55
|
-
if (changes(ARCHITECTURE).some((c) => c.status === "deleted")) {
|
|
56
|
-
refuse(
|
|
57
|
-
"gtd land: decompose: keep .gtd/ARCHITECTURE.md — the packages are built from it; it is removed with the last package",
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
@inmem
|
|
3
3
|
Feature: a refactor that carried the whole feature still gets a feature review
|
|
4
4
|
|
|
5
|
-
With
|
|
5
|
+
With GTD_FLAG_RISK_TIER off, a refactor-first
|
|
6
6
|
refactor can land everything the requirements ask for. The feature slice's
|
|
7
7
|
implement turn then has nothing to author: its empty turn completes the
|
|
8
8
|
step, and with no code changed since the refactor the feature review covers
|
|
@@ -12,7 +12,6 @@ Feature: a refactor that carried the whole feature still gets a feature review
|
|
|
12
12
|
Given the shipped workflows, as gtd install scaffolds them
|
|
13
13
|
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
14
14
|
And an environment variable "GTD_REFACTORQUALITYREVIEWS" set to ""
|
|
15
|
-
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
16
15
|
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
17
16
|
And a file "src/calc.ts" with:
|
|
18
17
|
"""
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
@inmem
|
|
3
3
|
Feature: a refactor-first judgment walks one fixed path to idle
|
|
4
4
|
|
|
5
|
-
With
|
|
5
|
+
With GTD_FLAG_RISK_TIER off, a naive pass touching
|
|
6
6
|
an existing non-allowlisted file is ruled refactor-first, is refactored and
|
|
7
7
|
that refactor reviewed, and only then implemented. The flags are
|
|
8
8
|
pinned so the per-flag test runs never move it off its path.
|
|
@@ -11,7 +11,6 @@ Feature: a refactor-first judgment walks one fixed path to idle
|
|
|
11
11
|
Given the shipped workflows, as gtd install scaffolds them
|
|
12
12
|
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
13
13
|
And an environment variable "GTD_REFACTORQUALITYREVIEWS" set to ""
|
|
14
|
-
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
15
14
|
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
16
15
|
And a file "src/calc.ts" with:
|
|
17
16
|
"""
|
|
@@ -154,7 +153,6 @@ Feature: a refactor-first judgment walks one fixed path to idle
|
|
|
154
153
|
gtd(human): build.review.await-review → build.review.closing
|
|
155
154
|
gtd(check): build.review.closing → idle
|
|
156
155
|
"""
|
|
157
|
-
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_naive_pass=1"
|
|
158
156
|
And the commit "gtd(human): idle → unwind" has a body containing "Gtd-Var: flag_risk_tier=0"
|
|
159
157
|
And the commit "gtd(agent): seam.naive → seam.judge" has a body containing "Gtd-Kept:"
|
|
160
158
|
And the commit "gtd(judge): seam.judge → refactor.deriving" has a body containing "\"answer\":\"refactor-first\""
|
|
@@ -10,7 +10,6 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
|
|
|
10
10
|
Background:
|
|
11
11
|
Given the shipped workflows, as gtd install scaffolds them
|
|
12
12
|
And an environment variable "GTD_QUALITYREVIEWS" set to ""
|
|
13
|
-
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
14
13
|
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
15
14
|
|
|
16
15
|
Scenario: with a narrowed seamAllow a test file added by the refactor is refused
|