chore: drop the flat content mirror and native install support (ADR-0024)
apm becomes the only supported install path. The flat mirror at each plugin root existed solely so Claude Code's native `claude plugin install` could convention-scan plugin content (ADR-0017). With no native consumers, it cost ~20,000 tracked lines plus ~2,100 lines of sync tooling and ~88s of every push to guard content apm never reads — and its only automated gate, `claude plugin validate --strict`, passes on a plugin with zero content, so it could not detect the defect ADR-0017 was created to fix. Removes the mirror (213 files), the six per-plugin manifest pairs, sync-plugin-content.sh, its 1,289-line test, the orphaned marketplace-plugins.sh, and the check-plugin-content-sync and validate-plugins pre-push hooks. The root `marketplace:` block and .claude-plugin/ catalogue stay: apm's own marketplace consumers read that same file, so `<name>@holocron` short names keep working. tests/run-bats.sh now excludes .claude/skills/. apm installs from .apm/, which carries the tests/ dirs the mirror stripped, so deployed .bats files would otherwise be discovered and double-run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
This commit is contained in:
@@ -10,4 +10,4 @@ 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`) — source-only. `scripts/sync-plugin-content.sh` strips `<category>/<name>/tests` from the generated mirror (ADR-0017), so this file exists in a repo checkout of `.apm/skills/agent-author/` and not in an installed plugin.
|
||||
Tests: `tests/new-agent.bats` (requires `bats-support` and `bats-assert`).
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "kyberforge",
|
||||
"version": "1.6.2",
|
||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
"email": "defame1297@rkdr.net",
|
||||
"url": "https://git.dev.rkdr.net/Defame1297/"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/kyberforge",
|
||||
"repository": "https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/kyberforge",
|
||||
"keywords": [
|
||||
"marketplace",
|
||||
"plugin",
|
||||
"skill-authoring",
|
||||
"agent-authoring",
|
||||
"apm"
|
||||
]
|
||||
}
|
||||
20
plugins/kyberforge/.github/plugin/plugin.json
vendored
20
plugins/kyberforge/.github/plugin/plugin.json
vendored
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "kyberforge",
|
||||
"version": "1.6.2",
|
||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
"email": "defame1297@rkdr.net",
|
||||
"url": "https://git.dev.rkdr.net/Defame1297/"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/kyberforge",
|
||||
"repository": "https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/kyberforge",
|
||||
"keywords": [
|
||||
"marketplace",
|
||||
"plugin",
|
||||
"skill-authoring",
|
||||
"agent-authoring",
|
||||
"apm"
|
||||
]
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
name: apm-orchestrate
|
||||
|
||||
description: Use when an agent caller needs a multi-step apm package or marketplace operation dispatched and safety-gated, including the same operation fanned out across a monorepo. Not apm binary or agent-runtime install -> apm-install.
|
||||
|
||||
source_keys:
|
||||
- context7-microsoft-apm
|
||||
|
||||
disallowedTools: Edit, Write, NotebookEdit
|
||||
---
|
||||
|
||||
You are the orchestrator for apm package/marketplace operations. Your one job is routing and safety-gating: you do not decide manifest content yourself, you delegate to `apm-workflow` and enforce confirmation on irreversible operations. You never edit files. Every manifest or primitive that changes under your dispatch is written by `apm-workflow` or by `apm` itself — never by an edit you make.
|
||||
|
||||
You resolve the package root once per dispatched operation (the directory containing that package's `apm.yml`) and carry it forward as session context rather than making every call re-resolve it.
|
||||
|
||||
**Scope:** this orchestrator routes `apm-workflow`'s five concerns only — configure/scaffold, marketplace, install, compile/pack/publish, audit. It does not route `apm-install` (binary install, agent-runtime setup) — that's a one-time machine bootstrap, not a per-package, fan-out-able operation, and has no orchestrator counterpart. Confirm `apm --version` succeeds before dispatching any operation; if it fails, tell the caller to run `apm-install` first rather than attempting recovery here.
|
||||
|
||||
## Hard rules
|
||||
|
||||
These are non-negotiable regardless of `confirm` or any skill-local override:
|
||||
- `apm publish` claims a version on a registry — treat it as irreversible. Refuse without explicit `confirm: true`; always dispatch with `--dry-run -v` first and surface that output to the caller before the real publish, even when `confirm: true` was given.
|
||||
- Never guess the marketplace-add direction from context — resolve strictly from the operation name (`add-package` vs `add-marketplace`); see apm-workflow/references/marketplace.md Gotchas for why the two are easy to conflate.
|
||||
- `type:` correctness is `apm-workflow`'s call — do not pre-set or second-guess it.
|
||||
- A clean plain `apm audit` is not a CI-equivalent pass — if the caller's intent is a CI gate, dispatch `audit-ci`, not `audit`.
|
||||
- Check the `apm experimental enable registries` precondition before dispatching any operation that depends on a named registry, and fail with a clear diagnostic rather than silently no-op'ing like apm itself does — see apm-workflow/references/configure.md Gotchas for the underlying constraint (summarised in its SKILL.md Gotchas).
|
||||
- You are read-only against the working tree. Never create, edit, or delete a file — not an `apm.yml`, not a `.apm/` primitive, not compiled output, not a scratch note. `edit-config` is an operation you *route* to `apm-workflow`, never one you perform: dispatching it is allowed only when the caller asked for that edit, never as your own repair of something you noticed.
|
||||
|
||||
## Inputs
|
||||
|
||||
- **operation:** string, one of:
|
||||
- configure: init-package, edit-config (→ author/edit an existing package's `apm.yml` directly — adding a dependency, script, registries block, or adding/removing a `marketplace.packages[]` entry, including the local-package registration `apm marketplace package add` cannot perform; not a distinct `apm` CLI verb, just a manifest edit, optionally followed by `compile: pack` if it affects a published marketplace listing)
|
||||
- marketplace: init-marketplace, check-marketplace, add-package (→ `apm marketplace package add` — register a **remote** package reference (`owner/repo`, host URL, or full URL) into a marketplace being built; it rejects a local path, so a local package is not this operation — route it to `edit-config` instead), add-marketplace (→ `apm marketplace add` — register a marketplace as a consumer)
|
||||
- install: install (→ `apm install [PACKAGE_REF]` — resolve/fetch dependencies declared in `apm.yml` against `apm.lock.yaml`; no arguments re-resolves everything)
|
||||
- compile: compile, pack, publish, run-script
|
||||
- audit: audit, audit-ci
|
||||
- **package_root:** string, path to the directory containing the target `apm.yml` (required for every operation except `init-marketplace` when scaffolding the repo root)
|
||||
- **parameters:** object, operation-specific arguments (package name for `add-package`, script name for `run-script`, registry name, etc.)
|
||||
- **context:** object (optional), session state to carry forward (resolved registry config, marketplace root)
|
||||
- **confirm:** boolean (optional), explicit confirmation required for `publish`
|
||||
|
||||
## Process
|
||||
|
||||
1. Validate the request structure and check if `operation` is known
|
||||
2. Check the request against the Hard rules above (publish confirmation, marketplace-add direction, `type:` delegation, audit-vs-audit-ci, registries precondition) — refuse outright on violation, independent of `confirm`
|
||||
3. If `operation` is `publish`: require `confirm: true`, dispatch `--dry-run -v` first regardless, surface that output, else fail with structured "requires explicit confirmation" error
|
||||
4. Verify `apm --version` succeeds; if not, fail with a diagnostic pointing to `apm-install`
|
||||
5. Invoke `apm-workflow` via `Skill` with the resolved action, `package_root`, and parameters
|
||||
6. If fanning across multiple packages, dispatch independent packages in parallel when no shared state or ordering dependency exists between them; loop package-by-package (strictly sequential) only for packages with a real dependency on another package's completion. Either way, collect per-package results and failures rather than aborting on the first failure
|
||||
7. Catch and handle apm errors: retry once for a dependency-not-yet-scaffolded failure after the caller confirms the dependency exists; otherwise return error structure with diagnostics. If the failure looks trivially fixable by a one-line manifest edit — a missing `category:`, a typo'd `source:`, a version that disagrees between a package and the catalog — name that fix in `suggestions` and stop. Do not apply it yourself and do not self-dispatch an `edit-config` to apply it
|
||||
8. Aggregate all outputs and return as structured JSON
|
||||
|
||||
## Output
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success" | "error" | "partial",
|
||||
"operation": "<operation_name>",
|
||||
"result": {
|
||||
"output": "<apm-workflow output or result, or a list of per-package results when fanned out>",
|
||||
"context": { "package_root": "...", "resolved_registry": "..." }
|
||||
},
|
||||
"error": {
|
||||
"message": "<human-readable error>",
|
||||
"code": "<error type: not_confirmed | apm_unavailable | manifest_invalid | dependency_unresolved | publish_failed>",
|
||||
"recovery_attempted": true | false,
|
||||
"suggestions": ["<suggestion1>", "<suggestion2>"]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/.apm/hooks/check-apm-current.sh",
|
||||
"timeout": 380,
|
||||
"type": "command"
|
||||
}
|
||||
],
|
||||
"matcher": "startup"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
---
|
||||
name: agent-audit
|
||||
description: >
|
||||
Use when the user wants an agent definition audited — "audit this agent",
|
||||
"review my agent file", "is this ready to ship" — or after hand-editing an
|
||||
agent outside agent-author. Not applying fixes -> agent-author. Not a skill
|
||||
directory -> skill-audit.
|
||||
allowed-tools: Bash Read
|
||||
metadata:
|
||||
version: "1.0.2"
|
||||
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
|
||||
|
||||
- Do not narrate 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.
|
||||
- Agents take the same 250/400-character description gates as skills and **no body word gate at all** — an agent body becomes the system prompt of a fresh context, so the 900-word skill ceiling does not transfer. Judge an over-long agent body through the delegation check, never by word count.
|
||||
- At plugin/APM scope the agent is a single vendor-neutral file by design, so provider safety stops meaning Claude-Code-versus-Copilot field leakage there.
|
||||
- Vale reporting `0 files` scanned means NOT RUN, not clean. Fall back to full Step 3 judgment for every dimension it would have covered.
|
||||
|
||||
## Step 1 — Deterministic checks
|
||||
|
||||
Resolve all three paths against this skill's own directory so they work from a repo checkout and an installed plugin cache alike. Run exactly:
|
||||
|
||||
```bash
|
||||
bash scripts/validate.sh <agent-file>
|
||||
bash scripts/validate-provenance.sh <agent-file>
|
||||
bash scripts/vale-wrap.sh <agent-file> [<counterpart-file>]
|
||||
```
|
||||
|
||||
`validate.sh` takes either half of a project/user-scope pair or the single plugin/APM-scope file, detects the provider from the extension and the scope by walking up, then checks required fields, kebab-case `name`, `FILL IN:` placeholders, template HTML comments left in frontmatter, the description budget (250 chars SUGGESTION, 400 FAIL, measured on the folded YAML value) and the fields that scope permits. Its findings become the `### Structure` dimension — its FAILs and its SUGGESTIONs both — except the ones the Step 2 scope contract re-routes.
|
||||
|
||||
If a validation script fails or cannot run — Bash denied, `python3` or `vale` absent, `references/field-inventory.md` missing — read `references/validation-scripts.md`; what these scripts measure is not reproducible by reading.
|
||||
|
||||
`validate-provenance.sh` prints nothing on success, so read its exit code before you read its silence. **0** is a genuine pass, including the silent exit 0 at project or user scope, where plugin-scope provenance does not apply. **1** means real findings: its FAILs and INFOs become a separate `### Provenance` dimension, and it emits Why and Fix itself — surface those verbatim. **2** means the check never ran — a bad argument or a missing dependency, reason on stderr, no findings and often no stdout at all. On a 2, report `### Provenance` as unverified and quote the stderr reason; never grade it as a clean pass. `validate.sh` uses the same 2 tier.
|
||||
|
||||
`vale-wrap.sh` applies the bundled `Kyberforge` style as a prefilter. Pass no `--config`; the wrapper locates its own. At project/user scope pass both files of the pair, not only the one you were handed. Every rule is graded `error`, so every alert is a FAIL. Report each one citing its rule ID, filed under the dimension it belongs to, and do not re-derive it by judgment:
|
||||
|
||||
| Rule | Dimension |
|
||||
|---|---|
|
||||
| `Kyberforge.DescriptionOpener`, `Kyberforge.CompositionNote`, `Kyberforge.VagueWording`, `KyberforgeCopilot.ProactivePhrase` | description |
|
||||
| `Kyberforge.SentenceOpenerThereIs`, `Kyberforge.PaddingPhrase` | body |
|
||||
|
||||
## Step 2 — Read the agent and load its scope contract
|
||||
|
||||
Read the agent file end to end, and at project/user scope its counterpart too. A path containing `.apm/agents/` is plugin/APM scope; anything else is project or user scope. Each contract names the dimensions that apply there and where `validate.sh` findings other than Structure belong:
|
||||
|
||||
| Scope | Read |
|
||||
|---|---|
|
||||
| plugin/APM | `references/scope-plugin-apm.md` |
|
||||
| project, user | `references/scope-project-user.md` |
|
||||
|
||||
## Step 3 — Qualitative audit
|
||||
|
||||
Read `references/finding-criteria.md` first — every dimension's FAIL and SUGGESTION criteria. Load the rubric below only for a dimension the criteria put in play: one carrying a candidate finding, or one where the criterion alone does not settle the call.
|
||||
|
||||
| Dimension | Rubric |
|
||||
|---|---|
|
||||
| description | `references/description-quality.md` |
|
||||
| body, delegation, comment-discipline | `references/body-and-delegation.md` |
|
||||
|
||||
Each rubric is the reasoning behind its criteria, not a second copy of them. Cite file and line number for every finding.
|
||||
|
||||
## Step 4 — Report
|
||||
|
||||
Open with a coverage line naming every dimension checked. At project/user scope:
|
||||
|
||||
```text
|
||||
Checked: structure · provider-safety · description · body · delegation · comment-discipline · pair-consistency · provenance
|
||||
```
|
||||
|
||||
At plugin/APM scope, drop `pair-consistency` — there is no pair to check.
|
||||
|
||||
Then output only the dimensions that have findings, grouped under H3 headings, FAILs before SUGGESTIONs within each. Omit clean dimensions — their absence is what confirms they passed.
|
||||
|
||||
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 holding one line: `PASS`, `PASS (N suggestions)`, or `FAIL (N fails · M suggestions)`, each optionally followed by ` · P info`. INFO findings are observational and never change PASS/FAIL; omit `· P info` when there are none. Add a second line, `Run agent-author to address findings.`, whenever there is at least one finding. Do not apply fixes — report and propose only.
|
||||
@@ -1,7 +0,0 @@
|
||||
StylesPath = styles
|
||||
|
||||
[**/agents/*.md]
|
||||
BasedOnStyles = Kyberforge
|
||||
|
||||
[**/*.agent.md]
|
||||
BasedOnStyles = Kyberforge, KyberforgeCopilot
|
||||
@@ -1,13 +0,0 @@
|
||||
extends: existence
|
||||
message: "Composition or architecture note in a description: '%s' — a description carries a trigger, one capability clause and a boundary clause only; move this to README.md"
|
||||
level: error
|
||||
scope: text.frontmatter.description
|
||||
ignorecase: true
|
||||
tokens:
|
||||
- cross-cutting
|
||||
- shared (skill|agent)
|
||||
- human-facing
|
||||
- entry[- ]point
|
||||
- composes
|
||||
- rather than duplicating
|
||||
- replaces the (old|former|previous)
|
||||
@@ -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\b'
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- context7-websites-code-claude
|
||||
- claude-code-plugins-docs
|
||||
- claude-code-subagents-docs
|
||||
- context7-github-en-copilot
|
||||
- github-custom-agents-configuration
|
||||
---
|
||||
|
||||
# Body, Delegation and Comment Discipline Reference
|
||||
|
||||
Upstream source: Claude Code subagent and plugin references, GitHub Copilot custom-agents
|
||||
configuration. House contract: the context budget.
|
||||
|
||||
Read this when judging the **body**, **delegation** and **comment-discipline** dimensions.
|
||||
|
||||
## 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.
|
||||
|
||||
## Agents take no body word gate
|
||||
|
||||
A skill body is gated at 600 words SUGGESTION / 900 FAIL; an agent body is deliberately gated at
|
||||
nothing. The two are not the same construct: a skill body is loaded into the caller's live
|
||||
context and competes with the conversation already there, while an agent body *becomes* the system
|
||||
prompt of a fresh context that has nothing else in it. The rationale for the 900-word ceiling does
|
||||
not transfer, so:
|
||||
|
||||
- **Never report an agent body as too long on a word count.** There is no number to cite.
|
||||
- **Never add such a gate to `scripts/validate.sh`.** `tests/validate.bats` pins its absence with a
|
||||
body far past 900 words that must still pass, and adding one would contradict the ADR.
|
||||
- The one length signal that does apply is the Copilot runtime's 30,000-character body limit, which
|
||||
`validate.sh` already reports as a SUGGESTION because content past it is silently truncated.
|
||||
|
||||
Length is judged through the delegation check below instead, which is the defect a word count was
|
||||
standing in for anyway.
|
||||
|
||||
## The delegation check
|
||||
|
||||
A plugin-scope agent is a single `.apm/agents/<name>.agent.md` file with no sibling `references/`
|
||||
directory. It cannot progressively disclose to itself — it can only delegate to skills. So a
|
||||
procedure spelled out in an agent body that a skill the agent invokes already owns is not a
|
||||
shortcut: it is a second copy of that procedure, and the second copy drifts. This is the
|
||||
characteristic agent defect, the way a stale README row is the characteristic skill defect.
|
||||
|
||||
**An agent body that restates a procedure owned by a skill it can invoke is a FAIL.** The Fix is
|
||||
always the same shape: invoke `<skill>` instead.
|
||||
|
||||
How to apply it: for each procedural block in the body — a rule list, a numbered sequence, a
|
||||
constraint table — ask which skill owns that procedure. If the agent names that skill anywhere (its
|
||||
dispatch table, its routing prose, its frontmatter), the block is a restatement and the skill is
|
||||
already there to be invoked.
|
||||
|
||||
Worked example. The three `*-orchestrate` agents exist to compose domain skills — `git-orchestrate`
|
||||
(933 body words), `gitea-orchestrate` (1,199) and `apm-orchestrate` (1,080) — so any step they
|
||||
spell out that the composed skill already owns is the defect. `git-orchestrate:24-31` carries a
|
||||
"Hard rules" list (Conventional Commits types, atomic commits, never commit secrets, git trailers)
|
||||
that `git-commits` owns and that `git-orchestrate:44` routes to by name; `:39` concedes the point
|
||||
outright, noting the sub-skills "carry their own local copies of these rules". Two copies, one
|
||||
authority, and nothing keeping them in step.
|
||||
|
||||
What is **not** a finding under this rule, because no skill owns it:
|
||||
|
||||
- The dispatch table itself — which operation routes to which skill.
|
||||
- Safety gates the agent enforces before dispatching, and refusals it makes on its own authority.
|
||||
- The input contract and the structured output the agent's caller consumes.
|
||||
- Session state the agent carries across skill invocations.
|
||||
|
||||
## What the body is for
|
||||
|
||||
Include what the fresh context lacks:
|
||||
|
||||
- A direct role instruction opening the prompt: `You are a [role]. When invoked, [action].`
|
||||
- One bounded job, stated so the agent knows what it must refuse.
|
||||
- The dispatch, gates, inputs and outputs listed above.
|
||||
- **Error handling** — what the agent does on malformed, missing or contradictory input: stop and
|
||||
report, or degrade to a named fallback. Absent it, the agent invents a recovery, and a
|
||||
subagent's invented recovery is invisible to its caller until the output is wrong.
|
||||
- Non-obvious environment facts and project-specific conventions it cannot infer.
|
||||
- One default per decision point with one escape hatch.
|
||||
|
||||
Do not include at all:
|
||||
|
||||
- Concepts the agent already knows (what JSON is, how HTTP works, what a CSV is)
|
||||
- Exhaustive option lists — pick a default; the agent does not benefit from choosing
|
||||
- Steps the agent handles independently — over-specifying leads to unproductive paths
|
||||
- Restatements of the description, which is already in context
|
||||
|
||||
## Comment discipline
|
||||
|
||||
Inspect every comment block in the YAML frontmatter and apply the core test to each: *would the
|
||||
agent get this wrong without this comment?* Template scaffolding — `# Optional. <long
|
||||
explanation>`, more than a line or two of inline guidance per field — belongs to development, not
|
||||
to a shipped file. At plugin/APM scope the stakes are higher than tidiness: `apm compile` copies
|
||||
frontmatter verbatim to every target, `<!-- ... -->` is not valid YAML, and `validate.sh` FAILs a
|
||||
frontmatter block that still contains one.
|
||||
|
||||
## Where the criteria live
|
||||
|
||||
Every FAIL and SUGGESTION criterion for these dimensions is in `references/finding-criteria.md`,
|
||||
which Step 3 reads on every run. This file is the reasoning behind them, loaded only when that file
|
||||
puts the body, delegation or comment-discipline dimension in play.
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- context7-websites-code-claude
|
||||
- claude-code-subagents-docs
|
||||
- context7-github-en-copilot
|
||||
- github-custom-agents-configuration
|
||||
---
|
||||
|
||||
# Agent Description Quality Reference
|
||||
|
||||
Upstream source: Claude Code subagent reference, GitHub Copilot custom-agents configuration.
|
||||
House contract: the context budget. The house contract is narrower than either
|
||||
platform's schema rather than a reinterpretation of it: where both speak, both must be satisfied.
|
||||
|
||||
## Why the description is the expensive part
|
||||
|
||||
At startup an agent loads only the `name` and `description` of every installed skill and agent.
|
||||
The body is never seen until the agent is invoked. The description therefore carries the entire
|
||||
triggering burden **and** is paid for in every session, whether the agent fires or not.
|
||||
|
||||
A second cost is less obvious and is a correctness hazard rather than a token cost: a description
|
||||
that summarises the workflow is a shortcut the caller takes *instead of* reading the body. A
|
||||
measured failure upstream — a description saying "code review between tasks" — produced one review
|
||||
where the body's flowchart specified two.
|
||||
|
||||
## Step 0 — establish which contract applies
|
||||
|
||||
Read the frontmatter before judging a single word.
|
||||
|
||||
- **`disable-model-invocation: true`** — the agent is hand-invoked. Its description is never
|
||||
matched against user intent, so it is not a routing string. It carries **one plain human-facing
|
||||
sentence** stating what the agent does. Audit it for that and nothing else. Reporting a missing
|
||||
trigger clause, a missing boundary clause or absent indirect triggers on a hand-invoked agent is a
|
||||
wrong finding, not a strict one. The field is Copilot-only and not on the vendor-neutral APM
|
||||
allowlist, so this case arises in a Copilot `.agent.md` at project/user scope and nowhere else.
|
||||
Its Claude Code counterpart has no equivalent field and stays model-invoked, so the two halves of
|
||||
the pair carrying differently shaped descriptions is expected there rather than a
|
||||
pair-consistency finding.
|
||||
`user-invocable: false` does not belong in this bullet. The two are separate fields with opposite
|
||||
defaults — `disable-model-invocation` (default `false`) governs runtime auto-selection,
|
||||
`user-invocable` (default `true`) governs manual invocation, and the retired `infer` field was
|
||||
replaced by the pair rather than by either one. So `user-invocable: false` says nothing about
|
||||
whether the agent is model-routed: judge that from `disable-model-invocation` alone, and where
|
||||
that is absent the three-part shape below still applies. `user-invocable` carries no
|
||||
description-quality contract of its own and is out of this file's scope entirely.
|
||||
- **No such flag** — the agent is model-invoked and the rest of this file applies.
|
||||
|
||||
## The three-part shape
|
||||
|
||||
A model-invoked description carries exactly three things:
|
||||
|
||||
1. **Trigger clause.** When to invoke, phrased imperatively: `Use when ...`. Not `This agent ...` —
|
||||
the caller is deciding whether to act, not reading a catalogue entry.
|
||||
2. **At most one capability clause.** What it does, in one clause. Never an enumeration.
|
||||
3. **Boundary clause.** Compressed form: `Not <thing> -> <skill-name>.` The target must resolve to
|
||||
a real skill directory or agent file in the authoring source.
|
||||
|
||||
Everything else belongs in the body or in the plugin's `README.md`.
|
||||
|
||||
## Indirect triggers — conditional, never blanket
|
||||
|
||||
Add "even if the user doesn't say X" **only where the user's natural phrasing genuinely omits the
|
||||
domain word.** True for the `gitea-*` family: people say "create an issue", not "create a Gitea
|
||||
issue". False for `git-commits`: nobody asks for a commit without saying commit. A blanket
|
||||
indirect-trigger clause on an agent whose domain word is unavoidable is padding charged to every
|
||||
session.
|
||||
|
||||
## Near-miss exclusions
|
||||
|
||||
Add a boundary clause only where a sibling skill or agent could plausibly steal the activation. Use
|
||||
strong near-misses — queries that share keywords but need something different — not weak ones. One
|
||||
boundary clause per genuine near-miss; a list of four is enumeration wearing a boundary's clothes.
|
||||
|
||||
## Before / after
|
||||
|
||||
```yaml
|
||||
# FAIL — a noun-phrase opener rather than a trigger, capability enumeration in
|
||||
# place of one capability clause, and no boundary clause at all, preloaded into
|
||||
# every session forever. (The live git-orchestrate description, 254 chars.)
|
||||
description: Orchestrates git workflow operations for other agents. Invoke when a
|
||||
caller needs a multi-step or destructive git operation (rebase, force-push, branch
|
||||
deletion) coordinated across domain skills with safety gates, session context, and
|
||||
structured results.
|
||||
|
||||
# PASS — trigger, one capability clause, boundary. The operation list and the
|
||||
# safety-gate mechanics are the body's job; the router cannot act on them.
|
||||
description: >
|
||||
Use when an agent caller needs a multi-step or destructive git operation
|
||||
dispatched and safety-gated. Not conversational git help -> git-workflow.
|
||||
```
|
||||
|
||||
## Where the criteria live
|
||||
|
||||
Every FAIL and SUGGESTION criterion for this dimension is in `references/finding-criteria.md`,
|
||||
which Step 3 reads on every run. This file is the reasoning behind them, loaded only when that file
|
||||
puts the description dimension in play.
|
||||
@@ -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
|
||||
---
|
||||
|
||||
## 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 disallowedTools
|
||||
|
||||
Parsing note: `validate.sh` reads the **first** non-empty, non-`#`, non-`---` line under each
|
||||
heading as a whitespace-separated token list, and stops there. Keep the token line immediately
|
||||
below its heading; explanatory prose goes after it, as here.
|
||||
|
||||
Why `disallowedTools` is on a list that is otherwise vendor-neutral, when `tools` is not
|
||||
(ADR-0016 and its 2026-08-14 amendment): the two are not symmetric. `tools` is an **allowlist**
|
||||
whose vocabulary differs per harness — Claude Code names its own tools, Copilot CLI uses aliases
|
||||
(`execute`/`read`/`edit`/`search`/`agent`/`web`) — so a value correct for one is wrong for the
|
||||
other, and `apm compile` copies frontmatter verbatim with no per-target integrator to reconcile
|
||||
them. `disallowedTools` is a **denylist**, and denying by name is safe under verbatim copy: a name
|
||||
the other harness does not recognise denies nothing, so the worst case is that the fence is absent
|
||||
there, never that the wrong capability is granted. Claude Code honours it for plugin subagents: its
|
||||
plugin agent-definition reference names the fields plugin agents silently ignore (`hooks`,
|
||||
`mcpServers`, `permissionMode`), and `disallowedTools` is not among them.
|
||||
|
||||
`disallowedTools` also appears in `claude-code-only-fields` above, and that stays correct: at
|
||||
project/user scope it is still a Claude-only field and must not appear in a Copilot `.agent.md`.
|
||||
The two lists answer different questions — "may this field cross the CC/Copilot file boundary" for
|
||||
a real pair, versus "is this field safe under verbatim copy to every target" for a single
|
||||
vendor-neutral APM file.
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- context7-websites-code-claude
|
||||
- claude-code-plugins-docs
|
||||
- claude-code-subagents-docs
|
||||
- context7-github-en-copilot
|
||||
- github-custom-agents-configuration
|
||||
---
|
||||
|
||||
# Finding Criteria
|
||||
|
||||
Every FAIL and SUGGESTION criterion, for every qualitative dimension, and nothing else. The
|
||||
reasoning each criterion stands on, its worked examples and its house rules stay in that
|
||||
dimension's rubric, which Step 3 loads only for a dimension this file puts in play.
|
||||
|
||||
Two rules on using it:
|
||||
|
||||
- A criterion that plainly applies is a finding. Write it up citing file and line.
|
||||
- A criterion that might apply, or whose call the wording here does not settle, is a reason to load
|
||||
that dimension's rubric — never a reason to drop the candidate. This file decides which rubrics
|
||||
to read; it does not settle a close call on its own.
|
||||
|
||||
## description — `references/description-quality.md`
|
||||
|
||||
Flag as FAIL if:
|
||||
|
||||
- **Over 400 characters.** Measured on the folded YAML value, not the raw source lines.
|
||||
`validate.sh` reports the number; do not re-derive it, but do point the Fix at what to cut. Agent
|
||||
descriptions have no platform-documented ceiling of their own, so 400 is the only hard limit
|
||||
there is — do not go looking for a backstop behind it.
|
||||
- **Internal mechanics appear in the description.** Any of:
|
||||
- capability enumeration or a feature list;
|
||||
- output-format detail ("Produces a compact findings report with Why and Fix per finding");
|
||||
- composition or architecture notes ("composes X rather than duplicating Y", "a cross-cutting
|
||||
shared agent", "the human-facing entry point", "replaces the old flat invocation");
|
||||
- implementation detail ("self-validates via a bundled deterministic script").
|
||||
|
||||
None of it can change a routing decision and all of it is preloaded.
|
||||
`Kyberforge.CompositionNote` catches the common phrasings deterministically; the rest is
|
||||
judgment. This is the rule that deflates a description, so apply it before reaching for length.
|
||||
- **The same trigger stated twice in two registers** — a verb list, then the same verbs re-quoted
|
||||
as user phrasings, usually in the same order. One register, whichever routes better.
|
||||
- **Descriptive rather than imperative phrasing** (`This agent ...`, `This is the ...`).
|
||||
`Kyberforge.DescriptionOpener` catches any opener matching `^This`.
|
||||
- **Vague capabilities** ("helps with agents" where "audits an agent definition pair" was
|
||||
available). `Kyberforge.VagueWording` catches the known filler; imprecision outside that list is
|
||||
judgment.
|
||||
- **`Use proactively` in a Copilot or vendor-neutral description.**
|
||||
`KyberforgeCopilot.ProactivePhrase` catches it. The phrase steers the Claude Code runtime and
|
||||
does nothing anywhere else, so in a `.agent.md` it is preloaded text that buys no behaviour.
|
||||
- **Trigger-list, boundary or indirect-trigger content on a hand-invoked agent** — see Step 0 of
|
||||
`references/description-quality.md`.
|
||||
|
||||
Flag as SUGGESTION if:
|
||||
|
||||
- **Over 250 characters** but at or under 400. This tier is what moves the corpus average; the FAIL
|
||||
tier only stops outliers. Report it rather than treating a 399-character description as clean.
|
||||
- A near-miss exclusion is present but targets a weak near-miss.
|
||||
- An indirect trigger is present and warranted but could name the omitted phrasing more precisely.
|
||||
|
||||
**An unresolved boundary target is not graded here.** `validate.sh` resolves boundary targets for
|
||||
agent files at both scopes and tiers the verdict itself — route notation (`/name`, an arrow form)
|
||||
is an ERROR, the bare prose form a SUGGESTION unless a second target in the same sentence resolves.
|
||||
Step 1 has already filed it under `### Structure` at that tier. Take the script's verdict rather
|
||||
than re-resolving the name by hand, and do not re-grade it under description: a hand-walk over a
|
||||
different universe can contradict the script, and re-grading puts one target in the report twice.
|
||||
What is left to judgment is semantic and the script cannot reach it: whether a target that *does*
|
||||
resolve is the right sibling to exclude, and whether a clause naming no target at all ("examine the
|
||||
files manually") should have named one.
|
||||
|
||||
## body, delegation and comment-discipline — `references/body-and-delegation.md`
|
||||
|
||||
Flag as FAIL if:
|
||||
|
||||
- The body restates a procedure owned by a skill the agent can invoke — Fix: invoke `<skill>`
|
||||
instead
|
||||
- A sentence answers "no" to the core test — it is padding
|
||||
- A decision point presents a menu of options with no default
|
||||
- An instruction repeats content already in the description
|
||||
- Frontmatter comments are template scaffolding rather than instruction, or are HTML comments at
|
||||
plugin/APM scope
|
||||
- A prescriptive sequence is used where flexibility is fine, or the reverse
|
||||
|
||||
Flag as SUGGESTION if:
|
||||
|
||||
- The body does not open with a direct role instruction
|
||||
- The body specifies no error handling — nothing tells the agent what to do with malformed,
|
||||
missing or contradictory input
|
||||
- The job the agent describes is unbounded, or bounded only implicitly
|
||||
- A rationale is missing from a rule the agent is expected to enforce — present but unexplained
|
||||
- Comments are useful but verbose enough to bury the field they annotate
|
||||
|
||||
**Never report an agent body as too long on a word count.** A skill body is gated at 600/900 words;
|
||||
an agent body is deliberately gated at nothing, because an agent body *becomes* the
|
||||
system prompt of a fresh context rather than competing with a live conversation. No number exists
|
||||
to cite. The one length signal that applies is the Copilot runtime's 30,000-character body limit,
|
||||
which `validate.sh` already reports as a SUGGESTION. Length is judged through the delegation FAIL
|
||||
above instead.
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- claude-code-plugins-docs
|
||||
- claude-code-subagents-docs
|
||||
- github-custom-agents-configuration
|
||||
---
|
||||
|
||||
# Plugin/APM Scope Contract
|
||||
|
||||
Read this when the agent file sits at `<package>/.apm/agents/<name>.agent.md` — a single
|
||||
vendor-neutral file inside an APM package, with no counterpart anywhere.
|
||||
|
||||
## What is different here
|
||||
|
||||
`apm compile` copies an agent's frontmatter **verbatim** to every target harness. There is no
|
||||
per-target integrator to reconcile a Claude-Code-only field with a Copilot-only one, so the file
|
||||
cannot carry either (ADR-0016). That single fact drives everything below.
|
||||
|
||||
## Frontmatter allowlist
|
||||
|
||||
The permitted keys are the `apm-agent-allowlist` section of `references/field-inventory.md`. Read
|
||||
them from there. Do not recite the list in a finding, do not work from memory, and do not trust any
|
||||
restatement of it you find elsewhere in this repo: the list is data with one home (ADR-0009), it
|
||||
has changed before, and `validate.sh` parses that same section at load time, so a recitation is a
|
||||
copy that can disagree with the check the agent just ran.
|
||||
|
||||
`field-inventory.md` records why a denylist-shaped field is admitted where an allowlist-shaped one
|
||||
is not. Read that note before arguing with a finding about it.
|
||||
|
||||
## Dimension routing
|
||||
|
||||
`validate.sh` findings land as follows at this scope:
|
||||
|
||||
| Finding | Dimension |
|
||||
|---|---|
|
||||
| any frontmatter key outside the allowlist; body over the 30,000-character Copilot limit | Provider safety |
|
||||
| everything else — missing or malformed field, `name` not matching the filename stem, empty body, absent frontmatter, template HTML comments, description length | Structure |
|
||||
| — | Pair consistency never applies |
|
||||
|
||||
**Provider safety means something else here.** At project/user scope it asks whether a field leaked
|
||||
across the Claude Code / Copilot boundary. At this scope there is no boundary and no pair: it asks
|
||||
whether every field survives a verbatim copy to *every* target. Report it in those terms — a
|
||||
finding phrased as "CC-only field in a Copilot file" is the wrong finding here.
|
||||
|
||||
**Pair consistency never applies.** There is one file by design. `validate.sh` never emits a
|
||||
missing-counterpart FAIL at this scope, and neither do you, under any circumstance. Drop
|
||||
`pair-consistency` from the Step 4 coverage line rather than reporting it clean.
|
||||
|
||||
## Behaviour the schema cannot express
|
||||
|
||||
Read the description and body. If either implies a need the vendor-neutral frontmatter can no
|
||||
longer express — a tool restriction, `isolation`, `memory`, or another Claude-only behaviour a
|
||||
hand-authored CC file could have declared — flag it as a **SUGGESTION, never a FAIL**. This is a
|
||||
known upstream schema limitation (ADR-0016), not an authoring mistake, and the finding exists to
|
||||
give the author visibility into the gap rather than to imply the schema can be made to close it.
|
||||
|
||||
Example: a body saying "only use Read and Grep, never Edit" with no `tools` field to enforce it.
|
||||
A denylist-shaped restriction is the available half of that — see `field-inventory.md`.
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- context7-websites-code-claude
|
||||
- claude-code-subagents-docs
|
||||
- context7-github-en-copilot
|
||||
- github-custom-agents-configuration
|
||||
---
|
||||
|
||||
# Project and User Scope Contract
|
||||
|
||||
Read this when the agent file is not under `.apm/agents/` — a Claude Code `.md` and Copilot CLI
|
||||
`.agent.md` **pair**, at project scope (`<repo>/.claude/agents/` and `<repo>/.github/agents/`) or
|
||||
user scope (`~/.claude/agents/` and `~/.copilot/agents/`). `validate.sh` derives the counterpart
|
||||
from whichever half it was handed; audit both.
|
||||
|
||||
## The pair is a house convention
|
||||
|
||||
Neither platform requires a counterpart file. The pair is a kyberforge convention (ADR-0005), so a
|
||||
missing counterpart is a FAIL against **this repo's** convention and must be labelled that way in
|
||||
the finding, not presented as a platform spec failure.
|
||||
|
||||
## Dimension routing
|
||||
|
||||
`validate.sh` findings land as follows at these scopes:
|
||||
|
||||
| Finding | Dimension |
|
||||
|---|---|
|
||||
| a Claude-Code-only field in the Copilot file, a Copilot-only field in the CC file, a tool the runtime withholds from subagents, body over the 30,000-character Copilot limit | Provider safety |
|
||||
| counterpart file not found | Pair consistency |
|
||||
| everything else — missing or malformed field, name format, empty body, absent frontmatter, description length | Structure |
|
||||
|
||||
The two field lists are the `claude-code-only-fields` and `copilot-only-fields` sections of
|
||||
`references/field-inventory.md`. Read them from there rather than from memory; `validate.sh` parses
|
||||
those same sections, so any restatement is a copy that can disagree with the check (ADR-0009).
|
||||
|
||||
## Field and naming rules that differ by provider
|
||||
|
||||
- `name` must match the filename stem in a **Copilot CLI** `.agent.md`. Claude Code imposes no such
|
||||
rule, so a CC file whose `name` differs from its filename is not a finding.
|
||||
- A Copilot **cloud/IDE** agent — one under `.github/copilot/agents/` — may omit `name` entirely.
|
||||
If it carries one, it still has to be kebab-case.
|
||||
- `Use proactively` is meaningful in a CC description and steers the runtime to offer the agent
|
||||
unprompted. In a Copilot description it does nothing; `KyberforgeCopilot.ProactivePhrase` flags
|
||||
it. The Copilot equivalent is `disable-model-invocation`, which changes the description contract
|
||||
entirely — see `references/description-quality.md`, Step 0.
|
||||
|
||||
## Pair consistency
|
||||
|
||||
Check that:
|
||||
|
||||
- Both files exist.
|
||||
- Both system prompt bodies are non-empty (`validate.sh` covers this; do it by hand only when the
|
||||
script could not run).
|
||||
- The two files describe the **same job**. Divergent capability claims across the pair mean one
|
||||
half was edited and the other was not, which is the defect this dimension exists to catch.
|
||||
- Descriptions may legitimately differ in *shape* when the Copilot half is hand-invoked — that is
|
||||
the Step 0 case in `references/description-quality.md`, not a pair-consistency finding.
|
||||
|
||||
Keep `pair-consistency` in the Step 4 coverage line at these scopes.
|
||||
@@ -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/finding-criteria.md, references/field-inventory.md, references/description-quality.md, references/body-and-delegation.md, references/scope-project-user.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/finding-criteria.md, references/field-inventory.md, references/body-and-delegation.md, references/scope-plugin-apm.md, references/validation-scripts.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/finding-criteria.md, references/field-inventory.md, references/description-quality.md, references/body-and-delegation.md, references/scope-plugin-apm.md, references/scope-project-user.md, references/validation-scripts.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/finding-criteria.md, references/field-inventory.md, references/description-quality.md, references/body-and-delegation.md, references/scope-project-user.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/finding-criteria.md, references/field-inventory.md, references/description-quality.md, references/body-and-delegation.md, references/scope-plugin-apm.md, references/scope-project-user.md, references/validation-scripts.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`
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- claude-code-plugins-docs
|
||||
- claude-code-subagents-docs
|
||||
- github-custom-agents-configuration
|
||||
---
|
||||
|
||||
# Validation Scripts Reference
|
||||
|
||||
Read this when a Step 1 script fails, cannot run, or reports something that needs interpreting.
|
||||
Nothing here is needed on a clean run.
|
||||
|
||||
## Report the gap, do not guess
|
||||
|
||||
If a script cannot run at all — Bash denied, `python3` unavailable, `vale` not installed — say so
|
||||
as an **INFO** finding naming the script and the missing dependency, then fall back to the manual
|
||||
checks below. An INFO never changes PASS/FAIL. Silently omitting the dimension a script would have
|
||||
covered reports a clean audit that checked less than it claims to have checked.
|
||||
|
||||
## How the scripts detect scope
|
||||
|
||||
`validate.sh` and `validate-provenance.sh` walk up from the agent file's directory and stop at the
|
||||
first of these:
|
||||
|
||||
1. An `apm.yml` carrying a top-level `type: instructions|skill|hybrid|prompts` line — **plugin/APM
|
||||
scope**, and that directory is the package root. An `apm.yml` with no `type:` is a
|
||||
marketplace-only manifest: skip it and keep walking.
|
||||
2. `$HOME` — **user scope**, checked before `.git` so a dotfiles-managed home directory that is its
|
||||
own repo cannot shadow it.
|
||||
3. A `.git` directory or file — **project scope**.
|
||||
4. The filesystem root — **project scope**.
|
||||
|
||||
`plugin.json` and `.claude-plugin/plugin.json` are not scope signals. A directory holding only a
|
||||
`plugin.json` and no `apm.yml` falls through to project or user scope.
|
||||
|
||||
`validate-provenance.sh` exits 0 silently when that walk does not land on a package root, and again
|
||||
when the package has no provenance data. Check the exit code before you believe the silence:
|
||||
|
||||
- **0** — a pass, not a skip you need to investigate. Both silent cases above land here.
|
||||
- **1** — real findings, on stdout with Why and Fix.
|
||||
- **2** — the check never ran. A missing, doubled, non-file or wrongly-named argument, an
|
||||
undecodable `apm.yml`, or an absent `python3`, each with a diagnostic on stderr and no findings
|
||||
at all. Report the `### Provenance` dimension as unverified and quote the reason. An exit 2 is
|
||||
never a clean pass: empty stdout there means nothing was checked, not that nothing was wrong.
|
||||
|
||||
## Manual fallback
|
||||
|
||||
**Every scope:** required fields present (`name`, `description`, non-empty body); `name` is
|
||||
kebab-case; no `FILL IN:` placeholders in the description or body; the description at or under 400
|
||||
characters measured on the folded YAML value.
|
||||
|
||||
**Plugin/APM scope:** `name` matches the filename stem; no HTML comments left in the frontmatter;
|
||||
no frontmatter key outside the `apm-agent-allowlist` section of `references/field-inventory.md` —
|
||||
open that file, do not work from memory.
|
||||
|
||||
**Project/user scope:** the counterpart file exists; `name` matches the filename stem in the
|
||||
Copilot `.agent.md` only (Claude Code files are exempt); no key from `claude-code-only-fields` in
|
||||
the Copilot file and none from `copilot-only-fields` in the CC file, both read from
|
||||
`references/field-inventory.md`.
|
||||
|
||||
## Script-specific failures
|
||||
|
||||
- **`Error: field-inventory.md not found` (exit 2).** `validate.sh` reads its field lists from
|
||||
`references/field-inventory.md` at load time and refuses to run without it, rather than falling
|
||||
back to a hardcoded list that could disagree with the file (ADR-0009). Restore the file; do not
|
||||
work around it.
|
||||
- **`vale` reports `0 files`.** Treat the pass as NOT RUN, not as clean, and fall back to full
|
||||
Step 3 judgment for the dimensions it would have covered. The `Kyberforge` style is scoped to
|
||||
`**/agents/*.md` and `**/*.agent.md`, and `KyberforgeCopilot` to `**/*.agent.md` alone — a file
|
||||
outside those globs is silently not linted.
|
||||
- **`E100 Runtime error ... does not exist` (exit 2) from `vale-wrap.sh`.** An explicit relative
|
||||
`--config` was passed. Pass none: the wrapper locates its own `assets/vale/.vale.ini` from its
|
||||
own path. Do not read this exit code as vale being unavailable.
|
||||
- **A path argument that does not exist is a hard error** in `vale-wrap.sh`, deliberately: bare
|
||||
`vale` would fall back to reading stdin and print a clean-looking `0 errors ... in stdin`, which
|
||||
the `0 files` guard above does not catch.
|
||||
@@ -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.
|
||||
@@ -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[@]}"}
|
||||
@@ -1,632 +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 Usage error, or the argument is not an agent file this script can read
|
||||
|
||||
An exit code of 2 is NOT a finding. SKILL.md tells the auditor to surface a
|
||||
non-zero exit as findings, so a usage error leaving exit 1 with nothing on
|
||||
stdout was indistinguishable from a clean-but-failing run. Environment and
|
||||
argument problems exit 2; only real findings exit 1.
|
||||
|
||||
Exit 2 and the silent exit 0 answer two DIFFERENT questions, and neither may
|
||||
be spelled with the other's code:
|
||||
|
||||
exit 2 the argument is not something this script can audit at all — it is
|
||||
missing, doubled, not a file, or not named .md / .agent.md. Decided
|
||||
before the scope walk-up runs, from the argument alone.
|
||||
exit 0 the argument IS a readable agent file, and the scope walk-up found
|
||||
no type:-bearing apm.yml above it before hitting the \$HOME, .git or
|
||||
filesystem-root boundary. That is a real verdict about a real file —
|
||||
"this agent is user or project scope, so plugin-scope provenance
|
||||
does not apply to it" — not a rejected input.
|
||||
|
||||
scripts/check-scope-walkup-sync.sh's fixture 6 pins the second: a real agent
|
||||
file under a \$HOME with a type-bearing apm.yml ABOVE it must exit 0 with empty
|
||||
output. Widening exit 2 to cover "the walk-up found no package" would break
|
||||
that fixture AND would be wrong on its own terms, because new-agent.sh happily
|
||||
scaffolds exactly that layout.
|
||||
|
||||
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). An explicit '(none)' skips silently; a Contributing files block
|
||||
this parser cannot read is reported as an INFO saying checks 3 and 4 did
|
||||
not run, never skipped silently.
|
||||
4 Contributing files back-reference the parent slug in their source_keys
|
||||
5 Research doc field present and not placeholder
|
||||
|
||||
This script has no counterpart to skill-audit's checks 6, 7 and 8 (Research
|
||||
doc field / upstream forward / upstream reverse are numbered 6, 7, 8 there and
|
||||
5 here): an agent at plugin scope is a single file with a plugin-root
|
||||
sources.md, so there is no references/ tree to walk and no upstream research
|
||||
source index to cross-check. parse_status() and the sources.md-basename gate
|
||||
that those checks need exist only in the skill-audit copy.
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Usage and environment problems exit 2, findings exit 1. See the usage text
|
||||
# above for why the two must not share a code, and for why "not plugin scope"
|
||||
# is neither of them. This is a deliberate divergence from validate.sh, which
|
||||
# has no 2 tier for content: validate.sh always prints PASS lines, so a usage
|
||||
# error there is visibly not a findings report. This script prints NOTHING on a
|
||||
# clean run, so exit 1 plus empty stdout was the only signal a caller got
|
||||
# either way.
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Error: agent-file is required." >&2
|
||||
echo "" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Extra positional arguments were silently dropped, so a typo'd flag or a second
|
||||
# path looked like it had been honoured.
|
||||
if [[ $# -gt 1 ]]; then
|
||||
echo "Error: expected exactly one argument, got $#: $*" >&2
|
||||
echo "" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# python3 is a HARD dependency. Without this preflight a missing interpreter
|
||||
# produced 'line NN: python3: command not found' and exit 127 — an exit code no
|
||||
# caller maps to anything, from a message that names this script's line number
|
||||
# rather than the missing dependency.
|
||||
if ! command -v python3 > /dev/null 2>&1; then
|
||||
echo "Error: python3 is required but was not found on PATH." >&2
|
||||
echo " Why: skipping the provenance checks entirely would be a vacuous pass." >&2
|
||||
echo " Fix: install python3 (pre-commit itself is a Python application, so it is almost certainly already present)." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# A path that does not exist, or exists but is not a regular file, used to reach
|
||||
# the Python body, get os.path.dirname()'d into some ancestor directory and then
|
||||
# either report a silent exit 0 (no package above it) or — worse — audit a
|
||||
# DIFFERENT agent's package while naming the typo'd path. A typo'd target was
|
||||
# indistinguishable from a clean agent. vale-wrap.sh hard-errors on a
|
||||
# nonexistent path for exactly this reason.
|
||||
#
|
||||
# This is decided from the argument alone, before any walk-up runs, so it cannot
|
||||
# collide with the not-plugin-scope exit 0: that verdict is only ever reached by
|
||||
# a file that got past here.
|
||||
if [[ ! -e "$1" ]]; then
|
||||
echo "Error: no such file: $1" >&2
|
||||
echo " Why: a nonexistent target would otherwise report a silent pass." >&2
|
||||
echo " Fix: pass the path of the agent file to validate." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -f "$1" ]]; then
|
||||
echo "Error: not a regular file: $1" >&2
|
||||
echo " Why: this script audits one agent file, not a directory of them, and reporting a directory as a pass hides the wrong-target mistake." >&2
|
||||
echo " Fix: pass the agent file itself — .apm/agents/<name>.agent.md — not its parent directory." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# The extension check used to live inside the Python body. It stays exit 2 and
|
||||
# keeps its wording; it moves up here so that every "this argument is not
|
||||
# auditable" verdict is reached in one place, before the interpreter starts and
|
||||
# before the scope walk-up can turn a bad argument into a silent exit 0.
|
||||
case "$1" in
|
||||
*.agent.md | *.md) ;;
|
||||
*)
|
||||
echo "Error: unrecognized extension '$(basename "$1")' — expected .md or .agent.md" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
python3 -u - "$1" <<'PYTHON'
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
# Output is UTF-8 for the same reason input is: under LC_ALL=C the streams
|
||||
# default to ASCII, and every finding this script prints contains an em dash.
|
||||
# Pinning only the reads moved the crash from the read to the write — a
|
||||
# UnicodeEncodeError inside print_findings(), which loses the whole report
|
||||
# after all the checks have already run.
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
_stream.reconfigure(encoding='utf-8')
|
||||
except AttributeError: # pragma: no cover — Python < 3.7
|
||||
pass
|
||||
|
||||
agent_file = os.path.abspath(sys.argv[1])
|
||||
agent_dir = os.path.dirname(agent_file)
|
||||
|
||||
# --- Input ----------------------------------------------------------------
|
||||
# Ported from the skill-audit copy, where the same two problems were already
|
||||
# fixed.
|
||||
#
|
||||
# read_text() pins UTF-8 explicitly instead of inheriting
|
||||
# locale.getpreferredencoding(), which is ASCII under LC_ALL=C — an ordinary em
|
||||
# dash in an agent file or in sources.md then aborted the run with a bare
|
||||
# UnicodeDecodeError traceback, or, at the one call site that wrapped its read
|
||||
# in `except Exception: return []`, reported the unreadable file as having no
|
||||
# source_keys and therefore as clean. A file that genuinely is not UTF-8 still
|
||||
# fails; it just says which file and why.
|
||||
#
|
||||
# strip_bom() runs on every read because a leading BOM defeats
|
||||
# parse_frontmatter()'s `^---` anchor, which silently disabled check 2 on a
|
||||
# BOM-prefixed agent file: no frontmatter parsed means no source_keys parsed
|
||||
# means nothing to validate.
|
||||
|
||||
|
||||
class EncodingError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def strip_bom(text):
|
||||
return text[1:] if text.startswith(u'\ufeff') else text
|
||||
|
||||
|
||||
def read_text(path):
|
||||
"""File contents as text, UTF-8 and BOM-free, with a diagnostic instead of a traceback."""
|
||||
try:
|
||||
with open(path, encoding='utf-8') as fh:
|
||||
return strip_bom(fh.read())
|
||||
except UnicodeDecodeError as exc:
|
||||
raise EncodingError(
|
||||
"not valid UTF-8 (%s at byte %d) — re-save the file as UTF-8; "
|
||||
"this gate does not guess at other encodings"
|
||||
% (exc.reason, exc.start))
|
||||
|
||||
# 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.
|
||||
#
|
||||
# Returning None here means NOT PLUGIN SCOPE, which is a verdict, not an error:
|
||||
# the caller exits 0 silently, and scripts/check-scope-walkup-sync.sh fixture 6
|
||||
# pins that. It is deliberately NOT folded into the exit-2 tier above.
|
||||
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):
|
||||
# An apm.yml is a manifest this script must be able to READ to
|
||||
# classify scope at all. Under LC_ALL=C the old bare open() decoded
|
||||
# as ASCII, so a manifest with an accented author name raised
|
||||
# UnicodeDecodeError mid-walk and killed the run with a traceback.
|
||||
# It is an environment problem, not a finding, so it exits 2 rather
|
||||
# than being swallowed into a silent "no package here".
|
||||
try:
|
||||
content = read_text(apm_yml)
|
||||
except EncodingError as exc:
|
||||
print(
|
||||
"Error: %s is %s" % (apm_yml, exc),
|
||||
file=sys.stderr)
|
||||
sys.exit(2)
|
||||
if any(TYPE_RE.match(line) for line in content.splitlines()):
|
||||
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 ---
|
||||
|
||||
# The trailing character class used to be CONSUMING — `[^`\n]` — so a
|
||||
# `FILL IN:` at end of line matched nothing and escaped checks 1 and 5
|
||||
# entirely. `- **Description:** FILL IN:` is the most likely spelling of a
|
||||
# half-written entry, and it was the one spelling the placeholder gate could
|
||||
# not see. The exclusion it was really expressing is "not inside backticks",
|
||||
# which a lookahead states without eating a character.
|
||||
PLACEHOLDER_RE = re.compile(r'(?<!`)FILL IN:(?!`)')
|
||||
|
||||
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)
|
||||
|
||||
# ===== BEGIN SHARED CONTRIBUTING-FILES PARSER =====
|
||||
# ONE parser, embedded VERBATIM in two scripts:
|
||||
# plugins/kyberforge/.apm/skills/skill-audit/scripts/validate-provenance.sh
|
||||
# plugins/kyberforge/.apm/skills/agent-audit/scripts/validate-provenance.sh
|
||||
# The block between these markers must stay byte-identical in both. It is
|
||||
# copied rather than imported because a cache-installed plugin's scripts cannot
|
||||
# read files outside their own plugin directory, so there is no single file both
|
||||
# can share — the same constraint that forces the ADR-0020 boundary resolver to
|
||||
# be duplicated across three scripts. Edit one copy, then paste it over the
|
||||
# other.
|
||||
#
|
||||
# tests/test-adr0020-contract.sh hashes both copies and fails on drift. Before
|
||||
# it did, the agent-audit copy's docstring merely ASSERTED the two were
|
||||
# "behaviourally identical" and nothing checked it — which is how the two
|
||||
# already-diverged spellings of the bullet loop went unnoticed.
|
||||
#
|
||||
# Requires: re (imported by the host script).
|
||||
|
||||
|
||||
def parse_contributing_files(content, slug):
|
||||
"""Find the Contributing files for a given slug H2 in content.
|
||||
|
||||
Both authored forms are accepted, because both are in use across the
|
||||
corpus and only recognising the first silently skipped the contributing-
|
||||
file checks on every sources.md written the other way:
|
||||
|
||||
- **Contributing files:** SKILL.md, references/a.md
|
||||
|
||||
**Contributing files:**
|
||||
- SKILL.md (what this source contributed)
|
||||
- references/a.md (what this source contributed)
|
||||
|
||||
Returns a list of paths with any trailing parenthetical note stripped.
|
||||
Note the bullet form's notes may themselves contain commas, so the list
|
||||
is built per bullet rather than by splitting the joined value.
|
||||
|
||||
The three return values are NOT interchangeable, and callers depend on
|
||||
the distinction:
|
||||
|
||||
[path, ...] the entry names contributing files
|
||||
[] the entry EXPLICITLY records "(none)"
|
||||
None the entry says nothing this parser can read
|
||||
|
||||
Only an explicit "(none)" yields []. A "Contributing files:" heading
|
||||
followed by a numbered list, by `*` bullets, or by prose parses nothing
|
||||
and returns None, never [] — a caller reads [] as a deliberate "no
|
||||
contributing files" record and SKIPS its check on that basis, so a parse
|
||||
failure returning [] would silently disable the check instead of leaving
|
||||
the unreadable entry exposed to it.
|
||||
"""
|
||||
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)
|
||||
|
||||
def strip_note(entry):
|
||||
# "references/a.md (why)" -> "references/a.md"
|
||||
return re.sub(r'\s*\(.*$', '', entry).strip()
|
||||
|
||||
# Inline form: value on the same line, comma-separated, no notes.
|
||||
cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE)
|
||||
if cf_m:
|
||||
value = cf_m.group(1).strip()
|
||||
if value.startswith("(none"):
|
||||
return []
|
||||
return [p for p in (strip_note(x) for x in value.split(","))
|
||||
if p] or None
|
||||
|
||||
# Bullet form: heading on its own line, one file per following bullet.
|
||||
cf_m = re.search(r'^\*\*Contributing files:\*\*\s*$', block, re.MULTILINE)
|
||||
if not cf_m:
|
||||
return None
|
||||
files = []
|
||||
for line in block[cf_m.end():].splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
if files:
|
||||
break
|
||||
continue
|
||||
if not line.startswith("- "):
|
||||
break
|
||||
entry = line[2:].strip()
|
||||
if entry.startswith("(none"):
|
||||
return []
|
||||
entry = strip_note(entry)
|
||||
if entry:
|
||||
files.append(entry)
|
||||
return files or None
|
||||
# ===== END SHARED CONTRIBUTING-FILES PARSER =====
|
||||
|
||||
def parse_research_docs(content, slug):
|
||||
"""Every Research doc value under a given slug H2, in document order.
|
||||
|
||||
The caller uses the first and reports the rest. Returning only the first —
|
||||
what this did before — meant a second '- **Research doc:**' line in one
|
||||
entry was silently ignored, so an author who added a doc rather than
|
||||
replacing one got check 5 run against the old value and no hint that the
|
||||
new one was never looked at.
|
||||
"""
|
||||
pattern = re.compile(
|
||||
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
|
||||
re.MULTILINE | re.DOTALL
|
||||
)
|
||||
m = pattern.search(content)
|
||||
if not m:
|
||||
return []
|
||||
block = m.group(1)
|
||||
return [v.strip() for v in
|
||||
re.findall(r'^\- \*\*Research doc:\*\* (.+)$', block, re.MULTILINE)]
|
||||
|
||||
findings = []
|
||||
has_fail = False
|
||||
|
||||
# A finding identical in every field is the same finding, and the same file is
|
||||
# now reached by more than one check — the agent file is read once for its own
|
||||
# source_keys and again as a contributing file, so an unreadable one would
|
||||
# otherwise be reported twice with the same words. Distinct findings about the
|
||||
# same file still both appear.
|
||||
def _record(entry):
|
||||
if entry not in findings:
|
||||
findings.append(entry)
|
||||
|
||||
def emit_fail(desc, fpath, why, fix):
|
||||
global has_fail
|
||||
has_fail = True
|
||||
_record(("FAIL", desc, fpath, why, fix, None))
|
||||
|
||||
# INFO does not set has_fail and does not change the exit code. It is for a
|
||||
# check that could not RUN — an unverified entry, not a broken one — and it
|
||||
# exists so that "did not run" is never spelled the same way as "passed".
|
||||
def emit_info(desc, fpath, note):
|
||||
_record(("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()
|
||||
|
||||
def emit_unreadable(rel, exc):
|
||||
"""Report a file this script cannot decode. Never a silent skip."""
|
||||
emit_fail(
|
||||
f"File is {exc}",
|
||||
rel,
|
||||
f"'{rel}' cannot be decoded, so its frontmatter — and any source_keys in it — "
|
||||
f"cannot be read. This used to be swallowed by a bare 'except Exception: return []', "
|
||||
f"which reported the unreadable file as having no source_keys and therefore as clean.",
|
||||
f"Re-save '{rel}' as UTF-8."
|
||||
)
|
||||
|
||||
# --- Collect source_keys from agent pair ---
|
||||
def get_source_keys_from_file(fpath, rel):
|
||||
if not os.path.isfile(fpath):
|
||||
return []
|
||||
try:
|
||||
content = read_text(fpath)
|
||||
except EncodingError as exc:
|
||||
emit_unreadable(rel, exc)
|
||||
return []
|
||||
fm, _ = parse_frontmatter(content)
|
||||
return parse_source_keys(fm)
|
||||
|
||||
# Plugin/APM scope is a single vendor-neutral file — no counterpart to merge.
|
||||
rel_given = os.path.relpath(agent_file, plugin_root)
|
||||
given_keys = get_source_keys_from_file(agent_file, rel_given)
|
||||
all_source_keys = given_keys
|
||||
|
||||
sources_md_exists = os.path.isfile(sources_md_path)
|
||||
|
||||
# Early exit: nothing to validate. The read above can itself raise a finding —
|
||||
# an unreadable agent file — so print before leaving; the clean case still
|
||||
# prints nothing and exits 0.
|
||||
if not all_source_keys and not sources_md_exists:
|
||||
print_findings()
|
||||
sys.exit(1 if has_fail else 0)
|
||||
|
||||
sources_content = None
|
||||
sources_slugs = set()
|
||||
if sources_md_exists:
|
||||
try:
|
||||
sources_content = read_text(sources_md_path)
|
||||
except EncodingError as exc:
|
||||
emit_unreadable("sources.md", exc)
|
||||
print_findings()
|
||||
sys.exit(1)
|
||||
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:
|
||||
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 ---
|
||||
|
||||
# Every per-slug parser below — parse_contributing_files, parse_research_docs —
|
||||
# locates its block with pattern.search(), so a slug written twice resolves to
|
||||
# the FIRST block every time. Iterating the raw heading list therefore checked
|
||||
# the first block's fields twice and the second block's never: a duplicated slug
|
||||
# is half-validated, and looked fully validated. The duplicate is announced and
|
||||
# the repeat visit dropped.
|
||||
all_slugs = parse_h2_slugs(sources_content)
|
||||
unique_slugs = []
|
||||
for _slug in all_slugs:
|
||||
if _slug in unique_slugs:
|
||||
continue
|
||||
unique_slugs.append(_slug)
|
||||
_count = all_slugs.count(_slug)
|
||||
if _count > 1:
|
||||
emit_info(
|
||||
f"Duplicate '## {_slug}' entry in sources.md — only the first block is checked",
|
||||
f"sources.md (## {_slug})",
|
||||
f"'## {_slug}' appears {_count} times. Every field parser here takes the first match, so the "
|
||||
f"second and later blocks' Contributing files and Research doc are never validated — "
|
||||
f"checks 3, 4 and 5 did not run for them. "
|
||||
f"Merge the blocks into one entry, or give each a distinct slug and reference it from source_keys."
|
||||
)
|
||||
|
||||
for slug in unique_slugs:
|
||||
# Checks 3 and 4: Contributing files exist (paths relative to plugin root),
|
||||
# and back-reference the slug. `[]` and None are NOT the same answer here.
|
||||
# `[]` is the author writing "(none)" — there is nothing to check and the
|
||||
# skip is correct. None is a Contributing-files block this parser cannot
|
||||
# read, and skipping THAT silently disables both checks on the one entry
|
||||
# least likely to be right, which is the failure mode
|
||||
# parse_contributing_files' own docstring warns about. Say so out loud.
|
||||
cf_files = parse_contributing_files(sources_content, slug)
|
||||
if cf_files is None:
|
||||
emit_info(
|
||||
f"Contributing-file checks skipped for '{slug}' — the Contributing files block could not be parsed",
|
||||
f"sources.md (## {slug})",
|
||||
f"The '## {slug}' entry has no Contributing files list this parser can read — a missing field, a bare heading, '*' bullets, a numbered list, or prose all read as unparsable rather than as an empty declaration. "
|
||||
f"Checks 3 and 4 did not run for this slug, so nothing verified that its contributing files exist or name it back. "
|
||||
f"Write the value as '- **Contributing files:** <comma-separated paths>', or as a '**Contributing files:**' heading followed by '- ' bullets — "
|
||||
f"or record '(none)' if this source contributed no files."
|
||||
)
|
||||
elif cf_files:
|
||||
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
|
||||
try:
|
||||
cf_content = read_text(cf_abs)
|
||||
except EncodingError as exc:
|
||||
emit_unreadable(cf_rel, exc)
|
||||
continue
|
||||
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_values = parse_research_docs(sources_content, slug)
|
||||
if len(rd_values) > 1:
|
||||
emit_info(
|
||||
f"Multiple '- **Research doc:**' lines for '{slug}' — only the first is used",
|
||||
f"sources.md (## {slug})",
|
||||
f"The '## {slug}' entry has {len(rd_values)} Research doc lines; check 5 ran against the first "
|
||||
f"('{rd_values[0]}') and never looked at the rest. "
|
||||
f"Keep one Research doc line per entry — if a slug genuinely came from two documents, split it into two slugs, "
|
||||
f"or name the extra document inside the first value's annotation where it is at least visible."
|
||||
)
|
||||
rd_value = rd_values[0] if rd_values else None
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
||||
---
|
||||
name: agent-author
|
||||
description: >
|
||||
Use when the user wants to create a new agent definition file from scratch, or
|
||||
apply grill findings, audit findings, or inline feedback to an existing one.
|
||||
Not read-only review -> `agent-audit`. Not skills -> `skill-author`.
|
||||
allowed-tools: Bash Read Write Edit
|
||||
metadata:
|
||||
version: "1.0.2"
|
||||
category: factory
|
||||
source_keys:
|
||||
- context7-websites-code-claude
|
||||
- claude-code-plugins-docs
|
||||
- claude-code-subagents-docs
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
- At plugin/APM scope `tools` and every Claude-only field are omitted entirely, not merely ignored: `apm compile` copies frontmatter verbatim to both harnesses, so fencing a read-only agent with `tools:` is wrong on one of them. `disallowedTools` is the one restriction that survives (ADR-0016).
|
||||
- That fence is partial. It denies only the tools it names, never `Bash`, which a plugin-scope agent inherits — a shell redirect still writes. State the read-only boundary in the body too.
|
||||
- An agent body carries no word gate; delegation replaces it. A plugin/APM agent is one file with no sibling `references/` directory, so it cannot disclose to itself, only invoke skills — and a body restating a procedure an invocable skill owns is an `agent-audit` FAIL.
|
||||
- Duplicate `name` values in one scope: Claude Code discards one silently. Verify uniqueness before shipping.
|
||||
|
||||
## Step 1 — Dispatch
|
||||
|
||||
| Condition | Flow | Reference |
|
||||
|---|---|---|
|
||||
| No agent file at the target path(s) | Create | `references/create.md` |
|
||||
| A file exists, at least one improvement signal present | Improve | `references/improve.md` |
|
||||
| A file exists, no signals | Stop and ask | — |
|
||||
|
||||
Signals: grill output, `agent-audit` findings, inline feedback, session context describing what went wrong. With none, ask: "No improvement signals found. Did you mean to create a new agent, or do you have feedback to apply?"
|
||||
|
||||
Read only the reference for the resolved flow. Capture `rtk git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
|
||||
## Step 2 — Scope
|
||||
|
||||
Scope decides which fields exist, so resolve it first. `scripts/new-agent.sh` walks up for a `type:`-bearing `apm.yml` and prints the scope it chose — read that output.
|
||||
|
||||
| Resolved scope | Emits | Read |
|
||||
|---|---|---|
|
||||
| plugin/APM | one vendor-neutral `.apm/agents/<name>.agent.md` | `references/plugin-scope.md` |
|
||||
| project or user | a Claude Code `.md` + Copilot `.agent.md` pair | `references/project-user-scope.md` |
|
||||
|
||||
Read only the file for the resolved scope; the other describes fields this run cannot use. If precedence, cache isolation or path conventions matter, read `references/deployment-modes.md`.
|
||||
|
||||
## Step 3 — Contract
|
||||
|
||||
Before writing or editing a `description`, or restructuring a body, read `references/contract.md` — the three-part shape, banned content, the delegation rule and the body pattern.
|
||||
|
||||
Gates `agent-audit` enforces at every scope:
|
||||
|
||||
- **Description** — a trigger clause, at most one capability clause, and a boundary clause shaped `Not <thing> -> <name>` that resolves to a real skill or agent. 250 characters SUGGESTION, 400 FAIL, value only: an agent's `name` and `description` is preloaded into every session exactly as a skill's is.
|
||||
- **Body** — no word gate, and a delegation check in its place: name the skill to invoke rather than restating what it does.
|
||||
- **Invocation** — decide whether the agent is model-delegated or reached only by name. Only Copilot's cloud/IDE format expresses that in frontmatter (`disable-model-invocation`, `user-invocable`).
|
||||
|
||||
At every scope, five tools reach no subagent whatever `tools` says — `AskUserQuestion`, `EnterPlanMode`, `ExitPlanMode`, `ScheduleWakeup`, `WaitForMcpServers`. Never write a body that has the agent ask the user a question or enter plan mode; it describes a turn the runtime cannot give it.
|
||||
|
||||
## Step 4 — Validate and close
|
||||
|
||||
Invoke `agent-audit` on each file written and resolve every FAIL before reporting done. It checks the field allowlist, name-to-stem match, leftover placeholders and template comments, the description budget and the Copilot body limit — do not hand-check those.
|
||||
|
||||
At plugin/APM scope bump the resolved package's `apm.yml` `version` — **minor** on create, **patch** on improve — because consumers compare it to detect updates. Project and user scope have no manifest.
|
||||
|
||||
**Commit verification.** Once the audit is clean, run `rtk git add` and `rtk git commit` — do not stop at staging. Re-run `rtk git log --oneline -1` and confirm the hash changed from Step 1's. A non-empty `git diff --stat` is not proof: staged-but-uncommitted work is part of no commit and is lost if the tree is cleaned up. Report done only once the hash has changed.
|
||||
@@ -1,10 +0,0 @@
|
||||
# assets/
|
||||
|
||||
## templates/
|
||||
|
||||
Annotated agent definition templates copied by `scripts/new-agent.sh` when scaffolding a new agent.
|
||||
All three scaffold the `description` in the three-part ADR-0020 shape — a `Use when` trigger clause, at most one capability clause, and a boundary clause — rather than the deleted action-verb opener, and each carries a delegate-don't-restate note in the body.
|
||||
|
||||
- **`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.template`** — 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). Frontmatter is limited to the `apm-agent-allowlist` section of `agent-audit`'s `references/field-inventory.md` — the authoritative list, read from there as data by `agent-audit`'s `validate.sh`; this file deliberately does not restate it. No `tools` and no Claude-only knobs, since `apm compile` copies frontmatter verbatim to both the Claude Code and Copilot CLI targets with no per-target integrator; `disallowedTools` is scaffolded as an opt-in comment because a denylist, unlike the `tools` allowlist, survives that copy (ADR-0016 and its 2026-08-14 amendment).
|
||||
@@ -1,92 +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, with no per-target field integrator to reconcile
|
||||
anything, so a harness-specific value is wrong on at least one of them.
|
||||
|
||||
This template does not restate the permitted-field list. The authoritative
|
||||
list is the `apm-agent-allowlist` section of agent-audit's
|
||||
references/field-inventory.md, which agent-audit's validate.sh reads from
|
||||
there as data — a list copied into a template goes stale one step further
|
||||
out than the list itself. Every field scaffolded below is on it; before
|
||||
adding any other field, check that section.
|
||||
|
||||
The shape rule behind the list (ADR-0016 and its 2026-08-14 amendment):
|
||||
`tools` is an ALLOWLIST whose vocabulary differs per harness — Claude tool
|
||||
names vs Copilot's execute/read/edit/search/agent/web — so one value is
|
||||
wrong on one target. Never add it here; omitting it means inherit-all-tools
|
||||
on both harnesses, which is never wrong. `disallowedTools` is a DENYLIST
|
||||
and is allowed for exactly that reason: a name the other harness does not
|
||||
recognise denies nothing, so the worst case is a missing fence, never a
|
||||
wrongly granted capability. Claude-only knobs (isolation, maxTurns, effort,
|
||||
memory, permissionMode) have no Copilot equivalent and stay out.
|
||||
|
||||
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: Use when <trigger>. <One capability clause.> Not <thing> -> <name>.
|
||||
<!-- Required. The primary signal for autonomous delegation, and preloaded into every
|
||||
session whether or not this agent is ever used. Three parts, nothing else:
|
||||
a trigger clause opening "Use when", at most one capability clause, and a
|
||||
boundary clause naming a real sibling skill or agent.
|
||||
250 characters is the target, 400 the hard ceiling (ADR-0020).
|
||||
Do not open with an action verb ("Reviews...", "Analyzes...") — that rule was
|
||||
deleted.
|
||||
Never write "Use proactively" here. It steers the Claude Code runtime and does
|
||||
nothing anywhere else, and this file compiles to a Copilot `.agent.md` too, where
|
||||
agent-audit's KyberforgeCopilot.ProactivePhrase rule grades it a hard FAIL.
|
||||
The phrase is CC-only; at this scope, a precise trigger clause does that job.
|
||||
Example: "Use when a diff needs checking for injected credentials before it
|
||||
merges. Not prose or style linting -> `lint-runner`." -->
|
||||
|
||||
<!-- model: sonnet
|
||||
Optional. Aliases: sonnet, opus, haiku, fable. Or full model ID.
|
||||
Omit to inherit the runtime default on whichever harness compiles this file. -->
|
||||
|
||||
<!-- disallowedTools: Edit, Write, NotebookEdit
|
||||
Optional. Denylist, applied before `tools` and taking precedence over it.
|
||||
Add it when this agent is read-only — it is the one tool restriction that
|
||||
survives verbatim copy (see the header comment). Claude Code honours it for
|
||||
plugin subagents — confirmed. Copilot's handling of the key is unconfirmed;
|
||||
ADR-0016 accepts that as a stated risk rather than a settled fact.
|
||||
It denies only the tools it names. It does NOT deny Bash, which this agent
|
||||
inherits, so a shell redirect still writes — state the read-only boundary
|
||||
in the system prompt body as well, not in frontmatter alone. -->
|
||||
|
||||
<!-- 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.
|
||||
|
||||
<!-- Delegate, don't restate. If an installed skill already owns a procedure this agent
|
||||
needs, name it ("invoke `git-commits`") instead of transcribing it — a body that
|
||||
restates a procedure an invocable skill owns is an agent-audit FAIL. One job per
|
||||
agent. Delete this comment before shipping. -->
|
||||
|
||||
## 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.
|
||||
|
||||
## Errors
|
||||
|
||||
FILL IN: What does the agent do on malformed, missing or contradictory input?
|
||||
State whether it stops and reports, or degrades to a named fallback — and what it
|
||||
tells the caller either way. An agent with no error handling invents a recovery,
|
||||
and an invented recovery is invisible until the output is wrong.
|
||||
@@ -1,113 +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: Use when <trigger>. <One capability clause.> Not <thing> -> <name>.
|
||||
<!-- Required. The primary signal for autonomous delegation, and preloaded into every
|
||||
session whether or not this agent is ever used. Three parts, nothing else:
|
||||
a trigger clause opening "Use when", at most one capability clause, and a
|
||||
boundary clause naming a real sibling skill or agent.
|
||||
250 characters is the target, 400 the hard ceiling (ADR-0020).
|
||||
Do not open with an action verb ("Reviews...", "Analyzes...") — that rule was
|
||||
deleted.
|
||||
"Use proactively" is valid HERE and only here: it steers the Claude Code runtime
|
||||
to offer this agent unprompted. Add it only if that is what you want. If you add
|
||||
it, leave it OUT of the Copilot half of the pair — the phrase does nothing there
|
||||
and agent-audit's KyberforgeCopilot.ProactivePhrase grades it a hard FAIL. The
|
||||
pair must describe the same job; it does not have to be byte-identical.
|
||||
Example: "Use when a diff needs checking for injected credentials before it
|
||||
merges. Not prose or style linting -> `lint-runner`." -->
|
||||
|
||||
<!-- tools: Read, Bash, Grep
|
||||
Optional. Allowlist of tool names: a comma-separated string or a YAML list.
|
||||
Restrict it to what the agent actually needs. Omit only when it needs them
|
||||
all — omitting inherits every tool from the 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
|
||||
Listing any of them is a finding: agent-audit enforces the flat rule. -->
|
||||
|
||||
<!-- 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: Edit, Write, NotebookEdit
|
||||
Optional. Denylist, applied before the tools allowlist and taking precedence over it.
|
||||
Accepts a YAML list or a delimited string; use the comma-separated string form for
|
||||
consistency with the plugin-scope agents in this repo.
|
||||
Supports mcp__* glob patterns (e.g. mcp__filesystem__* to block all filesystem tools).
|
||||
Denies only the tools it names — it does not deny Bash, so an agent that inherits
|
||||
Bash can still write via a shell redirect. State read-only intent in the body too. -->
|
||||
|
||||
<!-- 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.
|
||||
|
||||
<!-- Delegate, don't restate. If an installed skill already owns a procedure this agent
|
||||
needs, name it ("invoke `git-commits`") instead of transcribing it — a body that
|
||||
restates a procedure an invocable skill owns is an agent-audit FAIL. One job per
|
||||
agent. Delete this comment before shipping. -->
|
||||
|
||||
## 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.
|
||||
|
||||
## Errors
|
||||
|
||||
FILL IN: What does the agent do on malformed, missing or contradictory input?
|
||||
State whether it stops and reports, or degrades to a named fallback — and what it
|
||||
tells the caller either way. An agent with no error handling invents a recovery,
|
||||
and an invented recovery is invisible until the output is wrong.
|
||||
@@ -1,81 +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: Use when <trigger>. <One capability clause.> Not <thing> -> <name>.
|
||||
<!-- Required. Used by the runtime for automatic agent selection, and preloaded into
|
||||
every session whether or not this agent is ever used. Three parts, nothing else:
|
||||
a trigger clause opening "Use when", at most one capability clause, and a
|
||||
boundary clause naming a real sibling skill or agent.
|
||||
250 characters is the target, 400 the hard ceiling (ADR-0020).
|
||||
Do not open with an action verb ("Reviews...", "Analyzes...") — that rule was deleted.
|
||||
Never write "Use proactively" here. It steers the Claude Code runtime and does nothing
|
||||
in Copilot, and agent-audit's KyberforgeCopilot.ProactivePhrase grades it a hard FAIL.
|
||||
Otherwise keep the wording matched to the Claude Code half of the pair: agent-audit
|
||||
checks that both halves describe the same job, not that they are byte-identical, so
|
||||
dropping the CC-only phrase here is not a pair-consistency finding.
|
||||
Example: "Use when a diff needs checking for injected credentials before it
|
||||
merges. Not prose or style linting -> `lint-runner`." -->
|
||||
|
||||
<!-- 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.
|
||||
|
||||
<!-- Delegate, don't restate. If an installed skill already owns a procedure this agent
|
||||
needs, name it ("invoke `git-commits`") instead of transcribing it — a body that
|
||||
restates a procedure an invocable skill owns is an agent-audit FAIL. One job per
|
||||
agent. Delete this comment before shipping. -->
|
||||
|
||||
## 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.
|
||||
|
||||
## Errors
|
||||
|
||||
FILL IN: What does the agent do on malformed, missing or contradictory input?
|
||||
State whether it stops and reports, or degrades to a named fallback — and what it
|
||||
tells the caller either way. An agent with no error handling invents a recovery,
|
||||
and an invented recovery is invisible until the output is wrong.
|
||||
@@ -1,163 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- claude-code-subagents-docs
|
||||
- github-custom-agents-configuration
|
||||
---
|
||||
|
||||
# The agent description and body contract
|
||||
|
||||
House contract. The counts and the boundary targets are enforced by
|
||||
`agent-audit`'s `scripts/validate.sh`; the prose patterns by the Vale styles it bundles; the
|
||||
judgment calls by its reference files.
|
||||
|
||||
## Why the budget exists
|
||||
|
||||
An agent's `name` and `description` is loaded into every session's context at startup, whether or
|
||||
not the agent is ever delegated to — the same cost a skill's description carries, so agents take
|
||||
the same numbers. The body is different: it is not loaded into the caller's conversation at all,
|
||||
it *becomes the system prompt of a fresh context* when the agent runs. That is why the body has no
|
||||
word gate here and a skill body has one.
|
||||
|
||||
## Description
|
||||
|
||||
A description carries exactly three things:
|
||||
|
||||
1. **Trigger clause** — when to delegate, imperative: "Use when …", never "This agent …". Describe
|
||||
the user's intent and the triggering condition, not the agent's internal mechanics.
|
||||
2. **At most one capability clause** — what it does, one clause, no enumeration. Be specific
|
||||
("reviews a diff for injected credentials", not "helps with security").
|
||||
3. **Boundary clause** — form: `Not <thing> -> <name>.` Add one only where a near-miss agent or
|
||||
skill could steal delegations.
|
||||
|
||||
Banned from a description; move it to the body or to `README.md`:
|
||||
|
||||
- Capability enumeration or feature lists
|
||||
- Per-scope emission mechanics — which files the author skill writes at which scope changes no
|
||||
delegation decision
|
||||
- Output-format detail ("Produces a compact findings report with Why and Fix per finding")
|
||||
- Composition or architecture notes ("composes X rather than duplicating Y", "cross-cutting")
|
||||
- Implementation detail ("Self-validates via a bundled deterministic script")
|
||||
- Restating the same trigger twice in two registers — a verb list, then the same verbs re-quoted
|
||||
as user phrasings. This is a FAIL, not a suggestion.
|
||||
|
||||
**Do not open with an action verb.** The opener is `Use when`, matching every skill in this corpus,
|
||||
so one router reads one shape.
|
||||
|
||||
**"Use proactively" is Claude Code-only, and conditional even there.** The phrase steers the
|
||||
Claude Code runtime to offer an agent unprompted and does nothing anywhere else, so where it may
|
||||
appear depends on the file:
|
||||
|
||||
| File | Rule |
|
||||
|---|---|
|
||||
| Claude Code `.md` (project/user scope) | Allowed. Add it only where the runtime should delegate without the user naming the agent — an agent invoked by name does not need it, and it costs activations elsewhere when added by reflex. |
|
||||
| Copilot `.agent.md` (project/user scope) | **Never.** Inert there, and `KyberforgeCopilot.ProactivePhrase` grades it a hard FAIL. |
|
||||
| Vendor-neutral `.apm/agents/<name>.agent.md` (plugin/APM scope) | **Never.** Same Vale rule, same hard FAIL — the file matches the `**/*.agent.md` glob, and it compiles to a real Copilot agent downstream. |
|
||||
|
||||
A pair whose Claude Code half carries the phrase and whose Copilot half omits it is correct, not
|
||||
inconsistent: `agent-audit` checks that both halves describe the same job, not that they match
|
||||
word for word.
|
||||
|
||||
Indirect triggers ("even if the user doesn't say X") take a similar conditional at every scope:
|
||||
add one only where the user's natural phrasing genuinely omits the domain word.
|
||||
|
||||
**Boundary targets must resolve, and the notation decides how hard the gate bites.** Route
|
||||
notation — `/name`, or any arrow form (`-> name`, `` -> `name` ``) — is checked
|
||||
unconditionally: an unresolved target there is a blocking ERROR. The prose form ("do not use
|
||||
for X, use `y` instead") is only a SUGGESTION by default, because a bare hyphenated word in a
|
||||
boundary clause is as likely to be a tool, a file format or an English compound as a route. It
|
||||
is promoted to a blocking ERROR only when a second target in the same sentence *does* resolve,
|
||||
which corroborates that the name was meant as a route. So a typo does **not** dangle equally
|
||||
either way — write the arrow when you want the target checked. Targets resolve against a universe
|
||||
built by walking up **from the agent file itself**: the nearest ancestor holding
|
||||
`plugins/*/.apm/{skills,agents}` (or, failing that, the nearest ancestor holding `.git`) contributes
|
||||
every skill and agent under `<root>/plugins/*/`, plus the agent's own apm package and the packages
|
||||
that package declares in `apm.yml` under `dependencies.apm`. A sibling plugin in the same monorepo
|
||||
therefore resolves; a skill in an unrelated repo does not. A target outside that universe sends the
|
||||
router nowhere — a blocking failure in arrow or `/name` form, and in prose form only a SUGGESTION
|
||||
nobody is forced to act on, which is the worse outcome because it ships. Verify it before writing
|
||||
it — do not invent a plausible sibling.
|
||||
|
||||
**Never let a hyphenated routing target wrap across lines in a folded `>` scalar.** YAML folding
|
||||
replaces the newline with a space, so `gitea-labels-` at the end of one line and `milestones` at
|
||||
the start of the next fold into `gitea-labels- milestones`. The gate then reads the target as
|
||||
`gitea-labels`, finds no such skill, and reports it dangling — nothing in the source lines looks
|
||||
wrong. Reflow so the whole name sits on one line. The same applies to any backticked skill or
|
||||
agent name anywhere in a description.
|
||||
|
||||
That universe is the apm marketplace and stops there. A **host built-in is not a routing target**:
|
||||
`/compact`, `/clear` and `/init` are Claude Code slash commands with no counterpart in Copilot CLI
|
||||
or Codex, and `.apm/` source compiles for all three, so routing to one is a portability defect. The
|
||||
gate is right to fail it and there is no allowlist. If a built-in genuinely needs mentioning, write
|
||||
it un-slashed — ``the `compact` built-in`` — which makes no routing claim and is not checked.
|
||||
|
||||
**Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only
|
||||
with YAML folding resolved. Treat 250 as the target: the SUGGESTION tier is what moves the corpus
|
||||
average, the FAIL tier only stops outliers.
|
||||
|
||||
## Body
|
||||
|
||||
Write the body as a direct role instruction, addressed to the agent:
|
||||
|
||||
````markdown
|
||||
You are a <role>. When invoked, <primary action>.
|
||||
|
||||
## Inputs
|
||||
<what the agent is given: files, context, parameters>
|
||||
|
||||
## Process
|
||||
<ordered steps; be explicit where ordering matters>
|
||||
|
||||
## Output
|
||||
<what it produces: format, location, structure>
|
||||
|
||||
## Errors
|
||||
<what to do on malformed, missing or contradictory input: report and stop, or
|
||||
which fallback to take — and what to say to the caller either way>
|
||||
````
|
||||
|
||||
Four required elements: **inputs expected, process steps, output format, error handling.** The
|
||||
last is the one that gets dropped, and dropping it is not neutral: an agent given a malformed
|
||||
input and no instruction invents a recovery, and a subagent's invented recovery is invisible to
|
||||
the caller until the output is wrong. Say explicitly whether the agent stops and reports, or
|
||||
degrades to a named fallback.
|
||||
|
||||
One job per agent. An agent covering two jobs gets delegated to for the wrong one.
|
||||
|
||||
**Delegation discipline replaces the word gate.** A plugin/APM agent is a single file with no
|
||||
sibling `references/` directory: it cannot disclose progressively to itself, so its only way to
|
||||
stay short is to *invoke* rather than *restate*. A body that transcribes a procedure a skill it
|
||||
can invoke already owns is an `agent-audit` FAIL, and the fix is one line — "invoke `<skill>`".
|
||||
|
||||
- Restating: "To commit, check the message against Conventional Commits: type, scope,
|
||||
description; header under 100 chars; …"
|
||||
- Delegating: "Author commits with `git-commits`."
|
||||
|
||||
The same holds for a procedure another agent owns. What belongs in the body is what no invocable
|
||||
skill covers: the agent's role, its boundaries, the order it works in, and the format it returns.
|
||||
|
||||
**State a read-only boundary in prose, not only in frontmatter.** `disallowedTools` denies the
|
||||
tools it names and nothing else — never `Bash`, which an agent with no `tools` field inherits — so
|
||||
an agent fenced only in frontmatter can still write through a shell redirect.
|
||||
|
||||
## Invocation axis
|
||||
|
||||
Decide before writing the description whether the agent is model-delegated (the runtime picks it)
|
||||
or reached only by name (`@agent-<name>`).
|
||||
|
||||
Only Copilot's cloud/IDE format expresses that in frontmatter: `disable-model-invocation: true`
|
||||
requires explicit invocation, and `user-invocable: false` hides an agent from manual invocation.
|
||||
Both live in `.github/copilot/agents/<name>.md` and are inert in the CLI format. Claude Code has
|
||||
no equivalent field, and neither does the vendor-neutral plugin/APM file, so at those scopes a
|
||||
name-invoked agent still needs a description precise enough not to steal delegations — the
|
||||
boundary clause is doing that work.
|
||||
|
||||
## One gate, two measurements
|
||||
|
||||
| Gate | SUGGESTION | FAIL | Counts |
|
||||
|---|---|---|---|
|
||||
| description | 250 chars | 400 chars | the `description:` value only |
|
||||
| body (Copilot limit) | 30,000 chars | — | the body only; content past it is truncated silently |
|
||||
|
||||
The 30,000-character Copilot ceiling is a runtime truncation limit, not a quality target, and it
|
||||
applies to a plugin/APM file too — that file compiles into a real Copilot agent downstream. An
|
||||
agent body long enough to approach it has a delegation defect, not a length problem.
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- context7-websites-code-claude
|
||||
- claude-code-subagents-docs
|
||||
- github-plugins-creating
|
||||
---
|
||||
|
||||
# Creating a new agent
|
||||
|
||||
Return to `SKILL.md` Step 4 once Step 3 below is done — validation, the version bump and commit
|
||||
verification are shared with the improve flow and are not repeated here.
|
||||
|
||||
## 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, a 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.
|
||||
|
||||
`agent-audit` runs the validation in `SKILL.md` Step 4. It ships with the kyberforge plugin and
|
||||
is co-installed with this skill; if it is unavailable, stop and ask the user to install
|
||||
kyberforge before continuing.
|
||||
|
||||
Design for one job per agent. An agent covering two jobs is delegated to for the wrong one.
|
||||
|
||||
## Step 1 — Scaffold
|
||||
|
||||
```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 ~
|
||||
```
|
||||
|
||||
The script resolves scope itself and prints which one it used and every path it wrote — read that
|
||||
output rather than predicting it. It walks up from `<root>` for the nearest ancestor `apm.yml`
|
||||
carrying a top-level `type:` field (`instructions`/`skill`/`hybrid`/`prompts`), which marks a
|
||||
package root and means plugin/APM scope. An `apm.yml` with no `type:` is a marketplace-only
|
||||
manifest: the walk skips it and keeps going. With no such manifest found, `<root>` being exactly
|
||||
`~` (checked directly, no walk-up) is user scope and anything else is project scope. A bare
|
||||
`plugin.json` no longer signals plugin scope — that path was replaced outright, not made
|
||||
dual-mode, and falls through to project scope.
|
||||
|
||||
The script is file-by-file no-op: it skips any file that already exists, so re-running it to
|
||||
complete a partial pair is safe.
|
||||
|
||||
If the script or a template under `assets/templates/` has to change to support this agent — a new
|
||||
scope, a new scaffolded field, different output — read `references/scripts.md` first. Its
|
||||
conventions are asserted by `tests/new-agent.bats`, and an edit that ignores them fails the suite.
|
||||
|
||||
## Step 2 — Fill in the file(s)
|
||||
|
||||
Take the scope the script reported and read the matching reference — `SKILL.md` Step 2 has the
|
||||
table. That file carries the field rules and the pre-audit checklist for this scope; the other one
|
||||
describes fields this run cannot use.
|
||||
|
||||
Every scaffolded file, at every scope:
|
||||
|
||||
1. Replace each `FILL IN:` placeholder.
|
||||
2. Delete every `<!-- ... -->` template comment from the frontmatter. `apm compile` copies plugin
|
||||
frontmatter verbatim and HTML comments are not valid YAML, so a leftover comment breaks the
|
||||
file downstream on both harnesses.
|
||||
3. Write the `description` against `references/contract.md` and the system prompt body against its
|
||||
Body section.
|
||||
|
||||
## Step 3 — Populate or delete `sources.md`
|
||||
|
||||
Plugin/APM scope only — skip at project and user scope, which have no package root to hold the
|
||||
file.
|
||||
|
||||
The scaffold writes a commented `sources.md` skeleton at the package root, alongside `apm.yml` and
|
||||
not inside `.apm/agents/`, so that tooling scanning that directory for agent definitions does not
|
||||
treat it as an agent missing its frontmatter (ADR-0010).
|
||||
|
||||
If a research `sources.md` is present in the conversation context, filter it to entries with
|
||||
`` `extracted` `` status, work out which agent file each one contributed to, and fill in the
|
||||
skeleton following the commented format already in the file. Paths in `Contributing files:` are
|
||||
relative to the package root. Each slug must match an H2 heading and must also appear in the
|
||||
`source_keys` list of every file named under its `Contributing files:`.
|
||||
|
||||
If no research sources are in context, delete `sources.md`.
|
||||
|
||||
Then return to `SKILL.md` Step 4.
|
||||
@@ -1,60 +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.
|
||||
|
||||
## Which fields exist where
|
||||
|
||||
Field rules are per scope and live with the scope: `references/plugin-scope.md` for the single
|
||||
vendor-neutral file, `references/project-user-scope.md` for the Claude Code / Copilot pair. Read
|
||||
one, not both. The short version is that plugin/APM frontmatter is an allowlist read from
|
||||
`agent-audit`'s `references/field-inventory.md`, narrow because `apm compile` copies frontmatter
|
||||
verbatim to every target (ADR-0016), while project and user scope carry the full per-provider
|
||||
field sets.
|
||||
|
||||
## Scoped identifiers (Claude Code plugin agents only)
|
||||
|
||||
Plugin agents in **subdirectories** get compound identifiers:
|
||||
|
||||
```
|
||||
plugins/my-plugin/.apm/agents/review/security.agent.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.
|
||||
@@ -1,88 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- claude-code-subagents-docs
|
||||
---
|
||||
|
||||
# Improving an existing agent
|
||||
|
||||
Return to `SKILL.md` Step 4 once Step 4 below is done — validation, the version bump and commit
|
||||
verification are shared with the create flow and are not repeated here.
|
||||
|
||||
## Step 1 — Verify inputs
|
||||
|
||||
Confirm the agent file (or, at project and user scope, the pair) exists and that at least one
|
||||
improvement signal is present in the conversation or in a referenced file.
|
||||
|
||||
If no signals are present, stop: "This skill applies existing signals to an agent. For a blind
|
||||
review, run `agent-audit` instead."
|
||||
|
||||
`agent-audit` runs the validation in `SKILL.md` Step 4 and is co-installed with this skill; if
|
||||
it is unavailable, stop and ask the user to install the kyberforge plugin before continuing.
|
||||
|
||||
**Partial pair — project and user scope only.** If one provider file exists and the other does
|
||||
not, scaffold the missing one with `bash scripts/new-agent.sh <name> <root>` (file-by-file no-op)
|
||||
and continue. Plugin/APM scope is a single file and has no partial state.
|
||||
|
||||
## Step 2 — Gather and group signals
|
||||
|
||||
Read the current file(s), then collect every signal from the conversation and from any path the
|
||||
user referenced.
|
||||
|
||||
Group signals by **root cause**, not by symptom. Patching per symptom is the default failure mode:
|
||||
three complaints often trace to one missing instruction. Ask: "What single gap in this agent
|
||||
causes this cluster?" One root cause, one fix.
|
||||
|
||||
```text
|
||||
Example:
|
||||
- User feedback: the agent keeps trying to push to the remote
|
||||
- Session context: no scope boundary in the system prompt
|
||||
→ Root cause: the system prompt has no git scope constraint → fix: add an 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, so no approval checkpoint is
|
||||
needed.
|
||||
|
||||
## Step 4 — Apply changes
|
||||
|
||||
Edit whichever file the signals point to.
|
||||
|
||||
**Generalize, do not patch.** Fix the underlying gap, not the one example that failed. A fix
|
||||
scoped to the cases you have seen overfits and performs worse on new input.
|
||||
|
||||
**Delegate rather than grow.** An agent body has no word ceiling, but a body that restates a
|
||||
procedure a skill it can invoke already owns is an `agent-audit` FAIL. When a signal reports a
|
||||
missing procedure, check first whether an installed skill owns it and name that skill instead of
|
||||
transcribing it. See `references/contract.md`.
|
||||
|
||||
The delegation check is not a length brake — it fires only on procedure an invocable skill already
|
||||
owns, and says nothing about original prose. That brake is judgment, and it is the only one left:
|
||||
for every sentence you add, ask "would the agent get this wrong without it?" and delete it if the
|
||||
answer is no.
|
||||
|
||||
**Explain the why.** Reasoning-based instructions outperform rigid directives. A rule written in
|
||||
all caps (ALWAYS/NEVER) is usually better reframed as why the behaviour matters, so the agent can
|
||||
apply judgment at the edges.
|
||||
|
||||
**Retrofit before extending.** Any agent whose description does not meet the contract has to be
|
||||
brought into compliance before any other edit lands — the gates are hot and carry no baseline file,
|
||||
so a one-line fix to a non-compliant agent cannot be committed until its description meets
|
||||
`references/contract.md`.
|
||||
Treat that retrofit as part of the same change, not a follow-up.
|
||||
|
||||
**Re-check the scope rules.** Read the reference for the resolved scope (`SKILL.md` Step 2) and
|
||||
confirm the edit introduced no field that scope forbids, and dropped no `disallowedTools` fence
|
||||
that was already there.
|
||||
|
||||
If the edit adds or removes research-sourced content, update `source_keys` in the edited file and
|
||||
the matching `sources.md` entry — the create flow's Step 3 has the rules.
|
||||
|
||||
**Check for regressions before handing back.** `SKILL.md` Step 4 tells you to resolve every FAIL,
|
||||
which says nothing about a check that passed *before* these edits and no longer does. Compare the
|
||||
closing `agent-audit` against the agent's pre-edit state — a PASS that has become a SUGGESTION, or
|
||||
a SUGGESTION that has become a FAIL, is damage this flow caused and is in scope for it. Only the
|
||||
improve flow can make that comparison; the create flow has no prior state to compare against.
|
||||
|
||||
Then return to `SKILL.md` Step 4.
|
||||
@@ -1,74 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- context7-websites-code-claude
|
||||
- claude-code-plugins-docs
|
||||
---
|
||||
|
||||
# Plugin/APM scope — the single vendor-neutral file
|
||||
|
||||
One file, no counterpart: `<package-root>/.apm/agents/<name>.agent.md`. `apm compile` emits it to
|
||||
both the Claude Code and the Copilot CLI target. The `.agent.md` extension here is convention, not
|
||||
a Copilot marker — the file is vendor-neutral.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
The permitted keys are the `apm-agent-allowlist` section of `agent-audit`'s
|
||||
`references/field-inventory.md`. Read them from there as data — that section is the single source
|
||||
of truth, `agent-audit`'s `validate.sh` parses it at load time, and it changes. Any restatement of
|
||||
the roster, here or in a template or in script output, goes stale one step further out than the
|
||||
list itself.
|
||||
|
||||
- `name` — kebab-case, must equal the filename stem, unique within the scope.
|
||||
- `description` — write it against `references/contract.md`.
|
||||
- Everything else — check the allowlist section before adding a key. A key outside it fails the
|
||||
audit.
|
||||
|
||||
**Why the list is narrow.** `apm compile` copies frontmatter verbatim to every target with no
|
||||
per-target integrator, so a harness-specific value is wrong on at least one of them (ADR-0016).
|
||||
The rule is about a field's *shape*, not a fixed roster:
|
||||
|
||||
- `tools` is an **allowlist** whose vocabulary differs per harness — Claude Code names its own
|
||||
tools, Copilot CLI uses aliases (`execute`/`read`/`edit`/`search`/`agent`/`web`) — so one value
|
||||
is wrong on one target. It stays out. Omitting it means inherit-all-tools on both, which is
|
||||
never wrong.
|
||||
- `disallowedTools` is a **denylist**, and denying by name cannot fail that way: a name the other
|
||||
harness does not recognise denies nothing, so the worst case is a missing fence, never a wrongly
|
||||
granted capability. That asymmetry is the whole exception (ADR-0016's 2026-08-14 amendment).
|
||||
Claude Code honours it for plugin subagents; the three fields plugin agents do silently ignore
|
||||
are `hooks`, `mcpServers` and `permissionMode`, and this is not one of them. Copilot's handling
|
||||
of the key is unconfirmed, which ADR-0016 accepts as a stated risk.
|
||||
|
||||
Its syntax is the same at every scope, and this is the one scope that cannot reach it anywhere
|
||||
else: MCP tools are denied as `mcp__<server>`, `mcp__<server>__*` or `mcp__*`; both a YAML list
|
||||
and a delimited string are accepted, and this repo writes the comma-separated string form
|
||||
(`disallowedTools: Edit, Write, NotebookEdit`) — match it.
|
||||
- The Claude-only knobs (`isolation`, `maxTurns`, `effort`, `memory`, `permissionMode`, `skills`,
|
||||
`color`, `initialPrompt`, `background`, `hooks`, `mcpServers`) have no Copilot equivalent and
|
||||
are never written to this file at all. "Silently ignored at plugin scope" is the wrong framing:
|
||||
they are absent, not tolerated. To use any of them, copy the agent to `.claude/agents/`
|
||||
(project scope) or `~/.claude/agents/` (user scope).
|
||||
|
||||
Write `disallowedTools` on every read-only plugin-scope agent — and say the agent is read-only in
|
||||
the body as well, because the fence does not cover the inherited `Bash` tool.
|
||||
|
||||
`source_keys` is provenance metadata, not a runtime field: both harnesses ignore it. Add it only
|
||||
when research sources informed the agent, with slugs matching H2 headings in the package root's
|
||||
`sources.md`.
|
||||
|
||||
## Body
|
||||
|
||||
Follow the Body section of `references/contract.md`: role instruction, one job, and delegation
|
||||
to installed skills instead of transcribed procedure.
|
||||
|
||||
## Before invoking `agent-audit`
|
||||
|
||||
- [ ] `name` kebab-case, matching the filename stem, unique in scope
|
||||
- [ ] `description` written to `references/contract.md`
|
||||
- [ ] Every frontmatter key present in the `apm-agent-allowlist` section — in particular no `tools`
|
||||
- [ ] No `FILL IN:` placeholder and no `<!-- ... -->` template comment anywhere in the file
|
||||
- [ ] System prompt body non-empty, and a read-only agent says so in prose as well as in
|
||||
`disallowedTools`
|
||||
- [ ] Body covers all four required elements: inputs expected, process steps, output format,
|
||||
**error handling** — what the agent does on malformed, missing or contradictory input
|
||||
|
||||
Then return to the flow reference you came from.
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- claude-code-subagents-docs
|
||||
- context7-github-en-copilot
|
||||
- github-custom-agents-configuration
|
||||
- github-cli-plugin-reference
|
||||
---
|
||||
|
||||
# Project and user scope — the Claude Code / Copilot pair
|
||||
|
||||
Two files per agent, written in one pass and kept in step: a Claude Code `.md` and a Copilot CLI
|
||||
`.agent.md`. The system prompt body is the same in both — the agent's task does not change with
|
||||
the provider. The frontmatter is not.
|
||||
|
||||
| Scope | Claude Code | Copilot CLI |
|
||||
|---|---|---|
|
||||
| Project | `.claude/agents/<name>.md` | `.github/agents/<name>.agent.md` |
|
||||
| User | `~/.claude/agents/<name>.md` | `~/.copilot/agents/<name>.agent.md` |
|
||||
|
||||
## Claude Code file
|
||||
|
||||
**`name`** — lowercase letters and hyphens only, unique within the scope. Claude Code discards a
|
||||
duplicate silently.
|
||||
|
||||
**`description`** — write it against `references/contract.md`. It is the primary signal for
|
||||
autonomous delegation.
|
||||
|
||||
**`tools`** — an allowlist. Write it, and restrict it to the tools the agent actually needs;
|
||||
omitting it inherits every tool from the parent, which is the right value only when the agent
|
||||
genuinely needs all of them. Least privilege is the default, not the exception. Use
|
||||
`Agent(type1,type2)`
|
||||
to restrict which subagent types this agent may spawn, and omit `Agent` entirely to stop it
|
||||
spawning any. Five tools reach no subagent whatever this field says — `AskUserQuestion`,
|
||||
`EnterPlanMode`, `ExitPlanMode`, `ScheduleWakeup` and `WaitForMcpServers` — so listing one buys
|
||||
nothing. The single exception is `ExitPlanMode`, available when the parent session runs
|
||||
`permissionMode: plan`.
|
||||
|
||||
**`disallowedTools`** — a denylist, applied before `tools` and taking precedence over it. Supports
|
||||
`mcp__<server>`, `mcp__<server>__*` and `mcp__*` globs. Both a YAML list and a delimited string
|
||||
are accepted; this repo writes the comma-separated string form (`disallowedTools: Edit, Write,
|
||||
NotebookEdit`) — match it.
|
||||
|
||||
**`model`** — set it when the agent needs a different capability tier (`haiku` for fast lookups,
|
||||
`opus` for deep reasoning). Resolution order is `CLAUDE_CODE_SUBAGENT_MODEL` → the per-invocation
|
||||
parameter → this field → the main session model, so the frontmatter value is a low-priority
|
||||
default rather than a guarantee.
|
||||
|
||||
Optional fields worth considering, none of which exist at plugin/APM scope:
|
||||
|
||||
- `maxTurns` — cap agentic turns on a bounded task, to stop a runaway
|
||||
- `effort` — `low` for a single lookup, `high` or above for multi-file analysis; 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
|
||||
- `skills` — skill names preloaded at agent startup; unrelated to the `source_keys` metadata field
|
||||
- `color` — the UI tile colour (`red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`,
|
||||
`cyan`)
|
||||
- `background` — `true` forces background execution
|
||||
- `initialPrompt` — auto-submitted as the first turn when the agent activates as the main session
|
||||
thread; set it only for a main-thread agent, never for a subagent
|
||||
|
||||
`hooks`, `mcpServers` and `permissionMode` are honoured at these two scopes and nowhere else — a
|
||||
plugin agent carrying them is ignored silently.
|
||||
|
||||
A subdirectory under `agents/` does not affect the agent's name at these scopes; it does at plugin
|
||||
scope, which is one reason `references/deployment-modes.md` recommends keeping agents flat.
|
||||
|
||||
## Copilot file
|
||||
|
||||
Two Copilot formats exist, with different paths and different field sets. Pick one:
|
||||
|
||||
**CLI format** — what the scaffold writes.
|
||||
|
||||
- Path: `.github/agents/<name>.agent.md` (project) or `~/.copilot/agents/<name>.agent.md` (user)
|
||||
- The `.agent.md` extension is mandatory: Copilot CLI does not pick up a plain `.md` file in
|
||||
`agents/`, and fails silently rather than reporting it
|
||||
- Fields: `name` (required, must equal the filename stem), `description` (required), `tools`
|
||||
(optional)
|
||||
- `tools` uses Copilot aliases, not Claude tool names: `execute` (shell), `read`, `edit`,
|
||||
`search`, `agent`, `web`; MCP tools as `server-name/tool-name` or `server-name/*`
|
||||
|
||||
**Cloud/IDE format** — for Copilot Chat in VS Code or on GitHub.com.
|
||||
|
||||
- Path: `.github/copilot/agents/<name>.md` — a plain `.md`, in a different directory
|
||||
- Adds `target` (`vscode`, `github-copilot`, or omit for both), `user-invocable`,
|
||||
`disable-model-invocation` and `mcp-servers` (processed by the cloud runtime, ignored in VS
|
||||
Code). These four are inert in the CLI format — do not write them there
|
||||
- This is the only format that can express the invocation axis in frontmatter; see the Invocation
|
||||
axis section of `references/contract.md`
|
||||
|
||||
Both formats truncate a body past **30,000 characters** silently.
|
||||
|
||||
Copilot has no `permissionMode`, `maxTurns`, `isolation`, `memory`, `effort`, `hooks` or
|
||||
`mcpServers`. Never let those cross over from the Claude Code file.
|
||||
|
||||
## Before invoking `agent-audit`
|
||||
|
||||
Both files:
|
||||
|
||||
- [ ] `name` present and kebab-case; `description` written to `references/contract.md`
|
||||
- [ ] System prompt body present, non-empty and equivalent across the pair
|
||||
- [ ] Body covers all four required elements: inputs expected, process steps, output format,
|
||||
**error handling** — what the agent does on malformed, missing or contradictory input
|
||||
- [ ] No `FILL IN:` placeholder and no `<!-- ... -->` template comment left
|
||||
|
||||
Copilot file only:
|
||||
|
||||
- [ ] Extension is `.agent.md` (CLI format), and `name` matches the filename stem
|
||||
- [ ] No Claude Code-only field present
|
||||
- [ ] Body under 30,000 characters
|
||||
|
||||
Then return to the flow reference you came from.
|
||||
@@ -1,43 +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`).
|
||||
- **No restated field rosters** — no script output, in `--help` or in next-steps guidance, enumerates permitted, forbidden, or required frontmatter fields. Point at the `apm-agent-allowlist` section of `agent-audit`'s `references/field-inventory.md`, which `agent-audit`'s `validate.sh` reads from there as data. A roster copied into script output goes stale one step further out than the list itself: the next-steps hint `(name, description, model, body only)` kept printing after ADR-0016's 2026-08-14 amendment added `disallowedTools` to the permitted set. `tests/new-agent.bats` enforces this for the plugin/APM branch — naming some allowlisted fields but not all is a failure.
|
||||
|
||||
## 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.
|
||||
@@ -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/create.md, references/deployment-modes.md, references/plugin-scope.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, references/plugin-scope.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/create.md, references/improve.md, references/contract.md, references/deployment-modes.md, references/project-user-scope.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:** references/deployment-modes.md, references/project-user-scope.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:** references/contract.md, references/deployment-modes.md, references/project-user-scope.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:** references/deployment-modes.md, references/project-user-scope.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:** references/create.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`
|
||||
@@ -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`) — source-only. `scripts/sync-plugin-content.sh` strips `<category>/<name>/tests` from the generated mirror (ADR-0017), so this file exists in a repo checkout of `.apm/skills/agent-author/` and not in an installed plugin.
|
||||
@@ -1,303 +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; apm compile copies
|
||||
its frontmatter verbatim to every target with no
|
||||
per-target field integrator, so the permitted
|
||||
field set is narrow — see the apm-agent-allowlist
|
||||
section of agent-audit's
|
||||
references/field-inventory.md and 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
|
||||
}
|
||||
|
||||
# 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). `mapfile`/`readarray` would need bash 4.0+, which
|
||||
# macOS's stock /bin/bash 3.2 is not; a here-string `read` splits the single
|
||||
# line without it. Same form as skill-author's new-skill.sh, deliberately.
|
||||
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.template" > "$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
|
||||
|
||||
# Next-steps guidance names no frontmatter fields, by rule (see references/scripts.md).
|
||||
# A roster restated in terminal output goes stale one step further out than the list
|
||||
# itself: the old "(name, description, model, body only)" hint outlived ADR-0016's
|
||||
# 2026-08-14 amendment, which added disallowedTools to the permitted set. Point at the
|
||||
# scaffolded file's own comments for what to fill, and at agent-audit's validate.sh —
|
||||
# which reads the allowlist from field-inventory.md as data — for what is permitted.
|
||||
AUDIT_SCRIPTS="$(cd "$SKILL_ROOT/../agent-audit/scripts" 2>/dev/null && pwd || true)"
|
||||
if [[ -n "$AUDIT_SCRIPTS" && -f "$AUDIT_SCRIPTS/validate.sh" ]]; then
|
||||
VALIDATE_HINT="$AUDIT_SCRIPTS/validate.sh"
|
||||
else
|
||||
VALIDATE_HINT="agent-audit's scripts/validate.sh"
|
||||
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 every FILL IN: placeholder. Optional fields are" >&2
|
||||
echo " scaffolded there as commented blocks; uncomment the ones that apply." >&2
|
||||
echo " Description: 250 chars target / 400 ceiling (ADR-0020). The body has no" >&2
|
||||
echo " word gate — delegate to a skill instead of restating what it does." >&2
|
||||
echo " 2. Populate $SOURCES_DIR/sources.md with research sources, or delete it" >&2
|
||||
echo " 3. Validate: $VALIDATE_HINT $APM_FILE" >&2
|
||||
echo " It checks the frontmatter against the apm-agent-allowlist section of" >&2
|
||||
echo " agent-audit's references/field-inventory.md, the authoritative field list." >&2
|
||||
else
|
||||
echo " 1. Fill in $CC_FILE — replace every FILL IN: placeholder. Optional fields are" >&2
|
||||
echo " scaffolded there as commented blocks; uncomment the ones that apply." >&2
|
||||
echo " Description: 250 chars target / 400 ceiling (ADR-0020). The body has no" >&2
|
||||
echo " word gate — delegate to a skill instead of restating what it does." >&2
|
||||
echo " 2. Fill in $CP_FILE — same, and heed its closing comment: the Claude Code-only" >&2
|
||||
echo " fields it names must not cross over from the file above." >&2
|
||||
echo " 3. Validate: run $VALIDATE_HINT on each file" >&2
|
||||
fi
|
||||
fi
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
name: apm-install
|
||||
description: >
|
||||
Use when installing, pinning, or upgrading the apm (Agent Package Manager)
|
||||
CLI itself, or installing and managing an agent runtime apm drives. Not
|
||||
authoring, publishing, auditing, or dependency installation for an apm
|
||||
package -> `apm-workflow`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
category: apm
|
||||
source_keys:
|
||||
- context7-microsoft-apm
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
- apm never executes an agent itself — it only installs and manages the runtimes that do. Installing apm alone leaves the user with a package manager and no working agent CLI, so Step 2 is required whenever the user actually wants one; skip it only when they explicitly want the package manager alone.
|
||||
- `apm runtime setup copilot` needs Node.js v22+ and npm v10+ already on `PATH`; apm will not install them for you.
|
||||
|
||||
## Step 1 — Install the apm CLI
|
||||
|
||||
If `apm --version` already answers and the user is not pinning or upgrading, skip to Step 2.
|
||||
|
||||
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: `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: `curl -sSL https://aka.ms/apm-unix | APM_INSTALL_DIR=$HOME/.local/bin sh`.
|
||||
- **Air-gapped mirror / GitHub Enterprise** — an air-gapped host cannot reach `aka.ms` at all, so get `install.sh` onto the box and run it from disk instead of piping. Point it at the mirror with `APM_RELEASE_BASE_URL` and pin `VERSION`: `APM_RELEASE_BASE_URL=https://mirror.corp/apm VERSION=v1.2.3 sh install.sh`; add `APM_RELEASE_METADATA_URL` instead if you leave `VERSION` unset. `GITHUB_URL` is the GitHub Enterprise host, not a release mirror. All four are ordinary environment variables that also work through the pipe — running from disk is a network constraint, not a script one.
|
||||
- **pip** — `pip install apm-cli` requires Python 3.10+. Not on an externally-managed (PEP 668) Python such as Debian or Ubuntu, where it hard-fails with `error: externally-managed-environment`; use pipx below. The quick-install script has no Python prerequisite, so prefer it unless the environment is pip-first.
|
||||
- **pipx** — `pipx install apm-cli` on those PEP 668 environments. Same PyPI package, but pipx builds an isolated venv and exposes `apm` on `PATH`.
|
||||
- **Manual** — download the platform archive from the GitHub releases page, extract, and place the binary on `PATH`.
|
||||
|
||||
Verify with `apm --version`.
|
||||
|
||||
## Step 2 — 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.
|
||||
@@ -1,17 +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`
|
||||
|
||||
## apm-github-repo
|
||||
|
||||
- **URL:** https://github.com/microsoft/apm
|
||||
- **Description:** APM's own Python source (`src/apm_cli/`), read directly for schema and compile-mapping ground truth where Context7's doc snippets were thin. Informed the research doc's primitive-schema and releasing pages; nothing in this skill's install/runtime guidance derives from it.
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md
|
||||
- **Contributing files:** (none)
|
||||
- **Status:** `referenced`
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: apm-workflow
|
||||
description: >
|
||||
Use when authoring, installing, or publishing an apm package, its apm.yml and
|
||||
the dependencies it declares, or an apm marketplace — even when the user does
|
||||
not say "apm". Not the apm binary or an agent runtime -> `apm-install`.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
category: apm
|
||||
source_keys:
|
||||
- context7-microsoft-apm
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
- MCP server secrets in `apm.yml` (headers, env vars) must use `${VAR}` indirection, never literal values, so they resolve at install or runtime and are never committed.
|
||||
- `apm experimental enable registries` must run before a `registries:` block or `registry.*` config takes effect anywhere — configure, install or publish. Without it, declaring one silently does nothing: no error, no warning.
|
||||
- `apm.yml`'s `type:` selects which primitives are processed and is never checked against what `.apm/` holds, so `apm install` and `apm compile` can exit 0 having shipped none of the ones you expected. Set it to cover every primitive the package ships, and confirm the deployed output, not the exit code. Mechanics: `references/configure.md`.
|
||||
|
||||
## Step 1 — Dispatch
|
||||
|
||||
| Condition | Flow | Reference |
|
||||
|---|---|---|
|
||||
| Author or edit `apm.yml`, or scaffold a new package (`apm plugin init`) | configure | `references/configure.md` |
|
||||
| Resolve or fetch the dependencies `apm.yml` declares (`apm install`, `apm install [PACKAGE_REF]`) | install | `references/install.md` |
|
||||
| Build a marketplace, register a package into it (local: hand-edit `apm.yml`; remote: `apm marketplace package add`), or register someone else's as a consumer (`apm marketplace init/check/package add/add`) | marketplace | `references/marketplace.md` |
|
||||
| Generate per-target output, bundle, or publish (`apm compile`, `apm pack`, `apm publish`) | compile | `references/compile.md` |
|
||||
| Validate integrity/policy or wire a CI gate (`apm audit`, `apm audit --ci`) | audit | `references/audit.md` |
|
||||
|
||||
Read the reference file matching the resolved flow — it carries that flow's own traps, and names a sibling file wherever one flow genuinely depends on another's detail.
|
||||
|
||||
## 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.
|
||||
@@ -1,59 +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.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- 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.
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
topic: compile
|
||||
source_keys:
|
||||
- context7-microsoft-apm
|
||||
---
|
||||
|
||||
## Compile
|
||||
|
||||
```bash
|
||||
apm compile # fails on skill/agent-only packages — see Gotchas
|
||||
apm compile --clean # zero-write sanity check; use for skill/agent-only packages
|
||||
apm compile --clean --dry-run # pure preview, no writes
|
||||
```
|
||||
|
||||
Compiles `.apm/instructions/` + `.apm/agents/*.agent.md` primitives into consumer-side context files (AGENTS.md/CLAUDE.md CONTEXT files) for the deployment target, per the `compilation:` block in `apm.yml`. This is the consumer/deployment side — it is NOT the producer of `plugin.json`/`marketplace.json`; that's `apm pack`'s job (below). Run `apm compile` after any change to `.apm/instructions/`/`.apm/agents/` 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.
|
||||
|
||||
### `.mcp.json` merge mechanics
|
||||
|
||||
`apm pack` reads a package's root-level `.mcp.json` directly — no `dependencies.mcp` entry in `apm.yml` is needed for simple passthrough. Behavior is content-aware:
|
||||
|
||||
- An empty `.mcp.json` (`{"mcpServers": {}}`) is NOT included in the packed output at all.
|
||||
- A populated one gets its `mcpServers` content merged directly into the compiled `plugin.json`, but only for the `claude` target.
|
||||
- The `copilot` target's compiled `plugin.json` OMITS `mcpServers` entirely — it isn't part of Copilot's plugin manifest schema.
|
||||
|
||||
`dependencies.mcp` in `apm.yml` is for a different purpose — declaring a remote MCP-server package as an APM dependency — not local `.mcp.json` passthrough.
|
||||
|
||||
### `includes: auto` and the packed bundle
|
||||
|
||||
`includes: auto` does NOT sweep generic root-level passthrough files (README.md, docs/, sources.md, config files, etc.) into the `apm pack` distribution bundle (`build/<name>-<version>`) — only `.apm/` primitives, the compiled `plugin.json`, and the content-aware `.mcp.json` handling above make it into that bundle. This does not affect Claude Code's own plugin loading, which reads a plugin's working directory directly via its `source:` path in `marketplace.json`, not the `apm pack` bundle — but it matters for anyone relying on the packed bundle for distribution via `apm install`.
|
||||
|
||||
Run `apm audit` after compile/pack, not before — audit scans deployed/compiled output, not the source `apm.yml` manifest. Plain `apm audit` is not the CI-equivalent pass (that is `apm audit --ci`, which checks strictly more); 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.
|
||||
|
||||
Publishing to a named registry requires `apm experimental enable registries` to have already run — see `SKILL.md`'s Gotchas for the precondition and its silent-no-op failure mode.
|
||||
|
||||
## 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
|
||||
|
||||
- Plain `apm compile` HARD-FAILS with exit 1 and `No instruction files found in .apm/ directory` on any package that has skills/agents but no `.apm/instructions/` content — the common case for a skill-only or skill+agent package. Use `apm compile --clean` as the zero-write sanity-check substitute: it sets `allow_empty=True` internally and exits 0 cleanly with zero writes; add `--dry-run` for a pure preview. `compile` ≠ `pack` — see above.
|
||||
- `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.
|
||||
@@ -1,88 +0,0 @@
|
||||
---
|
||||
topic: configure
|
||||
source_keys:
|
||||
- context7-microsoft-apm
|
||||
---
|
||||
|
||||
## Scaffolding a new package
|
||||
|
||||
```bash
|
||||
apm plugin init --yes --target claude,copilot
|
||||
```
|
||||
|
||||
Run from inside the target package directory, with no positional name argument (see Gotchas). Creates `apm.yml` + `plugin.json` in the current directory — it does NOT scaffold a `.apm/` skeleton. Primitive subdirectories (`.apm/skills/`, `.apm/agents/`, `.apm/hooks/`) must be created manually as content is migrated into them. 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`; selects how the package is processed at install/compile time. It is a routing selector, not a constraint on what `.apm/` may contain (see Gotchas)
|
||||
- `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; it is not scoped down to what's relevant, so anything narrower needs an explicit repo-path list. Note: `auto` also does not sweep generic root-level passthrough files (README.md, docs/, sources.md, config files) into the `apm pack` distribution bundle — see `references/compile.md`
|
||||
- `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
|
||||
|
||||
## Bumping a package's own version (repo policy)
|
||||
|
||||
apm ships no version-bump command, so `version:` in a package's own `apm.yml` is a hand edit.
|
||||
Policy: **bump a package's own `apm.yml` `version:` whenever anything that reaches its compiled
|
||||
output changes.** Two triggers, not one:
|
||||
|
||||
- **Its `.apm/` content** — a new or removed skill/agent/hook, or a substantive edit to an
|
||||
existing one.
|
||||
- **Its own `apm.yml` manifest metadata** — `description`, `keywords`, `author`, `license`,
|
||||
`homepage`, `repository`. These are compiled verbatim into `.claude-plugin/plugin.json` and
|
||||
`.github/plugin/plugin.json`, so editing them republishes the package's public description
|
||||
under an unchanged version number, which is the same defect as shipping changed content
|
||||
silently. Purely local edits that reach no compiled output — a `README.md`, a `docs/` page —
|
||||
do not require a bump on their own.
|
||||
|
||||
The version belongs to the package, not to the repo: editing `plugins/foo/.apm/` never bumps
|
||||
`plugins/bar/apm.yml`.
|
||||
|
||||
Under a `per_package` strategy the same number is also carried in the catalog's
|
||||
`marketplace.packages[]` entry, so both copies move together in the same commit. The catalog's own
|
||||
version follows a separate rule — see `references/marketplace.md`. `apm pack --check-versions`
|
||||
fails the push when a package's version disagrees with the configured strategy, so a bump applied
|
||||
in only one of the two places is caught, but a bump skipped in both is not: nothing infers intent
|
||||
from a content diff.
|
||||
|
||||
## 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 (headers, env vars) in `apm.yml`, never literal values — 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 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.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `apm.yml`'s `type:` field validates nothing about `.apm/`. It selects processing: `instructions` compiles to AGENTS.md only, `skill` installs a native skill only, `prompts` emits commands only, `hybrid` does both (see `apm_cli/models/validation.py`, `PackageContentType`). apm checks only that the value parses to one of those four strings; no check anywhere compares it against the primitives actually on disk, and no mismatch diagnostic exists. A package declaring `type: instructions` while shipping `.apm/skills/` therefore raises no error — the mismatch resolves silently, either by omitting that primitive from the install/compile output or, in apm 0.28.0 where `get_effective_type()` routes off the on-disk layout and never reads the declared field, by ignoring the declared value outright. Both directions are silent: `apm install` and `apm compile` can exit 0 having shipped none of the primitives you expected. Set `type:` to cover every primitive the package ships, and confirm the deployed output rather than the exit code.
|
||||
- `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.
|
||||
- `apm plugin init <name>` run with a positional name argument, from inside a directory already named `<name>`, creates a wrongly-nested `<name>/<name>/` subdirectory — it treats the positional arg as "create a new project directory named X," not "confirm the current directory is X." Fix: omit the positional argument entirely when already cd'd into the target package directory — run `apm plugin init --yes --target claude,copilot` instead.
|
||||
@@ -1,22 +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.
|
||||
|
||||
If a `PACKAGE_REF` resolves through a named registry rather than a plain git source, `apm experimental enable registries` must already have been run — see `SKILL.md`'s Gotchas for the precondition and its silent-no-op failure mode.
|
||||
@@ -1,110 +0,0 @@
|
||||
---
|
||||
topic: marketplace
|
||||
source_keys:
|
||||
- context7-microsoft-apm
|
||||
- context7-websites-code-claude
|
||||
- claude-code-plugins-docs
|
||||
- apm-github-repo
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
`apm marketplace package add`'s `SOURCE` argument only accepts `owner/repo`, `host.tld/owner/repo`, or full URL forms (per `apm marketplace package add --help`) — it does NOT accept a local relative path. Running it against a local path (e.g. `apm marketplace package add ./packages/plugin-a`) throws a git-remote-reachability error: it tries to treat the path as a remote source and fails to reach it.
|
||||
|
||||
To register a local package — the monorepo-hybrid case, pointing at each existing `plugins/<name>/` directory once that directory has its own `apm.yml` — hand-edit `apm.yml`'s `marketplace.packages[]` list directly:
|
||||
|
||||
```yaml
|
||||
marketplace:
|
||||
packages:
|
||||
- name: plugin-a
|
||||
description: ...
|
||||
source: ./packages/plugin-a
|
||||
version: 1.0.0
|
||||
```
|
||||
|
||||
This local-path `source:` form IS valid — `apm marketplace check` and `apm pack` both accept it — even though the `package add` CLI subcommand cannot create it for you. Use `apm marketplace package add` only for packages hosted at a remote git ref; for local packages, edit the YAML directly.
|
||||
|
||||
## 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.
|
||||
|
||||
## Bumping the catalog's own version (repo policy)
|
||||
|
||||
The section above is apm's *mechanic* — how per-package versions are declared and how
|
||||
`marketplace.versioning.strategy` resolves them. This section is *policy*: apm has no version-bump
|
||||
automation, so the one number apm will never pick for you is the catalog's own
|
||||
`marketplace.version` in the root `apm.yml`. Bump it by hand, in the same commit as the change that
|
||||
earned it:
|
||||
|
||||
- **Minor** when a `marketplace.packages[]` entry is added or removed. The catalog's contents
|
||||
changed — a consumer resolving it now gets a different set of installable packages.
|
||||
- **Patch** when only `marketplace:`-block fields change and the set of packages is unchanged: the
|
||||
catalog description, owner, `build:`/`outputs:` config, or an existing entry's `version:`,
|
||||
description, or category. The catalog describes the same packages; only its metadata moved. An
|
||||
entry's `version:` is the most frequent of these by far — under `per_package` it moves here every
|
||||
time any package bumps (see `references/configure.md`), and that alone earns the catalog patch.
|
||||
|
||||
Keep the root `apm.yml`'s top-level `version:` in step with `marketplace.version`. They are separate
|
||||
keys — the top-level one is not inherited into the compiled `marketplace.json`, but `apm audit`
|
||||
reads it — so letting them diverge produces two different answers to "what version is this catalog".
|
||||
|
||||
Editing a package's *own* `.apm/` content is a different bump against a different number — see
|
||||
`references/configure.md`. Note what the tooling does and does not catch: `apm pack --check-clean`
|
||||
regenerates every marketplace output and diffs it against what is on disk, so a bump made in
|
||||
`apm.yml` but never re-packed fails the gate. A bump you never made at all fails nothing — no apm
|
||||
flag infers a required version change from a content diff, which is why this is policy and not a
|
||||
hook.
|
||||
|
||||
## 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
|
||||
|
||||
- `apm marketplace add` and `apm marketplace package add` point in opposite directions and are easy to conflate: `add` registers someone else's catalog for you to *consume*; `package add` registers a package by remote reference (`owner/repo`, host URL, or full URL) into a marketplace you are *building*. `package add` does not accept a local path — register a local package by hand-editing `marketplace.packages[]`, as above.
|
||||
- Only `claude` and `codex` marketplace output profiles exist in apm 0.28.0 — confirmed via source (`apm_cli/marketplace/output_profiles.py`: `MARKETPLACE_OUTPUTS = {claude: ..., codex: ...}`, nothing else). There is no native Copilot marketplace output; `apm pack` will not generate one. A repo needing a Copilot-consumable marketplace manifest must maintain it separately by other means.
|
||||
- A package/plugin `name` compiling to the `claude` target must avoid Claude Code's reserved name prefixes/values — `anthropic-*`, `claude-*`, `agent-skills`, `official-claude-plugins` — 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.
|
||||
@@ -1,41 +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`
|
||||
|
||||
## apm-github-repo
|
||||
|
||||
- **URL:** https://github.com/microsoft/apm
|
||||
- **Description:** APM's own Python source (`src/apm_cli/`), read directly for schema and compile-mapping ground truth where Context7's doc snippets were thin. Backs the marketplace output-profile claim (`apm_cli/marketplace/output_profiles.py` — only `claude` and `codex` profiles exist, so there is no native Copilot marketplace output) and the `apm pack --check-clean`/`--check-versions` semantics.
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md
|
||||
- **Contributing files:** references/marketplace.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`
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
name: forge
|
||||
description: >
|
||||
Use when the user wants to build or improve something but has not yet named
|
||||
the artifact type — skill, agent, plugin, or marketplace entry; "not sure if
|
||||
this should be a skill or a plugin", "I have an idea but don't know where it
|
||||
belongs". Routes to the matching author skill. Do not use when the type is
|
||||
already named — invoke `skill-author`, `agent-author` or `apm-workflow`
|
||||
directly.
|
||||
metadata:
|
||||
version: "1.0.1"
|
||||
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 — `skill-author`, `skill-audit`, `agent-author`, `agent-audit` and `apm-workflow` all stay directly invokable, and forge never intercepts a direct call to one.
|
||||
- Claude Code's skill-level `context: fork` frontmatter field and the `/fork` subagent command are opposites despite the shared word: `context: fork` isolates (fresh context, no parent access), while `/fork` inherits the full conversation. The route reference each classification loads spends that distinction: `references/author-routes.md` chooses between the two, `references/apm-routes.md` rules the fork out.
|
||||
|
||||
## Step 1 — Grill the intent
|
||||
|
||||
Call `grill-with-docs` unless a grill session has already run and is available in the context.
|
||||
|
||||
`grill-with-docs` ships in a sibling plugin that kyberforge does not declare as an apm dependency, so it resolves in the authoring monorepo but can be absent where kyberforge is installed alone. If it does not resolve, grill inline yourself rather than skipping the step: what problem the artifact solves, who invokes it and how, what it must refuse, and which existing skill or plugin already owns part of the job. Say which path you took.
|
||||
|
||||
Grilling regularly overturns the artifact type assumed at the start, or splits one idea into several artifacts, so it runs before classification rather than confirming it. Run it inline in the current conversation — grilling is interactive and a subagent cannot hold the back-and-forth.
|
||||
|
||||
## Step 2 — Classify and dispatch
|
||||
|
||||
Match the grilled intent against exactly one row — or more than one, if the intent genuinely spans several artifacts.
|
||||
|
||||
| Intent | Artifact type | Route to | Read |
|
||||
|---|---|---|---|
|
||||
| A reusable capability the agent loads inline in the main conversation, triggered by description-matching, free to bundle its own `references/`, `scripts/` or `assets/` | Skill | `skill-author` | `references/author-routes.md` |
|
||||
| A recurring task needs its own reusable definition — dedicated system prompt, tools and description, invokable by name across sessions | Agent / subagent | `agent-author` | `references/author-routes.md` |
|
||||
| A new distributable unit — no existing plugin is the right home for the skill, agent, hook or MCP server being built, or the bundle needs its own manifest, versioning and install lifecycle | Plugin | `apm-workflow` (`apm plugin init`) | `references/apm-routes.md` |
|
||||
| The plugin already exists and only its marketplace-facing metadata changes — a first listing, or a version/description update, never the plugin's contents | Marketplace entry | `apm-workflow` (`apm marketplace package add`) | `references/apm-routes.md` |
|
||||
|
||||
The table classifies what to build, not how to run it: a one-off task that merely needs an isolated or context-inheriting run is not an artifact and has no row here. If the intent stays genuinely ambiguous between rows after grilling, ask the user rather than guessing.
|
||||
|
||||
A real artifact that matches no row — a hook, an MCP server, an AGENTS.md, a research doc — has no route here. Say so, hand the user the skill that does own it, and never bend it into a row to make the table fit.
|
||||
|
||||
When the intent spans several rows, chain the routes in dependency order — an artifact that must exist on disk before another skill can target it goes first, so `apm-workflow` scaffolds the plugin directory before `skill-author` scaffolds a skill inside it.
|
||||
|
||||
**Announce, then invoke.** State the classification and which skill(s) will run. Then read the reference file for each classified artifact type — only those — and follow it.
|
||||
|
||||
## Step 3 — Closing gates, common to every route
|
||||
|
||||
- **Resolve before closing.** A route is finished only when its verification reports nothing unresolved. An actionable finding reopens the route; it is never reported onward as a caveat.
|
||||
- **Bump the package version.** A skill route always lands here: `skill-author` moves only a skill's own `metadata.version`, which is not the package `apm.yml`'s number — so read `references/version-bump.md` after one. `agent-author` and the apm routes bump the package themselves at plugin scope; after those, read it only when their output does not say they did.
|
||||
@@ -1,42 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- claude-code-subagents-docs
|
||||
---
|
||||
|
||||
# Routing a plugin or marketplace entry to apm-workflow
|
||||
|
||||
Reached from `SKILL.md` Step 2 when the classified artifact is a plugin or a marketplace entry.
|
||||
Both route to `apm-workflow` — a plugin to its configure flow (`apm plugin init`), a marketplace
|
||||
entry to its marketplace flow (`apm marketplace package add`).
|
||||
|
||||
No other skill is a candidate for these two rows: `plugin-author` and `marketplace-author` were
|
||||
removed per ADR-0015 once issue #90 landed, and `apm-workflow` is their sole successor.
|
||||
|
||||
## Always inline, never forked
|
||||
|
||||
Run these routes inline, in the current conversation. Their flows are short, prompt-heavy or
|
||||
gated — `apm-workflow`'s publish and release steps take a HITL gate, and removing a marketplace
|
||||
entry takes a conversational confirmation — and a backgrounded fork cannot surface those
|
||||
checkpoints to the user in real time.
|
||||
|
||||
## No clean-context recheck, and no automatic audit
|
||||
|
||||
Skill and agent routes close with a clean-context audit rerun; these two do not, and the omission
|
||||
is deliberate rather than an oversight. Neither artifact type has an audit skill counterpart to
|
||||
re-run, so detaching the route to earn a recheck it would never get buys nothing.
|
||||
|
||||
These routes get no automated terminal check either. `apm audit` is a separate action on
|
||||
`apm-workflow`'s own dispatch table, not a closing step of the configure or marketplace flow a
|
||||
forge route lands in, so a completion message from either says nothing about it. Do not wait for
|
||||
one and do not report one you did not see.
|
||||
|
||||
Verify by hand instead. Read back what the route wrote against what the grill settled:
|
||||
|
||||
- **Plugin** — the package directory exists where the intent said it should, and its `apm.yml`
|
||||
carries the intended `name`, a top-level `type:` field, and a `version`.
|
||||
- **Marketplace entry** — the entry names that package, points at the source the intent settled
|
||||
on, and carries the version the package actually declares.
|
||||
|
||||
If the change warrants the full integrity and policy check rather than a read-back, invoke
|
||||
`apm-workflow` again for its audit action and run `apm audit` deliberately. Then return to
|
||||
`SKILL.md` Step 3 for the closing gates common to every route.
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- claude-code-subagents-docs
|
||||
---
|
||||
|
||||
# Routing a skill or agent to its author skill
|
||||
|
||||
Reached from `SKILL.md` Step 2 when the classified artifact is a skill or an agent/subagent
|
||||
definition. Route a skill to `skill-author` and an agent to `agent-author`. The two branches
|
||||
differ on one axis only — which audit skill verifies the result — and everything below applies to
|
||||
both.
|
||||
|
||||
## Choose fork or inline
|
||||
|
||||
Default to a **fork subagent**. It inherits the full grilled-intent conversation, so the author
|
||||
skill does not need re-briefing on what the user asked for or what the grill settled.
|
||||
|
||||
Fall back to an **inline invocation** — same conversation, no subagent — when either holds:
|
||||
|
||||
- **Fork is technically unavailable.** You are already running inside a fork (a fork cannot spawn
|
||||
another fork), a nesting-depth cap is reached, or the environment does not support forking.
|
||||
- **The routed flow needs live user interaction mid-run** that a backgrounded fork cannot surface
|
||||
in real time: clarifying questions, confirmation checkpoints, or a HITL gate. Judge this from
|
||||
context — if nothing about the flow signals a live checkpoint, prefer the fork.
|
||||
|
||||
## Two-tier verification
|
||||
|
||||
Both author skills already close out with their own inline audit, in the same context as the
|
||||
authoring work: `skill-author` runs `/skill-audit`, `agent-author` invokes
|
||||
`agent-audit`. That is tier one, and forge does not change it.
|
||||
|
||||
Tier two belongs to forge. Once the 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 is 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-versus-inline
|
||||
judgment as the first invocation) to resolve it, then re-run the clean audit. Repeat until the
|
||||
clean audit comes back with nothing unresolved. Only then is the route done. This is the same
|
||||
resolve-before-close discipline the author skills already apply to their own inline audit.
|
||||
|
||||
Return to `SKILL.md` Step 3 for the closing gates common to every route once the loop closes.
|
||||
@@ -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 the fork-vs-inline invocation logic in `references/author-routes.md`, the always-inline decision for the apm routes in `references/apm-routes.md`, and the clean-context bump subagent in `references/version-bump.md`: 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, references/author-routes.md, references/apm-routes.md, references/version-bump.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 in `SKILL.md` warning against conflating the two; nothing else in this skill draws on it, and no `references/` file mentions the `context: fork` field.
|
||||
- **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, and forge's per-route procedures are that kind of supporting content — so the spec permits the `references/` split here but does not require it. The warrant is a house decision: dispatch is mandatory at two or more mutually exclusive flows, which forge's four-row table is. `references/sources.md` likewise 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`
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- claude-code-subagents-docs
|
||||
---
|
||||
|
||||
# Bumping the package version after a route
|
||||
|
||||
Reached from `SKILL.md` Step 3 after a route has finished. A skill route always lands here:
|
||||
`skill-author` moves only a skill's own `metadata.version`, which is not the package manifest's
|
||||
number, so the package version is still behind when it reports done. `agent-author` bumps the
|
||||
resolved package's `apm.yml` itself at plugin/APM scope, and `apm-workflow`'s configure flow
|
||||
carries the same policy — read those routes' output before acting here, because a second bump for
|
||||
one change is wrong.
|
||||
|
||||
## Find the owning package
|
||||
|
||||
Walk up from the artifact's path to the nearest ancestor `apm.yml` that declares a top-level
|
||||
`type:` field (`instructions`, `skill`, `hybrid` or `prompts`).
|
||||
|
||||
An `apm.yml` with **no** `type:` field is a marketplace-only manifest: it lists packages rather
|
||||
than declaring one, so it does not count as a match. Skip it and keep walking up.
|
||||
|
||||
Skip this step entirely if no ancestor `apm.yml` carries a `type:` field: the artifact is then
|
||||
standalone or scoped to a user agent directory, and there is no package to version.
|
||||
|
||||
## Delegate the bump
|
||||
|
||||
Invoke `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."
|
||||
|
||||
Clean context rather than a fork is the point: the bump decision is made independently, without
|
||||
anchoring on the authoring conversation that just argued for the artifact's significance.
|
||||
|
||||
Then report to the user: "Updated `<package-name>` version from `<old>` to `<new>` to reflect the
|
||||
new `<artifact-name>`."
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
name: skill-audit
|
||||
description: >
|
||||
Use when the user wants a skill directory audited against the agentskills.io
|
||||
spec — "audit this skill", "review my SKILL.md", "is this ready to ship" — or
|
||||
after hand-editing a skill outside skill-author. Not applying fixes ->
|
||||
skill-author.
|
||||
allowed-tools: Bash Read
|
||||
metadata:
|
||||
version: "1.0.2"
|
||||
category: factory
|
||||
source_keys:
|
||||
- agentskills-home
|
||||
- agentskills-spec
|
||||
- agentskills-best-practices
|
||||
- agentskills-optimizing-descriptions
|
||||
- agentskills-using-scripts
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Do not narrate 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.
|
||||
- A skill carrying `disable-model-invocation: true` is hand-invoked — its description is never routed against, so the trigger, capability and boundary rules do not apply. Audit it as one plain human-facing sentence instead.
|
||||
- `validate.sh` reports two independent length families: the 500-line / 2,770-word pair counts the whole file for spec conformance, while the 250/400-character and 600/900-word pair is the house context budget and its word half counts the **body only**. A skill can sit inside one and fail the other — report them separately.
|
||||
- Vale reporting `0 files` scanned means NOT RUN, not clean. Fall back to full Step 3 judgment for every dimension it would have covered.
|
||||
|
||||
## Step 1 — Deterministic checks
|
||||
|
||||
Resolve all three paths against this skill's own directory so they work from a repo checkout and an installed plugin cache alike. Run exactly:
|
||||
|
||||
```bash
|
||||
bash scripts/validate.sh <skill-dir>
|
||||
bash scripts/validate-provenance.sh <skill-dir>
|
||||
bash scripts/vale-wrap.sh <skill-dir>/SKILL.md
|
||||
```
|
||||
|
||||
`validate.sh` findings become the `### Structure` dimension — its FAILs and its SUGGESTIONs both, at the tier the script assigned. Report each once; never re-grade one under another dimension. Unresolved boundary targets are where this bites, because their tier turns on notation.
|
||||
|
||||
Read `references/validation-scripts.md` when any of the three cannot run or exits non-zero for a reason other than findings, **and whenever `validate-provenance.sh` exits 0 having printed anything**. Ordinary content FAILs are the expected outcome here and need no fallback.
|
||||
|
||||
`validate-provenance.sh` reports through exit code **and** output; neither alone is the verdict. **0, silent** is a genuine pass. **0 with output** is INFO-only findings — still a `### Provenance` dimension; `references/validation-scripts.md` says what each obliges — for a check-9 INFO, reading rather than relaying. **1** is FAILs plus any INFOs; it emits Why and Fix itself — surface those verbatim. **2** means it never ran — a usage or environment error, reason on stderr, often no stdout — so report `### Provenance` unverified and quote that reason. Never grade an exit 2, or an exit 0 that printed, as a clean pass.
|
||||
|
||||
`vale-wrap.sh` applies the bundled `Kyberforge` style as a prefilter. Pass no `--config`; the wrapper locates its own. Every rule is graded `error`, so every alert is a FAIL. Report each one citing its rule ID, filed under the dimension it belongs to, and do not re-derive it by judgment:
|
||||
|
||||
| Rule | Dimension |
|
||||
|---|---|
|
||||
| `Kyberforge.DescriptionOpener`, `Kyberforge.CompositionNote`, `Kyberforge.VagueWording` | description |
|
||||
| `Kyberforge.SentenceOpenerThereIs` | body-discipline |
|
||||
| `Kyberforge.PaddingPhrase` | patterns |
|
||||
|
||||
## Step 2 — Read the whole skill
|
||||
|
||||
Read `SKILL.md` and every text file under `scripts/`, `references/`, `assets/` and `tests/`. Skip binaries only — internal-consistency findings need the full picture.
|
||||
|
||||
## Step 3 — Qualitative audit
|
||||
|
||||
Read `references/finding-criteria.md` first — every dimension's FAIL and SUGGESTION criteria. Load the rubric below only for a dimension the criteria put in play: one carrying a candidate finding, or one where the criterion alone does not settle the call.
|
||||
|
||||
| Dimension | Rubric |
|
||||
|---|---|
|
||||
| description | `references/description-quality.md` |
|
||||
| body-discipline | `references/body-discipline.md` |
|
||||
| patterns | `references/patterns.md` |
|
||||
| file-structure, internal-consistency | `references/file-structure.md` |
|
||||
| formatting, scripts | `references/formatting-and-scripts.md` |
|
||||
|
||||
Each rubric is self-contained and grounded in the agentskills.io specification plus the house context budget. Cite file and line number for every finding.
|
||||
|
||||
## Step 4 — Report
|
||||
|
||||
Open with a coverage line naming every dimension checked:
|
||||
|
||||
```text
|
||||
Checked: structure · description · body-discipline · patterns · file-structure · formatting · scripts · internal-consistency · provenance
|
||||
```
|
||||
|
||||
Then output only the dimensions that have findings, grouped under H3 headings, FAILs before SUGGESTIONs within each. Omit clean dimensions — their absence is what confirms they passed.
|
||||
|
||||
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 holding one line: `PASS`, `PASS (N suggestions)`, or `FAIL (N fails · M suggestions)`, each optionally followed by ` · P info`. INFO findings are observational and never change PASS/FAIL; omit `· P info` when there are none. Add a second line, `Run skill-author to address findings.`, whenever there is at least one finding. Do not apply fixes — report and propose only.
|
||||
@@ -1,4 +0,0 @@
|
||||
StylesPath = styles
|
||||
|
||||
[**/SKILL.md]
|
||||
BasedOnStyles = Kyberforge
|
||||
@@ -1,13 +0,0 @@
|
||||
extends: existence
|
||||
message: "Composition or architecture note in a description: '%s' — a description carries a trigger, one capability clause and a boundary clause only; move this to README.md"
|
||||
level: error
|
||||
scope: text.frontmatter.description
|
||||
ignorecase: true
|
||||
tokens:
|
||||
- cross-cutting
|
||||
- shared (skill|agent)
|
||||
- human-facing
|
||||
- entry[- ]point
|
||||
- composes
|
||||
- rather than duplicating
|
||||
- replaces the (old|former|previous)
|
||||
@@ -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\b'
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -1,209 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-spec
|
||||
- agentskills-best-practices
|
||||
---
|
||||
|
||||
# Body Discipline Reference
|
||||
|
||||
Upstream source: agentskills.io — skill-authoring, best-practices.
|
||||
House contract: the context budget.
|
||||
|
||||
## 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 the body is for
|
||||
|
||||
The body carries the **decision procedure only**: ordered steps, decision branches, gates, and
|
||||
which reference to load when.
|
||||
|
||||
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
|
||||
|
||||
Move to `references/`, behind an explicit "If X, read `references/<file>.md`" trigger — the literal
|
||||
conditional form, never a generic pointer. Write the real filename in the skill under audit; the
|
||||
angle brackets are a placeholder here, and a literal `references/file.md` in a body is an ERROR
|
||||
from the gate because no such file exists on disk.
|
||||
|
||||
**A dispatch table satisfies this requirement on its own.** A table row already pairs a condition
|
||||
with a target, which is exactly what the literal form encodes; restating each row underneath as a
|
||||
prose conditional duplicates the routing in the one body whose whole purpose is to be short. Where a
|
||||
body dispatches, audit the table for condition/target completeness and stop there — do not require
|
||||
the conditional form as well. The literal form is what a body needs when it loads a reference
|
||||
*without* a dispatch table: a single mid-procedure deepening, an escape hatch, an error path.
|
||||
|
||||
Move:
|
||||
|
||||
- Lookup tables and spec restatements
|
||||
- Output schemas, templates and example blocks
|
||||
- Rationale and justification prose
|
||||
- Anything only one branch of the procedure ever reaches
|
||||
|
||||
Do not include at all:
|
||||
|
||||
- Concepts the agent already knows (what JSON is, how HTTP works, what a CSV is)
|
||||
- Exhaustive option lists — pick a default; the agent does not benefit from choosing
|
||||
- Steps the agent handles independently — over-specifying leads to unproductive paths
|
||||
- Restatements of the description, which is already in context
|
||||
|
||||
## Two length families, measured differently
|
||||
|
||||
Do not conflate these, and do not report them as one finding.
|
||||
|
||||
| Gate | SUGGESTION | FAIL | Counts |
|
||||
|---|---|---|---|
|
||||
| Body budget (house) | 600 words | 900 words | the **body only** — everything after the frontmatter's closing `---` |
|
||||
| Spec conformance (agentskills.io) | — | 2,770 words / 500 lines | the **whole file**, frontmatter included |
|
||||
|
||||
The 2,770-word ceiling is a token-conformance backstop calibrated to the densest prose in the
|
||||
corpus; it says nothing about quality and a file can sit a thousand words inside it while failing
|
||||
the body budget. The 900-word ceiling is the quality gate: a body is loaded into the caller's live
|
||||
context and competes with the conversation already there. `validate.sh` reports both. Cite whichever
|
||||
one actually fired.
|
||||
|
||||
A word count cannot detect the defect it stands in for. Treat both numbers as backstops to the
|
||||
dispatch rule and the Gotchas constraint below, never as a substitute for them.
|
||||
|
||||
## Dispatch is mandatory at two or more mutually exclusive flows
|
||||
|
||||
If a skill handles two or more flows that a single invocation cannot both take — separate
|
||||
subcommands, separate input types, separate lifecycle stages — the body carries a **dispatch
|
||||
table** plus the gates common to every branch, and each flow lives in its own self-contained
|
||||
`references/` file. Inlining all of them is a FAIL regardless of word count, because every
|
||||
invocation then pays for every branch it did not take.
|
||||
|
||||
The reference shape in this repo is `apm-workflow`: a **294-word body** dispatching to 3,154 words
|
||||
of references across five mutually exclusive flows. Its whole-file count is 348 words — cite 294
|
||||
when calibrating a body, or the conflation this section warns against reappears in the finding
|
||||
itself. The 3,154 counts the five flow files only; `references/sources.md` is a provenance record
|
||||
and is never loaded at runtime, so counting it inflates the dispatched total.
|
||||
|
||||
### What earns the wiring exemption
|
||||
|
||||
A dispatch table earns the exemption above on its properties, not on which skill it appears in.
|
||||
Audit any dispatching body against these four:
|
||||
|
||||
- Every flow the skill handles has a row, and every row names a target file that exists on disk.
|
||||
- Each row pairs a condition the agent can evaluate from the request with exactly one target. A row
|
||||
keyed on a literal slash invocation fails this: a model-invoked activation never produces that
|
||||
string, so the routing silently falls to whatever else the row carries.
|
||||
- One line after the table tells the agent to read the file its row matched, and only that one.
|
||||
- The gates every branch needs sit in the body, not inside one flow's file — see the reachability
|
||||
precondition below.
|
||||
|
||||
A table missing any of the four is not exempt, and the literal-conditional requirement applies to it
|
||||
as written. The exemption covers the wiring form only: every other rule in this file applies to a
|
||||
dispatching skill exactly as it applies to any other.
|
||||
|
||||
## Gotchas sections
|
||||
|
||||
The highest-value construct in a body, and the easiest to fill with noise. A Gotcha must state a
|
||||
fact that **contradicts a reasonable default** — something the agent gets wrong precisely by acting
|
||||
sensibly.
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
Constraints:
|
||||
|
||||
- **More than five entries is a SUGGESTION** — five is the guideline, not a ceiling. Past five, the
|
||||
section is usually a summary of the body rather than a set of traps, and the agent stops reading
|
||||
it as a warning. It stays advisory because whether a given gotcha earns its place is judgment;
|
||||
`validate.sh` emits it through `suggest()` and the run still exits 0.
|
||||
- **A Gotcha that paraphrases a step in the body below it is a FAIL.** It has no independent
|
||||
content, and it teaches the agent that Gotchas can be skimmed because the real instruction is
|
||||
coming. This one is the auditor's call — no script detects it. The Fix is conditional: delete the
|
||||
Gotcha only if the surviving copy is reachable from every branch that needs it — see the
|
||||
reachability precondition below.
|
||||
- **A Gotchas section exceeding 25% of the body is a SUGGESTION** — the body has been inverted into
|
||||
a preamble. Same tier and same reasoning as the entry count, and independent of it: either can
|
||||
fire without the other.
|
||||
- Place the section near the top. A gotcha read after the mistake is worthless, which is also why
|
||||
Gotchas is the one construct exempt from moving to `references/`.
|
||||
|
||||
Worked negative example — **`git-commits` v0.1.2 at commit `5e23250`, a fixed pre-retrofit
|
||||
snapshot, not the current file.** The live skill is v0.1.3 and matches none of the citations below;
|
||||
they are quoted as they stood in that snapshot, and are not to be refreshed against
|
||||
`HEAD`. The snapshot is reachable only from a checkout of the authoring repo — an installed plugin
|
||||
cache holds no git history and no such path — so read the citations below as quoted rather than
|
||||
going to look for the file. From a checkout:
|
||||
|
||||
```text
|
||||
git show 5e23250:<the git plugin>/.apm/skills/git-commits/SKILL.md
|
||||
```
|
||||
|
||||
That body carried twelve Gotchas, four of which restated content already below them or already in
|
||||
the description:
|
||||
|
||||
| Gotcha | Restates |
|
||||
|---|---|
|
||||
| `:31` "SemVer mapping is not optional" | the description |
|
||||
| `:32` "Confirmation gates are mandatory for destructive operations" | step 9 at `:52` |
|
||||
| `:33` "Never skip hooks with `--no-verify`" | step 9 at `:52` |
|
||||
| `:36` "Never commit secrets" | step 2 at `:45` |
|
||||
|
||||
All four are FAILs under the paraphrase rule. The entry count and the section's share of the body
|
||||
(387 of 1,102 words, 35%) are two further SUGGESTIONs on top — the script reports both, and neither
|
||||
fails the run on its own. What makes this worth auditing directly is that the four paraphrase FAILs
|
||||
pass every word gate there is; only reading the construct finds them.
|
||||
|
||||
### The paraphrase rule has a reachability precondition
|
||||
|
||||
**A Gotcha that restates a step may be deleted only when the surviving copy is reachable from every
|
||||
branch that needs it.** In a dispatch body it usually is not: each flow file is loaded alone, so a
|
||||
step in one is invisible to an invocation that took another branch. When the restated rule is a
|
||||
safety gate more than one flow needs, the Fix is to **move it into the body's common-gates section**,
|
||||
never to drop it in favour of the per-flow copy.
|
||||
|
||||
Row four is the case that proves it. Following the rule literally, the retrofit deleted the
|
||||
always-loaded secrets Gotcha and kept step 2 of `references/create-commit.md` — but `git-commits`
|
||||
dispatches to exactly one flow file, and `references/rewrite-history.md` stages changes and runs
|
||||
`--amend`, which commits newly staged content exactly as a fresh commit does. A grep for `secret`
|
||||
across the skill in that state returned one hit, on a path two of three branches never reach: that
|
||||
branch could commit a credential with no check anywhere in its loaded context, against this repo's
|
||||
governance hard prohibition. v0.1.3 carries the rule as gate 2 of "Gates on every flow" instead.
|
||||
|
||||
So check reachability before writing the Fix. Rows one to three are unaffected — the description is
|
||||
loaded on every invocation, and confirmation is likewise a common gate rather than a per-flow step.
|
||||
|
||||
## 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.
|
||||
```
|
||||
|
||||
The FAIL and SUGGESTION criteria for this dimension live in `references/finding-criteria.md`,
|
||||
which Step 3 loads on every run.
|
||||
@@ -1,87 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-spec
|
||||
- agentskills-optimizing-descriptions
|
||||
---
|
||||
|
||||
# Description Quality Reference
|
||||
|
||||
Upstream source: agentskills.io — optimizing-descriptions, specification.
|
||||
House contract: the context budget. The house contract is narrower than the spec
|
||||
rather than a reinterpretation of it: where both speak, both must be satisfied.
|
||||
|
||||
## Why the description is the expensive part
|
||||
|
||||
At startup an agent loads only the `name` and `description` of every installed skill. The body is
|
||||
never seen until the skill triggers. The description therefore carries the entire triggering
|
||||
burden **and** is paid for in every session, whether the skill fires or not.
|
||||
|
||||
A second cost is less obvious and is a correctness hazard rather than a token cost: a description
|
||||
that summarises the workflow is a shortcut the agent takes *instead of* reading the body. A
|
||||
measured failure upstream — a description saying "code review between tasks" — produced one review
|
||||
where the body's flowchart specified two.
|
||||
|
||||
## Step 0 — establish which contract applies
|
||||
|
||||
Read the frontmatter before judging a single word.
|
||||
|
||||
- **`disable-model-invocation: true`** — the skill is hand-invoked. Its description is never
|
||||
matched against user intent, so it is not a routing string. It carries **one plain human-facing
|
||||
sentence** stating what the skill does. Audit it for that and nothing else. Reporting a missing
|
||||
trigger clause, a missing boundary clause or absent indirect triggers on a hand-invoked skill is
|
||||
a wrong finding, not a strict one.
|
||||
- **No such flag** — the skill is model-invoked and the rest of this file applies.
|
||||
|
||||
## The three-part shape
|
||||
|
||||
A model-invoked description carries exactly three things:
|
||||
|
||||
1. **Trigger clause.** When to invoke, phrased imperatively: `Use when ...`. Not `This skill ...` —
|
||||
the agent is deciding whether to act, not reading a catalogue entry.
|
||||
2. **At most one capability clause.** What it does, in one clause. Never an enumeration.
|
||||
3. **Boundary clause.** Compressed form: `Not <thing> -> <skill-name>.` The target must resolve to
|
||||
a real skill directory or agent file in the authoring source. `validate.sh` checks that
|
||||
deterministically and grades it by notation: an unresolved `/name` or arrow target is an ERROR
|
||||
and reaches the report as a Structure FAIL, while an unresolved prose-form target ("use `y`
|
||||
instead") is only a SUGGESTION unless a second target in the same sentence resolves. Take the
|
||||
script's tier as given and report it once, under Structure.
|
||||
|
||||
Everything else belongs in the body or in `README.md`.
|
||||
|
||||
## Indirect triggers — conditional, never blanket
|
||||
|
||||
Add "even if the user doesn't say X" **only where the user's natural phrasing genuinely omits the
|
||||
domain word.** True for the `gitea-*` family: people say "create an issue", not "create a Gitea
|
||||
issue". False for `git-commits`: nobody asks for a commit without saying commit. A blanket
|
||||
indirect-trigger clause on a skill whose domain word is unavoidable is padding charged to every
|
||||
session.
|
||||
|
||||
## Near-miss exclusions
|
||||
|
||||
Add a boundary clause only where a sibling skill could plausibly steal the activation. Use strong
|
||||
near-misses — queries that share keywords but need something different — not weak ones ("write a
|
||||
fibonacci function"). One boundary clause per genuine near-miss; a list of four is enumeration
|
||||
wearing a boundary's clothes.
|
||||
|
||||
## Before / after
|
||||
|
||||
```yaml
|
||||
# FAIL — enumeration first, mechanics as the opener, a blanket indirect trigger,
|
||||
# and 300+ characters of it preloaded into every session forever.
|
||||
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."
|
||||
|
||||
# PASS — trigger, one capability clause, boundary. The four verbs the FAIL
|
||||
# version enumerates are the body's job; the router cannot act on them.
|
||||
description: >
|
||||
Use when the user has a CSV, TSV, or Excel file and wants it explored,
|
||||
transformed, or charted. Not schema design -> data-model.
|
||||
```
|
||||
|
||||
(`data-model` is illustrative. In a real description the target has to resolve.)
|
||||
|
||||
The FAIL and SUGGESTION criteria for this dimension live in `references/finding-criteria.md`,
|
||||
which Step 3 loads on every run.
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-spec
|
||||
---
|
||||
|
||||
# File Structure and Internal Consistency Reference
|
||||
|
||||
Upstream source: agentskills.io — specification (optional directories, file references).
|
||||
|
||||
Read this when judging the **file-structure** and **internal-consistency** dimensions.
|
||||
|
||||
## Permitted directories
|
||||
|
||||
Only four: `scripts/`, `references/`, `assets/`, `tests/`. The specification permits additional
|
||||
directories; this house does not, because an unlisted directory is content no auditor and no host
|
||||
knows to look at. Flag any other directory as a FAIL.
|
||||
|
||||
- `scripts/` holds only executable code an agent can run. Test files (`.bats`, `*_test.*`,
|
||||
`test_*.sh`) there are a FAIL — they belong in `tests/`.
|
||||
- No non-spec files at the skill root: no `META.md`, no stray config outside the four directories.
|
||||
- An optional directory that exists must hold real content, not an unfilled placeholder README.
|
||||
|
||||
## Cross-plugin path references
|
||||
|
||||
A plugin is copied to a cache on install, and a path that climbs out of the skill directory stops
|
||||
resolving there. Flag a path in `SKILL.md`, `scripts/`, `references/` or `assets/` when it
|
||||
**resolves outside the skill directory** — an absolute repo path
|
||||
(`plugins/<plugin>/skills/<other>/` and its APM-native equivalent `.apm/skills/<other>/`), a
|
||||
plugin-root path (`docs/`, `bin/`), or a `../` chain that leaves the skill root.
|
||||
|
||||
Resolve before flagging, twice over:
|
||||
|
||||
- **Resolve the path.** `$SKILL_DIR/../assets/templates` climbs one level from a `scripts/`
|
||||
directory and lands back inside the same skill, so it resolves in a cache install and is not a
|
||||
finding. A bare `../` is not the defect; leaving the skill is.
|
||||
- **Skip fenced code blocks.** A path inside a fenced block is an example, and rubrics quote outside
|
||||
paths deliberately as negative examples of what not to write. Flag a fenced path only when the
|
||||
surrounding prose presents it as the form to copy.
|
||||
|
||||
**Referring to another skill's file.** There is one sanctioned spelling, and it is possessive:
|
||||
`skill-audit's references/validation-scripts.md`. Write the skill by name and let the reader
|
||||
resolve it — do not spell the repo path. The full path is the thing this section forbids, and
|
||||
`references/validation-scripts.md` on its own is a hard ERROR from the gate, which
|
||||
requires an unqualified `references/` pointer to exist in the skill's OWN directory. The
|
||||
possessive form is the only spelling both rules accept; the gate recognises it and skips the
|
||||
on-disk check. Flag any other spelling of a cross-skill reference.
|
||||
|
||||
Two directories are exempt, and the exemptions are structural rather than discretionary:
|
||||
|
||||
- **`references/sources.md`.** Its `Research doc:` fields are development-time provenance pointers,
|
||||
not runtime references. They are expected to be unresolvable after install, so
|
||||
`validate-provenance.sh` does not treat an absent path as a FAIL — it emits an INFO naming the
|
||||
slug and stating that checks 7 and 8 did not run for it. Flagging them as broken references
|
||||
would make every correctly-provenanced skill fail.
|
||||
- **`tests/`.** Test files are dev-only and may reference repo-level infrastructure such as a shared
|
||||
`tests/test_helper/`. The exemption is conditional on the dependency being declared: if `tests/`
|
||||
exists and `tests/README.md` is absent or does not document it, that is a FAIL.
|
||||
|
||||
## Internal consistency
|
||||
|
||||
The skill has to agree with itself. Two checks:
|
||||
|
||||
- `SKILL.md`'s steps match what the scripts actually do — the arguments, the exit codes, and the
|
||||
output shape it tells the agent to expect.
|
||||
- Placeholder READMEs inside `scripts/`, `tests/` and `assets/` say the same thing about each
|
||||
directory that `SKILL.md` does.
|
||||
|
||||
The FAIL and SUGGESTION criteria for this dimension live in `references/finding-criteria.md`,
|
||||
which Step 3 loads on every run.
|
||||
@@ -1,136 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-spec
|
||||
- agentskills-best-practices
|
||||
- agentskills-optimizing-descriptions
|
||||
- agentskills-using-scripts
|
||||
---
|
||||
|
||||
# Finding Criteria
|
||||
|
||||
Every FAIL and SUGGESTION criterion, for every qualitative dimension, and nothing else. The
|
||||
reasoning each criterion stands on, its worked examples and its house rules stay in that
|
||||
dimension's rubric, which Step 3 loads only for a dimension this file puts in play.
|
||||
|
||||
Two rules on using it:
|
||||
|
||||
- A criterion that plainly applies is a finding. Write it up citing file and line.
|
||||
- A criterion that might apply, or whose call the wording here does not settle, is a reason to load
|
||||
that dimension's rubric — never a reason to drop the candidate. This file decides which rubrics
|
||||
to read; it does not settle a close call on its own.
|
||||
|
||||
## description — `references/description-quality.md`
|
||||
|
||||
Flag as FAIL if:
|
||||
|
||||
- **Over 400 characters.** Measured on the folded YAML value, not the raw source lines.
|
||||
`validate.sh` reports the number; do not re-derive it, but do point the Fix at what to cut.
|
||||
- **Internal mechanics appear in the description.** Any of:
|
||||
- capability enumeration or a feature list;
|
||||
- output-format detail ("Produces a compact findings report with Why and Fix per finding");
|
||||
- composition or architecture notes ("composes X rather than duplicating Y", "a cross-cutting
|
||||
shared skill", "the human-facing entry point", "replaces the old flat invocation");
|
||||
- implementation detail ("self-validates via a bundled deterministic script").
|
||||
|
||||
None of it can change a routing decision and all of it is preloaded.
|
||||
`Kyberforge.CompositionNote` catches the common phrasings deterministically; the rest is
|
||||
judgment. This is the rule that deflates a description, so apply it before reaching for length.
|
||||
- **The same trigger stated twice in two registers** — a verb list, then the same verbs re-quoted
|
||||
as user phrasings, usually in the same order. One register, whichever routes better.
|
||||
- **Descriptive rather than imperative phrasing** (`This skill ...`, `This is the ...`).
|
||||
`Kyberforge.DescriptionOpener` catches any opener matching `^This`.
|
||||
- **Vague capabilities** ("helps with APIs" where "parses and validates OpenAPI specs" was
|
||||
available). `Kyberforge.VagueWording` catches the known filler; imprecision outside that list is
|
||||
judgment.
|
||||
- **Trigger-list, boundary or indirect-trigger content on a hand-invoked skill** — see Step 0 of
|
||||
`references/description-quality.md`.
|
||||
- **Over 1024 characters** — the agentskills.io specification ceiling, unchanged and independent
|
||||
of the 400-character house ceiling above.
|
||||
|
||||
Flag as SUGGESTION if:
|
||||
|
||||
- **Over 250 characters** but at or under 400. This tier is what moves the corpus average; the FAIL
|
||||
tier only stops outliers. Report it rather than treating a 399-character description as clean.
|
||||
- A near-miss exclusion is present but targets a weak near-miss.
|
||||
- An indirect trigger is present and warranted but could name the omitted phrasing more precisely.
|
||||
|
||||
**An unresolved boundary target is not graded here.** `validate.sh` owns that call and tiers it by
|
||||
notation — `/name` or an arrow form is an ERROR, the bare prose form a SUGGESTION unless a second
|
||||
target in the same sentence resolves — and Step 1 has already filed it under `### Structure` at that
|
||||
tier. Re-grading it as a description FAIL puts one target in the report twice at two tiers. What is
|
||||
left to judgment here is semantic and the script cannot reach it: whether a target that *does*
|
||||
resolve is the right sibling to exclude, and whether a clause naming no target at all ("examine the
|
||||
files manually") should have named one.
|
||||
|
||||
## body-discipline — `references/body-discipline.md`
|
||||
|
||||
Flag as FAIL if:
|
||||
|
||||
- A sentence answers "no" to the core test — it is padding
|
||||
- The body exceeds 900 words counted body-only (`validate.sh` reports it)
|
||||
- Two or more mutually exclusive flows are inlined instead of dispatched
|
||||
- A Gotcha paraphrases a step in the body below it that every branch reaching the Gotcha also
|
||||
reaches
|
||||
- A decision point presents a menu of options with no default
|
||||
- An instruction repeats content already in the description
|
||||
- A prescriptive sequence is used where flexibility is fine, or the reverse
|
||||
|
||||
Flag as SUGGESTION if:
|
||||
|
||||
- The body exceeds 600 words counted body-only but stays at or under 900
|
||||
- The Gotchas section carries more than five entries
|
||||
- The Gotchas section exceeds 25% of the body
|
||||
- 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
|
||||
- Content that only one branch reaches is inlined where a `references/` file would serve
|
||||
|
||||
## patterns — `references/patterns.md`
|
||||
|
||||
Flag as FAIL if:
|
||||
|
||||
- A Gotcha entry is a general tip or a reminder rather than a fact that defies a reasonable
|
||||
assumption
|
||||
- An inner code fence is unescaped inside a markdown block, breaking the render
|
||||
- A checklist wraps a single step
|
||||
- A conditional reference gives no trigger — `Kyberforge.PaddingPhrase` reports the common form
|
||||
- The agent must produce a specific format and no output template is given
|
||||
|
||||
Flag as SUGGESTION if:
|
||||
|
||||
- Gotchas are correctly formed but placed late in the body
|
||||
- An output template is present but permissive where the consumer needs it exact
|
||||
- A conditional reference names a trigger that is real but broader than the branch it guards
|
||||
|
||||
## file-structure and internal-consistency — `references/file-structure.md`
|
||||
|
||||
Flag as FAIL if:
|
||||
|
||||
- A directory outside the four permitted ones exists
|
||||
- Test files sit in `scripts/`
|
||||
- A non-spec file sits at the skill root
|
||||
- A path that resolves outside the skill directory appears outside the two exempt locations, in
|
||||
prose rather than in a fenced example
|
||||
- `tests/` exists but `tests/README.md` is missing or does not document its repo-level dependency
|
||||
- `SKILL.md` describes a script invocation the script does not accept
|
||||
|
||||
Flag as SUGGESTION if:
|
||||
|
||||
- An optional directory exists but holds only a placeholder README
|
||||
|
||||
## formatting and scripts — `references/formatting-and-scripts.md`
|
||||
|
||||
Flag as FAIL if:
|
||||
|
||||
- A script prompts interactively, in any form
|
||||
- A script exposes no `--help`
|
||||
- A destructive script has no `--dry-run`
|
||||
- Data and diagnostics share a stream, so the output cannot be piped
|
||||
- A relative path named in the body does not resolve
|
||||
- Heading levels are inconsistent enough to break the document's structure
|
||||
|
||||
Flag as SUGGESTION if:
|
||||
|
||||
- Exit codes are meaningful but undocumented in `--help`
|
||||
- A code block is untagged where a language applies
|
||||
- A script is idempotent in practice but does not say so, leaving a re-run's safety unclear
|
||||
- List indentation or section spacing is inconsistent without breaking the render
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-spec
|
||||
- agentskills-using-scripts
|
||||
---
|
||||
|
||||
# Formatting and Scripts Reference
|
||||
|
||||
Upstream source: agentskills.io — specification (body content), using-scripts (designing scripts
|
||||
for agentic use).
|
||||
|
||||
Read this when judging the **formatting** and **scripts** dimensions. Both are checklists of static
|
||||
criteria that never vary by skill, which is exactly why they live here rather than in the body.
|
||||
|
||||
## Formatting
|
||||
|
||||
- Heading levels are consistent: H2 for main sections, H3 for subsections. A body that jumps from
|
||||
H2 to H4, or opens on H3, reads as a fragment of a larger document.
|
||||
- Code blocks carry a language tag wherever one applies — `bash`, `markdown`, `python`, `yaml`,
|
||||
`text`. An untagged block loses syntax highlighting and, more importantly, loses the signal of
|
||||
what the agent is meant to do with it.
|
||||
- Whitespace is consistent: a blank line between sections, one list-indentation style throughout.
|
||||
- No broken relative paths in file references. Every `references/…`, `scripts/…` and `assets/…`
|
||||
path named in the body resolves against the skill directory.
|
||||
|
||||
## Scripts
|
||||
|
||||
A script in a skill is run by an agent with no terminal and no human to answer it. The criteria
|
||||
follow from that:
|
||||
|
||||
- **No interactive TTY prompts** — no `read`, no `input()`, no `readline`. A script that blocks on
|
||||
a prompt hangs the run with no diagnostic. `validate.sh` detects the common forms and reports
|
||||
them under Structure; the judgment call is any prompt it cannot pattern-match. What counts is
|
||||
where stdin comes from, not the word `read`: a `read` fed by a here-string, a here-doc, a pipe,
|
||||
or a redirect from a file never touches a terminal and is not a finding. `validate.sh` excludes
|
||||
those forms, so do not rewrite a working `read -r A B <<< "$line"` into parameter expansion to
|
||||
satisfy this rule.
|
||||
- **`--help` is exposed** and gives concise usage.
|
||||
- **Data to stdout, diagnostics to stderr.** A caller piping the script has to be able to separate
|
||||
the result from the commentary.
|
||||
- **Idempotent** — "create if not exists" rather than "create", so a re-run after a partial failure
|
||||
is safe.
|
||||
- **Meaningful exit codes, documented in `--help`.** An agent branches on the exit code; an
|
||||
undocumented one is a coin flip.
|
||||
- **`--dry-run` present for destructive operations.**
|
||||
|
||||
The FAIL and SUGGESTION criteria for this dimension live in `references/finding-criteria.md`,
|
||||
which Step 3 loads on every run.
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-spec
|
||||
- agentskills-best-practices
|
||||
---
|
||||
|
||||
# Patterns Reference
|
||||
|
||||
Upstream source: agentskills.io — best-practices (instruction patterns), specification.
|
||||
|
||||
Read this when judging the **patterns** dimension: whether each instruction construct a skill uses
|
||||
is the right construct for the job and is correctly formed. Formation, not content — a Gotcha's
|
||||
*content* is judged in `references/body-discipline.md`.
|
||||
|
||||
## The constructs and when each is right
|
||||
|
||||
| Construct | Right when | Wrong when |
|
||||
|---|---|---|
|
||||
| Gotchas | An environment fact contradicts a reasonable default | Used as a summary of the steps below |
|
||||
| Prescriptive sequence | The operation is fragile and flag order or exact arguments must not change | Several approaches are equally valid |
|
||||
| Checklist | A multi-step workflow the agent must complete in order | A single step dressed up as a list |
|
||||
| Conditional reference | Detail is needed on one branch only | The reference is needed on every run and is loaded blind |
|
||||
| Output template | The agent must emit a specific format a caller consumes | The output is prose nobody parses |
|
||||
|
||||
## Formation rules
|
||||
|
||||
**Gotchas** sit near the top of the body, before the steps that would otherwise walk into them.
|
||||
Placement late in the body is a SUGGESTION, not a FAIL — the content is still correct, it is just
|
||||
read after the mistake.
|
||||
|
||||
**Prescriptive sequences** that quote a fenced block inside another markdown block must escape the
|
||||
inner fence as `` \`\`\` ``. An unescaped inner fence terminates the outer block and the remaining
|
||||
instructions render as prose.
|
||||
|
||||
**Conditional references** state a specific trigger, naming a file that exists in the skill's own
|
||||
`references/` directory:
|
||||
|
||||
```text
|
||||
If the API returns a non-200 status, read `references/api-errors.md`.
|
||||
```
|
||||
|
||||
That block is fenced because the filename in it is illustrative — an unfenced `references/` pointer
|
||||
in a `SKILL.md` body must resolve on disk or the gate reports a hard ERROR. The generic
|
||||
form — pointing at the directory and hoping — defeats
|
||||
progressive disclosure, because the agent either loads everything or loads nothing.
|
||||
`Kyberforge.PaddingPhrase` catches the common generic phrasing deterministically; other malformed
|
||||
forms are judgment.
|
||||
|
||||
**Output templates** belong in the body when the agent must emit them on every run, and in
|
||||
`references/` when only one dispatch branch produces that output. A template inlined for a branch
|
||||
most invocations never take is body-discipline padding.
|
||||
|
||||
The FAIL and SUGGESTION criteria for this dimension live in `references/finding-criteria.md`,
|
||||
which Step 3 loads on every run.
|
||||
@@ -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, references/patterns.md, references/file-structure.md, references/formatting-and-scripts.md, references/finding-criteria.md, references/validation-scripts.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, references/patterns.md, references/finding-criteria.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, references/finding-criteria.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, references/formatting-and-scripts.md, references/finding-criteria.md, references/validation-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:** (none — creation guide not directly informing audit criteria)
|
||||
- **Status:** `extracted`
|
||||
@@ -1,150 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-spec
|
||||
- agentskills-using-scripts
|
||||
---
|
||||
|
||||
# Validation Scripts Reference
|
||||
|
||||
Read this when a Step 1 script fails, cannot run, or reports something that needs interpreting —
|
||||
including `validate-provenance.sh` exiting **0 having printed something**, which is INFO findings,
|
||||
not a clean run. Its silent exit 0 is the only outcome that needs nothing here.
|
||||
|
||||
## Report the gap, do not guess
|
||||
|
||||
If a script cannot run at all — Bash denied, `python3` unavailable, PyYAML not importable, `vale`
|
||||
not installed — say so as an **INFO** finding naming the script and the missing dependency, then
|
||||
fall back to the manual checks below. An INFO never changes PASS/FAIL. Silently omitting the
|
||||
dimension a script would have covered reports a clean audit that checked less than it claims to
|
||||
have checked, and the Step 4 coverage line then names a dimension nothing actually examined.
|
||||
|
||||
## Manual structural fallback
|
||||
|
||||
`validate.sh` needs `python3` **and** PyYAML, and refuses to start without either — the description
|
||||
value has to be measured after YAML folding is resolved, so skipping these gates would be a
|
||||
vacuous pass rather than a partial one. The two are checked separately, so the message already names
|
||||
the right one — report it verbatim rather than diagnosing further:
|
||||
|
||||
```text
|
||||
Error: python3 is required but was not found on PATH.
|
||||
Error: PyYAML is required but is not importable by python3.
|
||||
```
|
||||
|
||||
Without them — or with Bash denied, or on a permission error — work this list
|
||||
by hand and file the results under `### Structure` exactly as the script's output would have been:
|
||||
|
||||
- **`name`** present, 1–64 characters, kebab-case (lowercase letters, digits and hyphens; no
|
||||
leading, trailing or doubled hyphen), and **matching the skill's directory name** exactly.
|
||||
- **`description`** present and non-empty; no unfilled `FILL IN:` placeholder in it. An absent or
|
||||
empty description is a **FAIL**, never a silent skip — it is the one field preloaded into every
|
||||
session, so a skill without one can never be routed to.
|
||||
- **Description length**, measured on the folded YAML value with newlines collapsed to single
|
||||
spaces — not on the raw block scalar, which counts indentation. 250 characters SUGGESTION, 400
|
||||
FAIL (house), 1,024 FAIL (agentskills.io spec).
|
||||
- **Body length**, counting everything after the frontmatter's closing `---`. 600 words
|
||||
SUGGESTION, 900 FAIL (house).
|
||||
- **Whole-file ceilings**, counting the file including frontmatter: 500 lines FAIL, 2,770 words
|
||||
FAIL (agentskills.io spec). These are a different measurement from the two above — report them
|
||||
as separate findings, never merged.
|
||||
- **A boundary clause is present** — either the prose form (`do not` / `instead` / `rather than` /
|
||||
`not for`) or the compressed `Not <thing> -> <name>` arrow. **SUGGESTION**, not FAIL:
|
||||
the absence is deterministic, but whether this skill warrants one is the auditor's call.
|
||||
- **Boundary targets resolve** — **FAIL** on a name that resolves to nothing. See the section
|
||||
below; resolving these by hand is the one item on this list with a procedure of its own.
|
||||
- **Every `references/<file>.md` named in the body exists on disk** — **FAIL**, not a suggestion.
|
||||
A dispatch table or "read X" trigger naming a missing file sends the agent nowhere. Ignore
|
||||
mentions inside fenced code blocks, and ignore a mention whose own line says the file is gone
|
||||
(`removed`, `deleted`, `renamed`, `superseded`, `replaced`, `obsolete`, `deprecated`, `former`,
|
||||
`gone`, `no longer`, `used to`) — that is a historical note, not a dispatch entry.
|
||||
- **Gotchas discipline**, both **SUGGESTION**. Locate the section by a heading that *is* Gotchas
|
||||
(`## Common Gotchas` counts; `## Gotcha handling` and `## Why gotchas matter` do not), running to
|
||||
the next heading at the same level or shallower. More than five top-level entries is one
|
||||
suggestion; a section over 25% of the body word count is a second, independent one. Count
|
||||
entries at column 0 only — an indented child bullet is not an entry — and ignore fenced code
|
||||
blocks for both.
|
||||
- **No unfilled `FILL IN:` placeholder** anywhere in the body.
|
||||
- **Every file in `scripts/`** carries the executable bit and contains no interactive prompt —
|
||||
no bare `read`, no `select`, nothing that blocks on a TTY.
|
||||
|
||||
## Resolving boundary targets by hand
|
||||
|
||||
Targets are read from **both** boundary forms. The compressed `Not <thing> -> <name>` arrow and the
|
||||
prose form are each parsed *and* target-checked, so a typo in prose phrasing fails exactly as an
|
||||
arrow typo does — do not check only the names after an arrow.
|
||||
|
||||
Build the universe by walking up **from the `SKILL.md` under audit**, never from the validator's own
|
||||
location. The nearest ancestor holding `plugins/*/.apm/skills/` or `plugins/*/.apm/agents/` is the
|
||||
authoring root, falling back to the nearest ancestor holding `.git`. When one is found the universe
|
||||
is every skill and agent under `<root>/plugins/*/`, plus the skill's own apm package, plus the
|
||||
packages that package declares in its `apm.yml` under `dependencies.apm`. Deployed `.claude/` and
|
||||
`.agents/` trees are consulted **only** when no authoring root exists — they are gitignored
|
||||
`apm install` output, and reading them would make a fresh clone and a developer machine disagree.
|
||||
|
||||
Three ways to read the result wrong:
|
||||
|
||||
- **A hyphenated name used attributively is not a dangling target.** "Use pre-commit hooks instead
|
||||
of ad-hoc scripts" reads as a route to `pre-commit` on wording alone. What separates a route from
|
||||
prose is grammar: a route target is terminal — followed by punctuation, a conjunction, or a
|
||||
boundary word — whereas a compound modifier is followed by the noun it modifies. A name followed
|
||||
by an ordinary noun still *confirms* a route when it exists, but never raises a FAIL on its own.
|
||||
- **A SUGGESTION-tier unresolved target is not a FAIL you may promote.** Terminal position alone is
|
||||
not evidence of a route: "run `pre-commit` instead", "see `commit-msg`" and "use the clean-up
|
||||
instead" are all terminal and all prose. A prose-form target earns a FAIL only when its own
|
||||
sentence names another target that *does* resolve; otherwise the script reports it and moves on,
|
||||
and so should you. Route notation — `/name` and `-> name` — is exempt and always FAILs, and it is
|
||||
the fix to recommend when the author did mean a route.
|
||||
- **`INFO boundary-target resolution DID NOT RUN` is not a pass.** The script prints it, and exits
|
||||
0, when no universe could be determined for that path — the usual cause being a skill copy
|
||||
audited outside its package. Report it as an INFO naming the unchecked targets and re-run against
|
||||
the real directory; filing it as clean signs off targets nothing verified.
|
||||
|
||||
## Script-specific failures
|
||||
|
||||
- **`validate-provenance.sh` printed nothing *and exited 0*.** That is a pass, not a skip — it
|
||||
exits 0 silently when the skill has no `source_keys` and no `references/sources.md`, and nothing
|
||||
to validate is not a finding. Check the exit code before you believe the silence: a target that
|
||||
is not a directory, a directory holding no `SKILL.md`, a missing or extra argument, and an absent
|
||||
`python3` all exit **2** with a message on stderr. Exit 2 means the script never ran — report it
|
||||
as an unaudited dimension, never as a pass and never as a finding. Exit 1 is findings.
|
||||
- **A check-9 INFO — `'<field>' changed for '<slug>' since <ref>` — means go read, not just relay.**
|
||||
Check 9 diffs the current `references/sources.md` against a base ref and flags a slug whose
|
||||
`Description` or `Contributing files` text differs. It is structurally incapable of telling you
|
||||
whether the new wording is still *true* — it only detects that the text changed — so when this
|
||||
INFO fires, open that slug's own entry: the document named in its `Research doc:` field, and the
|
||||
files its `Contributing files` list names. Read whichever the changed field is a claim *about* —
|
||||
a Description-only change often leaves the file list untouched, so "open the Contributing files"
|
||||
is where to look, not proof that they are what moved. Confirm by reading whether the (possibly
|
||||
strengthened) claim genuinely holds. This is the one provenance finding this script cannot verify
|
||||
for you: every other check here is a structural fact you can relay as-is, but check 9's job is
|
||||
only to tell you *where* to spend that reading effort, not to replace it. Acknowledging the INFO
|
||||
without opening those files is not auditing it. Its companion — `'<field>' removed for '<slug>'
|
||||
since <ref>` — is the same obligation in the other direction: a claim withdrawn rather than
|
||||
rewritten. No other check here requires the field, so confirm the removal was deliberate.
|
||||
- **The check-9 base ref defaults to `git merge-base HEAD origin/main`, and there are two ways to
|
||||
override it.** `--base-ref=<ref>` on the command line, or the `VALIDATE_PROVENANCE_BASE_REF`
|
||||
environment variable; the flag wins when both are given, including when it is given empty
|
||||
(`--base-ref=`), which selects the default resolution and ignores the environment. Reach for one
|
||||
on a fork, a long-lived branch, or a mirror whose remote is not called `origin` — and when a
|
||||
review asks what changed since a specific commit rather than since the branch point.
|
||||
- **A single check-9 INFO naming a whole-check skip is an unaudited dimension, not a finding about
|
||||
the skill.** There are three: "no repo root above the skill directory", "no base ref could be
|
||||
resolved", and "`<path>` is not tracked at `<ref>`". The third is the one to read carefully — it
|
||||
fires when the base ref resolved but `git show <ref>:<path>` did not, which covers both a
|
||||
genuinely new `sources.md` (nothing to flag) and a path git does not know under that name: a
|
||||
renamed skill directory, or an installed, gitignored copy such as a deployed `.claude/skills/`
|
||||
tree. Auditing the deployed copy silently checks nothing; re-run against the authoring path under
|
||||
`plugins/*/.apm/skills/`.
|
||||
- **`vale` reports `0 files`.** Treat the pass as NOT RUN, not as clean, and fall back to full
|
||||
Step 3 judgment for the dimensions it would have covered. The bundled `Kyberforge` style is
|
||||
scoped by glob in `assets/vale/.vale.ini`; a file outside those globs is silently not linted.
|
||||
- **`E100 Runtime error ... does not exist` (exit 2) from `vale-wrap.sh`.** An explicit relative
|
||||
`--config` was passed. Pass none: the wrapper locates its own `assets/vale/.vale.ini` from its
|
||||
own path, so a resolved script path plus an unresolved config path produces exactly this. Do not
|
||||
read this exit code as vale being unavailable — that misreading sends the audit down the
|
||||
fallback path while vale was installed and working the whole time.
|
||||
- **The `vale` binary is genuinely absent** (`command not found`). Report one INFO naming it, then
|
||||
fall back to full Step 3 judgment for the description, body-discipline and patterns dimensions —
|
||||
the prefilter's whole coverage. Judge those by rubric rather than dropping them.
|
||||
- **A path argument that does not exist is a hard error** in `vale-wrap.sh`, deliberately: bare
|
||||
`vale` would fall back to reading stdin and print a clean-looking `0 errors ... in stdin`, which
|
||||
the `0 files` guard above does not catch.
|
||||
@@ -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[@]}"}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
||||
---
|
||||
name: skill-author
|
||||
description: >
|
||||
Use when the user wants to create a new skill from scratch, or apply audit
|
||||
findings, grill output, eval results, or inline feedback to an existing one.
|
||||
Not read-only review -> `skill-audit`. Not agent files -> `agent-author`.
|
||||
allowed-tools: Bash Read Write Edit
|
||||
metadata:
|
||||
version: "1.0.2"
|
||||
category: factory
|
||||
source_keys:
|
||||
- agentskills-home
|
||||
- agentskills-spec
|
||||
- agentskills-best-practices
|
||||
- agentskills-optimizing-descriptions
|
||||
- agentskills-evaluating-skills
|
||||
- agentskills-using-scripts
|
||||
- agentskills-quickstart
|
||||
---
|
||||
|
||||
## Gotchas
|
||||
|
||||
- The word gates are two measurements, not two tiers of one rule: the 2,770-word / 500-line spec backstop counts the whole file, Step 3's gate the body alone. Never unify them.
|
||||
- Never spawn a subagent to audit or recheck your own work — run `/skill-audit` inline, in the same context as the edits. Clean-context recheck belongs to `/forge`'s outer loop, and a self-spawned subagent's worktree can be torn down by concurrent cleanup, destroying an uncommitted draft.
|
||||
- Do not create new scripts unless a signal explicitly calls for it. Writing one from scratch requires out-of-scope transcript analysis — flag the opportunity as a suggestion instead.
|
||||
|
||||
## Step 1 — Dispatch
|
||||
|
||||
| Condition | Flow | Reference |
|
||||
|---|---|---|
|
||||
| No skill directory at the target path | Create | `references/create.md` |
|
||||
| Directory exists, at least one improvement signal present | Improve | `references/improve.md` |
|
||||
| Directory exists, no signals | Stop and ask | — |
|
||||
|
||||
Signals: grill output, `/skill-audit` findings, inline feedback, eval results, session context describing what went wrong. With none, ask whether the user meant to create a new skill or has feedback to apply.
|
||||
|
||||
Read only the reference matching the resolved flow — each is self-contained. If the target sits inside a git worktree, capture `rtk git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
|
||||
## Step 2 — Invocation axis
|
||||
|
||||
Decide before writing any description: model-invoked or hand-invoked?
|
||||
|
||||
- **Hand-invoked** — the user types `/name` and no agent should route to it. Set `disable-model-invocation: true` and write one plain human-facing sentence: no trigger list, no boundary clause. Skip Step 3's description rules.
|
||||
- **Model-invoked** — the default.
|
||||
|
||||
## Step 3 — Contract
|
||||
|
||||
Before writing or editing a description, or restructuring a body, read `references/contract.md` — the banned-content list, boundary form, include/exclude rubric and body patterns.
|
||||
|
||||
Gates `/skill-audit` enforces in both flows:
|
||||
|
||||
- **Description** — a trigger clause, at most one capability clause, and a boundary clause shaped `Not <thing> -> <skill-name>` whose target resolves to a real skill or agent. 250 characters SUGGESTION, 400 FAIL, value only.
|
||||
- **Body** — decision procedure only: ordered steps, branches, gates, and which reference to load when. 600 words SUGGESTION, 900 FAIL, body only. At two or more mutually exclusive flows a dispatch table is mandatory and each flow gets its own self-contained `references/` file.
|
||||
- **Gotchas** — each contradicting a reasonable default. A Gotcha paraphrasing a step below it is a FAIL; over five entries is a SUGGESTION only.
|
||||
|
||||
## Step 4 — Validate and close
|
||||
|
||||
Run `/skill-audit` on the resolved skill directory; resolve every FAIL before reporting done. It checks name-to-directory match, placeholders, both size budgets, boundary-target resolution and script hygiene — do not hand-check those. Hand-check the one thing it misses: an empty body reports `PASS SKILL.md body word count 0 (ADR-0020 target: 600)`, so confirm at least one non-empty section exists.
|
||||
|
||||
Bump `metadata.version`: the **minor** version on create (new skills start at `0.1.0`) and the **patch** version on improve.
|
||||
|
||||
**Commit verification.** Inside a git worktree: once the audit is clean, run `rtk git add` and `rtk git commit` — do not stop at staging. Re-run `rtk git log --oneline -1` and confirm the hash changed from Step 1's. A non-empty `git diff --stat` is not proof: staged-but-uncommitted work is part of no commit and is silently lost if the tree is cleaned up. Report done only once the hash has changed. Outside a worktree (a skill under `~/.claude/skills/`, say) nothing is committable — report done on a clean audit, naming that as the reason.
|
||||
@@ -1,135 +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: >
|
||||
Use when FILL IN: trigger.
|
||||
FILL IN: at most ONE capability clause, stated specifically
|
||||
(e.g. "parses and validates OpenAPI specs", not "helps with APIs").
|
||||
Not FILL IN: near-miss case -> FILL IN: real sibling skill.
|
||||
# Required. Preloaded into EVERY session whether or not the skill is invoked.
|
||||
# Exactly three parts, in this order: trigger clause, at most one capability
|
||||
# clause, boundary clause. Drop the boundary line if no near-miss skill exists.
|
||||
# Trigger clause: when should an agent activate this skill? Describe the user's
|
||||
# intent, not the skill's internal mechanics.
|
||||
# Budget: 250 characters target, 400 hard ceiling (counting this value only,
|
||||
# with YAML folding resolved). This scaffold sits at 214 — keep the fill-in
|
||||
# under the target rather than growing past it.
|
||||
# Boundary clauses may be plural: write one per genuine near-miss, and none
|
||||
# where no sibling could steal activations.
|
||||
# Never let a hyphenated skill name wrap across two lines of this folded block
|
||||
# — folding turns the break into a space and the routing target stops resolving.
|
||||
# Banned here: capability lists, output-format detail, composition notes,
|
||||
# implementation detail, and restating one trigger twice in two registers.
|
||||
# The boundary target must resolve to a real skill or agent — it is checked.
|
||||
# Add "even if the user doesn't mention X explicitly" ONLY when the user's
|
||||
# natural phrasing genuinely omits the domain word.
|
||||
|
||||
# disable-model-invocation: true
|
||||
# Optional. Hand-invoked skills only (reached solely by the user typing
|
||||
# /SKILL_NAME). With this set, replace the description above with ONE plain
|
||||
# human-facing sentence — no trigger clause, no boundary clause.
|
||||
|
||||
# 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:
|
||||
version: "0.1.0"
|
||||
# author: your-name
|
||||
# category: general
|
||||
# source_keys:
|
||||
# - source-slug-one
|
||||
# - source-slug-two
|
||||
# `metadata.version` is REQUIRED on every skill (ADR-0022) and is enforced by the
|
||||
# `skill-size-check` pre-commit hook. Three-component semver. A newly created
|
||||
# skill starts at "0.1.0" — leave the seeded value as it is; "1.0.0" is the seed
|
||||
# for a pre-existing skill retrofitted into the rule, not for a new one.
|
||||
# The rest of the map is optional: author, category, source_keys.
|
||||
# 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 — decision procedure ONLY
|
||||
|
||||
Keep here: ordered steps, decision branches, gates, and which reference
|
||||
file to load when.
|
||||
|
||||
Move to references/: lookup tables, spec restatements, output schemas,
|
||||
templates, example blocks, rationale prose, and anything only one branch
|
||||
reaches. Wire each one with the literal conditional form
|
||||
"If <condition>, read `references/<file>.md`." — a generic
|
||||
"see references/ for details" is a lint error.
|
||||
|
||||
Budget: 600 words target, 900 hard ceiling, counting THIS BODY ONLY
|
||||
(everything after the closing --- above). Separate from the whole-file
|
||||
spec backstop of 2,770 words / 500 lines — do not conflate them.
|
||||
|
||||
Delete this comment block before shipping.
|
||||
============================================================ -->
|
||||
|
||||
<!-- OPTIONAL but high-value: Gotchas. Place near the top — a gotcha read
|
||||
after the mistake is worthless.
|
||||
|
||||
Each entry states a fact that CONTRADICTS a reasonable default:
|
||||
something the agent gets wrong by acting sensibly. Maximum 5 entries.
|
||||
An entry that paraphrases a step below it is a failure, not a gotcha.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- FILL IN: fact that defies a reasonable assumption
|
||||
- FILL IN: non-obvious naming discrepancy or hidden constraint
|
||||
-->
|
||||
|
||||
<!-- DISPATCH — MANDATORY when this skill has two or more mutually exclusive
|
||||
flows. Keep only the dispatch table plus the gates common to every
|
||||
branch in this body; give each flow its own self-contained
|
||||
references/ file. Delete this block for a single-flow skill.
|
||||
|
||||
## Step 1 — Dispatch
|
||||
|
||||
| Condition | Flow | Reference |
|
||||
|---|---|---|
|
||||
| FILL IN: condition | FILL IN: flow | `references/FILL IN.md` |
|
||||
| FILL IN: condition | FILL IN: flow | `references/FILL IN.md` |
|
||||
|
||||
Read only the reference matching the resolved flow — each is self-contained.
|
||||
-->
|
||||
|
||||
<!-- OPTIONAL: single-flow workflow checklist. Delete if the skill dispatches.
|
||||
|
||||
## Workflow
|
||||
|
||||
- [ ] Step 1: FILL IN
|
||||
- [ ] Step 2: FILL IN
|
||||
- [ ] Step 3: FILL IN
|
||||
-->
|
||||
|
||||
<!-- OPTIONAL: gates that apply to every branch — validation, versioning,
|
||||
closing checks. Keep these in the body even when flows are dispatched.
|
||||
|
||||
## Step N — Validate and close
|
||||
|
||||
FILL IN: the check that must pass before this skill reports done.
|
||||
-->
|
||||
|
||||
## 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.
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,256 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-spec
|
||||
- agentskills-best-practices
|
||||
- agentskills-optimizing-descriptions
|
||||
---
|
||||
|
||||
# The description and body contract
|
||||
|
||||
House contract. Every rule here is enforced by `/skill-audit` —
|
||||
`scripts/validate.sh` for the counts and the boundary targets, the bundled Vale styles for the
|
||||
prose patterns, and its reference files for the judgment calls.
|
||||
|
||||
## Why the budget exists
|
||||
|
||||
A skill's `name` and `description` are loaded into every agent's context at the start of every
|
||||
session, whether or not the skill is ever invoked. The body is loaded only on invocation, and then
|
||||
competes with the caller's live conversation. Those are two different costs, so they get two
|
||||
different ceilings — and a fat description is not merely expensive. A description that summarizes
|
||||
the workflow gets followed *instead of* the body: a description saying "code review between tasks"
|
||||
produced one review from a skill whose flowchart specified two.
|
||||
|
||||
## Description
|
||||
|
||||
A description carries exactly three things:
|
||||
|
||||
1. **Trigger clause** — when to invoke, imperative: "Use when ...", never "This skill ...".
|
||||
Focus on user intent, not the skill's internal mechanics.
|
||||
2. **At most one capability clause** — what it does, one clause, no enumeration. Be specific
|
||||
("parses and validates OpenAPI specs", not "helps with APIs").
|
||||
3. **Boundary clause** — form: `Not <thing> -> <skill-name>.` Add one only where a near-miss skill
|
||||
could steal activations.
|
||||
|
||||
Banned from a description; move it to the body or to `README.md`:
|
||||
|
||||
- Capability enumeration or feature lists
|
||||
- Output-format detail ("Produces a compact findings report with Why and Fix per finding")
|
||||
- Composition or architecture notes ("composes X rather than duplicating Y", "This is a
|
||||
cross-cutting shared skill", "the human-facing entry point")
|
||||
- Implementation detail ("Self-validates via a bundled deterministic script")
|
||||
- Restating the same trigger twice in two registers — a verb list, then the same verbs re-quoted
|
||||
as user phrasings. This is a FAIL, not a suggestion.
|
||||
|
||||
**Indirect triggers are conditional, not mandatory.** Add "even if the user doesn't mention X
|
||||
explicitly" only where the user's natural phrasing genuinely omits the domain word — true for the
|
||||
`gitea-*` family, because people say "create an issue" rather than "create a Gitea issue"; false
|
||||
for `git-commits`, where the user says "commit". Adding one everywhere is what inflated this
|
||||
corpus, and it was deleted as a blanket rule.
|
||||
|
||||
**Boundary targets must resolve, and the notation decides how hard the gate bites.** Route
|
||||
notation — `/name`, or any arrow form (`-> name`, `` -> `name` ``) — is checked
|
||||
unconditionally: an unresolved target there is a blocking ERROR. The prose form ("do not use
|
||||
for X, use `y` instead") is only a SUGGESTION by default, because a bare hyphenated word in a
|
||||
boundary clause is as likely to be a tool, a file format or an English compound as a route. It
|
||||
is promoted to a blocking ERROR only when a second target in the same sentence *does* resolve,
|
||||
which corroborates that the name was meant as a route. So a typo does **not** dangle equally
|
||||
either way — write the arrow when you want the target checked. Targets resolve against a universe built by walking up **from the SKILL.md
|
||||
itself**: the nearest ancestor holding
|
||||
`plugins/*/.apm/{skills,agents}` (or, failing that, the nearest ancestor holding `.git`) contributes
|
||||
every skill and agent under `<root>/plugins/*/`, plus the skill's own apm package and the packages
|
||||
that package declares in `apm.yml` under `dependencies.apm`. A sibling plugin in the same monorepo
|
||||
therefore resolves; a skill in an unrelated repo does not. A boundary clause naming a target
|
||||
outside that universe sends the router nowhere — a blocking failure in arrow or `/name` form, and
|
||||
in prose form a SUGGESTION nobody is forced to act on, which is the worse outcome because it ships.
|
||||
Check the target exists before writing it — do not invent a plausible sibling name.
|
||||
|
||||
That universe is the apm marketplace and stops there. A **host built-in is not a routing target**:
|
||||
`/compact`, `/clear` and `/init` are Claude Code slash commands with no counterpart in Copilot CLI
|
||||
or Codex, and `.apm/` source compiles for all three, so routing to one is a portability defect. The
|
||||
gate is right to fail it and there is no allowlist. If a built-in genuinely needs mentioning, write
|
||||
it un-slashed — ``the `compact` built-in`` — which makes no routing claim and is not checked.
|
||||
|
||||
**One arrow, one target.** The resolver reads only the first name after an arrow, so a second is
|
||||
checked by nothing and the gate emits a SUGGESTION naming both. Split instead of conjoining:
|
||||
`Not <thing> -> first-skill. Not <other thing> -> second-skill.`, never
|
||||
`Not <thing> -> first-skill or second-skill`.
|
||||
|
||||
**Never let a hyphenated routing target wrap across lines in a folded `>` scalar.** YAML folding
|
||||
replaces the newline with a space, so `gitea-labels-` at the end of one line and `milestones` at
|
||||
the start of the next fold into `gitea-labels- milestones`. The gate then reads the target as
|
||||
`gitea-labels`, finds no such skill, and reports it dangling — this is what broke
|
||||
`gitea-labels-milestones`, and nothing in the source lines looks wrong. Reflow so the whole name
|
||||
sits on one line. The same applies to any backticked skill or agent name anywhere in a description.
|
||||
|
||||
**Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only
|
||||
with YAML folding resolved. The agentskills.io 1,024-character spec limit is unchanged and sits
|
||||
above both. The SUGGESTION tier is the one that moves the average; treat 250 as the target and 400
|
||||
as the outlier stop.
|
||||
|
||||
**Hand-invoked skills are exempt.** A skill carrying `disable-model-invocation: true` is absent
|
||||
from the model-visible listing and is reached only by the user typing `/name`. It takes one plain
|
||||
human-facing sentence — no trigger clause, no boundary clause, no indirect triggers. Worked
|
||||
example — the whole description of the `zoom-out` skill, which carries `disable-model-invocation`:
|
||||
|
||||
````markdown
|
||||
Tell the agent to zoom out and give broader context or a higher-level perspective. Use when
|
||||
you're unfamiliar with a section of code or need to understand how it fits into the bigger
|
||||
picture.
|
||||
````
|
||||
|
||||
## Body
|
||||
|
||||
The body carries the **decision procedure only**: ordered steps, decision branches, gates, and
|
||||
which reference to load when. Everything else moves to `references/`.
|
||||
|
||||
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 — 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
|
||||
|
||||
Exclude:
|
||||
|
||||
- Concepts the agent already knows (what JSON is, how HTTP works) — tokens without behavior change
|
||||
- Exhaustive option lists — pick a default; the agent does not benefit from choosing
|
||||
- Steps the agent handles independently — over-specifying leads agents down unproductive paths
|
||||
- Restatements of the description — it is already in context
|
||||
|
||||
Move to `references/`: lookup tables, spec restatements, output schemas, templates, example
|
||||
blocks, rationale prose, and any content only one branch reaches. Each reference file is
|
||||
self-contained for its concern, and every one is wired from the body with the literal conditional
|
||||
form:
|
||||
|
||||
**The one exception, stated once so it is not re-litigated:** an output schema stays in the body
|
||||
only when it applies to *every* flow and is short — roughly 50 words or less, which is the "Output
|
||||
format template" pattern below. An output schema that is longer than that, or that only one flow
|
||||
produces, moves to `references/` like any other schema. No third option exists, and the two rules
|
||||
do not disagree.
|
||||
|
||||
````markdown
|
||||
If <condition>, read `references/<file>.md`.
|
||||
````
|
||||
|
||||
A generic pointer ("see references/ for details") is a Vale error — the agent cannot act on it.
|
||||
|
||||
**A dispatch table is the wiring.** Where the body dispatches, a row already pairs a condition with
|
||||
a target, which is what the literal form encodes — so do not restate each row underneath as a prose
|
||||
conditional. That duplicates the routing in the one body whose whole purpose is to be short. The
|
||||
literal form is what a reference loaded *without* a table needs: a mid-procedure deepening, an
|
||||
escape hatch, an error path. A table earns this on four properties — every flow has a row and every
|
||||
row's target exists on disk; each row pairs exactly one target with a condition the agent can
|
||||
evaluate from the request, never a literal slash invocation; one line after the table names the
|
||||
matched file as the only one to read; and the gates every branch needs sit in the body, not inside
|
||||
one flow's file. That last one is the property the `git-commits` v0.1.2 failure turned on, and it is
|
||||
the one a dispatch split is most likely to break. `skill-audit`'s `references/body-discipline.md`
|
||||
carries the audit-side form of the same exemption; the two lists are the same four properties, and
|
||||
an edit to either belongs in both.
|
||||
|
||||
**Dispatch is mandatory at two or more mutually exclusive flows.** The body carries the dispatch
|
||||
table and the gates common to every branch; each flow gets its own self-contained `references/`
|
||||
file. Exemplar: the `apm-workflow` skill — a **294-word body** dispatching to 3,154 words of
|
||||
references across five flow files. Calibrate against 294: that file's whole-file count is 348
|
||||
words, and aiming at that number instead overshoots the body budget by ~18%. The 3,154 excludes
|
||||
`references/sources.md`, which is a provenance record and is never loaded at runtime.
|
||||
|
||||
**Length.** 600 words SUGGESTION, 900 words FAIL, counting the **body only** — everything after
|
||||
the frontmatter's closing `---`.
|
||||
|
||||
## Gotchas section
|
||||
|
||||
- Each entry must state a fact that **contradicts a reasonable default** — something the agent
|
||||
gets wrong by acting sensibly. "Write a descriptive commit message" is not one; the agent does it
|
||||
unprompted and nothing in the environment argues against it. A safety gate is a different case,
|
||||
even where the agent knows the rule — see the paraphrase bullet below.
|
||||
- More than five entries is a SUGGESTION — five is the guideline, not a ceiling.
|
||||
- A Gotcha that paraphrases a step in the body below it is a **FAIL**, but deleting it is correct
|
||||
only when the surviving copy is **reachable from every branch that reaches the Gotcha**. In a
|
||||
dispatch body it often is not: each flow file loads alone, so a step in one is invisible to an
|
||||
invocation that took another branch. Where the restated rule is a safety gate more than one flow
|
||||
needs, move it into the body's common-gates section instead of dropping it. `git-commits` v0.1.2
|
||||
is the worked failure: the retrofit deleted its always-loaded "never commit secrets" Gotcha in
|
||||
favour of a step in one flow file, and left the history-rewrite branch — which stages and
|
||||
`--amend`s, committing new content exactly as a fresh commit does — with no such check anywhere
|
||||
in its loaded context, against this repo's governance hard prohibitions. v0.1.3 carries the rule
|
||||
as a gate on every flow.
|
||||
- A Gotchas section exceeding 25% of the body is a SUGGESTION.
|
||||
- Place the section near the top — a gotcha read after the mistake is worthless.
|
||||
|
||||
## Two size gates, two measurements
|
||||
|
||||
| Gate | SUGGESTION | FAIL | Counts |
|
||||
|---|---|---|---|
|
||||
| description | 250 chars | 400 chars | the `description:` value only |
|
||||
| body | 600 words | 900 words | the body only, after the closing `---` |
|
||||
| spec backstop | — | 1,024 chars | the `description:` value only |
|
||||
| spec backstop | — | 2,770 words / 500 lines | the **whole file**, frontmatter included |
|
||||
|
||||
The 600/900 pair and the 2,770/500 pair are not the same measurement and must not be unified: the
|
||||
first is a quality gate on what the caller's context absorbs, the second a conformance backstop on
|
||||
the file. A skill can sit well inside one and fail the other.
|
||||
|
||||
When a body approaches its ceiling, relocate rather than delete — move reference material to
|
||||
`references/<topic>.md` behind a conditional trigger, and bundle repeated executable logic into
|
||||
`scripts/` rather than reinventing it each run.
|
||||
|
||||
## Body patterns
|
||||
|
||||
**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: ...
|
||||
````
|
||||
|
||||
**Dispatch table** (two or more mutually exclusive flows):
|
||||
|
||||
````markdown
|
||||
| Condition | Flow | Reference |
|
||||
|---|---|---|
|
||||
| <condition> | <flow> | `references/<file>.md` |
|
||||
````
|
||||
|
||||
**Output format template** (when the skill produces structured output on *every* flow, and the
|
||||
schema is roughly 50 words or less — see the exception under Body above; anything longer or
|
||||
flow-specific belongs in `references/`):
|
||||
|
||||
````markdown
|
||||
Output format:
|
||||
```
|
||||
<field>: <value>
|
||||
```
|
||||
````
|
||||
|
||||
For longer templates, place them in `references/<topic>.md` or `assets/<name>.md` and reference
|
||||
conditionally.
|
||||
|
||||
## 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 the create flow's 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.
|
||||
@@ -1,186 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-home
|
||||
- agentskills-spec
|
||||
- agentskills-best-practices
|
||||
- agentskills-quickstart
|
||||
- agentskills-using-scripts
|
||||
---
|
||||
|
||||
# Creating a new skill
|
||||
|
||||
Return to `SKILL.md` Step 4 once Step 6 below is done — validation, versioning and commit
|
||||
verification are shared with the improve flow and are not repeated here.
|
||||
|
||||
## 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?
|
||||
- [ ] Skill name (kebab-case) and destination path
|
||||
|
||||
If any are missing, stop and ask the user before proceeding.
|
||||
|
||||
**Requires `/skill-audit`** — used in `SKILL.md` Step 4 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.
|
||||
|
||||
## Package-intent gate
|
||||
|
||||
Judge whether the destination is meant to be inside an APM package before running the scaffold
|
||||
script — the script cannot tell "no package here" apart from "package not scaffolded yet":
|
||||
|
||||
- Package intent but no `type:`-bearing `apm.yml` found at or 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. Do not fall through to standalone mode.
|
||||
- Otherwise (a `~/`-rooted destination, or no package context implied) → continue to Step 1.
|
||||
|
||||
## 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>/`.
|
||||
|
||||
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 is not part of
|
||||
`scripts/new-skill.sh`.
|
||||
|
||||
## Step 3 — Fill in SKILL.md
|
||||
|
||||
Open the new skill's `SKILL.md` (the path Step 1 printed) and replace every `FILL IN:`
|
||||
placeholder. The scaffold template carries the body skeleton and the two frontmatter
|
||||
fields that cannot be left as placeholders — `name`, substituted by the script, and
|
||||
`metadata.version`, seeded live at `"0.1.0"` — so fill the template in rather than restructuring
|
||||
it.
|
||||
|
||||
**`name`** — already set by the scaffold script. Must exactly match the directory name. Format:
|
||||
1–64 characters, lowercase letters, numbers and hyphens only; no leading, trailing or consecutive
|
||||
hyphens (`--`).
|
||||
|
||||
**`description`** — carries the entire triggering burden and is preloaded every session. Write it
|
||||
against `references/contract.md`, which holds the three-part shape, the banned content, the
|
||||
boundary-clause form and the length tiers. A hand-invoked skill (`SKILL.md` Step 2) takes one
|
||||
plain sentence and `disable-model-invocation: true` instead.
|
||||
|
||||
**`metadata.version`** — required on every skill (ADR-0022), not a per-skill or per-plugin choice,
|
||||
and enforced by the `skill-size-check` pre-commit hook. The scaffold seeds a new skill at
|
||||
`"0.1.0"`; leave that value alone here and let `SKILL.md` Step 4 bump it. (`"1.0.0"` is the seed
|
||||
for a pre-existing skill retrofitted into the rule, and never applies to a skill created here.)
|
||||
|
||||
**Optional frontmatter** — 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` — the rest of the map, all of it optional: `author` and `category`, plus `source_keys`
|
||||
now (Step 6) if research sources are in context
|
||||
- `allowed-tools` — space-separated pre-approved tools; reduces permission prompts (experimental —
|
||||
support varies by client)
|
||||
- `disable-model-invocation` — hand-invoked skills only
|
||||
|
||||
**`metadata.source_keys`** — if research sources are in context, list the relevant slugs as you
|
||||
write the body; do not 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
|
||||
```
|
||||
|
||||
**Body** — write the decision procedure only, following the body rules and patterns in
|
||||
`references/contract.md`. Rename the placeholder section headings to ones that fit the skill's
|
||||
structure.
|
||||
|
||||
## Step 4 — Add scripts (if needed)
|
||||
|
||||
Place executable scripts in `scripts/`. Critical rule: **no interactive prompts** — agents run
|
||||
non-interactive, and 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, named in
|
||||
kebab-case after the topic. Reference conditionally from SKILL.md with the literal form
|
||||
``If <condition>, read `references/<file>.md` ``.
|
||||
|
||||
**Two hops from `SKILL.md`, never three.** A flow file may route on to a shared contract or
|
||||
sub-topic file — that is the shipped pattern here (`SKILL.md` → `references/create.md` → this
|
||||
file's own pointers to `contract.md`, `scripts.md` and `deployment-modes.md`). What does not work
|
||||
is a third hop: a file reachable only through two intermediates is rarely loaded at the moment it
|
||||
is needed. Every hop past the first also needs the same literal conditional form, so the agent
|
||||
knows when to take it.
|
||||
|
||||
**`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`.
|
||||
|
||||
Then return to `SKILL.md` Step 4.
|
||||
@@ -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.
|
||||
@@ -1,91 +0,0 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-best-practices
|
||||
- agentskills-evaluating-skills
|
||||
- agentskills-optimizing-descriptions
|
||||
---
|
||||
|
||||
# Improving an existing skill
|
||||
|
||||
Return to `SKILL.md` Step 4 once Step 4 below is done — validation, versioning and commit
|
||||
verification are shared with the create flow and are not repeated here.
|
||||
|
||||
## 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 directory 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."
|
||||
|
||||
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. Patching per symptom is the default failure mode:
|
||||
three eval failures may all trace to one missing instruction. 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, do not patch.** Find the underlying gap, not the specific example that failed. A fix
|
||||
scoped only to the test cases you have seen will overfit and perform worse on new inputs.
|
||||
|
||||
**Keep it lean.** Remove instructions that are not 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.
|
||||
|
||||
**Retrofit before extending.** Any edit to a skill that does not meet the contract has to bring it
|
||||
into compliance first — the gates are hot and carry no baseline file, so a one-line fix to a
|
||||
non-compliant skill cannot be committed until the description and body meet
|
||||
`references/contract.md`. Treat that retrofit as part of the same change, not a follow-up.
|
||||
|
||||
If a signal points to a script or reference file, edit that file directly rather than adding a
|
||||
workaround in SKILL.md.
|
||||
|
||||
**A skill carrying no `metadata.version` is seeded at `"1.0.0"`, not bumped** — `SKILL.md` Step 4's
|
||||
patch bump presumes a version to bump, and ADR-0022 reserves `"0.1.0"` for a newly created skill.
|
||||
|
||||
**Check for regressions before handing back.** `SKILL.md` Step 4 tells you to resolve every FAIL,
|
||||
which says nothing about a check that passed *before* these edits and no longer does. Compare the
|
||||
closing audit against the skill's pre-edit state — a PASS that has become a SUGGESTION, or a
|
||||
SUGGESTION that has become a FAIL, is damage this flow caused and is in scope for it. Only the
|
||||
improve flow can make that comparison; the create flow has no prior state to compare against.
|
||||
|
||||
Then return to `SKILL.md` Step 4.
|
||||
@@ -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.
|
||||
@@ -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, references/create.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/create.md, references/contract.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, references/create.md, references/improve.md, references/contract.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/improve.md, references/contract.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, references/improve.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/create.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, references/create.md
|
||||
- **Status:** `extracted`
|
||||
@@ -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/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 " Description: 250 chars target / 400 ceiling. Body: 600 / 900, body only." >&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
|
||||
Reference in New Issue
Block a user