create-egregore 0.17.4 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/assets/egregore-launcher.js +1965 -0
  2. package/assets/egregore-launcher.sh +385 -0
  3. package/bin/cli.js +472 -81
  4. package/lib/api.js +2 -2
  5. package/lib/auth.js +255 -20
  6. package/lib/codex-runtime.js +268 -0
  7. package/lib/commands.js +31 -0
  8. package/lib/context.js +1 -1
  9. package/lib/local.js +616 -189
  10. package/lib/setup.js +822 -84
  11. package/lib/ui.js +780 -84
  12. package/package.json +11 -3
  13. package/runtime/codex/.codex/config.toml +15 -0
  14. package/runtime/codex/.codex/hooks/branch-guard.js +234 -0
  15. package/runtime/codex/.codex/hooks.json +17 -0
  16. package/runtime/codex/.codex/skills/activity/SKILL.md +70 -0
  17. package/runtime/codex/.codex/skills/add/SKILL.md +25 -0
  18. package/runtime/codex/.codex/skills/announce/SKILL.md +54 -0
  19. package/runtime/codex/.codex/skills/answer/SKILL.md +25 -0
  20. package/runtime/codex/.codex/skills/archive/SKILL.md +41 -0
  21. package/runtime/codex/.codex/skills/ask/SKILL.md +59 -0
  22. package/runtime/codex/.codex/skills/branch/SKILL.md +25 -0
  23. package/runtime/codex/.codex/skills/checkup/SKILL.md +41 -0
  24. package/runtime/codex/.codex/skills/comment/SKILL.md +25 -0
  25. package/runtime/codex/.codex/skills/commit/SKILL.md +25 -0
  26. package/runtime/codex/.codex/skills/contribute/SKILL.md +25 -0
  27. package/runtime/codex/.codex/skills/dark-mode/SKILL.md +25 -0
  28. package/runtime/codex/.codex/skills/dashboard/SKILL.md +59 -0
  29. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +70 -0
  30. package/runtime/codex/.codex/skills/delete-user/SKILL.md +25 -0
  31. package/runtime/codex/.codex/skills/edit/SKILL.md +25 -0
  32. package/runtime/codex/.codex/skills/edit-process/SKILL.md +25 -0
  33. package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +25 -0
  34. package/runtime/codex/.codex/skills/emissary/SKILL.md +41 -0
  35. package/runtime/codex/.codex/skills/env/SKILL.md +25 -0
  36. package/runtime/codex/.codex/skills/handoff/SKILL.md +67 -0
  37. package/runtime/codex/.codex/skills/harvest/SKILL.md +107 -0
  38. package/runtime/codex/.codex/skills/infra/SKILL.md +41 -0
  39. package/runtime/codex/.codex/skills/ingest/SKILL.md +25 -0
  40. package/runtime/codex/.codex/skills/ingest-google/SKILL.md +25 -0
  41. package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  42. package/runtime/codex/.codex/skills/invite/SKILL.md +40 -0
  43. package/runtime/codex/.codex/skills/issue/SKILL.md +41 -0
  44. package/runtime/codex/.codex/skills/me/SKILL.md +25 -0
  45. package/runtime/codex/.codex/skills/meeting/SKILL.md +41 -0
  46. package/runtime/codex/.codex/skills/note/SKILL.md +25 -0
  47. package/runtime/codex/.codex/skills/onboarding/SKILL.md +41 -0
  48. package/runtime/codex/.codex/skills/pr/SKILL.md +25 -0
  49. package/runtime/codex/.codex/skills/project/SKILL.md +41 -0
  50. package/runtime/codex/.codex/skills/pull/SKILL.md +25 -0
  51. package/runtime/codex/.codex/skills/push/SKILL.md +25 -0
  52. package/runtime/codex/.codex/skills/quest/SKILL.md +77 -0
  53. package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +25 -0
  54. package/runtime/codex/.codex/skills/reflect/SKILL.md +41 -0
  55. package/runtime/codex/.codex/skills/review-pr/SKILL.md +41 -0
  56. package/runtime/codex/.codex/skills/save/SKILL.md +62 -0
  57. package/runtime/codex/.codex/skills/setup/SKILL.md +25 -0
  58. package/runtime/codex/.codex/skills/summon/SKILL.md +41 -0
  59. package/runtime/codex/.codex/skills/sync-repos/SKILL.md +25 -0
  60. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +25 -0
  61. package/runtime/codex/.codex/skills/telemetry/SKILL.md +25 -0
  62. package/runtime/codex/.codex/skills/test/SKILL.md +41 -0
  63. package/runtime/codex/.codex/skills/the-spiral/SKILL.md +84 -0
  64. package/runtime/codex/.codex/skills/todo/SKILL.md +41 -0
  65. package/runtime/codex/.codex/skills/tui-design/SKILL.md +41 -0
  66. package/runtime/codex/.codex/skills/tutorial/SKILL.md +41 -0
  67. package/runtime/codex/.codex/skills/update/SKILL.md +25 -0
  68. package/runtime/codex/.codex/skills/view/SKILL.md +41 -0
  69. package/runtime/codex/.codex/skills/visual-explain/SKILL.md +41 -0
  70. package/runtime/codex/.codex/skills/wrap/SKILL.md +55 -0
  71. package/runtime/codex/.codex/spec-manifest.json +108 -0
  72. package/runtime/codex/AGENTS.md +328 -0
  73. package/runtime/codex/bin/activity-data.sh +180 -0
  74. package/runtime/codex/bin/agent.sh +669 -0
  75. package/runtime/codex/bin/codex-install-skills.sh +78 -0
  76. package/runtime/codex/bin/codex-render-spec.mjs +308 -0
  77. package/runtime/codex/bin/codex-session-start.sh +238 -0
  78. package/runtime/codex/bin/codex-skill-render.mjs +315 -0
  79. package/runtime/codex/bin/codex-sync-skills.sh +223 -0
  80. package/runtime/codex/bin/dashboard-data.sh +178 -0
  81. package/runtime/codex/bin/graph-batch.sh +60 -0
  82. package/runtime/codex/bin/graph-op.sh +397 -0
  83. package/runtime/codex/bin/graph-wal.sh +228 -0
  84. package/runtime/codex/bin/graph.sh +141 -0
  85. package/runtime/codex/bin/handoff-pr-backfill.sh +86 -0
  86. package/runtime/codex/bin/handoff-run.sh +569 -0
  87. package/runtime/codex/bin/handoff-save-egregore.sh +131 -0
  88. package/runtime/codex/bin/index-handoff.sh +306 -0
  89. package/runtime/codex/bin/lib/artifact-id.sh +46 -0
  90. package/runtime/codex/bin/lib/config.sh +71 -0
  91. package/runtime/codex/bin/lib/context.sh +511 -0
  92. package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -0
  93. package/runtime/codex/bin/lib/edit-loopback.mjs +652 -0
  94. package/runtime/codex/bin/lib/ensure-develop.sh +183 -0
  95. package/runtime/codex/bin/lib/git-sync.sh +262 -0
  96. package/runtime/codex/bin/lib/greeting.sh +432 -0
  97. package/runtime/codex/bin/lib/hash.sh +13 -0
  98. package/runtime/codex/bin/lib/identity.sh +164 -0
  99. package/runtime/codex/bin/lib/permissions.sh +106 -0
  100. package/runtime/codex/bin/lib/time.sh +56 -0
  101. package/runtime/codex/bin/lib/worktree-links.sh +54 -0
  102. package/runtime/codex/bin/notify.sh +207 -0
  103. package/runtime/codex/bin/publish-artifact.sh +187 -0
  104. package/runtime/codex/bin/publish-references.sh +144 -0
  105. package/runtime/codex/bin/repo-state.sh +99 -0
  106. package/runtime/codex/bin/telemetry.sh +350 -0
  107. package/runtime/codex/bin/worktree-create.sh +115 -0
  108. package/runtime/codex/bin/worktree-remove.sh +50 -0
  109. package/runtime/codex/bin/worktree.sh +199 -0
  110. package/runtime/codex/manifest.json +162 -0
@@ -0,0 +1,511 @@
1
+ # shellcheck shell=bash
2
+ # context.sh — Context gathering for session-start.sh
3
+ #
4
+ # Runs 10 parallel background subshells to gather session context:
5
+ # 1. Recent handoffs
6
+ # 2. Active quests
7
+ # 3. User's last git activity
8
+ # 3b. Personal todos from graph
9
+ # 4. Team presence (graph + git branches)
10
+ # 5. Soul self-summary
11
+ # 6. Handoffs addressed to user
12
+ # 7. Graph health check
13
+ # 8. Telegram health check
14
+ # 9. Lifecycle events (merged PRs, implemented handoffs)
15
+ # 10. Last Pulse brief
16
+ #
17
+ # All subshells write to $CTX_DIR (a temp directory).
18
+ # Ends with `wait` + reading health results back.
19
+ #
20
+ # Inputs: SCRIPT_DIR, AUTHOR, LOCAL_MODE, STATE_FILE, CONFIG, ENV_FILE,
21
+ # HEALTH_GRAPH, HEALTH_TELEGRAM
22
+ # Outputs: CTX_DIR (populated), TIME_OF_DAY, HEALTH_GRAPH, HEALTH_TELEGRAM
23
+
24
+ CTX_DIR=$(mktemp -d)
25
+
26
+ # Time of day
27
+ HOUR=$(date +%H)
28
+ if [ "$HOUR" -lt 12 ]; then TIME_OF_DAY="morning"
29
+ elif [ "$HOUR" -lt 17 ]; then TIME_OF_DAY="afternoon"
30
+ else TIME_OF_DAY="evening"
31
+ fi
32
+
33
+ # 1. Recent handoffs (background)
34
+ (
35
+ JSON="[]"
36
+ if [ -d "$SCRIPT_DIR/memory/handoffs" ]; then
37
+ FILES=$(find -L "$SCRIPT_DIR/memory/handoffs" -name '*.md' -not -name 'index*' -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -3)
38
+ JSON="["
39
+ FIRST=true
40
+ for F in $FILES; do
41
+ [ -z "$F" ] && continue
42
+ NAME=$(basename "$F" .md)
43
+ PREVIEW=$(head -c 120 "$F" 2>/dev/null | tr '\n' ' ' | sed 's/"/\\"/g')
44
+ $FIRST || JSON="$JSON,"
45
+ JSON="$JSON{\"name\":\"$NAME\",\"preview\":\"$PREVIEW\"}"
46
+ FIRST=false
47
+ done
48
+ JSON="$JSON]"
49
+ fi
50
+ echo "$JSON" > "$CTX_DIR/handoffs"
51
+ ) &
52
+
53
+ # 2. Quests (background)
54
+ (
55
+ JSON="[]"
56
+ if [ -d "$SCRIPT_DIR/memory/quests" ]; then
57
+ JSON="["
58
+ FIRST=true
59
+ for F in "$SCRIPT_DIR/memory/quests/"*.md; do
60
+ [ -e "$F" ] || continue
61
+ echo "$F" | grep -q draft && continue
62
+ NAME=$(basename "$F" .md)
63
+ $FIRST || JSON="$JSON,"
64
+ JSON="$JSON\"$NAME\""
65
+ FIRST=false
66
+ done
67
+ JSON="$JSON]"
68
+ fi
69
+ echo "$JSON" > "$CTX_DIR/quests"
70
+ ) &
71
+
72
+ # 3. User's last activity (background)
73
+ (
74
+ git log --author="$AUTHOR" --format="%ar|%s" -1 2>/dev/null > "$CTX_DIR/activity" || echo "" > "$CTX_DIR/activity"
75
+ ) &
76
+
77
+ # 3b. Personal todos from graph (background)
78
+ (
79
+ _API_URL=$(jq -r '.api_url // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
80
+ _API_KEY=$(grep '^EGREGORE_API_KEY=' "${ENV_FILE:-$SCRIPT_DIR/.env}" 2>/dev/null | cut -d'=' -f2-)
81
+ if [ -n "$_API_URL" ] && [ -n "$_API_KEY" ]; then
82
+ TODO_RAW=$(bash "$SCRIPT_DIR/bin/graph.sh" query \
83
+ "MATCH (t:Todo)-[:BY]->(p:Person {github: \$me}) WHERE t.status IN ['open', 'blocked'] OPTIONAL MATCH (t)-[:PART_OF]->(q:Quest) RETURN t.text AS text, t.priority AS priority, t.status AS status, t.created AS created, q.id AS quest ORDER BY t.priority DESC, t.created DESC LIMIT 5" \
84
+ "$(jq -n --arg me "$AUTHOR" '{me: $me}')" 2>/dev/null || echo "")
85
+ if [ -n "$TODO_RAW" ]; then
86
+ echo "$TODO_RAW" | jq '[.values[] | {text: .[0], priority: (.[1] // 0), status: .[2], created: .[3], quest: (.[4] // "")}]' 2>/dev/null || echo "[]"
87
+ else
88
+ echo "[]"
89
+ fi
90
+ else
91
+ echo "[]"
92
+ fi
93
+ ) > "$CTX_DIR/todos" 2>/dev/null &
94
+
95
+ # 4. Team presence — last seen + active branches (background)
96
+ (
97
+ # --- Read config inside subshell ---
98
+ _API_URL=$(jq -r '.api_url // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
99
+ _API_KEY=$(grep '^EGREGORE_API_KEY=' "${ENV_FILE:-$SCRIPT_DIR/.env}" 2>/dev/null | cut -d'=' -f2-)
100
+
101
+ # --- Graph: last-seen per person (excluding self) ---
102
+ GRAPH_DATA="[]"
103
+ if [ -n "$_API_URL" ] && [ -n "$_API_KEY" ]; then
104
+ CYPHER="MATCH (s:Session)-[:BY]->(p:Person) WHERE p.github <> \$me RETURN p.name AS name, toString(max(coalesce(s.startedAt, datetime(s.date)))) AS lastSeen ORDER BY lastSeen DESC"
105
+ GRAPH_RAW=$(bash "$SCRIPT_DIR/bin/graph.sh" query "$CYPHER" "$(jq -n --arg me "$AUTHOR" '{me: $me}')" 2>/dev/null || echo "")
106
+ if [ -n "$GRAPH_RAW" ]; then
107
+ GRAPH_DATA=$(echo "$GRAPH_RAW" | jq '[.values[] | {name: .[0], lastSeen: .[1]}]' 2>/dev/null || echo "[]")
108
+ fi
109
+ fi
110
+
111
+ # --- Git: branch tip commit dates per person (primary recency signal) ---
112
+ # git for-each-ref is fast and gives the actual last commit date per branch
113
+ GIT_COMMIT_DATA="{}"
114
+ GIT_REF_DATA=$(git -C "$SCRIPT_DIR" for-each-ref --sort=-committerdate \
115
+ --format='%(refname:short)|%(committerdate:unix)' refs/remotes/origin/dev/ 2>/dev/null || echo "")
116
+ if [ -n "$GIT_REF_DATA" ]; then
117
+ GIT_COMMIT_DATA=$(echo "$GIT_REF_DATA" | sed 's|^origin/dev/||' | while IFS='|' read -r REF_PATH REF_EPOCH || [ -n "$REF_PATH" ]; do
118
+ [ -z "$REF_PATH" ] && continue
119
+ REF_AUTHOR=$(echo "$REF_PATH" | cut -d'/' -f1 | tr '[:upper:]' '[:lower:]')
120
+ echo "${REF_AUTHOR}|${REF_EPOCH}"
121
+ done | sort -t'|' -k1,1 -k2,2rn | sort -t'|' -k1,1 -u | jq -Rn '
122
+ [inputs | split("|") | {(.[0]): .[1]}]
123
+ | add // {}
124
+ ' 2>/dev/null || echo "{}")
125
+ fi
126
+
127
+ # --- Git: active dev/* branches (excluding self) ---
128
+ SELF_LC=$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]')
129
+ SELF_DISPLAY_LC=""
130
+ if [ -f "$SCRIPT_DIR/.egregore-state.json" ]; then
131
+ SELF_DISPLAY_LC=$(jq -r '.display_name // empty' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null | tr '[:upper:]' '[:lower:]')
132
+ fi
133
+ BRANCH_DATA=$(git -C "$SCRIPT_DIR" branch -r --format='%(refname:short)' 2>/dev/null | grep "^origin/dev/" | sed 's|^origin/dev/||' || echo "")
134
+
135
+ BRANCH_MAP="{}"
136
+ if [ -n "$BRANCH_DATA" ]; then
137
+ BRANCH_MAP=$(echo "$BRANCH_DATA" | while IFS='/' read -r B_AUTHOR B_SLUG_REST || [ -n "$B_AUTHOR" ]; do
138
+ [ -z "$B_AUTHOR" ] && continue
139
+ B_AUTHOR_LC=$(echo "$B_AUTHOR" | tr '[:upper:]' '[:lower:]')
140
+ [ "$B_AUTHOR_LC" = "$SELF_LC" ] && continue
141
+ [ -n "$SELF_DISPLAY_LC" ] && [ "$B_AUTHOR_LC" = "$SELF_DISPLAY_LC" ] && continue
142
+ B_HUMAN=$(echo "$B_SLUG_REST" | tr '-' ' ')
143
+ echo "${B_AUTHOR_LC}|${B_HUMAN}"
144
+ done | jq -Rn '
145
+ [inputs | split("|") | {author: .[0], branch: .[1]}]
146
+ | group_by(.author)
147
+ | map({(.[0].author): [.[] | .branch]})
148
+ | add // {}
149
+ ' 2>/dev/null || echo "{}")
150
+ fi
151
+
152
+ # --- Local-mode presence: scan session + wrap files for lastSeen ---
153
+ # When graph has no data, these files provide temporal presence information
154
+ FILE_PRESENCE="{}"
155
+ if [ "$GRAPH_DATA" = "[]" ]; then
156
+ FILE_PRESENCE=$(
157
+ {
158
+ # Scan session logs, wraps, and handoffs (all use **Author**: / **Date**: format)
159
+ for DIR in "$SCRIPT_DIR/memory/sessions" "$SCRIPT_DIR/memory/wraps" "$SCRIPT_DIR/memory/handoffs"; do
160
+ [ -d "$DIR" ] || continue
161
+ find -L "$DIR" -name '*.md' -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -20 | while read -r SFILE; do
162
+ [ -z "$SFILE" ] && continue
163
+ S_AUTH=$(grep -m1 '^\*\*Author\*\*:' "$SFILE" 2>/dev/null | sed 's/^\*\*Author\*\*:[[:space:]]*//')
164
+ S_DATE=$(grep -m1 '^\*\*Date\*\*:' "$SFILE" 2>/dev/null | sed 's/^\*\*Date\*\*:[[:space:]]*//')
165
+ [ -z "$S_AUTH" ] && continue
166
+ echo "${S_AUTH}|${S_DATE}"
167
+ done
168
+ done
169
+ } | sort -t'|' -k1,1 -k2,2r | sort -t'|' -k1,1 -u | while IFS='|' read -r FP_NAME FP_DATE; do
170
+ [ -z "$FP_NAME" ] && continue
171
+ FP_LC=$(echo "$FP_NAME" | tr '[:upper:]' '[:lower:]')
172
+ echo "${FP_LC}|${FP_DATE}"
173
+ done | jq -Rn '
174
+ [inputs | split("|") | {(.[0]): .[1]}]
175
+ | add // {}
176
+ ' 2>/dev/null || echo "{}"
177
+ )
178
+ fi
179
+
180
+ # --- Relative time + epoch helpers (cross-platform) ---
181
+ NOW_EPOCH=$(date +%s)
182
+
183
+ iso_to_epoch() {
184
+ local iso="$1"
185
+ local clean=$(echo "$iso" | sed 's/Z$//; s/+00:00$//; s/\.[0-9]*//')
186
+ [[ "$clean" != *T* ]] && clean="${clean}T00:00:00"
187
+ TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$clean" "+%s" 2>/dev/null || \
188
+ TZ=UTC date -d "$clean" +%s 2>/dev/null || echo "0"
189
+ }
190
+
191
+ epoch_to_relative() {
192
+ local epoch="$1"
193
+ [ "$epoch" = "0" ] && echo "--" && return
194
+ local delta=$(( NOW_EPOCH - epoch ))
195
+ if [ "$delta" -lt 60 ]; then echo "just now"
196
+ elif [ "$delta" -lt 3600 ]; then echo "$(( delta / 60 ))m ago"
197
+ elif [ "$delta" -lt 86400 ]; then echo "$(( delta / 3600 ))h ago"
198
+ else
199
+ # Calendar-day based comparison for accurate "yesterday" / "Xd ago"
200
+ local event_date today_date
201
+ event_date=$(date -r "$epoch" +%Y-%m-%d 2>/dev/null || \
202
+ date -d "@$epoch" +%Y-%m-%d 2>/dev/null || echo "")
203
+ today_date=$(date +%Y-%m-%d)
204
+ if [ -z "$event_date" ] || [ "$event_date" = "$today_date" ]; then echo "today"
205
+ else
206
+ local today_e event_e
207
+ today_e=$(date -j -f "%Y-%m-%d" "$today_date" +%s 2>/dev/null || \
208
+ date -d "$today_date" +%s 2>/dev/null || echo "$NOW_EPOCH")
209
+ event_e=$(date -j -f "%Y-%m-%d" "$event_date" +%s 2>/dev/null || \
210
+ date -d "$event_date" +%s 2>/dev/null || echo "$epoch")
211
+ local days=$(( (today_e - event_e) / 86400 ))
212
+ [ "$days" -lt 1 ] && days=1
213
+ if [ "$days" -eq 1 ]; then echo "yesterday"
214
+ else echo "${days}d ago"
215
+ fi
216
+ fi
217
+ fi
218
+ }
219
+
220
+ # --- Merge git commits + graph + file presence + branches into presence array ---
221
+ ALL_NAMES=$(echo "$GRAPH_DATA" | jq -r '.[].name' 2>/dev/null || echo "")
222
+ FILE_NAMES=$(echo "$FILE_PRESENCE" | jq -r 'keys[]' 2>/dev/null || echo "")
223
+ BRANCH_NAMES=$(echo "$BRANCH_MAP" | jq -r 'keys[]' 2>/dev/null || echo "")
224
+ GIT_NAMES=$(echo "$GIT_COMMIT_DATA" | jq -r 'keys[]' 2>/dev/null || echo "")
225
+
226
+ UNION_NAMES=$(printf "%s\n%s\n%s\n%s" "$ALL_NAMES" "$FILE_NAMES" "$BRANCH_NAMES" "$GIT_NAMES" | tr '[:upper:]' '[:lower:]' | sort -u | grep -v '^$' | grep -v "^${SELF_LC}$" | grep -v "^${SELF_DISPLAY_LC}$" || echo "")
227
+
228
+ if [ -z "$UNION_NAMES" ]; then
229
+ echo "[]" > "$CTX_DIR/team"
230
+ exit 0
231
+ fi
232
+
233
+ # --- Build presence JSON ---
234
+ PRESENCE="["
235
+ FIRST=true
236
+ for PNAME in $UNION_NAMES; do
237
+ # Git commit date is authoritative (actual work shipped).
238
+ # Graph/file presence are fallbacks only (session opens ≠ real activity).
239
+ GIT_EPOCH=$(echo "$GIT_COMMIT_DATA" | jq -r --arg n "$PNAME" '.[$n] // empty' 2>/dev/null || true)
240
+
241
+ LAST_SEEN_EPOCH="0"
242
+ if [ -n "$GIT_EPOCH" ] && [ "$GIT_EPOCH" -gt 0 ] 2>/dev/null; then
243
+ LAST_SEEN_EPOCH="$GIT_EPOCH"
244
+ else
245
+ # Fallback: graph session startedAt
246
+ GRAPH_ISO=$(echo "$GRAPH_DATA" | jq -r --arg n "$PNAME" '.[] | select((.name | ascii_downcase) == $n) | .lastSeen // empty' 2>/dev/null | head -1)
247
+ if [ -n "$GRAPH_ISO" ]; then
248
+ LAST_SEEN_EPOCH=$(iso_to_epoch "$GRAPH_ISO")
249
+ fi
250
+ # Fallback: file-based presence (local mode)
251
+ if [ "$LAST_SEEN_EPOCH" -le 0 ] 2>/dev/null; then
252
+ FILE_ISO=$(echo "$FILE_PRESENCE" | jq -r --arg n "$PNAME" '.[$n] // empty' 2>/dev/null || true)
253
+ if [ -n "$FILE_ISO" ]; then
254
+ LAST_SEEN_EPOCH=$(iso_to_epoch "$FILE_ISO")
255
+ fi
256
+ fi
257
+ fi
258
+
259
+ if [ "$LAST_SEEN_EPOCH" -gt 0 ] 2>/dev/null; then
260
+ LAST_SEEN_REL=$(epoch_to_relative "$LAST_SEEN_EPOCH")
261
+ else
262
+ LAST_SEEN_EPOCH="0"
263
+ LAST_SEEN_REL="--"
264
+ fi
265
+
266
+ BRANCHES_JSON=$(echo "$BRANCH_MAP" | jq --arg n "$PNAME" '.[$n] // []' 2>/dev/null || echo "[]")
267
+ BR_COUNT=$(echo "$BRANCHES_JSON" | jq 'length' 2>/dev/null || echo "0")
268
+ if [ "$BR_COUNT" -gt 2 ]; then
269
+ EXTRA=$((BR_COUNT - 2))
270
+ BRANCHES_JSON=$(echo "$BRANCHES_JSON" | jq --arg e "+${EXTRA} more" '[.[0:2][], $e]' 2>/dev/null || echo "$BRANCHES_JSON")
271
+ fi
272
+
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}')
275
+ $FIRST || PRESENCE="$PRESENCE,"
276
+ PRESENCE="$PRESENCE$ENTRY"
277
+ FIRST=false
278
+ done
279
+ PRESENCE="$PRESENCE]"
280
+
281
+ PRESENCE=$(echo "$PRESENCE" | jq 'sort_by(-.last_seen_sort)' 2>/dev/null || echo "$PRESENCE")
282
+ echo "$PRESENCE" > "$CTX_DIR/team"
283
+ ) &
284
+
285
+ # 5. Soul self-summary (background)
286
+ (
287
+ SUMMARY=""
288
+ if [ -f "$SCRIPT_DIR/egregore.md" ]; then
289
+ SUMMARY=$(sed -n '/^## Self-Summary/,/^##/p' "$SCRIPT_DIR/egregore.md" | sed '1d;/^##/d' | sed 's/^[[:space:]]*//' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
290
+ fi
291
+ echo "$SUMMARY" > "$CTX_DIR/soul_summary"
292
+ ) &
293
+
294
+ # 6. Handoffs addressed to user (background, enriched with author/topic)
295
+ (
296
+ JSON="[]"
297
+ RICH="[]"
298
+ if [ -d "$SCRIPT_DIR/memory/handoffs" ]; then
299
+ # Match github username, display name, github_name first word, and people-file name
300
+ # Handoff files may use "to: name", "**To**: name", or "To: name"
301
+ _DISPLAY=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
302
+ _GH_NAME=$(jq -r '.github_name // empty' "$STATE_FILE" 2>/dev/null)
303
+ _FIRST_NAME=$(echo "$_GH_NAME" | awk '{print $1}')
304
+ # Also check people file: memory/people/{author}.md has "# Name" on first line
305
+ _PEOPLE_NAME=""
306
+ _PEOPLE_FILE="$SCRIPT_DIR/memory/people/$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]').md"
307
+ [ -f "$_PEOPLE_FILE" ] && _PEOPLE_NAME=$(sed -n 's/^# *//p' "$_PEOPLE_FILE" 2>/dev/null | head -1)
308
+ _GREP_PAT="[Tt]o[*]*: *$AUTHOR\|[Tt]o[*]*:$AUTHOR"
309
+ for _VARIANT in "$_DISPLAY" "$_FIRST_NAME" "$_PEOPLE_NAME"; do
310
+ [ -z "$_VARIANT" ] && continue
311
+ [ "$_VARIANT" = "$AUTHOR" ] && continue
312
+ # Skip if already in pattern (avoid duplicates)
313
+ echo "$_GREP_PAT" | grep -qF "$_VARIANT" && continue
314
+ _GREP_PAT="$_GREP_PAT\|[Tt]o[*]*: *$_VARIANT\|[Tt]o[*]*:$_VARIANT"
315
+ done
316
+ ADDRESSED=$(grep -rli "$_GREP_PAT" "$SCRIPT_DIR/memory/handoffs/" 2>/dev/null | sort -r | head -5 || true)
317
+ JSON="["
318
+ RICH="["
319
+ FIRST=true
320
+ for AF in $ADDRESSED; do
321
+ [ -z "$AF" ] && continue
322
+ 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
335
+ # Extract repo state table if present
336
+ AF_REPO_STATE="[]"
337
+ AF_REPO_TABLE=$(awk '/^## Repo State/{found=1; next} found && /^#/{exit} found && /^\|[^-]/ && !/^\| Repo/{print}' "$AF" 2>/dev/null || true)
338
+ if [ -n "$AF_REPO_TABLE" ]; then
339
+ AF_REPO_STATE=$(echo "$AF_REPO_TABLE" | while IFS='|' read -r _ R_REPO R_BRANCH R_PR R_BASE _; do
340
+ R_REPO=$(echo "$R_REPO" | xargs 2>/dev/null || true)
341
+ R_BRANCH=$(echo "$R_BRANCH" | xargs 2>/dev/null || true)
342
+ R_PR=$(echo "$R_PR" | xargs 2>/dev/null | sed 's/^#//' | sed 's/—//' || true)
343
+ R_BASE=$(echo "$R_BASE" | xargs 2>/dev/null || true)
344
+ [ -z "$R_REPO" ] && continue
345
+ if [ -n "$R_PR" ] && [ "$R_PR" -eq "$R_PR" ] 2>/dev/null; then
346
+ printf '{"repo":"%s","branch":"%s","pr":%s,"base":"%s"}\n' "$R_REPO" "$R_BRANCH" "$R_PR" "$R_BASE"
347
+ else
348
+ printf '{"repo":"%s","branch":"%s","pr":null,"base":"%s"}\n' "$R_REPO" "$R_BRANCH" "$R_BASE"
349
+ fi
350
+ done | jq -sc '.' 2>/dev/null || echo "[]")
351
+ fi
352
+ $FIRST || JSON="$JSON,"
353
+ $FIRST || RICH="$RICH,"
354
+ 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}"
358
+ FIRST=false
359
+ done
360
+ JSON="$JSON]"
361
+ RICH="$RICH]"
362
+ fi
363
+ echo "$JSON" > "$CTX_DIR/addressed"
364
+ echo "$RICH" > "$CTX_DIR/addressed_rich"
365
+ ) &
366
+
367
+ # 6.5. Pending questions addressed to user (background)
368
+ (
369
+ PENDING_JSON="[]"
370
+ QDIR="$SCRIPT_DIR/memory/knowledge/questions"
371
+ if [ -d "$QDIR" ]; then
372
+ # Match against the same identity variants used for handoffs (display name,
373
+ # github first-name, people-file name) so "to: oz" matches author "oguzhan".
374
+ _DISPLAY_Q=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
375
+ _GH_NAME_Q=$(jq -r '.github_name // empty' "$STATE_FILE" 2>/dev/null)
376
+ _FIRST_NAME_Q=$(echo "$_GH_NAME_Q" | awk '{print tolower($1)}')
377
+ _PEOPLE_FILE_Q="$SCRIPT_DIR/memory/people/$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]').md"
378
+ _PEOPLE_NAME_Q=""
379
+ [ -f "$_PEOPLE_FILE_Q" ] && _PEOPLE_NAME_Q=$(sed -n 's/^# *//p' "$_PEOPLE_FILE_Q" 2>/dev/null | head -1 | awk '{print tolower($1)}')
380
+
381
+ LINES=""
382
+ for QF in "$QDIR"/*.md; do
383
+ [ -f "$QF" ] || continue
384
+ Q_TO=$(sed -n 's/^to:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"' | tr '[:upper:]' '[:lower:]' | awk '{print $1}')
385
+ [ -z "$Q_TO" ] && continue
386
+ _MATCH="false"
387
+ for _V in "$AUTHOR" "$_DISPLAY_Q" "$_FIRST_NAME_Q" "$_PEOPLE_NAME_Q"; do
388
+ [ -z "$_V" ] && continue
389
+ _V_LC=$(echo "$_V" | tr '[:upper:]' '[:lower:]')
390
+ [ "$Q_TO" = "$_V_LC" ] && _MATCH="true" && break
391
+ done
392
+ [ "$_MATCH" = "true" ] || continue
393
+ Q_STATUS=$(sed -n 's/^status:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"')
394
+ [ "$Q_STATUS" = "pending" ] || continue
395
+ Q_FROM=$(sed -n 's/^from:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"')
396
+ Q_TOPIC=$(sed -n 's/^topic:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"')
397
+ Q_HID=$(sed -n 's/^harvest_id:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"')
398
+ Q_REL="${QF#$SCRIPT_DIR/}"
399
+ LINE=$(jq -nc \
400
+ --arg from "$Q_FROM" \
401
+ --arg topic "$Q_TOPIC" \
402
+ --arg hid "$Q_HID" \
403
+ --arg file "$Q_REL" \
404
+ '{from:$from, topic:$topic, harvest_id:$hid, file:$file}' 2>/dev/null)
405
+ [ -n "$LINE" ] && LINES="$LINES$LINE
406
+ "
407
+ done
408
+ if [ -n "$LINES" ]; then
409
+ PENDING_JSON=$(echo "$LINES" | jq -sc '.' 2>/dev/null || echo "[]")
410
+ fi
411
+ fi
412
+ echo "$PENDING_JSON" > "$CTX_DIR/pending_questions"
413
+ ) &
414
+
415
+ # 7. Graph health (background — zero added latency, runs in parallel)
416
+ (
417
+ if [ "$LOCAL_MODE" = "true" ]; then
418
+ echo "skip"
419
+ elif bash "$SCRIPT_DIR/bin/graph.sh" test 2>/dev/null | grep -q "Connected"; then
420
+ echo "ok"
421
+ else
422
+ echo "fail"
423
+ fi
424
+ ) > "$CTX_DIR/graph_health" 2>/dev/null &
425
+
426
+ # 8. Telegram health (background)
427
+ (
428
+ if [ "$LOCAL_MODE" = "true" ]; then
429
+ echo "skip"
430
+ elif bash "$SCRIPT_DIR/bin/notify.sh" test 2>/dev/null | grep -q "connected"; then
431
+ echo "ok"
432
+ else
433
+ echo "fail"
434
+ fi
435
+ ) > "$CTX_DIR/telegram_health" 2>/dev/null &
436
+
437
+ # 9. Lifecycle events: merged PRs + implemented handoffs (background)
438
+ (
439
+ GH_USER_LC=""
440
+ if [ -f "$STATE_FILE" ]; then
441
+ GH_USER_LC=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
442
+ fi
443
+ DISPLAY_NAME_LC=""
444
+ if [ -f "$STATE_FILE" ]; then
445
+ DISPLAY_NAME_LC=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
446
+ fi
447
+ # Get last session end date (use 7 days ago as fallback)
448
+ LAST_END=$(bash "$SCRIPT_DIR/bin/graph.sh" query "
449
+ MATCH (s:Session)-[:BY]->(p:Person {github: \$gh})
450
+ WHERE s.wrappedAt IS NOT NULL
451
+ RETURN toString(s.wrappedAt) AS t
452
+ ORDER BY s.wrappedAt DESC SKIP 1 LIMIT 1
453
+ " "$(jq -n --arg gh "$GH_USER_LC" '{gh: $gh}')" 2>/dev/null | jq -r '.values[0][0] // empty' 2>/dev/null)
454
+ if [ -z "$LAST_END" ]; then
455
+ LAST_END="$(date -v-7d +%Y-%m-%d 2>/dev/null || date -d '7 days ago' +%Y-%m-%d 2>/dev/null || echo '2026-03-03')T00:00:00Z"
456
+ fi
457
+
458
+ MERGED_JSON="[]"
459
+ if [ -n "$GH_USER_LC" ]; then
460
+ MERGED_JSON=$(bash "$SCRIPT_DIR/bin/graph-op.sh" my-merged-prs "$GH_USER_LC" "$(echo "$LAST_END" | cut -dT -f1)" 2>/dev/null || echo "[]")
461
+ fi
462
+
463
+ IMPL_JSON="[]"
464
+ IMPL_NAME="${DISPLAY_NAME_LC:-$GH_USER_LC}"
465
+ if [ -n "$IMPL_NAME" ]; then
466
+ IMPL_JSON=$(bash "$SCRIPT_DIR/bin/graph-op.sh" my-implemented-handoffs "$IMPL_NAME" "$(echo "$LAST_END" | cut -dT -f1)" 2>/dev/null || echo "[]")
467
+ fi
468
+
469
+ jq -n --argjson merged "$MERGED_JSON" --argjson impl "$IMPL_JSON" \
470
+ '{merged_prs: $merged, implemented_handoffs: $impl}' 2>/dev/null || echo '{"merged_prs":[],"implemented_handoffs":[]}'
471
+ ) > "$CTX_DIR/lifecycle" 2>/dev/null &
472
+
473
+ # 10. Last Pulse brief (background) — gated on feature flag + API key
474
+ (
475
+ PULSE_ENABLED=$(jq -r '.features.pulse // "false"' "$CONFIG" 2>/dev/null || echo "false")
476
+ HAS_API_KEY=false
477
+ _env_file="${ENV_FILE:-$SCRIPT_DIR/.env}"
478
+ if [ -f "$_env_file" ]; then
479
+ _key=$(grep '^EGREGORE_API_KEY=' "$_env_file" 2>/dev/null | cut -d'=' -f2- || true)
480
+ [ -n "$_key" ] && HAS_API_KEY=true
481
+ fi
482
+
483
+ if [ "$PULSE_ENABLED" = "true" ] && [ "$HAS_API_KEY" = "true" ] && [ -n "$GH_USER_LC" ]; then
484
+ BRIEF_RESULT=$(bash "$SCRIPT_DIR/bin/graph.sh" query "
485
+ MATCH (p:Person {github: \$gh})
486
+ WHERE p.lastBrief IS NOT NULL
487
+ AND p.lastBriefDate >= date() - duration('P7D')
488
+ RETURN p.lastBrief, toString(p.lastBriefDate), p.lastRecommendations
489
+ " "$(jq -n --arg gh "$GH_USER_LC" '{gh: $gh}')" 2>/dev/null || echo '{"values":[]}')
490
+ BRIEF=$(echo "$BRIEF_RESULT" | jq -r '.values[0][0] // empty' 2>/dev/null || true)
491
+ BRIEF_DATE=$(echo "$BRIEF_RESULT" | jq -r '.values[0][1] // empty' 2>/dev/null || true)
492
+ RECS=$(echo "$BRIEF_RESULT" | jq -c '.values[0][2] // []' 2>/dev/null || echo '[]')
493
+ if [ -n "$BRIEF" ]; then
494
+ jq -n --arg brief "$BRIEF" --arg date "$BRIEF_DATE" --argjson recs "$RECS" \
495
+ '{brief: $brief, date: $date, recommendations: $recs}'
496
+ else
497
+ echo '{}'
498
+ fi
499
+ else
500
+ echo '{}'
501
+ fi
502
+ ) > "$CTX_DIR/pulse_brief" 2>/dev/null &
503
+
504
+ # Wait for all context gathering + health checks to finish
505
+ wait
506
+
507
+ # --- Read service health results ---
508
+ GRAPH_RESULT=$(cat "$CTX_DIR/graph_health" 2>/dev/null || echo "")
509
+ if [ -n "$GRAPH_RESULT" ]; then HEALTH_GRAPH="$GRAPH_RESULT"; fi
510
+ TELEGRAM_RESULT=$(cat "$CTX_DIR/telegram_health" 2>/dev/null || echo "")
511
+ if [ -n "$TELEGRAM_RESULT" ]; then HEALTH_TELEGRAM="$TELEGRAM_RESULT"; fi
@@ -0,0 +1,142 @@
1
+ # shellcheck shell=bash
2
+ # dashboard-artifact.sh — Generate a session dashboard artifact with a shareable URL
3
+ #
4
+ # Runs after context.sh (all $CTX_DIR/* files populated), before greeting.sh.
5
+ # Compiles gathered context into a markdown document, generates HTML via
6
+ # egregore-artifacts, publishes it, and exports DASHBOARD_URL for greeting.sh.
7
+ #
8
+ # Inputs: SCRIPT_DIR, CTX_DIR, AUTHOR, GREETING_NAME, LOCAL_MODE,
9
+ # EGREGORE_SESSION_ID, FIRST_SESSION
10
+ # Outputs: DASHBOARD_URL (exported for greeting.sh)
11
+
12
+ DASHBOARD_URL=""
13
+
14
+ # Skip conditions: first session, onboarding incomplete, no session ID
15
+ if [ "${FIRST_SESSION:-false}" = "true" ]; then
16
+ return 0 2>/dev/null || exit 0
17
+ fi
18
+ if [ -z "${EGREGORE_SESSION_ID:-}" ]; then
19
+ return 0 2>/dev/null || exit 0
20
+ fi
21
+
22
+ # --- Read gathered context ---
23
+ _DB_ADDRESSED=$(cat "$CTX_DIR/addressed_rich" 2>/dev/null || echo "[]")
24
+ _DB_ADDR_COUNT=$(echo "$_DB_ADDRESSED" | jq 'length' 2>/dev/null || echo "0")
25
+ _DB_QUESTS=$(cat "$CTX_DIR/quests" 2>/dev/null || echo "[]")
26
+ _DB_QUEST_COUNT=$(echo "$_DB_QUESTS" | jq 'length' 2>/dev/null || echo "0")
27
+ _DB_TEAM=$(cat "$CTX_DIR/team" 2>/dev/null || echo "[]")
28
+ _DB_TEAM_COUNT=$(echo "$_DB_TEAM" | jq 'length' 2>/dev/null || echo "0")
29
+ _DB_LIFECYCLE=$(cat "$CTX_DIR/lifecycle" 2>/dev/null || echo '{}')
30
+ _DB_MERGED_COUNT=$(echo "$_DB_LIFECYCLE" | jq '.merged_prs.values // [] | length' 2>/dev/null || echo "0")
31
+ _DB_IMPL_COUNT=$(echo "$_DB_LIFECYCLE" | jq '.implemented_handoffs.values // [] | length' 2>/dev/null || echo "0")
32
+ _DB_PULSE=$(cat "$CTX_DIR/pulse_brief" 2>/dev/null || echo '{}')
33
+ _DB_PULSE_BRIEF=$(echo "$_DB_PULSE" | jq -r '.brief // empty' 2>/dev/null || true)
34
+
35
+ # Skip if all context is empty — nothing worth showing
36
+ _DB_HAS_CONTENT="false"
37
+ if [ "$_DB_ADDR_COUNT" -gt 0 ] 2>/dev/null; then _DB_HAS_CONTENT="true"; fi
38
+ if [ "$_DB_QUEST_COUNT" -gt 0 ] 2>/dev/null; then _DB_HAS_CONTENT="true"; fi
39
+ if [ "$_DB_TEAM_COUNT" -gt 0 ] 2>/dev/null; then _DB_HAS_CONTENT="true"; fi
40
+ if [ "$_DB_MERGED_COUNT" -gt 0 ] 2>/dev/null; then _DB_HAS_CONTENT="true"; fi
41
+ if [ "$_DB_IMPL_COUNT" -gt 0 ] 2>/dev/null; then _DB_HAS_CONTENT="true"; fi
42
+
43
+ if [ "$_DB_HAS_CONTENT" = "false" ]; then
44
+ return 0 2>/dev/null || exit 0
45
+ fi
46
+
47
+ # --- Compile markdown ---
48
+ _DB_DATE=$(date +%Y-%m-%d)
49
+ _DB_ORG=$(jq -r '.org_name // "Egregore"' "$SCRIPT_DIR/egregore.json" 2>/dev/null || echo "Egregore")
50
+ _DB_MD="/tmp/egregore-dashboard-${EGREGORE_SESSION_ID}.md"
51
+ mkdir -p /tmp
52
+
53
+ {
54
+ printf '# Session Dashboard — %s\n\n' "$_DB_ORG"
55
+
56
+ # --- Pulse Brief (first — sets the tone) ---
57
+ if [ -n "$_DB_PULSE_BRIEF" ]; then
58
+ printf '## Pulse\n\n'
59
+ printf '✦ %s\n\n' "$_DB_PULSE_BRIEF"
60
+ fi
61
+
62
+ # --- Handoffs & Asks (dense format matching terminal TUI) ---
63
+ if [ "$_DB_ADDR_COUNT" -gt 0 ] 2>/dev/null; then
64
+ printf '## Handoffs & Asks\n\n'
65
+ _DB_NOW_SEC=$(date +%s)
66
+ echo "$_DB_ADDRESSED" | jq -r '.[] | "\(.author // "unknown")\t\(.topic // "Untitled")\t\(.date // "--")"' 2>/dev/null | while IFS=$'\t' read -r _HF_AUTHOR _HF_TOPIC _HF_DATE; do
67
+ [ -z "$_HF_AUTHOR" ] && continue
68
+ # Compute relative time
69
+ _HF_DATE_ONLY="${_HF_DATE%%T*}"
70
+ _HF_EPOCH=$(date -j -f "%Y-%m-%d" "$_HF_DATE_ONLY" +%s 2>/dev/null || \
71
+ date -d "$_HF_DATE_ONLY" +%s 2>/dev/null || echo "0")
72
+ _HF_DAYS=$(( (_DB_NOW_SEC - _HF_EPOCH) / 86400 ))
73
+ if [ "$_HF_DAYS" -le 0 ] 2>/dev/null; then _HF_AGO="today"
74
+ elif [ "$_HF_DAYS" -eq 1 ] 2>/dev/null; then _HF_AGO="yesterday"
75
+ else _HF_AGO="${_HF_DAYS}d ago"
76
+ fi
77
+ _HF_NAME_LC=$(printf '%s' "$_HF_AUTHOR" | awk '{print tolower($1)}')
78
+ printf '● %s → you: %s (%s)\n\n' "$_HF_NAME_LC" "$_HF_TOPIC" "$_HF_AGO"
79
+ done
80
+ fi
81
+
82
+ # --- Team Activity (right after handoffs, recent only) ---
83
+ if [ "$_DB_TEAM_COUNT" -gt 0 ] 2>/dev/null; then
84
+ _DB_NOW=$(date +%s)
85
+ _DB_RECENCY=$((7 * 86400))
86
+ _DB_TEAM_FILTERED=$(echo "$_DB_TEAM" | jq --argjson now "$_DB_NOW" --argjson window "$_DB_RECENCY" \
87
+ '[.[] | select(.last_seen_sort > 0 and ($now - .last_seen_sort) < $window)]' 2>/dev/null || echo "[]")
88
+ _DB_TEAM_FCOUNT=$(echo "$_DB_TEAM_FILTERED" | jq 'length' 2>/dev/null || echo "0")
89
+ if [ "$_DB_TEAM_FCOUNT" -gt 0 ] 2>/dev/null; then
90
+ printf '## Team Activity\n\n'
91
+ printf '| Name | Last Seen | Working On |\n'
92
+ printf '|------|-----------|------------|\n'
93
+ echo "$_DB_TEAM_FILTERED" | jq -r '.[] | "| \(.name) | \(.last_seen) | \(.branches | join(", ")) |"' 2>/dev/null || true
94
+ printf '\n'
95
+ fi
96
+ fi
97
+
98
+ # --- What Changed ---
99
+ if [ "$_DB_MERGED_COUNT" -gt 0 ] 2>/dev/null || [ "$_DB_IMPL_COUNT" -gt 0 ] 2>/dev/null; then
100
+ printf '## What Changed\n\n'
101
+ if [ "$_DB_MERGED_COUNT" -gt 0 ] 2>/dev/null; then
102
+ echo "$_DB_LIFECYCLE" | jq -r '.merged_prs.values[]? | "- ✓ PR #\(.[0]) \(.[2] // .[1] // "")"' 2>/dev/null || true
103
+ fi
104
+ if [ "$_DB_IMPL_COUNT" -gt 0 ] 2>/dev/null; then
105
+ echo "$_DB_LIFECYCLE" | jq -r '.implemented_handoffs.values[]? | "- ✓ \(.[1] // "someone") worked on: \(.[0] // "handoff")"' 2>/dev/null || true
106
+ fi
107
+ printf '\n'
108
+ fi
109
+
110
+ # --- Active Quests (bottom — reference, exclude meta entries) ---
111
+ if [ "$_DB_QUEST_COUNT" -gt 0 ] 2>/dev/null; then
112
+ _DB_QUESTS_FILTERED=$(echo "$_DB_QUESTS" | jq '[.[] | select(. != "_template" and . != "index")][:10]' 2>/dev/null || echo "[]")
113
+ _DB_QF_COUNT=$(echo "$_DB_QUESTS_FILTERED" | jq 'length' 2>/dev/null || echo "0")
114
+ if [ "$_DB_QF_COUNT" -gt 0 ] 2>/dev/null; then
115
+ printf '## Active Quests\n\n'
116
+ echo "$_DB_QUESTS_FILTERED" | jq -r '.[] | "- \(.)"' 2>/dev/null || true
117
+ printf '\n'
118
+ fi
119
+ fi
120
+
121
+ } > "$_DB_MD" 2>/dev/null
122
+
123
+ # --- Publish markdown → HTML → hosted URL ---
124
+ # publish-artifact.sh handles HTML generation internally (npx egregore-artifacts)
125
+ # then uploads. Don't pre-generate HTML — that causes double-wrapping.
126
+ # https:// URLs are universally cmd+clickable across all terminal emulators.
127
+ # publish-artifact.sh may output npx progress lines before the URL — extract just the URL
128
+ _DB_PUBLISH_OUT=$(bash "$SCRIPT_DIR/bin/publish-artifact.sh" document "$_DB_MD" \
129
+ --title "Session Dashboard" \
130
+ --author "${GREETING_NAME:-$AUTHOR}" 2>/dev/null || true)
131
+ DASHBOARD_URL=$(echo "$_DB_PUBLISH_OUT" | grep -o 'https://[^ ]*' | tail -1)
132
+
133
+ # Fallback: generate HTML locally and use file:// URL (offline, no API key, etc.)
134
+ if [ -z "$DASHBOARD_URL" ]; then
135
+ _DB_HTML="/tmp/egregore-artifacts/dashboard-${EGREGORE_SESSION_ID}.html"
136
+ mkdir -p /tmp/egregore-artifacts
137
+ if npx egregore-artifacts document "$_DB_MD" --output "$_DB_HTML" --no-open >/dev/null 2>&1 && [ -f "$_DB_HTML" ]; then
138
+ DASHBOARD_URL="file://${_DB_HTML}"
139
+ fi
140
+ fi
141
+
142
+ rm -f "$_DB_MD"