@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +30 -9
- package/claude/hooks/context.ts +66 -0
- package/claude/hooks/drive.ts +3 -0
- package/claude/hooks/register.tsx +69 -6
- package/dist/chunks/{reserved-BAv71I4i.mjs → reserved-sfMdy7v7.mjs} +83 -22
- package/dist/chunks/{tail-kar1zhOb.mjs → tail-Cgca5Gp-.mjs} +1 -1
- package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
- package/dist/gtd.bundle.mjs +498 -56
- package/dist/testkit.mjs +115 -3
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +277 -58
- 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 +355 -75
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +10 -3
- package/src/cli/Cli.ts +76 -1
- package/src/cli/Program.test.ts +1 -0
- package/src/cli/Program.ts +28 -0
- package/src/cli/ToolCommands.ts +3 -0
- package/src/edge/Edge.ts +11 -0
- package/src/flows/runtime.ts +11 -0
- package/src/replay/Replay.ts +51 -2
- package/src/testkit/Layers.ts +1 -0
- package/src/testkit/steps.ts +35 -0
- package/src/wire/BeatDocument.ts +12 -2
- package/src/wire/BeatStatus.ts +3 -0
- package/src/wire/ContextLimit.fixture.ts +30 -0
- package/src/wire/ContextLimit.test.ts +96 -0
- package/src/wire/ContextLimit.ts +169 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/types.ts +7 -0
- package/src/workflow/Workflow.ts +9 -1
- package/src/workflows/feature/context-limit.feature +37 -0
- package/src/workflows/feature/coverage.ts +22 -8
- package/src/workflows/feature/criteria.test.ts +17 -1
- package/src/workflows/feature/criteria.ts +26 -3
- package/src/workflows/feature/feature.ts +69 -16
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/refactor-absorbs.feature +12 -5
- package/src/workflows/feature/refactor-first.feature +11 -2
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +269 -0
- package/src/workflows/feature/refactor-review.feature +45 -1
- package/src/workflows/feature/refactor.ts +9 -3
- package/src/workflows/feature/seam.test.ts +108 -3
- package/src/workflows/feature/seam.ts +169 -31
- 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/fences.ts +5 -0
- package/src/workflows/shared/options.test.ts +11 -2
- package/src/workflows/shared/options.ts +19 -4
- package/src/workflows/shared/steps.ts +2 -0
- package/src/workflows/shared/text.ts +129 -28
- package/src/workflows/shared/vars.ts +1 -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
|
|
|
@@ -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
|
-
|
|
364
|
-
? `- The package
|
|
365
|
-
|
|
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
|
-
}${
|
|
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
|
+
)
|