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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_ENGINEER.md +114 -1
- claude_mpm/agents/BASE_OPS.md +156 -1
- claude_mpm/agents/INSTRUCTIONS.md +120 -11
- claude_mpm/agents/WORKFLOW.md +160 -10
- claude_mpm/agents/templates/agentic-coder-optimizer.json +17 -12
- claude_mpm/agents/templates/react_engineer.json +217 -0
- claude_mpm/agents/templates/web_qa.json +40 -4
- claude_mpm/cli/__init__.py +3 -5
- claude_mpm/commands/mpm-browser-monitor.md +370 -0
- claude_mpm/commands/mpm-monitor.md +177 -0
- claude_mpm/dashboard/static/built/components/code-viewer.js +1076 -2
- claude_mpm/dashboard/static/built/components/ui-state-manager.js +465 -2
- claude_mpm/dashboard/static/css/dashboard.css +2 -0
- claude_mpm/dashboard/static/js/browser-console-monitor.js +495 -0
- claude_mpm/dashboard/static/js/components/browser-log-viewer.js +763 -0
- claude_mpm/dashboard/static/js/components/code-viewer.js +931 -340
- claude_mpm/dashboard/static/js/components/diff-viewer.js +891 -0
- claude_mpm/dashboard/static/js/components/file-change-tracker.js +443 -0
- claude_mpm/dashboard/static/js/components/file-change-viewer.js +690 -0
- claude_mpm/dashboard/static/js/components/ui-state-manager.js +307 -19
- claude_mpm/dashboard/static/js/socket-client.js +2 -2
- claude_mpm/dashboard/static/test-browser-monitor.html +470 -0
- claude_mpm/dashboard/templates/index.html +62 -99
- claude_mpm/services/cli/unified_dashboard_manager.py +1 -1
- claude_mpm/services/monitor/daemon.py +69 -36
- claude_mpm/services/monitor/daemon_manager.py +186 -29
- claude_mpm/services/monitor/handlers/browser.py +451 -0
- claude_mpm/services/monitor/server.py +272 -5
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/METADATA +1 -1
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/RECORD +35 -29
- claude_mpm/agents/templates/agentic-coder-optimizer.md +0 -44
- claude_mpm/agents/templates/agentic_coder_optimizer.json +0 -238
- claude_mpm/agents/templates/test-non-mpm.json +0 -20
- claude_mpm/dashboard/static/dist/components/code-viewer.js +0 -2
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/WHEEL +0 -0
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.2.39.dist-info → claude_mpm-4.2.42.dist-info}/licenses/LICENSE +0 -0
- {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}"],
|
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,
|
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,
|
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
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
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(
|
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(
|
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(
|
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,
|
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}"],
|
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
|
-
#
|
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,
|
499
|
-
self.logger.error(
|
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
|
947
|
+
except Exception:
|
791
948
|
# Logging might not work in daemon process after fork
|
792
949
|
pass
|
793
950
|
|