create-egregore 0.17.4 → 0.18.1

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 (106) hide show
  1. package/assets/egregore-launcher.js +1965 -0
  2. package/assets/egregore-launcher.sh +385 -0
  3. package/bin/cli.js +472 -81
  4. package/lib/api.js +2 -2
  5. package/lib/auth.js +255 -20
  6. package/lib/codex-runtime.js +268 -0
  7. package/lib/commands.js +31 -0
  8. package/lib/context.js +1 -1
  9. package/lib/local.js +616 -189
  10. package/lib/setup.js +822 -84
  11. package/lib/ui.js +780 -84
  12. package/package.json +12 -3
  13. package/runtime/codex/.codex/config.toml +15 -0
  14. package/runtime/codex/.codex/hooks/branch-guard.js +234 -0
  15. package/runtime/codex/.codex/hooks.json +17 -0
  16. package/runtime/codex/.codex/skills/activity/SKILL.md +70 -0
  17. package/runtime/codex/.codex/skills/add/SKILL.md +25 -0
  18. package/runtime/codex/.codex/skills/announce/SKILL.md +54 -0
  19. package/runtime/codex/.codex/skills/answer/SKILL.md +25 -0
  20. package/runtime/codex/.codex/skills/archive/SKILL.md +41 -0
  21. package/runtime/codex/.codex/skills/ask/SKILL.md +59 -0
  22. package/runtime/codex/.codex/skills/branch/SKILL.md +25 -0
  23. package/runtime/codex/.codex/skills/checkup/SKILL.md +41 -0
  24. package/runtime/codex/.codex/skills/commit/SKILL.md +25 -0
  25. package/runtime/codex/.codex/skills/contribute/SKILL.md +25 -0
  26. package/runtime/codex/.codex/skills/dark-mode/SKILL.md +25 -0
  27. package/runtime/codex/.codex/skills/dashboard/SKILL.md +59 -0
  28. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +70 -0
  29. package/runtime/codex/.codex/skills/delete-user/SKILL.md +25 -0
  30. package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +25 -0
  31. package/runtime/codex/.codex/skills/emissary/SKILL.md +41 -0
  32. package/runtime/codex/.codex/skills/env/SKILL.md +25 -0
  33. package/runtime/codex/.codex/skills/handoff/SKILL.md +67 -0
  34. package/runtime/codex/.codex/skills/harvest/SKILL.md +107 -0
  35. package/runtime/codex/.codex/skills/infra/SKILL.md +41 -0
  36. package/runtime/codex/.codex/skills/ingest/SKILL.md +25 -0
  37. package/runtime/codex/.codex/skills/ingest-google/SKILL.md +25 -0
  38. package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  39. package/runtime/codex/.codex/skills/invite/SKILL.md +40 -0
  40. package/runtime/codex/.codex/skills/issue/SKILL.md +41 -0
  41. package/runtime/codex/.codex/skills/me/SKILL.md +25 -0
  42. package/runtime/codex/.codex/skills/meeting/SKILL.md +41 -0
  43. package/runtime/codex/.codex/skills/note/SKILL.md +25 -0
  44. package/runtime/codex/.codex/skills/onboarding/SKILL.md +41 -0
  45. package/runtime/codex/.codex/skills/pr/SKILL.md +25 -0
  46. package/runtime/codex/.codex/skills/project/SKILL.md +41 -0
  47. package/runtime/codex/.codex/skills/pull/SKILL.md +25 -0
  48. package/runtime/codex/.codex/skills/push/SKILL.md +25 -0
  49. package/runtime/codex/.codex/skills/quest/SKILL.md +77 -0
  50. package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +25 -0
  51. package/runtime/codex/.codex/skills/reflect/SKILL.md +41 -0
  52. package/runtime/codex/.codex/skills/review-pr/SKILL.md +41 -0
  53. package/runtime/codex/.codex/skills/save/SKILL.md +62 -0
  54. package/runtime/codex/.codex/skills/setup/SKILL.md +25 -0
  55. package/runtime/codex/.codex/skills/summon/SKILL.md +41 -0
  56. package/runtime/codex/.codex/skills/sync-repos/SKILL.md +25 -0
  57. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +25 -0
  58. package/runtime/codex/.codex/skills/telemetry/SKILL.md +25 -0
  59. package/runtime/codex/.codex/skills/test/SKILL.md +41 -0
  60. package/runtime/codex/.codex/skills/the-spiral/SKILL.md +84 -0
  61. package/runtime/codex/.codex/skills/todo/SKILL.md +41 -0
  62. package/runtime/codex/.codex/skills/tui-design/SKILL.md +41 -0
  63. package/runtime/codex/.codex/skills/tutorial/SKILL.md +41 -0
  64. package/runtime/codex/.codex/skills/update/SKILL.md +25 -0
  65. package/runtime/codex/.codex/skills/view/SKILL.md +41 -0
  66. package/runtime/codex/.codex/skills/visual-explain/SKILL.md +41 -0
  67. package/runtime/codex/.codex/skills/wrap/SKILL.md +55 -0
  68. package/runtime/codex/.codex/spec-manifest.json +108 -0
  69. package/runtime/codex/AGENTS.md +40 -0
  70. package/runtime/codex/bin/activity-data.sh +177 -0
  71. package/runtime/codex/bin/agent.sh +669 -0
  72. package/runtime/codex/bin/codex-install-skills.sh +78 -0
  73. package/runtime/codex/bin/codex-render-spec.mjs +308 -0
  74. package/runtime/codex/bin/codex-session-start.sh +238 -0
  75. package/runtime/codex/bin/codex-skill-render.mjs +315 -0
  76. package/runtime/codex/bin/codex-sync-skills.sh +223 -0
  77. package/runtime/codex/bin/dashboard-data.sh +174 -0
  78. package/runtime/codex/bin/graph-batch.sh +60 -0
  79. package/runtime/codex/bin/graph-op.sh +380 -0
  80. package/runtime/codex/bin/graph-wal.sh +228 -0
  81. package/runtime/codex/bin/graph.sh +141 -0
  82. package/runtime/codex/bin/handoff-pr-backfill.sh +86 -0
  83. package/runtime/codex/bin/handoff-run.sh +431 -0
  84. package/runtime/codex/bin/handoff-save-egregore.sh +131 -0
  85. package/runtime/codex/bin/index-handoff.sh +306 -0
  86. package/runtime/codex/bin/lib/artifact-id.sh +46 -0
  87. package/runtime/codex/bin/lib/config.sh +71 -0
  88. package/runtime/codex/bin/lib/context.sh +463 -0
  89. package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -0
  90. package/runtime/codex/bin/lib/ensure-develop.sh +183 -0
  91. package/runtime/codex/bin/lib/git-sync.sh +262 -0
  92. package/runtime/codex/bin/lib/greeting.sh +414 -0
  93. package/runtime/codex/bin/lib/hash.sh +13 -0
  94. package/runtime/codex/bin/lib/identity.sh +164 -0
  95. package/runtime/codex/bin/lib/permissions.sh +106 -0
  96. package/runtime/codex/bin/lib/time.sh +56 -0
  97. package/runtime/codex/bin/lib/worktree-links.sh +54 -0
  98. package/runtime/codex/bin/notify.sh +207 -0
  99. package/runtime/codex/bin/publish-artifact.sh +173 -0
  100. package/runtime/codex/bin/publish-references.sh +144 -0
  101. package/runtime/codex/bin/repo-state.sh +99 -0
  102. package/runtime/codex/bin/telemetry.sh +350 -0
  103. package/runtime/codex/bin/worktree-create.sh +118 -0
  104. package/runtime/codex/bin/worktree-remove.sh +50 -0
  105. package/runtime/codex/bin/worktree.sh +198 -0
  106. package/runtime/codex/manifest.json +155 -0
@@ -0,0 +1,431 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ # handoff-run.sh — atomic orchestrator for /handoff.
4
+ #
5
+ # Everything mechanical happens here, in one bash process, so the main
6
+ # session renders a single collapsed Bash tool block instead of 6+.
7
+ #
8
+ # Usage:
9
+ # bash bin/handoff-run.sh \
10
+ # --author <handle> \
11
+ # --topic "<short topic>" \
12
+ # [--recipient <name>] \
13
+ # [--project <name>] \
14
+ # <<'HANDOFFEOF'
15
+ # ...full markdown body of the handoff...
16
+ # HANDOFFEOF
17
+ #
18
+ # The full file body is read from stdin. The script:
19
+ # 1. Computes the file path: memory/handoffs/YYYY-MM/DD-author-slug.md
20
+ # 2. Writes the file
21
+ # 3. Appends ## Repo State section (from bin/repo-state.sh) if non-empty
22
+ # 4. Prepends memory/handoffs/index.md with a new entry
23
+ # 5. Runs bin/index-handoff.sh (connected mode only) — creates Session node
24
+ # and returns a subgraph snapshot in the result JSON
25
+ # 6. Commits + pushes memory repo (with orphan-stash dance if needed)
26
+ # 7. Publishes HTML artifact via bin/publish-artifact.sh
27
+ # 8. Sends Telegram notification via bin/notify.sh (if recipient)
28
+ # 9. Emits ONE status line and ONE JSON blob on stdout
29
+ #
30
+ # Exit codes:
31
+ # 0 on success (even if graph/publish/notify individually fail — they are
32
+ # non-blocking and reported in the JSON).
33
+ # 1 on unrecoverable failure (file write, memory push, bad args).
34
+
35
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
36
+ CONFIG="$SCRIPT_DIR/egregore.json"
37
+
38
+ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
39
+ sed -n '2,30p' "$0"
40
+ exit 0
41
+ fi
42
+
43
+ # --- Parse args ----------------------------------------------------------
44
+ AUTHOR=""
45
+ TOPIC=""
46
+ RECIPIENT=""
47
+ PROJECT=""
48
+ NO_PUSH=0
49
+ NO_NOTIFY=0
50
+ NO_PUBLISH=0
51
+
52
+ while [ $# -gt 0 ]; do
53
+ case "$1" in
54
+ --author) AUTHOR="${2:?}"; shift 2 ;;
55
+ --topic) TOPIC="${2:?}"; shift 2 ;;
56
+ --recipient) RECIPIENT="${2:-}"; shift 2 ;;
57
+ --project) PROJECT="${2:-}"; shift 2 ;;
58
+ --no-push) NO_PUSH=1; shift ;;
59
+ --no-notify) NO_NOTIFY=1; shift ;;
60
+ --no-publish) NO_PUBLISH=1; shift ;;
61
+ *) echo "Unknown arg: $1" >&2; exit 1 ;;
62
+ esac
63
+ done
64
+
65
+ [ -z "$AUTHOR" ] && { echo "Missing --author" >&2; exit 1; }
66
+ [ -z "$TOPIC" ] && { echo "Missing --topic" >&2; exit 1; }
67
+
68
+ MODE=$(jq -r '.mode // "connected"' "$CONFIG" 2>/dev/null || echo "connected")
69
+ TODAY=$(date +%Y-%m-%d)
70
+ YYYY_MM=$(date +%Y-%m)
71
+ DD=$(date +%d)
72
+
73
+ # --- Derive slug + file path --------------------------------------------
74
+ _slugify() {
75
+ echo "$1" \
76
+ | tr '[:upper:]' '[:lower:]' \
77
+ | sed -E 's/[^a-z0-9]+/-/g' \
78
+ | sed -E 's/^-+|-+$//g' \
79
+ | cut -c1-50
80
+ }
81
+
82
+ SLUG=$(_slugify "$TOPIC")
83
+ REL_FILE="handoffs/${YYYY_MM}/${DD}-${AUTHOR}-${SLUG}.md"
84
+ ABS_FILE="$SCRIPT_DIR/memory/${REL_FILE}"
85
+
86
+ # Collision avoidance: append -N if file already exists
87
+ N=2
88
+ while [ -e "$ABS_FILE" ]; do
89
+ REL_FILE="handoffs/${YYYY_MM}/${DD}-${AUTHOR}-${SLUG}-${N}.md"
90
+ ABS_FILE="$SCRIPT_DIR/memory/${REL_FILE}"
91
+ N=$((N+1))
92
+ [ $N -gt 99 ] && { echo "Too many collisions for $SLUG" >&2; exit 1; }
93
+ done
94
+
95
+ mkdir -p "$(dirname "$ABS_FILE")"
96
+
97
+ # --- Write the file from stdin ------------------------------------------
98
+ cat > "$ABS_FILE"
99
+
100
+ if [ ! -s "$ABS_FILE" ]; then
101
+ echo "Empty stdin — handoff body is required" >&2
102
+ rm -f "$ABS_FILE"
103
+ exit 1
104
+ fi
105
+
106
+ # --- Inject **To**: line if --recipient given and body omits it -----
107
+ # index-handoff.sh parses recipients from the file body. Skills
108
+ # normally write the To line themselves, but callers that invoke
109
+ # handoff-run.sh directly often skip it. Inject after the Author
110
+ # line so the graph gets the HANDED_TO edge.
111
+ if [ -n "$RECIPIENT" ] && ! grep -qi "^\*\*[Tt]o\*\*:" "$ABS_FILE"; then
112
+ awk -v r="$RECIPIENT" '
113
+ !injected && /^\*\*[Aa]uthor\*\*:/ {
114
+ print
115
+ print "**To**: " r
116
+ injected=1
117
+ next
118
+ }
119
+ { print }
120
+ ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
121
+ fi
122
+
123
+ # --- Append repo state ---------------------------------------------------
124
+ # --no-pr skips `gh pr list` (~400–600ms per repo). A detached PR backfill
125
+ # after the parallel phase rewrites `—` → `#N` and re-pushes memory.
126
+ REPO_STATE=$(bash "$SCRIPT_DIR/bin/repo-state.sh" --no-pr 2>/dev/null || true)
127
+ if [ -n "$REPO_STATE" ]; then
128
+ printf '\n%s\n' "$REPO_STATE" >> "$ABS_FILE"
129
+ fi
130
+
131
+ # --- Prepend memory/handoffs/index.md -----------------------------------
132
+ INDEX="$SCRIPT_DIR/memory/handoffs/index.md"
133
+ mkdir -p "$(dirname "$INDEX")"
134
+ [ -f "$INDEX" ] || printf '# Handoffs\n\n' > "$INDEX"
135
+
136
+ if [ -n "$RECIPIENT" ]; then
137
+ IDX_LINE="- **${TODAY}** — ${AUTHOR}: ${TOPIC} (handoff to ${RECIPIENT})"
138
+ else
139
+ IDX_LINE="- **${TODAY}** — ${AUTHOR}: ${TOPIC} (handoff)"
140
+ fi
141
+
142
+ # Insert after the first blank line (keeping the # header intact)
143
+ awk -v line="$IDX_LINE" '
144
+ BEGIN { inserted = 0 }
145
+ /^$/ && !inserted { print; print line; inserted = 1; next }
146
+ { print }
147
+ END { if (!inserted) print line }
148
+ ' "$INDEX" > "$INDEX.new" && mv "$INDEX.new" "$INDEX"
149
+
150
+ # --- Parallel stage: graph + memory + (publish → notify) ----------------
151
+ #
152
+ # Three independent branches fork here and join before we emit the result:
153
+ # A. Graph index — reads ABS_FILE, writes Session node to Neo4j, and
154
+ # returns the handoff's neighborhood as subgraph JSON.
155
+ # B. Memory push — commits + pushes memory repo (orphan-stash dance).
156
+ # C. Publish+notify — publishes HTML artifact, then (if recipient is set)
157
+ # sends Telegram with the artifact URL embedded.
158
+ #
159
+ # Each branch writes its result to a tmpfile. Wall-clock is max(A,B,C).
160
+ #
161
+ # The published artifact runs its own live re-query via bin/graph.sh at
162
+ # render time, so it picks up the subgraph even though Branch A may not
163
+ # have indexed the session yet by the time Branch C fires publish.
164
+
165
+ MEMORY_DIR="$SCRIPT_DIR/memory"
166
+
167
+ TMPD=$(mktemp -d -t handoff-run-XXXXXX)
168
+ trap 'rm -rf "$TMPD"' EXIT
169
+
170
+ # Extract briefing text once (used by publish and notify) so we don't
171
+ # re-awk the same file inside multiple workers.
172
+ BRIEFING_LEAD=$(awk '/^## Briefing/{found=1; next} found && /^##/{exit} found && NF{print}' "$ABS_FILE" 2>/dev/null \
173
+ | head -2 | tr '\n' ' ' | cut -c1-200 || true)
174
+ BRIEFING_SHORT=$(awk '/^## Briefing/{found=1; next} found && /^##/{exit} found && NF{print}' "$ABS_FILE" 2>/dev/null \
175
+ | head -3 | tr '\n' ' ' | cut -c1-280 || true)
176
+
177
+ # --- Branch A: Graph index ---
178
+ (
179
+ if [ "$MODE" = "connected" ]; then
180
+ GJ=$(bash "$SCRIPT_DIR/bin/index-handoff.sh" "$ABS_FILE" 2>/dev/null || echo '{}')
181
+ SID=$(echo "$GJ" | jq -r '.sessionId // ""' 2>/dev/null || echo "")
182
+ RES=$(echo "$GJ" | jq -r '.resolved // 0' 2>/dev/null || echo 0)
183
+ SG=$(echo "$GJ" | jq -c '.subgraph // null' 2>/dev/null || echo "null")
184
+ echo "$SG" > "$TMPD/subgraph"
185
+ if [ -n "$SID" ]; then
186
+ printf '%s\n%s\nok\n' "$SID" "$RES" > "$TMPD/graph"
187
+ else
188
+ printf '\n0\noffline\n' > "$TMPD/graph"
189
+ fi
190
+ else
191
+ echo "null" > "$TMPD/subgraph"
192
+ printf '\n0\nskipped\n' > "$TMPD/graph"
193
+ fi
194
+ ) &
195
+ PID_GRAPH=$!
196
+
197
+ # --- Branch D: Today's artifacts (connected mode only) ---
198
+ # The graph is the right tool for this: indexed by date + author + excluded
199
+ # tag, returns a small structured list. The filesystem would require a full
200
+ # walk of memory/knowledge/* to answer. Runs in parallel; Branch B waits on
201
+ # this before committing so the file always has the section.
202
+ (
203
+ ARTIFACTS_JSON="[]"
204
+ if [ "$MODE" = "connected" ]; then
205
+ YY=$(date +%Y); MM=$(date +%-m); DD_NUM=$(date +%-d)
206
+ PARAMS=$(jq -nc --arg gh "$AUTHOR" '{gh:$gh}')
207
+ RAW=$(bash "$SCRIPT_DIR/bin/graph.sh" query "
208
+ MATCH (a:Artifact)-[:CONTRIBUTED_BY]->(p:Person {github: \$gh})
209
+ WHERE a.created >= datetime({year: $YY, month: $MM, day: $DD_NUM})
210
+ AND NOT 'tutorial-generated' IN coalesce(a.topics, [])
211
+ RETURN a.title AS title, a.type AS type, a.filePath AS path
212
+ ORDER BY a.created DESC
213
+ LIMIT 5" "$PARAMS" 2>/dev/null || echo '{}')
214
+ # Response shape: {"fields":[...], "values":[[...], ...]}. Convert to
215
+ # an array of {title, type, path} objects.
216
+ ARTIFACTS_JSON=$(echo "$RAW" | jq -c '
217
+ (.fields // []) as $f
218
+ | (.values // [])
219
+ | map(. as $row | reduce range(0; ($f | length)) as $i ({}; .[$f[$i]] = $row[$i]))
220
+ ' 2>/dev/null || echo '[]')
221
+ [ -z "$ARTIFACTS_JSON" ] && ARTIFACTS_JSON="[]"
222
+ fi
223
+ echo "$ARTIFACTS_JSON" > "$TMPD/artifacts"
224
+ ) &
225
+ PID_ARTIFACTS=$!
226
+
227
+ # --- Branch B: Memory commit + push ---
228
+ (
229
+ if [ "$NO_PUSH" = "1" ]; then
230
+ echo "skipped (--no-push)" > "$TMPD/memory"
231
+ else
232
+ # Wait for Branch D (artifacts query) so the committed file has the
233
+ # `## Session Artifacts` section. D is fast (~1s) and runs parallel
234
+ # with Branch A; this wait doesn't extend the critical path because
235
+ # Branch C (publish) is the bottleneck.
236
+ wait "$PID_ARTIFACTS" 2>/dev/null || true
237
+ if [ -s "$TMPD/artifacts" ]; then
238
+ ART_COUNT=$(jq 'length' "$TMPD/artifacts" 2>/dev/null || echo 0)
239
+ if [ "${ART_COUNT:-0}" -gt 0 ]; then
240
+ {
241
+ printf '\n## Session Artifacts\n\n'
242
+ jq -r '.[] | "- \(.type // "Artifact"): \(.title) -> \(.path // "")"' "$TMPD/artifacts" 2>/dev/null
243
+ } >> "$ABS_FILE"
244
+ fi
245
+ fi
246
+
247
+ cd "$MEMORY_DIR"
248
+ git add "$REL_FILE" handoffs/index.md >/dev/null 2>&1
249
+
250
+ if [ -n "$RECIPIENT" ]; then
251
+ MSG_COMMIT="Handoff: ${TOPIC} (to ${RECIPIENT})"
252
+ else
253
+ MSG_COMMIT="Handoff: ${TOPIC}"
254
+ fi
255
+
256
+ git commit -m "$MSG_COMMIT" >/dev/null 2>&1 || true
257
+
258
+ STASHED=0
259
+ if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
260
+ git stash push -u -m "handoff-run pre-pull stash" >/dev/null 2>&1 && STASHED=1
261
+ fi
262
+
263
+ MEM_OK=0
264
+ for i in 1 2 3; do
265
+ if git pull --rebase origin main --quiet >/dev/null 2>&1 \
266
+ && git push origin main --quiet >/dev/null 2>&1; then
267
+ MEM_OK=1
268
+ break
269
+ fi
270
+ sleep 1
271
+ done
272
+
273
+ [ "$STASHED" = "1" ] && git stash pop >/dev/null 2>&1 || true
274
+
275
+ if [ "$MEM_OK" = "1" ]; then
276
+ echo "ok" > "$TMPD/memory"
277
+ else
278
+ echo "failed" > "$TMPD/memory"
279
+ fi
280
+ fi
281
+ ) &
282
+ PID_MEMORY=$!
283
+
284
+ # --- Branch C: Publish artifact → Notify ---
285
+ (
286
+ AURL=""
287
+ if [ "$NO_PUBLISH" = "0" ] && [ -x "$SCRIPT_DIR/bin/publish-artifact.sh" ]; then
288
+ AURL=$(bash "$SCRIPT_DIR/bin/publish-artifact.sh" handoff "$ABS_FILE" \
289
+ --title "$TOPIC" \
290
+ --author "$AUTHOR" \
291
+ --description "$BRIEFING_LEAD" 2>/dev/null || echo "")
292
+ fi
293
+ echo "$AURL" > "$TMPD/artifact"
294
+
295
+ NS="skipped"
296
+ if [ "$NO_NOTIFY" = "0" ]; then
297
+ if [ -n "$AURL" ]; then
298
+ MSG_NOTIFY="Handoff from ${AUTHOR}: ${TOPIC}
299
+
300
+ \"${BRIEFING_SHORT}\"
301
+
302
+ View: ${AURL}"
303
+ else
304
+ MSG_NOTIFY="Handoff from ${AUTHOR}: ${TOPIC}
305
+
306
+ \"${BRIEFING_SHORT}\""
307
+ fi
308
+
309
+ # Always notify. With a recipient (connected mode) → DM. Otherwise → group.
310
+ # Self-handoffs still post to the group so they're visible on /activity
311
+ # and in the Telegram feed — a handoff without a Telegram beat is invisible.
312
+ if [ -n "$RECIPIENT" ] && [ "$MODE" = "connected" ]; then
313
+ NR=$(bash "$SCRIPT_DIR/bin/notify.sh" send "$RECIPIENT" "$MSG_NOTIFY" 2>/dev/null || echo "Failed")
314
+ else
315
+ NR=$(bash "$SCRIPT_DIR/bin/notify.sh" group "$MSG_NOTIFY" 2>/dev/null || echo "Failed")
316
+ fi
317
+
318
+ if echo "$NR" | grep -qi "^Sent"; then
319
+ NS="sent"
320
+ elif echo "$NR" | grep -qi "^Failed"; then
321
+ NS="failed"
322
+ else
323
+ NS="unknown"
324
+ fi
325
+ fi
326
+ echo "$NS" > "$TMPD/notify"
327
+ ) &
328
+ PID_PUBNOT=$!
329
+
330
+ # Join (never fail parent — each branch reports its own status).
331
+ # Branch D (PID_ARTIFACTS) is already awaited inside Branch B, but we wait
332
+ # again here to be safe if B skipped the wait (--no-push path).
333
+ wait "$PID_GRAPH" 2>/dev/null || true
334
+ wait "$PID_MEMORY" 2>/dev/null || true
335
+ wait "$PID_PUBNOT" 2>/dev/null || true
336
+ wait "$PID_ARTIFACTS" 2>/dev/null || true
337
+
338
+ # --- Collect results from tmpfiles --------------------------------------
339
+ SESSION_ID=""
340
+ RESOLVED=0
341
+ GRAPH_STATUS="skipped"
342
+ if [ -s "$TMPD/graph" ]; then
343
+ SESSION_ID=$(sed -n '1p' "$TMPD/graph")
344
+ RESOLVED=$(sed -n '2p' "$TMPD/graph")
345
+ GRAPH_STATUS=$(sed -n '3p' "$TMPD/graph")
346
+ fi
347
+ [ -z "$RESOLVED" ] && RESOLVED=0
348
+
349
+ SUBGRAPH_JSON=$(cat "$TMPD/subgraph" 2>/dev/null || echo "null")
350
+ [ -z "$SUBGRAPH_JSON" ] && SUBGRAPH_JSON="null"
351
+
352
+ MEMORY_STATUS=$(cat "$TMPD/memory" 2>/dev/null || echo "skipped")
353
+ ARTIFACT_URL=$(cat "$TMPD/artifact" 2>/dev/null || echo "")
354
+ NOTIFY_STATUS=$(cat "$TMPD/notify" 2>/dev/null || echo "skipped")
355
+ ARTIFACTS_ARR=$(cat "$TMPD/artifacts" 2>/dev/null || echo "[]")
356
+ [ -z "$ARTIFACTS_ARR" ] && ARTIFACTS_ARR="[]"
357
+
358
+ # --- Detached PR-number backfill ----------------------------------------
359
+ # repo-state.sh was called with --no-pr for speed; now fire the backfill in
360
+ # the background so PR columns get populated without blocking this script.
361
+ # Reparented to init via `( cmd & ) >/dev/null 2>&1` — survives our exit.
362
+ if [ "$MEMORY_STATUS" = "ok" ] && grep -q '^## Repo State' "$ABS_FILE" 2>/dev/null; then
363
+ ( bash "$SCRIPT_DIR/bin/handoff-pr-backfill.sh" "$ABS_FILE" "$TOPIC" >/dev/null 2>&1 & ) >/dev/null 2>&1
364
+ fi
365
+
366
+ # --- Emit minimal stdout, stash structured data in a tmpfile ------------
367
+ #
368
+ # The Bash tool block shows stdout verbatim and collapses after ~3 lines.
369
+ # We keep stdout to exactly one compact status line so the block is
370
+ # visually quiet. Full structured data goes to a well-known tmpfile that
371
+ # the caller (skill) reads to render the rich card.
372
+ STATUS_BITS=("saved")
373
+ [ "$GRAPH_STATUS" = "ok" ] && STATUS_BITS+=("graphed")
374
+ [ "$MEMORY_STATUS" = "ok" ] && STATUS_BITS+=("pushed")
375
+ if [ "$NOTIFY_STATUS" = "sent" ]; then
376
+ if [ -n "$RECIPIENT" ]; then
377
+ STATUS_BITS+=("${RECIPIENT} notified")
378
+ else
379
+ STATUS_BITS+=("group notified")
380
+ fi
381
+ elif [ "$NOTIFY_STATUS" = "unknown" ] && [ -n "$RECIPIENT" ]; then
382
+ STATUS_BITS+=("${RECIPIENT} relayed to group")
383
+ fi
384
+ [ -n "$ARTIFACT_URL" ] && STATUS_BITS+=("published")
385
+
386
+ STATUS_LINE=""
387
+ for bit in "${STATUS_BITS[@]}"; do
388
+ if [ -z "$STATUS_LINE" ]; then
389
+ STATUS_LINE="$bit"
390
+ else
391
+ STATUS_LINE="${STATUS_LINE} · ${bit}"
392
+ fi
393
+ done
394
+
395
+ # Stash structured result for the caller
396
+ RESULT_FILE="${TMPDIR:-/tmp}/handoff-run-result.json"
397
+ jq -cn \
398
+ --arg mode "$MODE" \
399
+ --arg file "$REL_FILE" \
400
+ --arg absFile "$ABS_FILE" \
401
+ --arg sessionId "$SESSION_ID" \
402
+ --argjson resolved "${RESOLVED:-0}" \
403
+ --arg graphStatus "$GRAPH_STATUS" \
404
+ --arg memoryStatus "$MEMORY_STATUS" \
405
+ --arg notifyStatus "$NOTIFY_STATUS" \
406
+ --arg artifactUrl "$ARTIFACT_URL" \
407
+ --arg recipient "$RECIPIENT" \
408
+ --arg topic "$TOPIC" \
409
+ --arg author "$AUTHOR" \
410
+ --argjson subgraph "$SUBGRAPH_JSON" \
411
+ --argjson artifacts "$ARTIFACTS_ARR" \
412
+ '{
413
+ mode:$mode,
414
+ file:$file,
415
+ absFile:$absFile,
416
+ sessionId:$sessionId,
417
+ resolved:$resolved,
418
+ graphStatus:$graphStatus,
419
+ memoryStatus:$memoryStatus,
420
+ notifyStatus:$notifyStatus,
421
+ artifactUrl:$artifactUrl,
422
+ recipient:$recipient,
423
+ topic:$topic,
424
+ author:$author,
425
+ subgraph:$subgraph,
426
+ artifacts:$artifacts
427
+ }' > "$RESULT_FILE"
428
+
429
+ # One clean line for the Bash tool block.
430
+ # The caller reads structured data from $TMPDIR/handoff-run-result.json.
431
+ echo "⇌ ${STATUS_LINE}"
@@ -0,0 +1,131 @@
1
+ #!/bin/bash
2
+ # handoff-save-egregore.sh — background save+auto-merge for the core repo after a handoff.
3
+ #
4
+ # /handoff renders its card first, then fires this detached. The user never waits on git.
5
+ #
6
+ # Behavior:
7
+ # - If nothing to save (clean working tree + up-to-date with origin) → exit silently.
8
+ # - If on develop/main/master → create dev/{author}/handoff-{YYYY-MM-DD} branch from origin/develop.
9
+ # - Commit all changes, rebase onto origin/develop, push.
10
+ # - If push succeeds: create PR to develop (or reuse existing), then:
11
+ # - Markdown-only diff → `gh pr merge --auto --merge` (auto-merges when checks pass).
12
+ # - Code/config present → leave PR open for review (no auto-merge).
13
+ #
14
+ # Usage:
15
+ # handoff-save-egregore.sh <author> <topic> [--repo-dir <path>]
16
+ #
17
+ # --repo-dir overrides the default working directory (normally the script's
18
+ # own repo root, resolved from $0). Tests and sandbox runs MUST pass it;
19
+ # without it, the script operates on the real repo regardless of caller cwd.
20
+ #
21
+ # Detached usage (the skill calls it this way):
22
+ # ( bash bin/handoff-save-egregore.sh "$AUTHOR" "$TOPIC" >/dev/null 2>&1 & ) >/dev/null 2>&1
23
+ #
24
+ # Exit 0 on any non-fatal outcome. The caller never reads stdout/stderr — all status
25
+ # lives in the PR itself and in `git log`.
26
+
27
+ set -uo pipefail
28
+
29
+ AUTHOR="${1:-}"
30
+ TOPIC="${2:-session work}"
31
+ shift 2 2>/dev/null || true
32
+
33
+ REPO_DIR=""
34
+ while [ $# -gt 0 ]; do
35
+ case "$1" in
36
+ --repo-dir) REPO_DIR="${2:-}"; shift 2 ;;
37
+ *) shift ;;
38
+ esac
39
+ done
40
+
41
+ [ -n "$AUTHOR" ] || { echo "handoff-save-egregore: missing author" >&2; exit 1; }
42
+
43
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
44
+ # If --repo-dir wasn't given, default to the script's own repo root.
45
+ [ -z "$REPO_DIR" ] && REPO_DIR="$SCRIPT_DIR"
46
+ cd "$REPO_DIR" || exit 1
47
+
48
+ # --- Early exit if nothing to do -----------------------------------------
49
+ DIRTY=0
50
+ [ -n "$(git status --porcelain 2>/dev/null)" ] && DIRTY=1
51
+
52
+ CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
53
+ AHEAD=0
54
+ if [ -n "$CURRENT_BRANCH" ]; then
55
+ # Count commits ahead of origin/develop on the current branch (works from any
56
+ # dev/* branch or from develop itself).
57
+ AHEAD=$(git rev-list --count "origin/develop..HEAD" 2>/dev/null || echo 0)
58
+ fi
59
+
60
+ if [ "$DIRTY" = "0" ] && [ "$AHEAD" = "0" ]; then
61
+ exit 0
62
+ fi
63
+
64
+ # --- Protect the protected branches: create a working branch if needed --
65
+ case "$CURRENT_BRANCH" in
66
+ develop|main|master|"")
67
+ SLUG_DATE=$(date +%Y-%m-%d)
68
+ NEW_BRANCH="dev/${AUTHOR}/handoff-${SLUG_DATE}"
69
+ git fetch origin develop --quiet 2>/dev/null || true
70
+ git checkout -b "$NEW_BRANCH" origin/develop --quiet 2>/dev/null || \
71
+ git checkout "$NEW_BRANCH" --quiet 2>/dev/null || exit 0
72
+ CURRENT_BRANCH="$NEW_BRANCH"
73
+ ;;
74
+ esac
75
+
76
+ # --- Commit whatever is uncommitted --------------------------------------
77
+ if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
78
+ git add -A 2>/dev/null || true
79
+ git commit -m "Handoff: ${TOPIC}" --quiet 2>/dev/null || true
80
+ fi
81
+
82
+ # --- Rebase onto develop, fall back to merge if rebase conflicts ---------
83
+ git fetch origin develop --quiet 2>/dev/null || true
84
+ if ! git rebase origin/develop --quiet 2>/dev/null; then
85
+ git rebase --abort 2>/dev/null || true
86
+ if ! git merge origin/develop --quiet -m "Sync with develop" 2>/dev/null; then
87
+ # Conflicts we can't auto-resolve. Leave the branch as-is; the user will see it
88
+ # next session or when they run /save explicitly.
89
+ git merge --abort 2>/dev/null || true
90
+ exit 0
91
+ fi
92
+ fi
93
+
94
+ # --- Push ---------------------------------------------------------------
95
+ if ! git push -u origin "$CURRENT_BRANCH" --quiet 2>/dev/null; then
96
+ # Push failed (network, auth). Commits are safe locally.
97
+ exit 0
98
+ fi
99
+
100
+ # --- PR: reuse open PR on this branch, or create one --------------------
101
+ command -v gh >/dev/null 2>&1 || exit 0
102
+
103
+ PR_NUMBER=""
104
+ EXISTING=$(gh pr list --head "$CURRENT_BRANCH" --state open --json number,baseRefName --jq '.[0]' 2>/dev/null)
105
+ EXISTING_NUMBER=$(echo "$EXISTING" | jq -r '.number // empty' 2>/dev/null)
106
+ EXISTING_BASE=$(echo "$EXISTING" | jq -r '.baseRefName // empty' 2>/dev/null)
107
+
108
+ if [ -n "$EXISTING_NUMBER" ]; then
109
+ PR_NUMBER="$EXISTING_NUMBER"
110
+ if [ "$EXISTING_BASE" != "develop" ]; then
111
+ gh pr edit "$PR_NUMBER" --base develop >/dev/null 2>&1 || true
112
+ fi
113
+ else
114
+ PR_URL=$(gh pr create \
115
+ --base develop \
116
+ --head "$CURRENT_BRANCH" \
117
+ --title "Handoff: ${TOPIC}" \
118
+ --body "Auto-created by /handoff. Session work for: ${TOPIC}" \
119
+ 2>/dev/null || echo "")
120
+ PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$' || echo "")
121
+ fi
122
+
123
+ [ -n "$PR_NUMBER" ] || exit 0
124
+
125
+ # --- Auto-merge iff markdown-only ---------------------------------------
126
+ NON_MD=$(git diff origin/develop --name-only 2>/dev/null | grep -v '\.md$' | head -1 || echo "")
127
+ if [ -z "$NON_MD" ]; then
128
+ gh pr merge "$PR_NUMBER" --auto --merge >/dev/null 2>&1 || true
129
+ fi
130
+
131
+ exit 0