<?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>Forem: Lutz Leonhardt</title>
    <description>The latest articles on Forem by Lutz Leonhardt (@lutz_leonhardt).</description>
    <link>https://forem.com/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>Forem: Lutz Leonhardt</title>
      <link>https://forem.com/lutz_leonhardt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/lutz_leonhardt"/>
    <language>en</language>
    <item>
      <title>I stopped maintaining my GTD system. That's why it finally works.</title>
      <dc:creator>Lutz Leonhardt</dc:creator>
      <pubDate>Thu, 07 May 2026 15:03:28 +0000</pubDate>
      <link>https://forem.com/lutz_leonhardt/i-stopped-maintaining-my-gtd-system-thats-why-it-finally-works-mno</link>
      <guid>https://forem.com/lutz_leonhardt/i-stopped-maintaining-my-gtd-system-thats-why-it-finally-works-mno</guid>
      <description>&lt;p&gt;For something like fifteen years I tried to make Getting Things Done stick. Evernote, Everdo, OmniFocus, Things, Todoist, Notion — same pattern every time: read the book, build the inbox, schedule the weekly review, three weeks of discipline, then drift. Once the lists stop reflecting reality, you stop trusting them. Once you stop trusting them, you stop using them. The system dies, you blame yourself, and a year later you try the next app.&lt;/p&gt;

&lt;p&gt;The method was never the problem. The maintenance was.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqaciz369thhxqfl15uml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqaciz369thhxqfl15uml.png" alt="Screenshot of Everdo task manager: 79 items in Inbox, 13 in Waiting, 97 in Focus, dozens of unprocessed generic tasks listed below." width="800" height="1380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Inbox: 79. Focus: 97. Focus is supposed to be 3–5. This is what "I'll review it on Sunday" looks like after a year of Sundays.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sorting the inbox. Reviewing lists. Re-tagging things by context, energy, location, project. Filling in metadata that the system needs to be useful but that you, sitting on the couch at 9pm, do not want to fill in. The barrier to capture a task in a "proper" GTD app is absurdly high. And that's before the weekly review you're already three weeks behind on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The accidental hack that held
&lt;/h2&gt;

&lt;p&gt;A few months ago I tried something stupid. I made a flat folder in Obsidian — &lt;code&gt;inbox.md&lt;/code&gt;, &lt;code&gt;focus.md&lt;/code&gt;, &lt;code&gt;next-actions.md&lt;/code&gt;, &lt;code&gt;waiting.md&lt;/code&gt;, &lt;code&gt;someday-maybe.md&lt;/code&gt;, a &lt;code&gt;daily/&lt;/code&gt; directory — and a &lt;code&gt;CLAUDE.md&lt;/code&gt; next to them with the rules: where things live, how they move, what "sync" means, when to push back. Then I pointed Claude Cowork at the folder and just talked.&lt;/p&gt;

&lt;p&gt;That's it. No app. No plugin. No fancy schema. Markdown files plus a rulebook.&lt;/p&gt;

&lt;p&gt;The conversation looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"What's on for tomorrow?"&lt;/em&gt;&lt;br&gt;
&lt;em&gt;"New task: send the client quote, due Friday."&lt;/em&gt;&lt;br&gt;
&lt;em&gt;"Push the dentist thing to next week."&lt;/em&gt;&lt;br&gt;
&lt;em&gt;"Sync my daily note."&lt;/em&gt;&lt;br&gt;
&lt;em&gt;"It's Friday — time for the weekly review?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude moves the files, keeps the daily notes in sync, flags inconsistencies ("you have task X in Focus but it's not in today's plan — should I add it?"), and reminds me about the review. I haven't opened a task list manually in months. I just talk. The system stays alive without me feeling I have to keep it alive.&lt;/p&gt;

&lt;p&gt;The full ruleset is published. It's the &lt;a href="https://getkeppt.com/method/" rel="noopener noreferrer"&gt;method page on getkeppt.com&lt;/a&gt;. Drop the &lt;code&gt;CLAUDE.md&lt;/code&gt; into a folder, add the markdown files, point any decent agent at it. Works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual product is trust, not voice
&lt;/h2&gt;

&lt;p&gt;The obvious framing for this is "voice + AI + markdown." That framing is wrong, and I only saw it after a few weeks of real use.&lt;/p&gt;

&lt;p&gt;What makes this work is not voice. Voice is a delivery mechanism. What makes this work is that the system behaves like a &lt;em&gt;conservative bookkeeper&lt;/em&gt;, not a creative assistant. It moves files. It flags drift. It asks before it invents. If it ever silently misfiles a task or creatively interprets what I meant, the trust is gone, and a GTD system you don't trust is dead inventory. I've killed enough of those to know.&lt;/p&gt;

&lt;p&gt;So the design rule that fell out of the experience is simple: no creative interpretation, no silent edits, no "helpful" reorganization. Show the diff. Ask when unclear. Keep the user in the loop. The boring answer is the right answer. That single principle ends up shaping the architecture more than any model choice does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch, and why this is becoming an app
&lt;/h2&gt;

&lt;p&gt;Cowork is a developer tool. Without a Claude Code setup and a willingness to write a &lt;code&gt;CLAUDE.md&lt;/code&gt;, you can't reproduce any of this. That's a small audience for something that, for me at least, fixed a fifteen-year-old problem.&lt;/p&gt;

&lt;p&gt;So I'm turning it into an app — and finding out whether the thing that fixed my own system survives outside my own setup. &lt;strong&gt;One chat surface. Voice or text, whichever has less friction in the moment. GTD running in the engine room&lt;/strong&gt; — inbox triage, daily plan, weekly review, consistency checks — all maintained by the model, not by you. Tasks live as plain markdown that you can always open and read. No dashboards. No tagging UI. No second system to maintain.&lt;/p&gt;

&lt;p&gt;The pitch and the static prototype are on &lt;a href="https://getkeppt.com" rel="noopener noreferrer"&gt;getkeppt.com&lt;/a&gt;. The mockup on the landing page is exactly that: a mockup. The real app is being built now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building this in the open
&lt;/h2&gt;

&lt;p&gt;I'm shipping the build publicly — repo, specs, task log, prompts, what breaks. The roadmap is split into phases that each end on a real validation checkpoint, not a calendar date.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 (in progress):&lt;/strong&gt; a CLI that proves the prompts and the tool loop against a real Obsidian vault. No server. No auth. No payments. Just the engine, hardened against my own files and my own usage. If that works, the rest has a foundation. If it doesn't, no UI on top is going to save it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2:&lt;/strong&gt; Express backend, Angular + Capacitor mobile shell, voice input, hosted storage. The actual product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3:&lt;/strong&gt; everything I'm deliberately not doing yet.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/lutzleonhardt/keppt-app" rel="noopener noreferrer"&gt;github.com/lutzleonhardt/keppt-app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Beta list: &lt;a href="https://getkeppt.com" rel="noopener noreferrer"&gt;getkeppt.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agentic workflow behind the build is public too — the &lt;a href="https://github.com/lutzleonhardt/skill-kit-agentic-workflow" rel="noopener noreferrer"&gt;Skill Kit Adjutant&lt;/a&gt; — but that's a separate rabbit hole.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next post
&lt;/h2&gt;

&lt;p&gt;The next thing that breaks in a system like this is not the model. It's the prompt design for consistency. Specifically: how do you write rules that survive long conversations, parallel sessions, and the model's natural urge to be helpful when "helpful" is exactly what you don't want? That's Post #2.&lt;/p&gt;

&lt;p&gt;If you've ever bailed on a productivity system not because capture was hard, but because maintenance slowly drifted away from reality — I'm curious what kept you going, or what finally broke it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>gtd</category>
    </item>
    <item>
      <title>The Frankenstein Meeting Room: How to Stitch Angular, React, and Svelte Into One App</title>
      <dc:creator>Lutz Leonhardt</dc:creator>
      <pubDate>Wed, 06 May 2026 14:25:24 +0000</pubDate>
      <link>https://forem.com/lutz_leonhardt/-the-frankenstein-meeting-room-how-to-stitch-angular-react-and-svelte-into-one-app-351g</link>
      <guid>https://forem.com/lutz_leonhardt/-the-frankenstein-meeting-room-how-to-stitch-angular-react-and-svelte-into-one-app-351g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 1 of a series. The build follows in subsequent posts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq8eag7obcwxxpgn2g6tk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq8eag7obcwxxpgn2g6tk.png" alt="Hero — Frankenstein Meeting Room mockup" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three frontend frameworks in the same business domain is the rule, not the exception. One team adopted Angular years ago. Another fell in love with React. The M&amp;amp;A team brought a Vue app along. The standard answer is &lt;em&gt;Rewrite&lt;/em&gt; — years, millions, often failing.&lt;/p&gt;

&lt;p&gt;There is another answer: let them live together. This post walks through the architectural spec for a small but real demo that does exactly that — Angular, React, and Svelte inside a single workspace, sharing one business context. Call it Frankenstein-Driven Architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Frankenstein Reality
&lt;/h2&gt;

&lt;p&gt;Heterogeneity in enterprise frontends isn't a temporary mess to be cleaned up. It's a permanent condition. Acquisitions bring new stacks. Teams pick what they know. Industry tides shift; once-favored frameworks fall out of fashion long before the apps written in them stop earning money.&lt;/p&gt;

&lt;p&gt;The rewrite-first culture treats this as a problem to be eliminated. Two years, ten engineers, one framework to rule them all. By the time the rewrite ships, the dominant framework has changed again, the original team has left, and the business questions whether any of it was worth doing.&lt;/p&gt;

&lt;p&gt;The alternative is to design &lt;em&gt;with&lt;/em&gt; the heterogeneity instead of against it. Stop asking &lt;em&gt;how do we make everything one framework?&lt;/em&gt; and start asking &lt;em&gt;how do we let multiple frameworks share one product?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architectural Principle
&lt;/h2&gt;

&lt;p&gt;The whole spec rests on one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Remote owns capability. Host owns business context and persistence.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each remote is responsible for &lt;em&gt;what it does best&lt;/em&gt; — drawing, diagramming, reporting, scheduling. The host is responsible for &lt;em&gt;what the business is about&lt;/em&gt; — meetings, customers, orders, claims. Remotes do not own state. They do not own routing. They do not own the user. They render a capability when handed a context, and they emit changes when the user does something.&lt;/p&gt;

&lt;p&gt;In this demo, the Angular host owns the meeting context. The React remote owns whiteboarding via Excalidraw. The Svelte remote owns diagram editing via Mermaid. The principle scales: replace whiteboard with reporting, diagrams with scheduling, the structure stays the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Islands, Not Components
&lt;/h2&gt;

&lt;p&gt;When the host runs Angular and a remote runs React, there is no shared component model, no shared hook system, no shared reactivity. There is no React-component-inside-Angular-template trick that survives contact with reality.&lt;/p&gt;

&lt;p&gt;So each remote is a complete, self-contained application — an island. What it exposes to the host is not a React component or a Svelte component, but a Custom Element that wraps the entire app and boots it on mount.&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WhiteboardRemote&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;HTMLElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;connectedCallback&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;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRoot&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;App&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="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;disconnectedCallback&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;root&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;unmount&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="nx"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;whiteboard-remote&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;WhiteboardRemote&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host then consumes the remote like any other DOM element:&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;whiteboard-remote&amp;gt;&amp;lt;/whiteboard-remote&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Web Components are the boundary because Web Components are a browser standard. Angular, React, Svelte, Vue — all four know how to render and listen to a Custom Element. The browser, not the framework, owns the integration contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Channel, Four Events
&lt;/h2&gt;

&lt;p&gt;If the host is the only orchestrator, communication runs through one channel: an event bus. No initial state via attributes. No properties that have to be set before mount. Remotes are &lt;em&gt;dumb on mount&lt;/em&gt; — they know nothing until the bus tells them.&lt;/p&gt;

&lt;p&gt;Four events cover the entire cross-framework communication:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;context:request&lt;/code&gt; — Remote → Host, &lt;em&gt;"I just mounted, what's the current context?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;event:selected&lt;/code&gt; — Host → Remotes, &lt;em&gt;"the user is now looking at meeting X, here's its data"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;drawing:changed&lt;/code&gt; — React → Host, &lt;em&gt;"the whiteboard changed, here's the new payload"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;diagram:changed&lt;/code&gt; — Svelte → Host, &lt;em&gt;"the diagram changed, here's the new source"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bus itself is fifteen lines of TypeScript wrapping a &lt;code&gt;globalThis&lt;/code&gt;-pinned &lt;code&gt;EventTarget&lt;/code&gt;. No library. No broker. The wrapper provides typed &lt;code&gt;emit&lt;/code&gt; and &lt;code&gt;on&lt;/code&gt; so neither end has to remember which payload belongs to which event.&lt;/p&gt;

&lt;p&gt;The flow for the most important interaction — &lt;em&gt;the user clicks a meeting in the calendar and both remotes update&lt;/em&gt; — is one round-trip:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2ec4z3k02lgyo4nrt5h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2ec4z3k02lgyo4nrt5h.png" alt=" " width="800" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The host is the hub, the remotes are spokes. Spokes never talk to each other directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native Federation as the Vehicle
&lt;/h2&gt;

&lt;p&gt;The plumbing that lets the host actually load the React and Svelte bundles at runtime is &lt;strong&gt;Native Federation v4&lt;/strong&gt; — Manfred Steyer's framework-agnostic, ESM- and import-map-native successor to Webpack Module Federation.&lt;/p&gt;

&lt;p&gt;Two adapters do the work. The Angular adapter (&lt;code&gt;@angular-architects/native-federation-v4&lt;/code&gt;) wires the host: a &lt;code&gt;dynamic-host&lt;/code&gt; schematic generates a two-phase bootstrap (init federation first, bootstrap Angular second), a &lt;code&gt;federation.manifest.json&lt;/code&gt; listing remote URLs, and a builder that splits shared dependencies into separate chunks. The esbuild adapter (&lt;code&gt;@softarc/native-federation-esbuild&lt;/code&gt;) builds the remotes: a small &lt;code&gt;build.mjs&lt;/code&gt; script drives &lt;code&gt;runEsBuildBuilder&lt;/code&gt; and produces a &lt;code&gt;remoteEntry.json&lt;/code&gt; plus its bundle. No Vite involved — the official remote adapter is esbuild-based and framework-agnostic.&lt;/p&gt;

&lt;p&gt;The runtime is the &lt;strong&gt;Orchestrator&lt;/strong&gt; (&lt;code&gt;@softarc/native-federation-orchestrator&lt;/code&gt;), v4's recommended replacement for the classic runtime. It does semver-aware version resolution for shared dependencies, caches &lt;code&gt;remoteEntry.json&lt;/code&gt; data across reloads, and handles share scopes for multi-team setups.&lt;/p&gt;

&lt;p&gt;The same machinery is what makes this pattern interesting for &lt;strong&gt;migration&lt;/strong&gt;. A team running an Angular monolith can carve a new feature out as a federated remote in any framework — React, Svelte, whatever the team picks — without touching the existing app. Old code keeps shipping, new capabilities arrive as islands. There is no all-or-nothing rewrite gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Demo
&lt;/h2&gt;

&lt;p&gt;The demo is deliberately small. A meeting room app where the user picks a meeting from a calendar, and the meeting opens with two artifacts side by side: a whiteboard sketch (React + Excalidraw) and a sequence diagram (Svelte + Mermaid). Both are real, iconic open-source applications, embedded as full islands.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9bqvm1kivrfl3mvflwwl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9bqvm1kivrfl3mvflwwl.png" alt="Mockup of the demo layout" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three columns. The Angular calendar (Schedule-X) on the left. The two remotes stacked in the middle. Meeting details and a live event-bus log on the right. Click a meeting, both remotes load that meeting's data. Draw on the whiteboard, the host persists. Switch to a different meeting, both remotes follow the context. Open DevTools and the Network tab shows three frameworks loaded — Angular, React, Svelte — talking through one event bus.&lt;/p&gt;

&lt;p&gt;The full spec is in the repo: &lt;a href="https://github.com/lutzleonhardt/FrankensteinMeetingRoom/blob/main/specs/SPEC.md" rel="noopener noreferrer"&gt;https://github.com/lutzleonhardt/FrankensteinMeetingRoom/blob/main/specs/SPEC.md&lt;/a&gt;. Read it if you want the actual &lt;code&gt;Meeting&lt;/code&gt; type, the &lt;code&gt;MeetingService&lt;/code&gt; skeleton with stale-update guards, the &lt;code&gt;bus.ts&lt;/code&gt; wrapper, the &lt;code&gt;federation.config.mjs&lt;/code&gt; for both remotes, the workspace layout, and the milestones the build will follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Notes from the Spec'ing Process
&lt;/h2&gt;

&lt;p&gt;A spec rarely arrives clean on the first pass. Two corrections from this one are worth sharing because they were genuine surprises during the design conversation, not lessons from a textbook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Vite Adapter" doesn't exist.&lt;/strong&gt; Going in, the assumption was that Vite-based remotes were the standard path — Vite is everyone's modern build tool, after all. Reading the actual Native Federation docs revealed that the official adapter is &lt;code&gt;@softarc/native-federation-esbuild&lt;/code&gt;. Vite is &lt;em&gt;not&lt;/em&gt; officially supported. The adapter is framework-agnostic and runs from a hand-written &lt;code&gt;build.mjs&lt;/code&gt;, which initially feels backward but turns out to be cleaner: no Vite-Federation interop magic, no plugin ecosystem assumptions, just esbuild plus your framework's source-transform plugin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One channel, not two.&lt;/strong&gt; The first instinct was to send initial meeting data via Custom Element properties (the standard Web Components idiom) and use the bus only for ongoing changes. Two channels, two mental models, two places to look when something doesn't render. The spec collapsed this into a single channel: the bus carries everything, including the initial context that a freshly-mounted remote requests via &lt;code&gt;context:request&lt;/code&gt;. The remotes become dumber, the architecture clearer, and the workshop pitch tightens to one line: &lt;em&gt;"the only thing crossing the framework boundary is a bus event."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This post is part 1. The repo will host the spec and the build, milestone by milestone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;M1&lt;/strong&gt; — Workspace scaffolded, Angular host with empty federation manifest running on &lt;code&gt;:4200&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;M2&lt;/strong&gt; — Calendar, meeting service with persistence, three-column layout, event-bus log&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;M3&lt;/strong&gt; — React Whiteboard remote — first federation stitching live&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;M4&lt;/strong&gt; — Svelte Mermaid remote — both remotes federated, the money-shot becomes recordable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;M5&lt;/strong&gt; — Polish, README, optional CRUD niceties&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each milestone produces a usable artifact you can stop at and demo. The next post will follow M1 + M2 — the host shell, why the two-phase bootstrap matters, and what &lt;em&gt;„the host is also a &lt;code&gt;remoteEntry.json&lt;/code&gt;"&lt;/em&gt; actually means.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/lutzleonhardt/FrankensteinMeetingRoom" rel="noopener noreferrer"&gt;https://github.com/lutzleonhardt/FrankensteinMeetingRoom&lt;/a&gt;&lt;br&gt;
Spec: &lt;a href="https://github.com/lutzleonhardt/FrankensteinMeetingRoom/blob/main/specs/SPEC.md" rel="noopener noreferrer"&gt;https://github.com/lutzleonhardt/FrankensteinMeetingRoom/blob/main/specs/SPEC.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your enterprise frontend looks more like a museum than a monolith, this is the pattern that makes that a feature, not a problem.&lt;/p&gt;

</description>
      <category>angular</category>
      <category>architecture</category>
      <category>frontend</category>
      <category>react</category>
    </item>
    <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://forem.com/lutz_leonhardt/i-used-ai-agents-to-migrate-44-angular-components-the-review-changed-my-mind-4pop</link>
      <guid>https://forem.com/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>
