Skip to content

Commit 1bd3807

Browse files
heman4tclaude
andcommitted
fix(codegraph): index check requires the db file; uninstall prunes dangling Gemini fileName refs
Tier-3 polish, follow-up to the non-interactive/PATH/gitignore fixes. 1. codegraph-session-check.sh: treat a project as initialized only when .codegraph/ actually contains an index db (*.db), not merely the dir. An aborted/half-finished `codegraph init` leaves a bare .codegraph/ dir that previously made once-mode go silent on a broken index. The check stays a pure filesystem test (no per-session `codegraph status`), preserving once-mode's fast no-op; deep corruption stays status/--always's job. This aligns the hook with the policy's 'indexed = .codegraph/ exists AND status succeeds'. 2. uninstall.sh: reverse install's Gemini context.fileName additions, but ONLY for entries whose instruction file no longer exists after uninstall (a dangling reference). A GEMINI.md/AGENTS.md the user still has is left referenced, so config they rely on is never removed. (Previously uninstall left every fileName entry.) AGENTS.md is pruned in project scope only; globally that entry means 'read each project's AGENTS.md', which the global install does not own. Regenerated agent-primer.sh; added smoke coverage (half-built index nudges; dangling refs pruned; a surviving file's ref kept). 69 smoke checks pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1a11b7a commit 1bd3807

4 files changed

Lines changed: 165 additions & 9 deletions

File tree

agent-primer.sh

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,20 @@ emit() {
106106
esac
107107
}
108108
109+
# An initialized index = the .codegraph/ dir AND at least one SQLite db file inside it.
110+
# We check for the db file rather than running `codegraph status` on purpose: a bare
111+
# .codegraph/ directory left behind by an aborted/half-finished `init` would otherwise
112+
# look "set up" and make once-mode go silent on a broken index. Running `codegraph status`
113+
# every session is exactly the cost once-mode removes, so this cheap filesystem check is the
114+
# right middle ground; deep corruption is still `codegraph status`/--always's job.
115+
index_initialized() {
116+
[ -d "$PROJECT_DIR/.codegraph" ] || return 1
117+
for _db in "$PROJECT_DIR"/.codegraph/*.db; do
118+
[ -f "$_db" ] && return 0
119+
done
120+
return 1
121+
}
122+
109123
# --- checks ----------------------------------------------------------------
110124
111125
if ! command -v codegraph >/dev/null 2>&1; then
@@ -130,9 +144,9 @@ If the user says no, proceed without CodeGraph and mention that structural code
130144
exit 0
131145
fi
132146
133-
if [ ! -d "$PROJECT_DIR/.codegraph" ]; then
134-
emit "[CodeGraph] CLI present, but this project is NOT initialized (no .codegraph/ at
135-
${PROJECT_DIR}).
147+
if ! index_initialized; then
148+
emit "[CodeGraph] CLI present, but this project is NOT initialized (no index DB under
149+
${PROJECT_DIR}/.codegraph/).
136150
Do NOT leave this as a passive note. Before substantive work, ask using the host's structured
137151
prompt/confirmation dialog when one is available (the same kind of UI used for permissions or
138152
command approvals, such as a request_user_input or confirm tool). If no such tool exists, ask this
@@ -1644,6 +1658,55 @@ PY
16441658
else FAILED=1; note "ERROR: failed to remove $tag hooks from $file"; fi
16451659
}
16461660
1661+
# Reverse install's Gemini `context.fileName` additions (AGENTS.md / GEMINI.md), but ONLY for
1662+
# entries whose instruction file is now gone — a dangling reference. If the file still exists the
1663+
# user may rely on it, so we leave it. Args after FILE are NAME=PATH pairs; NAME is pruned only when
1664+
# PATH does not exist. Tidies an emptied fileName/context. Atomic; refuses malformed JSON.
1665+
prune_gemini_filename() { # prune_gemini_filename SETTINGS_JSON NAME=PATH [NAME=PATH ...]
1666+
local file="$1"; shift; [ -f "$file" ] || return 0
1667+
local prune="" pair name path
1668+
for pair in "$@"; do
1669+
name="${pair%%=*}"; path="${pair#*=}"
1670+
[ -e "$path" ] || prune="$prune $name"
1671+
done
1672+
prune="$(printf '%s' "$prune" | sed 's/^ *//')"
1673+
[ -z "$prune" ] && return 0
1674+
if [ "$DRYRUN" = 1 ]; then note "would prune dangling fileName entries ($prune) from $file"; return 0; fi
1675+
if [ "$HAVE_PY" = 0 ]; then note "python3 not found — remove dangling [$prune] from context.fileName in $file by hand"; return 0; fi
1676+
if CG_FILE="$file" CG_PRUNE="$prune" "$PY" - <<'PY'
1677+
import os, json, sys, tempfile
1678+
f=os.environ["CG_FILE"]; prune=set(os.environ["CG_PRUNE"].split())
1679+
raw=open(f,encoding="utf-8").read()
1680+
if not raw.strip(): sys.exit(0)
1681+
try: data=json.loads(raw)
1682+
except Exception as ex:
1683+
sys.stderr.write(f"[agent-primer] {f}: invalid JSON ({ex}); refusing to modify it\n"); sys.exit(2)
1684+
if not isinstance(data, dict): sys.exit(0)
1685+
ctx=data.get("context")
1686+
if not isinstance(ctx, dict): sys.exit(0)
1687+
names=ctx.get("fileName")
1688+
if isinstance(names, str): names=[names]
1689+
if not isinstance(names, list): sys.exit(0)
1690+
new=[n for n in names if n not in prune]
1691+
if new==names: sys.exit(0)
1692+
if new: ctx["fileName"]=new
1693+
else:
1694+
ctx.pop("fileName", None)
1695+
if not ctx: data.pop("context", None)
1696+
text=json.dumps(data, indent=2)+"\n"
1697+
d=os.path.dirname(f) or "."; fd,tmp=tempfile.mkstemp(dir=d, prefix=".ap-", suffix=".tmp")
1698+
try:
1699+
with os.fdopen(fd,"w",encoding="utf-8") as o: o.write(text)
1700+
os.replace(tmp,f)
1701+
except Exception as ex:
1702+
try: os.unlink(tmp)
1703+
except OSError: pass
1704+
sys.stderr.write(f"[agent-primer] {f}: write failed ({ex})\n"); sys.exit(3)
1705+
PY
1706+
then note "pruned dangling fileName entries ($prune) from $file"
1707+
else FAILED=1; note "ERROR: failed to prune fileName entries from $file"; fi
1708+
}
1709+
16471710
note "uninstall scope=$SCOPE target=$ROOT agents=$AGENTS dry-run=$DRYRUN purge=$PURGE"
16481711
16491712
if selected claude; then
@@ -1656,7 +1719,15 @@ if selected cursor; then
16561719
[ -n "$CURSOR_RULE_DIR" ] && for n in $STANDALONE_NAMES; do rm_path "$CURSOR_RULE_DIR/$n.mdc"; done
16571720
unhook_json "$HFILE" cursor
16581721
fi
1659-
if selected gemini; then strip_markers "$GEMINI_INSTR"; unhook_json "$GS" gemini; fi # leaves context.fileName (harmless, user may rely on it)
1722+
if selected gemini; then
1723+
strip_markers "$GEMINI_INSTR"; unhook_json "$GS" gemini
1724+
# Reverse install's context.fileName additions — but only entries now pointing at a deleted
1725+
# file (strip_markers removes GEMINI.md/AGENTS.md when they held only our blocks). A file the
1726+
# user still has is left referenced. AGENTS.md is project-scope only: globally that entry means
1727+
# "read each project's AGENTS.md", which agent-primer's global install does not own.
1728+
if [ "$SCOPE" = "project" ]; then prune_gemini_filename "$GS" "GEMINI.md=$GEMINI_INSTR" "AGENTS.md=$ROOT/AGENTS.md"
1729+
else prune_gemini_filename "$GS" "GEMINI.md=$GEMINI_INSTR"; fi
1730+
fi
16601731
if selected opencode; then rm_path "$OPENCODE_PLUG"; rm_path "${OPENCODE_PLUG%/*}/primer-session-check.js"; strip_markers "$OPENCODE_INSTR"; fi
16611732
if selected antigravity; then
16621733
[ -n "$ANTI_RULE_DIR" ] && for n in $STANDALONE_NAMES; do rm_path "$ANTI_RULE_DIR/$n.md"; done

codegraph-session-check.sh

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ emit() {
9696
esac
9797
}
9898

99+
# An initialized index = the .codegraph/ dir AND at least one SQLite db file inside it.
100+
# We check for the db file rather than running `codegraph status` on purpose: a bare
101+
# .codegraph/ directory left behind by an aborted/half-finished `init` would otherwise
102+
# look "set up" and make once-mode go silent on a broken index. Running `codegraph status`
103+
# every session is exactly the cost once-mode removes, so this cheap filesystem check is the
104+
# right middle ground; deep corruption is still `codegraph status`/--always's job.
105+
index_initialized() {
106+
[ -d "$PROJECT_DIR/.codegraph" ] || return 1
107+
for _db in "$PROJECT_DIR"/.codegraph/*.db; do
108+
[ -f "$_db" ] && return 0
109+
done
110+
return 1
111+
}
112+
99113
# --- checks ----------------------------------------------------------------
100114

101115
if ! command -v codegraph >/dev/null 2>&1; then
@@ -120,9 +134,9 @@ If the user says no, proceed without CodeGraph and mention that structural code
120134
exit 0
121135
fi
122136

123-
if [ ! -d "$PROJECT_DIR/.codegraph" ]; then
124-
emit "[CodeGraph] CLI present, but this project is NOT initialized (no .codegraph/ at
125-
${PROJECT_DIR}).
137+
if ! index_initialized; then
138+
emit "[CodeGraph] CLI present, but this project is NOT initialized (no index DB under
139+
${PROJECT_DIR}/.codegraph/).
126140
Do NOT leave this as a passive note. Before substantive work, ask using the host's structured
127141
prompt/confirmation dialog when one is available (the same kind of UI used for permissions or
128142
command approvals, such as a request_user_input or confirm tool). If no such tool exists, ask this

tests/smoke.sh

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ chk "trailing --agents does not hang" 'guard bash "$INSTALL" --project "$(mk
2727
chk "trailing --format hook does not hang" 'guard bash "$ROOT/codegraph-session-check.sh" --format >/dev/null 2>&1; [ $? -ne 142 ]'
2828

2929
echo "== once-mode hook (default) + --always opt-out =="
30-
OM="$(mk)"; mkdir -p "$OM/.codegraph"
30+
# A real index = the .codegraph/ dir AND a SQLite db inside it (an aborted init leaves a bare dir).
31+
OM="$(mk)"; mkdir -p "$OM/.codegraph"; : > "$OM/.codegraph/codegraph.db"
3132
if command -v codegraph >/dev/null 2>&1; then
3233
chk "once-mode: silent when project is set up" '[ -z "$(guard bash "$ROOT/codegraph-session-check.sh" --format text --project "$OM" 2>/dev/null)" ]'
3334
chk "--always: prints index-present block" 'guard bash "$ROOT/codegraph-session-check.sh" --format text --project "$OM" --always 2>/dev/null | grep -q "Index present"'
35+
OMH="$(mk)"; mkdir -p "$OMH/.codegraph" # dir present but NO index db = half-built init
36+
chk "half-built .codegraph (no db) nudges init" 'guard bash "$ROOT/codegraph-session-check.sh" --format text --project "$OMH" 2>/dev/null | grep -q "codegraph init -i"'
3437
OM2="$(mk)"
3538
chk "not set up: still nudges codegraph init" 'guard bash "$ROOT/codegraph-session-check.sh" --format text --project "$OM2" 2>/dev/null | grep -q "codegraph init -i"'
3639
chk "not set up: asks through prompt dialog" 'guard bash "$ROOT/codegraph-session-check.sh" --format text --project "$OM2" 2>/dev/null | grep -F "prompt/confirmation dialog" >/dev/null && guard bash "$ROOT/codegraph-session-check.sh" --format text --project "$OM2" 2>/dev/null | grep -F "Want me to run" >/dev/null'
@@ -136,6 +139,17 @@ chk "AGENTS.md block-free after uninstall" '! has_block "$U/AGENTS.md" codegraph
136139
chk "claude hook removed" '! grep -q codegraph-session-check.sh "$U/.claude/settings.json" 2>/dev/null'
137140
chk "cursor superpowers.mdc removed" '[ ! -f "$U/.cursor/rules/superpowers.mdc" ]'
138141
chk "kit dir removed" '[ ! -d "$U/tools/agent-primer" ]'
142+
chk "gemini fileName: dangling GEMINI.md pruned" '! grep -q "GEMINI.md" "$U/.gemini/settings.json" 2>/dev/null'
143+
chk "gemini fileName: dangling AGENTS.md pruned" '! grep -q "AGENTS.md" "$U/.gemini/settings.json" 2>/dev/null'
144+
chk "gemini settings.json still valid JSON" 'vjson "$U/.gemini/settings.json"'
145+
146+
echo "== gemini fileName: a surviving file's ref is kept =="
147+
GK="$(mk)"; guard bash "$INSTALL" --project "$GK" --agents gemini >/dev/null 2>&1
148+
printf '\nuser-kept content\n' >> "$GK/GEMINI.md" # GEMINI.md now has non-ours content → survives strip
149+
guard bash "$UNINSTALL" --project "$GK" --agents gemini >/dev/null 2>&1
150+
chk "gemini: surviving GEMINI.md preserved" '[ -f "$GK/GEMINI.md" ]'
151+
chk "gemini: surviving GEMINI.md ref kept" 'grep -q "GEMINI.md" "$GK/.gemini/settings.json" 2>/dev/null'
152+
chk "gemini: dangling AGENTS.md ref still pruned" '! grep -q "AGENTS.md" "$GK/.gemini/settings.json" 2>/dev/null'
139153

140154
echo "== global install + uninstall (isolated HOME) =="
141155
H="$(mk)"; guard env HOME="$H" bash "$INSTALL" --global >/dev/null 2>&1

uninstall.sh

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,55 @@ PY
268268
else FAILED=1; note "ERROR: failed to remove $tag hooks from $file"; fi
269269
}
270270

271+
# Reverse install's Gemini `context.fileName` additions (AGENTS.md / GEMINI.md), but ONLY for
272+
# entries whose instruction file is now gone — a dangling reference. If the file still exists the
273+
# user may rely on it, so we leave it. Args after FILE are NAME=PATH pairs; NAME is pruned only when
274+
# PATH does not exist. Tidies an emptied fileName/context. Atomic; refuses malformed JSON.
275+
prune_gemini_filename() { # prune_gemini_filename SETTINGS_JSON NAME=PATH [NAME=PATH ...]
276+
local file="$1"; shift; [ -f "$file" ] || return 0
277+
local prune="" pair name path
278+
for pair in "$@"; do
279+
name="${pair%%=*}"; path="${pair#*=}"
280+
[ -e "$path" ] || prune="$prune $name"
281+
done
282+
prune="$(printf '%s' "$prune" | sed 's/^ *//')"
283+
[ -z "$prune" ] && return 0
284+
if [ "$DRYRUN" = 1 ]; then note "would prune dangling fileName entries ($prune) from $file"; return 0; fi
285+
if [ "$HAVE_PY" = 0 ]; then note "python3 not found — remove dangling [$prune] from context.fileName in $file by hand"; return 0; fi
286+
if CG_FILE="$file" CG_PRUNE="$prune" "$PY" - <<'PY'
287+
import os, json, sys, tempfile
288+
f=os.environ["CG_FILE"]; prune=set(os.environ["CG_PRUNE"].split())
289+
raw=open(f,encoding="utf-8").read()
290+
if not raw.strip(): sys.exit(0)
291+
try: data=json.loads(raw)
292+
except Exception as ex:
293+
sys.stderr.write(f"[agent-primer] {f}: invalid JSON ({ex}); refusing to modify it\n"); sys.exit(2)
294+
if not isinstance(data, dict): sys.exit(0)
295+
ctx=data.get("context")
296+
if not isinstance(ctx, dict): sys.exit(0)
297+
names=ctx.get("fileName")
298+
if isinstance(names, str): names=[names]
299+
if not isinstance(names, list): sys.exit(0)
300+
new=[n for n in names if n not in prune]
301+
if new==names: sys.exit(0)
302+
if new: ctx["fileName"]=new
303+
else:
304+
ctx.pop("fileName", None)
305+
if not ctx: data.pop("context", None)
306+
text=json.dumps(data, indent=2)+"\n"
307+
d=os.path.dirname(f) or "."; fd,tmp=tempfile.mkstemp(dir=d, prefix=".ap-", suffix=".tmp")
308+
try:
309+
with os.fdopen(fd,"w",encoding="utf-8") as o: o.write(text)
310+
os.replace(tmp,f)
311+
except Exception as ex:
312+
try: os.unlink(tmp)
313+
except OSError: pass
314+
sys.stderr.write(f"[agent-primer] {f}: write failed ({ex})\n"); sys.exit(3)
315+
PY
316+
then note "pruned dangling fileName entries ($prune) from $file"
317+
else FAILED=1; note "ERROR: failed to prune fileName entries from $file"; fi
318+
}
319+
271320
note "uninstall scope=$SCOPE target=$ROOT agents=$AGENTS dry-run=$DRYRUN purge=$PURGE"
272321

273322
if selected claude; then
@@ -280,7 +329,15 @@ if selected cursor; then
280329
[ -n "$CURSOR_RULE_DIR" ] && for n in $STANDALONE_NAMES; do rm_path "$CURSOR_RULE_DIR/$n.mdc"; done
281330
unhook_json "$HFILE" cursor
282331
fi
283-
if selected gemini; then strip_markers "$GEMINI_INSTR"; unhook_json "$GS" gemini; fi # leaves context.fileName (harmless, user may rely on it)
332+
if selected gemini; then
333+
strip_markers "$GEMINI_INSTR"; unhook_json "$GS" gemini
334+
# Reverse install's context.fileName additions — but only entries now pointing at a deleted
335+
# file (strip_markers removes GEMINI.md/AGENTS.md when they held only our blocks). A file the
336+
# user still has is left referenced. AGENTS.md is project-scope only: globally that entry means
337+
# "read each project's AGENTS.md", which agent-primer's global install does not own.
338+
if [ "$SCOPE" = "project" ]; then prune_gemini_filename "$GS" "GEMINI.md=$GEMINI_INSTR" "AGENTS.md=$ROOT/AGENTS.md"
339+
else prune_gemini_filename "$GS" "GEMINI.md=$GEMINI_INSTR"; fi
340+
fi
284341
if selected opencode; then rm_path "$OPENCODE_PLUG"; rm_path "${OPENCODE_PLUG%/*}/primer-session-check.js"; strip_markers "$OPENCODE_INSTR"; fi
285342
if selected antigravity; then
286343
[ -n "$ANTI_RULE_DIR" ] && for n in $STANDALONE_NAMES; do rm_path "$ANTI_RULE_DIR/$n.md"; done

0 commit comments

Comments
 (0)