In the last article, I wrote about a release story that was weaker than the engine underneath it.
This time the mistake was worse: I was rewarding the wrong behavior in the product itself. I built a planning agent and kept judging it by the one output that looks best in a demo: an approved plan. Then the field test hit me with the number I did not want to explain.
96 of 97 strict goals escalated. At first glance, that sounds like an agent that failed almost every hard task. I think the opposite is closer to the truth. For a high-stakes planning system, a confident plan with one hidden gap is often more dangerous than a clean refusal with one precise question. That was the real product lesson in PlannerCritic: the safest thing an agent can do is not always produce a plan. Sometimes it is to stop, point at the blocker, and force a human decision before state changes.
The Metric That Looked Like Failure
Here is the metric that should have made me nervous:
- in
v0.2.1, 96 of 97 strict goals escalated - in
v0.2.2, the inherited strict result held at 96 of 97 - across the inherited top-level contract, 73 of 73 balanced goals approved and 8 of 8 inherited adversarial goals blocked
If you judge an agent by approval rate, that number looks terrible. The system said no almost every time the goal got hard. That sounds like failure until you ask a better question:
What if a large share of those 96 approvals would have been worse than the refusals?
That was the product lesson I did not want at first.
An agent that gives you a plausible answer when it cannot justify the answer is not being helpful. It is handing you uncertainty in a form that feels executable.
That is one of the most dangerous outputs a planning system can produce.
The Most Dangerous Output Is a Plausible Plan
The bad version of an agent planner is familiar because most of us have already seen it. It produces a plan that looks coherent. It has steps. It has sequencing. It has enough domain vocabulary to feel competent. And somewhere in the middle there is one quiet sentence doing all the damage:
"Assumes the rollback artifact already exists."
Or:
"Requires the migration window to remain within prior latency bounds."
Or:
"This should work if the dependency graph is complete."
That kind of caveat is not safety. It is a confession disguised as caution. The agent is telling you it does not know whether the plan is safe. Then it hides that uncertainty inside prose and hopes the human notices before the executor does.
I built PlannerCritic specifically to avoid that shape of failure. The repo is public, the release notes are public, and the control surfaces I am talking about live in real files you can inspect instead of in architecture-slide fiction.
The engine's job is not to produce the maximum number of plans. Its job is to produce one of three trustworthy terminal states:
- approved
- escalated
- blocked
That sounds obvious when you write it down. It was much less obvious when I was staring at approval rates and trying to tell a clean success story.
Why the Refusal Was Often the Right Output
The strict-goal corpus exists for one reason: to pressure the system exactly where guessing would be expensive. These are not goals where "best effort" is charming. These are goals where a missing precondition, weak rollback story, or unresolved authority boundary should stop the loop.
So when 96 of 97 strict goals escalated, there were two possible interpretations.
- The engine was too weak to finish hard work.
- The engine was strong enough to refuse hard work it could not justify.
The answer turned out to be: sometimes both, but much more often the second one than I expected.
That distinction matters because a refusal is not useful by default. A refusal becomes useful only when it shrinks uncertainty into a precise next decision.
Not "human review required."
Not "please inspect this plan."
Not "additional context needed."
Those are just dead ends with better manners.
The useful refusal is the one that says:
I cannot approve this because blocker X is unresolved, and the one thing I need from a human is answer Y.
That is not a fallback path. That is a product surface.
In the repo, that surface is visible in the actual types and control code:
-
src/planner_critic/types.pydefines theEscalationcontract -
src/planner_critic/escalation.pyenforces how escalations are created and resolved -
src/planner_critic/loop/_controller.pyis where the loop decides whether to approve, revise, or escalate
Human-in-the-Loop Is Fake Until the Wiring Is Real
This is the part I think a lot of agent tooling still gets wrong.
It is very easy to draw a human-in-the-loop box on an architecture diagram. It is much harder to make the human authority path real in the shipped system.
In v0.2.1, PlannerCritic had an uncomfortable gap: the approving_authority rule was proven in tests, but not actually reachable from the shipped CLI, HTTP, or MCP surfaces. That was documented as failure mode F-14 and tracked as #238.
That meant the engine could describe the right control model more rigorously than it could enforce it.
I think this is a common failure mode in agent infrastructure. We validate the principle. We forget the surface. We prove the guardrail in unit tests. We do not notice the shipped entry points route around it.
That is not an implementation detail. That is the difference between a real refusal and a decorative one.
v0.2.2 tightened that path in exactly the unglamorous ways it needed:
-
approving_authoritywas wired through CLI, HTTP, and MCP surfaces (#238) - the field-test harness stopped auto-approving escalations as a hidden convenience (#253)
- the escalation audit trail started persisting who resolved what via
resolved_by(#261)
None of those features make for a flashy launch screenshot. All of them make the refusal trustworthy.
A Bad Refusal Spends Human Attention
What finally changed my mind was not philosophy. It was operator burden.
By v0.2.2, the operational benchmark on 181 traces showed:
- approved p50 latency: 24.69s
- escalated p50 latency: 45.97s
- mean blockers per goal: 2.92
- mean advisories per goal: 2.74
- decisions per 100 goals: 55.2
Those numbers are not just system stats. They are a human-attention bill.
Every escalation consumes reviewer time.
Every blocker is a question someone has to parse.
Every vague refusal pushes diagnosis work onto the operator instead of the engine.
That is why the real quality bar is not "did the system refuse?" It is:
Did the system refuse in a way that made the next human decision faster, smaller, and safer?
This is the same lesson I keep running into with AI systems more broadly.
If the machine cannot finish the task, it still has one job left: reduce the cost of human recovery.
The Product Does Not End at Refusal
There is a mirror image to the refusal problem. Refusal helps before execution. Diagnosis helps after execution.
When an approved plan fails, the user's question changes from:
"Why didn't the engine approve?"
to:
"What actually broke, and what do I fix first?"
That is why plancritic diagnose matters.
And here the design instinct had to be almost anti-AI.
The diagnosis path is valuable precisely because it is deterministic. It is a rule matcher, not a storyteller. It maps a trace to a failure category, root cause, and suggested fix. If it cannot classify the failure, it says unclassified instead of improvising.
The user-facing surface for that work is plancritic diagnose, and the design is documented in docs/design/developer-surfaces-design.md.
I think that matters more than most builders admit.
A post-failure tool that hallucinates a clean explanation is often worse than no explanation at all. At least raw traces do not pretend to know.
So the architecture lesson on both sides ended up being the same:
- before execution: do not guess, escalate precisely
- after execution: do not narrate, diagnose deterministically
That pair is much closer to the actual product than "agent writes plans."
The Metric I Think Many Builders Still Get Wrong
The default success metric for an agent planner is still something like:
- approval rate
- completion rate
- success rate
- convergence rate
Those are not useless metrics. They are incomplete in exactly the wrong direction.
Because they quietly assume the right outcome is more output.
I do not think that assumption survives serious systems for very long.
For high-stakes agent work, I increasingly think the better question is:
When the system should not proceed, how well does it stop?
Not whether it stopped.
How well.
Did it identify the blocker correctly?
Did it route to the right authority?
Did it ask one precise question instead of five vague ones?
Did it preserve enough structure that the human could act without re-reading the entire trace?
Did the resolution meaningfully reduce downstream failure, or did it just create a ceremony around uncertainty?
Those are product metrics too. They just do not look as flattering in a launch post.
The Uncomfortable Part
I do not want to romanticize refusal. A refusal can still hide a weak planner. Some escalations are genuinely the right safety outcome. Some escalations are just the planner surfacing its own structural limitation in a nicer format.
That was one of the hardest lessons from the earlier field tests too. Sometimes the system is correctly cautious. Sometimes it is underpowered. Sometimes it is both in the same run.
So I do not think the right takeaway is "more refusals = better agent."
I think the right takeaway is harsher:
a good agent must earn both its yes and its no.
Its approvals have to be justified.
Its refusals have to be actionable.
Its diagnoses have to be honest.
And its control surfaces have to be real, not merely described.
The Real Product Was the Boundary, Not the Plan
I started this engine thinking the approved plan was the hero artifact. Now I think that was too small a frame. The approved plan matters. But the actual product is the decision boundary around the plan:
- approve when the case is strong
- refuse when the case is weak
- block when the request is adversarial
- diagnose when execution disproves the plan anyway
That is the thing I trust more now. Not that the system produces plans. That it knows when not to.
The Question I Want Builders To Fight Me On
If you build agents, this is the question I think matters more than most benchmark charts:
In your system, what counts as a successful refusal?
Not a vague one. Not a safety-branded dead end. A refusal that genuinely lowers the cost of the next human decision.
Because I think a lot of us are still rewarding our agents for having an answer when we should be rewarding them for knowing exactly when they shouldn't.
I would push on three things if you are building in this space:
- Do your metrics treat a correct refusal as success, or do they quietly classify it as a non-answer?
- If your agent refuses, can it point to the exact blocker and the exact authority needed to unblock it?
- When your agent says yes, how do you know it was earned instead of merely plausible?
I think most agent products still have a stronger story for producing plans than for justifying them. That is fine in a demo. It is dangerous in a system somebody might actually trust.
Previous PlannerCritic articles
- A Reader Audited My OSS Release in Public
- Article 1: I Ran 157 Agent Plans Against a Real LLM
- Article 2: I Told My LLM Critic to Be Adversarial
- Article 3: The Planner Made the Same 3 Mistakes Every Time
- Article 4: I Ran 170 Agent Goals for $0.49
- Article 5: I Tried to Prompt-Inject My Own Agent Engine
Links
- Repo: https://github.com/deghosal-2026/planner-critic-engine
- Escalation type: https://github.com/deghosal-2026/planner-critic-engine/blob/main/src/planner_critic/types.py
- Escalation manager: https://github.com/deghosal-2026/planner-critic-engine/blob/main/src/planner_critic/escalation.py
- Loop controller: https://github.com/deghosal-2026/planner-critic-engine/blob/main/src/planner_critic/loop/_controller.py
- Diagnose surface: https://github.com/deghosal-2026/planner-critic-engine/blob/main/docs/reference/api.md
- Developer surfaces design: https://github.com/deghosal-2026/planner-critic-engine/blob/main/docs/design/developer-surfaces-design.md
- v0.2.2 field-test report: https://github.com/deghosal-2026/planner-critic-engine/blob/main/docs/field-test/v0.2.2/field-test-results-0.2.2.md
- v0.2.2 release notes: https://github.com/deghosal-2026/planner-critic-engine/blob/main/docs/reference/release-notes-v0.2.2.md
- Issue trail: #238, #253, #261
Next in the sequence: My LLM Critic Disagreed With Itself on Every Trial. The Safe Part Was the Code I Didn’t Trust It to Touch.
Top comments (5)
This is very close to a problem I've been working on from a slightly different direction.
The part that stands out to me is freezing the claim before looking at the evidence.
Once the claim is fixed, the question changes from “does the system look correct?” to something much stricter: “does the available evidence actually support this specific claim?”
We've found that this distinction matters enormously. Code can be correct while documentation is wrong; tests can pass while the operational claim is still unsupported; and the person who built the system is often the least independent person to verify the story around it.
Your public audit is a very good example of why claim verification should probably be treated as a separate engineering discipline rather than just another testing step.
Really interesting work.
Esto está muy cerca de un problema en el que llevo trabajando desde una dirección ligeramente distinta.
La parte que más me llama la atención es congelar el claim antes de mirar la evidencia.
Una vez fijado el claim, la pregunta deja de ser “¿parece correcto el sistema?” y pasa a ser algo mucho más estricto: “¿la evidencia disponible respalda realmente esta afirmación concreta?”
Hemos comprobado que esta distinción importa muchísimo. El código puede ser correcto mientras la documentación está equivocada; los tests pueden pasar mientras el claim operativo sigue sin estar respaldado; y la persona que construyó el sistema suele ser precisamente la menos independiente para verificar la historia que se cuenta sobre él.
Tu auditoría pública es un ejemplo muy bueno de por qué la verificación de claims probablemente debería tratarse como una disciplina de ingeniería separada y no simplemente como otro paso más de testing.
Trabajo muy interesante.
This is a great example of why “all tests passed” and “the application works” need to be treated as two different claims.
The most important finding here is the gap between code-level correctness and runtime usability. 2,588 visible assertions passing and a successful production build provide strong evidence for the implementation, but neither validates the critical path a real user follows: configure environment → start server → initialize Maps/Cesium/WebGL → load external data → interact with the globe.
The Google Maps dependency is particularly important. If src/main.js throws during initialization when GOOGLE_MAPS_API_KEY is absent, then the application has effectively made a paid external service a hard startup dependency. I’d strongly consider changing that architecture so the application can boot into a degraded/free mode first, with the premium/photorealistic layer enabled only when the required credentials are available.
Your suggestion of an OSM-based first-run mode is therefore more than a UX improvement it creates a much better testability boundary. The core globe, layer management, focus allocation, state handling, and interaction model could be validated independently from third-party billing and API availability.
I also think the hanging test process deserves attention. If the assertions complete but Node remains alive for 90+ seconds, identifying the open handle would be valuable. Running Node with diagnostic tooling or explicitly inspecting active handles/resources could distinguish an HTTP connection, timer, worker, browser process, or other resource leak from test-runner behavior.
And the Puppeteer failure highlights another important distinction: environment failure ≠ application failure, but it still means the release hasn't received end-to-end validation. A CI matrix with a real browser/WebGL-capable runner would make that boundary much clearer.
Overall, I appreciate the methodology here: separating installation, security checks, assertions, build validation, startup behavior, and browser QA instead of collapsing everything into a binary pass/fail.
I work with a small Canada-based remote development team, and we're particularly interested in long-term collaboration around AI-assisted development, testing, automation, and complex web applications
I would like to get to know you better. Would you please contact me? telegram@CRDT_CTO
F-14 is the more interesting finding here, not the escalation count. I hit the same shape in my own agent permissions: 198 allow entries, zero deny, and 53 of those exist only because of how folders happen to be named. The rule I thought I wrote was about what the agent may do; what actually gated it was a path string, and only counting showed me that. Worth checking whether CLI,
HTTP and MCP each construct the approving_authority check, or all call one function that owns it.
Thanks for reporting - Issue #297 filed: [F-14 regression] approving_authority enforcement still dormant on all shipped surfaces — MCP server also drops principal. The #261 fix was incomplete: it plumbed principal forwarding, but no surface binds approving_authority from the stored AcceptanceContract, so the PermissionError gate is always short-circuited. The MCP server wrapper (mcp.py) also drops principal entirely — tool schemas and handlers lack the parameter.