create-egregore 0.19.3 → 0.19.6

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 (154) hide show
  1. package/assets/egregore-launcher.js +410 -26
  2. package/bin/cli.js +34 -20
  3. package/lib/local.js +84 -11
  4. package/lib/pi-runtime.js +186 -0
  5. package/lib/setup.js +56 -11
  6. package/lib/ui.js +1 -1
  7. package/package.json +8 -6
  8. package/runtime/codex/.codex/hooks/search-hint.js +65 -0
  9. package/runtime/codex/.codex/hooks.json +13 -0
  10. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +14 -10
  11. package/runtime/codex/.codex/skills/minion/SKILL.md +25 -0
  12. package/runtime/codex/.codex/skills/scroll/SKILL.md +168 -0
  13. package/runtime/codex/.codex/skills/scroll/agents/openai.yaml +4 -0
  14. package/runtime/codex/.codex/skills/scroll/assets/shell.html +542 -0
  15. package/runtime/codex/.codex/skills/search/SKILL.md +56 -0
  16. package/runtime/codex/.codex/skills/session-view/SKILL.md +25 -0
  17. package/runtime/codex/.codex/skills/thread/SKILL.md +25 -0
  18. package/runtime/codex/.codex/skills/view/SKILL.md +147 -32
  19. package/runtime/codex/.codex/skills/view/agents/openai.yaml +4 -0
  20. package/runtime/codex/.codex/skills/view/assets/compose-scaffold.html +221 -0
  21. package/runtime/codex/.codex/spec-manifest.json +4 -4
  22. package/runtime/codex/AGENTS.md +31 -5
  23. package/runtime/codex/bin/activity-data.sh +46 -4
  24. package/runtime/codex/bin/agent.sh +10 -2
  25. package/runtime/codex/bin/codex-install-skills.sh +1 -1
  26. package/runtime/codex/bin/codex-render-spec.mjs +24 -2
  27. package/runtime/codex/bin/codex-session-start.sh +6 -60
  28. package/runtime/codex/bin/codex-skill-render.mjs +20 -12
  29. package/runtime/codex/bin/codex-sync-skills.sh +4 -0
  30. package/runtime/codex/bin/graph-op.sh +56 -5
  31. package/runtime/codex/bin/graph.sh +36 -0
  32. package/runtime/codex/bin/handoff-run.sh +157 -7
  33. package/runtime/codex/bin/handoff-save-egregore.sh +6 -1
  34. package/runtime/codex/bin/index-handoff.sh +9 -3
  35. package/runtime/codex/bin/lib/context.sh +97 -18
  36. package/runtime/codex/bin/lib/dashboard-artifact.sh +1 -1
  37. package/runtime/codex/bin/lib/git-sync.sh +35 -9
  38. package/runtime/codex/bin/lib/greeting.sh +30 -104
  39. package/runtime/codex/bin/lib/metrics.sh +374 -0
  40. package/runtime/codex/bin/publish-artifact.sh +43 -1
  41. package/runtime/codex/bin/settings.sh +274 -0
  42. package/runtime/codex/manifest.json +18 -1
  43. package/runtime/pi/.codex/config.toml +15 -0
  44. package/runtime/pi/.codex/hooks/branch-guard.js +242 -0
  45. package/runtime/pi/.codex/hooks/search-hint.js +65 -0
  46. package/runtime/pi/.codex/hooks.json +30 -0
  47. package/runtime/pi/.codex/skills/activity/SKILL.md +70 -0
  48. package/runtime/pi/.codex/skills/add/SKILL.md +25 -0
  49. package/runtime/pi/.codex/skills/announce/SKILL.md +54 -0
  50. package/runtime/pi/.codex/skills/answer/SKILL.md +25 -0
  51. package/runtime/pi/.codex/skills/archive/SKILL.md +41 -0
  52. package/runtime/pi/.codex/skills/ask/SKILL.md +59 -0
  53. package/runtime/pi/.codex/skills/branch/SKILL.md +25 -0
  54. package/runtime/pi/.codex/skills/checkup/SKILL.md +41 -0
  55. package/runtime/pi/.codex/skills/commit/SKILL.md +25 -0
  56. package/runtime/pi/.codex/skills/contribute/SKILL.md +25 -0
  57. package/runtime/pi/.codex/skills/dark-mode/SKILL.md +25 -0
  58. package/runtime/pi/.codex/skills/dashboard/SKILL.md +59 -0
  59. package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +74 -0
  60. package/runtime/pi/.codex/skills/delete-user/SKILL.md +25 -0
  61. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +25 -0
  62. package/runtime/pi/.codex/skills/emissary/SKILL.md +41 -0
  63. package/runtime/pi/.codex/skills/env/SKILL.md +25 -0
  64. package/runtime/pi/.codex/skills/handoff/SKILL.md +67 -0
  65. package/runtime/pi/.codex/skills/harvest/SKILL.md +107 -0
  66. package/runtime/pi/.codex/skills/ingest/SKILL.md +25 -0
  67. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +25 -0
  68. package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  69. package/runtime/pi/.codex/skills/invite/SKILL.md +40 -0
  70. package/runtime/pi/.codex/skills/issue/SKILL.md +41 -0
  71. package/runtime/pi/.codex/skills/me/SKILL.md +25 -0
  72. package/runtime/pi/.codex/skills/meeting/SKILL.md +41 -0
  73. package/runtime/pi/.codex/skills/minion/SKILL.md +25 -0
  74. package/runtime/pi/.codex/skills/note/SKILL.md +25 -0
  75. package/runtime/pi/.codex/skills/onboarding/SKILL.md +41 -0
  76. package/runtime/pi/.codex/skills/pr/SKILL.md +25 -0
  77. package/runtime/pi/.codex/skills/project/SKILL.md +41 -0
  78. package/runtime/pi/.codex/skills/pull/SKILL.md +25 -0
  79. package/runtime/pi/.codex/skills/push/SKILL.md +25 -0
  80. package/runtime/pi/.codex/skills/quest/SKILL.md +77 -0
  81. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +25 -0
  82. package/runtime/pi/.codex/skills/reflect/SKILL.md +41 -0
  83. package/runtime/pi/.codex/skills/review-pr/SKILL.md +41 -0
  84. package/runtime/pi/.codex/skills/save/SKILL.md +62 -0
  85. package/runtime/pi/.codex/skills/scroll/SKILL.md +168 -0
  86. package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +4 -0
  87. package/runtime/pi/.codex/skills/scroll/assets/shell.html +542 -0
  88. package/runtime/pi/.codex/skills/search/SKILL.md +56 -0
  89. package/runtime/pi/.codex/skills/session-view/SKILL.md +25 -0
  90. package/runtime/pi/.codex/skills/setup/SKILL.md +25 -0
  91. package/runtime/pi/.codex/skills/summon/SKILL.md +41 -0
  92. package/runtime/pi/.codex/skills/sync-repos/SKILL.md +25 -0
  93. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +25 -0
  94. package/runtime/pi/.codex/skills/telemetry/SKILL.md +25 -0
  95. package/runtime/pi/.codex/skills/test/SKILL.md +41 -0
  96. package/runtime/pi/.codex/skills/the-spiral/SKILL.md +84 -0
  97. package/runtime/pi/.codex/skills/thread/SKILL.md +25 -0
  98. package/runtime/pi/.codex/skills/todo/SKILL.md +41 -0
  99. package/runtime/pi/.codex/skills/tui-design/SKILL.md +41 -0
  100. package/runtime/pi/.codex/skills/tutorial/SKILL.md +41 -0
  101. package/runtime/pi/.codex/skills/update/SKILL.md +25 -0
  102. package/runtime/pi/.codex/skills/view/SKILL.md +156 -0
  103. package/runtime/pi/.codex/skills/view/agents/openai.yaml +4 -0
  104. package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +221 -0
  105. package/runtime/pi/.codex/skills/visual-explain/SKILL.md +41 -0
  106. package/runtime/pi/.codex/skills/wrap/SKILL.md +55 -0
  107. package/runtime/pi/.codex/spec-manifest.json +114 -0
  108. package/runtime/pi/.pi/APPEND_SYSTEM.md +255 -0
  109. package/runtime/pi/.pi/extensions/egregore.ts +324 -0
  110. package/runtime/pi/.pi/settings.json +6 -0
  111. package/runtime/pi/.pi/spec-manifest.json +12 -0
  112. package/runtime/pi/AGENTS.md +350 -0
  113. package/runtime/pi/bin/activity-data.sh +242 -0
  114. package/runtime/pi/bin/agent.sh +677 -0
  115. package/runtime/pi/bin/codex-install-skills.sh +78 -0
  116. package/runtime/pi/bin/codex-render-spec.mjs +394 -0
  117. package/runtime/pi/bin/codex-session-start.sh +188 -0
  118. package/runtime/pi/bin/codex-skill-render.mjs +323 -0
  119. package/runtime/pi/bin/codex-sync-skills.sh +227 -0
  120. package/runtime/pi/bin/dashboard-data.sh +178 -0
  121. package/runtime/pi/bin/graph-batch.sh +60 -0
  122. package/runtime/pi/bin/graph-op.sh +431 -0
  123. package/runtime/pi/bin/graph-wal.sh +228 -0
  124. package/runtime/pi/bin/graph.sh +177 -0
  125. package/runtime/pi/bin/handoff-pr-backfill.sh +86 -0
  126. package/runtime/pi/bin/handoff-run.sh +581 -0
  127. package/runtime/pi/bin/handoff-save-egregore.sh +136 -0
  128. package/runtime/pi/bin/index-handoff.sh +312 -0
  129. package/runtime/pi/bin/lib/artifact-id.sh +46 -0
  130. package/runtime/pi/bin/lib/config.sh +71 -0
  131. package/runtime/pi/bin/lib/context.sh +542 -0
  132. package/runtime/pi/bin/lib/dashboard-artifact.sh +142 -0
  133. package/runtime/pi/bin/lib/ensure-develop.sh +183 -0
  134. package/runtime/pi/bin/lib/git-sync.sh +288 -0
  135. package/runtime/pi/bin/lib/greeting.sh +340 -0
  136. package/runtime/pi/bin/lib/hash.sh +13 -0
  137. package/runtime/pi/bin/lib/identity.sh +174 -0
  138. package/runtime/pi/bin/lib/metrics.sh +374 -0
  139. package/runtime/pi/bin/lib/permissions.sh +106 -0
  140. package/runtime/pi/bin/lib/time.sh +56 -0
  141. package/runtime/pi/bin/lib/worktree-links.sh +54 -0
  142. package/runtime/pi/bin/notify.sh +207 -0
  143. package/runtime/pi/bin/pi-branch-policy.mjs +15 -0
  144. package/runtime/pi/bin/pi-product-workflows.mjs +324 -0
  145. package/runtime/pi/bin/pi-render-spec.mjs +114 -0
  146. package/runtime/pi/bin/pi-session-start.sh +24 -0
  147. package/runtime/pi/bin/publish-artifact.sh +201 -0
  148. package/runtime/pi/bin/publish-references.sh +144 -0
  149. package/runtime/pi/bin/repo-state.sh +99 -0
  150. package/runtime/pi/bin/telemetry.sh +391 -0
  151. package/runtime/pi/bin/worktree-create.sh +118 -0
  152. package/runtime/pi/bin/worktree-remove.sh +50 -0
  153. package/runtime/pi/bin/worktree.sh +198 -0
  154. package/runtime/pi/manifest.json +176 -0
@@ -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" "$PARAMS" 2>/dev/null || echo '{}')
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" ] && [ -x "$SCRIPT_DIR/bin/publish-artifact.sh" ]; then
288
- AURL=$(bash "$SCRIPT_DIR/bin/publish-artifact.sh" handoff "$ABS_FILE" \
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
- echo "$AURL" > "$TMPD/artifact"
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 "$AURL" ]; then
429
+ if [ -n "$NOTIFY_URL" ]; then
298
430
  MSG_NOTIFY="Handoff from ${AUTHOR}: ${TOPIC}
299
431
 
300
432
  \"${BRIEFING_SHORT}\"
301
433
 
302
- View: ${AURL}"
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
- git add -A 2>/dev/null || true
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}')" 2>/dev/null || true
282
+ "$(jq -n --arg sid "$CURRENT_SID" '{sid: $sid}')" >/dev/null 2>&1 || true
277
283
  fi
278
284
 
279
285
  # --- Parse response ---
@@ -1,7 +1,7 @@
1
1
  # shellcheck shell=bash
2
2
  # context.sh — Context gathering for session-start.sh
3
3
  #
4
- # Runs 10 parallel background subshells to gather session context:
4
+ # Runs 11 parallel background subshells to gather session context:
5
5
  # 1. Recent handoffs
6
6
  # 2. Active quests
7
7
  # 3. User's last git activity
@@ -13,6 +13,7 @@
13
13
  # 8. Telegram health check
14
14
  # 9. Lifecycle events (merged PRs, implemented handoffs)
15
15
  # 10. Last Pulse brief
16
+ # 11. Momentum metrics
16
17
  #
17
18
  # All subshells write to $CTX_DIR (a temp directory).
18
19
  # Ends with `wait` + reading health results back.
@@ -23,6 +24,8 @@
23
24
 
24
25
  CTX_DIR=$(mktemp -d)
25
26
 
27
+ source "$SCRIPT_DIR/bin/lib/handoff-meta.sh" >/dev/null 2>/dev/null || true
28
+
26
29
  # Time of day
27
30
  HOUR=$(date +%H)
28
31
  if [ "$HOUR" -lt 12 ]; then TIME_OF_DAY="morning"
@@ -177,6 +180,31 @@ fi
177
180
  )
178
181
  fi
179
182
 
183
+ # --- "Working on": latest session/handoff/wrap topic per person (file-based) ---
184
+ # Gives every team surface (greeting "around", dashboard) something legible in
185
+ # local mode, where git branches are usually empty for teammates.
186
+ _WM_TAB=$(printf '\t')
187
+ WORK_MAP=$(
188
+ {
189
+ for DIR in "$SCRIPT_DIR/memory/sessions" "$SCRIPT_DIR/memory/handoffs" "$SCRIPT_DIR/memory/wraps"; do
190
+ [ -d "$DIR" ] || continue
191
+ find -L "$DIR" -name '*.md' -not -name 'index*' -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -30 | while read -r WFILE; do
192
+ [ -z "$WFILE" ] && continue
193
+ W_AUTH=$(grep -m1 '^\*\*Author\*\*:' "$WFILE" 2>/dev/null | sed 's/^\*\*Author\*\*:[[:space:]]*//')
194
+ W_DATE=$(grep -m1 '^\*\*Date\*\*:' "$WFILE" 2>/dev/null | sed 's/^\*\*Date\*\*:[[:space:]]*//')
195
+ W_TOPIC=$(grep -m1 '^# ' "$WFILE" 2>/dev/null | sed 's/^#[[:space:]]*//; s/^Session:[[:space:]]*//; s/^Handoff:[[:space:]]*//; s/^Wrap:[[:space:]]*//')
196
+ [ -z "$W_AUTH" ] && continue
197
+ [ -z "$W_TOPIC" ] && continue
198
+ printf '%s%s%s%s%s\n' "$W_AUTH" "$_WM_TAB" "$W_DATE" "$_WM_TAB" "$W_TOPIC"
199
+ done
200
+ done
201
+ } | sort -t"$_WM_TAB" -k1,1 -k2,2r | sort -t"$_WM_TAB" -k1,1 -u | while IFS="$_WM_TAB" read -r WM_NAME WM_DATE WM_TOPIC; do
202
+ [ -z "$WM_NAME" ] && continue
203
+ WM_LC=$(echo "$WM_NAME" | tr '[:upper:]' '[:lower:]')
204
+ printf '%s%s%s\n' "$WM_LC" "$_WM_TAB" "$WM_TOPIC"
205
+ done | jq -Rn --arg tab "$_WM_TAB" '[inputs | split($tab) | select(length>=2) | {(.[0]): (.[1] // "")}] | add // {}' 2>/dev/null || echo "{}"
206
+ )
207
+
180
208
  # --- Relative time + epoch helpers (cross-platform) ---
181
209
  NOW_EPOCH=$(date +%s)
182
210
 
@@ -270,8 +298,9 @@ fi
270
298
  BRANCHES_JSON=$(echo "$BRANCHES_JSON" | jq --arg e "+${EXTRA} more" '[.[0:2][], $e]' 2>/dev/null || echo "$BRANCHES_JSON")
271
299
  fi
272
300
 
273
- ENTRY=$(jq -n --arg name "$PNAME" --arg seen "$LAST_SEEN_REL" --argjson sort "$LAST_SEEN_EPOCH" --argjson branches "$BRANCHES_JSON" \
274
- '{name: $name, last_seen: $seen, last_seen_sort: $sort, branches: $branches}')
301
+ WORK=$(echo "$WORK_MAP" | jq -r --arg n "$PNAME" '.[$n] // ""' 2>/dev/null)
302
+ ENTRY=$(jq -n --arg name "$PNAME" --arg seen "$LAST_SEEN_REL" --argjson sort "$LAST_SEEN_EPOCH" --argjson branches "$BRANCHES_JSON" --arg work "$WORK" \
303
+ '{name: $name, last_seen: $seen, last_seen_sort: $sort, branches: $branches, working_on: $work}')
275
304
  $FIRST || PRESENCE="$PRESENCE,"
276
305
  PRESENCE="$PRESENCE$ENTRY"
277
306
  FIRST=false
@@ -320,18 +349,9 @@ fi
320
349
  for AF in $ADDRESSED; do
321
350
  [ -z "$AF" ] && continue
322
351
  AF_NAME=$(basename "$AF" .md)
323
- AF_AUTHOR=$(sed -n 's/^\*\*Author\*\*: *//p' "$AF" 2>/dev/null | head -1)
324
- if [ -z "$AF_AUTHOR" ]; then
325
- AF_AUTHOR=$(sed -n 's/^From: *//p' "$AF" 2>/dev/null | head -1)
326
- fi
327
- AF_TOPIC=$(sed -n 's/^# Handoff: *//p' "$AF" 2>/dev/null | head -1)
328
- if [ -z "$AF_TOPIC" ]; then
329
- AF_TOPIC=$(sed -n 's/^# *//p' "$AF" 2>/dev/null | head -1)
330
- fi
331
- AF_DATE=$(sed -n 's/^\*\*Date\*\*: *//p' "$AF" 2>/dev/null | head -1)
332
- if [ -z "$AF_DATE" ]; then
333
- AF_DATE=$(sed -n 's/^Date: *//p' "$AF" 2>/dev/null | head -1)
334
- fi
352
+ AF_AUTHOR=$(eg_handoff_author "$AF")
353
+ AF_TOPIC=$(eg_handoff_topic "$AF")
354
+ AF_DATE=$(eg_handoff_date "$AF")
335
355
  # Extract repo state table if present
336
356
  AF_REPO_STATE="[]"
337
357
  AF_REPO_TABLE=$(awk '/^## Repo State/{found=1; next} found && /^#/{exit} found && /^\|[^-]/ && !/^\| Repo/{print}' "$AF" 2>/dev/null || true)
@@ -352,9 +372,14 @@ fi
352
372
  $FIRST || JSON="$JSON,"
353
373
  $FIRST || RICH="$RICH,"
354
374
  JSON="$JSON\"$AF_NAME\""
355
- AF_TOPIC_ESC=$(echo "$AF_TOPIC" | sed 's/"/\\"/g')
356
- AF_AUTHOR_ESC=$(echo "$AF_AUTHOR" | sed 's/"/\\"/g')
357
- RICH="$RICH{\"name\":\"$AF_NAME\",\"author\":\"$AF_AUTHOR_ESC\",\"topic\":\"$AF_TOPIC_ESC\",\"date\":\"$AF_DATE\",\"repoState\":$AF_REPO_STATE}"
375
+ AF_ENTRY=$(jq -cn \
376
+ --arg name "$AF_NAME" \
377
+ --arg author "$AF_AUTHOR" \
378
+ --arg topic "$AF_TOPIC" \
379
+ --arg date "$AF_DATE" \
380
+ --argjson repoState "$AF_REPO_STATE" \
381
+ '{name:$name, author:$author, topic:$topic, date:$date, repoState:$repoState}' 2>/dev/null || echo "{}")
382
+ RICH="$RICH$AF_ENTRY"
358
383
  FIRST=false
359
384
  done
360
385
  JSON="$JSON]"
@@ -364,6 +389,54 @@ fi
364
389
  echo "$RICH" > "$CTX_DIR/addressed_rich"
365
390
  ) &
366
391
 
392
+ # 6.5. Pending questions addressed to user (background)
393
+ (
394
+ PENDING_JSON="[]"
395
+ QDIR="$SCRIPT_DIR/memory/knowledge/questions"
396
+ if [ -d "$QDIR" ]; then
397
+ # Match against the same identity variants used for handoffs (display name,
398
+ # github first-name, people-file name) so "to: oz" matches author "oguzhan".
399
+ _DISPLAY_Q=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
400
+ _GH_NAME_Q=$(jq -r '.github_name // empty' "$STATE_FILE" 2>/dev/null)
401
+ _FIRST_NAME_Q=$(echo "$_GH_NAME_Q" | awk '{print tolower($1)}')
402
+ _PEOPLE_FILE_Q="$SCRIPT_DIR/memory/people/$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]').md"
403
+ _PEOPLE_NAME_Q=""
404
+ [ -f "$_PEOPLE_FILE_Q" ] && _PEOPLE_NAME_Q=$(sed -n 's/^# *//p' "$_PEOPLE_FILE_Q" 2>/dev/null | head -1 | awk '{print tolower($1)}')
405
+
406
+ LINES=""
407
+ for QF in "$QDIR"/*.md; do
408
+ [ -f "$QF" ] || continue
409
+ Q_TO=$(sed -n 's/^to:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"' | tr '[:upper:]' '[:lower:]' | awk '{print $1}')
410
+ [ -z "$Q_TO" ] && continue
411
+ _MATCH="false"
412
+ for _V in "$AUTHOR" "$_DISPLAY_Q" "$_FIRST_NAME_Q" "$_PEOPLE_NAME_Q"; do
413
+ [ -z "$_V" ] && continue
414
+ _V_LC=$(echo "$_V" | tr '[:upper:]' '[:lower:]')
415
+ [ "$Q_TO" = "$_V_LC" ] && _MATCH="true" && break
416
+ done
417
+ [ "$_MATCH" = "true" ] || continue
418
+ Q_STATUS=$(sed -n 's/^status:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"')
419
+ [ "$Q_STATUS" = "pending" ] || continue
420
+ Q_FROM=$(sed -n 's/^from:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"')
421
+ Q_TOPIC=$(sed -n 's/^topic:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"')
422
+ Q_HID=$(sed -n 's/^harvest_id:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"')
423
+ Q_REL="${QF#$SCRIPT_DIR/}"
424
+ LINE=$(jq -nc \
425
+ --arg from "$Q_FROM" \
426
+ --arg topic "$Q_TOPIC" \
427
+ --arg hid "$Q_HID" \
428
+ --arg file "$Q_REL" \
429
+ '{from:$from, topic:$topic, harvest_id:$hid, file:$file}' 2>/dev/null)
430
+ [ -n "$LINE" ] && LINES="$LINES$LINE
431
+ "
432
+ done
433
+ if [ -n "$LINES" ]; then
434
+ PENDING_JSON=$(echo "$LINES" | jq -sc '.' 2>/dev/null || echo "[]")
435
+ fi
436
+ fi
437
+ echo "$PENDING_JSON" > "$CTX_DIR/pending_questions"
438
+ ) &
439
+
367
440
  # 7. Graph health (background — zero added latency, runs in parallel)
368
441
  (
369
442
  if [ "$LOCAL_MODE" = "true" ]; then
@@ -453,6 +526,12 @@ fi
453
526
  fi
454
527
  ) > "$CTX_DIR/pulse_brief" 2>/dev/null &
455
528
 
529
+ # 11. Momentum metrics (background)
530
+ (
531
+ source "$SCRIPT_DIR/bin/lib/metrics.sh"
532
+ _metrics_gather
533
+ ) > "$CTX_DIR/metrics" 2>/dev/null &
534
+
456
535
  # Wait for all context gathering + health checks to finish
457
536
  wait
458
537
 
@@ -90,7 +90,7 @@ mkdir -p /tmp
90
90
  printf '## Team Activity\n\n'
91
91
  printf '| Name | Last Seen | Working On |\n'
92
92
  printf '|------|-----------|------------|\n'
93
- echo "$_DB_TEAM_FILTERED" | jq -r '.[] | "| \(.name) | \(.last_seen) | \(.branches | join(", ")) |"' 2>/dev/null || true
93
+ echo "$_DB_TEAM_FILTERED" | jq -r '.[] | ((.working_on // "")) as $w | (.branches // []) as $b | "| \(.name) | \(.last_seen) | \(if $w != "" then $w else ($b | join(", ")) end) |"' 2>/dev/null || true
94
94
  printf '\n'
95
95
  fi
96
96
  fi
@@ -26,7 +26,20 @@ git config pull.rebase true 2>/dev/null || true
26
26
  export GIT_HTTP_LOW_SPEED_LIMIT=1000 # abort if <1KB/s
27
27
  export GIT_HTTP_LOW_SPEED_TIME=10 # for 10 seconds
28
28
 
29
- git fetch origin --quiet 2>/dev/null &
29
+ # Attendant fast path: the ambient daemon (bin/attendant.sh) fetches origin,
30
+ # memory, and managed repos every few minutes. If its warm marker is fresh,
31
+ # refs are already up to date — skip the network fetches and launch on local
32
+ # ops only. Upstream fetch (framework update check) is not covered by the
33
+ # attendant and stays unconditional below.
34
+ _REFS_FRESH="false"
35
+ _ATT_SLUG=$(jq -r '.slug // "egregore"' "$SCRIPT_DIR/egregore.json" 2>/dev/null || echo "egregore")
36
+ _ATT_MARKER="$HOME/.egregore/attendant/${_ATT_SLUG}-$(echo -n "$SCRIPT_DIR" | cksum | cut -d' ' -f1).warm-ts"
37
+ if [ -f "$_ATT_MARKER" ]; then
38
+ _ATT_AGE=$(( $(date +%s) - $(cat "$_ATT_MARKER" 2>/dev/null || echo 0) ))
39
+ [ "$_ATT_AGE" -lt 360 ] && _REFS_FRESH="true"
40
+ fi
41
+
42
+ [ "$_REFS_FRESH" != "true" ] && git fetch origin --quiet 2>/dev/null &
30
43
 
31
44
  # Fetch upstream framework (for update check — non-blocking)
32
45
  # Default: official egregore-core repo. Set upstream_url in egregore.json to override,
@@ -40,17 +53,19 @@ fi
40
53
 
41
54
  # Sync memory in parallel (symlink or direct directory — both valid)
42
55
  MEMORY_SYNCED="false"
43
- if [ -d "$SCRIPT_DIR/memory/.git" ]; then
56
+ if [ -d "$SCRIPT_DIR/memory/.git" ] && [ "$_REFS_FRESH" != "true" ]; then
44
57
  git -C "$SCRIPT_DIR/memory" fetch origin --quiet 2>/dev/null &
45
58
  fi
46
59
 
47
60
  # Fetch managed repos in parallel
48
61
  MANAGED_REPOS=$(jq -r '(.repos[]? // empty) | if type == "object" then .name else . end' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
49
- for REPO in $MANAGED_REPOS; do
50
- if [ -d "$SCRIPT_DIR/../$REPO/.git" ]; then
51
- git -C "$SCRIPT_DIR/../$REPO" fetch origin --quiet 2>/dev/null &
52
- fi
53
- done
62
+ if [ "$_REFS_FRESH" != "true" ]; then
63
+ for REPO in $MANAGED_REPOS; do
64
+ if [ -d "$SCRIPT_DIR/../$REPO/.git" ]; then
65
+ git -C "$SCRIPT_DIR/../$REPO" fetch origin --quiet 2>/dev/null &
66
+ fi
67
+ done
68
+ fi
54
69
 
55
70
  # Wait for all fetches
56
71
  wait 2>/dev/null || true
@@ -93,6 +108,16 @@ else
93
108
  HEALTH_GIT="fail"
94
109
  fi
95
110
 
111
+ # --- Guarded staging -------------------------------------------------------
112
+ # git_add_guarded() lives in the shared lib so the /handoff auto-save uses the
113
+ # exact same logic. Fallback to plain `git add -A` if the lib can't be sourced,
114
+ # so an unattended auto-save never silently no-ops.
115
+ # shellcheck source=bin/lib/git-safe.sh
116
+ . "$SCRIPT_DIR/bin/lib/git-safe.sh" 2>/dev/null || true
117
+ if ! type git_add_guarded >/dev/null 2>&1; then
118
+ git_add_guarded() { git add -A 2>/dev/null || true; }
119
+ fi
120
+
96
121
  # --- Setup develop (wrapped — failures set HEALTH_GIT but don't crash) ---
97
122
  setup_develop() {
98
123
  # Ensure develop branch exists locally
@@ -143,8 +168,9 @@ setup_develop() {
143
168
  if [[ "$CURRENT_BRANCH" != "develop" ]]; then
144
169
  # Check for uncommitted work (staged + unstaged + untracked in tracked dirs)
145
170
  if [ -n "$(git status --porcelain 2>/dev/null | head -1)" ]; then
146
- # Save uncommitted work so nothing is lost
147
- git add -A 2>/dev/null
171
+ # Save uncommitted work so nothing is lost (guarded: never auto-commit
172
+ # stray submodule pointers from sibling repos in the checkout)
173
+ git_add_guarded
148
174
  git commit -m "Auto-save: uncommitted work from $CURRENT_BRANCH" --quiet 2>/dev/null || true
149
175
  SAVED_BRANCH="$CURRENT_BRANCH"
150
176
  fi