feat(kyberforge): retarget agent-author to plugin-scope .apm/agents/
Plugin scope now authors a single vendor-neutral .apm/agents/<name>.agent.md file (name/description/model/body only) instead of a Claude Code + Copilot CLI pair, per ADR-0016: apm compile has no per-target field integrator, so tools: and all Claude-only fields (isolation/maxTurns/effort/memory/ permissionMode) are omitted entirely rather than shipping a value that's guaranteed wrong on one harness. Scope detection switches from a flat plugin.json check to the same apm.yml+type: walk-up agent-audit's validate.sh already uses. Project scope and user scope are unaffected — both keep the existing dual-file pair model. Refs: #89
This commit is contained in:
@@ -4,7 +4,7 @@ Creates and improves agent definition files for Claude Code and GitHub Copilot C
|
||||
|
||||
## What it does
|
||||
|
||||
Scaffolds and fills in agent definition files (`.md` for Claude Code, `.agent.md` for Copilot CLI) at plugin, project, or user scope. Always generates both provider files in one pass. Also applies improvement signals — grill output, inline feedback, session context — to existing agent files. Bumps the plugin version in both manifests after every change (minor for new agents, patch for improvements).
|
||||
Scaffolds and fills in agent definition files at plugin/APM, project, or user scope. Project and user scope always generate a Claude Code + Copilot CLI file pair (`.md` + `.agent.md`) in one pass. Plugin/APM scope generates a single vendor-neutral `.apm/agents/<name>.agent.md` file instead — no separate Claude Code / Copilot split, since `apm compile` has no per-target field integrator (see ADR-0016). Also applies improvement signals — grill output, inline feedback, session context — to existing agent files. Bumps the version after every change: the resolved package's `apm.yml` at plugin/APM scope (minor for new agents, patch for improvements); project/user scope has no manifest to bump.
|
||||
|
||||
## Before you start
|
||||
|
||||
@@ -21,7 +21,7 @@ Have ready: the agent's name (kebab-case), the root directory (plugin root, proj
|
||||
bash scripts/new-agent.sh <agent-name> <root>
|
||||
|
||||
# Examples:
|
||||
bash scripts/new-agent.sh code-reviewer plugins/kyberforge/
|
||||
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 ~
|
||||
```
|
||||
@@ -31,12 +31,13 @@ bash scripts/new-agent.sh security-reviewer ~
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SKILL.md` | Skill instructions for agents |
|
||||
| `scripts/new-agent.sh` | Scaffolds Claude Code and Copilot CLI agent files from templates |
|
||||
| `references/deployment-modes.md` | Plugin vs project vs user scope: restrictions, scoped identifiers, path conventions |
|
||||
| `scripts/new-agent.sh` | Scaffolds agent definition file(s) from templates — a single `.apm/agents/<name>.agent.md` at plugin/APM scope, or a Claude Code + Copilot CLI pair at project/user scope |
|
||||
| `references/deployment-modes.md` | Plugin/APM vs project vs user scope: restrictions, scoped identifiers, path conventions |
|
||||
| `references/scripts.md` | Conventions for new-agent.sh and any future scripts: contract, template variables, file placement, error messages |
|
||||
| `references/sources.md` | Research provenance — sources that informed this skill |
|
||||
| `assets/templates/claude-code.md` | Annotated Claude Code agent definition template |
|
||||
| `assets/templates/copilot.agent.md` | Annotated Copilot CLI agent definition template |
|
||||
| `assets/templates/claude-code.md` | Annotated Claude Code agent definition template (project/user scope) |
|
||||
| `assets/templates/copilot.agent.md` | Annotated Copilot CLI agent definition template (project/user scope) |
|
||||
| `assets/templates/apm-agent.md` | Annotated vendor-neutral APM agent definition template (plugin/APM scope) |
|
||||
| `tests/new-agent.bats` | bats tests for `scripts/new-agent.sh` |
|
||||
| `assets/README.md` | Directory meta-documentation for assets/ |
|
||||
| `references/README.md` | Directory meta-documentation for references/ |
|
||||
|
||||
@@ -3,14 +3,16 @@ name: agent-author
|
||||
description: >
|
||||
Use when the user wants to create a new agent definition file from scratch
|
||||
("write an agent for X", "build a subagent that does Y", "create an agent
|
||||
definition for Z"), or improve an existing one. Handles both Claude Code and
|
||||
GitHub Copilot CLI agent formats, at plugin, project, and user scope — always
|
||||
generating both provider files in one pass. Also use when the user provides
|
||||
inline feedback about an agent's behavior and wants it applied, or when a
|
||||
grill session has produced findings the user wants acted on — even if they
|
||||
don't say "improve" explicitly. Do not use for read-only review — examine
|
||||
agent files manually or run a grill session to generate improvement signals.
|
||||
Do not use to author skills — use /skill-author instead.
|
||||
definition for Z"), or improve an existing one. Handles agent definitions at
|
||||
plugin/APM, project, and user scope. Project and user scope always generate
|
||||
a Claude Code (`.md`) + Copilot CLI (`.agent.md`) file pair in one pass;
|
||||
plugin/APM scope generates a single vendor-neutral `.apm/agents/<name>.agent.md`
|
||||
file instead (no per-target Claude Code / Copilot split). Also use when the
|
||||
user provides inline feedback about an agent's behavior and wants it applied,
|
||||
or when a grill session has produced findings the user wants acted on — even
|
||||
if they don't say "improve" explicitly. Do not use for read-only review —
|
||||
examine agent files manually or run a grill session to generate improvement
|
||||
signals. Do not use to author skills — use /skill-author instead.
|
||||
allowed-tools: Bash Read Write Edit
|
||||
metadata:
|
||||
category: factory
|
||||
@@ -26,18 +28,19 @@ metadata:
|
||||
|
||||
## Gotchas
|
||||
|
||||
- If the agent lives inside a plugin (its path contains a `plugin.json`), bump the plugin version after every change — in both `plugin.json` and `.claude-plugin/plugin.json` in the same edit pass. Convention: new agent → minor bump; improvement or fix → patch bump. Plugin consumers compare this version to detect updates; skipping it makes the change invisible.
|
||||
- Plugin agents silently ignore `hooks`, `mcpServers`, and `permissionMode` — these fields have no effect and produce no warning. They only work in `.claude/agents/` or `~/.claude/agents/`.
|
||||
- At plugin/APM scope, bump the resolved package's `apm.yml` `version` after every change — minor for a new agent, patch for a fix. Consumers compare this version to detect updates; skipping it hides the change.
|
||||
- At plugin/APM scope, `tools` and all Claude-only fields (`isolation`, `maxTurns`, `effort`, `memory`, `permissionMode`, `disallowedTools`, `skills`, `color`, `initialPrompt`, `background`, `hooks`, `mcpServers`) are omitted entirely, not merely restricted (ADR-0016: `apm compile` copies frontmatter verbatim to both harnesses with no per-target integrator, so a harness-specific value is wrong on at least one). Only project/user scope supports these fields.
|
||||
- An `apm.yml` with no top-level `type:` field is a marketplace-only manifest, not a package root — the walk-up skips it and keeps going.
|
||||
- `AskUserQuestion`, `EnterPlanMode`, `ExitPlanMode`, `ScheduleWakeup`, and `WaitForMcpServers` are never available to any subagent regardless of the `tools` field. Exception: `ExitPlanMode` is available when the parent session runs in `permissionMode: plan`.
|
||||
- Duplicate `name` values in the same scope: Claude Code silently discards one without warning. Always verify uniqueness before shipping.
|
||||
- Plugin agents in subdirectories get scoped identifiers (`plugin:folder:name`). Keep agents flat in `agents/` to avoid this.
|
||||
- Copilot CLI agent files **must** use the `.agent.md` extension. A plain `.md` file is not picked up by Copilot.
|
||||
- Copilot has no `permissionMode`, `maxTurns`, `isolation`, or `memory` fields — do not include them in the Copilot file.
|
||||
- Plugin agents in subdirectories get scoped identifiers (`plugin:folder:name`) — keep agents flat in `agents/` to avoid this. Applies to project/user-scope Claude Code agents only.
|
||||
- Copilot CLI agent files **must** use the `.agent.md` extension — a plain `.md` file isn't picked up. The plugin/APM-scope single file also ends in `.agent.md` by convention, but it's vendor-neutral, not Copilot-only — it compiles to Claude Code too.
|
||||
- Copilot has no `permissionMode`, `maxTurns`, `isolation`, or `memory` fields — do not include them in project/user-scope Copilot files.
|
||||
- `model` resolution order for Claude Code: `CLAUDE_CODE_SUBAGENT_MODEL` env var → per-invocation parameter → frontmatter `model` → main session model. The frontmatter value is a low-priority default, not a guarantee.
|
||||
|
||||
## Route
|
||||
|
||||
If the destination is a plugin directory, read `references/deployment-modes.md`.
|
||||
If the destination resolves to plugin/APM scope (scope detection in Step 1 finds a `type:`-bearing `apm.yml` at or above the root), read `references/deployment-modes.md`.
|
||||
|
||||
Determine which flow before touching the filesystem:
|
||||
|
||||
@@ -53,7 +56,7 @@ Signals: grill session output, inline user feedback, session context describing
|
||||
|
||||
Before touching the filesystem, confirm you have:
|
||||
- [ ] Agent name (kebab-case, e.g. `code-reviewer`)
|
||||
- [ ] Root directory (plugin root, project root, or `~` for user scope)
|
||||
- [ ] Root directory (a path inside a package for plugin/APM scope, project root, or `~` for user scope)
|
||||
- [ ] Agent purpose — one sentence describing the task this agent handles
|
||||
- [ ] Trigger condition — when should the runtime delegate to this agent?
|
||||
|
||||
@@ -71,19 +74,25 @@ bash scripts/new-agent.sh <name> <root>
|
||||
|
||||
Examples:
|
||||
```bash
|
||||
bash scripts/new-agent.sh code-reviewer plugins/kyberforge/
|
||||
bash scripts/new-agent.sh code-reviewer packages/my-package/ # plugin/APM scope if packages/my-package/apm.yml has a type: field
|
||||
bash scripts/new-agent.sh deploy-assistant .
|
||||
bash scripts/new-agent.sh security-reviewer ~
|
||||
```
|
||||
|
||||
**Scope detection (script handles this automatically):**
|
||||
- Root contains `plugin.json`, `.claude-plugin/plugin.json`, `.plugin/plugin.json`, or `.github/plugin/plugin.json` → plugin scope → creates `<root>/agents/<name>.md` + `<root>/agents/<name>.agent.md`
|
||||
- Root is a project directory (no plugin marker) → creates `<root>/.claude/agents/<name>.md` + `<root>/.github/agents/<name>.agent.md`
|
||||
- Root is `~` → creates `~/.claude/agents/<name>.md` + `~/.copilot/agents/<name>.agent.md`
|
||||
**Scope detection (script handles this automatically).** The script walks up from `<root>` for a package boundary — same shape `agent-audit`'s `validate.sh` uses:
|
||||
- Nearest ancestor `apm.yml` with a top-level `type:` field (`instructions`/`skill`/`hybrid`/`prompts`) → **plugin/APM scope** → `<package-root>/.apm/agents/<name>.agent.md` (single vendor-neutral file). A `type:`-less `apm.yml` is marketplace-only — skipped, walk continues upward.
|
||||
- No such `apm.yml`, `<root>` is a project directory → **project scope** (unchanged) → `<root>/.claude/agents/<name>.md` + `<root>/.github/agents/<name>.agent.md`
|
||||
- `<root>` is exactly `~` (checked directly, no walk-up) → **user scope** (unchanged) → `~/.claude/agents/<name>.md` + `~/.copilot/agents/<name>.agent.md`
|
||||
|
||||
A bare `plugin.json` with no `apm.yml` no longer signals plugin scope — that path is fully replaced, not dual-mode; it falls through to project scope.
|
||||
|
||||
The script is file-by-file no-op — it skips any file that already exists.
|
||||
|
||||
### Step 2 — Fill in the Claude Code agent file (`<name>.md`)
|
||||
### Step 2 — Fill in the agent file(s)
|
||||
|
||||
**At plugin/APM scope**, there is exactly one file: `<package-root>/.apm/agents/<name>.agent.md`. Frontmatter carries ONLY `name`, `description`, and optionally `model` — never `tools` or the other Claude-only fields listed in Gotchas (ADR-0016). Fill in `name`, `description`, `model`, and the system prompt body per the guidance below; the rest of this step's field-by-field guidance (tools, maxTurns, effort, memory, isolation, disallowedTools, skills, color, initialPrompt, background) is project/user scope only. Skip Step 3 and go to Step 4.
|
||||
|
||||
**At project/user scope**, continue below to fill in both provider files — this step covers the Claude Code file (`<name>.md`); Step 3 covers the Copilot file.
|
||||
|
||||
Open the scaffolded Claude Code file. Replace every `FILL IN:` placeholder. **Remove all template documentation comments from the YAML frontmatter after filling in required fields** — these are marked with `<!--` and `-->` and must be deleted before shipping.
|
||||
|
||||
@@ -95,9 +104,9 @@ Open the scaffolded Claude Code file. Replace every `FILL IN:` placeholder. **Re
|
||||
- Specific about the triggering condition and expertise domain
|
||||
- Under 300 characters preferred
|
||||
|
||||
**`tools`** — restrict to what the agent actually needs. Omit to inherit all tools. Use `Agent(type1,type2)` to limit which subagent types this agent can spawn; omit `Agent` entirely to prevent spawning.
|
||||
**`tools`** (project/user scope only — never at plugin/APM scope) — restrict to what the agent actually needs. Omit to inherit all tools. Use `Agent(type1,type2)` to limit which subagent types this agent can spawn; omit `Agent` entirely to prevent spawning.
|
||||
|
||||
**Optional fields worth considering:**
|
||||
**Optional fields worth considering (project/user scope only — never at plugin/APM scope):**
|
||||
- `model`: set when this agent needs a different capability tier (`haiku` for fast tasks, `opus` for deep reasoning)
|
||||
- `maxTurns`: set a cap to prevent runaway agents on bounded tasks
|
||||
- `effort`: set to `low` for single-lookup tasks, `high` or above for deep reasoning or multi-file analysis — overrides session effort level; omit to inherit
|
||||
@@ -109,7 +118,7 @@ Open the scaffolded Claude Code file. Replace every `FILL IN:` placeholder. **Re
|
||||
- `initialPrompt`: auto-submitted as the first turn when this agent activates as the main session thread; only set when this agent is intended for main-thread activation
|
||||
- `background`: set `true` to force background execution
|
||||
|
||||
**`source_keys`** — top-level list of research source slugs that informed this agent. Add only when research sources were used (i.e. entries with `` `extracted` `` status are in context from a prior `/research` session). Each slug must match an H2 heading in `sources.md` (plugin root). Omit entirely when no research was used.
|
||||
**`source_keys`** — top-level list of research source slugs that informed this agent. Add only when research sources were used (i.e. entries with `` `extracted` `` status are in context from a prior `/research` session). Each slug must match an H2 heading in `sources.md` — see Step 4 for where that file lives (plugin/APM scope only). Omit entirely when no research was used.
|
||||
|
||||
```yaml
|
||||
source_keys:
|
||||
@@ -121,12 +130,14 @@ source_keys:
|
||||
- Cover: inputs expected, process steps, output format, error handling
|
||||
- One job per agent
|
||||
|
||||
### Step 3 — Fill in the Copilot agent file
|
||||
### Step 3 — Fill in the Copilot agent file (project/user scope only)
|
||||
|
||||
Skip this step entirely at plugin/APM scope — there is no separate Copilot file there. The single `.apm/agents/<name>.agent.md` file from Step 2 already compiles to both Claude Code and Copilot CLI via `apm compile`.
|
||||
|
||||
**Two distinct Copilot agent formats** exist, with different paths and field sets. Choose one based on the deployment target:
|
||||
|
||||
**CLI format** (default — what the scaffold creates):
|
||||
- Path: `.github/agents/<name>.agent.md` (project) or `<plugin>/agents/<name>.agent.md` (plugin)
|
||||
- Path: `.github/agents/<name>.agent.md` (project) or `~/.copilot/agents/<name>.agent.md` (user)
|
||||
- Extension: **must be `.agent.md`**
|
||||
- Supported fields: `name` (required), `description` (required), `tools` (optional)
|
||||
- `tools` uses Copilot aliases: `execute` (shell), `read`, `edit`, `search`, `agent`, `web`
|
||||
@@ -145,14 +156,14 @@ source_keys:
|
||||
|
||||
The system prompt body should match the Claude Code version — the agent's task definition is the same across providers.
|
||||
|
||||
### Step 4 — Populate or delete `sources.md` (plugin scope only)
|
||||
### Step 4 — Populate or delete `sources.md` (plugin/APM scope only)
|
||||
|
||||
Skip this step at project and user scope. The file lives at the plugin root, not inside `agents/` — `claude plugin validate --strict` auto-discovers every `.md` under `agents/` as an agent requiring frontmatter, and a flat provenance file would fail that check (see ADR-0010).
|
||||
Skip at project/user scope. The file lives at the package root (alongside `apm.yml`), not inside `.apm/agents/` — otherwise tooling that scans that directory for agent definitions would treat it as an agent needing frontmatter (ADR-0010).
|
||||
|
||||
If a research `sources.md` is present in the conversation context:
|
||||
1. Filter to entries with `` `extracted` `` status only.
|
||||
2. For each entry, identify which agent files in the pair it contributed to.
|
||||
3. Write `sources.md` at the plugin root using the format below. Paths in `Contributing files:` are relative to the plugin root.
|
||||
2. For each entry, identify which agent file it contributed to.
|
||||
3. Write `sources.md` at the package root using the format below. Paths in `Contributing files:` are relative to the package root.
|
||||
|
||||
```markdown
|
||||
# Sources
|
||||
@@ -162,11 +173,11 @@ If a research `sources.md` is present in the conversation context:
|
||||
- **URL:** <source URL>
|
||||
- **Research doc:** <path/to/research/sources.md relative to repo root>
|
||||
- **Description:** <what this source covers>
|
||||
- **Contributing files:** agents/<name>.md, agents/<name>.agent.md
|
||||
- **Contributing files:** .apm/agents/<name>.agent.md
|
||||
- **Status:** `extracted`
|
||||
```
|
||||
|
||||
Each slug must match an H2 heading, and each slug must also appear in the `source_keys` list of every file listed under `Contributing files:`.
|
||||
Each slug must match an H2 heading, and each slug must also appear in the `source_keys` list of the file listed under `Contributing files:`.
|
||||
|
||||
If no research sources are in context, delete `sources.md`.
|
||||
|
||||
@@ -174,15 +185,22 @@ If no research sources are in context, delete `sources.md`.
|
||||
|
||||
Run this checklist before invoking the audit:
|
||||
|
||||
**Claude Code file (`<name>.md`):**
|
||||
**Plugin/APM scope — single file (`<name>.agent.md`):**
|
||||
- [ ] `name` field present, kebab-case, unique in scope
|
||||
- [ ] `description` field present and action-first
|
||||
- [ ] If plugin scope: no `hooks`, `mcpServers`, or `permissionMode` (silently ignored at plugin scope)
|
||||
- [ ] Frontmatter contains ONLY `name`, `description`, and optionally `model` (plus `source_keys` if research-sourced) — no `tools`, `isolation`, `maxTurns`, `effort`, `memory`, `permissionMode`, `disallowedTools`, `skills`, `color`, `initialPrompt`, `background`, `hooks`, or `mcpServers`
|
||||
- [ ] System prompt body present and non-empty
|
||||
- [ ] No `FILL IN:` placeholders remain
|
||||
- [ ] No `<!-- -->` template comments remain in frontmatter
|
||||
|
||||
**Copilot CLI file (`<name>.agent.md`):**
|
||||
**Project/user scope — Claude Code file (`<name>.md`):**
|
||||
- [ ] `name` field present, kebab-case, unique in scope
|
||||
- [ ] `description` field present and action-first
|
||||
- [ ] System prompt body present and non-empty
|
||||
- [ ] No `FILL IN:` placeholders remain
|
||||
- [ ] No `<!-- -->` template comments remain in frontmatter
|
||||
|
||||
**Project/user scope — Copilot CLI file (`<name>.agent.md`):**
|
||||
- [ ] File extension is `.agent.md` (not `.md`)
|
||||
- [ ] `name` field matches the filename stem (e.g. `name: my-agent` in `my-agent.agent.md`)
|
||||
- [ ] `description` field present
|
||||
@@ -191,9 +209,9 @@ Run this checklist before invoking the audit:
|
||||
- [ ] Body does not exceed 30,000 characters
|
||||
- [ ] No `<!-- -->` template comments remain in frontmatter
|
||||
|
||||
If the destination is inside a plugin directory, apply a **minor bump** to the `version` field in both `plugin.json` and `.claude-plugin/plugin.json` at the plugin root in the same edit pass (e.g. `1.0.4` → `1.1.0`).
|
||||
At plugin/APM scope, apply a **minor bump** to the resolved package's `apm.yml` `version` (single manifest, e.g. `1.0.4` → `1.1.0`).
|
||||
|
||||
Invoke the `kyberforge:agent-audit` skill directly on the created files to confirm the pair is valid before closing.
|
||||
Invoke `kyberforge:agent-audit` on the created file(s) before closing — validates the pair at project/user scope, the single file at plugin/APM scope.
|
||||
|
||||
**Commit verification.** Capture `git log --oneline -1` before Step 1 and keep it. Once the audit is clean, run `git add` and `git commit` for the new agent files — do not stop at staging. Then run `git log --oneline -1` again and confirm the hash changed from the one you captured at the start. A non-empty `git diff --stat` is not sufficient proof of completion: staged-but-uncommitted work isn't part of any commit and can be silently lost if the working tree is cleaned up before a commit lands. Only report the agent as done once the hash has actually changed.
|
||||
|
||||
@@ -209,7 +227,7 @@ Verify `kyberforge:agent-audit` is available — it ships with the kyberforge pl
|
||||
|
||||
Capture `git log --oneline -1` now, before making any edits — Step 5 needs it to verify a real commit landed.
|
||||
|
||||
**Partial state** — if one provider file exists but the other does not, scaffold the missing file first (run `bash scripts/new-agent.sh <name> <root>` — the file-by-file no-op means only the missing file is created), then continue with the improve flow on both files.
|
||||
**Partial state (project/user scope only)** — if one provider file exists but not the other, scaffold the missing one (`bash scripts/new-agent.sh <name> <root>`, file-by-file no-op) then continue. Doesn't apply at plugin/APM scope — single file, no partial-pair state.
|
||||
|
||||
### Step 2 — Gather and group signals
|
||||
|
||||
@@ -230,7 +248,7 @@ Before editing, state which root causes were identified, what evidence supports
|
||||
|
||||
### Step 4 — Apply changes
|
||||
|
||||
Edit any file the signals point to. Generalize the fix — find the underlying gap, not the specific example that failed. For every sentence you add, ask: "Would the agent get this wrong without it?" A shorter, focused definition consistently outperforms an exhaustive one. For Copilot files, verify no Claude Code-only fields are introduced.
|
||||
Edit any file the signals point to. Generalize the fix — find the underlying gap, not the specific example that failed. For every sentence you add, ask: "Would the agent get this wrong without it?" A shorter, focused definition consistently outperforms an exhaustive one. For Copilot files, verify no Claude Code-only fields are introduced. For a plugin/APM-scope single file, verify no field beyond `name`, `description`, `model`, and `source_keys` is introduced.
|
||||
|
||||
If the edit adds or removes research-sourced content, update `source_keys` in the edited file(s) and the corresponding entry in `sources.md` per Create flow's Step 4.
|
||||
|
||||
@@ -238,8 +256,8 @@ If the edit adds or removes research-sourced content, update `source_keys` in th
|
||||
|
||||
Re-run the validation checklist from the create flow's Step 5 on any edited file.
|
||||
|
||||
If the agent lives inside a plugin directory, apply a **patch bump** to the `version` field in both `plugin.json` and `.claude-plugin/plugin.json` at the plugin root in the same edit pass (e.g. `1.0.4` → `1.0.5`).
|
||||
At plugin/APM scope, apply a **patch bump** to the resolved package's `apm.yml` `version` (e.g. `1.0.4` → `1.0.5`).
|
||||
|
||||
Invoke the `kyberforge:agent-audit` skill directly on the edited files to confirm no regressions before closing.
|
||||
Invoke `kyberforge:agent-audit` on the edited file(s) to confirm no regressions — the pair at project/user scope, the single file at plugin/APM scope.
|
||||
|
||||
**Commit verification.** Capture `git log --oneline -1` at the start of Step 1 and keep it. Once the audit is clean, run `git add` and `git commit` for the changed files — do not stop at staging. Then run `git log --oneline -1` again and confirm the hash changed from the one you captured at the start. A non-empty `git diff --stat` is not sufficient proof of completion: staged-but-uncommitted work isn't part of any commit and can be silently lost if the working tree is cleaned up before a commit lands. Only report the improvement as done once the hash has actually changed.
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
|
||||
Annotated agent definition templates copied by `scripts/new-agent.sh` when scaffolding a new agent.
|
||||
|
||||
- **`claude-code.md`** — Claude Code agent definition template. Includes all supported frontmatter fields (required and optional) with inline guidance comments and `FILL IN:` placeholders. Notes which fields are silently ignored for plugin agents.
|
||||
- **`copilot.agent.md`** — Copilot CLI agent definition template (CLI format). 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`).
|
||||
- **`claude-code.md`** — Claude Code agent definition template (project/user scope). Includes all supported frontmatter fields (required and optional) with inline guidance comments and `FILL IN:` placeholders.
|
||||
- **`copilot.agent.md`** — Copilot CLI agent definition template (CLI format, project/user scope). Excludes cloud/IDE-only fields (`target`, `user-invocable`, `disable-model-invocation`, `mcp-servers`) and Claude Code-only fields. Uses Copilot tool aliases (`execute`, `read`, `edit`, `search`, `agent`, `web`).
|
||||
- **`apm-agent.md`** — Vendor-neutral APM agent definition template (plugin/APM scope). Only `name`, `description`, and optional `model` in frontmatter — no `tools` and no Claude-only fields, since `apm compile` copies frontmatter verbatim to both the Claude Code and Copilot CLI targets with no per-target integrator (ADR-0016).
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
<!-- Vendor-neutral APM agent definition (plugin/APM scope).
|
||||
Path: <package-root>/.apm/agents/<name>.agent.md — one file, no counterpart.
|
||||
`apm compile` copies this frontmatter verbatim to BOTH the Claude Code and
|
||||
Copilot CLI targets — there is no per-target field integrator. Claude's
|
||||
`tools:` (space-separated string) and Copilot's `tools:` (alias list) are
|
||||
incompatible vocabularies, and Claude-only fields (isolation, maxTurns,
|
||||
effort, memory, permissionMode) have no Copilot equivalent. A value correct
|
||||
for one harness is guaranteed wrong on the other, so this scope carries
|
||||
ONLY the three fields below — full stop (see ADR-0016).
|
||||
|
||||
Do NOT add: tools, isolation, maxTurns, effort, memory, permissionMode,
|
||||
disallowedTools, skills, color, initialPrompt, background, hooks, or
|
||||
mcpServers. Omitting `tools` means inherit-all-tools on both harnesses,
|
||||
which is never wrong.
|
||||
|
||||
Fill in all FILL IN: placeholders. Delete template comments before shipping. -->
|
||||
|
||||
name: AGENT_NAME
|
||||
<!-- Required. Lowercase letters and hyphens only. Must be unique within the scope. -->
|
||||
|
||||
description: FILL IN: Action-first description of what this agent does and when to invoke it.
|
||||
<!-- Required. The primary signal for autonomous delegation.
|
||||
Start with a verb: "Reviews...", "Analyzes...", "Generates..."
|
||||
Be specific about the triggering condition and expertise domain.
|
||||
Example: "Reviews pull request diffs for security issues. Use proactively after code changes." -->
|
||||
|
||||
<!-- model: sonnet
|
||||
Optional. Aliases: sonnet, opus, haiku, fable. Or full model ID.
|
||||
Omit to inherit the runtime default on whichever harness compiles this file. -->
|
||||
|
||||
<!-- source_keys:
|
||||
- slug-name
|
||||
Development-only. Add when research sources informed this agent (slugs must match
|
||||
sources.md at the package root — same directory as apm.yml).
|
||||
Omit when no research was used. Not a runtime field — silently ignored by both harnesses. -->
|
||||
---
|
||||
|
||||
FILL IN: System prompt body. Write as a direct role instruction.
|
||||
|
||||
You are a FILL IN: role description. When invoked, FILL IN: primary action.
|
||||
|
||||
## Inputs
|
||||
|
||||
FILL IN: What inputs does this agent expect? (files, context, parameters)
|
||||
|
||||
## Process
|
||||
|
||||
FILL IN: Steps the agent takes. Be specific about ordering if it matters.
|
||||
|
||||
## Output
|
||||
|
||||
FILL IN: What does the agent produce? Format, location, structure.
|
||||
@@ -18,22 +18,15 @@ Agent definitions deploy at three scopes and behave differently at each. The sco
|
||||
|---|---|---|---|
|
||||
| User | `~/.claude/agents/` | `~/.copilot/agents/` | All sessions for this user |
|
||||
| Project | `.claude/agents/` | `.github/agents/` or `.copilot/agents/` | This repo only |
|
||||
| Plugin | `<plugin>/agents/` | `<plugin>/agents/` | Sessions with the plugin installed |
|
||||
| Plugin/APM | `<package-root>/.apm/agents/<name>.agent.md` — single vendor-neutral file, `apm compile` emits it to both targets | *(same file)* | Sessions with the plugin/package installed |
|
||||
|
||||
When the same agent `name` appears at multiple scopes, **user scope wins over project scope wins over plugin scope** in Claude Code. In Copilot CLI, repo-level agents override enterprise and org-level; home-directory (user) agents override repo-level on name collision.
|
||||
|
||||
## Plugin scope restrictions
|
||||
|
||||
Plugin agents (agents shipped inside a plugin's `agents/` directory) have fewer capabilities than project or user agents:
|
||||
Plugin/APM agents (`.apm/agents/<name>.agent.md`) carry only `name`, `description`, and optionally `model` in frontmatter — full stop (see ADR-0016). `apm compile` copies this frontmatter verbatim to both the Claude Code and Copilot CLI compile targets with no per-target integrator: Claude's `tools:` (space-separated string) and Copilot's `tools:` (alias list) are incompatible vocabularies, and Claude-only fields have no Copilot equivalent, so any harness-specific value is guaranteed wrong on at least one target.
|
||||
|
||||
**Claude Code — silently ignored for plugin agents:**
|
||||
- `hooks` — per-agent event handlers; copy the agent to `.claude/agents/` to use these
|
||||
- `mcpServers` — inline MCP server definitions; not available in plugin context
|
||||
- `permissionMode` — silently ignored; the parent session's permission mode applies
|
||||
|
||||
**Copilot CLI — unsupported or ignored:**
|
||||
- `mcp-servers` — cloud agents only; ignored in VS Code and most IDEs
|
||||
- `metadata` — key-value annotations; ignored in VS Code
|
||||
This makes the old "silently ignored at plugin scope" framing moot. It's not that `hooks`, `mcpServers`, `permissionMode`, `tools`, `isolation`, `maxTurns`, `effort`, `memory`, `disallowedTools`, `skills`, `color`, `initialPrompt`, or `background` are merely ignored at this scope — they are never written to the file at all. Copy the agent to `.claude/agents/` (project scope) or `~/.claude/agents/` (user scope) to use any of them.
|
||||
|
||||
## Scoped identifiers (Claude Code plugin agents only)
|
||||
|
||||
@@ -59,6 +52,6 @@ Agents at project or user scope are read directly from disk; cache isolation doe
|
||||
|---|---|---|
|
||||
| User | `~/.copilot/agents/<name>.agent.md` | Home directory |
|
||||
| Project | `.github/agents/<name>.agent.md` | Standard; also `.copilot/agents/` |
|
||||
| Plugin | `<plugin>/agents/<name>.agent.md` | Inside plugin root |
|
||||
| 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** — Copilot CLI does not pick up plain `.md` files in the `agents/` directory.
|
||||
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.
|
||||
|
||||
@@ -24,7 +24,7 @@ Do not add additional substitution tokens unless you update both the template fi
|
||||
|
||||
## File placement
|
||||
|
||||
The script creates files at paths determined by scope detection (plugin / project / user). Scope is detected from the presence of `plugin.json`, `.claude-plugin/plugin.json`, `.plugin/plugin.json`, or `.github/plugin/plugin.json` in the root directory. If scope detection logic changes, update the `new-agent.sh` usage comment and `SKILL.md` Step 1 scope detection table in the same pass.
|
||||
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
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
## new-agent.sh
|
||||
|
||||
Scaffolds agent definition files for both Claude Code and GitHub Copilot CLI from a single root directory input.
|
||||
Scaffolds agent definition file(s) from a single root directory input.
|
||||
|
||||
```
|
||||
Usage: new-agent.sh <agent-name> <root>
|
||||
```
|
||||
|
||||
Detects scope from the root: `plugin.json` present → plugin scope (both files in `<root>/agents/`); `~` → user scope (`~/.claude/agents/` + `~/.copilot/agents/`); otherwise project scope (`.claude/agents/` + `.github/agents/`). Each file is a no-op if it already exists. See `--help` for full usage.
|
||||
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`).
|
||||
|
||||
@@ -9,19 +9,29 @@ usage() {
|
||||
cat <<EOF
|
||||
Usage: new-agent.sh <agent-name> <root>
|
||||
|
||||
Scaffold agent definition files for Claude Code and GitHub Copilot CLI.
|
||||
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 Root directory — determines scope:
|
||||
plugin scope : root contains plugin.json
|
||||
→ creates <root>/agents/<name>.md
|
||||
→ creates <root>/agents/<name>.agent.md
|
||||
→ creates <root>/sources.md (if absent)
|
||||
project scope : root is a project directory (no plugin.json)
|
||||
root Starting directory — scope is resolved by walking up from here:
|
||||
plugin/APM scope : nearest ancestor (at/above root) whose apm.yml
|
||||
has a top-level type: field (instructions,
|
||||
skill, hybrid, or prompts) — an apm.yml
|
||||
without type: is a marketplace-only manifest
|
||||
and is skipped, the walk continues upward
|
||||
→ creates <package-root>/.apm/agents/<name>.agent.md
|
||||
(single vendor-neutral file — no tools,
|
||||
isolation, maxTurns, effort, memory, or
|
||||
permissionMode; apm compile has no per-target
|
||||
field integrator, see ADR-0016)
|
||||
→ creates <package-root>/sources.md (if absent)
|
||||
project scope : no type:-bearing apm.yml found; root is a
|
||||
project directory
|
||||
→ creates <root>/.claude/agents/<name>.md
|
||||
→ creates <root>/.github/agents/<name>.agent.md
|
||||
user scope : root is ~ (home directory)
|
||||
user scope : root is exactly ~ (home directory; checked
|
||||
directly, no walk-up)
|
||||
→ creates ~/.claude/agents/<name>.md
|
||||
→ creates ~/.copilot/agents/<name>.agent.md
|
||||
|
||||
@@ -71,22 +81,71 @@ if [[ ! -d "$ROOT" ]]; then
|
||||
echo "Error: root directory '$ROOT' does not exist." >&2
|
||||
exit 1
|
||||
fi
|
||||
ROOT="$(cd "$ROOT" && pwd)"
|
||||
|
||||
# Detect scope
|
||||
if [[ -f "$ROOT/plugin.json" || -f "$ROOT/.claude-plugin/plugin.json" || -f "$ROOT/.plugin/plugin.json" || -f "$ROOT/.github/plugin/plugin.json" ]]; then
|
||||
# --- 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.
|
||||
# - a .git directory marks the project-scope boundary — stop.
|
||||
# - filesystem root reached with neither found — boundary-reached.
|
||||
find_package_root() {
|
||||
local current="$1"
|
||||
while true; do
|
||||
if [[ -f "$current/apm.yml" ]] && grep -qE '^type:[[:space:]]*(instructions|skill|hybrid|prompts)([[:space:]]|$)' "$current/apm.yml"; then
|
||||
echo "plugin"
|
||||
echo "$current"
|
||||
return
|
||||
fi
|
||||
if [[ -d "$current/.git" ]]; then
|
||||
echo "project"
|
||||
echo "$current"
|
||||
return
|
||||
fi
|
||||
local parent
|
||||
parent="$(dirname "$current")"
|
||||
if [[ "$parent" == "$current" ]]; then
|
||||
echo "boundary-reached"
|
||||
echo "$current"
|
||||
return
|
||||
fi
|
||||
current="$parent"
|
||||
done
|
||||
}
|
||||
|
||||
WALK_RESULT="$(find_package_root "$ROOT")"
|
||||
WALK_KIND="$(echo "$WALK_RESULT" | sed -n '1p')"
|
||||
WALK_ROOT="$(echo "$WALK_RESULT" | sed -n '2p')"
|
||||
|
||||
PACKAGE_ROOT=""
|
||||
case "$WALK_KIND" in
|
||||
plugin)
|
||||
SCOPE="plugin"
|
||||
elif [[ "$ROOT" == "$HOME" ]]; then
|
||||
SCOPE="user"
|
||||
else
|
||||
PACKAGE_ROOT="$WALK_ROOT"
|
||||
;;
|
||||
project)
|
||||
SCOPE="project"
|
||||
fi
|
||||
;;
|
||||
boundary-reached)
|
||||
if [[ "$ROOT" == "$HOME" ]]; then
|
||||
SCOPE="user"
|
||||
else
|
||||
# Default fallback, same as the pre-walk-up script: no plugin/APM
|
||||
# marker and not exactly $HOME means project scope.
|
||||
SCOPE="project"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Determine file destinations
|
||||
case "$SCOPE" in
|
||||
plugin)
|
||||
CC_DIR="$ROOT/agents"
|
||||
CP_DIR="$ROOT/agents"
|
||||
SOURCES_DIR="$ROOT"
|
||||
APM_DIR="$PACKAGE_ROOT/.apm/agents"
|
||||
SOURCES_DIR="$PACKAGE_ROOT"
|
||||
;;
|
||||
project)
|
||||
CC_DIR="$ROOT/.claude/agents"
|
||||
@@ -100,33 +159,47 @@ case "$SCOPE" in
|
||||
;;
|
||||
esac
|
||||
|
||||
CC_FILE="$CC_DIR/$AGENT_NAME.md"
|
||||
CP_FILE="$CP_DIR/$AGENT_NAME.agent.md"
|
||||
|
||||
# Create directories
|
||||
mkdir -p "$CC_DIR"
|
||||
mkdir -p "$CP_DIR"
|
||||
|
||||
# Copy Claude Code template (no-op if exists)
|
||||
created_any=false
|
||||
if [[ -f "$CC_FILE" ]]; then
|
||||
echo "Skipping '$CC_FILE' — already exists." >&2
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
# Copy Copilot template (no-op if exists)
|
||||
if [[ -f "$CP_FILE" ]]; then
|
||||
# Copy Copilot template (no-op if exists)
|
||||
if [[ -f "$CP_FILE" ]]; then
|
||||
echo "Skipping '$CP_FILE' — already exists." >&2
|
||||
else
|
||||
else
|
||||
sed "s/AGENT_NAME/$AGENT_NAME/g" "$TEMPLATES_DIR/copilot.agent.md" > "$CP_FILE"
|
||||
echo "Created: $CP_FILE" >&2
|
||||
created_any=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create sources.md at plugin scope (no-op if exists)
|
||||
# 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
|
||||
@@ -135,7 +208,7 @@ if [[ -n "$SOURCES_DIR" ]]; then
|
||||
cat > "$SOURCES_FILE" <<'SOURCES'
|
||||
# Sources
|
||||
|
||||
<!-- List research sources that informed agents in this directory.
|
||||
<!-- 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. -->
|
||||
|
||||
@@ -143,7 +216,7 @@ if [[ -n "$SOURCES_DIR" ]]; then
|
||||
- **URL:** <url>
|
||||
- **Research doc:** <relative-path-to-upstream-research-sources-file>
|
||||
- **Description:** <what this source covers>
|
||||
- **Contributing files:** agents/<name>.md, agents/<name>.agent.md
|
||||
- **Contributing files:** .apm/agents/<name>.agent.md
|
||||
- **Status:** `extracted` -->
|
||||
SOURCES
|
||||
echo "Created: $SOURCES_FILE" >&2
|
||||
@@ -158,12 +231,13 @@ else
|
||||
echo "Scope: $SCOPE" >&2
|
||||
echo "" >&2
|
||||
echo "Next steps:" >&2
|
||||
if [[ "$SCOPE" == "plugin" ]]; then
|
||||
echo " 1. Fill in $APM_FILE — replace all FILL IN: placeholders (name, description, model, body only)" >&2
|
||||
echo " 2. Populate $SOURCES_DIR/sources.md with research sources, or delete it" >&2
|
||||
echo " 3. Validate: check required fields (name, description, system prompt) in the file" >&2
|
||||
else
|
||||
echo " 1. Fill in $CC_FILE — replace all FILL IN: placeholders" >&2
|
||||
echo " 2. Fill in $CP_FILE — replace all FILL IN: placeholders" >&2
|
||||
if [[ -n "$SOURCES_DIR" ]]; then
|
||||
echo " 3. Populate $SOURCES_DIR/sources.md with research sources, or delete it" >&2
|
||||
echo " 4. Validate: check required fields (name, description, system prompt) in both files" >&2
|
||||
else
|
||||
echo " 3. Validate: check required fields (name, description, system prompt) in both files" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -24,62 +24,129 @@ teardown() {
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin scope (plugin.json present at root)
|
||||
# Plugin/APM scope (type:-bearing apm.yml at root)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "plugin scope: creates both agent files in agents/" {
|
||||
touch "$ROOT/plugin.json"
|
||||
@test "plugin/APM scope: creates single agent file in .apm/agents/" {
|
||||
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
|
||||
run bash "$SCRIPT" my-agent "$ROOT"
|
||||
assert_success
|
||||
assert [ -f "$ROOT/agents/my-agent.md" ]
|
||||
assert [ -f "$ROOT/agents/my-agent.agent.md" ]
|
||||
assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ]
|
||||
}
|
||||
|
||||
@test "plugin scope: creates agents/ directory if missing" {
|
||||
touch "$ROOT/plugin.json"
|
||||
@test "plugin/APM scope: does not create the old dual-file pair" {
|
||||
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
|
||||
run bash "$SCRIPT" my-agent "$ROOT"
|
||||
assert_success
|
||||
assert [ -d "$ROOT/agents" ]
|
||||
assert [ ! -f "$ROOT/agents/my-agent.md" ]
|
||||
assert [ ! -f "$ROOT/agents/my-agent.agent.md" ]
|
||||
}
|
||||
|
||||
@test "plugin scope: creates sources.md at plugin root" {
|
||||
touch "$ROOT/plugin.json"
|
||||
@test "plugin/APM scope: creates .apm/agents/ directory if missing" {
|
||||
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
|
||||
run bash "$SCRIPT" my-agent "$ROOT"
|
||||
assert_success
|
||||
assert [ -d "$ROOT/.apm/agents" ]
|
||||
}
|
||||
|
||||
@test "plugin/APM scope: creates sources.md at package root" {
|
||||
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
|
||||
run bash "$SCRIPT" my-agent "$ROOT"
|
||||
assert_success
|
||||
assert [ -f "$ROOT/sources.md" ]
|
||||
}
|
||||
|
||||
@test "plugin scope: no-op if claude code file already exists" {
|
||||
touch "$ROOT/plugin.json"
|
||||
mkdir -p "$ROOT/agents"
|
||||
echo "existing" > "$ROOT/agents/my-agent.md"
|
||||
@test "plugin/APM scope: no-op if agent file already exists" {
|
||||
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
|
||||
mkdir -p "$ROOT/.apm/agents"
|
||||
echo "existing" > "$ROOT/.apm/agents/my-agent.agent.md"
|
||||
run bash "$SCRIPT" my-agent "$ROOT"
|
||||
assert_success
|
||||
run grep "existing" "$ROOT/agents/my-agent.md"
|
||||
run grep "existing" "$ROOT/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "plugin scope: no-op if copilot file already exists" {
|
||||
touch "$ROOT/plugin.json"
|
||||
mkdir -p "$ROOT/agents"
|
||||
echo "existing" > "$ROOT/agents/my-agent.agent.md"
|
||||
run bash "$SCRIPT" my-agent "$ROOT"
|
||||
assert_success
|
||||
run grep "existing" "$ROOT/agents/my-agent.agent.md"
|
||||
@test "plugin/APM scope: frontmatter has no tools/isolation/maxTurns/effort/memory/permissionMode keys" {
|
||||
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
|
||||
bash "$SCRIPT" my-agent "$ROOT"
|
||||
file="$ROOT/.apm/agents/my-agent.agent.md"
|
||||
fm="$(sed -n '/^---$/,/^---$/p' "$file")"
|
||||
# Column-0 key lines only — comment bodies in the template are indented,
|
||||
# so this anchor naturally excludes commented-out example fields.
|
||||
run grep -E '^(tools|isolation|maxTurns|effort|memory|permissionMode|disallowedTools|skills|color|initialPrompt|background|hooks|mcpServers):' <<< "$fm"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "plugin/APM scope: frontmatter carries only name, description, model, source_keys fields" {
|
||||
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
|
||||
bash "$SCRIPT" my-agent "$ROOT"
|
||||
file="$ROOT/.apm/agents/my-agent.agent.md"
|
||||
fm="$(sed -n '/^---$/,/^---$/p' "$file")"
|
||||
keys="$(grep -oE '^[a-zA-Z][a-zA-Z0-9_-]*:' <<< "$fm" | sed 's/:$//' | sort -u)"
|
||||
for key in $keys; do
|
||||
if [[ "$key" != "name" && "$key" != "description" && "$key" != "model" && "$key" != "source_keys" ]]; then
|
||||
fail "unexpected frontmatter key: $key"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
@test "plugin/APM scope: sources.md contributing-files template mentions the single-file path" {
|
||||
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
|
||||
bash "$SCRIPT" my-agent "$ROOT"
|
||||
run grep ".apm/agents/<name>.agent.md" "$ROOT/sources.md"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "plugin scope: still creates missing file when one already exists" {
|
||||
touch "$ROOT/plugin.json"
|
||||
mkdir -p "$ROOT/agents"
|
||||
echo "existing" > "$ROOT/agents/my-agent.md"
|
||||
@test "plugin/APM scope: template AGENT_NAME substituted" {
|
||||
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
|
||||
bash "$SCRIPT" my-agent "$ROOT"
|
||||
run grep "my-agent" "$ROOT/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "plugin/APM scope: walk-up finds apm.yml at an ancestor directory, not just root arg" {
|
||||
printf 'name: my-package\ntype: skill\n' > "$ROOT/apm.yml"
|
||||
mkdir -p "$ROOT/nested/subdir"
|
||||
run bash "$SCRIPT" my-agent "$ROOT/nested/subdir"
|
||||
assert_success
|
||||
assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ]
|
||||
}
|
||||
|
||||
@test "plugin/APM scope: apm.yml without type: is skipped (marketplace-only manifest)" {
|
||||
printf 'name: my-marketplace\n' > "$ROOT/apm.yml"
|
||||
mkdir -p "$ROOT/.git"
|
||||
run bash "$SCRIPT" my-agent "$ROOT"
|
||||
assert_success
|
||||
assert [ -f "$ROOT/agents/my-agent.agent.md" ]
|
||||
assert [ ! -f "$ROOT/.apm/agents/my-agent.agent.md" ]
|
||||
assert [ -f "$ROOT/.claude/agents/my-agent.md" ]
|
||||
}
|
||||
|
||||
@test "plugin/APM scope: type:-less apm.yml at leaf falls through to a type:-bearing apm.yml higher up" {
|
||||
printf 'name: outer-package\ntype: skill\n' > "$ROOT/apm.yml"
|
||||
mkdir -p "$ROOT/inner"
|
||||
printf 'name: inner-marketplace\n' > "$ROOT/inner/apm.yml"
|
||||
run bash "$SCRIPT" my-agent "$ROOT/inner"
|
||||
assert_success
|
||||
assert [ -f "$ROOT/.apm/agents/my-agent.agent.md" ]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Non-plugin scope (no plugin.json)
|
||||
# Old plugin.json marker is no longer recognized (full switch, no dual-mode)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "bare plugin.json (no apm.yml) is no longer detected as plugin scope — falls through to project scope" {
|
||||
touch "$ROOT/plugin.json"
|
||||
run bash "$SCRIPT" my-agent "$ROOT"
|
||||
assert_success
|
||||
assert [ ! -f "$ROOT/agents/my-agent.md" ]
|
||||
assert [ ! -f "$ROOT/agents/my-agent.agent.md" ]
|
||||
assert [ ! -f "$ROOT/.apm/agents/my-agent.agent.md" ]
|
||||
assert [ -f "$ROOT/.claude/agents/my-agent.md" ]
|
||||
assert [ -f "$ROOT/.github/agents/my-agent.agent.md" ]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Non-plugin (project) scope
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "non-plugin scope: creates claude code file in .claude/agents/" {
|
||||
@@ -122,6 +189,28 @@ teardown() {
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "project scope: detected via .git present above root, output still relative to root arg" {
|
||||
mkdir -p "$ROOT/repo/.git"
|
||||
mkdir -p "$ROOT/repo/pkg"
|
||||
run bash "$SCRIPT" my-agent "$ROOT/repo/pkg"
|
||||
assert_success
|
||||
assert [ -f "$ROOT/repo/pkg/.claude/agents/my-agent.md" ]
|
||||
assert [ -f "$ROOT/repo/pkg/.github/agents/my-agent.agent.md" ]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# User scope
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "user scope: root exactly \$HOME creates files under ~/.claude and ~/.copilot" {
|
||||
FAKE_HOME="$(mktemp -d)"
|
||||
run env HOME="$FAKE_HOME" bash "$SCRIPT" my-agent "~"
|
||||
assert_success
|
||||
assert [ -f "$FAKE_HOME/.claude/agents/my-agent.md" ]
|
||||
assert [ -f "$FAKE_HOME/.copilot/agents/my-agent.agent.md" ]
|
||||
rm -rf "$FAKE_HOME"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Name validation
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -164,21 +253,3 @@ teardown() {
|
||||
run bash "$SCRIPT" my-agent "/nonexistent/path"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Template content
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "plugin scope: claude code template contains AGENT_NAME substituted" {
|
||||
touch "$ROOT/plugin.json"
|
||||
bash "$SCRIPT" my-agent "$ROOT"
|
||||
run grep "my-agent" "$ROOT/agents/my-agent.md"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "plugin scope: copilot template contains AGENT_NAME substituted" {
|
||||
touch "$ROOT/plugin.json"
|
||||
bash "$SCRIPT" my-agent "$ROOT"
|
||||
run grep "my-agent" "$ROOT/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user