Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- HYBRID packages (apm.yml + SKILL.md without `.apm/`) now install as skill bundles per agentskills.io semantics; previously `apm install` rejected this layout in the validator and emitted no inline error, producing a perceived hang. (#946)
- CLAUDE_SKILL packages (SKILL.md only) that ship co-located resource directories (`agents/`, `assets/`, `scripts/`) now route through the skill-bundle install path. (#946)
- Direct-dependency integration failures now print a `[x]` line immediately and exit 1 instead of failing silently. (#946)
- HYBRID metadata model: `apm.yml` description and `SKILL.md` description are independent fields with different consumers and are no longer merged; `apm pack` warns when `apm.yml` is missing `description` on a HYBRID package. (#946)
- Symlink hardening extended to all three `skill_integrator` copytree paths. (#946)
- `apm install` (user scope): `init_link_resolver` now scopes `discover_primitives` to `~/.apm/` instead of `~/`, preventing recursive-glob across the entire home directory. Fixes #830 (#850)
- Audit blindness for local `.apm/` content -- `apm audit --ci` now detects drift, missing files, and content tampering on locally-authored files (not just installed packages). (#887)
- Packer leak risk: local-content fields (`local_deployed_files`, `local_deployed_file_hashes`) are now stripped from bundled lockfiles, preventing phantom self-entries on unpack. (#887)
Expand Down
19 changes: 19 additions & 0 deletions docs/src/content/docs/getting-started/first-package.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,25 @@ audit while you author; pack produces the plugin bundle when you ship.
For the full reference, see the [Pack & Distribute guide](/apm/guides/pack-distribute/)
and the [Plugin authoring guide](/apm/guides/plugins/).

## Choosing a package layout

APM recognizes three layouts. Pick the one that matches what you are shipping:

- **One skill** -- put `SKILL.md` at the repo root, with optional
`agents/`, `assets/`, or `scripts/` directories alongside it. Add
`apm.yml` if you need dependency management (this is a HYBRID package).
APM installs the whole directory as a single skill bundle.

- **Multiple primitives** -- use the `.apm/` directory with `skills/`,
`agents/`, `instructions/` subdirectories (the layout used in this guide).
APM hoists each primitive into the consumer's runtime dirs individually.

- **Claude plugin** -- if you already have a `plugin.json`, APM can consume
it directly without restructuring.

For the full comparison and metadata precedence rules, see
[Package Types](../../reference/package-types/).

## Next steps

- [Anatomy of an APM Package](/apm/introduction/anatomy-of-an-apm-package/)
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/examples.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Examples"
sidebar:
order: 4
order: 5
---

This guide showcases real-world APM workflows, from simple automation to enterprise-scale AI development patterns. Learn through practical examples that demonstrate the power of structured AI workflows.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "apm experimental"
description: "Manage opt-in experimental feature flags. Evaluate new or changing behaviour without affecting APM defaults."
sidebar:
order: 5
order: 6
label: "Experimental Flags"
---

Expand Down
125 changes: 125 additions & 0 deletions docs/src/content/docs/reference/package-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
title: "Package Types"
sidebar:
order: 4
---

APM supports three package layouts, each with distinct install semantics.
Pick the layout that matches the author's intent -- APM preserves it.

## Layout summary

| Root signal | Author intent | Install semantic |
|---|---|---|
| `.apm/` (with or without apm.yml) | "I have N independent primitives" | Hoist each primitive into the target's runtime dirs |
| `SKILL.md` (alone or with apm.yml -- HYBRID) | "I am one skill bundle" | Copy the whole bundle to `<target>/skills/<name>/` |
| `plugin.json` / `.claude-plugin/` | Claude plugin collection | Dissect via plugin artifact mapping |

## APM package (`.apm/` directory)

The classic APM layout. Primitives live under `.apm/` in typed subdirectories.
`apm install` hoists each primitive into the consumer's runtime directories
individually.

```
my-package/
+-- apm.yml
+-- .apm/
+-- skills/
| +-- pr-description/SKILL.md
+-- agents/
| +-- reviewer.agent.md
+-- instructions/
+-- team-standards.instructions.md
```

**What gets installed:** each skill, agent, and instruction is copied to its
corresponding runtime directory (e.g. `.github/skills/`, `.github/agents/`).

**When to choose:** you are shipping multiple independent primitives that
consumers may override or extend individually.

## Skill bundle (`SKILL.md` at root)

A single skill with co-located resources. The presence of `SKILL.md` at the
package root tells APM: "this entire directory is one skill -- install it as
a unit."

An optional `apm.yml` alongside `SKILL.md` makes this a **HYBRID** package.
APM still installs it as a skill bundle, but gains dependency resolution,
version metadata, and script support from the manifest.

```
code-review-skill/
+-- SKILL.md
+-- agents/
| +-- reviewer.agent.md
+-- assets/
| +-- checklist.md
+-- scripts/
| +-- lint-check.sh
+-- apm.yml # optional -- enables dependencies and scripts
```

**What gets installed:** the entire directory tree is copied to
`<target>/skills/<name>/`, preserving internal structure.

**When to choose:** you are shipping one cohesive skill that bundles its own
agents, assets, or scripts. The skill's internal layout is part of its
contract -- APM will not rearrange it.

### Metadata model (HYBRID packages)

`apm.yml` and `SKILL.md` each own their `description` field
**independently** -- APM never merges or backfills one from the other.
The two strings serve different consumers:

- `apm.yml.description` is a short human-facing tagline rendered by
`apm view`, `apm search`, `apm deps list`, and registry/marketplace
listings.
- `SKILL.md` `description` (frontmatter) is the agent-runtime
invocation matcher consumed by Claude, Copilot, and other runtimes
per the agentskills.io spec. APM copies `SKILL.md` byte-for-byte
into `<target>/skills/<name>/` and never reads or mutates this
field.

Other apm.yml fields (`name`, `version`, `license`, `dependencies`,
`scripts`) are owned exclusively by `apm.yml` -- there is no
SKILL.md-side equivalent and nothing to merge. `allowed-tools` lives
exclusively in `SKILL.md` frontmatter and is consumed by the agent
runtime.

When you ship a HYBRID package, populate both descriptions
independently: keep `apm.yml.description` to a short tagline (under
~80 characters) and write `SKILL.md` in whatever length and tone the
agent runtime expects. `apm pack` warns when `apm.yml.description` is
missing so the human-facing surfaces do not degrade silently while
the agent runtime keeps working.

## Plugin collection (`plugin.json`)

A Claude-native plugin layout. APM dissects the plugin artifacts and maps
them into runtime directories.

```
my-plugin/
+-- plugin.json
+-- agents/
| +-- helper.agent.md
+-- skills/
+-- search/SKILL.md
```

**What gets installed:** each artifact listed in `plugin.json` is mapped to
the appropriate runtime directory via `_map_plugin_artifacts`.

**When to choose:** you already have a Claude plugin and want APM to
consume it without restructuring.

## See also

- [Your First Package](../../getting-started/first-package/) -- hands-on
walkthrough for scaffolding and publishing.
- [CLI Commands](../cli-commands/) -- `apm install`, `apm pack`, and all
options.
- [Manifest Schema](../manifest-schema/) -- full `apm.yml` field reference.
2 changes: 1 addition & 1 deletion packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

| Command | Purpose | Key flags |
|---------|---------|-----------|
| `apm install [PKGS...]` | Install packages | `--update` refresh refs, `--force` overwrite, `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated), `--dev`, `-g` global, `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--mcp NAME` add MCP entry, `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry |
| `apm install [PKGS...]` | Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json)) | `--update` refresh refs, `--force` overwrite, `--dry-run`, `--verbose`, `--only [apm\|mcp]`, `--target` (comma-separated), `--dev`, `-g` global, `--trust-transitive-mcp`, `--parallel-downloads N`, `--allow-insecure`, `--allow-insecure-host HOSTNAME`, `--mcp NAME` add MCP entry, `--transport`, `--url`, `--env KEY=VAL`, `--header KEY=VAL`, `--mcp-version`, `--registry URL` custom MCP registry |
| `apm uninstall PKGS...` | Remove packages | `--dry-run`, `-g` global |
| `apm prune` | Remove orphaned packages | `--dry-run` |
| `apm deps list` | List installed packages | `-g` global, `--all` both scopes, `--insecure` |
Expand Down
36 changes: 35 additions & 1 deletion packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,40 @@
# Package Authoring

## Package directory structure
## Supported package layouts

APM recognizes three layouts. The shape of the package root tells APM
how to install it:

| Root signal | Author intent | Install semantic |
|---|---|---|
| `.apm/` (with or without apm.yml) | Multiple independent primitives | Hoist each primitive into the consumer runtime dirs |
| `SKILL.md` (alone, or with apm.yml = HYBRID) | One skill bundle | Copy whole tree to `<target>/skills/<name>/` |
| `plugin.json` / `.claude-plugin/` | Claude plugin collection | Dissect via plugin artifact mapping |

The HYBRID layout (apm.yml + SKILL.md) is a single skill bundle that
also uses APM dependency resolution. APM installs it as a skill -- it
does NOT dissect the bundle into top-level primitives. Co-located
subdirectories like `agents/`, `assets/`, `scripts/` are bundle
resources, not standalone primitives.

In a HYBRID package, `apm.yml` and `SKILL.md` each own their
`description` field **independently** -- APM never merges or
backfills one from the other:
- `apm.yml.description` is a short human-facing tagline rendered by
`apm view`, `apm search`, `apm deps list`, and registry listings.
- `SKILL.md` `description` (frontmatter) is the agent-runtime
invocation matcher (per agentskills.io). APM copies `SKILL.md`
byte-for-byte and never reads or mutates this field.
- `allowed-tools` lives exclusively in `SKILL.md` frontmatter; there
is no apm.yml-side equivalent.
- `name`, `version`, `license`, `dependencies`, `scripts` live
exclusively in `apm.yml`.

Populate both descriptions when you ship a HYBRID package. `apm pack`
warns when `apm.yml.description` is missing so listings do not
degrade silently while the agent runtime keeps working.

## Package directory structure (APM layout)

```
my-package/
Expand Down
26 changes: 26 additions & 0 deletions src/apm_cli/bundle/packer.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,38 @@ def pack_bundle(

# 2. Read apm.yml for name / version / config target
apm_yml_path = project_root / "apm.yml"
skill_md_path = project_root / "SKILL.md"
is_hybrid_root = apm_yml_path.exists() and skill_md_path.exists()
try:
package = APMPackage.from_apm_yml(apm_yml_path)
pkg_name = package.name
pkg_version = package.version or "0.0.0"
config_target = package.target

# HYBRID author guard: apm.yml.description and SKILL.md
# description serve different consumers (human-facing CLI/search
# vs. agent-runtime invocation matcher) and are NOT merged. If
# the author shipped a SKILL.md description but left
# apm.yml.description blank, the human-facing surfaces (apm view,
# apm search, marketplace listings) will degrade silently while
# Claude/Copilot still invoke the skill correctly. Warn loudly
# at pack time -- this is the publish gate for the AUTHOR.
if is_hybrid_root and not package.description and logger:
try:
import frontmatter as _frontmatter
with open(skill_md_path, "r", encoding="utf-8") as _f:
_skill_post = _frontmatter.load(_f)
_skill_desc = _skill_post.metadata.get("description")
except Exception:
_skill_desc = None
if _skill_desc:
logger.warning(
"apm.yml is missing 'description'. SKILL.md has its own "
"description, but that is for agent invocation -- not "
"for 'apm view' or search. Add a short tagline to "
"apm.yml: description: \"One-line human summary\""
)

# Guard: reject local-path dependencies (non-portable)
for dep_ref in package.get_apm_dependencies():
if dep_ref.is_local:
Expand Down
17 changes: 16 additions & 1 deletion src/apm_cli/commands/deps/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,25 @@ def _get_detailed_package_info(package_path: Path) -> Dict[str, Any]:
package = APMPackage.from_apm_yml(apm_yml_path)
context_count, workflow_count = _count_package_files(package_path)
primitives = _count_primitives(package_path)
# HYBRID-aware description rendering: when apm.yml omits its
# tagline but a SKILL.md sits alongside, surface the empty
# apm.yml.description as `--` plus an inline annotation. The
# SKILL.md description is intentionally NOT borrowed -- it is
# an agent invocation matcher, not a human tagline.
is_hybrid = (package_path / "SKILL.md").exists()
if package.description:
desc = package.description
elif is_hybrid:
desc = (
"-- (set 'description' in apm.yml; SKILL.md "
"description is for agent runtime)"
)
else:
desc = 'No description'
return {
'name': package.name,
'version': package.version or 'unknown',
'description': package.description or 'No description',
'description': desc,
'author': package.author or 'Unknown',
'source': package.source or 'local',
'install_path': str(package_path.resolve()),
Expand Down
8 changes: 6 additions & 2 deletions src/apm_cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
_has_local_apm_content,
_project_has_root_primitives,
)
from apm_cli.install.errors import PolicyViolationError
from apm_cli.install.errors import DirectDependencyError, PolicyViolationError
from apm_cli.install.insecure_policy import (
_InsecureDependencyInfo,
_allow_insecure_host_callback,
Expand Down Expand Up @@ -912,7 +912,7 @@ def _run_mcp_install(


@click.command(
help="Install APM and MCP dependencies (auto-creates apm.yml; use --allow-insecure for http:// packages)"
help="Install APM and MCP dependencies (supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json); auto-creates apm.yml; use --allow-insecure for http:// packages)"
)
@click.argument("packages", nargs=-1)
@click.option("--runtime", help="Target specific runtime only (copilot, codex, vscode)")
Expand Down Expand Up @@ -1597,6 +1597,10 @@ def install(ctx, packages, runtime, exclude, only, update, dry_run, force, verbo
except InsecureDependencyPolicyError:
_maybe_rollback_manifest(_snapshot_manifest_path, _manifest_snapshot, logger)
sys.exit(1)
except DirectDependencyError as e:
_maybe_rollback_manifest(_snapshot_manifest_path, _manifest_snapshot, logger)
logger.error(str(e))
sys.exit(1)
except click.UsageError:
# Conflict matrix / argv parser raises UsageError -- let Click
# render with exit code 2 and the standard "Usage: ..." prefix.
Expand Down
Loading
Loading