@pmelab/gtd 30.1.0 → 32.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 (33) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +29 -23
  3. package/claude/hooks/ship.ts +7 -3
  4. package/dist/chunks/{reserved-DDiiAZ8U.mjs → reserved-CHTJTzpA.mjs} +52 -3
  5. package/dist/chunks/{tail-ILPlELuy.mjs → tail-CCsDBbZe.mjs} +1 -1
  6. package/dist/workflows/cleanup.mjs +2 -2
  7. package/dist/workflows/feature.mjs +137 -8
  8. package/dist/workflows/fix.mjs +2 -2
  9. package/dist/workflows/review.mjs +2 -2
  10. package/dist/workflows/rollout.mjs +1 -1
  11. package/dist/workflows.mjs +188 -10
  12. package/package.json +1 -1
  13. package/src/workflows/feature/acceptance-criteria-tier.feature +25 -0
  14. package/src/workflows/feature/acceptance-criteria.feature +189 -0
  15. package/src/workflows/feature/criteria.test.ts +106 -0
  16. package/src/workflows/feature/criteria.ts +168 -0
  17. package/src/workflows/feature/fast-path.feature +4 -0
  18. package/src/workflows/feature/feature.test.ts +41 -11
  19. package/src/workflows/feature/feature.ts +7 -4
  20. package/src/workflows/feature/flag-mode.feature +8 -0
  21. package/src/workflows/feature/index.ts +1 -0
  22. package/src/workflows/feature/one-step-land.feature +4 -0
  23. package/src/workflows/feature/planning.ts +2 -0
  24. package/src/workflows/feature/refactor-absorbs.feature +4 -0
  25. package/src/workflows/feature/refactor-first.feature +4 -0
  26. package/src/workflows/feature/refactor-review.feature +24 -0
  27. package/src/workflows/feature/risk-tier.feature +32 -0
  28. package/src/workflows/feature/seam-rejection.feature +8 -0
  29. package/src/workflows/feature/seam.feature +8 -0
  30. package/src/workflows/feature/seam.ts +12 -7
  31. package/src/workflows/feature/tier-pin.feature +4 -0
  32. package/src/workflows/index.ts +1 -0
  33. package/src/workflows/shared/text.ts +77 -2
@@ -21,6 +21,7 @@ export {
21
21
  } from "./shared/tail.js"
22
22
  export { lensesOf } from "../workflow-kit/index.js"
23
23
  export * from "./feature/steps.js"
24
+ export * from "./feature/criteria.js"
24
25
  export * from "./feature/planning.js"
25
26
  export * from "./feature/refactor.js"
26
27
  export * from "./feature/seam.js"
@@ -9,6 +9,7 @@ import {
9
9
  skillsFor,
10
10
  start,
11
11
  type AgentOptions,
12
+ type JudgeQuestion,
12
13
  type SummaryContext,
13
14
  } from "../../flows/index.js"
14
15
  import { escapeRegExp } from "./registry.js"
@@ -115,6 +116,22 @@ const codeThreadReplies = (): string => {
115
116
  ${list}`
116
117
  }
117
118
 
119
+ const acceptanceCriteriaBlock = `
120
+ ## Acceptance criteria
121
+
122
+ Whichever lap this is, \`.gtd/REQUIREMENTS.md\` carries a
123
+ \`## Acceptance Criteria\` section: one \`- AC-<n>: <criterion>\` line
124
+ per criterion, numbered from \`AC-1\`. They bind every later step —
125
+ the build is done when each one holds — so each must be checkable: a
126
+ test or a command shows it holds or not. A later lap keeps every ID:
127
+ reword a criterion in place, append a new one with the next unused
128
+ number, delete a dropped one — never renumber, never reuse an ID. When
129
+ the file holds no list (a review loop-back rewrites it), recover the
130
+ last settled one from history (\`git log -p -- .gtd/REQUIREMENTS.md\`)
131
+ and carry its IDs over. Landing is refused while a criterion has no ID or
132
+ two share one.
133
+ `
134
+
118
135
  const riskTierBlock = `
119
136
  ## Risk tier and production signals
120
137
 
@@ -160,7 +177,7 @@ ${footnoteFoldIn}${codeThreadReplies()}
160
177
  or both
161
178
  - Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
162
179
  uncommitted and finish once it reflects this lap's own work
163
-
180
+ ${acceptanceCriteriaBlock}
164
181
  ## First lap
165
182
 
166
183
  \`.gtd/REQUIREMENTS.md\` does not exist yet (or holds whatever the
@@ -216,6 +233,47 @@ ${questionBarReturn}
216
233
  everything else — the build assumes the suite started green
217
234
  ${riskTier ? riskTierBlock : ""}`
218
235
 
236
+ export const clearCriteriaQuestion: JudgeQuestion = {
237
+ id: "clear",
238
+ primitive: "noul",
239
+ instructions:
240
+ "state.criteria lists the acceptance criteria the human's sketch (state.sketch) stated, as gtd numbered them. Decide whether they can stand as the binding definition of done, with no design conversation.",
241
+ criteria:
242
+ "yes: every criterion is clear and checkable — a test or a command can show it holds or not — and together they say what to build. no: any one criterion is vague, subjective or untestable, or the sketch leaves an open product question. Unsure means no.",
243
+ }
244
+
245
+ export const clearCriteriaMessage = (): string =>
246
+ `The sketch stated its own acceptance criteria. Run \`gtd judge answer\` and
247
+ pipe \`yes\` when every one is clear and checkable, so design is skipped and
248
+ they are recorded as they are — or land without a verdict to run the
249
+ ordinary design conversation.
250
+ `
251
+
252
+ export const criterionLine = (c: { readonly id: string; readonly text: string }): string =>
253
+ `- ${c.id}: ${c.text.replace(/\s+/g, " ")}`
254
+
255
+ /** Writes \`.gtd/REQUIREMENTS.md\` from criteria the sketch stated, wrapped the way oxfmt writes it. */
256
+ export const normaliseCriteriaScript = (stated: {
257
+ readonly path: string
258
+ readonly commit: string
259
+ readonly criteria: readonly { readonly id: string; readonly text: string }[]
260
+ }): string => {
261
+ const lines = [
262
+ "# Requirements",
263
+ "",
264
+ `Stated in \`${stated.path}\` by the sketch:`,
265
+ "",
266
+ `\`git show ${stated.commit}:${stated.path}\``,
267
+ "",
268
+ "## Acceptance Criteria",
269
+ "",
270
+ ...stated.criteria.flatMap((c) => wrapItem(criterionLine(c))),
271
+ ]
272
+ return `mkdir -p .gtd
273
+ printf '%s\\n' ${lines.map(quote).join(" ")} > ${quote(".gtd/REQUIREMENTS.md")}
274
+ `
275
+ }
276
+
219
277
  export const designSystem = (): string =>
220
278
  `${designPersona}
221
279
 
@@ -650,7 +708,17 @@ What each change does next (then run \`gtd land\`):
650
708
  const wrapItem = (item: string): string[] => {
651
709
  const out: string[] = []
652
710
  let line = ""
653
- for (const word of item.split(" ")) {
711
+ // A word that would read as a list marker or heading at a line start stays on the line before.
712
+ const words = item
713
+ .split(" ")
714
+ .reduce<string[]>(
715
+ (acc, word) =>
716
+ acc.length > 0 && /^([-+*>#]|\d+[.)])$/.test(word)
717
+ ? [...acc.slice(0, -1), `${acc.at(-1)!} ${word}`]
718
+ : [...acc, word],
719
+ [],
720
+ )
721
+ for (const word of words) {
654
722
  if (line !== "" && line.length + 1 + word.length > 80) {
655
723
  out.push(line)
656
724
  line = ` ${word}`
@@ -1066,9 +1134,15 @@ export interface SeamRejection {
1066
1134
  readonly p: number
1067
1135
  }
1068
1136
 
1137
+ const criteriaBinding = (refused: boolean): string =>
1138
+ `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
1139
+ the file, never from memory, and stop only when each one holds. They are
1140
+ settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`
1141
+
1069
1142
  export const naivePassPrompt = (rejection?: SeamRejection): string =>
1070
1143
  `${stateFileRules}
1071
1144
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1145
+ ${criteriaBinding(false)}
1072
1146
  - Implement them the quickest way that works — the naive version. Do not
1073
1147
  restructure, abstract or refactor around the change; a later step judges
1074
1148
  the shape of your diff and a reviewer makes it fit
@@ -1095,6 +1169,7 @@ export const implementBuildingPrompt = (
1095
1169
  ): string =>
1096
1170
  `${stateFileRules}
1097
1171
  - Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
1172
+ ${criteriaBinding(true)}
1098
1173
  ${
1099
1174
  afterRefactor === true
1100
1175
  ? `- A reviewed refactor has just landed and may already carry part of the change,