@pmelab/gtd 32.1.0 → 33.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +18 -15
- package/dist/chunks/{reserved-2fV0mj14.mjs → reserved-BU5Zi-nX.mjs} +67 -11
- package/dist/chunks/{tail-BynhCfuv.mjs → tail-DuXanjIY.mjs} +1 -1
- package/dist/testkit.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +156 -32
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +220 -40
- package/package.json +1 -1
- package/src/workflows/feature/acceptance-criteria.feature +8 -0
- package/src/workflows/feature/fast-path.feature +8 -2
- package/src/workflows/feature/feature.test.ts +19 -9
- package/src/workflows/feature/naive-acceptance.feature +140 -0
- package/src/workflows/feature/one-step-land.feature +8 -2
- package/src/workflows/feature/refactor-absorbs.feature +6 -1
- package/src/workflows/feature/refactor-first.feature +9 -3
- package/src/workflows/feature/refactor-lens-models.feature +6 -1
- package/src/workflows/feature/refactor-review.feature +50 -5
- package/src/workflows/feature/refactor.ts +4 -3
- package/src/workflows/feature/risk-tier.feature +10 -0
- package/src/workflows/feature/seam-rejection.feature +19 -6
- package/src/workflows/feature/seam.feature +70 -16
- package/src/workflows/feature/seam.test.ts +173 -3
- package/src/workflows/feature/seam.ts +195 -30
- package/src/workflows/shared/text.test.ts +40 -0
- package/src/workflows/shared/text.ts +97 -16
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import { posix } from "node:path"
|
|
1
2
|
import {
|
|
2
3
|
changes,
|
|
4
|
+
check,
|
|
5
|
+
env,
|
|
3
6
|
head,
|
|
4
7
|
human,
|
|
5
8
|
judgeDiffShape,
|
|
6
9
|
kept,
|
|
10
|
+
quote,
|
|
11
|
+
read,
|
|
7
12
|
removeScript,
|
|
8
13
|
run,
|
|
9
14
|
scope,
|
|
10
15
|
slice,
|
|
11
16
|
vars,
|
|
17
|
+
type Change,
|
|
12
18
|
type DiffShapeResult,
|
|
13
19
|
type KeptTip,
|
|
14
20
|
} from "../../flows/index.js"
|
|
@@ -16,12 +22,19 @@ import { healthy } from "../shared/health.js"
|
|
|
16
22
|
import { coder, FEEDBACK, REQUIREMENTS } from "../shared/steps.js"
|
|
17
23
|
import * as t from "../shared/text.js"
|
|
18
24
|
import { defaults } from "../shared/vars.js"
|
|
19
|
-
import { requireCriteria, requireCriteriaFrozen } from "./criteria.js"
|
|
20
|
-
import {
|
|
25
|
+
import { criteriaOf, requireCriteria, requireCriteriaFrozen } from "./criteria.js"
|
|
26
|
+
import {
|
|
27
|
+
COMMENT,
|
|
28
|
+
holdsCallSites,
|
|
29
|
+
isTestFile,
|
|
30
|
+
refactorStrays,
|
|
31
|
+
reviewedRefactor,
|
|
32
|
+
} from "./refactor.js"
|
|
33
|
+
import { isCode } from "./review.js"
|
|
21
34
|
import { flagModeOn } from "../shared/options.js"
|
|
22
35
|
import type { RiskTier } from "./tier.js"
|
|
23
36
|
|
|
24
|
-
/** Naive-pass retries after a `reject
|
|
37
|
+
/** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
|
|
25
38
|
const NAIVE_RETRIES = 2
|
|
26
39
|
|
|
27
40
|
/** A change to an existing file outside the allowlist is a stray even when the task needs it: the judge decides. */
|
|
@@ -35,14 +48,121 @@ export const judgeSeam = (tip: KeptTip): Promise<DiffShapeResult> =>
|
|
|
35
48
|
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path),
|
|
36
49
|
})
|
|
37
50
|
|
|
38
|
-
export const naivePass = (
|
|
39
|
-
t.agentWithSkills("naive", t.naivePassPrompt(
|
|
40
|
-
label:
|
|
51
|
+
export const naivePass = (retry?: t.NaiveRetry): Promise<void> =>
|
|
52
|
+
t.agentWithSkills("naive", t.naivePassPrompt(retry), {
|
|
53
|
+
label: retry === undefined ? "Writing the naive pass" : "Rewriting the naive pass",
|
|
41
54
|
keep: true,
|
|
42
55
|
model: coder(),
|
|
43
56
|
system: t.builderSystem(),
|
|
44
57
|
})
|
|
45
58
|
|
|
59
|
+
// Only a string an import, `require` or `mock` call takes: a URL route or path-like data is not one.
|
|
60
|
+
const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g
|
|
61
|
+
const EXTENSION = /\.[\w-]+$/
|
|
62
|
+
const ALIAS = /^(?:[@~]\/|#)/
|
|
63
|
+
|
|
64
|
+
/** The specifiers an added `path` answers to: itself, without its extension, and its directory for an `index`. */
|
|
65
|
+
const namesOf = (path: string): string[] => {
|
|
66
|
+
const bare = path.replace(EXTENSION, "")
|
|
67
|
+
return posix.basename(bare) === "index" ? [path, bare, posix.dirname(bare)] : [path, bare]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The repository paths `test` imports, each with and without an extension: a
|
|
72
|
+
* relative specifier resolved from its directory, an aliased one (`@/`, `~/`,
|
|
73
|
+
* `#`) as a path tail, anything else as written. Comments are skipped; a
|
|
74
|
+
* computed import goes unseen.
|
|
75
|
+
*/
|
|
76
|
+
const importsOf = (test: Change): { readonly path: string; readonly tail: boolean }[] =>
|
|
77
|
+
[...(test.after ?? "").replace(COMMENT, "").matchAll(IMPORTED)].flatMap(([, , spec]) => {
|
|
78
|
+
const relative = /^\.\.?(\/|$)/.test(spec!)
|
|
79
|
+
const tail = ALIAS.test(spec!)
|
|
80
|
+
const path = relative ? posix.join(posix.dirname(test.path), spec!) : spec!.replace(ALIAS, "")
|
|
81
|
+
return [path, path.replace(EXTENSION, "")].map((p) => ({ path: p, tail }))
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const reaches = (imports: ReturnType<typeof importsOf>, added: string): boolean =>
|
|
85
|
+
namesOf(added).some((name) =>
|
|
86
|
+
imports.some(({ path, tail }) => name === path || (tail && name.endsWith(`/${path}`))),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const named = (id: string, text: string): boolean =>
|
|
90
|
+
new RegExp(`(?<![\\w-])${id}(?!\\d)`).test(text)
|
|
91
|
+
|
|
92
|
+
/** The lines `change` added: an ID an earlier change left in the file does not count. */
|
|
93
|
+
const addedLines = (change: Change): string => {
|
|
94
|
+
const before = new Set((change.before ?? "").split("\n"))
|
|
95
|
+
return change
|
|
96
|
+
.after!.split("\n")
|
|
97
|
+
.filter((line) => !before.has(line))
|
|
98
|
+
.join("\n")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Why `tip`'s tests fall short, or `undefined`: a criterion ID of `file` no
|
|
103
|
+
* test line of the pass names, or a test importing a file the pass added —
|
|
104
|
+
* naive structure a reset and refactor replaces, which the tests must survive.
|
|
105
|
+
*/
|
|
106
|
+
export const acceptanceFault = (file: string, tip: KeptTip): string | undefined => {
|
|
107
|
+
const ids = (criteriaOf(read(file) ?? "") ?? []).map((c) => c.id).filter((id) => id !== "")
|
|
108
|
+
const tests = tip.changes.filter(
|
|
109
|
+
(c) => c.after !== undefined && isCode(c.path) && isTestFile(c.path),
|
|
110
|
+
)
|
|
111
|
+
const lines = tests.map(addedLines)
|
|
112
|
+
const untested = ids.filter((id) => !lines.some((text) => named(id, text)))
|
|
113
|
+
if (untested.length > 0)
|
|
114
|
+
return `acceptance-tests: no test of the naive pass names ${untested.join(", ")} — write a behaviour-level test for each criterion with its ID in the test or scenario name, e.g. "[${untested[0]}] …"`
|
|
115
|
+
const added = tip.changes
|
|
116
|
+
.filter((c) => c.status === "added" && isCode(c.path) && !isTestFile(c.path))
|
|
117
|
+
.map((c) => c.path)
|
|
118
|
+
const reaching = tests
|
|
119
|
+
.filter((test) => holdsCallSites(test.path))
|
|
120
|
+
.flatMap((test) => {
|
|
121
|
+
const imports = importsOf(test)
|
|
122
|
+
return added
|
|
123
|
+
.filter((path) => reaches(imports, path))
|
|
124
|
+
.map((path) => ` - ${test.path} → ${path}`)
|
|
125
|
+
})
|
|
126
|
+
if (reaching.length > 0)
|
|
127
|
+
return `acceptance-internals: these tests import files the naive pass added — test through code that existed before it (a public entry point, an existing module's exports):\n${reaching.join("\n")}`
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const restore = (path: string): string =>
|
|
132
|
+
`if git cat-file -e ${quote(`HEAD:${path}`)} 2>/dev/null; then git checkout -q HEAD -- ${quote(path)}; else rm -f -- ${quote(path)}; fi`
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Run the suite with `tip`'s diff applied, then take it out again; resolves
|
|
136
|
+
* `true` when green. The pass's paths go back to HEAD before (an interrupted
|
|
137
|
+
* run left them applied) and after; afterwards so does anything else the run
|
|
138
|
+
* changed, judged against what was already dirty, which is left alone.
|
|
139
|
+
*/
|
|
140
|
+
const naiveGreen = (tip: KeptTip): Promise<boolean> => {
|
|
141
|
+
const paths = tip.changes.map((c) => restore(c.path))
|
|
142
|
+
return check(
|
|
143
|
+
"check",
|
|
144
|
+
[
|
|
145
|
+
...paths,
|
|
146
|
+
"dirty=$(git -c core.quotePath=false ls-files -m -d -o --exclude-standard)",
|
|
147
|
+
`if ! git diff --binary ${quote(`${tip.hash}^`)} ${quote(tip.hash)} -- | git apply; then`,
|
|
148
|
+
" echo 'gtd: the kept naive pass no longer applies to HEAD; no test ran'",
|
|
149
|
+
" exit 1",
|
|
150
|
+
"fi",
|
|
151
|
+
"(",
|
|
152
|
+
env.testCommand ?? "",
|
|
153
|
+
")",
|
|
154
|
+
"code=$?",
|
|
155
|
+
...paths,
|
|
156
|
+
"git -c core.quotePath=false ls-files -m -d -o --exclude-standard | while IFS= read -r path; do",
|
|
157
|
+
' printf \'%s\\n\' "$dirty" | grep -qxF -- "$path" && continue',
|
|
158
|
+
' if git cat-file -e "HEAD:$path" 2>/dev/null; then git checkout -q HEAD -- "$path"; else rm -f -- "$path"; fi',
|
|
159
|
+
"done",
|
|
160
|
+
'exit "$code"',
|
|
161
|
+
].join("\n"),
|
|
162
|
+
{ report: FEEDBACK, label: "Running the naive pass's tests" },
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
46
166
|
export interface ImplementOptions {
|
|
47
167
|
readonly reference?: string | undefined
|
|
48
168
|
readonly tier?: RiskTier | undefined
|
|
@@ -103,34 +223,79 @@ interface Decision {
|
|
|
103
223
|
readonly shape?: DiffShapeResult
|
|
104
224
|
}
|
|
105
225
|
|
|
226
|
+
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
227
|
+
const steer = async (retry: t.NaiveRetry): Promise<boolean> => {
|
|
228
|
+
await human("steer", {
|
|
229
|
+
message: t.seamSteerMessage(retry),
|
|
230
|
+
label: "Steer or overrule the naive pass",
|
|
231
|
+
file: REQUIREMENTS,
|
|
232
|
+
acceptClean: true,
|
|
233
|
+
})
|
|
234
|
+
if (changes().length === 0) return false
|
|
235
|
+
requireCriteria(REQUIREMENTS)
|
|
236
|
+
return true
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
type Verdict =
|
|
240
|
+
| { readonly kind: "retry"; readonly retry: t.NaiveRetry }
|
|
241
|
+
| { readonly kind: "steered" }
|
|
242
|
+
| { readonly kind: "overruled" }
|
|
243
|
+
|
|
244
|
+
/** Rejects, test faults and red checks share one budget; a steer or an overrule refills it. */
|
|
245
|
+
const budget = (): ((next: t.NaiveRetry) => Promise<Verdict>) => {
|
|
246
|
+
let retries = 0
|
|
247
|
+
return async (next) => {
|
|
248
|
+
if (retries < NAIVE_RETRIES) {
|
|
249
|
+
retries++
|
|
250
|
+
return { kind: "retry", retry: next }
|
|
251
|
+
}
|
|
252
|
+
const steered = await steer(next)
|
|
253
|
+
retries = 0
|
|
254
|
+
// A steered or overruled red pass leaves its report behind: nothing reads it any more.
|
|
255
|
+
if (read(FEEDBACK) !== undefined)
|
|
256
|
+
await run("clear", removeScript([FEEDBACK]), { label: "Clearing the red check" })
|
|
257
|
+
return { kind: steered ? "steered" : "overruled" }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
interface Vetting {
|
|
262
|
+
readonly again: ReturnType<typeof budget>
|
|
263
|
+
/** Set once a human overrules a test fault: the rule does not fit this change, so later passes skip it. */
|
|
264
|
+
waived: boolean
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
|
|
268
|
+
const vetted = async (tip: KeptTip, vetting: Vetting): Promise<Verdict | undefined> => {
|
|
269
|
+
const fault = vetting.waived ? undefined : acceptanceFault(REQUIREMENTS, tip)
|
|
270
|
+
if (fault !== undefined) {
|
|
271
|
+
const verdict = await vetting.again({ fault, hash: tip.hash })
|
|
272
|
+
if (verdict.kind !== "overruled") return verdict
|
|
273
|
+
vetting.waived = true
|
|
274
|
+
}
|
|
275
|
+
if (await naiveGreen(tip)) return undefined
|
|
276
|
+
const verdict = await vetting.again({ red: tip.hash })
|
|
277
|
+
return verdict.kind === "overruled" ? undefined : verdict
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
|
|
281
|
+
const ruled = async (tip: KeptTip, again: Vetting["again"]): Promise<Decision | Verdict> => {
|
|
282
|
+
const shape = await judgeSeam(tip)
|
|
283
|
+
if (shape.choice === "land") return { reference: tip.hash }
|
|
284
|
+
if (shape.choice === "refactor-first") return { reference: tip.hash, shape }
|
|
285
|
+
const next = await again({ strays: shape.strays, p: shape.judgment?.answers.shape?.p ?? 0 })
|
|
286
|
+
return next.kind === "overruled" ? { reference: tip.hash } : next
|
|
287
|
+
}
|
|
288
|
+
|
|
106
289
|
const decide = (): Promise<Decision> =>
|
|
107
290
|
scope("seam", async () => {
|
|
108
|
-
|
|
109
|
-
let
|
|
291
|
+
const vetting: Vetting = { again: budget(), waived: false }
|
|
292
|
+
let retry: t.NaiveRetry | undefined
|
|
110
293
|
for (;;) {
|
|
111
|
-
await naivePass(
|
|
294
|
+
await naivePass(retry)
|
|
112
295
|
const tip = kept("naive")!
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
return { reference: tip.hash, shape }
|
|
117
|
-
}
|
|
118
|
-
rejection = { strays: shape.strays, p: shape.judgment?.answers.shape?.p ?? 0 }
|
|
119
|
-
if (retries < NAIVE_RETRIES) {
|
|
120
|
-
retries++
|
|
121
|
-
continue
|
|
122
|
-
}
|
|
123
|
-
await human("steer", {
|
|
124
|
-
message: t.seamSteerMessage(rejection),
|
|
125
|
-
label: "Steer or overrule the rejected naive pass",
|
|
126
|
-
file: REQUIREMENTS,
|
|
127
|
-
acceptClean: true,
|
|
128
|
-
})
|
|
129
|
-
// Any edit steers, with a fresh budget and no stale rejection; clean overrules.
|
|
130
|
-
if (changes().length === 0) return { reference: tip.hash }
|
|
131
|
-
requireCriteria(REQUIREMENTS)
|
|
132
|
-
rejection = undefined
|
|
133
|
-
retries = 0
|
|
296
|
+
const outcome = (await vetted(tip, vetting)) ?? (await ruled(tip, vetting.again))
|
|
297
|
+
if (!("kind" in outcome)) return outcome
|
|
298
|
+
retry = outcome.kind === "retry" ? outcome.retry : undefined
|
|
134
299
|
}
|
|
135
300
|
})
|
|
136
301
|
|
|
@@ -965,6 +965,22 @@ describe("the seam phase's texts", () => {
|
|
|
965
965
|
expect(prompt).toContain("solved the task the wrong way")
|
|
966
966
|
})
|
|
967
967
|
|
|
968
|
+
it("asks for a breadth-first pass with ID-tagged behaviour tests, green before it ends", () => {
|
|
969
|
+
const prompt = renderText(() => naivePassPrompt())
|
|
970
|
+
expect(prompt).toContain("breadth-first")
|
|
971
|
+
expect(prompt).toContain("`[AC-3] …`")
|
|
972
|
+
expect(prompt).toContain("never a file this pass adds")
|
|
973
|
+
expect(prompt).toContain("green")
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
it("has a red retry pick up the kept attempt instead of starting over", () => {
|
|
977
|
+
const prompt = renderText(() => naivePassPrompt({ red: "abc123" }))
|
|
978
|
+
expect(prompt).toContain("left the suite red")
|
|
979
|
+
expect(prompt).toContain("git diff --binary abc123^ abc123 | git apply")
|
|
980
|
+
expect(prompt).toContain(".gtd/FEEDBACK.md")
|
|
981
|
+
expect(prompt).not.toContain("rejected")
|
|
982
|
+
})
|
|
983
|
+
|
|
968
984
|
it("cites the reference as reading material only and asks for a new release flag", () => {
|
|
969
985
|
const prompt = renderText(() => implementBuildingPrompt("abc123"))
|
|
970
986
|
expect(prompt).toContain("git show abc123")
|
|
@@ -1019,6 +1035,30 @@ describe("the seam phase's texts", () => {
|
|
|
1019
1035
|
expect(message).toContain("src/flows/helpers.ts")
|
|
1020
1036
|
})
|
|
1021
1037
|
|
|
1038
|
+
it("hands a faulty pass back with the fault and the kept attempt to pick up", () => {
|
|
1039
|
+
const fault = {
|
|
1040
|
+
fault: "acceptance-tests: no test of the naive pass names AC-2",
|
|
1041
|
+
hash: "abc123",
|
|
1042
|
+
}
|
|
1043
|
+
const prompt = renderText(() => naivePassPrompt(fault))
|
|
1044
|
+
expect(prompt).toContain("names AC-2")
|
|
1045
|
+
expect(prompt).toContain("git diff --binary abc123^ abc123 | git apply")
|
|
1046
|
+
const message = renderText(() => seamSteerMessage(fault))
|
|
1047
|
+
expect(message).toContain("names AC-2")
|
|
1048
|
+
expect(message).toContain("`seam.check` runs its suite")
|
|
1049
|
+
expect(message).toContain("Overrule")
|
|
1050
|
+
})
|
|
1051
|
+
|
|
1052
|
+
it("names the red suite, its report and what overruling it means", () => {
|
|
1053
|
+
const message = renderText(() => seamSteerMessage({ red: "abc123" }))
|
|
1054
|
+
expect(message).toContain("The last\nattempt left the suite red")
|
|
1055
|
+
expect(message).toContain(".gtd/FEEDBACK.md")
|
|
1056
|
+
expect(message).toContain("edit `.gtd/REQUIREMENTS.md`")
|
|
1057
|
+
expect(message).toContain("Overrule")
|
|
1058
|
+
expect(message).toContain("seam judge")
|
|
1059
|
+
expect(message).not.toContain("outside the expected set")
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1022
1062
|
it("names every unreadable path for a by-hand landing", () => {
|
|
1023
1063
|
const message = renderText(() =>
|
|
1024
1064
|
seamByHandMessage(["vendor/a.bin", "src/flows/helpers.ts"], ["vendor/a.bin"], "abc123"),
|
|
@@ -1134,31 +1134,72 @@ export interface SeamRejection {
|
|
|
1134
1134
|
readonly p: number
|
|
1135
1135
|
}
|
|
1136
1136
|
|
|
1137
|
+
/** A naive pass whose suite `seam.check` found red; `red` is its kept tip. */
|
|
1138
|
+
export interface NaiveRed {
|
|
1139
|
+
readonly red: string
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/** A naive pass whose tests break the acceptance-test rules; `hash` is its kept tip. */
|
|
1143
|
+
export interface NaiveFault {
|
|
1144
|
+
readonly fault: string
|
|
1145
|
+
readonly hash: string
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
export type NaiveRetry = SeamRejection | NaiveRed | NaiveFault
|
|
1149
|
+
|
|
1150
|
+
const isRed = (retry: NaiveRetry): retry is NaiveRed => "red" in retry
|
|
1151
|
+
const isFault = (retry: NaiveRetry): retry is NaiveFault => "fault" in retry
|
|
1152
|
+
|
|
1153
|
+
const pickUp = (hash: string): string =>
|
|
1154
|
+
`Pick it up — \`git diff --binary ${hash}^ ${hash} | git apply\``
|
|
1155
|
+
|
|
1137
1156
|
const criteriaBinding = (refused: boolean): string =>
|
|
1138
1157
|
`- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
|
|
1139
1158
|
the file, never from memory, and stop only when each one holds. They are
|
|
1140
1159
|
settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`
|
|
1141
1160
|
|
|
1142
|
-
|
|
1161
|
+
const naiveRetry = (retry: NaiveRetry): string =>
|
|
1162
|
+
isFault(retry)
|
|
1163
|
+
? `
|
|
1164
|
+
Your previous attempt's (\`${retry.hash}\`) tests fall short:
|
|
1165
|
+
${retry.fault}
|
|
1166
|
+
${pickUp(retry.hash)} — and fix its tests; do not start over.
|
|
1167
|
+
`
|
|
1168
|
+
: isRed(retry)
|
|
1169
|
+
? `
|
|
1170
|
+
Your previous attempt (\`${retry.red}\`) left the suite red; \`.gtd/FEEDBACK.md\`
|
|
1171
|
+
holds the output. ${pickUp(retry.red)}
|
|
1172
|
+
— and fix it until the suite is green; do not start over.
|
|
1173
|
+
`
|
|
1174
|
+
: `
|
|
1175
|
+
Your previous attempt was rejected: the judge answered \`reject\` (p=${retry.p}),
|
|
1176
|
+
meaning the naive pass solved the task the wrong way — discard it and
|
|
1177
|
+
re-implement. It changed these files outside the expected set:
|
|
1178
|
+
${listed(retry.strays)}
|
|
1179
|
+
Start over and choose a different approach; do not rebuild the same diff.
|
|
1180
|
+
`
|
|
1181
|
+
|
|
1182
|
+
export const naivePassPrompt = (retry?: NaiveRetry): string =>
|
|
1143
1183
|
`${stateFileRules}
|
|
1144
1184
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1145
1185
|
${criteriaBinding(false)}
|
|
1146
1186
|
- Implement them the quickest way that works — the naive version. Do not
|
|
1147
1187
|
restructure, abstract or refactor around the change; a later step judges
|
|
1148
1188
|
the shape of your diff and a reviewer makes it fit
|
|
1149
|
-
-
|
|
1189
|
+
- Work breadth-first: rough out every part of the change before finishing any
|
|
1190
|
+
one, so a run cut off midway still shows the whole shape
|
|
1191
|
+
- Write behaviour-level acceptance tests, at least one per criterion, each
|
|
1192
|
+
naming its ID in the test or scenario name (\`[AC-3] …\`). Test through code
|
|
1193
|
+
that existed before this pass — a public entry point, the CLI, an existing
|
|
1194
|
+
module's exports — never a file this pass adds: that is naive structure a
|
|
1195
|
+
later reset and refactor replaces. A pass that leaves a criterion's ID out
|
|
1196
|
+
of its tests, or whose test imports a file it added, is sent back
|
|
1197
|
+
- Run \`${env.testCommand}\` and finish only once it is green with every
|
|
1198
|
+
criterion's test passing: the next step runs it on your diff and sends a red
|
|
1199
|
+
pass back
|
|
1200
|
+
- Leave \`.gtd/\` untouched
|
|
1150
1201
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
1151
|
-
${
|
|
1152
|
-
rejection === undefined
|
|
1153
|
-
? ""
|
|
1154
|
-
: `
|
|
1155
|
-
Your previous attempt was rejected: the judge answered \`reject\` (p=${rejection.p}),
|
|
1156
|
-
meaning the naive pass solved the task the wrong way — discard it and
|
|
1157
|
-
re-implement. It changed these files outside the expected set:
|
|
1158
|
-
${listed(rejection.strays)}
|
|
1159
|
-
Start over and choose a different approach; do not rebuild the same diff.
|
|
1160
|
-
`
|
|
1161
|
-
}`
|
|
1202
|
+
${retry === undefined ? "" : naiveRetry(retry)}`
|
|
1162
1203
|
|
|
1163
1204
|
export const implementBuildingPrompt = (
|
|
1164
1205
|
reference: string | undefined,
|
|
@@ -1216,9 +1257,49 @@ ${fixFeedbackPrompt}
|
|
|
1216
1257
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
1217
1258
|
`
|
|
1218
1259
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1260
|
+
const steerChoices = (overrule: string): string => `Two ways forward (then run \`gtd land\`):
|
|
1261
|
+
- **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
|
|
1262
|
+
pass runs again with a fresh retry budget
|
|
1263
|
+
- **Overrule** — ${overrule}
|
|
1264
|
+
|
|
1265
|
+
An edit together with an approval counts as steer. Only
|
|
1266
|
+
\`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
|
|
1267
|
+
branch unjudged.
|
|
1268
|
+
`
|
|
1269
|
+
|
|
1270
|
+
export const seamSteerMessage = (retry: NaiveRetry): string =>
|
|
1271
|
+
isFault(retry)
|
|
1272
|
+
? `The naive pass failed three times and the retries are spent. The last
|
|
1273
|
+
attempt's tests (\`git show ${retry.hash}\`) fall short:
|
|
1274
|
+
${retry.fault}
|
|
1275
|
+
|
|
1276
|
+
${steerChoices(`land a clean tree to go on with the last attempt as it is:
|
|
1277
|
+
\`seam.check\` runs its suite and the seam judge rules on its shape. Use it
|
|
1278
|
+
when the rule cannot fit this change (tests the globs do not see, or no
|
|
1279
|
+
entry point that existed before). Risk: a criterion may have no test that
|
|
1280
|
+
survives a later refactor — the choice is recorded in history.`)}`
|
|
1281
|
+
: isRed(retry)
|
|
1282
|
+
? `The naive pass failed three times and the retries are spent. The last
|
|
1283
|
+
attempt left the suite red: \`.gtd/FEEDBACK.md\` holds the last run's output; \`git show ${retry.red}\` shows
|
|
1284
|
+
the last attempt.
|
|
1285
|
+
|
|
1286
|
+
Two ways forward (then run \`gtd land\`):
|
|
1287
|
+
- **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
|
|
1288
|
+
pass runs again with a fresh retry budget
|
|
1289
|
+
- **Overrule** — land a clean tree to go on with the last attempt as it is:
|
|
1290
|
+
the seam judge rules on its shape, and the implementation's health loop
|
|
1291
|
+
must turn the suite green. Risk: the criteria's tests never passed on the
|
|
1292
|
+
naive pass — the choice is recorded in history.
|
|
1293
|
+
|
|
1294
|
+
An edit together with an approval counts as steer. Only
|
|
1295
|
+
\`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
|
|
1296
|
+
branch unjudged.
|
|
1297
|
+
`
|
|
1298
|
+
: seamRejectedMessage(retry)
|
|
1299
|
+
|
|
1300
|
+
const seamRejectedMessage = (rejection: SeamRejection): string =>
|
|
1301
|
+
`The naive pass failed three times and the retries are spent. The judge
|
|
1302
|
+
rejected the last attempt: it changed these files outside the expected set:
|
|
1222
1303
|
${listed(rejection.strays)}
|
|
1223
1304
|
|
|
1224
1305
|
Two ways forward (then run \`gtd land\`):
|