Remove dead bash hooks after Python port
- Delete the four ported bash hooks from plugins/memory/hooks (now live only in git history; runtime uses the Python ports) - Update tests/README to reflect removal; test harness drives Python hooks via python-wrappers adapters (preserved) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
48e4d77795
commit
fa639cfad7
|
|
@ -1,55 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# post-tool-use-write.sh — PostToolUse hook for Write/Edit tool calls
|
|
||||||
# Fires after every Write or Edit; updates the Graphify graph if the file
|
|
||||||
# is a .md file inside the configured vault.
|
|
||||||
|
|
||||||
# Ensure log directory exists
|
|
||||||
mkdir -p ~/.cache/graphify
|
|
||||||
|
|
||||||
# Parse config with python3+PyYAML; handles missing file, missing key, and tilde
|
|
||||||
VAULT_PATH=$(python3 -c '
|
|
||||||
import yaml, os
|
|
||||||
p = os.path.expanduser("~/.claude/plugins/memory/config.yaml")
|
|
||||||
cfg = (yaml.safe_load(open(p)) or {}) if os.path.exists(p) else {}
|
|
||||||
print(os.path.expanduser(cfg.get("vault_path", "~/Documents/SecondBrain")))
|
|
||||||
')
|
|
||||||
|
|
||||||
# Capture stdin
|
|
||||||
STDIN=$(cat)
|
|
||||||
|
|
||||||
# Extract fields
|
|
||||||
FILE_PATH=$(echo "$STDIN" | jq -r '.tool_input.file_path // empty')
|
|
||||||
SESSION_ID=$(echo "$STDIN" | jq -r '.session_id // empty')
|
|
||||||
|
|
||||||
# Guard: exit silently if FILE_PATH is empty
|
|
||||||
if [[ -z "$FILE_PATH" ]]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Guard: exit silently if file does not end in .md
|
|
||||||
if [[ "$FILE_PATH" != *.md ]]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Guard: exit silently if file is not under VAULT_PATH
|
|
||||||
if [[ "$FILE_PATH" != "$VAULT_PATH"* ]]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Record vault write and invalidate rebuild stamp
|
|
||||||
LOG="$HOME/.cache/graphify/post-tool-use.log"
|
|
||||||
|
|
||||||
# Always record touched path for session-end reconciliation
|
|
||||||
TOUCH_FILE="/tmp/claude-vault-touched-$SESSION_ID"
|
|
||||||
echo "$FILE_PATH" >> "$TOUCH_FILE"
|
|
||||||
|
|
||||||
# Invalidate the rebuild stamp so next SessionStart triggers an incremental --update rebuild
|
|
||||||
STAMP_FILE="$HOME/.cache/graphify/vault-rebuild.stamp"
|
|
||||||
if rm -f "$STAMP_FILE"; then
|
|
||||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Invalidated rebuild stamp: $FILE_PATH" >> "$LOG"
|
|
||||||
else
|
|
||||||
# Deletion failed (shouldn't happen), but never block
|
|
||||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] WARN: Could not delete rebuild stamp for $FILE_PATH" >> "$LOG"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# UserPromptSubmit hook — injects project graph path on the FIRST user prompt per session.
|
|
||||||
# Outputs: {"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": "..."}}
|
|
||||||
# On subsequent prompts: outputs {} (no-op).
|
|
||||||
|
|
||||||
# Do NOT set -e — failures are expected and handled.
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 0. Read stdin
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
STDIN_JSON="$(cat 2>/dev/null || true)"
|
|
||||||
|
|
||||||
SESSION_ID="$(printf '%s' "$STDIN_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('session_id','unknown'))" 2>/dev/null || true)"
|
|
||||||
CWD="$(printf '%s' "$STDIN_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('cwd',''))" 2>/dev/null || true)"
|
|
||||||
CWD="${CWD:-$PWD}"
|
|
||||||
|
|
||||||
FLAG_FILE="/tmp/memory-context-injected-${SESSION_ID:-unknown}"
|
|
||||||
|
|
||||||
if [ -f "$FLAG_FILE" ]; then
|
|
||||||
echo '{}'
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
touch "$FLAG_FILE"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 1. Project graph pointer (only output)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
CONTEXT=""
|
|
||||||
|
|
||||||
PROJECT_ROOT="$(git -C "$CWD" rev-parse --show-toplevel 2>/dev/null || true)"
|
|
||||||
if [ -n "$PROJECT_ROOT" ]; then
|
|
||||||
PROJECT_GRAPH="$PROJECT_ROOT/graphify-out/graph.json"
|
|
||||||
if [ -f "$PROJECT_GRAPH" ]; then
|
|
||||||
CONTEXT="## Project Graph
|
|
||||||
|
|
||||||
Project graph: ${PROJECT_GRAPH}
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 2. Emit JSON
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
OUTPUT="$(python3 -c "import json, sys; print(json.dumps({'hookSpecificOutput': {'hookEventName': 'UserPromptSubmit', 'additionalContext': sys.argv[1]}}))" "$CONTEXT" 2>/dev/null)"
|
|
||||||
printf '%s\n' "$OUTPUT"
|
|
||||||
exit 0
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# session-end.sh — SessionEnd hook
|
|
||||||
# Fires at session end; appends a journal entry to the vault's daily log.
|
|
||||||
# Exit code is ignored by Claude Code — used for cleanup/logging only.
|
|
||||||
|
|
||||||
# Capture stdin
|
|
||||||
STDIN=$(cat)
|
|
||||||
|
|
||||||
# Extract fields
|
|
||||||
SESSION_ID=$(echo "$STDIN" | jq -r '.session_id // empty')
|
|
||||||
REASON=$(echo "$STDIN" | jq -r '.reason // empty')
|
|
||||||
|
|
||||||
# Guard: if SESSION_ID is empty, use PID as fallback to ensure unique path
|
|
||||||
if [ -z "$SESSION_ID" ]; then
|
|
||||||
SESSION_ID="unknown-$$"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Parse config with python3+PyYAML; handles missing file, missing key, and tilde
|
|
||||||
VAULT_PATH=$(python3 -c '
|
|
||||||
import yaml, os
|
|
||||||
p = os.path.expanduser("~/.claude/plugins/memory/config.yaml")
|
|
||||||
cfg = (yaml.safe_load(open(p)) or {}) if os.path.exists(p) else {}
|
|
||||||
print(os.path.expanduser(cfg.get("vault_path", "~/Documents/SecondBrain")))
|
|
||||||
')
|
|
||||||
|
|
||||||
# Compute journal path
|
|
||||||
JOURNAL_DIR="$VAULT_PATH/journal"
|
|
||||||
JOURNAL_PATH="$JOURNAL_DIR/$(date -u +%Y-%m-%d).md"
|
|
||||||
|
|
||||||
# Ensure journal directory exists
|
|
||||||
mkdir -p "$JOURNAL_DIR"
|
|
||||||
|
|
||||||
# Create journal note if absent (minimal frontmatter)
|
|
||||||
if [[ ! -f "$JOURNAL_PATH" ]]; then
|
|
||||||
cat >"$JOURNAL_PATH" <<'EOF'
|
|
||||||
---
|
|
||||||
summary: Daily session log
|
|
||||||
tags: [scope/global, type/log]
|
|
||||||
---
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Read touched paths from temp file
|
|
||||||
TOUCH_FILE="/tmp/claude-vault-touched-$SESSION_ID"
|
|
||||||
TOUCHED=$(cat "$TOUCH_FILE" 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
# Format touched paths list
|
|
||||||
if [[ -z "$TOUCHED" ]]; then
|
|
||||||
TOUCHED_LIST="(none)"
|
|
||||||
else
|
|
||||||
TOUCHED_LIST="$TOUCHED"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Determine project context
|
|
||||||
PROJECT=$(git rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-unknown}")
|
|
||||||
|
|
||||||
# UTC timestamp for entry header
|
|
||||||
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
||||||
|
|
||||||
# Append journal entry
|
|
||||||
{
|
|
||||||
echo ""
|
|
||||||
echo "## Session — $TIMESTAMP"
|
|
||||||
echo ""
|
|
||||||
echo "**Project:** $PROJECT"
|
|
||||||
echo "**Reason:** $REASON"
|
|
||||||
echo "**Vault notes touched:**"
|
|
||||||
echo "$TOUCHED_LIST"
|
|
||||||
} >> "$JOURNAL_PATH"
|
|
||||||
|
|
||||||
# Cleanup temp file
|
|
||||||
rm -f "$TOUCH_FILE"
|
|
||||||
|
|
||||||
# ── memsearch memory git sync (fail-safe) ───────────────────────────────────
|
|
||||||
# Auto-commit + push the memsearch memory store (~/.memsearch) on session end.
|
|
||||||
# We own this sync here so we never patch the marketplace memsearch plugin
|
|
||||||
# (which clobbers on update). The whitelist .gitignore in ~/.memsearch tracks
|
|
||||||
# only memory/*.md + .gitignore, so `add -A` is safe.
|
|
||||||
#
|
|
||||||
# Robustness contract: this block MUST NEVER fail session shutdown.
|
|
||||||
# - Skips silently if ~/.memsearch is not a git repo.
|
|
||||||
# - Only commits when something is staged (no empty commits).
|
|
||||||
# - Push is hard-timeboxed and failures are swallowed; the next session's
|
|
||||||
# hook catches up since the daily memory files are append-only.
|
|
||||||
# Wrapped in a subshell with `|| true` so nothing here can abort the hook.
|
|
||||||
(
|
|
||||||
MEMSEARCH_DIR="/home/jared/.memsearch"
|
|
||||||
if [ -d "$MEMSEARCH_DIR/.git" ]; then
|
|
||||||
git -C "$MEMSEARCH_DIR" add -A >/dev/null 2>&1
|
|
||||||
if ! git -C "$MEMSEARCH_DIR" diff --cached --quiet; then
|
|
||||||
git -C "$MEMSEARCH_DIR" commit -m "memsearch: session memory $(date +%F)" >/dev/null 2>&1
|
|
||||||
echo "[session-end] memsearch memory committed"
|
|
||||||
fi
|
|
||||||
timeout 30 git -C "$MEMSEARCH_DIR" push --quiet origin main >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
) || true
|
|
||||||
# ────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# SessionStart hook for the memory plugin.
|
|
||||||
# Invoked by Claude Code at the start of every session with JSON on stdin.
|
|
||||||
# Must return sub-second; heavy work is detached in the background.
|
|
||||||
# Context injection is handled by session-context.sh (UserPromptSubmit hook).
|
|
||||||
|
|
||||||
# Never let any individual failure exit the whole script.
|
|
||||||
# Do NOT set -e — grep-no-match, graphify errors, etc. are all expected.
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 0. Read stdin (ignore errors — fields may be absent on older Claude Code)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
STDIN_JSON="$(cat 2>/dev/null || true)"
|
|
||||||
HOOK_SOURCE="$(printf '%s' "$STDIN_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('source','unknown'))" 2>/dev/null || true)"
|
|
||||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] session-start fired: source=${HOOK_SOURCE:-unknown} cwd=$(pwd)" >> /tmp/memory-hook.log 2>/dev/null || true
|
|
||||||
CWD="$(printf '%s' "$STDIN_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('cwd',''))" 2>/dev/null || true)"
|
|
||||||
CWD="${CWD:-$PWD}"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 1. Parse config.yaml — emit shell variable assignments, eval them in.
|
|
||||||
# python3 + PyYAML are guaranteed present via graphify.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
eval "$(python3 - <<'PY'
|
|
||||||
import yaml, os, shlex, sys
|
|
||||||
|
|
||||||
CONFIG_PATH = os.path.expanduser("~/.claude/plugins/memory/config.yaml")
|
|
||||||
try:
|
|
||||||
with open(CONFIG_PATH) as f:
|
|
||||||
cfg = yaml.safe_load(f) or {}
|
|
||||||
except Exception:
|
|
||||||
cfg = {}
|
|
||||||
|
|
||||||
out = {
|
|
||||||
"VAULT_PATH": os.path.expanduser(cfg.get("vault_path", "~/Documents/SecondBrain")),
|
|
||||||
"GRAPHIFY_OUTPUT_DIR": os.path.expanduser(cfg.get("graphify_output_dir", "~/Documents/SecondBrain/.graphify")),
|
|
||||||
"MODEL": cfg.get("ollama_model", "qwen25-coder-7b-16k"),
|
|
||||||
"STALE_DAYS": str(cfg.get("stale_threshold_days", 7)),
|
|
||||||
}
|
|
||||||
for k, v in out.items():
|
|
||||||
print(f"{k}={shlex.quote(str(v))}")
|
|
||||||
|
|
||||||
# Export env block so the background rebuild process inherits them.
|
|
||||||
for k, v in (cfg.get("env") or {}).items():
|
|
||||||
print(f"export {k}={shlex.quote(str(v))}")
|
|
||||||
PY
|
|
||||||
2>/dev/null || true)"
|
|
||||||
|
|
||||||
# Apply fallbacks in case the python3 block produced nothing.
|
|
||||||
VAULT_PATH="${VAULT_PATH:-$HOME/Documents/SecondBrain}"
|
|
||||||
GRAPHIFY_OUTPUT_DIR="${GRAPHIFY_OUTPUT_DIR:-$HOME/Documents/SecondBrain/.graphify}"
|
|
||||||
MODEL="${MODEL:-qwen25-coder-7b-16k}"
|
|
||||||
STALE_DAYS="${STALE_DAYS:-7}"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 2. Cache dir + file paths
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
CACHE_DIR="$HOME/.cache/graphify"
|
|
||||||
STAMP="$CACHE_DIR/vault-rebuild.stamp"
|
|
||||||
LOCK="$CACHE_DIR/vault-rebuild.lock"
|
|
||||||
LOGFILE="$CACHE_DIR/vault-rebuild.log"
|
|
||||||
|
|
||||||
mkdir -p "$CACHE_DIR" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Export vars needed by the detached bash -c subprocess.
|
|
||||||
export VAULT_PATH MODEL STAMP LOCK LOGFILE
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# 3. Staleness check + detached background rebuild
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
_needs_rebuild() {
|
|
||||||
[ ! -f "$STAMP" ] && return 0
|
|
||||||
# Use python3 for portable mtime arithmetic (avoids `date -d` vs BSD split).
|
|
||||||
python3 - <<PY 2>/dev/null
|
|
||||||
import os, time
|
|
||||||
stamp = "$STAMP"
|
|
||||||
stale_days = int("$STALE_DAYS")
|
|
||||||
try:
|
|
||||||
age_days = (time.time() - os.path.getmtime(stamp)) / 86400
|
|
||||||
exit(0 if age_days > stale_days else 1)
|
|
||||||
except Exception:
|
|
||||||
exit(0)
|
|
||||||
PY
|
|
||||||
}
|
|
||||||
|
|
||||||
if _needs_rebuild; then
|
|
||||||
if [ -f "$LOCK" ]; then
|
|
||||||
echo "[memory/session-start] rebuild already running (lock present), skipping" >> "$LOGFILE" 2>/dev/null || true
|
|
||||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] session-start: rebuild skipped (lock present)" >> /tmp/memory-hook.log 2>/dev/null || true
|
|
||||||
else
|
|
||||||
# Create lock, then spawn fully detached (no inherited stdin/stdout/stderr).
|
|
||||||
touch "$LOCK" 2>/dev/null || true
|
|
||||||
# shellcheck disable=SC2016
|
|
||||||
nohup bash -c \
|
|
||||||
'graphify extract "$VAULT_PATH" --backend ollama --model "$MODEL" --force \
|
|
||||||
>> "$LOGFILE" 2>&1 \
|
|
||||||
&& date > "$STAMP" && rm -f "$LOCK" \
|
|
||||||
|| (rm -f "$LOCK"; echo "rebuild failed" >> "$LOGFILE")' \
|
|
||||||
</dev/null >/dev/null 2>&1 &
|
|
||||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] session-start: rebuild triggered" >> /tmp/memory-hook.log 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] session-start: rebuild not needed" >> /tmp/memory-hook.log 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
|
|
@ -116,9 +116,12 @@ journal fixture is expected.
|
||||||
|
|
||||||
## Running all scenarios
|
## Running all scenarios
|
||||||
|
|
||||||
|
The bash hooks (`hooks/*.sh`) were removed after parity was verified — they live in git
|
||||||
|
history only. The only supported invocation uses the Python hooks via `tests/python-wrappers/`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/jared/dev/cc-os/plugins/memory/tests
|
cd /home/jared/dev/cc-os/plugins/memory/tests
|
||||||
bash generate-fixtures.sh
|
HOOKS_DIR="$(pwd)/python-wrappers" bash generate-fixtures.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The script:
|
The script:
|
||||||
|
|
@ -129,16 +132,19 @@ The script:
|
||||||
|
|
||||||
## Replaying against the Python hooks
|
## Replaying against the Python hooks
|
||||||
|
|
||||||
The Python hooks are now canonical. The bash `.sh` files remain alongside them for reference but are no longer the source of truth. To validate parity (e.g. after modifying a Python hook):
|
The Python hooks are canonical. The bash `.sh` files were removed after parity was verified
|
||||||
|
(2026-06-12) and exist only in git history. To validate the Python hooks against golden
|
||||||
|
fixtures (e.g. after modifying a hook):
|
||||||
|
|
||||||
1. Ensure each Python hook is executable at a known path (they live in `../hooks/`).
|
1. Ensure the Python hooks are executable in `../hooks/` and the wrappers are in
|
||||||
|
`tests/python-wrappers/`.
|
||||||
|
|
||||||
2. Run generate-fixtures.sh with HOOKS_DIR and --replay-dir overridden:
|
2. Run generate-fixtures.sh via python-wrappers with a replay dir:
|
||||||
```bash
|
```bash
|
||||||
HOOKS_DIR=/path/to/python-hooks \
|
HOOKS_DIR=/home/jared/dev/cc-os/plugins/memory/tests/python-wrappers \
|
||||||
bash tests/generate-fixtures.sh --replay-dir /tmp/replay-out
|
bash tests/generate-fixtures.sh --replay-dir /tmp/replay-out
|
||||||
```
|
```
|
||||||
This writes new fixtures to `/tmp/replay-out` using the Python hooks instead of bash hooks.
|
This writes new fixtures to `/tmp/replay-out` using the Python hooks.
|
||||||
|
|
||||||
3. Diff against the committed golden directory:
|
3. Diff against the committed golden directory:
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -156,9 +162,8 @@ since golden fixtures were generated). The before==after safety invariant is alw
|
||||||
1. **session-start-stale is inherently racy.** The hook spawns graphify via `nohup ... &`
|
1. **session-start-stale is inherently racy.** The hook spawns graphify via `nohup ... &`
|
||||||
and exits immediately. The harness polls for the shim log up to 5 seconds. On heavily
|
and exits immediately. The harness polls for the shim log up to 5 seconds. On heavily
|
||||||
loaded systems the poll may time out; if so, `graphify-shim-args.txt` will say
|
loaded systems the poll may time out; if so, `graphify-shim-args.txt` will say
|
||||||
`(graphify not invoked within 5s)` — re-run in that case. This is a known caveat of the
|
`(graphify not invoked within 5s)` — re-run in that case. The Python port uses the same
|
||||||
bash design; the Python port should spawn the same detached pattern or document the
|
detached spawn pattern.
|
||||||
behavioral difference.
|
|
||||||
|
|
||||||
2. **session-start cwd in log lines is normalized to `__CWD__`.** The hook logs
|
2. **session-start cwd in log lines is normalized to `__CWD__`.** The hook logs
|
||||||
`cwd=$(pwd)` (shell invocation directory, not the JSON `cwd` field). This varies by
|
`cwd=$(pwd)` (shell invocation directory, not the JSON `cwd` field). This varies by
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue