create-egregore 0.17.4 → 0.18.0

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 (110) 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 +11 -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/comment/SKILL.md +25 -0
  25. package/runtime/codex/.codex/skills/commit/SKILL.md +25 -0
  26. package/runtime/codex/.codex/skills/contribute/SKILL.md +25 -0
  27. package/runtime/codex/.codex/skills/dark-mode/SKILL.md +25 -0
  28. package/runtime/codex/.codex/skills/dashboard/SKILL.md +59 -0
  29. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +70 -0
  30. package/runtime/codex/.codex/skills/delete-user/SKILL.md +25 -0
  31. package/runtime/codex/.codex/skills/edit/SKILL.md +25 -0
  32. package/runtime/codex/.codex/skills/edit-process/SKILL.md +25 -0
  33. package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +25 -0
  34. package/runtime/codex/.codex/skills/emissary/SKILL.md +41 -0
  35. package/runtime/codex/.codex/skills/env/SKILL.md +25 -0
  36. package/runtime/codex/.codex/skills/handoff/SKILL.md +67 -0
  37. package/runtime/codex/.codex/skills/harvest/SKILL.md +107 -0
  38. package/runtime/codex/.codex/skills/infra/SKILL.md +41 -0
  39. package/runtime/codex/.codex/skills/ingest/SKILL.md +25 -0
  40. package/runtime/codex/.codex/skills/ingest-google/SKILL.md +25 -0
  41. package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  42. package/runtime/codex/.codex/skills/invite/SKILL.md +40 -0
  43. package/runtime/codex/.codex/skills/issue/SKILL.md +41 -0
  44. package/runtime/codex/.codex/skills/me/SKILL.md +25 -0
  45. package/runtime/codex/.codex/skills/meeting/SKILL.md +41 -0
  46. package/runtime/codex/.codex/skills/note/SKILL.md +25 -0
  47. package/runtime/codex/.codex/skills/onboarding/SKILL.md +41 -0
  48. package/runtime/codex/.codex/skills/pr/SKILL.md +25 -0
  49. package/runtime/codex/.codex/skills/project/SKILL.md +41 -0
  50. package/runtime/codex/.codex/skills/pull/SKILL.md +25 -0
  51. package/runtime/codex/.codex/skills/push/SKILL.md +25 -0
  52. package/runtime/codex/.codex/skills/quest/SKILL.md +77 -0
  53. package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +25 -0
  54. package/runtime/codex/.codex/skills/reflect/SKILL.md +41 -0
  55. package/runtime/codex/.codex/skills/review-pr/SKILL.md +41 -0
  56. package/runtime/codex/.codex/skills/save/SKILL.md +62 -0
  57. package/runtime/codex/.codex/skills/setup/SKILL.md +25 -0
  58. package/runtime/codex/.codex/skills/summon/SKILL.md +41 -0
  59. package/runtime/codex/.codex/skills/sync-repos/SKILL.md +25 -0
  60. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +25 -0
  61. package/runtime/codex/.codex/skills/telemetry/SKILL.md +25 -0
  62. package/runtime/codex/.codex/skills/test/SKILL.md +41 -0
  63. package/runtime/codex/.codex/skills/the-spiral/SKILL.md +84 -0
  64. package/runtime/codex/.codex/skills/todo/SKILL.md +41 -0
  65. package/runtime/codex/.codex/skills/tui-design/SKILL.md +41 -0
  66. package/runtime/codex/.codex/skills/tutorial/SKILL.md +41 -0
  67. package/runtime/codex/.codex/skills/update/SKILL.md +25 -0
  68. package/runtime/codex/.codex/skills/view/SKILL.md +41 -0
  69. package/runtime/codex/.codex/skills/visual-explain/SKILL.md +41 -0
  70. package/runtime/codex/.codex/skills/wrap/SKILL.md +55 -0
  71. package/runtime/codex/.codex/spec-manifest.json +108 -0
  72. package/runtime/codex/AGENTS.md +328 -0
  73. package/runtime/codex/bin/activity-data.sh +180 -0
  74. package/runtime/codex/bin/agent.sh +669 -0
  75. package/runtime/codex/bin/codex-install-skills.sh +78 -0
  76. package/runtime/codex/bin/codex-render-spec.mjs +308 -0
  77. package/runtime/codex/bin/codex-session-start.sh +238 -0
  78. package/runtime/codex/bin/codex-skill-render.mjs +315 -0
  79. package/runtime/codex/bin/codex-sync-skills.sh +223 -0
  80. package/runtime/codex/bin/dashboard-data.sh +178 -0
  81. package/runtime/codex/bin/graph-batch.sh +60 -0
  82. package/runtime/codex/bin/graph-op.sh +397 -0
  83. package/runtime/codex/bin/graph-wal.sh +228 -0
  84. package/runtime/codex/bin/graph.sh +141 -0
  85. package/runtime/codex/bin/handoff-pr-backfill.sh +86 -0
  86. package/runtime/codex/bin/handoff-run.sh +569 -0
  87. package/runtime/codex/bin/handoff-save-egregore.sh +131 -0
  88. package/runtime/codex/bin/index-handoff.sh +306 -0
  89. package/runtime/codex/bin/lib/artifact-id.sh +46 -0
  90. package/runtime/codex/bin/lib/config.sh +71 -0
  91. package/runtime/codex/bin/lib/context.sh +511 -0
  92. package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -0
  93. package/runtime/codex/bin/lib/edit-loopback.mjs +652 -0
  94. package/runtime/codex/bin/lib/ensure-develop.sh +183 -0
  95. package/runtime/codex/bin/lib/git-sync.sh +262 -0
  96. package/runtime/codex/bin/lib/greeting.sh +432 -0
  97. package/runtime/codex/bin/lib/hash.sh +13 -0
  98. package/runtime/codex/bin/lib/identity.sh +164 -0
  99. package/runtime/codex/bin/lib/permissions.sh +106 -0
  100. package/runtime/codex/bin/lib/time.sh +56 -0
  101. package/runtime/codex/bin/lib/worktree-links.sh +54 -0
  102. package/runtime/codex/bin/notify.sh +207 -0
  103. package/runtime/codex/bin/publish-artifact.sh +187 -0
  104. package/runtime/codex/bin/publish-references.sh +144 -0
  105. package/runtime/codex/bin/repo-state.sh +99 -0
  106. package/runtime/codex/bin/telemetry.sh +350 -0
  107. package/runtime/codex/bin/worktree-create.sh +115 -0
  108. package/runtime/codex/bin/worktree-remove.sh +50 -0
  109. package/runtime/codex/bin/worktree.sh +199 -0
  110. package/runtime/codex/manifest.json +162 -0
@@ -0,0 +1,569 @@
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
+ RAW=$(bash "$SCRIPT_DIR/bin/graph.sh" query "
207
+ MATCH (a:Artifact)-[:CONTRIBUTED_BY]->(p:Person {github: \$gh})
208
+ WHERE a.created >= datetime({year: $YY, month: $MM, day: $DD_NUM})
209
+ AND NOT 'tutorial-generated' IN coalesce(a.topics, [])
210
+ RETURN a.title AS title, a.type AS type, a.filePath AS path
211
+ ORDER BY a.created DESC
212
+ LIMIT 5" "$(jq -nc --arg gh "$AUTHOR" '{gh:$gh}')" 2>/dev/null || echo '{}')
213
+ # Response shape: {"fields":[...], "values":[[...], ...]}. Convert to
214
+ # an array of {title, type, path} objects.
215
+ ARTIFACTS_JSON=$(echo "$RAW" | jq -c '
216
+ (.fields // []) as $f
217
+ | (.values // [])
218
+ | map(. as $row | reduce range(0; ($f | length)) as $i ({}; .[$f[$i]] = $row[$i]))
219
+ ' 2>/dev/null || echo '[]')
220
+ [ -z "$ARTIFACTS_JSON" ] && ARTIFACTS_JSON="[]"
221
+ fi
222
+ echo "$ARTIFACTS_JSON" > "$TMPD/artifacts"
223
+ ) &
224
+ PID_ARTIFACTS=$!
225
+
226
+ # --- Branch B: Memory commit + push ---
227
+ (
228
+ if [ "$NO_PUSH" = "1" ]; then
229
+ echo "skipped (--no-push)" > "$TMPD/memory"
230
+ else
231
+ # Wait for Branch D (artifacts query) so the committed file has the
232
+ # `## Session Artifacts` section. D is fast (~1s) and runs parallel
233
+ # with Branch A; this wait doesn't extend the critical path because
234
+ # Branch C (publish) is the bottleneck.
235
+ wait "$PID_ARTIFACTS" 2>/dev/null || true
236
+ if [ -s "$TMPD/artifacts" ]; then
237
+ ART_COUNT=$(jq 'length' "$TMPD/artifacts" 2>/dev/null || echo 0)
238
+ if [ "${ART_COUNT:-0}" -gt 0 ]; then
239
+ {
240
+ printf '\n## Session Artifacts\n\n'
241
+ jq -r '.[] | "- \(.type // "Artifact"): \(.title) -> \(.path // "")"' "$TMPD/artifacts" 2>/dev/null
242
+ } >> "$ABS_FILE"
243
+ fi
244
+ fi
245
+
246
+ cd "$MEMORY_DIR"
247
+ git add "$REL_FILE" handoffs/index.md >/dev/null 2>&1
248
+
249
+ if [ -n "$RECIPIENT" ]; then
250
+ MSG_COMMIT="Handoff: ${TOPIC} (to ${RECIPIENT})"
251
+ else
252
+ MSG_COMMIT="Handoff: ${TOPIC}"
253
+ fi
254
+
255
+ git commit -m "$MSG_COMMIT" >/dev/null 2>&1 || true
256
+
257
+ STASHED=0
258
+ if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
259
+ git stash push -u -m "handoff-run pre-pull stash" >/dev/null 2>&1 && STASHED=1
260
+ fi
261
+
262
+ MEM_OK=0
263
+ for i in 1 2 3; do
264
+ if git pull --rebase origin main --quiet >/dev/null 2>&1 \
265
+ && git push origin main --quiet >/dev/null 2>&1; then
266
+ MEM_OK=1
267
+ break
268
+ fi
269
+ sleep 1
270
+ done
271
+
272
+ [ "$STASHED" = "1" ] && git stash pop >/dev/null 2>&1 || true
273
+
274
+ if [ "$MEM_OK" = "1" ]; then
275
+ echo "ok" > "$TMPD/memory"
276
+ else
277
+ echo "failed" > "$TMPD/memory"
278
+ fi
279
+ fi
280
+ ) &
281
+ PID_MEMORY=$!
282
+
283
+ # --- Branch C: Publish artifact → Notify ---
284
+ #
285
+ # Strategy: handoffs publish ONCE, through the emissary write API, using
286
+ # `render_mode: "custom"` so the server serves OUR pre-rendered HTML
287
+ # verbatim (the same egregore-artifacts handoff template /view/ has
288
+ # always used). This gives us:
289
+ # - one artifact, one URL (no legacy + new double-publish)
290
+ # - permanent egregore.xyz/emissary/e/<id> URL with /raw endpoint
291
+ # - handoff design fully owned client-side (egregore-artifacts) — the
292
+ # emissary server template is never invoked for handoffs and remains
293
+ # completely untouched
294
+ #
295
+ # Fallback: if emissary publish fails (token missing, network, size cap,
296
+ # rendering failure), fall back to the legacy publish-artifact.sh path so
297
+ # a handoff still gets a Telegram-able URL.
298
+ (
299
+ V1URL=""
300
+ AURL=""
301
+ if [ "$NO_PUBLISH" = "0" ]; then
302
+ EMISSARY_CONFIG="$HOME/.egregore/emissary-config.json"
303
+ EMISSARY_TOKEN=""
304
+ if [ -f "$EMISSARY_CONFIG" ]; then
305
+ EMISSARY_TOKEN=$(jq -r '.auth_token // empty' "$EMISSARY_CONFIG" 2>/dev/null)
306
+ fi
307
+ EMISSARY_API_URL="${EMISSARY_API_URL:-https://egregore-production-55f2.up.railway.app/api/v1/emissary}"
308
+
309
+ if [ -n "$EMISSARY_TOKEN" ]; then
310
+ # Step 1: render the handoff to HTML locally using egregore-artifacts.
311
+ # Same renderer + template publish-artifact.sh uses today — handoffs
312
+ # keep their familiar look.
313
+ RENDERED_HTML="$TMPD/handoff.html"
314
+ RENDER_OK=0
315
+ LOCAL_CLI="$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js"
316
+ if [ "${EGREGORE_USE_PUBLISHED:-0}" != "1" ] && [ -f "$LOCAL_CLI" ] && [ -d "$SCRIPT_DIR/packages/egregore-artifacts/node_modules/react" ]; then
317
+ if node "$LOCAL_CLI" handoff "$ABS_FILE" --output "$RENDERED_HTML" >/dev/null 2>&1; then
318
+ RENDER_OK=1
319
+ fi
320
+ else
321
+ if npx egregore-artifacts handoff "$ABS_FILE" --output "$RENDERED_HTML" >/dev/null 2>&1; then
322
+ RENDER_OK=1
323
+ fi
324
+ fi
325
+
326
+ # Step 2: build the emissary payload. If rendering succeeded AND the
327
+ # HTML fits the server's 100KB cap, send it with render_mode=custom.
328
+ # If not, fall through to the structured-only path (server-rendered
329
+ # via the emissary template — undesirable for handoffs but still
330
+ # better than no URL).
331
+ if [ "$RENDER_OK" = "1" ] && [ -f "$RENDERED_HTML" ]; then
332
+ RENDER_SIZE=$(wc -c < "$RENDERED_HTML" | tr -d ' ')
333
+ if [ "$RENDER_SIZE" -gt 0 ] && [ "$RENDER_SIZE" -lt 100000 ]; then
334
+ # Author display name
335
+ DISPLAY_NAME="$AUTHOR"
336
+ PERSON_FILE="$SCRIPT_DIR/memory/people/${AUTHOR}.md"
337
+ if [ -f "$PERSON_FILE" ]; then
338
+ DISPLAY_NAME=$(head -1 "$PERSON_FILE" | sed 's/^# //')
339
+ fi
340
+
341
+ # addressed_to: only set if there's a recipient
342
+ ADDRESSED_TO="[]"
343
+ if [ -n "$RECIPIENT" ]; then
344
+ RECIP_HANDLE=$(echo "$RECIPIENT" | tr '[:upper:]' '[:lower:]' | awk '{print $1}')
345
+ ADDRESSED_TO=$(jq -nc --arg h "$RECIP_HANDLE" --arg d "$RECIPIENT" '[{"handle":$h,"display":$d}]')
346
+ fi
347
+
348
+ CLAIM=$(echo "$BRIEFING_LEAD" | cut -c1-200)
349
+
350
+ # body.prose is schema-required non-empty even when render_mode is
351
+ # "custom" (server validates structurally regardless of render
352
+ # mode). The visible page is the custom HTML; prose just needs to
353
+ # be non-empty plain text for /raw consumers and validation. Use
354
+ # the briefing lead (which the lead/claim is already derived from).
355
+ PROSE_TEXT="${BRIEFING_LEAD:-$TOPIC}"
356
+
357
+ # Use --rawfile so the rendered HTML is read directly from disk
358
+ # into a JSON-string. Faster than shell-var roundtrip and avoids
359
+ # any quoting headaches for HTML.
360
+ V1JSON=$(jq -nc \
361
+ --arg topic "$TOPIC" \
362
+ --arg claim "$CLAIM" \
363
+ --arg summary "$CLAIM" \
364
+ --arg prose "$PROSE_TEXT" \
365
+ --argjson addressed "$ADDRESSED_TO" \
366
+ --rawfile render_html "$RENDERED_HTML" \
367
+ '{
368
+ kind: "continuation",
369
+ topic: $topic,
370
+ claim: $claim,
371
+ summary: $summary,
372
+ body: { prose: $prose },
373
+ audience: {
374
+ addressed_to: $addressed,
375
+ visible_to: "public",
376
+ extendable_by: "anyone"
377
+ },
378
+ parents: [],
379
+ render_mode: "custom",
380
+ render_html: $render_html
381
+ }')
382
+
383
+ V1RESULT=$(curl -sf -X POST "$EMISSARY_API_URL/emissaries" \
384
+ -H "Content-Type: application/json" \
385
+ -H "Authorization: Bearer $EMISSARY_TOKEN" \
386
+ -d "$V1JSON" 2>/dev/null || echo '{}')
387
+
388
+ V1URL=$(echo "$V1RESULT" | jq -r '.url // ""' 2>/dev/null || echo "")
389
+ fi
390
+ fi
391
+ fi
392
+ fi
393
+
394
+ # Fallback: if the emissary publish didn't produce a URL (no token, no
395
+ # network, size cap, render failure), fire the legacy publish-artifact.sh
396
+ # so handoffs still get a Telegram-able URL.
397
+ if [ -z "$V1URL" ] && [ "$NO_PUBLISH" = "0" ] && [ -x "$SCRIPT_DIR/bin/publish-artifact.sh" ]; then
398
+ AURL=$(bash "$SCRIPT_DIR/bin/publish-artifact.sh" handoff "$ABS_FILE" \
399
+ --title "$TOPIC" \
400
+ --author "$AUTHOR" \
401
+ --description "$BRIEFING_LEAD" 2>/dev/null || echo "")
402
+ fi
403
+
404
+ # Prefer v1 (emissary-rendered) URL when available; fall back to the
405
+ # legacy egregore-artifacts URL only if the v1 publish failed.
406
+ NOTIFY_URL=""
407
+ if [ -n "$V1URL" ]; then
408
+ echo "$V1URL" > "$TMPD/artifact"
409
+ NOTIFY_URL="$V1URL"
410
+ else
411
+ echo "$AURL" > "$TMPD/artifact"
412
+ NOTIFY_URL="$AURL"
413
+ fi
414
+
415
+ NS="skipped"
416
+ if [ "$NO_NOTIFY" = "0" ]; then
417
+ if [ -n "$NOTIFY_URL" ]; then
418
+ MSG_NOTIFY="Handoff from ${AUTHOR}: ${TOPIC}
419
+
420
+ \"${BRIEFING_SHORT}\"
421
+
422
+ View: ${NOTIFY_URL}"
423
+ else
424
+ MSG_NOTIFY="Handoff from ${AUTHOR}: ${TOPIC}
425
+
426
+ \"${BRIEFING_SHORT}\""
427
+ fi
428
+
429
+ # Always notify. With a recipient (connected mode) → DM. Otherwise → group.
430
+ # Self-handoffs still post to the group so they're visible on /activity
431
+ # and in the Telegram feed — a handoff without a Telegram beat is invisible.
432
+ if [ -n "$RECIPIENT" ] && [ "$MODE" = "connected" ]; then
433
+ NR=$(bash "$SCRIPT_DIR/bin/notify.sh" send "$RECIPIENT" "$MSG_NOTIFY" 2>/dev/null || echo "Failed")
434
+ else
435
+ NR=$(bash "$SCRIPT_DIR/bin/notify.sh" group "$MSG_NOTIFY" 2>/dev/null || echo "Failed")
436
+ fi
437
+
438
+ if echo "$NR" | grep -qi "^Sent"; then
439
+ NS="sent"
440
+ elif echo "$NR" | grep -qi "^Failed"; then
441
+ NS="failed"
442
+ else
443
+ NS="unknown"
444
+ fi
445
+ fi
446
+ echo "$NS" > "$TMPD/notify"
447
+ ) &
448
+ PID_PUBNOT=$!
449
+
450
+ # Join (never fail parent — each branch reports its own status).
451
+ # Branch D (PID_ARTIFACTS) is already awaited inside Branch B, but we wait
452
+ # again here to be safe if B skipped the wait (--no-push path).
453
+ wait "$PID_GRAPH" 2>/dev/null || true
454
+ wait "$PID_MEMORY" 2>/dev/null || true
455
+ wait "$PID_PUBNOT" 2>/dev/null || true
456
+ wait "$PID_ARTIFACTS" 2>/dev/null || true
457
+
458
+ # --- Collect results from tmpfiles --------------------------------------
459
+ SESSION_ID=""
460
+ RESOLVED=0
461
+ GRAPH_STATUS="skipped"
462
+ if [ -s "$TMPD/graph" ]; then
463
+ SESSION_ID=$(sed -n '1p' "$TMPD/graph")
464
+ RESOLVED=$(sed -n '2p' "$TMPD/graph")
465
+ GRAPH_STATUS=$(sed -n '3p' "$TMPD/graph")
466
+ fi
467
+ [ -z "$RESOLVED" ] && RESOLVED=0
468
+ # Sanitize: --argjson requires a number. If RESOLVED came back as a slug or
469
+ # anything non-numeric (the index-handoff.sh output format has drifted in
470
+ # the past — defensive guard), fall back to 0 so the result file still builds.
471
+ if ! echo "$RESOLVED" | grep -qE '^[0-9]+$'; then
472
+ RESOLVED=0
473
+ fi
474
+
475
+ SUBGRAPH_JSON=$(cat "$TMPD/subgraph" 2>/dev/null || echo "null")
476
+ [ -z "$SUBGRAPH_JSON" ] && SUBGRAPH_JSON="null"
477
+ # Sanitize: --argjson requires valid JSON. Multiple writes to the same
478
+ # tmpfile have produced concatenated values in the past (e.g. "null\n{...}"),
479
+ # which jq rejects on multi-document input. Validate; on failure, take the
480
+ # LAST non-empty line and re-validate; if still bad, fall back to null.
481
+ if ! printf '%s' "$SUBGRAPH_JSON" | jq -s 'length == 1' 2>/dev/null | grep -q '^true$'; then
482
+ LAST_LINE=$(printf '%s\n' "$SUBGRAPH_JSON" | awk 'NF{last=$0} END{print last}')
483
+ if printf '%s' "$LAST_LINE" | jq -e '.' >/dev/null 2>&1; then
484
+ SUBGRAPH_JSON="$LAST_LINE"
485
+ else
486
+ SUBGRAPH_JSON="null"
487
+ fi
488
+ fi
489
+
490
+ MEMORY_STATUS=$(cat "$TMPD/memory" 2>/dev/null || echo "skipped")
491
+ ARTIFACT_URL=$(cat "$TMPD/artifact" 2>/dev/null || echo "")
492
+ NOTIFY_STATUS=$(cat "$TMPD/notify" 2>/dev/null || echo "skipped")
493
+ ARTIFACTS_ARR=$(cat "$TMPD/artifacts" 2>/dev/null || echo "[]")
494
+ [ -z "$ARTIFACTS_ARR" ] && ARTIFACTS_ARR="[]"
495
+
496
+ # --- Detached PR-number backfill ----------------------------------------
497
+ # repo-state.sh was called with --no-pr for speed; now fire the backfill in
498
+ # the background so PR columns get populated without blocking this script.
499
+ # Reparented to init via `( cmd & ) >/dev/null 2>&1` — survives our exit.
500
+ if [ "$MEMORY_STATUS" = "ok" ] && grep -q '^## Repo State' "$ABS_FILE" 2>/dev/null; then
501
+ ( bash "$SCRIPT_DIR/bin/handoff-pr-backfill.sh" "$ABS_FILE" "$TOPIC" >/dev/null 2>&1 & ) >/dev/null 2>&1
502
+ fi
503
+
504
+ # --- Emit minimal stdout, stash structured data in a tmpfile ------------
505
+ #
506
+ # The Bash tool block shows stdout verbatim and collapses after ~3 lines.
507
+ # We keep stdout to exactly one compact status line so the block is
508
+ # visually quiet. Full structured data goes to a well-known tmpfile that
509
+ # the caller (skill) reads to render the rich card.
510
+ STATUS_BITS=("saved")
511
+ [ "$GRAPH_STATUS" = "ok" ] && STATUS_BITS+=("graphed")
512
+ [ "$MEMORY_STATUS" = "ok" ] && STATUS_BITS+=("pushed")
513
+ if [ "$NOTIFY_STATUS" = "sent" ]; then
514
+ if [ -n "$RECIPIENT" ]; then
515
+ STATUS_BITS+=("${RECIPIENT} notified")
516
+ else
517
+ STATUS_BITS+=("group notified")
518
+ fi
519
+ elif [ "$NOTIFY_STATUS" = "unknown" ] && [ -n "$RECIPIENT" ]; then
520
+ STATUS_BITS+=("${RECIPIENT} relayed to group")
521
+ fi
522
+ [ -n "$ARTIFACT_URL" ] && STATUS_BITS+=("published")
523
+
524
+ STATUS_LINE=""
525
+ for bit in "${STATUS_BITS[@]}"; do
526
+ if [ -z "$STATUS_LINE" ]; then
527
+ STATUS_LINE="$bit"
528
+ else
529
+ STATUS_LINE="${STATUS_LINE} · ${bit}"
530
+ fi
531
+ done
532
+
533
+ # Stash structured result for the caller
534
+ RESULT_FILE="${TMPDIR:-/tmp}/handoff-run-result.json"
535
+ jq -cn \
536
+ --arg mode "$MODE" \
537
+ --arg file "$REL_FILE" \
538
+ --arg absFile "$ABS_FILE" \
539
+ --arg sessionId "$SESSION_ID" \
540
+ --argjson resolved "${RESOLVED:-0}" \
541
+ --arg graphStatus "$GRAPH_STATUS" \
542
+ --arg memoryStatus "$MEMORY_STATUS" \
543
+ --arg notifyStatus "$NOTIFY_STATUS" \
544
+ --arg artifactUrl "$ARTIFACT_URL" \
545
+ --arg recipient "$RECIPIENT" \
546
+ --arg topic "$TOPIC" \
547
+ --arg author "$AUTHOR" \
548
+ --argjson subgraph "$SUBGRAPH_JSON" \
549
+ --argjson artifacts "$ARTIFACTS_ARR" \
550
+ '{
551
+ mode:$mode,
552
+ file:$file,
553
+ absFile:$absFile,
554
+ sessionId:$sessionId,
555
+ resolved:$resolved,
556
+ graphStatus:$graphStatus,
557
+ memoryStatus:$memoryStatus,
558
+ notifyStatus:$notifyStatus,
559
+ artifactUrl:$artifactUrl,
560
+ recipient:$recipient,
561
+ topic:$topic,
562
+ author:$author,
563
+ subgraph:$subgraph,
564
+ artifacts:$artifacts
565
+ }' > "$RESULT_FILE"
566
+
567
+ # One clean line for the Bash tool block.
568
+ # The caller reads structured data from $TMPDIR/handoff-run-result.json.
569
+ echo "⇌ ${STATUS_LINE}"