@pmelab/gtd 65.0.0 → 66.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 +3 -2
- package/claude/hooks/drive.ts +10 -1
- package/claude/hooks/git.ts +2 -2
- package/claude/hooks/pr.ts +80 -20
- package/claude/hooks/register.tsx +19 -4
- package/dist/chunks/{flows-DGs0W3HI.mjs → flows-CmQplvh1.mjs} +134 -40
- package/dist/chunks/{steering-DEh_MS40.mjs → steering-CMrePUiY.mjs} +115 -17
- package/dist/chunks/{testkit-CgrLmpdJ.mjs → testkit-BJhAAVGt.mjs} +179 -34
- package/dist/chunks/{wire-BNS8ciNu.mjs → wire-xTbzFc26.mjs} +7 -3
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +430 -100
- package/dist/gtd.core.mjs +147 -134
- package/dist/steering.mjs +1 -1
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +2 -2
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +185 -18
- package/dist/wire.mjs +1 -1
- package/dist/workflow-kit.mjs +9 -3
- package/package.json +1 -1
- package/src/cli/Cli.ts +1 -1
- package/src/cli/Program.ts +18 -2
- package/src/edge/Edge.ts +93 -11
- package/src/flows/helpers.ts +36 -18
- package/src/flows/index.test.ts +3 -0
- package/src/flows/index.ts +5 -0
- package/src/flows/runtime.ts +123 -16
- package/src/install/Install.ts +9 -2
- package/src/platform/Git.test.ts +7 -11
- package/src/platform/NodeSync.ts +2 -0
- package/src/platform/SyncIo.ts +2 -0
- package/src/platform/Workspace.test.ts +37 -0
- package/src/platform/Workspace.ts +36 -0
- package/src/replay/Replay.ts +33 -1
- package/src/replay/Trailers.ts +3 -0
- package/src/replay/Tree.ts +5 -0
- package/src/steering/FORMATS.md +3 -1
- package/src/steering/SteeringFormat.ts +13 -0
- package/src/steering/fixtures/review/sectioned.md +1 -1
- package/src/steering/fixtures/review/sectioned.read.json +0 -1
- package/src/steering/index.ts +1 -0
- package/src/steering/review.test.ts +181 -5
- package/src/steering/review.ts +160 -18
- package/src/step/RepoSnapshot.ts +4 -0
- package/src/step/planStep.ts +6 -6
- package/src/twins/EmittedScriptRecognizer.ts +1 -1
- package/src/twins/InMemRepo.ts +24 -3
- package/src/twins/Layers.ts +1 -0
- package/src/twins/SyncIo.test.ts +19 -4
- package/src/twins/SyncIo.ts +13 -1
- package/src/wire/BeatDocument.test.ts +15 -0
- package/src/wire/BeatDocument.ts +8 -2
- package/src/wire/Demand.ts +4 -2
- package/src/wire/golden/1/beat-capture.json +1 -0
- package/src/wire/golden/1/beat-message.json +1 -0
- package/src/wire/golden/1/beat-prompt.json +1 -0
- package/src/wire/golden/1/beat-script.json +1 -0
- package/src/wire/golden/1/beat-stalled.json +1 -0
- package/src/workflow/Workflow.ts +2 -0
- package/src/workflow-kit/index.test.ts +1 -0
- package/src/workflow-kit/index.ts +1 -0
- package/src/workflow-kit/scenarios.ts +8 -0
package/README.md
CHANGED
|
@@ -501,8 +501,9 @@ and gtd moves `<b>` only when the workflow calls `ship()`, which squashes the
|
|
|
501
501
|
process onto `<b>` as one commit (see
|
|
502
502
|
[Shipping](docs/configuration.md#shipping)); what it committed after its last
|
|
503
503
|
ship stays on `gtd/<b>`. `gtd abandon` switches back to `<b>` with the unshipped
|
|
504
|
-
work uncommitted and deletes `gtd/<b>`; `gtd restore` recreates it
|
|
505
|
-
commit
|
|
504
|
+
work uncommitted and deletes `gtd/<b>`; `gtd restore` recreates it, also when
|
|
505
|
+
`<b>` still sits on the abandoned ship commit. The closing commit records
|
|
506
|
+
`Gtd-Branch: <b>`.
|
|
506
507
|
|
|
507
508
|
When a process finishes, the landing that closes it also keeps the whole run
|
|
508
509
|
behind a ref, `refs/gtd/history/<entry>`, so a squash or a deleted branch
|
package/claude/hooks/drive.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type Beat = {
|
|
|
19
19
|
system?: string
|
|
20
20
|
skills?: string[]
|
|
21
21
|
validate?: string
|
|
22
|
+
fresh?: string | null
|
|
22
23
|
access?: AccessDef
|
|
23
24
|
judge?: unknown
|
|
24
25
|
changes?: { status: string; path: string }[]
|
|
@@ -36,6 +37,8 @@ export type Turn = {
|
|
|
36
37
|
memory: string
|
|
37
38
|
resume: boolean
|
|
38
39
|
prompt: string
|
|
40
|
+
/** A validator retry: what a replacement subagent starts from — the original prompt plus the correction. */
|
|
41
|
+
fresh?: string
|
|
39
42
|
label?: string
|
|
40
43
|
model?: string
|
|
41
44
|
system?: string
|
|
@@ -163,7 +166,12 @@ async function followUp(io: Io, b: Beat, text: string): Promise<TurnEnd> {
|
|
|
163
166
|
const turn = turnOf(b)
|
|
164
167
|
const r = await io.resume(turn.memory, text)
|
|
165
168
|
if (r.ok || !r.isLost) return r
|
|
166
|
-
|
|
169
|
+
const { fresh, ...rest } = turn
|
|
170
|
+
return io.turn({
|
|
171
|
+
...rest,
|
|
172
|
+
resume: false,
|
|
173
|
+
prompt: `${fresh ?? turn.prompt}\n\n${LOST}\n\n${text}`,
|
|
174
|
+
})
|
|
167
175
|
}
|
|
168
176
|
|
|
169
177
|
const LOST =
|
|
@@ -177,6 +185,7 @@ function turnOf(b: Beat): Turn {
|
|
|
177
185
|
memory,
|
|
178
186
|
resume: isTrue(b.session?.resume),
|
|
179
187
|
prompt: b.content ?? "",
|
|
188
|
+
...(b.fresh ? { fresh: b.fresh } : {}),
|
|
180
189
|
label: b.label,
|
|
181
190
|
model: b.model || undefined,
|
|
182
191
|
system: b.system || undefined,
|
package/claude/hooks/git.ts
CHANGED
|
@@ -29,7 +29,7 @@ export async function defaultBranch(git: (...args: string[]) => Promise<string>)
|
|
|
29
29
|
// needs --force-with-lease on an existing origin/<branch>. `fresh`: origin
|
|
30
30
|
// lacks the branch, so any tracking ref is stale. Returns why the push failed,
|
|
31
31
|
// or nothing.
|
|
32
|
-
export async function pushBranch(io: GitIo, branch: string, fresh = false) {
|
|
32
|
+
export async function pushBranch(io: GitIo, branch: string, fresh = false, head = "HEAD") {
|
|
33
33
|
const ok = async (...args: string[]) => (await io.run(["git", ...args])).code === 0
|
|
34
34
|
const push = async (args: string[], failure: string) => {
|
|
35
35
|
const r = await attempt(io, ["git", "push", ...args])
|
|
@@ -42,7 +42,7 @@ export async function pushBranch(io: GitIo, branch: string, fresh = false) {
|
|
|
42
42
|
io.log(`Pushing ${branch} to origin…`)
|
|
43
43
|
return push(["--set-upstream", "origin", branch], "git push failed.")
|
|
44
44
|
}
|
|
45
|
-
if (!(await ok("merge-base", "--is-ancestor",
|
|
45
|
+
if (!(await ok("merge-base", "--is-ancestor", head, `origin/${branch}`))) {
|
|
46
46
|
io.log(`Pushing ${branch} to origin…`)
|
|
47
47
|
return push(
|
|
48
48
|
["--force-with-lease", "origin", branch],
|
package/claude/hooks/pr.ts
CHANGED
|
@@ -29,9 +29,24 @@ const PLACEHOLDER = "_your answer_"
|
|
|
29
29
|
|
|
30
30
|
const fail = (text: string): Done => ({ ok: false, text })
|
|
31
31
|
|
|
32
|
+
const GTD_BRANCH = "gtd/"
|
|
33
|
+
|
|
34
|
+
// The pull request belongs to `<b>`; a process runs on `gtd/<b>`.
|
|
35
|
+
const userBranchOf = (branch: string) =>
|
|
36
|
+
branch.startsWith(GTD_BRANCH) ? branch.slice(GTD_BRANCH.length) : branch
|
|
37
|
+
|
|
32
38
|
export async function currentPr(io: GitIo): Promise<Pr | string> {
|
|
33
|
-
const
|
|
34
|
-
const
|
|
39
|
+
const head = (await io.run(["git", "rev-parse", "--abbrev-ref", "HEAD"])).out.trim()
|
|
40
|
+
const branch = userBranchOf(head)
|
|
41
|
+
// `gtd/<b>` has no pull request of its own.
|
|
42
|
+
const v = await io.run([
|
|
43
|
+
"gh",
|
|
44
|
+
"pr",
|
|
45
|
+
"view",
|
|
46
|
+
...(branch === head ? [] : [branch]),
|
|
47
|
+
"--json",
|
|
48
|
+
"number,url",
|
|
49
|
+
])
|
|
35
50
|
if (v.code !== 0) return `gh pr view failed: ${v.err.trim()}`
|
|
36
51
|
try {
|
|
37
52
|
const { url } = JSON.parse(v.out) as { url: string }
|
|
@@ -226,17 +241,16 @@ export function missingReasons(review: string) {
|
|
|
226
241
|
|
|
227
242
|
// The first commit since `since` whose review answers any of `ids`: the gate
|
|
228
243
|
// that decided them. Undefined while none has landed.
|
|
229
|
-
export async function decided(io: GitIo, since: string, ids: readonly string[]) {
|
|
244
|
+
export async function decided(io: GitIo, since: string, ids: readonly string[], tip = "HEAD") {
|
|
230
245
|
// A reset or rebase since the import leaves nothing to find the gate in.
|
|
231
|
-
if ((await io.run(["git", "merge-base", "--is-ancestor", since,
|
|
232
|
-
return "gone"
|
|
246
|
+
if ((await io.run(["git", "merge-base", "--is-ancestor", since, tip])).code !== 0) return "gone"
|
|
233
247
|
const log = await io.run([
|
|
234
248
|
"git",
|
|
235
249
|
"log",
|
|
236
250
|
"--reverse",
|
|
237
251
|
"--first-parent",
|
|
238
252
|
"--format=%H",
|
|
239
|
-
`${since}
|
|
253
|
+
`${since}..${tip}`,
|
|
240
254
|
])
|
|
241
255
|
if (log.code !== 0) return "gone"
|
|
242
256
|
for (const commit of log.out.split("\n").filter(Boolean)) {
|
|
@@ -255,7 +269,7 @@ export type PrRest = { isReviewGate: boolean; isDone: boolean; branch: string }
|
|
|
255
269
|
// gate drops them.
|
|
256
270
|
export function prAction(pr: PrFeedback | undefined, at: PrRest) {
|
|
257
271
|
if (!pr) return "none"
|
|
258
|
-
if (at.branch !== pr.branch) return "forget"
|
|
272
|
+
if (userBranchOf(at.branch) !== pr.branch) return "forget"
|
|
259
273
|
if (pr.importedAt === undefined) return at.isReviewGate ? "import" : at.isDone ? "forget" : "none"
|
|
260
274
|
return at.isReviewGate || at.isDone ? "reply" : "none"
|
|
261
275
|
}
|
|
@@ -265,7 +279,7 @@ export function prAction(pr: PrFeedback | undefined, at: PrRest) {
|
|
|
265
279
|
export function replyText(a: Answer, fix: string | undefined) {
|
|
266
280
|
if (a.choice === "wontfix") return a.text ? `Won't fix: ${a.text}` : "Won't fix."
|
|
267
281
|
if (a.choice === "answer") return a.text
|
|
268
|
-
return fix ? `Fixed in ${fix}.` : "Fixed as suggested
|
|
282
|
+
return fix ? `Fixed in ${fix}.` : "Fixed as suggested."
|
|
269
283
|
}
|
|
270
284
|
|
|
271
285
|
const REPLY = `mutation($thread: ID!, $body: String!) {
|
|
@@ -275,32 +289,77 @@ const RESOLVE = `mutation($thread: ID!) {
|
|
|
275
289
|
resolveReviewThread(input: { threadId: $thread }) { thread { isResolved } }
|
|
276
290
|
}`
|
|
277
291
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
// the
|
|
292
|
+
export type ProcessTip = { branch: string; tip: string; isShipped: boolean }
|
|
293
|
+
|
|
294
|
+
// Where the process's commits are: HEAD on `gtd/<b>`, `gtd/<b>` once HEAD is
|
|
295
|
+
// back on `<b>`. Without a `gtd/<b>` the process ran on `<b>` itself.
|
|
296
|
+
export async function processTip(io: GitIo): Promise<ProcessTip> {
|
|
297
|
+
const head = (await io.run(["git", "rev-parse", "--abbrev-ref", "HEAD"])).out.trim()
|
|
298
|
+
const branch = userBranchOf(head)
|
|
299
|
+
if (branch !== head) return { branch, tip: "HEAD", isShipped: true }
|
|
300
|
+
const ref = `refs/heads/${GTD_BRANCH}${branch}`
|
|
301
|
+
const has = (await io.run(["git", "rev-parse", "--verify", "--quiet", ref])).code === 0
|
|
302
|
+
return { branch, tip: has ? ref : "HEAD", isShipped: has }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// The oldest ship on `<b>` whose process tip holds `fix`.
|
|
306
|
+
async function shipOf(io: GitIo, branch: string, fix: string) {
|
|
307
|
+
const ships = await io.run([
|
|
308
|
+
"git",
|
|
309
|
+
"log",
|
|
310
|
+
"--reverse",
|
|
311
|
+
"--first-parent",
|
|
312
|
+
"--grep=^Gtd-Tip:",
|
|
313
|
+
"--format=%H",
|
|
314
|
+
`refs/heads/${branch}`,
|
|
315
|
+
])
|
|
316
|
+
for (const ship of ships.out.split("\n").filter(Boolean)) {
|
|
317
|
+
const body = await io.run(["git", "show", "-s", "--format=%B", ship])
|
|
318
|
+
const tip = /^Gtd-Tip: ([0-9a-f]+)$/m.exec(body.out)?.[1]
|
|
319
|
+
if (tip && (await io.run(["git", "merge-base", "--is-ancestor", fix, tip])).code === 0)
|
|
320
|
+
return ship
|
|
321
|
+
}
|
|
322
|
+
return undefined
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Replies to each answered thread and resolves it. A process on `gtd/<b>` ships
|
|
326
|
+
// its fixes to `<b>`: a "fixed" reply waits for the ship carrying the fix, then
|
|
327
|
+
// pushes `<b>` and names that commit; a finished process without one replies
|
|
328
|
+
// without a link. A thread that fails is named and left open.
|
|
281
329
|
export async function reply(
|
|
282
330
|
io: GitIo,
|
|
283
331
|
host: string,
|
|
284
332
|
gate: string,
|
|
285
333
|
decidedAnswers: readonly Answer[],
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
if (pushed) return { ...fail(pushed), isPushed: false }
|
|
334
|
+
isDone = true,
|
|
335
|
+
): Promise<Done & { isPushed: boolean; waiting: Answer[] }> {
|
|
336
|
+
const { branch, tip, isShipped } = await processTip(io)
|
|
290
337
|
const log = await io.run([
|
|
291
338
|
"git",
|
|
292
339
|
"log",
|
|
293
340
|
"--reverse",
|
|
294
341
|
"--first-parent",
|
|
295
342
|
"--format=%H",
|
|
296
|
-
`${gate}
|
|
343
|
+
`${gate}..${tip}`,
|
|
297
344
|
"--",
|
|
298
345
|
".",
|
|
299
346
|
":(exclude).gtd",
|
|
300
347
|
])
|
|
301
|
-
const
|
|
348
|
+
const code = log.out.split("\n").find(Boolean)
|
|
349
|
+
const ship = isShipped && code ? await shipOf(io, branch, code) : undefined
|
|
350
|
+
const fix = isShipped ? ship : code
|
|
351
|
+
const waiting = decidedAnswers.filter(
|
|
352
|
+
(a) => isShipped && !isDone && a.choice === "fix" && code !== undefined && !ship,
|
|
353
|
+
)
|
|
354
|
+
const ready = decidedAnswers.filter((a) => !waiting.includes(a))
|
|
355
|
+
if (ready.length === 0) return { ok: true, text: "", isPushed: false, waiting }
|
|
356
|
+
// Only a reply naming a commit needs it on GitHub.
|
|
357
|
+
if (fix !== undefined && ready.some((a) => a.choice === "fix")) {
|
|
358
|
+
const pushed = await pushBranch(io, branch, false, isShipped ? `refs/heads/${branch}` : "HEAD")
|
|
359
|
+
if (pushed) return { ...fail(pushed), isPushed: false, waiting }
|
|
360
|
+
}
|
|
302
361
|
const failed: string[] = []
|
|
303
|
-
for (const a of
|
|
362
|
+
for (const a of ready) {
|
|
304
363
|
const sent = await io.run([
|
|
305
364
|
"gh",
|
|
306
365
|
"api",
|
|
@@ -331,12 +390,13 @@ export async function reply(
|
|
|
331
390
|
).code === 0
|
|
332
391
|
if (!resolved) failed.push(a.title)
|
|
333
392
|
}
|
|
334
|
-
const done =
|
|
393
|
+
const done = ready.length - failed.length
|
|
335
394
|
const text = `Replied to and resolved ${done} review thread(s).`
|
|
336
395
|
return failed.length === 0
|
|
337
|
-
? { ok: true, text, isPushed: true }
|
|
396
|
+
? { ok: true, text, isPushed: true, waiting }
|
|
338
397
|
: {
|
|
339
398
|
...fail(`${text}\nThese failed and stay open:\n${failed.map((t) => `- ${t}`).join("\n")}`),
|
|
340
399
|
isPushed: true,
|
|
400
|
+
waiting,
|
|
341
401
|
}
|
|
342
402
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
decided,
|
|
16
16
|
missingReasons,
|
|
17
17
|
reply,
|
|
18
|
+
processTip,
|
|
18
19
|
REVIEW_FILE,
|
|
19
20
|
unresolvedThreads,
|
|
20
21
|
withThreads,
|
|
@@ -217,7 +218,7 @@ function io($: $): Io {
|
|
|
217
218
|
// held it): start one, as the sh driver falls back to `--session-id`.
|
|
218
219
|
const spawned = await $.agent.spawn({
|
|
219
220
|
subagentType: await persona($, t),
|
|
220
|
-
prompt: t.prompt,
|
|
221
|
+
prompt: t.fresh ?? t.prompt,
|
|
221
222
|
description: t.label || "gtd",
|
|
222
223
|
model: await workerModel($, t.model),
|
|
223
224
|
cwd: root,
|
|
@@ -573,10 +574,12 @@ async function replyThreads($: $, stop: Stop) {
|
|
|
573
574
|
const pr = (await read($, run)).pr
|
|
574
575
|
if (!pr?.importedAt) return
|
|
575
576
|
const io = gitIo($)
|
|
577
|
+
const { tip } = await processTip(io)
|
|
576
578
|
const found = await decided(
|
|
577
579
|
io,
|
|
578
580
|
pr.importedAt,
|
|
579
581
|
pr.threads.map((t) => t.id),
|
|
582
|
+
tip,
|
|
580
583
|
)
|
|
581
584
|
if (found === "gone") {
|
|
582
585
|
$.ui.log("gtd pr: the history moved since the review threads were added; they get no replies.")
|
|
@@ -592,12 +595,24 @@ async function replyThreads($: $, stop: Stop) {
|
|
|
592
595
|
$.ui.status("replying to the review threads…")
|
|
593
596
|
let replied
|
|
594
597
|
try {
|
|
595
|
-
replied = await reply(io, pr.host, found.gate, found.answers)
|
|
598
|
+
replied = await reply(io, pr.host, found.gate, found.answers, stop.kind === "done")
|
|
596
599
|
} finally {
|
|
597
600
|
$.ui.status(undefined)
|
|
598
601
|
}
|
|
599
|
-
|
|
600
|
-
|
|
602
|
+
if (replied.text) {
|
|
603
|
+
for (const line of replied.text.split("\n")) $.ui.log(line)
|
|
604
|
+
$.ui.toast(replied.text.split("\n")[0]!)
|
|
605
|
+
}
|
|
606
|
+
if (replied.waiting.length > 0) {
|
|
607
|
+
// Unshipped fixes keep their threads; answered ones are not answered twice.
|
|
608
|
+
if (replied.isPushed) {
|
|
609
|
+
const keep = new Set(replied.waiting.map((a) => a.thread))
|
|
610
|
+
await update($, run, (x) =>
|
|
611
|
+
x.pr ? { ...x, pr: { ...x.pr, threads: x.pr.threads.filter((t) => keep.has(t.id)) } } : x,
|
|
612
|
+
)
|
|
613
|
+
}
|
|
614
|
+
return
|
|
615
|
+
}
|
|
601
616
|
if (replied.isPushed) await forgetPr($)
|
|
602
617
|
}
|
|
603
618
|
|
|
@@ -37,40 +37,106 @@ const ctx = () => {
|
|
|
37
37
|
}
|
|
38
38
|
return context;
|
|
39
39
|
};
|
|
40
|
+
const validatingBy = /* @__PURE__ */ new WeakMap();
|
|
41
|
+
const DEFAULT_CAP = 3;
|
|
42
|
+
const noValidatorStep = (kind, name) => {
|
|
43
|
+
const validating = validatingBy.get(ctx());
|
|
44
|
+
if (validating !== void 0) throw new Error(`gtd: step "${name}": ${kind}() cannot run inside the validator of "${validating.name}" — a validator may only run check, run and judge steps`);
|
|
45
|
+
};
|
|
46
|
+
const noteValidatorPaths = () => {
|
|
47
|
+
const validating = validatingBy.get(ctx());
|
|
48
|
+
if (validating !== void 0) for (const c of ctx().changes()) validating.paths.add(c.path);
|
|
49
|
+
};
|
|
50
|
+
const errorList = (result) => result === void 0 ? [] : typeof result === "string" ? [result] : result;
|
|
40
51
|
/** An agent turn: `prompt` is emitted, the driver lands the diff. */
|
|
41
|
-
const agent = (name, prompt, options = {}) =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
const agent = async (name, prompt, options = {}) => {
|
|
53
|
+
noValidatorStep("agent", name);
|
|
54
|
+
const context = ctx();
|
|
55
|
+
const { validate, cap = DEFAULT_CAP, exhausted, correction } = options;
|
|
56
|
+
const base = typeof validate === "function" ? context.head() : "";
|
|
57
|
+
await context.step({
|
|
58
|
+
kind: "agent",
|
|
59
|
+
name,
|
|
60
|
+
prompt,
|
|
61
|
+
options
|
|
62
|
+
});
|
|
63
|
+
if (typeof validate !== "function") return;
|
|
64
|
+
const own = /* @__PURE__ */ new Set();
|
|
65
|
+
const agentPaths = /* @__PURE__ */ new Set();
|
|
66
|
+
let validated = base;
|
|
67
|
+
const validateNow = async () => {
|
|
68
|
+
for (const c of context.changesSince(validated)) agentPaths.add(c.path);
|
|
69
|
+
const turn = { get changes() {
|
|
70
|
+
return toChanges(context.changesSince(base).filter((c) => !own.has(c.path) || agentPaths.has(c.path)));
|
|
71
|
+
} };
|
|
72
|
+
validatingBy.set(context, {
|
|
73
|
+
name,
|
|
74
|
+
paths: own
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
return errorList(await scope(`${name}.validate`, () => validate(turn)));
|
|
78
|
+
} finally {
|
|
79
|
+
validatingBy.delete(context);
|
|
80
|
+
validated = context.head();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
let errors = await validateNow();
|
|
84
|
+
let retries = 0;
|
|
85
|
+
while (errors.length > 0) {
|
|
86
|
+
if (retries >= cap) {
|
|
87
|
+
if (exhausted !== void 0) await exhausted(errors);
|
|
88
|
+
else await human(`${name}.invalid`, errors.join("\n"), { acceptClean: true });
|
|
89
|
+
retries = 0;
|
|
90
|
+
} else {
|
|
91
|
+
retries++;
|
|
92
|
+
const text = correction !== void 0 ? correction(errors) : `The result did not pass validation. Fix these errors:\n\n${errors.join("\n")}\n`;
|
|
93
|
+
await context.step({
|
|
94
|
+
kind: "agent",
|
|
95
|
+
name,
|
|
96
|
+
prompt: text,
|
|
97
|
+
options,
|
|
98
|
+
original: prompt
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
errors = await validateNow();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
47
104
|
/** A human gate: the process rests until a person lands a turn. */
|
|
48
|
-
const human = (name, messageOrOptions, options) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
105
|
+
const human = (name, messageOrOptions, options) => {
|
|
106
|
+
noValidatorStep("human", name);
|
|
107
|
+
return ctx().step({
|
|
108
|
+
kind: "human",
|
|
109
|
+
name,
|
|
110
|
+
options: typeof messageOrOptions === "string" ? {
|
|
111
|
+
...options,
|
|
112
|
+
message: messageOrOptions
|
|
113
|
+
} : messageOrOptions ?? {}
|
|
114
|
+
});
|
|
115
|
+
};
|
|
56
116
|
/** A check: `body` runs at the edge and its effect on the tree is committed. */
|
|
57
|
-
const run = (name, body, options = {}) =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
117
|
+
const run = async (name, body, options = {}) => {
|
|
118
|
+
await ctx().step({
|
|
119
|
+
kind: "run",
|
|
120
|
+
name,
|
|
121
|
+
body,
|
|
122
|
+
options
|
|
123
|
+
});
|
|
124
|
+
noteValidatorPaths();
|
|
125
|
+
};
|
|
63
126
|
/**
|
|
64
127
|
* Squashes HEAD's tree, with `.gtd/` as the user's branch has it, into one commit on the user's branch
|
|
65
128
|
* with `message`, and moves that branch there; HEAD stays on `gtd/<b>`.
|
|
66
129
|
* Resolves to the new commit's hash, or `undefined` when nothing shipped.
|
|
67
130
|
*/
|
|
68
|
-
const ship = (name, message, options = {}) =>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
131
|
+
const ship = (name, message, options = {}) => {
|
|
132
|
+
noValidatorStep("ship", name);
|
|
133
|
+
return ctx().step({
|
|
134
|
+
kind: "ship",
|
|
135
|
+
name,
|
|
136
|
+
message,
|
|
137
|
+
options
|
|
138
|
+
});
|
|
139
|
+
};
|
|
74
140
|
/** A judge gate: resolves to what the judge answered, for the flow to decide on. */
|
|
75
141
|
const judge = (name, spec) => {
|
|
76
142
|
const { questions, evidence, ...options } = spec;
|
|
@@ -84,10 +150,16 @@ const judge = (name, spec) => {
|
|
|
84
150
|
questions: canonical,
|
|
85
151
|
evidence,
|
|
86
152
|
options
|
|
153
|
+
}).then((judgment) => {
|
|
154
|
+
noteValidatorPaths();
|
|
155
|
+
return judgment;
|
|
87
156
|
});
|
|
88
157
|
};
|
|
89
158
|
/** End the episode from any depth. The next episode starts the flow afresh. */
|
|
90
|
-
const restart = () =>
|
|
159
|
+
const restart = () => {
|
|
160
|
+
noValidatorStep("restart", "restart");
|
|
161
|
+
return ctx().step({ kind: "restart" });
|
|
162
|
+
};
|
|
91
163
|
/** Refuse the pending turn: nothing lands, the process stays where it rests. */
|
|
92
164
|
const refuse = (message) => ctx().refuse(message);
|
|
93
165
|
/**
|
|
@@ -359,13 +431,18 @@ const check = async (name, command, report, options = {}) => {
|
|
|
359
431
|
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
360
432
|
/** The last step's changes outside `.gtd/`. */
|
|
361
433
|
const codeChanges = () => changes().filter((c) => isCode(c.path));
|
|
434
|
+
/** Validator form of `requireProgress`: the agent-addressed error, or `undefined` when the turn is valid. */
|
|
435
|
+
const progressErrors = (file, turnChanges) => {
|
|
436
|
+
const change = turnChanges.get(file);
|
|
437
|
+
if (change?.status !== "deleted") return void 0;
|
|
438
|
+
if (turnChanges.some((c) => isCode(c.path))) return void 0;
|
|
439
|
+
if ((change.before ?? "").trim().startsWith("NOTHING ACTIONABLE")) return void 0;
|
|
440
|
+
return `${file} was deleted without addressing its instructions — implement the changes it lists (then delete it), don't just remove the file. Its content was:\n${change.before ?? ""}`;
|
|
441
|
+
};
|
|
362
442
|
/** Refuse a turn whose only change deletes `file` — unless it says it found nothing to do. */
|
|
363
443
|
const requireProgress = (file) => {
|
|
364
|
-
const
|
|
365
|
-
if (
|
|
366
|
-
if (codeChanges().length > 0) return;
|
|
367
|
-
if ((change.before ?? "").trim().startsWith("NOTHING ACTIONABLE")) return;
|
|
368
|
-
refuse(`gtd land: feedback-progress: ${file} was deleted without addressing its instructions — implement the changes it lists (then delete it), don't just remove the file.`);
|
|
444
|
+
const error = progressErrors(file, changes());
|
|
445
|
+
if (error !== void 0) refuse(`gtd land: feedback-progress: ${error}`);
|
|
369
446
|
};
|
|
370
447
|
/** Refuse a turn that leaves a question in `file` unanswered. An untouched tree is silence, and allowed. */
|
|
371
448
|
const requireAnswers = (file) => {
|
|
@@ -388,18 +465,32 @@ const requireThreadsClosed = (file) => {
|
|
|
388
465
|
if (open.length === 0) return;
|
|
389
466
|
refuse(`gtd land: open-threads: ${countOf(open)} thread(s) in ${file} or code comments wait on you — reply with a conclusion, or delete the thread:\n${open.join("\n")}`);
|
|
390
467
|
};
|
|
468
|
+
/** Validator form of `requireReplies`: the agent-addressed error, or `undefined` when every thread is answered. */
|
|
469
|
+
const repliesErrors = (file) => {
|
|
470
|
+
const open = [...threadLines(file, "agent"), ...codeThreadLines("agent")];
|
|
471
|
+
if (open.length === 0) return void 0;
|
|
472
|
+
return `${countOf(open)} thread(s) in ${file} or code comments still wait on the agent — append one "- A:" reply to each (one "A:" comment line, same token and indentation, directly below a code thread's last line), or fold a concluded thread and delete it:\n${open.join("\n")}`;
|
|
473
|
+
};
|
|
391
474
|
/** Refuse an agent turn that leaves a thread in `file`, or a code thread in a changed file, waiting on the agent (its last entry is `H:`) or with a syntax fault. */
|
|
392
475
|
const requireReplies = (file) => {
|
|
393
|
-
const
|
|
394
|
-
if (
|
|
395
|
-
refuse(`gtd land: unanswered-threads: ${countOf(open)} thread(s) in ${file} or code comments still wait on the agent — append one "- A:" reply to each (one "A:" comment line, same token and indentation, directly below a code thread's last line), or fold a concluded thread and delete it:\n${open.join("\n")}`);
|
|
476
|
+
const error = repliesErrors(file);
|
|
477
|
+
if (error !== void 0) refuse(`gtd land: unanswered-threads: ${error}`);
|
|
396
478
|
};
|
|
397
|
-
|
|
398
|
-
const requireRevert = (edited, base) => {
|
|
479
|
+
const revertFix = (edited, base) => {
|
|
399
480
|
const residue = edited.filter((c) => read(c.path) !== c.before).map((c) => c.path);
|
|
400
|
-
if (residue.length === 0) return;
|
|
481
|
+
if (residue.length === 0) return void 0;
|
|
401
482
|
const quoted = residue.map((path) => `'${path.replace(/'/g, "'\\''")}'`).join(" ");
|
|
402
|
-
|
|
483
|
+
return `${residue.join(", ")} still differ from ${base}~1 — the revert did not take. Run \`git checkout ${base}~1 -- ${quoted}\``;
|
|
484
|
+
};
|
|
485
|
+
/** Validator form of `requireRevert`: reads the tree, not the turn's changes. */
|
|
486
|
+
const revertErrors = (edited, base) => {
|
|
487
|
+
const fix = revertFix(edited, base);
|
|
488
|
+
return fix === void 0 ? void 0 : `${fix}.`;
|
|
489
|
+
};
|
|
490
|
+
/** Refuse a turn that did not undo the code edits `edited` recorded, back to their content before `base`. */
|
|
491
|
+
const requireRevert = (edited, base) => {
|
|
492
|
+
const fix = revertFix(edited, base);
|
|
493
|
+
if (fix !== void 0) refuse(`gtd land: require-revert: ${fix}, then \`gtd land\` again.`);
|
|
403
494
|
};
|
|
404
495
|
const SHAPES = [
|
|
405
496
|
"land",
|
|
@@ -687,10 +778,12 @@ var flows_exports = /* @__PURE__ */ __exportAll({
|
|
|
687
778
|
moveScript: () => moveScript,
|
|
688
779
|
numeric: () => numeric,
|
|
689
780
|
openQuestions: () => openQuestions,
|
|
781
|
+
progressErrors: () => progressErrors,
|
|
690
782
|
quote: () => quote,
|
|
691
783
|
read: () => read,
|
|
692
784
|
refuse: () => refuse,
|
|
693
785
|
removeScript: () => removeScript,
|
|
786
|
+
repliesErrors: () => repliesErrors,
|
|
694
787
|
requireAnswers: () => requireAnswers,
|
|
695
788
|
requireProgress: () => requireProgress,
|
|
696
789
|
requireReplies: () => requireReplies,
|
|
@@ -698,6 +791,7 @@ var flows_exports = /* @__PURE__ */ __exportAll({
|
|
|
698
791
|
requireThreadsClosed: () => requireThreadsClosed,
|
|
699
792
|
restart: () => restart,
|
|
700
793
|
restoreScript: () => restoreScript,
|
|
794
|
+
revertErrors: () => revertErrors,
|
|
701
795
|
revertScript: () => revertScript,
|
|
702
796
|
run: () => run,
|
|
703
797
|
scope: () => scope,
|
|
@@ -715,4 +809,4 @@ var flows_exports = /* @__PURE__ */ __exportAll({
|
|
|
715
809
|
writeScript: () => writeScript,
|
|
716
810
|
wrote: () => wrote
|
|
717
811
|
});
|
|
718
|
-
export {
|
|
812
|
+
export { read as $, revertScript as A, envSettings as B, wrote as C, quote as D, moveScript as E, accessFor as F, installContext as G, glob as H, agent as I, mark as J, judge as K, changes as L, writeScript as M, FLOWS_PROTOCOL as N, removeScript as O, FlowsProtocolError as P, protocolMismatch as Q, changesSince as R, seamGate as S, checkScript as T, head as U, envSettingsOf as V, human as W, matches as X, markedOptions as Y, openQuestions as Z, requireProgress as _, defineWorkflow as a, sections as at, requireThreadsClosed as b, check as c, ship as ct, hasThreadFor as d, threads as dt, refuse as et, judgeDiffShape as f, workflow as ft, requireAnswers as g, repliesErrors as h, configure as i, sectionBodies as it, showScript as j, restoreScript as k, codeChanges as l, skillsFor as lt, progressErrors as m, configProblems as n, run as nt, isFactoryWorkflow as o, settings as ot, numeric as p, kept as q, defineConfig as r, scope as rt, answered as s, settingsOf as st, flows_exports as t, restart as tt, displayPath as u, start as ut, requireReplies as v, applyScript as w, revertErrors as x, requireRevert as y, codeThreads as z };
|