You Can Measure a Request Before You Send It — Sizing Agent Tasks by Working Backward from Rework Rate
When an Antigravity agent returns code that misses the mark, the cause is rarely the wording of the prompt. It is the size of the task. Here is a Python scorer that grades a request before you send it, plus what happened when I scored 80 past requests against their actual rework outcomes.
On a Thursday evening I rewrote the same request three times. Working as an indie developer, the time lost in a stall comes straight out of my own pocket. I asked Antigravity to rework an article listing page, looked at the diff, and rolled back to a checkpoint. I polished the wording and rolled back again. By the time the third attempt landed, I had spent longer than writing it myself would have taken.
The next morning I dumped my recent request log to CSV and read through it. Every request I had rolled back shared something — and it had nothing to do with how carefully the prompt was written. Every rolled-back request was heavy in either the number of files it touched or the number of decisions it contained.
Those are not matters of taste. They are quantities you can count before you press send. And if you can count them before you send, you can judge them before you send. This is the record of building that judge and running it against 80 of my own requests.
One metric: rework rate
"Output quality" is slippery. If a human grades it on a five-point scale, the score drifts with how tired that human is. So I narrowed it to a single binary.
Rework rate = the share of requests that were not accepted on the first pass, requiring a rollback, a substantial rewrite, or a course correction through follow-up instructions.
Recording it is trivial: label each request accepted or reworked. If you use Antigravity's checkpoints, the presence of a rollback is the label. I pulled mine from editor session logs, but a handwritten note works. What matters is that the label is binary. Binary labels correlate; five-point impressions do not.
Three weeks of requests came to 80. Overall rework rate: 41%. I had been redoing nearly half of everything I asked for. Seeing that number for the first time sat heavily in my chest.
Scoring a request before it is sent
The scorer reads the request text. Its only job is to stop oversized requests from leaving the keyboard, so it does no real parsing. It counts six factors.
Past six files, later edits stop agreeing with earlier ones
Open decisions
"whatever makes sense", "the best approach", "you decide"
×3.0
The single strongest predictor of rework
Implementation plus tests
"and write tests", "with tests"
×2.5
Different modes of thought; one of them always gets sloppy
Bundled asks
"while you're at it", "also", "at the same time"
×2.0
Bundling hides added decisions
Standing rules inline
"never use any", "comments in Japanese"
×1.0
Pulls attention away from the actual ask
Non-self-contained work
"wire it up later", "for now", "just a stub"
×1.5
Intermediate artifacts create reconciliation work downstream
The weights are not invented. I started every factor at 1.0, ran a logistic regression over the 80 labeled requests, kept only the ordering of the coefficients, and rounded to numbers I could hold in my head. Eighty samples do not justify precise coefficients. Preserving the order is enough.
# preflight_score.py — grade a request before it reaches the agentimport refrom dataclasses import dataclassDECISION_MARKERS = [ "whatever makes sense", "the best approach", "you decide", "as appropriate", "figure out", "up to you", "however you like",]TEST_MARKERS = ["and write tests", "with tests", "plus tests", "include tests"]BUNDLE_MARKERS = ["while you're at it", "also", "at the same time", "and then"]POLICY_MARKERS = ["never use any", "comments in Japanese", "naming convention", "formatter"]DEFERRAL_MARKERS = ["wire it up later", "for now", "just a stub", "temporarily"]FILE_PAT = re.compile(r"[\w./-]+\.(tsx?|jsx?|py|go|rs|mdx?|css)\b")COMPONENT_PAT = re.compile(r"\b[A-Z][a-zA-Z0-9]{2,}\b")WEIGHTS = { "files": 2.0, "decisions": 3.0, "tests": 2.5, "bundles": 2.0, "policies": 1.0, "deferrals": 1.5,}@dataclassclass Score: total: float factors: dict @property def band(self) -> str: if self.total < 6: return "GREEN" if self.total < 12: return "AMBER" return "RED"def _count(text: str, markers: list[str]) -> int: lowered = text.lower() return sum(lowered.count(m.lower()) for m in markers)def score_request(text: str) -> Score: files = len(set(FILE_PAT.findall(text))) + len(set(COMPONENT_PAT.findall(text))) factors = { # Up to three files is harmless. Penalize only the excess. "files": max(0, files - 3), "decisions": _count(text, DECISION_MARKERS), "tests": _count(text, TEST_MARKERS), "bundles": _count(text, BUNDLE_MARKERS), "policies": _count(text, POLICY_MARKERS), "deferrals": _count(text, DEFERRAL_MARKERS), } total = sum(WEIGHTS[k] * v for k, v in factors.items()) return Score(total=total, factors=factors)if __name__ == "__main__": import sys s = score_request(sys.stdin.read()) print(f"score={s.total:.1f} band={s.band}") for k, v in s.factors.items(): if v: print(f" {k}: {v} (+{WEIGHTS[k] * v:.1f})") sys.exit(0 if s.band != "RED" else 1)
Penalizing only files beyond the third is a small implementation choice that turned out to matter. Scoring in direct proportion to file count put weight on tiny single-file requests and blurred the threshold. Flattening the harmless region to zero lets the threshold sit exactly where the danger starts.
The decision factor carries the heaviest weight because the data said so. "Add authentication" folds three judgments into four words: how sessions are managed, where the token lives, what happens on failure. The agent will not ask. It will quietly pick the most common option for each. When that choice collides with how the existing codebase does things, the diff breaks silently.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A preflight scorer that grades a request before it reaches the agent, built on lightweight heuristics rather than parsing
✦Retrospective scoring of 80 real requests, with first-pass acceptance measured per score band
✦Three axes for splitting a task that actually lower the score, and which instructions belong in AGENTS.md instead
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
I fed the archived request text straight into the scorer and joined it against the recorded outcomes.
# requests.jsonl: {"text": "...", "outcome": "accepted"|"reworked"}python3 - <<'PY'import json, collectionsfrom preflight_score import score_requestbands = collections.defaultdict(lambda: {"accepted": 0, "reworked": 0})for line in open("requests.jsonl"): r = json.loads(line) bands[score_request(r["text"]).band][r["outcome"]] += 1for band in ("GREEN", "AMBER", "RED"): a, w = bands[band]["accepted"], bands[band]["reworked"] n = a + w if n: print(f"{band:6} n={n:3d} rework={w / n:.0%}")PY
Here is what came back.
Band
Requests
Accepted first pass
Rework rate
GREEN (< 6.0)
34
30
12%
AMBER (6.0–11.9)
27
16
41%
RED (≥ 12.0)
19
1
95%
Exactly one of the nineteen RED requests landed on the first pass. It was "bump every dependency to the latest minor" — a mechanical sweep across many files with essentially zero decisions in it. The scorer saw the file count and flagged RED. It passed anyway, because there was nothing to decide.
That single outlier is the honest limit of the metric. File count is a proxy for decision count. Where the proxy diverges from the thing it stands for, the score is wrong. In practice I added a --mechanical flag that zeroes the file factor for sweeps like this. A threshold with no escape hatch gets ignored, and an ignored threshold is the same as no threshold at all.
One more pitfall showed up only in production use. I put the scorer in CI to check my request templates, and the variable placeholders inside those templates were counted as component names, pinning every template at RED. Renaming placeholders to lowercase snake_case worked around it. The heuristics assume prose written for a human reader — an obvious constraint I had stopped seeing.
Three axes that actually lower the score
When a request comes back AMBER or RED, shortening it at random accomplishes nothing. Each factor responds to a different cut.
Axis 1 — pull the decisions forward
If decisions is carrying the score, split the design question out and ask it alone: "Given the existing Worker setup, should sessions live in a cookie or in KV?" Adopt the answer, then ask for the implementation. In my log, requests that went through this one extra step dropped from 41% rework to 15%. The design question takes five minutes. The rewrite takes an hour.
Axis 2 — pull the dependents in
When deferrals is carrying the score, splitting makes things worse. "Make this return a Promise" leaves every caller broken. Asking for the return type change and the async/await conversion at three call sites touches more files but scores lower and lands cleanly. The rule I refuse to bend: each task must end in a working state.
Axis 3 — push the tests behind
I separate tests unconditionally. Once the implementation diff is settled, "write tests for this diff" produces better coverage. Tests written alongside an implementation tend to freeze that implementation's mistakes in place.
Standing rules belong in AGENTS.md
A request carrying the policies factor does not have a granularity problem. It has a channel problem. "Never use any" and "comments in Japanese" belong in AGENTS.md, not in every prompt.
There is a practical trick here. Writing a rule down does not make it hold, so have the checker read the rule from the same file the agent reads.
# agents_rules.py — make AGENTS.md conventions readable from CI tooimport re, pathlibdef load_rules(path="AGENTS.md") -> dict[str, str]: """Extract only bullets shaped like `- rule_id: description`.""" text = pathlib.Path(path).read_text(encoding="utf-8") return dict(re.findall(r"^-\s+([a-z0-9_]+):\s*(.+)$", text, re.M))def assert_known(rule_id: str) -> str: rules = load_rules() if rule_id not in rules: raise KeyError(f"{rule_id} is not defined in AGENTS.md") return rules[rule_id]
One source of truth, referenced by the same ID from prompts and from CI. Now the failure mode "it is written in AGENTS.md but nobody reads it" surfaces as a red build. I started with three rules — no_any, ja_comments, test_framework — and three turned out to be enough. Every rule you add past that is a rule that quietly stops being followed.
Enforcing it at send time
A scorer you remember to run is a scorer you stop running. I wrapped it in a shell function that interrupts only on RED.
# ~/.zshrcag-ask() { local body body="$(cat)" # read the request from stdin if ! printf '%s' "$body" | python3 ~/bin/preflight_score.py; then printf '\nThis is RED. Split it before sending? [y/N] ' >&2 read -r ans [ "$ans" = "y" ] || return 1 fi printf '%s' "$body" | pbcopy echo "-> copied to clipboard" >&2}
It does not block. It pauses. Asked once whether I really mean to send this, I split the request myself about half the time. A hard block would have me hunting for a way around it. Keep the friction small and unavoidable — that balance is the whole reason the habit survived.
Three weeks in, overall rework rate had fallen from 41% to 19%. The scorer did not get smarter. I simply stopped sending RED. What the metric changed was not the agent. It was my hand on the keyboard.
What three weeks of running it revealed
I went back and read the low-scoring requests that were reworked anyway. They shared one thing: the information lived outside the request text. A request beginning "following on from what we discussed yesterday" scores a harmless 2.0. The agent has no yesterday.
The scorer can only see inside the request. The moment a request depends on something outside itself, the metric goes silent. The only remedy I found was to spell the continuation out explicitly, tedious as that is. I am still deciding whether to add a seventh factor next to deferrals that counts implicit context references — "yesterday", "earlier", "that thing" — which would be one more heuristic on a pile of heuristics.
There is a second gap. Some of the requests that scored low and passed first time were too small to have handed to an agent at all. Changing if (x > 0) to if (x >= 0) scores GREEN, passes, and would have been faster to type myself. Rework rate does not detect waste at the lower bound. A metric illuminates one side only — an obvious thing I had to implement before I understood.
Until I started measuring, I believed I was bad at writing prompts. The wording was never the problem. The problem was how much I asked for at once, which is a quantity you can count before you send. What you can count, you can reduce. Start with what you can count.
Label twenty of your own requests as accepted or reworked and run them through the scorer above. If the correlation shows up, you can set the thresholds from your own numbers rather than mine.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.