@pmelab/gtd 56.0.0 → 57.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 (46) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +20 -27
  3. package/dist/chunks/{reserved-B8EC-WE8.mjs → reserved-X2RkNHin.mjs} +34 -116
  4. package/dist/chunks/{review-HdrX0FJ7.mjs → review-v7sh5GF4.mjs} +2 -2
  5. package/dist/chunks/{tail-BSLah2mA.mjs → tail-S04KEewi.mjs} +1 -1
  6. package/dist/workflows/cleanup.mjs +2 -2
  7. package/dist/workflows/feature.mjs +52 -162
  8. package/dist/workflows/fix.mjs +2 -2
  9. package/dist/workflows/review.mjs +3 -3
  10. package/dist/workflows/rollout.mjs +1 -1
  11. package/dist/workflows.mjs +81 -274
  12. package/package.json +1 -1
  13. package/src/workflows/feature/criteria-coverage.feature +0 -1
  14. package/src/workflows/feature/criteria.test.ts +1 -41
  15. package/src/workflows/feature/criteria.ts +2 -14
  16. package/src/workflows/feature/fast-path.feature +0 -1
  17. package/src/workflows/feature/feature.ts +5 -6
  18. package/src/workflows/feature/flag-mode.feature +1 -1
  19. package/src/workflows/feature/lens-models.feature +0 -1
  20. package/src/workflows/feature/lens-review.feature +0 -1
  21. package/src/workflows/feature/naive-test.feature +258 -0
  22. package/src/workflows/feature/one-step-land.feature +0 -1
  23. package/src/workflows/feature/refactor-absorbs.feature +0 -1
  24. package/src/workflows/feature/refactor-first.feature +0 -1
  25. package/src/workflows/feature/refactor-rebuild.feature +0 -1
  26. package/src/workflows/feature/refactor-review.feature +0 -1
  27. package/src/workflows/feature/review-gate.feature +1 -2
  28. package/src/workflows/feature/reviewer-walkthrough.feature +0 -1
  29. package/src/workflows/feature/seam-rejection.feature +0 -1
  30. package/src/workflows/feature/seam.feature +0 -1
  31. package/src/workflows/feature/seam.test.ts +1 -2
  32. package/src/workflows/feature/seam.ts +56 -10
  33. package/src/workflows/feature/standards.feature +0 -1
  34. package/src/workflows/shared/options.ts +9 -1
  35. package/src/workflows/shared/prose.ts +0 -6
  36. package/src/workflows/shared/skills.test.ts +0 -4
  37. package/src/workflows/shared/skills.ts +0 -5
  38. package/src/workflows/shared/steps.ts +1 -1
  39. package/src/workflows/shared/text.test.ts +22 -41
  40. package/src/workflows/shared/text.ts +42 -142
  41. package/src/workflows/shared/vars.ts +2 -3
  42. package/src/workflows/feature/hands-on-skip.feature +0 -316
  43. package/src/workflows/feature/prototype-rounds.feature +0 -156
  44. package/src/workflows/feature/prototype.feature +0 -428
  45. package/src/workflows/feature/prototype.test.ts +0 -43
  46. package/src/workflows/feature/prototype.ts +0 -198
@@ -1,198 +0,0 @@
1
- import { answered, changes, human, judge, read, refuse, scope, vars } from "../../flows/index.js"
2
- import { HEADING, markFenced } from "../shared/fences.js"
3
- import { coder, PROTOTYPE, REQUIREMENTS } from "../shared/steps.js"
4
- import * as t from "../shared/text.js"
5
- import { defaults } from "../shared/vars.js"
6
- import { green } from "./green.js"
7
- import { criteriaOf, criterionIds, handsOnIds, requireCriteriaFrozen } from "./criteria.js"
8
-
9
- /** Only a `no` at this or above skips a criterion: no verdict, or an unsure one, sends it to the hands-on test. */
10
- const UNOBSERVABLE_MIN_P = 0.5
11
-
12
- /** The `prototypeRounds` setting. Anything but a non-negative integer fails rather than guess a cap. */
13
- export const prototypeRounds = (): number => {
14
- const value = vars.prototypeRounds ?? defaults.prototypeRounds!
15
- if (!/^(0|[1-9]\d*)$/.test(value))
16
- refuse(`gtd: prototypeRounds must be a non-negative integer, got ${JSON.stringify(value)}`)
17
- return Number(value)
18
- }
19
-
20
- export type Verdict = "pass" | "fail" | "blocked"
21
-
22
- export interface Item {
23
- /** A criterion ID, or `Problem <title>`. */
24
- readonly subject: string
25
- readonly verdict: Verdict
26
- }
27
-
28
- export interface Report {
29
- readonly unguided: boolean
30
- readonly items: readonly Item[]
31
- /** Headings that read as an item but miss the shape: a failure there would go unseen. */
32
- readonly malformed: readonly string[]
33
- }
34
-
35
- const CHUNK = /^(AC-\d+|Problem\b.*?)\s*:\s*(pass|fail|blocked)$/i
36
- const UNGUIDED = /^no testing instructions$/i
37
-
38
- /** The `## ` chunks of a prototype report; a heading in neither shape is prose, not an item. */
39
- export const reportOf = (text: string): Report => {
40
- let unguided = false
41
- const items: Item[] = []
42
- const malformed: string[] = []
43
- for (const { line, fenced } of markFenced(text)) {
44
- const heading = fenced ? null : HEADING.exec(line)
45
- if (heading?.[1] !== "##") continue
46
- if (UNGUIDED.test(heading[2]!)) unguided = true
47
- const chunk = CHUNK.exec(heading[2]!)
48
- if (chunk === null && /^(ac-\d|problem)/i.test(heading[2]!)) malformed.push(heading[2]!)
49
- if (chunk !== null)
50
- items.push({
51
- subject: /^ac-/i.test(chunk[1]!) ? chunk[1]!.toUpperCase() : chunk[1]!,
52
- verdict: chunk[2]!.toLowerCase() as Verdict,
53
- })
54
- }
55
- return { unguided, items, malformed }
56
- }
57
-
58
- /**
59
- * The report the testing turn left, refused unless it is the only change and
60
- * names each of `ids` once, with its open items: any other criterion is not
61
- * this test's to fix.
62
- */
63
- const requireReport = (
64
- ids: readonly string[],
65
- retest: boolean,
66
- ): { readonly unguided: boolean; readonly open: readonly Item[] } => {
67
- const others = changes()
68
- .map((c) => c.path)
69
- .filter((path) => path !== PROTOTYPE)
70
- if (others.length > 0)
71
- refuse(
72
- `gtd land: prototype-report: test in a disposable place and change nothing but ${PROTOTYPE} — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}`,
73
- )
74
- const text = read(PROTOTYPE)
75
- if (text === undefined) return refuse(`gtd land: prototype-report: write ${PROTOTYPE}`)
76
- const report = reportOf(text)
77
- if (report.malformed.length > 0)
78
- refuse(
79
- `gtd land: prototype-report: head each chunk \`## <ID>: pass|fail|blocked\` or \`## Problem <title>: pass|fail|blocked\` — these miss it:\n${report.malformed.map((h) => ` - ${h}`).join("\n")}`,
80
- )
81
- // Only a first test with nothing else in it: a chunk beside it was tested after all, and a
82
- // re-test's open items must reach a person, not slip past as untested.
83
- if (report.unguided && report.items.length === 0 && !retest) return { unguided: true, open: [] }
84
- const elsewhere = criterionIds(read(REQUIREMENTS) ?? "").filter((id) => !ids.includes(id))
85
- const count = (id: string): number => report.items.filter((i) => i.subject === id).length
86
- const wrong = ids.filter((id) => count(id) !== 1)
87
- if (wrong.length > 0)
88
- refuse(
89
- `gtd land: prototype-report: ${PROTOTYPE} needs exactly one chunk headed \`## <ID>: pass|fail|blocked\` for ${wrong.join(", ")}`,
90
- )
91
- // A mistyped ID stays open: only a known criterion outside `ids` is not this test's to fix.
92
- const open = report.items.filter((i) => i.verdict !== "pass" && !elsewhere.includes(i.subject))
93
- return { unguided: false, open }
94
- }
95
-
96
- /**
97
- * The criteria a person can observe by using the change, by the judge: those
98
- * tagged `(no hands-on)` are never asked about.
99
- */
100
- const observable = async (): Promise<string[]> => {
101
- const requirements = read(REQUIREMENTS) ?? ""
102
- const own = handsOnIds(requirements)
103
- const criteria = (criteriaOf(requirements) ?? []).filter((c) => own.includes(c.id))
104
- if (criteria.length === 0) return []
105
- const { answers } = await judge("observable", {
106
- questions: criteria.map(t.observableQuestion),
107
- evidence: { requirements },
108
- message: t.observableMessage(),
109
- label: "Judging which criteria to test by hand",
110
- })
111
- return own.filter((id) => !answered(answers[id], "no", UNOBSERVABLE_MIN_P))
112
- }
113
-
114
- const frozen = (): void => requireCriteriaFrozen(REQUIREMENTS)
115
-
116
- const testing = async (
117
- ids: readonly string[],
118
- retest: boolean,
119
- ): Promise<ReturnType<typeof requireReport>> => {
120
- const skipped = criterionIds(read(REQUIREMENTS) ?? "").filter((id) => !ids.includes(id))
121
- await t.agentWithSkills("testing", t.prototypeTestingPrompt(skipped, retest), {
122
- label: retest ? "Re-testing the change by hand" : "Testing the change by hand",
123
- file: PROTOTYPE,
124
- model: coder(),
125
- system: t.testerSystem(),
126
- // A test that saw exactly what the last round saw leaves the report as it was.
127
- allowEmpty: true,
128
- })
129
- return requireReport(ids, retest)
130
- }
131
-
132
- const fixing = async (): Promise<void> => {
133
- await scope("fix", () =>
134
- t.agentWithSkills("fixing", t.prototypeFixingPrompt(), {
135
- label: "Fixing what the hands-on test found",
136
- file: PROTOTYPE,
137
- model: coder(),
138
- system: t.builderSystem(),
139
- // A `fail` the tester got wrong has nothing to fix: the re-test says so, or the cap ends it.
140
- allowEmpty: true,
141
- }),
142
- )
143
- // The re-test may land empty: a report the fixer edited would pass untested.
144
- if (changes().some((c) => c.path === PROTOTYPE))
145
- refuse(`gtd land: prototype-fix: leave ${PROTOTYPE} as it is — the next round re-tests from it`)
146
- frozen()
147
- }
148
-
149
- /** A person's rest; resolves `true` when they changed something to test again, `false` on a clean overrule. */
150
- const askPerson = async (name: string, message: string, label: string): Promise<boolean> => {
151
- await human(name, { message, label, file: PROTOTYPE, acceptClean: true })
152
- // A note in the report alone tests nothing new.
153
- if (!changes().some((c) => c.path !== PROTOTYPE)) return false
154
- frozen()
155
- return true
156
- }
157
-
158
- /**
159
- * Use the change by hand: a testing turn reports on every criterion a person
160
- * can observe, a fix turn takes each failure, and the change is tested again —
161
- * until the report is clean, or `prototypeRounds` fixes are spent and a person
162
- * decides. A change with no such criterion is not tested by hand.
163
- */
164
- export const prototypeTest = async (): Promise<void> => {
165
- const cap = prototypeRounds()
166
- if (cap === 0) return
167
- await scope("prototype", async () => {
168
- const ids = await observable()
169
- if (ids.length === 0) return
170
- let fixes = 0
171
- let retest = false
172
- for (;;) {
173
- const { unguided, open } = await testing(ids, retest)
174
- retest = !unguided
175
- if (unguided) {
176
- const instructed = await askPerson(
177
- "unguided",
178
- t.prototypeUnguidedMessage(),
179
- "No way to test by hand",
180
- )
181
- if (!instructed) return
182
- } else if (open.length === 0) return
183
- else if (fixes < cap && open.some((i) => i.verdict === "fail")) {
184
- fixes++
185
- await fixing()
186
- } else {
187
- const changed = await askPerson(
188
- "open",
189
- t.prototypeOpenMessage(open, fixes),
190
- "Hands-on test still failing",
191
- )
192
- if (!changed) return
193
- fixes = 0
194
- }
195
- await green()
196
- }
197
- })
198
- }