create-egregore 0.20.1 → 0.20.3

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 (58) hide show
  1. package/assets/egregore-launcher.js +34 -19
  2. package/bin/cli.js +3 -63
  3. package/lib/local.js +4 -20
  4. package/lib/setup.js +12 -16
  5. package/package.json +2 -2
  6. package/runtime/codex/.codex/spec-manifest.json +3 -3
  7. package/runtime/codex/AGENTS.md +23 -9
  8. package/runtime/codex/bin/agent.sh +15 -3
  9. package/runtime/codex/bin/attendant.sh +14 -301
  10. package/runtime/codex/bin/autosave.sh +1 -27
  11. package/runtime/codex/bin/codex-render-spec.mjs +5 -3
  12. package/runtime/codex/bin/codex-session-start.sh +3 -3
  13. package/runtime/codex/bin/contribute-guard.sh +79 -0
  14. package/runtime/codex/bin/handoff-save-egregore.sh +1 -1
  15. package/runtime/codex/bin/lib/git-sync.sh +16 -0
  16. package/runtime/codex/bin/lib/greeting.sh +2 -2
  17. package/runtime/codex/bin/search.sh +46 -4
  18. package/runtime/codex/bin/session-autosave.sh +6 -1
  19. package/runtime/codex/bin/settings.sh +62 -3
  20. package/runtime/codex/bin/worktree-create.sh +4 -1
  21. package/runtime/codex/manifest.json +1 -0
  22. package/runtime/pi/.codex/spec-manifest.json +3 -3
  23. package/runtime/pi/.pi/APPEND_SYSTEM.md +23 -9
  24. package/runtime/pi/.pi/spec-manifest.json +2 -2
  25. package/runtime/pi/AGENTS.md +23 -9
  26. package/runtime/pi/bin/agent.sh +15 -3
  27. package/runtime/pi/bin/attendant.sh +14 -301
  28. package/runtime/pi/bin/autosave.sh +1 -27
  29. package/runtime/pi/bin/codex-render-spec.mjs +5 -3
  30. package/runtime/pi/bin/codex-session-start.sh +3 -3
  31. package/runtime/pi/bin/contribute-guard.sh +79 -0
  32. package/runtime/pi/bin/handoff-save-egregore.sh +1 -1
  33. package/runtime/pi/bin/lib/git-sync.sh +16 -0
  34. package/runtime/pi/bin/lib/greeting.sh +2 -2
  35. package/runtime/pi/bin/search.sh +46 -4
  36. package/runtime/pi/bin/session-autosave.sh +6 -1
  37. package/runtime/pi/bin/settings.sh +62 -3
  38. package/runtime/pi/bin/worktree-create.sh +4 -1
  39. package/runtime/pi/manifest.json +1 -0
  40. package/runtime/prime/.codex/spec-manifest.json +3 -3
  41. package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +24 -10
  42. package/runtime/prime/.prime/agent/spec-manifest.json +2 -2
  43. package/runtime/prime/AGENTS.md +23 -9
  44. package/runtime/prime/bin/agent.sh +15 -3
  45. package/runtime/prime/bin/attendant.sh +14 -301
  46. package/runtime/prime/bin/autosave.sh +1 -27
  47. package/runtime/prime/bin/codex-render-spec.mjs +5 -3
  48. package/runtime/prime/bin/codex-session-start.sh +3 -3
  49. package/runtime/prime/bin/contribute-guard.sh +79 -0
  50. package/runtime/prime/bin/handoff-save-egregore.sh +1 -1
  51. package/runtime/prime/bin/lib/git-sync.sh +16 -0
  52. package/runtime/prime/bin/lib/greeting.sh +2 -2
  53. package/runtime/prime/bin/prime-render-spec.mjs +1 -1
  54. package/runtime/prime/bin/search.sh +46 -4
  55. package/runtime/prime/bin/session-autosave.sh +6 -1
  56. package/runtime/prime/bin/settings.sh +62 -3
  57. package/runtime/prime/bin/worktree-create.sh +4 -1
  58. package/runtime/prime/manifest.json +1 -0
@@ -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
@@ -193,7 +193,7 @@ The integration branch is \`develop\` unless top-level \`egregore.json.base_bran
193
193
 
194
194
  **Fallback:** If \`bin/agent.sh branch\` fails, resolve \`{base}\` with
195
195
  \`_get_base_branch\`, then use
196
- \`git checkout -b dev/{author}/{slug} origin/{base}\`.
196
+ \`git checkout --no-track -b dev/{author}/{slug} origin/{base}\`.
197
197
 
198
198
  4. Update graph (fire-and-forget): \`bash bin/graph-op.sh set-topic "$(cat .egregore-session-id 2>/dev/null)" "topic from slug" "dev/author/slug" 2>/dev/null &\`
199
199
 
@@ -259,9 +259,11 @@ The \`.codex/hooks/branch-guard.js\` PreToolUse hook (launcher \`--enable hooks\
259
259
  Wait for the reply, then branch.
260
260
  - **The user explicitly requested the protected branch** → that request is consent. Record it with \`echo '{branch}' > .egregore-branch-consent\`, then retry. Never create the marker merely to silence the hook.
261
261
 
262
- Memory, managed-repo, and runtime-state writes bypass the project guard — if one triggers it, correct the target/context instead of asking for consent.
262
+ If this Codex build does not support hooks, keep the same rule.
263
263
 
264
- If this Codex build does not support hooks, follow the same discipline as a standing instruction: never write or commit on the configured base, develop, main, or master.
264
+ **History:** Do not merge, cherry-pick, or rebase another task merely to read
265
+ it; use \`gh pr view/diff\`, \`git show\`, or a temporary worktree. Integrate
266
+ only for dependencies. Deletion needs no rewrite unless purging secrets.
265
267
 
266
268
  ### Onboarding exception
267
269
 
@@ -71,9 +71,9 @@ source "$SCRIPT_DIR/bin/lib/git-sync.sh" >/dev/null 2>/dev/null
71
71
  # Warm-state parity with the Claude SessionStart path: hand context.sh the
72
72
  # pre-baked snapshot and route greeting reads through the warm graph cache.
73
73
  # The attendant daemon is deliberately NOT started from the Codex/Pi path —
74
- # its dead-session detection reads Claude transcripts only, so a live Codex
75
- # or Pi session with 12 quiet minutes would look dead and could have its
76
- # unfinished work ripcorded/auto-pushed. Codex sessions still benefit from
74
+ # its session-liveness check reads Claude transcripts only, so a live Codex
75
+ # or Pi session with 12 quiet minutes would look idle and its sweep would
76
+ # run mid-work. Codex sessions still benefit from
77
77
  # any snapshot a Claude session's attendant maintains; runtime-neutral
78
78
  # liveness tracking is the prerequisite for starting the daemon here.
79
79
  if [ -n "${_ATT_KEY:-}" ]; then
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ set -u
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
5
+ CONFIG="$SCRIPT_DIR/egregore.json"
6
+ EXPECTED_REPO=""
7
+
8
+ usage() {
9
+ echo "usage: bash bin/contribute-guard.sh [--config <path>] [--expect <owner/repo>]" >&2
10
+ exit 2
11
+ }
12
+
13
+ fail() {
14
+ echo "contribute: $1" >&2
15
+ exit "${2:-2}"
16
+ }
17
+
18
+ while [ $# -gt 0 ]; do
19
+ case "$1" in
20
+ --config)
21
+ [ $# -ge 2 ] || usage
22
+ CONFIG="$2"
23
+ shift 2
24
+ ;;
25
+ --expect)
26
+ [ $# -ge 2 ] || usage
27
+ EXPECTED_REPO="$2"
28
+ shift 2
29
+ ;;
30
+ --help|-h)
31
+ usage
32
+ ;;
33
+ *)
34
+ usage
35
+ ;;
36
+ esac
37
+ done
38
+
39
+ [ -f "$CONFIG" ] || fail "cannot read configuration at $CONFIG; refusing to select an upstream target"
40
+
41
+ UPSTREAM_URL=$(jq -er '
42
+ if has("upstream_url") then
43
+ if .upstream_url == null then ""
44
+ elif (.upstream_url | type) == "string" then .upstream_url
45
+ else error("upstream_url must be a string")
46
+ end
47
+ else ""
48
+ end
49
+ ' "$CONFIG" 2>/dev/null) ||
50
+ fail "configuration is invalid; refusing to select an upstream target"
51
+
52
+ if [ "$UPSTREAM_URL" = "none" ]; then
53
+ fail 'disabled because upstream_url is "none"; this checkout is a framework source, so use the save workflow for its configured integration branch' 3
54
+ fi
55
+
56
+ [ -n "$UPSTREAM_URL" ] || UPSTREAM_URL="https://github.com/egregore-labs/egregore.git"
57
+
58
+ case "$UPSTREAM_URL" in
59
+ https://github.com/*/*)
60
+ UPSTREAM_REPO="${UPSTREAM_URL#*github.com/}"
61
+ ;;
62
+ git@github.com:*/*)
63
+ UPSTREAM_REPO="${UPSTREAM_URL#git@github.com:}"
64
+ ;;
65
+ *)
66
+ fail "unsupported upstream_url '$UPSTREAM_URL'; expected a GitHub repository URL"
67
+ ;;
68
+ esac
69
+
70
+ UPSTREAM_REPO="${UPSTREAM_REPO%/}"
71
+ UPSTREAM_REPO="${UPSTREAM_REPO%.git}"
72
+ printf '%s\n' "$UPSTREAM_REPO" | grep -Eq '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$' ||
73
+ fail "invalid upstream repository '$UPSTREAM_REPO'; refusing external Git operations"
74
+
75
+ if [ -n "$EXPECTED_REPO" ] && [ "$EXPECTED_REPO" != "$UPSTREAM_REPO" ]; then
76
+ fail "target changed from '$EXPECTED_REPO' to '$UPSTREAM_REPO'; refusing external Git operations"
77
+ fi
78
+
79
+ printf '%s\n' "$UPSTREAM_REPO"
@@ -135,7 +135,7 @@ if [ -z "$CURRENT_BRANCH" ] || [ "$CURRENT_BRANCH" = "$BASE_BRANCH" ] \
135
135
  # hash from the autosave sweep) — same-name pushes wedge on non-FF.
136
136
  NEW_BRANCH="dev/${AUTHOR}/${KIND}-${SLUG_DATE}${BRANCH_SUFFIX:+-$BRANCH_SUFFIX}"
137
137
  git fetch origin "$BASE_BRANCH" --quiet 2>/dev/null || true
138
- git checkout -b "$NEW_BRANCH" "$BASE_REF" --quiet 2>/dev/null || \
138
+ git checkout --no-track -b "$NEW_BRANCH" "$BASE_REF" --quiet 2>/dev/null || \
139
139
  git checkout "$NEW_BRANCH" --quiet 2>/dev/null || exit 0
140
140
  CURRENT_BRANCH="$NEW_BRANCH"
141
141
  fi
@@ -32,6 +32,22 @@ if [ -z "${BASE_BRANCH:-}" ]; then
32
32
  fi
33
33
  fi
34
34
 
35
+ # Migrate task branches created by older Egregore versions. `git branch
36
+ # <task> origin/<base>` implicitly made the task branch track the protected
37
+ # integration branch, so a bare push could target it when push.default was
38
+ # `upstream`. Task branches remain unpublished until the explicit save/push
39
+ # flow creates their same-name remote and sets that as upstream.
40
+ _CURRENT_TASK_BRANCH="$(git branch --show-current 2>/dev/null || true)"
41
+ case "$_CURRENT_TASK_BRANCH" in
42
+ dev/*|feature/*|bugfix/*)
43
+ _CURRENT_TASK_UPSTREAM="$(git rev-parse --abbrev-ref '@{upstream}' 2>/dev/null || true)"
44
+ if [ "$_CURRENT_TASK_UPSTREAM" = "origin/$BASE_BRANCH" ]; then
45
+ git branch --unset-upstream "$_CURRENT_TASK_BRANCH" >/dev/null 2>&1 || true
46
+ fi
47
+ ;;
48
+ esac
49
+ unset _CURRENT_TASK_BRANCH _CURRENT_TASK_UPSTREAM
50
+
35
51
  # --- Ensure pull.rebase is set (prevents "divergent branches" errors) ---
36
52
  git config pull.rebase true 2>/dev/null || true
37
53
 
@@ -13,7 +13,7 @@
13
13
  # - Telemetry emit
14
14
  # - First session welcome
15
15
  #
16
- # Inputs: SCRIPT_DIR, STATE_FILE, CONFIG, CTX_DIR, BRANCH, COMMITS_AHEAD,
16
+ # Inputs: SCRIPT_DIR, STATE_FILE, CONFIG, CTX_DIR, BRANCH, BASE_BRANCH, COMMITS_AHEAD,
17
17
  # AUTHOR, LOCAL_MODE, MEMORY_SYNCED, SAVED_BRANCH,
18
18
  # HEALTH_*, FRAMEWORK_VERSION, TIME_OF_DAY, EGREGORE_SESSION_ID,
19
19
  # FIRST_SESSION, DISPLAY_NAME_STATE, DASHBOARD_URL
@@ -515,5 +515,5 @@ else
515
515
  echo ""
516
516
  echo "IMPORTANT: Display the above greeting to the user exactly as-is (preserve the ASCII art formatting and ornamented status) on their first message. Then ask: What are you working on?"
517
517
  echo ""
518
- echo "BRANCH RULE: When the user responds with what they're working on, your FIRST action is to create a working branch: git fetch origin develop --quiet && git checkout -b dev/{author}/{topic-slug} origin/develop. Do this BEFORE any other work. Derive the topic slug from their description. If they ask a pure question with no work intent, skip branching."
518
+ echo "BRANCH RULE: When the user responds with what they're working on, your FIRST action is to create a working branch: git fetch origin $BASE_BRANCH --quiet && git checkout --no-track -b dev/{author}/{topic-slug} origin/$BASE_BRANCH. Do this BEFORE any other work. Derive the topic slug from their description. If they ask a pure question with no work intent, skip branching."
519
519
  fi