@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.
- package/README.md +212 -58
- package/bin/gtd +184 -21
- package/dist/gtd.bundle.mjs +4436 -2966
- 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 ->
|
|
14
|
-
#
|
|
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 "$
|
|
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
|
|
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-
|
|
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
|
|
532
|
-
if ! step_out="$(
|
|
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
|
|
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="$(
|
|
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
|
|
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`
|
|
657
|
-
#
|
|
658
|
-
#
|
|
659
|
-
#
|
|
660
|
-
#
|
|
661
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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="$(
|
|
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
|