Skip to content

fix(install): route HYBRID and CLAUDE_SKILL packages through skill-bundle path#946

Merged
danielmeppiel merged 4 commits intomainfrom
fix/hybrid-skill-bundle
Apr 26, 2026
Merged

fix(install): route HYBRID and CLAUDE_SKILL packages through skill-bundle path#946
danielmeppiel merged 4 commits intomainfrom
fix/hybrid-skill-bundle

Conversation

@danielmeppiel
Copy link
Copy Markdown
Collaborator

TL;DR

apm install danielmeppiel/genesis -g appeared to hang because the validator silently rejected packages shipped as a single skill bundle at the repo root (apm.yml + SKILL.md, no .apm/). This PR teaches APM to recognize the skill-bundle layout as a first-class shape and route it through the existing _integrate_native_skill copy path, so the skill installs as one bundle with its co-located agents/, assets/, and scripts/ intact. CLAUDE_SKILL packages (SKILL.md alone) get the same treatment. Direct-dependency failures now print a [x] line inline and exit 1 instead of failing silently.

Problem (WHY)

  • The validator at models/validation.py had three branches: APM (.apm/), CLAUDE_SKILL (SKILL.md only), CLAUDE_PLUGIN (plugin.json). HYBRID (apm.yml + SKILL.md) was detected by detect_package_type but then fell through to _validate_apm_package_with_yml, which mandates a .apm/ directory and produces Missing required directory: .apm/.
  • package_validator.py enforced the same .apm/ mandate without consulting the package type, so even fixing validation alone would not help.
  • When direct-dep validation failed, integrate.py returned deltas is None and silently continued the loop. No [x] line, no diagnostic. The user sees CLI sit at the integrate phase until the run completes with a clean exit code, hence "perceived hang."
  • Hoisting a skill bundle's internal agents/ into .github/agents/ would violate authorial intent. agentskills.io's encapsulation contract is that agents/, assets/, scripts/ inside a skill directory are private resources, "agents pattern-match well against concrete structures" -- the structure IS the contract.

Note

CEO-ratified principle from the panel review: APM respects authorial intent at the layout level. Three layouts -> three install semantics. The shape of the package root determines how APM installs it.

Approach (WHAT)

Root signal Author intent Install semantic
.apm/ (with or without apm.yml) Multiple independent primitives Hoist into target's primitive 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 _map_plugin_artifacts

Metadata precedence for HYBRID: apm.yml wins on name, version, license, dependencies, scripts; SKILL.md frontmatter wins on description, allowed-tools. Conflicts go to apm.yml with a verbose-mode warning. This keeps APM-owned fields (deps, scripts) authoritative while leaving Claude-runtime fields owned by the skill.

Implementation (HOW)

File Change
src/apm_cli/models/validation.py New _validate_hybrid_package() validator (~60 lines); dispatcher routing at _validate_package_structure. Improved error wording for INVALID and missing-.apm/ cases.
src/apm_cli/deps/package_validator.py Gated the .apm/ directory mandate behind detect_package_type() -- only required for APM_PACKAGE and INVALID. HYBRID and CLAUDE_SKILL are exempt.
src/apm_cli/install/phases/integrate.py Computes direct_dep_keys from manifest; emits [x] via logger.error() and pushes to DiagnosticCollector when deltas is None for a direct dep; sets ctx.direct_dep_failed = True.
src/apm_cli/install/context.py New direct_dep_failed: bool = False field.
src/apm_cli/install/pipeline.py After integrate phase, renders diagnostics summary and raises RuntimeError when ctx.direct_dep_failed. Outer handler in commands/install.py catches and exits 1.
tests/test_apm_package_models.py TestHybridPackageValidation (5 tests) + TestClaudeSkillPackageValidation (2 tests).
docs/src/content/docs/reference/package-types.md New reference page documenting the three layouts and metadata precedence.
docs/src/content/docs/getting-started/first-package.md Added "Choosing a package layout" section linking to the reference.
packages/apm-guide/.apm/skills/apm-usage/package-authoring.md Updated per Rule 4 -- HYBRID/CLAUDE_SKILL section with the three-layouts table.
src/apm_cli/commands/install.py One-line help-text update mentioning supported layouts.
CHANGELOG.md Entry under ### Fixed.

The skill integrator's existing _integrate_native_skill (shutil.copytree(package_path, target_skill_dir, ignore=shutil.ignore_patterns('.apm'))) already copies arbitrary subdirectories. No changes needed there. Encapsulation is preserved because distributed_compiler.py only scans .github/agents/*.agent.md and DEPENDENCY_PRIMITIVE_PATTERNS only scans inside .apm/agents/ -- bundled agents/ files inside skills/<name>/agents/ are never hoisted.

Diagrams

Flow: validator dispatch by detected package type. Each shape routes to its own validator and integrator -- HYBRID joins CLAUDE_SKILL on the bundle path.

flowchart TD
    A[Package root] --> B{detect_package_type}
    B -->|.apm/ present, no SKILL.md| C[APM_PACKAGE]
    B -->|SKILL.md only| D[CLAUDE_SKILL]
    B -->|apm.yml + SKILL.md| E[HYBRID]
    B -->|plugin.json| F[CLAUDE_PLUGIN]
    C --> G[_validate_apm_package_with_yml<br/>mandates .apm/]
    D --> H[_validate_claude_skill]
    E --> I[_validate_hybrid_package<br/>NEW]
    F --> J[_validate_claude_plugin]
    G --> K[Hoist primitives to target dirs]
    H --> L[_integrate_native_skill<br/>copy tree to skills/name/]
    I --> L
    J --> M[_map_plugin_artifacts<br/>dissect by manifest]
Loading

Failure path: when a direct dep returns deltas is None, the new fail-loud path surfaces the error to the user and exits non-zero instead of silently continuing.

sequenceDiagram
    participant User
    participant Pipeline
    participant Integrate as integrate phase
    participant Logger as CommandLogger
    participant Diag as DiagnosticCollector
    User->>Pipeline: apm install <dep>
    Pipeline->>Integrate: run integrate
    Integrate->>Integrate: run_integration_template -> deltas=None
    Note over Integrate: dep_key in direct_dep_keys
    Integrate->>Logger: error("[x] Package X failed to integrate")
    Integrate->>Diag: error(msg, package=X, detail=...)
    Integrate->>Pipeline: ctx.direct_dep_failed=True
    Pipeline->>Diag: render_summary()
    Pipeline->>User: raise RuntimeError -> exit 1
Loading

Trade-offs

  • Apm.yml-wins precedence on shared fields. Rejected SKILL.md-wins because dependencies/scripts are APM-owned semantics; the skill's runtime metadata (description, allowed-tools) stays SKILL.md-owned. Verbose warning gives authors a deterministic signal when they conflict.
  • Did NOT add ensure_path_within guards to _integrate_native_skill in this PR. Sec audit confirmed the gap is pre-existing on main (CLAUDE_SKILL already reaches that path). Filing as a P1 follow-up rather than expanding scope. "Favor small, chainable primitives over monolithic frameworks."
  • Lockfile per-deployed-file hashing for skill bundles is a known gap: deployed_files records directory paths so the cleanup gate cannot detect intra-bundle edits. Filing as a P2 follow-up; out of scope here.
  • Routing log is downstream, not at validator. The package_type_info() verbose line already fires from sources.py:536 for downloaded packages -- no duplicate log added at validation time.

Benefits

  1. apm install danielmeppiel/genesis -g works end-to-end (was the bug report).
  2. Authors can ship a single skill at the repo root with co-located agents//assets//scripts/ -- the agentskills.io canonical shape.
  3. Direct-dep failures now exit 1 with a [x] line and an entry in the install summary -- no more "perceived hang".
  4. Authorial intent is preserved at install time: a skill bundle stays a skill bundle, never gets dissected into top-level primitives.
  5. New reference page + skill-resource update make the three layouts explicit and discoverable.

Validation

$ uv run pytest tests/unit tests/test_console.py \
    --deselect tests/test_apm_package_models.py::TestGenericHostSubdirectoryRoundTrip::test_build_download_ref_preserves_virtual_path -q
...
5489 passed, 1 warning, 27 subtests passed in 31.21s

The deselected test is a pre-existing Mock-wiring bug unrelated to this PR (Mock object compared against a string -- result.host == 'git.example.com' fails because host was wired through a Mock). It also fails on main.

New tests added (7)

TestHybridPackageValidation:

  • test_hybrid_package_with_apm_yml_and_skill_md_validates
  • test_hybrid_package_without_apm_dir_validates
  • test_hybrid_package_with_optional_subdirs_validates
  • test_hybrid_package_missing_skill_md_invalid
  • test_hybrid_package_with_apm_dir_still_validates

TestClaudeSkillPackageValidation:

  • test_claude_skill_with_agents_dir_not_misclassified_as_plugin
  • test_claude_skill_only_skill_md_validates

How to test

  • cd /tmp && uv tool run --from /Users/<you>/Repos/awd-cli-hybrid-skill apm install danielmeppiel/genesis -g -- expect a clean install with a [+] Installed skill genesis line and exit 0.
  • Inspect ~/.apm/skills/genesis/ -- expect SKILL.md, agents/genesis-architect.agent.md, and any other co-located bundle resources to be present.
  • Inspect ~/.apm/agents/ -- expect genesis-architect.agent.md to NOT be there (encapsulation invariant).
  • Construct a HYBRID package locally with a deliberately broken apm.yml (e.g. invalid YAML), install with --verbose, expect a [x] line naming the package and a non-zero exit code.
  • apm install --help -- expect the new layout hint in the description.

Panel review trail

Single-comment verdict from the apm-review-panel was synthesized in the planning conversation. The 5 mandatory specialists + CEO + Growth Hacker ratified the design; auth-expert was inactive (no core/auth.py, token_manager.py, azure_cli.py, github_downloader.py, marketplace/client.py, or validation.py auth surface touched). Subsequent subagent reviews on the implementation:

  • Python Architect: implementation passes; CLAUDE_SKILL path verified end-to-end.
  • Supply Chain Security: safe to merge as-is; three pre-existing follow-ups filed (symlink rejection in skill_integrator.copytree, per-file deployed-file hashes for bundles, enterprise/security.md:205 accuracy).
  • CLI Logging: 2 fixes applied (tightened error message; pushed to DiagnosticCollector). All paths route through CommandLogger.
  • DevX UX: docs delivered (new package-types.md, first-package.md section, install help one-liner).

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

…ndle path

HYBRID packages (apm.yml + SKILL.md without .apm/) and CLAUDE_SKILL
packages (SKILL.md alone with co-located agents/, assets/, scripts/)
are now installed as single skill bundles per agentskills.io semantics
instead of being rejected by the validator.

CEO-ratified principle: APM respects authorial intent at the layout
level. The shape of the package root determines install semantics:
.apm/ -> hoist primitives; SKILL.md -> bundle copy; plugin.json ->
plugin dissection.

Direct-dependency integration failures now print a [x] line inline and
exit 1 via fail-loud propagation through pipeline. Errors are pushed
to DiagnosticCollector for the end-of-install summary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 26, 2026 06:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates APM's install/validation flow to treat repo-root skill bundles (SKILL.md at root, optionally with apm.yml = HYBRID) as first-class package layouts, routing them through the skill-bundle install path and surfacing direct-dependency failures loudly instead of silently continuing.

Changes:

  • Add HYBRID (apm.yml + SKILL.md, no .apm/) validation path and relax .apm/ requirements where appropriate.
  • Fail-loud on direct dependency integration failures (inline error + diagnostics summary + non-zero exit).
  • Document the supported package layouts and update help text/tests accordingly.
Show a summary per file
File Description
src/apm_cli/models/validation.py Routes HYBRID packages through a new validator and improves certain validation errors.
src/apm_cli/deps/package_validator.py Gates .apm/ requirement by detected package type.
src/apm_cli/install/phases/integrate.py Marks and logs direct-dependency failures when integration returns None.
src/apm_cli/install/context.py Adds direct_dep_failed flag to the install context.
src/apm_cli/install/pipeline.py Aborts install after integrate when a direct dependency failed.
src/apm_cli/commands/install.py Updates CLI help text to mention supported layouts.
tests/test_apm_package_models.py Adds tests for HYBRID and CLAUDE_SKILL validation behaviors.
docs/src/content/docs/reference/package-types.md Adds a new reference page describing package layouts and semantics.
docs/src/content/docs/getting-started/first-package.md Adds guidance on choosing a package layout and links to the reference.
docs/src/content/docs/reference/examples.md Sidebar order adjustment to accommodate the new reference page.
docs/src/content/docs/reference/experimental.md Sidebar order adjustment to accommodate the new reference page.
packages/apm-guide/.apm/skills/apm-usage/package-authoring.md Updates package-authoring guidance for the new layouts.
CHANGELOG.md Adds an Unreleased “Fixed” entry for the new behavior.

Copilot's findings

  • Files reviewed: 13/13 changed files
  • Comments generated: 6

Comment on lines 375 to +397
if deltas is None:
# Direct dependency failure: log immediately so the user
# sees [x] output inline instead of only in the deferred
# diagnostic summary (fixes "perceived hang" on HYBRID
# packages whose validation previously failed silently).
if dep_key in direct_dep_keys:
logger = ctx.logger
_fail_msg = (
f"Package {dep_key} failed to integrate "
f"(validation or download error)."
)
if logger:
logger.error(_fail_msg)
if ctx.diagnostics:
ctx.diagnostics.error(
_fail_msg,
package=dep_key,
detail=(
f"Resolved at {install_path}. "
f"Run with --verbose for details."
),
)
ctx.direct_dep_failed = True
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New fail-loud behavior (inline [x] log + ctx.direct_dep_failed -> pipeline abort) is user-facing, but there does not appear to be a unit test exercising this path (e.g., patch run_integration_template to return None for a direct dep and assert the pipeline/command exits non-zero and emits the diagnostic). Adding a focused test would prevent regressions back to the previous silent-continue behavior.

Copilot uses AI. Check for mistakes.
Comment thread CHANGELOG.md Outdated

### 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. The new validator dispatcher also accepts CLAUDE_SKILL packages (SKILL.md only) that ship co-located resource directories like `agents/`, `assets/`, or `scripts/`. Direct-dependency integration failures now print a `[x]` line immediately and exit 1 instead of failing silently.
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHANGELOG entry does not follow the repository's Keep a Changelog convention: it should be a single concise bullet ending with the PR number in parentheses (e.g. "... (#123)"). Consider splitting this into shorter bullets (HYBRID support, CLAUDE_SKILL resource dirs, fail-loud direct-dep integration) and adding the PR number.

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +21 to +22
- apm.yml wins for `name`, `version`, `license`, `dependencies`, `scripts`.
- SKILL.md frontmatter wins for `description`, `allowed-tools`.
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs claim SKILL.md frontmatter wins for description, but the implementation only uses SKILL.md as a fallback when apm.yml omits description (see _validate_hybrid_package: sets description only when not package.description). Please update this precedence description to match actual behavior (or adjust code/tests if SKILL.md should override).

Suggested change
- apm.yml wins for `name`, `version`, `license`, `dependencies`, `scripts`.
- SKILL.md frontmatter wins for `description`, `allowed-tools`.
- apm.yml wins for `name`, `version`, `license`, `dependencies`, `scripts`, and any other shared field it sets.
- `SKILL.md` frontmatter provides `description` only as a fallback when `apm.yml` omits it.

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +78
- **apm.yml wins** for: `name`, `version`, `license`, `dependencies`, `scripts`.
- **SKILL.md frontmatter wins** for: `description`, `allowed-tools`.
- On any shared-field conflict, apm.yml takes precedence and APM emits a
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metadata precedence documented here says SKILL.md frontmatter wins for description, but current validation logic treats SKILL.md as a fallback only when apm.yml does not set description. Please align this section with the implemented merge behavior (or change implementation/tests if SKILL.md is intended to override).

Suggested change
- **apm.yml wins** for: `name`, `version`, `license`, `dependencies`, `scripts`.
- **SKILL.md frontmatter wins** for: `description`, `allowed-tools`.
- On any shared-field conflict, apm.yml takes precedence and APM emits a
- **apm.yml wins** for: `name`, `version`, `license`, `description`,
`dependencies`, `scripts`.
- **SKILL.md frontmatter provides** skill-specific metadata such as
`allowed-tools`, and can supply `description` only when `apm.yml` does not.
- On any shared-field conflict, `apm.yml` takes precedence and APM emits a

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +82
# Check for .apm directory -- only mandatory for APM_PACKAGE layout.
# HYBRID and CLAUDE_SKILL packages may ship without .apm/.
from ..models.validation import detect_package_type, PackageType

pkg_type, _ = detect_package_type(package_path)
apm_dir = package_path / ".apm"
if not apm_dir.exists():
result.add_error("Missing required directory: .apm/")
return result

if not apm_dir.is_dir():
result.add_error(".apm must be a directory")
return result
if pkg_type in (PackageType.APM_PACKAGE, PackageType.INVALID) or pkg_type is None:
if not apm_dir.exists():
result.add_error("Missing required directory: .apm/")
return result

if not apm_dir.is_dir():
result.add_error(".apm must be a directory")
return result
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For HYBRID packages you now allow missing .apm/, but the rest of this method still assumes .apm/ exists and will emit a misleading warning ("No primitive files found in .apm/ directory") even though HYBRID skill bundles are not expected to have primitives there. Consider short-circuiting for HYBRID/CLAUDE_SKILL layouts (or gating the primitive scan/warning behind apm_dir.exists()) so validation output matches the selected package layout.

Copilot uses AI. Check for mistakes.
Comment on lines +327 to +333
if ctx.direct_dep_failed:
if ctx.diagnostics and ctx.diagnostics.has_diagnostics:
ctx.diagnostics.render_summary()
raise RuntimeError(
"One or more direct dependencies failed validation. "
"Run with --verbose for details."
)
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This raises a new RuntimeError after integrate, but run_install_pipeline's broad except Exception will wrap it as RuntimeError("Failed to resolve APM dependencies: ..."), which is both misleading (failure was integrate/validation) and may hide the intended message. Consider using a dedicated exception type (and re-raising it like PolicyViolationError), or adjusting the outer wrapper to preserve the original message for this failure mode.

Copilot uses AI. Check for mistakes.
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 26, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict

Disposition: REQUEST_CHANGES (two required pre-merge fixes; core logic is correct and well-tested)


Per-persona findings

Python Architect: This is a routine PR (new validation branch + new dataclass field + fail-loud error surfacing). No new base classes or protocol changes; the existing validator dispatcher pattern is cleanly extended.

1. OO / Class Diagram

classDiagram
    direction TD
    class PackageType {
        <<Enum>>
        APM_PACKAGE
        CLAUDE_SKILL
        HYBRID
        HOOK_PACKAGE
        MARKETPLACE_PLUGIN
        INVALID
    }
    class ValidationResult {
        <<ValueObject>>
        +is_valid bool
        +errors List[str]
        +warnings List[str]
        +package APMPackage
        +package_type PackageType
        +add_error(error)
        +add_warning(warning)
    }
    class DetectionEvidence {
        <<ValueObject>>
        +has_apm_yml bool
        +has_skill_md bool
        +has_plugin_evidence bool
    }
    class validate_apm_package {
        <<Pure>>
        +detect_package_type()
        +dispatch to sub-validator
    }
    class _validate_hybrid_package {
        <<Pure>>
        +check .apm/ present
        +parse apm.yml
        +merge SKILL.md frontmatter
    }
    class PackageValidator {
        <<IOBoundary>>
        +validate(package_path) ValidationResult
        +_check_primitive_content()
    }
    class InstallContext {
        <<ValueObject>>
        +direct_dep_failed bool
        +all_apm_deps List
        +deps_to_install List
        +diagnostics DiagnosticCollector
        +logger CommandLogger
    }
    class integrate_run {
        <<IOBoundary>>
        +run(ctx) None
        +direct_dep_keys set
    }
    class run_install_pipeline {
        <<IOBoundary>>
        +run_install_pipeline()
        +fail-loud on direct_dep_failed
    }
    validate_apm_package ..> PackageType : classifies with
    validate_apm_package ..> ValidationResult : returns
    validate_apm_package ..> _validate_hybrid_package : dispatches HYBRID
    validate_apm_package ..> DetectionEvidence : reads
    _validate_hybrid_package ..> ValidationResult : returns
    PackageValidator ..> validate_apm_package : calls
    integrate_run ..> InstallContext : reads/writes
    run_install_pipeline ..> integrate_run : calls
    run_install_pipeline ..> InstallContext : reads direct_dep_failed
    class _validate_hybrid_package:::touched
    class PackageValidator:::touched
    class InstallContext:::touched
    class integrate_run:::touched
    class run_install_pipeline:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading

2. Execution Flow Diagram

flowchart TD
    A["apm install pkg"] --> B["run_install_pipeline() [install/pipeline.py]"]
    B --> C["_integrate_phase.run(ctx) [install/phases/integrate.py]"]
    C --> D["direct_dep_keys = set(dep.get_unique_key() for dep in ctx.all_apm_deps)"]
    D --> E["for dep in ctx.deps_to_install"]
    E --> F["run_integration_template(source) [FS]"]
    F --> G["validate_apm_package(package_path) [models/validation.py]"]
    G --> H["detect_package_type(package_path)"]
    H --> I{PackageType?}
    I -- HYBRID + no .apm/ --> J["_validate_hybrid_package()\nparse apm.yml, read SKILL.md frontmatter [I/O]"]
    I -- HYBRID + .apm/ present --> K["_validate_apm_package_with_yml() [I/O]"]
    I -- APM_PACKAGE --> K
    I -- CLAUDE_SKILL --> L["_validate_claude_skill() [I/O]"]
    J --> M["ValidationResult returned"]
    K --> M
    L --> M
    M --> N{deltas is None?}
    N -- "No -- integrated OK" --> O["accumulate deltas; installed_count += 1"]
    O --> E
    N -- "Yes + dep_key in direct_dep_keys" --> P["logger.error(_fail_msg)\nctx.diagnostics.error(...)\nctx.direct_dep_failed = True"]
    P --> E
    N -- "Yes + transitive dep" --> E
    E --> Q{ctx.direct_dep_failed?}
    Q -- Yes --> R["diagnostics.render_summary()\nraise RuntimeError [exit 1]"]
    Q -- No --> S["_update_gitignore_for_apm_modules() [FS]"]
    S --> T["install complete [+]"]
Loading

Design patterns

  • Used in this PR: Dispatch table -- validate_apm_package() acts as a type-keyed dispatcher; _validate_hybrid_package fits cleanly alongside the existing _validate_claude_skill / _validate_apm_package_with_yml siblings. Collect-then-render -- ctx.direct_dep_failed flag + DiagnosticCollector.render_summary() extends the existing phase-coordination pattern.
  • Pragmatic suggestion: none -- the current shape is the simplest correct design at this scope.

Minor concern: ctx.all_apm_deps is populated from get_apm_dependencies() + get_dev_apm_dependencies() (direct deps from apm.yml). Naming it all_apm_deps is slightly misleading -- a reader might assume it includes transitive deps. The direct_dep_keys computation is correct in practice, but a comment (# direct deps declared in apm.yml, not transitive) near the InstallContext field definition would prevent future bugs.


CLI Logging Expert: The fail-loud error path (logger.error() + ctx.diagnostics.error() + deferred render_summary()) correctly implements the collect-then-render pattern. Two observations:

  1. Double-logging: The inline logger.error(_fail_msg) and the ctx.diagnostics.error(_fail_msg, ...) use the same message text. When render_summary() runs, the user will see the message twice (inline + summary). This is acceptable behavior (inline = "this failed NOW"; summary = "here are all failures"), but the inline message should be slightly terser to avoid feeling redundant. Consider: f"[x] {dep_key}: integration failed -- see summary below" for the inline path, reserving the full message for the diagnostic entry.

  2. RuntimeError message in pipeline.py: "One or more direct dependencies failed validation. Run with --verbose for details." does not name which dependency failed. The DiagnosticCollector renders the full detail, but if the RuntimeError is caught and re-displayed anywhere, the dep name is lost. Low risk for now; worth a follow-up to thread the failed dep names through.

  3. CHANGELOG entry is missing the PR number (#946). Per conventions every entry must end with (#PR_NUMBER).


DevX UX Expert: The experience improvement is real and well-executed. Specific observations:

  1. first-package.md "Choosing a package layout" section: Clean three-bullet summary with concrete guidance. Passes the first-run test -- a new author can make the right choice without reading the full reference.

  2. package-types.md reference page: The "Layout summary" table is exactly the right format. "When to choose" guidance is actionable. This is the kind of content that prevents support questions.

  3. doc/code inconsistency on description precedence (this is the highest-priority finding in this PR):

    package-types.md states: "SKILL.md frontmatter wins for: description, allowed-tools"
    package-authoring.md states: "SKILL.md frontmatter wins for description, allowed-tools."

    But _validate_hybrid_package implements:

    if skill_description and not package.description:
        package.description = skill_description

    apm.yml description wins when set; SKILL.md is only the fallback. The test test_hybrid_apm_yml_description_wins_over_skill_md confirms this and notes "(CEO call)". Both docs are wrong on this point. Required fix before merge.

  4. commands.md not updated: Per Rule 4, the packages/apm-guide/.apm/skills/apm-usage/commands.md must be updated when CLI command help text changes. The install command help text changed in this PR; commands.md was not updated.

  5. install help text: The new text ("supports APM packages, Claude skills (SKILL.md), and plugin collections (plugin.json); auto-creates apm.yml...") is informative but verbose for a one-line help string. Acceptable; discoverability benefit outweighs the length cost.


Supply Chain Security Expert: No new security surface introduced. Observations:

  1. Path handling in HYBRID skill-bundle path: _validate_hybrid_package reads apm.yml and SKILL.md from package_path. The path originates in apm_modules_dir (controlled), and the existing download/unpack chain is responsible for containment. No new path traversal surface.

  2. frontmatter.load() on package-supplied SKILL.md: Metadata from a remote package (e.g. description) is merged into the install context. The merge is local-only (not sent to network) and is only used for logging/display. The bare except Exception catches both ImportError and parse errors -- over-broad but acceptable since the consequence is only a warning.

  3. Fail-loud is a security improvement: The previous silent-continue on direct dep validation failure was a mild integrity gap. Surfacing it immediately and exiting non-zero makes APM fail-closed on direct dep failures, which is the correct default.

  4. Pre-existing symlink concern in skill integrator: skill_integrator.py has 3 shutil.copytree calls that follow symlinks (lack symlinks=True + no ignore_symlinks guard). HYBRID packages now route through the skill integrator, meaning this pre-existing gap applies to more package layouts. This is not introduced by this PR but worth tracking as a follow-up issue.

  5. install_path in diagnostic detail: Local filesystem path surfaced in a diagnostic message -- expected, helpful, not a leak.


Auth Expert: Not activated -- this PR modifies package type detection (models/validation.py), package validator (deps/package_validator.py), and install pipeline phase coordination (install/phases/integrate.py, install/pipeline.py). None of these files touch authentication, token management, credential resolution, or AuthResolver host classification.


OSS Growth Hacker: Strong growth signal in this PR.

  1. agentskills.io alignment is the headline: This PR makes APM install-compatible with the SKILL.md layout that is the canonical format for gh skill (launched 2026-04-16, gh v2.90.0 public preview). Story hook: "APM now installs packages from the GitHub Skills ecosystem (gh skill) without any restructuring -- apm.yml + SKILL.md just works." This is a cross-ecosystem adoption unlock and should be the top item in the release announcement.

  2. "No silent failures" is a retention story: The [x] inline error fix converts a perceived hang (user gives up, opens an issue, or leaves) into an actionable failure message. This reduces the support burden and makes the first-install experience trustworthy.

  3. package-types.md reduces contributor friction: A clear reference for "which layout should I use?" is a prerequisite for community-authored HYBRID packages. Protect this page.

  4. CHANGELOG entry needs cleanup for release narrative use: it is missing (#946), runs as one very long sentence, and mixes three separate improvements. Suggest splitting into three concise bullets for the release post.

  5. Side-channel to CEO: The gh skill angle is worth a standalone announcement tweet / release highlight -- not just buried in the CHANGELOG.


CEO arbitration

The core change is correct, well-tested, and strategically important -- agentskills.io compatibility is an adoption unlock that aligns with the gh skill ecosystem timing. Specialists agree on the fix direction; there are no architectural disagreements to arbitrate. Two issues block merge:

(1) Doc/code inconsistency on description precedence. Both package-types.md and package-authoring.md claim SKILL.md frontmatter wins for description, but the code (and the test, which explicitly cites "CEO call") implement apm.yml winning when it provides a description. The docs must match the code. The correct statement is: "apm.yml wins for name, version, license, dependencies, scripts, and description when set; SKILL.md frontmatter fills in description and allowed-tools only when apm.yml does not set them."

(2) commands.md not updated. The apm install help text changed; packages/apm-guide/.apm/skills/apm-usage/commands.md must reflect it (Rule 4, same-PR requirement).

The double-logging concern and the all_apm_deps naming ambiguity are follow-up items, not blockers. The pre-existing symlink gap in the skill integrator should be tracked as a separate issue. The CHANGELOG entry missing (#946) is a trivial pre-merge fix that can go in the same commit.


Required actions before merge

  1. Fix description-precedence language in docs. In docs/src/content/docs/reference/package-types.md (Metadata precedence section) and packages/apm-guide/.apm/skills/apm-usage/package-authoring.md, replace "SKILL.md frontmatter wins for: description, allowed-tools" with "apm.yml wins for description when set; SKILL.md frontmatter fills in description and allowed-tools only when apm.yml omits them."

  2. Update packages/apm-guide/.apm/skills/apm-usage/commands.md to reflect the new apm install help text (per Rule 4: CLI help changes must be mirrored to the shipped skill resource in the same PR).

  3. Add (#946) to the CHANGELOG entry. The current entry is missing the PR number reference required by CHANGELOG.md conventions.


Optional follow-ups

  • Open a dedicated issue to add ignore_symlinks protection to the 3 shutil.copytree calls in src/apm_cli/integration/skill_integrator.py (lines ~330, ~568, ~818). HYBRID packages now route through this path, making the pre-existing gap more reachable.
  • Add a comment to the all_apm_deps field in src/apm_cli/install/context.py clarifying it holds only direct deps from apm.yml (not the full transitive closure), to prevent future misreading.
  • Consider terser inline error message in integrate.py to avoid the double-output feel ([x] <dep_key>: integration failed inline; full detail in render_summary()).
  • Release announcement: lead with the gh skill / agentskills.io compatibility angle -- this is the headline for the next release post.

Generated by PR Review Panel for issue #946 · ● 966.7K ·

…eview fixes

Round-2 review fixes for PR #946:

Conceptual: apm.yml.description and SKILL.md description are independent
fields with different consumers (CLI/search vs. agent invocation matcher
per agentskills.io) and are NOT merged. Removed the silent backfill that
let SKILL.md leak into APMPackage.description. apm pack now emits a hard
warning when apm.yml is missing 'description' on a HYBRID package -- the
publish-time gate for the AUTHOR.

Security: ignore_symlinks now wraps all 3 shutil.copytree call sites in
skill_integrator.py (lines 330, 568, 818). The third site composes
ignore_symlinks with shutil.ignore_patterns('.apm') via a closure.

UX: integrate.py emits direct-dep failures via diagnostics OR logger
(not both), with full detail deferred to render_summary. apm view shows
a HYBRID-aware hint when apm.yml.description is missing.

Docs/skill-resources: replaced 'metadata precedence' framing with
'metadata model' (independence + two consumers) in docs/reference and
in packages/apm-guide/.apm/skills/apm-usage/. commands.md install row
purpose updated to match new help text. CHANGELOG entry extended and
tagged with (#946).

Tests: 5494 passed (1 pre-existing failure unrelated to this PR).
Added TestPackHybridDescriptionWarning (3 tests),
TestSkillIntegratorCopytreeSymlinkContainment (2 tests), and 2
HYBRID-independence tests in test_apm_package_models.py.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel
Copy link
Copy Markdown
Collaborator Author

Round 3 update — all required findings addressed

Pushed 11d86528 addressing the round-2 review (3 required + 3 promoted-to-required):

Conceptual fix: metadata model

The reviewer caught that docs claimed "SKILL.md frontmatter wins for description, allowed-tools" — wrong on both counts. Reframed as two independent fields with two consumers (CLI/search vs. agent invocation matcher per agentskills.io). APM no longer merges them. The silent backfill in validation.py is removed. apm pack now emits a hard warning when apm.yml is missing description on a HYBRID package — author-time publish gate.

What changed

  • validation.py — strict independence; descriptions never cross the apm.yml↔SKILL.md boundary
  • skill_integrator.pyignore_symlinks on all 3 shutil.copytree paths (line 818 composes with .apm filter via closure)
  • packer.py — HYBRID author warning at pack time
  • integrate.py — single emission path for direct-dep failures (no more double-output)
  • _utils.pyapm view HYBRID-aware description hint
  • context.py — clarifying comment on all_apm_deps
  • Docsreference/package-types.md + package-authoring.md rewritten as "Metadata model" (independence)
  • commands.md skill-resource — install row purpose mirrors new help text
  • CHANGELOG.md(#946) appended; entry extended

Tests

  • 5494 passed (1 pre-existing failure on TestGenericHostSubdirectoryRoundTrip::test_build_download_ref_preserves_virtual_path unrelated)
  • Added: TestPackHybridDescriptionWarning (3), TestSkillIntegratorCopytreeSymlinkContainment (2), 2 independence tests in test_apm_package_models.py

Panel verdict (round 3 pre-push)

APPROVED-WITH-NITS from apm-review-panel. Non-blocking follow-ups:

  1. (security) Add a behavioural integration test driving copy_skill_to_target with a real symlink
  2. (architecture) Hoist inline ignore_symlinks import in skill_integrator.py to module level
  3. (UX low-priority) Tighten apm view HYBRID hint for narrow terminals

These are post-merge follow-ups (will file issues if approved).

@danielmeppiel danielmeppiel added panel-review Trigger the apm-review-panel gh-aw workflow and removed panel-review Trigger the apm-review-panel gh-aw workflow labels Apr 26, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict

Disposition: REQUEST_CHANGES (two small surgical pre-merge fixes required; all security and architectural findings are positive)


Per-persona findings

Python Architect: The PR correctly extends the existing Strategy + type-dispatch pattern (PackageType enum -> validate_apm_package dispatcher) with a new _validate_hybrid_package function that follows the same shape as _validate_apm_package_with_yml, _validate_claude_skill, and _validate_marketplace_plugin. The back-compat fallback (delegate to _validate_apm_package_with_yml when .apm/ is present) is architecturally sound. InstallContext.direct_dep_failed follows the existing dataclass flag propagation pattern used by installed_count and similar counters.

classDiagram
    direction LR

    class PackageType {
        <<Enumeration>>
        APM_PACKAGE
        CLAUDE_SKILL
        HYBRID
        MARKETPLACE_PLUGIN
        INVALID
    }

    class ValidationResult {
        <<ValueObject>>
        +package_type PackageType
        +package APMPackage
        +errors List
        +add_error(msg)
        +add_warning(msg)
    }

    class validate_apm_package {
        <<Dispatcher>>
        routes_by_PackageType()
    }

    class _validate_hybrid_package {
        back_compat_apm_dir_fallback()
        skill_bundle_no_apm_path()
    }

    class _validate_apm_package_with_yml {
    }

    class _validate_claude_skill {
    }

    class PackageValidator {
        validate(package_path)
    }

    class SkillIntegrator {
        copy_skill_to_target()
        integrate_package_skills()
        sync_integration()
    }

    class BaseIntegrator {
        <<Abstract>>
    }

    class InstallContext {
        all_apm_deps List
        direct_dep_failed bool
    }

    validate_apm_package ..> _validate_hybrid_package : HYBRID
    validate_apm_package ..> _validate_apm_package_with_yml : APM_PACKAGE
    validate_apm_package ..> _validate_claude_skill : CLAUDE_SKILL
    _validate_hybrid_package ..> _validate_apm_package_with_yml : delegates when .apm/ exists
    PackageValidator ..> validate_apm_package : calls
    SkillIntegrator --|> BaseIntegrator
    ValidationResult o-- PackageType
Loading
flowchart TD
    A[apm install pkg] --> B[resolve phase]
    B --> C[integrate phase]
    C --> D[build direct_dep_keys from ctx.all_apm_deps]
    D --> E{for each dep}
    E --> F[run_integration_template]
    F --> G{deltas is None?}
    G -- No --> H[accumulate counters]
    H --> E
    G -- Yes, direct dep --> I[diagnostics.error OR logger.error]
    I --> J[ctx.direct_dep_failed = True]
    J --> E
    E -- done --> K{ctx.direct_dep_failed?}
    K -- Yes --> L[diagnostics.render_summary]
    L --> M[raise RuntimeError -- exits non-zero]
    K -- No --> N[write lockfile, update gitignore]
Loading

Design patterns:

  • Strategy + type-dispatch: detect_package_type() -> validate_apm_package() dispatcher. HYBRID is a clean addition to this chain.
  • Collect-then-render: ctx.diagnostics.error() collects; render_summary() emits once in pipeline. No double-output.
  • Dataclass flag propagation: direct_dep_failed follows the installed_count / counter pattern on InstallContext.

Minor issues: (1) builtins.set(...) at integrate.py:306 -- idiomatic Python uses set(...) directly; if set is shadowed, document why. (2) raise RuntimeError(...) in pipeline.py should use a domain exception (e.g. InstallError) for clean catch-site discrimination -- this is an existing pattern gap, amplified here.


CLI Logging Expert: The direct-dep failure path in integrate.py correctly follows collect-then-render: ctx.diagnostics.error(dep_key, detail="Resolved at {install_path}. Run with --verbose for details.") collects inline; render_summary() in pipeline.py renders once. The elif ctx.logger: fallback is correct defensive coding. Pack-time warning via logger.warning(...) is the right severity and channel.

Two output regressions flagged:

  1. deps/_utils.py description annotation uses "-- (set 'description' in apm.yml; ...)" as a prefix. The -- prefix is inconsistent with STATUS_SYMBOLS. Should use "[!] set 'description' in apm.yml; SKILL.md description is for agent runtime" to stay on-convention.
  2. install command help text is now 175 chars. Click wraps it to 3+ lines on an 80-col terminal, producing cluttered apm --help output. See Required Actions.

DevX UX Expert: Error message improvements are textbook good UX. "Missing required directory: .apm/ -- an APM package with apm.yml needs a .apm/ directory... Alternatively, add a SKILL.md to make this a skill bundle or hybrid package." names the problem, explains the constraint, gives the next action -- exactly right.

first-package.md updated -- funnel docs in sync. Pack-time warning at the correct publish gate. HYBRID apm deps list annotation is useful for authors but uses the wrong prefix (see CLI Logging Expert).

Blocking: The install command help text is a UX regression. npm/pip/cargo fit install help in ~50 chars. Suggested replacement (see Required Actions).


Supply Chain Security Expert: This PR resolves a known symlink containment gap. All 3 shutil.copytree call sites in skill_integrator.py (lines ~330, ~568, ~818) now pass ignore=ignore_symlinks. The site-3 closure correctly unions ignore_symlinks with shutil.ignore_patterns('.apm') via set union -- no symlink can escape either gate. The .apm/ filter also prevents the author's primitive layout from leaking into the deployed skill tree.

Path containment for the new HYBRID path: package_path is derived from apm_modules_dir / dep_key; dep_key goes through validate_path_segments upstream -- protected. The _validate_hybrid_package SKILL.md parse wraps frontmatter.load() in except Exception with a warning; python-frontmatter uses safe YAML -- acceptable risk.

Fail-loud raises RuntimeError before lockfile write and gitignore update. Fail-closed behavior confirmed. No security regressions found.


Auth Expert: Not activated -- PR changes packer.py, models/validation.py, deps/package_validator.py, skill_integrator.py, install/pipeline.py, install/phases/integrate.py, install/context.py, and commands/deps/_utils.py. None of the fast-path auth files match; no auth behavior, token management, credential resolution, or host classification is touched.


OSS Growth Hacker: HYBRID + agentskills.io support is APM's strongest 2026 growth lever. gh skill (launched gh v2.90.0, agentskills.io spec) bundles can now be installed via APM without restructuring -- enabling the "universal agent package manager" story. The "Alternatively, add a SKILL.md..." error message cross-sells HYBRID at the moment of failure, which is high-conversion discovery. Pack-time description warning improves marketplace listing quality, which improves INSTALL conversion from apm search.

Side-channel to CEO: consider whether HYBRID support warrants a v0.x minor bump and a dedicated announcement to ride the agentskills.io + gh skill external tailwind.


CEO arbitration

Specialists are in strong agreement: the architecture is correct, the security hardening is a genuine fix of a known gap, and the HYBRID + agentskills.io support is strategically sound. The only disagreements are on surface-level output issues -- both CLI Logging Expert and DevX UX Expert independently flag the install help text length and the [!] symbol gap, and I ratify both as required pre-merge fixes. The security and architecture are APPROVED as-is. The HYBRID + agentskills.io direction is ratified as a major growth vector; the agentskills.io compatibility story compounds with the gh skill launch and should be highlighted in the release narrative. The fail-loud direct-dep behavior is correct -- fail-closed before lockfile write is the right default.


Required actions before merge

  1. Shorten install command help text (src/apm_cli/commands/install.py:915). Current text is 175 chars and wraps badly on 80-col terminals. Suggested replacement:

    help="Install APM packages, Claude skills, and plugin collections (auto-creates apm.yml)"

    The --allow-insecure flag has its own help text; it does not need to appear in the command one-liner.

  2. Fix description annotation prefix (src/apm_cli/commands/deps/_utils.py, the elif is_hybrid: branch). Replace "-- (set 'description' in apm.yml; SKILL.md description is for agent runtime)" with "[!] set 'description' in apm.yml; SKILL.md description is for agent runtime" to stay consistent with STATUS_SYMBOLS conventions.


Optional follow-ups

  • Replace raise RuntimeError("One or more direct dependencies failed validation...") in src/apm_cli/install/pipeline.py with a domain-specific InstallError (or existing APM exception type) for clean catch-site discrimination in callers.
  • Replace builtins.set(...) with set(...) at src/apm_cli/install/phases/integrate.py:306 unless set is actively shadowed in that scope (if so, add an inline comment explaining why).
  • Evaluate whether HYBRID package support should be promoted from experimental to stable in docs/src/content/docs/reference/experimental.md, given that agentskills.io and gh skill v2.90.0 are now public.
  • Consider a v0.x minor-version bump and dedicated "What's New" post for the HYBRID + agentskills.io story to ride the external tailwind from the gh skill launch.

Generated by PR Review Panel for issue #946 · ● 646.7K ·

@danielmeppiel
Copy link
Copy Markdown
Collaborator Author

@copilot fix the identified regressions as per last comment

…r warning, fail-loud test, CHANGELOG format

Agent-Logs-Url: https://github.com/microsoft/apm/sessions/72a23a26-82de-47ba-a39e-3d4f2aa44094

Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 26, 2026

@copilot fix the identified regressions as per last comment

Fixed in ee1f06f. Four regressions from the review addressed:

  1. DirectDependencyError -- new typed exception in errors.py bypasses the broad except Exception wrapper in pipeline.py (same re-raise pattern as PolicyViolationError). commands/install.py catches it cleanly. No more triple-wrapped "Error installing dependencies: Failed to resolve APM dependencies: One or more..." messages.

  2. package_validator.py warning -- primitive scan now gated behind apm_dir.exists() so HYBRID/CLAUDE_SKILL packages skip the misleading "No primitive files found in .apm/ directory" warning.

  3. Fail-loud test -- 4 new tests in tests/unit/install/test_direct_dep_failure.py: integrate sets flag, diagnostics pushed, pipeline raises DirectDependencyError (not generic RuntimeError), transitive failures don't trigger.

  4. CHANGELOG -- single bullet split into 5 concise per-concern bullets, each ending with (#946).

5617 passed, 1 deselected (pre-existing), 0 failures.

@danielmeppiel danielmeppiel merged commit bea28f4 into main Apr 26, 2026
9 checks passed
@danielmeppiel danielmeppiel deleted the fix/hybrid-skill-bundle branch April 26, 2026 10:25
@danielmeppiel danielmeppiel mentioned this pull request Apr 26, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

panel-review Trigger the apm-review-panel gh-aw workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants