@pmelab/gtd 58.1.0 → 59.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 (142) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +115 -390
  3. package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
  4. package/dist/gtd.bundle.mjs +76 -59
  5. package/dist/testkit/gherkin.mjs +30 -2
  6. package/dist/testkit.mjs +1 -1
  7. package/package.json +3 -42
  8. package/skills/authoring/SKILL.md +31 -25
  9. package/src/cli/Cli.test.ts +15 -19
  10. package/src/cli/Cli.ts +2 -4
  11. package/src/cli/Program.test.ts +60 -29
  12. package/src/cli/Program.ts +5 -7
  13. package/src/edge/Edge.ts +3 -3
  14. package/src/flags/index.test.ts +0 -13
  15. package/src/flags/index.ts +1 -3
  16. package/src/gherkin/git-control.feature +78 -0
  17. package/src/gherkin/index.ts +35 -1
  18. package/src/install/Install.test.ts +14 -11
  19. package/src/install/Install.ts +38 -15
  20. package/src/install/index.ts +1 -2
  21. package/src/replay/Replay.test.ts +1 -1
  22. package/src/replay/index.ts +1 -1
  23. package/src/step/planStart.ts +6 -1
  24. package/src/testkit/TestRepo.test.ts +118 -2
  25. package/src/testkit/TestRepo.ts +57 -6
  26. package/src/testkit/index.ts +1 -0
  27. package/src/twins/EmittedScriptRecognizer.ts +12 -14
  28. package/src/twins/FlowScripts.ts +10 -9
  29. package/src/twins/InMemRepo.ts +26 -1
  30. package/src/twins/Layers.ts +12 -1
  31. package/src/twins/index.ts +6 -1
  32. package/src/workflow/index.ts +1 -0
  33. package/src/workflow/load.test.ts +138 -113
  34. package/src/workflow/load.ts +1 -1
  35. package/src/workflow/vars.test.ts +30 -3
  36. package/src/workflow/vars.ts +34 -2
  37. package/src/workflow-kit/index.ts +1 -1
  38. package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
  39. package/dist/chunks/review-Bfaror38.mjs +0 -427
  40. package/dist/chunks/tail-Bd7L940p.mjs +0 -355
  41. package/dist/chunks/testkit-D1-auYau.mjs +0 -10362
  42. package/dist/workflows/cleanup.mjs +0 -36
  43. package/dist/workflows/feature.mjs +0 -1018
  44. package/dist/workflows/fix.mjs +0 -41
  45. package/dist/workflows/review.mjs +0 -45
  46. package/dist/workflows/rollout.mjs +0 -48
  47. package/dist/workflows.mjs +0 -40651
  48. package/src/install/ConfigScaffold.ts +0 -22
  49. package/src/workflows/cleanup/cleanup-path.feature +0 -94
  50. package/src/workflows/cleanup/cleanup.feature +0 -315
  51. package/src/workflows/cleanup/cleanup.test.ts +0 -276
  52. package/src/workflows/cleanup/cleanup.ts +0 -52
  53. package/src/workflows/cleanup/index.ts +0 -6
  54. package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
  55. package/src/workflows/feature/acceptance-criteria.feature +0 -389
  56. package/src/workflows/feature/criteria-coverage.feature +0 -102
  57. package/src/workflows/feature/criteria.test.ts +0 -147
  58. package/src/workflows/feature/criteria.ts +0 -267
  59. package/src/workflows/feature/fast-path.feature +0 -93
  60. package/src/workflows/feature/feature.test.ts +0 -338
  61. package/src/workflows/feature/feature.ts +0 -156
  62. package/src/workflows/feature/green.ts +0 -19
  63. package/src/workflows/feature/index.ts +0 -18
  64. package/src/workflows/feature/lens-models.feature +0 -69
  65. package/src/workflows/feature/lens-review.feature +0 -566
  66. package/src/workflows/feature/naive-acceptance.feature +0 -147
  67. package/src/workflows/feature/naive-test.feature +0 -263
  68. package/src/workflows/feature/one-step-land.feature +0 -93
  69. package/src/workflows/feature/planning.ts +0 -41
  70. package/src/workflows/feature/refactor-absorbs.feature +0 -131
  71. package/src/workflows/feature/refactor-first.feature +0 -158
  72. package/src/workflows/feature/refactor-lens-models.feature +0 -79
  73. package/src/workflows/feature/refactor-rebuild.feature +0 -273
  74. package/src/workflows/feature/refactor-review.feature +0 -1528
  75. package/src/workflows/feature/refactor.test.ts +0 -1220
  76. package/src/workflows/feature/refactor.ts +0 -368
  77. package/src/workflows/feature/release-flag.feature +0 -167
  78. package/src/workflows/feature/release-flag.test.ts +0 -49
  79. package/src/workflows/feature/release-flag.ts +0 -46
  80. package/src/workflows/feature/review-gate.feature +0 -241
  81. package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
  82. package/src/workflows/feature/risk-tier.feature +0 -351
  83. package/src/workflows/feature/seam-rejection.feature +0 -146
  84. package/src/workflows/feature/seam.feature +0 -297
  85. package/src/workflows/feature/seam.test.ts +0 -527
  86. package/src/workflows/feature/seam.ts +0 -426
  87. package/src/workflows/feature/standards.feature +0 -354
  88. package/src/workflows/feature/steps.test.ts +0 -65
  89. package/src/workflows/feature/steps.ts +0 -45
  90. package/src/workflows/feature/tier-pin.feature +0 -66
  91. package/src/workflows/feature/tier.test.ts +0 -166
  92. package/src/workflows/feature/tier.ts +0 -56
  93. package/src/workflows/fix/fix-lens-origin.feature +0 -114
  94. package/src/workflows/fix/fix-quality.feature +0 -212
  95. package/src/workflows/fix/fix.feature +0 -79
  96. package/src/workflows/fix/fix.test.ts +0 -74
  97. package/src/workflows/fix/fix.ts +0 -52
  98. package/src/workflows/fix/index.ts +0 -6
  99. package/src/workflows/index.ts +0 -47
  100. package/src/workflows/review/index.ts +0 -6
  101. package/src/workflows/review/review-door.feature +0 -199
  102. package/src/workflows/review/review.test.ts +0 -27
  103. package/src/workflows/review/review.ts +0 -60
  104. package/src/workflows/rollout/index.ts +0 -6
  105. package/src/workflows/rollout/rollout.feature +0 -510
  106. package/src/workflows/rollout/rollout.test.ts +0 -263
  107. package/src/workflows/rollout/rollout.ts +0 -82
  108. package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
  109. package/src/workflows/shared/access.test.ts +0 -62
  110. package/src/workflows/shared/access.ts +0 -24
  111. package/src/workflows/shared/config.fixture.ts +0 -12
  112. package/src/workflows/shared/fences.ts +0 -24
  113. package/src/workflows/shared/health.test.ts +0 -148
  114. package/src/workflows/shared/health.ts +0 -191
  115. package/src/workflows/shared/learn.test.ts +0 -140
  116. package/src/workflows/shared/learn.ts +0 -201
  117. package/src/workflows/shared/lenses.test.ts +0 -39
  118. package/src/workflows/shared/lenses.ts +0 -294
  119. package/src/workflows/shared/options.test.ts +0 -157
  120. package/src/workflows/shared/options.ts +0 -92
  121. package/src/workflows/shared/progress.feature +0 -124
  122. package/src/workflows/shared/prose.ts +0 -263
  123. package/src/workflows/shared/recheck.feature +0 -60
  124. package/src/workflows/shared/registry.test.ts +0 -345
  125. package/src/workflows/shared/registry.ts +0 -183
  126. package/src/workflows/shared/reserved.ts +0 -11
  127. package/src/workflows/shared/review.test.ts +0 -1062
  128. package/src/workflows/shared/review.ts +0 -446
  129. package/src/workflows/shared/skills.test.ts +0 -121
  130. package/src/workflows/shared/skills.ts +0 -49
  131. package/src/workflows/shared/standards.test.ts +0 -90
  132. package/src/workflows/shared/standards.ts +0 -156
  133. package/src/workflows/shared/steps.test.ts +0 -219
  134. package/src/workflows/shared/steps.ts +0 -201
  135. package/src/workflows/shared/tail.test.ts +0 -454
  136. package/src/workflows/shared/tail.ts +0 -264
  137. package/src/workflows/shared/text.fixture.ts +0 -128
  138. package/src/workflows/shared/text.test.ts +0 -1430
  139. package/src/workflows/shared/text.ts +0 -1700
  140. package/src/workflows/shared/threads.test.ts +0 -129
  141. package/src/workflows/shared/vars.ts +0 -47
  142. package/src/workflows/shared/workflows.steps.ts +0 -54
@@ -1,90 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
- import {
3
- conflictFor,
4
- conflictTitle,
5
- parseStandards,
6
- renderStandards,
7
- standardsBlock,
8
- } from "./standards.js"
9
-
10
- const conflict = { text: "Calculators are functions.", olds: ["Calculators are classes."] }
11
-
12
- describe("standards file", () => {
13
- it("round-trips decided and undecided standards", () => {
14
- const standards = { decided: ["Prefer early returns."], undecided: [conflict] }
15
- expect(parseStandards(renderStandards(standards))).toEqual(standards)
16
- })
17
-
18
- it("reads a hand-edited file: any heading groups decided ones, wrapped lines join", () => {
19
- const text = [
20
- "# Standards",
21
- "",
22
- "Some prose.",
23
- "",
24
- "## Testing",
25
- "",
26
- "1. Every bug fix",
27
- " gets a test.",
28
- "",
29
- "## Undecided",
30
- "",
31
- "- New",
32
- " - contradicts: Old",
33
- ].join("\n")
34
- expect(parseStandards(text)).toEqual({
35
- decided: ["Every bug fix gets a test."],
36
- undecided: [{ text: "New", olds: ["Old"] }],
37
- })
38
- })
39
- })
40
-
41
- describe("a formatter-wrapped file", () => {
42
- it("keeps a wrapped contradicts line with its old standard", () => {
43
- const text = [
44
- "## Undecided",
45
- "",
46
- "- Calculators are plain functions. (re: subtract has no class)",
47
- " - contradicts: Won't fix: subtract has no class — Calculators read better as",
48
- " classes.",
49
- ].join("\n")
50
- expect(parseStandards(text).undecided).toEqual([
51
- {
52
- text: "Calculators are plain functions. (re: subtract has no class)",
53
- olds: ["Won't fix: subtract has no class — Calculators read better as classes."],
54
- },
55
- ])
56
- })
57
- })
58
-
59
- describe("conflict notes", () => {
60
- it("keep their title when a formatter rewrites the emphasis", () => {
61
- const standards = { decided: [], undecided: [{ text: "Use _named_ exports.", olds: [] }] }
62
- expect(
63
- conflictFor(standards, conflictTitle({ text: "Use *named* exports.", olds: [] })),
64
- ).toBeDefined()
65
- })
66
-
67
- it("are one per standard, however often the file lists it", () => {
68
- const text = "## Undecided\n\n- New\n - contradicts: Old\n- New\n - contradicts: Old\n"
69
- expect(parseStandards(text).undecided).toHaveLength(1)
70
- })
71
-
72
- it("title back to the conflict it asks about", () => {
73
- const standards = { decided: [], undecided: [conflict] }
74
- expect(conflictFor(standards, conflictTitle(conflict))).toBe(conflict)
75
- expect(conflictFor(standards, "Standard conflict 00000000")).toBeUndefined()
76
- })
77
- })
78
-
79
- describe("standardsBlock", () => {
80
- it("is empty without decided standards", () => {
81
- expect(standardsBlock(undefined)).toBe("")
82
- expect(standardsBlock(renderStandards({ decided: [], undecided: [conflict] }))).toBe("")
83
- })
84
-
85
- it("lists only the decided ones", () => {
86
- const block = standardsBlock(renderStandards({ decided: ["A."], undecided: [conflict] }))
87
- expect(block).toContain("- A.")
88
- expect(block).not.toContain(conflict.text)
89
- })
90
- })
@@ -1,156 +0,0 @@
1
- import { ANSWER_LEAD, FREE_TEXT_PLACEHOLDER } from "../../steering/index.js"
2
-
3
- export const STANDARDS = ".gtd/STANDARDS.md"
4
-
5
- /** A learned standard that contradicts decided ones, waiting on the human. */
6
- export interface Conflict {
7
- readonly text: string
8
- /** The decided standards it contradicts. */
9
- readonly olds: readonly string[]
10
- }
11
-
12
- export interface Standards {
13
- readonly decided: readonly string[]
14
- readonly undecided: readonly Conflict[]
15
- }
16
-
17
- const UNDECIDED = "Undecided"
18
-
19
- // The same class as the review's `Gtd-Mark` anchors, so a title reads alike in both.
20
- export const oneLine = (text: string): string =>
21
- // eslint-disable-next-line no-control-regex -- the control range IS the point
22
- text.replace(/[\s\x00-\x1f\x7f-\x9f\u2028\u2029]+/g, " ").trim()
23
-
24
- const ITEM = /^(?:[-*+]|\d+[.)])\s+(.*)$/
25
- const CONTRADICTS = /^\s+[-*+]\s+contradicts:\s*(.*)$/i
26
-
27
- interface Item {
28
- text: string
29
- /** Set under `## Undecided`. */
30
- olds?: string[]
31
- }
32
-
33
- // A formatter wraps long lines: the tail belongs to the last `contradicts:` once there is one.
34
- const wrapped = (item: Item, line: string): void => {
35
- const olds = item.olds
36
- if (olds !== undefined && olds.length > 0) olds[olds.length - 1] += ` ${line.trim()}`
37
- else item.text += ` ${line.trim()}`
38
- }
39
-
40
- /** What tells two standards apart: not the emphasis or spacing a formatter may rewrite. */
41
- export const standardKey = (text: string): string =>
42
- oneLine(text.replace(/[*_`\\]/g, "")).toLowerCase()
43
-
44
- // Each standard once, so every conflict note gets a title of its own.
45
- const toStandards = (items: readonly Item[]): Standards => {
46
- const seen = new Set<string>()
47
- const fresh = (text: string): boolean => {
48
- const key = standardKey(text)
49
- if (key === "" || seen.has(key)) return false
50
- seen.add(key)
51
- return true
52
- }
53
- const decided = items.filter((i) => i.olds === undefined).map((i) => oneLine(i.text))
54
- const undecided = items.flatMap((i) =>
55
- i.olds === undefined
56
- ? []
57
- : [{ text: oneLine(i.text), olds: i.olds.map(oneLine).filter((o) => o !== "") }],
58
- )
59
- return { decided: decided.filter(fresh), undecided: undecided.filter((c) => fresh(c.text)) }
60
- }
61
-
62
- /**
63
- * Every top-level bullet is one standard, those under `## Undecided` waiting
64
- * on the human; a hand-edited file may group the rest under any heading.
65
- */
66
- export const parseStandards = (text: string): Standards => {
67
- const items: Item[] = []
68
- let pending = false
69
- let last: Item | undefined
70
- for (const line of text.split("\n")) {
71
- const heading = /^(#{1,6})\s+(.*)$/.exec(line)
72
- const item = ITEM.exec(line)
73
- const old = CONTRADICTS.exec(line)
74
- if (heading !== null) {
75
- pending = heading[1]!.length > 1 && heading[2]!.trim() === UNDECIDED
76
- last = undefined
77
- } else if (item !== null) {
78
- last = pending ? { text: item[1]!, olds: [] } : { text: item[1]! }
79
- items.push(last)
80
- } else if (old !== null && last?.olds !== undefined) last.olds.push(old[1]!)
81
- else if (/^\s+\S/.test(line) && last !== undefined) wrapped(last, line)
82
- else if (line.trim() !== "") last = undefined
83
- }
84
- return toStandards(items)
85
- }
86
-
87
- export const renderStandards = ({ decided, undecided }: Standards): string => {
88
- const parts = [
89
- "# Standards",
90
- "Kept from your review answers for every agent turn. One standard per bullet.",
91
- ...(decided.length === 0 ? [] : [decided.map((s) => `- ${s}`).join("\n")]),
92
- ...(undecided.length === 0
93
- ? []
94
- : [
95
- `## ${UNDECIDED}`,
96
- undecided
97
- .map((c) => [`- ${c.text}`, ...c.olds.map((o) => ` - contradicts: ${o}`)].join("\n"))
98
- .join("\n"),
99
- ]),
100
- ]
101
- return `${parts.join("\n\n")}\n`
102
- }
103
-
104
- // FNV-1a over the key: a conflict's chunk title must survive markdown parsing and a
105
- // formatter's rewrite of STANDARDS.md, which its text may not.
106
- const hash = (raw: string): string => {
107
- const text = standardKey(raw)
108
- let h = 0x811c9dc5
109
- for (let i = 0; i < text.length; i++) h = Math.imul(h ^ text.charCodeAt(i), 0x01000193)
110
- return (h >>> 0).toString(16).padStart(8, "0")
111
- }
112
-
113
- const CONFLICT_TITLE = /^Standard conflict ([0-9a-f]{8})$/
114
-
115
- export const conflictTitle = (c: Conflict): string => `Standard conflict ${hash(c.text)}`
116
-
117
- export const conflictFor = (standards: Standards, title: string): Conflict | undefined => {
118
- const id = CONFLICT_TITLE.exec(title)?.[1]
119
- return id === undefined ? undefined : standards.undecided.find((c) => hash(c.text) === id)
120
- }
121
-
122
- export const isConflictTitle = (title: string): boolean => CONFLICT_TITLE.test(title)
123
-
124
- const KEEP_NEW = "keep the new one (drop the old)"
125
- const KEEP_OLD = "keep the old one (discard the new)"
126
-
127
- /** A conflict's chunk body; its three boxes read as fix / won't fix / own answer by position. */
128
- export const conflictBody = (c: Conflict): string =>
129
- [
130
- ...(c.olds.length === 0
131
- ? [
132
- `A standard learned from your answers contradicted ones another answer since replaced in \`${STANDARDS}\`. Keep it, discard it, or reword it.`,
133
- `New: ${c.text}`,
134
- ]
135
- : [
136
- `A standard learned from your answers contradicts ${c.olds.length === 1 ? "one" : "some"} already in \`${STANDARDS}\`. Your own answer replaces both with its wording.`,
137
- `New: ${c.text}`,
138
- `Old:\n\n${c.olds.map((o) => `- ${o}`).join("\n")}`,
139
- ]),
140
- `${ANSWER_LEAD}\n\n- [ ] ${KEEP_NEW}\n- [ ] ${KEEP_OLD}\n- [ ] ${FREE_TEXT_PLACEHOLDER}`,
141
- ].join("\n\n")
142
-
143
- /** What every agent turn is told while `.gtd/STANDARDS.md` holds decided standards. */
144
- export const standardsBlock = (text: string | undefined): string => {
145
- const { decided } = parseStandards(text ?? "")
146
- if (decided.length === 0) return ""
147
- return `## Standards
148
-
149
- \`${STANDARDS}\` holds the standards the human set in earlier reviews.
150
- Follow every one in what you write. Reviewing, never raise a finding a
151
- standard rules out — a \`Won't fix:\` standard is a finding the human
152
- rejected, so never raise it or one like it again — and raise breaking a
153
- standard as a finding.
154
-
155
- ${decided.map((s) => `- ${s}`).join("\n")}`
156
- }
@@ -1,219 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
- import { installContext, type ScopeOptions, type StepRequest } from "../../flows/index.js"
3
- import { captureStep, fixtureContext } from "./text.fixture.js"
4
- import * as steps from "./steps.js"
5
-
6
- const capture = (
7
- fn: () => Promise<void>,
8
- scope: string,
9
- skills?: Readonly<Record<string, readonly string[]>>,
10
- ): Promise<StepRequest> => captureStep(fn, { scope, ...(skills !== undefined ? { skills } : {}) })
11
-
12
- const agentPrompt = (request: StepRequest): string => {
13
- if (request.kind !== "agent") throw new Error(`expected an agent step, got ${request.kind}`)
14
- return request.prompt
15
- }
16
-
17
- describe("the bundled workflow's steps declare skills — a bundled step's rendered prompt carries its bundled skills.ts preamble", () => {
18
- it("fix carries build.fix's bundled skills", async () => {
19
- const prompt = agentPrompt(await capture(() => steps.fixCheck(), "build"))
20
- expect(prompt).toContain("debugging-and-error-recovery")
21
- })
22
-
23
- it("describeEscalation carries health.describe's bundled skills", async () => {
24
- const prompt = agentPrompt(await capture(() => steps.describeEscalation(), "build"))
25
- expect(prompt).toContain("debugging-and-error-recovery")
26
- })
27
-
28
- it("reviewQuality names its lens scope, build.quality.<lens>.reviewing", async () => {
29
- const request = await capture(() => steps.reviewQuality("owasp-security"), "build")
30
- if (request.kind !== "agent") throw new Error("unreachable")
31
- expect(request.name).toBe("quality.owasp-security.reviewing")
32
- })
33
-
34
- it("an unknown lens's preamble names the lens itself, from the bundled per-lens key", async () => {
35
- const prompt = agentPrompt(await capture(() => steps.reviewQuality("owasp-security"), "build"))
36
- expect(prompt).toContain("missing one: owasp-security")
37
- })
38
-
39
- it("a configured build.quality.<lens> entry replaces the lens's skills in the preamble, every other lens untouched", async () => {
40
- const skills = { "build.quality.owasp-security": ["my-org-checklist"] }
41
- const prompt = agentPrompt(
42
- await capture(() => steps.reviewQuality("owasp-security"), "build", skills),
43
- )
44
- expect(prompt).toContain("my-org-checklist")
45
- expect(prompt).toContain("owasp-security")
46
- })
47
-
48
- it("correctness carries all four trace points", async () => {
49
- const request = await capture(() => steps.reviewQuality("correctness"), "build")
50
- const body = JSON.stringify(request)
51
- for (const point of [
52
- "partial-failure",
53
- "format, not just its presence",
54
- "parallel write paths",
55
- "contract test",
56
- ])
57
- expect(body).toContain(point)
58
- })
59
-
60
- it.each(["conventions", "spec-challenge", "criteria-coverage"])(
61
- "%s carries its brief",
62
- async (lens) => {
63
- const request = await capture(() => steps.reviewQuality(lens), "build")
64
- expect(JSON.stringify(request)).toContain(
65
- steps.builtInLenses[lens]!.brief.split("\n")[0]!.slice(0, 40),
66
- )
67
- },
68
- )
69
-
70
- it("an unknown lens has no brief", async () => {
71
- const request = await capture(() => steps.reviewQuality("my-lens"), "build")
72
- expect(JSON.stringify(request)).not.toContain("This lens's brief")
73
- })
74
-
75
- it("fixQuality carries build.fix.quality's bundled skills", async () => {
76
- const prompt = agentPrompt(await capture(() => steps.fixQuality(), "build"))
77
- expect(prompt).toContain("incremental-implementation, code-simplification")
78
- })
79
-
80
- it("reviewing carries build.review.reviewing's bundled skills", async () => {
81
- const prompt = agentPrompt(await capture(() => steps.reviewing("base"), "build"))
82
- expect(prompt).toContain("code-review-and-quality")
83
- })
84
-
85
- it("an empty configured list leaves that step's skills empty, and its rendered prompt bare", async () => {
86
- const request = await capture(() => steps.fixCheck(), "build", { build: [] })
87
- if (request.kind !== "agent") throw new Error("unreachable")
88
- expect(request.prompt).not.toContain("Load whatever's listed here")
89
- })
90
-
91
- it("fixRisks fixes every risk, tolerates an empty turn, and leaves .gtd/REVIEW.md alone", async () => {
92
- const request = await capture(
93
- () => steps.fixRisks([{ id: "risk-1", anchor: "calc ./a.ts#1-1", text: "Risk: drops it" }]),
94
- "build",
95
- )
96
- if (request.kind !== "agent") throw new Error("unreachable")
97
- expect(request.name).toBe("review.fix.risks.fixing")
98
- expect(request.options).toMatchObject({ allowEmpty: true })
99
- expect(request.prompt).toContain("debugging-and-error-recovery, incremental-implementation")
100
- expect(request.prompt).toContain("risk-1 — calc ./a.ts#1-1")
101
- expect(request.prompt).toContain("Risk: drops it")
102
- expect(request.prompt).toContain("Leave `.gtd/REVIEW.md` untouched")
103
- })
104
-
105
- it("reviewing names the carry-over commit only when given one", async () => {
106
- const bare = await capture(() => steps.reviewing("base"), "build")
107
- const carried = await capture(() => steps.reviewing("base", "abc1234"), "build")
108
- if (bare.kind !== "agent" || carried.kind !== "agent") throw new Error("unreachable")
109
- expect(bare.prompt).not.toContain("Carry-over")
110
- expect(carried.prompt).toContain("Carry-over: commit `abc1234`")
111
- })
112
- })
113
-
114
- describe("the bundled workflow's model hints", () => {
115
- const modelOf = async (
116
- fn: () => Promise<void>,
117
- context: Parameters<typeof captureStep>[1] = {},
118
- ): Promise<string | undefined> => {
119
- const request = await captureStep(fn, { scope: "build", ...context })
120
- if (request.kind !== "agent") throw new Error(`expected an agent step, got ${request.kind}`)
121
- return request.options.model
122
- }
123
-
124
- it.each([
125
- ["correctness", "smart"],
126
- ["owasp-security", "smart"],
127
- ["spec-challenge", "smart"],
128
- ["conventions", "base"],
129
- ["ponytail-review", "base"],
130
- ["test-audit", "base"],
131
- ["my-lens", "smart"],
132
- ])("lens %s defaults to %s", async (lens, model) => {
133
- expect(await modelOf(() => steps.reviewQuality(lens))).toBe(model)
134
- })
135
-
136
- it("a lens's default follows the planner or coder setting it stands for", async () => {
137
- const env = { plannerModel: "opus", coderModel: "sonnet" }
138
- expect(await modelOf(() => steps.reviewQuality("correctness"), { env })).toBe("opus")
139
- expect(await modelOf(() => steps.reviewQuality("conventions"), { env })).toBe("sonnet")
140
- })
141
-
142
- it("a model passed in wins over the lens's default; a blank one is none", async () => {
143
- expect(await modelOf(() => steps.reviewQuality("correctness", undefined, "base"))).toBe("base")
144
- expect(await modelOf(() => steps.reviewQuality("conventions", undefined, "opus"))).toBe("opus")
145
- expect(await modelOf(() => steps.reviewQuality("conventions", undefined, ""))).toBe("base")
146
- })
147
-
148
- it("a lens named like an Object member still gets its default", async () => {
149
- expect(await modelOf(() => steps.reviewQuality("constructor"))).toBe("smart")
150
- })
151
-
152
- it("the fix loop runs on the coder model, base by default", async () => {
153
- expect(await modelOf(() => steps.fixCheck())).toBe("base")
154
- expect(await modelOf(() => steps.fixQuality())).toBe("base")
155
- expect(await modelOf(() => steps.fixCheck(), { env: { coderModel: "sonnet" } })).toBe("sonnet")
156
- })
157
-
158
- it("answerReviewQuestions answers inline in .gtd/REVIEW.md as the reviewer, with its bundled skills", async () => {
159
- const request = await capture(
160
- () =>
161
- steps.answerReviewQuestions([{ id: "note-1", anchor: "calc ./a.ts#1-1", text: "why?" }]),
162
- "build",
163
- )
164
- if (request.kind !== "agent") throw new Error("unreachable")
165
- expect(request.name).toBe("review.answer-review-questions")
166
- expect(request.options).toMatchObject({
167
- file: ".gtd/REVIEW.md",
168
- mode: "review",
169
- })
170
- expect(request.prompt).toContain("code-review-and-quality")
171
- expect(request.prompt).toContain("note-1 — calc ./a.ts#1-1")
172
- expect(request.prompt).toContain("`A: ` line")
173
- expect(request.prompt).toContain("gtd check review .gtd/REVIEW.md")
174
- })
175
-
176
- it("fixNits fixes every nit in one turn and leaves .gtd/REVIEW.md alone", async () => {
177
- const request = await capture(
178
- () =>
179
- steps.fixNits([
180
- { id: "note-1", anchor: "calc ./a.ts#1-1", text: "typo" },
181
- { id: "note-2", anchor: "calc ./a.ts#2-2", text: "semicolon" },
182
- ]),
183
- "build",
184
- )
185
- if (request.kind !== "agent") throw new Error("unreachable")
186
- expect(request.name).toBe("review.fix.nits.fixing")
187
- expect(request.prompt).toContain("incremental-implementation, code-simplification")
188
- expect(request.prompt).toContain("typo")
189
- expect(request.prompt).toContain("semicolon")
190
- expect(request.prompt).toContain("Leave `.gtd/REVIEW.md` untouched")
191
- })
192
-
193
- it("fixNits with access runs in a review.fix.nits scope, the step named fixing inside it", async () => {
194
- const scopes: ScopeOptions[] = []
195
- const steps_: StepRequest[] = []
196
- const access = { write: ["src/a.ts", ".gtd/**"] }
197
- installContext(
198
- fixtureContext(
199
- { scope: "refactor.build" },
200
- {
201
- step: (request) => {
202
- steps_.push(request)
203
- return Promise.resolve()
204
- },
205
- pushScope: (s) => void scopes.push(s),
206
- popScope: () => undefined,
207
- changesSince: () => [],
208
- },
209
- ),
210
- )
211
- try {
212
- await steps.fixNits([], access)
213
- } finally {
214
- installContext(undefined)
215
- }
216
- expect(scopes).toEqual([{ name: "review.fix.nits", access }])
217
- expect(steps_.map((r) => ("name" in r ? r.name : ""))).toEqual(["fixing"])
218
- })
219
- })
@@ -1,201 +0,0 @@
1
- import { changesSince, env, human, scope, type ScopeAccess } from "../../flows/index.js"
2
- import { removedAssertions, type CollectRound } from "../../workflow-kit/index.js"
3
- import * as t from "./text.js"
4
-
5
- // The bundled workflow's single steps. A step's name is relative to the
6
- // scope its caller runs it in — `triage` inside `scope("design")` is
7
- // `design.triage` — and the full name is public API: renaming it strands
8
- // every process resting on it.
9
-
10
- export const FEEDBACK = ".gtd/FEEDBACK.md"
11
- export const ESCALATION = ".gtd/ESCALATION.md"
12
- export const REQUIREMENTS = ".gtd/REQUIREMENTS.md"
13
- export const REVIEW = ".gtd/REVIEW.md"
14
- export const REFACTOR = ".gtd/REFACTOR.md"
15
- export const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md"
16
- export const QUALITY = ".gtd/QUALITY.md"
17
- export const NAIVE_TEST = ".gtd/NAIVE-TEST.md"
18
-
19
- export const planner = (): string => env.plannerModel ?? ""
20
- export const coder = (): string => env.coderModel ?? ""
21
-
22
- // ── Keeping the suite green ─────────────────────────────────────────────────
23
-
24
- export const fixCheck = (rules?: string): Promise<void> =>
25
- t.agentWithSkills("fix", t.buildFixPrompt(rules), {
26
- label: "Fixing the check",
27
- file: FEEDBACK,
28
- model: coder(),
29
- system: t.finisherSystem(),
30
- })
31
-
32
- export const describeEscalation = (rules?: string): Promise<void> =>
33
- t.agentWithSkills("health.describe", t.healthDescribePrompt(rules), {
34
- label: "Describing the escalation",
35
- file: FEEDBACK,
36
- model: coder(),
37
- system: t.escalationSystem(),
38
- allowEmpty: true,
39
- })
40
-
41
- export const escalate = (): Promise<void> =>
42
- human("health.stop", {
43
- message: t.healthStopMessage(),
44
- label: "Escalating to a human",
45
- file: ESCALATION,
46
- acceptClean: true,
47
- })
48
-
49
- export const escalationExhausted = (): Promise<void> =>
50
- human("health.exhausted", {
51
- message: t.healthExhaustedMessage(),
52
- label: "Escalation exhausted",
53
- file: ESCALATION,
54
- acceptClean: true,
55
- })
56
-
57
- // ── Quality and review ──────────────────────────────────────────────────────
58
-
59
- export interface BuiltInLens {
60
- readonly skills: readonly string[]
61
- readonly brief: string
62
- }
63
-
64
- /** Lenses the workflow defines itself, not bundled skills: `skills/` is not in the npm package, and a missing lens skill burns a turn silently. */
65
- export const builtInLenses: Readonly<Record<string, BuiltInLens>> = {
66
- correctness: { skills: ["code-review-and-quality"], brief: t.correctnessBrief },
67
- conventions: { skills: [], brief: t.conventionsBrief },
68
- "spec-challenge": { skills: [], brief: t.specChallengeBrief },
69
- "criteria-coverage": { skills: [], brief: t.criteriaCoverageBrief },
70
- }
71
-
72
- const coderLenses: ReadonlySet<string> = new Set(["conventions", "ponytail-review", "test-audit"])
73
-
74
- /**
75
- * One quality review through `lens`; each lens is its own scope, so its own
76
- * conversation and skills. `requirements`, when given, are quoted in the prompt.
77
- */
78
- export const reviewQuality = (
79
- lens: string,
80
- base?: string,
81
- model?: string,
82
- requirements?: string,
83
- ): Promise<void> =>
84
- t.agentWithSkills(
85
- `quality.${lens}.reviewing`,
86
- t.buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief, base, requirements),
87
- {
88
- label: "Reviewing (one quality lens)",
89
- file: QUALITY,
90
- model: model || (coderLenses.has(lens) ? coder() : planner()),
91
- system: t.reviewerSystem(),
92
- allowEmpty: true,
93
- },
94
- )
95
-
96
- export const fixQuality = (rules?: string): Promise<void> =>
97
- t.agentWithSkills("fix.quality.fixing", t.buildFixQualityPrompt(rules), {
98
- label: "Fixing quality findings",
99
- file: QUALITY,
100
- model: coder(),
101
- system: t.finisherSystem(),
102
- allowEmpty: true,
103
- })
104
-
105
- /** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given; `extra` joins the prompt. */
106
- export const reviewing = (
107
- base: string,
108
- carry?: string,
109
- lap: t.ReviewRange = "first",
110
- extra = "",
111
- ): Promise<void> =>
112
- t.agentWithSkills(
113
- "review.reviewing",
114
- t.buildReviewReviewingPrompt(base, lap, carry, removedAssertions(changesSince(base)), extra),
115
- {
116
- label: "Reviewing",
117
- file: REVIEW,
118
- mode: "review",
119
- model: planner(),
120
- system: t.reviewerSystem(),
121
- base,
122
- },
123
- )
124
-
125
- /** Fix the `Risk:`-marked notes the reviewer named; an empty turn means the risk was judged false. */
126
- export const fixRisks = (
127
- notes: readonly t.NoteInput[],
128
- access?: ScopeAccess,
129
- rules?: string,
130
- ): Promise<void> => {
131
- const fixing = (name: string): Promise<void> =>
132
- t.agentWithSkills(name, t.buildReviewFixRisksPrompt(notes, rules), {
133
- label: "Fixing the reviewer's risks",
134
- file: REVIEW,
135
- model: planner(),
136
- system: t.reviewerSystem(),
137
- allowEmpty: true,
138
- })
139
- return access === undefined
140
- ? fixing("review.fix.risks.fixing")
141
- : scope({ name: "review.fix.risks", access }, () => fixing("fixing"))
142
- }
143
-
144
- export const awaitReview = (
145
- base: string,
146
- message = t.buildReviewAwaitReviewMessage(base),
147
- ): Promise<void> =>
148
- human("review.await-review", {
149
- message,
150
- label: "Awaiting your review",
151
- file: REVIEW,
152
- mode: "review",
153
- base,
154
- acceptClean: true,
155
- })
156
-
157
- /** Answer every `question` note inline in `.gtd/REVIEW.md`. */
158
- export const answerReviewQuestions = (notes: readonly t.NoteInput[]): Promise<void> =>
159
- t.agentWithSkills("review.answer-review-questions", t.buildReviewAnswerQuestionsPrompt(notes), {
160
- label: "Answering your questions",
161
- file: REVIEW,
162
- mode: "review",
163
- model: planner(),
164
- system: t.reviewerSystem(),
165
- })
166
-
167
- /** Fix every `nit` note in one turn. */
168
- export const fixNits = (
169
- notes: readonly t.NoteInput[],
170
- access?: ScopeAccess,
171
- rules?: string,
172
- ): Promise<void> => {
173
- const fixing = (name: string): Promise<void> =>
174
- t.agentWithSkills(name, t.buildReviewFixNitsPrompt(notes, rules), {
175
- label: "Fixing your nits",
176
- file: REVIEW,
177
- model: planner(),
178
- system: t.reviewerSystem(),
179
- })
180
- return access === undefined
181
- ? fixing("review.fix.nits.fixing")
182
- : scope({ name: "review.fix.nits", access }, () => fixing("fixing"))
183
- }
184
-
185
- /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
186
- export const collecting = (capture: string, round?: CollectRound): Promise<void> =>
187
- t.agentWithSkills("review.collecting", t.buildReviewCollectingPrompt(capture, undefined, round), {
188
- label: "Collecting your feedback",
189
- file: REQUIREMENTS,
190
- mode: "qa",
191
- model: planner(),
192
- system: t.reviewerSystem(),
193
- allowEmpty: true,
194
- })
195
-
196
- /** The round at commit `round` left no `.gtd/REVIEW.md`. */
197
- export const reviewMissing = (round: string): Promise<void> =>
198
- human("review.review-missing", {
199
- message: t.buildReviewReviewMissingMessage(round),
200
- label: "Nothing to review",
201
- })