create-egregore 0.19.23 → 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 (119) 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 +35 -11
  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-sync-skills.sh +1 -0
  18. package/runtime/codex/bin/graph.sh +21 -1
  19. package/runtime/codex/bin/handoff-run.sh +25 -5
  20. package/runtime/codex/bin/lib/context.sh +220 -172
  21. package/runtime/codex/bin/lib/dashboard-artifact.sh +113 -13
  22. package/runtime/codex/bin/lib/git-sync.sh +44 -8
  23. package/runtime/codex/bin/lib/greeting.sh +34 -11
  24. package/runtime/codex/bin/lib/identity.sh +32 -14
  25. package/runtime/codex/bin/person.py +20 -0
  26. package/runtime/codex/manifest.json +3 -72
  27. package/runtime/pi/.codex/skills/handoff/SKILL.md +35 -11
  28. package/runtime/pi/.codex/skills/save/SKILL.md +39 -4
  29. package/runtime/pi/.codex/spec-manifest.json +2 -2
  30. package/runtime/pi/.pi/APPEND_SYSTEM.md +18 -1
  31. package/runtime/pi/.pi/spec-manifest.json +2 -2
  32. package/runtime/pi/AGENTS.md +18 -1
  33. package/runtime/pi/bin/agent.sh +28 -14
  34. package/runtime/pi/bin/attendant.sh +169 -6
  35. package/runtime/pi/bin/codex-render-spec.mjs +18 -1
  36. package/runtime/pi/bin/codex-session-start.sh +13 -0
  37. package/runtime/pi/bin/codex-sync-skills.sh +1 -0
  38. package/runtime/pi/bin/graph.sh +21 -1
  39. package/runtime/pi/bin/handoff-run.sh +25 -5
  40. package/runtime/pi/bin/lib/context.sh +220 -172
  41. package/runtime/pi/bin/lib/dashboard-artifact.sh +113 -13
  42. package/runtime/pi/bin/lib/git-sync.sh +44 -8
  43. package/runtime/pi/bin/lib/greeting.sh +34 -11
  44. package/runtime/pi/bin/lib/identity.sh +32 -14
  45. package/runtime/pi/bin/person.py +20 -0
  46. package/runtime/pi/bin/pi-product-workflows.mjs +5 -1
  47. package/runtime/pi/manifest.json +3 -72
  48. package/runtime/codex/.codex/skills/add/SKILL.md +0 -29
  49. package/runtime/codex/.codex/skills/answer/SKILL.md +0 -29
  50. package/runtime/codex/.codex/skills/ask/SKILL.md +0 -67
  51. package/runtime/codex/.codex/skills/audit/SKILL.md +0 -29
  52. package/runtime/codex/.codex/skills/autosave/SKILL.md +0 -29
  53. package/runtime/codex/.codex/skills/branch/SKILL.md +0 -29
  54. package/runtime/codex/.codex/skills/commit/SKILL.md +0 -29
  55. package/runtime/codex/.codex/skills/demo-mode/SKILL.md +0 -29
  56. package/runtime/codex/.codex/skills/deploy-bridge/SKILL.md +0 -29
  57. package/runtime/codex/.codex/skills/design/SKILL.md +0 -29
  58. package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +0 -29
  59. package/runtime/codex/.codex/skills/emissary/SKILL.md +0 -45
  60. package/runtime/codex/.codex/skills/env/SKILL.md +0 -29
  61. package/runtime/codex/.codex/skills/ingest-google/SKILL.md +0 -29
  62. package/runtime/codex/.codex/skills/minion/SKILL.md +0 -29
  63. package/runtime/codex/.codex/skills/mock/SKILL.md +0 -29
  64. package/runtime/codex/.codex/skills/pr/SKILL.md +0 -29
  65. package/runtime/codex/.codex/skills/product-voice/SKILL.md +0 -29
  66. package/runtime/codex/.codex/skills/project/SKILL.md +0 -45
  67. package/runtime/codex/.codex/skills/pull/SKILL.md +0 -29
  68. package/runtime/codex/.codex/skills/push/SKILL.md +0 -29
  69. package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +0 -29
  70. package/runtime/codex/.codex/skills/review-pr/SKILL.md +0 -45
  71. package/runtime/codex/.codex/skills/scroll/SKILL.md +0 -288
  72. package/runtime/codex/.codex/skills/scroll/agents/openai.yaml +0 -4
  73. package/runtime/codex/.codex/skills/scroll/assets/shell.html +0 -871
  74. package/runtime/codex/.codex/skills/search/SKILL.md +0 -79
  75. package/runtime/codex/.codex/skills/session-view/SKILL.md +0 -29
  76. package/runtime/codex/.codex/skills/setup/SKILL.md +0 -29
  77. package/runtime/codex/.codex/skills/summon/SKILL.md +0 -45
  78. package/runtime/codex/.codex/skills/task/SKILL.md +0 -29
  79. package/runtime/codex/.codex/skills/teams-connect/SKILL.md +0 -29
  80. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +0 -29
  81. package/runtime/codex/.codex/skills/the-spiral/SKILL.md +0 -84
  82. package/runtime/codex/.codex/skills/thread/SKILL.md +0 -29
  83. package/runtime/codex/.codex/skills/todo/SKILL.md +0 -45
  84. package/runtime/pi/.codex/skills/add/SKILL.md +0 -29
  85. package/runtime/pi/.codex/skills/answer/SKILL.md +0 -29
  86. package/runtime/pi/.codex/skills/ask/SKILL.md +0 -67
  87. package/runtime/pi/.codex/skills/audit/SKILL.md +0 -29
  88. package/runtime/pi/.codex/skills/autosave/SKILL.md +0 -29
  89. package/runtime/pi/.codex/skills/branch/SKILL.md +0 -29
  90. package/runtime/pi/.codex/skills/commit/SKILL.md +0 -29
  91. package/runtime/pi/.codex/skills/demo-mode/SKILL.md +0 -29
  92. package/runtime/pi/.codex/skills/deploy-bridge/SKILL.md +0 -29
  93. package/runtime/pi/.codex/skills/design/SKILL.md +0 -29
  94. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +0 -29
  95. package/runtime/pi/.codex/skills/emissary/SKILL.md +0 -45
  96. package/runtime/pi/.codex/skills/env/SKILL.md +0 -29
  97. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +0 -29
  98. package/runtime/pi/.codex/skills/minion/SKILL.md +0 -29
  99. package/runtime/pi/.codex/skills/mock/SKILL.md +0 -29
  100. package/runtime/pi/.codex/skills/pr/SKILL.md +0 -29
  101. package/runtime/pi/.codex/skills/product-voice/SKILL.md +0 -29
  102. package/runtime/pi/.codex/skills/project/SKILL.md +0 -45
  103. package/runtime/pi/.codex/skills/pull/SKILL.md +0 -29
  104. package/runtime/pi/.codex/skills/push/SKILL.md +0 -29
  105. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +0 -29
  106. package/runtime/pi/.codex/skills/review-pr/SKILL.md +0 -45
  107. package/runtime/pi/.codex/skills/scroll/SKILL.md +0 -288
  108. package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +0 -4
  109. package/runtime/pi/.codex/skills/scroll/assets/shell.html +0 -871
  110. package/runtime/pi/.codex/skills/search/SKILL.md +0 -79
  111. package/runtime/pi/.codex/skills/session-view/SKILL.md +0 -29
  112. package/runtime/pi/.codex/skills/setup/SKILL.md +0 -29
  113. package/runtime/pi/.codex/skills/summon/SKILL.md +0 -45
  114. package/runtime/pi/.codex/skills/task/SKILL.md +0 -29
  115. package/runtime/pi/.codex/skills/teams-connect/SKILL.md +0 -29
  116. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +0 -29
  117. package/runtime/pi/.codex/skills/the-spiral/SKILL.md +0 -84
  118. package/runtime/pi/.codex/skills/thread/SKILL.md +0 -29
  119. package/runtime/pi/.codex/skills/todo/SKILL.md +0 -45
@@ -12,6 +12,8 @@ set -euo pipefail
12
12
  # [--recipient <name>] \
13
13
  # [--project <name>] \
14
14
  # [--intent <action|feedback|fyi>] \
15
+ # [--content-mode <supplied|generated>] \
16
+ # [--include-session-artifacts] \
15
17
  # <<'HANDOFFEOF'
16
18
  # ...full markdown body of the handoff...
17
19
  # HANDOFFEOF
@@ -19,7 +21,7 @@ set -euo pipefail
19
21
  # The full file body is read from stdin. The script:
20
22
  # 1. Computes the file path: memory/handoffs/YYYY-MM/DD-author-slug.md
21
23
  # 2. Writes the file
22
- # 3. Appends ## Repo State section (from bin/repo-state.sh) if non-empty
24
+ # 3. In generated mode, appends ## Repo State when non-empty
23
25
  # 4. Prepends memory/handoffs/index.md with a new entry
24
26
  # 5. Runs bin/index-handoff.sh (connected mode only) — creates Session node
25
27
  # and returns a subgraph snapshot in the result JSON
@@ -51,6 +53,8 @@ NO_PUSH=0
51
53
  NO_NOTIFY=0
52
54
  NO_PUBLISH=0
53
55
  COMPOSED="" # optional house-kit JSON — the agent-composed render spec
56
+ CONTENT_MODE="generated"
57
+ INCLUDE_SESSION_ARTIFACTS=0
54
58
 
55
59
  while [ $# -gt 0 ]; do
56
60
  case "$1" in
@@ -60,6 +64,8 @@ while [ $# -gt 0 ]; do
60
64
  --project) PROJECT="${2:-}"; shift 2 ;;
61
65
  --intent) INTENT="${2:-}"; shift 2 ;;
62
66
  --composed) COMPOSED="${2:-}"; shift 2 ;;
67
+ --content-mode) CONTENT_MODE="${2:-}"; shift 2 ;;
68
+ --include-session-artifacts) INCLUDE_SESSION_ARTIFACTS=1; shift ;;
63
69
  --no-push) NO_PUSH=1; shift ;;
64
70
  --no-notify) NO_NOTIFY=1; shift ;;
65
71
  --no-publish) NO_PUBLISH=1; shift ;;
@@ -72,7 +78,7 @@ done
72
78
  # markdown file is ALWAYS the memory record + graph index either way.
73
79
  RENDER_TYPE="handoff"
74
80
  RENDER_SRC="" # resolved to $ABS_FILE after the file is written, unless composed
75
- if [ -n "$COMPOSED" ] && [ -f "$COMPOSED" ]; then
81
+ if [ "$CONTENT_MODE" = "generated" ] && [ -n "$COMPOSED" ] && [ -f "$COMPOSED" ]; then
76
82
  RENDER_TYPE="composed"
77
83
  RENDER_SRC="$COMPOSED"
78
84
  fi
@@ -83,6 +89,14 @@ case "$INTENT" in
83
89
  action|feedback|fyi) ;;
84
90
  *) echo "Invalid --intent: $INTENT (action|feedback|fyi)" >&2; exit 1 ;;
85
91
  esac
92
+ case "$CONTENT_MODE" in
93
+ supplied|generated) ;;
94
+ *) echo "Invalid --content-mode: $CONTENT_MODE (supplied|generated)" >&2; exit 1 ;;
95
+ esac
96
+ if [ "$CONTENT_MODE" = "supplied" ] && [ -n "$COMPOSED" ]; then
97
+ echo "--composed cannot be used with --content-mode supplied" >&2
98
+ exit 1
99
+ fi
86
100
 
87
101
  MODE=$(jq -r '.mode // "connected"' "$CONFIG" 2>/dev/null || echo "connected")
88
102
  TODAY=$(date +%Y-%m-%d)
@@ -140,6 +154,7 @@ MISSING_FROM=0; _has_frontmatter_key from || _has_frontmatter_key author ||
140
154
  MISSING_DATE=0; _has_frontmatter_key date || MISSING_DATE=1
141
155
  MISSING_TOPIC=0; _has_frontmatter_key topic || MISSING_TOPIC=1
142
156
  MISSING_INTENT=0; _has_frontmatter_key intent || MISSING_INTENT=1
157
+ MISSING_CONTENT_MODE=0; _has_frontmatter_key content_mode || MISSING_CONTENT_MODE=1
143
158
  MISSING_RECIPIENT=0
144
159
  if [ -n "$RECIPIENT" ]; then
145
160
  _has_frontmatter_key addressed_to || _has_frontmatter_key to || MISSING_RECIPIENT=1
@@ -150,9 +165,10 @@ awk \
150
165
  -v schema="$MISSING_SCHEMA" -v mode="$MISSING_MODE" -v kind="$MISSING_KIND" \
151
166
  -v from="$MISSING_FROM" -v date_missing="$MISSING_DATE" \
152
167
  -v topic_missing="$MISSING_TOPIC" -v intent_missing="$MISSING_INTENT" \
168
+ -v content_mode_missing="$MISSING_CONTENT_MODE" \
153
169
  -v recipient_missing="$MISSING_RECIPIENT" \
154
170
  -v author="$AUTHOR" -v date="$TODAY" -v topic="$TOPIC" \
155
- -v intent="$INTENT" -v recipient="$RECIPIENT" '
171
+ -v intent="$INTENT" -v recipient="$RECIPIENT" -v content_mode="$CONTENT_MODE" '
156
172
  function metadata() {
157
173
  if (schema) print "capture_schema: egregore-capture/v1"
158
174
  if (mode) print "capture_mode: addressed"
@@ -162,13 +178,14 @@ awk \
162
178
  if (date_missing) print "date: " date
163
179
  if (topic_missing) print "topic: " topic
164
180
  if (intent_missing) print "intent: " intent
181
+ if (content_mode_missing) print "content_mode: " content_mode
165
182
  }
166
183
  BEGIN {
167
184
  if (!has_frontmatter) {
168
185
  print "---"
169
186
  metadata()
170
187
  print "---"
171
- print ""
188
+ if (content_mode != "supplied") print ""
172
189
  }
173
190
  }
174
191
  has_frontmatter && NR == 1 {
@@ -182,7 +199,10 @@ awk \
182
199
  # --- Append repo state ---------------------------------------------------
183
200
  # --no-pr skips `gh pr list` (~400–600ms per repo). A detached PR backfill
184
201
  # after the parallel phase rewrites `—` → `#N` and re-pushes memory.
185
- REPO_STATE=$(bash "$SCRIPT_DIR/bin/repo-state.sh" --no-pr 2>/dev/null || true)
202
+ REPO_STATE=""
203
+ if [ "$CONTENT_MODE" = "generated" ] || [ "$INCLUDE_SESSION_ARTIFACTS" = "1" ]; then
204
+ REPO_STATE=$(bash "$SCRIPT_DIR/bin/repo-state.sh" --no-pr 2>/dev/null || true)
205
+ fi
186
206
  if [ -n "$REPO_STATE" ]; then
187
207
  printf '\n%s\n' "$REPO_STATE" >> "$ABS_FILE"
188
208
  fi
@@ -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