From ae6817d86f420f0a5e7e5a22c3cc19da49a32a92 Mon Sep 17 00:00:00 2001 From: "Tobias J. Endres" Date: Sun, 23 Aug 2026 18:24:25 +0200 Subject: [PATCH] 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 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 --- corentic-package-manager/README.md | 31 ++++- corentic-package-manager/cpm.py | 123 +++++++++++++++--- .../microsoft-design-review/.gitignore | 2 + .../microsoft-issue-autopilot/.gitignore | 2 + .../skills/cpm-setup/SKILL.md | 19 ++- 5 files changed, 153 insertions(+), 24 deletions(-) create mode 100644 corentic-package-manager/examples/microsoft-design-review/.gitignore create mode 100644 corentic-package-manager/examples/microsoft-issue-autopilot/.gitignore diff --git a/corentic-package-manager/README.md b/corentic-package-manager/README.md index b562e02..9dd4729 100644 --- a/corentic-package-manager/README.md +++ b/corentic-package-manager/README.md @@ -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 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 python3 /path/to/cpm.py setup --package /path/to/package # 3. Activate -opencode # project config gets loaded +cd /path/to/package # the config lives in /.opencode/ +opencode /preset acme-job-applications # 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) python3 cpm.py check --package examples/package -# 3. Render -cd -python3 /path/to/cpm.py render --package /path/to/package +# 3. Render + install package primitives +cd /path/to/package +python3 /path/to/cpm.py render --package . +apm install # deploys skills/instructions from APM dependencies ``` ## The three files @@ -83,7 +85,7 @@ Creates `~/.config/cpm/model-mapping.yaml` (if not present). With `--fill`, all ### `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` @@ -169,13 +171,28 @@ cd package && opencode /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 | Problem | Cause/Fix | |---|---| | `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 | -| `/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 | | YAML errors | Follow the line number in the error message | diff --git a/corentic-package-manager/cpm.py b/corentic-package-manager/cpm.py index 11f2e40..a8e6238 100644 --- a/corentic-package-manager/cpm.py +++ b/corentic-package-manager/cpm.py @@ -28,6 +28,8 @@ import argparse import copy import json import re +import shutil +import subprocess import sys from pathlib import Path @@ -329,6 +331,63 @@ def package_output_path(package_dir: Path) -> Path: 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: package_dir = Path(args.package).resolve() 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}") _report(warnings) - print( - "\nNext steps:\n" - " 1. start opencode (project config gets loaded)\n" - f" 2. /preset {sanitize_preset_name(str(profile['id']))}\n" - " 3. reload OpenCode -> team is active" - ) + steps = [ + "\nNext steps:", + f" 1. start opencode from {output.parent.parent.resolve()} " + "(the config lives in its .opencode/ directory)", + ] + 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 @@ -423,21 +496,27 @@ def _report(warnings: list[str], dry_run: bool = False) -> None: # Interactive assistants +_MODELS_LISTED = False + + def _choose_model(model_class: str, models: list[str]) -> str: """Interactively pick a model for a model class.""" + global _MODELS_LISTED print(f"\nNo model assigned for '{model_class}'.") if models: - print("Available models (from opencode.json(c)):") - for i, model in enumerate(models, 1): - print(f" {i}. {model}") - print(" 0. Enter model ID manually") + if not _MODELS_LISTED: + print("Available models (from opencode.json(c)):") + for i, model in enumerate(models, 1): + print(f" {i}. {model}") + print(" 0. Enter model ID manually") + _MODELS_LISTED = True while True: - choice = input("Selection: ").strip() + choice = input(f"Selection for '{model_class}': ").strip() if choice == "0": return input("Model ID (provider/model): ").strip() if choice.isdigit() and 1 <= int(choice) <= len(models): return models[int(choice) - 1] - print("Invalid selection.") + print(f"Invalid selection. Enter 1-{len(models)} or 0.") else: print("No models found in opencode.json(c).") 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 - " "please fill in manually.") - print("\nNext step:") - print(f" cpm.py check --package --mapping {mapping_path}") + print("\nNext steps:") + 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 --mapping {mapping_path}") return 0 @@ -525,7 +609,16 @@ def cmd_setup(args: argparse.Namespace) -> int: output=args.output, 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: diff --git a/corentic-package-manager/examples/microsoft-design-review/.gitignore b/corentic-package-manager/examples/microsoft-design-review/.gitignore new file mode 100644 index 0000000..bf3dd49 --- /dev/null +++ b/corentic-package-manager/examples/microsoft-design-review/.gitignore @@ -0,0 +1,2 @@ +# Generated artifacts +.opencode/ diff --git a/corentic-package-manager/examples/microsoft-issue-autopilot/.gitignore b/corentic-package-manager/examples/microsoft-issue-autopilot/.gitignore new file mode 100644 index 0000000..bf3dd49 --- /dev/null +++ b/corentic-package-manager/examples/microsoft-issue-autopilot/.gitignore @@ -0,0 +1,2 @@ +# Generated artifacts +.opencode/ diff --git a/corentic-package-manager/skills/cpm-setup/SKILL.md b/corentic-package-manager/skills/cpm-setup/SKILL.md index 79ad7da..6a6e81d 100644 --- a/corentic-package-manager/skills/cpm-setup/SKILL.md +++ b/corentic-package-manager/skills/cpm-setup/SKILL.md @@ -55,9 +55,24 @@ python3 corentic-package-manager/cpm.py render --package 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 ` → 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 +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 ` (the generated config lives in its `.opencode/` directory!) → start `opencode` → `/preset ` → 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