diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/APM-Packages.iml b/.idea/APM-Packages.iml
new file mode 100644
index 0000000..7aa9c26
--- /dev/null
+++ b/.idea/APM-Packages.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..90b8dcb
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apm-omos-agent-teams-v3.md b/apm-omos-agent-teams-v3.md
new file mode 100644
index 0000000..01cb359
--- /dev/null
+++ b/apm-omos-agent-teams-v3.md
@@ -0,0 +1,453 @@
+# APM, Agent-Teams und das kleine Problem namens Realität
+
+*Wie APM-Packages Team-Empfehlungen ausliefern können – mit oh-my-opencode-slim als erstem konkretem Target.*
+
+Der Agenten-Stack entwickelt sich gerade mit der Geschwindigkeit eines explodierenden Werkzeugschuppens. Jeden Monat erscheint ein neues Manifest, ein neues Plugin-Format oder eine neue Orchestrierungsebene. Alles löst einen legitimen Teil des Problems. Fast nichts passt nahtlos zusammen.
+
+Das ist kein Vorwurf. Es ist der normale Zustand eines jungen Ökosystems. Der interessante Teil beginnt dort, wo die bestehenden Stücke bereits ausreichen, um eine sinnvolle Brcke zu bauen.
+
+Diese Brcke kann so aussehen:
+
+> Ein APM-Package liefert Skills, Instructions, MCP-Abhngigkeiten und eine deklarative Empfehlung fr ein Agenten-Team. Ein Harness-spezifischer Adapter übersetzt diese Empfehlung in die lokale Team- und Modellkonfiguration.
+
+Fr oh-my-opencode-slim nenne ich diesen Adapter im Folgenden **`omos`**.
+
+`omos` ist dabei ein Design fr eine Erweiterung bzw. ein Sidecar-Tool. Es ist heute kein offiziell eingebautes APM-Target.
+
+## Das Ökosystem heute
+
+Die vorhandenen Formate lassen sich grob in fnf Gruppen einteilen.
+
+| Ebene | Beispiele | Zustndigkeit |
+|---|---|---|
+| Portable Komponenten | Agent Plugins | Skills und MCP-Server verteilen |
+| Dependency Management | APM | Packages, Lockfile, Policies, Deployment |
+| Team-Komposition | Spawnfile, Harness-Configs | Rollen, Delegation, Agentenbeziehungen |
+| Runtime-Profile | oh-my-opencode-slim Presets | Modelle und Optionen je Agent |
+| Governance | Agent Manifest | Identitt, Grenzen, Verantwortung, Auditierbarkeit |
+
+Das Problem ist nicht fehlende Funktionalitt. Das Problem ist die Lcke zwischen diesen Schichten.
+
+### Agent Plugins
+
+Agent Plugins 1.0 definiert ein bewusst kleines, portables Plugin-Format. Im Kern enthlt ein Plugin eine `plugin.json` und kann Skills sowie MCP-Server bereitstellen. Erweiterungen sind mglich, mssen aber im `extensions`-Block ber einen Reverse-Domain-Namespace isoliert werden.
+
+Das ist sehr gut fr Interoperabilitt. Ein Skill bleibt ein Skill. Ein MCP bleibt ein MCP. Der Standard versucht nicht, die Laufzeit eines jeden Agentenframeworks zu regieren.
+
+Offen bleiben jedoch Dependencies, getestete Versionen, Teamrollen, Modellprofile und die Aktivierung im konkreten Harness.
+
+### APM
+
+APM besetzt die nchste Ebene: Package- und Dependency-Management fr Agent-Kontext.
+
+Ein APM-Manifest kann Abhngigkeiten auf andere APM-Packages, MCP-Server und LSP-Server beschreiben. APM verwaltet Auflsung, Lockfile und Installation. ber Targets kompiliert es Primitives in Harness-spezifische Dateien. Das native `opencode`-Target installiert Skills, Agents und Commands in die passenden `.opencode/`-Pfade.
+
+```yaml
+name: acme/research-assistant
+version: 1.0.0
+type: hybrid
+
+targets:
+ - opencode
+
+dependencies:
+ apm:
+ - acme/base-engineering
+ mcp:
+ - name: repository-search
+ registry: false
+ transport: stdio
+ command: repository-search-mcp
+ tools: [search_code, read_file]
+```
+
+APM beantwortet damit: Welche Artefakte gehren zusammen? Welche Versionen wurden zusammen getestet? Welche MCPs werden bentigt? Wohin werden Skills und Instructions installiert?
+
+Was APM aktuell nicht standardisiert beschreibt, ist Team-Semantik: Rollen, Delegation, Modell-Empfehlungen und Aktivierung eines Team-Profils.
+
+### Spawnfile
+
+Spawnfile adressiert die Teamachse. Agenten und Teams knnen deklarativ mit Rollen, Zustndigkeiten, Kommunikationswegen und Subagenten beschrieben werden. Die offene Stelle liegt bei Dependencies und Verteilung: Ein Team-Manifest installiert keine versionierten Skills, MCPs und Policies.
+
+### oh-my-opencode-slim
+
+oh-my-opencode-slim sitzt nahe an der Runtime. Sein Preset-System kann je Agent Modell, Temperatur, Variante und provider-spezifische Optionen umschalten. Presets werden ber `/preset ` whrend einer laufenden Session aktiviert.
+
+```jsonc
+{
+ "presets": {
+ "deep-review": {
+ "orchestrator": { "model": "provider/strong-generalist" },
+ "oracle": { "model": "provider/high-reasoning", "variant": "thinking" },
+ "librarian": { "model": "provider/fast-research" }
+ }
+ }
+}
+```
+
+Das beantwortet die Frage, wie unterschiedliche Rollen unterschiedliche Modellprofile erhalten. Es fehlt die Herkunft: Welches Package empfiehlt dieses Team? Welche Skills und MCPs gehren dazu? Welche Version wurde getestet?
+
+## Die Lcke
+
+```mermaid
+flowchart LR
+ AP[Agent Plugins] -->|Skills und MCPs| PK[Package-Inhalt]
+ APM[APM] -->|Dependencies, Lockfile, Installation| PK
+ SF[Spawnfile] -->|Rollen und Teamstruktur| TM[Team-Modell]
+ OMOS[oh-my-opencode-slim] -->|Presets, Modelle, Runtime-Switching| RT[aktive Runtime]
+ PK -. fehlende Verbindung .-> TM
+ TM -. fehlende Verbindung .-> RT
+```
+
+Die Lcke verlangt kein zehntes allumfassendes YAML-Format. Sie verlangt eine kleine Kompositionsschicht.
+
+## Die APM-OMOS-Idee
+
+Ein APM-Package bleibt Source of Truth fr Skills, Instructions, MCP-Abhngigkeiten, Versionen, Dependency-Closure und Policies. Zusstzlich enthlt es ein Team-Profil als Empfehlung fr Harness-Adapter.
+
+```yaml
+schema: acme.team-profile/v1
+id: acme.document-research
+description: Team-Profil fr Recherche, Erstellung und Qualittsprfung quellenbasierter Dokumente.
+
+roles:
+ - id: orchestrator
+ purpose: Zerlegt Aufgaben, delegiert, integriert Ergebnisse
+ model_class: strong-generalist
+ - id: researcher
+ purpose: Sammelt und bewertet Quellen
+ model_class: fast-research
+ capabilities:
+ mcps: [web-research, repository-search]
+ skills: [job-matching]
+ - id: writer
+ purpose: Erstellt strukturierte Dokumententwrfe
+ model_class: strong-writing
+ capabilities:
+ mcps: [document-write]
+ skills: [application-writing]
+ - id: checker
+ purpose: Prft Quellenbezug, Konsistenz und Vollstndigkeit
+ model_class: high-reasoning
+ capabilities:
+ mcps: [document-read]
+ skills: [application-review]
+```
+
+Das Profil verwendet Modellklassen wie `high-reasoning` statt konkreter Modell-IDs. Der Nutzer hinterlegt in einer lokalen Mapping-Tabelle, welche Modell-IDs fr welche Modellklasse verwendet werden sollen.
+
+```yaml
+# ~/.config/apm-team/model-mapping.yaml
+model_classes:
+ strong-generalist: anthropic/claude-sonnet-4-6
+ fast-research: openai/gpt-5.4-mini
+ strong-writing: anthropic/claude-sonnet-4-6
+ high-reasoning: openai/gpt-5.5
+```
+
+## Was `omos` macht
+
+`omos` ist der Übersetzer zwischen Team-Profil und oh-my-opencode-slim. Er liest installierte APM-Packages, findet Team-Profile, schlägt Modellklassen gegen die lokale Mapping-Tabelle nach und generiert namespacete Presets fr das Projekt.
+
+```mermaid
+flowchart TD
+ P[APM Package: team-profile.yaml] --> O[omos liest]
+ M[~/.config/apm-team/model-mapping.yaml] --> O
+ O -->|Prfe| X{Alle Modellklassen gemappt?}
+ X -->|Nein| E[Fehler: fehlendes Mapping fr Rolle Y]
+ X -->|Ja| G[Generiere OMOS-Preset mit konkreten Modell-IDs]
+ G --> V[Validiere: Modelle in OMOS verfgbar?]
+ V -->|Nein| F[Fehler: Modell Z nicht im OMOS-Setup]
+ V -->|Ja| C[Schreibe .opencode/oh-my-opencode-slim.json]
+ C --> D[Fertig]
+```
+
+`omos` rtt nicht. Er übersetzt deterministisch:
+
+1. Team-Profil lesen (Modellklassen + MCP/Skill-Capabilities).
+2. Lokale Mapping-Tabelle laden (Modellklasse → konkrete Modell-ID).
+3. OMOS-Preset generieren (konkrete Modell-IDs + MCP/Skill-Allowlists).
+4. Validieren: Sind die Modelle im OMOS-Setup verfgbar?
+5. Preset in `.opencode/oh-my-opencode-slim.json` schreiben.
+
+## Mehrere Packages, ein Projekt
+
+Jedes Profil erhlt eine globale, stabile ID wie `acme.job-applications`, `acme.security-audit` oder `acme.code-review`. `omos` merged Presets anhand dieser IDs. Das aktive Team wird nicht durch die letzte Installation gewhlt.
+
+```mermaid
+flowchart LR
+ A[Package: Job Applications] --> M[Preset Merge]
+ B[Package: Security Audit] --> M
+ C[Package: Code Review] --> M
+ M --> P[Projekt-Preset-Registry]
+ P --> U[Nutzer oder expliziter Workflow]
+ U --> S[/preset acme-job-applications]
+```
+
+## Beispiel: Autobewerber als Team-Package
+
+Ein Bewerbungsassistent eignet sich gut als Beispiel: Web-Recherche, personenbezogene Daten, Dokumentenproduktion, Qualittsprfung, Benachrichtigungen und externe Aktionen treffen zusammen. Der Mechanismus bleibt derselbe fr Code-Review, Sales Research, Compliance oder Incident Response.
+
+### Rollen und OMOS-Mapping
+
+| Fachliche Rolle | OMOS-Rolle | Zweck |
+|---|---|---|
+| Koordination | `orchestrator` | Zerlegt Aufgaben, delegiert, integriert |
+| Recherche | `librarian` (Alias `researcher`) | Sucht und bewertet Stellen |
+| Schreiben | Custom Agent `writer` | Erstellt Anschreiben und Unterlagen |
+| Prfung | `oracle` (Alias `checker`) | Prft Fakten, Ton, Vollstndigkeit |
+| Benachrichtigung | Custom Agent `notifier` | Informiert den Nutzer |
+
+### Was OMOS tatschlich begrenzen kann
+
+OMOS bietet pro Agent zwei brauchbare Kontrollflchen:
+
+1. **MCP-Zugriff** (`mcps`-Array)
+2. **Skill-Zugriff** (`skills`-Array)
+
+Beides kann je Agent in einem Preset als Allowlist formuliert werden. `[]` bedeutet keine Freigabe, `["!*"]` verweigert alles; bei Konflikten gewinnt die Verweigerung.
+
+```jsonc
+{
+ "presets": {
+ "acme-job-applications": {
+ "orchestrator": {
+ "model": "anthropic/claude-sonnet-4-6",
+ "mcps": [],
+ "skills": ["job-orchestration"]
+ },
+
+ "librarian": {
+ "displayName": "researcher",
+ "model": "openai/gpt-5.4-mini",
+ "mcps": ["job-search", "candidate-profile-read"],
+ "skills": ["job-matching"]
+ },
+
+ "writer": {
+ "model": "anthropic/claude-sonnet-4-6",
+ "mcps": ["candidate-profile-read", "overleaf"],
+ "skills": ["application-writing"]
+ },
+
+ "oracle": {
+ "displayName": "checker",
+ "model": "openai/gpt-5.5",
+ "variant": "high",
+ "mcps": ["candidate-profile-read", "overleaf"],
+ "skills": ["application-review"]
+ },
+
+ "notifier": {
+ "model": "openai/gpt-5.4-mini",
+ "mcps": ["notification-draft"],
+ "skills": []
+ }
+ }
+ }
+}
+```
+
+Damit erhlt der Researcher keinen Overleaf-Zugang. Der Writer sieht keine Job-Suchtools. Der Notifier bekommt keinen Browser und keinen Schreibzugriff auf die Bewerbung.
+
+### Die wichtige Einschrnkung
+
+`mcps` und `skills` schtzen nur den Zugriff auf genau diese zwei Ebenen. Die Aussage „der Agent darf niemals eine Bewerbung absenden" bentigt Verteidigung in mehreren Schichten:
+
+| Schicht | Durchsetzung |
+|---|---|
+| OMOS-Rollenprompt | Klare Verhaltensregel: nie einreichen |
+| OMOS MCP-Allowlist | `application-submit` MCP gar nicht zuweisen |
+| MCP-Server selbst | Submission-Tool verlangt Approval-Token |
+| Workflow-State | `submit` nur aus Zustand `user_approved` erlaubt |
+| OpenCode-/Sandbox-Policy | Schreib-, Shell- und Browserrechte begrenzen |
+| User Interface | Explizite, sichtbare Freigabe vor jeder Auenwirkung |
+
+Die echte Autorisierungsgrenze muss am **MCP beziehungsweise Backend** liegen. Ein Prompt ist eine Verhaltensanweisung. Eine MCP-Allowlist ist Capability Scoping. Ein Server, der ohne gltiges Freigabe-Token keine Submission annimmt, ist die harte Grenze.
+
+Fr deinen Use Case wrde ich den Notification-MCP so schneiden:
+
+```text
+notification-draft → darf Entwurf erzeugen
+notification-send → braucht userApprovalId
+application-submit → braucht userApprovalId + applicationDraftId
+```
+
+Und im Backend:
+
+```mermaid
+sequenceDiagram
+ participant C as Checker
+ participant N as Notifier MCP
+ participant U as Nutzer
+ participant S as Submission MCP
+
+ C->>N: createDraftNotification(applicationDraftId)
+ N-->>U: Stelle gefunden, Entwurf geprft
+ U->>U: Prft PDF und Fakten
+ U->>S: approve(applicationDraftId)
+ S-->>S: issue userApprovalId
+ U->>S: submit(applicationDraftId, userApprovalId)
+ S-->>U: Bewerbung eingereicht
+```
+
+Der Notifier kann dann technisch niemals eine Bewerbung absenden, weil er keinen Submission-MCP besitzt und das Submission-Backend einen vom Nutzer stammenden Freigabenachweis verlangt.
+
+### Das Team-Profil
+
+```yaml
+schema: acme.team-profile/v1
+id: acme.job-applications
+description: Human-in-the-loop-Team zur Recherche, Vorbereitung und Prfung individueller Bewerbungsunterlagen.
+
+roles:
+ - id: orchestrator
+ purpose: Zerlegt Aufgaben, delegiert, integriert Ergebnisse
+ model_class: strong-generalist
+ capabilities:
+ mcps: []
+ skills: [job-orchestration]
+
+ - id: researcher
+ purpose: Sucht und bewertet Stellenausschreibungen
+ model_class: fast-research
+ capabilities:
+ mcps: [job-search, candidate-profile-read]
+ skills: [job-matching]
+
+ - id: writer
+ purpose: Erstellt auf Fakten basierende Anschreiben
+ model_class: strong-writing
+ capabilities:
+ mcps: [candidate-profile-read, overleaf]
+ skills: [application-writing]
+
+ - id: checker
+ purpose: Prft Fakten, Ton und Vollstndigkeit
+ model_class: high-reasoning
+ capabilities:
+ mcps: [candidate-profile-read, overleaf]
+ skills: [application-review]
+
+ - id: notifier
+ purpose: Informiert den Nutzer ber geprfte Entwrfe
+ model_class: cheap-reliable
+ capabilities:
+ mcps: [notification-draft]
+ skills: []
+```
+
+Die lokale Mapping-Tabelle:
+
+```yaml
+# ~/.config/apm-team/model-mapping.yaml
+model_classes:
+ strong-generalist: anthropic/claude-sonnet-4-6
+ fast-research: openai/gpt-5.4-mini
+ strong-writing: anthropic/claude-sonnet-4-6
+ high-reasoning: openai/gpt-5.5
+ cheap-reliable: openai/gpt-5.4-mini
+```
+
+Das generierte OMOS-Preset:
+
+```jsonc
+{
+ "presets": {
+ "acme-job-applications": {
+ "orchestrator": {
+ "model": "anthropic/claude-sonnet-4-6",
+ "mcps": [],
+ "skills": ["job-orchestration"]
+ },
+ "librarian": {
+ "displayName": "researcher",
+ "model": "openai/gpt-5.4-mini",
+ "mcps": ["job-search", "candidate-profile-read"],
+ "skills": ["job-matching"]
+ },
+ "writer": {
+ "model": "anthropic/claude-sonnet-4-6",
+ "mcps": ["candidate-profile-read", "overleaf"],
+ "skills": ["application-writing"]
+ },
+ "oracle": {
+ "displayName": "checker",
+ "model": "openai/gpt-5.5",
+ "variant": "high",
+ "mcps": ["candidate-profile-read", "overleaf"],
+ "skills": ["application-review"]
+ },
+ "notifier": {
+ "model": "openai/gpt-5.4-mini",
+ "mcps": ["notification-draft"],
+ "skills": []
+ }
+ }
+ }
+}
+```
+
+Das Ganze lsst sich ber ein OMOS-Preset aktivieren:
+
+```text
+/preset acme-job-applications
+```
+
+Der Nutzer erhlt danach keine unsichtbare Bewerbungsmaschine. Er erhlt ein explizit aktiviertes, nachvollziehbares Team mit klaren Zustndigkeiten.
+
+## Wie das heute beginnen kann
+
+Die Idee muss nicht auf eine APM-Spec-nderung warten.
+
+Ein erster Prototyp braucht nur drei Bausteine:
+
+1. **Ein normales APM-Package**
+ Skills, Instructions und MCP-Abhngigkeiten werden ber APM installiert.
+
+2. **Ein Team-Profil als zustzliche Package-Datei**
+ Zum Beispiel `team-profile.yaml`, mit versioniertem Schema und eindeutiger ID.
+
+3. **Ein `omos` Sidecar-CLI**
+ Es liest die installierten Packages, generiert namespacete OMOS-Presets und validiert Konflikte.
+
+Der Ablauf wre:
+
+```bash
+apm install acme/job-application-team
+apm-team render --harness omos
+opencode
+```
+
+Dann:
+
+```text
+/preset acme-job-applications
+```
+
+APM selbst muss dafr zunchst keine unbekannten Targets akzeptieren. Tatschlich kennt das aktuelle Manifest nur einen festen Satz dokumentierter Targets; unbekannte Zielnamen fhren zu einem Fehler.
+
+Das Sidecar ist daher der pragmatische Anfang. Es kann als externes Tool reifen, Daten ber realistische Nutzung liefern und spter als offizieller APM-Adapter oder als Erweiterung im APM-Ö´kosystem landen.
+
+## Der Kern
+
+Die Idee lautet nicht: „APM wird jetzt ein Multi-Agent-Framework."
+
+APM soll sein, was es gut kann: Package-Management fr Agent-Kontext.
+
+oh-my-opencode-slim soll sein, was es gut kann: konkrete Rollen, Modelle und Laufzeit-Presets verwalten.
+
+Dazwischen liegt ein kleines, wertvolles Stck Infrastruktur:
+
+> Ein APM-Package beschreibt nicht nur, welche Fhigkeiten installiert werden. Es kann auch erklren, welches Team diese Fhigkeiten sinnvoll verwendet.
+
+Damit wird ein Package vom Ordner voller Skills zu einem reproduzierbaren Arbeitsmodell:
+
+- versionierte Rollen;
+- berprfbare Tool-Zugriffe;
+- lokale Modell-Policies;
+- mehrere Teams pro Projekt;
+- explizite Aktivierung;
+- menschliche Freigabe an kritischen Grenzen.
+
+Und das ist wesentlich ntzlicher als ein weiterer „autonomer Agent", der nach drei Minuten Browserzugriff beschliet, dass er nun Personalabteilung spielen darf.
\ No newline at end of file
diff --git a/omos/omos.py b/omos/omos.py
new file mode 100644
index 0000000..7d2b865
--- /dev/null
+++ b/omos/omos.py
@@ -0,0 +1,427 @@
+#!/usr/bin/env python3
+"""omos – Adapter zwischen APM-Team-Profilen und oh-my-opencode-slim.
+
+Liest ein team-profile.yaml aus einem APM-Package, löst Modellklassen gegen
+eine lokale Mapping-Tabelle auf und generiert ein namespacetes Preset für
+oh-my-opencode-slim.
+
+Kommandos:
+ omos render Team-Profil in OMOS-Preset übersetzen und schreiben
+ omos check Validieren ohne zu schreiben (Dry-Run)
+
+Beispiele:
+ python3 omos.py render --package examples/package --mapping ~/.config/apm-team/model-mapping.yaml
+ python3 omos.py check --package examples/package
+"""
+
+from __future__ import annotations
+
+import argparse
+import copy
+import json
+import re
+import sys
+from pathlib import Path
+
+import yaml
+
+# Eingebaute OMOS-Agenten. Alles andere wird als Custom Agent behandelt.
+BUILTIN_AGENTS = {
+ "orchestrator",
+ "oracle",
+ "librarian",
+ "explorer",
+ "fixer",
+ "designer",
+ "council",
+ "observer",
+}
+
+DEFAULT_MAPPING_PATH = Path.home() / ".config" / "apm-team" / "model-mapping.yaml"
+DEFAULT_TEAM_FILE = "team-profile.yaml"
+DEFAULT_OUTPUT = Path(".opencode") / "oh-my-opencode-slim.json"
+PROVENANCE_SUFFIX = ".omos-provenance.json"
+
+SUPPORTED_SCHEMA = ("corentic.team-profile/v1", "acme.team-profile/v1")
+
+
+class OmosError(Exception):
+ """Fehler mit nutzerlesbarer Ursache."""
+
+
+# ---------------------------------------------------------------------------
+# Laden
+
+
+def load_yaml(path: Path) -> dict:
+ if not path.exists():
+ raise OmosError(f"Datei nicht gefunden: {path}")
+ try:
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
+ except yaml.YAMLError as exc:
+ raise OmosError(f"YAML-Fehler in {path}: {exc}") from exc
+ if not isinstance(data, dict):
+ raise OmosError(f"{path} enthält kein YAML-Mapping")
+ return data
+
+
+def load_team_profile(package_dir: Path, team_file: str | None) -> tuple[dict, Path]:
+ candidates = (
+ [package_dir / team_file]
+ if team_file
+ else [package_dir / DEFAULT_TEAM_FILE, package_dir / "team.yaml"]
+ )
+ for candidate in candidates:
+ if candidate.exists():
+ profile = load_yaml(candidate)
+ schema = profile.get("schema")
+ if schema and schema not in SUPPORTED_SCHEMA:
+ print(
+ f"Warnung: Unbekanntes Schema '{schema}'. "
+ f"Unterstützt: {', '.join(SUPPORTED_SCHEMA)}. Fahre fort."
+ )
+ if not profile.get("id"):
+ raise OmosError(f"{candidate}: Feld 'id' fehlt")
+ roles = profile.get("roles")
+ if not isinstance(roles, list) or not roles:
+ raise OmosError(f"{candidate}: Keine Rollen definiert ('roles')")
+ return profile, candidate
+ searched = ", ".join(str(c) for c in candidates)
+ raise OmosError(f"Kein Team-Profil gefunden. Gesucht: {searched}")
+
+
+def load_mapping(mapping_path: Path | None) -> dict:
+ path = mapping_path or DEFAULT_MAPPING_PATH
+ if not path.exists():
+ raise OmosError(
+ f"Modell-Mapping nicht gefunden: {path}\n"
+ "Lege die Datei an, z. B.:\n"
+ " model_classes:\n"
+ " fast-research: ollama/qwen3.5:9b\n"
+ " high-reasoning:\n"
+ " model: ollama/qwen3.6:35b-a3b-q4_K_M\n"
+ " variant: thinking"
+ )
+ data = load_yaml(path)
+ classes = data.get("model_classes")
+ if not isinstance(classes, dict) or not classes:
+ raise OmosError(f"{path}: Sektion 'model_classes' fehlt oder ist leer")
+ return classes
+
+
+def resolve_model(model_class_entry: object, model_class: str, role_id: str) -> dict:
+ """Löst einen Mapping-Eintrag (string oder dict) zu Agent-Feldern auf."""
+ if isinstance(model_class_entry, str):
+ return {"model": model_class_entry}
+ if isinstance(model_class_entry, dict):
+ entry = {k: v for k, v in model_class_entry.items()}
+ if "model" not in entry:
+ raise OmosError(
+ f"Rolle '{role_id}': Mapping für '{model_class}' hat kein 'model'-Feld"
+ )
+ return entry
+ raise OmosError(f"Ungültiger Mapping-Eintrag für '{model_class}': {model_class_entry!r}")
+
+
+def sanitize_preset_name(team_id: str) -> str:
+ return re.sub(r"[^a-zA-Z0-9_-]+", "-", team_id).strip("-").lower()
+
+
+# ---------------------------------------------------------------------------
+# Übersetzung
+
+
+def build_preset(profile: dict, mapping: dict) -> tuple[dict, dict, list[str]]:
+ """Erzeugt (preset, custom_agents, warnings)."""
+ preset: dict = {}
+ custom_agents: dict = {}
+ warnings: list[str] = []
+
+ for role in profile["roles"]:
+ role_id = role.get("id")
+ if not role_id:
+ raise OmosError("Rolle ohne 'id' gefunden")
+
+ model_class = role.get("model_class")
+ if not model_class:
+ raise OmosError(f"Rolle '{role_id}': 'model_class' fehlt")
+ if model_class not in mapping:
+ raise OmosError(
+ f"Rolle '{role_id}': Modellklasse '{model_class}' ist nicht gemappt.\n"
+ f"Ergänze in deinem Mapping:\n"
+ f" {model_class}: "
+ )
+
+ agent_fields = resolve_model(mapping[model_class], model_class, role_id)
+
+ capabilities = role.get("capabilities", {}) or {}
+ mcps = list(capabilities.get("mcps", []) or [])
+ skills = list(capabilities.get("skills", []) or [])
+ purpose = str(role.get("purpose", "")).strip()
+
+ omos_agent = role.get("omos_agent", role_id)
+ if omos_agent == "custom":
+ # Explizit als Custom Agent markiert → rolleneigener Name.
+ agent_key = role_id
+ else:
+ agent_key = omos_agent
+
+ preset[agent_key] = {
+ **agent_fields,
+ "mcps": mcps,
+ "skills": skills,
+ }
+
+ if agent_key in BUILTIN_AGENTS and agent_key != role_id:
+ preset[agent_key]["displayName"] = role_id
+
+ if agent_key not in BUILTIN_AGENTS or omos_agent == "custom":
+ if agent_key not in custom_agents:
+ prompt = (
+ purpose
+ if purpose
+ else f"Custom Agent '{role_id}' aus Team-Profil "
+ f"'{profile.get('id', 'unbekannt')}'."
+ )
+ custom_agents[agent_key] = {
+ "model": agent_fields["model"],
+ "description": purpose or f"Custom subagent '{role_id}'",
+ "prompt": prompt,
+ # Dem Orchestrator sagen, wann er delegieren soll.
+ "orchestratorPrompt": (
+ f"@{agent_key}\n- Rolle: {purpose}\n"
+ "- Delegiere Aufgaben dieser Rolle an diesen Agenten."
+ if purpose
+ else f"@{agent_key}"
+ ),
+ }
+ else:
+ custom_agents[agent_key]["model"] = agent_fields["model"]
+
+ if not mcps and not skills:
+ warnings.append(
+ f"Rolle '{role_id}' ({agent_key}): keine MCPs/Skills zugewiesen "
+ "(rein koordinierend?)"
+ )
+
+ orchestrator_present = any(k == "orchestrator" for k in preset)
+ if not orchestrator_present:
+ warnings.append(
+ "Kein 'orchestrator' im Team. Ohne Orchestrator-Preset-Eintrag bleibt "
+ "dessen Modell unverändert."
+ )
+
+ return preset, custom_agents, warnings
+
+
+def merge_into_config(config: dict, preset_name: str, preset: dict,
+ custom_agents: dict) -> dict:
+ merged = copy.deepcopy(config)
+ presets = merged.setdefault("presets", {})
+ existing = presets.get(preset_name)
+ if existing:
+ print(
+ f"Hinweis: Preset '{preset_name}' existierte bereits und wird ersetzt "
+ "(andere Presets bleiben unberührt)."
+ )
+ presets[preset_name] = preset
+
+ agents = merged.setdefault("agents", {})
+ agents.update(custom_agents)
+ return merged
+
+
+# ---------------------------------------------------------------------------
+# MCP-Verfügbarkeit prüfen (best effort)
+
+
+def strip_jsonc(text: str) -> str:
+ text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
+ text = re.sub(r"(^|\s)//[^\n]*", r"\1", text)
+ return text
+
+
+def configured_mcps() -> set[str]:
+ candidates = [
+ Path.home() / ".config" / "opencode" / "opencode.jsonc",
+ Path.home() / ".config" / "opencode" / "opencode.json",
+ ]
+ for path in candidates:
+ if path.exists():
+ try:
+ data = json.loads(strip_jsonc(path.read_text(encoding="utf-8")))
+ return set((data.get("mcp") or {}).keys())
+ except (json.JSONDecodeError, OSError):
+ continue
+ return set()
+
+
+def check_mcps(preset: dict, warnings: list[str]) -> None:
+ available = configured_mcps()
+ if not available:
+ warnings.append(
+ "Konnte opencode.json(c) nicht lesen – MCP-Prüfung übersprungen."
+ )
+ return
+ needed: set[str] = set()
+ for agent in preset.values():
+ needed.update(agent.get("mcps", []) or [])
+ missing = sorted(m for m in needed if m not in available)
+ if missing:
+ warnings.append(
+ "MCPs im Team-Profil, aber nicht in opencode.json konfiguriert: "
+ + ", ".join(missing)
+ )
+
+
+# ---------------------------------------------------------------------------
+# Kommandos
+
+
+def package_output_path(package_dir: Path) -> Path:
+ return package_dir / DEFAULT_OUTPUT
+
+
+def cmd_render(args: argparse.Namespace) -> int:
+ package_dir = Path(args.package).resolve()
+ profile, profile_path = load_team_profile(package_dir, args.team_file)
+ mapping = load_mapping(Path(args.mapping) if args.mapping else None)
+
+ preset, custom_agents, warnings = build_preset(profile, mapping)
+
+ output = Path(args.output) if args.output else package_dir / DEFAULT_OUTPUT
+ if output.exists():
+ try:
+ config = json.loads(output.read_text(encoding="utf-8"))
+ except json.JSONDecodeError as exc:
+ raise OmosError(f"{output} ist kein gültiges JSON: {exc}")
+ else:
+ config = {}
+
+ config["$schema"] = (
+ "https://unpkg.com/oh-my-opencode-slim@latest/"
+ "oh-my-opencode-slim.schema.json"
+ )
+ merged = merge_into_config(
+ config, sanitize_preset_name(str(profile["id"])), preset, custom_agents
+ )
+
+ if args.dry_run:
+ print(json.dumps(merged, indent=2, ensure_ascii=False))
+ _report(warnings, dry_run=True)
+ return 0
+
+ output.parent.mkdir(parents=True, exist_ok=True)
+ output.write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding="utf-8")
+ print(f"Geschrieben: {output}")
+
+ provenance = {
+ "profile": profile["id"],
+ "profileFile": str(profile_path),
+ "teamId": profile["id"],
+ "description": profile.get("description", ""),
+ "presetName": sanitize_preset_name(str(profile["id"])),
+ "roles": {
+ r.get("id"): {
+ "omosAgent": r.get("omos_agent", r.get("id")),
+ "modelClass": r.get("model_class"),
+ "resolvedModel": mapping[r["model_class"]],
+ "mcps": (r.get("capabilities", {}) or {}).get("mcps", []),
+ "skills": (r.get("capabilities", {}) or {}).get("skills", []),
+ }
+ for r in profile["roles"]
+ },
+ }
+ provenance_path = output.with_suffix(PROVENANCE_SUFFIX)
+ provenance_path.write_text(
+ json.dumps(provenance, indent=2, ensure_ascii=False), encoding="utf-8"
+ )
+ print(f"Provenance: {provenance_path}")
+
+ _report(warnings)
+ print(
+ "\nNächste Schritte:\n"
+ " 1. opencode starten (Projekt-Config wird geladen)\n"
+ f" 2. /preset {sanitize_preset_name(str(profile['id']))}\n"
+ " 3. OpenCode neu laden → Team aktiv"
+ )
+ return 0
+
+
+def cmd_check(args: argparse.Namespace) -> int:
+ args.dry_run = True
+ # Dry-Run: vorhandene Projekt-Config einbeziehen, aber nie schreiben.
+ existing = package_output_path(Path(args.package).resolve())
+ args.output = str(existing) if existing.exists() else None
+ try:
+ cmd_render(args)
+ except OmosError as exc:
+ print(f"Fehler: {exc}", file=sys.stderr)
+ return 1
+ return 0
+
+
+def _report(warnings: list[str], dry_run: bool = False) -> None:
+ if dry_run:
+ print("\n--- Dry-Run: Es wurde nichts geschrieben ---")
+ if warnings:
+ print("\nWarnungen:")
+ for warning in warnings:
+ print(f" ⚠ {warning}")
+ else:
+ print("\nKeine Warnungen.")
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(
+ prog="omos",
+ description=(
+ "Adapter zwischen APM-Team-Profilen und oh-my-opencode-slim. "
+ "Übersetzt deterministisch: Team-Profil + lokales Modell-Mapping "
+ "-> OMOS-Preset."
+ ),
+ )
+ sub = parser.add_subparsers(dest="command", required=True)
+
+ def common(p: argparse.ArgumentParser) -> None:
+ p.add_argument(
+ "--package",
+ default=".",
+ help="Pfad zum APM-Package (mit team-profile.yaml)",
+ )
+ p.add_argument(
+ "--team-file",
+ help="Alternativer Dateiname des Team-Profils (Default: team-profile.yaml)",
+ )
+ p.add_argument(
+ "--mapping",
+ help=f"Pfad zur Modell-Mapping-Datei (Default: {DEFAULT_MAPPING_PATH})",
+ )
+
+ p_render = sub.add_parser("render", help="Preset generieren und schreiben")
+ common(p_render)
+ p_render.add_argument(
+ "--output",
+ help=f"Zieldatei (Default: {DEFAULT_OUTPUT} relativ zum Package)",
+ )
+ p_render.add_argument(
+ "--dry-run",
+ action="store_true",
+ help="Nur anzeigen, nichts schreiben",
+ )
+ p_render.set_defaults(func=cmd_render)
+
+ p_check = sub.add_parser("check", help="Validieren ohne zu schreiben")
+ common(p_check)
+ p_check.set_defaults(func=cmd_check)
+
+ args = parser.parse_args(argv)
+ try:
+ return args.func(args)
+ except OmosError as exc:
+ print(f"Fehler: {exc}", file=sys.stderr)
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())