create-egregore 0.19.8 → 0.19.11
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.
- package/lib/local.js +11 -1
- package/lib/setup.js +33 -0
- package/package.json +1 -1
- package/runtime/codex/.codex/hooks/branch-guard.js +242 -34
- package/runtime/codex/.codex/skills/activity/SKILL.md +18 -6
- package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +90 -55
- package/runtime/codex/.codex/skills/deploy-bridge/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/handoff/SKILL.md +6 -2
- package/runtime/codex/.codex/skills/save/SKILL.md +21 -3
- package/runtime/codex/.codex/skills/scroll/SKILL.md +54 -3
- package/runtime/codex/.codex/skills/task/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/tui-design/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/wrap/SKILL.md +4 -1
- package/runtime/codex/.codex/spec-manifest.json +15 -3
- package/runtime/codex/AGENTS.md +45 -9
- package/runtime/codex/bin/activity-data.sh +1 -1
- package/runtime/codex/bin/agent.sh +66 -23
- package/runtime/codex/bin/attendant.sh +525 -0
- package/runtime/codex/bin/capture-reconcile.sh +67 -0
- package/runtime/codex/bin/capture-run.sh +341 -0
- package/runtime/codex/bin/codex-render-spec.mjs +15 -4
- package/runtime/codex/bin/codex-session-start.sh +45 -3
- package/runtime/codex/bin/codex-skill-render.mjs +23 -4
- package/runtime/codex/bin/graph-op.sh +719 -49
- package/runtime/codex/bin/graph-wal.sh +3 -2
- package/runtime/codex/bin/handoff-run.sh +72 -0
- package/runtime/codex/bin/handoff-save-egregore.sh +51 -11
- package/runtime/codex/bin/index-handoff.sh +26 -17
- package/runtime/codex/bin/ingest-graph.sh +80 -0
- package/runtime/codex/bin/ingest-surface.html +429 -0
- package/runtime/codex/bin/ingest.py +614 -0
- package/runtime/codex/bin/ingest.sh +231 -0
- package/runtime/codex/bin/ingest_graph.py +776 -0
- package/runtime/codex/bin/ingest_surface.py +464 -0
- package/runtime/codex/bin/lib/git-sync.sh +2 -6
- package/runtime/codex/bin/lib/greeting.sh +157 -22
- package/runtime/codex/bin/lib/identity.sh +50 -29
- package/runtime/codex/bin/lib/noncode.sh +31 -0
- package/runtime/codex/bin/person.py +610 -0
- package/runtime/codex/bin/person.sh +448 -0
- package/runtime/codex/bin/session-autosave.sh +184 -0
- package/runtime/codex/bin/session-log.sh +199 -0
- package/runtime/codex/bin/settings.sh +1 -1
- package/runtime/codex/manifest.json +19 -1
- package/runtime/pi/.codex/hooks/branch-guard.js +242 -34
- package/runtime/pi/.codex/skills/activity/SKILL.md +18 -6
- package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +90 -55
- package/runtime/pi/.codex/skills/deploy-bridge/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/handoff/SKILL.md +6 -2
- package/runtime/pi/.codex/skills/save/SKILL.md +21 -3
- package/runtime/pi/.codex/skills/scroll/SKILL.md +54 -3
- package/runtime/pi/.codex/skills/task/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/tui-design/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/wrap/SKILL.md +4 -1
- package/runtime/pi/.codex/spec-manifest.json +15 -3
- package/runtime/pi/.pi/APPEND_SYSTEM.md +41 -5
- package/runtime/pi/.pi/extensions/egregore.ts +98 -8
- package/runtime/pi/.pi/spec-manifest.json +2 -2
- package/runtime/pi/AGENTS.md +45 -9
- package/runtime/pi/bin/activity-data.sh +1 -1
- package/runtime/pi/bin/agent.sh +66 -23
- package/runtime/pi/bin/attendant.sh +525 -0
- package/runtime/pi/bin/capture-reconcile.sh +67 -0
- package/runtime/pi/bin/capture-run.sh +341 -0
- package/runtime/pi/bin/codex-render-spec.mjs +15 -4
- package/runtime/pi/bin/codex-session-start.sh +45 -3
- package/runtime/pi/bin/codex-skill-render.mjs +23 -4
- package/runtime/pi/bin/graph-op.sh +719 -49
- package/runtime/pi/bin/graph-wal.sh +3 -2
- package/runtime/pi/bin/handoff-run.sh +72 -0
- package/runtime/pi/bin/handoff-save-egregore.sh +51 -11
- package/runtime/pi/bin/index-handoff.sh +26 -17
- package/runtime/pi/bin/ingest-graph.sh +80 -0
- package/runtime/pi/bin/ingest-surface.html +429 -0
- package/runtime/pi/bin/ingest.py +614 -0
- package/runtime/pi/bin/ingest.sh +231 -0
- package/runtime/pi/bin/ingest_graph.py +776 -0
- package/runtime/pi/bin/ingest_surface.py +464 -0
- package/runtime/pi/bin/lib/git-sync.sh +2 -6
- package/runtime/pi/bin/lib/greeting.sh +157 -22
- package/runtime/pi/bin/lib/identity.sh +50 -29
- package/runtime/pi/bin/lib/noncode.sh +31 -0
- package/runtime/pi/bin/person.py +610 -0
- package/runtime/pi/bin/person.sh +448 -0
- package/runtime/pi/bin/pi-branch-policy.mjs +164 -4
- package/runtime/pi/bin/pi-product-workflows.mjs +31 -2
- package/runtime/pi/bin/publish-artifact.sh +14 -0
- package/runtime/pi/bin/session-autosave.sh +184 -0
- package/runtime/pi/bin/session-log.sh +199 -0
- package/runtime/pi/bin/settings.sh +274 -0
- package/runtime/pi/manifest.json +20 -1
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
#!/bin/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"
|
|
@@ -1,10 +1,170 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
const MUTATING_COMMANDS = /(^|[;&|]\s*)(?:sudo\s+)?(rm|mv|cp|mkdir|rmdir|touch|chmod|chown|install|tee)\b/;
|
|
4
|
+
const MUTATING_GIT = /(^|[;&|]\s*)(?:sudo\s+)?git\s+(add|commit|push|merge|rebase|cherry-pick|tag|rm|mv)\b/;
|
|
5
|
+
const MUTATING_PACKAGES = /(^|[;&|]\s*)(?:sudo\s+)?(npm|pnpm|yarn|bun)\s+(install|add|remove|uninstall|update)\b/;
|
|
6
|
+
|
|
7
|
+
// A `>` only acts as a shell redirect when it sits OUTSIDE quotes. Quoted
|
|
8
|
+
// `>` shows up constantly in read-only commands — Cypher comparisons
|
|
9
|
+
// (`WHERE x >= y`), JS/Python formatters (`f"{a} -> {b}"`), jq filters —
|
|
10
|
+
// so the redirect test runs on a quote-stripped copy of the command.
|
|
11
|
+
// Quoted segments collapse to a placeholder char so a redirect with a
|
|
12
|
+
// quoted TARGET (`echo hi > "/tmp/x"`) is still detected.
|
|
13
|
+
function stripQuotedSegments(command) {
|
|
14
|
+
let out = "";
|
|
15
|
+
let quote = null;
|
|
16
|
+
for (let i = 0; i < command.length; i++) {
|
|
17
|
+
const ch = command[i];
|
|
18
|
+
if (quote) {
|
|
19
|
+
if (quote === '"' && ch === "\\") i++; // skip escaped char inside double quotes
|
|
20
|
+
else if (ch === quote) quote = null;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (ch === "'" || ch === '"') { quote = ch; out += "x"; continue; }
|
|
24
|
+
out += ch;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function maskQuotedOperators(command) {
|
|
30
|
+
let out = "";
|
|
31
|
+
let quote = null;
|
|
32
|
+
for (let i = 0; i < command.length; i++) {
|
|
33
|
+
const ch = command[i];
|
|
34
|
+
if (quote) {
|
|
35
|
+
if (quote === '"' && ch === "\\" && i + 1 < command.length) {
|
|
36
|
+
out += ch + command[++i];
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (ch === quote) quote = null;
|
|
40
|
+
out += /[><|;&]/.test(ch) ? " " : ch;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (ch === "'" || ch === '"') quote = ch;
|
|
44
|
+
out += ch;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function shellSegments(command) {
|
|
50
|
+
const segments = [];
|
|
51
|
+
let current = "";
|
|
52
|
+
let quote = null;
|
|
53
|
+
for (let i = 0; i < command.length; i++) {
|
|
54
|
+
const ch = command[i];
|
|
55
|
+
if (quote) {
|
|
56
|
+
current += ch;
|
|
57
|
+
if (quote === '"' && ch === "\\" && i + 1 < command.length) current += command[++i];
|
|
58
|
+
else if (ch === quote) quote = null;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === "'" || ch === '"') {
|
|
62
|
+
quote = ch;
|
|
63
|
+
current += ch;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (ch === ";" || ch === "|" || ch === "&" || ch === "\n") {
|
|
67
|
+
if (current.trim()) segments.push(current.trim());
|
|
68
|
+
current = "";
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
current += ch;
|
|
72
|
+
}
|
|
73
|
+
if (current.trim()) segments.push(current.trim());
|
|
74
|
+
return segments;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function shellWords(segment) {
|
|
78
|
+
const words = [];
|
|
79
|
+
let current = "";
|
|
80
|
+
let quote = null;
|
|
81
|
+
for (let i = 0; i < segment.length; i++) {
|
|
82
|
+
const ch = segment[i];
|
|
83
|
+
if (quote) {
|
|
84
|
+
if (quote === '"' && ch === "\\" && i + 1 < segment.length) current += segment[++i];
|
|
85
|
+
else if (ch === quote) quote = null;
|
|
86
|
+
else current += ch;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (ch === "'" || ch === '"') {
|
|
90
|
+
quote = ch;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (/\s/.test(ch)) {
|
|
94
|
+
if (current) words.push(current);
|
|
95
|
+
current = "";
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ch === "\\" && i + 1 < segment.length) current += segment[++i];
|
|
99
|
+
else current += ch;
|
|
100
|
+
}
|
|
101
|
+
if (current) words.push(current);
|
|
102
|
+
return words;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isRuntimeStatePath(target, projectDir) {
|
|
106
|
+
if (!target || /[$`*?{}]/.test(target)) return false;
|
|
107
|
+
const absolute = isAbsolute(target) ? resolve(target) : resolve(projectDir, target);
|
|
108
|
+
const rel = relative(resolve(projectDir), absolute);
|
|
109
|
+
if (rel.startsWith("..") || isAbsolute(rel)) return true;
|
|
110
|
+
if (rel === ".claude/worktrees" || rel.startsWith(".claude/worktrees/")) return false;
|
|
111
|
+
return rel === "memory" || rel.startsWith("memory/") ||
|
|
112
|
+
rel === ".claude" || rel.startsWith(".claude/") ||
|
|
113
|
+
rel === ".codex" || rel.startsWith(".codex/") ||
|
|
114
|
+
rel === ".pi" || rel.startsWith(".pi/") ||
|
|
115
|
+
rel === ".egregore-state.json" ||
|
|
116
|
+
rel === ".egregore-branch-consent" ||
|
|
117
|
+
rel === ".env";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function mutatesOnlyRuntimeState(command, projectDir) {
|
|
121
|
+
if (!isMutatingBash(command)) return false;
|
|
122
|
+
const visible = stripQuotedSegments(String(command));
|
|
123
|
+
if (MUTATING_GIT.test(visible) || MUTATING_PACKAGES.test(visible) ||
|
|
124
|
+
/\bsed\s+[^\n]*\s-i(?:\s|$)/.test(visible)) return false;
|
|
125
|
+
|
|
126
|
+
let foundTarget = false;
|
|
127
|
+
const pathCommands = new Set(["rm", "mv", "cp", "mkdir", "rmdir", "touch", "chmod", "chown", "install", "tee"]);
|
|
128
|
+
for (const segment of shellSegments(String(command))) {
|
|
129
|
+
const words = shellWords(segment);
|
|
130
|
+
while (words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0])) words.shift();
|
|
131
|
+
while (words[0] === "sudo" || words[0] === "command" || words[0] === "env") words.shift();
|
|
132
|
+
if (!words.length) continue;
|
|
133
|
+
const name = words.shift().split("/").pop();
|
|
134
|
+
if (!pathCommands.has(name)) continue;
|
|
135
|
+
|
|
136
|
+
const args = words.filter((word) => !word.startsWith("-"));
|
|
137
|
+
let targets = args;
|
|
138
|
+
if (name === "cp" || name === "install") targets = args.length ? [args[args.length - 1]] : [];
|
|
139
|
+
if (name === "chmod" || name === "chown") targets = args.slice(1);
|
|
140
|
+
if (!targets.length || targets.some((target) => !isRuntimeStatePath(target, projectDir))) return false;
|
|
141
|
+
foundTarget = true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const redirectSource = maskQuotedOperators(String(command));
|
|
145
|
+
const redirects = /(?:^|[\s;|&])(?:\d*)>>?\s*([^&\s;|]+)/g;
|
|
146
|
+
let redirect;
|
|
147
|
+
while ((redirect = redirects.exec(redirectSource))) {
|
|
148
|
+
const target = String(redirect[1] || "").replace(/^["']|["']$/g, "");
|
|
149
|
+
if (!target || target === "/dev/null") continue;
|
|
150
|
+
if (!isRuntimeStatePath(target, projectDir)) return false;
|
|
151
|
+
foundTarget = true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const tees = /\|\s*tee\s+(?:-a\s+)?([^&\s;|]+)/g;
|
|
155
|
+
let tee;
|
|
156
|
+
while ((tee = tees.exec(redirectSource))) {
|
|
157
|
+
const target = String(tee[1] || "").replace(/^["']|["']$/g, "");
|
|
158
|
+
if (!isRuntimeStatePath(target, projectDir)) return false;
|
|
159
|
+
foundTarget = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return foundTarget;
|
|
163
|
+
}
|
|
4
164
|
|
|
5
165
|
export function isMutatingBash(command) {
|
|
6
166
|
if (!command) return false;
|
|
7
|
-
const meaningfulRedirects = String(command)
|
|
167
|
+
const meaningfulRedirects = stripQuotedSegments(String(command))
|
|
8
168
|
.replace(/(?:^|\s)\d*>>?\s*\/dev\/null\b/g, " ")
|
|
9
169
|
.replace(/(?:^|\s)\d*>\s*&\d+\b/g, " ");
|
|
10
170
|
return MUTATING_COMMANDS.test(command) ||
|
|
@@ -235,11 +235,11 @@ export async function createHandoff(pi, root, params, signal) {
|
|
|
235
235
|
writeFileSync(body, buildHandoffDocument(params, author));
|
|
236
236
|
|
|
237
237
|
try {
|
|
238
|
-
const shell = 'tmp="$1"; body="$2"; shift
|
|
238
|
+
const shell = 'tmp="$1"; body="$2"; runner="$3"; shift 3; TMPDIR="$tmp" bash "$runner" --mode addressed "$@" < "$body"';
|
|
239
239
|
await checkedExec(
|
|
240
240
|
pi,
|
|
241
241
|
"bash",
|
|
242
|
-
["-c", shell, "egregore-pi-handoff", temp, body, ...runnerArgs],
|
|
242
|
+
["-c", shell, "egregore-pi-handoff", temp, body, join(root, "bin", "capture-run.sh"), ...runnerArgs],
|
|
243
243
|
{ signal, timeout: 180000 },
|
|
244
244
|
"Handoff",
|
|
245
245
|
);
|
|
@@ -263,6 +263,35 @@ export async function createHandoff(pi, root, params, signal) {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
export async function captureSessionEnd(pi, root, sessionFile, signal) {
|
|
267
|
+
const sessionIdFile = join(root, ".egregore-session-id");
|
|
268
|
+
if (!existsSync(sessionIdFile)) return false;
|
|
269
|
+
const sessionId = cleanArgs(readFileSync(sessionIdFile, "utf8"));
|
|
270
|
+
if (!sessionId) return false;
|
|
271
|
+
|
|
272
|
+
const temp = mkdtempSync(join(tmpdir(), "egregore-pi-session-end-"));
|
|
273
|
+
const input = join(temp, "session-end.json");
|
|
274
|
+
writeFileSync(input, JSON.stringify({
|
|
275
|
+
session_id: sessionId,
|
|
276
|
+
transcript_path: cleanArgs(sessionFile),
|
|
277
|
+
runtime: "pi",
|
|
278
|
+
}));
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const shell = 'bash "$1" < "$2"';
|
|
282
|
+
await checkedExec(
|
|
283
|
+
pi,
|
|
284
|
+
"bash",
|
|
285
|
+
["-c", shell, "egregore-pi-session-end", join(root, "bin", "session-log.sh"), input],
|
|
286
|
+
{ signal, timeout: 10000 },
|
|
287
|
+
"Session capture",
|
|
288
|
+
);
|
|
289
|
+
return true;
|
|
290
|
+
} finally {
|
|
291
|
+
rmSync(temp, { recursive: true, force: true });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
266
295
|
export async function publishScroll(pi, root, params, signal) {
|
|
267
296
|
const source = resolve(root, String(params.source || "").replace(/^@/, ""));
|
|
268
297
|
if (!existsSync(source)) throw new Error(`Scroll source not found: ${relative(root, source)}`);
|