@@ -145,12 +145,12 @@ def _clone_one(
145145 "tool_name" : tool_name ,
146146 }
147147
148+ # Each sub-path (adopt / fresh / fresh-dry-run) owns its own
149+ # custom_tool remap, since only it knows whether the target id is
150+ # real or a planned synthetic.
148151 if tgt_tool_id is None :
149152 return
150153
151- with lock :
152- self .ctx .remap .record ("custom_tool" , src_tool_id , tgt_tool_id )
153-
154154 # Neither the export blob nor list rows carry share axes —
155155 # share state comes from the source detail.
156156 self .apply_share (
@@ -163,6 +163,10 @@ def _clone_one(
163163 )
164164
165165 if self .ctx .options .dry_run :
166+ # Can't republish the registry without writing, but ToolInstance
167+ # needs a prompt_studio_registry remap to plan-count. Mirror it
168+ # with a planned id derived from the source registry (read-only).
169+ self ._record_planned_registry (src_tool_id , tool_name , lock )
166170 return
167171
168172 # Tools never exported on source (e.g. empty projects — backend
@@ -223,6 +227,30 @@ def _clone_one(
223227 tgt_regs [0 ]["prompt_registry_id" ],
224228 )
225229
230+ def _record_planned_registry (
231+ self , src_tool_id : str , tool_name : str , lock : threading .Lock
232+ ) -> None :
233+ """Dry-run: record a planned prompt_studio_registry remap from the
234+ source registry id, so ToolInstancePhase can resolve tool_id and
235+ plan-count. No-op for tools never exported on source (no registry).
236+ """
237+ try :
238+ src_regs = self .ctx .source .list_registries (custom_tool = src_tool_id )
239+ except Exception as e :
240+ logger .warning (
241+ "[dry-run] source registry lookup failed for tool '%s' "
242+ "(tool_instance plan may under-count): %s" ,
243+ tool_name ,
244+ e ,
245+ )
246+ return
247+ if not src_regs :
248+ return
249+ with lock :
250+ self .ctx .remap .record_planned (
251+ "prompt_studio_registry" , src_regs [0 ]["prompt_registry_id" ]
252+ )
253+
226254 def _adopt (
227255 self ,
228256 match : dict [str , Any ],
@@ -240,7 +268,8 @@ def _adopt(
240268 tgt_tool_id = match ["tool_id" ]
241269 if self .ctx .options .dry_run :
242270 with lock :
243- result .skipped += 1
271+ result .adopted += 1
272+ self .ctx .remap .record ("custom_tool" , src_tool_id , tgt_tool_id )
244273 logger .info (
245274 "[dry-run] would sync prompts into adopted tool '%s' src=%s -> tgt=%s" ,
246275 tool_name ,
@@ -260,6 +289,7 @@ def _adopt(
260289
261290 with lock :
262291 result .adopted += 1
292+ self .ctx .remap .record ("custom_tool" , src_tool_id , tgt_tool_id )
263293 logger .info (
264294 "adopted tool '%s' src=%s -> tgt=%s (prompts re-synced)" ,
265295 tool_name ,
@@ -276,14 +306,9 @@ def _create_fresh(
276306 result : PhaseResult ,
277307 lock : threading .Lock ,
278308 ) -> str | None :
279- if self .ctx .options .dry_run :
280- with lock :
281- result .skipped += 1
282- logger .info (
283- "[dry-run] would import tool '%s' src=%s" , tool_name , src_tool_id
284- )
285- return None
286-
309+ # Run the source-side validations even in dry-run — they decide
310+ # whether a real run would create or frictionless-skip, so the plan
311+ # counts must reflect them. Only the target-write steps are stubbed.
287312 default_profile = self ._source_default_profile (src_tool_id , tool_name )
288313 if default_profile is None :
289314 with lock :
@@ -300,6 +325,18 @@ def _create_fresh(
300325 )
301326 return None
302327
328+ if self .ctx .options .dry_run :
329+ # Target adapter resolution is skipped: adapters this run would
330+ # create don't exist on target yet, so it can't resolve. The
331+ # frictionless check above already caught the real skip cases.
332+ with lock :
333+ result .created += 1
334+ tgt_tool_id = self .ctx .remap .record_planned ("custom_tool" , src_tool_id )
335+ logger .info (
336+ "[dry-run] would import tool '%s' src=%s" , tool_name , src_tool_id
337+ )
338+ return tgt_tool_id
339+
303340 adapter_ids = self ._resolve_target_adapter_ids (default_profile , tool_name )
304341 if adapter_ids is None :
305342 with lock :
@@ -321,6 +358,7 @@ def _create_fresh(
321358 tgt_tool_id = tgt ["tool_id" ]
322359 with lock :
323360 result .created += 1
361+ self .ctx .remap .record ("custom_tool" , src_tool_id , tgt_tool_id )
324362 logger .info (
325363 "created tool '%s' src=%s -> tgt=%s (needs_adapter_config=%s)" ,
326364 tool_name ,
0 commit comments