feat(kyberforge): add instructions-author skill for .apm/instructions files
Scaffolds and revises apm instructions files, with a throwaway-package verification recipe because `apm compile --validate` always exits 0 and Claude Code drops `description`. Routed from forge and linked from apm-workflow's compile reference. Bumps kyberforge to 2.1.0 and the catalog to 0.5.2. Fixes #148 Co-Authored-By: Claude Code <[email protected]> Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
This commit is contained in:
1 parent
529ed31cef
commit
c52e351954
20 files changed
+719
-16
No files matched your search
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: instructions-author
|
||||
description: >
|
||||
Use when creating or revising an apm instructions file
|
||||
(`.apm/instructions/*.instructions.md`). Not read-only review ->
|
||||
`factory-audit`. Not skills -> `skill-author`. Not agents -> `agent-author`.
|
||||
Not AGENTS.md -> `agentsmd-author`.
|
||||
compatibility: Requires the apm CLI; behaviour verified against apm 0.28.0.
|
||||
allowed-tools: Bash Read Write Edit
|
||||
metadata:
|
||||
version: "0.1.0"
|
||||
category: factory
|
||||
source_keys:
|
||||
- apm-docs-site
|
||||
- apm-cli-0-28-0-experiments
|
||||
- claude-code-memory-docs
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Claude Code drops `description`; only Copilot and Cursor keep it. Write a body that explains itself.
|
||||
- Quote every `applyTo`. An unquoted `**/*.py` fails to parse, compile skips the file, and `apm install` still deploys it with no `paths:`, so it loads in every session and nothing errors.
|
||||
- `apm compile --validate` always exits 0 and hides the missing-`description`, missing-`applyTo` and empty-body warnings. It is not a lint gate.
|
||||
- Once rules sit in `.claude/rules/`, `apm compile --target claude` writes no `CLAUDE.md` and still exits 0; an exit-code check verifies nothing.
|
||||
- A source must be flat in `.apm/instructions/` and end `.instructions.md`; anything else is ignored or never installed.
|
||||
|
||||
## Step 1 — Dispatch
|
||||
|
||||
| Condition | Flow | Reference |
|
||||
|---|---|---|
|
||||
| No file at the target path | Create | `references/create.md` |
|
||||
| A file exists, at least one improvement signal present | Improve | `references/improve.md` |
|
||||
| A file exists, no signals | Stop and ask | — |
|
||||
|
||||
Signals: grill output, audit findings, inline feedback, a session describing a rule that loaded when it should not or failed to load. With none, ask whether the user meant to create a new file or has feedback to apply.
|
||||
|
||||
Read only the reference for the resolved flow. Capture `rtk git log --oneline -1` before touching the filesystem; Step 3 needs it.
|
||||
|
||||
## Step 2 — Contract
|
||||
|
||||
Gates on every file, whichever flow wrote it:
|
||||
|
||||
- **One topic per file.** Two topics are two files.
|
||||
- **Scope.** Omit `applyTo` only for a rule that must load in every session, and tell the user it then costs context at every launch.
|
||||
- **Stem.** It becomes the deployed filename, and install overwrites a hand-authored rule of the same name on most targets without a prompt. Check for a collision before choosing it.
|
||||
- **Body.** Bullets, paths in backticks, nothing assuming another file is loaded, under 200 lines.
|
||||
|
||||
If a field, glob or location is in question, read `references/schema.md`. If the question is which target keeps which field, or what compile does, read `references/target-mapping.md`.
|
||||
|
||||
## Step 3 — Validate and close
|
||||
|
||||
- [ ] Verify with a real compile and a throwaway deploy: read `references/verify.md`. Resolve every warning and confirm a scoped rule deploys with `paths:`.
|
||||
- [ ] Bump the owning package's `apm.yml` `version` — **minor** on create, **patch** on improve — because consumers compare it to detect updates.
|
||||
|
||||
`factory-audit` has no instructions checks yet, so nothing else gates the file; report only what the verification showed.
|
||||
|
||||
**Commit verification.** Once verification is clean, run `rtk git add` and `rtk git commit`. Re-run `rtk git log --oneline -1` and confirm the hash changed from Step 1's. A non-empty `git diff --stat` is not proof: staged-but-uncommitted work is lost if the tree is cleaned up. Report done only once the hash has changed.
|
||||
@@ -0,0 +1,5 @@
|
||||
# assets/
|
||||
|
||||
## templates/
|
||||
|
||||
- **`instructions.md`** — minimal valid `.apm/instructions/<name>.instructions.md`, copied by `scripts/new-instructions.sh`. Carries a `description`, a quoted `applyTo` and a one-topic body, each marked `FILL IN:`. The `applyTo` comment is the only guidance it carries; field semantics are in `references/schema.md`.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
# Delete these comments once filled in; Copilot receives this file verbatim.
|
||||
description: FILL IN: one line on what this rule covers. Only Copilot and Cursor keep it.
|
||||
applyTo: "FILL IN: quoted glob, e.g. **/*.py"
|
||||
# applyTo is always quoted: an unquoted ** is a YAML alias error and the rule
|
||||
# deploys unscoped. Several globs: "**/*.css,**/*.scss". Delete the line only
|
||||
# for a rule that must load in every session.
|
||||
---
|
||||
# FILL IN: one topic per file
|
||||
|
||||
- FILL IN: the first rule, stated as a bullet.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
source_keys:
|
||||
- apm-docs-site
|
||||
- apm-cli-0-28-0-experiments
|
||||
---
|
||||
|
||||
# Creating a new instructions file
|
||||
|
||||
Return to `SKILL.md` Step 3 once Step 3 below is done.
|
||||
|
||||
## Before touching the filesystem
|
||||
|
||||
Confirm, and ask the user for anything missing:
|
||||
|
||||
- [ ] The one topic the file covers. Two topics are two files.
|
||||
- [ ] Which files it governs, as a glob, or that it must load in every session.
|
||||
- [ ] A kebab-case stem. It becomes the deployed filename.
|
||||
|
||||
## Step 1 — Check the stem
|
||||
|
||||
Install overwrites a hand-authored file at `.claude/rules/<stem>.md`, `.cursor/rules/<stem>.mdc`, `.windsurf/rules/<stem>.md`, `.kiro/steering/<stem>.md` and `.agents/rules/<stem>.md` without a prompt. List those paths in the consuming project and choose another stem on any hit.
|
||||
|
||||
## Step 2 — Scaffold
|
||||
|
||||
```bash
|
||||
bash scripts/new-instructions.sh <name> <path-inside-the-package>
|
||||
```
|
||||
|
||||
The script walks up for a `type:`-bearing `apm.yml`. With none it exits 1 and names `/apm-workflow configure`; run that first, then retry. It never overwrites an existing file.
|
||||
|
||||
## Step 3 — Fill in
|
||||
|
||||
Replace every `FILL IN:` and delete the template's comments.
|
||||
|
||||
- `applyTo`: quoted. Omit it only for a rule that must load in every session, and say so to the user; it costs context at every launch.
|
||||
- `description`: one line. Write the body as if it were absent, because Claude Code never sees it.
|
||||
- Body: bullets, one topic, paths in backticks, nothing that assumes another file is loaded.
|
||||
|
||||
For glob syntax or a field question, read `references/schema.md`.
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source_keys:
|
||||
- apm-docs-site
|
||||
- apm-cli-0-28-0-experiments
|
||||
---
|
||||
|
||||
# Improving an existing instructions file
|
||||
|
||||
Return to `SKILL.md` Step 3 once the edits are made.
|
||||
|
||||
## Step 1 — Read the file and the signals
|
||||
|
||||
Read the file whole. Signals are grill output, audit findings, inline feedback, or a session describing a rule that loaded when it should not, or failed to load. Apply what the signals name and nothing else.
|
||||
|
||||
## Step 2 — Diagnose by symptom
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| A scoped rule loads in every Claude session | `applyTo` is unquoted or malformed, so install deployed no `paths:` | Quote it, then confirm with `references/verify.md` |
|
||||
| Compile warns "Failed to parse" | Broken frontmatter YAML | Repair the YAML; do not delete the field |
|
||||
| The rule is in `CLAUDE.md` but not `.claude/rules/` | The file is nested under `.apm/instructions/` | Move it up to the flat directory |
|
||||
| The rule appears nowhere | The name lacks `.instructions.md` | Rename it |
|
||||
| Claude ignores guidance written in `description` | Claude Code drops `description` | Move the substance into the body |
|
||||
| A hand-written rule vanished after install | The stem collided with a deployed name | Restore it from version control and rename the source stem |
|
||||
| The same rule reaches the agent twice | Cursor, Windsurf, Kiro, Codex and OpenCode get both a native file and an `AGENTS.md` copy | State it to the user; it is apm behaviour, not a defect in the file |
|
||||
|
||||
Cases not in the table: read `references/target-mapping.md`.
|
||||
|
||||
## Step 3 — Split or trim
|
||||
|
||||
A file covering two topics, or longer than 200 lines, becomes several files. Do the split only when a signal names it.
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
source_keys:
|
||||
- apm-docs-site
|
||||
- apm-github-repo
|
||||
- apm-cli-0-28-0-experiments
|
||||
- claude-code-memory-docs
|
||||
---
|
||||
|
||||
# The instructions source file
|
||||
|
||||
Verified against apm 0.28.0. Reached from `SKILL.md` Step 2 when a frontmatter field, a glob or the file's location is in question.
|
||||
|
||||
## Location and name
|
||||
|
||||
`.apm/instructions/<name>.instructions.md`, flat. The double extension is the discovery key and the stem is the primitive's name; there is no `name` field.
|
||||
|
||||
- A plain `.md` in that directory is ignored by both `apm compile` and `apm install`.
|
||||
- A file in a subdirectory is folded into compiled root files by compile but never deployed by install, so it reaches `CLAUDE.md` and `AGENTS.md` and no native rules directory.
|
||||
- The stem becomes the deployed filename: `<stem>.md`, `<stem>.mdc`, or `<stem>.instructions.md`, by target.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
Only `description` and `applyTo` carry meaning. `author` and `version` are parsed and never emitted to any target.
|
||||
|
||||
- `description`: one line. The apm docs call it required; the binary only warns. Copilot and Cursor keep it; Claude Code, Windsurf, Kiro, Antigravity and every compiled root file drop it. Cursor auto-generates one from the first body sentence when it is missing.
|
||||
- `applyTo`: a glob scoping the rule. The apm docs list it as both required and optional; the binary treats it as optional, with a warning. Empty or absent means an unconditional rule.
|
||||
|
||||
### `applyTo` grammar
|
||||
|
||||
- One glob: `"**/*.py"`.
|
||||
- Several globs in one string, comma-separated: `"**/*.css,**/*.scss"`. Whitespace around segments is trimmed.
|
||||
- A YAML sequence is joined into the same comma form.
|
||||
- Brace alternation is never split: `"**/*.{css,scss},**/*.py"` is two patterns.
|
||||
- A literal comma in a pattern is `\,`; a literal backslash is `\\`.
|
||||
- Always quote the value. An unquoted `**/*.py` is a YAML alias error; see `SKILL.md` Gotchas for what apm then does.
|
||||
|
||||
## Body
|
||||
|
||||
Plain markdown. Official guidance: bullets over prose, one topic per file (`python-style` and `python-testing` are two files), paths in backticks, no greetings or meta-commentary, no assumption that other files are loaded. apm sets no size limit. The downstream tools do: Claude Code recommends under 200 lines per file and Cursor under 500.
|
||||
|
||||
## Validation
|
||||
|
||||
`Instruction.validate()` yields three findings, all demoted to warnings: missing `description`, missing `applyTo` ("will apply globally") and empty content. A broken relative link in the body is a fourth, also non-fatal.
|
||||
|
||||
- A real `apm compile` prints them. `apm compile --validate` prints none and exits 0 even for a file with all three problems.
|
||||
- `apm install` prints none.
|
||||
- `apm audit --ci` checks lockfile, deployed-file presence, content hash and hidden Unicode, not instruction content.
|
||||
- A file whose frontmatter does not parse is skipped by compile ("Failed to parse") but still deployed by install.
|
||||
|
||||
No standalone instructions validator exists, so enforcement is this skill's checks and `references/verify.md`.
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
source_keys:
|
||||
- apm-docs-site
|
||||
- apm-github-repo
|
||||
- apm-cli-0-28-0-experiments
|
||||
- claude-code-memory-docs
|
||||
- github-copilot-custom-instructions-docs
|
||||
- cursor-rules-docs
|
||||
---
|
||||
|
||||
# Sources
|
||||
|
||||
## apm-docs-site
|
||||
|
||||
- **URL:** https://microsoft.github.io/apm/
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md (digest: plugins/kyberforge/docs/research/docs/microsoft-apm/instructions-primitive-schema.md)
|
||||
- **Description:** Official apm documentation, the instructions-and-agents authoring page plus targets and compile pages: frontmatter requirements, per-target deploy paths, compile behaviour and flags.
|
||||
- **Contributing files:** SKILL.md, references/schema.md, references/target-mapping.md, references/create.md, references/improve.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## apm-github-repo
|
||||
|
||||
- **URL:** https://github.com/microsoft/apm
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md (digest: plugins/kyberforge/docs/research/docs/microsoft-apm/instructions-primitive-schema.md)
|
||||
- **Description:** apm's own Python source read for the Instruction model, discovery globs and per-target integrators.
|
||||
- **Contributing files:** references/schema.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## apm-cli-0-28-0-experiments
|
||||
|
||||
- **URL:** https://pypi.org/project/apm-cli/0.28.0/
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md (digest: plugins/kyberforge/docs/research/docs/microsoft-apm/instructions-gotchas.md)
|
||||
- **Description:** The installed apm-cli 0.28.0 package plus throwaway install, compile and audit experiments confirming validation severity, unquoted-glob handling, discovery asymmetry, dedup and overwrite behaviour.
|
||||
- **Contributing files:** SKILL.md, references/schema.md, references/target-mapping.md, references/verify.md, references/create.md, references/improve.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## claude-code-memory-docs
|
||||
|
||||
- **URL:** https://code.claude.com/docs/en/memory
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md (digest: plugins/kyberforge/docs/research/docs/microsoft-apm/instructions-target-mapping.md)
|
||||
- **Description:** Claude Code memory documentation: `.claude/rules/` loading, the `paths` field as the only field read, invalid YAML ignored, size guidance.
|
||||
- **Contributing files:** SKILL.md, references/schema.md, references/target-mapping.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## github-copilot-custom-instructions-docs
|
||||
|
||||
- **URL:** https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md (digest: plugins/kyberforge/docs/research/docs/microsoft-apm/instructions-target-mapping.md)
|
||||
- **Description:** GitHub Copilot repository custom instructions: `.github/instructions/*.instructions.md`, `applyTo` and `excludeAgent`, the separate repo-wide file.
|
||||
- **Contributing files:** references/target-mapping.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## cursor-rules-docs
|
||||
|
||||
- **URL:** https://cursor.com/docs/context/rules
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md (digest: plugins/kyberforge/docs/research/docs/microsoft-apm/instructions-target-mapping.md)
|
||||
- **Description:** Cursor project rules: the `.mdc` requirement, `description`, `globs` and `alwaysApply`, rule types, size guidance.
|
||||
- **Contributing files:** references/target-mapping.md
|
||||
- **Status:** `extracted`
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
source_keys:
|
||||
- apm-cli-0-28-0-experiments
|
||||
- apm-docs-site
|
||||
- claude-code-memory-docs
|
||||
- github-copilot-custom-instructions-docs
|
||||
- cursor-rules-docs
|
||||
---
|
||||
|
||||
# What each target receives
|
||||
|
||||
Verified against apm 0.28.0 and throwaway installs. Reached from `SKILL.md` Step 2 when the question is which target keeps which field. Source-file syntax is in `references/schema.md`.
|
||||
|
||||
## Two output paths
|
||||
|
||||
`apm install` writes one native file per instruction into each target's rules directory. `apm compile` writes root context files that concatenate instruction bodies, grouped by `applyTo`. Treat install as the primary path for Claude Code and Copilot, and compile as the path for targets with no native instructions directory.
|
||||
|
||||
## Install: deployed path and transform
|
||||
|
||||
| Target | Deployed path | Transform |
|
||||
|---|---|---|
|
||||
| copilot | `.github/instructions/<n>.instructions.md` | Verbatim copy |
|
||||
| claude | `.claude/rules/<n>.md` | `applyTo` becomes a `paths:` list; `description` dropped; no frontmatter at all without `applyTo` |
|
||||
| cursor | `.cursor/rules/<n>.mdc` | `applyTo` becomes `globs`; `description` kept; no `alwaysApply` written |
|
||||
| windsurf | `.windsurf/rules/<n>.md` | `trigger: glob` plus `globs`, or `trigger: always_on`; `description` dropped |
|
||||
| kiro | `.kiro/steering/<n>.md` | `inclusion: fileMatch` plus `fileMatchPattern`, or `inclusion: always`; `description` dropped |
|
||||
| antigravity | `.agents/rules/<n>.md` | `trigger: glob` plus `globs`, or no frontmatter; `description` dropped |
|
||||
| grok-build | `.grok/rules/<n>.instructions.md` | Verbatim copy |
|
||||
| codex, gemini, opencode and the rest | none | Reach instructions only through compile |
|
||||
|
||||
Windsurf, Kiro, Antigravity and Cursor do not deploy at user scope.
|
||||
|
||||
## Field survival
|
||||
|
||||
| Field | Claude | Copilot | Cursor | Windsurf, Kiro, Antigravity | Compiled root file |
|
||||
|---|---|---|---|---|---|
|
||||
| `applyTo` | as `paths` | verbatim | as `globs` | as each target's glob key | grouping only |
|
||||
| `description` | dropped | kept | kept | dropped | dropped |
|
||||
| `author`, `version` | dropped | kept only because the file is verbatim | dropped | dropped | dropped |
|
||||
|
||||
For Claude Code the body's first line or heading is the only descriptive text that survives, so the body must explain itself.
|
||||
|
||||
## Ownership and overwrite
|
||||
|
||||
- Claude, Cursor, Windsurf, Kiro and Antigravity treat each deployed file as apm-owned: install replaces a hand-authored file at the same path without a prompt. Copilot skips an unmanaged file ("local files exist, not managed by APM") until `apm install --force`.
|
||||
- Removing or renaming a source makes the next install delete the file it deployed.
|
||||
|
||||
## Compile
|
||||
|
||||
- `--target claude` writes `CLAUDE.md`; Gemini writes `GEMINI.md` and `AGENTS.md`; every other target writes `AGENTS.md`.
|
||||
- Compile skips instructions already deployed natively, for Claude, Copilot and Antigravity only. With rules populated, `--target claude` exits 0, prints "produced no output files" and writes nothing. `--force-instructions` (alias `--no-dedup`) overrides.
|
||||
- Cursor, Windsurf, Kiro, Grok, Codex and OpenCode have no dedup: compile writes `AGENTS.md` that repeats rules the tool already loads natively.
|
||||
- A compile with no instruction primitives exits 0.
|
||||
|
||||
## Native format facts
|
||||
|
||||
- Claude Code reads `.claude/rules/**/*.md` recursively. `paths` is the only field it reads, as a list or a comma-separated string; other fields are ignored. A rule without `paths` loads at every launch. Frontmatter that fails to parse is ignored and the rule loads without `paths`.
|
||||
- Copilot path-specific files need `applyTo` as a quoted comma-joined string; `excludeAgent` is the only other documented key. Repository-wide instructions are the separate `.github/copilot-instructions.md`.
|
||||
- Cursor ignores a plain `.md` in `.cursor/rules`. A rule with only a `description` is "Apply Intelligently", not always-on.
|
||||
|
||||
## Unverified
|
||||
|
||||
Cursor's handling of a YAML-list `globs`, Copilot's handling of unknown frontmatter keys, and runtime behaviour on Windsurf, Kiro and Antigravity. Say so rather than asserting any of them.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source_keys:
|
||||
- apm-cli-0-28-0-experiments
|
||||
---
|
||||
|
||||
# Verifying a file in a throwaway package
|
||||
|
||||
Reached from `SKILL.md` Step 3. Run step 2 outside the repo: `apm install` writes `apm_modules/`, `apm.lock.yaml` and a rules directory, and install overwrites hand-authored rule files without warning.
|
||||
|
||||
1. From the package root, a real compile, never `--validate`:
|
||||
|
||||
```bash
|
||||
apm compile --dry-run --target claude
|
||||
```
|
||||
|
||||
Resolve every warning it prints: missing `description`, missing `applyTo`, empty content, broken link, "Failed to parse".
|
||||
|
||||
2. For a scoped rule, deploy it where nothing else can be overwritten:
|
||||
|
||||
```bash
|
||||
d=$(mktemp -d)
|
||||
printf 'name: scratch\nversion: 0.1.0\ntype: instructions\ntargets:\n - claude\n' > "$d/apm.yml"
|
||||
mkdir -p "$d/.apm/instructions"
|
||||
cp <package-root>/.apm/instructions/<name>.instructions.md "$d/.apm/instructions/"
|
||||
(cd "$d" && apm install && cat .claude/rules/<name>.md)
|
||||
```
|
||||
|
||||
3. The deployed file must open with `paths:` listing the intended globs. No frontmatter block at all means `applyTo` was missing or did not parse: the rule would load in every session.
|
||||
|
||||
4. To check the compiled root file instead, compile in that same clean directory *before* installing, or pass `--force-instructions`; after an install, `--target claude` writes nothing.
|
||||
|
||||
Delete the directory afterwards. Report only what was observed; Cursor's list-form `globs` and the Windsurf, Kiro and Antigravity runtimes stay unverified.
|
||||
@@ -0,0 +1,3 @@
|
||||
# scripts/
|
||||
|
||||
- **`new-instructions.sh <name> <root>`** — scaffolds `<package-root>/.apm/instructions/<name>.instructions.md` from `assets/templates/instructions.md`. Walks up from `<root>` for the nearest `type:`-bearing `apm.yml`; exits 1 with a pointer to `/apm-workflow configure` when there is none. Never overwrites an existing file. Run `--help` for the full contract.
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEMPLATE="$SKILL_DIR/../assets/templates/instructions.md"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: new-instructions.sh <name> <root>
|
||||
|
||||
Scaffold an apm instructions file from the bundled template.
|
||||
|
||||
Arguments:
|
||||
name Kebab-case stem. Becomes <name>.instructions.md and, after install,
|
||||
the deployed rule's filename.
|
||||
root Existing path at or below the target package. The script walks up for
|
||||
the nearest apm.yml with a top-level type: field (instructions, skill,
|
||||
hybrid or prompts); an apm.yml without type: is a marketplace-only
|
||||
manifest and is skipped. Creates
|
||||
<package-root>/.apm/instructions/<name>.instructions.md
|
||||
|
||||
Exit codes:
|
||||
0 File created, or already existed (no-op)
|
||||
1 Invalid arguments, missing root, no package found, or template not found
|
||||
USAGE
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Error: name and root are required." >&2
|
||||
echo "" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NAME="$1"
|
||||
ROOT="${2/#\~/$HOME}"
|
||||
|
||||
if ! grep -qE '^[a-z0-9]+(-[a-z0-9]+)*$' <<< "$NAME"; then
|
||||
echo "Error: name must use lowercase letters, numbers, and hyphens only." >&2
|
||||
echo " No leading, trailing, or consecutive hyphens." >&2
|
||||
echo " Received: '$NAME'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$TEMPLATE" ]]; then
|
||||
echo "Error: template not found at '$TEMPLATE'." >&2
|
||||
echo " Run this script from its original location inside the instructions-author skill." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$ROOT" ]]; then
|
||||
echo "Error: root directory '$ROOT' does not exist." >&2
|
||||
exit 1
|
||||
fi
|
||||
ROOT="$(cd "$ROOT" && pwd)"
|
||||
|
||||
# Same marker as agent-author's new-agent.sh: a top-level type: naming one of
|
||||
# the four package types, with matching quotes if quoted.
|
||||
is_apm_package_manifest() {
|
||||
local apm_yml="$1" line
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$line" =~ ^type:[[:space:]]*(instructions|skill|hybrid|prompts)([[:space:]]|$) ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$line" =~ ^type:[[:space:]]*([\"\'])(instructions|skill|hybrid|prompts)([\"\'])([[:space:]]|$) ]] \
|
||||
&& [[ "${BASH_REMATCH[1]}" == "${BASH_REMATCH[3]}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done < "$apm_yml"
|
||||
return 1
|
||||
}
|
||||
|
||||
PACKAGE_ROOT=""
|
||||
current="$ROOT"
|
||||
while true; do
|
||||
if [[ -f "$current/apm.yml" ]] && is_apm_package_manifest "$current/apm.yml"; then
|
||||
PACKAGE_ROOT="$current"
|
||||
break
|
||||
fi
|
||||
if [[ -e "$current/.git" ]]; then
|
||||
break
|
||||
fi
|
||||
parent="$(dirname "$current")"
|
||||
[[ "$parent" == "$current" ]] && break
|
||||
current="$parent"
|
||||
done
|
||||
|
||||
if [[ -z "$PACKAGE_ROOT" ]]; then
|
||||
echo "Error: no apm package found at or above '$ROOT'." >&2
|
||||
echo " Instructions only deploy from a package's .apm/instructions/. Run" >&2
|
||||
echo " /apm-workflow configure (apm plugin init) there first, then retry." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEST_DIR="$PACKAGE_ROOT/.apm/instructions"
|
||||
DEST="$DEST_DIR/$NAME.instructions.md"
|
||||
|
||||
if [[ -f "$DEST" ]]; then
|
||||
echo "Skipping '$DEST' — already exists." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$DEST_DIR"
|
||||
cp "$TEMPLATE" "$DEST"
|
||||
echo "Created: $DEST" >&2
|
||||
echo "" >&2
|
||||
echo "Next steps:" >&2
|
||||
echo " 1. Fill in $DEST — replace every FILL IN: placeholder and delete the comments." >&2
|
||||
echo " 2. Check '$NAME' does not collide with a hand-authored rule: install overwrites" >&2
|
||||
echo " <target>/rules/$NAME.* on most targets without warning." >&2
|
||||
echo " 3. Verify with a real compile, not --validate: apm compile --dry-run --target <target>" >&2
|
||||
@@ -0,0 +1,15 @@
|
||||
# tests/
|
||||
|
||||
- **`new-instructions.bats`** — covers `scripts/new-instructions.sh` (name validation, package walk-up, no-package refusal, no-op on an existing file, template placeholders) and the apm behaviour the skill's gotchas rest on, run against throwaway packages: a filled scaffold compiles into `CLAUDE.md` and installs into `.claude/rules/` with `description` dropped, compile writes nothing once rules are installed, an unquoted `applyTo` installs unscoped, and `--validate` hides the warnings a real compile prints.
|
||||
|
||||
## Dependencies
|
||||
|
||||
The test file loads `bats-support` and `bats-assert` from the repo root's `tests/test_helper/`, and runs on the repo's bats submodule at `tests/bats/`. The first `bash tests/run-bats.sh` initialises the submodules.
|
||||
|
||||
The apm tests need the `apm` CLI on `PATH` and skip when it is absent. They assert apm 0.28.0 behaviour, so a failure after an apm upgrade is a finding about the skill's gotchas, not a flaky test.
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
tests/bats/bin/bats plugins/kyberforge/.apm/skills/instructions-author/tests/new-instructions.bats
|
||||
```
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../../../../../" && pwd)"
|
||||
load "$REPO_ROOT/tests/test_helper/bats-support/load"
|
||||
load "$REPO_ROOT/tests/test_helper/bats-assert/load"
|
||||
|
||||
SCRIPT="$(cd "$BATS_TEST_DIRNAME/../scripts" && pwd)/new-instructions.sh"
|
||||
ROOT="$(mktemp -d)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -rf "$ROOT"
|
||||
}
|
||||
|
||||
make_package() {
|
||||
printf 'name: my-package\nversion: 0.1.0\ntype: instructions\ntargets:\n - claude\n' > "$ROOT/apm.yml"
|
||||
}
|
||||
|
||||
# Replace every placeholder and drop the template's comments, leaving a valid file.
|
||||
fill() {
|
||||
sed -i -E \
|
||||
-e '/^#/{/^# FILL IN/!d}' \
|
||||
-e 's/^description: FILL IN.*/description: Python style rules/' \
|
||||
-e 's/^applyTo: .*/applyTo: "**\/*.py"/' \
|
||||
-e 's/^# FILL IN.*/# Python style/' \
|
||||
-e 's/^- FILL IN.*/- Use type hints./' \
|
||||
"$1"
|
||||
}
|
||||
|
||||
need_apm() {
|
||||
command -v apm >/dev/null 2>&1 || skip "apm CLI not installed"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Help and arguments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "--help exits 0" {
|
||||
run bash "$SCRIPT" --help
|
||||
assert_success
|
||||
assert_output --partial "Usage:"
|
||||
}
|
||||
|
||||
@test "missing arguments exits 1" {
|
||||
run bash "$SCRIPT"
|
||||
assert_failure
|
||||
assert_output --partial "name and root are required"
|
||||
}
|
||||
|
||||
@test "nonexistent root exits 1" {
|
||||
run bash "$SCRIPT" my-rule "$ROOT/missing"
|
||||
assert_failure
|
||||
assert_output --partial "does not exist"
|
||||
}
|
||||
|
||||
@test "rejects names that are not kebab-case" {
|
||||
make_package
|
||||
for bad in My-Rule my_rule -leading trailing- double--hyphen; do
|
||||
run bash "$SCRIPT" "$bad" "$ROOT"
|
||||
assert_failure
|
||||
assert_output --partial "lowercase letters"
|
||||
done
|
||||
assert [ ! -d "$ROOT/.apm" ]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Package resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "creates <name>.instructions.md under .apm/instructions/" {
|
||||
make_package
|
||||
run bash "$SCRIPT" my-rule "$ROOT"
|
||||
assert_success
|
||||
assert [ -f "$ROOT/.apm/instructions/my-rule.instructions.md" ]
|
||||
}
|
||||
|
||||
@test "walks up from a subdirectory to the package root" {
|
||||
make_package
|
||||
mkdir -p "$ROOT/deep/er"
|
||||
run bash "$SCRIPT" my-rule "$ROOT/deep/er"
|
||||
assert_success
|
||||
assert [ -f "$ROOT/.apm/instructions/my-rule.instructions.md" ]
|
||||
assert [ ! -d "$ROOT/deep/er/.apm" ]
|
||||
}
|
||||
|
||||
@test "skips a type-less apm.yml and keeps walking up" {
|
||||
make_package
|
||||
mkdir -p "$ROOT/marketplace"
|
||||
printf 'name: catalog\nmarketplace:\n packages: []\n' > "$ROOT/marketplace/apm.yml"
|
||||
run bash "$SCRIPT" my-rule "$ROOT/marketplace"
|
||||
assert_success
|
||||
assert [ -f "$ROOT/.apm/instructions/my-rule.instructions.md" ]
|
||||
assert [ ! -d "$ROOT/marketplace/.apm" ]
|
||||
}
|
||||
|
||||
@test "accepts a quoted type value" {
|
||||
printf 'name: p\ntype: "hybrid"\n' > "$ROOT/apm.yml"
|
||||
run bash "$SCRIPT" my-rule "$ROOT"
|
||||
assert_success
|
||||
assert [ -f "$ROOT/.apm/instructions/my-rule.instructions.md" ]
|
||||
}
|
||||
|
||||
@test "no package: exits 1, points at apm-workflow configure, writes nothing" {
|
||||
mkdir -p "$ROOT/.git"
|
||||
run bash "$SCRIPT" my-rule "$ROOT"
|
||||
assert_failure
|
||||
assert_output --partial "apm-workflow configure"
|
||||
assert [ ! -d "$ROOT/.apm" ]
|
||||
}
|
||||
|
||||
@test "does not walk above a .git boundary" {
|
||||
make_package
|
||||
mkdir -p "$ROOT/repo/.git"
|
||||
run bash "$SCRIPT" my-rule "$ROOT/repo"
|
||||
assert_failure
|
||||
assert [ ! -d "$ROOT/.apm" ]
|
||||
}
|
||||
|
||||
@test "no-op when the file already exists" {
|
||||
make_package
|
||||
mkdir -p "$ROOT/.apm/instructions"
|
||||
echo "existing" > "$ROOT/.apm/instructions/my-rule.instructions.md"
|
||||
run bash "$SCRIPT" my-rule "$ROOT"
|
||||
assert_success
|
||||
run cat "$ROOT/.apm/instructions/my-rule.instructions.md"
|
||||
assert_output "existing"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Template contents
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "scaffold carries FILL IN placeholders and a quoted applyTo" {
|
||||
make_package
|
||||
bash "$SCRIPT" my-rule "$ROOT"
|
||||
file="$ROOT/.apm/instructions/my-rule.instructions.md"
|
||||
run grep -c 'FILL IN' "$file"
|
||||
assert_success
|
||||
run grep -E '^applyTo: "' "$file"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# apm behaviour the skill's gotchas rest on (verified against apm 0.28.0)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "filled scaffold compiles for the Claude target into CLAUDE.md without the description" {
|
||||
need_apm
|
||||
make_package
|
||||
bash "$SCRIPT" my-rule "$ROOT"
|
||||
fill "$ROOT/.apm/instructions/my-rule.instructions.md"
|
||||
cd "$ROOT"
|
||||
run apm compile --target claude
|
||||
assert_success
|
||||
assert [ -f "$ROOT/CLAUDE.md" ]
|
||||
run grep -F 'Use type hints.' "$ROOT/CLAUDE.md"
|
||||
assert_success
|
||||
run grep -F 'Python style rules' "$ROOT/CLAUDE.md"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "install deploys .claude/rules with paths: and drops the description" {
|
||||
need_apm
|
||||
make_package
|
||||
bash "$SCRIPT" my-rule "$ROOT"
|
||||
fill "$ROOT/.apm/instructions/my-rule.instructions.md"
|
||||
cd "$ROOT"
|
||||
run apm install
|
||||
assert_success
|
||||
rule="$ROOT/.claude/rules/my-rule.md"
|
||||
assert [ -f "$rule" ]
|
||||
run grep -F 'paths:' "$rule"
|
||||
assert_success
|
||||
run grep -F '**/*.py' "$rule"
|
||||
assert_success
|
||||
run grep -F 'Python style rules' "$rule"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "once rules are installed, compile --target claude writes no CLAUDE.md and exits 0" {
|
||||
need_apm
|
||||
make_package
|
||||
bash "$SCRIPT" my-rule "$ROOT"
|
||||
fill "$ROOT/.apm/instructions/my-rule.instructions.md"
|
||||
cd "$ROOT"
|
||||
apm install
|
||||
run apm compile --target claude
|
||||
assert_success
|
||||
assert [ ! -f "$ROOT/CLAUDE.md" ]
|
||||
}
|
||||
|
||||
@test "an unquoted applyTo still installs, as a rule with no paths:" {
|
||||
need_apm
|
||||
make_package
|
||||
mkdir -p "$ROOT/.apm/instructions"
|
||||
printf -- '---\ndescription: x\napplyTo: **/*.py\n---\n# T\n\n- a\n' > "$ROOT/.apm/instructions/bad.instructions.md"
|
||||
cd "$ROOT"
|
||||
run apm install
|
||||
assert_success
|
||||
assert [ -f "$ROOT/.claude/rules/bad.md" ]
|
||||
run grep -F 'paths:' "$ROOT/.claude/rules/bad.md"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "compile --validate exits 0 and hides the warnings a real compile prints" {
|
||||
need_apm
|
||||
make_package
|
||||
mkdir -p "$ROOT/.apm/instructions"
|
||||
printf -- '---\napplyTo: "**/*.py"\n---\n' > "$ROOT/.apm/instructions/bare.instructions.md"
|
||||
cd "$ROOT"
|
||||
run apm compile --validate
|
||||
assert_success
|
||||
refute_output --partial "Missing 'description'"
|
||||
run apm compile --dry-run --target claude
|
||||
assert_success
|
||||
assert_output --partial "Missing 'description'"
|
||||
assert_output --partial "Empty content"
|
||||
}
|
||||
Reference in new issue
Block a user