claude-self-reflect 7.0.0 → 7.1.8
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/.claude/agents/csr-validator.md +87 -1
- package/.env.example +15 -0
- package/README.md +59 -0
- package/docker-compose.yaml +18 -9
- package/installer/setup-wizard-docker.js +87 -6
- package/installer/update-manager.js +88 -1
- package/mcp-server/src/standalone_client.py +314 -0
- package/package.json +1 -1
- package/src/runtime/hooks/__init__.py +21 -0
- package/src/runtime/hooks/ralph_state.py +397 -0
- package/src/runtime/hooks/session_end_hook.py +245 -0
- package/src/runtime/hooks/session_start_hook.py +259 -0
- package/src/runtime/precompact-hook.sh +60 -3
- package/src/runtime/unified_state_manager.py +35 -10
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Ralph SessionStart Hook - Searches CSR for relevant past sessions.
|
|
4
|
+
|
|
5
|
+
Triggered at session start. Uses existing CSR infrastructure to search
|
|
6
|
+
for relevant past Ralph sessions and injects context.
|
|
7
|
+
|
|
8
|
+
Enhanced Features (v7.1+):
|
|
9
|
+
- Error-centric search (find solutions to current blockers)
|
|
10
|
+
- Anti-pattern injection (surface failed approaches first)
|
|
11
|
+
- Winning strategy prioritization
|
|
12
|
+
- Multi-signal search (task + errors + patterns)
|
|
13
|
+
|
|
14
|
+
Input (stdin): JSON with session_id, transcript_path, source
|
|
15
|
+
Output (stdout): Context message for Claude
|
|
16
|
+
Exit code: 0 = success, 2 = blocking error
|
|
17
|
+
|
|
18
|
+
Attribution:
|
|
19
|
+
Search patterns inspired by https://github.com/frankbria/ralph-claude-code
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import sys
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
|
|
28
|
+
# Add project root to path
|
|
29
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
|
30
|
+
|
|
31
|
+
from src.runtime.hooks.ralph_state import (
|
|
32
|
+
load_state,
|
|
33
|
+
save_state,
|
|
34
|
+
RalphState,
|
|
35
|
+
is_ralph_session,
|
|
36
|
+
load_ralph_session_state,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
logging.basicConfig(level=logging.INFO)
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_project_root() -> Path:
|
|
44
|
+
"""Dynamically determine project root (works for any installation)."""
|
|
45
|
+
# This file is at: <project_root>/src/runtime/hooks/session_start_hook.py
|
|
46
|
+
return Path(__file__).parent.parent.parent.parent
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _error_signature(error: str) -> str:
|
|
50
|
+
"""Extract error signature for deduplication (matches RalphState method)."""
|
|
51
|
+
import re
|
|
52
|
+
sig = re.sub(r'line \d+', 'line N', error)
|
|
53
|
+
sig = re.sub(r'/[\w/.-]+/', '/.../', sig)
|
|
54
|
+
sig = re.sub(r'\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}', 'TIMESTAMP', sig)
|
|
55
|
+
return sig[:100]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def search_past_sessions(task: str, errors: list = None, limit: int = 3) -> dict:
|
|
59
|
+
"""Enhanced search: task + errors + anti-patterns."""
|
|
60
|
+
results = {
|
|
61
|
+
'similar_tasks': [],
|
|
62
|
+
'similar_errors': [],
|
|
63
|
+
'anti_patterns': [],
|
|
64
|
+
'winning_strategies': []
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
# Import CSR standalone client (dynamic path for any installation)
|
|
69
|
+
project_root = get_project_root()
|
|
70
|
+
mcp_server_path = project_root / "mcp-server" / "src"
|
|
71
|
+
if str(mcp_server_path) not in sys.path:
|
|
72
|
+
sys.path.insert(0, str(mcp_server_path))
|
|
73
|
+
from standalone_client import CSRStandaloneClient
|
|
74
|
+
client = CSRStandaloneClient()
|
|
75
|
+
|
|
76
|
+
# 1. Task-based search (existing behavior)
|
|
77
|
+
task_results = client.search(
|
|
78
|
+
query=f"ralph session: {task}",
|
|
79
|
+
limit=2,
|
|
80
|
+
min_score=0.5
|
|
81
|
+
)
|
|
82
|
+
results['similar_tasks'] = task_results
|
|
83
|
+
|
|
84
|
+
# 2. NEW: Error-based search (if we have current errors)
|
|
85
|
+
if errors:
|
|
86
|
+
for error in errors[:2]: # Top 2 errors only
|
|
87
|
+
sig = _error_signature(error)
|
|
88
|
+
error_results = client.search(
|
|
89
|
+
query=f"error blocked solved: {sig}",
|
|
90
|
+
limit=1,
|
|
91
|
+
min_score=0.6
|
|
92
|
+
)
|
|
93
|
+
results['similar_errors'].extend(error_results)
|
|
94
|
+
|
|
95
|
+
# 3. NEW: Anti-patterns search (failed approaches from incomplete sessions)
|
|
96
|
+
anti_results = client.search(
|
|
97
|
+
query=f"failed approach don't retry: {task}",
|
|
98
|
+
limit=2,
|
|
99
|
+
min_score=0.5
|
|
100
|
+
)
|
|
101
|
+
# Filter for incomplete/abandoned sessions
|
|
102
|
+
results['anti_patterns'] = [
|
|
103
|
+
r for r in anti_results
|
|
104
|
+
if r.get('metadata', {}).get('outcome') in ('INCOMPLETE', 'ABANDONED')
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
# 4. NEW: Winning strategies (successful sessions)
|
|
108
|
+
winners = client.search(
|
|
109
|
+
query=f"successful solution completed: {task}",
|
|
110
|
+
limit=1,
|
|
111
|
+
min_score=0.6
|
|
112
|
+
)
|
|
113
|
+
results['winning_strategies'] = [
|
|
114
|
+
r for r in winners
|
|
115
|
+
if r.get('metadata', {}).get('outcome') == 'COMPLETED'
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
return results
|
|
119
|
+
|
|
120
|
+
except ImportError:
|
|
121
|
+
logger.warning("CSR standalone client not available, skipping memory search")
|
|
122
|
+
return results
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.error(f"Error searching CSR: {e}")
|
|
125
|
+
return results
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def format_past_sessions(results: dict) -> str:
|
|
129
|
+
"""Format search results with anti-patterns FIRST for fast loop efficiency."""
|
|
130
|
+
# Handle legacy list format
|
|
131
|
+
if isinstance(results, list):
|
|
132
|
+
results = {'similar_tasks': results}
|
|
133
|
+
|
|
134
|
+
has_content = any([
|
|
135
|
+
results.get('anti_patterns'),
|
|
136
|
+
results.get('winning_strategies'),
|
|
137
|
+
results.get('similar_errors'),
|
|
138
|
+
results.get('similar_tasks')
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
if not has_content:
|
|
142
|
+
return ""
|
|
143
|
+
|
|
144
|
+
output = ["# Ralph Memory (from CSR)\n"]
|
|
145
|
+
output.append("Use these insights to avoid repeating mistakes.\n")
|
|
146
|
+
|
|
147
|
+
# 1. ANTI-PATTERNS FIRST (most important for fast loops)
|
|
148
|
+
if results.get('anti_patterns'):
|
|
149
|
+
output.append("## DON'T RETRY THESE")
|
|
150
|
+
for r in results['anti_patterns']:
|
|
151
|
+
# Extract failed approaches from metadata
|
|
152
|
+
failed = r.get('metadata', {}).get('failed_approaches', [])
|
|
153
|
+
if failed:
|
|
154
|
+
for f in failed[:3]:
|
|
155
|
+
output.append(f"- {f}")
|
|
156
|
+
else:
|
|
157
|
+
# Fallback to content preview
|
|
158
|
+
content = r.get('content', r.get('preview', ''))[:150]
|
|
159
|
+
output.append(f"- {content}...")
|
|
160
|
+
|
|
161
|
+
# 2. WINNING STRATEGIES (proven approaches)
|
|
162
|
+
if results.get('winning_strategies'):
|
|
163
|
+
output.append("\n## PROVEN APPROACHES")
|
|
164
|
+
for r in results['winning_strategies']:
|
|
165
|
+
strategies = r.get('metadata', {}).get('successful_strategies', [])
|
|
166
|
+
if strategies:
|
|
167
|
+
for s in strategies[:3]:
|
|
168
|
+
output.append(f"- {s}")
|
|
169
|
+
else:
|
|
170
|
+
content = r.get('content', r.get('preview', ''))[:150]
|
|
171
|
+
output.append(f"- {content}...")
|
|
172
|
+
|
|
173
|
+
# 3. PAST ERROR SOLUTIONS
|
|
174
|
+
if results.get('similar_errors'):
|
|
175
|
+
output.append("\n## PAST ERROR SOLUTIONS")
|
|
176
|
+
for r in results['similar_errors']:
|
|
177
|
+
score = r.get('score', 0)
|
|
178
|
+
content = r.get('content', r.get('preview', ''))[:200]
|
|
179
|
+
output.append(f"- (score: {score:.2f}) {content}...")
|
|
180
|
+
|
|
181
|
+
# 4. SIMILAR TASKS (context, less actionable)
|
|
182
|
+
if results.get('similar_tasks'):
|
|
183
|
+
output.append("\n## RELATED SESSIONS")
|
|
184
|
+
for i, r in enumerate(results['similar_tasks'][:2], 1):
|
|
185
|
+
score = r.get('score', 0)
|
|
186
|
+
outcome = r.get('metadata', {}).get('outcome', 'Unknown')
|
|
187
|
+
content = r.get('content', r.get('preview', ''))[:150]
|
|
188
|
+
output.append(f"- [{outcome}] (score: {score:.2f}) {content}...")
|
|
189
|
+
|
|
190
|
+
# Extract key learnings
|
|
191
|
+
if learnings := r.get('metadata', {}).get('learnings'):
|
|
192
|
+
for learning in learnings[:2]:
|
|
193
|
+
output.append(f" - Learning: {learning}")
|
|
194
|
+
|
|
195
|
+
return "\n".join(output)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def main():
|
|
199
|
+
"""Main hook entry point."""
|
|
200
|
+
# Read hook input from stdin (official protocol)
|
|
201
|
+
try:
|
|
202
|
+
input_data = json.load(sys.stdin)
|
|
203
|
+
except (json.JSONDecodeError, EOFError):
|
|
204
|
+
input_data = {}
|
|
205
|
+
|
|
206
|
+
session_id = input_data.get('session_id', '')
|
|
207
|
+
source = input_data.get('source', 'startup')
|
|
208
|
+
|
|
209
|
+
logger.info(f"SessionStart hook triggered: source={source}, session_id={session_id[:20] if session_id else 'none'}...")
|
|
210
|
+
|
|
211
|
+
# Check if this is a Ralph session
|
|
212
|
+
if not is_ralph_session():
|
|
213
|
+
logger.info("No Ralph session detected (checked .claude/ralph-loop.local.md and .ralph_state.md)")
|
|
214
|
+
sys.exit(0)
|
|
215
|
+
|
|
216
|
+
# Load current state (supports both ralph-wiggum and custom formats)
|
|
217
|
+
state = load_ralph_session_state()
|
|
218
|
+
if not state:
|
|
219
|
+
logger.warning("Could not load Ralph state")
|
|
220
|
+
sys.exit(0)
|
|
221
|
+
|
|
222
|
+
# Search for past sessions with similar task AND current errors
|
|
223
|
+
logger.info(f"Searching CSR for past sessions related to: {state.task[:50]}...")
|
|
224
|
+
results = search_past_sessions(
|
|
225
|
+
task=state.task,
|
|
226
|
+
errors=state.blocking_errors if hasattr(state, 'blocking_errors') else None
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Count total results
|
|
230
|
+
total_results = sum([
|
|
231
|
+
len(results.get('similar_tasks', [])),
|
|
232
|
+
len(results.get('similar_errors', [])),
|
|
233
|
+
len(results.get('anti_patterns', [])),
|
|
234
|
+
len(results.get('winning_strategies', []))
|
|
235
|
+
])
|
|
236
|
+
|
|
237
|
+
if total_results > 0:
|
|
238
|
+
# Write context file for Claude to read
|
|
239
|
+
context = format_past_sessions(results)
|
|
240
|
+
past_sessions_path = Path('.ralph_past_sessions.md')
|
|
241
|
+
past_sessions_path.write_text(context)
|
|
242
|
+
|
|
243
|
+
# Log breakdown
|
|
244
|
+
logger.info(f"Found {total_results} relevant results:")
|
|
245
|
+
logger.info(f" - Anti-patterns: {len(results.get('anti_patterns', []))}")
|
|
246
|
+
logger.info(f" - Winning strategies: {len(results.get('winning_strategies', []))}")
|
|
247
|
+
logger.info(f" - Error matches: {len(results.get('similar_errors', []))}")
|
|
248
|
+
logger.info(f" - Similar tasks: {len(results.get('similar_tasks', []))}")
|
|
249
|
+
|
|
250
|
+
print(f"# Loaded {total_results} relevant past sessions")
|
|
251
|
+
print(f"# See .ralph_past_sessions.md for details")
|
|
252
|
+
else:
|
|
253
|
+
logger.info("No relevant past sessions found")
|
|
254
|
+
|
|
255
|
+
sys.exit(0)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == '__main__':
|
|
259
|
+
main()
|
|
@@ -2,9 +2,21 @@
|
|
|
2
2
|
# PreCompact hook for Claude Self-Reflect
|
|
3
3
|
# Place this in ~/.claude/hooks/precompact or source it from there
|
|
4
4
|
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
# Determine project root dynamically from this script's location
|
|
6
|
+
# This file is at: <project_root>/src/runtime/precompact-hook.sh
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
DEFAULT_PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
9
|
+
|
|
10
|
+
# Configuration (allows override via environment)
|
|
11
|
+
CLAUDE_REFLECT_DIR="${CLAUDE_REFLECT_DIR:-$DEFAULT_PROJECT_ROOT}"
|
|
12
|
+
# Support both .venv and venv directories
|
|
13
|
+
if [ -d "$CLAUDE_REFLECT_DIR/.venv" ]; then
|
|
14
|
+
VENV_PATH="${VENV_PATH:-$CLAUDE_REFLECT_DIR/.venv}"
|
|
15
|
+
elif [ -d "$CLAUDE_REFLECT_DIR/venv" ]; then
|
|
16
|
+
VENV_PATH="${VENV_PATH:-$CLAUDE_REFLECT_DIR/venv}"
|
|
17
|
+
else
|
|
18
|
+
VENV_PATH="${VENV_PATH:-$CLAUDE_REFLECT_DIR/.venv}"
|
|
19
|
+
fi
|
|
8
20
|
IMPORT_TIMEOUT="${IMPORT_TIMEOUT:-30}"
|
|
9
21
|
|
|
10
22
|
# Check if Claude Self-Reflect is installed
|
|
@@ -29,5 +41,50 @@ timeout $IMPORT_TIMEOUT bash -c "
|
|
|
29
41
|
echo "Quick import timed out after ${IMPORT_TIMEOUT}s" >&2
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
# ============================================================
|
|
45
|
+
# RALPH MEMORY INTEGRATION (Added for Memory-Augmented Ralph)
|
|
46
|
+
# ============================================================
|
|
47
|
+
|
|
48
|
+
# If Ralph session, backup state to CSR before compaction
|
|
49
|
+
# Check both ralph-wiggum format and custom format
|
|
50
|
+
RALPH_STATE_FILE=""
|
|
51
|
+
if [ -f ".claude/ralph-loop.local.md" ]; then
|
|
52
|
+
RALPH_STATE_FILE=".claude/ralph-loop.local.md"
|
|
53
|
+
elif [ -f ".ralph_state.md" ]; then
|
|
54
|
+
RALPH_STATE_FILE=".ralph_state.md"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [ -n "$RALPH_STATE_FILE" ]; then
|
|
58
|
+
echo "📝 Backing up Ralph state to CSR..." >&2
|
|
59
|
+
|
|
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
|
|
61
|
+
import sys
|
|
62
|
+
import os
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
from datetime import datetime
|
|
65
|
+
|
|
66
|
+
# Dynamic path resolution
|
|
67
|
+
project_root = os.environ.get('CLAUDE_REFLECT_DIR', str(Path.home() / 'claude-self-reflect'))
|
|
68
|
+
sys.path.insert(0, str(Path(project_root) / 'mcp-server' / 'src'))
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
from standalone_client import CSRStandaloneClient
|
|
72
|
+
|
|
73
|
+
ralph_state_file = os.environ.get('RALPH_STATE_FILE', '.ralph_state.md')
|
|
74
|
+
state_content = Path(ralph_state_file).read_text()
|
|
75
|
+
client = CSRStandaloneClient()
|
|
76
|
+
|
|
77
|
+
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"]
|
|
80
|
+
)
|
|
81
|
+
print("✅ Ralph state backed up to CSR", file=sys.stderr)
|
|
82
|
+
except ImportError:
|
|
83
|
+
print("CSR client not available, skipping backup", file=sys.stderr)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Backup failed: {e}", file=sys.stderr)
|
|
86
|
+
PYTHON
|
|
87
|
+
fi
|
|
88
|
+
|
|
32
89
|
# Always exit successfully to not block compacting
|
|
33
90
|
exit 0
|
|
@@ -336,6 +336,10 @@ class UnifiedStateManager:
|
|
|
336
336
|
except Exception as e:
|
|
337
337
|
raise ValueError(f"Invalid path: {file_path}: {e}")
|
|
338
338
|
|
|
339
|
+
# Detect if running in Docker by checking for Docker-mounted paths
|
|
340
|
+
docker_paths = ["/logs", "/config", "/app/data"]
|
|
341
|
+
in_docker = any(Path(dp).exists() for dp in docker_paths)
|
|
342
|
+
|
|
339
343
|
# Docker to local path mappings
|
|
340
344
|
path_mappings = [
|
|
341
345
|
("/logs/", "/.claude/projects/"),
|
|
@@ -343,8 +347,15 @@ class UnifiedStateManager:
|
|
|
343
347
|
("/app/data/", "/.claude/projects/")
|
|
344
348
|
]
|
|
345
349
|
|
|
346
|
-
#
|
|
350
|
+
# In Docker: validate against Docker paths BEFORE transformation
|
|
351
|
+
# This ensures security validation uses paths that actually exist
|
|
347
352
|
path_str = str(resolved)
|
|
353
|
+
original_path_str = path_str # Keep original for Docker validation
|
|
354
|
+
|
|
355
|
+
# Check if this is a Docker path that we should validate directly
|
|
356
|
+
is_docker_path = in_docker and any(path_str.startswith(dp) for dp in docker_paths)
|
|
357
|
+
|
|
358
|
+
# Apply Docker mappings for storage consistency (not for validation)
|
|
348
359
|
for docker_path, local_path in path_mappings:
|
|
349
360
|
if path_str.startswith(docker_path):
|
|
350
361
|
home = str(Path.home())
|
|
@@ -359,21 +370,35 @@ class UnifiedStateManager:
|
|
|
359
370
|
]
|
|
360
371
|
|
|
361
372
|
# Add Docker paths if they exist
|
|
362
|
-
for docker_path in
|
|
373
|
+
for docker_path in docker_paths:
|
|
363
374
|
docker_base = Path(docker_path)
|
|
364
375
|
if docker_base.exists():
|
|
365
376
|
allowed_bases.append(docker_base)
|
|
366
377
|
|
|
367
378
|
# Check if path is within allowed directories
|
|
368
379
|
path_allowed = False
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
380
|
+
|
|
381
|
+
# In Docker: validate original path against Docker mounts
|
|
382
|
+
if is_docker_path:
|
|
383
|
+
original_resolved = Path(original_path_str).resolve()
|
|
384
|
+
for base in allowed_bases:
|
|
385
|
+
try:
|
|
386
|
+
if base.exists():
|
|
387
|
+
original_resolved.relative_to(base)
|
|
388
|
+
path_allowed = True
|
|
389
|
+
break
|
|
390
|
+
except ValueError:
|
|
391
|
+
continue
|
|
392
|
+
else:
|
|
393
|
+
# Local environment: validate transformed path
|
|
394
|
+
for base in allowed_bases:
|
|
395
|
+
try:
|
|
396
|
+
if base.exists():
|
|
397
|
+
resolved.relative_to(base)
|
|
398
|
+
path_allowed = True
|
|
399
|
+
break
|
|
400
|
+
except ValueError:
|
|
401
|
+
continue
|
|
377
402
|
|
|
378
403
|
# Allow test paths when running tests
|
|
379
404
|
if not path_allowed:
|