claude-mpm 4.2.39__py3-none-any.whl → 4.2.42__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.
Files changed (39) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/BASE_ENGINEER.md +114 -1
  3. claude_mpm/agents/BASE_OPS.md +156 -1
  4. claude_mpm/agents/INSTRUCTIONS.md +120 -11
  5. claude_mpm/agents/WORKFLOW.md +160 -10
  6. claude_mpm/agents/templates/agentic-coder-optimizer.json +17 -12
  7. claude_mpm/agents/templates/react_engineer.json +217 -0
  8. claude_mpm/agents/templates/web_qa.json +40 -4
  9. claude_mpm/cli/__init__.py +3 -5
  10. claude_mpm/commands/mpm-browser-monitor.md +370 -0
  11. claude_mpm/commands/mpm-monitor.md +177 -0
  12. claude_mpm/dashboard/static/built/components/code-viewer.js +1076 -2
  13. claude_mpm/dashboard/static/built/components/ui-state-manager.js +465 -2
  14. claude_mpm/dashboard/static/css/dashboard.css +2 -0
  15. claude_mpm/dashboard/static/js/browser-console-monitor.js +495 -0
  16. claude_mpm/dashboard/static/js/components/browser-log-viewer.js +763 -0
  17. claude_mpm/dashboard/static/js/components/code-viewer.js +931 -340
  18. claude_mpm/dashboard/static/js/components/diff-viewer.js +891 -0
  19. claude_mpm/dashboard/static/js/components/file-change-tracker.js +443 -0
  20. claude_mpm/dashboard/static/js/components/file-change-viewer.js +690 -0
  21. claude_mpm/dashboard/static/js/components/ui-state-manager.js +307 -19
  22. claude_mpm/dashboard/static/js/socket-client.js +2 -2
  23. claude_mpm/dashboard/static/test-browser-monitor.html +470 -0
  24. claude_mpm/dashboard/templates/index.html +62 -99
  25. claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
  26. claude_mpm/services/monitor/daemon.py +69 -36
  27. claude_mpm/services/monitor/daemon_manager.py +186 -29
  28. claude_mpm/services/monitor/handlers/browser.py +451 -0
  29. claude_mpm/services/monitor/server.py +272 -5
  30. {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/METADATA +1 -1
  31. {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/RECORD +35 -29
  32. claude_mpm/agents/templates/agentic-coder-optimizer.md +0 -44
  33. claude_mpm/agents/templates/agentic_coder_optimizer.json +0 -238
  34. claude_mpm/agents/templates/test-non-mpm.json +0 -20
  35. claude_mpm/dashboard/static/dist/components/code-viewer.js +0 -2
  36. {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/WHEEL +0 -0
  37. {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/entry_points.txt +0 -0
  38. {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/licenses/LICENSE +0 -0
  39. {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/top_level.txt +0 -0
@@ -149,7 +149,7 @@ class DaemonManager:
149
149
  # Try IPv4 first (most common)
150
150
  test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
151
151
  test_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
152
-
152
+
153
153
  # Use 127.0.0.1 for localhost to match what the server does
154
154
  bind_host = "127.0.0.1" if self.host == "localhost" else self.host
155
155
  test_sock.bind((bind_host, self.port))
@@ -201,7 +201,10 @@ class DaemonManager:
201
201
  try:
202
202
  # Find processes using the port
203
203
  result = subprocess.run(
204
- ["lsof", "-ti", f":{self.port}"], capture_output=True, text=True, check=False
204
+ ["lsof", "-ti", f":{self.port}"],
205
+ capture_output=True,
206
+ text=True,
207
+ check=False,
205
208
  )
206
209
 
207
210
  if result.returncode != 0 or not result.stdout.strip():
@@ -220,35 +223,41 @@ class DaemonManager:
220
223
  process_info = subprocess.run(
221
224
  ["ps", "-p", str(pid), "-o", "comm="],
222
225
  capture_output=True,
223
- text=True, check=False,
226
+ text=True,
227
+ check=False,
224
228
  )
225
229
 
226
230
  # Get full command to check if it's our monitor process
227
231
  cmd_info = subprocess.run(
228
232
  ["ps", "-p", str(pid), "-o", "command="],
229
233
  capture_output=True,
230
- text=True, check=False,
234
+ text=True,
235
+ check=False,
231
236
  )
232
-
237
+
233
238
  if cmd_info.returncode != 0:
234
239
  continue
235
-
240
+
236
241
  full_command = cmd_info.stdout.strip().lower()
237
242
  process_name = process_info.stdout.strip().lower()
238
-
243
+
239
244
  # Check if this is our monitor/socketio process specifically
240
245
  # Look for monitor, socketio, dashboard, or our specific port
241
- is_monitor = any([
242
- "monitor" in full_command,
243
- "socketio" in full_command,
244
- "dashboard" in full_command,
245
- f"port={self.port}" in full_command,
246
- f":{self.port}" in full_command,
247
- "unified_monitor" in full_command,
248
- ])
249
-
246
+ is_monitor = any(
247
+ [
248
+ "monitor" in full_command,
249
+ "socketio" in full_command,
250
+ "dashboard" in full_command,
251
+ f"port={self.port}" in full_command,
252
+ f":{self.port}" in full_command,
253
+ "unified_monitor" in full_command,
254
+ ]
255
+ )
256
+
250
257
  if is_monitor and "python" in process_name:
251
- self.logger.info(f"Killing monitor process {pid}: {full_command[:100]}")
258
+ self.logger.info(
259
+ f"Killing monitor process {pid}: {full_command[:100]}"
260
+ )
252
261
  os.kill(pid, signal.SIGTERM)
253
262
 
254
263
  # Wait briefly for graceful shutdown
@@ -329,7 +338,7 @@ class DaemonManager:
329
338
 
330
339
  def _kill_claude_mpm_processes(self) -> bool:
331
340
  """Kill any claude-mpm monitor processes specifically.
332
-
341
+
333
342
  This targets monitor/dashboard/socketio processes only,
334
343
  NOT general Claude instances.
335
344
 
@@ -338,7 +347,9 @@ class DaemonManager:
338
347
  """
339
348
  try:
340
349
  # Look for monitor-specific processes
341
- result = subprocess.run(["ps", "aux"], capture_output=True, text=True, check=False)
350
+ result = subprocess.run(
351
+ ["ps", "aux"], capture_output=True, text=True, check=False
352
+ )
342
353
 
343
354
  if result.returncode != 0:
344
355
  return False
@@ -349,10 +360,14 @@ class DaemonManager:
349
360
  for line in lines:
350
361
  line_lower = line.lower()
351
362
  # Only target monitor/dashboard/socketio processes
352
- if any(["monitor" in line_lower and "claude" in line_lower,
363
+ if any(
364
+ [
365
+ "monitor" in line_lower and "claude" in line_lower,
353
366
  "dashboard" in line_lower and "claude" in line_lower,
354
367
  "socketio" in line_lower,
355
- f":{self.port}" in line_lower and "python" in line_lower]):
368
+ f":{self.port}" in line_lower and "python" in line_lower,
369
+ ]
370
+ ):
356
371
  parts = line.split()
357
372
  if len(parts) > 1:
358
373
  try:
@@ -395,7 +410,8 @@ class DaemonManager:
395
410
  process_info = subprocess.run(
396
411
  ["ps", "-p", str(pid), "-o", "comm="],
397
412
  capture_output=True,
398
- text=True, check=False,
413
+ text=True,
414
+ check=False,
399
415
  )
400
416
 
401
417
  if "python" in process_info.stdout.lower():
@@ -441,7 +457,10 @@ class DaemonManager:
441
457
  """
442
458
  try:
443
459
  result = subprocess.run(
444
- ["lsof", "-ti", f":{self.port}"], capture_output=True, text=True, check=False
460
+ ["lsof", "-ti", f":{self.port}"],
461
+ capture_output=True,
462
+ text=True,
463
+ check=False,
445
464
  )
446
465
 
447
466
  if result.returncode == 0 and result.stdout.strip():
@@ -485,9 +504,145 @@ class DaemonManager:
485
504
  self.logger.error(f"Cannot start daemon - port {self.port} is in use")
486
505
  return False
487
506
 
488
- # Daemonize the process
507
+ # Use subprocess for clean daemon startup (v4.2.40)
508
+ # This avoids fork() issues with Python threading
509
+ if self.use_subprocess_daemon():
510
+ return self.start_daemon_subprocess()
511
+ # Fallback to traditional fork (kept for compatibility)
489
512
  return self.daemonize()
490
513
 
514
+ def use_subprocess_daemon(self) -> bool:
515
+ """Check if we should use subprocess instead of fork for daemonization.
516
+
517
+ Returns:
518
+ True to use subprocess (safer), False to use traditional fork
519
+ """
520
+ # Check if we're already in a subprocess to prevent infinite recursion
521
+ if os.environ.get("CLAUDE_MPM_SUBPROCESS_DAEMON") == "1":
522
+ # We're already in a subprocess, use traditional fork
523
+ return False
524
+
525
+ # Otherwise, use subprocess for monitor daemon to avoid threading issues
526
+ return True
527
+
528
+ def start_daemon_subprocess(self) -> bool:
529
+ """Start daemon using subprocess.Popen for clean process isolation.
530
+
531
+ This avoids all the fork() + threading issues by starting the monitor
532
+ in a completely fresh process with no inherited threads or locks.
533
+
534
+ Returns:
535
+ True if daemon started successfully
536
+ """
537
+ try:
538
+ # Build command to run monitor in foreground mode in subprocess
539
+ import sys
540
+
541
+ python_exe = sys.executable
542
+
543
+ # Run 'claude-mpm monitor start' in subprocess with environment variable
544
+ # to indicate we're already in a subprocess (prevents infinite recursion)
545
+ cmd = [
546
+ python_exe,
547
+ "-m",
548
+ "claude_mpm.cli",
549
+ "monitor",
550
+ "start",
551
+ "--background", # Run as daemon
552
+ "--port",
553
+ str(self.port),
554
+ "--host",
555
+ self.host,
556
+ ]
557
+
558
+ # Set environment variable to prevent recursive subprocess creation
559
+ env = os.environ.copy()
560
+ env["CLAUDE_MPM_SUBPROCESS_DAEMON"] = "1"
561
+
562
+ self.logger.info(f"Starting monitor daemon via subprocess: {' '.join(cmd)}")
563
+
564
+ # Open log file for output redirection
565
+ log_file_handle = None
566
+ if self.log_file:
567
+ log_file_handle = open(self.log_file, "a")
568
+ log_file = log_file_handle
569
+ else:
570
+ log_file = subprocess.DEVNULL
571
+
572
+ try:
573
+ # Start the subprocess detached from parent
574
+ # Redirect stdout/stderr to log file to capture output
575
+ process = subprocess.Popen(
576
+ cmd,
577
+ stdin=subprocess.DEVNULL,
578
+ stdout=log_file,
579
+ stderr=subprocess.STDOUT if self.log_file else subprocess.DEVNULL,
580
+ start_new_session=True, # Create new process group
581
+ close_fds=(
582
+ False if self.log_file else True
583
+ ), # Keep log file open if redirecting
584
+ env=env, # Pass modified environment
585
+ )
586
+
587
+ # Close the log file handle now that subprocess has it
588
+ if log_file_handle:
589
+ log_file_handle.close()
590
+
591
+ # Get the process PID
592
+ pid = process.pid
593
+ self.logger.info(f"Monitor subprocess started with PID {pid}")
594
+
595
+ # Wait for the subprocess to write its PID file
596
+ # The subprocess will write the PID file after it starts successfully
597
+ max_wait = 10 # seconds
598
+ start_time = time.time()
599
+
600
+ while time.time() - start_time < max_wait:
601
+ # Check if process is still running
602
+ if process.poll() is not None:
603
+ # Process exited
604
+ self.logger.error(
605
+ f"Monitor daemon exited with code {process.returncode}"
606
+ )
607
+ return False
608
+
609
+ # Check if PID file was written
610
+ if self.pid_file.exists():
611
+ try:
612
+ with open(self.pid_file) as f:
613
+ written_pid = int(f.read().strip())
614
+ if written_pid == pid:
615
+ # PID file written correctly, check port
616
+ if (
617
+ not self._is_port_available()
618
+ ): # Port NOT available means it's in use (good!)
619
+ self.logger.info(
620
+ f"Monitor daemon successfully started on port {self.port}"
621
+ )
622
+ return True
623
+ except:
624
+ pass # PID file not ready yet
625
+
626
+ time.sleep(0.5)
627
+
628
+ # Timeout waiting for daemon to start
629
+ self.logger.error("Timeout waiting for monitor daemon to start")
630
+ # Try to kill the process if it's still running
631
+ if process.poll() is None:
632
+ process.terminate()
633
+ time.sleep(1)
634
+ if process.poll() is None:
635
+ process.kill()
636
+ return False
637
+ finally:
638
+ # Clean up log file handle if still open
639
+ if log_file_handle and not log_file_handle.closed:
640
+ log_file_handle.close()
641
+
642
+ except Exception as e:
643
+ self.logger.error(f"Failed to start daemon via subprocess: {e}")
644
+ return False
645
+
491
646
  def daemonize(self) -> bool:
492
647
  """Daemonize the current process.
493
648
 
@@ -495,12 +650,14 @@ class DaemonManager:
495
650
  True if successful (in parent), doesn't return in child
496
651
  """
497
652
  # Guard against re-entrant execution after fork
498
- if hasattr(self, '_forking_in_progress'):
499
- self.logger.error("CRITICAL: Detected re-entrant daemonize call after fork!")
653
+ if hasattr(self, "_forking_in_progress"):
654
+ self.logger.error(
655
+ "CRITICAL: Detected re-entrant daemonize call after fork!"
656
+ )
500
657
  return False
501
-
658
+
502
659
  self._forking_in_progress = True
503
-
660
+
504
661
  try:
505
662
  # Clean up asyncio event loops before forking
506
663
  self._cleanup_event_loops()
@@ -787,7 +944,7 @@ class DaemonManager:
787
944
  f.write("success")
788
945
  f.flush() # Ensure it's written immediately
789
946
  os.fsync(f.fileno()) # Force write to disk
790
- except Exception as e:
947
+ except Exception:
791
948
  # Logging might not work in daemon process after fork
792
949
  pass
793
950