<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lutz Leonhardt</title>
    <description>The latest articles on DEV Community by Lutz Leonhardt (@lutz_leonhardt).</description>
    <link>https://dev.to/lutz_leonhardt</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3876255%2F3dab236f-4f54-4e36-8f07-62b7ca366cca.png</url>
      <title>DEV Community: Lutz Leonhardt</title>
      <link>https://dev.to/lutz_leonhardt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lutz_leonhardt"/>
    <language>en</language>
    <item>
      <title>I Used AI Agents to Migrate 44 Angular Components. The Review Changed My Mind.</title>
      <dc:creator>Lutz Leonhardt</dc:creator>
      <pubDate>Mon, 13 Apr 2026 10:01:47 +0000</pubDate>
      <link>https://dev.to/lutz_leonhardt/i-used-ai-agents-to-migrate-44-angular-components-the-review-changed-my-mind-4pop</link>
      <guid>https://dev.to/lutz_leonhardt/i-used-ai-agents-to-migrate-44-angular-components-the-review-changed-my-mind-4pop</guid>
      <description>&lt;p&gt;I used AI agents to migrate 44 Angular components in SAP Spartacus from Reactive Forms to Signal Forms.&lt;/p&gt;

&lt;p&gt;On the first run, 34 looked successful.&lt;/p&gt;

&lt;p&gt;The review phase showed that "successful" did not mean what I thought it meant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI scales transformation. It does not guarantee equivalence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article covers the initial migration run, what the follow-up review exposed, and how I would structure a large AI-assisted refactoring in a real client project today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration Target: Signal Forms Stage 1
&lt;/h2&gt;

&lt;p&gt;Angular 21.2 ships &lt;code&gt;SignalFormControl&lt;/code&gt; — a bridge between Reactive Forms and Signal Forms. &lt;a href="https://www.angulararchitects.io/blog/migrating-to-angular-signal-forms-interop-with-reactive-forms/" rel="noopener noreferrer"&gt;Manfred Steyer's blog post&lt;/a&gt; describes the interop pattern: replace individual &lt;code&gt;FormControl&lt;/code&gt; instances with &lt;code&gt;SignalFormControl&lt;/code&gt;, keep &lt;code&gt;FormGroup&lt;/code&gt; and templates largely intact, swap &lt;code&gt;Validators.*&lt;/code&gt; for signal-based validators.&lt;/p&gt;

&lt;p&gt;I call this &lt;strong&gt;Stage 1&lt;/strong&gt;: a drop-in replacement with minimal blast radius. No full template rewrite. No &lt;code&gt;FormArray&lt;/code&gt; migration (there's no &lt;code&gt;SignalFormArray&lt;/code&gt; yet).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;form&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UntypedFormGroup&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UntypedFormBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;ngOnInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Validators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Validators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
      &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Validators&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SignalFormControl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/forms/signals/compat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@angular/forms/signals&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;emailControl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SignalFormControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;passwordControl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SignalFormControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormGroup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emailControl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passwordControl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more &lt;code&gt;FormBuilder&lt;/code&gt; injection. No more &lt;code&gt;ngOnInit&lt;/code&gt; initialization. Validators live in a schema function. Templates keep working because &lt;code&gt;SignalFormControl&lt;/code&gt; extends &lt;code&gt;AbstractControl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Independent components, repetitive steps, an existing test suite — this looked like a near-perfect fit for agentic refactoring. That was true for the transformation itself. It was not true for validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Two Manual Migrations
&lt;/h2&gt;

&lt;p&gt;I migrated &lt;code&gt;CartCouponComponent&lt;/code&gt; and &lt;code&gt;CartQuickOrderFormComponent&lt;/code&gt; by hand. Simple forms, straightforward validators. But even on these, I hit an edge case that became one of the most important migration rules:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;required&lt;/code&gt; HTML attribute trap.&lt;/strong&gt; If your template has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;required=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;formControlName=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Angular's built-in &lt;code&gt;RequiredValidator&lt;/code&gt; directive activates automatically and calls &lt;code&gt;setValidators()&lt;/code&gt; on the bound control. &lt;code&gt;SignalFormControl&lt;/code&gt; does not support dynamic validator mutation and throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NG01920: Dynamically adding and removing validators is not supported in signal forms.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix: remove the &lt;code&gt;required&lt;/code&gt; attribute from the template — the validator already lives in the &lt;code&gt;SignalFormControl&lt;/code&gt; schema. Even a migration that looks like a drop-in replacement has hidden edge cases.&lt;/p&gt;

&lt;p&gt;After those two manual migrations, I extracted a reusable process and wrote it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Artifacts
&lt;/h2&gt;

&lt;p&gt;The entire orchestration rested on three markdown files:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;goal.md&lt;/code&gt; — The Orchestration Protocol.&lt;/strong&gt; Startup sequence, branch strategy, sub-agent loop, abort criteria. When to spawn, when to merge, when to give up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SignalMigration.md&lt;/code&gt; — The Playbook.&lt;/strong&gt; Step-by-step migration rules, validator mapping, import paths, special cases, verification commands. This was not "prompt engineering" — it was a technical playbook written the way I would document the task for another developer on a team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Plan.md&lt;/code&gt; — The Bill of Materials.&lt;/strong&gt; All 44 target components with Nx library, file paths, and status. The orchestrator used it as a state machine: &lt;code&gt;TODO&lt;/code&gt; → &lt;code&gt;IN_PROGRESS&lt;/code&gt; → &lt;code&gt;SUCCESS&lt;/code&gt; / &lt;code&gt;FAILED&lt;/code&gt; / &lt;code&gt;SKIP&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Orchestrator Architecture
&lt;/h2&gt;

&lt;p&gt;The orchestrator was a Claude Code agent. It read &lt;code&gt;goal.md&lt;/code&gt;, followed the protocol, and spawned one sub-agent per component using isolated git worktrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;subagent_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;general-purpose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;isolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;worktree&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    You are migrating CheckoutLoginComponent from Reactive Forms
    to SignalFormControl.

    Read first: /SignalFormMigration/SignalMigration.md

    Files to migrate:
    - feature-libs/checkout/base/components/checkout-login/
        checkout-login.component.ts

    Nx library for tests: @spartacus/checkout/base

    After migration, run the verification build.
    Report: SUCCESS with commit hash, or FAILURE with error description.
  `&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;isolation: "worktree"&lt;/code&gt; parameter was critical. Each sub-agent got its own copy of the repository, branched from &lt;code&gt;feature/signal-forms-migration&lt;/code&gt;. It could change files, run tests, and commit without interfering with other migrations.&lt;/p&gt;

&lt;p&gt;On success, the orchestrator merged the worktree branch back into the feature branch using &lt;code&gt;--no-ff&lt;/code&gt;. On failure, the worktree was discarded and the failure documented.&lt;/p&gt;

&lt;p&gt;In total, the migration PR ended up with 94 commits. The initial pipeline ran in a single evening — roughly two to three hours of agent time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial Results
&lt;/h2&gt;

&lt;p&gt;Out of 44 target components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;34&lt;/strong&gt; completed the initial migration pipeline and were merged automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5&lt;/strong&gt; failed during migration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5&lt;/strong&gt; were skipped because they used &lt;code&gt;FormArray&lt;/code&gt;, which has no Stage 1 equivalent yet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a &lt;strong&gt;77% initial automation rate&lt;/strong&gt; across all 44 targets (34/44). If you exclude the 5 components that were intentionally skipped because &lt;code&gt;FormArray&lt;/code&gt; has no compatible Stage 1 migration path, the initial run reached &lt;strong&gt;87%&lt;/strong&gt; across attempted components (34/39).&lt;/p&gt;

&lt;p&gt;That initial result was real and useful. It proved that the mechanical part of the migration could be scaled across a large Angular codebase.&lt;/p&gt;

&lt;p&gt;But the review phase changed how I interpret those numbers.&lt;/p&gt;

&lt;p&gt;In the first pipeline, "SUCCESS" meant: the migration completed and no immediate TypeScript-level blocker remained. It did &lt;strong&gt;not&lt;/strong&gt; reliably mean that unit tests had run, that runtime behavior was unchanged, or that validation semantics were preserved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Failure Taxonomy from the Initial Run
&lt;/h2&gt;

&lt;p&gt;The 5 explicit failures were already interesting because they clustered around one API boundary: &lt;strong&gt;&lt;code&gt;SignalFormControl&lt;/code&gt; does not support imperative validator or error mutation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure 1: &lt;code&gt;CsagentLoginFormComponent&lt;/code&gt;&lt;/strong&gt; — The template had &lt;code&gt;required="true"&lt;/code&gt; on inputs. Angular's &lt;code&gt;RequiredValidator&lt;/code&gt; directive called &lt;code&gt;setValidators()&lt;/code&gt;, which triggered &lt;code&gt;NG01920&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure 2: &lt;code&gt;OrderGuestRegisterFormComponent&lt;/code&gt;&lt;/strong&gt; — Used &lt;code&gt;CustomFormValidators.passwordsMustMatch&lt;/code&gt;, a cross-field validator that called &lt;code&gt;setErrors()&lt;/code&gt; on another control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failures 3 &amp;amp; 4: &lt;code&gt;DeliveryModeDatePickerComponent&lt;/code&gt; and &lt;code&gt;DateRangeModalComponent&lt;/code&gt;&lt;/strong&gt; — Both routed controls through a shared date picker component using &lt;code&gt;[formControl]&lt;/code&gt;. Internally, Angular's form setup path called &lt;code&gt;setValidators()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure 5: &lt;code&gt;VerifyRegisterVerificationTokenFormComponent&lt;/code&gt;&lt;/strong&gt; — A combination of &lt;code&gt;setErrors()&lt;/code&gt; in error handlers, &lt;code&gt;form.enable()&lt;/code&gt; in tests, and cross-field validators using imperative patterns.&lt;/p&gt;

&lt;p&gt;These failures were useful because they revealed a consistent incompatibility pattern: code that reaches into the control and mutates validation or error state imperatively does not map cleanly to Signal Forms.&lt;/p&gt;

&lt;p&gt;That alone would already have made for a decent migration story. But the later review surfaced a more important lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Review Changed
&lt;/h2&gt;

&lt;p&gt;I ran an adversarial code review against the migration diff, and it found problems that the initial pipeline had missed entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example 1: Tests never ran.&lt;/strong&gt; In several worktrees, &lt;code&gt;npm install&lt;/code&gt; had not been executed. The &lt;code&gt;@types/jasmine&lt;/code&gt; package was missing, so &lt;code&gt;nx test&lt;/code&gt; could not run at all. The sub-agents noted this as a warning — and then reported SUCCESS anyway, because the TypeScript compiler showed no errors. This meant that a significant number of "successful" migrations were never actually test-verified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example 2: Silently changed email validation semantics.&lt;/strong&gt; In &lt;code&gt;AsmCreateCustomerFormComponent&lt;/code&gt;, the agent replaced Spartacus's &lt;code&gt;CustomFormValidators.emailValidator&lt;/code&gt; with Angular's built-in signal &lt;code&gt;email()&lt;/code&gt; validator. But these use different regular expressions. The Spartacus validator accepts addresses like &lt;code&gt;email@[123.123.123.123]&lt;/code&gt; and rejects &lt;code&gt;email@example&lt;/code&gt; — Angular's does the opposite. The migration silently changed which email addresses the form accepts, with no test catching the difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example 3: Validator side effects triggered at wrong time.&lt;/strong&gt; In &lt;code&gt;AsmBindCartComponent&lt;/code&gt;, a custom validator contained a side effect (&lt;code&gt;resetDeeplinkCart()&lt;/code&gt;) that cleared UI state. After migration to &lt;code&gt;SignalFormControl&lt;/code&gt;, the timing of validator execution changed. The review found that this could reset a deeplink alert immediately after it was set — a regression invisible in unit tests.&lt;/p&gt;

&lt;p&gt;These are not mechanical errors. They are semantic changes that require domain knowledge to detect. No import check or template scan would have caught them.&lt;/p&gt;

&lt;p&gt;The honest conclusion: &lt;strong&gt;automated transformation is not the same as validated correctness.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently Today
&lt;/h2&gt;

&lt;p&gt;If I were running this in a real client project, I would use the same core idea — playbook-driven agentic migration — but change the process significantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrate in waves of five.&lt;/strong&gt; Instead of pushing 44 targets through one autonomous pipeline, group them into waves of 4–5 components. Mix each wave deliberately: 2 simple components alongside 2–3 with known edge cases like template &lt;code&gt;required&lt;/code&gt; attributes or custom validators.&lt;/p&gt;

&lt;p&gt;After each wave, the orchestrator writes a summary and &lt;strong&gt;stops&lt;/strong&gt;. The human reviews the results, decides whether to update the playbook, and approves the next wave. This is a hard constraint in the orchestration protocol. Agents that are allowed to cross wave boundaries without human approval will repeat systematic mistakes across dozens of components before anyone notices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the worktrees alive.&lt;/strong&gt; Do not discard worktrees after the agent reports success. The worktree is the crime scene. Keep it around so you can inspect the diff, run tests manually, and trace what the agent actually did versus what it claimed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock unit tests before and after.&lt;/strong&gt; Run the full test suite &lt;em&gt;before&lt;/em&gt; the migration as a baseline. Run it again &lt;em&gt;after&lt;/em&gt;. If tests cannot run at all — missing dependencies, broken setup — the migration gets status &lt;code&gt;ABORT&lt;/code&gt;, not &lt;code&gt;SUCCESS&lt;/code&gt;. Green tests confirm syntactic correctness. They say nothing about semantic equivalence. That is where review starts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enforce hard constraints outside the prompt.&lt;/strong&gt; The orchestration protocol limits each sub-agent to three test runs after the migration. I enforced this through the prompt. Some agents ignored it. In a production workflow, I would wrap the agent invocation in a deterministic harness — a script that counts test executions externally and terminates the process after the limit. Prompting is a request. A wrapper script is a mechanism. Any constraint that the agent must not violate belongs outside the agent's control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a second model for adversarial review.&lt;/strong&gt; A different frontier model reading the migration diff with adversarial intent catches a different class of errors than the model that wrote the code. Tools like &lt;a href="https://github.com/openai/codex-plugin-cc" rel="noopener noreferrer"&gt;codex-plugin-cc&lt;/a&gt; or Windsurf's Codemaps can help here — the principle is what matters: structural overview and adversarial challenge from a separate perspective.&lt;/p&gt;

&lt;p&gt;In this migration, the adversarial review flagged two real issues the pipeline had missed: a validator side effect that could reset active-cart deeplink state on every template subscription, and an email validator replacement that silently changed which addresses the form accepts. Both were invisible to unit tests. Both would have shipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human review closes the loop.&lt;/strong&gt; After the adversarial model review, a senior developer reviews the wave. The human decides whether a semantic difference is acceptable, whether a validator change matches business intent, and whether the migration is truly done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The resulting workflow per wave:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agent transforms 4–5 components in isolated worktrees&lt;/li&gt;
&lt;li&gt;Unit tests gate syntactic correctness&lt;/li&gt;
&lt;li&gt;Second model challenges semantic equivalence&lt;/li&gt;
&lt;li&gt;Human reviews and decides&lt;/li&gt;
&lt;li&gt;Update the playbook with new edge cases&lt;/li&gt;
&lt;li&gt;Next wave&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Real Takeaway
&lt;/h2&gt;

&lt;p&gt;At scale, the problem is not transformation. &lt;strong&gt;The problem is verification.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The honest result of this experiment: agentic migration works — but only inside a structured process with explicit quality gates. Run it in small waves. Keep the evidence. Let the agent do the mechanical work. Let a second model challenge it. Let a human decide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent transforms. Second model challenges. Human decides.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the workflow. Not "fully autonomous migration." Not "human reviews everything manually." A layered process where each step catches what the previous one cannot.&lt;/p&gt;

&lt;p&gt;Context engineering &amp;gt; prompt engineering. The hard part was never writing a clever prompt. It was doing the first migrations manually, extracting the right rules, documenting the edge cases, deciding what counts as "done," and designing a process where speed and trust are not in conflict.&lt;/p&gt;

&lt;p&gt;The AI did not invent the strategy. A senior developer defined the migration model, the playbook, and the quality gates. The AI scaled the repetitive part. The review caught what scaling missed.&lt;/p&gt;

&lt;p&gt;That is where the real leverage is: not asking an agent to "do the migration," but designing a process that makes automation fast, inspectable, and safe.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This migration builds on &lt;a href="https://www.angulararchitects.io/blog/migrating-to-angular-signal-forms-interop-with-reactive-forms/" rel="noopener noreferrer"&gt;Manfred Steyer's blog post on SignalFormControl interop&lt;/a&gt;. The initial migration run is at &lt;a href="https://github.com/lutzleonhardt/spartacus/pull/1" rel="noopener noreferrer"&gt;github.com/lutzleonhardt/spartacus/pull/1&lt;/a&gt;. The refined wave-based orchestration protocol and migration logs are on the &lt;a href="https://github.com/lutzleonhardt/spartacus/tree/feature/signal-forms-migration-v2" rel="noopener noreferrer"&gt;signal-forms-migration-v2 branch&lt;/a&gt;, including the &lt;a href="https://github.com/lutzleonhardt/spartacus/blob/feature/signal-forms-migration-v2/SignalFormMigration/goal.md" rel="noopener noreferrer"&gt;full orchestration protocol&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>angular</category>
      <category>ai</category>
      <category>contextengineering</category>
      <category>refactoring</category>
    </item>
  </channel>
</rss>
