Skip to content
Closed
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
1 change: 1 addition & 0 deletions .apm/instructions/architecture.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ semicolon-delimited, and specific to the file(s) that own the fact.
| Effective marketplace output path | marketplace/output_profiles.py (resolve_effective_output_path) | `src/apm_cli/marketplace/output_profiles.py` |
| Bootstrap project-name validation and fallback | core/project_name.py (resolve_bootstrap_project_name) | `src/apm_cli/core/project_name.py` |
| Marketplace raw-structure diagnostics | marketplace/models.py parser; validator.py consumes them | `src/apm_cli/marketplace/models.py`; `src/apm_cli/marketplace/validator.py` |
| Selectable and deployable skill source | integration/skill_integrator.py (SkillIntegrator.skill_source_dir) | `src/apm_cli/integration/skill_integrator.py` |
| Agent Plugins v1 contract interpretation, component discovery, and portable manifest authority | agent_plugins/loader.py (load_agent_plugin, _load_apm_configuration) | `src/apm_cli/agent_plugins/loader.py`; `src/apm_cli/agent_plugins/ir.py` |
| Agent Plugin producer portable-surface admission | bundle/agent_plugin_exporter.py (_require_portable_agent_plugin) | `src/apm_cli/bundle/agent_plugin_exporter.py` |
| APMPackage interpreted-manifest construction | models/apm_package.py (APMPackage.from_mapping) | `src/apm_cli/models/apm_package.py` |
Expand Down
1 change: 1 addition & 0 deletions .github/instructions/architecture.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ semicolon-delimited, and specific to the file(s) that own the fact.
| Effective marketplace output path | marketplace/output_profiles.py (resolve_effective_output_path) | `src/apm_cli/marketplace/output_profiles.py` |
| Bootstrap project-name validation and fallback | core/project_name.py (resolve_bootstrap_project_name) | `src/apm_cli/core/project_name.py` |
| Marketplace raw-structure diagnostics | marketplace/models.py parser; validator.py consumes them | `src/apm_cli/marketplace/models.py`; `src/apm_cli/marketplace/validator.py` |
| Selectable and deployable skill source | integration/skill_integrator.py (SkillIntegrator.skill_source_dir) | `src/apm_cli/integration/skill_integrator.py` |
| Agent Plugins v1 contract interpretation, component discovery, and portable manifest authority | agent_plugins/loader.py (load_agent_plugin, _load_apm_configuration) | `src/apm_cli/agent_plugins/loader.py`; `src/apm_cli/agent_plugins/ir.py` |
| Agent Plugin producer portable-surface admission | bundle/agent_plugin_exporter.py (_require_portable_agent_plugin) | `src/apm_cli/bundle/agent_plugin_exporter.py` |
| APMPackage interpreted-manifest construction | models/apm_package.py (APMPackage.from_mapping) | `src/apm_cli/models/apm_package.py` |
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Windows binary is now Authenticode-signed in the release workflow, eliminating
the `Trojan:Script/Wacatac.H!ml` Windows Defender false positive on unsigned
PyInstaller bundles. (#2435)
- `apm install --skill <name>` now selects individual skills from plugins that
declare the conventional `"skills": ["./skills/"]` container instead of
reporting `Available: (none)`. Manifest declarations remain authoritative,
unsafe symlink sources are ignored, and malformed or colliding skill entries
now produce actionable diagnostics. (closes #2530) - by @edenfunf (#2536)
- Multi-target `apm compile` now avoids repeating expensive project analysis
for each target, making multi-target runs scale like single-target runs
without changing generated output. (closes #2482)
Expand Down
5 changes: 3 additions & 2 deletions CONFORMANCE.json
Original file line number Diff line number Diff line change
Expand Up @@ -544,9 +544,10 @@
"keyword": "MUST",
"section": "4.3.2",
"status": "active",
"test_count": 1,
"test_count": 2,
"tests": [
"tests/spec_conformance/test_manifest_reqs.py::test_consumer_diagnoses_empty_skill_subset_match"
"tests/spec_conformance/test_manifest_reqs.py::test_consumer_diagnoses_empty_skill_subset_match",
"tests/spec_conformance/test_manifest_reqs.py::test_consumer_names_skills_a_plugin_collection_exposes"
]
},
{
Expand Down
2 changes: 1 addition & 1 deletion CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ All four conformance classes (Producer, Consumer, Registry, Governance) carry ac
| [req-mf-019](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-019) | MUST | 4.2.4 | consumer | active | 1 |
| [req-mf-020](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-020) | MUST | 4.1 | consumer | active | 1 |
| [req-mf-021](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-021) | MUST | 4.8 | producer | active | 1 |
| [req-mf-022](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-022) | MUST | 4.3.2 | consumer | active | 1 |
| [req-mf-022](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-022) | MUST | 4.3.2 | consumer | active | 2 |
| [req-mf-023](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-023) | MUST | 4.5 | consumer | active | 1 |
| [req-mf-024](docs/src/content/docs/specs/openapm-v0.1.md#req-mf-024) | MUST | 4.3.2 | consumer | active | 1 |
| [req-pl-001](docs/src/content/docs/specs/openapm-v0.1.md#req-pl-001) | MUST | 6.1 | governance | active | 1 |
Expand Down
4 changes: 2 additions & 2 deletions apm.lock.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2783,7 +2783,7 @@ deployments:
owners:
- .
active_owner: .
content_hash: sha256:ab625ab2263bde79d1b5fca67c69afdee1a07037a9a0507f39704378eefea9e6
content_hash: sha256:8c44ae62413f8af09e0cbf7f93e486683f3aeeed89afc6c9c3fa6128a2b17650
- kind: project-relative
target: copilot
value: .github/instructions/changelog.instructions.md
Expand Down Expand Up @@ -3290,7 +3290,7 @@ local_deployed_file_hashes:
.github/agents/spec-tag-architect.agent.md: sha256:82907265c5e7cf1ac61ad96866fa7c5683b69c8f09b7a4c5f3cc241acc9568ca
.github/agents/supply-chain-security-expert.agent.md: sha256:8fb8cc426d6af17ba084a28b3f026c2b475b62e3ca63ed2f88b83bd823f877af
.github/agents/test-coverage-expert.agent.md: sha256:48c2172d1f18a394fa83ef9dc2be0b9b921a4e51e976498165250fed66369711
.github/instructions/architecture.instructions.md: sha256:ab625ab2263bde79d1b5fca67c69afdee1a07037a9a0507f39704378eefea9e6
.github/instructions/architecture.instructions.md: sha256:8c44ae62413f8af09e0cbf7f93e486683f3aeeed89afc6c9c3fa6128a2b17650
.github/instructions/changelog.instructions.md: sha256:1e51ec4c74e847967962bd279dc4c6e582c5d3578490b3c28d5f3acd3e05f73e
.github/instructions/cicd.instructions.md: sha256:33201cb88ea2f34b4950a9b52f87dc8dfb682796aaf53068ba7ae406c0c5e2c2
.github/instructions/cli.instructions.md: sha256:8e39e8d5047ce88575cb02f87c2bcede584dfef258bd86f7466c7badf136541a
Expand Down
23 changes: 23 additions & 0 deletions docs/src/content/docs/reference/package-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,29 @@ root, install exits non-zero before deployment or lockfile commit. Likewise,
Omit an optional field or use an empty list when the plugin has no component
of that type.

A declared `skills` entry can name either one skill directory or a container
whose immediate children are skills. If the entry has no reachable `SKILL.md`
at either depth, APM warns that nothing is selectable or deployable. Declare
the skill directory itself, or place each skill one level below the container.

For a container:

```json
{"skills": ["./skills/"]}
```

For one skill:

```json
{"skills": ["./skills/search"]}
```

Either form makes `search` selectable:

```bash
apm install owner/repo --skill search
```

**When to choose:** you already have a Claude plugin and want APM to
consume it without restructuring. This is still the no-flag default output
of `apm pack` and `apm plugin init`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,10 @@ for the portable format.

When `apm.yml` declares `target: claude` or `target: copilot` (or the plural `targets:` equivalent), `apm pack` also generates an ecosystem-specific `plugin.json` automatically -- authors no longer need to maintain this file manually. The manifest is synthesised from `apm.yml` identity fields (`name`, `version`, `description`, `author`, `license`). See the apm pack reference (reference/cli/pack/#plugin-manifests) for output paths, credential stripping, and per-ecosystem differences, or run `apm pack --help`.

In a hand-authored `plugin.json`, declare either one skill directory or a
container of immediate skill directories; see the
[plugin collection reference](../../../../../docs/src/content/docs/reference/package-types.md#plugin-collection-pluginjson).

#### Shipping `bin/` executables (Claude Code only)

A marketplace plugin may ship a root `bin/` directory of executable
Expand Down
95 changes: 95 additions & 0 deletions scripts/lint-architecture-boundaries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,101 @@ if [ "$mcp_runtime_variable_owner_defs" -ne 1 ] \
violations=$((violations + 1))
fi

echo "[*] AC33: skill source routing authority"
skill_source_owner="src/apm_cli/integration/skill_integrator.py"
skill_source_check=$(python3 - "$skill_source_owner" <<'PY'
import ast
import sys
from pathlib import Path

tree = ast.parse(Path(sys.argv[1]).read_text(encoding="utf-8"))
methods = {
node.name: node
for node in ast.walk(tree)
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name
in {
"skill_source_dir",
"available_skill_names",
"integrate_package_skill",
"_promote_sub_skills_standalone",
}
}
errors = []
if set(methods) != {
"skill_source_dir",
"available_skill_names",
"integrate_package_skill",
"_promote_sub_skills_standalone",
}:
errors.append("required methods are missing")


def calls_owner(node: ast.AST, receiver: str) -> bool:
return any(
isinstance(call, ast.Call)
and isinstance(call.func, ast.Attribute)
and call.func.attr == "skill_source_dir"
and isinstance(call.func.value, ast.Name)
and call.func.value.id == receiver
and len(call.args) == 1
and isinstance(call.args[0], ast.Name)
and call.args[0].id == "package_info"
for call in ast.walk(node)
)


if "available_skill_names" in methods and not calls_owner(
methods["available_skill_names"], "SkillIntegrator"
):
errors.append("available_skill_names must call SkillIntegrator.skill_source_dir(package_info)")
if "integrate_package_skill" in methods and not calls_owner(
methods["integrate_package_skill"], "self"
):
errors.append("integrate_package_skill must call self.skill_source_dir(package_info)")
if "integrate_package_skill" in methods:
standalone_calls = [
call
for call in ast.walk(methods["integrate_package_skill"])
if isinstance(call, ast.Call)
and isinstance(call.func, ast.Attribute)
and call.func.attr == "_promote_sub_skills_standalone"
]
if not standalone_calls or any(
len(call.args) < 3
or not isinstance(call.args[2], ast.Name)
or call.args[2].id != "source_dir"
for call in standalone_calls
):
errors.append("standalone deployment must receive the canonical source_dir")
if "_promote_sub_skills_standalone" in methods:
standalone_source = ast.unparse(methods["_promote_sub_skills_standalone"])
if "package_path / '.apm' / 'skills'" in standalone_source:
errors.append("standalone deployment must not reconstruct the normalized source path")
if "skill_source_dir" in methods:
owner_source = ast.unparse(methods["skill_source_dir"])
if "PackageType.MARKETPLACE_PLUGIN" not in owner_source:
errors.append("skill_source_dir must preserve marketplace plugin manifest authority")
owner_constants = {
node.value
for node in ast.walk(methods["skill_source_dir"])
if isinstance(node, ast.Constant) and isinstance(node.value, str)
}
if not {".apm", "skills"}.issubset(owner_constants):
errors.append("skill_source_dir must own the normalized plugin skills path")

if errors:
print("\n".join(errors))
raise SystemExit(1)
PY
)
skill_source_status=$?
if [ "$skill_source_status" -ne 0 ]; then
echo "[x] Skill source selection must route through SkillIntegrator.skill_source_dir"
echo "$skill_source_check"
violations=$((violations + 1))
fi

echo "[*] AC34: hash-visible generated files use canonical LF writers"
if ! python3 scripts/check_hash_visible_lf_writes.py; then
echo "[x] Hash-visible generated files must route through canonical LF writers"
Expand Down
Loading
Loading