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 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.10",
4
4
  "description": "Give Claude perfect memory of all your conversations - Installation wizard for Python MCP server",
5
5
  "keywords": [
6
6
  "claude",
@@ -39,6 +39,7 @@
39
39
  "scripts/auto-migrate.cjs",
40
40
  "scripts/migrate-to-unified-state.py",
41
41
  "scripts/csr-status",
42
+ "scripts/ralph/**",
42
43
  "mcp-server/src/**/*.py",
43
44
  "mcp-server/pyproject.toml",
44
45
  "mcp-server/run-mcp.sh",
@@ -0,0 +1,309 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # Ralph Memory Integration - Backup and Restore Script
4
+ # =============================================================================
5
+ # Usage:
6
+ # ./backup_and_restore.sh backup # Create full backup
7
+ # ./backup_and_restore.sh restore <dir> # Restore from backup directory
8
+ # ./backup_and_restore.sh verify <dir> # Verify backup integrity
9
+ # ./backup_and_restore.sh list # List available backups
10
+ # =============================================================================
11
+
12
+ set -e
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
16
+ BACKUP_BASE="$HOME/.claude-self-reflect/backups"
17
+
18
+ # Colors for output
19
+ RED='\033[0;31m'
20
+ GREEN='\033[0;32m'
21
+ YELLOW='\033[1;33m'
22
+ NC='\033[0m' # No Color
23
+
24
+ log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
25
+ log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
26
+ log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
27
+
28
+ # =============================================================================
29
+ # BACKUP FUNCTION
30
+ # =============================================================================
31
+ do_backup() {
32
+ local BACKUP_DIR="$BACKUP_BASE/$(date +%Y%m%d_%H%M%S)_pre_ralph_memory"
33
+ mkdir -p "$BACKUP_DIR"
34
+
35
+ log_info "Creating backup at: $BACKUP_DIR"
36
+
37
+ # 1. Stop services for consistent backup
38
+ log_info "Stopping services for consistent backup..."
39
+ docker stop claude-reflection-batch-watcher claude-reflection-batch-monitor 2>/dev/null || true
40
+ sleep 2
41
+
42
+ # 2. Backup Qdrant data volume
43
+ log_info "Backing up Qdrant data volume..."
44
+ if docker volume inspect qdrant_data > /dev/null 2>&1; then
45
+ docker run --rm \
46
+ -v qdrant_data:/data:ro \
47
+ -v "$BACKUP_DIR":/backup \
48
+ alpine tar czf /backup/qdrant_data.tar.gz -C /data .
49
+ log_info "Qdrant backup: $(du -h "$BACKUP_DIR/qdrant_data.tar.gz" | cut -f1)"
50
+ else
51
+ log_warn "Qdrant volume not found, skipping"
52
+ fi
53
+
54
+ # 3. Backup CSR config directory
55
+ log_info "Backing up CSR config..."
56
+ if [ -d "$HOME/.claude-self-reflect/config" ]; then
57
+ tar czf "$BACKUP_DIR/csr_config.tar.gz" -C "$HOME/.claude-self-reflect" config
58
+ else
59
+ log_warn "CSR config not found, skipping"
60
+ fi
61
+
62
+ # 4. Backup batch queue
63
+ if [ -d "$HOME/.claude-self-reflect/batch_queue" ]; then
64
+ tar czf "$BACKUP_DIR/csr_batch_queue.tar.gz" -C "$HOME/.claude-self-reflect" batch_queue
65
+ fi
66
+
67
+ # 5. Backup batch state
68
+ if [ -d "$HOME/.claude-self-reflect/batch_state" ]; then
69
+ tar czf "$BACKUP_DIR/csr_batch_state.tar.gz" -C "$HOME/.claude-self-reflect" batch_state
70
+ fi
71
+
72
+ # 6. Save git state
73
+ log_info "Saving git state..."
74
+ cd "$PROJECT_ROOT"
75
+ echo "$(git rev-parse HEAD)" > "$BACKUP_DIR/git_head.txt"
76
+ echo "$(git branch --show-current)" > "$BACKUP_DIR/git_branch.txt"
77
+ git diff > "$BACKUP_DIR/git_diff.patch" 2>/dev/null || true
78
+ git diff --cached > "$BACKUP_DIR/git_staged.patch" 2>/dev/null || true
79
+
80
+ # 7. Restart services
81
+ log_info "Restarting services..."
82
+ docker start claude-reflection-batch-watcher claude-reflection-batch-monitor 2>/dev/null || true
83
+
84
+ # 8. Create manifest
85
+ cat > "$BACKUP_DIR/manifest.json" << EOF
86
+ {
87
+ "created": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
88
+ "git_commit": "$(cat "$BACKUP_DIR/git_head.txt")",
89
+ "git_branch": "$(cat "$BACKUP_DIR/git_branch.txt")",
90
+ "qdrant_backup": $([ -f "$BACKUP_DIR/qdrant_data.tar.gz" ] && echo "true" || echo "false"),
91
+ "config_backup": $([ -f "$BACKUP_DIR/csr_config.tar.gz" ] && echo "true" || echo "false"),
92
+ "project_root": "$PROJECT_ROOT"
93
+ }
94
+ EOF
95
+
96
+ log_info "Backup complete!"
97
+ echo ""
98
+ echo "Backup directory: $BACKUP_DIR"
99
+ echo "Files:"
100
+ ls -lh "$BACKUP_DIR"
101
+ echo ""
102
+ echo "To restore: $0 restore $BACKUP_DIR"
103
+ }
104
+
105
+ # =============================================================================
106
+ # RESTORE FUNCTION
107
+ # =============================================================================
108
+ do_restore() {
109
+ local BACKUP_DIR="$1"
110
+
111
+ if [ -z "$BACKUP_DIR" ]; then
112
+ log_error "Usage: $0 restore <backup_directory>"
113
+ exit 1
114
+ fi
115
+
116
+ if [ ! -d "$BACKUP_DIR" ]; then
117
+ log_error "Backup directory not found: $BACKUP_DIR"
118
+ exit 1
119
+ fi
120
+
121
+ if [ ! -f "$BACKUP_DIR/manifest.json" ]; then
122
+ log_error "Invalid backup: manifest.json not found"
123
+ exit 1
124
+ fi
125
+
126
+ log_warn "This will restore system state from backup."
127
+ log_warn "Current changes will be lost!"
128
+ echo ""
129
+ read -p "Are you sure? (type 'yes' to confirm): " confirm
130
+ if [ "$confirm" != "yes" ]; then
131
+ log_info "Restore cancelled"
132
+ exit 0
133
+ fi
134
+
135
+ echo ""
136
+ log_info "Starting restore from: $BACKUP_DIR"
137
+
138
+ # 1. Stop all services
139
+ log_info "Stopping all services..."
140
+ docker stop claude-reflection-batch-watcher claude-reflection-batch-monitor claude-reflection-qdrant 2>/dev/null || true
141
+ sleep 3
142
+
143
+ # 2. Restore Qdrant data
144
+ if [ -f "$BACKUP_DIR/qdrant_data.tar.gz" ]; then
145
+ log_info "Restoring Qdrant data..."
146
+ docker run --rm \
147
+ -v qdrant_data:/data \
148
+ -v "$BACKUP_DIR":/backup \
149
+ alpine sh -c "rm -rf /data/* && tar xzf /backup/qdrant_data.tar.gz -C /data"
150
+ fi
151
+
152
+ # 3. Restore CSR config
153
+ if [ -f "$BACKUP_DIR/csr_config.tar.gz" ]; then
154
+ log_info "Restoring CSR config..."
155
+ rm -rf "$HOME/.claude-self-reflect/config"
156
+ tar xzf "$BACKUP_DIR/csr_config.tar.gz" -C "$HOME/.claude-self-reflect/"
157
+ fi
158
+
159
+ # 4. Restore batch queue
160
+ if [ -f "$BACKUP_DIR/csr_batch_queue.tar.gz" ]; then
161
+ rm -rf "$HOME/.claude-self-reflect/batch_queue"
162
+ tar xzf "$BACKUP_DIR/csr_batch_queue.tar.gz" -C "$HOME/.claude-self-reflect/"
163
+ fi
164
+
165
+ # 5. Restore batch state
166
+ if [ -f "$BACKUP_DIR/csr_batch_state.tar.gz" ]; then
167
+ rm -rf "$HOME/.claude-self-reflect/batch_state"
168
+ tar xzf "$BACKUP_DIR/csr_batch_state.tar.gz" -C "$HOME/.claude-self-reflect/"
169
+ fi
170
+
171
+ # 6. Restore git state
172
+ log_info "Restoring git state..."
173
+ cd "$PROJECT_ROOT"
174
+ ORIGINAL_COMMIT=$(cat "$BACKUP_DIR/git_head.txt")
175
+ git reset --hard "$ORIGINAL_COMMIT"
176
+
177
+ # 7. Restart services
178
+ log_info "Restarting services..."
179
+ docker start claude-reflection-qdrant 2>/dev/null || true
180
+ sleep 5
181
+ docker start claude-reflection-batch-watcher claude-reflection-batch-monitor 2>/dev/null || true
182
+
183
+ log_info "Restore complete!"
184
+ echo ""
185
+ echo "Verifying services..."
186
+ docker ps --filter "name=claude" --format "table {{.Names}}\t{{.Status}}"
187
+ }
188
+
189
+ # =============================================================================
190
+ # VERIFY FUNCTION
191
+ # =============================================================================
192
+ do_verify() {
193
+ local BACKUP_DIR="$1"
194
+
195
+ if [ -z "$BACKUP_DIR" ]; then
196
+ log_error "Usage: $0 verify <backup_directory>"
197
+ exit 1
198
+ fi
199
+
200
+ if [ ! -d "$BACKUP_DIR" ]; then
201
+ log_error "Backup directory not found: $BACKUP_DIR"
202
+ exit 1
203
+ fi
204
+
205
+ echo "=========================================="
206
+ echo "Verifying backup: $BACKUP_DIR"
207
+ echo "=========================================="
208
+ echo ""
209
+
210
+ local ALL_OK=true
211
+
212
+ # Check manifest
213
+ if [ -f "$BACKUP_DIR/manifest.json" ]; then
214
+ log_info "✓ Manifest found"
215
+ cat "$BACKUP_DIR/manifest.json" | python3 -m json.tool 2>/dev/null || log_warn "Manifest is not valid JSON"
216
+ else
217
+ log_error "✗ Manifest missing"
218
+ ALL_OK=false
219
+ fi
220
+
221
+ # Check Qdrant backup
222
+ if [ -f "$BACKUP_DIR/qdrant_data.tar.gz" ]; then
223
+ local SIZE=$(du -h "$BACKUP_DIR/qdrant_data.tar.gz" | cut -f1)
224
+ log_info "✓ Qdrant backup ($SIZE)"
225
+ # Verify tar integrity
226
+ tar tzf "$BACKUP_DIR/qdrant_data.tar.gz" > /dev/null 2>&1 && log_info " ✓ Archive integrity OK" || { log_error " ✗ Archive corrupted"; ALL_OK=false; }
227
+ else
228
+ log_warn "✗ Qdrant backup missing"
229
+ fi
230
+
231
+ # Check config backup
232
+ if [ -f "$BACKUP_DIR/csr_config.tar.gz" ]; then
233
+ local SIZE=$(du -h "$BACKUP_DIR/csr_config.tar.gz" | cut -f1)
234
+ log_info "✓ Config backup ($SIZE)"
235
+ else
236
+ log_warn "○ Config backup missing (optional)"
237
+ fi
238
+
239
+ # Check git state
240
+ if [ -f "$BACKUP_DIR/git_head.txt" ]; then
241
+ log_info "✓ Git state saved: $(cat "$BACKUP_DIR/git_head.txt" | head -c 8)..."
242
+ else
243
+ log_error "✗ Git state missing"
244
+ ALL_OK=false
245
+ fi
246
+
247
+ echo ""
248
+ if $ALL_OK; then
249
+ log_info "Backup verification: PASSED"
250
+ else
251
+ log_error "Backup verification: FAILED"
252
+ exit 1
253
+ fi
254
+ }
255
+
256
+ # =============================================================================
257
+ # LIST FUNCTION
258
+ # =============================================================================
259
+ do_list() {
260
+ echo "=========================================="
261
+ echo "Available Backups"
262
+ echo "=========================================="
263
+
264
+ if [ ! -d "$BACKUP_BASE" ]; then
265
+ log_info "No backups found at $BACKUP_BASE"
266
+ exit 0
267
+ fi
268
+
269
+ for dir in "$BACKUP_BASE"/*; do
270
+ if [ -d "$dir" ] && [ -f "$dir/manifest.json" ]; then
271
+ local NAME=$(basename "$dir")
272
+ local CREATED=$(python3 -c "import json; print(json.load(open('$dir/manifest.json'))['created'])" 2>/dev/null || echo "unknown")
273
+ local SIZE=$(du -sh "$dir" | cut -f1)
274
+ echo " $NAME ($SIZE, created: $CREATED)"
275
+ fi
276
+ done
277
+
278
+ echo ""
279
+ echo "To verify: $0 verify <backup_directory>"
280
+ echo "To restore: $0 restore <backup_directory>"
281
+ }
282
+
283
+ # =============================================================================
284
+ # MAIN
285
+ # =============================================================================
286
+ case "$1" in
287
+ backup)
288
+ do_backup
289
+ ;;
290
+ restore)
291
+ do_restore "$2"
292
+ ;;
293
+ verify)
294
+ do_verify "$2"
295
+ ;;
296
+ list)
297
+ do_list
298
+ ;;
299
+ *)
300
+ echo "Usage: $0 {backup|restore|verify|list}"
301
+ echo ""
302
+ echo "Commands:"
303
+ echo " backup Create full backup of Docker volumes and git state"
304
+ echo " restore <dir> Restore from specified backup directory"
305
+ echo " verify <dir> Verify backup integrity"
306
+ echo " list List available backups"
307
+ exit 1
308
+ ;;
309
+ esac