create-egregore 0.19.3 → 0.19.6

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 (154) hide show
  1. package/assets/egregore-launcher.js +410 -26
  2. package/bin/cli.js +34 -20
  3. package/lib/local.js +84 -11
  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 +8 -6
  8. package/runtime/codex/.codex/hooks/search-hint.js +65 -0
  9. package/runtime/codex/.codex/hooks.json +13 -0
  10. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +14 -10
  11. package/runtime/codex/.codex/skills/minion/SKILL.md +25 -0
  12. package/runtime/codex/.codex/skills/scroll/SKILL.md +168 -0
  13. package/runtime/codex/.codex/skills/scroll/agents/openai.yaml +4 -0
  14. package/runtime/codex/.codex/skills/scroll/assets/shell.html +542 -0
  15. package/runtime/codex/.codex/skills/search/SKILL.md +56 -0
  16. package/runtime/codex/.codex/skills/session-view/SKILL.md +25 -0
  17. package/runtime/codex/.codex/skills/thread/SKILL.md +25 -0
  18. package/runtime/codex/.codex/skills/view/SKILL.md +147 -32
  19. package/runtime/codex/.codex/skills/view/agents/openai.yaml +4 -0
  20. package/runtime/codex/.codex/skills/view/assets/compose-scaffold.html +221 -0
  21. package/runtime/codex/.codex/spec-manifest.json +4 -4
  22. package/runtime/codex/AGENTS.md +31 -5
  23. package/runtime/codex/bin/activity-data.sh +46 -4
  24. package/runtime/codex/bin/agent.sh +10 -2
  25. package/runtime/codex/bin/codex-install-skills.sh +1 -1
  26. package/runtime/codex/bin/codex-render-spec.mjs +24 -2
  27. package/runtime/codex/bin/codex-session-start.sh +6 -60
  28. package/runtime/codex/bin/codex-skill-render.mjs +20 -12
  29. package/runtime/codex/bin/codex-sync-skills.sh +4 -0
  30. package/runtime/codex/bin/graph-op.sh +56 -5
  31. package/runtime/codex/bin/graph.sh +36 -0
  32. package/runtime/codex/bin/handoff-run.sh +157 -7
  33. package/runtime/codex/bin/handoff-save-egregore.sh +6 -1
  34. package/runtime/codex/bin/index-handoff.sh +9 -3
  35. package/runtime/codex/bin/lib/context.sh +97 -18
  36. package/runtime/codex/bin/lib/dashboard-artifact.sh +1 -1
  37. package/runtime/codex/bin/lib/git-sync.sh +35 -9
  38. package/runtime/codex/bin/lib/greeting.sh +30 -104
  39. package/runtime/codex/bin/lib/metrics.sh +374 -0
  40. package/runtime/codex/bin/publish-artifact.sh +43 -1
  41. package/runtime/codex/bin/settings.sh +274 -0
  42. package/runtime/codex/manifest.json +18 -1
  43. package/runtime/pi/.codex/config.toml +15 -0
  44. package/runtime/pi/.codex/hooks/branch-guard.js +242 -0
  45. package/runtime/pi/.codex/hooks/search-hint.js +65 -0
  46. package/runtime/pi/.codex/hooks.json +30 -0
  47. package/runtime/pi/.codex/skills/activity/SKILL.md +70 -0
  48. package/runtime/pi/.codex/skills/add/SKILL.md +25 -0
  49. package/runtime/pi/.codex/skills/announce/SKILL.md +54 -0
  50. package/runtime/pi/.codex/skills/answer/SKILL.md +25 -0
  51. package/runtime/pi/.codex/skills/archive/SKILL.md +41 -0
  52. package/runtime/pi/.codex/skills/ask/SKILL.md +59 -0
  53. package/runtime/pi/.codex/skills/branch/SKILL.md +25 -0
  54. package/runtime/pi/.codex/skills/checkup/SKILL.md +41 -0
  55. package/runtime/pi/.codex/skills/commit/SKILL.md +25 -0
  56. package/runtime/pi/.codex/skills/contribute/SKILL.md +25 -0
  57. package/runtime/pi/.codex/skills/dark-mode/SKILL.md +25 -0
  58. package/runtime/pi/.codex/skills/dashboard/SKILL.md +59 -0
  59. package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +74 -0
  60. package/runtime/pi/.codex/skills/delete-user/SKILL.md +25 -0
  61. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +25 -0
  62. package/runtime/pi/.codex/skills/emissary/SKILL.md +41 -0
  63. package/runtime/pi/.codex/skills/env/SKILL.md +25 -0
  64. package/runtime/pi/.codex/skills/handoff/SKILL.md +67 -0
  65. package/runtime/pi/.codex/skills/harvest/SKILL.md +107 -0
  66. package/runtime/pi/.codex/skills/ingest/SKILL.md +25 -0
  67. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +25 -0
  68. package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  69. package/runtime/pi/.codex/skills/invite/SKILL.md +40 -0
  70. package/runtime/pi/.codex/skills/issue/SKILL.md +41 -0
  71. package/runtime/pi/.codex/skills/me/SKILL.md +25 -0
  72. package/runtime/pi/.codex/skills/meeting/SKILL.md +41 -0
  73. package/runtime/pi/.codex/skills/minion/SKILL.md +25 -0
  74. package/runtime/pi/.codex/skills/note/SKILL.md +25 -0
  75. package/runtime/pi/.codex/skills/onboarding/SKILL.md +41 -0
  76. package/runtime/pi/.codex/skills/pr/SKILL.md +25 -0
  77. package/runtime/pi/.codex/skills/project/SKILL.md +41 -0
  78. package/runtime/pi/.codex/skills/pull/SKILL.md +25 -0
  79. package/runtime/pi/.codex/skills/push/SKILL.md +25 -0
  80. package/runtime/pi/.codex/skills/quest/SKILL.md +77 -0
  81. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +25 -0
  82. package/runtime/pi/.codex/skills/reflect/SKILL.md +41 -0
  83. package/runtime/pi/.codex/skills/review-pr/SKILL.md +41 -0
  84. package/runtime/pi/.codex/skills/save/SKILL.md +62 -0
  85. package/runtime/pi/.codex/skills/scroll/SKILL.md +168 -0
  86. package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +4 -0
  87. package/runtime/pi/.codex/skills/scroll/assets/shell.html +542 -0
  88. package/runtime/pi/.codex/skills/search/SKILL.md +56 -0
  89. package/runtime/pi/.codex/skills/session-view/SKILL.md +25 -0
  90. package/runtime/pi/.codex/skills/setup/SKILL.md +25 -0
  91. package/runtime/pi/.codex/skills/summon/SKILL.md +41 -0
  92. package/runtime/pi/.codex/skills/sync-repos/SKILL.md +25 -0
  93. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +25 -0
  94. package/runtime/pi/.codex/skills/telemetry/SKILL.md +25 -0
  95. package/runtime/pi/.codex/skills/test/SKILL.md +41 -0
  96. package/runtime/pi/.codex/skills/the-spiral/SKILL.md +84 -0
  97. package/runtime/pi/.codex/skills/thread/SKILL.md +25 -0
  98. package/runtime/pi/.codex/skills/todo/SKILL.md +41 -0
  99. package/runtime/pi/.codex/skills/tui-design/SKILL.md +41 -0
  100. package/runtime/pi/.codex/skills/tutorial/SKILL.md +41 -0
  101. package/runtime/pi/.codex/skills/update/SKILL.md +25 -0
  102. package/runtime/pi/.codex/skills/view/SKILL.md +156 -0
  103. package/runtime/pi/.codex/skills/view/agents/openai.yaml +4 -0
  104. package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +221 -0
  105. package/runtime/pi/.codex/skills/visual-explain/SKILL.md +41 -0
  106. package/runtime/pi/.codex/skills/wrap/SKILL.md +55 -0
  107. package/runtime/pi/.codex/spec-manifest.json +114 -0
  108. package/runtime/pi/.pi/APPEND_SYSTEM.md +255 -0
  109. package/runtime/pi/.pi/extensions/egregore.ts +324 -0
  110. package/runtime/pi/.pi/settings.json +6 -0
  111. package/runtime/pi/.pi/spec-manifest.json +12 -0
  112. package/runtime/pi/AGENTS.md +350 -0
  113. package/runtime/pi/bin/activity-data.sh +242 -0
  114. package/runtime/pi/bin/agent.sh +677 -0
  115. package/runtime/pi/bin/codex-install-skills.sh +78 -0
  116. package/runtime/pi/bin/codex-render-spec.mjs +394 -0
  117. package/runtime/pi/bin/codex-session-start.sh +188 -0
  118. package/runtime/pi/bin/codex-skill-render.mjs +323 -0
  119. package/runtime/pi/bin/codex-sync-skills.sh +227 -0
  120. package/runtime/pi/bin/dashboard-data.sh +178 -0
  121. package/runtime/pi/bin/graph-batch.sh +60 -0
  122. package/runtime/pi/bin/graph-op.sh +431 -0
  123. package/runtime/pi/bin/graph-wal.sh +228 -0
  124. package/runtime/pi/bin/graph.sh +177 -0
  125. package/runtime/pi/bin/handoff-pr-backfill.sh +86 -0
  126. package/runtime/pi/bin/handoff-run.sh +581 -0
  127. package/runtime/pi/bin/handoff-save-egregore.sh +136 -0
  128. package/runtime/pi/bin/index-handoff.sh +312 -0
  129. package/runtime/pi/bin/lib/artifact-id.sh +46 -0
  130. package/runtime/pi/bin/lib/config.sh +71 -0
  131. package/runtime/pi/bin/lib/context.sh +542 -0
  132. package/runtime/pi/bin/lib/dashboard-artifact.sh +142 -0
  133. package/runtime/pi/bin/lib/ensure-develop.sh +183 -0
  134. package/runtime/pi/bin/lib/git-sync.sh +288 -0
  135. package/runtime/pi/bin/lib/greeting.sh +340 -0
  136. package/runtime/pi/bin/lib/hash.sh +13 -0
  137. package/runtime/pi/bin/lib/identity.sh +174 -0
  138. package/runtime/pi/bin/lib/metrics.sh +374 -0
  139. package/runtime/pi/bin/lib/permissions.sh +106 -0
  140. package/runtime/pi/bin/lib/time.sh +56 -0
  141. package/runtime/pi/bin/lib/worktree-links.sh +54 -0
  142. package/runtime/pi/bin/notify.sh +207 -0
  143. package/runtime/pi/bin/pi-branch-policy.mjs +15 -0
  144. package/runtime/pi/bin/pi-product-workflows.mjs +324 -0
  145. package/runtime/pi/bin/pi-render-spec.mjs +114 -0
  146. package/runtime/pi/bin/pi-session-start.sh +24 -0
  147. package/runtime/pi/bin/publish-artifact.sh +201 -0
  148. package/runtime/pi/bin/publish-references.sh +144 -0
  149. package/runtime/pi/bin/repo-state.sh +99 -0
  150. package/runtime/pi/bin/telemetry.sh +391 -0
  151. package/runtime/pi/bin/worktree-create.sh +118 -0
  152. package/runtime/pi/bin/worktree-remove.sh +50 -0
  153. package/runtime/pi/bin/worktree.sh +198 -0
  154. package/runtime/pi/manifest.json +176 -0
@@ -4,9 +4,8 @@
4
4
  # Renders the full session greeting:
5
5
  # - ASCII art banner
6
6
  # - Identity line (org/repo + user + branch)
7
- # - "For you" section (handoffs, last activity)
8
- # - "Around" section (team presence)
9
- # - "Merged" section (PRs, implemented handoffs)
7
+ # - Momentum board (sessions, commits, handoffs, knowledge)
8
+ # - Pending questions signal
10
9
  # - Health footer
11
10
  # - Alias migration
12
11
  # - Session context JSON (hidden, for Claude)
@@ -64,108 +63,24 @@ if [ "$ID_PADDING" -lt 1 ]; then ID_PADDING=1; fi
64
63
  printf "\n%s%*s%s\n" "$IDENTITY_LEFT" "$ID_PADDING" "" "$IDENTITY_RIGHT"
65
64
  echo "$SEPARATOR"
66
65
 
67
- # --- Shared rendering state ---
68
- TEAM_DATA=$(cat "$CTX_DIR/team" 2>/dev/null || echo "[]")
69
- TEAM_DATA_COUNT=$(echo "$TEAM_DATA" | jq 'length' 2>/dev/null || echo "0")
70
- NOW_RENDER=$(date +%s)
71
- ONLINE_THRESHOLD=7200 # 2 hours — considered "online"
72
- RECENCY_DAYS=3
73
- RECENCY_THRESHOLD=$((RECENCY_DAYS * 86400))
74
- TODAY_DATE=$(date +%Y-%m-%d)
75
- TODAY_MIDNIGHT=$(date -j -f "%Y-%m-%d" "$TODAY_DATE" +%s 2>/dev/null || \
76
- date -d "$TODAY_DATE" +%s 2>/dev/null || echo "$NOW_RENDER")
77
- # Column layout: 2(indent) + 2(dot+space) + 9(name) + 12(time) + 40(info) = 65
78
- MAX_INFO=40
79
-
80
- # --- Section 1: For you (always present) ---
81
- echo " ◦ for you"
82
- ADDRESSED_RICH=$(cat "$CTX_DIR/addressed_rich" 2>/dev/null || echo "[]")
83
- ADDRESSED_COUNT=$(echo "$ADDRESSED_RICH" | jq 'length' 2>/dev/null || echo "0")
84
-
85
- # Handoffs addressed to you
86
- if [ "$ADDRESSED_COUNT" -gt 0 ] 2>/dev/null && [ "$ADDRESSED_COUNT" != "0" ]; then
87
- echo "$ADDRESSED_RICH" | jq -r '.[] | "\(.author)\t\(.date)\t\(.topic)"' 2>/dev/null | while IFS=$'\t' read -r H_AUTHOR H_DATE H_TOPIC; do
88
- [ -z "$H_AUTHOR" ] && continue
89
- # Convert date to epoch (UTC) and compute calendar-day diff
90
- H_DATE_ONLY="${H_DATE%%T*}"
91
- H_EVENT_MIDNIGHT=$(date -j -f "%Y-%m-%d" "$H_DATE_ONLY" +%s 2>/dev/null || \
92
- date -d "$H_DATE_ONLY" +%s 2>/dev/null || echo "0")
93
- H_CAL_DAYS=$(( (TODAY_MIDNIGHT - H_EVENT_MIDNIGHT) / 86400 ))
94
- # Skip if older than recency threshold
95
- [ "$H_CAL_DAYS" -gt "$RECENCY_DAYS" ] 2>/dev/null && continue
96
- H_NAME=$(echo "$H_AUTHOR" | awk '{print tolower($1)}')
97
- if [ ${#H_NAME} -gt 8 ]; then H_NAME="${H_NAME:0:8}"; fi
98
- # Online status from team presence data
99
- H_SEEN_EPOCH=$(echo "$TEAM_DATA" | jq -r --arg n "$H_NAME" '[.[] | select((.name | ascii_downcase) == $n) | .last_seen_sort] | .[0] // 0' 2>/dev/null || echo "0")
100
- if [ "$H_SEEN_EPOCH" -gt 0 ] 2>/dev/null && [ $(( NOW_RENDER - H_SEEN_EPOCH )) -lt $ONLINE_THRESHOLD ] 2>/dev/null; then
101
- DOT="●"
102
- else
103
- DOT="○"
104
- fi
105
- # Calendar-day relative label
106
- if [ "$H_CAL_DAYS" -le 0 ] 2>/dev/null; then H_AGO="today"
107
- elif [ "$H_CAL_DAYS" -eq 1 ] 2>/dev/null; then H_AGO="yesterday"
108
- else H_AGO="${H_CAL_DAYS}d ago"
109
- fi
110
- if [ ${#H_TOPIC} -gt $MAX_INFO ]; then H_TOPIC="${H_TOPIC:0:$((MAX_INFO - 1))}…"; fi
111
- printf " %s %-9s%-12s%s\n" "$DOT" "$H_NAME" "$H_AGO" "$H_TOPIC"
112
- done
113
- fi
114
-
115
- # Fallback: last activity if no handoffs
116
- if [ "$ADDRESSED_COUNT" = "0" ] || [ -z "$ADDRESSED_COUNT" ]; then
117
- LAST_ACTIVITY=$(cat "$CTX_DIR/activity" 2>/dev/null || echo "")
118
- if [ -n "$LAST_ACTIVITY" ]; then
119
- LA_TIME=$(echo "$LAST_ACTIVITY" | cut -d'|' -f1)
120
- # Compact relative time: "20 hours ago" → "20h ago", "3 days ago" → "3d ago"
121
- LA_TIME=$(echo "$LA_TIME" | sed 's/ hours\{0,1\} ago/h ago/; s/ days\{0,1\} ago/d ago/; s/ minutes\{0,1\} ago/m ago/; s/ weeks\{0,1\} ago/w ago/; s/ months\{0,1\} ago/mo ago/')
122
- LA_MSG=$(echo "$LAST_ACTIVITY" | cut -d'|' -f2-)
123
- if [ ${#LA_MSG} -gt $MAX_INFO ]; then LA_MSG="${LA_MSG:0:$((MAX_INFO - 1))}…"; fi
124
- printf " ◇ %-9s%-12s%s\n" "you" "$LA_TIME" "$LA_MSG"
125
- fi
126
- fi
127
-
128
- echo ""
129
- # --- Section 2: Around (team presence with online status) ---
130
- if [ "$TEAM_DATA_COUNT" -gt 0 ] 2>/dev/null && [ "$TEAM_DATA_COUNT" != "0" ]; then
131
- echo " ◦ around"
132
- # Presence roster with online/offline dots (hide inactive >3 days)
133
- STALE_THRESHOLD=$((RECENCY_DAYS * 86400))
134
- echo "$TEAM_DATA" | jq -r '.[] | "\(.name)\t\(.last_seen_sort)\t\(.last_seen)\t\(.branches | join(", "))"' 2>/dev/null | while IFS=$'\t' read -r P_NAME P_EPOCH P_SEEN P_BRANCHES; do
135
- [ -z "$P_NAME" ] && continue
136
- # Skip if no recent activity (within recency window)
137
- if [ "$P_EPOCH" -le 0 ] 2>/dev/null; then
138
- continue
139
- elif [ $(( NOW_RENDER - P_EPOCH )) -ge $STALE_THRESHOLD ] 2>/dev/null; then
140
- continue
141
- fi
142
- if [ ${#P_NAME} -gt 8 ]; then P_NAME="${P_NAME:0:8}"; fi
143
- # Online: last seen within threshold
144
- if [ $(( NOW_RENDER - P_EPOCH )) -lt $ONLINE_THRESHOLD ] 2>/dev/null; then
145
- DOT="●"
146
- else
147
- DOT="○"
148
- fi
149
- if [ ${#P_BRANCHES} -gt $MAX_INFO ]; then
150
- P_BRANCHES="${P_BRANCHES:0:$((MAX_INFO - 9))}... +more"
151
- fi
152
- printf " %s %-9s%-12s%s\n" "$DOT" "$P_NAME" "$P_SEEN" "$P_BRANCHES"
153
- done
154
- fi
155
-
156
- # --- Section 3: Recently merged PRs + implemented handoffs ---
157
- LIFECYCLE_JSON=$(cat "$CTX_DIR/lifecycle" 2>/dev/null || echo '{}')
158
- MERGED_COUNT=$(echo "$LIFECYCLE_JSON" | jq '.merged_prs.values // [] | length' 2>/dev/null || echo "0")
159
- IMPL_COUNT=$(echo "$LIFECYCLE_JSON" | jq '.implemented_handoffs.values // [] | length' 2>/dev/null || echo "0")
160
-
161
- if [ "$MERGED_COUNT" -gt 0 ] 2>/dev/null || [ "$IMPL_COUNT" -gt 0 ] 2>/dev/null; then
162
- echo ""
163
- echo " ◦ merged"
164
- if [ "$MERGED_COUNT" -gt 0 ] 2>/dev/null; then
165
- echo "$LIFECYCLE_JSON" | jq -r '.merged_prs.values[]? // empty | " ✓ PR #\(.[0]) \(.[1])"' 2>/dev/null || true
66
+ # --- Momentum board ---
67
+ source "$SCRIPT_DIR/bin/lib/metrics.sh"
68
+ _render_momentum_board
69
+
70
+ # Pending questions addressed to you (one summary line — /answer to engage)
71
+ PENDING_Q=$(cat "$CTX_DIR/pending_questions" 2>/dev/null || echo "[]")
72
+ PENDING_Q_COUNT=$(echo "$PENDING_Q" | jq 'length' 2>/dev/null || echo "0")
73
+ if [ "$PENDING_Q_COUNT" -gt 0 ] 2>/dev/null && [ "$PENDING_Q_COUNT" != "0" ]; then
74
+ PQ_SENDERS=$(echo "$PENDING_Q" | jq -r '[.[].from] | unique | join(", ")' 2>/dev/null)
75
+ if [ "$PENDING_Q_COUNT" = "1" ]; then
76
+ PQ_NOUN="question"
77
+ else
78
+ PQ_NOUN="questions"
166
79
  fi
167
- if [ "$IMPL_COUNT" -gt 0 ] 2>/dev/null; then
168
- echo "$LIFECYCLE_JSON" | jq -r '.implemented_handoffs.values[]? // empty | " ✓ \(.[1]) worked on your handoff"' 2>/dev/null || true
80
+ if [ -n "$PQ_SENDERS" ]; then
81
+ printf " ◐ %s pending %s from %s — /answer to engage\n" "$PENDING_Q_COUNT" "$PQ_NOUN" "$PQ_SENDERS"
82
+ else
83
+ printf " ◐ %s pending %s — /answer to engage\n" "$PENDING_Q_COUNT" "$PQ_NOUN"
169
84
  fi
170
85
  fi
171
86
 
@@ -256,6 +171,10 @@ if [ -n "${BOARD_URL:-}" ]; then
256
171
  printf " ◆ %s (board)\n" "$BOARD_URL"
257
172
  fi
258
173
 
174
+ if [ -n "${LOOM_DOCTOR_BRIEF:-}" ]; then
175
+ printf " %s\n" "$LOOM_DOCTOR_BRIEF"
176
+ fi
177
+
259
178
  # Framework updates come through PRs to develop — no separate auto-update channel.
260
179
  # The develop sync above already keeps framework files current.
261
180
  # Use /update for manual upstream pulls when needed.
@@ -288,6 +207,12 @@ CONTEXT_TEAM=$(cat "$CTX_DIR/team" 2>/dev/null || echo "[]")
288
207
  CONTEXT_SOUL=$(cat "$CTX_DIR/soul_summary" 2>/dev/null || echo "")
289
208
  CONTEXT_LIFECYCLE=$(cat "$CTX_DIR/lifecycle" 2>/dev/null || echo '{"merged_prs":[],"implemented_handoffs":[]}')
290
209
  CONTEXT_PULSE=$(cat "$CTX_DIR/pulse_brief" 2>/dev/null || echo '{}')
210
+ if [ -f "$CTX_DIR/metrics" ]; then
211
+ CONTEXT_METRICS=$(jq -c . "$CTX_DIR/metrics" 2>/dev/null || echo "{}")
212
+ [ -n "$CONTEXT_METRICS" ] || CONTEXT_METRICS="{}"
213
+ else
214
+ CONTEXT_METRICS="{}"
215
+ fi
291
216
 
292
217
  # --- Write compact subagent context cache (reuses already-gathered data) ---
293
218
  SUBAGENT_CTX="/tmp/egregore-subagent-ctx-${EGREGORE_SESSION_ID}.txt"
@@ -332,6 +257,7 @@ cat << CTXEOF
332
257
  "team_recent_memory": $CONTEXT_TEAM,
333
258
  "soul_self_summary": "$CONTEXT_SOUL",
334
259
  "lifecycle": $CONTEXT_LIFECYCLE,
260
+ "momentum": $CONTEXT_METRICS,
335
261
  "pulse": $CONTEXT_PULSE
336
262
  }
337
263
  -->
@@ -0,0 +1,374 @@
1
+ # shellcheck shell=bash
2
+ # metrics.sh — Momentum metrics gathering and rendering for startup greetings.
3
+ #
4
+ # Function-only library. Safe to source from bash or zsh.
5
+
6
+ _metrics_zero_json() {
7
+ printf '%s\n' '{"source":"local","sessions":{"buckets":[0,0,0,0,0],"week":0,"prev":0,"all":0},"commits":{"buckets":[0,0,0,0,0],"week":0,"prev":0,"authors":0,"all":0},"handoffs":{"buckets":[0,0,0,0,0],"week":0,"prev":0,"avg_resolution_days":null,"all":0},"knowledge":{"buckets":[0,0,0,0,0],"week":0,"prev":0,"all":0},"members":{"active":0,"total":0}}'
8
+ }
9
+
10
+ _num() {
11
+ awk '{n=$1+0; if (n < 0) n=0; printf "%d\n", n}' 2>/dev/null <<EOF
12
+ $1
13
+ EOF
14
+ }
15
+
16
+ _num_float() {
17
+ awk '{n=$1+0; if (n < 0) n=0; printf "%.1f\n", n}' 2>/dev/null <<EOF
18
+ $1
19
+ EOF
20
+ }
21
+
22
+ _normalize_buckets() {
23
+ set -- $(printf '%s' "$1")
24
+ printf '%d %d %d %d %d\n' "$(_num "${1:-0}")" "$(_num "${2:-0}")" "$(_num "${3:-0}")" "$(_num "${4:-0}")" "$(_num "${5:-0}")"
25
+ }
26
+
27
+ _buckets_json() {
28
+ set -- $(printf '%s' "$(_normalize_buckets "$1")")
29
+ jq -n --argjson a "$(_num "${1:-0}")" --argjson b "$(_num "${2:-0}")" \
30
+ --argjson c "$(_num "${3:-0}")" --argjson d "$(_num "${4:-0}")" \
31
+ --argjson e "$(_num "${5:-0}")" '[$a,$b,$c,$d,$e]' 2>/dev/null || echo '[0,0,0,0,0]'
32
+ }
33
+
34
+ _bucket_week() {
35
+ set -- $(printf '%s' "$(_normalize_buckets "$1")")
36
+ _num "${5:-0}"
37
+ }
38
+
39
+ _bucket_prev() {
40
+ set -- $(printf '%s' "$(_normalize_buckets "$1")")
41
+ _num "${4:-0}"
42
+ }
43
+
44
+ _spark() {
45
+ local values max out v idx glyph
46
+ values="${1:-0 0 0 0 0}"
47
+ max=0
48
+ set -- $(printf '%s' "$values")
49
+ [ "$#" -eq 0 ] && set -- 0 0 0 0 0
50
+ for v in "$@"; do
51
+ v=$(_num "$v")
52
+ [ "$v" -gt "$max" ] 2>/dev/null && max="$v"
53
+ done
54
+ out=""
55
+ for v in "$@"; do
56
+ v=$(_num "$v")
57
+ if [ "$max" -le 0 ] 2>/dev/null; then
58
+ idx=0
59
+ else
60
+ idx=$(( (v * 7) / max ))
61
+ fi
62
+ # Select glyphs explicitly: substring slicing can split UTF-8 characters
63
+ # under Bash 3.2 or a C locale.
64
+ case "$idx" in
65
+ 0) glyph="▁" ;;
66
+ 1) glyph="▂" ;;
67
+ 2) glyph="▃" ;;
68
+ 3) glyph="▄" ;;
69
+ 4) glyph="▅" ;;
70
+ 5) glyph="▆" ;;
71
+ 6) glyph="▇" ;;
72
+ *) glyph="█" ;;
73
+ esac
74
+ out="${out}${glyph}"
75
+ done
76
+ printf '%s\n' "$out"
77
+ }
78
+
79
+ _pct_delta() {
80
+ local cur prev pct
81
+ cur=$(_num "${1:-0}")
82
+ prev=$(_num "${2:-0}")
83
+ [ "$prev" -le 0 ] 2>/dev/null && return 0
84
+ pct=$(( ((cur - prev) * 100) / prev ))
85
+ if [ "$pct" -gt 0 ] 2>/dev/null; then
86
+ printf '↑ %d%%\n' "$pct"
87
+ elif [ "$pct" -lt 0 ] 2>/dev/null; then
88
+ printf '↓ %d%%\n' "$(( -pct ))"
89
+ else
90
+ printf '→ 0%%\n'
91
+ fi
92
+ }
93
+
94
+ _commafy() {
95
+ awk '
96
+ function comma(s, out, sign) {
97
+ s = int(s + 0)
98
+ if (s < 0) { sign = "-"; s = -s } else { sign = "" }
99
+ s = sprintf("%d", s)
100
+ out = ""
101
+ while (length(s) > 3) {
102
+ out = "," substr(s, length(s) - 2, 3) out
103
+ s = substr(s, 1, length(s) - 3)
104
+ }
105
+ return sign s out
106
+ }
107
+ { print comma($1) }
108
+ ' 2>/dev/null <<EOF
109
+ ${1:-0}
110
+ EOF
111
+ }
112
+
113
+ _git_added_buckets() {
114
+ local repo now paths
115
+ repo="$1"
116
+ shift
117
+ now=$(date +%s)
118
+ if [ -d "$repo" ] || [ -f "$repo" ]; then
119
+ git -C "$repo" log --diff-filter=A --since="35 days ago" --format='%ct' -- "$@" 2>/dev/null | \
120
+ awk -v now="$now" '
121
+ BEGIN { for (i=0;i<5;i++) b[i]=0 }
122
+ /^[0-9]+$/ {
123
+ days = int((now - $1) / 86400)
124
+ idx = 4 - int(days / 7)
125
+ if (idx >= 0 && idx < 5) b[idx]++
126
+ }
127
+ END { printf "%d %d %d %d %d\n", b[0], b[1], b[2], b[3], b[4] }
128
+ ' 2>/dev/null || echo "0 0 0 0 0"
129
+ else
130
+ echo "0 0 0 0 0"
131
+ fi
132
+ }
133
+
134
+ _find_count_md() {
135
+ find -L "$@" -name '*.md' ! -name 'index*' 2>/dev/null | wc -l | awk '{print $1+0}' 2>/dev/null || echo 0
136
+ }
137
+
138
+ _metrics_gather() {
139
+ local _root _mem _scratch _API_URL _API_KEY _connected
140
+ _root="${SCRIPT_DIR:-$PWD}"
141
+ _mem="$_root/memory"
142
+ _scratch=$(mktemp -d 2>/dev/null || mktemp -d -t egregore-metrics)
143
+ _API_URL=$(jq -r '.api_url // empty' "$_root/egregore.json" 2>/dev/null || echo "")
144
+ _API_KEY=$(grep '^EGREGORE_API_KEY=' "${ENV_FILE:-$_root/.env}" 2>/dev/null | cut -d'=' -f2- || true)
145
+ _connected=false
146
+ [ -n "$_API_URL" ] && [ -n "$_API_KEY" ] && _connected=true
147
+
148
+ (
149
+ if [ "$_connected" = true ]; then
150
+ raw=$(bash "$_root/bin/graph.sh" query "MATCH (s:Session) WHERE date(s.date) >= date() - duration('P35D')
151
+ OPTIONAL MATCH (s)-[:BY]->(p:Person)
152
+ WITH duration.inDays(date(s.date), date()).days / 7 AS weeksAgo,
153
+ count(s) AS sessions, count(DISTINCT p) AS people
154
+ RETURN weeksAgo, sessions, people ORDER BY weeksAgo" 2>/dev/null || echo "")
155
+ printf '%s' "$raw" | jq -r '
156
+ (.values // []) as $v
157
+ | if ($v|length) == 0 then empty else
158
+ reduce $v[] as $r ({b:[0,0,0,0,0], a:0, ok:0};
159
+ ($r[0] // 99 | tonumber? // 99) as $w
160
+ | if $w >= 0 and $w <= 4 then
161
+ .b[4 - $w] = (($r[1] // 0 | tonumber? // 0) | floor)
162
+ | .ok = 1
163
+ | if $w == 0 then .a = (($r[2] // 0 | tonumber? // 0) | floor) else . end
164
+ else . end)
165
+ | "\(.b|join(" "))|\(.a)|\(.ok)"
166
+ end
167
+ ' 2>/dev/null || echo "0 0 0 0 0|0|0"
168
+ else
169
+ echo "0 0 0 0 0|0|0"
170
+ fi
171
+ ) > "$_scratch/graph_sessions" 2>/dev/null &
172
+
173
+ (
174
+ if [ "$_connected" = true ]; then
175
+ raw=$(bash "$_root/bin/graph.sh" query "MATCH (s:Session) WITH count(s) AS totalSessions MATCH (p:Person)-[:MEMBER_OF]->(:Org) RETURN totalSessions, count(DISTINCT p) AS members" 2>/dev/null || echo "")
176
+ total=$(printf '%s' "$raw" | jq -r '.values[0][0] // 0' 2>/dev/null || echo 0)
177
+ members=$(printf '%s' "$raw" | jq -r '.values[0][1] // 0' 2>/dev/null || echo 0)
178
+ if [ "$(_num "$members")" -eq 0 ] 2>/dev/null; then
179
+ raw2=$(bash "$_root/bin/graph.sh" query "MATCH (p:Person) RETURN count(p)" 2>/dev/null || echo "")
180
+ members=$(printf '%s' "$raw2" | jq -r '.values[0][0] // 0' 2>/dev/null || echo 0)
181
+ fi
182
+ printf '%s|%s\n' "$(_num "$total")" "$(_num "$members")"
183
+ else
184
+ echo "0|0"
185
+ fi
186
+ ) > "$_scratch/graph_totals" 2>/dev/null &
187
+
188
+ (
189
+ if [ "$_connected" = true ]; then
190
+ raw=$(bash "$_root/bin/graph.sh" query "MATCH (s:Session)-[:HANDED_TO]->(:Person)
191
+ WHERE s.handoffStatus = 'done' AND date(s.date) >= date() - duration('P30D')
192
+ AND s.handoffReadDate IS NOT NULL
193
+ WITH duration.inDays(date(s.date), date(s.handoffReadDate)).days AS d
194
+ RETURN avg(d) AS avgDays, count(*) AS n" 2>/dev/null || echo "")
195
+ avg=$(printf '%s' "$raw" | jq -r '.values[0][0] // empty' 2>/dev/null || echo "")
196
+ n=$(printf '%s' "$raw" | jq -r '.values[0][1] // 0' 2>/dev/null || echo 0)
197
+ if [ "$(_num "$n")" -gt 0 ] 2>/dev/null && [ -n "$avg" ]; then _num_float "$avg"; else echo ""; fi
198
+ else
199
+ echo ""
200
+ fi
201
+ ) > "$_scratch/handoff_resolution" 2>/dev/null &
202
+
203
+ (
204
+ buckets=$(_git_added_buckets "$_mem" sessions wraps)
205
+ all=$(_find_count_md "$_root/memory/sessions" "$_root/memory/wraps")
206
+ printf '%s|%s\n' "$buckets" "$all"
207
+ ) > "$_scratch/local_sessions" 2>/dev/null &
208
+
209
+ (
210
+ now=$(date +%s)
211
+ if [ -d "$_mem" ] || [ -f "$_mem" ]; then
212
+ git -C "$_mem" log --since="35 days ago" --format='%ct|%ae' 2>/dev/null | \
213
+ awk -F'|' -v now="$now" '
214
+ BEGIN { for (i=0;i<5;i++) b[i]=0 }
215
+ {
216
+ days = int((now - $1) / 86400)
217
+ idx = 4 - int(days / 7)
218
+ if (idx >= 0 && idx < 5) {
219
+ b[idx]++
220
+ if (idx == 4 && $2 != "") a[$2]=1
221
+ }
222
+ }
223
+ END {
224
+ n=0; for (k in a) n++
225
+ printf "%d %d %d %d %d|%d\n", b[0], b[1], b[2], b[3], b[4], n
226
+ }
227
+ ' 2>/dev/null || echo "0 0 0 0 0|0"
228
+ else
229
+ echo "0 0 0 0 0|0"
230
+ fi
231
+ ) > "$_scratch/commits" 2>/dev/null &
232
+
233
+ (
234
+ if [ -d "$_mem" ] || [ -f "$_mem" ]; then git -C "$_mem" rev-list --count HEAD 2>/dev/null || echo 0; else echo 0; fi
235
+ ) > "$_scratch/commits_all" 2>/dev/null &
236
+
237
+ (
238
+ buckets=$(_git_added_buckets "$_mem" handoffs)
239
+ all=$(_find_count_md "$_root/memory/handoffs")
240
+ printf '%s|%s\n' "$buckets" "$all"
241
+ ) > "$_scratch/handoffs" 2>/dev/null &
242
+
243
+ (
244
+ buckets=$(_git_added_buckets "$_mem" knowledge/decisions knowledge/findings knowledge/patterns)
245
+ all=$(_find_count_md "$_root/memory/knowledge/decisions" "$_root/memory/knowledge/findings" "$_root/memory/knowledge/patterns")
246
+ printf '%s|%s\n' "$buckets" "$all"
247
+ ) > "$_scratch/knowledge" 2>/dev/null &
248
+
249
+ (
250
+ _find_count_md "$_root/memory/people"
251
+ ) > "$_scratch/members_total_local" 2>/dev/null &
252
+
253
+ wait
254
+
255
+ gs=$(cat "$_scratch/graph_sessions" 2>/dev/null || echo "0 0 0 0 0|0|0")
256
+ gt=$(cat "$_scratch/graph_totals" 2>/dev/null || echo "0|0")
257
+ lsess=$(cat "$_scratch/local_sessions" 2>/dev/null || echo "0 0 0 0 0|0")
258
+ commits=$(cat "$_scratch/commits" 2>/dev/null || echo "0 0 0 0 0|0")
259
+ handoffs=$(cat "$_scratch/handoffs" 2>/dev/null || echo "0 0 0 0 0|0")
260
+ knowledge=$(cat "$_scratch/knowledge" 2>/dev/null || echo "0 0 0 0 0|0")
261
+
262
+ graph_valid=$(_num "$(printf '%s' "$gs" | awk -F'|' '{print $3}')")
263
+ if [ "$graph_valid" -gt 0 ] 2>/dev/null; then
264
+ source_name="graph"
265
+ session_buckets=$(_normalize_buckets "$(printf '%s' "$gs" | awk -F'|' '{print $1}')")
266
+ session_all=$(_num "$(printf '%s' "$gt" | awk -F'|' '{print $1}')")
267
+ members_active=$(_num "$(printf '%s' "$gs" | awk -F'|' '{print $2}')")
268
+ members_total=$(_num "$(printf '%s' "$gt" | awk -F'|' '{print $2}')")
269
+ if [ "$session_all" -le 0 ] 2>/dev/null; then
270
+ session_all=$(_num "$(printf '%s' "$lsess" | awk -F'|' '{print $2}')")
271
+ fi
272
+ if [ "$members_total" -le 0 ] 2>/dev/null; then
273
+ members_total=$(_num "$(cat "$_scratch/members_total_local" 2>/dev/null || echo 0)")
274
+ fi
275
+ else
276
+ source_name="local"
277
+ session_buckets=$(_normalize_buckets "$(printf '%s' "$lsess" | awk -F'|' '{print $1}')")
278
+ session_all=$(_num "$(printf '%s' "$lsess" | awk -F'|' '{print $2}')")
279
+ members_active=$(_num "$(printf '%s' "$commits" | awk -F'|' '{print $2}')")
280
+ members_total=$(_num "$(cat "$_scratch/members_total_local" 2>/dev/null || echo 0)")
281
+ fi
282
+
283
+ commit_buckets=$(_normalize_buckets "$(printf '%s' "$commits" | awk -F'|' '{print $1}')")
284
+ handoff_buckets=$(_normalize_buckets "$(printf '%s' "$handoffs" | awk -F'|' '{print $1}')")
285
+ knowledge_buckets=$(_normalize_buckets "$(printf '%s' "$knowledge" | awk -F'|' '{print $1}')")
286
+ avg_res=$(cat "$_scratch/handoff_resolution" 2>/dev/null || echo "")
287
+
288
+ session_buckets_json=$(_buckets_json "$session_buckets")
289
+ commit_buckets_json=$(_buckets_json "$commit_buckets")
290
+ handoff_buckets_json=$(_buckets_json "$handoff_buckets")
291
+ knowledge_buckets_json=$(_buckets_json "$knowledge_buckets")
292
+
293
+ if [ -n "$avg_res" ]; then
294
+ jq -n --arg source "$source_name" \
295
+ --argjson sb "$session_buckets_json" --argjson sw "$(_bucket_week "$session_buckets")" --argjson sp "$(_bucket_prev "$session_buckets")" --argjson sall "$session_all" \
296
+ --argjson cb "$commit_buckets_json" --argjson cw "$(_bucket_week "$commit_buckets")" --argjson cp "$(_bucket_prev "$commit_buckets")" --argjson ca "$(_num "$(printf '%s' "$commits" | awk -F'|' '{print $2}')")" --argjson call "$(_num "$(cat "$_scratch/commits_all" 2>/dev/null || echo 0)")" \
297
+ --argjson hb "$handoff_buckets_json" --argjson hw "$(_bucket_week "$handoff_buckets")" --argjson hp "$(_bucket_prev "$handoff_buckets")" --argjson havg "$avg_res" --argjson hall "$(_num "$(printf '%s' "$handoffs" | awk -F'|' '{print $2}')")" \
298
+ --argjson kb "$knowledge_buckets_json" --argjson kw "$(_bucket_week "$knowledge_buckets")" --argjson kp "$(_bucket_prev "$knowledge_buckets")" --argjson kall "$(_num "$(printf '%s' "$knowledge" | awk -F'|' '{print $2}')")" \
299
+ --argjson ma "$members_active" --argjson mt "$members_total" \
300
+ '{source:$source,sessions:{buckets:$sb,week:$sw,prev:$sp,all:$sall},commits:{buckets:$cb,week:$cw,prev:$cp,authors:$ca,all:$call},handoffs:{buckets:$hb,week:$hw,prev:$hp,avg_resolution_days:$havg,all:$hall},knowledge:{buckets:$kb,week:$kw,prev:$kp,all:$kall},members:{active:$ma,total:$mt}}' 2>/dev/null || _metrics_zero_json
301
+ else
302
+ jq -n --arg source "$source_name" \
303
+ --argjson sb "$session_buckets_json" --argjson sw "$(_bucket_week "$session_buckets")" --argjson sp "$(_bucket_prev "$session_buckets")" --argjson sall "$session_all" \
304
+ --argjson cb "$commit_buckets_json" --argjson cw "$(_bucket_week "$commit_buckets")" --argjson cp "$(_bucket_prev "$commit_buckets")" --argjson ca "$(_num "$(printf '%s' "$commits" | awk -F'|' '{print $2}')")" --argjson call "$(_num "$(cat "$_scratch/commits_all" 2>/dev/null || echo 0)")" \
305
+ --argjson hb "$handoff_buckets_json" --argjson hw "$(_bucket_week "$handoff_buckets")" --argjson hp "$(_bucket_prev "$handoff_buckets")" --argjson hall "$(_num "$(printf '%s' "$handoffs" | awk -F'|' '{print $2}')")" \
306
+ --argjson kb "$knowledge_buckets_json" --argjson kw "$(_bucket_week "$knowledge_buckets")" --argjson kp "$(_bucket_prev "$knowledge_buckets")" --argjson kall "$(_num "$(printf '%s' "$knowledge" | awk -F'|' '{print $2}')")" \
307
+ --argjson ma "$members_active" --argjson mt "$members_total" \
308
+ '{source:$source,sessions:{buckets:$sb,week:$sw,prev:$sp,all:$sall},commits:{buckets:$cb,week:$cw,prev:$cp,authors:$ca,all:$call},handoffs:{buckets:$hb,week:$hw,prev:$hp,avg_resolution_days:null,all:$hall},knowledge:{buckets:$kb,week:$kw,prev:$kp,all:$kall},members:{active:$ma,total:$mt}}' 2>/dev/null || _metrics_zero_json
309
+ fi
310
+ rm -rf "$_scratch" 2>/dev/null || true
311
+ }
312
+
313
+ _metrics_read() {
314
+ local file filter fallback
315
+ file="${CTX_DIR:-}/metrics"
316
+ filter="$1"
317
+ fallback="$2"
318
+ if [ -f "$file" ]; then
319
+ jq -r "$filter" "$file" 2>/dev/null || echo "$fallback"
320
+ else
321
+ echo "$fallback"
322
+ fi
323
+ }
324
+
325
+ _render_momentum_board() {
326
+ local separator line_width header_left header_right pad
327
+ local sb sw sp sall cb cw ca call hb hw havg hall kb kw kall active total
328
+ local sdelta sann cann hann kann know_week
329
+ separator="${SEPARATOR:- ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄}"
330
+ line_width="${LINE_WIDTH:-67}"
331
+
332
+ sb=$(_metrics_read '(.sessions.buckets // [0,0,0,0,0]) | .[0:5] | map(. // 0) | join(" ")' "0 0 0 0 0")
333
+ sw=$(_num "$(_metrics_read '.sessions.week // 0' "0")")
334
+ sp=$(_num "$(_metrics_read '.sessions.prev // 0' "0")")
335
+ sall=$(_num "$(_metrics_read '.sessions.all // 0' "0")")
336
+ cb=$(_metrics_read '(.commits.buckets // [0,0,0,0,0]) | .[0:5] | map(. // 0) | join(" ")' "0 0 0 0 0")
337
+ cw=$(_num "$(_metrics_read '.commits.week // 0' "0")")
338
+ ca=$(_num "$(_metrics_read '.commits.authors // 0' "0")")
339
+ call=$(_num "$(_metrics_read '.commits.all // 0' "0")")
340
+ hb=$(_metrics_read '(.handoffs.buckets // [0,0,0,0,0]) | .[0:5] | map(. // 0) | join(" ")' "0 0 0 0 0")
341
+ hw=$(_num "$(_metrics_read '.handoffs.week // 0' "0")")
342
+ havg=$(_metrics_read '.handoffs.avg_resolution_days // empty' "")
343
+ hall=$(_num "$(_metrics_read '.handoffs.all // 0' "0")")
344
+ kb=$(_metrics_read '(.knowledge.buckets // [0,0,0,0,0]) | .[0:5] | map(. // 0) | join(" ")' "0 0 0 0 0")
345
+ kw=$(_num "$(_metrics_read '.knowledge.week // 0' "0")")
346
+ kall=$(_num "$(_metrics_read '.knowledge.all // 0' "0")")
347
+ active=$(_num "$(_metrics_read '.members.active // 0' "0")")
348
+ total=$(_num "$(_metrics_read '.members.total // 0' "0")")
349
+
350
+ header_left=" ◦ momentum · this week"
351
+ header_right="${active} of ${total} active"
352
+ pad=$((line_width - ${#header_left} - ${#header_right}))
353
+ [ "$pad" -lt 1 ] && pad=1
354
+ printf "%s%*s%s\n" "$header_left" "$pad" "" "$header_right"
355
+
356
+ sdelta=$(_pct_delta "$sw" "$sp")
357
+ sann=""
358
+ [ -n "$sdelta" ] && sann="${sdelta} vs last week"
359
+ cann=""
360
+ if [ "$ca" -gt 0 ] 2>/dev/null; then
361
+ if [ "$ca" -eq 1 ] 2>/dev/null; then cann="1 author"; else cann="${ca} authors"; fi
362
+ fi
363
+ hann=""
364
+ if [ -n "$havg" ]; then hann="$(_num_float "$havg")d avg resolution"; fi
365
+ kann="$(_commafy "$kall") artifacts total"
366
+ know_week="+${kw}"
367
+
368
+ printf " %-11s%5s 5-week %s %s\n\n" "sessions" "$sw" "$(_spark "$sb")" "$sann"
369
+ printf " %-11s%5s 5-week %s %s\n\n" "commits" "$cw" "$(_spark "$cb")" "$cann"
370
+ printf " %-11s%5s 5-week %s %s\n\n" "handoffs" "$hw" "$(_spark "$hb")" "$hann"
371
+ printf " %-11s%5s 5-week %s %s\n" "knowledge" "$know_week" "$(_spark "$kb")" "$kann"
372
+ echo "$separator"
373
+ printf " %s sessions · %s commits · %s handoffs all-time\n" "$(_commafy "$sall")" "$(_commafy "$call")" "$(_commafy "$hall")"
374
+ }
@@ -33,6 +33,7 @@ DESCRIPTION=""
33
33
  ARTIFACT_ID=""
34
34
  RAW_HTML=0
35
35
  SKIP_REFERENCES=0
36
+ PAIR=""
36
37
 
37
38
  while [[ $# -gt 0 ]]; do
38
39
  case "$1" in
@@ -40,6 +41,7 @@ while [[ $# -gt 0 ]]; do
40
41
  --author) AUTHOR="$2"; shift 2 ;;
41
42
  --description) DESCRIPTION="$2"; shift 2 ;;
42
43
  --id) ARTIFACT_ID="$2"; shift 2 ;;
44
+ --pair) PAIR="$2"; shift 2 ;;
43
45
  --raw-html) RAW_HTML=1; shift ;;
44
46
  --no-references) SKIP_REFERENCES=1; shift ;;
45
47
  *)
@@ -72,6 +74,7 @@ fi
72
74
  if [ -f "$SCRIPT_DIR/.env" ]; then
73
75
  EGREGORE_API_URL="${EGREGORE_API_URL:-$(grep '^EGREGORE_API_URL=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
74
76
  EGREGORE_API_KEY="${EGREGORE_API_KEY:-$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
77
+ EGREGORE_USE_PUBLISHED="${EGREGORE_USE_PUBLISHED:-$(grep '^EGREGORE_USE_PUBLISHED=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
75
78
  fi
76
79
 
77
80
  API_URL="${EGREGORE_API_URL:-$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)}"
@@ -79,9 +82,33 @@ API_KEY="${EGREGORE_API_KEY:-}"
79
82
  ORG_SLUG="$(jq -r '.slug // empty' "$CONFIG" 2>/dev/null)"
80
83
  VIEW_BASE="https://egregore.xyz/view"
81
84
 
85
+ # Resolve "use published renderer" from env first, then egregore.json. The
86
+ # env var lets individuals override per-shell; egregore.json lets an
87
+ # instance (e.g. the upstream dev repo) opt in for every teammate.
88
+ if [ -z "${EGREGORE_USE_PUBLISHED:-}" ]; then
89
+ CONFIG_USE_PUBLISHED="$(jq -r '.prefer_published_artifacts // empty' "$CONFIG" 2>/dev/null)"
90
+ if [ "$CONFIG_USE_PUBLISHED" = "true" ]; then
91
+ EGREGORE_USE_PUBLISHED=1
92
+ fi
93
+ fi
94
+
82
95
  # Public relay for OSS share endpoint
83
96
  RELAY_URL="https://egregore-production-55f2.up.railway.app"
84
97
 
98
+ # --- Artifact hosting off-switch (compliance / privacy) ---
99
+ # An instance can disable ALL egregore.xyz publishing via `bin/settings.sh hosting off`
100
+ # (sets features.publishing=false). Absent/true means ON, so existing instances are
101
+ # unaffected. We bail BEFORE any render or upload so "off" truly does nothing and costs
102
+ # nothing. Exit 3 is distinct from usage errors; fire-and-forget callers ignore it.
103
+ # NOTE: test `!= false`, not jq's `// "true"` — `//` treats an explicit `false` as empty
104
+ # and would return the default, silently defeating the off-switch.
105
+ PUBLISHING_ENABLED="$(jq -r 'if .features.publishing == false then "false" else "true" end' "$CONFIG" 2>/dev/null || echo "true")"
106
+ if [ "$PUBLISHING_ENABLED" = "false" ]; then
107
+ echo "Artifact hosting is off for this egregore (features.publishing=false); nothing uploaded." >&2
108
+ echo "Turn it on with: bin/settings.sh hosting on (or: egregore settings → Hosting)." >&2
109
+ exit 3
110
+ fi
111
+
85
112
  # --- Prepare HTML ---
86
113
  TMP_HTML="/tmp/egregore-artifacts/publish-$$.html"
87
114
  mkdir -p /tmp/egregore-artifacts
@@ -98,13 +125,21 @@ else
98
125
  if [ -n "$API_KEY" ] && [ -n "$ORG_SLUG" ]; then
99
126
  RENDER_ARGS+=(--org-slug "$ORG_SLUG" --view-base "$VIEW_BASE")
100
127
  fi
128
+ # Palette family (meridian default | agronomic | sovereign). Role-token driven,
129
+ # so it themes any composed artifact with no extra CSS.
130
+ if [ -n "$PAIR" ]; then
131
+ RENDER_ARGS+=(--pair "$PAIR")
132
+ fi
101
133
 
102
134
  # Prefer the in-repo package copy when it's present AND its dependencies
103
135
  # have been installed — that way local edits to packages/egregore-artifacts
104
136
  # are exercised without waiting for an npm release. Fall back to the
105
137
  # published `npx egregore-artifacts` otherwise.
138
+ # Set EGREGORE_USE_PUBLISHED=1 to force the npm-published renderer even
139
+ # when a local install is present — useful in the upstream dev repo to
140
+ # dogfood what teammates and OSS users actually run.
106
141
  LOCAL_CLI="$SCRIPT_DIR/packages/egregore-artifacts/bin/cli.js"
107
- if [ -f "$LOCAL_CLI" ] && [ -d "$SCRIPT_DIR/packages/egregore-artifacts/node_modules/react" ]; then
142
+ if [ "${EGREGORE_USE_PUBLISHED:-0}" != "1" ] && [ -f "$LOCAL_CLI" ] && [ -d "$SCRIPT_DIR/packages/egregore-artifacts/node_modules/react" ]; then
108
143
  if ! node "$LOCAL_CLI" "${RENDER_ARGS[@]}" >/dev/null 2>&1; then
109
144
  exit 0
110
145
  fi
@@ -158,6 +193,13 @@ if [ -n "$RESPONSE" ]; then
158
193
  URL=$(echo "$RESPONSE" | jq -r '.url // empty' 2>/dev/null)
159
194
  if [ -n "$URL" ]; then
160
195
  echo "$URL"
196
+ # Register in the local artifact registry (memory/artifacts/) so this hosted
197
+ # artifact is findable by CONTENT, not just name — grep/ls (Claude Code /
198
+ # Codex, OSS), and search + graph (paid). Never fatal to the publish.
199
+ bash "$SCRIPT_DIR/bin/artifact-register.sh" \
200
+ --id "$ARTIFACT_ID" --url "$URL" --type "$TYPE" --title "$TITLE" \
201
+ --author "$AUTHOR" --source "$FILE" --description "$DESCRIPTION" \
202
+ >/dev/null 2>&1 || true
161
203
  fi
162
204
  fi
163
205