swarph-cli 0.7.6__tar.gz → 0.7.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {swarph_cli-0.7.6/src/swarph_cli.egg-info → swarph_cli-0.7.8}/PKG-INFO +3 -2
  2. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/pyproject.toml +7 -2
  3. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/commands/chat.py +16 -3
  5. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/commands/install_hook.py +7 -0
  6. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/commands/spawn.py +90 -0
  7. {swarph_cli-0.7.6 → swarph_cli-0.7.8/src/swarph_cli.egg-info}/PKG-INFO +3 -2
  8. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli.egg-info/requires.txt +3 -0
  9. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_spawn_command.py +105 -0
  10. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/LICENSE +0 -0
  11. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/README.md +0 -0
  12. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/setup.cfg +0 -0
  13. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/caller.py +0 -0
  14. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/cell.py +0 -0
  15. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/commands/__init__.py +0 -0
  16. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/commands/daemon.py +0 -0
  17. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/commands/hook_output.py +0 -0
  18. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/commands/import_session.py +0 -0
  19. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/commands/onboard.py +0 -0
  20. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/commands/ratify.py +0 -0
  21. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/commands/watchdog.py +0 -0
  22. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/main.py +0 -0
  23. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/parsers/__init__.py +0 -0
  24. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/parsers/claude.py +0 -0
  25. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  26. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  27. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  28. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli.egg-info/SOURCES.txt +0 -0
  29. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  30. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  31. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/src/swarph_cli.egg-info/top_level.txt +0 -0
  32. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_cell_loader.py +0 -0
  33. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_chat_command.py +0 -0
  34. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_claude_parser.py +0 -0
  35. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_daemon_command.py +0 -0
  36. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_hook_output.py +0 -0
  37. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_import_command.py +0 -0
  38. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_install_hook.py +0 -0
  39. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_main.py +0 -0
  40. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_onboard_command.py +0 -0
  41. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_ratify_command.py +0 -0
  42. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_smoke_chat.py +0 -0
  43. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_smoke_one_shot.py +0 -0
  44. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_smoke_phase_5_5.py +0 -0
  45. {swarph_cli-0.7.6 → swarph_cli-0.7.8}/tests/test_watchdog.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.7.6
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).
3
+ Version: 0.7.8
4
+ Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration: one-shot + chat REPL, multi-provider routing (gemini/claude/deepseek/openai/grok), cell.yaml spawn, session import, and watchdog. v0.7.8 fixes Windows chat-REPL portability: stdlib `readline` is POSIX-only, so native Windows fell back to bare input() and echoed Enter as ^M (CR=Ctrl-M) while breaking line editing adds a `pyreadline3` fallback (win32-only dependency) plus a CR-strip guard on parsed input.
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/darw007d/swarph-cli
@@ -27,6 +27,7 @@ License-File: LICENSE
27
27
  Requires-Dist: swarph-mesh>=0.5.0
28
28
  Requires-Dist: swarph-shared>=0.3.0
29
29
  Requires-Dist: PyYAML>=6.0
30
+ Requires-Dist: pyreadline3>=3.4; sys_platform == "win32"
30
31
  Provides-Extra: dev
31
32
  Requires-Dist: pytest>=7.0; extra == "dev"
32
33
  Dynamic: license-file
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swarph-cli"
7
- version = "0.7.6"
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)."
7
+ version = "0.7.8"
8
+ description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration: one-shot + chat REPL, multi-provider routing (gemini/claude/deepseek/openai/grok), cell.yaml spawn, session import, and watchdog. v0.7.8 fixes Windows chat-REPL portability: stdlib `readline` is POSIX-only, so native Windows fell back to bare input() and echoed Enter as ^M (CR=Ctrl-M) while breaking line editing adds a `pyreadline3` fallback (win32-only dependency) plus a CR-strip guard on parsed input."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.10"
@@ -42,6 +42,11 @@ dependencies = [
42
42
  # Stays a swarph-cli dep since file I/O is operator-tooling-layer
43
43
  # concern; swarph-shared cell module is pure-stdlib.
44
44
  "PyYAML>=6.0",
45
+ # Windows portability: stdlib `readline` is POSIX-only, so the chat
46
+ # REPL falls back to bare input() on native Windows — which echoes
47
+ # Enter as ^M (CR=Ctrl-M) + breaks line editing in mintty/Git-Bash.
48
+ # pyreadline3 supplies the readline interface on Windows only.
49
+ "pyreadline3>=3.4; sys_platform == 'win32'",
45
50
  ]
46
51
 
47
52
  [project.urls]
@@ -16,6 +16,6 @@ The architecture splits CLI from substrate so:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- __version__ = "0.7.6"
19
+ __version__ = "0.7.8"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -141,12 +141,25 @@ def _print(msg: str = "", *, file=None) -> None:
141
141
  def _read_line(prompt: str) -> str:
142
142
  """Single-line input. Tests monkeypatch this to inject scripted
143
143
  input. Production uses stdlib ``input()`` — readline is auto-loaded
144
- by import-time on POSIX, giving line editing + history for free."""
144
+ at import-time on POSIX, giving line editing + history for free.
145
+
146
+ Windows portability: stdlib ``readline`` is POSIX-only, so on native
147
+ Windows it's absent and ``input()`` runs raw. In mintty/Git-Bash and
148
+ some non-conhost consoles that makes the Enter keypress echo a raw
149
+ carriage return as ``^M`` (CR = 0x0D = Ctrl-M) and breaks line editing.
150
+ ``pyreadline3`` provides the readline interface on Windows; we fall back
151
+ to it so native consoles get proper line discipline. The ``rstrip``
152
+ below also strips any stray ``\\r`` from the parsed value as a guard,
153
+ independent of the echo fix. (Cleanest of all: run swarph inside WSL,
154
+ which is POSIX and loads stdlib readline directly.)"""
145
155
  try:
146
156
  import readline # noqa: F401 — side-effect-only on POSIX
147
157
  except ImportError:
148
- pass # Windows / minimal builds; raw input still works
149
- return input(prompt)
158
+ try:
159
+ import pyreadline3 # noqa: F401 — Windows readline shim
160
+ except ImportError:
161
+ pass # minimal build; raw input still works (rstrip guards CR)
162
+ return input(prompt).rstrip("\r\n")
150
163
 
151
164
 
152
165
  def _format_attribution(
@@ -22,6 +22,13 @@ Idempotent: rerun safe. Detects existing hook entries pointing at
22
22
  Skip when SWARPH_SPAWN=1 env (set by ``swarph spawn``) — avoids
23
23
  double-injection when the spawn path already passed the prompt via
24
24
  ``--append-system-prompt``.
25
+
26
+ NOTE (2026-05-17): IF this file grows to manage 3+ DISTINCT hook events
27
+ (beyond SessionStart), revisit adopting a hook framework
28
+ (claude-hooks / cchooks / similar). Solo eval 2026-05-17 deferred
29
+ framework adoption because 1-hook surface didn't justify the
30
+ 750-LOC dependency. See lab memory ``project_deferred_decisions.md``
31
+ for the full threshold conditions + revisit checklist.
25
32
  """
26
33
 
27
34
  from __future__ import annotations
@@ -201,6 +201,61 @@ def _resolve_cell(args: argparse.Namespace) -> tuple[Cell, Optional[str]]:
201
201
  return load_cell(path), requested_role
202
202
 
203
203
 
204
+ def _validate_routing(cell: Cell) -> None:
205
+ """Phase 1B v0 (2026-05-19) — read + validate ``cell.extra.routing``.
206
+
207
+ Phase 1B-primary architecture commits the cell-membrane framing:
208
+ Claude CLI is the membrane for Anthropic-side spawns; non-Anthropic
209
+ cells get a different membrane (currently NOT implemented).
210
+
211
+ cell.yaml ``routing`` field shape (v0):
212
+ ::
213
+ routing:
214
+ native: anthropic # only valid value in v0
215
+
216
+ v0 accepts ``routing`` absent (= default anthropic) OR
217
+ ``routing.native: anthropic``. Any other value raises CellError
218
+ pointing at Phase 1B v1+ direction.
219
+
220
+ Forward-compat: reads via ``Cell.extra`` (same pattern as
221
+ cursor_path / tmux_session shipped in v0.7.2 before graduating to
222
+ typed fields in swarph-shared 0.4.x). When swarph-shared graduates
223
+ ``routing`` to a typed Cell field, this helper swaps to typed
224
+ access with no API surface change for cell.yaml authors.
225
+
226
+ See research/swarph_cli/CELL_MEMBRANE_PHASE_0_RFC.md §5 (Phase 1B)
227
+ for the architectural context. See lab memory
228
+ ``project_next_up.md`` for the commander 2026-05-19 decision that
229
+ Phase 1B is primary + path (c) Anthropic-only v0.
230
+ """
231
+ extra = cell.extra or {}
232
+ routing = extra.get("routing")
233
+ if routing is None:
234
+ return # No routing field → default Anthropic, allow
235
+ if not isinstance(routing, dict):
236
+ raise CellError(
237
+ f"swarph spawn: cell.yaml `routing` must be a mapping, "
238
+ f"got {type(routing).__name__}. See "
239
+ f"research/swarph_cli/CELL_MEMBRANE_PHASE_0_RFC.md for the "
240
+ f"valid v0 schema."
241
+ )
242
+ native = routing.get("native", "anthropic")
243
+ if native == "anthropic":
244
+ return # Explicit Anthropic OR default → allow
245
+ # Any other native value is a Phase 1B v1+ feature not yet built
246
+ raise CellError(
247
+ f"swarph spawn: cell.yaml `routing.native: {native!r}` is not "
248
+ f"supported in v0 of Phase 1B (Anthropic-only). Non-Anthropic "
249
+ f"routing (e.g. routing.native: openrouter) is Phase 1B v1+ "
250
+ f"scope, deferred per commander 2026-05-19 until a concrete "
251
+ f"non-Anthropic-cell use case emerges. For now, remove the "
252
+ f"`routing.native` field OR set it to 'anthropic' to use the "
253
+ f"existing Claude CLI spawn path. See "
254
+ f"research/swarph_cli/CELL_MEMBRANE_PHASE_0_RFC.md §5 for the "
255
+ f"architectural direction."
256
+ )
257
+
258
+
204
259
  def _session_state_exists(session_id: str) -> bool:
205
260
  """True if Claude Code already has on-disk session state for this UUID.
206
261
 
@@ -346,6 +401,15 @@ def run_spawn(argv: Optional[list[str]] = None) -> int:
346
401
  print(f"swarph spawn: {exc}", file=sys.stderr)
347
402
  return 1
348
403
 
404
+ # Phase 1B v0 (2026-05-19): validate cell.yaml routing field.
405
+ # In v0 only `routing.native: anthropic` (or absent) is accepted.
406
+ # Future non-Anthropic dispatch is Phase 1B v1+ scope.
407
+ try:
408
+ _validate_routing(cell)
409
+ except CellError as exc:
410
+ print(f"swarph spawn: {exc}", file=sys.stderr)
411
+ return 1
412
+
349
413
  # When user typed a slot-role (e.g. `swarph spawn drop-on-meta-edge-2`)
350
414
  # the cell.yaml resolved to the BASE file (drop-on-meta-edge.yaml) so
351
415
  # cell.role = "drop-on-meta-edge". But the operator wants slot 2's
@@ -418,6 +482,32 @@ def run_spawn(argv: Optional[list[str]] = None) -> int:
418
482
  )
419
483
  return 127
420
484
 
485
+ # Windows-platform known-issues banner. Claude Code's TUI (Ink-based)
486
+ # has documented input/rendering bugs on Windows native consoles
487
+ # (conhost.exe in particular). Specific symptom commander hit
488
+ # 2026-05-17 on workstation-lc: pressing Enter inserts literal 'm'
489
+ # character instead of submitting. See docs/WINDOWS_KNOWN_ISSUES.md
490
+ # for the full hypothesis chain + workarounds (Windows Terminal vs
491
+ # conhost, WSL2 fallback, TERM env injection).
492
+ #
493
+ # Banner is suppressed by --no-banner OR when the operator has
494
+ # already acknowledged via SWARPH_WIN_ACK=1 in env (set once after
495
+ # reading the doc).
496
+ if (
497
+ sys.platform == "win32"
498
+ and not args.no_banner
499
+ and not os.environ.get("SWARPH_WIN_ACK")
500
+ ):
501
+ print(
502
+ "swarph spawn: WARNING — Windows shell detected. Claude Code's "
503
+ "TUI has documented input/rendering issues on Windows native "
504
+ "consoles (conhost.exe). Known symptom: Enter inserts literal "
505
+ "'m' character. See docs/WINDOWS_KNOWN_ISSUES.md for "
506
+ "workarounds (use Windows Terminal not conhost, or WSL2). "
507
+ "Set SWARPH_WIN_ACK=1 in env to suppress this warning.",
508
+ file=sys.stderr,
509
+ )
510
+
421
511
  try:
422
512
  os.chdir(cell.cwd)
423
513
  except OSError as exc:
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.7.6
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).
3
+ Version: 0.7.8
4
+ Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration: one-shot + chat REPL, multi-provider routing (gemini/claude/deepseek/openai/grok), cell.yaml spawn, session import, and watchdog. v0.7.8 fixes Windows chat-REPL portability: stdlib `readline` is POSIX-only, so native Windows fell back to bare input() and echoed Enter as ^M (CR=Ctrl-M) while breaking line editing adds a `pyreadline3` fallback (win32-only dependency) plus a CR-strip guard on parsed input.
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/darw007d/swarph-cli
@@ -27,6 +27,7 @@ License-File: LICENSE
27
27
  Requires-Dist: swarph-mesh>=0.5.0
28
28
  Requires-Dist: swarph-shared>=0.3.0
29
29
  Requires-Dist: PyYAML>=6.0
30
+ Requires-Dist: pyreadline3>=3.4; sys_platform == "win32"
30
31
  Provides-Extra: dev
31
32
  Requires-Dist: pytest>=7.0; extra == "dev"
32
33
  Dynamic: license-file
@@ -2,5 +2,8 @@ swarph-mesh>=0.5.0
2
2
  swarph-shared>=0.3.0
3
3
  PyYAML>=6.0
4
4
 
5
+ [:sys_platform == "win32"]
6
+ pyreadline3>=3.4
7
+
5
8
  [dev]
6
9
  pytest>=7.0
@@ -428,3 +428,108 @@ def test_build_claude_argv_uses_resume_when_state_exists(fake_cell_yaml, tmp_pat
428
428
  assert "--session-id" not in argv
429
429
  # UUID still passed (just as --resume's value not --session-id's)
430
430
  assert uuid in argv
431
+
432
+
433
+ # ---------------------------------------------------------------------------
434
+ # Phase 1B v0 — cell.yaml routing field (2026-05-19)
435
+ # ---------------------------------------------------------------------------
436
+
437
+
438
+ def test_validate_routing_absent_allows(fake_cell_yaml):
439
+ """No `routing` field → default Anthropic, _validate_routing returns silently."""
440
+ from swarph_cli.commands.spawn import _validate_routing
441
+ cell = load_cell(fake_cell_yaml)
442
+ # fake_cell_yaml has no routing field → should pass
443
+ _validate_routing(cell) # no exception = pass
444
+
445
+
446
+ def test_validate_routing_explicit_anthropic_allows(tmp_path):
447
+ """`routing.native: anthropic` → allowed (explicit form)."""
448
+ from swarph_cli.commands.spawn import _validate_routing
449
+ payload = {
450
+ "schema_version": SCHEMA_VERSION_V1,
451
+ "name": "lab-ovh",
452
+ "role": "lab-test",
453
+ "cwd": str(tmp_path),
454
+ "provider": "claude",
455
+ "routing": {"native": "anthropic"},
456
+ }
457
+ p = tmp_path / "cell.yaml"
458
+ p.write_text(yaml.safe_dump(payload), encoding="utf-8")
459
+ cell = load_cell(p)
460
+ _validate_routing(cell) # no exception = pass
461
+
462
+
463
+ def test_validate_routing_non_anthropic_rejects(tmp_path):
464
+ """`routing.native: openrouter` → rejected with v0 + v1 direction message."""
465
+ from swarph_cli.commands.spawn import _validate_routing
466
+ from swarph_cli.cell import CellError
467
+ payload = {
468
+ "schema_version": SCHEMA_VERSION_V1,
469
+ "name": "lab-ovh",
470
+ "role": "lab-test",
471
+ "cwd": str(tmp_path),
472
+ "provider": "claude",
473
+ "routing": {"native": "openrouter"},
474
+ }
475
+ p = tmp_path / "cell.yaml"
476
+ p.write_text(yaml.safe_dump(payload), encoding="utf-8")
477
+ cell = load_cell(p)
478
+ with pytest.raises(CellError) as exc_info:
479
+ _validate_routing(cell)
480
+ err = str(exc_info.value)
481
+ assert "openrouter" in err
482
+ assert "v0" in err
483
+ assert "Phase 1B" in err
484
+ assert "anthropic" in err # should point at the only valid value
485
+
486
+
487
+ def test_validate_routing_non_dict_rejects(tmp_path):
488
+ """`routing: "anthropic"` (string instead of dict) → schema error."""
489
+ from swarph_cli.commands.spawn import _validate_routing
490
+ from swarph_cli.cell import CellError
491
+ payload = {
492
+ "schema_version": SCHEMA_VERSION_V1,
493
+ "name": "lab-ovh",
494
+ "role": "lab-test",
495
+ "cwd": str(tmp_path),
496
+ "provider": "claude",
497
+ "routing": "anthropic", # WRONG — should be dict
498
+ }
499
+ p = tmp_path / "cell.yaml"
500
+ p.write_text(yaml.safe_dump(payload), encoding="utf-8")
501
+ cell = load_cell(p)
502
+ with pytest.raises(CellError) as exc_info:
503
+ _validate_routing(cell)
504
+ assert "mapping" in str(exc_info.value)
505
+
506
+
507
+ def test_validate_routing_omitted_native_allows(tmp_path):
508
+ """`routing: {}` (empty dict, no native key) → defaults to anthropic, allows."""
509
+ from swarph_cli.commands.spawn import _validate_routing
510
+ payload = {
511
+ "schema_version": SCHEMA_VERSION_V1,
512
+ "name": "lab-ovh",
513
+ "role": "lab-test",
514
+ "cwd": str(tmp_path),
515
+ "provider": "claude",
516
+ "routing": {},
517
+ }
518
+ p = tmp_path / "cell.yaml"
519
+ p.write_text(yaml.safe_dump(payload), encoding="utf-8")
520
+ cell = load_cell(p)
521
+ _validate_routing(cell) # default anthropic = allowed
522
+
523
+
524
+ def test_run_spawn_rejects_non_anthropic_routing_in_dry_run(
525
+ fake_cell_yaml, isolated_xdg, capsys
526
+ ):
527
+ """End-to-end: `swarph spawn --dry-run` with non-anthropic routing → exit 1 + error message."""
528
+ payload = yaml.safe_load(fake_cell_yaml.read_text())
529
+ payload["routing"] = {"native": "gemini"}
530
+ fake_cell_yaml.write_text(yaml.safe_dump(payload))
531
+ rc = run_spawn(["--dry-run", str(fake_cell_yaml)])
532
+ assert rc == 1
533
+ captured = capsys.readouterr()
534
+ assert "gemini" in captured.err
535
+ assert "Phase 1B" in captured.err
File without changes
File without changes
File without changes