@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.
- package/README.md +117 -11
- package/bin/gtd +101 -16
- package/dist/gtd.bundle.mjs +57 -837
- 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 —
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
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:
|
|
118
|
-
#
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
404
|
-
#
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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
|