create-egregore 0.19.33 → 0.20.0

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 (171) hide show
  1. package/assets/egregore-launcher.js +118 -13
  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 +45 -3
  7. package/package.json +4 -3
  8. package/runtime/codex/.codex/skills/ingest-notion/SKILL.md +29 -0
  9. package/runtime/codex/.codex/skills/notion-connect/SKILL.md +29 -0
  10. package/runtime/codex/.codex/skills/teams-connect/SKILL.md +29 -0
  11. package/runtime/codex/.codex/spec-manifest.json +3 -3
  12. package/runtime/codex/AGENTS.md +11 -4
  13. package/runtime/codex/bin/activity-data.sh +4 -8
  14. package/runtime/codex/bin/attendant.sh +13 -0
  15. package/runtime/codex/bin/autosave.sh +27 -1
  16. package/runtime/codex/bin/codex-render-spec.mjs +6 -0
  17. package/runtime/codex/bin/codex-session-start.sh +3 -1
  18. package/runtime/codex/bin/graph-batch.sh +25 -6
  19. package/runtime/codex/bin/graph-op.sh +5 -4
  20. package/runtime/codex/bin/graph.sh +31 -5
  21. package/runtime/codex/bin/ingest.py +81 -5
  22. package/runtime/codex/bin/lib/context.sh +26 -1
  23. package/runtime/codex/bin/lib/greeting.sh +4 -1
  24. package/runtime/codex/bin/lib/worktree-links.sh +25 -0
  25. package/runtime/codex/manifest.json +7 -1
  26. package/runtime/pi/.codex/skills/ingest-notion/SKILL.md +29 -0
  27. package/runtime/pi/.codex/skills/notion-connect/SKILL.md +29 -0
  28. package/runtime/pi/.codex/skills/teams-connect/SKILL.md +29 -0
  29. package/runtime/pi/.codex/spec-manifest.json +3 -3
  30. package/runtime/pi/.pi/APPEND_SYSTEM.md +3 -2
  31. package/runtime/pi/.pi/extensions/egregore.ts +17 -6
  32. package/runtime/pi/.pi/spec-manifest.json +2 -2
  33. package/runtime/pi/AGENTS.md +11 -4
  34. package/runtime/pi/bin/activity-data.sh +4 -8
  35. package/runtime/pi/bin/attendant.sh +13 -0
  36. package/runtime/pi/bin/autosave.sh +27 -1
  37. package/runtime/pi/bin/codex-render-spec.mjs +6 -0
  38. package/runtime/pi/bin/codex-session-start.sh +3 -1
  39. package/runtime/pi/bin/graph-batch.sh +25 -6
  40. package/runtime/pi/bin/graph-op.sh +5 -4
  41. package/runtime/pi/bin/graph.sh +31 -5
  42. package/runtime/pi/bin/ingest.py +81 -5
  43. package/runtime/pi/bin/lib/context.sh +26 -1
  44. package/runtime/pi/bin/lib/greeting.sh +4 -1
  45. package/runtime/pi/bin/lib/worktree-links.sh +25 -0
  46. package/runtime/pi/bin/pi-branch-policy.mjs +138 -1
  47. package/runtime/pi/bin/pi-product-workflows.mjs +35 -7
  48. package/runtime/pi/manifest.json +7 -1
  49. package/runtime/prime/.claude/context/notification-consent.md +66 -0
  50. package/runtime/prime/.codex/config.toml +17 -0
  51. package/runtime/prime/.codex/hooks/branch-guard.js +461 -0
  52. package/runtime/prime/.codex/hooks/search-hint.js +67 -0
  53. package/runtime/prime/.codex/hooks.json +30 -0
  54. package/runtime/prime/.codex/skills/activity/SKILL.md +82 -0
  55. package/runtime/prime/.codex/skills/announce/SKILL.md +67 -0
  56. package/runtime/prime/.codex/skills/archive/SKILL.md +45 -0
  57. package/runtime/prime/.codex/skills/branch/SKILL.md +29 -0
  58. package/runtime/prime/.codex/skills/checkup/SKILL.md +45 -0
  59. package/runtime/prime/.codex/skills/commit/SKILL.md +29 -0
  60. package/runtime/prime/.codex/skills/contribute/SKILL.md +29 -0
  61. package/runtime/prime/.codex/skills/create-skill/SKILL.md +29 -0
  62. package/runtime/prime/.codex/skills/dark-mode/SKILL.md +29 -0
  63. package/runtime/prime/.codex/skills/dashboard/SKILL.md +59 -0
  64. package/runtime/prime/.codex/skills/deep-reflect/SKILL.md +109 -0
  65. package/runtime/prime/.codex/skills/delete-user/SKILL.md +29 -0
  66. package/runtime/prime/.codex/skills/emissary/SKILL.md +45 -0
  67. package/runtime/prime/.codex/skills/handoff/SKILL.md +166 -0
  68. package/runtime/prime/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
  69. package/runtime/prime/.codex/skills/harvest/SKILL.md +131 -0
  70. package/runtime/prime/.codex/skills/ingest/SKILL.md +29 -0
  71. package/runtime/prime/.codex/skills/ingest-notion/SKILL.md +29 -0
  72. package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +45 -0
  73. package/runtime/prime/.codex/skills/invite/SKILL.md +44 -0
  74. package/runtime/prime/.codex/skills/issue/SKILL.md +45 -0
  75. package/runtime/prime/.codex/skills/me/SKILL.md +29 -0
  76. package/runtime/prime/.codex/skills/meeting/SKILL.md +45 -0
  77. package/runtime/prime/.codex/skills/note/SKILL.md +29 -0
  78. package/runtime/prime/.codex/skills/notion-connect/SKILL.md +29 -0
  79. package/runtime/prime/.codex/skills/onboarding/SKILL.md +45 -0
  80. package/runtime/prime/.codex/skills/pr/SKILL.md +29 -0
  81. package/runtime/prime/.codex/skills/pull/SKILL.md +29 -0
  82. package/runtime/prime/.codex/skills/push/SKILL.md +29 -0
  83. package/runtime/prime/.codex/skills/quest/SKILL.md +80 -0
  84. package/runtime/prime/.codex/skills/reflect/SKILL.md +45 -0
  85. package/runtime/prime/.codex/skills/review-pr/SKILL.md +45 -0
  86. package/runtime/prime/.codex/skills/save/SKILL.md +125 -0
  87. package/runtime/prime/.codex/skills/slack-connect/SKILL.md +29 -0
  88. package/runtime/prime/.codex/skills/summon/SKILL.md +45 -0
  89. package/runtime/prime/.codex/skills/sync-repos/SKILL.md +29 -0
  90. package/runtime/prime/.codex/skills/teams-connect/SKILL.md +29 -0
  91. package/runtime/prime/.codex/skills/telegram-connect/SKILL.md +29 -0
  92. package/runtime/prime/.codex/skills/telemetry/SKILL.md +29 -0
  93. package/runtime/prime/.codex/skills/test/SKILL.md +45 -0
  94. package/runtime/prime/.codex/skills/the-spiral/SKILL.md +84 -0
  95. package/runtime/prime/.codex/skills/tui-design/SKILL.md +45 -0
  96. package/runtime/prime/.codex/skills/tutorial/SKILL.md +45 -0
  97. package/runtime/prime/.codex/skills/update/SKILL.md +29 -0
  98. package/runtime/prime/.codex/skills/view/SKILL.md +156 -0
  99. package/runtime/prime/.codex/skills/view/agents/openai.yaml +4 -0
  100. package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +221 -0
  101. package/runtime/prime/.codex/skills/visual-explain/SKILL.md +45 -0
  102. package/runtime/prime/.codex/skills/wrap/SKILL.md +58 -0
  103. package/runtime/prime/.codex/spec-manifest.json +132 -0
  104. package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +393 -0
  105. package/runtime/prime/.prime/agent/extensions/egregore.ts +460 -0
  106. package/runtime/prime/.prime/agent/settings.json +4 -0
  107. package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +34 -0
  108. package/runtime/prime/.prime/agent/skills/egregore/pyproject.toml +13 -0
  109. package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +156 -0
  110. package/runtime/prime/.prime/agent/spec-manifest.json +12 -0
  111. package/runtime/prime/AGENTS.md +489 -0
  112. package/runtime/prime/bin/activity-data.sh +238 -0
  113. package/runtime/prime/bin/agent.sh +775 -0
  114. package/runtime/prime/bin/attendant.sh +707 -0
  115. package/runtime/prime/bin/autosave.sh +112 -0
  116. package/runtime/prime/bin/capture-reconcile.sh +69 -0
  117. package/runtime/prime/bin/capture-run.sh +341 -0
  118. package/runtime/prime/bin/codex-install-skills.sh +78 -0
  119. package/runtime/prime/bin/codex-render-spec.mjs +440 -0
  120. package/runtime/prime/bin/codex-session-start.sh +265 -0
  121. package/runtime/prime/bin/codex-skill-render.mjs +347 -0
  122. package/runtime/prime/bin/codex-sync-skills.sh +232 -0
  123. package/runtime/prime/bin/connect-refresh.sh +33 -0
  124. package/runtime/prime/bin/dashboard-data.sh +178 -0
  125. package/runtime/prime/bin/graph-batch.sh +79 -0
  126. package/runtime/prime/bin/graph-op.sh +1102 -0
  127. package/runtime/prime/bin/graph-wal.sh +229 -0
  128. package/runtime/prime/bin/graph.sh +223 -0
  129. package/runtime/prime/bin/handoff-pr-backfill.sh +86 -0
  130. package/runtime/prime/bin/handoff-run.sh +671 -0
  131. package/runtime/prime/bin/handoff-save-egregore.sh +212 -0
  132. package/runtime/prime/bin/index-handoff.sh +322 -0
  133. package/runtime/prime/bin/ingest-graph.sh +80 -0
  134. package/runtime/prime/bin/ingest-surface.html +495 -0
  135. package/runtime/prime/bin/ingest.py +756 -0
  136. package/runtime/prime/bin/ingest.sh +279 -0
  137. package/runtime/prime/bin/ingest_graph.py +856 -0
  138. package/runtime/prime/bin/ingest_surface.py +544 -0
  139. package/runtime/prime/bin/lib/artifact-id.sh +46 -0
  140. package/runtime/prime/bin/lib/config.sh +116 -0
  141. package/runtime/prime/bin/lib/context.sh +621 -0
  142. package/runtime/prime/bin/lib/dashboard-artifact.sh +242 -0
  143. package/runtime/prime/bin/lib/ensure-develop.sh +183 -0
  144. package/runtime/prime/bin/lib/git-sync.sh +406 -0
  145. package/runtime/prime/bin/lib/greeting.sh +519 -0
  146. package/runtime/prime/bin/lib/hash.sh +13 -0
  147. package/runtime/prime/bin/lib/identity.sh +213 -0
  148. package/runtime/prime/bin/lib/metrics.sh +374 -0
  149. package/runtime/prime/bin/lib/noncode.sh +31 -0
  150. package/runtime/prime/bin/lib/permissions.sh +106 -0
  151. package/runtime/prime/bin/lib/time.sh +56 -0
  152. package/runtime/prime/bin/lib/worktree-links.sh +79 -0
  153. package/runtime/prime/bin/notify.sh +636 -0
  154. package/runtime/prime/bin/person.py +630 -0
  155. package/runtime/prime/bin/person.sh +448 -0
  156. package/runtime/prime/bin/pi-branch-policy.mjs +312 -0
  157. package/runtime/prime/bin/pi-product-workflows.mjs +394 -0
  158. package/runtime/prime/bin/prime-render-spec.mjs +123 -0
  159. package/runtime/prime/bin/prime-session-start.sh +24 -0
  160. package/runtime/prime/bin/publish-artifact.sh +271 -0
  161. package/runtime/prime/bin/publish-references.sh +144 -0
  162. package/runtime/prime/bin/repo-state.sh +96 -0
  163. package/runtime/prime/bin/search.sh +240 -0
  164. package/runtime/prime/bin/session-autosave.sh +209 -0
  165. package/runtime/prime/bin/session-log.sh +199 -0
  166. package/runtime/prime/bin/settings.sh +304 -0
  167. package/runtime/prime/bin/telemetry.sh +391 -0
  168. package/runtime/prime/bin/worktree-create.sh +128 -0
  169. package/runtime/prime/bin/worktree-remove.sh +50 -0
  170. package/runtime/prime/bin/worktree.sh +198 -0
  171. package/runtime/prime/manifest.json +178 -0
@@ -0,0 +1,448 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # One identity spine for .egregore-state.json, memory/people, Supabase, and
5
+ # Neo4j. GitHub's numeric id is the durable key when available.
6
+
7
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
8
+ SCRIPT_DIR="$ROOT"
9
+ CONFIG="$ROOT/egregore.json"
10
+ STATE="$ROOT/.egregore-state.json"
11
+ PERSON_PY="$ROOT/bin/person.py"
12
+
13
+ # shellcheck source=bin/lib/config.sh
14
+ source "$ROOT/bin/lib/config.sh"
15
+
16
+ usage() {
17
+ echo 'usage: bash bin/person.sh {sync|onboard|set-name NAME|set-email EMAIL|show|backfill [--dry-run|--apply] [--include-removed]}' >&2
18
+ exit 2
19
+ }
20
+
21
+ remote_config() {
22
+ api_url="${EGREGORE_API_URL:-$(_config_val api_url)}"
23
+ api_key="${EGREGORE_API_KEY:-$(_load_env_var EGREGORE_API_KEY)}"
24
+ }
25
+
26
+ sync_remote_identity() {
27
+ identity="$1"
28
+ remote_config
29
+
30
+ api_status="not-configured"
31
+ platform_user_id=""
32
+ if [ -n "$api_url" ] && [ -n "$api_key" ]; then
33
+ api_payload="$(printf '%s' "$identity" | jq '{
34
+ github_username,
35
+ github_id,
36
+ github_name,
37
+ github_aliases,
38
+ display_name,
39
+ email,
40
+ emails
41
+ }')"
42
+ api_result="$(curl -sf "${api_url}/api/user/ensure" \
43
+ -H "Authorization: Bearer ${api_key}" \
44
+ -H "Content-Type: application/json" \
45
+ -d "$api_payload" \
46
+ --max-time 10 2>/dev/null || true)"
47
+ api_status="$(printf '%s' "$api_result" | jq -r '.status // "failed"' 2>/dev/null || echo failed)"
48
+ platform_user_id="$(printf '%s' "$api_result" | jq -r '.user_id // empty' 2>/dev/null || true)"
49
+ fi
50
+
51
+ # A removed membership is terminal until an admin explicitly re-invites
52
+ # the person. Never let startup recreate their graph membership.
53
+ if [ "$api_status" = "removed" ]; then
54
+ printf '%s' "$identity" | jq \
55
+ --arg platform_user_id "$platform_user_id" \
56
+ '. + {
57
+ status:"removed",
58
+ supabase:"removed",
59
+ graph:"skipped-removed",
60
+ platform_user_id:$platform_user_id,
61
+ merged_duplicates:0
62
+ }'
63
+ return 0
64
+ fi
65
+
66
+ params="$(printf '%s' "$identity" | jq --arg platformUserId "$platform_user_id" '{
67
+ personId:.person_id,
68
+ github:.github_username,
69
+ githubId:(if .github_id == null then "" else (.github_id|tostring) end),
70
+ fullName:.github_name,
71
+ displayName:.display_name,
72
+ primaryEmail:(.email // ""),
73
+ emails,
74
+ githubAliases:.github_aliases,
75
+ previousNames:.previous_names,
76
+ profilePath:.profile_path,
77
+ allowNameMatch:(
78
+ (.allow_name_match // false)
79
+ or (.source_profile // "") != ""
80
+ or ((.aliases_reconciled // []) | length) > 0
81
+ ),
82
+ platformUserId:$platformUserId
83
+ }')"
84
+
85
+ graph_status="failed"
86
+ if graph_result="$(bash "$ROOT/bin/graph.sh" query "
87
+ OPTIONAL MATCH (legacy:Person)
88
+ WHERE NOT coalesce(legacy.kind, '') IN ['external', 'identity_alias']
89
+ AND coalesce(legacy.status, 'active') = 'active'
90
+ AND legacy.ingestRef IS NULL
91
+ AND (
92
+ legacy.personId = \$personId
93
+ OR (\$githubId <> '' AND coalesce(toString(legacy.githubId), '') = \$githubId)
94
+ OR toLower(coalesce(legacy.github, '')) = toLower(\$github)
95
+ OR toLower(coalesce(legacy.github, '')) IN
96
+ [alias IN \$githubAliases | toLower(alias)]
97
+ OR (
98
+ \$allowNameMatch
99
+ AND legacy.personId IS NULL
100
+ AND legacy.githubId IS NULL
101
+ AND (legacy.github IS NULL OR legacy.github = '')
102
+ AND toLower(coalesce(legacy.name, '')) = toLower(\$displayName)
103
+ )
104
+ )
105
+ WITH legacy
106
+ ORDER BY CASE
107
+ WHEN legacy.personId = \$personId THEN 0
108
+ WHEN \$githubId <> '' AND coalesce(toString(legacy.githubId), '') = \$githubId THEN 1
109
+ WHEN toLower(coalesce(legacy.github, '')) = toLower(\$github) THEN 2
110
+ ELSE 3
111
+ END
112
+ WITH head(collect(legacy)) AS legacy
113
+ FOREACH (_ IN CASE WHEN legacy IS NULL THEN [] ELSE [1] END |
114
+ SET legacy.personId = \$personId
115
+ )
116
+ MERGE (p:Person {personId: \$personId})
117
+ ON CREATE SET p.joined = date()
118
+ SET p.github = \$github,
119
+ p.githubId = CASE WHEN \$githubId = '' THEN p.githubId ELSE \$githubId END,
120
+ p.kind = 'member',
121
+ p.org = \$_org,
122
+ p.status = 'active',
123
+ p.removedAt = null,
124
+ p.identityStatus = CASE WHEN \$githubId = '' THEN 'provisional' ELSE 'verified' END,
125
+ p.platformUserId = CASE WHEN \$platformUserId = '' THEN p.platformUserId ELSE \$platformUserId END,
126
+ p.name = \$displayName,
127
+ p.displayName = \$displayName,
128
+ p.fullName = \$fullName,
129
+ p.email = CASE WHEN \$primaryEmail = '' THEN p.email ELSE \$primaryEmail END,
130
+ p.emails = reduce(acc = coalesce(p.emails, []), value IN \$emails |
131
+ CASE WHEN value IN acc THEN acc ELSE acc + value END),
132
+ p.githubAliases = reduce(acc = coalesce(p.githubAliases, []), value IN \$githubAliases |
133
+ CASE WHEN value IN acc THEN acc ELSE acc + value END),
134
+ p.previousNames = reduce(acc = coalesce(p.previousNames, []), value IN \$previousNames |
135
+ CASE WHEN value IN acc THEN acc ELSE acc + value END),
136
+ p.profilePath = \$profilePath,
137
+ p.identityVersion = 1,
138
+ p.identityUpdatedAt = datetime()
139
+ WITH p
140
+ MATCH (o:Org {id: \$_org})
141
+ MERGE (p)-[:MEMBER_OF]->(o)
142
+ RETURN p.personId AS personId
143
+ " "$params" 2>/dev/null)"; then
144
+ if printf '%s' "$graph_result" | jq -e \
145
+ --arg personId "$(printf '%s' "$identity" | jq -r .person_id)" \
146
+ '.values[0][0] == $personId' >/dev/null 2>&1; then
147
+ graph_status="synced"
148
+ fi
149
+ fi
150
+
151
+ merged_duplicates=0
152
+ if [ "$graph_status" = "synced" ]; then
153
+ duplicates="$(bash "$ROOT/bin/graph.sh" query "
154
+ MATCH (real:Person {personId: \$personId})
155
+ MATCH (duplicate:Person)
156
+ WHERE duplicate <> real AND (
157
+ duplicate.personId = \$personId
158
+ OR (\$githubId <> '' AND coalesce(toString(duplicate.githubId), '') = \$githubId)
159
+ OR toLower(coalesce(duplicate.github, '')) = toLower(\$github)
160
+ OR toLower(coalesce(duplicate.github, '')) IN \$githubAliases
161
+ OR (
162
+ duplicate.githubId IS NULL
163
+ AND duplicate.personId IS NULL
164
+ AND (duplicate.github IS NULL OR duplicate.github = '')
165
+ AND any(email IN coalesce(duplicate.emails, []) WHERE email IN \$emails)
166
+ )
167
+ OR (
168
+ \$allowNameMatch
169
+ AND
170
+ (duplicate.github IS NULL OR duplicate.github = '')
171
+ AND duplicate.personId IS NULL
172
+ AND toLower(coalesce(duplicate.name, '')) = toLower(\$displayName)
173
+ )
174
+ )
175
+ AND NOT coalesce(duplicate.kind, '') IN ['external', 'identity_alias']
176
+ AND coalesce(duplicate.status, 'active') = 'active'
177
+ AND duplicate.ingestRef IS NULL
178
+ RETURN DISTINCT coalesce(duplicate.github, duplicate.name) AS ref
179
+ " "$params" 2>/dev/null || echo '{"values":[]}')"
180
+ while IFS= read -r duplicate; do
181
+ [ -n "$duplicate" ] || continue
182
+ if bash "$ROOT/bin/graph-op.sh" merge-person "$(printf '%s' "$identity" | jq -r .person_id)" \
183
+ "$duplicate" >/dev/null 2>&1; then
184
+ merged_duplicates=$((merged_duplicates + 1))
185
+ fi
186
+ done < <(printf '%s' "$duplicates" | jq -r '.values[]?[0] // empty')
187
+ fi
188
+
189
+ status="synced"
190
+ [ "$api_status" = "ok" ] || status="partial"
191
+ [ "$graph_status" = "synced" ] || status="partial"
192
+ printf '%s' "$identity" | jq \
193
+ --arg status "$status" \
194
+ --arg supabase "$api_status" \
195
+ --arg graph "$graph_status" \
196
+ --arg platform_user_id "$platform_user_id" \
197
+ --argjson merged_duplicates "$merged_duplicates" \
198
+ '. + {
199
+ status:$status,
200
+ supabase:$supabase,
201
+ graph:$graph,
202
+ platform_user_id:$platform_user_id,
203
+ merged_duplicates:$merged_duplicates
204
+ }'
205
+ }
206
+
207
+ backfill_people() {
208
+ mode="dry-run"
209
+ include_removed="false"
210
+ shift
211
+ while [ "$#" -gt 0 ]; do
212
+ case "$1" in
213
+ --dry-run) mode="dry-run" ;;
214
+ --apply) mode="apply" ;;
215
+ --include-removed) include_removed="true" ;;
216
+ *) usage ;;
217
+ esac
218
+ shift
219
+ done
220
+
221
+ if [ "$(_detect_mode)" = "local" ]; then
222
+ jq -n '{status:"unavailable",reason:"person backfill requires the hosted member roster in this configuration"}'
223
+ return 1
224
+ fi
225
+ command -v gh >/dev/null 2>&1 || {
226
+ jq -n '{status:"error",error:"GitHub CLI is required to resolve stable user ids"}'
227
+ return 1
228
+ }
229
+
230
+ remote_config
231
+ if [ -z "$api_url" ] || [ -z "$api_key" ]; then
232
+ jq -n '{status:"error",error:"connected API configuration is incomplete"}'
233
+ return 1
234
+ fi
235
+ org_slug="$(_config_val slug)"
236
+ members_result="$(curl -sf "${api_url}/api/org/${org_slug}/members?include_removed=true" \
237
+ -H "Authorization: Bearer ${api_key}" \
238
+ --max-time 20 2>/dev/null || true)"
239
+ if ! printf '%s' "$members_result" | jq -e '.members | type == "array"' >/dev/null 2>&1; then
240
+ jq -n '{status:"error",error:"could not load the hosted member roster"}'
241
+ return 1
242
+ fi
243
+ if ! printf '%s' "$members_result" | jq -e \
244
+ '.members | length == 0 or all(has("github_id") and has("github_aliases"))' \
245
+ >/dev/null 2>&1; then
246
+ jq -n '{status:"blocked",error:"the hosted API is still using the pre-identity member contract; deploy it before backfilling"}'
247
+ return 1
248
+ fi
249
+
250
+ results='[]'
251
+ excluded_removed=0
252
+ while IFS= read -r member; do
253
+ [ -n "$member" ] || continue
254
+ member_status="$(printf '%s' "$member" | jq -r '.status // "active"')"
255
+ if [ "$member_status" != "active" ] && [ "$include_removed" != "true" ]; then
256
+ excluded_removed=$((excluded_removed + 1))
257
+ continue
258
+ fi
259
+
260
+ previous_github="$(printf '%s' "$member" | jq -r '.github_username // empty')"
261
+ if ! printf '%s' "$previous_github" | grep -Eq '^[A-Za-z0-9-]{1,39}$'; then
262
+ item="$(jq -n --arg github "$previous_github" '{
263
+ github_username:$github,status:"skipped",reason:"invalid GitHub login"
264
+ }')"
265
+ results="$(printf '%s' "$results" | jq --argjson item "$item" '. + [$item]')"
266
+ continue
267
+ fi
268
+
269
+ github_result="$(gh api "users/${previous_github}" \
270
+ --jq '{login,id,name,email}' 2>/dev/null || true)"
271
+ if ! printf '%s' "$github_result" | jq -e \
272
+ '.login and (.id | type == "number")' >/dev/null 2>&1; then
273
+ item="$(jq -n --arg github "$previous_github" '{
274
+ github_username:$github,status:"unresolved",reason:"GitHub identity could not be resolved"
275
+ }')"
276
+ results="$(printf '%s' "$results" | jq --argjson item "$item" '. + [$item]')"
277
+ continue
278
+ fi
279
+
280
+ github="$(printf '%s' "$github_result" | jq -r .login)"
281
+ github_id="$(printf '%s' "$github_result" | jq -r .id)"
282
+ github_name="$(printf '%s' "$github_result" | jq -r '.name // empty')"
283
+ public_email="$(printf '%s' "$github_result" | jq -r '.email // empty')"
284
+ display_name="$(printf '%s' "$member" | jq -r \
285
+ --arg githubName "$github_name" --arg github "$github" \
286
+ '.display_name // (if $githubName != "" then $githubName else $github end)')"
287
+ role="$(printf '%s' "$member" | jq -r '.member_role // .role // "Member"')"
288
+ joined="$(printf '%s' "$member" | jq -r '.joined_at // empty' | cut -c1-10)"
289
+ aliases="$(printf '%s' "$member" | jq -r \
290
+ --arg old "$previous_github" --arg current "$github" '
291
+ ((.github_aliases // []) + (if ($old|ascii_downcase) != ($current|ascii_downcase) then [$old] else [] end))
292
+ | map(select(type == "string" and length > 0)) | unique | join(",")
293
+ ')"
294
+ emails="$(printf '%s' "$member" | jq -r \
295
+ --arg public "$public_email" '
296
+ ((.emails // []) + [(.primary_email // ""), $public])
297
+ | map(select(type == "string" and length > 0)) | unique | join(",")
298
+ ')"
299
+
300
+ profile_args=(
301
+ sync-profile
302
+ --github "$github"
303
+ --github-id "$github_id"
304
+ --github-name "$github_name"
305
+ --display-name "$display_name"
306
+ --previous-github "$previous_github"
307
+ --github-aliases "$aliases"
308
+ --emails "$emails"
309
+ --role "$role"
310
+ --joined "$joined"
311
+ )
312
+ [ "$mode" = "apply" ] || profile_args+=(--dry-run)
313
+ if ! identity="$(python3 "$PERSON_PY" "${profile_args[@]}")"; then
314
+ item="$(jq -n --arg github "$github" '{
315
+ github_username:$github,status:"skipped",reason:"profile reconciliation failed"
316
+ }')"
317
+ results="$(printf '%s' "$results" | jq --argjson item "$item" '. + [$item]')"
318
+ continue
319
+ fi
320
+ roster_name_matches="$(printf '%s' "$members_result" | jq \
321
+ --arg displayName "$display_name" '
322
+ [.members[]
323
+ | (.display_name // "")
324
+ | select(length > 0)
325
+ | ascii_downcase
326
+ | select(. == ($displayName | ascii_downcase))]
327
+ | length
328
+ ')"
329
+ allow_name_match="false"
330
+ [ "$roster_name_matches" -eq 1 ] && allow_name_match="true"
331
+ identity="$(printf '%s' "$identity" | jq \
332
+ --argjson allowNameMatch "$allow_name_match" \
333
+ '.allow_name_match = $allowNameMatch')"
334
+
335
+ if [ "$mode" = "apply" ]; then
336
+ synced="$(sync_remote_identity "$identity")"
337
+ report="$(printf '%s' "$synced" | jq 'del(.email,.emails,.allow_name_match)')"
338
+ else
339
+ report="$(printf '%s' "$identity" | jq \
340
+ '. + {status:"planned"} | del(.email,.emails,.allow_name_match)')"
341
+ fi
342
+ results="$(printf '%s' "$results" | jq --argjson item "$report" '. + [$item]')"
343
+ done < <(printf '%s' "$members_result" | jq -c '.members[]')
344
+
345
+ printf '%s' "$results" | jq \
346
+ --arg mode "$mode" \
347
+ --arg org "$org_slug" \
348
+ --argjson excluded_removed "$excluded_removed" '
349
+ {
350
+ status:(if any(.[]; .status == "partial") then "partial" else "ok" end),
351
+ mode:$mode,
352
+ org:$org,
353
+ summary:{
354
+ considered:length,
355
+ planned_or_synced:[.[] | select(.status == "planned" or .status == "synced")]|length,
356
+ partial:[.[] | select(.status == "partial")]|length,
357
+ unresolved:[.[] | select(.status == "unresolved")]|length,
358
+ skipped:[.[] | select(.status == "skipped")]|length,
359
+ excluded_removed:$excluded_removed
360
+ },
361
+ people:.
362
+ }'
363
+ }
364
+
365
+ command="${1:-}"
366
+ [ -n "$command" ] || usage
367
+
368
+ if [ "$command" = "show" ]; then
369
+ jq '{
370
+ person_id,
371
+ display_name,
372
+ github_username,
373
+ github_id,
374
+ github_name,
375
+ email,
376
+ emails,
377
+ github_aliases,
378
+ previous_names,
379
+ identity_version
380
+ }' "$STATE"
381
+ exit 0
382
+ fi
383
+
384
+ if [ "$command" = "backfill" ]; then
385
+ backfill_people "$@"
386
+ exit $?
387
+ fi
388
+
389
+ display_args=()
390
+ email_args=()
391
+ onboard_args=()
392
+ case "$command" in
393
+ sync) ;;
394
+ onboard) onboard_args=(--onboarded) ;;
395
+ set-name)
396
+ name="${2:-}"
397
+ [ -n "$name" ] || usage
398
+ if [ "${#name}" -gt 60 ] || ! printf '%s' "$name" | grep -Eq "^[[:alnum:]À-ž .'-]+$"; then
399
+ echo '{"status":"error","error":"display name must be 1-60 name characters"}'
400
+ exit 2
401
+ fi
402
+ display_args=(--display-name "$name")
403
+ ;;
404
+ set-email)
405
+ email="${2:-}"
406
+ [ -n "$email" ] || usage
407
+ if [ "${#email}" -gt 254 ] || ! printf '%s' "$email" | grep -Eq '^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$'; then
408
+ echo '{"status":"error","error":"email must be a valid address"}'
409
+ exit 2
410
+ fi
411
+ email_args=(--email "$email")
412
+ ;;
413
+ *) usage ;;
414
+ esac
415
+
416
+ github_args=()
417
+ state_github="$(jq -r '.github_username // empty' "$STATE" 2>/dev/null)"
418
+ if command -v gh >/dev/null 2>&1; then
419
+ gh_identity="$(gh api user --jq '{login,id,name,email}' 2>/dev/null || true)"
420
+ gh_login="$(printf '%s' "$gh_identity" | jq -r '.login // empty' 2>/dev/null)"
421
+ gh_id="$(printf '%s' "$gh_identity" | jq -r '.id // empty' 2>/dev/null)"
422
+ state_github_id="$(jq -r '.github_id // empty' "$STATE" 2>/dev/null)"
423
+ gh_login_lc="$(printf '%s' "$gh_login" | tr '[:upper:]' '[:lower:]')"
424
+ state_github_lc="$(printf '%s' "$state_github" | tr '[:upper:]' '[:lower:]')"
425
+ if [ -n "$gh_login" ] && {
426
+ [ -z "$state_github" ] ||
427
+ [ "$gh_login_lc" = "$state_github_lc" ] ||
428
+ { [ -n "$gh_id" ] && [ -n "$state_github_id" ] && [ "$gh_id" = "$state_github_id" ]; }
429
+ }; then
430
+ github_args=(--github "$gh_login")
431
+ gh_email="$(printf '%s' "$gh_identity" | jq -r '.email // empty')"
432
+ [ -z "$gh_id" ] || github_args+=(--github-id "$gh_id")
433
+ [ -z "$gh_email" ] || github_args+=(--email "$gh_email")
434
+ fi
435
+ fi
436
+
437
+ if [ "$(jq -r '.onboarding_complete // false' "$STATE" 2>/dev/null)" = "true" ]; then
438
+ onboard_args=(--onboarded)
439
+ fi
440
+ identity="$(python3 "$PERSON_PY" sync-local \
441
+ "${display_args[@]}" "${github_args[@]}" "${email_args[@]}" "${onboard_args[@]}")"
442
+
443
+ if [ "$(_detect_mode)" = "local" ]; then
444
+ printf '%s' "$identity" | jq '. + {status:"synced-local",supabase:"unavailable",graph:"unavailable"}'
445
+ exit 0
446
+ fi
447
+
448
+ sync_remote_identity "$identity"