@pmelab/gtd 8.2.0 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -115,10 +115,12 @@ steering-file entries are chosen by which file you create:
115
115
  per-package **agentic review** that verifies the package against its spec.
116
116
 
117
117
  Both flows converge on the same tail: an agent hands you a `.gtd/REVIEW.md`
118
- checkbox review of the diff — tick a box as you review each hunk (ticking just
119
- records "I read this"), and leave a **comment** to request changes: a note on a
120
- line, an inline `// TODO`-style comment in the code, or a direct code edit. Any
121
- comment sends a build + re-review round — an agent first turns your comments
118
+ checkbox review of the diff — the prompt never inlines the diff itself; it names
119
+ the commit the changes are based at and the agent runs `git diff` to read the
120
+ range before writing the review. Tick a box as you review each hunk (ticking
121
+ just records "I read this"), and leave a **comment** to request changes: a note
122
+ on a line, an inline `// TODO`-style comment in the code, or a direct code edit.
123
+ Any comment sends a build + re-review round — an agent first turns your comments
122
124
  into an explicit instruction list, then a build turn implements it (a re-review
123
125
  then covers only the follow-through, and a hand-edit is treated as your own fix
124
126
  the agent completes without reverting your lines; a comment can't be silently
@@ -306,6 +308,13 @@ name surfaced in `gtd next --json`/`gtd status`. The driver uses it for its
306
308
  per-beat progress lines; an outer wrapper (a terminal multiplexer, a notifier)
307
309
  can use it the same way.
308
310
 
311
+ A state can also declare an optional `memory:` scope label: consecutive agent
312
+ turns sharing the same label continue one agent session (remembered in the git
313
+ dir, so it survives restarts across the same worktree), while a changed or
314
+ absent label starts a fresh one. If the remembered session no longer exists
315
+ (retention expired, `~/.claude/projects` wiped, a machine change), the driver
316
+ degrades to a fresh session with a warning instead of stopping the loop.
317
+
309
318
  ### Terminal-multiplexer status: a herdr wrapper
310
319
 
311
320
  [herdr](https://herdr.dev) shows a per-pane status in its sidebar. This wrapper
@@ -470,6 +479,22 @@ workflow:
470
479
  mode: <modeName> # optional, requires "file" — a built-in (qa/review/prose) or a `modes:` entry
471
480
  ```
472
481
 
482
+ Besides `it.vars` (below), a `script`/`prompt`/`message`/`commit` template sees:
483
+
484
+ - **`it.startCommit`** — the process's diff base (the commit the current cycle
485
+ started from, or a `gtd review <commitish>` entry's resolved base).
486
+ - **`it.reviewBase`** — the previous review round's boundary, falling back to
487
+ `it.startCommit` on a first review.
488
+ - **`it.retainedBase`** — the process's trace/retry boundary, what a squash
489
+ actually keeps (never moved by a `gtd review` entry).
490
+ - **`it.currentCommit`** / **`it.previousCommit`** — HEAD's hash and its parent,
491
+ at render time.
492
+
493
+ A template never sees rendered diff CONTENT — no field carries a diff. It names
494
+ a base and leaves the agent to run `git diff <base>` itself; this keeps every
495
+ render cheap (no diff computed on `gtd next`/`gtd status`/`gtd lsp`) and the
496
+ prompt small and cacheable.
497
+
473
498
  Authoring or editing a workflow with a coding agent? `skills/authoring/SKILL.md`
474
499
  is the agent-facing contract for producing a valid `workflow:` — the state
475
500
  model, pattern grammar, load-time rules, and how to verify a change compiles.
package/bin/gtd CHANGED
@@ -23,7 +23,16 @@ set -euo pipefail
23
23
  # Every call the loop body makes below goes through `run_gtd` too, so the loop
24
24
  # always drives the exact same build/source it was launched from — no
25
25
  # dependence on a `gtd` binary being on PATH.
26
- SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
26
+ # Follow symlinks first: npm's global install exposes this script as a
27
+ # symlink in the node prefix's bin/, and dist/ only exists next to the REAL
28
+ # script inside the package.
29
+ SCRIPT_SOURCE="${BASH_SOURCE[0]}"
30
+ while [[ -L "$SCRIPT_SOURCE" ]]; do
31
+ SCRIPT_DIR="$(cd -- "$(dirname -- "$SCRIPT_SOURCE")" &>/dev/null && pwd)"
32
+ SCRIPT_SOURCE="$(readlink "$SCRIPT_SOURCE")"
33
+ [[ "$SCRIPT_SOURCE" != /* ]] && SCRIPT_SOURCE="$SCRIPT_DIR/$SCRIPT_SOURCE"
34
+ done
35
+ SCRIPT_DIR="$(cd -- "$(dirname -- "$SCRIPT_SOURCE")" &>/dev/null && pwd)"
27
36
 
28
37
  run_gtd() {
29
38
  if [[ -n "${GTD_BIN:-}" ]]; then
@@ -172,6 +181,23 @@ emit_output_since() {
172
181
  emit_block "$bytes"
173
182
  }
174
183
 
184
+ # A resume that failed because the session it names no longer exists — claude
185
+ # prints "No conversation found with session ID: <uuid>". Loose on purpose: any
186
+ # adapter whose CLI says something session-shaped gets the same recovery
187
+ # instead of stopping the loop.
188
+ SESSION_GONE_RE='no conversation found with session id|no such session|session (id )?[^ ]*( was)? not found|session [^ ]* does not exist'
189
+
190
+ # session_gone_since <offset> — true when the log bytes appended since <offset>
191
+ # carry the missing-session signature above. Blind (returns false) when no log
192
+ # is configured, so recovery is best-effort, never a hard dependency.
193
+ session_gone_since() {
194
+ local offset="$1" bytes=""
195
+ if [[ -n "${GTD_LOOP_LOG:-}" && -f "$GTD_LOOP_LOG" ]]; then
196
+ bytes="$(tail -c "+$((offset + 1))" "$GTD_LOOP_LOG" 2>/dev/null || true)"
197
+ fi
198
+ [[ -n "$bytes" ]] && grep -qiE "$SESSION_GONE_RE" <<<"$bytes"
199
+ }
200
+
175
201
  # emit_warn <message> — a surfaced but NON-fatal failure (a check script that
176
202
  # exited non-zero).
177
203
  emit_warn() {
@@ -330,7 +356,18 @@ fi
330
356
  # still resumes one planning session. The scope, session id, and a
331
357
  # resume-or-fresh flag are exported as $GTD_LOOP_MEMORY / $GTD_LOOP_SESSION_ID
332
358
  # / $GTD_LOOP_MEMORY_RESUME; the default adapter turns them into
333
- # `--resume`/`--session-id`. An adapter that ignores them keeps working.
359
+ # `--resume`/`--session-id`. An adapter that ignores them keeps working. A
360
+ # remembered session that has since vanished (the retention window elapsed, a
361
+ # wiped `~/.claude/projects`, a machine change) degrades to a fresh session
362
+ # with a warning rather than failing the turn — see run_agent_turn's own
363
+ # comment.
364
+
365
+ # new_session_id — mints an opaque session id for a fresh memory scope
366
+ # (uuidgen, falling back to the kernel's random uuid, or empty when neither is
367
+ # available — the default adapter then omits --session-id entirely).
368
+ new_session_id() {
369
+ uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || echo ""
370
+ }
334
371
 
335
372
  prev_state=""
336
373
  prev_content=""
@@ -362,7 +399,7 @@ fi
362
399
  # the resume flag; the memory scope ($memory/$session_id) is read from the
363
400
  # enclosing iteration, so a same-turn re-prompt (the validate gate) resumes the
364
401
  # same session.
365
- run_agent_turn() {
402
+ invoke_agent() {
366
403
  local prompt="$1" do_resume="$2" cmd="${GTD_LOOP_AGENT_CMD:-}"
367
404
  if [[ -z "$cmd" ]]; then
368
405
  cmd='claude -p "$GTD_LOOP_PROMPT"'
@@ -379,6 +416,33 @@ run_agent_turn() {
379
416
  >>"${GTD_LOOP_LOG:-/dev/null}" 2>&1
380
417
  }
381
418
 
419
+ # run_agent_turn <prompt> <resume> — invoke_agent, with one recovery: a
420
+ # resumed turn (do_resume == 1) that fails AND whose output carries the
421
+ # missing-session signature (session_gone_since) means the marker's session no
422
+ # longer exists on disk. Rather than failing the turn (and every turn after
423
+ # it, since the dead id would just be resumed again), mint a fresh session,
424
+ # rewrite $memory_marker with it — the self-healing part, so a later launch
425
+ # doesn't repeat this — and retry the SAME prompt once with resume off. $resume
426
+ # and $session_id are the enclosing iteration's globals; recovery rewrites them
427
+ # too, so a later validate-gate re-prompt in the same turn resumes the newly
428
+ # pinned session. Blind to the failure mode when session_gone_since can't tell
429
+ # (no log configured): a genuine agent failure then correctly propagates.
430
+ run_agent_turn() {
431
+ local prompt="$1" do_resume="$2" offset status=0
432
+ offset="$(log_offset)"
433
+ invoke_agent "$prompt" "$do_resume" && return 0
434
+ status=$?
435
+ if [[ "$do_resume" == "1" ]] && session_gone_since "$offset"; then
436
+ emit_warn "the remembered agent session for '$memory' is gone — continuing in a fresh session (this turn starts without its previous context)"
437
+ session_id="$(new_session_id)"
438
+ resume=0
439
+ printf '%s\n%s\n' "$memory" "$session_id" >"$memory_marker"
440
+ invoke_agent "$prompt" 0
441
+ return $?
442
+ fi
443
+ return $status
444
+ }
445
+
382
446
  # The furthest commit already reported to the terminal by report_commits (see
383
447
  # its own comment) — a global high-water mark rather than a per-beat
384
448
  # head_before, so a review checkout window's HEAD rewind between beats never
@@ -528,7 +592,7 @@ $next_out"
528
592
  resume=1
529
593
  session_id="$prev_session"
530
594
  else
531
- session_id="$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "")"
595
+ session_id="$(new_session_id)"
532
596
  fi
533
597
  printf '%s\n%s\n' "$memory" "$session_id" >"$memory_marker"
534
598
  else