swarph-cli 0.10.0__tar.gz → 0.10.2__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 (90) hide show
  1. {swarph_cli-0.10.0/src/swarph_cli.egg-info → swarph_cli-0.10.2}/PKG-INFO +53 -1
  2. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/README.md +52 -0
  3. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/pyproject.toml +1 -1
  4. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/__init__.py +1 -1
  5. swarph_cli-0.10.2/src/swarph_cli/commands/compress.py +80 -0
  6. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/init.py +9 -1
  7. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/watchdog.py +383 -37
  8. swarph_cli-0.10.2/src/swarph_cli/compress/__init__.py +0 -0
  9. swarph_cli-0.10.2/src/swarph_cli/compress/levers.py +52 -0
  10. swarph_cli-0.10.2/src/swarph_cli/compress/marker.py +39 -0
  11. swarph_cli-0.10.2/src/swarph_cli/compress/verify.py +81 -0
  12. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/main.py +1 -0
  13. {swarph_cli-0.10.0 → swarph_cli-0.10.2/src/swarph_cli.egg-info}/PKG-INFO +53 -1
  14. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli.egg-info/SOURCES.txt +9 -0
  15. swarph_cli-0.10.2/tests/test_compress_command.py +33 -0
  16. swarph_cli-0.10.2/tests/test_compress_levers.py +38 -0
  17. swarph_cli-0.10.2/tests/test_compress_marker.py +20 -0
  18. swarph_cli-0.10.2/tests/test_compress_verify.py +62 -0
  19. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_init_command.py +5 -1
  20. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_watchdog.py +36 -4
  21. swarph_cli-0.10.2/tests/test_watchdog_model_rung.py +881 -0
  22. swarph_cli-0.10.0/tests/test_watchdog_model_rung.py +0 -465
  23. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/LICENSE +0 -0
  24. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/setup.cfg +0 -0
  25. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/caller.py +0 -0
  26. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/cell.py +0 -0
  27. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/__init__.py +0 -0
  28. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/add.py +0 -0
  29. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/chat.py +0 -0
  30. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/daemon.py +0 -0
  31. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/hook_output.py +0 -0
  32. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/hooks.py +0 -0
  33. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/import_session.py +0 -0
  34. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/install_hook.py +0 -0
  35. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/mcp_server.py +0 -0
  36. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/memory_sync.py +0 -0
  37. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/mesh.py +0 -0
  38. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/onboard.py +0 -0
  39. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/protocol_handler.py +0 -0
  40. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/ratify.py +0 -0
  41. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/security.py +0 -0
  42. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/commands/spawn.py +0 -0
  43. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/parsers/__init__.py +0 -0
  44. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/parsers/claude.py +0 -0
  45. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  46. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  47. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  48. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  49. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  50. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli.egg-info/requires.txt +0 -0
  51. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/src/swarph_cli.egg-info/top_level.txt +0 -0
  52. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_add_security_gate.py +0 -0
  53. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_artifact_add.py +0 -0
  54. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_artifact_hash.py +0 -0
  55. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_artifact_lib.py +0 -0
  56. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_artifact_mcp.py +0 -0
  57. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_artifact_skill.py +0 -0
  58. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_artifact_tool.py +0 -0
  59. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_artifact_uri.py +0 -0
  60. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_cell_loader.py +0 -0
  61. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_chat_command.py +0 -0
  62. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_claude_parser.py +0 -0
  63. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_daemon_command.py +0 -0
  64. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_feature_to_uri.py +0 -0
  65. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_hook_output.py +0 -0
  66. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_hooks_add.py +0 -0
  67. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_hooks_bundle.py +0 -0
  68. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_hooks_init.py +0 -0
  69. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_hooks_lifecycle.py +0 -0
  70. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_hooks_merge.py +0 -0
  71. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_import_command.py +0 -0
  72. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_install_hook.py +0 -0
  73. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_main.py +0 -0
  74. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_mcp_server.py +0 -0
  75. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_memory_sync.py +0 -0
  76. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_mesh_command.py +0 -0
  77. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_mesh_sidecar.py +0 -0
  78. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_onboard_command.py +0 -0
  79. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_protocol_handler.py +0 -0
  80. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_ratify_command.py +0 -0
  81. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_security.py +0 -0
  82. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_smoke_chat.py +0 -0
  83. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_smoke_one_shot.py +0 -0
  84. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_smoke_phase_5_5.py +0 -0
  85. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_spawn_command.py +0 -0
  86. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_spawn_windows_relaunch.py +0 -0
  87. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_watchdog_dm_wake.py +0 -0
  88. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  89. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  90. {swarph_cli-0.10.0 → swarph_cli-0.10.2}/tests/test_watchdog_stale_peers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.10.0
3
+ Version: 0.10.2
4
4
  Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.5: foolproof Windows auto-relaunch (genuine-WT detection via process ancestry, not WT_SESSION).
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
@@ -348,6 +348,58 @@ Pong!
348
348
  # 3+26t $0.0000 0.73s caller=cli.oneshot.ubuntu provider=gemini
349
349
  ```
350
350
 
351
+ ### `swarph compress` (v0.11 — context-surface compression)
352
+
353
+ Compress a **machine-read context surface** (memory index, manual, agent brief) to
354
+ reclaim always-loaded space/tokens. The principle: fluent natural language carries
355
+ redundancy a model infers for free — *the decompressor is the model itself*. Proven
356
+ by hand on the OMEGA swarm: `MEMORY.md` 37→16KB (58%), `CLAUDE.md` 241→21KB
357
+ always-loaded (91%, archival).
358
+
359
+ **Opt-in by marker (fails safe).** A file is compressible only if it carries an
360
+ explicit marker; unmarked files are left untouched. The model's judgment is spent
361
+ once, in-session, authoring the marker — runtime is pure-Python marker parsing, no
362
+ model in the hot path.
363
+
364
+ ```
365
+ <!-- swarph:compress lever=archival boundary="^## Session" -->
366
+ <!-- swarph:compress lever=shorthand pointer="](*.md)" floor=0.45 -->
367
+ ```
368
+
369
+ **Two levers, different risk classes:**
370
+
371
+ | Lever | What | Loss class | Model? |
372
+ |---|---|---|---|
373
+ | `archival` | relocate the cold tail below `boundary` to `<file>.archive.<ext>` + leave a pointer | **lossless** (nothing destroyed) | no — pure Python, zero tokens |
374
+ | `shorthand` | rewrite a pointer-bearing INDEX to telegraphic shorthand | lossy, **bounded to index-over-preserved-source** (recoverable by construction) | yes (`claude -p` subscription path) |
375
+
376
+ Shorthand is gated: redundancy-floor (refuse if already dense), links-superset
377
+ (every `[]()` survives), index-over-source (every entry keeps a resolvable
378
+ pointer), and an **adversarial verify-expand** (an independent model hunts for a
379
+ dropped fact; one found → abort).
380
+
381
+ ```bash
382
+ swarph compress MEMORY.md # dry-run: classify, propose, report savings
383
+ swarph compress MEMORY.md --apply # write (atomic tempfile→mv + .bak + verify-gate)
384
+ swarph compress MEMORY.md --verify-idempotent # assert compress(compress(x)) ≈ noop
385
+ ```
386
+
387
+ Dry-run is the default — nothing mutates without `--apply`. Cron-friendly exit codes:
388
+
389
+ | Code | Meaning |
390
+ |---|---|
391
+ | `0` | analyzed/savings-reported (or applied clean) |
392
+ | `2` | no such file |
393
+ | `3` | refused — unmarked (leave breathing) |
394
+ | `4` | refused — archival: no boundary line matched |
395
+ | `5` | refused — shorthand: below redundancy floor (already dense) |
396
+ | `6` | refused — shorthand dropped a link / lost a pointer-to-source |
397
+ | `7` | refused — adversarial verify-expand found a dropped fact |
398
+ | `8` | refused — not idempotent (second pass kept cutting; signal-eating alarm) |
399
+
400
+ Design spec: `docs/superpowers/specs/2026-06-11-swarph-context-compressor-design.md`
401
+ (in the hedge-fund-mcp repo).
402
+
351
403
  ### `--json` mode semantics
352
404
 
353
405
  `--json` is a **harness trigger**, not a strict-validation gate. When set, swarph routes the response through the swarph-mesh JSON harness:
@@ -313,6 +313,58 @@ Pong!
313
313
  # 3+26t $0.0000 0.73s caller=cli.oneshot.ubuntu provider=gemini
314
314
  ```
315
315
 
316
+ ### `swarph compress` (v0.11 — context-surface compression)
317
+
318
+ Compress a **machine-read context surface** (memory index, manual, agent brief) to
319
+ reclaim always-loaded space/tokens. The principle: fluent natural language carries
320
+ redundancy a model infers for free — *the decompressor is the model itself*. Proven
321
+ by hand on the OMEGA swarm: `MEMORY.md` 37→16KB (58%), `CLAUDE.md` 241→21KB
322
+ always-loaded (91%, archival).
323
+
324
+ **Opt-in by marker (fails safe).** A file is compressible only if it carries an
325
+ explicit marker; unmarked files are left untouched. The model's judgment is spent
326
+ once, in-session, authoring the marker — runtime is pure-Python marker parsing, no
327
+ model in the hot path.
328
+
329
+ ```
330
+ <!-- swarph:compress lever=archival boundary="^## Session" -->
331
+ <!-- swarph:compress lever=shorthand pointer="](*.md)" floor=0.45 -->
332
+ ```
333
+
334
+ **Two levers, different risk classes:**
335
+
336
+ | Lever | What | Loss class | Model? |
337
+ |---|---|---|---|
338
+ | `archival` | relocate the cold tail below `boundary` to `<file>.archive.<ext>` + leave a pointer | **lossless** (nothing destroyed) | no — pure Python, zero tokens |
339
+ | `shorthand` | rewrite a pointer-bearing INDEX to telegraphic shorthand | lossy, **bounded to index-over-preserved-source** (recoverable by construction) | yes (`claude -p` subscription path) |
340
+
341
+ Shorthand is gated: redundancy-floor (refuse if already dense), links-superset
342
+ (every `[]()` survives), index-over-source (every entry keeps a resolvable
343
+ pointer), and an **adversarial verify-expand** (an independent model hunts for a
344
+ dropped fact; one found → abort).
345
+
346
+ ```bash
347
+ swarph compress MEMORY.md # dry-run: classify, propose, report savings
348
+ swarph compress MEMORY.md --apply # write (atomic tempfile→mv + .bak + verify-gate)
349
+ swarph compress MEMORY.md --verify-idempotent # assert compress(compress(x)) ≈ noop
350
+ ```
351
+
352
+ Dry-run is the default — nothing mutates without `--apply`. Cron-friendly exit codes:
353
+
354
+ | Code | Meaning |
355
+ |---|---|
356
+ | `0` | analyzed/savings-reported (or applied clean) |
357
+ | `2` | no such file |
358
+ | `3` | refused — unmarked (leave breathing) |
359
+ | `4` | refused — archival: no boundary line matched |
360
+ | `5` | refused — shorthand: below redundancy floor (already dense) |
361
+ | `6` | refused — shorthand dropped a link / lost a pointer-to-source |
362
+ | `7` | refused — adversarial verify-expand found a dropped fact |
363
+ | `8` | refused — not idempotent (second pass kept cutting; signal-eating alarm) |
364
+
365
+ Design spec: `docs/superpowers/specs/2026-06-11-swarph-context-compressor-design.md`
366
+ (in the hedge-fund-mcp repo).
367
+
316
368
  ### `--json` mode semantics
317
369
 
318
370
  `--json` is a **harness trigger**, not a strict-validation gate. When set, swarph routes the response through the swarph-mesh JSON harness:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swarph-cli"
7
- version = "0.10.0"
7
+ version = "0.10.2"
8
8
  description = "The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.5: foolproof Windows auto-relaunch (genuine-WT detection via process ancestry, not WT_SESSION)."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -16,6 +16,6 @@ The architecture splits CLI from substrate so:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- __version__ = "0.10.0"
19
+ __version__ = "0.10.2"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -0,0 +1,80 @@
1
+ """`swarph compress <file>` — compress a machine-read surface. Dry-run by default;
2
+ marker opt-in (unmarked -> refuse). See docs spec 2026-06-11-swarph-context-compressor."""
3
+ from __future__ import annotations
4
+ import argparse, asyncio, shutil, sys, tempfile, os
5
+ from pathlib import Path
6
+ from ..compress.marker import parse_marker
7
+ from ..compress.levers import archival_split, shorthand
8
+ from ..compress import verify
9
+
10
+
11
+ def _build_parser() -> argparse.ArgumentParser:
12
+ p = argparse.ArgumentParser(prog="swarph compress", description="Compress a machine-read context surface.")
13
+ p.add_argument("file", help="path to the surface to compress")
14
+ p.add_argument("--apply", action="store_true", help="write changes (default: dry-run)")
15
+ p.add_argument("--verify-idempotent", action="store_true", help="assert compress(compress(x)) ~= noop")
16
+ p.add_argument("--force", action="store_true", help="override refusals (NOT for source-of-truth)")
17
+ return p
18
+
19
+
20
+ def _atomic_write(path: Path, content: str) -> None:
21
+ shutil.copy2(path, path.with_suffix(path.suffix + ".bak"))
22
+ fd, tmp = tempfile.mkstemp(dir=str(path.parent), suffix=".tmp")
23
+ with os.fdopen(fd, "w") as fh:
24
+ fh.write(content)
25
+ os.replace(tmp, path)
26
+
27
+
28
+ def run_compress(argv: list[str]) -> int:
29
+ args = _build_parser().parse_args(argv)
30
+ path = Path(args.file)
31
+ if not path.exists():
32
+ print(f"[compress] no such file: {path}", file=sys.stderr)
33
+ return 2
34
+ source = path.read_text()
35
+ marker = parse_marker(source)
36
+ if marker is None:
37
+ print(f"[compress] REFUSE: {path} carries no swarph:compress marker — leaving untouched", file=sys.stderr)
38
+ return 3
39
+
40
+ if marker.lever == "archival":
41
+ archive_path = path.with_suffix(".archive" + path.suffix)
42
+ res = archival_split(source, boundary=marker.boundary, archive_name=archive_path.name)
43
+ if res is None:
44
+ print(f"[compress] REFUSE: no boundary line matching {marker.boundary!r}", file=sys.stderr)
45
+ return 4
46
+ saved = len(source) - len(res.live)
47
+ print(f"[compress] archival: {path.name} {len(source)}B → {len(res.live)}B live "
48
+ f"(+{len(res.archive)}B → {archive_path.name}); saved {saved}B always-loaded")
49
+ if args.apply:
50
+ archive_path.write_text(res.archive)
51
+ _atomic_write(path, res.live)
52
+ print(f"[compress] applied (.bak left, archive {archive_path.name})")
53
+ return 0
54
+
55
+ # shorthand
56
+ floor = marker.floor if marker.floor is not None else 0.45
57
+ if not verify.above_floor(source, floor):
58
+ print(f"[compress] REFUSE: already dense (below redundancy floor {floor}) — nothing free to remove", file=sys.stderr)
59
+ return 5
60
+ out = asyncio.run(shorthand(source))
61
+ if not verify.links_preserved(source, out):
62
+ print("[compress] REFUSE: shorthand dropped a link (links not superset)", file=sys.stderr)
63
+ return 6
64
+ if not verify.entries_point_to_source(out, pointer=marker.pointer, base=path.parent):
65
+ print("[compress] REFUSE: an entry lost its pointer-to-source (index-over-source violated)", file=sys.stderr)
66
+ return 6
67
+ if not asyncio.run(verify.verify_expand(source, out)):
68
+ print("[compress] REFUSE: adversarial verify-expand found a dropped fact", file=sys.stderr)
69
+ return 7
70
+ if args.verify_idempotent:
71
+ out2 = asyncio.run(shorthand(out))
72
+ if not verify.idempotent(out, out2):
73
+ print("[compress] REFUSE: not idempotent — second pass kept cutting (signal-eating alarm)", file=sys.stderr)
74
+ return 8
75
+ saved = len(source) - len(out)
76
+ print(f"[compress] shorthand: {path.name} {len(source)}B → {len(out)}B; saved {saved}B ({saved*100//max(len(source),1)}%)")
77
+ if args.apply:
78
+ _atomic_write(path, out)
79
+ print("[compress] applied (.bak left)")
80
+ return 0
@@ -139,7 +139,15 @@ def run_init(argv: list[str]) -> int:
139
139
  cwd_raw = args.cwd or (_ask("Working dir (cwd)", str(Path.cwd())) if interactive else str(Path.cwd()))
140
140
  cwd = Path(cwd_raw).expanduser().resolve()
141
141
  tmux = args.tmux or (_ask("tmux session", name) if interactive else name)
142
- cursor = args.cursor or f"/tmp/{name}-cursor.json"
142
+ # Default MUST match where `swarph mesh sidecar` actually writes its
143
+ # cursor (<state-dir>/cursor.json, state-dir defaulting to
144
+ # ~/swarph_state/<name>/mesh-sidecar) — the watchdog reads THIS pin while
145
+ # the sidecar maintains THAT file. The old /tmp default left the two
146
+ # shipped components pointing at different paths out of the box
147
+ # (science-claude onboarding, 2026-06-10).
148
+ cursor = args.cursor or str(
149
+ Path.home() / "swarph_state" / name / "mesh-sidecar" / "cursor.json"
150
+ )
143
151
 
144
152
  # ── sandbox ──
145
153
  if provider == "codex":