docs(cpm): translate all CPM project files to English

CPM is an international project; only the blogpost stays German.
Translated: cpm.py docstring/messages/help texts, README.md,
cpm-setup skill, all team-profile.yaml files (incl. runtime_agent
comments), example apm.yml wrappers and the model-mapping template.
Also fixes DEFAULT_MAPPING_PATH pointing to ~/.config/apm-team
instead of ~/.config/cpm.
This commit is contained in:
Tobias J. Endres 2026-08-23 17:56:09 +02:00
parent 913be9d528
commit 0df9cec833
9 changed files with 341 additions and 347 deletions

View file

@ -1,168 +1,168 @@
# Corentic Package Manager (CPM) # Corentic Package Manager (CPM)
Ein **CPM-Package** ist ein Agent-Package bestehend aus einem [APM](https://microsoft.github.io/apm/)-Manifest plus Team-Konfiguration (`team-profile.yaml`). Der Corentic Package Manager übersetzt dieses Package in die native Konfiguration eines Ziel-Harnesses. A **CPM package** is an agent package consisting of an [APM](https://microsoft.github.io/apm/) manifest plus a team configuration (`team-profile.yaml`). The Corentic Package Manager translates this package into the native configuration of a target harness.
Übersetzungsziele: Translation targets:
| Ziel-Harness | Status | | Target harness | Status |
|---|---| |---|---|
| [oh-my-opencode-slim](https://ohmyopencodeslim.com/) | ✅ implementiert | | [oh-my-opencode-slim](https://ohmyopencodeslim.com/) | ✅ implemented |
| oh-my-pi | geplant | | oh-my-pi | planned |
| Codex / Claude / Copilot | Vision | | Codex / Claude / Copilot | vision |
Deterministisch der CPM rät nicht. Er nimmt keine Verfügbarkeit an und wählt keine Modelle automatisch aus. Deterministic the CPM does not guess. It assumes no availability and never picks models automatically.
``` ```
CPM-Package Nutzer Runtime CPM package User Runtime
apm.yml + + model-mapping.yaml → cpm render → .opencode/ apm.yml + + model-mapping.yaml → cpm render → .opencode/
team-profile.yaml (Modellklasse → oh-my-opencode-slim.json team-profile.yaml (model class → oh-my-opencode-slim.json
(Rollen, Modell- konkrete Modell-ID) (oder künftig: natives (roles, model concrete model ID) (or later: native config
klassen, MCPs/Skills) Config eines anderen Harness) classes, MCPs/skills) of another harness)
``` ```
## Voraussetzungen ## Prerequisites
- Python 3.10+ mit PyYAML (`pip install pyyaml`) - Python 3.10+ with PyYAML (`pip install pyyaml`)
- OpenCode mit installiertem Plugin [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) - OpenCode with the [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) plugin installed
- Ein CPM-Package mit `team-profile.yaml` - A CPM package with a `team-profile.yaml`
## Schnellstart ## Quickstart
```bash ```bash
# 1. Modell-Mapping anlegen interaktiv, mit Auswahl aus deinen opencode-Modellen # 1. Create the model mapping interactively, choosing from your opencode models
python3 cpm.py init --fill python3 cpm.py init --fill
# 2. Geführt: Team-Profil lesen, Mapping prüfen/auffüllen, rendern # 2. Guided: read team profile, check/fill mapping, render
cd <projekt> cd <project>
python3 /pfad/zu/cpm.py setup --package /pfad/zum/package python3 /path/to/cpm.py setup --package /path/to/package
# 3. Aktivieren # 3. Activate
opencode # Projekt-Config wird geladen opencode # project config gets loaded
/preset acme-job-applications /preset acme-job-applications
# Reload OpenCode → Team aktiv (by Design: kein Hot-Swap) # Reload OpenCode -> team is active (by design: no hot-swap)
``` ```
Alternativ manuell: Alternatively manually:
```bash ```bash
# 1. Modell-Mapping einmalig anlegen # 1. Create the model mapping once
mkdir -p ~/.config/cpm mkdir -p ~/.config/cpm
cp examples/model-mapping.yaml ~/.config/cpm/model-mapping.yaml cp examples/model-mapping.yaml ~/.config/cpm/model-mapping.yaml
$EDITOR ~/.config/cpm/model-mapping.yaml # an eigene Modelle anpassen $EDITOR ~/.config/cpm/model-mapping.yaml # adapt to your own models
# 2. Validieren (Dry-Run) # 2. Validate (dry run)
python3 cpm.py check --package examples/package python3 cpm.py check --package examples/package
# 3. Rendern # 3. Render
cd <projekt> cd <project>
python3 /pfad/zu/cpm.py render --package /pfad/zum/package python3 /path/to/cpm.py render --package /path/to/package
``` ```
## Die drei Dateien ## The three files
| Datei | Gehört | Zweck | Manuell editieren? | | File | Owned by | Purpose | Edit manually? |
|---|---|---|---| |---|---|---|---|
| `team-profile.yaml` | Package-Autor | Rollen, Modellklassen, MCP-/Skill-Allowlists pro Rolle | Ja (Autor) | | `team-profile.yaml` | Package author | Roles, model classes, MCP/skill allowlists per role | Yes (author) |
| `~/.config/cpm/model-mapping.yaml` | Nutzer | Modellklasse → konkrete Modell-ID (+ optional variant/temperature) | Ja (Nutzer) | | `~/.config/cpm/model-mapping.yaml` | User | Model class → concrete model ID (+ optional variant/temperature) | Yes (user) |
| `.opencode/oh-my-opencode-slim.json` | generiert | Preset mit konkreten Modell-IDs | **Nein** immer neu rendern | | `.opencode/oh-my-opencode-slim.json` | generated | Preset with concrete model IDs | **No** always re-render |
Dazu eine Provenance-Datei `.opencode/oh-my-opencode-slim.cpm-provenance.json`, die dokumentiert, welche Rolle welches Modell erhalten hat. Plus a provenance file `.opencode/oh-my-opencode-slim.cpm-provenance.json` documenting which role received which model.
## Kommandoreferenz ## Command reference
```bash ```bash
python3 cpm.py init [--mapping FILE] [--fill] # Mapping anlegen/füllen python3 cpm.py init [--mapping FILE] [--fill] # create/fill mapping
python3 cpm.py setup --package DIR [--mapping F] ... # geführter Ablauf python3 cpm.py setup --package DIR [--mapping F] ... # guided flow
python3 cpm.py render --package DIR [--mapping F] [--output F] [--dry-run] python3 cpm.py render --package DIR [--mapping F] [--output F] [--dry-run]
python3 cpm.py check --package DIR [--mapping F] # Dry-Run-Validierung python3 cpm.py check --package DIR [--mapping F] # dry-run validation
``` ```
### `init` Mapping anlegen ### `init` create the mapping
Erstellt `~/.config/cpm/model-mapping.yaml` (falls nicht vorhanden). Mit `--fill` werden alle `PLACEHOLDER`-Klassen interaktiv abgefragt: der CPM listet alle Modelle aus deiner `opencode.json(c)` auf, du wählst per Nummer oder gibst eine Modell-ID manuell ein. Creates `~/.config/cpm/model-mapping.yaml` (if not present). With `--fill`, all `PLACEHOLDER` classes are asked interactively: the CPM lists every model from your `opencode.json(c)`, you pick by number or enter a model ID manually.
### `setup` Geführter Ablauf ### `setup` guided flow
Der Ein-Kommando-Weg: zeigt das Team-Profil verständlich an, legt fehlendes Mapping an, fragt fehlende Modellklassen interaktiv ab (gleiche Auswahl wie `init --fill`), rendert und gibt die Aktivierungsschritte aus. The one-command path: shows the team profile in plain language, creates a missing mapping, interactively asks for missing model classes (same selection as `init --fill`), renders and prints the activation steps.
### `render` / `check` ### `render` / `check`
| Option | Default | Bedeutung | | Option | Default | Meaning |
|---|---|---| |---|---|---|
| `--package` | `.` | CPM-Package-Verzeichnis mit `team-profile.yaml` | | `--package` | `.` | CPM package directory containing `team-profile.yaml` |
| `--mapping` | `~/.config/cpm/model-mapping.yaml` | Lokale Mapping-Tabelle | | `--mapping` | `~/.config/cpm/model-mapping.yaml` | Local mapping table |
| `--output` | `<package>/.opencode/oh-my-opencode-slim.json` | Zieldatei | | `--output` | `<package>/.opencode/oh-my-opencode-slim.json` | Target file |
| `--team-file` | `team-profile.yaml` (alternativ `team.yaml`) | Alternativer Profilname | | `--team-file` | `team-profile.yaml` (alternatively `team.yaml`) | Alternative profile filename |
**Merge-Verhalten:** Existierende Presets in der Zieldatei bleiben unberührt; nur das Preset der Team-ID wird ersetzt. Mehrere Packages koexistieren damit im selben Projekt. Aktiviert wird explizit per `/preset <name>`. **Merge behavior:** Existing presets in the target file remain untouched; only the preset for this team ID is replaced. Multiple packages coexist in the same project. Activation is explicit via `/preset <name>`.
## Team-Profil-Schema (`corentic.team-profile/v1`) ## Team profile schema (`corentic.team-profile/v1`)
```yaml ```yaml
schema: corentic.team-profile/v1 schema: corentic.team-profile/v1
id: acme.job-applications # wird zum Preset-Namen (namespaced) id: acme.job-applications # becomes the preset name (namespaced)
description: ... description: ...
roles: roles:
- id: researcher # fachliche Rolle - id: researcher # domain role
purpose: ... # wird zu Prompt/Description für Custom Agents purpose: ... # becomes prompt/description for custom agents
runtime_agent: librarian # optional: Harness-Builtin oder 'custom' runtime_agent: librarian # optional: harness builtin or 'custom'
# (Default: role-id als Custom Agent) # (default: role id as custom agent)
model_class: fast-research # Pflicht, muss im Mapping existieren model_class: fast-research # required, must exist in the mapping
capabilities: capabilities:
mcps: [websearch, openviking] # MCP-Allowlist ([] = keine) mcps: [websearch, openviking] # MCP allowlist ([] = none)
skills: [job-application] # Skill-Allowlist ([] = keine) skills: [job-application] # skill allowlist ([] = none)
``` ```
Das Feld `runtime_agent` ist bewusst harness-neutral benannt: Der Adapter entscheidet, wie die Rolle im Ziel-Harness repräsentiert wird. Erkannte Builtins des oh-my-opencode-slim-Adapters: `orchestrator`, `oracle`, `librarian`, `explorer`, `fixer`, `designer`, `council`, `observer`. Alles andere (oder `runtime_agent: custom`) erzeugt einen Custom Agent inklusive `prompt` und `orchestratorPrompt` aus dem `purpose`-Feld. The `runtime_agent` field is deliberately harness-neutral: the adapter decides how the role is represented in the target harness. Builtins recognized by the oh-my-opencode-slim adapter: `orchestrator`, `oracle`, `librarian`, `explorer`, `fixer`, `designer`, `council`, `observer`. Everything else (or `runtime_agent: custom`) creates a custom agent including `prompt` and `orchestratorPrompt` derived from the `purpose` field.
### Mapping-Formate ### Mapping formats
```yaml ```yaml
model_classes: model_classes:
fast-research: ollama/qwen3.5:9b # einfach fast-research: ollama/qwen3.5:9b # simple
high-reasoning: # erweitert high-reasoning: # extended
model: ollama/qwen3.6:35b-a3b-q4_K_M model: ollama/qwen3.6:35b-a3b-q4_K_M
variant: thinking variant: thinking
temperature: 0.3 temperature: 0.3
``` ```
Alle Zusatzfelder werden 1:1 in den Agent-Eintrag des Presets übernommen. All extra fields are passed through 1:1 into the agent entry of the preset.
## Was der CPM prüft ## What the CPM checks
1. Team-Profil vorhanden, Schema bekannt, `id` und `roles` vorhanden 1. Team profile present, schema known, `id` and `roles` present
2. Jede `model_class` hat einen Mapping-Eintrag (sonst Fehler mit Lösungshinweis) 2. Every `model_class` has a mapping entry (otherwise error with fix hint)
3. MCPs gegen `~/.config/opencode/opencode.json(c)` abgleichen (**Warnung**, kein Abbruch) 3. MCPs matched against `~/.config/opencode/opencode.json(c)` (**warning**, not fatal)
4. Rollen ohne MCPs/Skills → Hinweis 4. Roles without MCPs/skills -> note
## Beispiele ## Examples
| Beispiel | Basis | Team | | Example | Based on | Team |
|---|---|---| |---|---|---|
| `examples/package/` | Eigenes `job-application`-Package (OpenViking + ShareLaTeX) | `acme.job-applications` Researcher, Writer, Checker, Notifier | | `examples/package/` | Own `job-application` package (OpenViking + ShareLaTeX) | `acme.job-applications` Researcher, Writer, Checker, Notifier |
| `examples/microsoft-design-review/` | Offizielles [microsoft/apm-sample-package](https://github.com/microsoft/apm-sample-package) | `acme.design-review` Reviewer (Oracle), Style-Checker (Fixer), Accessibility-Auditor | | `examples/microsoft-design-review/` | Official [microsoft/apm-sample-package](https://github.com/microsoft/apm-sample-package) | `acme.design-review` Reviewer (Oracle), Style-Checker (Fixer), Accessibility-Auditor |
| `examples/microsoft-issue-autopilot/` | Offizielles [`apm-issue-autopilot`](https://github.com/microsoft/apm/tree/main/packages/apm-issue-autopilot) aus dem microsoft/apm-Repo | `acme.issue-autopilot` Triager, Shepherd, PR-Writer, Reviewer (+ GitHub-MCP) | | `examples/microsoft-issue-autopilot/` | Official [`apm-issue-autopilot`](https://github.com/microsoft/apm/tree/main/packages/apm-issue-autopilot) from the microsoft/apm repo | `acme.issue-autopilot` Triager, Shepherd, PR-Writer, Reviewer (+ GitHub MCP) |
Die beiden Microsoft-Beispiele sind dünne CPM-Wrapper: Das eigene `apm.yml` zieht das offizielle Package als versionierte APM-Dependency (`microsoft/apm-sample-package#v1.0.0` bzw. Monorepo-Subpath `microsoft/apm/packages/apm-issue-autopilot`) und liefert nur die Team-Empfehlung dazu. The two Microsoft examples are thin CPM wrappers: their own `apm.yml` pulls the official package as a versioned APM dependency (`microsoft/apm-sample-package#v1.0.0` or monorepo subpath `microsoft/apm/packages/apm-issue-autopilot`) and only adds the team recommendation.
```bash ```bash
cd corentic-package-manager/examples cd corentic-package-manager/examples
# Trockenlauf: was würde generiert? # Dry run: what would be generated?
python3 ../cpm.py check --package package --mapping model-mapping.yaml python3 ../cpm.py check --package package --mapping model-mapping.yaml
python3 ../cpm.py check --package microsoft-design-review --mapping model-mapping.yaml python3 ../cpm.py check --package microsoft-design-review --mapping model-mapping.yaml
python3 ../cpm.py check --package microsoft-issue-autopilot --mapping model-mapping.yaml python3 ../cpm.py check --package microsoft-issue-autopilot --mapping model-mapping.yaml
# Rendern in das Beispiel-Projekt # Render into the example project
python3 ../cpm.py render --package package \ python3 ../cpm.py render --package package \
--mapping model-mapping.yaml \ --mapping model-mapping.yaml \
--output package/.opencode/oh-my-opencode-slim.json --output package/.opencode/oh-my-opencode-slim.json
``` ```
Erwartetes Ergebnis: Preset `acme-job-applications` mit fünf Agenten `orchestrator`, `librarian` (Alias `researcher`), Custom Agent `writer`, `oracle` (Alias `checker`), Custom Agent `notifier`. Der Researcher erhält keinen LaTeX-Zugang, der Writer keine Job-Suchtools, der Notifier gar keine externen Zugriffe. Expected result: preset `acme-job-applications` with five agents `orchestrator`, `librarian` (alias `researcher`), custom agent `writer`, `oracle` (alias `checker`), custom agent `notifier`. The researcher gets no LaTeX access, the writer sees no job-search tools, the notifier gets no external access at all.
Danach: Then:
```bash ```bash
cd package && opencode cd package && opencode
@ -171,16 +171,16 @@ cd package && opencode
## Troubleshooting ## Troubleshooting
| Problem | Ursache/Lösung | | Problem | Cause/Fix |
|---|---| |---|---|
| `Modellklasse X ist nicht gemappt` | Mapping ergänzen, erneut rendern | | `Model class X is not mapped` | Add to the mapping, re-render |
| `Warnung: MCP ... nicht konfiguriert` | MCP in `opencode.jsonc` einrichten oder aus dem Profil entfernen | | `Warning: MCP ... not configured` | Set up the MCP in `opencode.jsonc` or remove it from the profile |
| `/preset` zeigt nichts Neues | OpenCode nach dem Render neu laden; Preset greift nicht mid-session | | `/preset` does not show anything new | Reload OpenCode after rendering; presets do not apply mid-session |
| Preset weg nach manuellem Editieren | Datei ist generiert Änderungen gehören ins Mapping oder Team-Profil | | Preset lost after manual editing | The file is generated changes belong in the mapping or team profile |
| YAML-Fehler | Zeilenangabe aus der Fehlermeldung folgen | | YAML errors | Follow the line number in the error message |
## Grenzen (bewusst) ## Deliberate limits
- Der CPM startet keine Agenten und orchestriert nichts zur Laufzeit das macht der Ziel-Harness. - The CPM does not start agents and orchestrates nothing at runtime that is the job of the target harness.
- MCP-/Skill-Allowlists sind Capability Scoping, keine Sandbox. Irreversible Aktionen brauchen serverseitige Autorisierung + Human Approval. - MCP/skill allowlists are capability scoping, not a sandbox. Irreversible actions need server-side authorization + human approval.
- Die Modellverfügbarkeit wird gegen die opencode.jsonc-Namen geprüft, nicht per API-Healthcheck. - Model availability is checked against opencode.json(c) names, not via API health checks.

View file

@ -1,23 +1,22 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""cpm Corentic Package Manager. """cpm Corentic Package Manager.
Ein CPM-Package ist ein Agent-Package bestehend aus einem APM-Manifest plus A CPM package is an agent package consisting of an APM manifest plus a team
Team-Konfiguration (team-profile.yaml). Der CPM übersetzt dieses Package in configuration (team-profile.yaml). The CPM translates this package into the
die native Konfiguration eines Ziel-Harnesses. Erstes Übersetzungsziel: native configuration of a target harness. First translation target:
oh-my-opencode-slim. Weitere Ziele (oh-my-pi, codex, claude, copilot) sind oh-my-opencode-slim. Further targets (oh-my-pi, codex, claude, copilot) are
geplant. planned.
Liest ein team-profile.yaml, löst Modellklassen gegen eine lokale Reads a team-profile.yaml, resolves model classes against a local mapping
Mapping-Tabelle auf und generiert ein namespacetes Preset für den table and generates a namespaced preset for the target harness.
Ziel-Harness.
Kommandos: Commands:
cpm init Nutzer-Mapping anlegen (optional interaktiv füllen) cpm init Create user mapping file (optionally fill interactively)
cpm setup Geführt: Profil lesen, Mapping auffüllen, rendern cpm setup Guided: read profile, fill mapping, render
cpm render Team-Profil in Harness-Preset übersetzen und schreiben cpm render Translate team profile into a harness preset and write it
cpm check Validieren ohne zu schreiben (Dry-Run) cpm check Validate without writing (dry-run)
Beispiele: Examples:
python3 cpm.py setup --package examples/package python3 cpm.py setup --package examples/package
python3 cpm.py render --package examples/package --mapping ~/.config/cpm/model-mapping.yaml python3 cpm.py render --package examples/package --mapping ~/.config/cpm/model-mapping.yaml
python3 cpm.py check --package examples/package python3 cpm.py check --package examples/package
@ -34,8 +33,8 @@ from pathlib import Path
import yaml import yaml
# Eingebaute Agenten des ersten Übersetzungsziels oh-my-opencode-slim. # Built-in agents of the first translation target oh-my-opencode-slim.
# Alles andere wird als Custom Agent behandelt. # Everything else is treated as a custom agent.
BUILTIN_AGENTS = { BUILTIN_AGENTS = {
"orchestrator", "orchestrator",
"oracle", "oracle",
@ -47,31 +46,31 @@ BUILTIN_AGENTS = {
"observer", "observer",
} }
DEFAULT_MAPPING_PATH = Path.home() / ".config" / "apm-team" / "model-mapping.yaml" DEFAULT_MAPPING_PATH = Path.home() / ".config" / "cpm" / "model-mapping.yaml"
DEFAULT_TEAM_FILE = "team-profile.yaml" DEFAULT_TEAM_FILE = "team-profile.yaml"
DEFAULT_OUTPUT = Path(".opencode") / "oh-my-opencode-slim.json" DEFAULT_OUTPUT = Path(".opencode") / "oh-my-opencode-slim.json"
PROVENANCE_SUFFIX = ".cpm-provenance.json" PROVENANCE_SUFFIX = ".cpm-provenance.json"
SUPPORTED_SCHEMA = ("corentic.team-profile/v1", "acme.team-profile/v1") SUPPORTED_SCHEMA = ("corentic.team-profile/v1",)
class OmosError(Exception): class CpmError(Exception):
"""Fehler mit nutzerlesbarer Ursache.""" """Error with a user-readable cause."""
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Laden # Loading
def load_yaml(path: Path) -> dict: def load_yaml(path: Path) -> dict:
if not path.exists(): if not path.exists():
raise OmosError(f"Datei nicht gefunden: {path}") raise CpmError(f"File not found: {path}")
try: try:
data = yaml.safe_load(path.read_text(encoding="utf-8")) data = yaml.safe_load(path.read_text(encoding="utf-8"))
except yaml.YAMLError as exc: except yaml.YAMLError as exc:
raise OmosError(f"YAML-Fehler in {path}: {exc}") from exc raise CpmError(f"YAML error in {path}: {exc}") from exc
if not isinstance(data, dict): if not isinstance(data, dict):
raise OmosError(f"{path} enthält kein YAML-Mapping") raise CpmError(f"{path} does not contain a YAML mapping")
return data return data
@ -87,25 +86,25 @@ def load_team_profile(package_dir: Path, team_file: str | None) -> tuple[dict, P
schema = profile.get("schema") schema = profile.get("schema")
if schema and schema not in SUPPORTED_SCHEMA: if schema and schema not in SUPPORTED_SCHEMA:
print( print(
f"Warnung: Unbekanntes Schema '{schema}'. " f"Warning: Unknown schema '{schema}'. "
f"Unterstützt: {', '.join(SUPPORTED_SCHEMA)}. Fahre fort." f"Supported: {', '.join(SUPPORTED_SCHEMA)}. Continuing."
) )
if not profile.get("id"): if not profile.get("id"):
raise OmosError(f"{candidate}: Feld 'id' fehlt") raise CpmError(f"{candidate}: missing field 'id'")
roles = profile.get("roles") roles = profile.get("roles")
if not isinstance(roles, list) or not roles: if not isinstance(roles, list) or not roles:
raise OmosError(f"{candidate}: Keine Rollen definiert ('roles')") raise CpmError(f"{candidate}: no roles defined ('roles')")
return profile, candidate return profile, candidate
searched = ", ".join(str(c) for c in candidates) searched = ", ".join(str(c) for c in candidates)
raise OmosError(f"Kein Team-Profil gefunden. Gesucht: {searched}") raise CpmError(f"No team profile found. Searched: {searched}")
def load_mapping(mapping_path: Path | None) -> dict: def load_mapping(mapping_path: Path | None) -> dict:
path = mapping_path or DEFAULT_MAPPING_PATH path = mapping_path or DEFAULT_MAPPING_PATH
if not path.exists(): if not path.exists():
raise OmosError( raise CpmError(
f"Modell-Mapping nicht gefunden: {path}\n" f"Model mapping not found: {path}\n"
"Lege die Datei an, z. B.:\n" "Create the file, e.g.:\n"
" model_classes:\n" " model_classes:\n"
" fast-research: ollama/qwen3.5:9b\n" " fast-research: ollama/qwen3.5:9b\n"
" high-reasoning:\n" " high-reasoning:\n"
@ -115,22 +114,22 @@ def load_mapping(mapping_path: Path | None) -> dict:
data = load_yaml(path) data = load_yaml(path)
classes = data.get("model_classes") classes = data.get("model_classes")
if not isinstance(classes, dict) or not classes: if not isinstance(classes, dict) or not classes:
raise OmosError(f"{path}: Sektion 'model_classes' fehlt oder ist leer") raise CpmError(f"{path}: section 'model_classes' missing or empty")
return classes return classes
def resolve_model(model_class_entry: object, model_class: str, role_id: str) -> dict: 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.""" """Resolve a mapping entry (string or dict) into agent fields."""
if isinstance(model_class_entry, str): if isinstance(model_class_entry, str):
return {"model": model_class_entry} return {"model": model_class_entry}
if isinstance(model_class_entry, dict): if isinstance(model_class_entry, dict):
entry = {k: v for k, v in model_class_entry.items()} entry = {k: v for k, v in model_class_entry.items()}
if "model" not in entry: if "model" not in entry:
raise OmosError( raise CpmError(
f"Rolle '{role_id}': Mapping für '{model_class}' hat kein 'model'-Feld" f"Role '{role_id}': mapping for '{model_class}' has no 'model' field"
) )
return entry return entry
raise OmosError(f"Ungültiger Mapping-Eintrag für '{model_class}': {model_class_entry!r}") raise CpmError(f"Invalid mapping entry for '{model_class}': {model_class_entry!r}")
def sanitize_preset_name(team_id: str) -> str: def sanitize_preset_name(team_id: str) -> str:
@ -138,11 +137,11 @@ def sanitize_preset_name(team_id: str) -> str:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Übersetzung # Translation
def build_preset(profile: dict, mapping: dict) -> tuple[dict, dict, list[str]]: def build_preset(profile: dict, mapping: dict) -> tuple[dict, dict, list[str]]:
"""Erzeugt (preset, custom_agents, warnings).""" """Returns (preset, custom_agents, warnings)."""
preset: dict = {} preset: dict = {}
custom_agents: dict = {} custom_agents: dict = {}
warnings: list[str] = [] warnings: list[str] = []
@ -150,15 +149,15 @@ def build_preset(profile: dict, mapping: dict) -> tuple[dict, dict, list[str]]:
for role in profile["roles"]: for role in profile["roles"]:
role_id = role.get("id") role_id = role.get("id")
if not role_id: if not role_id:
raise OmosError("Rolle ohne 'id' gefunden") raise CpmError("Found a role without 'id'")
model_class = role.get("model_class") model_class = role.get("model_class")
if not model_class: if not model_class:
raise OmosError(f"Rolle '{role_id}': 'model_class' fehlt") raise CpmError(f"Role '{role_id}': 'model_class' is missing")
if model_class not in mapping: if model_class not in mapping:
raise OmosError( raise CpmError(
f"Rolle '{role_id}': Modellklasse '{model_class}' ist nicht gemappt.\n" f"Role '{role_id}': model class '{model_class}' is not mapped.\n"
f"Ergänze in deinem Mapping:\n" f"Add to your mapping:\n"
f" {model_class}: <provider/model>" f" {model_class}: <provider/model>"
) )
@ -171,7 +170,7 @@ def build_preset(profile: dict, mapping: dict) -> tuple[dict, dict, list[str]]:
runtime_agent = role.get("runtime_agent", role_id) runtime_agent = role.get("runtime_agent", role_id)
if runtime_agent == "custom": if runtime_agent == "custom":
# Explizit als Custom Agent markiert → rolleneigener Name. # Explicitly marked as custom agent -> use the role's own name.
agent_key = role_id agent_key = role_id
else: else:
agent_key = runtime_agent agent_key = runtime_agent
@ -190,17 +189,17 @@ def build_preset(profile: dict, mapping: dict) -> tuple[dict, dict, list[str]]:
prompt = ( prompt = (
purpose purpose
if purpose if purpose
else f"Custom Agent '{role_id}' aus Team-Profil " else f"Custom agent '{role_id}' from team profile "
f"'{profile.get('id', 'unbekannt')}'." f"'{profile.get('id', 'unknown')}'."
) )
custom_agents[agent_key] = { custom_agents[agent_key] = {
"model": agent_fields["model"], "model": agent_fields["model"],
"description": purpose or f"Custom subagent '{role_id}'", "description": purpose or f"Custom subagent '{role_id}'",
"prompt": prompt, "prompt": prompt,
# Dem Orchestrator sagen, wann er delegieren soll. # Tell the orchestrator when to delegate to this agent.
"orchestratorPrompt": ( "orchestratorPrompt": (
f"@{agent_key}\n- Rolle: {purpose}\n" f"@{agent_key}\n- Role: {purpose}\n"
"- Delegiere Aufgaben dieser Rolle an diesen Agenten." "- Delegate tasks of this role to this agent."
if purpose if purpose
else f"@{agent_key}" else f"@{agent_key}"
), ),
@ -210,15 +209,15 @@ def build_preset(profile: dict, mapping: dict) -> tuple[dict, dict, list[str]]:
if not mcps and not skills: if not mcps and not skills:
warnings.append( warnings.append(
f"Rolle '{role_id}' ({agent_key}): keine MCPs/Skills zugewiesen " f"Role '{role_id}' ({agent_key}): no MCPs/skills assigned "
"(rein koordinierend?)" "(purely coordinating?)"
) )
orchestrator_present = any(k == "orchestrator" for k in preset) orchestrator_present = any(k == "orchestrator" for k in preset)
if not orchestrator_present: if not orchestrator_present:
warnings.append( warnings.append(
"Kein 'orchestrator' im Team. Ohne Orchestrator-Preset-Eintrag bleibt " "No 'orchestrator' in the team. Without an orchestrator preset "
"dessen Modell unverändert." "entry its model stays unchanged."
) )
return preset, custom_agents, warnings return preset, custom_agents, warnings
@ -231,8 +230,8 @@ def merge_into_config(config: dict, preset_name: str, preset: dict,
existing = presets.get(preset_name) existing = presets.get(preset_name)
if existing: if existing:
print( print(
f"Hinweis: Preset '{preset_name}' existierte bereits und wird ersetzt " f"Note: preset '{preset_name}' already existed and will be replaced "
"(andere Presets bleiben unberührt)." "(other presets remain untouched)."
) )
presets[preset_name] = preset presets[preset_name] = preset
@ -242,7 +241,7 @@ def merge_into_config(config: dict, preset_name: str, preset: dict,
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# MCP-Verfügbarkeit prüfen (best effort) # MCP availability check (best effort)
def strip_jsonc(text: str) -> str: def strip_jsonc(text: str) -> str:
@ -251,12 +250,14 @@ def strip_jsonc(text: str) -> str:
return text return text
OPENCODE_CONFIGS = [
Path.home() / ".config" / "opencode" / "opencode.jsonc",
Path.home() / ".config" / "opencode" / "opencode.json",
]
def configured_mcps() -> set[str]: def configured_mcps() -> set[str]:
candidates = [ for path in OPENCODE_CONFIGS:
Path.home() / ".config" / "opencode" / "opencode.jsonc",
Path.home() / ".config" / "opencode" / "opencode.json",
]
for path in candidates:
if path.exists(): if path.exists():
try: try:
data = json.loads(strip_jsonc(path.read_text(encoding="utf-8"))) data = json.loads(strip_jsonc(path.read_text(encoding="utf-8")))
@ -267,13 +268,9 @@ def configured_mcps() -> set[str]:
def available_models() -> list[str]: def available_models() -> list[str]:
"""Listet alle provider/model-Kombinationen aus der opencode.json(c).""" """List all provider/model combinations from opencode.json(c)."""
candidates = [
Path.home() / ".config" / "opencode" / "opencode.jsonc",
Path.home() / ".config" / "opencode" / "opencode.json",
]
models: list[str] = [] models: list[str] = []
for path in candidates: for path in OPENCODE_CONFIGS:
if not path.exists(): if not path.exists():
continue continue
try: try:
@ -288,12 +285,12 @@ def available_models() -> list[str]:
INIT_TEMPLATE = """\ INIT_TEMPLATE = """\
# Lokale Mapping-Tabelle des NUTZERS. # Local mapping table of the USER.
# Ordnet abstrakte Modellklassen konkrete Modell-IDs zu. # Maps abstract model classes to concrete model IDs.
# Formate: # Formats:
# klasse: provider/modell # class: provider/model
# klasse: # class:
# model: provider/modell # model: provider/model
# variant: thinking # optional # variant: thinking # optional
# temperature: 0.7 # optional # temperature: 0.7 # optional
@ -310,7 +307,7 @@ def check_mcps(preset: dict, warnings: list[str]) -> None:
available = configured_mcps() available = configured_mcps()
if not available: if not available:
warnings.append( warnings.append(
"Konnte opencode.json(c) nicht lesen MCP-Prüfung übersprungen." "Could not read opencode.json(c) - MCP check skipped."
) )
return return
needed: set[str] = set() needed: set[str] = set()
@ -319,13 +316,13 @@ def check_mcps(preset: dict, warnings: list[str]) -> None:
missing = sorted(m for m in needed if m not in available) missing = sorted(m for m in needed if m not in available)
if missing: if missing:
warnings.append( warnings.append(
"MCPs im Team-Profil, aber nicht in opencode.json konfiguriert: " "MCPs referenced in the team profile but not configured in "
+ ", ".join(missing) "opencode.json: " + ", ".join(missing)
) )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Kommandos # Commands
def package_output_path(package_dir: Path) -> Path: def package_output_path(package_dir: Path) -> Path:
@ -344,7 +341,7 @@ def cmd_render(args: argparse.Namespace) -> int:
try: try:
config = json.loads(output.read_text(encoding="utf-8")) config = json.loads(output.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc: except json.JSONDecodeError as exc:
raise OmosError(f"{output} ist kein gültiges JSON: {exc}") raise CpmError(f"{output} is not valid JSON: {exc}")
else: else:
config = {} config = {}
@ -363,7 +360,7 @@ def cmd_render(args: argparse.Namespace) -> int:
output.parent.mkdir(parents=True, exist_ok=True) output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding="utf-8") output.write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"Geschrieben: {output}") print(f"Written: {output}")
provenance = { provenance = {
"profile": profile["id"], "profile": profile["id"],
@ -386,83 +383,83 @@ def cmd_render(args: argparse.Namespace) -> int:
provenance_path.write_text( provenance_path.write_text(
json.dumps(provenance, indent=2, ensure_ascii=False), encoding="utf-8" json.dumps(provenance, indent=2, ensure_ascii=False), encoding="utf-8"
) )
print(f"Provenance: {provenance_path}") print(f"Provenance: {provenance_path}")
_report(warnings) _report(warnings)
print( print(
"\nNächste Schritte:\n" "\nNext steps:\n"
" 1. opencode starten (Projekt-Config wird geladen)\n" " 1. start opencode (project config gets loaded)\n"
f" 2. /preset {sanitize_preset_name(str(profile['id']))}\n" f" 2. /preset {sanitize_preset_name(str(profile['id']))}\n"
" 3. OpenCode neu laden → Team aktiv" " 3. reload OpenCode -> team is active"
) )
return 0 return 0
def cmd_check(args: argparse.Namespace) -> int: def cmd_check(args: argparse.Namespace) -> int:
args.dry_run = True args.dry_run = True
# Dry-Run: vorhandene Projekt-Config einbeziehen, aber nie schreiben. # Dry run: include an existing project config, but never write.
existing = package_output_path(Path(args.package).resolve()) existing = package_output_path(Path(args.package).resolve())
args.output = str(existing) if existing.exists() else None args.output = str(existing) if existing.exists() else None
try: try:
cmd_render(args) cmd_render(args)
except OmosError as exc: except CpmError as exc:
print(f"Fehler: {exc}", file=sys.stderr) print(f"Error: {exc}", file=sys.stderr)
return 1 return 1
return 0 return 0
def _report(warnings: list[str], dry_run: bool = False) -> None: def _report(warnings: list[str], dry_run: bool = False) -> None:
if dry_run: if dry_run:
print("\n--- Dry-Run: Es wurde nichts geschrieben ---") print("\n--- Dry run: nothing was written ---")
if warnings: if warnings:
print("\nWarnungen:") print("\nWarnings:")
for warning in warnings: for warning in warnings:
print(f"{warning}") print(f"{warning}")
else: else:
print("\nKeine Warnungen.") print("\nNo warnings.")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Interaktive Assistenten # Interactive assistants
def _choose_model(model_class: str, models: list[str]) -> str: def _choose_model(model_class: str, models: list[str]) -> str:
"""Interaktive Auswahl eines Modells für eine Modellklasse.""" """Interactively pick a model for a model class."""
print(f"\nr '{model_class}' ist kein Modell zugeordnet.") print(f"\nNo model assigned for '{model_class}'.")
if models: if models:
print("Verfügbare Modelle (aus opencode.json(c)):") print("Available models (from opencode.json(c)):")
for i, model in enumerate(models, 1): for i, model in enumerate(models, 1):
print(f" {i}. {model}") print(f" {i}. {model}")
print(" 0. Modell-ID manuell eingeben") print(" 0. Enter model ID manually")
while True: while True:
choice = input("Auswahl: ").strip() choice = input("Selection: ").strip()
if choice == "0": if choice == "0":
return input("Modell-ID (provider/model): ").strip() return input("Model ID (provider/model): ").strip()
if choice.isdigit() and 1 <= int(choice) <= len(models): if choice.isdigit() and 1 <= int(choice) <= len(models):
return models[int(choice) - 1] return models[int(choice) - 1]
print("Ungültige Auswahl.") print("Invalid selection.")
else: else:
print("Keine Modelle in opencode.json(c) gefunden.") print("No models found in opencode.json(c).")
return input("Modell-ID (provider/model): ").strip() return input("Model ID (provider/model): ").strip()
def cmd_init(args: argparse.Namespace) -> int: def cmd_init(args: argparse.Namespace) -> int:
mapping_path = Path(args.mapping) if args.mapping else DEFAULT_MAPPING_PATH mapping_path = Path(args.mapping) if args.mapping else DEFAULT_MAPPING_PATH
if mapping_path.exists(): if mapping_path.exists():
print(f"Mapping existiert bereits: {mapping_path}") print(f"Mapping already exists: {mapping_path}")
else: else:
mapping_path.parent.mkdir(parents=True, exist_ok=True) mapping_path.parent.mkdir(parents=True, exist_ok=True)
mapping_path.write_text(INIT_TEMPLATE, encoding="utf-8") mapping_path.write_text(INIT_TEMPLATE, encoding="utf-8")
print(f"Angelegt: {mapping_path}") print(f"Created: {mapping_path}")
if args.fill: if args.fill:
missing = [k for k, v in load_mapping(mapping_path).items() missing = [k for k, v in load_mapping(mapping_path).items()
if v == "PLACEHOLDER"] if v == "PLACEHOLDER"]
models = available_models() models = available_models()
if missing and not models: if missing and not models:
raise OmosError( raise CpmError(
"Keine Modelle in ~/.config/opencode/opencode.json(c) gefunden. " "No models found in ~/.config/opencode/opencode.json(c). "
"Trage die Modell-IDs manuell ein oder richte opencode ein." "Enter the model IDs manually or set up opencode first."
) )
data = load_yaml(mapping_path) data = load_yaml(mapping_path)
for model_class in missing: for model_class in missing:
@ -474,33 +471,33 @@ def cmd_init(args: argparse.Namespace) -> int:
remaining = sum(1 for v in data["model_classes"].values() remaining = sum(1 for v in data["model_classes"].values()
if v == "PLACEHOLDER") if v == "PLACEHOLDER")
if remaining: if remaining:
print(f"\n{remaining} Eintrag/Einträge bleiben PLACEHOLDER " print(f"\n{remaining} entr(y/ies) left as PLACEHOLDER - "
"bitte manuell ergänzen.") "please fill in manually.")
print("\nNächster Schritt:") print("\nNext step:")
print(f" cpm.py check --package <package-dir> --mapping {mapping_path}") print(f" cpm.py check --package <package-dir> --mapping {mapping_path}")
return 0 return 0
def cmd_setup(args: argparse.Namespace) -> int: def cmd_setup(args: argparse.Namespace) -> int:
"""Geführter Ablauf: Profil lesen → Mapping auffüllen → rendern.""" """Guided flow: read profile -> fill mapping -> render."""
package_dir = Path(args.package).resolve() package_dir = Path(args.package).resolve()
profile, profile_path = load_team_profile(package_dir, args.team_file) profile, _profile_path = load_team_profile(package_dir, args.team_file)
mapping_path = Path(args.mapping) if args.mapping else DEFAULT_MAPPING_PATH mapping_path = Path(args.mapping) if args.mapping else DEFAULT_MAPPING_PATH
print(f"Team-Profil: {profile['id']}") print(f"Team profile: {profile['id']}")
if profile.get("description"): if profile.get("description"):
print(f" {str(profile['description']).strip()}") print(f" {str(profile['description']).strip()}")
print("Rollen:") print("Roles:")
for role in profile["roles"]: for role in profile["roles"]:
mcps = (role.get("capabilities", {}) or {}).get("mcps", []) mcps = (role.get("capabilities", {}) or {}).get("mcps", [])
skills = (role.get("capabilities", {}) or {}).get("skills", []) skills = (role.get("capabilities", {}) or {}).get("skills", [])
print(f" - {role['id']}{role.get('model_class')} " print(f" - {role['id']}{role.get('model_class')} "
f"(MCPs: {', '.join(mcps) or 'keine'}; " f"(MCPs: {', '.join(mcps) or 'none'}; "
f"Skills: {', '.join(skills) or 'keine'})") f"Skills: {', '.join(skills) or 'none'})")
if not mapping_path.exists(): if not mapping_path.exists():
print(f"\nMapping fehlt lege an: {mapping_path}") print(f"\nMapping missing - creating: {mapping_path}")
args_init = argparse.Namespace(mapping=str(mapping_path), fill=True) args_init = argparse.Namespace(mapping=str(mapping_path), fill=True)
cmd_init(args_init) cmd_init(args_init)
@ -518,9 +515,9 @@ def cmd_setup(args: argparse.Namespace) -> int:
yaml.safe_dump(classes, sort_keys=False, allow_unicode=True), yaml.safe_dump(classes, sort_keys=False, allow_unicode=True),
encoding="utf-8", encoding="utf-8",
) )
print(f"\nMapping aktualisiert: {mapping_path}") print(f"\nMapping updated: {mapping_path}")
print("\nRendern …") print("\nRendering ...")
args_render = argparse.Namespace( args_render = argparse.Namespace(
package=str(package_dir), package=str(package_dir),
team_file=args.team_file, team_file=args.team_file,
@ -535,9 +532,9 @@ def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="cpm", prog="cpm",
description=( description=(
"Adapter zwischen APM-Team-Profilen und oh-my-opencode-slim. " "Corentic Package Manager. Translates CPM packages (APM manifest + "
"Übersetzt deterministisch: Team-Profil + lokales Modell-Mapping " "team-profile.yaml) deterministically into harness-native presets: "
"-> Harness-Preset." "team profile + local model mapping -> harness preset."
), ),
) )
sub = parser.add_subparsers(dest="command", required=True) sub = parser.add_subparsers(dest="command", required=True)
@ -546,70 +543,70 @@ def main(argv: list[str] | None = None) -> int:
p.add_argument( p.add_argument(
"--package", "--package",
default=".", default=".",
help="Pfad zum APM-Package (mit team-profile.yaml)", help="Path to the CPM package directory (containing team-profile.yaml)",
) )
p.add_argument( p.add_argument(
"--team-file", "--team-file",
help="Alternativer Dateiname des Team-Profils (Default: team-profile.yaml)", help="Alternative filename of the team profile (default: team-profile.yaml)",
) )
p.add_argument( p.add_argument(
"--mapping", "--mapping",
help=f"Pfad zur Modell-Mapping-Datei (Default: {DEFAULT_MAPPING_PATH})", help=f"Path to the model mapping file (default: {DEFAULT_MAPPING_PATH})",
) )
p_render = sub.add_parser("render", help="Preset generieren und schreiben") p_render = sub.add_parser("render", help="Generate and write the preset")
common(p_render) common(p_render)
p_render.add_argument( p_render.add_argument(
"--output", "--output",
help=f"Zieldatei (Default: {DEFAULT_OUTPUT} relativ zum Package)", help=f"Target file (default: {DEFAULT_OUTPUT} relative to the package)",
) )
p_render.add_argument( p_render.add_argument(
"--dry-run", "--dry-run",
action="store_true", action="store_true",
help="Nur anzeigen, nichts schreiben", help="Print only, write nothing",
) )
p_render.set_defaults(func=cmd_render) p_render.set_defaults(func=cmd_render)
p_check = sub.add_parser("check", help="Validieren ohne zu schreiben") p_check = sub.add_parser("check", help="Validate without writing")
common(p_check) common(p_check)
p_check.set_defaults(func=cmd_check) p_check.set_defaults(func=cmd_check)
p_init = sub.add_parser( p_init = sub.add_parser(
"init", "init",
help="Modell-Mapping-Datei anlegen und optional Platzhalter füllen", help="Create the model mapping file, optionally filling placeholders",
) )
p_init.add_argument( p_init.add_argument(
"--mapping", "--mapping",
help=f"Zielpfad (Default: {DEFAULT_MAPPING_PATH})", help=f"Target path (default: {DEFAULT_MAPPING_PATH})",
) )
p_init.add_argument( p_init.add_argument(
"--fill", "--fill",
action="store_true", action="store_true",
help="Fehlende Modellklassen interaktiv aus opencode.json(c)-Modellen wählen", help="Interactively choose missing model classes from opencode.json(c) models",
) )
p_init.set_defaults(func=cmd_init) p_init.set_defaults(func=cmd_init)
p_setup = sub.add_parser( p_setup = sub.add_parser(
"setup", "setup",
help="Geführt: Profil lesen, Mapping auffüllen, rendern", help="Guided: read profile, fill mapping, render",
) )
common(p_setup) common(p_setup)
p_setup.add_argument( p_setup.add_argument(
"--output", "--output",
help=f"Zieldatei (Default: {DEFAULT_OUTPUT} relativ zum Package)", help=f"Target file (default: {DEFAULT_OUTPUT} relative to the package)",
) )
p_setup.add_argument( p_setup.add_argument(
"--dry-run", "--dry-run",
action="store_true", action="store_true",
help="Nur anzeigen, nichts schreiben", help="Print only, write nothing",
) )
p_setup.set_defaults(func=cmd_setup) p_setup.set_defaults(func=cmd_setup)
args = parser.parse_args(argv) args = parser.parse_args(argv)
try: try:
return args.func(args) return args.func(args)
except OmosError as exc: except CpmError as exc:
print(f"Fehler: {exc}", file=sys.stderr) print(f"Error: {exc}", file=sys.stderr)
return 1 return 1

View file

@ -1,15 +1,15 @@
# CPM-Wrapper um das offizielle Microsoft Sample-Package. # Thin CPM wrapper around the official Microsoft sample package.
# Installiert dessen Primitives (design-standards, design-review, # Installs its primitives (design-standards, design-review,
# accessibility-audit, style-checker, design-reviewer) und liefert # accessibility-audit, style-checker, design-reviewer) and adds the
# die Team-Empfehlung dafür. # team recommendation.
# #
# Original: https://github.com/microsoft/apm-sample-package # Original: https://github.com/microsoft/apm-sample-package
name: acme/design-review-cpm name: acme/design-review-cpm
version: 0.1.0 version: 0.1.0
description: > description: >
Design-Review-Team auf Basis des offiziellen microsoft/apm-sample-package: Design review team based on the official microsoft/apm-sample-package:
Reviewer, Style-Checker und Accessibility-Auditor unter einem Orchestrator. reviewer, style checker and accessibility auditor under one orchestrator.
author: Tobias Endres author: Tobias Endres
license: MIT license: MIT
type: hybrid type: hybrid

View file

@ -1,19 +1,19 @@
# team-profile.yaml Design-Review-Team # team-profile.yaml Design Review Team
# Rollen basierend auf den Primitives aus microsoft/apm-sample-package. # Roles based on the primitives from microsoft/apm-sample-package.
schema: corentic.team-profile/v1 schema: corentic.team-profile/v1
id: acme.design-review id: acme.design-review
description: > description: >
Team für strukturierte Design-Reviews: Style-Guideline-Prüfung, Team for structured design reviews: style-guideline checks,
Design-Review nach Vorlage und Accessibility-Audit unter einem design review per template and an accessibility audit under a
koordinierenden Reviewer. coordinating reviewer.
roles: roles:
- id: orchestrator - id: orchestrator
purpose: >- purpose: >-
Nimmt Review-Anfragen an, verteilt Style-Check, Design-Review Takes review requests, distributes style check, design review and
und Accessibility-Audit und konsolidiert die Findings zu einem Report. accessibility audit and consolidates the findings into one report.
model_class: strong-generalist model_class: strong-generalist
capabilities: capabilities:
mcps: [] mcps: []
@ -21,10 +21,11 @@ roles:
- id: reviewer - id: reviewer
purpose: >- purpose: >-
Führt das Design-Review gemäß der design-review.prompt.md-Vorlage durch. Performs the design review following the design-review.prompt.md
Bewertet UI-Konsistenz gegen die design-standards.instructions.md. template. Evaluates UI consistency against the
Meldet Findings mit Schweregrad, keine eigenmächtigen Codeänderungen. design-standards.instructions.md. Reports findings with severity,
runtime_agent: oracle # tiefes Prüfen = Oracle-Profil no unauthorized code changes.
runtime_agent: oracle # deep review = oracle profile
model_class: high-reasoning model_class: high-reasoning
capabilities: capabilities:
mcps: [] mcps: []
@ -32,9 +33,10 @@ roles:
- id: style-checker - id: style-checker
purpose: >- purpose: >-
Prüft Code mechanisch gegen die Style-Guidelines aus dem Mechanically checks code against the style guidelines from the
style-checker-Skill. Liefert Verstoßliste mit Datei/Zeile. Keine Fixes. style-checker skill. Returns a violation list with file/line.
runtime_agent: fixer # schnelle, gezielte Prüfungen No fixes.
runtime_agent: fixer # fast, targeted checks
model_class: fast-research model_class: fast-research
capabilities: capabilities:
mcps: [] mcps: []
@ -42,10 +44,10 @@ roles:
- id: accessibility-auditor - id: accessibility-auditor
purpose: >- purpose: >-
Auditet die Oberfläche gemäß accessibility-audit.prompt.md. Audits the surface according to accessibility-audit.prompt.md.
Kategorisiert Verstöße (kritisch/warnung/hinweis) und zitiert die Categorizes violations (critical/warning/note) and quotes the
betroffenen Stellen. affected locations.
runtime_agent: librarian # Recherche-/Prüfprofil runtime_agent: librarian # research/review profile
model_class: fast-research model_class: fast-research
capabilities: capabilities:
mcps: [] mcps: []

View file

@ -1,21 +1,20 @@
# CPM-Wrapper um apm-issue-autopilot aus dem offiziellen microsoft/apm-Repo. # Thin CPM wrapper around apm-issue-autopilot from the official microsoft/apm repo.
# #
# Das Original-Paket orchestriert bereits Skills (apm-triage-panel, # The upstream package already orchestrates skills (apm-triage-panel,
# shepherd-driver, pr-description-skill) und treibt Issues bis zum # shepherd-driver, pr-description-skill) and drives issues to a mergeable PR.
# mergebaren PR. Das Team-Profil hier bildet genau diese Phasen auf # The team profile below maps exactly those phases onto agent roles.
# Agentenrollen ab.
# #
# Original: https://github.com/microsoft/apm/tree/main/packages/apm-issue-autopilot # Upstream: https://github.com/microsoft/apm/tree/main/packages/apm-issue-autopilot
# APM unterstützt Monorepo-Subpaths, daher ist die Dependency direkt # APM supports monorepo subpaths, so the dependency points directly at the
# auf das Unterverzeichnis gerichtet. # subdirectory.
name: acme/issue-autopilot-cpm name: acme/issue-autopilot-cpm
version: 0.1.0 version: 0.1.0
description: > description: >
Issue-Triage-Team auf Basis von microsoft/apm packages/apm-issue-autopilot: Issue triage team based on microsoft/apm packages/apm-issue-autopilot:
Triage als Gate, Shepherd für den Drive-to-Merge, PR-Beschreibungen und triage as gate, shepherd for drive-to-merge, PR descriptions and a
eine prüfende Gegenstimme. Human-in-the-loop: Escalation an den Maintainer reviewing counter-check. Human-in-the-loop: escalate to the maintainer
bei Zweifel. on doubt.
author: Tobias Endres author: Tobias Endres
license: MIT license: MIT
type: hybrid type: hybrid

View file

@ -1,22 +1,21 @@
# team-profile.yaml Issue-Autopilot-Team # team-profile.yaml Issue Autopilot Team
# Rollen basierend auf den Phasen des offiziellen apm-issue-autopilot-Pakets: # Roles based on the phases of the official apm-issue-autopilot package:
# Triage → Entscheidung → Drive-to-Merge → PR-Beschreibung. # Triage -> Decision -> Drive-to-Merge -> PR description.
schema: corentic.team-profile/v1 schema: corentic.team-profile/v1
id: acme.issue-autopilot id: acme.issue-autopilot
description: > description: >
Treibt GitHub-Issues vom Intake bis zum mergebaren PR: Triage-Panel Drives GitHub issues from intake to mergeable PR: triage panel as the
als zentrales Gate, Shepherd für jeden akzeptierten Issue, central gate, a shepherd per accepted issue, a PR author with anchored
PR-Autor mit verankerter Beschreibung. Escaliert standardmäßig beim descriptions. Escalates to the maintainer by default on doubt.
Maintainer im Zweifelsfall.
roles: roles:
- id: orchestrator - id: orchestrator
purpose: >- purpose: >-
Nimmt die Issue-Liste entgegen, startet pro Issue die Triage und fasst Takes the issue list, starts triage per issue and consolidates into ONE
zu EIN konsolidiertem Review zusammen. Eskaliert Zweifelfälle explizit combined review. Escalates doubtful cases explicitly to the maintainer
an den Maintainer statt selbst zu entscheiden. instead of deciding itself.
model_class: strong-generalist model_class: strong-generalist
capabilities: capabilities:
mcps: [github] mcps: [github]
@ -24,10 +23,10 @@ roles:
- id: triager - id: triager
purpose: >- purpose: >-
Führt die Triage gemäß apm-triage-panel-Rubrik durch. Bewertet Performs triage according to the apm-triage-panel rubric. Evaluates
Reproduzierbarkeit, Scope, Priorität und Duplikate. Empfehlung mit reproducibility, scope, priority and duplicates. Recommendation with
Begründung, keine direkten Fixes. rationale, no direct fixes.
runtime_agent: oracle # Bewertungs-Gate = Reasoning-Profil runtime_agent: oracle # decision gate = reasoning profile
model_class: high-reasoning model_class: high-reasoning
capabilities: capabilities:
mcps: [github] mcps: [github]
@ -35,10 +34,10 @@ roles:
- id: shepherd - id: shepherd
purpose: >- purpose: >-
Treibt akzeptierte Issues über shepherd-driver bis zum mergebaren PR: Drives accepted issues via shepherd-driver to a mergeable PR:
Branch, Fix, Mutation-Break- und Lint-Gates, CI-Watch, Konfliktauflösung. branch, fix, mutation-break and lint gates, CI watch, conflict
Hält bei rotem CI inne. resolution. Halts on red CI.
runtime_agent: fixer # Umsetzung + Korrekturen runtime_agent: fixer # implementation + corrections
model_class: strong-writing model_class: strong-writing
capabilities: capabilities:
mcps: [github] mcps: [github]
@ -46,9 +45,9 @@ roles:
- id: pr-writer - id: pr-writer
purpose: >- purpose: >-
Verfasst die PR-Beschreibung mit pr-description-skill. Verankert sie Authors the PR description using pr-description-skill. Anchors it to
am Issue mit Acceptance-Kriterien und Testnachweis. Merged nie selbst. the issue with acceptance criteria and test evidence. Never merges.
runtime_agent: custom # eigener Custom Agent 'pr-writer' runtime_agent: custom # dedicated custom agent 'pr-writer'
model_class: strong-writing model_class: strong-writing
capabilities: capabilities:
mcps: [github] mcps: [github]
@ -56,8 +55,9 @@ roles:
- id: reviewer - id: reviewer
purpose: >- purpose: >-
Gegenprobe vor dem Push: prüft Fix gegen Issue-Akzeptanzkriterien Counter-check before push: verifies the fix against issue acceptance
und PRINCIPLES.md-Gate. Findings blockieren den Push, kein Self-Merge. criteria and the PRINCIPLES.md gate. Findings block the push,
no self-merge.
runtime_agent: oracle runtime_agent: oracle
model_class: high-reasoning model_class: high-reasoning
capabilities: capabilities:

View file

@ -1,11 +1,11 @@
# ~/.config/cpm/model-mapping.yaml # ~/.config/cpm/model-mapping.yaml
# #
# Lokale Mapping-Tabelle des NUTZERS. # Local mapping table of the USER.
# Ordnet abstrakte Modellklassen aus CPM-Packages (Team-Profile) konkrete, lokal # Maps abstract model classes from CPM packages (team profiles) to concrete,
# verfügbare Modell-IDs zu. Einmal anlegen, gilt für alle Packages. # locally available model IDs. Create once, applies to all packages.
# #
# Einfache Form: klasse: provider/modell-id # Simple form: class: provider/model-id
# Erweiterte Form: klasse: { model: ..., variant: ..., temperature: ... } # Extended form: class: { model: ..., variant: ..., temperature: ... }
model_classes: model_classes:
strong-generalist: ollama/qwen3.6:35b-a3b-q4_K_M strong-generalist: ollama/qwen3.6:35b-a3b-q4_K_M

View file

@ -1,61 +1,57 @@
# team-profile.yaml # team-profile.yaml - Job Application Team
# Deklaration des empfohlenen Agenten-Teams für das Package job-application. # Roles for the job-application package (OpenViking + ShareLaTeX).
#
# Diese Datei gehört dem Package-AUTOR. Sie beschreibt:
# - welche Rollen für den fachlichen Workflow sinnvoll sind,
# - welche Modellklasse jede Rolle benötigt (abstrakt, keine konkreten IDs),
# - welche Teilmenge der MCPs und Skills jede Rolle verwenden darf.
#
# Der Nutzer löst die Modellklassen lokal auf (siehe model-mapping.yaml).
# Der CPM (Corentic Package Manager) übersetzt dieses Profil in die native
# Konfiguration eines Ziel-Harnesses aktuell: oh-my-opencode-slim.
schema: corentic.team-profile/v1 schema: corentic.team-profile/v1
id: acme.job-applications id: acme.job-applications
description: > description: >
Human-in-the-loop-Team zur Recherche, Vorbereitung und Prüfung Human-in-the-loop team for researching, preparing and reviewing
individueller Bewerbungsunterlagen. individual job application documents.
roles: roles:
- id: orchestrator - id: orchestrator
purpose: Zerlegt Aufgaben, delegiert an Spezialisten, integriert Ergebnisse purpose: >-
Decomposes tasks, delegates to specialists and integrates results.
model_class: strong-generalist model_class: strong-generalist
capabilities: capabilities:
mcps: [] mcps: []
skills: [job-application] skills: [job-application]
- id: researcher - id: researcher
purpose: Sucht und bewertet Stellenausschreibungen gegen das Kandidatenprofil purpose: >-
runtime_agent: librarian # nutzt oh-my-opencode-slim-Builtin 'librarian' mit Alias Searches and evaluates job openings against the candidate profile.
runtime_agent: librarian # uses the oh-my-opencode-slim builtin with an alias
model_class: fast-research model_class: fast-research
capabilities: capabilities:
mcps: [websearch, webfetch, openviking] mcps: [websearch, webfetch, openviking]
skills: [job-application] skills: [job-application]
- id: writer - id: writer
purpose: Erstellt auf Fakten basierende Anschreiben und Lebensläufe in ShareLaTeX. purpose: >-
Nutze nur freigegebene Profilfakten. Erfinde keine Erfahrung. Creates fact-based cover letters and CVs in ShareLaTeX.
Reiche niemals selbstständig eine Bewerbung ein. Only use approved profile facts. Never invent experience.
runtime_agent: custom # eigener Custom Agent 'writer' Never submit an application autonomously.
runtime_agent: custom # dedicated custom agent 'writer'
model_class: strong-writing model_class: strong-writing
capabilities: capabilities:
mcps: [openviking, sharelatex] mcps: [openviking, sharelatex]
skills: [] skills: []
- id: checker - id: checker
purpose: Prüft Fakten, Ton und Vollständigkeit gegen das Kandidatenprofil. purpose: >-
Jede Behauptung braucht eine belegbare Quelle. Reviews facts, tone and completeness against the candidate profile.
runtime_agent: oracle # nutzt oh-my-opencode-slim-Builtin 'oracle' mit Alias Every claim requires a verifiable source.
runtime_agent: oracle # uses the oh-my-opencode-slim builtin with an alias
model_class: high-reasoning model_class: high-reasoning
capabilities: capabilities:
mcps: [openviking] mcps: [openviking]
skills: [] skills: []
- id: notifier - id: notifier
purpose: Informiert den Nutzer über fertige Entwürfe. purpose: >-
Versende niemals etwas ohne explizite Nutzerfreigabe. Informs the user about finished drafts.
runtime_agent: custom # eigener Custom Agent 'notifier' Never send anything without explicit user approval.
runtime_agent: custom # dedicated custom agent 'notifier'
model_class: cheap-reliable model_class: cheap-reliable
capabilities: capabilities:
mcps: [] mcps: []

View file

@ -1,6 +1,6 @@
--- ---
name: cpm-setup name: cpm-setup
description: Activate when the user wants to set up, render, or troubleshoot a CPM package (APM package + team-profile). Use for cpm init, cpm setup, cpm render, team-profile, model-mapping, oh-my-opencode-slim preset, fehlende Modellklassen. description: Activate when the user wants to set up, render, or troubleshoot a CPM package (APM package + team-profile). Use for cpm init, cpm setup, cpm render, team-profile, model-mapping, oh-my-opencode-slim preset, missing model classes.
allowed-tools: allowed-tools:
- "Bash(python3 corentic-package-manager/cpm.py *)" - "Bash(python3 corentic-package-manager/cpm.py *)"
- "Read" - "Read"
@ -11,65 +11,65 @@ allowed-tools:
# CPM Setup Skill # CPM Setup Skill
Führt den Nutzer durch: CPM-Package (APM-Manifest + Team-Profil) → lokales Modell-Mapping → gerendertes Harness-Preset (aktuell: oh-my-opencode-slim). Guides the user through: CPM package (APM manifest + team profile) → local model mapping → rendered harness preset (currently: oh-my-opencode-slim).
## Der Ablauf (ResolveValidateRender) ## The flow (ResolveValidateRender)
### 1. Team-Profil finden ### 1. Find the team profile
Prüfe, ob eine `team-profile.yaml` existiert (Package-Root oder `corentic-package-manager/examples/package/team-profile.yaml`). Lies sie und fasse dem Nutzer zusammen: Check whether a `team-profile.yaml` exists (package root or `corentic-package-manager/examples/package/team-profile.yaml`). Read it and summarize for the user:
``` ```
Profil: acme.job-applications Profile: acme.job-applications
Rollen: orchestrator → strong-generalist, researcher → fast-research, Roles: orchestrator → strong-generalist, researcher → fast-research,
writer → strong-writing, checker → high-reasoning, notifier → cheap-reliable writer → strong-writing, checker → high-reasoning, notifier → cheap-reliable
MCPs benötigt: openviking, sharelatex (+ websearch/webfetch als OpenCode-Native) MCPs required: openviking, sharelatex (+ websearch/webfetch as OpenCode-native)
``` ```
### 2. Mapping prüfen ### 2. Check the mapping
Prüfe `~/.config/cpm/model-mapping.yaml`. Für **jede** im Profil verwendete Modellklasse muss ein Eintrag existieren. Check `~/.config/cpm/model-mapping.yaml`. For **every** model class used in the profile an entry must exist.
Wenn eine Modellklasse fehlt, biete dem Nutzer konkrete Optionen an (verfügbare Modelle aus `~/.config/opencode/opencode.jsonc` auflisten): If a model class is missing, offer the user concrete options (list available models from `~/.config/opencode/opencode.jsonc`):
``` ```
Für 'high-reasoning' ist kein Modell zugeordnet. No model assigned for 'high-reasoning'.
1. Vorhandenes Modell wählen (z. B. ollama/qwen3.6:35b-a3b-q4_K_M) 1. Pick an existing model (e.g. ollama/qwen3.6:35b-a3b-q4_K_M)
2. Neues Provider/Modell in opencode.jsonc konfigurieren 2. Configure a new provider/model in opencode.jsonc
3. Abbrechen 3. Abort
``` ```
Nach Auswahl schreibe das Mapping und markiere optional `# selected_by: user`. After selection write the mapping and optionally mark `# selected_by: user`.
Alternativ die interaktiven Kommandos nutzen: `cpm init --fill` und `cpm setup`. Alternatively use the interactive commands: `cpm init --fill` and `cpm setup`.
### 3. MCPs prüfen ### 3. Check MCPs
Vergleiche die im Profil genannten MCPs mit der Sektion `mcp` in `~/.config/opencode/opencode.jsonc`. Fehlende dem Nutzer melden nicht stillschweigend ignorieren. Native OpenCode-Tools (`websearch`, `webfetch`) sind keine MCPs und brauchen keinen Eintrag. Compare the MCPs named in the profile against the `mcp` section in `~/.config/opencode/opencode.jsonc`. Report missing ones to the user never silently ignore. OpenCode-native tools (`websearch`, `webfetch`) are not MCPs and need no entry.
### 4. Rendern ### 4. Render
```bash ```bash
python3 corentic-package-manager/cpm.py render --package <package-dir> python3 corentic-package-manager/cpm.py render --package <package-dir>
``` ```
Zeige die Ausgabe inklusive Warnungen. Die Datei `.opencode/oh-my-opencode-slim.json` ist generiert nicht manuell editieren. Show the output including warnings. The file `.opencode/oh-my-opencode-slim.json` is generated never edit it manually.
### 5. Aktivieren ### 5. Activate
Nutzer anleiten: `opencode` starten → `/preset <team-id>` → Reload. Preset-Wechsel greifen erst nach einem Reload (by Design). Guide the user: start `opencode``/preset <team-id>` → reload. Preset switches only take effect after a reload (by design).
## Fehlerbehandlung ## Error handling
| Fehler | Lösung | | Error | Fix |
|---|---| |---|---|
| „Modellklasse X ist nicht gemappt" | Schritt 2: Mapping ergänzen | | "Model class X is not mapped" | Step 2: add to mapping |
| „MCP ... nicht in opencode.json konfiguriert" | Schritt 3: MCP einrichten oder Rolle anpassen | | "MCP ... not configured in opencode.json" | Step 3: set up the MCP or adjust the role |
| „Kein Team-Profil gefunden" | Pfad prüfen, `--team-file` verwenden | | "No team profile found" | Check path, use `--team-file` |
| Ungültiges YAML | Zeile aus Fehlermeldung zeigen | | Invalid YAML | Follow the line number in the error message |
## Regeln ## Rules
- Niemals selbst Modelle erraten oder zuordnen immer den Nutzer wählen lassen. - Never guess or assign models yourself always let the user choose.
- Niemals die generierte `.opencode/oh-my-opencode-slim.json` direkt editieren; Quelle sind Team-Profil + Mapping. - Never edit the generated `.opencode/oh-my-opencode-slim.json` directly; sources are the team profile + mapping.
- Sicherheitsgrenzen des Packages (MCP/Skill-Allowlists) nur verschärfen, nie aufweichen. - Only ever tighten package security boundaries (MCP/skill allowlists), never widen them.