swarph-cli 0.9.2__tar.gz → 0.9.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.9.2/src/swarph_cli.egg-info → swarph_cli-0.9.4}/PKG-INFO +2 -2
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/pyproject.toml +2 -2
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/cell.py +60 -8
- swarph_cli-0.9.4/src/swarph_cli/commands/init.py +237 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/commands/memory_sync.py +9 -4
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/commands/mesh.py +22 -7
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/commands/spawn.py +9 -1
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/main.py +1 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4/src/swarph_cli.egg-info}/PKG-INFO +2 -2
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli.egg-info/SOURCES.txt +2 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_cell_loader.py +103 -4
- swarph_cli-0.9.4/tests/test_init_command.py +83 -0
- swarph_cli-0.9.4/tests/test_memory_sync.py +92 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_mesh_command.py +21 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_mesh_sidecar.py +41 -2
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_spawn_command.py +45 -3
- swarph_cli-0.9.2/tests/test_memory_sync.py +0 -57
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/LICENSE +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/README.md +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/setup.cfg +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_import_command.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_main.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.9.2 → swarph_cli-0.9.4}/tests/test_watchdog.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),
|
|
3
|
+
Version: 0.9.4
|
|
4
|
+
Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.4: fixes session-pin resume across a changed cwd.
|
|
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),
|
|
7
|
+
version = "0.9.4"
|
|
8
|
+
description = "The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.4: fixes session-pin resume across a changed cwd."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -252,6 +252,42 @@ def load_cell(path: Path) -> Cell:
|
|
|
252
252
|
return cell
|
|
253
253
|
|
|
254
254
|
|
|
255
|
+
def _write_session_sidecar(target: Path, session_id: str, cwd) -> None:
|
|
256
|
+
"""Write the two-line ``{uuid, cwd}`` session sidecar atomically.
|
|
257
|
+
|
|
258
|
+
v0.9.4 (session-pin cwd-mismatch fix): the sidecar records the cwd
|
|
259
|
+
the session UUID was minted for so a later spawn from a DIFFERENT
|
|
260
|
+
cwd can detect the mismatch and re-pin a fresh UUID rather than
|
|
261
|
+
handing a project-scoped UUID to ``claude --resume`` from the wrong
|
|
262
|
+
project (which dies with "No conversation found with session ID").
|
|
263
|
+
|
|
264
|
+
Format (trailing-newline convention preserved):
|
|
265
|
+
|
|
266
|
+
<uuid>\\n
|
|
267
|
+
<cwd>\\n
|
|
268
|
+
"""
|
|
269
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
_atomic_write_text(target, f"{session_id}\n{str(cwd)}\n")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _read_session_sidecar(path: Path) -> tuple[Optional[str], Optional[str]]:
|
|
274
|
+
"""Parse a session sidecar → ``(uuid, cwd)``.
|
|
275
|
+
|
|
276
|
+
Returns ``(None, None)`` for a missing/empty file. ``cwd`` is
|
|
277
|
+
``None`` for a **legacy one-line** sidecar (bare UUID, pre-0.9.4)
|
|
278
|
+
— we cannot know which project that UUID belongs to.
|
|
279
|
+
"""
|
|
280
|
+
if not path.exists():
|
|
281
|
+
return None, None
|
|
282
|
+
raw = path.read_text(encoding="utf-8")
|
|
283
|
+
lines = raw.splitlines()
|
|
284
|
+
if not lines:
|
|
285
|
+
return None, None
|
|
286
|
+
uuid_str = lines[0].strip() or None
|
|
287
|
+
cwd_str = lines[1].strip() if len(lines) >= 2 and lines[1].strip() else None
|
|
288
|
+
return uuid_str, cwd_str
|
|
289
|
+
|
|
290
|
+
|
|
255
291
|
def load_or_create_session_id(
|
|
256
292
|
role: str,
|
|
257
293
|
cell: Cell,
|
|
@@ -266,6 +302,10 @@ def load_or_create_session_id(
|
|
|
266
302
|
2. ``new_instance=True`` AND base sidecar exists — auto-suffix slot
|
|
267
303
|
3. ``new_instance=True`` AND no base sidecar — fall through (degenerate)
|
|
268
304
|
4. session_state_path(role) reused — default re-resume path
|
|
305
|
+
(v0.9.4: only when the sidecar's recorded cwd matches cell.cwd;
|
|
306
|
+
on cwd-mismatch a fresh UUID is minted + re-pinned for the new
|
|
307
|
+
cwd, because ``claude --resume`` is PROJECT-scoped and a pin
|
|
308
|
+
from another cwd dies with "No conversation found")
|
|
269
309
|
5. mint new uuid4 + persist
|
|
270
310
|
|
|
271
311
|
Caller-side discipline (mother iter-1 #986): the ``effective_role``
|
|
@@ -282,23 +322,33 @@ def load_or_create_session_id(
|
|
|
282
322
|
sibling_role = next_free_slot_role(role)
|
|
283
323
|
sibling_state = session_state_path(sibling_role)
|
|
284
324
|
new_id = str(uuid.uuid4())
|
|
285
|
-
sibling_state
|
|
286
|
-
_atomic_write_text(sibling_state, new_id + "\n")
|
|
325
|
+
_write_session_sidecar(sibling_state, new_id, cell.cwd)
|
|
287
326
|
return new_id, True, sibling_role
|
|
288
327
|
|
|
289
328
|
state_file = session_state_path(role)
|
|
290
329
|
if state_file.exists():
|
|
291
|
-
|
|
292
|
-
if
|
|
330
|
+
existing_uuid, recorded_cwd = _read_session_sidecar(state_file)
|
|
331
|
+
if existing_uuid:
|
|
293
332
|
try:
|
|
294
|
-
|
|
333
|
+
valid_uuid = validate_uuid_str(existing_uuid)
|
|
295
334
|
except CellError:
|
|
296
335
|
# Corrupted state — fall through and regenerate.
|
|
297
|
-
|
|
336
|
+
valid_uuid = None
|
|
337
|
+
if valid_uuid is not None:
|
|
338
|
+
if recorded_cwd is not None and recorded_cwd != str(cell.cwd):
|
|
339
|
+
# v0.9.4: pin belongs to a different project. A
|
|
340
|
+
# project-scoped --resume would die from this cwd, so
|
|
341
|
+
# mint a fresh (clean) UUID + re-pin for the new cwd.
|
|
342
|
+
fresh = str(uuid.uuid4())
|
|
343
|
+
_write_session_sidecar(state_file, fresh, cell.cwd)
|
|
344
|
+
return fresh, True, role
|
|
345
|
+
# Legacy (recorded_cwd is None) → reuse as-is; do NOT
|
|
346
|
+
# rewrite, since the UUID's true origin cwd is unknown.
|
|
347
|
+
# Matching cwd → reuse.
|
|
348
|
+
return valid_uuid, False, role
|
|
298
349
|
|
|
299
350
|
new_id = str(uuid.uuid4())
|
|
300
|
-
state_file
|
|
301
|
-
_atomic_write_text(state_file, new_id + "\n")
|
|
351
|
+
_write_session_sidecar(state_file, new_id, cell.cwd)
|
|
302
352
|
return new_id, True, role
|
|
303
353
|
|
|
304
354
|
|
|
@@ -352,6 +402,8 @@ __all__ = [
|
|
|
352
402
|
"read_starter_prompt",
|
|
353
403
|
"load_cell",
|
|
354
404
|
"load_or_create_session_id",
|
|
405
|
+
"_read_session_sidecar",
|
|
406
|
+
"_write_session_sidecar",
|
|
355
407
|
# Backward-compat aliases
|
|
356
408
|
"_PEER_NAME_RE",
|
|
357
409
|
"_VALID_SCHEMA_VERSIONS",
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""``swarph init`` — interactive install wizard that scaffolds a validated
|
|
2
|
+
cell.yaml at the named registry path (``~/.config/swarph/cells/<name>.yaml``).
|
|
3
|
+
|
|
4
|
+
The default UX is a guided setup: name → LLM type → role/cwd/tmux → assisted
|
|
5
|
+
memory (y/n) → register (y/n) → confirm + write. Flags override any prompt, so
|
|
6
|
+
a fully-flagged invocation runs non-interactively (scripting / CI).
|
|
7
|
+
|
|
8
|
+
The cell dict is validated through ``swarph_shared.cell.parse_cell_dict`` BEFORE
|
|
9
|
+
writing — init never emits a cell.yaml that wouldn't parse. See
|
|
10
|
+
``docs/superpowers/specs/2026-06-02-swarph-init-design.md``.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import copy
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
|
|
23
|
+
from swarph_shared.cell import PEER_NAME_RE, VALID_PROVIDERS, parse_cell_dict
|
|
24
|
+
from swarph_cli.cell import CellError, cells_dir
|
|
25
|
+
|
|
26
|
+
_DEFAULT_GATEWAY = os.environ.get("MESH_GATEWAY_URL", "http://lab-ovh:8788")
|
|
27
|
+
_CODEX_SANDBOX_DEFAULT = "workspace-write"
|
|
28
|
+
_CODEX_SANDBOX_VALUES = ("workspace-write", "read-only")
|
|
29
|
+
|
|
30
|
+
# LLM type → (provider, blurb). The menu the wizard shows.
|
|
31
|
+
_LLM_CHOICES = [
|
|
32
|
+
("claude", "Anthropic Claude — claude membrane"),
|
|
33
|
+
("codex", "OpenAI / GPT — codex membrane (AGENTS.md)"),
|
|
34
|
+
("antigravity", "Google Gemini — agy membrane (firejail)"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Prompt helpers (interactive). All no-op when a flag already supplied the value.
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def _ask(prompt: str, default: Optional[str] = None) -> str:
|
|
43
|
+
suffix = f" [{default}]" if default else ""
|
|
44
|
+
try:
|
|
45
|
+
ans = input(f"{prompt}{suffix}: ").strip()
|
|
46
|
+
except EOFError:
|
|
47
|
+
ans = ""
|
|
48
|
+
return ans or (default or "")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _ask_yn(prompt: str, default: bool = False) -> bool:
|
|
52
|
+
d = "Y/n" if default else "y/N"
|
|
53
|
+
try:
|
|
54
|
+
ans = input(f"{prompt} [{d}]: ").strip().lower()
|
|
55
|
+
except EOFError:
|
|
56
|
+
ans = ""
|
|
57
|
+
if not ans:
|
|
58
|
+
return default
|
|
59
|
+
return ans in ("y", "yes")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _ask_llm() -> str:
|
|
63
|
+
print("\nSelect LLM type:")
|
|
64
|
+
for i, (prov, blurb) in enumerate(_LLM_CHOICES, 1):
|
|
65
|
+
print(f" {i}) {prov:12} {blurb}")
|
|
66
|
+
while True:
|
|
67
|
+
try:
|
|
68
|
+
ans = input("Choice [1-3]: ").strip()
|
|
69
|
+
except EOFError:
|
|
70
|
+
ans = "1"
|
|
71
|
+
if ans.isdigit() and 1 <= int(ans) <= len(_LLM_CHOICES):
|
|
72
|
+
return _LLM_CHOICES[int(ans) - 1][0]
|
|
73
|
+
if ans in VALID_PROVIDERS:
|
|
74
|
+
return ans
|
|
75
|
+
print(" (enter 1, 2, or 3)")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _https_normalize(repo: str) -> tuple[str, bool]:
|
|
79
|
+
"""Rewrite an SSH GitHub URL to HTTPS (this box auths via gh/HTTPS, not a
|
|
80
|
+
deploy key). Returns (url, rewritten)."""
|
|
81
|
+
if repo.startswith("git@github.com:"):
|
|
82
|
+
return "https://github.com/" + repo[len("git@github.com:"):], True
|
|
83
|
+
return repo, False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
87
|
+
p = argparse.ArgumentParser(
|
|
88
|
+
prog="swarph init",
|
|
89
|
+
description="Interactive wizard: scaffold a cell.yaml at "
|
|
90
|
+
"~/.config/swarph/cells/<name>.yaml. Flags override prompts.",
|
|
91
|
+
)
|
|
92
|
+
p.add_argument("name", nargs="?", default=None,
|
|
93
|
+
help="kebab-case mesh peer name (prompted if omitted)")
|
|
94
|
+
p.add_argument("--provider", choices=sorted(VALID_PROVIDERS), default=None,
|
|
95
|
+
help="spawn provider/membrane (prompted if omitted)")
|
|
96
|
+
p.add_argument("--role", default=None)
|
|
97
|
+
p.add_argument("--cwd", default=None)
|
|
98
|
+
p.add_argument("--tmux", default=None)
|
|
99
|
+
p.add_argument("--cursor", default=None)
|
|
100
|
+
p.add_argument("--sandbox", default=None)
|
|
101
|
+
p.add_argument("--gateway", default=_DEFAULT_GATEWAY)
|
|
102
|
+
p.add_argument("--assisted-memory", dest="assisted_memory", default=None, metavar="REPO",
|
|
103
|
+
help="enable git-backed memory at REPO (SSH→HTTPS normalized)")
|
|
104
|
+
p.add_argument("--starter", default=None)
|
|
105
|
+
p.add_argument("--symlink-cwd", dest="symlink_cwd", action="store_true")
|
|
106
|
+
p.add_argument("--force", action="store_true")
|
|
107
|
+
p.add_argument("--non-interactive", "-y", dest="non_interactive", action="store_true",
|
|
108
|
+
help="never prompt; require all values via flags")
|
|
109
|
+
return p
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def run_init(argv: list[str]) -> int:
|
|
113
|
+
args = _build_parser().parse_args(argv)
|
|
114
|
+
# Interactive unless explicitly disabled or stdin isn't a TTY.
|
|
115
|
+
interactive = (not args.non_interactive) and sys.stdin.isatty()
|
|
116
|
+
|
|
117
|
+
# ── name ──
|
|
118
|
+
name = args.name
|
|
119
|
+
if not name and interactive:
|
|
120
|
+
name = _ask("Cell / peer name (kebab-case)")
|
|
121
|
+
if not name:
|
|
122
|
+
print("swarph init: name required (positional or prompt)", file=sys.stderr)
|
|
123
|
+
return 2
|
|
124
|
+
if not PEER_NAME_RE.match(name):
|
|
125
|
+
print(f"swarph init: name {name!r} must match {PEER_NAME_RE.pattern} "
|
|
126
|
+
f"(kebab-case, no underscores)", file=sys.stderr)
|
|
127
|
+
return 2
|
|
128
|
+
|
|
129
|
+
# ── LLM type / provider ──
|
|
130
|
+
provider = args.provider
|
|
131
|
+
if not provider and interactive:
|
|
132
|
+
provider = _ask_llm()
|
|
133
|
+
if not provider:
|
|
134
|
+
print("swarph init: --provider required (or run interactively)", file=sys.stderr)
|
|
135
|
+
return 2
|
|
136
|
+
|
|
137
|
+
# ── role / cwd / tmux / cursor ──
|
|
138
|
+
role = args.role or (_ask("Role", name) if interactive else name)
|
|
139
|
+
cwd_raw = args.cwd or (_ask("Working dir (cwd)", str(Path.cwd())) if interactive else str(Path.cwd()))
|
|
140
|
+
cwd = Path(cwd_raw).expanduser().resolve()
|
|
141
|
+
tmux = args.tmux or (_ask("tmux session", name) if interactive else name)
|
|
142
|
+
cursor = args.cursor or f"/tmp/{name}-cursor.json"
|
|
143
|
+
|
|
144
|
+
# ── sandbox ──
|
|
145
|
+
if provider == "codex":
|
|
146
|
+
sandbox = args.sandbox or _CODEX_SANDBOX_DEFAULT
|
|
147
|
+
if sandbox not in _CODEX_SANDBOX_VALUES:
|
|
148
|
+
print(f"swarph init: codex --sandbox must be one of {_CODEX_SANDBOX_VALUES}, "
|
|
149
|
+
f"got {sandbox!r}", file=sys.stderr)
|
|
150
|
+
return 2
|
|
151
|
+
else:
|
|
152
|
+
sandbox = args.sandbox # antigravity default-on if None; claude ignores
|
|
153
|
+
|
|
154
|
+
cell: dict = {
|
|
155
|
+
"schema_version": "v1",
|
|
156
|
+
"name": name,
|
|
157
|
+
"role": role,
|
|
158
|
+
"provider": provider,
|
|
159
|
+
"cwd": str(cwd),
|
|
160
|
+
"tmux_session": tmux,
|
|
161
|
+
"cursor_path": cursor,
|
|
162
|
+
"mesh": {"gateway": args.gateway},
|
|
163
|
+
}
|
|
164
|
+
if sandbox is not None:
|
|
165
|
+
cell["sandbox"] = sandbox
|
|
166
|
+
if args.starter:
|
|
167
|
+
cell["starter_prompt_path"] = args.starter
|
|
168
|
+
|
|
169
|
+
# ── assisted memory (y/n) ──
|
|
170
|
+
am_repo = args.assisted_memory
|
|
171
|
+
if am_repo is None and interactive and _ask_yn("Use assisted memory (git-backed durable memory)?", False):
|
|
172
|
+
am_repo = _ask(" Memory git repo URL")
|
|
173
|
+
am_note = ""
|
|
174
|
+
if am_repo:
|
|
175
|
+
repo, rewritten = _https_normalize(am_repo)
|
|
176
|
+
cell["assisted_memory"] = {"enabled": True, "repo": repo, "interval_min": 15}
|
|
177
|
+
if rewritten:
|
|
178
|
+
am_note = (f" (repo normalized SSH→HTTPS: {repo}; ensure gh/HTTPS "
|
|
179
|
+
f"creds can reach it — a private repo needing SSH will fail otherwise)")
|
|
180
|
+
|
|
181
|
+
# ── validate BEFORE writing (parse_cell_dict mutates → deepcopy) ──
|
|
182
|
+
try:
|
|
183
|
+
parse_cell_dict(copy.deepcopy(cell))
|
|
184
|
+
except CellError as exc:
|
|
185
|
+
print(f"swarph init: refusing to write an invalid cell.yaml: {exc}", file=sys.stderr)
|
|
186
|
+
return 2
|
|
187
|
+
|
|
188
|
+
dest = cells_dir() / f"{name}.yaml"
|
|
189
|
+
if dest.exists() and not args.force:
|
|
190
|
+
if interactive and _ask_yn(f"{dest} exists — overwrite?", False):
|
|
191
|
+
pass
|
|
192
|
+
else:
|
|
193
|
+
print(f"swarph init: {dest} already exists (use --force)", file=sys.stderr)
|
|
194
|
+
return 2
|
|
195
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
dest.write_text(yaml.safe_dump(cell, sort_keys=False), encoding="utf-8")
|
|
197
|
+
print(f"swarph init: wrote {dest}{am_note}")
|
|
198
|
+
|
|
199
|
+
# ── optional cwd symlink ──
|
|
200
|
+
want_symlink = args.symlink_cwd or (interactive and _ask_yn(
|
|
201
|
+
f"Symlink {cwd}/cell.yaml → registry (so `swarph spawn` works from that dir too)?", False))
|
|
202
|
+
if want_symlink:
|
|
203
|
+
_symlink_cwd(cwd, dest, args.force)
|
|
204
|
+
|
|
205
|
+
# Resolved summary (echo the derived cwd/tmux/cursor — those were the snags).
|
|
206
|
+
print(" resolved:")
|
|
207
|
+
print(f" provider={provider} role={role} sandbox={sandbox if sandbox is not None else '(default)'}")
|
|
208
|
+
print(f" cwd={cwd}")
|
|
209
|
+
print(f" tmux_session={cell['tmux_session']} cursor_path={cell['cursor_path']}")
|
|
210
|
+
if "assisted_memory" in cell:
|
|
211
|
+
print(f" assisted_memory.repo={cell['assisted_memory']['repo']} (enabled)")
|
|
212
|
+
|
|
213
|
+
# Registration is NOT done here (R1 token mint-once is a once-only secret —
|
|
214
|
+
# init scaffolding from the operator's context shouldn't capture another
|
|
215
|
+
# cell's token; the cell self-adopts from ITS OWN context per the adoption
|
|
216
|
+
# doc, which is the forge-clean path). Deferred follow-up: a careful
|
|
217
|
+
# single-process register that captures→mode-600→verifies the raw token.
|
|
218
|
+
print(f"\nready: swarph spawn {name}")
|
|
219
|
+
print(f"next: the cell self-registers + adopts its per-peer token from its "
|
|
220
|
+
f"OWN context (see SWARPH_PEER_TOKEN_ADOPTION.md); then `swarph ratify {name}`.")
|
|
221
|
+
return 0
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _symlink_cwd(cwd: Path, dest: Path, force: bool) -> None:
|
|
225
|
+
link = cwd / "cell.yaml"
|
|
226
|
+
try:
|
|
227
|
+
cwd.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
if link.is_symlink() or link.exists():
|
|
229
|
+
if force:
|
|
230
|
+
link.unlink()
|
|
231
|
+
else:
|
|
232
|
+
print(f"swarph init: {link} exists; skip symlink (use --force)", file=sys.stderr)
|
|
233
|
+
return
|
|
234
|
+
link.symlink_to(dest)
|
|
235
|
+
print(f"swarph init: symlinked {link} → {dest}")
|
|
236
|
+
except OSError as exc:
|
|
237
|
+
print(f"swarph init: symlink failed (non-fatal): {exc}", file=sys.stderr)
|
|
@@ -39,7 +39,7 @@ def _get_files_to_sync(cell: Cell) -> list[tuple[str, Path]]:
|
|
|
39
39
|
if (cell.cwd / "AGENTS.md").exists():
|
|
40
40
|
files_to_sync.append(("AGENTS.md", cell.cwd / "AGENTS.md"))
|
|
41
41
|
|
|
42
|
-
elif cell.provider
|
|
42
|
+
elif cell.provider == "antigravity":
|
|
43
43
|
if (cell.cwd / "GEMINI.md").exists():
|
|
44
44
|
files_to_sync.append(("GEMINI.md", cell.cwd / "GEMINI.md"))
|
|
45
45
|
if (cell.cwd / "inbox-cursor.json").exists():
|
|
@@ -94,6 +94,7 @@ def perform_restore(cell: Cell) -> Optional[str]:
|
|
|
94
94
|
subprocess.run(["git", "-C", str(repo_dir), "pull", "--ff-only", "origin", "main"], check=True, capture_output=True)
|
|
95
95
|
except subprocess.CalledProcessError as exc:
|
|
96
96
|
print(f"swarph: memory pull --ff-only failed (diverged/offline?): {exc}", file=sys.stderr)
|
|
97
|
+
return None
|
|
97
98
|
|
|
98
99
|
# Walk the repo dir and copy everything back, EXCEPT .git and .gitignore
|
|
99
100
|
for root, dirs, files in os.walk(repo_dir):
|
|
@@ -104,6 +105,10 @@ def perform_restore(cell: Cell) -> Optional[str]:
|
|
|
104
105
|
continue
|
|
105
106
|
|
|
106
107
|
src = Path(root) / f
|
|
108
|
+
if f in ("CLAUDE.md", "AGENTS.md", "GEMINI.md") and src.stat().st_size == 0:
|
|
109
|
+
print(f"swarph: SAFETY: remote {f} is empty. Refusing to restore and clobber local.", file=sys.stderr)
|
|
110
|
+
continue
|
|
111
|
+
|
|
107
112
|
rel = src.relative_to(repo_dir)
|
|
108
113
|
|
|
109
114
|
dest = None
|
|
@@ -115,9 +120,9 @@ def perform_restore(cell: Cell) -> Optional[str]:
|
|
|
115
120
|
dest = Path.home() / ".claude" / rel
|
|
116
121
|
elif cell.provider == "claude" and rel.parts[0] == "inbox-cursor":
|
|
117
122
|
dest = Path.home() / ".claude" / "inbox-cursor"
|
|
118
|
-
elif cell.provider
|
|
123
|
+
elif cell.provider == "antigravity" and rel.parts[0] == "tmp":
|
|
119
124
|
dest = Path.home() / ".gemini" / rel
|
|
120
|
-
elif cell.provider
|
|
125
|
+
elif cell.provider == "antigravity" and rel.parts[0] == "history":
|
|
121
126
|
dest = Path.home() / ".gemini" / rel
|
|
122
127
|
|
|
123
128
|
if dest:
|
|
@@ -168,7 +173,7 @@ def run_memory_sync(argv: list[str]) -> int:
|
|
|
168
173
|
guard_file = cell.cwd / "CLAUDE.md"
|
|
169
174
|
elif cell.provider == "codex":
|
|
170
175
|
guard_file = cell.cwd / "AGENTS.md"
|
|
171
|
-
elif cell.provider
|
|
176
|
+
elif cell.provider == "antigravity":
|
|
172
177
|
guard_file = cell.cwd / "GEMINI.md"
|
|
173
178
|
|
|
174
179
|
if guard_file and (not guard_file.exists() or guard_file.stat().st_size == 0):
|
|
@@ -241,7 +241,8 @@ def _run_send(args: argparse.Namespace) -> int:
|
|
|
241
241
|
token,
|
|
242
242
|
)
|
|
243
243
|
if status < 200 or status >= 300:
|
|
244
|
-
|
|
244
|
+
detail = payload.get("detail", "<gateway error>")
|
|
245
|
+
print(f"swarph mesh send: gateway {status}: {detail}", file=sys.stderr)
|
|
245
246
|
return 1
|
|
246
247
|
print(
|
|
247
248
|
f"sent id={payload.get('id')} from={payload.get('from_node')} "
|
|
@@ -259,7 +260,8 @@ def _run_inbox(args: argparse.Namespace) -> int:
|
|
|
259
260
|
url = f"{args.gateway.rstrip('/')}/messages?{urllib.parse.urlencode(params)}"
|
|
260
261
|
status, payload = _http_get_json(url, token)
|
|
261
262
|
if status < 200 or status >= 300:
|
|
262
|
-
|
|
263
|
+
detail = payload.get("detail", "<gateway error>")
|
|
264
|
+
print(f"swarph mesh inbox: gateway {status}: {detail}", file=sys.stderr)
|
|
263
265
|
return 1
|
|
264
266
|
if args.json:
|
|
265
267
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
@@ -304,7 +306,8 @@ def _run_register(args: argparse.Namespace) -> int:
|
|
|
304
306
|
token,
|
|
305
307
|
)
|
|
306
308
|
if status < 200 or status >= 300:
|
|
307
|
-
|
|
309
|
+
detail = payload.get("detail", "<gateway error>")
|
|
310
|
+
print(f"swarph mesh register: gateway {status}: {detail}", file=sys.stderr)
|
|
308
311
|
return 1
|
|
309
312
|
peer_token = payload.get("peer_token")
|
|
310
313
|
token_status = payload.get("token_status")
|
|
@@ -329,7 +332,20 @@ def _default_sidecar_state_dir(self_name: str) -> Path:
|
|
|
329
332
|
def _read_cursor(path: Path) -> dict:
|
|
330
333
|
if not path.exists():
|
|
331
334
|
return {"last_msg_id": 0, "last_wake_at": 0.0}
|
|
332
|
-
|
|
335
|
+
try:
|
|
336
|
+
cursor = json.loads(path.read_text(encoding="utf-8"))
|
|
337
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
338
|
+
print(
|
|
339
|
+
f"[mesh-sidecar] ignoring unreadable cursor {path}: {exc}",
|
|
340
|
+
file=sys.stderr,
|
|
341
|
+
flush=True,
|
|
342
|
+
)
|
|
343
|
+
return {"last_msg_id": 0, "last_wake_at": 0.0}
|
|
344
|
+
if not isinstance(cursor, dict):
|
|
345
|
+
return {"last_msg_id": 0, "last_wake_at": 0.0}
|
|
346
|
+
cursor.setdefault("last_msg_id", 0)
|
|
347
|
+
cursor.setdefault("last_wake_at", 0.0)
|
|
348
|
+
return cursor
|
|
333
349
|
|
|
334
350
|
|
|
335
351
|
def _write_cursor_atomic(path: Path, cursor: dict) -> None:
|
|
@@ -447,12 +463,11 @@ def _sidecar_iteration(state: MeshSidecarState) -> None:
|
|
|
447
463
|
if _tmux_wake(state.tmux_target):
|
|
448
464
|
state.cursor["last_wake_at"] = now
|
|
449
465
|
state.wakes_sent += 1
|
|
466
|
+
state.cursor["last_msg_id"] = new_last_id
|
|
467
|
+
_write_cursor_atomic(state.cursor_path, state.cursor)
|
|
450
468
|
else:
|
|
451
469
|
print("[mesh-sidecar] wake suppressed by idle guard", flush=True)
|
|
452
470
|
|
|
453
|
-
state.cursor["last_msg_id"] = new_last_id
|
|
454
|
-
_write_cursor_atomic(state.cursor_path, state.cursor)
|
|
455
|
-
|
|
456
471
|
|
|
457
472
|
def _run_sidecar(args: argparse.Namespace) -> int:
|
|
458
473
|
state_dir_arg = Path(args.state_dir).expanduser() if args.state_dir else None
|
|
@@ -941,8 +941,16 @@ def run_spawn(argv: Optional[list[str]] = None) -> int:
|
|
|
941
941
|
inject_text = f"Your active task is in CURRENT_TASK.md — read it first:\n\n{current_task_text}"
|
|
942
942
|
if cell.provider == "claude":
|
|
943
943
|
spawn_argv.extend(["--append-system-prompt", inject_text])
|
|
944
|
-
elif cell.provider
|
|
944
|
+
elif cell.provider == "antigravity":
|
|
945
945
|
spawn_argv.extend(["--prompt-interactive", inject_text])
|
|
946
|
+
elif cell.provider == "codex":
|
|
947
|
+
agents_md = cell.cwd / "AGENTS.md"
|
|
948
|
+
if agents_md.exists():
|
|
949
|
+
content = agents_md.read_text(encoding="utf-8")
|
|
950
|
+
if "CURRENT_TASK.md" not in content:
|
|
951
|
+
agents_md.write_text(inject_text + "\n\n" + content, encoding="utf-8")
|
|
952
|
+
else:
|
|
953
|
+
agents_md.write_text(inject_text, encoding="utf-8")
|
|
946
954
|
except Exception as exc:
|
|
947
955
|
print(f"swarph spawn: restore failed: {exc}", file=sys.stderr)
|
|
948
956
|
|
|
@@ -72,6 +72,7 @@ Spec: https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/P
|
|
|
72
72
|
# only for disambiguation against one-shot prompts (rare).
|
|
73
73
|
_VERB_HANDLERS: dict[str, str] = {
|
|
74
74
|
# verb keyword: dotted-path to handler function (lazy-imported)
|
|
75
|
+
"init": "swarph_cli.commands.init.run_init",
|
|
75
76
|
"import": "swarph_cli.commands.import_session.run_import",
|
|
76
77
|
"chat": "swarph_cli.commands.chat.run_chat",
|
|
77
78
|
"onboard": "swarph_cli.commands.onboard.run_onboard",
|
|
@@ -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),
|
|
3
|
+
Version: 0.9.4
|
|
4
|
+
Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.4: fixes session-pin resume across a changed cwd.
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/darw007d/swarph-cli
|
|
@@ -16,6 +16,7 @@ src/swarph_cli/commands/chat.py
|
|
|
16
16
|
src/swarph_cli/commands/daemon.py
|
|
17
17
|
src/swarph_cli/commands/hook_output.py
|
|
18
18
|
src/swarph_cli/commands/import_session.py
|
|
19
|
+
src/swarph_cli/commands/init.py
|
|
19
20
|
src/swarph_cli/commands/install_hook.py
|
|
20
21
|
src/swarph_cli/commands/memory_sync.py
|
|
21
22
|
src/swarph_cli/commands/mesh.py
|
|
@@ -34,6 +35,7 @@ tests/test_claude_parser.py
|
|
|
34
35
|
tests/test_daemon_command.py
|
|
35
36
|
tests/test_hook_output.py
|
|
36
37
|
tests/test_import_command.py
|
|
38
|
+
tests/test_init_command.py
|
|
37
39
|
tests/test_install_hook.py
|
|
38
40
|
tests/test_main.py
|
|
39
41
|
tests/test_memory_sync.py
|
|
@@ -22,6 +22,8 @@ from swarph_cli.cell import (
|
|
|
22
22
|
load_or_create_session_id,
|
|
23
23
|
resolve_cell_path,
|
|
24
24
|
session_state_path,
|
|
25
|
+
_read_session_sidecar,
|
|
26
|
+
_write_session_sidecar,
|
|
25
27
|
)
|
|
26
28
|
|
|
27
29
|
|
|
@@ -264,7 +266,9 @@ def test_load_or_create_session_id_mints_and_persists(
|
|
|
264
266
|
uuid.UUID(sid1) # raises if not a valid UUID
|
|
265
267
|
sidecar = session_state_path("lab")
|
|
266
268
|
assert sidecar.exists()
|
|
267
|
-
|
|
269
|
+
rec_uuid, rec_cwd = _read_session_sidecar(sidecar)
|
|
270
|
+
assert rec_uuid == sid1
|
|
271
|
+
assert rec_cwd == str(cell.cwd)
|
|
268
272
|
|
|
269
273
|
|
|
270
274
|
def test_load_or_create_session_id_reuses_persisted_value(
|
|
@@ -287,7 +291,102 @@ def test_load_or_create_session_id_corrupted_sidecar_regenerates(
|
|
|
287
291
|
sid, generated, _role = load_or_create_session_id("lab", cell)
|
|
288
292
|
uuid.UUID(sid)
|
|
289
293
|
assert generated is True
|
|
290
|
-
|
|
294
|
+
rec_uuid, _rec_cwd = _read_session_sidecar(sidecar)
|
|
295
|
+
assert rec_uuid == sid
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ---------------------------------------------------------------------------
|
|
299
|
+
# v0.9.4 — session-pin cwd-mismatch fix (project-scoped --resume)
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_session_sidecar_fresh_mint_writes_two_line_uuid_and_cwd(
|
|
304
|
+
isolated_xdg, cell_yaml_factory, tmp_path
|
|
305
|
+
):
|
|
306
|
+
"""v0.9.4 — first mint records BOTH the uuid AND the cwd (cwd=A)."""
|
|
307
|
+
cell = load_cell(cell_yaml_factory()) # cwd == tmp_path (== A)
|
|
308
|
+
sid, gen, _role = load_or_create_session_id("lab", cell)
|
|
309
|
+
assert gen is True
|
|
310
|
+
uuid.UUID(sid)
|
|
311
|
+
rec_uuid, rec_cwd = _read_session_sidecar(session_state_path("lab"))
|
|
312
|
+
assert rec_uuid == sid
|
|
313
|
+
assert rec_cwd == str(tmp_path)
|
|
314
|
+
assert rec_cwd == str(cell.cwd)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_session_sidecar_same_cwd_reuses_same_uuid(
|
|
318
|
+
isolated_xdg, cell_yaml_factory, tmp_path
|
|
319
|
+
):
|
|
320
|
+
"""v0.9.4 — pre-pinned {uuid=U, cwd=A}; spawn from cwd=A reuses U."""
|
|
321
|
+
cell = load_cell(cell_yaml_factory()) # cwd == tmp_path (== A)
|
|
322
|
+
fixed = "550e8400-e29b-41d4-a716-446655440000"
|
|
323
|
+
_write_session_sidecar(session_state_path("lab"), fixed, str(tmp_path))
|
|
324
|
+
sid, gen, role = load_or_create_session_id("lab", cell)
|
|
325
|
+
assert sid == fixed
|
|
326
|
+
assert gen is False
|
|
327
|
+
assert role == "lab"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def test_session_sidecar_different_cwd_mints_fresh_and_repins(
|
|
331
|
+
isolated_xdg, cell_yaml_factory, tmp_path
|
|
332
|
+
):
|
|
333
|
+
"""v0.9.4 REGRESSION — the bug: pin minted for cwd=A, spawn from cwd=B.
|
|
334
|
+
|
|
335
|
+
A project-scoped ``claude --resume <U>`` from B dies with
|
|
336
|
+
"No conversation found". Fix: mint a FRESH (clean) uuid and re-pin
|
|
337
|
+
it for cwd=B.
|
|
338
|
+
"""
|
|
339
|
+
cwd_b = tmp_path / "project_b"
|
|
340
|
+
cwd_b.mkdir()
|
|
341
|
+
cell = load_cell(cell_yaml_factory(cwd=str(cwd_b))) # cell.cwd == B
|
|
342
|
+
fixed = "550e8400-e29b-41d4-a716-446655440000" # pinned for cwd=A
|
|
343
|
+
_write_session_sidecar(session_state_path("lab"), fixed, str(tmp_path))
|
|
344
|
+
|
|
345
|
+
sid, gen, role = load_or_create_session_id("lab", cell)
|
|
346
|
+
assert gen is True
|
|
347
|
+
assert sid != fixed # genuine fresh UUID
|
|
348
|
+
uuid.UUID(sid)
|
|
349
|
+
assert role == "lab"
|
|
350
|
+
# Sidecar now re-pinned for cwd=B with the fresh UUID.
|
|
351
|
+
rec_uuid, rec_cwd = _read_session_sidecar(session_state_path("lab"))
|
|
352
|
+
assert rec_uuid == sid
|
|
353
|
+
assert rec_cwd == str(cwd_b)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def test_session_sidecar_legacy_one_line_is_reused_not_reminted(
|
|
357
|
+
isolated_xdg, cell_yaml_factory, tmp_path
|
|
358
|
+
):
|
|
359
|
+
"""v0.9.4 back-compat — legacy bare-uuid (no cwd) sidecar is reused
|
|
360
|
+
as-is regardless of cwd (origin cwd unknown; do NOT re-mint)."""
|
|
361
|
+
cell = load_cell(cell_yaml_factory())
|
|
362
|
+
fixed = "550e8400-e29b-41d4-a716-446655440000"
|
|
363
|
+
sidecar = session_state_path("lab")
|
|
364
|
+
sidecar.parent.mkdir(parents=True, exist_ok=True)
|
|
365
|
+
sidecar.write_text(fixed + "\n") # legacy one-line format
|
|
366
|
+
sid, gen, role = load_or_create_session_id("lab", cell)
|
|
367
|
+
assert sid == fixed
|
|
368
|
+
assert gen is False
|
|
369
|
+
assert role == "lab"
|
|
370
|
+
# Not upgraded/rewritten — stays legacy one-line.
|
|
371
|
+
assert sidecar.read_text() == fixed + "\n"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_read_session_sidecar_parses_both_formats(tmp_path):
|
|
375
|
+
"""v0.9.4 — _read_session_sidecar handles 2-line AND legacy 1-line."""
|
|
376
|
+
two_line = tmp_path / "two.session-id"
|
|
377
|
+
two_line.write_text("550e8400-e29b-41d4-a716-446655440000\n/home/x/proj\n")
|
|
378
|
+
assert _read_session_sidecar(two_line) == (
|
|
379
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
380
|
+
"/home/x/proj",
|
|
381
|
+
)
|
|
382
|
+
one_line = tmp_path / "one.session-id"
|
|
383
|
+
one_line.write_text("550e8400-e29b-41d4-a716-446655440000\n")
|
|
384
|
+
assert _read_session_sidecar(one_line) == (
|
|
385
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
386
|
+
None,
|
|
387
|
+
)
|
|
388
|
+
missing = tmp_path / "missing.session-id"
|
|
389
|
+
assert _read_session_sidecar(missing) == (None, None)
|
|
291
390
|
|
|
292
391
|
|
|
293
392
|
def test_load_or_create_session_id_atomic_no_tempfile_left_behind(
|
|
@@ -435,8 +534,8 @@ def test_new_instance_with_existing_base_mints_into_slot_2(
|
|
|
435
534
|
base_path = session_state_path("drop")
|
|
436
535
|
sib_path = session_state_path("drop-2")
|
|
437
536
|
assert base_path.exists() and sib_path.exists()
|
|
438
|
-
assert base_path
|
|
439
|
-
assert sib_path
|
|
537
|
+
assert _read_session_sidecar(base_path)[0] == sid_base
|
|
538
|
+
assert _read_session_sidecar(sib_path)[0] == sid_sib
|
|
440
539
|
|
|
441
540
|
|
|
442
541
|
def test_new_instance_third_call_lands_at_slot_3(
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Tests for `swarph init` (cell.yaml scaffolder). Non-interactive (-y / flag)
|
|
2
|
+
path + helpers; cells_dir() isolated via XDG_CONFIG_HOME."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from swarph_cli.commands.init import run_init, _https_normalize
|
|
10
|
+
from swarph_cli.cell import cells_dir, load_cell
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture(autouse=True)
|
|
14
|
+
def isolated_cells(tmp_path, monkeypatch):
|
|
15
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))
|
|
16
|
+
return tmp_path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _run(*argv):
|
|
20
|
+
return run_init(list(argv))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_https_normalize():
|
|
24
|
+
assert _https_normalize("git@github.com:darw007d/x.git") == ("https://github.com/darw007d/x.git", True)
|
|
25
|
+
assert _https_normalize("https://github.com/darw007d/x.git") == ("https://github.com/darw007d/x.git", False)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_init_writes_validated_cell(tmp_path):
|
|
29
|
+
rc = _run("gpt-test", "--provider", "codex", "--cwd", str(tmp_path), "-y")
|
|
30
|
+
assert rc == 0
|
|
31
|
+
dest = cells_dir() / "gpt-test.yaml"
|
|
32
|
+
assert dest.exists()
|
|
33
|
+
c = load_cell(dest)
|
|
34
|
+
assert c.name == "gpt-test" and c.provider == "codex"
|
|
35
|
+
assert c.role == "gpt-test" # default = name
|
|
36
|
+
assert c.sandbox == "workspace-write" # codex default
|
|
37
|
+
assert c.extra["tmux_session"] == "gpt-test" # default = name
|
|
38
|
+
assert c.extra["cursor_path"] == "/tmp/gpt-test-cursor.json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_init_rejects_underscore_name(tmp_path):
|
|
42
|
+
assert _run("bad_name", "--provider", "codex", "--cwd", str(tmp_path), "-y") == 2
|
|
43
|
+
assert not (cells_dir() / "bad_name.yaml").exists()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_init_requires_provider_when_noninteractive(tmp_path):
|
|
47
|
+
assert _run("c1", "--cwd", str(tmp_path), "-y") == 2
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_init_refuses_existing_without_force(tmp_path):
|
|
51
|
+
assert _run("dup", "--provider", "codex", "--cwd", str(tmp_path), "-y") == 0
|
|
52
|
+
assert _run("dup", "--provider", "codex", "--cwd", str(tmp_path), "-y") == 2 # exists
|
|
53
|
+
assert _run("dup", "--provider", "codex", "--cwd", str(tmp_path), "-y", "--force") == 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_init_assisted_memory_https_normalized(tmp_path):
|
|
57
|
+
rc = _run("mem", "--provider", "antigravity", "--cwd", str(tmp_path), "-y",
|
|
58
|
+
"--assisted-memory", "git@github.com:darw007d/mem.git")
|
|
59
|
+
assert rc == 0
|
|
60
|
+
c = load_cell(cells_dir() / "mem.yaml")
|
|
61
|
+
assert c.assisted_memory["enabled"] is True
|
|
62
|
+
assert c.assisted_memory["repo"] == "https://github.com/darw007d/mem.git"
|
|
63
|
+
assert c.assisted_memory["interval_min"] == 15
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_init_antigravity_omits_sandbox(tmp_path):
|
|
67
|
+
rc = _run("agy-cell", "--provider", "antigravity", "--cwd", str(tmp_path), "-y")
|
|
68
|
+
assert rc == 0
|
|
69
|
+
c = load_cell(cells_dir() / "agy-cell.yaml")
|
|
70
|
+
assert c.sandbox is None # default-on, not written
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_init_codex_bad_sandbox_rejected(tmp_path):
|
|
74
|
+
assert _run("c", "--provider", "codex", "--sandbox", "bogus", "--cwd", str(tmp_path), "-y") == 2
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_init_symlink_cwd(tmp_path):
|
|
78
|
+
cwd = tmp_path / "cellwd"
|
|
79
|
+
rc = _run("linked", "--provider", "codex", "--cwd", str(cwd), "-y", "--symlink-cwd")
|
|
80
|
+
assert rc == 0
|
|
81
|
+
link = cwd / "cell.yaml"
|
|
82
|
+
assert link.is_symlink()
|
|
83
|
+
assert link.resolve() == (cells_dir() / "linked.yaml").resolve()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import subprocess
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from swarph_cli.commands.memory_sync import run_memory_sync, perform_restore
|
|
6
|
+
from swarph_cli.cell import Cell
|
|
7
|
+
|
|
8
|
+
def test_run_memory_sync_no_op_when_not_enabled(tmp_path):
|
|
9
|
+
cell = Cell(
|
|
10
|
+
name="test",
|
|
11
|
+
role="test",
|
|
12
|
+
cwd=tmp_path,
|
|
13
|
+
schema_version="v1",
|
|
14
|
+
session_id=None,
|
|
15
|
+
starter_prompt_path=None,
|
|
16
|
+
provider="claude",
|
|
17
|
+
sandbox=None,
|
|
18
|
+
lineage=None,
|
|
19
|
+
source_path=None,
|
|
20
|
+
assisted_memory={"enabled": False, "interval_min": 15},
|
|
21
|
+
extra={},
|
|
22
|
+
)
|
|
23
|
+
with patch("swarph_cli.commands.memory_sync.load_cell", return_value=cell):
|
|
24
|
+
assert run_memory_sync(["fake.yaml"]) == 0
|
|
25
|
+
|
|
26
|
+
def test_perform_restore_returns_none_when_not_enabled(tmp_path):
|
|
27
|
+
cell = Cell(
|
|
28
|
+
name="test",
|
|
29
|
+
role="test",
|
|
30
|
+
cwd=tmp_path,
|
|
31
|
+
schema_version="v1",
|
|
32
|
+
session_id=None,
|
|
33
|
+
starter_prompt_path=None,
|
|
34
|
+
provider="claude",
|
|
35
|
+
sandbox=None,
|
|
36
|
+
lineage=None,
|
|
37
|
+
source_path=None,
|
|
38
|
+
assisted_memory={"enabled": False, "interval_min": 15},
|
|
39
|
+
extra={},
|
|
40
|
+
)
|
|
41
|
+
assert perform_restore(cell) is None
|
|
42
|
+
|
|
43
|
+
def test_perform_restore_absent(tmp_path):
|
|
44
|
+
cell = Cell(
|
|
45
|
+
name="test",
|
|
46
|
+
role="test",
|
|
47
|
+
cwd=tmp_path,
|
|
48
|
+
schema_version="v1",
|
|
49
|
+
session_id=None,
|
|
50
|
+
starter_prompt_path=None,
|
|
51
|
+
provider="claude",
|
|
52
|
+
sandbox=None,
|
|
53
|
+
lineage=None,
|
|
54
|
+
source_path=None,
|
|
55
|
+
assisted_memory=None,
|
|
56
|
+
extra={},
|
|
57
|
+
)
|
|
58
|
+
assert perform_restore(cell) is None
|
|
59
|
+
|
|
60
|
+
def test_perform_restore_returns_none_on_pull_failure(tmp_path):
|
|
61
|
+
cell = Cell(
|
|
62
|
+
name="test", role="test", cwd=tmp_path, schema_version="v1", session_id=None,
|
|
63
|
+
starter_prompt_path=None, provider="claude", sandbox=None, lineage=None,
|
|
64
|
+
source_path=None, assisted_memory={"enabled": True, "repo": "test/repo"}, extra={},
|
|
65
|
+
)
|
|
66
|
+
with patch("swarph_cli.commands.memory_sync.get_memory_repo_path", return_value=tmp_path):
|
|
67
|
+
with patch("swarph_cli.commands.memory_sync._clone_if_missing", return_value=True):
|
|
68
|
+
with patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "git pull")):
|
|
69
|
+
assert perform_restore(cell) is None
|
|
70
|
+
|
|
71
|
+
def test_perform_restore_empty_guard(tmp_path):
|
|
72
|
+
repo_dir = tmp_path / "repo"
|
|
73
|
+
repo_dir.mkdir()
|
|
74
|
+
(repo_dir / "CLAUDE.md").write_text("")
|
|
75
|
+
(repo_dir / "CURRENT_TASK.md").write_text("Hello")
|
|
76
|
+
|
|
77
|
+
cell_dir = tmp_path / "cell"
|
|
78
|
+
cell_dir.mkdir()
|
|
79
|
+
(cell_dir / "CLAUDE.md").write_text("Old contents")
|
|
80
|
+
|
|
81
|
+
cell = Cell(
|
|
82
|
+
name="test", role="test", cwd=cell_dir, schema_version="v1", session_id=None,
|
|
83
|
+
starter_prompt_path=None, provider="claude", sandbox=None, lineage=None,
|
|
84
|
+
source_path=None, assisted_memory={"enabled": True, "repo": "test/repo"}, extra={},
|
|
85
|
+
)
|
|
86
|
+
with patch("swarph_cli.commands.memory_sync.get_memory_repo_path", return_value=repo_dir):
|
|
87
|
+
with patch("swarph_cli.commands.memory_sync._clone_if_missing", return_value=True):
|
|
88
|
+
with patch("subprocess.run"):
|
|
89
|
+
res = perform_restore(cell)
|
|
90
|
+
assert res == "Hello"
|
|
91
|
+
assert (cell_dir / "CLAUDE.md").read_text() == "Old contents"
|
|
92
|
+
assert (cell_dir / "CURRENT_TASK.md").read_text() == "Hello"
|
|
@@ -68,6 +68,27 @@ def test_mesh_send_gateway_error_is_nonsecret(monkeypatch, capsys):
|
|
|
68
68
|
assert "secret-token" not in err
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
def test_mesh_gateway_error_without_detail_does_not_echo_payload(monkeypatch, capsys):
|
|
72
|
+
monkeypatch.setenv("SWARPH_SELF", "gpt-ops")
|
|
73
|
+
monkeypatch.setenv("MESH_GATEWAY_TOKEN", "secret-token")
|
|
74
|
+
monkeypatch.setattr(
|
|
75
|
+
mesh,
|
|
76
|
+
"_post_json",
|
|
77
|
+
lambda url, body, token, *, timeout=10.0: (
|
|
78
|
+
403,
|
|
79
|
+
{"authorization": "secret-token", "body": {"peer_token": "minted-secret"}},
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
rc = mesh.run_mesh(["send", "lab-ovh", "--kind", "answer", "--content", "x"])
|
|
84
|
+
|
|
85
|
+
assert rc == 1
|
|
86
|
+
err = capsys.readouterr().err
|
|
87
|
+
assert "<gateway error>" in err
|
|
88
|
+
assert "secret-token" not in err
|
|
89
|
+
assert "minted-secret" not in err
|
|
90
|
+
|
|
91
|
+
|
|
71
92
|
def test_mesh_inbox_uses_to_query_and_unread(monkeypatch, capsys):
|
|
72
93
|
monkeypatch.setenv("SWARPH_SELF", "gpt-ops")
|
|
73
94
|
monkeypatch.setenv("MESH_GATEWAY_TOKEN", "tok")
|
|
@@ -116,7 +116,7 @@ def test_sidecar_filters_old_and_self_messages(tmp_path, monkeypatch):
|
|
|
116
116
|
assert json.loads(log_lines[0])["id"] == 12
|
|
117
117
|
|
|
118
118
|
|
|
119
|
-
def
|
|
119
|
+
def test_sidecar_idle_guard_suppresses_wake_without_advancing_cursor(
|
|
120
120
|
tmp_path, monkeypatch, capsys
|
|
121
121
|
):
|
|
122
122
|
state = _state(tmp_path)
|
|
@@ -132,12 +132,51 @@ def test_sidecar_idle_guard_suppresses_repeated_wake_but_advances_cursor(
|
|
|
132
132
|
monkeypatch.setattr(mesh, "_tmux_wake", lambda target: (_ for _ in ()).throw(AssertionError("guarded")))
|
|
133
133
|
monkeypatch.setattr(mesh.time, "time", lambda: 1000.0)
|
|
134
134
|
mesh._sidecar_iteration(state)
|
|
135
|
-
assert state.cursor["last_msg_id"] ==
|
|
135
|
+
assert state.cursor["last_msg_id"] == 0
|
|
136
136
|
assert state.cursor["last_wake_at"] == 995.0
|
|
137
137
|
assert state.wakes_sent == 0
|
|
138
|
+
assert not (tmp_path / "cursor.json").exists()
|
|
138
139
|
assert "wake suppressed" in capsys.readouterr().out
|
|
139
140
|
|
|
140
141
|
|
|
142
|
+
def test_sidecar_wakes_throttled_message_after_guard_window(tmp_path, monkeypatch):
|
|
143
|
+
state = _state(tmp_path)
|
|
144
|
+
state.cursor["last_wake_at"] = 995.0
|
|
145
|
+
now = 1000.0
|
|
146
|
+
|
|
147
|
+
monkeypatch.setattr(
|
|
148
|
+
mesh,
|
|
149
|
+
"_http_get_json",
|
|
150
|
+
lambda url, token, *, timeout=10.0: (
|
|
151
|
+
200,
|
|
152
|
+
{"messages": [{"id": 1, "from_node": "lab-ovh", "content": "new"}]},
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
wakes = []
|
|
156
|
+
monkeypatch.setattr(mesh, "_tmux_wake", lambda target: wakes.append(target) or True)
|
|
157
|
+
monkeypatch.setattr(mesh.time, "time", lambda: now)
|
|
158
|
+
|
|
159
|
+
mesh._sidecar_iteration(state)
|
|
160
|
+
assert wakes == []
|
|
161
|
+
assert state.cursor["last_msg_id"] == 0
|
|
162
|
+
|
|
163
|
+
now = 1060.0
|
|
164
|
+
mesh._sidecar_iteration(state)
|
|
165
|
+
assert wakes == ["gpt-ops-pane"]
|
|
166
|
+
assert state.cursor["last_msg_id"] == 1
|
|
167
|
+
assert state.cursor["last_wake_at"] == 1060.0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_sidecar_corrupt_cursor_defaults_without_crashing(tmp_path, capsys):
|
|
171
|
+
(tmp_path / "cursor.json").write_text("{", encoding="utf-8")
|
|
172
|
+
|
|
173
|
+
state = _state(tmp_path)
|
|
174
|
+
|
|
175
|
+
assert state.cursor["last_msg_id"] == 0
|
|
176
|
+
assert state.cursor["last_wake_at"] == 0.0
|
|
177
|
+
assert "ignoring unreadable cursor" in capsys.readouterr().err
|
|
178
|
+
|
|
179
|
+
|
|
141
180
|
def test_sidecar_network_failure_records_disconnect(tmp_path, monkeypatch):
|
|
142
181
|
state = _state(tmp_path)
|
|
143
182
|
monkeypatch.setattr(
|
|
@@ -459,7 +459,7 @@ def test_run_spawn_new_instance_with_base_mints_sibling_slot(
|
|
|
459
459
|
):
|
|
460
460
|
"""v0.7 PR-B — `--new-instance` with base sidecar present allocates
|
|
461
461
|
slot 2 AND persists. Sibling resumable via `swarph spawn <role>-2`."""
|
|
462
|
-
from swarph_cli.cell import session_state_path
|
|
462
|
+
from swarph_cli.cell import session_state_path, _read_session_sidecar
|
|
463
463
|
|
|
464
464
|
# First spawn establishes base slot
|
|
465
465
|
run_spawn(argv=[str(fake_cell_yaml), "--dry-run", "--print-id"])
|
|
@@ -474,8 +474,8 @@ def test_run_spawn_new_instance_with_base_mints_sibling_slot(
|
|
|
474
474
|
|
|
475
475
|
base_sidecar = session_state_path("lab-test")
|
|
476
476
|
sibling_sidecar = session_state_path("lab-test-2")
|
|
477
|
-
assert base_sidecar
|
|
478
|
-
assert sibling_sidecar
|
|
477
|
+
assert _read_session_sidecar(base_sidecar)[0] == base_uuid
|
|
478
|
+
assert _read_session_sidecar(sibling_sidecar)[0] == sibling_uuid
|
|
479
479
|
|
|
480
480
|
# Dry-run output shows the sibling slot label
|
|
481
481
|
assert "lab-test-2" in captured.err
|
|
@@ -970,3 +970,45 @@ def test_run_spawn_antigravity_dry_run(tmp_path, isolated_xdg, capsys):
|
|
|
970
970
|
captured = capsys.readouterr()
|
|
971
971
|
assert "agy --sandbox --add-dir" in captured.out
|
|
972
972
|
assert "provider: antigravity" in captured.err
|
|
973
|
+
|
|
974
|
+
def test_run_spawn_codex_assisted_memory_injects_agents_md(tmp_path, isolated_xdg, capsys, monkeypatch):
|
|
975
|
+
from swarph_cli.commands.spawn import run_spawn
|
|
976
|
+
import yaml
|
|
977
|
+
|
|
978
|
+
payload = {
|
|
979
|
+
"schema_version": "v1",
|
|
980
|
+
"name": "codex-test",
|
|
981
|
+
"role": "codex-test",
|
|
982
|
+
"cwd": str(tmp_path),
|
|
983
|
+
"provider": "codex",
|
|
984
|
+
"assisted_memory": {
|
|
985
|
+
"enabled": True,
|
|
986
|
+
"repo": "test-repo"
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
p = tmp_path / "cell.yaml"
|
|
990
|
+
p.write_text(yaml.safe_dump(payload), encoding="utf-8")
|
|
991
|
+
|
|
992
|
+
import os
|
|
993
|
+
os.chdir(tmp_path)
|
|
994
|
+
|
|
995
|
+
import swarph_cli.commands.memory_sync
|
|
996
|
+
monkeypatch.setattr(swarph_cli.commands.memory_sync, "perform_restore", lambda c: "Restore Task text")
|
|
997
|
+
monkeypatch.setattr("shutil.which", lambda name: "/bin/fake-codex")
|
|
998
|
+
|
|
999
|
+
exec_args = []
|
|
1000
|
+
def fake_execve(path, argv, env):
|
|
1001
|
+
exec_args.append((path, argv, env))
|
|
1002
|
+
monkeypatch.setattr("os.execve", fake_execve)
|
|
1003
|
+
|
|
1004
|
+
run_spawn([])
|
|
1005
|
+
|
|
1006
|
+
agents_md = tmp_path / "AGENTS.md"
|
|
1007
|
+
assert agents_md.exists()
|
|
1008
|
+
content = agents_md.read_text(encoding="utf-8")
|
|
1009
|
+
assert "Your active task is in CURRENT_TASK.md" in content
|
|
1010
|
+
assert "Restore Task text" in content
|
|
1011
|
+
|
|
1012
|
+
assert len(exec_args) == 1
|
|
1013
|
+
argv = exec_args[0][1]
|
|
1014
|
+
assert "--prompt-interactive" not in argv
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from unittest.mock import patch, MagicMock
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from swarph_cli.commands.memory_sync import run_memory_sync, perform_restore
|
|
5
|
-
from swarph_cli.cell import Cell
|
|
6
|
-
|
|
7
|
-
def test_run_memory_sync_no_op_when_not_enabled(tmp_path):
|
|
8
|
-
cell = Cell(
|
|
9
|
-
name="test",
|
|
10
|
-
role="test",
|
|
11
|
-
cwd=tmp_path,
|
|
12
|
-
schema_version="v1",
|
|
13
|
-
session_id=None,
|
|
14
|
-
starter_prompt_path=None,
|
|
15
|
-
provider="claude",
|
|
16
|
-
sandbox=None,
|
|
17
|
-
lineage=None,
|
|
18
|
-
source_path=None,
|
|
19
|
-
assisted_memory={"enabled": False, "interval_min": 15},
|
|
20
|
-
extra={},
|
|
21
|
-
)
|
|
22
|
-
with patch("swarph_cli.commands.memory_sync.load_cell", return_value=cell):
|
|
23
|
-
assert run_memory_sync(["fake.yaml"]) == 0
|
|
24
|
-
|
|
25
|
-
def test_perform_restore_returns_none_when_not_enabled(tmp_path):
|
|
26
|
-
cell = Cell(
|
|
27
|
-
name="test",
|
|
28
|
-
role="test",
|
|
29
|
-
cwd=tmp_path,
|
|
30
|
-
schema_version="v1",
|
|
31
|
-
session_id=None,
|
|
32
|
-
starter_prompt_path=None,
|
|
33
|
-
provider="claude",
|
|
34
|
-
sandbox=None,
|
|
35
|
-
lineage=None,
|
|
36
|
-
source_path=None,
|
|
37
|
-
assisted_memory={"enabled": False, "interval_min": 15},
|
|
38
|
-
extra={},
|
|
39
|
-
)
|
|
40
|
-
assert perform_restore(cell) is None
|
|
41
|
-
|
|
42
|
-
def test_perform_restore_absent(tmp_path):
|
|
43
|
-
cell = Cell(
|
|
44
|
-
name="test",
|
|
45
|
-
role="test",
|
|
46
|
-
cwd=tmp_path,
|
|
47
|
-
schema_version="v1",
|
|
48
|
-
session_id=None,
|
|
49
|
-
starter_prompt_path=None,
|
|
50
|
-
provider="claude",
|
|
51
|
-
sandbox=None,
|
|
52
|
-
lineage=None,
|
|
53
|
-
source_path=None,
|
|
54
|
-
assisted_memory=None,
|
|
55
|
-
extra={},
|
|
56
|
-
)
|
|
57
|
-
assert perform_restore(cell) is None
|
|
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
|