@pmelab/gtd 8.1.0 → 8.2.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.
Files changed (4) hide show
  1. package/README.md +117 -11
  2. package/bin/gtd +101 -16
  3. package/dist/gtd.bundle.mjs +57 -837
  4. package/package.json +1 -3
package/README.md CHANGED
@@ -115,10 +115,12 @@ steering-file entries are chosen by which file you create:
115
115
  per-package **agentic review** that verifies the package against its spec.
116
116
 
117
117
  Both flows converge on the same tail: an agent hands you a `.gtd/REVIEW.md`
118
- checkbox review of the diff — tick a box as you review each hunk (ticking just
119
- records "I read this"), and leave a **comment** to request changes: a note on a
120
- line, an inline `// TODO`-style comment in the code, or a direct code edit. Any
121
- comment sends a build + re-review round — an agent first turns your comments
118
+ checkbox review of the diff — the prompt never inlines the diff itself; it names
119
+ the commit the changes are based at and the agent runs `git diff` to read the
120
+ range before writing the review. Tick a box as you review each hunk (ticking
121
+ just records "I read this"), and leave a **comment** to request changes: a note
122
+ on a line, an inline `// TODO`-style comment in the code, or a direct code edit.
123
+ Any comment sends a build + re-review round — an agent first turns your comments
122
124
  into an explicit instruction list, then a build turn implements it (a re-review
123
125
  then covers only the follow-through, and a hand-edit is treated as your own fix
124
126
  the agent completes without reverting your lines; a comment can't be silently
@@ -285,20 +287,106 @@ re-launching it — its opening move captures whatever you left, so you never ru
285
287
 
286
288
  Anything richer at that boundary — opening your editor, desktop notifications,
287
289
  terminal-multiplexer status — is the job of an outer wrapper around `gtd`, not
288
- of the loop itself. Pass `--once` to restrict a run to exactly one beat — one
289
- human-gate capture, one check, or one agent turn — instead of driving all the
290
- way to idle.
290
+ of the loop itself; see
291
+ [Terminal-multiplexer status: a herdr wrapper](#terminal-multiplexer-status-a-herdr-wrapper)
292
+ below for a worked example. Pass `--once` to restrict a run to exactly one beat
293
+ — one human-gate capture, one check, or one agent turn — instead of driving all
294
+ the way to idle.
291
295
 
292
296
  Bare `gtd` prints one line per event — colored and emoji on a real terminal,
293
297
  plain ASCII under `NO_COLOR` or when piped — and redirects the noisier
294
298
  agent/check/step subprocess output to a per-repo/per-worktree log file (its path
295
- is the run's first output line, ready to `tail -f`).
299
+ is the run's first output line, ready to `tail -f`). Any execution that FAILS
300
+ also replays its last 20 lines of output inline on stderr, on top of the log
301
+ still holding the complete record: an agent turn that fails stops the loop (it
302
+ would fail identically next lap), while a check script that exits non-zero is
303
+ reported as a warning and the loop carries on, because the outcome lives in the
304
+ tree, not in the script's exit code.
296
305
 
297
306
  A workflow state can declare an optional `label:` — a human-readable display
298
307
  name surfaced in `gtd next --json`/`gtd status`. The driver uses it for its
299
308
  per-beat progress lines; an outer wrapper (a terminal multiplexer, a notifier)
300
309
  can use it the same way.
301
310
 
311
+ ### Terminal-multiplexer status: a herdr wrapper
312
+
313
+ [herdr](https://herdr.dev) shows a per-pane status in its sidebar. This wrapper
314
+ reports `gtd`'s own lifecycle to that status without any herdr-specific
315
+ knowledge in `gtd` itself: `working` while the loop drives, `blocked` when it
316
+ comes to rest on a human (a gate, a stall, a non-zero exit), and `idle`
317
+ (rendered as "done" once the pane's tab is unfocused) when a run ends without
318
+ anything owed. It needs nothing from `gtd` beyond `gtd next --json`'s `.actor`
319
+ field, which the loop's driver already reads at every beat.
320
+
321
+ Save this as `~/.local/bin/gtdh`, `chmod +x` it, and run `gtdh` in place of
322
+ `gtd` — it's a plain bash file, so it works from fish or any other shell, and it
323
+ forwards every argument (`gtdh --once`, `gtdh status`, ...).
324
+
325
+ ```bash
326
+ #!/usr/bin/env bash
327
+ # Report gtd's own lifecycle to the herdr pane it runs in: working while the
328
+ # loop drives, blocked when it rests on you, idle (shown as "done") otherwise.
329
+ # Needs nothing from gtd but `gtd next --json`. Outside herdr it is a plain
330
+ # passthrough. Requires `jq` — so does gtd's own loop driver.
331
+ set -uo pipefail
332
+
333
+ pane="${HERDR_PANE_ID:-}"
334
+
335
+ # `--agent gtd` is deliberate: herdr only lets screen detection (or a visible
336
+ # approval prompt) override a reported state when the reported label names a
337
+ # KNOWN agent. "gtd" names none, so the loop's `claude -p` turns can never move
338
+ # this pane's status.
339
+ report() { # report <state>
340
+ [ -n "$pane" ] || return 0
341
+ herdr pane report-agent "$pane" \
342
+ --source custom:gtd --agent gtd --state "$1" >/dev/null 2>&1 || true
343
+ }
344
+
345
+ report working
346
+ trap 'report blocked; exit 130' INT TERM
347
+
348
+ # HERDR_PANE_ID is unset for the loop: herdr's Claude Code hook needs it, and
349
+ # without it no `claude -p` turn reports a claude SESSION identity for this
350
+ # pane. That is load-bearing, not cosmetic — a pane that owns one rejects every
351
+ # later state report, so the report below would be dropped.
352
+ env -u HERDR_PANE_ID gtd "$@"
353
+ rc=$?
354
+
355
+ # Whose turn is it now? `gtd next --json` is mutation-free. A human actor means
356
+ # gtd is waiting on you; anything else means the run ended with nothing owed.
357
+ actor="$(gtd next --json 2>/dev/null | jq -r '.actor // ""' 2>/dev/null || true)"
358
+
359
+ if [ "$rc" -ne 0 ] || [ -z "$actor" ] || [ "$actor" = human ]; then
360
+ report blocked
361
+ else
362
+ report idle
363
+ fi
364
+
365
+ exit $rc
366
+ ```
367
+
368
+ A few things to know before relying on it:
369
+
370
+ - **Give `gtd` its own pane.** Run an interactive `claude` (or any agent whose
371
+ herdr integration reports session identity) in the same pane and that pane
372
+ owns a `herdr:claude` session until the process exits; while it does, the
373
+ wrapper's reports are silently ignored and the sidebar stops tracking `gtd`.
374
+ - **`blocked` covers the resting `idle` state too** — gtd's own `idle` is a
375
+ human gate (it waits for you to write a steering file), so a finished cycle
376
+ reads as "your turn", which is what it is.
377
+ - The status **persists after the wrapper exits** — that's the point: the
378
+ sidebar keeps showing which worktree is waiting on you. Hand the pane back to
379
+ ordinary detection with
380
+ `herdr pane release-agent "$HERDR_PANE_ID" --source custom:gtd --agent gtd`.
381
+ - A non-zero exit (a stall, a refusal, an error) reports `blocked`: herdr has no
382
+ failure state, and those all need a human.
383
+ - Ctrl-C reports `blocked` rather than leaving a stale `working`.
384
+
385
+ Optionally,
386
+ `herdr pane report-metadata "$HERDR_PANE_ID" --source custom:gtd-display --token summary=<text>`
387
+ sets a value renderable as `$summary` in an Agent sidebar row, if you want more
388
+ than the state itself.
389
+
302
390
  ## Configuration
303
391
 
304
392
  gtd reads an optional `.gtdrc` config file via
@@ -384,6 +472,22 @@ workflow:
384
472
  mode: <modeName> # optional, requires "file" — a built-in (qa/review/prose) or a `modes:` entry
385
473
  ```
386
474
 
475
+ Besides `it.vars` (below), a `script`/`prompt`/`message`/`commit` template sees:
476
+
477
+ - **`it.startCommit`** — the process's diff base (the commit the current cycle
478
+ started from, or a `gtd review <commitish>` entry's resolved base).
479
+ - **`it.reviewBase`** — the previous review round's boundary, falling back to
480
+ `it.startCommit` on a first review.
481
+ - **`it.retainedBase`** — the process's trace/retry boundary, what a squash
482
+ actually keeps (never moved by a `gtd review` entry).
483
+ - **`it.currentCommit`** / **`it.previousCommit`** — HEAD's hash and its parent,
484
+ at render time.
485
+
486
+ A template never sees rendered diff CONTENT — no field carries a diff. It names
487
+ a base and leaves the agent to run `git diff <base>` itself; this keeps every
488
+ render cheap (no diff computed on `gtd next`/`gtd status`/`gtd lsp`) and the
489
+ prompt small and cacheable.
490
+
387
491
  Authoring or editing a workflow with a coding agent? `skills/authoring/SKILL.md`
388
492
  is the agent-facing contract for producing a valid `workflow:` — the state
389
493
  model, pattern grammar, load-time rules, and how to verify a change compiles.
@@ -483,9 +587,11 @@ compiler's job at load time.
483
587
  per `review`-mode chunk that still has an unchecked hunk (an outline of the
484
588
  packages left to review) plus check/uncheck actions over those chunks,
485
589
  go-to-definition from a `review`-mode hunk line into the file it points at (at
486
- its `#line`), symbols over a `qa`-mode file's open questions, diagnostics for
487
- both (live as you edit), and a `gtd.openSteeringFile` command that jumps to the
488
- current state's steering file.
590
+ its `#line`), symbols over a `qa`-mode file's open questions plus "pick this
591
+ option"/"uncheck this option" code actions on each option — offered anywhere on
592
+ the option's list item, including any wrapped continuation lines, not just its
593
+ own `- [ ]` line — diagnostics for both (live as you edit), and a
594
+ `gtd.openSteeringFile` command that jumps to the current state's steering file.
489
595
 
490
596
  It is config-driven via each state's `file:`/`mode:`, and falls back to basename
491
597
  dispatch (`REVIEW.md` → `review`) with no config in sight. `qa` and `review` are
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
@@ -375,9 +429,10 @@ fi
375
429
 
376
430
  while true; do
377
431
  if ! next_json="$(run_gtd next --json 2>&1)"; then
378
- echo "gtd-loop: could not determine the next step:" >&2
379
- echo "$next_json" >&2
380
- 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
381
436
  exit 1
382
437
  fi
383
438
 
@@ -394,17 +449,29 @@ while true; do
394
449
  # (or an outer wrapper) edits and re-launches; the opening move above
395
450
  # captures what they left.
396
451
  emit_gate "$state" "$label"
397
- 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"
398
458
  exit 0
399
459
  fi
400
460
 
401
461
  if [[ "$kind" == "script" ]]; then
402
462
  emit_action "$M_CHECK" "$label"
403
- # Execute the emitted wrapper verbatim; its exit code is deliberately
404
- # ignored (`|| true` under `set -e`) — the script encodes its outcome in
405
- # the tree, and `gtd step` captures whatever it left there via the state's
406
- # own `on` patterns. Its own output goes to the log, not the terminal.
407
- 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
408
475
  if ! step_out="$(run_gtd step "$actor" 2>&1)"; then
409
476
  printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
410
477
  emit_error "$step_out"
@@ -478,24 +545,42 @@ $next_out"
478
545
  # driver-side equivalent, so the next rest is only ever handed a well-formed
479
546
  # file. gtd itself never validates at step time — this is the loop's job.
480
547
  emit_action "$M_AGENT" "$label"
481
- 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
482
557
 
483
558
  fix_attempt=0
484
559
  while ! validate_out="$(run_gtd validate 2>&1)"; do
485
560
  fix_attempt=$((fix_attempt + 1))
486
561
  if ((fix_attempt > 3)); then
562
+ printf '%s\n' "$validate_out" >>"$GTD_LOOP_LOG"
487
563
  emit_error "'$state' still fails \`gtd validate\` after 3 fix attempts:
488
564
  $validate_out
489
565
  Stopping rather than stepping with a malformed steering file."
490
566
  emit_log_pointer
491
567
  exit 1
492
568
  fi
569
+ printf '%s\n' "$validate_out" >>"$GTD_LOOP_LOG"
570
+ emit_action "$M_FIX" "attempt $fix_attempt"
493
571
  # Resume the same session when there is one (the agent just wrote the file);
494
572
  # otherwise a fresh invocation still gets the findings via the prompt.
495
573
  fix_resume=0
496
574
  [[ -n "$session_id" ]] && fix_resume=1
497
- emit_action "$M_FIX" "attempt $fix_attempt"
498
- 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
499
584
  done
500
585
 
501
586
  if ! step_out="$(run_gtd step "$actor" 2>&1)"; then