Skip to content
SunnyWriteUps
Go back

Angular Agentic Engineering From Spec to Ship

Edit page

Angular’s opinionated architecture—strict typing, DI trees, hierarchical injector scoping, standalone APIs, and signal-based reactivity—makes it uniquely well-suited for autonomous agents if you bind the agent within structural guardrails.


1. Ground Rules: The Modern Angular Context Baseline

LLMs frequently default to legacy Angular patterns (modules, ngOnInit subscriptions, zone-based change detection) unless explicitly bounded in your .agent/rules/angular.md or .cursorrules.

<!-- .agent/rules/angular.md -->
# Angular Modern Directives & Invariants

## Architecture Baseline
- Standalone components only (`imports: [...]`). Never generate `NgModule`.
- Reactivity: Use Angular Signals (`signal()`, `computed()`, `linkedSignal()`, `resource()`).
- Data inputs/outputs: Use signal inputs (`input()`, `input.required()`) and `output()`.
- Template flow: Use modern control flow (`@if`, `@for (item of items; track item.id)`, `@switch`, `@let`).
- Change Detection: `changeDetection: ChangeDetectionStrategy.OnPush` on every component.
- Injector pattern: Use the `inject(Service)` function; avoid constructor-based injection.

## Prohibitions
- DO NOT use RxJS `subscribe()` inside components; use `toSignal()` or `async` pipe.
- DO NOT use `any`; use strictly typed interfaces or `unknown` with narrowing.
- DO NOT touch files outside the designated feature directory.

2. Phase 1: Spec & Contract Scaffolding

Before generating templates or component classes, the agent must define Type Contracts, State Signatures, and Acceptance Tests.

┌────────────────────────────────────────────────────────┐
│ Phase 1: Contract-First Scaffolding                    │
│                                                        │
│  Feature Request                                       │
│         │                                              │
│         ▼                                              │
│  [types.ts]           ──> Zod Schemas + Domain Types   │
│         │                                              │
│         ▼                                              │
│  [feature.store.ts]   ──> Signal Store / State Contract│
│         │                                              │
│         ▼                                              │
│  [feature.spec.ts]    ──> Acceptance Test Harness      │
└────────────────────────────────────────────────────────┘

1. Types & Zod Schemas (feature.types.ts)

import { z } from 'zod';

export const UserSummarySchema = z.object({
  id: z.string().uuid(),
  displayName: z.string().min(2),
  role: z.enum(['admin', 'member', 'guest']),
  lastActive: z.string().datetime(),
});

export type UserSummary = z.infer<typeof UserSummarySchema>;

export interface UserListState {
  users: UserSummary[];
  filter: string;
  isLoading: boolean;
  selectedUserId: string | null;
}

2. State & Store Contract (user-list.store.ts)

Define the state machine and mutations upfront:

import { computed, inject, Injectable, signal } from '@angular/core';
import { UserSummary } from './feature.types';
import { UserApiService } from './user-api.service';

@Injectable()
export class UserListStore {
  private readonly api = inject(UserApiService);

  // State signals
  readonly users = signal<UserSummary[]>([]);
  readonly filter = signal<string>('');
  readonly isLoading = signal<boolean>(false);
  readonly selectedUserId = signal<string | null>(null);

  // Derived signals
  readonly filteredUsers = computed(() => {
    const q = this.filter().toLowerCase();
    return this.users().filter(u => u.displayName.toLowerCase().includes(q));
  });

  readonly selectedUser = computed(() => 
    this.users().find(u => u.id === this.selectedUserId()) ?? null
  );

  async loadUsers(): Promise<void> {
    this.isLoading.set(true);
    try {
      const data = await this.api.fetchUsers();
      this.users.set(data);
    } finally {
      this.isLoading.set(false);
    }
  }
}

3. Phase 2: Directed Component Implementation

With contracts locked, the agent implements the dumb/smart component boundary. Components focus exclusively on rendering and signal binding.

// src/app/features/users/user-list.component.ts
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
import { UserListStore } from './user-list.store';
import { UserCardComponent } from './ui/user-card.component';

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [UserCardComponent],
  providers: [UserListStore], // Scoped to component lifecycle
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="user-container">
      <header class="toolbar">
        <input 
          type="search" 
          placeholder="Filter members..."
          [value]="store.filter()" 
          (input)="onSearch($event)" />
      </header>

      @let loading = store.isLoading();
      @if (loading) {
        <div class="skeleton-loader" aria-busy="true">Loading users...</div>
      } @else {
        <section class="grid">
          @for (user of store.filteredUsers(); track user.id) {
            <app-user-card 
              [user]="user"
              [isSelected]="user.id === store.selectedUserId()"
              (selected)="store.selectedUserId.set($event)" />
          } @empty {
            <p class="empty-state">No matching users found.</p>
          }
        </section>
      }
    </div>
  `,
  styleUrl: './user-list.component.css'
})
export class UserListComponent implements OnInit {
  protected readonly store = inject(UserListStore);

  ngOnInit(): void {
    void this.store.loadUsers();
  }

  onSearch(event: Event): void {
    const value = (event.target as HTMLInputElement).value;
    this.store.filter.set(value);
  }
}

4. Phase 3: Legacy Angular Code & Migration Loops

When an agent works on legacy modules (NgModule, RxJS pipe soups, or zone-polluting components), enforce a characterization & strangler strategy:

[Legacy Component] ──> [Extract Characterization Tests] ──> [Wrap in Standalone Facade] ──> [Migrate to Signals]

Deterministic Migration Rules

Add a dedicated skill for legacy migration (.agent/skills/migrate-to-signals.md):

  1. Step 1: Test Invariant Guard: Run existing test suites. If tests do not exist, use the agent to write a snapshot test against the rendered DOM before altering code.
  2. Step 2: Signal Input Migration: Run the Angular CLI migration or prompt the agent to convert @Input() property: string to property = input<string>().
  3. Step 3: Observable Unwinding:

5. Phase 4: Deterministic Guardrail Hooks

Never let an agent commit or declare a task finished without passing automated AST and CLI checks. Use an orchestrator hook script:

#!/usr/bin/env bash
# .agent/hooks/verify-angular.sh
set -e

CHANGED_FILES=$(git diff --name-only HEAD)

echo "[*] Step 1: Prettier & Angular Template Formatting..."
npx prettier --write $CHANGED_FILES > /dev/null 2>&1

echo "[*] Step 2: ESLint with @angular-eslint..."
npx ng lint --fix || {
  echo "LINT_ERROR: Fix accessibility and architectural lint violations."
  exit 1
}

echo "[*] Step 3: Strict Angular Template Compilation (AOT)..."
# Runs full ngc compiler checks on templates and signal bindings
npx ng build --no-optimization --configuration=development || {
  echo "TEMPLATE_TYPE_ERROR: Angular template type-check failed."
  exit 1
}

echo "[*] Step 4: Component & Store Unit Tests..."
npx ng test --no-watch --browsers=ChromeHeadless || {
  echo "TEST_SUITE_FAILED: Fix broken regression tests."
  exit 1
}

echo "[✔] All Angular verification gates passed cleanly."

6. End-to-End Autonomous Pipeline

Workflow StageAgent Tool / InterfaceHuman / System Role
1. Spec ScopingRead design tickets & repo types via MCPHuman signs off on .agent/specs/feature/ contracts.
2. ScaffoldingGenerate store, types, and failing specVerified via tsc --noEmit.
3. ImplementationGenerate Standalone Component + Signal bindingsBounded strictly to feature directory.
4. VerificationRun verify-angular.sh hookScript returns compilation and template errors directly into the context window for auto-repair.
5. ShipGit branch + PR creation with architectural diffCI runs final production AOT build and bundle budget checks.

Edit page
Share this post on:

Next Post
How can coding agents be used productively in software development?

Comments