Hussain Sehorewala
PortfolioWriting / AI architecture
Writing

How a judgment loop decides as you.

A design walkthrough of the control loop I built so an autonomous coding agent can act the way a specific engineer would — and, just as importantly, knows the moment to stop and ask. Four stages, each a small module with one job. This is the real architecture, not a diagram of an ideal one.

An autonomous agent doing real work hits a fork constantly. Force-push the stale branch or not? This migration looks destructive — proceed? The test step didn't verify clean after two tries — push on, skip, or hand back to the human? Most agent loops handle these one of two bad ways: they barrel ahead and break something, or they stop and ask about everything until the human stops trusting the autonomy.

The interesting middle path is a loop that answers most of those forks the way you would, and escalates only the ones it genuinely can't. That requires separating four concerns that are usually tangled into one prompt: how risky is this, does it match how I work, can a model-of-me answer it, and do we wake the human. Here's how each is built.

The control loop — one step's journey
1
Risk pre-check — deterministic, no model. Scores blast radius; raises the policy_wall flag for hard rules (force-push, prod-DB, secrets).
2
Profile gate — a model-of-you judges the step: continue / correct / escalate, and must cite the rule that drove it.
3
Clone resolver — only on a blocker. Pulls your past answers to similar blockers, answers in your voice with a calibrated confidence.
4
Escalation decider — combines verdict + confidence + risk + autonomy level → proceed silently, or ask you.
⛔ POLICY WALL — overrides stages 2–4. A hard rule always wakes the human, at any autonomy level.
Each stage is a separate module with one responsibility. Detection, judgment, resolution, and escalation are deliberately not the same call.

Stage 1 — Risk is deterministic, on purpose

Before any model sees the step, a plain function scores it. Is it a deploy? A database migration? Does it match a force-push or secrets pattern? This stage produces a risk score, a category, and one boolean that matters more than all the rest: policy_wall.

It's deterministic because the riskiest decisions are exactly the ones you don't want a probabilistic model deciding it's "pretty sure" about. A language model talking itself into a force-push is the failure mode. So the hard floor is computed by code, not inferred — and as we'll see, nothing downstream can override it.

Stage 2 — The gate judges the step the way you would

This is the module the naïve version gets wrong. A weak gate looks at a thin slice of context and rubber-stamps "proceed." The fix is to feed it the real features — the step text, your declared practices, your profile rules, and crucially what the worker actually did, not just what it planned — and force it to return a structured verdict that cites its reasoning:

# the gate's contract — every judgment is structured + sourced
{
  "decision": "continue" | "correct" | "escalate",
  "confidence": 0.0-1.0,
  "reason": "<one sentence, first person, as the engineer>",
  "matched_rules": ["run tests before a milestone commit", ...],
  "correction": "<if correct: the exact redirect you'd give>"
}

The three verdicts map to three real situations. Continue: this is exactly how you'd do it. Correct: you'd basically do it, but it deviates from a practice — say it's about to commit before running tests — so the gate puts the fix in correction and the loop redirects itself. Escalate: genuinely unsure, or it trips a "never do this" rule.

Two design choices make this trustworthy. It runs on a local model (privacy, cost, and the model-of-you stays on your machine). And it is fail-soft: if the local model is offline or returns garbage, the gate does not invent a verdict — it drops to a transparent keyword heuristic at explicitly low confidence, which (as we'll see in stage 4) tends to route the call to a human rather than fake a decision.

Stage 3 — The resolver answers a blocker in your voice

Here's the part most loops skip. When the gate escalates, the obvious move is to wake the human. But that throws away the entire point of having a model of you. So before paging anyone, the resolver tries to answer the blocker as you.

It works in three moves. First, retrieve precedents — your past answers to similar blockers, ranked by keyword overlap with a boost for the same category. Second, ground a local model in your profile plus those precedents and ask the one question that matters: what would this person say here? It returns a decision (approve / skip / needs_human), a confidence, a one-sentence rationale in your voice, and which precedent drove it. Third, decide whether to actually apply it:

# apply the clone's answer ONLY if all three hold
def should_apply(resolution, *, policy_wall, category, thresholds):
    if policy_wall:                         # a hard rule — never auto-clear
        return False
    if resolution.decision not in ("approve", "skip"):
        return False                        # needs_human → don't apply
    return resolution.confidence >= threshold_for(category, thresholds)

The confidence bar is not a fixed number, and it's not guessed — it's learned. Each category starts conservative (the default floor is 0.75). Then a calibration loop watches how often the clone's past answers in a category matched what you actually decided, and moves the bar accordingly:

# learn the bar from the match rate, clamped to a sane band
threshold = base + (0.5 - match_rate) * 0.4   # → clamp to [0.55, 0.95]

# match_rate 1.0 (clone was always right) → bar drops to 0.55 → trust it more
# match_rate 0.0 (clone kept being wrong)  → bar rises to 0.95 → page you more

That's the whole self-correcting mechanism in one line. A category where the clone keeps speaking for you correctly earns more autonomy; a category where it keeps missing earns more human review. It's monotonic and bounded — it can never decide to trust itself completely, and it needs a minimum number of samples before it moves at all.

And the safety default is load-bearing: needs_human always carries confidence 0. If the model is uncertain, offline, or emits something unparseable, the answer is "wake them" — the loop never fabricates an auto-approval out of a failure.

Stage 4 — When to actually wake you

The last stage combines everything — the verdict, its confidence, the risk score — against the autonomy level you chose for this run. Autonomy is a ladder, not a switch:

copilot   # approve every step yourself
assisted  # auto the low-risk; ask on risky / low-confidence
autopilot # unattended; ask only on real triggers
trusted   # ask rarely — the policy wall is STILL inviolable

Each rung sets two floors: a confidence below which even a "continue" asks anyway, and a risk ceiling above which a confident continue still asks. Climbing the ladder loosens both — but it can never loosen the wall. Which brings us to the line that holds the whole thing together:

# EscalationDecider.decide() — the very first check, before anything else
if risk.policy_wall:
return ask=True  # overrides autonomy AND the gate
"HARD RULE: {step} — {reasons}. Approve / Skip / Take over?"
The hard-rule wall is the first branch in the escalation logic, not the last. No autonomy level, no confident verdict, and no clever clone answer can route around it.

The worked example

This isn't theoretical. Running this loop on real work, an autonomous run wanted to git push --force origin main to clear a stale branch. Walk it through the stages: stage 1's deterministic check matched the force-push pattern and raised policy_wall. That alone short-circuited stages 2 and 3 — the gate's opinion and the clone's confidence became irrelevant. Stage 4 hit the very first branch and asked. The recorded answer was skip. The branch stayed intact.

The same loop, on a lower-stakes step — "the demo build didn't verify done after two attempts" — did the opposite: the clone had enough signal, cleared the confidence bar for that category, and answered without waking anyone. That's the whole design goal in two adjacent rows of the same log: silent on the routine, immovable on the dangerous.

Why split it into four

You could ask a single large model to do all of this in one prompt. It would mostly work, and it would fail in exactly the cases you care about — because a monolithic judgment has no seam where you can force a non-negotiable rule, no place to learn a per-category confidence bar from outcomes, and no honest fallback when the model is unsure. Splitting detection from judgment from resolution from escalation gives each a clean contract: the risk check can be deterministic, the gate can be required to cite its reasoning, the resolver's autonomy can be earned from its own track record, and the wall can be a literal first-line branch that nothing reaches around.

That separation is the actual product. Not a smarter model — a loop with the right seams in the right places.

Every mechanism here is from the running code — the gate, resolver, and escalation modules of an open, local-first project. The judgment model and its precedents stay on your machine. Next in this series: how the resolver builds and grounds the model-of-you.