claude-mpm 5.6.10__py3-none-any.whl → 5.6.17__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

Files changed (61) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/cli/commands/commander.py +173 -3
  3. claude_mpm/cli/parsers/commander_parser.py +41 -8
  4. claude_mpm/cli/startup.py +104 -1
  5. claude_mpm/cli/startup_display.py +2 -1
  6. claude_mpm/commander/__init__.py +6 -0
  7. claude_mpm/commander/adapters/__init__.py +32 -3
  8. claude_mpm/commander/adapters/auggie.py +260 -0
  9. claude_mpm/commander/adapters/base.py +98 -1
  10. claude_mpm/commander/adapters/claude_code.py +32 -1
  11. claude_mpm/commander/adapters/codex.py +237 -0
  12. claude_mpm/commander/adapters/example_usage.py +310 -0
  13. claude_mpm/commander/adapters/mpm.py +389 -0
  14. claude_mpm/commander/adapters/registry.py +204 -0
  15. claude_mpm/commander/api/app.py +32 -16
  16. claude_mpm/commander/api/routes/messages.py +11 -11
  17. claude_mpm/commander/api/routes/projects.py +20 -20
  18. claude_mpm/commander/api/routes/sessions.py +19 -21
  19. claude_mpm/commander/api/routes/work.py +86 -50
  20. claude_mpm/commander/api/schemas.py +4 -0
  21. claude_mpm/commander/chat/cli.py +4 -0
  22. claude_mpm/commander/core/__init__.py +10 -0
  23. claude_mpm/commander/core/block_manager.py +325 -0
  24. claude_mpm/commander/core/response_manager.py +323 -0
  25. claude_mpm/commander/daemon.py +206 -10
  26. claude_mpm/commander/env_loader.py +59 -0
  27. claude_mpm/commander/memory/__init__.py +45 -0
  28. claude_mpm/commander/memory/compression.py +347 -0
  29. claude_mpm/commander/memory/embeddings.py +230 -0
  30. claude_mpm/commander/memory/entities.py +310 -0
  31. claude_mpm/commander/memory/example_usage.py +290 -0
  32. claude_mpm/commander/memory/integration.py +325 -0
  33. claude_mpm/commander/memory/search.py +381 -0
  34. claude_mpm/commander/memory/store.py +657 -0
  35. claude_mpm/commander/registry.py +10 -4
  36. claude_mpm/commander/runtime/monitor.py +32 -2
  37. claude_mpm/commander/work/executor.py +38 -20
  38. claude_mpm/commander/workflow/event_handler.py +25 -3
  39. claude_mpm/core/claude_runner.py +143 -0
  40. claude_mpm/core/output_style_manager.py +34 -7
  41. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-311.pyc +0 -0
  42. claude_mpm/hooks/claude_hooks/__pycache__/event_handlers.cpython-314.pyc +0 -0
  43. claude_mpm/hooks/claude_hooks/__pycache__/installer.cpython-311.pyc +0 -0
  44. claude_mpm/hooks/claude_hooks/auto_pause_handler.py +0 -0
  45. claude_mpm/hooks/claude_hooks/event_handlers.py +22 -0
  46. claude_mpm/hooks/claude_hooks/hook_handler.py +0 -0
  47. claude_mpm/hooks/claude_hooks/memory_integration.py +0 -0
  48. claude_mpm/hooks/claude_hooks/response_tracking.py +0 -0
  49. claude_mpm/hooks/claude_hooks/services/__pycache__/connection_manager.cpython-311.pyc +0 -0
  50. claude_mpm/hooks/templates/pre_tool_use_template.py +0 -0
  51. claude_mpm/scripts/start_activity_logging.py +0 -0
  52. claude_mpm/skills/__init__.py +2 -1
  53. claude_mpm/skills/bundled/pm/mpm-session-pause/SKILL.md +170 -0
  54. claude_mpm/skills/registry.py +295 -90
  55. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/METADATA +5 -3
  56. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/RECORD +55 -36
  57. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/WHEEL +0 -0
  58. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/entry_points.txt +0 -0
  59. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE +0 -0
  60. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/licenses/LICENSE-FAQ.md +0 -0
  61. {claude_mpm-5.6.10.dist-info → claude_mpm-5.6.17.dist-info}/top_level.txt +0 -0
@@ -6,13 +6,16 @@ and detects events using OutputParser.
6
6
 
7
7
  import asyncio
8
8
  import logging
9
- from typing import Dict, List, Optional
9
+ from typing import TYPE_CHECKING, Dict, List, Optional
10
10
 
11
11
  from ..events.manager import EventManager
12
12
  from ..models.events import Event
13
13
  from ..parsing.output_parser import OutputParser
14
14
  from ..tmux_orchestrator import TmuxOrchestrator
15
15
 
16
+ if TYPE_CHECKING:
17
+ from ..core.block_manager import BlockManager
18
+
16
19
  logger = logging.getLogger(__name__)
17
20
 
18
21
 
@@ -44,6 +47,7 @@ class RuntimeMonitor:
44
47
  event_manager: EventManager,
45
48
  poll_interval: float = 2.0,
46
49
  capture_lines: int = 1000,
50
+ block_manager: Optional["BlockManager"] = None,
47
51
  ):
48
52
  """Initialize runtime monitor.
49
53
 
@@ -53,6 +57,7 @@ class RuntimeMonitor:
53
57
  event_manager: EventManager for emitting events
54
58
  poll_interval: Seconds between polls (default: 2.0)
55
59
  capture_lines: Number of lines to capture (default: 1000)
60
+ block_manager: Optional BlockManager for automatic work blocking
56
61
 
57
62
  Raises:
58
63
  ValueError: If any required parameter is None
@@ -69,15 +74,17 @@ class RuntimeMonitor:
69
74
  self.event_manager = event_manager
70
75
  self.poll_interval = poll_interval
71
76
  self.capture_lines = capture_lines
77
+ self.block_manager = block_manager
72
78
 
73
79
  # Track active monitors: pane_target -> (project_id, task, last_output_hash)
74
80
  self._monitors: Dict[str, tuple[str, Optional[asyncio.Task], int]] = {}
75
81
  self._running = False
76
82
 
77
83
  logger.debug(
78
- "RuntimeMonitor initialized (interval: %.2fs, lines: %d)",
84
+ "RuntimeMonitor initialized (interval: %.2fs, lines: %d, block_manager: %s)",
79
85
  poll_interval,
80
86
  capture_lines,
87
+ "enabled" if block_manager else "disabled",
81
88
  )
82
89
 
83
90
  async def start_monitoring(self, pane_target: str, project_id: str) -> None:
@@ -284,6 +291,29 @@ class RuntimeMonitor:
284
291
  pane_target,
285
292
  )
286
293
 
294
+ # Automatically block work for blocking events
295
+ if self.block_manager:
296
+ for parse_result in parse_results:
297
+ # Get the created event from EventManager
298
+ # Events are created with matching titles, so find by title
299
+ pending_events = self.event_manager.get_pending(project_id)
300
+ for event in pending_events:
301
+ if (
302
+ event.title == parse_result.title
303
+ and event.is_blocking
304
+ ):
305
+ blocked_work = (
306
+ await self.block_manager.check_and_block(event)
307
+ )
308
+ if blocked_work:
309
+ logger.info(
310
+ "Event %s blocked %d work items: %s",
311
+ event.id,
312
+ len(blocked_work),
313
+ blocked_work,
314
+ )
315
+ break
316
+
287
317
  except Exception as e:
288
318
  logger.error(
289
319
  "Error in monitor loop for pane %s: %s",
@@ -51,16 +51,19 @@ class WorkExecutor:
51
51
 
52
52
  logger.debug(f"Initialized WorkExecutor for project {queue.project_id}")
53
53
 
54
- async def execute_next(self) -> bool:
54
+ async def execute_next(self, pane_target: Optional[str] = None) -> bool:
55
55
  """Execute next available work item.
56
56
 
57
57
  Gets next work from queue, starts it, and executes via RuntimeExecutor.
58
58
 
59
+ Args:
60
+ pane_target: Optional tmux pane target for execution
61
+
59
62
  Returns:
60
63
  True if work was executed, False if queue empty/blocked
61
64
 
62
65
  Example:
63
- >>> executed = await executor.execute_next()
66
+ >>> executed = await executor.execute_next("%5")
64
67
  >>> if not executed:
65
68
  ... print("No work available")
66
69
  """
@@ -71,10 +74,12 @@ class WorkExecutor:
71
74
  return False
72
75
 
73
76
  # Execute the work item
74
- await self.execute(work_item)
77
+ await self.execute(work_item, pane_target)
75
78
  return True
76
79
 
77
- async def execute(self, work_item: WorkItem) -> None:
80
+ async def execute(
81
+ self, work_item: WorkItem, pane_target: Optional[str] = None
82
+ ) -> None:
78
83
  """Execute a specific work item.
79
84
 
80
85
  Marks work as IN_PROGRESS and sends to RuntimeExecutor.
@@ -83,12 +88,13 @@ class WorkExecutor:
83
88
 
84
89
  Args:
85
90
  work_item: WorkItem to execute
91
+ pane_target: Optional tmux pane target for execution
86
92
 
87
93
  Raises:
88
94
  RuntimeError: If execution fails
89
95
 
90
96
  Example:
91
- >>> await executor.execute(work_item)
97
+ >>> await executor.execute(work_item, "%5")
92
98
  """
93
99
  # Mark as in progress
94
100
  if not self.queue.start(work_item.id):
@@ -103,17 +109,21 @@ class WorkExecutor:
103
109
  )
104
110
 
105
111
  try:
106
- # Send work content to runtime
107
- # Note: In actual implementation, this would integrate with
108
- # ProjectSession which manages the pane_target
109
- # For now, we assume runtime has active session
110
- # This will be properly integrated when wiring with ProjectSession
112
+ # Send work content to runtime if pane target provided
113
+ if pane_target:
114
+ await self.runtime.send_message(pane_target, work_item.content)
115
+ logger.info(
116
+ f"Work item {work_item.id} sent to pane {pane_target} for execution"
117
+ )
118
+ else:
119
+ logger.warning(
120
+ f"No pane target provided for work item {work_item.id}, "
121
+ f"work marked as in-progress but not sent to runtime"
122
+ )
111
123
 
112
124
  # Store work item ID in metadata for callback tracking
113
125
  work_item.metadata["execution_started"] = True
114
126
 
115
- logger.info(f"Work item {work_item.id} sent to runtime for execution")
116
-
117
127
  except Exception as e:
118
128
  logger.error(f"Failed to execute work item {work_item.id}: {e}")
119
129
  await self.handle_failure(work_item.id, str(e))
@@ -155,7 +165,7 @@ class WorkExecutor:
155
165
  else:
156
166
  logger.warning(f"Failed to mark work item {work_id} as failed")
157
167
 
158
- async def handle_block(self, work_id: str, reason: str) -> None:
168
+ async def handle_block(self, work_id: str, reason: str) -> bool:
159
169
  """Handle work being blocked by an event.
160
170
 
161
171
  Called when RuntimeMonitor detects a blocking event.
@@ -164,15 +174,19 @@ class WorkExecutor:
164
174
  work_id: Work item ID that is blocked
165
175
  reason: Reason for blocking (e.g., "Waiting for approval")
166
176
 
177
+ Returns:
178
+ True if work was successfully blocked, False otherwise
179
+
167
180
  Example:
168
- >>> await executor.handle_block("work-123", "Decision needed")
181
+ >>> success = await executor.handle_block("work-123", "Decision needed")
169
182
  """
170
183
  if self.queue.block(work_id, reason):
171
184
  logger.info(f"Work item {work_id} blocked: {reason}")
172
- else:
173
- logger.warning(f"Failed to mark work item {work_id} as blocked")
185
+ return True
186
+ logger.warning(f"Failed to mark work item {work_id} as blocked")
187
+ return False
174
188
 
175
- async def handle_unblock(self, work_id: str) -> None:
189
+ async def handle_unblock(self, work_id: str) -> bool:
176
190
  """Handle work being unblocked after event resolution.
177
191
 
178
192
  Called when EventHandler resolves a blocking event.
@@ -180,10 +194,14 @@ class WorkExecutor:
180
194
  Args:
181
195
  work_id: Work item ID to unblock
182
196
 
197
+ Returns:
198
+ True if work was successfully unblocked, False otherwise
199
+
183
200
  Example:
184
- >>> await executor.handle_unblock("work-123")
201
+ >>> success = await executor.handle_unblock("work-123")
185
202
  """
186
203
  if self.queue.unblock(work_id):
187
204
  logger.info(f"Work item {work_id} unblocked, resuming execution")
188
- else:
189
- logger.warning(f"Failed to unblock work item {work_id}")
205
+ return True
206
+ logger.warning(f"Failed to unblock work item {work_id}")
207
+ return False
@@ -5,12 +5,15 @@ user input and coordinates session pause/resume.
5
5
  """
6
6
 
7
7
  import logging
8
- from typing import Dict, List, Optional
8
+ from typing import TYPE_CHECKING, Dict, List, Optional
9
9
 
10
10
  from ..inbox import Inbox
11
11
  from ..models.events import BLOCKING_EVENTS, Event, EventStatus
12
12
  from ..project_session import ProjectSession
13
13
 
14
+ if TYPE_CHECKING:
15
+ from ..core.block_manager import BlockManager
16
+
14
17
  logger = logging.getLogger(__name__)
15
18
 
16
19
 
@@ -32,13 +35,17 @@ class EventHandler:
32
35
  """
33
36
 
34
37
  def __init__(
35
- self, inbox: Inbox, session_manager: Dict[str, ProjectSession]
38
+ self,
39
+ inbox: Inbox,
40
+ session_manager: Dict[str, ProjectSession],
41
+ block_manager: Optional["BlockManager"] = None,
36
42
  ) -> None:
37
43
  """Initialize event handler.
38
44
 
39
45
  Args:
40
46
  inbox: Inbox instance for event access
41
47
  session_manager: Dict mapping project_id -> ProjectSession
48
+ block_manager: Optional BlockManager for automatic work unblocking
42
49
 
43
50
  Raises:
44
51
  ValueError: If inbox or session_manager is None
@@ -51,8 +58,12 @@ class EventHandler:
51
58
  self.inbox = inbox
52
59
  self.session_manager = session_manager
53
60
  self._event_manager = inbox.events
61
+ self.block_manager = block_manager
54
62
 
55
- logger.debug("EventHandler initialized")
63
+ logger.debug(
64
+ "EventHandler initialized (block_manager: %s)",
65
+ "enabled" if block_manager else "disabled",
66
+ )
56
67
 
57
68
  async def process_event(self, event: Event) -> None:
58
69
  """Process an event - pause session if blocking.
@@ -137,6 +148,17 @@ class EventHandler:
137
148
  # Mark event as resolved
138
149
  self._event_manager.respond(event_id, response)
139
150
 
151
+ # Automatically unblock work items if BlockManager is available
152
+ if self.block_manager and was_blocking:
153
+ unblocked_work = await self.block_manager.check_and_unblock(event_id)
154
+ if unblocked_work:
155
+ logger.info(
156
+ "Event %s resolution unblocked %d work items: %s",
157
+ event_id,
158
+ len(unblocked_work),
159
+ unblocked_work,
160
+ )
161
+
140
162
  # If event was NOT blocking, no need to resume
141
163
  if not was_blocking:
142
164
  logger.debug("Event %s was non-blocking, no resume needed", event_id)
@@ -1,5 +1,7 @@
1
1
  """Claude runner with both exec and subprocess launch methods."""
2
2
 
3
+ import hashlib
4
+ import json
3
5
  import os
4
6
  from pathlib import Path
5
7
  from typing import Optional
@@ -211,9 +213,137 @@ class ClaudeRunner:
211
213
  }
212
214
  )
213
215
 
216
+ def _get_deployment_state_path(self) -> Path:
217
+ """Get path to deployment state file."""
218
+ return Path.cwd() / ".claude-mpm" / "deployment-state.json"
219
+
220
+ def _calculate_deployment_hash(self, agents_dir: Path) -> str:
221
+ """Calculate hash of all agent files for change detection.
222
+
223
+ Args:
224
+ agents_dir: Directory containing agent .md files
225
+
226
+ Returns:
227
+ SHA256 hash of agent file contents
228
+ """
229
+ if not agents_dir.exists():
230
+ return ""
231
+
232
+ # Get all .md files sorted for consistent hashing
233
+ agent_files = sorted(agents_dir.glob("*.md"))
234
+
235
+ hash_obj = hashlib.sha256()
236
+ for agent_file in agent_files:
237
+ # Include filename and content in hash
238
+ hash_obj.update(agent_file.name.encode())
239
+ try:
240
+ hash_obj.update(agent_file.read_bytes())
241
+ except Exception as e:
242
+ self.logger.debug(f"Error reading {agent_file} for hash: {e}")
243
+
244
+ return hash_obj.hexdigest()
245
+
246
+ def _check_deployment_state(self) -> bool:
247
+ """Check if agents are already deployed and up-to-date.
248
+
249
+ Returns:
250
+ True if agents are already deployed and match current version, False otherwise
251
+ """
252
+ state_file = self._get_deployment_state_path()
253
+ agents_dir = Path.cwd() / ".claude" / "agents"
254
+
255
+ # If state file doesn't exist, need to deploy
256
+ if not state_file.exists():
257
+ return False
258
+
259
+ # If agents directory doesn't exist, need to deploy
260
+ if not agents_dir.exists():
261
+ return False
262
+
263
+ try:
264
+ # Load deployment state
265
+ state_data = json.loads(state_file.read_text())
266
+
267
+ # Get current version from package
268
+ from claude_mpm import __version__
269
+
270
+ # Check if version matches
271
+ if state_data.get("version") != __version__:
272
+ self.logger.debug(
273
+ f"Version mismatch: {state_data.get('version')} != {__version__}"
274
+ )
275
+ return False
276
+
277
+ # Check if agent count and hash match
278
+ current_hash = self._calculate_deployment_hash(agents_dir)
279
+ stored_hash = state_data.get("deployment_hash", "")
280
+
281
+ if current_hash != stored_hash:
282
+ self.logger.debug("Agent deployment hash mismatch")
283
+ return False
284
+
285
+ # All checks passed - agents are already deployed
286
+ agent_count = state_data.get("agent_count", 0)
287
+ self.logger.debug(
288
+ f"Agents already deployed: {agent_count} agents (v{__version__})"
289
+ )
290
+ return True
291
+
292
+ except Exception as e:
293
+ self.logger.debug(f"Error checking deployment state: {e}")
294
+ return False
295
+
296
+ def _save_deployment_state(self, agent_count: int) -> None:
297
+ """Save current deployment state.
298
+
299
+ Args:
300
+ agent_count: Number of agents deployed
301
+ """
302
+ state_file = self._get_deployment_state_path()
303
+ agents_dir = Path.cwd() / ".claude" / "agents"
304
+
305
+ try:
306
+ import time
307
+
308
+ from claude_mpm import __version__
309
+
310
+ # Calculate deployment hash
311
+ deployment_hash = self._calculate_deployment_hash(agents_dir)
312
+
313
+ # Create state data
314
+ state_data = {
315
+ "version": __version__,
316
+ "agent_count": agent_count,
317
+ "deployment_hash": deployment_hash,
318
+ "deployed_at": time.time(),
319
+ }
320
+
321
+ # Ensure directory exists
322
+ state_file.parent.mkdir(parents=True, exist_ok=True)
323
+
324
+ # Write state file
325
+ state_file.write_text(json.dumps(state_data, indent=2))
326
+ self.logger.debug(f"Saved deployment state: {agent_count} agents")
327
+
328
+ except Exception as e:
329
+ self.logger.debug(f"Error saving deployment state: {e}")
330
+
214
331
  def setup_agents(self) -> bool:
215
332
  """Deploy native agents to .claude/agents/."""
216
333
  try:
334
+ # Check if agents are already deployed and up-to-date
335
+ if self._check_deployment_state():
336
+ agents_dir = Path.cwd() / ".claude" / "agents"
337
+ agent_count = len(list(agents_dir.glob("*.md")))
338
+ print(f"✓ Agents: {agent_count} cached")
339
+ if self.project_logger:
340
+ self.project_logger.log_system(
341
+ f"Agents already deployed: {agent_count} cached",
342
+ level="INFO",
343
+ component="deployment",
344
+ )
345
+ return True
346
+
217
347
  if self.project_logger:
218
348
  self.project_logger.log_system(
219
349
  "Starting agent deployment", level="INFO", component="deployment"
@@ -239,6 +369,12 @@ class ClaudeRunner:
239
369
 
240
370
  # Set Claude environment
241
371
  self.deployment_service.set_claude_environment()
372
+
373
+ # Save deployment state for future runs
374
+ agents_dir = Path.cwd() / ".claude" / "agents"
375
+ total_agents = len(list(agents_dir.glob("*.md")))
376
+ self._save_deployment_state(total_agents)
377
+
242
378
  return True
243
379
  self.logger.info("All agents already up to date")
244
380
  if self.project_logger:
@@ -247,6 +383,13 @@ class ClaudeRunner:
247
383
  level="INFO",
248
384
  component="deployment",
249
385
  )
386
+
387
+ # Save deployment state even if no changes
388
+ agents_dir = Path.cwd() / ".claude" / "agents"
389
+ if agents_dir.exists():
390
+ total_agents = len(list(agents_dir.glob("*.md")))
391
+ self._save_deployment_state(total_agents)
392
+
250
393
  return True
251
394
 
252
395
  except PermissionError as e:
@@ -297,6 +297,9 @@ class OutputStyleManager:
297
297
  target_path = style_config["target"]
298
298
  style_name = style_config["name"]
299
299
 
300
+ # Check if this is a fresh install (file doesn't exist yet)
301
+ is_fresh_install = not target_path.exists()
302
+
300
303
  # If content not provided, read from source
301
304
  if content is None:
302
305
  content = self.extract_output_style_content(style=style)
@@ -310,7 +313,9 @@ class OutputStyleManager:
310
313
 
311
314
  # Activate the style if requested
312
315
  if activate:
313
- self._activate_output_style(style_name)
316
+ self._activate_output_style(
317
+ style_name, is_fresh_install=is_fresh_install
318
+ )
314
319
 
315
320
  return True
316
321
 
@@ -318,12 +323,21 @@ class OutputStyleManager:
318
323
  self.logger.error(f"Failed to deploy {style} style: {e}")
319
324
  return False
320
325
 
321
- def _activate_output_style(self, style_name: str = "Claude MPM") -> bool:
326
+ def _activate_output_style(
327
+ self, style_name: str = "Claude MPM", is_fresh_install: bool = False
328
+ ) -> bool:
322
329
  """
323
330
  Update Claude Code settings to activate a specific output style.
324
331
 
332
+ Only activates the style if:
333
+ 1. No active style is currently set (first deployment), OR
334
+ 2. This is a fresh install (style file didn't exist before deployment)
335
+
336
+ This preserves user preferences if they've manually changed their active style.
337
+
325
338
  Args:
326
339
  style_name: Name of the style to activate (e.g., "Claude MPM", "Claude MPM Teacher")
340
+ is_fresh_install: Whether this is a fresh install (style file didn't exist before)
327
341
 
328
342
  Returns:
329
343
  True if activated successfully, False otherwise
@@ -342,8 +356,12 @@ class OutputStyleManager:
342
356
  # Check current active style
343
357
  current_style = settings.get("activeOutputStyle")
344
358
 
345
- # Update active output style if different
346
- if current_style != style_name:
359
+ # Only set activeOutputStyle if:
360
+ # 1. No active style is set (first deployment), OR
361
+ # 2. This is a fresh install (file didn't exist before deployment)
362
+ should_activate = current_style is None or is_fresh_install
363
+
364
+ if should_activate and current_style != style_name:
347
365
  settings["activeOutputStyle"] = style_name
348
366
 
349
367
  # Ensure settings directory exists
@@ -358,7 +376,10 @@ class OutputStyleManager:
358
376
  f"✅ Activated {style_name} output style (was: {current_style or 'none'})"
359
377
  )
360
378
  else:
361
- self.logger.debug(f"{style_name} output style already active")
379
+ self.logger.debug(
380
+ f"Preserving user preference: {current_style or 'none'} "
381
+ f"(skipping activation of {style_name})"
382
+ )
362
383
 
363
384
  return True
364
385
 
@@ -452,6 +473,10 @@ class OutputStyleManager:
452
473
  """
453
474
  results: Dict[str, bool] = {}
454
475
 
476
+ # Check if professional style exists BEFORE deployment
477
+ # This determines if this is a fresh install
478
+ professional_style_existed = self.styles["professional"]["target"].exists()
479
+
455
480
  for style_type_key in self.styles:
456
481
  # Deploy without activation
457
482
  # Cast is safe because we know self.styles keys are OutputStyleType
@@ -459,9 +484,11 @@ class OutputStyleManager:
459
484
  success = self.deploy_output_style(style=style_type, activate=False)
460
485
  results[style_type] = success
461
486
 
462
- # Activate the default style if requested
487
+ # Activate the default style if requested AND this is first deployment
463
488
  if activate_default and results.get("professional", False):
464
- self._activate_output_style("Claude MPM")
489
+ self._activate_output_style(
490
+ "Claude MPM", is_fresh_install=not professional_style_existed
491
+ )
465
492
 
466
493
  return results
467
494
 
File without changes
@@ -126,6 +126,15 @@ class EventHandlers:
126
126
  # Response tracking is optional - silently continue if it fails
127
127
  pass
128
128
 
129
+ # Record user message for auto-pause if active
130
+ auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
131
+ if auto_pause and auto_pause.is_pause_active():
132
+ try:
133
+ auto_pause.on_user_message(prompt)
134
+ except Exception as e:
135
+ if DEBUG:
136
+ _log(f"Auto-pause user message recording error: {e}")
137
+
129
138
  # Emit normalized event (namespace no longer needed with normalized events)
130
139
  self.hook_handler._emit_socketio_event("", "user_prompt", prompt_data)
131
140
 
@@ -603,6 +612,19 @@ class EventHandlers:
603
612
  if DEBUG:
604
613
  _log(f"Auto-pause error in handle_stop_fast: {e}")
605
614
 
615
+ # Finalize pause session if active
616
+ try:
617
+ if auto_pause.is_pause_active():
618
+ session_file = auto_pause.on_session_end()
619
+ if session_file:
620
+ if DEBUG:
621
+ _log(
622
+ f"✅ Auto-pause session finalized: {session_file.name}"
623
+ )
624
+ except Exception as e:
625
+ if DEBUG:
626
+ _log(f"❌ Failed to finalize auto-pause session: {e}")
627
+
606
628
  # Track response if enabled
607
629
  try:
608
630
  rtm = getattr(self.hook_handler, "response_tracking_manager", None)
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -24,7 +24,7 @@ Legacy System (maintained for compatibility):
24
24
  from .agent_skills_injector import AgentSkillsInjector
25
25
 
26
26
  # Legacy System (maintained for compatibility)
27
- from .registry import Skill, SkillsRegistry, get_registry
27
+ from .registry import Skill, SkillsRegistry, get_registry, validate_agentskills_spec
28
28
  from .skill_manager import SkillManager
29
29
  from .skills_registry import SkillsRegistry as SkillsRegistryHelper
30
30
  from .skills_service import SkillsService
@@ -39,4 +39,5 @@ __all__ = [
39
39
  # New Skills Integration System
40
40
  "SkillsService",
41
41
  "get_registry",
42
+ "validate_agentskills_spec",
42
43
  ]