create-egregore 0.19.22 → 0.19.24

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 (125) hide show
  1. package/assets/egregore-launcher.js +50 -7
  2. package/assets/egregore-launcher.sh +10 -2
  3. package/bin/cli.js +21 -3
  4. package/lib/codex-runtime.js +25 -3
  5. package/lib/connect-runtime.js +329 -0
  6. package/lib/pi-runtime.js +23 -3
  7. package/lib/setup.js +13 -0
  8. package/package.json +3 -3
  9. package/runtime/codex/.codex/skills/handoff/SKILL.md +99 -13
  10. package/runtime/codex/.codex/skills/save/SKILL.md +39 -4
  11. package/runtime/codex/.codex/spec-manifest.json +2 -2
  12. package/runtime/codex/AGENTS.md +18 -1
  13. package/runtime/codex/bin/agent.sh +28 -14
  14. package/runtime/codex/bin/attendant.sh +169 -6
  15. package/runtime/codex/bin/codex-render-spec.mjs +18 -1
  16. package/runtime/codex/bin/codex-session-start.sh +13 -0
  17. package/runtime/codex/bin/codex-skill-render.mjs +5 -0
  18. package/runtime/codex/bin/codex-sync-skills.sh +1 -0
  19. package/runtime/codex/bin/graph.sh +21 -1
  20. package/runtime/codex/bin/handoff-run.sh +88 -84
  21. package/runtime/codex/bin/index-handoff.sh +2 -1
  22. package/runtime/codex/bin/lib/context.sh +220 -172
  23. package/runtime/codex/bin/lib/dashboard-artifact.sh +113 -13
  24. package/runtime/codex/bin/lib/git-sync.sh +44 -8
  25. package/runtime/codex/bin/lib/greeting.sh +34 -11
  26. package/runtime/codex/bin/lib/identity.sh +32 -14
  27. package/runtime/codex/bin/person.py +20 -0
  28. package/runtime/codex/bin/publish-artifact.sh +25 -4
  29. package/runtime/codex/manifest.json +3 -72
  30. package/runtime/pi/.codex/skills/handoff/SKILL.md +99 -13
  31. package/runtime/pi/.codex/skills/save/SKILL.md +39 -4
  32. package/runtime/pi/.codex/spec-manifest.json +2 -2
  33. package/runtime/pi/.pi/APPEND_SYSTEM.md +18 -1
  34. package/runtime/pi/.pi/spec-manifest.json +2 -2
  35. package/runtime/pi/AGENTS.md +18 -1
  36. package/runtime/pi/bin/agent.sh +28 -14
  37. package/runtime/pi/bin/attendant.sh +169 -6
  38. package/runtime/pi/bin/codex-render-spec.mjs +18 -1
  39. package/runtime/pi/bin/codex-session-start.sh +13 -0
  40. package/runtime/pi/bin/codex-skill-render.mjs +5 -0
  41. package/runtime/pi/bin/codex-sync-skills.sh +1 -0
  42. package/runtime/pi/bin/graph.sh +21 -1
  43. package/runtime/pi/bin/handoff-run.sh +88 -84
  44. package/runtime/pi/bin/index-handoff.sh +2 -1
  45. package/runtime/pi/bin/lib/context.sh +220 -172
  46. package/runtime/pi/bin/lib/dashboard-artifact.sh +113 -13
  47. package/runtime/pi/bin/lib/git-sync.sh +44 -8
  48. package/runtime/pi/bin/lib/greeting.sh +34 -11
  49. package/runtime/pi/bin/lib/identity.sh +32 -14
  50. package/runtime/pi/bin/person.py +20 -0
  51. package/runtime/pi/bin/pi-product-workflows.mjs +19 -6
  52. package/runtime/pi/bin/publish-artifact.sh +25 -4
  53. package/runtime/pi/manifest.json +3 -72
  54. package/runtime/codex/.codex/skills/add/SKILL.md +0 -29
  55. package/runtime/codex/.codex/skills/answer/SKILL.md +0 -29
  56. package/runtime/codex/.codex/skills/ask/SKILL.md +0 -67
  57. package/runtime/codex/.codex/skills/audit/SKILL.md +0 -29
  58. package/runtime/codex/.codex/skills/autosave/SKILL.md +0 -29
  59. package/runtime/codex/.codex/skills/branch/SKILL.md +0 -29
  60. package/runtime/codex/.codex/skills/commit/SKILL.md +0 -29
  61. package/runtime/codex/.codex/skills/demo-mode/SKILL.md +0 -29
  62. package/runtime/codex/.codex/skills/deploy-bridge/SKILL.md +0 -29
  63. package/runtime/codex/.codex/skills/design/SKILL.md +0 -29
  64. package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +0 -29
  65. package/runtime/codex/.codex/skills/emissary/SKILL.md +0 -45
  66. package/runtime/codex/.codex/skills/env/SKILL.md +0 -29
  67. package/runtime/codex/.codex/skills/ingest-google/SKILL.md +0 -29
  68. package/runtime/codex/.codex/skills/minion/SKILL.md +0 -29
  69. package/runtime/codex/.codex/skills/mock/SKILL.md +0 -29
  70. package/runtime/codex/.codex/skills/pr/SKILL.md +0 -29
  71. package/runtime/codex/.codex/skills/product-voice/SKILL.md +0 -29
  72. package/runtime/codex/.codex/skills/project/SKILL.md +0 -45
  73. package/runtime/codex/.codex/skills/pull/SKILL.md +0 -29
  74. package/runtime/codex/.codex/skills/push/SKILL.md +0 -29
  75. package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +0 -29
  76. package/runtime/codex/.codex/skills/review-pr/SKILL.md +0 -45
  77. package/runtime/codex/.codex/skills/scroll/SKILL.md +0 -288
  78. package/runtime/codex/.codex/skills/scroll/agents/openai.yaml +0 -4
  79. package/runtime/codex/.codex/skills/scroll/assets/shell.html +0 -871
  80. package/runtime/codex/.codex/skills/search/SKILL.md +0 -79
  81. package/runtime/codex/.codex/skills/session-view/SKILL.md +0 -29
  82. package/runtime/codex/.codex/skills/setup/SKILL.md +0 -29
  83. package/runtime/codex/.codex/skills/summon/SKILL.md +0 -45
  84. package/runtime/codex/.codex/skills/task/SKILL.md +0 -29
  85. package/runtime/codex/.codex/skills/teams-connect/SKILL.md +0 -29
  86. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +0 -29
  87. package/runtime/codex/.codex/skills/the-spiral/SKILL.md +0 -84
  88. package/runtime/codex/.codex/skills/thread/SKILL.md +0 -29
  89. package/runtime/codex/.codex/skills/todo/SKILL.md +0 -45
  90. package/runtime/pi/.codex/skills/add/SKILL.md +0 -29
  91. package/runtime/pi/.codex/skills/answer/SKILL.md +0 -29
  92. package/runtime/pi/.codex/skills/ask/SKILL.md +0 -67
  93. package/runtime/pi/.codex/skills/audit/SKILL.md +0 -29
  94. package/runtime/pi/.codex/skills/autosave/SKILL.md +0 -29
  95. package/runtime/pi/.codex/skills/branch/SKILL.md +0 -29
  96. package/runtime/pi/.codex/skills/commit/SKILL.md +0 -29
  97. package/runtime/pi/.codex/skills/demo-mode/SKILL.md +0 -29
  98. package/runtime/pi/.codex/skills/deploy-bridge/SKILL.md +0 -29
  99. package/runtime/pi/.codex/skills/design/SKILL.md +0 -29
  100. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +0 -29
  101. package/runtime/pi/.codex/skills/emissary/SKILL.md +0 -45
  102. package/runtime/pi/.codex/skills/env/SKILL.md +0 -29
  103. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +0 -29
  104. package/runtime/pi/.codex/skills/minion/SKILL.md +0 -29
  105. package/runtime/pi/.codex/skills/mock/SKILL.md +0 -29
  106. package/runtime/pi/.codex/skills/pr/SKILL.md +0 -29
  107. package/runtime/pi/.codex/skills/product-voice/SKILL.md +0 -29
  108. package/runtime/pi/.codex/skills/project/SKILL.md +0 -45
  109. package/runtime/pi/.codex/skills/pull/SKILL.md +0 -29
  110. package/runtime/pi/.codex/skills/push/SKILL.md +0 -29
  111. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +0 -29
  112. package/runtime/pi/.codex/skills/review-pr/SKILL.md +0 -45
  113. package/runtime/pi/.codex/skills/scroll/SKILL.md +0 -288
  114. package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +0 -4
  115. package/runtime/pi/.codex/skills/scroll/assets/shell.html +0 -871
  116. package/runtime/pi/.codex/skills/search/SKILL.md +0 -79
  117. package/runtime/pi/.codex/skills/session-view/SKILL.md +0 -29
  118. package/runtime/pi/.codex/skills/setup/SKILL.md +0 -29
  119. package/runtime/pi/.codex/skills/summon/SKILL.md +0 -45
  120. package/runtime/pi/.codex/skills/task/SKILL.md +0 -29
  121. package/runtime/pi/.codex/skills/teams-connect/SKILL.md +0 -29
  122. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +0 -29
  123. package/runtime/pi/.codex/skills/the-spiral/SKILL.md +0 -84
  124. package/runtime/pi/.codex/skills/thread/SKILL.md +0 -29
  125. package/runtime/pi/.codex/skills/todo/SKILL.md +0 -45
@@ -33,6 +33,71 @@ elif [ "$HOUR" -lt 17 ]; then TIME_OF_DAY="afternoon"
33
33
  else TIME_OF_DAY="evening"
34
34
  fi
35
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
+
36
101
  # 1. Recent handoffs (background)
37
102
  (
38
103
  JSON="[]"
@@ -61,7 +126,7 @@ fi
61
126
  FIRST=true
62
127
  for F in "$SCRIPT_DIR/memory/quests/"*.md; do
63
128
  [ -e "$F" ] || continue
64
- echo "$F" | grep -q draft && continue
129
+ case "$F" in *draft*) continue ;; esac
65
130
  NAME=$(basename "$F" .md)
66
131
  $FIRST || JSON="$JSON,"
67
132
  JSON="$JSON\"$NAME\""
@@ -77,8 +142,8 @@ fi
77
142
  git log --author="$AUTHOR" --format="%ar|%s" -1 2>/dev/null > "$CTX_DIR/activity" || echo "" > "$CTX_DIR/activity"
78
143
  ) &
79
144
 
80
- # 3b. Personal todos from graph (background)
81
- (
145
+ # 3b. Personal todos from graph (background; seeded)
146
+ [ "$CTX_SEED_USED" != "true" ] && (
82
147
  _API_URL=$(jq -r '.api_url // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
83
148
  _API_KEY=$(grep '^EGREGORE_API_KEY=' "${ENV_FILE:-$SCRIPT_DIR/.env}" 2>/dev/null | cut -d'=' -f2-)
84
149
  if [ -n "$_API_URL" ] && [ -n "$_API_KEY" ]; then
@@ -96,6 +161,9 @@ fi
96
161
  ) > "$CTX_DIR/todos" 2>/dev/null &
97
162
 
98
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.
99
167
  (
100
168
  # --- Read config inside subshell ---
101
169
  _API_URL=$(jq -r '.api_url // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
@@ -104,7 +172,13 @@ fi
104
172
  # --- Graph: last-seen per person (excluding self) ---
105
173
  GRAPH_DATA="[]"
106
174
  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"
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"
108
182
  GRAPH_RAW=$(bash "$SCRIPT_DIR/bin/graph.sh" query "$CYPHER" "$(jq -n --arg me "$AUTHOR" '{me: $me}')" 2>/dev/null || echo "")
109
183
  if [ -n "$GRAPH_RAW" ]; then
110
184
  GRAPH_DATA=$(echo "$GRAPH_RAW" | jq '[.values[] | {name: .[0], lastSeen: .[1]}]' 2>/dev/null || echo "[]")
@@ -112,19 +186,26 @@ fi
112
186
  fi
113
187
 
114
188
  # --- 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
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.
116
193
  GIT_COMMIT_DATA="{}"
117
194
  GIT_REF_DATA=$(git -C "$SCRIPT_DIR" for-each-ref --sort=-committerdate \
118
195
  --format='%(refname:short)|%(committerdate:unix)' refs/remotes/origin/dev/ 2>/dev/null || echo "")
119
196
  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 '
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 '
125
205
  [inputs | split("|") | {(.[0]): .[1]}]
126
206
  | add // {}
127
207
  ' 2>/dev/null || echo "{}")
208
+ [ -z "$GIT_COMMIT_DATA" ] && GIT_COMMIT_DATA="{}"
128
209
  fi
129
210
 
130
211
  # --- Git: active dev/* branches (excluding self) ---
@@ -137,178 +218,142 @@ fi
137
218
 
138
219
  BRANCH_MAP="{}"
139
220
  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]}]
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("|"))}]
149
233
  | group_by(.author)
150
234
  | map({(.[0].author): [.[] | .branch]})
151
235
  | add // {}
152
236
  ' 2>/dev/null || echo "{}")
237
+ [ -z "$BRANCH_MAP" ] && BRANCH_MAP="{}"
153
238
  fi
154
239
 
155
240
  # --- Local-mode presence: scan session + wrap files for lastSeen ---
156
- # When graph has no data, these files provide temporal presence information
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.
157
244
  FILE_PRESENCE="{}"
158
245
  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
- )
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
181
266
  fi
182
267
 
183
268
  # --- "Working on": latest session/handoff/wrap topic per person (file-based) ---
184
269
  # Gives every team surface (greeting "around", dashboard) something legible in
185
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.
186
273
  _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
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="{}"
259
296
  fi
260
297
 
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"
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"
312
357
  ) &
313
358
 
314
359
  # 5. Soul self-summary (background)
@@ -437,8 +482,8 @@ fi
437
482
  echo "$PENDING_JSON" > "$CTX_DIR/pending_questions"
438
483
  ) &
439
484
 
440
- # 7. Graph health (background — zero added latency, runs in parallel)
441
- (
485
+ # 7. Graph health (background — zero added latency, runs in parallel; seeded)
486
+ [ "$CTX_SEED_USED" != "true" ] && (
442
487
  if [ "$LOCAL_MODE" = "true" ]; then
443
488
  echo "skip"
444
489
  elif bash "$SCRIPT_DIR/bin/graph.sh" test 2>/dev/null | grep -q "Connected"; then
@@ -454,7 +499,7 @@ fi
454
499
  # status field — an earlier grep for the word "connected" read a human sentence
455
500
  # that the consent rewrite removed, so the footer showed telegram ✗ while the
456
501
  # bot was answering.
457
- (
502
+ [ "$CTX_SEED_USED" != "true" ] && (
458
503
  if [ "$LOCAL_MODE" = "true" ]; then
459
504
  echo "skip"
460
505
  elif bash "$SCRIPT_DIR/bin/notify.sh" test 2>/dev/null \
@@ -465,8 +510,8 @@ fi
465
510
  fi
466
511
  ) > "$CTX_DIR/telegram_health" 2>/dev/null &
467
512
 
468
- # 9. Lifecycle events: merged PRs + implemented handoffs (background)
469
- (
513
+ # 9. Lifecycle events: merged PRs + implemented handoffs (background; seeded)
514
+ [ "$CTX_SEED_USED" != "true" ] && (
470
515
  GH_USER_LC=""
471
516
  if [ -f "$STATE_FILE" ]; then
472
517
  GH_USER_LC=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
@@ -501,8 +546,8 @@ fi
501
546
  '{merged_prs: $merged, implemented_handoffs: $impl}' 2>/dev/null || echo '{"merged_prs":[],"implemented_handoffs":[]}'
502
547
  ) > "$CTX_DIR/lifecycle" 2>/dev/null &
503
548
 
504
- # 10. Last Pulse brief (background) — gated on feature flag + API key
505
- (
549
+ # 10. Last Pulse brief (background) — gated on feature flag + API key; seeded
550
+ [ "$CTX_SEED_USED" != "true" ] && (
506
551
  PULSE_ENABLED=$(jq -r '.features.pulse // "false"' "$CONFIG" 2>/dev/null || echo "false")
507
552
  HAS_API_KEY=false
508
553
  _env_file="${ENV_FILE:-$SCRIPT_DIR/.env}"
@@ -533,6 +578,9 @@ fi
533
578
  ) > "$CTX_DIR/pulse_brief" 2>/dev/null &
534
579
 
535
580
  # 11. Momentum metrics (background)
581
+ # Always live, even when seeded: _metrics_gather mixes graph reads (which ride
582
+ # the warm cache) with local git/file scans — sessions, commits, handoffs,
583
+ # knowledge — whose freshness the greeting should reflect this instant.
536
584
  (
537
585
  source "$SCRIPT_DIR/bin/lib/metrics.sh"
538
586
  _metrics_gather