# Refreshed CLI evidence for issue 19018

PR: [#23159](https://git.cafe/pingdotgg/t3code/pull/23159). Render matrix: EMPTY; CLI only.

Final: a007595b42e2237ac5b8d91b1f294d24af2446e2.
Red: 44bd595e77a0622ab69bcdd0f34970b9b252bbc6.
Before baseline: a12d6ed9428d8e64d76ca86d178abbf564b57cee.
Superseded evidence: 9bf8c50e9a2976569d06a3cdbb2fedc6919fe1f2.

All output below was captured freshly, including the red test. No historical test output is reused.

Results:
- Baseline CLI: exit 0; populated V1 source has 1 thread, resulting V2 has 0.
- Final populated V1: exit 1, MigrateDevDbLegacySourceError; destination DB and nonempty live WAL SHA-256 unchanged; sentinel retained.
- Final schema-V2 with pending legacy: same refusal and preservation, exit 1.
- Final empty V1: exit 0; resulting V2 has 0 threads.
- Red focused suite: exit 1; only `refuses a legacy thread committed after the guard` fails; 1 failed / 9 passed.
- Final focused suite: exit 0; 10 passed, including late-write refusal and DB/WAL preservation, normal V2 retention, imported legacy acceptance, and pending-work clearing.
- Normal V2 oracle: 9 source threads -> exactly fork-thread, settled-thread, stopped-thread; pending auth/tasks/transfers cleared.
- Synthetic source bytes/counts unchanged. Worktree clean before and after.

The fix validates the attached read-only source inside the copy transaction. It also moves the destination checkpoint after the copy so refusal preserves destination DB/WAL. The red and green test files are identical (verified below). The race test injects the legacy commit through FileSystem.makeDirectory at the destination boundary, after the old guard and before copying. No sleeps or timing-based synchronization.

Reproduce: `python3 /tmp/triage-19018/evidence/repro.py /home/ubuntu/wt/issue-19018`.
Requires Node 24, Python 3, Git, installed dependencies, and these commits locally. The standalone script pins final HEAD, creates synthetic fixtures and detached baseline/red worktrees under /tmp, shares installed dependencies by symlink, runs only migrate-dev-db.test.ts, and leaves evidence for inspection. It never starts a server or accesses live T3 data. No tracked repository edits, pushes, public forge writes, browser, subagents, typechecks, or broad tests.

FRICTION evidence|existing artifacts pinned superseded 9bf8c50e9a|refreshed CLI and all 10 focused tests at exact final HEAD plus fresh red at 44bd595e77
FRICTION capture|verbose reporter flag also appeared as an extra git diff pathspec|captured corrected diff separately; exit 0 confirms identical test files; standalone repro corrected

## Actual commands and outputs

```text
Synthetic-only run directory: /tmp/issue19018-synthetic-nhhcl3ww

$ (cd /home/ubuntu/wt/issue-19018 && node --version)
v24.22.1
EXIT_STATUS=0

$ (cd /home/ubuntu/wt/issue-19018 && git rev-parse HEAD)
a007595b42e2237ac5b8d91b1f294d24af2446e2
EXIT_STATUS=0

$ (cd /home/ubuntu/wt/issue-19018 && git status --porcelain)

EXIT_STATUS=0

$ (cd /home/ubuntu/wt/issue-19018 && git rev-parse 70e5e384da)
70e5e384dab96a1d1e4a253ee5a9415ed5d8ce30
EXIT_STATUS=0

$ (cd /home/ubuntu/wt/issue-19018 && git worktree add --quiet --detach /tmp/issue19018-synthetic-nhhcl3ww/baseline-worktree a12d6ed942)

EXIT_STATUS=0

$ (cd /tmp/issue19018-synthetic-nhhcl3ww/baseline-worktree && git rev-parse HEAD)
a12d6ed9428d8e64d76ca86d178abbf564b57cee
EXIT_STATUS=0

$ (cd /home/ubuntu/wt/issue-19018 && git diff --exit-code 70e5e384da a12d6ed942 -- apps/server/scripts/migrate-dev-db.ts)

EXIT_STATUS=0

$ (cd /home/ubuntu/wt/issue-19018 && node /tmp/issue19018-synthetic-nhhcl3ww/fixture.mjs /tmp/issue19018-synthetic-nhhcl3ww/populated-v1.sqlite)
[07:52:53.661] INFO (#1): Migrations ran successfully {
  migrations: [
    '1_OrchestrationEvents',
    '2_OrchestrationCommandReceipts',
    '3_CheckpointDiffBlobs',
    '4_ProviderSessionRuntime',
    '5_Projections',
    '6_ProjectionThreadSessionRuntimeModeColumns',
    '7_ProjectionThreadMessageAttachments',
    '8_ProjectionThreadActivitySequence',
    '9_ProviderSessionRuntimeMode',
    '10_ProjectionThreadsRuntimeMode',
    '11_OrchestrationThreadCreatedRuntimeMode',
    '12_ProjectionThreadsInteractionMode',
    '13_ProjectionThreadProposedPlans',
    '14_ProjectionThreadProposedPlanImplementation',
    '15_ProjectionTurnsSourceProposedPlan',
    '16_CanonicalizeModelSelections',
    '17_ProjectionThreadsArchivedAt',
    '18_ProjectionThreadsArchivedAtIndex',
    '19_ProjectionSnapshotLookupIndexes',
    '20_AuthAccessManagement',
    '21_AuthSessionClientMetadata',
    '22_AuthSessionLastConnectedAt',
    '23_ProjectionThreadShellSummary',
    '24_BackfillProjectionThreadShellSummary',
    '25_CleanupInvalidProjectionPendingApprovals',
    '26_CanonicalizeModelSelectionOptions',
    '27_ProviderSessionRuntimeInstanceId',
    '28_ProjectionThreadSessionInstanceId',
    '29_ProjectionThreadDetailOrderingIndexes',
    '30_ProjectionThreadShellArchiveIndexes',
    '31_AuthAuthorizationScopes',
    '32_AuthPairingProofKeyThumbprint',
    '33_ProjectionThreadsSettled',
    '34_ProjectionThreadsSnoozed',
    '35_ProjectionThreadTitleRegeneration',
    '36_ProjectionThreadsPinned',
    '37_ProjectionTurnsKeysetIndex',
    '38_ProjectionThreadsPinOrderKey',
    '39_ProjectionProjectsDefaultThreadEnvMode',
    '40_ProjectionProjectFaviconPath',
    '41_AuthSessionClientConnection',
    '42_ProjectionThreadLinkedPullRequest',
    '43_ProjectionThreadsUnsettledAt',
    '44_ClearAutomaticProjectModelDefaults',
    '45_ProjectionProjectsAutoPull',
    '46_RepairAutomaticSettlementTimestamps',
    '47_ProjectionProjectIcon',
    '48_ProjectionThreadBranchPullRequest',
    '49_ProjectionThreadsActiveOrderKey',
    '50_ProjectionThreadPullRequests',
    '51_ProjectionThreadMessageContext',
    '52_ProjectionThreadTitleState',
    '53_PullRequestFilesViewed',
    '54_ProjectionThreadsAutoSettleDisabledAt'
  ]
}
EXIT_STATUS=0
SQLITE_INSPECTION {"exists": true, "orchestration_events": 0, "path": "/tmp/issue19018-synthetic-nhhcl3ww/populated-v1.sqlite", "projection_projects": 1, "projection_threads": 1, "sha256": "a7d91e2b504c9b0155c1b1e777433ac43aa0647c4cc82830a34ff04a3d15826d"}
SQLITE_INSPECTION {"exists": true, "orchestration_events": 0, "path": "/tmp/issue19018-synthetic-nhhcl3ww/empty-v1.sqlite", "projection_projects": 1, "projection_threads": 0, "sha256": "ffbd1076ac889cb3cebfb47549e80e37fd3a737030c0643495c624edd3879673"}

$ (cd /tmp/issue19018-synthetic-nhhcl3ww/baseline-worktree && node apps/server/scripts/migrate-dev-db.ts --source /tmp/issue19018-synthetic-nhhcl3ww/populated-v1.sqlite --base-dir /tmp/issue19018-synthetic-nhhcl3ww/before --projects 5 --threads-per-project 10)
Copying visible threads from /tmp/issue19018-synthetic-nhhcl3ww/populated-v1.sqlite (read-only)...
Running migrations on the snapshot...
[07:52:56.325] INFO (#1): Migrations ran successfully {
  migrations: [
    '55_OrchestrationV2',
    '56_RemoveRedundantProjectionIndexes',
    '57_ScheduledTaskWebhooks',
    '58_WebhookRelayDeliveries',
    '59_McpAppModelContext',
    '60_ThreadSnapshotWindowIndexes',
    '61_PullRequestReviewGuides',
    '62_ThreadFailureLookupIndex',
    '63_DropUnusedV1Projections',
    '64_CanonicalizeThreadPullRequestLinks',
    '65_CodexUsageNativeTurnIndex',
    '66_RepairClientSettlementTimestamps',
    '67_WebhookDedupe'
  ]
}
Pruning to 5 projects, 10 stopped threads each...
Compacting into /tmp/issue19018-synthetic-nhhcl3ww/before/userdata/statev2.sqlite...

Dev database ready: /tmp/issue19018-synthetic-nhhcl3ww/before/userdata/statev2.sqlite (796 KB)
  Synthetic Repro: 0 threads
  1 orchestration events kept
  Migrations applied: 55_OrchestrationV2, 56_RemoveRedundantProjectionIndexes, 57_ScheduledTaskWebhooks, 58_WebhookRelayDeliveries, 59_McpAppModelContext, 60_ThreadSnapshotWindowIndexes, 61_PullRequestReviewGuides, 62_ThreadFailureLookupIndex, 63_DropUnusedV1Projections, 64_CanonicalizeThreadPullRequestLinks, 65_CodexUsageNativeTurnIndex, 66_RepairClientSettlementTimestamps, 67_WebhookDedupe
EXIT_STATUS=0
SQLITE_INSPECTION {"exists": true, "orchestration_events": 1, "orchestration_v2_projection_threads": 0, "path": "/tmp/issue19018-synthetic-nhhcl3ww/before/userdata/statev2.sqlite", "projection_projects": 1, "projection_threads": 0, "sha256": "33cd6d4fb4118a7589ebb49b3c3e559b4c16df5520ba93a85aa7850c37cadbd8"}

$ (cd /home/ubuntu/wt/issue-19018 && node /tmp/issue19018-synthetic-nhhcl3ww/migrate.mjs /tmp/issue19018-synthetic-nhhcl3ww/v2-schema-pending-legacy.sqlite)
[07:52:58.053] INFO (#1): Migrations ran successfully {
  migrations: [
    '55_OrchestrationV2',
    '56_RemoveRedundantProjectionIndexes',
    '57_ScheduledTaskWebhooks',
    '58_WebhookRelayDeliveries',
    '59_McpAppModelContext',
    '60_ThreadSnapshotWindowIndexes',
    '61_PullRequestReviewGuides',
    '62_ThreadFailureLookupIndex',
    '63_DropUnusedV1Projections',
    '64_CanonicalizeThreadPullRequestLinks',
    '65_CodexUsageNativeTurnIndex',
    '66_RepairClientSettlementTimestamps',
    '67_WebhookDedupe'
  ]
}
EXIT_STATUS=0
SQLITE_INSPECTION {"exists": true, "orchestration_events": 1, "orchestration_v2_projection_threads": 0, "path": "/tmp/issue19018-synthetic-nhhcl3ww/v2-schema-pending-legacy.sqlite", "projection_projects": 1, "projection_threads": 1, "sha256": "ce9327c4a7c4d2cc6dfdc17eb31396e9cfd090d315c3c1f2ea1c54ed62631aa2"}
DESTINATION_BEFORE {"/tmp/issue19018-synthetic-nhhcl3ww/after-populated-v1/userdata/statev2.sqlite": "e3ba06536f7dbba337dee3c1c5f01b43660ce276abb54c5cee2d5defc5b970aa", "/tmp/issue19018-synthetic-nhhcl3ww/after-populated-v1/userdata/statev2.sqlite-wal": "30f7a61703b5c63c0a127ddd1a1e9d910a84ac8ef4dd1742c7e7491abc406527"}

$ (cd /home/ubuntu/wt/issue-19018 && node apps/server/scripts/migrate-dev-db.ts --source /tmp/issue19018-synthetic-nhhcl3ww/populated-v1.sqlite --base-dir /tmp/issue19018-synthetic-nhhcl3ww/after-populated-v1 --projects 5 --threads-per-project 10)
Copying visible threads from /tmp/issue19018-synthetic-nhhcl3ww/populated-v1.sqlite (read-only)...
[07:53:00.025] ERROR (#1): MigrateDevDbLegacySourceError: Source database '/tmp/issue19018-synthetic-nhhcl3ww/populated-v1.sqlite' holds 1 legacy threads the server has not imported into V2 yet, which this command cannot carry over. Start a server once on a T3 home holding a copy of it so it imports them, then pass the userdata/statev2.sqlite that server leaves behind as --source.
    at file:///home/ubuntu/wt/issue-19018/apps/server/scripts/migrate-dev-db.ts:282:25
    at copyDevDbSourceSlice (file:///home/ubuntu/wt/issue-19018/apps/server/scripts/migrate-dev-db.ts:522:12)
    at copyDevDbSourceSlice (definition) (file:///home/ubuntu/wt/issue-19018/apps/server/scripts/migrate-dev-db.ts:249:32)
    at runMigrateDevDb (file:///home/ubuntu/wt/issue-19018/apps/server/scripts/migrate-dev-db.ts:638:29)
    at runMigrateDevDb (definition) (file:///home/ubuntu/wt/issue-19018/apps/server/scripts/migrate-dev-db.ts:455:39)
EXIT_STATUS=1
DESTINATION_AFTER {"/tmp/issue19018-synthetic-nhhcl3ww/after-populated-v1/userdata/statev2.sqlite": "e3ba06536f7dbba337dee3c1c5f01b43660ce276abb54c5cee2d5defc5b970aa", "/tmp/issue19018-synthetic-nhhcl3ww/after-populated-v1/userdata/statev2.sqlite-wal": "30f7a61703b5c63c0a127ddd1a1e9d910a84ac8ef4dd1742c7e7491abc406527"}
ASSERTION: populated-v1 rejection preserves database and live WAL SHA-256 and sentinel
DESTINATION_BEFORE {"/tmp/issue19018-synthetic-nhhcl3ww/after-v2-schema-pending-legacy/userdata/statev2.sqlite": "e3ba06536f7dbba337dee3c1c5f01b43660ce276abb54c5cee2d5defc5b970aa", "/tmp/issue19018-synthetic-nhhcl3ww/after-v2-schema-pending-legacy/userdata/statev2.sqlite-wal": "5a7025e7f04f3405d688703cca72ac6838c11f0ebdf1af1abf63cbf14e2cf0b3"}

$ (cd /home/ubuntu/wt/issue-19018 && node apps/server/scripts/migrate-dev-db.ts --source /tmp/issue19018-synthetic-nhhcl3ww/v2-schema-pending-legacy.sqlite --base-dir /tmp/issue19018-synthetic-nhhcl3ww/after-v2-schema-pending-legacy --projects 5 --threads-per-project 10)
Copying visible threads from /tmp/issue19018-synthetic-nhhcl3ww/v2-schema-pending-legacy.sqlite (read-only)...
[07:53:02.174] ERROR (#1): MigrateDevDbLegacySourceError: Source database '/tmp/issue19018-synthetic-nhhcl3ww/v2-schema-pending-legacy.sqlite' holds 1 legacy threads the server has not imported into V2 yet, which this command cannot carry over. Start a server once on a T3 home holding a copy of it so it imports them, then pass the userdata/statev2.sqlite that server leaves behind as --source.
    at file:///home/ubuntu/wt/issue-19018/apps/server/scripts/migrate-dev-db.ts:282:25
    at copyDevDbSourceSlice (file:///home/ubuntu/wt/issue-19018/apps/server/scripts/migrate-dev-db.ts:522:12)
    at copyDevDbSourceSlice (definition) (file:///home/ubuntu/wt/issue-19018/apps/server/scripts/migrate-dev-db.ts:249:32)
    at runMigrateDevDb (file:///home/ubuntu/wt/issue-19018/apps/server/scripts/migrate-dev-db.ts:638:29)
    at runMigrateDevDb (definition) (file:///home/ubuntu/wt/issue-19018/apps/server/scripts/migrate-dev-db.ts:455:39)
EXIT_STATUS=1
DESTINATION_AFTER {"/tmp/issue19018-synthetic-nhhcl3ww/after-v2-schema-pending-legacy/userdata/statev2.sqlite": "e3ba06536f7dbba337dee3c1c5f01b43660ce276abb54c5cee2d5defc5b970aa", "/tmp/issue19018-synthetic-nhhcl3ww/after-v2-schema-pending-legacy/userdata/statev2.sqlite-wal": "5a7025e7f04f3405d688703cca72ac6838c11f0ebdf1af1abf63cbf14e2cf0b3"}
ASSERTION: v2-schema-pending-legacy rejection preserves database and live WAL SHA-256 and sentinel
SQLITE_INSPECTION {"exists": true, "orchestration_events": 1, "orchestration_v2_projection_threads": 0, "path": "/tmp/issue19018-synthetic-nhhcl3ww/v2-schema-pending-legacy.sqlite", "projection_projects": 1, "projection_threads": 1, "sha256": "ce9327c4a7c4d2cc6dfdc17eb31396e9cfd090d315c3c1f2ea1c54ed62631aa2"}

$ (cd /home/ubuntu/wt/issue-19018 && node apps/server/scripts/migrate-dev-db.ts --source /tmp/issue19018-synthetic-nhhcl3ww/empty-v1.sqlite --base-dir /tmp/issue19018-synthetic-nhhcl3ww/empty-success --projects 5 --threads-per-project 10)
Copying visible threads from /tmp/issue19018-synthetic-nhhcl3ww/empty-v1.sqlite (read-only)...
Running migrations on the snapshot...
[07:53:04.323] INFO (#1): Migrations ran successfully {
  migrations: [
    '55_OrchestrationV2',
    '56_RemoveRedundantProjectionIndexes',
    '57_ScheduledTaskWebhooks',
    '58_WebhookRelayDeliveries',
    '59_McpAppModelContext',
    '60_ThreadSnapshotWindowIndexes',
    '61_PullRequestReviewGuides',
    '62_ThreadFailureLookupIndex',
    '63_DropUnusedV1Projections',
    '64_CanonicalizeThreadPullRequestLinks',
    '65_CodexUsageNativeTurnIndex',
    '66_RepairClientSettlementTimestamps',
    '67_WebhookDedupe'
  ]
}
Pruning to 5 projects, 10 stopped threads each...
Compacting into /tmp/issue19018-synthetic-nhhcl3ww/empty-success/userdata/statev2.sqlite...

Dev database ready: /tmp/issue19018-synthetic-nhhcl3ww/empty-success/userdata/statev2.sqlite (796 KB)
  Synthetic Repro: 0 threads
  1 orchestration events kept
  Migrations applied: 55_OrchestrationV2, 56_RemoveRedundantProjectionIndexes, 57_ScheduledTaskWebhooks, 58_WebhookRelayDeliveries, 59_McpAppModelContext, 60_ThreadSnapshotWindowIndexes, 61_PullRequestReviewGuides, 62_ThreadFailureLookupIndex, 63_DropUnusedV1Projections, 64_CanonicalizeThreadPullRequestLinks, 65_CodexUsageNativeTurnIndex, 66_RepairClientSettlementTimestamps, 67_WebhookDedupe
EXIT_STATUS=0
SQLITE_INSPECTION {"exists": true, "orchestration_events": 1, "orchestration_v2_projection_threads": 0, "path": "/tmp/issue19018-synthetic-nhhcl3ww/empty-success/userdata/statev2.sqlite", "projection_projects": 1, "projection_threads": 0, "sha256": "903497c7ccc8c89d95eb73d420e0f31f7559bdfe4f6fb4737e363169c53e1ece"}
SQLITE_INSPECTION {"exists": true, "orchestration_events": 0, "path": "/tmp/issue19018-synthetic-nhhcl3ww/populated-v1.sqlite", "projection_projects": 1, "projection_threads": 1, "sha256": "a7d91e2b504c9b0155c1b1e777433ac43aa0647c4cc82830a34ff04a3d15826d"}
ASSERTION: populated V1 source bytes/counts unchanged across both CLI runs

$ (cd /home/ubuntu/wt/issue-19018 && git worktree add --quiet --detach /tmp/issue19018-synthetic-nhhcl3ww/red-worktree 44bd595e77)

EXIT_STATUS=0

$ (cd /tmp/issue19018-synthetic-nhhcl3ww/red-worktree && git rev-parse HEAD)
44bd595e77a0622ab69bcdd0f34970b9b252bbc6
EXIT_STATUS=0

$ (cd /home/ubuntu/wt/issue-19018 && git diff --exit-code 44bd595e77 HEAD -- apps/server/scripts/migrate-dev-db.test.ts --reporter=verbose)

EXIT_STATUS=0

$ (cd /tmp/issue19018-synthetic-nhhcl3ww/red-worktree && ./node_modules/.bin/vp test run apps/server/scripts/migrate-dev-db.test.ts --reporter=verbose)
note: You are running `vp test` as a Vite+ built-in command. If you meant to run the test npm script, use `vpr test` instead.

 RUN  v5.0.5 /tmp/issue19018-synthetic-nhhcl3ww/red-worktree

 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > keeps stopped and settled thread families and clears pending work 571ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > never copies an archived thread, and new events append after the source's 725ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > skips a thread family larger than the per-project limit 513ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > upgrades a source from before the V2 thread tables 293ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses a pre-V2 source 760ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses a V2 source whose legacy threads were n… 402ms
stdout | scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses a legacy thread committed after the guard
[07:53:52.159] ERROR (#2506): [Error: {"databasePath":"/tmp/migrate-dev-db-race-dest-zLvqFv/userdata/statev2.sqlite","sizeBytes":815104,"projects":[{"title":"Legacy","threads":0}],"eventCount":1,"executedMigrations":["55_OrchestrationV2","56_RemoveRedundantProjectionIndexes","57_ScheduledTaskWebhooks","58_WebhookRelayDeliveries","59_McpAppModelContext","60_ThreadSnapshotWindowIndexes","61_PullRequestReviewGuides","62_ThreadFailureLookupIndex","63_DropUnusedV1Projections","64_CanonicalizeThreadPullRequestLinks","65_CodexUsageNativeTurnIndex","66_RepairClientSettlementTimestamps","67_WebhookDedupe"]}] {
  databasePath: '/tmp/migrate-dev-db-race-dest-zLvqFv/userdata/statev2.sqlite',
  sizeBytes: 815104,
  projects: [ [Object: null prototype] { title: 'Legacy', threads: 0 } ],
  eventCount: 1,
  executedMigrations: [
    '55_OrchestrationV2',
    '56_RemoveRedundantProjectionIndexes',
    '57_ScheduledTaskWebhooks',
    '58_WebhookRelayDeliveries',
    '59_McpAppModelContext',
    '60_ThreadSnapshotWindowIndexes',
    '61_PullRequestReviewGuides',
    '62_ThreadFailureLookupIndex',
    '63_DropUnusedV1Projections',
    '64_CanonicalizeThreadPullRequestLinks',
    '65_CodexUsageNativeTurnIndex',
    '66_RepairClientSettlementTimestamps',
    '67_WebhookDedupe'
  ],
  [cause]: undefined
}

 × |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses a legacy thread committed after the guard 538ms
   → undefined
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses while a dev server holds the destination 296ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses a source that resolves to a destination path 5ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses to rebuild the shared home 257ms

⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses a legacy thread committed after the guard
{
  databasePath: '/tmp/migrate-dev-db-race-dest-zLvqFv/userdata/statev2.sqlite',
  sizeBytes: 815104,
  projects: [ { title: 'Legacy', threads: 0 } ],
  eventCount: 1,
  executedMigrations: [
    '55_OrchestrationV2',
    '56_RemoveRedundantProjectionIndexes',
    '57_ScheduledTaskWebhooks',
    '58_WebhookRelayDeliveries',
    '59_McpAppModelContext',
    '60_ThreadSnapshotWindowIndexes',
    '61_PullRequestReviewGuides',
    '62_ThreadFailureLookupIndex',
    '63_DropUnusedV1Projections',
    '64_CanonicalizeThreadPullRequestLinks',
    '65_CodexUsageNativeTurnIndex',
    '66_RepairClientSettlementTimestamps',
    '67_WebhookDedupe'
  ],
  stacks: []
}
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { databasePath: '/tmp/migrate-dev-db-race-dest-zLvqFv/userdata/statev2.sqlite', sizeBytes: 815104, projects: [ { title: 'Legacy', threads: 0 } ], eventCount: 1, executedMigrations: [ '55_OrchestrationV2', '56_RemoveRedundantProjectionIndexes', '57_ScheduledTaskWebhooks', '58_WebhookRelayDeliveries', '59_McpAppModelContext', '60_ThreadSnapshotWindowIndexes', '61_PullRequestReviewGuides', '62_ThreadFailureLookupIndex', '63_DropUnusedV1Projections', '64_CanonicalizeThreadPullRequestLinks', '65_CodexUsageNativeTurnIndex', '66_RepairClientSettlementTimestamps', '67_WebhookDedupe' ] }
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯


 Test Files  1 failed (1)
      Tests  1 failed | 9 passed (10)
   Start at  07:53:17
   Duration  35.48s (transform 72%, tests 13%, import 8%, setup 6%)
EXIT_STATUS=1
ASSERTION: deterministic injected filesystem boundary commits a V1 thread; red suite has exactly 1 failed / 9 passed; no sleeps.

$ (cd /home/ubuntu/wt/issue-19018 && ./node_modules/.bin/vp test run apps/server/scripts/migrate-dev-db.test.ts --reporter=verbose)
note: You are running `vp test` as a Vite+ built-in command. If you meant to run the test npm script, use `vpr test` instead.

 RUN  v5.0.5 /home/ubuntu/wt/issue-19018

 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > keeps stopped and settled thread families and clears pending work 389ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > never copies an archived thread, and new events append after the source's 360ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > skips a thread family larger than the per-project limit 405ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > upgrades a source from before the V2 thread tables 247ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses a pre-V2 source 510ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses a V2 source whose legacy threads were n… 493ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses a legacy thread committed after the guard 417ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses while a dev server holds the destination 368ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses a source that resolves to a destination path 6ms
 ✓ |t3| scripts/migrate-dev-db.test.ts > migrate-dev-db > refuses to rebuild the shared home 313ms

 Test Files  1 passed (1)
      Tests  10 passed (10)
   Start at  07:53:57
   Duration  17.96s (transform 52%, tests 22%, setup 14%, import 12%)
EXIT_STATUS=0
V2 fixture oracle (committed test): 9 source threads -> 3 eligible retained threads: fork-thread, settled-thread, stopped-thread; pending auth/tasks/transfers counts -> 0. Full focused suite above validates exact IDs/counts.

$ (cd /home/ubuntu/wt/issue-19018 && git status --porcelain)

EXIT_STATUS=0
PASS: all evidence assertions completed; no servers, browser, private state, or forge writes used.
TRANSCRIPT_PATH=/tmp/issue19018-synthetic-nhhcl3ww/transcript.txt
```

## Revision verification and implementation delta

```text
$ (cd /home/ubuntu/wt/issue-19018 && git rev-parse HEAD 44bd595e77 a12d6ed942 9bf8c50e9a)
a007595b42e2237ac5b8d91b1f294d24af2446e2
44bd595e77a0622ab69bcdd0f34970b9b252bbc6
a12d6ed9428d8e64d76ca86d178abbf564b57cee
9bf8c50e9a2976569d06a3cdbb2fedc6919fe1f2
EXIT_STATUS=0

$ (cd /home/ubuntu/wt/issue-19018 && git diff --exit-code 44bd595e77 HEAD -- apps/server/scripts/migrate-dev-db.test.ts)
EXIT_STATUS=0

$ (cd /home/ubuntu/wt/issue-19018 && git diff 44bd595e77 HEAD -- apps/server/scripts/migrate-dev-db.ts)
diff --git a/apps/server/scripts/migrate-dev-db.ts b/apps/server/scripts/migrate-dev-db.ts
index b14ea0a110..36b8c4087a 100644
--- a/apps/server/scripts/migrate-dev-db.ts
+++ b/apps/server/scripts/migrate-dev-db.ts
@@ -236,59 +236,53 @@ const rootIsVisible = (sql: SqlClient.SqlClient) => sql`
   AND json_extract(root.payload_json, '$.archivedAt') IS NULL`;
 
 /**
+ * One transaction reads the source, so the copy is consistent while the real server writes.
+ *
  * The server imports V1 threads into the V2 tables at runtime, not in a
  * migration, and the copy keeps only threads that already have V2 rows, so a
  * source with unimported threads would migrate cleanly and silently lose them.
- * Mirrors the first half of LegacyV1ThreadImporter's pendingThreadCount; the
- * other half (lazy transcript imports) survives the copy.
- */
-const ensureNoPendingLegacyThreads = Effect.fn("ensureDevDbNoPendingLegacyThreads")(function* (
-  sourcePath: string,
-) {
-  const sql = yield* SqlClient.SqlClient;
-  const tables = yield* sql<{ name: string }>`
-    SELECT name FROM sqlite_master WHERE type = 'table'
-      AND name IN ('projection_threads', 'orchestration_v2_projection_threads')`;
-  const names = new Set(tables.map((table) => table.name));
-  if (!names.has("projection_threads")) return;
-  // The V2 tables, application_event_version, and the partial index arrive in
-  // the same migration. Without the hint SQLite scans each thread's stream.
-  const [pending] = names.has("orchestration_v2_projection_threads")
-    ? yield* sql<{ count: number }>`
-        SELECT COUNT(*) AS count FROM projection_threads AS thread
-        WHERE NOT EXISTS (
-          SELECT 1
-          FROM orchestration_events AS event INDEXED BY orchestration_events_v2_created_threads_idx
-          WHERE event.application_event_version = 2
-            AND event.aggregate_kind = 'thread'
-            AND event.stream_id = thread.thread_id
-            AND event.event_type = 'thread.created'
-        )`
-    : yield* sql<{ count: number }>`SELECT COUNT(*) AS count FROM projection_threads`;
-  const legacyThreads = Number(pending?.count ?? 0);
-  if (legacyThreads > 0) {
-    return yield* new MigrateDevDbLegacySourceError({ sourcePath, legacyThreads });
-  }
-});
-
-/**
- * One transaction reads the source, so the copy is consistent while the real server writes.
+ * The check reads the same snapshot as the copy: a V1 server can commit its
+ * first thread at any moment before it. Mirrors the first half of
+ * LegacyV1ThreadImporter's pendingThreadCount; the other half (lazy transcript
+ * imports) survives the copy.
  */
 const copySourceSlice = Effect.fn("copyDevDbSourceSlice")(function* (sourcePath: string) {
   const sql = yield* SqlClient.SqlClient;
   yield* sql`ATTACH DATABASE ${`${NodeURL.pathToFileURL(sourcePath).href}?mode=ro`} AS src`;
-  const schema = yield* sql<{ type: string; name: string; sql: string }>`
-    SELECT type, name, sql FROM src.sqlite_master
-    WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'`;
-  const threadColumns = yield* sql<{ name: string; notnull: number }>`
-    SELECT m.name, c."notnull" FROM src.sqlite_master m, pragma_table_info(m.name, 'src') c
-    WHERE m.type = 'table' AND c.name = 'thread_id'`;
-  const threadIdNullable = new Map(threadColumns.map((row) => [row.name, row.notnull === 0]));
-  const tables = schema.filter((entry) => entry.type === "table");
-  const filtered = tables.some((table) => table.name === "orchestration_v2_projection_threads");
 
   yield* sql.withTransaction(
     Effect.gen(function* () {
+      const schema = yield* sql<{ type: string; name: string; sql: string }>`
+        SELECT type, name, sql FROM src.sqlite_master
+        WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'`;
+      const threadColumns = yield* sql<{ name: string; notnull: number }>`
+        SELECT m.name, c."notnull" FROM src.sqlite_master m, pragma_table_info(m.name, 'src') c
+        WHERE m.type = 'table' AND c.name = 'thread_id'`;
+      const threadIdNullable = new Map(threadColumns.map((row) => [row.name, row.notnull === 0]));
+      const tables = schema.filter((entry) => entry.type === "table");
+      const filtered = tables.some((table) => table.name === "orchestration_v2_projection_threads");
+
+      if (tables.some((table) => table.name === "projection_threads")) {
+        // The V2 tables, application_event_version, and the partial index arrive in
+        // the same migration. Without the hint SQLite scans each thread's stream.
+        const [pending] = filtered
+          ? yield* sql<{ count: number }>`
+              SELECT COUNT(*) AS count FROM src.projection_threads AS thread
+              WHERE NOT EXISTS (
+                SELECT 1
+                FROM src.orchestration_events AS event INDEXED BY orchestration_events_v2_created_threads_idx
+                WHERE event.application_event_version = 2
+                  AND event.aggregate_kind = 'thread'
+                  AND event.stream_id = thread.thread_id
+                  AND event.event_type = 'thread.created'
+              )`
+          : yield* sql<{ count: number }>`SELECT COUNT(*) AS count FROM src.projection_threads`;
+        const legacyThreads = Number(pending?.count ?? 0);
+        if (legacyThreads > 0) {
+          return yield* new MigrateDevDbLegacySourceError({ sourcePath, legacyThreads });
+        }
+      }
+
       for (const table of tables) yield* sql.unsafe(table.sql).unprepared;
       if (filtered) {
         yield* sql`CREATE TEMP TABLE copied_threads (thread_id TEXT PRIMARY KEY)`;
@@ -511,19 +505,7 @@ export const runMigrateDevDb = Effect.fn("runMigrateDevDb")(function* (
     }
   }
 
-  // Before ensureNotInUse, whose checkpoint already rewrites the destination.
-  yield* ensureNoPendingLegacyThreads(sourcePath).pipe(
-    Effect.provide(NodeSqliteClient.layer({ filename: sourcePath, readonly: true })),
-    Effect.catchTags({
-      SqlError: (cause) =>
-        Effect.fail(
-          new MigrateDevDbPhaseError({ phase: "snapshot", databasePath: sourcePath, cause }),
-        ),
-    }),
-  );
-
   yield* fs.makeDirectory(stateDir, { recursive: true });
-  yield* ensureNotInUse(databasePath);
 
   const wrapPhase =
     (phase: MigrateDevDbPhaseError["phase"], phaseDatabasePath: string) =>
@@ -539,8 +521,16 @@ export const runMigrateDevDb = Effect.fn("runMigrateDevDb")(function* (
     yield* Console.log(`Copying visible threads from ${sourcePath} (read-only)...`);
     yield* copySourceSlice(sourcePath).pipe(
       Effect.provide(NodeSqliteClient.layer({ filename: snapshotPath })),
-      wrapPhase("snapshot", sourcePath),
+      Effect.catchTags({
+        SqlError: (cause) =>
+          Effect.fail(
+            new MigrateDevDbPhaseError({ phase: "snapshot", databasePath: sourcePath, cause }),
+          ),
+      }),
     );
+    // After the copy, whose legacy check must refuse before this checkpoint
+    // rewrites the destination.
+    yield* ensureNotInUse(databasePath);
 
     // Migrate before pruning: a source older than this checkout would
     // otherwise crash the prune queries on columns that don't exist yet.
EXIT_STATUS=0

$ (cd /home/ubuntu/wt/issue-19018 && git status --porcelain)
EXIT_STATUS=0
```
