2026 03 10 Generate Edit Deploy Pipeline

Documentation for Plans

Generate → Edit → Deploy Pipeline Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build the complete pipeline from "user describes a platform" → Vitto generates high-quality code → user edits in Studio (backed by GitLab) → one-click deploy to Cloudflare.

Architecture: Code generation produces a full NX app shell + modules. All generated code is committed to a GitLab repo (feature branch + draft MR). Vitto Studio loads files directly from GitLab (not local disk). Edits in Studio commit back to GitLab. Deploy button triggers Cloudflare Pages direct upload from GitLab source.

Tech Stack: Angular 20 (standalone, signals, OnPush), NestJS backends, GitLab REST API v4, Cloudflare Pages Direct Upload API, R2 storage, RxJS, SSE streaming.

FUTURE: Local dev mode — For power users/dev team, add optional local file sync (SAVE_GENERATED_LOCALLY=true + file watcher → GitLab push). Deferred to avoid overcomplicating the primary cloud-first flow.


Chunk 1: Fix Code Generation Quality

The scaffold fallback (generateScaffoldFiles() in module-gen-phase.executor.ts) produces code that violates every rule in implement.prompt.md. When the LLM fails or times out, users get broken modules. The scaffold MUST match the LLM prompt's golden examples.

Task 1.1: Fix Model Generation — Add BaseEntity

Files:

In generateScaffoldFiles(), around line 1283-1287, change the model generation:

// BEFORE (line 1287):
content: `${statusType}export interface ${entityClass} {\n${fieldLines.join('\n')}\n}\n`,

// AFTER:
content: `import { BaseEntity } from '@libs-core/shared-models';\n\n${statusType}export interface ${entityClass} extends BaseEntity {\n${fieldLines.join('\n')}\n}\n`,

Remove lines 1274-1276 — BaseEntity already provides these:

// REMOVE these 3 lines:
if (!reservedFields.has('id')) fieldLines.unshift('  id: string;');
if (!reservedFields.has('createdAt')) fieldLines.push('  createdAt: string;');
if (!reservedFields.has('updatedAt')) fieldLines.push('  updatedAt: string;');

Run: cd APIs/backend-vitto-api && npx tsc --noEmit --pretty 2>&1 | head -30 Expected: No errors related to model generation

git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.1] fix: scaffold models extend BaseEntity instead of manual id/dates"

Task 1.2: Fix Service Generation — Extend BaseCrudApiService

Files:

Replace the service generation block (lines 1312-1354) with:

files.push({
  moduleName: slug,
  category,
  path: `src/lib/services/${slug}-api.service.ts`,
  content: [
    `import { Injectable, inject } from '@angular/core';`,
    `import { Observable } from 'rxjs';`,
    `import { map } from 'rxjs';`,
    `import { BaseCrudApiService, CrudPaginatedResult } from '@libs-core/module-base';`,
    `import { PaginationRequest } from '@libs-core/shared-models';`,
    `import { UniverseDataService } from '@libs-foundational/core-angular';`,
    primaryEntity ? `import { ${primaryClass} } from '../models/${primarySlug}.model';` : '',
    ``,
    `@Injectable({ providedIn: 'root' })`,
    `export class ${className}ApiService extends BaseCrudApiService<${primaryClass}> {`,
    `  protected readonly baseUrl = '';`,
    `  protected readonly entityName = '${primaryClass}';`,
    `  private readonly dataService = inject(UniverseDataService);`,
    `  private readonly collectionName = '${this.pluralize(primarySlug)}';`,
    ``,
    `  override getList(params?: PaginationRequest): Observable<CrudPaginatedResult<${primaryClass}>> {`,
    `    return this.dataService.query<${primaryClass}>(this.collectionName, {`,
    `      search: params?.search,`,
    `      page: params?.page,`,
    `      pageSize: params?.limit,`,
    `    }).pipe(`,
    `      map(res => ({ items: res.data, totalItems: res.total ?? res.data.length }))`,
    `    );`,
    `  }`,
    ``,
    `  override getById(id: string): Observable<${primaryClass}> {`,
    `    return this.dataService.read<${primaryClass}>(this.collectionName, id).pipe(`,
    `      map(entity => {`,
    `        if (!entity) throw new Error('${primaryClass} not found');`,
    `        return entity;`,
    `      })`,
    `    );`,
    `  }`,
    ``,
    `  override create(data: Partial<${primaryClass}>): Observable<${primaryClass}> {`,
    `    return this.dataService.create<${primaryClass}>(this.collectionName, data as ${primaryClass}).pipe(`,
    `      map(res => res.data as ${primaryClass})`,
    `    );`,
    `  }`,
    ``,
    `  override update(id: string, data: Partial<${primaryClass}>): Observable<${primaryClass}> {`,
    `    return this.dataService.update<${primaryClass}>(this.collectionName, id, data as ${primaryClass}).pipe(`,
    `      map(() => ({ ...data, id } as ${primaryClass}))`,
    `    );`,
    `  }`,
    ``,
    `  override delete(id: string): Observable<void> {`,
    `    return this.dataService.delete(this.collectionName, id).pipe(map(() => void 0));`,
    `  }`,
    `}`,
    ``,
  ].filter(l => l !== undefined).join('\n'),
});

Change the barrel export filename from ${slug}.service to ${slug}-api.service:

// Update the services/index.ts generation (around line 1361):
content: `export * from './${slug}-api.service';\n`,

Check if this.pluralize() exists on the class. If not, add:

private pluralize(slug: string): string {
  if (slug.endsWith('s')) return slug;
  if (slug.endsWith('y')) return slug.slice(0, -1) + 'ies';
  return slug + 's';
}

Run: cd APIs/backend-vitto-api && npx tsc --noEmit --pretty 2>&1 | head -30 Expected: No errors

git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.2] fix: scaffold services extend BaseCrudApiService with proper CRUD"

Task 1.3: Fix List Component — Use useCrudList Composable

Files:

Replace the list component TS generation (lines 1365-1433) with the golden example from implement.prompt.md:

files.push({
  moduleName: slug,
  category,
  path: `src/lib/components/${primarySlug}-list/${primarySlug}-list.component.ts`,
  content: [
    `import { Component, ChangeDetectionStrategy, inject } from '@angular/core';`,
    `import { CommonModule } from '@angular/common';`,
    `import { forkJoin, map } from 'rxjs';`,
    `import { useCrudList } from '@libs-core/module-base';`,
    `import { GgsCrudListShellComponent } from '@libs-core/ui-framework';`,
    `import { ${className}ApiService } from '../../services/${slug}-api.service';`,
    primaryEntity ? `import type { ${primaryClass} } from '../../models/${primarySlug}.model';` : '',
    ``,
    `@Component({`,
    `  selector: 'mod-${primarySlug}-list',`,
    `  standalone: true,`,
    `  imports: [CommonModule, GgsCrudListShellComponent],`,
    `  changeDetection: ChangeDetectionStrategy.OnPush,`,
    `  templateUrl: './${primarySlug}-list.component.html',`,
    `  styleUrl: './${primarySlug}-list.component.scss',`,
    `})`,
    `export class ${primaryClass}ListComponent {`,
    `  private readonly api = inject(${className}ApiService);`,
    ``,
    `  readonly crud = useCrudList<${primaryClass}>({`,
    `    loadFn: (req) => this.api.getList(req),`,
    `    basePath: '/${slug}',`,
    `    entityName: '${primaryClass}',`,
    `    deleteFn: (ids) => forkJoin(ids.map(id => this.api.delete(id))).pipe(map(() => void 0)),`,
    `  });`,
    `}`,
    ``,
  ].filter(l => l !== undefined).join('\n'),
});

Replace the list HTML generation (lines 1436-1505) with:

files.push({
  moduleName: slug,
  category,
  path: `src/lib/components/${primarySlug}-list/${primarySlug}-list.component.html`,
  content: `<ggs-crud-list-shell [crud]="crud" />\n`,
});

Replace the verbose SCSS with minimal styling (the shell component handles layout):

files.push({
  moduleName: slug,
  category,
  path: `src/lib/components/${primarySlug}-list/${primarySlug}-list.component.scss`,
  content: `:host { display: block; padding: 1.5rem; }\n`,
});

Run: cd APIs/backend-vitto-api && npx tsc --noEmit --pretty 2>&1 | head -30

git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.3] fix: scaffold list component uses useCrudList + GgsCrudListShellComponent"

Task 1.4: Fix Routes — Add authGuard

Files:

Replace the routes generation:

files.push({
  moduleName: slug,
  category,
  path: `src/lib/${slug}.routes.ts`,
  content: [
    `import { Routes } from '@angular/router';`,
    `import { authGuard } from '@libs-core/security';`,
    ``,
    `export const ${routesConstName}: Routes = [`,
    `  {`,
    `    path: '',`,
    `    canActivate: [authGuard],`,
    `    loadComponent: () => import('./components/${primarySlug}-list/${primarySlug}-list.component')`,
    `      .then(m => m.${primaryClass}ListComponent),`,
    `    data: { title: '${displayName}', breadcrumb: '${displayName}' },`,
    `  },`,
    `  {`,
    `    path: 'create',`,
    `    canActivate: [authGuard],`,
    `    loadComponent: () => import('./components/${primarySlug}-form/${primarySlug}-form.component')`,
    `      .then(m => m.${primaryClass}FormComponent),`,
    `    data: { title: 'Create ${displayName}', breadcrumb: 'Create', mode: 'create' },`,
    `  },`,
    `  {`,
    `    path: ':id',`,
    `    canActivate: [authGuard],`,
    `    loadComponent: () => import('./components/${primarySlug}-detail/${primarySlug}-detail.component')`,
    `      .then(m => m.${primaryClass}DetailComponent),`,
    `    data: { title: '${displayName} Details', breadcrumb: 'Details' },`,
    `  },`,
    `  {`,
    `    path: ':id/edit',`,
    `    canActivate: [authGuard],`,
    `    loadComponent: () => import('./components/${primarySlug}-form/${primarySlug}-form.component')`,
    `      .then(m => m.${primaryClass}FormComponent),`,
    `    data: { title: 'Edit ${displayName}', breadcrumb: 'Edit', mode: 'edit' },`,
    `  },`,
    `];`,
    ``,
  ].join('\n'),
});
git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.4] fix: scaffold routes include authGuard on all paths"

Task 1.5: Fix Domain Events Generation

Files:

Add after the routes block (~line 1552):

// --- Domain Events ---
if (primaryEntity) {
  files.push({
    moduleName: slug,
    category,
    path: `src/lib/events/${primarySlug}.events.ts`,
    content: [
      `import { DomainEvent } from '@libs-core/event-system';`,
      `import { ${primaryClass} } from '../models/${primarySlug}.model';`,
      ``,
      `export class ${primaryClass}CreatedEvent extends DomainEvent<{ ${primaryCamel}: ${primaryClass} }> {`,
      `  override readonly eventName = '${slug}.${primarySlug}.created';`,
      `}`,
      ``,
      `export class ${primaryClass}UpdatedEvent extends DomainEvent<{ ${primaryCamel}: ${primaryClass} }> {`,
      `  override readonly eventName = '${slug}.${primarySlug}.updated';`,
      `}`,
      ``,
      `export class ${primaryClass}DeletedEvent extends DomainEvent<{ id: string }> {`,
      `  override readonly eventName = '${slug}.${primarySlug}.deleted';`,
      `}`,
      ``,
    ].join('\n'),
  });
}

Update the src/index.ts barrel to include events:

// In the barrel generation (~line 1624):
content: [
  `export * from './lib/${slug}.module';`,
  `export * from './lib/models';`,
  `export * from './lib/services';`,
  `export * from './lib/${slug}.routes';`,
  primaryEntity ? `export * from './lib/events/${primarySlug}.events';` : '',
  ``,
].filter(Boolean).join('\n'),
git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.5] feat: scaffold generates domain events for CRUD operations"

Task 1.6: Add Detail + Form Component Scaffolds

Currently the scaffold only generates a list component. Detail and Form are referenced in routes but never generated — guaranteed 404 at runtime.

Files:

Add after the list component block:

// --- Detail Component ---
files.push({
  moduleName: slug,
  category,
  path: `src/lib/components/${primarySlug}-detail/${primarySlug}-detail.component.ts`,
  content: [
    `import { Component, ChangeDetectionStrategy, inject } from '@angular/core';`,
    `import { CommonModule } from '@angular/common';`,
    `import { RouterModule } from '@angular/router';`,
    `import { useCrudDetail } from '@libs-core/module-base';`,
    `import { ${className}ApiService } from '../../services/${slug}-api.service';`,
    primaryEntity ? `import type { ${primaryClass} } from '../../models/${primarySlug}.model';` : '',
    ``,
    `@Component({`,
    `  selector: 'mod-${primarySlug}-detail',`,
    `  standalone: true,`,
    `  imports: [CommonModule, RouterModule],`,
    `  changeDetection: ChangeDetectionStrategy.OnPush,`,
    `  templateUrl: './${primarySlug}-detail.component.html',`,
    `  styleUrl: './${primarySlug}-detail.component.scss',`,
    `})`,
    `export class ${primaryClass}DetailComponent {`,
    `  private readonly api = inject(${className}ApiService);`,
    ``,
    `  readonly detail = useCrudDetail<${primaryClass}>({`,
    `    loadFn: (id) => this.api.getById(id),`,
    `    basePath: '/${slug}',`,
    `    entityName: '${primaryClass}',`,
    `  });`,
    `}`,
    ``,
  ].filter(l => l !== undefined).join('\n'),
});

files.push({
  moduleName: slug,
  category,
  path: `src/lib/components/${primarySlug}-detail/${primarySlug}-detail.component.html`,
  content: [
    `<div class="detail-container">`,
    `  @if (detail.loading()) {`,
    `    <div class="loading">Loading...</div>`,
    `  }`,
    `  @if (detail.error(); as err) {`,
    `    <div class="error">{{ err }}</div>`,
    `  }`,
    `  @if (detail.entity(); as item) {`,
    `    <div class="detail-header">`,
    `      <h2>{{ item['name'] || item.id }}</h2>`,
    `      <a [routerLink]="['edit']" class="btn btn-primary">Edit</a>`,
    `    </div>`,
    `    <div class="detail-body">`,
    `      <pre>{{ item | json }}</pre>`,
    `    </div>`,
    `  }`,
    `</div>`,
    ``,
  ].join('\n'),
});

files.push({
  moduleName: slug,
  category,
  path: `src/lib/components/${primarySlug}-detail/${primarySlug}-detail.component.scss`,
  content: `:host { display: block; padding: 1.5rem; }\n.detail-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }\n`,
});
// --- Form Component ---
files.push({
  moduleName: slug,
  category,
  path: `src/lib/components/${primarySlug}-form/${primarySlug}-form.component.ts`,
  content: [
    `import { Component, ChangeDetectionStrategy, inject } from '@angular/core';`,
    `import { CommonModule } from '@angular/common';`,
    `import { ReactiveFormsModule } from '@angular/forms';`,
    `import { useCrudForm } from '@libs-core/module-base';`,
    `import { ${className}ApiService } from '../../services/${slug}-api.service';`,
    primaryEntity ? `import type { ${primaryClass} } from '../../models/${primarySlug}.model';` : '',
    ``,
    `@Component({`,
    `  selector: 'mod-${primarySlug}-form',`,
    `  standalone: true,`,
    `  imports: [CommonModule, ReactiveFormsModule],`,
    `  changeDetection: ChangeDetectionStrategy.OnPush,`,
    `  templateUrl: './${primarySlug}-form.component.html',`,
    `  styleUrl: './${primarySlug}-form.component.scss',`,
    `})`,
    `export class ${primaryClass}FormComponent {`,
    `  private readonly api = inject(${className}ApiService);`,
    ``,
    `  readonly form = useCrudForm<${primaryClass}>({`,
    `    loadFn: (id) => this.api.getById(id),`,
    `    createFn: (data) => this.api.create(data),`,
    `    updateFn: (id, data) => this.api.update(id, data),`,
    `    basePath: '/${slug}',`,
    `    entityName: '${primaryClass}',`,
    `  });`,
    `}`,
    ``,
  ].filter(l => l !== undefined).join('\n'),
});

files.push({
  moduleName: slug,
  category,
  path: `src/lib/components/${primarySlug}-form/${primarySlug}-form.component.html`,
  content: [
    `<div class="form-container">`,
    `  <h2>{{ form.isEditMode() ? 'Edit' : 'Create' }} ${displayName}</h2>`,
    `  @if (form.loading()) {`,
    `    <div class="loading">Loading...</div>`,
    `  }`,
    `  @if (form.error(); as err) {`,
    `    <div class="error">{{ err }}</div>`,
    `  }`,
    `  <form [formGroup]="form.formGroup()" (ngSubmit)="form.submit()">`,
    `    <!-- Form fields will be generated based on entity spec -->`,
    `    <div class="form-actions">`,
    `      <button type="button" class="btn btn-secondary" (click)="form.cancel()">Cancel</button>`,
    `      <button type="submit" class="btn btn-primary" [disabled]="form.saving()">`,
    `        {{ form.saving() ? 'Saving...' : (form.isEditMode() ? 'Update' : 'Create') }}`,
    `      </button>`,
    `    </div>`,
    `  </form>`,
    `</div>`,
    ``,
  ].join('\n'),
});

files.push({
  moduleName: slug,
  category,
  path: `src/lib/components/${primarySlug}-form/${primarySlug}-form.component.scss`,
  content: `:host { display: block; padding: 1.5rem; }\n.form-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1.5rem; }\n`,
});
cd APIs/backend-vitto-api && npx tsc --noEmit --pretty 2>&1 | head -30
git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.6] feat: scaffold generates detail + form components with composables"

Task 1.7: Add Vitto Chat Context Instructions to Generated Modules

This is the groundbreaking feature. Each generated module includes a .vitto/instructions.json file that tells Vitto what it can do when the user clicks any area of the code.

Files:

Add to generateScaffoldFiles(), inside the module loop:

// --- Vitto Chat Context Instructions ---
const vittoInstructions = {
  moduleId: slug,
  moduleName: displayName,
  version: '1.0.0',
  editableAreas: [
    {
      id: `${slug}:model`,
      files: [`src/lib/models/${primarySlug}.model.ts`],
      description: `Entity model for ${displayName}`,
      vittoActions: [
        { action: 'add-field', label: 'Add a new field', prompt: `Add a new field to the ${primaryClass} entity. Ask the user what field name and type.` },
        { action: 'add-validation', label: 'Add validation rules', prompt: `Add validation rules to ${primaryClass} fields.` },
        { action: 'add-computed', label: 'Add computed property', prompt: `Add a computed/derived property to ${primaryClass}.` },
      ],
    },
    {
      id: `${slug}:list`,
      files: [`src/lib/components/${primarySlug}-list/${primarySlug}-list.component.ts`, `src/lib/components/${primarySlug}-list/${primarySlug}-list.component.html`],
      description: `List view for ${displayName}`,
      vittoActions: [
        { action: 'add-column', label: 'Add table column', prompt: `Add a new column to the ${primaryClass} list table.` },
        { action: 'add-filter', label: 'Add filter', prompt: `Add a filter to the ${primaryClass} list.` },
        { action: 'add-bulk-action', label: 'Add bulk action', prompt: `Add a bulk action (e.g., export, delete selected) to the ${primaryClass} list.` },
        { action: 'customize-layout', label: 'Change layout', prompt: `Change the list layout (table, cards, grid) for ${primaryClass}.` },
      ],
    },
    {
      id: `${slug}:detail`,
      files: [`src/lib/components/${primarySlug}-detail/${primarySlug}-detail.component.ts`, `src/lib/components/${primarySlug}-detail/${primarySlug}-detail.component.html`],
      description: `Detail view for ${displayName}`,
      vittoActions: [
        { action: 'add-section', label: 'Add detail section', prompt: `Add a new section to the ${primaryClass} detail view.` },
        { action: 'add-related', label: 'Show related data', prompt: `Show related entities on the ${primaryClass} detail page.` },
        { action: 'add-action', label: 'Add action button', prompt: `Add an action button to the ${primaryClass} detail page.` },
      ],
    },
    {
      id: `${slug}:form`,
      files: [`src/lib/components/${primarySlug}-form/${primarySlug}-form.component.ts`, `src/lib/components/${primarySlug}-form/${primarySlug}-form.component.html`],
      description: `Create/Edit form for ${displayName}`,
      vittoActions: [
        { action: 'add-field', label: 'Add form field', prompt: `Add a new form field to the ${primaryClass} form.` },
        { action: 'add-validation', label: 'Add form validation', prompt: `Add validation to the ${primaryClass} form fields.` },
        { action: 'conditional-field', label: 'Add conditional field', prompt: `Add a conditional field that shows/hides based on another field value.` },
        { action: 'add-step', label: 'Convert to wizard', prompt: `Convert the ${primaryClass} form into a multi-step wizard.` },
      ],
    },
    {
      id: `${slug}:service`,
      files: [`src/lib/services/${slug}-api.service.ts`],
      description: `API service for ${displayName}`,
      vittoActions: [
        { action: 'add-method', label: 'Add API method', prompt: `Add a custom API method to the ${className}ApiService.` },
        { action: 'add-caching', label: 'Add caching', prompt: `Add response caching to the ${className}ApiService methods.` },
      ],
    },
    {
      id: `${slug}:routes`,
      files: [`src/lib/${slug}.routes.ts`],
      description: `Route configuration for ${displayName}`,
      vittoActions: [
        { action: 'add-route', label: 'Add new page', prompt: `Add a new route/page to the ${displayName} module.` },
        { action: 'add-guard', label: 'Add route guard', prompt: `Add a custom guard to restrict access to specific ${displayName} pages.` },
      ],
    },
  ],
};

files.push({
  moduleName: slug,
  category,
  path: `.vitto/instructions.json`,
  content: JSON.stringify(vittoInstructions, null, 2) + '\n',
});
git add APIs/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts
git commit -m "[S199.7] feat: generate .vitto/instructions.json for contextual chat editing"

Task 1.8: Enforce Auto-Corrections (Not Just Warn)

Files:

Search for warning-only methods that detect useCrudList, BaseCrudApiService, BaseEntity violations.

For each detection that currently only logs: apply a regex-based fix. Specifically:

git add APIs/backend-vitto-api/src/app/module-generation/validation/auto-correction.service.ts
git commit -m "[S199.8] fix: auto-correction enforces BaseEntity and BaseCrudApiService"

Chunk 2: App Shell Generation

Currently the pipeline generates loose modules but no app container. A generated platform needs a runnable Angular app with routing, theme, and module registration.

Task 2.1: Create App Shell Generator

Files:

import { Injectable, Logger } from '@nestjs/common';

interface AppShellConfig {
  platformName: string;
  platformSlug: string;
  modules: Array<{ slug: string; className: string; category: string; displayName: string }>;
  theme?: { primaryColor?: string; accentColor?: string };
}

interface GeneratedFile {
  path: string;
  content: string;
  moduleName: string;
  category: string;
}

@Injectable()
export class AppShellGeneratorService {
  private readonly logger = new Logger('AppShellGenerator');

  generate(config: AppShellConfig): GeneratedFile[] {
    const files: GeneratedFile[] = [];
    const { platformSlug, platformName, modules } = config;

    // main.ts
    files.push(this.generateMain(platformSlug));
    // app.config.ts
    files.push(this.generateAppConfig(platformSlug, platformName, modules));
    // app.routes.ts
    files.push(this.generateAppRoutes(platformSlug, modules));
    // app.component.ts + html + scss
    files.push(...this.generateAppComponent(platformSlug, platformName));
    // index.html
    files.push(this.generateIndexHtml(platformSlug, platformName));
    // project.json (NX app)
    files.push(this.generateProjectJson(platformSlug));
    // tsconfig.app.json
    files.push(this.generateTsConfigApp(platformSlug));
    // styles.scss (entry point)
    files.push(this.generateStyles(platformSlug));

    return files;
  }
  // ... (implementation methods follow in Step 2)
}

Key methods — generateAppRoutes() connects all modules:

private generateAppRoutes(slug: string, modules: AppShellConfig['modules']): GeneratedFile {
  const moduleImports = modules.map(m =>
    `  { path: '${m.slug}', loadChildren: () => import('@module/${m.slug}').then(m => m.${m.className}Routes) },`
  ).join('\n');

  return {
    path: `apps/${slug}/src/app/app.routes.ts`,
    content: [
      `import { Routes } from '@angular/router';`,
      `import { authGuard } from '@libs-core/security';`,
      ``,
      `export const appRoutes: Routes = [`,
      `  { path: '', redirectTo: 'dashboard', pathMatch: 'full' },`,
      moduleImports,
      `  { path: '**', redirectTo: 'dashboard' },`,
      `];`,
      ``,
    ].join('\n'),
    moduleName: slug,
    category: 'app',
  };
}

generateAppConfig() registers all module manifests:

private generateAppConfig(slug: string, name: string, modules: AppShellConfig['modules']): GeneratedFile {
  return {
    path: `apps/${slug}/src/app/app.config.ts`,
    content: [
      `import { ApplicationConfig, provideAppInitializer, inject } from '@angular/core';`,
      `import { provideRouter, withComponentInputBinding } from '@angular/router';`,
      `import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';`,
      `import { ModuleRegistryService } from '@libs-core/module-base';`,
      `import { appRoutes } from './app.routes';`,
      ``,
      `export const appConfig: ApplicationConfig = {`,
      `  providers: [`,
      `    provideRouter(appRoutes, withComponentInputBinding()),`,
      `    provideHttpClient(withInterceptorsFromDi()),`,
      `    provideAppInitializer(() => {`,
      `      const registry = inject(ModuleRegistryService);`,
      `      // Module manifests registered dynamically at runtime`,
      `    }),`,
      `  ],`,
      `};`,
      ``,
    ].join('\n'),
    moduleName: slug,
    category: 'app',
  };
}
git add APIs/backend-vitto-api/src/app/module-generation/generators/
git commit -m "[S199.9] feat: app shell generator produces runnable Angular app"

Chunk 3: GitLab-Backed Studio (Replace Local Disk)

Task 3.1: Add GitLab File Operations to StudioApiService

Files:

Replace disk-based methods with GitLab API calls:

// New methods — all go through backend-ai-api which proxies to GitLab
listModulesFromRepo(spaceId: string, universeId: string): Observable<{ modules: string[] }> {
  return this.http.get<{ modules: string[] }>(
    `${this.aiApiUrl}/studio/repos/${spaceId}/${universeId}/modules`
  );
}

getModuleStructureFromRepo(spaceId: string, universeId: string, moduleSlug: string): Observable<ModuleStructure> {
  return this.http.get<ModuleStructure>(
    `${this.aiApiUrl}/studio/repos/${spaceId}/${universeId}/modules/${moduleSlug}/structure`
  );
}

getFileFromRepo(spaceId: string, universeId: string, filePath: string): Observable<{ content: string }> {
  return this.http.get<{ content: string }>(
    `${this.aiApiUrl}/studio/repos/${spaceId}/${universeId}/file`,
    { params: { path: filePath } }
  );
}

commitFileToRepo(spaceId: string, universeId: string, filePath: string, content: string, message: string): Observable<{ sha: string }> {
  return this.http.post<{ sha: string }>(
    `${this.aiApiUrl}/studio/repos/${spaceId}/${universeId}/file`,
    { path: filePath, content, message }
  );
}
git add libs/libs-structural/vitto-studio/src/lib/services/studio-api.service.ts
git commit -m "[S199.10] feat: studio API methods for GitLab-backed file operations"

Task 3.2: Create Backend Studio Repo Controller

Files:

@Controller('studio/repos')
export class StudioRepoController {
  constructor(private readonly universeGit: UniverseGitService) {}

  @Get(':spaceId/:universeId/modules')
  async listModules(@Param('spaceId') spaceId: string, @Param('universeId') universeId: string) {
    const tree = await this.universeGit.getFileTree(spaceId, universeId, 'modules/', 'develop');
    const modules = tree.filter(f => f.type === 'tree' && f.path.startsWith('modules/') && f.path.split('/').length === 3)
      .map(f => f.path.split('/')[2]);
    return { modules: [...new Set(modules)] };
  }

  @Get(':spaceId/:universeId/modules/:slug/structure')
  async getModuleStructure(...) { /* recursive tree listing */ }

  @Get(':spaceId/:universeId/file')
  async getFile(@Query('path') filePath: string, ...) {
    const content = await this.universeGit.getFileContent(spaceId, universeId, filePath, 'develop');
    return { content };
  }

  @Post(':spaceId/:universeId/file')
  async commitFile(@Body() body: { path: string; content: string; message: string }, ...) {
    const result = await this.universeGit.commitFiles(repoPath, 'develop', [{ path: body.path, content: body.content }], body.message);
    return { sha: result.sha };
  }
}

Task 3.3: Update VfsSyncService to Load from GitLab

Files:

Change the hydration flow to use GitLab endpoints instead of local disk scanning. Keep fromDisk as an option for future local dev mode.

When a user edits a file in Studio, debounce + commit to the GitLab branch:

private syncFileEdit(node: VfsNode): void {
  if (node.sourceType === 'module' && node.isGenerated) {
    this.api.commitFileToRepo(this.spaceId, this.universeId, node.path, node.content, `edit: ${node.name}`)
      .pipe(debounceTime(1000), takeUntilDestroyed(this.destroyRef))
      .subscribe();
  }
}

Chunk 4: Deploy Pipeline (Studio → Cloudflare)

Task 4.1: Create Studio Deploy Endpoint on Vitto

Files:

The endpoint should:

  1. Pull latest from GitLab repo (develop branch)
  2. Run ng build (or use direct upload without build for non-compiled assets)
  3. Upload dist to Cloudflare Pages via DirectCloudflareDeployService
  4. Stream progress via SSE
@Controller('studio')
export class StudioDeployController {
  @Post('deploy')
  async deploy(@Body() body: { spaceId: string; target: 'cloudflare' | 'subdomain' }, @Res() res: Response) {
    // 1. Get GitLab repo for this space
    // 2. Pull all files from develop branch
    // 3. Generate .gitlab-ci.yml (or trigger direct upload)
    // 4. Stream progress events
  }
}

Emit events: deploy.started, deploy.building, deploy.uploading, deploy.complete, deploy.failed

Task 4.2: Create Draft MR After Generation

Files:

In the INTEGRATE phase, after files are committed to the feature branch:

// After commitFiles() succeeds:
const mrResult = await this.universeGit.createMergeRequest(
  gitConfig.repoPath,
  featureBranch,
  'develop',
  `Draft: Generated ${executeOutput.modules.length} modules`,
  { draft: true }
);

this.emitPhaseEvent(sseSubject, session.id, 'module-gen.mr-created' as any, {
  mrUrl: mrResult.webUrl,
  mrId: mrResult.id,
  branch: featureBranch,
});

The frontend should display a clickable link to the GitLab MR.

Task 4.3: Wire Cloudflare Preview Deploy

Files:

Instead of waiting for GitLab CI, use CloudflarePagesService.uploadFiles() directly with files from the GitLab repo. This gives instant preview URLs.

sseSubject.next({
  type: 'deploy.complete',
  data: {
    url: deployResult.url,
    previewUrl: deployResult.previewUrl,
    projectName: cloudflareProjectName,
  },
});

Chunk 5: Vitto Chat Contextual Editing

Task 5.1: Load .vitto/instructions.json in Chat Context

Files:

When the chat context includes a module ID or the user clicks a module area in Studio:

  1. Read .vitto/instructions.json from the module (via GitLab)
  2. Inject the relevant editableAreas[].vittoActions into the system prompt
  3. Vitto now knows exactly what actions are available for that code area
{
  name: 'edit_module_file',
  description: 'Edit a file in a generated module. Commits the change to GitLab.',
  parameters: {
    spaceId: { type: 'string' },
    universeId: { type: 'string' },
    filePath: { type: 'string' },
    content: { type: 'string' },
    commitMessage: { type: 'string' },
  },
}

When user clicks an editable area in Studio, send a message to Vitto chat with:

{
  "context": {
    "moduleId": "player-management",
    "area": "list",
    "file": "src/lib/components/player-list/player-list.component.ts",
    "availableActions": ["add-column", "add-filter", "add-bulk-action", "customize-layout"]
  }
}

Vitto can then offer: "I can add a column, add filters, add bulk actions, or change the layout. What would you like?"


Chunk 6: SSE Refresh Resilience

Task 6.1: Generation Session Persistence

Files:

On each phase completion, persist the session state so it survives page refresh:

await this.sessionsCollection.updateOne(
  { _id: session.id },
  { $set: { phase: currentPhase, status: 'running', lastEvent: event, updatedAt: new Date() } },
  { upsert: true }
);
@Get('generation/active/:spaceId')
async getActiveGeneration(@Param('spaceId') spaceId: string) {
  return this.moduleGen.getActiveSession(spaceId);
}

In the frontend chat engine, on initialization:

// Check if there's an active generation for this space
this.api.getActiveGeneration(spaceId).subscribe(session => {
  if (session?.status === 'running') {
    this.resubscribeToSSE(session.taskId);
    this.showReconnectedMessage(session);
  }
});

Execution Order

  1. Chunk 1 (Tasks 1.1-1.8): Fix code quality — must be first, affects all subsequent generation
  2. Chunk 2 (Task 2.1): App shell — needed for deployable output
  3. Chunk 3 (Tasks 3.1-3.3): GitLab-backed Studio — the storage backbone
  4. Chunk 4 (Tasks 4.1-4.3): Deploy pipeline — connects Studio to Cloudflare
  5. Chunk 5 (Task 5.1): Vitto contextual editing — the "groundbreaking feature"
  6. Chunk 6 (Task 6.1): SSE resilience — polish for production reliability