feat(omos): sidecar adapter prototype
Deterministic translator between APM team profiles and oh-my-opencode-slim presets. Reads team-profile.yaml, resolves model classes against a user-local mapping table, generates namespaced presets plus provenance. Includes check (dry-run), merge-by-preset-id, custom agent generation from purpose fields, and MCP availability warnings.
This commit is contained in:
parent
8ada9fb6e4
commit
907f43d40b
7 changed files with 913 additions and 0 deletions
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
10
.idea/APM-Packages.iml
generated
Normal file
10
.idea/APM-Packages.iml
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PackageRequirementsSettings" />
|
||||||
|
<component name="ReSTService" />
|
||||||
|
</module>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/APM-Packages.iml" filepath="$PROJECT_DIR$/.idea/APM-Packages.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
453
apm-omos-agent-teams-v3.md
Normal file
453
apm-omos-agent-teams-v3.md
Normal file
|
|
@ -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 <name>` 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.
|
||||||
427
omos/omos.py
Normal file
427
omos/omos.py
Normal file
|
|
@ -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}: <provider/model>"
|
||||||
|
)
|
||||||
|
|
||||||
|
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())
|
||||||
Loading…
Add table
Reference in a new issue