create-egregore 0.19.33 → 0.20.1

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 (250) hide show
  1. package/assets/egregore-launcher.js +141 -14
  2. package/bin/cli.js +19 -3
  3. package/lib/connect-runtime.js +4 -2
  4. package/lib/local.js +15 -1
  5. package/lib/prime-runtime.js +208 -0
  6. package/lib/setup.js +66 -5
  7. package/package.json +4 -3
  8. package/runtime/codex/.codex/config.toml +3 -0
  9. package/runtime/codex/.codex/skills/archive/SKILL.md +1 -1
  10. package/runtime/codex/.codex/skills/branch/SKILL.md +1 -1
  11. package/runtime/codex/.codex/skills/checkup/SKILL.md +1 -1
  12. package/runtime/codex/.codex/skills/commit/SKILL.md +1 -1
  13. package/runtime/codex/.codex/skills/contribute/SKILL.md +1 -1
  14. package/runtime/codex/.codex/skills/delete-user/SKILL.md +1 -1
  15. package/runtime/codex/.codex/skills/ingest/SKILL.md +1 -1
  16. package/runtime/codex/.codex/skills/ingest-corpus/SKILL.md +29 -0
  17. package/runtime/codex/.codex/skills/ingest-notion/SKILL.md +29 -0
  18. package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +1 -1
  19. package/runtime/codex/.codex/skills/issue/SKILL.md +1 -1
  20. package/runtime/codex/.codex/skills/me/SKILL.md +1 -1
  21. package/runtime/codex/.codex/skills/meeting/SKILL.md +1 -1
  22. package/runtime/codex/.codex/skills/note/SKILL.md +1 -1
  23. package/runtime/codex/.codex/skills/notion-connect/SKILL.md +29 -0
  24. package/runtime/codex/.codex/skills/onboarding/SKILL.md +1 -1
  25. package/runtime/codex/.codex/skills/pr/SKILL.md +1 -1
  26. package/runtime/codex/.codex/skills/pull/SKILL.md +1 -1
  27. package/runtime/codex/.codex/skills/push/SKILL.md +1 -1
  28. package/runtime/codex/.codex/skills/reflect/SKILL.md +1 -1
  29. package/runtime/codex/.codex/skills/review-pr/SKILL.md +1 -1
  30. package/runtime/codex/.codex/skills/save/SKILL.md +3 -1
  31. package/runtime/codex/.codex/skills/slack-connect/SKILL.md +1 -1
  32. package/runtime/codex/.codex/skills/summon/SKILL.md +1 -1
  33. package/runtime/codex/.codex/skills/sync-repos/SKILL.md +1 -1
  34. package/runtime/codex/.codex/skills/teams-connect/SKILL.md +29 -0
  35. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +1 -1
  36. package/runtime/codex/.codex/skills/telemetry/SKILL.md +1 -1
  37. package/runtime/codex/.codex/skills/test/SKILL.md +1 -1
  38. package/runtime/codex/.codex/skills/tutorial/SKILL.md +1 -1
  39. package/runtime/codex/.codex/skills/update/SKILL.md +1 -1
  40. package/runtime/codex/.codex/spec-manifest.json +5 -5
  41. package/runtime/codex/AGENTS.md +41 -35
  42. package/runtime/codex/bin/activity-data.sh +4 -8
  43. package/runtime/codex/bin/agent.sh +17 -5
  44. package/runtime/codex/bin/attendant.sh +13 -0
  45. package/runtime/codex/bin/autosave.sh +27 -1
  46. package/runtime/codex/bin/codex-render-spec.mjs +33 -32
  47. package/runtime/codex/bin/codex-session-start.sh +3 -1
  48. package/runtime/codex/bin/codex-sync-skills.sh +6 -1
  49. package/runtime/codex/bin/graph-batch.sh +25 -6
  50. package/runtime/codex/bin/graph-op.sh +5 -4
  51. package/runtime/codex/bin/graph.sh +31 -5
  52. package/runtime/codex/bin/handoff-pr-backfill.sh +1 -1
  53. package/runtime/codex/bin/handoff-run.sh +8 -3
  54. package/runtime/codex/bin/handoff-save-egregore.sh +19 -7
  55. package/runtime/codex/bin/ingest.py +341 -10
  56. package/runtime/codex/bin/lib/context.sh +26 -1
  57. package/runtime/codex/bin/lib/git-message.sh +37 -0
  58. package/runtime/codex/bin/lib/git-sync.sh +10 -2
  59. package/runtime/codex/bin/lib/greeting.sh +4 -1
  60. package/runtime/codex/bin/lib/worktree-links.sh +25 -0
  61. package/runtime/codex/bin/session-autosave.sh +7 -1
  62. package/runtime/codex/bin/settings.sh +1 -1
  63. package/runtime/codex/manifest.json +10 -1
  64. package/runtime/pi/.codex/config.toml +3 -0
  65. package/runtime/pi/.codex/skills/archive/SKILL.md +1 -1
  66. package/runtime/pi/.codex/skills/branch/SKILL.md +1 -1
  67. package/runtime/pi/.codex/skills/checkup/SKILL.md +1 -1
  68. package/runtime/pi/.codex/skills/commit/SKILL.md +1 -1
  69. package/runtime/pi/.codex/skills/contribute/SKILL.md +1 -1
  70. package/runtime/pi/.codex/skills/delete-user/SKILL.md +1 -1
  71. package/runtime/pi/.codex/skills/ingest/SKILL.md +1 -1
  72. package/runtime/pi/.codex/skills/ingest-corpus/SKILL.md +29 -0
  73. package/runtime/pi/.codex/skills/ingest-notion/SKILL.md +29 -0
  74. package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +1 -1
  75. package/runtime/pi/.codex/skills/issue/SKILL.md +1 -1
  76. package/runtime/pi/.codex/skills/me/SKILL.md +1 -1
  77. package/runtime/pi/.codex/skills/meeting/SKILL.md +1 -1
  78. package/runtime/pi/.codex/skills/note/SKILL.md +1 -1
  79. package/runtime/pi/.codex/skills/notion-connect/SKILL.md +29 -0
  80. package/runtime/pi/.codex/skills/onboarding/SKILL.md +1 -1
  81. package/runtime/pi/.codex/skills/pr/SKILL.md +1 -1
  82. package/runtime/pi/.codex/skills/pull/SKILL.md +1 -1
  83. package/runtime/pi/.codex/skills/push/SKILL.md +1 -1
  84. package/runtime/pi/.codex/skills/reflect/SKILL.md +1 -1
  85. package/runtime/pi/.codex/skills/review-pr/SKILL.md +1 -1
  86. package/runtime/pi/.codex/skills/save/SKILL.md +3 -1
  87. package/runtime/pi/.codex/skills/slack-connect/SKILL.md +1 -1
  88. package/runtime/pi/.codex/skills/summon/SKILL.md +1 -1
  89. package/runtime/pi/.codex/skills/sync-repos/SKILL.md +1 -1
  90. package/runtime/pi/.codex/skills/teams-connect/SKILL.md +29 -0
  91. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +1 -1
  92. package/runtime/pi/.codex/skills/telemetry/SKILL.md +1 -1
  93. package/runtime/pi/.codex/skills/test/SKILL.md +1 -1
  94. package/runtime/pi/.codex/skills/tutorial/SKILL.md +1 -1
  95. package/runtime/pi/.codex/skills/update/SKILL.md +1 -1
  96. package/runtime/pi/.codex/spec-manifest.json +5 -5
  97. package/runtime/pi/.pi/APPEND_SYSTEM.md +57 -33
  98. package/runtime/pi/.pi/extensions/egregore.ts +17 -6
  99. package/runtime/pi/.pi/spec-manifest.json +2 -2
  100. package/runtime/pi/AGENTS.md +41 -35
  101. package/runtime/pi/bin/activity-data.sh +4 -8
  102. package/runtime/pi/bin/agent.sh +17 -5
  103. package/runtime/pi/bin/attendant.sh +13 -0
  104. package/runtime/pi/bin/autosave.sh +27 -1
  105. package/runtime/pi/bin/codex-render-spec.mjs +33 -32
  106. package/runtime/pi/bin/codex-session-start.sh +3 -1
  107. package/runtime/pi/bin/codex-sync-skills.sh +6 -1
  108. package/runtime/pi/bin/graph-batch.sh +25 -6
  109. package/runtime/pi/bin/graph-op.sh +5 -4
  110. package/runtime/pi/bin/graph.sh +31 -5
  111. package/runtime/pi/bin/handoff-pr-backfill.sh +1 -1
  112. package/runtime/pi/bin/handoff-run.sh +8 -3
  113. package/runtime/pi/bin/handoff-save-egregore.sh +19 -7
  114. package/runtime/pi/bin/ingest.py +341 -10
  115. package/runtime/pi/bin/lib/context.sh +26 -1
  116. package/runtime/pi/bin/lib/git-message.sh +37 -0
  117. package/runtime/pi/bin/lib/git-sync.sh +10 -2
  118. package/runtime/pi/bin/lib/greeting.sh +4 -1
  119. package/runtime/pi/bin/lib/worktree-links.sh +25 -0
  120. package/runtime/pi/bin/pi-branch-policy.mjs +138 -1
  121. package/runtime/pi/bin/pi-product-workflows.mjs +35 -7
  122. package/runtime/pi/bin/pi-render-spec.mjs +52 -26
  123. package/runtime/pi/bin/session-autosave.sh +7 -1
  124. package/runtime/pi/bin/settings.sh +1 -1
  125. package/runtime/pi/manifest.json +10 -1
  126. package/runtime/prime/.claude/context/notification-consent.md +66 -0
  127. package/runtime/prime/.codex/config.toml +20 -0
  128. package/runtime/prime/.codex/hooks/branch-guard.js +461 -0
  129. package/runtime/prime/.codex/hooks/search-hint.js +67 -0
  130. package/runtime/prime/.codex/hooks.json +30 -0
  131. package/runtime/prime/.codex/skills/activity/SKILL.md +82 -0
  132. package/runtime/prime/.codex/skills/announce/SKILL.md +67 -0
  133. package/runtime/prime/.codex/skills/archive/SKILL.md +45 -0
  134. package/runtime/prime/.codex/skills/branch/SKILL.md +29 -0
  135. package/runtime/prime/.codex/skills/checkup/SKILL.md +45 -0
  136. package/runtime/prime/.codex/skills/commit/SKILL.md +29 -0
  137. package/runtime/prime/.codex/skills/contribute/SKILL.md +29 -0
  138. package/runtime/prime/.codex/skills/create-skill/SKILL.md +29 -0
  139. package/runtime/prime/.codex/skills/dark-mode/SKILL.md +29 -0
  140. package/runtime/prime/.codex/skills/dashboard/SKILL.md +59 -0
  141. package/runtime/prime/.codex/skills/deep-reflect/SKILL.md +109 -0
  142. package/runtime/prime/.codex/skills/delete-user/SKILL.md +29 -0
  143. package/runtime/prime/.codex/skills/emissary/SKILL.md +45 -0
  144. package/runtime/prime/.codex/skills/handoff/SKILL.md +166 -0
  145. package/runtime/prime/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
  146. package/runtime/prime/.codex/skills/harvest/SKILL.md +131 -0
  147. package/runtime/prime/.codex/skills/ingest/SKILL.md +29 -0
  148. package/runtime/prime/.codex/skills/ingest-corpus/SKILL.md +29 -0
  149. package/runtime/prime/.codex/skills/ingest-notion/SKILL.md +29 -0
  150. package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +45 -0
  151. package/runtime/prime/.codex/skills/invite/SKILL.md +44 -0
  152. package/runtime/prime/.codex/skills/issue/SKILL.md +45 -0
  153. package/runtime/prime/.codex/skills/me/SKILL.md +29 -0
  154. package/runtime/prime/.codex/skills/meeting/SKILL.md +45 -0
  155. package/runtime/prime/.codex/skills/note/SKILL.md +29 -0
  156. package/runtime/prime/.codex/skills/notion-connect/SKILL.md +29 -0
  157. package/runtime/prime/.codex/skills/onboarding/SKILL.md +45 -0
  158. package/runtime/prime/.codex/skills/pr/SKILL.md +29 -0
  159. package/runtime/prime/.codex/skills/pull/SKILL.md +29 -0
  160. package/runtime/prime/.codex/skills/push/SKILL.md +29 -0
  161. package/runtime/prime/.codex/skills/quest/SKILL.md +80 -0
  162. package/runtime/prime/.codex/skills/reflect/SKILL.md +45 -0
  163. package/runtime/prime/.codex/skills/review-pr/SKILL.md +45 -0
  164. package/runtime/prime/.codex/skills/save/SKILL.md +127 -0
  165. package/runtime/prime/.codex/skills/slack-connect/SKILL.md +29 -0
  166. package/runtime/prime/.codex/skills/summon/SKILL.md +45 -0
  167. package/runtime/prime/.codex/skills/sync-repos/SKILL.md +29 -0
  168. package/runtime/prime/.codex/skills/teams-connect/SKILL.md +29 -0
  169. package/runtime/prime/.codex/skills/telegram-connect/SKILL.md +29 -0
  170. package/runtime/prime/.codex/skills/telemetry/SKILL.md +29 -0
  171. package/runtime/prime/.codex/skills/test/SKILL.md +45 -0
  172. package/runtime/prime/.codex/skills/the-spiral/SKILL.md +84 -0
  173. package/runtime/prime/.codex/skills/tui-design/SKILL.md +45 -0
  174. package/runtime/prime/.codex/skills/tutorial/SKILL.md +45 -0
  175. package/runtime/prime/.codex/skills/update/SKILL.md +29 -0
  176. package/runtime/prime/.codex/skills/view/SKILL.md +156 -0
  177. package/runtime/prime/.codex/skills/view/agents/openai.yaml +4 -0
  178. package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +221 -0
  179. package/runtime/prime/.codex/skills/visual-explain/SKILL.md +45 -0
  180. package/runtime/prime/.codex/skills/wrap/SKILL.md +58 -0
  181. package/runtime/prime/.codex/spec-manifest.json +132 -0
  182. package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +416 -0
  183. package/runtime/prime/.prime/agent/extensions/egregore.ts +460 -0
  184. package/runtime/prime/.prime/agent/settings.json +4 -0
  185. package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +34 -0
  186. package/runtime/prime/.prime/agent/skills/egregore/pyproject.toml +13 -0
  187. package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +156 -0
  188. package/runtime/prime/.prime/agent/spec-manifest.json +12 -0
  189. package/runtime/prime/AGENTS.md +488 -0
  190. package/runtime/prime/bin/activity-data.sh +238 -0
  191. package/runtime/prime/bin/agent.sh +787 -0
  192. package/runtime/prime/bin/attendant.sh +707 -0
  193. package/runtime/prime/bin/autosave.sh +112 -0
  194. package/runtime/prime/bin/capture-reconcile.sh +69 -0
  195. package/runtime/prime/bin/capture-run.sh +341 -0
  196. package/runtime/prime/bin/codex-install-skills.sh +78 -0
  197. package/runtime/prime/bin/codex-render-spec.mjs +435 -0
  198. package/runtime/prime/bin/codex-session-start.sh +265 -0
  199. package/runtime/prime/bin/codex-skill-render.mjs +347 -0
  200. package/runtime/prime/bin/codex-sync-skills.sh +237 -0
  201. package/runtime/prime/bin/connect-refresh.sh +33 -0
  202. package/runtime/prime/bin/dashboard-data.sh +178 -0
  203. package/runtime/prime/bin/graph-batch.sh +79 -0
  204. package/runtime/prime/bin/graph-op.sh +1102 -0
  205. package/runtime/prime/bin/graph-wal.sh +229 -0
  206. package/runtime/prime/bin/graph.sh +223 -0
  207. package/runtime/prime/bin/handoff-pr-backfill.sh +86 -0
  208. package/runtime/prime/bin/handoff-run.sh +676 -0
  209. package/runtime/prime/bin/handoff-save-egregore.sh +224 -0
  210. package/runtime/prime/bin/index-handoff.sh +322 -0
  211. package/runtime/prime/bin/ingest-graph.sh +80 -0
  212. package/runtime/prime/bin/ingest-surface.html +495 -0
  213. package/runtime/prime/bin/ingest.py +1011 -0
  214. package/runtime/prime/bin/ingest.sh +279 -0
  215. package/runtime/prime/bin/ingest_graph.py +856 -0
  216. package/runtime/prime/bin/ingest_surface.py +544 -0
  217. package/runtime/prime/bin/lib/artifact-id.sh +46 -0
  218. package/runtime/prime/bin/lib/config.sh +116 -0
  219. package/runtime/prime/bin/lib/context.sh +621 -0
  220. package/runtime/prime/bin/lib/dashboard-artifact.sh +242 -0
  221. package/runtime/prime/bin/lib/ensure-develop.sh +183 -0
  222. package/runtime/prime/bin/lib/git-message.sh +37 -0
  223. package/runtime/prime/bin/lib/git-sync.sh +414 -0
  224. package/runtime/prime/bin/lib/greeting.sh +519 -0
  225. package/runtime/prime/bin/lib/hash.sh +13 -0
  226. package/runtime/prime/bin/lib/identity.sh +213 -0
  227. package/runtime/prime/bin/lib/metrics.sh +374 -0
  228. package/runtime/prime/bin/lib/noncode.sh +31 -0
  229. package/runtime/prime/bin/lib/permissions.sh +106 -0
  230. package/runtime/prime/bin/lib/time.sh +56 -0
  231. package/runtime/prime/bin/lib/worktree-links.sh +79 -0
  232. package/runtime/prime/bin/notify.sh +636 -0
  233. package/runtime/prime/bin/person.py +630 -0
  234. package/runtime/prime/bin/person.sh +448 -0
  235. package/runtime/prime/bin/pi-branch-policy.mjs +312 -0
  236. package/runtime/prime/bin/pi-product-workflows.mjs +394 -0
  237. package/runtime/prime/bin/prime-render-spec.mjs +147 -0
  238. package/runtime/prime/bin/prime-session-start.sh +24 -0
  239. package/runtime/prime/bin/publish-artifact.sh +271 -0
  240. package/runtime/prime/bin/publish-references.sh +144 -0
  241. package/runtime/prime/bin/repo-state.sh +96 -0
  242. package/runtime/prime/bin/search.sh +240 -0
  243. package/runtime/prime/bin/session-autosave.sh +215 -0
  244. package/runtime/prime/bin/session-log.sh +199 -0
  245. package/runtime/prime/bin/settings.sh +304 -0
  246. package/runtime/prime/bin/telemetry.sh +391 -0
  247. package/runtime/prime/bin/worktree-create.sh +128 -0
  248. package/runtime/prime/bin/worktree-remove.sh +50 -0
  249. package/runtime/prime/bin/worktree.sh +198 -0
  250. package/runtime/prime/manifest.json +181 -0
@@ -0,0 +1,213 @@
1
+ # shellcheck shell=bash
2
+ # identity.sh — User identity resolution for session-start.sh
3
+ #
4
+ # Resolves the current user's identity through multiple fallback layers:
5
+ # 1. .egregore-state.json github_username (stored during setup)
6
+ # 2. GitHub API auto-detect via GITHUB_TOKEN
7
+ # 3. Graph person lookup (existing team member on new machine)
8
+ # 4. Global git config user.name (last resort)
9
+ #
10
+ # Inputs: STATE_FILE, ENV_FILE, SCRIPT_DIR, HEALTH_GITHUB
11
+ # Outputs: AUTHOR, DISPLAY_NAME_STATE, FIRST_SESSION, HEALTH_GITHUB,
12
+ # EGREGORE_USER, EGREGORE_ORG, EGREGORE_SESSION_ID
13
+ # Writes .egregore-session-id and ~/.egregore/session-*.id
14
+
15
+ STORED_USERNAME=""
16
+ FIRST_SESSION=""
17
+ AUTHOR=""
18
+ if [ -f "$STATE_FILE" ]; then
19
+ STORED_USERNAME=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
20
+ fi
21
+
22
+ DISPLAY_NAME_STATE=""
23
+ if [ -f "$STATE_FILE" ]; then
24
+ DISPLAY_NAME_STATE=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
25
+ fi
26
+
27
+ if [ -n "$STORED_USERNAME" ]; then
28
+ # Identity stored during setup — use it and ensure repo-local git config matches
29
+ AUTHOR="$STORED_USERNAME"
30
+ HEALTH_GITHUB="ok"
31
+ CURRENT_LOCAL=$(git config --local user.name 2>/dev/null || echo "")
32
+ if [ "$CURRENT_LOCAL" != "$STORED_USERNAME" ]; then
33
+ STORED_NAME=$(jq -r '.github_name // empty' "$STATE_FILE" 2>/dev/null)
34
+ git config user.name "${STORED_NAME:-$STORED_USERNAME}" 2>/dev/null || true
35
+ git config user.email "${STORED_USERNAME}@users.noreply.github.com" 2>/dev/null || true
36
+ fi
37
+ else
38
+ # No stored identity — try GitHub API to auto-detect (self-healing for pre-fix installs)
39
+ if [ -f "$ENV_FILE" ]; then
40
+ GH_TOKEN=$(grep '^GITHUB_TOKEN=' "$ENV_FILE" 2>/dev/null | cut -d'=' -f2-)
41
+ if [ -n "$GH_TOKEN" ]; then
42
+ GH_USER_JSON=$(curl -s -H "Authorization: token $GH_TOKEN" https://api.github.com/user --max-time 5 2>/dev/null || echo "")
43
+ GH_LOGIN=$(echo "$GH_USER_JSON" | jq -r '.login // empty' 2>/dev/null)
44
+ GH_ID=$(echo "$GH_USER_JSON" | jq -r '.id // empty' 2>/dev/null)
45
+ GH_NAME=$(echo "$GH_USER_JSON" | jq -r '.name // empty' 2>/dev/null)
46
+ GH_EMAIL=$(echo "$GH_USER_JSON" | jq -r '.email // empty' 2>/dev/null)
47
+ if [ -n "$GH_LOGIN" ]; then
48
+ GH_ID_JSON="null"
49
+ GH_PERSON_ID="github-login:$(echo "$GH_LOGIN" | tr '[:upper:]' '[:lower:]')"
50
+ if [[ "$GH_ID" =~ ^[0-9]+$ ]]; then
51
+ GH_ID_JSON="$GH_ID"
52
+ GH_PERSON_ID="github:$GH_ID"
53
+ fi
54
+ AUTHOR="$GH_LOGIN"
55
+ HEALTH_GITHUB="ok"
56
+ # Set repo-local config and save to state for next time
57
+ git config user.name "${GH_NAME:-$GH_LOGIN}" 2>/dev/null || true
58
+ git config user.email "${GH_LOGIN}@users.noreply.github.com" 2>/dev/null || true
59
+ # Save to state file so we don't need API call next time
60
+ # Include onboarding_complete + name so it doesn't re-trigger onboarding
61
+ # Determine if founder or joiner: if github_username != github_org, they're a joiner
62
+ GITHUB_ORG_CFG=$(jq -r '.github_org // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
63
+ if [ -n "$GITHUB_ORG_CFG" ] && [ "$GH_LOGIN" != "$GITHUB_ORG_CFG" ]; then
64
+ USAGE_TYPE="joiner_group"
65
+ else
66
+ USAGE_TYPE="founder_group"
67
+ fi
68
+ if [ -f "$STATE_FILE" ]; then
69
+ # Existing state file — update identity but preserve onboarding status
70
+ HAS_ONBOARDING=$(jq -r '.onboarding_complete // "unset"' "$STATE_FILE" 2>/dev/null)
71
+ if [ "$HAS_ONBOARDING" = "unset" ]; then
72
+ # State file exists but no onboarding flag — set to false to trigger it
73
+ jq --arg u "$GH_LOGIN" --arg n "${GH_NAME:-$GH_LOGIN}" --arg ut "$USAGE_TYPE" \
74
+ --argjson gid "$GH_ID_JSON" --arg pid "$GH_PERSON_ID" --arg email "$GH_EMAIL" \
75
+ '.github_username = $u | .github_id = $gid | .person_id = $pid |
76
+ .github_name = $n | .name = $n |
77
+ (if $email != "" then .email = ($email | ascii_downcase) else . end) |
78
+ .onboarding_complete = false | .usage_type = $ut' "$STATE_FILE" > "$STATE_FILE.tmp" \
79
+ && mv "$STATE_FILE.tmp" "$STATE_FILE"
80
+ else
81
+ jq --arg u "$GH_LOGIN" --arg n "${GH_NAME:-$GH_LOGIN}" --arg ut "$USAGE_TYPE" \
82
+ --argjson gid "$GH_ID_JSON" --arg pid "$GH_PERSON_ID" --arg email "$GH_EMAIL" \
83
+ '.github_username = $u | .github_id = $gid | .person_id = $pid |
84
+ .github_name = $n | .name = $n |
85
+ (if $email != "" then .email = ($email | ascii_downcase) else . end) |
86
+ .usage_type = $ut' "$STATE_FILE" > "$STATE_FILE.tmp" \
87
+ && mv "$STATE_FILE.tmp" "$STATE_FILE"
88
+ fi
89
+ FIRST_SESSION="false"
90
+ else
91
+ # No state file — check graph to see if this person already exists
92
+ # (handles fresh clone / new machine for existing team members)
93
+ GRAPH_PERSON=""
94
+ GRAPH_DISPLAY_NAME=""
95
+ GRAPH_PERSON=$(bash "$SCRIPT_DIR/bin/graph.sh" query \
96
+ "MATCH (p:Person)
97
+ WHERE NOT coalesce(p.kind, '') IN ['external', 'identity_alias']
98
+ AND coalesce(p.status, 'active') = 'active'
99
+ AND p.ingestRef IS NULL
100
+ AND (p.github = \$github
101
+ OR (\$githubId <> '' AND coalesce(toString(p.githubId), '') = \$githubId)
102
+ OR toLower(\$github) IN [alias IN coalesce(p.githubAliases, []) | toLower(alias)])
103
+ RETURN p.name AS name" \
104
+ "$(jq -n --arg github "$GH_LOGIN" --arg githubId "$GH_ID" \
105
+ '{github: $github, githubId: $githubId}')" 2>/dev/null || echo "")
106
+ if echo "$GRAPH_PERSON" | jq -e '.values | length > 0' &>/dev/null; then
107
+ GRAPH_DISPLAY_NAME=$(echo "$GRAPH_PERSON" | jq -r '.values[0][0] // empty' 2>/dev/null)
108
+ # Existing team member — skip onboarding
109
+ jq -n --arg github "$GH_LOGIN" --argjson githubId "$GH_ID_JSON" \
110
+ --arg personId "$GH_PERSON_ID" --arg githubName "${GH_NAME:-$GH_LOGIN}" \
111
+ --arg name "${GRAPH_DISPLAY_NAME:-${GH_NAME:-$GH_LOGIN}}" \
112
+ --arg displayName "${GRAPH_DISPLAY_NAME:-}" --arg email "$GH_EMAIL" \
113
+ --arg usageType "$USAGE_TYPE" \
114
+ '{
115
+ github_username: $github, github_id: $githubId, person_id: $personId,
116
+ github_name: $githubName, name: $name, display_name: $displayName,
117
+ email: (if $email == "" then null else ($email | ascii_downcase) end),
118
+ onboarding_complete: true, usage_type: $usageType,
119
+ session_tracking: true, transcript_sharing: true, telemetry: true,
120
+ contact_preference: "all", telemetry_noticed: true
121
+ }' > "$STATE_FILE"
122
+ FIRST_SESSION="false"
123
+ else
124
+ # Genuinely new user — trigger onboarding
125
+ jq -n --arg github "$GH_LOGIN" --argjson githubId "$GH_ID_JSON" \
126
+ --arg personId "$GH_PERSON_ID" --arg githubName "${GH_NAME:-$GH_LOGIN}" \
127
+ --arg email "$GH_EMAIL" --arg usageType "$USAGE_TYPE" \
128
+ '{
129
+ github_username: $github, github_id: $githubId, person_id: $personId,
130
+ github_name: $githubName, name: $githubName,
131
+ email: (if $email == "" then null else ($email | ascii_downcase) end),
132
+ onboarding_complete: false, usage_type: $usageType, first_session: true
133
+ }' > "$STATE_FILE"
134
+ FIRST_SESSION="true"
135
+ fi
136
+ fi
137
+ fi
138
+ fi
139
+ fi
140
+
141
+ # Final fallback: git config user.name (global or local)
142
+ if [ -z "$AUTHOR" ]; then
143
+ FULLNAME=$(git config user.name 2>/dev/null || echo "")
144
+ AUTHOR=$(echo "$FULLNAME" | tr '[:upper:]' '[:lower:]' | cut -d' ' -f1)
145
+ fi
146
+ fi
147
+
148
+ if [ -z "$AUTHOR" ]; then
149
+ AUTHOR="unknown"
150
+ HEALTH_GITHUB="fail"
151
+ fi
152
+
153
+ # --- Validate gh CLI identity matches git identity ---
154
+ # The gh CLI has its own auth (keyring) separate from .env GITHUB_TOKEN.
155
+ # If they diverge, PRs get created under the wrong account.
156
+ # The `gh api user` round-trip (~1s) is off the boot path: each start reports
157
+ # the marker the previous background check left, then refreshes it detached.
158
+ # A mismatch therefore surfaces one session late — acceptable for a warning
159
+ # about a rare, persistent condition.
160
+ if command -v gh &>/dev/null && [ -n "$AUTHOR" ] && [ "$AUTHOR" != "unknown" ]; then
161
+ _GH_MISMATCH_FILE="$HOME/.egregore/gh-identity-mismatch-$(echo -n "${MAIN_PROJECT_DIR:-$SCRIPT_DIR}" | cksum | cut -d' ' -f1)"
162
+ # Marker format: line 1 = the AUTHOR the check ran as, line 2 = the gh login
163
+ # observed. Warn only when line 1 matches the current AUTHOR — the same
164
+ # machine/instance can serve different git identities, and Alice's stale
165
+ # marker must not tell Bob that Bob mismatches Bob.
166
+ _GH_MARKER_AUTHOR=$(sed -n '1p' "$_GH_MISMATCH_FILE" 2>/dev/null || true)
167
+ GH_CLI_USER=$(sed -n '2p' "$_GH_MISMATCH_FILE" 2>/dev/null || true)
168
+ if [ -n "$GH_CLI_USER" ] && [ "$_GH_MARKER_AUTHOR" = "$AUTHOR" ]; then
169
+ echo ""
170
+ echo "WARNING: Identity mismatch detected."
171
+ echo " Git identity (.env): $AUTHOR"
172
+ echo " gh CLI (active): $GH_CLI_USER"
173
+ echo " PRs will be created as '$GH_CLI_USER', not '$AUTHOR'."
174
+ echo " Fix: gh auth switch --user $AUTHOR"
175
+ echo ""
176
+ fi
177
+ ( (
178
+ _GH_LIVE=$(gh api user --jq '.login' 2>/dev/null || true)
179
+ if [ -n "$_GH_LIVE" ]; then
180
+ if [ "$(echo "$_GH_LIVE" | tr '[:upper:]' '[:lower:]')" != "$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]')" ]; then
181
+ mkdir -p "$HOME/.egregore" 2>/dev/null
182
+ printf '%s\n%s\n' "$AUTHOR" "$_GH_LIVE" > "$_GH_MISMATCH_FILE.tmp" \
183
+ && mv "$_GH_MISMATCH_FILE.tmp" "$_GH_MISMATCH_FILE"
184
+ else
185
+ rm -f "$_GH_MISMATCH_FILE"
186
+ fi
187
+ fi
188
+ ) >/dev/null 2>&1 & ) 2>/dev/null
189
+ fi
190
+
191
+ # --- Export telemetry identity env vars ---
192
+ export EGREGORE_USER="$AUTHOR"
193
+ export EGREGORE_ORG="$(jq -r '.slug // .github_org // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null || true)"
194
+ export EGREGORE_SESSION_ID="$(date -u +%Y%m%dT%H%M%S)-${AUTHOR}-$$"
195
+
196
+ # Persist session ID to file so telemetry.sh can read it
197
+ # (env vars from hooks don't propagate into the Claude Code agent)
198
+ SESSION_ID_DIR="$HOME/.egregore"
199
+ mkdir -p "$SESSION_ID_DIR"
200
+ PROJ_HASH=$(echo -n "$SCRIPT_DIR" | md5 2>/dev/null || echo -n "$SCRIPT_DIR" | md5sum 2>/dev/null | cut -d' ' -f1)
201
+ echo "$EGREGORE_SESSION_ID" > "$SESSION_ID_DIR/session-${PROJ_HASH}.id"
202
+ # Also write project-local file (CLAUDE.md reads this — no glob needed)
203
+ echo "$EGREGORE_SESSION_ID" > "$SCRIPT_DIR/.egregore-session-id"
204
+
205
+ # Model stamp hashes the git common-dir root because telemetry.sh resolves it that way.
206
+ MODEL_REPO_ROOT=$(git -C "$SCRIPT_DIR" rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's|/\.git$||') || MODEL_REPO_ROOT="$SCRIPT_DIR"
207
+ MODEL_PROJ_HASH=$(echo -n "$MODEL_REPO_ROOT" | md5 2>/dev/null || echo -n "$MODEL_REPO_ROOT" | md5sum 2>/dev/null | cut -d' ' -f1)
208
+ MODEL_STAMP_FILE="$SESSION_ID_DIR/session-model-${MODEL_PROJ_HASH}"
209
+ if [ -n "${CLAUDE_MODEL:-}" ]; then
210
+ echo "$CLAUDE_MODEL" > "$MODEL_STAMP_FILE"
211
+ else
212
+ rm -f "$MODEL_STAMP_FILE"
213
+ fi
@@ -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,31 @@
1
+ #!/usr/bin/env bash
2
+ # noncode.sh — shared "non-coding change" classifier for auto-merge decisions.
3
+ #
4
+ # The org rule: non-coding PRs auto-merge to develop; code goes to review.
5
+ # Non-coding means:
6
+ # - any .md file, anywhere
7
+ # - anything under content directories (artifacts/, docs/, .threads/) —
8
+ # html/css/images/design bundles there are authored content, not shipped code
9
+ # - session-state droppings (.egregore-worktree-tty)
10
+ # Everything else — bin/, api/, packages/, .claude/, site 2/, root config — is
11
+ # code and stays open for review.
12
+ #
13
+ # Sourced by handoff-save-egregore.sh, session-autosave.sh, agent.sh, and the
14
+ # /save skill. Keep the policy HERE — call sites must not grow their own.
15
+
16
+ # noncode_filter — reads paths on stdin, prints the first code-bearing path.
17
+ # Empty output = everything is non-coding.
18
+ noncode_filter() {
19
+ grep -Ev '\.md$' \
20
+ | grep -Ev '^(artifacts|docs|\.threads)/' \
21
+ | grep -Ev '^\.egregore-worktree-tty$' \
22
+ | head -1 || true
23
+ }
24
+
25
+ # noncode_blockers [base-ref] [repo-dir] — first code-bearing path committed
26
+ # on HEAD relative to base (three-dot: only our side of the diff).
27
+ # Empty output = the branch is non-coding and safe to auto-merge.
28
+ noncode_blockers() {
29
+ local base="${1:-origin/develop}" dir="${2:-.}"
30
+ git -C "$dir" diff "$base...HEAD" --name-only 2>/dev/null | noncode_filter
31
+ }