swarph-cli 0.3.0__tar.gz → 0.4.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 (30) hide show
  1. {swarph_cli-0.3.0/src/swarph_cli.egg-info → swarph_cli-0.4.0}/PKG-INFO +38 -6
  2. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/README.md +36 -4
  3. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/pyproject.toml +2 -2
  4. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/__init__.py +1 -1
  5. swarph_cli-0.4.0/src/swarph_cli/commands/onboard.py +377 -0
  6. swarph_cli-0.4.0/src/swarph_cli/commands/ratify.py +283 -0
  7. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/main.py +10 -4
  8. {swarph_cli-0.3.0 → swarph_cli-0.4.0/src/swarph_cli.egg-info}/PKG-INFO +38 -6
  9. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/SOURCES.txt +6 -1
  10. swarph_cli-0.4.0/tests/test_onboard_command.py +279 -0
  11. swarph_cli-0.4.0/tests/test_ratify_command.py +224 -0
  12. swarph_cli-0.4.0/tests/test_smoke_phase_5_5.py +144 -0
  13. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/LICENSE +0 -0
  14. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/setup.cfg +0 -0
  15. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/caller.py +0 -0
  16. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/commands/__init__.py +0 -0
  17. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/commands/chat.py +0 -0
  18. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/commands/import_session.py +0 -0
  19. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/parsers/__init__.py +0 -0
  20. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/parsers/claude.py +0 -0
  21. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  22. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  23. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/requires.txt +0 -0
  24. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
  25. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/tests/test_chat_command.py +0 -0
  26. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/tests/test_claude_parser.py +0 -0
  27. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/tests/test_import_command.py +0 -0
  28. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/tests/test_main.py +0 -0
  29. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/tests/test_smoke_chat.py +0 -0
  30. {swarph_cli-0.3.0 → swarph_cli-0.4.0}/tests/test_smoke_one_shot.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.3.0
4
- Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.3.0 ships the Phase 5 `swarph chat` REPL on top of Phase 2 one-shot + Phase 2.5 import (PLAN.md §13).
3
+ Version: 0.4.0
4
+ Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.4.0 ships Phase 5.5 `swarph onboard` + `swarph ratify` on top of Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL (PLAN.md §13 / §15).
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/darw007d/swarph-cli
@@ -49,13 +49,45 @@ This is one of three repos in the v0.3.x architecture:
49
49
 
50
50
  ## Status
51
51
 
52
- **v0.3.0 — Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL.** Three verbs ship:
52
+ **v0.4.0 — Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL + Phase 5.5 onboard/ratify.** Five verbs ship:
53
53
 
54
54
  1. `swarph "prompt"` — Phase 2 one-shot mode (any of five providers)
55
55
  2. `swarph chat` — Phase 5 interactive REPL with multi-turn history + slash commands
56
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>` — **NEW** Phase 5.5 mechanics-phase onboarding (PLAN.md §15.4)
58
+ 5. `swarph ratify <peer-name>` — **NEW** Phase 5.5 witness ratification (PLAN.md §15.4a)
57
59
 
58
- Subsequent phases extend the CLI surface (`--ask <peer>`, onboard/ratify, daemon, additional source formats).
60
+ Subsequent phases extend the CLI surface (`--ask <peer>`, daemon).
61
+
62
+ ### `swarph onboard` + `swarph ratify` (Phase 5.5)
63
+
64
+ Per PLAN.md §15, onboarding splits into a **mechanics phase** (`swarph onboard`) that automates the boring parts (registry POST, scaffolding, token resolution) and a **manual contract phase** (the new peer composes the handshake DM in their own words). A witness peer judges the handshake and runs `swarph ratify <peer>` to flip `ratified=true`, gating `task_claim` server-side.
65
+
66
+ ```bash
67
+ # New peer self-onboards
68
+ $ swarph onboard razorpeter
69
+ [1/6] validate_node_name('razorpeter') ok
70
+ [2/6] prepare peer-registry row ok
71
+ [3/6] resolve MESH_GATEWAY_TOKEN ok
72
+ [4/6] POST .../peers/register ok (registered_unratified=true)
73
+ [5/6] verify_subscription_setup() ok
74
+ [6/6] scaffold ~/swarph_state/razorpeter/ ok
75
+
76
+ [manual] handshake template at /tmp/razorpeter-handshake.md
77
+ Edit each section in your own words, then send to your witness peer.
78
+
79
+ # After peer composes + sends handshake, witness ratifies
80
+ $ SWARPH_WITNESS=lab-ovh swarph ratify razorpeter \
81
+ --reason "handshake covers all four invariants in own words"
82
+ [1/6] validate_node_name('razorpeter') ok
83
+ [2/6] verify witness 'lab-ovh' is ratified ok
84
+ [3/6] verify 'razorpeter' is registered_unratified ok
85
+ [4/6] PATCH .../peers/razorpeter ok
86
+ [5/6] verify peer_ratifications audit row ok (id=N reason='...')
87
+ [6/6] invalidate local TTL cache ok
88
+ ```
89
+
90
+ Server-side gating (mesh-gateway PR A): unratified peers can read inbox + send DMs (so the handshake itself works) but `task_claim` returns 403. Witness must itself be ratified — no self-ratification, no unratified-witnesses-ratifying-others. Audit log (`peer_ratifications`) is append-only.
59
91
 
60
92
  ### `swarph chat`
61
93
 
@@ -158,9 +190,9 @@ Pong!
158
190
  | **0** | Scaffold — entry-point + status banner |
159
191
  | **2** (v0.1.0) | One-shot mode: `swarph "hello" --provider gemini` |
160
192
  | **2.5** (v0.2.0) | `swarph import` — Claude JSONL → swarph-native session format |
161
- | **5** (v0.3.0 — this release) | **`swarph chat` interactive REPL** — multi-turn against any of five adapters + slash commands (`/help`, `/clear`, `/system`, `/provider`, `/model`, `/history`, `/cost`, `/quit`) |
193
+ | **5** (v0.3.0) | `swarph chat` interactive REPL — multi-turn against any of five adapters + slash commands |
194
+ | **5.5** (v0.4.0 — this release) | **`swarph onboard <peer-name>` + `swarph ratify <peer-name>`** — six mechanics steps + handshake template + witness flip (PLAN.md §15) |
162
195
  | **3** | `--ask <peer>` mesh-aware one-shot via MeshClient |
163
- | **5.5** | `swarph onboard <peer-name>` + `swarph ratify <peer-name>` (PLAN.md §15) |
164
196
  | **5.6** | `swarph daemon` foreground drain loop + REPL drain coroutine + `/inbox`, `/reply` (PLAN.md §16) |
165
197
  | **6** | (already done) PyPI publish |
166
198
 
@@ -17,13 +17,45 @@ This is one of three repos in the v0.3.x architecture:
17
17
 
18
18
  ## Status
19
19
 
20
- **v0.3.0 — Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL.** Three verbs ship:
20
+ **v0.4.0 — Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL + Phase 5.5 onboard/ratify.** Five verbs ship:
21
21
 
22
22
  1. `swarph "prompt"` — Phase 2 one-shot mode (any of five providers)
23
23
  2. `swarph chat` — Phase 5 interactive REPL with multi-turn history + slash commands
24
24
  3. `swarph import <path>` — Phase 2.5 session import (Claude JSONL → swarph-native, with `--report-only` for honest pre-commit inspection)
25
+ 4. `swarph onboard <peer-name>` — **NEW** Phase 5.5 mechanics-phase onboarding (PLAN.md §15.4)
26
+ 5. `swarph ratify <peer-name>` — **NEW** Phase 5.5 witness ratification (PLAN.md §15.4a)
25
27
 
26
- Subsequent phases extend the CLI surface (`--ask <peer>`, onboard/ratify, daemon, additional source formats).
28
+ Subsequent phases extend the CLI surface (`--ask <peer>`, daemon).
29
+
30
+ ### `swarph onboard` + `swarph ratify` (Phase 5.5)
31
+
32
+ Per PLAN.md §15, onboarding splits into a **mechanics phase** (`swarph onboard`) that automates the boring parts (registry POST, scaffolding, token resolution) and a **manual contract phase** (the new peer composes the handshake DM in their own words). A witness peer judges the handshake and runs `swarph ratify <peer>` to flip `ratified=true`, gating `task_claim` server-side.
33
+
34
+ ```bash
35
+ # New peer self-onboards
36
+ $ swarph onboard razorpeter
37
+ [1/6] validate_node_name('razorpeter') ok
38
+ [2/6] prepare peer-registry row ok
39
+ [3/6] resolve MESH_GATEWAY_TOKEN ok
40
+ [4/6] POST .../peers/register ok (registered_unratified=true)
41
+ [5/6] verify_subscription_setup() ok
42
+ [6/6] scaffold ~/swarph_state/razorpeter/ ok
43
+
44
+ [manual] handshake template at /tmp/razorpeter-handshake.md
45
+ Edit each section in your own words, then send to your witness peer.
46
+
47
+ # After peer composes + sends handshake, witness ratifies
48
+ $ SWARPH_WITNESS=lab-ovh swarph ratify razorpeter \
49
+ --reason "handshake covers all four invariants in own words"
50
+ [1/6] validate_node_name('razorpeter') ok
51
+ [2/6] verify witness 'lab-ovh' is ratified ok
52
+ [3/6] verify 'razorpeter' is registered_unratified ok
53
+ [4/6] PATCH .../peers/razorpeter ok
54
+ [5/6] verify peer_ratifications audit row ok (id=N reason='...')
55
+ [6/6] invalidate local TTL cache ok
56
+ ```
57
+
58
+ Server-side gating (mesh-gateway PR A): unratified peers can read inbox + send DMs (so the handshake itself works) but `task_claim` returns 403. Witness must itself be ratified — no self-ratification, no unratified-witnesses-ratifying-others. Audit log (`peer_ratifications`) is append-only.
27
59
 
28
60
  ### `swarph chat`
29
61
 
@@ -126,9 +158,9 @@ Pong!
126
158
  | **0** | Scaffold — entry-point + status banner |
127
159
  | **2** (v0.1.0) | One-shot mode: `swarph "hello" --provider gemini` |
128
160
  | **2.5** (v0.2.0) | `swarph import` — Claude JSONL → swarph-native session format |
129
- | **5** (v0.3.0 — this release) | **`swarph chat` interactive REPL** — multi-turn against any of five adapters + slash commands (`/help`, `/clear`, `/system`, `/provider`, `/model`, `/history`, `/cost`, `/quit`) |
161
+ | **5** (v0.3.0) | `swarph chat` interactive REPL — multi-turn against any of five adapters + slash commands |
162
+ | **5.5** (v0.4.0 — this release) | **`swarph onboard <peer-name>` + `swarph ratify <peer-name>`** — six mechanics steps + handshake template + witness flip (PLAN.md §15) |
130
163
  | **3** | `--ask <peer>` mesh-aware one-shot via MeshClient |
131
- | **5.5** | `swarph onboard <peer-name>` + `swarph ratify <peer-name>` (PLAN.md §15) |
132
164
  | **5.6** | `swarph daemon` foreground drain loop + REPL drain coroutine + `/inbox`, `/reply` (PLAN.md §16) |
133
165
  | **6** | (already done) PyPI publish |
134
166
 
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swarph-cli"
7
- version = "0.3.0"
8
- description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.3.0 ships the Phase 5 `swarph chat` REPL on top of Phase 2 one-shot + Phase 2.5 import (PLAN.md §13)."
7
+ version = "0.4.0"
8
+ description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.4.0 ships Phase 5.5 `swarph onboard` + `swarph ratify` on top of Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL (PLAN.md §13 / §15)."
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.3.0"
19
+ __version__ = "0.4.0"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -0,0 +1,377 @@
1
+ """``swarph onboard <peer-name>`` — Phase 5.5 mechanics-phase per PLAN.md §15.4.
2
+
3
+ Six mechanics steps execute automatically; the seventh — composing
4
+ and sending the handshake DM — is **manual by design** (§15.1) so the
5
+ new peer's own-words ack of the four invariants reflects active
6
+ understanding rather than boilerplate provisioning.
7
+
8
+ Idempotent: rerun safe. Each step's gateway call is upsert-shaped
9
+ (POST /peers/register on conflict updates) or guarded (scaffold dir
10
+ mkdir -p). Re-running on an already-onboarded peer surfaces "already
11
+ registered" without harming state.
12
+
13
+ Auth resolution (step 3):
14
+ 1. ``MESH_GATEWAY_TOKEN`` env var
15
+ 2. ``~/.swarph/secrets.toml`` mode 0600 (read-only — does not auto-create)
16
+ 3. Interactive prompt
17
+ 4. NEVER from argv (would land in shell history)
18
+
19
+ Cross-runtime (§15.6 #10): Claude-only in v0; Gemini/non-Claude
20
+ runtime scaffolding lands alongside that adapter's Phase 6 rollout.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import json
27
+ import os
28
+ import sys
29
+ import urllib.error
30
+ import urllib.request
31
+ from getpass import getpass
32
+ from pathlib import Path
33
+ from typing import Optional
34
+
35
+
36
+ _HANDSHAKE_TEMPLATE = """\
37
+ # Handshake DM — {peer}
38
+
39
+ > **Manual step.** Per PLAN.md §15.1, the contract phase is preserved
40
+ > as a manual artifact so your own-words acknowledgement reflects active
41
+ > understanding, not provisioning. Edit each section below in your own
42
+ > words. Generic boilerplate will be flagged + rejected by the witness.
43
+
44
+ ## 1. DM SEMANTICS
45
+
46
+ > Reference: hedge-fund-mcp `CLAUDE.md` Science Claude Mesh Bootstrap
47
+ > section, "DM semantics: AI-to-AI is the default…"
48
+
49
+ [your own-words ack here — what does AI-to-AI-by-default mean for
50
+ how you'll handle routine peer DMs vs. ones crossing a privilege
51
+ boundary?]
52
+
53
+ ## 2. Framing-contagion
54
+
55
+ > Reference: auto-memory `project_peer_name_canonical.md`. Your
56
+ > canonical name in the registry is `{peer}`.
57
+
58
+ [your own-words ack — how will you stay canonical + flag wrong-name
59
+ DMs you receive?]
60
+
61
+ ## 3. Transparency-by-default
62
+
63
+ > Reference: swarph paper main draft §3.7.
64
+
65
+ [your own-words ack — what does transparency-by-default look like
66
+ when you slip? When do you DM peers vs. self-fix?]
67
+
68
+ ## 4. Mesh-secrets out-of-band
69
+
70
+ > Reference: hedge-fund-mcp `CLAUDE.md` Critical operational rules,
71
+ > "Mesh secrets out-of-band only" bullet.
72
+
73
+ [your own-words ack — what counts as a mesh secret + what's your
74
+ fallback channel when you must convey one?]
75
+
76
+ ---
77
+
78
+ **To send:**
79
+
80
+ ```
81
+ swarph "$(<{tmp_path})" --provider <your-llm> --caller {peer}.handshake.witness-{witness}
82
+ ```
83
+
84
+ Or paste the rendered text into a DM via the gateway's
85
+ ``POST /messages`` API to your witness peer (default
86
+ ``science-claude``). The witness will read both this DM AND any
87
+ imported session JSONL (§17.2a flow), then run
88
+ ``swarph ratify {peer}`` to flip ``ratified=true``.
89
+
90
+ **Status:** registered_unratified=true. You can read inbox + send
91
+ DMs (so the handshake itself works), but ``task_claim`` is
92
+ gateway-refused until ratified.
93
+ """
94
+
95
+
96
+ def _build_parser() -> argparse.ArgumentParser:
97
+ p = argparse.ArgumentParser(
98
+ prog="swarph onboard",
99
+ description="Phase 5.5 mechanics-phase peer onboarding per PLAN.md §15.4.",
100
+ )
101
+ p.add_argument("peer", help="canonical peer name (e.g. razorpeter)")
102
+ p.add_argument(
103
+ "--gateway",
104
+ default=os.environ.get("MESH_GATEWAY_URL", "http://localhost:8788"),
105
+ help="mesh-gateway base URL (default: $MESH_GATEWAY_URL or http://localhost:8788)",
106
+ )
107
+ p.add_argument(
108
+ "--token-file",
109
+ default=None,
110
+ help="explicit path to a secrets file (mode 0600 expected). "
111
+ "Default resolution order: $MESH_GATEWAY_TOKEN env → ~/.swarph/secrets.toml → prompt.",
112
+ )
113
+ p.add_argument(
114
+ "--state-dir",
115
+ default=None,
116
+ help="local state directory root (default: ~/swarph_state).",
117
+ )
118
+ p.add_argument(
119
+ "--url",
120
+ default=None,
121
+ help="this peer's HTTP URL for the registry (default: http://<peer>:8787).",
122
+ )
123
+ p.add_argument(
124
+ "--capability",
125
+ action="append",
126
+ default=[],
127
+ help="capability advert as KEY=VALUE (repeatable). VALUE parsed as JSON if possible. "
128
+ 'Defaults to {"can_claim_tasks": true} if none given.',
129
+ )
130
+ return p
131
+
132
+
133
+ def _resolve_token(token_file_arg: Optional[str]) -> str:
134
+ """Step 3 — token resolution per §15.4. Read-only on the secrets file
135
+ (does not auto-create per drop DM #726 #3 — privilege boundary)."""
136
+ env_tok = os.environ.get("MESH_GATEWAY_TOKEN")
137
+ if env_tok:
138
+ return env_tok
139
+
140
+ secrets_path = (
141
+ Path(token_file_arg).expanduser()
142
+ if token_file_arg
143
+ else Path.home() / ".swarph" / "secrets.toml"
144
+ )
145
+ if secrets_path.exists():
146
+ try:
147
+ mode = secrets_path.stat().st_mode & 0o777
148
+ if mode != 0o600:
149
+ print(
150
+ f"swarph onboard: WARNING: {secrets_path} mode is {oct(mode)}, "
151
+ f"expected 0600. Continuing — fix manually with `chmod 600 {secrets_path}`.",
152
+ file=sys.stderr,
153
+ )
154
+ content = secrets_path.read_text(encoding="utf-8")
155
+ for line in content.splitlines():
156
+ line = line.strip()
157
+ if line.startswith("#") or not line:
158
+ continue
159
+ if line.startswith("MESH_GATEWAY_TOKEN"):
160
+ val = line.split("=", 1)[1].strip().strip('"').strip("'")
161
+ if val:
162
+ return val
163
+ except Exception as exc:
164
+ print(
165
+ f"swarph onboard: failed to read {secrets_path}: {exc}", file=sys.stderr
166
+ )
167
+
168
+ print(
169
+ f"swarph onboard: MESH_GATEWAY_TOKEN not in env, not found in {secrets_path}.\n"
170
+ f" Canonical secrets.toml shape (mode 0600):\n"
171
+ f" MESH_GATEWAY_TOKEN=<your-token>\n"
172
+ f" Falling back to interactive prompt.",
173
+ file=sys.stderr,
174
+ )
175
+ return getpass("MESH_GATEWAY_TOKEN: ").strip()
176
+
177
+
178
+ def _post_json(
179
+ url: str, body: dict, token: str, *, method: str = "POST"
180
+ ) -> tuple[int, dict]:
181
+ """Tiny stdlib HTTP client. Avoids httpx dep at the CLI layer.
182
+
183
+ Returns (status, parsed_body). On non-2xx, parsed_body is the error
184
+ JSON payload (best-effort) so callers can surface gateway error text."""
185
+ data = json.dumps(body).encode("utf-8")
186
+ req = urllib.request.Request(
187
+ url,
188
+ data=data,
189
+ method=method,
190
+ headers={
191
+ "Authorization": f"Bearer {token}",
192
+ "Content-Type": "application/json",
193
+ },
194
+ )
195
+ try:
196
+ with urllib.request.urlopen(req, timeout=10) as resp:
197
+ payload = json.loads(resp.read().decode("utf-8") or "{}")
198
+ return resp.status, payload
199
+ except urllib.error.HTTPError as exc:
200
+ try:
201
+ err_body = json.loads(exc.read().decode("utf-8") or "{}")
202
+ except Exception:
203
+ err_body = {"detail": str(exc)}
204
+ return exc.code, err_body
205
+
206
+
207
+ def _parse_capability(spec: str) -> tuple[str, object]:
208
+ """``KEY=VALUE`` → (key, value). VALUE parsed as JSON when possible
209
+ (so ``can_claim_tasks=true`` lands as bool, not string)."""
210
+ if "=" not in spec:
211
+ raise argparse.ArgumentTypeError(f"capability {spec!r} not KEY=VALUE shape")
212
+ k, v = spec.split("=", 1)
213
+ try:
214
+ return k.strip(), json.loads(v)
215
+ except json.JSONDecodeError:
216
+ return k.strip(), v
217
+
218
+
219
+ def run_onboard(argv: list[str]) -> int:
220
+ """Entry point invoked by ``swarph_cli.main`` verb dispatch.
221
+
222
+ Returns process exit code: 0 on success, 1 on validation fail,
223
+ 2 on gateway error."""
224
+ args = _build_parser().parse_args(argv)
225
+
226
+ # ── Step 1: validate_node_name ───────────────────────────────────
227
+ print(f"[1/6] validate_node_name({args.peer!r})")
228
+ try:
229
+ from swarph_shared.peer_registry import (
230
+ validate_node_name,
231
+ NotInRegistry,
232
+ GatewayUnreachableError,
233
+ )
234
+ except ImportError as exc:
235
+ print(f"swarph onboard: missing swarph-shared>=0.2.0: {exc}", file=sys.stderr)
236
+ return 1
237
+
238
+ # NotInRegistry is expected here — onboard's whole point is that
239
+ # the peer doesn't exist yet. We only enforce the regex shape.
240
+ try:
241
+ from swarph_shared.peer_registry import NAMING_CONVENTION_REGEX, KNOWN_ALIASES
242
+ except ImportError:
243
+ print("swarph onboard: peer_registry primitives missing", file=sys.stderr)
244
+ return 1
245
+
246
+ canonical = KNOWN_ALIASES.get(args.peer, args.peer)
247
+ if canonical != args.peer:
248
+ print(
249
+ f" WARN: {args.peer!r} resolved to canonical {canonical!r} "
250
+ f"(contagion alias)",
251
+ file=sys.stderr,
252
+ )
253
+ if not NAMING_CONVENTION_REGEX.match(canonical):
254
+ print(
255
+ f"swarph onboard: {canonical!r} fails naming convention "
256
+ f"(^[a-z][a-z0-9-]*[a-z0-9]$)",
257
+ file=sys.stderr,
258
+ )
259
+ return 1
260
+ print(f" ok ({canonical!r})")
261
+
262
+ # ── Step 2: would-write peer-registry row (effectively step 4) ───
263
+ # The PLAN's step 2 is logically subsumed by step 4 (the gateway
264
+ # POST is the only persistent registry write). We surface it as a
265
+ # planning/dry-run line for operator clarity.
266
+ capabilities = dict(_parse_capability(c) for c in args.capability) if args.capability else {
267
+ "can_claim_tasks": True
268
+ }
269
+ print(f"[2/6] prepare peer-registry row (caps={capabilities})")
270
+
271
+ # ── Step 3: resolve MESH_GATEWAY_TOKEN ───────────────────────────
272
+ print("[3/6] resolve MESH_GATEWAY_TOKEN")
273
+ token = _resolve_token(args.token_file)
274
+ if not token:
275
+ print("swarph onboard: empty token", file=sys.stderr)
276
+ return 1
277
+ print(" ok")
278
+
279
+ # ── Step 4: POST /peers/register ─────────────────────────────────
280
+ peer_url = args.url or f"http://{canonical}:8787"
281
+ print(f"[4/6] POST {args.gateway}/peers/register")
282
+ status, body = _post_json(
283
+ f"{args.gateway}/peers/register",
284
+ {"name": canonical, "url": peer_url, "capabilities": capabilities},
285
+ token,
286
+ )
287
+ if status != 200:
288
+ print(
289
+ f"swarph onboard: gateway register failed: {status} {body}",
290
+ file=sys.stderr,
291
+ )
292
+ return 2
293
+ if body.get("registered_unratified") is False:
294
+ print(
295
+ f" ok (already ratified — peer existed pre-Phase-5.5 or was "
296
+ f"witness-flipped already)"
297
+ )
298
+ else:
299
+ print(f" ok (registered_unratified=true)")
300
+
301
+ # ── Step 5: subscription auth check ──────────────────────────────
302
+ print("[5/6] verify_subscription_setup()")
303
+ try:
304
+ from swarph_shared import verify_subscription_setup
305
+
306
+ # The function returns either True or raises an informative error;
307
+ # catch broadly so onboarding doesn't blow up on Claude-runtime-only
308
+ # checks when the peer is non-Claude (§15.6 #10 deferred to Phase 6).
309
+ verify_subscription_setup()
310
+ print(" ok (Claude subscription credentials + binary verified)")
311
+ except Exception as exc:
312
+ print(
313
+ f" WARN: {type(exc).__name__}: {exc}\n"
314
+ f" Subscription path won't work for this peer until resolved. "
315
+ f"Non-Claude runtimes (Gemini, etc.) ship in Phase 6 per §15.6 #10.",
316
+ file=sys.stderr,
317
+ )
318
+
319
+ # ── Step 6: scaffold local state directory ───────────────────────
320
+ state_root = (
321
+ Path(args.state_dir).expanduser()
322
+ if args.state_dir
323
+ else Path.home() / "swarph_state"
324
+ )
325
+ peer_dir = state_root / canonical
326
+ print(f"[6/6] scaffold {peer_dir}")
327
+ peer_dir.mkdir(parents=True, exist_ok=True)
328
+ try:
329
+ peer_dir.chmod(0o700)
330
+ except OSError:
331
+ pass # best-effort; Windows or fs without POSIX modes
332
+ inbox_log = peer_dir / "inbox.log"
333
+ cursor_path = peer_dir / "cursor.json"
334
+ env_example = peer_dir / ".env.example"
335
+ daemon_sh = peer_dir / "run-daemon.sh"
336
+
337
+ if not inbox_log.exists():
338
+ inbox_log.touch()
339
+ if not cursor_path.exists():
340
+ cursor_path.write_text(
341
+ json.dumps({"last_msg_id": 0, "tasks_snapshot": {}}, indent=2),
342
+ encoding="utf-8",
343
+ )
344
+ if not env_example.exists():
345
+ env_example.write_text(
346
+ f"# swarph state for {canonical}\n"
347
+ f"MESH_GATEWAY_TOKEN=\n"
348
+ f"MESH_GATEWAY_URL={args.gateway}\n",
349
+ encoding="utf-8",
350
+ )
351
+ if not daemon_sh.exists():
352
+ daemon_sh.write_text(
353
+ f"#!/usr/bin/env bash\n"
354
+ f"# Phase 5.6 launcher — runs `swarph daemon` with this peer's state.\n"
355
+ f"# Pre-launch via: nohup ./run-daemon.sh &\n"
356
+ f"exec swarph daemon --state-dir {peer_dir}\n",
357
+ encoding="utf-8",
358
+ )
359
+ daemon_sh.chmod(0o755)
360
+ print(f" ok (inbox.log, cursor.json, .env.example, run-daemon.sh)")
361
+
362
+ # ── Step 7: handshake template (MANUAL) ──────────────────────────
363
+ tmp_path = Path(f"/tmp/{canonical}-handshake.md")
364
+ tmp_path.write_text(
365
+ _HANDSHAKE_TEMPLATE.format(
366
+ peer=canonical, witness="science-claude", tmp_path=tmp_path
367
+ ),
368
+ encoding="utf-8",
369
+ )
370
+ print(
371
+ f"\n[manual] handshake template at {tmp_path}\n"
372
+ f" Edit each section in your own words, then send to your witness peer.\n"
373
+ f" After witness reads + judges sufficient, they run:\n"
374
+ f" swarph ratify {canonical} --reason \"<short text>\"\n"
375
+ f" to flip ratified=true.\n"
376
+ )
377
+ return 0