Diese Seite erklärt, wie die Agentic Software Factory technisch aufgebaut ist — von der UI-Schicht bis zu den Adaptern, die mit den Coding-CLIs sprechen. Sie richtet sich an Architektinnen und Entwickler, die einen Überblick über Schichten, Module, Datenflüsse und die wichtigsten Designentscheidungen wollen, ohne sofort in den Code einzutauchen. Wer Schritt-für-Schritt durch die Bedienung will, ist in der Einführung besser aufgehoben.
Die Plattform ist eine Control Plane für AI-gestützte Softwareentwicklung. Sie kapselt vier Aufgaben in einer einzigen Webanwendung: Projekt-Erfassung, Artefakt-Generierung, Run-Orchestrierung und Quality-Bewertung. Du arbeitest nicht direkt mit der Shell-CLI von Claude Code, Codex, Gemini oder Aider — die Plattform ruft diese Tools als Subprozesse auf, sammelt deren Output, schreibt ihn strukturiert in die Datenbank und macht ihn live in der UI sichtbar.
Die Architektur folgt zwei sehr klassischen Prinzipien, die in v1 bewusst nicht mit Buzzwords überfrachtet werden:
run, wizard, settings) hat eine domain-, eine application-, eine web- und eine infrastructure-Schicht. Domain hängt von nichts ab, Application kennt nur Domain, Web kennt Application, Infrastructure implementiert Domain-Ports.Browser → Spring MVC + Thymeleaf → Application Services → Domain Model
↓
PostgreSQL ←─ Repositories ←─ Adapter (Claude / Codex / Gemini / Aider / Mock / Git / Filesystem)
Innerhalb des Maven-Moduls app liegen alle fachlichen Module unter io.softwarefabrik.app.<modul>. Pro Modul gibt es maximal vier Unterpakete:
domain/ — Entities, Value Objects, Repositories als Interface, fachliche Services. Keine Spring-, JPA- oder Web-Annotationen.application/ — Use-Case-Services, die Domain-Methoden orchestrieren. Hier liegen Transaktionsgrenzen (@Transactional).web/ — Spring-MVC-Controller, Thymeleaf-Bindings, DTOs für die UI, REST-Endpunkte für SSE.infrastructure/ — JPA-Implementierung der Repositories, externe Adapter, Filesystem-Zugriffe, Scheduled Jobs.domain hängt von nichts ab.application hängt nur von domain ab.web hängt von application ab, nicht direkt von domain.infrastructure implementiert domain-Ports und kann Spring-Magie nutzen.domain-Paket eines anderen Moduls, sondern nur über dessen application-Service.ArchUnit-Tests im Modul app/src/test/java/.../architecture/ pinnen genau diese Regeln. Wer eine falsche Abhängigkeit reinzieht, kriegt einen roten CI-Lauf — die Architektur wird also nicht durch gute Absicht, sondern durch automatisierte Tests garantiert.
Die Module der v0.8.1-Codebasis im Überblick:
Hier ein kurzer Steckbrief pro Modul, damit du beim Lesen des Codes oder beim Suchen einer Funktion schnell weißt, wo du nachschauen musst:
Verwaltet die Agentenrollen (Architect, Developer, Reviewer, QA, Security, Documentation, Merge/Release) inkl. preferredModel pro Rolle. Hauptklassen: AgentDefinition, AgentRoleService. Routen: /agents.
Append-only Audit-Log für sicherheitsrelevante Aktionen — Login-Versuche, Setting-Änderungen, Run-Starts, Approval-Entscheidungen. Hauptklasse: AuditEvent. Tabelle audit_event.
Shared utilities ohne fachlichen Bezug: ID-Generator, Clock-Abstraktion, gemeinsame Validatoren. Bewusst klein gehalten.
Neu seit v0.6.0: schreibt vor jedem Run .claude/settings.local.json (Plugin-Whitelist + Skills-Pfad) und .claude/agents/<role>.md pro aktivem Team-Mitglied ins Workspace. Hauptklassen: PluginCatalog, SkillsCatalog, ConductorWorkspaceWriter, ConductorRunPreparation.
Das Herz der Run-Ausführung: ExecutionAdapterRegistry, Sandbox-Implementierungen LocalProcessSandbox + ContainerProcessSandbox mit ExecutionSandboxFactory (Auswahl per Setting execution.sandbox.variant), Adapter für Claude, Codex, Gemini, Aider und Mock. Pro Adapter ein eigenes infrastructure/<name>-Paket. Unter claudecode/ seit v0.7.0 zusätzlich ClaudeStreamJsonParser für Live-Token-Events und TokenEstimator für lokale Vorab-Schätzungen.
Git-Operationen im Workspace: git init, Auto-Commits, Checkpoints, Diff- und Log-Ausgabe für die Run-Detail-Seite. Seit v0.8.0 diffSeit(workspace, sinceSha, maxBytes) für Inline-Diffs vor Approval. Shellt direkt zu git statt JGit, um die Kompatibilitätsmatrix klein zu halten.
Lizenzlogik: DEMO / COMMUNITY / FULL Tiers, Lease-JWT-Verifikation gegen Keycloak-Public-Key, Limit-Enforcement. Routen: /license, /admin/license.
Approval-Policies: welche Phasen brauchen menschliche Freigabe, welche laufen automatisch durch. Speichert ApprovalDecision-Records.
Die ProjectDefinition-Entity mit allen Editor-Feldern (Vision, Zielgruppe, Tech, Architektur, Sicherheit, …). Routen: /projects, /projects/{id}/edit.
Generiert die sechs Markdown-Artefakte (PROJECT.md, INSTRUCTIONS.md, AGENTS.md, WORKFLOW.md, DEFINITION_OF_DONE.md, README.md) aus den Projektfeldern. Templates liegen unter resources/prompts/.
Aggregiert die Reviewer-Findings zum Quality-Gate-Verdict (PASSED / WARNING / FAILED / SKIPPED / ERROR). Sonderregeln für SECURITY/HIGH und ARCHITECTURE/CRITICAL. Routen: /runs/{id}/quality-gate.
Implementierungen der Reviewer: aider-review, claude-review, security, architecture-reviewer, hallucination-review. CLI-Aufrufer im read-only-Modus oder statische Heuristiken.
Das Run-Modell mit Phasen, Status, Logs, Token-Usage und Workspace-Pfad. Hauptklassen: Run, RunPhase, RunOrchestrationService, WorkspaceService. Routen: /runs, /runs/{id}.
Verschlüsselte Speicherung von API-Keys (Anthropic, OpenAI, Gemini) im Postgres. AES-GCM mit Master-Key aus SOFTWAREFABRIK_SECRETS_MASTER_KEY. Routen: /integrations.
Spring-Security-Konfiguration: Login, Bootstrap-Admin, Rollenmodell (USER / ADMIN), CSRF, BCrypt-Passwörter. Hauptklasse: SecurityConfig.
Globale Plattform-Defaults unter /einstellungen mit 5-Minuten-TTL-Cache und Override-Ordnung PROJECT > USER > GLOBAL > YAML. Tabelle app_setting (V9). Seit v0.7.0 zusätzlich Schlüssel execution.sandbox.variant für die Container-Sandbox.
Team-Zusammenstellung aus Agentenrollen pro Projekt. Wird in AGENTS.md serialisiert.
Build-Validierung pro Run: mvn verify, npm run build oder konfigurierter Build-Befehl. Schreibt BuildResult.
Querschnittsthemen der UI: globale Layouts, kopfbereich.html, Theme-Toggle, Dashboard-Controller. Keine fachliche Logik.
Vier-Schritt-Assistent unter /wizard. Hauptklassen: WizardController, WizardService, WizardDraft, TemplateRegistry, ToggleRegistry, VersionLookupClient, WizardCostEstimator. Tabelle wizard_draft (V12), version_cache (V13). Sechs Templates registriert (Spring Boot, Static Frontend, .NET, Python, Node, Existing-Repo-Import); v0.8.x bringt Progress-Stepper, Objective-Vorschau und lokale Kosten-Schätzung in Schritt 4.
Der typische Lebenszyklus eines Projekts durchläuft fünf Stationen. Jede schreibt persistent in Postgres, jede ist in der UI sichtbar:
┌──────────┐ ┌────────────┐ ┌────────────┐ ┌───────┐ ┌──────────────┐
│ Wizard │ → │ Project │ → │ Artefakte │ → │ Run │ → │ Quality Gate │
│ /wizard │ │ (DRAFT) │ │ (Markdown) │ │ │ │ (Verdict) │
└──────────┘ └────────────┘ └────────────┘ └───────┘ └──────────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
wizard_draft project_def prompt_artifact run, run_phase quality_gate_run
run_log,
token_usage
wizard_draft (überlebt Browser-Reload und Server-Restart). Bei Abschluss wird ein ProjectDefinition erzeugt und der Draft auf completed gesetzt.DRAFT, geht nach Artefakt-Generierung auf READY.PromptAssemblyService aus den Projektfeldern erzeugt. Sie sind editierbar — der Agent liest immer die zuletzt gespeicherte Version.RunOrchestrationService ist asynchron (Spring @Async), die UI pollt nicht, sondern hört per SSE.Die Plattform nutzt PostgreSQL als einzige Datenquelle. Schema-Änderungen laufen ausschließlich über Flyway-Migrationen unter app/src/main/resources/db/migration/. Jede Migration ist nummeriert (V<n>__<name>.sql) und unveränderlich, sobald sie einmal in Produktion war — neue Änderungen kommen als zusätzliche Migration, nicht als Edit der alten.
| Migration | Inhalt |
|---|---|
V1 | Initial-Schema: project_definition, agent_definition, team, run, run_phase. |
V2 | Logs und Token-Usage: run_log, token_usage_event. |
V3 | Approval-Policies: approval_policy, approval_decision. |
V4 | Verschlüsselte Secrets: integration_secret. |
V5 | Audit-Log: audit_event. |
V6 | Quality-Gate-Tabellen: quality_gate_run, review_finding. |
V7 | License-Tabellen: license, license_lease. |
V8 | Build-Resultate: build_result. |
V9 | v0.4.0: app_setting mit Scope-Spalte (GLOBAL / USER / PROJECT) und Audit-Trail. |
V10 | Run-Tags und Quick-Start-Marker. |
V11 | v0.4.0: agent_preferred_model — Spalte für rolle-spezifische Modellauswahl. |
V12 | v0.4.0: wizard_draft — JSON-Spalte für State, Schritt-Zähler, Template-ID, Cleanup-TTL. |
V13 | v0.4.0: version_cache — Cache-Key, JSON-Wert, refreshed_at, last_error. Java-berechnete Staleness, kein DB-Computed-Column (damit das H2-Test-Profil funktional bleibt). |
V14 | v0.6.0: erweiterte agent_definition für Conductor (Mission, aktive Skills). |
V15 | v0.7.0: run_template — Adapter, Team, Objective, optionaler Projekt-Scope, Audit-Felder. Quelle für "Run aus Template starten". |
GENERATED ALWAYS AS. Wo das doch mal nötig wäre, gibt es ein db.migration.h2/-Override.Adapter sind Spring-Beans, die das Interface ExecutionAdapter implementieren. Beim Start sammelt die ExecutionAdapterRegistry alle verfügbaren Beans in einer Map <Name → Adapter>. Der Name kommt aus der Adapter-Konstante (z.B. "claudecode", "codex", "gemini", "aider", "mock").
Beim Start eines Runs wird der zu nutzende Adapter in dieser Reihenfolge bestimmt:
preferredAdapter hat.app_setting mit Scope GLOBAL.application.yml als letzter Fallback.Diese Override-Reihenfolge — PROJECT > USER > GLOBAL > YAML — wird vom SettingService implementiert und gilt überall in der Plattform, nicht nur für Adapter.
// vereinfacht
ExecutionAdapter adapter = registry.byName(
settingService.resolve("execution.adapter.default", scope)
);
Damit der Wizard immer aktuelle stabile Versionen vorbelegen kann, hält die Plattform einen Cache mit Versions-Lookups. Der Cache wird täglich um 03:00 Uhr per @Scheduled-Job aktualisiert; ein zweiter Job läuft um 04:00 Uhr und löscht abgelaufene wizard_draft-Einträge (TTL 30 Tage).
Die Versions-Lookups folgen dem Strategy-Pattern: ein VersionLookupClient-Interface, drei Implementierungen:
https://search.maven.org/solrsearch/select für Java-Bibliotheken (Spring Boot, ArchUnit, OWASP Dependency-Check).https://api.github.com/repos/<owner>/<repo>/releases/latest (z.B. für Trivy CLI).https://registry.npmjs.org/<package> für Node-Pakete (Playwright).Der Cache-Key ist eine fachliche Konstante, kein technischer Pfad — z.B. spring-boot.3.x, archunit.latest, owasp.dependency-check.latest, trivy.cli.latest, playwright.npm.latest. So lässt sich die Quelle wechseln, ohne die Konsumenten zu brechen.
Staleness wird in Java berechnet (Schwelle: 25 Stunden; gibt einen Toleranzpuffer für den 03:00-Job): now - refreshed_at > 25h. Wir machen das bewusst nicht als Postgres-GENERATED-Spalte, weil das H2-Test-Profil sonst stolpern würde.
Admins können den Cache unter /einstellungen/wizard/versions inspizieren. Pro Eintrag werden Cache-Key, gelieferter Wert, refreshed_at, last_error und ein Stale-Badge angezeigt; ein Knopf "Jetzt manuell refreshen" triggert den Lookup synchron für genau diesen Key.
Jeder Run bekommt einen eigenen Workspace — ein lokales Verzeichnis unter SOFTWAREFABRIK_WORKSPACES_ROOT/<project-slug>/<run-id>. Der Adapter (z.B. claudecode) wird in diesem Verzeichnis als eigener Prozess gestartet (LocalProcessSandbox). Jeder Run hat damit seine eigene Git-Historie, sein eigenes Build-Output, seine eigene node_modules oder target-Folder — Runs können nicht versehentlich Files anderer Runs überschreiben.
Die LocalProcessSandbox setzt Umgebungsvariablen sauber pro Prozess (kein System.setenv auf Plattform-Ebene), terminiert Prozesse hart bei Cancel/Timeout (destroyForcibly()) und schreibt stdout und stderr zeilenweise in die run_log-Tabelle plus parallel in den SSE-Stream.
ContainerProcessSandbox (ADR-0011 Accepted, Variante B). Sie startet jeden Agent in einem ephemerem Docker- oder Podman-Container mit --cpus 2 --memory 4g --pids-limit 512 --read-only, Bindmount auf den Workspace und --network=none als Default. Aktiviert per Setting execution.sandbox.variant=container; fehlt Docker im PATH, fällt die ExecutionSandboxFactory mit Log-Warnung auf die lokale Variante zurück. Single-Tenant-Hosts merken nichts; Enterprise-Tier kann ohne Code-Änderung umschalten.Sicherheit ist in der Architektur nicht ein Modul, sondern eine Querschnittsdisziplin. Die wichtigsten Punkte:
SOFTWAREFABRIK_ADMIN_USER + ...PASSWORD. Wird das Passwort weggelassen, wird kein Admin angelegt — schwache Defaults wären ein Bug.SOFTWAREFABRIK_SECRETS_MASTER_KEY verschlüsselt im Postgres abgelegt. Nicht im Klartext, nicht im Log.audit_event festgehalten. Append-only, mit Timestamp, User-Subject und Diff der Änderung.EXECUTION, COMPLETION) warten auf eine bewusste Nutzerentscheidung. Default-Policy ist konservativ — Approval an für Plan- und Review-Übergänge.127.0.0.1:5432 statt 0.0.0.0:5432, damit nicht versehentlich der Port nach außen offen ist. Trivy/OWASP-Scans laufen pro CI-Build.Das Settings-Modul ist das Schaltbrett der Plattform. Es kombiniert drei Designentscheidungen, die zusammen funktionieren:
GLOBAL, USER, PROJECT). SettingService.resolve(key, context) sucht von eng nach weit: erst PROJECT, dann USER, dann GLOBAL, dann YAML.SettingService cached resolved Werte. Damit kostet ein resolve in 99 % der Fälle nichts. Bei einer UI-Änderung wird der Cache nicht hart invalidiert — er wartet maximal 5 Minuten, dann ist der neue Wert da. Kein App-Restart nötig.app_setting erzeugt einen audit_event-Eintrag mit Subject, Key, Old-Value, New-Value. Damit ist jederzeit nachvollziehbar, wer wann was geändert hat.Konkrete Settings, die heute verwaltbar sind:
| Key | Bedeutung | Typischer Wert |
|---|---|---|
workspace.root | Wurzelverzeichnis für alle Run-Workspaces | /var/softwarefabrik/workspaces |
git.user.name | Git-Author für Auto-Commits | Software Factory Bot |
git.user.email | Git-E-Mail | bot@softwarefabrik.local |
execution.adapter.default | Default-Execution-Adapter | claudecode |
execution.model.claudecode | Default-Modell für Claude Code | claude-sonnet-4-6 |
execution.model.codex | Default-Modell für Codex | gpt-5 |
budget.tokens.daily | Tages-Token-Cap | 2000000 |
budget.tokens.weekly | Wochen-Token-Cap | 10000000 |
budget.threshold.soft | Soft-Schwelle in Prozent (warnt nur) | 80 |
Die Run-Detail-Seite streamt drei Dinge live aus dem Backend in den Browser: Logs, Phasen-Updates und Token-Counter. Statt Polling nutzt die Plattform Server-Sent Events (SSE) — ein offener HTTP-Stream, der vom Backend einseitig Push-Nachrichten an den Browser schickt.
:keepalive-Comment. Das hält die Verbindung lebendig und enttarnt schnell tote TCP-Sockets.retry: 5000, also nach 5 s.Die SSE-Endpoints liegen unter /runs/{id}/stream/logs, /runs/{id}/stream/phases, /runs/{id}/stream/tokens. Falls SSE durch einen Reverse-Proxy nicht durchkommt (manche Corporate-Setups blocken HTTP-Streaming), schaltet die UI einen Polling-Fallback an, der dieselben Daten in 2-Sekunden-Takten holt.
Tests sind nicht optional, sondern Architekturbestandteil. Drei Säulen:
app/src/test/java/.../<modul>/<...Service>Test.java. Schnell, ohne Spring-Context, mit Mockito.@WebMvcTest, @DataJpaTest) gegen H2.web direkt in infrastructure greift, kriegt einen roten Test.Coverage-Gate: JaCoCo 80/80 (Lines + Branches). Wer einen Service ohne Test mergen will, kommt durch mvn verify nicht durch. Das ist absichtlich streng — die Plattform ist kein Prototyp mehr.
CI-Pipeline (.github/workflows/ci.yml): Build + Test + ArchUnit + JaCoCo + OWASP Dependency-Check + Trivy-File-Scan. OWASP läuft non-blocking ohne NVD-Key, Trivy nur im dedizierten Job.
Genauso wichtig wie das, was die Plattform tut, ist das, was sie absichtlich nicht tut. Diese Auslassungen halten v1 lesbar und ausbaubar:
mvn verify oder npm run build laufen im Workspace, nicht in einem Build-Cluster. Wer das skalieren will, baut sich dafür eine separate Infrastruktur.Neben dem Produkt-Backend existiert ein eigener Stack für Identität und Lizenzierung, entkoppelt von der Plattform-DB:
Der Client startet nach der Installation im registrierungsfreien DEMO-Modus (nur Mock-Adapter, harte Limits im Code, kein Server-Kontakt). Nach einer Registrierung per Device-Flow wird automatisch eine COMMUNITY-Lizenz angelegt; ein Admin kann den Tier in der Admin-UI auf FULL anheben. Das Lease legt fest, welcher Adapter verwendet werden darf und wie Run-Anlage und Team-Größe begrenzt sind.