skill-author/agent-author's #89 retarget needs to know exactly how each .apm/ primitive compiles to Claude Code and Copilot CLI output. The existing microsoft-apm corpus only had a full schema for skills and one minimal example for agents, and nothing for prompts/instructions/hooks. Deepened via APM's own Python source (not just docs) where prose was thin. Key finding for #89: agents have no per-target integrator, so apm compile does a naive verbatim copy to both Claude and Copilot, unlike prompts/instructions/hooks which each get real per-target reconstruction. That means the agent primitive's `tools:` field can't express both harnesses' incompatible vocabularies at once — a real upstream gap, not something we can schema our way around.
7.1 KiB
topic, source_keys
| topic | source_keys | ||
|---|---|---|---|
| instructions-primitive-schema |
|
File location, naming, and frontmatter
.apm/instructions/*.instructions.md. Confirmed as the genuine required extension (not assumed) via APM's own discovery glob in apm_cli/primitives/discovery.py: **/.apm/instructions/*.instructions.md (and the .github/instructions/ mirror, plus a bare **/*.instructions.md fallback).
Unlike prompts and hooks, instructions do have a small, concretely modeled dataclass — apm_cli.primitives.models.Instruction — because instructions feed APM's own compile pipeline (they get folded into root context files), not just pass-through deployment:
@dataclass
class Instruction:
name: str
file_path: Path
description: str
apply_to: str # from frontmatter key "applyTo"; empty means global/unconditional
content: str
author: str | None = None
version: str | None = None
source: str | None = None
Frontmatter fields: description (required by convention — its absence is a validation error) and applyTo (a glob or comma-separated glob list, or a YAML sequence — APM normalizes all three input shapes into one canonical comma-separated form internally via normalize_apply_to/parse_apply_to). No applyTo means the rule is treated as unconditional — folded into root context files as always-on guidance rather than scoped to specific paths.
Instruction.validate() produces these built-in errors/warnings:
- Missing
description→ error:"Missing 'description' in frontmatter". - Missing
applyTo→ warning-level:"No 'applyTo' pattern specified -- instruction will apply globally"(not fatal — it's accepted, just broad). - Empty body → error:
"Empty content".
Compile-time mapping: two entirely different mechanisms per target
This is the biggest divergence from the agent/skill/prompt primitives, and the one most likely to surprise: Claude Code does not get a verbatim copy of the .instructions.md file at all.
Copilot CLI — verbatim, native primitive. PrimitiveMapping("instructions", ".instructions.md", "github_instructions") on the copilot target has no output_compare flag, so InstructionIntegrator copies content through unchanged, preserving the original applyTo: frontmatter byte-for-byte (per the integrator's own docstring: "Copilot: .github/instructions/ (verbatim, preserving applyTo:)"). This is deployed by apm install, not apm compile.
At Copilot user scope only (~/.copilot/), individual files are not deployed — Copilot CLI at user scope reads a single copilot-instructions.md, so APM concatenates all instructions into that one file instead (user_primitive_overrides: {"instructions": PrimitiveMapping("", ".md", "copilot_user_instructions")}). Project-scope behavior (per-file, .github/instructions/) is unaffected.
Claude Code — real reconstruction into .claude/rules/, with field-dropping. PrimitiveMapping("rules", ".md", "claude_rules", output_compare=True) marks this as one of APM's four "rule formats" (RULE_FORMATS = {cursor_rules, claude_rules, windsurf_rules, kiro_steering}) that transform their source rather than copy it. InstructionIntegrator._convert_to_claude_rules():
- Parses the source frontmatter and pulls only
applyTo—descriptionis dropped entirely, not carried into the output in any form. - Converts
applyTointo apaths:YAML list (oneparse_apply_to()-split glob per line), e.g.applyTo: "**/*.py"→paths:\n - "**/*.py". - If there was no
applyTo(unconditional instruction), the output has no frontmatter at all — just the raw body, matching Claude's convention that files withoutpaths:in.claude/rules/apply unconditionally. - Filename is renamed:
<x>.instructions.md→<x>.md(the primitive'sextensionfield,.md, replaces the source suffix — this is the general rule for everyoutput_compare=True"rule format").
This is architecturally the same category of lossy, real transformation the prior agent-primitive research found for Codex/Kiro agents — except here it's the default behavior for Claude specifically (not an opt-out edge case), and it applies even though Claude and Copilot are both first-class, actively-supported targets.
Compile-time file placement
| Target | Output path | Transform |
|---|---|---|
| Copilot CLI (project scope) | .github/instructions/<name>.instructions.md |
Verbatim byte copy, applyTo: preserved as-is |
Copilot CLI (user scope, ~/.copilot/) |
~/.copilot/copilot-instructions.md |
Concatenated — all instructions merged into one file, because Copilot CLI at user scope reads only that single file |
| Claude Code | .claude/rules/<name>.md |
Reconstructed: applyTo → paths: YAML list; description dropped; no frontmatter at all if unconditional |
Additionally, apm compile (distinct from apm install) can also fold instruction content directly into root context files — AGENTS.md (single-file or per-directory "distributed" mode) and the Claude-specific parallel format CLAUDE.md/per-directory CLAUDE.md — grouped by directory using applyTo pattern analysis (context_optimizer.optimize_instruction_placement). To avoid duplicating content between the native .claude/rules/+.github/instructions/ deployment (from apm install) and this root-context fold-in (from apm compile), a skip_instructions config flag (and compilation.placement.min_instructions_per_file in apm.yml) actively suppresses the redundant copy in AGENTS.md/CLAUDE.md once native per-target files exist — apm compile --target claude --force-instructions overrides this dedup when an author explicitly wants both.
Validation constraints and gotchas
- The
descriptionfield is real for Copilot but silently discarded for Claude. An author who relies ondescriptionto explain why a rule exists (common practice, since Copilot's.instructions.mdUI can surface it) gets that context deleted on every Claude compile — there's no config to keep it as a comment or otherwise. - No content-level validation for the
paths:conversion — ifapplyTocontains a patternparse_apply_tocan't split sensibly, the resultingpaths:list is whatever falls out; no dedicated schema check catches a malformed glob before deploy. - Directory-distribution logic for AGENTS.md/CLAUDE.md is heuristic, not declarative —
context_optimizer.optimize_instruction_placementpicks placement directories fromapplyTopatterns algorithmically;compilation.placement.min_instructions_per_fileinapm.yml(default effectively 1) is the only tuning knob, and setting it above 1 causes under-populated directories to have their instructions bubbled up to the parent directory rather than dropped. - Same "no dedicated primitive validation function" gap noted for agents —
Instruction.validate()inprimitives/models.pyis the only validation, and it is invoked as part of the generic primitive-discovery/compile pipeline, not as a standaloneapm auditcheck comparable to what exists forapm.ymlitself.