@pmelab/gtd 37.0.0 → 38.1.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 +15 -0
- package/dist/chunks/{reserved-zAfoJtR7.mjs → reserved-CfQ8eurM.mjs} +85 -17
- package/dist/chunks/{tail-BRC4DGdV.mjs → tail-C7yaWvaS.mjs} +1 -1
- package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
- package/dist/gtd.bundle.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +318 -76
- 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 +419 -109
- package/package.json +1 -1
- package/src/testkit/steps.ts +39 -0
- package/src/wire/ContextLimit.ts +1 -1
- package/src/workflows/feature/continuation-cap.feature +65 -0
- package/src/workflows/feature/continuation-rebuild.feature +88 -0
- package/src/workflows/feature/continuation-slices.feature +64 -0
- package/src/workflows/feature/continuation.feature +205 -0
- package/src/workflows/feature/coverage.ts +14 -6
- package/src/workflows/feature/criteria.test.ts +17 -1
- package/src/workflows/feature/criteria.ts +37 -3
- package/src/workflows/feature/feature.ts +64 -15
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/refactor.ts +64 -30
- package/src/workflows/feature/seam.ts +157 -48
- package/src/workflows/feature/slicing-cap.feature +14 -0
- package/src/workflows/feature/slicing.feature +225 -0
- package/src/workflows/feature/slicing.test.ts +49 -0
- package/src/workflows/feature/slicing.ts +105 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/continuation.ts +92 -0
- package/src/workflows/shared/fences.ts +5 -0
- package/src/workflows/shared/options.ts +3 -0
- package/src/workflows/shared/steps.ts +2 -0
- package/src/workflows/shared/text.test.ts +6 -2
- package/src/workflows/shared/text.ts +153 -22
- package/src/workflows/shared/vars.ts +6 -0
- package/src/workflows/shared/workflows.steps.ts +13 -0
|
@@ -290,6 +290,30 @@ printf '%s\\n' ${lines.map(quote).join(" ")} > ${quote(".gtd/REQUIREMENTS.md")}
|
|
|
290
290
|
`
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
export const slicingPrompt = (cap: number): string =>
|
|
294
|
+
`${stateFileRules}
|
|
295
|
+
- Read \`.gtd/REQUIREMENTS.md\`: its \`## Acceptance Criteria\` are settled
|
|
296
|
+
and more than ${cap}, too many to build in one slice
|
|
297
|
+
- Group them into slices in \`.gtd/SLICES.md\`, one \`## <title>\` heading per
|
|
298
|
+
slice over a \`- AC-<n>\` list item per criterion it builds. Put criteria of
|
|
299
|
+
the same flow or subsystem together; every criterion in exactly one slice,
|
|
300
|
+
at most ${cap} per slice
|
|
301
|
+
- Keep it coarse: as few slices as the cap allows, never one per criterion
|
|
302
|
+
- Order the slices so each builds on the ones before: they run in that order,
|
|
303
|
+
each from where the previous one landed
|
|
304
|
+
- Change nothing but \`.gtd/SLICES.md\` — leave the criteria as they are
|
|
305
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
306
|
+
`
|
|
307
|
+
|
|
308
|
+
/** The line telling a slice's turn which criteria are its own; empty for a single slice. */
|
|
309
|
+
const sliceLine = (ids: readonly string[] | undefined): string =>
|
|
310
|
+
ids === undefined
|
|
311
|
+
? ""
|
|
312
|
+
: `- This slice builds ${ids.join(", ")} only: every rule here about the criteria
|
|
313
|
+
means these. The others are for other slices, before or after it — leave
|
|
314
|
+
them unbuilt and untested
|
|
315
|
+
`
|
|
316
|
+
|
|
293
317
|
export const designSystem = (): string =>
|
|
294
318
|
`${designPersona}
|
|
295
319
|
|
|
@@ -351,6 +375,7 @@ export const refactorBuildingPrompt = ({
|
|
|
351
375
|
reference,
|
|
352
376
|
feedback = false,
|
|
353
377
|
note = false,
|
|
378
|
+
from,
|
|
354
379
|
}: {
|
|
355
380
|
pkg: string
|
|
356
381
|
strays: readonly string[]
|
|
@@ -358,6 +383,8 @@ export const refactorBuildingPrompt = ({
|
|
|
358
383
|
reference?: string | undefined
|
|
359
384
|
feedback?: boolean | undefined
|
|
360
385
|
note?: boolean | undefined
|
|
386
|
+
/** A continuation of a refactor turn that stopped at its context limit; `open` are its untouched paths. */
|
|
387
|
+
from?: Handoff | undefined
|
|
361
388
|
}): string =>
|
|
362
389
|
`${stateFileRules}
|
|
363
390
|
- The package to implement is \`${pkg}\`; never delete it
|
|
@@ -386,8 +413,9 @@ ${
|
|
|
386
413
|
refactor that touches none of it is wrong, however clean — never
|
|
387
414
|
swap in an unrelated tidy-up
|
|
388
415
|
`
|
|
389
|
-
}
|
|
390
|
-
|
|
416
|
+
}${handoffRule()}
|
|
417
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
418
|
+
${from === undefined ? "" : continuationLines(from)}`
|
|
391
419
|
|
|
392
420
|
export const refactorFixPrompt = (strays: readonly string[], pattern: string): string =>
|
|
393
421
|
`${stateFileRules}
|
|
@@ -1174,8 +1202,8 @@ export type NaiveRetry = SeamRejection | NaiveRed | NaiveFault
|
|
|
1174
1202
|
const isRed = (retry: NaiveRetry): retry is NaiveRed => "red" in retry
|
|
1175
1203
|
const isFault = (retry: NaiveRetry): retry is NaiveFault => "fault" in retry
|
|
1176
1204
|
|
|
1177
|
-
const pickUp = (hash: string): string =>
|
|
1178
|
-
`Pick it up — \`git diff --binary ${hash}^ ${hash} | git apply\``
|
|
1205
|
+
const pickUp = (hash: string, exclude?: string): string =>
|
|
1206
|
+
`Pick it up — \`git diff --binary --no-color --no-ext-diff --no-textconv ${hash}^ ${hash}${exclude === undefined ? "" : ` -- ${quote(":(top)")} ${quote(`:(top,exclude)${exclude}`)}`} | git apply\``
|
|
1179
1207
|
|
|
1180
1208
|
const criteriaBinding = (refused: boolean): string =>
|
|
1181
1209
|
`- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
|
|
@@ -1203,10 +1231,75 @@ ${listed(retry.strays)}
|
|
|
1203
1231
|
Start over and choose a different approach; do not rebuild the same diff.
|
|
1204
1232
|
`
|
|
1205
1233
|
|
|
1206
|
-
|
|
1234
|
+
/** A turn that stopped at its context limit: the snapshot it left, its handoff note, what it left open. */
|
|
1235
|
+
export interface Handoff {
|
|
1236
|
+
/** The commit holding the stopped turn's work: its kept tip, or the turn's own commit. */
|
|
1237
|
+
readonly snapshot: string
|
|
1238
|
+
readonly note: string
|
|
1239
|
+
readonly open: readonly string[]
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const quoted = (text: string): string =>
|
|
1243
|
+
text
|
|
1244
|
+
.trimEnd()
|
|
1245
|
+
.split("\n")
|
|
1246
|
+
.map((line) => (line === "" ? "" : ` > ${line}`))
|
|
1247
|
+
.join("\n")
|
|
1248
|
+
|
|
1249
|
+
/** Every turn a continuation may pick up tells its agent where the note goes. */
|
|
1250
|
+
const handoffRule = (): string =>
|
|
1251
|
+
`- Should you be told to stop at a context limit, write the handoff note to
|
|
1252
|
+
\`.gtd/HANDOFF.md\` — what you touched, what was next — and finish your turn:
|
|
1253
|
+
a fresh turn picks up from what you leave, broken or not. That file is the one
|
|
1254
|
+
exception to any path rule here; write it at no other time`
|
|
1255
|
+
|
|
1256
|
+
/** `pickUp`: how the snapshot gets into the tree — absent when it already is. */
|
|
1257
|
+
const continuationLines = (from: Handoff, pickUp?: string): string =>
|
|
1258
|
+
`
|
|
1259
|
+
Your previous turn stopped at its context limit; this fresh turn picks up from
|
|
1260
|
+
its snapshot (\`${from.snapshot}\`). ${pickUp === undefined ? "Its work is already in the tree." : `${pickUp}.`} Its handoff note:
|
|
1261
|
+
|
|
1262
|
+
${quoted(from.note === "" ? "(empty)" : from.note)}
|
|
1263
|
+
|
|
1264
|
+
Still open: ${from.open.length === 0 ? "nothing the workflow can count — finish what the note says is next" : from.open.join(", ")}.
|
|
1265
|
+
${pickUp === undefined ? "Delete `.gtd/HANDOFF.md` once you are done; rewrite it should you be stopped again.\n" : ""}`
|
|
1266
|
+
|
|
1267
|
+
export const oversizedMessage = (
|
|
1268
|
+
phase: string,
|
|
1269
|
+
from: Handoff,
|
|
1270
|
+
why: { readonly cap: number } | { readonly stall: number },
|
|
1271
|
+
narrowable = true,
|
|
1272
|
+
): string =>
|
|
1273
|
+
`The ${phase} keeps outgrowing its context: ${
|
|
1274
|
+
"cap" in why
|
|
1275
|
+
? why.cap === 0
|
|
1276
|
+
? "continuations are off (`continuations` is 0) and the turn stopped at its context limit"
|
|
1277
|
+
: `${why.cap} continuation${why.cap === 1 ? "" : "s"} ran and the last still stopped at its context limit`
|
|
1278
|
+
: `the last ${why.stall} continuation${why.stall === 1 ? "" : "s"} closed nothing still open`
|
|
1279
|
+
}. The last snapshot is \`${from.snapshot}\`; still open: ${from.open.length === 0 ? "nothing countable" : from.open.join(", ")}.
|
|
1280
|
+
|
|
1281
|
+
Its handoff note:
|
|
1282
|
+
|
|
1283
|
+
${quoted(from.note === "" ? "(empty)" : from.note)}
|
|
1284
|
+
|
|
1285
|
+
Land to go on: a fresh round of continuations picks up from that snapshot${
|
|
1286
|
+
narrowable
|
|
1287
|
+
? ` — narrow
|
|
1288
|
+
\`.gtd/REQUIREMENTS.md\` first if the change is too big, or raise the context limit`
|
|
1289
|
+
: ` — raise
|
|
1290
|
+
the context limit first if the refactor is too big`
|
|
1291
|
+
}. \`gtd abandon\` gives the change up.
|
|
1292
|
+
`
|
|
1293
|
+
|
|
1294
|
+
/** `from`: a continuation of a pass that stopped at its context limit, in place of `retry`'s pick-up. */
|
|
1295
|
+
export const naivePassPrompt = (
|
|
1296
|
+
retry?: NaiveRetry,
|
|
1297
|
+
slice?: readonly string[],
|
|
1298
|
+
from?: Handoff,
|
|
1299
|
+
): string =>
|
|
1207
1300
|
`${stateFileRules}
|
|
1208
1301
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1209
|
-
${criteriaBinding(false)}
|
|
1302
|
+
${sliceLine(slice)}${criteriaBinding(false)}
|
|
1210
1303
|
- Implement them the quickest way that works — the naive version. Do not
|
|
1211
1304
|
restructure, abstract or refactor around the change; it may land exactly as
|
|
1212
1305
|
you leave it, and a reviewer makes it fit
|
|
@@ -1221,9 +1314,34 @@ ${criteriaBinding(false)}
|
|
|
1221
1314
|
- Run \`${env.testCommand}\` and finish only once it is green with every
|
|
1222
1315
|
criterion's test passing: the next step runs it on your diff and sends a red
|
|
1223
1316
|
pass back
|
|
1224
|
-
- Leave \`.gtd/\` untouched
|
|
1317
|
+
- Leave \`.gtd/\` untouched, a handoff note excepted
|
|
1318
|
+
${handoffRule()}
|
|
1225
1319
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
1226
|
-
${
|
|
1320
|
+
${
|
|
1321
|
+
from !== undefined
|
|
1322
|
+
? `${continuationLines(from, pickUp(from.snapshot, ".gtd/HANDOFF.md"))}${
|
|
1323
|
+
retry === undefined
|
|
1324
|
+
? ""
|
|
1325
|
+
: isFault(retry)
|
|
1326
|
+
? `
|
|
1327
|
+
The pass this continues was a retry: its tests fell short, and still must not:
|
|
1328
|
+
${retry.fault}
|
|
1329
|
+
`
|
|
1330
|
+
: isRed(retry)
|
|
1331
|
+
? `
|
|
1332
|
+
The pass this continues was a retry after a red suite (\`.gtd/FEEDBACK.md\`
|
|
1333
|
+
holds the output): finish only once the suite is green.
|
|
1334
|
+
`
|
|
1335
|
+
: `
|
|
1336
|
+
The pass this continues was a retry after a \`reject\`: keep away from the
|
|
1337
|
+
approach that changed these files outside the expected set:
|
|
1338
|
+
${listed(retry.strays)}
|
|
1339
|
+
`
|
|
1340
|
+
}`
|
|
1341
|
+
: retry === undefined
|
|
1342
|
+
? ""
|
|
1343
|
+
: naiveRetry(retry)
|
|
1344
|
+
}`
|
|
1227
1345
|
|
|
1228
1346
|
/** The naive pass's kept tip, its acceptance test files and the criterion IDs they name. */
|
|
1229
1347
|
export interface Acceptance {
|
|
@@ -1289,6 +1407,25 @@ const afterRefactorLine = (rebuild: Rebuild): string =>
|
|
|
1289
1407
|
rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
|
|
1290
1408
|
`
|
|
1291
1409
|
|
|
1410
|
+
const flagLine = (
|
|
1411
|
+
flagMode: boolean,
|
|
1412
|
+
tier: "low" | "high" | undefined,
|
|
1413
|
+
amend: boolean | undefined,
|
|
1414
|
+
sliced: boolean,
|
|
1415
|
+
): string =>
|
|
1416
|
+
!flagMode
|
|
1417
|
+
? "- Add no release flag — this project ships without release flags"
|
|
1418
|
+
: tier === "low"
|
|
1419
|
+
? "- Add no release flag — this change is low risk tier"
|
|
1420
|
+
: amend === true
|
|
1421
|
+
? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag"
|
|
1422
|
+
: sliced
|
|
1423
|
+
? `- Put the new behaviour behind the release flag an earlier slice of this
|
|
1424
|
+
change added; only when none did, behind a new one, following the
|
|
1425
|
+
repository's flag conventions`
|
|
1426
|
+
: `- Put the new behaviour behind a new release flag, following the
|
|
1427
|
+
repository's flag conventions`
|
|
1428
|
+
|
|
1292
1429
|
export const implementBuildingPrompt = (
|
|
1293
1430
|
reference: string | undefined,
|
|
1294
1431
|
tier?: "low" | "high",
|
|
@@ -1296,10 +1433,12 @@ export const implementBuildingPrompt = (
|
|
|
1296
1433
|
afterRefactor?: boolean,
|
|
1297
1434
|
flagMode = true,
|
|
1298
1435
|
rebuild: Rebuild = {},
|
|
1436
|
+
slice?: readonly string[],
|
|
1437
|
+
from?: Handoff,
|
|
1299
1438
|
): string =>
|
|
1300
1439
|
`${stateFileRules}
|
|
1301
1440
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1302
|
-
${criteriaBinding(true)}
|
|
1441
|
+
${sliceLine(slice)}${criteriaBinding(true)}
|
|
1303
1442
|
${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${
|
|
1304
1443
|
amend === true
|
|
1305
1444
|
? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
@@ -1318,22 +1457,14 @@ ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${
|
|
|
1318
1457
|
Read it for ideas; never apply, cherry-pick or check it out — write
|
|
1319
1458
|
the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
|
|
1320
1459
|
`
|
|
1321
|
-
}${rebuildLines(rebuild)}${
|
|
1322
|
-
!flagMode
|
|
1323
|
-
? "- Add no release flag — this project ships without release flags"
|
|
1324
|
-
: tier === "low"
|
|
1325
|
-
? "- Add no release flag — this change is low risk tier"
|
|
1326
|
-
: amend === true
|
|
1327
|
-
? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag"
|
|
1328
|
-
: `- Put the new behaviour behind a new release flag, following the
|
|
1329
|
-
repository's flag conventions`
|
|
1330
|
-
}
|
|
1460
|
+
}${rebuildLines(rebuild)}${flagLine(flagMode, tier, amend, slice !== undefined)}
|
|
1331
1461
|
- Write the tests first, then the code that makes them pass. Name each
|
|
1332
1462
|
criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
|
|
1333
1463
|
green a judge checks every ID has a passing test, and sends uncovered ones back
|
|
1464
|
+
${handoffRule()}
|
|
1334
1465
|
- Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
|
|
1335
1466
|
finish your turn
|
|
1336
|
-
`
|
|
1467
|
+
${from === undefined ? "" : continuationLines(from)}`
|
|
1337
1468
|
|
|
1338
1469
|
export const implementFixPrompt = (): string =>
|
|
1339
1470
|
`${stateFileRules}
|
|
@@ -1437,10 +1568,10 @@ export const coverageMessage = (): string =>
|
|
|
1437
1568
|
counts every criterion as uncovered.
|
|
1438
1569
|
`
|
|
1439
1570
|
|
|
1440
|
-
export const coveringPrompt = (uncovered: readonly string[]): string =>
|
|
1571
|
+
export const coveringPrompt = (uncovered: readonly string[], slice?: readonly string[]): string =>
|
|
1441
1572
|
`${stateFileRules}
|
|
1442
1573
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1443
|
-
${criteriaBinding(true)}
|
|
1574
|
+
${sliceLine(slice)}${criteriaBinding(true)}
|
|
1444
1575
|
- A judge found no passing test in this change that covers ${uncovered.join(", ")}.
|
|
1445
1576
|
Write a behaviour-level test for each, naming its ID in the test or scenario
|
|
1446
1577
|
name (\`[${uncovered[0]}] …\`), and fix the code if the test shows it falls short
|
|
@@ -26,6 +26,7 @@ export const defaults: Readonly<Record<string, string>> = {
|
|
|
26
26
|
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
27
27
|
seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
|
|
28
28
|
flagMode: "true",
|
|
29
|
+
sliceMaxCriteria: "8",
|
|
29
30
|
// GTD_FLAG_RISK_TIER, recorded at process start; only "1" is on.
|
|
30
31
|
flag_risk_tier: "0",
|
|
31
32
|
// Decides the turn count: one `build.quality.<lens>` scope per entry, each
|
|
@@ -33,6 +34,11 @@ export const defaults: Readonly<Record<string, string>> = {
|
|
|
33
34
|
qualityReviews:
|
|
34
35
|
"correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
|
|
35
36
|
refactorQualityReviews: "correctness, ponytail-review, conventions",
|
|
37
|
+
// Per phase (naive pass, refactor, build): continuations of a turn that
|
|
38
|
+
// stopped at its context limit before a person is asked, and how many in a
|
|
39
|
+
// row may close nothing still open.
|
|
40
|
+
continuations: "3",
|
|
41
|
+
continuationStall: "2",
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
/** Environment settings. */
|
|
@@ -52,3 +52,16 @@ Then("no commit body contains {string}", (world: TestkitWorld, text: string) =>
|
|
|
52
52
|
.find((m) => m.includes(text))
|
|
53
53
|
assert.ok(hit === undefined, `a commit body contains "${text}":\n${hit}`)
|
|
54
54
|
})
|
|
55
|
+
|
|
56
|
+
Then(
|
|
57
|
+
"slice {string} begins where slice {string} ends",
|
|
58
|
+
async (world: TestkitWorld, later: string, earlier: string) => {
|
|
59
|
+
const { stdout } = await world.repo().gtd("slices")
|
|
60
|
+
const range = (name: string): string[] => {
|
|
61
|
+
const line = stdout.split("\n").find((l) => l.startsWith(`${name} `))
|
|
62
|
+
assert.ok(line !== undefined, stdout)
|
|
63
|
+
return line.slice(name.length + 1).split("..")
|
|
64
|
+
}
|
|
65
|
+
assert.strictEqual(range(later)[0], range(earlier)[1], stdout)
|
|
66
|
+
},
|
|
67
|
+
)
|