AI agents

AI agents: the signal the agent reads after editing.

An autonomous agent does not need a screenshot. It needs a short, reliable answer to one precise question: “did my edit do exactly what it had to, and nothing else?”.

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

  1. Discover before guessing. render-loop -a capabilities -f json returns every action, its required options, its JSON shape and the meaning of its exit codes. render-loop -a advise -f json gives the modus operandi and pitfalls.
  2. Capture before touching the code. render-loop -a capture -u <url> -t before. Without a “before” snapshot there is nothing to diff.
  3. Edit, then serve the new file. Watch out for opcache: an immediate recheck can read the old version and show an empty diff.
  4. Re-check. render-loop -a recheck -u <url> -f json. The diff must contain only the intended edit.
  5. Declare the blast radius. --expect-selector='.price' gives a binary verdict and exits non-zero if a change leaks outside the selector.
  6. 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.
agent loop
# 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 summary first. lines_added, lines_removed, hunks, tags_touched and changes. Unfold diff only when the summary is non-empty.
  • Use changes to 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. recheck and compare exit 0 even with a diff. To branch on status, pass --exit-code.
  • Remember that changes is 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.

No false pass. A wrong or missing selector returns 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.00 must stay visible.
  • Project config. Point -c at a versioned file in the project, not at the read-only one in vendor/.
  • 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: ps is 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.