swarph-cli 0.9.1__tar.gz → 0.9.2__tar.gz

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 (51) hide show
  1. {swarph_cli-0.9.1/src/swarph_cli.egg-info → swarph_cli-0.9.2}/PKG-INFO +2 -2
  2. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/pyproject.toml +2 -2
  3. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/watchdog.py +41 -22
  5. {swarph_cli-0.9.1 → swarph_cli-0.9.2/src/swarph_cli.egg-info}/PKG-INFO +2 -2
  6. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_watchdog.py +55 -6
  7. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/LICENSE +0 -0
  8. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/README.md +0 -0
  9. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/setup.cfg +0 -0
  10. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/caller.py +0 -0
  11. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/cell.py +0 -0
  12. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/__init__.py +0 -0
  13. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/chat.py +0 -0
  14. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/daemon.py +0 -0
  15. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/hook_output.py +0 -0
  16. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/import_session.py +0 -0
  17. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/install_hook.py +0 -0
  18. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/memory_sync.py +0 -0
  19. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/mesh.py +0 -0
  20. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/onboard.py +0 -0
  21. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/ratify.py +0 -0
  22. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/spawn.py +0 -0
  23. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/main.py +0 -0
  24. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/parsers/__init__.py +0 -0
  25. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/parsers/claude.py +0 -0
  26. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  27. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  28. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  29. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli.egg-info/SOURCES.txt +0 -0
  30. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  31. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  32. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli.egg-info/requires.txt +0 -0
  33. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli.egg-info/top_level.txt +0 -0
  34. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_cell_loader.py +0 -0
  35. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_chat_command.py +0 -0
  36. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_claude_parser.py +0 -0
  37. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_daemon_command.py +0 -0
  38. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_hook_output.py +0 -0
  39. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_import_command.py +0 -0
  40. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_install_hook.py +0 -0
  41. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_main.py +0 -0
  42. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_memory_sync.py +0 -0
  43. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_mesh_command.py +0 -0
  44. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_mesh_sidecar.py +0 -0
  45. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_onboard_command.py +0 -0
  46. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_ratify_command.py +0 -0
  47. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_smoke_chat.py +0 -0
  48. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_smoke_one_shot.py +0 -0
  49. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_smoke_phase_5_5.py +0 -0
  50. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_spawn_command.py +0 -0
  51. {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_spawn_windows_relaunch.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.9.1
4
- Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider spawn (claude/codex/antigravity per cell.provider via a ProviderMembrane), cell.yaml, `swarph mesh` (send/inbox/register with per-peer tokens) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.1 hardens the membrane billing-scrub + import path-traversal + watchdog session-scoping (adversarial-sweep).
3
+ Version: 0.9.2
4
+ Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider spawn (claude/codex/antigravity per cell.provider via a ProviderMembrane), cell.yaml, `swarph mesh` (send/inbox/register with per-peer tokens) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.2 makes the watchdog deploy-safe: no false A2 when no tmux server is reachable (root-cron), and F3 uses window/session activity so active sessions are correctly detected.
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/darw007d/swarph-cli
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swarph-cli"
7
- version = "0.9.1"
8
- description = "The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider spawn (claude/codex/antigravity per cell.provider via a ProviderMembrane), cell.yaml, `swarph mesh` (send/inbox/register with per-peer tokens) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.1 hardens the membrane billing-scrub + import path-traversal + watchdog session-scoping (adversarial-sweep)."
7
+ version = "0.9.2"
8
+ description = "The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider spawn (claude/codex/antigravity per cell.provider via a ProviderMembrane), cell.yaml, `swarph mesh` (send/inbox/register with per-peer tokens) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.2 makes the watchdog deploy-safe: no false A2 when no tmux server is reachable (root-cron), and F3 uses window/session activity so active sessions are correctly detected."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.10"
@@ -16,6 +16,6 @@ The architecture splits CLI from substrate so:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- __version__ = "0.9.1"
19
+ __version__ = "0.9.2"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -393,10 +393,24 @@ def _process_alive(tmux_session: str) -> bool:
393
393
  capture_output=True, text=True, timeout=5,
394
394
  )
395
395
  if panes.returncode != 0:
396
- return False # session has no panes / doesn't exist not alive
396
+ # list-panes fails for TWO very different reasons:
397
+ # (a) the tmux server is reachable but this session is genuinely
398
+ # gone → really dead → False (let A2 fire).
399
+ # (b) there is no tmux server reachable from THIS uid (e.g. the
400
+ # watchdog runs as root while sessions live under ubuntu's
401
+ # /tmp/tmux-1000 socket) → we simply CAN'T determine liveness
402
+ # via tmux → must NOT false-fire A2.
403
+ # Distinguish by probing the server itself.
404
+ server = subprocess.run(
405
+ ["tmux", "list-sessions"],
406
+ capture_output=True, text=True, timeout=5,
407
+ )
408
+ if server.returncode != 0:
409
+ return True # no reachable tmux server here → can't tell → assume alive
410
+ return False # server reachable, session absent → genuinely dead
397
411
  pane_pids = {int(p) for p in panes.stdout.split() if p.strip().isdigit()}
398
412
  if not pane_pids:
399
- return False
413
+ return True # ambiguous (session with no pane pids) → don't false-fire
400
414
 
401
415
  pg = subprocess.run(
402
416
  ["pgrep", "-f", "claude"],
@@ -434,34 +448,39 @@ def _tmux_send_keys(name: str, text: str) -> bool:
434
448
 
435
449
 
436
450
  def _pane_activity_age_sec(name: str) -> Optional[int]:
437
- """Age in seconds since the tmux pane's last activity event.
438
-
439
- Reads tmux's `#{pane_activity}` format variable, which returns a unix
440
- epoch timestamp of the most recent activity in the active pane of the
441
- target session. Returns None if tmux is missing, the session doesn't
442
- exist, or tmux's output isn't parseable as an integer epoch.
443
-
444
- Used by F3 (mother #1087 / drop-on-meta-edge proposal) as a third
445
- AND-gate input to distinguish (a) session genuinely stalled from (b)
446
- session actively working in a long bash block. cursor-mtime alone
447
- measures "time since last turn-end" not "time since last activity";
448
- pane_activity covers the mid-turn-active case.
449
-
450
- Returns None on detection error so the caller can fall through to
451
- the legacy AND-gate behavior F3 is a strengthening of the gate,
452
- not a replacement of it.
451
+ """Age in seconds since the target session's most recent tmux activity.
452
+
453
+ Reads tmux activity-timestamp format vars and uses the MOST RECENT
454
+ (max epoch) across pane / window / session. ``#{pane_activity}`` is only
455
+ populated when monitor-activity is on (empty on a default tmux 3.x), so
456
+ relying on it alone made F3 a no-op it returned None and never
457
+ suppressed A1 for a genuinely-active session (adversarial-deploy finding
458
+ 2026-06-02). ``#{window_activity}`` / ``#{session_activity}`` are tracked
459
+ unconditionally and give the same "is this session alive right now" signal.
460
+
461
+ Used by F3 (mother #1087) as a third AND-gate input to distinguish (a) a
462
+ session genuinely stalled from (b) one actively working in a long bash
463
+ block where cursor-mtime (last turn-end) is stale but the session is alive.
464
+
465
+ Returns None only when NO activity timestamp is parseable (tmux missing /
466
+ session absent), so the caller falls through to the legacy AND-gate.
453
467
  """
454
468
  try:
455
469
  result = subprocess.run(
456
- ["tmux", "display", "-p", "-t", name, "#{pane_activity}"],
470
+ ["tmux", "display", "-p", "-t", name,
471
+ "#{pane_activity}|#{window_activity}|#{session_activity}"],
457
472
  capture_output=True, text=True, timeout=5,
458
473
  )
459
474
  if result.returncode != 0:
460
475
  return None
461
- out = result.stdout.strip()
462
- if not out:
476
+ epochs = []
477
+ for tok in result.stdout.strip().split("|"):
478
+ tok = tok.strip()
479
+ if tok.isdigit():
480
+ epochs.append(int(tok))
481
+ if not epochs:
463
482
  return None
464
- return max(0, _now() - int(out))
483
+ return max(0, _now() - max(epochs))
465
484
  except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError):
466
485
  return None
467
486
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.9.1
4
- Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider spawn (claude/codex/antigravity per cell.provider via a ProviderMembrane), cell.yaml, `swarph mesh` (send/inbox/register with per-peer tokens) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.1 hardens the membrane billing-scrub + import path-traversal + watchdog session-scoping (adversarial-sweep).
3
+ Version: 0.9.2
4
+ Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider spawn (claude/codex/antigravity per cell.provider via a ProviderMembrane), cell.yaml, `swarph mesh` (send/inbox/register with per-peer tokens) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.2 makes the watchdog deploy-safe: no false A2 when no tmux server is reachable (root-cron), and F3 uses window/session activity so active sessions are correctly detected.
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/darw007d/swarph-cli
@@ -132,6 +132,7 @@ def test_stale_cursor_alive_process_unread_dms_fires_a1(
132
132
  with patch("swarph_cli.commands.watchdog._process_alive", return_value=True), \
133
133
  patch("swarph_cli.commands.watchdog._gateway_unread_count", return_value=3), \
134
134
  patch("swarph_cli.commands.watchdog._tmux_session_exists", return_value=True), \
135
+ patch("swarph_cli.commands.watchdog._pane_activity_age_sec", return_value=None), \
135
136
  patch("swarph_cli.commands.watchdog._tmux_send_keys", return_value=True) as send_mock:
136
137
  rc = run_watchdog(argv=[
137
138
  "--check", "--cell", "lab",
@@ -279,6 +280,7 @@ def test_a1_fires_at_most_once_per_stale_window(
279
280
  with patch("swarph_cli.commands.watchdog._process_alive", return_value=True), \
280
281
  patch("swarph_cli.commands.watchdog._gateway_unread_count", return_value=3), \
281
282
  patch("swarph_cli.commands.watchdog._tmux_session_exists", return_value=True), \
283
+ patch("swarph_cli.commands.watchdog._pane_activity_age_sec", return_value=None), \
282
284
  patch("swarph_cli.commands.watchdog._tmux_send_keys", return_value=True) as send_mock:
283
285
  # First invocation — A1 fires
284
286
  rc1 = run_watchdog(argv=[
@@ -322,6 +324,7 @@ def test_a1_rearms_after_cursor_advance(
322
324
  with patch("swarph_cli.commands.watchdog._process_alive", return_value=True), \
323
325
  patch("swarph_cli.commands.watchdog._gateway_unread_count", return_value=2), \
324
326
  patch("swarph_cli.commands.watchdog._tmux_session_exists", return_value=True), \
327
+ patch("swarph_cli.commands.watchdog._pane_activity_age_sec", return_value=None), \
325
328
  patch("swarph_cli.commands.watchdog._tmux_send_keys", return_value=True) as send_mock:
326
329
  # First A1 fires
327
330
  run_watchdog(argv=[
@@ -482,6 +485,7 @@ def test_a2_escalation_clears_a1_marker(
482
485
  with patch("swarph_cli.commands.watchdog._process_alive", return_value=True), \
483
486
  patch("swarph_cli.commands.watchdog._gateway_unread_count", return_value=5), \
484
487
  patch("swarph_cli.commands.watchdog._tmux_session_exists", return_value=True), \
488
+ patch("swarph_cli.commands.watchdog._pane_activity_age_sec", return_value=None), \
485
489
  patch("swarph_cli.commands.watchdog._tmux_send_keys", return_value=True):
486
490
  # First fire — record marker
487
491
  run_watchdog(argv=[
@@ -728,10 +732,13 @@ class _R:
728
732
  self.stdout = stdout
729
733
 
730
734
 
731
- def _fake_run_factory(tmux_rc, tmux_out, pgrep_rc, pgrep_out):
735
+ def _fake_run_factory(panes_rc, panes_out, pgrep_rc, pgrep_out, sessions_rc=0):
736
+ """Mock subprocess.run distinguishing tmux list-panes vs list-sessions."""
732
737
  def fake_run(cmd, **kw):
733
- if cmd[0] == "tmux":
734
- return _R(tmux_rc, tmux_out)
738
+ if cmd[0] == "tmux" and cmd[1] == "list-panes":
739
+ return _R(panes_rc, panes_out)
740
+ if cmd[0] == "tmux" and cmd[1] == "list-sessions":
741
+ return _R(sessions_rc, "" if sessions_rc else "lab: 1 windows\n")
735
742
  if cmd[0] == "pgrep":
736
743
  return _R(pgrep_rc, pgrep_out)
737
744
  return _R(1, "")
@@ -761,14 +768,56 @@ def test_process_alive_true_when_claude_under_session(monkeypatch):
761
768
  assert _wd._process_alive("mysess") is True
762
769
 
763
770
 
764
- def test_process_alive_false_when_session_absent(monkeypatch):
765
- """tmux has no such session → not alive (don't fall through to host-wide)."""
771
+ def test_process_alive_false_when_session_absent_but_server_up(monkeypatch):
772
+ """Server reachable + this session genuinely gone dead (A2 may fire)."""
766
773
  monkeypatch.setattr(_wd.subprocess, "run",
767
- _fake_run_factory(1, "", 0, "2000\n"))
774
+ _fake_run_factory(1, "", 0, "2000\n", sessions_rc=0))
768
775
  assert _wd._process_alive("ghost") is False
769
776
 
770
777
 
778
+ def test_process_alive_assumes_alive_when_no_tmux_server(monkeypatch):
779
+ """Regression (deploy 2026-06-02): the watchdog runs as ROOT, whose tmux
780
+ socket is empty — list-panes AND list-sessions both fail. We CANNOT
781
+ determine liveness via tmux, so must assume alive, NOT false-fire an A2
782
+ respawn of a session that's actually alive under ubuntu's tmux."""
783
+ monkeypatch.setattr(_wd.subprocess, "run",
784
+ _fake_run_factory(1, "", 0, "2000\n", sessions_rc=1))
785
+ assert _wd._process_alive("lab") is True
786
+
787
+
771
788
  def test_process_alive_false_when_no_claude_anywhere(monkeypatch):
772
789
  monkeypatch.setattr(_wd.subprocess, "run",
773
790
  _fake_run_factory(0, "1000\n", 1, ""))
774
791
  assert _wd._process_alive("mysess") is False
792
+
793
+
794
+ # ---------------------------------------------------------------------------
795
+ # _pane_activity_age_sec — fallback across pane/window/session (F3 fix 2026-06-02)
796
+ # ---------------------------------------------------------------------------
797
+
798
+
799
+ def test_pane_activity_age_falls_back_when_pane_empty(monkeypatch):
800
+ """pane_activity is empty without monitor-activity (tmux 3.x). F3 must fall
801
+ back to window/session activity, NOT return None — returning None made F3 a
802
+ no-op and let A1 fire against a genuinely-active session."""
803
+ recent = _wd._now() - 30
804
+ monkeypatch.setattr(_wd.subprocess, "run",
805
+ lambda cmd, **kw: _R(0, f"|{recent}|{recent - 5}\n"))
806
+ age = _wd._pane_activity_age_sec("lab")
807
+ assert age is not None
808
+ assert 25 <= age <= 40 # ~30s, allowing for execution slack
809
+
810
+
811
+ def test_pane_activity_age_none_when_all_blank(monkeypatch):
812
+ monkeypatch.setattr(_wd.subprocess, "run", lambda cmd, **kw: _R(0, "||\n"))
813
+ assert _wd._pane_activity_age_sec("lab") is None
814
+
815
+
816
+ def test_pane_activity_age_takes_most_recent(monkeypatch):
817
+ """Uses the MAX (most recent) epoch across the three vars."""
818
+ now = _wd._now()
819
+ # pane empty, window 500s ago, session 10s ago → most recent = 10s
820
+ monkeypatch.setattr(_wd.subprocess, "run",
821
+ lambda cmd, **kw: _R(0, f"|{now - 500}|{now - 10}\n"))
822
+ age = _wd._pane_activity_age_sec("lab")
823
+ assert age is not None and age <= 20
File without changes
File without changes
File without changes