feat(kyberforge): retarget forge skills to author/audit APM content #93

Merged
Defame1297 merged 14 commits from feat/89-apm-native-authoring into main 2026-08-12 11:48:50 +00:00
5 changed files with 211 additions and 40 deletions
Showing only changes of commit 922eff3960 - Show all commits

View File

@@ -14,12 +14,14 @@ Routes to one of two flows based on context: if no skill directory exists at the
## Placement
| Type | Path |
|------|------|
| Direct (available immediately after install) | `~/.agents/skills/<name>/` |
| Plugin (installed via marketplace) | `plugins/<plugin>/skills/<name>/` |
`scripts/new-skill.sh` resolves the mode automatically by walking up from the given path — see `SKILL.md` Step 1 for the full algorithm.
If the destination is inside a plugin directory, read `references/deployment-modes.md` — cache isolation rules apply.
| Mode | Path | Chosen when |
|------|------|-------------|
| Standalone | `<path>/<name>/` | No `apm.yml` with a top-level `type:` field is found walking up from `<path>`, before hitting `.git` or the filesystem root |
| Package (APM) | `<package-root>/.apm/skills/<name>/` | A type-bearing `apm.yml` is found at or above `<path>` — `<path>` just needs to be somewhere inside the package |
If the destination resolves inside an APM package, read `references/deployment-modes.md` — self-containment rules apply to `apm compile` output the same way they applied to plugin cache isolation.
## Usage
@@ -33,8 +35,8 @@ If the destination is inside a plugin directory, read `references/deployment-mod
|------|---------|
| `README.md` | Human-readable overview of the skill and its files |
| `SKILL.md` | Skill instructions for agents |
| `scripts/new-skill.sh` | Copies annotated templates to the destination to scaffold a new skill |
| `references/deployment-modes.md` | Plugin vs standalone differences and cache isolation rules (loaded on demand) |
| `scripts/new-skill.sh` | Walks up from the given path to resolve package vs standalone mode, then copies annotated templates to the resolved destination |
| `references/deployment-modes.md` | APM package vs standalone differences and self-containment/cache-isolation rules (loaded on demand) |
| `references/scripts.md` | Package runners, inline dependency patterns, and full script contract (loaded on demand) |
| `references/sources.md` | Upstream research sources and which skill files each contributed to |
| `assets/templates/SKILL.md` | Annotated SKILL.md template |

View File

@@ -38,6 +38,11 @@ Determine which flow to follow before touching the filesystem:
Signals include: grill session output, `/skill-audit` findings (PASS/FAIL punch list), inline user feedback, session context describing what went wrong.
**Before running the scaffold script**, judge whether the destination is meant to be inside an APM package — the script can't tell "no package here" apart from "package not scaffolded yet":
- Package intent but no `type:`-bearing `apm.yml` found at/above the destination (e.g. "add to my apm package", or a sibling `.apm/`/`apm.yml` exists nearby) → **stop**, tell the user to run `/apm-workflow configure` (`apm plugin init`, from inside the package directory) first, then retry. Don't fall through to standalone mode.
- Otherwise (a `~/`-rooted destination, or no package context implied) → run `scripts/new-skill.sh`; it resolves package vs. standalone automatically (see Step 1).
## Creating a new skill
### Prerequisites
@@ -51,33 +56,42 @@ Design for one coherent user intent — skills too narrow force multiple loads p
- [ ] A clear purpose — what specific task will this skill handle?
- [ ] Trigger scenarios — when should an agent activate it, including indirect cases?
- [ ] Skill name (kebab-case) and destination path
- [ ] Capture `git log --oneline -1` now, before touching the filesystem — Step 6 needs it to verify a real commit landed
- [ ] Capture `git log --oneline -1` now, before touching the filesystem — Step 7 needs it to verify a real commit landed
If any are missing, stop and ask the user before proceeding.
**Requires `/skill-audit`** — used in Step 6 for final validation. Both skills ship in the kyberforge plugin and are co-installed. If `/skill-audit` is unavailable, stop and ask the user to install the kyberforge plugin before continuing.
**Requires `/skill-audit`** — used in Step 7 for final validation. Both skills ship in the kyberforge plugin and are co-installed. If `/skill-audit` is unavailable, stop and ask the user to install the kyberforge plugin before continuing.
### Step 1 — Scaffold
Run the copy script with the skill name and destination directory:
Run the copy script with the skill name and a path inside or at the target:
```bash
bash scripts/new-skill.sh <skill-name> <destination-dir>
bash scripts/new-skill.sh <skill-name> <path>
```
The script walks up from `<path>` for a package boundary: an ancestor `apm.yml` with a top-level `type:` field (`instructions`/`skill`/`hybrid`/`prompts`) means **package mode** — scaffolds into `<package-root>/.apm/skills/<skill-name>/`, not under `<path>` (a subdirectory of the package works fine as `<path>`). A `type:`-less `apm.yml` is a marketplace-only manifest, skipped. Hitting `.git` or the filesystem root first means **standalone mode** — scaffolds directly into `<path>/<skill-name>/`, same as before.
Examples:
```bash
# Package mode — packages/my-pkg/apm.yml already has `type: skill`
bash scripts/new-skill.sh my-tool packages/my-pkg/
# Standalone mode — no apm.yml/.git above ~/.agents/skills/
bash scripts/new-skill.sh my-tool ~/.agents/skills/
bash scripts/new-skill.sh data-analyzer plugins/myplugin/skills/
```
This creates `<destination-dir>/<skill-name>/` with annotated templates ready to fill in.
The script prints which mode it used and where the skill landed — read its output.
If the destination is inside a plugin directory (path contains a `plugin.json`), read `references/deployment-modes.md` before adding any file references to SKILL.md.
In package mode, read `references/deployment-modes.md` before adding any file references to SKILL.md.
### Step 2 — Fill in SKILL.md
### Step 2 — Update `apm.yml` includes (package mode only)
Open `<destination-dir>/<skill-name>/SKILL.md`. Replace every `FILL IN:` placeholder.
Skip in standalone mode. In package mode, check the resolved package's `apm.yml`: if `includes:` is an explicit list (not `auto`), append `.apm/skills/<skill-name>/` to it if not already present, preserving YAML formatting. If `includes: auto` or the field is absent, do nothing — `auto` already covers the new skill. Use Read/Edit directly on `apm.yml`; this isn't part of `scripts/new-skill.sh`.
### Step 3 — Fill in SKILL.md
Open the new skill's `SKILL.md` (the path Step 1 printed). Replace every `FILL IN:` placeholder.
**Frontmatter**
@@ -97,7 +111,7 @@ Open `<destination-dir>/<skill-name>/SKILL.md`. Replace every `FILL IN:` placeho
- `metadata` — key-value map; use `author`, `version`, `category`; add `source_keys` now (see below) if research sources are in context
- `allowed-tools` — space-separated pre-approved tools; reduces permission prompts (experimental — support varies by client)
**`metadata.source_keys`** — if research sources are in context, list the relevant slugs here as you write the body; don't defer this to Step 5. Agents that fill in source_keys late tend to omit it entirely. Example:
**`metadata.source_keys`** — if research sources are in context, list the relevant slugs here as you write the body; don't defer this to Step 6. Agents that fill in source_keys late tend to omit it entirely. Example:
```yaml
metadata:
source_keys:
@@ -105,7 +119,7 @@ metadata:
- another-slug
```
**Embedding org-specific policy** — if a skill encodes a rule sourced from an org convention file (e.g. `core/instructions/*.md`), inline that content directly into the skill (SKILL.md or a `references/` file) rather than pointing to the file's path. Plugins must be self-contained and portable — the org file may not exist wherever the plugin is installed, and in this repo such files are meant to be deleted once their content is fully embedded downstream. Tag the inlined content with a `source_keys` entry using the same `references/sources.md` schema as Step 5, noting in the `Research doc:` field that the source is an org convention rather than a plugin research corpus entry, so provenance survives after the source file is gone.
**Embedding org-specific policy** — if a skill encodes a rule sourced from an org convention file (e.g. `core/instructions/*.md`), inline that content directly into the skill (SKILL.md or a `references/` file) rather than pointing to the file's path. Plugins must be self-contained and portable — the org file may not exist wherever the plugin is installed, and in this repo such files are meant to be deleted once their content is fully embedded downstream. Tag the inlined content with a `source_keys` entry using the same `references/sources.md` schema as Step 6, noting in the `Research doc:` field that the source is an org convention rather than a plugin research corpus entry, so provenance survives after the source file is gone.
**Body — include only what the agent lacks**
@@ -175,7 +189,7 @@ Keep `SKILL.md` under 500 lines; 5,000 tokens is the recommended body budget. Wh
- Move reference material to `references/<topic>.md` and load it conditionally
- Bundle repeated executable logic into `scripts/` rather than reinventing each run
### Step 3 — Add scripts (if needed)
### Step 4 — Add scripts (if needed)
Place executable scripts in `scripts/`. Critical rule: **no interactive prompts** — agents run non-interactive; blocking on TTY input hangs indefinitely. Accept all input via flags, env vars, or stdin.
@@ -183,7 +197,7 @@ If adding a script, read `references/scripts.md` first — it covers the full co
If no scripts are needed, delete `scripts/README.md` and the `scripts/` directory.
### Step 4 — Add references, assets, and tests (if needed)
### Step 5 — Add references, assets, and tests (if needed)
**`references/`** — additional documentation loaded on demand. One topic per file.
Reference conditionally from SKILL.md: `If <condition>, read references/<file>.md`.
@@ -198,7 +212,7 @@ not in `scripts/`. See `tests/README.md` for setup instructions.
If not needed, delete the placeholder READMEs and their directories.
### Step 5 — Populate or delete `references/sources.md`
### Step 6 — Populate or delete `references/sources.md`
If a research `sources.md` is present in the conversation context:
@@ -210,7 +224,7 @@ If a research `sources.md` is present in the conversation context:
If no research `sources.md` is in context, delete `references/sources.md`.
### Step 6 — Validate and close
### Step 7 — Validate and close
Before running the audit, confirm:
- [ ] Skill name matches the directory name exactly
@@ -218,7 +232,7 @@ Before running the audit, confirm:
- [ ] Body has at least one non-empty section
- [ ] No `FILL IN:` placeholders remain in any file
Run `/skill-audit` on `<destination-dir>/<skill-name>`.
Run `/skill-audit` on the skill directory Step 1 reported — either `<package-root>/.apm/skills/<skill-name>/` or `<path>/<skill-name>/`.
All FAIL findings must be resolved before the skill is considered done.

View File

@@ -5,7 +5,7 @@ source_keys:
# Deployment Modes
Skills deploy in two modes. Both resolve relative paths from the skill root — the SKILL.md body works the same in either. Differences only arise when referencing files *outside* the skill directory.
Skills deploy standalone, or as part of a package — either a legacy plugin-mode cache install or an APM (`apm.yml`-governed `.apm/` tree, compiled via `apm compile`). All resolve relative paths from the skill root — the SKILL.md body works the same in any of them. Differences only arise when referencing files *outside* the skill directory.
## Cache isolation (plugin mode)
@@ -19,6 +19,18 @@ plugins/kyberforge/skills/other-skill/ # breaks
Fix: duplicate the file into the skill's own `scripts/` or `assets/`. There is no plugin-level `shared/` mechanism — the spec defines no cross-skill sharing, and `../` paths are broken by construction.
## Compiled output (APM package mode)
For a package (an `apm.yml`-governed `.apm/` source tree), the deployable artifact is generated by `apm compile` per target harness — not produced by copying the raw `.apm/` directory wholesale the way a plugin cache install copies a plugin directory. The same self-containment rule still applies at the skill level: **file references inside `.apm/skills/<name>/` must not reach outside that skill's own directory.**
```
../other-skill/validate.sh # breaks
.apm/skills/other-skill/ # breaks
../../shared/utils.sh # breaks
```
Fix: duplicate the file into the skill's own `scripts/` or `assets/`, same as plugin mode. `apm.yml`'s `includes:` list (when explicit, not `auto`) controls what gets published from the package, but it is not a cross-skill sharing mechanism — each skill directory must still stand alone.
## Env vars (plugin mode only)
These variables are injected when the plugin is loaded from an install cache. They are **not available in standalone mode.**
@@ -36,7 +48,7 @@ Deployed directly to `~/.agents/skills/<name>/`. No plugin context, no env vars
## Cross-tool portability
`SKILL.md` is portable — the same file works in Claude Code and Copilot CLI. Agent definitions and manifest files (`plugin.json`, `hooks.json`) are tool-specific and must be authored separately per tool.
`SKILL.md` is portable — the same file works in Claude Code and Copilot CLI, whether deployed standalone or compiled from an APM package. `apm.yml` is the source manifest: it is itself tool-agnostic (one file describes the package regardless of target), but `apm compile` produces per-target compiled output — a Claude Code plugin tree, a Copilot CLI tree, etc. — from it. Legacy hand-authored manifest files (`plugin.json`, `hooks.json`) are tool-specific and authored separately per tool; they sit outside the `apm.yml`-based flow.
## Shared assets between skills

View File

@@ -6,22 +6,42 @@ TEMPLATES_DIR="$SKILL_DIR/../assets/templates"
usage() {
cat <<EOF
Usage: new-skill.sh <skill-name> <destination-dir>
Usage: new-skill.sh <skill-name> <path>
Create a new skill scaffold by copying annotated templates to the destination.
Create a new skill scaffold by copying annotated templates to the resolved
destination. <path> is any existing path inside or at the target — a
package or a standalone location. It does not have to be a package root
itself.
The script walks up from <path> to pick one of two modes:
Package mode:
If an apm.yml with a top-level 'type:' field (instructions, skill,
hybrid, or prompts) is found at or above <path>, the skill is
scaffolded into <package-root>/.apm/skills/<skill-name>/ — not under
<path> itself. An apm.yml with no 'type:' field is a marketplace-only
manifest, not a package; it is skipped and the walk continues upward.
Standalone mode:
If the walk reaches a '.git' directory or the filesystem root without
finding a type-bearing apm.yml, the skill is scaffolded directly into
<path>/<skill-name>/, exactly as <path> was given.
Arguments:
skill-name Kebab-case skill identifier (e.g. my-tool, data-analyzer).
Must match the directory name exactly.
destination-dir Parent directory to create the skill in.
Examples: ~/.agents/skills/ plugins/myplugin/skills/
skill-name Kebab-case skill identifier (e.g. my-tool, data-analyzer).
Must match the directory name exactly.
path Any existing path inside/at the target. Used to locate the
package (package mode) or as the literal parent directory
(standalone mode). Must already exist.
Examples: ~/.agents/skills/ packages/my-pkg/some/subdir/
Output:
Creates <destination-dir>/<skill-name>/ with annotated templates ready to fill in.
Package mode: <package-root>/.apm/skills/<skill-name>/
Standalone mode: <path>/<skill-name>/
Exit codes:
0 Scaffold created successfully, or destination already exists (no-op)
1 Invalid arguments, missing destination parent, or templates not found
1 Invalid arguments, missing path, or templates not found
EOF
}
@@ -31,14 +51,14 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
fi
if [[ $# -lt 2 ]]; then
echo "Error: skill-name and destination-dir are required." >&2
echo "Error: skill-name and path are required." >&2
echo "" >&2
usage >&2
exit 1
fi
SKILL_NAME="$1"
DEST_DIR="$2"
TARGET_INPUT="$2"
# Validate skill name format
if ! echo "$SKILL_NAME" | grep -qE '^[a-z0-9]+(-[a-z0-9]+)*$'; then
@@ -55,13 +75,58 @@ if [[ ! -d "$TEMPLATES_DIR" ]]; then
exit 1
fi
# Validate destination exists
if [[ ! -d "$DEST_DIR" ]]; then
echo "Error: destination directory '$DEST_DIR' does not exist." >&2
# Validate path exists
if [[ ! -d "$TARGET_INPUT" ]]; then
echo "Error: path '$TARGET_INPUT' does not exist." >&2
exit 1
fi
TARGET="$DEST_DIR/$SKILL_NAME"
# ---------------------------------------------------------------------------
# Walk up from <path> looking for a type-bearing apm.yml (package mode) or a
# .git boundary / filesystem root (standalone mode). An apm.yml with no
# top-level 'type:' field is a marketplace-only manifest — skip it and keep
# walking up. Prints two lines: the resolved root, then the mode.
# ---------------------------------------------------------------------------
find_package_root() {
local current
current="$(cd "$1" && pwd)"
while true; do
if [[ -f "$current/apm.yml" ]]; then
if grep -qE '^type:[[:space:]]*(instructions|skill|hybrid|prompts)\b' "$current/apm.yml"; then
echo "$current"
echo "package"
return 0
fi
# apm.yml exists but has no type: field — marketplace-only manifest.
# Not a package match; keep walking up.
fi
if [[ -d "$current/.git" ]]; then
echo "$current"
echo "no-package"
return 0
fi
local parent
parent="$(dirname "$current")"
if [[ "$parent" == "$current" ]]; then
echo "$current"
echo "no-package"
return 0
fi
current="$parent"
done
}
# `mapfile`/`readarray` are bash 4.0+ builtins with no fallback on macOS's
# stock /bin/bash 3.2 — read the two output lines individually instead.
WALK_OUTPUT="$(find_package_root "$TARGET_INPUT")"
PKG_ROOT="$(echo "$WALK_OUTPUT" | sed -n '1p')"
MODE="$(echo "$WALK_OUTPUT" | sed -n '2p')"
if [[ "$MODE" == "package" ]]; then
TARGET="$PKG_ROOT/.apm/skills/$SKILL_NAME"
else
TARGET="$TARGET_INPUT/$SKILL_NAME"
fi
# Destination already exists — treat as a no-op so retries are safe
if [[ -d "$TARGET" ]]; then
@@ -69,6 +134,8 @@ if [[ -d "$TARGET" ]]; then
exit 0
fi
mkdir -p "$(dirname "$TARGET")"
# Copy templates to destination
cp -r "$TEMPLATES_DIR" "$TARGET"
@@ -77,7 +144,16 @@ sed -i "s/SKILL_NAME/$SKILL_NAME/g" "$TARGET/SKILL.md"
sed -i "s/SKILL_NAME/$SKILL_NAME/g" "$TARGET/README.md"
sed -i "s/SKILL_NAME/$SKILL_NAME/g" "$TARGET/tests/README.md"
echo "Scaffold created: $TARGET" >&2
if [[ "$MODE" == "package" ]]; then
echo "Mode: package — type-bearing apm.yml found at '$PKG_ROOT'" >&2
echo "Scaffold created: $TARGET" >&2
echo "" >&2
echo "Note: if '$PKG_ROOT/apm.yml' has an explicit 'includes:' list (not 'auto')," >&2
echo " add '.apm/skills/$SKILL_NAME/' to it." >&2
else
echo "Mode: standalone — no type-bearing apm.yml found above '$TARGET_INPUT'" >&2
echo "Scaffold created: $TARGET" >&2
fi
echo "" >&2
echo "Next steps:" >&2
echo " 1. Fill in $TARGET/SKILL.md — replace all FILL IN: placeholders" >&2

View File

@@ -117,3 +117,70 @@ teardown() {
assert_success
assert_output --partial "nothing to do"
}
# ---------------------------------------------------------------------------
# Mode detection: package vs standalone
# ---------------------------------------------------------------------------
@test "standalone mode is chosen when no apm.yml or .git is found above path" {
run bash "$SCRIPT" my-tool "$DEST"
assert_success
assert_output --partial "Mode: standalone"
assert [ -d "$DEST/my-tool" ]
}
@test "package mode: writes into <package-root>/.apm/skills/<name>, not under the given subdir" {
mkdir -p "$DEST/pkg/sub/deep"
cat > "$DEST/pkg/apm.yml" <<'EOF'
name: my-pkg
version: 1.0.0
type: skill
EOF
run bash "$SCRIPT" my-tool "$DEST/pkg/sub/deep"
assert_success
assert_output --partial "Mode: package"
assert [ -d "$DEST/pkg/.apm/skills/my-tool" ]
assert [ ! -d "$DEST/pkg/sub/deep/my-tool" ]
}
@test "package mode scaffold contains SKILL.md with name substituted" {
mkdir -p "$DEST/pkg"
cat > "$DEST/pkg/apm.yml" <<'EOF'
name: my-pkg
version: 1.0.0
type: hybrid
EOF
bash "$SCRIPT" my-tool "$DEST/pkg"
assert [ -f "$DEST/pkg/.apm/skills/my-tool/SKILL.md" ]
run grep "my-tool" "$DEST/pkg/.apm/skills/my-tool/SKILL.md"
assert_success
}
@test "walk-up skips a type-less apm.yml (marketplace-only) and finds a real package root further up" {
mkdir -p "$DEST/mid/sub"
cat > "$DEST/apm.yml" <<'EOF'
name: root-pkg
version: 1.0.0
type: skill
EOF
cat > "$DEST/mid/apm.yml" <<'EOF'
marketplace:
owner: acme
packages: []
EOF
run bash "$SCRIPT" my-tool "$DEST/mid/sub"
assert_success
assert_output --partial "Mode: package"
assert [ -d "$DEST/.apm/skills/my-tool" ]
assert [ ! -d "$DEST/mid/.apm" ]
}
@test "walk-up stops at .git boundary and uses standalone mode at the given path" {
mkdir -p "$DEST/repo/.git"
mkdir -p "$DEST/repo/sub"
run bash "$SCRIPT" my-tool "$DEST/repo/sub"
assert_success
assert_output --partial "Mode: standalone"
assert [ -d "$DEST/repo/sub/my-tool" ]
assert [ ! -d "$DEST/repo/my-tool" ]
}