create-egregore 0.19.5 → 0.19.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/assets/egregore-launcher.js +411 -25
  2. package/bin/cli.js +34 -20
  3. package/lib/local.js +21 -5
  4. package/lib/pi-runtime.js +186 -0
  5. package/lib/setup.js +56 -11
  6. package/lib/ui.js +1 -1
  7. package/package.json +7 -5
  8. package/runtime/codex/.codex/spec-manifest.json +1 -1
  9. package/runtime/codex/AGENTS.md +7 -2
  10. package/runtime/codex/bin/codex-render-spec.mjs +6 -1
  11. package/runtime/codex/bin/codex-session-start.sh +5 -59
  12. package/runtime/codex/bin/codex-skill-render.mjs +7 -5
  13. package/runtime/codex/bin/lib/context.sh +8 -1
  14. package/runtime/codex/bin/lib/greeting.sh +12 -108
  15. package/runtime/codex/bin/lib/metrics.sh +374 -0
  16. package/runtime/codex/bin/publish-artifact.sh +14 -0
  17. package/runtime/codex/bin/settings.sh +274 -0
  18. package/runtime/codex/manifest.json +2 -0
  19. package/runtime/pi/.codex/config.toml +15 -0
  20. package/runtime/pi/.codex/hooks/branch-guard.js +242 -0
  21. package/runtime/pi/.codex/hooks/search-hint.js +65 -0
  22. package/runtime/pi/.codex/hooks.json +30 -0
  23. package/runtime/pi/.codex/skills/activity/SKILL.md +70 -0
  24. package/runtime/pi/.codex/skills/add/SKILL.md +25 -0
  25. package/runtime/pi/.codex/skills/announce/SKILL.md +54 -0
  26. package/runtime/pi/.codex/skills/answer/SKILL.md +25 -0
  27. package/runtime/pi/.codex/skills/archive/SKILL.md +41 -0
  28. package/runtime/pi/.codex/skills/ask/SKILL.md +59 -0
  29. package/runtime/pi/.codex/skills/branch/SKILL.md +25 -0
  30. package/runtime/pi/.codex/skills/checkup/SKILL.md +41 -0
  31. package/runtime/pi/.codex/skills/commit/SKILL.md +25 -0
  32. package/runtime/pi/.codex/skills/contribute/SKILL.md +25 -0
  33. package/runtime/pi/.codex/skills/dark-mode/SKILL.md +25 -0
  34. package/runtime/pi/.codex/skills/dashboard/SKILL.md +59 -0
  35. package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +74 -0
  36. package/runtime/pi/.codex/skills/delete-user/SKILL.md +25 -0
  37. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +25 -0
  38. package/runtime/pi/.codex/skills/emissary/SKILL.md +41 -0
  39. package/runtime/pi/.codex/skills/env/SKILL.md +25 -0
  40. package/runtime/pi/.codex/skills/handoff/SKILL.md +67 -0
  41. package/runtime/pi/.codex/skills/harvest/SKILL.md +107 -0
  42. package/runtime/pi/.codex/skills/ingest/SKILL.md +25 -0
  43. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +25 -0
  44. package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  45. package/runtime/pi/.codex/skills/invite/SKILL.md +40 -0
  46. package/runtime/pi/.codex/skills/issue/SKILL.md +41 -0
  47. package/runtime/pi/.codex/skills/me/SKILL.md +25 -0
  48. package/runtime/pi/.codex/skills/meeting/SKILL.md +41 -0
  49. package/runtime/pi/.codex/skills/minion/SKILL.md +25 -0
  50. package/runtime/pi/.codex/skills/note/SKILL.md +25 -0
  51. package/runtime/pi/.codex/skills/onboarding/SKILL.md +41 -0
  52. package/runtime/pi/.codex/skills/pr/SKILL.md +25 -0
  53. package/runtime/pi/.codex/skills/project/SKILL.md +41 -0
  54. package/runtime/pi/.codex/skills/pull/SKILL.md +25 -0
  55. package/runtime/pi/.codex/skills/push/SKILL.md +25 -0
  56. package/runtime/pi/.codex/skills/quest/SKILL.md +77 -0
  57. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +25 -0
  58. package/runtime/pi/.codex/skills/reflect/SKILL.md +41 -0
  59. package/runtime/pi/.codex/skills/review-pr/SKILL.md +41 -0
  60. package/runtime/pi/.codex/skills/save/SKILL.md +62 -0
  61. package/runtime/pi/.codex/skills/scroll/SKILL.md +168 -0
  62. package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +4 -0
  63. package/runtime/pi/.codex/skills/scroll/assets/shell.html +542 -0
  64. package/runtime/pi/.codex/skills/search/SKILL.md +56 -0
  65. package/runtime/pi/.codex/skills/session-view/SKILL.md +25 -0
  66. package/runtime/pi/.codex/skills/setup/SKILL.md +25 -0
  67. package/runtime/pi/.codex/skills/summon/SKILL.md +41 -0
  68. package/runtime/pi/.codex/skills/sync-repos/SKILL.md +25 -0
  69. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +25 -0
  70. package/runtime/pi/.codex/skills/telemetry/SKILL.md +25 -0
  71. package/runtime/pi/.codex/skills/test/SKILL.md +41 -0
  72. package/runtime/pi/.codex/skills/the-spiral/SKILL.md +84 -0
  73. package/runtime/pi/.codex/skills/thread/SKILL.md +25 -0
  74. package/runtime/pi/.codex/skills/todo/SKILL.md +41 -0
  75. package/runtime/pi/.codex/skills/tui-design/SKILL.md +41 -0
  76. package/runtime/pi/.codex/skills/tutorial/SKILL.md +41 -0
  77. package/runtime/pi/.codex/skills/update/SKILL.md +25 -0
  78. package/runtime/pi/.codex/skills/view/SKILL.md +156 -0
  79. package/runtime/pi/.codex/skills/view/agents/openai.yaml +4 -0
  80. package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +221 -0
  81. package/runtime/pi/.codex/skills/visual-explain/SKILL.md +41 -0
  82. package/runtime/pi/.codex/skills/wrap/SKILL.md +55 -0
  83. package/runtime/pi/.codex/spec-manifest.json +114 -0
  84. package/runtime/pi/.pi/APPEND_SYSTEM.md +255 -0
  85. package/runtime/pi/.pi/extensions/egregore.ts +324 -0
  86. package/runtime/pi/.pi/settings.json +6 -0
  87. package/runtime/pi/.pi/spec-manifest.json +12 -0
  88. package/runtime/pi/AGENTS.md +350 -0
  89. package/runtime/pi/bin/activity-data.sh +242 -0
  90. package/runtime/pi/bin/agent.sh +677 -0
  91. package/runtime/pi/bin/codex-install-skills.sh +78 -0
  92. package/runtime/pi/bin/codex-render-spec.mjs +394 -0
  93. package/runtime/pi/bin/codex-session-start.sh +188 -0
  94. package/runtime/pi/bin/codex-skill-render.mjs +323 -0
  95. package/runtime/pi/bin/codex-sync-skills.sh +227 -0
  96. package/runtime/pi/bin/dashboard-data.sh +178 -0
  97. package/runtime/pi/bin/graph-batch.sh +60 -0
  98. package/runtime/pi/bin/graph-op.sh +431 -0
  99. package/runtime/pi/bin/graph-wal.sh +228 -0
  100. package/runtime/pi/bin/graph.sh +177 -0
  101. package/runtime/pi/bin/handoff-pr-backfill.sh +86 -0
  102. package/runtime/pi/bin/handoff-run.sh +581 -0
  103. package/runtime/pi/bin/handoff-save-egregore.sh +136 -0
  104. package/runtime/pi/bin/index-handoff.sh +312 -0
  105. package/runtime/pi/bin/lib/artifact-id.sh +46 -0
  106. package/runtime/pi/bin/lib/config.sh +71 -0
  107. package/runtime/pi/bin/lib/context.sh +542 -0
  108. package/runtime/pi/bin/lib/dashboard-artifact.sh +142 -0
  109. package/runtime/pi/bin/lib/ensure-develop.sh +183 -0
  110. package/runtime/pi/bin/lib/git-sync.sh +288 -0
  111. package/runtime/pi/bin/lib/greeting.sh +340 -0
  112. package/runtime/pi/bin/lib/hash.sh +13 -0
  113. package/runtime/pi/bin/lib/identity.sh +174 -0
  114. package/runtime/pi/bin/lib/metrics.sh +374 -0
  115. package/runtime/pi/bin/lib/permissions.sh +106 -0
  116. package/runtime/pi/bin/lib/time.sh +56 -0
  117. package/runtime/pi/bin/lib/worktree-links.sh +54 -0
  118. package/runtime/pi/bin/notify.sh +207 -0
  119. package/runtime/pi/bin/pi-branch-policy.mjs +15 -0
  120. package/runtime/pi/bin/pi-product-workflows.mjs +324 -0
  121. package/runtime/pi/bin/pi-render-spec.mjs +114 -0
  122. package/runtime/pi/bin/pi-session-start.sh +24 -0
  123. package/runtime/pi/bin/publish-artifact.sh +201 -0
  124. package/runtime/pi/bin/publish-references.sh +144 -0
  125. package/runtime/pi/bin/repo-state.sh +99 -0
  126. package/runtime/pi/bin/telemetry.sh +391 -0
  127. package/runtime/pi/bin/worktree-create.sh +118 -0
  128. package/runtime/pi/bin/worktree-remove.sh +50 -0
  129. package/runtime/pi/bin/worktree.sh +198 -0
  130. package/runtime/pi/manifest.json +176 -0
@@ -0,0 +1,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
+ }
@@ -0,0 +1,106 @@
1
+ # shellcheck shell=bash
2
+ # permissions.sh — Soft-enforced admin-only access for Egregore content.
3
+ #
4
+ # Content can be marked admin-only via frontmatter:
5
+ #
6
+ # ---
7
+ # admin: true
8
+ # ---
9
+ #
10
+ # Admins are listed in egregore.json under "admins": ["github1", "github2", ...].
11
+ #
12
+ # Two enforcement layers:
13
+ #
14
+ # - Read-side (CLI listings, /view rendering) — soft, tag-not-hide:
15
+ # admin entries appear in /activity and /dashboard listings tagged so admins
16
+ # can identify them at a glance. For non-admins the title/topic/branch are
17
+ # replaced with an opaque "[admin]" placeholder — they see admin work
18
+ # exists but the topic doesn't leak. Anyone with git access still bypasses
19
+ # by `cat`-ing the file.
20
+ #
21
+ # - Publish-side (egregore.xyz upload) — strict: publish-artifact.sh refuses
22
+ # to push admin-only files because egregore.xyz URLs are not auth-gated.
23
+ # Override with --allow-admin (set by /view after explicit user
24
+ # confirmation via AskUserQuestion).
25
+
26
+ # Get list of admin GitHub usernames (newline-separated, lowercase)
27
+ _get_admins() {
28
+ local config="${CONFIG:-${SCRIPT_DIR:-$(pwd)}/egregore.json}"
29
+ if [ -f "$config" ]; then
30
+ jq -r '.admins[]? // empty' "$config" 2>/dev/null | tr '[:upper:]' '[:lower:]'
31
+ fi
32
+ }
33
+
34
+ # Returns 0 if username is an admin, 1 otherwise. Empty username = not admin.
35
+ _is_admin() {
36
+ local username="${1:-}"
37
+ [ -z "$username" ] && return 1
38
+ local lc
39
+ lc=$(printf '%s' "$username" | tr '[:upper:]' '[:lower:]')
40
+ _get_admins | grep -qx "$lc"
41
+ }
42
+
43
+ # Returns 0 if the file's frontmatter contains `admin: true`, 1 otherwise.
44
+ # Tolerates whitespace and case in the value. Looks at the first ~60 lines
45
+ # to keep the scan bounded.
46
+ _is_admin_only_file() {
47
+ local path="$1"
48
+ [ -f "$path" ] || return 1
49
+ awk '
50
+ BEGIN { in_fm = 0; found = 0 }
51
+ NR == 1 && /^---[[:space:]]*$/ { in_fm = 1; next }
52
+ in_fm && /^---[[:space:]]*$/ { exit (found ? 0 : 1) }
53
+ in_fm && tolower($0) ~ /^admin:[[:space:]]*true[[:space:]]*$/ { found = 1 }
54
+ NR > 60 { exit (found ? 0 : 1) }
55
+ END { exit (found ? 0 : 1) }
56
+ ' "$path" 2>/dev/null
57
+ }
58
+
59
+ # Returns 0 if the file's frontmatter has the `admin:` key set at all (true or
60
+ # false), 1 otherwise. Used by the admin-content-detector hook to skip files
61
+ # whose admin status was already decided — including an explicit `admin: false`
62
+ # meaning "reviewed, public on purpose". Without this, the hook would re-prompt
63
+ # on every edit of a sensitive-keyword file the user has already cleared.
64
+ _has_admin_decision() {
65
+ local path="$1"
66
+ [ -f "$path" ] || return 1
67
+ awk '
68
+ BEGIN { in_fm = 0; found = 0 }
69
+ NR == 1 && /^---[[:space:]]*$/ { in_fm = 1; next }
70
+ in_fm && /^---[[:space:]]*$/ { exit (found ? 0 : 1) }
71
+ in_fm && tolower($0) ~ /^admin:[[:space:]]*(true|false)[[:space:]]*$/ { found = 1 }
72
+ NR > 60 { exit (found ? 0 : 1) }
73
+ END { exit (found ? 0 : 1) }
74
+ ' "$path" 2>/dev/null
75
+ }
76
+
77
+ # Resolve current user's GitHub username (lowercase). Falls back through:
78
+ # $1 → .egregore-state.json → git user.name
79
+ _current_user() {
80
+ local explicit="${1:-}"
81
+ if [ -n "$explicit" ]; then
82
+ printf '%s' "$explicit" | tr '[:upper:]' '[:lower:]'
83
+ return 0
84
+ fi
85
+ local state="${SCRIPT_DIR:-$(pwd)}/.egregore-state.json"
86
+ if [ -f "$state" ]; then
87
+ local u
88
+ u=$(jq -r '.github_username // empty' "$state" 2>/dev/null)
89
+ if [ -n "$u" ]; then
90
+ printf '%s' "$u" | tr '[:upper:]' '[:lower:]'
91
+ return 0
92
+ fi
93
+ fi
94
+ git config user.name 2>/dev/null | tr '[:upper:]' '[:lower:]'
95
+ }
96
+
97
+ # Returns 0 if the file's body contains sensitive keywords (fundraising, legal,
98
+ # personnel, strategic). Used by the admin-content-detector hook to flag files
99
+ # that probably should be admin-only but aren't yet. Whole-word match on a
100
+ # short curated list — easy to extend without retraining intuition.
101
+ _looks_sensitive() {
102
+ local path="$1"
103
+ [ -f "$path" ] || return 1
104
+ # Word-boundary regex (case-insensitive). Keep this list curated, not exhaustive.
105
+ grep -qiE '\b(fundrais(e|ing)|term[ -]?sheet|cap[ -]?table|valuation|runway|investor|VC[s]?|raise|seed[ -]?round|series[ -][a-d]|board[ -]prep|equity[ -]grant|stock[ -]option|RSU|salary|compensation|payroll|severance|PIP|NDA|attorney|legal[ -]hold|lawsuit|acquisition|merger|due[ -]diligence)\b' "$path"
106
+ }
@@ -0,0 +1,56 @@
1
+ #!/bin/bash
2
+ # Cross-platform time helpers — sourced by scripts that need time formatting.
3
+
4
+ # Convert ISO timestamp to epoch seconds (macOS + Linux compatible)
5
+ # Assumes input is UTC (Z or +00:00 suffix, or bare — treated as UTC)
6
+ _iso_to_epoch() {
7
+ local iso="$1"
8
+ local clean
9
+ clean=$(echo "$iso" | sed 's/Z$//; s/+00:00$//; s/\.[0-9]*//')
10
+ # If date-only (no T), append midnight to avoid macOS date -j filling current time
11
+ [[ "$clean" != *T* ]] && clean="${clean}T00:00:00"
12
+ # macOS → date -j, Linux → date -d
13
+ TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$clean" "+%s" 2>/dev/null || \
14
+ TZ=UTC date -d "$clean" +%s 2>/dev/null || echo "0"
15
+ }
16
+
17
+ # Convert epoch to relative time string (e.g. "3h ago", "yesterday")
18
+ # Uses calendar-day comparison for multi-day ranges (not raw delta)
19
+ _epoch_to_relative() {
20
+ local epoch="$1"
21
+ local now="${2:-$(date +%s)}"
22
+ [ "$epoch" = "0" ] && echo "--" && return
23
+ local delta=$(( now - epoch ))
24
+ if [ "$delta" -lt 60 ]; then echo "just now"
25
+ elif [ "$delta" -lt 3600 ]; then echo "$(( delta / 60 ))m ago"
26
+ elif [ "$delta" -lt 86400 ]; then echo "$(( delta / 3600 ))h ago"
27
+ else
28
+ # Calendar-day based: compare local dates for accurate "yesterday" / "Xd ago"
29
+ local event_date today_date
30
+ event_date=$(date -r "$epoch" +%Y-%m-%d 2>/dev/null || \
31
+ date -d "@$epoch" +%Y-%m-%d 2>/dev/null || echo "")
32
+ today_date=$(date +%Y-%m-%d)
33
+ if [ -z "$event_date" ] || [ "$event_date" = "$today_date" ]; then echo "today"
34
+ else
35
+ local today_e event_e
36
+ today_e=$(date -j -f "%Y-%m-%d" "$today_date" +%s 2>/dev/null || \
37
+ date -d "$today_date" +%s 2>/dev/null || echo "$now")
38
+ event_e=$(date -j -f "%Y-%m-%d" "$event_date" +%s 2>/dev/null || \
39
+ date -d "$event_date" +%s 2>/dev/null || echo "$epoch")
40
+ local days=$(( (today_e - event_e) / 86400 ))
41
+ [ "$days" -lt 1 ] && days=1
42
+ if [ "$days" -eq 1 ]; then echo "yesterday"
43
+ else echo "${days}d ago"
44
+ fi
45
+ fi
46
+ fi
47
+ }
48
+
49
+ # Millisecond timer (for latency measurement)
50
+ _millis() {
51
+ if command -v gdate &>/dev/null; then
52
+ gdate +%s%3N
53
+ else
54
+ python3 -c "import time; print(int(time.time() * 1000))" 2>/dev/null || echo "0"
55
+ fi
56
+ }
@@ -0,0 +1,54 @@
1
+ # shellcheck shell=bash
2
+ # worktree-links.sh - shared-state bootstrap for git worktrees.
3
+ #
4
+ # Claude Code worktrees are normally created through bin/worktree-create.sh,
5
+ # which links memory/, .env, and local state from the main checkout. Portable
6
+ # runtimes such as Codex can land in a plain git worktree, so startup and
7
+ # agent.sh use this helper to self-heal those links before reading context.
8
+
9
+ egregore_main_project_dir() {
10
+ local project_dir="${1:?project dir required}"
11
+ if [ -f "$project_dir/.git" ]; then
12
+ local wt_gitdir
13
+ wt_gitdir=$(sed 's/^gitdir: //' "$project_dir/.git" 2>/dev/null || true)
14
+ if [ -n "$wt_gitdir" ]; then
15
+ cd "$wt_gitdir/../../.." 2>/dev/null && pwd
16
+ return
17
+ fi
18
+ fi
19
+ cd "$project_dir" 2>/dev/null && pwd
20
+ }
21
+
22
+ egregore_link_shared_state() {
23
+ local worktree_dir="${1:?worktree dir required}"
24
+ local main_dir="${2:-}"
25
+ local file src dst resolved
26
+
27
+ [ -f "$worktree_dir/.git" ] || return 0
28
+ [ -n "$main_dir" ] || main_dir="$(egregore_main_project_dir "$worktree_dir")"
29
+ [ -n "$main_dir" ] || return 0
30
+ [ "$main_dir" != "$worktree_dir" ] || return 0
31
+
32
+ for file in memory .env .egregore-state.json .egregore-session-id; do
33
+ src="$main_dir/$file"
34
+ dst="$worktree_dir/$file"
35
+
36
+ if [ "$file" = "memory" ]; then
37
+ if [ -L "$src" ] || [ -d "$src" ]; then
38
+ resolved="$(realpath "$src" 2>/dev/null || true)"
39
+ [ -n "$resolved" ] && [ -d "$resolved" ] || continue
40
+ src="$resolved"
41
+ else
42
+ continue
43
+ fi
44
+ else
45
+ [ -f "$src" ] || continue
46
+ fi
47
+
48
+ if [ -L "$dst" ]; then
49
+ ln -sfn "$src" "$dst" 2>/dev/null || true
50
+ elif [ ! -e "$dst" ]; then
51
+ ln -s "$src" "$dst" 2>/dev/null || true
52
+ fi
53
+ done
54
+ }