create-egregore 0.21.0-runtime-mvp.17 → 0.21.0-runtime-mvp.18

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // egregore-launcher-version: 0.21.0-runtime-mvp.17
2
+ // egregore-launcher-version: 0.21.0-runtime-mvp.18
3
3
  //
4
4
  // egregore — launch and browse Egregore instances from your terminal.
5
5
  // Installed by create-egregore. Reads ~/.egregore/instances.json.
@@ -35,7 +35,7 @@ const crypto = require("node:crypto");
35
35
  const { spawnSync, spawn } = require("node:child_process");
36
36
  const readline = require("node:readline");
37
37
 
38
- const VERSION = "0.21.0-runtime-mvp.17";
38
+ const VERSION = "0.21.0-runtime-mvp.18";
39
39
  const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
40
40
  const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
41
41
  const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.21.0-runtime-mvp.17",
3
+ "version": "0.21.0-runtime-mvp.18",
4
4
  "description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code, Codex, or Pi.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,19 +1,16 @@
1
1
  #!/usr/bin/env bash
2
- # The attendant — Egregore's ambient listener. A per-instance daemon that
3
- # watches for work Claude Code sessions leave behind when they die without
4
- # ceremony (terminal closed, laptop shut) and captures it:
2
+ # The attendant — Egregore's per-instance background daemon. It pre-pays the
3
+ # launch path and keeps projections fresh. It never writes on a user's behalf:
5
4
  #
6
- # ripcord dead session + no handoff → deterministic auto-handoff to
7
- # memory/handoffs/ (kind: ripcord), assembled from the session
8
- # transcript + git state. No model, no API calls.
9
- # auto-push dirty/unpushed dev|feature|bugfix branches in this repo's
10
- # worktrees, once no session is active → commit + push the
11
- # branch. Never merges, never touches develop/main.
5
+ # warm fetch refs, replay the graph cache, bake the context seed every
6
+ # few minutes so session start finds everything hot.
7
+ # lifecycle run the handoff lifecycle job (Connected) on its own cadence.
8
+ # weaver trigger the Thread projection once no session is active.
12
9
  #
13
- # In-harness hooks (SessionEnd, PreCompact) die with the process; the
14
- # attendant lives outside it. The signal is Claude Code's own transcript
15
- # files (~/.claude/projects/<key>/*.jsonl), written continuously regardless
16
- # of how a session ends: transcript gone stale + work on disk = pull the cord.
10
+ # Ambient capture — ripcord notes and branch auto-push — was removed on
11
+ # 2026-09-03: a session that ends without /handoff or /wrap leaves nothing
12
+ # behind. Claude Code's transcript files (~/.claude/projects/<key>/*.jsonl)
13
+ # are read only to tell whether a session is still active.
17
14
  #
18
15
  # Usage:
19
16
  # bash bin/attendant.sh ensure # spawn daemon if not running (fire-and-forget from session-start)
@@ -23,7 +20,7 @@
23
20
  # bash bin/attendant.sh stop # kill the daemon
24
21
  #
25
22
  # Env overrides (mainly for tests):
26
- # ATTENDANT_STALE_MIN minutes of transcript silence = dead session (default 12)
23
+ # ATTENDANT_STALE_MIN minutes of transcript silence = no active session (default 12)
27
24
  # ATTENDANT_DRY_RUN=1 log actions, mutate nothing
28
25
  # ATTENDANT_HOME state dir (default ~/.egregore/attendant)
29
26
  # CLAUDE_PROJECTS_DIR transcript root (default ~/.claude/projects)
@@ -41,8 +38,6 @@ fi
41
38
  CONFIG="$MAIN_DIR/egregore.json"
42
39
  MEMORY_DIR="${EGREGORE_MEMORY_DIR:-$MAIN_DIR/memory}"
43
40
  STALE_MIN="${ATTENDANT_STALE_MIN:-12}"
44
- MAX_AGE_MIN=1440 # ignore transcripts older than 24h
45
- RIPCORD_MIN_BYTES=20000 # skip trivial sessions
46
41
  DRY_RUN="${ATTENDANT_DRY_RUN:-0}"
47
42
  CLAUDE_PROJECTS="${CLAUDE_PROJECTS_DIR:-$HOME/.claude/projects}"
48
43
 
@@ -52,7 +47,6 @@ STATE_DIR="${ATTENDANT_HOME:-$HOME/.egregore/attendant}"
52
47
  PIDFILE="$STATE_DIR/$KEY.pid"
53
48
  VERSIONFILE="$STATE_DIR/$KEY.version"
54
49
  LOGFILE="$STATE_DIR/$KEY.log"
55
- JOURNAL="$STATE_DIR/$KEY.journal"
56
50
  mkdir -p "$STATE_DIR"
57
51
 
58
52
  # Munged project key, same rule Claude Code uses for ~/.claude/projects dirs.
@@ -86,9 +80,8 @@ _trigger_auto_thread() {
86
80
  [ -f "$runner" ] || return 0
87
81
  [ "$DRY_RUN" = "1" ] && dry_arg="--dry-run"
88
82
 
89
- # Projection is downstream of capture. Run it detached so a slow or broken
90
- # renderer can never delay ripcord persistence or branch auto-save. The
91
- # runner's atomic lock coalesces ripcord + end-of-sweep triggers.
83
+ # Run the projection detached so a slow or broken renderer can never delay
84
+ # the sweep. The runner's atomic lock coalesces overlapping triggers.
92
85
  (
93
86
  if ! bash "$runner" \
94
87
  --repo-dir "$MAIN_DIR" \
@@ -99,23 +92,6 @@ _trigger_auto_thread() {
99
92
  ) >> "$LOGFILE" 2>&1 &
100
93
  }
101
94
 
102
- # Personal off switch for ambient capture. ripcord_enabled=false disables BOTH
103
- # ripcord notes and auto-push of dirty worktrees — a user who opts out of
104
- # ambient capture must not find silent commits either. The attendant's other
105
- # duties (graph cache, context seed, handoff lifecycle) are unaffected.
106
- # NOTE: jq's `//` treats false as empty (the auto_update:false gotcha), so the
107
- # test compares the raw value, never `.key // true`.
108
- _capture_enabled() {
109
- [ "$(jq -r '.ripcord_enabled' "$MAIN_DIR/.egregore-state.json" 2>/dev/null)" != "false" ]
110
- }
111
-
112
- _author() {
113
- local a
114
- a=$(jq -r '.github_username // empty' "$MAIN_DIR/.egregore-state.json" 2>/dev/null)
115
- [ -z "$a" ] && a=$(git -C "$MAIN_DIR" config user.name 2>/dev/null | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
116
- echo "${a:-unknown}"
117
- }
118
-
119
95
  # --- Session liveness ------------------------------------------------------
120
96
 
121
97
  _transcript_dirs() {
@@ -132,264 +108,6 @@ _active_session() {
132
108
  return 1
133
109
  }
134
110
 
135
- _stale_transcripts() {
136
- local d
137
- for d in $(_transcript_dirs); do
138
- find "$d" -maxdepth 1 -name '*.jsonl' -mmin "+$STALE_MIN" -mmin "-$MAX_AGE_MIN" -size "+${RIPCORD_MIN_BYTES}c" 2>/dev/null
139
- done
140
- }
141
-
142
- # --- Ripcord ---------------------------------------------------------------
143
-
144
- _last_prompts() {
145
- # Last few human prompts from a transcript, cleaned of harness noise.
146
- jq -r 'select(.type=="user") | .message.content
147
- | if type=="string" then .
148
- else ([.[]? | select(.type?=="text") | .text] | join(" ")) end' "$1" 2>/dev/null \
149
- | grep -v -e '^\s*$' -e 'system-reminder' -e '<command-' -e 'tool_use_error' -e '^Caveat:' -e '<task-notification' \
150
- -e '<local-command-caveat' -e '</local-command-caveat>' -e '<bash-input>' -e '<bash-stdout>' -e '<bash-stderr>' \
151
- | tail -5 | cut -c1-300
152
- }
153
-
154
- _slugify() {
155
- printf '%s' "$1" \
156
- | tr '[:upper:]' '[:lower:]' \
157
- | sed 's/<[^>]*>/ /g; s/[`*_#|:;,.!?()[\]{}"'"'"']/ /g; s/[^a-z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//' \
158
- | cut -c1-72 \
159
- | sed 's/-$//'
160
- }
161
-
162
- _prompt_topic_slug() {
163
- local prompts="$1"
164
- local line clean lower slug score best_score=0 best_slug=""
165
-
166
- while IFS= read -r line; do
167
- [ -z "$line" ] && continue
168
- lower=$(printf '%s' "$line" | tr '[:upper:]' '[:lower:]')
169
- case "$lower" in
170
- start|*request\ interrupted*|*system-reminder*|*local-command-caveat*|*bash-input*|*bash-stdout*|*bash-stderr*|*tool_use_error*)
171
- continue
172
- ;;
173
- esac
174
-
175
- clean=$(printf '%s' "$line" | sed 's/^>[[:space:]]*//; s/<[^>]*>/ /g; s/[|`*_#]/ /g')
176
- slug=$(_slugify "$clean")
177
- [ -z "$slug" ] && continue
178
- [ "${#slug}" -lt 8 ] && continue
179
-
180
- score=${#slug}
181
- case "$slug" in
182
- *handoff*|*emissary*|*harvest*|*ssh*|*pr*|*review*|*egregore*|*invite*|*deploy*|*release*)
183
- score=$((score + 20))
184
- ;;
185
- esac
186
-
187
- if [ "$score" -gt "$best_score" ]; then
188
- best_score="$score"
189
- best_slug="$slug"
190
- fi
191
- done <<EOF
192
- $prompts
193
- EOF
194
-
195
- printf '%s\n' "$best_slug"
196
- }
197
-
198
- _branch_topic_slug() {
199
- local gitsum="$1"
200
- local author="$2"
201
- local branch rest score best_score=0 best_slug=""
202
-
203
- while IFS= read -r branch; do
204
- [ -z "$branch" ] && continue
205
- case "$branch" in
206
- develop|main|master) continue ;;
207
- dev/"$author"/*) rest="${branch#dev/$author/}"; score=90 ;;
208
- feature/*|bugfix/*|hotfix/*) rest="${branch#*/}"; score=80 ;;
209
- dev/*/*) rest="${branch#dev/}"; rest="${rest#*/}"; score=60 ;;
210
- */*) rest="${branch#*/}"; score=50 ;;
211
- *) rest="$branch"; score=40 ;;
212
- esac
213
-
214
- rest=$(_slugify "$rest")
215
- [ -z "$rest" ] && continue
216
- if [ "$score" -gt "$best_score" ]; then
217
- best_score="$score"
218
- best_slug="$rest"
219
- fi
220
- done <<EOF
221
- $(printf '%s\n' "$gitsum" | awk -F'`' '/^- `/{print $2}')
222
- EOF
223
-
224
- printf '%s\n' "$best_slug"
225
- }
226
-
227
- _ripcord_topic_slug() {
228
- local prompts="$1"
229
- local gitsum="$2"
230
- local author="$3"
231
- local slug
232
-
233
- slug=$(_prompt_topic_slug "$prompts")
234
- [ -z "$slug" ] && slug=$(_branch_topic_slug "$gitsum" "$author")
235
- [ -z "$slug" ] && slug="session"
236
- printf '%s\n' "$slug"
237
- }
238
-
239
- _topic_from_slug() {
240
- printf '%s\n' "$1" | tr '-' ' '
241
- }
242
-
243
- _git_summary() {
244
- # Branch + dirty/unpushed state for the main checkout and every worktree.
245
- local wt branch dirty ahead
246
- git -C "$MAIN_DIR" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2}' | while read -r wt; do
247
- branch=$(git -C "$wt" branch --show-current 2>/dev/null)
248
- [ -z "$branch" ] && continue
249
- dirty=$(git -C "$wt" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
250
- ahead=$(git -C "$wt" rev-list --count '@{u}..HEAD' 2>/dev/null || echo "?")
251
- if [ "$dirty" != "0" ] || { [ "$ahead" != "0" ] && [ "$ahead" != "?" ]; }; then
252
- echo "- \`$branch\` — $dirty uncommitted, $ahead unpushed ($(basename "$wt"))"
253
- git -C "$wt" status --porcelain 2>/dev/null | head -8 | sed 's/^/ /'
254
- fi
255
- done
256
- }
257
-
258
- _ripcord() {
259
- local transcript="$1" base author date_ym date_d slug topic file prompts gitsum
260
- base=$(basename "$transcript" .jsonl)
261
-
262
- _capture_enabled || return 0
263
-
264
- grep -q "$base" "$JOURNAL" 2>/dev/null && return 0
265
- # Session already ended with ceremony? Then the attendant has no business
266
- # here. Scan only the tail — a session that merely *read* the /handoff or
267
- # /save skill text mid-way would otherwise false-positive.
268
- if tail -c 300000 "$transcript" 2>/dev/null | grep -q -e 'handoff-run.sh' -e 'wraps/'; then
269
- echo "ripcord-skip $base (explicit handoff/wrap)" >> "$JOURNAL"
270
- return 0
271
- fi
272
-
273
- author=$(_author)
274
- prompts=$(_last_prompts "$transcript")
275
- gitsum=$(_git_summary)
276
-
277
- # Nothing on disk and nothing said — not worth a handoff.
278
- if [ -z "$gitsum" ] && [ -z "$prompts" ]; then
279
- echo "ripcord-skip $base (no work found)" >> "$JOURNAL"
280
- return 0
281
- fi
282
-
283
- slug=$(_ripcord_topic_slug "$prompts" "$gitsum" "$author")
284
- topic=$(_topic_from_slug "$slug")
285
-
286
- date_ym=$(date '+%Y-%m'); date_d=$(date '+%d')
287
- file="$MEMORY_DIR/handoffs/$date_ym/${date_d}-${author}-ripcord-${slug}.md"
288
-
289
- if [ "$DRY_RUN" = "1" ]; then
290
- log "DRY ripcord → $file"
291
- _trigger_auto_thread "ripcord"
292
- return 0
293
- fi
294
-
295
- mkdir -p "$(dirname "$file")"
296
- {
297
- echo "---"
298
- echo "date: $(date '+%Y-%m-%d')"
299
- echo "author: $author"
300
- echo "to: $author"
301
- echo "kind: ripcord"
302
- echo "status: draft"
303
- echo "topic: $topic"
304
- echo "---"
305
- echo ""
306
- echo "# Ripcord: $topic"
307
- echo ""
308
- echo "Automatic capture — this session ended without an explicit handoff."
309
- echo "Assembled by the attendant from the flight recorder (transcript + git state)."
310
- echo ""
311
- if [ -n "$gitsum" ]; then
312
- echo "## Where the work lives"
313
- echo ""
314
- echo "$gitsum"
315
- echo ""
316
- fi
317
- if [ -n "$prompts" ]; then
318
- echo "## Last things the user asked for"
319
- echo ""
320
- echo "$prompts" | sed 's/^/> /'
321
- echo ""
322
- fi
323
- echo "## Pick it up"
324
- echo ""
325
- echo "Check out the branch above and run \`/activity\` for surrounding context."
326
- echo "Session transcript id: \`$base\` ($(date -r "$transcript" '+%Y-%m-%d %H:%M' 2>/dev/null))"
327
- } > "$file"
328
-
329
- # Commit + push memory (retry for concurrent pushers; tolerate no remote).
330
- local rel="${file#"$MEMORY_DIR"/}"
331
- git -C "$MEMORY_DIR" add "$rel" 2>/dev/null
332
- git -C "$MEMORY_DIR" commit -q -m "Ripcord: auto-handoff for $author ($slug)" 2>/dev/null
333
- if git -C "$MEMORY_DIR" remote get-url origin >/dev/null 2>&1; then
334
- local i
335
- for i in 1 2 3; do
336
- git -C "$MEMORY_DIR" pull --rebase origin main --quiet 2>/dev/null && \
337
- git -C "$MEMORY_DIR" push origin main --quiet 2>/dev/null && break
338
- sleep 2
339
- done
340
- fi
341
- bash "$MAIN_DIR/bin/index-handoff.sh" "$rel" >/dev/null 2>&1 || true
342
-
343
- echo "ripcord $base $rel" >> "$JOURNAL"
344
- log "ripcord → memory/$rel"
345
- _trigger_auto_thread "ripcord"
346
- }
347
-
348
- # --- Auto-push -------------------------------------------------------------
349
-
350
- _auto_push() {
351
- _capture_enabled || return 0
352
- local wt branch dirty ahead upstream
353
- git -C "$MAIN_DIR" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2}' | while read -r wt; do
354
- branch=$(git -C "$wt" branch --show-current 2>/dev/null)
355
- case "$branch" in dev/*|feature/*|bugfix/*) ;; *) continue ;; esac
356
- # Mid-rebase/merge — never touch.
357
- local gd; gd=$(git -C "$wt" rev-parse --git-dir 2>/dev/null)
358
- { [ -d "$gd/rebase-merge" ] || [ -d "$gd/rebase-apply" ] || [ -f "$gd/MERGE_HEAD" ]; } && continue
359
-
360
- dirty=$(git -C "$wt" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
361
- # Only act on work that has itself gone quiet (nothing modified recently).
362
- if [ "$dirty" != "0" ]; then
363
- local fresh
364
- fresh=$(git -C "$wt" status --porcelain 2>/dev/null | sed 's/^...//' | head -50 | while read -r f; do
365
- find "$wt/$f" -mmin "-$STALE_MIN" 2>/dev/null | head -1
366
- done | head -1)
367
- [ -n "$fresh" ] && continue
368
- if [ "$DRY_RUN" = "1" ]; then log "DRY auto-commit $branch ($dirty files, $wt)"; else
369
- git -C "$wt" add -A 2>/dev/null
370
- git -C "$wt" commit -q -m "chore(attendant): ripcord auto-save
371
-
372
- Uncommitted work captured after the session went quiet.
373
-
374
- Co-Authored-By: Egregore Attendant <noreply@egregore.xyz>" 2>/dev/null
375
- log "auto-commit $branch ($dirty files)"
376
- fi
377
- fi
378
-
379
- upstream=$(git -C "$wt" rev-parse --abbrev-ref '@{u}' 2>/dev/null || true)
380
- if [ -z "$upstream" ]; then
381
- ahead=$(git -C "$wt" rev-list --count origin/develop..HEAD 2>/dev/null || echo 0)
382
- else
383
- ahead=$(git -C "$wt" rev-list --count '@{u}..HEAD' 2>/dev/null || echo 0)
384
- fi
385
- if [ "${ahead:-0}" != "0" ]; then
386
- if [ "$DRY_RUN" = "1" ]; then log "DRY auto-push $branch ($ahead commits)"; else
387
- git -C "$wt" push -u origin "$branch" --quiet 2>/dev/null && log "auto-push $branch ($ahead commits)"
388
- fi
389
- fi
390
- done
391
- }
392
-
393
111
  # --- Warm: pre-pay the launch path -------------------------------------------
394
112
  # Session-start's cost is network: git fetches + the greeting's graph queries.
395
113
  # The attendant pays them in the background every WARM_MIN minutes so launch
@@ -594,11 +312,6 @@ cmd_sweep() {
594
312
  log "sweep: session active — standing by"
595
313
  return 0
596
314
  fi
597
- local t
598
- for t in $(_stale_transcripts); do
599
- _ripcord "$t"
600
- done
601
- _auto_push
602
315
  _trigger_auto_thread "sweep"
603
316
  }
604
317
 
@@ -632,7 +345,7 @@ cmd_ensure() {
632
345
  fi
633
346
  # Atomic spawn gate: the pidfile check above is check-then-spawn, so two
634
347
  # concurrent cold starts could both reach here and launch two daemons that
635
- # overlap for a full cycle (lifecycle jobs, ripcord, auto-push). mkdir is
348
+ # overlap for a full cycle (lifecycle jobs, warm, projection). mkdir is
636
349
  # the portable atomic primitive; a crashed spawner's stale lock breaks
637
350
  # after 5 minutes. Losing the race means someone else is spawning — done.
638
351
  local _spawn_lock="$STATE_DIR/$KEY.spawn-lock"
@@ -28,17 +28,6 @@ set -uo pipefail
28
28
  SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
29
29
  STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
30
30
 
31
- # The attendant runs against the MAIN checkout and reads its state file, so
32
- # ambient-capture settings must be written there — a worktree keeps its own
33
- # copy, and writing the flag into it would silently do nothing.
34
- MAIN_STATE_DIR="$SCRIPT_DIR"
35
- if [ -f "$SCRIPT_DIR/.git" ]; then
36
- _WT_GITDIR=$(sed 's/^gitdir: //' "$SCRIPT_DIR/.git" 2>/dev/null || true)
37
- [ -n "$_WT_GITDIR" ] && MAIN_STATE_DIR=$(cd "$_WT_GITDIR/../../.." 2>/dev/null && pwd || echo "$SCRIPT_DIR")
38
- fi
39
- MAIN_STATE_FILE="$MAIN_STATE_DIR/.egregore-state.json"
40
- [ -f "$MAIN_STATE_FILE" ] || MAIN_STATE_FILE="$STATE_FILE"
41
-
42
31
  [ -f "$STATE_FILE" ] || echo '{}' > "$STATE_FILE"
43
32
 
44
33
  # NB: jq's `//` swallows false — status would show "on" for enabled:false.
@@ -53,11 +42,6 @@ set_key() { # key value(json-encoded)
53
42
  jq ".$1 = $2" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
54
43
  }
55
44
 
56
- _set_main() { # key value(json-encoded) — writes where the attendant reads
57
- jq ".$1 = $2" "$MAIN_STATE_FILE" > "$MAIN_STATE_FILE.tmp" \
58
- && mv "$MAIN_STATE_FILE.tmp" "$MAIN_STATE_FILE"
59
- }
60
-
61
45
  status() {
62
46
  local enabled scope publish
63
47
  enabled=$(get '.autosave_enabled' 'false')
@@ -66,7 +50,6 @@ status() {
66
50
  echo "autosave: $([ "$enabled" = "true" ] && echo on || echo "off (opt-in — 'autosave on' to join the experiment)")"
67
51
  echo "scope: $scope (all = handoffs + artifacts + docs · handoffs = memory repo only)"
68
52
  echo "publish: $publish (gate = PRs wait for your merge · auto = merge to develop automatically)"
69
- echo "ripcord: $(jq -r 'if (.ripcord_enabled != null) then (.ripcord_enabled|tostring) else "true" end' "$MAIN_STATE_FILE" 2>/dev/null || echo true) (crash-recovery notes + branch auto-push when a session dies without /wrap)"
70
53
  }
71
54
 
72
55
  case "${1:-status}" in
@@ -78,15 +61,6 @@ case "${1:-status}" in
78
61
  all|handoffs) set_key autosave_scope "\"$2\"" && echo "scope: $2" ;;
79
62
  *) echo "usage: autosave.sh scope all|handoffs" >&2; exit 2 ;;
80
63
  esac ;;
81
- ripcord)
82
- # Ambient crash-recovery capture (the attendant): ripcord notes AND
83
- # auto-push of dirty task branches. Separate from the autosave
84
- # experiment above — this one defaults ON.
85
- case "${2:-}" in
86
- on) _set_main ripcord_enabled true && echo "ripcord: on — dead sessions leave a recovery note" ;;
87
- off) _set_main ripcord_enabled false && echo "ripcord: off — no ambient notes, no branch auto-push" ;;
88
- *) echo "usage: autosave.sh ripcord on|off" >&2; exit 2 ;;
89
- esac ;;
90
64
  publish)
91
65
  case "${2:-}" in
92
66
  gate|auto) set_key autosave_publish "\"$2\"" && echo "publish: $2" ;;
@@ -108,5 +82,5 @@ case "${1:-status}" in
108
82
  echo "✗ PR #$n did not merge (conflict with develop?) — it stays open"
109
83
  fi
110
84
  done ;;
111
- *) echo "usage: autosave.sh [status|on|off|scope all|handoffs|publish gate|auto|merge|ripcord on|off]" >&2; exit 2 ;;
85
+ *) echo "usage: autosave.sh [status|on|off|scope all|handoffs|publish gate|auto|merge]" >&2; exit 2 ;;
112
86
  esac
@@ -77,9 +77,9 @@ source "$SCRIPT_DIR/bin/lib/git-sync.sh" >/dev/null 2>/dev/null
77
77
  # Warm-state parity with the Claude SessionStart path: hand context.sh the
78
78
  # pre-baked snapshot and route greeting reads through the warm graph cache.
79
79
  # The attendant daemon is deliberately NOT started from the Codex/Pi path —
80
- # its dead-session detection reads Claude transcripts only, so a live Codex
81
- # or Pi session with 12 quiet minutes would look dead and could have its
82
- # unfinished work ripcorded/auto-pushed. Codex sessions still benefit from
80
+ # its session-liveness check reads Claude transcripts only, so a live Codex
81
+ # or Pi session with 12 quiet minutes would look idle and its sweep would
82
+ # run mid-work. Codex sessions still benefit from
83
83
  # any snapshot a Claude session's attendant maintains; runtime-neutral
84
84
  # liveness tracking is the prerequisite for starting the daemon here.
85
85
  if [ -n "${_ATT_KEY:-}" ]; then
@@ -574,10 +574,10 @@ jq -n \
574
574
  > "$BASELINE_FILE" 2>/dev/null || true
575
575
 
576
576
  # ============================================================
577
- # 7c. Attendant — ambient capture daemon (fire-and-forget)
577
+ # 7c. Attendant — background daemon (fire-and-forget)
578
578
  # ============================================================
579
- # Watches for sessions that die without ceremony (terminal closed) and
580
- # pulls the ripcord: auto-handoff + auto-push of stranded dev branches.
579
+ # Keeps the launch path warm (refs, graph cache, context seed), runs the
580
+ # handoff lifecycle job, and triggers the Thread projection when idle.
581
581
  # `ensure` is a pidfile check (~ms); the daemon itself runs detached.
582
582
  ( bash "$SCRIPT_DIR/bin/attendant.sh" ensure >/dev/null 2>&1 & ) 2>/dev/null
583
583