Files
holocron/plugins/kyberforge/docs/research/docs/microsoft-apm/prompt-primitive-schema.md
Defame1297 ff2b8b6c1b docs(kyberforge): complete apm hooks, instructions and prompts research
Re-verify the three primitive schema docs against the installed apm-cli
0.28.0 source and live installs, and add an authoring checklist to each.

Corrections to the earlier docs:
- Copilot hooks are not reshaped: events are renamed, paths rewritten and
  version: 1 added, but command/timeout are not renamed to bash/timeoutSec.
- A malformed .claude/settings.json is overwritten on install, losing
  user content.
- Instruction validate() messages are warnings only; apm compile
  --validate never fails on them.

Refs #94

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
2026-09-28 16:30:26 +00:00

8.3 KiB

topic, source_keys
topic source_keys
prompt-primitive-schema
apm-cli-installed-source
apm-docs-llms-full
apm-github-repo
context7-microsoft-apm

Checked against the installed apm-cli 0.28.0 source (integration/prompt_integrator.py, integration/command_integrator.py, integration/base_integrator.py, integration/targets.py, security/gate.py) and a live apm install of a scratch package targeting claude and copilot (2026-09-28).

File location, naming, discovery

  • PromptIntegrator.find_prompt_files() and CommandIntegrator.find_prompt_files() both run find_files_by_glob(pkg, "*.prompt.md", subdirs=[".apm/prompts"]).
    • The search covers the package root and .apm/prompts/, and is non-recursive.
    • Symlinks and hardlinks are rejected.
    • .apm/prompts/ is canonical. Root files are discovered for backward compatibility (published docs).
  • Identity comes from the filename minus .prompt.md. That name is the Copilot filename unchanged, and the Claude /command name.
    • integrate_commands_for_target runs validate_path_segments(base_name, context="command filename") against traversal names.
    • A duplicate name in the root and in .apm/prompts/ collides. The published docs say "later writer wins on copilot and the transform fails on Claude/Cursor". This was not verified here.
  • Prompts are not in discover_primitives() and have no model class. apm compile and apm compile --validate never look at them.
  • Prompts are deployed only by apm install.
  • There is no .apm/commands/ primitive. A Claude command is the compiled form of a prompt.

Frontmatter

There is no closed schema. What survives is decided per target.

_PRESERVED_COMMAND_KEYS (exact, 0.28.0):

  • description
  • allowed-tools
  • allowedTools
  • model
  • argument-hint
  • argumentHint
  • input

The user-facing list (_PRESERVED_COMMAND_KEYS_DISPLAY) omits the camelCase aliases.

input: shapes accepted by _extract_input_names:

Shape Names extracted
input: [file, focus] (list of strings) each string
input: list of one-key maps (- file: "desc") each map's keys
input: file (single string) that string
input: {file: desc, focus: desc} (map) its keys
  • Names must match _INPUT_NAME_RE = ^[A-Za-z][\w-]{0,63}$.
  • Invalid names and non-string entries are rejected, with the warning input: rejected N invalid name(s) (must match [A-Za-z][\w-]{0,63}): <first 5>.
  • Whitespace-only entries are dropped silently.
  • Upstream docs bug. The published "Commands" example writes - name: pr_number / description: … inside a single map. _extract_input_names reads the map's keys, so the arguments come out as name and description rather than pr_number. This was verified live: arguments: [name, description], argument-hint: <name> <description>. Use - pr_number: "desc" instead.

Other keys seen in docs:

  • Copilot-only picker metadata: name, agent, mode, tools.
  • Cursor and other targets: author, mcp, parameters.
  • Copilot App workflow keys: interval, schedule_hour, schedule_day, reasoning_effort, per the published docs. The source has a copilot_app_workflow_integrator module, but it was not traced here.

None of these other keys survive the Claude transform.

Per-target mapping

Copilot, verbatim. Mapping: PrimitiveMapping("prompts", ".prompt.md", "github_prompt"). PromptIntegrator.copy_prompt resolves links and normalises line endings to LF. In the live run a diff against the source was empty, and every key survived, including dropped-for-Claude keys. ${input:x} stays as written. At user scope the prompt goes to ~/.copilot/prompts/.

Claude, reconstructed. Mapping: PrimitiveMapping("commands", ".md", "claude_command"), which goes through CommandIntegrator._transform_prompt_to_command. The transform:

  • Builds new frontmatter from description, allowed-tools (the allowedTools alias is accepted), model, and argument-hint (the argumentHint alias is accepted).
  • Adds arguments: [names] from input. When there is no explicit argument-hint, it synthesises argument-hint: "<a> <b>".
  • Emits keys in alphabetical order, because frontmatter.dumps sorts them.
  • Rewrites the body with the regex \$\{\{?\s*input\s*:\s*([\w-]+)\s*\}?\} → $name. This runs only when at least one valid input name exists, and then it rewrites every ${input:…}, including names not declared in input:. Live: ${input:undeclared} became $undeclared, while a prompt with no input: kept ${input:x} literally.
  • Reports dropped keys, sorted(source_keys - _PRESERVED_COMMAND_KEYS), as the exact warning Claude command <name>: frontmatter keys not supported for claude commands and were dropped: <keys>. Supported keys: allowed-tools, argument-hint, description, input, model.
  • Emits the info message Mapped input -> command arguments in <file>: [...].
  • Scans the compiled text with SecurityGate.scan_text(BLOCK_POLICY). A critical hidden-character finding skips the write.
  • Deploys at user scope to ~/.claude/commands/, or to $CLAUDE_CONFIG_DIR/commands/ if that variable is set.

Cursor, OpenCode and Grok Build reuse the same claude_command transformer. Gemini writes TOML. Windsurf writes workflows. Codex gets nothing.

Live output of review.prompt.md:

---
allowed-tools:
- Read
- Grep
argument-hint: <file> <focus>
arguments:
- file
- focus
description: Review a file
model: sonnet
---

Review $file focusing on $focus and $undeclared.
Target Output path Transform
Copilot .github/prompts/<name>.prompt.md Verbatim (links resolved)
Claude .claude/commands/<name>.md Preserved-key subset; input becomes arguments; ${input:x} becomes $x

Validation constraints and gotchas

  • APM never validates description: the transform only copies it if present. Deploying a prompt with no frontmatter at all was not tested.
  • $ARGUMENTS and other native Claude syntax pass through untouched. They appear literally in the Copilot copy.
  • A prompt that relies on Copilot-only keys (agent, tools, mode) loses them on Claude. The only signal is the install-time warning.
  • A pre-install hidden-Unicode scan (install/helpers/security_scan, BLOCK_POLICY) runs on source files. apm compile does not re-scan, so run apm audit before publishing (published docs).
  • apm run <script> --param k=v compiles a prompt with parameters bound. See cli-reference.md.

Authoring checklist

Must (an author skill enforces these; an audit skill checks them):

  1. The path is .apm/prompts/<name>.prompt.md, directly in that directory and not a symlink. <name> is unique across .apm/prompts/ and the package root, and is a safe path segment. Source: find_prompt_files, validate_path_segments.
  2. description is present and non-empty. It is the picker and command description on both targets, and apm does not check it. Source: _transform_prompt_to_command.
  3. Every input: name matches ^[A-Za-z][\w-]{0,63}$. The object form is - <name>: "<desc>", never - name: <name>. Source: _INPUT_NAME_RE, _extract_input_names.
  4. Every ${input:x} in the body refers to a name declared in input:, and every declared name is used. If input: is empty or absent, no ${input:…} may appear, because it would reach Claude unrewritten. Source: the rewrite regex and its condition.
  5. Frontmatter keys are limited to the preserved set (description, allowed-tools, model, argument-hint, input) unless a Copilot-only key is intended and its Claude drop is accepted. Source: _PRESERVED_COMMAND_KEYS.

Should: 6. Use the kebab-case spellings allowed-tools and argument-hint, not the camelCase aliases. Source: _PRESERVED_COMMAND_KEYS_DISPLAY. 7. Omit argument-hint when input: is set, unless the synthesised <a> <b> form is inadequate. Source: _transform_prompt_to_command. 8. Give model a slug the target accepts. Copilot ignores allowed-tools and model (published docs). 9. Keep one intent per prompt, and write the body as second-person instructions (published "Author a prompt" guide).

Audit-only (apm does not check these): missing description; undeclared or unused inputs; ${input:…} without input:; Copilot-only keys in a Claude-targeted package; name collisions between the root and .apm/prompts/.