@@ -37,6 +37,8 @@ import { openReplaySnapshotStore, resolveReplaySnapshotDbPath } from "./replay-s
3737import type { ReplaySnapshotStore } from "./replay-snapshot.js" ;
3838import { initDenyHookSynthesisStore , resolveDenyHookSynthesisDbPath } from "./deny-hook-synthesis.js" ;
3939import type { DenyHookSynthesisStore } from "./deny-hook-synthesis.js" ;
40+ import { countWorktreeSlotsToPurge , openWorktreeAllocator , resolveWorktreeAllocatorDbPath } from "./worktree-allocator.js" ;
41+ import type { WorktreeAllocator } from "./worktree-allocator.js" ;
4042import { resolveAttemptLogDbPath } from "./attempt-log.js" ;
4143import {
4244 CLAIM_LEDGER_PURGE_SPEC ,
@@ -79,7 +81,8 @@ type PurgeOpenerKey =
7981 | "initPolicyVerdictCacheStore"
8082 | "initRankedCandidatesStore"
8183 | "openReplaySnapshotStore"
82- | "initDenyHookSynthesisStore" ;
84+ | "initDenyHookSynthesisStore"
85+ | "openWorktreeAllocator" ;
8386
8487export type PurgeCliOptions = {
8588 openClaimLedger ?: ( ) => ClaimLedger ;
@@ -94,6 +97,7 @@ export type PurgeCliOptions = {
9497 initRankedCandidatesStore ?: ( ) => RankedCandidatesStore ;
9598 openReplaySnapshotStore ?: ( ) => ReplaySnapshotStore ;
9699 initDenyHookSynthesisStore ?: ( ) => DenyHookSynthesisStore ;
100+ openWorktreeAllocator ?: ( ) => WorktreeAllocator ;
97101 resolveDbPaths ?: Record < string , ( ) => string > ;
98102} ;
99103
@@ -104,6 +108,10 @@ type PurgeTarget = {
104108 resolveDbPath : ( ) => string ;
105109 spec ?: LedgerPurgeSpec ;
106110 specs ?: LedgerPurgeSpec [ ] ;
111+ // A store whose per-repo purge lives on its store object (not a generic DELETE spec) supplies its own read-only
112+ // dry-run counter instead of `spec`/`specs`, so --dry-run counts EXACTLY the rows the real purge would touch
113+ // (e.g. worktree-allocator, whose fixed slot pool is cleared with a status-guarded UPDATE, never a DELETE).
114+ countDryRun ?: ( db : DatabaseSync , repoFullName : string ) => number ;
107115} ;
108116
109117const REAL_PURGE_TARGETS : PurgeTarget [ ] = [
@@ -124,6 +132,11 @@ const REAL_PURGE_TARGETS: PurgeTarget[] = [
124132 { name : "ranked-candidates" , optionKey : "initRankedCandidatesStore" , opener : initRankedCandidatesStore , resolveDbPath : resolveRankedCandidatesDbPath , spec : RANKED_CANDIDATES_PURGE_SPEC } ,
125133 { name : "replay-snapshot" , optionKey : "openReplaySnapshotStore" , opener : openReplaySnapshotStore , resolveDbPath : resolveReplaySnapshotDbPath , spec : REPLAY_SNAPSHOT_PURGE_SPEC } ,
126134 { name : "deny-hook-synthesis" , optionKey : "initDenyHookSynthesisStore" , opener : initDenyHookSynthesisStore , resolveDbPath : resolveDenyHookSynthesisDbPath , spec : DENY_HOOK_SYNTHESIS_PURGE_SPEC } ,
135+ // worktree-allocator's `worktree_slots` is a FIXED pool of pre-allocated slot rows, not an append-only ledger, so
136+ // the generic DELETE-based spec is the wrong shape (deleting a slot would shrink the pool below maxConcurrency).
137+ // Its purge lives on the store object (a `status = 'free'` UPDATE blanking the repo columns, never touching a
138+ // live `active` slot), and its dry-run count uses the matching read-only counter — no `spec`/`specs` (#8320).
139+ { name : "worktree-allocator" , optionKey : "openWorktreeAllocator" , opener : openWorktreeAllocator , resolveDbPath : resolveWorktreeAllocatorDbPath , countDryRun : countWorktreeSlotsToPurge } ,
127140] ;
128141
129142export type ParsedPurgeArgs = { json : boolean ; dryRun : boolean ; repoFullName : string } | { error : string } ;
@@ -216,13 +229,14 @@ export function runPurgeDryRun(
216229 const resolveDbPaths = options . resolveDbPaths ?? { } ;
217230 const stores : PurgeDryRunStoreResult [ ] = REAL_PURGE_TARGETS . map ( ( target ) => {
218231 const dbPath = ( resolveDbPaths [ target . name ] ?? target . resolveDbPath ) ( ) ;
219- // A target scopes one table (`spec`) or -- for governor-state -- several in one file (`specs`); sum the
220- // per-table counts against the single read-only handle so the preview matches what a real purge removes.
221- // Every REAL_PURGE_TARGETS entry declares exactly one of the two, so `target.spec` is always set here.
222- const specs = target . specs ?? [ target . spec ! ] ;
232+ // A target scopes one table (`spec`), several in one file (`specs`, e.g. governor-state), or supplies its own
233+ // read-only counter (`countDryRun`, e.g. worktree-allocator's status-guarded slot count). Every entry declares
234+ // exactly one of the three, so the `target.spec!` fallback below is only reached when neither other is set.
223235 try {
224236 const wouldPurge = countExistingRows ( dbPath , ( db ) =>
225- specs . reduce ( ( sum , spec ) => sum + countStoreByRepo ( db , spec , parsed . repoFullName ) , 0 ) ,
237+ target . countDryRun
238+ ? target . countDryRun ( db , parsed . repoFullName )
239+ : ( target . specs ?? [ target . spec ! ] ) . reduce ( ( sum , spec ) => sum + countStoreByRepo ( db , spec , parsed . repoFullName ) , 0 ) ,
226240 ) ;
227241 return { store : target . name , wouldPurge } ;
228242 } catch ( error ) {
0 commit comments