create-egregore 0.19.33 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/assets/egregore-launcher.js +118 -13
  2. package/bin/cli.js +19 -3
  3. package/lib/connect-runtime.js +4 -2
  4. package/lib/local.js +15 -1
  5. package/lib/prime-runtime.js +208 -0
  6. package/lib/setup.js +45 -3
  7. package/package.json +4 -3
  8. package/runtime/codex/.codex/skills/ingest-notion/SKILL.md +29 -0
  9. package/runtime/codex/.codex/skills/notion-connect/SKILL.md +29 -0
  10. package/runtime/codex/.codex/skills/teams-connect/SKILL.md +29 -0
  11. package/runtime/codex/.codex/spec-manifest.json +3 -3
  12. package/runtime/codex/AGENTS.md +11 -4
  13. package/runtime/codex/bin/activity-data.sh +4 -8
  14. package/runtime/codex/bin/attendant.sh +13 -0
  15. package/runtime/codex/bin/autosave.sh +27 -1
  16. package/runtime/codex/bin/codex-render-spec.mjs +6 -0
  17. package/runtime/codex/bin/codex-session-start.sh +3 -1
  18. package/runtime/codex/bin/graph-batch.sh +25 -6
  19. package/runtime/codex/bin/graph-op.sh +5 -4
  20. package/runtime/codex/bin/graph.sh +31 -5
  21. package/runtime/codex/bin/ingest.py +81 -5
  22. package/runtime/codex/bin/lib/context.sh +26 -1
  23. package/runtime/codex/bin/lib/greeting.sh +4 -1
  24. package/runtime/codex/bin/lib/worktree-links.sh +25 -0
  25. package/runtime/codex/manifest.json +7 -1
  26. package/runtime/pi/.codex/skills/ingest-notion/SKILL.md +29 -0
  27. package/runtime/pi/.codex/skills/notion-connect/SKILL.md +29 -0
  28. package/runtime/pi/.codex/skills/teams-connect/SKILL.md +29 -0
  29. package/runtime/pi/.codex/spec-manifest.json +3 -3
  30. package/runtime/pi/.pi/APPEND_SYSTEM.md +3 -2
  31. package/runtime/pi/.pi/extensions/egregore.ts +17 -6
  32. package/runtime/pi/.pi/spec-manifest.json +2 -2
  33. package/runtime/pi/AGENTS.md +11 -4
  34. package/runtime/pi/bin/activity-data.sh +4 -8
  35. package/runtime/pi/bin/attendant.sh +13 -0
  36. package/runtime/pi/bin/autosave.sh +27 -1
  37. package/runtime/pi/bin/codex-render-spec.mjs +6 -0
  38. package/runtime/pi/bin/codex-session-start.sh +3 -1
  39. package/runtime/pi/bin/graph-batch.sh +25 -6
  40. package/runtime/pi/bin/graph-op.sh +5 -4
  41. package/runtime/pi/bin/graph.sh +31 -5
  42. package/runtime/pi/bin/ingest.py +81 -5
  43. package/runtime/pi/bin/lib/context.sh +26 -1
  44. package/runtime/pi/bin/lib/greeting.sh +4 -1
  45. package/runtime/pi/bin/lib/worktree-links.sh +25 -0
  46. package/runtime/pi/bin/pi-branch-policy.mjs +138 -1
  47. package/runtime/pi/bin/pi-product-workflows.mjs +35 -7
  48. package/runtime/pi/manifest.json +7 -1
  49. package/runtime/prime/.claude/context/notification-consent.md +66 -0
  50. package/runtime/prime/.codex/config.toml +17 -0
  51. package/runtime/prime/.codex/hooks/branch-guard.js +461 -0
  52. package/runtime/prime/.codex/hooks/search-hint.js +67 -0
  53. package/runtime/prime/.codex/hooks.json +30 -0
  54. package/runtime/prime/.codex/skills/activity/SKILL.md +82 -0
  55. package/runtime/prime/.codex/skills/announce/SKILL.md +67 -0
  56. package/runtime/prime/.codex/skills/archive/SKILL.md +45 -0
  57. package/runtime/prime/.codex/skills/branch/SKILL.md +29 -0
  58. package/runtime/prime/.codex/skills/checkup/SKILL.md +45 -0
  59. package/runtime/prime/.codex/skills/commit/SKILL.md +29 -0
  60. package/runtime/prime/.codex/skills/contribute/SKILL.md +29 -0
  61. package/runtime/prime/.codex/skills/create-skill/SKILL.md +29 -0
  62. package/runtime/prime/.codex/skills/dark-mode/SKILL.md +29 -0
  63. package/runtime/prime/.codex/skills/dashboard/SKILL.md +59 -0
  64. package/runtime/prime/.codex/skills/deep-reflect/SKILL.md +109 -0
  65. package/runtime/prime/.codex/skills/delete-user/SKILL.md +29 -0
  66. package/runtime/prime/.codex/skills/emissary/SKILL.md +45 -0
  67. package/runtime/prime/.codex/skills/handoff/SKILL.md +166 -0
  68. package/runtime/prime/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
  69. package/runtime/prime/.codex/skills/harvest/SKILL.md +131 -0
  70. package/runtime/prime/.codex/skills/ingest/SKILL.md +29 -0
  71. package/runtime/prime/.codex/skills/ingest-notion/SKILL.md +29 -0
  72. package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +45 -0
  73. package/runtime/prime/.codex/skills/invite/SKILL.md +44 -0
  74. package/runtime/prime/.codex/skills/issue/SKILL.md +45 -0
  75. package/runtime/prime/.codex/skills/me/SKILL.md +29 -0
  76. package/runtime/prime/.codex/skills/meeting/SKILL.md +45 -0
  77. package/runtime/prime/.codex/skills/note/SKILL.md +29 -0
  78. package/runtime/prime/.codex/skills/notion-connect/SKILL.md +29 -0
  79. package/runtime/prime/.codex/skills/onboarding/SKILL.md +45 -0
  80. package/runtime/prime/.codex/skills/pr/SKILL.md +29 -0
  81. package/runtime/prime/.codex/skills/pull/SKILL.md +29 -0
  82. package/runtime/prime/.codex/skills/push/SKILL.md +29 -0
  83. package/runtime/prime/.codex/skills/quest/SKILL.md +80 -0
  84. package/runtime/prime/.codex/skills/reflect/SKILL.md +45 -0
  85. package/runtime/prime/.codex/skills/review-pr/SKILL.md +45 -0
  86. package/runtime/prime/.codex/skills/save/SKILL.md +125 -0
  87. package/runtime/prime/.codex/skills/slack-connect/SKILL.md +29 -0
  88. package/runtime/prime/.codex/skills/summon/SKILL.md +45 -0
  89. package/runtime/prime/.codex/skills/sync-repos/SKILL.md +29 -0
  90. package/runtime/prime/.codex/skills/teams-connect/SKILL.md +29 -0
  91. package/runtime/prime/.codex/skills/telegram-connect/SKILL.md +29 -0
  92. package/runtime/prime/.codex/skills/telemetry/SKILL.md +29 -0
  93. package/runtime/prime/.codex/skills/test/SKILL.md +45 -0
  94. package/runtime/prime/.codex/skills/the-spiral/SKILL.md +84 -0
  95. package/runtime/prime/.codex/skills/tui-design/SKILL.md +45 -0
  96. package/runtime/prime/.codex/skills/tutorial/SKILL.md +45 -0
  97. package/runtime/prime/.codex/skills/update/SKILL.md +29 -0
  98. package/runtime/prime/.codex/skills/view/SKILL.md +156 -0
  99. package/runtime/prime/.codex/skills/view/agents/openai.yaml +4 -0
  100. package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +221 -0
  101. package/runtime/prime/.codex/skills/visual-explain/SKILL.md +45 -0
  102. package/runtime/prime/.codex/skills/wrap/SKILL.md +58 -0
  103. package/runtime/prime/.codex/spec-manifest.json +132 -0
  104. package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +393 -0
  105. package/runtime/prime/.prime/agent/extensions/egregore.ts +460 -0
  106. package/runtime/prime/.prime/agent/settings.json +4 -0
  107. package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +34 -0
  108. package/runtime/prime/.prime/agent/skills/egregore/pyproject.toml +13 -0
  109. package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +156 -0
  110. package/runtime/prime/.prime/agent/spec-manifest.json +12 -0
  111. package/runtime/prime/AGENTS.md +489 -0
  112. package/runtime/prime/bin/activity-data.sh +238 -0
  113. package/runtime/prime/bin/agent.sh +775 -0
  114. package/runtime/prime/bin/attendant.sh +707 -0
  115. package/runtime/prime/bin/autosave.sh +112 -0
  116. package/runtime/prime/bin/capture-reconcile.sh +69 -0
  117. package/runtime/prime/bin/capture-run.sh +341 -0
  118. package/runtime/prime/bin/codex-install-skills.sh +78 -0
  119. package/runtime/prime/bin/codex-render-spec.mjs +440 -0
  120. package/runtime/prime/bin/codex-session-start.sh +265 -0
  121. package/runtime/prime/bin/codex-skill-render.mjs +347 -0
  122. package/runtime/prime/bin/codex-sync-skills.sh +232 -0
  123. package/runtime/prime/bin/connect-refresh.sh +33 -0
  124. package/runtime/prime/bin/dashboard-data.sh +178 -0
  125. package/runtime/prime/bin/graph-batch.sh +79 -0
  126. package/runtime/prime/bin/graph-op.sh +1102 -0
  127. package/runtime/prime/bin/graph-wal.sh +229 -0
  128. package/runtime/prime/bin/graph.sh +223 -0
  129. package/runtime/prime/bin/handoff-pr-backfill.sh +86 -0
  130. package/runtime/prime/bin/handoff-run.sh +671 -0
  131. package/runtime/prime/bin/handoff-save-egregore.sh +212 -0
  132. package/runtime/prime/bin/index-handoff.sh +322 -0
  133. package/runtime/prime/bin/ingest-graph.sh +80 -0
  134. package/runtime/prime/bin/ingest-surface.html +495 -0
  135. package/runtime/prime/bin/ingest.py +756 -0
  136. package/runtime/prime/bin/ingest.sh +279 -0
  137. package/runtime/prime/bin/ingest_graph.py +856 -0
  138. package/runtime/prime/bin/ingest_surface.py +544 -0
  139. package/runtime/prime/bin/lib/artifact-id.sh +46 -0
  140. package/runtime/prime/bin/lib/config.sh +116 -0
  141. package/runtime/prime/bin/lib/context.sh +621 -0
  142. package/runtime/prime/bin/lib/dashboard-artifact.sh +242 -0
  143. package/runtime/prime/bin/lib/ensure-develop.sh +183 -0
  144. package/runtime/prime/bin/lib/git-sync.sh +406 -0
  145. package/runtime/prime/bin/lib/greeting.sh +519 -0
  146. package/runtime/prime/bin/lib/hash.sh +13 -0
  147. package/runtime/prime/bin/lib/identity.sh +213 -0
  148. package/runtime/prime/bin/lib/metrics.sh +374 -0
  149. package/runtime/prime/bin/lib/noncode.sh +31 -0
  150. package/runtime/prime/bin/lib/permissions.sh +106 -0
  151. package/runtime/prime/bin/lib/time.sh +56 -0
  152. package/runtime/prime/bin/lib/worktree-links.sh +79 -0
  153. package/runtime/prime/bin/notify.sh +636 -0
  154. package/runtime/prime/bin/person.py +630 -0
  155. package/runtime/prime/bin/person.sh +448 -0
  156. package/runtime/prime/bin/pi-branch-policy.mjs +312 -0
  157. package/runtime/prime/bin/pi-product-workflows.mjs +394 -0
  158. package/runtime/prime/bin/prime-render-spec.mjs +123 -0
  159. package/runtime/prime/bin/prime-session-start.sh +24 -0
  160. package/runtime/prime/bin/publish-artifact.sh +271 -0
  161. package/runtime/prime/bin/publish-references.sh +144 -0
  162. package/runtime/prime/bin/repo-state.sh +96 -0
  163. package/runtime/prime/bin/search.sh +240 -0
  164. package/runtime/prime/bin/session-autosave.sh +209 -0
  165. package/runtime/prime/bin/session-log.sh +199 -0
  166. package/runtime/prime/bin/settings.sh +304 -0
  167. package/runtime/prime/bin/telemetry.sh +391 -0
  168. package/runtime/prime/bin/worktree-create.sh +128 -0
  169. package/runtime/prime/bin/worktree-remove.sh +50 -0
  170. package/runtime/prime/bin/worktree.sh +198 -0
  171. package/runtime/prime/manifest.json +178 -0
@@ -0,0 +1,621 @@
1
+ # shellcheck shell=bash
2
+ # context.sh — Context gathering for session-start.sh
3
+ #
4
+ # Runs 11 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
+ # 11. Momentum metrics
17
+ #
18
+ # All subshells write to $CTX_DIR (a temp directory).
19
+ # Ends with `wait` + reading health results back.
20
+ #
21
+ # Inputs: SCRIPT_DIR, AUTHOR, LOCAL_MODE, STATE_FILE, CONFIG, ENV_FILE,
22
+ # HEALTH_GRAPH, HEALTH_TELEGRAM
23
+ # Outputs: CTX_DIR (populated), TIME_OF_DAY, HEALTH_GRAPH, HEALTH_TELEGRAM
24
+
25
+ CTX_DIR=$(mktemp -d)
26
+
27
+ source "$SCRIPT_DIR/bin/lib/handoff-meta.sh" >/dev/null 2>/dev/null || true
28
+
29
+ # Time of day
30
+ HOUR=$(date +%H)
31
+ if [ "$HOUR" -lt 12 ]; then TIME_OF_DAY="morning"
32
+ elif [ "$HOUR" -lt 17 ]; then TIME_OF_DAY="afternoon"
33
+ else TIME_OF_DAY="evening"
34
+ fi
35
+
36
+ # --- Attendant-baked seed ---------------------------------------------------
37
+ # bin/attendant.sh pre-bakes a context snapshot tar every warm cycle
38
+ # (_prebake_context). When the bake is fresh and its provenance matches, reuse
39
+ # its purely graph-bound results — todos, lifecycle, pulse, service health —
40
+ # and run every collector with local git/file inputs live below (handoffs,
41
+ # quests, activity, soul, addressed, questions, team presence, metrics), so
42
+ # everything file/git-derived reflects this instant. Purely graph-derived
43
+ # fields are then at most one warm cycle stale, and the boot path pays
44
+ # near-zero blocking graph round-trips (live collectors ride the warm cache).
45
+ #
46
+ # The seed is trusted only when ALL of these hold — otherwise fall back to a
47
+ # full live gather:
48
+ # - the tar extracts cleanly (atomic rename on the writer side means we see
49
+ # a whole bake or none, never a half-written directory)
50
+ # - .baked-author matches this session's author (shared machines)
51
+ # - .baked-local-mode matches this session's mode (a connected bake must
52
+ # not leak graph data into a session that switched to local)
53
+ # - .baked-config matches this checkout's egregore.json (org/API repoint,
54
+ # or a worktree branch carrying a different config)
55
+ # - .baked-scope matches the EFFECTIVE endpoint/credential fingerprint
56
+ # (EGREGORE_API_URL/KEY overrides can point at a different tenant than
57
+ # the committed config names)
58
+ # - .baked-schema matches this checkout's context.sh revision (a bake from
59
+ # another collector schema may be missing or misformatting files)
60
+ # - .baked-ts is numeric, not in the future, and younger than 15min
61
+ # - every seeded collector's file actually landed in CTX_DIR (a partial
62
+ # copy must not suppress live collectors)
63
+ CTX_SEED_USED="false"
64
+ if [ -n "${CTX_SEED_TAR:-}" ] && [ -f "${CTX_SEED_TAR:-}" ]; then
65
+ _SEED_TMP=$(mktemp -d)
66
+ if tar -xf "$CTX_SEED_TAR" -C "$_SEED_TMP" 2>/dev/null; then
67
+ _SEED_TS=$(cat "$_SEED_TMP/.baked-ts" 2>/dev/null || echo 0)
68
+ case "$_SEED_TS" in ''|*[!0-9]*) _SEED_TS=0 ;; esac
69
+ _SEED_AUTHOR=$(cat "$_SEED_TMP/.baked-author" 2>/dev/null || echo "")
70
+ _SEED_MODE=$(cat "$_SEED_TMP/.baked-local-mode" 2>/dev/null || echo "")
71
+ _SEED_CONFIG=$(cat "$_SEED_TMP/.baked-config" 2>/dev/null || echo "")
72
+ _SEED_SCOPE=$(cat "$_SEED_TMP/.baked-scope" 2>/dev/null || echo "")
73
+ _SEED_SCHEMA=$(cat "$_SEED_TMP/.baked-schema" 2>/dev/null || echo "")
74
+ _NOW_CONFIG=$(cksum "${CONFIG:-$SCRIPT_DIR/egregore.json}" 2>/dev/null | cut -d' ' -f1)
75
+ _NOW_SCHEMA=$(cksum "$SCRIPT_DIR/bin/lib/context.sh" 2>/dev/null | cut -d' ' -f1)
76
+ # Effective endpoint/credential, mirroring bin/graph.sh's resolution order
77
+ # (process env → .env → committed api_url).
78
+ _NOW_URL="${EGREGORE_API_URL:-$(grep '^EGREGORE_API_URL=' "${ENV_FILE:-$SCRIPT_DIR/.env}" 2>/dev/null | cut -d'=' -f2- || true)}"
79
+ _NOW_KEY="${EGREGORE_API_KEY:-$(grep '^EGREGORE_API_KEY=' "${ENV_FILE:-$SCRIPT_DIR/.env}" 2>/dev/null | cut -d'=' -f2- || true)}"
80
+ [ -n "$_NOW_URL" ] || _NOW_URL=$(jq -r '.api_url // empty' "${CONFIG:-$SCRIPT_DIR/egregore.json}" 2>/dev/null)
81
+ _NOW_SCOPE=$(echo -n "${_NOW_URL}|${_NOW_KEY}" | cksum | cut -d' ' -f1)
82
+ _SEED_AGE=$(( $(date +%s) - _SEED_TS ))
83
+ if [ -n "$_SEED_AUTHOR" ] && [ "$_SEED_AUTHOR" = "$AUTHOR" ] \
84
+ && [ "$_SEED_MODE" = "${LOCAL_MODE:-false}" ] \
85
+ && [ -n "$_SEED_CONFIG" ] && [ "$_SEED_CONFIG" = "$_NOW_CONFIG" ] \
86
+ && [ -n "$_SEED_SCOPE" ] && [ "$_SEED_SCOPE" = "$_NOW_SCOPE" ] \
87
+ && [ -n "$_SEED_SCHEMA" ] && [ "$_SEED_SCHEMA" = "$_NOW_SCHEMA" ] \
88
+ && [ "$_SEED_AGE" -ge 0 ] && [ "$_SEED_AGE" -lt 900 ]; then
89
+ if cp "$_SEED_TMP"/* "$CTX_DIR/" 2>/dev/null; then
90
+ # Completeness: every collector this seed replaces must be present.
91
+ CTX_SEED_USED="true"
92
+ for _SEED_REQ in todos lifecycle pulse_brief graph_health telegram_health; do
93
+ [ -f "$CTX_DIR/$_SEED_REQ" ] || { CTX_SEED_USED="false"; break; }
94
+ done
95
+ fi
96
+ fi
97
+ fi
98
+ rm -rf "$_SEED_TMP"
99
+ fi
100
+
101
+ # 1. Recent handoffs (background)
102
+ (
103
+ JSON="[]"
104
+ if [ -d "$SCRIPT_DIR/memory/handoffs" ]; then
105
+ 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)
106
+ JSON="["
107
+ FIRST=true
108
+ for F in $FILES; do
109
+ [ -z "$F" ] && continue
110
+ NAME=$(basename "$F" .md)
111
+ PREVIEW=$(head -c 120 "$F" 2>/dev/null | tr '\n' ' ' | sed 's/"/\\"/g')
112
+ $FIRST || JSON="$JSON,"
113
+ JSON="$JSON{\"name\":\"$NAME\",\"preview\":\"$PREVIEW\"}"
114
+ FIRST=false
115
+ done
116
+ JSON="$JSON]"
117
+ fi
118
+ echo "$JSON" > "$CTX_DIR/handoffs"
119
+ ) &
120
+
121
+ # 2. Quests (background)
122
+ (
123
+ JSON="[]"
124
+ if [ -d "$SCRIPT_DIR/memory/quests" ]; then
125
+ JSON="["
126
+ FIRST=true
127
+ for F in "$SCRIPT_DIR/memory/quests/"*.md; do
128
+ [ -e "$F" ] || continue
129
+ case "$F" in *draft*) continue ;; esac
130
+ NAME=$(basename "$F" .md)
131
+ $FIRST || JSON="$JSON,"
132
+ JSON="$JSON\"$NAME\""
133
+ FIRST=false
134
+ done
135
+ JSON="$JSON]"
136
+ fi
137
+ echo "$JSON" > "$CTX_DIR/quests"
138
+ ) &
139
+
140
+ # 3. User's last activity (background)
141
+ (
142
+ git log --author="$AUTHOR" --format="%ar|%s" -1 2>/dev/null > "$CTX_DIR/activity" || echo "" > "$CTX_DIR/activity"
143
+ ) &
144
+
145
+ # 3b. Personal todos from graph (background; seeded)
146
+ [ "$CTX_SEED_USED" != "true" ] && (
147
+ _API_URL=$(jq -r '.api_url // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
148
+ _API_KEY=$(grep '^EGREGORE_API_KEY=' "${ENV_FILE:-$SCRIPT_DIR/.env}" 2>/dev/null | cut -d'=' -f2-)
149
+ if [ -n "$_API_URL" ] && [ -n "$_API_KEY" ]; then
150
+ TODO_RAW=$(bash "$SCRIPT_DIR/bin/graph.sh" query \
151
+ "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" \
152
+ "$(jq -n --arg me "$AUTHOR" '{me: $me}')" 2>/dev/null || echo "")
153
+ if [ -n "$TODO_RAW" ]; then
154
+ echo "$TODO_RAW" | jq '[.values[] | {text: .[0], priority: (.[1] // 0), status: .[2], created: .[3], quest: (.[4] // "")}]' 2>/dev/null || echo "[]"
155
+ else
156
+ echo "[]"
157
+ fi
158
+ else
159
+ echo "[]"
160
+ fi
161
+ ) > "$CTX_DIR/todos" 2>/dev/null &
162
+
163
+ # 4. Team presence — last seen + active branches (background)
164
+ # Always live, even when seeded: its branch/file inputs are local and cheap
165
+ # (single awk/jq passes), and stale "last seen 2m ago" lines are the most
166
+ # user-visible form of drift. The one graph query inside rides the warm cache.
167
+ (
168
+ # --- Read config inside subshell ---
169
+ _API_URL=$(jq -r '.api_url // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
170
+ _API_KEY=$(grep '^EGREGORE_API_KEY=' "${ENV_FILE:-$SCRIPT_DIR/.env}" 2>/dev/null | cut -d'=' -f2-)
171
+
172
+ # --- Graph: last-seen per person (excluding self) ---
173
+ GRAPH_DATA="[]"
174
+ if [ -n "$_API_URL" ] && [ -n "$_API_KEY" ]; then
175
+ # String-compare the temporals: `datetime(s.date)` is an invalid cast when
176
+ # s.date is a Date (Neo.ClientError.Statement.TypeError), which made this
177
+ # query fail silently on every boot — GRAPH_DATA stayed empty, the greeting
178
+ # fell back to git/file presence, and the FILE_PRESENCE scan below ran in
179
+ # connected mode too. ISO-8601 strings order lexicographically, so max()
180
+ # over toString() is correct across DateTime, Date, and legacy string rows.
181
+ CYPHER="MATCH (s:Session)-[:BY]->(p:Person) WHERE p.github <> \$me RETURN p.name AS name, max(coalesce(toString(s.startedAt), toString(s.date))) AS lastSeen ORDER BY lastSeen DESC"
182
+ GRAPH_RAW=$(bash "$SCRIPT_DIR/bin/graph.sh" query "$CYPHER" "$(jq -n --arg me "$AUTHOR" '{me: $me}')" 2>/dev/null || echo "")
183
+ if [ -n "$GRAPH_RAW" ]; then
184
+ GRAPH_DATA=$(echo "$GRAPH_RAW" | jq '[.values[] | {name: .[0], lastSeen: .[1]}]' 2>/dev/null || echo "[]")
185
+ fi
186
+ fi
187
+
188
+ # --- Git: branch tip commit dates per person (primary recency signal) ---
189
+ # git for-each-ref is fast and gives the actual last commit date per branch.
190
+ # Single awk pass — the previous per-branch cut+tr loop spawned 3 processes
191
+ # per ref, and busy orgs carry hundreds of origin/dev/* refs. Refs arrive
192
+ # sorted by -committerdate, so the first epoch per author is their newest.
193
+ GIT_COMMIT_DATA="{}"
194
+ GIT_REF_DATA=$(git -C "$SCRIPT_DIR" for-each-ref --sort=-committerdate \
195
+ --format='%(refname:short)|%(committerdate:unix)' refs/remotes/origin/dev/ 2>/dev/null || echo "")
196
+ if [ -n "$GIT_REF_DATA" ]; then
197
+ GIT_COMMIT_DATA=$(printf '%s\n' "$GIT_REF_DATA" | awk -F'|' '
198
+ {
199
+ p = $1; sub(/^origin\/dev\//, "", p)
200
+ split(p, seg, "/"); a = tolower(seg[1])
201
+ if (a != "" && !(a in best)) best[a] = $2
202
+ }
203
+ END { for (a in best) printf "%s|%s\n", a, best[a] }
204
+ ' 2>/dev/null | jq -Rn '
205
+ [inputs | split("|") | {(.[0]): .[1]}]
206
+ | add // {}
207
+ ' 2>/dev/null || echo "{}")
208
+ [ -z "$GIT_COMMIT_DATA" ] && GIT_COMMIT_DATA="{}"
209
+ fi
210
+
211
+ # --- Git: active dev/* branches (excluding self) ---
212
+ SELF_LC=$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]')
213
+ SELF_DISPLAY_LC=""
214
+ if [ -f "$SCRIPT_DIR/.egregore-state.json" ]; then
215
+ SELF_DISPLAY_LC=$(jq -r '.display_name // empty' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null | tr '[:upper:]' '[:lower:]')
216
+ fi
217
+ BRANCH_DATA=$(git -C "$SCRIPT_DIR" branch -r --format='%(refname:short)' 2>/dev/null | grep "^origin/dev/" | sed 's|^origin/dev/||' || echo "")
218
+
219
+ BRANCH_MAP="{}"
220
+ if [ -n "$BRANCH_DATA" ]; then
221
+ # Single awk pass — the previous per-branch echo+tr loop spawned 2-4
222
+ # processes per branch over hundreds of refs.
223
+ BRANCH_MAP=$(printf '%s\n' "$BRANCH_DATA" | awk -v self="$SELF_LC" -v selfd="$SELF_DISPLAY_LC" '
224
+ {
225
+ if (split($0, seg, "/") < 2) next
226
+ a = tolower(seg[1])
227
+ if (a == "" || a == self || (selfd != "" && a == selfd)) next
228
+ h = $0; sub(/^[^\/]*\//, "", h); gsub(/-/, " ", h)
229
+ printf "%s|%s\n", a, h
230
+ }
231
+ ' 2>/dev/null | jq -Rn '
232
+ [inputs | split("|") | {author: .[0], branch: (.[1:] | join("|"))}]
233
+ | group_by(.author)
234
+ | map({(.[0].author): [.[] | .branch]})
235
+ | add // {}
236
+ ' 2>/dev/null || echo "{}")
237
+ [ -z "$BRANCH_MAP" ] && BRANCH_MAP="{}"
238
+ fi
239
+
240
+ # --- Local-mode presence: scan session + wrap files for lastSeen ---
241
+ # When graph has no data, these files provide temporal presence information.
242
+ # Single awk pass over all candidate files — the previous per-file
243
+ # grep+sed loop spawned 2-3 processes per file (~60 files) on every boot.
244
+ FILE_PRESENCE="{}"
245
+ if [ "$GRAPH_DATA" = "[]" ]; then
246
+ FP_FILES=$(for DIR in "$SCRIPT_DIR/memory/sessions" "$SCRIPT_DIR/memory/wraps" "$SCRIPT_DIR/memory/handoffs"; do
247
+ [ -d "$DIR" ] || continue
248
+ find -L "$DIR" -name '*.md' -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -20
249
+ done)
250
+ if [ -n "$FP_FILES" ]; then
251
+ FILE_PRESENCE=$(printf '%s\n' "$FP_FILES" | tr '\n' '\0' | xargs -0 awk '
252
+ function flush() { if (a != "") printf "%s|%s\n", a, d }
253
+ FNR == 1 { if (NR != 1) flush(); a=""; d="" }
254
+ a == "" && /^\*\*Author\*\*:/ { s = $0; sub(/^\*\*Author\*\*:[ \t]*/, "", s); a = s }
255
+ d == "" && /^\*\*Date\*\*:/ { s = $0; sub(/^\*\*Date\*\*:[ \t]*/, "", s); d = s }
256
+ END { flush() }
257
+ ' 2>/dev/null \
258
+ | sort -t'|' -k1,1 -k2,2r | sort -t'|' -k1,1 -u \
259
+ | awk -F'|' '{ print tolower($1) "|" $2 }' \
260
+ | jq -Rn '
261
+ [inputs | split("|") | {(.[0]): .[1]}]
262
+ | add // {}
263
+ ' 2>/dev/null || echo "{}")
264
+ [ -z "$FILE_PRESENCE" ] && FILE_PRESENCE="{}"
265
+ fi
266
+ fi
267
+
268
+ # --- "Working on": latest session/handoff/wrap topic per person (file-based) ---
269
+ # Gives every team surface (greeting "around", dashboard) something legible in
270
+ # local mode, where git branches are usually empty for teammates.
271
+ # Single awk pass — the previous per-file grep+sed loop spawned ~6 processes
272
+ # per file over ~90 files on every boot.
273
+ _WM_TAB=$(printf '\t')
274
+ WM_FILES=$(for DIR in "$SCRIPT_DIR/memory/sessions" "$SCRIPT_DIR/memory/handoffs" "$SCRIPT_DIR/memory/wraps"; do
275
+ [ -d "$DIR" ] || continue
276
+ find -L "$DIR" -name '*.md' -not -name 'index*' -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -30
277
+ done)
278
+ WORK_MAP="{}"
279
+ if [ -n "$WM_FILES" ]; then
280
+ WORK_MAP=$(printf '%s\n' "$WM_FILES" | tr '\n' '\0' | xargs -0 awk '
281
+ function flush() { if (a != "" && t != "") printf "%s\t%s\t%s\n", a, d, t }
282
+ FNR == 1 { if (NR != 1) flush(); a=""; d=""; t="" }
283
+ a == "" && /^\*\*Author\*\*:/ { s = $0; sub(/^\*\*Author\*\*:[ \t]*/, "", s); a = s }
284
+ d == "" && /^\*\*Date\*\*:/ { s = $0; sub(/^\*\*Date\*\*:[ \t]*/, "", s); d = s }
285
+ t == "" && /^# / {
286
+ s = $0; sub(/^#[ \t]*/, "", s)
287
+ sub(/^Session:[ \t]*/, "", s); sub(/^Handoff:[ \t]*/, "", s); sub(/^Wrap:[ \t]*/, "", s)
288
+ t = s
289
+ }
290
+ END { flush() }
291
+ ' 2>/dev/null \
292
+ | sort -t"$_WM_TAB" -k1,1 -k2,2r | sort -t"$_WM_TAB" -k1,1 -u \
293
+ | awk -F"$_WM_TAB" '{ print tolower($1) "\t" $3 }' \
294
+ | jq -Rn '[inputs | split("\t") | select(length >= 2) | {(.[0]): (.[1] // "")}] | add // {}' 2>/dev/null || echo "{}")
295
+ [ -z "$WORK_MAP" ] && WORK_MAP="{}"
296
+ fi
297
+
298
+ # --- Merge git commits + graph + file presence + branches into presence array ---
299
+ # One jq pass. The previous version looped over the name union in bash,
300
+ # spawning ~8 jq/date processes per teammate — at 16+ teammates that was the
301
+ # single largest block of session-start's boot time (~5s). All epoch parsing,
302
+ # relative-time formatting, merging, and sorting now happens inside jq.
303
+ # Git commit date is authoritative (actual work shipped); graph/file presence
304
+ # are fallbacks only (session opens ≠ real activity).
305
+ NOW_EPOCH=$(date +%s)
306
+ PRESENCE=$(jq -nc \
307
+ --argjson graph "$GRAPH_DATA" \
308
+ --argjson git "$GIT_COMMIT_DATA" \
309
+ --argjson branches "$BRANCH_MAP" \
310
+ --argjson files "$FILE_PRESENCE" \
311
+ --argjson work "$WORK_MAP" \
312
+ --arg now "$NOW_EPOCH" --arg self "$SELF_LC" --arg selfdisp "$SELF_DISPLAY_LC" '
313
+ def iso2e:
314
+ if . == null or . == "" then 0
315
+ else (sub("Z$"; "") | sub("\\+00:00$"; "") | sub("\\.[0-9]+"; "")
316
+ | (if test("T") then . else . + "T00:00:00" end)
317
+ | (try (strptime("%Y-%m-%dT%H:%M:%S") | mktime) catch 0))
318
+ end;
319
+ def day: (try strflocaltime("%Y-%m-%d") catch (gmtime | strftime("%Y-%m-%d")));
320
+ def rel($nowe): . as $e |
321
+ if $e <= 0 then "--"
322
+ else ($nowe - $e) as $d |
323
+ if $d < 60 then "just now"
324
+ elif $d < 3600 then "\($d / 60 | floor)m ago"
325
+ elif $d < 86400 then "\($d / 3600 | floor)h ago"
326
+ elif ($e | day) == ($nowe | day) then "today"
327
+ else
328
+ ((try (($nowe | day | strptime("%Y-%m-%d") | mktime)
329
+ - ($e | day | strptime("%Y-%m-%d") | mktime)) catch $d) / 86400
330
+ | floor | if . < 1 then 1 else . end) as $days |
331
+ if $days == 1 then "yesterday" else "\($days)d ago" end
332
+ end
333
+ end;
334
+ ($now | tonumber) as $nowe |
335
+ ($graph
336
+ | map({key: ((.name // "") | ascii_downcase), value: (.lastSeen // "")})
337
+ | group_by(.key)
338
+ | map({key: .[0].key, value: (map(.value) | max)})
339
+ | from_entries) as $g |
340
+ ([($g, $files, $branches, $git) | keys[]] | map(ascii_downcase) | unique
341
+ | map(select(. != "" and . != $self and . != $selfdisp))) as $names |
342
+ [ $names[] | . as $n |
343
+ (($git[$n] // "0") | (try tonumber catch 0)) as $ge |
344
+ (if $ge > 0 then $ge
345
+ else ((($g[$n] // "") | iso2e) as $gge |
346
+ if $gge > 0 then $gge else (($files[$n] // "") | iso2e) end)
347
+ end) as $epoch |
348
+ ($branches[$n] // []) as $b |
349
+ { name: $n,
350
+ last_seen: ($epoch | rel($nowe)),
351
+ last_seen_sort: $epoch,
352
+ branches: (if ($b | length) > 2 then ($b[0:2] + ["+\(($b | length) - 2) more"]) else $b end),
353
+ working_on: ($work[$n] // "") }
354
+ ] | sort_by(-.last_seen_sort)
355
+ ' 2>/dev/null || echo "[]")
356
+ echo "${PRESENCE:-[]}" > "$CTX_DIR/team"
357
+ ) &
358
+
359
+ # 5. Soul self-summary (background)
360
+ (
361
+ SUMMARY=""
362
+ if [ -f "$SCRIPT_DIR/egregore.md" ]; then
363
+ SUMMARY=$(sed -n '/^## Self-Summary/,/^##/p' "$SCRIPT_DIR/egregore.md" | sed '1d;/^##/d' | sed 's/^[[:space:]]*//' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
364
+ fi
365
+ echo "$SUMMARY" > "$CTX_DIR/soul_summary"
366
+ ) &
367
+
368
+ # 6. Handoffs addressed to user (background, enriched with author/topic)
369
+ (
370
+ JSON="[]"
371
+ RICH="[]"
372
+ if [ -d "$SCRIPT_DIR/memory/handoffs" ]; then
373
+ # One definition everywhere: in connected mode the list comes from the
374
+ # graph's open-handoffs named read (open = pending/unread/read/claimed,
375
+ # matched by name/github/aliases) — the same definition the dashboard
376
+ # uses. The file grep below stays as the local-mode/offline fallback.
377
+ ADDRESSED=""
378
+ if [ "${LOCAL_MODE:-false}" != "true" ]; then
379
+ _GRAPH_LIST=$(bash "$SCRIPT_DIR/bin/graph-op.sh" open-handoffs "$AUTHOR" 5 2>/dev/null || true)
380
+ if echo "$_GRAPH_LIST" | jq -e '.values | length > 0' >/dev/null 2>&1; then
381
+ ADDRESSED=$(echo "$_GRAPH_LIST" | jq -r '.values[][0]' 2>/dev/null | awk '!seen[$0]++' | while IFS= read -r _SID; do
382
+ # Two on-disk layouts: dated subdirs (YYYY-MM/DD-slug.md, the id's
383
+ # own shape) and flat files at the handoffs root. Probe both, else
384
+ # a graph-listed handoff silently vanishes from the greeting.
385
+ _MONTH="${_SID:0:7}"
386
+ _REST="${_SID:8}"
387
+ _F="$SCRIPT_DIR/memory/handoffs/$_MONTH/$_REST.md"
388
+ if [ -f "$_F" ]; then
389
+ echo "$_F"
390
+ elif [ -f "$SCRIPT_DIR/memory/handoffs/$_SID.md" ]; then
391
+ echo "$SCRIPT_DIR/memory/handoffs/$_SID.md"
392
+ fi
393
+ done | head -5)
394
+ fi
395
+ fi
396
+ # Match github username, display name, github_name first word, and people-file name
397
+ # Handoff files may use "to: name", "**To**: name", or "To: name"
398
+ _DISPLAY=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
399
+ _GH_NAME=$(jq -r '.github_name // empty' "$STATE_FILE" 2>/dev/null)
400
+ _FIRST_NAME=$(echo "$_GH_NAME" | awk '{print $1}')
401
+ # Also check people file: memory/people/{author}.md has "# Name" on first line
402
+ _PEOPLE_NAME=""
403
+ _PEOPLE_FILE="$SCRIPT_DIR/memory/people/$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]').md"
404
+ [ -f "$_PEOPLE_FILE" ] && _PEOPLE_NAME=$(sed -n 's/^# *//p' "$_PEOPLE_FILE" 2>/dev/null | head -1)
405
+ _GREP_PAT="[Tt]o[*]*: *$AUTHOR\|[Tt]o[*]*:$AUTHOR"
406
+ for _VARIANT in "$_DISPLAY" "$_FIRST_NAME" "$_PEOPLE_NAME"; do
407
+ [ -z "$_VARIANT" ] && continue
408
+ [ "$_VARIANT" = "$AUTHOR" ] && continue
409
+ # Skip if already in pattern (avoid duplicates)
410
+ echo "$_GREP_PAT" | grep -qF "$_VARIANT" && continue
411
+ _GREP_PAT="$_GREP_PAT\|[Tt]o[*]*: *$_VARIANT\|[Tt]o[*]*:$_VARIANT"
412
+ done
413
+ if [ -z "$ADDRESSED" ]; then
414
+ ADDRESSED=$(grep -rli "$_GREP_PAT" "$SCRIPT_DIR/memory/handoffs/" 2>/dev/null | sort -r | head -5 || true)
415
+ fi
416
+ JSON="["
417
+ RICH="["
418
+ FIRST=true
419
+ for AF in $ADDRESSED; do
420
+ [ -z "$AF" ] && continue
421
+ AF_NAME=$(basename "$AF" .md)
422
+ AF_AUTHOR=$(eg_handoff_author "$AF")
423
+ AF_TOPIC=$(eg_handoff_topic "$AF")
424
+ AF_DATE=$(eg_handoff_date "$AF")
425
+ # Extract repo state table if present
426
+ AF_REPO_STATE="[]"
427
+ AF_REPO_TABLE=$(awk '/^## Repo State/{found=1; next} found && /^#/{exit} found && /^\|[^-]/ && !/^\| Repo/{print}' "$AF" 2>/dev/null || true)
428
+ if [ -n "$AF_REPO_TABLE" ]; then
429
+ AF_REPO_STATE=$(echo "$AF_REPO_TABLE" | while IFS='|' read -r _ R_REPO R_BRANCH R_PR R_BASE _; do
430
+ R_REPO=$(echo "$R_REPO" | xargs 2>/dev/null || true)
431
+ R_BRANCH=$(echo "$R_BRANCH" | xargs 2>/dev/null || true)
432
+ R_PR=$(echo "$R_PR" | xargs 2>/dev/null | sed 's/^#//' | sed 's/—//' || true)
433
+ R_BASE=$(echo "$R_BASE" | xargs 2>/dev/null || true)
434
+ [ -z "$R_REPO" ] && continue
435
+ if [ -n "$R_PR" ] && [ "$R_PR" -eq "$R_PR" ] 2>/dev/null; then
436
+ printf '{"repo":"%s","branch":"%s","pr":%s,"base":"%s"}\n' "$R_REPO" "$R_BRANCH" "$R_PR" "$R_BASE"
437
+ else
438
+ printf '{"repo":"%s","branch":"%s","pr":null,"base":"%s"}\n' "$R_REPO" "$R_BRANCH" "$R_BASE"
439
+ fi
440
+ done | jq -sc '.' 2>/dev/null || echo "[]")
441
+ fi
442
+ $FIRST || JSON="$JSON,"
443
+ $FIRST || RICH="$RICH,"
444
+ JSON="$JSON\"$AF_NAME\""
445
+ AF_ENTRY=$(jq -cn \
446
+ --arg name "$AF_NAME" \
447
+ --arg author "$AF_AUTHOR" \
448
+ --arg topic "$AF_TOPIC" \
449
+ --arg date "$AF_DATE" \
450
+ --argjson repoState "$AF_REPO_STATE" \
451
+ '{name:$name, author:$author, topic:$topic, date:$date, repoState:$repoState}' 2>/dev/null || echo "{}")
452
+ RICH="$RICH$AF_ENTRY"
453
+ FIRST=false
454
+ done
455
+ JSON="$JSON]"
456
+ RICH="$RICH]"
457
+ fi
458
+ echo "$JSON" > "$CTX_DIR/addressed"
459
+ echo "$RICH" > "$CTX_DIR/addressed_rich"
460
+ ) &
461
+
462
+ # 6.5. Pending questions addressed to user (background)
463
+ (
464
+ PENDING_JSON="[]"
465
+ QDIR="$SCRIPT_DIR/memory/knowledge/questions"
466
+ if [ -d "$QDIR" ]; then
467
+ # Match against the same identity variants used for handoffs (display name,
468
+ # github first-name, people-file name) so "to: oz" matches author "oguzhan".
469
+ _DISPLAY_Q=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
470
+ _GH_NAME_Q=$(jq -r '.github_name // empty' "$STATE_FILE" 2>/dev/null)
471
+ _FIRST_NAME_Q=$(echo "$_GH_NAME_Q" | awk '{print tolower($1)}')
472
+ _PEOPLE_FILE_Q="$SCRIPT_DIR/memory/people/$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]').md"
473
+ _PEOPLE_NAME_Q=""
474
+ [ -f "$_PEOPLE_FILE_Q" ] && _PEOPLE_NAME_Q=$(sed -n 's/^# *//p' "$_PEOPLE_FILE_Q" 2>/dev/null | head -1 | awk '{print tolower($1)}')
475
+
476
+ LINES=""
477
+ for QF in "$QDIR"/*.md; do
478
+ [ -f "$QF" ] || continue
479
+ Q_TO=$(sed -n 's/^to:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"' | tr '[:upper:]' '[:lower:]' | awk '{print $1}')
480
+ [ -z "$Q_TO" ] && continue
481
+ _MATCH="false"
482
+ for _V in "$AUTHOR" "$_DISPLAY_Q" "$_FIRST_NAME_Q" "$_PEOPLE_NAME_Q"; do
483
+ [ -z "$_V" ] && continue
484
+ _V_LC=$(echo "$_V" | tr '[:upper:]' '[:lower:]')
485
+ [ "$Q_TO" = "$_V_LC" ] && _MATCH="true" && break
486
+ done
487
+ [ "$_MATCH" = "true" ] || continue
488
+ Q_STATUS=$(sed -n 's/^status:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"')
489
+ [ "$Q_STATUS" = "pending" ] || continue
490
+ Q_FROM=$(sed -n 's/^from:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"')
491
+ Q_TOPIC=$(sed -n 's/^topic:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"')
492
+ Q_HID=$(sed -n 's/^harvest_id:[[:space:]]*//p' "$QF" 2>/dev/null | head -1 | tr -d '"')
493
+ Q_REL="${QF#$SCRIPT_DIR/}"
494
+ LINE=$(jq -nc \
495
+ --arg from "$Q_FROM" \
496
+ --arg topic "$Q_TOPIC" \
497
+ --arg hid "$Q_HID" \
498
+ --arg file "$Q_REL" \
499
+ '{from:$from, topic:$topic, harvest_id:$hid, file:$file}' 2>/dev/null)
500
+ [ -n "$LINE" ] && LINES="$LINES$LINE
501
+ "
502
+ done
503
+ if [ -n "$LINES" ]; then
504
+ PENDING_JSON=$(echo "$LINES" | jq -sc '.' 2>/dev/null || echo "[]")
505
+ fi
506
+ fi
507
+ echo "$PENDING_JSON" > "$CTX_DIR/pending_questions"
508
+ ) &
509
+
510
+ # 7. Graph health (background — zero added latency, runs in parallel; seeded)
511
+ [ "$CTX_SEED_USED" != "true" ] && (
512
+ if [ "$LOCAL_MODE" = "true" ]; then
513
+ echo "skip"
514
+ elif bash "$SCRIPT_DIR/bin/graph.sh" test 2>/dev/null | grep -q "Connected"; then
515
+ echo "ok"
516
+ else
517
+ echo "fail"
518
+ fi
519
+ ) > "$CTX_DIR/graph_health" 2>/dev/null &
520
+
521
+ # 8. Telegram health (background)
522
+ # notify.sh test emits JSON, not prose: {"status":"ok"} connected,
523
+ # {"status":"configured"} local, {"status":"offline"} otherwise. Match the
524
+ # status field — an earlier grep for the word "connected" read a human sentence
525
+ # that the consent rewrite removed, so the footer showed telegram ✗ while the
526
+ # bot was answering.
527
+ [ "$CTX_SEED_USED" != "true" ] && (
528
+ if [ "$LOCAL_MODE" = "true" ]; then
529
+ echo "skip"
530
+ elif bash "$SCRIPT_DIR/bin/notify.sh" test 2>/dev/null \
531
+ | jq -e '.status == "ok" or .status == "configured"' >/dev/null 2>&1; then
532
+ echo "ok"
533
+ else
534
+ echo "fail"
535
+ fi
536
+ ) > "$CTX_DIR/telegram_health" 2>/dev/null &
537
+
538
+ # 9. Lifecycle events: merged PRs + implemented handoffs (background; seeded)
539
+ [ "$CTX_SEED_USED" != "true" ] && (
540
+ GH_USER_LC=""
541
+ if [ -f "$STATE_FILE" ]; then
542
+ GH_USER_LC=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
543
+ fi
544
+ DISPLAY_NAME_LC=""
545
+ if [ -f "$STATE_FILE" ]; then
546
+ DISPLAY_NAME_LC=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
547
+ fi
548
+ # Get last session end date (use 7 days ago as fallback)
549
+ LAST_END=$(bash "$SCRIPT_DIR/bin/graph.sh" query "
550
+ MATCH (s:Session)-[:BY]->(p:Person {github: \$gh})
551
+ WHERE s.wrappedAt IS NOT NULL
552
+ RETURN toString(s.wrappedAt) AS t
553
+ ORDER BY s.wrappedAt DESC SKIP 1 LIMIT 1
554
+ " "$(jq -n --arg gh "$GH_USER_LC" '{gh: $gh}')" 2>/dev/null | jq -r '.values[0][0] // empty' 2>/dev/null)
555
+ if [ -z "$LAST_END" ]; then
556
+ 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"
557
+ fi
558
+
559
+ MERGED_JSON="[]"
560
+ if [ -n "$GH_USER_LC" ]; then
561
+ 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 "[]")
562
+ fi
563
+
564
+ IMPL_JSON="[]"
565
+ IMPL_NAME="${DISPLAY_NAME_LC:-$GH_USER_LC}"
566
+ if [ -n "$IMPL_NAME" ]; then
567
+ IMPL_JSON=$(bash "$SCRIPT_DIR/bin/graph-op.sh" my-implemented-handoffs "$IMPL_NAME" "$(echo "$LAST_END" | cut -dT -f1)" 2>/dev/null || echo "[]")
568
+ fi
569
+
570
+ jq -n --argjson merged "$MERGED_JSON" --argjson impl "$IMPL_JSON" \
571
+ '{merged_prs: $merged, implemented_handoffs: $impl}' 2>/dev/null || echo '{"merged_prs":[],"implemented_handoffs":[]}'
572
+ ) > "$CTX_DIR/lifecycle" 2>/dev/null &
573
+
574
+ # 10. Last Pulse brief (background) — gated on feature flag + API key; seeded
575
+ [ "$CTX_SEED_USED" != "true" ] && (
576
+ PULSE_ENABLED=$(jq -r '.features.pulse // "false"' "$CONFIG" 2>/dev/null || echo "false")
577
+ HAS_API_KEY=false
578
+ _env_file="${ENV_FILE:-$SCRIPT_DIR/.env}"
579
+ if [ -f "$_env_file" ]; then
580
+ _key=$(grep '^EGREGORE_API_KEY=' "$_env_file" 2>/dev/null | cut -d'=' -f2- || true)
581
+ [ -n "$_key" ] && HAS_API_KEY=true
582
+ fi
583
+
584
+ if [ "$PULSE_ENABLED" = "true" ] && [ "$HAS_API_KEY" = "true" ] && [ -n "$GH_USER_LC" ]; then
585
+ BRIEF_RESULT=$(bash "$SCRIPT_DIR/bin/graph.sh" query "
586
+ MATCH (p:Person {github: \$gh})
587
+ WHERE p.lastBrief IS NOT NULL
588
+ AND p.lastBriefDate >= date() - duration('P7D')
589
+ RETURN p.lastBrief, toString(p.lastBriefDate), p.lastRecommendations
590
+ " "$(jq -n --arg gh "$GH_USER_LC" '{gh: $gh}')" 2>/dev/null || echo '{"values":[]}')
591
+ BRIEF=$(echo "$BRIEF_RESULT" | jq -r '.values[0][0] // empty' 2>/dev/null || true)
592
+ BRIEF_DATE=$(echo "$BRIEF_RESULT" | jq -r '.values[0][1] // empty' 2>/dev/null || true)
593
+ RECS=$(echo "$BRIEF_RESULT" | jq -c '.values[0][2] // []' 2>/dev/null || echo '[]')
594
+ if [ -n "$BRIEF" ]; then
595
+ jq -n --arg brief "$BRIEF" --arg date "$BRIEF_DATE" --argjson recs "$RECS" \
596
+ '{brief: $brief, date: $date, recommendations: $recs}'
597
+ else
598
+ echo '{}'
599
+ fi
600
+ else
601
+ echo '{}'
602
+ fi
603
+ ) > "$CTX_DIR/pulse_brief" 2>/dev/null &
604
+
605
+ # 11. Momentum metrics (background)
606
+ # Always live, even when seeded: _metrics_gather mixes graph reads (which ride
607
+ # the warm cache) with local git/file scans — sessions, commits, handoffs,
608
+ # knowledge — whose freshness the greeting should reflect this instant.
609
+ (
610
+ source "$SCRIPT_DIR/bin/lib/metrics.sh"
611
+ _metrics_gather
612
+ ) > "$CTX_DIR/metrics" 2>/dev/null &
613
+
614
+ # Wait for all context gathering + health checks to finish
615
+ wait
616
+
617
+ # --- Read service health results ---
618
+ GRAPH_RESULT=$(cat "$CTX_DIR/graph_health" 2>/dev/null || echo "")
619
+ if [ -n "$GRAPH_RESULT" ]; then HEALTH_GRAPH="$GRAPH_RESULT"; fi
620
+ TELEGRAM_RESULT=$(cat "$CTX_DIR/telegram_health" 2>/dev/null || echo "")
621
+ if [ -n "$TELEGRAM_RESULT" ]; then HEALTH_TELEGRAM="$TELEGRAM_RESULT"; fi