@@ -1614,11 +1614,94 @@ 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 a skill id when ``raw_path`` points at ``*/skills/<id>/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+ return skill_id
1630+ return None
1631+
1632+
1633+ def _parse_capture_flag (command : object ) -> str | None :
1634+ """Extract ``--capture <id>`` from a verify command string when present."""
1635+ if not isinstance (command , str ) or not command .strip ():
1636+ return None
1637+ try :
1638+ tokens = shlex .split (command , posix = os .name != "nt" )
1639+ except ValueError :
1640+ return None
1641+ for index , token in enumerate (tokens ):
1642+ if token == "--capture" and index + 1 < len (tokens ):
1643+ value = tokens [index + 1 ]
1644+ if value and not value .startswith ("-" ):
1645+ return value
1646+ if token .startswith ("--capture=" ):
1647+ value = token .split ("=" , 1 )[1 ]
1648+ if value :
1649+ return value
1650+ return None
1651+
1652+
1653+ def _record_exercised_artifact (state : dict [str , Any ], artifact_id : str | None , * , kind : str = "skill" ) -> bool :
1654+ """Persist the most specific non-generic exercised artifact on session state."""
1655+ from .. import outcome_cmd
1656+
1657+ if not isinstance (artifact_id , str ):
1658+ return False
1659+ trimmed = artifact_id .strip ()
1660+ if not trimmed :
1661+ return False
1662+ current = state .get ("exercised_artifact_id" )
1663+ if (
1664+ trimmed == outcome_cmd .DEFAULT_CAPTURE_ARTIFACT_ID
1665+ and isinstance (current , str )
1666+ and current .strip ()
1667+ and current .strip () != outcome_cmd .DEFAULT_CAPTURE_ARTIFACT_ID
1668+ ):
1669+ return False
1670+ if current == trimmed and state .get ("exercised_artifact_kind" ) == kind :
1671+ return False
1672+ state ["exercised_artifact_id" ] = trimmed
1673+ state ["exercised_artifact_kind" ] = kind
1674+ return True
1675+
1676+
1677+ def exercised_artifact_for_fingerprint (target : Path , session_fingerprint : str ) -> str | None :
1678+ """Look up an exercised artifact id for a Claude session fingerprint."""
1679+ for state in iter_session_states (target , limit = MAX_RECENT_SESSION_STATES ):
1680+ if state .get ("session_fingerprint" ) != session_fingerprint :
1681+ continue
1682+ artifact_id = state .get ("exercised_artifact_id" )
1683+ if isinstance (artifact_id , str ) and artifact_id .strip ():
1684+ return artifact_id .strip ()
1685+ return None
1686+
1687+
1688+ def _verify_replacement (
1689+ target : Path ,
1690+ command : str ,
1691+ session_fingerprint : str ,
1692+ * ,
1693+ capture_artifact_id : str | None = None ,
1694+ ) -> str :
1695+ from .. import outcome_cmd
1696+
1697+ artifact_id = outcome_cmd .resolve_capture_artifact_id (
1698+ capture_artifact_id ,
1699+ exercised_artifact_for_fingerprint (target , session_fingerprint ),
1700+ )
16181701 return (
16191702 f"{ CLAUDE_SESSION_ENV } ={ shlex .quote (session_fingerprint )} "
16201703 f"brigade work verify run --target { shlex .quote (str (target ))} "
1621- f"--command { shlex .quote (command )} --capture brigade-work "
1704+ f"--command { shlex .quote (command )} --capture { shlex . quote ( artifact_id ) } "
16221705 )
16231706
16241707
@@ -1684,6 +1767,11 @@ def _normalize_state(target: Path, session_id: str, payload: dict[str, Any] | No
16841767 session_repos = payload .get ("session_repos" )
16851768 if isinstance (session_repos , list ) and all (isinstance (item , str ) for item in session_repos ):
16861769 normalized ["session_repos" ] = list (session_repos )
1770+ exercised = payload .get ("exercised_artifact_id" )
1771+ if isinstance (exercised , str ) and exercised .strip ():
1772+ normalized ["exercised_artifact_id" ] = exercised .strip ()
1773+ kind = payload .get ("exercised_artifact_kind" )
1774+ normalized ["exercised_artifact_kind" ] = kind if isinstance (kind , str ) and kind .strip () else "skill"
16871775 return normalized
16881776
16891777
@@ -1769,17 +1857,24 @@ def handle_payload(event: str, payload: dict[str, Any]) -> dict[str, Any] | None
17691857 return None
17701858 state ["verify_denied_count" ] = int (state .get ("verify_denied_count" ) or 0 ) + 1
17711859 write_session_state (target , session_id , state )
1860+ capture_artifact_id = state .get ("exercised_artifact_id" )
1861+ if not isinstance (capture_artifact_id , str ):
1862+ capture_artifact_id = None
17721863 if _has_unsupported_verifier_structure (str (command )):
1864+ from .. import outcome_cmd
1865+
1866+ capture_id = outcome_cmd .resolve_capture_artifact_id (capture_artifact_id )
17731867 reason = (
17741868 "Route verification through Brigade so failed, rejected, and passing results create receipts.\n "
17751869 "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 `."
1870+ f "from the verifier, then run that verifier with `brigade work verify run --capture { capture_id } `."
17771871 )
17781872 else :
17791873 replacement = _verify_replacement (
17801874 target ,
17811875 _first_verifier_command (str (command )),
17821876 str (state ["session_fingerprint" ]),
1877+ capture_artifact_id = capture_artifact_id ,
17831878 )
17841879 reason = (
17851880 "Route verification through Brigade so failed, rejected, and passing results create receipts.\n "
@@ -1798,7 +1893,14 @@ def handle_payload(event: str, payload: dict[str, Any]) -> dict[str, Any] | None
17981893 raw_post_tool_input = payload .get ("tool_input" )
17991894 post_tool_input : dict [str , Any ] = raw_post_tool_input if isinstance (raw_post_tool_input , dict ) else {}
18001895 command = post_tool_input .get ("command" )
1896+ if tool_name == "Read" :
1897+ skill_id = _skill_id_from_skill_path (target , post_tool_input .get ("file_path" ))
1898+ if _record_exercised_artifact (state , skill_id ):
1899+ write_session_state (target , session_id , state )
1900+ return None
18011901 if tool_name == "Bash" and (_is_routed_verify (command ) or _is_brigade_run (command )):
1902+ if _is_routed_verify (command ):
1903+ _record_exercised_artifact (state , _parse_capture_flag (command ))
18021904 state .pop ("pending_bash_fingerprint" , None )
18031905 state .pop ("pending_bash_started_at" , None )
18041906 write_session_state (target , session_id , state )
@@ -1841,9 +1943,16 @@ def handle_payload(event: str, payload: dict[str, Any]) -> dict[str, Any] | None
18411943 tool_input = raw_tool_input if isinstance (raw_tool_input , dict ) else {}
18421944 command = tool_input .get ("command" )
18431945 if payload .get ("tool_name" ) == "Bash" and (is_raw_verification (command ) or _is_routed_verify (command )):
1946+ from .. import outcome_cmd
1947+
1948+ capture_id = outcome_cmd .resolve_capture_artifact_id (
1949+ _parse_capture_flag (command ),
1950+ state .get ("exercised_artifact_id" ) if isinstance (state .get ("exercised_artifact_id" ), str ) else None ,
1951+ )
18441952 return _additional_context (
18451953 "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`." ,
1954+ "The failed or rejected verification must remain recorded in Brigade before retrying. Inspect the receipt, fix the cause, then rerun through "
1955+ f"`brigade work verify run --capture { capture_id } `." ,
18471956 )
18481957 return None
18491958
@@ -1868,7 +1977,13 @@ def handle_payload(event: str, payload: dict[str, Any]) -> dict[str, Any] | None
18681977 or stop_state .get ("started_at" )
18691978 )
18701979 if not _receipt_since (stop_target , receipt_threshold , session_fingerprint = fingerprint ):
1871- replacement = _verify_replacement (stop_target , "<test>" , fingerprint )
1980+ exercised = stop_state .get ("exercised_artifact_id" )
1981+ replacement = _verify_replacement (
1982+ stop_target ,
1983+ "<test>" ,
1984+ fingerprint ,
1985+ capture_artifact_id = exercised if isinstance (exercised , str ) else None ,
1986+ )
18721987 blocking_failures .append (f"{ stop_target } : run `{ replacement } `" )
18731988 elif not _handoff_since (stop_target , stop_state .get ("started_at" )):
18741989 handoff_target = stop_target
0 commit comments