@pmelab/gtd 38.0.0 → 38.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -0
- package/dist/chunks/{reserved-sfMdy7v7.mjs → reserved-CfQ8eurM.mjs} +55 -11
- package/dist/chunks/{tail-Cgca5Gp-.mjs → tail-C7yaWvaS.mjs} +1 -1
- package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
- package/dist/gtd.bundle.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +158 -42
- 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 +230 -70
- package/package.json +1 -1
- package/src/testkit/steps.ts +4 -0
- package/src/wire/ContextLimit.ts +1 -1
- package/src/workflows/feature/continuation-cap.feature +65 -0
- package/src/workflows/feature/continuation-rebuild.feature +88 -0
- package/src/workflows/feature/continuation-slices.feature +64 -0
- package/src/workflows/feature/continuation.feature +205 -0
- package/src/workflows/feature/criteria.ts +11 -0
- package/src/workflows/feature/refactor.ts +64 -30
- package/src/workflows/feature/seam.ts +102 -30
- package/src/workflows/shared/continuation.ts +92 -0
- package/src/workflows/shared/options.ts +2 -0
- package/src/workflows/shared/steps.ts +1 -0
- package/src/workflows/shared/text.test.ts +6 -2
- package/src/workflows/shared/text.ts +104 -8
- package/src/workflows/shared/vars.ts +5 -0
|
@@ -18,13 +18,15 @@ import {
|
|
|
18
18
|
run,
|
|
19
19
|
scope,
|
|
20
20
|
slice as sliced,
|
|
21
|
+
start,
|
|
21
22
|
vars,
|
|
22
23
|
wrote,
|
|
23
24
|
type Change,
|
|
24
25
|
type DiffShapeResult,
|
|
25
26
|
type KeptTip,
|
|
26
27
|
} from "../../flows/index.js"
|
|
27
|
-
import {
|
|
28
|
+
import { continuing, phase, type Handoff, type Phase } from "../shared/continuation.js"
|
|
29
|
+
import { coder, FEEDBACK, HANDOFF, REFACTOR_NOTE, REQUIREMENTS, SLICES } from "../shared/steps.js"
|
|
28
30
|
import * as t from "../shared/text.js"
|
|
29
31
|
import { defaults } from "../shared/vars.js"
|
|
30
32
|
import { green, requireCoverage } from "./coverage.js"
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
criterionIds,
|
|
33
35
|
requireCriteria,
|
|
34
36
|
requireCriteriaFrozen,
|
|
37
|
+
requireCriteriaIfEdited,
|
|
35
38
|
sliceIds,
|
|
36
39
|
type CriteriaSlice,
|
|
37
40
|
} from "./criteria.js"
|
|
@@ -60,14 +63,47 @@ export const judgeSeam = (tip: KeptTip): Promise<DiffShapeResult> =>
|
|
|
60
63
|
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path),
|
|
61
64
|
})
|
|
62
65
|
|
|
63
|
-
export const naivePass = (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
export const naivePass = (
|
|
67
|
+
retry?: t.NaiveRetry,
|
|
68
|
+
slice?: CriteriaSlice,
|
|
69
|
+
from?: Handoff,
|
|
70
|
+
): Promise<void> =>
|
|
71
|
+
t.agentWithSkills("naive", t.naivePassPrompt(retry, sliceIds(slice), from), {
|
|
72
|
+
label:
|
|
73
|
+
from !== undefined
|
|
74
|
+
? "Continuing the naive pass"
|
|
75
|
+
: retry === undefined
|
|
76
|
+
? "Writing the naive pass"
|
|
77
|
+
: "Rewriting the naive pass",
|
|
66
78
|
keep: true,
|
|
67
79
|
model: coder(),
|
|
68
80
|
system: t.builderSystem(),
|
|
69
81
|
})
|
|
70
82
|
|
|
83
|
+
const handoffOf = (tip: KeptTip, slice: CriteriaSlice | undefined): Handoff | undefined => {
|
|
84
|
+
const note = tip.changes.find((c) => c.path === HANDOFF && c.after !== undefined)
|
|
85
|
+
if (note === undefined) return undefined
|
|
86
|
+
return {
|
|
87
|
+
snapshot: tip.hash,
|
|
88
|
+
note: note.after!.trim(),
|
|
89
|
+
open: unnamed(idsOf(REQUIREMENTS, slice), acceptanceTests(tip.changes)),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A naive pass, continued until a pass ends without a handoff note: the snapshot is its kept tip. */
|
|
94
|
+
const naiveContinued = (
|
|
95
|
+
retry: t.NaiveRetry | undefined,
|
|
96
|
+
slice: CriteriaSlice | undefined,
|
|
97
|
+
naive: Phase,
|
|
98
|
+
) =>
|
|
99
|
+
continuing(naive, async (from, fresh) => {
|
|
100
|
+
const tip = await fresh(async () => {
|
|
101
|
+
await naivePass(retry, slice, from)
|
|
102
|
+
return kept("naive")!
|
|
103
|
+
})
|
|
104
|
+
return { result: tip, handoff: handoffOf(tip, slice) }
|
|
105
|
+
})
|
|
106
|
+
|
|
71
107
|
// Only a string an import, `require` or `mock` call takes: a URL route or path-like data is not one.
|
|
72
108
|
const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g
|
|
73
109
|
const EXTENSION = /\.[\w-]+$/
|
|
@@ -258,6 +294,56 @@ const applied = async (tip: string): Promise<boolean> => {
|
|
|
258
294
|
return !wrote(FEEDBACK) && codeChanges().length > 0
|
|
259
295
|
}
|
|
260
296
|
|
|
297
|
+
/** `carried` limited to the criteria `.gtd/REQUIREMENTS.md` lists now; a file listing none narrows nothing. */
|
|
298
|
+
const settled = (
|
|
299
|
+
carried: readonly string[] | undefined,
|
|
300
|
+
slice: CriteriaSlice | undefined,
|
|
301
|
+
): readonly string[] => {
|
|
302
|
+
const listed = idsOf(REQUIREMENTS, slice)
|
|
303
|
+
if (carried === undefined) return listed
|
|
304
|
+
return listed.length === 0 ? carried : carried.filter((id) => listed.includes(id))
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// A snapshot lands on the branch as it is; the health loop greens the whole build after.
|
|
308
|
+
// Open IDs count every test line since the process start, so an amend lap's earlier tests close theirs.
|
|
309
|
+
const build = (options: ImplementOptions): Promise<void> =>
|
|
310
|
+
continuing(phase("build", requireCriteriaIfEdited), async (from, fresh) => {
|
|
311
|
+
await fresh(() =>
|
|
312
|
+
t.agentWithSkills(
|
|
313
|
+
"building",
|
|
314
|
+
t.implementBuildingPrompt(
|
|
315
|
+
options.reference,
|
|
316
|
+
options.tier,
|
|
317
|
+
options.amend,
|
|
318
|
+
options.afterRefactor,
|
|
319
|
+
flagModeOn(),
|
|
320
|
+
options.rebuild,
|
|
321
|
+
sliceIds(options.slice),
|
|
322
|
+
from,
|
|
323
|
+
),
|
|
324
|
+
{
|
|
325
|
+
label: from === undefined ? "Implementing" : "Continuing the implementation",
|
|
326
|
+
model: coder(),
|
|
327
|
+
system: t.builderSystem(),
|
|
328
|
+
...(options.afterRefactor === true ? { allowEmpty: true } : {}),
|
|
329
|
+
},
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
requireCriteriaFrozen(REQUIREMENTS)
|
|
333
|
+
// Committed by the snapshot, so a turn stopped again may leave it as it was: present means stopped.
|
|
334
|
+
const note = read(HANDOFF)
|
|
335
|
+
if (note === undefined) return { result: undefined }
|
|
336
|
+
const ids = settled(options.rebuild?.acceptance?.ids, options.slice)
|
|
337
|
+
return {
|
|
338
|
+
result: undefined,
|
|
339
|
+
handoff: {
|
|
340
|
+
snapshot: head(),
|
|
341
|
+
note: note.trim(),
|
|
342
|
+
open: unnamed(ids, acceptanceTests(changesSince(start()))),
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
|
|
261
347
|
export function implement(options?: ImplementOptions): Promise<void>
|
|
262
348
|
/** @deprecated Pass `implement({ reference, tier, amend })`. */
|
|
263
349
|
export function implement(
|
|
@@ -276,27 +362,11 @@ export function implement(
|
|
|
276
362
|
const since = head()
|
|
277
363
|
const acceptance = options.rebuild?.acceptance
|
|
278
364
|
const built = options.naive === undefined || !(await applied(options.naive))
|
|
279
|
-
if (built)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
options.tier,
|
|
285
|
-
options.amend,
|
|
286
|
-
options.afterRefactor,
|
|
287
|
-
flagModeOn(),
|
|
288
|
-
options.rebuild,
|
|
289
|
-
sliceIds(options.slice),
|
|
290
|
-
),
|
|
291
|
-
{
|
|
292
|
-
label: "Implementing",
|
|
293
|
-
model: coder(),
|
|
294
|
-
system: t.builderSystem(),
|
|
295
|
-
...(options.afterRefactor === true ? { allowEmpty: true } : {}),
|
|
296
|
-
},
|
|
297
|
-
)
|
|
298
|
-
requireCriteriaFrozen(REQUIREMENTS)
|
|
299
|
-
if (built && acceptance !== undefined) requireAcceptance(acceptance, since)
|
|
365
|
+
if (built) await build(options)
|
|
366
|
+
else requireCriteriaFrozen(REQUIREMENTS)
|
|
367
|
+
// Only the criteria still listed: a person may narrow them at `implement.oversized`.
|
|
368
|
+
if (built && acceptance !== undefined)
|
|
369
|
+
requireAcceptance({ ...acceptance, ids: settled(acceptance.ids, options.slice) }, since)
|
|
300
370
|
// Fix turns are held to the carried tests too: deleting a red one must not buy a green suite.
|
|
301
371
|
await green(
|
|
302
372
|
built && acceptance !== undefined ? () => requireAcceptanceKept(acceptance) : undefined,
|
|
@@ -311,9 +381,11 @@ export const closeOut = (): Promise<void> => scope("implement", () => closing(fa
|
|
|
311
381
|
|
|
312
382
|
/** A slice with `more` after it keeps the criteria and the slicing for the next one. */
|
|
313
383
|
const closing = (more: boolean): Promise<void> =>
|
|
314
|
-
run(
|
|
315
|
-
|
|
316
|
-
|
|
384
|
+
run(
|
|
385
|
+
"closing",
|
|
386
|
+
removeScript(more ? [REFACTOR_NOTE, HANDOFF] : [REQUIREMENTS, REFACTOR_NOTE, SLICES, HANDOFF]),
|
|
387
|
+
{ label: "Closing out the implementation" },
|
|
388
|
+
)
|
|
317
389
|
|
|
318
390
|
interface Decision {
|
|
319
391
|
readonly reference: string
|
|
@@ -392,10 +464,10 @@ const ruled = async (tip: KeptTip, again: Vetting["again"]): Promise<Decision |
|
|
|
392
464
|
const decide = (slice: CriteriaSlice | undefined): Promise<Decision> =>
|
|
393
465
|
scope("seam", async () => {
|
|
394
466
|
const vetting: Vetting = { again: budget(), waived: false }
|
|
467
|
+
const naive = phase("naive pass", requireCriteriaIfEdited)
|
|
395
468
|
let retry: t.NaiveRetry | undefined
|
|
396
469
|
for (;;) {
|
|
397
|
-
await
|
|
398
|
-
const tip = kept("naive")!
|
|
470
|
+
const tip = await naiveContinued(retry, slice, naive)
|
|
399
471
|
const outcome =
|
|
400
472
|
(await vetted(tip, vetting, slice)) ??
|
|
401
473
|
(flagModeOn() ? await ruled(tip, vetting.again) : { reference: tip.hash, tip })
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { human, refuse, scope, vars } from "../../flows/index.js"
|
|
2
|
+
import * as t from "./text.js"
|
|
3
|
+
import type { Handoff } from "./text.js"
|
|
4
|
+
import { defaults } from "./vars.js"
|
|
5
|
+
|
|
6
|
+
export type { Handoff }
|
|
7
|
+
|
|
8
|
+
export interface Turned<T> {
|
|
9
|
+
readonly result: T
|
|
10
|
+
/** Set when the turn wrote a handoff note, i.e. stopped at its context limit. */
|
|
11
|
+
readonly handoff?: Handoff | undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Runs a turn's agent step in a conversation of its own, or in the caller's. */
|
|
15
|
+
export type Fresh = <T>(fn: () => Promise<T>) => Promise<T>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One phase's continuations, shared by every `continuing()` call in it (a naive
|
|
19
|
+
* retry, a refactor lap) so the caps hold per phase. `spent`: once a turn hit
|
|
20
|
+
* its context limit, resuming that conversation would only hit it again.
|
|
21
|
+
*/
|
|
22
|
+
export interface Phase {
|
|
23
|
+
readonly name: string
|
|
24
|
+
spent: boolean
|
|
25
|
+
count: number
|
|
26
|
+
stalled: number
|
|
27
|
+
/** Run after a landing at `oversized`, where a person may narrow the requirements; absent, the gate does not suggest it. */
|
|
28
|
+
readonly steered?: (() => void) | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const phase = (name: string, steered?: () => void): Phase => ({
|
|
32
|
+
name,
|
|
33
|
+
spent: false,
|
|
34
|
+
count: 0,
|
|
35
|
+
stalled: 0,
|
|
36
|
+
steered,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const own: Fresh = (fn) => fn()
|
|
40
|
+
// Every `scope()` call is a new conversation; the names keep continuations
|
|
41
|
+
// apart in history from later turns of a phase that merely start afresh.
|
|
42
|
+
const continued: Fresh = (fn) => scope("continue", fn)
|
|
43
|
+
const afresh: Fresh = (fn) => scope("fresh", fn)
|
|
44
|
+
|
|
45
|
+
const setting = (name: "continuations" | "continuationStall", least: number): number => {
|
|
46
|
+
const raw = vars[name] ?? defaults[name]!
|
|
47
|
+
const n = Number(raw)
|
|
48
|
+
if (raw.trim() === "" || !Number.isInteger(n) || n < least)
|
|
49
|
+
refuse(`gtd: ${name} must be a whole number of at least ${least}, got ${JSON.stringify(raw)}`)
|
|
50
|
+
return n
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run `turn` until one ends without a handoff note: each stopped turn is picked
|
|
55
|
+
* up by a continuation in a fresh conversation. Past `continuations` of them in
|
|
56
|
+
* the phase, or after `continuationStall` in a row that close nothing still
|
|
57
|
+
* open, the phase rests at `oversized` for a person; landing there grants a new round.
|
|
58
|
+
*/
|
|
59
|
+
export const continuing = async <T>(
|
|
60
|
+
phase: Phase,
|
|
61
|
+
turn: (from: Handoff | undefined, fresh: Fresh) => Promise<Turned<T>>,
|
|
62
|
+
): Promise<T> => {
|
|
63
|
+
const cap = setting("continuations", 0)
|
|
64
|
+
const stall = setting("continuationStall", 1)
|
|
65
|
+
let last = await turn(undefined, phase.spent ? afresh : own)
|
|
66
|
+
while (last.handoff !== undefined) {
|
|
67
|
+
phase.spent = true
|
|
68
|
+
const from = last.handoff
|
|
69
|
+
if (phase.count >= cap || phase.stalled >= stall) {
|
|
70
|
+
await human("oversized", {
|
|
71
|
+
message: t.oversizedMessage(
|
|
72
|
+
phase.name,
|
|
73
|
+
from,
|
|
74
|
+
phase.count >= cap ? { cap } : { stall },
|
|
75
|
+
phase.steered !== undefined,
|
|
76
|
+
),
|
|
77
|
+
label: "Too big for its context",
|
|
78
|
+
acceptClean: true,
|
|
79
|
+
})
|
|
80
|
+
phase.steered?.()
|
|
81
|
+
phase.count = 0
|
|
82
|
+
phase.stalled = 0
|
|
83
|
+
}
|
|
84
|
+
last = await turn(from, continued)
|
|
85
|
+
phase.count++
|
|
86
|
+
// Nothing countable left open is no stall: only the cap stops such a phase.
|
|
87
|
+
const closed =
|
|
88
|
+
from.open.length === 0 || from.open.some((item) => !(last.handoff?.open ?? []).includes(item))
|
|
89
|
+
phase.stalled = closed ? 0 : phase.stalled + 1
|
|
90
|
+
}
|
|
91
|
+
return last.result
|
|
92
|
+
}
|
|
@@ -24,6 +24,8 @@ export const ShippedOptions = Schema.Struct({
|
|
|
24
24
|
sliceMaxCriteria: setting,
|
|
25
25
|
qualityReviews: setting,
|
|
26
26
|
refactorQualityReviews: setting,
|
|
27
|
+
continuations: setting,
|
|
28
|
+
continuationStall: setting,
|
|
27
29
|
testCommand: setting,
|
|
28
30
|
plannerModel: setting,
|
|
29
31
|
coderModel: setting,
|
|
@@ -15,6 +15,7 @@ export const REFACTOR = ".gtd/REFACTOR.md"
|
|
|
15
15
|
export const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md"
|
|
16
16
|
export const QUALITY = ".gtd/QUALITY.md"
|
|
17
17
|
export const SLICES = ".gtd/SLICES.md"
|
|
18
|
+
export const HANDOFF = ".gtd/HANDOFF.md"
|
|
18
19
|
|
|
19
20
|
export const planner = (): string => env.plannerModel ?? ""
|
|
20
21
|
export const coder = (): string => env.coderModel ?? ""
|
|
@@ -976,7 +976,9 @@ describe("the seam phase's texts", () => {
|
|
|
976
976
|
it("has a red retry pick up the kept attempt instead of starting over", () => {
|
|
977
977
|
const prompt = renderText(() => naivePassPrompt({ red: "abc123" }))
|
|
978
978
|
expect(prompt).toContain("left the suite red")
|
|
979
|
-
expect(prompt).toContain(
|
|
979
|
+
expect(prompt).toContain(
|
|
980
|
+
"git diff --binary --no-color --no-ext-diff --no-textconv abc123^ abc123 | git apply",
|
|
981
|
+
)
|
|
980
982
|
expect(prompt).toContain(".gtd/FEEDBACK.md")
|
|
981
983
|
expect(prompt).not.toContain("rejected")
|
|
982
984
|
})
|
|
@@ -1042,7 +1044,9 @@ describe("the seam phase's texts", () => {
|
|
|
1042
1044
|
}
|
|
1043
1045
|
const prompt = renderText(() => naivePassPrompt(fault))
|
|
1044
1046
|
expect(prompt).toContain("names AC-2")
|
|
1045
|
-
expect(prompt).toContain(
|
|
1047
|
+
expect(prompt).toContain(
|
|
1048
|
+
"git diff --binary --no-color --no-ext-diff --no-textconv abc123^ abc123 | git apply",
|
|
1049
|
+
)
|
|
1046
1050
|
const message = renderText(() => seamSteerMessage(fault))
|
|
1047
1051
|
expect(message).toContain("names AC-2")
|
|
1048
1052
|
expect(message).toContain("`seam.check` runs its suite")
|
|
@@ -375,6 +375,7 @@ export const refactorBuildingPrompt = ({
|
|
|
375
375
|
reference,
|
|
376
376
|
feedback = false,
|
|
377
377
|
note = false,
|
|
378
|
+
from,
|
|
378
379
|
}: {
|
|
379
380
|
pkg: string
|
|
380
381
|
strays: readonly string[]
|
|
@@ -382,6 +383,8 @@ export const refactorBuildingPrompt = ({
|
|
|
382
383
|
reference?: string | undefined
|
|
383
384
|
feedback?: boolean | undefined
|
|
384
385
|
note?: boolean | undefined
|
|
386
|
+
/** A continuation of a refactor turn that stopped at its context limit; `open` are its untouched paths. */
|
|
387
|
+
from?: Handoff | undefined
|
|
385
388
|
}): string =>
|
|
386
389
|
`${stateFileRules}
|
|
387
390
|
- The package to implement is \`${pkg}\`; never delete it
|
|
@@ -410,8 +413,9 @@ ${
|
|
|
410
413
|
refactor that touches none of it is wrong, however clean — never
|
|
411
414
|
swap in an unrelated tidy-up
|
|
412
415
|
`
|
|
413
|
-
}
|
|
414
|
-
|
|
416
|
+
}${handoffRule()}
|
|
417
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
418
|
+
${from === undefined ? "" : continuationLines(from)}`
|
|
415
419
|
|
|
416
420
|
export const refactorFixPrompt = (strays: readonly string[], pattern: string): string =>
|
|
417
421
|
`${stateFileRules}
|
|
@@ -1198,8 +1202,8 @@ export type NaiveRetry = SeamRejection | NaiveRed | NaiveFault
|
|
|
1198
1202
|
const isRed = (retry: NaiveRetry): retry is NaiveRed => "red" in retry
|
|
1199
1203
|
const isFault = (retry: NaiveRetry): retry is NaiveFault => "fault" in retry
|
|
1200
1204
|
|
|
1201
|
-
const pickUp = (hash: string): string =>
|
|
1202
|
-
`Pick it up — \`git diff --binary ${hash}^ ${hash} | git apply\``
|
|
1205
|
+
const pickUp = (hash: string, exclude?: string): string =>
|
|
1206
|
+
`Pick it up — \`git diff --binary --no-color --no-ext-diff --no-textconv ${hash}^ ${hash}${exclude === undefined ? "" : ` -- ${quote(":(top)")} ${quote(`:(top,exclude)${exclude}`)}`} | git apply\``
|
|
1203
1207
|
|
|
1204
1208
|
const criteriaBinding = (refused: boolean): string =>
|
|
1205
1209
|
`- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
|
|
@@ -1227,7 +1231,72 @@ ${listed(retry.strays)}
|
|
|
1227
1231
|
Start over and choose a different approach; do not rebuild the same diff.
|
|
1228
1232
|
`
|
|
1229
1233
|
|
|
1230
|
-
|
|
1234
|
+
/** A turn that stopped at its context limit: the snapshot it left, its handoff note, what it left open. */
|
|
1235
|
+
export interface Handoff {
|
|
1236
|
+
/** The commit holding the stopped turn's work: its kept tip, or the turn's own commit. */
|
|
1237
|
+
readonly snapshot: string
|
|
1238
|
+
readonly note: string
|
|
1239
|
+
readonly open: readonly string[]
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const quoted = (text: string): string =>
|
|
1243
|
+
text
|
|
1244
|
+
.trimEnd()
|
|
1245
|
+
.split("\n")
|
|
1246
|
+
.map((line) => (line === "" ? "" : ` > ${line}`))
|
|
1247
|
+
.join("\n")
|
|
1248
|
+
|
|
1249
|
+
/** Every turn a continuation may pick up tells its agent where the note goes. */
|
|
1250
|
+
const handoffRule = (): string =>
|
|
1251
|
+
`- Should you be told to stop at a context limit, write the handoff note to
|
|
1252
|
+
\`.gtd/HANDOFF.md\` — what you touched, what was next — and finish your turn:
|
|
1253
|
+
a fresh turn picks up from what you leave, broken or not. That file is the one
|
|
1254
|
+
exception to any path rule here; write it at no other time`
|
|
1255
|
+
|
|
1256
|
+
/** `pickUp`: how the snapshot gets into the tree — absent when it already is. */
|
|
1257
|
+
const continuationLines = (from: Handoff, pickUp?: string): string =>
|
|
1258
|
+
`
|
|
1259
|
+
Your previous turn stopped at its context limit; this fresh turn picks up from
|
|
1260
|
+
its snapshot (\`${from.snapshot}\`). ${pickUp === undefined ? "Its work is already in the tree." : `${pickUp}.`} Its handoff note:
|
|
1261
|
+
|
|
1262
|
+
${quoted(from.note === "" ? "(empty)" : from.note)}
|
|
1263
|
+
|
|
1264
|
+
Still open: ${from.open.length === 0 ? "nothing the workflow can count — finish what the note says is next" : from.open.join(", ")}.
|
|
1265
|
+
${pickUp === undefined ? "Delete `.gtd/HANDOFF.md` once you are done; rewrite it should you be stopped again.\n" : ""}`
|
|
1266
|
+
|
|
1267
|
+
export const oversizedMessage = (
|
|
1268
|
+
phase: string,
|
|
1269
|
+
from: Handoff,
|
|
1270
|
+
why: { readonly cap: number } | { readonly stall: number },
|
|
1271
|
+
narrowable = true,
|
|
1272
|
+
): string =>
|
|
1273
|
+
`The ${phase} keeps outgrowing its context: ${
|
|
1274
|
+
"cap" in why
|
|
1275
|
+
? why.cap === 0
|
|
1276
|
+
? "continuations are off (`continuations` is 0) and the turn stopped at its context limit"
|
|
1277
|
+
: `${why.cap} continuation${why.cap === 1 ? "" : "s"} ran and the last still stopped at its context limit`
|
|
1278
|
+
: `the last ${why.stall} continuation${why.stall === 1 ? "" : "s"} closed nothing still open`
|
|
1279
|
+
}. The last snapshot is \`${from.snapshot}\`; still open: ${from.open.length === 0 ? "nothing countable" : from.open.join(", ")}.
|
|
1280
|
+
|
|
1281
|
+
Its handoff note:
|
|
1282
|
+
|
|
1283
|
+
${quoted(from.note === "" ? "(empty)" : from.note)}
|
|
1284
|
+
|
|
1285
|
+
Land to go on: a fresh round of continuations picks up from that snapshot${
|
|
1286
|
+
narrowable
|
|
1287
|
+
? ` — narrow
|
|
1288
|
+
\`.gtd/REQUIREMENTS.md\` first if the change is too big, or raise the context limit`
|
|
1289
|
+
: ` — raise
|
|
1290
|
+
the context limit first if the refactor is too big`
|
|
1291
|
+
}. \`gtd abandon\` gives the change up.
|
|
1292
|
+
`
|
|
1293
|
+
|
|
1294
|
+
/** `from`: a continuation of a pass that stopped at its context limit, in place of `retry`'s pick-up. */
|
|
1295
|
+
export const naivePassPrompt = (
|
|
1296
|
+
retry?: NaiveRetry,
|
|
1297
|
+
slice?: readonly string[],
|
|
1298
|
+
from?: Handoff,
|
|
1299
|
+
): string =>
|
|
1231
1300
|
`${stateFileRules}
|
|
1232
1301
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1233
1302
|
${sliceLine(slice)}${criteriaBinding(false)}
|
|
@@ -1245,9 +1314,34 @@ ${sliceLine(slice)}${criteriaBinding(false)}
|
|
|
1245
1314
|
- Run \`${env.testCommand}\` and finish only once it is green with every
|
|
1246
1315
|
criterion's test passing: the next step runs it on your diff and sends a red
|
|
1247
1316
|
pass back
|
|
1248
|
-
- Leave \`.gtd/\` untouched
|
|
1317
|
+
- Leave \`.gtd/\` untouched, a handoff note excepted
|
|
1318
|
+
${handoffRule()}
|
|
1249
1319
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
1250
|
-
${
|
|
1320
|
+
${
|
|
1321
|
+
from !== undefined
|
|
1322
|
+
? `${continuationLines(from, pickUp(from.snapshot, ".gtd/HANDOFF.md"))}${
|
|
1323
|
+
retry === undefined
|
|
1324
|
+
? ""
|
|
1325
|
+
: isFault(retry)
|
|
1326
|
+
? `
|
|
1327
|
+
The pass this continues was a retry: its tests fell short, and still must not:
|
|
1328
|
+
${retry.fault}
|
|
1329
|
+
`
|
|
1330
|
+
: isRed(retry)
|
|
1331
|
+
? `
|
|
1332
|
+
The pass this continues was a retry after a red suite (\`.gtd/FEEDBACK.md\`
|
|
1333
|
+
holds the output): finish only once the suite is green.
|
|
1334
|
+
`
|
|
1335
|
+
: `
|
|
1336
|
+
The pass this continues was a retry after a \`reject\`: keep away from the
|
|
1337
|
+
approach that changed these files outside the expected set:
|
|
1338
|
+
${listed(retry.strays)}
|
|
1339
|
+
`
|
|
1340
|
+
}`
|
|
1341
|
+
: retry === undefined
|
|
1342
|
+
? ""
|
|
1343
|
+
: naiveRetry(retry)
|
|
1344
|
+
}`
|
|
1251
1345
|
|
|
1252
1346
|
/** The naive pass's kept tip, its acceptance test files and the criterion IDs they name. */
|
|
1253
1347
|
export interface Acceptance {
|
|
@@ -1340,6 +1434,7 @@ export const implementBuildingPrompt = (
|
|
|
1340
1434
|
flagMode = true,
|
|
1341
1435
|
rebuild: Rebuild = {},
|
|
1342
1436
|
slice?: readonly string[],
|
|
1437
|
+
from?: Handoff,
|
|
1343
1438
|
): string =>
|
|
1344
1439
|
`${stateFileRules}
|
|
1345
1440
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
@@ -1366,9 +1461,10 @@ ${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${
|
|
|
1366
1461
|
- Write the tests first, then the code that makes them pass. Name each
|
|
1367
1462
|
criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
|
|
1368
1463
|
green a judge checks every ID has a passing test, and sends uncovered ones back
|
|
1464
|
+
${handoffRule()}
|
|
1369
1465
|
- Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
|
|
1370
1466
|
finish your turn
|
|
1371
|
-
`
|
|
1467
|
+
${from === undefined ? "" : continuationLines(from)}`
|
|
1372
1468
|
|
|
1373
1469
|
export const implementFixPrompt = (): string =>
|
|
1374
1470
|
`${stateFileRules}
|
|
@@ -34,6 +34,11 @@ export const defaults: Readonly<Record<string, string>> = {
|
|
|
34
34
|
qualityReviews:
|
|
35
35
|
"correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
|
|
36
36
|
refactorQualityReviews: "correctness, ponytail-review, conventions",
|
|
37
|
+
// Per phase (naive pass, refactor, build): continuations of a turn that
|
|
38
|
+
// stopped at its context limit before a person is asked, and how many in a
|
|
39
|
+
// row may close nothing still open.
|
|
40
|
+
continuations: "3",
|
|
41
|
+
continuationStall: "2",
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
/** Environment settings. */
|