@pmelab/gtd 7.5.0 → 7.6.1
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/README.md +5 -1
- package/bin/gtd +237 -28
- package/docs/loop.md +29 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -181,7 +181,11 @@ code), waits for you to save and exit, then captures your edit itself and keeps
|
|
|
181
181
|
driving — or halts if you left nothing changed — so you never run
|
|
182
182
|
`gtd step human` by hand. Pass `--no-edit` (or set `GTD_NO_EDIT`) to fall back
|
|
183
183
|
to halting and printing the gate instead, if you'd rather edit and re-launch it
|
|
184
|
-
yourself.
|
|
184
|
+
yourself. Bare `gtd` prints one line per event — colored and emoji on a real
|
|
185
|
+
terminal, plain ASCII under `NO_COLOR` or when piped — and redirects the noisier
|
|
186
|
+
agent/check/step subprocess output to a per-repo/per-worktree log file.
|
|
187
|
+
`gtd log` opens that logfile in your editor. See
|
|
188
|
+
[Driving the loop](docs/loop.md).
|
|
185
189
|
|
|
186
190
|
Before wiring gtd into a repo, note the
|
|
187
191
|
[repository requirements](docs/cli.md#repository-requirements) — most
|
package/bin/gtd
CHANGED
|
@@ -10,6 +10,10 @@ set -euo pipefail
|
|
|
10
10
|
# - `edit [path]` -> handled entirely here in bash, never forwarded to the
|
|
11
11
|
# bundle: opens `${VISUAL:-$EDITOR}` on `path`, or (no `path` given) on the
|
|
12
12
|
# resolved rest's `.file` (or `.` when absent).
|
|
13
|
+
# - `log` -> handled entirely here in bash, never forwarded to the bundle:
|
|
14
|
+
# opens `${VISUAL:-$EDITOR}` on the current repo/worktree's loop logfile
|
|
15
|
+
# (see `resolve_log_path`), refusing rather than opening an empty buffer
|
|
16
|
+
# when no loop has produced one yet.
|
|
13
17
|
# - no arguments, or exactly `loop` with no further arguments -> fall through
|
|
14
18
|
# into the loop body below (the loop protocol comment further down).
|
|
15
19
|
# - `loop` with additional arguments (other than a lone `--no-edit`) ->
|
|
@@ -51,6 +55,157 @@ launch_editor() {
|
|
|
51
55
|
bash -c "$editor \"\$1\"" gtd-edit "$target"
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
# ── Rendering primitives (capability detection, palette/markers, emitters) ──
|
|
59
|
+
# Additive: nothing below calls these yet. `FANCY=1` only when stdout (fd 1)
|
|
60
|
+
# is a real terminal AND `NO_COLOR` is unset — any pipe/redirect/non-tty
|
|
61
|
+
# stdout, or `NO_COLOR` set to any value, gives plain ASCII with no escape
|
|
62
|
+
# codes, per the NO_COLOR convention (https://no-color.org).
|
|
63
|
+
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then FANCY=1; else FANCY=0; fi
|
|
64
|
+
|
|
65
|
+
if [[ "$FANCY" == 1 ]]; then
|
|
66
|
+
C_RESET=$'\e[0m'; C_BOLD=$'\e[1m'; C_DIM=$'\e[2m'
|
|
67
|
+
C_CYAN=$'\e[36m'; C_GREEN=$'\e[32m'; C_YELLOW=$'\e[33m'; C_RED=$'\e[31m'
|
|
68
|
+
M_TRANS="➡️"; M_AGENT="🤖"; M_CHECK="🔧"; M_HUMAN="✍️"
|
|
69
|
+
M_FIX="🔁"; M_COMMIT="✅"; M_SETTLE="🎉"; M_ERROR="❌"
|
|
70
|
+
else
|
|
71
|
+
C_RESET=""; C_BOLD=""; C_DIM=""
|
|
72
|
+
C_CYAN=""; C_GREEN=""; C_YELLOW=""; C_RED=""
|
|
73
|
+
M_TRANS="->"; M_AGENT="[agent]"; M_CHECK="[check]"; M_HUMAN="[you]"
|
|
74
|
+
M_FIX="[fix]"; M_COMMIT="[commit]"; M_SETTLE="[done]"; M_ERROR="[err]"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# emit_transition <from> <to> <files> — a captured state-to-state move. The
|
|
78
|
+
# "→" separator is literal (not FANCY-dependent) — the same separator the
|
|
79
|
+
# engine's commit grammar itself uses, per the package spec.
|
|
80
|
+
emit_transition() {
|
|
81
|
+
local from="$1" to="$2" files="$3"
|
|
82
|
+
if [[ -n "$files" ]]; then
|
|
83
|
+
printf '%s %s%s → %s%s (%s%s%s)\n' \
|
|
84
|
+
"$M_TRANS" "$C_BOLD$C_CYAN" "$from" "$to" "$C_RESET" \
|
|
85
|
+
"$C_DIM" "$files" "$C_RESET"
|
|
86
|
+
else
|
|
87
|
+
printf '%s %s%s → %s%s\n' \
|
|
88
|
+
"$M_TRANS" "$C_BOLD$C_CYAN" "$from" "$to" "$C_RESET"
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# emit_action <marker> <label> — the heartbeat printed before a turn runs.
|
|
93
|
+
emit_action() {
|
|
94
|
+
local marker="$1" label="$2"
|
|
95
|
+
printf '%s %s%s\n' "$marker" "$label" "$C_RESET"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# emit_commit <subject> <files> — a non-transition capture (a bare-subject
|
|
99
|
+
# self-loop, or the squash's agent-authored final commit).
|
|
100
|
+
emit_commit() {
|
|
101
|
+
local subject="$1" files="$2"
|
|
102
|
+
if [[ -n "$files" ]]; then
|
|
103
|
+
printf '%s %s%s%s (%s%s%s)\n' "$M_COMMIT" "$C_GREEN" "$subject" "$C_RESET" \
|
|
104
|
+
"$C_DIM" "$files" "$C_RESET"
|
|
105
|
+
else
|
|
106
|
+
printf '%s %s%s%s\n' "$M_COMMIT" "$C_GREEN" "$subject" "$C_RESET"
|
|
107
|
+
fi
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# emit_gate <state> <message> — the human-instruction line at a message rest.
|
|
111
|
+
emit_gate() {
|
|
112
|
+
local message="$2"
|
|
113
|
+
printf '%s %s%s%s\n' "$M_HUMAN" "$C_BOLD$C_YELLOW" "$message" "$C_RESET"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# emit_settled <state> — the terminal "nothing left to do" signal.
|
|
117
|
+
emit_settled() {
|
|
118
|
+
printf '%s %ssettled — nothing left to do%s\n' "$M_SETTLE" "$C_GREEN" "$C_RESET"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# emit_error <message> — refusals/stalls/failures, to stderr; <message> may be
|
|
122
|
+
# multi-line and is passed through verbatim.
|
|
123
|
+
emit_error() {
|
|
124
|
+
local message="$1"
|
|
125
|
+
printf '%s %s%s%s\n' "$M_ERROR" "$C_RED" "$message" "$C_RESET" >&2
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# emit_log_pointer — the "see the log" line printed right after every
|
|
129
|
+
# emit_error call, to stderr: suppression must never hide a failure, so every
|
|
130
|
+
# error points at where the redirected subprocess output actually went.
|
|
131
|
+
emit_log_pointer() {
|
|
132
|
+
if [[ "$FANCY" == 1 ]]; then
|
|
133
|
+
printf '%s📄 see %s%s\n' "$C_DIM" "$GTD_LOOP_LOG" "$C_RESET" >&2
|
|
134
|
+
else
|
|
135
|
+
printf '%ssee %s%s\n' "$C_DIM" "$GTD_LOOP_LOG" "$C_RESET" >&2
|
|
136
|
+
fi
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# ── Git-derivation helpers (parse_subject, report_commits) ──────────────────
|
|
140
|
+
# Additive: nothing below calls these yet. Replaces today's
|
|
141
|
+
# head_before/head_after string comparisons in a later change.
|
|
142
|
+
|
|
143
|
+
# parse_subject <subject> — sets S_ACTOR / S_FROM / S_TO / S_KIND. Patterns are
|
|
144
|
+
# held in variables (rather than inlined) so bash's [[ =~ ]] doesn't need the
|
|
145
|
+
# literal parens/spaces quoted inline.
|
|
146
|
+
parse_subject() {
|
|
147
|
+
local subject="$1"
|
|
148
|
+
local transition_re='^gtd\(([^)]+)\): (.+) → (.+)$'
|
|
149
|
+
local capture_re='^gtd\(([^)]+)\): (.+)$'
|
|
150
|
+
S_ACTOR="" S_FROM="" S_TO="" S_KIND=""
|
|
151
|
+
if [[ "$subject" =~ $transition_re ]]; then
|
|
152
|
+
S_KIND=transition
|
|
153
|
+
S_ACTOR="${BASH_REMATCH[1]}"
|
|
154
|
+
S_FROM="${BASH_REMATCH[2]}"
|
|
155
|
+
S_TO="${BASH_REMATCH[3]}"
|
|
156
|
+
elif [[ "$subject" =~ $capture_re ]]; then
|
|
157
|
+
S_KIND=capture
|
|
158
|
+
S_ACTOR="${BASH_REMATCH[1]}"
|
|
159
|
+
S_TO="${BASH_REMATCH[2]}"
|
|
160
|
+
else
|
|
161
|
+
S_KIND=squash
|
|
162
|
+
S_TO="$subject"
|
|
163
|
+
fi
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# report_commits <head_before> — walks <head_before>..HEAD in order, emitting
|
|
167
|
+
# a transition/commit line per new commit; returns non-zero (having emitted
|
|
168
|
+
# nothing) when there were no new commits.
|
|
169
|
+
report_commits() {
|
|
170
|
+
local head_before="$1" sha subject files reported=0
|
|
171
|
+
while IFS= read -r sha; do
|
|
172
|
+
[[ -z "$sha" ]] && continue
|
|
173
|
+
reported=1
|
|
174
|
+
subject="$(git log -1 --format=%s "$sha")"
|
|
175
|
+
files="$(git diff-tree --no-commit-id --name-only -r "$sha" | paste -sd, - | sed 's/,/, /g')"
|
|
176
|
+
parse_subject "$subject"
|
|
177
|
+
case "$S_KIND" in
|
|
178
|
+
transition) emit_transition "$S_FROM" "$S_TO" "$files" ;;
|
|
179
|
+
capture | squash) emit_commit "$subject" "$files" ;;
|
|
180
|
+
esac
|
|
181
|
+
done < <(git rev-list --reverse "$head_before"..HEAD)
|
|
182
|
+
[[ "$reported" == 1 ]]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# ── Log file (resolve_log_path, gtd log) ────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
# worktree_git_dir — the git dir of the working tree gtd is operating in,
|
|
188
|
+
# derived purely from the cwd. GIT_DIR/GIT_WORK_TREE are scrubbed for this one
|
|
189
|
+
# call so an inherited GIT_DIR (e.g. leaked from a parent git process, a hook,
|
|
190
|
+
# or another worktree's shell) can never point our per-worktree state at a
|
|
191
|
+
# DIFFERENT worktree — that would let two concurrently looping worktrees
|
|
192
|
+
# collide on one log/memory file. A linked worktree's git-dir is its own
|
|
193
|
+
# private .git/worktrees/<name>, so this is per-repository AND per-worktree.
|
|
194
|
+
worktree_git_dir() {
|
|
195
|
+
env -u GIT_DIR -u GIT_WORK_TREE -u GIT_COMMON_DIR -u GIT_INDEX_FILE \
|
|
196
|
+
git rev-parse --git-dir
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# resolve_log_path — echoes the loop's logfile path: $GTD_LOOP_LOG verbatim if
|
|
200
|
+
# set, else "$(worktree_git_dir)/gtd-loop.log".
|
|
201
|
+
resolve_log_path() {
|
|
202
|
+
if [[ -n "${GTD_LOOP_LOG:-}" ]]; then
|
|
203
|
+
echo "$GTD_LOOP_LOG"
|
|
204
|
+
else
|
|
205
|
+
echo "$(worktree_git_dir)/gtd-loop.log"
|
|
206
|
+
fi
|
|
207
|
+
}
|
|
208
|
+
|
|
54
209
|
# --no-edit / GTD_NO_EDIT govern only the loop's automatic editor launching at
|
|
55
210
|
# human gates (see handle_human_gate further down) — `gtd edit` itself never
|
|
56
211
|
# reads either. `--no-edit` is recognised in exactly the two sanctioned
|
|
@@ -82,6 +237,21 @@ if [[ "${1:-}" == "edit" ]]; then
|
|
|
82
237
|
exit 0
|
|
83
238
|
fi
|
|
84
239
|
|
|
240
|
+
if [[ "${1:-}" == "log" ]]; then
|
|
241
|
+
shift
|
|
242
|
+
if [[ $# -gt 0 ]]; then
|
|
243
|
+
echo "gtd log: takes no arguments" >&2
|
|
244
|
+
exit 2
|
|
245
|
+
fi
|
|
246
|
+
log_path="$(resolve_log_path)"
|
|
247
|
+
if [[ ! -f "$log_path" ]]; then
|
|
248
|
+
echo "gtd log: no loop log yet at $log_path" >&2
|
|
249
|
+
exit 1
|
|
250
|
+
fi
|
|
251
|
+
launch_editor "$log_path"
|
|
252
|
+
exit 0
|
|
253
|
+
fi
|
|
254
|
+
|
|
85
255
|
if [[ $# -gt 0 && "$1" != "loop" ]]; then
|
|
86
256
|
run_gtd "$@"
|
|
87
257
|
exit $?
|
|
@@ -166,8 +336,8 @@ herdr_release() { # hand the pane back to Herdr's normal detection
|
|
|
166
336
|
# opening move (when editing is on) and by every "message" rest the main loop
|
|
167
337
|
# encounters. $1 is the rest's `next --json` blob.
|
|
168
338
|
#
|
|
169
|
-
# Editing OFF ($edit_disabled): today's behavior, unchanged —
|
|
170
|
-
# and exit 0 without touching an editor.
|
|
339
|
+
# Editing OFF ($edit_disabled): today's behavior, unchanged — render the gate
|
|
340
|
+
# via emit_gate and exit 0 without touching an editor.
|
|
171
341
|
#
|
|
172
342
|
# Editing ON (the default): open the editor on the rest's `.file` (or `.` when
|
|
173
343
|
# absent), wait for it to exit, then run `gtd step human`:
|
|
@@ -187,7 +357,7 @@ handle_human_gate() {
|
|
|
187
357
|
if [[ "$edit_disabled" == 1 ]]; then
|
|
188
358
|
herdr_report blocked "$label"
|
|
189
359
|
herdr_notify "gtd needs you — $(basename "$PWD")" "$label"
|
|
190
|
-
|
|
360
|
+
emit_gate "$state" "$label"
|
|
191
361
|
run_gtd next
|
|
192
362
|
exit 0
|
|
193
363
|
fi
|
|
@@ -198,19 +368,22 @@ handle_human_gate() {
|
|
|
198
368
|
|
|
199
369
|
head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
|
|
200
370
|
if ! step_out="$(run_gtd step human 2>&1)"; then
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
371
|
+
emit_error "could not capture your changes at the human gate:
|
|
372
|
+
$step_out
|
|
373
|
+
Fix the reported issue and re-run gtd-loop."
|
|
374
|
+
emit_log_pointer
|
|
204
375
|
exit 1
|
|
205
376
|
fi
|
|
206
377
|
head_after="$(git rev-parse HEAD 2>/dev/null || echo none)"
|
|
207
378
|
if [[ "$head_before" == "$head_after" ]]; then
|
|
208
379
|
herdr_report idle "$label"
|
|
209
380
|
herdr_release
|
|
210
|
-
|
|
381
|
+
emit_action "$M_HUMAN" "done for now (nothing captured)"
|
|
211
382
|
exit 0
|
|
212
383
|
fi
|
|
213
|
-
#
|
|
384
|
+
# A new commit was captured — render it, then return normally so the
|
|
385
|
+
# caller keeps driving.
|
|
386
|
+
report_commits "$head_before"
|
|
214
387
|
}
|
|
215
388
|
|
|
216
389
|
trap 'code=$?; if [ "$code" -ne 0 ]; then
|
|
@@ -222,7 +395,18 @@ prev_state=""
|
|
|
222
395
|
prev_content=""
|
|
223
396
|
# Where the last "prompt" turn's `memory` scope + session id are remembered
|
|
224
397
|
# across iterations and across restarts. In the git dir, never the work tree.
|
|
225
|
-
memory_marker="$(
|
|
398
|
+
memory_marker="$(worktree_git_dir 2>/dev/null || echo .git)/gtd-loop-memory"
|
|
399
|
+
|
|
400
|
+
# Truncated once here (not every iteration), so the file always reflects the
|
|
401
|
+
# current run but survives after the loop exits for a later `gtd log`.
|
|
402
|
+
log_path="$(resolve_log_path)"
|
|
403
|
+
: >"$log_path"
|
|
404
|
+
export GTD_LOOP_LOG="$log_path"
|
|
405
|
+
if [[ "$FANCY" == 1 ]]; then
|
|
406
|
+
printf '%s📄 log: %s (tail -f to follow)%s\n' "$C_DIM" "$log_path" "$C_RESET"
|
|
407
|
+
else
|
|
408
|
+
printf '%slog: %s (tail -f to follow)%s\n' "$C_DIM" "$log_path" "$C_RESET"
|
|
409
|
+
fi
|
|
226
410
|
|
|
227
411
|
# Swappable agent adapter: GTD_LOOP_AGENT_CMD lets any coding agent CLI stand
|
|
228
412
|
# in for the default, receiving the prompt via $GTD_LOOP_PROMPT, the state's
|
|
@@ -249,7 +433,8 @@ run_agent_turn() {
|
|
|
249
433
|
cmd="$cmd"' --dangerously-skip-permissions'
|
|
250
434
|
fi
|
|
251
435
|
GTD_LOOP_PROMPT="$prompt" GTD_LOOP_MODEL="$model" GTD_LOOP_MEMORY="$memory" \
|
|
252
|
-
GTD_LOOP_SESSION_ID="$session_id" GTD_LOOP_MEMORY_RESUME="$do_resume" bash -c "$cmd"
|
|
436
|
+
GTD_LOOP_SESSION_ID="$session_id" GTD_LOOP_MEMORY_RESUME="$do_resume" bash -c "$cmd" \
|
|
437
|
+
>>"${GTD_LOOP_LOG:-/dev/null}" 2>&1
|
|
253
438
|
}
|
|
254
439
|
|
|
255
440
|
# Opening move: capture the human's pending edit so gtd-loop is the ONLY command
|
|
@@ -273,17 +458,19 @@ run_agent_turn() {
|
|
|
273
458
|
# step silently, refusal surfaced and halts, a clean/committed gate just falls
|
|
274
459
|
# into the loop below.
|
|
275
460
|
if ! peek_json="$(run_gtd next --json 2>&1)"; then
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
461
|
+
emit_error "could not determine the next step:
|
|
462
|
+
$peek_json
|
|
463
|
+
Run \`gtd status\` to inspect the repo."
|
|
464
|
+
emit_log_pointer
|
|
279
465
|
exit 1
|
|
280
466
|
fi
|
|
281
467
|
if [[ "$(jq -r .kind <<<"$peek_json")" == "message" ]]; then
|
|
282
468
|
if [[ "$edit_disabled" == 1 ]]; then
|
|
283
469
|
if ! step_out="$(run_gtd step human 2>&1)"; then
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
470
|
+
emit_error "could not capture your changes at the human gate:
|
|
471
|
+
$step_out
|
|
472
|
+
Fix the reported issue and re-run gtd-loop."
|
|
473
|
+
emit_log_pointer
|
|
287
474
|
exit 1
|
|
288
475
|
fi
|
|
289
476
|
else
|
|
@@ -318,18 +505,24 @@ while true; do
|
|
|
318
505
|
herdr_report working "$label"
|
|
319
506
|
|
|
320
507
|
if [[ "$kind" == "script" ]]; then
|
|
508
|
+
emit_action "$M_CHECK" "$label"
|
|
321
509
|
head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
|
|
322
510
|
# Execute the emitted wrapper verbatim; its exit code is deliberately
|
|
323
511
|
# ignored (`|| true` under `set -e`) — the script encodes its outcome in
|
|
324
512
|
# the tree, and `gtd step` captures whatever it left there via the state's
|
|
325
|
-
# own `on` patterns.
|
|
326
|
-
bash -c "$content" || true
|
|
327
|
-
run_gtd step "$actor"
|
|
328
|
-
|
|
329
|
-
|
|
513
|
+
# own `on` patterns. Its own output goes to the log, not the terminal.
|
|
514
|
+
bash -c "$content" >>"$GTD_LOOP_LOG" 2>&1 || true
|
|
515
|
+
if ! step_out="$(run_gtd step "$actor" 2>&1)"; then
|
|
516
|
+
printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
|
|
517
|
+
emit_error "$step_out"
|
|
518
|
+
emit_log_pointer
|
|
519
|
+
exit 1
|
|
520
|
+
fi
|
|
521
|
+
printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
|
|
522
|
+
if ! report_commits "$head_before"; then
|
|
330
523
|
herdr_report idle "$label"
|
|
331
524
|
herdr_release
|
|
332
|
-
|
|
525
|
+
emit_settled "$state"
|
|
333
526
|
exit 0
|
|
334
527
|
fi
|
|
335
528
|
prev_state=""
|
|
@@ -341,8 +534,10 @@ while true; do
|
|
|
341
534
|
# agent-owned state and content repeating means the last dispatch made no
|
|
342
535
|
# progress; stop rather than spin on it.
|
|
343
536
|
if [[ "$state" == "$prev_state" && "$content" == "$prev_content" ]]; then
|
|
344
|
-
|
|
345
|
-
|
|
537
|
+
next_out="$(run_gtd next 2>&1)" || true
|
|
538
|
+
emit_error "no progress at '$state' — the agent's last turn changed nothing. Stopping to avoid spinning.
|
|
539
|
+
$next_out"
|
|
540
|
+
emit_log_pointer
|
|
346
541
|
exit 1
|
|
347
542
|
fi
|
|
348
543
|
prev_state="$state"
|
|
@@ -382,23 +577,37 @@ while true; do
|
|
|
382
577
|
# cap) before stepping — the plain-`gtd next` self-validation instruction's
|
|
383
578
|
# driver-side equivalent, so the next rest is only ever handed a well-formed
|
|
384
579
|
# file. gtd itself never validates at step time — this is the loop's job.
|
|
580
|
+
emit_action "$M_AGENT" "$label"
|
|
581
|
+
head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
|
|
385
582
|
run_agent_turn "$content" "$resume"
|
|
386
583
|
|
|
387
584
|
fix_attempt=0
|
|
388
585
|
while ! validate_out="$(run_gtd validate 2>&1)"; do
|
|
389
586
|
fix_attempt=$((fix_attempt + 1))
|
|
390
587
|
if ((fix_attempt > 3)); then
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
588
|
+
emit_error "'$state' still fails \`gtd validate\` after 3 fix attempts:
|
|
589
|
+
$validate_out
|
|
590
|
+
Stopping rather than stepping with a malformed steering file."
|
|
591
|
+
emit_log_pointer
|
|
394
592
|
exit 1
|
|
395
593
|
fi
|
|
396
594
|
# Resume the same session when there is one (the agent just wrote the file);
|
|
397
595
|
# otherwise a fresh invocation still gets the findings via the prompt.
|
|
398
596
|
fix_resume=0
|
|
399
597
|
[[ -n "$session_id" ]] && fix_resume=1
|
|
598
|
+
emit_action "$M_FIX" "attempt $fix_attempt"
|
|
400
599
|
run_agent_turn "$content"$'\n\n'"Your last turn does not pass \`gtd validate\`. Fix these format violations, then finish:"$'\n'"$validate_out" "$fix_resume"
|
|
401
600
|
done
|
|
402
601
|
|
|
403
|
-
run_gtd step "$actor"
|
|
602
|
+
if ! step_out="$(run_gtd step "$actor" 2>&1)"; then
|
|
603
|
+
printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
|
|
604
|
+
emit_error "$step_out"
|
|
605
|
+
emit_log_pointer
|
|
606
|
+
exit 1
|
|
607
|
+
fi
|
|
608
|
+
printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
|
|
609
|
+
# A no-op step (nothing matched a declared pattern on a clean tree) reports
|
|
610
|
+
# zero commits here — report_commits returns non-zero, and that's fine: the
|
|
611
|
+
# NEXT iteration's stall check is what catches unchanged state/content.
|
|
612
|
+
report_commits "$head_before" || true
|
|
404
613
|
done
|
package/docs/loop.md
CHANGED
|
@@ -137,7 +137,7 @@ while true; do
|
|
|
137
137
|
content="$(jq -r .content <<<"$next_json")"
|
|
138
138
|
|
|
139
139
|
if [[ "$kind" == "message" ]]; then
|
|
140
|
-
echo "
|
|
140
|
+
echo "your turn ($state)" # bin/gtd renders this via emit_gate instead
|
|
141
141
|
gtd next
|
|
142
142
|
exit 0
|
|
143
143
|
fi
|
|
@@ -148,7 +148,7 @@ while true; do
|
|
|
148
148
|
gtd step "$actor" >/dev/null # capture whatever it left in the tree
|
|
149
149
|
head_after="$(git rev-parse HEAD 2>/dev/null || echo none)"
|
|
150
150
|
if [[ "$head_before" == "$head_after" ]]; then
|
|
151
|
-
echo "
|
|
151
|
+
echo "settled ($state: check passed, nothing to do)" # bin/gtd renders this via emit_settled instead
|
|
152
152
|
exit 0
|
|
153
153
|
fi
|
|
154
154
|
continue
|
|
@@ -164,6 +164,33 @@ The driver — not the prompt text — owns ending the agent's turn
|
|
|
164
164
|
(`gtd step "$actor"` right after the agent acts): every default-workflow agent
|
|
165
165
|
prompt says explicitly not to run `gtd step agent` itself.
|
|
166
166
|
|
|
167
|
+
`bin/gtd log` opens the current repo/worktree's loop logfile —
|
|
168
|
+
`"$(worktree_git_dir)/gtd-loop.log"` by default, or `$GTD_LOOP_LOG` verbatim
|
|
169
|
+
when set — in `${VISUAL:-$EDITOR}`, the same editor resolution `bin/gtd edit`
|
|
170
|
+
uses. It takes no arguments and errors out (rather than opening an empty buffer)
|
|
171
|
+
if no loop has produced a log yet. `worktree_git_dir` derives the git dir from
|
|
172
|
+
the cwd with `GIT_DIR`/`GIT_WORK_TREE` scrubbed, so an inherited `GIT_DIR`
|
|
173
|
+
(leaked from a parent git process, a hook, or another worktree's shell) can
|
|
174
|
+
never key the log — or the loop's memory marker — to a different worktree; two
|
|
175
|
+
worktrees looping at once each keep their own log.
|
|
176
|
+
|
|
177
|
+
## Rendered output and logging
|
|
178
|
+
|
|
179
|
+
`bin/gtd` detects its output capability once at startup (`FANCY`, true when
|
|
180
|
+
stdout is a real terminal AND `NO_COLOR` is unset): on a real terminal it prints
|
|
181
|
+
one colored/emoji line per event, and under `NO_COLOR` or a piped/redirected
|
|
182
|
+
stdout it prints the same events as plain ASCII with no escape codes, per the
|
|
183
|
+
[NO_COLOR convention](https://no-color.org). Normal events (a state transition,
|
|
184
|
+
a captured commit, an agent/check turn starting, settling at idle) render to
|
|
185
|
+
stdout; refusals, stalls, and validate-cap halts render to stderr, each followed
|
|
186
|
+
by a `📄 see <log>` pointer (plain: `see <log>`).
|
|
187
|
+
|
|
188
|
+
The three previously terminal-visible subprocess streams — the agent turn's own
|
|
189
|
+
output, the check script's own output, and `gtd step`'s own output — are no
|
|
190
|
+
longer printed directly: they're appended to the per-repo/per-worktree log file
|
|
191
|
+
(truncated once at the start of the run) instead, and surfaced with
|
|
192
|
+
`bin/gtd log` above.
|
|
193
|
+
|
|
167
194
|
## Agent memory scope
|
|
168
195
|
|
|
169
196
|
A `"prompt"` beat may carry an optional `memory` key — an opaque scope label the
|