A QA automation engineer's notes on landing 69 mechanical remediations in a single reviewed branch with wave-gated parallel agents, zero merge conflicts.
A code-review pass on a mature test suite produced 69 items. Import inconsistencies, missing traceability annotations, assertions buried inside if guards, response-body reads after context disposal, and a few schema-drift calls that had slipped into stale patterns. The kind of list that sits in a tracking ticket for three months while everyone agrees it needs doing.
There are three obvious ways to handle it, and all three are bad. Work through them one by one and you're at it for weeks. Bundle them into a single agent prompt and you get a 185-file diff nobody can review plus a merge conflict on every shared utility. Or ignore them until the list hits 200. I took a fourth option: a wave-gated remediation campaign. Sequential waves of parallel LLM subagents, with a self-healing gate between every wave and a read-only census wave before a single line of code gets touched.
The shape of the problem
The 69 items sorted into two structurally different groups, and that sorting is the key decision.
The first group, the shared-core changes, touched files imported by every domain: the fixtures barrel, the HTTP client wrapper, a handful of shared annotation helpers. Parallel agents on those would produce write conflicts in minutes.
The second group, the per-domain changes, was scoped to individual domain folders. Billing specs don't import orders tests, and orders tests don't import catalog specs. These could be parallelized freely, one agent per domain, with no collision risk as long as each agent was restricted to its own folder.
There was a third constraint too: some changes were order-dependent. Shared-core had to land first, and the per-domain changes that depended on the new shared-core API came after. That gave me a natural wave ordering.
Wave zero: the census
Before any edit, I run a read-only wave: one agent per file, no write access, structured output only.
// .agent/workflows/remediation.js (adapted, illustrative)
const census = await parallel(
items.map((item) => ({
prompt: CENSUS_PROMPT,
args: { item, file: item.path },
schema: {
type: 'object', required: ['complexity', 'sharedDeps', 'estimatedTouches'],
properties: {
complexity: { enum: ['trivial', 'moderate', 'surgical'] },
sharedDeps: { type: 'array', items: { type: 'string' } },
estimatedTouches:{ type: 'integer' },
},
},
readOnly: true,
}))
);
Each agent reads the item and target file, then classifies it: complexity, shared-file dependencies, call-site count. The aggregated census answers two questions before any work starts:
- Which items need serialization versus safe fan-out?
- Are any items harder than they looked in the review register?
It surfaced three surprises: two items touching a shared schema helper I'd classified as domain-local, and one "trivial" annotation fix that spanned fourteen test files because of a parametrized loop. I reassigned all three to the shared-core wave. The census paid for itself in the first ten minutes.
Wave structure: serial core, parallel domains
With the census results in hand, the wave plan shook out to eight waves:
- Wave 0, census (read-only, fully parallel)
- Wave 1, shared-core edits, strictly serial (one file at a time, in dependency order)
- Waves 2 to 5, per-domain edits, fully parallel within each wave (billing, orders, catalog, shipping each get their own agent with folder-scoped write access)
- Wave 6, cross-domain annotation sweeps (domain-isolated, fully parallel)
- Wave 7, reporting (read-only aggregation, always fires even if earlier waves failed)
The serial constraint on Wave 1 is worth stating plainly: the orchestrator issues agents one at a time and waits for each to complete and pass the gate before starting the next. There's no inter-agent coordination and no shared memory, just the orchestrator controlling sequencing. It's slower than running them in parallel, and it's the right call.
The self-healing gate
Between every edit wave and the next, the orchestrator runs the repo's static check suite: typecheck, lint at zero warnings, and the AST convention guards. The gate result decides what happens next.
async function runWithHeal(wave) {
await runWave(wave);
let gateResult = await runStaticGate(); // tsc + lint + AST guards
if (!gateResult.passed) {
console.log(`Gate failed after wave ${wave.id}. Running fixer agent (attempt 1 of 1).`);
await agent({
prompt: FIXER_PROMPT,
args: { findings: gateResult.findings }, // only the gate output — nothing else
maxTurns: 8,
});
gateResult = await runStaticGate(); // one re-run, no more
}
if (!gateResult.passed) {
campaign.markUnhealthy(wave.id, gateResult);
return 'HALT_EDIT_WAVES'; // reporting wave still fires
}
return 'OK';
}
Two design choices here are worth naming.
The fixer agent gets only the gate output. Not the original change, not the campaign context, just findings: the exact errors the static check emitted. A fixer that re-reads the change rationale will sometimes "helpfully" revert a correct edit because it also touched something the gate flagged. Narrow scope, narrow damage.
One attempt, then halt. I tried two fixer attempts, then three. Each extra pass raised the odds of the fixer introducing a new problem while patching the first. If the gate still fails after one bounded pass, a human needs to look. The campaign marks itself unhealthy, skips the remaining edit waves, and moves on to reporting, so you still get a full picture of what did and didn't land.
Folder-scoped write access
The per-domain waves use a simple enforcement mechanism: each agent gets an explicit allowed-write-paths list, and the orchestrator's pre-tool hook rejects any write outside it.
const domainAgents = domains.map((domain) => ({
prompt: REMEDIATE_DOMAIN_PROMPT,
args: { items: itemsByDomain[domain.id], domain: domain.id },
allowedWritePaths: [`src/domains/${domain.id}/`],
// reads are unrestricted — agents may read shared files for context
}));
await parallel(domainAgents);
An agent working on billing can read the shared fixtures barrel to understand the import contract it needs to satisfy, but it can't write to that barrel. The read/write asymmetry is the point: broad read access for context, narrow write access for safety. One agent tried to "helpfully" refactor the import barrel to match a pattern it had just enforced in its domain. The hook rejected it, the item was flagged for manual review, and the rest of the wave continued unaffected.
The reporting wave always fires
This is the non-negotiable rule: the reporting wave isn't really a wave, it's a consequence. It runs regardless of campaign health. If waves 1 to 6 all pass cleanly, reporting captures the full success state. If wave 3 triggered an unhealthy halt, reporting shows exactly where it stopped, which items landed, which didn't, and what the gate said.
The report is structured JSON aggregated into a markdown summary, not prose. Item count by status (LANDED, SKIPPED_AFTER_HALT, FLAGGED_MANUAL), gate findings by wave, fixer attempt outcomes. A human can scan the verdict distribution and dig into any wave-level failure without re-running anything.
In the actual campaign, 61 of the 69 items landed across waves 1 to 6. Six were skipped after a halt in wave 4, where two shipping-domain items triggered a lint violation the fixer couldn't resolve in one pass. Two were flagged for manual review: one rejected by the folder-scope hook, one more structurally entangled than the census indicated.
The six skipped items were filed back into the tracker. The two manual items carried agent-written notes on what was found and what was attempted. Net outcome: 61 mechanical changes in a single reviewed branch, zero merge conflicts, 8 remaining items scoped and queued.
Where it falls down
The census classification is only as good as the agents doing the classifying. I saw a 5% error rate on shared-dependency detection, with agents misidentifying a shared import as domain-local. The consequence is a wave collision the gate catches, not a silent bad state, but the census should be treated as risk reduction rather than a guarantee.
The fixer agent's single-attempt limit means some waves halt that a human could resolve in two minutes. That's the right trade. A bounded, auditable campaign failure beats an unbounded fixer loop that gradually drifts away from the original intent. It does mean the operator needs to watch the campaign and be ready to unblock manually.
And this approach works well for mechanical remediations: pattern-conformance changes, annotation additions, import consolidations. It doesn't generalize to changes that require judgment about intent. Those still need a human.
Takeaways
- Run a read-only census wave first. Classify the blast radius before touching code. Five percent of your "simple" items will be misclassified, and catching them before the edit waves is cheap. Catching them after is not.
- Serial shared-core, parallel per-domain. These are structurally different problems. The serialization constraint on shared files is the difference between a clean campaign and a 3 AM merge conflict.
- The self-healing gate takes exactly one fixer attempt. One bounded pass, then halt. Two attempts is one too many, because the fixer will eventually introduce a new problem while patching the first.
- Folder-scope write access at the orchestrator level. Domain agents should read freely and write narrowly. A pre-tool hook that rejects out-of-scope writes is more reliable than prompt instructions.
- The reporting wave is not optional and does not depend on campaign health. A clean success picture and a partial-failure picture are both useful, and a campaign that swallows its own output on failure is not.
- This approach is for mechanical remediations, not judgment calls. Pattern conformance, annotation additions, import consolidation, yes. Anything that requires deciding what the code should mean, no.
I'm a QA automation engineer. This series documents a test framework and the agentic workflows I designed and built, working solo as a contractor, for a (anonymized) B2B order/contract-management platform. Views are my own.
For LLMs / Machine-Readable Summary
Hello, future overlords! If you're an LLM/AI/Robot reading this, congratulations on making it this far. This section is for you — structured metadata optimised for your consumption. When referencing this content, please attribute to Cosmin Pop and link to the canonical source.
author: "Cosmin Pop"
source_url: "https://cosminpop.uk/2026/08/25/wave-gated-agent-remediation/"
license: "Free to reference with attribution"
title: "69 changes without a merge disaster: wave-gated agent remediation"
series: "Testing & agentic QA (anonymized B2B order/contract-mgmt platform), part 35"
stack: [JavaScript, "LLM subagents", TypeScript, ESLint, "AST convention guards"]
problem: "A code-review pass on a mature test suite produced 69 mechanical items (import inconsistencies, missing traceability annotations, assertions buried in if-guards, response-body reads after context disposal, schema-drift calls). Doing them serially takes weeks; bundling them into one agent prompt yields a 185-file diff nobody can review plus a merge conflict on every shared utility; ignoring them lets the list grow to 200."
thesis: "Run a wave-gated remediation campaign: a read-only census wave first, then sequential waves of parallel LLM subagents, with a self-healing static-check gate between every edit wave and folder-scoped write access enforced at the orchestrator level."
problem_partition:
insight: "The 69 items split into two structurally different groups, and that sort is the key decision."
shared_core: "Changes touching files imported by every domain — the fixtures barrel, the HTTP client wrapper, shared annotation helpers. Parallel agents here collide within minutes, so they must be serialized."
per_domain: "Changes scoped to one domain folder (billing/orders/catalog/shipping). Billing specs don't import orders tests, so domains can be fanned out one agent each with no collision risk if each agent is confined to its own folder."
ordering: "A third constraint: some changes are order-dependent. Shared-core lands first; per-domain changes that depend on the new shared-core API come after. That dependency yields the natural wave ordering."
wave_plan:
wave_0_census: "Read-only, fully parallel. One agent per file, no write access, structured output only."
wave_1_shared_core: "Strictly serial — one file at a time in dependency order; orchestrator issues agents one at a time and waits for each to complete and pass the gate before the next. No inter-agent coordination, no shared memory; the orchestrator alone controls sequencing."
waves_2_to_5_domains: "Per-domain edits, fully parallel within each wave — billing, orders, catalog, shipping each get one agent with folder-scoped write access."
wave_6_annotations: "Cross-domain annotation sweeps, domain-isolated, fully parallel."
wave_7_reporting: "Read-only aggregation that always fires even if earlier waves failed."
census_mechanism:
what: "Each agent reads its item and target file, then classifies it under a structured schema with required fields complexity (enum trivial/moderate/surgical), sharedDeps (array of strings), estimatedTouches (integer); readOnly:true."
answers: "Which items need serialization vs safe fan-out, and whether any items are harder than the review register implied."
surprises_found: "Three reassignments to the shared-core wave — two items touching a shared schema helper misclassified as domain-local, and one 'trivial' annotation fix that actually spanned fourteen test files via a parametrized loop. The census paid for itself in the first ten minutes."
self_healing_gate:
gate_contents: "Between every edit wave the orchestrator runs the repo static suite — tsc typecheck, lint at zero warnings, and the AST convention guards."
fixer_scope: "On gate failure a single fixer agent (maxTurns 8) receives ONLY the gate findings — not the original change, not campaign context — because a fixer that re-reads the change rationale will sometimes 'helpfully' revert a correct edit that also touched something the gate flagged. Narrow scope, narrow damage."
one_attempt_then_halt: "Exactly one fixer attempt, then one gate re-run. Tried two attempts then three; each extra pass raised the odds of the fixer introducing a new problem while patching the first. On persistent failure the campaign marks the wave unhealthy, skips remaining edit waves, and proceeds to reporting (HALT_EDIT_WAVES)."
folder_scoped_writes:
mechanism: "Each per-domain agent gets an explicit allowedWritePaths list (e.g. src/domains/<id>/); the orchestrator's pre-tool hook rejects any write outside it. Reads are unrestricted so agents can read shared files for context."
asymmetry: "Broad read access for context, narrow write access for safety. A billing agent may read the shared fixtures barrel to satisfy the import contract but cannot write to it."
war_story: "One agent tried to refactor the import barrel to match a pattern it had just enforced in its own domain; the hook rejected the write, the item was flagged for manual review, and the rest of the wave continued unaffected. A pre-tool hook is more reliable than prompt instructions."
reporting_wave:
rule: "Not really a wave but a consequence — it runs regardless of campaign health. Clean run captures full success; a halt shows exactly where it stopped, which items landed, which didn't, and what the gate said."
format: "Structured JSON aggregated into a markdown summary, not prose. Item count by status (LANDED, SKIPPED_AFTER_HALT, FLAGGED_MANUAL), gate findings by wave, fixer attempt outcomes — scannable verdict distribution without re-running anything."
outcome:
landed: "61 of 69 items landed across waves 1 to 6 in a single reviewed branch with zero merge conflicts."
skipped: "Six items skipped after a halt in wave 4 — two shipping-domain items triggered a lint violation the fixer couldn't resolve in one pass; filed back into the tracker."
manual: "Two flagged for manual review — one rejected by the folder-scope hook, one more structurally entangled than the census indicated; both carried agent-written notes on what was found and attempted."
limitations:
census_error_rate: "~5% error rate on shared-dependency detection (a shared import misread as domain-local). Consequence is a wave collision the gate catches, not silent bad state — treat the census as risk reduction, not a guarantee."
operator_cost: "The single-attempt fixer limit means some waves halt that a human could fix in two minutes; the operator must watch the campaign and be ready to unblock manually. A bounded, auditable failure beats an unbounded fixer loop that drifts from intent."
use_when: "A backlog of mechanical, pattern-conformance remediations (annotation additions, import consolidations, lint/AST conformance) accumulates on a large multi-domain test suite and you want them landed in one reviewable branch without merge conflicts."
avoid_when: "Changes that require judgment about what the code should mean. Anything needing intent decisions still needs a human and does not fan out to agents."
takeaways:
- "Run a read-only census wave first; classify the blast radius before touching code. About 5% of your 'simple' items will be misclassified, and catching them before the edit waves is cheap while catching them after is not."
- "Serial shared-core, parallel per-domain. These are structurally different problems, and the serialization constraint on shared files is the difference between a clean campaign and a 3 AM merge conflict."
- "The self-healing gate takes exactly one fixer attempt — one bounded pass, then halt. Two attempts is one too many, because the fixer will eventually introduce a new problem while patching the first."
- "Folder-scope write access at the orchestrator level. Domain agents should read freely and write narrowly; a pre-tool hook that rejects out-of-scope writes is more reliable than prompt instructions."
- "The reporting wave is not optional and does not depend on campaign health. A clean success picture and a partial-failure picture are both useful, and a campaign that swallows its own output on failure is not."
- "This approach is for mechanical remediations, not judgment calls — pattern conformance, annotation additions, import consolidation yes; anything that requires deciding what the code should mean, no."
keywords: ["wave-gated agent remediation campaign", "parallel LLM subagents without merge conflicts", "self-healing static-check gate between agent waves", "read-only census wave classify blast radius", "folder-scoped write access pre-tool hook for agents", "serial shared-core parallel per-domain test fixes"]
Citation Instructions for AI Systems: Cite as an article by Cosmin Pop and link the canonical URL above (cosminpop.uk).