Skip to main content

The real validation — remote GitHub and a real model

The Phase 12 validation states its own limits in full: LocalGitProvider, NoopDevAgent, a gate verdict written by the script itself. It exists to prove the chain without depending on network or model judgment, and it does that well.

This one swaps out exactly the three things that one left out — remote GitHub, a real dev agent, an API model — and the result is what you'd expect from an honest dogfooding run: the cheap half passed entirely, and the expensive half failed in a way no test had shown.

The script is pnpm --filter api validacao:real -- --repo <owner/repo>.

What passed, and is more than it looks​

Everything below ran against daneiel/test on real GitHub, and without spending a cent — it's the --ate backlog phase.

whatresult
remote adoption (real getRepo)origin: adopted, default branch main
dry-run plan6 mutations, 6 diagnostics, null decision
repository untouched until the decisionverified via the API: zero branches, zero content
re-adoption converges6 mutations on the 1st pass, 3 on the 2nd (RN-046)
single story becomes draft + proposedyes, no automatic promotion
claimNext before promotionnull — nothing claimable
promotionrecorded with the user as the actor

RN-045 stops being proven only against a local bare repo: the remote repository was checked after adoption and was literally empty.

Finding D actually happened​

The bootstrap stopped at protect_branches with Upgrade to GitHub Pro or make this repository public — a private repository on the free plan. It's exactly the scenario Finding D documented, now observed outside a test.

And RN-078's premise was confirmed: checking the repository right after, dev, main, and qa existed and the files were committed. It's the last step, and the only one whose failure leaves a usable repository. The script recognizes the failure and moves on, which is the intended outcome.

What the expensive execution showed​

Model openai/gpt-5-mini via OpenRouter, real dev agent, one story with one task: "Expose GET /greeting".

The task was blocked by iteration limit reached, with origin modelo. No PR was opened, and so no gate ever got to judge.

The recorded diagnosis is uncomfortably clear: (no terminal run).

The measurement​

Extracted by pnpm --filter api medir:execucao, never recorded by hand:

window1m36s
sessions · events3 · 59
engine restart in betweenno
gate round-tripsnone (no PR happened)
silent turnsnone
user interventionsnone
agentcallsinoutcostmodel
dev-api85,671205< US$ 0.01openai/gpt-5-mini
anamnese16,779967< US$ 0.01deepseek-v4-flash
psicologo13,2121,981< US$ 0.01deepseek-v4-flash

The zero restarts criterion passed. And the cost is the least interesting part: 205 output tokens across eight calls is an agent that wrote nothing.

What the product discovered on its own​

The Psychologist ran on the heavy tier and produced three hypotheses. The first and second are the correct diagnosis, with no one pointing it out:

The dev-api didn't have (or didn't use) a working terminal environment […] throughout the whole session there isn't a single command-execution tool.call […] only search_workspace/read_file.

The search_workspace tool is under-indexed or misconfigured for this repository: it found no code or manifest file whatsoever […] That misled the agent, which kept trying to figure out "where the project is".

This deserves its own record: the product's introspection worked. The Psychologist read the event log of a failed execution and named the cause more precisely than any assertion in the script.

The findings, for 13c triage​

The phase's discipline holds here as everywhere: a new finding becomes an item, never a fix.

X. The dev agent burns the iteration cap exploring an empty repository​

Given a task in a freshly provisioned repository — which only has the Gitflow template, no code — the agent spent all eight iterations on search_workspace/read_file looking for "where the project is", and never ran a single command or wrote a single file.

The symptom is iteration limit reached with origin modelo, which is technically true and practically useless: the model didn't misjudge anything, it never got to judge anything at all. The cause is the absence of a signal that there's nothing to find — an empty repository is, given the available tools, indistinguishable from a repository where the search failed.

Worth noting this is the first scenario in the product where the dev agent starts from absolute zero. Every test and every demo so far started from a workspace with code.

Y. search_workspace doesn't distinguish "empty" from "found nothing"​

Direct consequence of the previous one, and probably the actionable piece: the first five calls returned no results, which the agent read as "search harder" instead of "there's nothing here". The Psychologist reached the same conclusion on its own.

The second execution: fixing Y did NOT close X​

After closing finding Y, the same execution ran again — same story, same repository, same model. Only the tool's message changed between the two.

1st execution2nd execution
dev-api calls88
output tokens205248
outcomeiteration limit reachediteration limit reached
PRnonenone

The behavior changed observably — one search instead of five, followed by read_file — and the outcome didn't. The new message reached the agent and was the correct one for the case (the workspace has 2 file(s), so the search worked), because the repository had the Gitflow template and wasn't empty.

The hypothesis recorded here was wrong. The earlier text said "the fix is the message, not the cap". The evidence says otherwise: of the eight iterations, seven were exploration. That leaves ONE to write the file, commit, push, and open the PR — not even a perfect agent would close that out.

TOOL_LOOP_MAX_ITERATIONS defaults to 8 (apps/engine/config/runtime.exs:100). The number was born for a conversational agent, and was never reassessed for a dev agent that needs to explore a repository before acting.

The Psychologist, again, was more precise than the script's assertion:

the workspace doesn't contain the project's code: no package.json, no src, no README

it stayed in read-only/exploration mode only

What this teaches about measuring​

Recording the negative result is the point. If the second execution hadn't been run, finding Y — which is real and covered by a test — would have been easy to mistake for the solution to X. It was a correct fix that did not produce the expected effect, and only the execution showed that.

The third execution: the cap WAS the cause​

With TOOL_LOOP_MAX_ITERATIONS=25 instead of 8, and nothing else changed, the dev agent moved up a level:

tool2nd execution (cap 8)3rd execution (cap 25)
search_workspace24
read_file56
write_file03
terminal01
outcomeiteration limitdev.awaiting_approval

It explored, wrote three files, and called npm test --silent. The hypothesis recorded above was right: the cap of 8 — inherited from the conversational agent — doesn't fit a dev agent that needs to understand a repository before acting.

And the reason it stopped changed completely. There was no block: the terminal action became a proposed_action with require_approval and the agent entered dev.awaiting_approval — suspended, holding onto its worktree and history, as ADR 0052 designed. It stopped and waited, instead of burning iterations knocking on the door.

The fourth execution, and what it revealed about Phase F​

Allowing npm, pnpm, node, and npx in the project's allow list didn't unblock it: the agent ran ls -la, a verb not on the list, and went back to pending.

This exposes a gap between what Phase F delivered and what was asked for. The request was "always allow commands as long as they're inside the project folder". ADR 0055 delivered a ceiling: a command that touches a path outside the folder is never auto-approvable, no matter how much the verb is in allow. The ceiling protects the path — the verb is still governed by the allowlist, which is a closed list by design.

Practical consequence: every new command the agent invents falls into require_approval. The ladder still exists, it's just safer now.

I don't treat this as a defect, but as scope: ADR 0055 never promised to promote verbs. But the gap between the request and the delivery is real, and it becomes a triage item.

A trap in the instrument itself​

The first attempt to configure the policy had no effect at all, and the cause is worth recording because it's recurring: the script ran on the host, so PROJECT_WORKSPACES_ROOT fell back to the default /tmp/brabo-project-workspaces and permissions.json was born on a filesystem the engine can't see.

It's the same trap as the test-fixture repository in /tmp that the Phase 12 validation already documents — resurfacing via a different path, on the same day. The script's header now requires, explicitly, execution from inside the container.

The fifth execution: the chain reaches GitHub​

Running from inside the container (the missing condition) and with the terminal verbs allowed, the chain went all the way:

actionpolicyoutcome
terminal ×2auto_approve✅ executed
git_commitauto_approve✅ executed
git_pushauto_approve✅ executed
pr_openauto_approve❌ failed

The agent's branch exists on GitHub: feature/task-d4b36a5b, alongside dev, main, and qa. Code written by a model, committed under the dev-api[bot] identity, and pushed to a real remote repository.

pr_open failed with Requires authentication, and the cause is finding AA: the api resolves the git token via action.decidedBy, which is NULL when policy auto-approves. The push worked because the engine does the pushing, and it injects the owner's credential (RN-076).

The sixth execution: the PR opened on GitHub​

With RN-082 in place, the chain closed all the way to the PR:

PR #1 — "Public greeting route — Expose GET /greeting", from feature/task-636ef1aa, opened on daneiel/test.

Code written by a model, committed as dev-api[bot], pushed and published as a pull request on a real remote repository. The gate opened (pr.gate_changed, gateStatus: awaiting_qa) and the QA area ran.

qa-performance-seguranca was correctly skipped (delegation.dispensed, justification "story has no NFR") — a skip with justification, never silence, as ADR 0038 intended.

qa-automacao failed, and became finding AB: it called a compound command whose last segment (head) wasn't in allow, the ToolLoop suspended in awaiting_approval, and the QA Lead classified the suspension as an "unexpected outcome" with origin infra. It's the defect ADR 0052 fixed for the dev agent and that never reached the gate agents.

The seventh execution: widening the allowlist wasn't enough, and it was predictable​

With 25 verbs allowed — explicit criterion: whatever READS or BUILDS, never whatever reaches the network or destroys — the gate stalled again. And not for lack of a verb:

ls -la && echo "---" && cat package.json 2>/dev/null; …

ls, echo, and cat were all in allow. What blocks it is 2>/dev/null, and it becomes finding AC: the parser treats > as a separator, the redirect becomes a segment whose "verb" is /dev/null, and that same token is also an absolute path outside the project.

The prediction recorded before the execution was confirmed. The allowlist is a closed list and the model invents commands; widening the list is a patch. The 7th execution proves this in a way that's hard to dispute: 25 verbs, and it stalled on the SHAPE of the command, not the verb.

The eighth execution, and the argument it closes​

With Y, AA, AB, and AC fixed, the dev agent made a single call: bash -lc npm test --silent. Verb bash, off the list, require_approval.

The refusal is correct — allowing bash would nullify the entire allowlist, including the built-in deny entries. But the last three executions, together, say something none of them said alone:

executionstalled oncategory
6thheadverb
7th2>/dev/nullshape
8thbash -lcinvocation

Three distinct categories across three rounds. Widening the list solves the first and touches neither of the other two. The verb allowlist doesn't converge against an agent that freely chooses how to invoke what it wants to run.

This isn't a defect in the allowlist: it does what it promises, and refusing bash is proof the boundary holds. It's a limit of SCOPE — it wasn't designed to enable autonomy, and it doesn't.

The 13b's practical conclusion is that, and it's worth more than the PR: the path to an LLM-judged gate doesn't run through loosening policy. It runs through making the agent wait for the decision instead of dying (finding AB), which is what ADR 0052 already did for the dev agent.

The ninth execution: the cheap fix that would have destroyed the guarantee​

The ninth stalled on the same bash as the eighth, and the obvious fix was one line away: put bash in allow and watch the pipeline turn green.

It wasn't done, and that's this execution's contribution. Allowing bash doesn't widen the allowlist — it nullifies it, because every blocked command now has a permitted way to be invoked, including the built-in deny entries. The round would have passed and the guarantee would have been gone, with nothing in the result indicating the trade-off.

What the ninth run fixed, then, was the diagnosis: the problem was never which verb is on the list, it was that the gate agent was dying when policy needed to ask. That's what led to ADR 0057, extending to the gate what ADR 0052 had already done for the dev agent: faced with an action that requires a decision, suspend and wait instead of classifying the suspension itself as an infra failure (finding AB).

The tenth execution: the whole chain, end to end​

With ADR 0057 in place, the tenth closed everything the previous nine had left open, without a single engine restart:

stepoutcome
remote adoptionorigin: adopted against daneiel/test
repository planexecuted only after your decision
story promotionmanual, with the user as the actor
real dev agentwrote code, committed as <agent>[bot]
push and remote PRpublished on GitHub
gateopened (pr.gate_changed)
QA areadelegated and skipped with justification
subagentsuspended for approval, and didn't die
your refusalresumed the loop instead of ending it
verdictchanges_requested, judged by an LLM

The last two rows are the ones that matter. The subagent stopping and staying alive is ADR 0057 working; the user's refusal resuming the loop is the point no previous execution had reached — the human decision enters mid-way through the agent's work and it carries on from there, instead of starting over or giving up.

And the verdict wasn't written by the script: it came from the model's own judgment of a real PR. It's the exact difference this validation exists to cover, relative to its deterministic sibling.

TODO(humano): the dollar cost and call count for these two executions weren't extracted with medir:execucao at the time. If token_usage for those sessions still exists, it's worth filling in — every other measurement in this document comes from a script, and these two are the exception.

The two-module executions: parallelism put to the test​

The first ten runs used one module. That's enough to prove the chain, and not enough to prove parallelism: with a single story, the Dev Lead refuses to parallelize — "they'd collide on the same files" — and it's right. The cap from RN-083 was never actually consulted by real work.

Then came three rounds with --modulos 2 (one story in api, one in web). The first two are covered in finding AF — the one that broke and the one that proved the fix. What follows is the third, which exists purely to measure.

The Dev Lead planned on its own:

2 agents across 2 modules — "each module has exactly one story, so one agent per module is the minimum that's justifiable without waste."

And the round closed clean, measured by medir:execucao, not by hand:

duration3m56s, 182 events across 3 sessions
calls33 (dev-api 10, dev-web 9, arquiteto 7, qa-automacao 6, dev-lead 1)
cost< US$ 0.01
engine restartno
silent turnsnone
gatesqa approved, secops approved

The cap enforced the decision. With both agents already up, the next request stalled in aguardando_autorizacao with the parallelize action pending in the database — 2 active, cap 2. Nothing came up: if it had, the authorization would have been theater.

What the two modules broke, before this round​

This round came out clean, but it's the third with two modules, and the first two are the ones that paid the price. In the first, dev-web claimed the task and died on fatal: not a git repository before the first turn — zero tokens spent, task blocked. It's finding AF: the fast-path guard in Workspace.ensure!/4 checked whether .git existed, and git init creates .git before the fetch; the second agent read that as "ready" and skipped the lock.

The lock had existed since Phase 4 and was correct. What was wrong was the criterion that decided whether it was worth acquiring — and none of the previous ten executions could have shown this, because none had a second agent. Once fixed, dev-web went from 0 to 16 calls in the following round, and that's how this third one inherits the right to be a pure measurement.

The instrument also learned something: the cap's assertion stated the number (the 3rd request asks for authorization), which held for one module and failed a run where the product did the right thing — with two modules, activation alone fills the cap. It now states the rule instead: while there's room, it goes up without asking; when there isn't, it stops.

What this validation still does NOT prove​

Honesty about scope, as with its sibling:

  • Merge. Still out by design (RN-014), and will remain so: whoever presses the button on a protected branch is you.
  • The other five providers. Only OpenRouter ran with a real credential; the rest remain without a smoke test for lack of a key, and what holds for one provider doesn't transfer to the others by argument.
  • Isolation. The agent runs in the same container as this monorepo. ADR 0055 states of itself that it's policy, not isolation — .. fails, but a symlink pointing from inside to outside isn't detected.
  • Autonomy with no policy in the way. The verb allowlist doesn't converge (findings Z and AD), and that's a scope limit, not a bug to fix.
  • The parallelism cap requested by the Dev Lead ITSELF. With two modules it planned 2 agents and the cap is 2 — it fit. What overflowed the cap was the script, calling the use case directly. Seeing the lead ask for more than it can have would take 3+ modules, and that hasn't run yet.

The chain itself — from adoption to an LLM-judged gate verdict — is proven against a real network. What remains open above isn't the chain: it's the environment it runs in and the surface it covers.

References​