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.
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/PKG-INFO +65 -9
- swarph_cli-0.5.0/src/swarph_cli.egg-info/PKG-INFO → swarph_cli-0.7.0/README.md +61 -38
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/pyproject.toml +12 -3
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/__init__.py +1 -1
- swarph_cli-0.7.0/src/swarph_cli/cell.py +360 -0
- swarph_cli-0.7.0/src/swarph_cli/commands/hook_output.py +123 -0
- swarph_cli-0.7.0/src/swarph_cli/commands/install_hook.py +313 -0
- swarph_cli-0.7.0/src/swarph_cli/commands/spawn.py +407 -0
- swarph_cli-0.7.0/src/swarph_cli/commands/watchdog.py +404 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/main.py +15 -2
- swarph_cli-0.5.0/README.md → swarph_cli-0.7.0/src/swarph_cli.egg-info/PKG-INFO +94 -6
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli.egg-info/SOURCES.txt +11 -1
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli.egg-info/requires.txt +2 -1
- swarph_cli-0.7.0/tests/test_cell_loader.py +500 -0
- swarph_cli-0.7.0/tests/test_hook_output.py +210 -0
- swarph_cli-0.7.0/tests/test_install_hook.py +269 -0
- swarph_cli-0.7.0/tests/test_spawn_command.py +341 -0
- swarph_cli-0.7.0/tests/test_watchdog.py +305 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/LICENSE +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/setup.cfg +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_import_command.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_main.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.5.0 → swarph_cli-0.7.0}/tests/test_smoke_one_shot.py +0 -0
- {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.
|
|
4
|
-
Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.
|
|
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.
|
|
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.
|
|
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
|
|
57
|
-
4. `swarph
|
|
58
|
-
5. `swarph
|
|
59
|
-
6. `swarph
|
|
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.
|
|
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
|
|
57
|
-
4. `swarph
|
|
58
|
-
5. `swarph
|
|
59
|
-
6. `swarph
|
|
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
|
-
|
|
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.
|
|
8
|
-
description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.
|
|
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
|
-
|
|
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]
|
|
@@ -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
|
+
]
|