Files
holocron/plugins/kyberforge/docs/research/docs/microsoft-apm/prompt-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

6.1 KiB
Raw Blame History

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

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.md suffix from the filename to derive command_name.
  • Builds an entirely new frontmatter object containing only these preserved keys: description, allowed-tools (accepts allowedTools alias), model, argument-hint (accepts argumentHint alias).
  • Maps APM's input: list to Claude's arguments: list, and synthesizes argument-hint from it if not already set.
  • Rewrites body placeholders ${input:name} / ${{input:name}} to Claude's native $name syntax via regex substitution.
  • Computes dropped_keys = source_frontmatter_keys - preserved_keys and surfaces it as an install-time diagnostic warning — so author, 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_command format_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 from input:; anything that fails is dropped from arguments: and reported as a warning listing up to 5 rejected names (input: rejected N invalid name(s) ... ). A malformed input: entry does not fail the install — it silently loses that one argument.
  • Filename-derived identity is security-checked. integrate_commands_for_target calls validate_path_segments(base_name, context="command filename") specifically to reject a package shipping a .prompt.md file 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/PromptPrimitive validation class exists in apm_cli/models/validation.py or apm_cli/primitives/models.py — same gap pattern documented for the agent primitive. apm.yml's type: prompts package-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.