claude-self-reflect 7.1.8 → 7.1.10
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 +2 -1
- package/scripts/ralph/backup_and_restore.sh +309 -0
- package/scripts/ralph/install_hooks.sh +244 -0
- package/scripts/ralph/test_with_rollback.sh +195 -0
- 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
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# Ralph Memory Integration - Hook Installation Script
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Installs Ralph memory hooks into Claude Code's hook system.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# ./install_hooks.sh # Install hooks
|
|
9
|
+
# ./install_hooks.sh --check # Check installation status
|
|
10
|
+
# ./install_hooks.sh --remove # Remove hooks
|
|
11
|
+
# =============================================================================
|
|
12
|
+
|
|
13
|
+
set -e
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
17
|
+
CLAUDE_HOOKS_DIR="$HOME/.claude/hooks"
|
|
18
|
+
CLAUDE_SETTINGS="$HOME/.claude/settings.json"
|
|
19
|
+
|
|
20
|
+
# Colors
|
|
21
|
+
RED='\033[0;31m'
|
|
22
|
+
GREEN='\033[0;32m'
|
|
23
|
+
YELLOW='\033[1;33m'
|
|
24
|
+
NC='\033[0m'
|
|
25
|
+
|
|
26
|
+
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
27
|
+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
28
|
+
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
29
|
+
|
|
30
|
+
check_installation() {
|
|
31
|
+
echo "=========================================="
|
|
32
|
+
echo "Ralph Memory Integration - Status Check"
|
|
33
|
+
echo "=========================================="
|
|
34
|
+
|
|
35
|
+
local all_ok=true
|
|
36
|
+
|
|
37
|
+
# Check hooks directory
|
|
38
|
+
if [ -d "$CLAUDE_HOOKS_DIR" ]; then
|
|
39
|
+
log_info "✓ Hooks directory exists: $CLAUDE_HOOKS_DIR"
|
|
40
|
+
else
|
|
41
|
+
log_warn "○ Hooks directory not found: $CLAUDE_HOOKS_DIR"
|
|
42
|
+
all_ok=false
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Check for our hooks symlinks or copies
|
|
46
|
+
if [ -f "$CLAUDE_HOOKS_DIR/ralph-session-start.py" ] || [ -L "$CLAUDE_HOOKS_DIR/ralph-session-start.py" ]; then
|
|
47
|
+
log_info "✓ SessionStart hook installed"
|
|
48
|
+
else
|
|
49
|
+
log_warn "○ SessionStart hook not installed"
|
|
50
|
+
all_ok=false
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if [ -f "$CLAUDE_HOOKS_DIR/ralph-session-end.py" ] || [ -L "$CLAUDE_HOOKS_DIR/ralph-session-end.py" ]; then
|
|
54
|
+
log_info "✓ SessionEnd hook installed"
|
|
55
|
+
else
|
|
56
|
+
log_warn "○ SessionEnd hook not installed"
|
|
57
|
+
all_ok=false
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Check settings.json for hook configuration
|
|
61
|
+
if [ -f "$CLAUDE_SETTINGS" ]; then
|
|
62
|
+
if grep -q "ralph" "$CLAUDE_SETTINGS" 2>/dev/null; then
|
|
63
|
+
log_info "✓ Settings.json contains Ralph configuration"
|
|
64
|
+
else
|
|
65
|
+
log_warn "○ Settings.json does not contain Ralph configuration"
|
|
66
|
+
all_ok=false
|
|
67
|
+
fi
|
|
68
|
+
else
|
|
69
|
+
log_warn "○ Settings.json not found"
|
|
70
|
+
all_ok=false
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Check source hooks exist
|
|
74
|
+
if [ -f "$PROJECT_ROOT/src/runtime/hooks/session_start_hook.py" ]; then
|
|
75
|
+
log_info "✓ Source hooks available"
|
|
76
|
+
else
|
|
77
|
+
log_error "✗ Source hooks missing!"
|
|
78
|
+
all_ok=false
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
echo ""
|
|
82
|
+
if $all_ok; then
|
|
83
|
+
log_info "All hooks properly installed"
|
|
84
|
+
else
|
|
85
|
+
log_warn "Some hooks are missing. Run: $0 to install"
|
|
86
|
+
fi
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
install_hooks() {
|
|
90
|
+
echo "=========================================="
|
|
91
|
+
echo "Ralph Memory Integration - Installing"
|
|
92
|
+
echo "=========================================="
|
|
93
|
+
|
|
94
|
+
# Create hooks directory if needed
|
|
95
|
+
mkdir -p "$CLAUDE_HOOKS_DIR"
|
|
96
|
+
log_info "Created hooks directory: $CLAUDE_HOOKS_DIR"
|
|
97
|
+
|
|
98
|
+
# Create symlinks to our hooks
|
|
99
|
+
ln -sf "$PROJECT_ROOT/src/runtime/hooks/session_start_hook.py" "$CLAUDE_HOOKS_DIR/ralph-session-start.py"
|
|
100
|
+
ln -sf "$PROJECT_ROOT/src/runtime/hooks/session_end_hook.py" "$CLAUDE_HOOKS_DIR/ralph-session-end.py"
|
|
101
|
+
log_info "Created hook symlinks"
|
|
102
|
+
|
|
103
|
+
# Create or update settings.json with hook configuration
|
|
104
|
+
if [ ! -f "$CLAUDE_SETTINGS" ]; then
|
|
105
|
+
echo '{}' > "$CLAUDE_SETTINGS"
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Use Python to safely merge hook configuration
|
|
109
|
+
python3 << PYTHON
|
|
110
|
+
import json
|
|
111
|
+
from pathlib import Path
|
|
112
|
+
|
|
113
|
+
settings_path = Path("$CLAUDE_SETTINGS")
|
|
114
|
+
project_root = "$PROJECT_ROOT"
|
|
115
|
+
|
|
116
|
+
# Load existing settings
|
|
117
|
+
try:
|
|
118
|
+
settings = json.loads(settings_path.read_text())
|
|
119
|
+
except:
|
|
120
|
+
settings = {}
|
|
121
|
+
|
|
122
|
+
# Ensure hooks section exists
|
|
123
|
+
if 'hooks' not in settings:
|
|
124
|
+
settings['hooks'] = {}
|
|
125
|
+
|
|
126
|
+
# Add Ralph hooks if not present
|
|
127
|
+
ralph_hooks = {
|
|
128
|
+
"SessionStart": [{
|
|
129
|
+
"matcher": "startup|resume",
|
|
130
|
+
"hooks": [{
|
|
131
|
+
"type": "command",
|
|
132
|
+
"command": f"{project_root}/venv/bin/python3 {project_root}/src/runtime/hooks/session_start_hook.py 2>/dev/null || true"
|
|
133
|
+
}]
|
|
134
|
+
}],
|
|
135
|
+
"SessionEnd": [{
|
|
136
|
+
"hooks": [{
|
|
137
|
+
"type": "command",
|
|
138
|
+
"command": f"{project_root}/venv/bin/python3 {project_root}/src/runtime/hooks/session_end_hook.py 2>/dev/null || true"
|
|
139
|
+
}]
|
|
140
|
+
}]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Merge (don't overwrite existing hooks)
|
|
144
|
+
for hook_type, hook_configs in ralph_hooks.items():
|
|
145
|
+
if hook_type not in settings['hooks']:
|
|
146
|
+
settings['hooks'][hook_type] = []
|
|
147
|
+
|
|
148
|
+
# Check if Ralph hook already exists
|
|
149
|
+
existing = settings['hooks'][hook_type]
|
|
150
|
+
ralph_cmd = f"{project_root}/src/runtime/hooks"
|
|
151
|
+
|
|
152
|
+
has_ralph = any(
|
|
153
|
+
ralph_cmd in str(h.get('hooks', []))
|
|
154
|
+
for h in existing
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if not has_ralph:
|
|
158
|
+
settings['hooks'][hook_type].extend(hook_configs)
|
|
159
|
+
print(f" Added {hook_type} hook")
|
|
160
|
+
else:
|
|
161
|
+
print(f" {hook_type} hook already exists")
|
|
162
|
+
|
|
163
|
+
# Write back
|
|
164
|
+
settings_path.write_text(json.dumps(settings, indent=2))
|
|
165
|
+
print("Settings updated successfully")
|
|
166
|
+
PYTHON
|
|
167
|
+
|
|
168
|
+
log_info "Hook configuration added to settings.json"
|
|
169
|
+
|
|
170
|
+
echo ""
|
|
171
|
+
log_info "Installation complete!"
|
|
172
|
+
echo ""
|
|
173
|
+
echo "To verify: $0 --check"
|
|
174
|
+
echo ""
|
|
175
|
+
echo "NOTE: The hooks will activate when:"
|
|
176
|
+
echo " 1. You start a Ralph loop with /ralph-wiggum:ralph-loop"
|
|
177
|
+
echo " 2. The hooks detect .claude/ralph-loop.local.md"
|
|
178
|
+
echo " 3. Session events (start/end) trigger memory operations"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
remove_hooks() {
|
|
182
|
+
echo "=========================================="
|
|
183
|
+
echo "Ralph Memory Integration - Removing"
|
|
184
|
+
echo "=========================================="
|
|
185
|
+
|
|
186
|
+
# Remove symlinks
|
|
187
|
+
rm -f "$CLAUDE_HOOKS_DIR/ralph-session-start.py"
|
|
188
|
+
rm -f "$CLAUDE_HOOKS_DIR/ralph-session-end.py"
|
|
189
|
+
log_info "Removed hook symlinks"
|
|
190
|
+
|
|
191
|
+
# Remove from settings.json
|
|
192
|
+
if [ -f "$CLAUDE_SETTINGS" ]; then
|
|
193
|
+
python3 << PYTHON
|
|
194
|
+
import json
|
|
195
|
+
from pathlib import Path
|
|
196
|
+
|
|
197
|
+
settings_path = Path("$CLAUDE_SETTINGS")
|
|
198
|
+
project_root = "$PROJECT_ROOT"
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
settings = json.loads(settings_path.read_text())
|
|
202
|
+
except:
|
|
203
|
+
exit(0)
|
|
204
|
+
|
|
205
|
+
if 'hooks' not in settings:
|
|
206
|
+
exit(0)
|
|
207
|
+
|
|
208
|
+
# Remove Ralph-related hooks
|
|
209
|
+
for hook_type in ['SessionStart', 'SessionEnd']:
|
|
210
|
+
if hook_type in settings['hooks']:
|
|
211
|
+
settings['hooks'][hook_type] = [
|
|
212
|
+
h for h in settings['hooks'][hook_type]
|
|
213
|
+
if project_root not in str(h)
|
|
214
|
+
]
|
|
215
|
+
if not settings['hooks'][hook_type]:
|
|
216
|
+
del settings['hooks'][hook_type]
|
|
217
|
+
|
|
218
|
+
if not settings['hooks']:
|
|
219
|
+
del settings['hooks']
|
|
220
|
+
|
|
221
|
+
settings_path.write_text(json.dumps(settings, indent=2))
|
|
222
|
+
print("Settings updated")
|
|
223
|
+
PYTHON
|
|
224
|
+
log_info "Removed hook configuration from settings.json"
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
echo ""
|
|
228
|
+
log_info "Removal complete!"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# =============================================================================
|
|
232
|
+
# MAIN
|
|
233
|
+
# =============================================================================
|
|
234
|
+
case "${1:-}" in
|
|
235
|
+
--check)
|
|
236
|
+
check_installation
|
|
237
|
+
;;
|
|
238
|
+
--remove)
|
|
239
|
+
remove_hooks
|
|
240
|
+
;;
|
|
241
|
+
*)
|
|
242
|
+
install_hooks
|
|
243
|
+
;;
|
|
244
|
+
esac
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# Ralph Memory Integration - Test Runner with Automatic Rollback
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Runs all integration tests and automatically rolls back if any fail.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# ./test_with_rollback.sh <backup_directory>
|
|
9
|
+
#
|
|
10
|
+
# Example:
|
|
11
|
+
# ./test_with_rollback.sh ~/.claude-self-reflect/backups/20260104_120000_pre_ralph_memory
|
|
12
|
+
# =============================================================================
|
|
13
|
+
|
|
14
|
+
set -e # Exit on any error
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
18
|
+
BACKUP_DIR="$1"
|
|
19
|
+
|
|
20
|
+
# Colors
|
|
21
|
+
RED='\033[0;31m'
|
|
22
|
+
GREEN='\033[0;32m'
|
|
23
|
+
YELLOW='\033[1;33m'
|
|
24
|
+
NC='\033[0m'
|
|
25
|
+
|
|
26
|
+
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
27
|
+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
28
|
+
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
29
|
+
|
|
30
|
+
# Validate backup directory
|
|
31
|
+
if [ -z "$BACKUP_DIR" ]; then
|
|
32
|
+
echo "Usage: $0 <backup_directory>"
|
|
33
|
+
echo ""
|
|
34
|
+
echo "Create a backup first:"
|
|
35
|
+
echo " ./backup_and_restore.sh backup"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [ ! -d "$BACKUP_DIR" ]; then
|
|
40
|
+
log_error "Backup directory not found: $BACKUP_DIR"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
if [ ! -f "$BACKUP_DIR/manifest.json" ]; then
|
|
45
|
+
log_error "Invalid backup: manifest.json not found"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
echo "=========================================="
|
|
50
|
+
echo "Ralph Memory Integration Test Runner"
|
|
51
|
+
echo "=========================================="
|
|
52
|
+
echo "Backup: $BACKUP_DIR"
|
|
53
|
+
echo "Project: $PROJECT_ROOT"
|
|
54
|
+
echo "=========================================="
|
|
55
|
+
echo ""
|
|
56
|
+
|
|
57
|
+
cd "$PROJECT_ROOT"
|
|
58
|
+
|
|
59
|
+
# Activate virtual environment if it exists
|
|
60
|
+
if [ -d "$PROJECT_ROOT/venv" ]; then
|
|
61
|
+
source "$PROJECT_ROOT/venv/bin/activate"
|
|
62
|
+
log_info "Using venv Python: $(which python)"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# =============================================================================
|
|
66
|
+
# ROLLBACK FUNCTION
|
|
67
|
+
# =============================================================================
|
|
68
|
+
rollback() {
|
|
69
|
+
echo ""
|
|
70
|
+
echo -e "${RED}=========================================="
|
|
71
|
+
echo "TESTS FAILED - INITIATING AUTOMATIC ROLLBACK"
|
|
72
|
+
echo "==========================================${NC}"
|
|
73
|
+
echo ""
|
|
74
|
+
|
|
75
|
+
# Use the backup_and_restore script for actual restore
|
|
76
|
+
"$SCRIPT_DIR/backup_and_restore.sh" restore "$BACKUP_DIR" << EOF
|
|
77
|
+
yes
|
|
78
|
+
EOF
|
|
79
|
+
|
|
80
|
+
echo ""
|
|
81
|
+
log_error "ROLLBACK COMPLETE - System restored to pre-implementation state"
|
|
82
|
+
exit 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Trap errors and rollback
|
|
86
|
+
trap rollback ERR
|
|
87
|
+
|
|
88
|
+
# =============================================================================
|
|
89
|
+
# RUN TESTS
|
|
90
|
+
# =============================================================================
|
|
91
|
+
|
|
92
|
+
log_info "Running tests with automatic rollback on failure..."
|
|
93
|
+
echo ""
|
|
94
|
+
|
|
95
|
+
# Test 1: Check directory structure
|
|
96
|
+
log_info "Test 1: Checking directory structure..."
|
|
97
|
+
[ -d "src/runtime/hooks" ] || { log_error "Missing: src/runtime/hooks/"; exit 1; }
|
|
98
|
+
echo " ✓ src/runtime/hooks/ exists"
|
|
99
|
+
|
|
100
|
+
# Test 2: Check ralph_state module
|
|
101
|
+
log_info "Test 2: Testing ralph_state module..."
|
|
102
|
+
python3 -c "
|
|
103
|
+
from src.runtime.hooks.ralph_state import RalphState, load_state, save_state, is_ralph_session
|
|
104
|
+
import tempfile
|
|
105
|
+
from pathlib import Path
|
|
106
|
+
|
|
107
|
+
# Test create
|
|
108
|
+
state = RalphState.create_new('Test task', 'Test complete')
|
|
109
|
+
assert state.task == 'Test task', 'Task mismatch'
|
|
110
|
+
assert state.iteration == 1, 'Iteration should be 1'
|
|
111
|
+
assert state.session_id.startswith('ralph_'), 'Invalid session ID'
|
|
112
|
+
|
|
113
|
+
# Test roundtrip
|
|
114
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
115
|
+
path = Path(tmpdir) / '.ralph_state.md'
|
|
116
|
+
state.failed_approaches = ['Approach A']
|
|
117
|
+
state.learnings = ['Learning 1']
|
|
118
|
+
save_state(state, path)
|
|
119
|
+
|
|
120
|
+
loaded = load_state(path)
|
|
121
|
+
assert loaded.task == state.task, 'Roundtrip failed: task'
|
|
122
|
+
assert 'Approach A' in loaded.failed_approaches, 'Roundtrip failed: approaches'
|
|
123
|
+
|
|
124
|
+
print(' ✓ RalphState module working correctly')
|
|
125
|
+
"
|
|
126
|
+
|
|
127
|
+
# Test 3: SessionStart hook
|
|
128
|
+
log_info "Test 3: Testing session_start_hook..."
|
|
129
|
+
echo '{"session_id": "test123", "source": "startup"}' | timeout 10 python3 src/runtime/hooks/session_start_hook.py 2>&1 || true
|
|
130
|
+
echo " ✓ session_start_hook exits cleanly"
|
|
131
|
+
|
|
132
|
+
# Test 4: SessionEnd hook
|
|
133
|
+
log_info "Test 4: Testing session_end_hook..."
|
|
134
|
+
echo '{"session_id": "test123", "reason": "clear"}' | timeout 10 python3 src/runtime/hooks/session_end_hook.py 2>&1 || true
|
|
135
|
+
echo " ✓ session_end_hook exits cleanly"
|
|
136
|
+
|
|
137
|
+
# Test 5: PreCompact hook enhancement
|
|
138
|
+
log_info "Test 5: Checking precompact-hook.sh enhancement..."
|
|
139
|
+
if grep -q "RALPH MEMORY INTEGRATION" src/runtime/precompact-hook.sh 2>/dev/null; then
|
|
140
|
+
echo " ✓ precompact-hook.sh contains Ralph integration"
|
|
141
|
+
else
|
|
142
|
+
log_warn " ○ precompact-hook.sh not yet enhanced (may be pending)"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# Test 6: Qdrant connectivity
|
|
146
|
+
log_info "Test 6: Checking Qdrant connectivity..."
|
|
147
|
+
curl -sf http://localhost:6333/collections > /dev/null && echo " ✓ Qdrant accessible" || log_warn " ○ Qdrant not accessible (may not be required)"
|
|
148
|
+
|
|
149
|
+
# Test 7: Run pytest unit tests
|
|
150
|
+
log_info "Test 7: Running pytest unit tests..."
|
|
151
|
+
if [ -f "tests/ralph/test_ralph_integration.py" ]; then
|
|
152
|
+
python -m pytest tests/ralph/test_ralph_integration.py -v --tb=short -k "not Compaction" 2>&1 || { log_error "Pytest unit tests failed"; exit 1; }
|
|
153
|
+
echo " ✓ Pytest unit tests passed"
|
|
154
|
+
else
|
|
155
|
+
log_warn " ○ Integration tests not yet created"
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
# Test 8: Run CRITICAL compaction scenario tests (requires CSR running)
|
|
159
|
+
log_info "Test 8: Running CRITICAL compaction scenario tests..."
|
|
160
|
+
if curl -sf http://localhost:6333/collections > /dev/null 2>&1; then
|
|
161
|
+
# CSR is available, run compaction tests
|
|
162
|
+
python -m pytest tests/ralph/test_ralph_integration.py -v --tb=short -k "Compaction" 2>&1 || {
|
|
163
|
+
log_error "CRITICAL: Compaction scenario tests failed!"
|
|
164
|
+
log_error "These tests verify the core value proposition:"
|
|
165
|
+
log_error " - PreCompact backs up state to CSR"
|
|
166
|
+
log_error " - State recovery after compaction"
|
|
167
|
+
log_error " - Cross-session memory works"
|
|
168
|
+
exit 1
|
|
169
|
+
}
|
|
170
|
+
echo " ✓ Compaction scenario tests passed"
|
|
171
|
+
else
|
|
172
|
+
log_warn " ○ CSR not available, skipping compaction tests"
|
|
173
|
+
log_warn " (Start Qdrant to run: docker start claude-reflection-qdrant)"
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# Test 9: Docker services still healthy
|
|
177
|
+
log_info "Test 9: Checking Docker services..."
|
|
178
|
+
docker ps --filter "name=claude-reflection-qdrant" --format "{{.Status}}" | grep -q "Up" && echo " ✓ Qdrant container healthy" || log_warn " ○ Qdrant container not running"
|
|
179
|
+
docker ps --filter "name=claude-reflection-batch-watcher" --format "{{.Status}}" | grep -q "Up" && echo " ✓ Batch watcher healthy" || log_warn " ○ Batch watcher not running"
|
|
180
|
+
|
|
181
|
+
echo ""
|
|
182
|
+
echo -e "${GREEN}=========================================="
|
|
183
|
+
echo "ALL TESTS PASSED"
|
|
184
|
+
echo "==========================================${NC}"
|
|
185
|
+
echo ""
|
|
186
|
+
echo "The implementation is verified and safe."
|
|
187
|
+
echo ""
|
|
188
|
+
echo "Next steps:"
|
|
189
|
+
echo " 1. Review changes: git diff main"
|
|
190
|
+
echo " 2. Commit: git add -A && git commit -m 'feat: add Ralph memory integration hooks'"
|
|
191
|
+
echo " 3. Push: git push -u origin feat/ralph-csr-integration"
|
|
192
|
+
echo " 4. Create PR: gh pr create"
|
|
193
|
+
echo ""
|
|
194
|
+
echo "Backup retained at: $BACKUP_DIR"
|
|
195
|
+
echo "(You can delete it after merge: rm -rf $BACKUP_DIR)"
|
|
@@ -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
|