Why a render loop
After an edit, an agent can read a source diff and believe everything
is fine. But the diff does not show the real effect: a changed shared
include, a common function, a query that empties a table,
a concurrent edit on the same page. On a legacy page with no test,
nobody sees those effects before production.
RenderLoop compares the rendered state before and after. It normalizes the HTML, masks declared noise, then diffs. The agent receives a compact summary and a unified diff. Anything that is not the intended change is a regression signal.
An agent often works within a narrow perimeter: one file, one component, one function. RenderLoop shows the rendered effect of its edit, including indirect consequences that fall outside that perimeter. With this feedback, the agent can identify a regression on a subject it never intended to touch — a shared include, a neighbouring block, a value rendered elsewhere — instead of discovering it in production.
The loop to follow
-
Discover before guessing.
render-loop -a capabilities -f jsonreturns every action, its required options, its JSON shape and the meaning of its exit codes.render-loop -a advise -f jsongives the modus operandi and pitfalls. -
Capture before touching the code.
render-loop -a capture -u <url> -t before. Without a “before” snapshot there is nothing to diff. - Edit, then serve the new file. Watch out for opcache: an immediate recheck can read the old version and show an empty diff.
-
Re-check.
render-loop -a recheck -u <url> -f json. The diff must contain only the intended edit. -
Declare the blast radius.
--expect-selector='.price'gives a binary verdict and exits non-zero if a change leaks outside the selector. -
Special cases.
JS/SPA page: push the HTML with
-a store. Fragment fed by an internal AJAX call: capture the endpoint with--ajax, or as POST with--method POST --data.
# 1. machine contract
$ render-loop -a capabilities -f json
# 2. before state
$ render-loop -a capture -u "http://localhost:8000/account" -t before
Snapshot saved: localhost_account_1792492262.7813
# 3. edit the code
# 4. verdict, with the scope declared upfront
$ render-loop -a recheck -u "http://localhost:8000/account" \
--expect-selector='.balance' -f json
{
"summary": { "lines_added": 1, "lines_removed": 1, "hunks": 1, "changes": [ … ] },
"assertion": { "satisfied": true, "inside_scope_changed": true, … },
"diff": "@@ -12,7 +12,7 @@"
}
exit 0 — nothing leaked outside .balance
A machine contract, not help text
capabilities describes the CLI itself. An agent does not
have to parse Symfony's help output or guess the options: it reads a
stable manifest.
{
"tool": "render-loop",
"formats": ["table", "json"],
"exit_codes": { … },
"actions": [
{
"name": "recheck",
"summary": "…",
"required": ["url"],
"optional": ["exit-code", "expect-selector", …],
"output": "…",
"exit_code": "…"
}
]
}
advise goes further: it returns the how, not just
the what. The same content is versioned in
AGENTS.md
for agents that read the repository before running anything. If the
two ever disagree, the CLI wins.
{
"audience": "AI coding agents",
"one_liner": "…",
"workflow": ["…"],
"reading_the_output": ["…"],
"pitfalls": ["…"]
}
Read the output without burning tokens
-
Read
summaryfirst.lines_added,lines_removed,hunks,tags_touchedandchanges. Unfolddiffonly when the summary is non-empty. -
Use
changesto understand. One entry per localized change:kind,selector,context,before,after. No need to parse the unified diff. -
Decide on the JSON, not the exit code.
recheckandcompareexit 0 even with a diff. To branch on status, pass--exit-code. -
Remember that
changesis best-effort. On messy markup, a structural insertion can empty the array. The unified diff is the reliable floor.
Declare the blast radius
The classic agent trap is to see an unexpected change and rationalize
it after the fact. --expect-selector reverses the order:
intent is declared before seeing the diff.
render-loop -a recheck -u "http://localhost/page.php" \
--expect-selector='.price' -f json
The option is repeatable. The assertion block isolates the
leak in out_of_scope, and the exit code is non-zero when
satisfied is false. This is a per-edit guard, not a frozen
regression net: for that, use golden master.
satisfied: false with the reason “matched no element”.
satisfied: true does not prove the edit is correct, only
that no visible change leaked.
Pitfalls to avoid
- Selector-only masking. Never a content regex: a price at
0.00must stay visible. - Project config. Point
-cat a versioned file in the project, not at the read-only one invendor/. - opcache. Check that the server serves the new file before concluding there is no diff.
- Session cookie. Pass the value through
RENDERLOOP_COOKIE, never as an argument:psis public. - Allowed hosts. Capture is bounded by an allow-list and does not follow redirects. Keep that list tight.
- Scope. Rendering only: no database, no export, no PDF, no API. A critical computation deserves a unit test.