feat(cpm): integrate 'apm install' into the guided flow

The rendered preset only configures agents; the skills/instructions
that role allowlists reference are deployed by APM. cpm setup now
detects apm.yml dependencies and offers to run 'apm install' after
rendering (manual command printed when the CLI is missing or stdin
is not a TTY). render/check print it as an explicit next step.

Also:
- next steps now name the absolute directory to start opencode from
  (fixes presets appearing 'missing' when opencode runs in the wrong
  cwd, as hit during first real-world test)
- init no longer prints literal <package-dir> placeholders
- model list is shown once instead of repeating per class
- README: new 'Using the team' section (primitives -> reload ->
  sanity check -> work via orchestrator), activation writes to the
  global user config, troubleshooting entry for missing skills
This commit is contained in:
Tobias J. Endres 2026-08-23 18:24:25 +02:00
parent 0df9cec833
commit ae6817d86f
5 changed files with 153 additions and 24 deletions

View file

@ -32,12 +32,13 @@ team-profile.yaml (model class → oh-my-opencode-slim.json
# 1. Create the model mapping interactively, choosing from your opencode models # 1. Create the model mapping interactively, choosing from your opencode models
python3 cpm.py init --fill python3 cpm.py init --fill
# 2. Guided: read team profile, check/fill mapping, render # 2. Guided: read team profile, check/fill mapping, render, install primitives
cd <project> cd <project>
python3 /path/to/cpm.py setup --package /path/to/package python3 /path/to/cpm.py setup --package /path/to/package
# 3. Activate # 3. Activate
opencode # project config gets loaded cd /path/to/package # the config lives in <package>/.opencode/
opencode
/preset acme-job-applications /preset acme-job-applications
# Reload OpenCode -> team is active (by design: no hot-swap) # Reload OpenCode -> team is active (by design: no hot-swap)
``` ```
@ -53,9 +54,10 @@ $EDITOR ~/.config/cpm/model-mapping.yaml # adapt to your own models
# 2. Validate (dry run) # 2. Validate (dry run)
python3 cpm.py check --package examples/package python3 cpm.py check --package examples/package
# 3. Render # 3. Render + install package primitives
cd <project> cd /path/to/package
python3 /path/to/cpm.py render --package /path/to/package python3 /path/to/cpm.py render --package .
apm install # deploys skills/instructions from APM dependencies
``` ```
## The three files ## The three files
@ -83,7 +85,7 @@ Creates `~/.config/cpm/model-mapping.yaml` (if not present). With `--fill`, all
### `setup` guided flow ### `setup` guided flow
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. 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 then handles package primitives: if the package declares APM dependencies, `setup` offers to run `apm install` for you (or prints the manual command if the `apm` CLI is not installed).
### `render` / `check` ### `render` / `check`
@ -169,13 +171,28 @@ cd package && opencode
/preset acme-job-applications /preset acme-job-applications
``` ```
## Using the team
Rendering + activation only configures the agents. To actually work with the team:
1. **Install primitives.** If the package declares APM dependencies, run `apm install` in the package directory (`cpm setup` offers this automatically). This deploys the skills/instructions that role allowlists reference into `.opencode/skills/` etc.
2. **Reload OpenCode.** Agents (preset) and skills are loaded at startup.
3. **Sanity check.** In OpenCode: `/agents` lists the team members; "ping all agents" verifies they respond.
4. **Work via the orchestrator.** Just state your task in normal language e.g. *"Review the components in src/ against our design standards"*. The orchestrator decomposes it and delegates according to the generated routing prompts: style violations → `@style-checker`, design judgment → `@reviewer`, audits → the auditor agent. It then consolidates the findings.
Two things to know:
- The preset switch (`/preset`) and skill loading both require an OpenCode restart/reload by design.
- Activating a preset writes its name to your **global** user config (`~/.config/opencode/oh-my-opencode-slim.json`), so it persists across sessions and projects until you switch again.
## Troubleshooting ## Troubleshooting
| Problem | Cause/Fix | | Problem | Cause/Fix |
|---|---| |---|---|
| `Model class X is not mapped` | Add to the mapping, re-render | | `Model class X is not mapped` | Add to the mapping, re-render |
| `Warning: MCP ... not configured` | Set up the MCP in `opencode.jsonc` or remove it from the profile | | `Warning: MCP ... not configured` | Set up the MCP in `opencode.jsonc` or remove it from the profile |
| `/preset` does not show anything new | Reload OpenCode after rendering; presets do not apply mid-session | | `/preset` does not show anything new | Start opencode in the directory containing `.opencode/` and reload; presets do not apply mid-session |
| Agent references a skill that does not exist | Run `apm install` in the package directory the preset only configures agents, primitives come from APM |
| Preset lost after manual editing | The file is generated changes belong in the mapping or team profile | | Preset lost after manual editing | The file is generated changes belong in the mapping or team profile |
| YAML errors | Follow the line number in the error message | | YAML errors | Follow the line number in the error message |

View file

@ -28,6 +28,8 @@ import argparse
import copy import copy
import json import json
import re import re
import shutil
import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@ -329,6 +331,63 @@ def package_output_path(package_dir: Path) -> Path:
return package_dir / DEFAULT_OUTPUT return package_dir / DEFAULT_OUTPUT
def has_apm_dependencies(package_dir: Path) -> bool:
"""True if the package declares an apm.yml with non-empty APM dependencies."""
manifest = package_dir / "apm.yml"
if not manifest.exists():
return False
try:
data = load_yaml(manifest)
except CpmError:
return False
deps = (data.get("dependencies") or {}).get("apm") or []
return bool(deps)
def apm_cli_available() -> bool:
return shutil.which("apm") is not None
def ensure_apm_install(package_dir: Path, dry_run: bool, interactive: bool) -> None:
"""Make sure the user knows about (and ideally ran) 'apm install'.
The rendered preset only configures agents; the skills/MCP primitives
referenced by the team profile are deployed by APM.
"""
if not has_apm_dependencies(package_dir):
return
print(
"\nAPM dependencies detected: this package references skills/instructions "
"from other packages that must be installed before the team can use them."
)
if dry_run:
print(f" Dry run: run 'apm install' in {package_dir} yourself.")
return
if apm_cli_available() and interactive and sys.stdin.isatty():
answer = input("Run 'apm install' now? [y/N] ").strip().lower()
if answer in ("y", "yes"):
print()
result = subprocess.run(["apm", "install"], cwd=package_dir)
if result.returncode == 0:
print("\napm install finished successfully.")
else:
print(
f"\n'apm install' exited with code {result.returncode}. "
f"Fix the issue above and re-run it in {package_dir}.",
file=sys.stderr,
)
return
else:
if not apm_cli_available():
print(
" The 'apm' CLI was not found on PATH.\n"
" Install it via: pip install apm-cli (or: brew install microsoft/apm/apm)"
)
print(f" Run manually:\n cd {package_dir}\n apm install")
def cmd_render(args: argparse.Namespace) -> int: def cmd_render(args: argparse.Namespace) -> int:
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)
@ -386,12 +445,26 @@ def cmd_render(args: argparse.Namespace) -> int:
print(f"Provenance: {provenance_path}") print(f"Provenance: {provenance_path}")
_report(warnings) _report(warnings)
print( steps = [
"\nNext steps:\n" "\nNext steps:",
" 1. start opencode (project config gets loaded)\n" f" 1. start opencode from {output.parent.parent.resolve()} "
f" 2. /preset {sanitize_preset_name(str(profile['id']))}\n" "(the config lives in its .opencode/ directory)",
" 3. reload OpenCode -> team is active" ]
) if has_apm_dependencies(package_dir):
steps.append(
" 2. run 'apm install' there first - the team references skills "
"from APM dependencies that are not deployed yet"
)
steps.append(
f" 3. /preset {sanitize_preset_name(str(profile['id']))}"
)
steps.append(" 4. reload OpenCode -> team is active")
else:
steps.append(
f" 2. /preset {sanitize_preset_name(str(profile['id']))}"
)
steps.append(" 3. reload OpenCode -> team is active")
print("\n".join(steps))
return 0 return 0
@ -423,21 +496,27 @@ def _report(warnings: list[str], dry_run: bool = False) -> None:
# Interactive assistants # Interactive assistants
_MODELS_LISTED = False
def _choose_model(model_class: str, models: list[str]) -> str: def _choose_model(model_class: str, models: list[str]) -> str:
"""Interactively pick a model for a model class.""" """Interactively pick a model for a model class."""
global _MODELS_LISTED
print(f"\nNo model assigned for '{model_class}'.") print(f"\nNo model assigned for '{model_class}'.")
if models: if models:
print("Available models (from opencode.json(c)):") if not _MODELS_LISTED:
for i, model in enumerate(models, 1): print("Available models (from opencode.json(c)):")
print(f" {i}. {model}") for i, model in enumerate(models, 1):
print(" 0. Enter model ID manually") print(f" {i}. {model}")
print(" 0. Enter model ID manually")
_MODELS_LISTED = True
while True: while True:
choice = input("Selection: ").strip() choice = input(f"Selection for '{model_class}': ").strip()
if choice == "0": if choice == "0":
return input("Model 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("Invalid selection.") print(f"Invalid selection. Enter 1-{len(models)} or 0.")
else: else:
print("No models found in opencode.json(c).") print("No models found in opencode.json(c).")
return input("Model ID (provider/model): ").strip() return input("Model ID (provider/model): ").strip()
@ -474,8 +553,13 @@ def cmd_init(args: argparse.Namespace) -> int:
print(f"\n{remaining} entr(y/ies) left as PLACEHOLDER - " print(f"\n{remaining} entr(y/ies) left as PLACEHOLDER - "
"please fill in manually.") "please fill in manually.")
print("\nNext step:") print("\nNext steps:")
print(f" cpm.py check --package <package-dir> --mapping {mapping_path}") if mapping_path == DEFAULT_MAPPING_PATH:
print(" Mapping is in place. Validate or render a package, e.g.:")
print(" cpm.py check --package ./my-cpm-package")
print(" cpm.py setup --package ./my-cpm-package")
else:
print(f" cpm.py check --package <your-package-dir> --mapping {mapping_path}")
return 0 return 0
@ -525,7 +609,16 @@ def cmd_setup(args: argparse.Namespace) -> int:
output=args.output, output=args.output,
dry_run=args.dry_run, dry_run=args.dry_run,
) )
return cmd_render(args_render) result = cmd_render(args_render)
# Deploy the package primitives (skills/instructions/MCP configs) that
# the team references. The preset alone only configures the agents.
ensure_apm_install(
package_dir,
dry_run=args.dry_run,
interactive=not args.dry_run,
)
return result
def main(argv: list[str] | None = None) -> int: def main(argv: list[str] | None = None) -> int:

View file

@ -0,0 +1,2 @@
# Generated artifacts
.opencode/

View file

@ -0,0 +1,2 @@
# Generated artifacts
.opencode/

View file

@ -55,9 +55,24 @@ python3 corentic-package-manager/cpm.py render --package <package-dir>
Show the output including warnings. The file `.opencode/oh-my-opencode-slim.json` is generated never edit it manually. Show the output including warnings. The file `.opencode/oh-my-opencode-slim.json` is generated never edit it manually.
### 5. Activate ### 5. Install package primitives
Guide the user: start `opencode``/preset <team-id>` → reload. Preset switches only take effect after a reload (by design). If the package declares an `apm.yml` with APM dependencies, the skills/instructions the roles reference are NOT deployed by cpm APM does that:
```bash
cd <package-dir>
apm install # requires the apm CLI (pip install apm-cli)
```
`cpm setup` offers this step automatically; when guiding manually, always run it before activation if dependencies exist.
### 6. Activate
Guide the user: `cd <package-dir>` (the generated config lives in its `.opencode/` directory!) → start `opencode``/preset <team-id>` → reload. Preset switches and skill loading both take effect only after a reload (by design). Activating writes the preset name to the global user config, so it persists across projects until switched again.
## Using the team
After activation the user works via the orchestrator in normal language ("Review X against our standards"). The orchestrator delegates per the generated routing prompts and consolidates results. Verify first with `/agents` and "ping all agents".
## Error handling ## Error handling