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