@pmelab/gtd 23.2.0 → 24.1.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 (236) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +44 -11
  3. package/claude/hooks/access.ts +1 -1
  4. package/dist/chunks/Schema-Dh-mkwkq.mjs +25353 -0
  5. package/dist/chunks/reserved-ejn-LVYO.mjs +1388 -0
  6. package/dist/chunks/rolldown-runtime-8H4AJuhK.mjs +14 -0
  7. package/dist/chunks/steering-D-PvRC5W.mjs +16296 -0
  8. package/dist/chunks/wire-DA7q2ikx.mjs +473 -0
  9. package/dist/flows.mjs +525 -0
  10. package/dist/gtd.bundle.mjs +43830 -56044
  11. package/dist/steering.mjs +2 -0
  12. package/dist/testkit.mjs +13006 -0
  13. package/dist/wire.mjs +2 -0
  14. package/dist/workflow-kit.mjs +352 -0
  15. package/dist/workflows/feature.mjs +315 -0
  16. package/dist/workflows/fix.mjs +40 -0
  17. package/dist/workflows/review.mjs +25 -0
  18. package/dist/workflows.mjs +1953 -1724
  19. package/package.json +70 -8
  20. package/skills/authoring/SKILL.md +5 -4
  21. package/src/cli/Cli.test.ts +1329 -0
  22. package/src/cli/Cli.ts +1348 -0
  23. package/src/cli/Program.test.ts +2262 -0
  24. package/src/cli/Program.ts +1331 -0
  25. package/src/cli/ToolCommands.ts +43 -0
  26. package/src/cli/index.ts +14 -0
  27. package/src/edge/Edge.test.ts +1016 -0
  28. package/src/edge/Edge.ts +1225 -0
  29. package/src/edge/index.ts +16 -0
  30. package/src/emit/Emit.test.ts +425 -0
  31. package/src/emit/Emit.ts +139 -0
  32. package/src/emit/GitScript.test.ts +260 -0
  33. package/src/emit/GitScript.ts +131 -0
  34. package/src/emit/ModeContradiction.test.ts +120 -0
  35. package/src/emit/ModeContradiction.ts +66 -0
  36. package/src/emit/OutcomeScript.test.ts +238 -0
  37. package/src/emit/OutcomeScript.ts +73 -0
  38. package/src/emit/SteeringMode.test.ts +274 -0
  39. package/src/emit/SteeringMode.ts +256 -0
  40. package/src/emit/index.ts +40 -0
  41. package/src/flows/factory.ts +5 -1
  42. package/src/flows/runtime.ts +3 -0
  43. package/src/install/ConfigScaffold.ts +19 -0
  44. package/src/install/Install.test.ts +426 -0
  45. package/src/install/Install.ts +634 -0
  46. package/src/install/Version.ts +27 -0
  47. package/src/install/index.ts +3 -0
  48. package/src/platform/CommandRunner.ts +74 -0
  49. package/src/platform/Commentary.test.ts +126 -0
  50. package/src/platform/Commentary.ts +92 -0
  51. package/src/platform/Git.test.ts +50 -0
  52. package/src/platform/Git.ts +552 -0
  53. package/src/platform/Host.ts +42 -0
  54. package/src/platform/RetainedHistory.test.ts +65 -0
  55. package/src/platform/RetainedHistory.ts +34 -0
  56. package/src/platform/Workspace.test.ts +669 -0
  57. package/src/platform/Workspace.ts +631 -0
  58. package/src/platform/WorktreeState.test.ts +162 -0
  59. package/src/platform/WorktreeState.ts +50 -0
  60. package/src/platform/index.ts +22 -0
  61. package/src/replay/Access.test.ts +53 -0
  62. package/src/replay/Access.ts +46 -0
  63. package/src/replay/Format.test.ts +20 -0
  64. package/src/replay/Format.ts +14 -0
  65. package/src/replay/Replay.test.ts +887 -0
  66. package/src/replay/Replay.ts +669 -0
  67. package/src/replay/Trailers.test.ts +117 -0
  68. package/src/replay/Trailers.ts +213 -0
  69. package/src/replay/Tree.test.ts +25 -0
  70. package/src/replay/Tree.ts +45 -0
  71. package/src/replay/index.ts +11 -0
  72. package/src/steering/Blocks.fixture.ts +6 -0
  73. package/src/steering/Blocks.test.ts +167 -0
  74. package/src/steering/Blocks.ts +321 -0
  75. package/src/steering/CodeThreads.test.ts +127 -0
  76. package/src/steering/CodeThreads.ts +197 -0
  77. package/src/steering/Edits.test.ts +19 -0
  78. package/src/steering/Edits.ts +20 -0
  79. package/src/steering/Eol.test.ts +16 -0
  80. package/src/steering/Eol.ts +2 -0
  81. package/src/steering/FORMATS.md +58 -0
  82. package/src/steering/Footnotes.test.ts +977 -0
  83. package/src/steering/Footnotes.ts +661 -0
  84. package/src/steering/Inline.fixture.ts +7 -0
  85. package/src/steering/Inline.test.ts +227 -0
  86. package/src/steering/Inline.ts +195 -0
  87. package/src/steering/MarkdownTree.test.ts +247 -0
  88. package/src/steering/MarkdownTree.ts +260 -0
  89. package/src/steering/Outline.ts +19 -0
  90. package/src/steering/SteeringFormat.test.ts +91 -0
  91. package/src/steering/SteeringFormat.ts +267 -0
  92. package/src/steering/SteeringFormats.test.ts +163 -0
  93. package/src/steering/Threads.ts +163 -0
  94. package/src/steering/contract.test.ts +264 -0
  95. package/src/steering/fixtures/code-threads/cache.stripped.ts.txt +6 -0
  96. package/src/steering/fixtures/code-threads/cache.ts.md +15 -0
  97. package/src/steering/fixtures/free-form/notes.md +12 -0
  98. package/src/steering/fixtures/free-form/notes.read.json +42 -0
  99. package/src/steering/fixtures/qa/answered.md +23 -0
  100. package/src/steering/fixtures/qa/answered.read.json +119 -0
  101. package/src/steering/fixtures/qa/requirements.md +160 -0
  102. package/src/steering/fixtures/qa/requirements.read.json +475 -0
  103. package/src/steering/fixtures/review/review.md +76 -0
  104. package/src/steering/fixtures/review/review.read.json +237 -0
  105. package/src/steering/fixtures/review/ticked.md +23 -0
  106. package/src/steering/fixtures/review/ticked.read.json +99 -0
  107. package/src/steering/fixtures/review-notes/baseline.md +14 -0
  108. package/src/steering/fixtures/review-notes/noted.md +19 -0
  109. package/src/steering/freeform.test.ts +311 -0
  110. package/src/steering/freeform.ts +148 -0
  111. package/src/steering/index.test.ts +62 -0
  112. package/src/steering/index.ts +140 -0
  113. package/src/steering/qa.test.ts +2160 -0
  114. package/src/steering/qa.ts +1104 -0
  115. package/src/steering/review.test.ts +1829 -0
  116. package/src/steering/review.ts +1180 -0
  117. package/src/step/LandStep.ts +31 -0
  118. package/src/step/RepoSnapshot.ts +29 -0
  119. package/src/step/accessRefusal.test.ts +58 -0
  120. package/src/step/accessRefusal.ts +26 -0
  121. package/src/step/index.ts +5 -0
  122. package/src/step/planStart.test.ts +202 -0
  123. package/src/step/planStart.ts +167 -0
  124. package/src/step/planStep.test.ts +104 -0
  125. package/src/step/planStep.ts +89 -0
  126. package/src/step/snapshot.fixture.ts +28 -0
  127. package/src/testkit/EmittedScriptRecognizer.test.ts +639 -0
  128. package/src/testkit/EmittedScriptRecognizer.ts +642 -0
  129. package/src/testkit/FakeGitOperations.ts +95 -0
  130. package/src/testkit/GitTiers.test.ts +11 -0
  131. package/src/testkit/GitTiers.ts +1052 -0
  132. package/src/testkit/InMemRepo.ts +467 -0
  133. package/src/testkit/Layers.test.ts +58 -0
  134. package/src/testkit/Layers.ts +241 -0
  135. package/src/testkit/StrictGitOperations.ts +31 -0
  136. package/src/testkit/TestRepo.test.ts +119 -0
  137. package/src/testkit/TestRepo.ts +274 -0
  138. package/src/testkit/cliIo.ts +36 -0
  139. package/src/testkit/index.ts +9 -0
  140. package/src/testkit/steps.ts +177 -0
  141. package/src/testkit/testkit.feature +65 -0
  142. package/src/wire/BeatDocument.test.ts +419 -0
  143. package/src/wire/BeatDocument.ts +114 -0
  144. package/src/wire/BeatPlain.test.ts +113 -0
  145. package/src/wire/BeatPlain.ts +127 -0
  146. package/src/wire/BeatStatus.test.ts +59 -0
  147. package/src/wire/BeatStatus.ts +81 -0
  148. package/src/wire/Demand.test.ts +142 -0
  149. package/src/wire/Demand.ts +108 -0
  150. package/src/wire/ExitCodes.test.ts +21 -0
  151. package/src/wire/ExitCodes.ts +16 -0
  152. package/src/wire/Glob.ts +29 -0
  153. package/src/wire/Judge.ts +22 -0
  154. package/src/wire/Land.test.ts +120 -0
  155. package/src/wire/Land.ts +75 -0
  156. package/src/wire/Select.test.ts +148 -0
  157. package/src/wire/Select.ts +78 -0
  158. package/src/wire/Sessions.test.ts +59 -0
  159. package/src/wire/Sessions.ts +39 -0
  160. package/src/wire/WireSchema.test.ts +162 -0
  161. package/src/wire/constants.ts +22 -0
  162. package/src/wire/golden/1/beat-capture.json +34 -0
  163. package/src/wire/golden/1/beat-message.json +34 -0
  164. package/src/wire/golden/1/beat-prompt.json +44 -0
  165. package/src/wire/golden/1/beat-script.json +34 -0
  166. package/src/wire/golden/1/beat-stalled.json +34 -0
  167. package/src/wire/golden/1/judge.json +14 -0
  168. package/src/wire/golden/1/land-noop.json +10 -0
  169. package/src/wire/golden/1/land.json +10 -0
  170. package/src/wire/index.ts +12 -0
  171. package/src/wire/types.ts +46 -0
  172. package/src/workflow/Diagnostic.test.ts +93 -0
  173. package/src/workflow/Diagnostic.ts +54 -0
  174. package/src/workflow/SteeringFormats.test.ts +27 -0
  175. package/src/workflow/SteeringFormats.ts +6 -0
  176. package/src/workflow/Workflow.ts +77 -0
  177. package/src/workflow/compile.test.ts +53 -0
  178. package/src/workflow/compile.ts +56 -0
  179. package/src/workflow/discovery.ts +122 -0
  180. package/src/workflow/fingerprint.test.ts +52 -0
  181. package/src/workflow/fingerprint.ts +53 -0
  182. package/src/workflow/index.ts +33 -0
  183. package/src/workflow/load.test.ts +1017 -0
  184. package/src/workflow/load.ts +836 -0
  185. package/src/workflow/skills.test.ts +109 -0
  186. package/src/workflow/skills.ts +90 -0
  187. package/src/workflow/vars.ts +33 -0
  188. package/src/workflow-kit/health.test.ts +30 -0
  189. package/src/workflow-kit/health.ts +86 -0
  190. package/src/workflow-kit/index.ts +7 -0
  191. package/src/workflow-kit/quality.test.ts +17 -0
  192. package/src/workflow-kit/quality.ts +45 -0
  193. package/src/workflow-kit/review.ts +324 -0
  194. package/src/{workflows → workflow-kit}/scenarios.ts +21 -28
  195. package/src/workflows/feature/feature.test.ts +32 -0
  196. package/src/workflows/{bundled.ts → feature/feature.ts} +11 -63
  197. package/src/workflows/feature/index.ts +10 -0
  198. package/src/workflows/{packages.test.ts → feature/packages.test.ts} +2 -2
  199. package/src/workflows/{packages.ts → feature/packages.ts} +3 -3
  200. package/src/workflows/{planning.ts → feature/planning.ts} +2 -3
  201. package/src/workflows/{review.test.ts → feature/review.test.ts} +9 -44
  202. package/src/workflows/feature/review.ts +106 -0
  203. package/src/workflows/feature/steps.test.ts +82 -0
  204. package/src/workflows/feature/steps.ts +92 -0
  205. package/src/workflows/fix/fix.feature +80 -0
  206. package/src/workflows/fix/fix.test.ts +74 -0
  207. package/src/workflows/fix/fix.ts +52 -0
  208. package/src/workflows/fix/index.ts +6 -0
  209. package/src/workflows/index.ts +36 -2
  210. package/src/workflows/review/index.ts +6 -0
  211. package/src/workflows/review/review.test.ts +99 -0
  212. package/src/workflows/review/review.ts +33 -0
  213. package/src/workflows/{access.test.ts → shared/access.test.ts} +1 -1
  214. package/src/workflows/{access.ts → shared/access.ts} +3 -5
  215. package/src/workflows/shared/config.fixture.ts +12 -0
  216. package/src/workflows/{health.test.ts → shared/health.test.ts} +1 -1
  217. package/src/workflows/{health.ts → shared/health.ts} +38 -55
  218. package/src/workflows/{options.test.ts → shared/options.test.ts} +2 -2
  219. package/src/workflows/{options.ts → shared/options.ts} +14 -2
  220. package/src/workflows/shared/reserved.ts +11 -0
  221. package/src/workflows/{scenarios.test.ts → shared/scenarios.test.ts} +1 -1
  222. package/src/workflows/shared/scenarios.ts +35 -0
  223. package/src/workflows/{skills.test.ts → shared/skills.test.ts} +3 -1
  224. package/src/workflows/{skills.ts → shared/skills.ts} +7 -6
  225. package/src/workflows/{steps.test.ts → shared/steps.test.ts} +1 -66
  226. package/src/workflows/{steps.ts → shared/steps.ts} +8 -98
  227. package/src/workflows/shared/tail.test.ts +205 -0
  228. package/src/workflows/shared/tail.ts +135 -0
  229. package/src/workflows/{text.fixture.ts → shared/text.fixture.ts} +2 -2
  230. package/src/workflows/{text.ts → shared/text.ts} +48 -7
  231. package/src/workflows/{threads.test.ts → shared/threads.test.ts} +1 -1
  232. package/src/workflows/shared/workflows.steps.ts +7 -0
  233. package/src/workflows/review.ts +0 -362
  234. /package/src/workflows/{prose.ts → shared/prose.ts} +0 -0
  235. /package/src/workflows/{text.test.ts → shared/text.test.ts} +0 -0
  236. /package/src/workflows/{vars.ts → shared/vars.ts} +0 -0
@@ -0,0 +1,106 @@
1
+ import { answered, judge, numeric, vars, type JudgeQuestion } from "../../flows/index.js"
2
+ import type { ReviewNote } from "../../steering/index.js"
3
+ import * as kit from "../../workflow-kit/index.js"
4
+ import { healthy, type EscalationCount } from "../shared/health.js"
5
+ import { guarded, type FrozenScenarios } from "../shared/scenarios.js"
6
+ import {
7
+ awaitReview,
8
+ fixCheck,
9
+ fixRisks,
10
+ REQUIREMENTS,
11
+ REVIEW,
12
+ reviewing,
13
+ reviewMissing,
14
+ } from "../shared/steps.js"
15
+ import { closeReview, tail } from "../shared/tail.js"
16
+ import * as t from "../shared/text.js"
17
+ import type { BuiltPlan } from "./packages.js"
18
+ import { answerReviewQuestions, collecting, fixNits } from "./steps.js"
19
+
20
+ export type { ReviewOutcome } from "../../workflow-kit/index.js"
21
+
22
+ const VERDICTS: readonly kit.Verdict[] = ["edit", "question", "nit", "praise"]
23
+
24
+ /** One four-way choice per note the human added to `.gtd/REVIEW.md`. */
25
+ const verdictQuestion = (note: ReviewNote): JudgeQuestion => ({
26
+ id: note.id,
27
+ primitive: "choice",
28
+ instructions: `Classify the human's note on review item "${note.anchor}" (in .gtd/REVIEW.md). Its evidence holds the anchor, the reviewer's text and the human's text.`,
29
+ criteria:
30
+ "edit: a request to change behaviour, design or scope — anything needing a plan, or any note you are unsure about. question: asks something and wants an answer, with no change requested. nit: a small, local, unambiguous fix (naming, typo, formatting, comment wording) needing no re-plan. praise: an approving remark with nothing to do.",
31
+ })
32
+
33
+ /**
34
+ * One judge rest, one verdict per note. Dismissing a note is the risky
35
+ * direction, so only a confident non-`edit` verdict on uncut evidence counts;
36
+ * a cut, unanswered or below-floor note is an `edit`, and a blank floor makes
37
+ * every note one.
38
+ */
39
+ const triage = async (notes: readonly ReviewNote[]): Promise<ReadonlyMap<string, kit.Verdict>> => {
40
+ const { answers, truncated } = await judge("review.triage", {
41
+ questions: notes.map(verdictQuestion),
42
+ evidence: Object.fromEntries(
43
+ notes.map((n) => [
44
+ n.id,
45
+ `Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`,
46
+ ]),
47
+ ),
48
+ message: t.buildReviewTriageMessage(),
49
+ label: "Judging each note",
50
+ })
51
+ const minP = numeric(vars.reviewNoteActionable, Infinity)
52
+ return new Map(
53
+ notes.map((n): [string, kit.Verdict] => {
54
+ const verdict = VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP))
55
+ return [n.id, truncated.includes(n.id) || verdict === undefined ? "edit" : verdict]
56
+ }),
57
+ )
58
+ }
59
+
60
+ const isCode = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
61
+
62
+ /**
63
+ * A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
64
+ * reviews and signs off or comments, and each note is judged: edits go to
65
+ * another planning lap, questions are answered in place, nits fixed in one
66
+ * batch, praise dropped.
67
+ */
68
+ export const review = (
69
+ base: string,
70
+ escalations: EscalationCount = { rounds: 0 },
71
+ frozen?: FrozenScenarios,
72
+ ): Promise<kit.ReviewOutcome> =>
73
+ kit.review(base, {
74
+ file: REVIEW,
75
+ feedback: REQUIREMENTS,
76
+ isCode,
77
+ reviewing,
78
+ fixRisks: (risks) => guarded(frozen, () => fixRisks(risks), "review")(),
79
+ keepGreen: () => healthy(guarded(frozen, fixCheck, "review"), { escalations }),
80
+ awaitReview,
81
+ reviewMissing,
82
+ collecting,
83
+ triage,
84
+ answerQuestions: answerReviewQuestions,
85
+ fixNits: (notes) => guarded(frozen, () => fixNits(notes), "review")(),
86
+ close: closeReview,
87
+ captures: {
88
+ edits: t.reviewEditsCapture,
89
+ notes: t.reviewNotesCapture,
90
+ editNotes: t.reviewEditNotesCapture,
91
+ },
92
+ refusals: {
93
+ deleted:
94
+ "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
95
+ unmatched:
96
+ "gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing",
97
+ },
98
+ })
99
+
100
+ /** The build tail: fix (when entered red), keep green, the quality lap, then human review since `base`. */
101
+ export const buildTail = (
102
+ fixFirst: boolean,
103
+ base: string,
104
+ built?: BuiltPlan,
105
+ ): Promise<kit.ReviewOutcome> =>
106
+ tail(fixFirst, (escalations, frozen) => review(base, escalations, frozen), built)
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import type { StepRequest } from "../../flows/index.js"
3
+ import { captureStep } from "../shared/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 feature workflow's own steps declare skills — a step's rendered prompt carries its bundled skills.ts preamble", () => {
18
+ it("triage carries design.triage's bundled skills", async () => {
19
+ const prompt = agentPrompt(await capture(() => steps.triage("base"), "design"))
20
+ expect(prompt).toContain("spec-driven-development, planning-and-task-breakdown")
21
+ })
22
+
23
+ it("author carries architecture.author's bundled skills", async () => {
24
+ const prompt = agentPrompt(await capture(() => steps.author(), "architecture"))
25
+ expect(prompt).toContain("api-and-interface-design, documentation-and-adrs, ponytail")
26
+ })
27
+
28
+ it("decompose carries architecture.decompose's bundled skills", async () => {
29
+ const prompt = agentPrompt(await capture(() => steps.decompose(), "architecture"))
30
+ expect(prompt).toContain("incremental-implementation, planning-and-task-breakdown")
31
+ })
32
+
33
+ it("build carries packages.item.building's bundled skills", async () => {
34
+ const prompt = agentPrompt(await capture(() => steps.build("pkg"), "packages.item"))
35
+ expect(prompt).toContain("test-driven-development, incremental-implementation")
36
+ })
37
+
38
+ it("fixSuite carries packages.item.fix.suite's bundled skills", async () => {
39
+ const prompt = agentPrompt(await capture(() => steps.fixSuite(), "packages.item"))
40
+ expect(prompt).toContain("debugging-and-error-recovery")
41
+ })
42
+
43
+ it("collecting carries build.review.collecting's bundled skills", async () => {
44
+ const prompt = agentPrompt(await capture(() => steps.collecting("capture"), "build"))
45
+ expect(prompt).toContain("code-review-and-quality")
46
+ })
47
+
48
+ it("answerReviewQuestions answers inline in .gtd/REVIEW.md as the reviewer, with its bundled skills", async () => {
49
+ const request = await capture(
50
+ () =>
51
+ steps.answerReviewQuestions([{ id: "note-1", anchor: "calc ./a.ts#1-1", text: "why?" }]),
52
+ "build",
53
+ )
54
+ if (request.kind !== "agent") throw new Error("unreachable")
55
+ expect(request.name).toBe("review.answer-review-questions")
56
+ expect(request.options).toMatchObject({
57
+ file: ".gtd/REVIEW.md",
58
+ mode: "review",
59
+ })
60
+ expect(request.prompt).toContain("code-review-and-quality")
61
+ expect(request.prompt).toContain("note-1 — calc ./a.ts#1-1")
62
+ expect(request.prompt).toContain("`A: ` line")
63
+ expect(request.prompt).toContain("gtd check review .gtd/REVIEW.md")
64
+ })
65
+
66
+ it("fixNits fixes every nit in one turn and leaves .gtd/REVIEW.md alone", async () => {
67
+ const request = await capture(
68
+ () =>
69
+ steps.fixNits([
70
+ { id: "note-1", anchor: "calc ./a.ts#1-1", text: "typo" },
71
+ { id: "note-2", anchor: "calc ./a.ts#2-2", text: "semicolon" },
72
+ ]),
73
+ "build",
74
+ )
75
+ if (request.kind !== "agent") throw new Error("unreachable")
76
+ expect(request.name).toBe("review.fix.nits.fixing")
77
+ expect(request.prompt).toContain("incremental-implementation, code-simplification")
78
+ expect(request.prompt).toContain("typo")
79
+ expect(request.prompt).toContain("semicolon")
80
+ expect(request.prompt).toContain("Leave `.gtd/REVIEW.md` untouched")
81
+ })
82
+ })
@@ -0,0 +1,92 @@
1
+ import { human } from "../../flows/index.js"
2
+ import * as t from "../shared/text.js"
3
+ import { ARCHITECTURE, coder, FEEDBACK, planner, REQUIREMENTS, REVIEW } from "../shared/steps.js"
4
+
5
+ /** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
6
+ export const triage = (base: string): Promise<void> =>
7
+ t.agentWithSkills("triage", t.designTriagePrompt(base), {
8
+ label: "Triaging the change",
9
+ file: REQUIREMENTS,
10
+ mode: "qa",
11
+ model: planner(),
12
+ system: t.designSystem(),
13
+ })
14
+
15
+ export const author = (): Promise<void> =>
16
+ t.agentWithSkills("author", t.architectureAuthorPrompt(), {
17
+ label: "Refining the technical plan",
18
+ file: ARCHITECTURE,
19
+ mode: "qa",
20
+ model: planner(),
21
+ system: t.architectSystem(),
22
+ })
23
+
24
+ export const decompose = (): Promise<void> =>
25
+ t.agentWithSkills("decompose.decomposing", t.architectureDecomposePrompt(), {
26
+ label: "Decomposing into packages",
27
+ model: planner(),
28
+ system: t.architectSystem(),
29
+ })
30
+
31
+ export const answerProductQuestions = (): Promise<void> =>
32
+ human("gate.answer", {
33
+ message: t.designGateAnswerMessage(),
34
+ label: "Awaiting your product answers",
35
+ file: REQUIREMENTS,
36
+ mode: "qa",
37
+ acceptClean: true,
38
+ })
39
+
40
+ export const answerTechnicalQuestions = (): Promise<void> =>
41
+ human("gate.answer", {
42
+ message: t.architectureGateAnswerMessage(),
43
+ label: "Awaiting your technical answers",
44
+ file: ARCHITECTURE,
45
+ mode: "qa",
46
+ acceptClean: true,
47
+ })
48
+
49
+ export const build = (pkg: string): Promise<void> =>
50
+ t.agentWithSkills("building", t.packagesItemBuildingPrompt(pkg), {
51
+ label: "Building",
52
+ model: coder(),
53
+ system: t.builderSystem(),
54
+ })
55
+
56
+ export const fixSuite = (): Promise<void> =>
57
+ t.agentWithSkills("fix.suite.fixing", t.packagesItemFixSuitePrompt(), {
58
+ label: "Fixing the check",
59
+ file: FEEDBACK,
60
+ model: coder(),
61
+ system: t.builderSystem(),
62
+ })
63
+
64
+ /** Answer every `question` note inline in `.gtd/REVIEW.md`. */
65
+ export const answerReviewQuestions = (notes: readonly t.NoteInput[]): Promise<void> =>
66
+ t.agentWithSkills("review.answer-review-questions", t.buildReviewAnswerQuestionsPrompt(notes), {
67
+ label: "Answering your questions",
68
+ file: REVIEW,
69
+ mode: "review",
70
+ model: planner(),
71
+ system: t.reviewerSystem(),
72
+ })
73
+
74
+ /** Fix every `nit` note in one turn. */
75
+ export const fixNits = (notes: readonly t.NoteInput[]): Promise<void> =>
76
+ t.agentWithSkills("review.fix.nits.fixing", t.buildReviewFixNitsPrompt(notes), {
77
+ label: "Fixing your nits",
78
+ file: REVIEW,
79
+ model: planner(),
80
+ system: t.reviewerSystem(),
81
+ })
82
+
83
+ /** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
84
+ export const collecting = (capture: string): Promise<void> =>
85
+ t.agentWithSkills("review.collecting", t.buildReviewCollectingPrompt(capture), {
86
+ label: "Collecting your feedback",
87
+ file: REQUIREMENTS,
88
+ mode: "qa",
89
+ model: planner(),
90
+ system: t.reviewerSystem(),
91
+ allowEmpty: true,
92
+ })
@@ -0,0 +1,80 @@
1
+ Feature: fix fixes the notes review hands it
2
+
3
+ `review` and `fix` meet only in `.gtd/REVIEW.md`: a review the human leaves
4
+ a note in ends with the reviewer's copy kept beside it, and `gtd door fix`
5
+ picks both up.
6
+
7
+ Background:
8
+ Given the shipped workflows, as gtd install scaffolds them
9
+ And an environment variable "GTD_QUALITYREVIEWS" set to ""
10
+ And a file "src/calc.ts" with:
11
+ """
12
+ export const add = (a: number, b: number) => a + b
13
+ """
14
+ And the working tree is committed as "feat: add calculator"
15
+
16
+ Scenario: a note left in review is fixed, and both review files are removed
17
+ Given step "review-gate.check" changes nothing
18
+ And step "build.health.check" changes nothing
19
+ And step "build.review.reviewing" writes ".gtd/REVIEW.md" with:
20
+ """
21
+ # Review: abc1234
22
+
23
+ <!-- base: 0000000 -->
24
+
25
+ ## calc
26
+
27
+ - [ ] ./src/calc.ts#1-1
28
+ new add function
29
+ """
30
+ When gtd opens door "review" with "HEAD~1"
31
+ And the process is driven
32
+ Then the process rests at "build.review.await-review"
33
+
34
+ Given a file ".gtd/REVIEW.md" with:
35
+ """
36
+ # Review: abc1234
37
+
38
+ <!-- base: 0000000 -->
39
+
40
+ ## calc
41
+
42
+ - [ ] ./src/calc.ts#1-1
43
+ new add function — rename it to sum
44
+ """
45
+ And step "build.review.handing-off" runs its script
46
+ When gtd lands
47
+ And the process is driven
48
+ Then the process rests at "idle"
49
+ And ".gtd/REVIEW.orig.md" contains "new add function"
50
+
51
+ Given step "notes.fixing" writes "src/calc.ts" with:
52
+ """
53
+ export const sum = (a: number, b: number) => a + b
54
+ """
55
+ And step "notes.closing" runs its script
56
+ When gtd opens door "fix"
57
+ Then the resting step's content contains "rename it to sum"
58
+ # The fix goes to its own review, which the same scripted reviewer writes
59
+ # afresh — the handed-off review itself is gone, as the reviewer's copy shows.
60
+ When the process is driven
61
+ Then the process rests at "build.review.await-review"
62
+ And the commit subjects end with:
63
+ """
64
+ gtd(human): notes.fixing
65
+ gtd(agent): notes.fixing → notes.closing
66
+ gtd(check): notes.closing → build.health.check
67
+ gtd(check): build.health.check → build.review.reviewing
68
+ gtd(agent): build.review.reviewing → build.review.await-review
69
+ """
70
+ And ".gtd/REVIEW.orig.md" does not exist
71
+ And ".gtd/REVIEW.md" contains "new add function"
72
+ And "src/calc.ts" contains "sum"
73
+
74
+ Scenario: a reviewer's copy with no review is refused
75
+ Given a file ".gtd/REVIEW.orig.md" with:
76
+ """
77
+ # Review: abc1234
78
+ """
79
+ And the working tree is committed as "chore: stray copy"
80
+ Then opening door "fix" is refused with "has no .gtd/REVIEW.md"
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { runsItsScript, testRepo } from "../../testkit/index.js"
3
+ import { CONFIG_SCAFFOLD } from "../../install/index.js"
4
+ import { reviewDoc } from "../shared/config.fixture.js"
5
+
6
+ const REVIEW = ".gtd/REVIEW.md"
7
+ const ORIG = ".gtd/REVIEW.orig.md"
8
+ const CALC = "export const add = (a: number, b: number) => a + b\n"
9
+
10
+ describe("the fix workflow", () => {
11
+ it("with no handed-off review, repairs a red baseline and hands it to review", async () => {
12
+ const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_QUALITYREVIEWS: "" } })
13
+ await repo.start("fix")
14
+ const rest = await repo.drive({
15
+ "fix-precheck": (_, r) => r.write(".gtd/FEEDBACK.md", "1 test failing\n"),
16
+ "build.fix": (beat, r) => {
17
+ expect(beat.content).toContain(".gtd/FEEDBACK.md")
18
+ r.remove(".gtd/FEEDBACK.md")
19
+ r.write("src/repair.ts", "export const repaired = true\n")
20
+ },
21
+ "build.health.check": () => undefined,
22
+ })
23
+ expect(rest).toMatchObject({ state: "build.review.reviewing", actor: "agent" })
24
+ expect(repo.subjects().slice(1)).toEqual([
25
+ "gtd(human): fix-precheck",
26
+ "gtd(check): fix-precheck → build.fix",
27
+ "gtd(agent): build.fix → build.health.check",
28
+ "gtd(check): build.health.check → build.review.reviewing",
29
+ ])
30
+ })
31
+
32
+ it("with a green baseline, has nothing to fix", async () => {
33
+ const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_QUALITYREVIEWS: "" } })
34
+ await repo.start("fix")
35
+ const rest = await repo.drive({ "fix-precheck": () => undefined })
36
+ expect(rest.state).toBe("idle")
37
+ expect(repo.subjects().at(-1)).toBe("gtd(check): fix-precheck → idle")
38
+ })
39
+
40
+ it("fixes a handed-off review's notes and risks in one turn, removes both review files, then checks health", async () => {
41
+ const repo = testRepo({
42
+ workflow: CONFIG_SCAFFOLD,
43
+ env: { GTD_QUALITYREVIEWS: "" },
44
+ files: {
45
+ "src/calc.ts": CALC,
46
+ [REVIEW]: reviewDoc("add — rename it to sum", "Risk: drops the carry"),
47
+ [ORIG]: reviewDoc("add", "Risk: drops the carry"),
48
+ },
49
+ })
50
+ await repo.door("fix")
51
+ const rest = await repo.drive({
52
+ "notes.fixing": (beat, r) => {
53
+ expect(beat.content).toContain("rename it to sum")
54
+ expect(beat.content).toContain("Risk: drops the carry")
55
+ r.write("src/calc.ts", CALC.replace("add", "sum"))
56
+ },
57
+ "notes.closing": runsItsScript,
58
+ })
59
+ expect(rest.state).toBe("build.health.check")
60
+ expect(repo.subjects().slice(1)).toEqual([
61
+ "gtd(human): notes.fixing",
62
+ "gtd(agent): notes.fixing → notes.closing",
63
+ "gtd(check): notes.closing → build.health.check",
64
+ ])
65
+ expect(repo.read("src/calc.ts")).toContain("sum")
66
+ expect(repo.read(REVIEW)).toBeUndefined()
67
+ expect(repo.read(ORIG)).toBeUndefined()
68
+ })
69
+
70
+ it("refuses a review with no reviewer's copy, so no note is deleted unread", async () => {
71
+ const repo = testRepo({ workflow: CONFIG_SCAFFOLD, files: { [REVIEW]: reviewDoc("add") } })
72
+ await expect(repo.door("fix")).rejects.toThrow(/not handed off/)
73
+ })
74
+ })
@@ -0,0 +1,52 @@
1
+ import { read, refuse, removeScript, run, scope, start } from "../../flows/index.js"
2
+ import { reviewNotes, reviewRisks } from "../../steering/index.js"
3
+ import { baseline } from "../shared/health.js"
4
+ import { shipped } from "../shared/options.js"
5
+ import { coder, REVIEW } from "../shared/steps.js"
6
+ import { handOffTail, REVIEW_ORIG } from "../shared/tail.js"
7
+ import * as t from "../shared/text.js"
8
+
9
+ /**
10
+ * Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
11
+ * risk its reviewer marked, in one turn; the review files go once it is done.
12
+ */
13
+ export const fixReviewNotes = (): Promise<void> =>
14
+ scope("notes", async () => {
15
+ const record = read(REVIEW) ?? ""
16
+ const orig = read(REVIEW_ORIG)
17
+ // Without the reviewer's copy no note can be told from the review's own text,
18
+ // and closing would delete notes never read — also keeps a stray review out.
19
+ if (orig === undefined) {
20
+ return refuse(
21
+ `gtd: ${REVIEW} has no ${REVIEW_ORIG}, so it was not handed off by a review — remove it, or restore ${REVIEW_ORIG}.`,
22
+ )
23
+ }
24
+ const notes = reviewNotes(orig, record)
25
+ await t.agentWithSkills("fixing", t.fixReviewNotesPrompt(notes, reviewRisks(record)), {
26
+ label: "Fixing your review notes",
27
+ file: REVIEW,
28
+ model: coder(),
29
+ system: t.finisherSystem(),
30
+ // Praise and questions are answered without an edit; the hand-off review that follows catches an unaddressed note.
31
+ allowEmpty: true,
32
+ })
33
+ await run("closing", removeScript([REVIEW, REVIEW_ORIG]), { label: "Closing the review" })
34
+ })
35
+
36
+ /**
37
+ * Fix what a review handed on, else repair a red baseline — then the
38
+ * hand-off review of the result. A note there is handed to the next `fix`.
39
+ */
40
+ export const fixFlow = async (): Promise<void> => {
41
+ if (read(REVIEW) !== undefined) {
42
+ await fixReviewNotes()
43
+ return handOffTail(false, start())
44
+ }
45
+ if (read(REVIEW_ORIG) !== undefined) {
46
+ return refuse(`gtd: ${REVIEW_ORIG} has no ${REVIEW} — remove it, or restore ${REVIEW}.`)
47
+ }
48
+ if (await baseline("fix-precheck")) return
49
+ await handOffTail(true, start())
50
+ }
51
+
52
+ export const fix = shipped(fixFlow, { fix: {} })
@@ -0,0 +1,6 @@
1
+ import { fix } from "./fix.js"
2
+
3
+ export { fix, fixFlow, fixReviewNotes } from "./fix.js"
4
+ export * from "../shared/reserved.js"
5
+
6
+ export default fix
@@ -1,4 +1,38 @@
1
1
  // The `@pmelab/gtd/workflows` entry, built on its own: core never imports it,
2
2
  // a repository's `gtd.config.ts` does.
3
- export * from "./bundled.js"
4
- export { default } from "./bundled.js"
3
+ import type { Doors } from "../flows/index.js"
4
+ import { feature } from "./feature/feature.js"
5
+
6
+ export { threads, type ThreadInfo } from "../flows/index.js"
7
+ export * from "./shared/reserved.js"
8
+ export { ShippedOptions } from "./shared/options.js"
9
+ export { agentWithSkills } from "./shared/text.js"
10
+ export * from "./shared/steps.js"
11
+ export * from "./shared/health.js"
12
+ export * from "./shared/scenarios.js"
13
+ export {
14
+ closeReview,
15
+ fixQualityFindings,
16
+ handOffReview,
17
+ handOffTail,
18
+ qualityLap,
19
+ REVIEW_ORIG,
20
+ tail,
21
+ type TailPlan,
22
+ } from "./shared/tail.js"
23
+ export { lensesOf } from "../workflow-kit/index.js"
24
+ export * from "./feature/steps.js"
25
+ export * from "./feature/planning.js"
26
+ export * from "./feature/packages.js"
27
+ export { buildTail, type ReviewOutcome } from "./feature/review.js"
28
+ export * from "./feature/feature.js"
29
+ export { base, review, reviewFlow } from "./review/review.js"
30
+ export { fix, fixFlow, fixReviewNotes } from "./fix/fix.js"
31
+
32
+ export default feature
33
+
34
+ /**
35
+ * @deprecated Empty: each shipped workflow builds its own doors, so a config
36
+ * gets `fix` and `review [base]` by exporting `fix` and `review`.
37
+ */
38
+ export const doors: Doors = {}
@@ -0,0 +1,6 @@
1
+ import { review } from "./review.js"
2
+
3
+ export { review, reviewFlow } from "./review.js"
4
+ export * from "../shared/reserved.js"
5
+
6
+ export default review
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { runsItsScript, testRepo, type TestRepo } from "../../testkit/index.js"
3
+ import { CONFIG_SCAFFOLD } from "../../install/index.js"
4
+ import { reviewDoc } from "../shared/config.fixture.js"
5
+ import { base } from "./review.js"
6
+
7
+ const REVIEW = ".gtd/REVIEW.md"
8
+ const ORIG = ".gtd/REVIEW.orig.md"
9
+
10
+ /** A repository with one commit to review, the review run until the human holds `.gtd/REVIEW.md`. */
11
+ const reviewed = async (): Promise<TestRepo> => {
12
+ const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_QUALITYREVIEWS: "" } })
13
+ repo.write("src/calc.ts", "export const add = (a: number, b: number) => a + b\n")
14
+ repo.commit("feat: add calculator")
15
+ await repo.door("review", "HEAD~1")
16
+ const rest = await repo.drive({
17
+ "review-gate.check": () => undefined,
18
+ "build.health.check": () => undefined,
19
+ "build.review.reviewing": (beat, r) => {
20
+ expect(beat.file).toBe(REVIEW)
21
+ r.write(REVIEW, reviewDoc("new add function"))
22
+ },
23
+ })
24
+ expect(rest).toMatchObject({ state: "build.review.await-review", actor: "human" })
25
+ return repo
26
+ }
27
+
28
+ describe("the review workflow", () => {
29
+ it("checks, reviews and hands the review to the human", async () => {
30
+ const repo = await reviewed()
31
+ expect(repo.subjects().slice(2)).toEqual([
32
+ "gtd(human): review-gate.check",
33
+ "gtd(check): review-gate.check → build.health.check",
34
+ "gtd(check): build.health.check → build.review.reviewing",
35
+ "gtd(agent): build.review.reviewing → build.review.await-review",
36
+ ])
37
+ })
38
+
39
+ it("closes a review the human signs off", async () => {
40
+ const repo = await reviewed()
41
+ repo.write(REVIEW, reviewDoc("new add function").replace("- [ ]", "- [x]"))
42
+ await repo.land()
43
+ const rest = await repo.drive({ "build.review.closing": runsItsScript })
44
+ expect(rest.state).toBe("idle")
45
+ expect(repo.subjects().slice(-2)).toEqual([
46
+ "gtd(human): build.review.await-review → build.review.closing",
47
+ "gtd(check): build.review.closing → idle",
48
+ ])
49
+ expect(repo.read(REVIEW)).toBeUndefined()
50
+ })
51
+
52
+ it("never lands a review the review format rejects", async () => {
53
+ const repo = testRepo({ workflow: CONFIG_SCAFFOLD, env: { GTD_QUALITYREVIEWS: "" } })
54
+ repo.write("src/calc.ts", "export const add = (a: number, b: number) => a + b\n")
55
+ repo.commit("feat: add calculator")
56
+ await repo.door("review", "HEAD~1")
57
+ await expect(
58
+ repo.drive({
59
+ "review-gate.check": () => undefined,
60
+ "build.health.check": () => undefined,
61
+ "build.review.reviewing": (_, r) => r.write(REVIEW, "not a review\n"),
62
+ }),
63
+ ).rejects.toThrow(/validating \.gtd\/REVIEW\.md/)
64
+ expect(repo.subjects().at(-1)).toBe("gtd(check): build.health.check → build.review.reviewing")
65
+ })
66
+
67
+ it("ends with the review and the reviewer's copy kept when the human leaves a note", async () => {
68
+ const repo = await reviewed()
69
+ repo.write(REVIEW, reviewDoc("new add function — rename it to sum"))
70
+ await repo.land()
71
+ const rest = await repo.drive({
72
+ "build.review.handing-off": runsItsScript,
73
+ })
74
+ expect(rest.state).toBe("idle")
75
+ expect(repo.subjects().slice(-2)).toEqual([
76
+ "gtd(human): build.review.await-review → build.review.handing-off",
77
+ "gtd(check): build.review.handing-off → idle",
78
+ ])
79
+ expect(repo.read(REVIEW)).toContain("rename it to sum")
80
+ expect(repo.read(ORIG)).toBe(reviewDoc("new add function"))
81
+ })
82
+
83
+ it("refuses a pending hand-off before requesting any step, the gate's check included", async () => {
84
+ const repo = testRepo({
85
+ workflow: CONFIG_SCAFFOLD,
86
+ files: { [REVIEW]: reviewDoc("note"), [ORIG]: reviewDoc("orig") },
87
+ })
88
+ await expect(repo.door("review")).rejects.toThrow(".gtd/REVIEW.orig.md is a pending hand-off")
89
+ expect(repo.subjects()).toEqual(["chore: initial commit"])
90
+ })
91
+ })
92
+
93
+ describe("the deprecated shipped base", () => {
94
+ it("answers for a workflow named review only, mapping reviewBase", () => {
95
+ expect(base("review", {})).toBe("")
96
+ expect(base("review", { reviewBase: "v1" })).toBe("v1")
97
+ expect(base("audit", { reviewBase: "v1" })).toBeUndefined()
98
+ })
99
+ })