@pmelab/gtd 27.4.0 → 27.5.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 (44) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +8 -0
  3. package/dist/chunks/{Schema-CyImoHTj.mjs → Schema-DO5ttJaI.mjs} +1 -1
  4. package/dist/chunks/{reserved-BPdVZkgv.mjs → reserved-DM5W_LAt.mjs} +64 -3
  5. package/dist/chunks/{tail-iLAMoigl.mjs → tail-6gvLXXcg.mjs} +1 -1
  6. package/dist/flows.mjs +4 -2
  7. package/dist/gtd.bundle.mjs +44 -9
  8. package/dist/testkit.mjs +43 -10
  9. package/dist/workflows/cleanup.mjs +2 -2
  10. package/dist/workflows/feature.mjs +140 -64
  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 +346 -107
  15. package/package.json +1 -1
  16. package/src/cli/Cli.test.ts +49 -1
  17. package/src/cli/Cli.ts +20 -3
  18. package/src/edge/Edge.test.ts +45 -1
  19. package/src/edge/Edge.ts +15 -4
  20. package/src/flags/index.test.ts +14 -1
  21. package/src/flags/index.ts +14 -3
  22. package/src/flows/helpers.ts +7 -3
  23. package/src/replay/judgeDiffShape.test.ts +43 -0
  24. package/src/step/planStart.ts +3 -2
  25. package/src/testkit/TestRepo.ts +3 -0
  26. package/src/testkit/steps.ts +18 -0
  27. package/src/workflow/index.ts +8 -1
  28. package/src/workflow/vars.test.ts +23 -1
  29. package/src/workflow/vars.ts +24 -0
  30. package/src/workflows/feature/feature.ts +9 -2
  31. package/src/workflows/feature/index.ts +1 -0
  32. package/src/workflows/feature/seam-pin.feature +52 -0
  33. package/src/workflows/feature/seam.feature +387 -0
  34. package/src/workflows/feature/seam.test.ts +88 -0
  35. package/src/workflows/feature/seam.ts +110 -0
  36. package/src/workflows/index.ts +1 -0
  37. package/src/workflows/shared/options.test.ts +12 -1
  38. package/src/workflows/shared/options.ts +1 -0
  39. package/src/workflows/shared/skills.test.ts +20 -2
  40. package/src/workflows/shared/skills.ts +6 -0
  41. package/src/workflows/shared/text.test.ts +59 -0
  42. package/src/workflows/shared/text.ts +86 -0
  43. package/src/workflows/shared/vars.ts +4 -0
  44. package/src/workflows/shared/workflows.steps.ts +39 -1
@@ -17,6 +17,11 @@ import {
17
17
  buildReviewCollectingPrompt,
18
18
  designGateAnswerMessage,
19
19
  designTriagePrompt,
20
+ implementBuildingPrompt,
21
+ implementFixPrompt,
22
+ naivePassPrompt,
23
+ seamByHandMessage,
24
+ seamSteerMessage,
20
25
  summaryPrompt,
21
26
  withSkills,
22
27
  } from "./text.js"
@@ -441,3 +446,57 @@ describe("buildReviewReviewingPrompt scenarios", () => {
441
446
  expect(prompt).not.toContain("Weakened:")
442
447
  })
443
448
  })
449
+
450
+ describe("the seam phase's texts", () => {
451
+ const rejection = { strays: ["src/flows/helpers.ts", "src/flows/runtime.ts"], p: 0.85 }
452
+
453
+ it("asks the first naive pass for the quickest working change, with no rejection", () => {
454
+ const prompt = renderText(() => naivePassPrompt())
455
+ expect(prompt).toContain(".gtd/REQUIREMENTS.md")
456
+ expect(prompt).not.toContain("rejected")
457
+ })
458
+
459
+ it("names the reject verdict, its p, every stray and the criterion on a retry", () => {
460
+ const prompt = renderText(() => naivePassPrompt(rejection))
461
+ expect(prompt).toContain("reject")
462
+ expect(prompt).toContain("0.85")
463
+ expect(prompt).toContain("`src/flows/helpers.ts`")
464
+ expect(prompt).toContain("`src/flows/runtime.ts`")
465
+ expect(prompt).toContain("solved the task the wrong way")
466
+ })
467
+
468
+ it("cites the reference as reading material only and asks for a new release flag", () => {
469
+ const prompt = renderText(() => implementBuildingPrompt("abc123"))
470
+ expect(prompt).toContain("git show abc123")
471
+ expect(prompt).toContain("never apply")
472
+ expect(prompt).toContain("release flag")
473
+ })
474
+
475
+ it("keeps a fix turn to the feedback file's rules", () => {
476
+ expect(renderText(() => implementFixPrompt())).toContain(".gtd/FEEDBACK.md")
477
+ })
478
+
479
+ it("names both steer answers and the overrule risk, and lists every stray", () => {
480
+ const message = renderText(() => seamSteerMessage(rejection))
481
+ expect(message).toContain("edit `.gtd/REQUIREMENTS.md`")
482
+ expect(message).toContain("clean")
483
+ expect(message).toContain("Overrule")
484
+ expect(message).toContain("unaddressed")
485
+ expect(message).toContain("src/flows/helpers.ts")
486
+ })
487
+
488
+ it("names every unreadable path for a by-hand landing", () => {
489
+ const message = renderText(() =>
490
+ seamByHandMessage(["vendor/a.bin", "src/flows/helpers.ts"], ["vendor/a.bin"], "abc123"),
491
+ )
492
+ expect(message).toContain("`vendor/a.bin` (unreadable)")
493
+ expect(message).toContain("`src/flows/helpers.ts`\n")
494
+ expect(message).toContain("git show abc123 --")
495
+ })
496
+
497
+ it("keeps a stray path with a newline on one line", () => {
498
+ const message = renderText(() => seamSteerMessage({ strays: ["a\nb.ts"], p: 0.9 }))
499
+ expect(message).toContain('`"a\\nb.ts"`')
500
+ expect(message).not.toContain("a\nb.ts")
501
+ })
502
+ })
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  agent,
3
3
  codeThreads,
4
+ displayPath,
4
5
  env,
5
6
  head,
6
7
  quote,
@@ -1017,3 +1018,88 @@ ${it.processCostByModel.map((m) => `- ${m.model}: ${m.cost}\n`).join("")}
1017
1018
  Print the closing message and stop — this writes nothing itself.
1018
1019
  `
1019
1020
  }
1021
+
1022
+ // ── The seam phase (release flag `naive_pass`) ──────────────────────────────
1023
+
1024
+ const listed = (paths: readonly string[], unreadable: readonly string[] = []): string =>
1025
+ paths
1026
+ .map(
1027
+ (path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`,
1028
+ )
1029
+ .join("\n")
1030
+
1031
+ export interface SeamRejection {
1032
+ readonly strays: readonly string[]
1033
+ readonly p: number
1034
+ }
1035
+
1036
+ export const naivePassPrompt = (rejection?: SeamRejection): string =>
1037
+ `${stateFileRules}
1038
+ - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1039
+ - Implement them the quickest way that works — the naive version. Do not
1040
+ restructure, abstract or refactor around the change; a later step judges
1041
+ the shape of your diff and a reviewer makes it fit
1042
+ - Add tests as you go, and leave \`.gtd/\` untouched
1043
+ - Leave everything uncommitted and finish your turn — do not commit
1044
+ ${
1045
+ rejection === undefined
1046
+ ? ""
1047
+ : `
1048
+ Your previous attempt was rejected: the judge answered \`reject\` (p=${rejection.p}),
1049
+ meaning the naive pass solved the task the wrong way — discard it and
1050
+ re-implement. It changed these files outside the expected set:
1051
+ ${listed(rejection.strays)}
1052
+ Start over and choose a different approach; do not rebuild the same diff.
1053
+ `
1054
+ }`
1055
+
1056
+ export const implementBuildingPrompt = (reference: string): string =>
1057
+ `${stateFileRules}
1058
+ - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1059
+ - Implement them properly, fitting the codebase's seams and conventions
1060
+ - Reference only: \`git show ${reference}\` shows an earlier naive attempt.
1061
+ Read it for ideas; never apply, cherry-pick or check it out — write
1062
+ the change fresh
1063
+ - Put the new behaviour behind a new release flag, following the
1064
+ repository's flag conventions
1065
+ - Write the tests first, then the code that makes them pass
1066
+ - Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
1067
+ finish your turn
1068
+ `
1069
+
1070
+ export const implementFixPrompt = (): string =>
1071
+ `${stateFileRules}
1072
+ ${fixFeedbackPrompt}
1073
+ - Leave everything uncommitted and finish your turn — do not commit
1074
+ `
1075
+
1076
+ export const seamSteerMessage = (rejection: SeamRejection): string =>
1077
+ `The naive pass was rejected three times and the retries are spent.
1078
+ The last attempt changed these files outside the expected set:
1079
+ ${listed(rejection.strays)}
1080
+
1081
+ Two ways forward (then run \`gtd land\`):
1082
+ - **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
1083
+ pass runs again with a fresh retry budget
1084
+ - **Overrule** — land a clean tree to accept the last attempt's approach:
1085
+ the implementation is built against it as reference, with no seam gate
1086
+ and no refactor. Risk: this ships the seam the judge objected to,
1087
+ unaddressed — the choice is recorded in history.
1088
+
1089
+ An edit together with an approval counts as steer. Only
1090
+ \`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
1091
+ branch unjudged.
1092
+ `
1093
+
1094
+ export const seamByHandMessage = (
1095
+ strays: readonly string[],
1096
+ unreadable: readonly string[],
1097
+ reference: string,
1098
+ ): string =>
1099
+ `The judge could not read some of these files, so the diff was never
1100
+ fully judged and no refactor runs. Unreadable ones are marked:
1101
+ ${listed(strays, unreadable)}
1102
+
1103
+ See them with \`git show ${reference} -- <path>\` and review by hand. Land a
1104
+ clean tree to go on to the implementation, which runs next.
1105
+ `
@@ -12,6 +12,10 @@ export const defaults: Readonly<Record<string, string>> = {
12
12
  reviewNoteActionable: "0.7",
13
13
  reviewSeverityMinP: "0.7",
14
14
  refactorFlagPattern: "\\bGTD_FLAG_\\w+",
15
+ seamAllow:
16
+ "**/*.test.*, **/*.spec.*, **/*.feature, **/tests/**, **/test/**, **/__tests__/**, .gtd/**",
17
+ // GTD_FLAG_NAIVE_PASS, recorded at process start; only "1" is on.
18
+ flag_naive_pass: "0",
15
19
  // Decides the turn count: one `build.quality.<lens>` scope per entry, each
16
20
  // keyed in `./skills.ts`.
17
21
  qualityReviews:
@@ -1,7 +1,45 @@
1
- import { Given } from "quickpickle"
1
+ import { After, Given, Then } from "quickpickle"
2
2
  import { CONFIG_SCAFFOLD } from "../../install/index.js"
3
+ import assert from "node:assert"
3
4
  import type { TestkitWorld } from "../../testkit/index.js"
4
5
 
5
6
  Given("the shipped workflows, as gtd install scaffolds them", (world: TestkitWorld) => {
6
7
  world.configure(CONFIG_SCAFFOLD)
7
8
  })
9
+
10
+ // Ambient, not explicit: the repository's own env is fixed when it is created,
11
+ // and only a real environment variable can change between two steps of one process.
12
+ const restores: (() => void)[] = []
13
+
14
+ Given(
15
+ "the ambient environment variable {string} is {string}",
16
+ (_: TestkitWorld, name: string, value: string) => {
17
+ const before = process.env[name]
18
+ restores.push(() => {
19
+ if (before === undefined) delete process.env[name]
20
+ else process.env[name] = before
21
+ })
22
+ process.env[name] = value
23
+ },
24
+ )
25
+
26
+ After(() => {
27
+ while (restores.length > 0) restores.pop()!()
28
+ })
29
+
30
+ Then("the last commit body contains {string}", (world: TestkitWorld, text: string) => {
31
+ const message = world.repo().messages().at(-1) ?? ""
32
+ assert.ok(message.includes(text), message)
33
+ })
34
+
35
+ Then(
36
+ "the commit {string} has a body containing {string}",
37
+ (world: TestkitWorld, subject: string, text: string) => {
38
+ const message = world
39
+ .repo()
40
+ .messages()
41
+ .find((m) => m.split("\n")[0] === subject)
42
+ assert.ok(message !== undefined, `no commit "${subject}"`)
43
+ assert.ok(message.includes(text), message)
44
+ },
45
+ )