@pmelab/gtd 7.11.2 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/gtd CHANGED
@@ -3,34 +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: naming both together is a dedicated usage error,
12
- # exit 2), `--once` restricts the loop to exactly one beat
13
- # (one human gate, one script check+step, or one agent prompt+step) before
14
- # exiting, and `--no-notify` (or `GTD_NO_NOTIFY`) suppresses the loop's
15
- # Herdr desktop notifications for this run while leaving its silent Herdr
16
- # pane-state reporting untouched. Dispatch then continues on the
17
- # remaining arguments as if none of them had ever been given.
18
- # - `edit [path]` -> handled entirely here in bash, never forwarded to the
19
- # bundle: opens `${VISUAL:-$EDITOR}` on `path`, or (no `path` given) on the
20
- # resolved rest's `.file` (or `.` when absent). Low-level plumbing now that
21
- # `--edit` covers the common "force an edit, then keep driving" case; this
22
- # subcommand remains for opening a specific path, or the current rest's
23
- # file, WITHOUT driving the loop afterward.
24
- # - `log` -> handled entirely here in bash, never forwarded to the bundle:
25
- # opens `${VISUAL:-$EDITOR}` on the current repo/worktree's loop logfile
26
- # (see `resolve_log_path`), refusing rather than opening an empty buffer
27
- # when no loop has produced one yet.
28
- # - no arguments, or exactly `loop` with no further arguments (other than the
29
- # flags above) -> fall through into the loop body below (the loop protocol
30
- # 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).
31
12
  # - `loop` with an unrecognised extra argument -> usage error, exit 2.
32
13
  # - anything else -> hand off to the bundle via `run_gtd`, forwarding all
33
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.
34
19
  # `run_gtd` is the ONE place that knows how to invoke "the bundle": by default
35
20
  # it resolves `dist/gtd.bundle.mjs` relative to this script's own location
36
21
  # (not cwd), but `GTD_BIN` (a dev/test override for the whole invocation
@@ -48,29 +33,11 @@ run_gtd() {
48
33
  fi
49
34
  }
50
35
 
51
- # Shared editor-launch helper — used by both the `edit` subcommand below and
52
- # the loop's human-gate flow further down. Resolves the editor git-style
53
- # ($VISUAL, then $EDITOR — no fallback to `vi`), then launches it blocking in
54
- # the foreground with the target path passed as a positional parameter
55
- # (never string-spliced into the command), so a multi-word command
56
- # (`code --wait`) and a path with special characters both work. A launch
57
- # failure (bad/missing editor binary) is a plain non-zero return, propagating
58
- # under `set -e`; the editor's own exit code is otherwise ignored entirely —
59
- # the outcome is judged from tree state afterward, not from this function.
60
- launch_editor() {
61
- local target="$1" editor="${VISUAL:-${EDITOR:-}}"
62
- if [[ -z "$editor" ]]; then
63
- echo "gtd: no editor configured — set \$EDITOR (or \$VISUAL)" >&2
64
- return 1
65
- fi
66
- bash -c "$editor \"\$1\"" gtd-edit "$target"
67
- }
68
-
69
36
  # ── Rendering primitives (capability detection, palette/markers, emitters) ──
70
- # Additive: nothing below calls these yet. `FANCY=1` only when stdout (fd 1)
71
- # is a real terminal AND `NO_COLOR` is unset — any pipe/redirect/non-tty
72
- # stdout, or `NO_COLOR` set to any value, gives plain ASCII with no escape
73
- # 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).
74
41
  if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then FANCY=1; else FANCY=0; fi
75
42
 
76
43
  if [[ "$FANCY" == 1 ]]; then
@@ -158,8 +125,6 @@ emit_log_pointer() {
158
125
  }
159
126
 
160
127
  # ── Git-derivation helpers (parse_subject, report_commits) ──────────────────
161
- # Additive: nothing below calls these yet. Replaces today's
162
- # head_before/head_after string comparisons in a later change.
163
128
 
164
129
  # parse_subject <subject> — sets S_ACTOR / S_FROM / S_TO / S_KIND. Patterns are
165
130
  # held in variables (rather than inlined) so bash's [[ =~ ]] doesn't need the
@@ -209,7 +174,7 @@ report_commits() {
209
174
  [[ "$reported" == 1 ]]
210
175
  }
211
176
 
212
- # ── Log file (resolve_log_path, gtd log) ────────────────────────────────────
177
+ # ── Log file (resolve_log_path) ─────────────────────────────────────────────
213
178
 
214
179
  # worktree_git_dir — the git dir of the working tree gtd is operating in,
215
180
  # derived purely from the cwd. GIT_DIR/GIT_WORK_TREE are scrubbed for this one
@@ -233,27 +198,16 @@ resolve_log_path() {
233
198
  fi
234
199
  }
235
200
 
236
- # --no-edit / --edit (-e) / --once / --no-notify govern only the loop driver,
237
- # recognised in exactly two positions — bare (e.g. `gtd --edit --once`) or
238
- # immediately after `loop` (e.g. `gtd loop --once`) — in any order, each at
239
- # most once. `--edit` and `--no-edit` are mutually exclusive (forcing and
240
- # suppressing the editor in the same run is not a coherent request): naming
241
- # both, with no other unrecognised token present, is rejected right here with
242
- # a dedicated usage error (exit 2) — it never reaches the bundle's own
243
- # unknown-option guard. `--no-notify` conflicts with nothing (it's orthogonal
244
- # to editing and to `--once`). A recognised, valid combination is stripped
245
- # here before anything below looks at "$@", so the bundle's own unknown-option
246
- # guard never sees it; anything else (an unknown flag, a duplicate flag, or a
247
- # flag mixed with a real subcommand) is left untouched and falls through to
248
- # the existing dispatch below, which forwards a non-flag first argument to the
249
- # bundle (whose own unknown-option guard rejects it) or rejects a `loop`
250
- # invocation carrying an unrecognised extra argument.
251
- edit_disabled=0
252
- 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.
253
210
  once_mode=0
254
- notify_disabled=0
255
- [[ -n "${GTD_NO_EDIT:-}" ]] && edit_disabled=1
256
- [[ -n "${GTD_NO_NOTIFY:-}" ]] && notify_disabled=1
257
211
 
258
212
  if [[ "${1:-}" == "loop" ]]; then
259
213
  loop_form=1
@@ -264,39 +218,17 @@ else
264
218
  fi
265
219
 
266
220
  valid_combo=1
267
- saw_unknown=0
268
- n_no_edit=0
269
- n_edit=0
270
221
  n_once=0
271
- n_no_notify=0
272
222
  for a in ${loop_flag_args[@]+"${loop_flag_args[@]}"}; do
273
223
  case "$a" in
274
- --no-edit) n_no_edit=$((n_no_edit + 1)) ;;
275
- --edit | -e) n_edit=$((n_edit + 1)) ;;
276
224
  --once) n_once=$((n_once + 1)) ;;
277
- --no-notify) n_no_notify=$((n_no_notify + 1)) ;;
278
- *) saw_unknown=1 ;;
225
+ *) valid_combo=0 ;;
279
226
  esac
280
227
  done
281
- # A pure loop-flag invocation (no unrecognised token) naming both --edit and
282
- # --no-edit gets a dedicated usage error here, rather than falling through to
283
- # the bundle's own unknown-option rejection below.
284
- if [[ "$saw_unknown" == 0 ]] && ((n_edit >= 1 && n_no_edit >= 1)); then
285
- echo "gtd: --edit and --no-edit are mutually exclusive" >&2
286
- exit 2
287
- fi
288
- if ((saw_unknown == 1 || n_no_edit > 1 || n_edit > 1 || n_once > 1 || n_no_notify > 1)); then
289
- valid_combo=0
290
- fi
228
+ ((n_once > 1)) && valid_combo=0
291
229
 
292
- if [[ "$valid_combo" == 1 ]]; then
293
- ((n_no_edit >= 1)) && edit_disabled=1
294
- ((n_edit >= 1)) && edit_forced=1
295
- ((n_once >= 1)) && once_mode=1
296
- ((n_no_notify >= 1)) && notify_disabled=1
297
- # --edit overrides an ambient GTD_NO_EDIT/--no-edit for THIS run — the
298
- # explicit ask to force the editor wins over the suppressing default.
299
- ((edit_forced)) && edit_disabled=0
230
+ if [[ "$valid_combo" == 1 && "$n_once" == 1 ]]; then
231
+ once_mode=1
300
232
  if [[ "$loop_form" == 1 ]]; then
301
233
  set -- loop
302
234
  else
@@ -304,37 +236,6 @@ if [[ "$valid_combo" == 1 ]]; then
304
236
  fi
305
237
  fi
306
238
 
307
- if [[ "${1:-}" == "edit" ]]; then
308
- shift
309
- if [[ $# -gt 0 ]]; then
310
- launch_editor "$1"
311
- else
312
- if ! peek_json="$(run_gtd next --json 2>&1)"; then
313
- echo "gtd edit: could not determine the next step:" >&2
314
- echo "$peek_json" >&2
315
- exit 1
316
- fi
317
- file="$(jq -r '.file // empty' <<<"$peek_json")"
318
- launch_editor "${file:-.}"
319
- fi
320
- exit 0
321
- fi
322
-
323
- if [[ "${1:-}" == "log" ]]; then
324
- shift
325
- if [[ $# -gt 0 ]]; then
326
- echo "gtd log: takes no arguments" >&2
327
- exit 2
328
- fi
329
- log_path="$(resolve_log_path)"
330
- if [[ ! -f "$log_path" ]]; then
331
- echo "gtd log: no loop log yet at $log_path" >&2
332
- exit 1
333
- fi
334
- launch_editor "$log_path"
335
- exit 0
336
- fi
337
-
338
239
  if [[ $# -gt 0 && "$1" != "loop" ]]; then
339
240
  run_gtd "$@"
340
241
  exit $?
@@ -343,16 +244,14 @@ if [[ "${1:-}" == "loop" && $# -gt 1 ]]; then
343
244
  echo "gtd: 'loop' takes no arguments" >&2
344
245
  exit 2
345
246
  fi
346
- # otherwise ($# -eq 0, or exactly "loop" with no further args, --no-edit
347
- # already stripped above): fall through into the existing loop body below,
348
- # 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.
349
249
 
350
- # Drives the v3 pattern-machine loop (see docs/loop.md): each iteration asks
250
+ # Drives the v3 pattern-machine loop: each iteration asks
351
251
  # `gtd next --json` who's up and what they should do, dispatching on `kind`:
352
- # - "message" (a human rest) -> see handle_human_gate above: with editing on
353
- # (the default) open the editor at the gate and step human, driving on a
354
- # captured commit; with editing off (--no-edit/GTD_NO_EDIT), halt and tell
355
- # 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.
356
255
  # - "script" (a check rest) -> execute `content` (the emitted wrapper
357
256
  # script) verbatim via bash — exit code deliberately ignored, the outcome
358
257
  # lives in the tree — then `gtd step <actor>` to capture it; zero new
@@ -365,8 +264,7 @@ fi
365
264
  # adapter invoked at a "prompt" rest; the default adapter appends `--model
366
265
  # "$GTD_LOOP_MODEL"` only when it's non-empty.
367
266
  #
368
- # `.memory` (optional, opaque — see skills/loop/SKILL.md "Agent memory scope")
369
- # is the agent-memory scope label. gtd never interprets it; the driver does:
267
+ # `.memory` (optional, opaque) is the agent-memory scope label. gtd never interprets it; the driver does:
370
268
  # consecutive "prompt" turns emitting the SAME label continue ONE agent
371
269
  # session (so the agent still remembers its previous turn), and a changed
372
270
  # label — or the first turn, or an unset label — starts fresh. That is what
@@ -379,109 +277,6 @@ fi
379
277
  # resume-or-fresh flag are exported as $GTD_LOOP_MEMORY / $GTD_LOOP_SESSION_ID
380
278
  # / $GTD_LOOP_MEMORY_RESUME; the default adapter turns them into
381
279
  # `--resume`/`--session-id`. An adapter that ignores them keeps working.
382
- herdr_ok() { [ "${HERDR_ENV:-}" = 1 ] && [ -n "${HERDR_PANE_ID:-}" ] && command -v herdr >/dev/null 2>&1; }
383
-
384
- # Run a `herdr` invocation best-effort. Normally output is discarded and a
385
- # failure is swallowed (`|| true`) so a missing/incompatible herdr never blocks
386
- # the loop. Set GTD_HERDR_DEBUG=1 to instead surface every call and its exit
387
- # code on stderr — the escape hatch that makes a silent CLI-signature mismatch
388
- # (e.g. an arg herdr rejects) visible again.
389
- herdr_run() {
390
- if [ "${GTD_HERDR_DEBUG:-}" = 1 ]; then
391
- printf 'gtd/herdr: %s\n' "$*" >&2
392
- herdr "$@" >&2
393
- printf 'gtd/herdr: exit=%s\n' "$?" >&2
394
- else
395
- herdr "$@" >/dev/null 2>&1 || true
396
- fi
397
- }
398
-
399
- # NOTE: herdr requires the positional <PANE_ID> BEFORE the options for
400
- # `pane` subcommands — passing it last makes herdr reject the first option as
401
- # `unknown option` (exit 2). Keep the pane id first here.
402
- herdr_report() { # $1=state (working|blocked|idle) $2=message
403
- herdr_ok || return 0
404
- herdr_run pane report-agent "$HERDR_PANE_ID" \
405
- --source herdr:gtd --agent gtd --state "$1" --message "$2"
406
- }
407
-
408
- # Suppressible via GTD_NO_NOTIFY/--no-notify (set into $notify_disabled at
409
- # parse time, above) — the OS popup this call makes is the only thing that
410
- # opts out; herdr_report/herdr_release (silent sidebar status) are unaffected.
411
- herdr_notify() { # $1=title $2=body
412
- [[ "$notify_disabled" == 1 ]] && return 0
413
- herdr_ok || return 0
414
- herdr_run notification show "$1" --body "$2" --sound request
415
- }
416
-
417
- herdr_release() { # hand the pane back to Herdr's normal detection
418
- herdr_ok || return 0
419
- herdr_run pane release-agent "$HERDR_PANE_ID" --source herdr:gtd --agent gtd
420
- }
421
-
422
- # Shared human-gate handler for a "message" (human) rest — used both by the
423
- # opening move (when editing is on) and by every "message" rest the main loop
424
- # encounters. $1 is the rest's `next --json` blob.
425
- #
426
- # Editing OFF ($edit_disabled): today's behavior, unchanged — render the gate
427
- # via emit_gate and exit 0 without touching an editor.
428
- #
429
- # Editing ON (the default): open the editor on the rest's `.file` (or `.` when
430
- # absent), wait for it to exit, then run `gtd step human`:
431
- # - a NEW commit was authored -> return normally so the caller keeps driving.
432
- # - ZERO commits were authored -> halt, exit 0 (closing the editor without a
433
- # capturable change means "done for now" — and keeps a gate with no `C`
434
- # pattern, e.g. idle, from being reopened forever).
435
- # - the step REFUSES (non-zero exit: malformed steering file, unfinished
436
- # review) -> surface it verbatim and halt, exit non-zero — the same
437
- # wording an opening-move refusal already used before this function
438
- # existed.
439
- handle_human_gate() {
440
- local json="$1" file label state step_out head_before head_after
441
- label="$(jq -r '.label // .state' <<<"$json")"
442
- state="$(jq -r .state <<<"$json")"
443
-
444
- if [[ "$edit_disabled" == 1 ]]; then
445
- herdr_report blocked "$label"
446
- herdr_notify "gtd needs you — $(basename "$PWD")" "$label"
447
- emit_gate "$state" "$label"
448
- run_gtd next
449
- exit 0
450
- fi
451
-
452
- file="$(jq -r '.file // empty' <<<"$json")"
453
- herdr_report blocked "$label"
454
- launch_editor "${file:-.}"
455
- herdr_report working "$label"
456
-
457
- head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
458
- if ! step_out="$(run_gtd step human 2>&1)"; then
459
- emit_error "could not capture your changes at the human gate:
460
- $step_out
461
- Fix the reported issue and re-run gtd-loop."
462
- emit_log_pointer
463
- exit 1
464
- fi
465
- head_after="$(git rev-parse HEAD 2>/dev/null || echo none)"
466
- if [[ "$head_before" == "$head_after" ]]; then
467
- herdr_report idle "$label"
468
- herdr_release
469
- emit_action "$M_HUMAN" "done for now (nothing captured)"
470
- exit 0
471
- fi
472
- # A new commit was captured — render it, then return normally so the
473
- # caller keeps driving, unless --once says this one human gate is the whole
474
- # run.
475
- report_commits
476
- if [[ "$once_mode" == 1 ]]; then
477
- exit 0
478
- fi
479
- }
480
-
481
- trap 'code=$?; if [ "$code" -ne 0 ]; then
482
- herdr_report blocked "${GTD_LAST_LABEL:-stopped}: exited $code"
483
- herdr_notify "gtd stopped — $(basename "$PWD")" "${GTD_LAST_LABEL:-loop} failed (exit $code)"
484
- fi' EXIT
485
280
 
486
281
  prev_state=""
487
282
  prev_content=""
@@ -491,7 +286,7 @@ prev_head=""
491
286
  memory_marker="$(worktree_git_dir 2>/dev/null || echo .git)/gtd-loop-memory"
492
287
 
493
288
  # Truncated once here (not every iteration), so the file always reflects the
494
- # current run but survives after the loop exits for a later `gtd log`.
289
+ # current run but survives after the loop exits.
495
290
  log_path="$(resolve_log_path)"
496
291
  : >"$log_path"
497
292
  export GTD_LOOP_LOG="$log_path"
@@ -549,16 +344,9 @@ reported_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
549
344
  # and a mid-cycle restart must simply resume driving, not fail. A clean gate is
550
345
  # a harmless no-op; a genuine refusal there (a malformed steering file, or an
551
346
  # edit no `on` pattern accepts) is surfaced and stops the loop rather than being
552
- # driven past.
553
- #
554
- # Editing ON: route through the exact same gate logic the main loop uses below
555
- # (open the editor there first, THEN step) instead of blindly stepping human
556
- # and moving on. Editing OFF: unchanged from before this function existed —
557
- # step silently, refusal surfaced and halts, a clean/committed gate just falls
558
- # into the loop below. Either way, --once treats a captured commit here as the
559
- # run's one beat and exits (see handle_human_gate's own once-mode exit for the
560
- # editing-ON path); a clean/no-op gate captures nothing, so it isn't a beat —
561
- # it falls into the loop below exactly as it would without --once.
347
+ # driven past. --once treats a captured commit here as the run's one beat and
348
+ # exits; a clean/no-op gate captures nothing, so it isn't a beat — it falls
349
+ # into the loop below exactly as it would without --once.
562
350
  if ! peek_json="$(run_gtd next --json 2>&1)"; then
563
351
  emit_error "could not determine the next step:
564
352
  $peek_json
@@ -568,33 +356,20 @@ Run \`gtd status\` to inspect the repo."
568
356
  fi
569
357
  peek_kind="$(jq -r .kind <<<"$peek_json")"
570
358
  if [[ "$peek_kind" == "message" ]]; then
571
- if [[ "$edit_disabled" == 1 ]]; then
572
- opening_head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
573
- if ! step_out="$(run_gtd step human 2>&1)"; then
574
- emit_error "could not capture your changes at the human gate:
359
+ opening_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:
575
362
  $step_out
576
363
  Fix the reported issue and re-run gtd-loop."
577
- emit_log_pointer
578
- exit 1
579
- fi
580
- # This branch reports nothing today — advance the marker past whatever was
581
- # just (silently) captured so the first loop-body beat below doesn't newly
582
- # print it.
583
- reported_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
584
- if [[ "$once_mode" == 1 && "$opening_head_before" != "$reported_head" ]]; then
585
- exit 0
586
- fi
587
- else
588
- handle_human_gate "$peek_json"
364
+ emit_log_pointer
365
+ exit 1
589
366
  fi
590
- elif [[ "$edit_forced" == 1 ]]; then
591
- # Honesty caveat: a forced edit only means something at a human gate — the
592
- # machine isn't resting at one right now, so say so instead of pretending,
593
- # and just keep driving.
594
- if [[ "$FANCY" == 1 ]]; then
595
- printf '%sℹ️ --edit: not at a human gate yet — continuing to drive%s\n' "$C_DIM" "$C_RESET"
596
- else
597
- printf '%s--edit: not at a human gate yet — continuing to drive%s\n' "$C_DIM" "$C_RESET"
367
+ # This branch reports nothing — advance the marker past whatever was just
368
+ # (silently) captured so the first loop-body beat below doesn't newly
369
+ # print it.
370
+ reported_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
371
+ if [[ "$once_mode" == 1 && "$opening_head_before" != "$reported_head" ]]; then
372
+ exit 0
598
373
  fi
599
374
  fi
600
375
 
@@ -613,17 +388,16 @@ while true; do
613
388
  model="$(jq -r '.model // empty' <<<"$next_json")"
614
389
  memory="$(jq -r '.memory // empty' <<<"$next_json")"
615
390
  label="$(jq -r '.label // .state' <<<"$next_json")"
616
- GTD_LAST_LABEL="$label"
617
391
 
618
392
  if [[ "$kind" == "message" ]]; then
619
- # handle_human_gate exits 0/non-zero itself on a halt/refusal; it only
620
- # returns here when a new commit was captured, so we keep driving.
621
- handle_human_gate "$next_json"
622
- continue
393
+ # The non-autonomous halt: print the gate, hand off, exit 0. The human
394
+ # (or an outer wrapper) edits and re-launches; the opening move above
395
+ # captures what they left.
396
+ emit_gate "$state" "$label"
397
+ run_gtd next
398
+ exit 0
623
399
  fi
624
400
 
625
- herdr_report working "$label"
626
-
627
401
  if [[ "$kind" == "script" ]]; then
628
402
  emit_action "$M_CHECK" "$label"
629
403
  # Execute the emitted wrapper verbatim; its exit code is deliberately
@@ -639,8 +413,6 @@ while true; do
639
413
  fi
640
414
  printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
641
415
  if ! report_commits; then
642
- herdr_report idle "$label"
643
- herdr_release
644
416
  emit_settled "$state"
645
417
  exit 0
646
418
  fi
@@ -652,12 +424,12 @@ while true; do
652
424
  continue
653
425
  fi
654
426
 
655
- # kind == "prompt": stall detection (skills/loop/SKILL.md) — the same
427
+ # kind == "prompt": stall detection — the same
656
428
  # agent-owned state and content repeating, WITH NO COMMIT authored since the
657
429
  # last time we saw this beat, means the last dispatch made no progress; stop
658
430
  # rather than spin on it. The HEAD check is what distinguishes a genuine
659
- # stall from a return lap through a human gate (e.g. plan-review ->
660
- # planning): a static prompt looks identical to last time, but a commit
431
+ # stall from a return lap back into the same state (e.g. checking ->
432
+ # fixing): a static prompt looks identical to last time, but a commit
661
433
  # landed in between, so HEAD has moved and this isn't a stall.
662
434
  cur_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
663
435
  if [[ "$state" == "$prev_state" && "$content" == "$prev_content" && "$cur_head" == "$prev_head" ]]; then