@pmelab/gtd 35.0.0 → 37.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 +45 -29
- 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-C8Fm3s72.mjs → reserved-zAfoJtR7.mjs} +62 -24
- package/dist/chunks/{tail-oaYewBN_.mjs → tail-BRC4DGdV.mjs} +1 -1
- package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
- package/dist/flows.mjs +15 -10
- package/dist/gtd.bundle.mjs +511 -64
- package/dist/testkit.mjs +158 -26
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +138 -38
- 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 +208 -66
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +3 -3
- 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/flows/scripts.ts +31 -9
- package/src/replay/Replay.ts +51 -2
- package/src/testkit/FlowScripts.test.ts +58 -0
- package/src/testkit/FlowScripts.ts +61 -24
- package/src/testkit/Layers.ts +1 -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/acceptance-criteria.feature +5 -1
- package/src/workflows/feature/context-limit.feature +37 -0
- package/src/workflows/feature/coverage.feature +6 -3
- package/src/workflows/feature/coverage.ts +8 -2
- package/src/workflows/feature/fast-path.feature +11 -19
- package/src/workflows/feature/feature.test.ts +24 -4
- package/src/workflows/feature/feature.ts +14 -10
- package/src/workflows/feature/flag-mode.feature +17 -9
- package/src/workflows/feature/lens-models.feature +7 -1
- package/src/workflows/feature/naive-acceptance.feature +2 -2
- package/src/workflows/feature/one-step-land.feature +10 -12
- package/src/workflows/feature/quality-unresolved.feature +7 -1
- 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/risk-tier.feature +12 -4
- package/src/workflows/feature/seam-rejection.feature +4 -9
- package/src/workflows/feature/seam.feature +29 -26
- package/src/workflows/feature/seam.test.ts +117 -6
- package/src/workflows/feature/seam.ts +158 -39
- package/src/workflows/review/review-door.feature +12 -4
- package/src/workflows/shared/options.test.ts +11 -2
- package/src/workflows/shared/options.ts +18 -4
- package/src/workflows/shared/steps.ts +1 -0
- package/src/workflows/shared/text.test.ts +8 -0
- package/src/workflows/shared/text.ts +91 -22
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
|
|
@@ -430,18 +437,19 @@ you open the `review` door.
|
|
|
430
437
|
3. **You wait.** The agent first implements the requirements the quickest way
|
|
431
438
|
that works — the naive pass — and a judge reads where it changed code (see
|
|
432
439
|
[the naive pass](#the-naive-pass) below; with [flag mode](#the-naive-pass)
|
|
433
|
-
off,
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
440
|
+
off, no judge runs). A pass the judge lets through is the implementation; one
|
|
441
|
+
whose seam is refactored first is built again test-first. Then it is checked
|
|
442
|
+
against the full suite (`testCommand`) and fixed until it passes — so keep
|
|
443
|
+
that suite fast; gtd has no lighter one. Scenario wording is not frozen: a
|
|
444
|
+
review's `## Scenarios` chunk marks any assertion an existing scenario lost
|
|
445
|
+
as `Weakened:` — **risk: `feature` has no review, so a build turn that
|
|
446
|
+
weakens a scenario reaches `idle` unflagged unless you open the `review`
|
|
447
|
+
door.** After the build the quality lap runs, and the process ends: the
|
|
448
|
+
feature workflow has no human review of the finished change. Two points along
|
|
449
|
+
the process (one with flag mode off) are judged rather than always asking you
|
|
450
|
+
outright — each stops and hands you a verdict to make (`gtd judge answer`, or
|
|
451
|
+
land with a clean tree to accept the conservative default, which never skips
|
|
452
|
+
work; `gtd judge run --provider fixed --answers <path>` — or the
|
|
445
453
|
`GTD_JUDGE_ANSWERS` env var, inline JSON — answers one from a file, piped
|
|
446
454
|
between `gtd judge --json` and `gtd judge answer`;
|
|
447
455
|
`gtd judge run --provider jev` asks TypeSafe's Jev instead, with the key in
|
|
@@ -605,16 +613,23 @@ its ID (`[AC-3] …`) and reaches only code that existed before the pass. The pa
|
|
|
605
613
|
is sent back while a criterion's ID is missing from its tests, a test imports a
|
|
606
614
|
file it added, or the suite is red on it. A judge checks whether it changed
|
|
607
615
|
anything outside the expected set (tests, steering files; see `seamAllow`).
|
|
608
|
-
Incidental changes land
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
616
|
+
Incidental changes land: the pass itself is the implementation, with no rebuild,
|
|
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
|
|
629
|
+
implementation is green, a judge checks every criterion ID has a covering test:
|
|
630
|
+
an uncovered one goes back to the agent (twice, then to you) instead of ending
|
|
631
|
+
the feature, and each covered ID is recorded as a `Gtd-Mark: covers=<id>`
|
|
632
|
+
trailer. See
|
|
618
633
|
[Acceptance-criteria coverage](docs/configuration.md#acceptance-criteria-coverage).
|
|
619
634
|
Feedback at the refactor review re-runs the refactor, never design; a
|
|
620
635
|
refactor-review change the refactor cannot build is refused, and the only way
|
|
@@ -623,12 +638,13 @@ out is `gtd abandon`. A process from 27.5.0 or later whose judge ruled
|
|
|
623
638
|
that slice. Details in
|
|
624
639
|
[the naive-pass path](docs/configuration.md#the-naive-pass-path).
|
|
625
640
|
|
|
626
|
-
**Flag mode (`flagMode`, on by default).** On, the
|
|
627
|
-
|
|
628
|
-
|
|
641
|
+
**Flag mode (`flagMode`, on by default).** On, the naive pass's judge runs as
|
|
642
|
+
above and a rebuilt implementation is asked for a new release flag (none at a
|
|
643
|
+
`low` risk tier); a pass the judge lets land is not asked for one. A project
|
|
644
|
+
that is not publicly released turns it off explicitly —
|
|
629
645
|
`feature({ flagMode: false })` in `gtd.config.ts`, it is never detected: then no
|
|
630
|
-
flag is asked for, no seam judge or refactor runs, and the
|
|
631
|
-
|
|
646
|
+
flag is asked for, no seam judge or refactor runs, and the naive pass lands as
|
|
647
|
+
the implementation, straight through the health loop and the quality lap. See
|
|
632
648
|
[`flagMode`](docs/configuration.md#the-bundled-workflows-settings).
|
|
633
649
|
|
|
634
650
|
**Risk tier (flag `GTD_FLAG_RISK_TIER=1`, off by default).** Triage proposes a
|
|
@@ -751,8 +767,8 @@ install for any of these but `./testkit`: gtd resolves a `gtd.config.ts`'s
|
|
|
751
767
|
The `gtd` bin's commands — `next`, `land`, `abandon`, `restore`, `validate`,
|
|
752
768
|
`check`, `uncheck`, `install`, `summary`, `base`, `door`, `doors`, `slices`,
|
|
753
769
|
`version`, `help`, `history list`, `history show`, `history fetch`, `strays`,
|
|
754
|
-
and the tools `ui`, `lsp`, `loop`, `judge` (`judge answer`,
|
|
755
|
-
listed in the
|
|
770
|
+
and the tools `ui`, `lsp`, `loop`, `context-hook`, `judge` (`judge answer`,
|
|
771
|
+
`judge run`) — are listed in the
|
|
756
772
|
[CLI reference](https://github.com/pmelab/gtd/blob/main/docs/cli.md).
|
|
757
773
|
`gtd slices` lists the spans a flow's `slice()` calls mark. It refuses (exit 1)
|
|
758
774
|
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) => {
|
|
@@ -391,6 +391,7 @@ const agentWithSkills = (name, prompt, options = {}) => {
|
|
|
391
391
|
return agent(name, withSkills(skillsFor(name).join(", "), body), options);
|
|
392
392
|
};
|
|
393
393
|
const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
|
|
394
|
+
const naiveApplyFailure = (commit) => `gtd could not apply the naive pass ${commit} to the working tree; it is built fresh instead.`;
|
|
394
395
|
const idleMessage = () => `No active gtd process.
|
|
395
396
|
|
|
396
397
|
To start one, make ANY change — a hand-edit to real code, a scratch
|
|
@@ -634,9 +635,11 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
|
634
635
|
- Introduce no new flag read and add no second read of an existing one:
|
|
635
636
|
every match of \`${pattern}\` may move, never increase in number`;
|
|
636
637
|
const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
|
|
637
|
-
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false }) => `${stateFileRules}
|
|
638
|
+
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false }) => `${stateFileRules}
|
|
638
639
|
- The package to implement is \`${pkg}\`; never delete it
|
|
639
|
-
${
|
|
640
|
+
${note ? `- The package's \`## Refactor note\` section names the seams to open and the
|
|
641
|
+
shape each takes, written from the whole naive pass: build exactly those
|
|
642
|
+
` : ""}${feedback ? `- The package carries a \`## Review feedback\` section: a human's review of the
|
|
640
643
|
last refactor. Act on it, within the rules below
|
|
641
644
|
` : ""}${refactorRules(strays, pattern)}
|
|
642
645
|
${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
|
|
@@ -1239,8 +1242,8 @@ const naivePassPrompt = (retry) => `${stateFileRules}
|
|
|
1239
1242
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1240
1243
|
${criteriaBinding(false)}
|
|
1241
1244
|
- Implement them the quickest way that works — the naive version. Do not
|
|
1242
|
-
restructure, abstract or refactor around the change;
|
|
1243
|
-
|
|
1245
|
+
restructure, abstract or refactor around the change; it may land exactly as
|
|
1246
|
+
you leave it, and a reviewer makes it fit
|
|
1244
1247
|
- Work breadth-first: rough out every part of the change before finishing any
|
|
1245
1248
|
one, so a run cut off midway still shows the whole shape
|
|
1246
1249
|
- Write behaviour-level acceptance tests, at least one per criterion, each
|
|
@@ -1255,21 +1258,50 @@ ${criteriaBinding(false)}
|
|
|
1255
1258
|
- Leave \`.gtd/\` untouched
|
|
1256
1259
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
1257
1260
|
${retry === void 0 ? "" : naiveRetry(retry)}`;
|
|
1258
|
-
const
|
|
1261
|
+
const carries = (rebuild) => (rebuild.acceptance?.ids.length ?? 0) > 0;
|
|
1262
|
+
const rebuildLines = ({ note, acceptance }) => `${note === void 0 ? "" : `- The refactor note (\`.gtd/REFACTOR-NOTE.md\`) named the seams the refactor
|
|
1263
|
+
opened; plug the change into them:
|
|
1264
|
+
|
|
1265
|
+
${note.trimEnd().split("\n").map((line) => line === "" ? "" : ` > ${line}`).join("\n")}
|
|
1266
|
+
|
|
1267
|
+
`}${acceptance === void 0 || !carries({ acceptance }) ? "" : `- Carry over the naive pass's acceptance tests for ${acceptance.ids.join(", ")}, the
|
|
1268
|
+
one part of it you take as it is: \`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`.
|
|
1269
|
+
Copy each test with its \`[AC-<n>]\` name into the tree as the refactor left
|
|
1270
|
+
it — check a file out of the tip whole only where the refactor did not touch
|
|
1271
|
+
it — changing only what reaches the release flag. A turn whose changed tests
|
|
1272
|
+
leave an ID out is refused, and the suite must be green with them
|
|
1273
|
+
`}`;
|
|
1274
|
+
const refactorNotePrompt = (reference, strays) => `${stateFileRules}
|
|
1275
|
+
- The seam judge ruled the naive pass refactor-first: it is never applied.
|
|
1276
|
+
A behaviour-neutral refactor lands first, then the change is rebuilt on
|
|
1277
|
+
the refactored base. Both turns read the note you write
|
|
1278
|
+
- Read the whole naive pass: \`git show ${reference}\`. It changed these
|
|
1279
|
+
existing files outside the allowlist:
|
|
1280
|
+
${listed(strays)}
|
|
1281
|
+
- Write \`.gtd/REFACTOR-NOTE.md\`: which seams the refactor opens (the existing
|
|
1282
|
+
code to restructure, by path and symbol) and the shape each takes — the
|
|
1283
|
+
signatures and where the new behaviour will plug in. Name only the files
|
|
1284
|
+
listed above: the refactor may change no other path. Never the feature's own
|
|
1285
|
+
behaviour, and no flag read: the refactor adds neither
|
|
1286
|
+
- Change nothing else — a turn that does is refused
|
|
1287
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
1288
|
+
`;
|
|
1289
|
+
const afterRefactorLine = (rebuild) => `- A reviewed refactor has just landed and may already carry part of the change,
|
|
1290
|
+
though never a release flag. Build only what is still missing, held to every
|
|
1291
|
+
rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
|
|
1292
|
+
`;
|
|
1293
|
+
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}) => `${stateFileRules}
|
|
1259
1294
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1260
1295
|
${criteriaBinding(true)}
|
|
1261
|
-
${afterRefactor === true ?
|
|
1262
|
-
though never a release flag. Build only what is still missing, held to every
|
|
1263
|
-
rule below; if nothing is, change nothing and finish your turn
|
|
1264
|
-
` : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
1296
|
+
${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
1265
1297
|
the re-derived requirements instead of rewriting it
|
|
1266
1298
|
` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
|
|
1267
1299
|
` : `- Implement them the quickest way that works — the naive version. Do not
|
|
1268
1300
|
restructure, abstract or refactor around the change
|
|
1269
1301
|
`}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|
|
1270
1302
|
Read it for ideas; never apply, cherry-pick or check it out — write
|
|
1271
|
-
the change fresh
|
|
1272
|
-
`}${!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" : `- Put the new behaviour behind a new release flag, following the
|
|
1303
|
+
the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
|
|
1304
|
+
`}${rebuildLines(rebuild)}${!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" : `- Put the new behaviour behind a new release flag, following the
|
|
1273
1305
|
repository's flag conventions`}
|
|
1274
1306
|
- Write the tests first, then the code that makes them pass. Name each
|
|
1275
1307
|
criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
|
|
@@ -1290,12 +1322,13 @@ An edit together with an approval counts as steer. Only
|
|
|
1290
1322
|
\`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
|
|
1291
1323
|
branch unjudged.
|
|
1292
1324
|
`;
|
|
1293
|
-
|
|
1325
|
+
/** `judged`: whether an overruled pass still meets the seam judge (`flagMode` on). */
|
|
1326
|
+
const seamSteerMessage = (retry, judged = true) => isFault(retry) ? `The naive pass failed three times and the retries are spent. The last
|
|
1294
1327
|
attempt's tests (\`git show ${retry.hash}\`) fall short:
|
|
1295
1328
|
${retry.fault}
|
|
1296
1329
|
|
|
1297
1330
|
${steerChoices(`land a clean tree to go on with the last attempt as it is:
|
|
1298
|
-
\`seam.check\` runs its suite and the seam judge rules on its shape. Use it
|
|
1331
|
+
\`seam.check\` runs its suite and ${judged ? "the seam judge rules on its shape" : "it lands as the implementation"}. Use it
|
|
1299
1332
|
when the rule cannot fit this change (tests the globs do not see, or no
|
|
1300
1333
|
entry point that existed before). Risk: a criterion may have no test that
|
|
1301
1334
|
survives a later refactor — the choice is recorded in history.`)}` : isRed(retry) ? `The naive pass failed three times and the retries are spent. The last
|
|
@@ -1306,7 +1339,7 @@ Two ways forward (then run \`gtd land\`):
|
|
|
1306
1339
|
- **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
|
|
1307
1340
|
pass runs again with a fresh retry budget
|
|
1308
1341
|
- **Overrule** — land a clean tree to go on with the last attempt as it is:
|
|
1309
|
-
the seam judge rules on its shape, and the implementation's health loop
|
|
1342
|
+
${judged ? "the seam judge rules on its shape" : "it lands as the implementation"}, and the implementation's health loop
|
|
1310
1343
|
must turn the suite green. Risk: the criteria's tests never passed on the
|
|
1311
1344
|
naive pass — the choice is recorded in history.
|
|
1312
1345
|
|
|
@@ -1322,9 +1355,8 @@ Two ways forward (then run \`gtd land\`):
|
|
|
1322
1355
|
- **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
|
|
1323
1356
|
pass runs again with a fresh retry budget (the risk tier stays as the
|
|
1324
1357
|
design gate settled it; a tier edit here is not applied)
|
|
1325
|
-
- **Overrule** — land a clean tree to accept the last attempt
|
|
1326
|
-
the implementation
|
|
1327
|
-
and no refactor. Risk: this ships the seam the judge objected to,
|
|
1358
|
+
- **Overrule** — land a clean tree to accept the last attempt as it is: it
|
|
1359
|
+
lands as the implementation, with no seam gate and no refactor. Risk: this ships the seam the judge objected to,
|
|
1328
1360
|
unaddressed — the choice is recorded in history.
|
|
1329
1361
|
|
|
1330
1362
|
An edit together with an approval counts as steer. Only
|
|
@@ -1378,6 +1410,7 @@ const ESCALATION = ".gtd/ESCALATION.md";
|
|
|
1378
1410
|
const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
|
|
1379
1411
|
const REVIEW = ".gtd/REVIEW.md";
|
|
1380
1412
|
const REFACTOR = ".gtd/REFACTOR.md";
|
|
1413
|
+
const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
|
|
1381
1414
|
const QUALITY = ".gtd/QUALITY.md";
|
|
1382
1415
|
const planner = () => env.plannerModel ?? "";
|
|
1383
1416
|
const coder = () => env.coderModel ?? "";
|
|
@@ -1577,7 +1610,11 @@ const ShippedOptions = Struct({
|
|
|
1577
1610
|
testCommand: setting,
|
|
1578
1611
|
plannerModel: setting,
|
|
1579
1612
|
coderModel: setting,
|
|
1580
|
-
flagMode: optionalWith(Boolean$, { exact: true })
|
|
1613
|
+
flagMode: optionalWith(Boolean$, { exact: true }),
|
|
1614
|
+
contextLimit: optionalWith(Struct({
|
|
1615
|
+
soft: optionalWith(Number$, { exact: true }),
|
|
1616
|
+
hard: optionalWith(Number$, { exact: true })
|
|
1617
|
+
}), { exact: true })
|
|
1581
1618
|
});
|
|
1582
1619
|
const over = (base, options) => ({
|
|
1583
1620
|
...base,
|
|
@@ -1589,12 +1626,13 @@ const flagModeOn = () => {
|
|
|
1589
1626
|
if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
|
|
1590
1627
|
return value === "true";
|
|
1591
1628
|
};
|
|
1629
|
+
const limited = (flow, context) => context === void 0 ? flow : () => scope({ context }, flow);
|
|
1592
1630
|
/** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
|
|
1593
1631
|
const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
|
|
1594
1632
|
onExcessProperty: "error",
|
|
1595
1633
|
errors: "all"
|
|
1596
|
-
}), (options) => ({
|
|
1597
|
-
flow,
|
|
1634
|
+
}), ({ contextLimit, ...options }) => ({
|
|
1635
|
+
flow: limited(flow, contextLimit),
|
|
1598
1636
|
defaults: over(defaults, options),
|
|
1599
1637
|
envDefaults: over(envDefaults, options),
|
|
1600
1638
|
doors,
|
|
@@ -1607,14 +1645,14 @@ const FlagOptions = Struct({
|
|
|
1607
1645
|
const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions, {
|
|
1608
1646
|
onExcessProperty: "error",
|
|
1609
1647
|
errors: "all"
|
|
1610
|
-
}), (options) => {
|
|
1648
|
+
}), ({ contextLimit, ...options }) => {
|
|
1611
1649
|
const flagDefaults = {
|
|
1612
1650
|
...defaults,
|
|
1613
1651
|
flag: "",
|
|
1614
1652
|
flagRegistry: defaultFlagRegistry
|
|
1615
1653
|
};
|
|
1616
1654
|
return {
|
|
1617
|
-
flow,
|
|
1655
|
+
flow: limited(flow, contextLimit),
|
|
1618
1656
|
defaults: over(flagDefaults, options),
|
|
1619
1657
|
envDefaults: over(envDefaults, options),
|
|
1620
1658
|
doors: { [name]: {
|
|
@@ -1672,4 +1710,4 @@ const steering = {
|
|
|
1672
1710
|
[REVIEW]: "review"
|
|
1673
1711
|
};
|
|
1674
1712
|
//#endregion
|
|
1675
|
-
export {
|
|
1713
|
+
export { normaliseCriteriaScript as $, buildReviewAwaitAfterDeferMessage as A, criterionLine as B, fixRisks as C, registryEntry as Ct, reviewing as D, reviewQuality as E, clearCriteriaMessage as F, finisherSystem as G, designGateAnswerMessage as H, clearCriteriaQuestion as I, idleMessage as J, fixReviewNotesPrompt as K, coverageMessage as L, buildReviewFixNitsPrompt as M, buildReviewTriageMessage as N, agentWithSkills as O, builderSystem as P, naivePassPrompt as Q, coverageQuestion as R, fixQuality as S, flipProblems as St, reviewMissing as T, designSystem as U, deferNotesScript as V, designTriagePrompt as W, implementFixPrompt as X, implementBuildingPrompt as Y, naiveApplyFailure as Z, REQUIREMENTS as _, defaults as _t, flagModeOn as a, refactorRules as at, coder as b, flagMentions as bt, baseline as c, reviewGateBlockedMessage as ct, guarded as d, seamByHandMessage as dt, qualityUnresolvedMessage as et, healthy as f, seamSteerMessage as ft, REFACTOR_NOTE as g, unwindFailure as gt, REFACTOR as h, unwindFailedMessage as ht, skills as i, refactorNotePrompt as it, buildReviewCollectingPrompt as j, buildReviewAnswerQuestionsPrompt as k, escalation as l, reviewNotesCapture as lt, QUALITY as m, uncoveredMessage as mt, summary as n, refactorDroppedRefusal as nt, flagShipped as o, reviewEditNotesCapture as ot, FEEDBACK as p, startGateBlockedMessage as pt, handOffAwaitReviewMessage as q, access as r, refactorFixPrompt as rt, shipped as s, reviewEditsCapture as st, steering as t, refactorBuildingPrompt as tt, gate as u, reviewerSystem as ut, REVIEW as v, envDefaults as vt, planner as w, requireFlag as wt, fixCheck as x, flagReferences as xt, awaitReview as y, testFileGlobs as yt, coveringPrompt 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 fixRisks, D as reviewing, E as reviewQuality, S as fixQuality, T as reviewMissing, d as guarded, f as healthy, l as escalation, m as QUALITY, q as handOffAwaitReviewMessage, v as REVIEW, x as fixCheck, y as awaitReview } from "./reserved-zAfoJtR7.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";
|