Files
holocron/plugins/kyberforge/docs/research/docs/microsoft-apm/instructions-primitive-schema.md
Defame1297 5e296bcfef docs(kyberforge): research all five APM primitive schemas
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.
2026-08-11 16:39:54 +00:00

7.1 KiB

topic, source_keys
topic source_keys
instructions-primitive-schema
context7-microsoft-apm
apm-github-repo

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 — description is dropped entirely, not carried into the output in any form.
  • Converts applyTo into a paths: YAML list (one parse_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 without paths: in .claude/rules/ apply unconditionally.
  • Filename is renamed: <x>.instructions.md → <x>.md (the primitive's extension field, .md, replaces the source suffix — this is the general rule for every output_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 description field is real for Copilot but silently discarded for Claude. An author who relies on description to explain why a rule exists (common practice, since Copilot's .instructions.md UI 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 — if applyTo contains a pattern parse_apply_to can't split sensibly, the resulting paths: 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_placement picks placement directories from applyTo patterns algorithmically; compilation.placement.min_instructions_per_file in apm.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() in primitives/models.py is the only validation, and it is invoked as part of the generic primitive-discovery/compile pipeline, not as a standalone apm audit check comparable to what exists for apm.yml itself.