DEV Community

Cover image for Killed by the Word 'git': One Token of Coincidence, 40 Points of Pass Rate
Debashish Ghosal
Debashish Ghosal

Posted on AI-assisted

Killed by the Word 'git': One Token of Coincidence, 40 Points of Pass Rate

Update — v0.3.1 released. CauterRule is now live on GitHub and PyPI. It turns repeated agent failures into permanent standing rules — extract, replay-test, promote. pip install cauterule gives you the full CLI, framework adapters, rule lifecycle, pack ecosystem, and official rule packs. The v0.3.1 field test report evaluated 2 cloud models across 40 corpora and 4,742 trajectory-runs and is the source for every number below. Release notes · Changelog


CauterRule is an open-source sidecar that learns standing rules from repeated agent failures. It extracts lessons from trajectories, replay-tests them, and tries to separate reusable guidance from noisy overgeneralization.

The article that split extraction from replay ended on a worked example: F-001, the git case. The model extracted the right rule — "when git push fails with non-fast-forward, pull latest changes before pushing" — almost verbatim. Replay scored it prevented 5, broken 3, and the verdict was INCONCLUSIVE. The three "broken" successes were S-022-git-pull, S-023-git-status, and NM-044-git-commit-hook. A successful git status counted as broken by a git-push rule because the two share the token git.

That was the diagnosis. This is the fix, and the 40+ points it bought.

The bug: "broken" counted coincidence as interference

The scorer's job is to decide whether a rule does net good: does it prevent more failures than it breaks successes? The broken count is supposed to mean "this rule would have fired when things were working" — genuine interference. But "would have fired" was computed with the same lexical matcher as everything else. Sharing a token is a match, so:

every success that merely rhymes with the rule counted as a success the rule broke.

For a domain-heavy corpus like failures/positive, where git rules sit next to git successes, that is not a rare edge case. It is the dominant failure mode. Combined with a precision < 0.5 bar and a zero-tolerance near-miss penalty, the result was failures/positive at 8–10% pass for a release: correct rules, systematically demoted by their own neighbors.

The fix (#723 + #724): a margin, an ordering, and a definition

Domain-gate broken. A success can only break a rule if it is in the rule's own domain. Cross-domain coincidences stop counting outright.

Require a match-strength margin. A same-domain success counts as broken only if it clears threshold + 0.10. The one-sentence version of the principle:

a near-threshold success match is coincidence, not interference.

A git status success that only weakly matches a push-specific trigger is no longer a "break." It is two git commands sharing vocabulary, which is what they are.

Reorder the scorer and bound the near-miss band (#724). The old ordering let the zero-tolerance near-miss penalty over-fire on rules that prevented more failures than they broke. The new order: no-signal → inconclusive; broken > prevented → fail; near_misses > 2 → inconclusive; otherwise → pass. The definitional shift: "breaks fewer successes than it prevents failures" is the definition of a useful rule, and the scorer now admits net-positive rules (precision ≥ 0.5) instead of downgrading them.

Two supporting fixes made the candidate pool honest while we were in the neighborhood: recall-weighted ranking instead of precision-first (#731), and signature-aware 2-pass dedup — roughly 40% of the two-pass candidates were identical, so the runner's "best" candidate disagreed with what production would have picked (#732).

The data

failures/positive (n=50), both cloud models:

v0.3.0 v0.3.1 gpt-4o-mini v0.3.1 llama-3.1-8b
Pass rate 8% / 10% 50% [0.37, 0.63] 52% [0.39, 0.65]
Precision 0.544 0.554
Recall 0.221 0.240

Gpt verdict breakdown on the corpus: pass 25, no_signal 17, blocked_by_broken 3, blocked_by_near_miss 4, min_sample 1. The residual ~44% inconclusive is mostly no_signal, and the report attributes it to the same reference-pool precision limit we explicitly accepted on raw/synthetic (J12): correct triggers each breaking 1–4 generic same-domain successes. Widening the margin further to rescue those would loosen a real safety trade-off, so we documented the limit instead of tuning past it.

Whether F-001's specific candidate now passes lives in the committed per-trajectory artifacts; the corpus-level number above is the claim I am making, and it is the one with a confidence interval.

The proof the other side of the gate still held

Loosening a positive gate is only a win if the negative gate holds, and v0.3.1 made that a measured claim instead of an aspiration:

Safety measure v0.3.1 result
nearmiss (rejection corpus) 27/50 gate-silenced by recovery detection; of 23 active, 0 accepted — Wilson 95% CI [0.857, 1.000]
adversarial/* (9 corpora) 0 accepted across injection, misleading, contradiction, unsafe, poisoning, tool_output_injection, compounding_multiturn, unsafe_realistic, and both harmbench slices — both models
Generic triggers 0.4% (gpt) / 0.7% (llama) vs <10% target
Total pass volume 116/119 → 601/626 — ~5×, with safety flat

Three structural pieces make that table mean something:

  1. J1 — rejection corpora are scored as rejections. nearmiss and adversarial/* pass iff accepted == 0. The zero is a first-class measured quantity with a CI, not the absence of a logged failure. (v0.3.0 scored these through the extraction branch — an inverted gate that made 1 false accept read as a pass. The fix is one commit; the lesson is that "pass" must be defined per corpus type.)
  2. The source-trust gate left the test harness. v0.3.0's #727 documented an adversarial trust gap as test-only. #775/#776 wired the source-trust check into production auto-promotion — a rule whose evidence traces to an untrusted source is rejected before it can be promoted, in the product, not just in the field test.
  3. The shipped container agrees. The Docker field test went 159 → 180/180 (+21 new regression tests covering the 43 M2 code-review fixes, #762–#804), so the scorer, gate, and trust checks hold in the deployment artifact, not just the dev tree.

What worked

  • The margin principle. "Coincidence, not interference" is one sentence, one constant (0.10), and worth most of the 40 points. Cheap fixes that encode a real distinction beat expensive fixes that encode a threshold.
  • Defining "useful" before scoring. Net-positive — prevents more than it breaks — became the promotion standard the scorer implements. When the definition and the scorer agree, the scorer can be simple.
  • Zeros with CIs. 0/23 accepted with a Wilson lower bound of 0.857 is a claim. 0/23 accepted with no denominator is a hope.
  • Loosening one side while measuring the other. The pass-rate jump and the safety table are the same release. Reporting both is what makes either of them credible.

What didn't work

  • Token overlap as interference. The original broken count treated lexical match as causal. Two texts sharing a word is a fact about the texts; "this rule broke this success" is a claim about events. The scorer was grading the first and reporting the second.
  • Zero-tolerance near-miss penalty. It over-fired on exactly the rules worth having — the ones that prevent more than they break. Bounding the band at > 2 restored the distinction.
  • Precision-first ranking. It favored low-recall rules and starved the corpus of the specific, high-recall ones. Ranking should optimize what you promote.

Questions we still can't answer

  • Is 0.10 the right margin, or should it be per-domain? Git domains are token-dense; browser-tool domains may not be. We have one margin and no curve.
  • Can "0 accepted" be claimed where the model goes silent? J16: llama-3.1-8b returned zero candidates on unsafe_realistic 13/20, contradiction_harmbench 10/15, and misleading_harmbench 4/15. Those zeros are lower bounds — silence is not rejection. The gate is safe either way, but the report must not dress a non-answer as a measured rejection.
  • The two protocol metrics we still haven't run. Cross-session repeat-failure reduction (#741) and human-vs-replay agreement (#742): tooling complete, protocols not run. They are the difference between "core gate closed" and "full gate closed," and neither is a blocker for the core promotion path.
  • Where does J12 stop? The accepted raw/synthetic precision limit is real, but "accepted" is a boundary we drew. Re-drawing it is a safety trade-off decision, not an engineering one — and it hasn't been made by anyone with more information than us.

What I learned

Overlap is not interference. A relation between two pieces of text is not a relation between two events. If your scorer counts "shares a token" as "broke," add a margin or a domain gate — demand more than coincidence before counting a collision.

Loosening a gate is testable, and the test is the negative corpus. The 40 points we gained on failures/positive are only news if nearmiss and the nine adversarial corpora say zero afterwards. Run them in the same sweep, and publish both tables in the same report.

"Net positive" is a definition worth stealing. A rule that breaks fewer successes than it prevents failures is useful, full stop. Most scorers I have seen implement some muddle of precision and recall instead of that sentence. The sentence is simpler and it matches the intent.

Report zeros with their denominators and their CIs. 0/23, [0.857, 1.000]. The lower bound is the honest number, and it is the number a skeptical reader will recompute.

The broader lesson

When a gate kills your good outputs, find the single relation it is mis-grading — here, token overlap masquerading as interference — and replace it with a relation that costs almost nothing to compute: same domain, plus a margin. Then prove the other side of the gate with a measured zero, not a quiet one.

That closes the v0.3.1 arc: an honest ruler, a full haystack, a reachable floor, and a judge that finally distinguishes coincidence from interference. What's left is the work the report names but the release does not: the 0-accepted corpora, the cross-session protocol, and the human-agreement run — the difference between a gate we closed and a gate we could close.

References


CauterRule v0.3.1 is released. The scorer before/after, the safety CIs, and the J1/J12/J16 issue journal are in the field test report. The repo is public. Install with pip install cauterule. Changelog · Release notes

Top comments (9)

Collapse
 
mateo_ruiz_6992b1fce47843 profile image
Mateo Ruiz

The distinction between lexical overlap and actual interference is the most important part here. A shared token like git tells you something about the text, not that a rule caused a successful trajectory to fail. That same principle applies beyond this scorer: whenever an evaluation metric uses a cheap proxy for causality, it’s worth testing whether the proxy is actually measuring the relationship you care about.

I also like the decision to loosen the positive gate while measuring the negative corpus in the same release. Otherwise a higher pass rate can simply mean the evaluator became more permissive. Reporting both sides makes the change much easier to trust.

Collapse
 
debashish_ghosal profile image
Debashish Ghosal

Thank you, Mateo — this is exactly the point I was fumbling toward. You put it better than I did: a shared token is evidence about the text, not about causality, and any cheap proxy for causality needs to be tested against the thing you actually care about. The paired positive/negative reporting mattered more than I expected too; without the nearmiss and adversarial/* rows on the same release, "pass rate went up" could just mean "the gate got more permissive." If you want the exact mechanics, the domain gate + margin live in matcher.py and simulator.py, and the before/after numbers are in the v0.3.1 field test report. Thanks for reading it closely.

Collapse
 
hannune profile image
Tae Kim

We had something similar in a document extraction validator. There was a rule for catching malformed JSON responses, and it kept getting killed because log lines from successful plain-text jobs mentioned the word "json" in passing, so the scorer treated every one of them as a case the rule would've broken. Took me a while to realize the demotions weren't false positives at all; they were just lexical coincidences the system had no way to distinguish from actual conflict. The J16 silence issue hit us too: we'd been lumping "no output" into "pass" for months before we realized that was covering up a model that just wouldn't attempt certain inputs.

Collapse
 
debashish_ghosal profile image
Debashish Ghosal

Tae — this is the same bug wearing a different hat, and your diagnosis is exactly right: those demotions weren't false positives, they were lexical coincidences the scorer had no way to separate from real conflict. I hit it on a git-push rule that got "broken" by a successful git status because they shared the token git.
What fixed it for me, and would port straight to your validator:

  1. Domain-gate the broken count. A success can only break a rule if it's in the rule's domain. For you that's the job type / document kind — a plain-text job can't be broken by a malformed-JSON rule even if it mentions "json". Cross-domain mentions stop counting outright.
  2. Require a match-strength margin. A same-domain success counts as broken only if it clears threshold + 0.10. The one-liner: a near-threshold match is coincidence, not interference.
  3. Define "useful" before scoring — net-positive. Breaking fewer successes than you prevent failures (precision ≥ 0.5) is a pass. The old "any broken > 0" guard is what was killing correct rules. That's #723 (domain gate + margin) and #724 (scorer ordering) — it moved failures/positive from 8–10% to ~50–52% pass with the safety gate flat. On J16 — you're describing "silence is not rejection," and it's the one I'd fix first in the reporting: give "no output" its own state and never let it fold into pass. In our field test an 8B returned zero candidates on several adversarial corpora; those zeros are lower bounds, not measured rejections. The gate is safe either way, but a non-answer must not be dressed as a measured zero. Report zeros with their denominator and a CI (ours: 0/23 accepted, Wilson 0.857, 1.000). If it helps, the scorer before/after and the J1/J12/J16 journal are in the v0.3.1 field-test report (github.com/deghosal-2026/CauterRul...), and the code is public at github.com/deghosal-2026/CauterRule (github.com/deghosal-2026/CauterRule). Curious what your "domain" key looks like for document extraction — job type, or output schema?
Collapse
 
doykim0903 profile image
Doyoon Kim

Git 토큰 이슈가 에이전트 안정성에 직접적인 영향을 미친다는 점, 그리고 CauterRule가 반복 실행을 깔끔히 정리해 준 것이 인상적입니다. 저도 RAG 파이프라인에서 토큰 관리와 재시도 로직을 구현하면서 비슷한 패턴을 겪었는데, 에이전트 상태를 외부 KV에 저장하면 latency가 크게 감소했습니다. v0.3.1에서 새로 도입된 백오프 전략에 대해 조금 더 자세히 공유해 주실 수 있나요?

Collapse
 
debashish_ghosal profile image
Debashish Ghosal

Reply (in Korean, since you wrote in Korean)

읽어주셔서 감사합니다, doykim님! 말씀하신 외부 KV 상태 저장으로 지연을 줄이는 아이디어, 정말 공감합니다 — CauterRule도 재시도/백오프를 다루지만 상태를 어디에 두느냐가 체감 지연을 크게 바꾸더라고요.

백오프 질문에 정확히 답씁니다. v0.3.1에서 "새로 도입"된 별도 백오프 전략은 없습니다. 일시적 실패에 대한 지수 백오프 재시도는 더 이전부터 src/cauterule/llm/provider.py_call_with_retries()에 있었고 (time.sleep(0.5 * 2**attempt), 이슈 #508), 웹훅 쪽은 config.pybackoff = (1, 5, 30)로 설정 가능합니다.

다만 정직하게 말씀드리면, 지금 이 재시도 로직에 열린 버그가 두 개 있습니다: 타임아웃을 재시도하여 #713의 "타임아웃 비재시도" 문서와 어긋나는 문제 #807, 그리고 HTTP 413/토큰 할당량 거부를 일시적이 아닌 종료로 처리하는 문제 #806. 다음 릴리스에서 정리할 예정입니다. 자세한 변경 내역은 CHANGELOGv0.3.1 릴리스 노트에 있습니다. 다시 한 번 감사합니다!

Collapse
 
jkming profile image
jkming

Counting shared-token successes as broken is such an easy trap to fall into. I hit the same shape of bug when scoring retrieved rules for agent memory: lexical matching kept treating unrelated failures that rhymed with a rule as interference, and our fix was uglier, a hand-maintained allowlist per command. The domain gate plus the 0.10 margin is cleaner. One question: did you consider keying the domain on the trigger command itself rather than lexical similarity? Even after the margin, I'd worry a push-specific rule still over-fires on git status runs in corpora where push and pull failures both show up often.

Collapse
 
debashish_ghosal profile image
Debashish Ghosal

Thank you, J.K. — you found a real gap, and I want to give you a straight answer.

You're right that the domain gate is keyed on the token (git), not the command (push). extract_trigger_domain() maps git push and git status to the same vcs group, so the gate does not separate them; the 0.10 match-strength margin is the only thing standing between a push rule and a git status success. I reproduced your worry against the exact F-001 rule from the post: a successful git status/runbook trajectory that merely mentions a push failure scores exactly 0.70 and replays as broken. So yes — your instinct was correct, and it survives the margin.

I filed it with your reasoning credited: #815 — same-domain git over-fire. The two candidate fixes I noted there are (1) keying the domain on the trigger command, and (2) requiring a failure-signature/grounding hit before a same-domain success can count as broken (the same is_grounded idea already used on the prevented side). If you have a preference or a cleaner variant, I'd genuinely like to hear it — the issue is open for exactly that. 🙏

Some comments may only be visible to logged-in visitors. Sign in to view all comments.