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.
- package/assets/egregore-launcher.js +34 -19
- package/bin/cli.js +3 -63
- package/lib/local.js +4 -20
- package/lib/setup.js +12 -16
- package/package.json +2 -2
- package/runtime/codex/.codex/spec-manifest.json +3 -3
- package/runtime/codex/AGENTS.md +23 -9
- package/runtime/codex/bin/agent.sh +15 -3
- package/runtime/codex/bin/attendant.sh +14 -301
- package/runtime/codex/bin/autosave.sh +1 -27
- package/runtime/codex/bin/codex-render-spec.mjs +5 -3
- package/runtime/codex/bin/codex-session-start.sh +3 -3
- package/runtime/codex/bin/contribute-guard.sh +79 -0
- package/runtime/codex/bin/handoff-save-egregore.sh +1 -1
- package/runtime/codex/bin/lib/git-sync.sh +16 -0
- package/runtime/codex/bin/lib/greeting.sh +2 -2
- package/runtime/codex/bin/search.sh +46 -4
- package/runtime/codex/bin/session-autosave.sh +6 -1
- package/runtime/codex/bin/settings.sh +62 -3
- package/runtime/codex/bin/worktree-create.sh +4 -1
- package/runtime/codex/manifest.json +1 -0
- package/runtime/pi/.codex/spec-manifest.json +3 -3
- package/runtime/pi/.pi/APPEND_SYSTEM.md +23 -9
- package/runtime/pi/.pi/spec-manifest.json +2 -2
- package/runtime/pi/AGENTS.md +23 -9
- package/runtime/pi/bin/agent.sh +15 -3
- package/runtime/pi/bin/attendant.sh +14 -301
- package/runtime/pi/bin/autosave.sh +1 -27
- package/runtime/pi/bin/codex-render-spec.mjs +5 -3
- package/runtime/pi/bin/codex-session-start.sh +3 -3
- package/runtime/pi/bin/contribute-guard.sh +79 -0
- package/runtime/pi/bin/handoff-save-egregore.sh +1 -1
- package/runtime/pi/bin/lib/git-sync.sh +16 -0
- package/runtime/pi/bin/lib/greeting.sh +2 -2
- package/runtime/pi/bin/search.sh +46 -4
- package/runtime/pi/bin/session-autosave.sh +6 -1
- package/runtime/pi/bin/settings.sh +62 -3
- package/runtime/pi/bin/worktree-create.sh +4 -1
- package/runtime/pi/manifest.json +1 -0
- package/runtime/prime/.codex/spec-manifest.json +3 -3
- package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +24 -10
- package/runtime/prime/.prime/agent/spec-manifest.json +2 -2
- package/runtime/prime/AGENTS.md +23 -9
- package/runtime/prime/bin/agent.sh +15 -3
- package/runtime/prime/bin/attendant.sh +14 -301
- package/runtime/prime/bin/autosave.sh +1 -27
- package/runtime/prime/bin/codex-render-spec.mjs +5 -3
- package/runtime/prime/bin/codex-session-start.sh +3 -3
- package/runtime/prime/bin/contribute-guard.sh +79 -0
- package/runtime/prime/bin/handoff-save-egregore.sh +1 -1
- package/runtime/prime/bin/lib/git-sync.sh +16 -0
- package/runtime/prime/bin/lib/greeting.sh +2 -2
- package/runtime/prime/bin/prime-render-spec.mjs +1 -1
- package/runtime/prime/bin/search.sh +46 -4
- package/runtime/prime/bin/session-autosave.sh +6 -1
- package/runtime/prime/bin/settings.sh +62 -3
- package/runtime/prime/bin/worktree-create.sh +4 -1
- package/runtime/prime/manifest.json +1 -0
|
@@ -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
|
|
@@ -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
|
-
|
|
262
|
+
If this Codex build does not support hooks, keep the same rule.
|
|
263
263
|
|
|
264
|
-
|
|
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
|
|
75
|
-
# or Pi session with 12 quiet minutes would look
|
|
76
|
-
#
|
|
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
|
|
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
|
|
@@ -107,7 +107,7 @@ Read the selected skill's \`SKILL.md\` completely before acting. Codex wording i
|
|
|
107
107
|
|
|
108
108
|
The \`egregore\` Python-backed skill exposes the mechanics as kernel functions — \`import egregore\`, then \`egregore.search(query)\`, \`egregore.activity()\`, \`egregore.handoff(to, topic, body)\`, \`egregore.save(message, topic)\`, \`egregore.branch(topic)\`, \`egregore.notify_plan(recipient, message)\`. Prefer these over re-deriving \`bin/\` CLI usage by reading scripts. \`notify_plan\` is proposal-only: approval and dispatch always pass through the explicit human Send / Edit / Cancel checkpoint.
|
|
109
109
|
|
|
110
|
-
"Get the latest changes" inside a session means \`/reload\` — it re-reads instructions, context files, skills, and extensions from disk. Never \`git pull\`, \`git reset\`, or \`git checkout\` the working branch to pick up updates. When pivoting topics inside a worktree that carries unmerged branch work, create the new branch from the current HEAD, not \`origin/
|
|
110
|
+
"Get the latest changes" inside a session means \`/reload\` — it re-reads instructions, context files, skills, and extensions from disk. Never \`git pull\`, \`git reset\`, or \`git checkout\` the working branch to pick up updates. When pivoting topics inside a worktree that carries unmerged branch work, create the new branch from the current HEAD, not \`origin/{base}\` — branching from the configured base in such a worktree removes the unmerged files from disk.
|
|
111
111
|
|
|
112
112
|
Kernel shell escapes can be time-capped by the harness. Egregore \`bin/\` scripts that sync git state or query the connected graph legitimately run 10–120 seconds: run them with a generous timeout (a \`%%bash\` cell, or the longest limit the harness exposes). If a command dies suspiciously fast (~5s) with partial output, treat it as a timeout — rerun once with a longer limit or render the workflow's designed one-line fallback. Never debug data plumbing inside a product workflow. Connected mode works normally here: graph reads and \`bin/search.sh\` enrichment use the instance's configured credentials, and when graph data is degraded say so in one plain line rather than narrating connectivity theories.`, CODEX_COMMAND_LEAD);
|
|
113
113
|
|