create-egregore 0.19.5 → 0.19.6

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 (130) hide show
  1. package/assets/egregore-launcher.js +407 -25
  2. package/bin/cli.js +34 -20
  3. package/lib/local.js +21 -5
  4. package/lib/pi-runtime.js +186 -0
  5. package/lib/setup.js +56 -11
  6. package/lib/ui.js +1 -1
  7. package/package.json +7 -5
  8. package/runtime/codex/.codex/spec-manifest.json +1 -1
  9. package/runtime/codex/AGENTS.md +7 -2
  10. package/runtime/codex/bin/codex-render-spec.mjs +6 -1
  11. package/runtime/codex/bin/codex-session-start.sh +5 -59
  12. package/runtime/codex/bin/codex-skill-render.mjs +7 -5
  13. package/runtime/codex/bin/lib/context.sh +8 -1
  14. package/runtime/codex/bin/lib/greeting.sh +12 -108
  15. package/runtime/codex/bin/lib/metrics.sh +374 -0
  16. package/runtime/codex/bin/publish-artifact.sh +14 -0
  17. package/runtime/codex/bin/settings.sh +274 -0
  18. package/runtime/codex/manifest.json +2 -0
  19. package/runtime/pi/.codex/config.toml +15 -0
  20. package/runtime/pi/.codex/hooks/branch-guard.js +242 -0
  21. package/runtime/pi/.codex/hooks/search-hint.js +65 -0
  22. package/runtime/pi/.codex/hooks.json +30 -0
  23. package/runtime/pi/.codex/skills/activity/SKILL.md +70 -0
  24. package/runtime/pi/.codex/skills/add/SKILL.md +25 -0
  25. package/runtime/pi/.codex/skills/announce/SKILL.md +54 -0
  26. package/runtime/pi/.codex/skills/answer/SKILL.md +25 -0
  27. package/runtime/pi/.codex/skills/archive/SKILL.md +41 -0
  28. package/runtime/pi/.codex/skills/ask/SKILL.md +59 -0
  29. package/runtime/pi/.codex/skills/branch/SKILL.md +25 -0
  30. package/runtime/pi/.codex/skills/checkup/SKILL.md +41 -0
  31. package/runtime/pi/.codex/skills/commit/SKILL.md +25 -0
  32. package/runtime/pi/.codex/skills/contribute/SKILL.md +25 -0
  33. package/runtime/pi/.codex/skills/dark-mode/SKILL.md +25 -0
  34. package/runtime/pi/.codex/skills/dashboard/SKILL.md +59 -0
  35. package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +74 -0
  36. package/runtime/pi/.codex/skills/delete-user/SKILL.md +25 -0
  37. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +25 -0
  38. package/runtime/pi/.codex/skills/emissary/SKILL.md +41 -0
  39. package/runtime/pi/.codex/skills/env/SKILL.md +25 -0
  40. package/runtime/pi/.codex/skills/handoff/SKILL.md +67 -0
  41. package/runtime/pi/.codex/skills/harvest/SKILL.md +107 -0
  42. package/runtime/pi/.codex/skills/ingest/SKILL.md +25 -0
  43. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +25 -0
  44. package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  45. package/runtime/pi/.codex/skills/invite/SKILL.md +40 -0
  46. package/runtime/pi/.codex/skills/issue/SKILL.md +41 -0
  47. package/runtime/pi/.codex/skills/me/SKILL.md +25 -0
  48. package/runtime/pi/.codex/skills/meeting/SKILL.md +41 -0
  49. package/runtime/pi/.codex/skills/minion/SKILL.md +25 -0
  50. package/runtime/pi/.codex/skills/note/SKILL.md +25 -0
  51. package/runtime/pi/.codex/skills/onboarding/SKILL.md +41 -0
  52. package/runtime/pi/.codex/skills/pr/SKILL.md +25 -0
  53. package/runtime/pi/.codex/skills/project/SKILL.md +41 -0
  54. package/runtime/pi/.codex/skills/pull/SKILL.md +25 -0
  55. package/runtime/pi/.codex/skills/push/SKILL.md +25 -0
  56. package/runtime/pi/.codex/skills/quest/SKILL.md +77 -0
  57. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +25 -0
  58. package/runtime/pi/.codex/skills/reflect/SKILL.md +41 -0
  59. package/runtime/pi/.codex/skills/review-pr/SKILL.md +41 -0
  60. package/runtime/pi/.codex/skills/save/SKILL.md +62 -0
  61. package/runtime/pi/.codex/skills/scroll/SKILL.md +168 -0
  62. package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +4 -0
  63. package/runtime/pi/.codex/skills/scroll/assets/shell.html +542 -0
  64. package/runtime/pi/.codex/skills/search/SKILL.md +56 -0
  65. package/runtime/pi/.codex/skills/session-view/SKILL.md +25 -0
  66. package/runtime/pi/.codex/skills/setup/SKILL.md +25 -0
  67. package/runtime/pi/.codex/skills/summon/SKILL.md +41 -0
  68. package/runtime/pi/.codex/skills/sync-repos/SKILL.md +25 -0
  69. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +25 -0
  70. package/runtime/pi/.codex/skills/telemetry/SKILL.md +25 -0
  71. package/runtime/pi/.codex/skills/test/SKILL.md +41 -0
  72. package/runtime/pi/.codex/skills/the-spiral/SKILL.md +84 -0
  73. package/runtime/pi/.codex/skills/thread/SKILL.md +25 -0
  74. package/runtime/pi/.codex/skills/todo/SKILL.md +41 -0
  75. package/runtime/pi/.codex/skills/tui-design/SKILL.md +41 -0
  76. package/runtime/pi/.codex/skills/tutorial/SKILL.md +41 -0
  77. package/runtime/pi/.codex/skills/update/SKILL.md +25 -0
  78. package/runtime/pi/.codex/skills/view/SKILL.md +156 -0
  79. package/runtime/pi/.codex/skills/view/agents/openai.yaml +4 -0
  80. package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +221 -0
  81. package/runtime/pi/.codex/skills/visual-explain/SKILL.md +41 -0
  82. package/runtime/pi/.codex/skills/wrap/SKILL.md +55 -0
  83. package/runtime/pi/.codex/spec-manifest.json +114 -0
  84. package/runtime/pi/.pi/APPEND_SYSTEM.md +255 -0
  85. package/runtime/pi/.pi/extensions/egregore.ts +324 -0
  86. package/runtime/pi/.pi/settings.json +6 -0
  87. package/runtime/pi/.pi/spec-manifest.json +12 -0
  88. package/runtime/pi/AGENTS.md +350 -0
  89. package/runtime/pi/bin/activity-data.sh +242 -0
  90. package/runtime/pi/bin/agent.sh +677 -0
  91. package/runtime/pi/bin/codex-install-skills.sh +78 -0
  92. package/runtime/pi/bin/codex-render-spec.mjs +394 -0
  93. package/runtime/pi/bin/codex-session-start.sh +188 -0
  94. package/runtime/pi/bin/codex-skill-render.mjs +323 -0
  95. package/runtime/pi/bin/codex-sync-skills.sh +227 -0
  96. package/runtime/pi/bin/dashboard-data.sh +178 -0
  97. package/runtime/pi/bin/graph-batch.sh +60 -0
  98. package/runtime/pi/bin/graph-op.sh +431 -0
  99. package/runtime/pi/bin/graph-wal.sh +228 -0
  100. package/runtime/pi/bin/graph.sh +177 -0
  101. package/runtime/pi/bin/handoff-pr-backfill.sh +86 -0
  102. package/runtime/pi/bin/handoff-run.sh +581 -0
  103. package/runtime/pi/bin/handoff-save-egregore.sh +136 -0
  104. package/runtime/pi/bin/index-handoff.sh +312 -0
  105. package/runtime/pi/bin/lib/artifact-id.sh +46 -0
  106. package/runtime/pi/bin/lib/config.sh +71 -0
  107. package/runtime/pi/bin/lib/context.sh +542 -0
  108. package/runtime/pi/bin/lib/dashboard-artifact.sh +142 -0
  109. package/runtime/pi/bin/lib/ensure-develop.sh +183 -0
  110. package/runtime/pi/bin/lib/git-sync.sh +288 -0
  111. package/runtime/pi/bin/lib/greeting.sh +340 -0
  112. package/runtime/pi/bin/lib/hash.sh +13 -0
  113. package/runtime/pi/bin/lib/identity.sh +174 -0
  114. package/runtime/pi/bin/lib/metrics.sh +374 -0
  115. package/runtime/pi/bin/lib/permissions.sh +106 -0
  116. package/runtime/pi/bin/lib/time.sh +56 -0
  117. package/runtime/pi/bin/lib/worktree-links.sh +54 -0
  118. package/runtime/pi/bin/notify.sh +207 -0
  119. package/runtime/pi/bin/pi-branch-policy.mjs +15 -0
  120. package/runtime/pi/bin/pi-product-workflows.mjs +324 -0
  121. package/runtime/pi/bin/pi-render-spec.mjs +114 -0
  122. package/runtime/pi/bin/pi-session-start.sh +24 -0
  123. package/runtime/pi/bin/publish-artifact.sh +201 -0
  124. package/runtime/pi/bin/publish-references.sh +144 -0
  125. package/runtime/pi/bin/repo-state.sh +99 -0
  126. package/runtime/pi/bin/telemetry.sh +391 -0
  127. package/runtime/pi/bin/worktree-create.sh +118 -0
  128. package/runtime/pi/bin/worktree-remove.sh +50 -0
  129. package/runtime/pi/bin/worktree.sh +198 -0
  130. package/runtime/pi/manifest.json +176 -0
@@ -0,0 +1,581 @@
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
+ COMPOSED="" # optional house-kit JSON — the agent-composed render spec
52
+
53
+ while [ $# -gt 0 ]; do
54
+ case "$1" in
55
+ --author) AUTHOR="${2:?}"; shift 2 ;;
56
+ --topic) TOPIC="${2:?}"; shift 2 ;;
57
+ --recipient) RECIPIENT="${2:-}"; shift 2 ;;
58
+ --project) PROJECT="${2:-}"; shift 2 ;;
59
+ --composed) COMPOSED="${2:-}"; shift 2 ;;
60
+ --no-push) NO_PUSH=1; shift ;;
61
+ --no-notify) NO_NOTIFY=1; shift ;;
62
+ --no-publish) NO_PUBLISH=1; shift ;;
63
+ *) echo "Unknown arg: $1" >&2; exit 1 ;;
64
+ esac
65
+ done
66
+
67
+ # The render source: the agent-composed house-kit JSON when supplied (rich
68
+ # component render), else the markdown handoff (deterministic floor). The
69
+ # markdown file is ALWAYS the memory record + graph index either way.
70
+ RENDER_TYPE="handoff"
71
+ RENDER_SRC="" # resolved to $ABS_FILE after the file is written, unless composed
72
+ if [ -n "$COMPOSED" ] && [ -f "$COMPOSED" ]; then
73
+ RENDER_TYPE="composed"
74
+ RENDER_SRC="$COMPOSED"
75
+ fi
76
+
77
+ [ -z "$AUTHOR" ] && { echo "Missing --author" >&2; exit 1; }
78
+ [ -z "$TOPIC" ] && { echo "Missing --topic" >&2; exit 1; }
79
+
80
+ MODE=$(jq -r '.mode // "connected"' "$CONFIG" 2>/dev/null || echo "connected")
81
+ TODAY=$(date +%Y-%m-%d)
82
+ YYYY_MM=$(date +%Y-%m)
83
+ DD=$(date +%d)
84
+
85
+ # --- Derive slug + file path --------------------------------------------
86
+ _slugify() {
87
+ echo "$1" \
88
+ | tr '[:upper:]' '[:lower:]' \
89
+ | sed -E 's/[^a-z0-9]+/-/g' \
90
+ | sed -E 's/^-+|-+$//g' \
91
+ | cut -c1-50
92
+ }
93
+
94
+ SLUG=$(_slugify "$TOPIC")
95
+ REL_FILE="handoffs/${YYYY_MM}/${DD}-${AUTHOR}-${SLUG}.md"
96
+ ABS_FILE="$SCRIPT_DIR/memory/${REL_FILE}"
97
+
98
+ # Collision avoidance: append -N if file already exists
99
+ N=2
100
+ while [ -e "$ABS_FILE" ]; do
101
+ REL_FILE="handoffs/${YYYY_MM}/${DD}-${AUTHOR}-${SLUG}-${N}.md"
102
+ ABS_FILE="$SCRIPT_DIR/memory/${REL_FILE}"
103
+ N=$((N+1))
104
+ [ $N -gt 99 ] && { echo "Too many collisions for $SLUG" >&2; exit 1; }
105
+ done
106
+
107
+ mkdir -p "$(dirname "$ABS_FILE")"
108
+
109
+ # --- Write the file from stdin ------------------------------------------
110
+ cat > "$ABS_FILE"
111
+
112
+ if [ ! -s "$ABS_FILE" ]; then
113
+ echo "Empty stdin — handoff body is required" >&2
114
+ rm -f "$ABS_FILE"
115
+ exit 1
116
+ fi
117
+
118
+ # --- Inject **To**: line if --recipient given and body omits it -----
119
+ # index-handoff.sh parses recipients from the file body. Skills
120
+ # normally write the To line themselves, but callers that invoke
121
+ # handoff-run.sh directly often skip it. Inject after the Author
122
+ # line so the graph gets the HANDED_TO edge.
123
+ if [ -n "$RECIPIENT" ] && ! grep -qi "^\*\*[Tt]o\*\*:" "$ABS_FILE"; then
124
+ awk -v r="$RECIPIENT" '
125
+ !injected && /^\*\*[Aa]uthor\*\*:/ {
126
+ print
127
+ print "**To**: " r
128
+ injected=1
129
+ next
130
+ }
131
+ { print }
132
+ ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
133
+ fi
134
+
135
+ # --- Append repo state ---------------------------------------------------
136
+ # --no-pr skips `gh pr list` (~400–600ms per repo). A detached PR backfill
137
+ # after the parallel phase rewrites `—` → `#N` and re-pushes memory.
138
+ REPO_STATE=$(bash "$SCRIPT_DIR/bin/repo-state.sh" --no-pr 2>/dev/null || true)
139
+ if [ -n "$REPO_STATE" ]; then
140
+ printf '\n%s\n' "$REPO_STATE" >> "$ABS_FILE"
141
+ fi
142
+
143
+ # --- Prepend memory/handoffs/index.md -----------------------------------
144
+ INDEX="$SCRIPT_DIR/memory/handoffs/index.md"
145
+ mkdir -p "$(dirname "$INDEX")"
146
+ [ -f "$INDEX" ] || printf '# Handoffs\n\n' > "$INDEX"
147
+
148
+ if [ -n "$RECIPIENT" ]; then
149
+ IDX_LINE="- **${TODAY}** — ${AUTHOR}: ${TOPIC} (handoff to ${RECIPIENT})"
150
+ else
151
+ IDX_LINE="- **${TODAY}** — ${AUTHOR}: ${TOPIC} (handoff)"
152
+ fi
153
+
154
+ # Insert after the first blank line (keeping the # header intact)
155
+ awk -v line="$IDX_LINE" '
156
+ BEGIN { inserted = 0 }
157
+ /^$/ && !inserted { print; print line; inserted = 1; next }
158
+ { print }
159
+ END { if (!inserted) print line }
160
+ ' "$INDEX" > "$INDEX.new" && mv "$INDEX.new" "$INDEX"
161
+
162
+ # --- Parallel stage: graph + memory + (publish → notify) ----------------
163
+ #
164
+ # Three independent branches fork here and join before we emit the result:
165
+ # A. Graph index — reads ABS_FILE, writes Session node to Neo4j, and
166
+ # returns the handoff's neighborhood as subgraph JSON.
167
+ # B. Memory push — commits + pushes memory repo (orphan-stash dance).
168
+ # C. Publish+notify — publishes HTML artifact, then (if recipient is set)
169
+ # sends Telegram with the artifact URL embedded.
170
+ #
171
+ # Each branch writes its result to a tmpfile. Wall-clock is max(A,B,C).
172
+ #
173
+ # The published artifact runs its own live re-query via bin/graph.sh at
174
+ # render time, so it picks up the subgraph even though Branch A may not
175
+ # have indexed the session yet by the time Branch C fires publish.
176
+
177
+ MEMORY_DIR="$SCRIPT_DIR/memory"
178
+
179
+ TMPD=$(mktemp -d -t handoff-run-XXXXXX)
180
+ trap 'rm -rf "$TMPD"' EXIT
181
+
182
+ # Extract briefing text once (used by publish and notify) so we don't
183
+ # re-awk the same file inside multiple workers.
184
+ BRIEFING_LEAD=$(awk '/^## Briefing/{found=1; next} found && /^##/{exit} found && NF{print}' "$ABS_FILE" 2>/dev/null \
185
+ | head -2 | tr '\n' ' ' | cut -c1-200 || true)
186
+ BRIEFING_SHORT=$(awk '/^## Briefing/{found=1; next} found && /^##/{exit} found && NF{print}' "$ABS_FILE" 2>/dev/null \
187
+ | head -3 | tr '\n' ' ' | cut -c1-280 || true)
188
+
189
+ # --- Branch A: Graph index ---
190
+ (
191
+ if [ "$MODE" = "connected" ]; then
192
+ GJ=$(bash "$SCRIPT_DIR/bin/index-handoff.sh" "$ABS_FILE" 2>/dev/null || echo '{}')
193
+ SID=$(echo "$GJ" | jq -r '.sessionId // ""' 2>/dev/null || echo "")
194
+ RES=$(echo "$GJ" | jq -r '.resolved // 0' 2>/dev/null || echo 0)
195
+ SG=$(echo "$GJ" | jq -c '.subgraph // null' 2>/dev/null || echo "null")
196
+ echo "$SG" > "$TMPD/subgraph"
197
+ if [ -n "$SID" ]; then
198
+ printf '%s\n%s\nok\n' "$SID" "$RES" > "$TMPD/graph"
199
+ else
200
+ printf '\n0\noffline\n' > "$TMPD/graph"
201
+ fi
202
+ else
203
+ echo "null" > "$TMPD/subgraph"
204
+ printf '\n0\nskipped\n' > "$TMPD/graph"
205
+ fi
206
+ ) &
207
+ PID_GRAPH=$!
208
+
209
+ # --- Branch D: Today's artifacts (connected mode only) ---
210
+ # The graph is the right tool for this: indexed by date + author + excluded
211
+ # tag, returns a small structured list. The filesystem would require a full
212
+ # walk of memory/knowledge/* to answer. Runs in parallel; Branch B waits on
213
+ # this before committing so the file always has the section.
214
+ (
215
+ ARTIFACTS_JSON="[]"
216
+ if [ "$MODE" = "connected" ]; then
217
+ YY=$(date +%Y); MM=$(date +%-m); DD_NUM=$(date +%-d)
218
+ RAW=$(bash "$SCRIPT_DIR/bin/graph.sh" query "
219
+ MATCH (a:Artifact)-[:CONTRIBUTED_BY]->(p:Person {github: \$gh})
220
+ WHERE a.created >= datetime({year: $YY, month: $MM, day: $DD_NUM})
221
+ AND NOT 'tutorial-generated' IN coalesce(a.topics, [])
222
+ RETURN a.title AS title, a.type AS type, a.filePath AS path
223
+ ORDER BY a.created DESC
224
+ LIMIT 5" "$(jq -nc --arg gh "$AUTHOR" '{gh:$gh}')" 2>/dev/null || echo '{}')
225
+ # Response shape: {"fields":[...], "values":[[...], ...]}. Convert to
226
+ # an array of {title, type, path} objects.
227
+ ARTIFACTS_JSON=$(echo "$RAW" | jq -c '
228
+ (.fields // []) as $f
229
+ | (.values // [])
230
+ | map(. as $row | reduce range(0; ($f | length)) as $i ({}; .[$f[$i]] = $row[$i]))
231
+ ' 2>/dev/null || echo '[]')
232
+ [ -z "$ARTIFACTS_JSON" ] && ARTIFACTS_JSON="[]"
233
+ fi
234
+ echo "$ARTIFACTS_JSON" > "$TMPD/artifacts"
235
+ ) &
236
+ PID_ARTIFACTS=$!
237
+
238
+ # --- Branch B: Memory commit + push ---
239
+ (
240
+ if [ "$NO_PUSH" = "1" ]; then
241
+ echo "skipped (--no-push)" > "$TMPD/memory"
242
+ else
243
+ # Wait for Branch D (artifacts query) so the committed file has the
244
+ # `## Session Artifacts` section. D is fast (~1s) and runs parallel
245
+ # with Branch A; this wait doesn't extend the critical path because
246
+ # Branch C (publish) is the bottleneck.
247
+ wait "$PID_ARTIFACTS" 2>/dev/null || true
248
+ if [ -s "$TMPD/artifacts" ]; then
249
+ ART_COUNT=$(jq 'length' "$TMPD/artifacts" 2>/dev/null || echo 0)
250
+ if [ "${ART_COUNT:-0}" -gt 0 ]; then
251
+ {
252
+ printf '\n## Session Artifacts\n\n'
253
+ jq -r '.[] | "- \(.type // "Artifact"): \(.title) -> \(.path // "")"' "$TMPD/artifacts" 2>/dev/null
254
+ } >> "$ABS_FILE"
255
+ fi
256
+ fi
257
+
258
+ cd "$MEMORY_DIR"
259
+ git add "$REL_FILE" handoffs/index.md >/dev/null 2>&1
260
+
261
+ if [ -n "$RECIPIENT" ]; then
262
+ MSG_COMMIT="Handoff: ${TOPIC} (to ${RECIPIENT})"
263
+ else
264
+ MSG_COMMIT="Handoff: ${TOPIC}"
265
+ fi
266
+
267
+ git commit -m "$MSG_COMMIT" >/dev/null 2>&1 || true
268
+
269
+ STASHED=0
270
+ if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
271
+ git stash push -u -m "handoff-run pre-pull stash" >/dev/null 2>&1 && STASHED=1
272
+ fi
273
+
274
+ MEM_OK=0
275
+ for i in 1 2 3; do
276
+ if git pull --rebase origin main --quiet >/dev/null 2>&1 \
277
+ && git push origin main --quiet >/dev/null 2>&1; then
278
+ MEM_OK=1
279
+ break
280
+ fi
281
+ sleep 1
282
+ done
283
+
284
+ [ "$STASHED" = "1" ] && git stash pop >/dev/null 2>&1 || true
285
+
286
+ if [ "$MEM_OK" = "1" ]; then
287
+ echo "ok" > "$TMPD/memory"
288
+ else
289
+ echo "failed" > "$TMPD/memory"
290
+ fi
291
+ fi
292
+ ) &
293
+ PID_MEMORY=$!
294
+
295
+ # --- Branch C: Publish artifact → Notify ---
296
+ #
297
+ # Strategy: handoffs publish ONCE, through the emissary write API, using
298
+ # `render_mode: "custom"` so the server serves OUR pre-rendered HTML
299
+ # verbatim (the same egregore-artifacts handoff template /view/ has
300
+ # always used). This gives us:
301
+ # - one artifact, one URL (no legacy + new double-publish)
302
+ # - permanent egregore.xyz/emissary/e/<id> URL with /raw endpoint
303
+ # - handoff design fully owned client-side (egregore-artifacts) — the
304
+ # emissary server template is never invoked for handoffs and remains
305
+ # completely untouched
306
+ #
307
+ # Fallback: if emissary publish fails (token missing, network, size cap,
308
+ # rendering failure), fall back to the legacy publish-artifact.sh path so
309
+ # a handoff still gets a Telegram-able URL.
310
+ (
311
+ V1URL=""
312
+ AURL=""
313
+ if [ "$NO_PUBLISH" = "0" ]; then
314
+ EMISSARY_CONFIG="$HOME/.egregore/emissary-config.json"
315
+ EMISSARY_TOKEN=""
316
+ if [ -f "$EMISSARY_CONFIG" ]; then
317
+ EMISSARY_TOKEN=$(jq -r '.auth_token // empty' "$EMISSARY_CONFIG" 2>/dev/null)
318
+ fi
319
+ EMISSARY_API_URL="${EMISSARY_API_URL:-https://egregore-production-55f2.up.railway.app/api/v1/emissary}"
320
+
321
+ if [ -n "$EMISSARY_TOKEN" ]; then
322
+ # Step 1: render the handoff to HTML locally using egregore-artifacts.
323
+ # Same renderer + template publish-artifact.sh uses today — handoffs
324
+ # keep their familiar look.
325
+ RENDERED_HTML="$TMPD/handoff.html"
326
+ RENDER_OK=0
327
+ LOCAL_CLI="$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js"
328
+ if [ "${EGREGORE_USE_PUBLISHED:-0}" != "1" ] && [ -f "$LOCAL_CLI" ] && [ -d "$SCRIPT_DIR/packages/egregore-artifacts/node_modules/react" ]; then
329
+ if node "$LOCAL_CLI" "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" --output "$RENDERED_HTML" >/dev/null 2>&1; then
330
+ RENDER_OK=1
331
+ fi
332
+ else
333
+ if npx egregore-artifacts "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" --output "$RENDERED_HTML" >/dev/null 2>&1; then
334
+ RENDER_OK=1
335
+ fi
336
+ fi
337
+
338
+ # Step 2: build the emissary payload. If rendering succeeded AND the
339
+ # HTML fits the server's 100KB cap, send it with render_mode=custom.
340
+ # If not, fall through to the structured-only path (server-rendered
341
+ # via the emissary template — undesirable for handoffs but still
342
+ # better than no URL).
343
+ if [ "$RENDER_OK" = "1" ] && [ -f "$RENDERED_HTML" ]; then
344
+ RENDER_SIZE=$(wc -c < "$RENDERED_HTML" | tr -d ' ')
345
+ if [ "$RENDER_SIZE" -gt 0 ] && [ "$RENDER_SIZE" -lt 100000 ]; then
346
+ # Author display name
347
+ DISPLAY_NAME="$AUTHOR"
348
+ PERSON_FILE="$SCRIPT_DIR/memory/people/${AUTHOR}.md"
349
+ if [ -f "$PERSON_FILE" ]; then
350
+ DISPLAY_NAME=$(head -1 "$PERSON_FILE" | sed 's/^# //')
351
+ fi
352
+
353
+ # addressed_to: only set if there's a recipient
354
+ ADDRESSED_TO="[]"
355
+ if [ -n "$RECIPIENT" ]; then
356
+ RECIP_HANDLE=$(echo "$RECIPIENT" | tr '[:upper:]' '[:lower:]' | awk '{print $1}')
357
+ ADDRESSED_TO=$(jq -nc --arg h "$RECIP_HANDLE" --arg d "$RECIPIENT" '[{"handle":$h,"display":$d}]')
358
+ fi
359
+
360
+ CLAIM=$(echo "$BRIEFING_LEAD" | cut -c1-200)
361
+
362
+ # body.prose is schema-required non-empty even when render_mode is
363
+ # "custom" (server validates structurally regardless of render
364
+ # mode). The visible page is the custom HTML; prose just needs to
365
+ # be non-empty plain text for /raw consumers and validation. Use
366
+ # the briefing lead (which the lead/claim is already derived from).
367
+ PROSE_TEXT="${BRIEFING_LEAD:-$TOPIC}"
368
+
369
+ # Use --rawfile so the rendered HTML is read directly from disk
370
+ # into a JSON-string. Faster than shell-var roundtrip and avoids
371
+ # any quoting headaches for HTML.
372
+ V1JSON=$(jq -nc \
373
+ --arg topic "$TOPIC" \
374
+ --arg claim "$CLAIM" \
375
+ --arg summary "$CLAIM" \
376
+ --arg prose "$PROSE_TEXT" \
377
+ --argjson addressed "$ADDRESSED_TO" \
378
+ --rawfile render_html "$RENDERED_HTML" \
379
+ '{
380
+ kind: "continuation",
381
+ topic: $topic,
382
+ claim: $claim,
383
+ summary: $summary,
384
+ body: { prose: $prose },
385
+ audience: {
386
+ addressed_to: $addressed,
387
+ visible_to: "public",
388
+ extendable_by: "anyone"
389
+ },
390
+ parents: [],
391
+ render_mode: "custom",
392
+ render_html: $render_html
393
+ }')
394
+
395
+ V1RESULT=$(curl -sf -X POST "$EMISSARY_API_URL/emissaries" \
396
+ -H "Content-Type: application/json" \
397
+ -H "Authorization: Bearer $EMISSARY_TOKEN" \
398
+ -d "$V1JSON" 2>/dev/null || echo '{}')
399
+
400
+ V1URL=$(echo "$V1RESULT" | jq -r '.url // ""' 2>/dev/null || echo "")
401
+ fi
402
+ fi
403
+ fi
404
+ fi
405
+
406
+ # Fallback: if the emissary publish didn't produce a URL (no token, no
407
+ # network, size cap, render failure), fire the legacy publish-artifact.sh
408
+ # so handoffs still get a Telegram-able URL.
409
+ if [ -z "$V1URL" ] && [ "$NO_PUBLISH" = "0" ] && [ -x "$SCRIPT_DIR/bin/publish-artifact.sh" ]; then
410
+ AURL=$(bash "$SCRIPT_DIR/bin/publish-artifact.sh" "$RENDER_TYPE" "${RENDER_SRC:-$ABS_FILE}" \
411
+ --title "$TOPIC" \
412
+ --author "$AUTHOR" \
413
+ --description "$BRIEFING_LEAD" 2>/dev/null || echo "")
414
+ fi
415
+
416
+ # Prefer v1 (emissary-rendered) URL when available; fall back to the
417
+ # legacy egregore-artifacts URL only if the v1 publish failed.
418
+ NOTIFY_URL=""
419
+ if [ -n "$V1URL" ]; then
420
+ echo "$V1URL" > "$TMPD/artifact"
421
+ NOTIFY_URL="$V1URL"
422
+ else
423
+ echo "$AURL" > "$TMPD/artifact"
424
+ NOTIFY_URL="$AURL"
425
+ fi
426
+
427
+ NS="skipped"
428
+ if [ "$NO_NOTIFY" = "0" ]; then
429
+ if [ -n "$NOTIFY_URL" ]; then
430
+ MSG_NOTIFY="Handoff from ${AUTHOR}: ${TOPIC}
431
+
432
+ \"${BRIEFING_SHORT}\"
433
+
434
+ View: ${NOTIFY_URL}"
435
+ else
436
+ MSG_NOTIFY="Handoff from ${AUTHOR}: ${TOPIC}
437
+
438
+ \"${BRIEFING_SHORT}\""
439
+ fi
440
+
441
+ # Always notify. With a recipient (connected mode) → DM. Otherwise → group.
442
+ # Self-handoffs still post to the group so they're visible on /activity
443
+ # and in the Telegram feed — a handoff without a Telegram beat is invisible.
444
+ if [ -n "$RECIPIENT" ] && [ "$MODE" = "connected" ]; then
445
+ NR=$(bash "$SCRIPT_DIR/bin/notify.sh" send "$RECIPIENT" "$MSG_NOTIFY" 2>/dev/null || echo "Failed")
446
+ else
447
+ NR=$(bash "$SCRIPT_DIR/bin/notify.sh" group "$MSG_NOTIFY" 2>/dev/null || echo "Failed")
448
+ fi
449
+
450
+ if echo "$NR" | grep -qi "^Sent"; then
451
+ NS="sent"
452
+ elif echo "$NR" | grep -qi "^Failed"; then
453
+ NS="failed"
454
+ else
455
+ NS="unknown"
456
+ fi
457
+ fi
458
+ echo "$NS" > "$TMPD/notify"
459
+ ) &
460
+ PID_PUBNOT=$!
461
+
462
+ # Join (never fail parent — each branch reports its own status).
463
+ # Branch D (PID_ARTIFACTS) is already awaited inside Branch B, but we wait
464
+ # again here to be safe if B skipped the wait (--no-push path).
465
+ wait "$PID_GRAPH" 2>/dev/null || true
466
+ wait "$PID_MEMORY" 2>/dev/null || true
467
+ wait "$PID_PUBNOT" 2>/dev/null || true
468
+ wait "$PID_ARTIFACTS" 2>/dev/null || true
469
+
470
+ # --- Collect results from tmpfiles --------------------------------------
471
+ SESSION_ID=""
472
+ RESOLVED=0
473
+ GRAPH_STATUS="skipped"
474
+ if [ -s "$TMPD/graph" ]; then
475
+ SESSION_ID=$(sed -n '1p' "$TMPD/graph")
476
+ RESOLVED=$(sed -n '2p' "$TMPD/graph")
477
+ GRAPH_STATUS=$(sed -n '3p' "$TMPD/graph")
478
+ fi
479
+ [ -z "$RESOLVED" ] && RESOLVED=0
480
+ # Sanitize: --argjson requires a number. If RESOLVED came back as a slug or
481
+ # anything non-numeric (the index-handoff.sh output format has drifted in
482
+ # the past — defensive guard), fall back to 0 so the result file still builds.
483
+ if ! echo "$RESOLVED" | grep -qE '^[0-9]+$'; then
484
+ RESOLVED=0
485
+ fi
486
+
487
+ SUBGRAPH_JSON=$(cat "$TMPD/subgraph" 2>/dev/null || echo "null")
488
+ [ -z "$SUBGRAPH_JSON" ] && SUBGRAPH_JSON="null"
489
+ # Sanitize: --argjson requires valid JSON. Multiple writes to the same
490
+ # tmpfile have produced concatenated values in the past (e.g. "null\n{...}"),
491
+ # which jq rejects on multi-document input. Validate; on failure, take the
492
+ # LAST non-empty line and re-validate; if still bad, fall back to null.
493
+ if ! printf '%s' "$SUBGRAPH_JSON" | jq -s 'length == 1' 2>/dev/null | grep -q '^true$'; then
494
+ LAST_LINE=$(printf '%s\n' "$SUBGRAPH_JSON" | awk 'NF{last=$0} END{print last}')
495
+ if printf '%s' "$LAST_LINE" | jq -e '.' >/dev/null 2>&1; then
496
+ SUBGRAPH_JSON="$LAST_LINE"
497
+ else
498
+ SUBGRAPH_JSON="null"
499
+ fi
500
+ fi
501
+
502
+ MEMORY_STATUS=$(cat "$TMPD/memory" 2>/dev/null || echo "skipped")
503
+ ARTIFACT_URL=$(cat "$TMPD/artifact" 2>/dev/null || echo "")
504
+ NOTIFY_STATUS=$(cat "$TMPD/notify" 2>/dev/null || echo "skipped")
505
+ ARTIFACTS_ARR=$(cat "$TMPD/artifacts" 2>/dev/null || echo "[]")
506
+ [ -z "$ARTIFACTS_ARR" ] && ARTIFACTS_ARR="[]"
507
+
508
+ # --- Detached PR-number backfill ----------------------------------------
509
+ # repo-state.sh was called with --no-pr for speed; now fire the backfill in
510
+ # the background so PR columns get populated without blocking this script.
511
+ # Reparented to init via `( cmd & ) >/dev/null 2>&1` — survives our exit.
512
+ if [ "$MEMORY_STATUS" = "ok" ] && grep -q '^## Repo State' "$ABS_FILE" 2>/dev/null; then
513
+ ( bash "$SCRIPT_DIR/bin/handoff-pr-backfill.sh" "$ABS_FILE" "$TOPIC" >/dev/null 2>&1 & ) >/dev/null 2>&1
514
+ fi
515
+
516
+ # --- Emit minimal stdout, stash structured data in a tmpfile ------------
517
+ #
518
+ # The Bash tool block shows stdout verbatim and collapses after ~3 lines.
519
+ # We keep stdout to exactly one compact status line so the block is
520
+ # visually quiet. Full structured data goes to a well-known tmpfile that
521
+ # the caller (skill) reads to render the rich card.
522
+ STATUS_BITS=("saved")
523
+ [ "$GRAPH_STATUS" = "ok" ] && STATUS_BITS+=("graphed")
524
+ [ "$MEMORY_STATUS" = "ok" ] && STATUS_BITS+=("pushed")
525
+ if [ "$NOTIFY_STATUS" = "sent" ]; then
526
+ if [ -n "$RECIPIENT" ]; then
527
+ STATUS_BITS+=("${RECIPIENT} notified")
528
+ else
529
+ STATUS_BITS+=("group notified")
530
+ fi
531
+ elif [ "$NOTIFY_STATUS" = "unknown" ] && [ -n "$RECIPIENT" ]; then
532
+ STATUS_BITS+=("${RECIPIENT} relayed to group")
533
+ fi
534
+ [ -n "$ARTIFACT_URL" ] && STATUS_BITS+=("published")
535
+
536
+ STATUS_LINE=""
537
+ for bit in "${STATUS_BITS[@]}"; do
538
+ if [ -z "$STATUS_LINE" ]; then
539
+ STATUS_LINE="$bit"
540
+ else
541
+ STATUS_LINE="${STATUS_LINE} · ${bit}"
542
+ fi
543
+ done
544
+
545
+ # Stash structured result for the caller
546
+ RESULT_FILE="${TMPDIR:-/tmp}/handoff-run-result.json"
547
+ jq -cn \
548
+ --arg mode "$MODE" \
549
+ --arg file "$REL_FILE" \
550
+ --arg absFile "$ABS_FILE" \
551
+ --arg sessionId "$SESSION_ID" \
552
+ --argjson resolved "${RESOLVED:-0}" \
553
+ --arg graphStatus "$GRAPH_STATUS" \
554
+ --arg memoryStatus "$MEMORY_STATUS" \
555
+ --arg notifyStatus "$NOTIFY_STATUS" \
556
+ --arg artifactUrl "$ARTIFACT_URL" \
557
+ --arg recipient "$RECIPIENT" \
558
+ --arg topic "$TOPIC" \
559
+ --arg author "$AUTHOR" \
560
+ --argjson subgraph "$SUBGRAPH_JSON" \
561
+ --argjson artifacts "$ARTIFACTS_ARR" \
562
+ '{
563
+ mode:$mode,
564
+ file:$file,
565
+ absFile:$absFile,
566
+ sessionId:$sessionId,
567
+ resolved:$resolved,
568
+ graphStatus:$graphStatus,
569
+ memoryStatus:$memoryStatus,
570
+ notifyStatus:$notifyStatus,
571
+ artifactUrl:$artifactUrl,
572
+ recipient:$recipient,
573
+ topic:$topic,
574
+ author:$author,
575
+ subgraph:$subgraph,
576
+ artifacts:$artifacts
577
+ }' > "$RESULT_FILE"
578
+
579
+ # One clean line for the Bash tool block.
580
+ # The caller reads structured data from $TMPDIR/handoff-run-result.json.
581
+ echo "⇌ ${STATUS_LINE}"