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):
- 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.
- Step 2: Signal Input Migration: Run the Angular CLI migration or prompt the agent to convert
@Input() property: stringtoproperty = input<string>(). - Step 3: Observable Unwinding:
- Convert service stream queries from
this.service.data$.subscribe(...)toreadonly data = toSignal(this.service.data$, { initialValue: [] }). - Eliminate manual
unsubscribe()andtakeUntilDestroyed().
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 Stage | Agent Tool / Interface | Human / System Role |
|---|---|---|
| 1. Spec Scoping | Read design tickets & repo types via MCP | Human signs off on .agent/specs/feature/ contracts. |
| 2. Scaffolding | Generate store, types, and failing spec | Verified via tsc --noEmit. |
| 3. Implementation | Generate Standalone Component + Signal bindings | Bounded strictly to feature directory. |
| 4. Verification | Run verify-angular.sh hook | Script returns compilation and template errors directly into the context window for auto-repair. |
| 5. Ship | Git branch + PR creation with architectural diff | CI runs final production AOT build and bundle budget checks. |