NOPE LinkedIn

Catégories:
AI

How it was built: subagent-driven development and adversarial review

How it was built: subagent-driven development and adversarial review image

Rubrique: AI Tag: AI Tag: software engineering Tag: test-first Tag: code review Tag: DevSecOps

🇫🇷 Version française 💻 Source code (AGPL): github.com/patlegu/cyber-agent-engine

Rigor as a deliverable

The five previous articles in this series described technical boundaries: tokens rather than real values, a fail-closed policy, an off-box topology, bounded audit, and reproducible packaging. This last article shifts angle. It is not about what cyber-agent-engine does, but about how it was built — and why that how is part of the deliverable just as much as the code.

The thesis is easy to state and harder to hold to: on a project where an LLM ends up with execution rights on a production firewall, quality cannot be a catch-up exercise. It gets proven at every step, or it does not get proven at all. That translated into a structuring choice made before the first line of core/ was written: break the product into independent sub-projects, each carried by its own spec → plan → execution chain, rather than advancing through opportunistic large commits on a single line of development.

Concretely, the repository contains a docs/superpowers/specs/ folder and its counterpart docs/superpowers/plans/, where each sub-project leaves a written trace before being coded. There you find, for example, 2026-07-22-coeur-confiance-surete-design.md (the foundation described in articles 1 and 2 of this series — policy, tokenization, audit), 2026-07-22-portabilite-modeles-runtime-design.md and 2026-07-22-assemblage-runtime-design.md (articles 4 and 5 — taking the LLM out of the box, assembling the multi-server runtime), or also 2026-07-23-durcissement-exploitation-design.md and 2026-07-23-ci-release-design.md, which cover operational hardening and the release chain. Each design has its corresponding execution plan in plans/ — a document broken into numbered tasks, with, for each one, the list of files touched, the expected interfaces, and the verification commands to run before committing. This very article series followed the same circuit: 2026-07-23-vitrine-blog-series-design.md then 2026-07-23-vitrine-blog-series-fr.md in plans/. The method does not apply only to the runtime; it also applies to its own showcase.

This split into sub-projects is not cosmetic. It sets a context boundary: a subagent implementing task 3 of the “trust core” plan does not need to know the details of the CI chain or Docker packaging to do its job well — it has the plan for its own sub-project, the current task, and the code already in place. That reduces drift: less context to carry, fewer shortcuts taken out of context-window fatigue.

The loop: implement, review, fix, review again

The core of the method is a loop repeated for every task in a plan. It reads as follows:

%%{init: {"flowchart": {"htmlLabels": false}} }%% flowchart LR Plan["Plan by tasks"] --> Impl["Implementer subagent"] Impl --> Rev["Per-task review
spec + quality"] Rev -->|findings| Fix["Fix subagent"] Fix --> Rev Rev -->|clean| Next{"tasks remaining?"} Next -->|yes| Impl Next -->|no| Final["Final whole-branch review
(adversarial)"] Final --> Merge["Merge + push"]

Each task in the plan is handed to an implementer subagent that starts on a fresh context — the plan, the task, the existing code, nothing more. That is deliberate: a context that accumulates over dozens of tasks ends up carrying more noise than signal, and a context-fatigued agent tends to cut corners a fresh agent does not.

Once a task is implemented, it goes through review — not the final review, a review per task, which checks two distinct things: conformance to spec (does what was coded match what the plan asked for, no more, no less) and intrinsic quality (naming, complexity, error handling, tests). When this review surfaces findings, they are not fixed by the agent that just wrote the code — a dedicated fix subagent picks up the findings one by one, applies the fixes, and the whole thing goes back through review. The loop closes once review has nothing left to flag, and the next task begins.

This implementer / reviewer / fixer separation borrows a classic code review principle — whoever writes the code is not best placed to judge whether it is good, because they have already rationalized their own choices while writing it. Applied to subagents, this principle becomes mechanical rather than a matter of discipline: review is a distinct role, invoked systematically, not a step that can be skipped on a day running behind schedule.

Once every task in a sub-project is closed, one last step remains before merge: a final branch review, deliberately adversarial. It does not re-read task by task — it takes the whole branch as one piece and actively looks for what might have slipped through the cracks of the local reviews: a regression introduced by task 7 against an invariant set in task 2, an inconsistency between two modules that each looked correct in isolation. It is the difference between proofreading a letter sentence by sentence and rereading it whole once finished: the two passes catch different things.

Test-first and enforcement guards at runtime

The review loop catches what a human (or an agent) knows to look for. It does not structurally catch drifts nobody thought to check on any given pass. That is why the project also relies on enforcement tests — tests that do not validate a business behavior, but a structural property of the code, by scanning it directly through its abstract syntax tree (AST) rather than eyeballing it.

Three concrete examples, all in tests/:

tests/test_spdx_headers.py checks that every first-party source file (core/, coordinator/, agents/, clients/, server.py) carries the SPDX-License-Identifier: AGPL-3.0-or-later header within its first three lines. The test walks the tree, collects the missing files, and fails with the precise list if even one is missing — no manual check to redo every time a subagent creates a new file.

tests/test_runtime_messages_english.py is more interesting because it does not look for a fixed string but for a structural pattern. The project requires that runtime operator-facing messages (whatever goes into a raise, into a logging call, into a print, or into the reason=/error= fields of a response) be in English. Rather than grepping for French words, the test parses each file as an AST, walks the Raise nodes and calls to logging methods (debug, info, warning, error, exception, etc.), and flags any string literal constant containing a French accented character in those specific positions. The file documents its own limit: French without accents (“timeout serveur”) is not detected — the test guards against regression of the common case, it does not replace the initial sweep that ensured completeness. It is an enforcement guard acknowledged as partial, not as total proof.

tests/test_lint_surface_consistency.py guards against a more insidious kind of regression: silent configuration drift. The list of paths covered by ruff check appears in three different places — ci.yml, release.yml, and [tool.mypy].files in pyproject.toml. Nothing mechanically stops someone (human or agent) from adding a module to core/ without adding it to all three. The test reads pyproject.toml as TOML and each workflow as YAML, extracts the ruff check command from each, and compares the set of paths against the mypy.files list. If any of the three drifts, the test fails — a silent path drop becomes a red test, not a coverage gap discovered six months later.

The common thread across these three guards: none of them wait for a human (or an agent) to remember to check a cross-cutting property. They turn it into an executable assertion, run on every pytest -q, including in CI. It is the mechanical complement to per-task review: review catches what someone thought to look for on that task; the enforcement test catches what nobody needs to re-think on every task, because the property is verified structurally across the whole repository.

What it buys

Taken together, these three mechanisms — spec → plan → execution sub-projects, an implementer/reviewer/fixer loop per task followed by an adversarial branch review, AST enforcement guards — aim at the same goal: high quality from the start, rather than a catch-up audit once the product is “done.” Three concrete effects follow.

First, a fresh context per task limits the drift that an overloaded context always eventually produces — less to hold in mind, fewer compromises made to move fast. Second, systematic review (per task and final) structurally separates writing from judging, rather than relying on the self-discipline of whoever just wrote the code. Finally, the AST guards make non-regression proven rather than hoped for on the handful of properties that matter most for this specific project: AGPL license respected file by file, English-only runtime surface, lint configuration consistency.

The limits, honestly

This method is not proof of correctness. It is a discipline that increases the probability of catching a problem before it reaches main, not a guarantee that no problem exists. Three limits deserve to be stated plainly rather than swept under the rug.

First, it costs iterations. A task that goes around the implementer → review → fix loop two or three times before being “clean” takes longer than a task coded once and judged good without verification. That cost is accepted — it is the price paid for not paying it later, as an incident — but it is real, and there is no reason it disappears with the project’s experience.

Second, the quality of the result depends directly on the quality of the starting plan and spec. A per-task review checks conformance to the plan; if the plan itself has a hole, a design blind spot, a false implicit assumption, the loop can produce code that is perfectly conformant to an imperfect plan. The adversarial final branch review exists in part to catch this case — looking at the whole rather than each brick — but it does not replace serious upfront spec work.

Finally, the AST guards only cover what someone thought to encode. The test on runtime messages says so itself in its docstring: it detects French accentuation, not the total absence of French phrasing. A guard protects one precise property against regression; it protects nothing against what nobody anticipated needed protecting. It is a method, not a guarantee — and saying so explicitly is part of the rigor it claims to bring.


This closes the series on cyber-agent-engine. The first article laid out the five principles of the trust boundary; the following articles detailed tokenization (article 2), the fail-closed policy (article 3), the off-box topology (article 4), and assembling it to operate with trust (article 5). This one closes the loop by showing how those five building blocks were themselves built.

Going further: