@pmelab/gtd 8.2.1 → 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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/bin/gtd +68 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -308,6 +308,13 @@ name surfaced in `gtd next --json`/`gtd status`. The driver uses it for its
308
308
  per-beat progress lines; an outer wrapper (a terminal multiplexer, a notifier)
309
309
  can use it the same way.
310
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
+
311
318
  ### Terminal-multiplexer status: a herdr wrapper
312
319
 
313
320
  [herdr](https://herdr.dev) shows a per-pane status in its sidebar. This wrapper
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "8.2.1",
3
+ "version": "8.3.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {