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
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-self-reflect",
|
|
3
|
-
"version": "7.1.
|
|
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
|