@pmelab/gtd 7.4.2 → 7.6.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
@@ -106,6 +106,9 @@ handful of commands drive it:
106
106
  pending changes, and which action leads where. The panel and diagram highlight
107
107
  refresh live (~every 3s) while the page is open, so advancing the process
108
108
  elsewhere shows up without a manual refresh.
109
+ - **`gtd edit [path]`** — open `<path>` (or, with no argument, the current
110
+ resting state's steering file, or the repo dir) in `${VISUAL:-$EDITOR}`,
111
+ blocking until it exits.
109
112
 
110
113
  `gtd version` (or `gtd --version`/`-v`) prints the installed version and exits;
111
114
  `gtd help` (or `gtd --help`/`-h`) prints the command list. Both short-circuit
@@ -172,9 +175,16 @@ the file.
172
175
 
173
176
  Bare `gtd` (or `gtd loop`) is a ready-to-run driver for the whole protocol —
174
177
  point it at a repo and it runs the loop until it's your turn. It is the only
175
- command you run: at a gate you edit files (answer a plan question, tick a review
176
- box, fix code) and re-launch it, and it captures your edit as its opening move
177
- before driving on — so you never run `gtd step human` by hand. See
178
+ command you run: at a human gate it now opens `${VISUAL:-$EDITOR}` on the gate's
179
+ file for you automatically (answer a plan question, tick a review box, fix
180
+ code), waits for you to save and exit, then captures your edit itself and keeps
181
+ driving — or halts if you left nothing changed — so you never run
182
+ `gtd step human` by hand. Pass `--no-edit` (or set `GTD_NO_EDIT`) to fall back
183
+ to halting and printing the gate instead, if you'd rather edit and re-launch it
184
+ yourself. Bare `gtd` prints one line per event — colored and emoji on a real
185
+ terminal, plain ASCII under `NO_COLOR` or when piped — and redirects the noisier
186
+ agent/check/step subprocess output to a per-repo/per-worktree log file.
187
+ `gtd log` opens that logfile in your editor. See
178
188
  [Driving the loop](docs/loop.md).
179
189
 
180
190
  Before wiring gtd into a repo, note the
package/bin/gtd CHANGED
@@ -3,9 +3,21 @@ 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` (bare, or immediately after `loop`) -> stripped here and
7
+ # disables the loop's automatic editor launching for this run (see
8
+ # `edit_disabled`/`GTD_NO_EDIT` below); dispatch then continues on the
9
+ # remaining arguments as if it had never been given.
10
+ # - `edit [path]` -> handled entirely here in bash, never forwarded to the
11
+ # bundle: opens `${VISUAL:-$EDITOR}` on `path`, or (no `path` given) on the
12
+ # resolved rest's `.file` (or `.` when absent).
13
+ # - `log` -> handled entirely here in bash, never forwarded to the bundle:
14
+ # opens `${VISUAL:-$EDITOR}` on the current repo/worktree's loop logfile
15
+ # (see `resolve_log_path`), refusing rather than opening an empty buffer
16
+ # when no loop has produced one yet.
6
17
  # - no arguments, or exactly `loop` with no further arguments -> fall through
7
18
  # into the loop body below (the loop protocol comment further down).
8
- # - `loop` with additional arguments -> usage error, exit 2.
19
+ # - `loop` with additional arguments (other than a lone `--no-edit`) ->
20
+ # usage error, exit 2.
9
21
  # - anything else -> hand off to the bundle via `run_gtd`, forwarding all
10
22
  # arguments unchanged (e.g. `bin/gtd status`, `bin/gtd step human`).
11
23
  # `run_gtd` is the ONE place that knows how to invoke "the bundle": by default
@@ -25,6 +37,211 @@ run_gtd() {
25
37
  fi
26
38
  }
27
39
 
40
+ # Shared editor-launch helper — used by both the `edit` subcommand below and
41
+ # the loop's human-gate flow further down. Resolves the editor git-style
42
+ # ($VISUAL, then $EDITOR — no fallback to `vi`), then launches it blocking in
43
+ # the foreground with the target path passed as a positional parameter
44
+ # (never string-spliced into the command), so a multi-word command
45
+ # (`code --wait`) and a path with special characters both work. A launch
46
+ # failure (bad/missing editor binary) is a plain non-zero return, propagating
47
+ # under `set -e`; the editor's own exit code is otherwise ignored entirely —
48
+ # the outcome is judged from tree state afterward, not from this function.
49
+ launch_editor() {
50
+ local target="$1" editor="${VISUAL:-${EDITOR:-}}"
51
+ if [[ -z "$editor" ]]; then
52
+ echo "gtd: no editor configured — set \$EDITOR (or \$VISUAL)" >&2
53
+ return 1
54
+ fi
55
+ bash -c "$editor \"\$1\"" gtd-edit "$target"
56
+ }
57
+
58
+ # ── Rendering primitives (capability detection, palette/markers, emitters) ──
59
+ # Additive: nothing below calls these yet. `FANCY=1` only when stdout (fd 1)
60
+ # is a real terminal AND `NO_COLOR` is unset — any pipe/redirect/non-tty
61
+ # stdout, or `NO_COLOR` set to any value, gives plain ASCII with no escape
62
+ # codes, per the NO_COLOR convention (https://no-color.org).
63
+ if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then FANCY=1; else FANCY=0; fi
64
+
65
+ if [[ "$FANCY" == 1 ]]; then
66
+ C_RESET=$'\e[0m'; C_BOLD=$'\e[1m'; C_DIM=$'\e[2m'
67
+ C_CYAN=$'\e[36m'; C_GREEN=$'\e[32m'; C_YELLOW=$'\e[33m'; C_RED=$'\e[31m'
68
+ M_TRANS="➡️"; M_AGENT="🤖"; M_CHECK="🔧"; M_HUMAN="✍️"
69
+ M_FIX="🔁"; M_COMMIT="✅"; M_SETTLE="🎉"; M_ERROR="❌"
70
+ else
71
+ C_RESET=""; C_BOLD=""; C_DIM=""
72
+ C_CYAN=""; C_GREEN=""; C_YELLOW=""; C_RED=""
73
+ M_TRANS="->"; M_AGENT="[agent]"; M_CHECK="[check]"; M_HUMAN="[you]"
74
+ M_FIX="[fix]"; M_COMMIT="[commit]"; M_SETTLE="[done]"; M_ERROR="[err]"
75
+ fi
76
+
77
+ # emit_transition <from> <to> <files> — a captured state-to-state move. The
78
+ # "→" separator is literal (not FANCY-dependent) — the same separator the
79
+ # engine's commit grammar itself uses, per the package spec.
80
+ emit_transition() {
81
+ local from="$1" to="$2" files="$3"
82
+ if [[ -n "$files" ]]; then
83
+ printf '%s %s%s → %s%s (%s%s%s)\n' \
84
+ "$M_TRANS" "$C_BOLD$C_CYAN" "$from" "$to" "$C_RESET" \
85
+ "$C_DIM" "$files" "$C_RESET"
86
+ else
87
+ printf '%s %s%s → %s%s\n' \
88
+ "$M_TRANS" "$C_BOLD$C_CYAN" "$from" "$to" "$C_RESET"
89
+ fi
90
+ }
91
+
92
+ # emit_action <marker> <label> — the heartbeat printed before a turn runs.
93
+ emit_action() {
94
+ local marker="$1" label="$2"
95
+ printf '%s %s%s\n' "$marker" "$label" "$C_RESET"
96
+ }
97
+
98
+ # emit_commit <subject> <files> — a non-transition capture (a bare-subject
99
+ # self-loop, or the squash's agent-authored final commit).
100
+ emit_commit() {
101
+ local subject="$1" files="$2"
102
+ if [[ -n "$files" ]]; then
103
+ printf '%s %s%s%s (%s%s%s)\n' "$M_COMMIT" "$C_GREEN" "$subject" "$C_RESET" \
104
+ "$C_DIM" "$files" "$C_RESET"
105
+ else
106
+ printf '%s %s%s%s\n' "$M_COMMIT" "$C_GREEN" "$subject" "$C_RESET"
107
+ fi
108
+ }
109
+
110
+ # emit_gate <state> <message> — the human-instruction line at a message rest.
111
+ emit_gate() {
112
+ local message="$2"
113
+ printf '%s %s%s%s\n' "$M_HUMAN" "$C_BOLD$C_YELLOW" "$message" "$C_RESET"
114
+ }
115
+
116
+ # emit_settled <state> — the terminal "nothing left to do" signal.
117
+ emit_settled() {
118
+ printf '%s %ssettled — nothing left to do%s\n' "$M_SETTLE" "$C_GREEN" "$C_RESET"
119
+ }
120
+
121
+ # emit_error <message> — refusals/stalls/failures, to stderr; <message> may be
122
+ # multi-line and is passed through verbatim.
123
+ emit_error() {
124
+ local message="$1"
125
+ printf '%s %s%s%s\n' "$M_ERROR" "$C_RED" "$message" "$C_RESET" >&2
126
+ }
127
+
128
+ # emit_log_pointer — the "see the log" line printed right after every
129
+ # emit_error call, to stderr: suppression must never hide a failure, so every
130
+ # error points at where the redirected subprocess output actually went.
131
+ emit_log_pointer() {
132
+ if [[ "$FANCY" == 1 ]]; then
133
+ printf '%s📄 see %s%s\n' "$C_DIM" "$GTD_LOOP_LOG" "$C_RESET" >&2
134
+ else
135
+ printf '%ssee %s%s\n' "$C_DIM" "$GTD_LOOP_LOG" "$C_RESET" >&2
136
+ fi
137
+ }
138
+
139
+ # ── Git-derivation helpers (parse_subject, report_commits) ──────────────────
140
+ # Additive: nothing below calls these yet. Replaces today's
141
+ # head_before/head_after string comparisons in a later change.
142
+
143
+ # parse_subject <subject> — sets S_ACTOR / S_FROM / S_TO / S_KIND. Patterns are
144
+ # held in variables (rather than inlined) so bash's [[ =~ ]] doesn't need the
145
+ # literal parens/spaces quoted inline.
146
+ parse_subject() {
147
+ local subject="$1"
148
+ local transition_re='^gtd\(([^)]+)\): (.+) → (.+)$'
149
+ local capture_re='^gtd\(([^)]+)\): (.+)$'
150
+ S_ACTOR="" S_FROM="" S_TO="" S_KIND=""
151
+ if [[ "$subject" =~ $transition_re ]]; then
152
+ S_KIND=transition
153
+ S_ACTOR="${BASH_REMATCH[1]}"
154
+ S_FROM="${BASH_REMATCH[2]}"
155
+ S_TO="${BASH_REMATCH[3]}"
156
+ elif [[ "$subject" =~ $capture_re ]]; then
157
+ S_KIND=capture
158
+ S_ACTOR="${BASH_REMATCH[1]}"
159
+ S_TO="${BASH_REMATCH[2]}"
160
+ else
161
+ S_KIND=squash
162
+ S_TO="$subject"
163
+ fi
164
+ }
165
+
166
+ # report_commits <head_before> — walks <head_before>..HEAD in order, emitting
167
+ # a transition/commit line per new commit; returns non-zero (having emitted
168
+ # nothing) when there were no new commits.
169
+ report_commits() {
170
+ local head_before="$1" sha subject files reported=0
171
+ while IFS= read -r sha; do
172
+ [[ -z "$sha" ]] && continue
173
+ reported=1
174
+ subject="$(git log -1 --format=%s "$sha")"
175
+ files="$(git diff-tree --no-commit-id --name-only -r "$sha" | paste -sd, - | sed 's/,/, /g')"
176
+ parse_subject "$subject"
177
+ case "$S_KIND" in
178
+ transition) emit_transition "$S_FROM" "$S_TO" "$files" ;;
179
+ capture | squash) emit_commit "$subject" "$files" ;;
180
+ esac
181
+ done < <(git rev-list --reverse "$head_before"..HEAD)
182
+ [[ "$reported" == 1 ]]
183
+ }
184
+
185
+ # ── Log file (resolve_log_path, gtd log) ────────────────────────────────────
186
+
187
+ # resolve_log_path — echoes the loop's logfile path: $GTD_LOOP_LOG verbatim if
188
+ # set, else "$(git rev-parse --git-dir)/gtd-loop.log" (per-repository AND
189
+ # per-worktree automatically, since a linked worktree's git-dir is its own
190
+ # private .git/worktrees/<name>).
191
+ resolve_log_path() {
192
+ if [[ -n "${GTD_LOOP_LOG:-}" ]]; then
193
+ echo "$GTD_LOOP_LOG"
194
+ else
195
+ echo "$(git rev-parse --git-dir)/gtd-loop.log"
196
+ fi
197
+ }
198
+
199
+ # --no-edit / GTD_NO_EDIT govern only the loop's automatic editor launching at
200
+ # human gates (see handle_human_gate further down) — `gtd edit` itself never
201
+ # reads either. `--no-edit` is recognised in exactly the two sanctioned
202
+ # positions (bare, or right after `loop`) and stripped before anything below
203
+ # looks at "$@", so the bundle's own unknown-option guard never sees it.
204
+ edit_disabled=0
205
+ [[ -n "${GTD_NO_EDIT:-}" ]] && edit_disabled=1
206
+ if [[ "${1:-}" == "--no-edit" && $# -eq 1 ]]; then
207
+ edit_disabled=1
208
+ shift
209
+ elif [[ "${1:-}" == "loop" && "${2:-}" == "--no-edit" && $# -eq 2 ]]; then
210
+ edit_disabled=1
211
+ set -- loop
212
+ fi
213
+
214
+ if [[ "${1:-}" == "edit" ]]; then
215
+ shift
216
+ if [[ $# -gt 0 ]]; then
217
+ launch_editor "$1"
218
+ else
219
+ if ! peek_json="$(run_gtd next --json 2>&1)"; then
220
+ echo "gtd edit: could not determine the next step:" >&2
221
+ echo "$peek_json" >&2
222
+ exit 1
223
+ fi
224
+ file="$(jq -r '.file // empty' <<<"$peek_json")"
225
+ launch_editor "${file:-.}"
226
+ fi
227
+ exit 0
228
+ fi
229
+
230
+ if [[ "${1:-}" == "log" ]]; then
231
+ shift
232
+ if [[ $# -gt 0 ]]; then
233
+ echo "gtd log: takes no arguments" >&2
234
+ exit 2
235
+ fi
236
+ log_path="$(resolve_log_path)"
237
+ if [[ ! -f "$log_path" ]]; then
238
+ echo "gtd log: no loop log yet at $log_path" >&2
239
+ exit 1
240
+ fi
241
+ launch_editor "$log_path"
242
+ exit 0
243
+ fi
244
+
28
245
  if [[ $# -gt 0 && "$1" != "loop" ]]; then
29
246
  run_gtd "$@"
30
247
  exit $?
@@ -33,12 +250,16 @@ if [[ "${1:-}" == "loop" && $# -gt 1 ]]; then
33
250
  echo "gtd: 'loop' takes no arguments" >&2
34
251
  exit 2
35
252
  fi
36
- # otherwise ($# -eq 0, or exactly "loop" with no further args): fall through
37
- # into the existing loop body below, unchanged in behavior.
253
+ # otherwise ($# -eq 0, or exactly "loop" with no further args, --no-edit
254
+ # already stripped above): fall through into the existing loop body below,
255
+ # unchanged in behavior.
38
256
 
39
257
  # Drives the v3 pattern-machine loop (see docs/loop.md): each iteration asks
40
258
  # `gtd next --json` who's up and what they should do, dispatching on `kind`:
41
- # - "message" (a human rest) -> halt, tell the user what gtd is waiting on.
259
+ # - "message" (a human rest) -> see handle_human_gate above: with editing on
260
+ # (the default) open the editor at the gate and step human, driving on a
261
+ # captured commit; with editing off (--no-edit/GTD_NO_EDIT), halt and tell
262
+ # the user what gtd is waiting on, unchanged from before this feature.
42
263
  # - "script" (a check rest) -> execute `content` (the emitted wrapper
43
264
  # script) verbatim via bash — exit code deliberately ignored, the outcome
44
265
  # lives in the tree — then `gtd step <actor>` to capture it; zero new
@@ -101,6 +322,60 @@ herdr_release() { # hand the pane back to Herdr's normal detection
101
322
  herdr_run pane release-agent "$HERDR_PANE_ID" --source herdr:gtd --agent gtd
102
323
  }
103
324
 
325
+ # Shared human-gate handler for a "message" (human) rest — used both by the
326
+ # opening move (when editing is on) and by every "message" rest the main loop
327
+ # encounters. $1 is the rest's `next --json` blob.
328
+ #
329
+ # Editing OFF ($edit_disabled): today's behavior, unchanged — render the gate
330
+ # via emit_gate and exit 0 without touching an editor.
331
+ #
332
+ # Editing ON (the default): open the editor on the rest's `.file` (or `.` when
333
+ # absent), wait for it to exit, then run `gtd step human`:
334
+ # - a NEW commit was authored -> return normally so the caller keeps driving.
335
+ # - ZERO commits were authored -> halt, exit 0 (closing the editor without a
336
+ # capturable change means "done for now" — and keeps a gate with no `C`
337
+ # pattern, e.g. idle, from being reopened forever).
338
+ # - the step REFUSES (non-zero exit: malformed steering file, unfinished
339
+ # review) -> surface it verbatim and halt, exit non-zero — the same
340
+ # wording an opening-move refusal already used before this function
341
+ # existed.
342
+ handle_human_gate() {
343
+ local json="$1" file label state step_out head_before head_after
344
+ label="$(jq -r '.label // .state' <<<"$json")"
345
+ state="$(jq -r .state <<<"$json")"
346
+
347
+ if [[ "$edit_disabled" == 1 ]]; then
348
+ herdr_report blocked "$label"
349
+ herdr_notify "gtd needs you — $(basename "$PWD")" "$label"
350
+ emit_gate "$state" "$label"
351
+ run_gtd next
352
+ exit 0
353
+ fi
354
+
355
+ file="$(jq -r '.file // empty' <<<"$json")"
356
+ herdr_report working "$label"
357
+ launch_editor "${file:-.}"
358
+
359
+ head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
360
+ if ! step_out="$(run_gtd step human 2>&1)"; then
361
+ emit_error "could not capture your changes at the human gate:
362
+ $step_out
363
+ Fix the reported issue and re-run gtd-loop."
364
+ emit_log_pointer
365
+ exit 1
366
+ fi
367
+ head_after="$(git rev-parse HEAD 2>/dev/null || echo none)"
368
+ if [[ "$head_before" == "$head_after" ]]; then
369
+ herdr_report idle "$label"
370
+ herdr_release
371
+ emit_action "$M_HUMAN" "done for now (nothing captured)"
372
+ exit 0
373
+ fi
374
+ # A new commit was captured — render it, then return normally so the
375
+ # caller keeps driving.
376
+ report_commits "$head_before"
377
+ }
378
+
104
379
  trap 'code=$?; if [ "$code" -ne 0 ]; then
105
380
  herdr_report blocked "${GTD_LAST_LABEL:-stopped}: exited $code"
106
381
  herdr_notify "gtd stopped — $(basename "$PWD")" "${GTD_LAST_LABEL:-loop} failed (exit $code)"
@@ -112,6 +387,17 @@ prev_content=""
112
387
  # across iterations and across restarts. In the git dir, never the work tree.
113
388
  memory_marker="$(git rev-parse --git-dir 2>/dev/null || echo .git)/gtd-loop-memory"
114
389
 
390
+ # Truncated once here (not every iteration), so the file always reflects the
391
+ # current run but survives after the loop exits for a later `gtd log`.
392
+ log_path="$(resolve_log_path)"
393
+ : >"$log_path"
394
+ export GTD_LOOP_LOG="$log_path"
395
+ if [[ "$FANCY" == 1 ]]; then
396
+ printf '%s📄 log: %s (tail -f to follow)%s\n' "$C_DIM" "$log_path" "$C_RESET"
397
+ else
398
+ printf '%slog: %s (tail -f to follow)%s\n' "$C_DIM" "$log_path" "$C_RESET"
399
+ fi
400
+
115
401
  # Swappable agent adapter: GTD_LOOP_AGENT_CMD lets any coding agent CLI stand
116
402
  # in for the default, receiving the prompt via $GTD_LOOP_PROMPT, the state's
117
403
  # opaque model hint (if any) via $GTD_LOOP_MODEL, and the memory scope via
@@ -137,7 +423,8 @@ run_agent_turn() {
137
423
  cmd="$cmd"' --dangerously-skip-permissions'
138
424
  fi
139
425
  GTD_LOOP_PROMPT="$prompt" GTD_LOOP_MODEL="$model" GTD_LOOP_MEMORY="$memory" \
140
- GTD_LOOP_SESSION_ID="$session_id" GTD_LOOP_MEMORY_RESUME="$do_resume" bash -c "$cmd"
426
+ GTD_LOOP_SESSION_ID="$session_id" GTD_LOOP_MEMORY_RESUME="$do_resume" bash -c "$cmd" \
427
+ >>"${GTD_LOOP_LOG:-/dev/null}" 2>&1
141
428
  }
142
429
 
143
430
  # Opening move: capture the human's pending edit so gtd-loop is the ONLY command
@@ -154,18 +441,30 @@ run_agent_turn() {
154
441
  # a harmless no-op; a genuine refusal there (a malformed steering file, or an
155
442
  # edit no `on` pattern accepts) is surfaced and stops the loop rather than being
156
443
  # driven past.
444
+ #
445
+ # Editing ON: route through the exact same gate logic the main loop uses below
446
+ # (open the editor there first, THEN step) instead of blindly stepping human
447
+ # and moving on. Editing OFF: unchanged from before this function existed —
448
+ # step silently, refusal surfaced and halts, a clean/committed gate just falls
449
+ # into the loop below.
157
450
  if ! peek_json="$(run_gtd next --json 2>&1)"; then
158
- echo "gtd-loop: could not determine the next step:" >&2
159
- echo "$peek_json" >&2
160
- echo "Run \`gtd status\` to inspect the repo." >&2
451
+ emit_error "could not determine the next step:
452
+ $peek_json
453
+ Run \`gtd status\` to inspect the repo."
454
+ emit_log_pointer
161
455
  exit 1
162
456
  fi
163
457
  if [[ "$(jq -r .kind <<<"$peek_json")" == "message" ]]; then
164
- if ! step_out="$(run_gtd step human 2>&1)"; then
165
- echo "gtd-loop: could not capture your changes at the human gate:" >&2
166
- echo "$step_out" >&2
167
- echo "Fix the reported issue and re-run gtd-loop." >&2
168
- exit 1
458
+ if [[ "$edit_disabled" == 1 ]]; then
459
+ if ! step_out="$(run_gtd step human 2>&1)"; then
460
+ emit_error "could not capture your changes at the human gate:
461
+ $step_out
462
+ Fix the reported issue and re-run gtd-loop."
463
+ emit_log_pointer
464
+ exit 1
465
+ fi
466
+ else
467
+ handle_human_gate "$peek_json"
169
468
  fi
170
469
  fi
171
470
 
@@ -186,31 +485,34 @@ while true; do
186
485
  label="$(jq -r '.label // .state' <<<"$next_json")"
187
486
  GTD_LAST_LABEL="$label"
188
487
 
189
- if [[ "$kind" != "message" ]]; then
190
- herdr_report working "$label"
191
- fi
192
-
193
488
  if [[ "$kind" == "message" ]]; then
194
- herdr_report blocked "$label"
195
- herdr_notify "gtd needs you — $(basename "$PWD")" "$label"
196
- echo "--- Your turn ($state) ---"
197
- run_gtd next
198
- exit 0
489
+ # handle_human_gate exits 0/non-zero itself on a halt/refusal; it only
490
+ # returns here when a new commit was captured, so we keep driving.
491
+ handle_human_gate "$next_json"
492
+ continue
199
493
  fi
200
494
 
495
+ herdr_report working "$label"
496
+
201
497
  if [[ "$kind" == "script" ]]; then
498
+ emit_action "$M_CHECK" "$label"
202
499
  head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
203
500
  # Execute the emitted wrapper verbatim; its exit code is deliberately
204
501
  # ignored (`|| true` under `set -e`) — the script encodes its outcome in
205
502
  # the tree, and `gtd step` captures whatever it left there via the state's
206
- # own `on` patterns.
207
- bash -c "$content" || true
208
- run_gtd step "$actor" >/dev/null
209
- head_after="$(git rev-parse HEAD 2>/dev/null || echo none)"
210
- if [[ "$head_before" == "$head_after" ]]; then
503
+ # own `on` patterns. Its own output goes to the log, not the terminal.
504
+ bash -c "$content" >>"$GTD_LOOP_LOG" 2>&1 || true
505
+ if ! step_out="$(run_gtd step "$actor" 2>&1)"; then
506
+ printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
507
+ emit_error "$step_out"
508
+ emit_log_pointer
509
+ exit 1
510
+ fi
511
+ printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
512
+ if ! report_commits "$head_before"; then
211
513
  herdr_report idle "$label"
212
514
  herdr_release
213
- echo "--- Settled ($state: check passed, nothing to do) ---"
515
+ emit_settled "$state"
214
516
  exit 0
215
517
  fi
216
518
  prev_state=""
@@ -222,8 +524,10 @@ while true; do
222
524
  # agent-owned state and content repeating means the last dispatch made no
223
525
  # progress; stop rather than spin on it.
224
526
  if [[ "$state" == "$prev_state" && "$content" == "$prev_content" ]]; then
225
- echo "gtd-loop: no progress at '$state' — the agent's last turn changed nothing. Stopping to avoid spinning." >&2
226
- run_gtd next >&2
527
+ next_out="$(run_gtd next 2>&1)" || true
528
+ emit_error "no progress at '$state' — the agent's last turn changed nothing. Stopping to avoid spinning.
529
+ $next_out"
530
+ emit_log_pointer
227
531
  exit 1
228
532
  fi
229
533
  prev_state="$state"
@@ -263,23 +567,37 @@ while true; do
263
567
  # cap) before stepping — the plain-`gtd next` self-validation instruction's
264
568
  # driver-side equivalent, so the next rest is only ever handed a well-formed
265
569
  # file. gtd itself never validates at step time — this is the loop's job.
570
+ emit_action "$M_AGENT" "$label"
571
+ head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
266
572
  run_agent_turn "$content" "$resume"
267
573
 
268
574
  fix_attempt=0
269
575
  while ! validate_out="$(run_gtd validate 2>&1)"; do
270
576
  fix_attempt=$((fix_attempt + 1))
271
577
  if ((fix_attempt > 3)); then
272
- echo "gtd-loop: '$state' still fails \`gtd validate\` after 3 fix attempts:" >&2
273
- echo "$validate_out" >&2
274
- echo "Stopping rather than stepping with a malformed steering file." >&2
578
+ emit_error "'$state' still fails \`gtd validate\` after 3 fix attempts:
579
+ $validate_out
580
+ Stopping rather than stepping with a malformed steering file."
581
+ emit_log_pointer
275
582
  exit 1
276
583
  fi
277
584
  # Resume the same session when there is one (the agent just wrote the file);
278
585
  # otherwise a fresh invocation still gets the findings via the prompt.
279
586
  fix_resume=0
280
587
  [[ -n "$session_id" ]] && fix_resume=1
588
+ emit_action "$M_FIX" "attempt $fix_attempt"
281
589
  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"
282
590
  done
283
591
 
284
- run_gtd step "$actor" >/dev/null
592
+ if ! step_out="$(run_gtd step "$actor" 2>&1)"; then
593
+ printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
594
+ emit_error "$step_out"
595
+ emit_log_pointer
596
+ exit 1
597
+ fi
598
+ printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
599
+ # A no-op step (nothing matched a declared pattern on a clean tree) reports
600
+ # zero commits here — report_commits returns non-zero, and that's fine: the
601
+ # NEXT iteration's stall check is what catches unchanged state/content.
602
+ report_commits "$head_before" || true
285
603
  done
@@ -205428,6 +205428,10 @@ Commands:
205428
205428
  an agent through gtd next/gtd step calls until the
205429
205429
  workflow returns to its initial state again. A bare gtd
205430
205430
  invocation and gtd loop both launch it identically
205431
+ edit [path] Open \${VISUAL:-$EDITOR} on <path>, or — with no path — on
205432
+ the resolved rest's declared file (or the repo directory
205433
+ if it declares none). Handled entirely by bin/gtd, not
205434
+ this bundle
205431
205435
  init Scaffold a minimal .gtdrc.json for this repo, seeding the
205432
205436
  default variables you are most likely to change (the test
205433
205437
  command) and a Prettier formatting suggestion. gtd runs its
package/docs/cli.md CHANGED
@@ -45,6 +45,10 @@ Commands:
45
45
  commits that would be lost
46
46
  next Print the resolved rest's rendered script/prompt/message
47
47
  (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/GTD_NO_EDIT
48
52
  status Print the resolved rest's state/actor and which declared
49
53
  pattern (if any) each pending change matches (no mutation)
50
54
  validate Format and validate the steering file the resolved rest
@@ -63,6 +67,8 @@ Options:
63
67
  --no-open (gtd visualize only) do not open the browser
64
68
  --cost=<n> (gtd step only) record the invocation's token cost
65
69
  --model=<name> (gtd step only, with --cost) tag that cost's model
70
+ --no-edit (bare gtd or gtd loop only) disable the loop's automatic
71
+ editor launching at human gates
66
72
  --version, -v Print version and exit
67
73
  --help, -h Print this help and exit
68
74
  ```
@@ -79,12 +85,32 @@ history relative to cwd, so it refuses with a clear error if invoked from a
79
85
  subdirectory.
80
86
 
81
87
  `--json`, `--cost=<n>`, and `--model=<name>` (the latter two only for
82
- `gtd step`) are the only long options. Any other `--` option (including a typo
83
- like `--jsn`) is rejected with a usage error rather than silently ignored, so a
84
- mistyped flag can never degrade a JSON caller to plain-text mode. A bare
85
- `--cost`/`--model` with no value, a non-numeric or negative `--cost`, an empty
86
- `--model`, `--model` without `--cost`, or either flag on any command other than
87
- `gtd step` are all usage errors.
88
+ `gtd step`) are the only long options the compiled bundle recognizes. Any other
89
+ `--` option (including a typo like `--jsn`) is rejected with a usage error
90
+ rather than silently ignored, so a mistyped flag can never degrade a JSON caller
91
+ to plain-text mode. A bare `--cost`/`--model` with no value, a non-numeric or
92
+ negative `--cost`, an empty `--model`, `--model` without `--cost`, or either
93
+ flag on any command other than `gtd step` are all usage errors.
94
+
95
+ `--no-edit` is a separate, bash-level flag handled entirely by `bin/gtd` itself,
96
+ stripped before anything reaches the bundle — see [`--no-edit`](#--no-edit)
97
+ below.
98
+
99
+ ## `--no-edit`
100
+
101
+ A flag on the loop driver only — bare `gtd --no-edit` or `gtd loop --no-edit`
102
+ (the two are equivalent, since bare `gtd` and `gtd loop` are themselves
103
+ equivalent). Recognized in exactly those two positions; any other
104
+ placement/combination (e.g. `gtd step --no-edit`, or combined with any other
105
+ argument) is a usage error. The `GTD_NO_EDIT` environment variable (any
106
+ non-empty value) does the same thing.
107
+
108
+ Disables the loop's default behavior of launching an editor at human gates,
109
+ restoring the previous halt-and-print-and-exit behavior with no editor involved.
110
+ It governs only the loop's own automatic launching — `gtd edit` invoked directly
111
+ never reads it (see [`gtd edit`](#gtd-edit-path) above). See
112
+ [Driving the loop](loop.md) and `skills/loop/SKILL.md` for the full gate-flow
113
+ description.
88
114
 
89
115
  ## `gtd init`
90
116
 
@@ -408,6 +434,40 @@ which are JSON-only. `--json` emits
408
434
  when the state has no `on` (a commit state); a per-edge `describe` is likewise
409
435
  omitted when that edge declares none.
410
436
 
437
+ ## `gtd edit [path]`
438
+
439
+ Handled entirely in `bin/gtd`'s own bash — unlike every other subcommand, it is
440
+ **never forwarded** to the compiled bundle (`dist/gtd.bundle.mjs`).
441
+
442
+ With `<path>` given (repo-relative), opens it in `${VISUAL:-$EDITOR}` (git's own
443
+ precedence — `$VISUAL` first, then `$EDITOR`; no fallback to `vi`), blocking in
444
+ the foreground until the editor exits. It creates nothing if `<path>` doesn't
445
+ exist — the path is handed straight to the editor command verbatim. The editor's
446
+ own exit code is ignored entirely; success is judged from tree state afterward,
447
+ not from this command.
448
+
449
+ With no argument, it peeks with `gtd next --json` to find the resolved rest's
450
+ declared `.file`, and opens that (repo-relative), or the repo directory (`.`)
451
+ when the resolved state declares no `file:`. If `gtd next --json` itself fails
452
+ (e.g. not in a repo), it prints an error and exits non-zero without attempting
453
+ to open an editor:
454
+
455
+ ```
456
+ gtd edit: could not determine the next step:
457
+ <gtd next --json's own error output>
458
+ ```
459
+
460
+ `gtd edit` **never** reads `--no-edit`/`GTD_NO_EDIT` — those two govern only the
461
+ loop's own automatic editor launching at human gates (see
462
+ [Driving the loop](loop.md) and `skills/loop/SKILL.md`), not this command.
463
+ Invoked directly, `gtd edit` always launches, unconditionally.
464
+
465
+ If neither `$VISUAL` nor `$EDITOR` is set, no editor is launched:
466
+
467
+ ```
468
+ gtd: no editor configured — set $EDITOR (or $VISUAL)
469
+ ```
470
+
411
471
  ## Running `script` rests (no `gtd` subcommand)
412
472
 
413
473
  There is no `gtd` subcommand that executes a workflow script — gtd never runs a
package/docs/loop.md CHANGED
@@ -41,16 +41,35 @@ loop picks their change up.
41
41
 
42
42
  **A `"message"` rest is a real handoff, not a slow beat.** No driver signs off
43
43
  for the human: in the bundled workflow the loop runs the whole cycle unattended
44
- up to `await-review` and then STOPS there with the review checkout window open
44
+ up to `await-review` and then rests there with the review checkout window open
45
45
  (the reviewable diff surfaced as uncommitted changes — see
46
46
  [STATES.md §11](../STATES.md#11-the-review-checkout-window)). Advancing takes a
47
47
  human edit to `.gtd/REVIEW.md`: ticking every box with **no** comment signs off
48
48
  into the squash finale, while any comment (a note, or a code edit of your own)
49
- is feedback that routes into another build + re-review round. Re-launching the
50
- loop after that edit picks the decision up. Until then the loop halts on every
51
- run — that is the contract, not a stall. A review nobody is going to finish is
52
- ended with [`gtd abandon`](cli.md#gtd-abandon---json), which drops the process's
53
- commits and keeps their content as uncommitted changes.
49
+ is feedback that routes into another build + re-review round. That gate is the
50
+ contract, not a stall — nothing advances until the edit is captured, whether the
51
+ capture happens within the same run or a later one. A review nobody is going to
52
+ finish is ended with [`gtd abandon`](cli.md#gtd-abandon---json), which drops the
53
+ process's commits and keeps their content as uncommitted changes.
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`'s `--no-edit`](cli.md#--no-edit) 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.
54
73
 
55
74
  ```bash
56
75
  gtd next --json # ask who's up and what they should do
@@ -73,16 +92,18 @@ when the machine rests at a `"message"` gate — so `gtd` is the only command a
73
92
  human runs. `bin/gtd` is the packaged entry point: invoked bare, or with `loop`
74
93
  as its first argument, it runs this exact script; any other first argument (e.g.
75
94
  `next`, `step`, `status`) hands off to `node dist/gtd.bundle.mjs` instead. The
76
- loop body it runs adds four things on top of the reference script above: it
77
- stops with a diagnostic if the same `"prompt"` state/content repeat with no
78
- progress (see `skills/loop/SKILL.md`'s "Stall detection"); it lets
79
- `GTD_LOOP_AGENT_CMD` swap in any coding agent CLI in place of the default
80
- `claude -p`, receiving the prompt via `$GTD_LOOP_PROMPT`; it exports the
81
- resolved state's optional `model` hint as `$GTD_LOOP_MODEL`, appending
82
- `--model "$GTD_LOOP_MODEL"` to the default `claude -p` invocation whenever it's
83
- non-empty; and it acts on the optional `memory` scope hint (see "Agent memory
84
- scope" below) — continuing one agent session across consecutive same-scope turns
85
- and starting fresh when the scope changes.
95
+ loop body it runs adds five things on top of the reference script above: it
96
+ opens an editor at every `"message"` gate instead of just printing it (see
97
+ "Editing is on by default" above; `--no-edit`/`GTD_NO_EDIT` fall back to the
98
+ reference script's plain print-and-exit); it stops with a diagnostic if the same
99
+ `"prompt"` state/content repeat with no progress (see `skills/loop/SKILL.md`'s
100
+ "Stall detection"); it lets `GTD_LOOP_AGENT_CMD` swap in any coding agent CLI in
101
+ place of the default `claude -p`, receiving the prompt via `$GTD_LOOP_PROMPT`;
102
+ it exports the resolved state's optional `model` hint as `$GTD_LOOP_MODEL`,
103
+ appending `--model "$GTD_LOOP_MODEL"` to the default `claude -p` invocation
104
+ whenever it's non-empty; and it acts on the optional `memory` scope hint (see
105
+ "Agent memory scope" below) — continuing one agent session across consecutive
106
+ same-scope turns and starting fresh when the scope changes.
86
107
 
87
108
  `bin/gtd` resolves both its bundle hand-off and the loop's own internal `gtd`
88
109
  calls against `dist/gtd.bundle.mjs` next to its own location. Set `GTD_BIN` — a
@@ -116,7 +137,7 @@ while true; do
116
137
  content="$(jq -r .content <<<"$next_json")"
117
138
 
118
139
  if [[ "$kind" == "message" ]]; then
119
- echo "--- Your turn ($state) ---"
140
+ echo "your turn ($state)" # bin/gtd renders this via emit_gate instead
120
141
  gtd next
121
142
  exit 0
122
143
  fi
@@ -127,7 +148,7 @@ while true; do
127
148
  gtd step "$actor" >/dev/null # capture whatever it left in the tree
128
149
  head_after="$(git rev-parse HEAD 2>/dev/null || echo none)"
129
150
  if [[ "$head_before" == "$head_after" ]]; then
130
- echo "--- Settled ($state: check passed, nothing to do) ---"
151
+ echo "settled ($state: check passed, nothing to do)" # bin/gtd renders this via emit_settled instead
131
152
  exit 0
132
153
  fi
133
154
  continue
@@ -143,6 +164,29 @@ The driver — not the prompt text — owns ending the agent's turn
143
164
  (`gtd step "$actor"` right after the agent acts): every default-workflow agent
144
165
  prompt says explicitly not to run `gtd step agent` itself.
145
166
 
167
+ `bin/gtd log` opens the current repo/worktree's loop logfile —
168
+ `"$(git rev-parse --git-dir)/gtd-loop.log"` by default, or `$GTD_LOOP_LOG`
169
+ verbatim when set — in `${VISUAL:-$EDITOR}`, the same editor resolution
170
+ `bin/gtd edit` uses. It takes no arguments and errors out (rather than opening
171
+ an empty buffer) if no loop has produced a log yet.
172
+
173
+ ## Rendered output and logging
174
+
175
+ `bin/gtd` detects its output capability once at startup (`FANCY`, true when
176
+ stdout is a real terminal AND `NO_COLOR` is unset): on a real terminal it prints
177
+ one colored/emoji line per event, and under `NO_COLOR` or a piped/redirected
178
+ stdout it prints the same events as plain ASCII with no escape codes, per the
179
+ [NO_COLOR convention](https://no-color.org). Normal events (a state transition,
180
+ a captured commit, an agent/check turn starting, settling at idle) render to
181
+ stdout; refusals, stalls, and validate-cap halts render to stderr, each followed
182
+ by a `📄 see <log>` pointer (plain: `see <log>`).
183
+
184
+ The three previously terminal-visible subprocess streams — the agent turn's own
185
+ output, the check script's own output, and `gtd step`'s own output — are no
186
+ longer printed directly: they're appended to the per-repo/per-worktree log file
187
+ (truncated once at the start of the run) instead, and surfaced with
188
+ `bin/gtd log` above.
189
+
146
190
  ## Agent memory scope
147
191
 
148
192
  A `"prompt"` beat may carry an optional `memory` key — an opaque scope label the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "7.4.2",
3
+ "version": "7.6.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": {