Skip to content

feat(skills): support package namespaces for skills#1028

Open
shreejaykurhade wants to merge 16 commits intomicrosoft:mainfrom
shreejaykurhade:feature/support-package-namespaces
Open

feat(skills): support package namespaces for skills#1028
shreejaykurhade wants to merge 16 commits intomicrosoft:mainfrom
shreejaykurhade:feature/support-package-namespaces

Conversation

@shreejaykurhade
Copy link
Copy Markdown
Contributor

@shreejaykurhade shreejaykurhade commented Apr 29, 2026

Add an optional namespace field to apm.yml, deploy package-owned skills and promoted sub-skills under skills/<namespace>/<skill-name>, and persist the namespace in apm.lock.yaml.

Also document the namespace schema and add coverage for validation, namespaced deployment, lockfile round-tripping, and lockfile assembly.

Description

This PR implements opt-in package namespaces for skill deployment.

Today, installed skills from project packages, direct dependencies, and transitive dependencies are flattened into the same skills/ directory. That makes it harder to identify where a skill came from and easier to accidentally edit dependency-owned skills while working in a project.

With this change, package authors can declare a safe single-segment namespace in apm.yml. When present, APM deploys native package skills and promoted .apm/skills/ sub-skills under:

skills/<namespace>/<skill-name>

Packages without namespace keep the existing flat layout, so current packages remain backward compatible.

Fixes #739

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance / refactor

Testing

  • Tested locally
  • All existing tests pass
  • Added tests for new functionality (if applicable)

Local checks run:

python -m pytest tests\test_apm_package_models.py
python -m pytest tests\unit\integration\test_skill_integrator.py tests\test_lockfile.py
python -m pytest tests\unit\test_drift_detection.py tests\unit\test_install_update.py -k "build_download_ref or drift"
python -m pytest tests\unit\test_artifactory_support.py -k "download_ref or registry_prefix"

Add an optional namespace field to apm.yml, deploy package-owned
skills and promoted sub-skills under skills/<namespace>/<skill-name>,
and persist the namespace in apm.lock.yaml.

Also document the namespace schema and add coverage for validation,
namespaced deployment, lockfile round-tripping, and lockfile assembly.
…ge-namespaces

# Conflicts:
#	src/apm_cli/install/phases/lockfile.py
#	src/apm_cli/integration/skill_integrator.py
#	src/apm_cli/models/apm_package.py
Copilot AI review requested due to automatic review settings April 29, 2026 01: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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@shreejaykurhade
Copy link
Copy Markdown
Contributor Author

@danielmeppiel @sergio-sisternes-epam please review.

@shreejaykurhade shreejaykurhade changed the title feat: support package namespaces for skills feat(skills): support package namespaces for skills Apr 29, 2026
Copilot and others added 3 commits April 30, 2026 17:37
Resolve conflicts: keep namespace fields and modern PEP 604 union syntax;
align skill_integrator collision tracking with namespace-aware owner keys.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
_build_ownership_maps now keys by skill identity (path under skills/),
not just leaf SKILL.md, so the test fixture's expected key changes from
'SKILL.md' to 'remote-skill/SKILL.md'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Eight required findings across four active panelists, all sharing a single root cause: the feature is complete at the code level and invisible at every user-facing surface. The code underneath is sound; the feature needs its communication layer to match its ambition.

Required before merge (8 items)

  • [cli-logging-expert] Namespace routing silently changes skill install path with no user-visible output at src/apm_cli/integration/skill_integrator.py:163

    • Why: When a package declares namespace: my-ns, skills deploy to skills/my-ns/skill-name/ with no distinct log message. Users cannot tell from CLI output why a skill landed in a different path. Namespace routing is a UX-observable behavior change that fails the "So What?" test in reverse -- the user can't tell why the skill landed where it did.
    • Suggested fix: In the skill deploy loop, emit a logger.verbose_detail() when namespace is not None, e.g. f'Skill deployed under namespace "{namespace}": skills/{namespace}/{skill_name}/' (verbose mode only).
  • [cli-logging-expert] Namespace not surfaced in install summary / tree output at src/apm_cli/integration/skill_integrator.py

    • Why: The IntegrationResult.path for namespaced skills is skills/ns/skill-name but callers may strip the namespace segment when building tree labels, showing only skill-name with no hint of namespace. No changes were made to call sites consuming IntegrationResult.path.
    • Suggested fix: Audit all call sites consuming IntegrationResult.path to confirm the full namespace/skill-name segment is preserved in the install tree.
  • [devx-ux-expert] Removal of .github/copilot-instructions.md generation is not documented as a breaking change at CHANGELOG.md

    • Why: The CHANGELOG [Unreleased] section has no mention of the removal. Users who relied on apm compile --target vscode generating .github/copilot-instructions.md will silently lose that output. Behavioral removals that change observable output must appear under BREAKING per the project's own conventions.
    • Suggested fix: Add BREAKING: apm compile --target vscode/all no longer generates .github/copilot-instructions.md. If your workflow consumed this generated file, copy it to your repo manually or manage it via a dedicated APM package.
  • [devx-ux-expert] apm install gives no user-visible signal that skills installed under a namespace path at src/apm_cli/integration/skill_integrator.py

    • Why: When namespace: acme is set, skills land at .github/skills/acme/<skill-name>/ instead of .github/skills/<skill-name>/. A developer who looks for .github/skills/<skill-name>/ will not find it and will have no CLI hint explaining why.
    • Suggested fix: Append the namespace segment to per-skill install confirmation messages, e.g. skill acme/brand-guidelines -> .github/skills/acme/brand-guidelines/.
  • [devx-ux-expert] namespace is absent from apm init generated templates, making it undiscoverable without reading docs at templates/hello-world/apm.yml

    • Why: The hello-world template has no commented-out namespace field. Users following the 5-minute apm init path will never see the field exists. By contrast, target: appears commented in templates. Namespace follows the same opt-in pattern and deserves the same discoverability treatment.
    • Suggested fix: Add commented block: # namespace: my-org # Optional: skills install under skills/my-org/<skill-name>/ (kebab-case, max 64 chars)
  • [supply-chain-security-expert] Deleted SSRF/shell-metachar tests without replacement at tests/unit/install/test_mcp_warnings.py

    • Why: tests/unit/install/test_mcp_warnings.py (308 lines) covering F5 SSRF and F7 shell-metacharacter safety warnings was deleted. The production module src/apm_cli/install/mcp/warnings.py still exists with those guards. Removing the only tests for SSRF host classification and shell-metachar detection on the MCP trust surface is a hard security regression independent of the namespace feature.
    • Suggested fix: Restore the deleted test file, or if intentionally moved/reorganized, provide the replacement location in this PR.
  • [oss-growth-hacker] Namespace feature has zero CHANGELOG entry in [Unreleased] at CHANGELOG.md

    • Why: The [Unreleased] section contains no mention of namespace support. This is a net-new feature that enables enterprise multi-package installs without collision -- exactly the kind of release beat that drives adoption. Users who read changelogs to decide whether to upgrade will never know it exists.
    • Suggested fix: Add: **Package namespaces.** Declare \namespace: acme` in `apm.yml` to install skills under `skills/acme/(name)/` -- ship multiple packages from the same org without name collisions. (feat(skills): support package namespaces for skills #1028)`
  • [oss-growth-hacker] Removal of copilot-instructions.md generation from AgentsCompiler is a breaking change with no CHANGELOG entry and no migration guidance at CHANGELOG.md

    • Why: Users who relied on apm compile to produce .github/copilot-instructions.md will silently stop getting it after upgrade. No CHANGELOG entry, no migration.md section, no deprecation warning. migration.md still lists copilot-instructions.md as something APM leaves untouched -- which is now misleading since APM used to write it.
    • Suggested fix: Add a BREAKING entry under [Unreleased] and add a migration.md row for users upgrading from copilot-instructions generation.

Nits (11 items, skip if you want)

  • [python-architect] validate_path_segments is called before the regex in namespace validation, making it effectively dead code -- the regex is strictly more restrictive and excludes . and .. via character class. Reorder: run slash-check and regex first. (src/apm_cli/models/apm_package.py:225)
  • [python-architect] The consecutive-hyphen check if "--" in namespace could be eliminated by upgrading the regex to use a negative lookahead: ^[a-z0-9]([a-z0-9]|-(?!-))*[a-z0-9]$|^[a-z0-9]$. (src/apm_cli/models/apm_package.py:239)
  • [python-architect] Namespace propagation snippet is identically repeated in all three DependencySource subclasses (LocalDependencySource, CachedDependencySource, FreshDependencySource). Three call sites is the codebase's own abstraction threshold -- extract a _attach_package_metadata(ctx, dep_key, package_info) helper. (src/apm_cli/install/sources.py)
  • [cli-logging-expert] minimal -> vscode effective_target fix may cause users to see vscode description string in progress output when they started with minimal. Consider printing the raw detected target alongside the effective target in verbose mode. (src/apm_cli/core/target_detection.py:213)
  • [cli-logging-expert] Note: copilot_root_instructions_* stat keys were removed cleanly -- grep confirms zero orphaned references in renderers or summary formatters. No action needed.
  • [cli-logging-expert] Note: No raw print() or stdlib logging calls were introduced for namespace paths -- all new warning paths use the established chain. Clean.
  • [devx-ux-expert] skills.md Package Namespaces section has no before/after directory tree example. Every other section in skills.md uses visual tree comparisons; add one here. (docs/src/content/docs/guides/skills.md:325)
  • [devx-ux-expert] No "when to use namespace" heuristic in docs. Cargo crate namespaces and npm scoped packages both provide well-known when-to-use guidance. Add a short callout. (docs/src/content/docs/guides/skills.md:339)
  • [supply-chain-security-expert] skill_integrator.py calls shutil.rmtree(target_skill_dir) directly at line 926 instead of via safe_rmtree(). Validated namespace makes traversal infeasible today, but the design rule is that all deletion derived from user-controlled input must flow through the cleanup chokepoint. (src/apm_cli/integration/skill_integrator.py:926)
  • [oss-growth-hacker] Namespace docs tell "what" but not "why" -- no collision story. Add one motivating sentence before the YAML snippet in skills.md. (docs/src/content/docs/guides/skills.md:327)
  • [oss-growth-hacker] No cross-link from manifest-schema.md namespace entry to skills.md namespace guide. Add a "See also" pointer. (docs/src/content/docs/reference/manifest-schema.md:162)

CEO arbitration

This PR ships a genuine enterprise capability -- package namespaces -- but ships it half-dressed. Eight required findings across four active panelists share a single root cause: the feature is complete at the code level and invisible at every user-facing surface. The CHANGELOG [Unreleased] section has no entry for namespace support and no BREAKING entry for the removal of .github/copilot-instructions.md generation. Those are not documentation nits; they are trust failures. Users who read changelogs to decide whether to upgrade will not learn the feature exists, and users who built workflows on apm compile will silently lose a generated artifact with no migration line. Per operating principle 1: breaking changes are allowed; silent breaking changes are not.

The second cluster of required findings is the in-session visibility gap. When a skill installs under skills/acme/brand-guidelines/ instead of the expected skills/brand-guidelines/, the CLI emits no signal distinguishing the two paths. cli-logging-expert and devx-ux-expert surfaced this from different angles -- logging fidelity and developer mental model -- and both are correct. The supply-chain panelist adds a test-coverage regression: the only test file covering SSRF host classification and shell-metacharacter safety on the MCP trust surface was deleted without replacement. That is a hard security regression independent of the namespace feature and must be resolved before merge.

The path to APPROVE is well-defined: (1) add CHANGELOG entries for namespace support and the copilot-instructions.md removal with a migration line, (2) emit a per-skill log line in verbose mode when namespace routing changes the install path, (3) audit IntegrationResult.path consumers to confirm the full namespace segment is preserved in tree output, (4) restore or replace test_mcp_warnings.py, and (5) add a commented namespace field to the hello-world template. None of these require architectural rework. The code underneath is sound.

Dissent resolved: cli-logging-expert (REQUIRED: silent namespace path change) and devx-ux-expert (REQUIRED: no user-visible signal for namespaced installs) are not in conflict -- they are the same finding from logging and UX frames respectively. Both are upheld. A single verbose-mode log line per skill and an audit of tree-output consumers satisfies both panelists. All four active panelists converged on the CHANGELOG gap independently, which strengthens confidence in that finding.

Growth/positioning note: Namespace support is the "package manager for teams" unlock that lets an org publish acme-security, acme-brand, and acme-devex as separate versioned packages without path collisions -- the enterprise multi-package story APM has needed. Fix the CHANGELOG and migration note in this PR, then lead the release post with the namespace story. The two changes together -- new capability plus clean breaking-change communication -- are the kind of release beat that converts early adopters into champions.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR

    class APMPackage {
        <<ValueObject>>
        +name str
        +version str
        +namespace str | None
        +from_apm_yml(path) APMPackage
    }

    class LockedDependency {
        <<ValueObject>>
        +repo_url str
        +namespace str | None
        +deployed_files list~str~
        +to_dict() dict
        +from_dict(d) LockedDependency
    }

    class InstallContext {
        <<ValueObject>>
        +package_namespaces dict~str,str~
        +package_types dict~str,str~
        +package_hashes dict~str,str~
    }

    class DependencySource {
        <<Strategy>>
        +materialize(dep_ref, ctx) Materialization
    }

    class LocalDependencySource {
        <<ConcreteStrategy>>
        +materialize(dep_ref, ctx) Materialization
    }

    class CachedDependencySource {
        <<ConcreteStrategy>>
        +materialize(dep_ref, ctx) Materialization
    }

    class FreshDependencySource {
        <<ConcreteStrategy>>
        +materialize(dep_ref, ctx) Materialization
    }

    class LockfileBuilder {
        +build(lockfile) LockFile
        +_attach_package_namespaces(lockfile)
        +_attach_package_types(lockfile)
    }

    class SkillIntegrator {
        <<BaseIntegrator>>
        +copy_skill_to_target(package_info, ...)
        +_promote_sub_skills(sub_skills_dir, ..., namespace)
        +_build_ownership_maps(project_root)
    }

    DependencySource <|-- LocalDependencySource
    DependencySource <|-- CachedDependencySource
    DependencySource <|-- FreshDependencySource
    LocalDependencySource ..> InstallContext : writes package_namespaces
    CachedDependencySource ..> InstallContext : writes package_namespaces
    FreshDependencySource ..> InstallContext : writes package_namespaces
    LockfileBuilder ..> InstallContext : reads package_namespaces
    LockfileBuilder ..> LockedDependency : sets namespace
    SkillIntegrator ..> APMPackage : reads namespace

    class APMPackage:::touched
    class LockedDependency:::touched
    class InstallContext:::touched
    class LocalDependencySource:::touched
    class CachedDependencySource:::touched
    class FreshDependencySource:::touched
    class LockfileBuilder:::touched
    class SkillIntegrator:::touched

    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A([apm install]) --> B[DependencySource.materialize\ninstall/sources.py]
    B --> B1["[FS] read apm.yml via APMPackage.from_apm_yml\nmodels/apm_package.py"]
    B1 --> B2{namespace field\nin apm.yml?}
    B2 -- yes --> B3[validate_path_segments + slash/regex checks\nmodels/apm_package.py:225-240]
    B2 -- no --> B4[APMPackage.namespace = None]
    B3 --> B5["[CTX] ctx.package_namespaces[dep_key] = namespace\ninstall/sources.py:226"]
    B4 --> C
    B5 --> C[LockfileBuilder.build\ninstall/phases/lockfile.py]
    C --> C1[_attach_package_namespaces\ninstall/phases/lockfile.py:131]
    C1 --> C2["[LOCK] LockedDependency.namespace = namespace\ndeps/lockfile.py:37"]
    C2 --> D[SkillIntegrator.copy_skill_to_target\nintegration/skill_integrator.py:372]
    D --> D1["_package_namespace(package_info)\nskill_integrator.py:157"]
    D1 --> D2{namespace?}
    D2 -- yes --> D3["[FS] skill_dir = skills/ns/skill-name/\nskill_integrator.py:163"]
    D2 -- no --> D4["[FS] skill_dir = skills/skill-name/\nskill_integrator.py:167"]
    D3 --> D5["shutil.copytree to namespaced dir"]
    D4 --> D5
    D5 --> E([collision check via _build_ownership_maps])
Loading

Design patterns

  • Used in this PR: Value Object (Dataclass) -- APMPackage.namespace and LockedDependency.namespace are immutable data fields flowing through the pipeline without identity semantics.
  • Used in this PR: Pure Functions -- _package_namespace, _skill_dir, _skill_owner_key, _skill_owner_key_from_deployed_path are side-effect-free helpers; trivially testable.
  • Used in this PR: Strategy (pre-existing, extended) -- LocalDependencySource / CachedDependencySource / FreshDependencySource each repeat the namespace-propagation snippet, following the pre-existing package_types pattern.
  • Pragmatic suggestion: Extract a _attach_package_metadata(ctx, dep_key, package_info) helper shared by all three DependencySource subclasses -- three call sites share exactly this logic, which is the codebase's own threshold for abstraction. Single change point when a fourth context attribute is added.

No required findings.

CLI Logging Expert

Required findings are in the top section.

Nits:

  • copilot_root_instructions_* stat keys removed cleanly -- grep confirms zero orphaned references. No action needed.
  • minimal -> vscode effective_target fix may cause users to see vscode description string when they started with minimal. Consider printing raw detected target alongside effective target in verbose mode. (src/apm_cli/core/target_detection.py:213)
  • No raw print() or stdlib logging calls introduced for namespace paths. All new warning paths use the established chain. Clean.

DevX UX Expert

Required findings are in the top section.

Nits:

  • skills.md Package Namespaces section has no before/after directory tree. Every other section uses visual trees; add one here. (docs/src/content/docs/guides/skills.md:325)
  • No "when to use namespace" heuristic. Add a short callout before the YAML example. (docs/src/content/docs/guides/skills.md:339)

Supply Chain Security Expert

Required findings are in the top section.

Nits:

  • skill_integrator.py:926 calls shutil.rmtree(target_skill_dir) directly; the design rule is that all deletion derived from user-controlled input must flow through safe_rmtree(). Validated namespace makes traversal infeasible today, but hardening this now prevents future regressions.

Auth Expert

Inactive -- No fast-path auth files are touched; drift.py refactors lockfile host-restoration guards (not AuthResolver token resolution).

OSS Growth Hacker

Required findings are in the top section.

Nits:

  • Namespace docs tell "what" but not "why" -- no collision story. Add one motivating sentence before the YAML snippet. (docs/src/content/docs/guides/skills.md:327)
  • No cross-link from manifest-schema.md namespace entry to skills.md namespace guide. Add a "See also" pointer. (docs/src/content/docs/reference/manifest-schema.md:162)

Verdict computed deterministically: 8 required findings across 5 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #1028 · ● 4.3M ·

@github-actions github-actions Bot added panel-rejected Apm-review-panel verdict: REJECT. Removed automatically on next push. and removed panel-review Trigger the apm-review-panel gh-aw workflow labels Apr 30, 2026
…crosoft#1028 round 2)

Folds 8 panel-required findings from the round-2 verdict. Code beneath
namespace routing was sound but the feature was invisible at every CLI
and docs surface. This commit closes that visibility gap.

- CHANGELOG [Unreleased]: add namespace feature entry and BREAKING
  entry for the removal of .github/copilot-instructions.md generation
  (findings 3, 7, 8).
- skill_integrator: emit verbose_detail per skill when namespace
  routing changes the deploy path (finding 1).
- services (install summary): preserve the namespace segment in the
  tree-output label and add per-skill confirmation 'skill ns/name
  integrated -> .github/skills/ns/' so namespaced installs are visible
  without --verbose (findings 2, 4).
- templates/hello-world/apm.yml: commented namespace field for
  discoverability via 'apm init' (finding 5).
- tests/unit/install/test_mcp_warnings.py: restored from origin/main;
  the file was deleted on this branch with no replacement, regressing
  SSRF and shell-metachar coverage on the MCP trust surface (finding 6).
- migration.md: note that APM no longer writes
  .github/copilot-instructions.md so the 'leaves untouched' contract
  remains accurate (finding 8).
- packages/apm-guide/.apm/skills/apm-usage/package-authoring.md:
  document the namespace field and the user-visible install signals
  (Rule 4 alignment).

Regression tests:
- verbose log surfaces namespace and is silent when absent
- IntegrationResult.target_paths preserves the namespace segment
- install tree label preserves namespace and per-skill ns/name label

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Path-containment gap and two logging-consistency issues block merge; the namespace feature itself is architecturally sound and strategically valuable.

Required before merge (3 items)

  • [cli-logging-expert] Inconsistent capitalisation between namespaced and non-namespaced integration messages at src/apm_cli/install/services.py:319

    • Why: The non-namespaced path emits |-- Skill integrated -> ... (capital S) while the namespaced path emits |-- skill acme/name integrated -> ... (lowercase s). Same event, inconsistent casing makes the output look like two different message types and breaks the "Name the thing" rule uniformly.
    • Suggested fix: Normalise to capital-S to match the existing non-namespaced message: |-- Skill {_pkg_namespace}/{_skill_name} integrated -> {_skill_target_str}
  • [cli-logging-expert] Sub-skills message hides the namespace in a parenthetical annotation instead of leading with it at src/apm_cli/install/services.py:332

    • Why: The message |-- 3 skill(s) integrated -> skills/acme/ (namespace: acme) repeats "acme" twice and buries what changed in a parenthetical. Violates "Lead with the outcome" and signal-to-noise. The path already contains the namespace; the (namespace: ...) suffix is pure noise.
    • Suggested fix: Drop the _ns_suffix entirely. The target path already encodes the namespace. Message becomes |-- 3 skill(s) integrated -> skills/acme/
  • [supply-chain-security-expert] _skill_dir constructs paths with / namespace / skill_name without a runtime ensure_path_within containment check at src/apm_cli/integration/skill_integrator.py:163

    • Why: Parse-time regex validation (^[a-z0-9]([a-z0-9-]*[a-z0-9])?$) prevents traversal sequences in namespace values themselves, but does not defend against a symlink planted at skills/<namespace> by a prior malicious install -- shutil.copytree will follow that symlink and write outside target_skills_root. ensure_path_within calls Path.resolve() and catches this class of attack. _skill_dir is the only new path-join in the install surface not covered by the mandatory chokepoint.
    • Suggested fix: After computing the return value in _skill_dir, call ensure_path_within(path, target_skills_root) (already imported at line 1036) before returning it. Raise PathTraversalError on failure so callers fail closed.

Nits (12 items, skip if you want)

  • [python-architect] _package_namespace is a private helper leaking from integration/ into install/services.py via lazy import -- a @property namespace on PackageInfo would make the access typed, testable, and eliminate the cross-boundary private import.
  • [python-architect] _skill_owner_key_from_deployed_path fallback silently returns a leaf filename when /skills/ is absent -- returning None and letting callers skip would be safer.
  • [python-architect] Namespace validation in from_apm_yml is partially redundant -- the regex already prohibits /, \, uppercase, and --; the preceding validate_path_segments call and explicit separator checks overlap.
  • [cli-logging-expert] verbose_detail message in _integrate_native_skill hardcodes a relative path fragment (skills/namespace/name/) that could diverge from the actual target root used by the integrator (e.g. copilot-cowork).
  • [cli-logging-expert] Inline from apm_cli.integration.skill_integrator import _package_namespace inside a per-skill code path should be a top-level import to match project norms.
  • [devx-ux-expert] cli-commands.md compile section has no mention of the copilot-instructions.md removal -- a user consulting that doc directly gets no signal about the breaking change.
  • [devx-ux-expert] New --verbose namespace output behavior not reflected in the apm install section of cli-commands.md.
  • [devx-ux-expert] migration.md breaking-change note is buried as a parenthetical in existing prose rather than a dedicated callout block.
  • [supply-chain-security-expert] LockedDependency.namespace is deserialized from the lockfile with no re-validation against the namespace regex -- latent risk if new code ever uses the field for path operations.
  • [supply-chain-security-expert] Changing namespace in apm.yml is not detected as drift, so skills deployed under the old namespace are not cleaned up on reinstall.
  • [oss-growth-hacker] Namespace feature has no runnable 60-second proof -- a one-liner apm install output snippet showing skills/acme/my-skill/ in the CHANGELOG or docs section would close the proof gap.
  • [oss-growth-hacker] CHANGELOG breaking-change migration hint is prose-only with no copy-paste command -- a one-liner recovery command would reduce friction for existing users scanning CHANGELOG at upgrade time.

CEO arbitration

Three required findings block this PR across two domains: message-casing consistency (cli-logging-expert, 2 findings) and a path-containment gap (supply-chain-security-expert, 1 finding). There is no disagreement between panelists -- the findings are orthogonal and all five active reviewers landed without conflict. The security finding is the more urgent of the two: the namespaced _skill_dir path-join is the only new install-surface path construction not covered by ensure_path_within, and symlink-based traversal after a prior malicious install is a realistic attack vector for a package manager. This must be fixed before merge; no strategic override applies. The logging findings are mechanical but visible -- inconsistent casing on the same install event will generate user confusion and support noise at exactly the moment namespace adoption is peaking, so fixing them now is cheaper than a follow-up patch. The breaking compile change (no more copilot-instructions.md on apm compile --target vscode/all) is well-mitigated by CHANGELOG and migration.md, consistent with the "ship fast, communicate clearly" principle, and does not require additional blocking action.

Dissent resolved: No panelist disagreements to resolve. python-architect and cli-logging-expert both flagged the inline cross-boundary private import (_package_namespace) but both held it at nit -- it is a code-quality issue, not a correctness or security issue, and can be addressed in a follow-up refactor.

Growth/positioning note: Namespace is the org-level unlock APM has been missing -- the "ship 5 packages from one org without collisions" story is the adoption wedge for platform and devex teams. Recommend a short demo GIF of apm install producing a skills/acme/*/ tree as part of the release post, and a proactively pinned FAQ issue on the compile breaking change to absorb the expected support noise. The namespace feature deserves its own CHANGELOG headline section, not a bullet under general changes.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR

    class APMPackage {
        <<ValueObject>>
        +name str
        +version str
        +namespace str | None
        +from_apm_yml(path) APMPackage
    }

    class PackageInfo {
        <<ValueObject>>
        +package APMPackage
        +install_path Path
        +package_type PackageType
        +dependency_ref DependencyReference
    }

    class LockedDependency {
        <<ValueObject>>
        +repo_url str
        +namespace str | None
        +deployed_files list[str]
        +get_unique_key() str
    }

    class InstallContext {
        <<ValueObject>>
        +project_root Path
        +package_namespaces dict[str, str]
        +package_deployed_files dict[str, list[str]]
        +package_types dict[str, str]
    }

    class SkillIntegrator {
        <<BaseIntegrator>>
        -_native_skill_session_owners dict
        +integrate_package_skill(package_info, project_root) SkillIntegrationResult
        +_integrate_native_skill(package_info) SkillIntegrationResult
        +_promote_sub_skills(sub_skills_dir, target_skills_root, parent_name, namespace) tuple
        +_build_ownership_maps(project_root) tuple
    }

    class BaseIntegrator {
        <<Abstract>>
    }

    class LockfileBuilder {
        +build_and_save(ctx) None
        +_attach_package_namespaces(lockfile) None
    }

    class _namespace_helpers {
        <<Pure>>
        +_package_namespace(package_info) str | None
        +_skill_dir(root, skill_name, namespace) Path
        +_skill_owner_key(skill_name, namespace) str
        +_skill_owner_key_from_deployed_path(deployed_path) str
    }

    class DependencySource {
        <<Strategy>>
        +acquire(ctx) Materialization
    }
    class LocalDependencySource {
        <<ConcreteStrategy>>
        +acquire(ctx) Materialization
    }
    class CachedDependencySource {
        <<ConcreteStrategy>>
        +acquire(ctx) Materialization
    }
    class FreshDependencySource {
        <<ConcreteStrategy>>
        +acquire(ctx) Materialization
    }

    note for _namespace_helpers "All four helpers are module-level pure\nfunctions in skill_integrator.py --\nno state, no I/O, no side-effects"
    note for DependencySource "Strategy pattern: three concrete sources\nall write ctx.package_namespaces[dep_key]\nfrom package.namespace after acquire()"

    BaseIntegrator <|-- SkillIntegrator
    DependencySource <|-- LocalDependencySource
    DependencySource <|-- CachedDependencySource
    DependencySource <|-- FreshDependencySource
    PackageInfo *-- APMPackage
    SkillIntegrator ..> _namespace_helpers : calls
    SkillIntegrator ..> PackageInfo : reads
    LockfileBuilder ..> InstallContext : reads ctx.package_namespaces
    LockfileBuilder ..> LockedDependency : writes namespace
    LocalDependencySource ..> InstallContext : writes package_namespaces
    CachedDependencySource ..> InstallContext : writes package_namespaces
    FreshDependencySource ..> InstallContext : writes package_namespaces

    class APMPackage:::touched
    class LockedDependency:::touched
    class InstallContext:::touched
    class SkillIntegrator:::touched
    class LockfileBuilder:::touched
    class _namespace_helpers:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A([apm install]) --> B[sources.py\nLocalDependencySource.acquire / CachedDependencySource.acquire / FreshDependencySource.acquire]
    B --> C{"getattr(package_info.package,\n'namespace', None) truthy?"}
    C -- yes --> D["[I/O] ctx.package_namespaces[dep_key] = namespace\n(sources.py:227 / 383 / 554)"]
    C -- no --> E[skip namespace registration]
    D --> F[services.py\nintegrate_package_primitives]
    E --> F
    F --> G["lazy import _package_namespace from skill_integrator\n(services.py:239)"]
    G --> H[skill_integrator.py\n_package_namespace(package_info)\n-> getattr chain -> str | None]
    H --> I["SkillIntegrator.integrate_package_skill\n-> _integrate_native_skill OR _promote_sub_skills_standalone"]
    I --> J["_skill_dir(target_skills_root, skill_name, namespace)\n-> root/namespace/skill_name/ OR root/skill_name/"]
    J --> K{"[FS] target_skill_dir.exists()?"}
    K -- yes --> L["[FS] shutil.rmtree(target_skill_dir)\nafter collision/ownership check via _skill_owner_key"]
    K -- no --> M["[FS] target_skill_dir.parent.mkdir(parents=True)"]
    L --> M
    M --> N["[FS] shutil.copytree(package_path, target_skill_dir,\nignore=_ignore_symlinks_and_apm)"]
    N --> O["SkillIntegrationResult.target_paths.append(target_skill_dir)"]
    O --> P[phases/lockfile.py\nLockfileBuilder._attach_package_namespaces]
    P --> Q{dep_key in ctx.package_namespaces?}
    Q -- yes --> R["[LOCK] dep.namespace = ctx.package_namespaces[dep_key]"]
    Q -- no --> S[dep.namespace stays None]
    R --> T([apm.lock.yaml written with namespace field])
    S --> T
Loading

Design patterns

  • Used in this PR: Pure helper functions (_skill_dir, _skill_owner_key, _skill_owner_key_from_deployed_path, _package_namespace) -- all four are stateless, side-effect-free, and testable in isolation.
  • Used in this PR: Value Object (Dataclass-as-value-object) -- APMPackage.namespace, LockedDependency.namespace, InstallContext.package_namespaces carry the namespace field through the pipeline without mutation after construction.
  • Used in this PR: Strategy (existing, extended) -- all three DependencySource subclasses now uniformly write ctx.package_namespaces[dep_key] after acquire(), keeping namespace propagation symmetric across acquisition paths.
  • Pragmatic suggestion: expose namespace: str | None as a @property on PackageInfo (delegating to self.package.namespace) -- eliminates the cross-module import of the private _package_namespace helper in services.py and gives the accessor a typed home.

CLI Logging Expert

Required:

  1. Inconsistent capitalisation between namespaced and non-namespaced integration messages (src/apm_cli/install/services.py:319) -- normalise to capital-S Skill in both paths.
  2. Sub-skills message (namespace: acme) parenthetical is redundant noise -- path already encodes the namespace; drop _ns_suffix entirely.

Nits:

  • verbose_detail hardcodes skills/namespace/name/ path fragment that could diverge from actual target root.
  • Inline from apm_cli.integration.skill_integrator import _package_namespace inside per-skill loop should be a top-level import.

DevX UX Expert

No findings.

Nits:

  • cli-commands.md compile section missing a breaking-change callout for the copilot-instructions.md removal.
  • cli-commands.md apm install --verbose section not updated for new namespace verbose output.
  • migration.md breaking-change note buried as a parenthetical; a dedicated callout block would be more discoverable.

Supply Chain Security Expert

Required:

  1. _skill_dir at src/apm_cli/integration/skill_integrator.py:163 joins target_skills_root / namespace / skill_name without calling ensure_path_within -- symlink-planted traversal is not caught. Fix: call ensure_path_within(path, target_skills_root) before returning and raise PathTraversalError on failure.

Nits:

  • LockedDependency.namespace deserialized from lockfile without re-validation against the namespace regex; latent risk for future path operations.
  • Namespace rename not detected as drift; stale skills under the old namespace path are not cleaned up automatically.

Auth Expert

Inactive -- No auth-fast-path files changed; PR introduces package namespace routing and compile behavior change, neither of which touches AuthResolver, token management, credential resolution, or host classification.

OSS Growth Hacker

No findings.

Nits:

  • Namespace feature has no runnable 60-second proof in docs or CHANGELOG -- an apm install output snippet showing skills/acme/my-skill/ would close the gap.
  • CHANGELOG breaking-change migration hint is prose-only with no copy-paste command.

Growth side-channel: Namespace is the org-level unlock APM has been missing. "Ship 5 packages from one org without collisions" is the platform/devex adoption wedge. Recommend a demo GIF + pinned FAQ issue for the compile breaking change.

Verdict computed deterministically: 3 required findings across 5 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #1028 · ● 3.4M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
Address all 3 required findings from PR microsoft#1028 round 3 review panel:

1. supply-chain-security: add runtime ensure_path_within containment
   guard inside _skill_dir (skill_integrator.py:163). Parse-time regex
   blocks traversal in namespace values, but a symlink planted at
   skills/<namespace> by a prior malicious install would otherwise let
   shutil.copytree write outside target_skills_root. Guard covers both
   namespaced and flat branches at the single chokepoint.

2. cli-logging: normalise capital-S in namespaced per-skill confirmation
   message (services.py:266) so namespaced and non-namespaced events
   share identical casing.

3. cli-logging: drop the redundant '(namespace: ...)' suffix from the
   sub-skills integrated message (services.py:272-274). The target path
   already encodes the namespace; the parenthetical was pure duplication.

Adds TestSkillDirContainmentGuard regression tests (3) exercising the
exact symlink-planted attack vector. Full unit suite (6904 tests) green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Three independent specialists converge on an unfinished namespace input contract; migration path for the compile breaking change is insufficient; CLI discoverability gaps block merge.

Required before merge (12 items)

  • [python-architect] No parse-time namespace validation in the manifest parser at src/apm_cli/integration/skill_integrator.py

    • Why: The namespace field flows from apm.yml into path construction (_skill_dir) without any regex enforcement at parse time. ensure_path_within is a last-resort containment check, not an input contract. A namespace like my..ns or ../evil could produce surprising behaviour before containment fires. APM's pattern is to reject bad manifests early.
    • Suggested fix: Add a manifest-level validator in the manifest dataclass or its from_dict/validate step that raises ManifestError when namespace doesn't match an allowed pattern. Mirror how other constrained string fields are validated today.
  • [python-architect] Leaking private symbol _package_namespace across module boundary via lazy import at src/apm_cli/install/services.py

    • Why: services.py does from apm_cli.integration.skill_integrator import _package_namespace inside a function body. The leading underscore signals module-private. Importing it from outside the module creates a hidden coupling: any rename/move silently breaks services.py at runtime, not import-time.
    • Suggested fix: Rename to package_namespace() (public), move top-level import to module header in services.py. Alternatively inline the two-level getattr at the single call site in services.py.
  • [python-architect] Three identical ctx.package_namespaces assignment patterns in sources.py should be abstracted at src/apm_cli/install/sources.py

    • Why: The pattern if getattr(x.package, 'namespace', None): ctx.package_namespaces[dep_key] = x.package.namespace appears verbatim at three independent call sites (local, cached, remote). Per the architecture rule 'abstract when 3+ call sites share the same logic pattern', this should be a helper.
    • Suggested fix: Extract a private helper _store_namespace(ctx, dep_key, package_info) that reads the namespace and writes to ctx.package_namespaces, then call it from all three acquire paths.
  • [cli-logging-expert] Log message does not lead with outcome; namespace/name identity buried after pipe decoration at src/apm_cli/install/services.py:265

    • Why: Rule 1 -- Lead with the outcome. The new message |-- Skill acme/brand-guidelines integrated -> .github/skills/acme/ buries the subject mid-sentence after the pipe decoration. The verb and subject must come first per the Newspaper test.
    • Suggested fix: Rewrite as |-- Integrated skill acme/brand-guidelines -> .github/skills/acme/ so the verb comes first, then the subject, then the destination.
  • [cli-logging-expert] --verbose per-skill namespace line fires from skill_integrator.py while services.py emits an unconditional message -- two layers, inconsistent formats at src/apm_cli/install/services.py:262

    • Why: Non-verbose output shows namespace identity from services.py, but the documented verbose-only detail fires separately from skill_integrator.py with a different format (acme/brand-guidelines vs "acme": skills/acme/brand-guidelines/). On --verbose, namespace appears twice in inconsistent formats. Progressive disclosure is broken.
    • Suggested fix: Gate services.py namespace label on non-verbose output only, OR remove the skill_integrator verbose_detail line and consolidate into services.py with proper verbose gating. Pick one authoritative source.
  • [devx-ux-expert] cli-commands.md not updated despite new namespace behavior surfacing in CLI output at docs/src/content/docs/reference/cli-commands.md

    • Why: Per the Key Rule: a CLI change not reflected in cli-commands.md is incomplete by definition. The namespace field changes apm install output (tree shows skill <namespace>/<name> integrated) and apm compile (breaking change to --target vscode/all). A user running apm install --help will see no mention of namespace or the breaking compile behavior.
    • Suggested fix: Update cli-commands.md to document: (1) namespace field effect on apm install output format, (2) the --verbose per-skill namespace line, (3) the breaking change to apm compile --target vscode/all.
  • [devx-ux-expert] Regex does not enforce documented constraints (max 64 chars, no consecutive hyphens) at docs/src/content/docs/reference/manifest-schema.md

    • Why: The docs state namespace must be max 64 characters and disallow consecutive --, but the regex ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ enforces neither. Users who read the docs expect rejection of a--b or a 65-char slug, but the validator silently accepts them. Lockfile and deployed paths will contain values the docs say are illegal.
    • Suggested fix: Either update the regex to ^[a-z0-9]([a-z0-9]|-(?!-))*[a-z0-9]$ and add a length check (max 64), or remove those constraints from the docs if not enforced. Surface a clear error: 'namespace "a--b" is invalid: consecutive hyphens are not allowed.'
  • [devx-ux-expert] Breaking change to apm compile --target vscode/all lacks a structured migration path in migration.md

    • Why: Users who relied on the generated .github/copilot-instructions.md in CI are given only 'copy your last generated version' -- no command, no example, no automation. Violates the Recovery lens: every breaking change must name what changed, why, and one concrete executable next action.
    • Suggested fix: Add a migration.md section with: (1) before/after example of what was generated, (2) a concrete shell command (git show HEAD~1:.github/copilot-instructions.md > .github/copilot-instructions.md), and (3) CI pipeline guidance.
  • [devx-ux-expert] apm init does not prompt for namespace and the field is not discoverable via apm init --help

    • Why: Namespace prevents skill-name collisions across packages. Org users who skip it will hit confusing collisions later with no indication they had a tool to prevent it. Breaks the First-run lens: a new user must reach success without reading more than the README quickstart.
    • Suggested fix: Add an optional --namespace flag to apm init (or an interactive prompt with a skippable default), and update cli-commands.md accordingly.
  • [supply-chain-security-expert] Lockfile from_dict deserializes namespace with no validation at src/apm_cli/deps/lockfile.py:150

    • Why: namespace=data.get('namespace') accepts any YAML value verbatim -- no type check, no regex, no length cap. If a lockfile is tampered, the namespace field could be an arbitrary string. Even though path construction today flows from freshly-parsed validated manifests, the unvalidated LockedDependency.namespace value is a latent injection point for any future code reading it for path operations.
    • Suggested fix: After data.get('namespace'), validate the value is a str, check it matches ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$, and either set to None or raise on mismatch. This mirrors the validation already present in apm_package.py.
  • [oss-growth-hacker] Breaking change migration path is insufficient -- no worked example at CHANGELOG.md

    • Why: The CHANGELOG describes the breaking change correctly but sends users to 'copy your last generated version' without a code example for either path. Projects like bun and uv win community trust on breaking changes by providing an exact before/after snippet. Without one, this becomes a support ticket magnet.
    • Suggested fix: Add a fenced before/after block in CHANGELOG: 'Before: apm compile --target vscode/all wrote .github/copilot-instructions.md. After: that file is yours. To keep the old behavior: [concrete command]. See migration.md.'
  • [oss-growth-hacker] Namespace docs open with syntax, not the 'why' -- fails the 10-second story test at docs/src/content/docs/guides/skills.md

    • Why: The guides/skills.md addition jumps straight to YAML. A curious first-time reader has no idea why namespaces matter. The conversion story (multiple orgs, no collisions) is buried or absent. httpie and gh open feature docs with a one-sentence pain statement before showing code.
    • Suggested fix: Prepend a 2-sentence motivation before the YAML block: 'If you publish multiple APM packages from the same org, skills from different packages can overwrite each other in the flat layout. Namespaces isolate them: acme-tools and acme-infra each own their skill directory.'

Nits (16 items, skip if you want)

  • [python-architect] _skill_owner_key_from_deployed_path string parsing fallback silently produces a key that won't match _skill_owner_key output at src/apm_cli/integration/skill_integrator.py
  • [python-architect] Double getattr chain in _package_namespace is correct but visually noisy; consider a two-step guard or typed Protocol at src/apm_cli/integration/skill_integrator.py
  • [python-architect] drift.py getattr locals are safe but the motivation (backward compat with older lockfiles) is worth a one-line comment at src/apm_cli/drift.py
  • [python-architect] Verify _promote_sub_skills namespace: str | None = None parameter has full type annotation in the actual file at src/apm_cli/integration/skill_integrator.py
  • [cli-logging-expert] _skill_target_str change is semantically correct for namespaced paths; add an inline comment: # parent_rel already includes the skills/ segment (and namespace if set) at src/apm_cli/install/services.py:254
  • [cli-logging-expert] Fallback label "copilot-cowork/skills" hardcodes a path segment that may drift; extract to a named constant or derive from target metadata at src/apm_cli/install/services.py:248
  • [devx-ux-expert] Install tree output skill acme/my-skill integrated -> .github/skills/acme/ uses / as a separator that looks like a path; consider skill [acme] my-skill -> .github/skills/acme/my-skill/ to visually separate namespace from skill name
  • [devx-ux-expert] Adding or changing a namespace on an existing package is a breaking change to consumers that silently leaves orphaned flat-path directories; document this in migration.md and the namespace guide
  • [devx-ux-expert] No consecutive hyphens rule is not encoded in the schema Pattern row; either fix the regex or add a Constraints row listing max length and hyphen rules separately
  • [supply-chain-security-expert] _skill_dir docstring claims regex validation rejects traversal but the function performs no regex check itself; make the function self-contained with a validate_path_segments call at src/apm_cli/integration/skill_integrator.py:163
  • [supply-chain-security-expert] drift.py getattr refactor is neutral-to-strengthening (no security check bypassed); consider adding a comment explaining backward compat intent at src/apm_cli/drift.py
  • [supply-chain-security-expert] No cryptographic coverage of the namespace field in the lockfile; document that the lockfile must be committed and code-reviewed like source (long-term: add lockfile HMAC)
  • [oss-growth-hacker] CHANGELOG hook is accurate but not shareable; consider: 'Package namespaces: scope your skills like npm -- declare namespace: acme and your skills install under skills/acme/<name>/ so two packages from the same org never collide' at CHANGELOG.md
  • [oss-growth-hacker] No template updated to demonstrate namespace; add # namespace: my-org # optional: scope skills to avoid name collisions as a commented line in the default apm.yml template
  • [oss-growth-hacker] README not updated; namespace feature is invisible to new visitors; consider one bullet under Features
  • [oss-growth-hacker] Breaking change marker lacks visual prominence; move to a ### Breaking Changes section above ### Added at CHANGELOG.md

CEO arbitration

Three independent panelists -- python-architect, supply-chain-security-expert, and devx-ux-expert -- converge on a single root defect: the namespace field has no enforced validation contract. The regex in manifest-schema.md documents constraints (max 64 chars, no consecutive hyphens) that the actual regex ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ does not enforce; the lockfile deserializer accepts any YAML value verbatim; and the skill_integrator relies on ensure_path_within as a last-resort containment check rather than rejecting malformed input at parse time. These are not three separate issues -- they are one unfinished input contract manifesting at three layers. The fix must be atomic: a single validated namespace pattern, applied at manifest parse, lockfile deserialization, and documented in the schema.

Two panelists -- devx-ux-expert and oss-growth-hacker -- independently flag the migration path as insufficient. The CHANGELOG sends users to 'copy your last generated version' with no command, no before/after example, and no CI guidance. Projects that win community trust on breaking changes provide an exact shell one-liner. The cli-logging-expert's finding that namespace identity is buried mid-message and the oss-growth-hacker's finding that skills.md opens with YAML syntax rather than the 'why' are complementary legibility failures -- one in terminal output, one in documentation -- both indicating the namespace concept is not yet legible to a first-time user.

The devx-ux-expert raises two independent UX gaps: cli-commands.md is not updated to reflect namespace behavior, and apm init neither prompts for namespace nor exposes it via --help. Both are required under the Key Rule. The cli-logging-expert's progressive disclosure finding (namespace logged twice in inconsistent formats under --verbose) should be resolved in the same pass as the services.py log message rewrite.

Dissent resolved: No panelist-vs-panelist contradiction exists in this return set. The python-architect and supply-chain-security-expert findings on namespace validation are reinforcing, not competing. On the cli-logging question of whether to gate the services.py namespace label on non-verbose output or consolidate into services.py: the panel sides with consolidation into services.py with proper verbose gating, as it eliminates the dual-layer inconsistency rather than papering over it.

Growth/positioning note: The namespace feature is the first signal that APM is moving toward a multi-publisher ecosystem -- analogous to npm orgs or OCI namespaces for containers. This is a significant positioning moment: 'APM is where AI skill packages are published, versioned, and namespaced.' The current PR buries this. A short release post framing namespaces as 'APM now has package scopes' -- with a GIF or terminal recording of installing two same-named skills from different orgs without collision -- would convert a breaking change news cycle into a positioning win. The breaking compile change should get its own callout with a migration one-liner to absorb negative signal before it becomes support volume.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR
    class LockedDependency {
        +str name
        +str version
        +str namespace
        +list deployed_files
        +dict deployed_file_hashes
        +to_dict() dict
        +from_dict(data) LockedDependency
    }
    class InstallContext {
        <<dataclass>>
        +dict package_types
        +dict package_namespaces
        +dict package_deployed_files
        +set intended_dep_keys
    }
    class LockfileBuildPhase {
        <<Phase>>
        +ctx InstallContext
        +build_and_save() None
        -_attach_deployed_files(lockfile) None
        -_attach_package_types(lockfile) None
        -_attach_package_namespaces(lockfile) None
    }
    class SkillIntegratorHelpers {
        <<module-level functions>>
        +_package_namespace(package_info) str|None
        +_skill_dir(root, name, ns) Path
        +_skill_owner_key(name, ns) str
        +_skill_owner_key_from_deployed_path(path) str
    }
    class InstallSourcesAcquire {
        <<Strategy>>
        +acquire() Materialization|None
    }
    class LockFile {
        +dict dependencies
    }

    LockfileBuildPhase --> InstallContext : reads
    LockfileBuildPhase --> LockFile : mutates
    LockFile --> LockedDependency : contains
    InstallSourcesAcquire --> InstallContext : writes package_namespaces
    SkillIntegratorHelpers ..> InstallContext : indirectly via services.py

    class LockedDependency:::touched
    class InstallContext:::touched
    class LockfileBuildPhase:::touched
    class SkillIntegratorHelpers:::touched
    class InstallSourcesAcquire:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A([apm install invoked]) --> B[sources.py acquire\nLocalSource / CachedSource / RemoteSource]
    B -- IO: read apm.yml --> C{package.namespace set?}
    C -- yes --> D[ctx.package_namespaces dep_key = namespace\nsources.py:223,379,550]
    C -- no --> E[skip]
    D --> F[LockfileBuildPhase.build_and_save\ninstall/phases/lockfile.py]
    E --> F
    F --> G[_attach_package_namespaces\nlockfile.py]
    G -- LOCK: mutate lockfile --> H[LockedDependency.namespace = namespace\ndeps/lockfile.py]
    H --> I[FS: write apm.lock.yaml]
    F --> J[install/services.py: integrate_skill]
    J -- lazy import --> K[_package_namespace\nskill_integrator.py:154]
    K --> L{namespace truthy?}
    L -- yes --> M[_skill_dir\nskill_integrator.py:162\nensure_path_within check]
    L -- no --> N[legacy flat path\nskill_integrator.py]
    M -- FS: copytree --> O[skills/namespace/skill_name/]
    N -- FS: copytree --> P[skills/skill_name/]
    O --> Q[_skill_owner_key\nskill_integrator.py:175]
    Q --> R[update owned_by map\nFS: write .apm-manifest.json]
    J --> S[log: Skill ns/name integrated -> path/]
Loading

Design patterns

  • Used in this PR: Strategy -- InstallSourcesAcquire variants (local, cached, remote) each implement acquire(); namespace extraction added at all three strategy endpoints. Chain of Responsibility -- LockfileBuildPhase.attach* methods; _attach_package_namespaces correctly follows after _attach_package_types. Module-level pure helpers -- _skill_dir, _skill_owner_key, _package_namespace in skill_integrator.py; no state, no self, correct placement; _skill_dir doubles as a path-security guard.
  • Pragmatic suggestion: Extract _store_namespace(ctx, dep_key, package_info) helper in sources.py to collapse the three identical assignment patterns -- a net win at current scope.

Required findings:

  1. No parse-time namespace validation in the manifest parser
  2. Leaking private symbol _package_namespace across module boundary via lazy import
  3. Three identical ctx.package_namespaces assignment patterns in sources.py should be abstracted

Nits: See aggregated nits section above.

CLI Logging Expert

Required findings:

  1. Log message does not lead with outcome; identity buried after pipe decoration (services.py:265)
  2. --verbose per-skill namespace line fires from two layers with inconsistent formats; progressive disclosure broken (services.py:262)

Nits:

  • _skill_target_str change is semantically correct for namespaced paths; add inline comment explaining parent_rel already includes skills/ segment
  • _log_integration delegates to logger.tree_item (CommandLogger) -- correct, no anti-pattern
  • Fallback label "copilot-cowork/skills" hardcodes a path segment; extract to constant

DevX UX Expert

Required findings:

  1. cli-commands.md not updated despite new namespace behavior surfacing in CLI output
  2. Regex does not enforce documented constraints (max 64 chars, no consecutive hyphens)
  3. Breaking change to apm compile --target vscode/all lacks structured migration path in migration.md
  4. apm init does not prompt for namespace and field is not discoverable via apm init --help

Nits:

  • Install tree output format may confuse users unfamiliar with namespace concept; consider [acme] visual separator
  • Backward compat story creates silent collision risk for mixed namespaced/flat installs; document in migration guide
  • No consecutive hyphens rule not encoded in schema Pattern row

Supply Chain Security Expert

Required findings:

  1. Lockfile from_dict deserializes namespace with no validation (lockfile.py:150) -- latent injection point for future code

Nits:

  • _skill_dir docstring claims regex validation rejects traversal but function performs no regex check itself
  • drift.py getattr refactor is neutral-to-strengthening (confirmed: no security check bypassed)
  • Namespace determinism satisfied but no cryptographic coverage of namespace field in lockfile

Auth Expert

Inactive -- No auth-sensitive files changed; drift.py refactor uses getattr for registry_prefix/host/resolved_ref but does not alter token management, credential resolution, or AuthResolver behavior.

OSS Growth Hacker

Required findings:

  1. Breaking change migration path is insufficient -- no worked example (CHANGELOG.md)
  2. Namespace docs open with syntax, not the 'why' -- fails the 10-second story test (docs/guides/skills.md)

Nits:

  • CHANGELOG hook is accurate but not shareable; consider npm-scopes framing
  • No template updated to demonstrate namespace
  • README not updated; namespace feature invisible to new visitors
  • Breaking change marker lacks visual prominence; move to ### Breaking Changes section

Growth/positioning side-channel: See growth signal in CEO arbitration above.

Verdict computed deterministically: 12 required findings across 5 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #1028 · ● 2.8M ·

@github-actions
Copy link
Copy Markdown

APM Review Panel Verdict: REJECT

Three independent specialists converge on an unfinished namespace input contract; migration path for the compile breaking change is insufficient; CLI discoverability gaps block merge.

Required before merge (12 items)

  • [python-architect] No parse-time namespace validation in the manifest parser at src/apm_cli/integration/skill_integrator.py

    • Why: The namespace field flows from apm.yml into path construction (_skill_dir) without any regex enforcement at parse time. ensure_path_within is a last-resort containment check, not an input contract. A namespace like my..ns or ../evil could produce surprising behaviour before containment fires.
    • Suggested fix: Add a manifest-level validator that raises ManifestError when namespace does not match an allowed pattern, mirroring how other constrained string fields are validated today.
  • [python-architect] Leaking private symbol _package_namespace across module boundary via lazy import at src/apm_cli/install/services.py

    • Why: services.py does from apm_cli.integration.skill_integrator import _package_namespace inside a function body. The underscore signals module-private. Any rename silently breaks services.py at runtime, not import-time.
    • Suggested fix: Rename to package_namespace() (public), move import to module header in services.py, or inline the two-level getattr at the single call site.
  • [python-architect] Three identical ctx.package_namespaces assignment patterns in sources.py at src/apm_cli/install/sources.py

    • Why: The same getattr/assign pattern appears verbatim at three independent call sites (local, cached, remote). Per APM's architecture rule: abstract when 3+ call sites share the same logic.
    • Suggested fix: Extract a private helper _store_namespace(ctx, dep_key, package_info) called from all three acquire paths.
  • [cli-logging-expert] Log message does not lead with outcome; identity buried after pipe decoration at src/apm_cli/install/services.py:265

    • Why: Rule 1 -- Lead with the outcome. |-- Skill acme/brand-guidelines integrated -> buries the subject mid-sentence. Verb and subject must come first per the Newspaper test.
    • Suggested fix: Rewrite as |-- Integrated skill acme/brand-guidelines -> .github/skills/acme/.
  • [cli-logging-expert] --verbose per-skill namespace line fires from skill_integrator.py while services.py emits an unconditional message -- two layers, inconsistent formats at src/apm_cli/install/services.py:262

    • Why: On --verbose, namespace appears twice with different formats. Progressive disclosure is broken.
    • Suggested fix: Gate services.py namespace label on non-verbose output only, OR remove the skill_integrator verbose_detail line and consolidate into services.py with proper verbose gating.
  • [devx-ux-expert] cli-commands.md not updated despite new namespace behavior surfacing in CLI output

    • Why: Per the Key Rule: a CLI change not reflected in cli-commands.md is incomplete by definition. Namespace changes apm install tree output and apm compile behavior.
    • Suggested fix: Update cli-commands.md to document (1) namespace field effect on install output, (2) verbose per-skill line, (3) the breaking compile change.
  • [devx-ux-expert] Regex does not enforce documented constraints (max 64 chars, no consecutive hyphens) at docs/src/content/docs/reference/manifest-schema.md

    • Why: Docs say namespace is max 64 chars and disallows --, but regex ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ enforces neither. Users who read docs expect rejection of a--b or a 65-char slug, but validator silently accepts them.
  • [devx-ux-expert] Breaking change to apm compile --target vscode/all lacks a structured migration path in migration.md

    • Why: Users who relied on the generated .github/copilot-instructions.md in CI are given only 'copy your last generated version' -- no command, no example. Violates the Recovery lens.
    • Suggested fix: Add a migration.md section with a before/after example and concrete shell command (e.g. git show HEAD~1:.github/copilot-instructions.md > .github/copilot-instructions.md).
  • [devx-ux-expert] apm init does not prompt for namespace and the field is not discoverable via apm init --help

    • Why: Org users who skip namespace will hit confusing collisions later with no indication they had a tool to prevent it. Breaks the First-run lens.
    • Suggested fix: Add optional --namespace flag to apm init (or interactive skippable prompt) and update cli-commands.md.
  • [supply-chain-security-expert] Lockfile from_dict deserializes namespace with no validation at src/apm_cli/deps/lockfile.py:150

    • Why: namespace=data.get('namespace') accepts any YAML value verbatim. A tampered lockfile could inject an arbitrary string. Even though path construction today flows from validated manifests, the unvalidated LockedDependency.namespace is a latent injection point.
    • Suggested fix: After data.get('namespace'), validate against ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ and set to None or raise on mismatch.
  • [oss-growth-hacker] Breaking change migration path is insufficient -- no worked example at CHANGELOG.md

    • Why: The CHANGELOG sends users to 'copy your last generated version' without a code example. Projects like bun and uv win community trust by providing an exact before/after snippet.
    • Suggested fix: Add a fenced before/after block in CHANGELOG and link to a migration guide with a concrete shell command.
  • [oss-growth-hacker] Namespace docs open with syntax, not the 'why' -- fails the 10-second story test at docs/src/content/docs/guides/skills.md

    • Why: The guides/skills.md addition jumps straight to YAML. A first-time reader has no idea why namespaces matter. The conversion story (multiple orgs, no collisions) is buried.
    • Suggested fix: Prepend a 2-sentence motivation: 'If you publish multiple APM packages from the same org, skills from different packages can overwrite each other in the flat layout. Namespaces isolate them.'

Nits (16 items, skip if you want)

  • [python-architect] _skill_owner_key_from_deployed_path fallback silently produces a key that won't match _skill_owner_key output; log a warning when /skills/ not found at src/apm_cli/integration/skill_integrator.py
  • [python-architect] Double getattr chain in _package_namespace is correct but visually noisy; consider a two-step guard or typed Protocol
  • [python-architect] drift.py getattr locals are safe; add a comment explaining backward compat with older lockfiles at src/apm_cli/drift.py
  • [python-architect] Verify _promote_sub_skills namespace: str | None = None parameter has full type annotation in the actual file
  • [cli-logging-expert] _skill_target_str change is semantically correct; add inline comment: # parent_rel already includes the skills/ segment (and namespace if set) at src/apm_cli/install/services.py:254
  • [cli-logging-expert] Fallback label copilot-cowork/skills hardcodes a path segment that may drift; extract to a named constant
  • [devx-ux-expert] Install tree output format skill acme/my-skill integrated may confuse users; consider skill [acme] my-skill -> to visually separate namespace from skill name
  • [devx-ux-expert] Adding/changing namespace on an existing package silently leaves orphaned flat-path directories; document this in the namespace guide
  • [devx-ux-expert] No consecutive hyphens rule not encoded in the schema Pattern row; either fix regex or add a Constraints row
  • [supply-chain-security-expert] _skill_dir docstring claims regex validation rejects traversal but the function performs no regex check; consider adding validate_path_segments at function entry
  • [supply-chain-security-expert] drift.py getattr refactor is neutral-to-strengthening (no security check bypassed); add a comment explaining the intent
  • [supply-chain-security-expert] No cryptographic coverage of the namespace field in the lockfile; document that the lockfile must be committed and code-reviewed like source
  • [oss-growth-hacker] CHANGELOG hook accurate but not shareable; consider npm-scopes framing: 'scope your skills like npm -- declare namespace: acme'
  • [oss-growth-hacker] No template updated to demonstrate namespace; add commented # namespace: my-org line to default apm.yml template
  • [oss-growth-hacker] README not updated; namespace feature invisible to new visitors; consider one bullet under Features
  • [oss-growth-hacker] Breaking change marker lacks visual prominence; move to a ### Breaking Changes section above ### Added

CEO arbitration

Three independent panelists -- python-architect, supply-chain-security-expert, and devx-ux-expert -- converge on a single root defect: the namespace field has no enforced validation contract. The regex in manifest-schema.md documents constraints (max 64 chars, no consecutive hyphens) that the actual regex ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ does not enforce; the lockfile deserializer accepts any YAML value verbatim; and the skill_integrator relies on ensure_path_within as a last-resort containment check rather than rejecting malformed input at parse time. These are not three separate issues -- they are one unfinished input contract manifesting at three layers. The fix must be atomic: a single validated namespace pattern, applied at manifest parse, lockfile deserialization, and documented in the schema.

Two panelists -- devx-ux-expert and oss-growth-hacker -- independently flag the migration path as insufficient. The CHANGELOG sends users to 'copy your last generated version' with no command, no example, and no CI guidance. The cli-logging-expert's finding that namespace identity is buried mid-message and the oss-growth-hacker's finding that skills.md opens with YAML syntax rather than the 'why' are complementary legibility failures -- both indicating the namespace concept is not yet legible to a first-time user.

The devx-ux-expert raises two independent UX gaps: cli-commands.md is not updated and apm init neither prompts for namespace nor exposes it via --help. The cli-logging-expert's progressive disclosure finding (namespace logged twice in inconsistent formats under --verbose) should be resolved in the same pass as the services.py log message rewrite.

Dissent resolved: No panelist-vs-panelist contradiction exists. On the cli-logging question: the panel sides with consolidating into services.py with proper verbose gating, eliminating the dual-layer inconsistency rather than papering over it.

Growth/positioning note: The namespace feature is the first signal that APM is moving toward a multi-publisher ecosystem -- analogous to npm orgs or OCI namespaces for containers. This is a significant positioning moment: 'APM is where AI skill packages are published, versioned, and namespaced.' A short release post framing namespaces as 'APM now has package scopes' -- with a terminal recording of installing two same-named skills from different orgs without collision -- would convert a breaking change news cycle into a positioning win.


Per-persona findings (full)

Python Architect

classDiagram
    direction LR
    class LockedDependency {
        +str name
        +str version
        +str namespace
        +list deployed_files
        +to_dict() dict
        +from_dict(data) LockedDependency
    }
    class InstallContext {
        <<dataclass>>
        +dict package_types
        +dict package_namespaces
        +dict package_deployed_files
    }
    class LockfileBuildPhase {
        <<Phase>>
        +ctx InstallContext
        +build_and_save() None
        -_attach_package_namespaces(lockfile) None
    }
    class SkillIntegratorHelpers {
        <<module-level functions>>
        +_package_namespace(package_info) str|None
        +_skill_dir(root, name, ns) Path
        +_skill_owner_key(name, ns) str
        +_skill_owner_key_from_deployed_path(path) str
    }
    class InstallSourcesAcquire {
        <<Strategy>>
        +acquire() Materialization|None
    }
    class LockFile {
        +dict dependencies
    }
    LockfileBuildPhase --> InstallContext : reads
    LockfileBuildPhase --> LockFile : mutates
    LockFile --> LockedDependency : contains
    InstallSourcesAcquire --> InstallContext : writes package_namespaces
    class LockedDependency:::touched
    class InstallContext:::touched
    class LockfileBuildPhase:::touched
    class SkillIntegratorHelpers:::touched
    class InstallSourcesAcquire:::touched
    classDef touched fill:#fff3b0,stroke:#d47600
Loading
flowchart TD
    A([apm install invoked]) --> B[sources.py acquire\nLocalSource / CachedSource / RemoteSource]
    B -- IO: read apm.yml --> C{package.namespace set?}
    C -- yes --> D[ctx.package_namespaces dep_key = namespace\nsources.py 223 379 550]
    C -- no --> E[skip]
    D --> F[LockfileBuildPhase.build_and_save\ninstall/phases/lockfile.py]
    E --> F
    F --> G[_attach_package_namespaces\nphases/lockfile.py]
    G -- LOCK: mutate lockfile --> H[LockedDependency.namespace = namespace\ndeps/lockfile.py]
    H --> I[FS: write apm.lock.yaml]
    F --> J[install/services.py: integrate_skill]
    J -- lazy import --> K[_package_namespace\nskill_integrator.py:154]
    K --> L{namespace truthy?}
    L -- yes --> M[_skill_dir\nskill_integrator.py:162\nensure_path_within check]
    L -- no --> N[legacy flat path]
    M -- FS: copytree --> O[skills/namespace/skill_name/]
    N -- FS: copytree --> P[skills/skill_name/]
    O --> Q[_skill_owner_key\nskill_integrator.py:175]
    Q --> R[update owned_by map\nFS: write .apm-manifest.json]
    J --> S[log: Integrated skill ns/name -> path/]
Loading

Design patterns

  • Used in this PR: Strategy -- InstallSourcesAcquire variants each implement acquire(); namespace extraction added at all three strategy endpoints (repetitive, see required finding). Chain of Responsibility -- LockfileBuildPhase.attach* methods; _attach_package_namespaces follows correctly after _attach_package_types. Module-level pure helpers -- _skill_dir, _skill_owner_key, _package_namespace; no state, correct placement; _skill_dir doubles as path-security guard.
  • Pragmatic suggestion: Extract _store_namespace(ctx, dep_key, package_info) helper in sources.py to collapse the three identical patterns -- net win at current scope.

Required: 3. Nits: 4. (See aggregated sections above.)

CLI Logging Expert

Required: 2. Nits: 3. (See aggregated sections above.)

DevX UX Expert

Required: 4. Nits: 3. (See aggregated sections above.)

Supply Chain Security Expert

Required: 1. Nits: 3. (See aggregated sections above.)

Auth Expert

Inactive -- No auth-sensitive files changed; drift.py refactor uses getattr for registry_prefix/host/resolved_ref but does not alter token management, credential resolution, or AuthResolver behavior.

OSS Growth Hacker

Required: 2. Nits: 4. (See aggregated sections above.)

Growth/positioning side-channel: The namespace feature is the first signal that APM is moving toward a multi-publisher ecosystem. See growth note in CEO arbitration above.

Verdict computed deterministically: 12 required findings across 5 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically.

Note

🔒 Integrity filter blocked 2 items

The following items were blocked because they don't meet the GitHub integrity level.

To allow these resources, lower min-integrity in your GitHub frontmatter:

tools:
  github:
    min-integrity: approved  # merged | approved | unapproved | none

Generated by PR Review Panel for issue #1028 · ● 2.8M ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label Apr 30, 2026
@danielmeppiel
Copy link
Copy Markdown
Collaborator

@shreejaykurhade it is not clear to me which harnesses support finding skills in subdirectories?

@shreejaykurhade
Copy link
Copy Markdown
Contributor Author

@danielmeppiel
I don’t have a confirmed list of harnesses that support recursive discovery under skills/<namespace>/<skill-name>/SKILL.md.

Copilot SDK documents discovery from immediate subdirectories of the skill parent, and Claude’s nested discovery is for .claude/skills/ directories in nested project folders, not namespace folders under one skills root.

So I changed the implementation to avoid requiring recursive discovery:

skills/<namespace>-<skill-name>/SKILL.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

panel-rejected Apm-review-panel verdict: REJECT. Removed automatically on next push.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Support package namespaces

4 participants