@@ -1614,11 +1614,97 @@ def _session_fingerprint(session_id: str) -> str:
16141614 return localio .stable_hash ({"claude_session_id" : session_id })
16151615
16161616
1617- def _verify_replacement (target : Path , command : str , session_fingerprint : str ) -> str :
1617+ def _skill_id_from_skill_path (target : Path , raw_path : object ) -> str | None :
1618+ """Return an installed target skill id when ``raw_path`` names its ``SKILL.md``."""
1619+ if not isinstance (raw_path , str ) or not raw_path :
1620+ return None
1621+ path = Path (raw_path ).expanduser ()
1622+ if not path .is_absolute ():
1623+ path = target / path
1624+ parts = path .parts
1625+ for index , part in enumerate (parts ):
1626+ if part == "skills" and index + 2 < len (parts ) and parts [index + 2 ] == "SKILL.md" :
1627+ skill_id = parts [index + 1 ]
1628+ if skill_id and skill_id not in {"." , ".." }:
1629+ from .. import outcome_cmd
1630+
1631+ if outcome_cmd ._artifact_known (target , skill_id , "skill" ):
1632+ return skill_id
1633+ return None
1634+
1635+
1636+ def _parse_capture_flag (command : object ) -> str | None :
1637+ """Extract ``--capture <id>`` from a verify command string when present."""
1638+ if not isinstance (command , str ) or not command .strip ():
1639+ return None
1640+ try :
1641+ tokens = shlex .split (command , posix = os .name != "nt" )
1642+ except ValueError :
1643+ return None
1644+ for index , token in enumerate (tokens ):
1645+ if token == "--capture" and index + 1 < len (tokens ):
1646+ value = tokens [index + 1 ]
1647+ if value and not value .startswith ("-" ):
1648+ return value
1649+ if token .startswith ("--capture=" ):
1650+ value = token .split ("=" , 1 )[1 ]
1651+ if value :
1652+ return value
1653+ return None
1654+
1655+
1656+ def _record_exercised_artifact (state : dict [str , Any ], artifact_id : str | None , * , kind : str = "skill" ) -> bool :
1657+ """Persist the most specific non-generic exercised artifact on session state."""
1658+ from .. import outcome_cmd
1659+
1660+ if not isinstance (artifact_id , str ):
1661+ return False
1662+ trimmed = artifact_id .strip ()
1663+ if not trimmed :
1664+ return False
1665+ current = state .get ("exercised_artifact_id" )
1666+ if (
1667+ trimmed == outcome_cmd .DEFAULT_CAPTURE_ARTIFACT_ID
1668+ and isinstance (current , str )
1669+ and current .strip ()
1670+ and current .strip () != outcome_cmd .DEFAULT_CAPTURE_ARTIFACT_ID
1671+ ):
1672+ return False
1673+ if current == trimmed and state .get ("exercised_artifact_kind" ) == kind :
1674+ return False
1675+ state ["exercised_artifact_id" ] = trimmed
1676+ state ["exercised_artifact_kind" ] = kind
1677+ return True
1678+
1679+
1680+ def exercised_artifact_for_fingerprint (target : Path , session_fingerprint : str ) -> str | None :
1681+ """Look up an exercised artifact id for a Claude session fingerprint."""
1682+ for state in iter_session_states (target , limit = MAX_RECENT_SESSION_STATES ):
1683+ if state .get ("session_fingerprint" ) != session_fingerprint :
1684+ continue
1685+ artifact_id = state .get ("exercised_artifact_id" )
1686+ if isinstance (artifact_id , str ) and artifact_id .strip ():
1687+ return artifact_id .strip ()
1688+ return None
1689+
1690+
1691+ def _verify_replacement (
1692+ target : Path ,
1693+ command : str ,
1694+ session_fingerprint : str ,
1695+ * ,
1696+ capture_artifact_id : str | None = None ,
1697+ ) -> str :
1698+ from .. import outcome_cmd
1699+
1700+ artifact_id = outcome_cmd .resolve_capture_artifact_id (
1701+ capture_artifact_id ,
1702+ exercised_artifact_for_fingerprint (target , session_fingerprint ),
1703+ )
16181704 return (
16191705 f"{ CLAUDE_SESSION_ENV } ={ shlex .quote (session_fingerprint )} "
16201706 f"brigade work verify run --target { shlex .quote (str (target ))} "
1621- f"--command { shlex .quote (command )} --capture brigade-work "
1707+ f"--command { shlex .quote (command )} --capture { shlex . quote ( artifact_id ) } "
16221708 )
16231709
16241710
@@ -1684,6 +1770,11 @@ def _normalize_state(target: Path, session_id: str, payload: dict[str, Any] | No
16841770 session_repos = payload .get ("session_repos" )
16851771 if isinstance (session_repos , list ) and all (isinstance (item , str ) for item in session_repos ):
16861772 normalized ["session_repos" ] = list (session_repos )
1773+ exercised = payload .get ("exercised_artifact_id" )
1774+ if isinstance (exercised , str ) and exercised .strip ():
1775+ normalized ["exercised_artifact_id" ] = exercised .strip ()
1776+ kind = payload .get ("exercised_artifact_kind" )
1777+ normalized ["exercised_artifact_kind" ] = kind if isinstance (kind , str ) and kind .strip () else "skill"
16871778 return normalized
16881779
16891780
@@ -1769,17 +1860,24 @@ def handle_payload(event: str, payload: dict[str, Any]) -> dict[str, Any] | None
17691860 return None
17701861 state ["verify_denied_count" ] = int (state .get ("verify_denied_count" ) or 0 ) + 1
17711862 write_session_state (target , session_id , state )
1863+ capture_artifact_id = state .get ("exercised_artifact_id" )
1864+ if not isinstance (capture_artifact_id , str ):
1865+ capture_artifact_id = None
17721866 if _has_unsupported_verifier_structure (str (command )):
1867+ from .. import outcome_cmd
1868+
1869+ capture_id = outcome_cmd .resolve_capture_artifact_id (capture_artifact_id )
17731870 reason = (
17741871 "Route verification through Brigade so failed, rejected, and passing results create receipts.\n "
17751872 "Split shell grouping, command substitution, pipelines, redirection, or complex directory changes "
1776- "from the verifier, then run that verifier with `brigade work verify run --capture brigade-work `."
1873+ f "from the verifier, then run that verifier with `brigade work verify run --capture { capture_id } `."
17771874 )
17781875 else :
17791876 replacement = _verify_replacement (
17801877 target ,
17811878 _first_verifier_command (str (command )),
17821879 str (state ["session_fingerprint" ]),
1880+ capture_artifact_id = capture_artifact_id ,
17831881 )
17841882 reason = (
17851883 "Route verification through Brigade so failed, rejected, and passing results create receipts.\n "
@@ -1798,7 +1896,14 @@ def handle_payload(event: str, payload: dict[str, Any]) -> dict[str, Any] | None
17981896 raw_post_tool_input = payload .get ("tool_input" )
17991897 post_tool_input : dict [str , Any ] = raw_post_tool_input if isinstance (raw_post_tool_input , dict ) else {}
18001898 command = post_tool_input .get ("command" )
1899+ if tool_name == "Read" :
1900+ skill_id = _skill_id_from_skill_path (target , post_tool_input .get ("file_path" ))
1901+ if _record_exercised_artifact (state , skill_id ):
1902+ write_session_state (target , session_id , state )
1903+ return None
18011904 if tool_name == "Bash" and (_is_routed_verify (command ) or _is_brigade_run (command )):
1905+ if _is_routed_verify (command ):
1906+ _record_exercised_artifact (state , _parse_capture_flag (command ))
18021907 state .pop ("pending_bash_fingerprint" , None )
18031908 state .pop ("pending_bash_started_at" , None )
18041909 write_session_state (target , session_id , state )
@@ -1841,9 +1946,16 @@ def handle_payload(event: str, payload: dict[str, Any]) -> dict[str, Any] | None
18411946 tool_input = raw_tool_input if isinstance (raw_tool_input , dict ) else {}
18421947 command = tool_input .get ("command" )
18431948 if payload .get ("tool_name" ) == "Bash" and (is_raw_verification (command ) or _is_routed_verify (command )):
1949+ from .. import outcome_cmd
1950+
1951+ capture_id = outcome_cmd .resolve_capture_artifact_id (
1952+ _parse_capture_flag (command ),
1953+ state .get ("exercised_artifact_id" ) if isinstance (state .get ("exercised_artifact_id" ), str ) else None ,
1954+ )
18441955 return _additional_context (
18451956 "PostToolUseFailure" ,
1846- "The failed or rejected verification must remain recorded in Brigade before retrying. Inspect the receipt, fix the cause, then rerun through `brigade work verify run --capture brigade-work`." ,
1957+ "The failed or rejected verification must remain recorded in Brigade before retrying. Inspect the receipt, fix the cause, then rerun through "
1958+ f"`brigade work verify run --capture { capture_id } `." ,
18471959 )
18481960 return None
18491961
@@ -1868,7 +1980,13 @@ def handle_payload(event: str, payload: dict[str, Any]) -> dict[str, Any] | None
18681980 or stop_state .get ("started_at" )
18691981 )
18701982 if not _receipt_since (stop_target , receipt_threshold , session_fingerprint = fingerprint ):
1871- replacement = _verify_replacement (stop_target , "<test>" , fingerprint )
1983+ exercised = stop_state .get ("exercised_artifact_id" )
1984+ replacement = _verify_replacement (
1985+ stop_target ,
1986+ "<test>" ,
1987+ fingerprint ,
1988+ capture_artifact_id = exercised if isinstance (exercised , str ) else None ,
1989+ )
18721990 blocking_failures .append (f"{ stop_target } : run `{ replacement } `" )
18731991 elif not _handoff_since (stop_target , stop_state .get ("started_at" )):
18741992 handoff_target = stop_target
0 commit comments