Skip to content

Commit a332ee4

Browse files
feat(memory): B4 reconcile native MEMORY.md + canonical main-repo project_id (Wave 4)
- project_memory.py: non-destructive GREEN-node projection into native Claude MEMORY.md (delimited block, preserves user content). memory-projection.sh (SessionEnd, background) wires it. - dream-consolidate.sh: SessionEnd background dream --dry-run (shadow-first; --apply gated behind RALPH_DREAM_APPLY=1). - Addendum 2: canonical project_id = main repo (worktree-unwrapped) across tree_store/recall_v2/migrate/learn_capture/project_memory. Single resolver (resolve_main_repo_root). Durable knowledge is project-scoped, not per-worktree. - tree_store deferred_index context manager: O(n^2) -> O(n) index writes (re-migration 2028 nodes: ~10min -> 3.25s). - Migrated 2028 nodes to canonical id (0 RED, 0 UNSET); projection populated. - test_threat_model_invariants.py (RED never persists, authority, provenance, raw never in listings, node_id safe, project isolation, traversal, corrupt tolerance) + test_project_memory.py. 111 memory tests; pyright 0/0. Note: ~/.claude/settings.json (SessionEnd registration) is live-system state, not tracked in-repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f5c0c94 commit a332ee4

7 files changed

Lines changed: 1277 additions & 37 deletions

File tree

.claude/hooks/dream-consolidate.sh

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env bash
2+
# dream-consolidate.sh — Offline consolidation (L3 dream) in SHADOW mode (Phase B3/B4)
3+
# Hook: SessionEnd
4+
# VERSION: 3.1.0
5+
#
6+
# Runs scripts/memory/dream.py to consolidate Ralph learning sources into L3
7+
# candidates. SHADOW-MODE-FIRST (plan risk mitigation): this hook ONLY runs
8+
# `--dry-run`, which writes NOTHING to ~/.ralph/layers/. It writes a timestamped
9+
# report under ~/.ralph/logs/ for review. The L3 layer is never modified
10+
# automatically.
11+
#
12+
# HOW TO ENABLE --apply (do NOT do this until shadow output is reviewed):
13+
# 1. Inspect a few reports in ~/.ralph/logs/dream/ and confirm candidates are
14+
# sane (no RED material, correct target layers, reasonable counts).
15+
# 2. Set the env flag in ~/.claude/settings.json env block (or export before
16+
# a session): RALPH_DREAM_APPLY=1
17+
# 3. Re-run a session; this hook will then call `dream.py --apply`, writing
18+
# ~/.ralph/layers/L3_dream.md. Revert by unsetting RALPH_DREAM_APPLY.
19+
# The default (flag unset / != 1) is and stays dry-run.
20+
#
21+
# Fire-and-forget: respond with SessionEnd JSON first, run dream in a detached
22+
# worker so session teardown is never blocked.
23+
#
24+
# Output (SessionEnd): {"continue": true} (per tests/HOOK_FORMAT_REFERENCE.md).
25+
#
26+
# SECURITY: umask 077, stdin length limit, worktree-safe path resolution.
27+
28+
# --- Parent mode: respond immediately, fork detached worker ---
29+
if [[ "${DREAM_CONSOLIDATE_WORKER:-0}" != "1" ]]; then
30+
umask 077
31+
INPUT=$(head -c 100000)
32+
DREAM_CONSOLIDATE_WORKER=1 nohup bash "$0" <<<"$INPUT" >/dev/null 2>&1 &
33+
disown 2>/dev/null || true
34+
echo '{"continue": true}'
35+
exit 0
36+
fi
37+
38+
# --- Worker mode (detached) ---
39+
set -euo pipefail
40+
umask 077
41+
42+
trap 'exit 0' ERR EXIT
43+
44+
INPUT=$(head -c 100000) # drained for parity; not currently parsed.
45+
: "${INPUT:=}"
46+
47+
LOG_DIR="${HOME}/.ralph/logs"
48+
DREAM_LOG_DIR="${LOG_DIR}/dream"
49+
mkdir -p "$DREAM_LOG_DIR"
50+
LOG_FILE="${LOG_DIR}/dream-consolidate.log"
51+
52+
log() { echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] $*" >> "$LOG_FILE" 2>/dev/null || true; }
53+
54+
_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
55+
# shellcheck source=/dev/null
56+
source "${_HOOK_DIR}/lib/worktree-utils.sh" 2>/dev/null || true
57+
58+
SCRIPTS_DIR="$(cd "${_HOOK_DIR}/../.." 2>/dev/null && pwd)/scripts/memory"
59+
DREAM="${SCRIPTS_DIR}/dream.py"
60+
61+
if [[ ! -f "$DREAM" ]]; then
62+
log "dream.py not found at $DREAM — skipping"
63+
trap - EXIT
64+
exit 0
65+
fi
66+
67+
TS="$(date -u +"%Y%m%dT%H%M%SZ")"
68+
REPORT="${DREAM_LOG_DIR}/dream-${TS}.json"
69+
70+
# SHADOW MODE (default): --dry-run writes no layer. --apply ONLY when the
71+
# operator opts in explicitly via RALPH_DREAM_APPLY=1.
72+
if [[ "${RALPH_DREAM_APPLY:-0}" == "1" ]]; then
73+
log "RALPH_DREAM_APPLY=1 — running dream.py --apply (L3 will be written)"
74+
if python3 "$DREAM" --apply --json > "$REPORT" 2>> "$LOG_FILE"; then
75+
log "dream apply OK — report: $REPORT"
76+
else
77+
log "dream apply failed (non-critical) — see $LOG_FILE"
78+
fi
79+
else
80+
log "shadow mode — running dream.py --dry-run (no L3 written)"
81+
if python3 "$DREAM" --dry-run --json > "$REPORT" 2>> "$LOG_FILE"; then
82+
log "dream dry-run OK — report: $REPORT"
83+
else
84+
log "dream dry-run failed (non-critical) — see $LOG_FILE"
85+
fi
86+
fi
87+
88+
trap - EXIT
89+
exit 0

.claude/hooks/memory-projection.sh

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env bash
2+
# memory-projection.sh — Native MEMORY.md GREEN-node projection (Phase B4)
3+
# Hook: SessionEnd
4+
# VERSION: 3.1.0
5+
#
6+
# Reconciles Claude's native project memory with the Ralph memory tree without
7+
# drift: it regenerates a single delimited block of the top GREEN nodes inside
8+
# ~/.claude/projects/<id>/memory/MEMORY.md. All content OUTSIDE the block is
9+
# preserved — the projection NEVER clobbers the user's native memory.
10+
#
11+
# Fire-and-forget (same pattern as vault-fact-extractor.sh): respond with the
12+
# SessionEnd JSON immediately, then run the projection in a detached worker so
13+
# session teardown is never blocked.
14+
#
15+
# Output (SessionEnd): {"continue": true} (per tests/HOOK_FORMAT_REFERENCE.md;
16+
# SessionEnd cannot block and does NOT support hookSpecificOutput).
17+
#
18+
# SECURITY: umask 077, stdin length limit, worktree-safe path resolution.
19+
20+
# --- Parent mode: respond immediately, fork detached worker ---
21+
if [[ "${MEMORY_PROJECTION_WORKER:-0}" != "1" ]]; then
22+
umask 077
23+
INPUT=$(head -c 100000)
24+
MEMORY_PROJECTION_WORKER=1 nohup bash "$0" <<<"$INPUT" >/dev/null 2>&1 &
25+
disown 2>/dev/null || true
26+
echo '{"continue": true}'
27+
exit 0
28+
fi
29+
30+
# --- Worker mode (detached) ---
31+
set -euo pipefail
32+
umask 077
33+
34+
# Detached worker: log-and-exit on error (no JSON needed; parent already replied).
35+
trap 'exit 0' ERR EXIT
36+
37+
INPUT=$(head -c 100000) # drained for parity; not currently parsed.
38+
: "${INPUT:=}"
39+
40+
LOG_DIR="${HOME}/.ralph/logs"
41+
mkdir -p "$LOG_DIR"
42+
LOG_FILE="${LOG_DIR}/memory-projection.log"
43+
44+
log() { echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] $*" >> "$LOG_FILE" 2>/dev/null || true; }
45+
46+
# Worktree-safe path resolution.
47+
_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
48+
# shellcheck source=/dev/null
49+
source "${_HOOK_DIR}/lib/worktree-utils.sh" 2>/dev/null || {
50+
get_safe_project_root() { git rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-.}"; }
51+
}
52+
53+
SCRIPTS_DIR="$(cd "${_HOOK_DIR}/../.." 2>/dev/null && pwd)/scripts/memory"
54+
PROJECT_MEMORY="${SCRIPTS_DIR}/project_memory.py"
55+
56+
if [[ ! -f "$PROJECT_MEMORY" ]]; then
57+
log "project_memory.py not found at $PROJECT_MEMORY — skipping"
58+
trap - EXIT
59+
exit 0
60+
fi
61+
62+
# project_memory.py auto-detects the main repo (unwrapping worktrees) so the
63+
# projection always targets the canonical native project id.
64+
if python3 "$PROJECT_MEMORY" --apply >> "$LOG_FILE" 2>&1; then
65+
log "projection applied"
66+
else
67+
log "projection failed (non-critical)"
68+
fi
69+
70+
trap - EXIT
71+
exit 0

0 commit comments

Comments
 (0)