@pmelab/gtd 36.0.0 → 38.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.
Files changed (62) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +30 -9
  3. package/claude/hooks/context.ts +66 -0
  4. package/claude/hooks/drive.ts +3 -0
  5. package/claude/hooks/register.tsx +69 -6
  6. package/dist/chunks/{reserved-BAv71I4i.mjs → reserved-sfMdy7v7.mjs} +83 -22
  7. package/dist/chunks/{tail-kar1zhOb.mjs → tail-Cgca5Gp-.mjs} +1 -1
  8. package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
  9. package/dist/gtd.bundle.mjs +498 -56
  10. package/dist/testkit.mjs +115 -3
  11. package/dist/wire.mjs +2 -2
  12. package/dist/workflows/cleanup.mjs +2 -2
  13. package/dist/workflows/feature.mjs +277 -58
  14. package/dist/workflows/fix.mjs +2 -2
  15. package/dist/workflows/review.mjs +2 -2
  16. package/dist/workflows/rollout.mjs +1 -1
  17. package/dist/workflows.mjs +355 -75
  18. package/package.json +1 -1
  19. package/src/cli/Cli.test.ts +10 -3
  20. package/src/cli/Cli.ts +76 -1
  21. package/src/cli/Program.test.ts +1 -0
  22. package/src/cli/Program.ts +28 -0
  23. package/src/cli/ToolCommands.ts +3 -0
  24. package/src/edge/Edge.ts +11 -0
  25. package/src/flows/runtime.ts +11 -0
  26. package/src/replay/Replay.ts +51 -2
  27. package/src/testkit/Layers.ts +1 -0
  28. package/src/testkit/steps.ts +35 -0
  29. package/src/wire/BeatDocument.ts +12 -2
  30. package/src/wire/BeatStatus.ts +3 -0
  31. package/src/wire/ContextLimit.fixture.ts +30 -0
  32. package/src/wire/ContextLimit.test.ts +96 -0
  33. package/src/wire/ContextLimit.ts +169 -0
  34. package/src/wire/index.ts +1 -0
  35. package/src/wire/types.ts +7 -0
  36. package/src/workflow/Workflow.ts +9 -1
  37. package/src/workflows/feature/context-limit.feature +37 -0
  38. package/src/workflows/feature/coverage.ts +22 -8
  39. package/src/workflows/feature/criteria.test.ts +17 -1
  40. package/src/workflows/feature/criteria.ts +26 -3
  41. package/src/workflows/feature/feature.ts +69 -16
  42. package/src/workflows/feature/index.ts +1 -0
  43. package/src/workflows/feature/refactor-absorbs.feature +12 -5
  44. package/src/workflows/feature/refactor-first.feature +11 -2
  45. package/src/workflows/feature/refactor-lens-models.feature +4 -0
  46. package/src/workflows/feature/refactor-rebuild.feature +269 -0
  47. package/src/workflows/feature/refactor-review.feature +45 -1
  48. package/src/workflows/feature/refactor.ts +9 -3
  49. package/src/workflows/feature/seam.test.ts +108 -3
  50. package/src/workflows/feature/seam.ts +169 -31
  51. package/src/workflows/feature/slicing-cap.feature +14 -0
  52. package/src/workflows/feature/slicing.feature +225 -0
  53. package/src/workflows/feature/slicing.test.ts +49 -0
  54. package/src/workflows/feature/slicing.ts +105 -0
  55. package/src/workflows/index.ts +1 -0
  56. package/src/workflows/shared/fences.ts +5 -0
  57. package/src/workflows/shared/options.test.ts +11 -2
  58. package/src/workflows/shared/options.ts +19 -4
  59. package/src/workflows/shared/steps.ts +2 -0
  60. package/src/workflows/shared/text.ts +129 -28
  61. package/src/workflows/shared/vars.ts +1 -0
  62. 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
 
@@ -350,22 +374,30 @@ export const refactorBuildingPrompt = ({
350
374
  pattern,
351
375
  reference,
352
376
  feedback = false,
377
+ note = false,
353
378
  }: {
354
379
  pkg: string
355
380
  strays: readonly string[]
356
381
  pattern: string
357
382
  reference?: string | undefined
358
383
  feedback?: boolean | undefined
384
+ note?: boolean | undefined
359
385
  }): string =>
360
386
  `${stateFileRules}
361
387
  - The package to implement is \`${pkg}\`; never delete it
362
388
  ${
363
- feedback
364
- ? `- The package carries a \`## Review feedback\` section: a human's review of the
365
- last refactor. Act on it, within the rules below
389
+ note
390
+ ? `- The package's \`## Refactor note\` section names the seams to open and the
391
+ shape each takes, written from the whole naive pass: build exactly those
366
392
  `
367
393
  : ""
368
- }${refactorRules(strays, pattern)}
394
+ }${
395
+ feedback
396
+ ? `- The package carries a \`## Review feedback\` section: a human's review of the
397
+ last refactor. Act on it, within the rules below
398
+ `
399
+ : ""
400
+ }${refactorRules(strays, pattern)}
369
401
  ${
370
402
  reference === undefined
371
403
  ? ""
@@ -1195,10 +1227,10 @@ ${listed(retry.strays)}
1195
1227
  Start over and choose a different approach; do not rebuild the same diff.
1196
1228
  `
1197
1229
 
1198
- export const naivePassPrompt = (retry?: NaiveRetry): string =>
1230
+ export const naivePassPrompt = (retry?: NaiveRetry, slice?: readonly string[]): string =>
1199
1231
  `${stateFileRules}
1200
1232
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1201
- ${criteriaBinding(false)}
1233
+ ${sliceLine(slice)}${criteriaBinding(false)}
1202
1234
  - Implement them the quickest way that works — the naive version. Do not
1203
1235
  restructure, abstract or refactor around the change; it may land exactly as
1204
1236
  you leave it, and a reviewer makes it fit
@@ -1217,24 +1249,102 @@ ${criteriaBinding(false)}
1217
1249
  - Leave everything uncommitted and finish your turn — do not commit
1218
1250
  ${retry === undefined ? "" : naiveRetry(retry)}`
1219
1251
 
1252
+ /** The naive pass's kept tip, its acceptance test files and the criterion IDs they name. */
1253
+ export interface Acceptance {
1254
+ readonly hash: string
1255
+ readonly tests: readonly string[]
1256
+ readonly ids: readonly string[]
1257
+ }
1258
+
1259
+ /** What a rebuild after a refactor-first judgment carries over from the naive pass. */
1260
+ export interface Rebuild {
1261
+ readonly note?: string | undefined
1262
+ readonly acceptance?: Acceptance | undefined
1263
+ }
1264
+
1265
+ const carries = (rebuild: Rebuild): boolean => (rebuild.acceptance?.ids.length ?? 0) > 0
1266
+
1267
+ const rebuildLines = ({ note, acceptance }: Rebuild): string =>
1268
+ `${
1269
+ note === undefined
1270
+ ? ""
1271
+ : `- The refactor note (\`.gtd/REFACTOR-NOTE.md\`) named the seams the refactor
1272
+ opened; plug the change into them:
1273
+
1274
+ ${note
1275
+ .trimEnd()
1276
+ .split("\n")
1277
+ .map((line) => (line === "" ? "" : ` > ${line}`))
1278
+ .join("\n")}
1279
+
1280
+ `
1281
+ }${
1282
+ acceptance === undefined || !carries({ acceptance })
1283
+ ? ""
1284
+ : `- Carry over the naive pass's acceptance tests for ${acceptance.ids.join(", ")}, the
1285
+ one part of it you take as it is: \`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`.
1286
+ Copy each test with its \`[AC-<n>]\` name into the tree as the refactor left
1287
+ it — check a file out of the tip whole only where the refactor did not touch
1288
+ it — changing only what reaches the release flag. A turn whose changed tests
1289
+ leave an ID out is refused, and the suite must be green with them
1290
+ `
1291
+ }`
1292
+
1293
+ export const refactorNotePrompt = (reference: string, strays: readonly string[]): string =>
1294
+ `${stateFileRules}
1295
+ - The seam judge ruled the naive pass refactor-first: it is never applied.
1296
+ A behaviour-neutral refactor lands first, then the change is rebuilt on
1297
+ the refactored base. Both turns read the note you write
1298
+ - Read the whole naive pass: \`git show ${reference}\`. It changed these
1299
+ existing files outside the allowlist:
1300
+ ${listed(strays)}
1301
+ - Write \`.gtd/REFACTOR-NOTE.md\`: which seams the refactor opens (the existing
1302
+ code to restructure, by path and symbol) and the shape each takes — the
1303
+ signatures and where the new behaviour will plug in. Name only the files
1304
+ listed above: the refactor may change no other path. Never the feature's own
1305
+ behaviour, and no flag read: the refactor adds neither
1306
+ - Change nothing else — a turn that does is refused
1307
+ - Leave everything uncommitted and finish your turn — do not commit
1308
+ `
1309
+
1310
+ const afterRefactorLine = (rebuild: Rebuild): string =>
1311
+ `- A reviewed refactor has just landed and may already carry part of the change,
1312
+ though never a release flag. Build only what is still missing, held to every
1313
+ rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
1314
+ `
1315
+
1316
+ const flagLine = (
1317
+ flagMode: boolean,
1318
+ tier: "low" | "high" | undefined,
1319
+ amend: boolean | undefined,
1320
+ sliced: boolean,
1321
+ ): string =>
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
+ : sliced
1329
+ ? `- Put the new behaviour behind the release flag an earlier slice of this
1330
+ change added; only when none did, behind a new one, following the
1331
+ repository's flag conventions`
1332
+ : `- Put the new behaviour behind a new release flag, following the
1333
+ repository's flag conventions`
1334
+
1220
1335
  export const implementBuildingPrompt = (
1221
1336
  reference: string | undefined,
1222
1337
  tier?: "low" | "high",
1223
1338
  amend?: boolean,
1224
1339
  afterRefactor?: boolean,
1225
1340
  flagMode = true,
1341
+ rebuild: Rebuild = {},
1342
+ slice?: readonly string[],
1226
1343
  ): string =>
1227
1344
  `${stateFileRules}
1228
1345
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1229
- ${criteriaBinding(true)}
1230
- ${
1231
- afterRefactor === true
1232
- ? `- A reviewed refactor has just landed and may already carry part of the change,
1233
- though never a release flag. Build only what is still missing, held to every
1234
- rule below; if nothing is, change nothing and finish your turn
1235
- `
1236
- : ""
1237
- }${
1346
+ ${sliceLine(slice)}${criteriaBinding(true)}
1347
+ ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${
1238
1348
  amend === true
1239
1349
  ? `- The change already exists in the tree from an earlier lap; amend it to meet
1240
1350
  the re-derived requirements instead of rewriting it
@@ -1250,18 +1360,9 @@ ${
1250
1360
  ? ""
1251
1361
  : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
1252
1362
  Read it for ideas; never apply, cherry-pick or check it out — write
1253
- the change fresh
1363
+ the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
1254
1364
  `
1255
- }${
1256
- !flagMode
1257
- ? "- Add no release flag — this project ships without release flags"
1258
- : tier === "low"
1259
- ? "- Add no release flag — this change is low risk tier"
1260
- : amend === true
1261
- ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag"
1262
- : `- Put the new behaviour behind a new release flag, following the
1263
- repository's flag conventions`
1264
- }
1365
+ }${rebuildLines(rebuild)}${flagLine(flagMode, tier, amend, slice !== undefined)}
1265
1366
  - Write the tests first, then the code that makes them pass. Name each
1266
1367
  criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
1267
1368
  green a judge checks every ID has a passing test, and sends uncovered ones back
@@ -1371,10 +1472,10 @@ export const coverageMessage = (): string =>
1371
1472
  counts every criterion as uncovered.
1372
1473
  `
1373
1474
 
1374
- export const coveringPrompt = (uncovered: readonly string[]): string =>
1475
+ export const coveringPrompt = (uncovered: readonly string[], slice?: readonly string[]): string =>
1375
1476
  `${stateFileRules}
1376
1477
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1377
- ${criteriaBinding(true)}
1478
+ ${sliceLine(slice)}${criteriaBinding(true)}
1378
1479
  - A judge found no passing test in this change that covers ${uncovered.join(", ")}.
1379
1480
  Write a behaviour-level test for each, naming its ID in the test or scenario
1380
1481
  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
@@ -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
+ )