Skip to content
Open
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Package namespaces.** Declare `namespace: acme` in `apm.yml` to install skills under direct runtime-visible directories like `skills/acme-<name>/` -- ship multiple packages from the same org without name collisions while staying compatible with harnesses that expect direct child skill folders. The namespace flows from manifest into `apm.lock.yaml`, the install tree, and per-skill confirmation messages so the routing is visible end-to-end. (#1028)

### Changed

- **BREAKING: `apm compile --target vscode/all` no longer generates `.github/copilot-instructions.md`.** APM now leaves that file alone, matching its "additive, never overwrite user files" contract. If your workflow consumed this generated file, copy your last generated version into the repo and manage it manually, or move the content into a dedicated APM package. (#1028)

```bash
# Before: apm compile --target vscode/all wrote this file for you.
# After: the file is repository-owned. To keep the previous generated copy:
git show HEAD~1:.github/copilot-instructions.md > .github/copilot-instructions.md
```

See `docs/src/content/docs/getting-started/migration.md` for CI guidance.
- **BREAKING: `apm pack` now produces a Claude Code plugin directory by default — zero extra flags, schema-validated `plugin.json`, convention dirs auto-discovered.** The legacy APM bundle layout is preserved under `--format apm`. Migration: CI workflows and scripts that consume the legacy bundle must add `--format apm` (the [`microsoft/apm-action`](https://github.com/microsoft/apm-action) wrapper has been updated accordingly). (#1061)
- **Plugin manifest schema conformance.** The synthesized/written `plugin.json` no longer emits `agents`/`skills`/`commands`/`instructions` keys pointing at the convention directories — these are auto-discovered by Claude Code, and per the [official schema](https://json.schemastore.org/claude-code-plugin.json) those array entries must be `./*.md` paths to *additional* files. The convention dirs themselves are still copied to disk. When stripping such keys from an authored `plugin.json`, `apm pack` now emits a warning so authors can clean up their source. (#1061)

Expand Down
18 changes: 17 additions & 1 deletion docs/src/content/docs/getting-started/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,23 @@ sidebar:
order: 5
---

APM is additive. It never deletes, overwrites, or modifies your existing configuration files. Your current `.github/copilot-instructions.md`, `AGENTS.md`, `.claude/` config, `.cursor-rules` — all stay exactly where they are, untouched.
APM is additive. It never deletes, overwrites, or modifies your existing configuration files. Your current `.github/copilot-instructions.md`, `AGENTS.md`, `.claude/` config, `.cursor-rules` -- all stay exactly where they are, untouched.

:::caution[Unreleased compile change]
`apm compile --target vscode` and `apm compile --target all` no longer write `.github/copilot-instructions.md`. Existing files stay in place, but APM will not regenerate that path.

Before: `apm compile --target vscode` generated `AGENTS.md`, `.github/` primitives, and `.github/copilot-instructions.md`.

After: `apm compile --target vscode` generates `AGENTS.md` and `.github/` primitives only.

To keep the last generated file from the previous commit:

```bash
git show HEAD~1:.github/copilot-instructions.md > .github/copilot-instructions.md
```

For CI, remove assertions that expect APM to regenerate `.github/copilot-instructions.md`, or commit the file and manage it as a normal repository-owned file.
:::

## Add APM in three steps

Expand Down
46 changes: 46 additions & 0 deletions docs/src/content/docs/guides/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,52 @@ name: my-project
target: vscode # or claude, or all
```

## Package Namespaces

If you publish multiple APM packages from the same org, skills from different
packages can otherwise collide in the flat layout. Namespaces isolate them:
`acme-tools` and `acme-infra` can each own a skill directory without overwriting
the other package's files.

Package authors opt in with `namespace` in `apm.yml`:

```yaml
name: acme-tools
version: 1.0.0
namespace: acme
type: skill
```

With this manifest, APM deploys package-owned skills under
`.github/skills/acme-<skill-name>/` (and the equivalent skills directory for
other targets). The skill remains a direct child of the target `skills/`
directory so GitHub Copilot, Claude, Codex, and other harnesses can discover it
without relying on recursive skill lookup. Packages without `namespace` keep
the legacy flat layout.

Before:

```text
.github/
└── skills/
└── brand-guidelines/
```

After:

```text
.github/
└── skills/
└── acme-brand-guidelines/
```

Use a namespace when your org publishes more than one package, when multiple
teams publish skills with common names like `review` or `brand-guidelines`, or
when you want dependency-owned skills to be visually separate from local skills.
Adding or changing a namespace is a consumer-visible path change; run
`apm install` after updating the manifest and remove any old flat-path skill
directories only after confirming they are no longer referenced.

## Best Practices

### 1. Clear Naming
Expand Down
13 changes: 13 additions & 0 deletions docs/src/content/docs/reference/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ apm init [PROJECT_NAME] [OPTIONS]
- `-y, --yes` - Skip interactive prompts and use auto-detected defaults
- `--plugin` - Initialize as a plugin authoring project (creates `plugin.json` + `apm.yml` with `devDependencies`)
- `--marketplace` - Seed `apm.yml` with a `marketplace:` authoring block. See the [Authoring a marketplace guide](../../guides/marketplace-authoring/).
- `--namespace TEXT` - Add an optional package namespace so package-owned skills install under `skills/<namespace>-<skill-name>/`

**Examples:**
```bash
Expand All @@ -57,11 +58,15 @@ apm init my-plugin --plugin

# Initialize a project that also publishes a marketplace
apm init my-marketplace --marketplace

# Initialize with a package namespace for owned skills
apm init my-package --namespace acme
```

**Behavior:**
- **Minimal by default**: Creates only `apm.yml` with auto-detected metadata
- **Interactive mode**: Prompts for project details unless `--yes` specified
- **Namespace** (`--namespace`): Writes `namespace:` to `apm.yml`; interactive mode also prompts for it and allows an empty value
- **Auto-detection**: Automatically detects author from `git config user.name` and description from project context
- **Brownfield friendly**: Works cleanly in existing projects without file pollution
- **Plugin mode** (`--plugin`): Creates both `plugin.json` and `apm.yml` with an empty `devDependencies` section. Plugin names must be kebab-case (`^[a-z][a-z0-9-]{0,63}$`), max 64 characters
Expand Down Expand Up @@ -131,6 +136,8 @@ See [Dependencies: Transport selection](../../guides/dependencies/#transport-sel
- `apm install <package>`: Installs **only** the specified package (adds to `apm.yml` if not present)
- Each `http://` dependency is warned at install time before any fetch begins
- Transitive `http://` dependencies are allowed automatically when they use the same host as a direct insecure dependency you approved with `--allow-insecure`; other transitive hosts require `--allow-insecure-host HOSTNAME`
- Packages that declare `namespace:` in `apm.yml` install package-owned skills under `skills/<namespace>-<skill-name>/` and show the namespace identity in install output, for example `Integrated skill acme/brand-guidelines -> .github/skills/acme-brand-guidelines/`
- `apm install --verbose` keeps the same namespace-aware install tree while adding the usual file-level diagnostic detail; namespace routing is reported from the install summary layer, not as a second per-skill line

**Claude Code: prompt `input:` -> slash command `arguments:`:**

Expand Down Expand Up @@ -326,6 +333,7 @@ Skills are copied directly to target directories:

- **Primary**: `.github/skills/{skill-name}/` — Entire skill folder copied
- **Compatibility**: `.claude/skills/{skill-name}/` — Also copied if `.claude/` folder exists
- **Namespaced packages**: `.github/skills/{namespace}-{skill-name}/` and `.claude/skills/{namespace}-{skill-name}/`

**Example Integration Output**:
```
Expand Down Expand Up @@ -1712,6 +1720,11 @@ target: [claude, copilot] # multiple targets -- only these are compiled/install
| `gemini` | GEMINI.md, .gemini/commands/, .gemini/skills/ | Gemini CLI |
| `all` | All of the above | Universal compatibility |

`apm compile --target vscode` and `apm compile --target all` no longer write
`.github/copilot-instructions.md`. Existing files at that path are left under
your control; keep them manually or move the content into an APM package if your
workflow still depends on that file.

**Examples:**
```bash
# Basic compilation with auto-detected context
Expand Down
1 change: 1 addition & 0 deletions docs/src/content/docs/reference/lockfile-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ fields:
| `depth` | integer | MUST | Dependency depth. `1` = direct dependency, `2`+ = transitive. |
| `resolved_by` | string | MAY | `repo_url` of the parent that introduced this transitive dependency. Present only when `depth >= 2`. |
| `package_type` | string | MUST | Package type: `apm_package`, `plugin`, `virtual`, or other registered types. |
| `namespace` | string | MAY | Manifest-declared namespace applied to package-owned skills. Omitted for legacy flat installs. |
| `content_hash` | string | MAY | SHA-256 hash of the package file tree, in the format `"sha256:<hex>"`. Used to verify cached packages on subsequent installs. Omitted for local path dependencies. See [section 4.4](#44-content-integrity). |
| `is_dev` | boolean | MAY | `true` if the dependency was resolved through [`devDependencies`](../manifest-schema/#5-devdependencies). Omitted when `false`. Dev deps are excluded from `apm pack` plugin output (and from `--format apm` bundles). |
| `deployed_files` | array of strings | MUST | Every file path APM deployed for this dependency, relative to project root. |
Expand Down
36 changes: 33 additions & 3 deletions docs/src/content/docs/reference/manifest-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ author: <string>
license: <string>
target: <enum>
type: <enum>
namespace: <string>
scripts: <map<string, string>>
includes: <enum | list<string>>
dependencies:
Expand Down Expand Up @@ -158,7 +159,36 @@ Declares how the package's content is processed during install and compile. Curr
| `hybrid` | Both AGENTS.md compilation and skill installation. |
| `prompts` | Commands/prompts only. No instructions or skills. |

### 3.8. `scripts`
### 3.8. `namespace`

| | |
|---|---|
| **Type** | `string` |
| **Required** | OPTIONAL |
| **Pattern** | `^[a-z0-9](?:[a-z0-9]|-(?!-))*[a-z0-9]$|^[a-z0-9]$` |
| **Description** | Optional namespace for package-owned skills. Max 64 characters. Consecutive hyphens (`--`) are not allowed. |

When present, installed native skills and promoted `.apm/skills/` entries are
deployed under `skills/<namespace>-<skill-name>/` instead of the legacy flat
`skills/<skill-name>/` layout. The namespace remains a direct child directory
prefix because target harnesses document direct skill directories, not a
portable recursive `skills/**/SKILL.md` contract. Packages without `namespace`
continue to install flat for backward compatibility.

Namespace values MUST be a single safe path segment. Resolvers MUST reject empty
values, traversal (`.` or `..`), path separators, uppercase characters, and
filesystem-unsafe punctuation.

See also: [Package Namespaces](../../guides/skills/#package-namespaces).

```yaml
name: acme-tools
version: 1.0.0
namespace: acme
type: skill
```

### 3.9. `scripts`

| | |
|---|---|
Expand All @@ -168,7 +198,7 @@ Declares how the package's content is processed during install and compile. Curr
| **Value** | Shell command string |
| **Description** | Named commands executed via `apm run <name>`. MUST support `--param key=value` substitution. |

### 3.9. `includes`
### 3.10. `includes`

| | |
|---|---|
Expand Down Expand Up @@ -200,7 +230,7 @@ includes: auto

When `policy.manifest.require_explicit_includes` is `true` (see [Governance guide](../../enterprise/governance-guide/)), only form 3 passes the policy check; `auto` and undeclared are rejected at install/audit time by the `explicit-includes` policy check (not at YAML parse time).

### 3.10. `policy`
### 3.11. `policy`

| | |
|---|---|
Expand Down
16 changes: 16 additions & 0 deletions packages/apm-guide/.apm/skills/apm-usage/package-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ the same way at every entry point. Invalid values fail at parse time with a
message naming the apm.yml path and the offending token -- they do **not**
silently fall through to auto-detect.

## Manifest fields: `namespace:` (optional)

Declare `namespace: <segment>` at the top level of `apm.yml` to install the
package's skills under `skills/<namespace>/<skill-name>/` instead of the legacy
flat `skills/<skill-name>/` layout. This lets one org publish multiple
packages (e.g. `acme-security`, `acme-brand`) without skill-name collisions.

- The segment must be kebab-case: lowercase letters, digits, and hyphens, max
64 characters, no leading/trailing hyphen, no consecutive `--`.
- Omitting `namespace:` keeps the legacy flat layout.
- The namespace flows from `apm.yml` into `apm.lock.yaml` (per-dependency
`namespace:` field) and surfaces in `apm install` tree output as
`skill <namespace>/<name> integrated -> .github/skills/<namespace>/`.
- `--verbose` adds a per-skill line: `Skill deployed under namespace
"<namespace>": skills/<namespace>/<skill-name>/`.

| Form | Behaviour |
|------|-----------|
| `target: copilot` | Single token; allowed values: `vscode`, `agents`, `copilot`, `claude`, `cursor`, `opencode`, `codex`, `all` |
Expand Down
2 changes: 2 additions & 0 deletions src/apm_cli/commands/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,8 @@ def _create_minimal_apm_yml(config, plugin=False, target_path=None):
# gate what gets deployed.
"includes": "auto",
}
if config.get("namespace"):
apm_yml_data["namespace"] = config["namespace"]

if plugin:
apm_yml_data["devDependencies"] = {"apm": []}
Expand Down
38 changes: 34 additions & 4 deletions src/apm_cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@

from ..constants import APM_YML_FILENAME
from ..core.command_logger import CommandLogger
from ..models.apm_package import validate_namespace
from ..utils.console import (
_create_files_table,
_rich_panel,
)
from ._helpers import (
ERROR,
INFO,
RESET,
_create_minimal_apm_yml,
Expand All @@ -39,9 +41,13 @@
is_flag=True,
help="Seed apm.yml with a 'marketplace:' authoring block",
)
@click.option(
"--namespace",
help="Optional namespace to prevent skill name collisions",
)
@click.option("--verbose", "-v", is_flag=True, help="Show detailed output")
@click.pass_context
def init(ctx, project_name, yes, plugin, marketplace_flag, verbose):
def init(ctx, project_name, yes, plugin, marketplace_flag, namespace, verbose):
"""Initialize a new APM project (like npm init).

Creates a minimal apm.yml with auto-detected metadata.
Expand All @@ -50,6 +56,9 @@ def init(ctx, project_name, yes, plugin, marketplace_flag, verbose):
"""
logger = CommandLogger("init", verbose=verbose)
try:
if namespace:
namespace = validate_namespace(namespace)

# Handle explicit current directory
if project_name == ".":
project_name = None
Expand Down Expand Up @@ -100,10 +109,12 @@ def init(ctx, project_name, yes, plugin, marketplace_flag, verbose):

# Get project configuration (interactive mode or defaults)
if not yes:
config = _interactive_project_setup(final_project_name, logger)
config = _interactive_project_setup(final_project_name, logger, namespace=namespace)
else:
# Use auto-detected defaults
config = _get_default_config(final_project_name)
if namespace:
config["namespace"] = namespace

# Plugin mode uses 0.1.0 as default version
if plugin and yes:
Expand Down Expand Up @@ -209,7 +220,7 @@ def init(ctx, project_name, yes, plugin, marketplace_flag, verbose):
sys.exit(1)


def _interactive_project_setup(default_name, logger):
def _interactive_project_setup(default_name, logger, namespace=None):
"""Interactive setup for new APM projects with auto-detection."""
from ._helpers import _auto_detect_author, _auto_detect_description, _validate_project_name

Expand Down Expand Up @@ -239,11 +250,19 @@ def _interactive_project_setup(default_name, logger):
version = Prompt.ask("Version", default="1.0.0").strip()
description = Prompt.ask("Description", default=auto_description).strip()
author = Prompt.ask("Author", default=auto_author).strip()
if not namespace:
namespace = Prompt.ask("Namespace (optional)", default="").strip()
else:
namespace = namespace.strip()
if namespace:
namespace = validate_namespace(namespace)

summary_content = f"""name: {name}
version: {version}
description: {description}
author: {author}"""
if namespace:
summary_content += f"\nnamespace: {namespace}"
console.print(Panel(summary_content, title="About to create", border_style="cyan"))

if not Confirm.ask("\nIs this OK?", default=True):
Expand All @@ -267,20 +286,31 @@ def _interactive_project_setup(default_name, logger):
version = click.prompt("Version", default="1.0.0").strip()
description = click.prompt("Description", default=auto_description).strip()
author = click.prompt("Author", default=auto_author).strip()
if not namespace:
namespace = click.prompt("Namespace (optional)", default="").strip()
else:
namespace = namespace.strip()
if namespace:
namespace = validate_namespace(namespace)

click.echo(f"\n{INFO}About to create:{RESET}")
click.echo(f" name: {name}")
click.echo(f" version: {version}")
click.echo(f" description: {description}")
click.echo(f" author: {author}")
if namespace:
click.echo(f" namespace: {namespace}")

if not click.confirm("\nIs this OK?", default=True):
logger.progress("Aborted.")
sys.exit(0)

return {
config = {
"name": name,
"version": version,
"description": description,
"author": author,
}
if namespace:
config["namespace"] = namespace
return config
Loading