create-egregore 0.19.3 → 0.19.5
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 +3 -1
- package/lib/local.js +63 -6
- package/package.json +2 -2
- package/runtime/codex/.codex/hooks/search-hint.js +65 -0
- package/runtime/codex/.codex/hooks.json +13 -0
- package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +14 -10
- package/runtime/codex/.codex/skills/minion/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/scroll/SKILL.md +168 -0
- package/runtime/codex/.codex/skills/scroll/agents/openai.yaml +4 -0
- package/runtime/codex/.codex/skills/scroll/assets/shell.html +542 -0
- package/runtime/codex/.codex/skills/search/SKILL.md +56 -0
- package/runtime/codex/.codex/skills/session-view/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/thread/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/view/SKILL.md +147 -32
- package/runtime/codex/.codex/skills/view/agents/openai.yaml +4 -0
- package/runtime/codex/.codex/skills/view/assets/compose-scaffold.html +221 -0
- package/runtime/codex/.codex/spec-manifest.json +3 -3
- package/runtime/codex/AGENTS.md +24 -3
- package/runtime/codex/bin/activity-data.sh +46 -4
- package/runtime/codex/bin/agent.sh +10 -2
- package/runtime/codex/bin/codex-install-skills.sh +1 -1
- package/runtime/codex/bin/codex-render-spec.mjs +18 -1
- package/runtime/codex/bin/codex-session-start.sh +1 -1
- package/runtime/codex/bin/codex-skill-render.mjs +13 -7
- package/runtime/codex/bin/codex-sync-skills.sh +4 -0
- package/runtime/codex/bin/graph-op.sh +56 -5
- package/runtime/codex/bin/graph.sh +36 -0
- package/runtime/codex/bin/handoff-run.sh +157 -7
- package/runtime/codex/bin/handoff-save-egregore.sh +6 -1
- package/runtime/codex/bin/index-handoff.sh +9 -3
- package/runtime/codex/bin/lib/context.sh +89 -17
- package/runtime/codex/bin/lib/dashboard-artifact.sh +1 -1
- package/runtime/codex/bin/lib/git-sync.sh +35 -9
- package/runtime/codex/bin/lib/greeting.sh +25 -3
- package/runtime/codex/bin/publish-artifact.sh +29 -1
- package/runtime/codex/manifest.json +16 -1
|
@@ -71,7 +71,7 @@ for skill_dir in "$SOURCE_DIR"/*; do
|
|
|
71
71
|
[ -f "$skill_dir/SKILL.md" ] || continue
|
|
72
72
|
name="$(basename "$skill_dir")"
|
|
73
73
|
mkdir -p "$TARGET_DIR/$name"
|
|
74
|
-
cp "$skill_dir
|
|
74
|
+
cp -R "$skill_dir/." "$TARGET_DIR/$name/"
|
|
75
75
|
copied=$((copied + 1))
|
|
76
76
|
done
|
|
77
77
|
|
|
@@ -73,6 +73,12 @@ portable memory protocol below.
|
|
|
73
73
|
Use the runtime-neutral bridge instead of Claude Code slash commands:
|
|
74
74
|
|
|
75
75
|
\`\`\`bash
|
|
76
|
+
bin/agent.sh branch --topic "auth review"
|
|
77
|
+
bin/agent.sh save --message "Save: auth review" --topic "auth review"
|
|
78
|
+
bin/agent.sh wrap --from alice --topic "auth review" \\
|
|
79
|
+
--summary "Implemented the OAuth callback parser and documented follow-ups." \\
|
|
80
|
+
--body "Open threads: review error cases and browser redirects."
|
|
81
|
+
|
|
76
82
|
bin/agent.sh handoff --from alice --to bob --topic "auth review" \\
|
|
77
83
|
--body "Implemented the OAuth callback parser. Bob should review error cases."
|
|
78
84
|
|
|
@@ -88,6 +94,17 @@ Each command writes to the existing Git-backed \`memory/\` repository and pushes
|
|
|
88
94
|
when the memory repo has an \`origin\` remote. Agents that cannot run shell may
|
|
89
95
|
write the same markdown files directly, following \`docs/AGENT-PROTOCOL.md\`.
|
|
90
96
|
|
|
97
|
+
## Handoffs vs Emissaries
|
|
98
|
+
|
|
99
|
+
\`bin/agent.sh handoff\` is the runtime-neutral path for internal team
|
|
100
|
+
session-handoffs: it writes to \`memory/handoffs/\`, updates the index, and uses
|
|
101
|
+
the same \`bin/handoff-run.sh\` machinery as Claude Code's \`/handoff\`.
|
|
102
|
+
|
|
103
|
+
Portable external capsules are **emissaries**, not team handoffs. Claude Code
|
|
104
|
+
routes those through \`/emissary\`; non-Claude agents should use the
|
|
105
|
+
\`egregore-emissary\` CLI/skill when it is installed. Do not use the deprecated
|
|
106
|
+
\`egregore-handoff\` CLI for Egregore project \`/handoff\` work.
|
|
107
|
+
|
|
91
108
|
## Compatibility
|
|
92
109
|
|
|
93
110
|
Claude Code users continue using \`/handoff\`, \`/activity\`, \`/ask\`, \`/save\`, and
|
|
@@ -216,7 +233,7 @@ If this Codex build does not support hooks, follow the same discipline as a stan
|
|
|
216
233
|
|
|
217
234
|
If the startup card output contains \`onboarding_needed\`, invoke the \`$onboarding\` skill instead of greeting.`;
|
|
218
235
|
|
|
219
|
-
const COMMAND_AWARENESS_PREPEND = `Codex reserves leading \`/\` for built-ins, so Egregore workflows are **skills**, not slash commands. Invoke them with the matching \`$name\` skill token or from natural language intent ("show activity", "make a handoff"). Hand-written native Codex skills: \`$activity\`, \`$handoff\`, \`$wrap\`, \`$announce\`, \`$harvest\`, \`$the-spiral\`, \`$dashboard\`, \`$deep-reflect\`, \`$quest\`, \`$invite\`, \`$ask\`, \`$save\`. Every other workflow has a generated adapter skill of the same name. \`$save\` is the user-facing abstraction for committing, pushing, opening or reusing pull requests, and syncing memory — never make users manage the git workflow by hand.`;
|
|
236
|
+
const COMMAND_AWARENESS_PREPEND = `Codex reserves leading \`/\` for built-ins, so Egregore workflows are **skills**, not slash commands. Invoke them with the matching \`$name\` skill token or from natural language intent ("show activity", "make a handoff"). Hand-written native Codex skills: \`$activity\`, \`$handoff\`, \`$wrap\`, \`$announce\`, \`$harvest\`, \`$the-spiral\`, \`$dashboard\`, \`$deep-reflect\`, \`$quest\`, \`$invite\`, \`$ask\`, \`$save\`, \`$view\`, and \`$scroll\`. Every other workflow has a generated adapter skill of the same name. \`$save\` is the user-facing abstraction for committing, pushing, opening or reusing pull requests, and syncing memory — never make users manage the git workflow by hand.`;
|
|
220
237
|
|
|
221
238
|
const SOCRATIC_BODY = `**Triggers**: "ask me questions", "question me", "help me think through", or any request to be questioned.
|
|
222
239
|
|
|
@@ -229,7 +229,7 @@ Follow AGENTS.md before code changes.
|
|
|
229
229
|
After the user names what they are working on, use bin/agent.sh branch --topic "<work topic>" if you are not already in an appropriate task worktree, then continue file work from the printed worktree path.
|
|
230
230
|
Use bin/agent.sh to update shared activity: handoff, ask, and answer write to memory/ and push when possible.
|
|
231
231
|
Use bin/agent.sh handoff for internal team session-handoffs. Portable external capsules are emissaries; use egregore-emissary when installed, not the deprecated egregore-handoff CLI.
|
|
232
|
-
Codex reserves leading slash commands for built-ins, so custom Egregore /activity-style commands will not reach the agent. Egregore native Codex skills are: \$activity, \$handoff, \$wrap, \$announce, \$harvest, \$the-spiral, \$dashboard, \$deep-reflect, \$quest, \$invite, \$ask, and \$
|
|
232
|
+
Codex reserves leading slash commands for built-ins, so custom Egregore /activity-style commands will not reach the agent. Egregore native Codex skills are: \$activity, \$handoff, \$wrap, \$announce, \$harvest, \$the-spiral, \$dashboard, \$deep-reflect, \$quest, \$invite, \$ask, \$save, \$view, and \$scroll. Natural-language requests such as "show activity", "make a handoff", or "render this as a scroll" work too. Remaining mirrored workflows are adapter skills for long-tail coverage.
|
|
233
233
|
\$save is the user-facing abstraction for committing, pushing, opening or reusing pull requests, and syncing memory; do not make users manage the git workflow by hand.
|
|
234
234
|
Egregore Codex workflows are skill invocations, not Claude Code slash commands. Do not rely on Claude Code-only command machinery.
|
|
235
235
|
If onboarding_complete is false, use the memory protocol directly and make sure memory/people/${AUTHOR}.md exists before the first handoff.
|
|
@@ -143,7 +143,7 @@ function renderActivity(data) {
|
|
|
143
143
|
const org = data.org || data.organization || 'Egregore';
|
|
144
144
|
const date = data.date || '';
|
|
145
145
|
const me = data.me?.github || data.me?.name || data.github_username || '';
|
|
146
|
-
const handoffs = firstArray(data, ['handoffs', 'pending_handoffs', 'me.handoffs', 'addressed_handoffs']);
|
|
146
|
+
const handoffs = firstArray(data, ['handoffs_to_me', 'handoffs', 'pending_handoffs', 'me.handoffs', 'addressed_handoffs']);
|
|
147
147
|
const questions = firstArray(data, ['questions', 'pending_questions', 'me.questions']);
|
|
148
148
|
const prs = asArray(data.prs);
|
|
149
149
|
const mine = firstArray(data, ['local_sessions.my_sessions', 'my_sessions', 'sessions']);
|
|
@@ -159,12 +159,18 @@ function renderActivity(data) {
|
|
|
159
159
|
if (questions.length === 0 && handoffs.length === 0) {
|
|
160
160
|
lines.push(padLine('nothing pending'));
|
|
161
161
|
} else {
|
|
162
|
-
questions.
|
|
163
|
-
lines.push(padLine(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
162
|
+
if (questions.length > 0) {
|
|
163
|
+
lines.push(padLine('QUESTIONS'));
|
|
164
|
+
questions.slice(0, 4).forEach((q) => {
|
|
165
|
+
lines.push(padLine(`● ${valueOf(q, ['from', 'author', 'asker'], 'someone')}: ${valueOf(q, ['topic', 'title', 'question'], 'question')}`));
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (handoffs.length > 0) {
|
|
169
|
+
lines.push(padLine('HANDOFFS'));
|
|
170
|
+
handoffs.slice(0, 4).forEach((h) => {
|
|
171
|
+
lines.push(padLine(`○ ${valueOf(h, ['author', 'from'], 'someone')}: ${valueOf(h, ['topic', 'title'], 'handoff')}`));
|
|
172
|
+
});
|
|
173
|
+
}
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
lines.push(rule());
|
|
@@ -19,8 +19,11 @@ set -euo pipefail
|
|
|
19
19
|
# remove-name to keep-name, stores remove-name as alias
|
|
20
20
|
# create-harvest <id> <topic> <intent> <initiator>
|
|
21
21
|
# Create a Harvest node and link to initiator (WAL-backed)
|
|
22
|
-
# create-harvest-session <harvest-id> <session-id> <person-name>
|
|
23
|
-
# Create a HarvestSession linked to a Harvest and Person
|
|
22
|
+
# create-harvest-session <harvest-id> <session-id> <person-name> [status]
|
|
23
|
+
# Create a HarvestSession linked to a Harvest and Person.
|
|
24
|
+
# status defaults to 'active' (back-compat). Valid values:
|
|
25
|
+
# pending | active | answered | complete | incorporated.
|
|
26
|
+
# Async respondents should be created with 'pending'.
|
|
24
27
|
# record-harvest-turn <session-id> <turn> <question> <intent> [answer] [eval]
|
|
25
28
|
# Record a question-answer turn in a HarvestSession
|
|
26
29
|
# complete-harvest <harvest-id> <artifact-path>
|
|
@@ -42,6 +45,7 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
|
|
42
45
|
echo " merge-person <keep> <rm> Merge two Person nodes"
|
|
43
46
|
echo " claim-handoff <sid> <ho> Link implementing session to handoff"
|
|
44
47
|
echo " create-pr <sid> <num> <repo> <author> [title]"
|
|
48
|
+
echo " register-artifact <id> <url> <title> <type> <author> <topics> <excerpt>"
|
|
45
49
|
echo " create-harvest / complete-harvest / record-harvest-turn"
|
|
46
50
|
echo " wal-status Show WAL pending count"
|
|
47
51
|
exit 0
|
|
@@ -202,6 +206,39 @@ case "$OP" in
|
|
|
202
206
|
bash "$GS" query "$CYPHER" "$PARAMS"
|
|
203
207
|
;;
|
|
204
208
|
|
|
209
|
+
register-artifact)
|
|
210
|
+
# Index a published (hosted) artifact as an Artifact node so the finder can
|
|
211
|
+
# retrieve it from the graph in connected mode — by content (title/topics/
|
|
212
|
+
# excerpt CONTAINS) and by relationship (who made it, which quest). The node
|
|
213
|
+
# id is the registry FILENAME STEM (date-author-slug), matching sync-graph.sh
|
|
214
|
+
# so a later /save sync upserts the same node instead of duplicating it.
|
|
215
|
+
# args: <id> <url> <title> <type> <author-handle> <topics-csv> <excerpt>
|
|
216
|
+
A_ID="${1:?missing id}"
|
|
217
|
+
A_URL="${2:?missing url}"
|
|
218
|
+
A_TITLE="${3:-}"
|
|
219
|
+
A_TYPE="${4:-artifact}"
|
|
220
|
+
A_AUTHOR="$(printf '%s' "${5:-}" | tr '[:upper:]' '[:lower:]' | awk '{print $1}')"
|
|
221
|
+
A_TOPICS_JSON="$(printf '%s' "${6:-}" | jq -Rc 'split(",") | map(gsub("^\\s+|\\s+$";"")) | map(select(length > 0))' 2>/dev/null || echo '[]')"
|
|
222
|
+
A_EXCERPT="$(printf '%s' "${7:-}" | tr '\n' ' ' | cut -c1-2000)"
|
|
223
|
+
CYPHER="
|
|
224
|
+
MERGE (a:Artifact {id: \$id})
|
|
225
|
+
ON CREATE SET a.created = datetime()
|
|
226
|
+
SET a.title = \$title, a.type = \$type, a.url = \$url,
|
|
227
|
+
a.topics = \$topics, a.excerpt = \$excerpt, a.published = true
|
|
228
|
+
WITH a
|
|
229
|
+
OPTIONAL MATCH (p:Person)
|
|
230
|
+
WHERE \$author <> '' AND (toLower(p.name) = \$author OR p.github = \$author)
|
|
231
|
+
FOREACH (_ IN CASE WHEN p IS NOT NULL THEN [1] ELSE [] END |
|
|
232
|
+
MERGE (a)-[:CONTRIBUTED_BY]->(p))
|
|
233
|
+
RETURN a.id AS id
|
|
234
|
+
"
|
|
235
|
+
PARAMS="$(jq -n --arg id "$A_ID" --arg url "$A_URL" --arg title "$A_TITLE" --arg type "$A_TYPE" \
|
|
236
|
+
--arg author "$A_AUTHOR" --argjson topics "$A_TOPICS_JSON" --arg excerpt "$A_EXCERPT" \
|
|
237
|
+
'{id: $id, url: $url, title: $title, type: $type, author: $author, topics: $topics, excerpt: $excerpt}')"
|
|
238
|
+
bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
|
|
239
|
+
bash "$GS" query "$CYPHER" "$PARAMS"
|
|
240
|
+
;;
|
|
241
|
+
|
|
205
242
|
check-implements)
|
|
206
243
|
SID="${1:?missing session-id}"
|
|
207
244
|
bash "$GS" query "
|
|
@@ -305,16 +342,30 @@ case "$OP" in
|
|
|
305
342
|
HID="${1:?missing harvest-id}"
|
|
306
343
|
HSID="${2:?missing session-id}"
|
|
307
344
|
PERSON="${3:?missing person-name}"
|
|
308
|
-
|
|
345
|
+
STATUS="${4:-active}"
|
|
346
|
+
case "$STATUS" in
|
|
347
|
+
pending|active|answered|complete|incorporated) ;;
|
|
348
|
+
*) jq -n --arg s "$STATUS" '{error:("invalid status: "+$s),valid:["pending","active","answered","complete","incorporated"]}' >&2; exit 2 ;;
|
|
349
|
+
esac
|
|
350
|
+
CYPHER="
|
|
309
351
|
MATCH (h:Harvest {id: \$hid})
|
|
310
352
|
MERGE (hs:HarvestSession {id: \$hsid})
|
|
311
|
-
ON CREATE SET hs.status =
|
|
353
|
+
ON CREATE SET hs.status = \$status, hs.created = datetime()
|
|
312
354
|
MERGE (h)-[:HAS_SESSION]->(hs)
|
|
313
355
|
WITH hs
|
|
314
356
|
MATCH (p:Person) WHERE toLower(p.name) = toLower(\$person) OR p.github = \$person
|
|
315
357
|
MERGE (hs)-[:WITH]->(p)
|
|
316
358
|
RETURN hs.id AS id, hs.status AS status
|
|
317
|
-
"
|
|
359
|
+
"
|
|
360
|
+
PARAMS="$(jq -n --arg hid "$HID" --arg hsid "$HSID" --arg person "$PERSON" --arg status "$STATUS" '{hid: $hid, hsid: $hsid, person: $person, status: $status}')"
|
|
361
|
+
bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
|
|
362
|
+
OUT="$(bash "$GS" query "$CYPHER" "$PARAMS")"
|
|
363
|
+
if [ "$(echo "$OUT" | jq -r '.results | length' 2>/dev/null)" = "0" ]; then
|
|
364
|
+
jq -n --arg hid "$HID" --arg hsid "$HSID" --arg person "$PERSON" \
|
|
365
|
+
'{error:"create-harvest-session: harvest or person not found",hid:$hid,hsid:$hsid,person:$person}' >&2
|
|
366
|
+
exit 3
|
|
367
|
+
fi
|
|
368
|
+
echo "$OUT"
|
|
318
369
|
;;
|
|
319
370
|
|
|
320
371
|
record-harvest-turn)
|
|
@@ -42,6 +42,22 @@ _millis() {
|
|
|
42
42
|
fi
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
# --- Optional read-through cache (opt-in via EGREGORE_GRAPH_CACHE_TTL) ---
|
|
46
|
+
# Read-only queries can be served from a local cache that bin/attendant.sh
|
|
47
|
+
# keeps warm in the background, so callers on the launch path (session-start
|
|
48
|
+
# greeting) pay zero network round-trips when warm. TTL in seconds; unset/0
|
|
49
|
+
# disables. Mutating queries are never cached.
|
|
50
|
+
_CACHE_TTL="${EGREGORE_GRAPH_CACHE_TTL:-0}"
|
|
51
|
+
_CACHE_DIR="$HOME/.egregore/graph-cache/$(echo -n "$SCRIPT_DIR" | cksum | cut -d' ' -f1)"
|
|
52
|
+
|
|
53
|
+
_cache_key() {
|
|
54
|
+
echo -n "$1|${2:-}" | cksum | cut -d' ' -f1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_is_read_query() {
|
|
58
|
+
! echo "$1" | grep -qiE '\b(MERGE|CREATE|SET|DELETE|DETACH|REMOVE)\b'
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
if [ -n "$API_URL" ] && [ -n "$API_KEY" ]; then
|
|
46
62
|
# === API MODE: Call Egregore API gateway ===
|
|
47
63
|
|
|
@@ -54,6 +70,19 @@ if [ -n "$API_URL" ] && [ -n "$API_KEY" ]; then
|
|
|
54
70
|
params="{}"
|
|
55
71
|
fi
|
|
56
72
|
|
|
73
|
+
local cache_file=""
|
|
74
|
+
if [ "$_CACHE_TTL" != "0" ] && _is_read_query "$cypher"; then
|
|
75
|
+
cache_file="$_CACHE_DIR/$(_cache_key "$cypher" "$params").json"
|
|
76
|
+
if [ -f "$cache_file" ]; then
|
|
77
|
+
local now cts
|
|
78
|
+
now=$(date +%s)
|
|
79
|
+
cts=$(jq -r '.ts // 0' "$cache_file" 2>/dev/null || echo 0)
|
|
80
|
+
if [ $(( now - cts )) -lt "$_CACHE_TTL" ]; then
|
|
81
|
+
jq -c '.result' "$cache_file" 2>/dev/null && return 0
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
|
|
57
86
|
local body
|
|
58
87
|
body=$(jq -n --arg stmt "$cypher" --argjson params "$params" \
|
|
59
88
|
'{statement: $stmt, parameters: $params}')
|
|
@@ -86,6 +115,13 @@ if [ -n "$API_URL" ] && [ -n "$API_KEY" ]; then
|
|
|
86
115
|
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "graph_query" \
|
|
87
116
|
"$(jq -n --argjson latency "$latency_ms" '{latency_ms: $latency}')" 2>/dev/null &
|
|
88
117
|
|
|
118
|
+
# Store for the attendant to keep warm (query+params travel with the result)
|
|
119
|
+
if [ -n "$cache_file" ]; then
|
|
120
|
+
mkdir -p "$_CACHE_DIR" 2>/dev/null
|
|
121
|
+
jq -n --arg q "$cypher" --argjson p "$params" --argjson ts "$(date +%s)" --argjson r "$response" \
|
|
122
|
+
'{query: $q, params: $p, ts: $ts, result: $r}' > "$cache_file" 2>/dev/null || true
|
|
123
|
+
fi
|
|
124
|
+
|
|
89
125
|
echo "$response"
|
|
90
126
|
}
|
|
91
127
|
|
|
@@ -48,6 +48,7 @@ PROJECT=""
|
|
|
48
48
|
NO_PUSH=0
|
|
49
49
|
NO_NOTIFY=0
|
|
50
50
|
NO_PUBLISH=0
|
|
51
|
+
COMPOSED="" # optional house-kit JSON — the agent-composed render spec
|
|
51
52
|
|
|
52
53
|
while [ $# -gt 0 ]; do
|
|
53
54
|
case "$1" in
|
|
@@ -55,6 +56,7 @@ while [ $# -gt 0 ]; do
|
|
|
55
56
|
--topic) TOPIC="${2:?}"; shift 2 ;;
|
|
56
57
|
--recipient) RECIPIENT="${2:-}"; shift 2 ;;
|
|
57
58
|
--project) PROJECT="${2:-}"; shift 2 ;;
|
|
59
|
+
--composed) COMPOSED="${2:-}"; shift 2 ;;
|
|
58
60
|
--no-push) NO_PUSH=1; shift ;;
|
|
59
61
|
--no-notify) NO_NOTIFY=1; shift ;;
|
|
60
62
|
--no-publish) NO_PUBLISH=1; shift ;;
|
|
@@ -62,6 +64,16 @@ while [ $# -gt 0 ]; do
|
|
|
62
64
|
esac
|
|
63
65
|
done
|
|
64
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
|
+
|
|
65
77
|
[ -z "$AUTHOR" ] && { echo "Missing --author" >&2; exit 1; }
|
|
66
78
|
[ -z "$TOPIC" ] && { echo "Missing --topic" >&2; exit 1; }
|
|
67
79
|
|
|
@@ -203,14 +215,13 @@ PID_GRAPH=$!
|
|
|
203
215
|
ARTIFACTS_JSON="[]"
|
|
204
216
|
if [ "$MODE" = "connected" ]; then
|
|
205
217
|
YY=$(date +%Y); MM=$(date +%-m); DD_NUM=$(date +%-d)
|
|
206
|
-
PARAMS=$(jq -nc --arg gh "$AUTHOR" '{gh:$gh}')
|
|
207
218
|
RAW=$(bash "$SCRIPT_DIR/bin/graph.sh" query "
|
|
208
219
|
MATCH (a:Artifact)-[:CONTRIBUTED_BY]->(p:Person {github: \$gh})
|
|
209
220
|
WHERE a.created >= datetime({year: $YY, month: $MM, day: $DD_NUM})
|
|
210
221
|
AND NOT 'tutorial-generated' IN coalesce(a.topics, [])
|
|
211
222
|
RETURN a.title AS title, a.type AS type, a.filePath AS path
|
|
212
223
|
ORDER BY a.created DESC
|
|
213
|
-
LIMIT 5" "$
|
|
224
|
+
LIMIT 5" "$(jq -nc --arg gh "$AUTHOR" '{gh:$gh}')" 2>/dev/null || echo '{}')
|
|
214
225
|
# Response shape: {"fields":[...], "values":[[...], ...]}. Convert to
|
|
215
226
|
# an array of {title, type, path} objects.
|
|
216
227
|
ARTIFACTS_JSON=$(echo "$RAW" | jq -c '
|
|
@@ -282,24 +293,145 @@ PID_ARTIFACTS=$!
|
|
|
282
293
|
PID_MEMORY=$!
|
|
283
294
|
|
|
284
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.
|
|
285
310
|
(
|
|
311
|
+
V1URL=""
|
|
286
312
|
AURL=""
|
|
287
|
-
if [ "$NO_PUBLISH" = "0" ]
|
|
288
|
-
|
|
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}" \
|
|
289
411
|
--title "$TOPIC" \
|
|
290
412
|
--author "$AUTHOR" \
|
|
291
413
|
--description "$BRIEFING_LEAD" 2>/dev/null || echo "")
|
|
292
414
|
fi
|
|
293
|
-
|
|
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
|
|
294
426
|
|
|
295
427
|
NS="skipped"
|
|
296
428
|
if [ "$NO_NOTIFY" = "0" ]; then
|
|
297
|
-
if [ -n "$
|
|
429
|
+
if [ -n "$NOTIFY_URL" ]; then
|
|
298
430
|
MSG_NOTIFY="Handoff from ${AUTHOR}: ${TOPIC}
|
|
299
431
|
|
|
300
432
|
\"${BRIEFING_SHORT}\"
|
|
301
433
|
|
|
302
|
-
View: ${
|
|
434
|
+
View: ${NOTIFY_URL}"
|
|
303
435
|
else
|
|
304
436
|
MSG_NOTIFY="Handoff from ${AUTHOR}: ${TOPIC}
|
|
305
437
|
|
|
@@ -345,9 +477,27 @@ if [ -s "$TMPD/graph" ]; then
|
|
|
345
477
|
GRAPH_STATUS=$(sed -n '3p' "$TMPD/graph")
|
|
346
478
|
fi
|
|
347
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
|
|
348
486
|
|
|
349
487
|
SUBGRAPH_JSON=$(cat "$TMPD/subgraph" 2>/dev/null || echo "null")
|
|
350
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
|
|
351
501
|
|
|
352
502
|
MEMORY_STATUS=$(cat "$TMPD/memory" 2>/dev/null || echo "skipped")
|
|
353
503
|
ARTIFACT_URL=$(cat "$TMPD/artifact" 2>/dev/null || echo "")
|
|
@@ -45,6 +45,11 @@ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
45
45
|
[ -z "$REPO_DIR" ] && REPO_DIR="$SCRIPT_DIR"
|
|
46
46
|
cd "$REPO_DIR" || exit 1
|
|
47
47
|
|
|
48
|
+
# Shared safe-staging guard — never auto-commit stray sibling-repo gitlinks.
|
|
49
|
+
# shellcheck source=bin/lib/git-safe.sh
|
|
50
|
+
. "$SCRIPT_DIR/bin/lib/git-safe.sh" 2>/dev/null || true
|
|
51
|
+
type git_add_guarded >/dev/null 2>&1 || git_add_guarded() { git add -A 2>/dev/null || true; }
|
|
52
|
+
|
|
48
53
|
# --- Early exit if nothing to do -----------------------------------------
|
|
49
54
|
DIRTY=0
|
|
50
55
|
[ -n "$(git status --porcelain 2>/dev/null)" ] && DIRTY=1
|
|
@@ -75,7 +80,7 @@ esac
|
|
|
75
80
|
|
|
76
81
|
# --- Commit whatever is uncommitted --------------------------------------
|
|
77
82
|
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
78
|
-
|
|
83
|
+
git_add_guarded
|
|
79
84
|
git commit -m "Handoff: ${TOPIC}" --quiet 2>/dev/null || true
|
|
80
85
|
fi
|
|
81
86
|
|
|
@@ -131,6 +131,10 @@ fi
|
|
|
131
131
|
PROJECT="$(md_field 'Project')"
|
|
132
132
|
[ -z "$PROJECT" ] && PROJECT="$(yaml_extract 'project')"
|
|
133
133
|
|
|
134
|
+
# Handoff kind: used by readers to distinguish automatic captures from manual handoffs.
|
|
135
|
+
KIND="$(md_field 'Kind')"
|
|
136
|
+
[ -z "$KIND" ] && KIND="$(yaml_extract 'kind')"
|
|
137
|
+
|
|
134
138
|
# Summary: 5-level precedence chain
|
|
135
139
|
# 1. Frontmatter summary field
|
|
136
140
|
SUMMARY="$(yaml_extract 'summary')"
|
|
@@ -198,8 +202,9 @@ fi
|
|
|
198
202
|
|
|
199
203
|
Q1_CYPHER="MATCH (p:Person) WHERE toLower(p.name) = \$author OR p.github = \$author OR toLower(p.fullName) = \$author OR \$author IN [x IN coalesce(p.previousNames, []) | toLower(x)]
|
|
200
204
|
MERGE (s:Session {id: \$sessionId})
|
|
201
|
-
ON CREATE SET s.date = date(\$date), s.topic = \$topic, s.summary = \$summary, s.filePath = \$filePath, s.handoffStatus = 'pending', s.repoState = \$repoState, s.startedAt = datetime(\$date + 'T00:00:00Z')
|
|
202
|
-
ON MATCH SET s.topic = \$topic, s.summary = \$summary, s.filePath = \$filePath, s.repoState = \$repoState
|
|
205
|
+
ON CREATE SET s.date = date(\$date), s.topic = \$topic, s.summary = \$summary, s.filePath = \$filePath, s.author = \$author, s.handoffKind = CASE WHEN \$kind = '' THEN null ELSE \$kind END, s.handoffStatus = 'pending', s.repoState = \$repoState, s.startedAt = datetime(\$date + 'T00:00:00Z')
|
|
206
|
+
ON MATCH SET s.topic = \$topic, s.summary = \$summary, s.filePath = \$filePath, s.author = \$author, s.handoffKind = CASE WHEN \$kind = '' THEN null ELSE \$kind END, s.repoState = \$repoState
|
|
207
|
+
WITH s, p
|
|
203
208
|
MERGE (s)-[:BY]->(p)${PROJECT_CYPHER}${HANDED_TO_CYPHER}
|
|
204
209
|
RETURN s.id AS sessionId"
|
|
205
210
|
|
|
@@ -210,6 +215,7 @@ Q1_PARAMS=$(jq -n \
|
|
|
210
215
|
--arg topic "$TOPIC" \
|
|
211
216
|
--arg summary "$SUMMARY" \
|
|
212
217
|
--arg filePath "$REL_PATH" \
|
|
218
|
+
--arg kind "$KIND" \
|
|
213
219
|
--arg repoState "$REPO_STATE_JSON" \
|
|
214
220
|
"${_PROJECT_ARGS[@]:+${_PROJECT_ARGS[@]}}" \
|
|
215
221
|
"${_RECIP_ARGS[@]:+${_RECIP_ARGS[@]}}" \
|
|
@@ -273,7 +279,7 @@ CURRENT_SID=$(cat "$HOME/.egregore/session-${PROJ_HASH}.id" 2>/dev/null || echo
|
|
|
273
279
|
if [ -n "$CURRENT_SID" ]; then
|
|
274
280
|
bash "$SCRIPT_DIR/bin/graph.sh" query \
|
|
275
281
|
"MATCH (s:Session {id: \$sid}) SET s.status = 'handed_off' RETURN s.id" \
|
|
276
|
-
"$(jq -n --arg sid "$CURRENT_SID" '{sid: $sid}')"
|
|
282
|
+
"$(jq -n --arg sid "$CURRENT_SID" '{sid: $sid}')" >/dev/null 2>&1 || true
|
|
277
283
|
fi
|
|
278
284
|
|
|
279
285
|
# --- Parse response ---
|