@@ -344,6 +344,102 @@ def _prune_verify_runs(target: Path, keep: int = VERIFY_RUNS_KEEP) -> int:
344344 return removed
345345
346346
347+ def _normalize_command_identity (command : str | list [str ]) -> str :
348+ """Canonicalize one command without discarding execution-affecting prefixes."""
349+ if isinstance (command , list ):
350+ return shlex .join (command )
351+ try :
352+ return shlex .join (shlex .split (command ))
353+ except ValueError :
354+ return command
355+
356+
357+ def _planned_commands_display (commands : list [str | list [str ]]) -> list [str ]:
358+ return [shlex .join (command ) if isinstance (command , list ) else command for command in commands ]
359+
360+
361+ def _planned_commands_identity (commands : list [str | list [str ]]) -> list [str ]:
362+ return [_normalize_command_identity (command ) for command in commands ]
363+
364+
365+ def _receipt_planned_commands_identity (receipt : dict [str , Any ]) -> list [str ] | None :
366+ """Return normalized command identity from a verify receipt."""
367+ planned = receipt .get ("planned_commands" )
368+ if isinstance (planned , list ) and all (isinstance (item , str ) for item in planned ):
369+ return [_normalize_command_identity (item ) for item in planned ]
370+ commands = receipt .get ("commands" )
371+ if not isinstance (commands , list ):
372+ return None
373+ display : list [str ] = []
374+ for command in commands :
375+ if isinstance (command , dict ):
376+ command_text = command .get ("command" )
377+ if isinstance (command_text , str ):
378+ display .append (_normalize_command_identity (command_text ))
379+ return display or None
380+
381+
382+ def _verify_receipt_evidence_ref (receipt : dict [str , Any ]) -> str :
383+ path = receipt .get ("path" )
384+ if isinstance (path , str ) and path :
385+ return str (Path (path ) / "receipt.json" )
386+ run_id = receipt .get ("run_id" )
387+ target = receipt .get ("target" )
388+ if isinstance (run_id , str ) and run_id and isinstance (target , str ) and target :
389+ return str (Path (target ) / ".brigade" / "work" / "verify-runs" / run_id / "receipt.json" )
390+ return ""
391+
392+
393+ def _verify_receipt_has_outcome_capture (target : Path , receipt : dict [str , Any ]) -> bool :
394+ from .. import outcome_cmd
395+
396+ evidence_ref = _verify_receipt_evidence_ref (receipt )
397+ if not evidence_ref :
398+ return False
399+ resolved = Path (evidence_ref ).expanduser ().resolve ()
400+ for record in outcome_cmd .load_records (target ):
401+ if not record .evidence_ref :
402+ continue
403+ try :
404+ if Path (record .evidence_ref ).expanduser ().resolve () == resolved :
405+ return True
406+ except OSError :
407+ if record .evidence_ref == evidence_ref :
408+ return True
409+ return False
410+
411+
412+ def _find_uncaptured_failed_verify_receipt (target : Path , planned_identity : list [str ]) -> dict [str , Any ] | None :
413+ for receipt in _verify_receipts (target ):
414+ if _receipt_planned_commands_identity (receipt ) != planned_identity :
415+ continue
416+ if receipt .get ("status" ) != "failed" :
417+ return None
418+ if _verify_receipt_has_outcome_capture (target , receipt ):
419+ return None
420+ return receipt
421+ return None
422+
423+
424+ def _capture_before_retry_message (run_id : str ) -> str :
425+ return f"brigade outcome capture brigade-work --run-id { run_id } "
426+
427+
428+ def _enforce_capture_before_retry (target : Path , planned_identity : list [str ], * , mode : str ) -> int | None :
429+ """Block or warn when the latest matching failed receipt has no outcome capture."""
430+ if mode == "off" :
431+ return None
432+ failed = _find_uncaptured_failed_verify_receipt (target , planned_identity )
433+ if failed is None :
434+ return None
435+ message = _capture_before_retry_message (str (failed .get ("run_id" ) or "" ))
436+ if mode == "block" :
437+ print (f"error: { message } " , file = sys .stderr )
438+ return 1
439+ print (f"warning: { message } " , file = sys .stderr )
440+ return None
441+
442+
347443def _latest_verify_receipt (target : Path ) -> dict [str , Any ] | None :
348444 receipts = _verify_receipts (target )
349445 return receipts [0 ] if receipts else None
@@ -568,7 +664,7 @@ def _run_verify_commands(
568664 "evidence" : _verification_evidence_payload (target ),
569665 "commands" : [],
570666 "tree_fingerprint" : _tree_fingerprint (target ),
571- "planned_commands" : [ shlex . join ( c ) if isinstance ( c , list ) else c for c in commands ] ,
667+ "planned_commands" : _planned_commands_display ( commands ) ,
572668 }
573669 _stamp_harness_session (receipt )
574670 try :
@@ -943,12 +1039,21 @@ def verify_run(
9431039 if not planned :
9441040 print ("error: no verification commands found; pass --command" , file = sys .stderr )
9451041 return 2
1042+ planned_display = _planned_commands_display (planned )
1043+ planned_identity = _planned_commands_identity (planned )
1044+ try :
1045+ capture_before_retry = config .resolve_capture_before_retry (target )
1046+ except ValueError as exc :
1047+ print (f"error: { exc } " , file = sys .stderr )
1048+ return 2
1049+ blocked_rc = _enforce_capture_before_retry (target , planned_identity , mode = capture_before_retry )
1050+ if blocked_rc is not None :
1051+ return blocked_rc
9461052 try :
9471053 receipt = None
9481054 if reuse :
9491055 fingerprint = _tree_fingerprint (target )
9501056 latest = _latest_verify_receipt (target )
951- planned_display = [shlex .join (c ) if isinstance (c , list ) else c for c in planned ]
9521057 if (
9531058 fingerprint is not None
9541059 and latest is not None
0 commit comments