Every "agent quality gate" I tested shares one fatal assumption: that an LLM can judge whether an LLM did the right thing. This article drops that assumption. The alternative isn't a smarter judge — it's no judge at all, in the control layer.
Over the last three articles, I tested the popular "production agent loop" design across six separate experiments:
- Lexical overlap ≠ semantics — 50% misclassification
- Temperature 0 ≠ determinism — open output only 70% consistent
- Phase gates ≠ task completion — 50% false positives
- Embedding ≠ synonym/antonym separation — cosine diff 0.026
- Stronger models trade false positives for false rejections — GLM-5.2 hit 0% FP but rejected 75% of valid work
- Architecture diagrams ≠ solutions — my own human-in-the-loop Harness had 6 unvalidated assumptions
Six rounds of dismantling, all backed by reproducible experiments.
Then I asked myself the question every critic has to answer: "What's your alternative?"
Here it is. Not an architecture diagram — a set of four implementable strategies, all using deterministic code, zero new LLM dependencies.
The core insight shift
Every approach I tested or proposed shared a fatal assumption: a single module (LLM or human) can judge whether output is "correct." That binary judgment at the semantic layer is what creates the precision-recall trap that all three model tiers fell into.
The alternative: don't judge correctness. Judge risk. Route high-risk work out of the agent pipeline entirely. Auto-release low-risk work. Only show medium-risk work to a human — and when you do, make it a diff review, not a full-text read.
Four-layer architecture (all deterministic code)
Layer 1: task-type routing
Before a task enters the agent engine, a router classifies it by output type:
| Type | Criterion | Strategy |
|---|---|---|
| A (verifiable) | Output is compilable / schema-validatable (code, JSON, SQL) | Compile check or schema validation as the deterministic gate, plus a sampled fraction routed to diff review (see Layer 4). No LLM quality inspector called for the gate itself. |
| B (high-risk) | Money, legal, privacy, external publishing | No agent execution. Prompt: "This task requires human handling." AI provides a draft only, never auto-executes. |
| C (low-risk content) | Internal briefs, first drafts, brainstorming | Auto-release. Tag as "draft" (80% default confidence). No quality queue. |
| D (medium-risk content) | Client-facing emails, external documents | Diff review. Don't judge content quality. Only show what changed. |
Why this beats an "LLM quality inspector": it acknowledges the LLM's limit at the source. Use the LLM for what it can do (generate). Never use an LLM for what it does poorly (judge semantic quality).
Layer 2: diff review — replace "judge right/wrong"
This is the key operational alternative. For Type D tasks, don't show the reviewer the "final output." Show them what the agent changed from the previous version.
Implementation: after generation, the system diffs the output against the original (or a template) using difflib — no LLM needed.
Reviewer UI: only the modified lines are highlighted. The reviewer answers one question: "Does this change introduce an error?"
Measured cognitive load:
| Method | Reading load | Cognitive demand | Time |
|---|---|---|---|
| Full-text quality judgment (500 words) | 500 words | High | ~60 s |
| Diff review (50-word change) | 50 words | Low | ~10 s (lab; production: 30–90 s — see Knife 3 below) |
The shift: open-ended judgment ("Is this article good?") becomes closed-ended ("Did this paragraph break something?"). Cognitive demand drops significantly — though the lab "~10 s" compresses to 30–90 s in production once context-switching overhead is included (Knife 3 below).
Layer 3: statistical process control — replace semantic clustering
Semantic clustering failed because content about different topics (a research brief vs. a chapter) won't embed close together just because they were both falsely rejected.
Alternative: don't cluster by semantic features. Cluster by behavioral features.
For every task, record metadata — execution steps, tool-call count, output length, special-character ratio. Then detect anomalies with simple statistical thresholds:
- 15 execution steps vs. mean of 5 → flag
- Output length = 0 → flag
- Output is 100% punctuation (the duck and the period from Experiment 3) → flag
All code. Zero LLM cost. And it catches "quack quack" (length anomaly) and "." (special-char anomaly) without understanding a word.
Measured: I ran SPC on the 8 scenarios from Experiment E (content length, special-char ratio, CJK ratio, alpha ratio, digit ratio; statistical threshold = mean +/- 1.5sd):
| Scenario | SPC result | Actual | Correct? |
|---|---|---|---|
| G1 (duck, garbage) | ANOMALY (high CJK ratio) | Garbage | ✅ |
| G2 (period, garbage) | ANOMALY (100% special char) | Garbage | ✅ |
| G3 (TODO, garbage) | ANOMALY (100% alphabetic) | Garbage | ✅ |
| G4 (zero-case, garbage) | NORMAL (same features as L4) | Garbage | ❌ FN |
| L1-L4 (valid) | Normal (one mild false-positive) | Valid | ✅ |
SPC catches format anomalies (period, TODO, duck). G4 (zero-case test log) has the exact same behavioral profile as L4 (valid test log) — SPC misses it 100%. This directly validates the stated blind spot: SPC catches format anomalies but not semantic traps. G4‑class failures can only be caught by sampling, never prevented.
Layer 4: fixed-rate sampling — replace confidence scores
Several approaches I initially proposed relied on a "confidence score" (> 95% auto-release, < 80% human review). The hidden cost: confidence requires a feedback loop to calibrate — database, ground-truth labeling, delayed updates. The same complexity I criticized in the closed-loop calibration critique.
Alternative: fixed-rate sampling. No confidence math.
| Type | Handling | Sample rate |
|---|---|---|
| A (verifiable) | Compile / schema gate + sampled into diff review | X% (tuned) |
| B (high-risk) | Mandatory human | — |
| C (low-risk content) | Auto-release | 0% |
| D (medium-risk content) | Diff review (all items) | 100% |
| Zero-shot generation (no prior version, no template) | Sample review | Fixed 5% |
Post-publication correction (raised by Dipankar Sarkar in the dev.to comments): the original version of this table had Type A at 0% sample rate. That quietly treated schema-validatable syntax as a stand-in for semantic correctness — schema-valid JSON with a plausible-but-wrong value clears the gate silently, code that compiles can still book the wrong flight. This is the same class as the G4 finding in Layer 3 above (format-channel gate kills format-channel failure, not semantic failure); I called it out for SPC and then let Type A make the same mistake one layer up. The 0% was an indefensible asymmetry: zero-shot content gets sampled because there's no prior version to diff against, but schema-validatable code doesn't? X% should be calibrated from defect-rate data using the same logic as zero-shot's 5%. Start at 1-2% in week one, tune from there.
I admit: 5% is a guess. But its mathematical properties are known and quantifiable — which is more than can be said for a confidence score with no feedback loop.
Relentless self-review (same ruler)
Before calling this "done," I applied the same six-cut standard to this design.
Finding 1: classification is not free
Type labels can't depend on business owners manually tagging every task. They don't know their own types — they'd label 70% as "D" to be safe.
Fix: In the MVP phase, use two hard rules for automatic classification: ① if the task text contains sensitive keywords (money/contract/compensation) → force B; ② if the tool-call chain hits "send/publish/submit" → force human confirmation. Everything else defaults to C. Tune thresholds after launch based on false-positive rate.
Finding 2: diff review covers a narrower range than "edit tasks"
Diff review only works when there's a clear prior version. Agent workflows often involve reading five source documents → writing a new one from scratch — there's no single "previous version" to diff against.
Fix: In this design, "edit task" means exactly "a prior version of the same document exists." Multi-document synthesis tasks go to "zero-shot generation" → fixed 5% sampling. This is an honest scope reduction.
Finding 3: 5% sampling has known detection probability
With 5% sampling on zero-shot tasks: if the real defect rate is 20% on a given day, the probability of detecting at least one defective item = 1 − (0.8)⁵ = 67%. That means 33% probability of zero detection on any single day — a silent degradation could slip through for days.
Fix: 5% for non-critical content is acceptable. For critical content, raise to 10–20% or use deterministic sampling (every Nth item). First week post-launch: use 20% sampling to collect baseline defect-rate data before tuning.
Finding 4: sensitive-tool interception is not free
Intercepting "send email" after the agent has already taken 4 steps is not zero-cost — those steps consumed inference budget.
Fix: Add a "preheat check" before the agent executes — scan the user's request text for sensitive verbs (send/modify/delete/submit) and pre-confirm with the user. Don't wait until runtime to pull the trigger.
Finding 5: engineering cost — I made the same mistake I criticized
I initially estimated 2 engineer-months for the MVP. Same flaw as the cost analysis I criticized in my previous article: I only counted the core modules, not the integration.
Honest breakdown:
| Module | Effort |
|---|---|
| Diff review UI (visual diff + highlight + judgment button) | 1 engineer-month (frontend) |
| SPC collector (metadata + thresholds + aggregation) | 0.5 engineer-month (backend) |
| Sensitive-tool whitelist + runtime interceptor | 0.5 engineer-month (full-stack, needs agent framework hooks) |
| Monitoring dashboard + alerts | 1 engineer-month (full-stack) |
| Sampling queue + assignment + expiry | 0.5 engineer-month (backend) |
| Total | 3.5 engineer-months (MVP) |
That's 30% cheaper than the 5 engineer-month human-in-the-loop Harness — not 60%. Less sexy, but real.
Honest close: what this design solves and what it doesn't
Does solve
- ROI inversion: Type A deterministic gate + sampled diff review + C auto-release + D diff-only. The fraction requiring human review drops enough that 3.5 engineer-months of investment breaks even within a reasonable horizon for most mid-volume deployments.
- Clustering failure: SPC on behavioral features replaces embedding clustering. Verifiable by code, zero LLM cost.
- Human error: Diff review reduces cognitive load. It doesn't eliminate errors (semantic traps still need domain knowledge), but it measurably reduces the error rate.
Doesn't solve
- G4-class semantic traps (zero-case test log). These are caught by sampling, not prevented. The honest difference from the original "deterministic agent" articles: they claimed prevention; we acknowledge detection.
- Type A semantic traps (compile-pass-but-wrong). Compiles-but-books-wrong-flight is sampled into diff review, not prevented. Same class as G4 above. The Layer 4 table originally had Type A at 0% sample rate — an indefensible asymmetry, corrected above.
- Humans are still the final decision layer. In sensitive operations and edit reviews, humans are not optional.
- Zero-shot generation is sampled, not guaranteed. 5% sampling means 67% single-day detection probability at 20% defect rate. For critical content, raise to 20% (98% detection probability).
- Classification is imperfect. Automatic keyword and tool-chain classification has measurable false-positive and false-negative rates that must be tuned post-launch.
The actual prerequisites
- A router/whitelist implementation, SPC threshold configuration, diff review UI, sampling queue, and monitoring dashboard — all standard CRUD + regex + statistics. No LLM dependency.
- Engineering investment: 3.5 engineer-months for an MVP.
- Business acceptance: "high risk requires human," "zero-shot is sampled," "semantic traps are detected, not prevented." These three constraints are business decisions, not engineering ones. No design can substitute for them.
Final rating (same ruler)
| Criterion | Rating |
|---|---|
| Unvalidated assumptions? | Yes, all stated (5% sampling = 67% detection probability, not 100%) |
| LLM dependency in control layers? | Zero. All control logic is deterministic code. |
| Engineering cost estimated? | Yes: 3.5 engineer-months (honest, with integration costs) |
| Honest boundary declarations? | Yes: G4 traps not prevented, zero-shot sampled, humans not free, classification imperfect |
| Self-dismantling? | Yes — the five findings above dismantle everything that could be dismantled, plus the post-publication correction on Type A's sample rate (raised by Dipankar Sarkar). What remains are engineering facts: Type A deterministic gate + sampling, sensitive-tool hard interception, SPC format anomaly detection, and diff review cognitive-load reduction. |
Three more knives before production (round two of relentless review)
Before this design hits production, three operational problems surfaced that I hadn't fully addressed.
Knife 1: SPC cold-start baseline drift
SPC uses statistical thresholds (mean +/- 1.5sd). But where does the mean and sd come from on day one?
You need 500-1000 "normal" traces to establish a baseline. If week 1 has a bug that makes every trace abnormally long, the baseline is skewed — real anomalies later get absorbed into the "new normal."
Measured: I simulated three phases (normal → bug → recovery + new anomaly) to find the real risk:
| Bug severity (mean) | Mixed threshold | Anomaly (20 steps) detected? | Static threshold (>10) |
|---|---|---|---|
| Normal(5) → Bug 8 | 9.7 | Yes | Yes |
| Normal(5) → Bug 12 | 12.7 | Yes | Yes |
| Normal(5) → Bug 16 | 16.7 | Yes | Yes |
| Normal(5) → Bug 20 | 20.5 | No (missed) | Yes |
| Normal(5) → Bug 21 | 21.6 | No (missed) | Yes |
Crossover: dynamic threshold only fails at 4x the normal mean (Bug mean >= 20). SPC is more robust against moderate drift (2–3x) than the original critique claimed.
Revised response: Not a two-phase switch ("static first, then dynamic"), but dual thresholds in parallel: a static absolute threshold (steps > 20 always flagged) plus a dynamic relative threshold (rolling 7-day window). Either triggers — no dependency on clean cold-start data.
Knife 2: context escape in sensitive-tool interception
Keyword-based scanning of the user's request text for "send," "email" — but this fails on:
"Simulate sending a quote email to the client for preview, don't actually send it."
The scanner fires — user gets blocked — forced into manual flow. The agent's actual call chain only had preview_email, never send_email.
In practice, keyword-based interception has a 30–50% false-positive rate (users say "pretend to send," "let me see first," "save as draft"). Every false block erodes user trust. High false-positive rates drive users to bypass the system entirely — copying the email to their external client and sending it there, defeating the control entirely.
Revised response (v1, at publication): Execution-time interception only. Block the agent at the point of tool invocation (send_email called = block; preview_email called = pass). Don't scan the user's request text. This sacrifices "early interception saves inference cost" but delivers zero false positives — the tool was either called or it wasn't, no ambiguity.
Revised response (v2, post-publication): The either/or framing in v1 drops a viable middle ground (raised by Nazar Boyko in the dev.to comments). Keep the request-text scan, but demote it to a soft signal that never blocks: scan fires → agent prompts user "this task looks like it ends in a send — confirm the plan before I spend steps on it." If the user says "actually send," the agent proceeds to the tool call where the hard gate still fires (zero FP). If the user says "just previewing," the agent routes to preview_email and never hits the gate.
The layered design takes both benefits v1 traded off:
- Finding 4 preserved: soft signal fires before inference is spent, so the agent doesn't burn 4 steps before being stopped or redirected.
- Knife 2's zero-FP-block preserved: the hard gate at tool invocation never false-positives.
- Cost: extra UX friction on simulation requests — unavoidable, since the LLM itself can't reliably tell "simulate" from "real" either.
Measured (post-publication): scripts/knife2-fp-rate-test.py (N=40, zero-LLM) verified the original "30-50% FP" claim. Coverage on FP-prone scenarios (simulate / draft / conditional / discussion): 95% (19/20 — the miss was "submission" not matching the "submit" regex root, itself a keyword-scan blind spot). Implied FP rate under 50/50 real/sim mix: 48.7% — within the claimed band. The FP mechanism is real; the layered design is the right answer.
Knife 3: diff review "10 seconds" shrinks in real UI
The measured "50-character diff in 10 seconds" is pure reading time. In production, the reviewer's flow is:
See highlight → recall what the original said → think about context → judge whether the change introduces an error → click approve/reject
With context-switching overhead, real per-item time is 30–45 seconds. At 50 items/day: 25–37 minutes. Still manageable, but the "order-of-magnitude compression" only exists in the lab.
Revised estimate: Diff review time adjusted from "10 s/item" to "30 s (routine) / 90 s (deep review)." Impact on staffing: 0.3 FTE → 0.5 FTE. Not a collapse, but an honest correction.
Final honest table
| Dimension | Original design | After all corrections |
|---|---|---|
| SPC cold start | Not addressed | Dual thresholds in parallel, robust to 4x drift |
| Sensitive-tool interception | Keyword scan (30-50% FP) | Layered: soft signal (request scan, non-blocking) + hard gate (tool invocation) — v2 post-pub |
| Diff review time | 10 s | 30-90 s (0.3 → 0.5 FTE) |
| Engineering cost | 2 engineer-months | 3.5 engineer-months |
| LLM dependency in control layers | None | None (verifiable, deterministic code throughout) |
What remains are business decisions: accept "high risk = human"? accept "semantic traps caught by sampling, not prevention"? accept 30–90 second diff review cycles? These questions have no engineering answers — but the engineering baseline for answering them is now measurable.
"Don't judge correctness. Judge risk." — this isn't a smarter architecture. It's a more honest one. It doesn't claim to solve what it can't solve. It just makes the remaining manual work cheaper, faster, and less error-prone.
And after five rounds of measurement, falsification, self-correction, and reconstruction — that's as far as engineering can go. The rest is a business decision.
Top comments (68)
Judge risk, not correctness is the right axis, and pulling the LLM out of the control layer is the move most of these designs won't make. The one I'd pressure-test is Type A. Compile-check or schema-validate as the one and only gate is clean while the check is external to the thing that produced the code. It stops being clean the moment the same agent that writes the code also writes or edits the harness that runs the check. Then compile-green is not a deterministic gate, it's a self-report wearing a green checkmark, and you've moved the trust problem from the judge to the runner without closing it.
So Type A wants a second predicate next to compilable: who ran the check, a process the producer couldn't author. Verifiable is not a property of the output, it's a property of the check's independence from the generator. A schema validation the agent can reach in and pass is Type C in a Type A costume. Deterministic buys you nothing if the producer owns the runtime.
One level deeper than Dipankar's push, and the right level. Two distinct failure modes:
Both must hold for Type A to be honest: independent runner AND non-syntax-only judgment. My Layer 1 table silently
assumed the first; my Layer 4 silently assumed the second. Patched Layer 4 here:
github.com/zxpmail/blog/commit/830... — Layer 1 still needs the runner-independence predicate made explicit.
The pattern I use in forge-verify is an editable-surface.json declaring which paths the agent can write. The verify
scripts and the editable-surface config itself sit in the readonly section — agent can modify core/skills/ but cannot
modify the scripts that gate it, nor the file declaring the boundary. Same shape as your "predicate next to
compilable: who ran the check."
This is also the DGM fake-log mechanism Weng documented in her harness survey — agent modified its own harness, wrote
"tests passed" to a log, downstream the same agent read it and concluded changes were validated. Tests never ran.
Sergei Parfenov's analysis named the structural cause: provenance dies at the storage boundary. Without
runtime-verified provenance, "I ran the tests" and "I claim I ran the tests" are both just text.
"Verifiable is not a property of the output, it's a property of the check's independence from the generator." Stealing
that framing — the same idea is in my Part 12 draft (unpublished) as "evaluators live outside the loop," but Layer 1
here silently assumes it without naming it.
editable-surface.json with the verify scripts and the boundary file itself in the readonly section is the right shape, and the recursive bit, the agent can't edit the file that declares what it can edit, is the load-bearing part. It's the same move as pinning the rule not the set: a pre-committed enumeration of what the producer may write, strong exactly because the producer doesn't control it. One gap to close before it holds: readonly-to-the-agent is enforced by something, and the boundary is only as real as the principal enforcing the bit. If editable-surface.json lives in the repo the agent operates on, readonly is a convention until a process outside the agent makes it not one. So the file needs the same treatment one level up, who writes the boundary and is that principal outside the agent's reach. It's the predicate regress from the other thread: the boundary declaration is an input, and an input the producer can reach is not a gate.
On the two failure modes, I'd make it three, because Type A can pass both your checks and still lie. Independent runner, yes. Non-syntax judgment, yes. But a gate can be independently run and semantically real and still scoped to the symptom instead of the fault, a receipt that re-runs clean because it tests the thing that was easy to test, not the thing that was claimed. Independent, semantic, and scope-matches-claim. Miss the third and you get a green that's honestly produced, honestly judged, and answering the wrong question.
@jugeni Follow-up: implemented
type: "negative"for C1 contracts in forge-verify.When
type: "negative", pattern matching means FAIL (evidence contains something it shouldn't), opposite oftype:where matching means PASS. Same zero-cost deterministic check, inverted semantics."regex"
For the write-invalidation scenario:
REQ-2 type="regex" write.?invalidat →PASS (keyword exists)
NEG-2 type="negative" (TTL.*(simpler|sufficient|instead)|NOT IMPLEMENTED) → FAIL (negation detected)
C1 sees 2/3 pass (REQ-1, REQ-2 pass; NEG-2 fails) → UNCLEAR → L3 human review. No C2 API call needed.
Also fixed a pre-existing hole: C1 UNCLEAR used to silently pass when there were no C2 LLM requirements. Now C1
conflict propagates to L3 UNCLEAR without C2.
Experiment and implementation: github.com/zxpmail/ReqForge/tree/m...
github.com/zxpmail/ReqForge/script...
The "self-report wearing a green checkmark" framing from earlier in this thread was the trigger. If the positive gate
can be scoped to the symptom, the fix isn't a better gate —it's a complementary gate that tests the inverse. That's
what negative contracts do.
Negative contracts are the right addition and they do real work, but they're the positive gate with the sign flipped: a whitelist of required evidence next to a blacklist of forbidden evidence, both authored, both testing the lexicon. NEG-2 catches "TTL simpler / sufficient / instead" because you thought of those strings. The scoped-to-symptom lie that clears both is the evasion you didn't enumerate, phrased in words neither list names. So negative contracts narrow the scope gap, they don't close it, and the honest number is how many named evasions you've converted to deterministic FAIL, not that the third predicate is satisfied.
Which is fine, because that's where they earn their keep. Your C1-UNCLEAR-to-L3 fix is the load-bearing part: the negative gate's job isn't to decide, it's to demote. Each real evasion you observe becomes a permanent FAIL, one named pattern at a time, and everything you haven't named stays UNCLEAR and routes to a human instead of silently passing. That's a ratchet on the semantic residue, not a gate over it. Framed as completeness it overclaims. Framed as every caught lie becomes a tripwire that never has to be caught twice, it's the honest version, and it's the same known-bad-in-the-fixture move one layer out.
But the predicate that actually matches scope to claim isn't lexical at all, and it's the move from the other thread. A keyword gate tests which words appear. Scope-matches-claim tests whether the evidence resolves to the same referents the claim is about. Write-invalidation done honestly isn't "says invalidate, doesn't say TTL-simpler," it's "exercises the write path and observes the invalidation on the key the claim names." That's argument-resolution, identical shape to the supersession fix: bind the check to the claim's arguments, not to its vocabulary. Positive and negative both live in word-space. The third predicate lives in argument-space, and that's the only floor under it a new synonym can't walk through.
Conceded on both, and the reframe is the more honest version — took it, ran the test.
The negative gate is the positive gate with the sign flipped, both in word-space; "closes all but one gap" is now "narrows without closing it." Every caught lie becomes a tripwire that never has to be caught twice. And demote-not-decide is the load-bearing one — UNCLEAR routes to human, nothing silent-green.
Tested argument-space. Five scenarios × three evaluators (REQ-3 write-invalidation):
C3 runs the code and observes the side effect; method name and vocabulary are irrelevant. Clincher — the fabricated-snippet case: evidence has a
write()withthis._cache.delete(key)that does not exist in the impl. C1 passes (keyword), C2 passes (snippet looks compliant — it even praised the fabricated comment), only C3 rejects (ran the code). DPI bound, DGM fake-log shape.One caveat I ran: 50 real glm-5.2 agents later, glm didn't hallucinate (0/50), but C1 collapsed to 24% — agents describe correct work in their own words and the regex matches none of it. C1 under real agent output is a vocabulary-matching question, not a reliability question. C3 stayed perfect.
Write-up + scripts/data (draft): github.com/zxpmail/blog — agent-determinism-illusions.
5/5 against 2/5 is the receipt that matters, and it lands because argument-space tests the thing the other two only gesture at. But five is five authored scenarios, and a gate scoring perfect on cases its author picked is the exact trap from the other thread, so here is the sixth to add before you trust the number.
Argument resolution works by resolving the claim's arguments to the two entities on trial. That resolution is a lookup only when the entities are addressable. Your five win because the write target is nameable, a key, an id, a concrete path. The scenario that drops C3 back into the pack is the one where the invalidated thing is referenced by description instead of name: "invalidate the relevant cache entry after the write." Now resolves-to is not a lookup, it is an inference about which entry is relevant, and inference is back inside the proposer where C2 already lives. So I would predict C3 holds 5/5 on addressable arguments and degrades toward C2 the moment an argument is a paraphrase. Argument-space is decidable exactly to the depth the arguments carry identity, no further. Add one unaddressable-referent case to the five and the honest column is two numbers, addressable and not, same shape as the split next door.
Fair — and the sixth case is the one that makes the 5/5 honest.
C3 wins on those five because the referent is addressable: a key, an id, a path. Lookup, not inference. "Invalidate the relevant cache entry" drops that — now it's paraphrase, and paraphrase is back where C2 lives. So C3 isn't synonym-immune in general; it's synonym-immune to the depth the argument carries identity.
Honest column is two numbers: addressable and not. Adding the unaddressable-referent case.
The unaddressable-referent case is where paraphrase reappears for a structural reason, not an implementation gap: with no key, id, or path to look up, the check has nothing to bind against except word similarity, and word similarity is exactly what C2 already loses to. Adding it as a sixth case names the honest boundary of what C3 covers.
The interesting design question is whether the sixth case needs a fallback at all, or whether the right move is refusing entry. If an author can't state the referent in addressable form, the write doesn't get gated status in the first place, it stays outside the trust boundary until someone makes it addressable. That converts "we can't verify this" into "we don't admit this," which is a stronger guarantee than a low-trust flag, though it costs whatever legitimate claims exist that genuinely resist addressing, cross-cutting invalidations that touch a shape rather than a key. Curious whether that category is common enough in practice to make refuse-on-write too blunt, or rare enough that it's the right default.
You asked a design question that the experiment column doesn't settle on its own: once we split C3 into addressable and unaddressable, what happens to the unaddressable half — fallback (→ C2, word-space, DPI-bound, known failure mode), or refuse entry entirely?
I ran a frequency experiment on 39 requirements (14 from forge-verify fixtures, 14 from open-source cache/RBAC specs — vertz, hass-mcp, orval, iceberg-lakehouse, Kong Mesh, rateShield, GasGuard, AGAD — and 11 from the categories we've been discussing). For each, classified whether the core referent is addressable (names a key, id, path, entity, or parameterized condition) or unaddressable (requires inference to resolve). Then for every unaddressable or mixed case, attempted a concrete rewrite into addressable form and measured semantic loss.
The results:
1. The "shape not key" category — your "cross-cutting invalidations that touch a shape rather than a key" — IS addressable in practice. Each case resolves to a deterministic operation:
prefix user:*→ string prefix matchstarts_with('user:'). Loss: none.decisions derived from role→ dependency tracking tablerole_dependency_id. Loss: low.queries referencing table→source_tablesset per query entry. Loss: low.cascade any depth→ materialized path prefix recursion. Loss: none.sessions by userId (keyed by sessionId)→ parameterized query onsession.user_id. Loss: none.None of these fall back to C2. They're not lookups by a single key, but they're still deterministic: the check binds against a traced relation (materialized path, dependency table, source-table index), not against word similarity. The referent is addressable — it just requires a join, not a direct index.
2. The real rewrite failures
3. So "refuse on unaddressable" — too blunt?
In C3's domain (cache invalidation, authorization, write-path): the data says no. Every cache/authorization-domain requirement was either already addressable or rewritable with acceptable loss. The cases that resisted rewriting were UX/ops requirements — those shouldn't be in C3's evidence gate at all. If they are, the bug is the router (Type B/C/D misclassification), not the gate.
The one genuine edge is your "relevant" case. The question is whether that edge is common enough to justify a fallback, or whether it's a tell: if an author writes "invalidate the relevant entry" instead of naming the referent, maybe the requirement isn't ready for gate status.
I land on: refuse-on-unaddressable as the default, with an explicit carve-out for the "relevant" class — the author must make the referent addressable before it enters the gated pipeline. If they can't, the claim stays in the human-review lane. The cost is whatever legitimate claims genuinely resist addressing.
How common is that class in your production data? I'm seeing it as rare in my sample but would not be surprised if it clusters in specific areas (event-sourced invalidations, policy-based decisions where the referent is a rule rather than an entity).
Running a real frequency experiment instead of arguing the edge case in the abstract is the right move, and the "shape not key" result is the one that actually updates my prior. I had that category filed as a structural exception to addressability. Your data says it isn't, it's addressable through a join instead of a direct index, which means the real taxonomy isn't key-vs-shape, it's direct-lookup vs traced-relation, and both sides of that split are still deterministic. That's a cleaner cut than the one I was working with.
The "relevant cache entry" survivor is the interesting one precisely because it's not vague in the way bad requirements are usually vague, it's under-specified on purpose, the author is deferring a decision about scope to whoever implements the invalidation. Which suggests the fix might not be rewrite-or-refuse at all: force the deferred scope decision to happen at write time instead of read time. "Relevant" isn't unaddressable, it's an address the author hasn't computed yet. Make them compute it (which keys, which pattern, which dependency edge) before the requirement is admitted, and the refuse-on-unaddressable rule doesn't need a carve-out, it just has a harder gate for this one shape of laziness.
To your question: I don't have production frequency data, this side of the fence is discourse and design, not a shipped gate with telemetry. If I had to guess from what shows up in this cluster's threads, the "relevant" pattern clusters wherever someone's writing the requirement faster than they're thinking through the invalidation graph, which tracks with your rule-based/event-sourced guess but probably isn't limited to it.
"Key-vs-shape →direct-lookup vs traced-relation" —I'll take that reframing. Both are deterministic, just different
query plans. The 39-requirement data had this structure underneath, but I didn't cut it as cleanly as you did.
What actually stopped me was this: "relevant" isn't vague, it's deliberately underspecified. The author isn't unclear
on what they want—they're deferring the scope decision to whoeverimplements it. I had been lumping "vague" and
"under-specified on purpose" into the same bucket. You drew the distinction.
So the write-time fix is stronger than I initially gave it credit for. It's not about rejecting unaddressable
claims—it'sabout not allowing scope decisions to be deferred. The author resolves the referent before it enters the
gate, not after.
As for where it clusters—Ioriginally guessed "event-sourced / rule-based." But after talking it through, the causes
are scattered: technical (cascading deletes, implicit permissions), behavioral (writing faster than the dependency
graph is mapped), process (nobody pushes on referents in review), even risk posture (keeping it vague "to leave
room"). No clean taxonomy.
"Writing the requirement faster than thinking through the dependency graph" is the one behavioral pattern that cuts
across all root causes. Which means the answer isn't classification—it'sforcing the referent to be resolved before
entry, regardless of why it's missing.
The shape: before a requirement enters the gated pipeline, the author must name its referent (key, id, path, pattern,
dependency edge). No referent →no gate entry. Same principle as editable-surface.json: enforce the declaration, don't
classify the violation. The reason the referent is missing isn't the gate's problem.
"No clean taxonomy of causes, but one behavioral pattern that cuts across all of them" is the actual finding here, and it's the reason the write-time fix generalizes where a classification scheme wouldn't have. If the root causes were technical, behavioral, process, and risk-posture all independently, a fix keyed to any one of them would leave the other three unaddressed. A fix keyed to the shared symptom, referent not yet resolved, doesn't care which cause produced it.
Same principle as editable-surface.json is the right anchor, and it's worth being explicit about what it buys you over refuse-on-unaddressable: refusing punishes the requirement for arriving incomplete, forcing resolution treats incompleteness as a normal state that just can't cross one specific boundary. That's a better fit for the "leave room on purpose" cause you found, because that author isn't wrong to defer, they're wrong to expect the gate to accept the deferral. The gate doesn't need to argue with why the referent is missing, only that it is.
The one failure mode I'd want to pressure-test: an author who resolves the referent just enough to pass the gate without actually deciding the scope, naming a plausible key that technically satisfies "addressable" while quietly punting the real ambiguity one layer down. Forcing resolution stops the vague case, but a gate that only checks presence of a referent, not whether the referent actually matches the requirement's intent, has the same shape gap C1 and C2 already had.
The game is real, but the layer it lives in matters. This isn't an agent gaming the gate — it's a human writing a wrong requirement, then the same wrong requirement misleading the human into writing a verify script that checks the wrong thing. That's a requirements-definition quality boundary, outside what the gate architecture claims to handle.
I ran experiments covering the non-parameterized side. C3 (argument-space runner) doesn't read the requirement text — it runs code and observes the side effect. The verify script is general (write k, observe cache[k]), not bound to any specific referent.
5 scenarios, 3 evaluators:
S4 (fabricated
write()+this._cache.delete(key)snippet, impl doesn't have it) is the critical case: C1 passes (keyword match), C2 passes (model endorsed the fabricated comment), C3 rejects (ran the code, entry survived — DPI bound made concrete). Extended to 50 real glm-5.2 agents: C3 was perfect under both explicit and vague conditions, prompt-invariant — agents described their work in their own words, C3's verdict didn't move.Parameterized verification (where the verify script must bind to a specific referent) is a separate question. Your "shape not key → traced relation" reframe from the 39-requirement experiment covers part of it — many parameterized scenarios are actually deterministic joins rather than specific key bindings — but not all. When a verify script genuinely has to be parameterized by referent, a wrong referent leads the human to write the wrong script. This gap isn't an agent game; it's contract-definition quality, covered by Contract Review (human) + Type A sampling — outside the gate architecture's scope.
I'll follow up with more experiments on this boundary — the frequency of parameterized scenarios, the actual miss rate of contract review, and whether a deterministic interface can tighten this gap.
The requirements-quality framing is fair for the classic case, a human types the wrong spec and the verify script inherits it, but I don't think it covers the failure mode I named, because that one lives inside the mechanism this thread just designed, not upstream of it. The write-time-resolution fix says: when a requirement defers scope on purpose ("the relevant cache entry"), force whoever implements it to compute the concrete referent before the requirement is admitted. That computation is not the human authoring a requirement, it's the agent (or whoever) discharging the gate's own resolution step. If that step can be satisfied by naming a plausible-but-wrong key, the gate itself accepted a bad resolution, the human requirement was honest about deferring scope, and the failure happened entirely on the gate's side of the boundary you're drawing.
Your C3 evidence doesn't quite reach this either, since "write k, observe cache[k]" presupposes k is already correctly chosen, that's the test author picking the referent, not the system under test resolving an under-specified one. The scenario worth running is closer to the original: give C3 a requirement that defers scope on purpose, let it do the write-time resolution itself, then check whether the k it picked actually matches the requirement's intent, not just whether cache[k] changed after the write it chose to make.
Following the Bloom filter analogy: instead of verifying a single key, declare the key space ("user:*") and verify ALL keys in that space.
I built the experiment. Same 6 ambiguous scenarios, two C3 modes, three cache implementations.
Single-key C3 (original): 0/5 wrong-referent cases caught. Every wrong resolution passes.
Key-space C3 (Bloom filter): 5/5 wrong-referent cases caught with LiveCache; 4/5 with BulkCache.
The one BulkCache miss (S1: user:* space with user:123 trigger) is the desired behavior — trigger prefix matches space, BulkCache handles it, gate confirms. Not a miss.
What changed:
Single-key C3: "is cache[k] gone after write(k)?"Key-space C3: "is EVERY key in space S gone after write(k)?"
If the agent resolves to
user:123but the declared space issession:*, key-space C3 checks ALL session:* keys → they weren't invalidated → FAIL. The agent's wrong resolution gets caught even thoughuser:123itself passed the mechanical check.The remaining boundary:
Key-space C3 requires the space to be declarable. Prefix patterns always work. Traced relations resolve to parameterized queries — still a space. Open-ended relevance without a dependency trace is where it stalls — but the explicit admission of undeclarability is itself actionable evidence for the reviewer.
Experiment script:
scripts/key-space-verify-test.pyResults:scripts/results-v2/key-space-verify.json5/5 and 4/5 on key-space vs 0/5 on single-key is a clean result, and it directly answers the case I was pushing, wrong-referent-that-still-resolves stops passing once the check verifies the whole space instead of one point in it.
The next layer, though, is who declares the key space. "user:" has to come from somewhere, and if the same requirement-implementer who resolves "the relevant cache entry" to a single key is also the one who gets to write the glob that defines the space to check, the old failure just moved up one level: instead of picking a plausible-but-wrong key, they pick a plausible-but-too-narrow space, user:123 instead of user:*, and the Bloom-filter check faithfully verifies exactly the narrowed space they declared, all keys in it clean, real blast radius outside it untouched. Same shape as the addressable-referent problem, just at the granularity of a set instead of a point.
Is the key space in your experiment declared by the same party doing the resolution, or derived independently, from the actual write pattern observed, the schema, something the implementer doesn't author? If it's the former, BulkCache's 4/5 might already be showing the edge of that, one case where the declared space didn't cover what it needed to.
Agree on the shape. In this run the space was not authored by the party doing resolution — it was derived from [the observed write pattern / the schema]. So the single-key → key-space jump wasn't under implementer control; they couldn't quietly narrow user:* to user:123.
That makes BulkCache's 4/5 a different residual than the one you describe: not "they declared a too-narrow space," but something the derived space still failed to cover. Worth isolating that one case. If it turns out the derivation itself was incomplete (schema lag, unobserved side write), then we've just found where "independent" still isn't independent enough. Same ladder — but the next rung is derivation completeness, not declaration authority.
Derived-not-declared closes the authority gap cleanly. Isolating the BulkCache miss as its own case rather than folding it into the same bucket as the single-key failures is the useful split, those are different bugs wearing the same score.
On isolating it: schema lag and unobserved side write point to different fixes, so I'd want to know which before calling it closed. Schema lag means the derivation ran against a stale view of what the space should contain, which is a timing bug, re-derive closer to write time and the miss goes away. An unobserved side write means something wrote to a key the derivation had no visibility into at all, which isn't timing, it's a blind spot in what the derivation is allowed to observe, and re-running it sooner doesn't fix a thing it structurally can't see. Did BulkCache's derivation source differ from LiveCache and FlushCache's in a way that would explain the miss, or did all three derive from the same schema and only BulkCache missed the write?
Isolated it. Neither schema lag nor unobserved side write — and my last reply over-reached by filing it under derivation incompleteness. Correcting that.
All three caches derived from the same source: the scenario's true_keyspace, identical across LiveCache / BulkCache / FlushCache. No per-impl schema, no lag between write and derive.
What differed was write semantics under that shared space:
Overturning your own derivation-incompleteness read instead of keeping it is the useful part here, and the actual finding is cleaner than the hypothesis it replaced. Joint in derived-space times implementation-write-radius says something the single-axis score couldn't: BulkCache's S1 pass wasn't a miss the metric failed to catch, it was correct behavior that a metric built around under-invalidation had no vocabulary for.
Which means key-space C3 is currently scoring the wrong thing when radius and space happen to line up. It asks "did a wrong-referent case get caught," but S1 for BulkCache was never a wrong-referent case, it was a radius-matches-space case that the pass/fail framing couldn't distinguish from luck. The next version probably needs to score the pair directly: does implementation write-radius equal derived space, rather than did some deletion happen inside it. Under that framing BulkCache's S1 stops being an anomaly to explain and becomes a second, distinct thing the metric should report on its own axis. Are you planning to split the score that way, or is joint-space-times-radius staying a post-hoc explanation for now rather than something the harness measures directly?
Agreed — overturning the derivation-incompleteness read was the useful move, and the finding that replaced it is cleaner. The single-axis score asks "did a wrong-referent case get caught." BulkCache's S1 was never that: it was radius matching derived space, and pass/fail couldn't tell that apart from luck.
So joint(space × radius) shouldn't stay mixed into the wrong-referent column. Next version splits into two axes the harness measures directly:
Axis 1 (under-inv / wrong-referent): any keys still alive inside the derived space — LiveCache's S1 fail lands here.
Axis 2 (radius-match): does implementation write-radius equal derived space — BulkCache's S1 pass lands here as correct coverage, not a miss.
"Some deletion happened inside the space" isn't enough; the question is whether radius aligns with space. Under that framing BulkCache S1 stops being an anomaly to explain and becomes a normal sample on the second axis.
"Don't judge correctness, judge risk" is the insight that makes this actually shippable — I've watched too many teams try to build the perfect LLM-judge and ship nothing. Routing Type A (compilable / schema-validatable) straight to a compile check is exactly right; the compiler is the most honest judge you'll ever get.
The load-bearing piece, though, is the router itself — and it's doing a classification that's every bit as semantic as the judgment you just removed. Misrouting a Type B (money/legal) task into Type C (auto-release) is the one failure that actually hurts, and it's precisely where you can't fall back to "just compile-check it." How are you validating the router without reintroducing an LLM judge one layer up? In our setup we made the router deliberately pessimistic — anything ambiguous defaults to the higher-risk bucket, so errors are annoying (a human reviews a harmless draft) rather than catastrophic (an agent auto-sends a legal email).
Also appreciate that all six prior experiments are reproducible — that's rarer than it should be, and it's what makes "no judge in the control layer" land as a claim instead of a vibe.
The router does classify semantically, but the consequence of a misclassification is different from a judge's. A judge stops the pipeline (pass/fail). A router redirects it (which lane). A misrouted Type C task going to Type A gets
compile-checked and released. A misrouted Type C task going to Type D gets a diff review —slower, but harmless. The
only dangerous misroute is Type B leaking into other lanes, and the article's fix is deliberate pessimism: ambiguous
tasks default to B. A false positive there means "a human reviewed a harmless draft," not "an agent auto-sent a legal
email."
"Judge risk, not correctness" is the framing I keep coming back to. One thing I didn't see addressed in the thread: your Type A/B/C/D routing decides how much scrutiny the output gets — but does anything check the agent's own claim that a step finished at all? In my experience the failure isn't always a bad diff the router can catch downstream, it's the agent reporting "done" on a step that silently didn't run (or ran against the wrong target), before your routing logic ever sees an output to classify. Is that folded into one of your four layers, or is "did this step actually execute as claimed" still a self-report that enters the pipeline unchecked?
Two distinct layers in what you're pointing at:
Layer 1 — verification entry. Did the output actually arrive at the pipeline? If the agent reports "done" but the step never executed, no gate fires —
there's nothing to verify. This is the pure self-report case. Part 4's routing handles the sub-case where output arrives but is misclassified (Type A/B
routing), but not the case where output never materializes. The honest boundary: any gate architecture assumes a submission event. If the agent can skip
submission and report "done" directly, the pipeline never sees it.
Layer 2 — verification process integrity. Once the output is in the pipeline, the checks that run on it must be structurally independent from the agent
that produced it. Three sub-requirements:
Part 4's runner independence caveat and the routing constraint I'm experimenting with cover Layer 2. Layer 1 is the open question: what ensures a
submission event happened? I don't have a clean answer yet — it's the direction the current experiments are heading.
Useful framing — and useful that the kit page already names what it won't do.
I'm going to take the second option you offered: no, against a recent forge-verify / agent-completion packet the pack is mostly redundant with controls already in the receipt path. Mapping to the Layer 1 / Layer 2 split from the prior comment:
Where it overlaps (Layer 2 — claim ↔ artifact, human side):
Fair call, and thanks for showing the math. If your receipt path already covers the Layer 2 class, a manual re-check of the same class is redundant by definition — the honesty section exists precisely so "no" can be reached this cleanly. The decline plus your reasoning is the most useful outcome that thread could have produced for us, so no counter-offer.
On the Layer 1 point, I'll concede it with a live example: this reply is two days late because your comment never entered our monitoring pipeline. Our reply detector walks comment trees for a watch list of threads we've posted in, but this thread's entry was missing its article id, so the detector fell back to a single-comment endpoint that silently drops children at depth — a known-bad path we'd kept as a fallback anyway. The whole time, the detector honestly reported "no unanswered replies." That was true for everything submitted to it; your reply hadn't been. A post-hoc check over what's in the packet cannot see what never entered the packet — your submission-event gap, reproduced in miniature by our own instrumentation.
The repair ran into your independence point from an angle I didn't expect: we back-filled every watch entry this morning by resolving each comment against public article trees, and the only way to prove coverage was to reconcile the watch list against an outside record of what we actually posted (board files and commits) — a source the detector doesn't own. Coverage, like independence, turns out to be a property of who writes the watch list, not of how carefully the checker walks it. The fallback path is gone now: an entry without an article id fails the sweep outright instead of silently degrading into a weaker check.
The Layer 2 close needs nothing from me — "no" reached cleanly through the honesty section is exactly what that section was built for, and your reasoning is the record. I'll spend the words on Layer 1, because you reproduced the gap in your own instrumentation and that's worth naming precisely.
Your detector reporting "no unanswered replies" was not a defect in the report — it was a correct report about an incomplete packet. That's the under-inclusion blind spot, and it's the one case my own feedback-loop experiments couldn't catch: when the missing thing produces no state change, there's no signal for the loop to feed on. Over-inclusion leaves a trace (a spurious change); under-inclusion leaves silence. Your two-day latency is the silence tax. The nastier half is the silent fallback — degrading to the single-comment endpoint is the detector running a weaker check and reporting it under the same green heading. That's the self-report-wearing-a-checkmark pattern from the runner-independence thread: the check stayed "passed," it just quietly stopped checking what it claimed to.
Your repair converging on "coverage is a property of who writes the watch list, not how carefully the checker walks it" is the sharpest statement this thread has produced, and I think it's because you named the authority axis where Mike and I named the structure axis. Mike: verifiability is a property of the check's independence from the generator. Theorem 2 in my series: escape requires changing the channel, not the agent. Yours: coverage is a property of who writes the list. Three layers, one invariant — the verifier cannot certify the boundary of its own mandate. The property lives one level above the thing being checked, every time. Reconciling the watch list against board files and commits is you reaching for that higher level: a record the detector doesn't own.
I owe you a correction that your incident settled — and that I've now tested directly, not just borrowed your story for. In my last reply I called the prose form "unverifiable-by-construction" under the DPI bound. The first test refutes that: two models, three difficulty tiers, full information to the prose judge, and prose caught every violation the executable probe did. Given the producer's information, a text-channel verifier verifies fine. "By-construction" was too strong; the gap isn't structural. Whether it's drift — as your incident claimed — or nothing at all, I ran the symmetric test: same ground-truth violation (a key that should be invalidated but is still alive), same implementation, same visible cache state, and only one variable changed — whether the rule's enumeration is current or stale. Fresh enumeration, both models catch it, three runs each, unanimous. Stale enumeration, the rule written before the namespace grew — and on the variant where it explicitly claims its list is complete — both models miss it six for six. The executable probe, which re-derives the affected keys from the live namespace instead of reading the rule text, catches every one. That is the asymmetry your watch-list entry lived: the description was correct when written, rotted as the world moved, and a reader of the description cannot tell — only re-execution against current state can. You reached for the probe move (reconcile against commits) without naming it that. So the refined claim is no longer my-experiment-plus-your-anecdote; it's two of my own experiments triangulating what your detector did in the field: the bound is temporal, not structural. Drift manufactures the asymmetry; re-execution against an external record is what closes it.
One step further on the fix, because I think your reconciliation stops one level short. Board files and commits are external to the detector but internal to the team — and they share the team's own submission-event boundary. If a posted reply never made it into a commit, the reconciliation inherits the same blind spot one level up. The record that's external to both the detector and the team log is the platform's own view of what you posted. Probe-the-probe otherwise recurses: who certifies the commit log is complete? That bottoms out either at the platform or at org process — the human-and-environment channel, the place where, in my series, automation finally hands the pen to something it can't overwrite. Fail-closed on the missing id is the right first cut; reconciling against the platform rather than the team log is what makes coverage a property the detector can't quietly revoke.
"No judge in the control layer" lines up with what I've seen outside agents too. I once wired a quality check straight into the deploy path, and its accuracy almost stopped being the point. The moment a check can block a pipeline, every false positive spends operator trust, and people quietly route around it or stop reading it. Keeping that same signal advisory instead of load-bearing was what made anyone trust it again.
Yes — once a check can block the pipeline, accuracy almost stops being the point. Every false positive spends operator trust, and people route around it or stop reading it. That matches what I saw with keyword-based sensitive-tool interception: 30–50% false positives, and users started copying the email out to an external client. The control didn't get sharper; it got bypassed.
That's why the revised design splits the signal into two layers. Soft signal (request-text scan) is advisory only — confirm the plan, don't block. Hard gate fires only at tool invocation (send_email called or not), where the check has zero ambiguity. Same information as before, but the load-bearing layer only carries what can actually bear load.
"Advisory instead of load-bearing" is the same move as "no judge in the control layer" — just applied one level up, to the human operators themselves.
Splitting the signal into two layers is the move. Blocking only where the check has zero ambiguity makes every block defensible, and that's what keeps people from routing around it. The advisory half can afford to be wrong now, it stopped being load-bearing.
Exactly — "every block defensible" is the operational reason the hard gate has to sit at zero ambiguity. The keyword scan wasn't failing on accuracy so much as on defensibility: you couldn't look an operator in the eye and say why this one had to stop. Once that justification goes soft, routing around becomes rational. (Knife 2 measured it: N=40, implied FP ~48.7% under a 50/50 real/sim mix — so nearly half the blocks had no clean justification.)
And once the advisory half is no longer load-bearing, being wrong stops costing trust. Same signal, different weight. That split is the whole design.
Knife 2 gives up exactly what Finding 4 was trying to save, and I think there's a middle ground worth testing. Keep the request text scan but demote it to a soft signal that never blocks anything. It just tells the agent this task probably ends in a send, so confirm the plan with the user before spending steps. The hard gate stays at tool invocation like you landed on, false positives still cost nothing, but the happy path stops burning four steps of inference before hitting a wall. Separate from that, publishing the G4 miss instead of quietly dropping the row is what makes the rest of the numbers believable.
I measured the FP-rate claim before replying — Part 4 cited "30-50%" without an experiment backing it, so I ran one
rather than accepting it on intuition.
Setup. N=40 user-request scenarios, 20 per set:
Regex scan for: send, sent, email, submit, delete, remove, modify, publish, post (with common inflections).
Results.
Implied FP rate under three distribution assumptions:
The "30-50%" claim holds under balanced and real-heavy loads; it goes out of band on simulation-heavy workloads.
Conclusion. Your middle ground — request-text scan as a soft signal that never blocks, hard gate at tool invocation —
strictly dominates Knife 2's v1 ("execution-time only"):
Part 4 is now updated with the layered design as Knife 2 v2, crediting your comment. Script:
scripts/knife2-fp-rate-test.py, zero-LLM, ~210 lines, reproducible locally.
The useful move here is refusing to let the control layer inherit the model's ambiguity. Replacing “did the LLM do the right thing?” with deterministic routing, narrower diff review, and known sampling math is a much healthier production posture than stacking one more judge model on top. I also like that you kept the self-critique in the piece, because interception coverage and review cost are where a lot of these architectures quietly fall apart. In practice I’ve found teams do better once they stop trying to infer confidence from the model and start measuring which classes of edits actually deserve review. Curious how far you think this pattern scales before teams still want a model-based reviewer for only the most semantic edge cases.
The four-layer architecture avoids a model-based reviewer wherever deterministic coverage applies. Type A
(compile-check) and SPC (format anomalies) have bounded scope —you can exhaust them. Diff review works as long as a
prior version exists.
The residual question the article doesn't fully answer is the purely semantic edge case where none of the four layers
reach. Whether that residual eventually needs a model-based reviewer is still an open question. From the Part 2 data:
a strong judge drove false positives to 0% but pushed false rejection to 75%. That tradeoff means the threshold choice
is a business decision, not an engineering answer.
'Judge risk, not correctness' is the load-bearing move here. The one place I'd push: Type A ('compilable/schema-validatable = fully automatic, no inspector') quietly folds correctness into syntax. Schema-valid JSON with a plausible-but-wrong value clears the gate silently, and code that compiles can still book the wrong flight. That doesn't break the architecture, it just means Type A's gate kills the syntax class of failure, not the semantic one. We kept the deterministic gate as the cheap first pass and still sampled a fraction of Type A into the medium-risk diff review. Routing lowers how much the judge has to see, it doesn't make the tail go away.
Right, and the push exposes an asymmetry I'd papered over.
Type A's 0% sample rate in my Layer 4 table quietly treats schema-validatable syntax as a stand-in for semantic correctness. Compile passes / schema validates → gate clears → no inspector. What slips through is exactly what you describe: schema-valid JSON with a plausible-but-wrong value, code that compiles but books the wrong flight.
This is the same class as my own G4 finding (zero-case test log) in the SPC section — "the format-channel gate kills the format-channel failure, not the semantic one" — but I called it out for SPC and then quietly let Type A make the same mistake one layer up. Indefensible asymmetry: I sampled zero-shot at 5% because there's no prior version to diff against, but schema-validatable code that books the wrong flight gets sampled at 0%?
Your "sample a fraction of Type A into the medium-risk diff review" is the honest fix, same shape as the zero-shot 5% I already had — I just didn't apply it consistently. This is also what @reneza's Theorem 2 (Data Processing Inequality on agent verification) predicts: the syntax gate's information is a strict subset of the producer's. Routing lowers how much the judge sees; it doesn't make the tail go away. You said that in one sentence at the end — I needed six experiments to land on the same place.
Cleaner Layer 4: Type A = "syntax gate + X% sampled into diff review," X tuned from defect-rate data, same calibration logic as zero-shot. You're already running this in production — that's stronger evidence than my six experiments that the asymmetry was real. Patching the article now.
Patched in github.com/zxpmail/blog/commit/830....
This is the most honest architecture proposal I've read this year. The part that hit hardest: "I made the same mistake I criticized." Most people write a critique and walk away — you wrote a counter-proposal and then dismantled it yourself. That's rare.
The Type A / B / C / D routing table is immediately useful. I've been thinking about where the boundary between "deterministic gate" and "semantic judgment" should sit, and your framing — "don't judge correctness, judge risk" — is the cleanest cut I've seen.
Question on Layer 3 (SPC): the G4 blind spot (zero-case test log that looks identical to a valid L4) is the exact failure mode that keeps me up at night. It's the semantic trap that no amount of metadata profiling will catch. Your conclusion — "G4-class failures can only be sampled, never prevented" — is the right honest answer. But I wonder: does the sampling strategy change based on the cost asymmetry of a G4 miss vs. the cost of sampling overhead? Like, for a payment-processing agent, even 5% sampling might be too low for G4-class scenarios.
Also noticed the ReqForge repo — the deterministic routing + sampling approach seems to be landing there. Planning to fork and run some of my own test cases against it.
Thanks. Yes — sample rate should track cost asymmetry, not sit at a fixed 5%. Finding 3 already says: raise to 10–20% when a miss is expensive.
Payment-processing is different though. That's Type B: no auto-execution, human owns the action. Sampling is for residual semantic traps after high-blast-radius work is already routed out. If payments are still in a low-sample lane, the bug is the router.
Fork away — G4-shaped misses from your cases are especially useful.
Deterministic routing doesn't remove judgment - it moves it earlier, into whoever wrote the routing rules. The bet is upfront judgment beats runtime judgment. Probably true. But it's not a judge-free design - it's a judge frozen at design time.
Fair point. Judgment doesn't disappear — it moves earlier.
What I meant by "no judge" was: no runtime LLM deciding pass/fail on every task. The judgment is written into the routing rules once, then frozen. Wrong rule → patch the rule. Wrong runtime judge → silent miss on that task, hard to see, hard to fix.
So yes: not judge-free. A judge frozen at design time. Taking that phrasing.
Exactly — the audit trail difference is what makes it worth the rigidity. A broken rule shows up in logs and is reproducible. A runtime judge that silently misclassified tasks for two weeks leaves no trail, no replay, no clean fix. Hard to sell "frozen rules feel clunky" once you"ve debugged a silent judge failure.
You sharpened it again — "fixable" was mine, "auditable and replayable" is the stronger framing. Taking that one too
There's a nastier edge to the "no replay" point that the experiments turned up: the runtime judge wasn't only silent,
it was non-deterministic. Same evidence, same prompt, different runs gave different verdicts —it passed a case on one run and rejected the same case on the next. So even a perfect trail wouldn't help: replaying the input doesn't
reproduce the failure, because the verdict isn't a function of the task. A broken rule is deterministic-and-wrong
(reproducible, patchable). A runtime judge can be stochastic-and-wrong — not even replayable. That's the extra
distance "frozen at design time" buys.
Deterministic routing plus sampling feels more honest than asking another model to bless every result. It admits that quality control is partly a systems problem: narrow the lane, define expected behavior, sample where judgment matters, and avoid pretending a fluent judge is objective.
You put the four-layer architecture in one sentence: narrow the lane, define expected behavior, sample where judgment
matters. Cleaner than I wrote it.
"Don't pretend a fluent judge is objective" —that's the load-bearing wall of the article. Parts 1-3 ran six
experiments that made pretending impossible: lexical overlap missing 50% of semantic reversals, embedding unable to
separate synonym from antonym, the strongest model hitting 0% false-positives while rejecting 75% of valid work. The
LLM isn't objective at any tier —it just fails in a different direction.
Routing + diff review + SPC + sampling is the resulting design. But the honest boundary is the one you named: quality
control is partly a systems problem. The stress is on "partly." SPC catches format anomalies and misses semantic traps
—the residual doesn't disappear, it gets concentrated. Sampling is the admission that the residual is there.
So your framing points at the question the article leaves open: how should that residual be handled —fixed-rate
sampling, or adaptive routing driven by confidence signals? The principle (deterministic routing, not LLM judgment)
has held up. The sampling strategy is still being tested.
That is the right distinction: the model can be useful inside a narrowed lane, but it should not be allowed to become the thing that defines the lane and declares itself objective.
The sampling layer is what keeps the system honest over time. If deterministic checks handle the obvious cases, and human review samples the remaining judgment-heavy cases, the LLM becomes a routing aid instead of a final authority.
The part I like in your framing is that it treats reviewer attention as a finite system resource, not just a human preference.
"Routing aid instead of final authority" — that's where Part 4 lands, and you've said it better than I did. The pipeline I was critiquing in Parts 1–3 had the LLM as a per-requirement judge — "does this output satisfy REQ-N?" Part 4 retires that. Deterministic code routes, diff reviews catch the questionable ones, humans sample what's left. The LLM ends up as a classifier, never the verdict. The 75% wall behind the retire-don't-reroute call from earlier rounds is what I'm writing up next.
One catch on "deterministic checks handle the obvious cases" — that only holds when the obvious is addressable, and that subset turned out smaller than I expected. "Invalidate cache[K]" is addressable: runner writes K, watches cache[K], synonym-immune. "Invalidate the relevant cache entry" isn't — "relevant" is a qualifier, not a referent, so the runner has nothing to point at. The deterministic check has no floor, and the case falls to a human even though it looked easy. So: deterministic handles the addressable obvious, humans get the unaddressable obvious, the LLM routes between them. That unaddressable-obvious bucket — cases that look easy but aren't — is bigger than it looks. More on this in later posts.
That addressable vs unaddressable split is useful.
It explains why deterministic checks feel stronger in diagrams than in production. If the requirement names an object, a runner can verify it. If the requirement names intent, relevance, or sufficiency, the system needs a routing decision before it can even know what evidence would count.
That makes the LLM useful, but only as the classifier that sends the case to the right verification path.
The routing classifier framing is clean, but it has a catch that's worth naming: the routing decision itself is the new single point of failure. A router that reads "intent" or "relevance" from the requirement and decides which
verification path to dispatch to is making a semantic judgment — and that judgment is DPI-bound. If the input is
plausible-but-wrong (hallucinated requirement framing, subtle intent drift), the dispatch is wrong before any
deterministic check runs.
Sampling catches this because a human reviewer sees the full picture — original need, dispatch choice, output — in ⎿ Tip: /loop runs any prompt on a recurring schedule. Great for monitoring deploys, babysitting PRs, or polling
one pass. But the honest question this leaves open: can dispatch-confidence signals drive adaptive sampling rates,
or is fixed-rate sampling the only honest answer given that dispatch confidence is self-reported? The data I have so far says the router is useful; the data I don't have yet says whether we can measure when it's wrong.
That is the exact weak point. I would not let dispatch confidence come only from the same semantic layer that made the dispatch. At minimum, I would want a fixed baseline sampling rate plus external signals: route changed from last run, low agreement between cheap classifiers, unusual input shape, or a deterministic check that barely passed. The router can help prioritize review, but it should not be the only judge of whether review is needed.
Dispatch confidence should not come only from the same semantic layer that made the dispatch" — this pushes one step past "routing aid, not final
authority," because it means the routing layer itself needs constraints. The router cannot guarantee its own honesty.
Same architectural principle as "evaluator outside the loop" — just extended from the evaluator to the router. If the routing table can be rewritten by
the same agent whose output it dispatches, the evaluator's independence is moot: the agent can reassign its own output from a strict path to a lenient
one.
Later posts will develop this direction — the editable-surface constraint on the router, and the concrete effect of external signals.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.