@pmelab/gtd 7.11.1 → 8.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/README.md CHANGED
@@ -107,16 +107,10 @@ handful of commands drive it:
107
107
  and a "Current state" panel showing where the active process rests, its
108
108
  pending changes, and which action leads where. The panel and diagram highlight
109
109
  refresh live (~every 3s) while the page is open, so advancing the process
110
- elsewhere shows up without a manual refresh.
111
- - **`gtd edit [path]`** — open `<path>` (or, with no argument, the current
112
- resting state's steering file, or the repo dir) in `${VISUAL:-$EDITOR}`,
113
- blocking until it exits. Low-level plumbing — for "force an edit at the
114
- current human gate, then keep driving", pass `--edit` to the loop instead (see
115
- below).
116
-
117
- `gtd version` (or `gtd --version`/`-v`) prints the installed version and exits;
118
- `gtd help` (or `gtd --help`/`-h`) prints the command list. Both short-circuit
119
- before any repo work, so they run anywhere.
110
+ elsewhere shows up without a manual refresh. `gtd version` (or
111
+ `gtd --version`/`-v`) prints the installed version and exits; `gtd help` (or
112
+ `gtd --help`/`-h`) prints the command list. Both short-circuit before any repo
113
+ work, so they run anywhere.
120
114
 
121
115
  The loop is one beat, repeated: run `gtd next --json` and dispatch on `kind` —
122
116
  `"message"` means it's a human's move (stop and hand off); `"script"` means the
@@ -183,23 +177,20 @@ keep their own scratch/bookkeeping, independent of the per-file vars — relocat
183
177
  one steering file without touching it.
184
178
 
185
179
  Bare `gtd` (or `gtd loop`) is a ready-to-run driver for the whole protocol —
186
- point it at a repo and it runs the loop until it's your turn. It is the only
187
- command you run: at a human gate it now opens `${VISUAL:-$EDITOR}` on the gate's
188
- file for you automatically (answer a plan question, tick a review box, fix
189
- code), waits for you to save and exit, then captures your edit itself and keeps
190
- driving — or halts if you left nothing changed — so you never run
191
- `gtd step human` by hand. Pass `--no-edit` (or set `GTD_NO_EDIT`) to fall back
192
- to halting and printing the gate instead, if you'd rather edit and re-launch it
193
- yourself; pass `--edit`/`-e` to force the editor open at the gate right now,
194
- overriding an ambient `GTD_NO_EDIT`/`--no-edit`. Pass `--once` to restrict a run
195
- to exactly one beat — one human gate, one check, or one agent turn — instead of
196
- driving all the way to idle; it combines freely with `--edit`/`--no-edit`. Pass
197
- `--no-notify` (or set `GTD_NO_NOTIFY`) to suppress Herdr desktop notifications
198
- while keeping Herdr sidebar state reporting on. Bare `gtd` prints one line per
199
- event — colored and emoji on a real terminal, plain ASCII under `NO_COLOR` or
200
- when piped — and redirects the noisier agent/check/step subprocess output to a
201
- per-repo/per-worktree log file. `gtd log` opens that logfile in your editor. See
202
- [Driving the loop](docs/loop.md).
180
+ point it at a repo and it runs the loop until it's your turn. It drives the
181
+ autonomous states (agent turns, check runs) and stops at the first
182
+ non-autonomous one: reaching a human gate it prints the gate and exits. You act
183
+ by editing files (answer a plan question, tick a review box, fix code) and
184
+ re-launching it — its opening move captures whatever you left, so you never run
185
+ `gtd step human` by hand. Anything richer at that boundary — opening your
186
+ editor, desktop notifications, terminal-multiplexer status — is the job of an
187
+ outer wrapper around `gtd`, not of the loop itself. Pass `--once` to restrict a
188
+ run to exactly one beat — one human-gate capture, one check, or one agent turn —
189
+ instead of driving all the way to idle. Bare `gtd` prints one line per event —
190
+ colored and emoji on a real terminal, plain ASCII under `NO_COLOR` or when piped
191
+ — and redirects the noisier agent/check/step subprocess output to a
192
+ per-repo/per-worktree log file (its path is the run's first output line, ready
193
+ to `tail -f`). See [Driving the loop](docs/loop.md).
203
194
 
204
195
  Before wiring gtd into a repo, note the
205
196
  [repository requirements](docs/cli.md#repository-requirements) — most
@@ -225,14 +216,10 @@ diagnostics for it, though `gtd validate` and the `gtd step` gate still format
225
216
  and validate it like any other mode. Both halves are enforced by `gtd validate`
226
217
  and the `gtd step` gate.
227
218
 
228
- Herdr integration: a workflow state can declare an optional `label:` — a
229
- human-readable display name surfaced in `gtd next --json`/`gtd status`. The
230
- reference `bin/gtd` driver uses it to report its lifecycle
231
- (working/blocked/idle) to a [Herdr](https://herdr.dev) pane sidebar via the
232
- `herdr` CLI when running under Herdr (`HERDR_ENV=1`, a pane ID, and `herdr` on
233
- `$PATH`); outside Herdr this is a complete no-op. See
234
- [Driving the loop](docs/loop.md#herdr-integration-optional) for the full
235
- reporting contract.
219
+ A workflow state can declare an optional `label:` — a human-readable display
220
+ name surfaced in `gtd next --json`/`gtd status`. The reference `bin/gtd` driver
221
+ uses it for its per-beat progress lines; an outer wrapper (a terminal
222
+ multiplexer, a notifier) can use it the same way.
236
223
 
237
224
  ## Documentation
238
225
 
package/bin/gtd CHANGED
@@ -3,33 +3,19 @@ set -euo pipefail
3
3
 
4
4
  # This file is the single published `bin` entry: `bin/gtd`. Before anything
5
5
  # else, it dispatches on $1:
6
- # - `--no-edit` / `--edit` (`-e`) / `--once` / `--no-notify` (bare, or
7
- # immediately after `loop`, in any order/combination — see the
8
- # flag-parsing block below) -> stripped here; `--no-edit` disables the
9
- # loop's automatic editor launching for this run, `--edit` forces it on
10
- # for this run (overriding an ambient `GTD_NO_EDIT`/`--no-edit` — the two
11
- # are mutually exclusive), `--once` restricts the loop to exactly one beat
12
- # (one human gate, one script check+step, or one agent prompt+step) before
13
- # exiting, and `--no-notify` (or `GTD_NO_NOTIFY`) suppresses the loop's
14
- # Herdr desktop notifications for this run while leaving its silent Herdr
15
- # pane-state reporting untouched. Dispatch then continues on the
16
- # remaining arguments as if none of them had ever been given.
17
- # - `edit [path]` -> handled entirely here in bash, never forwarded to the
18
- # bundle: opens `${VISUAL:-$EDITOR}` on `path`, or (no `path` given) on the
19
- # resolved rest's `.file` (or `.` when absent). Low-level plumbing now that
20
- # `--edit` covers the common "force an edit, then keep driving" case; this
21
- # subcommand remains for opening a specific path, or the current rest's
22
- # file, WITHOUT driving the loop afterward.
23
- # - `log` -> handled entirely here in bash, never forwarded to the bundle:
24
- # opens `${VISUAL:-$EDITOR}` on the current repo/worktree's loop logfile
25
- # (see `resolve_log_path`), refusing rather than opening an empty buffer
26
- # when no loop has produced one yet.
27
- # - no arguments, or exactly `loop` with no further arguments (other than the
28
- # flags above) -> fall through into the loop body below (the loop protocol
29
- # comment further down).
6
+ # - `--once` (bare, or immediately after `loop`) -> stripped here; restricts
7
+ # the loop to exactly one beat (one human-gate capture, one script
8
+ # check+step, or one agent prompt+step) before exiting. Dispatch then
9
+ # continues on the remaining arguments as if it had never been given.
10
+ # - no arguments, or exactly `loop` with no further arguments -> fall through
11
+ # into the loop body below (the loop protocol comment further down).
30
12
  # - `loop` with an unrecognised extra argument -> usage error, exit 2.
31
13
  # - anything else -> hand off to the bundle via `run_gtd`, forwarding all
32
14
  # arguments unchanged (e.g. `bin/gtd status`, `bin/gtd step human`).
15
+ # The loop's whole job is to drive the process until it rests at a
16
+ # NON-AUTONOMOUS state (a human gate) or settles. Everything beyond that —
17
+ # editors, desktop notifications, terminal-multiplexer status — is an OUTER
18
+ # wrapper's responsibility, deliberately not this script's.
33
19
  # `run_gtd` is the ONE place that knows how to invoke "the bundle": by default
34
20
  # it resolves `dist/gtd.bundle.mjs` relative to this script's own location
35
21
  # (not cwd), but `GTD_BIN` (a dev/test override for the whole invocation
@@ -47,29 +33,11 @@ run_gtd() {
47
33
  fi
48
34
  }
49
35
 
50
- # Shared editor-launch helper — used by both the `edit` subcommand below and
51
- # the loop's human-gate flow further down. Resolves the editor git-style
52
- # ($VISUAL, then $EDITOR — no fallback to `vi`), then launches it blocking in
53
- # the foreground with the target path passed as a positional parameter
54
- # (never string-spliced into the command), so a multi-word command
55
- # (`code --wait`) and a path with special characters both work. A launch
56
- # failure (bad/missing editor binary) is a plain non-zero return, propagating
57
- # under `set -e`; the editor's own exit code is otherwise ignored entirely —
58
- # the outcome is judged from tree state afterward, not from this function.
59
- launch_editor() {
60
- local target="$1" editor="${VISUAL:-${EDITOR:-}}"
61
- if [[ -z "$editor" ]]; then
62
- echo "gtd: no editor configured — set \$EDITOR (or \$VISUAL)" >&2
63
- return 1
64
- fi
65
- bash -c "$editor \"\$1\"" gtd-edit "$target"
66
- }
67
-
68
36
  # ── Rendering primitives (capability detection, palette/markers, emitters) ──
69
- # Additive: nothing below calls these yet. `FANCY=1` only when stdout (fd 1)
70
- # is a real terminal AND `NO_COLOR` is unset — any pipe/redirect/non-tty
71
- # stdout, or `NO_COLOR` set to any value, gives plain ASCII with no escape
72
- # codes, per the NO_COLOR convention (https://no-color.org).
37
+ # `FANCY=1` only when stdout (fd 1) is a real terminal AND `NO_COLOR` is unset
38
+ # — any pipe/redirect/non-tty stdout, or `NO_COLOR` set to any value, gives
39
+ # plain ASCII with no escape codes, per the NO_COLOR convention
40
+ # (https://no-color.org).
73
41
  if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then FANCY=1; else FANCY=0; fi
74
42
 
75
43
  if [[ "$FANCY" == 1 ]]; then
@@ -157,8 +125,6 @@ emit_log_pointer() {
157
125
  }
158
126
 
159
127
  # ── Git-derivation helpers (parse_subject, report_commits) ──────────────────
160
- # Additive: nothing below calls these yet. Replaces today's
161
- # head_before/head_after string comparisons in a later change.
162
128
 
163
129
  # parse_subject <subject> — sets S_ACTOR / S_FROM / S_TO / S_KIND. Patterns are
164
130
  # held in variables (rather than inlined) so bash's [[ =~ ]] doesn't need the
@@ -183,11 +149,14 @@ parse_subject() {
183
149
  fi
184
150
  }
185
151
 
186
- # report_commits <head_before> — walks <head_before>..HEAD in order, emitting
187
- # a transition/commit line per new commit; returns non-zero (having emitted
188
- # nothing) when there were no new commits.
152
+ # report_commits — walks "$reported_head"..HEAD in order, emitting a
153
+ # transition/commit line per new commit, and advances $reported_head to HEAD
154
+ # when it reported at least one; returns non-zero (having emitted nothing,
155
+ # and leaving $reported_head unchanged) when there were no new commits. Global
156
+ # rather than per-beat so the review checkout window's HEAD rewinds between
157
+ # beats never cause an already-reported commit to replay.
189
158
  report_commits() {
190
- local head_before="$1" sha subject files reported=0
159
+ local sha subject files reported=0
191
160
  while IFS= read -r sha; do
192
161
  [[ -z "$sha" ]] && continue
193
162
  reported=1
@@ -198,11 +167,14 @@ report_commits() {
198
167
  transition) emit_transition "$S_FROM" "$S_TO" "$files" ;;
199
168
  capture | squash) emit_commit "$subject" "$files" ;;
200
169
  esac
201
- done < <(git rev-list --reverse "$head_before"..HEAD)
170
+ done < <(git rev-list --reverse "$reported_head"..HEAD)
171
+ if [[ "$reported" == 1 ]]; then
172
+ reported_head="$(git rev-parse HEAD)"
173
+ fi
202
174
  [[ "$reported" == 1 ]]
203
175
  }
204
176
 
205
- # ── Log file (resolve_log_path, gtd log) ────────────────────────────────────
177
+ # ── Log file (resolve_log_path) ─────────────────────────────────────────────
206
178
 
207
179
  # worktree_git_dir — the git dir of the working tree gtd is operating in,
208
180
  # derived purely from the cwd. GIT_DIR/GIT_WORK_TREE are scrubbed for this one
@@ -226,25 +198,16 @@ resolve_log_path() {
226
198
  fi
227
199
  }
228
200
 
229
- # --no-edit / --edit (-e) / --once / --no-notify govern only the loop driver,
230
- # recognised in exactly two positions — bare (e.g. `gtd --edit --once`) or
231
- # immediately after `loop` (e.g. `gtd loop --once`) — in any order, each at
232
- # most once. `--edit` and `--no-edit` are mutually exclusive (forcing and
233
- # suppressing the editor in the same run is not a coherent request);
234
- # `--no-notify` conflicts with nothing (it's orthogonal to editing and to
235
- # `--once`). A recognised, valid combination is stripped here before anything
236
- # below looks at "$@", so the bundle's own unknown-option guard never sees it;
237
- # anything else (an unknown flag, a duplicate/conflicting one, or a flag mixed
238
- # with a real subcommand) is left untouched and falls through to the existing
239
- # dispatch below, which forwards a non-flag first argument to the bundle
240
- # (whose own unknown-option guard rejects it) or rejects a `loop` invocation
241
- # carrying an unrecognised extra argument.
242
- edit_disabled=0
243
- edit_forced=0
201
+ # --once governs only the loop driver, recognised in exactly two positions —
202
+ # bare (`gtd --once`) or immediately after `loop` (`gtd loop --once`) — at
203
+ # most once. When recognised it is stripped here before anything below looks
204
+ # at "$@", so the bundle's own unknown-option guard never sees it; anything
205
+ # else (an unknown flag, a duplicate --once, or --once mixed with a real
206
+ # subcommand) is left untouched and falls through to the existing dispatch
207
+ # below, which forwards a non-flag first argument to the bundle (whose own
208
+ # unknown-option guard rejects it) or rejects a `loop` invocation carrying an
209
+ # unrecognised extra argument.
244
210
  once_mode=0
245
- notify_disabled=0
246
- [[ -n "${GTD_NO_EDIT:-}" ]] && edit_disabled=1
247
- [[ -n "${GTD_NO_NOTIFY:-}" ]] && notify_disabled=1
248
211
 
249
212
  if [[ "${1:-}" == "loop" ]]; then
250
213
  loop_form=1
@@ -255,31 +218,17 @@ else
255
218
  fi
256
219
 
257
220
  valid_combo=1
258
- n_no_edit=0
259
- n_edit=0
260
221
  n_once=0
261
- n_no_notify=0
262
222
  for a in ${loop_flag_args[@]+"${loop_flag_args[@]}"}; do
263
223
  case "$a" in
264
- --no-edit) n_no_edit=$((n_no_edit + 1)) ;;
265
- --edit | -e) n_edit=$((n_edit + 1)) ;;
266
224
  --once) n_once=$((n_once + 1)) ;;
267
- --no-notify) n_no_notify=$((n_no_notify + 1)) ;;
268
225
  *) valid_combo=0 ;;
269
226
  esac
270
227
  done
271
- if ((n_no_edit > 1 || n_edit > 1 || n_once > 1 || n_no_notify > 1 || (n_no_edit >= 1 && n_edit >= 1))); then
272
- valid_combo=0
273
- fi
228
+ ((n_once > 1)) && valid_combo=0
274
229
 
275
- if [[ "$valid_combo" == 1 ]]; then
276
- ((n_no_edit >= 1)) && edit_disabled=1
277
- ((n_edit >= 1)) && edit_forced=1
278
- ((n_once >= 1)) && once_mode=1
279
- ((n_no_notify >= 1)) && notify_disabled=1
280
- # --edit overrides an ambient GTD_NO_EDIT/--no-edit for THIS run — the
281
- # explicit ask to force the editor wins over the suppressing default.
282
- ((edit_forced)) && edit_disabled=0
230
+ if [[ "$valid_combo" == 1 && "$n_once" == 1 ]]; then
231
+ once_mode=1
283
232
  if [[ "$loop_form" == 1 ]]; then
284
233
  set -- loop
285
234
  else
@@ -287,37 +236,6 @@ if [[ "$valid_combo" == 1 ]]; then
287
236
  fi
288
237
  fi
289
238
 
290
- if [[ "${1:-}" == "edit" ]]; then
291
- shift
292
- if [[ $# -gt 0 ]]; then
293
- launch_editor "$1"
294
- else
295
- if ! peek_json="$(run_gtd next --json 2>&1)"; then
296
- echo "gtd edit: could not determine the next step:" >&2
297
- echo "$peek_json" >&2
298
- exit 1
299
- fi
300
- file="$(jq -r '.file // empty' <<<"$peek_json")"
301
- launch_editor "${file:-.}"
302
- fi
303
- exit 0
304
- fi
305
-
306
- if [[ "${1:-}" == "log" ]]; then
307
- shift
308
- if [[ $# -gt 0 ]]; then
309
- echo "gtd log: takes no arguments" >&2
310
- exit 2
311
- fi
312
- log_path="$(resolve_log_path)"
313
- if [[ ! -f "$log_path" ]]; then
314
- echo "gtd log: no loop log yet at $log_path" >&2
315
- exit 1
316
- fi
317
- launch_editor "$log_path"
318
- exit 0
319
- fi
320
-
321
239
  if [[ $# -gt 0 && "$1" != "loop" ]]; then
322
240
  run_gtd "$@"
323
241
  exit $?
@@ -326,16 +244,14 @@ if [[ "${1:-}" == "loop" && $# -gt 1 ]]; then
326
244
  echo "gtd: 'loop' takes no arguments" >&2
327
245
  exit 2
328
246
  fi
329
- # otherwise ($# -eq 0, or exactly "loop" with no further args, --no-edit
330
- # already stripped above): fall through into the existing loop body below,
331
- # unchanged in behavior.
247
+ # otherwise ($# -eq 0, or exactly "loop" with no further args, --once already
248
+ # stripped above): fall through into the loop body below.
332
249
 
333
250
  # Drives the v3 pattern-machine loop (see docs/loop.md): each iteration asks
334
251
  # `gtd next --json` who's up and what they should do, dispatching on `kind`:
335
- # - "message" (a human rest) -> see handle_human_gate above: with editing on
336
- # (the default) open the editor at the gate and step human, driving on a
337
- # captured commit; with editing off (--no-edit/GTD_NO_EDIT), halt and tell
338
- # the user what gtd is waiting on, unchanged from before this feature.
252
+ # - "message" (a human rest) -> the non-autonomous halt: print the gate and
253
+ # exit 0. Acting on it (opening an editor, notifying, whatever) is an
254
+ # outer wrapper's job; this loop only ever drives the AUTONOMOUS states.
339
255
  # - "script" (a check rest) -> execute `content` (the emitted wrapper
340
256
  # script) verbatim via bash — exit code deliberately ignored, the outcome
341
257
  # lives in the tree — then `gtd step <actor>` to capture it; zero new
@@ -362,118 +278,16 @@ fi
362
278
  # resume-or-fresh flag are exported as $GTD_LOOP_MEMORY / $GTD_LOOP_SESSION_ID
363
279
  # / $GTD_LOOP_MEMORY_RESUME; the default adapter turns them into
364
280
  # `--resume`/`--session-id`. An adapter that ignores them keeps working.
365
- herdr_ok() { [ "${HERDR_ENV:-}" = 1 ] && [ -n "${HERDR_PANE_ID:-}" ] && command -v herdr >/dev/null 2>&1; }
366
-
367
- # Run a `herdr` invocation best-effort. Normally output is discarded and a
368
- # failure is swallowed (`|| true`) so a missing/incompatible herdr never blocks
369
- # the loop. Set GTD_HERDR_DEBUG=1 to instead surface every call and its exit
370
- # code on stderr — the escape hatch that makes a silent CLI-signature mismatch
371
- # (e.g. an arg herdr rejects) visible again.
372
- herdr_run() {
373
- if [ "${GTD_HERDR_DEBUG:-}" = 1 ]; then
374
- printf 'gtd/herdr: %s\n' "$*" >&2
375
- herdr "$@" >&2
376
- printf 'gtd/herdr: exit=%s\n' "$?" >&2
377
- else
378
- herdr "$@" >/dev/null 2>&1 || true
379
- fi
380
- }
381
-
382
- # NOTE: herdr requires the positional <PANE_ID> BEFORE the options for
383
- # `pane` subcommands — passing it last makes herdr reject the first option as
384
- # `unknown option` (exit 2). Keep the pane id first here.
385
- herdr_report() { # $1=state (working|blocked|idle) $2=message
386
- herdr_ok || return 0
387
- herdr_run pane report-agent "$HERDR_PANE_ID" \
388
- --source herdr:gtd --agent gtd --state "$1" --message "$2"
389
- }
390
-
391
- # Suppressible via GTD_NO_NOTIFY/--no-notify (set into $notify_disabled at
392
- # parse time, above) — the OS popup this call makes is the only thing that
393
- # opts out; herdr_report/herdr_release (silent sidebar status) are unaffected.
394
- herdr_notify() { # $1=title $2=body
395
- [[ "$notify_disabled" == 1 ]] && return 0
396
- herdr_ok || return 0
397
- herdr_run notification show "$1" --body "$2" --sound request
398
- }
399
-
400
- herdr_release() { # hand the pane back to Herdr's normal detection
401
- herdr_ok || return 0
402
- herdr_run pane release-agent "$HERDR_PANE_ID" --source herdr:gtd --agent gtd
403
- }
404
-
405
- # Shared human-gate handler for a "message" (human) rest — used both by the
406
- # opening move (when editing is on) and by every "message" rest the main loop
407
- # encounters. $1 is the rest's `next --json` blob.
408
- #
409
- # Editing OFF ($edit_disabled): today's behavior, unchanged — render the gate
410
- # via emit_gate and exit 0 without touching an editor.
411
- #
412
- # Editing ON (the default): open the editor on the rest's `.file` (or `.` when
413
- # absent), wait for it to exit, then run `gtd step human`:
414
- # - a NEW commit was authored -> return normally so the caller keeps driving.
415
- # - ZERO commits were authored -> halt, exit 0 (closing the editor without a
416
- # capturable change means "done for now" — and keeps a gate with no `C`
417
- # pattern, e.g. idle, from being reopened forever).
418
- # - the step REFUSES (non-zero exit: malformed steering file, unfinished
419
- # review) -> surface it verbatim and halt, exit non-zero — the same
420
- # wording an opening-move refusal already used before this function
421
- # existed.
422
- handle_human_gate() {
423
- local json="$1" file label state step_out head_before head_after
424
- label="$(jq -r '.label // .state' <<<"$json")"
425
- state="$(jq -r .state <<<"$json")"
426
-
427
- if [[ "$edit_disabled" == 1 ]]; then
428
- herdr_report blocked "$label"
429
- herdr_notify "gtd needs you — $(basename "$PWD")" "$label"
430
- emit_gate "$state" "$label"
431
- run_gtd next
432
- exit 0
433
- fi
434
-
435
- file="$(jq -r '.file // empty' <<<"$json")"
436
- herdr_report blocked "$label"
437
- launch_editor "${file:-.}"
438
- herdr_report working "$label"
439
-
440
- head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
441
- if ! step_out="$(run_gtd step human 2>&1)"; then
442
- emit_error "could not capture your changes at the human gate:
443
- $step_out
444
- Fix the reported issue and re-run gtd-loop."
445
- emit_log_pointer
446
- exit 1
447
- fi
448
- head_after="$(git rev-parse HEAD 2>/dev/null || echo none)"
449
- if [[ "$head_before" == "$head_after" ]]; then
450
- herdr_report idle "$label"
451
- herdr_release
452
- emit_action "$M_HUMAN" "done for now (nothing captured)"
453
- exit 0
454
- fi
455
- # A new commit was captured — render it, then return normally so the
456
- # caller keeps driving, unless --once says this one human gate is the whole
457
- # run.
458
- report_commits "$head_before"
459
- if [[ "$once_mode" == 1 ]]; then
460
- exit 0
461
- fi
462
- }
463
-
464
- trap 'code=$?; if [ "$code" -ne 0 ]; then
465
- herdr_report blocked "${GTD_LAST_LABEL:-stopped}: exited $code"
466
- herdr_notify "gtd stopped — $(basename "$PWD")" "${GTD_LAST_LABEL:-loop} failed (exit $code)"
467
- fi' EXIT
468
281
 
469
282
  prev_state=""
470
283
  prev_content=""
284
+ prev_head=""
471
285
  # Where the last "prompt" turn's `memory` scope + session id are remembered
472
286
  # across iterations and across restarts. In the git dir, never the work tree.
473
287
  memory_marker="$(worktree_git_dir 2>/dev/null || echo .git)/gtd-loop-memory"
474
288
 
475
289
  # Truncated once here (not every iteration), so the file always reflects the
476
- # current run but survives after the loop exits for a later `gtd log`.
290
+ # current run but survives after the loop exits.
477
291
  log_path="$(resolve_log_path)"
478
292
  : >"$log_path"
479
293
  export GTD_LOOP_LOG="$log_path"
@@ -512,6 +326,12 @@ run_agent_turn() {
512
326
  >>"${GTD_LOOP_LOG:-/dev/null}" 2>&1
513
327
  }
514
328
 
329
+ # The furthest commit already reported to the terminal by report_commits (see
330
+ # its own comment) — a global high-water mark rather than a per-beat
331
+ # head_before, so a review checkout window's HEAD rewind between beats never
332
+ # causes an already-printed transition to replay.
333
+ reported_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
334
+
515
335
  # Opening move: capture the human's pending edit so gtd-loop is the ONLY command
516
336
  # a human runs. A human acts at a gate by editing files (a plan answer, a review
517
337
  # tick, a code fix) and re-launching the loop — they should never have to run
@@ -525,16 +345,9 @@ run_agent_turn() {
525
345
  # and a mid-cycle restart must simply resume driving, not fail. A clean gate is
526
346
  # a harmless no-op; a genuine refusal there (a malformed steering file, or an
527
347
  # edit no `on` pattern accepts) is surfaced and stops the loop rather than being
528
- # driven past.
529
- #
530
- # Editing ON: route through the exact same gate logic the main loop uses below
531
- # (open the editor there first, THEN step) instead of blindly stepping human
532
- # and moving on. Editing OFF: unchanged from before this function existed —
533
- # step silently, refusal surfaced and halts, a clean/committed gate just falls
534
- # into the loop below. Either way, --once treats a captured commit here as the
535
- # run's one beat and exits (see handle_human_gate's own once-mode exit for the
536
- # editing-ON path); a clean/no-op gate captures nothing, so it isn't a beat —
537
- # it falls into the loop below exactly as it would without --once.
348
+ # driven past. --once treats a captured commit here as the run's one beat and
349
+ # exits; a clean/no-op gate captures nothing, so it isn't a beat — it falls
350
+ # into the loop below exactly as it would without --once.
538
351
  if ! peek_json="$(run_gtd next --json 2>&1)"; then
539
352
  emit_error "could not determine the next step:
540
353
  $peek_json
@@ -544,29 +357,20 @@ Run \`gtd status\` to inspect the repo."
544
357
  fi
545
358
  peek_kind="$(jq -r .kind <<<"$peek_json")"
546
359
  if [[ "$peek_kind" == "message" ]]; then
547
- if [[ "$edit_disabled" == 1 ]]; then
548
- opening_head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
549
- if ! step_out="$(run_gtd step human 2>&1)"; then
550
- emit_error "could not capture your changes at the human gate:
360
+ opening_head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
361
+ if ! step_out="$(run_gtd step human 2>&1)"; then
362
+ emit_error "could not capture your changes at the human gate:
551
363
  $step_out
552
364
  Fix the reported issue and re-run gtd-loop."
553
- emit_log_pointer
554
- exit 1
555
- fi
556
- if [[ "$once_mode" == 1 && "$opening_head_before" != "$(git rev-parse HEAD 2>/dev/null || echo none)" ]]; then
557
- exit 0
558
- fi
559
- else
560
- handle_human_gate "$peek_json"
365
+ emit_log_pointer
366
+ exit 1
561
367
  fi
562
- elif [[ "$edit_forced" == 1 ]]; then
563
- # Honesty caveat: a forced edit only means something at a human gate — the
564
- # machine isn't resting at one right now, so say so instead of pretending,
565
- # and just keep driving.
566
- if [[ "$FANCY" == 1 ]]; then
567
- printf '%sℹ️ --edit: not at a human gate yet — continuing to drive%s\n' "$C_DIM" "$C_RESET"
568
- else
569
- printf '%s--edit: not at a human gate yet — continuing to drive%s\n' "$C_DIM" "$C_RESET"
368
+ # This branch reports nothing — advance the marker past whatever was just
369
+ # (silently) captured so the first loop-body beat below doesn't newly
370
+ # print it.
371
+ reported_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
372
+ if [[ "$once_mode" == 1 && "$opening_head_before" != "$reported_head" ]]; then
373
+ exit 0
570
374
  fi
571
375
  fi
572
376
 
@@ -585,20 +389,18 @@ while true; do
585
389
  model="$(jq -r '.model // empty' <<<"$next_json")"
586
390
  memory="$(jq -r '.memory // empty' <<<"$next_json")"
587
391
  label="$(jq -r '.label // .state' <<<"$next_json")"
588
- GTD_LAST_LABEL="$label"
589
392
 
590
393
  if [[ "$kind" == "message" ]]; then
591
- # handle_human_gate exits 0/non-zero itself on a halt/refusal; it only
592
- # returns here when a new commit was captured, so we keep driving.
593
- handle_human_gate "$next_json"
594
- continue
394
+ # The non-autonomous halt: print the gate, hand off, exit 0. The human
395
+ # (or an outer wrapper) edits and re-launches; the opening move above
396
+ # captures what they left.
397
+ emit_gate "$state" "$label"
398
+ run_gtd next
399
+ exit 0
595
400
  fi
596
401
 
597
- herdr_report working "$label"
598
-
599
402
  if [[ "$kind" == "script" ]]; then
600
403
  emit_action "$M_CHECK" "$label"
601
- head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
602
404
  # Execute the emitted wrapper verbatim; its exit code is deliberately
603
405
  # ignored (`|| true` under `set -e`) — the script encodes its outcome in
604
406
  # the tree, and `gtd step` captures whatever it left there via the state's
@@ -611,9 +413,7 @@ while true; do
611
413
  exit 1
612
414
  fi
613
415
  printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
614
- if ! report_commits "$head_before"; then
615
- herdr_report idle "$label"
616
- herdr_release
416
+ if ! report_commits; then
617
417
  emit_settled "$state"
618
418
  exit 0
619
419
  fi
@@ -626,9 +426,14 @@ while true; do
626
426
  fi
627
427
 
628
428
  # kind == "prompt": stall detection (skills/loop/SKILL.md) — the same
629
- # agent-owned state and content repeating means the last dispatch made no
630
- # progress; stop rather than spin on it.
631
- if [[ "$state" == "$prev_state" && "$content" == "$prev_content" ]]; then
429
+ # agent-owned state and content repeating, WITH NO COMMIT authored since the
430
+ # last time we saw this beat, means the last dispatch made no progress; stop
431
+ # rather than spin on it. The HEAD check is what distinguishes a genuine
432
+ # stall from a return lap back into the same state (e.g. checking ->
433
+ # fixing): a static prompt looks identical to last time, but a commit
434
+ # landed in between, so HEAD has moved and this isn't a stall.
435
+ cur_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
436
+ if [[ "$state" == "$prev_state" && "$content" == "$prev_content" && "$cur_head" == "$prev_head" ]]; then
632
437
  next_out="$(run_gtd next 2>&1)" || true
633
438
  emit_error "no progress at '$state' — the agent's last turn changed nothing. Stopping to avoid spinning.
634
439
  $next_out"
@@ -637,6 +442,7 @@ $next_out"
637
442
  fi
638
443
  prev_state="$state"
639
444
  prev_content="$content"
445
+ prev_head="$cur_head"
640
446
 
641
447
  # Agent-memory scope: compare this turn's `memory` label against the last
642
448
  # prompt turn's (persisted in $memory_marker). Same non-empty label AND a
@@ -673,7 +479,6 @@ $next_out"
673
479
  # driver-side equivalent, so the next rest is only ever handed a well-formed
674
480
  # file. gtd itself never validates at step time — this is the loop's job.
675
481
  emit_action "$M_AGENT" "$label"
676
- head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
677
482
  run_agent_turn "$content" "$resume"
678
483
 
679
484
  fix_attempt=0
@@ -704,7 +509,7 @@ Stopping rather than stepping with a malformed steering file."
704
509
  # A no-op step (nothing matched a declared pattern on a clean tree) reports
705
510
  # zero commits here — report_commits returns non-zero, and that's fine: the
706
511
  # NEXT iteration's stall check is what catches unchanged state/content.
707
- report_commits "$head_before" || true
512
+ report_commits || true
708
513
  if [[ "$once_mode" == 1 ]]; then
709
514
  exit 0
710
515
  fi
@@ -205450,12 +205450,9 @@ Commands:
205450
205450
  (no command), loop
205451
205451
  Launch the loop driver (bin/gtd), which repeatedly drives
205452
205452
  an agent through gtd next/gtd step calls until the
205453
- workflow returns to its initial state again. A bare gtd
205454
- invocation and gtd loop both launch it identically
205455
- edit [path] Open \${VISUAL:-$EDITOR} on <path>, or — with no path — on
205456
- the resolved rest's declared file (or the repo directory
205457
- if it declares none). Handled entirely by bin/gtd, not
205458
- this bundle
205453
+ workflow rests at a human gate (a non-autonomous state)
205454
+ or settles. A bare gtd invocation and gtd loop both
205455
+ launch it identically
205459
205456
  init Scaffold a minimal .gtdrc.json for this repo, seeding the
205460
205457
  default variables you are most likely to change (the test
205461
205458
  command) and a Prettier formatting suggestion. gtd runs its
package/docs/cli.md CHANGED
@@ -7,8 +7,9 @@ Commands:
7
7
  (no command), loop
8
8
  Launch the loop driver (bin/gtd), which repeatedly drives
9
9
  an agent through gtd next/gtd step calls until the
10
- workflow returns to its initial state again. A bare gtd
11
- invocation and gtd loop both launch it identically
10
+ workflow rests at a human gate (a non-autonomous state)
11
+ or settles. A bare gtd invocation and gtd loop both
12
+ launch it identically
12
13
  init Scaffold a minimal .gtdrc.json for this repo, seeding the
13
14
  default variables you are most likely to change (the test
14
15
  command) and a Prettier formatting suggestion. gtd runs its
@@ -45,12 +46,6 @@ Commands:
45
46
  commits that would be lost
46
47
  next Print the resolved rest's rendered script/prompt/message
47
48
  (no mutation)
48
- edit [path] Open <path> (repo-relative) in $VISUAL/$EDITOR, blocking
49
- until the editor exits. With no argument, opens the
50
- resolved rest's declared file (or the repo root if it
51
- declares none). Never reads --no-edit/--edit/GTD_NO_EDIT.
52
- Low-level plumbing — --edit covers the common "force an
53
- edit, then keep driving" case
54
49
  status Print the resolved rest's state/actor and which declared
55
50
  pattern (if any) each pending change matches (no mutation)
56
51
  validate Format and validate the steering file the resolved rest
@@ -69,15 +64,9 @@ Options:
69
64
  --no-open (gtd visualize only) do not open the browser
70
65
  --cost=<n> (gtd step only) record the invocation's token cost
71
66
  --model=<name> (gtd step only, with --cost) tag that cost's model
72
- --no-edit (bare gtd or gtd loop only) disable the loop's automatic
73
- editor launching at human gates
74
- --edit, -e (bare gtd or gtd loop only) force the editor open at the
75
- current human gate, overriding --no-edit/GTD_NO_EDIT; a
76
- no-op note (not an error) when not at a human gate
77
67
  --once (bare gtd or gtd loop only) run exactly one loop beat (one
78
- human gate, one script check+step, or one agent
79
- prompt+step), then exit — combines freely with
80
- --edit/--no-edit
68
+ human-gate capture, one script check+step, or one agent
69
+ prompt+step), then exit
81
70
  --version, -v Print version and exit
82
71
  --help, -h Print this help and exit
83
72
  ```
@@ -101,49 +90,30 @@ to plain-text mode. A bare `--cost`/`--model` with no value, a non-numeric or
101
90
  negative `--cost`, an empty `--model`, `--model` without `--cost`, or either
102
91
  flag on any command other than `gtd step` are all usage errors.
103
92
 
104
- `--no-edit`, `--edit`/`-e`, and `--once` are separate, bash-level flags handled
105
- entirely by `bin/gtd` itself, stripped before anything reaches the bundle — see
106
- below.
107
-
108
- ## `--no-edit`, `--edit`, `--once`
109
-
110
- Three flags on the loop driver only (`--edit` also has the short form `-e`),
111
- each recognized bare (`gtd --no-edit`) or immediately after `loop`
112
- (`gtd loop --no-edit`) — the two positions are equivalent, since bare `gtd` and
113
- `gtd loop` are themselves equivalent. All three combine freely with each other,
114
- in any order (e.g. `gtd --edit --once`, `gtd loop --once --edit`), except
115
- `--edit`/`--no-edit` together, which conflict (forcing and suppressing the
116
- editor in the same run is not a coherent request). Any other placement (e.g.
117
- `gtd step --once`), a duplicated flag (`gtd --once --once`), or the
118
- `--edit`/`--no-edit` conflict is a usage error, surfaced via whatever guard the
119
- misplaced argument first reaches (the compiled bundle's own
120
- unknown-option/unknown-command rejection for a misplaced flag, or
93
+ `--once` is a separate, bash-level flag handled entirely by `bin/gtd` itself,
94
+ stripped before anything reaches the bundle — see below.
95
+
96
+ ## `--once`
97
+
98
+ A flag on the loop driver only, recognized bare (`gtd --once`) or immediately
99
+ after `loop` (`gtd loop --once`) — the two positions are equivalent, since bare
100
+ `gtd` and `gtd loop` are themselves equivalent. Any other placement (e.g.
101
+ `gtd step --once`) or a duplicated flag (`gtd --once --once`) is a usage error,
102
+ surfaced via whatever guard the misplaced argument first reaches (the compiled
103
+ bundle's own unknown-option/unknown-command rejection for a misplaced flag, or
121
104
  `gtd: 'loop' takes no arguments` for one misplaced after `loop`) — never
122
- silently ignored or silently resolved one way.
123
-
124
- **`--no-edit`** (or the `GTD_NO_EDIT` environment variable, any non-empty value)
125
- disables the loop's default behavior of launching an editor at human gates,
126
- restoring the halt-and-print-and-exit behavior with no editor involved.
127
-
128
- **`--edit`** (`-e`) is `--no-edit`'s mirror: it forces the editor open at the
129
- current human gate, overriding an ambient `GTD_NO_EDIT`/`--no-edit` default for
130
- this one run. It is only meaningful when the machine currently rests at a human
131
- gate — the honesty caveat: at any other rest, forcing an edit is undefined, so
132
- `bin/gtd` prints a note that it isn't at a human gate yet and simply keeps
133
- driving, rather than pretending to force something. Both flags govern only the
134
- loop's own automatic launching — `gtd edit` invoked directly never reads either
135
- (see [`gtd edit`](#gtd-edit-path) above).
136
-
137
- **`--once`** restricts the loop to exactly one beat — one human gate, one script
138
- check+step, or one agent prompt+step — then exits, instead of driving all the
139
- way back to idle/settled. Precisely: at most one commit/transition is made;
140
- whichever of the three kinds the currently-resolved rest is, that one gets
105
+ silently ignored.
106
+
107
+ **`--once`** restricts the loop to exactly one beat — one human-gate capture,
108
+ one script check+step, or one agent prompt+step — then exits, instead of driving
109
+ all the way back to idle/settled. Precisely: at most one commit/transition is
110
+ made; whichever of the three kinds the currently-resolved rest is, that one gets
141
111
  processed (including any internal `gtd validate` fix-reprompt rounds a producing
142
112
  agent turn needs — those are all part of finishing the ONE prompt beat, not
143
113
  separate beats) and the loop exits 0 immediately after, without re-peeking at
144
114
  what comes next. A clean human gate with nothing to capture (the opening move's
145
- silent capture attempt under `--no-edit`, or a no-op editor session) is not a
146
- beat and falls through to the driver's ordinary halt, same as without `--once`.
115
+ silent capture attempt) is not a beat and falls through to the driver's ordinary
116
+ halt, same as without `--once`.
147
117
 
148
118
  See [Driving the loop](loop.md) and `skills/loop/SKILL.md` for the full
149
119
  gate-flow description.
@@ -470,46 +440,6 @@ which are JSON-only. `--json` emits
470
440
  when the state has no `on` (a commit state); a per-edge `describe` is likewise
471
441
  omitted when that edge declares none.
472
442
 
473
- ## `gtd edit [path]`
474
-
475
- Low-level plumbing: for the common case of "force an edit at the current human
476
- gate, then keep driving the loop", use `--edit` (below) instead. `gtd edit`
477
- itself only opens an editor on a path — it never drives anything afterward,
478
- which makes it the right tool for opening a SPECIFIC path (or peeking at the
479
- current rest's file) without starting a loop.
480
-
481
- Handled entirely in `bin/gtd`'s own bash — unlike every other subcommand, it is
482
- **never forwarded** to the compiled bundle (`dist/gtd.bundle.mjs`).
483
-
484
- With `<path>` given (repo-relative), opens it in `${VISUAL:-$EDITOR}` (git's own
485
- precedence — `$VISUAL` first, then `$EDITOR`; no fallback to `vi`), blocking in
486
- the foreground until the editor exits. It creates nothing if `<path>` doesn't
487
- exist — the path is handed straight to the editor command verbatim. The editor's
488
- own exit code is ignored entirely; success is judged from tree state afterward,
489
- not from this command.
490
-
491
- With no argument, it peeks with `gtd next --json` to find the resolved rest's
492
- declared `.file`, and opens that (repo-relative), or the repo directory (`.`)
493
- when the resolved state declares no `file:`. If `gtd next --json` itself fails
494
- (e.g. not in a repo), it prints an error and exits non-zero without attempting
495
- to open an editor:
496
-
497
- ```
498
- gtd edit: could not determine the next step:
499
- <gtd next --json's own error output>
500
- ```
501
-
502
- `gtd edit` **never** reads `--no-edit`/`--edit`/`GTD_NO_EDIT` — those govern
503
- only the loop's own automatic editor launching at human gates (see
504
- [Driving the loop](loop.md) and `skills/loop/SKILL.md`), not this command.
505
- Invoked directly, `gtd edit` always launches, unconditionally.
506
-
507
- If neither `$VISUAL` nor `$EDITOR` is set, no editor is launched:
508
-
509
- ```
510
- gtd: no editor configured — set $EDITOR (or $VISUAL)
511
- ```
512
-
513
443
  ## Running `script` rests (no `gtd` subcommand)
514
444
 
515
445
  There is no `gtd` subcommand that executes a workflow script — gtd never runs a
package/docs/loop.md CHANGED
@@ -52,38 +52,17 @@ capture happens within the same run or a later one. A review nobody is going to
52
52
  finish is ended with [`gtd abandon`](cli.md#gtd-abandon---json), which drops the
53
53
  process's commits and keeps their content as uncommitted changes.
54
54
 
55
- **Editing is on by default.** Reaching a `"message"` rest, `bin/gtd` opens
56
- `${VISUAL:-$EDITOR}` on the rest's declared `.file` (or the repo root `.` when
57
- the state declares none), blocking until the editor exits, then runs
58
- `gtd step human` to capture whatever changed:
59
-
60
- - a new commit was captured → the loop keeps driving on its own; re-launching is
61
- never necessary;
62
- - zero commits were captured (a clean tree once the editor closed) → the loop
63
- halts, exit 0 — "done for now";
64
- - the step refuses (a malformed steering file, or an edit matching no declared
65
- pattern) → the refusal is surfaced verbatim and the loop halts non-zero,
66
- exactly as a manual `gtd step human` refusal would.
67
-
68
- Set `GTD_NO_EDIT` (any non-empty value), or pass `--no-edit` — see
69
- [`cli.md`](cli.md#--no-edit---edit---once) for its exact syntax and positioning
70
- — to disable this and restore the original halt-and-print behavior: the loop
71
- prints the gate via `gtd next` and exits 0 without touching an editor, leaving
72
- the human to edit and re-launch the loop themselves.
73
-
74
- Pass `--edit` (`-e`) to force the editor open at the current human gate right
75
- now, overriding an ambient `GTD_NO_EDIT`/`--no-edit` for this one run — useful
76
- when a shell profile sets `GTD_NO_EDIT` by default but you want to edit
77
- interactively just this once. It only means something when the machine is
78
- actually resting at a human gate: at any other rest, `bin/gtd` prints a note
79
- that it isn't at one yet and simply keeps driving, rather than pretending to
80
- force something undefined.
81
-
82
- Pass `--once` to restrict the loop to exactly one beat — one human gate, one
83
- script check+step, or one agent prompt+step — then exit, instead of driving all
84
- the way to idle/settled. It combines freely with `--edit`/`--no-edit` (e.g.
85
- `gtd --edit --once` forces the editor at the current gate, captures the edit,
86
- and stops right there rather than continuing to drive).
55
+ **The loop drives only the autonomous states.** Reaching a `"message"` rest, it
56
+ prints the gate via `gtd next` and exits 0 — the human edits and re-launches the
57
+ loop, whose opening move captures what they left. Anything richer at that
58
+ boundary (opening an editor on the gate's file, desktop notifications,
59
+ terminal-multiplexer status) is deliberately an OUTER wrapper's responsibility,
60
+ not `bin/gtd`'s.
61
+
62
+ Pass `--once` — see [`cli.md`](cli.md#--once) for its exact syntax and
63
+ positioning — to restrict the loop to exactly one beat — one human-gate capture,
64
+ one script check+step, or one agent prompt+step — then exit, instead of driving
65
+ all the way to idle/settled.
87
66
 
88
67
  ```bash
89
68
  gtd next --json # ask who's up and what they should do
@@ -106,21 +85,17 @@ when the machine rests at a `"message"` gate — so `gtd` is the only command a
106
85
  human runs. `bin/gtd` is the packaged entry point: invoked bare, or with `loop`
107
86
  as its first argument, it runs this exact script; any other first argument (e.g.
108
87
  `next`, `step`, `status`) hands off to `node dist/gtd.bundle.mjs` instead. The
109
- loop body it runs adds six things on top of the reference script above: it opens
110
- an editor at every `"message"` gate instead of just printing it (see "Editing is
111
- on by default" above; `--no-edit`/`GTD_NO_EDIT` fall back to the reference
112
- script's plain print-and-exit, while `--edit`/`-e` forces it on for this run
113
- even over an ambient `GTD_NO_EDIT`); it can restrict itself to exactly one beat
114
- via `--once` instead of driving to idle/settled; it stops with a diagnostic if
115
- the same `"prompt"` state/content repeat with no progress (see
116
- `skills/loop/SKILL.md`'s "Stall detection"); it lets `GTD_LOOP_AGENT_CMD` swap
117
- in any coding agent CLI in place of the default `claude -p`, receiving the
118
- prompt via `$GTD_LOOP_PROMPT`; it exports the resolved state's optional `model`
119
- hint as `$GTD_LOOP_MODEL`, appending `--model "$GTD_LOOP_MODEL"` to the default
120
- `claude -p` invocation whenever it's non-empty; and it acts on the optional
121
- `memory` scope hint (see "Agent memory scope" below) — continuing one agent
122
- session across consecutive same-scope turns and starting fresh when the scope
123
- changes.
88
+ loop body it runs adds five things on top of the reference script above: it can
89
+ restrict itself to exactly one beat via `--once` instead of driving to
90
+ idle/settled; it stops with a diagnostic if the same `"prompt"` state/content
91
+ repeat with no progress (see `skills/loop/SKILL.md`'s "Stall detection"); it
92
+ lets `GTD_LOOP_AGENT_CMD` swap in any coding agent CLI in place of the default
93
+ `claude -p`, receiving the prompt via `$GTD_LOOP_PROMPT`; it exports the
94
+ resolved state's optional `model` hint as `$GTD_LOOP_MODEL`, appending
95
+ `--model "$GTD_LOOP_MODEL"` to the default `claude -p` invocation whenever it's
96
+ non-empty; and it acts on the optional `memory` scope hint (see "Agent memory
97
+ scope" below) — continuing one agent session across consecutive same-scope turns
98
+ and starting fresh when the scope changes.
124
99
 
125
100
  `bin/gtd` resolves both its bundle hand-off and the loop's own internal `gtd`
126
101
  calls against `dist/gtd.bundle.mjs` next to its own location. Set `GTD_BIN` — a
@@ -181,11 +156,8 @@ The driver — not the prompt text — owns ending the agent's turn
181
156
  (`gtd step "$actor"` right after the agent acts): every default-workflow agent
182
157
  prompt says explicitly not to run `gtd step agent` itself.
183
158
 
184
- `bin/gtd log` opens the current repo/worktree's loop logfile —
185
- `"$(worktree_git_dir)/gtd-loop.log"` by default, or `$GTD_LOOP_LOG` verbatim
186
- when set — in `${VISUAL:-$EDITOR}`, the same editor resolution `bin/gtd edit`
187
- uses. It takes no arguments and errors out (rather than opening an empty buffer)
188
- if no loop has produced a log yet. `worktree_git_dir` derives the git dir from
159
+ The loop's logfile lives at `"$(worktree_git_dir)/gtd-loop.log"` by default, or
160
+ `$GTD_LOOP_LOG` verbatim when set. `worktree_git_dir` derives the git dir from
189
161
  the cwd with `GIT_DIR`/`GIT_WORK_TREE` scrubbed, so an inherited `GIT_DIR`
190
162
  (leaked from a parent git process, a hook, or another worktree's shell) can
191
163
  never key the log — or the loop's memory marker — to a different worktree; two
@@ -205,8 +177,8 @@ by a `📄 see <log>` pointer (plain: `see <log>`).
205
177
  The three previously terminal-visible subprocess streams — the agent turn's own
206
178
  output, the check script's own output, and `gtd step`'s own output — are no
207
179
  longer printed directly: they're appended to the per-repo/per-worktree log file
208
- (truncated once at the start of the run) instead, and surfaced with
209
- `bin/gtd log` above.
180
+ (truncated once at the start of the run) instead — the loop prints the log's
181
+ path as its first line, ready to `tail -f`.
210
182
 
211
183
  ## Agent memory scope
212
184
 
@@ -245,45 +217,3 @@ example, to drive a different agent CLI:
245
217
  ```bash
246
218
  GTD_LOOP_AGENT_CMD='my-agent-cli --prompt "$GTD_LOOP_PROMPT"' gtd
247
219
  ```
248
-
249
- ## Herdr integration (optional)
250
-
251
- `bin/gtd` optionally reports its lifecycle to [Herdr](https://herdr.dev) (a
252
- terminal multiplexer for coding agents) via a `herdr` CLI binary, entirely at
253
- the driver's edge — gtd core never talks to Herdr. Every call is best-effort
254
- (guarded, output discarded, `|| true`), so a missing/failing `herdr` binary
255
- never blocks, slows, or fails the loop. Because that guard also hides a herdr
256
- CLI-signature mismatch, set `GTD_HERDR_DEBUG=1` to surface every `herdr` call
257
- and its exit code on stderr instead.
258
-
259
- The reporting is a no-op unless all three guard conditions hold: `HERDR_ENV=1`,
260
- a non-empty `$HERDR_PANE_ID`, and `herdr` on `$PATH`. When they do, `gtd` makes
261
- three kinds of calls (note the positional `<PANE_ID>` comes BEFORE the options —
262
- herdr's `pane` subcommands reject a trailing pane id):
263
-
264
- - `herdr pane report-agent "$HERDR_PANE_ID" --source herdr:gtd --agent gtd --state <state> --message <label>`
265
- - `herdr pane release-agent "$HERDR_PANE_ID" --source herdr:gtd --agent gtd`
266
- - `herdr notification show <title> --body <label> --sound request`
267
-
268
- mapped onto the loop's states like this:
269
-
270
- | Loop moment | Call(s) |
271
- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
272
- | Top of every non-`"message"` iteration | `report-agent --state working` |
273
- | Right before halting at a `"message"` (human) gate (editing off) | `report-agent --state blocked` + `notification show` |
274
- | While the editor is open at a human gate (editing on) | `report-agent --state blocked`, flipped back to `working` once it closes |
275
- | Right before a clean settle (a script rest, zero new commits) | `report-agent --state idle` + `release-agent` |
276
- | Any non-zero exit (stall guard, validate-cap, `gtd next` failure, unhandled failure) | `report-agent --state blocked` + `notification show`, via an `EXIT` trap |
277
-
278
- `working`/`blocked` reports use the resolved rest's `label` (falling back to
279
- `state`, per the field above); the trap falls back to `$GTD_LAST_LABEL`, the
280
- label tracked from the last completed iteration, when it fires outside the
281
- normal per-iteration flow. `report-agent` claims display authority for the
282
- `herdr:gtd` source, so `working` is re-reported every lap rather than once, to
283
- stay fresh against Herdr's own heuristic detection of the inner agent process.
284
-
285
- Set `GTD_NO_NOTIFY` (any non-empty value) or pass `--no-notify` to suppress the
286
- two `notification show` calls above (the human-gate halt and the exit trap) for
287
- this run — the OS popup + sound only. `report-agent`/`release-agent` (the silent
288
- Herdr sidebar status) are untouched either way, mirroring
289
- `--no-edit`/`GTD_NO_EDIT`'s flag/env shape.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "7.11.1",
3
+ "version": "8.0.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {