Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e024cd3
llm-judge: a shared background model judge for hooks
edbert-bot Sep 11, 2026
5efb763
invoker: wf-1789117938963-6/implement-llm-judge-library — Add engine/…
edbert-bot Sep 11, 2026
7be661c
invoker: wf-1789117938963-6/verify-llm-judge-library — Run the judge …
edbert-bot Sep 11, 2026
0e6fb2c
invoker: wf-1789117938963-6/scrub-handoff-artifacts — Terminal read-o…
edbert-bot Sep 11, 2026
d90b880
Merge experiment/wf-1789117938963-6/scrub-handoff-artifacts/g0.t0.a-a…
edbert-bot Sep 11, 2026
99852d1
llm-judge: deliver finished verdicts on the agent's next turn
edbert-bot Sep 11, 2026
e322f6e
invoker: wf-1789118621517-7/implement-llm-judge-inbox — Add the llm-j…
edbert-bot Sep 11, 2026
ece2f67
invoker: wf-1789118621517-7/verify-llm-judge-inbox — Run the inbox te…
edbert-bot Sep 11, 2026
84a9065
invoker: wf-1789118621517-7/scrub-handoff-artifacts — Terminal read-o…
edbert-bot Sep 11, 2026
81f2b22
Merge experiment/wf-1789118621517-7/scrub-handoff-artifacts/g0.t0.a-a…
edbert-bot Sep 11, 2026
fdc3f18
invoker: wf-1789119854592-8/implement-wrong-check-judge — wrong-check…
edbert-bot Sep 11, 2026
71ab0d8
invoker: wf-1789119854592-8/verify-wrong-check-judge — Run wrong-chec…
edbert-bot Sep 11, 2026
a200478
invoker: wf-1789119854592-8/scrub-handoff-artifacts — Terminal read-o…
edbert-bot Sep 11, 2026
78dc644
Merge experiment/wf-1789119854592-8/scrub-handoff-artifacts/g0.t0.a-a…
edbert-bot Sep 11, 2026
05eb32a
wrong-check-reflect: drop 7 comment lines the no-comments gate bans
EdbertChan Sep 11, 2026
2e0bb00
Add llm judge phrase loader
Sep 11, 2026
05bc597
invoker: wf-1789159008754-1/add-phrase-loader — Review claim: A check…
Sep 11, 2026
a3013d7
docs: add phrase judge skill
Sep 11, 2026
081809e
invoker: wf-1789159008754-1/document-phrase-judge-skill — Review clai…
Sep 11, 2026
cbf3b6b
invoker: wf-1789159008754-1/verify-phrase-loader-tests — Review claim…
Sep 11, 2026
b53b5d3
invoker: wf-1789159008754-1/verify-phrase-module-dormant — Review cla…
Sep 11, 2026
00bc2c1
invoker: wf-1789159008754-1/verify-phrase-skill-checks — Review claim…
Sep 11, 2026
4e78d32
Invoker: merge experiment/wf-1789159008754-1/verify-phrase-skill-chec…
Sep 11, 2026
2dbd843
Invoker: merge experiment/wf-1789159008754-1/verify-phrase-module-dor…
Sep 11, 2026
2a072f8
invoker: wf-1789159008754-1/scrub-handoff-artifacts — Review claim: N…
Sep 11, 2026
37bb35c
Merge experiment/wf-1789159008754-1/scrub-handoff-artifacts/g0.t0.a-a…
EdbertChan Sep 11, 2026
3008239
llm-judge phrases: ask the judge to match meaning in any wording
EdbertChan Sep 11, 2026
4274083
test_install: expect phrase-judge in the engine-only skill set
EdbertChan Sep 11, 2026
9f63b22
Merge origin/main into phrase judge (1) after #425's squash merge
EdbertChan Sep 11, 2026
6ad6afd
llm-judge: add after-tool delivery scripts
EdbertChan Sep 12, 2026
e9f096b
invoker: wf-1789160500325-2/add-after-tool-delivery-scripts — Review …
Sep 12, 2026
895ef75
invoker: wf-1789160500325-2/verify-after-tool-scripts-not-installed —…
EdbertChan Sep 12, 2026
6fd77d8
invoker: wf-1789160500325-2/verify-after-tool-delivery-tests — Review…
EdbertChan Sep 12, 2026
94d1a62
invoker: wf-1789160500325-2/verify-after-tool-scripts-not-installed —…
EdbertChan Sep 12, 2026
d654dce
invoker: wf-1789160500325-2/verify-after-tool-scripts-not-installed —…
EdbertChan Sep 12, 2026
2fba057
invoker: wf-1789160500325-2/verify-after-tool-scripts-not-installed —…
EdbertChan Sep 12, 2026
afb5f6a
Invoker: merge experiment/wf-1789160500325-2/verify-after-tool-script…
Sep 12, 2026
220b253
invoker: wf-1789160500325-2/scrub-handoff-artifacts — Review claim: N…
Sep 12, 2026
4aa295c
invoker: wf-1789195503118-10/flag-capability-list-copied-from-error-t…
Sep 12, 2026
e9aadbd
invoker: wf-1789195503118-10/verify-capability-list-provenance — Run …
Sep 12, 2026
1965f22
invoker: wf-1789195503118-10/scrub-handoff-artifacts — Terminal read-…
Sep 12, 2026
6707b0b
Merge experiment/wf-1789195503118-10/scrub-handoff-artifacts/g0.t0.a-…
EdbertChan Sep 12, 2026
0b5f0f4
Merge experiment/wf-1789160500325-2/scrub-handoff-artifacts into main
EdbertChan Sep 12, 2026
17a2b07
[Hooks Cannot See] (1) Teach two hooks the checks they could not see
EdbertChan Sep 13, 2026
3d2e9fe
Fix hook-freshness hermetic settings tests
Sep 13, 2026
5a64e2e
invoker: wf-1789287154251-49/repair — Repair PR #518 (failed check test)
Sep 13, 2026
79c77e9
Merge of #470
mergify[bot] Sep 13, 2026
146c80e
Merge of #483
mergify[bot] Sep 13, 2026
4e11354
Merge of #518
mergify[bot] Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion engine/hooks/hedge-runs-prove-it/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ Stays silent on a diagnosis inside a fence, a double-quoted span, a backtick
span, a markdown blockquote, a hypothetical ("if it's a zombie, ..."), and
past-tense narration of an old incident ("the task was a zombie").

## Capability lists copied from errors

An error message can expose a local fallback or hardcoded enumeration without
proving the capabilities of the system named in that error. The hook records
values in bracketed or comma-joined lists from error-shaped tool results. It
blocks an outgoing reply that repeats at least two of those values within 200
characters of `accepts`, `supports`, `known models`, `valid`, or `only` when
those values appeared nowhere in a non-error tool result.

One repeated value stays silent to avoid collisions on common tokens. The rule
also stays silent when a non-error tool result supplies the repeated values, or
when the reply attributes or retracts the enumeration with wording such as
`fallback`, `hardcoded`, `built-in`, `in the error`, or `retract`.

Mechanical half of `corpus/skills/cat-mode/SKILL.md`'s Verify rule:
"Unhedged root-cause or fix claims about live system behavior need
instrument-level proof in the same message, or `{{CAT-UNVERIFIED}}`." Four
Expand All @@ -73,7 +87,8 @@ escape hatch, not a free pass). Fail-open on parse or read errors;

## Files

- `detect.py` -- hedge, code-noun, and reason patterns; turn scan; `decide()`.
- `detect.py` -- hedge, diagnosis, and error-only capability-list patterns;
turn scan; `decide()`.
- `claude_stop_check.py` -- Claude Stop entrypoint.
- `claude.hook.json` / `install_claude_hook.py` -- settings.json merge (idempotent).
- `tests/fixtures/hedges_{fires,silent}.json` -- sanitized real replies and
Expand Down
126 changes: 118 additions & 8 deletions engine/hooks/hedge-runs-prove-it/detect.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""hedge-runs-prove-it: an unrun check about code or repo state is a prompt to verify.
"""hedge-runs-prove-it: unproven code, state, and capability claims must be verified.

Two shapes, two bars.
Three shapes, three bars.

A hedge -- "I think", "I believe", "probably", "should work", "presumably",
or a `{{CAT-UNVERIFIED}}` tag next to a code noun (a path, a backticked name,
Expand All @@ -18,6 +18,11 @@
Only instrument-level proof in the same message clears it: pasted output, a
`file:line`, a pid, an exit code, or an explicit `{{CAT-UNVERIFIED}}` tag.

A capability enumeration copied from error-shaped tool output is not proof of
what another system accepts or supports. A reply that repeats two or more of
those values beside a capability verb is blocked unless a non-error tool result
also supplied them, or the reply attributes the list as fallback/error data.

Hedges about things that are not code or state (a company's motive, a
filing date) are out of scope, and so is either shape quoted rather than
claimed -- anywhere inside a double-quoted, backticked or single-quoted run,
Expand Down Expand Up @@ -118,6 +123,36 @@
"file:line. Otherwise tag the claim: `{tag}`."
)

ERROR_OUTPUT_RE = re.compile(
r"\b(?:error|exception|fatal|failure|failed|invalid|unsupported|traceback)\b|"
r"\bnot\s+supported\b",
re.IGNORECASE,
)
BRACKETED_ENUM_RE = re.compile(r"\[([^\[\]\n]+)\]")
COMMA_ENUM_RE = re.compile(
r"(?<![A-Za-z0-9_.+/@:-])"
r"([A-Za-z0-9][A-Za-z0-9_.+/@:-]*(?:\s*,\s*[A-Za-z0-9][A-Za-z0-9_.+/@:-]*)+)"
r"(?![A-Za-z0-9_.+/@:-])"
)
ENUM_VALUE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.+/@:-]*$")
CAPABILITY_RE = re.compile(
r"\b(?:accepts?|supports?|known\s+models?|valid|only)\b",
re.IGNORECASE,
)
CAPABILITY_ATTRIBUTION_RE = re.compile(
r"\b(?:fallback|hardcoded|hard-coded|built[ -]in|retract(?:ed|ing|ion)?)\b|"
r"\b(?:in|from)\s+(?:the|this|that|an)\s+error\b|"
r"\b(?:the|this|that)\s+error\s+(?:says|lists|reported|showed)\b",
re.IGNORECASE,
)
VALUE_CHARS = "A-Za-z0-9_.+/@:-"

CAPABILITY_MESSAGE = (
"hedge-runs-prove-it: this reply asserts capability values copied only from "
"error-shaped tool output ({values}). Verify them from a non-error source, or "
"attribute them as an error, fallback, hardcoded, or built-in list."
)


def _sentence_after(text: str, start: int) -> str:
end = len(text)
Expand Down Expand Up @@ -215,6 +250,80 @@ def _is_human_user_line(data: dict) -> bool:
return bool(text.strip()) and not text.lstrip().startswith("<")


def _tool_result_text(block: dict) -> str:
content = block.get("content")
if isinstance(content, str):
return content
if not isinstance(content, list):
return ""
chunks: list[str] = []
for item in content:
if isinstance(item, str):
chunks.append(item)
elif isinstance(item, dict) and isinstance(item.get("text"), str):
chunks.append(item["text"])
return "\n".join(chunks)


def _tool_results(lines: list[dict]):
for data in lines:
message = data.get("message")
content = message.get("content") if isinstance(message, dict) else data.get("content")
if not isinstance(content, list):
continue
for block in content:
if isinstance(block, dict) and block.get("type") == "tool_result":
yield block, _tool_result_text(block)


def _enumerated_values(text: str) -> set[str]:
runs = [match.group(1) for match in BRACKETED_ENUM_RE.finditer(text or "")]
runs.extend(match.group(1) for match in COMMA_ENUM_RE.finditer(text or ""))
values: set[str] = set()
for run in runs:
for raw_value in run.split(","):
value = raw_value.strip().strip("'\"`")
if len(value) >= 2 and ENUM_VALUE_RE.fullmatch(value):
values.add(value.lower())
return values


def _value_occurs(text: str, value: str) -> bool:
return bool(re.search(
rf"(?<![{VALUE_CHARS}]){re.escape(value)}(?![{VALUE_CHARS}])",
text or "",
re.IGNORECASE,
))


def error_only_capability_values(lines: list[dict]) -> set[str]:
"""Enumerated values whose tool-result sources are all error-shaped."""
error_values: set[str] = set()
non_error_results: list[str] = []
for block, text in _tool_results(lines):
error_shaped = bool(block.get("is_error") or ERROR_OUTPUT_RE.search(text))
if error_shaped:
error_values.update(_enumerated_values(text))
else:
non_error_results.append(text)
return {
value for value in error_values
if not any(_value_occurs(text, value) for text in non_error_results)
}


def _capability_feedback(message: str, lines: list[dict]) -> str | None:
if not CAPABILITY_RE.search(message or "") or CAPABILITY_ATTRIBUTION_RE.search(message or ""):
return None
values = error_only_capability_values(lines)
for verb in CAPABILITY_RE.finditer(message):
window = message[max(0, verb.start() - PROXIMITY): verb.end() + PROXIMITY]
repeated = sorted(value for value in values if _value_occurs(window, value))
if len(repeated) >= 2:
return CAPABILITY_MESSAGE.format(values=", ".join(repeated[:4]))
return None


def parse_lines(raw_lines) -> list[dict]:
parsed: list[dict] = []
for raw in raw_lines:
Expand Down Expand Up @@ -257,11 +366,12 @@ def decide_from_lines(message: str, lines: list[dict]) -> str | None:
if diagnosis:
return diagnosis
hedges = code_hedges(message)
if not hedges:
return None
if verified_this_turn(lines):
return None
return MESSAGE.format(tag=markers.TAG_TEMPLATE, hedge=", ".join(f'"{h}"' for h in hedges[:3]))
if hedges and not verified_this_turn(lines):
return MESSAGE.format(
tag=markers.TAG_TEMPLATE,
hedge=", ".join(f'"{h}"' for h in hedges[:3]),
)
return _capability_feedback(message, lines)


def decide(payload: dict) -> str | None:
Expand All @@ -272,7 +382,7 @@ def decide(payload: dict) -> str | None:
diagnosis = _diagnosis_feedback(message)
if diagnosis:
return diagnosis
if not code_hedges(message):
if not code_hedges(message) and not CAPABILITY_RE.search(message):
return None
transcript_path = payload.get("transcript_path") or payload.get("transcriptPath") or ""
lines: list[dict] = []
Expand Down
61 changes: 61 additions & 0 deletions engine/hooks/hedge-runs-prove-it/tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,35 @@ def turn_lines(verified):
return lines


ERROR_CAPABILITY_LIST = (
'Error: Execution model "gpt-5.6-luna" is not supported for execution agent '
'"codex". Known models: [gpt-5.5, gpt-5.5-pro, gpt-5.4, gpt-5.4-pro].'
)


def capability_lines(with_non_error_source=False):
lines = turn_lines(False) + [
{"type": "assistant", "message": {"role": "assistant", "content": [
{"type": "tool_use", "id": "models-error", "name": "Task", "input": {}}
]}},
{"type": "user", "message": {"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "models-error", "content": ERROR_CAPABILITY_LIST}
]}},
]
if with_non_error_source:
lines.extend([
{"type": "assistant", "message": {"role": "assistant", "content": [
{"type": "tool_use", "id": "models-answer", "name": "Task", "input": {}}
]}},
{"type": "user", "message": {"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "models-answer", "content": (
"Supported execution models: gpt-5.5, gpt-5.5-pro, gpt-5.4, gpt-5.4-pro."
)}
]}},
])
return lines


def transcript_file(lines):
tmp = tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False, encoding="utf-8")
tmp.write("\n".join(json.dumps(line) for line in lines) + "\n")
Expand Down Expand Up @@ -218,5 +247,37 @@ def test_diagnosis_gate_ignores_an_unreadable_transcript(self):
}))


class TestBlocksCapabilitiesCopiedOnlyFromErrors(unittest.TestCase):
def test_hook_blocks_two_values_repeated_beside_only_accepts(self):
path = transcript_file(capability_lines())
try:
code, err = run_hook({
"last_assistant_message": "codex only accepts [gpt-5.5, gpt-5.5-pro].",
"transcript_path": path,
})
finally:
os.unlink(path)
self.assertEqual(code, 2)
self.assertIn("error-shaped tool output", err)

def test_allows_values_also_listed_by_non_error_tool_result(self):
self.assertIsNone(detect.decide_from_lines(
"codex only accepts [gpt-5.5, gpt-5.5-pro].",
capability_lines(with_non_error_source=True),
))

def test_allows_attributed_fallback_list(self):
self.assertIsNone(detect.decide_from_lines(
"The built-in fallback list says codex only accepts [gpt-5.5, gpt-5.5-pro].",
capability_lines(),
))

def test_allows_a_single_repeated_value(self):
self.assertIsNone(detect.decide_from_lines(
"codex only accepts gpt-5.5.",
capability_lines(),
))


if __name__ == "__main__":
unittest.main()
Loading
Loading