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