claude-self-reflect 7.1.8 → 7.1.9

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/README.md CHANGED
@@ -26,7 +26,7 @@ Give Claude perfect memory of all your conversations. Search past discussions in
26
26
 
27
27
  **100% Local by Default** • **20x Faster** • **Zero Configuration** • **Production Ready**
28
28
 
29
- > **Latest: v7.0 Automated Narratives** - 9.3x better search quality via AI-powered summaries. [Learn more →](#v70-automated-narrative-generation)
29
+ > **Latest: v7.1.9 Cross-Project Iteration Memory** - Ralph loops now share memory across ALL projects automatically. [Learn more →](#ralph-loop-memory-integration-v719)
30
30
 
31
31
  ## Why This Exists
32
32
 
@@ -326,13 +326,13 @@ Claude: [Searches across ALL your projects]
326
326
  </details>
327
327
 
328
328
  <details>
329
- <summary><b>Ralph Loop Memory Integration (v7.1+)</b></summary>
329
+ <summary><b>Ralph Loop Memory Integration (v7.1.9+)</b></summary>
330
330
 
331
331
  <div align="center">
332
332
  <img src="docs/images/ralph-loop-csr.png" alt="Ralph Loop with CSR Memory - From hamster wheel to upward spiral" width="800"/>
333
333
  </div>
334
334
 
335
- Use the [ralph-wiggum plugin](https://github.com/anthropics/claude-code-plugins/tree/main/ralph-wiggum) for long tasks? CSR automatically gives Ralph loops **cross-session memory**:
335
+ Use the [ralph-wiggum plugin](https://github.com/anthropics/claude-code-plugins/tree/main/ralph-wiggum) for long tasks? CSR automatically gives Ralph loops **cross-session AND cross-project memory**:
336
336
 
337
337
  **Core Features:**
338
338
  - **Automatic backup** before context compaction
@@ -340,43 +340,53 @@ Use the [ralph-wiggum plugin](https://github.com/anthropics/claude-code-plugins/
340
340
  - **Failed approach tracking** - never repeat the same mistakes
341
341
  - **Success pattern learning** - reuse what worked before
342
342
 
343
+ **v7.1.9 Cross-Project Iteration Memory (NEW!):**
344
+ - **Global Hook System** - Hooks fire for ALL projects, not just CSR
345
+ - **Stop Hook** - Captures iteration state after every Claude response
346
+ - **PreCompact Hook** - Backs up Ralph state before context compaction
347
+ - **Project Tagging** - Each entry tagged with `project_{name}` for cross-project visibility
348
+ - **Automatic Storage** - No manual protocol needed, hooks capture everything
349
+
343
350
  **v7.1+ Enhanced Features:**
344
- - **Error Signature Deduplication** - Normalizes errors (removes line numbers, paths, timestamps) to avoid redundant storage
345
- - **Output Decline Detection** - Circuit breaker pattern that detects >70% drop in output length
346
- - **Confidence-Based Exit** - 0-100 scoring based on signals (tasks complete, tests passing, no errors)
347
- - **Anti-Pattern Injection** - "DON'T RETRY THESE" section surfaces failed approaches first
348
- - **Work Type Tracking** - Categorizes sessions as IMPLEMENTATION/TESTING/DEBUGGING/DOCUMENTATION
349
- - **Error-Centric Search** - Finds past sessions by error pattern, not just task description
351
+ - **Error Signature Deduplication** - Normalizes errors (removes line numbers, paths, timestamps)
352
+ - **Output Decline Detection** - Circuit breaker pattern detects >70% output drop
353
+ - **Confidence-Based Exit** - 0-100 scoring (tasks complete, tests passing, no errors)
354
+ - **Anti-Pattern Injection** - "DON'T RETRY THESE" surfaces failed approaches first
355
+ - **Work Type Tracking** - IMPLEMENTATION/TESTING/DEBUGGING/DOCUMENTATION
356
+ - **Error-Centric Search** - Find past sessions by error pattern
350
357
 
351
358
  **Setup (one-time):**
352
359
  ```bash
353
- ./scripts/ralph/install_hooks.sh # Install CSR hooks
360
+ ./scripts/ralph/install_hooks.sh # Install CSR hooks globally
354
361
  ./scripts/ralph/install_hooks.sh --check # Verify installation
355
362
  ```
356
363
 
357
364
  **How it works:**
358
- 1. Start a Ralph loop: `/ralph-wiggum:ralph-loop "Build feature X"`
365
+ 1. Start a Ralph loop in ANY project: `/ralph-wiggum:ralph-loop "Build feature X"`
359
366
  2. Work naturally - state is tracked in `.claude/ralph-loop.local.md`
360
- 3. When compaction occurs, state is backed up to CSR
361
- 4. New sessions retrieve past learnings from CSR automatically
362
- 5. Anti-patterns and winning strategies are surfaced first
367
+ 3. **Stop hook** captures each iteration automatically
368
+ 4. **PreCompact hook** backs up state before compaction
369
+ 5. All entries stored with project tags for cross-project search
363
370
 
364
- **Files created:**
365
- - `.ralph_past_sessions.md` - Injected context from past sessions (auto-generated)
371
+ **Cross-Project Example:**
372
+ ```bash
373
+ # In project-a: learned Docker fix
374
+ # In project-b: CSR finds it automatically
375
+ reflect_on_past("docker memory issue")
376
+ # Returns: project_a session with Docker solution
377
+ ```
366
378
 
367
- **Verified proof (2026-01-04):**
379
+ **Verified proof (2026-01-05):**
368
380
  ```
369
- # Session start hook injects past sessions:
370
- INFO: Found 2 relevant results:
371
- - Anti-patterns: 0
372
- - Winning strategies: 0
373
- - Similar tasks: 2
374
-
375
- # Sessions stored in Qdrant:
376
- {
377
- "tags": ["ralph_session", "outcome_completed"],
378
- "timestamp": "2026-01-04T18:13:03.711262+00:00"
379
- }
381
+ # Hook collection shows cross-project entries:
382
+ Total entries: 23
383
+
384
+ 1. 05:25:06 📦 PreCompact | 📧 emailmon
385
+ 2. 05:22:00 🔄 Iteration | 🔍 claude-self-reflect
386
+ 3. 05:13:51 🔄 Iteration | 📧 emailmon
387
+
388
+ # Entries include project tags:
389
+ tags: ['__csr_hook_auto__', 'project_emailmon', 'ralph_iteration']
380
390
  ```
381
391
 
382
392
  [Full documentation →](docs/development/ralph-memory-integration.md)
@@ -8,6 +8,7 @@ from typing import Optional, List, Dict, Any
8
8
  from datetime import datetime, timezone
9
9
  from pathlib import Path
10
10
  import uuid
11
+ from xml.sax.saxutils import escape as xml_escape
11
12
 
12
13
  from fastmcp import Context
13
14
  from pydantic import Field
@@ -207,13 +208,108 @@ Timestamp: {metadata['timestamp']}"""
207
208
  <error>Conversation ID '{conversation_id}' not found in any project.</error>
208
209
  <suggestion>The conversation may not have been imported yet, or the ID may be incorrect.</suggestion>
209
210
  </conversation_file>"""
210
-
211
+
211
212
  except Exception as e:
212
213
  logger.error(f"Failed to get conversation file: {e}", exc_info=True)
213
214
  return f"""<conversation_file>
214
215
  <error>Failed to locate conversation: {str(e)}</error>
215
216
  </conversation_file>"""
216
217
 
218
+ async def get_session_learnings(
219
+ self,
220
+ ctx: Context,
221
+ session_id: str,
222
+ limit: int = 50
223
+ ) -> str:
224
+ """Get all learnings from a specific Ralph session.
225
+
226
+ This enables iteration-level memory: retrieve what was learned
227
+ in previous iterations of the SAME Ralph loop session.
228
+ """
229
+ from qdrant_client import models
230
+
231
+ await ctx.debug(f"Getting learnings for session: {session_id}")
232
+
233
+ try:
234
+ # Check runtime preference from environment
235
+ prefer_local = os.getenv('PREFER_LOCAL_EMBEDDINGS', 'true').lower() == 'true'
236
+ embedding_type = "local" if prefer_local else "voyage"
237
+ collection_name = f"reflections_{embedding_type}"
238
+
239
+ # Filter by session tag - matches reflections stored with session_{id} tag
240
+ session_filter = models.Filter(
241
+ must=[
242
+ models.FieldCondition(
243
+ key="tags",
244
+ match=models.MatchAny(any=[f"session_{session_id}"])
245
+ )
246
+ ]
247
+ )
248
+
249
+ results, _ = await self.qdrant_client.scroll(
250
+ collection_name=collection_name,
251
+ scroll_filter=session_filter,
252
+ limit=limit,
253
+ with_payload=True,
254
+ )
255
+
256
+ if not results:
257
+ await ctx.debug(f"No learnings found for session {session_id}")
258
+ return f"""<session_learnings>
259
+ <session_id>{session_id}</session_id>
260
+ <count>0</count>
261
+ <message>No learnings stored yet for this session. Use store_reflection() with tags=['session_{session_id}', 'iteration_N', 'ralph_iteration'] to store iteration learnings.</message>
262
+ </session_learnings>"""
263
+
264
+ # Format results
265
+ learnings = []
266
+ for point in results:
267
+ payload = point.payload or {}
268
+ tags = payload.get("tags", [])
269
+ # Extract iteration number from tags if present
270
+ iteration = "unknown"
271
+ for tag in tags:
272
+ if tag.startswith("iteration_"):
273
+ iteration = tag.replace("iteration_", "")
274
+ break
275
+
276
+ learnings.append({
277
+ "content": payload.get("content", ""),
278
+ "iteration": iteration,
279
+ "timestamp": payload.get("timestamp", ""),
280
+ "tags": tags
281
+ })
282
+
283
+ # Sort by timestamp (oldest first for chronological order)
284
+ learnings.sort(key=lambda x: x.get("timestamp", ""))
285
+
286
+ await ctx.debug(f"Found {len(learnings)} learnings for session {session_id}")
287
+
288
+ # Format as XML for structured output (escape special chars for safety)
289
+ learnings_xml = "\n".join([
290
+ f"""<learning iteration="{xml_escape(str(l['iteration']))}">
291
+ <timestamp>{xml_escape(str(l['timestamp']))}</timestamp>
292
+ <content>{xml_escape(str(l['content']))}</content>
293
+ <tags>{xml_escape(', '.join(str(t) for t in l['tags']))}</tags>
294
+ </learning>"""
295
+ for l in learnings
296
+ ])
297
+
298
+ return f"""<session_learnings>
299
+ <session_id>{session_id}</session_id>
300
+ <count>{len(learnings)}</count>
301
+ <learnings>
302
+ {learnings_xml}
303
+ </learnings>
304
+ </session_learnings>"""
305
+
306
+ except Exception as e:
307
+ logger.error(f"Failed to get session learnings: {e}", exc_info=True)
308
+ return f"""<session_learnings>
309
+ <session_id>{session_id}</session_id>
310
+ <error>Failed to retrieve learnings: {str(e)}</error>
311
+ </session_learnings>"""
312
+
217
313
 
218
314
  def register_reflection_tools(
219
315
  mcp,
@@ -249,5 +345,21 @@ def register_reflection_tools(
249
345
  """Get the full JSONL conversation file path for a conversation ID.
250
346
  This allows agents to read complete conversations instead of truncated excerpts."""
251
347
  return await tools.get_full_conversation(ctx, conversation_id, project)
252
-
348
+
349
+ @mcp.tool()
350
+ async def get_session_learnings(
351
+ ctx: Context,
352
+ session_id: str = Field(description="Ralph session ID to get learnings from (e.g., 'ralph_20260104_224757_iter1')"),
353
+ limit: int = Field(default=50, description="Maximum number of learnings to return")
354
+ ) -> str:
355
+ """Get all learnings from a specific Ralph session.
356
+
357
+ This enables iteration-level memory: retrieve what was learned
358
+ in previous iterations of the SAME Ralph loop session.
359
+
360
+ Use this at the START of each Ralph iteration to see what previous
361
+ iterations learned. Then use store_reflection() with session tags
362
+ to save learnings at the END of each iteration."""
363
+ return await tools.get_session_learnings(ctx, session_id, limit)
364
+
253
365
  logger.info("Reflection tools registered successfully")
@@ -188,13 +188,15 @@ class CSRStandaloneClient:
188
188
  def store_reflection(
189
189
  self,
190
190
  content: str,
191
- tags: List[str] = None
191
+ tags: List[str] = None,
192
+ collection: str = None
192
193
  ) -> str:
193
194
  """Store a reflection/insight.
194
195
 
195
196
  Args:
196
197
  content: The reflection content
197
198
  tags: Optional tags for categorization
199
+ collection: Optional custom collection name (for hooks to use separate storage)
198
200
 
199
201
  Returns:
200
202
  ID of stored reflection
@@ -204,7 +206,11 @@ class CSRStandaloneClient:
204
206
  embeddings = self._get_embedding_manager()
205
207
 
206
208
  # Determine collection name
207
- collection_name = f"reflections_{'local' if self.prefer_local else 'voyage'}"
209
+ # Hooks can specify a custom collection to keep their data separate
210
+ if collection:
211
+ collection_name = collection
212
+ else:
213
+ collection_name = f"reflections_{'local' if self.prefer_local else 'voyage'}"
208
214
 
209
215
  # Ensure collection exists
210
216
  try:
@@ -271,6 +277,66 @@ class CSRStandaloneClient:
271
277
  normalized = re.sub(r'_+', '_', normalized)
272
278
  return normalized.strip('_')
273
279
 
280
+ def get_session_learnings(
281
+ self,
282
+ session_id: str,
283
+ limit: int = 50,
284
+ collection: str = None
285
+ ) -> List[Dict[str, Any]]:
286
+ """Get all learnings from a specific Ralph session.
287
+
288
+ This enables iteration-level memory: retrieve what was learned
289
+ in previous iterations of the SAME Ralph loop session.
290
+
291
+ Args:
292
+ session_id: The session ID (e.g., "ralph_20260104_224757_iter1")
293
+ limit: Maximum number of reflections to return
294
+ collection: Optional custom collection (for hook-stored data)
295
+
296
+ Returns:
297
+ List of reflection payloads from this session, each containing:
298
+ - content: The reflection text
299
+ - tags: List of tags (includes iteration info)
300
+ - timestamp: When it was stored
301
+ """
302
+ from qdrant_client import models
303
+
304
+ client = self._get_client()
305
+ if collection:
306
+ collection_name = collection
307
+ else:
308
+ collection_name = f"reflections_{'local' if self.prefer_local else 'voyage'}"
309
+
310
+ # Filter by session tag - matches reflections stored with session_{id} tag
311
+ session_filter = models.Filter(
312
+ must=[
313
+ models.FieldCondition(
314
+ key="tags",
315
+ match=models.MatchAny(any=[f"session_{session_id}"])
316
+ )
317
+ ]
318
+ )
319
+
320
+ try:
321
+ results, _ = client.scroll(
322
+ collection_name=collection_name,
323
+ scroll_filter=session_filter,
324
+ limit=limit,
325
+ with_payload=True,
326
+ )
327
+
328
+ return [
329
+ {
330
+ "content": point.payload.get("content", ""),
331
+ "tags": point.payload.get("tags", []),
332
+ "timestamp": point.payload.get("timestamp", ""),
333
+ }
334
+ for point in results
335
+ ]
336
+ except Exception as e:
337
+ logger.error(f"Error getting session learnings: {e}")
338
+ return []
339
+
274
340
  def test_connection(self) -> bool:
275
341
  """Test if CSR is accessible.
276
342
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-self-reflect",
3
- "version": "7.1.8",
3
+ "version": "7.1.9",
4
4
  "description": "Give Claude perfect memory of all your conversations - Installation wizard for Python MCP server",
5
5
  "keywords": [
6
6
  "claude",
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Ralph Iteration Hook - Fires at EACH iteration boundary via Stop hook.
4
+
5
+ This is the ONLY viable hook for iteration-level memory because:
6
+ - Stop hook fires after EACH Claude response (iteration boundary)
7
+ - SessionStart/SessionEnd fire once per terminal session (useless)
8
+
9
+ When triggered:
10
+ 1. Store learnings from THIS iteration (with session_id + iteration tag)
11
+ 2. Retrieve learnings from PREVIOUS iterations of same session
12
+ 3. Output context for next iteration
13
+
14
+ Input (stdin): JSON with transcript, etc.
15
+ Output (stdout): Context message injected into next iteration
16
+ """
17
+
18
+ import sys
19
+ import json
20
+ import logging
21
+ import re
22
+ from pathlib import Path
23
+ from datetime import datetime
24
+
25
+ # Add project root to path
26
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
27
+
28
+ from src.runtime.hooks.ralph_state import (
29
+ is_ralph_session,
30
+ load_ralph_session_state,
31
+ )
32
+
33
+ logging.basicConfig(level=logging.INFO)
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ def get_project_root() -> Path:
38
+ return Path(__file__).parent.parent.parent.parent
39
+
40
+
41
+ def store_iteration_learnings(state, iteration: int) -> bool:
42
+ """Store learnings from current iteration to CSR."""
43
+ try:
44
+ project_root = get_project_root()
45
+ mcp_server_path = project_root / "mcp-server" / "src"
46
+ if str(mcp_server_path) not in sys.path:
47
+ sys.path.insert(0, str(mcp_server_path))
48
+ from standalone_client import CSRStandaloneClient
49
+ client = CSRStandaloneClient()
50
+
51
+ # Get working directory (project name)
52
+ cwd = Path.cwd()
53
+ # SECURITY: Sanitize project name to prevent injection
54
+ project_name = re.sub(r'[^a-zA-Z0-9_-]', '_', cwd.name)
55
+
56
+ # Get learnings (may be empty)
57
+ learnings = getattr(state, 'learnings', [])
58
+
59
+ # Create iteration-specific content (store even if no learnings to track hook fired)
60
+ content = f"""# Iteration {iteration} (Session: {state.session_id})
61
+
62
+ ## Project
63
+ {project_name} ({cwd})
64
+
65
+ ## Task
66
+ {state.task[:200] if state.task else '(no task)'}
67
+
68
+ ## Learnings
69
+ {chr(10).join(f'- {l}' for l in learnings) if learnings else '(none this iteration)'}
70
+
71
+ ## Current Approach
72
+ {state.current_approach or '(not set)'}
73
+
74
+ ## Files Modified
75
+ {chr(10).join(f'- {f}' for f in state.files_modified) if state.files_modified else '(none)'}
76
+ """
77
+
78
+ # Store with iteration-specific tags including project
79
+ tags = [
80
+ "__csr_hook_auto__", # Hook signature
81
+ f"project_{project_name}",
82
+ f"session_{state.session_id}",
83
+ f"iteration_{iteration}",
84
+ "ralph_iteration"
85
+ ]
86
+
87
+ client.store_reflection(
88
+ content=content,
89
+ tags=tags,
90
+ collection="csr_hook_sessions_local"
91
+ )
92
+
93
+ logger.info(f"Stored iteration {iteration} for {project_name} session {state.session_id}")
94
+ return True
95
+
96
+ except Exception as e:
97
+ logger.error(f"Error storing iteration learnings: {e}")
98
+ return False
99
+
100
+
101
+ def retrieve_iteration_learnings(session_id: str, current_iteration: int) -> list:
102
+ """Retrieve learnings from PREVIOUS iterations of this session."""
103
+ try:
104
+ project_root = get_project_root()
105
+ mcp_server_path = project_root / "mcp-server" / "src"
106
+ if str(mcp_server_path) not in sys.path:
107
+ sys.path.insert(0, str(mcp_server_path))
108
+ from standalone_client import CSRStandaloneClient
109
+ client = CSRStandaloneClient()
110
+
111
+ # Get learnings from this session (use hook collection)
112
+ learnings = client.get_session_learnings(
113
+ session_id,
114
+ limit=20,
115
+ collection="csr_hook_sessions_local"
116
+ )
117
+
118
+ # Filter to only previous iterations
119
+ previous = [
120
+ l for l in learnings
121
+ if any(f"iteration_{i}" in l.get('tags', [])
122
+ for i in range(1, current_iteration))
123
+ ]
124
+
125
+ return previous
126
+
127
+ except Exception as e:
128
+ logger.error(f"Error retrieving iteration learnings: {e}")
129
+ return []
130
+
131
+
132
+ def format_iteration_context(learnings: list, current_iteration: int) -> str:
133
+ """Format previous iteration learnings for injection."""
134
+ if not learnings:
135
+ return ""
136
+
137
+ output = [f"# Previous Iteration Learnings (for Iteration {current_iteration})"]
138
+ output.append("")
139
+
140
+ for l in learnings[:5]: # Max 5 previous iterations
141
+ content = l.get('content', '')[:500]
142
+ tags = l.get('tags', [])
143
+ iter_tag = [t for t in tags if t.startswith('iteration_')]
144
+ iter_num = iter_tag[0].split('_')[1] if iter_tag else '?'
145
+ output.append(f"## From Iteration {iter_num}")
146
+ output.append(content)
147
+ output.append("")
148
+
149
+ return "\n".join(output)
150
+
151
+
152
+ def main():
153
+ """Main hook entry point - fires at each iteration boundary."""
154
+ # Read hook input (required by Claude Code hook framework, but we use Ralph state file instead)
155
+ # The stdin contains transcript data, but Ralph state file is the authoritative source
156
+ try:
157
+ _ = json.load(sys.stdin) # Consume stdin as required by hook protocol
158
+ except (json.JSONDecodeError, EOFError):
159
+ pass # No input is fine - we get state from .ralph_state.md
160
+
161
+ # Check if Ralph loop active
162
+ if not is_ralph_session():
163
+ logger.info("No Ralph session - iteration hook skipped")
164
+ sys.exit(0)
165
+
166
+ # Load state
167
+ state = load_ralph_session_state()
168
+ if not state:
169
+ logger.warning("Could not load Ralph state")
170
+ sys.exit(0)
171
+
172
+ iteration = state.iteration
173
+ session_id = state.session_id
174
+
175
+ logger.info(f"Iteration hook: session={session_id}, iteration={iteration}")
176
+
177
+ # 1. Store learnings from THIS iteration
178
+ store_iteration_learnings(state, iteration)
179
+
180
+ # 2. Retrieve learnings from PREVIOUS iterations
181
+ previous_learnings = retrieve_iteration_learnings(session_id, iteration)
182
+
183
+ # 3. Output context for NEXT iteration
184
+ if previous_learnings:
185
+ context = format_iteration_context(previous_learnings, iteration + 1)
186
+ # Write to file for Claude to read
187
+ context_path = Path('.ralph_iteration_context.md')
188
+ context_path.write_text(context)
189
+ print(f"# Loaded {len(previous_learnings)} learnings from previous iterations")
190
+ print(f"# See .ralph_iteration_context.md for details")
191
+ else:
192
+ logger.info("No previous iteration learnings found")
193
+
194
+
195
+ if __name__ == "__main__":
196
+ main()
@@ -119,6 +119,11 @@ class RalphState:
119
119
  score += 10
120
120
  self.exit_confidence = min(100, score)
121
121
 
122
+ def increment_iteration(self) -> None:
123
+ """Increment iteration count and update timestamp."""
124
+ self.iteration += 1
125
+ self.updated_at = datetime.now().isoformat()
126
+
122
127
  def to_markdown(self) -> str:
123
128
  """Convert state to markdown format for .ralph_state.md"""
124
129
  # Format error signatures for display
@@ -141,7 +141,10 @@ Promise Met: {state.completion_promise_met}
141
141
  """
142
142
 
143
143
  # Store with outcome-aware tags
144
+ # IMPORTANT: __csr_hook_auto__ is a distinct signature that identifies
145
+ # hook-stored reflections vs manually-stored ones during development
144
146
  tags = [
147
+ "__csr_hook_auto__", # Hook signature - don't manually use this tag!
145
148
  "ralph_session",
146
149
  f"session_{state.session_id}",
147
150
  f"outcome_{outcome.lower()}",
@@ -165,7 +168,12 @@ Promise Met: {state.completion_promise_met}
165
168
 
166
169
  # Note: CSR store_reflection may not support metadata yet,
167
170
  # but we include the rich info in the narrative for searchability
168
- client.store_reflection(content=narrative, tags=tags)
171
+ # Use hook-specific collection so random agents don't pollute it
172
+ client.store_reflection(
173
+ content=narrative,
174
+ tags=tags,
175
+ collection="csr_hook_sessions_local" # Separate from user reflections
176
+ )
169
177
 
170
178
  logger.info(f"Stored session narrative: {outcome}, {state.iteration} iterations, confidence={exit_confidence}%")
171
179
 
@@ -179,7 +187,8 @@ Iterations: {state.iteration}
179
187
  """
180
188
  client.store_reflection(
181
189
  content=success_summary,
182
- tags=["ralph_success", "winning_strategy", f"work_type_{work_type.lower()}"]
190
+ tags=["__csr_hook_auto__", "ralph_success", "winning_strategy", f"work_type_{work_type.lower()}"],
191
+ collection="csr_hook_sessions_local"
183
192
  )
184
193
 
185
194
  return True
@@ -9,6 +9,13 @@ DEFAULT_PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
9
9
 
10
10
  # Configuration (allows override via environment)
11
11
  CLAUDE_REFLECT_DIR="${CLAUDE_REFLECT_DIR:-$DEFAULT_PROJECT_ROOT}"
12
+
13
+ # SECURITY: Validate CLAUDE_REFLECT_DIR to prevent directory traversal
14
+ if [[ "$CLAUDE_REFLECT_DIR" =~ \.\. ]]; then
15
+ echo "Error: CLAUDE_REFLECT_DIR contains directory traversal" >&2
16
+ exit 0 # Exit gracefully, don't block compacting
17
+ fi
18
+
12
19
  # Support both .venv and venv directories
13
20
  if [ -d "$CLAUDE_REFLECT_DIR/.venv" ]; then
14
21
  VENV_PATH="${VENV_PATH:-$CLAUDE_REFLECT_DIR/.venv}"
@@ -31,6 +38,17 @@ if [ ! -d "$VENV_PATH" ]; then
31
38
  exit 0 # Exit gracefully
32
39
  fi
33
40
 
41
+ # SECURITY: Validate VENV_PATH to prevent command injection
42
+ if [[ ! "$VENV_PATH" =~ ^[a-zA-Z0-9/_.-]+$ ]]; then
43
+ echo "Error: Invalid VENV_PATH contains unsafe characters" >&2
44
+ exit 0 # Exit gracefully, don't block compacting
45
+ fi
46
+
47
+ if [ ! -x "$VENV_PATH/bin/python3" ]; then
48
+ echo "Error: Python not found at $VENV_PATH/bin/python3" >&2
49
+ exit 0 # Exit gracefully
50
+ fi
51
+
34
52
  # Run quick import with timeout
35
53
  echo "Updating conversation memory..." >&2
36
54
  timeout $IMPORT_TIMEOUT bash -c "
@@ -57,7 +75,7 @@ fi
57
75
  if [ -n "$RALPH_STATE_FILE" ]; then
58
76
  echo "📝 Backing up Ralph state to CSR..." >&2
59
77
 
60
- RALPH_STATE_FILE="$RALPH_STATE_FILE" CLAUDE_REFLECT_DIR="$CLAUDE_REFLECT_DIR" python3 << 'PYTHON' 2>/dev/null || echo "Warning: Could not backup Ralph state" >&2
78
+ RALPH_STATE_FILE="$RALPH_STATE_FILE" CLAUDE_REFLECT_DIR="$CLAUDE_REFLECT_DIR" "$VENV_PATH/bin/python3" << 'PYTHON' || echo "Warning: Could not backup Ralph state" >&2
61
79
  import sys
62
80
  import os
63
81
  from pathlib import Path
@@ -68,17 +86,21 @@ project_root = os.environ.get('CLAUDE_REFLECT_DIR', str(Path.home() / 'claude-se
68
86
  sys.path.insert(0, str(Path(project_root) / 'mcp-server' / 'src'))
69
87
 
70
88
  try:
89
+ import re
71
90
  from standalone_client import CSRStandaloneClient
72
91
 
73
92
  ralph_state_file = os.environ.get('RALPH_STATE_FILE', '.ralph_state.md')
74
93
  state_content = Path(ralph_state_file).read_text()
94
+ # SECURITY: Sanitize project name to prevent injection
95
+ project_name = re.sub(r'[^a-zA-Z0-9_-]', '_', Path.cwd().name)
75
96
  client = CSRStandaloneClient()
76
97
 
77
98
  client.store_reflection(
78
- content=f"Pre-compaction Ralph state backup ({datetime.now().isoformat()}):\n\n{state_content}",
79
- tags=["ralph_state", "pre_compact_backup"]
99
+ content=f"Pre-compaction Ralph state backup ({datetime.now().isoformat()}):\n\nProject: {project_name}\n\n{state_content}",
100
+ tags=["__csr_hook_auto__", f"project_{project_name}", "ralph_state", "pre_compact_backup"],
101
+ collection="csr_hook_sessions_local"
80
102
  )
81
- print("✅ Ralph state backed up to CSR", file=sys.stderr)
103
+ print(f"✅ Ralph state ({project_name}) backed up to CSR", file=sys.stderr)
82
104
  except ImportError:
83
105
  print("CSR client not available, skipping backup", file=sys.stderr)
84
106
  except Exception as e: