@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.
Files changed (41) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +15 -0
  3. package/dist/chunks/{reserved-zAfoJtR7.mjs → reserved-CfQ8eurM.mjs} +85 -17
  4. package/dist/chunks/{tail-BRC4DGdV.mjs → tail-C7yaWvaS.mjs} +1 -1
  5. package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
  6. package/dist/gtd.bundle.mjs +1 -1
  7. package/dist/testkit.mjs +1 -1
  8. package/dist/wire.mjs +1 -1
  9. package/dist/workflows/cleanup.mjs +2 -2
  10. package/dist/workflows/feature.mjs +318 -76
  11. package/dist/workflows/fix.mjs +2 -2
  12. package/dist/workflows/review.mjs +2 -2
  13. package/dist/workflows/rollout.mjs +1 -1
  14. package/dist/workflows.mjs +419 -109
  15. package/package.json +1 -1
  16. package/src/testkit/steps.ts +39 -0
  17. package/src/wire/ContextLimit.ts +1 -1
  18. package/src/workflows/feature/continuation-cap.feature +65 -0
  19. package/src/workflows/feature/continuation-rebuild.feature +88 -0
  20. package/src/workflows/feature/continuation-slices.feature +64 -0
  21. package/src/workflows/feature/continuation.feature +205 -0
  22. package/src/workflows/feature/coverage.ts +14 -6
  23. package/src/workflows/feature/criteria.test.ts +17 -1
  24. package/src/workflows/feature/criteria.ts +37 -3
  25. package/src/workflows/feature/feature.ts +64 -15
  26. package/src/workflows/feature/index.ts +1 -0
  27. package/src/workflows/feature/refactor.ts +64 -30
  28. package/src/workflows/feature/seam.ts +157 -48
  29. package/src/workflows/feature/slicing-cap.feature +14 -0
  30. package/src/workflows/feature/slicing.feature +225 -0
  31. package/src/workflows/feature/slicing.test.ts +49 -0
  32. package/src/workflows/feature/slicing.ts +105 -0
  33. package/src/workflows/index.ts +1 -0
  34. package/src/workflows/shared/continuation.ts +92 -0
  35. package/src/workflows/shared/fences.ts +5 -0
  36. package/src/workflows/shared/options.ts +3 -0
  37. package/src/workflows/shared/steps.ts +2 -0
  38. package/src/workflows/shared/text.test.ts +6 -2
  39. package/src/workflows/shared/text.ts +153 -22
  40. package/src/workflows/shared/vars.ts +6 -0
  41. 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
- }- Leave everything uncommitted and finish your turn — do not commit
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
- export const naivePassPrompt = (retry?: NaiveRetry): string =>
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
- ${retry === undefined ? "" : naiveRetry(retry)}`
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
+ )