@pmelab/gtd 50.0.0 → 51.0.1

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 (55) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +32 -24
  3. package/dist/chunks/{reserved-CdOzKFVX.mjs → reserved-CUkk9XgL.mjs} +93 -4
  4. package/dist/chunks/{review-BChXEl2Y.mjs → review-BaCXlIT2.mjs} +2 -2
  5. package/dist/chunks/{tail-DRM2eC1z.mjs → tail-CX6Pdsql.mjs} +1 -1
  6. package/dist/gtd.bundle.mjs +26 -25
  7. package/dist/testkit.mjs +26 -25
  8. package/dist/workflows/cleanup.mjs +2 -2
  9. package/dist/workflows/feature.mjs +144 -11
  10. package/dist/workflows/fix.mjs +2 -2
  11. package/dist/workflows/review.mjs +3 -3
  12. package/dist/workflows/rollout.mjs +1 -1
  13. package/dist/workflows.mjs +234 -12
  14. package/package.json +1 -1
  15. package/src/edge/Edge.ts +1 -30
  16. package/src/replay/Replay.test.ts +4 -8
  17. package/src/replay/Replay.ts +35 -3
  18. package/src/replay/index.ts +7 -1
  19. package/src/workflows/feature/coverage.feature +1 -0
  20. package/src/workflows/feature/fast-path.feature +1 -0
  21. package/src/workflows/feature/feature.ts +4 -1
  22. package/src/workflows/feature/flag-mode.feature +1 -1
  23. package/src/workflows/feature/lens-models.feature +1 -0
  24. package/src/workflows/feature/lens-review.feature +1 -0
  25. package/src/workflows/feature/one-step-land.feature +1 -0
  26. package/src/workflows/feature/prototype-rounds.feature +152 -0
  27. package/src/workflows/feature/prototype.feature +419 -0
  28. package/src/workflows/feature/prototype.test.ts +43 -0
  29. package/src/workflows/feature/prototype.ts +181 -0
  30. package/src/workflows/feature/refactor-absorbs.feature +9 -1
  31. package/src/workflows/feature/refactor-first.feature +13 -3
  32. package/src/workflows/feature/refactor-rebuild.feature +29 -5
  33. package/src/workflows/feature/refactor-review.feature +111 -13
  34. package/src/workflows/feature/refactor.test.ts +14 -4
  35. package/src/workflows/feature/refactor.ts +2 -0
  36. package/src/workflows/feature/review-answers.feature +1 -0
  37. package/src/workflows/feature/review-gate.feature +1 -0
  38. package/src/workflows/feature/reviewer-walkthrough.feature +1 -0
  39. package/src/workflows/feature/seam-rejection.feature +1 -0
  40. package/src/workflows/feature/seam.feature +1 -0
  41. package/src/workflows/feature/seam.test.ts +3 -2
  42. package/src/workflows/feature/seam.ts +19 -5
  43. package/src/workflows/feature/severity-budget.feature +1 -0
  44. package/src/workflows/feature/severity-sorted-review.feature +1 -0
  45. package/src/workflows/feature/slice-findings.feature +1 -0
  46. package/src/workflows/feature/slicing-cap.feature +9 -0
  47. package/src/workflows/feature/slicing.feature +1 -0
  48. package/src/workflows/feature/standards.feature +1 -0
  49. package/src/workflows/shared/options.ts +1 -0
  50. package/src/workflows/shared/prose.ts +6 -0
  51. package/src/workflows/shared/skills.test.ts +4 -0
  52. package/src/workflows/shared/skills.ts +5 -0
  53. package/src/workflows/shared/steps.ts +1 -0
  54. package/src/workflows/shared/text.ts +104 -3
  55. package/src/workflows/shared/vars.ts +3 -0
package/src/edge/Edge.ts CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  isRecordableApproval,
38
38
  parseCommitMessage,
39
39
  replay,
40
+ scopeRunStart,
40
41
  sliceRanges,
41
42
  treeFromRecord,
42
43
  type EpisodeCommit,
@@ -791,36 +792,6 @@ const stepDefOf = (step: ReachedStep): StepDef => {
791
792
  }
792
793
  }
793
794
 
794
- /** Is `name` inside `scope`'s subtree? The root scope `""` holds everything. */
795
- const inScope = (name: string, scope: string): boolean =>
796
- scope === "" || name === scope || name.startsWith(`${scope}.`)
797
-
798
- /**
799
- * Where the current unbroken run of rests inside `scope`'s subtree began —
800
- * a dip into a descendant scope does not break it, a sibling or ancestor
801
- * does, and so does a fresh `scope()` call of the same scope.
802
- */
803
- const scopeRunStart = (trace: readonly ReachedStep[], scope: string): number => {
804
- let start = -1
805
- for (let k = 0; k < trace.length; k++) {
806
- if (!inScope(trace[k]!.memoryScope, scope)) continue
807
- const previous = trace[k - 1]
808
- const unbroken =
809
- previous !== undefined &&
810
- inScope(previous.memoryScope, scope) &&
811
- scopeCallOf(previous, scope) === scopeCallOf(trace[k]!, scope)
812
- if (!unbroken) start = k
813
- }
814
- return start
815
- }
816
-
817
- /** The innermost `scope()` call enclosing `scope` at `step` — a new call is a new conversation. */
818
- const scopeCallOf = (step: ReachedStep, scope: string): number | undefined => {
819
- let call: number | undefined
820
- for (const entry of step.scopeCalls) if (inScope(scope, entry.prefix)) call = entry.call
821
- return call
822
- }
823
-
824
795
  // Shown for the root scope, which has no name of its own.
825
796
  const ROOT_MEMORY_SCOPE_NAME = "root"
826
797
 
@@ -940,7 +940,7 @@ describe("replay: skills resolution", () => {
940
940
  expect(await wireSkills(inScope({ name: "a" }, step), { skills: { a: [] } })).toBeUndefined()
941
941
  })
942
942
 
943
- it("fails two agent steps in one memory scope with different lists", async () => {
943
+ it("runs two scope() calls of one memory scope with different lists as two conversations", async () => {
944
944
  const flow: Flow = async () => {
945
945
  await scope({ name: "a", skills: ["one"] }, async () => {
946
946
  await agent("first", "prompt")
@@ -959,9 +959,7 @@ describe("replay: skills resolution", () => {
959
959
  start: hashOf(0),
960
960
  budgetBytes: 1024,
961
961
  })
962
- expect(outcome.kind === "failed" && outcome.message).toContain(
963
- "different model, system prompt, skills or file access",
964
- )
962
+ expect(outcome.kind).toBe("rest")
965
963
  })
966
964
  })
967
965
 
@@ -1084,7 +1082,7 @@ describe("replay: access resolution", () => {
1084
1082
  expect(outcome.rest.access).toEqual({ read: null, write: ["src/a.ts"] })
1085
1083
  })
1086
1084
 
1087
- it("fails two agent steps in one memory scope with different scope access", async () => {
1085
+ it("runs two scope() calls of one memory scope with different access as two conversations", async () => {
1088
1086
  const flow: Flow = async () => {
1089
1087
  await scope({ name: "a", access: { write: ["one"] } }, () => agent("first", "prompt"))
1090
1088
  await scope({ name: "a", access: { write: ["two"] } }, () => agent("second", "prompt"))
@@ -1092,9 +1090,7 @@ describe("replay: access resolution", () => {
1092
1090
  const history = new History()
1093
1091
  history.land("agent", "a.first", 1, "a.second", { "x.txt": "x" })
1094
1092
  const outcome = await restOf(flow, {}, history)
1095
- expect(outcome.kind === "failed" && outcome.message).toContain(
1096
- "different model, system prompt, skills or file access",
1097
- )
1093
+ expect(outcome.kind).toBe("rest")
1098
1094
  })
1099
1095
 
1100
1096
  it("does not conflict two steps that differ only in steering file", async () => {
@@ -139,6 +139,36 @@ const memoryScopeOf = (name: string): string => {
139
139
  return dot === -1 ? "" : name.slice(0, dot)
140
140
  }
141
141
 
142
+ /** Is `name` inside `scope`'s subtree? The root scope `""` holds everything. */
143
+ const inScope = (name: string, scope: string): boolean =>
144
+ scope === "" || name === scope || name.startsWith(`${scope}.`)
145
+
146
+ /**
147
+ * Where the current unbroken run of rests inside `scope`'s subtree began —
148
+ * a dip into a descendant scope does not break it, a sibling or ancestor
149
+ * does, and so does a fresh `scope()` call of the same scope.
150
+ */
151
+ export const scopeRunStart = (trace: readonly ReachedStep[], scope: string): number => {
152
+ let start = -1
153
+ for (let k = 0; k < trace.length; k++) {
154
+ if (!inScope(trace[k]!.memoryScope, scope)) continue
155
+ const previous = trace[k - 1]
156
+ const unbroken =
157
+ previous !== undefined &&
158
+ inScope(previous.memoryScope, scope) &&
159
+ scopeCallOf(previous, scope) === scopeCallOf(trace[k]!, scope)
160
+ if (!unbroken) start = k
161
+ }
162
+ return start
163
+ }
164
+
165
+ /** The innermost `scope()` call enclosing `scope` at `step` — a new call is a new conversation. */
166
+ const scopeCallOf = (step: ReachedStep, scope: string): number | undefined => {
167
+ let call: number | undefined
168
+ for (const entry of step.scopeCalls) if (inScope(scope, entry.prefix)) call = entry.call
169
+ return call
170
+ }
171
+
142
172
  class Stop extends Error {}
143
173
 
144
174
  const short = (hash: string): string => hash.slice(0, 7)
@@ -505,14 +535,16 @@ export const replay = async (input: ReplayInput): Promise<ReplayOutcome> => {
505
535
  skills: step.skills,
506
536
  access: unfoldedAccess.get(step),
507
537
  }
508
- const seen = personas.get(step.memoryScope)
538
+ // Keyed like the memory key: a fresh scope() call is a fresh conversation, free to differ.
539
+ const conversation = `${step.memoryScope}#${scopeRunStart(trace, step.memoryScope)}`
540
+ const seen = personas.get(conversation)
509
541
  if (seen === undefined) {
510
- personas.set(step.memoryScope, identity)
542
+ personas.set(conversation, identity)
511
543
  return undefined
512
544
  }
513
545
  return samePersona(seen, identity)
514
546
  ? undefined
515
- : `gtd: "${step.name}" runs with a different model, system prompt, skills or file access than an earlier agent step in memory scope "${step.memoryScope || "root"}" — one scope is one conversation, and a conversation has one identity`
547
+ : `gtd: "${step.name}" runs with a different model, system prompt, skills or file access than an earlier agent step in its conversation in memory scope "${step.memoryScope || "root"}" — a conversation has one identity`
516
548
  }
517
549
 
518
550
  const DIVERGED = "the workflow changed under this process; run `gtd abandon` to start over"
@@ -1,4 +1,10 @@
1
- export { replay, type EpisodeCommit, type ReachedStep, type ReplayOutcome } from "./Replay.js"
1
+ export {
2
+ replay,
3
+ scopeRunStart,
4
+ type EpisodeCommit,
5
+ type ReachedStep,
6
+ type ReplayOutcome,
7
+ } from "./Replay.js"
2
8
  export { treeFromRecord, type TreeView } from "./Tree.js"
3
9
  export {
4
10
  formatCommitMessage,
@@ -9,6 +9,7 @@ Feature: a feature is done only when every acceptance criterion has a passing te
9
9
 
10
10
  Background:
11
11
  Given the shipped workflows, as gtd install scaffolds them
12
+ And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
12
13
  And an environment variable "GTD_QUALITYREVIEWS" set to ""
13
14
  And an environment variable "GTD_FLAGMODE" set to "false"
14
15
  And a file "src/calc.ts" with:
@@ -8,6 +8,7 @@ Feature: a trivial change takes the same few beats at either risk tier
8
8
 
9
9
  Background:
10
10
  Given the shipped workflows, as gtd install scaffolds them
11
+ And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
11
12
  And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
12
13
  And an environment variable "GTD_FLAG_RISK_TIER" set to "1"
13
14
  And a file "src/greeting.ts" with:
@@ -28,6 +28,7 @@ import {
28
28
  } from "./criteria.js"
29
29
  import { design } from "./planning.js"
30
30
  import { buildTail, humanReview, isCode, reUnwind, type ReviewOutcome } from "../shared/review.js"
31
+ import { prototypeRounds } from "./prototype.js"
31
32
  import { implement, implementing, seamBuild } from "./seam.js"
32
33
  import { sliceCap, sliceCriteria, slicing } from "./slicing.js"
33
34
  import { recordRisk, riskTierOn, type RiskTier } from "./tier.js"
@@ -36,7 +37,8 @@ import { recordRisk, riskTierOn, type RiskTier } from "./tier.js"
36
37
  // `unwind` reverts the sketch (its intent survives in history) → a
37
38
  // green-baseline gate → design → per slice of the criteria, a naive pass,
38
39
  // judged with `flagMode` on: it lands as the implementation, or (refactoring
39
- // first when the judge asks) is built fresh from → the health loop → the
40
+ // first when the judge asks) is built fresh from → the health loop → a
41
+ // hands-on test of every criterion, fixed and re-tested → the
40
42
  // triaged quality lap, whose leftovers are carried on → after the last slice,
41
43
  // a reviewer's walkthrough of the whole change, every slice's findings
42
44
  // among its chunks, for the human, which
@@ -227,6 +229,7 @@ export const ordinaryStart = async (): Promise<void> => {
227
229
  // Read right after idle, so a bad value refuses the landing that would pin it.
228
230
  flagModeOn()
229
231
  sliceCap()
232
+ prototypeRounds()
230
233
  const stated = statedCriteria(changes(), head())
231
234
  await unwind()
232
235
  if (changes(FEEDBACK).some((c) => c.status !== "deleted")) {
@@ -11,7 +11,7 @@ Feature: flag mode off lands the naive pass with no release flag, seam judge or
11
11
  """
12
12
  import { feature } from "@pmelab/gtd/workflows/feature"
13
13
  export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
14
- export default feature({ flagMode: false })
14
+ export default feature({ flagMode: false, prototypeRounds: "0" })
15
15
  """
16
16
  And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
17
17
  And a file "src/calc.ts" with:
@@ -12,6 +12,7 @@ Feature: each quality lens runs on its own model hint
12
12
  export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
13
13
  export default feature({
14
14
  flagMode: false,
15
+ prototypeRounds: "0",
15
16
  qualityReviews: "correctness, conventions, test-audit=opus",
16
17
  })
17
18
  """
@@ -9,6 +9,7 @@ Feature: the quality lap's findings are triaged before the review
9
9
 
10
10
  Background:
11
11
  Given the shipped workflows, as gtd install scaffolds them
12
+ And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
12
13
  And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
13
14
  And an environment variable "GTD_FLAGMODE" set to "false"
14
15
  And a file "src/calc.ts" with:
@@ -10,6 +10,7 @@ Feature: a naive pass the judge rules land is itself the implementation
10
10
 
11
11
  Background:
12
12
  Given the shipped workflows, as gtd install scaffolds them
13
+ And an environment variable "GTD_PROTOTYPEROUNDS" set to "0"
13
14
  And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
14
15
  And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
15
16
  And a file "src/greeting.ts" with:
@@ -0,0 +1,152 @@
1
+ @inmem
2
+ Feature: past its round cap the hands-on test rests at a person's gate
3
+
4
+ `prototypeRounds` caps the fix turns a slice's hands-on test gets. Once they
5
+ are spent and the report still has open items, the process rests at
6
+ implement.prototype.open naming them; a person's fix there is tested again
7
+ with a fresh budget.
8
+
9
+ Background:
10
+ Given the shipped workflows, as gtd install scaffolds them
11
+ And an environment variable "GTD_QUALITYREVIEWS" set to "correctness"
12
+ And an environment variable "GTD_FLAGMODE" set to "false"
13
+ And an environment variable "GTD_PROTOTYPEROUNDS" set to "1"
14
+ And a file "src/calc.ts" with:
15
+ """
16
+ export const add = (a: number, b: number) => a + b
17
+ """
18
+ And the working tree is committed as "feat: add calculator"
19
+ And a file "TODO.md" with:
20
+ """
21
+ # Add subtract
22
+
23
+ subtract(a, b) returns the difference.
24
+ """
25
+ And step "unwind" changes nothing
26
+ And step "start-gate.check" changes nothing
27
+ And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
28
+ """
29
+ # Requirements
30
+
31
+ ## Acceptance Criteria
32
+
33
+ - AC-1: subtract(a, b) returns a - b.
34
+ - AC-2: add is unchanged.
35
+ """
36
+ And step "design.gate.answer" changes nothing
37
+ And step "seam.naive" writes "src/calc.ts" with:
38
+ """
39
+ export const add = (a: number, b: number) => a + b
40
+ export const subtract = (a: number, b: number) => a + b
41
+ """
42
+ And step "seam.naive" writes "src/calc.test.ts" with:
43
+ """
44
+ it("[AC-1] subtracts", () => expect(subtract(3, 1)).toBe(2))
45
+ it("[AC-2] adds", () => expect(add(1, 1)).toBe(2))
46
+ """
47
+ And step "seam.check" changes nothing
48
+ And step "implement.applying" runs its script
49
+ And step "implement.health.check" changes nothing
50
+ And judge step "implement.coverage" answers:
51
+ """
52
+ [{"id":"AC-1","answer":"yes","p":0.9},{"id":"AC-2","answer":"yes","p":0.9}]
53
+ """
54
+ And step "implement.closing" runs its script
55
+
56
+ Scenario: past the round cap a person is asked, naming the open items
57
+ Given step "implement.prototype.testing", when prompted with "Find how this project", writes ".gtd/PROTOTYPE.md" with:
58
+ """
59
+ ## AC-1: fail
60
+
61
+ `subtract(3, 1)` printed 4. Expected 2.
62
+
63
+ ## AC-2: pass
64
+
65
+ Printed 2.
66
+ """
67
+ And step "implement.prototype.testing", when prompted with "This is a re-test", writes ".gtd/PROTOTYPE.md" with:
68
+ """
69
+ ## AC-1: fail
70
+
71
+ Re-tested: `subtract(3, 1)` printed -2. Expected 2.
72
+
73
+ ## AC-2: pass
74
+
75
+ Printed 2.
76
+ """
77
+ And step "implement.prototype.fix.fixing" writes "src/calc.ts" with:
78
+ """
79
+ export const add = (a: number, b: number) => a + b
80
+ export const subtract = (a: number, b: number) => b - a
81
+ """
82
+ And step "implement.prototype.health.check" changes nothing
83
+ When the process is driven
84
+ Then the process rests at "implement.prototype.open"
85
+ And the resting step's content contains "- AC-1: fail"
86
+ And the resting step's content contains "after 1 fix round"
87
+ And the resting step's content does not contain "AC-2"
88
+
89
+ Scenario: a person's change at the gate is tested again
90
+ Given step "implement.prototype.testing", when prompted with "Find how this project", writes ".gtd/PROTOTYPE.md" with:
91
+ """
92
+ ## AC-1: blocked
93
+
94
+ `subtract` is not exported from any entry point I could run.
95
+
96
+ ## AC-2: pass
97
+
98
+ Printed 2.
99
+ """
100
+ And step "implement.prototype.open" writes "src/calc.ts" with:
101
+ """
102
+ export const add = (a: number, b: number) => a + b
103
+ export const subtract = (a: number, b: number) => a - b
104
+ """
105
+ And step "implement.prototype.health.check" changes nothing
106
+ And step "implement.prototype.testing", when prompted with "This is a re-test", writes ".gtd/PROTOTYPE.md" with:
107
+ """
108
+ ## AC-1: pass
109
+
110
+ `subtract(3, 1)` printed 2.
111
+
112
+ ## AC-2: pass
113
+
114
+ Printed 2.
115
+ """
116
+ When the process is driven
117
+ Then the process rests at "build.quality.correctness.reviewing"
118
+ And the commit subjects end with:
119
+ """
120
+ gtd(agent): implement.prototype.testing → implement.prototype.open
121
+ gtd(human): implement.prototype.open → implement.prototype.health.check
122
+ gtd(check): implement.prototype.health.check → implement.prototype.testing
123
+ gtd(agent): implement.prototype.testing → implement.closing
124
+ gtd(check): implement.closing → build.quality.correctness.reviewing
125
+ """
126
+
127
+ Scenario: a re-test that sees exactly what the last round saw still ends the round
128
+ Given step "implement.prototype.testing" writes ".gtd/PROTOTYPE.md" with:
129
+ """
130
+ ## AC-1: fail
131
+
132
+ `subtract(3, 1)` printed 4. Expected 2.
133
+
134
+ ## AC-2: pass
135
+
136
+ Printed 2.
137
+ """
138
+ And step "implement.prototype.fix.fixing" writes "src/calc.ts" with:
139
+ """
140
+ export const add = (a: number, b: number) => a + b
141
+ export const subtract = (a: number, b: number) => b - a
142
+ """
143
+ And step "implement.prototype.health.check" changes nothing
144
+ When the process is driven
145
+ Then the process rests at "implement.prototype.open"
146
+ And the commit subjects end with:
147
+ """
148
+ gtd(agent): implement.prototype.testing → implement.prototype.fix.fixing
149
+ gtd(agent): implement.prototype.fix.fixing → implement.prototype.health.check
150
+ gtd(check): implement.prototype.health.check → implement.prototype.testing
151
+ gtd(agent): implement.prototype.testing → implement.prototype.open
152
+ """