diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,24 @@
 # Changelog
 
+## [0.4.3.0] - 2026-10-22
+
+### Fixed
+
+* The per-mutation `[X/Y]` counter counts the mutations that have started,
+  instead of naming each mutation's position in the manifest.
+
+  The manifest's order is not the order the mutations are tested in: the
+  groups of a run are started most expensive first and run concurrently, so
+  the positions arrived in the log out of order and in jumps of hundreds,
+  which told a reader nothing about how far along the run was.
+
+* A progress line is no longer cut in half by the end-of-run report.
+
+  The mutation phase block-buffered its progress output, so a line could be
+  flushed in two pieces, with the report that is written to stdout landing
+  between them.  It is line-buffered now, like the coverage phase.
+
+
 ## [0.4.2.0] - 2026-10-13
 
 ### Changed
diff --git a/src/Test/Syd/Mutation/Driver/Mutate.hs b/src/Test/Syd/Mutation/Driver/Mutate.hs
--- a/src/Test/Syd/Mutation/Driver/Mutate.hs
+++ b/src/Test/Syd/Mutation/Driver/Mutate.hs
@@ -21,6 +21,7 @@
 
 import Control.Concurrent (newQSem, signalQSem, threadDelay, waitQSem)
 import Control.Concurrent.Async (mapConcurrently, race)
+import Control.Concurrent.MVar (MVar, modifyMVar_, newMVar)
 import Control.Concurrent.STM (atomically, modifyTVar', newTVarIO, readTVarIO)
 import Control.Exception (bracket, bracket_)
 import qualified Control.Exception as Exception
@@ -40,7 +41,7 @@
 import Path
 import Path.IO (copyFile, ensureDir, ignoringAbsence, withSystemTempDir)
 import System.Exit (ExitCode (..))
-import System.IO (BufferMode (..), IOMode (..), hFlush, hSetBuffering, stderr, withFile)
+import System.IO (BufferMode (..), IOMode (..), hSetBuffering, stderr, withFile)
 import System.Process.Typed (proc, setStderr, setStdout, setWorkingDir, startProcess, stopProcess, useHandleOpen, waitExitCode)
 import Test.Syd.Mutation.AugmentedManifest
   ( AugmentedManifest (..),
@@ -153,6 +154,35 @@
       childAugDir
       (TestBaselineMap (Map.restrictKeys allBaselines (Set.fromList coveringHere)))
 
+-- | Count a mutation that is about to be tested, and write its progress line.
+--
+-- The number a mutation is shown under is how many have started, itself
+-- included, so the counter climbs by one per line and a reader can see how far
+-- along a run is.  It is not an identifier: which mutation gets which number
+-- depends on the order the concurrent workers happen to reach here in, and
+-- every mutation the report has anything to say about is named there by id.
+--
+-- Taking the number and writing the line happen under the same lock, so the
+-- numbers reach the log in the order they were handed out, and a debug run's
+-- multi-line diffs arrive whole instead of interleaved.
+emitMutationProgress ::
+  -- | How many mutations have started.
+  MVar Int ->
+  -- | Debug mode: follow the line with the mutation's source diff.
+  Bool ->
+  -- | How many mutations the run tests in total.
+  Int ->
+  AugmentedMutationRecord ->
+  IO ()
+emitMutationProgress startedVar debug total record =
+  modifyMVar_ startedVar $ \started -> do
+    let index = started + 1
+    hPutChunksUtf8With
+      With8BitColours
+      stderr
+      (unlinesChunks (renderMutationProgressEvent debug index total (MutationProgressEvent record)))
+    pure index
+
 -- | Parent process: read @manifest-augmented.json@ and spawn one child
 -- subprocess per mutation per suite that covers it.
 --
@@ -200,7 +230,13 @@
   Map.Map Text SuiteConfig ->
   IO MutationRunReport
 runMutationMode failFast debug augDir outDir childMemLimit mutationJobs suiteConfigs = do
-  hSetBuffering stderr (BlockBuffering Nothing)
+  -- LineBuffering on stderr, as the coverage phase does, so each progress
+  -- line reaches the fd when it ends rather than when the buffer happens to
+  -- fill.  Under a block buffer a line is cut wherever the flush lands, and
+  -- the report this writes to stdout at the end arrives in the gap, which
+  -- leaves a run's log with a half-written progress line wrapped around the
+  -- summary.
+  hSetBuffering stderr LineBuffering
   ensureDir outDir
   AugmentedManifest groups <- readAugmentedManifestFile augDir
   -- Validate that every covering-suite name in the manifest is in
@@ -228,16 +264,8 @@
     Just j | j > 0 -> pure (fromIntegral j)
     _ -> getNumCapabilities
   sem <- newQSem n
-  -- Progress counter: assign every mutation a stable 1-based index in source
-  -- order up front, so the per-mutation progress line can show an [X/Y]
-  -- indication.  This is a pure, immutable map (no shared mutable counter):
-  -- each mutation's number is its position in the manifest, so the numbers are
-  -- deterministic and reproducible rather than dependent on the
-  -- non-deterministic order in which concurrent workers happen to start.
-  let orderedRecords = concat [recs | AugmentedMutationGroup recs <- groups]
-      totalMutations = length orderedRecords
-      indexByMutationId =
-        Map.fromList (zip (map augmentedMutationRecordId orderedRecords) [1 :: Int ..])
+  let totalMutations = sum [length recs | AugmentedMutationGroup recs <- groups]
+  startedVar <- newMVar (0 :: Int)
   -- Each group's per-mutation results, accumulated in source order so a
   -- partial report (after a global fail-fast abort) reflects the work
   -- done.
@@ -245,7 +273,7 @@
   let runGroup' (gix, AugmentedMutationGroup recs) =
         runOneGroup
           failFast
-          (runOne baselines indexByMutationId totalMutations sem)
+          (runOne baselines startedVar totalMutations sem)
           ( \r ->
               atomically $
                 modifyTVar' groupResultsVar (Map.insertWith (++) gix [r])
@@ -301,12 +329,6 @@
   let renderedChunks = renderMutationRunReport jsonReport
   writeReportTxt renderedChunks outDir
   putChunksUtf8With With8BitColours (unlinesChunks renderedChunks)
-  -- Force out any block-buffered progress events before we return.  This
-  -- matters even though we don't 'exitWith' here ourselves: callers (e.g.
-  -- 'runDriver' under --fail-fast) may, and a buffered stderr line that
-  -- lands after the caller's stdout summary block would look like trailing
-  -- garbage in the build log.
-  hFlush stderr
   case mWorkerException of
     Left e -> case Exception.fromException e of
       Just MutationFailFast -> pure ()
@@ -321,12 +343,10 @@
   -- when fail-fast trips.
   pure jsonReport
   where
-    runOne baselines indexByMutationId totalMutations sem record =
+    runOne baselines startedVar totalMutations sem record =
       bracket_ (waitQSem sem) (signalQSem sem) $ do
         let mid = augmentedMutationRecordId record
-            -- This mutation's stable 1-based position in the manifest.
-            index = Map.findWithDefault 0 mid indexByMutationId
-        hPutChunksUtf8With With8BitColours stderr (unlinesChunks (renderMutationProgressEvent debug index totalMutations (MutationProgressEvent record)))
+        emitMutationProgress startedVar debug totalMutations record
         -- Only run suites that have at least one covering test for this
         -- mutation.
         let coveringBySuite =
diff --git a/sydtest-mutation-driver.cabal b/sydtest-mutation-driver.cabal
--- a/sydtest-mutation-driver.cabal
+++ b/sydtest-mutation-driver.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           sydtest-mutation-driver
-version:        0.4.2.0
+version:        0.4.3.0
 synopsis:       Out-of-process mutation testing driver for sydtest.
 description:    Standalone driver executable that orchestrates the coverage and mutation phases of sydtest's mutation testing infrastructure. Spawns instrumented sydtest test suite executables as children.
 category:       Testing
