@pmelab/gtd 8.3.1 → 8.5.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 (5) hide show
  1. package/README.md +320 -118
  2. package/bin/gtd +80 -29
  3. package/dist/gtd.bundle.mjs +4684 -3247
  4. package/package.json +11 -9
  5. package/schema.json +147 -265
package/bin/gtd CHANGED
@@ -369,11 +369,63 @@ new_session_id() {
369
369
  uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || echo ""
370
370
  }
371
371
 
372
+ # scope_of <key> — the scope part of a "<scope>#<hash>" memory key: everything
373
+ # before the first "#". Used to decide which existing table row a fresh entry
374
+ # into a scope displaces (see write_scope_session).
375
+ scope_of() {
376
+ printf '%s' "${1%%#*}"
377
+ }
378
+
379
+ # lookup_scope_session <key> — print the session id remembered for EXACTLY
380
+ # this key (a row whose first field string-equals it), or fail with no output
381
+ # if no row matches (a lookup miss -> the caller starts fresh). $memory_marker
382
+ # absent counts as no rows at all.
383
+ lookup_scope_session() {
384
+ local key="$1" row_key row_sid
385
+ [[ -f "$memory_marker" ]] || return 1
386
+ while read -r row_key row_sid; do
387
+ if [[ "$row_key" == "$key" ]]; then
388
+ printf '%s' "$row_sid"
389
+ return 0
390
+ fi
391
+ done <"$memory_marker"
392
+ return 1
393
+ }
394
+
395
+ # write_scope_session <key> <session_id> — replace the ONE row for this key's
396
+ # SCOPE (scope_of) with a fresh row for <key>, leaving every OTHER scope's row
397
+ # in the table untouched. A scope has at most one live conversation at any
398
+ # moment, so an existing row for this scope is dead the instant a new entry
399
+ # into it begins — this is the entire pruning story, no process concept
400
+ # needed (see the module's own memory-scope doc comment below). The table is
401
+ # rewritten via a sibling temp file + rename, so a crash mid-write never
402
+ # leaves a half-written table.
403
+ write_scope_session() {
404
+ local key="$1" session_id="$2" scope tmp row_key row_sid
405
+ scope="$(scope_of "$key")"
406
+ tmp="$(mktemp "${memory_marker}.XXXXXX")"
407
+ if [[ -f "$memory_marker" ]]; then
408
+ while read -r row_key row_sid; do
409
+ [[ "$(scope_of "$row_key")" == "$scope" ]] && continue
410
+ printf '%s %s\n' "$row_key" "$row_sid" >>"$tmp"
411
+ done <"$memory_marker"
412
+ fi
413
+ printf '%s %s\n' "$key" "$session_id" >>"$tmp"
414
+ mv "$tmp" "$memory_marker"
415
+ }
416
+
372
417
  prev_state=""
373
418
  prev_content=""
374
419
  prev_head=""
375
- # Where the last "prompt" turn's `memory` scope + session id are remembered
376
- # across iterations and across restarts. In the git dir, never the work tree.
420
+ # Where each scope's remembered agent session is persisted across iterations
421
+ # and across restarts, as a TABLE — one "<key> <session_id>" row per scope
422
+ # (keys contain no whitespace, so plain field-splitting is safe) — never a
423
+ # single last-label value: with machine-scoped memory, a scope's key can
424
+ # legitimately reappear AFTER the loop has run turns in a completely
425
+ # different (or nested) scope — e.g. build.fix -> into a nested health-gate
426
+ # child's own agent turn -> back to build.fix, still the same builder
427
+ # conversation. "The last label" can't represent that; a table where each
428
+ # scope owns exactly one row can. In the git dir, never the work tree.
377
429
  memory_marker="$(worktree_git_dir 2>/dev/null || echo .git)/gtd-loop-memory"
378
430
 
379
431
  # Truncated once here (not every iteration), so the file always reflects the
@@ -418,15 +470,17 @@ invoke_agent() {
418
470
 
419
471
  # run_agent_turn <prompt> <resume> — invoke_agent, with one recovery: a
420
472
  # 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.
473
+ # missing-session signature (session_gone_since) means the CURRENT scope's
474
+ # remembered session no longer exists on disk. Rather than failing the turn
475
+ # (and every turn after it, since the dead id would just be resumed again),
476
+ # mint a fresh session and rewrite ONLY this scope's row in $memory_marker
477
+ # (write_scope_session — every other scope's row survives untouched) — the
478
+ # self-healing part, so a later launch doesn't repeat this — and retry the
479
+ # SAME prompt once with resume off. $resume and $session_id are the enclosing
480
+ # iteration's globals; recovery rewrites them too, so a later validate-gate
481
+ # re-prompt in the same turn resumes the newly pinned session. Blind to the
482
+ # failure mode when session_gone_since can't tell (no log configured): a
483
+ # genuine agent failure then correctly propagates.
430
484
  run_agent_turn() {
431
485
  local prompt="$1" do_resume="$2" offset status=0
432
486
  offset="$(log_offset)"
@@ -436,7 +490,7 @@ run_agent_turn() {
436
490
  emit_warn "the remembered agent session for '$memory' is gone — continuing in a fresh session (this turn starts without its previous context)"
437
491
  session_id="$(new_session_id)"
438
492
  resume=0
439
- printf '%s\n%s\n' "$memory" "$session_id" >"$memory_marker"
493
+ write_scope_session "$memory" "$session_id"
440
494
  invoke_agent "$prompt" 0
441
495
  return $?
442
496
  fi
@@ -459,7 +513,7 @@ reported_head="$(git rev-parse HEAD 2>/dev/null || echo none)"
459
513
  # We peek with the pure, mutation-free `gtd next --json` and step ONLY when it
460
514
  # reports a human rest (`kind == "message"`): at an agent/check rest
461
515
  # `gtd step human` would refuse out-of-turn (non-zero, aborting under `set -e`),
462
- # and a mid-cycle restart must simply resume driving, not fail. A clean gate is
516
+ # and a mid-process restart must simply resume driving, not fail. A clean gate is
463
517
  # a harmless no-op; a genuine refusal there (a malformed steering file, or an
464
518
  # edit no `on` pattern accepts) is surfaced and stops the loop rather than being
465
519
  # driven past. --once treats a captured commit here as the run's one beat and
@@ -574,29 +628,26 @@ $next_out"
574
628
  prev_content="$content"
575
629
  prev_head="$cur_head"
576
630
 
577
- # Agent-memory scope: compare this turn's `memory` label against the last
578
- # prompt turn's (persisted in $memory_marker). Same non-empty label AND a
579
- # known prior session -> resume that session; otherwise start fresh with a
580
- # new session id. An unset label carries no constraint (harness default) and
581
- # clears the marker so a later labelled turn can't accidentally match it.
582
- prev_memory=""
583
- prev_session=""
584
- if [[ -f "$memory_marker" ]]; then
585
- prev_memory="$(sed -n 1p "$memory_marker")"
586
- prev_session="$(sed -n 2p "$memory_marker")"
587
- fi
631
+ # Agent-memory scope: look this turn's `memory` key up in the per-scope
632
+ # table (lookup_scope_session — exact string equality against a row's full
633
+ # key, "<scope>#<hash>", not just its scope part). A hit -> resume that
634
+ # session; a miss -> start fresh with a new session id, then write ONLY this
635
+ # key's scope's row (write_scope_session), leaving every other scope's row
636
+ # untouched — a scope can legitimately go quiet and reappear later (see the
637
+ # $memory_marker declaration's own comment) without losing its place. An
638
+ # absent/empty key carries no constraint (harness default) and leaves the
639
+ # table completely untouched — it may hold live sessions for OTHER scopes
640
+ # this turn knows nothing about, so it's neither read nor written here.
588
641
  resume=0
589
642
  session_id=""
590
643
  if [[ -n "$memory" ]]; then
591
- if [[ "$memory" == "$prev_memory" && -n "$prev_session" ]]; then
644
+ if found_session_id="$(lookup_scope_session "$memory")"; then
592
645
  resume=1
593
- session_id="$prev_session"
646
+ session_id="$found_session_id"
594
647
  else
595
648
  session_id="$(new_session_id)"
596
649
  fi
597
- printf '%s\n%s\n' "$memory" "$session_id" >"$memory_marker"
598
- else
599
- rm -f "$memory_marker"
650
+ write_scope_session "$memory" "$session_id"
600
651
  fi
601
652
 
602
653
  # Run the agent turn (see run_agent_turn's own comment for the adapter