@pmelab/gtd 47.0.0 → 48.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 +15 -13
- package/dist/gtd.bundle.mjs +203 -867
- package/dist/testkit.mjs +89 -53
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +0 -2
- package/src/cli/Cli.ts +0 -15
- package/src/cli/Program.test.ts +0 -1
- package/src/cli/Program.ts +0 -8
- package/src/cli/ToolCommands.ts +1 -2
- package/src/install/Install.ts +89 -35
- package/src/testkit/Layers.ts +0 -1
- package/src/wire/ContextLimit.ts +1 -1
package/dist/testkit.mjs
CHANGED
|
@@ -6154,8 +6154,15 @@ export CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1
|
|
|
6154
6154
|
export BASH_DEFAULT_TIMEOUT_MS=1800000
|
|
6155
6155
|
export BASH_MAX_TIMEOUT_MS=7200000
|
|
6156
6156
|
|
|
6157
|
+
# gtd's paths, scripts and checks are relative to the repository root.
|
|
6158
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
6159
|
+
|
|
6157
6160
|
beat=1
|
|
6158
6161
|
while :; do
|
|
6162
|
+
cost=
|
|
6163
|
+
model=
|
|
6164
|
+
judged_before="\${judged:-}"
|
|
6165
|
+
judged=
|
|
6159
6166
|
# One value per invocation: \`gtd next --json=<path>\` is a pure read (peek
|
|
6160
6167
|
# or dispatch, indistinguishable, always safe to call again), so nothing is
|
|
6161
6168
|
# lost by reading \`kind\`/\`idle\` as two separate calls rather than one
|
|
@@ -6183,14 +6190,22 @@ while :; do
|
|
|
6183
6190
|
# \`gtd judge run\` and land the verdict via \`gtd judge answer\`. POSIX sh has
|
|
6184
6191
|
# no pipefail, so each step is captured first and only piped on when it
|
|
6185
6192
|
# succeeded; if \`gtd judge run\` fails or \`gtd judge answer\` rejects the
|
|
6186
|
-
# verdict, its stderr reaches you and
|
|
6193
|
+
# verdict, its stderr reaches you and the gate is handed to you — on the
|
|
6194
|
+
# opening beat too, so it never lands unanswered — as it is when the gate
|
|
6195
|
+
# we just answered comes straight back.
|
|
6187
6196
|
message)
|
|
6188
|
-
if [ -n "$(gtd next --json=judge)" ]
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6197
|
+
if [ -n "$(gtd next --json=judge)" ]; then
|
|
6198
|
+
state="$(gtd next --json=state)"
|
|
6199
|
+
if [ "$state" != "$judged_before" ] &&
|
|
6200
|
+
verdict="$(gtd judge --json | gtd judge run)" &&
|
|
6201
|
+
script="$(printf '%s\\n' "$verdict" | gtd judge answer --json=script)"; then
|
|
6202
|
+
printf '%s\\n' "$script" | sh
|
|
6203
|
+
judged="$state"
|
|
6204
|
+
beat=$((beat + 1))
|
|
6205
|
+
continue
|
|
6206
|
+
fi
|
|
6207
|
+
gtd next
|
|
6208
|
+
exit 0
|
|
6194
6209
|
fi
|
|
6195
6210
|
[ "$beat" = 1 ] || {
|
|
6196
6211
|
gtd next
|
|
@@ -6209,45 +6224,83 @@ while :; do
|
|
|
6209
6224
|
model="$(gtd next --json=model)"
|
|
6210
6225
|
system="$(gtd next --json=system)"
|
|
6211
6226
|
validate="$(gtd next --json=validate)"
|
|
6227
|
+
context="$(gtd next --json=context)"
|
|
6212
6228
|
log="$(gtd next --json=log)"
|
|
6229
|
+
# Absolute, since the hook runs wherever the agent last cd'ed to.
|
|
6230
|
+
case "$log" in /*) ;; *) log="$(git rev-parse --show-toplevel)/$log" ;; esac
|
|
6213
6231
|
# \`model\` is a hint, not a model name — the bundled workflow ships
|
|
6214
6232
|
# \`smart\` and \`base\`, which \`claude --model\` rejects. Resolve them
|
|
6215
|
-
#
|
|
6216
|
-
#
|
|
6233
|
+
# from your environment; unset, the hint is dropped and the agent runs
|
|
6234
|
+
# its own default. Any other value is a real model name.
|
|
6217
6235
|
case "$model" in
|
|
6218
6236
|
smart) model="\${GTD_LOOP_MODEL_SMART:-}" ;;
|
|
6219
6237
|
base) model="\${GTD_LOOP_MODEL_BASE:-}" ;;
|
|
6220
6238
|
esac
|
|
6221
|
-
# The
|
|
6222
|
-
#
|
|
6223
|
-
#
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6239
|
+
# The step's context limit, as a Claude Code hook after every tool
|
|
6240
|
+
# call and before compacting. Past \`hard\` it stops the turn and
|
|
6241
|
+
# appends why to the stop file, so we land nothing and stop too.
|
|
6242
|
+
soft="$(printf '%s' "$context" | sed -n 's/.*"soft":\\([0-9.]*\\).*/\\1/p')"
|
|
6243
|
+
hard="$(printf '%s' "$context" | sed -n 's/.*"hard":\\([0-9.]*\\).*/\\1/p')"
|
|
6244
|
+
window="\${GTD_LOOP_CONTEXT_WINDOW:-}"
|
|
6245
|
+
case "$model" in *'[1m]'* | *'[1M]'*) window="\${window:-1000000}" ;; esac
|
|
6246
|
+
hook="gtd context-hook\${soft:+ --soft $soft}\${hard:+ --hard $hard}\${window:+ --window $window}"
|
|
6247
|
+
hooks="[{\\"hooks\\":[{\\"type\\":\\"command\\",\\"command\\":\\"$hook\\"}]}]"
|
|
6248
|
+
settings="{\\"hooks\\":{\\"PostToolUse\\":$hooks,\\"PreCompact\\":$hooks}}"
|
|
6249
|
+
stop="$log.context-stop"
|
|
6250
|
+
rm -f "$stop"
|
|
6251
|
+
# One turn, its prompt on stdin. The prompt embeds a full diff, so it
|
|
6252
|
+
# never goes on argv — argv is capped (~1 MB on macOS, and POSIX
|
|
6253
|
+
# guarantees only 4 KB, ARG_MAX), and a diff crosses that far sooner
|
|
6254
|
+
# than you'd expect. Every turn passes the same $model/$system:
|
|
6255
|
+
# whether \`claude --resume\` re-applies the original session's is a
|
|
6256
|
+
# harness detail gtd cannot verify from outside. Its JSON envelope
|
|
6257
|
+
# goes to the log, and its \`total_cost_usd\` adds up into $cost. An
|
|
6258
|
+
# \`is_error\` envelope (max turns, an error mid-turn) or one another
|
|
6259
|
+
# hook stopped fails the turn even at exit 0: landing it would land
|
|
6260
|
+
# the failure as work. Only a turn that never ran (no envelope: the
|
|
6261
|
+
# session flag was refused) is worth retrying under the other flag.
|
|
6262
|
+
turn() {
|
|
6263
|
+
out="$(GTD_LOOP_CONTEXT_STOP_FILE="$stop" claude -p "$@" \\
|
|
6264
|
+
\${model:+--model "$model"} \\
|
|
6265
|
+
\${system:+--system-prompt "$system"} \\
|
|
6266
|
+
--settings "$settings" --output-format json \\
|
|
6267
|
+
--dangerously-skip-permissions 2>>"$log")" && ok=0 || ok=$?
|
|
6268
|
+
printf '%s\\n' "$out" >>"$log"
|
|
6269
|
+
case "$out" in *'"type":"result"'*) ran=true ;; *) ran=false ;; esac
|
|
6270
|
+
case "$out" in
|
|
6271
|
+
*'"is_error":true'* | *'"terminal_reason":"hook_stopped"'*) ok=1 ;;
|
|
6272
|
+
esac
|
|
6273
|
+
usd="$(printf '%s\\n' "$out" | sed -n 's/.*"total_cost_usd":\\([0-9.eE+-]*\\).*/\\1/p' | tail -n 1)"
|
|
6274
|
+
[ -z "$usd" ] || cost="$(awk -v a="\${cost:-0}" -v b="$usd" 'BEGIN { printf "%.6f", a + b }')"
|
|
6275
|
+
[ "\${cost:-}" != 0.000000 ] || cost=
|
|
6276
|
+
if [ -s "$stop" ]; then
|
|
6277
|
+
cat "$stop" >&2
|
|
6278
|
+
exit 1
|
|
6279
|
+
fi
|
|
6280
|
+
return "$ok"
|
|
6281
|
+
}
|
|
6282
|
+
# Plain \`gtd next\` (not --json=content) renders the diff once.
|
|
6283
|
+
gtd next >"$log.prompt"
|
|
6231
6284
|
if [ "\${resume:-}" = true ]
|
|
6232
|
-
then
|
|
6233
|
-
else
|
|
6285
|
+
then first=--resume second=--session-id
|
|
6286
|
+
else first=--session-id second=--resume
|
|
6234
6287
|
fi
|
|
6288
|
+
turn "$first" "$session_id" <"$log.prompt" ||
|
|
6289
|
+
{ [ "$ran" = false ] && turn "$second" "$session_id" <"$log.prompt"; } || {
|
|
6290
|
+
echo "agent turn failed — see $log" >&2
|
|
6291
|
+
exit 1
|
|
6292
|
+
}
|
|
6235
6293
|
n=0
|
|
6236
6294
|
while [ -n "\${validate:-}" ] && ! fix="$(sh -c "$validate" 2>&1)"; do
|
|
6237
6295
|
n=$((n + 1)) && [ "$n" -gt 3 ] && { printf '%s\\n' "$fix" >&2; exit 1; }
|
|
6238
|
-
# $fix IS the fix prompt, verbatim
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
printf '%s' "$fix" | claude -p --resume "$session_id" \\
|
|
6247
|
-
\${model:+--model "$model"} \\
|
|
6248
|
-
\${system:+--system-prompt "$system"} \\
|
|
6249
|
-
--dangerously-skip-permissions >>"$log" 2>&1
|
|
6250
|
-
done ;;
|
|
6296
|
+
# $fix IS the fix prompt, verbatim.
|
|
6297
|
+
printf '%s' "$fix" >"$log.prompt"
|
|
6298
|
+
turn --resume "$session_id" <"$log.prompt" || {
|
|
6299
|
+
echo "fix turn failed — see $log" >&2
|
|
6300
|
+
exit 1
|
|
6301
|
+
}
|
|
6302
|
+
done
|
|
6303
|
+
rm -f "$log.prompt" ;;
|
|
6251
6304
|
esac
|
|
6252
6305
|
|
|
6253
6306
|
# Read every value the landing needs BEFORE piping the script to \`sh\`:
|
|
@@ -6257,7 +6310,8 @@ while :; do
|
|
|
6257
6310
|
# before the script runs and changes the tree out from under them.
|
|
6258
6311
|
settled="$(gtd land --json=settled)"
|
|
6259
6312
|
idle="$(gtd land --json=idle)"
|
|
6260
|
-
|
|
6313
|
+
# A prompt beat's turns record their cost, tagged with the model that ran.
|
|
6314
|
+
gtd land --json=script \${cost:+--cost="$cost"} \${cost:+\${model:+--model="$model"}} | sh
|
|
6261
6315
|
if [ "\${settled:-}" = true ]; then
|
|
6262
6316
|
[ "\${idle:-}" = true ] && gtd next
|
|
6263
6317
|
exit 0
|
|
@@ -10893,7 +10947,6 @@ const runInstallCommand = (out) => gen(function* () {
|
|
|
10893
10947
|
yield* (yield* Workspace).write(WORKFLOW_MODULE, CONFIG_SCAFFOLD);
|
|
10894
10948
|
yield* (yield* Narrator).warn(`gtd install: wrote ${WORKFLOW_MODULE}, importing gtd's shipped workflows — commit it before your first \`gtd land\``);
|
|
10895
10949
|
});
|
|
10896
|
-
const runLoopCommand = () => flatMap$4(ToolCommands, (tools) => tools.loop(selfGtd()));
|
|
10897
10950
|
/**
|
|
10898
10951
|
* Exactly one trailing newline — duplicated from `Cli.ts`'s own
|
|
10899
10952
|
* `normalizeTrailingNewline` rather than imported, to avoid a circular value
|
|
@@ -11471,7 +11524,6 @@ const needsOf = (kind) => {
|
|
|
11471
11524
|
switch (kind) {
|
|
11472
11525
|
case "lsp":
|
|
11473
11526
|
case "install":
|
|
11474
|
-
case "loop":
|
|
11475
11527
|
case "contextHook":
|
|
11476
11528
|
case "judgeRun":
|
|
11477
11529
|
case "strays": return "none";
|
|
@@ -11511,7 +11563,6 @@ const dispatchVoidCommand = (command, json, out) => {
|
|
|
11511
11563
|
return runCheckCommand(command.mode, command.file, command.openQuestions ?? false, command.openThreads ?? false);
|
|
11512
11564
|
case "uncheck": return runUncheckCommand(command.file);
|
|
11513
11565
|
case "install": return runInstallCommand(out);
|
|
11514
|
-
case "loop": return runLoopCommand();
|
|
11515
11566
|
case "contextHook": return runContextHookCommand(command, out);
|
|
11516
11567
|
case "summary": return runSummaryCommand(out, command.slice);
|
|
11517
11568
|
case "base": return runBaseCommand(out);
|
|
@@ -12038,20 +12089,6 @@ const COMMAND_ROWS = [
|
|
|
12038
12089
|
"else it writes nothing."
|
|
12039
12090
|
]
|
|
12040
12091
|
},
|
|
12041
|
-
{
|
|
12042
|
-
token: "loop",
|
|
12043
|
-
kind: "loop",
|
|
12044
|
-
arity: "none",
|
|
12045
|
-
details: [
|
|
12046
|
-
"Drive this worktree's process until it rests on you: read",
|
|
12047
|
-
"each beat, run the agent turn or script it demands, and",
|
|
12048
|
-
"land the result. Exits 0 at a human gate, at idle and when",
|
|
12049
|
-
"a landing settles; exits 1 on a stall or a failed turn or",
|
|
12050
|
-
"landing. The agent CLI and the model each workflow hint",
|
|
12051
|
-
"maps to are per-machine settings (GTD_LOOP_* variables, or",
|
|
12052
|
-
"~/.config/gtd/loop.json); see docs/driver.md"
|
|
12053
|
-
]
|
|
12054
|
-
},
|
|
12055
12092
|
{
|
|
12056
12093
|
token: "context-hook",
|
|
12057
12094
|
kind: "contextHook",
|
|
@@ -13403,7 +13440,6 @@ const missingTool = (name) => () => fail$5(/* @__PURE__ */ new Error(`gtd ${name
|
|
|
13403
13440
|
const missingToolCommands = {
|
|
13404
13441
|
ui: missingTool("ui"),
|
|
13405
13442
|
lsp: missingTool("lsp"),
|
|
13406
|
-
loop: missingTool("loop"),
|
|
13407
13443
|
contextHook: missingTool("context-hook"),
|
|
13408
13444
|
judge: () => Promise.reject(/* @__PURE__ */ new Error("gtd judge run: ToolCommands has no test double wired into testLayers()"))
|
|
13409
13445
|
};
|
package/package.json
CHANGED
package/src/cli/Cli.test.ts
CHANGED
|
@@ -1147,7 +1147,6 @@ describe("standaloneKinds / needsOf", () => {
|
|
|
1147
1147
|
"check",
|
|
1148
1148
|
"uncheck",
|
|
1149
1149
|
"install",
|
|
1150
|
-
"loop",
|
|
1151
1150
|
"contextHook",
|
|
1152
1151
|
"judgeRun",
|
|
1153
1152
|
"strays",
|
|
@@ -1159,7 +1158,6 @@ describe("standaloneKinds / needsOf", () => {
|
|
|
1159
1158
|
expect(needsOf("check")).toBe("fs")
|
|
1160
1159
|
expect(needsOf("uncheck")).toBe("fs")
|
|
1161
1160
|
expect(needsOf("install")).toBe("none")
|
|
1162
|
-
expect(needsOf("loop")).toBe("none")
|
|
1163
1161
|
expect(needsOf("contextHook")).toBe("none")
|
|
1164
1162
|
expect(needsOf("judgeRun")).toBe("none")
|
|
1165
1163
|
expect(needsOf("strays")).toBe("none")
|
package/src/cli/Cli.ts
CHANGED
|
@@ -642,20 +642,6 @@ const COMMAND_ROWS: readonly CommandRow[] = [
|
|
|
642
642
|
"else it writes nothing.",
|
|
643
643
|
],
|
|
644
644
|
},
|
|
645
|
-
{
|
|
646
|
-
token: "loop",
|
|
647
|
-
kind: "loop",
|
|
648
|
-
arity: "none",
|
|
649
|
-
details: [
|
|
650
|
-
"Drive this worktree's process until it rests on you: read",
|
|
651
|
-
"each beat, run the agent turn or script it demands, and",
|
|
652
|
-
"land the result. Exits 0 at a human gate, at idle and when",
|
|
653
|
-
"a landing settles; exits 1 on a stall or a failed turn or",
|
|
654
|
-
"landing. The agent CLI and the model each workflow hint",
|
|
655
|
-
"maps to are per-machine settings (GTD_LOOP_* variables, or",
|
|
656
|
-
"~/.config/gtd/loop.json); see docs/driver.md",
|
|
657
|
-
],
|
|
658
|
-
},
|
|
659
645
|
{
|
|
660
646
|
token: "context-hook",
|
|
661
647
|
kind: "contextHook",
|
|
@@ -1528,7 +1514,6 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1528
1514
|
| "next"
|
|
1529
1515
|
| "validate"
|
|
1530
1516
|
| "install"
|
|
1531
|
-
| "loop"
|
|
1532
1517
|
| "doors"
|
|
1533
1518
|
| "slices"
|
|
1534
1519
|
| "base"
|
package/src/cli/Program.test.ts
CHANGED
|
@@ -1685,7 +1685,6 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1685
1685
|
check: { kind: "check", mode: "qa", file: ".gtd/TODO.md" },
|
|
1686
1686
|
uncheck: { kind: "uncheck", file: ".gtd/REVIEW.md" },
|
|
1687
1687
|
install: { kind: "install" },
|
|
1688
|
-
loop: { kind: "loop" },
|
|
1689
1688
|
contextHook: { kind: "contextHook" },
|
|
1690
1689
|
summary: { kind: "summary" },
|
|
1691
1690
|
base: { kind: "base" },
|
package/src/cli/Program.ts
CHANGED
|
@@ -163,7 +163,6 @@ export type Command =
|
|
|
163
163
|
}
|
|
164
164
|
| { readonly kind: "uncheck"; readonly file: string }
|
|
165
165
|
| { readonly kind: "install" }
|
|
166
|
-
| { readonly kind: "loop" }
|
|
167
166
|
| {
|
|
168
167
|
readonly kind: "contextHook"
|
|
169
168
|
readonly soft?: number
|
|
@@ -239,9 +238,6 @@ const runInstallCommand = (
|
|
|
239
238
|
)
|
|
240
239
|
})
|
|
241
240
|
|
|
242
|
-
const runLoopCommand = (): Effect.Effect<void, Error, ToolCommands> =>
|
|
243
|
-
Effect.flatMap(ToolCommands, (tools) => tools.loop(selfGtd()))
|
|
244
|
-
|
|
245
241
|
/**
|
|
246
242
|
* `planLanding`'s result — a preview of what `gtd land` would do, plus the
|
|
247
243
|
* combined `script` the driver runs to do it. `script` is
|
|
@@ -1221,7 +1217,6 @@ export const needsOf = (kind: Command["kind"]): Needs => {
|
|
|
1221
1217
|
switch (kind) {
|
|
1222
1218
|
case "lsp":
|
|
1223
1219
|
case "install":
|
|
1224
|
-
case "loop":
|
|
1225
1220
|
case "contextHook":
|
|
1226
1221
|
case "judgeRun":
|
|
1227
1222
|
case "strays":
|
|
@@ -1240,7 +1235,6 @@ export const standaloneKinds = (): readonly Command["kind"][] => [
|
|
|
1240
1235
|
"check",
|
|
1241
1236
|
"uncheck",
|
|
1242
1237
|
"install",
|
|
1243
|
-
"loop",
|
|
1244
1238
|
"contextHook",
|
|
1245
1239
|
"judgeRun",
|
|
1246
1240
|
"strays",
|
|
@@ -1311,8 +1305,6 @@ const dispatchVoidCommand = (
|
|
|
1311
1305
|
return runUncheckCommand(command.file)
|
|
1312
1306
|
case "install":
|
|
1313
1307
|
return runInstallCommand(out)
|
|
1314
|
-
case "loop":
|
|
1315
|
-
return runLoopCommand()
|
|
1316
1308
|
case "contextHook":
|
|
1317
1309
|
return runContextHookCommand(command, out)
|
|
1318
1310
|
case "summary":
|
package/src/cli/ToolCommands.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Effect } from "effect"
|
|
|
3
3
|
import type { UiSettings } from "../flows/index.js"
|
|
4
4
|
import type { ContextHookRequest } from "../wire/index.js"
|
|
5
5
|
|
|
6
|
-
/** How to run THIS gtd — a
|
|
6
|
+
/** How to run THIS gtd — a tool (the LSP) never reads a different version's state than the one it ships with. */
|
|
7
7
|
export type SelfGtd = readonly [string, ...string[]]
|
|
8
8
|
|
|
9
9
|
export interface UiRequest {
|
|
@@ -38,7 +38,6 @@ export class ToolCommands extends Context.Tag("ToolCommands")<
|
|
|
38
38
|
out: { readonly write: (chunk: string) => void; readonly flush: () => void },
|
|
39
39
|
) => Effect.Effect<void, Error>
|
|
40
40
|
readonly lsp: (gtd: SelfGtd) => Effect.Effect<void, Error>
|
|
41
|
-
readonly loop: (gtd: SelfGtd) => Effect.Effect<void, Error>
|
|
42
41
|
/** The hook's stdout: a JSON document, or empty to let the turn go on. */
|
|
43
42
|
readonly contextHook: (request: ContextHookRequest) => Effect.Effect<string, Error>
|
|
44
43
|
readonly judge: (request: JudgeRequest) => Promise<string>
|
package/src/install/Install.ts
CHANGED
|
@@ -11,8 +11,15 @@ export CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1
|
|
|
11
11
|
export BASH_DEFAULT_TIMEOUT_MS=1800000
|
|
12
12
|
export BASH_MAX_TIMEOUT_MS=7200000
|
|
13
13
|
|
|
14
|
+
# gtd's paths, scripts and checks are relative to the repository root.
|
|
15
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
16
|
+
|
|
14
17
|
beat=1
|
|
15
18
|
while :; do
|
|
19
|
+
cost=
|
|
20
|
+
model=
|
|
21
|
+
judged_before="\${judged:-}"
|
|
22
|
+
judged=
|
|
16
23
|
# One value per invocation: \`gtd next --json=<path>\` is a pure read (peek
|
|
17
24
|
# or dispatch, indistinguishable, always safe to call again), so nothing is
|
|
18
25
|
# lost by reading \`kind\`/\`idle\` as two separate calls rather than one
|
|
@@ -40,14 +47,22 @@ while :; do
|
|
|
40
47
|
# \`gtd judge run\` and land the verdict via \`gtd judge answer\`. POSIX sh has
|
|
41
48
|
# no pipefail, so each step is captured first and only piped on when it
|
|
42
49
|
# succeeded; if \`gtd judge run\` fails or \`gtd judge answer\` rejects the
|
|
43
|
-
# verdict, its stderr reaches you and
|
|
50
|
+
# verdict, its stderr reaches you and the gate is handed to you — on the
|
|
51
|
+
# opening beat too, so it never lands unanswered — as it is when the gate
|
|
52
|
+
# we just answered comes straight back.
|
|
44
53
|
message)
|
|
45
|
-
if [ -n "$(gtd next --json=judge)" ]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
if [ -n "$(gtd next --json=judge)" ]; then
|
|
55
|
+
state="$(gtd next --json=state)"
|
|
56
|
+
if [ "$state" != "$judged_before" ] &&
|
|
57
|
+
verdict="$(gtd judge --json | gtd judge run)" &&
|
|
58
|
+
script="$(printf '%s\\n' "$verdict" | gtd judge answer --json=script)"; then
|
|
59
|
+
printf '%s\\n' "$script" | sh
|
|
60
|
+
judged="$state"
|
|
61
|
+
beat=$((beat + 1))
|
|
62
|
+
continue
|
|
63
|
+
fi
|
|
64
|
+
gtd next
|
|
65
|
+
exit 0
|
|
51
66
|
fi
|
|
52
67
|
[ "$beat" = 1 ] || {
|
|
53
68
|
gtd next
|
|
@@ -66,45 +81,83 @@ while :; do
|
|
|
66
81
|
model="$(gtd next --json=model)"
|
|
67
82
|
system="$(gtd next --json=system)"
|
|
68
83
|
validate="$(gtd next --json=validate)"
|
|
84
|
+
context="$(gtd next --json=context)"
|
|
69
85
|
log="$(gtd next --json=log)"
|
|
86
|
+
# Absolute, since the hook runs wherever the agent last cd'ed to.
|
|
87
|
+
case "$log" in /*) ;; *) log="$(git rev-parse --show-toplevel)/$log" ;; esac
|
|
70
88
|
# \`model\` is a hint, not a model name — the bundled workflow ships
|
|
71
89
|
# \`smart\` and \`base\`, which \`claude --model\` rejects. Resolve them
|
|
72
|
-
#
|
|
73
|
-
#
|
|
90
|
+
# from your environment; unset, the hint is dropped and the agent runs
|
|
91
|
+
# its own default. Any other value is a real model name.
|
|
74
92
|
case "$model" in
|
|
75
93
|
smart) model="\${GTD_LOOP_MODEL_SMART:-}" ;;
|
|
76
94
|
base) model="\${GTD_LOOP_MODEL_BASE:-}" ;;
|
|
77
95
|
esac
|
|
78
|
-
# The
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
# The step's context limit, as a Claude Code hook after every tool
|
|
97
|
+
# call and before compacting. Past \`hard\` it stops the turn and
|
|
98
|
+
# appends why to the stop file, so we land nothing and stop too.
|
|
99
|
+
soft="$(printf '%s' "$context" | sed -n 's/.*"soft":\\([0-9.]*\\).*/\\1/p')"
|
|
100
|
+
hard="$(printf '%s' "$context" | sed -n 's/.*"hard":\\([0-9.]*\\).*/\\1/p')"
|
|
101
|
+
window="\${GTD_LOOP_CONTEXT_WINDOW:-}"
|
|
102
|
+
case "$model" in *'[1m]'* | *'[1M]'*) window="\${window:-1000000}" ;; esac
|
|
103
|
+
hook="gtd context-hook\${soft:+ --soft $soft}\${hard:+ --hard $hard}\${window:+ --window $window}"
|
|
104
|
+
hooks="[{\\"hooks\\":[{\\"type\\":\\"command\\",\\"command\\":\\"$hook\\"}]}]"
|
|
105
|
+
settings="{\\"hooks\\":{\\"PostToolUse\\":$hooks,\\"PreCompact\\":$hooks}}"
|
|
106
|
+
stop="$log.context-stop"
|
|
107
|
+
rm -f "$stop"
|
|
108
|
+
# One turn, its prompt on stdin. The prompt embeds a full diff, so it
|
|
109
|
+
# never goes on argv — argv is capped (~1 MB on macOS, and POSIX
|
|
110
|
+
# guarantees only 4 KB, ARG_MAX), and a diff crosses that far sooner
|
|
111
|
+
# than you'd expect. Every turn passes the same $model/$system:
|
|
112
|
+
# whether \`claude --resume\` re-applies the original session's is a
|
|
113
|
+
# harness detail gtd cannot verify from outside. Its JSON envelope
|
|
114
|
+
# goes to the log, and its \`total_cost_usd\` adds up into $cost. An
|
|
115
|
+
# \`is_error\` envelope (max turns, an error mid-turn) or one another
|
|
116
|
+
# hook stopped fails the turn even at exit 0: landing it would land
|
|
117
|
+
# the failure as work. Only a turn that never ran (no envelope: the
|
|
118
|
+
# session flag was refused) is worth retrying under the other flag.
|
|
119
|
+
turn() {
|
|
120
|
+
out="$(GTD_LOOP_CONTEXT_STOP_FILE="$stop" claude -p "$@" \\
|
|
121
|
+
\${model:+--model "$model"} \\
|
|
122
|
+
\${system:+--system-prompt "$system"} \\
|
|
123
|
+
--settings "$settings" --output-format json \\
|
|
124
|
+
--dangerously-skip-permissions 2>>"$log")" && ok=0 || ok=$?
|
|
125
|
+
printf '%s\\n' "$out" >>"$log"
|
|
126
|
+
case "$out" in *'"type":"result"'*) ran=true ;; *) ran=false ;; esac
|
|
127
|
+
case "$out" in
|
|
128
|
+
*'"is_error":true'* | *'"terminal_reason":"hook_stopped"'*) ok=1 ;;
|
|
129
|
+
esac
|
|
130
|
+
usd="$(printf '%s\\n' "$out" | sed -n 's/.*"total_cost_usd":\\([0-9.eE+-]*\\).*/\\1/p' | tail -n 1)"
|
|
131
|
+
[ -z "$usd" ] || cost="$(awk -v a="\${cost:-0}" -v b="$usd" 'BEGIN { printf "%.6f", a + b }')"
|
|
132
|
+
[ "\${cost:-}" != 0.000000 ] || cost=
|
|
133
|
+
if [ -s "$stop" ]; then
|
|
134
|
+
cat "$stop" >&2
|
|
135
|
+
exit 1
|
|
136
|
+
fi
|
|
137
|
+
return "$ok"
|
|
138
|
+
}
|
|
139
|
+
# Plain \`gtd next\` (not --json=content) renders the diff once.
|
|
140
|
+
gtd next >"$log.prompt"
|
|
88
141
|
if [ "\${resume:-}" = true ]
|
|
89
|
-
then
|
|
90
|
-
else
|
|
142
|
+
then first=--resume second=--session-id
|
|
143
|
+
else first=--session-id second=--resume
|
|
91
144
|
fi
|
|
145
|
+
turn "$first" "$session_id" <"$log.prompt" ||
|
|
146
|
+
{ [ "$ran" = false ] && turn "$second" "$session_id" <"$log.prompt"; } || {
|
|
147
|
+
echo "agent turn failed — see $log" >&2
|
|
148
|
+
exit 1
|
|
149
|
+
}
|
|
92
150
|
n=0
|
|
93
151
|
while [ -n "\${validate:-}" ] && ! fix="$(sh -c "$validate" 2>&1)"; do
|
|
94
152
|
n=$((n + 1)) && [ "$n" -gt 3 ] && { printf '%s\\n' "$fix" >&2; exit 1; }
|
|
95
|
-
# $fix IS the fix prompt, verbatim
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
printf '%s' "$fix" | claude -p --resume "$session_id" \\
|
|
104
|
-
\${model:+--model "$model"} \\
|
|
105
|
-
\${system:+--system-prompt "$system"} \\
|
|
106
|
-
--dangerously-skip-permissions >>"$log" 2>&1
|
|
107
|
-
done ;;
|
|
153
|
+
# $fix IS the fix prompt, verbatim.
|
|
154
|
+
printf '%s' "$fix" >"$log.prompt"
|
|
155
|
+
turn --resume "$session_id" <"$log.prompt" || {
|
|
156
|
+
echo "fix turn failed — see $log" >&2
|
|
157
|
+
exit 1
|
|
158
|
+
}
|
|
159
|
+
done
|
|
160
|
+
rm -f "$log.prompt" ;;
|
|
108
161
|
esac
|
|
109
162
|
|
|
110
163
|
# Read every value the landing needs BEFORE piping the script to \`sh\`:
|
|
@@ -114,7 +167,8 @@ while :; do
|
|
|
114
167
|
# before the script runs and changes the tree out from under them.
|
|
115
168
|
settled="$(gtd land --json=settled)"
|
|
116
169
|
idle="$(gtd land --json=idle)"
|
|
117
|
-
|
|
170
|
+
# A prompt beat's turns record their cost, tagged with the model that ran.
|
|
171
|
+
gtd land --json=script \${cost:+--cost="$cost"} \${cost:+\${model:+--model="$model"}} | sh
|
|
118
172
|
if [ "\${settled:-}" = true ]; then
|
|
119
173
|
[ "\${idle:-}" = true ] && gtd next
|
|
120
174
|
exit 0
|
package/src/testkit/Layers.ts
CHANGED
|
@@ -205,7 +205,6 @@ const missingTool = (name: string) => () =>
|
|
|
205
205
|
const missingToolCommands: ToolCommands["Type"] = {
|
|
206
206
|
ui: missingTool("ui"),
|
|
207
207
|
lsp: missingTool("lsp"),
|
|
208
|
-
loop: missingTool("loop"),
|
|
209
208
|
contextHook: missingTool("context-hook"),
|
|
210
209
|
judge: () =>
|
|
211
210
|
Promise.reject(
|
package/src/wire/ContextLimit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The context-limit decision every driver shares:
|
|
1
|
+
// The context-limit decision every driver shares: `gtd context-hook`
|
|
2
2
|
// and the Claude Code mod both answer a hook event through `contextHook`, so
|
|
3
3
|
// the two cannot drift on thresholds, wording or which tokens count.
|
|
4
4
|
|