swarph-cli 0.7.2__tar.gz → 0.7.3__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 (45) hide show
  1. {swarph_cli-0.7.2/src/swarph_cli.egg-info → swarph_cli-0.7.3}/PKG-INFO +41 -1
  2. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/README.md +40 -0
  3. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/pyproject.toml +7 -1
  4. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/__init__.py +1 -1
  5. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/commands/watchdog.py +124 -2
  6. swarph_cli-0.7.3/src/swarph_cli/systemd/swarph-watchdog.default +9 -0
  7. swarph_cli-0.7.3/src/swarph_cli/systemd/swarph-watchdog.service +15 -0
  8. swarph_cli-0.7.3/src/swarph_cli/systemd/swarph-watchdog.timer +13 -0
  9. {swarph_cli-0.7.2 → swarph_cli-0.7.3/src/swarph_cli.egg-info}/PKG-INFO +41 -1
  10. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli.egg-info/SOURCES.txt +3 -0
  11. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_watchdog.py +62 -0
  12. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/LICENSE +0 -0
  13. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/setup.cfg +0 -0
  14. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/caller.py +0 -0
  15. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/cell.py +0 -0
  16. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/commands/__init__.py +0 -0
  17. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/commands/chat.py +0 -0
  18. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/commands/daemon.py +0 -0
  19. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/commands/hook_output.py +0 -0
  20. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/commands/import_session.py +0 -0
  21. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/commands/install_hook.py +0 -0
  22. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/commands/onboard.py +0 -0
  23. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/commands/ratify.py +0 -0
  24. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/commands/spawn.py +0 -0
  25. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/main.py +0 -0
  26. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/parsers/__init__.py +0 -0
  27. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli/parsers/claude.py +0 -0
  28. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  29. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  30. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli.egg-info/requires.txt +0 -0
  31. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/src/swarph_cli.egg-info/top_level.txt +0 -0
  32. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_cell_loader.py +0 -0
  33. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_chat_command.py +0 -0
  34. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_claude_parser.py +0 -0
  35. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_daemon_command.py +0 -0
  36. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_hook_output.py +0 -0
  37. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_import_command.py +0 -0
  38. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_install_hook.py +0 -0
  39. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_main.py +0 -0
  40. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_onboard_command.py +0 -0
  41. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_ratify_command.py +0 -0
  42. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_smoke_chat.py +0 -0
  43. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_smoke_one_shot.py +0 -0
  44. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_smoke_phase_5_5.py +0 -0
  45. {swarph_cli-0.7.2 → swarph_cli-0.7.3}/tests/test_spawn_command.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.7.0 ships Phase 7 substrate-doc R7 §11.1.7 operator-tooling layer in 5 increments: PR-A `--new-instance` flag (sibling-spawn case) + PR-B auto-suffix on collision (sibling-slot persistence) + PR-C SessionStart hook (closes bare-claude operator-paste gap) + watchdog (stranded-session recovery) + PR-D swarph-shared cell.yaml relocation (cell-yaml schema graduates to swarph-shared 0.3.0 kernel-tier; substrate-doc R7 §11.1.5 (O5) RESOLVED).
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
@@ -138,6 +138,46 @@ Loud-on-down (PLAN §16.5): never silently exits. Cursor writes are atomic (writ
138
138
 
139
139
  `--auto-act` flag is documented for v0.5.1+ when handler registration via `@swarph.on_dm(...)` lands; v0.5.0 ships surface-only mode (DMs printed + JSONL-logged to `inbox.log`, no automatic replies).
140
140
 
141
+ ### `swarph watchdog` (Phase 7 — v0.7 stranded-session detection, v0.7.3 systemd install)
142
+
143
+ Detects stranded Claude sessions (API throttle / harness death) via cursor-mtime + tmux pgrep AND-gate, and recovers via A1 tmux send-keys wake-prompt → A2 `swarph spawn` respawn. Cell.yaml-pinned cursor + tmux session (F4) since v0.7.2.
144
+
145
+ **One-shot mode (cron-callable, v0.7+):**
146
+ ```bash
147
+ */5 * * * * swarph watchdog --check --cell lab >> ~/.local/log/swarph-watchdog.log 2>&1
148
+ ```
149
+
150
+ **Systemd timer install (v0.7.3+ — closes ev_6954f748 substrate-component-installation-gap):**
151
+
152
+ ```bash
153
+ # Preview without writing (any user):
154
+ swarph watchdog --install-service --cell droplet --dry-run
155
+
156
+ # Install + enable (requires root for /etc/systemd/system writes):
157
+ sudo swarph watchdog --install-service --cell droplet
158
+ ```
159
+
160
+ This writes three files:
161
+
162
+ | Path | Purpose |
163
+ |------|---------|
164
+ | `/etc/systemd/system/swarph-watchdog.service` | `Type=oneshot`, runs `swarph watchdog --check` |
165
+ | `/etc/systemd/system/swarph-watchdog.timer` | Fires every 5 minutes (`OnUnitActiveSec=5min`) |
166
+ | `/etc/default/swarph-watchdog` | Sets `SWARPH_CELL=<role>` for the service env |
167
+
168
+ Then runs `systemctl daemon-reload && systemctl enable --now swarph-watchdog.timer`. Idempotent — re-running overwrites with current package version (newer-version semantics).
169
+
170
+ Monitoring:
171
+
172
+ ```bash
173
+ systemctl status swarph-watchdog.timer # is it scheduled?
174
+ systemctl list-timers swarph-watchdog.timer # next fire?
175
+ journalctl -u swarph-watchdog.service -f # live log
176
+ tail -f /var/log/swarph-watchdog.log # append-log alternative
177
+ ```
178
+
179
+ Why this matters: pre-v0.7.3, swarph-cli shipped the watchdog code but no install path. Lab ran it via cron (manual setup); droplet never installed it at all. A real production silence-window (drop's ~24min mute 2026-05-14 08:38→09:02 UTC after an Anthropic API error) made the install-gap visible. v0.7.3 closes it for any peer with one command.
180
+
141
181
  ### `swarph onboard` + `swarph ratify` (Phase 5.5)
142
182
 
143
183
  Per PLAN.md §15, onboarding splits into a **mechanics phase** (`swarph onboard`) that automates the boring parts (registry POST, scaffolding, token resolution) and a **manual contract phase** (the new peer composes the handshake DM in their own words). A witness peer judges the handshake and runs `swarph ratify <peer>` to flip `ratified=true`, gating `task_claim` server-side.
@@ -105,6 +105,46 @@ Loud-on-down (PLAN §16.5): never silently exits. Cursor writes are atomic (writ
105
105
 
106
106
  `--auto-act` flag is documented for v0.5.1+ when handler registration via `@swarph.on_dm(...)` lands; v0.5.0 ships surface-only mode (DMs printed + JSONL-logged to `inbox.log`, no automatic replies).
107
107
 
108
+ ### `swarph watchdog` (Phase 7 — v0.7 stranded-session detection, v0.7.3 systemd install)
109
+
110
+ Detects stranded Claude sessions (API throttle / harness death) via cursor-mtime + tmux pgrep AND-gate, and recovers via A1 tmux send-keys wake-prompt → A2 `swarph spawn` respawn. Cell.yaml-pinned cursor + tmux session (F4) since v0.7.2.
111
+
112
+ **One-shot mode (cron-callable, v0.7+):**
113
+ ```bash
114
+ */5 * * * * swarph watchdog --check --cell lab >> ~/.local/log/swarph-watchdog.log 2>&1
115
+ ```
116
+
117
+ **Systemd timer install (v0.7.3+ — closes ev_6954f748 substrate-component-installation-gap):**
118
+
119
+ ```bash
120
+ # Preview without writing (any user):
121
+ swarph watchdog --install-service --cell droplet --dry-run
122
+
123
+ # Install + enable (requires root for /etc/systemd/system writes):
124
+ sudo swarph watchdog --install-service --cell droplet
125
+ ```
126
+
127
+ This writes three files:
128
+
129
+ | Path | Purpose |
130
+ |------|---------|
131
+ | `/etc/systemd/system/swarph-watchdog.service` | `Type=oneshot`, runs `swarph watchdog --check` |
132
+ | `/etc/systemd/system/swarph-watchdog.timer` | Fires every 5 minutes (`OnUnitActiveSec=5min`) |
133
+ | `/etc/default/swarph-watchdog` | Sets `SWARPH_CELL=<role>` for the service env |
134
+
135
+ Then runs `systemctl daemon-reload && systemctl enable --now swarph-watchdog.timer`. Idempotent — re-running overwrites with current package version (newer-version semantics).
136
+
137
+ Monitoring:
138
+
139
+ ```bash
140
+ systemctl status swarph-watchdog.timer # is it scheduled?
141
+ systemctl list-timers swarph-watchdog.timer # next fire?
142
+ journalctl -u swarph-watchdog.service -f # live log
143
+ tail -f /var/log/swarph-watchdog.log # append-log alternative
144
+ ```
145
+
146
+ Why this matters: pre-v0.7.3, swarph-cli shipped the watchdog code but no install path. Lab ran it via cron (manual setup); droplet never installed it at all. A real production silence-window (drop's ~24min mute 2026-05-14 08:38→09:02 UTC after an Anthropic API error) made the install-gap visible. v0.7.3 closes it for any peer with one command.
147
+
108
148
  ### `swarph onboard` + `swarph ratify` (Phase 5.5)
109
149
 
110
150
  Per PLAN.md §15, onboarding splits into a **mechanics phase** (`swarph onboard`) that automates the boring parts (registry POST, scaffolding, token resolution) and a **manual contract phase** (the new peer composes the handshake DM in their own words). A witness peer judges the handshake and runs `swarph ratify <peer>` to flip `ratified=true`, gating `task_claim` server-side.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swarph-cli"
7
- version = "0.7.2"
7
+ version = "0.7.3"
8
8
  description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.7.0 ships Phase 7 substrate-doc R7 §11.1.7 operator-tooling layer in 5 increments: PR-A `--new-instance` flag (sibling-spawn case) + PR-B auto-suffix on collision (sibling-slot persistence) + PR-C SessionStart hook (closes bare-claude operator-paste gap) + watchdog (stranded-session recovery) + PR-D swarph-shared cell.yaml relocation (cell-yaml schema graduates to swarph-shared 0.3.0 kernel-tier; substrate-doc R7 §11.1.5 (O5) RESOLVED)."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -59,6 +59,12 @@ swarph = "swarph_cli.main:main"
59
59
  [tool.setuptools.packages.find]
60
60
  where = ["src"]
61
61
 
62
+ [tool.setuptools.package-data]
63
+ # v0.7.3: ship bundled systemd unit + timer + default templates so
64
+ # `swarph watchdog --install-service` can read them via importlib.resources.
65
+ # Closes ev_6954f748 substrate-component-installation-gap.
66
+ swarph_cli = ["systemd/*.service", "systemd/*.timer", "systemd/*.default"]
67
+
62
68
  [tool.pytest.ini_options]
63
69
  testpaths = ["tests"]
64
70
  addopts = "-v --tb=short"
@@ -16,6 +16,6 @@ The architecture splits CLI from substrate so:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- __version__ = "0.7.2"
19
+ __version__ = "0.7.3"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -91,6 +91,7 @@ Usage:
91
91
  [--gateway URL] [--tmux-session NAME]
92
92
  [--peer NAME] [--no-respawn]
93
93
  [--log PATH] [--verbose]
94
+ swarph watchdog --install-service [--cell ROLE] [--dry-run]
94
95
 
95
96
  Detects stranded Claude sessions (API throttle / harness death) and attempts
96
97
  recovery via tmux send-keys A1 wake-prompt, escalating to swarph spawn
@@ -99,6 +100,12 @@ respawn (A2) on persistent darkness.
99
100
  Designed for cron invocation:
100
101
  */5 * * * * swarph watchdog --check --cell lab >> ~/.local/log/swarph-watchdog.log 2>&1
101
102
 
103
+ OR systemd timer (v0.7.3+, closes ev_6954f748 substrate-component-installation-gap):
104
+ sudo swarph watchdog --install-service [--cell <role>]
105
+ # → installs /etc/systemd/system/swarph-watchdog.{service,timer}
106
+ # → installs /etc/default/swarph-watchdog with SWARPH_CELL=<role>
107
+ # → daemon-reload + enable --now swarph-watchdog.timer
108
+
102
109
  Detection (mother #1021 AND-gate design):
103
110
  PRIMARY: cursor file mtime — most-recent Claude action (drain script touches it)
104
111
  FALLBACK: pgrep claude on tmux session — confirms process aliveness
@@ -123,11 +130,12 @@ Flags:
123
130
  --verbose also write diagnostics to stderr
124
131
 
125
132
  Exit codes:
126
- 0 no action taken (session healthy or no unread DMs queued)
133
+ 0 no action taken (session healthy or no unread DMs queued); install ok
127
134
  1 A1 fired (wake-prompt sent)
128
135
  2 A2 fired (full respawn triggered)
129
136
  3 detection error (cursor unreadable / gateway unreachable)
130
- 4 configuration error (invalid args, no cell.yaml resolved)
137
+ 4 configuration error (invalid args, no cell.yaml resolved); install needs sudo
138
+ 5 install error (file write failed / systemctl failed)
131
139
  """
132
140
 
133
141
 
@@ -610,6 +618,107 @@ def run_check(args: argparse.Namespace) -> int:
610
618
  return 1 if sent else 4
611
619
 
612
620
 
621
+ _SYSTEMD_UNIT_DIR = Path("/etc/systemd/system")
622
+ _SYSTEMD_DEFAULT_DIR = Path("/etc/default")
623
+ _SYSTEMD_UNIT_NAMES = ("swarph-watchdog.service", "swarph-watchdog.timer")
624
+ _SYSTEMD_DEFAULT_NAME = "swarph-watchdog" # /etc/default/swarph-watchdog
625
+
626
+
627
+ def _bundled_systemd_files() -> dict[str, str]:
628
+ """Return {filename: content} for the 3 bundled systemd templates.
629
+
630
+ Reads from the package's bundled `systemd/` data directory via
631
+ importlib.resources. Works regardless of install method (pipx, pip,
632
+ editable, wheel-from-PyPI).
633
+ """
634
+ try:
635
+ from importlib.resources import files as _files
636
+ except ImportError: # pragma: no cover — Python <3.9 not supported anyway
637
+ from importlib_resources import files as _files # type: ignore[no-redef]
638
+
639
+ pkg_root = _files("swarph_cli") / "systemd"
640
+ out: dict[str, str] = {}
641
+ for name in (*_SYSTEMD_UNIT_NAMES, "swarph-watchdog.default"):
642
+ out[name] = (pkg_root / name).read_text(encoding="utf-8")
643
+ return out
644
+
645
+
646
+ def run_install_service(args: argparse.Namespace) -> int:
647
+ """Install systemd timer + service for periodic watchdog --check.
648
+
649
+ Idempotent: overwrites existing unit files (newer-version semantics).
650
+ Requires sudo for /etc/systemd/system writes unless --dry-run.
651
+
652
+ Exit codes:
653
+ 0 success (or dry-run completed)
654
+ 4 configuration error (non-root without --dry-run)
655
+ 5 install error (file write failed / systemctl failed)
656
+ """
657
+ files = _bundled_systemd_files()
658
+
659
+ # Template the default file with the requested role
660
+ default_content = files["swarph-watchdog.default"].replace(
661
+ "SWARPH_CELL=lab",
662
+ f"SWARPH_CELL={args.cell}",
663
+ 1,
664
+ )
665
+
666
+ targets = [
667
+ (_SYSTEMD_UNIT_DIR / _SYSTEMD_UNIT_NAMES[0], files[_SYSTEMD_UNIT_NAMES[0]]),
668
+ (_SYSTEMD_UNIT_DIR / _SYSTEMD_UNIT_NAMES[1], files[_SYSTEMD_UNIT_NAMES[1]]),
669
+ (_SYSTEMD_DEFAULT_DIR / _SYSTEMD_DEFAULT_NAME, default_content),
670
+ ]
671
+
672
+ if args.dry_run:
673
+ print(f"# DRY RUN — cell={args.cell}", file=sys.stderr)
674
+ for path, content in targets:
675
+ print(f"\n# would write {path}:", file=sys.stderr)
676
+ print(content, file=sys.stderr)
677
+ print(
678
+ "\n# would then run:\n"
679
+ "# sudo systemctl daemon-reload\n"
680
+ "# sudo systemctl enable --now swarph-watchdog.timer",
681
+ file=sys.stderr,
682
+ )
683
+ return 0
684
+
685
+ if os.geteuid() != 0:
686
+ print(
687
+ "ERROR: --install-service requires root. Re-run with sudo, or use "
688
+ "--dry-run to preview the install without writing.",
689
+ file=sys.stderr,
690
+ )
691
+ return 4
692
+
693
+ try:
694
+ for path, content in targets:
695
+ path.write_text(content, encoding="utf-8")
696
+ print(f"wrote {path}", file=sys.stderr)
697
+ except (OSError, PermissionError) as exc:
698
+ print(f"ERROR: failed to write unit files: {exc}", file=sys.stderr)
699
+ return 5
700
+
701
+ try:
702
+ subprocess.run(["systemctl", "daemon-reload"], check=True)
703
+ subprocess.run(
704
+ ["systemctl", "enable", "--now", "swarph-watchdog.timer"],
705
+ check=True,
706
+ )
707
+ except (subprocess.CalledProcessError, FileNotFoundError) as exc:
708
+ print(f"ERROR: systemctl failed: {exc}", file=sys.stderr)
709
+ return 5
710
+
711
+ print(
712
+ f"\nswarph-watchdog.timer installed + enabled for cell={args.cell}.\n"
713
+ f" status: systemctl status swarph-watchdog.timer\n"
714
+ f" logs: journalctl -u swarph-watchdog.service -f\n"
715
+ f" OR /var/log/swarph-watchdog.log\n"
716
+ f" next: systemctl list-timers swarph-watchdog.timer",
717
+ file=sys.stderr,
718
+ )
719
+ return 0
720
+
721
+
613
722
  def run_watchdog(argv: Optional[list[str]] = None) -> int:
614
723
  if argv is None:
615
724
  argv = sys.argv[2:] # skip "swarph watchdog"
@@ -623,6 +732,16 @@ def run_watchdog(argv: Optional[list[str]] = None) -> int:
623
732
  "--check", action="store_true",
624
733
  help="One-shot check (cron-callable; exits with status code).",
625
734
  )
735
+ p.add_argument(
736
+ "--install-service", action="store_true",
737
+ help="Install systemd timer + service for periodic --check invocation. "
738
+ "Requires sudo. Closes ev_6954f748 substrate-component-install gap.",
739
+ )
740
+ p.add_argument(
741
+ "--dry-run", action="store_true",
742
+ help="With --install-service: show what would be written without "
743
+ "writing. Useful for review or non-root preview.",
744
+ )
626
745
  p.add_argument("--cell", default=os.environ.get("SWARPH_CELL", "lab"))
627
746
  p.add_argument("--cursor", default=None)
628
747
  p.add_argument("--threshold", type=int, default=_DEFAULT_THRESHOLD_SEC)
@@ -646,6 +765,9 @@ def run_watchdog(argv: Optional[list[str]] = None) -> int:
646
765
  except SystemExit as exc:
647
766
  return int(exc.code or 0)
648
767
 
768
+ if args.install_service:
769
+ return run_install_service(args)
770
+
649
771
  if not args.check:
650
772
  print(_USAGE, file=sys.stderr)
651
773
  return 4
@@ -0,0 +1,9 @@
1
+ # /etc/default/swarph-watchdog — environment for swarph-watchdog.service
2
+ #
3
+ # SWARPH_CELL sets the cell role for the watchdog. The watchdog reads
4
+ # cell.yaml for this role to discover the cursor path + tmux session pin
5
+ # (per F4 cell.yaml-pinning landed in v0.7.2).
6
+ #
7
+ # If unset, watchdog defaults to "lab" (per the --cell argparse default).
8
+ # Override here per-vertex: droplet, gpu-wsl, razorpeter, etc.
9
+ SWARPH_CELL=lab
@@ -0,0 +1,15 @@
1
+ [Unit]
2
+ Description=Swarph watchdog one-shot check (stranded-session recovery)
3
+ Documentation=https://github.com/darw007d/swarph-cli
4
+ After=network-online.target
5
+ Wants=network-online.target
6
+
7
+ [Service]
8
+ Type=oneshot
9
+ EnvironmentFile=-/etc/default/swarph-watchdog
10
+ ExecStart=/usr/local/bin/swarph watchdog --check
11
+ StandardOutput=append:/var/log/swarph-watchdog.log
12
+ StandardError=append:/var/log/swarph-watchdog.log
13
+
14
+ [Install]
15
+ WantedBy=multi-user.target
@@ -0,0 +1,13 @@
1
+ [Unit]
2
+ Description=Run swarph watchdog every 5 minutes (stranded-session recovery)
3
+ Documentation=https://github.com/darw007d/swarph-cli
4
+ Requires=swarph-watchdog.service
5
+
6
+ [Timer]
7
+ OnBootSec=2min
8
+ OnUnitActiveSec=5min
9
+ AccuracySec=1min
10
+ Persistent=true
11
+
12
+ [Install]
13
+ WantedBy=timers.target
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.7.0 ships Phase 7 substrate-doc R7 §11.1.7 operator-tooling layer in 5 increments: PR-A `--new-instance` flag (sibling-spawn case) + PR-B auto-suffix on collision (sibling-slot persistence) + PR-C SessionStart hook (closes bare-claude operator-paste gap) + watchdog (stranded-session recovery) + PR-D swarph-shared cell.yaml relocation (cell-yaml schema graduates to swarph-shared 0.3.0 kernel-tier; substrate-doc R7 §11.1.5 (O5) RESOLVED).
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
@@ -138,6 +138,46 @@ Loud-on-down (PLAN §16.5): never silently exits. Cursor writes are atomic (writ
138
138
 
139
139
  `--auto-act` flag is documented for v0.5.1+ when handler registration via `@swarph.on_dm(...)` lands; v0.5.0 ships surface-only mode (DMs printed + JSONL-logged to `inbox.log`, no automatic replies).
140
140
 
141
+ ### `swarph watchdog` (Phase 7 — v0.7 stranded-session detection, v0.7.3 systemd install)
142
+
143
+ Detects stranded Claude sessions (API throttle / harness death) via cursor-mtime + tmux pgrep AND-gate, and recovers via A1 tmux send-keys wake-prompt → A2 `swarph spawn` respawn. Cell.yaml-pinned cursor + tmux session (F4) since v0.7.2.
144
+
145
+ **One-shot mode (cron-callable, v0.7+):**
146
+ ```bash
147
+ */5 * * * * swarph watchdog --check --cell lab >> ~/.local/log/swarph-watchdog.log 2>&1
148
+ ```
149
+
150
+ **Systemd timer install (v0.7.3+ — closes ev_6954f748 substrate-component-installation-gap):**
151
+
152
+ ```bash
153
+ # Preview without writing (any user):
154
+ swarph watchdog --install-service --cell droplet --dry-run
155
+
156
+ # Install + enable (requires root for /etc/systemd/system writes):
157
+ sudo swarph watchdog --install-service --cell droplet
158
+ ```
159
+
160
+ This writes three files:
161
+
162
+ | Path | Purpose |
163
+ |------|---------|
164
+ | `/etc/systemd/system/swarph-watchdog.service` | `Type=oneshot`, runs `swarph watchdog --check` |
165
+ | `/etc/systemd/system/swarph-watchdog.timer` | Fires every 5 minutes (`OnUnitActiveSec=5min`) |
166
+ | `/etc/default/swarph-watchdog` | Sets `SWARPH_CELL=<role>` for the service env |
167
+
168
+ Then runs `systemctl daemon-reload && systemctl enable --now swarph-watchdog.timer`. Idempotent — re-running overwrites with current package version (newer-version semantics).
169
+
170
+ Monitoring:
171
+
172
+ ```bash
173
+ systemctl status swarph-watchdog.timer # is it scheduled?
174
+ systemctl list-timers swarph-watchdog.timer # next fire?
175
+ journalctl -u swarph-watchdog.service -f # live log
176
+ tail -f /var/log/swarph-watchdog.log # append-log alternative
177
+ ```
178
+
179
+ Why this matters: pre-v0.7.3, swarph-cli shipped the watchdog code but no install path. Lab ran it via cron (manual setup); droplet never installed it at all. A real production silence-window (drop's ~24min mute 2026-05-14 08:38→09:02 UTC after an Anthropic API error) made the install-gap visible. v0.7.3 closes it for any peer with one command.
180
+
141
181
  ### `swarph onboard` + `swarph ratify` (Phase 5.5)
142
182
 
143
183
  Per PLAN.md §15, onboarding splits into a **mechanics phase** (`swarph onboard`) that automates the boring parts (registry POST, scaffolding, token resolution) and a **manual contract phase** (the new peer composes the handshake DM in their own words). A witness peer judges the handshake and runs `swarph ratify <peer>` to flip `ratified=true`, gating `task_claim` server-side.
@@ -23,6 +23,9 @@ src/swarph_cli/commands/spawn.py
23
23
  src/swarph_cli/commands/watchdog.py
24
24
  src/swarph_cli/parsers/__init__.py
25
25
  src/swarph_cli/parsers/claude.py
26
+ src/swarph_cli/systemd/swarph-watchdog.default
27
+ src/swarph_cli/systemd/swarph-watchdog.service
28
+ src/swarph_cli/systemd/swarph-watchdog.timer
26
29
  tests/test_cell_loader.py
27
30
  tests/test_chat_command.py
28
31
  tests/test_claude_parser.py
@@ -562,3 +562,65 @@ def test_watchdog_log_appends_across_invocations(isolated_state, monkeypatch):
562
562
  parsed_second = json.loads(lines[1])
563
563
  assert parsed_first["details"]["decision"] == "healthy_cursor_fresh"
564
564
  assert parsed_second["details"]["decision"] == "noop_no_unread"
565
+
566
+
567
+ # ---------------------------------------------------------------------------
568
+ # --install-service (v0.7.3 — closes ev_6954f748 substrate-component-install)
569
+ # ---------------------------------------------------------------------------
570
+
571
+
572
+ def test_install_service_dry_run_writes_no_files(isolated_state, capsys):
573
+ """--dry-run prints what would be written without touching the filesystem."""
574
+ rc = run_watchdog(argv=["--install-service", "--cell", "droplet", "--dry-run"])
575
+ assert rc == 0
576
+ captured = capsys.readouterr()
577
+ # Dry-run output goes to stderr
578
+ assert "DRY RUN" in captured.err
579
+ assert "cell=droplet" in captured.err
580
+ # All three target files surface in the preview
581
+ assert "/etc/systemd/system/swarph-watchdog.service" in captured.err
582
+ assert "/etc/systemd/system/swarph-watchdog.timer" in captured.err
583
+ assert "/etc/default/swarph-watchdog" in captured.err
584
+ # SWARPH_CELL was templated to the requested role
585
+ assert "SWARPH_CELL=droplet" in captured.err
586
+ # The bundled service file's identifying line shows up
587
+ assert "Swarph watchdog one-shot check" in captured.err
588
+
589
+
590
+ def test_install_service_dry_run_default_cell_is_lab(isolated_state, capsys):
591
+ """Without --cell, the dry-run preview keeps SWARPH_CELL=lab default."""
592
+ rc = run_watchdog(argv=["--install-service", "--dry-run"])
593
+ assert rc == 0
594
+ captured = capsys.readouterr()
595
+ assert "SWARPH_CELL=lab" in captured.err
596
+
597
+
598
+ def test_install_service_without_sudo_returns_4(isolated_state, capsys, monkeypatch):
599
+ """Non-root install (no --dry-run) refuses with helpful message + exit 4."""
600
+ monkeypatch.setattr("os.geteuid", lambda: 1000)
601
+ rc = run_watchdog(argv=["--install-service", "--cell", "droplet"])
602
+ assert rc == 4
603
+ captured = capsys.readouterr()
604
+ assert "requires root" in captured.err
605
+ assert "--dry-run" in captured.err # hint surfaces
606
+
607
+
608
+ def test_bundled_systemd_files_readable():
609
+ """Package-data manifest correctness — importlib.resources can read all
610
+ three bundled templates. Regression guard for pyproject package-data
611
+ declaration."""
612
+ from swarph_cli.commands.watchdog import _bundled_systemd_files
613
+
614
+ files = _bundled_systemd_files()
615
+ assert set(files.keys()) == {
616
+ "swarph-watchdog.service",
617
+ "swarph-watchdog.timer",
618
+ "swarph-watchdog.default",
619
+ }
620
+ # Service file has the expected Type=oneshot shape
621
+ assert "Type=oneshot" in files["swarph-watchdog.service"]
622
+ assert "ExecStart=/usr/local/bin/swarph watchdog --check" in files["swarph-watchdog.service"]
623
+ # Timer fires every 5 minutes
624
+ assert "OnUnitActiveSec=5min" in files["swarph-watchdog.timer"]
625
+ # Default file has the SWARPH_CELL=lab template line
626
+ assert "SWARPH_CELL=lab" in files["swarph-watchdog.default"]
File without changes
File without changes