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.
6.1 KiB
topic, source_keys
| topic | source_keys | ||
|---|---|---|---|
| prompt-primitive-schema |
|
File location, naming, and frontmatter
.apm/prompts/*.prompt.md (also discovered at the package root). Filename minus the .prompt.md suffix becomes the prompt's identity — used verbatim as the Copilot filename and, after transformation, as the Claude command name. No required-extension ambiguity: it is genuinely .prompt.md, confirmed both in docs and in APM's own PromptIntegrator.find_prompt_files (*.prompt.md) and CommandIntegrator.find_prompt_files (same glob).
There is no single closed frontmatter schema — APM's P1 "no invented primitive frontmatter" principle applies here too, so a prompt author writes whatever keys their primary target needs and APM passes or drops per-target. Keys seen in APM's own docs/examples:
| Field | Purpose |
|---|---|
description |
Shown in Copilot's prompt picker / used for discovery |
input |
List of parameter names (simple list, or list of {name: description} objects) referenced in body as ${input:name} |
allowed-tools (or allowedTools) |
Tool allowlist for the prompt's execution |
argument-hint (or argumentHint) |
Human-readable hint for expected arguments |
model |
Model override when the prompt runs |
author, mcp, parameters |
Cursor/other-target-specific metadata — not preserved by the shared Claude/Cursor command transformer (see below) |
Workflow-prompt-only keys (Copilot App / Copilot Workflows, not Copilot CLI): name, interval (manual/hourly/daily/weekly), schedule_hour (0–23 UTC), schedule_day (0–6, weekly only), mode (interactive/plan), reasoning_effort. These are flat top-level keys on the same .prompt.md file, consumed only by the Copilot App scheduler integration — irrelevant to Claude Code / Copilot CLI compilation and should not be treated as universal prompt schema.
Compile-time mapping: verbatim for Copilot, real reconstruction for Claude
Copilot CLI target — verbatim copy. PrimitiveMapping("prompts", ".prompt.md", "github_prompt") on the copilot target profile carries no output_compare flag, and PromptIntegrator.copy_prompt() reads the source file and writes it out unchanged (only markdown link targets get rewritten) via copy_prompt: "Copy prompt file verbatim with link resolution.". Every frontmatter key — including author, mcp, parameters — survives. Filename is untouched (get_target_filename returns source_file.name, "no -apm suffix").
Claude Code target — real reconstruction into a slash command, with field-dropping. There is no prompts: key at all in Claude's TargetProfile.primitives dict; instead prompts route through the shared CommandIntegrator, which transforms .prompt.md → Claude custom slash command markdown. CommandIntegrator._transform_prompt_to_command():
- Strips the
.prompt.mdsuffix from the filename to derivecommand_name. - Builds an entirely new frontmatter object containing only these preserved keys:
description,allowed-tools(acceptsallowedToolsalias),model,argument-hint(acceptsargumentHintalias). - Maps APM's
input:list to Claude'sarguments:list, and synthesizesargument-hintfrom it if not already set. - Rewrites body placeholders
${input:name}/${{input:name}}to Claude's native$namesyntax via regex substitution. - Computes
dropped_keys = source_frontmatter_keys - preserved_keysand surfaces it as an install-time diagnostic warning — soauthor,mcp,parameters, and any other non-listed key are silently dropped from the compiled output but not silently dropped from the user's awareness (a warning fires). - Cursor reuses this exact same transformer (
claude_commandformat_id) — same preserved-key set, same drops.
Compile-time file placement
| Target | Output path | Transform |
|---|---|---|
| Copilot CLI | .github/prompts/<name>.prompt.md |
Verbatim byte copy (links resolved) |
| Claude Code | .claude/commands/<name>.md |
Reconstructed: only description/allowed-tools/model/argument-hint/arguments survive; input: → arguments:; ${input:x} → $x |
Invocation surface differs correspondingly: Copilot exposes it via the prompts picker UI (select by name); Claude exposes it as /<name> <args> (same pattern Cursor, OpenCode, Gemini CLI, and Windsurf's workflows menu use for their own compiled copies).
Validation constraints and gotchas
- Input-name validation is real, not just documentation.
_extract_input_names()enforces[A-Za-z][\w-]{0,63}on every name pulled frominput:; anything that fails is dropped fromarguments:and reported as a warning listing up to 5 rejected names (input: rejected N invalid name(s) ...). A malformedinput:entry does not fail the install — it silently loses that one argument. - Filename-derived identity is security-checked.
integrate_commands_for_targetcallsvalidate_path_segments(base_name, context="command filename")specifically to reject a package shipping a.prompt.mdfile with a manipulated relative name (e.g.../../evil.prompt.md) that would otherwise escape the target commands directory. - The dropped-key warning is the only signal a Claude-only author gets that Cursor-specific frontmatter (
author,mcp,parameters) never reached the deployed file — there is no error, no hard failure, and no config flag to preserve those keys for Claude; the shared transformer's preserved-key list is fixed in code (_PRESERVED_COMMAND_KEYS), not configurable per package. - No dedicated
Prompt/PromptPrimitivevalidation class exists inapm_cli/models/validation.pyorapm_cli/primitives/models.py— same gap pattern documented for the agent primitive.apm.yml'stype: promptspackage-content-type ("Commands/prompts only, no instructions or skills") is validated at the package-type-detection level, not the individual-prompt level. - Slash commands and prompts share one source directory and one glob (
.apm/prompts/*.prompt.md) — there is no separate.apm/commands/primitive; "command" is purely a per-target compiled name for the same source file, not a distinct authoring primitive.