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 +38 -28
- package/mcp-server/src/reflection_tools.py +114 -2
- package/mcp-server/src/standalone_client.py +68 -2
- package/package.json +1 -1
- package/src/runtime/hooks/iteration_hook.py +196 -0
- package/src/runtime/hooks/ralph_state.py +5 -0
- package/src/runtime/hooks/session_end_hook.py +11 -2
- package/src/runtime/precompact-hook.sh +26 -4
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.
|
|
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)
|
|
345
|
-
- **Output Decline Detection** - Circuit breaker pattern
|
|
346
|
-
- **Confidence-Based Exit** - 0-100 scoring
|
|
347
|
-
- **Anti-Pattern Injection** - "DON'T RETRY THESE"
|
|
348
|
-
- **Work Type Tracking** -
|
|
349
|
-
- **Error-Centric Search** -
|
|
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.
|
|
361
|
-
4.
|
|
362
|
-
5.
|
|
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
|
-
**
|
|
365
|
-
|
|
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-
|
|
379
|
+
**Verified proof (2026-01-05):**
|
|
368
380
|
```
|
|
369
|
-
#
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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'
|
|
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:
|