feat: add package variables (${var:...}) for deployed primitives#736
feat: add package variables (${var:...}) for deployed primitives#736nathantoews wants to merge 1 commit intomicrosoft:mainfrom
Conversation
Add install-time variable substitution for deployed primitive files.
Package authors declare variables in apm.yml with defaults, and
consumers override them per-package. Placeholders (${var:name}) in
.agent.md, .instructions.md, SKILL.md, and other text files are
resolved during apm install.
- Add variables field to APMPackage and apm.yml parsing
- Add variable substitution engine (src/apm_cli/utils/variables.py)
- Integrate substitution into all integrators (agent, instruction,
skill, prompt, command) via BaseIntegrator
- Record resolved variables in apm.lock.yaml for reproducibility
- Fail with clear error when required variables have no resolution
- 66 new tests covering parsing, resolution, substitution, and
lockfile round-trip
- Update manifest-schema, dependencies guide, and package-authoring
docs
There was a problem hiding this comment.
Pull request overview
Adds install-time package variable support (${var:...}) so APM packages can ship parameterized primitives and have consumers override values via apm.yml, with resolved values recorded in the lockfile for repeatable installs.
Changes:
- Introduces a variable parsing/resolution + substitution utility and threads resolved variables through the install/integration pipeline.
- Updates integrators (agents/instructions/prompts/commands/skills) to apply substitution during deploy, and extends the lockfile model to persist resolved values.
- Adds a dedicated unit test suite plus documentation/schema + changelog updates describing the new variable feature.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_package_variables.py | New unit tests covering parsing, resolution, substitution, lockfile round-trip, and BaseIntegrator hooks. |
| src/apm_cli/utils/variables.py | New substitution engine: regex matching, parsing package declarations + consumer overrides, and directory-wide substitution for copied skills. |
| src/apm_cli/models/apm_package.py | Adds variables field to APMPackage and loads it from apm.yml. |
| src/apm_cli/integration/base_integrator.py | Adds set_variables() and apply_variable_substitution() shared by integrators. |
| src/apm_cli/integration/agent_integrator.py | Applies variable substitution during agent copy and Codex MD->TOML transformation. |
| src/apm_cli/integration/instruction_integrator.py | Applies variable substitution during instruction deployment (including Cursor/Claude conversions). |
| src/apm_cli/integration/prompt_integrator.py | Applies variable substitution during prompt deployment. |
| src/apm_cli/integration/command_integrator.py | Applies variable substitution to command content before writing targets. |
| src/apm_cli/integration/skill_integrator.py | Applies variable substitution across copied skill directories (including promoted sub-skills) and ensures BaseIntegrator init runs. |
| src/apm_cli/deps/lockfile.py | Adds resolved_variables to LockedDependency serialization/deserialization. |
| src/apm_cli/commands/install.py | Resolves per-package variables during install, sets variables on integrators, and writes resolved values into the lockfile. |
| packages/apm-guide/.apm/skills/apm-usage/package-authoring.md | Adds authoring guidance for declaring/overriding variables. |
| docs/src/content/docs/reference/manifest-schema.md | Extends manifest schema reference with the new variables section and lockfile field. |
| docs/src/content/docs/guides/dependencies.md | Documents variable usage for dependencies/consumers and clarifies schema references. |
| CHANGELOG.md | Adds an Unreleased entry for package variables. |
| else: | ||
| warnings.append( | ||
| f"Variable '{var_name}' for package '{package_name}' is unresolved " | ||
| f"-- ${{{var_name}}} placeholders will be left as-is" |
There was a problem hiding this comment.
The unresolved-variable warning message renders the placeholder as ${<name>} (e.g. ${stack}) rather than the actual ${var:<name>} syntax that users need to fix. Update the message so it references ${var:<name>} to avoid confusing consumers reading diagnostics.
| f"-- ${{{var_name}}} placeholders will be left as-is" | |
| f"-- ${{var:{var_name}}} placeholders will be left as-is" |
| logger.error(err_msg) | ||
|
|
||
| if errors: | ||
| return {} |
There was a problem hiding this comment.
Missing required variables currently only add diagnostics errors and then return {}; the install continues and deploys files with unresolved ${var:...} placeholders. This contradicts the spec/docs that say required variables must fail apm install. Consider hard-failing when errors is non-empty (e.g., raise an exception / return a sentinel that causes the caller to abort and exit non-zero), or ensure the main install flow exits when diagnostics.error_count > 0 for this case.
| return {} | |
| raise click.ClickException( | |
| f"Failed to resolve required variables for package '{pkg_name}'." | |
| ) |
| if not package_vars and not (consumer_apm_package.variables or {}): | ||
| return {} | ||
|
|
||
| consumer_overrides = parse_consumer_overrides(consumer_apm_package.variables) | ||
| pkg_name = package_info.package.name |
There was a problem hiding this comment.
PR description mentions consumer overrides under dependencies[].variables, but the implementation reads overrides from the top-level apm.yml variables mapping keyed by package name. Please align the PR description (and any docs, if needed) with the actual schema, or adjust the implementation to support the documented dependencies[].variables form.
| ### Added | ||
|
|
||
| - Package variables (`${var:...}`) for deployed primitives -- package authors declare variables in `apm.yml`, consumers override values, and placeholders are substituted at install time in `.agent.md`, `.instructions.md`, `SKILL.md`, and other text files | ||
| - `apm install` now automatically discovers and deploys local `.apm/` primitives (skills, instructions, agents, prompts, hooks, commands) to target directories, with local content taking priority over dependencies on collision (#626, #644) |
There was a problem hiding this comment.
The new Unreleased changelog entry does not follow the repo format: entries should be a single concise line ending with the PR number in parentheses (e.g. ... (#123)). Please update this line to match the established Keep a Changelog/SemVer conventions used throughout the file.
| - `apm install` now automatically discovers and deploys local `.apm/` primitives (skills, instructions, agents, prompts, hooks, commands) to target directories, with local content taking priority over dependencies on collision (#626, #644) | |
| - `apm install` now discovers and deploys local `.apm/` primitives, with local content taking priority over dependencies on collision (#626, #644) |
|
There is some support for input parameters in agent workflows. Adding variables is akin to creating a language and compiler/interpreter. That's something that I recognize as valuable but it's too early and we need discussion on design before. The place for variables should be the markdowns themselves and not the manifest I believe. Let's move this to a discussion |
Hello! APM team. This a feature I think would be super helpful to make apm a little more modular for abstractions of agents for installing. Please let me know what you think would be happy to make some changes.
Summary
Adds support for declaring and resolving variables in APM packages. Package authors can define
${var:name}placeholders in their primitive files, declare variables with defaults inapm.yml, and consumers can override values per-dependency. Variables are resolved at install time and substituted into all deployed text-based primitives.Motivation
Packages often need to be customizable without requiring consumers to fork or manually edit files after install. Variables provide a first-class mechanism for parameterization -- things like model names, API endpoints, project-specific paths, or behavioral toggles can be exposed as named variables with sensible defaults.
How it works
variablessection toapm.ymlwith names, defaults, and optional descriptions.${var:name}placeholders anywhere in their primitive text files (instructions, prompts, agents, skills, commands).apm.ymlunderdependencies[].variables.apm-lock.ymlfor reproducibility.Changes
src/apm_cli/utils/variables.py-- substitution engine (pattern matching, resolution, file walking)models/apm_package.py--variablesfield onAPMPackagedeps/lockfile.py--resolved_variablesonLockedDependencyintegration/base_integrator.py--set_variables()/apply_variable_substitution()on base classcommands/install.py-- resolve variables per-package and thread through integrationtests/unit/test_package_variables.py