meshcode 2.11.154__tar.gz → 2.11.155__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 (118) hide show
  1. {meshcode-2.11.154 → meshcode-2.11.155}/PKG-INFO +1 -1
  2. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/__init__.py +1 -1
  3. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/run_agent.py +101 -11
  4. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode.egg-info/PKG-INFO +1 -1
  5. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode.egg-info/SOURCES.txt +2 -0
  6. {meshcode-2.11.154 → meshcode-2.11.155}/pyproject.toml +1 -1
  7. meshcode-2.11.155/tests/test_replica_base_workspace_fallback.py +87 -0
  8. meshcode-2.11.155/tests/test_replica_boot_protocol_unconditional.py +64 -0
  9. {meshcode-2.11.154 → meshcode-2.11.155}/README.md +0 -0
  10. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/__main__.py +0 -0
  11. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/_launch_smoke.py +0 -0
  12. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/_session_handoff_template.py +0 -0
  13. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/_stop_hook_template.py +0 -0
  14. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/_update_guard.py +0 -0
  15. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/ascii_art.py +0 -0
  16. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/atomic_push.py +0 -0
  17. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/claude_update.py +0 -0
  18. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/cli.py +0 -0
  19. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/comms_v4.py +0 -0
  20. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/compat.py +0 -0
  21. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/daemon.py +0 -0
  22. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/date_parse.py +0 -0
  23. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/doctor.py +0 -0
  24. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/error_hints.py +0 -0
  25. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/exceptions.py +0 -0
  26. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/helper_visuals.py +0 -0
  27. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/hooks/__init__.py +0 -0
  28. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/hooks/push_guard.py +0 -0
  29. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/hooks/repo_path_lock.py +0 -0
  30. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/hostd.py +0 -0
  31. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/invites.py +0 -0
  32. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/launcher.py +0 -0
  33. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/launcher_install.py +0 -0
  34. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/__init__.py +0 -0
  35. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/__main__.py +0 -0
  36. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/backend.py +0 -0
  37. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/realtime.py +0 -0
  38. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/server.py +0 -0
  39. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/sleep_signals.py +0 -0
  40. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/swarm.py +0 -0
  41. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_backend.py +0 -0
  42. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_boot_timing.py +0 -0
  43. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_install_guard.py +0 -0
  44. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_prefs_claude_version.py +0 -0
  45. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  46. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
  47. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/meshcode_mcp/test_swarm.py +0 -0
  48. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/preferences.py +0 -0
  49. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/protocol_handler.py +0 -0
  50. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/protocol_v2.py +0 -0
  51. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/quickstart.py +0 -0
  52. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/rpc_allowlist.py +0 -0
  53. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/scripts/check_secrets.py +0 -0
  54. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/scripts/race_rate_harness.py +0 -0
  55. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/secrets.py +0 -0
  56. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/self_update.py +0 -0
  57. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/setup_clients.py +0 -0
  58. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/supervisor.py +0 -0
  59. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/up.py +0 -0
  60. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode/upload.py +0 -0
  61. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode.egg-info/dependency_links.txt +0 -0
  62. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode.egg-info/entry_points.txt +0 -0
  63. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode.egg-info/requires.txt +0 -0
  64. {meshcode-2.11.154 → meshcode-2.11.155}/meshcode.egg-info/top_level.txt +0 -0
  65. {meshcode-2.11.154 → meshcode-2.11.155}/setup.cfg +0 -0
  66. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_auto_update_hardening.py +0 -0
  67. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_autonomous_closegap_1.py +0 -0
  68. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_autonomous_closegap_2.py +0 -0
  69. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_autonomous_closegap_3.py +0 -0
  70. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_autonomous_prompt_inject.py +0 -0
  71. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_boot_bug_regression.py +0 -0
  72. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_color_truecolor.py +0 -0
  73. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_core.py +0 -0
  74. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_cross_agent_messaging.py +0 -0
  75. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_date_parse.py +0 -0
  76. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_doctor.py +0 -0
  77. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_epistemic_v1_python_sdk.py +0 -0
  78. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_epistemic_v1_stop_conditions.py +0 -0
  79. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_esc_deaf_state.py +0 -0
  80. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_exceptions.py +0 -0
  81. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_file_upload.py +0 -0
  82. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_helper_visuals.py +0 -0
  83. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_hostd_launch_pinned_env.py +0 -0
  84. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_hostd_serve_discovery_split.py +0 -0
  85. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_hostd_zombie_sessions.py +0 -0
  86. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_init_device_code.py +0 -0
  87. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_install_guard.py +0 -0
  88. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_launch_smoke.py +0 -0
  89. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_lease_sigterm_release.py +0 -0
  90. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_live_mesh_guard.py +0 -0
  91. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_mark_read_batch.py +0 -0
  92. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_marketplace_ratings.py +0 -0
  93. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_migration_integrity.py +0 -0
  94. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_preflight_hb_gate.py +0 -0
  95. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_pretrust_claude.py +0 -0
  96. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_push_guard.py +0 -0
  97. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_realtime_event_freshness.py +0 -0
  98. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_rls_cross_tenant.py +0 -0
  99. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_rm_guard.py +0 -0
  100. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_rpc_grants.py +0 -0
  101. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_rpc_migrations.py +0 -0
  102. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_run_agent_dry_run.py +0 -0
  103. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_run_agent_no_server_import.py +0 -0
  104. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_security_regressions.py +0 -0
  105. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_self_update_user_site.py +0 -0
  106. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_sentinel.py +0 -0
  107. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_session_replay_gate.py +0 -0
  108. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_setup_path.py +0 -0
  109. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_sleep_signals.py +0 -0
  110. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_status_enum_coverage.py +0 -0
  111. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_stay_on_loop_hook.py +0 -0
  112. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_stop_ghost_terminal.py +0 -0
  113. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_swarm_events.py +0 -0
  114. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_task_progress.py +0 -0
  115. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_terminal_lifecycle.py +0 -0
  116. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_up_launch_cmd.py +0 -0
  117. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_update_guard.py +0 -0
  118. {meshcode-2.11.154 → meshcode-2.11.155}/tests/test_wait_open_tasks_contradiction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.154
3
+ Version: 2.11.155
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "2.11.154"
2
+ __version__ = "2.11.155"
3
3
 
4
4
  # Exception hierarchy — eagerly imported (lightweight, no deps)
5
5
  from meshcode.exceptions import ( # noqa: F401
@@ -933,11 +933,18 @@ def _preflight_hb_enabled() -> bool:
933
933
  return os.environ.get("MESHCODE_HOSTD_SPAWN") != "1"
934
934
 
935
935
 
936
- def _report_launch_failure(agent: str, project: str, reason: str, detail: str) -> None:
936
+ def _report_launch_failure(agent: str, project: str, reason: str, detail: str,
937
+ status: str = "offline") -> None:
937
938
  """Editor spawn failed AFTER the pre-flight heartbeat already marked the
938
939
  agent online — revert the ghost 'online' row to offline and tell the
939
940
  dashboard WHY (launch-reliability fix 809d3b37, factors C + H).
940
941
 
942
+ `status` (must be a valid mc_agents.status per valid_agent_status): default
943
+ 'offline' reverts a ghost-online editor-spawn failure. The replica
944
+ workspace-provisioning failure (task 9698d738) passes 'needs_setup' — the
945
+ failure IS an unprovisioned workspace, so the dashboard CTA matches the fix
946
+ (`meshcode setup <proj> <agent>`). 'error' is NOT a valid status.
947
+
941
948
  This replaces a broken inline block in run()'s FileNotFoundError handler
942
949
  that referenced `api_key`/`project_id` — names that are never bound in
943
950
  run()'s scope — so the very RPC meant to surface "claude not installed"
@@ -987,12 +994,13 @@ def _report_launch_failure(agent: str, project: str, reason: str, detail: str) -
987
994
  except Exception:
988
995
  pass
989
996
 
990
- # 2) Revert the ghost 'online' the pre-flight heartbeat stamped (factor C).
997
+ # 2) Set the failure status (default 'offline' reverts the pre-flight ghost;
998
+ # 'needs_setup' for an unprovisioned replica workspace — factor C).
991
999
  status_body = json.dumps({
992
1000
  "p_api_key": api_key,
993
1001
  "p_project_id": project_id,
994
1002
  "p_agent_name": agent,
995
- "p_status": "offline",
1003
+ "p_status": status,
996
1004
  "p_task": f"launch failed: {reason}",
997
1005
  })
998
1006
  status_req = Request(
@@ -1005,6 +1013,49 @@ def _report_launch_failure(agent: str, project: str, reason: str, detail: str) -
1005
1013
  print(f"[meshcode] Launch-failure report skipped: {e}", file=sys.stderr)
1006
1014
 
1007
1015
 
1016
+ def _provision_replica_mcp_from_base(project: str, agent: str, ws: "Path") -> bool:
1017
+ """BLINDAJE (task 9698d738 / Samuel P0 "réplicas prenden pero no bootean"):
1018
+ provision a replica's .mcp.json by cloning the BASE agent's known-good one when a
1019
+ fresh setup_workspace() fails (e.g. the keychain is unreadable non-interactively at
1020
+ hostd spawn -> setup_workspace returns 2 -> run_agent used to `return 2` and DIE
1021
+ before the MCP server even started: instance_id NULL, heartbeat NEVER, never ran the
1022
+ lease = "prende pero no bootea").
1023
+
1024
+ A replica named '<base>-<N>' is the same user's clone, so the base's MCP server
1025
+ block (api key / project_id / keychain profile baked at its last setup) is valid for
1026
+ it. We copy it, swap ONLY the server id + MESHCODE_AGENT, and write it to the replica
1027
+ workspace. The base workspace exists whenever the base agent booted (the common
1028
+ case — and the empirically healthy replicas all had a pre-existing workspace).
1029
+ Returns True iff a .mcp.json was written. Never raises."""
1030
+ try:
1031
+ import re as _re
1032
+ m = _re.match(r"^(?P<base>.+)-(?P<n>\d+)$", agent)
1033
+ if not m:
1034
+ return False # not a '<base>-<N>' replica name — nothing to clone
1035
+ base = m.group("base")
1036
+ base_mcp = WORKSPACES_ROOT / f"{project}-{base}" / ".mcp.json"
1037
+ if not base_mcp.exists():
1038
+ return False
1039
+ doc = json.loads(base_mcp.read_text(encoding="utf-8"))
1040
+ servers = doc.get("mcpServers") or {}
1041
+ if not servers:
1042
+ return False
1043
+ # base workspaces hold exactly one server block
1044
+ _base_id, block = next(iter(servers.items()))
1045
+ block = json.loads(json.dumps(block)) # deep copy so we don't mutate the source
1046
+ env = block.get("env")
1047
+ if isinstance(env, dict):
1048
+ env["MESHCODE_AGENT"] = agent # the only identity that differs from the base
1049
+ new_id = f"meshcode-{project}-{agent}"
1050
+ ws.mkdir(parents=True, exist_ok=True)
1051
+ (ws / ".mcp.json").write_text(
1052
+ json.dumps({"mcpServers": {new_id: block}}, indent=2) + "\n", encoding="utf-8")
1053
+ return (ws / ".mcp.json").exists()
1054
+ except Exception as e:
1055
+ print(f"[meshcode] replica base-fallback .mcp.json failed: {e}", file=sys.stderr)
1056
+ return False
1057
+
1058
+
1008
1059
  # Repo-scoped launch (task 24e3dd44 / core-commander launch-diff). When `meshcode run
1009
1060
  # <agent> --repo <path>` is used, the agent boots with cwd=repo (not the meshcode
1010
1061
  # workspace), so its repo CLAUDE.md loads — we carry the boot protocol via
@@ -1293,19 +1344,44 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
1293
1344
  if not mcp_json_path.exists():
1294
1345
  # Regenerate via setup_workspace (has all data it needs from server)
1295
1346
  print(f"[meshcode] .mcp.json missing from workspace — regenerating...", file=sys.stderr)
1347
+ _regen_ok = False
1348
+ _regen_err = ""
1296
1349
  try:
1297
1350
  from .setup_clients import setup_workspace
1298
1351
  rc = setup_workspace(resolved_project, agent,
1299
1352
  keychain_profile=_preferred_keychain_profile(resolved_project, agent))
1300
- if rc == 0 and mcp_json_path.exists():
1353
+ _regen_ok = (rc == 0 and mcp_json_path.exists())
1354
+ if _regen_ok:
1301
1355
  print(f"[meshcode] .mcp.json regenerated successfully.", file=sys.stderr)
1302
1356
  else:
1303
- print(f"[meshcode] ERROR: could not regenerate .mcp.json.", file=sys.stderr)
1304
- print(f"[meshcode] Run `meshcode setup {resolved_project} {agent}` manually.", file=sys.stderr)
1305
- return 2
1357
+ _regen_err = f"setup_workspace rc={rc}"
1358
+ print(f"[meshcode] ERROR: could not regenerate .mcp.json ({_regen_err}).", file=sys.stderr)
1306
1359
  except Exception as e:
1360
+ _regen_err = repr(e)
1307
1361
  print(f"[meshcode] ERROR: .mcp.json missing and regeneration failed: {e}", file=sys.stderr)
1308
- print(f"[meshcode] Run `meshcode setup {resolved_project} {agent}` to fix.", file=sys.stderr)
1362
+
1363
+ # BLINDAJE 1 (task 9698d738): replica base-workspace fallback. A replica whose
1364
+ # workspace was never provisioned hits this path; if setup_workspace fails (e.g.
1365
+ # keychain unreadable non-interactively at spawn) the process used to `return 2`
1366
+ # and DIE pre-boot ("prende pero no bootea"). Clone the base agent's known-good
1367
+ # .mcp.json so the replica boots anyway.
1368
+ if not _regen_ok and _provision_replica_mcp_from_base(resolved_project, agent, ws) \
1369
+ and mcp_json_path.exists():
1370
+ print(f"[meshcode] .mcp.json provisioned from base agent (replica fallback, "
1371
+ f"task 9698d738).", file=sys.stderr)
1372
+ _regen_ok = True
1373
+
1374
+ # BLINDAJE 2 (task 9698d738): never die SILENT. If we still can't provision,
1375
+ # report the launch as FAILED (mc_resolve_launch status='failed' + status row)
1376
+ # so the dashboard shows WHY instead of a phantom 'spawned but offline' replica.
1377
+ if not _regen_ok:
1378
+ _report_launch_failure(
1379
+ agent, resolved_project,
1380
+ "workspace .mcp.json could not be provisioned",
1381
+ f"setup_workspace failed ({_regen_err or 'unknown'}) and no base "
1382
+ f"workspace to clone — run `meshcode setup {resolved_project} {agent}`",
1383
+ status="needs_setup")
1384
+ print(f"[meshcode] Run `meshcode setup {resolved_project} {agent}` manually.", file=sys.stderr)
1309
1385
  return 2
1310
1386
 
1311
1387
  # R2-5 (2.11.117) boot-always-latest: resolve target version (explicit pin
@@ -1672,10 +1748,24 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
1672
1748
  cmd += [
1673
1749
  "--mcp-config", str(mcp_json_path),
1674
1750
  "--settings", str(ws / ".claude" / "settings.json"),
1675
- "--append-system-prompt", MESHCODE_BOOT_PROTOCOL.format(
1676
- agent=agent, project=resolved_project,
1677
- role=(agent_role or "MCP-connected agent"), repo=repo_lock),
1678
1751
  ]
1752
+ # Boot protocol — ALWAYS injected (task 9698d738 / Samuel P0 "réplicas prenden
1753
+ # pero NO bootean en TODAS las meshes"). It was gated on repo_lock on the
1754
+ # assumption that non-repo launches load the protocol from the workspace
1755
+ # CLAUDE.md at cwd=ws. But hostd-spawned REPLICAS frequently have NO provisioned
1756
+ # ws/CLAUDE.md (mc_replicate_agent creates the DB row, not the disk workspace)
1757
+ # and/or chdir lands elsewhere -> with --append gated OFF they received ZERO boot
1758
+ # instructions -> claude starts (spawn) but never runs SESSION START -> never
1759
+ # boots, in every mesh. Injecting unconditionally GUARANTEES boot regardless of
1760
+ # workspace provisioning / cwd, and is idempotent with a present CLAUDE.md (same
1761
+ # text), so repo-scoped and provisioned launches are unchanged. repo defaults to
1762
+ # the workspace path when there's no repo-lock.
1763
+ cmd += [
1764
+ "--append-system-prompt", MESHCODE_BOOT_PROTOCOL.format(
1765
+ agent=agent, project=resolved_project,
1766
+ role=(agent_role or "MCP-connected agent"),
1767
+ repo=(repo_lock or str(ws))),
1768
+ ]
1679
1769
  cmd.extend(["--", "boot"])
1680
1770
  _launch_cwd = repo_lock or str(ws)
1681
1771
  if not os.path.isdir(_launch_cwd):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 2.11.154
3
+ Version: 2.11.155
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -91,6 +91,8 @@ tests/test_preflight_hb_gate.py
91
91
  tests/test_pretrust_claude.py
92
92
  tests/test_push_guard.py
93
93
  tests/test_realtime_event_freshness.py
94
+ tests/test_replica_base_workspace_fallback.py
95
+ tests/test_replica_boot_protocol_unconditional.py
94
96
  tests/test_rls_cross_tenant.py
95
97
  tests/test_rm_guard.py
96
98
  tests/test_rpc_grants.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "2.11.154"
7
+ version = "2.11.155"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,87 @@
1
+ """Regression test for task 9698d738 (Samuel P0): "réplicas prenden pero NO bootean".
2
+
3
+ REAL ROOT CAUSE (Firma A, empirically confirmed): a replica whose workspace was never
4
+ provisioned hits run_agent.py's ".mcp.json missing" path; setup_workspace() fails (e.g.
5
+ the keychain is unreadable non-interactively at hostd spawn) -> run_agent used to
6
+ `return 2` and the process DIED before the MCP server started (instance_id NULL,
7
+ heartbeat NEVER, never ran mc_acquire_agent_lease). Healthy replicas only differed by
8
+ having a PRE-EXISTING workspace (so they skipped setup_workspace).
9
+
10
+ FIX: _provision_replica_mcp_from_base() clones the BASE agent's known-good .mcp.json
11
+ for the replica (same user -> valid creds), so the replica boots even when fresh setup
12
+ fails. Plus a LOUD-fail (mc_resolve_launch status='failed') so a still-unprovisionable
13
+ replica is visible instead of a silent phantom (covered by the existing
14
+ _report_launch_failure reporter).
15
+
16
+ These tests exercise the fallback helper in isolation (run_agent's launch fn execs
17
+ claude + chdirs, so it isn't unit-callable end-to-end).
18
+ """
19
+ import json
20
+ import importlib
21
+
22
+ import pytest
23
+
24
+ ra = importlib.import_module("meshcode.run_agent")
25
+
26
+
27
+ def _write_base_workspace(root, project, base):
28
+ ws = root / f"{project}-{base}"
29
+ ws.mkdir(parents=True, exist_ok=True)
30
+ doc = {
31
+ "mcpServers": {
32
+ f"meshcode-{project}-{base}": {
33
+ "command": "python",
34
+ "args": ["-m", "meshcode.meshcode_mcp", "serve"],
35
+ "env": {
36
+ "MESHCODE_AGENT": base,
37
+ "MESHCODE_PROJECT": project,
38
+ "MESHCODE_PROJECT_ID": "pid-123",
39
+ "MESHCODE_KEYCHAIN_PROFILE": "default",
40
+ "MESHCODE_API_KEY": "mc_basekey",
41
+ },
42
+ }
43
+ }
44
+ }
45
+ (ws / ".mcp.json").write_text(json.dumps(doc), encoding="utf-8")
46
+ return ws
47
+
48
+
49
+ def test_replica_clones_base_mcp(tmp_path, monkeypatch):
50
+ monkeypatch.setattr(ra, "WORKSPACES_ROOT", tmp_path)
51
+ _write_base_workspace(tmp_path, "ca-growth", "commander")
52
+
53
+ replica_ws = tmp_path / "ca-growth-commander-1"
54
+ assert ra._provision_replica_mcp_from_base("ca-growth", "commander-1", replica_ws) is True
55
+
56
+ doc = json.loads((replica_ws / ".mcp.json").read_text(encoding="utf-8"))
57
+ servers = doc["mcpServers"]
58
+ # server id is rewritten to the replica's identity
59
+ assert "meshcode-ca-growth-commander-1" in servers
60
+ block = servers["meshcode-ca-growth-commander-1"]
61
+ # ONLY MESHCODE_AGENT changes — the rest is inherited from the base (valid creds)
62
+ assert block["env"]["MESHCODE_AGENT"] == "commander-1"
63
+ assert block["env"]["MESHCODE_API_KEY"] == "mc_basekey"
64
+ assert block["env"]["MESHCODE_PROJECT"] == "ca-growth"
65
+ assert block["env"]["MESHCODE_PROJECT_ID"] == "pid-123"
66
+ # command/args carried verbatim
67
+ assert block["args"] == ["-m", "meshcode.meshcode_mcp", "serve"]
68
+
69
+
70
+ def test_non_replica_name_is_not_cloned(tmp_path, monkeypatch):
71
+ monkeypatch.setattr(ra, "WORKSPACES_ROOT", tmp_path)
72
+ _write_base_workspace(tmp_path, "ca-growth", "commander")
73
+ # 'commander' has no -<N> suffix -> not a replica -> no clone attempted.
74
+ assert ra._provision_replica_mcp_from_base(
75
+ "ca-growth", "commander", tmp_path / "ca-growth-commander") is False
76
+
77
+
78
+ def test_missing_base_workspace_returns_false(tmp_path, monkeypatch):
79
+ monkeypatch.setattr(ra, "WORKSPACES_ROOT", tmp_path)
80
+ # base workspace absent -> nothing to clone -> False (caller then LOUD-fails).
81
+ assert ra._provision_replica_mcp_from_base(
82
+ "ca-growth", "commander-2", tmp_path / "ca-growth-commander-2") is False
83
+
84
+
85
+ if __name__ == "__main__":
86
+ import sys
87
+ sys.exit(pytest.main([__file__, "-q"]))
@@ -0,0 +1,64 @@
1
+ """Regression sentinel for task 9698d738 (Samuel P0): "réplicas prenden pero NO
2
+ bootean en TODAS las meshes".
3
+
4
+ ROOT CAUSE: run_agent.py injected the mesh boot protocol via
5
+ `--append-system-prompt MESHCODE_BOOT_PROTOCOL` ONLY inside `if repo_lock:`. But
6
+ hostd-spawned REPLICAS launch with repo_lock=None (no `--repo`) and frequently have
7
+ NO provisioned workspace CLAUDE.md (mc_replicate_agent creates the DB row, not the
8
+ disk workspace). With the --append gated off and no CLAUDE.md to fall back on, the
9
+ replica's Claude Code session received ZERO boot instructions -> it started (the
10
+ terminal "prende") but never ran SESSION START -> never called meshcode_boot ->
11
+ spawn-but-no-boot, in every mesh.
12
+
13
+ FIX: inject the boot protocol UNCONDITIONALLY (idempotent with a present CLAUDE.md).
14
+
15
+ This test fails if anyone re-nests the boot protocol under `if repo_lock:`.
16
+ Static AST guard (the launch fn execs claude + chdirs, so it isn't unit-callable).
17
+ """
18
+ import ast
19
+ import pathlib
20
+
21
+ RUN_AGENT = pathlib.Path(__file__).parent.parent / "meshcode" / "run_agent.py"
22
+
23
+
24
+ def _names_inside_repo_lock_true_branches(tree: ast.AST) -> set[str]:
25
+ """Every Name id referenced inside the TRUE branch of any `if repo_lock:`."""
26
+ found: set[str] = set()
27
+ for node in ast.walk(tree):
28
+ if isinstance(node, ast.If) and isinstance(node.test, ast.Name) \
29
+ and node.test.id == "repo_lock":
30
+ for stmt in node.body: # True branch only
31
+ for sub in ast.walk(stmt):
32
+ if isinstance(sub, ast.Name):
33
+ found.add(sub.id)
34
+ return found
35
+
36
+
37
+ def test_boot_protocol_injected_unconditionally():
38
+ tree = ast.parse(RUN_AGENT.read_text(encoding="utf-8"))
39
+
40
+ all_names = {n.id for n in ast.walk(tree) if isinstance(n, ast.Name)}
41
+ assert "MESHCODE_BOOT_PROTOCOL" in all_names, \
42
+ "MESHCODE_BOOT_PROTOCOL is no longer referenced in run_agent.py — boot protocol lost."
43
+
44
+ gated = _names_inside_repo_lock_true_branches(tree)
45
+ assert "MESHCODE_BOOT_PROTOCOL" not in gated, (
46
+ "REGRESSION (task 9698d738): the boot protocol (--append-system-prompt "
47
+ "MESHCODE_BOOT_PROTOCOL) is gated under `if repo_lock:` again. hostd-spawned "
48
+ "replicas have repo_lock=None and often no ws/CLAUDE.md -> they spawn but never "
49
+ "boot. Inject MESHCODE_BOOT_PROTOCOL unconditionally (outside the repo_lock if)."
50
+ )
51
+
52
+
53
+ def test_append_system_prompt_flag_present():
54
+ """The --append-system-prompt flag must still be emitted (it's the carrier)."""
55
+ consts = {n.value for n in ast.walk(ast.parse(RUN_AGENT.read_text(encoding="utf-8")))
56
+ if isinstance(n, ast.Constant) and isinstance(n.value, str)}
57
+ assert "--append-system-prompt" in consts, \
58
+ "--append-system-prompt flag missing — the boot protocol is no longer delivered."
59
+
60
+
61
+ if __name__ == "__main__":
62
+ test_boot_protocol_injected_unconditionally()
63
+ test_append_system_prompt_flag_present()
64
+ print("OK")
File without changes
File without changes
File without changes
File without changes