claude-mpm 4.9.0__py3-none-any.whl → 4.11.0__py3-none-any.whl
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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/templates/research.json +30 -23
- claude_mpm/cli/commands/mpm_init.py +250 -1
- claude_mpm/cli/commands/mpm_init_handler.py +41 -1
- claude_mpm/cli/commands/search.py +170 -4
- claude_mpm/cli/commands/session_pause_manager.py +389 -0
- claude_mpm/cli/commands/session_resume_manager.py +523 -0
- claude_mpm/cli/parsers/mpm_init_parser.py +59 -0
- claude_mpm/commands/mpm-init.md +78 -1
- claude_mpm/services/mcp_gateway/main.py +21 -0
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +141 -14
- claude_mpm/storage/state_storage.py +15 -15
- {claude_mpm-4.9.0.dist-info → claude_mpm-4.11.0.dist-info}/METADATA +10 -3
- {claude_mpm-4.9.0.dist-info → claude_mpm-4.11.0.dist-info}/RECORD +18 -16
- {claude_mpm-4.9.0.dist-info → claude_mpm-4.11.0.dist-info}/WHEEL +0 -0
- {claude_mpm-4.9.0.dist-info → claude_mpm-4.11.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.9.0.dist-info → claude_mpm-4.11.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.9.0.dist-info → claude_mpm-4.11.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,523 @@
|
|
1
|
+
"""
|
2
|
+
Session Resume Manager - Loads and analyzes paused session state.
|
3
|
+
|
4
|
+
This module provides functionality to resume a paused Claude MPM session by:
|
5
|
+
- Loading the most recent (or specified) paused session
|
6
|
+
- Checking for changes since the pause
|
7
|
+
- Detecting potential conflicts
|
8
|
+
- Generating context summary for seamless resumption
|
9
|
+
|
10
|
+
The resume manager ensures users have full awareness of what changed during the pause.
|
11
|
+
"""
|
12
|
+
|
13
|
+
import subprocess
|
14
|
+
from datetime import datetime
|
15
|
+
from pathlib import Path
|
16
|
+
from typing import Any, Dict, List, Optional
|
17
|
+
|
18
|
+
from rich.console import Console
|
19
|
+
from rich.panel import Panel
|
20
|
+
from rich.table import Table
|
21
|
+
|
22
|
+
from claude_mpm.core.logging_utils import get_logger
|
23
|
+
from claude_mpm.storage.state_storage import StateStorage
|
24
|
+
|
25
|
+
logger = get_logger(__name__)
|
26
|
+
console = Console()
|
27
|
+
|
28
|
+
|
29
|
+
class SessionResumeManager:
|
30
|
+
"""Manages resuming paused sessions with change detection."""
|
31
|
+
|
32
|
+
def __init__(self, project_path: Path):
|
33
|
+
"""Initialize Session Resume Manager.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
project_path: Path to the project directory
|
37
|
+
"""
|
38
|
+
self.project_path = project_path
|
39
|
+
self.session_dir = project_path / ".claude-mpm" / "sessions" / "pause"
|
40
|
+
self.storage = StateStorage()
|
41
|
+
|
42
|
+
def resume_session(self, session_id: Optional[str] = None) -> Dict[str, Any]:
|
43
|
+
"""Resume a paused session.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
session_id: Optional specific session ID to resume (defaults to most recent)
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
Dict containing resume context and warnings
|
50
|
+
"""
|
51
|
+
try:
|
52
|
+
# Load session state
|
53
|
+
if session_id:
|
54
|
+
session_state = self._load_session_by_id(session_id)
|
55
|
+
else:
|
56
|
+
session_state = self._load_latest_session()
|
57
|
+
|
58
|
+
if not session_state:
|
59
|
+
return {
|
60
|
+
"status": "error",
|
61
|
+
"message": (
|
62
|
+
"No paused sessions found"
|
63
|
+
if not session_id
|
64
|
+
else f"Session {session_id} not found"
|
65
|
+
),
|
66
|
+
}
|
67
|
+
|
68
|
+
# Analyze changes since pause
|
69
|
+
changes = self._analyze_changes_since_pause(session_state)
|
70
|
+
|
71
|
+
# Generate resume context
|
72
|
+
resume_context = self._generate_resume_context(session_state, changes)
|
73
|
+
|
74
|
+
# Display resume information
|
75
|
+
self._display_resume_info(session_state, changes)
|
76
|
+
|
77
|
+
return {
|
78
|
+
"status": "success",
|
79
|
+
"session_state": session_state,
|
80
|
+
"changes": changes,
|
81
|
+
"resume_context": resume_context,
|
82
|
+
}
|
83
|
+
|
84
|
+
except Exception as e:
|
85
|
+
logger.error(f"Failed to resume session: {e}")
|
86
|
+
return {"status": "error", "message": str(e)}
|
87
|
+
|
88
|
+
def _load_session_by_id(self, session_id: str) -> Optional[Dict[str, Any]]:
|
89
|
+
"""Load a specific session by ID.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
session_id: The session identifier
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
Session state dict or None if not found
|
96
|
+
"""
|
97
|
+
session_file = self.session_dir / f"{session_id}.json"
|
98
|
+
if not session_file.exists():
|
99
|
+
logger.warning(f"Session file not found: {session_file}")
|
100
|
+
return None
|
101
|
+
|
102
|
+
return self.storage.read_json(session_file)
|
103
|
+
|
104
|
+
def _load_latest_session(self) -> Optional[Dict[str, Any]]:
|
105
|
+
"""Load the most recent paused session.
|
106
|
+
|
107
|
+
Returns:
|
108
|
+
Session state dict or None if no sessions found
|
109
|
+
"""
|
110
|
+
if not self.session_dir.exists():
|
111
|
+
return None
|
112
|
+
|
113
|
+
session_files = sorted(self.session_dir.glob("session-*.json"))
|
114
|
+
if not session_files:
|
115
|
+
return None
|
116
|
+
|
117
|
+
# Load the most recent session
|
118
|
+
latest_file = session_files[-1]
|
119
|
+
return self.storage.read_json(latest_file)
|
120
|
+
|
121
|
+
def _analyze_changes_since_pause(
|
122
|
+
self, session_state: Dict[str, Any]
|
123
|
+
) -> Dict[str, Any]:
|
124
|
+
"""Analyze what changed since session was paused.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
session_state: The paused session state
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
Dict containing change analysis
|
131
|
+
"""
|
132
|
+
changes: Dict[str, Any] = {
|
133
|
+
"branch_changed": False,
|
134
|
+
"new_commits": [],
|
135
|
+
"new_commits_count": 0,
|
136
|
+
"working_directory_changes": {
|
137
|
+
"new_modified": [],
|
138
|
+
"new_untracked": [],
|
139
|
+
"resolved_files": [],
|
140
|
+
},
|
141
|
+
"warnings": [],
|
142
|
+
}
|
143
|
+
|
144
|
+
try:
|
145
|
+
git_context = session_state.get("git_context", {})
|
146
|
+
|
147
|
+
if not git_context.get("is_git_repo"):
|
148
|
+
return changes
|
149
|
+
|
150
|
+
# Check if branch changed
|
151
|
+
paused_branch = git_context.get("branch")
|
152
|
+
current_branch = self._get_current_branch()
|
153
|
+
|
154
|
+
if paused_branch and current_branch and paused_branch != current_branch:
|
155
|
+
changes["branch_changed"] = True
|
156
|
+
changes["warnings"].append(
|
157
|
+
f"Branch changed from '{paused_branch}' to '{current_branch}'"
|
158
|
+
)
|
159
|
+
|
160
|
+
# Check for new commits
|
161
|
+
paused_commits = git_context.get("recent_commits", [])
|
162
|
+
if paused_commits:
|
163
|
+
latest_paused_sha = paused_commits[0]["sha"]
|
164
|
+
new_commits = self._get_commits_since(latest_paused_sha)
|
165
|
+
changes["new_commits"] = new_commits
|
166
|
+
changes["new_commits_count"] = len(new_commits)
|
167
|
+
|
168
|
+
if new_commits:
|
169
|
+
changes["warnings"].append(
|
170
|
+
f"{len(new_commits)} new commit(s) since pause"
|
171
|
+
)
|
172
|
+
|
173
|
+
# Check working directory changes
|
174
|
+
paused_status = git_context.get("status", {})
|
175
|
+
current_status = self._get_current_status()
|
176
|
+
|
177
|
+
paused_modified = set(paused_status.get("modified_files", []))
|
178
|
+
current_modified = set(current_status.get("modified_files", []))
|
179
|
+
paused_untracked = set(paused_status.get("untracked_files", []))
|
180
|
+
current_untracked = set(current_status.get("untracked_files", []))
|
181
|
+
|
182
|
+
# Files that are newly modified
|
183
|
+
new_modified = list(current_modified - paused_modified)
|
184
|
+
# Files that are newly untracked
|
185
|
+
new_untracked = list(current_untracked - paused_untracked)
|
186
|
+
# Files that were modified but are now clean
|
187
|
+
resolved_files = list(paused_modified - current_modified)
|
188
|
+
|
189
|
+
changes["working_directory_changes"] = {
|
190
|
+
"new_modified": new_modified,
|
191
|
+
"new_untracked": new_untracked,
|
192
|
+
"resolved_files": resolved_files,
|
193
|
+
}
|
194
|
+
|
195
|
+
if new_modified:
|
196
|
+
changes["warnings"].append(
|
197
|
+
f"{len(new_modified)} file(s) modified since pause"
|
198
|
+
)
|
199
|
+
if new_untracked:
|
200
|
+
changes["warnings"].append(
|
201
|
+
f"{len(new_untracked)} new untracked file(s)"
|
202
|
+
)
|
203
|
+
|
204
|
+
except Exception as e:
|
205
|
+
logger.warning(f"Could not analyze changes: {e}")
|
206
|
+
changes["warnings"].append(f"Could not analyze changes: {e}")
|
207
|
+
|
208
|
+
return changes
|
209
|
+
|
210
|
+
def _get_current_branch(self) -> Optional[str]:
|
211
|
+
"""Get current git branch.
|
212
|
+
|
213
|
+
Returns:
|
214
|
+
Branch name or None
|
215
|
+
"""
|
216
|
+
try:
|
217
|
+
result = subprocess.run(
|
218
|
+
["git", "branch", "--show-current"],
|
219
|
+
cwd=str(self.project_path),
|
220
|
+
capture_output=True,
|
221
|
+
text=True,
|
222
|
+
check=True,
|
223
|
+
)
|
224
|
+
return result.stdout.strip()
|
225
|
+
except Exception:
|
226
|
+
return None
|
227
|
+
|
228
|
+
def _get_commits_since(self, since_sha: str) -> List[Dict[str, str]]:
|
229
|
+
"""Get commits since a specific SHA.
|
230
|
+
|
231
|
+
Args:
|
232
|
+
since_sha: The SHA to get commits after
|
233
|
+
|
234
|
+
Returns:
|
235
|
+
List of commit dicts
|
236
|
+
"""
|
237
|
+
try:
|
238
|
+
result = subprocess.run(
|
239
|
+
["git", "log", f"{since_sha}..HEAD", "--format=%h|%an|%ai|%s"],
|
240
|
+
cwd=str(self.project_path),
|
241
|
+
capture_output=True,
|
242
|
+
text=True,
|
243
|
+
check=True,
|
244
|
+
)
|
245
|
+
|
246
|
+
commits = []
|
247
|
+
for line in result.stdout.strip().split("\n"):
|
248
|
+
if not line:
|
249
|
+
continue
|
250
|
+
parts = line.split("|", 3)
|
251
|
+
if len(parts) == 4:
|
252
|
+
sha, author, timestamp, message = parts
|
253
|
+
commits.append(
|
254
|
+
{
|
255
|
+
"sha": sha,
|
256
|
+
"author": author,
|
257
|
+
"timestamp": timestamp,
|
258
|
+
"message": message,
|
259
|
+
}
|
260
|
+
)
|
261
|
+
|
262
|
+
return commits
|
263
|
+
|
264
|
+
except Exception as e:
|
265
|
+
logger.warning(f"Could not get commits: {e}")
|
266
|
+
return []
|
267
|
+
|
268
|
+
def _get_current_status(self) -> Dict[str, Any]:
|
269
|
+
"""Get current git status.
|
270
|
+
|
271
|
+
Returns:
|
272
|
+
Dict with status information
|
273
|
+
"""
|
274
|
+
status = {"clean": True, "modified_files": [], "untracked_files": []}
|
275
|
+
|
276
|
+
try:
|
277
|
+
result = subprocess.run(
|
278
|
+
["git", "status", "--porcelain"],
|
279
|
+
cwd=str(self.project_path),
|
280
|
+
capture_output=True,
|
281
|
+
text=True,
|
282
|
+
check=True,
|
283
|
+
)
|
284
|
+
|
285
|
+
modified_files = []
|
286
|
+
untracked_files = []
|
287
|
+
|
288
|
+
for line in result.stdout.strip().split("\n"):
|
289
|
+
if not line:
|
290
|
+
continue
|
291
|
+
status_code = line[:2]
|
292
|
+
file_path = line[3:]
|
293
|
+
|
294
|
+
if status_code.startswith("??"):
|
295
|
+
untracked_files.append(file_path)
|
296
|
+
else:
|
297
|
+
modified_files.append(file_path)
|
298
|
+
|
299
|
+
status = {
|
300
|
+
"clean": len(modified_files) == 0 and len(untracked_files) == 0,
|
301
|
+
"modified_files": modified_files,
|
302
|
+
"untracked_files": untracked_files,
|
303
|
+
}
|
304
|
+
|
305
|
+
except Exception as e:
|
306
|
+
logger.warning(f"Could not get status: {e}")
|
307
|
+
|
308
|
+
return status
|
309
|
+
|
310
|
+
def _generate_resume_context(
|
311
|
+
self, session_state: Dict[str, Any], changes: Dict[str, Any]
|
312
|
+
) -> str:
|
313
|
+
"""Generate formatted context summary for resuming.
|
314
|
+
|
315
|
+
Args:
|
316
|
+
session_state: The paused session state
|
317
|
+
changes: Analysis of changes since pause
|
318
|
+
|
319
|
+
Returns:
|
320
|
+
Formatted context string
|
321
|
+
"""
|
322
|
+
lines = []
|
323
|
+
lines.append("=" * 80)
|
324
|
+
lines.append("SESSION RESUME CONTEXT")
|
325
|
+
lines.append("=" * 80)
|
326
|
+
lines.append("")
|
327
|
+
|
328
|
+
# Session info
|
329
|
+
session_id = session_state.get("session_id", "unknown")
|
330
|
+
paused_at = session_state.get("paused_at", "unknown")
|
331
|
+
lines.append(f"Session ID: {session_id}")
|
332
|
+
lines.append(f"Paused at: {paused_at}")
|
333
|
+
lines.append("")
|
334
|
+
|
335
|
+
# Previous context
|
336
|
+
conversation = session_state.get("conversation", {})
|
337
|
+
lines.append("PREVIOUS CONTEXT:")
|
338
|
+
lines.append(f" Summary: {conversation.get('summary', 'Not provided')}")
|
339
|
+
lines.append("")
|
340
|
+
|
341
|
+
# Accomplishments
|
342
|
+
accomplishments = conversation.get("accomplishments", [])
|
343
|
+
if accomplishments:
|
344
|
+
lines.append("ACCOMPLISHMENTS:")
|
345
|
+
for item in accomplishments:
|
346
|
+
lines.append(f" - {item}")
|
347
|
+
lines.append("")
|
348
|
+
|
349
|
+
# Next steps
|
350
|
+
next_steps = conversation.get("next_steps", [])
|
351
|
+
if next_steps:
|
352
|
+
lines.append("PLANNED NEXT STEPS:")
|
353
|
+
for item in next_steps:
|
354
|
+
lines.append(f" - {item}")
|
355
|
+
lines.append("")
|
356
|
+
|
357
|
+
# Changes since pause
|
358
|
+
if changes.get("warnings"):
|
359
|
+
lines.append("CHANGES SINCE PAUSE:")
|
360
|
+
for warning in changes["warnings"]:
|
361
|
+
lines.append(f" ⚠️ {warning}")
|
362
|
+
lines.append("")
|
363
|
+
|
364
|
+
# New commits details
|
365
|
+
if changes.get("new_commits"):
|
366
|
+
lines.append("NEW COMMITS:")
|
367
|
+
for commit in changes["new_commits"][:5]:
|
368
|
+
lines.append(
|
369
|
+
f" - {commit['sha']}: {commit['message']} ({commit['author']})"
|
370
|
+
)
|
371
|
+
if len(changes["new_commits"]) > 5:
|
372
|
+
lines.append(f" ... and {len(changes['new_commits']) - 5} more")
|
373
|
+
lines.append("")
|
374
|
+
|
375
|
+
# Working directory changes
|
376
|
+
wd_changes = changes.get("working_directory_changes", {})
|
377
|
+
if wd_changes.get("new_modified"):
|
378
|
+
lines.append("NEWLY MODIFIED FILES:")
|
379
|
+
for file_path in wd_changes["new_modified"][:10]:
|
380
|
+
lines.append(f" - {file_path}")
|
381
|
+
if len(wd_changes["new_modified"]) > 10:
|
382
|
+
lines.append(f" ... and {len(wd_changes['new_modified']) - 10} more")
|
383
|
+
lines.append("")
|
384
|
+
|
385
|
+
# Todos
|
386
|
+
todos = session_state.get("todos", {})
|
387
|
+
active_todos = todos.get("active", [])
|
388
|
+
if active_todos:
|
389
|
+
lines.append("ACTIVE TODO ITEMS:")
|
390
|
+
for todo in active_todos:
|
391
|
+
status = todo.get("status", "unknown")
|
392
|
+
content = todo.get("content", "unknown")
|
393
|
+
lines.append(f" [{status}] {content}")
|
394
|
+
lines.append("")
|
395
|
+
|
396
|
+
lines.append("=" * 80)
|
397
|
+
lines.append("Ready to resume work!")
|
398
|
+
lines.append("=" * 80)
|
399
|
+
|
400
|
+
return "\n".join(lines)
|
401
|
+
|
402
|
+
def _display_resume_info(
|
403
|
+
self, session_state: Dict[str, Any], changes: Dict[str, Any]
|
404
|
+
) -> None:
|
405
|
+
"""Display resume information to console.
|
406
|
+
|
407
|
+
Args:
|
408
|
+
session_state: The paused session state
|
409
|
+
changes: Analysis of changes since pause
|
410
|
+
"""
|
411
|
+
console.print()
|
412
|
+
|
413
|
+
# Session header
|
414
|
+
session_id = session_state.get("session_id", "unknown")
|
415
|
+
paused_at = session_state.get("paused_at", "unknown")
|
416
|
+
|
417
|
+
# Parse timestamp for better display
|
418
|
+
try:
|
419
|
+
dt = datetime.fromisoformat(paused_at.replace("Z", "+00:00"))
|
420
|
+
paused_display = dt.strftime("%Y-%m-%d %H:%M:%S UTC")
|
421
|
+
except Exception:
|
422
|
+
paused_display = paused_at
|
423
|
+
|
424
|
+
console.print(
|
425
|
+
Panel(
|
426
|
+
f"[bold cyan]Resuming Session[/bold cyan]\n\n"
|
427
|
+
f"[yellow]Session ID:[/yellow] {session_id}\n"
|
428
|
+
f"[yellow]Paused at:[/yellow] {paused_display}",
|
429
|
+
title="🟢 Session Resume",
|
430
|
+
border_style="cyan",
|
431
|
+
)
|
432
|
+
)
|
433
|
+
|
434
|
+
# Previous context
|
435
|
+
conversation = session_state.get("conversation", {})
|
436
|
+
console.print("\n[bold]Previous Context:[/bold]")
|
437
|
+
console.print(f" {conversation.get('summary', 'Not provided')}")
|
438
|
+
|
439
|
+
# Accomplishments
|
440
|
+
accomplishments = conversation.get("accomplishments", [])
|
441
|
+
if accomplishments:
|
442
|
+
console.print("\n[bold green]Accomplishments:[/bold green]")
|
443
|
+
for item in accomplishments:
|
444
|
+
console.print(f" ✓ {item}")
|
445
|
+
|
446
|
+
# Next steps
|
447
|
+
next_steps = conversation.get("next_steps", [])
|
448
|
+
if next_steps:
|
449
|
+
console.print("\n[bold yellow]Planned Next Steps:[/bold yellow]")
|
450
|
+
for idx, item in enumerate(next_steps, 1):
|
451
|
+
console.print(f" {idx}. {item}")
|
452
|
+
|
453
|
+
# Changes/Warnings
|
454
|
+
warnings = changes.get("warnings", [])
|
455
|
+
if warnings:
|
456
|
+
console.print("\n[bold red]⚠️ Changes Since Pause:[/bold red]")
|
457
|
+
for warning in warnings:
|
458
|
+
console.print(f" • {warning}")
|
459
|
+
|
460
|
+
# New commits table
|
461
|
+
new_commits = changes.get("new_commits", [])
|
462
|
+
if new_commits:
|
463
|
+
console.print("\n[bold]New Commits:[/bold]")
|
464
|
+
table = Table(show_header=True, header_style="bold magenta")
|
465
|
+
table.add_column("SHA", style="yellow", width=8)
|
466
|
+
table.add_column("Author", style="green", width=20)
|
467
|
+
table.add_column("Message", style="white")
|
468
|
+
|
469
|
+
for commit in new_commits[:5]:
|
470
|
+
msg = commit["message"]
|
471
|
+
if len(msg) > 60:
|
472
|
+
msg = msg[:57] + "..."
|
473
|
+
table.add_row(commit["sha"], commit["author"], msg)
|
474
|
+
|
475
|
+
console.print(table)
|
476
|
+
if len(new_commits) > 5:
|
477
|
+
console.print(
|
478
|
+
f" [dim]... and {len(new_commits) - 5} more commits[/dim]"
|
479
|
+
)
|
480
|
+
|
481
|
+
# Active todos
|
482
|
+
todos = session_state.get("todos", {})
|
483
|
+
active_todos = todos.get("active", [])
|
484
|
+
if active_todos:
|
485
|
+
console.print("\n[bold]Active Todo Items:[/bold]")
|
486
|
+
for todo in active_todos:
|
487
|
+
status = todo.get("status", "unknown")
|
488
|
+
content = todo.get("content", "unknown")
|
489
|
+
status_icon = {
|
490
|
+
"pending": "⏸️",
|
491
|
+
"in_progress": "🔄",
|
492
|
+
"completed": "✅",
|
493
|
+
}.get(status, "❓")
|
494
|
+
console.print(f" {status_icon} [{status}] {content}")
|
495
|
+
|
496
|
+
console.print()
|
497
|
+
|
498
|
+
def list_available_sessions(self) -> List[Dict[str, Any]]:
|
499
|
+
"""List all available paused sessions.
|
500
|
+
|
501
|
+
Returns:
|
502
|
+
List of session information dicts
|
503
|
+
"""
|
504
|
+
if not self.session_dir.exists():
|
505
|
+
return []
|
506
|
+
|
507
|
+
sessions = []
|
508
|
+
for session_file in sorted(self.session_dir.glob("session-*.json")):
|
509
|
+
try:
|
510
|
+
state = self.storage.read_json(session_file)
|
511
|
+
if state:
|
512
|
+
sessions.append(
|
513
|
+
{
|
514
|
+
"session_id": state.get("session_id"),
|
515
|
+
"paused_at": state.get("paused_at"),
|
516
|
+
"summary": state.get("conversation", {}).get("summary"),
|
517
|
+
"file_path": str(session_file),
|
518
|
+
}
|
519
|
+
)
|
520
|
+
except Exception as e:
|
521
|
+
logger.warning(f"Could not read session file {session_file}: {e}")
|
522
|
+
|
523
|
+
return sessions
|
@@ -31,6 +31,8 @@ def add_mpm_init_subparser(subparsers: Any) -> None:
|
|
31
31
|
epilog=(
|
32
32
|
"Examples:\n"
|
33
33
|
" claude-mpm mpm-init # Initialize/update current directory\n"
|
34
|
+
" claude-mpm mpm-init pause # Pause current session\n"
|
35
|
+
" claude-mpm mpm-init resume # Resume latest session\n"
|
34
36
|
" claude-mpm mpm-init --review # Review project state without changes\n"
|
35
37
|
" claude-mpm mpm-init --update # Update existing CLAUDE.md\n"
|
36
38
|
" claude-mpm mpm-init --quick-update # Quick update based on recent git activity\n"
|
@@ -205,5 +207,62 @@ def add_mpm_init_subparser(subparsers: Any) -> None:
|
|
205
207
|
help="Path to project directory (default: current directory)",
|
206
208
|
)
|
207
209
|
|
210
|
+
# Add subparsers for pause/resume commands
|
211
|
+
subcommands = mpm_init_parser.add_subparsers(
|
212
|
+
dest="subcommand",
|
213
|
+
title="session management",
|
214
|
+
description="Commands for managing session pause/resume",
|
215
|
+
)
|
216
|
+
|
217
|
+
# Pause subcommand
|
218
|
+
pause_parser = subcommands.add_parser(
|
219
|
+
"pause",
|
220
|
+
help="Pause current session and save state",
|
221
|
+
description="Pause the current session and capture state for later resumption",
|
222
|
+
)
|
223
|
+
pause_parser.add_argument(
|
224
|
+
"--summary",
|
225
|
+
"-s",
|
226
|
+
type=str,
|
227
|
+
help="Summary of what you were working on",
|
228
|
+
)
|
229
|
+
pause_parser.add_argument(
|
230
|
+
"--accomplishment",
|
231
|
+
"-a",
|
232
|
+
action="append",
|
233
|
+
help="Accomplishment from this session (can be used multiple times)",
|
234
|
+
)
|
235
|
+
pause_parser.add_argument(
|
236
|
+
"--next-step",
|
237
|
+
"-n",
|
238
|
+
action="append",
|
239
|
+
help="Next step to continue work (can be used multiple times)",
|
240
|
+
)
|
241
|
+
pause_parser.add_argument(
|
242
|
+
"project_path",
|
243
|
+
nargs="?",
|
244
|
+
default=".",
|
245
|
+
help="Path to project directory (default: current directory)",
|
246
|
+
)
|
247
|
+
|
248
|
+
# Resume subcommand
|
249
|
+
resume_parser = subcommands.add_parser(
|
250
|
+
"resume",
|
251
|
+
help="Resume a paused session",
|
252
|
+
description="Resume the most recent (or specified) paused session",
|
253
|
+
)
|
254
|
+
resume_parser.add_argument(
|
255
|
+
"--session-id",
|
256
|
+
"-i",
|
257
|
+
type=str,
|
258
|
+
help="Specific session ID to resume (defaults to most recent)",
|
259
|
+
)
|
260
|
+
resume_parser.add_argument(
|
261
|
+
"project_path",
|
262
|
+
nargs="?",
|
263
|
+
default=".",
|
264
|
+
help="Path to project directory (default: current directory)",
|
265
|
+
)
|
266
|
+
|
208
267
|
# Set the command handler
|
209
268
|
mpm_init_parser.set_defaults(command="mpm-init")
|
claude_mpm/commands/mpm-init.md
CHANGED
@@ -7,6 +7,10 @@ Initialize or intelligently update your project for optimal use with Claude Code
|
|
7
7
|
```
|
8
8
|
/mpm-init # Auto-detects and offers update or create
|
9
9
|
/mpm-init update # Lightweight update based on recent git activity
|
10
|
+
/mpm-init catchup # Show recent commit history for context
|
11
|
+
/mpm-init pause # Pause session and save state
|
12
|
+
/mpm-init resume # Resume most recent paused session
|
13
|
+
/mpm-init resume --list # List all paused sessions
|
10
14
|
/mpm-init --review # Review project state without changes
|
11
15
|
/mpm-init --update # Full update of existing CLAUDE.md
|
12
16
|
/mpm-init --organize # Organize project structure
|
@@ -163,6 +167,76 @@ This displays:
|
|
163
167
|
|
164
168
|
Useful for understanding recent development activity and getting PM up to speed on project changes.
|
165
169
|
|
170
|
+
### Session Management (Pause/Resume)
|
171
|
+
|
172
|
+
Save and restore session state across Claude sessions:
|
173
|
+
|
174
|
+
**Pause Current Session:**
|
175
|
+
```bash
|
176
|
+
/mpm-init pause
|
177
|
+
```
|
178
|
+
|
179
|
+
This captures and saves:
|
180
|
+
- Conversation context and progress
|
181
|
+
- Current git repository state
|
182
|
+
- Active and completed todo items
|
183
|
+
- Working directory status
|
184
|
+
- Session timestamp and metadata
|
185
|
+
|
186
|
+
**Resume Previous Session:**
|
187
|
+
```bash
|
188
|
+
/mpm-init resume
|
189
|
+
```
|
190
|
+
|
191
|
+
This loads and analyzes:
|
192
|
+
- Most recent (or specified) paused session
|
193
|
+
- Changes since pause (git commits, file modifications)
|
194
|
+
- Potential conflicts or warnings
|
195
|
+
- Full context for seamless continuation
|
196
|
+
|
197
|
+
**Pause Options:**
|
198
|
+
- `-s, --summary TEXT`: Provide session summary
|
199
|
+
- `-a, --accomplishment TEXT`: Record accomplishments (can be used multiple times)
|
200
|
+
- `-n, --next-step TEXT`: Document next steps (can be used multiple times)
|
201
|
+
- `--no-commit`: Skip creating git commit with session info
|
202
|
+
|
203
|
+
**Resume Options:**
|
204
|
+
- `--session-id TEXT`: Resume specific session by ID
|
205
|
+
- `--list`: List all available paused sessions
|
206
|
+
|
207
|
+
**Example Usage:**
|
208
|
+
```bash
|
209
|
+
# Pause with detailed context
|
210
|
+
/mpm-init pause -s "Implemented authentication system" \
|
211
|
+
-a "Added login endpoint" \
|
212
|
+
-a "Created user model" \
|
213
|
+
-a "Wrote integration tests" \
|
214
|
+
-n "Add logout endpoint" \
|
215
|
+
-n "Implement password reset"
|
216
|
+
|
217
|
+
# Resume latest session
|
218
|
+
/mpm-init resume
|
219
|
+
|
220
|
+
# List available sessions
|
221
|
+
/mpm-init resume --list
|
222
|
+
|
223
|
+
# Resume specific session
|
224
|
+
/mpm-init resume --session-id session-20251020-012501
|
225
|
+
```
|
226
|
+
|
227
|
+
**Session Storage:**
|
228
|
+
- Sessions saved in `.claude-mpm/sessions/pause/`
|
229
|
+
- JSON format with secure permissions (0600)
|
230
|
+
- Includes checksums for data integrity
|
231
|
+
- Automatic git commit creation (unless `--no-commit`)
|
232
|
+
|
233
|
+
**Use Cases:**
|
234
|
+
- **Context Continuity**: Maintain context across multiple Claude sessions
|
235
|
+
- **Team Handoffs**: Save state before passing work to another team member
|
236
|
+
- **Long-running Projects**: Track progress over multiple work sessions
|
237
|
+
- **Break Points**: Document progress at natural stopping points
|
238
|
+
- **Change Awareness**: Detect what changed while you were away
|
239
|
+
|
166
240
|
### Review Project State
|
167
241
|
```bash
|
168
242
|
/mpm-init --review
|
@@ -268,10 +342,13 @@ The command delegates to the Agentic Coder Optimizer agent which:
|
|
268
342
|
## Notes
|
269
343
|
|
270
344
|
- **Quick Update vs Full Update**: Use `/mpm-init update` for fast activity-based updates (30 days), or `/mpm-init --update` for comprehensive doc refresh
|
345
|
+
- **Session Management**: Use `/mpm-init pause` to save state and `/mpm-init resume` to continue later with full context
|
271
346
|
- **Smart Mode**: Automatically detects existing CLAUDE.md and offers update vs recreate
|
272
347
|
- **Safe Updates**: Previous versions always archived before updating
|
273
348
|
- **Custom Content**: Your project-specific sections are preserved by default
|
274
|
-
- **Git Integration**: Analyzes recent commits to understand project evolution
|
349
|
+
- **Git Integration**: Analyzes recent commits to understand project evolution; pause creates optional git commits
|
350
|
+
- **Session Storage**: Paused sessions stored in `.claude-mpm/sessions/pause/` with secure permissions
|
351
|
+
- **Change Detection**: Resume automatically detects and reports changes since pause
|
275
352
|
- **Argument Processing**: The slash command processes the `update` argument and routes to `--quick-update` flag
|
276
353
|
- The command uses the Agentic Coder Optimizer agent for implementation
|
277
354
|
- AST analysis is enabled by default for comprehensive documentation
|