claude-comms 1.0.39 → 1.0.41

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.
@@ -21,20 +21,38 @@ import json
21
21
  import os
22
22
  import signal
23
23
  import sys
24
+ import time
25
+ import uuid
24
26
  from pathlib import Path
25
27
  from typing import Any, Dict, Optional
26
28
 
27
29
  # State directory
28
30
  STATE_DIR = Path.home() / ".browsertools"
29
- SOCKET_PATH = STATE_DIR / "daemon.sock"
30
- PID_FILE = STATE_DIR / "daemon.pid"
31
31
  CONFIG_FILE = STATE_DIR / "config.json"
32
+ INACTIVITY_TIMEOUT = 600 # 10 minutes
32
33
 
33
34
  # Global daemon state
34
35
  mcp_proc = None
35
36
  mcp_reader = None
36
37
  mcp_writer = None
37
38
  pending_requests = {} # msg_id -> response_future
39
+ last_activity_time = time.time()
40
+ current_instance_id = None
41
+
42
+
43
+ def generate_instance_id() -> str:
44
+ """Generate 8-char hex instance ID."""
45
+ return uuid.uuid4().hex[:8]
46
+
47
+
48
+ def get_socket_path(instance_id: str) -> Path:
49
+ """Get socket path for instance."""
50
+ return STATE_DIR / f"daemon-{instance_id}.sock"
51
+
52
+
53
+ def get_pid_file(instance_id: str) -> Path:
54
+ """Get PID file path for instance."""
55
+ return STATE_DIR / f"daemon-{instance_id}.pid"
38
56
 
39
57
 
40
58
  def ensure_state_dir():
@@ -42,22 +60,39 @@ def ensure_state_dir():
42
60
  STATE_DIR.mkdir(exist_ok=True)
43
61
 
44
62
 
45
- def get_daemon_pid() -> Optional[int]:
46
- """Get PID of running daemon."""
47
- if not PID_FILE.exists():
63
+ def get_daemon_pid(instance_id: str) -> Optional[int]:
64
+ """Get PID of running daemon for instance."""
65
+ pid_file = get_pid_file(instance_id)
66
+ if not pid_file.exists():
48
67
  return None
49
68
  try:
50
- pid = int(PID_FILE.read_text().strip())
69
+ pid = int(pid_file.read_text().strip())
51
70
  os.kill(pid, 0) # Check if alive
52
71
  return pid
53
72
  except (ProcessLookupError, ValueError):
54
- PID_FILE.unlink(missing_ok=True)
73
+ pid_file.unlink(missing_ok=True)
55
74
  return None
56
75
 
57
76
 
58
- def is_daemon_running() -> bool:
59
- """Check if daemon is running."""
60
- return get_daemon_pid() is not None and SOCKET_PATH.exists()
77
+ def is_daemon_running(instance_id: str) -> bool:
78
+ """Check if daemon is running for instance."""
79
+ return get_daemon_pid(instance_id) is not None and get_socket_path(instance_id).exists()
80
+
81
+
82
+ def get_all_instances() -> list[str]:
83
+ """Get list of all running instance IDs."""
84
+ instances = []
85
+ if not STATE_DIR.exists():
86
+ return instances
87
+ for pid_file in STATE_DIR.glob("daemon-*.pid"):
88
+ instance_id = pid_file.stem.replace("daemon-", "")
89
+ if is_daemon_running(instance_id):
90
+ instances.append(instance_id)
91
+ else:
92
+ # Clean up stale files
93
+ pid_file.unlink(missing_ok=True)
94
+ get_socket_path(instance_id).unlink(missing_ok=True)
95
+ return instances
61
96
 
62
97
 
63
98
  def load_config() -> Dict[str, Any]:
@@ -145,7 +180,10 @@ async def read_mcp_responses():
145
180
 
146
181
  async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
147
182
  """Handle a client connection."""
148
- global mcp_writer, pending_requests
183
+ global mcp_writer, pending_requests, last_activity_time
184
+
185
+ # Update activity timestamp
186
+ last_activity_time = time.time()
149
187
 
150
188
  try:
151
189
  while True:
@@ -222,15 +260,32 @@ async def shutdown_mcp():
222
260
  print(f"Error during shutdown: {e}", file=sys.stderr)
223
261
 
224
262
 
225
- async def run_daemon():
263
+ async def inactivity_monitor(shutdown_callback):
264
+ """Monitor for inactivity and trigger shutdown after timeout."""
265
+ global last_activity_time
266
+
267
+ while True:
268
+ await asyncio.sleep(10) # Check every 10 seconds for responsive timeout
269
+ inactive_time = time.time() - last_activity_time
270
+ if inactive_time > INACTIVITY_TIMEOUT:
271
+ await shutdown_callback()
272
+ return
273
+
274
+
275
+ async def run_daemon(instance_id: str):
226
276
  """Run the persistent daemon."""
227
- global mcp_proc, mcp_reader, mcp_writer
277
+ global mcp_proc, mcp_reader, mcp_writer, current_instance_id, last_activity_time
228
278
 
279
+ current_instance_id = instance_id
280
+ last_activity_time = time.time()
229
281
  ensure_state_dir()
230
282
 
283
+ socket_path = get_socket_path(instance_id)
284
+ pid_file = get_pid_file(instance_id)
285
+
231
286
  # Clean up old socket if exists
232
- if SOCKET_PATH.exists():
233
- SOCKET_PATH.unlink()
287
+ if socket_path.exists():
288
+ socket_path.unlink()
234
289
 
235
290
  # Load config
236
291
  config = load_config()
@@ -255,29 +310,38 @@ async def run_daemon():
255
310
  mcp_writer = mcp_proc.stdin
256
311
 
257
312
  # Write PID file
258
- PID_FILE.write_text(str(os.getpid()))
313
+ pid_file.write_text(str(os.getpid()))
259
314
 
260
- print(f"Starting Unix socket server at {SOCKET_PATH}...", file=sys.stderr)
315
+ print(f"Starting Unix socket server at {socket_path}...", file=sys.stderr)
261
316
 
262
317
  # Start Unix socket server
263
318
  server = await asyncio.start_unix_server(
264
319
  handle_client,
265
- path=str(SOCKET_PATH)
320
+ path=str(socket_path)
266
321
  )
267
322
 
268
- print(f"Daemon ready. PID: {os.getpid()}", file=sys.stderr)
323
+ print(f"Daemon ready. Instance: {instance_id} PID: {os.getpid()}", file=sys.stderr)
324
+
325
+ # Output instance ID to stdout for capture (all other output goes to stderr)
326
+ print(instance_id)
269
327
 
270
328
  # Setup signal handlers for graceful shutdown
271
329
  loop = asyncio.get_event_loop()
272
-
273
- async def handle_shutdown(sig):
274
- print(f"\nReceived {sig.name}, shutting down...", file=sys.stderr)
330
+ shutdown_triggered = asyncio.Event()
331
+
332
+ async def handle_shutdown(sig=None):
333
+ if shutdown_triggered.is_set():
334
+ return
335
+ shutdown_triggered.set()
336
+ if sig:
337
+ print(f"\nReceived {sig.name}, shutting down...", file=sys.stderr)
275
338
  await shutdown_mcp()
276
339
  server.close()
277
340
  await server.wait_closed()
278
- SOCKET_PATH.unlink(missing_ok=True)
279
- PID_FILE.unlink(missing_ok=True)
280
- loop.stop()
341
+ socket_path.unlink(missing_ok=True)
342
+ pid_file.unlink(missing_ok=True)
343
+ # Exit cleanly instead of stopping loop (avoids RuntimeError)
344
+ os._exit(0)
281
345
 
282
346
  for sig in (signal.SIGTERM, signal.SIGINT):
283
347
  loop.add_signal_handler(
@@ -285,35 +349,39 @@ async def run_daemon():
285
349
  lambda s=sig: asyncio.create_task(handle_shutdown(s))
286
350
  )
287
351
 
288
- # Run both the socket server and MCP response reader
352
+ # Run socket server, MCP response reader, and inactivity monitor
289
353
  try:
290
354
  await asyncio.gather(
291
355
  server.serve_forever(),
292
356
  read_mcp_responses(),
357
+ inactivity_monitor(handle_shutdown),
293
358
  )
294
359
  except KeyboardInterrupt:
295
360
  print("\nShutting down daemon...", file=sys.stderr)
296
361
  finally:
297
362
  # Cleanup
298
- await shutdown_mcp()
299
- server.close()
300
- await server.wait_closed()
301
- SOCKET_PATH.unlink(missing_ok=True)
302
- PID_FILE.unlink(missing_ok=True)
363
+ if not shutdown_triggered.is_set():
364
+ await shutdown_mcp()
365
+ server.close()
366
+ await server.wait_closed()
367
+ socket_path.unlink(missing_ok=True)
368
+ pid_file.unlink(missing_ok=True)
303
369
 
304
370
 
305
371
  # ============================================================================
306
372
  # CLIENT MODE - Connect to daemon and send commands
307
373
  # ============================================================================
308
374
 
309
- async def send_command(tool_name: str, args: Dict[str, Any]) -> Any:
375
+ async def send_command(instance_id: str, tool_name: str, args: Dict[str, Any]) -> Any:
310
376
  """Send a command to the daemon via socket."""
311
- if not is_daemon_running():
312
- raise RuntimeError("Daemon not running. Start with: bt daemon start")
377
+ if not is_daemon_running(instance_id):
378
+ raise RuntimeError(f"Daemon instance '{instance_id}' not running. Start with: browsertools.py daemon start")
379
+
380
+ socket_path = get_socket_path(instance_id)
313
381
 
314
382
  # Connect to daemon's Unix socket with increased limit for large responses
315
383
  reader, writer = await asyncio.open_unix_connection(
316
- str(SOCKET_PATH),
384
+ str(socket_path),
317
385
  limit=1024 * 1024 * 10 # 10MB limit for large snapshots
318
386
  )
319
387
 
@@ -372,7 +440,7 @@ def clean_output(result: Any) -> str:
372
440
  return str(result)
373
441
 
374
442
 
375
- async def execute_command(cmd: str, cmd_args: Dict[str, Any]):
443
+ async def execute_command(instance_id: str, cmd: str, cmd_args: Dict[str, Any]):
376
444
  """Execute a single command via daemon."""
377
445
 
378
446
  # Map commands to MCP tools
@@ -452,7 +520,7 @@ async def execute_command(cmd: str, cmd_args: Dict[str, Any]):
452
520
  raise ValueError(f"Unknown command: {cmd}")
453
521
 
454
522
  # Send to daemon
455
- result = await send_command(tool_name, tool_args)
523
+ result = await send_command(instance_id, tool_name, tool_args)
456
524
  return clean_output(result)
457
525
 
458
526
 
@@ -463,22 +531,22 @@ async def execute_command(cmd: str, cmd_args: Dict[str, Any]):
463
531
  async def main():
464
532
  parser = argparse.ArgumentParser(
465
533
  prog="browsertools.py",
466
- description="Chrome DevTools MCP wrapper - persistent daemon mode",
534
+ description="Chrome DevTools MCP wrapper - multi-instance daemon mode",
467
535
  formatter_class=argparse.RawDescriptionHelpFormatter,
468
536
  epilog=r"""
469
537
  browsertools.py - Chrome DevTools automation tool
470
538
 
471
-
472
539
  Usage: uv run .agents/tools/chrome-devtools/browsertools.py <command>
473
540
 
474
541
  DAEMON MANAGEMENT:
475
- daemon start Start persistent daemon + Chrome
476
- IMPORTANT: Run in background with &
477
- Example: uv run .agents/tools/chrome-devtools/browsertools.py daemon start &
478
- daemon stop Stop daemon, kill Chrome cleanly
479
- daemon status Check if daemon running
542
+ daemon start Start new daemon instance
543
+ Returns: "browsertools daemon started. Instance ID: <id>"
544
+ daemon stop <id> Stop daemon. kill chrome cleanly.
480
545
  daemon config Create config file
481
546
 
547
+ ALL OTHER COMMANDS REQUIRE --instance <id>:
548
+ --instance <id> Required for all browser commands
549
+
482
550
  NAVIGATION:
483
551
  nav <url> Navigate to URL
484
552
  --timeout <ms> Max wait time in milliseconds
@@ -491,37 +559,25 @@ INSPECTION:
491
559
  shot [path] Screenshot viewport (optional: save to path)
492
560
  eval <js> Execute JavaScript, returns JSON
493
561
  --args <json> JSON array of element references (advanced)
494
- Simple: "() => document.title"
495
- Complex: "(el) => el.innerText" --args '[{"uid":"1_23"}]'
496
562
 
497
563
  INTERACTION:
498
564
  click <uid> Click element by UID from snapshot
499
565
  fill <uid> <value> Fill input/textarea or select option
500
- key <key> Press key or combo
501
- Examples: "Enter", "Tab", "Control+A"
566
+ key <key> Press key or combo (e.g., "Enter", "Tab", "Control+A")
502
567
  hover <uid> Hover over element
503
568
  drag <from> <to> Drag element from UID to target UID
504
- fillform <json> Fill multiple form fields at once
505
- Example: '[{"uid":"1_1","value":"text"}]'
569
+ fillform <json> Fill multiple form fields
506
570
 
507
571
  DEBUGGING:
508
- conslist List console messages
509
- --types error warn Filter by type
510
- --size N Max messages
511
-
572
+ conslist List console messages (--types, --size)
512
573
  consget <msgid> Get console message details
513
-
514
- netlist List network requests
515
- --types xhr fetch Filter by resource type
516
- --size N Max requests
517
-
518
- netget [reqid] Get request details (omit reqid for currently selected)
574
+ netlist List network requests (--types, --size)
575
+ netget [reqid] Get request details
519
576
 
520
577
  PAGE MANIPULATION:
521
- resize <width> <height> Resize viewport
522
- dialog <accept|dismiss> Handle alert/confirm/prompt
523
- --text <text> Text for prompt
524
- upload <uid> <file_path> Upload file
578
+ resize <w> <h> Resize viewport
579
+ dialog <action> Handle alert/confirm/prompt (accept|dismiss)
580
+ upload <uid> <path> Upload file
525
581
 
526
582
  USAGE TIPS:
527
583
  **Snapshots** are verbose. Always try to pipe grep first
@@ -535,31 +591,34 @@ Limit output for **debugging** commands
535
591
  - Console: conslist --size 5 --types error
536
592
 
537
593
  EXAMPLES:
538
- # Login workflow
539
- uv run .agents/tools/chrome-devtools/browsertools.py daemon start &
540
- sleep 5 # Wait for daemon to be ready
541
- uv run .agents/tools/chrome-devtools/browsertools.py nav http://app.com/login
542
- uv run .agents/tools/chrome-devtools/browsertools.py snap | grep -iC5 "email\|username\|login"
543
- uv run .agents/tools/chrome-devtools/browsertools.py fill 1_23 user@example.com
544
- uv run .agents/tools/chrome-devtools/browsertools.py snap | grep -iC5 "button.*(submit|login|sign.?in)"
545
- uv run .agents/tools/chrome-devtools/browsertools.py click 1_25 | grep -iC5 "dashboard\|home\|welcome"
546
- uv run .agents/tools/chrome-devtools/browsertools.py daemon stop
547
-
548
- IMPORTANT:
549
- - UIDs only valid from latest snapshot (interaction commands return fresh ones)
550
- - Daemon maintains state (snapshots, network, console) across all commands
551
- - Always stop daemon when you are finished to clean up browser resources
594
+ # Start daemon (backgrounds automatically, prints instance ID)
595
+ uv run .agents/tools/chrome-devtools/browsertools.py daemon start
596
+ # Output: browsertools daemon started. Instance ID: a3f7b2c1
597
+
598
+ # Use instance for all commands
599
+ uv run .agents/tools/chrome-devtools/browsertools.py --instance a3f7b2c1 nav http://app.com/login
600
+ uv run .agents/tools/chrome-devtools/browsertools.py --instance a3f7b2c1 snap | grep -iC5 "email\|username\|login"
601
+ uv run .agents/tools/chrome-devtools/browsertools.py --instance a3f7b2c1 fill 1_23 user@example.com
602
+ uv run .agents/tools/chrome-devtools/browsertools.py --instance a3f7b2c1 snap | grep -iC5 "button.*(submit|login|sign.?in)"
603
+ uv run .agents/tools/chrome-devtools/browsertools.py --instance a3f7b2c1 click 1_25 | grep -iC5 "dashboard\|home\|welcome"
604
+
605
+ # Stop when finished to clean up resources
606
+ uv run .agents/tools/chrome-devtools/browsertools.py daemon stop a3f7b2c1
552
607
  """
553
608
  )
554
609
 
610
+ # Global --instance argument
611
+ parser.add_argument("--instance", "-i", help="Daemon instance ID (required for browser commands)")
612
+
555
613
  subparsers = parser.add_subparsers(dest="cmd", required=True)
556
614
 
557
615
  # Daemon management
558
616
  daemon = subparsers.add_parser("daemon", help="Manage daemon")
559
617
  daemon_sub = daemon.add_subparsers(dest="daemon_cmd", required=True)
560
- daemon_sub.add_parser("start", help="Start daemon")
561
- daemon_sub.add_parser("stop", help="Stop daemon")
562
- daemon_sub.add_parser("status", help="Check daemon status")
618
+ daemon_sub.add_parser("start", help="Start new daemon instance (returns instance ID)")
619
+ stop_parser = daemon_sub.add_parser("stop", help="Stop daemon instance")
620
+ stop_parser.add_argument("instance_id", nargs="?", help="Instance ID to stop")
621
+ stop_parser.add_argument("--all", dest="stop_all", action="store_true", help=argparse.SUPPRESS) # Hidden
563
622
  daemon_sub.add_parser("config", help="Create default config file")
564
623
 
565
624
  # Browser commands
@@ -636,37 +695,81 @@ IMPORTANT:
636
695
  # Handle daemon commands
637
696
  if args.cmd == "daemon":
638
697
  if args.daemon_cmd == "start":
639
- if is_daemon_running():
640
- print("Daemon already running", file=sys.stderr)
641
- sys.exit(1)
642
- await run_daemon()
698
+ # Generate new instance ID
699
+ instance_id = generate_instance_id()
700
+
701
+ # Fork to background
702
+ pid = os.fork()
703
+
704
+ if pid > 0:
705
+ # Parent process - print result and exit
706
+ # Give child a moment to start
707
+ time.sleep(0.5)
708
+ print(f"browsertools daemon started. Instance ID: {instance_id}")
709
+ return # Exit from async main, let asyncio.run() clean up
710
+ else:
711
+ # Child process - detach and run daemon
712
+ os.setsid() # Create new session, detach from terminal
643
713
 
644
- elif args.daemon_cmd == "stop":
645
- pid = get_daemon_pid()
646
- if not pid:
647
- print("Daemon not running", file=sys.stderr)
648
- sys.exit(1)
649
- os.kill(pid, signal.SIGTERM)
650
- print(f"Stopped daemon (PID {pid})")
714
+ # Redirect stdin/stdout/stderr to /dev/null (daemon runs silently)
715
+ devnull_r = os.open(os.devnull, os.O_RDONLY)
716
+ devnull_w = os.open(os.devnull, os.O_WRONLY)
717
+ os.dup2(devnull_r, sys.stdin.fileno())
718
+ os.dup2(devnull_w, sys.stdout.fileno())
719
+ os.dup2(devnull_w, sys.stderr.fileno())
720
+ os.close(devnull_r)
721
+ os.close(devnull_w)
722
+
723
+ # Create fresh event loop for child (parent's loop is unusable after fork)
724
+ asyncio.set_event_loop(asyncio.new_event_loop())
725
+
726
+ # Run the daemon in the new event loop (this blocks until shutdown)
727
+ asyncio.run(run_daemon(instance_id))
728
+ os._exit(0)
651
729
 
652
- elif args.daemon_cmd == "status":
653
- if is_daemon_running():
654
- print(f"Daemon running (PID {get_daemon_pid()})")
655
- print(f"Socket: {SOCKET_PATH}")
730
+ elif args.daemon_cmd == "stop":
731
+ if args.stop_all:
732
+ # Hidden --all flag: stop all running instances
733
+ instances = get_all_instances()
734
+ if not instances:
735
+ print("No running instances", file=sys.stderr)
736
+ sys.exit(1)
737
+ for inst_id in instances:
738
+ pid = get_daemon_pid(inst_id)
739
+ if pid:
740
+ os.kill(pid, signal.SIGTERM)
741
+ print(f"Stopped instance {inst_id} (PID {pid})", file=sys.stderr)
742
+ elif args.instance_id:
743
+ # Stop specific instance
744
+ pid = get_daemon_pid(args.instance_id)
745
+ if not pid:
746
+ print(f"Instance '{args.instance_id}' not running", file=sys.stderr)
747
+ sys.exit(1)
748
+ os.kill(pid, signal.SIGTERM)
749
+ print(f"Stopped instance {args.instance_id} (PID {pid})", file=sys.stderr)
656
750
  else:
657
- print("Daemon not running")
751
+ print("Error: instance_id required. Usage: daemon stop <instance_id>", file=sys.stderr)
752
+ sys.exit(1)
658
753
 
659
754
  elif args.daemon_cmd == "config":
660
755
  save_default_config()
661
- print(f"\nEdit {CONFIG_FILE} to customize MCP server settings.")
662
- print("\nExamples:")
663
- print(" Headless mode: --headless=true")
664
- print(" Custom Chrome: --executablePath=/path/to/chrome")
665
- print(" Sandbox: --headless=true --executablePath=/usr/local/bin/chromium-mcp")
756
+ print(f"\nEdit {CONFIG_FILE} to customize MCP server settings.", file=sys.stderr)
757
+ print("\nExamples:", file=sys.stderr)
758
+ print(" Headless mode: --headless=true", file=sys.stderr)
759
+ print(" Custom Chrome: --executablePath=/path/to/chrome", file=sys.stderr)
666
760
 
667
761
  return
668
762
 
669
- # Execute browser command
763
+ # Execute browser command - requires --instance
764
+ if not args.instance:
765
+ print("Error: --instance <id> is required for browser commands", file=sys.stderr)
766
+ print("Start a daemon first: uv run browsertools.py daemon start", file=sys.stderr)
767
+ sys.exit(1)
768
+
769
+ if not is_daemon_running(args.instance):
770
+ print(f"Error: Instance '{args.instance}' is not running", file=sys.stderr)
771
+ sys.exit(1)
772
+
670
773
  try:
671
774
  cmd_args_dict = {}
672
775
 
@@ -724,7 +827,7 @@ IMPORTANT:
724
827
  elif args.cmd == "fillform":
725
828
  cmd_args_dict["elements"] = json.loads(args.elements)
726
829
 
727
- output = await execute_command(args.cmd, cmd_args_dict)
830
+ output = await execute_command(args.instance, args.cmd, cmd_args_dict)
728
831
  print(output)
729
832
 
730
833
  except Exception as e:
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: feedback-executor
3
+ description: |
4
+ Analyses annotated-feedback entry, implements required change, validates, and optionally pushes to prod
5
+ color: green
6
+ model: inherit
7
+ ---
8
+
9
+ You are now opperating as Wait-n-Build
10
+ MISSION: implement changes requested via annotated-feedback mcp
11
+
12
+ It is CRITICAL that you follow the Wait-n-Build WORKFLOW step by step:
13
+
14
+ [WORKFLOW]
15
+ 1. CALIBRATE: Consume AGENT OPERATING PROCEDURES (AOP) `.agents/AGENTS.md`. and Execute AOP.CALIBRATE. You are working at project level UI/UX implementation, so DRINK design guide, brandkit spec, etc. not phase specific.
16
+ 2. IMPLEMENT:
17
+ - Retrieve full details via mcp__annotated-feedback__get (including visual annotations/screenshots)
18
+ - Update status to "active"
19
+ - Implement the requested changes
20
+ - Update status to "review"
21
+ 3. UAT: Run `uv run .agents/tools/chrome-devtools/browsertools.py --help` to learn how to use the UAT toolkit. use the UAT toolkit to:
22
+ - navigate to the dev server (see `.agents/repo.md`)
23
+ - manually execute user flows impacted by your change
24
+ - screenshot at each checkpoint, and PONDER visual issues and allignment with expectaions.
25
+ - check browser and dev server logs for errors
26
+ 4. Itterate, refine, UAT loop until satisfied.
27
+ 5. DEPLOY: run Pre-deployment validation build eg. `vercel build --yes`. when green, commit and push changes to kick off CI/CD. Mission accomplished!
28
+
29
+ [/WORKFLOW]
@@ -15,6 +15,7 @@ WAIT: Poll the mcp__annotated-feedback__list endpoint for feedback with status:
15
15
  - navigate to the dev server (see `.agents/repo.md`)
16
16
  - manually execute user flows impacted by your change
17
17
  - screenshot at each checkpoint, and PONDER visual issues and allignment with expectaions.
18
+ - check browser and dev server logs for errors
18
19
  4. Itterate, refine, UAT loop until satisfied.
19
20
  5. DEPLOY: run Pre-deployment validation build eg. `vercel build --yes`. when green, commit and push changes to kick off CI/CD. Mission accomplished!
20
21
  [/WORKFLOW]
@@ -18,7 +18,7 @@ Populate your initial Todos with your step by step WORKFLOW:
18
18
 
19
19
  [WORKFLOW]
20
20
  1. PLAN: ULTRATHINK and break down ASSIGNMENT into independent implementation tasks
21
- 2. IMPLEMENT: Launch a blended Implementation batch of engineers, codex and gemini consultants, each tasked with an with an independent Implementation task.
21
+ 2. IMPLEMENT: Launch a blended Implementation batch of engineers, codex and gemini consultants, each tasked with an with an independent Implementation task.
22
22
  3. REFINE: Launch a blended Review+Refine batch of engineers and consultants + 1 UAT. each tasked to assess and implementation of one of the previous batch agents (provide them with the task that was assigned to the previous agent). these agents will assess for task completness, success critiera met, with documentation. They should then refine the implementation if needed. this batch should be alternate the previous batch. eg. engeneer reviews gemini work, gemini reviews codex work, codex reviews engineer work.
23
23
  4. VERIFY: Launch a Assessing Only, Verification batch (Architect, Codex, Gemini, UAT) to assess the implementation completness of the full ASSIGNMENT. Each agent in this batch will provide their unique perspective, so give them the same comprehensive assessment task
24
24
  5...N. LOOP: *Decision:* did the Verification batch highlight ANY test failures, critical issues, missalignment, unmet ASSIGNMENT requirements? If so, **Add new Todos**: Loop through steps 3 & 4 batch Review+Refine -> Verification. Continue itterating until 100% pass/green/success reported by Verification batch
@@ -63,7 +63,7 @@ TEAM COLLABORATION:
63
63
 
64
64
 
65
65
  ⭐*The successful delivery of your assigned task, contributes to the high level Assignment:*⭐
66
- [User's exact ASSIGNMENT]
66
+ [Your full ASSIGNMENT]
67
67
 
68
68
  ⭐Ensure you are alligned with this North Star objective*⭐
69
69
 
@@ -79,7 +79,7 @@ Don't refer to prior batch agents by name. Instead, supply reference artifacts t
79
79
  ### Batch Composition Goals
80
80
  - **Minimum**: 5 agents per implementation batch
81
81
  - **Target**: 8-10 agents including support roles
82
- - **Include**: Support Architect/Researcher for guidance
82
+ - **Include**: roughly equal number of gemini, codex, and engineer implementers, and a support architect for guidance/coordination.
83
83
  - **Follow with**: Verification batch always
84
84
 
85
85
  ### Phase Management
@@ -92,7 +92,7 @@ Don't refer to prior batch agents by name. Instead, supply reference artifacts t
92
92
 
93
93
  ## Consultant Agent
94
94
  Consultants come in two variants: Codex and Gemini. leverage them both liberally to diversify.
95
- When you are batching multple engineers across implementation Tasks, use some consultant agents in place of some engineers. Split the work between Engineers, Gemini consultants and Codex consultants.
95
+ For implementation batches, distribute the tasks across engineers, codex consultants, and gemini consultants. There should be a roughly equal number of engineer, gemini, codex in implementation batches.
96
96
  When you are tasking an agent with read+document type task, recruit a codex and gemini Consultant with the exact same instructions (except a different names). Include the consultants in the same batch. This will net you diverse perspectives on the same research/plan/architect assignment.
97
97
 
98
98
 
@@ -66,7 +66,7 @@ TEAM COLLABORATION:
66
66
 
67
67
 
68
68
  ⭐*The successful delivery of your assigned task, contributes to the high level Assignment:*⭐
69
- [User's exact ASSIGNMENT]
69
+ [Your full ASSIGNMENT]
70
70
 
71
71
  ⭐Ensure you are alligned with this North Star objective*⭐
72
72
 
package/CLAUDE.md CHANGED
@@ -83,7 +83,7 @@ TEAM COLLABORATION:
83
83
  - All agents in a batch complete independently without waiting for others
84
84
 
85
85
  Parallel Specialization Examples
86
- - **Multiple Engineers/Consultants**: Each owns different module/file
86
+ - **Multiple Engineers+Consultants**: Each owns different module/file
87
87
  - **Support Roles**: Architect answers questions or researches and suggests solution approachs, Designer creates storybook assets, UAT manually performs user flows and provides feedback
88
88
 
89
89
  ### Inter-Batch Sequencing (Dependencies)
@@ -124,8 +124,8 @@ Research/Architecture → Planning → Implementation → Verification → Next
124
124
  #### New Feature Workflow
125
125
  1. **Discovery Batch**: [Architect, Designer, Consutant]
126
126
  2. **Planning Batch**: [Planner] - creates phase-id and WPs
127
- 3. **Implementation Batch**: [5-10 Engineers, Consultants with distinct role and focus + support agents (maximum 10 total per batch) ]
128
- 4. **Review+Refine Batch**: [5-10 Engineers, Consultants with previous batch task (swap Engineers and Consultants so they review/refine each others work) to review+refine + support agents (maximum 10 total per batch) ]
127
+ 3. **Implementation Batch**: [Engineers, Gemini and Codex Consultants with distinct role and focus + support agents (maximum 10 total per batch) ]
128
+ 4. **Review+Refine Batch**: [Engineers, Codex and Gemini Consultants with previous batch task (alternate Engineers and Consultants so they review/refine each others work) to review+refine + support agents (maximum 10 total per batch) ]
129
129
  5. **Verification + Reco Batch**: [Codex + Gemini + Architect + UAT to provide their completion assessments]
130
130
  6. **If verificaiton fail**: Loop Review+refine -> Verification Batches until PASS
131
131
 
@@ -167,7 +167,7 @@ Research/Architecture → Planning → Implementation → Verification → Next
167
167
 
168
168
  ## Consultant Agent
169
169
  Consultants come in two variants: Codex and Gemini. leverage them both liberally to diversify.
170
- When you are batching multple engineers across implementation Tasks, use some consultant agents in place of some engineers. Split the work between Engineers, Gemini consultants and Codex consultants.
170
+ For implementation batches, distribute the tasks across engineers, codex consultants, and gemini consultants. There should be a roughly equal number of engineer, gemini, codex in implementation batches.
171
171
  When you are tasking an agent with read+document type task, recruit a codex and gemini Consultant with the exact same instructions (except a different names). Include the consultants in the same batch. This will net you diverse perspectives on the same research/plan/architect assignment.
172
172
 
173
173
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-comms",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "description": "NPX installer for Claude Code hooks and observability setup",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -95,6 +95,7 @@ function _createDependencies(options = {}) {
95
95
  // Import real implementations
96
96
  const { GitHubFetcher } = require('./fetcher/github');
97
97
  const { FileWriter } = require('./installer/file-writer');
98
+ const fs = require('fs').promises;
98
99
 
99
100
  // Create real fetcher instance
100
101
  const fetcher = new GitHubFetcher({
@@ -122,7 +123,7 @@ function _createDependencies(options = {}) {
122
123
  progressReporter: options.progressReporter || null
123
124
  });
124
125
 
125
- // Create mock dependencies for logger and validator (these remain mocked for now)
126
+ // Create mock logger but use real validator with actual filesystem checks
126
127
  const mockDependencies = createMockDependencies({
127
128
  logger: {
128
129
  verbose: options.verbose,
@@ -130,11 +131,35 @@ function _createDependencies(options = {}) {
130
131
  }
131
132
  });
132
133
 
134
+ // Create a real validator that actually checks the filesystem
135
+ const realValidator = {
136
+ async validateTargetDirectory(_targetDir) {
137
+ return null; // No error
138
+ },
139
+ async validatePythonEnvironment() {
140
+ return null; // No error - skip python check for now
141
+ },
142
+ async checkExistingFiles(_targetDir) {
143
+ return null; // No error
144
+ },
145
+ async validateNetworkAccess() {
146
+ return null; // No error
147
+ },
148
+ async pathExists(filePath) {
149
+ try {
150
+ await fs.access(filePath);
151
+ return true;
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+ };
157
+
133
158
  return {
134
159
  fetcher: fetcher,
135
160
  writer: writer,
136
161
  logger: mockDependencies.logger,
137
- validator: mockDependencies.validator
162
+ validator: realValidator
138
163
  };
139
164
  }
140
165
 
@@ -334,13 +334,30 @@ class Installer extends EventEmitter {
334
334
  // Create backups if files exist
335
335
  await this._createBackupsIfNeeded(fetchedFiles);
336
336
 
337
- // Write files using injected writer
337
+ // Flatten files and filter out user-customizable files if they already exist
338
+ let filesToWrite = this._flattenFiles(fetchedFiles);
339
+
340
+ // Files that should not be overwritten if they exist (user customizations)
341
+ const preserveIfExists = [
342
+ { path: '.agents/repo.md', fullPath: path.join(this.options.targetDir, '.agents', 'repo.md') },
343
+ { path: '.agents/tools/chrome-devtools/config.json', fullPath: path.join(this.options.targetDir, '.agents', 'tools', 'chrome-devtools', 'config.json') }
344
+ ];
345
+
346
+ for (const file of preserveIfExists) {
347
+ const exists = await this.validator.pathExists(file.fullPath);
348
+ if (exists) {
349
+ this.logger.info(`${file.path} already exists, skipping (preserving user customizations)`);
350
+ filesToWrite = filesToWrite.filter(f => f.path !== file.path);
351
+ }
352
+ }
353
+
354
+ // Write files using injected writer (overwrite existing, no backups)
338
355
  const writeResult = await this.writer.writeDirectory(
339
- this._flattenFiles(fetchedFiles),
356
+ filesToWrite,
340
357
  {
341
358
  targetDir: this.options.targetDir,
342
- overwrite: this.options.force,
343
- backup: true,
359
+ overwrite: true,
360
+ backup: false,
344
361
  preservePermissions: true,
345
362
  dryRun: this.options.dryRun
346
363
  }
@@ -450,6 +467,11 @@ class Installer extends EventEmitter {
450
467
  this.state = InstallationState.VERIFYING;
451
468
  this._emitProgress('Verifying installation...', 90);
452
469
 
470
+ if (this.options.dryRun) {
471
+ this.logger.info('DRY RUN: Would verify installation');
472
+ return;
473
+ }
474
+
453
475
  const requiredPaths = [
454
476
  path.join(this.options.targetDir, '.claude'),
455
477
  path.join(this.options.targetDir, '.agents'),
@@ -706,29 +728,27 @@ class Installer extends EventEmitter {
706
728
  }
707
729
 
708
730
  /**
709
- * Create backups of existing files if needed
731
+ * Log info about existing files that will be overwritten
710
732
  */
711
733
  async _createBackupsIfNeeded(_fetchedFiles) {
712
- if (!this.options.force) {
713
- // Check for existing directories and files
714
- const claudeDir = path.join(this.options.targetDir, '.claude');
715
- const agentsDir = path.join(this.options.targetDir, '.agents');
716
- const claudeMd = path.join(this.options.targetDir, 'CLAUDE.md');
717
-
718
- const existingPaths = [];
719
- if (await this.validator.pathExists(claudeDir)) {
720
- existingPaths.push('.claude/');
721
- }
722
- if (await this.validator.pathExists(agentsDir)) {
723
- existingPaths.push('.agents/');
724
- }
725
- if (await this.validator.pathExists(claudeMd)) {
726
- existingPaths.push('CLAUDE.md');
727
- }
734
+ // Check for existing directories and files that will be overwritten
735
+ const claudeDir = path.join(this.options.targetDir, '.claude');
736
+ const agentsDir = path.join(this.options.targetDir, '.agents');
737
+ const claudeMd = path.join(this.options.targetDir, 'CLAUDE.md');
738
+
739
+ const existingPaths = [];
740
+ if (await this.validator.pathExists(claudeDir)) {
741
+ existingPaths.push('.claude/');
742
+ }
743
+ if (await this.validator.pathExists(agentsDir)) {
744
+ existingPaths.push('.agents/');
745
+ }
746
+ if (await this.validator.pathExists(claudeMd)) {
747
+ existingPaths.push('CLAUDE.md');
748
+ }
728
749
 
729
- if (existingPaths.length > 0) {
730
- this.logger.warn(`Existing files will be backed up: ${existingPaths.join(', ')}`);
731
- }
750
+ if (existingPaths.length > 0) {
751
+ this.logger.info(`Existing files will be overwritten: ${existingPaths.join(', ')}`);
732
752
  }
733
753
  }
734
754
 
@@ -1,11 +0,0 @@
1
- Meta Agent Assignment:
2
- #$ARGUMENTS
3
-
4
-
5
-
6
-
7
- Spin up N concurrent meta-agent-builder agents, where N is the number of Agents to be create and/or modified.
8
- Task each meta-agent-builder with a single agent to create/update
9
-
10
-
11
- REMEMBER: the meta-agent-builder is building the agent, not tasking the agent. give the meta-agent-builder a name, but dont tell them to award a specific AgentName to the agent they are creating/updating!