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