swarph-cli 0.7.9__tar.gz → 0.8.0__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 (46) hide show
  1. {swarph_cli-0.7.9/src/swarph_cli.egg-info → swarph_cli-0.8.0}/PKG-INFO +2 -2
  2. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/pyproject.toml +2 -2
  3. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/spawn.py +359 -136
  5. {swarph_cli-0.7.9 → swarph_cli-0.8.0/src/swarph_cli.egg-info}/PKG-INFO +2 -2
  6. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli.egg-info/SOURCES.txt +1 -0
  7. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_cell_loader.py +10 -2
  8. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_spawn_command.py +408 -6
  9. swarph_cli-0.8.0/tests/test_spawn_windows_relaunch.py +129 -0
  10. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/LICENSE +0 -0
  11. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/README.md +0 -0
  12. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/setup.cfg +0 -0
  13. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/caller.py +0 -0
  14. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/cell.py +0 -0
  15. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/__init__.py +0 -0
  16. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/chat.py +0 -0
  17. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/daemon.py +0 -0
  18. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/hook_output.py +0 -0
  19. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/import_session.py +0 -0
  20. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/install_hook.py +0 -0
  21. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/onboard.py +0 -0
  22. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/ratify.py +0 -0
  23. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/commands/watchdog.py +0 -0
  24. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/main.py +0 -0
  25. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/parsers/__init__.py +0 -0
  26. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/parsers/claude.py +0 -0
  27. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  28. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  29. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  30. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  31. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  32. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli.egg-info/requires.txt +0 -0
  33. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
  34. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_chat_command.py +0 -0
  35. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_claude_parser.py +0 -0
  36. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_daemon_command.py +0 -0
  37. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_hook_output.py +0 -0
  38. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_import_command.py +0 -0
  39. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_install_hook.py +0 -0
  40. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_main.py +0 -0
  41. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_onboard_command.py +0 -0
  42. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_ratify_command.py +0 -0
  43. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_smoke_chat.py +0 -0
  44. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_smoke_one_shot.py +0 -0
  45. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/tests/test_smoke_phase_5_5.py +0 -0
  46. {swarph_cli-0.7.9 → swarph_cli-0.8.0}/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.9
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.9 reverts the v0.7.8 pyreadline3 dependency + chat-REPL shim: the Windows 'Enter renders m' symptom traced to Claude Code's own native-Windows TUI (no hooks, recent build, fails at the trust prompt — Anthropic-side), NOT this REPL, so the shim addressed the wrong layer. Bare input() + a CR-strip guard remain.
3
+ Version: 0.8.0
4
+ Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration: one-shot + chat REPL, multi-provider routing, cell.yaml spawn, session import, watchdog. v0.8.0: multi-provider spawn membrane `swarph spawn` launches claude, codex (GPT), or antigravity/agy (Gemini) per cell.provider, each with billing-scrub + sandbox; plus the Windows conhost auto-relaunch into Windows Terminal (the Enter-inserts-'m' TUI fix).
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.7.9"
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.9 reverts the v0.7.8 pyreadline3 dependency + chat-REPL shim: the Windows 'Enter renders m' symptom traced to Claude Code's own native-Windows TUI (no hooks, recent build, fails at the trust prompt — Anthropic-side), NOT this REPL, so the shim addressed the wrong layer. Bare input() + a CR-strip guard remain."
7
+ version = "0.8.0"
8
+ description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration: one-shot + chat REPL, multi-provider routing, cell.yaml spawn, session import, watchdog. v0.8.0: multi-provider spawn membrane `swarph spawn` launches claude, codex (GPT), or antigravity/agy (Gemini) per cell.provider, each with billing-scrub + sandbox; plus the Windows conhost auto-relaunch into Windows Terminal (the Enter-inserts-'m' TUI fix)."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.10"
@@ -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.9"
19
+ __version__ = "0.8.0"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -26,6 +26,7 @@ from __future__ import annotations
26
26
  import argparse
27
27
  import os
28
28
  import shutil
29
+ import subprocess
29
30
  import sys
30
31
  from pathlib import Path
31
32
  from typing import Optional
@@ -58,7 +59,7 @@ _USAGE = """\
58
59
  Usage:
59
60
  swarph spawn [<role-or-path>] [--onboarding PATH-OR-URL]
60
61
  [--dry-run] [--no-starter] [--print-id]
61
- [-- claude-extra-args...]
62
+ [-- provider-extra-args...]
62
63
 
63
64
  Resolution (first match wins):
64
65
  --onboarding <path-or-url> explicit override
@@ -68,16 +69,31 @@ Resolution (first match wins):
68
69
  mesh-gateway://... v0.7+ — returns NotImplementedError now
69
70
 
70
71
  Flags:
71
- --dry-run Print the resolved claude command + cell summary; no exec
72
+ --dry-run Print the resolved provider command + cell summary; no exec
72
73
  --no-starter Skip starter-prompt injection even if cell.yaml sets one
73
74
  --print-id Print resolved session-id to stdout before exec (useful
74
75
  for shell scripts capturing the UUID for later resume)
75
76
 
76
- Anything after a literal `--` is passed through to claude unchanged
77
- (e.g. `swarph spawn lab -- --resume` to force the resume picker).
77
+ Anything after a literal `--` is passed through to the provider CLI unchanged
78
+ (e.g. `swarph spawn lab -- --resume` to force the Claude resume picker).
78
79
  """
79
80
 
80
81
 
82
+ _CODEX_BILLING_LEAK_KEYS = (
83
+ "OPENAI_API_KEY",
84
+ "OPENAI_API_BASE",
85
+ "OPENAI_BASE_URL",
86
+ "CODEX_API_KEY",
87
+ "OPENAI_ORG_ID",
88
+ "OPENAI_ORGANIZATION",
89
+ )
90
+
91
+ _CODEX_SANDBOX_VALUES = frozenset({"workspace-write", "read-only"})
92
+ _CODEX_DEFAULT_SANDBOX = "workspace-write"
93
+ _CODEX_APPROVAL = "on-request"
94
+ _CODEX_PRINT_ID_NOTE = "codex: fresh-session-per-spawn, no pinned id"
95
+
96
+
81
97
  def _build_parser() -> argparse.ArgumentParser:
82
98
  p = argparse.ArgumentParser(
83
99
  prog="swarph spawn",
@@ -202,36 +218,11 @@ def _resolve_cell(args: argparse.Namespace) -> tuple[Cell, Optional[str]]:
202
218
 
203
219
 
204
220
  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
- """
221
+ """Validate optional ``cell.extra.routing`` against the spawn provider."""
231
222
  extra = cell.extra or {}
232
223
  routing = extra.get("routing")
233
224
  if routing is None:
234
- return # No routing field → default Anthropic, allow
225
+ return
235
226
  if not isinstance(routing, dict):
236
227
  raise CellError(
237
228
  f"swarph spawn: cell.yaml `routing` must be a mapping, "
@@ -239,20 +230,33 @@ def _validate_routing(cell: Cell) -> None:
239
230
  f"research/swarph_cli/CELL_MEMBRANE_PHASE_0_RFC.md for the "
240
231
  f"valid v0 schema."
241
232
  )
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
233
+
234
+ provider_native = {
235
+ "claude": "anthropic",
236
+ "codex": "codex",
237
+ "antigravity": "antigravity",
238
+ }.get(cell.provider)
239
+ if provider_native is None:
240
+ raise CellError(
241
+ f"swarph spawn: provider {cell.provider!r} is not supported "
242
+ "by this spawn membrane."
243
+ )
244
+
245
+ native = routing.get("native", provider_native)
246
+
247
+ if cell.provider == "antigravity":
248
+ if native in ("antigravity", "gemini"):
249
+ return
250
+ expected = "'antigravity' or 'gemini'"
251
+ else:
252
+ if native == provider_native:
253
+ return
254
+ expected = repr(provider_native)
255
+
246
256
  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."
257
+ f"swarph spawn: cell.yaml `routing.native: {native!r}` does not "
258
+ f"match provider {cell.provider!r}. Expected routing.native "
259
+ f"{expected}, or omit the routing field."
256
260
  )
257
261
 
258
262
 
@@ -309,6 +313,75 @@ def _build_claude_argv(
309
313
  return argv
310
314
 
311
315
 
316
+ def _codex_sandbox(cell: Cell) -> str:
317
+ sandbox = getattr(cell, "sandbox", None) or _CODEX_DEFAULT_SANDBOX
318
+ if sandbox not in _CODEX_SANDBOX_VALUES:
319
+ raise CellError(
320
+ f"cell.yaml: sandbox {sandbox!r} is not valid for provider "
321
+ f"'codex'. Valid values: {sorted(_CODEX_SANDBOX_VALUES)}."
322
+ )
323
+ return sandbox
324
+
325
+
326
+ def _agy_env() -> dict[str, str]:
327
+ """Return a copy of the environment scrubbed of billing credentials."""
328
+ env = os.environ.copy()
329
+ env.pop("GOOGLE_APPLICATION_CREDENTIALS", None)
330
+ env.pop("GOOGLE_CLOUD_PROJECT", None)
331
+ env.pop("VERTEX_PROJECT", None)
332
+ env.pop("VERTEX_LOCATION", None)
333
+ return env
334
+
335
+
336
+ def _build_agy_argv(
337
+ cell: Cell, no_starter: bool, passthrough: list[str]
338
+ ) -> list[str]:
339
+ argv = ["agy"]
340
+
341
+ # codex is adding cell.sandbox; default ON, only off on explicit falsy
342
+ sandbox_attr = getattr(cell, "sandbox", None)
343
+ if sandbox_attr is not None:
344
+ is_sandbox = sandbox_attr
345
+ else:
346
+ is_sandbox = cell.extra.get("sandbox", True)
347
+
348
+ if is_sandbox is not False:
349
+ argv.append("--sandbox")
350
+
351
+ # Pass --add-dir <cwd> for directory setup.
352
+ argv.extend(["--add-dir", str(cell.cwd)])
353
+
354
+ if not no_starter and cell.starter_prompt_path:
355
+ argv.extend(["--prompt-interactive", read_starter_prompt(cell)])
356
+
357
+ argv.extend(passthrough)
358
+ return argv
359
+
360
+
361
+ def _build_codex_argv(cell: Cell, passthrough: list[str]) -> list[str]:
362
+ argv = [
363
+ "codex",
364
+ "-C",
365
+ str(cell.cwd),
366
+ "-s",
367
+ _codex_sandbox(cell),
368
+ "-a",
369
+ _CODEX_APPROVAL,
370
+ ]
371
+ argv.extend(passthrough)
372
+ return argv
373
+
374
+
375
+ def _scrubbed_codex_env() -> dict[str, str]:
376
+ env = {
377
+ key: value
378
+ for key, value in os.environ.items()
379
+ if key not in _CODEX_BILLING_LEAK_KEYS
380
+ }
381
+ env["SWARPH_SPAWN"] = "1"
382
+ return env
383
+
384
+
312
385
  def _print_banner() -> None:
313
386
  sys.stderr.write(_BANNER.format(version=__version__))
314
387
  sys.stderr.flush()
@@ -316,7 +389,7 @@ def _print_banner() -> None:
316
389
 
317
390
  def _print_dry_run(
318
391
  cell: Cell,
319
- session_id: str,
392
+ session_id: Optional[str],
320
393
  was_generated: bool,
321
394
  argv: list[str],
322
395
  new_instance: bool = False,
@@ -339,15 +412,29 @@ def _print_dry_run(
339
412
  print(f"# name: {cell.name}", file=sys.stderr)
340
413
  print(f"# role: {cell.role}", file=sys.stderr)
341
414
  print(f"# cwd: {cell.cwd}", file=sys.stderr)
342
- print(
343
- f"# session_id: {session_id} ({sid_label})",
344
- file=sys.stderr,
345
- )
346
- print(
347
- f"# starter: "
348
- f"{cell.starter_prompt_path or '(none)'}",
349
- file=sys.stderr,
350
- )
415
+ if cell.provider == "codex":
416
+ print(
417
+ "# session_id: codex: fresh-session-per-spawn, no pinned id "
418
+ "(cell.yaml session_id ignored)",
419
+ file=sys.stderr,
420
+ )
421
+ else:
422
+ print(
423
+ f"# session_id: {session_id} ({sid_label})",
424
+ file=sys.stderr,
425
+ )
426
+ if cell.provider == "codex":
427
+ print(
428
+ "# starter: cwd AGENTS.md auto-read by codex; no "
429
+ "--append-system-prompt injection",
430
+ file=sys.stderr,
431
+ )
432
+ else:
433
+ print(
434
+ f"# starter: "
435
+ f"{cell.starter_prompt_path or '(none)'}",
436
+ file=sys.stderr,
437
+ )
351
438
  print(f"# provider: {cell.provider}", file=sys.stderr)
352
439
  if cell.lineage is not None:
353
440
  print(
@@ -371,6 +458,69 @@ def _print_dry_run(
371
458
  print(" ".join(redacted))
372
459
 
373
460
 
461
+ def _relaunch_in_windows_terminal(
462
+ claude_bin: str, claude_argv: list[str], cwd: Path,
463
+ ) -> bool:
464
+ """Auto-fix the conhost TUI bug by relaunching the session in Windows Terminal.
465
+
466
+ On legacy Windows console (``conhost.exe``), Claude Code's Ink TUI breaks: the
467
+ SGR terminator ``m`` leaks from the output stream into stdin, so pressing Enter
468
+ inserts a literal ``m`` instead of submitting (see docs/WINDOWS_KNOWN_ISSUES.md).
469
+ Windows Terminal handles VT-input correctly. If we're on conhost AND ``wt.exe``
470
+ is available, relaunch the ``claude`` session inside Windows Terminal and return
471
+ True (the caller should exit this console). Otherwise return False (caller
472
+ proceeds in-place and warns).
473
+
474
+ No-op (returns False) when:
475
+ * not Windows;
476
+ * stdout is not an interactive TTY (CI / piped / redirected) — there is no
477
+ human console to relaunch from, and a detached WT window would be wrong;
478
+ * we are already inside a session WE spawned (``SWARPH_SPAWN`` set) — the
479
+ reliable loop-guard: a relaunched session can never re-relaunch, regardless
480
+ of how ``WT_SESSION`` behaves on this box;
481
+ * operator opted to stay put (``SWARPH_WIN_ACK=1``);
482
+ * already inside Windows Terminal (``WT_SESSION`` set) AND not force-requested
483
+ — the TUI works there. NOTE: some corporate setups INHERIT ``WT_SESSION``
484
+ into child ``conhost`` consoles, so this is a comfort heuristic, not ground
485
+ truth. Set ``SWARPH_FORCE_WT=1`` to relaunch anyway when you know you are on
486
+ a broken conhost that carries an inherited ``WT_SESSION``;
487
+ * ``wt.exe`` is not installed (e.g. locked-down corporate box) — caller warns.
488
+ """
489
+ if sys.platform != "win32":
490
+ return False
491
+ if not sys.stdout.isatty():
492
+ return False
493
+ if os.environ.get("SWARPH_SPAWN"):
494
+ return False
495
+ if os.environ.get("SWARPH_WIN_ACK"):
496
+ return False
497
+ if os.environ.get("WT_SESSION") and not os.environ.get("SWARPH_FORCE_WT"):
498
+ return False
499
+ wt = shutil.which("wt")
500
+ if not wt:
501
+ return False
502
+ # Relaunch claude inside Windows Terminal, in the cell's cwd, carrying
503
+ # SWARPH_SPAWN=1 so a SessionStart hook doesn't double-inject the starter.
504
+ # claude_argv[0] is the "claude" argv0; the real flags are claude_argv[1:].
505
+ wt_cmd = [wt, "-d", str(cwd), "--", claude_bin, *claude_argv[1:]]
506
+ env = {**os.environ, "SWARPH_SPAWN": "1"}
507
+ try:
508
+ subprocess.Popen(wt_cmd, env=env)
509
+ except OSError as exc:
510
+ print(
511
+ f"swarph spawn: Windows Terminal relaunch failed ({exc}); continuing "
512
+ f"in this console (TUI may misbehave — see docs/WINDOWS_KNOWN_ISSUES.md).",
513
+ file=sys.stderr,
514
+ )
515
+ return False
516
+ print(
517
+ "swarph spawn: relaunched the session in Windows Terminal (avoids the "
518
+ "conhost Enter-inserts-'m' TUI bug). This console can be closed.",
519
+ file=sys.stderr,
520
+ )
521
+ return True
522
+
523
+
374
524
  def run_spawn(argv: Optional[list[str]] = None) -> int:
375
525
  if argv is None:
376
526
  argv = sys.argv[2:] # skip "swarph spawn"
@@ -410,77 +560,127 @@ def run_spawn(argv: Optional[list[str]] = None) -> int:
410
560
  print(f"swarph spawn: {exc}", file=sys.stderr)
411
561
  return 1
412
562
 
413
- # When user typed a slot-role (e.g. `swarph spawn drop-on-meta-edge-2`)
414
- # the cell.yaml resolved to the BASE file (drop-on-meta-edge.yaml) so
415
- # cell.role = "drop-on-meta-edge". But the operator wants slot 2's
416
- # sidecar + display name. Use the user's typed role for the sidecar
417
- # lookup; cell.role stays the base role for cell-context (cwd, starter
418
- # prompt, lineage, provider).
419
- sidecar_role = requested_role if requested_role else cell.role
420
-
421
- try:
422
- session_id, was_generated, effective_role = load_or_create_session_id(
423
- sidecar_role, cell, new_instance=args.new_instance
424
- )
425
- except CellError as exc:
426
- print(f"swarph spawn: {exc}", file=sys.stderr)
427
- return 1
428
-
429
- if args.new_instance and cell.session_id:
430
- # Pinned cell.yaml session_id wins over --new-instance; surface
431
- # the conflict on stderr so the operator knows the flag was a
432
- # no-op for this cell.
433
- print(
434
- "swarph spawn: --new-instance ignored — cell.yaml pins "
435
- "session_id explicitly. Remove the pinned UUID from cell.yaml "
436
- "OR pass --session-id <new-uuid> on the claude command line "
437
- "via `-- --session-id <uuid>` passthrough to override.",
438
- file=sys.stderr,
439
- )
563
+ session_id: Optional[str]
564
+ was_generated = False
565
+ effective_role: Optional[str] = None
566
+
567
+ if cell.provider == "codex":
568
+ session_id = None
569
+ try:
570
+ spawn_argv = _build_codex_argv(cell, passthrough)
571
+ except CellError as exc:
572
+ print(f"swarph spawn: {exc}", file=sys.stderr)
573
+ return 1
574
+ elif cell.provider == "antigravity":
575
+ session_id = "(fresh-session-per-spawn, no pinned id)"
576
+ was_generated = True
577
+ sidecar_role = requested_role if requested_role else cell.role
578
+ effective_role = sidecar_role
579
+ try:
580
+ spawn_argv = _build_agy_argv(
581
+ cell, args.no_starter, passthrough
582
+ )
583
+ except CellError as exc:
584
+ print(f"swarph spawn: {exc}", file=sys.stderr)
585
+ return 1
586
+ else:
587
+ # When user typed a slot-role (e.g. `swarph spawn drop-on-meta-edge-2`)
588
+ # the cell.yaml resolved to the BASE file (drop-on-meta-edge.yaml) so
589
+ # cell.role = "drop-on-meta-edge". But the operator wants slot 2's
590
+ # sidecar + display name. Use the user's typed role for the sidecar
591
+ # lookup; cell.role stays the base role for cell-context (cwd, starter
592
+ # prompt, lineage, provider).
593
+ sidecar_role = requested_role if requested_role else cell.role
594
+
595
+ try:
596
+ session_id, was_generated, effective_role = load_or_create_session_id(
597
+ sidecar_role, cell, new_instance=args.new_instance
598
+ )
599
+ except CellError as exc:
600
+ print(f"swarph spawn: {exc}", file=sys.stderr)
601
+ return 1
602
+
603
+ if args.new_instance and cell.session_id:
604
+ # Pinned cell.yaml session_id wins over --new-instance; surface
605
+ # the conflict on stderr so the operator knows the flag was a
606
+ # no-op for this cell.
607
+ print(
608
+ "swarph spawn: --new-instance ignored — cell.yaml pins "
609
+ "session_id explicitly. Remove the pinned UUID from cell.yaml "
610
+ "OR pass --session-id <new-uuid> on the claude command line "
611
+ "via `-- --session-id <uuid>` passthrough to override.",
612
+ file=sys.stderr,
613
+ )
440
614
 
441
- if args.new_instance and effective_role == sidecar_role and not cell.session_id:
442
- # Degenerate case — --new-instance fired but no base sidecar
443
- # existed, so we minted into the BASE slot (treating as the
444
- # original). Surface this as a stderr note so the operator
445
- # understands the v0.7 PR-B auto-suffix didn't kick in.
446
- print(
447
- "swarph spawn: --new-instance fired on a role with no "
448
- f"existing sidecar — minted as the FIRST instance of "
449
- f"{cell.role!r} (base slot), not as a sibling. Spawn the "
450
- "original first via `swarph spawn <role>` (no --new-instance), "
451
- "then re-run with --new-instance to mint a true sibling.",
452
- file=sys.stderr,
453
- )
615
+ if args.new_instance and effective_role == sidecar_role and not cell.session_id:
616
+ # Degenerate case — --new-instance fired but no base sidecar
617
+ # existed, so we minted into the BASE slot (treating as the
618
+ # original). Surface this as a stderr note so the operator
619
+ # understands the v0.7 PR-B auto-suffix didn't kick in.
620
+ print(
621
+ "swarph spawn: --new-instance fired on a role with no "
622
+ f"existing sidecar — minted as the FIRST instance of "
623
+ f"{cell.role!r} (base slot), not as a sibling. Spawn the "
624
+ "original first via `swarph spawn <role>` (no --new-instance), "
625
+ "then re-run with --new-instance to mint a true sibling.",
626
+ file=sys.stderr,
627
+ )
454
628
 
455
- try:
456
- claude_argv = _build_claude_argv(
457
- cell, session_id, args.no_starter, passthrough,
458
- effective_role=effective_role,
459
- )
460
- except CellError as exc:
461
- print(f"swarph spawn: {exc}", file=sys.stderr)
462
- return 1
629
+ try:
630
+ spawn_argv = _build_claude_argv(
631
+ cell, session_id, args.no_starter, passthrough,
632
+ effective_role=effective_role,
633
+ )
634
+ except CellError as exc:
635
+ print(f"swarph spawn: {exc}", file=sys.stderr)
636
+ return 1
463
637
 
464
638
  if args.print_id:
465
- print(session_id)
639
+ if cell.provider == "codex":
640
+ print(_CODEX_PRINT_ID_NOTE)
641
+ else:
642
+ print(session_id)
466
643
 
467
644
  if args.dry_run:
468
645
  _print_dry_run(
469
- cell, session_id, was_generated, claude_argv,
646
+ cell, session_id, was_generated, spawn_argv,
470
647
  new_instance=args.new_instance,
471
648
  effective_role=effective_role,
472
649
  )
473
650
  return 0
474
651
 
475
- claude_bin = shutil.which("claude")
476
- if claude_bin is None:
477
- print(
478
- "swarph spawn: 'claude' binary not found on PATH. "
479
- "Install Claude Code (https://docs.anthropic.com/claude/claude-code) "
480
- "or set PATH explicitly.",
481
- file=sys.stderr,
482
- )
483
- return 127
652
+ if cell.provider == "codex":
653
+ provider_bin = shutil.which("codex")
654
+ if provider_bin is None:
655
+ print(
656
+ "swarph spawn: 'codex' binary not found on PATH. "
657
+ "Install Codex CLI or set PATH explicitly.",
658
+ file=sys.stderr,
659
+ )
660
+ return 127
661
+ elif cell.provider == "antigravity":
662
+ provider_bin = shutil.which("agy")
663
+ if provider_bin is None:
664
+ home_local = Path.home() / ".local" / "bin" / "agy"
665
+ if home_local.exists():
666
+ provider_bin = str(home_local)
667
+ if provider_bin is None:
668
+ print(
669
+ "swarph spawn: 'agy' binary not found on PATH. "
670
+ "Install Antigravity CLI or set PATH explicitly.",
671
+ file=sys.stderr,
672
+ )
673
+ return 127
674
+ else:
675
+ provider_bin = shutil.which("claude")
676
+ if provider_bin is None:
677
+ print(
678
+ "swarph spawn: 'claude' binary not found on PATH. "
679
+ "Install Claude Code (https://docs.anthropic.com/claude/claude-code) "
680
+ "or set PATH explicitly.",
681
+ file=sys.stderr,
682
+ )
683
+ return 127
484
684
 
485
685
  # Windows-platform known-issues banner. Claude Code's TUI (Ink-based)
486
686
  # has documented input/rendering bugs on Windows native consoles
@@ -493,38 +693,61 @@ def run_spawn(argv: Optional[list[str]] = None) -> int:
493
693
  # Banner is suppressed by --no-banner OR when the operator has
494
694
  # already acknowledged via SWARPH_WIN_ACK=1 in env (set once after
495
695
  # reading the doc).
696
+ # conhost TUI auto-fix (CLAUDE only — codex/agy don't use the claude TUI):
697
+ # on legacy Windows console (not Windows Terminal), relaunch the claude
698
+ # session in Windows Terminal where the Ink TUI works. Returns True (and we
699
+ # exit this console) only when it actually relaunched.
700
+ if cell.provider == "claude" and _relaunch_in_windows_terminal(
701
+ provider_bin, spawn_argv, cell.cwd
702
+ ):
703
+ return 0
704
+
705
+ # Still in a broken console (conhost with no wt.exe, or operator acked).
706
+ # Warn unless suppressed. Inside Windows Terminal (WT_SESSION set) the TUI
707
+ # works, so no warning fires there.
496
708
  if (
497
- sys.platform == "win32"
709
+ cell.provider == "claude"
710
+ and sys.platform == "win32"
498
711
  and not args.no_banner
499
712
  and not os.environ.get("SWARPH_WIN_ACK")
713
+ and not os.environ.get("WT_SESSION")
500
714
  ):
501
715
  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.",
716
+ "swarph spawn: WARNING — legacy Windows console (conhost) and Windows "
717
+ "Terminal (wt.exe) was not found, so the session couldn't be "
718
+ "auto-relaunched. Claude Code's TUI mis-handles input here (Enter "
719
+ "inserts literal 'm'). Install Windows Terminal (Microsoft Store) and "
720
+ "re-run, or use WSL2. See docs/WINDOWS_KNOWN_ISSUES.md. Set "
721
+ "SWARPH_WIN_ACK=1 to suppress and run here anyway.",
508
722
  file=sys.stderr,
509
723
  )
510
724
 
725
+ if cell.provider == "claude":
726
+ try:
727
+ os.chdir(cell.cwd)
728
+ except OSError as exc:
729
+ print(f"swarph spawn: cannot chdir to {cell.cwd}: {exc}", file=sys.stderr)
730
+ return 1
731
+
732
+ # v0.7 PR-C — set SWARPH_SPAWN=1 env so a SessionStart hook
733
+ # installed via `swarph install-hook` knows the prompt was
734
+ # already injected via --append-system-prompt and skips
735
+ # double-injection. The env propagates through execv since we
736
+ # don't use execve with a custom env.
737
+ os.environ["SWARPH_SPAWN"] = "1"
738
+ elif cell.provider == "antigravity":
739
+ env = _agy_env()
740
+ os.environ.clear()
741
+ os.environ.update(env)
742
+ os.environ["SWARPH_SPAWN"] = "1"
743
+
744
+ # exec-replace so the spawned provider session owns stdio +
745
+ # signals cleanly. argv[0] is preserved for ps-grep.
511
746
  try:
512
- os.chdir(cell.cwd)
513
- except OSError as exc:
514
- print(f"swarph spawn: cannot chdir to {cell.cwd}: {exc}", file=sys.stderr)
515
- return 1
516
-
517
- # v0.7 PR-C — set SWARPH_SPAWN=1 env so a SessionStart hook
518
- # installed via `swarph install-hook` knows the prompt was
519
- # already injected via --append-system-prompt and skips
520
- # double-injection. The env propagates through execv since we
521
- # don't use execve with a custom env.
522
- os.environ["SWARPH_SPAWN"] = "1"
523
-
524
- # exec-replace so the spawned claude session owns stdio +
525
- # signals cleanly. argv[0] is preserved as 'claude' for ps-grep.
526
- try:
527
- os.execv(claude_bin, claude_argv)
747
+ if cell.provider == "codex":
748
+ os.execve(provider_bin, spawn_argv, _scrubbed_codex_env())
749
+ else:
750
+ os.execv(provider_bin, spawn_argv)
528
751
  except OSError as exc:
529
752
  # execv only returns on failure.
530
753
  print(f"swarph spawn: exec failed: {exc}", file=sys.stderr)
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.7.9
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.9 reverts the v0.7.8 pyreadline3 dependency + chat-REPL shim: the Windows 'Enter renders m' symptom traced to Claude Code's own native-Windows TUI (no hooks, recent build, fails at the trust prompt — Anthropic-side), NOT this REPL, so the shim addressed the wrong layer. Bare input() + a CR-strip guard remain.
3
+ Version: 0.8.0
4
+ Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration: one-shot + chat REPL, multi-provider routing, cell.yaml spawn, session import, watchdog. v0.8.0: multi-provider spawn membrane `swarph spawn` launches claude, codex (GPT), or antigravity/agy (Gemini) per cell.provider, each with billing-scrub + sandbox; plus the Windows conhost auto-relaunch into Windows Terminal (the Enter-inserts-'m' TUI fix).
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/darw007d/swarph-cli
@@ -40,4 +40,5 @@ tests/test_smoke_chat.py
40
40
  tests/test_smoke_one_shot.py
41
41
  tests/test_smoke_phase_5_5.py
42
42
  tests/test_spawn_command.py
43
+ tests/test_spawn_windows_relaunch.py
43
44
  tests/test_watchdog.py