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,199 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Graceful session-end adapter: write a structured session log to memory/sessions/.
|
|
3
|
+
# Receives runtime-neutral JSON on stdin from Claude Code or Pi.
|
|
4
|
+
# Runs BEFORE transcript-archive.sh (needs obs buffer + worktree intact).
|
|
5
|
+
# Makes sessions visible in local-mode without requiring /handoff or /wrap.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Suppress all output — hook scripts must not leak to terminal
|
|
9
|
+
exec >/dev/null 2>&1
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)" || SCRIPT_DIR=""
|
|
12
|
+
if [ -z "$SCRIPT_DIR" ] || [ ! -d "$SCRIPT_DIR" ]; then
|
|
13
|
+
SCRIPT_DIR="${CLAUDE_PROJECT_DIR:-}"
|
|
14
|
+
fi
|
|
15
|
+
if [ -z "$SCRIPT_DIR" ] || [ ! -d "$SCRIPT_DIR" ]; then
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# --- Read hook input from stdin ---
|
|
20
|
+
INPUT=$(cat /dev/stdin 2>/dev/null || echo "{}")
|
|
21
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || true)
|
|
22
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null || true)
|
|
23
|
+
|
|
24
|
+
if [ -z "$SESSION_ID" ]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Sanitize SESSION_ID
|
|
29
|
+
SESSION_ID=$(echo "$SESSION_ID" | tr -cd 'a-zA-Z0-9_-')
|
|
30
|
+
if [ -z "$SESSION_ID" ] || [ ${#SESSION_ID} -gt 128 ]; then
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# --- Gate: memory must exist ---
|
|
35
|
+
if [ ! -L "$SCRIPT_DIR/memory" ] && [ ! -d "$SCRIPT_DIR/memory" ]; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# --- Read identity ---
|
|
40
|
+
STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
|
|
41
|
+
AUTHOR=""
|
|
42
|
+
DISPLAY_NAME=""
|
|
43
|
+
if [ -f "$STATE_FILE" ]; then
|
|
44
|
+
AUTHOR=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null || true)
|
|
45
|
+
DISPLAY_NAME=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null || true)
|
|
46
|
+
fi
|
|
47
|
+
AUTHOR=$(echo "$AUTHOR" | tr -cd 'a-zA-Z0-9_-')
|
|
48
|
+
if [ -z "$AUTHOR" ]; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# --- Git metadata ---
|
|
53
|
+
BRANCH=$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || echo "")
|
|
54
|
+
|
|
55
|
+
# --- Derive topic from branch name ---
|
|
56
|
+
TOPIC=""
|
|
57
|
+
if [ -n "$BRANCH" ] && [ "$BRANCH" != "develop" ] && [ "$BRANCH" != "main" ]; then
|
|
58
|
+
TOPIC=$(echo "$BRANCH" | sed 's|^dev/[^/]*/||; s|^feature/||; s|^bugfix/||' | tr '-' ' ')
|
|
59
|
+
fi
|
|
60
|
+
[ -z "$TOPIC" ] && TOPIC="session"
|
|
61
|
+
|
|
62
|
+
# --- Read baseline (written by session-start.sh) ---
|
|
63
|
+
BASELINE_FILE="/tmp/egregore-baseline-${SESSION_ID}.json"
|
|
64
|
+
BASELINE_COMMIT=""
|
|
65
|
+
BASELINE_STARTED=""
|
|
66
|
+
if [ -f "$BASELINE_FILE" ]; then
|
|
67
|
+
BASELINE_COMMIT=$(jq -r '.commit // empty' "$BASELINE_FILE" 2>/dev/null || true)
|
|
68
|
+
BASELINE_STARTED=$(jq -r '.started_at // empty' "$BASELINE_FILE" 2>/dev/null || true)
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# --- Extract timestamps from transcript ---
|
|
72
|
+
STARTED_AT=""
|
|
73
|
+
ENDED_AT=""
|
|
74
|
+
if [ -n "$TRANSCRIPT_PATH" ]; then
|
|
75
|
+
TRANSCRIPT_PATH="${TRANSCRIPT_PATH/#\~/$HOME}"
|
|
76
|
+
TRANSCRIPT_PATH=$(realpath "$TRANSCRIPT_PATH" 2>/dev/null || echo "")
|
|
77
|
+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
78
|
+
STARTED_AT=$(head -1 "$TRANSCRIPT_PATH" 2>/dev/null | jq -r '.timestamp // empty' 2>/dev/null || echo "")
|
|
79
|
+
ENDED_AT=$(tail -1 "$TRANSCRIPT_PATH" 2>/dev/null | jq -r '.timestamp // empty' 2>/dev/null || echo "")
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Use baseline start time if transcript didn't provide one
|
|
84
|
+
[ -z "$STARTED_AT" ] && STARTED_AT="$BASELINE_STARTED"
|
|
85
|
+
# Fallback to now
|
|
86
|
+
[ -z "$ENDED_AT" ] && ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
87
|
+
[ -z "$STARTED_AT" ] && STARTED_AT="$ENDED_AT"
|
|
88
|
+
|
|
89
|
+
# --- Compute duration ---
|
|
90
|
+
DURATION=""
|
|
91
|
+
DURATION_MIN=0
|
|
92
|
+
if [ -n "$STARTED_AT" ] && [ -n "$ENDED_AT" ]; then
|
|
93
|
+
S_EPOCH=$(python3 -c "from datetime import datetime; print(int(datetime.fromisoformat('$STARTED_AT'.replace('Z','+00:00')).timestamp()))" 2>/dev/null || echo "0")
|
|
94
|
+
E_EPOCH=$(python3 -c "from datetime import datetime; print(int(datetime.fromisoformat('$ENDED_AT'.replace('Z','+00:00')).timestamp()))" 2>/dev/null || echo "0")
|
|
95
|
+
if [ "$S_EPOCH" -gt 0 ] 2>/dev/null && [ "$E_EPOCH" -gt 0 ] 2>/dev/null; then
|
|
96
|
+
DURATION_MIN=$(( (E_EPOCH - S_EPOCH) / 60 ))
|
|
97
|
+
if [ "$DURATION_MIN" -lt 1 ] 2>/dev/null; then
|
|
98
|
+
DURATION="<1min"
|
|
99
|
+
elif [ "$DURATION_MIN" -lt 60 ] 2>/dev/null; then
|
|
100
|
+
DURATION="${DURATION_MIN}min"
|
|
101
|
+
else
|
|
102
|
+
HOURS=$(( DURATION_MIN / 60 ))
|
|
103
|
+
MINS=$(( DURATION_MIN % 60 ))
|
|
104
|
+
if [ "$MINS" -gt 0 ] 2>/dev/null; then
|
|
105
|
+
DURATION="${HOURS}h ${MINS}min"
|
|
106
|
+
else
|
|
107
|
+
DURATION="${HOURS}h"
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
[ -z "$DURATION" ] && DURATION="unknown"
|
|
113
|
+
|
|
114
|
+
# --- Read obs buffer for files touched ---
|
|
115
|
+
OBS_BUFFER="/tmp/egregore-obs-${SESSION_ID}.jsonl"
|
|
116
|
+
OBS_FILES=""
|
|
117
|
+
OBS_COUNT=0
|
|
118
|
+
if [ -f "$OBS_BUFFER" ] && [ -s "$OBS_BUFFER" ]; then
|
|
119
|
+
OBS_COUNT=$(wc -l < "$OBS_BUFFER" 2>/dev/null | tr -d ' ')
|
|
120
|
+
OBS_FILES=$(jq -r '.path // empty' "$OBS_BUFFER" 2>/dev/null | grep -v '^$' | grep -v '^unknown$' | sort -u | head -10 || true)
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# --- Count commits since baseline ---
|
|
124
|
+
COMMIT_COUNT=0
|
|
125
|
+
if [ -n "$BASELINE_COMMIT" ] && git -C "$SCRIPT_DIR" rev-parse "$BASELINE_COMMIT" >/dev/null 2>&1; then
|
|
126
|
+
COMMIT_COUNT=$(git -C "$SCRIPT_DIR" rev-list "${BASELINE_COMMIT}..HEAD" --count 2>/dev/null || echo "0")
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# --- Empty session guard ---
|
|
130
|
+
# Skip if: duration < 2 min AND no commits AND no obs buffer activity
|
|
131
|
+
if [ "$DURATION_MIN" -lt 2 ] 2>/dev/null && [ "$COMMIT_COUNT" -eq 0 ] 2>/dev/null && [ "$OBS_COUNT" -eq 0 ] 2>/dev/null; then
|
|
132
|
+
rm -f "$BASELINE_FILE" 2>/dev/null
|
|
133
|
+
exit 0
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# --- Deduplication: explicit /handoff or /wrap wins over baseline capture ---
|
|
137
|
+
TODAY=$(date +%d)
|
|
138
|
+
MONTH=$(date +%Y-%m)
|
|
139
|
+
if [ -n "$STARTED_AT" ]; then
|
|
140
|
+
MONTH=$(echo "$STARTED_AT" | grep -oE '^[0-9]{4}-[0-9]{2}' || date +%Y-%m)
|
|
141
|
+
TODAY=$(echo "$STARTED_AT" | grep -oE '^[0-9]{4}-[0-9]{2}-([0-9]{2})' | tail -c3 | sed 's/^-//' || date +%d)
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
AUTHOR_LC=$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]')
|
|
145
|
+
DISPLAY_LC=$(echo "$DISPLAY_NAME" | tr '[:upper:]' '[:lower:]')
|
|
146
|
+
AUTHOR_PATTERN="-${AUTHOR_LC}-"
|
|
147
|
+
[ -n "$DISPLAY_LC" ] && AUTHOR_PATTERN="${AUTHOR_PATTERN}|-${DISPLAY_LC}-"
|
|
148
|
+
for CAPTURE_DIR in \
|
|
149
|
+
"$SCRIPT_DIR/memory/handoffs/$MONTH" \
|
|
150
|
+
"$SCRIPT_DIR/memory/wraps/$MONTH"; do
|
|
151
|
+
[ -d "$CAPTURE_DIR" ] || continue
|
|
152
|
+
# Match the canonical handle or display name in today's filename.
|
|
153
|
+
EXISTING=""
|
|
154
|
+
for CANDIDATE in "$CAPTURE_DIR/${TODAY}-"*.md; do
|
|
155
|
+
[ -e "$CANDIDATE" ] || continue
|
|
156
|
+
CANDIDATE_NAME="$(basename "$CANDIDATE" | tr '[:upper:]' '[:lower:]')"
|
|
157
|
+
if printf '%s\n' "$CANDIDATE_NAME" | grep -Eq -- "$AUTHOR_PATTERN"; then
|
|
158
|
+
EXISTING="$CANDIDATE_NAME"
|
|
159
|
+
break
|
|
160
|
+
fi
|
|
161
|
+
done
|
|
162
|
+
if [ -n "$EXISTING" ]; then
|
|
163
|
+
rm -f "$BASELINE_FILE" 2>/dev/null
|
|
164
|
+
exit 0
|
|
165
|
+
fi
|
|
166
|
+
done
|
|
167
|
+
|
|
168
|
+
# --- Write through the shared capture engine -----------------------------
|
|
169
|
+
SUMMARY="${DURATION} session on ${TOPIC}; ${COMMIT_COUNT} commit(s), ${OBS_COUNT} observed action(s)."
|
|
170
|
+
{
|
|
171
|
+
if [ -n "$OBS_FILES" ]; then
|
|
172
|
+
echo "## Files"
|
|
173
|
+
echo "$OBS_FILES" | while read -r F; do
|
|
174
|
+
[ -n "$F" ] && echo "- $F"
|
|
175
|
+
done
|
|
176
|
+
echo ""
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
if [ "$COMMIT_COUNT" -gt 0 ] 2>/dev/null; then
|
|
180
|
+
echo "## Commits"
|
|
181
|
+
echo "$COMMIT_COUNT commits on ${BRANCH:-develop}"
|
|
182
|
+
echo ""
|
|
183
|
+
fi
|
|
184
|
+
} | bash "$SCRIPT_DIR/bin/capture-run.sh" \
|
|
185
|
+
--mode baseline \
|
|
186
|
+
--author "$AUTHOR" \
|
|
187
|
+
--display-name "$DISPLAY_NAME" \
|
|
188
|
+
--topic "$TOPIC" \
|
|
189
|
+
--summary "$SUMMARY" \
|
|
190
|
+
--session-id "$SESSION_ID" \
|
|
191
|
+
--branch "${BRANCH:-develop}" \
|
|
192
|
+
--date "$STARTED_AT" \
|
|
193
|
+
--duration "$DURATION" \
|
|
194
|
+
--async-push >/dev/null 2>&1 || true
|
|
195
|
+
|
|
196
|
+
# --- Clean up baseline ---
|
|
197
|
+
rm -f "$BASELINE_FILE" 2>/dev/null
|
|
198
|
+
|
|
199
|
+
exit 0
|
|
@@ -198,7 +198,7 @@ _people_file() {
|
|
|
198
198
|
if [ ! -f "$f" ]; then
|
|
199
199
|
inviter="$(jq -r '.display_name // .github_username // "admin"' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null || echo admin)"
|
|
200
200
|
today="$(date -u +%Y-%m-%d)"
|
|
201
|
-
printf -- '---\nname: %s\ngithub: %s\ninvited_by: %s\njoined: %s\n---\n' "$user" "$user" "$inviter" "$today" > "$f"
|
|
201
|
+
printf -- '---\nname: %s\nperson_id: github-login:%s\ngithub: %s\ninvited_by: %s\njoined: %s\n---\n' "$user" "$(printf '%s' "$user" | tr '[:upper:]' '[:lower:]')" "$user" "$inviter" "$today" > "$f"
|
|
202
202
|
fi
|
|
203
203
|
else
|
|
204
204
|
rm -f "$f"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": "egregore-runtime",
|
|
3
3
|
"policy": "public-safe subset derived from .syncignore; built by scripts/build-runtime-bundle.mjs",
|
|
4
|
-
"skillCount":
|
|
4
|
+
"skillCount": 58,
|
|
5
5
|
"skills": [
|
|
6
6
|
"activity",
|
|
7
7
|
"add",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"dashboard",
|
|
18
18
|
"deep-reflect",
|
|
19
19
|
"delete-user",
|
|
20
|
+
"deploy-bridge",
|
|
20
21
|
"egregore-voice",
|
|
21
22
|
"emissary",
|
|
22
23
|
"env",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"setup",
|
|
48
49
|
"summon",
|
|
49
50
|
"sync-repos",
|
|
51
|
+
"task",
|
|
50
52
|
"telegram-connect",
|
|
51
53
|
"telemetry",
|
|
52
54
|
"test",
|
|
@@ -80,6 +82,7 @@
|
|
|
80
82
|
".codex/skills/dashboard/SKILL.md",
|
|
81
83
|
".codex/skills/deep-reflect/SKILL.md",
|
|
82
84
|
".codex/skills/delete-user/SKILL.md",
|
|
85
|
+
".codex/skills/deploy-bridge/SKILL.md",
|
|
83
86
|
".codex/skills/egregore-voice/SKILL.md",
|
|
84
87
|
".codex/skills/emissary/SKILL.md",
|
|
85
88
|
".codex/skills/env/SKILL.md",
|
|
@@ -112,6 +115,7 @@
|
|
|
112
115
|
".codex/skills/setup/SKILL.md",
|
|
113
116
|
".codex/skills/summon/SKILL.md",
|
|
114
117
|
".codex/skills/sync-repos/SKILL.md",
|
|
118
|
+
".codex/skills/task/SKILL.md",
|
|
115
119
|
".codex/skills/telegram-connect/SKILL.md",
|
|
116
120
|
".codex/skills/telemetry/SKILL.md",
|
|
117
121
|
".codex/skills/test/SKILL.md",
|
|
@@ -130,6 +134,9 @@
|
|
|
130
134
|
"AGENTS.md",
|
|
131
135
|
"bin/activity-data.sh",
|
|
132
136
|
"bin/agent.sh",
|
|
137
|
+
"bin/attendant.sh",
|
|
138
|
+
"bin/capture-reconcile.sh",
|
|
139
|
+
"bin/capture-run.sh",
|
|
133
140
|
"bin/codex-install-skills.sh",
|
|
134
141
|
"bin/codex-render-spec.mjs",
|
|
135
142
|
"bin/codex-session-start.sh",
|
|
@@ -144,6 +151,12 @@
|
|
|
144
151
|
"bin/handoff-run.sh",
|
|
145
152
|
"bin/handoff-save-egregore.sh",
|
|
146
153
|
"bin/index-handoff.sh",
|
|
154
|
+
"bin/ingest-graph.sh",
|
|
155
|
+
"bin/ingest-surface.html",
|
|
156
|
+
"bin/ingest.py",
|
|
157
|
+
"bin/ingest.sh",
|
|
158
|
+
"bin/ingest_graph.py",
|
|
159
|
+
"bin/ingest_surface.py",
|
|
147
160
|
"bin/lib/artifact-id.sh",
|
|
148
161
|
"bin/lib/config.sh",
|
|
149
162
|
"bin/lib/context.sh",
|
|
@@ -154,13 +167,18 @@
|
|
|
154
167
|
"bin/lib/hash.sh",
|
|
155
168
|
"bin/lib/identity.sh",
|
|
156
169
|
"bin/lib/metrics.sh",
|
|
170
|
+
"bin/lib/noncode.sh",
|
|
157
171
|
"bin/lib/permissions.sh",
|
|
158
172
|
"bin/lib/time.sh",
|
|
159
173
|
"bin/lib/worktree-links.sh",
|
|
160
174
|
"bin/notify.sh",
|
|
175
|
+
"bin/person.py",
|
|
176
|
+
"bin/person.sh",
|
|
161
177
|
"bin/publish-artifact.sh",
|
|
162
178
|
"bin/publish-references.sh",
|
|
163
179
|
"bin/repo-state.sh",
|
|
180
|
+
"bin/session-autosave.sh",
|
|
181
|
+
"bin/session-log.sh",
|
|
164
182
|
"bin/settings.sh",
|
|
165
183
|
"bin/telemetry.sh",
|
|
166
184
|
"bin/worktree-create.sh",
|
|
@@ -48,22 +48,86 @@ function inside(child, parent) {
|
|
|
48
48
|
return rel === "" || (!!rel && !rel.startsWith("..") && !path.isAbsolute(rel));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
function nearestExistingDirectory(candidate) {
|
|
52
|
+
let cursor = path.resolve(candidate);
|
|
53
|
+
try {
|
|
54
|
+
if (fs.existsSync(cursor) && !fs.statSync(cursor).isDirectory()) cursor = path.dirname(cursor);
|
|
55
|
+
} catch {}
|
|
56
|
+
while (!fs.existsSync(cursor)) {
|
|
57
|
+
const parent = path.dirname(cursor);
|
|
58
|
+
if (parent === cursor) break;
|
|
59
|
+
cursor = parent;
|
|
60
|
+
}
|
|
61
|
+
return cursor;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function gitContext(candidate) {
|
|
65
|
+
if (!candidate) return null;
|
|
66
|
+
const probe = nearestExistingDirectory(candidate);
|
|
67
|
+
const top = run("git", ["-C", probe, "rev-parse", "--show-toplevel"], probe);
|
|
68
|
+
if (!top) return null;
|
|
69
|
+
const commonRaw = run("git", ["-C", top, "rev-parse", "--git-common-dir"], top);
|
|
70
|
+
const common = commonRaw
|
|
71
|
+
? realish(path.isAbsolute(commonRaw) ? commonRaw : path.join(top, commonRaw))
|
|
72
|
+
: realish(path.join(top, ".git"));
|
|
73
|
+
return { top: realish(top), common };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function shellRetarget(command) {
|
|
77
|
+
// Only retarget a single operation. Mixed command chains are evaluated from
|
|
78
|
+
// the hub context so one external `git -C` cannot mask a later project write.
|
|
79
|
+
if (shellSegments(command).length !== 1) return "";
|
|
80
|
+
const gitC = String(command || "").match(/(?:^|[;&|]\s*)git\s+-C\s+("[^"]*"|'[^']*'|[^\s;&|]+)/);
|
|
81
|
+
const cdAnd = String(command || "").match(/^\s*cd\s+("[^"]*"|'[^']*'|[^\s;&|]+)\s*&&/);
|
|
82
|
+
const raw = (gitC && gitC[1]) || (cdAnd && cdAnd[1]) || "";
|
|
83
|
+
const target = raw.replace(/^['"]|['"]$/g, "");
|
|
84
|
+
return /[$`*?]/.test(target) ? "" : target;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function operationCandidate(input, baseDir) {
|
|
88
|
+
const toolName = input.tool_name || "";
|
|
89
|
+
const toolInput = input.tool_input || {};
|
|
90
|
+
const explicitWorkdir = toolInput.workdir || toolInput.cwd || "";
|
|
91
|
+
let candidate = explicitWorkdir
|
|
92
|
+
? (path.isAbsolute(explicitWorkdir) ? explicitWorkdir : path.join(baseDir, explicitWorkdir))
|
|
93
|
+
: baseDir;
|
|
59
94
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
95
|
+
if (toolName === "Edit" || toolName === "Write") {
|
|
96
|
+
const target = toolInput.file_path || toolInput.path || "";
|
|
97
|
+
if (target) candidate = path.isAbsolute(target) ? target : path.join(candidate, target);
|
|
98
|
+
} else if (toolName === "Bash") {
|
|
99
|
+
const target = shellRetarget(toolInput.command || "");
|
|
100
|
+
if (target) candidate = path.isAbsolute(target) ? target : path.join(candidate, target);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return candidate;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function findProjectContext(input) {
|
|
107
|
+
// The launcher keeps EGREGORE_CODEX_PROJECT_DIR on the hub checkout while
|
|
108
|
+
// individual tool calls can target a task worktree, memory, or a managed
|
|
109
|
+
// repo. Compare Git common dirs: a task worktree shares the hub common dir
|
|
110
|
+
// and must use its own branch; memory/managed repos are separate and exempt.
|
|
111
|
+
const fallback = input.cwd || process.cwd();
|
|
112
|
+
const hubSeed = process.env.EGREGORE_CODEX_PROJECT_DIR || fallback;
|
|
113
|
+
const hub = gitContext(hubSeed);
|
|
114
|
+
const baseDir = input.cwd || hub?.top || fallback;
|
|
115
|
+
const candidate = operationCandidate(input, baseDir);
|
|
116
|
+
const operation = gitContext(candidate);
|
|
117
|
+
|
|
118
|
+
if (!hub) return { projectDir: operation?.top || realish(baseDir), external: false };
|
|
119
|
+
if (operation && operation.common === hub.common) {
|
|
120
|
+
return { projectDir: operation.top, external: false };
|
|
121
|
+
}
|
|
122
|
+
if (operation && operation.common !== hub.common) {
|
|
123
|
+
return { projectDir: hub.top, external: true };
|
|
64
124
|
}
|
|
65
125
|
|
|
66
|
-
|
|
126
|
+
const resolvedCandidate = realish(candidate);
|
|
127
|
+
if (!inside(resolvedCandidate, hub.top)) {
|
|
128
|
+
return { projectDir: hub.top, external: true };
|
|
129
|
+
}
|
|
130
|
+
return { projectDir: hub.top, external: false };
|
|
67
131
|
}
|
|
68
132
|
|
|
69
133
|
function currentBranch(projectDir) {
|
|
@@ -95,7 +159,7 @@ function isExemptPath(rawPath, projectDir, memoryDir) {
|
|
|
95
159
|
if (inside(resolved, target)) return true;
|
|
96
160
|
}
|
|
97
161
|
|
|
98
|
-
for (const file of [".egregore-state.json", ".env"]) {
|
|
162
|
+
for (const file of [".egregore-state.json", ".egregore-branch-consent", ".env"]) {
|
|
99
163
|
if (resolved === realish(path.join(projectDir, file))) return true;
|
|
100
164
|
}
|
|
101
165
|
|
|
@@ -133,12 +197,13 @@ function commandTargetsOtherRepo(command, projectDir, memoryDir) {
|
|
|
133
197
|
}
|
|
134
198
|
|
|
135
199
|
function isBranchSetupCommand(command) {
|
|
200
|
+
if (shellSegments(command).length !== 1) return false;
|
|
136
201
|
return (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
202
|
+
/^\s*bin\/agent\.sh\s+branch\b/.test(command) ||
|
|
203
|
+
/^\s*git\s+(checkout|switch)\s+(-b|-c)\b/.test(command) ||
|
|
204
|
+
/^\s*git\s+branch\s+(dev|feature|bugfix)\//.test(command) ||
|
|
205
|
+
/^\s*git\s+worktree\s+add\b/.test(command) ||
|
|
206
|
+
/^\s*bin\/worktree\.sh\s+setup\b/.test(command)
|
|
142
207
|
);
|
|
143
208
|
}
|
|
144
209
|
|
|
@@ -149,7 +214,134 @@ function stagedOnlyFrameworkPaths(projectDir) {
|
|
|
149
214
|
return files.length > 0 && files.every((file) => /^(bin\/|\.claude\/|\.codex\/|CLAUDE\.md$|AGENTS\.md$|skills\/)/.test(file));
|
|
150
215
|
}
|
|
151
216
|
|
|
217
|
+
function stripQuotedSegments(command) {
|
|
218
|
+
let out = "";
|
|
219
|
+
let quote = null;
|
|
220
|
+
for (let i = 0; i < String(command || "").length; i++) {
|
|
221
|
+
const ch = command[i];
|
|
222
|
+
if (quote) {
|
|
223
|
+
if (quote === '"' && ch === "\\") i++;
|
|
224
|
+
else if (ch === quote) quote = null;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (ch === "'" || ch === '"') {
|
|
228
|
+
quote = ch;
|
|
229
|
+
out += "x";
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
out += ch;
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function maskQuotedOperators(command) {
|
|
238
|
+
let out = "";
|
|
239
|
+
let quote = null;
|
|
240
|
+
for (let i = 0; i < String(command || "").length; i++) {
|
|
241
|
+
const ch = command[i];
|
|
242
|
+
if (quote) {
|
|
243
|
+
if (quote === '"' && ch === "\\") {
|
|
244
|
+
out += ch;
|
|
245
|
+
if (i + 1 < command.length) out += command[++i];
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (ch === quote) quote = null;
|
|
249
|
+
out += /[><|;&]/.test(ch) ? " " : ch;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (ch === "'" || ch === '"') quote = ch;
|
|
253
|
+
out += ch;
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function shellSegments(command) {
|
|
259
|
+
const segments = [];
|
|
260
|
+
let current = "";
|
|
261
|
+
let quote = null;
|
|
262
|
+
for (let i = 0; i < String(command || "").length; i++) {
|
|
263
|
+
const ch = command[i];
|
|
264
|
+
if (quote) {
|
|
265
|
+
current += ch;
|
|
266
|
+
if (quote === '"' && ch === "\\" && i + 1 < command.length) current += command[++i];
|
|
267
|
+
else if (ch === quote) quote = null;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (ch === "'" || ch === '"') {
|
|
271
|
+
quote = ch;
|
|
272
|
+
current += ch;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (ch === ";" || ch === "|" || ch === "&" || ch === "\n") {
|
|
276
|
+
if (current.trim()) segments.push(current.trim());
|
|
277
|
+
current = "";
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
current += ch;
|
|
281
|
+
}
|
|
282
|
+
if (current.trim()) segments.push(current.trim());
|
|
283
|
+
return segments;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function shellWords(segment) {
|
|
287
|
+
const words = [];
|
|
288
|
+
let current = "";
|
|
289
|
+
let quote = null;
|
|
290
|
+
for (let i = 0; i < String(segment || "").length; i++) {
|
|
291
|
+
const ch = segment[i];
|
|
292
|
+
if (quote) {
|
|
293
|
+
if (quote === '"' && ch === "\\" && i + 1 < segment.length) current += segment[++i];
|
|
294
|
+
else if (ch === quote) quote = null;
|
|
295
|
+
else current += ch;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (ch === "'" || ch === '"') {
|
|
299
|
+
quote = ch;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (/\s/.test(ch)) {
|
|
303
|
+
if (current) words.push(current);
|
|
304
|
+
current = "";
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (ch === "\\") {
|
|
308
|
+
if (i + 1 < segment.length) current += segment[++i];
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
current += ch;
|
|
312
|
+
}
|
|
313
|
+
if (current) words.push(current);
|
|
314
|
+
return words;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function filesystemMutationStatus(command, projectDir, memoryDir) {
|
|
318
|
+
const commands = new Set(["rm", "mv", "cp", "mkdir", "rmdir", "touch", "chmod", "chown", "ln", "tee"]);
|
|
319
|
+
let unsafe = false;
|
|
320
|
+
|
|
321
|
+
for (const segment of shellSegments(command)) {
|
|
322
|
+
const words = shellWords(segment);
|
|
323
|
+
while (words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0])) words.shift();
|
|
324
|
+
while (words[0] === "sudo" || words[0] === "command" || words[0] === "env") words.shift();
|
|
325
|
+
if (!words.length) continue;
|
|
326
|
+
|
|
327
|
+
const name = path.basename(words.shift());
|
|
328
|
+
if (!commands.has(name)) continue;
|
|
329
|
+
|
|
330
|
+
const args = words.filter((word) => !word.startsWith("-"));
|
|
331
|
+
let targets = args;
|
|
332
|
+
if (name === "cp" || name === "ln") targets = args.length ? [args[args.length - 1]] : [];
|
|
333
|
+
if (name === "chmod" || name === "chown") targets = args.slice(1);
|
|
334
|
+
|
|
335
|
+
if (!targets.length || targets.some((target) =>
|
|
336
|
+
/[$`*?{}]/.test(target) || !isExemptPath(target, projectDir, memoryDir)
|
|
337
|
+
)) unsafe = true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { unsafe };
|
|
341
|
+
}
|
|
342
|
+
|
|
152
343
|
function redirectLooksMutating(command, projectDir, memoryDir) {
|
|
344
|
+
const visible = maskQuotedOperators(command);
|
|
153
345
|
const redirectPatterns = [
|
|
154
346
|
/(?:^|[\s;|&])(?:\d*)>>?\s*([^&\s;|]+)/g,
|
|
155
347
|
/\|\s*tee\s+(?:-a\s+)?([^&\s;|]+)/g,
|
|
@@ -157,7 +349,7 @@ function redirectLooksMutating(command, projectDir, memoryDir) {
|
|
|
157
349
|
|
|
158
350
|
for (const pattern of redirectPatterns) {
|
|
159
351
|
let match;
|
|
160
|
-
while ((match = pattern.exec(
|
|
352
|
+
while ((match = pattern.exec(visible))) {
|
|
161
353
|
const target = String(match[1] || "").replace(/^["']|["']$/g, "");
|
|
162
354
|
if (!target || target === "/dev/null" || target.startsWith("$")) continue;
|
|
163
355
|
if (!isExemptPath(target, projectDir, memoryDir)) return true;
|
|
@@ -168,18 +360,31 @@ function redirectLooksMutating(command, projectDir, memoryDir) {
|
|
|
168
360
|
|
|
169
361
|
function shellLooksMutating(command, projectDir, memoryDir) {
|
|
170
362
|
if (!command.trim()) return false;
|
|
171
|
-
|
|
363
|
+
const visible = stripQuotedSegments(command);
|
|
364
|
+
if (isBranchSetupCommand(visible)) return false;
|
|
172
365
|
|
|
173
366
|
const mutatingPatterns = [
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
367
|
+
/(?:^|[;&|(\n]\s*)(?:sudo\s+)?git(?:\s+-C\s+\S+)?\s+(add|commit|push|rm|mv|reset|restore|checkout\s+--|clean|rebase|merge|cherry-pick)\b/,
|
|
368
|
+
/(?:^|[;&|(\n]\s*)(?:sudo\s+)?apply_patch\b/,
|
|
369
|
+
/(?:^|[;&|(\n]\s*)(?:sudo\s+)?(sed|perl)\b[^;&|]*\s-i(\s|$)/,
|
|
370
|
+
/(?:^|[;&|(\n]\s*)(?:sudo\s+)?(npm|pnpm|yarn|bun)\s+(install|add|remove|update|dedupe|ci)\b/,
|
|
371
|
+
/(?:^|[;&|(\n]\s*)(?:sudo\s+)?(go\s+mod\s+tidy|cargo\s+(fmt|fix|update)|rustfmt|prettier\s+--write|eslint\s+--fix)\b/,
|
|
179
372
|
];
|
|
180
373
|
|
|
181
|
-
|
|
182
|
-
|
|
374
|
+
const gitCommit = /(?:^|[;&|(\n]\s*)(?:sudo\s+)?git(?:\s+-C\s+\S+)?\s+commit\b/.test(visible);
|
|
375
|
+
if (gitCommit && stagedOnlyFrameworkPaths(projectDir)) return false;
|
|
376
|
+
|
|
377
|
+
const filesystem = filesystemMutationStatus(command, projectDir, memoryDir);
|
|
378
|
+
return mutatingPatterns.some((pattern) => pattern.test(visible)) ||
|
|
379
|
+
filesystem.unsafe ||
|
|
380
|
+
redirectLooksMutating(command, projectDir, memoryDir);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function isConsentCommand(command, branch) {
|
|
384
|
+
const escaped = branch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
385
|
+
return new RegExp(
|
|
386
|
+
`^\\s*echo\\s+['"]?${escaped}['"]?\\s*>\\s*(?:\\./)?\\.egregore-branch-consent\\s*$`
|
|
387
|
+
).test(String(command || ""));
|
|
183
388
|
}
|
|
184
389
|
|
|
185
390
|
function deny(reason) {
|
|
@@ -194,7 +399,9 @@ function deny(reason) {
|
|
|
194
399
|
|
|
195
400
|
function main() {
|
|
196
401
|
const input = parseJson(readStdin());
|
|
197
|
-
const
|
|
402
|
+
const context = findProjectContext(input);
|
|
403
|
+
if (context.external) return;
|
|
404
|
+
const projectDir = context.projectDir;
|
|
198
405
|
const branch = currentBranch(projectDir);
|
|
199
406
|
if (!isProtectedBranch(branch)) return;
|
|
200
407
|
|
|
@@ -211,11 +418,11 @@ function main() {
|
|
|
211
418
|
const memoryDir = realish(path.join(projectDir, "memory"));
|
|
212
419
|
const author = currentAuthor(projectDir);
|
|
213
420
|
const reason = [
|
|
214
|
-
`Protected branch (${branch}).
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`
|
|
218
|
-
"
|
|
421
|
+
`Protected branch (${branch}). Move project work to a task branch before writing.`,
|
|
422
|
+
`If the work topic is clear, run bin/agent.sh branch --topic "<work topic>" automatically (prefix dev/${author}/...), continue in the printed worktree, and briefly tell the user.`,
|
|
423
|
+
"Ask only when the work topic is genuinely ambiguous; ask for the topic, not for routine Git permission.",
|
|
424
|
+
`Only when the user explicitly requested writing on ${branch}, record consent with: echo '${branch}' > .egregore-branch-consent`,
|
|
425
|
+
"Memory, framework-runtime, and managed-repo writes are exempt and should not trigger this guard.",
|
|
219
426
|
].join("\n");
|
|
220
427
|
|
|
221
428
|
if (toolName === "apply_patch") {
|
|
@@ -234,6 +441,7 @@ function main() {
|
|
|
234
441
|
|
|
235
442
|
if (toolName === "Bash") {
|
|
236
443
|
const command = String(toolInput.command || "");
|
|
444
|
+
if (isConsentCommand(command, branch)) return;
|
|
237
445
|
if (commandTargetsOtherRepo(command, projectDir, memoryDir)) return;
|
|
238
446
|
if (shellLooksMutating(command, projectDir, memoryDir)) deny(reason);
|
|
239
447
|
}
|