@pmelab/gtd 36.0.0 → 38.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 +30 -9
- package/claude/hooks/context.ts +66 -0
- package/claude/hooks/drive.ts +3 -0
- package/claude/hooks/register.tsx +69 -6
- package/dist/chunks/{reserved-BAv71I4i.mjs → reserved-sfMdy7v7.mjs} +83 -22
- package/dist/chunks/{tail-kar1zhOb.mjs → tail-Cgca5Gp-.mjs} +1 -1
- package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
- package/dist/gtd.bundle.mjs +498 -56
- package/dist/testkit.mjs +115 -3
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +277 -58
- 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 +355 -75
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +10 -3
- package/src/cli/Cli.ts +76 -1
- package/src/cli/Program.test.ts +1 -0
- package/src/cli/Program.ts +28 -0
- package/src/cli/ToolCommands.ts +3 -0
- package/src/edge/Edge.ts +11 -0
- package/src/flows/runtime.ts +11 -0
- package/src/replay/Replay.ts +51 -2
- package/src/testkit/Layers.ts +1 -0
- package/src/testkit/steps.ts +35 -0
- package/src/wire/BeatDocument.ts +12 -2
- package/src/wire/BeatStatus.ts +3 -0
- package/src/wire/ContextLimit.fixture.ts +30 -0
- package/src/wire/ContextLimit.test.ts +96 -0
- package/src/wire/ContextLimit.ts +169 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/types.ts +7 -0
- package/src/workflow/Workflow.ts +9 -1
- package/src/workflows/feature/context-limit.feature +37 -0
- package/src/workflows/feature/coverage.ts +22 -8
- package/src/workflows/feature/criteria.test.ts +17 -1
- package/src/workflows/feature/criteria.ts +26 -3
- package/src/workflows/feature/feature.ts +69 -16
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/refactor-absorbs.feature +12 -5
- package/src/workflows/feature/refactor-first.feature +11 -2
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +269 -0
- package/src/workflows/feature/refactor-review.feature +45 -1
- package/src/workflows/feature/refactor.ts +9 -3
- package/src/workflows/feature/seam.test.ts +108 -3
- package/src/workflows/feature/seam.ts +169 -31
- package/src/workflows/feature/slicing-cap.feature +14 -0
- package/src/workflows/feature/slicing.feature +225 -0
- package/src/workflows/feature/slicing.test.ts +49 -0
- package/src/workflows/feature/slicing.ts +105 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/fences.ts +5 -0
- package/src/workflows/shared/options.test.ts +11 -2
- package/src/workflows/shared/options.ts +19 -4
- package/src/workflows/shared/steps.ts +2 -0
- package/src/workflows/shared/text.ts +129 -28
- package/src/workflows/shared/vars.ts +1 -0
- package/src/workflows/shared/workflows.steps.ts +13 -0
package/README.md
CHANGED
|
@@ -307,6 +307,13 @@ Or skip writing one: **`gtd loop`** is that loop, shipped with gtd. It drives
|
|
|
307
307
|
to real model names through per-machine settings — see
|
|
308
308
|
[`gtd loop`](https://github.com/pmelab/gtd/blob/main/docs/driver.md#the-bundled-driver-gtd-loop).
|
|
309
309
|
|
|
310
|
+
Both it and the Claude Code mod stop an agent turn that outgrows its context:
|
|
311
|
+
past 60% of the model's window the agent is asked to wrap up with a handoff
|
|
312
|
+
note, past 70% (or when the context is about to be compacted) the turn stops and
|
|
313
|
+
nothing lands. A step's `context` option, or the bundled workflows'
|
|
314
|
+
`contextLimit` option, moves the two limits — see
|
|
315
|
+
[Context limits](https://github.com/pmelab/gtd/blob/main/docs/driver.md#context-limits).
|
|
316
|
+
|
|
310
317
|
### Or drive it from inside Claude Code
|
|
311
318
|
|
|
312
319
|
The gtd mod runs the same loop inside an interactive Claude Code session
|
|
@@ -607,13 +614,18 @@ is sent back while a criterion's ID is missing from its tests, a test imports a
|
|
|
607
614
|
file it added, or the suite is red on it. A judge checks whether it changed
|
|
608
615
|
anything outside the expected set (tests, steering files; see `seamAllow`).
|
|
609
616
|
Incidental changes land: the pass itself is the implementation, with no rebuild,
|
|
610
|
-
straight through the health loop and the quality lap. A needed seam change
|
|
611
|
-
|
|
612
|
-
(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
+
straight through the health loop and the quality lap. A needed seam change
|
|
618
|
+
resets: the pass is never applied. An agent reads its whole diff and writes a
|
|
619
|
+
refactor note (`.gtd/REFACTOR-NOTE.md`: which seams, which shape); the seam is
|
|
620
|
+
refactored first, flag-free with the existing suite green, in its own slice with
|
|
621
|
+
its own quality lap (`refactorQualityReviews`) and review; then the feature is
|
|
622
|
+
rebuilt on the refactored base, behind a release flag, with the note and the
|
|
623
|
+
naive diff as reference. The rebuild is refused until its tests carry every
|
|
624
|
+
criterion ID the naive pass's tests named, and the suite must be green with
|
|
625
|
+
them. A wrong approach is retried twice, then rests for you to steer or overrule
|
|
626
|
+
it. `gtd slices` lists the `refactor` and `feature` slices. A refactor that
|
|
627
|
+
already carried the whole feature leaves the rebuild only the carried-over
|
|
628
|
+
tests, and the feature's quality lap covers the whole change. Once the
|
|
617
629
|
implementation is green, a judge checks every criterion ID has a covering test:
|
|
618
630
|
an uncovered one goes back to the agent (twice, then to you) instead of ending
|
|
619
631
|
the feature, and each covered ID is recorded as a `Gtd-Mark: covers=<id>`
|
|
@@ -626,6 +638,15 @@ out is `gtd abandon`. A process from 27.5.0 or later whose judge ruled
|
|
|
626
638
|
that slice. Details in
|
|
627
639
|
[the naive-pass path](docs/configuration.md#the-naive-pass-path).
|
|
628
640
|
|
|
641
|
+
**Coarse slices (`sliceMaxCriteria`, default 8).** A change with more acceptance
|
|
642
|
+
criteria than the cap is built in slices: after design an agent groups related
|
|
643
|
+
criteria (same flow or subsystem) into `.gtd/SLICES.md`, as few slices as the
|
|
644
|
+
cap allows, and each slice — naive pass, refactor and rebuild when judged so,
|
|
645
|
+
health loop, quality lap — runs in order from where the previous one landed.
|
|
646
|
+
`gtd slices` lists them as `feature-1`, `feature-2`, … (`refactor-<n>` ahead of
|
|
647
|
+
a refactored one). A change within the cap is one `feature` slice, as before.
|
|
648
|
+
See [Coarse slices](docs/configuration.md#coarse-slices).
|
|
649
|
+
|
|
629
650
|
**Flag mode (`flagMode`, on by default).** On, the naive pass's judge runs as
|
|
630
651
|
above and a rebuilt implementation is asked for a new release flag (none at a
|
|
631
652
|
`low` risk tier); a pass the judge lets land is not asked for one. A project
|
|
@@ -755,8 +776,8 @@ install for any of these but `./testkit`: gtd resolves a `gtd.config.ts`'s
|
|
|
755
776
|
The `gtd` bin's commands — `next`, `land`, `abandon`, `restore`, `validate`,
|
|
756
777
|
`check`, `uncheck`, `install`, `summary`, `base`, `door`, `doors`, `slices`,
|
|
757
778
|
`version`, `help`, `history list`, `history show`, `history fetch`, `strays`,
|
|
758
|
-
and the tools `ui`, `lsp`, `loop`, `judge` (`judge answer`,
|
|
759
|
-
listed in the
|
|
779
|
+
and the tools `ui`, `lsp`, `loop`, `context-hook`, `judge` (`judge answer`,
|
|
780
|
+
`judge run`) — are listed in the
|
|
760
781
|
[CLI reference](https://github.com/pmelab/gtd/blob/main/docs/cli.md).
|
|
761
782
|
`gtd slices` lists the spans a flow's `slice()` calls mark. It refuses (exit 1)
|
|
762
783
|
when HEAD neither closes nor sits inside a process, or when today's config
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
contextHook,
|
|
3
|
+
contextLimitFault,
|
|
4
|
+
contextWindowFor,
|
|
5
|
+
DEFAULT_CONTEXT_LIMIT,
|
|
6
|
+
parseContextWindow,
|
|
7
|
+
subagentTranscriptPath,
|
|
8
|
+
transcriptTokens,
|
|
9
|
+
} from "../../src/wire/ContextLimit.js"
|
|
10
|
+
import type { ContextDecision, ContextLimit } from "../../src/wire/ContextLimit.js"
|
|
11
|
+
|
|
12
|
+
// The mod's half of `gtd context-hook`: the same decision, for a worker
|
|
13
|
+
// subagent instead of a `claude -p` session.
|
|
14
|
+
|
|
15
|
+
export type Guard = { limit: ContextLimit; window: number }
|
|
16
|
+
|
|
17
|
+
/** A turn's limits from its beat's `context`, the model the subagent runs on and the window override; a bad window is ignored. */
|
|
18
|
+
export function guardOf(
|
|
19
|
+
context: { soft?: number; hard?: number } | undefined,
|
|
20
|
+
model: string | undefined,
|
|
21
|
+
windowEnv: string | undefined,
|
|
22
|
+
sessionWindow?: number,
|
|
23
|
+
): Guard | undefined {
|
|
24
|
+
const limit = { ...DEFAULT_CONTEXT_LIMIT, ...context }
|
|
25
|
+
if (contextLimitFault(limit) !== undefined) return undefined
|
|
26
|
+
// A worker with no model of its own runs on the session's, window included.
|
|
27
|
+
const own = model === undefined ? sessionWindow : undefined
|
|
28
|
+
return { limit, window: parseContextWindow(windowEnv) ?? own ?? contextWindowFor(model) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type HookEvent = {
|
|
32
|
+
hook_event_name: string
|
|
33
|
+
transcript_path: string
|
|
34
|
+
agent_id?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A worker's hook event decided. It is handed the MAIN session's transcript,
|
|
39
|
+
* so the worker's own is read instead.
|
|
40
|
+
*/
|
|
41
|
+
export async function decide(
|
|
42
|
+
e: HookEvent,
|
|
43
|
+
guard: Guard | undefined,
|
|
44
|
+
read: (path: string) => Promise<string | undefined>,
|
|
45
|
+
): Promise<ContextDecision | undefined> {
|
|
46
|
+
const event = e.hook_event_name
|
|
47
|
+
if (!guard || !e.agent_id || (event !== "PostToolUse" && event !== "PreCompact")) return undefined
|
|
48
|
+
const transcript =
|
|
49
|
+
event === "PostToolUse" && e.transcript_path
|
|
50
|
+
? await read(subagentTranscriptPath(e.transcript_path, e.agent_id))
|
|
51
|
+
: undefined
|
|
52
|
+
return contextHook({
|
|
53
|
+
event,
|
|
54
|
+
tokens: transcript === undefined ? undefined : transcriptTokens(transcript),
|
|
55
|
+
...guard,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A decision as a classic PostToolUse result. A hard stop only blocks here:
|
|
61
|
+
* the mod stops the worker's task itself, since `continue: false` from a
|
|
62
|
+
* subagent's hook is not documented to stop just that subagent.
|
|
63
|
+
*/
|
|
64
|
+
export function classicResult(decision: ContextDecision) {
|
|
65
|
+
return { block: decision.kind === "soft" ? decision.reason : decision.stopReason }
|
|
66
|
+
}
|
package/claude/hooks/drive.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type Beat = {
|
|
|
20
20
|
skills?: string[]
|
|
21
21
|
validate?: string
|
|
22
22
|
access?: AccessDef
|
|
23
|
+
context?: { soft?: number; hard?: number }
|
|
23
24
|
judge?: unknown
|
|
24
25
|
changes?: { status: string; path: string }[]
|
|
25
26
|
session?: { id?: string; resume?: boolean | string }
|
|
@@ -42,6 +43,7 @@ export type Turn = {
|
|
|
42
43
|
skills: readonly string[]
|
|
43
44
|
scope: string
|
|
44
45
|
access?: AccessDef
|
|
46
|
+
context?: { soft?: number; hard?: number }
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
// `isLost`: the scope's subagent no longer takes messages.
|
|
@@ -183,6 +185,7 @@ function turnOf(b: Beat): Turn {
|
|
|
183
185
|
skills: b.skills ?? [],
|
|
184
186
|
scope: memory.split("#")[0]!,
|
|
185
187
|
access: b.access,
|
|
188
|
+
context: b.context,
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
191
|
|
|
@@ -14,6 +14,9 @@ import { personaSpec, resumable } from "./persona"
|
|
|
14
14
|
import { JUDGE_SYSTEM, judgePrompt, judgeQuestions, judgeRunEnv, toVerdicts } from "./judge"
|
|
15
15
|
import type { Judgment } from "./judge"
|
|
16
16
|
import { catchFrom, throwTo } from "./handoff"
|
|
17
|
+
import { classicResult, decide, guardOf } from "./context"
|
|
18
|
+
import { contextHook } from "../../src/wire/ContextLimit.js"
|
|
19
|
+
import type { Guard, HookEvent } from "./context"
|
|
17
20
|
import { jobs } from "./jobs"
|
|
18
21
|
import { documents, readDocument } from "./schema"
|
|
19
22
|
import { READ_ONLY_GIT, ship } from "./ship"
|
|
@@ -70,6 +73,23 @@ const ours = new Set<string>()
|
|
|
70
73
|
const accesses = new Map<string, AccessDef>()
|
|
71
74
|
const setAccess = (id: string, a: AccessDef | undefined) =>
|
|
72
75
|
void (a ? accesses.set(id, a) : accesses.delete(id))
|
|
76
|
+
// Each turn agent's context limit, reset with its access; and why a worker
|
|
77
|
+
// was stopped at it, for the turn's end to report.
|
|
78
|
+
const guards = new Map<string, Guard>()
|
|
79
|
+
const contextStops = new Map<string, string>()
|
|
80
|
+
async function workerModel($: $, hint: string | undefined) {
|
|
81
|
+
return subagentModel(hint, {
|
|
82
|
+
smart: await $.env.get("GTD_PLANNERMODEL"),
|
|
83
|
+
base: await $.env.get("GTD_CODERMODEL"),
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
async function setGuard($: $, id: string, t: Pick<Turn, "context" | "model">) {
|
|
87
|
+
const model = await workerModel($, t.model)
|
|
88
|
+
const session = model === undefined ? (await $.session.usage()).context.window : undefined
|
|
89
|
+
const guard = guardOf(t.context, model, await $.env.get("GTD_LOOP_CONTEXT_WINDOW"), session)
|
|
90
|
+
if (guard) guards.set(id, guard)
|
|
91
|
+
else guards.delete(id)
|
|
92
|
+
}
|
|
73
93
|
const waiting = new Map<string, (end: TurnEnd) => void>()
|
|
74
94
|
const ended = new Map<string, TurnEnd>()
|
|
75
95
|
const personas = new Set<string>()
|
|
@@ -122,6 +142,8 @@ async function send($: $, agentId: string, text: string) {
|
|
|
122
142
|
async function forget($: $, memory: string, agentId: string) {
|
|
123
143
|
$.ui.log(lostLine(memory.split("#")[0]!))
|
|
124
144
|
accesses.delete(agentId)
|
|
145
|
+
guards.delete(agentId)
|
|
146
|
+
contextStops.delete(agentId)
|
|
125
147
|
await update($, scopes, (s) => {
|
|
126
148
|
const rest = { ...s }
|
|
127
149
|
if (rest[memory]?.agentId === agentId) delete rest[memory]
|
|
@@ -193,6 +215,7 @@ function io($: $): Io {
|
|
|
193
215
|
if (t.resume && entry && !known) $.ui.log(restartLine(t.scope))
|
|
194
216
|
if (t.resume && known) {
|
|
195
217
|
setAccess(known, t.access)
|
|
218
|
+
await setGuard($, known, t)
|
|
196
219
|
const end = send($, known, t.prompt)
|
|
197
220
|
await update($, run, (r) => ({ ...r, inflight: { agentId: known, memory: t.memory } }))
|
|
198
221
|
const ended = await end
|
|
@@ -205,16 +228,14 @@ function io($: $): Io {
|
|
|
205
228
|
subagentType: await persona($, t),
|
|
206
229
|
prompt: t.prompt,
|
|
207
230
|
description: t.label || "gtd",
|
|
208
|
-
model:
|
|
209
|
-
smart: await $.env.get("GTD_PLANNERMODEL"),
|
|
210
|
-
base: await $.env.get("GTD_CODERMODEL"),
|
|
211
|
-
}),
|
|
231
|
+
model: await workerModel($, t.model),
|
|
212
232
|
cwd: root,
|
|
213
233
|
})
|
|
214
234
|
const id = spawned.agentId
|
|
215
235
|
if (!id) return settle($, { ok: false, why: spawned.deny ?? "subagent spawn refused" })
|
|
216
236
|
ours.add(id)
|
|
217
237
|
setAccess(id, t.access)
|
|
238
|
+
await setGuard($, id, t)
|
|
218
239
|
await update($, agents, (list) => [...list, id].slice(-200))
|
|
219
240
|
await update($, scopes, (s) => ({ ...s, [t.memory]: { agentId: id, persona: name } }))
|
|
220
241
|
await update($, run, (r) => ({ ...r, inflight: { agentId: id, memory: t.memory } }))
|
|
@@ -912,6 +933,40 @@ async function isOurs($: $, id: string) {
|
|
|
912
933
|
return isWorker
|
|
913
934
|
}
|
|
914
935
|
|
|
936
|
+
// `gtd context-hook`'s decision for a worker: past its soft limit it is asked
|
|
937
|
+
// for a handoff note; past its hard limit, or about to compact, its task is
|
|
938
|
+
// stopped and its turn ends as a failure carrying the stop reason. Stopped off
|
|
939
|
+
// the hook, which runs inside the worker's own tool call.
|
|
940
|
+
async function stopWorker($: $, id: string, stopReason: string) {
|
|
941
|
+
if (contextStops.has(id)) return
|
|
942
|
+
contextStops.set(id, stopReason)
|
|
943
|
+
$.clock.after(0, () => {
|
|
944
|
+
void $.tool.call({ tool: "TaskStop", task_id: id }).catch((err: unknown) => {
|
|
945
|
+
$.ui.log(`gtd could not stop a subagent at its context limit: ${String(err)}`)
|
|
946
|
+
})
|
|
947
|
+
})
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function contextGuard($: $, e: HookEvent) {
|
|
951
|
+
const id = e.agent_id
|
|
952
|
+
if (!id || !ours.has(id)) return undefined
|
|
953
|
+
const decision = await decide(e, guards.get(id), async (path) =>
|
|
954
|
+
(await $.fs.exists(path)) ? $.fs.read(path) : undefined,
|
|
955
|
+
)
|
|
956
|
+
if (decision?.kind === "hard") await stopWorker($, id, decision.stopReason)
|
|
957
|
+
return decision && classicResult(decision)
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// A worker's own compaction carries its id here (`classic.PreCompact` is not
|
|
961
|
+
// documented to): compaction about to happen means its limit was missed.
|
|
962
|
+
async function compactGuard($: $, agentId: string | undefined) {
|
|
963
|
+
if (!agentId || !ours.has(agentId) || !guards.has(agentId)) return undefined
|
|
964
|
+
const decision = contextHook({ event: "PreCompact", tokens: undefined, ...guards.get(agentId)! })
|
|
965
|
+
if (decision?.kind !== "hard") return undefined
|
|
966
|
+
await stopWorker($, agentId, decision.stopReason)
|
|
967
|
+
return { skip: decision.stopReason }
|
|
968
|
+
}
|
|
969
|
+
|
|
915
970
|
// What a subagent this mod spawned may not do: write outside its step's
|
|
916
971
|
// access, or, answering into a file, anything but that.
|
|
917
972
|
function subagentDenial(agentId: string, e: Record<string, unknown>) {
|
|
@@ -982,8 +1037,13 @@ export const register: Register = (on) => {
|
|
|
982
1037
|
void launchPending($)
|
|
983
1038
|
}
|
|
984
1039
|
if (e.agentId && ours.has(e.agentId)) {
|
|
985
|
-
const
|
|
986
|
-
|
|
1040
|
+
const stopped = contextStops.get(e.agentId)
|
|
1041
|
+
contextStops.delete(e.agentId)
|
|
1042
|
+
const end: TurnEnd = stopped
|
|
1043
|
+
? { ok: false, why: stopped }
|
|
1044
|
+
: e.reason === "answer"
|
|
1045
|
+
? { ok: true }
|
|
1046
|
+
: { ok: false, why: `agent turn ended: ${e.reason}` }
|
|
987
1047
|
delegates.delete(e.agentId)
|
|
988
1048
|
const resolve = waiting.get(e.agentId)
|
|
989
1049
|
waiting.delete(e.agentId)
|
|
@@ -1014,6 +1074,9 @@ export const register: Register = (on) => {
|
|
|
1014
1074
|
return next(e)
|
|
1015
1075
|
})
|
|
1016
1076
|
|
|
1077
|
+
on("classic.PostToolUse", async ($, e, next) => (await contextGuard($, e)) ?? next(e))
|
|
1078
|
+
on("session.compact", async ($, e, next) => (await compactGuard($, e.agentId)) ?? next(e))
|
|
1079
|
+
|
|
1017
1080
|
// A subagent's hand-back arrives as a prompt to the main session, whose model
|
|
1018
1081
|
// would then act on it. gtd reads the turn's result from git instead.
|
|
1019
1082
|
on("prompt.submit", async ($, e, next) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, displayPath, env, head, human, judge, numeric, quote, read, refuse, scope, skillsFor, start, vars } from "../flows.mjs";
|
|
2
2
|
import { escalation as escalation$1, gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
|
|
3
|
-
import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct, n as Boolean$ } from "./Schema-DO5ttJaI.mjs";
|
|
3
|
+
import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct, n as Boolean$, s as Number$ } from "./Schema-DO5ttJaI.mjs";
|
|
4
4
|
//#region src/workflows/shared/registry.ts
|
|
5
5
|
const FILL = "";
|
|
6
6
|
const commentLength = (rest) => {
|
|
@@ -133,6 +133,7 @@ const defaults = {
|
|
|
133
133
|
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
134
134
|
seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
|
|
135
135
|
flagMode: "true",
|
|
136
|
+
sliceMaxCriteria: "8",
|
|
136
137
|
flag_risk_tier: "0",
|
|
137
138
|
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
|
|
138
139
|
refactorQualityReviews: "correctness, ponytail-review, conventions"
|
|
@@ -592,6 +593,24 @@ printf '%s\\n' ${[
|
|
|
592
593
|
].map(quote).join(" ")} > ${quote(".gtd/REQUIREMENTS.md")}
|
|
593
594
|
`;
|
|
594
595
|
};
|
|
596
|
+
const slicingPrompt = (cap) => `${stateFileRules}
|
|
597
|
+
- Read \`.gtd/REQUIREMENTS.md\`: its \`## Acceptance Criteria\` are settled
|
|
598
|
+
and more than ${cap}, too many to build in one slice
|
|
599
|
+
- Group them into slices in \`.gtd/SLICES.md\`, one \`## <title>\` heading per
|
|
600
|
+
slice over a \`- AC-<n>\` list item per criterion it builds. Put criteria of
|
|
601
|
+
the same flow or subsystem together; every criterion in exactly one slice,
|
|
602
|
+
at most ${cap} per slice
|
|
603
|
+
- Keep it coarse: as few slices as the cap allows, never one per criterion
|
|
604
|
+
- Order the slices so each builds on the ones before: they run in that order,
|
|
605
|
+
each from where the previous one landed
|
|
606
|
+
- Change nothing but \`.gtd/SLICES.md\` — leave the criteria as they are
|
|
607
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
608
|
+
`;
|
|
609
|
+
/** The line telling a slice's turn which criteria are its own; empty for a single slice. */
|
|
610
|
+
const sliceLine = (ids) => ids === void 0 ? "" : `- This slice builds ${ids.join(", ")} only: every rule here about the criteria
|
|
611
|
+
means these. The others are for other slices, before or after it — leave
|
|
612
|
+
them unbuilt and untested
|
|
613
|
+
`;
|
|
595
614
|
const designSystem = () => `${designPersona}
|
|
596
615
|
|
|
597
616
|
${agentConduct}`;
|
|
@@ -635,9 +654,11 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
|
635
654
|
- Introduce no new flag read and add no second read of an existing one:
|
|
636
655
|
every match of \`${pattern}\` may move, never increase in number`;
|
|
637
656
|
const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
|
|
638
|
-
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false }) => `${stateFileRules}
|
|
657
|
+
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false }) => `${stateFileRules}
|
|
639
658
|
- The package to implement is \`${pkg}\`; never delete it
|
|
640
|
-
${
|
|
659
|
+
${note ? `- The package's \`## Refactor note\` section names the seams to open and the
|
|
660
|
+
shape each takes, written from the whole naive pass: build exactly those
|
|
661
|
+
` : ""}${feedback ? `- The package carries a \`## Review feedback\` section: a human's review of the
|
|
641
662
|
last refactor. Act on it, within the rules below
|
|
642
663
|
` : ""}${refactorRules(strays, pattern)}
|
|
643
664
|
${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
|
|
@@ -1236,9 +1257,9 @@ re-implement. It changed these files outside the expected set:
|
|
|
1236
1257
|
${listed(retry.strays)}
|
|
1237
1258
|
Start over and choose a different approach; do not rebuild the same diff.
|
|
1238
1259
|
`;
|
|
1239
|
-
const naivePassPrompt = (retry) => `${stateFileRules}
|
|
1260
|
+
const naivePassPrompt = (retry, slice) => `${stateFileRules}
|
|
1240
1261
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1241
|
-
${criteriaBinding(false)}
|
|
1262
|
+
${sliceLine(slice)}${criteriaBinding(false)}
|
|
1242
1263
|
- Implement them the quickest way that works — the naive version. Do not
|
|
1243
1264
|
restructure, abstract or refactor around the change; it may land exactly as
|
|
1244
1265
|
you leave it, and a reviewer makes it fit
|
|
@@ -1256,22 +1277,54 @@ ${criteriaBinding(false)}
|
|
|
1256
1277
|
- Leave \`.gtd/\` untouched
|
|
1257
1278
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
1258
1279
|
${retry === void 0 ? "" : naiveRetry(retry)}`;
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1280
|
+
const carries = (rebuild) => (rebuild.acceptance?.ids.length ?? 0) > 0;
|
|
1281
|
+
const rebuildLines = ({ note, acceptance }) => `${note === void 0 ? "" : `- The refactor note (\`.gtd/REFACTOR-NOTE.md\`) named the seams the refactor
|
|
1282
|
+
opened; plug the change into them:
|
|
1283
|
+
|
|
1284
|
+
${note.trimEnd().split("\n").map((line) => line === "" ? "" : ` > ${line}`).join("\n")}
|
|
1285
|
+
|
|
1286
|
+
`}${acceptance === void 0 || !carries({ acceptance }) ? "" : `- Carry over the naive pass's acceptance tests for ${acceptance.ids.join(", ")}, the
|
|
1287
|
+
one part of it you take as it is: \`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`.
|
|
1288
|
+
Copy each test with its \`[AC-<n>]\` name into the tree as the refactor left
|
|
1289
|
+
it — check a file out of the tip whole only where the refactor did not touch
|
|
1290
|
+
it — changing only what reaches the release flag. A turn whose changed tests
|
|
1291
|
+
leave an ID out is refused, and the suite must be green with them
|
|
1292
|
+
`}`;
|
|
1293
|
+
const refactorNotePrompt = (reference, strays) => `${stateFileRules}
|
|
1294
|
+
- The seam judge ruled the naive pass refactor-first: it is never applied.
|
|
1295
|
+
A behaviour-neutral refactor lands first, then the change is rebuilt on
|
|
1296
|
+
the refactored base. Both turns read the note you write
|
|
1297
|
+
- Read the whole naive pass: \`git show ${reference}\`. It changed these
|
|
1298
|
+
existing files outside the allowlist:
|
|
1299
|
+
${listed(strays)}
|
|
1300
|
+
- Write \`.gtd/REFACTOR-NOTE.md\`: which seams the refactor opens (the existing
|
|
1301
|
+
code to restructure, by path and symbol) and the shape each takes — the
|
|
1302
|
+
signatures and where the new behaviour will plug in. Name only the files
|
|
1303
|
+
listed above: the refactor may change no other path. Never the feature's own
|
|
1304
|
+
behaviour, and no flag read: the refactor adds neither
|
|
1305
|
+
- Change nothing else — a turn that does is refused
|
|
1306
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
1307
|
+
`;
|
|
1308
|
+
const afterRefactorLine = (rebuild) => `- A reviewed refactor has just landed and may already carry part of the change,
|
|
1263
1309
|
though never a release flag. Build only what is still missing, held to every
|
|
1264
|
-
rule below; if nothing is, change nothing and finish your turn
|
|
1265
|
-
|
|
1310
|
+
rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
|
|
1311
|
+
`;
|
|
1312
|
+
const flagLine = (flagMode, tier, amend, sliced) => !flagMode ? "- Add no release flag — this project ships without release flags" : tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : sliced ? `- Put the new behaviour behind the release flag an earlier slice of this
|
|
1313
|
+
change added; only when none did, behind a new one, following the
|
|
1314
|
+
repository's flag conventions` : `- Put the new behaviour behind a new release flag, following the
|
|
1315
|
+
repository's flag conventions`;
|
|
1316
|
+
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}, slice) => `${stateFileRules}
|
|
1317
|
+
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1318
|
+
${sliceLine(slice)}${criteriaBinding(true)}
|
|
1319
|
+
${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
1266
1320
|
the re-derived requirements instead of rewriting it
|
|
1267
1321
|
` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
|
|
1268
1322
|
` : `- Implement them the quickest way that works — the naive version. Do not
|
|
1269
1323
|
restructure, abstract or refactor around the change
|
|
1270
1324
|
`}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|
|
1271
1325
|
Read it for ideas; never apply, cherry-pick or check it out — write
|
|
1272
|
-
the change fresh
|
|
1273
|
-
`}${
|
|
1274
|
-
repository's flag conventions`}
|
|
1326
|
+
the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
|
|
1327
|
+
`}${rebuildLines(rebuild)}${flagLine(flagMode, tier, amend, slice !== void 0)}
|
|
1275
1328
|
- Write the tests first, then the code that makes them pass. Name each
|
|
1276
1329
|
criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
|
|
1277
1330
|
green a judge checks every ID has a passing test, and sends uncovered ones back
|
|
@@ -1352,9 +1405,9 @@ const coverageMessage = () => `Judging acceptance-criteria coverage. Run \`gtd j
|
|
|
1352
1405
|
\`yes\`/\`no\` verdict per criterion ID — or land without a verdict, which
|
|
1353
1406
|
counts every criterion as uncovered.
|
|
1354
1407
|
`;
|
|
1355
|
-
const coveringPrompt = (uncovered) => `${stateFileRules}
|
|
1408
|
+
const coveringPrompt = (uncovered, slice) => `${stateFileRules}
|
|
1356
1409
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1357
|
-
${criteriaBinding(true)}
|
|
1410
|
+
${sliceLine(slice)}${criteriaBinding(true)}
|
|
1358
1411
|
- A judge found no passing test in this change that covers ${uncovered.join(", ")}.
|
|
1359
1412
|
Write a behaviour-level test for each, naming its ID in the test or scenario
|
|
1360
1413
|
name (\`[${uncovered[0]}] …\`), and fix the code if the test shows it falls short
|
|
@@ -1379,7 +1432,9 @@ const ESCALATION = ".gtd/ESCALATION.md";
|
|
|
1379
1432
|
const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
|
|
1380
1433
|
const REVIEW = ".gtd/REVIEW.md";
|
|
1381
1434
|
const REFACTOR = ".gtd/REFACTOR.md";
|
|
1435
|
+
const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
|
|
1382
1436
|
const QUALITY = ".gtd/QUALITY.md";
|
|
1437
|
+
const SLICES = ".gtd/SLICES.md";
|
|
1383
1438
|
const planner = () => env.plannerModel ?? "";
|
|
1384
1439
|
const coder = () => env.coderModel ?? "";
|
|
1385
1440
|
const fixCheck = (rules) => agentWithSkills("fix", buildFixPrompt(rules), {
|
|
@@ -1573,12 +1628,17 @@ const ShippedOptions = Struct({
|
|
|
1573
1628
|
reviewSeverityMinP: setting,
|
|
1574
1629
|
refactorFlagPattern: setting,
|
|
1575
1630
|
seamAllow: setting,
|
|
1631
|
+
sliceMaxCriteria: setting,
|
|
1576
1632
|
qualityReviews: setting,
|
|
1577
1633
|
refactorQualityReviews: setting,
|
|
1578
1634
|
testCommand: setting,
|
|
1579
1635
|
plannerModel: setting,
|
|
1580
1636
|
coderModel: setting,
|
|
1581
|
-
flagMode: optionalWith(Boolean$, { exact: true })
|
|
1637
|
+
flagMode: optionalWith(Boolean$, { exact: true }),
|
|
1638
|
+
contextLimit: optionalWith(Struct({
|
|
1639
|
+
soft: optionalWith(Number$, { exact: true }),
|
|
1640
|
+
hard: optionalWith(Number$, { exact: true })
|
|
1641
|
+
}), { exact: true })
|
|
1582
1642
|
});
|
|
1583
1643
|
const over = (base, options) => ({
|
|
1584
1644
|
...base,
|
|
@@ -1590,12 +1650,13 @@ const flagModeOn = () => {
|
|
|
1590
1650
|
if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
|
|
1591
1651
|
return value === "true";
|
|
1592
1652
|
};
|
|
1653
|
+
const limited = (flow, context) => context === void 0 ? flow : () => scope({ context }, flow);
|
|
1593
1654
|
/** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
|
|
1594
1655
|
const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
|
|
1595
1656
|
onExcessProperty: "error",
|
|
1596
1657
|
errors: "all"
|
|
1597
|
-
}), (options) => ({
|
|
1598
|
-
flow,
|
|
1658
|
+
}), ({ contextLimit, ...options }) => ({
|
|
1659
|
+
flow: limited(flow, contextLimit),
|
|
1599
1660
|
defaults: over(defaults, options),
|
|
1600
1661
|
envDefaults: over(envDefaults, options),
|
|
1601
1662
|
doors,
|
|
@@ -1608,14 +1669,14 @@ const FlagOptions = Struct({
|
|
|
1608
1669
|
const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions, {
|
|
1609
1670
|
onExcessProperty: "error",
|
|
1610
1671
|
errors: "all"
|
|
1611
|
-
}), (options) => {
|
|
1672
|
+
}), ({ contextLimit, ...options }) => {
|
|
1612
1673
|
const flagDefaults = {
|
|
1613
1674
|
...defaults,
|
|
1614
1675
|
flag: "",
|
|
1615
1676
|
flagRegistry: defaultFlagRegistry
|
|
1616
1677
|
};
|
|
1617
1678
|
return {
|
|
1618
|
-
flow,
|
|
1679
|
+
flow: limited(flow, contextLimit),
|
|
1619
1680
|
defaults: over(flagDefaults, options),
|
|
1620
1681
|
envDefaults: over(envDefaults, options),
|
|
1621
1682
|
doors: { [name]: {
|
|
@@ -1673,4 +1734,4 @@ const steering = {
|
|
|
1673
1734
|
[REVIEW]: "review"
|
|
1674
1735
|
};
|
|
1675
1736
|
//#endregion
|
|
1676
|
-
export {
|
|
1737
|
+
export { naivePassPrompt as $, buildReviewAnswerQuestionsPrompt as A, coveringPrompt as B, fixQuality as C, flagReferences as Ct, reviewQuality as D, reviewMissing as E, requireFlag as Et, builderSystem as F, designTriagePrompt as G, deferNotesScript as H, clearCriteriaMessage as I, handOffAwaitReviewMessage as J, finisherSystem as K, clearCriteriaQuestion as L, buildReviewCollectingPrompt as M, buildReviewFixNitsPrompt as N, reviewing as O, buildReviewTriageMessage as P, naiveApplyFailure as Q, coverageMessage as R, fixCheck as S, flagMentions as St, planner as T, registryEntry as Tt, designGateAnswerMessage as U, criterionLine as V, designSystem as W, implementBuildingPrompt as X, idleMessage as Y, implementFixPrompt as Z, REQUIREMENTS as _, unwindFailedMessage as _t, flagModeOn as a, refactorNotePrompt as at, awaitReview as b, envDefaults as bt, baseline as c, reviewEditsCapture as ct, guarded as d, reviewerSystem as dt, normaliseCriteriaScript as et, healthy as f, seamByHandMessage as ft, REFACTOR_NOTE as g, uncoveredMessage as gt, REFACTOR as h, startGateBlockedMessage as ht, skills as i, refactorFixPrompt as it, buildReviewAwaitAfterDeferMessage as j, agentWithSkills as k, escalation as l, reviewGateBlockedMessage as lt, QUALITY as m, slicingPrompt as mt, summary as n, refactorBuildingPrompt as nt, flagShipped as o, refactorRules as ot, FEEDBACK as p, seamSteerMessage as pt, fixReviewNotesPrompt as q, access as r, refactorDroppedRefusal as rt, shipped as s, reviewEditNotesCapture as st, steering as t, qualityUnresolvedMessage as tt, gate as u, reviewNotesCapture as ut, REVIEW as v, unwindFailure as vt, fixRisks as w, flipProblems as wt, coder as x, testFileGlobs as xt, SLICES as y, defaults as yt, coverageQuestion as z };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
|
|
2
2
|
import { fixFindings, lensEntriesOf, lensOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { C as fixQuality, D as reviewQuality, E as reviewMissing, J as handOffAwaitReviewMessage, O as reviewing, S as fixCheck, b as awaitReview, d as guarded, f as healthy, l as escalation, m as QUALITY, v as REVIEW, w as fixRisks } from "./reserved-sfMdy7v7.mjs";
|
|
4
4
|
//#region src/workflows/shared/tail.ts
|
|
5
5
|
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
6
6
|
const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|