swarph-cli 0.5.0__tar.gz → 0.7.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 (42) hide show
  1. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/PKG-INFO +65 -9
  2. swarph_cli-0.5.0/src/swarph_cli.egg-info/PKG-INFO → swarph_cli-0.7.0/README.md +61 -38
  3. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/pyproject.toml +12 -3
  4. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/__init__.py +1 -1
  5. swarph_cli-0.7.0/src/swarph_cli/cell.py +360 -0
  6. swarph_cli-0.7.0/src/swarph_cli/commands/hook_output.py +123 -0
  7. swarph_cli-0.7.0/src/swarph_cli/commands/install_hook.py +313 -0
  8. swarph_cli-0.7.0/src/swarph_cli/commands/spawn.py +407 -0
  9. swarph_cli-0.7.0/src/swarph_cli/commands/watchdog.py +404 -0
  10. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/main.py +15 -2
  11. swarph_cli-0.5.0/README.md → swarph_cli-0.7.0/src/swarph_cli.egg-info/PKG-INFO +94 -6
  12. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli.egg-info/SOURCES.txt +11 -1
  13. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli.egg-info/requires.txt +2 -1
  14. swarph_cli-0.7.0/tests/test_cell_loader.py +500 -0
  15. swarph_cli-0.7.0/tests/test_hook_output.py +210 -0
  16. swarph_cli-0.7.0/tests/test_install_hook.py +269 -0
  17. swarph_cli-0.7.0/tests/test_spawn_command.py +341 -0
  18. swarph_cli-0.7.0/tests/test_watchdog.py +305 -0
  19. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/LICENSE +0 -0
  20. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/setup.cfg +0 -0
  21. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/caller.py +0 -0
  22. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/commands/__init__.py +0 -0
  23. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/commands/chat.py +0 -0
  24. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/commands/daemon.py +0 -0
  25. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/commands/import_session.py +0 -0
  26. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/commands/onboard.py +0 -0
  27. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/commands/ratify.py +0 -0
  28. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/parsers/__init__.py +0 -0
  29. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/parsers/claude.py +0 -0
  30. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  31. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  32. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
  33. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_chat_command.py +0 -0
  34. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_claude_parser.py +0 -0
  35. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_daemon_command.py +0 -0
  36. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_import_command.py +0 -0
  37. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_main.py +0 -0
  38. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_onboard_command.py +0 -0
  39. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_ratify_command.py +0 -0
  40. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_smoke_chat.py +0 -0
  41. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_smoke_one_shot.py +0 -0
  42. {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_smoke_phase_5_5.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.5.0
4
- Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.5.0 ships Phase 5.6 `swarph daemon` (foreground inbox drain retires the orphaned-tail-F class) on top of Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL + Phase 5.5 onboard/ratify (PLAN.md §13 / §16).
3
+ Version: 0.7.0
4
+ Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.7.0 ships Phase 7 substrate-doc R7 §11.1.7 operator-tooling layer in 5 increments: PR-A `--new-instance` flag (sibling-spawn case) + PR-B auto-suffix on collision (sibling-slot persistence) + PR-C SessionStart hook (closes bare-claude operator-paste gap) + watchdog (stranded-session recovery) + PR-D swarph-shared cell.yaml relocation (cell-yaml schema graduates to swarph-shared 0.3.0 kernel-tier; substrate-doc R7 §11.1.5 (O5) RESOLVED).
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/darw007d/swarph-cli
@@ -25,7 +25,8 @@ Requires-Python: >=3.10
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
27
  Requires-Dist: swarph-mesh>=0.5.0
28
- Requires-Dist: swarph-shared>=0.2.0
28
+ Requires-Dist: swarph-shared>=0.3.0
29
+ Requires-Dist: PyYAML>=6.0
29
30
  Provides-Extra: dev
30
31
  Requires-Dist: pytest>=7.0; extra == "dev"
31
32
  Dynamic: license-file
@@ -49,16 +50,71 @@ This is one of three repos in the v0.3.x architecture:
49
50
 
50
51
  ## Status
51
52
 
52
- **v0.5.0 — Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL + Phase 5.5 onboard/ratify + Phase 5.6 daemon.** Six verbs ship:
53
+ **v0.6.0 — Phase 7 spawn ships.** Seven verbs total:
53
54
 
54
55
  1. `swarph "prompt"` — Phase 2 one-shot mode (any of five providers)
55
56
  2. `swarph chat` — Phase 5 interactive REPL with multi-turn history + slash commands
56
- 3. `swarph import <path>` — Phase 2.5 session import (Claude JSONL swarph-native, with `--report-only` for honest pre-commit inspection)
57
- 4. `swarph onboard <peer-name>` — Phase 5.5 mechanics-phase onboarding (PLAN.md §15.4)
58
- 5. `swarph ratify <peer-name>` — Phase 5.5 witness ratification (PLAN.md §15.4a)
59
- 6. `swarph daemon`**NEW** Phase 5.6 foreground inbox drain loop (PLAN.md §16); structurally retires the orphaned-tail-F class
57
+ 3. `swarph spawn <role>` — **NEW** Phase 7 long-lived `claude` session as a named mesh cell (`--name`/`--session-id`/`--append-system-prompt` pinning per substrate-doc R7 §11.1.7 4-layer R2 stack)
58
+ 4. `swarph import <path>` — Phase 2.5 session import (Claude JSONL → swarph-native)
59
+ 5. `swarph onboard <peer-name>` — Phase 5.5 mechanics-phase onboarding (PLAN.md §15.4)
60
+ 6. `swarph ratify <peer-name>` — Phase 5.5 witness ratification (PLAN.md §15.4a)
61
+ 7. `swarph daemon` — Phase 5.6 foreground inbox drain (PLAN.md §16)
60
62
 
61
- Subsequent phases extend the CLI surface (`--ask <peer>`, REPL drain coroutine + `/inbox` + `/reply` slash commands in 5.6b).
63
+ Subsequent phases extend the CLI surface (`--ask <peer>`, REPL drain coroutine + `/inbox` + `/reply` slash commands in 5.6b; non-Claude `spawn` providers + S-G `mesh-gateway://` URL form in v0.7).
64
+
65
+ ### `swarph spawn` (Phase 7 — v0.6.0)
66
+
67
+ Operator-tooling layer of substrate-doc R7 §11.1.7 4-layer R2 mechanism stack. Wraps `claude` with the three R5/R7 disambiguation flags:
68
+
69
+ * `--name <role>` — display name for `/resume` picker
70
+ * `--session-id <uuid>` — pinned UUID, persisted to `$XDG_STATE_HOME/swarph/sessions/<role>.session-id` so re-spawns reuse the same session (the R5 fix at the operator-tooling layer)
71
+ * `--append-system-prompt <text>` — starter prompt injected without manual paste (the R2 fix at the operator-tooling layer)
72
+
73
+ ```bash
74
+ # 1. Author a cell.yaml (one-time per role)
75
+ $ cat ~/.config/swarph/cells/lab.yaml
76
+ schema_version: v1
77
+ name: lab-ovh
78
+ role: lab
79
+ cwd: /home/ubuntu
80
+ starter_prompt_path: ~/.claude/session_start_reminder.txt
81
+ provider: claude
82
+
83
+ # 2. Summon the cell (long-lived claude session, exec-replaced)
84
+ $ swarph spawn lab
85
+ ╭───╮
86
+ │ ◉ │
87
+ ╭──┴───┴──╮
88
+ │ swarph │ v0.6.0
89
+ ╰──┬───┬──╯ spawn │ chat │ daemon
90
+ │ ◉ │
91
+ ╰───╯
92
+ [claude session takes over the terminal — same flags as `claude --name lab --session-id <uuid> --append-system-prompt <starter>`]
93
+
94
+ # 3. Resume the same cell after exit — same UUID, same session
95
+ $ swarph spawn lab # picker shows ONE entry: "lab" (R5 disambiguation)
96
+ ```
97
+
98
+ Resolution order for `swarph spawn <role-or-path>`:
99
+
100
+ 1. `--onboarding <path-or-url>` (alias: `--cell`) — explicit override
101
+ 2. Positional ending in `.yaml`/`.yml` or containing a path separator — literal path
102
+ 3. Plain role name — `$XDG_CONFIG_HOME/swarph/cells/<role>.yaml` (default `~/.config/swarph/cells/`)
103
+ 4. No positional given — auto-discover `./cell.yaml` in current directory
104
+
105
+ Useful flags:
106
+
107
+ | Flag | Effect |
108
+ |---|---|
109
+ | `--dry-run` | Print resolved `claude` command + cell summary; do not exec |
110
+ | `--no-starter` | Skip starter-prompt injection even if cell.yaml sets one |
111
+ | `--print-id` | Print resolved session-id to stdout (capture for shell scripts) |
112
+ | `--no-banner` | Suppress the swarph banner on stderr |
113
+ | `-- <claude-args>` | Pass remaining args through to claude unchanged |
114
+
115
+ cell.yaml schema is **frozen at `schema_version: "v1"`**. v0.7 migrates the parser to `swarph-shared` as a symbol-relocation only — v0.6 cell.yaml files keep working unchanged. Breaking changes require a `schema_version: "v2"` bump and parallel-supported-version window per `swarph-mesh` DEPRECATIONS discipline.
116
+
117
+ **Known limitations (v0.6).** Single-instance-per-role only. Re-running `swarph spawn <role>` reuses the persisted UUID (R5 fix), so sibling-spawn (alpha + beta co-existing on the same peer-id) requires v0.7's `--new-instance` flag. Manual sibling spawning via `tmux` + explicit `--session-id` pinning still works unchanged; v0.6 does not regress that path, it just doesn't yet expose a CLI shape for it.
62
118
 
63
119
  ### `swarph daemon` (Phase 5.6)
64
120
 
@@ -1,35 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: swarph-cli
3
- Version: 0.5.0
4
- Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.5.0 ships Phase 5.6 `swarph daemon` (foreground inbox drain — retires the orphaned-tail-F class) on top of Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL + Phase 5.5 onboard/ratify (PLAN.md §13 / §16).
5
- Author: Pierre Samson, Claude Opus
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/darw007d/swarph-cli
8
- Project-URL: Source, https://github.com/darw007d/swarph-cli
9
- Project-URL: Substrate, https://github.com/darw007d/swarph-mesh
10
- Project-URL: Spec, https://github.com/darw007d/hedge-fund-mcp/blob/main/research/swarph_cli/PLAN.md
11
- Keywords: swarph,llm,cli,mesh,gemini,claude,deepseek
12
- Classifier: Development Status :: 3 - Alpha
13
- Classifier: Environment :: Console
14
- Classifier: Intended Audience :: Developers
15
- Classifier: License :: OSI Approved :: MIT License
16
- Classifier: Operating System :: POSIX :: Linux
17
- Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Programming Language :: Python :: 3.13
22
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
- Classifier: Topic :: Utilities
24
- Requires-Python: >=3.10
25
- Description-Content-Type: text/markdown
26
- License-File: LICENSE
27
- Requires-Dist: swarph-mesh>=0.5.0
28
- Requires-Dist: swarph-shared>=0.2.0
29
- Provides-Extra: dev
30
- Requires-Dist: pytest>=7.0; extra == "dev"
31
- Dynamic: license-file
32
-
33
1
  # swarph-cli
34
2
 
35
3
  The `swarph` binary — multi-LLM CLI with mesh-gateway integration. Thin client over the [`swarph-mesh`](https://github.com/darw007d/swarph-mesh) substrate.
@@ -49,16 +17,71 @@ This is one of three repos in the v0.3.x architecture:
49
17
 
50
18
  ## Status
51
19
 
52
- **v0.5.0 — Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL + Phase 5.5 onboard/ratify + Phase 5.6 daemon.** Six verbs ship:
20
+ **v0.6.0 — Phase 7 spawn ships.** Seven verbs total:
53
21
 
54
22
  1. `swarph "prompt"` — Phase 2 one-shot mode (any of five providers)
55
23
  2. `swarph chat` — Phase 5 interactive REPL with multi-turn history + slash commands
56
- 3. `swarph import <path>` — Phase 2.5 session import (Claude JSONL swarph-native, with `--report-only` for honest pre-commit inspection)
57
- 4. `swarph onboard <peer-name>` — Phase 5.5 mechanics-phase onboarding (PLAN.md §15.4)
58
- 5. `swarph ratify <peer-name>` — Phase 5.5 witness ratification (PLAN.md §15.4a)
59
- 6. `swarph daemon`**NEW** Phase 5.6 foreground inbox drain loop (PLAN.md §16); structurally retires the orphaned-tail-F class
24
+ 3. `swarph spawn <role>` — **NEW** Phase 7 long-lived `claude` session as a named mesh cell (`--name`/`--session-id`/`--append-system-prompt` pinning per substrate-doc R7 §11.1.7 4-layer R2 stack)
25
+ 4. `swarph import <path>` — Phase 2.5 session import (Claude JSONL → swarph-native)
26
+ 5. `swarph onboard <peer-name>` — Phase 5.5 mechanics-phase onboarding (PLAN.md §15.4)
27
+ 6. `swarph ratify <peer-name>` — Phase 5.5 witness ratification (PLAN.md §15.4a)
28
+ 7. `swarph daemon` — Phase 5.6 foreground inbox drain (PLAN.md §16)
29
+
30
+ Subsequent phases extend the CLI surface (`--ask <peer>`, REPL drain coroutine + `/inbox` + `/reply` slash commands in 5.6b; non-Claude `spawn` providers + S-G `mesh-gateway://` URL form in v0.7).
31
+
32
+ ### `swarph spawn` (Phase 7 — v0.6.0)
33
+
34
+ Operator-tooling layer of substrate-doc R7 §11.1.7 4-layer R2 mechanism stack. Wraps `claude` with the three R5/R7 disambiguation flags:
35
+
36
+ * `--name <role>` — display name for `/resume` picker
37
+ * `--session-id <uuid>` — pinned UUID, persisted to `$XDG_STATE_HOME/swarph/sessions/<role>.session-id` so re-spawns reuse the same session (the R5 fix at the operator-tooling layer)
38
+ * `--append-system-prompt <text>` — starter prompt injected without manual paste (the R2 fix at the operator-tooling layer)
39
+
40
+ ```bash
41
+ # 1. Author a cell.yaml (one-time per role)
42
+ $ cat ~/.config/swarph/cells/lab.yaml
43
+ schema_version: v1
44
+ name: lab-ovh
45
+ role: lab
46
+ cwd: /home/ubuntu
47
+ starter_prompt_path: ~/.claude/session_start_reminder.txt
48
+ provider: claude
49
+
50
+ # 2. Summon the cell (long-lived claude session, exec-replaced)
51
+ $ swarph spawn lab
52
+ ╭───╮
53
+ │ ◉ │
54
+ ╭──┴───┴──╮
55
+ │ swarph │ v0.6.0
56
+ ╰──┬───┬──╯ spawn │ chat │ daemon
57
+ │ ◉ │
58
+ ╰───╯
59
+ [claude session takes over the terminal — same flags as `claude --name lab --session-id <uuid> --append-system-prompt <starter>`]
60
+
61
+ # 3. Resume the same cell after exit — same UUID, same session
62
+ $ swarph spawn lab # picker shows ONE entry: "lab" (R5 disambiguation)
63
+ ```
64
+
65
+ Resolution order for `swarph spawn <role-or-path>`:
66
+
67
+ 1. `--onboarding <path-or-url>` (alias: `--cell`) — explicit override
68
+ 2. Positional ending in `.yaml`/`.yml` or containing a path separator — literal path
69
+ 3. Plain role name — `$XDG_CONFIG_HOME/swarph/cells/<role>.yaml` (default `~/.config/swarph/cells/`)
70
+ 4. No positional given — auto-discover `./cell.yaml` in current directory
71
+
72
+ Useful flags:
73
+
74
+ | Flag | Effect |
75
+ |---|---|
76
+ | `--dry-run` | Print resolved `claude` command + cell summary; do not exec |
77
+ | `--no-starter` | Skip starter-prompt injection even if cell.yaml sets one |
78
+ | `--print-id` | Print resolved session-id to stdout (capture for shell scripts) |
79
+ | `--no-banner` | Suppress the swarph banner on stderr |
80
+ | `-- <claude-args>` | Pass remaining args through to claude unchanged |
81
+
82
+ cell.yaml schema is **frozen at `schema_version: "v1"`**. v0.7 migrates the parser to `swarph-shared` as a symbol-relocation only — v0.6 cell.yaml files keep working unchanged. Breaking changes require a `schema_version: "v2"` bump and parallel-supported-version window per `swarph-mesh` DEPRECATIONS discipline.
60
83
 
61
- Subsequent phases extend the CLI surface (`--ask <peer>`, REPL drain coroutine + `/inbox` + `/reply` slash commands in 5.6b).
84
+ **Known limitations (v0.6).** Single-instance-per-role only. Re-running `swarph spawn <role>` reuses the persisted UUID (R5 fix), so sibling-spawn (alpha + beta co-existing on the same peer-id) requires v0.7's `--new-instance` flag. Manual sibling spawning via `tmux` + explicit `--session-id` pinning still works unchanged; v0.6 does not regress that path, it just doesn't yet expose a CLI shape for it.
62
85
 
63
86
  ### `swarph daemon` (Phase 5.6)
64
87
 
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swarph-cli"
7
- version = "0.5.0"
8
- description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.5.0 ships Phase 5.6 `swarph daemon` (foreground inbox drain retires the orphaned-tail-F class) on top of Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL + Phase 5.5 onboard/ratify (PLAN.md §13 / §16)."
7
+ version = "0.7.0"
8
+ description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.7.0 ships Phase 7 substrate-doc R7 §11.1.7 operator-tooling layer in 5 increments: PR-A `--new-instance` flag (sibling-spawn case) + PR-B auto-suffix on collision (sibling-slot persistence) + PR-C SessionStart hook (closes bare-claude operator-paste gap) + watchdog (stranded-session recovery) + PR-D swarph-shared cell.yaml relocation (cell-yaml schema graduates to swarph-shared 0.3.0 kernel-tier; substrate-doc R7 §11.1.5 (O5) RESOLVED)."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.10"
@@ -32,7 +32,16 @@ dependencies = [
32
32
  # Phase 5 REPL exercises all five adapters; bumped to 0.5.0 for
33
33
  # OpenAI + Grok availability in /provider switch.
34
34
  "swarph-mesh>=0.5.0",
35
- "swarph-shared>=0.2.0",
35
+ # v0.7 PR-D step 2: swarph-shared 0.3.0 adds the cell module
36
+ # (universal cell.yaml schema; substrate-doc R7 §11.1.5 (O5)
37
+ # cell.yaml-format-home RESOLVED). swarph-cli's cell.py now
38
+ # re-exports data shapes from swarph_shared.cell + keeps file
39
+ # I/O + sidecar + slot allocation locally.
40
+ "swarph-shared>=0.3.0",
41
+ # Phase 7 spawn — cell.yaml parser. PyYAML 6.x is the standard.
42
+ # Stays a swarph-cli dep since file I/O is operator-tooling-layer
43
+ # concern; swarph-shared cell module is pure-stdlib.
44
+ "PyYAML>=6.0",
36
45
  ]
37
46
 
38
47
  [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.5.0"
19
+ __version__ = "0.7.0"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -0,0 +1,360 @@
1
+ """``swarph_cli.cell`` — file I/O + sidecar + slot allocation (v0.7 PR-D step 2).
2
+
3
+ After substrate-doc R7 §11.1.5 (O5) cell.yaml universal-genome relocation,
4
+ the data shapes + schema validation live in ``swarph-shared`` (v0.3.0+).
5
+ This module is the swarph-cli operator-tooling layer that consumes the
6
+ shared schema + adds:
7
+
8
+ * **File discovery**: ``cells_dir()``, ``discover_cell_in_cwd()``,
9
+ ``resolve_cell_path()``, ``is_mesh_gateway_url()``
10
+ * **File I/O**: ``load_cell()`` (YAML read → ``parse_cell_dict()``),
11
+ ``_atomic_write_text()``
12
+ * **Sidecar persistence**: ``session_state_path()``,
13
+ ``load_or_create_session_id()``
14
+ * **Slot allocation** (substrate-doc R7 §11.1.7 operator-tooling layer
15
+ per beta #892 B1): ``next_free_slot_role()``,
16
+ ``base_role_from_slot_role()``
17
+
18
+ Symbol-relocation only — the v0.6 + v0.7-pre-PR-D + v0.7-post-PR-D-step-2
19
+ APIs are byte-for-byte identical at the swarph_cli.cell import level.
20
+ v0.6 cell.yaml files keep working unchanged in v0.7+ per drop-mother
21
+ review #890 (C2) schema-stability commitment.
22
+
23
+ Re-export shim for backward-compat — historical imports still work:
24
+
25
+ from swarph_cli.cell import Cell, CellError, parse_cell_dict
26
+
27
+ is equivalent to (and recommended going forward):
28
+
29
+ from swarph_shared.cell import Cell, CellError, parse_cell_dict
30
+
31
+ Internal swarph-cli code uses the swarph-shared imports directly to
32
+ avoid two-hop indirection. External consumers of swarph-cli's cell
33
+ module continue to work unchanged.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import os
39
+ import uuid
40
+ from pathlib import Path
41
+ from typing import Optional
42
+
43
+ # Re-export data shapes + schema validation from swarph-shared 0.3.0+
44
+ # (substrate-doc R7 §11.1.5 (O5) cell.yaml-format-home RESOLVED).
45
+ from swarph_shared.cell import (
46
+ Cell,
47
+ CellError,
48
+ Lineage,
49
+ PEER_NAME_RE,
50
+ SCHEMA_VERSION_V1,
51
+ VALID_PROVIDERS,
52
+ VALID_SCHEMA_VERSIONS,
53
+ parse_cell_dict,
54
+ validate_uuid_str,
55
+ )
56
+
57
+ # Backward-compat aliases for v0.6 + v0.7-pre-PR-D-step-2 imports
58
+ # (a few internal swarph-cli call sites used the leading-underscore shape).
59
+ _PEER_NAME_RE = PEER_NAME_RE
60
+ _VALID_SCHEMA_VERSIONS = VALID_SCHEMA_VERSIONS
61
+ _VALID_PROVIDERS_V0_6 = VALID_PROVIDERS # historical name for the v0.6 frozenset
62
+ _validate_uuid = validate_uuid_str # historical helper name
63
+
64
+
65
+ def _config_root() -> Path:
66
+ """Return the active config root for cell lookups.
67
+
68
+ Honours ``$XDG_CONFIG_HOME`` per the XDG Base Directory spec; falls
69
+ back to ``~/.config/`` otherwise.
70
+ """
71
+ xdg = os.environ.get("XDG_CONFIG_HOME", "").strip()
72
+ if xdg:
73
+ return Path(xdg)
74
+ return Path.home() / ".config"
75
+
76
+
77
+ def cells_dir() -> Path:
78
+ """Default lookup directory for ``<role>.yaml`` files."""
79
+ return _config_root() / "swarph" / "cells"
80
+
81
+
82
+ def _state_root() -> Path:
83
+ xdg = os.environ.get("XDG_STATE_HOME", "").strip()
84
+ if xdg:
85
+ return Path(xdg)
86
+ return Path.home() / ".local" / "state"
87
+
88
+
89
+ def session_state_path(role: str) -> Path:
90
+ """Return the per-role persisted-session-id file path.
91
+
92
+ v0.6 persists generated UUIDs OUTSIDE cell.yaml so the cell file
93
+ stays purely declarative and is safe to commit to git. Roles
94
+ re-spawned with the same role name resume the same session via
95
+ this state file.
96
+
97
+ v0.7 PR-B (beta #892 B1) extends this to per-slot sidecars for
98
+ sibling instances: ``<role>.session-id`` is slot 1; ``<role>-2``,
99
+ ``<role>-3`` etc. are siblings minted via ``--new-instance``.
100
+ See ``next_free_slot_role()`` for the slot allocation policy.
101
+ """
102
+ return _state_root() / "swarph" / "sessions" / f"{role}.session-id"
103
+
104
+
105
+ def next_free_slot_role(base_role: str) -> str:
106
+ """Find the next free `<base_role>-N` slot for a sibling spawn.
107
+
108
+ v0.7 PR-B (beta #892 B1). Auto-suffix policy: siblings beyond
109
+ the first slot append ``-2``, ``-3``, ``-4`` etc. The naming
110
+ suffix matches the slot index, so:
111
+
112
+ * Slot 1 (the original): ``<base_role>``
113
+ * Slot 2 (first sibling): ``<base_role>-2``
114
+ * Slot 3 (second sibling): ``<base_role>-3``
115
+
116
+ Returns the synthesised role string for the next free slot.
117
+
118
+ Hard cap at slot 99 to avoid runaway loops.
119
+
120
+ Slot-reuse on unclean exit (beta iter-1 #987): a sibling instance
121
+ that dies without removing its sidecar leaves the slot
122
+ sidecar-occupied. Operator workaround at v0.7: ``rm`` the stale
123
+ sidecar. v0.8+ may ship ``swarph cleanup-sessions``.
124
+ """
125
+ for n in range(2, 100):
126
+ candidate = f"{base_role}-{n}"
127
+ if not session_state_path(candidate).exists():
128
+ return candidate
129
+ raise CellError(
130
+ f"next_free_slot_role: 99 sibling slots already occupied for "
131
+ f"base role {base_role!r}. Manual cleanup needed at "
132
+ f"{_state_root() / 'swarph' / 'sessions'}/."
133
+ )
134
+
135
+
136
+ def base_role_from_slot_role(role: str) -> str:
137
+ """Strip a trailing ``-N`` slot suffix from a role string.
138
+
139
+ v0.7 PR-B. Lets ``swarph spawn <base-role>-2`` resolve back to
140
+ the cell.yaml at ``<base-role>.yaml`` for shared cell-context
141
+ (same cwd, starter-prompt, lineage, etc.) while the sidecar
142
+ + display name use the slot-suffixed role.
143
+
144
+ Returns ``role`` unchanged if no trailing ``-<N>`` suffix is
145
+ present (preserves v0.6 behavior for non-sibling spawns).
146
+ """
147
+ parts = role.rsplit("-", 1)
148
+ if len(parts) == 2 and parts[1].isdigit() and 2 <= int(parts[1]) <= 99:
149
+ return parts[0]
150
+ return role
151
+
152
+
153
+ _MESH_GATEWAY_URL_PREFIX = "mesh-gateway://"
154
+
155
+
156
+ def is_mesh_gateway_url(spec: str) -> bool:
157
+ """True for v0.7+ ``mesh-gateway://...`` URL inputs (alpha #891 D2)."""
158
+ return spec.startswith(_MESH_GATEWAY_URL_PREFIX)
159
+
160
+
161
+ def resolve_cell_path(spec: str) -> Path:
162
+ """Resolve a ``swarph spawn`` positional/flag value to a cell file.
163
+
164
+ Precedence:
165
+ 1. spec ends in ``.yaml`` or ``.yml`` → treated as a literal path
166
+ 2. spec contains a path separator → treated as a literal path
167
+ 3. ``./cell.yaml`` exists in current cwd AND spec equals current cwd's
168
+ basename OR equals a special token ``.`` → use ``./cell.yaml``
169
+ 4. ``<cells_dir>/<spec>.yaml`` exists → use it
170
+ 5. v0.7 PR-B sibling-resume — strip trailing ``-N`` suffix from spec
171
+ and try ``<cells_dir>/<base-role>.yaml`` (lets ``swarph spawn
172
+ drop-on-meta-edge-2`` resolve back to base cell.yaml so siblings
173
+ share cell-context). Honors operator-intent: explicit base file
174
+ takes precedence (step 4) over slot-stripped fallback (step 5).
175
+ 6. otherwise → return ``<cells_dir>/<spec>.yaml`` (will fail on load
176
+ with a 'not found' error)
177
+ """
178
+ if spec == ".":
179
+ return Path.cwd() / "cell.yaml"
180
+ if spec.endswith((".yaml", ".yml")) or os.sep in spec:
181
+ return Path(spec).expanduser()
182
+
183
+ direct = cells_dir() / f"{spec}.yaml"
184
+ if direct.is_file():
185
+ return direct
186
+
187
+ # v0.7 PR-B sibling-resume fallback
188
+ base = base_role_from_slot_role(spec)
189
+ if base != spec:
190
+ base_path = cells_dir() / f"{base}.yaml"
191
+ if base_path.is_file():
192
+ return base_path
193
+
194
+ return direct # not found; let load_cell raise with a clear error
195
+
196
+
197
+ def discover_cell_in_cwd() -> Optional[Path]:
198
+ """Return ``./cell.yaml`` if it exists in the current cwd, else None."""
199
+ candidate = Path.cwd() / "cell.yaml"
200
+ return candidate if candidate.is_file() else None
201
+
202
+
203
+ def read_starter_prompt(cell: Cell) -> Optional[str]:
204
+ """Return the starter-prompt text for ``cell``, or None.
205
+
206
+ Free function rather than ``Cell.starter_prompt_text()`` method —
207
+ swarph-shared Cell is intentionally pure-stdlib + no I/O, so the
208
+ file-read lives at the swarph-cli operator-tooling layer. Callers
209
+ that previously did ``cell.starter_prompt_text()`` now do
210
+ ``read_starter_prompt(cell)``.
211
+
212
+ Raises CellError if starter_prompt_path is set but unreadable, so
213
+ spawn fails loudly rather than silently dropping role-priming.
214
+ """
215
+ if cell.starter_prompt_path is None:
216
+ return None
217
+ try:
218
+ return cell.starter_prompt_path.read_text(encoding="utf-8")
219
+ except OSError as exc:
220
+ raise CellError(
221
+ f"cell.yaml: starter_prompt_path "
222
+ f"'{cell.starter_prompt_path}' is not readable: {exc}"
223
+ ) from exc
224
+
225
+
226
+ def load_cell(path: Path) -> Cell:
227
+ """Parse + validate a cell.yaml file. Raises CellError on any failure.
228
+
229
+ Reads YAML from disk, then delegates to ``swarph_shared.cell.parse_cell_dict``
230
+ for the actual schema validation. Sets ``cell.source_path`` to the
231
+ file path (a swarph-cli-specific provenance bit; swarph-shared
232
+ leaves it None since it doesn't know about the file).
233
+ """
234
+ import yaml # local import — keeps `swarph --version` PyYAML-free
235
+
236
+ if not path.exists():
237
+ raise CellError(f"cell.yaml not found: {path}")
238
+
239
+ try:
240
+ raw = yaml.safe_load(path.read_text(encoding="utf-8"))
241
+ except yaml.YAMLError as exc:
242
+ raise CellError(f"cell.yaml is not valid YAML ({path}): {exc}") from exc
243
+
244
+ cell = parse_cell_dict(raw, source=str(path), base_dir=path.parent)
245
+ # File-I/O wrapper concern: post-validate cwd reachability + tag source
246
+ # path. swarph-shared parse_cell_dict intentionally doesn't touch the
247
+ # filesystem (kernel-tier discipline); the live cwd.is_dir() check is
248
+ # the swarph-cli operator-tooling concern.
249
+ if not cell.cwd.is_dir():
250
+ raise CellError(f"cell.yaml: 'cwd' is not a directory: {cell.cwd}")
251
+ cell.source_path = path
252
+ return cell
253
+
254
+
255
+ def load_or_create_session_id(
256
+ role: str,
257
+ cell: Cell,
258
+ new_instance: bool = False,
259
+ ) -> tuple[str, bool, str]:
260
+ """Resolve the session-id for a spawn invocation.
261
+
262
+ Returns ``(session_id, was_generated, effective_role)``.
263
+
264
+ Resolution order:
265
+ 1. cell.session_id (cell.yaml-pinned) — never generated
266
+ 2. ``new_instance=True`` AND base sidecar exists — auto-suffix slot
267
+ 3. ``new_instance=True`` AND no base sidecar — fall through (degenerate)
268
+ 4. session_state_path(role) reused — default re-resume path
269
+ 5. mint new uuid4 + persist
270
+
271
+ Caller-side discipline (mother iter-1 #986): the ``effective_role``
272
+ value is the authoritative source for ``claude --name`` AND
273
+ sidecar persistence — NOT ``cell.role`` (which stays the BASE role
274
+ for shared cell-context: cwd, starter prompt, lineage, provider).
275
+ """
276
+ if cell.session_id:
277
+ return cell.session_id, False, role
278
+
279
+ if new_instance:
280
+ base_state = session_state_path(role)
281
+ if base_state.exists():
282
+ sibling_role = next_free_slot_role(role)
283
+ sibling_state = session_state_path(sibling_role)
284
+ new_id = str(uuid.uuid4())
285
+ sibling_state.parent.mkdir(parents=True, exist_ok=True)
286
+ _atomic_write_text(sibling_state, new_id + "\n")
287
+ return new_id, True, sibling_role
288
+
289
+ state_file = session_state_path(role)
290
+ if state_file.exists():
291
+ existing = state_file.read_text(encoding="utf-8").strip()
292
+ if existing:
293
+ try:
294
+ return validate_uuid_str(existing), False, role
295
+ except CellError:
296
+ # Corrupted state — fall through and regenerate.
297
+ pass
298
+
299
+ new_id = str(uuid.uuid4())
300
+ state_file.parent.mkdir(parents=True, exist_ok=True)
301
+ _atomic_write_text(state_file, new_id + "\n")
302
+ return new_id, True, role
303
+
304
+
305
+ def _atomic_write_text(target: Path, content: str) -> None:
306
+ """Write text atomically: tempfile in the same dir, fsync, rename.
307
+
308
+ Per drop-mother review #890 (C1) — UUID writes are load-bearing for
309
+ R5 (session-resume identity disambiguation). A torn write that left
310
+ half a UUID in the state file would silently regenerate on next
311
+ spawn, defeating the disambiguation primitive entirely.
312
+ """
313
+ import tempfile
314
+
315
+ parent = target.parent
316
+ fd, tmp_path = tempfile.mkstemp(
317
+ prefix=f".{target.name}.", suffix=".tmp", dir=parent
318
+ )
319
+ try:
320
+ with os.fdopen(fd, "w", encoding="utf-8") as fp:
321
+ fp.write(content)
322
+ fp.flush()
323
+ os.fsync(fp.fileno())
324
+ os.replace(tmp_path, target)
325
+ except Exception:
326
+ try:
327
+ os.unlink(tmp_path)
328
+ except OSError:
329
+ pass
330
+ raise
331
+
332
+
333
+ __all__ = [
334
+ # Re-exports from swarph_shared.cell (v0.3.0+)
335
+ "Cell",
336
+ "CellError",
337
+ "Lineage",
338
+ "PEER_NAME_RE",
339
+ "SCHEMA_VERSION_V1",
340
+ "VALID_PROVIDERS",
341
+ "VALID_SCHEMA_VERSIONS",
342
+ "parse_cell_dict",
343
+ "validate_uuid_str",
344
+ # swarph-cli-local file I/O + sidecar + slot allocation
345
+ "cells_dir",
346
+ "session_state_path",
347
+ "next_free_slot_role",
348
+ "base_role_from_slot_role",
349
+ "is_mesh_gateway_url",
350
+ "resolve_cell_path",
351
+ "discover_cell_in_cwd",
352
+ "read_starter_prompt",
353
+ "load_cell",
354
+ "load_or_create_session_id",
355
+ # Backward-compat aliases
356
+ "_PEER_NAME_RE",
357
+ "_VALID_SCHEMA_VERSIONS",
358
+ "_VALID_PROVIDERS_V0_6",
359
+ "_validate_uuid",
360
+ ]