diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 956b527caed80d3f6bc1bba6d84ed188fe6bf232..a443c7b579be2a7906cbe37c215145d2ebac220f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -11,6 +11,7 @@ Added
 
 
 * Added more functions to the :class:`.fslmaths` wrapper (!431).
+* New :func:`.smoothest` wrapper function (!432).
 
 
 3.15.4 (Monday 27th November 2023)
diff --git a/doc/conf.py b/doc/conf.py
index 1e5e868f22fdc3ac48ad0fa58649f6225b32927b..073f4d5d29eb2d502bf516d2e81ed0764e24a7df 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -12,12 +12,70 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
+import glob
+import itertools as it
 import os
+import os.path as op
 import sys
 import datetime
 
 date = datetime.date.today()
 
+
+def check_for_missing_stubs():
+
+    docdir  = op.dirname(__file__)
+    basedir = op.join(docdir, '..')
+    modules = []
+
+    def tomodname(f):
+        if f.endswith('.py'):
+            f = f[:-3]
+        return op.relpath(op.join(dirpath, f), basedir).replace(op.sep, '.')
+
+    for dirpath, dirnames, filenames in os.walk(op.join(basedir, 'fsl')):
+        for d in dirnames:
+            if d == '__pycache__':
+                continue
+            if len(glob.glob(op.join(dirpath, d, '**', '*.py'), recursive=True)) == 0:
+                continue
+            modules.append(tomodname(d))
+
+        for f in filenames:
+            if not f.endswith('.py'):
+                continue
+            if f in ('__init__.py', '__main__.py'):
+                continue
+            modules.append(tomodname(f))
+
+    modules = [m for m in modules if not m.startswith('fsl.tests')]
+
+    # import fsl
+    # modules = recurse(fsl)
+    # modules = [m.name for m in modules]
+
+    # print()
+    # print()
+    # print()
+
+    for mod in modules:
+
+        docfile = op.join(docdir, f'{mod}.rst')
+
+        if not op.exists(docfile):
+            print(f'No doc file found for module: {mod}')
+
+    for docfile in glob.glob(op.join(docdir, '*.rst')):
+        docfile = op.relpath(docfile, basedir)
+        mod     = op.splitext(op.basename(docfile))[0]
+        if mod not in modules:
+            print(f'No module found for doc file: {docfile}')
+
+
+if __name__ == '__main__':
+    check_for_missing_stubs()
+
+
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
diff --git a/doc/fsl.wrappers.cluster_commands.rst b/doc/fsl.wrappers.cluster_commands.rst
new file mode 100644
index 0000000000000000000000000000000000000000..6749dc5be4c4ac104e436eaf39df919ac100f658
--- /dev/null
+++ b/doc/fsl.wrappers.cluster_commands.rst
@@ -0,0 +1,7 @@
+``fsl.wrappers.cluster_commands``
+=================================
+
+.. automodule:: fsl.wrappers.cluster_commands
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/doc/fsl.wrappers.oxford_asl.rst b/doc/fsl.wrappers.oxford_asl.rst
new file mode 100644
index 0000000000000000000000000000000000000000..cfdbdccb7b2792c27b5c5029ac3ce481c0a2a5eb
--- /dev/null
+++ b/doc/fsl.wrappers.oxford_asl.rst
@@ -0,0 +1,7 @@
+``fsl.wrappers.oxford_asl``
+===========================
+
+.. automodule:: fsl.wrappers.oxford_asl
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/doc/fsl.wrappers.rst b/doc/fsl.wrappers.rst
index 6277046d469cf4e0ce0cc6bd070ac286ab28b47e..792a188f11e2dab51ea062dc3ceff9a30a8c3dd7 100644
--- a/doc/fsl.wrappers.rst
+++ b/doc/fsl.wrappers.rst
@@ -8,6 +8,7 @@
    fsl.wrappers.bedpostx
    fsl.wrappers.bet
    fsl.wrappers.bianca
+   fsl.wrappers.cluster_commands
    fsl.wrappers.dtifit
    fsl.wrappers.eddy
    fsl.wrappers.epi_reg
@@ -23,6 +24,7 @@
    fsl.wrappers.fugue
    fsl.wrappers.melodic
    fsl.wrappers.misc
+   fsl.wrappers.oxford_asl
    fsl.wrappers.randomise
    fsl.wrappers.tbss
    fsl.wrappers.wrapperutils
diff --git a/fsl/tests/test_wrappers/test_cluster.py b/fsl/tests/test_wrappers/test_cluster.py
index f6b92c58e16bafe17a3ce0a5e2d3849a8c9d41cc..6e0cfb8763b73922634ef4d1a0963f31435a6f14 100644
--- a/fsl/tests/test_wrappers/test_cluster.py
+++ b/fsl/tests/test_wrappers/test_cluster.py
@@ -9,10 +9,17 @@ import contextlib
 import os
 import os.path as op
 import sys
+import textwrap as tw
+
+from unittest import mock
 
 import numpy as np
 
-from fsl.wrappers.cluster import cluster, _cluster
+from fsl.wrappers import wrapperutils as wutils
+from fsl.wrappers.cluster_commands import (cluster,
+                                           _cluster,
+                                           smoothest,
+                                           _smoothest)
 
 from .  import testenv
 from .. import mockFSLDIR
@@ -67,3 +74,56 @@ def test_cluster():
     assert np.all(np.isclose(data, expected))
     assert ''.join(titles) == mock_titles
     assert result1.stdout == result2.stdout
+
+
+def test_smoothest_wrapper():
+    with testenv('smoothest') as smoothest_exe:
+        result   = _smoothest(res='res', zstat='zstat', d=5, V=True)
+        expected = f'{smoothest_exe} --res=res --zstat=zstat -d 5 -V'
+        assert result.stdout[0] == expected
+
+        # auto detect residuals vs zstat
+        result   = _smoothest('res4d.nii.gz', d=5, V=True)
+        expected = f'{smoothest_exe} -d 5 -V --res=res4d.nii.gz'
+        assert result.stdout[0] == expected
+
+        result   = _smoothest('zstat1.nii.gz', d=5, V=True)
+        expected = f'{smoothest_exe} -d 5 -V --zstat=zstat1.nii.gz'
+        assert result.stdout[0] == expected
+
+
+def test_smoothest():
+
+    result = tw.dedent("""
+    FWHMx = 4.763 mm, FWHMy = 5.06668 mm, FWHMz = 4.71527 mm
+    DLH 0.324569 voxels^-3
+    VOLUME 244531 voxels
+    RESELS 14.224 voxels per resel
+    DLH 0.324569
+    VOLUME 244531
+    RESELS 14.224
+    FWHMvoxel 2.3815 2.53334 2.35763
+    FWHMmm 4.763 5.06668 4.71527
+    """)
+
+    result = wutils.FileOrThing.Results((result, ''))
+
+    expect = {
+        'DLH'       : 0.324569,
+        'VOLUME'    : 244531,
+        'RESELS'    : 14.224,
+        'FWHMvoxel' : [2.3815, 2.53334, 2.35763],
+        'FWHMmm'    : [4.763,  5.06668, 4.71527]
+    }
+
+    with mock.patch('fsl.wrappers.cluster_commands._smoothest',
+                    return_value=result):
+
+        result = smoothest('inimage')
+
+        assert result.keys() == expect.keys()
+        assert        np.isclose(result['DLH'],       expect['DLH'])
+        assert        np.isclose(result['VOLUME'],    expect['VOLUME'])
+        assert        np.isclose(result['RESELS'],    expect['RESELS'])
+        assert np.all(np.isclose(result['FWHMvoxel'], expect['FWHMvoxel']))
+        assert np.all(np.isclose(result['FWHMmm'],    expect['FWHMmm']))
diff --git a/fsl/wrappers/__init__.py b/fsl/wrappers/__init__.py
index aa90bb4566e7f7c8487d4344f8294bf05a9bc2ef..124c2115037eda8bcb23628f24ebe1261b45808c 100755
--- a/fsl/wrappers/__init__.py
+++ b/fsl/wrappers/__init__.py
@@ -101,7 +101,8 @@ from fsl.wrappers.wrapperutils       import (LOAD,
                                              fslwrapper,
                                              funcwrapper)
 from fsl.wrappers                    import (tbss,)
-from fsl.wrappers.cluster            import (cluster,)
+from fsl.wrappers.cluster_commands   import (cluster,
+                                             smoothest)
 from fsl.wrappers.bet                import (bet,
                                              robustfov)
 from fsl.wrappers.eddy               import (eddy,
diff --git a/fsl/wrappers/cluster.py b/fsl/wrappers/cluster_commands.py
similarity index 55%
rename from fsl/wrappers/cluster.py
rename to fsl/wrappers/cluster_commands.py
index 046a28ef597feeca52ec386a1956615acb5eb635..c3b28e96f1f91cd0b5c519afb6ac02505bcce7c0 100644
--- a/fsl/wrappers/cluster.py
+++ b/fsl/wrappers/cluster_commands.py
@@ -66,3 +66,59 @@ def _cluster(infile, thresh, **kwargs):
     cmd += wutils.applyArgStyle('--=', valmap=valmap, **kwargs)
 
     return cmd
+
+
+def smoothest(inimg=None, **kwargs):
+    """Wrapper for the ``smoothest`` command.
+
+    The residual or zstatistic image may be passed as the first positional
+    argument (``inimg``) - its type is inferred from the image file name if
+    possible.  If this is not possible (e.g. non-standard file names or
+    in-memory images), you must specify residual images via ``res``, or
+    zstatistic images via ``zstat``.
+
+    Returns a dictionary containing the parameters estimated by ``smoothest``,
+    e.g.::
+
+        {
+            'DLH'       : 1.25903,
+            'VOLUME'    : 239991,
+            'RESELS'    : 3.69574,
+            'FWHMvoxel' : [1.57816, 1.64219, 1.42603],
+            'FWHMmm'    : [3.15631, 3.28437, 2.85206]
+        }
+    """
+    result = _smoothest(**kwargs)
+    result = result.stdout[0]
+    result = result.strip().split('\n')[-5:]
+    values = {}
+
+    for line in result:
+        key, vals = line.split(maxsplit=1)
+        vals      = [float(v) for v in vals.split()]
+
+        if len(vals) == 1:
+            vals = vals[0]
+
+        values[key] = vals
+
+    return values
+
+
+@wutils.fileOrImage('inimg', 'r', 'res', 'z', 'zstat', 'm', 'mask')
+@wutils.fslwrapper
+def _smoothest(inimg=None, **kwargs):
+    """Actual wrapper for the ``smoothest`` command."""
+
+    if inimg is not None:
+        if   'res4d' in inimg: kwargs['res']   = inimg
+        elif 'zstat' in inimg: kwargs['zstat'] = inimg
+        else: raise RuntimeError('Cannot infer type of input '
+                                 f'image {inimg.name}')
+
+    valmap = {
+        'V'       : wutils.SHOW_IF_TRUE,
+        'verbose' : wutils.SHOW_IF_TRUE,
+    }
+
+    return ['smoothest'] + wutils.applyArgStyle('--=', valmap=valmap, **kwargs)
diff --git a/fsl/wrappers/fnirt.py b/fsl/wrappers/fnirt.py
index f4ca120fceebed4cdd97310087a1f816e544a979..d5c5bd21a02603522e74b810152ed0c9ea665e50 100644
--- a/fsl/wrappers/fnirt.py
+++ b/fsl/wrappers/fnirt.py
@@ -32,8 +32,13 @@ def fnirt(src, **kwargs):
 
     asrt.assertIsNifti(src)
 
+    valmap = {
+        'v'       : wutils.SHOW_IF_TRUE,
+        'verbose' : wutils.SHOW_IF_TRUE,
+    }
+
     cmd  = ['fnirt', '--in={}'.format(src)]
-    cmd += wutils.applyArgStyle('--=', **kwargs)
+    cmd += wutils.applyArgStyle('--=', valmap=valmap, **kwargs)
 
     return cmd
 
diff --git a/fsl/wrappers/fslstats.py b/fsl/wrappers/fslstats.py
index 67942fba59f1b79f34d5a18a93bcd34778a2fc35..c25dc238d55169962b2f0776f4ab472ee14ac788 100644
--- a/fsl/wrappers/fslstats.py
+++ b/fsl/wrappers/fslstats.py
@@ -31,20 +31,28 @@ class fslstats:
                  present in older versions.
 
 
+    This ``fslstats`` command::
+
+        fslstats image -r -p 95 -R
+
+
+    is equivalent to this function call::
+
+        fslstats('image').r.p(95).R.run()
+
+
     Any ``fslstats`` command-line option which does not require any arguments
     (e.g. ``-r``) can be set by accessing an attribute on a ``fslstats``
     object, e.g.::
 
-        stats = fslstats('image.nii.gz')
-        stats.r
+        fslstats('image.nii.gz').r.run()
 
 
     ``fslstats`` command-line options which do require additional arguments
     (e.g. ``-k``) can be set by calling a method on an ``fslstats`` object,
     e.g.::
 
-        stats = fslstats('image.nii.gz')
-        stats.k('mask.nii.gz')
+        stats = fslstats('image.nii.gz').k('mask.nii.gz').run()
 
 
     The ``fslstats`` command can be executed via the :meth:`run` method.