swarph-cli 0.7.2__tar.gz → 0.7.4__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.7.2/src/swarph_cli.egg-info → swarph_cli-0.7.4}/PKG-INFO +41 -1
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/README.md +40 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/pyproject.toml +7 -1
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/commands/watchdog.py +166 -2
- swarph_cli-0.7.4/src/swarph_cli/systemd/swarph-watchdog.default +9 -0
- swarph_cli-0.7.4/src/swarph_cli/systemd/swarph-watchdog.service +15 -0
- swarph_cli-0.7.4/src/swarph_cli/systemd/swarph-watchdog.timer +13 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4/src/swarph_cli.egg-info}/PKG-INFO +41 -1
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli.egg-info/SOURCES.txt +3 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_watchdog.py +150 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/LICENSE +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/setup.cfg +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/commands/spawn.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_import_command.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_main.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.7.2 → swarph_cli-0.7.4}/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.
|
|
3
|
+
Version: 0.7.4
|
|
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.
|
|
7
|
+
version = "0.7.4"
|
|
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"
|
|
@@ -66,6 +66,7 @@ import argparse
|
|
|
66
66
|
import json
|
|
67
67
|
import os
|
|
68
68
|
import shlex
|
|
69
|
+
import shutil
|
|
69
70
|
import subprocess
|
|
70
71
|
import sys
|
|
71
72
|
import time
|
|
@@ -91,6 +92,7 @@ Usage:
|
|
|
91
92
|
[--gateway URL] [--tmux-session NAME]
|
|
92
93
|
[--peer NAME] [--no-respawn]
|
|
93
94
|
[--log PATH] [--verbose]
|
|
95
|
+
swarph watchdog --install-service [--cell ROLE] [--dry-run]
|
|
94
96
|
|
|
95
97
|
Detects stranded Claude sessions (API throttle / harness death) and attempts
|
|
96
98
|
recovery via tmux send-keys A1 wake-prompt, escalating to swarph spawn
|
|
@@ -99,6 +101,12 @@ respawn (A2) on persistent darkness.
|
|
|
99
101
|
Designed for cron invocation:
|
|
100
102
|
*/5 * * * * swarph watchdog --check --cell lab >> ~/.local/log/swarph-watchdog.log 2>&1
|
|
101
103
|
|
|
104
|
+
OR systemd timer (v0.7.3+, closes ev_6954f748 substrate-component-installation-gap):
|
|
105
|
+
sudo swarph watchdog --install-service [--cell <role>]
|
|
106
|
+
# → installs /etc/systemd/system/swarph-watchdog.{service,timer}
|
|
107
|
+
# → installs /etc/default/swarph-watchdog with SWARPH_CELL=<role>
|
|
108
|
+
# → daemon-reload + enable --now swarph-watchdog.timer
|
|
109
|
+
|
|
102
110
|
Detection (mother #1021 AND-gate design):
|
|
103
111
|
PRIMARY: cursor file mtime — most-recent Claude action (drain script touches it)
|
|
104
112
|
FALLBACK: pgrep claude on tmux session — confirms process aliveness
|
|
@@ -123,11 +131,12 @@ Flags:
|
|
|
123
131
|
--verbose also write diagnostics to stderr
|
|
124
132
|
|
|
125
133
|
Exit codes:
|
|
126
|
-
0 no action taken (session healthy or no unread DMs queued)
|
|
134
|
+
0 no action taken (session healthy or no unread DMs queued); install ok
|
|
127
135
|
1 A1 fired (wake-prompt sent)
|
|
128
136
|
2 A2 fired (full respawn triggered)
|
|
129
137
|
3 detection error (cursor unreadable / gateway unreachable)
|
|
130
|
-
4 configuration error (invalid args, no cell.yaml resolved)
|
|
138
|
+
4 configuration error (invalid args, no cell.yaml resolved); install needs sudo
|
|
139
|
+
5 install error (file write failed / systemctl failed)
|
|
131
140
|
"""
|
|
132
141
|
|
|
133
142
|
|
|
@@ -610,6 +619,148 @@ def run_check(args: argparse.Namespace) -> int:
|
|
|
610
619
|
return 1 if sent else 4
|
|
611
620
|
|
|
612
621
|
|
|
622
|
+
_SYSTEMD_UNIT_DIR = Path("/etc/systemd/system")
|
|
623
|
+
_SYSTEMD_DEFAULT_DIR = Path("/etc/default")
|
|
624
|
+
_SYSTEMD_UNIT_NAMES = ("swarph-watchdog.service", "swarph-watchdog.timer")
|
|
625
|
+
_SYSTEMD_DEFAULT_NAME = "swarph-watchdog" # /etc/default/swarph-watchdog
|
|
626
|
+
# v0.7.3 bundled template's ExecStart placeholder — substituted at install time
|
|
627
|
+
# by `_resolve_swarph_bin()` to the actual binary path on the install host.
|
|
628
|
+
# Fixes the v0.7.3 hardcode that broke pipx-installed peers (binary at
|
|
629
|
+
# ~/.local/bin/swarph not /usr/local/bin/swarph).
|
|
630
|
+
_SWARPH_BIN_PLACEHOLDER = "/usr/local/bin/swarph"
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _resolve_swarph_bin() -> str:
|
|
634
|
+
"""Resolve the absolute path of the running swarph binary.
|
|
635
|
+
|
|
636
|
+
Resolution order:
|
|
637
|
+
1. ``sys.argv[0]`` if it's an absolute path — most reliable, equals
|
|
638
|
+
the path the user invoked
|
|
639
|
+
2. ``shutil.which(sys.argv[0])`` — bare-name invocation, look up in PATH
|
|
640
|
+
3. ``shutil.which("swarph")`` — generic PATH lookup as fallback
|
|
641
|
+
4. ``/usr/local/bin/swarph`` — last-resort default (matches v0.7.3
|
|
642
|
+
hardcode behavior; no regression if all three above fail)
|
|
643
|
+
|
|
644
|
+
ALWAYS returns an absolute path — systemd ExecStart requires absolute.
|
|
645
|
+
Relative inputs (e.g. ``venv/bin/swarph`` from editable installs) get
|
|
646
|
+
abspath'd against cwd. Never raises.
|
|
647
|
+
"""
|
|
648
|
+
invoked = sys.argv[0] or "swarph"
|
|
649
|
+
if Path(invoked).is_absolute():
|
|
650
|
+
return invoked
|
|
651
|
+
resolved = shutil.which(invoked) or shutil.which("swarph")
|
|
652
|
+
if not resolved:
|
|
653
|
+
return _SWARPH_BIN_PLACEHOLDER
|
|
654
|
+
return os.path.abspath(resolved)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _bundled_systemd_files() -> dict[str, str]:
|
|
658
|
+
"""Return {filename: content} for the 3 bundled systemd templates.
|
|
659
|
+
|
|
660
|
+
Reads from the package's bundled `systemd/` data directory via
|
|
661
|
+
importlib.resources. Works regardless of install method (pipx, pip,
|
|
662
|
+
editable, wheel-from-PyPI).
|
|
663
|
+
"""
|
|
664
|
+
try:
|
|
665
|
+
from importlib.resources import files as _files
|
|
666
|
+
except ImportError: # pragma: no cover — Python <3.9 not supported anyway
|
|
667
|
+
from importlib_resources import files as _files # type: ignore[no-redef]
|
|
668
|
+
|
|
669
|
+
pkg_root = _files("swarph_cli") / "systemd"
|
|
670
|
+
out: dict[str, str] = {}
|
|
671
|
+
for name in (*_SYSTEMD_UNIT_NAMES, "swarph-watchdog.default"):
|
|
672
|
+
out[name] = (pkg_root / name).read_text(encoding="utf-8")
|
|
673
|
+
return out
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def run_install_service(args: argparse.Namespace) -> int:
|
|
677
|
+
"""Install systemd timer + service for periodic watchdog --check.
|
|
678
|
+
|
|
679
|
+
Idempotent: overwrites existing unit files (newer-version semantics).
|
|
680
|
+
Requires sudo for /etc/systemd/system writes unless --dry-run.
|
|
681
|
+
|
|
682
|
+
Exit codes:
|
|
683
|
+
0 success (or dry-run completed)
|
|
684
|
+
4 configuration error (non-root without --dry-run)
|
|
685
|
+
5 install error (file write failed / systemctl failed)
|
|
686
|
+
"""
|
|
687
|
+
files = _bundled_systemd_files()
|
|
688
|
+
|
|
689
|
+
# v0.7.4: substitute the bundled service template's ExecStart placeholder
|
|
690
|
+
# with the actual swarph binary path on this host. Fixes the v0.7.3 hardcode
|
|
691
|
+
# that broke pipx-installed peers (binary at ~/.local/bin/swarph not
|
|
692
|
+
# /usr/local/bin/swarph). Pipx is the recommended install path on droplet
|
|
693
|
+
# + lab, so the hardcode bit BOTH peers on first install attempt today.
|
|
694
|
+
swarph_bin = _resolve_swarph_bin()
|
|
695
|
+
service_content = files[_SYSTEMD_UNIT_NAMES[0]].replace(
|
|
696
|
+
f"ExecStart={_SWARPH_BIN_PLACEHOLDER}",
|
|
697
|
+
f"ExecStart={swarph_bin}",
|
|
698
|
+
1,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
# Template the default file with the requested role
|
|
702
|
+
default_content = files["swarph-watchdog.default"].replace(
|
|
703
|
+
"SWARPH_CELL=lab",
|
|
704
|
+
f"SWARPH_CELL={args.cell}",
|
|
705
|
+
1,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
targets = [
|
|
709
|
+
(_SYSTEMD_UNIT_DIR / _SYSTEMD_UNIT_NAMES[0], service_content),
|
|
710
|
+
(_SYSTEMD_UNIT_DIR / _SYSTEMD_UNIT_NAMES[1], files[_SYSTEMD_UNIT_NAMES[1]]),
|
|
711
|
+
(_SYSTEMD_DEFAULT_DIR / _SYSTEMD_DEFAULT_NAME, default_content),
|
|
712
|
+
]
|
|
713
|
+
|
|
714
|
+
if args.dry_run:
|
|
715
|
+
print(f"# DRY RUN — cell={args.cell} swarph_bin={swarph_bin}", file=sys.stderr)
|
|
716
|
+
for path, content in targets:
|
|
717
|
+
print(f"\n# would write {path}:", file=sys.stderr)
|
|
718
|
+
print(content, file=sys.stderr)
|
|
719
|
+
print(
|
|
720
|
+
"\n# would then run:\n"
|
|
721
|
+
"# sudo systemctl daemon-reload\n"
|
|
722
|
+
"# sudo systemctl enable --now swarph-watchdog.timer",
|
|
723
|
+
file=sys.stderr,
|
|
724
|
+
)
|
|
725
|
+
return 0
|
|
726
|
+
|
|
727
|
+
if os.geteuid() != 0:
|
|
728
|
+
print(
|
|
729
|
+
"ERROR: --install-service requires root. Re-run with sudo, or use "
|
|
730
|
+
"--dry-run to preview the install without writing.",
|
|
731
|
+
file=sys.stderr,
|
|
732
|
+
)
|
|
733
|
+
return 4
|
|
734
|
+
|
|
735
|
+
try:
|
|
736
|
+
for path, content in targets:
|
|
737
|
+
path.write_text(content, encoding="utf-8")
|
|
738
|
+
print(f"wrote {path}", file=sys.stderr)
|
|
739
|
+
except (OSError, PermissionError) as exc:
|
|
740
|
+
print(f"ERROR: failed to write unit files: {exc}", file=sys.stderr)
|
|
741
|
+
return 5
|
|
742
|
+
|
|
743
|
+
try:
|
|
744
|
+
subprocess.run(["systemctl", "daemon-reload"], check=True)
|
|
745
|
+
subprocess.run(
|
|
746
|
+
["systemctl", "enable", "--now", "swarph-watchdog.timer"],
|
|
747
|
+
check=True,
|
|
748
|
+
)
|
|
749
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
|
750
|
+
print(f"ERROR: systemctl failed: {exc}", file=sys.stderr)
|
|
751
|
+
return 5
|
|
752
|
+
|
|
753
|
+
print(
|
|
754
|
+
f"\nswarph-watchdog.timer installed + enabled for cell={args.cell}.\n"
|
|
755
|
+
f" status: systemctl status swarph-watchdog.timer\n"
|
|
756
|
+
f" logs: journalctl -u swarph-watchdog.service -f\n"
|
|
757
|
+
f" OR /var/log/swarph-watchdog.log\n"
|
|
758
|
+
f" next: systemctl list-timers swarph-watchdog.timer",
|
|
759
|
+
file=sys.stderr,
|
|
760
|
+
)
|
|
761
|
+
return 0
|
|
762
|
+
|
|
763
|
+
|
|
613
764
|
def run_watchdog(argv: Optional[list[str]] = None) -> int:
|
|
614
765
|
if argv is None:
|
|
615
766
|
argv = sys.argv[2:] # skip "swarph watchdog"
|
|
@@ -623,6 +774,16 @@ def run_watchdog(argv: Optional[list[str]] = None) -> int:
|
|
|
623
774
|
"--check", action="store_true",
|
|
624
775
|
help="One-shot check (cron-callable; exits with status code).",
|
|
625
776
|
)
|
|
777
|
+
p.add_argument(
|
|
778
|
+
"--install-service", action="store_true",
|
|
779
|
+
help="Install systemd timer + service for periodic --check invocation. "
|
|
780
|
+
"Requires sudo. Closes ev_6954f748 substrate-component-install gap.",
|
|
781
|
+
)
|
|
782
|
+
p.add_argument(
|
|
783
|
+
"--dry-run", action="store_true",
|
|
784
|
+
help="With --install-service: show what would be written without "
|
|
785
|
+
"writing. Useful for review or non-root preview.",
|
|
786
|
+
)
|
|
626
787
|
p.add_argument("--cell", default=os.environ.get("SWARPH_CELL", "lab"))
|
|
627
788
|
p.add_argument("--cursor", default=None)
|
|
628
789
|
p.add_argument("--threshold", type=int, default=_DEFAULT_THRESHOLD_SEC)
|
|
@@ -646,6 +807,9 @@ def run_watchdog(argv: Optional[list[str]] = None) -> int:
|
|
|
646
807
|
except SystemExit as exc:
|
|
647
808
|
return int(exc.code or 0)
|
|
648
809
|
|
|
810
|
+
if args.install_service:
|
|
811
|
+
return run_install_service(args)
|
|
812
|
+
|
|
649
813
|
if not args.check:
|
|
650
814
|
print(_USAGE, file=sys.stderr)
|
|
651
815
|
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.
|
|
3
|
+
Version: 0.7.4
|
|
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,153 @@ 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"]
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
# ---------------------------------------------------------------------------
|
|
630
|
+
# v0.7.4 — _resolve_swarph_bin + ExecStart templating
|
|
631
|
+
# ---------------------------------------------------------------------------
|
|
632
|
+
#
|
|
633
|
+
# v0.7.3 shipped a hardcoded ExecStart=/usr/local/bin/swarph that broke
|
|
634
|
+
# pipx-installed peers (lab + drop both hit this on first install attempt
|
|
635
|
+
# 2026-05-14). v0.7.4 resolves the path at install time via _resolve_swarph_bin
|
|
636
|
+
# and substitutes into the bundled service template.
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def test_resolve_swarph_bin_absolute_argv0_wins(monkeypatch):
|
|
640
|
+
"""If sys.argv[0] is absolute, it's the most reliable signal — use it."""
|
|
641
|
+
from swarph_cli.commands.watchdog import _resolve_swarph_bin
|
|
642
|
+
|
|
643
|
+
monkeypatch.setattr("sys.argv", ["/home/ubuntu/.local/bin/swarph", "watchdog"])
|
|
644
|
+
assert _resolve_swarph_bin() == "/home/ubuntu/.local/bin/swarph"
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def test_resolve_swarph_bin_bare_name_resolved_via_path(monkeypatch, tmp_path):
|
|
648
|
+
"""Bare-name argv[0] resolves via PATH."""
|
|
649
|
+
from swarph_cli.commands.watchdog import _resolve_swarph_bin
|
|
650
|
+
|
|
651
|
+
fake_bin = tmp_path / "swarph"
|
|
652
|
+
fake_bin.write_text("#!/bin/sh\nexit 0\n")
|
|
653
|
+
fake_bin.chmod(0o755)
|
|
654
|
+
monkeypatch.setattr("sys.argv", ["swarph", "watchdog"])
|
|
655
|
+
monkeypatch.setattr("shutil.which", lambda name: str(fake_bin) if name == "swarph" else None)
|
|
656
|
+
assert _resolve_swarph_bin() == str(fake_bin)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def test_resolve_swarph_bin_falls_back_to_placeholder_if_unresolvable(monkeypatch):
|
|
660
|
+
"""All resolution paths fail → fall back to /usr/local/bin/swarph
|
|
661
|
+
(v0.7.3 hardcode behavior; no regression vs prior version)."""
|
|
662
|
+
from swarph_cli.commands.watchdog import _resolve_swarph_bin, _SWARPH_BIN_PLACEHOLDER
|
|
663
|
+
|
|
664
|
+
monkeypatch.setattr("sys.argv", ["swarph", "watchdog"])
|
|
665
|
+
monkeypatch.setattr("shutil.which", lambda name: None)
|
|
666
|
+
assert _resolve_swarph_bin() == _SWARPH_BIN_PLACEHOLDER
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def test_install_service_dry_run_substitutes_swarph_bin(isolated_state, capsys, monkeypatch):
|
|
670
|
+
"""Dry-run preview shows resolved swarph path in ExecStart, not hardcoded
|
|
671
|
+
/usr/local/bin/swarph (when the host actually has swarph elsewhere)."""
|
|
672
|
+
pipx_path = "/home/ubuntu/.local/bin/swarph"
|
|
673
|
+
monkeypatch.setattr("sys.argv", [pipx_path, "watchdog"])
|
|
674
|
+
rc = run_watchdog(argv=["--install-service", "--cell", "droplet", "--dry-run"])
|
|
675
|
+
assert rc == 0
|
|
676
|
+
captured = capsys.readouterr()
|
|
677
|
+
# Header surfaces the resolved binary
|
|
678
|
+
assert f"swarph_bin={pipx_path}" in captured.err
|
|
679
|
+
# Service file's ExecStart now points at pipx path, NOT the placeholder
|
|
680
|
+
assert f"ExecStart={pipx_path} watchdog --check" in captured.err
|
|
681
|
+
# And the v0.7.3-hardcoded path no longer appears in the preview
|
|
682
|
+
assert "ExecStart=/usr/local/bin/swarph watchdog --check" not in captured.err
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def test_install_service_dry_run_preserves_default_path_when_absolute(isolated_state, capsys, monkeypatch):
|
|
686
|
+
"""When sys.argv[0] IS /usr/local/bin/swarph (the v0.7.3 default path),
|
|
687
|
+
the substitution still happens but produces the same line — no diff vs
|
|
688
|
+
v0.7.3 for this case."""
|
|
689
|
+
monkeypatch.setattr("sys.argv", ["/usr/local/bin/swarph", "watchdog"])
|
|
690
|
+
rc = run_watchdog(argv=["--install-service", "--cell", "lab", "--dry-run"])
|
|
691
|
+
assert rc == 0
|
|
692
|
+
captured = capsys.readouterr()
|
|
693
|
+
assert "ExecStart=/usr/local/bin/swarph watchdog --check" in captured.err
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def test_resolve_swarph_bin_relative_with_slash_resolves_to_absolute(tmp_path, monkeypatch):
|
|
697
|
+
"""Relative path with slash (e.g. editable install's venv/bin/swarph)
|
|
698
|
+
must be absolutized — systemd ExecStart needs absolute. Regression guard
|
|
699
|
+
for the abspath fix on top of v0.7.4 path-autodetect."""
|
|
700
|
+
from swarph_cli.commands.watchdog import _resolve_swarph_bin
|
|
701
|
+
|
|
702
|
+
fake = tmp_path / "swarph"
|
|
703
|
+
fake.write_text("#!/bin/sh\nexit 0\n")
|
|
704
|
+
fake.chmod(0o755)
|
|
705
|
+
# Simulate `./swarph` from cwd=tmp_path
|
|
706
|
+
monkeypatch.chdir(tmp_path)
|
|
707
|
+
monkeypatch.setattr("sys.argv", ["./swarph", "watchdog"])
|
|
708
|
+
# shutil.which('./swarph') returns the relative path as-is when the
|
|
709
|
+
# input contains a slash. _resolve_swarph_bin must abspath it.
|
|
710
|
+
resolved = _resolve_swarph_bin()
|
|
711
|
+
assert Path(resolved).is_absolute(), (
|
|
712
|
+
f"resolver returned non-absolute path: {resolved!r}"
|
|
713
|
+
)
|
|
714
|
+
assert resolved == str(fake)
|
|
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
|