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

Filter by extension

Filter by extension

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

- Pin codex setup to `rust-v0.118.0` for security and reproducibility; update config to `wire_api = "responses"` (#663)
- Propagate headers and environment variables through OpenCode MCP adapter with defensive copies to prevent mutation (#622)
- Fix `apm compile --target claude` silently skipping dependency instructions stored in `.github/instructions/` (#631)
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Changelog formatting: add a blank line between the last bullet in ### Fixed and the next section header (### Changed) to match the surrounding Keep a Changelog style used elsewhere in this file.

Suggested change
- Fix `apm compile --target claude` silently skipping dependency instructions stored in `.github/instructions/` (#631)
- Fix `apm compile --target claude` silently skipping dependency instructions stored in `.github/instructions/` (#631)

Copilot uses AI. Check for mistakes.
### Changed

- `apm marketplace browse/search/add/update` now route through the registry proxy when `PROXY_REGISTRY_URL` is set; `PROXY_REGISTRY_ONLY=1` blocks direct GitHub API calls (#506)
Expand Down
65 changes: 45 additions & 20 deletions src/apm_cli/primitives/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@
]
}

# Dependency primitive patterns for .github directory within dependencies.
# Some packages store primitives in .github/ instead of (or in addition to) .apm/.
DEPENDENCY_GITHUB_PRIMITIVE_PATTERNS: Dict[str, List[str]] = {
'chatmode': [
"agents/*.agent.md",
"chatmodes/*.chatmode.md",
],
'instruction': ["instructions/*.instructions.md"],
'context': [
"context/*.context.md",
"memory/*.memory.md",
]
}


def discover_primitives(
base_dir: str = ".",
Expand Down Expand Up @@ -297,37 +311,48 @@ def get_dependency_declaration_order(base_dir: str) -> List[str]:
return []


def scan_directory_with_source(directory: Path, collection: PrimitiveCollection, source: str) -> None:
"""Scan a directory for primitives with a specific source tag.
def _scan_patterns(base_dir: Path, patterns: Dict[str, List[str]], collection: PrimitiveCollection, source: str) -> None:
"""Glob-scan-parse loop for one base directory and one patterns dict.

Args:
directory (Path): Directory to scan (e.g., apm_modules/package_name).
base_dir (Path): Directory to scan (e.g., dep/.apm or dep/.github).
patterns (Dict[str, List[str]]): Primitive-type → glob-pattern mapping.
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Non-ASCII arrow character () in the docstring violates the repo's printable-ASCII encoding rule and may cause encoding errors on some Windows terminals. Replace with an ASCII equivalent like "->".

This issue also appears on line 348 of the same file.

Suggested change
patterns (Dict[str, List[str]]): Primitive-type glob-pattern mapping.
patterns (Dict[str, List[str]]): Primitive-type -> glob-pattern mapping.

Copilot uses AI. Check for mistakes.
collection (PrimitiveCollection): Collection to add primitives to.
source (str): Source identifier for discovered primitives.
"""
# Look for .apm directory within the dependency
apm_dir = directory / ".apm"
if not apm_dir.exists():
# Even without .apm, check for SKILL.md (Claude Skills support)
_discover_skill_in_directory(directory, collection, source)
return

# Find and parse files for each primitive type
for primitive_type, patterns in DEPENDENCY_PRIMITIVE_PATTERNS.items():
for pattern in patterns:
full_pattern = str(apm_dir / pattern)
matching_files = glob.glob(full_pattern, recursive=True)

for file_path_str in matching_files:
for _primitive_type, type_patterns in patterns.items():
for pattern in type_patterns:
for file_path_str in glob.glob(str(base_dir / pattern), recursive=True):
file_path = Path(file_path_str)
if file_path.is_file() and _is_readable(file_path):
try:
primitive = parse_primitive_file(file_path, source=source)
collection.add_primitive(primitive)
except Exception as e:
print(f"Warning: Failed to parse dependency primitive {file_path}: {e}")

# Also check for SKILL.md in the dependency root


def scan_directory_with_source(directory: Path, collection: PrimitiveCollection, source: str) -> None:
"""Scan a directory for primitives with a specific source tag.

Args:
directory (Path): Directory to scan (e.g., apm_modules/package_name).
collection (PrimitiveCollection): Collection to add primitives to.
source (str): Source identifier for discovered primitives.
"""
# Scan .apm directory within the dependency
apm_dir = directory / ".apm"
if apm_dir.exists():
_scan_patterns(apm_dir, DEPENDENCY_PRIMITIVE_PATTERNS, collection, source)

# Also scan .github directory — some packages store primitives there instead of (or
# in addition to) .apm/. Without this, dependency instructions in .github/instructions/
# are silently skipped in the normal compile path (issue #631).
github_dir = directory / ".github"
if github_dir.exists():
_scan_patterns(github_dir, DEPENDENCY_GITHUB_PRIMITIVE_PATTERNS, collection, source)

# Check for SKILL.md in the dependency root
_discover_skill_in_directory(directory, collection, source)


Expand Down
40 changes: 40 additions & 0 deletions tests/unit/primitives/test_discovery_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,46 @@ def test_parse_error_in_dep_primitive_warns_and_continues(self):
self.assertIn("Warning", buf.getvalue())
self.assertEqual(collection.count(), 0)

def test_github_instructions_discovered_when_no_apm_dir(self):
"""Regression test for issue #631.

Dependency instructions stored in .github/instructions/ must be
included in compile --target claude without --local-only.
"""
dep_dir = Path(self.tmp) / "chkp-roniz" / "cc-rubber-duck"
_write(
dep_dir / ".github" / "instructions" / "rubber-duck.instructions.md",
INSTRUCTION_CONTENT,
)
collection = PrimitiveCollection()
scan_directory_with_source(
dep_dir, collection, source="dependency:chkp-roniz/cc-rubber-duck"
)
self.assertEqual(len(collection.instructions), 1)
self.assertEqual(
collection.instructions[0].source,
"dependency:chkp-roniz/cc-rubber-duck",
)

def test_github_instructions_discovered_alongside_apm_dir(self):
"""Regression test for issue #631.

When a dependency has both .apm/instructions/ and .github/instructions/,
primitives from both directories must be discovered.
"""
dep_dir = Path(self.tmp) / "owner" / "mixed-pkg"
_write(
dep_dir / ".apm" / "instructions" / "from-apm.instructions.md",
INSTRUCTION_CONTENT,
)
_write(
dep_dir / ".github" / "instructions" / "from-github.instructions.md",
INSTRUCTION_CONTENT,
)
collection = PrimitiveCollection()
scan_directory_with_source(dep_dir, collection, source="dependency:owner/mixed-pkg")
self.assertEqual(len(collection.instructions), 2)


class TestGetDependencyDeclarationOrder(unittest.TestCase):
"""Tests for get_dependency_declaration_order."""
Expand Down
Loading