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.
- {swarph_cli-0.9.1/src/swarph_cli.egg-info → swarph_cli-0.9.2}/PKG-INFO +2 -2
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/pyproject.toml +2 -2
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/watchdog.py +41 -22
- {swarph_cli-0.9.1 → swarph_cli-0.9.2/src/swarph_cli.egg-info}/PKG-INFO +2 -2
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_watchdog.py +55 -6
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/LICENSE +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/README.md +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/setup.cfg +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/mesh.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli.egg-info/SOURCES.txt +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_import_command.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_main.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.9.1 → swarph_cli-0.9.2}/tests/test_spawn_command.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
438
|
-
|
|
439
|
-
Reads tmux
|
|
440
|
-
epoch
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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,
|
|
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
|
-
|
|
462
|
-
|
|
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() -
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
|
765
|
-
"""
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|