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.
- package/assets/egregore-launcher.js +2 -2
- package/package.json +1 -1
- package/runtime/claude/bin/attendant.sh +14 -301
- package/runtime/claude/bin/autosave.sh +1 -27
- package/runtime/claude/bin/codex-session-start.sh +3 -3
- package/runtime/claude/bin/session-start.sh +3 -3
- package/runtime/codex/bin/attendant.sh +14 -301
- package/runtime/codex/bin/autosave.sh +1 -27
- package/runtime/codex/bin/codex-session-start.sh +3 -3
- package/runtime/pi/bin/attendant.sh +14 -301
- package/runtime/pi/bin/autosave.sh +1 -27
- package/runtime/pi/bin/codex-session-start.sh +3 -3
- package/runtime/prime/bin/attendant.sh +14 -301
- package/runtime/prime/bin/autosave.sh +1 -27
- package/runtime/prime/bin/codex-session-start.sh +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// egregore-launcher-version: 0.21.0-runtime-mvp.
|
|
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.
|
|
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.
|
|
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
|
|
3
|
-
#
|
|
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
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# files (~/.claude/projects/<key>/*.jsonl)
|
|
16
|
-
#
|
|
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 =
|
|
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
|
-
#
|
|
90
|
-
#
|
|
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,
|
|
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
|
|
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
|
|
81
|
-
# or Pi session with 12 quiet minutes would look
|
|
82
|
-
#
|
|
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 —
|
|
577
|
+
# 7c. Attendant — background daemon (fire-and-forget)
|
|
578
578
|
# ============================================================
|
|
579
|
-
#
|
|
580
|
-
#
|
|
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
|
|