feat(kyberforge): execute plugin-to-apm marketplace conversion

Why:
ADR-0015 established that Microsoft APM (apm.yml + .apm/) should replace
this repo's hand-authored plugin.json/marketplace.json model, with those
files becoming compiled output of `apm pack` instead of files edited by
hand via the (now-retired) plugin-author/marketplace-author skills.
Issue #90 was the deferred execution of that decision, gated on #88
(apm tooling) and #89 (apm-native agent-author/skill-author routing).

Implementation notes:
- All six plugins (bin, core, git, gitea, kyberforge, lint) now carry
  apm.yml + .apm/{skills,agents,hooks} as their authoring source. Skills
  moved with a plain git mv (content-identical across targets). Agents
  were re-authored, not moved: per ADR-0016, .apm/agents/*.agent.md
  compiles verbatim to both Claude and Copilot, so plugin-scope agents
  now carry only name/description/model/source_keys -- no tools: field,
  no Claude-only knobs (isolation, maxTurns, effort, memory,
  permissionMode).
- Root apm.yml registers all 7 marketplace packages (6 local plus
  mattpocock-skills as a remote entry) under versioning: per_package,
  matching this repo's existing independent-plugin-versioning practice.
- .claude-plugin/marketplace.json and every plugin's plugin.json are now
  apm-pack-compiled output, verified against the prior hand-maintained
  content: same names/descriptions/versions/licenses/authors, only
  cosmetic serialization differences (JSON key order, owner email vs.
  url, Unicode escaping).
- plugin-author and marketplace-author are retired now that apm-based
  authoring fully replaces their job; kyberforge bumped 1.3.1 -> 1.4.0
  for that removal, and the root marketplace catalog bumped
  0.3.1 -> 0.3.2 to match, per the version-bump convention now
  documented in apm-workflow's reference docs instead of a dedicated
  script (apm has no native version-bump automation).
- Fixed hardcoded pre-.apm/ path assumptions across
  .pre-commit-config.yaml, .pre-commit-hooks.yaml,
  scripts/check-scope-walkup-sync.sh, scripts/sync-vale-styles.sh,
  scripts/check-vale-style-sync.sh, six plugins' root plugin.json
  (stale skills/hooks/agents pointer fields that check-manifests.sh
  validates), and several tests/*.bats and tests/*.sh fixtures --
  including a bats REPO_ROOT relative-path depth bug (10 files, one
  extra .apm/ directory level to walk up) and a vale probe-path
  isolation regression introduced mid-fix.
- Corrected empirically-wrong assumptions surfaced this session in
  apm-workflow/apm-install's own reference docs: `apm marketplace
  package add` does not accept local paths (only owner/repo remote
  shorthand -- local packages are registered by editing apm.yml's
  marketplace.packages[] directly); `apm compile` is a consumer-side
  AGENTS.md/CLAUDE.md generator, not the plugin.json producer, and
  hard-fails on skill/agent-only packages without --clean; `apm plugin
  init <name>` nests a stray subdirectory when run with a positional
  name arg from inside a same-named directory; no native Copilot
  marketplace output profile exists; .mcp.json is merged into the
  compiled plugin.json content-aware and target-scoped, with no
  dependencies.mcp entry needed for simple passthrough; pipx is the
  correct pip fallback on externally-managed Python environments.
- Renamed agent-author's copilot.agent.md template asset to
  copilot.agent.md.template so apm compile's recursive *.agent.md glob
  stops misparsing the placeholder template as a real agent primitive.

Impact:
plugin.json and marketplace.json are compiled artifacts from here on --
editing them by hand is no longer the workflow; edit apm.yml/.apm/ and
run apm pack. CONTEXT.md's Plugin/Plugin marketplace glossary entries
reflect this. ADR-0001 is marked superseded, ADR-0006 moot, and
ADR-0010 updated for the new .apm/agents/ path (project/user scope
unaffected, per ADR-0016). Full local verification: claude plugin
validate --strict on all 6 plugins, apm audit --ci, apm marketplace
check, check-manifests.sh, and the full test suite (165/165 bats,
13/13 shell scripts) all pass clean.

Fixes: #90
Refs: #88, #89
ADR: 0015
ADR: 0016

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ub96PyaSRD9BHPktotj1pC
This commit is contained in:
2026-08-12 18:09:37 +00:00
parent 50d5c30a3c
commit 5e232503c4
289 changed files with 741 additions and 1974 deletions

View File

@@ -1,33 +0,0 @@
# skills/
Skills for this plugin. Each skill lives in its own subdirectory:
```
skills/
<skill-name>/
SKILL.md # required — frontmatter + skill body
META.md # required — provenance and audit fields
references/ # optional — on-demand reference docs
scripts/ # optional — executable helper scripts
assets/ # optional — templates, data files, lookup tables
```
**Shared** — both Claude Code and GitHub Copilot CLI read `skills/<name>/SKILL.md`.
To create or improve a skill, run `/skill-author` in a Claude Code session. To review a skill
without modifying it, run `/skill-audit`. Do not write SKILL.md by hand without following the
authoring standard — trigger descriptions and body discipline are required.
## Skills
| Skill | Description |
|---|---|
| `skill-author` | Create or improve a skill from scratch, audit findings, or inline feedback |
| `skill-audit` | Audit a skill directory against the agentskills.io spec and produce a findings report |
| `agent-author` | Author an agent definition file |
| `pc-author` | Create, add, remove, and configure `.pre-commit-config.yaml` |
| `pc-run` | Install, run, autoupdate, and maintain the pre-commit setup |
| `plugin-author` | (Deprecated — see apm-workflow) Create, update, and release plugin scaffolds for the holocron marketplace |
| `marketplace-author` | (Deprecated — see apm-workflow) Add, remove, and update plugin entries in the holocron marketplace manifest |
| `apm-install` | Install or upgrade the apm CLI and set up the agent runtimes it drives (Copilot CLI, Codex, Gemini, generic llm) |
| `apm-workflow` | Author apm.yml, scaffold an apm package/marketplace, install dependencies, and compile/pack/publish/audit apm content |

View File

@@ -1,57 +0,0 @@
# agent-audit
Audits an agent definition for correctness and quality — a single vendor-neutral file at
plugin/APM scope, or a Claude Code and Copilot file pair at project/user scope.
## What it does
At **plugin/APM scope**, accepts the single `.apm/agents/<name>.agent.md` file — there is no
counterpart. Structural checks via `validate.sh` hard-`FAIL` any frontmatter field outside the
vendor-neutral allowlist (`name`, `description`, `model`, `source_keys` — the last for
provenance tracking, checked separately by `validate-provenance.sh` against `sources.md`; see
ADR-0016), since `apm compile`
copies frontmatter verbatim to both harnesses and an unsafe field can't be silently dropped for
just one of them.
At **project/user scope**, accepts either file in a CC `.md` / Copilot `.agent.md` pair, derives
the counterpart automatically, and validates both. Runs structural checks via `validate.sh`
(required fields, kebab-case name, no placeholders, no CC-only fields in the Copilot file, no
Copilot-only fields in the CC file), provenance chain validation via `validate-provenance.sh`
(checks `source_keys` against `sources.md` at the plugin root — plugin/APM scope only), then
qualitative checks on description phrasing and system prompt quality. Step 1 also runs a
Vale-based prose sub-check via `vale-wrap.sh` against both files of the pair, using the
`Kyberforge` style (both files) and `KyberforgeCopilot` style (Copilot file only) — every alert
is a `FAIL`, cited by rule ID — falling back to Step 2 judgment when the `vale` binary is
unavailable or reports `0 files` scanned. Produces a compact findings report in the same format
as `skill-audit`.
## Usage
```
/agent-audit
```
Pass the path to either agent file as the argument.
## Files
| File | Purpose |
|------|---------|
| `SKILL.md` | Skill instructions for agents |
| `assets/vale/.vale.ini` | Vale config: scopes `Kyberforge` to `**/agents/*.md`, `Kyberforge`+`KyberforgeCopilot` to `**/*.agent.md` |
| `assets/vale/styles/Kyberforge/DescriptionOpener.yml` | Flags descriptions opening with "This skill/agent" instead of an imperative "Use when..." |
| `assets/vale/styles/Kyberforge/PaddingPhrase.yml` | Flags generic "see references/ for info" pointers instead of specific file references |
| `assets/vale/styles/Kyberforge/SentenceOpenerThereIs.yml` | Flags sentences opening with "There is/are" instead of naming the subject directly |
| `assets/vale/styles/Kyberforge/VagueWording.yml` | Flags vague capability wording ("helps with", "utilize", "assists with", "used for") in descriptions |
| `assets/vale/styles/KyberforgeCopilot/ProactivePhrase.yml` | Flags CC-specific "Use proactively" phrasing with no effect in Copilot descriptions |
| `references/README.md` | Directory documentation for references/ |
| `references/description-quality.md` | Qualitative guide for borderline description findings |
| `references/field-inventory.md` | Authoritative list of valid CC and Copilot agent fields |
| `references/sources.md` | Research provenance for skill content |
| `scripts/README.md` | Directory documentation for scripts/ |
| `scripts/validate.sh` | Structural validation script for agent file pairs |
| `scripts/validate-provenance.sh` | Provenance chain validation script for agent pairs against `sources.md` (plugin root) |
| `scripts/vale-wrap.sh` | Drop-in `vale` wrapper that works around a frontmatter-description NLP scope limitation |
| `tests/README.md` | Bats test dependency and run instructions |
| `tests/validate.bats` | Bats tests for validate.sh |
| `tests/validate-provenance.bats` | Bats tests for validate-provenance.sh |

View File

@@ -1,124 +0,0 @@
---
name: agent-audit
description: >
Use when the user wants to review an agent definition they wrote, says "audit this
agent", "check if my agent follows best practices", "review my agent file", or wants
to know if an agent pair is ready to ship — even if they don't use the word "audit".
Also invoke proactively after directly hand-editing an agent file pair outside
agent-author — an unaudited hand-edit is the same risk as unreviewed code.
Audits a Claude Code .md and Copilot .agent.md agent file pair across six dimensions:
structural validation, provider safety, description quality, body quality, comment
discipline, and pair consistency — plus provenance chain validation. Produces a
compact findings report
(findings only, no PASS noise) with Why and Fix per finding. Do not use to fix agent
files — use /agent-author instead. Do not use to audit SKILL.md files — use
/skill-audit instead.
allowed-tools: Bash Read
metadata:
category: factory
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- claude-code-subagents-docs
- context7-github-en-copilot
- github-custom-agents-configuration
---
## Gotchas
- The unit of authoring at project/user scope is always a pair (CC `.md` + Copilot `.agent.md`). A missing counterpart is a FAIL under the kyberforge project convention at those scopes — neither the CC nor the Copilot platform itself requires a counterpart file. Label such findings as project convention violations, not platform spec failures. **At plugin/APM scope there is no pair** — the unit of authoring is a single vendor-neutral `.apm/agents/<name>.agent.md` file, and Pair Consistency does not apply there at all (see below).
- Scope is detected by walking up from the agent file's directory: at each level, if `apm.yml` exists AND contains a top-level `type: instructions|skill|hybrid|prompts` line, that directory is an APM package root — plugin/APM scope. A `type:`-less `apm.yml` is marketplace-only (see `docs/research/docs/microsoft-apm/monorepo-and-repo-shapes.md`) — skip it and keep walking up. Otherwise, if `.git` is a directory at that level, stop there — project scope. If neither is found before the filesystem root, fall back to user scope at `$HOME`. `plugin.json`/`.claude-plugin/plugin.json` are no longer scope signals for this skill — a directory with only a `plugin.json` and no `apm.yml` falls through to project (or user) scope.
- `references/field-inventory.md` must exist for `validate.sh` to run. The script exits with an error if it is missing.
- Do not output findings while auditing — gather internally, surface in Step 3 report.
## Step 1 — Run structural validation
```bash
bash scripts/validate.sh <path-to-agent-file>
bash scripts/validate-provenance.sh <path-to-agent-file>
scripts/vale-wrap.sh <path-to-cc-file> <path-to-copilot-file> # project/user scope
scripts/vale-wrap.sh <path-to-apm-agent-file> # plugin/APM scope — single file
```
The script accepts either the CC file, the Copilot file, or (at plugin/APM scope) the single `.apm/agents/<name>.agent.md` file. It detects provider from extension and scope from the walk-up above, then runs the checks for that scope.
At **project/user scope** it derives the counterpart and runs the existing pair-based checks. Note FAILs and SUGGESTIONs for the `### Structure` and `### Provider safety` report dimensions. Findings about missing fields, bad name format, empty body, or missing frontmatter → `### Structure`. Findings about CC-only fields in a Copilot file, Copilot-only fields in a CC file, body length, or subagent-unavailable tools → `### Provider safety`. A missing counterpart file → `### Pair consistency`.
At **plugin/APM scope** there is no counterpart — the script instead checks the single file's frontmatter against the `apm-agent-allowlist` in `references/field-inventory.md` (`name`, `description`, `model`, `source_keys` — nothing else; `source_keys` is provenance metadata, not a provider-specific field, and is validated separately by `validate-provenance.sh` against `sources.md`). Findings about missing fields, bad name format, name/filename-stem mismatch, empty body, or missing frontmatter → `### Structure`, same as project/user scope. Findings about any field outside the allowlist (e.g. `tools`, or any Claude-only/Copilot-only field carried over from a hand-edit) and body length → `### Provider safety` — but the dimension's meaning shifts here: it is no longer a CC-vs-Copilot field-leakage check, it's a vendor-neutral-field-allowlist check, since `apm compile` verbatim-copies this file's frontmatter to every target and there is no per-target integrator to reconcile a CC-only or Copilot-only field (ADR-0016). `### Pair consistency` never applies at this scope — the script never emits a missing-counterpart FAIL here, because there is nothing to pair by design.
`vale-wrap.sh` ships inside this skill's own `scripts/` — resolve it relative to this skill's directory the same way `scripts/validate.sh` is resolved above, so the invocation works whether this skill is running from this repo or from an installed plugin cache. Pass no `--config`: handed none, the wrapper loads its own sibling `assets/vale/.vale.ini`, located from the script's path rather than from the cwd. Adding an explicit relative `--config` breaks exactly the case the self-location covers — a resolved script path plus an unresolved config path yields `E100 Runtime error ... does not exist`, exit 2, which the fallback below then misreads as "vale unavailable". At project/user scope, run it against both files of the pair (not just the one passed in); at plugin/APM scope, run it against the single file. `Kyberforge` applies to all of these files via the `**/agents/*.md` glob; `KyberforgeCopilot` applies to any `*.agent.md` file — including the plugin/APM-scope file, which already has that extension — via the `**/*.agent.md` glob, since its one rule (`Use proactively`) flags CC-specific phrasing that's meaningless in a vendor-neutral or Copilot description. Every Vale alert is a `FAIL` — all rules are graded `error` — so report each one in the `### Description` / `### Body` dimensions citing its rule ID (e.g. `KyberforgeCopilot.ProactivePhrase`). Skip and fall back to Step 2 judgment if the `vale` binary is unavailable. If Vale reports `0 files` scanned, treat the pass as NOT RUN — not as clean — and fall back to full Step 2 judgment for the dimensions it would have covered.
`validate-provenance.sh` operates at plugin/APM scope only — it walks up from the agent file's directory the same way `validate.sh` does (nearest ancestor `apm.yml` with a top-level `type:` field; skip a `type:`-less marketplace-only `apm.yml`; stop at `.git` or the filesystem root) and exits 0 silently if that walk doesn't land on a package root, or when no provenance data exists. When it does apply, it validates the chain between the single file's own `source_keys` and the package-scoped `sources.md` (package root — see ADR-0010). Note FAILs from this script for the `### Provenance` dimension — surface them verbatim with Why and Fix.
If the scripts cannot run (Bash denied, python3 unavailable), perform checks manually. At project/user scope: counterpart file exists, required fields present (`name`, `description`, non-empty body), `name` is kebab-case, Copilot CLI `.agent.md` `name` must match filename stem (CC files are exempt — the CC platform does not require name to match filename), no `FILL IN:` placeholders, no CC-only fields in Copilot file, no Copilot-only fields in CC file (read `references/field-inventory.md` for the authoritative field lists). At plugin/APM scope: required fields present (`name`, `description`, non-empty body), `name` is kebab-case and matches the filename stem, no `FILL IN:` placeholders, no frontmatter field outside `name`/`description`/`model`/`source_keys` (read the `apm-agent-allowlist` section of `references/field-inventory.md`; `source_keys` carries provenance metadata, checked separately by `validate-provenance.sh` against `sources.md`).
## Step 2 — Qualitative checks
Read both agent files. Work through each dimension internally. Collect findings only; report in Step 3.
**Description (both files):**
- Action-verb opening: description starts with a verb ("Reviews...", "Analyzes...", "Generates...") — FAIL if absent. Vale's `Kyberforge.DescriptionOpener` alert flags the specific known-bad "This agent..." opener directly; verifying an arbitrary opening word is genuinely a strong verb still requires judgment.
- Specificity: is the trigger condition stated precisely? — SUGGESTION if vague. Vale's `Kyberforge.VagueWording` alert covers known filler ("helps with", "utilize", ...) directly; report those as FAILs without re-deriving by judgment.
- `Use proactively` in a Copilot description: Vale's `KyberforgeCopilot.ProactivePhrase` alert (Copilot file only) flags this directly — report it without re-deriving by judgment.
If a description finding is borderline, read `references/description-quality.md`.
**Body:**
- Direct role instruction: system prompt opens with `You are a [role]. When invoked, [action].` — SUGGESTION if absent
- One job per agent: system prompt describes a single bounded task — SUGGESTION if scope appears unbounded
- Generic, non-specific reference pointers to the `references/` directory: Vale's `Kyberforge.PaddingPhrase` alert flags this directly — report it without re-deriving by judgment
- Sentences that open with "There is"/"There are": Vale's `Kyberforge.SentenceOpenerThereIs` alert flags this directly — report it without re-deriving by judgment
**Body/Frontmatter comments:**
- Inspect each comment block in the YAML frontmatter. For each comment, apply: *"Would the agent get this wrong without this comment?"* Flag any that answer "no" as padding.
- Look for patterns like `# Optional. <long explanation>` or extensive inline guidance (more than 1–2 lines per field) that should be condensed or removed before shipping.
- This mirrors skill-audit's body-discipline check but applies to template documentation in the frontmatter — template guidance belongs in development; agent-ready files should have minimal comments.
**Pair consistency (cross-file) — project/user scope only:**
- Both files exist — FAIL if counterpart is missing (kyberforge project convention; not a platform requirement from either CC or Copilot — label as such)
- The following checks are covered automatically by `validate.sh`; apply them manually only when the script cannot run: both system prompt bodies non-empty — FAIL if either is empty
- **Does not apply at plugin/APM scope** — there is only one file, by design; do not raise a Pair Consistency finding there under any circumstance.
**Unexpressable Claude-only behavior — plugin/APM scope only:**
- Read the description and body. If either implies a need the vendor-neutral frontmatter can no longer express — tool restriction, `isolation`, `memory`, or another Claude-only behavior that a hand-authored CC file could have declared — flag it as a SUGGESTION, never a FAIL. This is a known upstream schema limitation (APM's agent primitive has no per-target compile integrator, so `tools:`/`isolation`/etc. can't be emitted safely to both CC and Copilot — ADR-0016), not an authoring mistake. The finding exists to give the author visibility into the gap, not to imply the schema can be made to do something it can't.
- Example: a body that says "only use Read and Grep, never Edit" but the frontmatter has no `tools` field to enforce it — SUGGESTION, not FAIL.
## Step 3 — Report
Open with a coverage line. At project/user scope:
```text
Checked: structure · provider-safety · description · body · comment-discipline · pair-consistency · provenance
```
At plugin/APM scope, omit `pair-consistency` — it does not apply when there is no pair:
```text
Checked: structure · provider-safety · description · body · comment-discipline · provenance
```
Then output only dimensions that have findings, grouped under H3 headings, FAILs before SUGGESTIONs within each dimension. Omit clean dimensions entirely. `### Provenance` findings are sourced verbatim from `validate-provenance.sh` output — copy them without rephrasing.
For each finding:
```text
FAIL/SUGGESTION <finding> — file:line
Why: <why this is a problem>
Fix: <exact change — quote before/after where applicable>
```
Close with:
```text
## Result
PASS
PASS · P info
PASS (N suggestions)
PASS (N suggestions) · P info
FAIL (N fails · M suggestions)
FAIL (N fails · M suggestions) · P info
Run /agent-author to address findings.
```
Omit `Run /agent-author to address findings.` when there are no findings at all. Do not apply fixes — report and propose only.

View File

@@ -1,7 +0,0 @@
StylesPath = styles
[**/agents/*.md]
BasedOnStyles = Kyberforge
[**/*.agent.md]
BasedOnStyles = Kyberforge, KyberforgeCopilot

View File

@@ -1,7 +0,0 @@
extends: existence
message: "Description opens with '%s' — use an imperative 'Use when...' opener instead"
level: error
scope: text.frontmatter.description
ignorecase: true
raw:
- '^This (skill|agent)\b'

View File

@@ -1,7 +0,0 @@
extends: existence
message: "Generic reference pointer: '%s' — use the specific 'If X, read `references/file.md`' form instead"
level: error
scope: text
ignorecase: true
raw:
- 'see references?/? for (more )?(info|information|details)\b'

View File

@@ -1,7 +0,0 @@
extends: existence
message: "Don't start a sentence with '%s' — name the subject directly"
level: error
scope: sentence
ignorecase: false
raw:
- '^There\s(is|are)\b'

View File

@@ -1,10 +0,0 @@
extends: existence
message: "Vague capability wording: '%s' — state the capability precisely instead"
level: error
scope: text.frontmatter.description
ignorecase: true
tokens:
- helps with
- utilize
- assists with
- used for

View File

@@ -1,7 +0,0 @@
extends: existence
message: "'%s' is CC-specific phrasing with no effect in Copilot descriptions — remove it"
level: error
scope: text.frontmatter.description
ignorecase: true
tokens:
- Use proactively

View File

@@ -1,15 +0,0 @@
---
source_keys: []
---
# references/
Additional documentation agents load on demand.
## Files
| File | Purpose |
|------|---------|
| `description-quality.md` | Qualitative guide for borderline description findings — action-verb rules, specificity criteria, proactive-use caveat, length limits. |
| `field-inventory.md` | Canonical list of valid CC and Copilot agent definition fields. Load when the script needs authoritative field lists for structural validation. |
| `sources.md` | Research provenance records for skill content. Load only when tracing the origin of a specific rule or field constraint. |

View File

@@ -1,49 +0,0 @@
---
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- claude-code-subagents-docs
- context7-github-en-copilot
- github-custom-agents-configuration
---
# Agent Description Quality Reference
Load this file when a description finding is borderline and you need to make a precise call.
## Action-verb opening
The description must open with an imperative or present-tense verb that describes what the agent does ("Reviews...", "Audits...", "Generates...", "Analyzes..."). Avoid:
- Noun phrases: "An agent that..." — no verb
- "This agent..." or "Use this when..." — passive framing
- "Helps with..." — too vague to be a clear verb
**Borderline call:** "Validates and reviews..." is acceptable — two verbs is fine if both are specific. "Assists in reviewing..." is not — "assists" is vague filler.
## Specificity of trigger condition
The description must state what specifically triggers the agent. Generic phrasing fails:
- Too vague: "when the user needs help with agents"
- Acceptable: "when the user says 'audit this agent', 'check if my agent follows best practices', or wants to know if an agent pair is ready to ship"
Include indirect triggers: "even if they don't use the word 'audit'" or "even if the user doesn't phrase it as a review request". If the agent should activate on a recognisable user goal (not just literal keyword matches), name that goal.
**Borderline call:** If the description covers direct triggers but omits common indirect phrasings that a user would plausibly use, mark as SUGGESTION (not FAIL) — the agent still activates, just less reliably.
## `Use proactively`
For CC files: including "Use proactively" signals the CC runtime to offer the agent unprompted when conditions are met. This is CC-specific — use it when the agent should activate without an explicit user request.
For Copilot files: this phrase has no effect. Use `user-invocable: false` / `disable-model-invocation: true` for equivalent Copilot behavior. Flag `Use proactively` in a Copilot description as a SUGGESTION (not FAIL) — it causes no harm, just has no effect.
## Length and hard limits
- CC agent descriptions: no documented character limit, but keep under 500 characters to avoid truncation in UI contexts.
- Copilot agent descriptions: no separate documented limit, but the overall 30,000-character body limit applies to the full file.
- Skill descriptions (SKILL.md): hard 1024-character limit enforced by the platform.
## Do not use when
Include a "Do not use when..." clause only if a near-miss agent or skill exists that could steal activations. Omitting it is not a finding. Including it is correct when there is a real confusion risk (e.g., `/agent-audit` vs `/skill-audit`).
**Borderline call:** If the "Do not use when" clause is present but the exclusion described is already obvious from context, mark as SUGGESTION to tighten or remove — not FAIL.

View File

@@ -1,28 +0,0 @@
---
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- claude-code-subagents-docs
- context7-github-en-copilot
- github-custom-agents-configuration
---
## claude-code-fields
name description tools disallowedTools model effort maxTurns permissionMode skills mcpServers hooks memory background isolation color initialPrompt
## claude-code-only-fields
maxTurns isolation memory permissionMode effort hooks mcpServers disallowedTools skills initialPrompt color background
## copilot-fields
name description tools target model disable-model-invocation user-invocable mcp-servers metadata
## copilot-only-fields
target disable-model-invocation user-invocable mcp-servers metadata
## apm-agent-allowlist
name description model source_keys

View File

@@ -1,90 +0,0 @@
---
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- claude-code-subagents-docs
- context7-github-en-copilot
- github-custom-agents-configuration
---
# Sources
## context7-websites-code-claude
- **URL:** context7:/websites/code_claude
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code documentation site indexed by Context7 — plugin manifest schema, subagent definition types, marketplace JSON format, agent markdown file format
- **Contributing files:** SKILL.md, references/field-inventory.md, references/description-quality.md
- **Status:** `extracted`
## claude-code-plugins-docs
- **URL:** https://code.claude.com/docs/en/plugins
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code plugin authoring guide — plugin structure, manifest fields, loading methods, skill namespacing, agent activation, marketplace submission
- **Contributing files:** SKILL.md, references/field-inventory.md, references/description-quality.md
- **Status:** `extracted`
## claude-code-subagents-docs
- **URL:** https://code.claude.com/docs/en/sub-agents
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code subagent reference — definition format, all frontmatter fields, scope priority, built-in agents, CLI flags, environment variables, known limitations
- **Contributing files:** SKILL.md, references/field-inventory.md, references/description-quality.md
- **Status:** `extracted`
## context7-github-en-copilot
- **URL:** context7:/websites/github_en_copilot
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Official GitHub Copilot documentation indexed by Context7; covers CLI plugins, custom agents, SDK, and marketplace
- **Contributing files:** SKILL.md, references/field-inventory.md, references/description-quality.md
- **Status:** `extracted`
## github-custom-agents-configuration
- **URL:** https://docs.github.com/en/copilot/reference/custom-agents-configuration
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Reference for cloud and IDE custom agent definition format — frontmatter fields, tool aliases, MCP server config, secrets interpolation, scoping hierarchy
- **Contributing files:** SKILL.md, references/field-inventory.md, references/description-quality.md
- **Status:** `extracted`
## github-cli-plugin-reference
- **URL:** https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-plugin-reference
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Full CLI plugin reference — plugin.json schema, marketplace.json schema, all CLI commands and flags, install specification formats, loading precedence, env vars, LSP config
- **Contributing files:** (none)
- **Status:** `extracted`
## github-plugins-creating
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-creating
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** How-to for creating Copilot CLI plugins — plugin structure, agent and skill authoring, hooks format, MCP config, development lifecycle
- **Contributing files:** (none)
- **Status:** `extracted`
## github-plugins-finding-installing
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-finding-installing
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** User-facing guide to discovering and installing CLI plugins — marketplace browsing commands, install/update/uninstall workflow
- **Contributing files:** (none)
- **Status:** `extracted`
## github-plugins-marketplace
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-marketplace
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** How-to for creating and publishing a plugin marketplace — marketplace.json structure, hosting options, registration commands
- **Contributing files:** (none)
- **Status:** `extracted`
## github-sdk-custom-agents
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-sdk/features/custom-agents
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** SDK custom agent API — CustomAgentConfig fields in all five languages, session config, sub-agent lifecycle events, tool scoping, permission handling
- **Contributing files:** (none)
- **Status:** `extracted`

View File

@@ -1,47 +0,0 @@
# scripts/
Executable code bundled with this skill. Agents run scripts in this directory
to perform repeatable operations rather than reinventing the logic each run.
## When to add a script
Add a script when agents independently reinvent the same logic across runs —
building the same parser, chart, or validation routine from scratch each time.
Bundle it here once, tested and reliable.
## Script requirements (agentskills.io)
Scripts must be designed for non-interactive, agentic execution:
- **No interactive prompts** — agents run in non-interactive shells.
Accept all input via flags, env vars, or stdin. A script that blocks on
TTY input hangs indefinitely.
- **Expose `--help`** — this is how agents learn your script's interface.
Keep the output concise; it enters the agent's context window.
- **Structured output** — write data (JSON, CSV, TSV) to stdout.
Write progress, warnings, and diagnostics to stderr.
- **Idempotent** — prefer "create if not exists" over "create and fail on
duplicate". Agents may retry on failure.
- **Meaningful exit codes** — `0` for success, non-zero for failure.
Use distinct codes for different failure types; document them in `--help`.
- **Dry-run support** — add `--dry-run` for destructive operations.
## Self-contained scripts
Bundle dependencies inline so the agent can run the script with a single command.
Python (PEP 723 + uv):
```python
# /// script
# dependencies = ["requests>=2.31,<3"]
# requires-python = ">=3.11"
# ///
import requests
```
```bash
uv run scripts/my-script.py
```
## If no scripts are needed
Delete this README and the `scripts/` directory entirely.

View File

@@ -1,526 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Works around a Vale limitation: the `text.frontmatter.description` NLP scope
# silently stops matching once the `description:` value spans 2+ physical lines
# in any form YAML joins back into one string — a `>`/`>-`/`>+` folded block
# scalar (the style used by most skills/agents in this repo), a plain scalar
# wrapped onto continuation lines, or a double- or single-quoted scalar wrapped
# the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed
# value keeps exactly the line breaks the source has, and vale matches it fine
# (verified against vale 3.15.2), so literal blocks are deliberately left alone.
# This script flattens an affected description to a one-line scalar in a scratch
# copy — or, for the rare value no inline scalar can spell out verbatim, to a
# `|-` literal block with a single content line, which vale matches just as well
# (padding with blank lines so every other line number is unchanged), then
# runs the real `vale` binary against the copies. Drop-in replacement for calling
# `vale` directly: same args, same exit code, bar the two documented divergences
# below.
#
# "Same args" means relative paths — path arguments and the values of the
# path-valued flags (`--config`, `--output`, `--path`) alike — resolve against
# the caller's current directory, exactly as bare `vale` resolves them. The flag
# values are rewritten to absolute form because the run ends up `cd`'d into the
# scratch mirror, where a relative one would no longer resolve. (An earlier
# version resolved path arguments against the repo root, an invented convention
# that hard-errored on `--config ../../.vale.ini` from a subdirectory and, worse,
# silently dropped file arguments that didn't happen to resolve from the repo
# root — skipping the flattening this script exists for.)
#
# Divergence 1: with no `--config` at all, this script's own sibling
# `assets/vale/.vale.ini` is used instead of vale's upward search. pre-commit
# prefixes only `entry[0]` with the hook-repo clone path, so a `--config` in
# `.pre-commit-hooks.yaml` would resolve against the *consuming* repo and
# hard-fail (E100) for every external consumer. The manifest therefore passes the
# script alone, and an explicit `--config` from any other caller still wins.
#
# Divergence 2: a path-shaped argument that does not exist is a hard error
# (exit 2). Bare vale drops it, falls back to reading stdin, and prints
# `0 errors ... in stdin` with exit 0 — a typo'd target is then indistinguishable
# from a clean run. Both audit skills treat a `0 files` report as NOT RUN rather
# than clean, and `in stdin` does not match that guard, so the silent form would
# read as "prefilter clean" and skip the LLM fallback. Erroring is the only way
# to keep that guard honest. Linting prose piped on stdin is therefore
# unsupported here — it already was, since the no-path handoff closes stdin so
# vale can't block on a pipe that will never carry content.
#
# Vale prints each path exactly as it was handed to it, so the scratch tree
# mirrors the caller's absolute cwd: a relative path argument is passed through
# verbatim and resolves to its flattened copy, keeping the report byte-identical
# to bare `vale`'s. An absolute path inside the cwd is relativized to keep that
# property. Only an absolute path outside the cwd is rewritten to its scratch
# copy and so reports a scratch path — unavoidable, since a file can only be
# read from where it actually is.
cwd="$(pwd -P)"
# Every array below is expanded as `${arr[@]+"${arr[@]}"}`: bash before 4.4 —
# including the 3.2 that macOS still ships as /bin/bash — treats `"${arr[@]}"`
# on an empty array as an unbound variable under `set -u`. No expansion site is
# reachable while empty on today's control flow, so this is insurance against a
# later edit breaking that invariant, not a live fix.
vale_args=()
path_args=()
pending_flag=""
config_given=false
# `--output` takes either one of vale's built-in style names or a template file
# path. Only the file form needs absolutizing, and the built-in names have to be
# excluded by name *before* the existence test below: a file or directory
# literally called `line` in the caller's cwd would otherwise rewrite the
# built-in into `$cwd/line`, flipping vale into template mode (`E100 [template]
# Runtime error`) where bare vale just uses the built-in. `--path` has no such
# names — it is always a path — so the check is keyed on the flag too.
is_builtin_output() {
case "$2" in
line|JSON|CLI) [[ "$1" == "--output" ]] ;;
*) false ;;
esac
}
# Absolutizes a `--config` value against the caller's cwd. Shared by both
# argument forms below — separated (`--config X`) and joined (`--config=X`)
# — so the "already absolute vs. needs $cwd prefixed" check lives in exactly
# one place instead of being duplicated per form.
abs_config_value() {
if [[ "$1" == /* ]]; then
printf '%s' "$1"
else
printf '%s' "$cwd/$1"
fi
}
for arg in "$@"; do
if [[ -n "$pending_flag" ]]; then
# Value of a separated two-argv flag. It is never a lint target, however
# file-like it looks. The run ends up `cd`'d into the scratch mirror, so a
# value naming a file has to be absolutized here or it stops resolving.
case "$pending_flag" in
--config)
# Always a path, and required to exist.
vale_args+=("$(abs_config_value "$arg")")
;;
--output|--path)
# See `is_builtin_output` above for why the built-in `--output` names
# are excluded first. Anything that names nothing is passed through and
# left for vale to interpret.
if is_builtin_output "$pending_flag" "$arg"; then
vale_args+=("$arg")
elif [[ "$arg" != /* && -e "$arg" ]]; then
vale_args+=("$cwd/$arg")
else
vale_args+=("$arg")
fi
;;
*)
vale_args+=("$arg")
;;
esac
pending_flag=""
continue
fi
case "$arg" in
--config)
vale_args+=("$arg")
pending_flag="$arg"
config_given=true
continue
;;
--config=*)
vale_args+=("--config=$(abs_config_value "${arg#--config=}")")
config_given=true
continue
;;
# Same cwd-relative resolution for the `--flag=value` spelling of the two
# other path-valued flags.
--output=*|--path=*)
flag_val="${arg#*=}"
if is_builtin_output "${arg%%=*}" "$flag_val"; then
vale_args+=("$arg")
elif [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
vale_args+=("${arg%%=*}=$cwd/$flag_val")
else
vale_args+=("$arg")
fi
continue
;;
# Vale's remaining value-taking flags, per `vale --help` (3.x). In the
# separated two-argv form the value must not be classified as a lint target
# — `--output tmpl.tmpl` names a real template file, and treating it as
# input both lints the template and reorders argv so vale sees
# `--output --no-wrap`. The `--flag=value` form needs no entry here: it
# starts with `-` and falls through to vale untouched. A value flag added by
# some future vale release is simply absent from this list and lands back on
# today's behaviour, so this list going stale is never worse than not having
# it.
--ext|--filter|--glob|--minAlertLevel|--output|--path)
vale_args+=("$arg")
pending_flag="$arg"
continue
;;
# Vale's subcommands are bare words that name no file, so they would trip
# the not-found error below. A lint target literally named `sync` (no
# extension, no slash) is misread as the subcommand — accepted, because the
# alternative is failing every `vale-wrap.sh ls-config`.
ls-config|ls-dirs|ls-metrics|ls-vars|sync)
vale_args+=("$arg")
continue
;;
esac
if [[ "$arg" == -* ]]; then
vale_args+=("$arg")
continue
fi
# Everything left is a lint target: `vale [options] [input...]` has no third
# kind of argument. See divergence 2 above for why a missing one is fatal here.
if [[ ! -e "$arg" ]]; then
echo "vale-wrap.sh: no such file or directory: $arg" >&2
exit 2
fi
# An absolute path inside the caller's cwd is relativized so the report cites
# a path that resolves against the real tree. Left absolute, it would be
# rewritten to its scratch copy and printed as `/tmp/tmp.XXXX/...` — a real
# path to a file that is deleted on exit, which reads as a bug in any report
# quoting it. Absolute paths outside the cwd have no relative form and keep
# the scratch-path behaviour documented above.
if [[ "$arg" == "$cwd"/* ]]; then
path_args+=("${arg#"$cwd"/}")
else
path_args+=("$arg")
fi
done
if [[ "$config_given" == false ]]; then
vale_args+=(--config "$(cd "$(dirname "${BASH_SOURCE[0]}")/../assets/vale" && pwd)/.vale.ini")
fi
if [[ ${#path_args[@]} -eq 0 ]]; then
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
# block waiting on a pipe that will never carry content.
exec vale ${vale_args[@]+"${vale_args[@]}"} < /dev/null
fi
# `realpath -m` would be the obvious normalizer, but `-m` (canonicalize-missing)
# is a GNU extension the BSD realpath on macOS doesn't have — and every dest
# below is a path that doesn't exist yet. python3 is already a hard dependency.
abspath() {
python3 -c 'import os, sys; print(os.path.abspath(sys.argv[1]))' "$1"
}
flatten() {
# Two call shapes: `flatten src dest` (dest already resolved and inside the
# scratch tree — the per-markdown-file calls in the directory branch below)
# writes straight to `dest`. `flatten src raw_dest tmpdir` (the single-file
# branch further down) additionally resolves `raw_dest` the way a separate
# `abspath` call used to, applies the same sandbox-escape guard, and prints
# the resolved path — folding two python3 spawns per file into one.
python3 - "$@" <<'PYTHON'
import os
import re
import sys
src, dest_input = sys.argv[1], sys.argv[2]
tmpdir = sys.argv[3] if len(sys.argv) > 3 else None
if tmpdir is None:
dest = dest_input
else:
dest = os.path.abspath(dest_input)
if not dest.startswith(tmpdir + os.sep):
print(
f"vale-wrap.sh: refusing to lint '{src}': its scratch copy would "
f"land outside {tmpdir}",
file=sys.stderr,
)
sys.exit(2)
os.makedirs(os.path.dirname(dest), exist_ok=True)
# surrogateescape keeps a non-UTF-8 file (reachable via a directory argument)
# a byte-for-byte round trip instead of aborting the whole run on a decode error.
with open(src, encoding='utf-8', errors='surrogateescape') as fh:
content = fh.read()
# YAML 1.2 double-quoted escapes (spec 5.7 / 7.3.1). `\<newline>` is handled
# separately in unescape_double because it also swallows the next indentation.
DQ_ESCAPES = {
'0': '\0', 'a': '\a', 'b': '\b', 't': '\t', '\t': '\t', 'n': '\n',
'v': '\v', 'f': '\f', 'r': '\r', 'e': '\x1b', ' ': ' ', '"': '"',
'/': '/', '\\': '\\', 'N': '\x85', '_': '\xa0', 'L': '\u2028',
'P': '\u2029',
}
# First characters that make a plain (unquoted) scalar mean something other than
# text: YAML's c-indicator set.
PLAIN_UNSAFE_FIRST = '-?:,[]{}#&*!|>\'"%@`'
def unescape_double(text):
"""Decode a double-quoted YAML scalar's body to the string YAML parses."""
out = []
i = 0
while i < len(text):
char = text[i]
if char != '\\':
out.append(char)
i += 1
continue
i += 1
if i >= len(text):
break
esc = text[i]
if esc == '\n':
i += 1
while i < len(text) and text[i] in ' \t':
i += 1
continue
if esc in 'xuU':
width = {'x': 2, 'u': 4, 'U': 8}[esc]
digits = text[i + 1:i + 1 + width]
if len(digits) == width:
try:
out.append(chr(int(digits, 16)))
except ValueError:
pass
else:
i += 1 + width
continue
out.append(DQ_ESCAPES.get(esc, esc))
i += 1
return ''.join(out)
def close_quote(text, quote):
"""Index of the closing `quote` in `text`, which starts just past the
opening one. None while the scalar is still unterminated."""
i = 0
while i < len(text):
char = text[i]
if quote == '"' and char == '\\':
i += 2
continue
if char == quote:
if quote == "'" and text[i + 1:i + 2] == "'":
i += 2
continue
return i
i += 1
return None
def continuation_lines(rest):
"""Yield the physical lines of `rest` that continue the value started on the
`description:` line. Indentation-based and blank-line-tolerant, per YAML:
a blank line (any amount of whitespace) always stays inside; the indent is
set by the first content line; the value ends at the first line indented
less than that, at any line flush with the key (that is the next mapping
key, not a continuation), or at EOF."""
indent = None
for line in rest.splitlines(keepends=True):
text = line.rstrip('\n')
if text.strip() == '':
yield line
continue
line_indent = len(text) - len(text.lstrip(' \t'))
if line_indent == 0:
return
if indent is None:
indent = line_indent
elif line_indent < indent:
return
yield line
def emit(value):
"""Render `value` as a YAML scalar whose source text spells the value out
verbatim. Vale locates the description by matching the parsed value back
against the source, so a scalar carrying any escape — `''` in a
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
whole `text.frontmatter.description` scope vanish, the same failure this
script exists to work around. Verbatim forms only, therefore, tried in
descending order of fidelity. The first three occupy one physical line; the
`|-` fallback occupies two, which the caller accounts for when padding."""
if (value
and value[0] not in PLAIN_UNSAFE_FIRST
and ': ' not in value
and not value.endswith(':')
and ' #' not in value):
return value # plain: nothing needs escaping at all
if "'" not in value:
return "'" + value + "'" # single-quoted: only `'` would escape
if '"' not in value and '\\' not in value:
return '"' + value + '"' # double-quoted: only `"`/`\` would
# Last resort: the value needs quoting AND holds an apostrophe AND a double
# quote or backslash, so no *inline* scalar can carry it verbatim. A `|-`
# literal block can — a block scalar's body has no escape syntax at all, so
# `'`, `"`, `\` and `: ` all survive byte for byte, and vale still matches
# the description scope against it (the header above says the same of the
# `|` blocks this script deliberately leaves alone; verified against vale
# 3.15.2). One content line, indented two spaces, `-`-chomped so the parsed
# value is exactly `value` with no trailing newline.
return '|-\n ' + value
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
if fm_match:
fm = fm_match.group(2)
header_m = re.search(r'^description:[ \t]*', fm, re.MULTILINE)
else:
header_m = None
if header_m:
head_start = header_m.start()
value_start = header_m.end()
header_end = fm.find('\n', value_start)
header_end = len(fm) if header_end == -1 else header_end
first = fm[value_start:header_end]
body_start = header_end + 1
indicator = first.rstrip()
block_m = re.fullmatch(r'([|>])([+-]?[0-9]*|[0-9]*[+-]?)', indicator)
if block_m and block_m.group(1) == '|':
kind = None # literal blocks keep their line breaks; vale is fine
elif block_m:
kind = 'block' # folded (`>`): the value starts on the next line
elif indicator == '':
kind = 'block' # bare `description:`: a plain scalar on later lines
elif first[:1] == '"':
kind = 'double'
elif first[:1] == "'":
kind = 'single'
elif first[:1] in '#&*!':
kind = None # comment, anchor, alias or tag — not a plain scalar
else:
kind = 'plain'
text = ''
value_end = value_start
value_lines = 0
if kind in ('block', 'plain'):
body = ''.join(continuation_lines(fm[body_start:]))
value_end = body_start + len(body)
if kind == 'block':
text = body
value_lines = body.count('\n')
else:
text = fm[value_start:value_end]
value_lines = 1 + body.count('\n')
if ' #' in text or text.lstrip().startswith('#'):
# A `#` opens a comment inside a plain scalar. Folding it in
# would lint text YAML never treats as part of the value, so
# leave the file alone rather than lint the wrong string.
kind = None
elif kind in ('double', 'single'):
quote = '"' if kind == 'double' else "'"
inner_start = value_start + 1
acc = fm[inner_start:body_start]
idx = close_quote(acc, quote)
lines = continuation_lines(fm[body_start:])
while idx is None:
try:
acc += next(lines)
except StopIteration:
break
idx = close_quote(acc, quote)
if idx is None:
kind = None # unterminated quote: invalid YAML, leave it to vale
else:
inner = acc[:idx]
value_end = inner_start + idx + 1
text = unescape_double(inner) if quote == '"' else inner.replace("''", "'")
value_lines = 1 + inner.count('\n')
flat = re.sub(r'\s+', ' ', text).strip()
if kind and flat and value_lines >= 2:
# `value_end` can land mid-line, just past a closing quote, so extend to
# the end of that physical line and carry whatever follows (a trailing
# comment) across unchanged.
if value_end > 0 and fm[value_end - 1] == '\n':
span_end = value_end
trailer = ''
else:
newline = fm.find('\n', value_end)
span_end = len(fm) if newline == -1 else newline + 1
trailer = fm[value_end:span_end].rstrip('\n')
scalar = emit(flat)
# A trailing comment carried across from the original line stays on the
# `description:` line itself: after a block scalar's `|-` header it is
# still a comment, but inside the block body it would become part of the
# value.
head, newline_sep, block_body = scalar.partition('\n')
# The replacement displaces the whole span, so the blank-line pad makes
# up the difference between the lines it displaced and the lines it
# occupies — every later line number is unchanged. That is one line for
# the three inline forms and two for the `|-` block; the span itself is
# at least two lines here (`value_lines >= 2` is a precondition), so the
# pad count never goes negative.
pad = '\n' * (fm[head_start:span_end].count('\n') - 1 - scalar.count('\n'))
new_fm = (fm[:head_start] + 'description: ' + head + trailer
+ newline_sep + block_body + '\n' + pad + fm[span_end:])
content = (fm_match.group(1) + new_fm + fm_match.group(3)
+ content[fm_match.end():])
with open(dest, 'w', encoding='utf-8', errors='surrogateescape') as fh:
fh.write(content)
if tmpdir is not None:
print(dest)
PYTHON
}
tmpdir="$(cd "$(mktemp -d)" && pwd -P)"
trap 'rm -rf "$tmpdir"' EXIT
# Mirror of the caller's cwd inside the scratch tree; relative path arguments
# are resolved from here.
mirror="$tmpdir$cwd"
mkdir -p "$mirror"
argv_paths=()
for arg in ${path_args[@]+"${path_args[@]}"}; do
if [[ "$arg" == /* ]]; then
raw_dest="$tmpdir$arg"
else
raw_dest="$mirror/$arg"
fi
if [[ -d "$arg" ]]; then
dest="$(abspath "$raw_dest")"
# A path argument with enough leading `..` to climb past the mirror root would
# write outside the scratch dir. The real filesystem clamps such a path at
# `/`; the mirror can't, so refuse rather than scribble outside the sandbox.
case "$dest" in
"$tmpdir"/*) ;;
*)
echo "vale-wrap.sh: refusing to lint '$arg': its scratch copy would land outside $tmpdir" >&2
exit 2
;;
esac
mkdir -p "$(dirname "$dest")"
# A directory is mirrored whole — vale applies its own format filtering to
# the tree, so any file dropped here would be silently unlinted — and then
# every markdown file in the copy is flattened in place. `.git` is pruned:
# vale never lints it and copying it can dwarf the rest of the tree.
# `find -L` follows symlinks because vale does: it lints both a symlinked
# file and a file under a symlinked directory, and a bare `-type f` walk
# would report "0 files" where bare vale reports one. (A symlink loop makes
# `find` warn on stderr and carry on, which is also what vale does.) The
# second walk needs no `-L`: the mirror is all real files by construction.
mkdir -p "$dest"
while IFS= read -r -d '' rel; do
mkdir -p "$dest/$(dirname "$rel")"
cp "$arg/$rel" "$dest/$rel"
done < <(cd "$arg" && find -L . -name .git -prune -o -type f -print0)
while IFS= read -r -d '' md; do
flatten "$md" "$md"
done < <(find "$dest" -type f -name '*.md' -print0)
else
# `abspath` + `flatten` folded into one python3 process — see the comment
# atop `flatten` above.
dest="$(flatten "$arg" "$raw_dest" "$tmpdir")"
fi
if [[ "$arg" == /* ]]; then
argv_paths+=("$dest")
else
argv_paths+=("$arg")
fi
done
cd "$mirror"
vale ${vale_args[@]+"${vale_args[@]}"} ${argv_paths[@]+"${argv_paths[@]}"}

View File

@@ -1,290 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
Usage: validate-provenance.sh <agent-file>
Validate that an agent's sources provenance chain is complete and internally consistent.
Operates at plugin/APM scope only (a single vendor-neutral .apm/agents/<name>.agent.md
inside a package with a type:-bearing apm.yml) — exits 0 silently for project and user
scope agents.
Arguments:
agent-file Path to either the Claude Code .md or Copilot .agent.md agent file.
Exit codes:
0 All checks passed (or nothing to validate, or not plugin scope)
1 One or more checks failed
2 Script error (unrecognized file extension — expected .md or .agent.md)
Checks performed:
0 source_keys present in agent pair but sources.md absent
1 FILL IN: placeholders in sources.md
2 source_keys in agent files → slug exists in sources.md
3 Contributing files listed in sources.md exist on disk (plugin-root relative)
4 Contributing files back-reference the parent slug in their source_keys
5 Research doc field present and not placeholder
EOF
}
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage
exit 0
fi
if [[ $# -lt 1 ]]; then
echo "Error: agent-file is required." >&2
echo "" >&2
usage >&2
exit 1
fi
python3 -u - "$1" <<'PYTHON'
import sys
import os
import re
agent_file = os.path.abspath(sys.argv[1])
fname = os.path.basename(agent_file)
agent_dir = os.path.dirname(agent_file)
# --- Sanity-check extension (single vendor-neutral .agent.md file at plugin/APM scope) ---
if not (fname.endswith('.agent.md') or fname.endswith('.md')):
print(f"Error: unrecognized extension '{fname}' — expected .md or .agent.md", file=sys.stderr)
sys.exit(2)
# Matches a top-level `type:` line whose value is exactly one of the four
# package content types — identical to validate.sh's APM_TYPE_RE. Group 1's
# optional quote must be closed by \1 (or nothing), and the value must be
# followed by whitespace/end-of-line so a malformed value like `prompts-only`
# doesn't false-match on the `prompts` prefix.
TYPE_RE = re.compile(r"^type:\s*(['\"]?)(instructions|skill|hybrid|prompts)\1(?:\s|$)")
# --- Find package root: walk up for the nearest ancestor apm.yml that
# declares a top-level type: field. An apm.yml with no type: field is a
# marketplace-only manifest (see monorepo-and-repo-shapes.md) — skip it and
# keep walking. Stop at a $HOME boundary, a .git boundary, or the filesystem
# root: none of these is plugin/APM scope, so this script has nothing to
# check there.
def find_plugin_root(start_dir):
home = os.path.expanduser('~')
current = os.path.abspath(start_dir)
while True:
apm_yml = os.path.join(current, 'apm.yml')
if os.path.isfile(apm_yml):
with open(apm_yml) as f:
if any(TYPE_RE.match(line) for line in f):
return current
# $HOME is a non-plugin-scope boundary — checked before the .git test
# below (mirrors validate.sh's detect_scope ordering), so a
# dotfiles-managed $HOME (yadm, chezmoi bare-repo, etc.) can't shadow
# this check by being its own .git repo. Without this, the walk could
# continue past $HOME toward the filesystem root looking for a
# type-bearing apm.yml, misclassifying a user/project-scope file as
# plugin scope in rare ancestor layouts.
if current == home:
return None
# .git is a directory in a normal checkout but a file (`gitdir: ...`)
# in a git worktree — exists() covers both.
if os.path.exists(os.path.join(current, '.git')):
return None
parent = os.path.dirname(current)
if parent == current:
return None
current = parent
plugin_root = find_plugin_root(agent_dir)
if plugin_root is None:
sys.exit(0)
sources_md_path = os.path.join(plugin_root, 'sources.md')
# --- Helpers ---
PLACEHOLDER_RE = re.compile(r'(?<!`)FILL IN:[^`\n]')
def parse_frontmatter(content):
m = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
if not m:
return None, content
return m.group(1), content[m.end():]
def parse_source_keys(fm):
"""Extract top-level source_keys list from frontmatter string."""
if fm is None:
return []
keys = []
in_source_keys = False
for line in fm.splitlines():
if re.match(r'^source_keys:', line):
in_source_keys = True
continue
if in_source_keys:
m = re.match(r'^[ \t]+-\s+(\S+)', line)
if m:
keys.append(m.group(1).strip())
elif line and not line[0].isspace():
in_source_keys = False
return keys
def parse_h2_slugs(content):
return re.findall(r'^## (.+)$', content, re.MULTILINE)
def parse_contributing_files(content, slug):
pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL
)
m = pattern.search(content)
if not m:
return None
block = m.group(1)
cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE)
if not cf_m:
return None
return cf_m.group(1).strip()
def parse_research_doc(content, slug):
pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL
)
m = pattern.search(content)
if not m:
return None
block = m.group(1)
rd_m = re.search(r'^\- \*\*Research doc:\*\* (.+)$', block, re.MULTILINE)
if not rd_m:
return None
return rd_m.group(1).strip()
findings = []
has_fail = False
def emit_fail(desc, fpath, why, fix):
global has_fail
has_fail = True
findings.append(("FAIL", desc, fpath, why, fix))
def print_findings():
for kind, desc, fpath, why, fix in findings:
print(f"FAIL {desc} — {fpath}")
print(f" Why: {why}")
print(f" Fix: {fix}")
print()
# --- Collect source_keys from agent pair ---
def get_source_keys_from_file(fpath):
if not os.path.isfile(fpath):
return []
try:
with open(fpath) as f:
content = f.read()
except Exception:
return []
fm, _ = parse_frontmatter(content)
return parse_source_keys(fm)
# Plugin/APM scope is a single vendor-neutral file — no counterpart to merge.
given_keys = get_source_keys_from_file(agent_file)
all_source_keys = given_keys
sources_md_exists = os.path.isfile(sources_md_path)
# Early exit: nothing to validate
if not all_source_keys and not sources_md_exists:
sys.exit(0)
sources_content = None
sources_slugs = set()
if sources_md_exists:
with open(sources_md_path) as f:
sources_content = f.read()
sources_slugs = set(parse_h2_slugs(sources_content))
# --- Check 0: source_keys present but sources.md absent ---
if not sources_md_exists and all_source_keys:
rel_given = os.path.relpath(agent_file, plugin_root)
emit_fail(
"source_keys declared but sources.md is absent",
rel_given,
"source_keys references research provenance that has no sources index to validate against.",
"Create sources.md with an H2 entry for each slug referenced by source_keys."
)
print_findings()
sys.exit(1)
# --- Check 1: FILL IN: placeholders in sources.md ---
for line in sources_content.splitlines():
if PLACEHOLDER_RE.search(line):
emit_fail(
"Unfilled FILL IN: placeholder",
"sources.md",
"sources.md contains an unfilled placeholder, meaning provenance is incomplete.",
"Replace all 'FILL IN:' values in sources.md with real content."
)
break
# --- Check 2: source_keys in the agent file → slug exists in sources.md ---
for fpath, keys in [(agent_file, given_keys)]:
if not keys:
continue
rel = os.path.relpath(fpath, plugin_root)
for slug in keys:
if slug not in sources_slugs:
emit_fail(
f"source_keys slug '{slug}' not found in sources.md",
rel,
f"'{rel}' declares '{slug}' as a source but there is no '## {slug}' heading in sources.md.",
f"Add '## {slug}' entry to sources.md or remove '{slug}' from {rel} source_keys."
)
# --- Checks 3, 4, 5: Per-slug checks in sources.md ---
for slug in parse_h2_slugs(sources_content):
# Check 3: Contributing files exist (paths relative to plugin root)
cf_value = parse_contributing_files(sources_content, slug)
if cf_value and not cf_value.startswith("(none"):
cf_files = [p.strip() for p in cf_value.split(",") if p.strip()]
for cf_rel in cf_files:
cf_abs = os.path.join(plugin_root, cf_rel)
if not os.path.isfile(cf_abs):
emit_fail(
f"Contributing file '{cf_rel}' does not exist",
f"sources.md (## {slug})",
f"sources.md claims '{cf_rel}' was contributed to by slug '{slug}' but the file does not exist.",
f"Create '{cf_rel}' relative to the plugin root, or correct the path in sources.md."
)
else:
# Check 4: Bidirectional — file should list slug in its source_keys
with open(cf_abs) as f:
cf_content = f.read()
cf_fm, _ = parse_frontmatter(cf_content)
cf_keys = parse_source_keys(cf_fm)
if slug not in cf_keys:
emit_fail(
f"Contributing file '{cf_rel}' does not list '{slug}' in its source_keys",
f"sources.md (## {slug})",
f"sources.md says '{cf_rel}' was informed by '{slug}', but '{cf_rel}' does not declare '{slug}' in its top-level source_keys.",
f"Add '{slug}' to the top-level source_keys frontmatter in '{cf_rel}'."
)
# Check 5: Research doc field required
rd_value = parse_research_doc(sources_content, slug)
if rd_value is None:
emit_fail(
"Research doc field missing",
f"sources.md (## {slug})",
f"The '## {slug}' entry in sources.md has no '- **Research doc:**' line.",
f"Add '- **Research doc:** <path-or-(none)>' to the '## {slug}' entry in sources.md."
)
elif rd_value == "" or PLACEHOLDER_RE.search(rd_value):
emit_fail(
"Research doc field is empty or placeholder",
f"sources.md (## {slug})",
f"The '## {slug}' entry has an unfilled Research doc value.",
"Set '- **Research doc:**' to a real path relative to repo root, or '(none)' if not applicable."
)
print_findings()
sys.exit(1 if has_fail else 0)
PYTHON

View File

@@ -1,388 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
Usage: validate.sh <agent-file>
Validate an agent definition file against the agent definition spec.
At plugin/APM scope, <agent-file> is a single vendor-neutral
.apm/agents/<name>.agent.md file (frontmatter allowlist: name, description,
model — no counterpart file). At project or user scope, <agent-file> is
either half of a Claude Code .md / Copilot .agent.md pair.
Arguments:
agent-file Path to the agent file (or either half of a project/user-scope pair).
Exit codes:
0 All checks passed (may include SUGGESTIONs)
1 One or more checks failed
2 Script error (unrecognized file extension or missing field-inventory.md)
EOF
}
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage
exit 0
fi
if [[ $# -lt 1 ]]; then
echo "Error: agent-file is required." >&2
echo "" >&2
usage >&2
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
python3 -u - "$1" "$SCRIPT_DIR" <<'PYTHON'
import sys
import os
import re
agent_file = os.path.abspath(sys.argv[1])
script_dir = sys.argv[2]
fname = os.path.basename(agent_file)
# --- Detect provider (check .agent.md before .md) ---
if fname.endswith('.agent.md'):
provider = 'copilot'
name_stem = fname[:-len('.agent.md')]
elif fname.endswith('.md'):
provider = 'claude-code'
name_stem = fname[:-len('.md')]
else:
print(f"Error: unrecognized extension '{fname}' — expected .md or .agent.md", file=sys.stderr)
sys.exit(2)
# --- Load field-inventory.md ---
inv_path = os.path.normpath(os.path.join(script_dir, '..', 'references', 'field-inventory.md'))
if not os.path.isfile(inv_path):
print(f"Error: field-inventory.md not found at {inv_path}", file=sys.stderr)
sys.exit(2)
with open(inv_path) as f:
inv_content = f.read()
def parse_section_tokens(content, section_name):
lines = content.splitlines()
for i, line in enumerate(lines):
if line.strip() == f'## {section_name}':
for j in range(i + 1, len(lines)):
stripped = lines[j].strip()
if stripped and not stripped.startswith('#') and not stripped.startswith('---'):
return set(stripped.split())
return set()
cc_only_fields = parse_section_tokens(inv_content, 'claude-code-only-fields')
copilot_only_fields = parse_section_tokens(inv_content, 'copilot-only-fields')
apm_agent_allowlist = parse_section_tokens(inv_content, 'apm-agent-allowlist')
# Tools the runtime withholds from subagents regardless of the tools field
SUBAGENT_UNAVAILABLE_TOOLS = {
'AskUserQuestion', 'EnterPlanMode', 'ExitPlanMode', 'ScheduleWakeup', 'WaitForMcpServers',
}
# Copilot body length limit (chars) — content beyond this is silently truncated
COPILOT_BODY_LIMIT = 30000
# --- Helpers (shared by every scope) ---
failed = False
suggestions = []
def fail(msg):
global failed
failed = True
print(f"FAIL {msg}")
def suggest(msg):
suggestions.append(msg)
PLACEHOLDER_RE = re.compile(r'(?<!`)FILL IN:[^`\n]')
def parse_frontmatter(content):
m = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
if not m:
return None, content
return m.group(1), content[m.end():]
def extract_field(fm, field):
m = re.search(rf'^{re.escape(field)}:\s*(.+)', fm, re.MULTILINE)
return m.group(1).strip() if m else None
def get_frontmatter_keys(fm):
keys = set()
for line in fm.splitlines():
m = re.match(r'^([a-zA-Z][a-zA-Z0-9_-]*):', line)
if m:
keys.add(m.group(1))
return keys
def extract_tools_list(fm):
"""Extract tool names from the tools frontmatter field (space or comma separated)."""
val = extract_field(fm, 'tools')
if not val:
return set()
return set(re.split(r'[\s,]+', val.strip()))
def is_copilot_cloud_ide(fpath):
"""True if the file is a cloud/IDE Copilot agent (name is optional for these)."""
return '.github/copilot/agents' in os.path.abspath(fpath).replace(os.sep, '/')
# --- Detect scope ---
# APM_TYPE_RE matches a top-level (column-0) `type:` line in apm.yml whose value is
# exactly one of the four package content types. Group 1 captures an optional
# opening quote; \1 requires the same character (or nothing) to close it, so
# "skill" and '"skill"' both match but a mismatched quote doesn't. The value
# must then be followed by whitespace or end-of-line — not just a non-word
# character — so a malformed value like `prompts-only` is correctly rejected
# instead of false-matching on the `prompts` prefix.
APM_TYPE_RE = re.compile(r"^type:\s*(['\"]?)(instructions|skill|hybrid|prompts)\1(?:\s|$)")
def find_apm_package_root(apm_yml_path):
"""Return True if apm_yml_path has a top-level type: line (i.e. is a package
manifest, not a type:-less marketplace-only apm.yml)."""
with open(apm_yml_path) as f:
for line in f:
if APM_TYPE_RE.match(line):
return True
return False
def detect_scope(start_dir):
home = os.path.expanduser('~')
original_start = os.path.abspath(start_dir)
# Agent files conventionally live exactly two path segments below their
# scope root — <root>/.claude/agents, <root>/.github/agents,
# <root>/.copilot/agents, or <root>/.apm/agents (see new-agent.sh's
# CC_DIR/CP_DIR and user-scope dirs). Stripping those two segments
# recovers the same root new-agent.sh would have been invoked with to
# produce this exact file, independent of how far the walk below has to
# travel to find (or fail to find) a marker — mirrors new-agent.sh's
# `root` vs `current` distinction even though validate.sh is handed a
# file's directory, not the scope root itself.
#
# That arithmetic is only trustworthy when the path actually has this
# shape: parent directory literally named "agents", grandparent one of
# the four known scope-dir names. A hand-placed or otherwise
# non-conventional agent file (never produced by new-agent.sh) has no
# such guarantee — blindly trusting two-segments-up there could point at
# an unrelated ancestor. conventional_shape gates every use of
# conventional_root below; when it's false, the walked-to `current`
# directory is used instead, the same fallback this function used before
# conventional_root existed.
scope_dir_name = os.path.basename(os.path.dirname(original_start))
conventional_shape = (
os.path.basename(original_start) == 'agents'
and scope_dir_name in ('.claude', '.github', '.copilot', '.apm')
)
conventional_root = os.path.dirname(os.path.dirname(original_start))
current = original_start
while True:
apm_yml = os.path.join(current, 'apm.yml')
if os.path.isfile(apm_yml) and find_apm_package_root(apm_yml):
return 'plugin', current
# $HOME is the user-scope boundary — checked before the .git test
# below, so a dotfiles-managed $HOME (yadm, chezmoi bare-repo, etc.)
# can't shadow user scope by being its own .git repo. 'user' scope
# requires EITHER start_dir to BE $HOME itself (no walk-up — the
# new-agent.sh "root exactly $HOME" case) OR start_dir to sit at the
# conventional two-segments-below-root depth (i.e. $HOME IS that
# root, matching the real ~/.claude/agents or ~/.copilot/agents
# shape). Any other walk-up into $HOME — a marker-less directory
# nested deeper than that convention — resolves to project scope
# instead: a stray directory under $HOME can't be silently
# redirected into the shared global ~/.claude or ~/.copilot agent
# directories.
if current == home:
if original_start == home or (conventional_shape and conventional_root == home):
return 'user', home
return 'project', conventional_root if conventional_shape else current
# .git is a directory in a normal checkout but a file (`gitdir: ...`)
# in a git worktree — exists() covers both. Returns conventional_root,
# not current: new-agent.sh's project-scope file placement always
# uses its `$ROOT` argument directly, never the walked-up `.git`
# location, so a <root> one or more levels below the repo's .git
# (a subdirectory of a larger git-tracked tree — explicitly a
# supported case per new-agent.sh's usage text) must resolve to the
# same root new-agent.sh actually wrote to, not to the .git dir —
# unless the path lacks the conventional shape, in which case that
# arithmetic isn't trustworthy and current is used instead.
if os.path.exists(os.path.join(current, '.git')):
return 'project', conventional_root if conventional_shape else current
parent = os.path.dirname(current)
if parent == current:
return 'project', conventional_root if conventional_shape else current
current = parent
agent_dir = os.path.dirname(agent_file)
scope, scope_root = detect_scope(agent_dir)
# --- Plugin/APM scope: single vendor-neutral file, no counterpart ---
def check_apm_agent_file(fpath, allowlist, stem):
local_fname = os.path.basename(fpath)
with open(fpath) as f:
content = f.read()
fm, body = parse_frontmatter(content)
if fm is None:
fail(f"no valid YAML frontmatter (---...---) — {local_fname}")
return
# The apm-agent.md template embeds its authoring guidance as HTML
# comments inside the frontmatter block (so they render invisible in a
# Markdown preview but stay visible in the raw file). get_frontmatter_keys
# silently ignores any line that isn't a `key:` match, so a comment left
# behind at ship time would otherwise pass unnoticed — yet apm compile
# copies this frontmatter verbatim to both harnesses, and `<!-- -->` is
# not valid YAML, so yaml.safe_load breaks on both downstream (ADR-0016).
if re.search(r'<!--|-->', fm):
fail(f"frontmatter still contains template HTML comments (<!-- ... -->) "
f"— delete them before shipping — {local_fname}")
# Allowlist: only name/description/model may appear — no tools, no
# Claude-only or Copilot-only fields. apm compile verbatim-copies
# frontmatter to every target, so anything else is unsafe on at least
# one harness (ADR-0016).
fm_keys = get_frontmatter_keys(fm)
for key in sorted(fm_keys):
if key not in allowlist:
fail(f"field '{key}' is not in the vendor-neutral APM agent allowlist "
f"({', '.join(sorted(allowlist))}) — {local_fname}")
# name — required, kebab-case, must match filename stem (file is <name>.agent.md)
name_val = extract_field(fm, 'name')
if not name_val:
fail(f"name field is missing or empty — {local_fname}")
else:
if not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', name_val):
fail(f"name '{name_val}' is not kebab-case — {local_fname}")
if name_val != stem:
fail(f"name '{name_val}' does not match filename stem '{stem}' — {local_fname}")
# description — required, non-empty, no placeholder
desc_val = extract_field(fm, 'description')
if not desc_val:
fail(f"description field is missing or empty — {local_fname}")
else:
if PLACEHOLDER_RE.search(desc_val):
fail(f"description contains unfilled FILL IN: placeholder — {local_fname}")
# body — required, non-empty, no placeholder; same Copilot truncation risk
# applies since this file compiles verbatim into a real Copilot file downstream.
if not body.strip():
fail(f"system prompt body is empty — {local_fname}")
else:
if PLACEHOLDER_RE.search(body):
fail(f"body contains unfilled FILL IN: placeholder — {local_fname}")
if len(body) > COPILOT_BODY_LIMIT:
suggest(f"body exceeds {COPILOT_BODY_LIMIT:,} characters ({len(body):,} chars) — "
f"content beyond the limit is silently truncated by the Copilot runtime "
f"once apm compile emits it downstream — {local_fname}")
if scope == 'plugin':
check_apm_agent_file(agent_file, apm_agent_allowlist, name_stem)
for s in suggestions:
print(f"SUGGESTION {s}")
sys.exit(1 if failed else 0)
# --- Project/user scope: unchanged CC/Copilot pair validation ---
# --- Derive counterpart path ---
if scope == 'project':
if provider == 'claude-code':
counterpart = os.path.join(scope_root, '.github', 'agents', name_stem + '.agent.md')
counterpart_provider = 'copilot'
else:
counterpart = os.path.join(scope_root, '.claude', 'agents', name_stem + '.md')
counterpart_provider = 'claude-code'
else: # user
home = os.path.expanduser('~')
if provider == 'claude-code':
counterpart = os.path.join(home, '.copilot', 'agents', name_stem + '.agent.md')
counterpart_provider = 'copilot'
else:
counterpart = os.path.join(home, '.claude', 'agents', name_stem + '.md')
counterpart_provider = 'claude-code'
def check_file(fpath, file_provider):
local_fname = os.path.basename(fpath)
with open(fpath) as f:
content = f.read()
fm, body = parse_frontmatter(content)
if fm is None:
fail(f"no valid YAML frontmatter (---...---) — {local_fname}")
return
# name — required for CC and Copilot CLI; optional for Copilot cloud/IDE agents
cloud_ide = (file_provider == 'copilot' and is_copilot_cloud_ide(fpath))
name_val = extract_field(fm, 'name')
if not cloud_ide:
if not name_val:
fail(f"name field is missing or empty — {local_fname}")
else:
if not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', name_val):
fail(f"name '{name_val}' is not kebab-case — {local_fname}")
# Stem check applies to Copilot CLI only; CC docs say filename need not match name
if file_provider == 'copilot':
stem = local_fname[:-len('.agent.md')]
if name_val != stem:
fail(f"name '{name_val}' does not match filename stem '{stem}' — {local_fname}")
elif name_val and not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', name_val):
# cloud/IDE: name is optional, but if present it must be valid
fail(f"name '{name_val}' is not kebab-case — {local_fname}")
# description
desc_val = extract_field(fm, 'description')
if not desc_val:
fail(f"description field is missing or empty — {local_fname}")
else:
if PLACEHOLDER_RE.search(desc_val):
fail(f"description contains unfilled FILL IN: placeholder — {local_fname}")
# body
if not body.strip():
fail(f"system prompt body is empty — {local_fname}")
else:
if PLACEHOLDER_RE.search(body):
fail(f"body contains unfilled FILL IN: placeholder — {local_fname}")
# Copilot body length limit
if file_provider == 'copilot' and len(body) > COPILOT_BODY_LIMIT:
suggest(f"body exceeds {COPILOT_BODY_LIMIT:,} characters ({len(body):,} chars) — content beyond the limit is silently truncated by the Copilot runtime — {local_fname}")
# CC-only fields in Copilot file
if file_provider == 'copilot':
fm_keys = get_frontmatter_keys(fm)
for key in sorted(fm_keys):
if key in cc_only_fields:
fail(f"CC-only field '{key}' present in Copilot file — {local_fname}")
# Copilot-only fields in CC file
if file_provider == 'claude-code':
fm_keys = get_frontmatter_keys(fm)
for key in sorted(fm_keys):
if key in copilot_only_fields:
fail(f"Copilot-only field '{key}' present in CC file — {local_fname}")
# Subagent-unavailable tools listed in tools field
tools = extract_tools_list(fm)
unavailable = tools & SUBAGENT_UNAVAILABLE_TOOLS
for tool in sorted(unavailable):
suggest(f"'{tool}' is listed in tools but is never available to subagents — the runtime withholds it regardless — {local_fname}")
# --- Check counterpart exists ---
if not os.path.isfile(counterpart):
fail(f"counterpart file not found: {counterpart}")
sys.exit(1)
# --- Check both files ---
check_file(agent_file, provider)
check_file(counterpart, counterpart_provider)
for s in suggestions:
print(f"SUGGESTION {s}")
sys.exit(1 if failed else 0)
PYTHON

View File

@@ -1,33 +0,0 @@
# tests/
Test files for scripts bundled with this skill.
## When to add tests
Add tests here when the skill has scripts in `scripts/` that are complex enough
to break silently — validators, parsers, generators, anything with branching
logic or edge cases. Test infrastructure (`.bats`, `*_test.*`, `test_*.sh`)
belongs here, not in `scripts/`.
## Dependencies
Tests require [bats-support](https://github.com/bats-core/bats-support) and
[bats-assert](https://github.com/bats-core/bats-assert). The test files load
helpers from the repo root's `tests/test_helper/`.
From the repo root:
```bash
git clone https://github.com/bats-core/bats-support tests/test_helper/bats-support
git clone https://github.com/bats-core/bats-assert tests/test_helper/bats-assert
```
Run all tests for this skill (from the repo root):
```bash
bats <destination-dir>/agent-audit/tests/
```
## If no tests are needed
Delete this README and the `tests/` directory entirely.

View File

@@ -1,435 +0,0 @@
#!/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)/validate-provenance.sh"
TMPDIR="$(mktemp -d)"
# Helper: create an APM package root at <root> (apm.yml with a top-level
# type: line — a real package manifest, not marketplace-only) plus a
# single vendor-neutral agent file at <root>/.apm/agents/<name>.agent.md.
make_package() {
local root="$1"
mkdir -p "$root/.apm/agents"
cat > "$root/apm.yml" <<EOF
name: test-package
version: 0.1.0
type: skill
EOF
}
# Helper: create a clean agent file (no source_keys)
make_clean_agent() {
local root="$1"
local name="${2:-my-agent}"
cat > "$root/.apm/agents/${name}.agent.md" <<EOF
---
name: ${name}
description: A valid agent description.
---
You are a test agent.
EOF
}
# Helper: create an agent file with source_keys
make_agent_with_source_keys() {
local root="$1"
local name="${2:-my-agent}"
local slug="${3:-my-source}"
cat > "$root/.apm/agents/${name}.agent.md" <<EOF
---
name: ${name}
description: A valid agent description.
source_keys:
- ${slug}
---
You are a test agent.
EOF
}
# Helper: create a valid sources.md with one entry
make_sources_md() {
local root="$1"
local slug="${2:-my-source}"
local contrib="${3:-.apm/agents/my-agent.agent.md}"
local research="${4:-(none)}"
cat > "$root/sources.md" <<EOF
# Sources
## ${slug}
- **URL:** https://example.com/${slug}
- **Description:** A test source.
- **Contributing files:** ${contrib}
- **Research doc:** ${research}
- **Status:** \`extracted\`
EOF
}
}
teardown() {
rm -rf "$TMPDIR"
}
# ---------------------------------------------------------------------------
# --help
# ---------------------------------------------------------------------------
@test "--help exits 0" {
run bash "$SCRIPT" --help
assert_success
assert_output --partial "Usage:"
}
# ---------------------------------------------------------------------------
# Non-plugin/APM scope → exit 0 silently
# ---------------------------------------------------------------------------
@test "non-plugin scope: no apm.yml in tree → exit 0, no output" {
local dir="$TMPDIR/no-package"
mkdir -p "$dir/.apm/agents"
cat > "$dir/.apm/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
source_keys:
- my-source
---
You are a test agent.
EOF
run bash "$SCRIPT" "$dir/.apm/agents/my-agent.agent.md"
assert_success
assert_output ""
}
@test "non-plugin scope: apm.yml present but type:-less (marketplace-only) → exit 0, no output" {
local dir="$TMPDIR/marketplace-only"
mkdir -p "$dir/.apm/agents"
cat > "$dir/apm.yml" <<EOF
name: root-marketplace
marketplace:
owner: someone
packages:
- ./packages/plugin-a
EOF
cat > "$dir/.apm/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
source_keys:
- my-source
---
You are a test agent.
EOF
run bash "$SCRIPT" "$dir/.apm/agents/my-agent.agent.md"
assert_success
assert_output ""
}
@test "non-plugin scope: bare plugin.json (no apm.yml) is no longer a scope signal → exit 0, no output" {
local dir="$TMPDIR/old-plugin-json-only"
mkdir -p "$dir/agents"
echo '{"name":"test-plugin","version":"0.1.0"}' > "$dir/plugin.json"
cat > "$dir/agents/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
source_keys:
- my-source
---
You are a test agent.
EOF
run bash "$SCRIPT" "$dir/agents/my-agent.md"
assert_success
assert_output ""
}
@test "non-plugin scope: walk-up stops at .git boundary before reaching an ancestor apm.yml" {
local dir="$TMPDIR/repo"
mkdir -p "$dir/.git" "$dir/.apm/agents"
cat > "$dir/apm.yml" <<EOF
name: test-package
version: 0.1.0
type: skill
EOF
mkdir -p "$dir/sub/.apm/agents"
cat > "$dir/sub/.apm/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
source_keys:
- my-source
---
You are a test agent.
EOF
# sub/ has no .git and no apm.yml of its own; the real package apm.yml
# lives at $dir, but $dir/.git means the walk from sub/ should stop at
# sub/ itself only if sub/ had a .git — here .git is at $dir, ABOVE
# sub/, so the walk from sub/ reaches $dir/apm.yml before any .git.
# This test instead verifies the walk finds that package root correctly
# (a positive case) — see the dedicated .git-stops-first test below for
# the negative case.
run bash "$SCRIPT" "$dir/sub/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "FAIL"
}
@test "non-plugin scope: \$HOME boundary stops the walk before reaching an ancestor apm.yml above \$HOME" {
# A type-bearing apm.yml sits ABOVE the fake $HOME — if find_plugin_root
# didn't stop at $HOME, it would walk past it and misclassify this
# user/project-scope file as plugin scope, which would then FAIL on
# Check 0 (source_keys declared but sources.md absent) since sources.md
# doesn't exist at that ancestor apm.yml's location either.
local dir="$TMPDIR/anc"
mkdir -p "$dir"
cat > "$dir/apm.yml" <<EOF
name: outer-package
version: 0.1.0
type: skill
EOF
local fake_home="$dir/fakehome"
mkdir -p "$fake_home/.apm/agents"
cat > "$fake_home/.apm/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
source_keys:
- my-source
---
You are a test agent.
EOF
run env HOME="$fake_home" bash "$SCRIPT" "$fake_home/.apm/agents/my-agent.agent.md"
assert_success
assert_output ""
}
@test "non-plugin scope: .git between the agent file and an ancestor apm.yml stops the walk first" {
local dir="$TMPDIR/repo2"
mkdir -p "$dir/.apm/agents"
cat > "$dir/apm.yml" <<EOF
name: test-package
version: 0.1.0
type: skill
EOF
mkdir -p "$dir/sub/.git" "$dir/sub/.apm/agents"
cat > "$dir/sub/.apm/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
source_keys:
- my-source
---
You are a test agent.
EOF
run bash "$SCRIPT" "$dir/sub/.apm/agents/my-agent.agent.md"
assert_success
assert_output ""
}
# ---------------------------------------------------------------------------
# Early exit: no sources.md, no source_keys → exit 0, no output
# ---------------------------------------------------------------------------
@test "clean pass: no sources.md and no source_keys → exit 0, no output" {
local root="$TMPDIR/package"
make_package "$root"
make_clean_agent "$root"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_success
assert_output ""
}
# ---------------------------------------------------------------------------
# Check 0: source_keys present but sources.md absent → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: source_keys in agent file but sources.md absent" {
local root="$TMPDIR/package"
make_package "$root"
make_agent_with_source_keys "$root"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "FAIL"
}
# ---------------------------------------------------------------------------
# Check 1: FILL IN: placeholder in sources.md → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: FILL IN: placeholder in sources.md" {
local root="$TMPDIR/package"
make_package "$root"
make_agent_with_source_keys "$root"
cat > "$root/sources.md" <<EOF
# Sources
## my-source
- **URL:** FILL IN: add url
- **Description:** A test source.
- **Contributing files:** .apm/agents/my-agent.agent.md
- **Research doc:** (none)
- **Status:** \`extracted\`
EOF
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "FAIL"
}
@test "FILL IN: inside backticks in sources.md does not fail" {
local root="$TMPDIR/package"
make_package "$root"
make_agent_with_source_keys "$root"
make_sources_md "$root"
echo "Use \`FILL IN: value\` as example." >> "$root/sources.md"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_success
}
# ---------------------------------------------------------------------------
# Check 2: source_keys slug missing from sources.md → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: source_keys slug in agent file not present as H2 in sources.md" {
local root="$TMPDIR/package"
make_package "$root"
make_agent_with_source_keys "$root" "my-agent" "my-source"
make_sources_md "$root" "different-source" "(none)" "(none)"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "FAIL"
}
# ---------------------------------------------------------------------------
# Check 3: Contributing file path doesn't exist → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: Contributing file listed in sources.md does not exist" {
local root="$TMPDIR/package"
make_package "$root"
make_agent_with_source_keys "$root"
make_sources_md "$root" "my-source" ".apm/agents/nonexistent.agent.md"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "FAIL"
}
@test "pass: (none) in Contributing files is skipped" {
local root="$TMPDIR/package"
make_package "$root"
make_agent_with_source_keys "$root"
make_sources_md "$root" "my-source" "(none — not used directly)"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_success
}
# ---------------------------------------------------------------------------
# Check 5: Research doc field missing or placeholder → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: Research doc field missing from sources.md entry" {
local root="$TMPDIR/package"
make_package "$root"
make_agent_with_source_keys "$root"
cat > "$root/sources.md" <<EOF
# Sources
## my-source
- **URL:** https://example.com/my-source
- **Description:** A test source.
- **Contributing files:** .apm/agents/my-agent.agent.md
- **Status:** \`extracted\`
EOF
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "FAIL"
}
@test "FAIL: Research doc field is FILL IN: placeholder" {
local root="$TMPDIR/package"
make_package "$root"
make_agent_with_source_keys "$root"
cat > "$root/sources.md" <<EOF
# Sources
## my-source
- **URL:** https://example.com/my-source
- **Description:** A test source.
- **Contributing files:** .apm/agents/my-agent.agent.md
- **Research doc:** FILL IN: path to research doc
- **Status:** \`extracted\`
EOF
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "FAIL"
}
# ---------------------------------------------------------------------------
# Check 4: Bidirectional — contributing file missing slug in source_keys → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: Contributing file exists but does not list parent slug in source_keys" {
local root="$TMPDIR/package"
make_package "$root"
# agent file has source_keys: other-source (not my-source)
cat > "$root/.apm/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
source_keys:
- other-source
---
You are a test agent.
EOF
# sources.md says my-agent.agent.md contributed to my-source, but
# my-agent.agent.md doesn't list my-source
cat > "$root/sources.md" <<EOF
# Sources
## other-source
- **URL:** https://example.com/other-source
- **Description:** A test source.
- **Contributing files:** .apm/agents/my-agent.agent.md
- **Research doc:** (none)
- **Status:** \`extracted\`
## my-source
- **URL:** https://example.com/my-source
- **Description:** Another source.
- **Contributing files:** .apm/agents/my-agent.agent.md
- **Research doc:** (none)
- **Status:** \`extracted\`
EOF
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "FAIL"
}
# ---------------------------------------------------------------------------
# Clean full pass
# ---------------------------------------------------------------------------
@test "clean full pass: all checks satisfied" {
local root="$TMPDIR/package"
make_package "$root"
make_agent_with_source_keys "$root"
make_sources_md "$root"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_success
}

View File

@@ -1,641 +0,0 @@
#!/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)/validate.sh"
TMPDIR="$(mktemp -d)"
# Helper: create an APM package root at <root> (apm.yml with a top-level
# type: line, marking it a real package manifest — not marketplace-only)
# plus a single vendor-neutral agent file at
# <root>/.apm/agents/<name>.agent.md. <extra_frontmatter>, if given, is
# inserted as additional raw frontmatter lines (used to inject fields
# under test).
make_apm_agent() {
local root="$1"
local name="$2"
local extra_frontmatter="${3:-}"
mkdir -p "$root/.apm/agents"
cat > "$root/apm.yml" <<EOF
name: test-package
version: 0.1.0
type: skill
EOF
cat > "$root/.apm/agents/${name}.agent.md" <<EOF
---
name: ${name}
description: A valid agent description.
${extra_frontmatter}
---
You are a test agent. When invoked, do the thing.
EOF
}
}
teardown() {
rm -rf "$TMPDIR"
}
# ---------------------------------------------------------------------------
# Passing cases — project/user scope (unchanged)
# ---------------------------------------------------------------------------
@test "passes on a clean project-scope pair (CC file as input)" {
local root="$TMPDIR/project"
mkdir -p "$root/.git" "$root/.claude/agents" "$root/.github/agents"
cat > "$root/.claude/agents/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
cat > "$root/.github/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$root/.claude/agents/my-agent.md"
assert_success
refute_output --partial "FAIL"
}
@test "user scope: \$HOME being a dotfiles .git repo does not shadow user scope" {
local fake_home="$TMPDIR/fakehome"
mkdir -p "$fake_home/.git" "$fake_home/.claude/agents" "$fake_home/.copilot/agents"
cat > "$fake_home/.claude/agents/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
cat > "$fake_home/.copilot/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run env HOME="$fake_home" bash "$SCRIPT" "$fake_home/.claude/agents/my-agent.md"
assert_success
refute_output --partial "FAIL"
}
@test "user scope: agent file directly in \$HOME (start dir IS exactly \$HOME, no walk-up) resolves to user scope" {
local fake_home="$TMPDIR/fakehome-direct"
mkdir -p "$fake_home" "$fake_home/.copilot/agents"
cat > "$fake_home/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
cat > "$fake_home/.copilot/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run env HOME="$fake_home" bash "$SCRIPT" "$fake_home/my-agent.md"
assert_success
refute_output --partial "FAIL"
refute_output --partial "counterpart"
}
@test "project scope: a nested marker-less directory walked up into \$HOME resolves to project scope, not user scope (live repro of new-agent.sh's stray-directory case)" {
local fake_home="$TMPDIR/fakehome-nested"
local nested="$fake_home/scratch/testdir"
mkdir -p "$nested/.claude/agents" "$nested/.github/agents"
cat > "$nested/.claude/agents/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
cat > "$nested/.github/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run env HOME="$fake_home" bash "$SCRIPT" "$nested/.claude/agents/my-agent.md"
assert_success
refute_output --partial "FAIL"
refute_output --partial "counterpart"
}
@test "project scope: nested marker-less dir under \$HOME does NOT look for a counterpart under the shared \$HOME/.copilot or \$HOME/.github dirs" {
local fake_home="$TMPDIR/fakehome-nested2"
local nested="$fake_home/scratch/testdir"
mkdir -p "$nested/.claude/agents" "$fake_home/.copilot/agents"
cat > "$nested/.claude/agents/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
# Decoy counterpart at the *user*-scope location — if scope were
# misclassified as 'user' (the pre-fix bug), validate.sh would find this
# unrelated file and (wrongly) pass.
cat > "$fake_home/.copilot/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run env HOME="$fake_home" bash "$SCRIPT" "$nested/.claude/agents/my-agent.md"
assert_failure
assert_output --partial "counterpart file not found"
}
@test "project scope: filesystem-root fallback (no \$HOME in path, no markers found) resolves to project scope, not user scope" {
local unrelated_home="$TMPDIR/unrelated-home-never-reached"
local root="$TMPDIR/no-home-relation/deep/proj"
mkdir -p "$root/.claude/agents" "$root/.github/agents"
cat > "$root/.claude/agents/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
cat > "$root/.github/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run env HOME="$unrelated_home" bash "$SCRIPT" "$root/.claude/agents/my-agent.md"
assert_success
refute_output --partial "FAIL"
refute_output --partial "counterpart"
}
@test "project scope: <root> one level below a .git ancestor resolves scope to <root>, not to wherever .git was found (subdirectory of a larger git-tracked tree)" {
local repo="$TMPDIR/repo-with-subdir"
local root="$repo/subdir"
mkdir -p "$repo/.git" "$root/.claude/agents" "$root/.github/agents"
cat > "$root/.claude/agents/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
cat > "$root/.github/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
# new-agent.sh, invoked with <root> as its root argument, would place the
# counterpart at <root>/.github/agents — not at the repo root's
# .github/agents, even though .git lives at the repo root one level up.
run bash "$SCRIPT" "$root/.claude/agents/my-agent.md"
assert_success
refute_output --partial "FAIL"
refute_output --partial "counterpart"
}
@test "project scope: a non-conventional path (agent file not directly under a literal 'agents' dir) falls back to the nearest .git boundary instead of two-segments-up arithmetic" {
local outer="$TMPDIR/outer-repo"
local pkg="$outer/pkgA"
mkdir -p "$pkg/.git" "$pkg/.github/agents" "$pkg/extra"
# Misplaced file: sits two path segments below $outer (pkgA/extra), which
# matches the conventional_root arithmetic by coincidence, but its
# immediate parent dir is "extra", not "agents" — conventional_shape is
# false, so the fix must fall back to the nearest .git boundary (pkgA),
# not trust $outer.
cat > "$pkg/extra/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
# Counterpart at the nearest-.git root (pkgA), not at $outer — if the
# arithmetic were trusted here, validate.sh would look for a counterpart
# at $outer/.github/agents/my-agent.agent.md, which doesn't exist, and
# false-FAIL.
cat > "$pkg/.github/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$pkg/extra/my-agent.md"
assert_success
refute_output --partial "counterpart file not found"
}
# ---------------------------------------------------------------------------
# Failing cases — project/user scope: CC/Copilot pair checks
# ---------------------------------------------------------------------------
@test "fails when a CC-only field ('maxTurns') is present in a project-scope Copilot file" {
local root="$TMPDIR/project"
mkdir -p "$root/.git" "$root/.claude/agents" "$root/.github/agents"
cat > "$root/.claude/agents/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
cat > "$root/.github/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
maxTurns: 10
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$root/.claude/agents/my-agent.md"
assert_failure
assert_output --partial "CC-only field"
assert_output --partial "maxTurns"
}
@test "fails when a Copilot-only field ('target') is present in a project-scope CC file" {
local root="$TMPDIR/project"
mkdir -p "$root/.git" "$root/.claude/agents" "$root/.github/agents"
cat > "$root/.claude/agents/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
target: cli
---
You are a test agent. When invoked, do the thing.
EOF
cat > "$root/.github/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$root/.claude/agents/my-agent.md"
assert_failure
assert_output --partial "Copilot-only field"
assert_output --partial "target"
}
@test "fails when the Copilot counterpart is missing at project scope" {
local root="$TMPDIR/project"
mkdir -p "$root/.git" "$root/.claude/agents"
cat > "$root/.claude/agents/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$root/.claude/agents/my-agent.md"
assert_failure
assert_output --partial "counterpart file not found"
}
@test "--help exits 0 and shows Usage:" {
run bash "$SCRIPT" --help
assert_success
assert_output --partial "Usage:"
}
@test "fails when no arguments are given" {
run bash "$SCRIPT"
assert_failure
}
# ---------------------------------------------------------------------------
# Passing cases — plugin/APM scope
# ---------------------------------------------------------------------------
@test "passes on a clean plugin/APM-scope agent file (name/description only)" {
local root="$TMPDIR/pkg"
make_apm_agent "$root" "my-agent"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_success
refute_output --partial "FAIL"
}
@test "passes on a clean plugin/APM-scope agent file with optional model field" {
local root="$TMPDIR/pkg"
make_apm_agent "$root" "my-agent" "model: claude-opus-4"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_success
refute_output --partial "FAIL"
}
# ---------------------------------------------------------------------------
# Failing cases — plugin/APM scope: allowlist violations
# ---------------------------------------------------------------------------
@test "fails when 'tools' field is present in a plugin/APM-scope agent file" {
local root="$TMPDIR/pkg"
make_apm_agent "$root" "my-agent" "tools: Read Edit"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "tools"
}
@test "fails when a Claude-only field ('maxTurns') is present in a plugin/APM-scope agent file" {
local root="$TMPDIR/pkg"
make_apm_agent "$root" "my-agent" "maxTurns: 10"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "maxTurns"
}
@test "fails when a Copilot-only field ('target') is present in a plugin/APM-scope agent file" {
local root="$TMPDIR/pkg"
make_apm_agent "$root" "my-agent" "target: cli"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "target"
}
@test "fails when 'hooks' is present in a plugin/APM-scope agent file (outside allowlist)" {
local root="$TMPDIR/pkg"
make_apm_agent "$root" "my-agent" "hooks: {}"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "hooks"
}
# ---------------------------------------------------------------------------
# Failing cases — plugin/APM scope: structural checks
# ---------------------------------------------------------------------------
@test "fails when name is not kebab-case in a plugin/APM-scope agent file" {
local root="$TMPDIR/pkg"
mkdir -p "$root/.apm/agents"
cat > "$root/apm.yml" <<EOF
name: test-package
version: 0.1.0
type: skill
EOF
cat > "$root/.apm/agents/my-agent.agent.md" <<EOF
---
name: MyAgent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "kebab"
}
@test "fails when name does not match filename stem in a plugin/APM-scope agent file" {
local root="$TMPDIR/pkg"
mkdir -p "$root/.apm/agents"
cat > "$root/apm.yml" <<EOF
name: test-package
version: 0.1.0
type: skill
EOF
cat > "$root/.apm/agents/my-agent.agent.md" <<EOF
---
name: wrong-name
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "does not match filename stem"
}
@test "fails when 'name' field is missing from a plugin/APM-scope agent file" {
local root="$TMPDIR/pkg"
mkdir -p "$root/.apm/agents"
cat > "$root/apm.yml" <<EOF
name: test-package
version: 0.1.0
type: skill
EOF
cat > "$root/.apm/agents/my-agent.agent.md" <<EOF
---
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
}
@test "fails when 'description' field is missing from a plugin/APM-scope agent file" {
local root="$TMPDIR/pkg"
mkdir -p "$root/.apm/agents"
cat > "$root/apm.yml" <<EOF
name: test-package
version: 0.1.0
type: skill
EOF
cat > "$root/.apm/agents/my-agent.agent.md" <<EOF
---
name: my-agent
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
}
@test "fails when a template HTML comment is left in plugin/APM-scope frontmatter" {
local root="$TMPDIR/pkg"
mkdir -p "$root/.apm/agents"
cat > "$root/apm.yml" <<EOF
name: test-package
version: 0.1.0
type: skill
EOF
cat > "$root/.apm/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
<!-- model: sonnet
Optional. Omit to inherit the runtime default. -->
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
assert_output --partial "template HTML comments"
}
@test "fails when body contains unfilled FILL IN: placeholder in a plugin/APM-scope agent file" {
local root="$TMPDIR/pkg"
mkdir -p "$root/.apm/agents"
cat > "$root/apm.yml" <<EOF
name: test-package
version: 0.1.0
type: skill
EOF
cat > "$root/.apm/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
FILL IN: replace this with your system prompt.
EOF
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
}
# ---------------------------------------------------------------------------
# Plugin/APM scope: no pair, no counterpart concept
# ---------------------------------------------------------------------------
@test "never raises a 'counterpart' FAIL on a clean plugin/APM-scope agent file" {
local root="$TMPDIR/pkg"
make_apm_agent "$root" "my-agent"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_success
refute_output --partial "counterpart"
}
@test "never raises a 'counterpart' FAIL on a failing plugin/APM-scope agent file" {
local root="$TMPDIR/pkg"
make_apm_agent "$root" "my-agent" "tools: Read"
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
assert_failure
refute_output --partial "counterpart"
}
# ---------------------------------------------------------------------------
# Scope-detection walk-up
# ---------------------------------------------------------------------------
@test "walk-up skips a type:-less apm.yml and finds a real package root further up" {
local root="$TMPDIR/case"
mkdir -p "$root/.apm/agents/nested/deeper"
cat > "$root/apm.yml" <<EOF
name: real-package
version: 1.0.0
type: skill
EOF
# Closer to the agent file than the real package root, but has no type:
# line — marketplace-only per monorepo-and-repo-shapes.md, must be skipped.
cat > "$root/.apm/agents/nested/apm.yml" <<EOF
name: not-a-package-manifest
version: 1.0.0
EOF
cat > "$root/.apm/agents/nested/deeper/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$root/.apm/agents/nested/deeper/my-agent.agent.md"
assert_success
refute_output --partial "FAIL"
}
@test "type:-less apm.yml is not treated as plugin scope — falls through to project scope" {
local root="$TMPDIR/proj-marketplace"
mkdir -p "$root/.git" "$root/.claude/agents" "$root/.github/agents"
cat > "$root/apm.yml" <<EOF
name: marketplace-root
version: 1.0.0
marketplace:
packages: []
EOF
cat > "$root/.claude/agents/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
cat > "$root/.github/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$root/.claude/agents/my-agent.md"
assert_success
refute_output --partial "FAIL"
}
@test "a bare plugin.json with no apm.yml is no longer plugin scope — falls through to project scope" {
local root="$TMPDIR/proj-legacy-plugin-json"
mkdir -p "$root/.git" "$root/.claude/agents" "$root/.github/agents"
echo '{}' > "$root/plugin.json"
# 'hooks' is plugin-silently-ignored only at (old) plugin scope; at
# project scope it's a legitimate CC field. If this directory were
# mis-detected as plugin scope (old plugin.json-based logic), this would
# FAIL with a plugin-silently-ignored-fields finding on 'hooks'.
cat > "$root/.claude/agents/my-agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
hooks:
PostToolUse:
- match: ".*"
command: "echo done"
---
You are a test agent. When invoked, do the thing.
EOF
cat > "$root/.github/agents/my-agent.agent.md" <<EOF
---
name: my-agent
description: A valid agent description.
---
You are a test agent. When invoked, do the thing.
EOF
run bash "$SCRIPT" "$root/.claude/agents/my-agent.md"
assert_success
refute_output --partial "hooks"
}

View File

@@ -1,45 +0,0 @@
# agent-author
Creates and improves agent definition files for Claude Code and GitHub Copilot CLI.
## What it does
Scaffolds and fills in agent definition files at plugin/APM, project, or user scope. Project and user scope always generate a Claude Code + Copilot CLI file pair (`.md` + `.agent.md`) in one pass. Plugin/APM scope generates a single vendor-neutral `.apm/agents/<name>.agent.md` file instead — no separate Claude Code / Copilot split, since `apm compile` has no per-target field integrator (see ADR-0016). Also applies improvement signals — grill output, inline feedback, session context — to existing agent files. Bumps the version after every change: the resolved package's `apm.yml` at plugin/APM scope (minor for new agents, patch for improvements); project/user scope has no manifest to bump.
## Before you start
Have ready: the agent's name (kebab-case), the root directory (plugin root, project root, or `~`), a one-sentence purpose, and the triggering condition (when should the runtime delegate to this agent?).
## Usage
```
/agent-author
```
**Manual scaffold (human workflow):**
```bash
bash scripts/new-agent.sh <agent-name> <root>
# Examples:
bash scripts/new-agent.sh code-reviewer packages/my-package/ # plugin/APM scope if packages/my-package/apm.yml has a type: field
bash scripts/new-agent.sh deploy-assistant .
bash scripts/new-agent.sh security-reviewer ~
```
## Files
| File | Purpose |
|------|---------|
| `SKILL.md` | Skill instructions for agents |
| `scripts/new-agent.sh` | Scaffolds agent definition file(s) from templates — a single `.apm/agents/<name>.agent.md` at plugin/APM scope, or a Claude Code + Copilot CLI pair at project/user scope |
| `references/deployment-modes.md` | Plugin/APM vs project vs user scope: restrictions, scoped identifiers, path conventions |
| `references/scripts.md` | Conventions for new-agent.sh and any future scripts: contract, template variables, file placement, error messages |
| `references/sources.md` | Research provenance — sources that informed this skill |
| `assets/templates/claude-code.md` | Annotated Claude Code agent definition template (project/user scope) |
| `assets/templates/copilot.agent.md` | Annotated Copilot CLI agent definition template (project/user scope) |
| `assets/templates/apm-agent.md` | Annotated vendor-neutral APM agent definition template (plugin/APM scope) |
| `tests/new-agent.bats` | bats tests for `scripts/new-agent.sh` |
| `assets/README.md` | Directory meta-documentation for assets/ |
| `references/README.md` | Directory meta-documentation for references/ |
| `scripts/README.md` | Directory meta-documentation for scripts/ |
| `tests/README.md` | bats dependency instructions and run command |

View File

@@ -1,263 +0,0 @@
---
name: agent-author
description: >
Use when the user wants to create a new agent definition file from scratch
("write an agent for X", "build a subagent that does Y", "create an agent
definition for Z"), or improve an existing one. Handles agent definitions at
plugin/APM, project, and user scope. Project and user scope always generate
a Claude Code (`.md`) + Copilot CLI (`.agent.md`) file pair in one pass;
plugin/APM scope generates a single vendor-neutral `.apm/agents/<name>.agent.md`
file instead (no per-target Claude Code / Copilot split). Also use when the
user provides inline feedback about an agent's behavior and wants it applied,
or when a grill session has produced findings the user wants acted on — even
if they don't say "improve" explicitly. Do not use for read-only review —
examine agent files manually or run a grill session to generate improvement
signals. Do not use to author skills — use /skill-author instead.
allowed-tools: Bash Read Write Edit
metadata:
category: factory
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- claude-code-subagents-docs
- context7-github-en-copilot
- github-custom-agents-configuration
- github-cli-plugin-reference
- github-plugins-creating
---
## Gotchas
- At plugin/APM scope, bump the resolved package's `apm.yml` `version` after every change — minor for a new agent, patch for a fix. Consumers compare this version to detect updates; skipping it hides the change.
- At plugin/APM scope, `tools` and all Claude-only fields (`isolation`, `maxTurns`, `effort`, `memory`, `permissionMode`, `disallowedTools`, `skills`, `color`, `initialPrompt`, `background`, `hooks`, `mcpServers`) are omitted entirely, not merely restricted (ADR-0016: `apm compile` copies frontmatter verbatim to both harnesses with no per-target integrator, so a harness-specific value is wrong on at least one). Only project/user scope supports these fields.
- An `apm.yml` with no top-level `type:` field is a marketplace-only manifest, not a package root — the walk-up skips it and keeps going.
- `AskUserQuestion`, `EnterPlanMode`, `ExitPlanMode`, `ScheduleWakeup`, and `WaitForMcpServers` are never available to any subagent regardless of the `tools` field. Exception: `ExitPlanMode` is available when the parent session runs in `permissionMode: plan`.
- Duplicate `name` values in the same scope: Claude Code silently discards one without warning. Always verify uniqueness before shipping.
- Plugin agents in subdirectories get scoped identifiers (`plugin:folder:name`) — keep agents flat in `agents/` to avoid this. Applies to project/user-scope Claude Code agents only.
- Copilot CLI agent files **must** use the `.agent.md` extension — a plain `.md` file isn't picked up. The plugin/APM-scope single file also ends in `.agent.md` by convention, but it's vendor-neutral, not Copilot-only — it compiles to Claude Code too.
- Copilot has no `permissionMode`, `maxTurns`, `isolation`, or `memory` fields — do not include them in project/user-scope Copilot files.
- `model` resolution order for Claude Code: `CLAUDE_CODE_SUBAGENT_MODEL` env var → per-invocation parameter → frontmatter `model` → main session model. The frontmatter value is a low-priority default, not a guarantee.
## Route
If the destination resolves to plugin/APM scope (scope detection in Step 1 finds a `type:`-bearing `apm.yml` at or above the root), read `references/deployment-modes.md`.
Determine which flow before touching the filesystem:
- **Neither `<name>.md` nor `<name>.agent.md` exist at the target paths** → follow **Creating a new agent**
- **At least one file exists + improvement signals present** → follow **Improving an existing agent**
- **At least one file exists + no signals** → ask: "No improvement signals found. Did you mean to create a new agent, or do you have feedback to apply?"
Signals: grill session output, inline user feedback, session context describing what went wrong.
## Creating a new agent
### Prerequisites
Before touching the filesystem, confirm you have:
- [ ] Agent name (kebab-case, e.g. `code-reviewer`)
- [ ] Root directory (a path inside a package for plugin/APM scope, project root, or `~` for user scope)
- [ ] Agent purpose — one sentence describing the task this agent handles
- [ ] Trigger condition — when should the runtime delegate to this agent?
If any are missing, stop and ask before proceeding. Then capture `git log --oneline -1` before touching the filesystem — Step 5 needs it to verify a real commit landed.
Verify `kyberforge:agent-audit` is available — it ships with the kyberforge plugin and is co-installed with this skill. If unavailable, stop and tell the user to install the kyberforge plugin before continuing.
### Step 1 — Scaffold
Run the scaffold script with the agent name and root directory:
```bash
bash scripts/new-agent.sh <name> <root>
```
Examples:
```bash
bash scripts/new-agent.sh code-reviewer packages/my-package/ # plugin/APM scope if packages/my-package/apm.yml has a type: field
bash scripts/new-agent.sh deploy-assistant .
bash scripts/new-agent.sh security-reviewer ~
```
**Scope detection (script handles this automatically).** The script walks up from `<root>` for a package boundary — same shape `agent-audit`'s `validate.sh` uses:
- Nearest ancestor `apm.yml` with a top-level `type:` field (`instructions`/`skill`/`hybrid`/`prompts`) → **plugin/APM scope** → `<package-root>/.apm/agents/<name>.agent.md` (single vendor-neutral file). A `type:`-less `apm.yml` is marketplace-only — skipped, walk continues upward.
- No such `apm.yml`, `<root>` is a project directory → **project scope** (unchanged) → `<root>/.claude/agents/<name>.md` + `<root>/.github/agents/<name>.agent.md`
- `<root>` is exactly `~` (checked directly, no walk-up) → **user scope** (unchanged) → `~/.claude/agents/<name>.md` + `~/.copilot/agents/<name>.agent.md`
A bare `plugin.json` with no `apm.yml` no longer signals plugin scope — that path is fully replaced, not dual-mode; it falls through to project scope.
The script is file-by-file no-op — it skips any file that already exists.
### Step 2 — Fill in the agent file(s)
**At plugin/APM scope**, there is exactly one file: `<package-root>/.apm/agents/<name>.agent.md`. Frontmatter carries ONLY `name`, `description`, optionally `model`, and optionally `source_keys` (provenance metadata, not a runtime field — see the template) — never `tools` or the other Claude-only fields listed in Gotchas (ADR-0016). Fill in `name`, `description`, `model`, and the system prompt body per the guidance below; the rest of this step's field-by-field guidance (tools, maxTurns, effort, memory, isolation, disallowedTools, skills, color, initialPrompt, background) is project/user scope only. Skip Step 3 and go to Step 4.
**At project/user scope**, continue below to fill in both provider files — this step covers the Claude Code file (`<name>.md`); Step 3 covers the Copilot file.
Open the scaffolded Claude Code file. Replace every `FILL IN:` placeholder. **Remove all template documentation comments from the YAML frontmatter after filling in required fields** — these are marked with `<!--` and `-->` and must be deleted before shipping.
**`name`** — lowercase letters and hyphens only. Must be unique within the scope.
**`description`** — the most important field for autonomous delegation:
- Start with an action verb: "Reviews...", "Analyzes...", "Generates..."
- If this agent should trigger without explicit user direction, include "Use proactively" in the description
- Specific about the triggering condition and expertise domain
- Under 300 characters preferred
**`tools`** (project/user scope only — never at plugin/APM scope) — restrict to what the agent actually needs. Omit to inherit all tools. Use `Agent(type1,type2)` to limit which subagent types this agent can spawn; omit `Agent` entirely to prevent spawning.
**Optional fields worth considering (project/user scope only — never at plugin/APM scope):**
- `model`: set when this agent needs a different capability tier (`haiku` for fast tasks, `opus` for deep reasoning)
- `maxTurns`: set a cap to prevent runaway agents on bounded tasks
- `effort`: set to `low` for single-lookup tasks, `high` or above for deep reasoning or multi-file analysis — overrides session effort level; omit to inherit
- `memory`: `user`, `project`, or `local` — only when cross-session state is genuinely needed
- `isolation: worktree` — only when the agent modifies files and needs an isolated copy
- `disallowedTools`: space-separated denylist applied before `tools`; supports `mcp__*` glob patterns (e.g. `disallowedTools: mcp__filesystem__*`)
- `skills`: list of skill names preloaded at agent startup — different from the `source_keys` metadata field
- `color`: UI color for the agent tile (`red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`, `cyan`)
- `initialPrompt`: auto-submitted as the first turn when this agent activates as the main session thread; only set when this agent is intended for main-thread activation
- `background`: set `true` to force background execution
**`source_keys`** — top-level list of research source slugs that informed this agent. Add only when research sources were used (i.e. entries with `` `extracted` `` status are in context from a prior `/research` session). Each slug must match an H2 heading in `sources.md` — see Step 4 for where that file lives (plugin/APM scope only). Omit entirely when no research was used.
```yaml
source_keys:
- my-source-slug
```
**System prompt body** — write as a direct role instruction:
- Open with: "You are a [role]. When invoked, [primary action]."
- Cover: inputs expected, process steps, output format, error handling
- One job per agent
### Step 3 — Fill in the Copilot agent file (project/user scope only)
Skip this step entirely at plugin/APM scope — there is no separate Copilot file there. The single `.apm/agents/<name>.agent.md` file from Step 2 already compiles to both Claude Code and Copilot CLI via `apm compile`.
**Two distinct Copilot agent formats** exist, with different paths and field sets. Choose one based on the deployment target:
**CLI format** (default — what the scaffold creates):
- Path: `.github/agents/<name>.agent.md` (project) or `~/.copilot/agents/<name>.agent.md` (user)
- Extension: **must be `.agent.md`**
- Supported fields: `name` (required), `description` (required), `tools` (optional)
- `tools` uses Copilot aliases: `execute` (shell), `read`, `edit`, `search`, `agent`, `web`
- Body length limit: **30,000 characters** — content beyond this is silently truncated
**Cloud/IDE format** (use when targeting Copilot Chat in VS Code or GitHub.com):
- Path: `.github/copilot/agents/<name>.md` (note: plain `.md`, different directory)
- Additional fields available: `target` (`vscode`, `github-copilot`, or omit for both), `user-invocable` (set `false` to hide from manual invocation), `disable-model-invocation` (set `true` to require explicit user invocation), `mcp-servers` (MCP server config — processed by cloud runtime, ignored in VS Code)
- Body length limit: **30,000 characters** — silently truncated
**Do not include Claude Code-only fields in either format**: `maxTurns`, `isolation`, `memory`, `permissionMode`, `effort`, `hooks`, `mcpServers`.
**`source_keys`** — add the same top-level list as the CC file when research sources were used. Omit when no research was used.
**Remove all template documentation comments from the YAML frontmatter after filling in required fields** — these are marked with `<!--` and `-->` and must be deleted before shipping.
The system prompt body should match the Claude Code version — the agent's task definition is the same across providers.
### Step 4 — Populate or delete `sources.md` (plugin/APM scope only)
Skip at project/user scope. The file lives at the package root (alongside `apm.yml`), not inside `.apm/agents/` — otherwise tooling that scans that directory for agent definitions would treat it as an agent needing frontmatter (ADR-0010).
If a research `sources.md` is present in the conversation context:
1. Filter to entries with `` `extracted` `` status only.
2. For each entry, identify which agent file it contributed to.
3. Write `sources.md` at the package root using the format below. Paths in `Contributing files:` are relative to the package root.
```markdown
# Sources
## slug-name
- **URL:** <source URL>
- **Research doc:** <path/to/research/sources.md relative to repo root>
- **Description:** <what this source covers>
- **Contributing files:** .apm/agents/<name>.agent.md
- **Status:** `extracted`
```
Each slug must match an H2 heading, and each slug must also appear in the `source_keys` list of the file listed under `Contributing files:`.
If no research sources are in context, delete `sources.md`.
### Step 5 — Validate and close
Run this checklist before invoking the audit:
**Plugin/APM scope — single file (`<name>.agent.md`):**
- [ ] `name` field present, kebab-case, unique in scope
- [ ] `description` field present and action-first
- [ ] Frontmatter contains ONLY `name`, `description`, and optionally `model` (plus `source_keys` if research-sourced) — no `tools`, `isolation`, `maxTurns`, `effort`, `memory`, `permissionMode`, `disallowedTools`, `skills`, `color`, `initialPrompt`, `background`, `hooks`, or `mcpServers`
- [ ] System prompt body present and non-empty
- [ ] No `FILL IN:` placeholders remain
- [ ] No `<!-- -->` template comments remain in frontmatter
**Project/user scope — Claude Code file (`<name>.md`):**
- [ ] `name` field present, kebab-case, unique in scope
- [ ] `description` field present and action-first
- [ ] System prompt body present and non-empty
- [ ] No `FILL IN:` placeholders remain
- [ ] No `<!-- -->` template comments remain in frontmatter
**Project/user scope — Copilot CLI file (`<name>.agent.md`):**
- [ ] File extension is `.agent.md` (not `.md`)
- [ ] `name` field matches the filename stem (e.g. `name: my-agent` in `my-agent.agent.md`)
- [ ] `description` field present
- [ ] No Claude Code-only fields (`maxTurns`, `isolation`, `memory`, `permissionMode`, `effort`, `hooks`, `mcpServers`)
- [ ] System prompt body present and non-empty
- [ ] Body does not exceed 30,000 characters
- [ ] No `<!-- -->` template comments remain in frontmatter
At plugin/APM scope, apply a **minor bump** to the resolved package's `apm.yml` `version` (single manifest, e.g. `1.0.4` → `1.1.0`).
Invoke `kyberforge:agent-audit` on the created file(s) before closing — validates the pair at project/user scope, the single file at plugin/APM scope.
**Commit verification.** Capture `git log --oneline -1` before Step 1 and keep it. Once the audit is clean, run `git add` and `git commit` for the new agent files — do not stop at staging. Then run `git log --oneline -1` again and confirm the hash changed from the one you captured at the start. A non-empty `git diff --stat` is not sufficient proof of completion: staged-but-uncommitted work isn't part of any commit and can be silently lost if the working tree is cleaned up before a commit lands. Only report the agent as done once the hash has actually changed.
## Improving an existing agent
### Step 1 — Verify inputs
Confirm the agent files exist and at least one improvement signal is present in the conversation or a referenced file.
If no signals: "This skill applies existing signals to an agent. For a blind review, examine the files manually or run a grill session first."
Verify `kyberforge:agent-audit` is available — it ships with the kyberforge plugin and is co-installed with this skill. If unavailable, stop and tell the user to install the kyberforge plugin before continuing.
Capture `git log --oneline -1` now, before making any edits — Step 5 needs it to verify a real commit landed.
**Partial state (project/user scope only)** — if one provider file exists but not the other, scaffold the missing one (`bash scripts/new-agent.sh <name> <root>`, file-by-file no-op) then continue. Doesn't apply at plugin/APM scope — single file, no partial-pair state.
### Step 2 — Gather and group signals
Read the current agent file(s). Collect all signals from the conversation.
Group by **root cause**, not symptom. One root cause → one fix.
```text
Example:
- User feedback: agent keeps trying to push to remote
- Session context: no scope boundary in system prompt
→ Root cause: system prompt lacks git scope constraint → fix: add explicit boundary
```
### Step 3 — Announce planned changes
Before editing, state which root causes were identified, what evidence supports each, and which files will change. Then proceed — edits are reversible via git.
### Step 4 — Apply changes
Edit any file the signals point to. Generalize the fix — find the underlying gap, not the specific example that failed. For every sentence you add, ask: "Would the agent get this wrong without it?" A shorter, focused definition consistently outperforms an exhaustive one. For Copilot files, verify no Claude Code-only fields are introduced. For a plugin/APM-scope single file, verify no field beyond `name`, `description`, `model`, and `source_keys` is introduced.
If the edit adds or removes research-sourced content, update `source_keys` in the edited file(s) and the corresponding entry in `sources.md` per Create flow's Step 4.
### Step 5 — Validate and close
Re-run the validation checklist from the create flow's Step 5 on any edited file.
At plugin/APM scope, apply a **patch bump** to the resolved package's `apm.yml` `version` (e.g. `1.0.4` → `1.0.5`).
Invoke `kyberforge:agent-audit` on the edited file(s) to confirm no regressions — the pair at project/user scope, the single file at plugin/APM scope.
**Commit verification.** Capture `git log --oneline -1` at the start of Step 1 and keep it. Once the audit is clean, run `git add` and `git commit` for the changed files — do not stop at staging. Then run `git log --oneline -1` again and confirm the hash changed from the one you captured at the start. A non-empty `git diff --stat` is not sufficient proof of completion: staged-but-uncommitted work isn't part of any commit and can be silently lost if the working tree is cleaned up before a commit lands. Only report the improvement as done once the hash has actually changed.

View File

@@ -1,9 +0,0 @@
# assets/
## templates/
Annotated agent definition templates copied by `scripts/new-agent.sh` when scaffolding a new agent.
- **`claude-code.md`** — Claude Code agent definition template (project/user scope). Includes all supported frontmatter fields (required and optional) with inline guidance comments and `FILL IN:` placeholders.
- **`copilot.agent.md`** — Copilot CLI agent definition template (CLI format, project/user scope). Excludes cloud/IDE-only fields (`target`, `user-invocable`, `disable-model-invocation`, `mcp-servers`) and Claude Code-only fields. Uses Copilot tool aliases (`execute`, `read`, `edit`, `search`, `agent`, `web`).
- **`apm-agent.md`** — Vendor-neutral APM agent definition template (plugin/APM scope). Only `name`, `description`, optional `model`, and optional `source_keys` (provenance metadata, not a runtime field) in frontmatter — no `tools` and no Claude-only fields, since `apm compile` copies frontmatter verbatim to both the Claude Code and Copilot CLI targets with no per-target integrator (ADR-0016).

View File

@@ -1,54 +0,0 @@
---
<!-- Vendor-neutral APM agent definition (plugin/APM scope).
Path: <package-root>/.apm/agents/<name>.agent.md — one file, no counterpart.
`apm compile` copies this frontmatter verbatim to BOTH the Claude Code and
Copilot CLI targets — there is no per-target field integrator. Claude's
`tools:` (space-separated string) and Copilot's `tools:` (alias list) are
incompatible vocabularies, and Claude-only fields (isolation, maxTurns,
effort, memory, permissionMode) have no Copilot equivalent. A value correct
for one harness is guaranteed wrong on the other, so this scope carries
ONLY the fields below — full stop (see ADR-0016). `source_keys` is
provenance metadata, not a runtime field, and is exempt from that rule.
Do NOT add: tools, isolation, maxTurns, effort, memory, permissionMode,
disallowedTools, skills, color, initialPrompt, background, hooks, or
mcpServers. Omitting `tools` means inherit-all-tools on both harnesses,
which is never wrong.
Fill in all FILL IN: placeholders. Delete template comments before shipping. -->
name: AGENT_NAME
<!-- Required. Lowercase letters and hyphens only. Must be unique within the scope. -->
description: FILL IN: Action-first description of what this agent does and when to invoke it.
<!-- Required. The primary signal for autonomous delegation.
Start with a verb: "Reviews...", "Analyzes...", "Generates..."
Be specific about the triggering condition and expertise domain.
Example: "Reviews pull request diffs for security issues. Use proactively after code changes." -->
<!-- model: sonnet
Optional. Aliases: sonnet, opus, haiku, fable. Or full model ID.
Omit to inherit the runtime default on whichever harness compiles this file. -->
<!-- source_keys:
- slug-name
Development-only. Add when research sources informed this agent (slugs must match
sources.md at the package root — same directory as apm.yml).
Omit when no research was used. Not a runtime field — silently ignored by both harnesses. -->
---
FILL IN: System prompt body. Write as a direct role instruction.
You are a FILL IN: role description. When invoked, FILL IN: primary action.
## Inputs
FILL IN: What inputs does this agent expect? (files, context, parameters)
## Process
FILL IN: Steps the agent takes. Be specific about ordering if it matters.
## Output
FILL IN: What does the agent produce? Format, location, structure.

View File

@@ -1,86 +0,0 @@
---
<!-- Claude Code agent definition
Fill in all FILL IN: placeholders. Remove or uncomment optional fields as needed.
Delete template comments before shipping. -->
name: AGENT_NAME
<!-- Required. Lowercase letters and hyphens only. Must be unique within the scope.
Duplicate names are silently discarded — no warning is emitted. -->
description: FILL IN: Action-first description of what this agent does and when to invoke it.
<!-- Required. The primary signal for autonomous delegation.
Start with a verb: "Reviews...", "Analyzes...", "Generates..."
Include "Use proactively" to trigger automatic invocation.
Be specific about the triggering condition and domain.
Example: "Reviews pull request diffs for security issues. Use proactively after code changes." -->
<!-- tools: Read Bash Grep
Optional. Space-separated allowlist. Omit to inherit all tools from parent.
Use Agent(type1,type2) to restrict which subagent types this agent can spawn.
Omit Agent entirely to prevent this agent from spawning subagents.
Never available to subagents regardless of tools field:
AskUserQuestion, EnterPlanMode, ExitPlanMode, ScheduleWakeup, WaitForMcpServers
Exception: ExitPlanMode IS available when parent session runs in permissionMode: plan -->
<!-- model: sonnet
Optional. Aliases: sonnet, opus, haiku, fable. Or full model ID.
Omit to inherit from the main session.
Resolution order: CLAUDE_CODE_SUBAGENT_MODEL env var → per-invocation param → this field → session model. -->
<!-- effort: medium
Optional. low / medium / high / xhigh / max. Overrides session effort level for this agent. -->
<!-- maxTurns: 20
Optional. Integer cap on agentic turns. Prevents runaway on bounded tasks. -->
<!-- memory: project
Optional. user / project / local. Enables cross-session MEMORY.md (first 200 lines loaded at startup).
Auto-enables Read/Write/Edit tools. -->
<!-- isolation: worktree
Optional. Set to "worktree" to run in an isolated temporary git worktree.
Auto-cleaned if no changes are made. -->
<!-- color: blue
Optional. UI color: red, blue, green, yellow, purple, orange, pink, cyan. -->
<!-- background: false
Optional. Set true to force background execution. -->
<!-- disallowedTools: mcp__filesystem__write_file
Optional. Space-separated denylist, applied before the tools allowlist.
Supports mcp__* glob patterns (e.g. mcp__filesystem__* to block all filesystem tools). -->
<!-- skills:
- skill-name
Optional. Skill names preloaded into this agent's context at startup.
Different from the source_keys metadata field (which is provenance-only). -->
<!-- initialPrompt: "Start by reading the README."
Optional. Auto-submitted as the first turn when this agent activates as the main session thread.
Only set when this agent is intended for main-thread (not subagent) activation. -->
<!-- source_keys:
- slug-name
Development-only. Add when research sources informed this agent (slugs must match sources.md at the plugin root).
Omit when no research was used. Not a runtime field — silently ignored by Claude Code. -->
<!-- NOTE: hooks, mcpServers, and permissionMode are silently ignored for plugin agents.
Those fields only work in .claude/agents/ or ~/.claude/agents/. -->
---
FILL IN: System prompt body. Write as a direct role instruction.
You are a FILL IN: role description. When invoked, FILL IN: primary action.
## Inputs
FILL IN: What inputs does this agent expect? (files, context, parameters)
## Process
FILL IN: Steps the agent takes. Be specific about ordering if it matters.
## Output
FILL IN: What does the agent produce? Format, location, structure.

View File

@@ -1,59 +0,0 @@
---
<!-- GitHub Copilot CLI agent definition (CLI format — path: .github/agents/<name>.agent.md)
File extension MUST be .agent.md — a plain .md file is not picked up by Copilot CLI.
Fill in all FILL IN: placeholders. Remove or uncomment optional fields as needed.
Body length limit: 30,000 characters — content beyond this is silently truncated.
Delete template comments before shipping.
NOTE: This template is for the CLI format. The cloud/IDE format (path: .github/copilot/agents/<name>.md,
extension: .md) supports additional fields: target, user-invocable, disable-model-invocation, mcp-servers.
Do not add those fields here — they are silently ignored by the CLI runtime. -->
name: AGENT_NAME
<!-- Required. Kebab-case identifier. Home-directory version wins on name collision. -->
description: FILL IN: Action-first description of what this agent does and when to invoke it.
<!-- Required. Used by the runtime for automatic agent selection — quality matters.
Start with a verb: "Reviews...", "Analyzes...", "Generates..."
Example: "Reviews pull request diffs for security issues." -->
<!-- tools: ["read", "search", "edit"]
Optional. Array of tool names. Omit = all available tools. [] = no tools.
Copilot tool aliases (use these, not Claude Code names):
execute — run shell commands (aliases: shell, Bash, powershell)
read — read file contents (aliases: Read, NotebookRead)
edit — modify files (aliases: Edit, MultiEdit, Write, NotebookEdit)
search — search files (aliases: Grep, Glob)
agent — invoke sub-agents (aliases: custom-agent, Task)
web — web search and fetch (aliases: WebSearch, WebFetch)
For MCP tools: "server-name/tool-name" or "server-name/*" -->
<!-- model: claude-sonnet-4-5
Optional. Model to run this agent on.
Cloud/IDE-only fields (target, user-invocable, disable-model-invocation, mcp-servers)
are not valid in this CLI format — use the .github/copilot/agents/<name>.md path for those. -->
<!-- source_keys:
- slug-name
Development-only. Add when research sources informed this agent (slugs must match sources.md at the plugin root).
Omit when no research was used. Not a Copilot runtime field — silently ignored. -->
<!-- DO NOT include these Claude Code-only fields:
maxTurns, isolation, memory, permissionMode, effort, hooks, mcpServers -->
---
FILL IN: System prompt body. Should match the Claude Code version — the agent's task is the same across providers.
You are a FILL IN: role description. When invoked, FILL IN: primary action.
## Inputs
FILL IN: What inputs does this agent expect? (files, context, parameters)
## Process
FILL IN: Steps the agent takes. Be specific about ordering if it matters.
## Output
FILL IN: What does the agent produce? Format, location, structure.

View File

@@ -1,17 +0,0 @@
---
source_keys: []
---
# references/
## deployment-modes.md
Agent scope hierarchy, precedence rules, and per-scope restrictions. Covers: which fields are silently ignored for plugin agents (Claude Code and Copilot CLI), scoped identifiers for plugin subdirectory agents, cache isolation behaviour, and Copilot CLI path conventions. Loaded conditionally from SKILL.md when the destination is a plugin directory.
## scripts.md
Conventions for the `new-agent.sh` scaffold script and any future scripts added to this skill. Covers: what scripts should and should not do, file placement, error handling, template variable conventions, and the no-interactive-prompts rule.
## sources.md
Research provenance record for this skill. Lists the upstream research sources (claude-code-plugins and github-copilot-plugins research docs) that informed SKILL.md, the templates, and the deployment-modes reference. Used by `skill-audit` to validate the provenance chain.

View File

@@ -1,57 +0,0 @@
---
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- claude-code-subagents-docs
- context7-github-en-copilot
- github-custom-agents-configuration
- github-cli-plugin-reference
---
# Agent Deployment Modes
Agent definitions deploy at three scopes and behave differently at each. The scope determines which fields are honoured, where files must live, and what identifiers users invoke.
## Scope hierarchy and precedence
| Scope | Claude Code path | Copilot CLI path | Who it affects |
|---|---|---|---|
| User | `~/.claude/agents/` | `~/.copilot/agents/` | All sessions for this user |
| Project | `.claude/agents/` | `.github/agents/` or `.copilot/agents/` | This repo only |
| Plugin/APM | `<package-root>/.apm/agents/<name>.agent.md` — single vendor-neutral file, `apm compile` emits it to both targets | *(same file)* | Sessions with the plugin/package installed |
When the same agent `name` appears at multiple scopes, **user scope wins over project scope wins over plugin scope** in Claude Code. In Copilot CLI, repo-level agents override enterprise and org-level; home-directory (user) agents override repo-level on name collision.
## Plugin scope restrictions
Plugin/APM agents (`.apm/agents/<name>.agent.md`) carry only `name`, `description`, optionally `model`, and optionally `source_keys` (provenance metadata, not a runtime field — silently ignored by both harnesses) in frontmatter — full stop (see ADR-0016). `apm compile` copies this frontmatter verbatim to both the Claude Code and Copilot CLI compile targets with no per-target integrator: Claude's `tools:` (space-separated string) and Copilot's `tools:` (alias list) are incompatible vocabularies, and Claude-only fields have no Copilot equivalent, so any harness-specific value is guaranteed wrong on at least one target.
This makes the old "silently ignored at plugin scope" framing moot. It's not that `hooks`, `mcpServers`, `permissionMode`, `tools`, `isolation`, `maxTurns`, `effort`, `memory`, `disallowedTools`, `skills`, `color`, `initialPrompt`, or `background` are merely ignored at this scope — they are never written to the file at all. Copy the agent to `.claude/agents/` (project scope) or `~/.claude/agents/` (user scope) to use any of them.
## Scoped identifiers (Claude Code plugin agents only)
Plugin agents in **subdirectories** get compound identifiers:
```
plugins/my-plugin/agents/review/security.md → my-plugin:review:security
```
Users must invoke with `@agent-my-plugin:review:security`. **Keep agents flat in `agents/` to avoid this** — subdirectory nesting is rarely worth the UX cost.
At project and user scope, subdirectory path does not affect the agent's name.
## Cache isolation
When a plugin is installed, its directory is copied to a cache. **Any path that leaves the agent's plugin directory breaks post-install.** Agent definition files must be self-contained — they cannot reference scripts, templates, or shared files outside the plugin.
Agents at project or user scope are read directly from disk; cache isolation does not apply.
## Copilot CLI path conventions
| Scope | Expected path | Notes |
|---|---|---|
| User | `~/.copilot/agents/<name>.agent.md` | Home directory |
| Project | `.github/agents/<name>.agent.md` | Standard; also `.copilot/agents/` |
| Plugin/APM | `<package-root>/.apm/agents/<name>.agent.md` | Not a Copilot-only file — this is the single vendor-neutral source `apm compile` reads for the Copilot CLI target |
The `.agent.md` extension is **mandatory** for real Copilot CLI files (project/user scope) — Copilot CLI does not pick up plain `.md` files in the `agents/` directory. The plugin/APM source file also uses `.agent.md` by convention, since it compiles to Copilot CLI too, but it is not itself a Copilot file.

View File

@@ -1,42 +0,0 @@
---
source_keys: []
---
# Scripts Reference
Conventions for `scripts/new-agent.sh` and any future scripts in this skill.
## Contract
All scripts in this skill must follow these rules:
- **No interactive prompts** — agents run non-interactive; blocking on TTY input hangs indefinitely. Accept all input via positional arguments, flags, or environment variables.
- **Structured output** — file paths and status messages to stderr; nothing to stdout unless a downstream tool needs to consume it.
- **Idempotent** — "create if not exists" per file. The scaffold script skips any file that already exists; agents may safely re-run it.
- **Meaningful exit codes** — `0` success, `1` invalid arguments or precondition failure. Document in `--help`.
- **Self-contained** — no external package installs at runtime. The script uses only bash builtins and POSIX tools (`sed`, `mkdir`, `cat`).
## Template variables
The scaffold script uses `sed "s/AGENT_NAME/$AGENT_NAME/g"` to substitute the agent name into templates. Template files must use `AGENT_NAME` (all caps, no delimiters) as the substitution token.
Do not add additional substitution tokens unless you update both the template files and the script in the same edit pass.
## File placement
The script creates files at paths determined by scope detection (plugin/APM / project / user). Scope is resolved by walking up from the root directory: a `type:`-bearing `apm.yml` at or above the root marks the package root (plugin/APM scope, single file); an `apm.yml` without a `type:` field is a marketplace-only manifest and is skipped, the walk continues upward. If no such `apm.yml` is found, the root resolving to exactly `$HOME` is user scope; anything else is project scope. If scope detection logic changes, update the `new-agent.sh` usage comment and `SKILL.md` Step 1 scope detection description in the same pass.
## Error messages
On failure, state: what went wrong, what was expected, what to try. Example:
```
Error: agent-name must use lowercase letters, numbers, and hyphens only.
No leading, trailing, or consecutive hyphens.
Received: 'My_Agent'
```
Vague errors leave agents unable to self-correct.
## --help output
Keep `--help` concise — it may enter the agent's context window. Include: usage line, argument descriptions with scope detection table, exit codes. Omit prose explanations.

View File

@@ -1,95 +0,0 @@
---
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- claude-code-subagents-docs
- context7-github-en-copilot
- github-custom-agents-configuration
- github-cli-plugin-reference
- github-plugins-creating
- github-plugins-finding-installing
- github-plugins-marketplace
- github-sdk-custom-agents
---
# Sources
## context7-websites-code-claude
- **URL:** context7:/websites/code_claude
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code documentation site indexed by Context7 — plugin manifest schema, subagent definition types, marketplace JSON format, agent markdown file format
- **Contributing files:** SKILL.md, references/deployment-modes.md
- **Status:** `extracted`
## claude-code-plugins-docs
- **URL:** https://code.claude.com/docs/en/plugins
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code plugin authoring guide — plugin structure, manifest fields, loading methods, skill namespacing, agent activation, marketplace submission
- **Contributing files:** SKILL.md, references/deployment-modes.md
- **Status:** `extracted`
## claude-code-subagents-docs
- **URL:** https://code.claude.com/docs/en/sub-agents
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code subagent reference — definition format, all frontmatter fields, scope priority, built-in agents, CLI flags, environment variables, known limitations
- **Contributing files:** SKILL.md, references/deployment-modes.md
- **Status:** `extracted`
## context7-github-en-copilot
- **URL:** context7:/websites/github_en_copilot
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Official GitHub Copilot documentation indexed by Context7; covers CLI plugins, custom agents, SDK, and marketplace
- **Contributing files:** SKILL.md, references/deployment-modes.md
- **Status:** `extracted`
## github-custom-agents-configuration
- **URL:** https://docs.github.com/en/copilot/reference/custom-agents-configuration
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Reference for cloud and IDE custom agent definition format — frontmatter fields, tool aliases, MCP server config, secrets interpolation, scoping hierarchy
- **Contributing files:** SKILL.md, references/deployment-modes.md
- **Status:** `extracted`
## github-cli-plugin-reference
- **URL:** https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-plugin-reference
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Full CLI plugin reference — plugin.json schema, marketplace.json schema, all CLI commands and flags, install specification formats, loading precedence, env vars, LSP config
- **Contributing files:** SKILL.md, references/deployment-modes.md
- **Status:** `extracted`
## github-plugins-creating
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-creating
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** How-to for creating Copilot CLI plugins — plugin structure, agent and skill authoring, hooks format, MCP config, development lifecycle
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## github-plugins-finding-installing
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-finding-installing
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** User-facing guide to discovering and installing CLI plugins — marketplace browsing commands, install/update/uninstall workflow
- **Contributing files:** (none)
- **Status:** `extracted`
## github-plugins-marketplace
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-marketplace
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** How-to for creating and publishing a plugin marketplace — marketplace.json structure, hosting options, registration commands
- **Contributing files:** (none)
- **Status:** `extracted`
## github-sdk-custom-agents
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-sdk/features/custom-agents
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** SDK custom agent API — CustomAgentConfig fields in all five languages, session config, sub-agent lifecycle events, tool scoping, permission handling
- **Contributing files:** (none)
- **Status:** `extracted`

View File

@@ -1,13 +0,0 @@
# scripts/
## new-agent.sh
Scaffolds agent definition file(s) from a single root directory input.
```
Usage: new-agent.sh <agent-name> <root>
```
Resolves scope by walking up from `<root>`: a `type:`-bearing `apm.yml` found at or above `<root>` → plugin/APM scope (single file at `<package-root>/.apm/agents/<name>.agent.md`; an `apm.yml` without `type:` is a marketplace-only manifest and is skipped); `<root>` exactly `~` → user scope (`~/.claude/agents/` + `~/.copilot/agents/`); otherwise project scope (`<root>/.claude/agents/` + `<root>/.github/agents/`). Each file is a no-op if it already exists. See `--help` for full usage.
Tests: `tests/new-agent.bats` (requires `bats-support` and `bats-assert`).

View File

@@ -1,277 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_ROOT="$(cd "$SKILL_DIR/.." && pwd)"
TEMPLATES_DIR="$SKILL_ROOT/assets/templates"
usage() {
cat <<EOF
Usage: new-agent.sh <agent-name> <root>
Scaffold agent definition file(s) for Claude Code, GitHub Copilot CLI, and/or
vendor-neutral APM packages.
Arguments:
agent-name Kebab-case agent identifier (e.g. code-reviewer, deploy-assistant).
root Starting directory — scope is resolved by walking up from here:
plugin/APM scope : nearest ancestor (at/above root) whose apm.yml
has a top-level type: field (instructions,
skill, hybrid, or prompts) — an apm.yml
without type: is a marketplace-only manifest
and is skipped, the walk continues upward
→ creates <package-root>/.apm/agents/<name>.agent.md
(single vendor-neutral file — no tools,
isolation, maxTurns, effort, memory, or
permissionMode; apm compile has no per-target
field integrator, see ADR-0016)
→ creates <package-root>/sources.md (if absent)
project scope : no type:-bearing apm.yml found; root is a
project directory
→ creates <root>/.claude/agents/<name>.md
→ creates <root>/.github/agents/<name>.agent.md
user scope : root is exactly ~ (home directory; checked
directly, no walk-up)
→ creates ~/.claude/agents/<name>.md
→ creates ~/.copilot/agents/<name>.agent.md
Each file is created only if it does not already exist (no-op per file).
Exit codes:
0 Files created or already existed (no-op)
1 Invalid arguments, missing root, or templates not found
EOF
}
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage
exit 0
fi
if [[ $# -lt 2 ]]; then
echo "Error: agent-name and root are required." >&2
echo "" >&2
usage >&2
exit 1
fi
AGENT_NAME="$1"
ROOT="$2"
# Validate agent name format
if ! echo "$AGENT_NAME" | grep -qE '^[a-z0-9]+(-[a-z0-9]+)*$'; then
echo "Error: agent-name must use lowercase letters, numbers, and hyphens only." >&2
echo " No leading, trailing, or consecutive hyphens." >&2
echo " Received: '$AGENT_NAME'" >&2
exit 1
fi
# Validate templates directory
if [[ ! -d "$TEMPLATES_DIR" ]]; then
echo "Error: templates directory not found at '$TEMPLATES_DIR'." >&2
echo " Run this script from its original location inside the agent-author skill." >&2
exit 1
fi
# Expand tilde
ROOT="${ROOT/#\~/$HOME}"
# Validate root exists
if [[ ! -d "$ROOT" ]]; then
echo "Error: root directory '$ROOT' does not exist." >&2
exit 1
fi
ROOT="$(cd "$ROOT" && pwd)"
# True if apm_yml's top-level `type:` line names one of the four APM package
# types (instructions/skill/hybrid/prompts) — mirrors validate.sh's
# APM_TYPE_RE: an optional quote around the value must be closed by the
# *same* quote character (a mismatched or unterminated quote is rejected,
# not silently stripped), and the value must be followed by whitespace or
# end-of-line so `prompts-only` doesn't false-match on the `prompts` prefix.
# `|| [[ -n "$line" ]]` in the read condition also processes a final line
# that lacks a trailing newline, which `read` alone would otherwise skip.
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
}
# --- Walk-up package-root detection ---
#
# Mirrors agent-audit's validate.sh scope walk-up, with apm.yml + type: swapped
# in for the old plugin.json marker. Starting at ROOT, walk upward:
# - an apm.yml with a top-level `type:` field marks an APM package root
# (plugin/APM scope) — stop and return it.
# - an apm.yml with no `type:` field is a marketplace-only manifest — skip
# it, keep walking up.
# - user scope is checked directly at $HOME, no walk-up (see usage text
# above): ROOT itself being $HOME resolves to user scope, even if $HOME
# is itself a .git-tracked dotfiles directory (checked before the .git
# test below, so a dotfiles repo at $HOME can't shadow user scope).
# Walking *up into* $HOME from a nested directory with no apm.yml/.git
# of its own does NOT promote to user scope — it resolves to project
# scope instead, same as any other unmatched boundary, so a stray
# directory under $HOME can't be silently redirected into the shared
# global ~/.claude or ~/.copilot agent directories.
# - a .git file or directory marks the project-scope boundary (a worktree's
# .git is a file, not a directory) — stop.
# - filesystem root reached with neither found — project scope, same as
# any other unmatched boundary.
find_package_root() {
local root="$1" current="$1"
while true; do
if [[ -f "$current/apm.yml" ]] && is_apm_package_manifest "$current/apm.yml"; then
echo "plugin $current"
return
fi
if [[ "$current" == "$HOME" ]]; then
if [[ "$current" == "$root" ]]; then
echo "user $current"
return
fi
echo "project $current"
return
fi
if [[ -e "$current/.git" ]]; then
echo "project $current"
return
fi
local parent
parent="$(dirname "$current")"
if [[ "$parent" == "$current" ]]; then
echo "project $current"
return
fi
current="$parent"
done
}
# `read` consumes a single line, so kind and path are emitted on one
# space-separated line rather than two `echo`s — kind first (never contains
# spaces), path last (absorbs any spaces in the path safely).
WALK_RESULT="$(find_package_root "$ROOT")"
read -r WALK_KIND WALK_ROOT <<< "$WALK_RESULT"
PACKAGE_ROOT=""
case "$WALK_KIND" in
plugin)
SCOPE="plugin"
PACKAGE_ROOT="$WALK_ROOT"
;;
user)
SCOPE="user"
;;
project)
SCOPE="project"
;;
esac
# Determine file destinations
case "$SCOPE" in
plugin)
APM_DIR="$PACKAGE_ROOT/.apm/agents"
SOURCES_DIR="$PACKAGE_ROOT"
;;
project)
CC_DIR="$ROOT/.claude/agents"
CP_DIR="$ROOT/.github/agents"
SOURCES_DIR=""
;;
user)
CC_DIR="$HOME/.claude/agents"
CP_DIR="$HOME/.copilot/agents"
SOURCES_DIR=""
;;
esac
created_any=false
if [[ "$SCOPE" == "plugin" ]]; then
APM_FILE="$APM_DIR/$AGENT_NAME.agent.md"
mkdir -p "$APM_DIR"
if [[ -f "$APM_FILE" ]]; then
echo "Skipping '$APM_FILE' — already exists." >&2
else
sed "s/AGENT_NAME/$AGENT_NAME/g" "$TEMPLATES_DIR/apm-agent.md" > "$APM_FILE"
echo "Created: $APM_FILE" >&2
created_any=true
fi
else
CC_FILE="$CC_DIR/$AGENT_NAME.md"
CP_FILE="$CP_DIR/$AGENT_NAME.agent.md"
mkdir -p "$CC_DIR"
mkdir -p "$CP_DIR"
# Copy Claude Code template (no-op if exists)
if [[ -f "$CC_FILE" ]]; then
echo "Skipping '$CC_FILE' — already exists." >&2
else
sed "s/AGENT_NAME/$AGENT_NAME/g" "$TEMPLATES_DIR/claude-code.md" > "$CC_FILE"
echo "Created: $CC_FILE" >&2
created_any=true
fi
# Copy Copilot template (no-op if exists)
if [[ -f "$CP_FILE" ]]; then
echo "Skipping '$CP_FILE' — already exists." >&2
else
sed "s/AGENT_NAME/$AGENT_NAME/g" "$TEMPLATES_DIR/copilot.agent.md" > "$CP_FILE"
echo "Created: $CP_FILE" >&2
created_any=true
fi
fi
# Create sources.md at plugin/APM package root (no-op if exists)
if [[ -n "$SOURCES_DIR" ]]; then
SOURCES_FILE="$SOURCES_DIR/sources.md"
if [[ -f "$SOURCES_FILE" ]]; then
echo "Skipping '$SOURCES_FILE' — already exists." >&2
else
cat > "$SOURCES_FILE" <<'SOURCES'
# Sources
<!-- List research sources that informed agents in this package.
Follow the format below. Only include entries with `extracted` status.
Delete this file if no research sources informed these agents. -->
<!-- ## source-slug
- **URL:** <url>
- **Research doc:** <relative-path-to-upstream-research-sources-file>
- **Description:** <what this source covers>
- **Contributing files:** .apm/agents/<name>.agent.md
- **Status:** `extracted` -->
SOURCES
echo "Created: $SOURCES_FILE" >&2
created_any=true
fi
fi
if [[ "$created_any" == false ]]; then
echo "All files already exist — nothing to do." >&2
else
echo "" >&2
echo "Scope: $SCOPE" >&2
echo "" >&2
echo "Next steps:" >&2
if [[ "$SCOPE" == "plugin" ]]; then
echo " 1. Fill in $APM_FILE — replace all FILL IN: placeholders (name, description, model, body only)" >&2
echo " 2. Populate $SOURCES_DIR/sources.md with research sources, or delete it" >&2
echo " 3. Validate: check required fields (name, description, system prompt) in the file" >&2
else
echo " 1. Fill in $CC_FILE — replace all FILL IN: placeholders" >&2
echo " 2. Fill in $CP_FILE — replace all FILL IN: placeholders" >&2
echo " 3. Validate: check required fields (name, description, system prompt) in both files" >&2
fi
fi

View File

@@ -1,33 +0,0 @@
# tests/
Test files for scripts bundled with this skill.
## When to add tests
Add tests here when the skill has scripts in `scripts/` that are complex enough
to break silently — validators, parsers, generators, anything with branching
logic or edge cases. Test infrastructure (`.bats`, `*_test.*`, `test_*.sh`)
belongs here, not in `scripts/`.
## Dependencies
Tests require [bats-support](https://github.com/bats-core/bats-support) and
[bats-assert](https://github.com/bats-core/bats-assert). The test files load
helpers from the repo root's `tests/test_helper/`.
From the repo root:
```bash
git clone https://github.com/bats-core/bats-support tests/test_helper/bats-support
git clone https://github.com/bats-core/bats-assert tests/test_helper/bats-assert
```
Run all tests for this skill (from the repo root):
```bash
bats plugins/kyberforge/skills/agent-author/tests/
```
## If no tests are needed
Delete this README and the `tests/` directory entirely.

View File

@@ -1,301 +0,0 @@
#!/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-agent.sh"
ROOT="$(mktemp -d)"
}
teardown() {
rm -rf "$ROOT"
}
# ---------------------------------------------------------------------------
# Help
# ---------------------------------------------------------------------------
@test "--help exits 0" {
run bash "$SCRIPT" --help
assert_success
assert_output --partial "Usage:"
}
# ---------------------------------------------------------------------------
# Plugin/APM scope (type:-bearing apm.yml at root)
# ---------------------------------------------------------------------------
@test "plugin/APM scope: creates single agent file in .apm/agents/" {
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ]
}
@test "plugin/APM scope: does not create the old dual-file pair" {
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ ! -f "$ROOT/agents/my-agent.md" ]
assert [ ! -f "$ROOT/agents/my-agent.agent.md" ]
}
@test "plugin/APM scope: creates .apm/agents/ directory if missing" {
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ -d "$ROOT/.apm/agents" ]
}
@test "plugin/APM scope: creates sources.md at package root" {
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ -f "$ROOT/sources.md" ]
}
@test "plugin/APM scope: no-op if agent file already exists" {
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
mkdir -p "$ROOT/.apm/agents"
echo "existing" > "$ROOT/.apm/agents/my-agent.agent.md"
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
run grep "existing" "$ROOT/.apm/agents/my-agent.agent.md"
assert_success
}
@test "plugin/APM scope: frontmatter has no tools/isolation/maxTurns/effort/memory/permissionMode keys" {
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
bash "$SCRIPT" my-agent "$ROOT"
file="$ROOT/.apm/agents/my-agent.agent.md"
fm="$(sed -n '/^---$/,/^---$/p' "$file")"
# Column-0 key lines only — comment bodies in the template are indented,
# so this anchor naturally excludes commented-out example fields.
run grep -E '^(tools|isolation|maxTurns|effort|memory|permissionMode|disallowedTools|skills|color|initialPrompt|background|hooks|mcpServers):' <<< "$fm"
assert_failure
}
@test "plugin/APM scope: frontmatter carries only name, description, model, source_keys fields" {
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
bash "$SCRIPT" my-agent "$ROOT"
file="$ROOT/.apm/agents/my-agent.agent.md"
fm="$(sed -n '/^---$/,/^---$/p' "$file")"
keys="$(grep -oE '^[a-zA-Z][a-zA-Z0-9_-]*:' <<< "$fm" | sed 's/:$//' | sort -u)"
for key in $keys; do
if [[ "$key" != "name" && "$key" != "description" && "$key" != "model" && "$key" != "source_keys" ]]; then
fail "unexpected frontmatter key: $key"
fi
done
}
@test "plugin/APM scope: sources.md contributing-files template mentions the single-file path" {
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
bash "$SCRIPT" my-agent "$ROOT"
run grep ".apm/agents/<name>.agent.md" "$ROOT/sources.md"
assert_success
}
@test "plugin/APM scope: template AGENT_NAME substituted" {
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
bash "$SCRIPT" my-agent "$ROOT"
run grep "my-agent" "$ROOT/.apm/agents/my-agent.agent.md"
assert_success
}
@test "plugin/APM scope: walk-up finds apm.yml at an ancestor directory, not just root arg" {
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
mkdir -p "$ROOT/nested/subdir"
run bash "$SCRIPT" my-agent "$ROOT/nested/subdir"
assert_success
assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ]
}
@test "plugin/APM scope: apm.yml without type: is skipped (marketplace-only manifest)" {
printf 'name: my-marketplace\n' > "$ROOT/apm.yml"
mkdir -p "$ROOT/.git"
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ ! -f "$ROOT/.apm/agents/my-agent.agent.md" ]
assert [ -f "$ROOT/.claude/agents/my-agent.md" ]
}
@test "plugin/APM scope: type:-less apm.yml at leaf falls through to a type:-bearing apm.yml higher up" {
printf 'name: outer-package\ntype: skill\n' > "$ROOT/apm.yml"
mkdir -p "$ROOT/inner"
printf 'name: inner-marketplace\n' > "$ROOT/inner/apm.yml"
run bash "$SCRIPT" my-agent "$ROOT/inner"
assert_success
assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ]
}
@test "plugin/APM scope: matched-quote type value ('skill') is recognized" {
printf 'name: my-package\ntype: "skill"\n' > "$ROOT/apm.yml"
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ]
}
@test "plugin/APM scope: mismatched-quote type value is rejected, falls through to project scope" {
mkdir -p "$ROOT/.git"
printf "name: my-package\ntype: \"skill'\n" > "$ROOT/apm.yml"
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ ! -f "$ROOT/.apm/agents/my-agent.agent.md" ]
assert [ -f "$ROOT/.claude/agents/my-agent.md" ]
}
@test "plugin/APM scope: type: line is recognized even without a trailing newline on the file" {
printf 'name: my-package\ntype: skill' > "$ROOT/apm.yml"
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ]
}
# ---------------------------------------------------------------------------
# Old plugin.json marker is no longer recognized (full switch, no dual-mode)
# ---------------------------------------------------------------------------
@test "bare plugin.json (no apm.yml) is no longer detected as plugin scope — falls through to project scope" {
touch "$ROOT/plugin.json"
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ ! -f "$ROOT/agents/my-agent.md" ]
assert [ ! -f "$ROOT/agents/my-agent.agent.md" ]
assert [ ! -f "$ROOT/.apm/agents/my-agent.agent.md" ]
assert [ -f "$ROOT/.claude/agents/my-agent.md" ]
assert [ -f "$ROOT/.github/agents/my-agent.agent.md" ]
}
# ---------------------------------------------------------------------------
# Non-plugin (project) scope
# ---------------------------------------------------------------------------
@test "non-plugin scope: creates claude code file in .claude/agents/" {
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ -f "$ROOT/.claude/agents/my-agent.md" ]
}
@test "non-plugin scope: creates copilot file in .github/agents/" {
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ -f "$ROOT/.github/agents/my-agent.agent.md" ]
}
@test "non-plugin scope: creates .claude/agents/ directory if missing" {
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ -d "$ROOT/.claude/agents" ]
}
@test "non-plugin scope: creates .github/agents/ directory if missing" {
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ -d "$ROOT/.github/agents" ]
}
@test "non-plugin scope: no sources.md created" {
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
assert [ ! -f "$ROOT/.claude/agents/sources.md" ]
assert [ ! -f "$ROOT/.github/agents/sources.md" ]
}
@test "non-plugin scope: no-op if claude code file already exists" {
mkdir -p "$ROOT/.claude/agents"
echo "existing" > "$ROOT/.claude/agents/my-agent.md"
run bash "$SCRIPT" my-agent "$ROOT"
assert_success
run grep "existing" "$ROOT/.claude/agents/my-agent.md"
assert_success
}
@test "project scope: detected via .git present above root, output still relative to root arg" {
mkdir -p "$ROOT/repo/.git"
mkdir -p "$ROOT/repo/pkg"
run bash "$SCRIPT" my-agent "$ROOT/repo/pkg"
assert_success
assert [ -f "$ROOT/repo/pkg/.claude/agents/my-agent.md" ]
assert [ -f "$ROOT/repo/pkg/.github/agents/my-agent.agent.md" ]
}
# ---------------------------------------------------------------------------
# User scope
# ---------------------------------------------------------------------------
@test "user scope: root exactly \$HOME creates files under ~/.claude and ~/.copilot" {
FAKE_HOME="$(mktemp -d)"
run env HOME="$FAKE_HOME" bash "$SCRIPT" my-agent "~"
assert_success
assert [ -f "$FAKE_HOME/.claude/agents/my-agent.md" ]
assert [ -f "$FAKE_HOME/.copilot/agents/my-agent.agent.md" ]
rm -rf "$FAKE_HOME"
}
@test "user scope: \$HOME being a dotfiles .git repo does not shadow user scope" {
FAKE_HOME="$(mktemp -d)"
mkdir "$FAKE_HOME/.git"
run env HOME="$FAKE_HOME" bash "$SCRIPT" my-agent "~"
assert_success
assert [ -f "$FAKE_HOME/.claude/agents/my-agent.md" ]
assert [ -f "$FAKE_HOME/.copilot/agents/my-agent.agent.md" ]
refute [ -d "$FAKE_HOME/.github" ]
rm -rf "$FAKE_HOME"
}
@test "user scope is checked directly at \$HOME, no walk-up: a marker-less subdir under \$HOME resolves to project scope, not user scope" {
FAKE_HOME="$(mktemp -d)"
mkdir -p "$FAKE_HOME/scratch/testdir"
run env HOME="$FAKE_HOME" bash "$SCRIPT" my-agent "$FAKE_HOME/scratch/testdir"
assert_success
assert [ -f "$FAKE_HOME/scratch/testdir/.claude/agents/my-agent.md" ]
assert [ -f "$FAKE_HOME/scratch/testdir/.github/agents/my-agent.agent.md" ]
refute [ -f "$FAKE_HOME/.claude/agents/my-agent.md" ]
refute [ -f "$FAKE_HOME/.copilot/agents/my-agent.agent.md" ]
rm -rf "$FAKE_HOME"
}
# ---------------------------------------------------------------------------
# Name validation
# ---------------------------------------------------------------------------
@test "fails when no arguments given" {
run bash "$SCRIPT"
assert_failure
}
@test "fails when agent name contains uppercase" {
run bash "$SCRIPT" MyAgent "$ROOT"
assert_failure
}
@test "fails when agent name has consecutive hyphens" {
run bash "$SCRIPT" my--agent "$ROOT"
assert_failure
}
@test "fails when agent name has a leading hyphen" {
run bash "$SCRIPT" -my-agent "$ROOT"
assert_failure
}
@test "fails when agent name has a trailing hyphen" {
run bash "$SCRIPT" my-agent- "$ROOT"
assert_failure
}
@test "agent name with numbers is valid" {
run bash "$SCRIPT" agent-v2 "$ROOT"
assert_success
}
# ---------------------------------------------------------------------------
# Root validation
# ---------------------------------------------------------------------------
@test "fails when root directory does not exist" {
run bash "$SCRIPT" my-agent "/nonexistent/path"
assert_failure
}

View File

@@ -1,22 +0,0 @@
# apm-install
Installs and configures the `apm` (Agent Package Manager) CLI and the agent runtimes it manages.
## What it does
Covers the two provisioning steps for working with apm: installing the `apm` binary itself (quick-install script, pinned version, air-gapped mirror, or pip), and installing/managing an agent runtime apm drives (`apm runtime setup copilot|codex|gemini|llm`, listing installed runtimes, checking which one `apm run` defaults to).
## Usage
```
/apm-install
```
Once `apm` and a runtime are in place, use `apm-workflow` for authoring `apm.yml`, scaffolding packages/marketplaces, compiling, packing, publishing, and auditing.
## Files
| File | Purpose |
|------|---------|
| `SKILL.md` | Skill instructions for agents |
| `references/sources.md` | Provenance chain — research sources that informed this skill |

View File

@@ -1,53 +0,0 @@
---
name: apm-install
description: >
Use when the user wants to install the apm (Agent Package Manager) CLI
itself, pin or upgrade its version, set up an air-gapped/enterprise mirror
install, or install and manage an agent runtime that apm drives (Copilot
CLI, Codex, Gemini, generic llm) — "install apm", "set up apm", "pin apm to
a version", "apm runtime setup", "which runtime will apm run pick". Do not
use for authoring apm.yml, scaffolding a package/marketplace, compiling,
packing, publishing, or running apm audit — use apm-workflow for those.
metadata:
category: apm
source_keys:
- context7-microsoft-apm
---
## Gotchas
- apm does not execute agents itself — it only installs and manages the runtimes that do. "Install apm" and "install a runtime apm manages" are two separate steps; don't conflate them or skip the second when the user actually wants a working agent CLI, not just the package manager.
- The air-gapped/enterprise mirror path needs `GITHUB_URL` and `VERSION` set together against a downloaded `install.sh` — it does not work through the piped one-liner form.
- `pip install apm-cli` requires Python 3.10+; the quick-install script has no such prerequisite. Prefer the quick-install script unless the environment is pip-first.
- Installing the Copilot CLI runtime through `apm runtime setup copilot` requires Node.js v22+ and npm v10+ already present — apm does not install Node/npm for you.
## Install apm
Default:
```bash
curl -sSL https://aka.ms/apm-unix | sh
```
Escape hatches — combine as needed:
- Pin a version: append `@vX.Y.Z` to the piped script's arguments, e.g. `curl -sSL https://aka.ms/apm-unix | sh -s -- @v1.2.3`.
- Custom install directory: set `APM_INSTALL_DIR` on the piped script's command, e.g. `curl -sSL https://aka.ms/apm-unix | APM_INSTALL_DIR=$HOME/.local/bin sh`.
- Air-gapped / GitHub Enterprise mirror: download `install.sh` first, then run it with `GITHUB_URL` and `VERSION` set, e.g. `GITHUB_URL=https://github.corp.com VERSION=v1.2.3 sh install.sh`.
- pip (Python 3.10+ environments): `pip install apm-cli`.
- Manual: download the platform archive from the GitHub releases page, extract, place the binary on `PATH`.
Verify with `apm --version`.
## Install or manage an agent runtime
Default:
```bash
apm runtime setup copilot
```
Other targets: `apm runtime setup codex`, `apm runtime setup gemini`, `apm runtime setup llm`.
- `apm runtime list` — show installed runtimes.
- `apm runtime status` — show which runtime `apm run` will pick by default.
- `apm runtime remove <name> -y` — uninstall without an interactive prompt.

View File

@@ -1,9 +0,0 @@
# Sources
## context7-microsoft-apm
- **URL:** context7:/microsoft/apm
- **Description:** Microsoft APM (Agent Package Manager) — open-source dependency manager for AI agent configuration (skills, prompts, instructions, agents, hooks, MCP/LSP deps), applying a declare/lock/install/audit workflow.
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md
- **Contributing files:** SKILL.md
- **Status:** `extracted`

View File

@@ -1,33 +0,0 @@
# apm-workflow
Authors, scaffolds, compiles, and audits apm packages and marketplaces.
## What it does
Covers the apm.yml lifecycle a session moves through repeatedly: configuring/scaffolding a package manifest, resolving/fetching its declared dependencies, building or registering a marketplace, compiling/packing/publishing a distributable, and validating integrity via apm audit. Dispatches by requested action to one of five reference files, each self-contained for its concern.
## Before you start
Requires the `apm` binary and (for runtime-driven scripts) an agent runtime already installed — use `apm-install` first if either is missing.
## Usage
```
/apm-workflow configure
/apm-workflow install
/apm-workflow marketplace
/apm-workflow compile
/apm-workflow audit
```
## Files
| File | Purpose |
|------|---------|
| `SKILL.md` | Dispatch table and cross-cutting gotchas |
| `references/configure.md` | apm.yml schema, apm plugin init, dependency forms, MCP secrets, registries |
| `references/install.md` | apm install, apm install [PACKAGE_REF], --update, --target agent-skills |
| `references/marketplace.md` | Building/registering a marketplace, package registration, versioning, Claude Code reserved-name/publish-confirm gotchas |
| `references/compile.md` | apm compile / pack / publish / run, claude plugin validate agents/ gotcha |
| `references/audit.md` | apm audit, apm audit --ci, apm marketplace check, CI wiring, frozen installs, claude plugin validate terminal check |
| `references/sources.md` | Provenance chain — research sources that informed this skill |

View File

@@ -1,45 +0,0 @@
---
name: apm-workflow
description: >
Use when the user wants to author or edit an apm.yml manifest
(dependencies, scripts, compilation, policy, registries), scaffold a new
apm package or marketplace (apm plugin init, apm marketplace init/package
add), install or resolve dependencies declared in apm.yml (apm install,
apm install [PACKAGE_REF]), register a marketplace as a consumer,
compile/pack/publish an apm package for distribution, or validate/audit
apm.yml and installed content (apm audit, apm marketplace check) — even if
the user doesn't say "apm" explicitly, e.g. "set up the package manifest",
"scaffold this as an apm package", "install my apm dependencies", "resolve
apm.yml deps", "build the distributable", "check this passes CI". Do not
use for installing the apm binary itself or setting up an agent runtime —
use apm-install for those.
metadata:
category: apm
source_keys:
- context7-microsoft-apm
---
## Gotchas
- `apm.yml`'s `type:` field (`instructions`, `skill`, `hybrid`, `prompts`) constrains what `.apm/` may contain — set it before scaffolding content, not after. Changing it later doesn't retroactively validate what's already on disk.
- `includes: auto` publishes the authoritative local layout as-is. Anything narrower needs an explicit repo-path list — don't assume `auto` means "scoped down to what's relevant."
- `apm marketplace add` (registering a marketplace as a *consumer*, pointing at someone else's catalog) and `apm marketplace package add` (registering a local package *into* a marketplace you're building) are opposite directions of the same command family — don't conflate them.
- MCP server secrets (headers, env vars) inside `apm.yml` must use `${VAR}` indirection, never literal values, so they're resolved at install/runtime and never committed to the manifest.
- `apm experimental enable registries` must run before any `registry.*` config takes effect. Declaring a `registries:` block or running `apm config set registry.*` without it silently does nothing — no error, no warning.
- Plain `apm audit` and `apm audit --ci` check different things: plain `apm audit` scans deployed files for hidden Unicode only; `--ci` additionally runs lockfile-consistency checks, install-replay drift detection, and org policy checks. A clean plain `apm audit` is not a CI-equivalent pass.
## Step 1 — Dispatch
| Invocation | Action | Reference |
|---|---|---|
| `/apm-workflow configure` | Author/edit `apm.yml`; scaffold a new package (`apm plugin init`) | `references/configure.md` |
| `/apm-workflow install` | Resolve/fetch dependencies declared in `apm.yml` (`apm install`, `apm install [PACKAGE_REF]`) | `references/install.md` |
| `/apm-workflow marketplace` | Build a marketplace, register packages into it, or register a marketplace as a consumer (`apm marketplace init/check/package add/add`) | `references/marketplace.md` |
| `/apm-workflow compile` | Generate per-target output, bundle, or publish (`apm compile`, `apm pack`, `apm publish`) | `references/compile.md` |
| `/apm-workflow audit` | Validate integrity/policy or wire a CI gate (`apm audit`, `apm audit --ci`) | `references/audit.md` |
Read only the reference file matching the requested action — each is self-contained for its concern.
## Step 2 — Execute
Follow the matched reference file's instructions. Report back which `apm` command(s) were run (or drafted, if the user asked for a plan rather than execution) and their outcome.

View File

@@ -1,55 +0,0 @@
---
topic: audit
source_keys:
- context7-microsoft-apm
- context7-websites-code-claude
- claude-code-plugins-docs
---
## `apm audit`
```bash
apm audit # local: scan deployed files for hidden Unicode
apm audit --ci # CI gate: lockfile consistency + drift replay + policy
apm audit --file <path> # standalone: scan an arbitrary file
```
Exit code `0` on success, `1` on any violation — composes as a normal CI gate step. It does not replace general lint/test/security-scan CI steps, it sits alongside them.
## Policy checks
`apm audit --ci` auto-discovers an org policy from the git remote if `--policy`/`--policy-source` isn't given explicitly; `--no-policy` skips policy discovery for a single invocation.
## Marketplace ref validation
Separate from `apm audit`: `apm marketplace check` validates that every package reference declared in a marketplace's `apm.yml` actually resolves (correct path/ref, manifest present) — run before `apm pack`/publish, to catch a typo'd local path or stale pinned tag before it ships.
## CI integration example (GitHub Actions)
```yaml
jobs:
apm-audit:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install apm-cli==X.Y.Z # pin to the version standardized on
- run: apm install
- run: apm audit --ci -f sarif --output apm-audit.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: apm-audit.sarif }
```
`-f sarif --output <file>` emits SARIF for GitHub Code Scanning's inline PR annotations.
## Frozen installs
`apm install --frozen` fails instead of silently re-resolving if the lockfile is out of date — the CI equivalent of `npm ci` vs `npm install`. Use in any CI job that must not be allowed to drift the lockfile.
## Claude Code validation is a separate terminal check
For packages compiling to the `claude` target, also run `claude plugin validate [--strict]` against the compiled output as a terminal check. `apm audit`/`apm audit --ci` do not check Claude Code-marketplace-specific schema rules (reserved prefixes, the `agents/` stray-file behavior — see `references/compile.md` — etc.), so they don't substitute for it.

View File

@@ -1,48 +0,0 @@
---
topic: compile
source_keys:
- context7-microsoft-apm
---
## Compile
```bash
apm compile
```
Generates per-target output (Claude, Copilot, etc.) from the vendor-neutral `.apm/` primitive source tree, per the `compilation:` block in `apm.yml`. Run this after any change to `.apm/` content or to `compilation:`/`targets:` in `apm.yml`.
## Pack
```bash
apm pack --dry-run # resolve and print; do not write
apm pack --offline # cached refs only
apm pack --include-prerelease # allow pre-release tags
apm pack -v # per-entry resolution detail
apm pack --marketplace=claude --json # JSON output for CI pipelines
```
Bundles a producer package into a distributable artifact. Default to `--dry-run -v` first when packing something for the first time or after a dependency change — resolution errors surface before anything is written.
Run `apm audit` after compile/pack, not before — audit scans deployed/compiled output, not the source `apm.yml` manifest; see `references/audit.md`.
## Publish
```bash
apm publish --package acme/my-skill --dry-run -v
apm publish --package acme/my-skill
```
Publishes a producer package (root containing `apm.yml`, `.apm/`, and optionally a `registries:` block) to a registry. Always dry-run with `-v` first — publishing is not trivially reversible once a version tag is claimed on a registry.
## Run
```bash
apm run <script> [--param key=value]
```
Executes a named script defined under `scripts:` in `apm.yml`, with `--param` substituting values into the script's parameters.
## Gotchas
- `claude plugin validate --strict` auto-scans every `.md` file directly under a compiled plugin's `agents/` directory and treats it as an agent definition requiring frontmatter, independent of any manifest declaration (an empirically observed Claude Code platform behavior, not an `apm` mechanic). A non-agent `.md` file (notes, provenance record, README) that ends up under `.apm/agents/` in the source tree will still fail validation once compiled to the `claude` target. Keep such files outside `.apm/agents/` (e.g. at the package root), regardless of what `apm.yml` declares.

View File

@@ -1,59 +0,0 @@
---
topic: configure
source_keys:
- context7-microsoft-apm
---
## Scaffolding a new package
```bash
apm plugin init <name> --yes
```
Scaffolds `apm.yml` + a `.apm/` skeleton in the current directory. Run this once per package (e.g. once per `plugins/<name>/` directory in a monorepo-hybrid layout), not once for the whole repo.
## `apm.yml` — required fields
Only `name` and `version` (SemVer) are required:
```yaml
name: my-pkg
version: 1.0.0
```
## `apm.yml` — top-level keys
- `name`, `version` — required (see above)
- `description`, `author`, `license`, `homepage`, `repository`, `keywords` — standard package metadata
- `type` — `instructions | skill | hybrid | prompts`; constrains `.apm/` contents
- `targets` — which harnesses this package compiles to (plural list form preferred; legacy singular `target: copilot,claude` CSV form still accepted)
- `includes` — `auto` publishes the authoritative local layout as-is, or list explicit repo paths
- `dependencies`/`devDependencies` — `apm`/`mcp`/`lsp` entries; `devDependencies` share the same shape but are excluded from the shipped artifact
- `scripts` — named commands runnable via `apm run <name>`
- `compilation` — target/strategy/exclude/placement controls for `apm compile`/`apm pack`
- `policy` — e.g. `fetch_failure_default`
- `registries` — named registry endpoints for shorthand dependency resolution
- `marketplace` — owner + packages list; see `references/marketplace.md` for the full marketplace workflow
See `docs/research/docs/microsoft-apm/configuration.md` for the complete annotated schema.
## Dependency reference forms
`dependencies.apm` entries accept: a pinned tag (`owner/repo#tag`), a plain repo (uses default branch), a single primitive path within a repo, a raw git URL, a `git:`/`path:`/`ref:` object for finer control, or a local relative path (`./packages/my-shared-skills`).
## MCP server secrets
`${VAR}` indirection is required for MCP server secrets in `apm.yml` — see SKILL.md Gotchas.
## Registries (config-level, not `apm.yml`)
Any git repo is a valid package source by default — no registry required. To declare named registries for shorthand dependency resolution:
```bash
apm experimental enable registries # required first — see SKILL.md Gotchas
apm config set registry.corp-main.url https://artifactory.corp.example.com/apm
apm config set registry.corp-main.token eyJ...
apm config set registry.corp-main.default true
```
`apm config get`/`apm config unset` manage individual keys the same way.

View File

@@ -1,20 +0,0 @@
---
topic: install
source_keys:
- context7-microsoft-apm
---
## `apm install`
```bash
apm install # resolve/install everything in apm.yml against apm.lock.yaml
apm install acme/internal-tools#^1.0.0 # install and add this dependency
apm install --update # re-resolve; accept new upstream content even if it doesn't match the lockfile hash
apm install --target agent-skills # emit a vendor-neutral output dir instead of a harness-specific one
```
With no arguments, resolves and installs everything declared under `dependencies:`/`devDependencies:` in `apm.yml` against `apm.lock.yaml`. One or more `PACKAGE_REF` arguments (any of the forms in `references/configure.md`'s "Dependency reference forms" — pinned tag, plain repo, single primitive, raw git URL, `git:`/`path:`/`ref:` object, or local relative path) install that dependency and add it to `apm.yml`.
`--update` is the escape hatch for a lockfile hash mismatch against upstream — normal `apm install` treats that as drift and won't silently accept it; see `references/audit.md` for the CI-side check (`apm install --frozen`) that fails instead of re-resolving.
`--target agent-skills` generates the vendor-neutral output directory instead of a Claude/Copilot-specific one — for IDE-agnostic tool support.

View File

@@ -1,71 +0,0 @@
---
topic: marketplace
source_keys:
- context7-microsoft-apm
- context7-websites-code-claude
- claude-code-plugins-docs
---
## Building a marketplace from a producer repo
```bash
apm marketplace init # 1. add the marketplace: block to apm.yml
$EDITOR apm.yml # 2. describe each package
apm marketplace check # 3. validate refs resolve
apm pack # 4. build marketplace artifacts
```
5. Commit and release — commit the manifest changes, tag the release, and push, following your
project's established git tooling/conventions (a dedicated commit/release skill or workflow,
commitlint/Conventional Commits rules, required tags) rather than ad hoc raw `git` invocations,
if such tooling exists.
`apm pack` emits `.claude-plugin/marketplace.json` as one of its compile targets — an APM-based marketplace stays consumable by Claude Code's existing marketplace mechanism without a separately hand-maintained file.
## Registering a package into a marketplace you're building
```bash
apm marketplace package add ./packages/plugin-a --name plugin-a
```
Registers an already-existing local package path into the root marketplace listing without re-scaffolding it — this is the monorepo-hybrid command: point it at each existing `plugins/<name>/` directory once that directory has its own `apm.yml`.
## Registering a marketplace as a consumer
`apm marketplace add` accepts many source shapes:
```bash
apm marketplace add my-org/awesome-agents # GitHub shorthand
apm marketplace add gitlab.com/my-org/awesome-agents --host gitlab.com # GitLab
apm marketplace add https://gitea.example.com/org/repo.git#v1.0.0 --name custom # self-hosted git, pinned
apm marketplace add https://catalog.example.com/marketplace.json --name catalog # hosted marketplace.json
apm marketplace add git@gitea.example.com:org/repo.git --name custom # SSH
apm marketplace add /srv/marketplaces/agent-forge.git --name agent-forge # local bare repo/working dir
apm marketplace add ./vendor/marketplace.json --name vendor # local marketplace.json file
apm marketplace add file:///srv/marketplaces/agent-forge.git --name agent-forge # file:// form
```
The local-filesystem and `file://` forms need no hosted registry or network access — the fit for an internal/homelab setup.
## Per-package versioning
```yaml
marketplace:
versioning: { strategy: per_package }
packages:
- { name: plugin-a, source: ./packages/plugin-a, version: 2.0.0 }
- { name: plugin-b, source: ./packages/plugin-b, version: 0.1.0 }
```
Without this block, the default versioning strategy ties every listed package to the marketplace/root version.
## Which mechanism to use
- **Local packages, no distribution needed yet** — local-path dependencies in `apm.yml` (`./packages/my-shared-skills`); no marketplace or registry involved.
- **Internal catalog, still git-based, no server** — `apm marketplace add` against a local path, bare repo, `file://` URI, or a plain git host.
- **Package-level hosting at scale / access control** — registries (Artifactory-style REST endpoint); more infrastructure, only worth it once package count or access-control needs outgrow git-based discovery.
## Gotchas
- A package/plugin `name` compiling to the `claude` target must avoid Claude Code's reserved name prefixes (see `plugin-author/SKILL.md` Gotchas for the current list) — otherwise the compiled `.claude-plugin/marketplace.json` is rejected by Claude Code's validator. This is a Claude Code platform constraint, independent of how the manifest gets authored.
- Removing a package from `marketplace.packages[]` in `apm.yml` and re-packing changes the public/consumed catalog. Confirm with the user in conversation before removing the entry and running `apm pack` — this is a normal `apm.yml` edit (not a distinct apm-orchestrate operation with its own gate), so the confirmation is conversational, the same as any other consequential manifest edit.

View File

@@ -1,33 +0,0 @@
# Sources
## context7-microsoft-apm
- **URL:** context7:/microsoft/apm
- **Description:** Microsoft APM (Agent Package Manager) — open-source dependency manager for AI agent configuration (skills, prompts, instructions, agents, hooks, MCP/LSP deps), applying a declare/lock/install/audit workflow.
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md
- **Contributing files:** SKILL.md, references/configure.md, references/install.md, references/marketplace.md, references/compile.md, references/audit.md
- **Status:** `extracted`
## context7-websites-code-claude
- **URL:** context7:/websites/code_claude
- **Description:** Official Claude Code documentation site indexed by Context7 — marketplace manifest schema (reserved name prefixes), `claude plugin validate` command behavior. Claude Code platform facts that hold true independent of how the compiled manifest is authored (APM or hand-authored).
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Contributing files:** references/marketplace.md, references/audit.md
- **Status:** `extracted`
## claude-code-plugins-docs
- **URL:** https://code.claude.com/docs/en/plugins
- **Description:** Official Claude Code plugin authoring guide — marketplace submission constraints (reserved name prefixes), `claude plugin validate` behavior and its scope relative to other CI checks.
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Contributing files:** references/marketplace.md, references/audit.md
- **Status:** `extracted`
## claude-code-subagents-docs
- **URL:** https://code.claude.com/docs/en/sub-agents
- **Description:** Official Claude Code subagent reference — definition format, all frontmatter fields, scope priority, built-in agents, CLI flags, environment variables, known limitations
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Contributing files:** (none)
- **Status:** `referenced`

View File

@@ -1,37 +0,0 @@
# forge
Guided entry point for building or improving something in kyberforge when the target artifact type isn't decided yet.
## What it does
Grills the user's intent via `bin:grill-with-docs` (inline, interactive) against this repo's `CONTEXT.md` and `docs/adr/`, classifies the target artifact type (skill, agent/subagent definition, plugin, or marketplace entry), announces the classification, then routes to the matching author skill — chaining more than one, in dependency order, if the intent spans multiple artifact types.
Author-skill invocation defaults to a fork subagent (inherits the grilled-intent context) and falls back to inline when forking isn't possible or the routed flow needs live user interaction (clarifying questions, a HITL gate). After a `skill-author` or `agent-author` route finishes — each already closes out with its own inline audit — forge spins up a separate clean-context subagent to independently re-run the matching audit skill (`skill-audit` / `agent-audit`) as a distinct check on the finished artifact, not a duplicate of the inline one. If that clean audit turns up any unresolved finding, forge loops — re-invoke the author skill to resolve it, re-run the clean audit — until the clean audit comes back with nothing unresolved. `plugin-author` and `marketplace-author` routes get no recheck: they have no audit counterpart, and their real terminal check (`claude plugin validate`) is already part of their own flow.
## Before you start
Have a rough idea of what you want to build or change. forge doesn't require you to already know whether it's a skill, agent, plugin, or marketplace entry — that classification is its job.
## Usage
```
/forge
```
Skip forge and call the target skill directly (`/skill-author`, `/agent-author`, `/plugin-author`, `/marketplace-author`) when you already know the artifact type.
## Files
| File | Purpose |
|------|---------|
| `SKILL.md` | Skill instructions for agents |
| `references/sources.md` | Provenance chain — research sources that informed this skill |
## Routes to
| Artifact type | Skill |
|---|---|
| Skill | `kyberforge:skill-author` |
| Agent / subagent definition | `kyberforge:agent-author` |
| Plugin | `kyberforge:plugin-author` |
| Marketplace entry | `kyberforge:marketplace-author` |

View File

@@ -1,82 +0,0 @@
---
name: forge
description: >
Use when the user wants to build, add, or improve something
but hasn't yet named which of it (skill, agent, plugin, or marketplace
entry) they need — "I want to add something to kyberforge", "not sure if
this should be a skill or a plugin", "help me figure out what to build",
"I have an idea but don't know where it belongs". Grills the intent first,
classifies the target artifact type, then routes to the matching author
skill. Do not use when the user already names the target artifact type or
skill/agent explicitly (e.g. "run /skill-author on my-skill", "create an
agent for X") — route directly to that author skill instead, bypassing
forge.
metadata:
category: factory
source_keys:
- claude-code-subagents-docs
- context7-websites-code-claude
- agentskills-spec
---
## Gotchas
- forge is an optional guided entry point, not a gate — the six existing factory skills (`skill-author`, `skill-audit`, `agent-author`, `agent-audit`, `plugin-author`, `marketplace-author`) remain directly invokable and forge does not intercept those calls. Note `plugin-author` and `marketplace-author` are deprecated per ADR-0015 — superseded by `apm-workflow`, pending issue #90 — but still function normally until that lands.
- Claude Code's skill-level `context: fork` frontmatter field and the `/fork` subagent command are opposites despite sharing a name — `context: fork` isolates (fresh context, no parent access), while `/fork` inherits the full conversation. Keep this straight when deciding how to invoke a subagent in Step 3.
## Step 1 — Grill the intent
Call `bin:grill-with-docs` unless a grill session was already performed and is available in the context.
Grilling may surface that the artifact type assumed at the start is wrong, or that the idea splits into more than one artifact.
This step always runs inline, in the current conversation — grilling is interactive and a subagent cannot hold the back-and-forth.
## Step 2 — Classify the artifact type
Match the grilled intent against exactly one row (or more than one, if the intent genuinely spans several):
| Intent | Artifact type | Route to |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -----------------------------| ---------------------------------|
| A reusable capability or workflow the agent should load inline in the main conversation — triggered automatically by description-matching, not a fresh context, and free to bundle its own `references/`, `scripts/`, or `assets/` | Skill | `kyberforge:skill-author` |
| A recurring task needs its own reusable agent/subagent definition — dedicated system prompt, tools, and description, invokable by name across sessions | Agent / subagent definition | `kyberforge:agent-author` |
| A new distributable unit is needed — no existing plugin is the right home for the skill/agent/hook/MCP server being built, or the bundle needs its own manifest, versioning, and install lifecycle separate from what already exists | Plugin | `kyberforge:plugin-author` |
| The plugin itself already exists (or was just created) and only its marketplace-facing metadata needs to change — listing it for the first time, or updating its version/description entry — never the plugin's contents | Marketplace entry | `kyberforge:marketplace-author` |
If the intent is genuinely ambiguous between rows even after grilling, ask the user directly rather than guessing.
Note: `kyberforge:plugin-author` and `kyberforge:marketplace-author` are deprecated per ADR-0015, superseded by `apm-workflow` — pending issue #90, so routing to them is unchanged for now.
This table classifies what to build, not how to run it — a one-off task that merely needs an isolated vs. context-inheriting run (rather than a new, reusable definition) isn't an artifact at all; there's nothing here to route it to.
## Step 3 — Announce, then route
State the classification and which skill(s) will run before invoking anything.
**Invoking the author skill(s).** Default to a fork subagent — it inherits the full grilled-intent conversation, so the author skill doesn't need to be re-briefed. Fall back to an inline invocation (same conversation, no subagent) when either is true:
- **Fork is technically unavailable** — already running inside a fork (a fork cannot spawn another fork), a nesting-depth cap is reached, or the environment doesn't support forking.
- **The routed flow needs live user interaction mid-run** that a backgrounded fork can't surface in real time — clarifying questions, confirmation checkpoints, or a HITL gate (e.g. `plugin-author`'s release step). Judge this from context: if nothing about the routed flow signals a live checkpoint, prefer the fork subagent.
`plugin-author` and `marketplace-author` routes always run inline — their flows are short, prompt-heavy, or gated, and get no follow-up audit-recheck step to justify running detached (see below).
**After a skill or agent route finishes.** `skill-author` and `agent-author` already close out with their own inline audit (`skill-author` runs `/skill-audit`, `agent-author` invokes `kyberforge:agent-audit` directly) in the same context as the authoring work — that's unchanged. Once that author skill's run has finished, spin up a separate **clean-context subagent** (fresh, not forked, no inherited context) to independently re-run the same audit skill against the finished artifact. This is a distinct verification layer, not a duplicate: the inline audit shares context with the work it's checking and can share its blind spots, while the clean rerun has no stake in the result.
If the clean audit surfaces any unresolved finding — not only a disagreement with the inline pass, any actionable finding on its own — loop: re-invoke the author skill (same fork-vs-inline judgment as the initial invocation) to resolve it, then re-run the clean audit again. Repeat until the clean audit comes back with nothing unresolved. Only then is the route done — the same resolve-before-close discipline `skill-author`/`agent-author` already apply to their own inline audit.
When the intent spans multiple artifact types (e.g. a new skill inside a new plugin, then registering that plugin via `kyberforge:marketplace-author`), chain the routes in dependency order — an artifact that must exist on disk before another skill can target it goes first (e.g. `plugin-author` creates the plugin directory before `skill-author` scaffolds a skill inside it).
## Step 4 — Bump plugin version (if applicable)
After the routed skill finishes, check if the artifact was created or updated inside a package by walking up from the artifact's path to the nearest ancestor `apm.yml` that declares a top-level `type:` field (`instructions`/`skill`/`hybrid`/`prompts`). An `apm.yml` with no `type:` field is a marketplace-only manifest (see `plugins/kyberforge/docs/research/docs/microsoft-apm/monorepo-and-repo-shapes.md`) — it does not count as a match; skip it and keep walking up.
**Skip this step if:**
- No ancestor `apm.yml` with a `type:` field is found (the artifact is standalone or scoped to user agent directories)
- The author skill already bumped the package version (check the skill's audit output or completion message for version bump evidence)
**If a typed `apm.yml` is found and no version bump was done:**
Invoke `kyberforge:apm-workflow` as a **clean-context subagent** (fresh, not forked) with this brief:
> "The package at `<package-path>` gained a new `<artifact-type>` (`<artifact-name>`). Bump the `version` field in that package's `apm.yml`. Determine whether to bump minor (0.1.0) or patch (0.0.1) based on whether this is a new capability (minor) or a fix/refactor (patch). Do not release or tag — just update `apm.yml` and commit."
Use a clean-context subagent (not forked) so the version bump decision is made independently without anchoring to the earlier authoring context. This gives apm-workflow a clear, isolated directive.
Report completion to the user: "Updated `<package-name>` version from X.Y.Z to X.Y.Z to reflect the new `<artifact-name>`."

View File

@@ -1,81 +0,0 @@
# Sources
## claude-code-subagents-docs
- **URL:** https://code.claude.com/docs/en/sub-agents
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code subagent reference — definition format, all frontmatter fields, scope priority, built-in agents, CLI flags, environment variables, known limitations. Grounds Step 3's fork-vs-inline invocation logic: fork inherits full conversation history via `/fork` or `subagent_type: "fork"`, is not a declarable frontmatter field on any agent definition, cannot be nested (a fork cannot spawn another fork), and is a caller-side invocation choice rather than a property of the artifact being routed to.
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## context7-websites-code-claude
- **URL:** context7:/websites/code_claude
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code documentation site indexed by Context7 — confirms the `context: fork` skill-level frontmatter field means isolated/fresh execution, the opposite of what the `/fork` subagent command does (inherits conversation). Informs the Gotchas entry warning against conflating the two.
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## claude-code-plugins-docs
- **URL:** https://code.claude.com/docs/en/plugins
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code plugin authoring guide — plugin structure, manifest fields, loading methods, skill namespacing, agent activation, marketplace submission. Background context for Step 2's plugin/marketplace rows; no forge-specific content drawn directly from it beyond that.
- **Contributing files:** (none)
- **Status:** `extracted`
## agentskills-spec
- **URL:** https://agentskills.io/specification.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Complete SKILL.md format specification — confirms `assets/`, `references/`, and `scripts/` are warranted only by the bulk/reusability of supporting content (large reference material, executable code, templates), not by a skill's category. forge has none of that bulk, so a lean SKILL.md-plus-provenance-file shape is spec-legitimate; the `references/sources.md` in this directory exists for this repo's own provenance-chain convention (see `CONTEXT.md`), not because the spec requires it.
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## agentskills-home
- **URL:** https://agentskills.io/home.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Agent Skills overview — not drawn on directly for forge; listed for provenance completeness against the agentskillsio research doc.
- **Contributing files:** (none)
- **Status:** `extracted`
## agentskills-best-practices
- **URL:** https://agentskills.io/skill-creation/best-practices.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Best practices for skill creators — not drawn on directly for forge; listed for provenance completeness against the agentskillsio research doc.
- **Contributing files:** (none)
- **Status:** `extracted`
## agentskills-optimizing-descriptions
- **URL:** https://agentskills.io/skill-creation/optimizing-descriptions.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** How to test and improve skill descriptions for triggering accuracy — not drawn on directly for forge; listed for provenance completeness against the agentskillsio research doc.
- **Contributing files:** (none)
- **Status:** `extracted`
## agentskills-evaluating-skills
- **URL:** https://agentskills.io/skill-creation/evaluating-skills.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Eval-driven skill quality improvement — not drawn on directly for forge; listed for provenance completeness against the agentskillsio research doc.
- **Contributing files:** (none)
- **Status:** `extracted`
## agentskills-using-scripts
- **URL:** https://agentskills.io/skill-creation/using-scripts.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Using scripts in skills — not applicable to forge (no scripts/ directory); listed for provenance completeness against the agentskillsio research doc.
- **Contributing files:** (none)
- **Status:** `extracted`
## agentskills-quickstart
- **URL:** https://agentskills.io/skill-creation/quickstart.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Step-by-step first-skill walkthrough — not drawn on directly for forge; listed for provenance completeness against the agentskillsio research doc.
- **Contributing files:** (none)
- **Status:** `extracted`

View File

@@ -1,35 +0,0 @@
# marketplace-author
Adds, removes, and updates plugin entries in the holocron marketplace manifest.
## What it does
Manages entries in the `plugins[]` array of `marketplace.json`. Always updates both `.claude-plugin/marketplace.json` and `.github/plugin/marketplace.json` in the same edit pass — never one without the other. Routes automatically to add, remove, update, or create-from-scratch based on whether the files exist and whether the named plugin is already in the catalog. Bumps the catalog version on every mutation (minor for ADD/REMOVE, patch for UPDATE). Runs `claude plugin validate .` after every mutating operation.
## Before you start
Have ready: the plugin name (kebab-case), what you want to do (add/remove/update), and — for add — the source type and source value. If adding from an external repo, know the source type (local path, GitHub, git URL, or npm).
## Usage
```
/marketplace-author
```
No manual script. This skill is purely agentic — it reads, edits, and writes the marketplace files directly using the Read/Edit/Write tools.
## Files
| File | Purpose |
|------|---------|
| `SKILL.md` | Skill instructions for agents |
| `references/manifest-fields.md` | Full field reference for top-level and per-entry `marketplace.json` fields, all four source type shapes with examples, and why both files must stay identical |
| `references/sources.md` | Research provenance — sources that informed this skill |
| `references/README.md` | Directory meta-documentation for references/ |
## Marketplace files managed
| File | Read by |
|------|---------|
| `.claude-plugin/marketplace.json` | Claude Code |
| `.github/plugin/marketplace.json` | Copilot CLI |

View File

@@ -1,246 +0,0 @@
---
name: marketplace-author
description: >
Use when the user wants to add a plugin to the marketplace ("register my
plugin", "add to marketplace", "list plugin X"), remove an entry ("unlist
plugin X", "remove from marketplace"), or update an existing entry ("bump
the marketplace version", "update the description for Y"). Always updates
both .claude-plugin/marketplace.json and .github/plugin/marketplace.json in
the same pass. Out of scope: plugin scaffold and configuration — use
/plugin-author for that. Does not run `claude plugin marketplace add` or
equivalent CLI registration commands — only manages `marketplace.json`
entries. Deprecated per ADR-0015 — superseded by apm-workflow; kept until
issue #90 lands.
allowed-tools: Bash Read Write Edit
metadata:
category: factory
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- context7-github-en-copilot
- github-cli-plugin-reference
- github-plugins-marketplace
- github-plugins-finding-installing
---
> **Deprecated:** Superseded by `apm-workflow` per ADR-0015 — deletion pending issue #90 (not yet changed). Use `apm-workflow marketplace` (`apm marketplace init`/`package add`/`check`) for CREATE/ADD/REMOVE/UPDATE and `apm-workflow compile` (`apm pack` emits `.claude-plugin/marketplace.json`) instead.
## Gotchas
- Both marketplace files must be identical after every operation — never update one without the other in the same edit pass.
- Every catalog mutation (ADD, REMOVE, UPDATE) requires a catalog `version` bump in both files in the same edit pass. Clients cache the catalog and use the version to detect changes — skipping the bump means the new state is invisible until a forced refresh. Convention: ADD and REMOVE → minor bump (e.g. `0.1.1` → `0.2.0`); UPDATE → patch bump (e.g. `0.2.0` → `0.2.1`). The `version` field may be at the top level or nested inside `metadata` — bump whichever form is present.
- `source` for local plugins is a relative path from the marketplace root, not the plugin directory name alone (e.g. `"./plugins/kyberforge"`, not `"kyberforge"`).
- The `{ "source": "github", ... }` object form is only for GitHub. For GitLab, Gitea, or any other git host, use `{ "source": "git", "url": "https://..." }` with a full URL.
## Route
Determine which operation applies before touching any file:
- **Neither `.claude-plugin/marketplace.json` nor `.github/plugin/marketplace.json` exist** → follow **CREATE**
- **Only one file exists** → stop and note the mirror is missing; ask the user whether to create the missing mirror from the existing file, or whether this is an error. Do not proceed until both files are present or the user has explicitly directed you to create the missing one.
- **Both files exist + plugin name NOT in `plugins[]` + add/register/list intent** → follow **ADD**
- **Both files exist + plugin name IS in `plugins[]` + remove/unlist/delete intent** → follow **REMOVE**
- **Both files exist + plugin name IS in `plugins[]` + change/update/bump intent** → follow **UPDATE**
- **User asks to validate without any add/remove/update intent** → follow **VALIDATE**
- **Ambiguous** → ask: "Did you mean to add a new plugin entry, update an existing one, or remove one?"
---
## CREATE
Run this flow only when no marketplace.json exists anywhere in the repo.
### Prerequisites
Confirm you have:
- [ ] Marketplace name (kebab-case, e.g. `my-marketplace`)
- [ ] Owner name (and optionally email)
- [ ] Marketplace description (optional but recommended)
- [ ] At least one initial plugin entry (name, source, description)
If prerequisites are missing, ask before writing.
### Step 1 — Write `.claude-plugin/marketplace.json`
Create the file with the following structure (fill in the values from prerequisites):
```json
{
"name": "<marketplace-name>",
"owner": { "name": "<owner-name>", "email": "<owner-email>" },
"metadata": {
"description": "<marketplace-description>",
"version": "0.1.0"
},
"plugins": [
{
"name": "<plugin-name>",
"description": "<plugin-description>",
"source": "<source>"
}
]
}
```
Omit `"email"` if not provided. Omit `"metadata.version"` if the user does not want a pinned catalog version. The `metadata` object is the Copilot CLI canonical location for top-level description and version — Claude Code accepts both `metadata`-nested and top-level forms; use `metadata` for dual-tool repos.
### Step 2 — Write `.github/plugin/marketplace.json`
Write identical content to `.github/plugin/marketplace.json`. These two files must always be identical.
### Step 3 — Validate
Follow the **VALIDATE** flow.
---
## ADD
Run this flow when a plugin name does not yet exist in `plugins[]` and the intent is to add it.
### Prerequisites
Confirm you have:
- [ ] Plugin name (kebab-case; reserved prefixes `anthropic-*`, `claude-*`, `agent-skills`, `official-claude-plugins` are rejected by the validator)
- [ ] Plugin description
- [ ] Source type and source value (see source type branching below)
- [ ] Version (optional; omit for git-sourced plugins)
If you need details on a specific source type shape or per-entry optional fields, read `references/manifest-fields.md`.
### Source type branching
If the user has not specified a source type, assume local path (the most common case for in-repo plugins). Only ask if the user's intent is unclear: "I'll treat this as a local path plugin — is that right, or does it live on GitHub, a git URL, or npm?"
Source shapes per type:
**Local path:**
```json
"source": "./plugins/<name>"
```
**GitHub:**
```json
"source": { "source": "github", "repo": "owner/repo" }
```
Add `"ref": "<branch-or-tag>"` inside the object if the user specifies a branch or tag. Add `"sha": "<commit-sha>"` if pinning to an exact commit — `sha` takes precedence over `ref` when both are present.
**Git URL:**
```json
"source": { "source": "git", "url": "https://..." }
```
Add `"ref": "<branch-or-tag>"` inside the object if specified.
**npm:**
```json
"source": { "source": "npm", "package": "@scope/pkg", "version": "1.0.0" }
```
`version` is required for npm source.
### Entry shape
The full entry added to `plugins[]`:
```json
{
"name": "<name>",
"description": "<description>",
"source": <source per type above>
}
```
Include `"version": "<version>"` at the entry level only when the source is npm or when the user explicitly requests a pinned version in the catalog.
Include `"strict": false` when the plugin is a dual Claude Code / Copilot CLI plugin — this prevents Copilot from rejecting CC-specific fields in the plugin directory.
### Step 1 — Read both files
Read `.claude-plugin/marketplace.json` and `.github/plugin/marketplace.json`. Verify they are identical. If they differ, stop and report the divergence — do not proceed until the user resolves it.
### Step 2 — Add the entry
Append the new entry to the end of the `plugins[]` array in `.claude-plugin/marketplace.json`. Array order is not semantically significant, but always add at the end for consistency.
### Step 3 — Mirror
Apply the identical addition to `.github/plugin/marketplace.json` in the same edit pass.
### Step 4 — Bump catalog version
Apply a **minor bump** to the `version` field in both files in the same edit pass (e.g. `0.1.1` → `0.2.0`). Find the field at the top level or inside `metadata` — bump whichever form is present.
### Step 5 — Validate
Follow the **VALIDATE** flow.
---
## REMOVE
Run this flow when an entry exists in `plugins[]` and the intent is to remove it.
### Step 1 — Confirm the target
Read `.claude-plugin/marketplace.json`. Identify the entry to remove. State the full entry as it currently appears.
### Step 2 — HITL gate
State clearly before proceeding:
> "I will remove the `<name>` entry from both `.claude-plugin/marketplace.json` and `.github/plugin/marketplace.json`. This does not delete the plugin files. Confirm?"
Do not proceed until the user confirms. If the user says "yes" or equivalent, continue to Step 3.
### Step 3 — Remove from both files
Remove the entry from `plugins[]` in `.claude-plugin/marketplace.json`.
Apply the identical removal to `.github/plugin/marketplace.json` in the same edit pass.
### Step 4 — Bump catalog version
Apply a **minor bump** to the `version` field in both files in the same edit pass (e.g. `0.1.1` → `0.2.0`). Find the field at the top level or inside `metadata` — bump whichever form is present.
### Step 5 — Validate
Follow the **VALIDATE** flow.
---
## UPDATE
Run this flow when an entry exists in `plugins[]` and the intent is to change one or more fields.
If you need to verify a field name or source type shape, read `references/manifest-fields.md`.
### Step 1 — Read the current entry
Read `.claude-plugin/marketplace.json`. Show the current state of the target entry so the user can confirm the fields to change.
### Step 2 — Apply changes
State which fields will change and to what values, then edit `.claude-plugin/marketplace.json`.
Apply the identical change to `.github/plugin/marketplace.json` in the same edit pass.
### Step 3 — Bump catalog version
Apply a **patch bump** to the `version` field in both files in the same edit pass (e.g. `0.2.0` → `0.2.1`). Find the field at the top level or inside `metadata` — bump whichever form is present.
### Step 4 — Validate
Follow the **VALIDATE** flow.
---
## VALIDATE
Run from the repo root (not from the plugin directory or `.claude-plugin/`):
```bash
claude plugin validate .
```
Add `--strict` to promote warnings to errors — recommended in CI.
Report the output. If validation fails, describe the specific error and what needs to be fixed. Do not attempt to auto-fix validation errors unless the fix is unambiguous (e.g. a trailing comma that violates JSON syntax); otherwise, describe the fix and ask the user to confirm.

View File

@@ -1,9 +0,0 @@
# references/
## manifest-fields.md
Full field reference for `marketplace.json`. Covers: top-level fields (`name`, `owner`, `description`, `version`, `plugins`), per-entry fields (`name`, `description`, `source`, `version`, `author`), all four source type shapes (local path string, `github` object, `git` object, `npm` object) with examples, where each marketplace file lives and why both must stay identical. Load this before editing any marketplace.json file.
## sources.md
Research provenance record for this skill. Lists the upstream research sources (claude-code-plugins and github-copilot-plugins research docs) that informed SKILL.md and manifest-fields.md. Used by `skill-audit` to validate the provenance chain.

View File

@@ -1,177 +0,0 @@
---
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- context7-github-en-copilot
- github-cli-plugin-reference
- github-plugins-marketplace
---
# Marketplace Manifest Fields
Reference for all fields in `marketplace.json`. Applies to both `.claude-plugin/marketplace.json` and `.github/plugin/marketplace.json`, which must always be identical.
## File Locations
| File | Read by | Notes |
|---|---|---|
| `.claude-plugin/marketplace.json` | Claude Code | Primary location for Claude Code marketplace manifest |
| `.github/plugin/marketplace.json` | Copilot CLI | Canonical location for Copilot CLI marketplace manifest |
Both files must be kept identical at all times. Every operation that modifies one must apply the same change to the other in the same edit pass.
---
## Top-Level Fields
| Field | Required | Type | Description |
|---|---|---|---|
| `name` | Yes | string | Marketplace name. Kebab-case, max 64 chars. Becomes the marketplace identifier used in `plugin install <name>@<marketplace>`. |
| `owner` | Yes | object | `{ "name": string, "email"?: string }` — the marketplace maintainer. |
| `metadata` | No | object | `{ "description"?: string, "version"?: string, "pluginRoot"?: string }` — Copilot CLI canonical location for top-level description and version. Claude Code also accepts `description` and `version` directly at the top level; use `metadata` for dual-tool repos. |
| `description` | No | string | Top-level description — Claude Code only. For dual-tool repos, prefer `metadata.description` instead. |
| `version` | No | string | Top-level marketplace version — Claude Code only. For dual-tool repos, prefer `metadata.version` instead. |
| `plugins` | Yes | array | Array of plugin entry objects. See Per-Entry Fields below. |
---
## Per-Entry Fields (inside `plugins[]`)
| Field | Required | Type | Description |
|---|---|---|---|
| `name` | Yes | string | Plugin name. Kebab-case, max 64 chars. Must be unique within the marketplace. Reserved prefixes (`anthropic-*`, `claude-*`, `agent-skills`, `official-claude-plugins`) are rejected by the validator. |
| `source` | Yes | string or object | How to locate the plugin. See Source Types below. |
| `description` | No | string | Human-readable plugin description. Max 1024 chars (Copilot CLI schema). Displayed in browse output. |
| `version` | No | string | Pinned version for catalog display. Optional for git-sourced plugins — Claude Code derives version from git tags. Required for npm source. Include when the user wants an explicit pinned version visible in the catalog. |
| `author` | No | object | `{ "name": string, "email"?: string, "url"?: string }` — the plugin author. |
| `homepage` | No | string | URL for the plugin homepage or docs site. |
| `repository` | No | string | URL for the plugin source repository. |
| `license` | No | string | SPDX license identifier (e.g. `"MIT"`, `"Apache-2.0"`). |
| `keywords` | No | string[] | Search terms for discovery (feeds into Discover tab search index). |
| `category` | No | string | Single category label for grouping in the Discover tab. |
| `tags` | No | string[] | Additional classification tags. |
| `agents` | No | string or string[] | Override the path(s) to agent definition files inside the plugin directory. Defaults to `agents/`. |
| `skills` | No | string or string[] | Override the path(s) to skill directories inside the plugin. Defaults to `skills/`. |
| `commands` | No | string or string[] | Override the path(s) to command definition files. |
| `hooks` | No | string or object | Override the path(s) to hook definitions. |
| `mcpServers` | No | string or object | Override MCP server configuration for the plugin. |
| `lspServers` | No | string or object | Override LSP server configuration for the plugin. |
| `strict` | No | boolean | Default `true`. Set to `false` for relaxed schema validation — allows extra or CC-specific fields without failing Copilot CLI validation. Use this for dual Claude Code / Copilot CLI plugins. |
---
## Source Types
The `source` field accepts four forms.
### 1. Local path (string)
Plugin lives in the same repo as the marketplace.
```json
"source": "./plugins/<plugin-name>"
```
The path is relative from the marketplace root (the repo root where `marketplace.json` sits), **not** from the plugin directory. Always prefix with `./`.
**Example:**
```json
{
"name": "kyberforge",
"description": "Skills and agents for the Claude Code plugin factory.",
"source": "./plugins/kyberforge"
}
```
---
### 2. GitHub (object)
Plugin lives in a separate GitHub repository. GitHub shorthand only — do not use this form for GitLab, Gitea, or other hosts.
```json
"source": { "source": "github", "repo": "owner/repo" }
```
Optional fields inside the object:
- `"ref"` — branch name, tag, or commit SHA to pin. Omit to follow the default branch.
- `"sha"` — exact commit SHA; takes precedence over `ref` when both are present.
**Example:**
```json
{
"name": "deploy-tools",
"description": "Deployment automation.",
"source": { "source": "github", "repo": "acme-corp/deploy-tools-plugin", "ref": "v2.0.0" }
}
```
---
### 3. Git URL (object)
Plugin in any git host — GitHub, GitLab, Gitea, Bitbucket, or self-hosted — via full HTTPS or SSH URL. Use this instead of the `github` form for any non-GitHub host.
```json
"source": { "source": "git", "url": "https://..." }
```
Optional fields inside the object:
- `"ref"` — branch name, tag, or commit SHA.
**Examples:**
```json
{ "source": "git", "url": "https://gitlab.com/org/plugin.git" }
{ "source": "git", "url": "https://gitea.example.com/org/plugin.git", "ref": "v1.0.0" }
{ "source": "git", "url": "git@github.com:org/plugin.git" }
```
---
### 4. npm (object)
Plugin distributed as an npm package. `version` is required inside the object.
```json
"source": { "source": "npm", "package": "@scope/pkg", "version": "1.0.0" }
```
**Example:**
```json
{
"name": "formatter",
"description": "Code formatting plugin.",
"source": { "source": "npm", "package": "@acme/claude-formatter", "version": "3.1.0" }
}
```
---
## Why Both Files Must Stay Identical
`.claude-plugin/marketplace.json` is the Claude Code-native path. `.github/plugin/marketplace.json` is the Copilot CLI canonical path per the reference docs (`github/copilot-plugins` and `github/awesome-copilot` both use this path). Both tools are used in this repo, so both files must exist and match. A divergence creates a split-catalog state where the two tools see different plugins — this is a silent inconsistency that is hard to detect and diagnose. Treat them as a single logical file that happens to exist at two paths.
---
## Complete Example
```json
{
"name": "holocron",
"owner": { "name": "Defame1297", "email": "defame1297@rkdr.net" },
"description": "AI development skills for Claude Code and GitHub Copilot CLI.",
"version": "0.1.0",
"plugins": [
{
"name": "kyberforge",
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
"source": "./plugins/kyberforge"
},
{
"name": "external-tool",
"description": "An externally hosted plugin.",
"source": { "source": "github", "repo": "acme/external-tool" }
}
]
}
```

View File

@@ -1,147 +0,0 @@
---
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- context7-github-en-copilot
- github-cli-plugin-reference
- github-plugins-marketplace
- github-plugins-finding-installing
---
# Sources
## context7-websites-code-claude
- **URL:** context7:/websites/code_claude
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code documentation site indexed by Context7 — marketplace.json format, source types, `claude plugin validate` command, plugin update lifecycle, private marketplace registration, source URL formats
- **Contributing files:** SKILL.md, references/manifest-fields.md
- **Status:** `extracted`
## claude-code-plugins-docs
- **URL:** https://code.claude.com/docs/en/plugins
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code plugin authoring guide — `marketplace.json` schema, source type shapes (local path, github object, git object, npm object), `claude plugin validate .` behavior, end-to-end publish walkthrough, marketplace catalog format
- **Contributing files:** SKILL.md, references/manifest-fields.md
- **Status:** `extracted`
## claude-code-subagents-docs
- **URL:** https://code.claude.com/docs/en/sub-agents
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code subagent reference — definition format, all frontmatter fields, scope priority, built-in agents, CLI flags, environment variables, known limitations
- **Contributing files:** (none)
- **Status:** `referenced`
## context7-github-en-copilot
- **URL:** context7:/websites/github_en_copilot
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Official GitHub Copilot documentation indexed by Context7; covers CLI plugins, custom agents, SDK, and marketplace — including `metadata` object schema, per-entry optional fields, `strict` field behavior
- **Contributing files:** SKILL.md, references/manifest-fields.md
- **Status:** `extracted`
## github-cli-plugin-reference
- **URL:** https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-plugin-reference
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Full CLI plugin reference — `marketplace.json` schema (top-level and per-entry fields), all `copilot plugin marketplace` commands, install specification formats, `.github/plugin/marketplace.json` canonical path, `strict` field behavior
- **Contributing files:** SKILL.md, references/manifest-fields.md
- **Status:** `extracted`
## github-plugins-marketplace
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-marketplace
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** How-to for creating and publishing a Copilot CLI plugin marketplace — `marketplace.json` structure at `.github/plugin/marketplace.json`, per-entry fields, marketplace registration commands, reference implementations (`github/copilot-plugins`, `github/awesome-copilot`)
- **Contributing files:** SKILL.md, references/manifest-fields.md
- **Status:** `extracted`
## github-plugins-finding-installing
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-finding-installing
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** User-facing guide to discovering and installing CLI plugins — marketplace browsing commands, install/update/uninstall workflow; informs REMOVE flow design (unlisting does not uninstall from existing users)
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## github-custom-agents-configuration
- **URL:** https://docs.github.com/en/copilot/reference/custom-agents-configuration
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Reference for cloud and IDE custom agent definition format — frontmatter fields, tool aliases, MCP server config, secrets interpolation, scoping hierarchy
- **Contributing files:** (none)
- **Status:** `referenced`
## github-plugins-creating
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-creating
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** How-to for creating Copilot CLI plugins — plugin structure, agent and skill authoring, hooks format, MCP config, development lifecycle
- **Contributing files:** (none)
- **Status:** `referenced`
## github-sdk-custom-agents
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-sdk/features/custom-agents
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** SDK custom agent API — CustomAgentConfig fields in all five languages, session config, sub-agent lifecycle events, tool scoping, permission handling
- **Contributing files:** (none)
- **Status:** `referenced`
## github-changelog-copilot-extensions-ga
- **URL:** https://github.blog/changelog/2025-02-19-announcing-the-general-availability-of-github-copilot-extensions/
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Announcement of GitHub Copilot Extensions general availability (February 2025) — OIDC auth, all license tiers, VS Code/Visual Studio/JetBrains/GitHub.com support
- **Contributing files:** (none)
- **Status:** `referenced`
## github-changelog-copilot-extensions-sunset
- **URL:** https://github.blog/changelog/2025-09-24-deprecate-github-copilot-extensions-github-apps/
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Sunset notice for GitHub App-based Copilot Extensions — creation blocked Sep 24, 2025; full shutdown Nov 10, 2025; MCP servers recommended as replacement
- **Contributing files:** (none)
- **Status:** `referenced`
## github-docs-copilot-extensions-skillsets
- **URL:** https://docs.github.com/en/copilot/concepts/build-copilot-extensions/skillsets-for-copilot-extensions
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Concept doc for Copilot Extension skillsets — up to 5 skills per extension, Copilot handles routing/prompt crafting/response, contrast with agent extensions
- **Contributing files:** (none)
- **Status:** `referenced`
## github-docs-copilot-extensions-building
- **URL:** https://docs.github.com/en/copilot/building-copilot-extensions/setting-up-copilot-extensions
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** How-to for setting up a Copilot Extension — GitHub App registration, Copilot Chat permission, Copilot Editor Context permission, backend URL configuration
- **Contributing files:** (none)
- **Status:** `referenced`
## vscode-chat-participant-api
- **URL:** https://code.visualstudio.com/api/extension-guides/ai/chat
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** VS Code Chat Participant API — createChatParticipant(), package.json contributes.chatParticipants, Language Model API, @mention invocation in Copilot Chat
- **Contributing files:** (none)
- **Status:** `referenced`
## github-marketplace-copilot-extensions
- **URL:** https://github.com/marketplace?type=apps&copilot_app=true
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** GitHub Marketplace listing for Copilot Extensions — browsable list of available extensions (historical; page remains live but product is sunset)
- **Contributing files:** (none)
- **Status:** `referenced`
## github-docs-marketplace-listing-requirements
- **URL:** https://docs.github.com/en/apps/github-marketplace/creating-apps-for-github-marketplace/requirements-for-listing-an-app
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Requirements for listing a GitHub App on the GitHub Marketplace — verified publisher, capability description, UX stability, submission and review process
- **Contributing files:** (none)
- **Status:** `referenced`

View File

@@ -1,39 +0,0 @@
# plugin-author
Creates, updates, and releases plugin scaffolds for the holocron marketplace.
## What it does
Manages both manifests (`plugin.json` for Copilot CLI and `.claude-plugin/plugin.json` for Claude Code) in one pass. Three operations: create a new plugin scaffold with placeholder manifests and skeleton dirs; update configuration fields (shared fields updated in both manifests simultaneously); release a version with HITL gate before tagging.
Out of scope: plugin content (skills, agents, hooks, MCP servers inside those dirs) and `marketplace.json` entries.
## Before you start
Have ready: the plugin name (kebab-case) and the repo root path.
## Usage
```
/plugin-author
```
**Manual scaffold (human workflow):**
```bash
bash scripts/new-plugin.sh <plugin-name> <repo-root>
# Examples:
bash scripts/new-plugin.sh my-tools /root/ai-development
bash scripts/new-plugin.sh data-tools .
```
## Files
| File | Purpose |
|------|---------|
| `SKILL.md` | Skill instructions for agents |
| `scripts/new-plugin.sh` | Scaffolds both manifests and skeleton dirs for a new plugin |
| `references/manifest-fields.md` | All optional fields for both manifests beyond the scaffolded defaults |
| `references/sources.md` | Research provenance — sources that informed this skill |
| `scripts/README.md` | Directory meta-documentation for scripts/ |
| `references/README.md` | Directory meta-documentation for references/ |

View File

@@ -1,191 +0,0 @@
---
name: plugin-author
description: >
Use when the user wants to create a new plugin scaffold ("create a plugin
for X", "new plugin called Y"), update plugin configuration ("change the
description", "add keyword", "bump version"), or release a plugin version
("release", "tag", "publish"). Manages both Claude Code
(.claude-plugin/plugin.json) and Copilot CLI (plugin.json) manifests in one
pass. Do not use when the request is about plugin content (skills, agents,
hooks, or MCP servers inside those directories). Do not use for
marketplace.json entries — use /marketplace-author for that. Deprecated
per ADR-0015 — superseded by apm-workflow; kept until issue #90 lands.
allowed-tools: Bash Read Write Edit
metadata:
category: factory
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- claude-code-subagents-docs
- context7-github-en-copilot
- github-cli-plugin-reference
- github-plugins-creating
- github-plugins-finding-installing
---
> **Deprecated:** Superseded by `apm-workflow` per ADR-0015 — deletion pending issue #90 (not yet changed). Use `apm-workflow configure` (apm.yml authoring, `apm plugin init` scaffolding) for CREATE/UPDATE and `apm-workflow compile` (`apm compile`/`apm pack`/`apm publish`) for RELEASE instead.
## Gotchas
- Both manifests must carry identical `version` values — version parity is a hard invariant (ADR-0006). Never update version in one manifest without updating the other in the same edit pass.
- Every UPDATE to non-version fields (description, keywords, author, etc.) requires a patch bump to `version` in both manifests in the same edit pass. Consumers cache plugin metadata and use the version to detect changes — skipping the bump makes the update invisible until a forced refresh.
- `author.email` is placed in the Copilot manifest by convention; `author.url` is placed in the CC manifest by convention. Both fields are supported by both platforms — do not add them to the other manifest without a deliberate reason.
- `claude plugin tag --push` is irreversible: it creates a git tag and pushes it to remote. Always present the HITL gate and wait for explicit confirmation before running it.
- `claude plugin tag --push` requires a clean working tree and will fail if there are uncommitted changes. Commit or stash all changes before running it.
- `name` in both manifests must be kebab-case and must not use reserved prefixes: `anthropic-*`, `claude-*`, `agent-skills`, `official-claude-plugins`.
- Copilot manifest lookup order: `.plugin/plugin.json` → `plugin.json` → `.github/plugin/plugin.json` → `.claude-plugin/plugin.json`. The canonical location for the Copilot manifest in this repo is `plugin.json` at the plugin root.
- `displayName` is a CC platform field — Copilot has no equivalent. Do not add it to the Copilot manifest.
- `skills`, `agents`, `hooks`, `mcpServers` are declared in the Copilot manifest by convention — Copilot requires explicit path declarations while CC auto-discovers content from the plugin root. Both platforms support these fields; omit them from the CC manifest by convention.
- Agent files in a plugin's `agents/` directory silently ignore `hooks`, `mcpServers`, and `permissionMode` frontmatter fields.
- `claude plugin validate --strict` auto-discovers every `.md` file directly under `agents/` and treats it as an agent definition requiring frontmatter — this is independent of the manifest. An explicit `agents` array listing only the real agent files does not suppress the scan; unlisted `.md` files in the same directory still fail validation (tested empirically — see ADR-0010). Any non-agent file (notes, provenance records, READMEs) must live outside `agents/` — e.g. at the plugin root — regardless of what the manifest declares.
## Route
Determine which flow before touching the filesystem. Read both manifest files if the plugin directory exists.
- **Plugin directory does not exist** → follow **CREATE flow**
- **Plugin directory exists + version/release intent** ("release", "tag", "bump", "publish", "version") → follow **RELEASE flow**
- **Plugin directory exists + field change intent** ("update description", "add keyword", "change author") → follow **UPDATE flow**
- **Ambiguous** → ask: "Did you mean to create a new plugin, update its configuration, or release a version?"
Validate runs automatically before tagging (in RELEASE flow) and can be invoked explicitly at any time: `claude plugin validate plugins/<name>`.
## CREATE flow
### Prerequisites
Before touching the filesystem, confirm you have:
- [ ] Plugin name (kebab-case, e.g. `my-tools`)
- [ ] Repo root (absolute path or `.` for current directory)
If either is missing, stop and ask before proceeding.
### Step 1 — Scaffold
Run the scaffold script:
```bash
bash scripts/new-plugin.sh <name> <repo-root>
```
Examples:
```bash
bash scripts/new-plugin.sh my-tools /root/ai-development
bash scripts/new-plugin.sh data-tools .
```
The script creates under `<repo-root>/plugins/<name>/`:
- `plugin.json` — Copilot manifest with `FILL_IN_*` placeholders
- `.claude-plugin/plugin.json` — CC manifest with `FILL_IN_*` placeholders
- Empty skeleton directories: `skills/`, `agents/`, `hooks/`, `bin/`
Each file/dir is a no-op if it already exists.
### Step 2 — Fill in placeholders
Open both manifest files and replace every `FILL_IN_*` placeholder.
**Fields shared by both manifests** (must be identical in both):
- `name` — kebab-case plugin identifier (already set by script; verify it is correct)
- `description` — one or two sentences; what the plugin provides
- `version` — SemVer; defaults to `1.0.0`; must be identical in both manifests
- `author.name` — author display name
- `license` — SPDX identifier (default: `MIT`)
- `keywords` — search/discovery tags (default: `[]`)
**CC manifest fields** (`.claude-plugin/plugin.json` only):
- `displayName` — human-readable name shown in plugin manager; capitalised form of `name` (CC platform field — no Copilot equivalent)
- `author.url` — author URL (e.g. Gitea profile URL) (both platforms support this; placed here by convention)
**Copilot manifest fields** (`plugin.json` only):
- `author.email` — author email (both platforms support this; placed here by convention)
- `skills`, `agents`, `hooks`, `mcpServers` — paths; defaults are already set by the script (CC auto-discovers these; Copilot requires explicit declarations)
### Step 3 — Validate
Check:
- [ ] `name` identical in both manifests, kebab-case, no reserved prefixes
- [ ] `description` identical in both manifests, non-empty
- [ ] `version` identical in both manifests (version parity — ADR-0006)
- [ ] `author.name` identical in both manifests
- [ ] `license` identical in both manifests
- [ ] `keywords` identical in both manifests
- [ ] No `FILL_IN_*` placeholders remain
- [ ] `displayName` present in CC manifest only
- [ ] `author.url` in CC manifest, `author.email` in Copilot manifest
## UPDATE flow
### Step 1 — Read both manifests
Read `plugins/<name>/plugin.json` and `plugins/<name>/.claude-plugin/plugin.json`. Identify the current field values.
### Step 2 — Classify each change
For every field the user wants to change:
| Change type | What to update |
|---|---|
| Shared field (`name`, `description`, `version`, `author.name`, `license`, `keywords`) | Both manifests in the same edit pass |
| CC platform field (`displayName`) | `.claude-plugin/plugin.json` only — Copilot has no equivalent field |
| Copilot platform fields (`category`, `tags`, `extensions`) | `plugin.json` only — not in the CC manifest schema |
| CC scaffold convention (`author.url`) | `.claude-plugin/plugin.json` only — both platforms support this field; it is placed here by convention |
| Copilot scaffold convention (`author.email`, `skills`, `agents`, `hooks`, `mcpServers`) | `plugin.json` only by convention — CC also supports these fields; CC auto-discovers content from the plugin root rather than requiring explicit path declarations |
Never update a shared field in one manifest without updating the other in the same pass.
If the target field is not listed in the classification table, read `references/manifest-fields.md` for the full field list and platform support notes.
### Step 3 — Announce and apply
State which fields change and which files are affected. Then apply. For `version` changes not part of a release, bump both manifests in the same edit.
### Step 4 — Bump version
If the change did not explicitly target the `version` field, apply a **patch bump** to `version` in both manifests in the same edit pass (e.g. `1.2.3` → `1.2.4`). Skip this step only when the user has explicitly set the version themselves in Step 3.
### Step 5 — Validate
Re-run the validation checklist from CREATE flow Step 3 on both files.
## RELEASE flow
### Step 1 — Confirm version
If the user has not stated the new SemVer version, ask: "What version are you releasing?" Do not proceed until you have the version.
### Step 2 — Bump version in both manifests
Update `version` in both `plugin.json` and `.claude-plugin/plugin.json` in the same edit pass. Confirm they are identical after the edit.
### Step 3 — Validate
Run:
```bash
claude plugin validate plugins/<name>
```
Use `--strict` to promote warnings to errors: `claude plugin validate --strict plugins/<name>`.
Stop and report errors if validation fails. Do not proceed to tagging until validation passes.
### Step 4 — HITL gate
State exactly:
> "I will run `claude plugin tag --push` for plugin `<name>`, which will create git tag `<name>--v<version>` and push it to remote. This is irreversible. Confirm?"
Do not call the tool until the user explicitly confirms in the conversation.
### Step 5 — Tag and release
To preview without tagging or pushing: `claude plugin tag --dry-run`.
After explicit confirmation, run from the repo root:
```bash
claude plugin tag --push
```
Report the created tag name and confirm the push completed.

View File

@@ -1,9 +0,0 @@
# references/
## manifest-fields.md
Complete field reference for both plugin manifests. Covers all optional fields beyond the scaffolded defaults: field classification (shared / CC-only / Copilot-only), usage examples, and the version parity convention (ADR-0016). Loaded when a user asks to add a non-default field to either manifest.
## sources.md
Research provenance record for this skill. Lists the upstream research sources (claude-code-plugins and github-copilot-plugins research docs) that informed SKILL.md, the scaffold script, and the manifest-fields reference. Used by `skill-audit` to validate the provenance chain.

View File

@@ -1,146 +0,0 @@
---
topic: manifest-fields
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- context7-github-en-copilot
- github-cli-plugin-reference
- github-plugins-creating
- github-plugins-finding-installing
---
# Manifest Fields Reference
This document covers optional fields beyond the scaffolded defaults. Consult it when a user asks to add a non-default field to either manifest.
## Field Classification
Fields fall into three categories: **shared** (identical in both manifests), **platform** (one platform does not support the field at all), and **convention** (both platforms support the field, but the repo scaffold places it in one manifest only).
> CC auto-discovers content (skills, agents, hooks, MCP servers) from the plugin root; Copilot requires explicit path declarations. Convention fields in the CC manifest are omitted unless you have a deliberate reason to add them.
| Field | Copilot `plugin.json` | CC `.claude-plugin/plugin.json` | Notes |
|---|---|---|---|
| `name` | Yes (shared) | Yes (shared) | Identical in both; kebab-case; max 64 chars (Copilot) |
| `description` | Yes (shared) | Yes (shared) | Identical in both; max 1024 chars (Copilot) |
| `version` | Yes (shared) | Yes (shared) | Identical in both; SemVer; version parity required (ADR-0016) |
| `author.name` | Yes (shared) | Yes (shared) | Identical in both |
| `license` | Yes (shared) | Yes (shared) | Identical in both; SPDX identifier |
| `keywords` | Yes (shared) | Yes (shared) | Identical in both; string array |
| `displayName` | No (unsupported) | Yes | CC platform field — Copilot has no equivalent |
| `author.url` | Omitted (convention) | Yes (convention) | Author profile URL; CC scaffold places here; Copilot also supports this field |
| `author.email` | Yes (convention) | Omitted (convention) | Author email; Copilot scaffold places here; CC also supports this field |
| `agents` | Yes (convention) | Omitted (convention) | Path or array; default: `agents/`; Copilot requires explicit declaration; CC auto-discovers |
| `skills` | Yes (convention) | Omitted (convention) | Path or array; default: `skills/`; Copilot requires explicit declaration; CC auto-discovers |
| `hooks` | Yes (convention) | Omitted (convention) | Path to hooks config; Copilot requires explicit declaration; CC auto-discovers |
| `mcpServers` | Yes (convention) | Omitted (convention) | Path or object; Copilot requires explicit declaration; CC auto-discovers |
| `category` | Yes | No (unsupported) | Marketplace category string; Copilot platform field — not in CC manifest schema |
| `tags` | Yes | No (unsupported) | Additional taxonomy tags (distinct from `keywords`); Copilot platform field |
| `extensions` | Yes | No (unsupported) | Path, array, or `{ paths, exclusive: true }` to disable built-ins; Copilot platform field |
| `homepage` | Both (independent) | Both (independent) | Documentation URL; not required to be identical |
| `repository` | Both (independent) | Both (independent) | Source repo URL |
## Non-Default Optional Fields
### `homepage`
Documentation or project page URL. Shown in the plugin manager. Independent in each manifest — the two values do not need to match.
```json
// Copilot plugin.json
{ "homepage": "https://example.com/docs" }
// CC .claude-plugin/plugin.json
{ "homepage": "https://example.com/docs" }
```
### `repository`
Source repository URL. Independent in each manifest.
```json
{ "repository": "https://git.example.com/owner/repo" }
```
### `category` (Copilot-only)
Marketplace browsing category. Single string. Copilot manifest only.
```json
{ "category": "developer-tools" }
```
### `tags` (Copilot-only)
Additional taxonomy tags for Copilot marketplace browsing. Distinct from `keywords`.
```json
{ "tags": ["testing", "ci"] }
```
### `extensions` (Copilot-only)
Path to extension files, an array of paths, or an object. Use `{ "paths": [...], "exclusive": true }` to disable built-in extensions.
```json
{ "extensions": "extensions/" }
// or
{ "extensions": { "paths": ["extensions/"], "exclusive": true } }
```
### `lspServers`
Language Server Protocol configuration. Supported in both Copilot and CC manifests.
```json
{ "lspServers": ".lsp.json" }
```
### `outputStyles` (CC-only)
Path to output styles directory. Claude Code manifest only.
```json
{ "outputStyles": "styles/" }
```
### `experimental.themes` (CC-only)
Path to themes directory. Claude Code manifest only. Experimental — may change.
```json
{ "experimental": { "themes": "themes/" } }
```
### `experimental.monitors` (CC-only)
Path to `monitors.json`. Claude Code manifest only. Experimental.
```json
{ "experimental": { "monitors": "monitors.json" } }
```
### `dependencies` (CC-only)
Plugin dependencies. Each entry is a string (plugin name) or `{ "name": "<name>", "version": "<semver>" }`.
```json
{
"dependencies": [
"base-tools",
{ "name": "data-tools", "version": "^2.0.0" }
]
}
```
### `commands` (legacy, both)
Explicit list of `.md` command file paths. Deprecated in favour of `skills/`. Use `skills` instead for new plugins.
## Version Parity Convention (ADR-0016)
The `version` field must be present and identical in both manifests at all times. This is a hard invariant enforced by `/plugin-author` on every create, update, and release operation.
- If only the CC manifest had `version` before this convention was introduced, backfill the Copilot manifest immediately.
- Never change `version` in one manifest without changing it in the other in the same edit pass.
- The RELEASE flow bumps both manifests simultaneously before tagging.

View File

@@ -1,158 +0,0 @@
---
source_keys:
- context7-websites-code-claude
- claude-code-plugins-docs
- claude-code-subagents-docs
- context7-github-en-copilot
- github-cli-plugin-reference
- github-plugins-creating
- github-plugins-finding-installing
- github-custom-agents-configuration
- github-plugins-marketplace
- github-sdk-custom-agents
- github-changelog-copilot-extensions-ga
- github-changelog-copilot-extensions-sunset
- github-docs-copilot-extensions-skillsets
- github-docs-copilot-extensions-building
- vscode-chat-participant-api
- github-marketplace-copilot-extensions
- github-docs-marketplace-listing-requirements
---
# Sources
## context7-websites-code-claude
- **URL:** context7:/websites/code_claude
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code documentation site indexed by Context7 — plugin manifest schema, marketplace JSON format, `claude plugin` CLI commands, validation, tagging
- **Contributing files:** SKILL.md, references/manifest-fields.md
- **Status:** `extracted`
## claude-code-plugins-docs
- **URL:** https://code.claude.com/docs/en/plugins
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code plugin authoring guide — plugin structure, CC manifest fields (`displayName`, `author.url`, `version`, `outputStyles`, `experimental`), marketplace submission, `claude plugin tag`
- **Contributing files:** SKILL.md, references/manifest-fields.md
- **Status:** `extracted`
## github-cli-plugin-reference
- **URL:** https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-plugin-reference
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Full Copilot CLI plugin reference — `plugin.json` schema (all fields, types, constraints), marketplace.json schema, CLI commands, manifest lookup order
- **Contributing files:** SKILL.md, references/manifest-fields.md
- **Status:** `extracted`
## github-plugins-creating
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-creating
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** How-to for creating Copilot CLI plugins — plugin structure, Copilot manifest fields, development lifecycle, hooks format, MCP config
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## github-plugins-finding-installing
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-finding-and-installing
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Finding and installing Copilot CLI plugins — install spec formats, marketplace registration, `copilot plugin` CLI commands
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## claude-code-subagents-docs
- **URL:** https://code.claude.com/docs/en/sub-agents
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Description:** Official Claude Code subagent reference — definition format, all frontmatter fields, scope priority, built-in agents, CLI flags, environment variables, known limitations
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## context7-github-en-copilot
- **URL:** context7:/websites/github_en_copilot
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Official GitHub Copilot documentation indexed by Context7; covers CLI plugins, custom agents, SDK, and marketplace
- **Contributing files:** SKILL.md, references/manifest-fields.md
- **Status:** `extracted`
## github-custom-agents-configuration
- **URL:** https://docs.github.com/en/copilot/reference/custom-agents-configuration
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Reference for cloud and IDE custom agent definition format — frontmatter fields, tool aliases, MCP server config, secrets interpolation, scoping hierarchy
- **Contributing files:** (none)
- **Status:** `extracted`
## github-plugins-marketplace
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-marketplace
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** How-to for creating and publishing a plugin marketplace — marketplace.json structure, hosting options, registration commands
- **Contributing files:** (none)
- **Status:** `extracted`
## github-sdk-custom-agents
- **URL:** https://docs.github.com/en/copilot/how-tos/copilot-sdk/features/custom-agents
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** SDK custom agent API — CustomAgentConfig fields in all five languages, session config, sub-agent lifecycle events, tool scoping, permission handling
- **Contributing files:** (none)
- **Status:** `extracted`
## github-changelog-copilot-extensions-ga
- **URL:** https://github.blog/changelog/2025-02-19-announcing-the-general-availability-of-github-copilot-extensions/
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Announcement of GitHub Copilot Extensions general availability (February 2025) — OIDC auth, all license tiers, VS Code/Visual Studio/JetBrains/GitHub.com support
- **Contributing files:** (none)
- **Status:** `referenced`
## github-changelog-copilot-extensions-sunset
- **URL:** https://github.blog/changelog/2025-09-24-deprecate-github-copilot-extensions-github-apps/
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Sunset notice for GitHub App-based Copilot Extensions — creation blocked Sep 24, 2025; full shutdown Nov 10, 2025; MCP servers recommended as replacement
- **Contributing files:** (none)
- **Status:** `referenced`
## github-docs-copilot-extensions-skillsets
- **URL:** https://docs.github.com/en/copilot/concepts/build-copilot-extensions/skillsets-for-copilot-extensions
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Concept doc for Copilot Extension skillsets — up to 5 skills per extension, Copilot handles routing/prompt crafting/response, contrast with agent extensions
- **Contributing files:** (none)
- **Status:** `referenced`
## github-docs-copilot-extensions-building
- **URL:** https://docs.github.com/en/copilot/building-copilot-extensions/setting-up-copilot-extensions
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** How-to for setting up a Copilot Extension — GitHub App registration, Copilot Chat permission, Copilot Editor Context permission, backend URL configuration
- **Contributing files:** (none)
- **Status:** `referenced`
## vscode-chat-participant-api
- **URL:** https://code.visualstudio.com/api/extension-guides/ai/chat
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** VS Code Chat Participant API — createChatParticipant(), package.json contributes.chatParticipants, Language Model API, @mention invocation in Copilot Chat
- **Contributing files:** (none)
- **Status:** `referenced`
## github-marketplace-copilot-extensions
- **URL:** https://github.com/marketplace?type=apps&copilot_app=true
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** GitHub Marketplace listing for Copilot Extensions — browsable list of available extensions (historical; page remains live but product is sunset)
- **Contributing files:** (none)
- **Status:** `referenced`
## github-docs-marketplace-listing-requirements
- **URL:** https://docs.github.com/en/apps/github-marketplace/creating-apps-for-github-marketplace/requirements-for-listing-an-app
- **Research doc:** plugins/kyberforge/docs/research/docs/github-copilot-plugins/sources.md
- **Description:** Requirements for listing a GitHub App on the GitHub Marketplace — verified publisher, capability description, UX stability, submission and review process
- **Contributing files:** (none)
- **Status:** `referenced`

View File

@@ -1,11 +0,0 @@
# scripts/
## new-plugin.sh
Scaffolds a new plugin directory with both manifests and empty skeleton dirs.
```
Usage: new-plugin.sh <plugin-name> <repo-root>
```
Creates `<repo-root>/plugins/<plugin-name>/` containing: `plugin.json` (Copilot manifest), `.claude-plugin/plugin.json` (CC manifest), and empty `skills/`, `agents/`, `hooks/`, `bin/` directories. Both manifest files carry `FILL_IN_*` placeholders for fields the user must supply. Each file and directory is a no-op if it already exists. Does not touch `marketplace.json`. See `--help` for full usage.

View File

@@ -1,148 +0,0 @@
#!/usr/bin/env bash
# source_keys: github-cli-plugin-reference github-plugins-creating
set -euo pipefail
usage() {
cat <<EOF
Usage: new-plugin.sh <plugin-name> <repo-root>
Scaffold a new plugin directory with both manifests and skeleton dirs.
Arguments:
plugin-name Kebab-case plugin identifier (e.g. my-tools, data-tools).
Must be lowercase letters, numbers, and hyphens only.
No leading, trailing, or consecutive hyphens.
repo-root Absolute or relative path to the repository root.
The plugin is created at <repo-root>/plugins/<plugin-name>/.
Created structure:
<repo-root>/plugins/<plugin-name>/
plugin.json Copilot CLI manifest (FILL_IN_* placeholders)
.claude-plugin/
plugin.json Claude Code manifest (FILL_IN_* placeholders)
skills/ Empty skeleton directory
agents/ Empty skeleton directory
hooks/ Empty skeleton directory
bin/ Empty skeleton directory
Each file and directory is a no-op if it already exists.
Does NOT touch marketplace.json.
Exit codes:
0 Files created or already existed (no-op)
1 Invalid arguments or missing root
EOF
}
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage
exit 0
fi
if [[ $# -lt 2 ]]; then
echo "Error: plugin-name and repo-root are required." >&2
echo "" >&2
usage >&2
exit 1
fi
PLUGIN_NAME="$1"
REPO_ROOT="$2"
# Validate plugin name format
if ! echo "$PLUGIN_NAME" | grep -qE '^[a-z0-9]+(-[a-z0-9]+)*$'; then
echo "Error: plugin-name must use lowercase letters, numbers, and hyphens only." >&2
echo " No leading, trailing, or consecutive hyphens." >&2
echo " Received: '$PLUGIN_NAME'" >&2
exit 1
fi
# Expand tilde
REPO_ROOT="${REPO_ROOT/#\~/$HOME}"
# Resolve to absolute path
REPO_ROOT="$(cd "$REPO_ROOT" 2>/dev/null && pwd)" || {
echo "Error: repo-root directory '$2' does not exist." >&2
exit 1
}
PLUGIN_DIR="$REPO_ROOT/plugins/$PLUGIN_NAME"
CC_DIR="$PLUGIN_DIR/.claude-plugin"
COPILOT_MANIFEST="$PLUGIN_DIR/plugin.json"
CC_MANIFEST="$CC_DIR/plugin.json"
# Create directory skeleton
created_any=false
create_dir_if_missing() {
local dir="$1"
if [[ -d "$dir" ]]; then
echo "Skipping directory '$dir' — already exists." >&2
else
mkdir -p "$dir"
echo "Created directory: $dir" >&2
created_any=true
fi
}
create_dir_if_missing "$PLUGIN_DIR"
create_dir_if_missing "$CC_DIR"
create_dir_if_missing "$PLUGIN_DIR/skills"
create_dir_if_missing "$PLUGIN_DIR/agents"
create_dir_if_missing "$PLUGIN_DIR/hooks"
create_dir_if_missing "$PLUGIN_DIR/bin"
# Create Copilot manifest (plugin.json)
if [[ -f "$COPILOT_MANIFEST" ]]; then
echo "Skipping '$COPILOT_MANIFEST' — already exists." >&2
else
cat > "$COPILOT_MANIFEST" <<COPILOT_JSON
{
"name": "$PLUGIN_NAME",
"description": "FILL_IN_DESCRIPTION",
"version": "1.0.0",
"author": { "name": "FILL_IN_AUTHOR_NAME", "email": "FILL_IN_AUTHOR_EMAIL" },
"license": "MIT",
"keywords": [],
"agents": "agents/",
"skills": ["skills/"],
"hooks": "hooks.json",
"mcpServers": ".mcp.json"
}
COPILOT_JSON
echo "Created: $COPILOT_MANIFEST" >&2
created_any=true
fi
# Create Claude Code manifest (.claude-plugin/plugin.json)
if [[ -f "$CC_MANIFEST" ]]; then
echo "Skipping '$CC_MANIFEST' — already exists." >&2
else
cat > "$CC_MANIFEST" <<CC_JSON
{
"name": "$PLUGIN_NAME",
"displayName": "FILL_IN_DISPLAY_NAME",
"description": "FILL_IN_DESCRIPTION",
"version": "1.0.0",
"author": { "name": "FILL_IN_AUTHOR_NAME", "url": "FILL_IN_AUTHOR_URL" },
"license": "MIT",
"keywords": []
}
CC_JSON
echo "Created: $CC_MANIFEST" >&2
created_any=true
fi
if [[ "$created_any" == false ]]; then
echo "All files already exist — nothing to do." >&2
else
echo "" >&2
echo "Plugin: $PLUGIN_NAME" >&2
echo "Location: $PLUGIN_DIR" >&2
echo "" >&2
echo "Next steps:" >&2
echo " 1. Fill in $COPILOT_MANIFEST — replace all FILL_IN_* placeholders" >&2
echo " 2. Fill in $CC_MANIFEST — replace all FILL_IN_* placeholders" >&2
echo " 3. Verify version is identical in both manifests (version parity — ADR-0016)" >&2
echo " 4. Add plugin content: skills in skills/, agents in agents/, etc." >&2
fi

View File

@@ -1,38 +0,0 @@
# skill-audit
Audit a skill directory against the agentskills.io specification. Runs structural validation then a qualitative review across description quality, body discipline, patterns, formatting, file structure, scripts, and internal consistency, plus a provenance chain check.
## What it does
1. Runs `scripts/validate.sh` and `scripts/validate-provenance.sh` for structural and provenance checks, plus `scripts/vale-wrap.sh` — a Vale prefilter that deterministically flags known-bad description openers, vague wording, padding phrases, and "There is/are" sentence openers
2. Reads all files in the skill directory
3. Applies qualitative checks across seven dimensions
4. Outputs a compact findings report — findings only, grouped by dimension, each with Why and Fix — and a result block with handoff to /skill-improve
## Usage
```
/skill-audit
```
Provide the path to the skill directory to audit when invoking.
## Files
| File | Purpose |
|------|---------|
| `SKILL.md` | Skill instructions for agents |
| `scripts/validate.sh` | Structural validator — checks name format, name matches directory, description length, line count, placeholder detection, script executable bit, and interactive-prompt detection |
| `scripts/validate-provenance.sh` | Provenance validator — checks sources.md completeness, source_keys/slug consistency, Contributing files existence, bidirectional linkage, Research doc: fields, and upstream research doc alignment |
| `scripts/vale-wrap.sh` | Vale prefilter wrapper — runs the bundled `Kyberforge` Vale styles against SKILL.md and reports alerts as deterministic FAILs ahead of Step 3's qualitative review |
| `assets/vale/.vale.ini` | Vale configuration — points Vale at the bundled `Kyberforge` style path, self-located relative to `vale-wrap.sh` |
| `assets/vale/styles/Kyberforge/DescriptionOpener.yml` | Vale rule — flags literal "This skill..."/"This agent..." description openers |
| `assets/vale/styles/Kyberforge/PaddingPhrase.yml` | Vale rule — flags generic "see references/" padding phrasing in conditional references |
| `assets/vale/styles/Kyberforge/SentenceOpenerThereIs.yml` | Vale rule — flags body sentences starting with "There is"/"There are" |
| `assets/vale/styles/Kyberforge/VagueWording.yml` | Vale rule — flags known filler wording (e.g. "helps with", "utilize") |
| `references/description-quality.md` | Spec-grounded rubric for description auditing — loaded when a finding is borderline |
| `references/body-discipline.md` | Spec-grounded rubric for body discipline auditing — loaded when padding vs necessity is unclear |
| `references/sources.md` | Provenance record — agentskills.io sources that informed this skill and which files each contributed to |
| `tests/validate.bats` | Bats test suite for validate.sh |
| `tests/validate-provenance.bats` | Bats test suite for validate-provenance.sh |
| `tests/README.md` | Setup instructions for bats-support and bats-assert test dependencies |

View File

@@ -1,153 +0,0 @@
---
name: skill-audit
description: >
Use when the user wants to review a skill they wrote, says "audit this skill",
"check if my skill follows best practices", "review my SKILL.md", or wants to
know if a skill is ready to ship — even if they don't use the word "audit".
Also invoke proactively after directly hand-editing a skill's files outside
skill-author — an unaudited hand-edit is the same risk as unreviewed code.
Audits a skill directory against the agentskills.io specification — structural
checks plus qualitative review of description quality, body discipline, patterns,
formatting, file structure, scripts, and internal consistency, plus a provenance
chain check. Produces a compact findings report
(findings only, no PASS noise) with Why and Fix per finding, suitable for agent
handoff to /skill-improve or human auditability. Do not use to fix application
code bugs or perform general code review unrelated to skill quality.
Do not use when the user wants improvements applied — use /skill-improve instead.
allowed-tools: Bash Read
metadata:
category: factory
source_keys:
- agentskills-home
- agentskills-spec
- agentskills-best-practices
- agentskills-optimizing-descriptions
- agentskills-using-scripts
---
## Gotchas
- Do not output PASS/FAIL per check while auditing — gather findings internally and surface them only in the Step 4 report. Narrating each check as you go is the default failure mode here.
## Step 1 — Structural validation
```bash
bash scripts/validate.sh <skill-dir>
bash scripts/validate-provenance.sh <skill-dir>
scripts/vale-wrap.sh <skill-dir>/SKILL.md
```
Note any structural FAILs — they will appear in the report as a `### Structure` dimension. If the script cannot execute (python3 unavailable, Bash denied, or permission error), perform structural checks manually: name format, name matches directory, description length ≤1024 chars, SKILL.md ≤500 lines and ≤2770 words (the word count is a proxy for the ~5,000-token ceiling, and blocks a commit exactly like the line count does), no unfilled `FILL IN:` placeholders, scripts executable and free of interactive prompts.
Note any Provenance FAILs and INFO findings from `validate-provenance.sh` — they surface in the report as a `### Provenance` dimension (separate from `### Structure`). The script embeds full FAIL/INFO format with Why and Fix per finding; surface them verbatim.
`vale-wrap.sh` ships inside this skill's own `scripts/` — resolve it relative to this skill's directory the same way `scripts/validate.sh` is resolved above, so the invocation works whether this skill is running from this repo or from an installed plugin cache. Pass no `--config`: handed none, the wrapper loads its own sibling `assets/vale/.vale.ini`, located from the script's path rather than from the cwd. Adding an explicit relative `--config` breaks exactly the case the self-location covers — a resolved script path plus an unresolved config path yields `E100 Runtime error ... does not exist`, exit 2, which the fallback below then misreads as "vale unavailable". It applies that config's `Kyberforge` style — a deterministic prefilter for a subset of the Description/Patterns/Body dimensions below, not a replacement for Step 3. Every Vale alert is a `FAIL` — all rules are graded `error` — so report each one citing its rule ID (e.g. `Kyberforge.DescriptionOpener`). Skip and fall back to Step 3 judgment if the `vale` binary is unavailable. If Vale reports `0 files` scanned, treat the pass as NOT RUN — not as clean — and fall back to full Step 3 judgment for the dimensions it would have covered.
## Step 2 — Read all skill files
Read every file in the skill directory: `SKILL.md`, `README.md` (if present), all files in `scripts/`, `references/`, `assets/`, and `tests/`. Skip binary files only. Do not skip text files — internal consistency checks require the full picture.
## Step 3 — Qualitative audit
Work through each dimension internally. Collect findings only; report them in Step 4. Cite file and line number for every finding.
### Description
Vale's `Kyberforge.DescriptionOpener` ("This skill..." openers) and `Kyberforge.VagueWording` (filler like "helps with", "utilize") alerts from Step 1 — both FAILs — cover imperative phrasing and known vague-wording filler directly; report them as findings without re-deriving by judgment. The rest is still a judgment call:
- **Action-verb opening**: does the description start with a verb ("Audits...", "Reviews...", "Validates...")? Vale's `Kyberforge.DescriptionOpener` alert only catches the literal "This skill..." pattern — confirming an arbitrary opening word is genuinely a strong verb still requires judgment.
- **Specificity beyond the filler blocklist**: are capabilities stated precisely ("parses OpenAPI specs") or genuinely vaguely ("handles files")?
- **Indirect triggers**: does it cover cases where the user doesn't name the domain directly?
- **Near-miss exclusions**: are "Do not use when..." clauses present if a near-miss skill could steal activations?
- **Length**: under 1024 characters?
If a description finding is borderline or the distinction between PASS and FAIL is unclear, read `references/description-quality.md`.
### Body discipline
For each sentence in the body, apply: *"Would the agent get this wrong without this sentence?"* Flag any that answer "no" as padding.
- **Defaults not menus**: every decision point gives one default + one escape hatch, not a list of options
- **Why rationale**: include/exclude rules explain why, not just what
- **Control calibration**: prescriptive for fragile or critical sequences (e.g. a script invocation where flag order or exact arguments must not change); flexible where multiple approaches are valid
Vale's `Kyberforge.SentenceOpenerThereIs` alert from Step 1 (FAIL — sentences starting with "There is"/"There are") covers pattern-matchable body-wide filler directly; report it as a finding without re-deriving by judgment.
If uncertain whether a sentence is padding or whether a control decision is correctly calibrated, read `references/body-discipline.md`.
### Patterns
Check each pattern is appropriate and correctly formed:
- **Gotchas**: placed near the top; each entry is a specific fact that defies a reasonable assumption — not a general tip
- **Prescriptive sequence**: inner code fences escaped as `\`\`\`` when nested inside a markdown block
- **Checklists**: used for multi-step workflows, not single steps
- **Conditional references**: specific trigger stated ("If X, read `references/file.md`") — not a generic "see references/". Vale's `Kyberforge.PaddingPhrase` alert from Step 1 flags the generic phrasing directly; other malformed conditional-reference forms still require judgment.
- **Output templates**: present when the agent must produce a specific format; absent otherwise
### File structure
- Permitted directories: `scripts/`, `references/`, `assets/`, `tests/`; flag any other unlisted directory as FAIL — the spec allows additional dirs but this skill permits only these four to keep skills focused
- `scripts/` contains only executable code agents can run; test files (`.bats`, `*_test.*`, `test_*.sh`) in `scripts/` are a FAIL — they belong in `tests/`
- No non-spec files at the skill root (e.g. META.md, extra config files outside permitted directories)
- Optional directories contain real content — not just unfilled placeholder READMEs
- `README.md` present and accurately describes the skill and its files
- No cross-plugin path references in SKILL.md, scripts/, references/, or assets/ — paths using `../`, `../../`, or absolute repo paths (e.g. `plugins/<plugin>/skills/<other-skill>/`, or its APM-native equivalent `.apm/skills/<other-skill>/`) break when the plugin is installed to a cache; flag any found
- `references/sources.md` is exempt from the cross-plugin path check — `Research doc:` fields are development-only provenance pointers, not runtime references; they intentionally reference paths outside the skill directory and are expected to be non-resolvable after plugin install; `validate-provenance.sh` handles this gracefully by silently skipping upstream checks when those paths don't resolve
- `tests/` is exempt from the cross-plugin path check — test files are dev-only and may reference repo-level test infrastructure (e.g. a shared `tests/test_helper/`). This dependency must be declared in `tests/README.md`; flag if tests exist but `tests/README.md` is absent or does not document the dependency
### Formatting
- Heading levels consistent: H2 for main sections, H3 for subsections
- Code blocks fenced with a language tag where applicable (`bash`, `markdown`, `python`)
- Consistent whitespace: blank line between sections, consistent list indentation
- No broken relative paths in file references
### Scripts
- No interactive TTY prompts (`read`, `input()`, `readline`)
- `--help` exposed with concise usage
- Data to stdout, diagnostics to stderr
- Idempotent ("create if not exists")
- Meaningful exit codes documented in `--help`
- `--dry-run` present for destructive operations
### Internal consistency
- SKILL.md steps match what scripts actually do
- `README.md` file table lists every file that exists — no missing entries, no stale entries
- Placeholder READMEs in `scripts/`, `references/`, `assets/` consistent with what SKILL.md says about each directory
## Step 4 — Report
Open with a coverage line listing every dimension checked:
```text
Checked: structure · description · body-discipline · patterns · file-structure · formatting · scripts · internal-consistency · provenance
```
Then output only dimensions that have findings, grouped under H3 headings, FAILs before SUGGESTIONs within each dimension. Omit clean dimensions entirely — their absence confirms they passed.
For each finding:
```text
FAIL/SUGGESTION <finding> — file:line
Why: <why this is a problem>
Fix: <exact change — quote before/after where applicable>
```
Close with a result block:
```text
## Result
PASS
PASS (N suggestions)
PASS · P info
PASS (N suggestions) · P info
FAIL (N fails · M suggestions)
FAIL (N fails · M suggestions) · P info
Run /skill-improve to address findings.
```
INFO findings are observational — do not affect PASS/FAIL. Omit `· P info` when there are no INFO findings. Omit the `/skill-improve` line when there are no findings at all. Do not apply fixes — report and propose only.

View File

@@ -1,4 +0,0 @@
StylesPath = styles
[**/SKILL.md]
BasedOnStyles = Kyberforge

View File

@@ -1,7 +0,0 @@
extends: existence
message: "Description opens with '%s' — use an imperative 'Use when...' opener instead"
level: error
scope: text.frontmatter.description
ignorecase: true
raw:
- '^This (skill|agent)\b'

View File

@@ -1,7 +0,0 @@
extends: existence
message: "Generic reference pointer: '%s' — use the specific 'If X, read `references/file.md`' form instead"
level: error
scope: text
ignorecase: true
raw:
- 'see references?/? for (more )?(info|information|details)\b'

View File

@@ -1,7 +0,0 @@
extends: existence
message: "Don't start a sentence with '%s' — name the subject directly"
level: error
scope: sentence
ignorecase: false
raw:
- '^There\s(is|are)\b'

View File

@@ -1,10 +0,0 @@
extends: existence
message: "Vague capability wording: '%s' — state the capability precisely instead"
level: error
scope: text.frontmatter.description
ignorecase: true
tokens:
- helps with
- utilize
- assists with
- used for

View File

@@ -1,88 +0,0 @@
---
source_keys:
- agentskills-spec
- agentskills-best-practices
---
# Body Discipline Reference
Source: agentskills.io — skill-authoring
## The core test
For every sentence in the body, ask: **"Would the agent get this wrong without this instruction?"**
If no — cut it. The agent already knows it from general training. Adding it wastes tokens and dilutes the signal of what matters.
## What belongs in the body
Include content the agent lacks:
- Project-specific conventions and domain procedures it cannot infer
- Non-obvious edge cases and environment-specific gotchas
- The specific tools or sequences to use (not the full range of options)
- One default per decision point with one escape hatch
Do not include:
- Concepts the agent already knows (what JSON is, how HTTP works, what a CSV is)
- Exhaustive option lists — pick a default; the agent doesn't benefit from choosing
- Steps the agent handles independently — over-specifying leads to unproductive paths
- Restatements of the description — it's already in context
## Calibrating control
**Be prescriptive** when operations are fragile, consistency matters, or a specific sequence must be followed:
```markdown
Run exactly:
\`\`\`bash
python scripts/migrate.py --verify --backup
\`\`\`
Do not modify the command or add additional flags.
```
**Give freedom** when multiple approaches are valid. Explaining *why* outperforms rigid directives — agents make better decisions when they understand the purpose.
## Defaults not menus
Never present a list of equivalent options — pick one and mention the alternative briefly:
```markdown
# Too many options
Use pypdf, pdfplumber, PyMuPDF, or pdf2image...
# Default with escape hatch
Use pdfplumber for text extraction. For scanned PDFs requiring OCR, use pdf2image instead.
```
## Gotchas sections
Highest value content — environment-specific facts that defy reasonable assumptions. Place near the top of the body so the agent reads them before encountering the situation.
```markdown
## Gotchas
- The `users` table uses soft deletes. Always include `WHERE deleted_at IS NULL`.
- User ID is `user_id` in the database, `uid` in auth, `accountId` in billing. Same value.
```
Each entry must be a specific, surprising fact — not a general tip or reminder.
## Progressive disclosure
Keep `SKILL.md` under 500 lines. When more content is needed, move it to `references/` and load conditionally:
```markdown
If the API returns a non-200 status, read `references/api-errors.md`.
```
"If X, read Y" is more useful than "see references/ for details." The agent loads on demand rather than up front.
## Auditing guidance
Flag as FAIL if:
- A sentence answers "no" to the core test (would agent get this wrong without it?) — it is padding
- Decision points present a menu of options with no default
- Instructions repeat content already in the description
- Prescriptive sequences are used where flexibility is fine, or vice versa
Flag as SUGGESTION if:
- A rationale is missing from an include/exclude rule (present but unexplained)
- Gotchas are correct but placed late in the body rather than near the top
- A conditional reference trigger is vague ("see references/") rather than specific ("If X, read Y")

View File

@@ -1,54 +0,0 @@
---
source_keys:
- agentskills-spec
- agentskills-optimizing-descriptions
---
# Description Quality Reference
Source: agentskills.io — optimizing-descriptions
## How triggering works
At startup, agents load only the `name` and `description` of each skill. When a user's task matches a description, the agent reads the full `SKILL.md` into context. **The description carries the entire triggering burden** — the body is never seen until after triggering.
Agents typically consult skills only for tasks requiring knowledge beyond their defaults. Specialized knowledge — unfamiliar APIs, domain-specific workflows, uncommon formats — is where description wording makes the difference.
## What a good description does
- **Imperative phrasing** — "Use when..." not "This skill does...". The agent is deciding whether to act.
- **User intent, not mechanics** — describe what the user is trying to achieve, not how the skill works internally.
- **Err toward being pushy** — explicitly name contexts where the skill applies, including cases where the user doesn't name the domain: "even if they don't mention X explicitly."
- **Specificity over vagueness** — "parses and validates OpenAPI specs" beats "helps with APIs."
- **Near-miss exclusions** — add "Do not use when..." only if a near-miss skill exists that could steal activations. Use strong near-misses (queries that share keywords but need something different), not weak ones ("write a fibonacci function").
- **Hard limit: 1024 characters** — descriptions grow during revision; check length before finalising.
## Before / after
```yaml
# Weak
description: Process CSV files.
# Strong
description: >
Analyze CSV and tabular data files — compute summary statistics,
add derived columns, generate charts, and clean messy data. Use when
the user has a CSV, TSV, or Excel file and wants to explore, transform,
or visualize the data, even if they don't explicitly mention "CSV" or
"analysis."
```
The strong version names capabilities precisely and broadens applicability beyond explicit keyword matches.
## Auditing guidance
Flag as FAIL if:
- Phrasing is descriptive ("This skill...") not imperative ("Use when...")
- Capabilities are vague ("helps with APIs") — require precise verbs and nouns
- No indirect trigger coverage when indirect cases clearly exist
- No near-miss exclusions when a sibling skill could plausibly steal activations
- Length exceeds 1024 characters
Flag as SUGGESTION if:
- Indirect trigger coverage exists but could be more specific
- Near-miss exclusions are present but target weak near-misses only

View File

@@ -1,59 +0,0 @@
# Sources
<!-- agentskills.io/llms.txt was used for initial source discovery and is not listed below; it contributed no skill file content directly. -->
## agentskills-home
- **URL:** https://agentskills.io/home.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Agent Skills overview — what it is, why it exists, progressive disclosure model, ecosystem of 35+ implementing tools
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## agentskills-spec
- **URL:** https://agentskills.io/specification.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Complete SKILL.md format specification — frontmatter fields, constraints, body content, optional directories, progressive disclosure levels, file references, validation
- **Contributing files:** SKILL.md, references/body-discipline.md, references/description-quality.md
- **Status:** `extracted`
## agentskills-best-practices
- **URL:** https://agentskills.io/skill-creation/best-practices.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Best practices for skill creators — starting from real expertise, spending context wisely, calibrating control, instruction patterns (gotchas, templates, checklists, validation loops)
- **Contributing files:** SKILL.md, references/body-discipline.md
- **Status:** `extracted`
## agentskills-optimizing-descriptions
- **URL:** https://agentskills.io/skill-creation/optimizing-descriptions.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** How to systematically test and improve skill descriptions for triggering accuracy — eval queries, trigger rate testing, train/validation splits, optimization loop
- **Contributing files:** SKILL.md, references/description-quality.md
- **Status:** `extracted`
## agentskills-evaluating-skills
- **URL:** https://agentskills.io/skill-creation/evaluating-skills.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Eval-driven skill quality improvement — test case design, workspace structure, assertion writing, grading, benchmarking, human review, iteration loop
- **Contributing files:** (none — eval workflow not directly informing audit dimensions)
- **Status:** `extracted`
## agentskills-using-scripts
- **URL:** https://agentskills.io/skill-creation/using-scripts.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Using scripts in skills — one-off commands, self-contained scripts with inline dependencies, designing scripts for agentic use (no interactive prompts, --help, structured output, idempotency)
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## agentskills-quickstart
- **URL:** https://agentskills.io/skill-creation/quickstart.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Step-by-step guide to creating a first skill (roll-dice example), how discovery/activation/execution work in practice
- **Contributing files:** (none — creation guide not directly informing audit criteria)
- **Status:** `extracted`

View File

@@ -1,526 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Works around a Vale limitation: the `text.frontmatter.description` NLP scope
# silently stops matching once the `description:` value spans 2+ physical lines
# in any form YAML joins back into one string — a `>`/`>-`/`>+` folded block
# scalar (the style used by most skills/agents in this repo), a plain scalar
# wrapped onto continuation lines, or a double- or single-quoted scalar wrapped
# the same way. A `|`/`|-`/`|+` literal block scalar is NOT affected: its parsed
# value keeps exactly the line breaks the source has, and vale matches it fine
# (verified against vale 3.15.2), so literal blocks are deliberately left alone.
# This script flattens an affected description to a one-line scalar in a scratch
# copy — or, for the rare value no inline scalar can spell out verbatim, to a
# `|-` literal block with a single content line, which vale matches just as well
# (padding with blank lines so every other line number is unchanged), then
# runs the real `vale` binary against the copies. Drop-in replacement for calling
# `vale` directly: same args, same exit code, bar the two documented divergences
# below.
#
# "Same args" means relative paths — path arguments and the values of the
# path-valued flags (`--config`, `--output`, `--path`) alike — resolve against
# the caller's current directory, exactly as bare `vale` resolves them. The flag
# values are rewritten to absolute form because the run ends up `cd`'d into the
# scratch mirror, where a relative one would no longer resolve. (An earlier
# version resolved path arguments against the repo root, an invented convention
# that hard-errored on `--config ../../.vale.ini` from a subdirectory and, worse,
# silently dropped file arguments that didn't happen to resolve from the repo
# root — skipping the flattening this script exists for.)
#
# Divergence 1: with no `--config` at all, this script's own sibling
# `assets/vale/.vale.ini` is used instead of vale's upward search. pre-commit
# prefixes only `entry[0]` with the hook-repo clone path, so a `--config` in
# `.pre-commit-hooks.yaml` would resolve against the *consuming* repo and
# hard-fail (E100) for every external consumer. The manifest therefore passes the
# script alone, and an explicit `--config` from any other caller still wins.
#
# Divergence 2: a path-shaped argument that does not exist is a hard error
# (exit 2). Bare vale drops it, falls back to reading stdin, and prints
# `0 errors ... in stdin` with exit 0 — a typo'd target is then indistinguishable
# from a clean run. Both audit skills treat a `0 files` report as NOT RUN rather
# than clean, and `in stdin` does not match that guard, so the silent form would
# read as "prefilter clean" and skip the LLM fallback. Erroring is the only way
# to keep that guard honest. Linting prose piped on stdin is therefore
# unsupported here — it already was, since the no-path handoff closes stdin so
# vale can't block on a pipe that will never carry content.
#
# Vale prints each path exactly as it was handed to it, so the scratch tree
# mirrors the caller's absolute cwd: a relative path argument is passed through
# verbatim and resolves to its flattened copy, keeping the report byte-identical
# to bare `vale`'s. An absolute path inside the cwd is relativized to keep that
# property. Only an absolute path outside the cwd is rewritten to its scratch
# copy and so reports a scratch path — unavoidable, since a file can only be
# read from where it actually is.
cwd="$(pwd -P)"
# Every array below is expanded as `${arr[@]+"${arr[@]}"}`: bash before 4.4 —
# including the 3.2 that macOS still ships as /bin/bash — treats `"${arr[@]}"`
# on an empty array as an unbound variable under `set -u`. No expansion site is
# reachable while empty on today's control flow, so this is insurance against a
# later edit breaking that invariant, not a live fix.
vale_args=()
path_args=()
pending_flag=""
config_given=false
# `--output` takes either one of vale's built-in style names or a template file
# path. Only the file form needs absolutizing, and the built-in names have to be
# excluded by name *before* the existence test below: a file or directory
# literally called `line` in the caller's cwd would otherwise rewrite the
# built-in into `$cwd/line`, flipping vale into template mode (`E100 [template]
# Runtime error`) where bare vale just uses the built-in. `--path` has no such
# names — it is always a path — so the check is keyed on the flag too.
is_builtin_output() {
case "$2" in
line|JSON|CLI) [[ "$1" == "--output" ]] ;;
*) false ;;
esac
}
# Absolutizes a `--config` value against the caller's cwd. Shared by both
# argument forms below — separated (`--config X`) and joined (`--config=X`)
# — so the "already absolute vs. needs $cwd prefixed" check lives in exactly
# one place instead of being duplicated per form.
abs_config_value() {
if [[ "$1" == /* ]]; then
printf '%s' "$1"
else
printf '%s' "$cwd/$1"
fi
}
for arg in "$@"; do
if [[ -n "$pending_flag" ]]; then
# Value of a separated two-argv flag. It is never a lint target, however
# file-like it looks. The run ends up `cd`'d into the scratch mirror, so a
# value naming a file has to be absolutized here or it stops resolving.
case "$pending_flag" in
--config)
# Always a path, and required to exist.
vale_args+=("$(abs_config_value "$arg")")
;;
--output|--path)
# See `is_builtin_output` above for why the built-in `--output` names
# are excluded first. Anything that names nothing is passed through and
# left for vale to interpret.
if is_builtin_output "$pending_flag" "$arg"; then
vale_args+=("$arg")
elif [[ "$arg" != /* && -e "$arg" ]]; then
vale_args+=("$cwd/$arg")
else
vale_args+=("$arg")
fi
;;
*)
vale_args+=("$arg")
;;
esac
pending_flag=""
continue
fi
case "$arg" in
--config)
vale_args+=("$arg")
pending_flag="$arg"
config_given=true
continue
;;
--config=*)
vale_args+=("--config=$(abs_config_value "${arg#--config=}")")
config_given=true
continue
;;
# Same cwd-relative resolution for the `--flag=value` spelling of the two
# other path-valued flags.
--output=*|--path=*)
flag_val="${arg#*=}"
if is_builtin_output "${arg%%=*}" "$flag_val"; then
vale_args+=("$arg")
elif [[ "$flag_val" != /* && -n "$flag_val" && -e "$flag_val" ]]; then
vale_args+=("${arg%%=*}=$cwd/$flag_val")
else
vale_args+=("$arg")
fi
continue
;;
# Vale's remaining value-taking flags, per `vale --help` (3.x). In the
# separated two-argv form the value must not be classified as a lint target
# — `--output tmpl.tmpl` names a real template file, and treating it as
# input both lints the template and reorders argv so vale sees
# `--output --no-wrap`. The `--flag=value` form needs no entry here: it
# starts with `-` and falls through to vale untouched. A value flag added by
# some future vale release is simply absent from this list and lands back on
# today's behaviour, so this list going stale is never worse than not having
# it.
--ext|--filter|--glob|--minAlertLevel|--output|--path)
vale_args+=("$arg")
pending_flag="$arg"
continue
;;
# Vale's subcommands are bare words that name no file, so they would trip
# the not-found error below. A lint target literally named `sync` (no
# extension, no slash) is misread as the subcommand — accepted, because the
# alternative is failing every `vale-wrap.sh ls-config`.
ls-config|ls-dirs|ls-metrics|ls-vars|sync)
vale_args+=("$arg")
continue
;;
esac
if [[ "$arg" == -* ]]; then
vale_args+=("$arg")
continue
fi
# Everything left is a lint target: `vale [options] [input...]` has no third
# kind of argument. See divergence 2 above for why a missing one is fatal here.
if [[ ! -e "$arg" ]]; then
echo "vale-wrap.sh: no such file or directory: $arg" >&2
exit 2
fi
# An absolute path inside the caller's cwd is relativized so the report cites
# a path that resolves against the real tree. Left absolute, it would be
# rewritten to its scratch copy and printed as `/tmp/tmp.XXXX/...` — a real
# path to a file that is deleted on exit, which reads as a bug in any report
# quoting it. Absolute paths outside the cwd have no relative form and keep
# the scratch-path behaviour documented above.
if [[ "$arg" == "$cwd"/* ]]; then
path_args+=("${arg#"$cwd"/}")
else
path_args+=("$arg")
fi
done
if [[ "$config_given" == false ]]; then
vale_args+=(--config "$(cd "$(dirname "${BASH_SOURCE[0]}")/../assets/vale" && pwd)/.vale.ini")
fi
if [[ ${#path_args[@]} -eq 0 ]]; then
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
# block waiting on a pipe that will never carry content.
exec vale ${vale_args[@]+"${vale_args[@]}"} < /dev/null
fi
# `realpath -m` would be the obvious normalizer, but `-m` (canonicalize-missing)
# is a GNU extension the BSD realpath on macOS doesn't have — and every dest
# below is a path that doesn't exist yet. python3 is already a hard dependency.
abspath() {
python3 -c 'import os, sys; print(os.path.abspath(sys.argv[1]))' "$1"
}
flatten() {
# Two call shapes: `flatten src dest` (dest already resolved and inside the
# scratch tree — the per-markdown-file calls in the directory branch below)
# writes straight to `dest`. `flatten src raw_dest tmpdir` (the single-file
# branch further down) additionally resolves `raw_dest` the way a separate
# `abspath` call used to, applies the same sandbox-escape guard, and prints
# the resolved path — folding two python3 spawns per file into one.
python3 - "$@" <<'PYTHON'
import os
import re
import sys
src, dest_input = sys.argv[1], sys.argv[2]
tmpdir = sys.argv[3] if len(sys.argv) > 3 else None
if tmpdir is None:
dest = dest_input
else:
dest = os.path.abspath(dest_input)
if not dest.startswith(tmpdir + os.sep):
print(
f"vale-wrap.sh: refusing to lint '{src}': its scratch copy would "
f"land outside {tmpdir}",
file=sys.stderr,
)
sys.exit(2)
os.makedirs(os.path.dirname(dest), exist_ok=True)
# surrogateescape keeps a non-UTF-8 file (reachable via a directory argument)
# a byte-for-byte round trip instead of aborting the whole run on a decode error.
with open(src, encoding='utf-8', errors='surrogateescape') as fh:
content = fh.read()
# YAML 1.2 double-quoted escapes (spec 5.7 / 7.3.1). `\<newline>` is handled
# separately in unescape_double because it also swallows the next indentation.
DQ_ESCAPES = {
'0': '\0', 'a': '\a', 'b': '\b', 't': '\t', '\t': '\t', 'n': '\n',
'v': '\v', 'f': '\f', 'r': '\r', 'e': '\x1b', ' ': ' ', '"': '"',
'/': '/', '\\': '\\', 'N': '\x85', '_': '\xa0', 'L': '\u2028',
'P': '\u2029',
}
# First characters that make a plain (unquoted) scalar mean something other than
# text: YAML's c-indicator set.
PLAIN_UNSAFE_FIRST = '-?:,[]{}#&*!|>\'"%@`'
def unescape_double(text):
"""Decode a double-quoted YAML scalar's body to the string YAML parses."""
out = []
i = 0
while i < len(text):
char = text[i]
if char != '\\':
out.append(char)
i += 1
continue
i += 1
if i >= len(text):
break
esc = text[i]
if esc == '\n':
i += 1
while i < len(text) and text[i] in ' \t':
i += 1
continue
if esc in 'xuU':
width = {'x': 2, 'u': 4, 'U': 8}[esc]
digits = text[i + 1:i + 1 + width]
if len(digits) == width:
try:
out.append(chr(int(digits, 16)))
except ValueError:
pass
else:
i += 1 + width
continue
out.append(DQ_ESCAPES.get(esc, esc))
i += 1
return ''.join(out)
def close_quote(text, quote):
"""Index of the closing `quote` in `text`, which starts just past the
opening one. None while the scalar is still unterminated."""
i = 0
while i < len(text):
char = text[i]
if quote == '"' and char == '\\':
i += 2
continue
if char == quote:
if quote == "'" and text[i + 1:i + 2] == "'":
i += 2
continue
return i
i += 1
return None
def continuation_lines(rest):
"""Yield the physical lines of `rest` that continue the value started on the
`description:` line. Indentation-based and blank-line-tolerant, per YAML:
a blank line (any amount of whitespace) always stays inside; the indent is
set by the first content line; the value ends at the first line indented
less than that, at any line flush with the key (that is the next mapping
key, not a continuation), or at EOF."""
indent = None
for line in rest.splitlines(keepends=True):
text = line.rstrip('\n')
if text.strip() == '':
yield line
continue
line_indent = len(text) - len(text.lstrip(' \t'))
if line_indent == 0:
return
if indent is None:
indent = line_indent
elif line_indent < indent:
return
yield line
def emit(value):
"""Render `value` as a YAML scalar whose source text spells the value out
verbatim. Vale locates the description by matching the parsed value back
against the source, so a scalar carrying any escape — `''` in a
single-quoted scalar, `\\"` or `\\\\` in a double-quoted one — makes the
whole `text.frontmatter.description` scope vanish, the same failure this
script exists to work around. Verbatim forms only, therefore, tried in
descending order of fidelity. The first three occupy one physical line; the
`|-` fallback occupies two, which the caller accounts for when padding."""
if (value
and value[0] not in PLAIN_UNSAFE_FIRST
and ': ' not in value
and not value.endswith(':')
and ' #' not in value):
return value # plain: nothing needs escaping at all
if "'" not in value:
return "'" + value + "'" # single-quoted: only `'` would escape
if '"' not in value and '\\' not in value:
return '"' + value + '"' # double-quoted: only `"`/`\` would
# Last resort: the value needs quoting AND holds an apostrophe AND a double
# quote or backslash, so no *inline* scalar can carry it verbatim. A `|-`
# literal block can — a block scalar's body has no escape syntax at all, so
# `'`, `"`, `\` and `: ` all survive byte for byte, and vale still matches
# the description scope against it (the header above says the same of the
# `|` blocks this script deliberately leaves alone; verified against vale
# 3.15.2). One content line, indented two spaces, `-`-chomped so the parsed
# value is exactly `value` with no trailing newline.
return '|-\n ' + value
fm_match = re.match(r'^(---\n)(.*?\n)(---\n)', content, re.DOTALL)
if fm_match:
fm = fm_match.group(2)
header_m = re.search(r'^description:[ \t]*', fm, re.MULTILINE)
else:
header_m = None
if header_m:
head_start = header_m.start()
value_start = header_m.end()
header_end = fm.find('\n', value_start)
header_end = len(fm) if header_end == -1 else header_end
first = fm[value_start:header_end]
body_start = header_end + 1
indicator = first.rstrip()
block_m = re.fullmatch(r'([|>])([+-]?[0-9]*|[0-9]*[+-]?)', indicator)
if block_m and block_m.group(1) == '|':
kind = None # literal blocks keep their line breaks; vale is fine
elif block_m:
kind = 'block' # folded (`>`): the value starts on the next line
elif indicator == '':
kind = 'block' # bare `description:`: a plain scalar on later lines
elif first[:1] == '"':
kind = 'double'
elif first[:1] == "'":
kind = 'single'
elif first[:1] in '#&*!':
kind = None # comment, anchor, alias or tag — not a plain scalar
else:
kind = 'plain'
text = ''
value_end = value_start
value_lines = 0
if kind in ('block', 'plain'):
body = ''.join(continuation_lines(fm[body_start:]))
value_end = body_start + len(body)
if kind == 'block':
text = body
value_lines = body.count('\n')
else:
text = fm[value_start:value_end]
value_lines = 1 + body.count('\n')
if ' #' in text or text.lstrip().startswith('#'):
# A `#` opens a comment inside a plain scalar. Folding it in
# would lint text YAML never treats as part of the value, so
# leave the file alone rather than lint the wrong string.
kind = None
elif kind in ('double', 'single'):
quote = '"' if kind == 'double' else "'"
inner_start = value_start + 1
acc = fm[inner_start:body_start]
idx = close_quote(acc, quote)
lines = continuation_lines(fm[body_start:])
while idx is None:
try:
acc += next(lines)
except StopIteration:
break
idx = close_quote(acc, quote)
if idx is None:
kind = None # unterminated quote: invalid YAML, leave it to vale
else:
inner = acc[:idx]
value_end = inner_start + idx + 1
text = unescape_double(inner) if quote == '"' else inner.replace("''", "'")
value_lines = 1 + inner.count('\n')
flat = re.sub(r'\s+', ' ', text).strip()
if kind and flat and value_lines >= 2:
# `value_end` can land mid-line, just past a closing quote, so extend to
# the end of that physical line and carry whatever follows (a trailing
# comment) across unchanged.
if value_end > 0 and fm[value_end - 1] == '\n':
span_end = value_end
trailer = ''
else:
newline = fm.find('\n', value_end)
span_end = len(fm) if newline == -1 else newline + 1
trailer = fm[value_end:span_end].rstrip('\n')
scalar = emit(flat)
# A trailing comment carried across from the original line stays on the
# `description:` line itself: after a block scalar's `|-` header it is
# still a comment, but inside the block body it would become part of the
# value.
head, newline_sep, block_body = scalar.partition('\n')
# The replacement displaces the whole span, so the blank-line pad makes
# up the difference between the lines it displaced and the lines it
# occupies — every later line number is unchanged. That is one line for
# the three inline forms and two for the `|-` block; the span itself is
# at least two lines here (`value_lines >= 2` is a precondition), so the
# pad count never goes negative.
pad = '\n' * (fm[head_start:span_end].count('\n') - 1 - scalar.count('\n'))
new_fm = (fm[:head_start] + 'description: ' + head + trailer
+ newline_sep + block_body + '\n' + pad + fm[span_end:])
content = (fm_match.group(1) + new_fm + fm_match.group(3)
+ content[fm_match.end():])
with open(dest, 'w', encoding='utf-8', errors='surrogateescape') as fh:
fh.write(content)
if tmpdir is not None:
print(dest)
PYTHON
}
tmpdir="$(cd "$(mktemp -d)" && pwd -P)"
trap 'rm -rf "$tmpdir"' EXIT
# Mirror of the caller's cwd inside the scratch tree; relative path arguments
# are resolved from here.
mirror="$tmpdir$cwd"
mkdir -p "$mirror"
argv_paths=()
for arg in ${path_args[@]+"${path_args[@]}"}; do
if [[ "$arg" == /* ]]; then
raw_dest="$tmpdir$arg"
else
raw_dest="$mirror/$arg"
fi
if [[ -d "$arg" ]]; then
dest="$(abspath "$raw_dest")"
# A path argument with enough leading `..` to climb past the mirror root would
# write outside the scratch dir. The real filesystem clamps such a path at
# `/`; the mirror can't, so refuse rather than scribble outside the sandbox.
case "$dest" in
"$tmpdir"/*) ;;
*)
echo "vale-wrap.sh: refusing to lint '$arg': its scratch copy would land outside $tmpdir" >&2
exit 2
;;
esac
mkdir -p "$(dirname "$dest")"
# A directory is mirrored whole — vale applies its own format filtering to
# the tree, so any file dropped here would be silently unlinted — and then
# every markdown file in the copy is flattened in place. `.git` is pruned:
# vale never lints it and copying it can dwarf the rest of the tree.
# `find -L` follows symlinks because vale does: it lints both a symlinked
# file and a file under a symlinked directory, and a bare `-type f` walk
# would report "0 files" where bare vale reports one. (A symlink loop makes
# `find` warn on stderr and carry on, which is also what vale does.) The
# second walk needs no `-L`: the mirror is all real files by construction.
mkdir -p "$dest"
while IFS= read -r -d '' rel; do
mkdir -p "$dest/$(dirname "$rel")"
cp "$arg/$rel" "$dest/$rel"
done < <(cd "$arg" && find -L . -name .git -prune -o -type f -print0)
while IFS= read -r -d '' md; do
flatten "$md" "$md"
done < <(find "$dest" -type f -name '*.md' -print0)
else
# `abspath` + `flatten` folded into one python3 process — see the comment
# atop `flatten` above.
dest="$(flatten "$arg" "$raw_dest" "$tmpdir")"
fi
if [[ "$arg" == /* ]]; then
argv_paths+=("$dest")
else
argv_paths+=("$arg")
fi
done
cd "$mirror"
vale ${vale_args[@]+"${vale_args[@]}"} ${argv_paths[@]+"${argv_paths[@]}"}

View File

@@ -1,395 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
Usage: validate-provenance.sh <skill-dir>
Validate that a skill's sources provenance chain is complete and internally consistent.
Arguments:
skill-dir Path to the skill directory to validate.
Exit codes:
0 All checks passed (or nothing to validate)
1 One or more checks failed
Checks performed:
0 source_keys present but references/sources.md absent
1 FILL IN: placeholders in sources.md
2 source_keys in SKILL.md → slug exists in sources.md
3 source_keys in references/*.md → slug exists in sources.md (INFO if no source_keys)
4 Contributing files listed in sources.md exist on disk
5 Contributing files back-reference the parent slug in their source_keys
6 Research doc field present and not placeholder
7 Slug in sources.md present in upstream research doc (INFO only)
8 Extracted non-(none) slug in research doc present in sources.md
EOF
}
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage
exit 0
fi
if [[ $# -lt 1 ]]; then
echo "Error: skill-dir is required." >&2
echo "" >&2
usage >&2
exit 1
fi
python3 -u - "$1" <<'PYTHON'
import sys
import os
import re
skill_dir = os.path.abspath(sys.argv[1])
sources_md_path = os.path.join(skill_dir, "references", "sources.md")
refs_dir = os.path.join(skill_dir, "references")
# --- Helpers ---
PLACEHOLDER_RE = re.compile(r'(?<!`)FILL IN:[^`\n]')
def parse_frontmatter(content):
"""Return (frontmatter_str, body_str) or (None, content) if no frontmatter."""
m = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
if not m:
return None, content
return m.group(1), content[m.end():]
def parse_source_keys(fm):
"""Extract list of source_keys from frontmatter string (handles metadata.source_keys and top-level)."""
if fm is None:
return []
keys = []
# Match either:
# metadata:\n source_keys:\n - slug
# or:
# source_keys:\n - slug
in_source_keys = False
in_metadata = False
for line in fm.splitlines():
if re.match(r'^metadata:', line):
in_metadata = True
continue
if in_metadata and re.match(r'^ source_keys:', line):
in_source_keys = True
continue
if not in_metadata and re.match(r'^source_keys:', line):
in_source_keys = True
continue
if in_source_keys:
m = re.match(r'^[ \t]+-\s+(\S+)', line)
if m:
keys.append(m.group(1).strip())
elif line and not line[0].isspace():
in_source_keys = False
in_metadata = False
return keys
def parse_h2_slugs(content):
"""Return list of H2 heading values from a markdown file."""
return re.findall(r'^## (.+)$', content, re.MULTILINE)
def parse_contributing_files(content, slug):
"""Find the Contributing files value for a given slug H2 in content."""
# Find the H2 block for slug, then look for Contributing files line
pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL
)
m = pattern.search(content)
if not m:
return None
block = m.group(1)
cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE)
if not cf_m:
return None
return cf_m.group(1).strip()
def parse_research_doc(content, slug):
"""Find the Research doc value for a given slug H2 in content."""
pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL
)
m = pattern.search(content)
if not m:
return None
block = m.group(1)
rd_m = re.search(r'^\- \*\*Research doc:\*\* (.+)$', block, re.MULTILINE)
if not rd_m:
return None
return rd_m.group(1).strip()
def parse_status(content, slug):
"""Find the Status value for a given slug H2 in content."""
pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL
)
m = pattern.search(content)
if not m:
return None
block = m.group(1)
st_m = re.search(r'^\- \*\*Status:\*\* (.+)$', block, re.MULTILINE)
if not st_m:
return None
return st_m.group(1).strip()
def find_repo_root(start_dir):
"""Walk up from start_dir until we find a directory containing .git."""
current = start_dir
while True:
if os.path.exists(os.path.join(current, ".git")):
return current
parent = os.path.dirname(current)
if parent == current:
return None
current = parent
findings = []
has_fail = False
def emit_fail(desc, fpath, why, fix):
global has_fail
has_fail = True
findings.append(("FAIL", desc, fpath, why, fix, None))
def emit_info(desc, fpath, note):
findings.append(("INFO", desc, fpath, None, None, note))
def print_findings():
for entry in findings:
kind = entry[0]
desc = entry[1]
fpath = entry[2]
why = entry[3]
fix = entry[4]
note = entry[5]
if kind == "FAIL":
print(f"FAIL {desc} — {fpath}")
print(f" Why: {why}")
print(f" Fix: {fix}")
print()
else:
print(f"INFO {desc} — {fpath}")
print(f" Note: {note}")
print()
# --- Scan for any file with source_keys ---
def file_has_source_keys(fpath):
try:
with open(fpath) as f:
content = f.read()
except Exception:
return False
fm, _ = parse_frontmatter(content)
if fm is None:
return False
return bool(parse_source_keys(fm))
def find_files_with_source_keys():
"""Return list of (relative_path, abs_path) for all skill files with source_keys."""
results = []
for root, dirs, files in os.walk(skill_dir):
# Skip hidden dirs
dirs[:] = [d for d in dirs if not d.startswith('.')]
for fname in files:
if fname.endswith('.md'):
abs_path = os.path.join(root, fname)
if file_has_source_keys(abs_path):
rel = os.path.relpath(abs_path, skill_dir)
results.append((rel, abs_path))
return results
sources_md_exists = os.path.isfile(sources_md_path)
files_with_source_keys = find_files_with_source_keys()
# Early exit: nothing to validate
if not sources_md_exists and not files_with_source_keys:
sys.exit(0)
# Load sources.md if it exists
sources_content = None
if sources_md_exists:
with open(sources_md_path) as f:
sources_content = f.read()
sources_slugs = set(parse_h2_slugs(sources_content))
else:
sources_slugs = set()
# --- Check 0: source_keys without sources.md ---
if not sources_md_exists:
for rel, abs_path in files_with_source_keys:
emit_fail(
f"source_keys declared but references/sources.md is absent",
rel,
"source_keys references research provenance that has no sources index to validate against.",
"Create references/sources.md with an H2 entry for each slug referenced by source_keys."
)
print_findings()
sys.exit(1)
# --- Check 1: FILL IN: placeholders in sources.md ---
for line in sources_content.splitlines():
if PLACEHOLDER_RE.search(line):
emit_fail(
"Unfilled FILL IN: placeholder",
"references/sources.md",
"sources.md contains an unfilled placeholder, meaning provenance is incomplete.",
"Replace all 'FILL IN:' values in references/sources.md with real content."
)
break
# --- Check 2: source_keys in SKILL.md → slug exists in sources.md ---
skill_md_path = os.path.join(skill_dir, "SKILL.md")
if os.path.isfile(skill_md_path):
with open(skill_md_path) as f:
skill_content = f.read()
skill_fm, _ = parse_frontmatter(skill_content)
skill_source_keys = parse_source_keys(skill_fm)
for slug in skill_source_keys:
if slug not in sources_slugs:
emit_fail(
f"source_keys slug '{slug}' not found in sources.md",
"SKILL.md",
f"SKILL.md declares '{slug}' as a source but there is no '## {slug}' heading in references/sources.md.",
f"Add '## {slug}' entry to references/sources.md or remove '{slug}' from SKILL.md source_keys."
)
# --- Check 3: source_keys in references/*.md → slug exists in sources.md (INFO if no source_keys) ---
if os.path.isdir(refs_dir):
for fname in sorted(os.listdir(refs_dir)):
if not fname.endswith('.md'):
continue
if fname == "sources.md":
continue
fpath = os.path.join(refs_dir, fname)
rel = os.path.relpath(fpath, skill_dir)
with open(fpath) as f:
ref_content = f.read()
ref_fm, _ = parse_frontmatter(ref_content)
ref_keys = parse_source_keys(ref_fm)
if not ref_keys:
emit_info(
f"No source_keys frontmatter",
rel,
"This references file has no source_keys — provenance cannot be verified. "
"Add source_keys frontmatter listing the slugs from references/sources.md that informed this file."
)
else:
for slug in ref_keys:
if slug not in sources_slugs:
emit_fail(
f"source_keys slug '{slug}' not found in sources.md",
rel,
f"'{rel}' declares '{slug}' as a source but there is no '## {slug}' heading in references/sources.md.",
f"Add '## {slug}' entry to references/sources.md or remove '{slug}' from {rel} source_keys."
)
# --- Checks 4, 5, 6, 7, 8: Per-slug checks in sources.md ---
repo_root = find_repo_root(skill_dir)
# Collect all research doc paths we'll check (for Check 8)
research_docs_seen = {} # abs_path → set of slugs in sources.md that reference it
for slug in parse_h2_slugs(sources_content):
# Check 4: Contributing files exist
cf_value = parse_contributing_files(sources_content, slug)
if cf_value and not cf_value.startswith("(none"):
# Split by comma
cf_files = [p.strip() for p in cf_value.split(",") if p.strip()]
for cf_rel in cf_files:
cf_abs = os.path.join(skill_dir, cf_rel)
if not os.path.isfile(cf_abs):
emit_fail(
f"Contributing file '{cf_rel}' does not exist",
f"references/sources.md (## {slug})",
f"sources.md claims '{cf_rel}' was contributed to by slug '{slug}' but the file does not exist.",
f"Create '{cf_rel}' relative to the skill directory, or correct the path in sources.md."
)
else:
# Check 5: Bidirectional — file should list slug in its source_keys
# Skip sources.md itself
if cf_rel == "references/sources.md":
continue
with open(cf_abs) as f:
cf_content = f.read()
cf_fm, _ = parse_frontmatter(cf_content)
cf_keys = parse_source_keys(cf_fm)
if slug not in cf_keys:
emit_fail(
f"Contributing file '{cf_rel}' does not list '{slug}' in its source_keys",
f"references/sources.md (## {slug})",
f"sources.md says '{cf_rel}' was informed by '{slug}', but '{cf_rel}' does not declare '{slug}' in its source_keys frontmatter.",
f"Add '{slug}' to the source_keys frontmatter of '{cf_rel}'."
)
# Check 6: Research doc field required
rd_value = parse_research_doc(sources_content, slug)
if rd_value is None:
emit_fail(
f"Research doc field missing",
f"references/sources.md (## {slug})",
f"The '## {slug}' entry in sources.md has no '- **Research doc:**' line.",
f"Add '- **Research doc:** <path-or-(none)>' to the '## {slug}' entry in references/sources.md."
)
elif rd_value == "" or PLACEHOLDER_RE.search(rd_value):
emit_fail(
f"Research doc field is empty or placeholder",
f"references/sources.md (## {slug})",
f"The '## {slug}' entry has an unfilled Research doc value.",
f"Set '- **Research doc:**' to a real path relative to repo root, or '(none)' if not applicable."
)
else:
# Check 7: Upstream forward — slug should appear in research doc
if repo_root and not rd_value.startswith("(none"):
rd_abs = os.path.join(repo_root, rd_value)
if os.path.isfile(rd_abs):
with open(rd_abs) as f:
rd_content = f.read()
rd_slugs = set(parse_h2_slugs(rd_content))
if slug not in rd_slugs:
emit_info(
f"Slug '{slug}' not found as H2 in research doc '{rd_value}'",
f"references/sources.md (## {slug})",
f"The research doc '{rd_value}' does not have a '## {slug}' heading. "
f"The provenance link may be imprecise — the slug name in sources.md may differ from the research doc's heading."
)
# Track for Check 8
if rd_abs not in research_docs_seen:
research_docs_seen[rd_abs] = (rd_value, set())
research_docs_seen[rd_abs][1].add(slug)
# --- Check 8: Upstream reverse ---
for rd_abs, (rd_rel, known_slugs) in research_docs_seen.items():
with open(rd_abs) as f:
rd_content = f.read()
for rd_slug in parse_h2_slugs(rd_content):
# Parse this slug's Contributing files and Status in the research doc
rd_cf = parse_contributing_files(rd_content, rd_slug)
rd_status = parse_status(rd_content, rd_slug)
# Skip if contributing files start with (none
if rd_cf and rd_cf.startswith("(none"):
continue
# Skip if status is not `extracted`
if rd_status != "`extracted`":
continue
# This slug should be in sources.md
if rd_slug not in sources_slugs:
emit_fail(
f"Research doc slug '{rd_slug}' missing from skill sources.md",
f"references/sources.md",
f"The research doc '{rd_rel}' has '## {rd_slug}' with status `extracted` and contributing files, "
f"but this skill's sources.md has no '## {rd_slug}' entry.",
f"Add '## {rd_slug}' to references/sources.md or mark it as '(none)' in the research doc's Contributing files."
)
print_findings()
sys.exit(1 if has_fail else 0)
PYTHON

View File

@@ -1,199 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
Usage: validate.sh <skill-dir>
Validate a skill directory against the agentskills.io specification.
Arguments:
skill-dir Path to the skill directory containing SKILL.md.
Exit codes:
0 All checks passed
1 One or more checks failed
EOF
}
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage
exit 0
fi
if [[ $# -lt 1 ]]; then
echo "Error: skill-dir is required." >&2
echo "" >&2
usage >&2
exit 1
fi
python3 -u - "$1" <<'PYTHON'
import sys
import os
import re
skill_dir = os.path.abspath(sys.argv[1])
skill_md = os.path.join(skill_dir, "SKILL.md")
if not os.path.isfile(skill_md):
print(f"Error: '{skill_md}' not found.", file=sys.stderr)
sys.exit(1)
with open(skill_md) as f:
content = f.read()
failed = False
def ok(msg):
print(f"PASS {msg}")
def fail(msg):
global failed
print(f"FAIL {msg}")
failed = True
# --- Parse frontmatter ---
fm_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
if not fm_match:
fail("No valid YAML frontmatter block found (expected ---...---)")
sys.exit(1)
fm = fm_match.group(1)
body_start = fm_match.end()
# Extract name
name_m = re.search(r'^name:\s*(\S+)', fm, re.MULTILINE)
name = name_m.group(1).strip('"\'') if name_m else ""
# Extract description — inline or block scalar (> or |)
desc = ""
desc_m = re.search(r'^description:\s*([>|])\n((?:[ \t]+.+\n?)+)', fm, re.MULTILINE)
if desc_m:
raw = desc_m.group(2)
desc = re.sub(r'\s+', ' ', raw).strip()
else:
desc_inline = re.search(r'^description:\s*(.+)', fm, re.MULTILINE)
if desc_inline:
desc = desc_inline.group(1).strip()
dir_name = os.path.basename(skill_dir)
# --- Checks ---
# name present
if name:
ok(f"name present: '{name}'")
else:
fail("name field is missing or empty")
# name matches directory
if name and dir_name:
if name == dir_name:
ok(f"name '{name}' matches directory '{dir_name}'")
else:
fail(f"name '{name}' does not match directory '{dir_name}'")
# name length
if name:
if len(name) <= 64:
ok(f"name length {len(name)} chars (limit: 64)")
else:
fail(f"name '{name}' is {len(name)} chars — exceeds 64-character limit")
# name format
if name:
if re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', name):
ok(f"name format valid (kebab-case)")
else:
fail(f"name '{name}' is invalid — use lowercase letters, numbers, and hyphens only; no leading, trailing, or consecutive hyphens")
# description present
if desc:
ok(f"description present")
else:
fail("description field is missing or empty")
# description length
if desc:
dlen = len(desc)
if dlen <= 1024:
ok(f"description length {dlen} chars (limit: 1024)")
else:
fail(f"description length {dlen} chars — exceeds 1024-character limit")
# Unfilled placeholder detection — matches FILL IN: followed by actual content,
# but not backtick-quoted references like `FILL IN:` used in instructions.
PLACEHOLDER_RE = re.compile(r'(?<!`)FILL IN:[^`\n]')
# description contains unfilled placeholder
if desc and PLACEHOLDER_RE.search(desc):
fail("description still contains 'FILL IN:' placeholder — replace before shipping")
else:
if desc:
ok("description has no unfilled placeholders")
# SKILL.md size ceilings (agentskills.io skill-authoring.md: 500 lines,
# ~5,000 tokens). Both constants are DUPLICATED from the repo-root pre-commit
# hook scripts/skill-size-check.sh — a plugin skill's scripts cannot read files
# outside the plugin directory once the plugin is cache-installed, so there is
# no single source to share. Keep the two in sync by hand: if they drift, this
# audit will report a skill ready to ship that the commit hook then rejects.
MAX_LINES = 500
# Word-count proxy for the ~5,000-token ceiling, calibrated to the densest
# prose in the corpus (7.22 chars/word): 2770 words is ~20,000 characters,
# ~5,000 tokens at 4 characters per token. See skill-size-check.sh's header
# for the full measurement.
MAX_WORDS = 2770
line_count = len(content.splitlines())
if line_count <= MAX_LINES:
ok(f"SKILL.md line count {line_count} (limit: {MAX_LINES})")
else:
fail(f"SKILL.md line count {line_count} — exceeds {MAX_LINES}-line limit")
# str.split() with no argument splits on runs of whitespace, matching the
# `wc -w` the hook uses, and counts the whole file including frontmatter.
word_count = len(content.split())
if word_count <= MAX_WORDS:
ok(f"SKILL.md word count {word_count} (limit: {MAX_WORDS}, proxy for ~5,000 tokens)")
else:
fail(f"SKILL.md word count {word_count} — exceeds {MAX_WORDS}-word limit (proxy for ~5,000 tokens)")
# Body unfilled placeholders
body = content[body_start:]
fill_matches = PLACEHOLDER_RE.findall(body)
if fill_matches:
fail(f"SKILL.md body contains {len(fill_matches)} unfilled 'FILL IN:' placeholder(s)")
else:
ok("SKILL.md body has no unfilled placeholders")
# Scripts checks
scripts_dir = os.path.join(skill_dir, "scripts")
if os.path.isdir(scripts_dir):
scripts = [f for f in os.listdir(scripts_dir)
if os.path.isfile(os.path.join(scripts_dir, f)) and not f.endswith('.md')]
for fname in scripts:
fpath = os.path.join(scripts_dir, fname)
with open(fpath) as f:
sc = f.read()
# Interactive prompt heuristic
if re.search(r'^\s*(read\s|input\()', sc, re.MULTILINE):
fail(f"scripts/{fname}: may use interactive input (read/input detected)")
else:
ok(f"scripts/{fname}: no interactive prompts detected")
# Executable bit
if os.access(fpath, os.X_OK):
ok(f"scripts/{fname}: is executable")
else:
fail(f"scripts/{fname}: not executable — run: chmod +x {fpath}")
# Summary
print()
if not failed:
print("All checks passed.")
sys.exit(0)
else:
print("One or more checks failed.")
sys.exit(1)
PYTHON

View File

@@ -1,29 +0,0 @@
# tests/
Test files for scripts bundled with this skill.
## Dependencies
Tests require [bats-support](https://github.com/bats-core/bats-support) and
[bats-assert](https://github.com/bats-core/bats-assert). The test files load
helpers from the repo root's `tests/test_helper/`.
From the repo root:
```bash
git clone https://github.com/bats-core/bats-support tests/test_helper/bats-support
git clone https://github.com/bats-core/bats-assert tests/test_helper/bats-assert
```
Run all tests for this skill (from the repo root):
```bash
bats plugins/kyberforge/skills/skill-audit/tests/
```
## Files
| File | Purpose |
|------|---------|
| `validate.bats` | Bats test suite for `scripts/validate.sh` |
| `validate-provenance.bats` | Bats test suite for `scripts/validate-provenance.sh` |

View File

@@ -1,514 +0,0 @@
#!/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)/validate-provenance.sh"
TMPDIR="$(mktemp -d)"
# Helper: create a minimal skill directory with no sources.md and no source_keys
make_clean_skill() {
local dir="$1"
local name
name="$(basename "$dir")"
mkdir -p "$dir"
cat > "$dir/SKILL.md" <<EOF
---
name: $name
description: A valid skill description.
---
## Step 1
Do the thing.
EOF
}
# Helper: create a skill with source_keys in SKILL.md
make_skill_with_source_keys() {
local dir="$1"
local name
name="$(basename "$dir")"
mkdir -p "$dir"
cat > "$dir/SKILL.md" <<EOF
---
name: $name
description: A valid skill description.
metadata:
source_keys:
- my-source
---
## Step 1
Do the thing.
EOF
}
# Helper: create a valid sources.md with one entry
make_sources_md() {
local dir="$1"
local slug="${2:-my-source}"
local contrib="${3:-SKILL.md}"
local research="${4:-(none)}"
mkdir -p "$dir/references"
cat > "$dir/references/sources.md" <<EOF
# Sources
## ${slug}
- **URL:** https://example.com/${slug}
- **Description:** A test source.
- **Contributing files:** ${contrib}
- **Research doc:** ${research}
- **Status:** \`extracted\`
EOF
}
}
teardown() {
rm -rf "$TMPDIR"
}
# ---------------------------------------------------------------------------
# Cycle 1 — --help
# ---------------------------------------------------------------------------
@test "--help exits 0" {
run bash "$SCRIPT" --help
assert_success
assert_output --partial "Usage:"
}
# ---------------------------------------------------------------------------
# Cycle 2 — Early exit: no sources.md, no source_keys → exit 0, no output
# ---------------------------------------------------------------------------
@test "clean pass: no sources.md and no source_keys anywhere → exit 0, no output" {
local skill="$TMPDIR/my-skill"
make_clean_skill "$skill"
run bash "$SCRIPT" "$skill"
assert_success
assert_output ""
}
# ---------------------------------------------------------------------------
# Cycle 3 — Check 0: source_keys present but no sources.md → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: source_keys in SKILL.md but sources.md absent" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
run bash "$SCRIPT" "$skill"
assert_failure
assert_output --partial "FAIL"
}
# ---------------------------------------------------------------------------
# Cycle 4 — Check 1: FILL IN: placeholder in sources.md → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: FILL IN: placeholder in sources.md" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
mkdir -p "$skill/references"
cat > "$skill/references/sources.md" <<EOF
# Sources
## my-source
- **URL:** FILL IN: add url
- **Description:** A test source.
- **Contributing files:** SKILL.md
- **Research doc:** (none)
- **Status:** \`extracted\`
EOF
run bash "$SCRIPT" "$skill"
assert_failure
assert_output --partial "FAIL"
}
@test "FILL IN: inside backticks in sources.md does not fail" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
make_sources_md "$skill"
echo "Use \`FILL IN: value\` as example." >> "$skill/references/sources.md"
run bash "$SCRIPT" "$skill"
assert_success
}
# ---------------------------------------------------------------------------
# Cycle 5 — Check 2: source_keys slug missing from sources.md → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: source_keys slug in SKILL.md not present as H2 in sources.md" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
mkdir -p "$skill/references"
cat > "$skill/references/sources.md" <<EOF
# Sources
## different-source
- **URL:** https://example.com/different-source
- **Description:** A different source.
- **Contributing files:** SKILL.md
- **Research doc:** (none)
- **Status:** \`extracted\`
EOF
run bash "$SCRIPT" "$skill"
assert_failure
assert_output --partial "FAIL"
}
# ---------------------------------------------------------------------------
# Cycle 6 — Check 4: Contributing file path doesn't exist → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: Contributing file listed in sources.md does not exist" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
make_sources_md "$skill" "my-source" "references/nonexistent.md"
run bash "$SCRIPT" "$skill"
assert_failure
assert_output --partial "FAIL"
}
@test "pass: (none) in Contributing files is skipped" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
make_sources_md "$skill" "my-source" "(none — not used directly)"
run bash "$SCRIPT" "$skill"
assert_success
}
# ---------------------------------------------------------------------------
# Cycle 7 — Check 6: Research doc field missing → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: Research doc field missing from sources.md entry" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
mkdir -p "$skill/references"
cat > "$skill/references/sources.md" <<EOF
# Sources
## my-source
- **URL:** https://example.com/my-source
- **Description:** A test source.
- **Contributing files:** SKILL.md
- **Status:** \`extracted\`
EOF
run bash "$SCRIPT" "$skill"
assert_failure
assert_output --partial "FAIL"
}
@test "FAIL: Research doc field is FILL IN: placeholder" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
mkdir -p "$skill/references"
cat > "$skill/references/sources.md" <<EOF
# Sources
## my-source
- **URL:** https://example.com/my-source
- **Description:** A test source.
- **Contributing files:** SKILL.md
- **Research doc:** FILL IN: path to research doc
- **Status:** \`extracted\`
EOF
run bash "$SCRIPT" "$skill"
assert_failure
assert_output --partial "FAIL"
}
# ---------------------------------------------------------------------------
# Cycle 8 — Check 5: Bidirectional mismatch → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: Contributing file exists but does not list parent slug in source_keys" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
make_sources_md "$skill" "my-source" "SKILL.md"
# SKILL.md has source_keys: my-source, but let's change it to NOT have my-source
cat > "$skill/SKILL.md" <<EOF
---
name: my-skill
description: A valid skill description.
metadata:
source_keys:
- other-source
---
## Step 1
Do the thing.
EOF
mkdir -p "$skill/references"
cat > "$skill/references/sources.md" <<EOF
# Sources
## other-source
- **URL:** https://example.com/other-source
- **Description:** A test source.
- **Contributing files:** SKILL.md
- **Research doc:** (none)
- **Status:** \`extracted\`
EOF
# Now add my-source that references SKILL.md but SKILL.md doesn't back-reference it
cat >> "$skill/references/sources.md" <<EOF
## my-source
- **URL:** https://example.com/my-source
- **Description:** Another source.
- **Contributing files:** SKILL.md
- **Research doc:** (none)
- **Status:** \`extracted\`
EOF
run bash "$SCRIPT" "$skill"
assert_failure
assert_output --partial "FAIL"
}
# ---------------------------------------------------------------------------
# Cycle 9 — Check 3: references/*.md with no source_keys → INFO (exit 0)
# ---------------------------------------------------------------------------
@test "INFO: references doc with no source_keys frontmatter emits INFO but exits 0" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
make_sources_md "$skill"
mkdir -p "$skill/references"
cat > "$skill/references/extra.md" <<EOF
# Extra Reference
No frontmatter here.
EOF
run bash "$SCRIPT" "$skill"
assert_success
assert_output --partial "INFO"
}
@test "pass: references doc with source_keys all matching sources.md exits 0" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
make_sources_md "$skill"
mkdir -p "$skill/references"
cat > "$skill/references/extra.md" <<EOF
---
source_keys:
- my-source
---
# Extra Reference
Content here.
EOF
run bash "$SCRIPT" "$skill"
assert_success
}
# ---------------------------------------------------------------------------
# Cycle 10 — Clean full pass: valid sources.md, all source_keys match, files exist
# ---------------------------------------------------------------------------
@test "clean full pass: all checks satisfied" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
make_sources_md "$skill"
run bash "$SCRIPT" "$skill"
assert_success
}
# ---------------------------------------------------------------------------
# Cycle 11 — Check 7: Upstream forward: slug in sources.md not in research doc → INFO
# ---------------------------------------------------------------------------
@test "INFO: slug in sources.md not found in research doc → INFO, exits 0" {
local skill="$TMPDIR/my-skill"
make_skill_with_source_keys "$skill"
# Create a research doc that does NOT have the slug
local research_dir="$TMPDIR/research"
mkdir -p "$research_dir"
cat > "$research_dir/my-research.md" <<EOF
# Research
## different-slug
- **Contributing files:** (none)
- **Status:** \`extracted\`
EOF
# Use a path relative to repo root — we'll place research doc inside TMPDIR
# and reference it as absolute for test purposes.
# The script finds repo root by walking up from skill-dir until .git is found.
# Since TMPDIR won't have .git, we simulate a repo structure.
local fake_repo="$TMPDIR/fakerepo"
mkdir -p "$fake_repo"
touch "$fake_repo/.git" # fake .git marker
local skill2="$fake_repo/my-skill"
mkdir -p "$skill2"
cat > "$skill2/SKILL.md" <<EOF
---
name: my-skill
description: A valid skill description.
metadata:
source_keys:
- my-source
---
## Step 1
Do the thing.
EOF
mkdir -p "$skill2/references"
mkdir -p "$fake_repo/docs/research"
cat > "$fake_repo/docs/research/my-research.md" <<EOF
# Research
## different-slug
- **Contributing files:** (none)
- **Status:** \`extracted\`
EOF
cat > "$skill2/references/sources.md" <<EOF
# Sources
## my-source
- **URL:** https://example.com/my-source
- **Description:** A test source.
- **Contributing files:** SKILL.md
- **Research doc:** docs/research/my-research.md
- **Status:** \`extracted\`
EOF
run bash "$SCRIPT" "$skill2"
assert_success
assert_output --partial "INFO"
}
# ---------------------------------------------------------------------------
# Cycle 12 — Check 8: Upstream reverse: extracted slug in research doc not in sources.md → FAIL
# ---------------------------------------------------------------------------
@test "FAIL: extracted non-(none) slug in research doc missing from skill sources.md" {
local fake_repo="$TMPDIR/fakerepo"
mkdir -p "$fake_repo"
touch "$fake_repo/.git"
local skill="$fake_repo/my-skill"
mkdir -p "$skill"
cat > "$skill/SKILL.md" <<EOF
---
name: my-skill
description: A valid skill description.
metadata:
source_keys:
- my-source
---
## Step 1
Do the thing.
EOF
mkdir -p "$skill/references"
mkdir -p "$fake_repo/docs/research"
# Research doc has my-source (extracted, with a contributing file) AND extra-source (also extracted)
cat > "$fake_repo/docs/research/my-research.md" <<EOF
# Research
## my-source
- **Contributing files:** some-skill/SKILL.md
- **Status:** \`extracted\`
## extra-source
- **Contributing files:** some-skill/references/extra.md
- **Status:** \`extracted\`
EOF
cat > "$skill/references/sources.md" <<EOF
# Sources
## my-source
- **URL:** https://example.com/my-source
- **Description:** A test source.
- **Contributing files:** SKILL.md
- **Research doc:** docs/research/my-research.md
- **Status:** \`extracted\`
EOF
run bash "$SCRIPT" "$skill"
assert_failure
assert_output --partial "FAIL"
}
@test "pass: extracted slug in research doc with (none) contributing files is not required in sources.md" {
local fake_repo="$TMPDIR/fakerepo"
mkdir -p "$fake_repo"
touch "$fake_repo/.git"
local skill="$fake_repo/my-skill"
mkdir -p "$skill"
cat > "$skill/SKILL.md" <<EOF
---
name: my-skill
description: A valid skill description.
metadata:
source_keys:
- my-source
---
## Step 1
Do the thing.
EOF
mkdir -p "$skill/references"
mkdir -p "$fake_repo/docs/research"
cat > "$fake_repo/docs/research/my-research.md" <<EOF
# Research
## my-source
- **Contributing files:** some-skill/SKILL.md
- **Status:** \`extracted\`
## extra-source
- **Contributing files:** (none — not relevant)
- **Status:** \`extracted\`
EOF
cat > "$skill/references/sources.md" <<EOF
# Sources
## my-source
- **URL:** https://example.com/my-source
- **Description:** A test source.
- **Contributing files:** SKILL.md
- **Research doc:** docs/research/my-research.md
- **Status:** \`extracted\`
EOF
run bash "$SCRIPT" "$skill"
assert_success
}

View File

@@ -1,198 +0,0 @@
#!/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)/validate.sh"
TMPDIR="$(mktemp -d)"
# Helper: create a minimal valid skill directory
make_valid_skill() {
local dir="$1"
local name
name="$(basename "$dir")"
mkdir -p "$dir/scripts"
cat > "$dir/SKILL.md" <<EOF
---
name: $name
description: A valid skill description that is well within the limit.
---
## Step 1
Do the thing.
EOF
}
}
teardown() {
rm -rf "$TMPDIR"
}
# ---------------------------------------------------------------------------
# Passing cases
# ---------------------------------------------------------------------------
@test "passes on a valid minimal skill" {
local skill="$TMPDIR/my-skill"
make_valid_skill "$skill"
run bash "$SCRIPT" "$skill"
assert_success
}
@test "--help exits 0" {
run bash "$SCRIPT" --help
assert_success
assert_output --partial "Usage:"
}
@test "passes when scripts/ directory is absent" {
local skill="$TMPDIR/my-skill"
make_valid_skill "$skill"
rmdir "$skill/scripts"
run bash "$SCRIPT" "$skill"
assert_success
}
@test "FILL IN: inside backticks does not fail" {
local skill="$TMPDIR/my-skill"
make_valid_skill "$skill"
echo "Use \`FILL IN: value\` as an example." >> "$skill/SKILL.md"
run bash "$SCRIPT" "$skill"
assert_success
}
@test "passes at exactly 1024-char description" {
local skill="$TMPDIR/my-skill"
local name
name="$(basename "$skill")"
mkdir -p "$skill"
local desc
desc="$(python3 -c "print('x' * 1024)")"
cat > "$skill/SKILL.md" <<EOF
---
name: $name
description: $desc
---
## Step 1
Do the thing.
EOF
run bash "$SCRIPT" "$skill"
assert_success
}
@test "passes at exactly 500 lines" {
local skill="$TMPDIR/my-skill"
make_valid_skill "$skill"
local current
current="$(wc -l < "$skill/SKILL.md")"
local needed=$(( 500 - current ))
python3 -c "print('\n' * $needed, end='')" >> "$skill/SKILL.md"
run bash "$SCRIPT" "$skill"
assert_success
}
# ---------------------------------------------------------------------------
# Failing cases
# ---------------------------------------------------------------------------
@test "fails when SKILL.md is missing" {
local skill="$TMPDIR/my-skill"
mkdir -p "$skill"
run bash "$SCRIPT" "$skill"
assert_failure
}
@test "fails when name does not match directory" {
local skill="$TMPDIR/my-skill"
make_valid_skill "$skill"
sed -i 's/^name: .*/name: wrong-name/' "$skill/SKILL.md"
run bash "$SCRIPT" "$skill"
assert_failure
}
@test "fails when description exceeds 1024 chars" {
local skill="$TMPDIR/my-skill"
local name
name="$(basename "$skill")"
mkdir -p "$skill"
local desc
desc="$(python3 -c "print('x' * 1025)")"
cat > "$skill/SKILL.md" <<EOF
---
name: $name
description: $desc
---
## Step 1
Do the thing.
EOF
run bash "$SCRIPT" "$skill"
assert_failure
}
@test "fails when SKILL.md exceeds 500 lines" {
local skill="$TMPDIR/my-skill"
make_valid_skill "$skill"
python3 -c "print('\n' * 500)" >> "$skill/SKILL.md"
run bash "$SCRIPT" "$skill"
assert_failure
}
@test "fails when body contains unfilled FILL IN: placeholder" {
local skill="$TMPDIR/my-skill"
make_valid_skill "$skill"
echo "FILL IN: replace this" >> "$skill/SKILL.md"
run bash "$SCRIPT" "$skill"
assert_failure
}
@test "fails when a script is not executable" {
local skill="$TMPDIR/my-skill"
make_valid_skill "$skill"
echo "#!/usr/bin/env bash" > "$skill/scripts/helper.sh"
chmod -x "$skill/scripts/helper.sh"
run bash "$SCRIPT" "$skill"
assert_failure
}
@test "fails when a script has an interactive prompt" {
local skill="$TMPDIR/my-skill"
make_valid_skill "$skill"
printf '#!/usr/bin/env bash\nread -p "Enter value: " VAL\n' > "$skill/scripts/helper.sh"
chmod +x "$skill/scripts/helper.sh"
run bash "$SCRIPT" "$skill"
assert_failure
}
@test "fails when name contains consecutive hyphens" {
local skill="$TMPDIR/my--skill"
make_valid_skill "$skill"
run bash "$SCRIPT" "$skill"
assert_failure
}
@test "fails when name has a leading hyphen" {
local skill="$TMPDIR/-my-skill"
make_valid_skill "$skill"
run bash "$SCRIPT" "$skill"
assert_failure
}
@test "fails when no frontmatter block is present" {
local skill="$TMPDIR/my-skill"
mkdir -p "$skill"
echo "Just some content with no frontmatter." > "$skill/SKILL.md"
run bash "$SCRIPT" "$skill"
assert_failure
}
@test "fails when no arguments are given" {
run bash "$SCRIPT"
assert_failure
}

View File

@@ -1,54 +0,0 @@
# skill-author
Author and refine skills conforming to the [agentskills.io](https://agentskills.io) specification — create new skills from scratch or apply improvement signals to existing ones.
## What it does
Routes to one of two flows based on context: if no skill directory exists at the target path, it scaffolds the directory from annotated templates, fills in `SKILL.md` and supporting files, and validates the result. If an existing skill directory and improvement signals are both present, it groups those signals by root cause and applies targeted edits, then re-validates. In both flows, bumps the skill's `metadata.version` when present (minor for create, patch for improve).
## Before you start
- Run `/grill-me` to resolve design decisions before creating a new skill
- Collect domain research, examples, and constraints
- Know the skill name (kebab-case) and destination path
## Placement
`scripts/new-skill.sh` resolves the mode automatically by walking up from the given path — see `SKILL.md` Step 1 for the full algorithm.
| 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
```
/skill-author
```
## Files
| File | Purpose |
|------|---------|
| `README.md` | Human-readable overview of the skill and its files |
| `SKILL.md` | Skill instructions for agents |
| `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 |
| `assets/templates/README.md` | Annotated README template for the new skill |
| `assets/templates/scripts/README.md` | Placeholder for bundled scripts |
| `assets/templates/references/README.md` | Placeholder for reference docs |
| `assets/templates/references/sources.md` | Sources provenance template for new skills |
| `assets/templates/assets/README.md` | Placeholder for static assets |
| `assets/templates/tests/README.md` | Placeholder for test files |
| `tests/new-skill.bats` | Bats test suite for `scripts/new-skill.sh` |
| `tests/README.md` | Setup instructions for bats-support and bats-assert test dependencies |
## Spec reference
[agentskills.io specification](https://agentskills.io/specification.md)

View File

@@ -1,306 +0,0 @@
---
name: skill-author
description: >
Use when the user wants to create a new skill from scratch ("write a skill
for X", "build a skill that does Y", "create a SKILL.md for Z") or improve
an existing one ("improve this skill", "fix based on feedback", "apply these
audit findings", "update based on grill output"). Also use when the user provides inline feedback
about a skill's behavior and wants it applied, or when a grill session, eval
run, or audit has produced findings the user wants acted on — even if they
don't say "improve" explicitly. Do not use for read-only review — use
/skill-audit instead. Do not use to author agent definition files.
allowed-tools: Bash Read Write Edit
metadata:
category: factory
source_keys:
- agentskills-home
- agentskills-spec
- agentskills-best-practices
- agentskills-optimizing-descriptions
- agentskills-evaluating-skills
- agentskills-using-scripts
- agentskills-quickstart
---
## Gotchas
- Patching per symptom is the default failure mode. Three eval failures may all trace to one missing instruction — always identify the root cause before editing.
- Do not create new scripts unless a signal explicitly calls for it. Writing scripts from scratch requires transcript analysis that is out of scope here; flag the opportunity as a suggestion instead.
- Never spawn a subagent to audit or recheck your own work during an authoring pass. Run `/skill-audit` yourself, inline, in the same context as the edits you just made. A *separate* independent recheck via a clean-context subagent is the `/forge` skill's outer-loop responsibility exclusively — delegating it inward here duplicates that layer and introduces a race: a stray self-spawned subagent can have its worktree torn down by concurrent cleanup, destroying an uncommitted draft before it was ever safe.
## Route
Determine which flow to follow before touching the filesystem:
- **No skill directory at the target path** → follow **Creating a new skill**
- **Directory exists + at least one improvement signal present** → follow **Improving an existing skill**
- **Directory exists + no signals present** → ask: "No improvement signals found. Did you mean to create a new skill, or do you have feedback to apply?"
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
Run `/grill-me` on the skill's design and research the target domain first.
Share those outputs in this conversation: grill context, research docs, examples, constraints.
Design for one coherent user intent — skills too narrow force multiple loads per task; too broad are hard to activate precisely.
**Before touching the filesystem, verify you have:**
- [ ] 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 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 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 a path inside or at the target:
```bash
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/
```
The script prints which mode it used and where the skill landed — read its output.
In package mode, read `references/deployment-modes.md` before adding any file references to SKILL.md.
### Step 2 — Update `apm.yml` includes (package mode only)
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**
**`name`** — already set by the scaffold script. Must exactly match the directory name. Format: 1–64 characters, lowercase letters/numbers/hyphens only, no leading, trailing, or consecutive hyphens (`--`).
**`description`** — carries the entire triggering burden. Rules:
- Imperative: "Use when..." not "This skill..."
- Focus on user intent, not implementation — describe what the user is trying to achieve, not the skill's internal mechanics
- Specific about capabilities ("parses and validates OpenAPI specs", not "helps with APIs")
- Include indirect triggers: "even if the user doesn't mention X explicitly"
- Add "Do not use when..." only if a near-miss skill exists that could steal activations
- Hard limit: 1024 characters — count before finalizing
**Optional fields** — uncomment and fill in or remove entirely:
- `license` — include when distributing the skill externally
- `compatibility` — include if the skill requires specific tools, runtimes, or network access (max 500 characters)
- `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 6. Agents that fill in source_keys late tend to omit it entirely. Example:
```yaml
metadata:
source_keys:
- my-source-slug
- 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 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**
Rename the placeholder section heading to one that fits the skill's structure — `## Step 1`, `## Workflow`, `## Instructions`, etc.
Ask of every sentence: "Would the agent get this wrong without it?" Cut anything that answers "no."
**Include:**
- Non-obvious sequences or ordering constraints — the agent may skip or reorder steps without this
- Domain conventions the agent cannot infer from general knowledge — this is the core value a skill adds
- One default per decision point, plus one escape hatch — never a menu; menus cause the agent to pause or pick arbitrarily
- Gotchas — facts that defy reasonable assumptions; the agent will get these wrong every time without them
**Exclude:**
- Concepts the agent already knows (what JSON is, how HTTP works) — adds tokens without changing behavior
- Exhaustive option lists — pick a default; the agent doesn't benefit from choosing
- Steps the agent handles independently — over-specifying leads agents to follow unproductive paths
- Restatements of the description — it's already in context; repeating it wastes the token budget
**Patterns**
**Gotchas** — highest value; place near the top:
````markdown
## Gotchas
- <Fact that defies a reasonable assumption>
- <Non-obvious naming discrepancy or hidden constraint>
````
**Default with escape hatch** (not a menu):
````markdown
Use <X> for <task>. For <edge case>, use <Y> instead.
````
**Prescriptive sequence** (when order is critical or fragile):
````markdown
Run exactly:
```bash
<command>
```
Do not modify flags.
````
**Checklist** (multi-step workflows):
````markdown
- [ ] Step 1: ...
- [ ] Step 2: ...
````
**Conditional reference** (progressive disclosure — load only when needed):
````markdown
If <condition>, read `references/<file>.md`.
````
**Output format template** (when the skill produces structured output):
````markdown
Output format:
```
<field>: <value>
<field>: <value>
```
````
For longer templates, place in `assets/<name>.md` and reference conditionally.
**Size budget**
Keep `SKILL.md` under 500 lines; 5,000 tokens is the recommended body budget. When approaching the limit:
- Move reference material to `references/<topic>.md` and load it conditionally
- Bundle repeated executable logic into `scripts/` rather than reinventing each run
### 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.
If adding a script, read `references/scripts.md` first — it covers the full contract: structured output, pinned versions, self-contained deps, idempotency, exit codes, dry-run, error messages, and output size limits.
If no scripts are needed, delete `scripts/README.md` and the `scripts/` directory.
### 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`.
Keep reference chains one level deep — a reference file that references another reference file is rarely loaded correctly.
**`assets/`** — static resources: templates, schemas, lookup tables.
Reference by relative path from SKILL.md.
**`tests/`** — test files for scripts in `scripts/`. Use when scripts are complex
enough to break silently. Test infrastructure (`.bats`, `*_test.*`) belongs here,
not in `scripts/`. See `tests/README.md` for setup instructions.
If not needed, delete the placeholder READMEs and their directories.
### Step 6 — Populate or delete `references/sources.md`
If a research `sources.md` is present in the conversation context:
1. Read it and filter to entries with `` `extracted` `` status only.
2. For each entry, determine which skill files it contributed to (SKILL.md and any files in references/ that drew from it). Update `Contributing files` accordingly — list skill files, not research topic files.
3. Write the updated content to `references/sources.md`. For each entry, include `- **Research doc:** <path>` where `<path>` is the relative path from the repo root to the plugin-level research sources file this entry was drawn from (e.g. `plugins/myplugin/docs/research/docs/<topic>/sources.md`). This field is required on every entry — it makes the provenance chain explicit and is validated by `/skill-audit`.
4. Add `source_keys` to the frontmatter of `SKILL.md` (under `metadata`) listing the slugs of sources that informed it.
5. For each file in `references/` that was informed by research sources, add `source_keys` frontmatter (same format as research topic files) listing the relevant slugs.
If no research `sources.md` is in context, delete `references/sources.md`.
### Step 7 — Validate and close
Before running the audit, confirm:
- [ ] Skill name matches the directory name exactly
- [ ] `description` field is present and non-empty
- [ ] Body has at least one non-empty section
- [ ] No `FILL IN:` placeholders remain in any file
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.
If the skill is versioned (`metadata.version`), set it to the next **minor** version (e.g. `0.2.0` → `0.3.0`). New skills without a prior version start at `0.1.0`.
**Commit verification.** Capture `git log --oneline -1` before Step 1 and keep it. Once the audit is clean, run `git add` and `git commit` for the new skill files — do not stop at staging. Then run `git log --oneline -1` again and confirm the hash changed from the one you captured at the start. A non-empty `git diff --stat` is not sufficient proof of completion: staged-but-uncommitted work isn't part of any commit and can be silently lost if the working tree is cleaned up before a commit lands. Only report the skill as done once the hash has actually changed.
## Improving an existing skill
### Step 1 — Verify inputs
Confirm the skill directory path exists and that at least one improvement signal is present in the conversation or a referenced file.
If the skill dir is missing, ask for it. If no signals are present, stop: "This skill applies existing signals to a skill. For a blind review without signals, use `/skill-audit` instead."
Capture `git log --oneline -1` now, before making any edits — Step 5 needs it to verify a real commit landed.
Signals can come from anywhere in the conversation or referenced files:
- Grill session output (most common predecessor in the factory sequence)
- `/skill-audit` findings (PASS/FAIL/SUGGESTION punch list)
- Human feedback (feedback.json, inline in conversation, PR or issue comments)
- Session context describing what went wrong
Also verify the `name` field in frontmatter matches the skill's directory name exactly.
### Step 2 — Gather and group signals
Read the current skill files (SKILL.md and any files in scripts/, references/, assets/, tests/). Then collect all signals from the conversation and any file paths the user has referenced.
Group signals by **root cause**, not symptom. Ask: "What single gap in the skill causes this cluster of failures?" One root cause → one fix. Do not make a separate edit for each symptom.
```text
Example:
- Session context: output format is wrong on every run
- Audit finding: no output template defined
- User feedback: "I always have to ask it to format the output"
→ Root cause: SKILL.md has no output format specification → one fix: add an output template
```
### Step 3 — Announce planned changes
Before editing, state:
- Which root causes were identified and what evidence supports each
- Which files will be changed and what will change in each
Then proceed — edits are reversible via git, no approval checkpoint needed.
### Step 4 — Apply changes
Edit any file in the skill directory that the signals point to: SKILL.md, scripts/, references/, assets/, tests/, README.md.
**Generalize, don't patch.** Find the underlying gap, not the specific example that failed. A fix scoped only to the test cases you've seen will overfit and perform worse on new inputs.
**Keep it lean.** Remove instructions that aren't pulling their weight. For every sentence you add, ask: "Would the agent get this wrong without it?" A shorter, focused skill consistently outperforms an exhaustive one.
**Explain the why.** Reasoning-based instructions outperform rigid directives. If you find yourself writing a rule in all caps (ALWAYS/NEVER), reframe it: explain why the behavior matters so the agent can apply judgment in edge cases.
If a signal points to a script or reference file, edit that file directly rather than adding a workaround in SKILL.md.
### Step 5 — Validate and close
Before running the audit, confirm:
- [ ] Skill name still matches the directory name
- [ ] No `FILL IN:` placeholders were introduced
- [ ] No previously-passing audit checks were broken by the edits
Run `/skill-audit` on the skill directory. Resolve any FAIL findings before considering the improvement complete.
If the skill is versioned (`metadata.version`), bump the **patch** version (e.g. `0.1.0` → `0.1.1`).
**Commit verification.** Capture `git log --oneline -1` at the start of Step 1 and keep it. Once the audit is clean, run `git add` and `git commit` for the changed files — do not stop at staging. Then run `git log --oneline -1` again and confirm the hash changed from the one you captured at the start. A non-empty `git diff --stat` is not sufficient proof of completion: staged-but-uncommitted work isn't part of any commit and can be silently lost if the working tree is cleaned up before a commit lands. Only report the improvement as done once the hash has actually changed.

View File

@@ -1,51 +0,0 @@
# SKILL_NAME
<!-- FILL IN: One sentence describing what this skill does. -->
## What it does
<!-- FILL IN: 2–4 sentences. What task does this skill handle?
What does the agent produce or accomplish when it runs? -->
## Before you start
<!-- FILL IN: List any prerequisites the user should have ready.
Examples: research docs, a grill session, specific input files, credentials.
Delete this section if the skill has no meaningful prerequisites. -->
## Usage
```
/SKILL_NAME
```
<!-- FILL IN: Add any required or common arguments.
If the skill takes no arguments, delete the code block above and just keep the slash command. -->
<!-- OPTIONAL: Manual (human) workflow — include if the skill bundles scripts a human can run directly.
**Manual workflow:**
```bash
# FILL IN: step-by-step commands
```
-->
## Files
<!-- FILL IN: List each file individually. Remove rows for directories you deleted.
Replace the example rows below with your actual files. -->
| File | Purpose |
|------|---------|
| `SKILL.md` | Skill instructions for agents |
| `scripts/your-script.sh` | FILL IN: what this script does |
| `references/your-doc.md` | FILL IN: what this reference covers |
| `assets/your-asset.json` | FILL IN: what this asset is |
| `tests/your-test.bats` | FILL IN: what this test covers |
<!-- OPTIONAL: Spec reference — include if this skill implements or follows an external standard.
## Spec reference
[FILL IN: Spec name](FILL IN: URL)
-->

View File

@@ -1,105 +0,0 @@
---
# SKILL.md — agentskills.io skill definition
# Fill in all FILL IN: placeholders. Remove comment blocks that don't apply.
name: SKILL_NAME
# Required. Must exactly match the parent directory name.
# Valid characters: lowercase letters, numbers, hyphens.
# Invalid: uppercase, leading/trailing/consecutive hyphens.
# Max length: 64 characters.
# Examples: my-tool, data-analyzer, pdf-processor
description: >
FILL IN: What does this skill do? State capabilities specifically
(e.g. "parses and validates OpenAPI specs", not "helps with APIs").
Use when FILL IN: when should an agent activate this skill?
Include indirect triggers: even if the user doesn't mention X explicitly.
Do not use when FILL IN: near-miss exclusions — remove this line if none apply.
# license: MIT
# Optional. License name (e.g. MIT, Apache-2.0) or relative path to a bundled
# license file. Include when distributing this skill. Omit for private/internal use.
# compatibility: Requires python3 >= 3.10 and uv
# Optional. 1–500 characters. State tool requirements, runtime versions,
# and network access needs. Omit for skills with no special environment requirements.
# metadata:
# author: your-name
# version: "1.0"
# category: general
# source_keys:
# - source-slug-one
# - source-slug-two
# Optional. Arbitrary key-value map. Common keys: author, version, category.
# source_keys: populated when built from /research output. Lists slugs from references/sources.md.
# Also add source_keys to each references/*.md file that was informed by research.
# allowed-tools: Bash Read Write
# Optional (experimental — support varies by client).
# Space-separated list of pre-approved tools.
# Use when tool usage is known and bounded, to reduce permission prompts.
---
<!-- ============================================================
SKILL BODY
Include only what the agent lacks:
- Domain conventions the agent cannot infer from general knowledge
- Non-obvious sequences or ordering constraints
- One default per decision point + one escape hatch (never a menu)
- Gotchas — facts that defy reasonable assumptions
Omit:
- Concepts the agent already knows
- Exhaustive option lists
- Steps the agent handles independently
- Restatements of the description
Size budget: under 500 lines / 5000 tokens.
Move reference material to references/ and load it conditionally.
Bundle repeated executable logic into scripts/.
Delete this comment block before shipping.
============================================================ -->
<!-- OPTIONAL: Gotchas section — highest-value content. Place near the top.
Add facts that defy reasonable assumptions or non-obvious constraints.
## Gotchas
- FILL IN: fact that defies a reasonable assumption
- FILL IN: non-obvious naming discrepancy or hidden constraint
-->
<!-- OPTIONAL: Multi-step workflow checklist.
## Workflow
- [ ] Step 1: FILL IN
- [ ] Step 2: FILL IN
- [ ] Step 3: FILL IN
-->
<!-- OPTIONAL: Output format template — use when the agent must produce a specific format.
## Output format
Use this structure:
```markdown
# [FILL IN: Title]
## FILL IN: Section
FILL IN: what goes here
```
-->
<!-- OPTIONAL: Conditional reference — load documentation only when needed.
If FILL IN: condition, read `references/FILL IN: filename.md`.
-->
## FILL IN: <section-name (e.g. Step 1, Workflow, Instructions)>
FILL IN: Add your skill instructions here. Replace this section header and body with your skill content.

View File

@@ -1,29 +0,0 @@
# assets/
Static resources bundled with this skill: templates, schemas, lookup tables,
sample data, images.
## When to add an asset
Add a file here when the skill needs a static resource that:
- Would be tedious to reproduce in instructions (a full JSON schema, a CSV
lookup table, a binary template)
- Needs to be referenced by path rather than inlined in SKILL.md
## How to reference from SKILL.md
Use a relative path from the skill root:
```markdown
Use the schema at `assets/response-schema.json` to validate output.
```
Or instruct the agent to load it conditionally:
```markdown
If validating output format, use `assets/response-schema.json`.
```
## If no assets are needed
Delete this README and the `assets/` directory entirely.

View File

@@ -1,31 +0,0 @@
# references/
Additional documentation agents load on demand. Files here extend SKILL.md
without bloating its core context.
## When to add a reference file
Move content here when SKILL.md is approaching 500 lines, or when a topic
is only relevant in specific circumstances (error handling, edge cases,
domain-specific sub-procedures).
## How to reference from SKILL.md
Load conditionally — tell the agent exactly when to read each file:
```markdown
If the API returns a non-200 status, read `references/api-errors.md`.
```
Avoid generic "see references/ for details" — the agent loads context on
demand, so give it a precise trigger condition.
## File conventions
- One topic per file — focused files mean less unnecessary context loaded
- Kebab-case filenames (e.g. `api-errors.md`, `output-formats.md`)
- Keep files under 200 lines where possible
## If no reference files are needed
Delete this README and the `references/` directory entirely.

View File

@@ -1,14 +0,0 @@
# Sources
<!-- Populated at Step 5 of skill authoring, after all skill files are written.
For each research source with status `extracted`, record which skill files
it contributed to under Contributing files.
Delete this file if no research sources were provided as input. -->
## FILL IN: source-slug
- **URL:** FILL IN
- **Description:** FILL IN
- **Research doc:** FILL IN: path to the plugin-level research sources file this entry came from (e.g. plugins/myplugin/docs/research/docs/<topic>/sources.md), relative to repo root
- **Contributing files:** FILL IN: comma-separated list of skill files this source informed (e.g. SKILL.md, references/foo.md). Use `(none)` if the source was consulted but contributed no file content directly.
- **Status:** `extracted`

View File

@@ -1,47 +0,0 @@
# scripts/
Executable code bundled with this skill. Agents run scripts in this directory
to perform repeatable operations rather than reinventing the logic each run.
## When to add a script
Add a script when agents independently reinvent the same logic across runs —
building the same parser, chart, or validation routine from scratch each time.
Bundle it here once, tested and reliable.
## Script requirements (agentskills.io)
Scripts must be designed for non-interactive, agentic execution:
- **No interactive prompts** — agents run in non-interactive shells.
Accept all input via flags, env vars, or stdin. A script that blocks on
TTY input hangs indefinitely.
- **Expose `--help`** — this is how agents learn your script's interface.
Keep the output concise; it enters the agent's context window.
- **Structured output** — write data (JSON, CSV, TSV) to stdout.
Write progress, warnings, and diagnostics to stderr.
- **Idempotent** — prefer "create if not exists" over "create and fail on
duplicate". Agents may retry on failure.
- **Meaningful exit codes** — `0` for success, non-zero for failure.
Use distinct codes for different failure types; document them in `--help`.
- **Dry-run support** — add `--dry-run` for destructive operations.
## Self-contained scripts
Bundle dependencies inline so the agent can run the script with a single command.
Python (PEP 723 + uv):
```python
# /// script
# dependencies = ["requests>=2.31,<3"]
# requires-python = ">=3.11"
# ///
import requests
```
```bash
uv run scripts/my-script.py
```
## If no scripts are needed
Delete this README and the `scripts/` directory entirely.

View File

@@ -1,33 +0,0 @@
# tests/
Test files for scripts bundled with this skill.
## When to add tests
Add tests here when the skill has scripts in `scripts/` that are complex enough
to break silently — validators, parsers, generators, anything with branching
logic or edge cases. Test infrastructure (`.bats`, `*_test.*`, `test_*.sh`)
belongs here, not in `scripts/`.
## Dependencies
Tests require [bats-support](https://github.com/bats-core/bats-support) and
[bats-assert](https://github.com/bats-core/bats-assert). The test files load
helpers from the repo root's `tests/test_helper/`.
From the repo root:
```bash
git clone https://github.com/bats-core/bats-support tests/test_helper/bats-support
git clone https://github.com/bats-core/bats-assert tests/test_helper/bats-assert
```
Run all tests for this skill (from the repo root):
```bash
bats <destination-dir>/SKILL_NAME/tests/
```
## If no tests are needed
Delete this README and the `tests/` directory entirely.

View File

@@ -1,55 +0,0 @@
---
source_keys:
- agentskills-spec
---
# Deployment Modes
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)
When a plugin is installed, its directory is copied to a cache. Only the plugin's own files are copied. **Any path that leaves the skill directory breaks post-install:**
```
../other-skill/validate.sh # breaks
plugins/kyberforge/skills/other-skill/ # breaks
../../shared/utils.sh # 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.**
| Variable | Value |
|----------|-------|
| `${CLAUDE_PLUGIN_ROOT}` | Absolute path to the plugin's install directory. Changes on update. |
| `${CLAUDE_PLUGIN_DATA}` | Persistent directory that survives updates. Use for `node_modules`, generated state, caches. |
Use `${CLAUDE_PLUGIN_ROOT}` only in hook commands and `.mcp.json` configs — not in SKILL.md body text, since standalone deployments won't have it.
## Standalone mode
Deployed directly to `~/.agents/skills/<name>/`. No plugin context, no env vars injected. All file references must resolve within the skill directory. Skill invocations (e.g. `/skill-audit`) work if the called skill is also installed.
## Cross-tool portability
`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
If two skills in the same plugin need the same file, duplicate it into each skill's `assets/` or `scripts/`. Add a comment in both copies noting the mirror relationship so they stay in sync when the spec changes.

View File

@@ -1,89 +0,0 @@
---
source_keys:
- agentskills-using-scripts
---
# Scripts Reference
## Package runners (no install required)
When an existing package does what you need, use a runner directly in SKILL.md without writing a script file.
| Runner | Language | Notes |
|--------|----------|-------|
| `uvx package@version` | Python | Recommended. Aggressive caching via uv. |
| `pipx run 'package==version'` | Python | Broader OS availability. |
| `npx package@version` | Node.js | Ships with npm/Node.js. |
| `bunx package@version` | Node.js | Bun environments only. |
| `deno run npm:package@version` | TypeScript | Requires permission flags (`--allow-read`, etc.). |
| `go run golang.org/x/...@version` | Go | Built into Go toolchain. |
Always pin versions. Never use `pip install` or `npm install -g` at runtime — they are not idempotent and pollute the environment.
## Inline dependency patterns
Use these when the script requires packages but should remain a single portable file.
**Python (PEP 723 + uv):**
```python
# /// script
# dependencies = [
# "beautifulsoup4>=4.12,<5",
# ]
# requires-python = ">=3.12"
# ///
from bs4 import BeautifulSoup
```
```bash
uv run scripts/extract.py
```
**TypeScript (Deno):**
```typescript
#!/usr/bin/env -S deno run
import * as cheerio from "npm:cheerio@1.0.0";
```
```bash
deno run scripts/extract.ts
```
**TypeScript (Bun):**
```typescript
#!/usr/bin/env bun
import * as cheerio from "cheerio@1.0.0";
```
```bash
bun run scripts/extract.ts
```
**Ruby (bundler/inline):**
```ruby
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'nokogiri', '~> 1.16'
end
```
```bash
ruby scripts/extract.rb
```
## Script contract
Rules for all agentic scripts:
- **Self-contained** — bundle dependencies inline so the agent can run the script with a single command; do not require a separate install step
- **Structured output** — data (JSON, CSV) to stdout; diagnostics and progress to stderr
- **Idempotent** — "create if not exists"; agents may retry on failure
- **Input constraints** — validate inputs early; reject unknown or ambiguous values with a clear error rather than proceeding silently
- **Meaningful exit codes** — `0` success, non-zero failure; document in `--help`
- **Dry-run support** — add `--dry-run` for destructive operations; pair with `--confirm`/`--force` for operations that can't be undone
- **Error messages** — on failure, state what went wrong, what was expected, and what to try; vague errors leave agents unable to self-correct
## --help output
Keep `--help` output concise — it enters the agent's context window. Include: usage line, one-line description, options with defaults, exit codes. Omit prose explanations.
## Output size
Many harnesses truncate tool output beyond 10–30K characters. Default to a summary or a reasonable output limit. For scripts that can produce large output: support `--offset N` for pagination, or use `--output FILE` to write to disk and keep stdout clean.

View File

@@ -1,70 +0,0 @@
---
source_keys:
- agentskills-home
- agentskills-spec
- agentskills-best-practices
- agentskills-optimizing-descriptions
- agentskills-evaluating-skills
- agentskills-using-scripts
- agentskills-quickstart
---
# Sources
<!-- agentskills.io/llms.txt was used for initial source discovery and is not listed below; it contributed no skill file content directly. -->
## agentskills-home
- **URL:** https://agentskills.io/home.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Agent Skills overview — what it is, why it exists, progressive disclosure model, ecosystem of 35+ implementing tools
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## agentskills-spec
- **URL:** https://agentskills.io/specification.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Complete SKILL.md format specification — frontmatter fields, constraints, body content, optional directories, progressive disclosure levels, file references, validation
- **Contributing files:** SKILL.md, references/deployment-modes.md
- **Status:** `extracted`
## agentskills-best-practices
- **URL:** https://agentskills.io/skill-creation/best-practices.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Best practices for skill creators — starting from real expertise, spending context wisely, calibrating control, instruction patterns (gotchas, templates, checklists, validation loops)
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## agentskills-optimizing-descriptions
- **URL:** https://agentskills.io/skill-creation/optimizing-descriptions.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** How to systematically test and improve skill descriptions for triggering accuracy — eval queries, trigger rate testing, train/validation splits, optimization loop
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## agentskills-evaluating-skills
- **URL:** https://agentskills.io/skill-creation/evaluating-skills.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Eval-driven skill quality improvement — test case design, workspace structure, assertion writing, grading, benchmarking, human review, iteration loop
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## agentskills-using-scripts
- **URL:** https://agentskills.io/skill-creation/using-scripts.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Using scripts in skills — one-off commands, self-contained scripts with inline dependencies, designing scripts for agentic use (no interactive prompts, --help, structured output, idempotency)
- **Contributing files:** SKILL.md, references/scripts.md
- **Status:** `extracted`
## agentskills-quickstart
- **URL:** https://agentskills.io/skill-creation/quickstart.md
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
- **Description:** Step-by-step guide to creating a first skill (roll-dice example), how discovery/activation/execution work in practice
- **Contributing files:** SKILL.md
- **Status:** `extracted`

View File

@@ -1,189 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMPLATES_DIR="$SKILL_DIR/../assets/templates"
usage() {
cat <<EOF
Usage: new-skill.sh <skill-name> <path>
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.
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:
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 path, or templates not found
EOF
}
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage
exit 0
fi
if [[ $# -lt 2 ]]; then
echo "Error: skill-name and path are required." >&2
echo "" >&2
usage >&2
exit 1
fi
SKILL_NAME="$1"
TARGET_INPUT="$2"
# Validate skill name format
if ! echo "$SKILL_NAME" | grep -qE '^[a-z0-9]+(-[a-z0-9]+)*$'; then
echo "Error: skill-name must use lowercase letters, numbers, and hyphens only." >&2
echo " No leading, trailing, or consecutive hyphens." >&2
echo " Received: '$SKILL_NAME'" >&2
exit 1
fi
# Validate templates directory exists
if [[ ! -d "$TEMPLATES_DIR" ]]; then
echo "Error: templates directory not found at '$TEMPLATES_DIR'." >&2
echo " Run this script from its original location inside the skill-author skill." >&2
exit 1
fi
# Validate path exists
if [[ ! -d "$TARGET_INPUT" ]]; then
echo "Error: path '$TARGET_INPUT' does not exist." >&2
exit 1
fi
# True if apm_yml's top-level `type:` line names one of the four APM package
# types (instructions/skill/hybrid/prompts) — mirrors validate.sh's
# APM_TYPE_RE: an optional quote around the value must be closed by the
# *same* quote character (a mismatched or unterminated quote is rejected,
# not silently stripped), and the value must be followed by whitespace or
# end-of-line so `prompts-only` doesn't false-match on the `prompts` prefix.
# `|| [[ -n "$line" ]]` in the read condition also processes a final line
# that lacks a trailing newline, which `read` alone would otherwise skip.
# Identical to agent-author's new-agent.sh copy of this helper.
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
}
# ---------------------------------------------------------------------------
# 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 one space-separated line: mode, then the resolved root.
# ---------------------------------------------------------------------------
find_package_root() {
local current
current="$(cd "$1" && pwd)"
while true; do
if [[ -f "$current/apm.yml" ]]; then
if is_apm_package_manifest "$current/apm.yml"; then
echo "package $current"
return 0
fi
# apm.yml exists but has no type: field — marketplace-only manifest.
# Not a package match; keep walking up.
fi
# .git is a directory in a normal checkout but a file (`gitdir: ...`) in
# a git worktree — -e covers both.
if [[ -e "$current/.git" ]]; then
echo "no-package $current"
return 0
fi
local parent
parent="$(dirname "$current")"
if [[ "$parent" == "$current" ]]; then
echo "no-package $current"
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 single space-separated output line with a
# plain `read` instead (bash 3.2-safe). `read` consumes only one line, so
# mode and path must be on the same line: MODE first (never contains
# whitespace), PKG_ROOT last (safely absorbs a path containing spaces).
WALK_OUTPUT="$(find_package_root "$TARGET_INPUT")"
read -r MODE PKG_ROOT <<< "$WALK_OUTPUT"
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
echo "Scaffold already exists at '$TARGET' — nothing to do." >&2
exit 0
fi
mkdir -p "$(dirname "$TARGET")"
# Copy templates to destination
cp -r "$TEMPLATES_DIR" "$TARGET"
# Set skill name in templates
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"
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
echo " 2. Add scripts to scripts/ if needed (or delete the directory)" >&2
echo " 3. Add docs to references/ if needed (or delete the directory)" >&2
echo " 4. Add resources to assets/ if needed (or delete the directory)" >&2
echo " 5. Add tests to tests/ if the skill has scripts (or delete the directory)" >&2
echo " 6. Populate references/sources.md with research sources, or delete it" >&2
echo " 7. Validate: run /skill-audit on $TARGET" >&2

View File

@@ -1,28 +0,0 @@
# tests/
Test files for scripts bundled with this skill.
## Dependencies
Tests require [bats-support](https://github.com/bats-core/bats-support) and
[bats-assert](https://github.com/bats-core/bats-assert). The test files load
helpers from the repo root's `tests/test_helper/`.
From the repo root:
```bash
git clone https://github.com/bats-core/bats-support tests/test_helper/bats-support
git clone https://github.com/bats-core/bats-assert tests/test_helper/bats-assert
```
Run all tests for this skill (from the repo root):
```bash
bats plugins/kyberforge/skills/skill-author/tests/
```
## Files
| File | Purpose |
|------|---------|
| `new-skill.bats` | Bats test suite for `scripts/new-skill.sh` |

View File

@@ -1,208 +0,0 @@
#!/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-skill.sh"
DEST="$(mktemp -d)"
}
teardown() {
rm -rf "$DEST"
}
# ---------------------------------------------------------------------------
# Passing cases
# ---------------------------------------------------------------------------
@test "--help exits 0" {
run bash "$SCRIPT" --help
assert_success
assert_output --partial "Usage:"
}
@test "creates scaffold directory at destination" {
run bash "$SCRIPT" my-tool "$DEST"
assert_success
assert [ -d "$DEST/my-tool" ]
}
@test "scaffold contains SKILL.md" {
bash "$SCRIPT" my-tool "$DEST"
assert [ -f "$DEST/my-tool/SKILL.md" ]
}
@test "scaffold contains README.md" {
bash "$SCRIPT" my-tool "$DEST"
assert [ -f "$DEST/my-tool/README.md" ]
}
@test "scaffold contains scripts/, references/, assets/, tests/ directories" {
bash "$SCRIPT" my-tool "$DEST"
assert [ -d "$DEST/my-tool/scripts" ]
assert [ -d "$DEST/my-tool/references" ]
assert [ -d "$DEST/my-tool/assets" ]
assert [ -d "$DEST/my-tool/tests" ]
}
@test "substitutes skill name in SKILL.md" {
bash "$SCRIPT" my-tool "$DEST"
run grep "my-tool" "$DEST/my-tool/SKILL.md"
assert_success
}
@test "substitutes skill name in README.md" {
bash "$SCRIPT" my-tool "$DEST"
run grep "my-tool" "$DEST/my-tool/README.md"
assert_success
}
@test "substitutes skill name in tests/README.md" {
bash "$SCRIPT" my-tool "$DEST"
run grep "my-tool" "$DEST/my-tool/tests/README.md"
assert_success
}
@test "skill name with numbers is valid" {
run bash "$SCRIPT" my-tool-2 "$DEST"
assert_success
assert [ -d "$DEST/my-tool-2" ]
}
@test "next-steps output references /skill-audit not validate.sh" {
run bash "$SCRIPT" my-tool "$DEST"
assert_output --partial "/skill-audit"
refute_output --partial "validate.sh"
}
# ---------------------------------------------------------------------------
# Failing cases
# ---------------------------------------------------------------------------
@test "fails when no arguments given" {
run bash "$SCRIPT"
assert_failure
}
@test "fails when skill name contains uppercase" {
run bash "$SCRIPT" MyTool "$DEST"
assert_failure
}
@test "fails when skill name has consecutive hyphens" {
run bash "$SCRIPT" my--tool "$DEST"
assert_failure
}
@test "fails when skill name has a leading hyphen" {
run bash "$SCRIPT" -my-tool "$DEST"
assert_failure
}
@test "fails when skill name has a trailing hyphen" {
run bash "$SCRIPT" my-tool- "$DEST"
assert_failure
}
@test "fails when destination directory does not exist" {
run bash "$SCRIPT" my-tool "/nonexistent/path"
assert_failure
}
@test "exits 0 when target already exists (no-op)" {
mkdir -p "$DEST/my-tool"
run bash "$SCRIPT" my-tool "$DEST"
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" ]
}
@test "package mode: matched-quote type value ('skill') is recognized" {
mkdir -p "$DEST/pkg"
printf 'name: my-pkg\ntype: "skill"\n' > "$DEST/pkg/apm.yml"
run bash "$SCRIPT" my-tool "$DEST/pkg"
assert_success
assert_output --partial "Mode: package"
}
@test "mismatched-quote type value is rejected, falls through to standalone mode" {
printf "name: my-pkg\ntype: \"skill'\n" > "$DEST/apm.yml"
run bash "$SCRIPT" my-tool "$DEST"
assert_success
assert_output --partial "Mode: standalone"
}
@test "type: line is recognized even without a trailing newline on apm.yml" {
printf 'name: my-pkg\ntype: skill' > "$DEST/apm.yml"
run bash "$SCRIPT" my-tool "$DEST"
assert_success
assert_output --partial "Mode: package"
}