diff --git a/plugins/memory/hooks/post-tool-use-write.sh b/plugins/memory/hooks/post-tool-use-write.sh deleted file mode 100755 index c835303..0000000 --- a/plugins/memory/hooks/post-tool-use-write.sh +++ /dev/null @@ -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 diff --git a/plugins/memory/hooks/session-context.sh b/plugins/memory/hooks/session-context.sh deleted file mode 100755 index 917c1b9..0000000 --- a/plugins/memory/hooks/session-context.sh +++ /dev/null @@ -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 diff --git a/plugins/memory/hooks/session-end.sh b/plugins/memory/hooks/session-end.sh deleted file mode 100755 index e63f2be..0000000 --- a/plugins/memory/hooks/session-end.sh +++ /dev/null @@ -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 diff --git a/plugins/memory/hooks/session-start.sh b/plugins/memory/hooks/session-start.sh deleted file mode 100755 index 5090d40..0000000 --- a/plugins/memory/hooks/session-start.sh +++ /dev/null @@ -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 - </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 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 diff --git a/plugins/memory/tests/README.md b/plugins/memory/tests/README.md index 4332789..52703d9 100644 --- a/plugins/memory/tests/README.md +++ b/plugins/memory/tests/README.md @@ -116,9 +116,12 @@ journal fixture is expected. ## 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 cd /home/jared/dev/cc-os/plugins/memory/tests -bash generate-fixtures.sh +HOOKS_DIR="$(pwd)/python-wrappers" bash generate-fixtures.sh ``` The script: @@ -129,16 +132,19 @@ The script: ## 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 - 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 ``` - 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: ```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 ... &` 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 - `(graphify not invoked within 5s)` — re-run in that case. This is a known caveat of the - bash design; the Python port should spawn the same detached pattern or document the - behavioral difference. + `(graphify not invoked within 5s)` — re-run in that case. The Python port uses the same + detached spawn pattern. 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