@pmelab/gtd 8.4.0 → 8.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.
Files changed (4) hide show
  1. package/README.md +212 -58
  2. package/bin/gtd +184 -21
  3. package/dist/gtd.bundle.mjs +4436 -2966
  4. package/package.json +11 -9
package/bin/gtd CHANGED
@@ -10,8 +10,13 @@ set -euo pipefail
10
10
  # - no arguments, or exactly `loop` with no further arguments -> fall through
11
11
  # into the loop body below (the loop protocol comment further down).
12
12
  # - `loop` with an unrecognised extra argument -> usage error, exit 2.
13
- # - anything else -> hand off to the bundle via `run_gtd`, forwarding all
14
- # arguments unchanged (e.g. `bin/gtd status`, `bin/gtd step human`).
13
+ # - anything else -> a WRITE subcommand (`step`, the `--entry` forms,
14
+ # `abandon`, `restore` — see `is_write_command`) routes through
15
+ # `run_gtd_command`, which actually performs the bundle's emitted
16
+ # `required`/`optional` scripts (the bundle itself only ever PRINTS them
17
+ # now); everything else (`status`, `next`, `validate`, `visualize`, `lsp`,
18
+ # `check`, `init`) hands off straight to the bundle via `run_gtd`,
19
+ # forwarding all arguments unchanged (e.g. `bin/gtd status`).
15
20
  # The loop's whole job is to drive the process until it rests at a
16
21
  # NON-AUTONOMOUS state (a human gate) or settles. Everything beyond that —
17
22
  # editors, desktop notifications, terminal-multiplexer status — is an OUTER
@@ -61,6 +66,19 @@ else
61
66
  M_FIX="[fix]"; M_COMMIT="[commit]"; M_SETTLE="[done]"; M_ERROR="[err]"; M_WARN="[warn]"; M_ELLIPSIS="..."
62
67
  fi
63
68
 
69
+ # `jq` used to be a loop-only dependency (the loop's own `next --json`
70
+ # parsing); now every write subcommand's `--json` response (parsed by
71
+ # `run_gtd_command`, below) carries the `required`/`optional` scripts this
72
+ # script must execute for ANY invocation, not just `loop`. Checked once, up
73
+ # front, before any dispatch below ever reaches for `jq` — a plain
74
+ # `echo`/`exit` rather than `emit_error` since that helper (and the
75
+ # fancy/plain markers it relies on) isn't the point; keeping the check
76
+ # dependency-free is.
77
+ if ! command -v jq >/dev/null 2>&1; then
78
+ echo "gtd: jq is required (used to parse the bundle's --json output) — install it and retry" >&2
79
+ exit 1
80
+ fi
81
+
64
82
  # emit_file_rows <files> — render a commit's changed files as dim, indented
65
83
  # rows BELOW the transition/commit line: at most 3, then a "… (N more)" row
66
84
  # when there are more. <files> is a newline-separated list (git diff-tree
@@ -234,9 +252,18 @@ parse_subject() {
234
252
  # when it reported at least one; returns non-zero (having emitted nothing,
235
253
  # and leaving $reported_head unchanged) when there were no new commits. Global
236
254
  # rather than per-beat so the review checkout window's HEAD rewinds between
237
- # beats never cause an already-reported commit to replay.
255
+ # beats never cause an already-reported commit to replay. $reported_head ==
256
+ # "none" (the repo had no commits at all when it was last captured) can't
257
+ # form a "none"..HEAD range — `git rev-list` would fail resolving "none" as a
258
+ # revision — so that case walks the whole history from the root instead.
238
259
  report_commits() {
239
260
  local sha subject files reported=0
261
+ local range
262
+ if [[ "$reported_head" == none ]]; then
263
+ range="HEAD"
264
+ else
265
+ range="$reported_head..HEAD"
266
+ fi
240
267
  while IFS= read -r sha; do
241
268
  [[ -z "$sha" ]] && continue
242
269
  reported=1
@@ -247,13 +274,130 @@ report_commits() {
247
274
  transition) emit_transition "$S_FROM" "$S_TO" "$files" ;;
248
275
  capture | squash) emit_commit "$subject" "$files" ;;
249
276
  esac
250
- done < <(git rev-list --reverse "$reported_head"..HEAD)
277
+ done < <(git rev-list --reverse "$range")
251
278
  if [[ "$reported" == 1 ]]; then
252
279
  reported_head="$(git rev-parse HEAD)"
253
280
  fi
254
281
  [[ "$reported" == 1 ]]
255
282
  }
256
283
 
284
+ # ── Write-command execution (is_write_command, run_gtd_command) ────────────
285
+ # The bundle no longer performs a WRITE subcommand's git effect itself: its
286
+ # `--json` response for `step`/the `--entry` forms/`abandon`/`restore` now
287
+ # carries `required` (a bash script that performs the command's whole
288
+ # effect — commit/reset/etc, including any review-window close/open
289
+ # management) and `optional` (presentation-only, e.g. re-opening the review
290
+ # checkout window — safe to skip), alongside whichever keys that command's
291
+ # JSON always carried. This script is the ONE driver for every invocation
292
+ # (not just the loop's), so it — not the bundle — must run those scripts.
293
+
294
+ # is_write_command <token> — true for exactly the tokens whose command
295
+ # mutates the repo: `step` (with or without `--entry` trailing), `abandon`,
296
+ # `restore`, and the bare `--entry` short form (`gtd --entry <state>`, where
297
+ # `--entry` itself is $1 since there is no subcommand token in front of it).
298
+ # Everything else (next/status/validate/visualize/lsp/check/init, or an
299
+ # unrecognised token) is read-only or has its own arity/usage errors and is
300
+ # left to the existing plain `run_gtd "$@"` forwarding.
301
+ # `--entry` takes both spellings (`--entry <state>` and `--entry=<state>`), so
302
+ # the bare short form has to be recognised either way.
303
+ is_write_command() {
304
+ case "${1:-}" in
305
+ step | abandon | restore | --entry | --entry=*) return 0 ;;
306
+ *) return 1 ;;
307
+ esac
308
+ }
309
+
310
+ # run_gtd_command <subcommand-and-args...> — the ONE execution path for every
311
+ # write subcommand, used both by the direct dispatch below and by the loop
312
+ # body's own step calls (so there is one execution path rather than two, not
313
+ # a loop-only one and a separate direct one). Fetches the bundle's `--json`
314
+ # response for "$@" (forcing --json on regardless of whether the caller's own
315
+ # arguments already carry it — repeating `--json` is harmless, see
316
+ # `Cli.ts`'s arity-0 flag handling), runs `.required` via `bash -c` (aborting
317
+ # — returning its exit code — before `.optional` ever runs, mirroring `set
318
+ # -e`), then `.optional` via `bash -c` (a non-zero exit there is only ever a
319
+ # warning: it is presentation-only, e.g. re-opening the review checkout
320
+ # window, per its own "safe to skip" billing). On success it prints, on
321
+ # stdout, the plain human-facing line in TODAY's exact wording — the same
322
+ # text `runStepCommand`/`runEntryCommand`/`runAbandonCommand`/
323
+ # `runRestoreCommand` printed themselves before this rewrite moved the git
324
+ # write into `required`. A direct caller lets that line stream straight to
325
+ # the terminal; the loop instead captures it (`$(... 2>&1)`, the file's
326
+ # established idiom) and logs it, since its own on-screen presentation is
327
+ # `report_commits`. `abandon`/`restore` have no `subject` in their JSON (never
328
+ # did — the original handlers computed it from git AFTER writing), so this
329
+ # recomputes it from HEAD after `.required` has run; every other field used
330
+ # below (`state`, `from`, `head`, `to`) is one of the contract's own preserved
331
+ # "existing keys". On failure (the JSON fetch itself — a refusal/usage error —
332
+ # or `.required`), the combined fetch output is sent to stderr and the
333
+ # function returns non-zero; a failing `.required`'s own output streams
334
+ # (or is captured by the caller) exactly as it happened, unredirected here.
335
+ run_gtd_command() {
336
+ local subcmd="${1:-}" json_out required optional opt_status
337
+ local state subject head to from
338
+
339
+ if ! json_out="$(run_gtd "$@" --json 2>&1)"; then
340
+ printf '%s' "$json_out" >&2
341
+ return 1
342
+ fi
343
+
344
+ # `// empty` guards a command whose JSON doesn't (yet, or ever) carry these
345
+ # keys for a given branch — e.g. `gtd abandon`'s "nothing to abandon" no-op
346
+ # reply has neither key at all — so a missing/null field reads as "" (jq's
347
+ # own `null` stringifies to the 4-character text "null" otherwise, which
348
+ # would misread as a non-empty script).
349
+ required="$(jq -r '.required // empty' <<<"$json_out")"
350
+ optional="$(jq -r '.optional // empty' <<<"$json_out")"
351
+
352
+ if [[ -n "$required" ]]; then
353
+ bash -c "$required" || return $?
354
+ fi
355
+
356
+ if [[ -n "$optional" ]]; then
357
+ opt_status=0
358
+ bash -c "$optional" || opt_status=$?
359
+ if ((opt_status != 0)); then
360
+ emit_warn "the presentation-only follow-up after '$subcmd' failed (exit $opt_status) — continuing"
361
+ fi
362
+ fi
363
+
364
+ case "$subcmd" in
365
+ abandon)
366
+ if [[ -z "$required" ]]; then
367
+ state="$(jq -r '.state' <<<"$json_out")"
368
+ printf 'no gtd process is underway (resting at "%s") — nothing to abandon\n' "$state"
369
+ else
370
+ from="$(jq -r '.from' <<<"$json_out")"
371
+ state="$(jq -r '.state' <<<"$json_out")"
372
+ head="$(jq -r '.head' <<<"$json_out")"
373
+ subject="$(git log -1 --format=%s HEAD)"
374
+ printf 'abandoned the process resting at "%s" — HEAD is back at %s ("%s"), resting at "%s".\n' \
375
+ "$from" "${head:0:7}" "$subject" "$state"
376
+ printf 'Everything the process produced is kept as uncommitted changes (`git status`); discard them with `git checkout -- . && git clean -fd .gtd` for a clean tree.\n'
377
+ fi
378
+ ;;
379
+ restore)
380
+ to="$(jq -r '.to' <<<"$json_out")"
381
+ state="$(jq -r '.state' <<<"$json_out")"
382
+ subject="$(git log -1 --format=%s HEAD)"
383
+ printf 'restored the retained history — HEAD is back at %s ("%s"), resting at "%s". Resume with the loop, or `git reset` to any earlier turn to restart from there.\n' \
384
+ "${to:0:7}" "$subject" "$state"
385
+ ;;
386
+ *)
387
+ # `step` (subject nullable — a no-op prints "nothing to do") and the
388
+ # `--entry` forms (subject never null — always "committed: ...") share
389
+ # this one branch: the null-check alone tells them apart.
390
+ subject="$(jq -r '.subject' <<<"$json_out")"
391
+ if [[ -n "$subject" && "$subject" != "null" ]]; then
392
+ printf 'committed: %s\n' "$subject"
393
+ else
394
+ state="$(jq -r '.state' <<<"$json_out")"
395
+ printf 'nothing to do at "%s"\n' "$state"
396
+ fi
397
+ ;;
398
+ esac
399
+ }
400
+
257
401
  # ── Log file (resolve_log_path) ─────────────────────────────────────────────
258
402
 
259
403
  # worktree_git_dir — the git dir of the working tree gtd is operating in,
@@ -317,6 +461,10 @@ if [[ "$valid_combo" == 1 && "$n_once" == 1 ]]; then
317
461
  fi
318
462
 
319
463
  if [[ $# -gt 0 && "$1" != "loop" ]]; then
464
+ if is_write_command "${1:-}"; then
465
+ run_gtd_command "$@"
466
+ exit $?
467
+ fi
320
468
  run_gtd "$@"
321
469
  exit $?
322
470
  fi
@@ -501,7 +649,7 @@ run_agent_turn() {
501
649
  # its own comment) — a global high-water mark rather than a per-beat
502
650
  # head_before, so a review checkout window's HEAD rewind between beats never
503
651
  # causes an already-printed transition to replay.
504
- reported_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
652
+ reported_head="$(git rev-parse --verify --quiet HEAD || echo none)"
505
653
 
506
654
  # Opening move: capture the human's pending edit so gtd-loop is the ONLY command
507
655
  # a human runs. A human acts at a gate by editing files (a plan answer, a review
@@ -513,7 +661,7 @@ reported_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
513
661
  # We peek with the pure, mutation-free `gtd next --json` and step ONLY when it
514
662
  # reports a human rest (`kind == "message"`): at an agent/check rest
515
663
  # `gtd step human` would refuse out-of-turn (non-zero, aborting under `set -e`),
516
- # and a mid-cycle restart must simply resume driving, not fail. A clean gate is
664
+ # and a mid-process restart must simply resume driving, not fail. A clean gate is
517
665
  # a harmless no-op; a genuine refusal there (a malformed steering file, or an
518
666
  # edit no `on` pattern accepts) is surfaced and stops the loop rather than being
519
667
  # driven past. --once treats a captured commit here as the run's one beat and
@@ -528,8 +676,8 @@ Run \`gtd status\` to inspect the repo."
528
676
  fi
529
677
  peek_kind="$(jq -r .kind <<<"$peek_json")"
530
678
  if [[ "$peek_kind" == "message" ]]; then
531
- opening_head_before="$(git rev-parse HEAD 2>/dev/null || echo none)"
532
- if ! step_out="$(run_gtd step human 2>&1)"; then
679
+ opening_head_before="$(git rev-parse --verify --quiet HEAD || echo none)"
680
+ if ! step_out="$(run_gtd_command step human 2>&1)"; then
533
681
  emit_error "could not capture your changes at the human gate:
534
682
  $step_out
535
683
  Fix the reported issue and re-run gtd-loop."
@@ -539,7 +687,7 @@ Fix the reported issue and re-run gtd-loop."
539
687
  # This branch reports nothing — advance the marker past whatever was just
540
688
  # (silently) captured so the first loop-body beat below doesn't newly
541
689
  # print it.
542
- reported_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
690
+ reported_head="$(git rev-parse --verify --quiet HEAD || echo none)"
543
691
  if [[ "$once_mode" == 1 && "$opening_head_before" != "$reported_head" ]]; then
544
692
  exit 0
545
693
  fi
@@ -590,7 +738,7 @@ Run \`gtd status\` to inspect the repo."
590
738
  emit_warn "the check script at '$state' exited $script_status — continuing; the outcome is whatever it left in the tree:"
591
739
  emit_output_since "$script_offset"
592
740
  fi
593
- if ! step_out="$(run_gtd step "$actor" 2>&1)"; then
741
+ if ! step_out="$(run_gtd_command step "$actor" 2>&1)"; then
594
742
  printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
595
743
  emit_error "$step_out"
596
744
  emit_log_pointer
@@ -616,7 +764,7 @@ Run \`gtd status\` to inspect the repo."
616
764
  # stall from a return lap back into the same state (e.g. checking ->
617
765
  # fixing): a static prompt looks identical to last time, but a commit
618
766
  # landed in between, so HEAD has moved and this isn't a stall.
619
- cur_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
767
+ cur_head="$(git rev-parse --verify --quiet HEAD || echo none)"
620
768
  if [[ "$state" == "$prev_state" && "$content" == "$prev_content" && "$cur_head" == "$prev_head" ]]; then
621
769
  next_out="$(run_gtd next 2>&1)" || true
622
770
  emit_error "no progress at '$state' — the agent's last turn changed nothing. Stopping to avoid spinning.
@@ -653,12 +801,15 @@ $next_out"
653
801
  # Run the agent turn (see run_agent_turn's own comment for the adapter
654
802
  # contract). Then the self-validation gate: a producing state may declare a
655
803
  # `file:`/`mode:` pair (e.g. the default workflow's grilling/reviewing),
656
- # making its output's format checkable. `gtd validate` is a no-op (exit 0)
657
- # when the resolved rest has nothing to validate, so run it after EVERY agent
658
- # turn; on findings, re-prompt the SAME agent session to fix them (up to a
659
- # cap) before stepping — the plain-`gtd next` self-validation instruction's
660
- # driver-side equivalent, so the next rest is only ever handed a well-formed
661
- # file. gtd itself never validates at step time — this is the loop's job.
804
+ # making its output's format checkable. `gtd validate --json` always exits
805
+ # 0 now — it hands back the SCRIPT that formats/checks the file (`.script`,
806
+ # empty when the resolved rest has nothing to validate) rather than running
807
+ # it itself, so this driver runs that script after EVERY agent turn and
808
+ # re-prompts the SAME agent session on ITS non-zero exit (up to a cap)
809
+ # before stepping — the plain-`gtd next` self-validation instruction's
810
+ # driver-side equivalent, so the next rest is only ever handed a
811
+ # well-formed file. gtd itself never validates at step time — this is the
812
+ # loop's job.
662
813
  emit_action "$M_AGENT" "$label"
663
814
  turn_offset="$(log_offset)"
664
815
  agent_status=0
@@ -670,12 +821,24 @@ $next_out"
670
821
  exit 1
671
822
  fi
672
823
 
824
+ if ! validate_json="$(run_gtd validate --json 2>&1)"; then
825
+ emit_error "could not resolve the validation script at '$state':
826
+ $validate_json
827
+ Stopping rather than stepping with a possibly-malformed steering file."
828
+ emit_log_pointer
829
+ exit 1
830
+ fi
831
+ validate_script="$(jq -r '.script' <<<"$validate_json")"
832
+
673
833
  fix_attempt=0
674
- while ! validate_out="$(run_gtd validate 2>&1)"; do
834
+ while [[ -n "$validate_script" ]]; do
835
+ validate_status=0
836
+ validate_out="$(bash -c "$validate_script" 2>&1)" || validate_status=$?
837
+ ((validate_status == 0)) && break
675
838
  fix_attempt=$((fix_attempt + 1))
676
839
  if ((fix_attempt > 3)); then
677
840
  printf '%s\n' "$validate_out" >>"$GTD_LOOP_LOG"
678
- emit_error "'$state' still fails \`gtd validate\` after 3 fix attempts:
841
+ emit_error "'$state' still fails its own validation script after 3 fix attempts:
679
842
  $validate_out
680
843
  Stopping rather than stepping with a malformed steering file."
681
844
  emit_log_pointer
@@ -689,7 +852,7 @@ Stopping rather than stepping with a malformed steering file."
689
852
  [[ -n "$session_id" ]] && fix_resume=1
690
853
  turn_offset="$(log_offset)"
691
854
  agent_status=0
692
- run_agent_turn "$content"$'\n\n'"Your last turn does not pass \`gtd validate\`. Fix these format violations, then finish:"$'\n'"$validate_out" "$fix_resume" || agent_status=$?
855
+ run_agent_turn "$content"$'\n\n'"Your last turn does not pass its own validation script. Fix these format violations, then finish:"$'\n'"$validate_out" "$fix_resume" || agent_status=$?
693
856
  if ((agent_status != 0)); then
694
857
  emit_error "the agent's fix attempt $fix_attempt at '$state' failed (exit $agent_status):"
695
858
  emit_output_since "$turn_offset"
@@ -698,7 +861,7 @@ Stopping rather than stepping with a malformed steering file."
698
861
  fi
699
862
  done
700
863
 
701
- if ! step_out="$(run_gtd step "$actor" 2>&1)"; then
864
+ if ! step_out="$(run_gtd_command step "$actor" 2>&1)"; then
702
865
  printf '%s\n' "$step_out" >>"$GTD_LOOP_LOG"
703
866
  emit_error "$step_out"
704
867
  emit_log_pointer