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
8.3 KiB
topic, source_keys
| topic | source_keys | ||||
|---|---|---|---|---|---|
| prompt-primitive-schema |
|
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()andCommandIntegrator.find_prompt_files()both runfind_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).
- The search covers the package root and
- Identity comes from the filename minus
.prompt.md. That name is the Copilot filename unchanged, and the Claude/commandname.integrate_commands_for_targetrunsvalidate_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 compileandapm compile --validatenever 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):
descriptionallowed-toolsallowedToolsmodelargument-hintargumentHintinput
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_namesreads the map's keys, so the arguments come out asnameanddescriptionrather thanpr_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 acopilot_app_workflow_integratormodule, 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(theallowedToolsalias is accepted),model, andargument-hint(theargumentHintalias is accepted). - Adds
arguments: [names]frominput. When there is no explicitargument-hint, it synthesisesargument-hint: "<a> <b>". - Emits keys in alphabetical order, because
frontmatter.dumpssorts 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 ininput:. Live:${input:undeclared}became$undeclared, while a prompt with noinput:kept${input:x}literally. - Reports dropped keys,
sorted(source_keys - _PRESERVED_COMMAND_KEYS), as the exact warningClaude 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. $ARGUMENTSand 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 compiledoes not re-scan, so runapm auditbefore publishing (published docs). apm run <script> --param k=vcompiles a prompt with parameters bound. Seecli-reference.md.
Authoring checklist
Must (an author skill enforces these; an audit skill checks them):
- 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. descriptionis 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.- 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. - Every
${input:x}in the body refers to a name declared ininput:, and every declared name is used. Ifinput:is empty or absent, no${input:…}may appear, because it would reach Claude unrewritten. Source: the rewrite regex and its condition. - 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/.