@pmelab/gtd 8.0.0 → 8.2.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/bin/gtd CHANGED
@@ -44,12 +44,12 @@ if [[ "$FANCY" == 1 ]]; then
44
44
  C_RESET=$'\e[0m'; C_BOLD=$'\e[1m'; C_DIM=$'\e[2m'
45
45
  C_CYAN=$'\e[36m'; C_GREEN=$'\e[32m'; C_YELLOW=$'\e[33m'; C_RED=$'\e[31m'
46
46
  M_TRANS="➡️"; M_AGENT="🤖"; M_CHECK="🔧"; M_HUMAN="✍️"
47
- M_FIX="🔁"; M_COMMIT="✅"; M_SETTLE="🎉"; M_ERROR="❌"; M_ELLIPSIS="…"
47
+ M_FIX="🔁"; M_COMMIT="✅"; M_SETTLE="🎉"; M_ERROR="❌"; M_WARN="⚠️"; M_ELLIPSIS="…"
48
48
  else
49
49
  C_RESET=""; C_BOLD=""; C_DIM=""
50
50
  C_CYAN=""; C_GREEN=""; C_YELLOW=""; C_RED=""
51
51
  M_TRANS="->"; M_AGENT="[agent]"; M_CHECK="[check]"; M_HUMAN="[you]"
52
- M_FIX="[fix]"; M_COMMIT="[commit]"; M_SETTLE="[done]"; M_ERROR="[err]"; M_ELLIPSIS="..."
52
+ M_FIX="[fix]"; M_COMMIT="[commit]"; M_SETTLE="[done]"; M_ERROR="[err]"; M_WARN="[warn]"; M_ELLIPSIS="..."
53
53
  fi
54
54
 
55
55
  # emit_file_rows <files> — render a commit's changed files as dim, indented
@@ -114,8 +114,10 @@ emit_error() {
114
114
  }
115
115
 
116
116
  # emit_log_pointer — the "see the log" line printed right after every
117
- # emit_error call, to stderr: suppression must never hide a failure, so every
118
- # error points at where the redirected subprocess output actually went.
117
+ # emit_error call, to stderr: it names the log as the OVERFLOW channel — the
118
+ # failing execution's own output is already replayed inline (see emit_block/
119
+ # emit_output_since below); the log is where the rest of it, and the full
120
+ # run's other output, lives.
119
121
  emit_log_pointer() {
120
122
  if [[ "$FANCY" == 1 ]]; then
121
123
  printf '%s📄 see %s%s\n' "$C_DIM" "$GTD_LOOP_LOG" "$C_RESET" >&2
@@ -124,6 +126,58 @@ emit_log_pointer() {
124
126
  fi
125
127
  }
126
128
 
129
+ # Suppressed subprocess output goes to $GTD_LOOP_LOG in full, but a FAILING
130
+ # execution also replays inline: mark the log's size before the call, print
131
+ # what was appended since. Bounded to the last $ERR_TAIL_LINES lines — the log
132
+ # holds the rest, and the overflow row says where.
133
+ ERR_TAIL_LINES=20
134
+
135
+ # log_offset — the log's current size in bytes (0 when it doesn't exist yet).
136
+ log_offset() {
137
+ local n=0
138
+ if [[ -n "${GTD_LOOP_LOG:-}" && -f "$GTD_LOOP_LOG" ]]; then n="$(wc -c <"$GTD_LOOP_LOG")"; fi
139
+ printf '%s' "${n// /}"
140
+ }
141
+
142
+ # emit_block <text> — replay captured subprocess output to stderr, dim and
143
+ # indented like emit_file_rows, capped at the LAST $ERR_TAIL_LINES lines with
144
+ # an overflow row naming the log. Empty <text> prints nothing. CSI escapes are
145
+ # stripped under FANCY=0 (the plain-ASCII promise), using a sed form (\e, not
146
+ # \x1b) that both BSD and GNU sed accept.
147
+ emit_block() {
148
+ local text="$1" total overflow
149
+ [[ -z "$text" ]] && return 0
150
+ if [[ "$FANCY" == 0 ]]; then
151
+ text="$(sed $'s/\e\\[[0-9;]*[a-zA-Z]//g' <<<"$text")"
152
+ fi
153
+ total="$(wc -l <<<"$text")"
154
+ total="${total// /}"
155
+ if ((total > ERR_TAIL_LINES)); then
156
+ overflow=$((total - ERR_TAIL_LINES))
157
+ printf ' %s%s (%d earlier lines in %s)%s\n' "$C_DIM" "$M_ELLIPSIS" "$overflow" "$GTD_LOOP_LOG" "$C_RESET" >&2
158
+ text="$(tail -n "$ERR_TAIL_LINES" <<<"$text")"
159
+ fi
160
+ while IFS= read -r line; do
161
+ printf ' %s%s%s\n' "$C_DIM" "$line" "$C_RESET" >&2
162
+ done <<<"$text"
163
+ }
164
+
165
+ # emit_output_since <offset> — emit_block over the log bytes appended since
166
+ # <offset>.
167
+ emit_output_since() {
168
+ local offset="$1" bytes=""
169
+ if [[ -n "${GTD_LOOP_LOG:-}" && -f "$GTD_LOOP_LOG" ]]; then
170
+ bytes="$(tail -c "+$((offset + 1))" "$GTD_LOOP_LOG" 2>/dev/null || true)"
171
+ fi
172
+ emit_block "$bytes"
173
+ }
174
+
175
+ # emit_warn <message> — a surfaced but NON-fatal failure (a check script that
176
+ # exited non-zero).
177
+ emit_warn() {
178
+ printf '%s %s%s%s\n' "$M_WARN" "$C_YELLOW" "$1" "$C_RESET" >&2
179
+ }
180
+
127
181
  # ── Git-derivation helpers (parse_subject, report_commits) ──────────────────
128
182
 
129
183
  # parse_subject <subject> — sets S_ACTOR / S_FROM / S_TO / S_KIND. Patterns are
@@ -247,7 +301,7 @@ fi
247
301
  # otherwise ($# -eq 0, or exactly "loop" with no further args, --once already
248
302
  # stripped above): fall through into the loop body below.
249
303
 
250
- # Drives the v3 pattern-machine loop (see docs/loop.md): each iteration asks
304
+ # Drives the v3 pattern-machine loop: each iteration asks
251
305
  # `gtd next --json` who's up and what they should do, dispatching on `kind`:
252
306
  # - "message" (a human rest) -> the non-autonomous halt: print the gate and
253
307
  # exit 0. Acting on it (opening an editor, notifying, whatever) is an
@@ -264,8 +318,7 @@ fi
264
318
  # adapter invoked at a "prompt" rest; the default adapter appends `--model
265
319
  # "$GTD_LOOP_MODEL"` only when it's non-empty.
266
320
  #
267
- # `.memory` (optional, opaque — see skills/loop/SKILL.md "Agent memory scope")
268
- # is the agent-memory scope label. gtd never interprets it; the driver does:
321
+ # `.memory` (optional, opaque) is the agent-memory scope label. gtd never interprets it; the driver does:
269
322
  # consecutive "prompt" turns emitting the SAME label continue ONE agent
270
323
  # session (so the agent still remembers its previous turn), and a changed
271
324
  # label — or the first turn, or an unset label — starts fresh. That is what
@@ -376,9 +429,10 @@ fi
376
429
 
377
430
  while true; do
378
431
  if ! next_json="$(run_gtd next --json 2>&1)"; then
379
- echo "gtd-loop: could not determine the next step:" >&2
380
- echo "$next_json" >&2
381
- echo "Run \`gtd status\` to inspect the repo." >&2
432
+ emit_error "could not determine the next step:
433
+ $next_json
434
+ Run \`gtd status\` to inspect the repo."
435
+ emit_log_pointer
382
436
  exit 1
383
437
  fi
384
438
 
@@ -395,17 +449,29 @@ while true; do
395
449
  # (or an outer wrapper) edits and re-launches; the opening move above
396
450
  # captures what they left.
397
451
  emit_gate "$state" "$label"
398
- run_gtd next
452
+ if ! gate_out="$(run_gtd next 2>&1)"; then
453
+ emit_error "$gate_out"
454
+ emit_log_pointer
455
+ exit 1
456
+ fi
457
+ printf '%s\n' "$gate_out"
399
458
  exit 0
400
459
  fi
401
460
 
402
461
  if [[ "$kind" == "script" ]]; then
403
462
  emit_action "$M_CHECK" "$label"
404
- # Execute the emitted wrapper verbatim; its exit code is deliberately
405
- # ignored (`|| true` under `set -e`) — the script encodes its outcome in
406
- # the tree, and `gtd step` captures whatever it left there via the state's
407
- # own `on` patterns. Its own output goes to the log, not the terminal.
408
- bash -c "$content" >>"$GTD_LOOP_LOG" 2>&1 || true
463
+ # Execute the emitted wrapper verbatim; its exit code decides nothing
464
+ # (the script encodes its outcome in the tree, and `gtd step` captures
465
+ # whatever it left there via the state's own `on` patterns) but a non-zero
466
+ # exit is surfaced as a warning — its own output goes to the log, and to
467
+ # the terminal too when it exits non-zero.
468
+ script_offset="$(log_offset)"
469
+ script_status=0
470
+ bash -c "$content" >>"$GTD_LOOP_LOG" 2>&1 || script_status=$?
471
+ if ((script_status != 0)); then
472
+ emit_warn "the check script at '$state' exited $script_status — continuing; the outcome is whatever it left in the tree:"
473
+ emit_output_since "$script_offset"
474
+ fi
409
475
  if ! step_out="$(run_gtd step "$actor" 2>&1)"; then
410
476
  printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
411
477
  emit_error "$step_out"
@@ -425,7 +491,7 @@ while true; do
425
491
  continue
426
492
  fi
427
493
 
428
- # kind == "prompt": stall detection (skills/loop/SKILL.md) — the same
494
+ # kind == "prompt": stall detection — the same
429
495
  # agent-owned state and content repeating, WITH NO COMMIT authored since the
430
496
  # last time we saw this beat, means the last dispatch made no progress; stop
431
497
  # rather than spin on it. The HEAD check is what distinguishes a genuine
@@ -479,24 +545,42 @@ $next_out"
479
545
  # driver-side equivalent, so the next rest is only ever handed a well-formed
480
546
  # file. gtd itself never validates at step time — this is the loop's job.
481
547
  emit_action "$M_AGENT" "$label"
482
- run_agent_turn "$content" "$resume"
548
+ turn_offset="$(log_offset)"
549
+ agent_status=0
550
+ run_agent_turn "$content" "$resume" || agent_status=$?
551
+ if ((agent_status != 0)); then
552
+ emit_error "the agent turn at '$state' failed (exit $agent_status):"
553
+ emit_output_since "$turn_offset"
554
+ emit_log_pointer
555
+ exit 1
556
+ fi
483
557
 
484
558
  fix_attempt=0
485
559
  while ! validate_out="$(run_gtd validate 2>&1)"; do
486
560
  fix_attempt=$((fix_attempt + 1))
487
561
  if ((fix_attempt > 3)); then
562
+ printf '%s\n' "$validate_out" >>"$GTD_LOOP_LOG"
488
563
  emit_error "'$state' still fails \`gtd validate\` after 3 fix attempts:
489
564
  $validate_out
490
565
  Stopping rather than stepping with a malformed steering file."
491
566
  emit_log_pointer
492
567
  exit 1
493
568
  fi
569
+ printf '%s\n' "$validate_out" >>"$GTD_LOOP_LOG"
570
+ emit_action "$M_FIX" "attempt $fix_attempt"
494
571
  # Resume the same session when there is one (the agent just wrote the file);
495
572
  # otherwise a fresh invocation still gets the findings via the prompt.
496
573
  fix_resume=0
497
574
  [[ -n "$session_id" ]] && fix_resume=1
498
- emit_action "$M_FIX" "attempt $fix_attempt"
499
- 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"
575
+ turn_offset="$(log_offset)"
576
+ agent_status=0
577
+ 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" || agent_status=$?
578
+ if ((agent_status != 0)); then
579
+ emit_error "the agent's fix attempt $fix_attempt at '$state' failed (exit $agent_status):"
580
+ emit_output_since "$turn_offset"
581
+ emit_log_pointer
582
+ exit 1
583
+ fi
500
584
  done
501
585
 
502
586
  if ! step_out="$(run_gtd step "$actor" 2>&1)"; then