@pmelab/gtd 27.2.0 → 27.4.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 (67) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +14 -1
  3. package/dist/chunks/{reserved-B2QaImnN.mjs → reserved-BPdVZkgv.mjs} +29 -4
  4. package/dist/chunks/{tail-BnhIxCYv.mjs → tail-iLAMoigl.mjs} +1 -1
  5. package/dist/chunks/{wire-BY1S-obW.mjs → wire-C12dahrH.mjs} +6 -4
  6. package/dist/flows.mjs +24 -1
  7. package/dist/gtd.bundle.mjs +174 -23
  8. package/dist/testkit.mjs +327 -27
  9. package/dist/wire.mjs +1 -1
  10. package/dist/workflows/cleanup.mjs +2 -2
  11. package/dist/workflows/feature.mjs +119 -6
  12. package/dist/workflows/fix.mjs +2 -2
  13. package/dist/workflows/review.mjs +2 -2
  14. package/dist/workflows/rollout.mjs +1 -1
  15. package/dist/workflows.mjs +146 -8
  16. package/package.json +1 -1
  17. package/src/cli/Cli.ts +5 -5
  18. package/src/cli/History.ts +5 -0
  19. package/src/cli/Program.ts +1 -0
  20. package/src/edge/Edge.test.ts +160 -0
  21. package/src/edge/Edge.ts +90 -8
  22. package/src/emit/GitScript.test.ts +39 -3
  23. package/src/emit/GitScript.ts +25 -1
  24. package/src/emit/index.ts +2 -0
  25. package/src/flows/helpers.ts +28 -0
  26. package/src/flows/runtime.ts +7 -0
  27. package/src/install/Install.ts +2 -1
  28. package/src/platform/Git.test.ts +25 -1
  29. package/src/platform/Git.ts +30 -0
  30. package/src/platform/index.ts +1 -0
  31. package/src/replay/Format.test.ts +16 -1
  32. package/src/replay/Replay.test.ts +91 -0
  33. package/src/replay/Replay.ts +22 -2
  34. package/src/replay/Trailers.test.ts +110 -1
  35. package/src/replay/Trailers.ts +37 -1
  36. package/src/replay/index.ts +2 -0
  37. package/src/replay/seamGate.test.ts +62 -0
  38. package/src/step/LandStep.ts +1 -0
  39. package/src/step/planStep.test.ts +32 -0
  40. package/src/step/planStep.ts +3 -1
  41. package/src/testkit/EmittedScriptRecognizer.test.ts +27 -0
  42. package/src/testkit/EmittedScriptRecognizer.ts +33 -0
  43. package/src/testkit/EmittedScripts.test.ts +9 -0
  44. package/src/testkit/FakeGitOperations.ts +5 -0
  45. package/src/testkit/GitTiers.ts +158 -0
  46. package/src/testkit/InMemRepo.ts +38 -3
  47. package/src/wire/BeatDocument.test.ts +25 -0
  48. package/src/wire/BeatDocument.ts +4 -2
  49. package/src/wire/BeatPlain.test.ts +1 -0
  50. package/src/wire/BeatStatus.test.ts +2 -0
  51. package/src/wire/BeatStatus.ts +13 -2
  52. package/src/wire/WireSchema.test.ts +10 -0
  53. package/src/wire/types.ts +8 -0
  54. package/src/workflow/Workflow.ts +1 -0
  55. package/src/workflows/feature/index.ts +1 -0
  56. package/src/workflows/feature/refactor.test.ts +466 -0
  57. package/src/workflows/feature/refactor.ts +178 -0
  58. package/src/workflows/index.ts +1 -0
  59. package/src/workflows/shared/access.ts +2 -1
  60. package/src/workflows/shared/health.test.ts +67 -3
  61. package/src/workflows/shared/health.ts +5 -3
  62. package/src/workflows/shared/options.test.ts +8 -0
  63. package/src/workflows/shared/options.ts +1 -0
  64. package/src/workflows/shared/skills.test.ts +6 -0
  65. package/src/workflows/shared/skills.ts +5 -1
  66. package/src/workflows/shared/text.ts +35 -0
  67. package/src/workflows/shared/vars.ts +1 -0
@@ -13,6 +13,8 @@ describe("retryQuestion", () => {
13
13
  })
14
14
  })
15
15
 
16
+ type W = { write(p: string, c: string): void }
17
+
16
18
  describe("gate guard", () => {
17
19
  const config = (guard: string) =>
18
20
  [
@@ -23,9 +25,8 @@ describe("gate guard", () => {
23
25
  ].join("\n")
24
26
 
25
27
  const red = {
26
- first: (_: unknown, r: { write(p: string, c: string): void }) => r.write("f", "1\n"),
27
- "suite.check": (_: unknown, r: { write(p: string, c: string): void }) =>
28
- r.write(".gtd/FEEDBACK.md", "red\n"),
28
+ first: (_: unknown, r: W) => r.write("f", "1\n"),
29
+ "suite.check": (_: unknown, r: W) => r.write(".gtd/FEEDBACK.md", "red\n"),
29
30
  }
30
31
 
31
32
  it("runs the guard after a blocked landing and lets its refusal stop it", async () => {
@@ -50,3 +51,66 @@ describe("gate guard", () => {
50
51
  expect(rest.state).toBe("suite.blocked")
51
52
  })
52
53
  })
54
+
55
+ describe("healthy describeGuard", () => {
56
+ const config = (options: string) =>
57
+ [
58
+ `import { healthy } from "@pmelab/gtd/workflows"`,
59
+ `import { agent, read, refuse } from "@pmelab/gtd/flows"`,
60
+ `const describeGuard = () => { refuse("describe-guarded: nope") }`,
61
+ `const guard = () => { refuse("guarded: nope") }`,
62
+ `export const held = async () => { await agent("first", "go"); await healthy(() => agent("fix", "go"), ${options}) }`,
63
+ `export default held`,
64
+ ].join("\n")
65
+
66
+ let runs = 0
67
+ const handlers = (extra: Record<string, unknown>) => ({
68
+ "health.check": (_: unknown, r: W) => r.write(".gtd/FEEDBACK.md", `red ${runs++}\n`),
69
+ "health.judge": () => '[{"id":"verdict","answer":"progress","p":0.9}]',
70
+ fix: (_: unknown, r: W) => r.write("f", `${runs}\n`),
71
+ first: (_: unknown, r: W) => r.write("start", "1\n"),
72
+ ...extra,
73
+ })
74
+ const make = (options: string) =>
75
+ testRepo({ workflow: config(options), env: { GTD_TEST_COMMAND: "true" } })
76
+
77
+ it("runs after a describe landing and its refusal stops the turn", async () => {
78
+ runs = 0
79
+ const repo = make("{ describeGuard }")
80
+ await repo.start("held")
81
+ await expect(repo.drive(handlers({ "health.describe": () => undefined }))).rejects.toThrow(
82
+ "describe-guarded: nope",
83
+ )
84
+ })
85
+
86
+ it("never runs after the human stop rest", async () => {
87
+ runs = 0
88
+ // Refuses only once a person's edit exists, which only a post-stop run can see.
89
+ const repo = make(
90
+ '{ describeGuard: () => { if (read("human.txt") !== undefined) refuse("describe-guarded: late") } }',
91
+ )
92
+ await repo.start("held")
93
+ const rest = await repo.drive(handlers({ "health.describe": () => undefined }))
94
+ expect(rest.state).toBe("health.stop")
95
+ const after = await repo.drive(
96
+ handlers({ "health.stop": (_: unknown, r: W) => r.write("human.txt", "edit\n") }),
97
+ )
98
+ expect(after.state).not.toBe("health.stop")
99
+ })
100
+
101
+ it("keeps guard running after describe", async () => {
102
+ runs = 0
103
+ // Refuses only once the describe turn has written its analysis.
104
+ const repo = make(
105
+ '{ guard: () => { if (read(".gtd/ESCALATION.md") !== undefined) refuse("guarded: nope") } }',
106
+ )
107
+ await repo.start("held")
108
+ await expect(
109
+ repo.drive(
110
+ handlers({
111
+ "health.describe": (_: unknown, r: W) => r.write(".gtd/ESCALATION.md", "why\n"),
112
+ }),
113
+ ),
114
+ ).rejects.toThrow("guarded: nope")
115
+ })
116
+ })
@@ -38,9 +38,9 @@ export const guarded =
38
38
  guard?.()
39
39
  }
40
40
 
41
- const guardedSteps = (guard?: Guard): kit.EscalationSteps => ({
41
+ const guardedSteps = (guard?: Guard, describeGuard?: Guard): kit.EscalationSteps => ({
42
42
  limit: escalationSteps.limit,
43
- describe: guarded(escalationSteps.describe, guard),
43
+ describe: guarded(guarded(escalationSteps.describe, guard), describeGuard),
44
44
  stop: guarded(escalationSteps.stop, guard),
45
45
  exhausted: guarded(escalationSteps.exhausted, guard),
46
46
  })
@@ -98,6 +98,8 @@ export interface HealthOptions {
98
98
  readonly sweepOnGreen?: readonly string[]
99
99
  /** Runs after every fix and escalation turn. */
100
100
  readonly guard?: Guard | undefined
101
+ /** Runs after the escalation's agent turn only; the human rests stay unguarded. */
102
+ readonly describeGuard?: Guard | undefined
101
103
  }
102
104
 
103
105
  const runSuite = (options: HealthOptions): Promise<boolean> =>
@@ -121,7 +123,7 @@ export const healthy = (fix: () => Promise<void>, options: HealthOptions = {}):
121
123
  fixesSoFar: options.fixesSoFar ?? 0,
122
124
  sameFailure,
123
125
  escalations: options.escalations ?? { rounds: 0 },
124
- escalation: guardedSteps(options.guard),
126
+ escalation: guardedSteps(options.guard, options.describeGuard),
125
127
  })
126
128
 
127
129
  /**
@@ -32,6 +32,14 @@ describe("the shipped workflow factories", () => {
32
32
  })
33
33
  })
34
34
 
35
+ it("default refactorFlagPattern to GTD_FLAG_ matches and let a factory override win", () => {
36
+ expect(defaults.refactorFlagPattern).toBe("\\bGTD_FLAG_\\w+")
37
+ expect(configure(feature({ refactorFlagPattern: "\\bFF_\\w+" }))).toMatchObject({
38
+ ok: true,
39
+ build: { defaults: { refactorFlagPattern: "\\bFF_\\w+" } },
40
+ })
41
+ })
42
+
35
43
  it("build the shipped settings unchanged with no options", () => {
36
44
  for (const factory of [feature, fix, review]) {
37
45
  expect(configure(factory)).toMatchObject({ ok: true, build: { defaults, envDefaults } })
@@ -16,6 +16,7 @@ export const ShippedOptions = Schema.Struct({
16
16
  judgeIdenticalMinP: setting,
17
17
  reviewNoteActionable: setting,
18
18
  reviewSeverityMinP: setting,
19
+ refactorFlagPattern: setting,
19
20
  qualityReviews: setting,
20
21
  testCommand: setting,
21
22
  plannerModel: setting,
@@ -26,6 +26,9 @@ describe("the bundled workflow's skills export", () => {
26
26
  "build.review.fix.nits",
27
27
  "build.review.fix.risks",
28
28
  "notes",
29
+ "refactor",
30
+ "refactor.fix.suite",
31
+ "refactor.health",
29
32
  "build.quality.correctness",
30
33
  "build.quality.owasp-security",
31
34
  "build.quality.ponytail-review",
@@ -57,6 +60,9 @@ describe("the bundled workflow's skills export", () => {
57
60
  "build.review.fix.nits": ["incremental-implementation", "code-simplification"],
58
61
  "build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
59
62
  notes: ["incremental-implementation", "code-simplification"],
63
+ refactor: ["ponytail"],
64
+ "refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
65
+ "refactor.health": ["ponytail", "debugging-and-error-recovery"],
60
66
  })
61
67
  })
62
68
 
@@ -4,7 +4,7 @@ import { lensesOf, lensScopes } from "../../workflow-kit/index.js"
4
4
  // The bundled workflow's default skills, one list per SCOPE, keyed by the
5
5
  // scope's full name — the same name a `config.skills` entry addresses. A
6
6
  // nested scope with no entry inherits its parent's; group scopes
7
- // (`packages.item.fix`, `build.fix`, `build.review.fix`, `build.quality`) run
7
+ // (`packages.item.fix`, `build.fix`, `build.review.fix`, `build.quality`, `refactor.fix`) run
8
8
  // no turn and carry no key. `build.quality.<lens>` is one key per
9
9
  // `qualityReviews` entry, so the export is a function of the vars.
10
10
  export const skills = (
@@ -25,6 +25,10 @@ export const skills = (
25
25
  "build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
26
26
  // `fix`'s turn over the notes a review handed on.
27
27
  notes: ["incremental-implementation", "code-simplification"],
28
+ // Every refactor agent turn loads `ponytail`.
29
+ refactor: ["ponytail"],
30
+ "refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
31
+ "refactor.health": ["ponytail", "debugging-and-error-recovery"],
28
32
  ...lensScopes(
29
33
  "build.quality",
30
34
  lensesOf(vars.qualityReviews ?? ""),
@@ -422,6 +422,41 @@ ${fixFeedbackPrompt}
422
422
  - Leave everything uncommitted and finish your turn — do not commit
423
423
  `
424
424
 
425
+ const refactorRules = (strays: readonly string[], pattern: string): string =>
426
+ `- Behaviour-neutral: the suite's outcome must not change
427
+ - Change only these paths, nothing else — a test file only when it is
428
+ one of them:
429
+ ${strays.map((path) => ` - \`${path}\``).join("\n")}
430
+ - Introduce no new flag read and add no second read of an existing one:
431
+ every match of \`${pattern}\` may move, never increase in number`
432
+
433
+ export const refactorBuildingPrompt = (
434
+ pkg: string,
435
+ strays: readonly string[],
436
+ pattern: string,
437
+ reference?: string,
438
+ ): string =>
439
+ `${stateFileRules}
440
+ - The package to implement is \`${pkg}\`; never delete it
441
+ ${refactorRules(strays, pattern)}
442
+ ${
443
+ reference === undefined
444
+ ? ""
445
+ : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
446
+ earlier attempt at this change. Read it for ideas; never apply,
447
+ cherry-pick or check it out — write a fresh refactor
448
+ `
449
+ }- Leave everything uncommitted and finish your turn — do not commit
450
+ `
451
+
452
+ export const refactorFixPrompt = (strays: readonly string[], pattern: string): string =>
453
+ `${stateFileRules}
454
+ ${fixFeedbackPrompt}
455
+ ${refactorRules(strays, pattern)}
456
+ - Never do another package's work, even when it would green the suite
457
+ - Leave everything uncommitted and finish your turn — do not commit
458
+ `
459
+
425
460
  export const healthJudgeMessage = (): string =>
426
461
  `The check is still red, and this isn't the first round —
427
462
  \`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
@@ -11,6 +11,7 @@ export const defaults: Readonly<Record<string, string>> = {
11
11
  judgeIdenticalMinP: "0.7",
12
12
  reviewNoteActionable: "0.7",
13
13
  reviewSeverityMinP: "0.7",
14
+ refactorFlagPattern: "\\bGTD_FLAG_\\w+",
14
15
  // Decides the turn count: one `build.quality.<lens>` scope per entry, each
15
16
  // keyed in `./skills.ts`.
16
17
  qualityReviews: