create-egregore 0.19.5 → 0.19.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/assets/egregore-launcher.js +411 -25
  2. package/bin/cli.js +34 -20
  3. package/lib/local.js +21 -5
  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 +7 -5
  8. package/runtime/codex/.codex/spec-manifest.json +1 -1
  9. package/runtime/codex/AGENTS.md +7 -2
  10. package/runtime/codex/bin/codex-render-spec.mjs +6 -1
  11. package/runtime/codex/bin/codex-session-start.sh +5 -59
  12. package/runtime/codex/bin/codex-skill-render.mjs +7 -5
  13. package/runtime/codex/bin/lib/context.sh +8 -1
  14. package/runtime/codex/bin/lib/greeting.sh +12 -108
  15. package/runtime/codex/bin/lib/metrics.sh +374 -0
  16. package/runtime/codex/bin/publish-artifact.sh +14 -0
  17. package/runtime/codex/bin/settings.sh +274 -0
  18. package/runtime/codex/manifest.json +2 -0
  19. package/runtime/pi/.codex/config.toml +15 -0
  20. package/runtime/pi/.codex/hooks/branch-guard.js +242 -0
  21. package/runtime/pi/.codex/hooks/search-hint.js +65 -0
  22. package/runtime/pi/.codex/hooks.json +30 -0
  23. package/runtime/pi/.codex/skills/activity/SKILL.md +70 -0
  24. package/runtime/pi/.codex/skills/add/SKILL.md +25 -0
  25. package/runtime/pi/.codex/skills/announce/SKILL.md +54 -0
  26. package/runtime/pi/.codex/skills/answer/SKILL.md +25 -0
  27. package/runtime/pi/.codex/skills/archive/SKILL.md +41 -0
  28. package/runtime/pi/.codex/skills/ask/SKILL.md +59 -0
  29. package/runtime/pi/.codex/skills/branch/SKILL.md +25 -0
  30. package/runtime/pi/.codex/skills/checkup/SKILL.md +41 -0
  31. package/runtime/pi/.codex/skills/commit/SKILL.md +25 -0
  32. package/runtime/pi/.codex/skills/contribute/SKILL.md +25 -0
  33. package/runtime/pi/.codex/skills/dark-mode/SKILL.md +25 -0
  34. package/runtime/pi/.codex/skills/dashboard/SKILL.md +59 -0
  35. package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +74 -0
  36. package/runtime/pi/.codex/skills/delete-user/SKILL.md +25 -0
  37. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +25 -0
  38. package/runtime/pi/.codex/skills/emissary/SKILL.md +41 -0
  39. package/runtime/pi/.codex/skills/env/SKILL.md +25 -0
  40. package/runtime/pi/.codex/skills/handoff/SKILL.md +67 -0
  41. package/runtime/pi/.codex/skills/harvest/SKILL.md +107 -0
  42. package/runtime/pi/.codex/skills/ingest/SKILL.md +25 -0
  43. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +25 -0
  44. package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  45. package/runtime/pi/.codex/skills/invite/SKILL.md +40 -0
  46. package/runtime/pi/.codex/skills/issue/SKILL.md +41 -0
  47. package/runtime/pi/.codex/skills/me/SKILL.md +25 -0
  48. package/runtime/pi/.codex/skills/meeting/SKILL.md +41 -0
  49. package/runtime/pi/.codex/skills/minion/SKILL.md +25 -0
  50. package/runtime/pi/.codex/skills/note/SKILL.md +25 -0
  51. package/runtime/pi/.codex/skills/onboarding/SKILL.md +41 -0
  52. package/runtime/pi/.codex/skills/pr/SKILL.md +25 -0
  53. package/runtime/pi/.codex/skills/project/SKILL.md +41 -0
  54. package/runtime/pi/.codex/skills/pull/SKILL.md +25 -0
  55. package/runtime/pi/.codex/skills/push/SKILL.md +25 -0
  56. package/runtime/pi/.codex/skills/quest/SKILL.md +77 -0
  57. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +25 -0
  58. package/runtime/pi/.codex/skills/reflect/SKILL.md +41 -0
  59. package/runtime/pi/.codex/skills/review-pr/SKILL.md +41 -0
  60. package/runtime/pi/.codex/skills/save/SKILL.md +62 -0
  61. package/runtime/pi/.codex/skills/scroll/SKILL.md +168 -0
  62. package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +4 -0
  63. package/runtime/pi/.codex/skills/scroll/assets/shell.html +542 -0
  64. package/runtime/pi/.codex/skills/search/SKILL.md +56 -0
  65. package/runtime/pi/.codex/skills/session-view/SKILL.md +25 -0
  66. package/runtime/pi/.codex/skills/setup/SKILL.md +25 -0
  67. package/runtime/pi/.codex/skills/summon/SKILL.md +41 -0
  68. package/runtime/pi/.codex/skills/sync-repos/SKILL.md +25 -0
  69. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +25 -0
  70. package/runtime/pi/.codex/skills/telemetry/SKILL.md +25 -0
  71. package/runtime/pi/.codex/skills/test/SKILL.md +41 -0
  72. package/runtime/pi/.codex/skills/the-spiral/SKILL.md +84 -0
  73. package/runtime/pi/.codex/skills/thread/SKILL.md +25 -0
  74. package/runtime/pi/.codex/skills/todo/SKILL.md +41 -0
  75. package/runtime/pi/.codex/skills/tui-design/SKILL.md +41 -0
  76. package/runtime/pi/.codex/skills/tutorial/SKILL.md +41 -0
  77. package/runtime/pi/.codex/skills/update/SKILL.md +25 -0
  78. package/runtime/pi/.codex/skills/view/SKILL.md +156 -0
  79. package/runtime/pi/.codex/skills/view/agents/openai.yaml +4 -0
  80. package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +221 -0
  81. package/runtime/pi/.codex/skills/visual-explain/SKILL.md +41 -0
  82. package/runtime/pi/.codex/skills/wrap/SKILL.md +55 -0
  83. package/runtime/pi/.codex/spec-manifest.json +114 -0
  84. package/runtime/pi/.pi/APPEND_SYSTEM.md +255 -0
  85. package/runtime/pi/.pi/extensions/egregore.ts +324 -0
  86. package/runtime/pi/.pi/settings.json +6 -0
  87. package/runtime/pi/.pi/spec-manifest.json +12 -0
  88. package/runtime/pi/AGENTS.md +350 -0
  89. package/runtime/pi/bin/activity-data.sh +242 -0
  90. package/runtime/pi/bin/agent.sh +677 -0
  91. package/runtime/pi/bin/codex-install-skills.sh +78 -0
  92. package/runtime/pi/bin/codex-render-spec.mjs +394 -0
  93. package/runtime/pi/bin/codex-session-start.sh +188 -0
  94. package/runtime/pi/bin/codex-skill-render.mjs +323 -0
  95. package/runtime/pi/bin/codex-sync-skills.sh +227 -0
  96. package/runtime/pi/bin/dashboard-data.sh +178 -0
  97. package/runtime/pi/bin/graph-batch.sh +60 -0
  98. package/runtime/pi/bin/graph-op.sh +431 -0
  99. package/runtime/pi/bin/graph-wal.sh +228 -0
  100. package/runtime/pi/bin/graph.sh +177 -0
  101. package/runtime/pi/bin/handoff-pr-backfill.sh +86 -0
  102. package/runtime/pi/bin/handoff-run.sh +581 -0
  103. package/runtime/pi/bin/handoff-save-egregore.sh +136 -0
  104. package/runtime/pi/bin/index-handoff.sh +312 -0
  105. package/runtime/pi/bin/lib/artifact-id.sh +46 -0
  106. package/runtime/pi/bin/lib/config.sh +71 -0
  107. package/runtime/pi/bin/lib/context.sh +542 -0
  108. package/runtime/pi/bin/lib/dashboard-artifact.sh +142 -0
  109. package/runtime/pi/bin/lib/ensure-develop.sh +183 -0
  110. package/runtime/pi/bin/lib/git-sync.sh +288 -0
  111. package/runtime/pi/bin/lib/greeting.sh +340 -0
  112. package/runtime/pi/bin/lib/hash.sh +13 -0
  113. package/runtime/pi/bin/lib/identity.sh +174 -0
  114. package/runtime/pi/bin/lib/metrics.sh +374 -0
  115. package/runtime/pi/bin/lib/permissions.sh +106 -0
  116. package/runtime/pi/bin/lib/time.sh +56 -0
  117. package/runtime/pi/bin/lib/worktree-links.sh +54 -0
  118. package/runtime/pi/bin/notify.sh +207 -0
  119. package/runtime/pi/bin/pi-branch-policy.mjs +15 -0
  120. package/runtime/pi/bin/pi-product-workflows.mjs +324 -0
  121. package/runtime/pi/bin/pi-render-spec.mjs +114 -0
  122. package/runtime/pi/bin/pi-session-start.sh +24 -0
  123. package/runtime/pi/bin/publish-artifact.sh +201 -0
  124. package/runtime/pi/bin/publish-references.sh +144 -0
  125. package/runtime/pi/bin/repo-state.sh +99 -0
  126. package/runtime/pi/bin/telemetry.sh +391 -0
  127. package/runtime/pi/bin/worktree-create.sh +118 -0
  128. package/runtime/pi/bin/worktree-remove.sh +50 -0
  129. package/runtime/pi/bin/worktree.sh +198 -0
  130. package/runtime/pi/manifest.json +176 -0
@@ -0,0 +1,542 @@
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
+ # 1. Recent handoffs (background)
37
+ (
38
+ JSON="[]"
39
+ if [ -d "$SCRIPT_DIR/memory/handoffs" ]; then
40
+ 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)
41
+ JSON="["
42
+ FIRST=true
43
+ for F in $FILES; do
44
+ [ -z "$F" ] && continue
45
+ NAME=$(basename "$F" .md)
46
+ PREVIEW=$(head -c 120 "$F" 2>/dev/null | tr '\n' ' ' | sed 's/"/\\"/g')
47
+ $FIRST || JSON="$JSON,"
48
+ JSON="$JSON{\"name\":\"$NAME\",\"preview\":\"$PREVIEW\"}"
49
+ FIRST=false
50
+ done
51
+ JSON="$JSON]"
52
+ fi
53
+ echo "$JSON" > "$CTX_DIR/handoffs"
54
+ ) &
55
+
56
+ # 2. Quests (background)
57
+ (
58
+ JSON="[]"
59
+ if [ -d "$SCRIPT_DIR/memory/quests" ]; then
60
+ JSON="["
61
+ FIRST=true
62
+ for F in "$SCRIPT_DIR/memory/quests/"*.md; do
63
+ [ -e "$F" ] || continue
64
+ echo "$F" | grep -q draft && continue
65
+ NAME=$(basename "$F" .md)
66
+ $FIRST || JSON="$JSON,"
67
+ JSON="$JSON\"$NAME\""
68
+ FIRST=false
69
+ done
70
+ JSON="$JSON]"
71
+ fi
72
+ echo "$JSON" > "$CTX_DIR/quests"
73
+ ) &
74
+
75
+ # 3. User's last activity (background)
76
+ (
77
+ git log --author="$AUTHOR" --format="%ar|%s" -1 2>/dev/null > "$CTX_DIR/activity" || echo "" > "$CTX_DIR/activity"
78
+ ) &
79
+
80
+ # 3b. Personal todos from graph (background)
81
+ (
82
+ _API_URL=$(jq -r '.api_url // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
83
+ _API_KEY=$(grep '^EGREGORE_API_KEY=' "${ENV_FILE:-$SCRIPT_DIR/.env}" 2>/dev/null | cut -d'=' -f2-)
84
+ if [ -n "$_API_URL" ] && [ -n "$_API_KEY" ]; then
85
+ TODO_RAW=$(bash "$SCRIPT_DIR/bin/graph.sh" query \
86
+ "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" \
87
+ "$(jq -n --arg me "$AUTHOR" '{me: $me}')" 2>/dev/null || echo "")
88
+ if [ -n "$TODO_RAW" ]; then
89
+ echo "$TODO_RAW" | jq '[.values[] | {text: .[0], priority: (.[1] // 0), status: .[2], created: .[3], quest: (.[4] // "")}]' 2>/dev/null || echo "[]"
90
+ else
91
+ echo "[]"
92
+ fi
93
+ else
94
+ echo "[]"
95
+ fi
96
+ ) > "$CTX_DIR/todos" 2>/dev/null &
97
+
98
+ # 4. Team presence — last seen + active branches (background)
99
+ (
100
+ # --- Read config inside subshell ---
101
+ _API_URL=$(jq -r '.api_url // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
102
+ _API_KEY=$(grep '^EGREGORE_API_KEY=' "${ENV_FILE:-$SCRIPT_DIR/.env}" 2>/dev/null | cut -d'=' -f2-)
103
+
104
+ # --- Graph: last-seen per person (excluding self) ---
105
+ GRAPH_DATA="[]"
106
+ if [ -n "$_API_URL" ] && [ -n "$_API_KEY" ]; then
107
+ 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"
108
+ GRAPH_RAW=$(bash "$SCRIPT_DIR/bin/graph.sh" query "$CYPHER" "$(jq -n --arg me "$AUTHOR" '{me: $me}')" 2>/dev/null || echo "")
109
+ if [ -n "$GRAPH_RAW" ]; then
110
+ GRAPH_DATA=$(echo "$GRAPH_RAW" | jq '[.values[] | {name: .[0], lastSeen: .[1]}]' 2>/dev/null || echo "[]")
111
+ fi
112
+ fi
113
+
114
+ # --- Git: branch tip commit dates per person (primary recency signal) ---
115
+ # git for-each-ref is fast and gives the actual last commit date per branch
116
+ GIT_COMMIT_DATA="{}"
117
+ GIT_REF_DATA=$(git -C "$SCRIPT_DIR" for-each-ref --sort=-committerdate \
118
+ --format='%(refname:short)|%(committerdate:unix)' refs/remotes/origin/dev/ 2>/dev/null || echo "")
119
+ if [ -n "$GIT_REF_DATA" ]; then
120
+ GIT_COMMIT_DATA=$(echo "$GIT_REF_DATA" | sed 's|^origin/dev/||' | while IFS='|' read -r REF_PATH REF_EPOCH || [ -n "$REF_PATH" ]; do
121
+ [ -z "$REF_PATH" ] && continue
122
+ REF_AUTHOR=$(echo "$REF_PATH" | cut -d'/' -f1 | tr '[:upper:]' '[:lower:]')
123
+ echo "${REF_AUTHOR}|${REF_EPOCH}"
124
+ done | sort -t'|' -k1,1 -k2,2rn | sort -t'|' -k1,1 -u | jq -Rn '
125
+ [inputs | split("|") | {(.[0]): .[1]}]
126
+ | add // {}
127
+ ' 2>/dev/null || echo "{}")
128
+ fi
129
+
130
+ # --- Git: active dev/* branches (excluding self) ---
131
+ SELF_LC=$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]')
132
+ SELF_DISPLAY_LC=""
133
+ if [ -f "$SCRIPT_DIR/.egregore-state.json" ]; then
134
+ SELF_DISPLAY_LC=$(jq -r '.display_name // empty' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null | tr '[:upper:]' '[:lower:]')
135
+ fi
136
+ BRANCH_DATA=$(git -C "$SCRIPT_DIR" branch -r --format='%(refname:short)' 2>/dev/null | grep "^origin/dev/" | sed 's|^origin/dev/||' || echo "")
137
+
138
+ BRANCH_MAP="{}"
139
+ if [ -n "$BRANCH_DATA" ]; then
140
+ BRANCH_MAP=$(echo "$BRANCH_DATA" | while IFS='/' read -r B_AUTHOR B_SLUG_REST || [ -n "$B_AUTHOR" ]; do
141
+ [ -z "$B_AUTHOR" ] && continue
142
+ B_AUTHOR_LC=$(echo "$B_AUTHOR" | tr '[:upper:]' '[:lower:]')
143
+ [ "$B_AUTHOR_LC" = "$SELF_LC" ] && continue
144
+ [ -n "$SELF_DISPLAY_LC" ] && [ "$B_AUTHOR_LC" = "$SELF_DISPLAY_LC" ] && continue
145
+ B_HUMAN=$(echo "$B_SLUG_REST" | tr '-' ' ')
146
+ echo "${B_AUTHOR_LC}|${B_HUMAN}"
147
+ done | jq -Rn '
148
+ [inputs | split("|") | {author: .[0], branch: .[1]}]
149
+ | group_by(.author)
150
+ | map({(.[0].author): [.[] | .branch]})
151
+ | add // {}
152
+ ' 2>/dev/null || echo "{}")
153
+ fi
154
+
155
+ # --- Local-mode presence: scan session + wrap files for lastSeen ---
156
+ # When graph has no data, these files provide temporal presence information
157
+ FILE_PRESENCE="{}"
158
+ if [ "$GRAPH_DATA" = "[]" ]; then
159
+ FILE_PRESENCE=$(
160
+ {
161
+ # Scan session logs, wraps, and handoffs (all use **Author**: / **Date**: format)
162
+ for DIR in "$SCRIPT_DIR/memory/sessions" "$SCRIPT_DIR/memory/wraps" "$SCRIPT_DIR/memory/handoffs"; do
163
+ [ -d "$DIR" ] || continue
164
+ find -L "$DIR" -name '*.md' -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -20 | while read -r SFILE; do
165
+ [ -z "$SFILE" ] && continue
166
+ S_AUTH=$(grep -m1 '^\*\*Author\*\*:' "$SFILE" 2>/dev/null | sed 's/^\*\*Author\*\*:[[:space:]]*//')
167
+ S_DATE=$(grep -m1 '^\*\*Date\*\*:' "$SFILE" 2>/dev/null | sed 's/^\*\*Date\*\*:[[:space:]]*//')
168
+ [ -z "$S_AUTH" ] && continue
169
+ echo "${S_AUTH}|${S_DATE}"
170
+ done
171
+ done
172
+ } | sort -t'|' -k1,1 -k2,2r | sort -t'|' -k1,1 -u | while IFS='|' read -r FP_NAME FP_DATE; do
173
+ [ -z "$FP_NAME" ] && continue
174
+ FP_LC=$(echo "$FP_NAME" | tr '[:upper:]' '[:lower:]')
175
+ echo "${FP_LC}|${FP_DATE}"
176
+ done | jq -Rn '
177
+ [inputs | split("|") | {(.[0]): .[1]}]
178
+ | add // {}
179
+ ' 2>/dev/null || echo "{}"
180
+ )
181
+ fi
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
+
208
+ # --- Relative time + epoch helpers (cross-platform) ---
209
+ NOW_EPOCH=$(date +%s)
210
+
211
+ iso_to_epoch() {
212
+ local iso="$1"
213
+ local clean=$(echo "$iso" | sed 's/Z$//; s/+00:00$//; s/\.[0-9]*//')
214
+ [[ "$clean" != *T* ]] && clean="${clean}T00:00:00"
215
+ TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$clean" "+%s" 2>/dev/null || \
216
+ TZ=UTC date -d "$clean" +%s 2>/dev/null || echo "0"
217
+ }
218
+
219
+ epoch_to_relative() {
220
+ local epoch="$1"
221
+ [ "$epoch" = "0" ] && echo "--" && return
222
+ local delta=$(( NOW_EPOCH - epoch ))
223
+ if [ "$delta" -lt 60 ]; then echo "just now"
224
+ elif [ "$delta" -lt 3600 ]; then echo "$(( delta / 60 ))m ago"
225
+ elif [ "$delta" -lt 86400 ]; then echo "$(( delta / 3600 ))h ago"
226
+ else
227
+ # Calendar-day based comparison for accurate "yesterday" / "Xd ago"
228
+ local event_date today_date
229
+ event_date=$(date -r "$epoch" +%Y-%m-%d 2>/dev/null || \
230
+ date -d "@$epoch" +%Y-%m-%d 2>/dev/null || echo "")
231
+ today_date=$(date +%Y-%m-%d)
232
+ if [ -z "$event_date" ] || [ "$event_date" = "$today_date" ]; then echo "today"
233
+ else
234
+ local today_e event_e
235
+ today_e=$(date -j -f "%Y-%m-%d" "$today_date" +%s 2>/dev/null || \
236
+ date -d "$today_date" +%s 2>/dev/null || echo "$NOW_EPOCH")
237
+ event_e=$(date -j -f "%Y-%m-%d" "$event_date" +%s 2>/dev/null || \
238
+ date -d "$event_date" +%s 2>/dev/null || echo "$epoch")
239
+ local days=$(( (today_e - event_e) / 86400 ))
240
+ [ "$days" -lt 1 ] && days=1
241
+ if [ "$days" -eq 1 ]; then echo "yesterday"
242
+ else echo "${days}d ago"
243
+ fi
244
+ fi
245
+ fi
246
+ }
247
+
248
+ # --- Merge git commits + graph + file presence + branches into presence array ---
249
+ ALL_NAMES=$(echo "$GRAPH_DATA" | jq -r '.[].name' 2>/dev/null || echo "")
250
+ FILE_NAMES=$(echo "$FILE_PRESENCE" | jq -r 'keys[]' 2>/dev/null || echo "")
251
+ BRANCH_NAMES=$(echo "$BRANCH_MAP" | jq -r 'keys[]' 2>/dev/null || echo "")
252
+ GIT_NAMES=$(echo "$GIT_COMMIT_DATA" | jq -r 'keys[]' 2>/dev/null || echo "")
253
+
254
+ 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 "")
255
+
256
+ if [ -z "$UNION_NAMES" ]; then
257
+ echo "[]" > "$CTX_DIR/team"
258
+ exit 0
259
+ fi
260
+
261
+ # --- Build presence JSON ---
262
+ PRESENCE="["
263
+ FIRST=true
264
+ for PNAME in $UNION_NAMES; do
265
+ # Git commit date is authoritative (actual work shipped).
266
+ # Graph/file presence are fallbacks only (session opens ≠ real activity).
267
+ GIT_EPOCH=$(echo "$GIT_COMMIT_DATA" | jq -r --arg n "$PNAME" '.[$n] // empty' 2>/dev/null || true)
268
+
269
+ LAST_SEEN_EPOCH="0"
270
+ if [ -n "$GIT_EPOCH" ] && [ "$GIT_EPOCH" -gt 0 ] 2>/dev/null; then
271
+ LAST_SEEN_EPOCH="$GIT_EPOCH"
272
+ else
273
+ # Fallback: graph session startedAt
274
+ GRAPH_ISO=$(echo "$GRAPH_DATA" | jq -r --arg n "$PNAME" '.[] | select((.name | ascii_downcase) == $n) | .lastSeen // empty' 2>/dev/null | head -1)
275
+ if [ -n "$GRAPH_ISO" ]; then
276
+ LAST_SEEN_EPOCH=$(iso_to_epoch "$GRAPH_ISO")
277
+ fi
278
+ # Fallback: file-based presence (local mode)
279
+ if [ "$LAST_SEEN_EPOCH" -le 0 ] 2>/dev/null; then
280
+ FILE_ISO=$(echo "$FILE_PRESENCE" | jq -r --arg n "$PNAME" '.[$n] // empty' 2>/dev/null || true)
281
+ if [ -n "$FILE_ISO" ]; then
282
+ LAST_SEEN_EPOCH=$(iso_to_epoch "$FILE_ISO")
283
+ fi
284
+ fi
285
+ fi
286
+
287
+ if [ "$LAST_SEEN_EPOCH" -gt 0 ] 2>/dev/null; then
288
+ LAST_SEEN_REL=$(epoch_to_relative "$LAST_SEEN_EPOCH")
289
+ else
290
+ LAST_SEEN_EPOCH="0"
291
+ LAST_SEEN_REL="--"
292
+ fi
293
+
294
+ BRANCHES_JSON=$(echo "$BRANCH_MAP" | jq --arg n "$PNAME" '.[$n] // []' 2>/dev/null || echo "[]")
295
+ BR_COUNT=$(echo "$BRANCHES_JSON" | jq 'length' 2>/dev/null || echo "0")
296
+ if [ "$BR_COUNT" -gt 2 ]; then
297
+ EXTRA=$((BR_COUNT - 2))
298
+ BRANCHES_JSON=$(echo "$BRANCHES_JSON" | jq --arg e "+${EXTRA} more" '[.[0:2][], $e]' 2>/dev/null || echo "$BRANCHES_JSON")
299
+ fi
300
+
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}')
304
+ $FIRST || PRESENCE="$PRESENCE,"
305
+ PRESENCE="$PRESENCE$ENTRY"
306
+ FIRST=false
307
+ done
308
+ PRESENCE="$PRESENCE]"
309
+
310
+ PRESENCE=$(echo "$PRESENCE" | jq 'sort_by(-.last_seen_sort)' 2>/dev/null || echo "$PRESENCE")
311
+ echo "$PRESENCE" > "$CTX_DIR/team"
312
+ ) &
313
+
314
+ # 5. Soul self-summary (background)
315
+ (
316
+ SUMMARY=""
317
+ if [ -f "$SCRIPT_DIR/egregore.md" ]; then
318
+ SUMMARY=$(sed -n '/^## Self-Summary/,/^##/p' "$SCRIPT_DIR/egregore.md" | sed '1d;/^##/d' | sed 's/^[[:space:]]*//' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
319
+ fi
320
+ echo "$SUMMARY" > "$CTX_DIR/soul_summary"
321
+ ) &
322
+
323
+ # 6. Handoffs addressed to user (background, enriched with author/topic)
324
+ (
325
+ JSON="[]"
326
+ RICH="[]"
327
+ if [ -d "$SCRIPT_DIR/memory/handoffs" ]; then
328
+ # Match github username, display name, github_name first word, and people-file name
329
+ # Handoff files may use "to: name", "**To**: name", or "To: name"
330
+ _DISPLAY=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
331
+ _GH_NAME=$(jq -r '.github_name // empty' "$STATE_FILE" 2>/dev/null)
332
+ _FIRST_NAME=$(echo "$_GH_NAME" | awk '{print $1}')
333
+ # Also check people file: memory/people/{author}.md has "# Name" on first line
334
+ _PEOPLE_NAME=""
335
+ _PEOPLE_FILE="$SCRIPT_DIR/memory/people/$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]').md"
336
+ [ -f "$_PEOPLE_FILE" ] && _PEOPLE_NAME=$(sed -n 's/^# *//p' "$_PEOPLE_FILE" 2>/dev/null | head -1)
337
+ _GREP_PAT="[Tt]o[*]*: *$AUTHOR\|[Tt]o[*]*:$AUTHOR"
338
+ for _VARIANT in "$_DISPLAY" "$_FIRST_NAME" "$_PEOPLE_NAME"; do
339
+ [ -z "$_VARIANT" ] && continue
340
+ [ "$_VARIANT" = "$AUTHOR" ] && continue
341
+ # Skip if already in pattern (avoid duplicates)
342
+ echo "$_GREP_PAT" | grep -qF "$_VARIANT" && continue
343
+ _GREP_PAT="$_GREP_PAT\|[Tt]o[*]*: *$_VARIANT\|[Tt]o[*]*:$_VARIANT"
344
+ done
345
+ ADDRESSED=$(grep -rli "$_GREP_PAT" "$SCRIPT_DIR/memory/handoffs/" 2>/dev/null | sort -r | head -5 || true)
346
+ JSON="["
347
+ RICH="["
348
+ FIRST=true
349
+ for AF in $ADDRESSED; do
350
+ [ -z "$AF" ] && continue
351
+ AF_NAME=$(basename "$AF" .md)
352
+ AF_AUTHOR=$(eg_handoff_author "$AF")
353
+ AF_TOPIC=$(eg_handoff_topic "$AF")
354
+ AF_DATE=$(eg_handoff_date "$AF")
355
+ # Extract repo state table if present
356
+ AF_REPO_STATE="[]"
357
+ AF_REPO_TABLE=$(awk '/^## Repo State/{found=1; next} found && /^#/{exit} found && /^\|[^-]/ && !/^\| Repo/{print}' "$AF" 2>/dev/null || true)
358
+ if [ -n "$AF_REPO_TABLE" ]; then
359
+ AF_REPO_STATE=$(echo "$AF_REPO_TABLE" | while IFS='|' read -r _ R_REPO R_BRANCH R_PR R_BASE _; do
360
+ R_REPO=$(echo "$R_REPO" | xargs 2>/dev/null || true)
361
+ R_BRANCH=$(echo "$R_BRANCH" | xargs 2>/dev/null || true)
362
+ R_PR=$(echo "$R_PR" | xargs 2>/dev/null | sed 's/^#//' | sed 's/—//' || true)
363
+ R_BASE=$(echo "$R_BASE" | xargs 2>/dev/null || true)
364
+ [ -z "$R_REPO" ] && continue
365
+ if [ -n "$R_PR" ] && [ "$R_PR" -eq "$R_PR" ] 2>/dev/null; then
366
+ printf '{"repo":"%s","branch":"%s","pr":%s,"base":"%s"}\n' "$R_REPO" "$R_BRANCH" "$R_PR" "$R_BASE"
367
+ else
368
+ printf '{"repo":"%s","branch":"%s","pr":null,"base":"%s"}\n' "$R_REPO" "$R_BRANCH" "$R_BASE"
369
+ fi
370
+ done | jq -sc '.' 2>/dev/null || echo "[]")
371
+ fi
372
+ $FIRST || JSON="$JSON,"
373
+ $FIRST || RICH="$RICH,"
374
+ JSON="$JSON\"$AF_NAME\""
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"
383
+ FIRST=false
384
+ done
385
+ JSON="$JSON]"
386
+ RICH="$RICH]"
387
+ fi
388
+ echo "$JSON" > "$CTX_DIR/addressed"
389
+ echo "$RICH" > "$CTX_DIR/addressed_rich"
390
+ ) &
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
+
440
+ # 7. Graph health (background — zero added latency, runs in parallel)
441
+ (
442
+ if [ "$LOCAL_MODE" = "true" ]; then
443
+ echo "skip"
444
+ elif bash "$SCRIPT_DIR/bin/graph.sh" test 2>/dev/null | grep -q "Connected"; then
445
+ echo "ok"
446
+ else
447
+ echo "fail"
448
+ fi
449
+ ) > "$CTX_DIR/graph_health" 2>/dev/null &
450
+
451
+ # 8. Telegram health (background)
452
+ (
453
+ if [ "$LOCAL_MODE" = "true" ]; then
454
+ echo "skip"
455
+ elif bash "$SCRIPT_DIR/bin/notify.sh" test 2>/dev/null | grep -q "connected"; then
456
+ echo "ok"
457
+ else
458
+ echo "fail"
459
+ fi
460
+ ) > "$CTX_DIR/telegram_health" 2>/dev/null &
461
+
462
+ # 9. Lifecycle events: merged PRs + implemented handoffs (background)
463
+ (
464
+ GH_USER_LC=""
465
+ if [ -f "$STATE_FILE" ]; then
466
+ GH_USER_LC=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
467
+ fi
468
+ DISPLAY_NAME_LC=""
469
+ if [ -f "$STATE_FILE" ]; then
470
+ DISPLAY_NAME_LC=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
471
+ fi
472
+ # Get last session end date (use 7 days ago as fallback)
473
+ LAST_END=$(bash "$SCRIPT_DIR/bin/graph.sh" query "
474
+ MATCH (s:Session)-[:BY]->(p:Person {github: \$gh})
475
+ WHERE s.wrappedAt IS NOT NULL
476
+ RETURN toString(s.wrappedAt) AS t
477
+ ORDER BY s.wrappedAt DESC SKIP 1 LIMIT 1
478
+ " "$(jq -n --arg gh "$GH_USER_LC" '{gh: $gh}')" 2>/dev/null | jq -r '.values[0][0] // empty' 2>/dev/null)
479
+ if [ -z "$LAST_END" ]; then
480
+ 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"
481
+ fi
482
+
483
+ MERGED_JSON="[]"
484
+ if [ -n "$GH_USER_LC" ]; then
485
+ 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 "[]")
486
+ fi
487
+
488
+ IMPL_JSON="[]"
489
+ IMPL_NAME="${DISPLAY_NAME_LC:-$GH_USER_LC}"
490
+ if [ -n "$IMPL_NAME" ]; then
491
+ IMPL_JSON=$(bash "$SCRIPT_DIR/bin/graph-op.sh" my-implemented-handoffs "$IMPL_NAME" "$(echo "$LAST_END" | cut -dT -f1)" 2>/dev/null || echo "[]")
492
+ fi
493
+
494
+ jq -n --argjson merged "$MERGED_JSON" --argjson impl "$IMPL_JSON" \
495
+ '{merged_prs: $merged, implemented_handoffs: $impl}' 2>/dev/null || echo '{"merged_prs":[],"implemented_handoffs":[]}'
496
+ ) > "$CTX_DIR/lifecycle" 2>/dev/null &
497
+
498
+ # 10. Last Pulse brief (background) — gated on feature flag + API key
499
+ (
500
+ PULSE_ENABLED=$(jq -r '.features.pulse // "false"' "$CONFIG" 2>/dev/null || echo "false")
501
+ HAS_API_KEY=false
502
+ _env_file="${ENV_FILE:-$SCRIPT_DIR/.env}"
503
+ if [ -f "$_env_file" ]; then
504
+ _key=$(grep '^EGREGORE_API_KEY=' "$_env_file" 2>/dev/null | cut -d'=' -f2- || true)
505
+ [ -n "$_key" ] && HAS_API_KEY=true
506
+ fi
507
+
508
+ if [ "$PULSE_ENABLED" = "true" ] && [ "$HAS_API_KEY" = "true" ] && [ -n "$GH_USER_LC" ]; then
509
+ BRIEF_RESULT=$(bash "$SCRIPT_DIR/bin/graph.sh" query "
510
+ MATCH (p:Person {github: \$gh})
511
+ WHERE p.lastBrief IS NOT NULL
512
+ AND p.lastBriefDate >= date() - duration('P7D')
513
+ RETURN p.lastBrief, toString(p.lastBriefDate), p.lastRecommendations
514
+ " "$(jq -n --arg gh "$GH_USER_LC" '{gh: $gh}')" 2>/dev/null || echo '{"values":[]}')
515
+ BRIEF=$(echo "$BRIEF_RESULT" | jq -r '.values[0][0] // empty' 2>/dev/null || true)
516
+ BRIEF_DATE=$(echo "$BRIEF_RESULT" | jq -r '.values[0][1] // empty' 2>/dev/null || true)
517
+ RECS=$(echo "$BRIEF_RESULT" | jq -c '.values[0][2] // []' 2>/dev/null || echo '[]')
518
+ if [ -n "$BRIEF" ]; then
519
+ jq -n --arg brief "$BRIEF" --arg date "$BRIEF_DATE" --argjson recs "$RECS" \
520
+ '{brief: $brief, date: $date, recommendations: $recs}'
521
+ else
522
+ echo '{}'
523
+ fi
524
+ else
525
+ echo '{}'
526
+ fi
527
+ ) > "$CTX_DIR/pulse_brief" 2>/dev/null &
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
+
535
+ # Wait for all context gathering + health checks to finish
536
+ wait
537
+
538
+ # --- Read service health results ---
539
+ GRAPH_RESULT=$(cat "$CTX_DIR/graph_health" 2>/dev/null || echo "")
540
+ if [ -n "$GRAPH_RESULT" ]; then HEALTH_GRAPH="$GRAPH_RESULT"; fi
541
+ TELEGRAM_RESULT=$(cat "$CTX_DIR/telegram_health" 2>/dev/null || echo "")
542
+ if [ -n "$TELEGRAM_RESULT" ]; then HEALTH_TELEGRAM="$TELEGRAM_RESULT"; fi