Skip to content

Commit f41416a

Browse files
fix(outcome): attribute capture to the exercised skill, not brigade-work
Hard-coded brigade-work capture ids in hook replacements and capture-before-retry messages collapsed distinct skill signals into one rank bucket. Stamp capture intent on verify receipts, track the exercised skill in Claude session state, and fall back to brigade-work only when unknown. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 397e08a commit f41416a

5 files changed

Lines changed: 335 additions & 11 deletions

File tree

src/brigade/claude_hooks/runtime.py

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

src/brigade/outcome_cmd.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,25 @@
2626
_LOCK_WAIT_SECONDS = 30.0
2727
_LOCK_SENTINEL_BYTE = b"\0"
2828

29+
# Generic fallback when no exercised skill/card is known. Prefer a real artifact
30+
# id at capture time so distinct skills do not collapse into one rank bucket.
31+
DEFAULT_CAPTURE_ARTIFACT_ID = "brigade-work"
32+
2933

3034
class OutcomeLedgerError(RuntimeError):
3135
"""Outcome ledger persistence failed; callers must not assume the append succeeded."""
3236

3337

38+
def resolve_capture_artifact_id(*candidates: str | None) -> str:
39+
"""Return the first non-empty capture id, else the generic brigade-work fallback."""
40+
for candidate in candidates:
41+
if isinstance(candidate, str):
42+
trimmed = candidate.strip()
43+
if trimmed:
44+
return trimmed
45+
return DEFAULT_CAPTURE_ARTIFACT_ID
46+
47+
3448
def _records_lock_path(target: Path) -> Path:
3549
return _records_path(target).parent / ".records.lock"
3650

src/brigade/work_cmd/verification.py

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -718,18 +718,53 @@ def _find_uncaptured_failed_verify_receipt(target: Path, planned_identity: list[
718718
return None
719719

720720

721-
def _capture_before_retry_message(run_id: str) -> str:
722-
return f"brigade outcome capture brigade-work --run-id {run_id}"
721+
def _receipt_capture_artifact_id(receipt: dict[str, Any] | None) -> str | None:
722+
"""Return the artifact id stamped on a verify receipt for outcome capture, if any."""
723+
if not isinstance(receipt, dict):
724+
return None
725+
capture = receipt.get("outcome_capture")
726+
if not isinstance(capture, dict):
727+
return None
728+
artifact_id = capture.get("artifact_id")
729+
if isinstance(artifact_id, str) and artifact_id.strip():
730+
return artifact_id.strip()
731+
return None
723732

724733

725-
def _enforce_capture_before_retry(target: Path, planned_identity: list[str], *, mode: str) -> int | None:
734+
def _stamp_outcome_capture(receipt: dict[str, Any], capture: str | None, capture_kind: str) -> None:
735+
"""Record the intended outcome artifact on the receipt before digests are sealed."""
736+
if not capture:
737+
return
738+
receipt["outcome_capture"] = {
739+
"artifact_id": capture,
740+
"artifact_kind": capture_kind,
741+
}
742+
743+
744+
def _capture_before_retry_message(run_id: str, *, artifact_id: str) -> str:
745+
return f"brigade outcome capture {artifact_id} --run-id {run_id}"
746+
747+
748+
def _enforce_capture_before_retry(
749+
target: Path,
750+
planned_identity: list[str],
751+
*,
752+
mode: str,
753+
capture_artifact_id: str | None = None,
754+
) -> int | None:
726755
"""Block or warn when the latest matching failed receipt has no outcome capture."""
727756
if mode == "off":
728757
return None
729758
failed = _find_uncaptured_failed_verify_receipt(target, planned_identity)
730759
if failed is None:
731760
return None
732-
message = _capture_before_retry_message(str(failed.get("run_id") or ""))
761+
from .. import outcome_cmd
762+
763+
artifact_id = outcome_cmd.resolve_capture_artifact_id(
764+
capture_artifact_id,
765+
_receipt_capture_artifact_id(failed),
766+
)
767+
message = _capture_before_retry_message(str(failed.get("run_id") or ""), artifact_id=artifact_id)
733768
if mode == "block":
734769
print(f"error: {message}", file=sys.stderr)
735770
return 1
@@ -1045,6 +1080,8 @@ def _run_verify_commands(
10451080
*,
10461081
graphtrail_timeout: float,
10471082
manifest: verify_manifest.VerifyManifest | None = None,
1083+
capture: str | None = None,
1084+
capture_kind: str = "skill",
10481085
) -> tuple[dict[str, Any], int]:
10491086
started = helpers._now()
10501087
run_id = f"{started.strftime('%Y%m%d-%H%M%S')}-work-verify-{uuid4().hex[:6]}"
@@ -1070,6 +1107,7 @@ def _run_verify_commands(
10701107
}
10711108
receipt.update(identity)
10721109
_stamp_harness_session(receipt)
1110+
_stamp_outcome_capture(receipt, capture, capture_kind)
10731111
try:
10741112
graph_delta_before = graphtrail_delta.capture_before(target, run_dir, timeout=graphtrail_timeout)
10751113
except KeyboardInterrupt:
@@ -1203,6 +1241,9 @@ def _write_reused_receipt(
12031241
latest: dict[str, Any],
12041242
planned_display: list[str],
12051243
timeout: int,
1244+
*,
1245+
capture: str | None = None,
1246+
capture_kind: str = "skill",
12061247
) -> tuple[dict[str, Any], int]:
12071248
"""Write a fresh receipt dir that records a reused passing run (no commands executed)."""
12081249
started = helpers._now()
@@ -1225,6 +1266,7 @@ def _write_reused_receipt(
12251266
}
12261267
receipt.update(identity)
12271268
_stamp_harness_session(receipt)
1269+
_stamp_outcome_capture(receipt, capture, capture_kind)
12281270
reused_from = latest.get("run_id")
12291271
if isinstance(reused_from, str) and reused_from:
12301272
receipt["reused_from"] = reused_from
@@ -1488,7 +1530,12 @@ def verify_run(
14881530
except ValueError as exc:
14891531
print(f"error: {exc}", file=sys.stderr)
14901532
return 2
1491-
blocked_rc = _enforce_capture_before_retry(target, planned_identity, mode=capture_before_retry)
1533+
blocked_rc = _enforce_capture_before_retry(
1534+
target,
1535+
planned_identity,
1536+
mode=capture_before_retry,
1537+
capture_artifact_id=capture,
1538+
)
14921539
if blocked_rc is not None:
14931540
return blocked_rc
14941541
try:
@@ -1503,14 +1550,23 @@ def verify_run(
15031550
and latest.get("tree_fingerprint") == fingerprint
15041551
and latest.get("planned_commands") == planned_display
15051552
):
1506-
receipt, rc = _write_reused_receipt(target, latest, planned_display, timeout)
1553+
receipt, rc = _write_reused_receipt(
1554+
target,
1555+
latest,
1556+
planned_display,
1557+
timeout,
1558+
capture=capture,
1559+
capture_kind=capture_kind,
1560+
)
15071561
if receipt is None:
15081562
receipt, rc = _run_verify_commands(
15091563
target,
15101564
planned,
15111565
timeout,
15121566
graphtrail_timeout=effective_graphtrail_timeout,
15131567
manifest=manifest,
1568+
capture=capture,
1569+
capture_kind=capture_kind,
15141570
)
15151571
except KeyboardInterrupt:
15161572
print("error: verification canceled by user", file=sys.stderr)

0 commit comments

Comments
 (0)