@pmelab/gtd 8.5.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 +135 -5
- package/bin/gtd +183 -20
- package/dist/gtd.bundle.mjs +900 -308
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -227,13 +227,26 @@ Commands:
|
|
|
227
227
|
next Print the resolved rest's rendered script/prompt/message (no mutation)
|
|
228
228
|
status Print the resolved rest's state/actor and which declared
|
|
229
229
|
pattern (if any) each pending change matches (no mutation)
|
|
230
|
-
validate
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
validate Print the script that formats (when declared) then
|
|
231
|
+
validates the resolved rest's steering file, using its
|
|
232
|
+
mode's commands (its file:/mode:), instead of running it —
|
|
233
|
+
a driver runs the script and reads the findings from its
|
|
234
|
+
own exit code/output. Always exits 0; --json emits
|
|
235
|
+
{state, file?, mode?, script} (script is "" when there is
|
|
236
|
+
nothing to validate; plain text prints "nothing to
|
|
237
|
+
validate" in that case)
|
|
233
238
|
lsp Start the LSP server for .gtd/ steering files (stdio)
|
|
234
239
|
visualize Serve an interactive diagram of the active workflow on a
|
|
235
240
|
local web server (--port <n>, --no-open; --json prints the
|
|
236
241
|
model and exits)
|
|
242
|
+
check <mode> <file>
|
|
243
|
+
Read <file> and run the built-in steering format named
|
|
244
|
+
<mode> (see `gtd validate`'s modes: qa, review) over its
|
|
245
|
+
contents, printing each finding one per line and exiting
|
|
246
|
+
non-zero when there are any. Resolves no workflow state and
|
|
247
|
+
reads no config — standalone, runnable from any directory
|
|
248
|
+
with <mode>/<file> given explicitly. This is what a
|
|
249
|
+
workflow's emitted validation script invokes as a leaf step
|
|
237
250
|
version Print version and exit
|
|
238
251
|
help Print this help and exit
|
|
239
252
|
|
|
@@ -469,6 +482,123 @@ Optionally,
|
|
|
469
482
|
sets a value renderable as `$summary` in an Agent sidebar row, if you want more
|
|
470
483
|
than the state itself.
|
|
471
484
|
|
|
485
|
+
## Writing your own driver
|
|
486
|
+
|
|
487
|
+
`bin/gtd` is gtd's own reference driver, not a privileged one — the engine
|
|
488
|
+
itself is a supported public surface, and anything below holds for any driver
|
|
489
|
+
you write against it. gtd decides and prints; it never touches git itself. The
|
|
490
|
+
four commands that change anything — `gtd step [<actor>]` (including
|
|
491
|
+
`--entry <state>`), `gtd abandon`, and `gtd restore` — perform no git write when
|
|
492
|
+
run. Instead, under `--json`, each one's output object carries two extra string
|
|
493
|
+
fields, `required` and `optional`: bash scripts for YOU to execute. (Plain-text
|
|
494
|
+
output, without `--json`, never shows these — it prints only the human-facing
|
|
495
|
+
result line, e.g. `committed: <subject>`, so a plain-text caller never sees a
|
|
496
|
+
script at all.) A driver that only prints gtd's plain-text output is not driving
|
|
497
|
+
anything; to actually land a turn, run `gtd ... --json`, pull `required`/
|
|
498
|
+
`optional` out with `jq`, and execute them.
|
|
499
|
+
|
|
500
|
+
- **`required`** is everything that decides what lands in git — closing an open
|
|
501
|
+
review checkout window, the resting state's own steering-mode
|
|
502
|
+
`format:`/`validate:` commands, then the commit or squash itself (`gtd step`
|
|
503
|
+
and `gtd --entry <state>`), or the ref update and reset that undo a process
|
|
504
|
+
(`gtd abandon`, `gtd restore`). Run this one. Skipping it means the turn never
|
|
505
|
+
lands.
|
|
506
|
+
- **`optional`** is presentation only: re-opening the review checkout window
|
|
507
|
+
(the `<<<<<<< HEAD` diff view) after a `gtd step` lands at a
|
|
508
|
+
`reviewWindow: true` state, so an editor's diff view has something to show.
|
|
509
|
+
Skip it and you lose nothing but that view — the workflow is still driven
|
|
510
|
+
correctly. `gtd abandon`/`gtd restore` always emit `optional: ""` (there is no
|
|
511
|
+
window to reopen after either).
|
|
512
|
+
|
|
513
|
+
Either field may be the empty string `""`, meaning "nothing to do here" — a
|
|
514
|
+
no-op `gtd step` (a clean tree matching no `on` pattern) emits
|
|
515
|
+
`required: ""`/`optional: ""` and exits zero; run nothing and move on. Both
|
|
516
|
+
scripts are self-contained (each carries its own precondition assert and retry
|
|
517
|
+
helper) and safe to run standalone, in sequence, or not at all — paste either
|
|
518
|
+
into a terminal and it does exactly what it says.
|
|
519
|
+
|
|
520
|
+
### Failure taxonomy and recovery
|
|
521
|
+
|
|
522
|
+
Two different things can go wrong, and they mean different things:
|
|
523
|
+
|
|
524
|
+
- **`gtd` itself exits non-zero.** Nothing was attempted — this is a refusal (a
|
|
525
|
+
guard rejected the turn) or a usage error. No script was ever produced.
|
|
526
|
+
- **An emitted script exits non-zero when YOU run it.** Something may have
|
|
527
|
+
partially happened — e.g. a `gtd_retry`-wrapped git write landed but a later
|
|
528
|
+
step in the same script failed.
|
|
529
|
+
|
|
530
|
+
Recovery is the same in both cases: **re-invoke `gtd`.** It re-reads the real
|
|
531
|
+
repository state fresh every time — never a cached plan — and emits whatever
|
|
532
|
+
still needs to happen from there. This works because every emitted script opens
|
|
533
|
+
by asserting its own precondition
|
|
534
|
+
(`[ "$(git rev-parse --verify --quiet HEAD 2>/dev/null)" = <expected> ] || { ...; exit 1; }`,
|
|
535
|
+
and the same shape for a review window's saved ref — see `src/Emit.ts`'s
|
|
536
|
+
`headAssertion`/`reviewWindowAssertion`), so a script generated against a
|
|
537
|
+
repository state that has since moved refuses loudly instead of corrupting
|
|
538
|
+
anything. In a repository with no commits yet, `<expected>` is the empty string
|
|
539
|
+
— the deciding read's own convention for "no commits yet" — so the first
|
|
540
|
+
workflow commit is allowed to land instead of being blocked by an unborn `HEAD`.
|
|
541
|
+
**Emitted scripts are re-runnable**: this is the single most important property
|
|
542
|
+
for a driver's recovery logic. Re-running a script that already fully applied is
|
|
543
|
+
a no-op (its git writes are `--allow-empty` commits, idempotent ref updates, and
|
|
544
|
+
tolerant staged-restore calls), and re-running one that only partially applied
|
|
545
|
+
resumes correctly, because the precondition either still holds (nothing landed
|
|
546
|
+
yet — safe to retry verbatim) or gtd's next invocation reads the new real state
|
|
547
|
+
and emits a fresh script for what remains. A driver never needs its own
|
|
548
|
+
retry/resume logic beyond "if the script failed, ask gtd again."
|
|
549
|
+
|
|
550
|
+
### Prerequisites
|
|
551
|
+
|
|
552
|
+
- **`jq`** — to pull the `required`/`optional` strings back out of gtd's
|
|
553
|
+
`--json` output.
|
|
554
|
+
- **`gtd` on `PATH`** — a mode's seeded `validate:` command (the one the
|
|
555
|
+
compiler fills in for the built-in `qa`/`review` formats) is literally the
|
|
556
|
+
string `gtd check <mode> '<file>'`, invoked by NAME from inside an emitted
|
|
557
|
+
script, not by absolute path (see `src/SteeringFormats.ts`'s
|
|
558
|
+
`seededValidateCommand`). This is a deliberate trade: a readable, overridable,
|
|
559
|
+
copy-pasteable command in exchange for depending on shell name resolution at
|
|
560
|
+
the moment the script runs. The sharp edge: if the `gtd` binary you invoked to
|
|
561
|
+
GENERATE the script differs from the `gtd` that resolves on `PATH` when the
|
|
562
|
+
script later RUNS (a locally-built dev binary vs. a globally-installed
|
|
563
|
+
release, say), you can get version skew between the two — the command that
|
|
564
|
+
validates may not be the command that decided. Keep the two in sync (one `gtd`
|
|
565
|
+
on `PATH`, consistently) if you care about that gap.
|
|
566
|
+
|
|
567
|
+
### The normalization-only contract on `format:`
|
|
568
|
+
|
|
569
|
+
A mode's `format:` command may reformat a steering file — whitespace, wrapping,
|
|
570
|
+
reordering — but must NEVER change what a step-capture guard would decide. gtd's
|
|
571
|
+
guards (the review sign-off check, the feedback-progress check, the
|
|
572
|
+
answer-completeness check — `src/StepGuards.ts`) decide ONCE, against whichever
|
|
573
|
+
bytes are on disk at the moment `gtd step` runs, which may be before OR after an
|
|
574
|
+
emitted script's own `format:` line has run (the script runs `format:` then
|
|
575
|
+
`validate:` then the commit — see `src/SteeringMode.ts`'s
|
|
576
|
+
`renderSteeringCommands` — but gtd's decision and the driver's script execution
|
|
577
|
+
are different processes at different times, so there's no guaranteed ordering
|
|
578
|
+
between "gtd decided" and "the script formatted"). That's only safe because
|
|
579
|
+
every built-in guard judges content, not incidental formatting (e.g. it
|
|
580
|
+
normalizes `[ ]`/`[x]` checkboxes before comparing). If you plug in your own
|
|
581
|
+
`format:` command, the same rule binds it: a formatter that also changes meaning
|
|
582
|
+
— ticking a box, stripping a paragraph — makes the guard's decision and the
|
|
583
|
+
file's actual content disagree, and gtd will not catch that for you.
|
|
584
|
+
|
|
585
|
+
### Built-in steering formats are ordinary modes
|
|
586
|
+
|
|
587
|
+
`qa` and `review` are gtd's two built-in steering-file formats (parsed and
|
|
588
|
+
validated in-process because `gtd lsp` needs the same parsers for live
|
|
589
|
+
diagnostics), but their `validate:` is not hardcoded or hidden: the compiler
|
|
590
|
+
SEEDS every workflow's `modes:` map with `qa`/`review` entries whose `validate:`
|
|
591
|
+
is the same `gtd check <mode> '<file>'` string described above
|
|
592
|
+
(`src/PatternConfig.ts`, `src/SteeringFormats.ts`'s `seededValidateCommand`).
|
|
593
|
+
That seeded command is visible in `gtd visualize`'s compiled model and in the
|
|
594
|
+
editor JSON schema like any other mode, and it's overridable the same way any
|
|
595
|
+
mode is — declare `modes: { qa: { validate: "your-own-command" } }` (in the
|
|
596
|
+
workflow or in `.gtdrc`) and your command displaces the seed; declaring only a
|
|
597
|
+
`format:` for `qa`/`review` composes with the seeded `validate:` rather than
|
|
598
|
+
replacing it. There is no special-cased built-in behavior a driver needs to know
|
|
599
|
+
about beyond the ordinary mode-resolution rules already documented under
|
|
600
|
+
[The `workflow:` key](#the-workflow-key).
|
|
601
|
+
|
|
472
602
|
## Configuration
|
|
473
603
|
|
|
474
604
|
gtd reads an optional `.gtdrc` config file via
|
|
@@ -799,8 +929,8 @@ but `gtd lsp` never runs a shell command per keystroke, so live diagnostics
|
|
|
799
929
|
become one `Information` notice pointing at `gtd validate` instead of the
|
|
800
930
|
built-in findings. Any OTHER mode name — `prose` (used by the simple flow's plan
|
|
801
931
|
file), or a project's own declared name — has no built-in format, so it gets no
|
|
802
|
-
live editor support at all: `gtd validate` and the `gtd step`
|
|
803
|
-
and validate it like any other mode.
|
|
932
|
+
live editor support at all: `gtd validate`'s emitted script and the `gtd step`
|
|
933
|
+
gate still format and validate it like any other mode.
|
|
804
934
|
|
|
805
935
|
## Development
|
|
806
936
|
|
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
|
|
@@ -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
|