* feat(#1243): consume Claude plugin-provided skills via native Skill-tool directive + grant Skill to agent_skills-consumer agents - Relax global skill name validation to accept namespaced form `^[A-Za-z0-9_-]+(:[A-Za-z0-9_-]+)*$` - Namespaced names (containing colon) on claude runtime emit a Skill-tool load directive instead of a @-include line - Namespaced names on non-claude runtimes are skipped with a warning - Bare unresolved names retain existing warn-and-skip behavior (no promotion to directive) - Grant `Skill` tool to all 22 agent_skills consumer agents; 5 generated agents updated via research-profiles.cjs + regen, 17 hand-authored agents edited directly - Add 16 TDD tests in describe('bug #1243') covering happy/mixed/precedence/negative/cross-runtime/regression/grant cases Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(#1243): document plugin-provided skills in agent_skills Update the Agent Skills Injection reference in CONFIGURATION.md with the three entry forms (project-relative, global:<name>, global:<plugin>:<skill>), the Claude-only runtime behaviour of the namespaced form and the warn-skip on other runtimes, the plugin pre-install prerequisite, and the consumer-agent Skill tool grant. Add docs/how-to/attach-a-plugin-skill-to-a-gsd-agent.md with a step-by-step guide for installing the plugin, locating the namespaced skill name, wiring it into agent_skills, and verifying injection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#1243): align agent_skills docs with emitted block format + mixed-block regression test (code-review) - Replace two-section mixed-block example (bogus "Load these plugin-provided skills using the Skill tool:" header) with the actual single-section inline format in CONFIGURATION.md and docs/how-to/attach-a-plugin-skill-to-a-gsd-agent.md - Fix quoted warning text in how-to doc to exactly match the emitted string: [agent-skills] WARNING: Plugin-namespaced skill "global:<name>" requires a Skill-tool-capable runtime (claude) — skipping on runtime "<runtime>" - Replace phantom agent slugs (gsd-checker, gsd-researcher, gsd-advisor, gsd-synthesizer) in CONFIGURATION.md Supported Agent Types with real agents/gsd-*.md examples (gsd-plan-checker, gsd-phase-researcher, gsd-code-reviewer, gsd-ui-auditor, gsd-research-synthesizer) - Add byte-identical mixed-block regression test: one path-resolvable global skill + one plugin-namespaced skill on claude runtime → asserts r.ir.block === single-section interleaved block, no secondary header Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(#1243): regenerate agent-size baseline for the Skill-tool grant The 22 agent_skills-consumer agents each grew +7 bytes from adding `Skill` to their tools list; refresh the committed per-agent size baseline (#1074 guard). * chore(#1243): add Added changeset fragment * fix(#1243): traceable allow-test-rule ref + separator-agnostic byte-identical tests (CI) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
5
.changeset/rapid-pumas-fly.md
Normal file
5
.changeset/rapid-pumas-fly.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
pr: 1261
|
||||
---
|
||||
**`agent_skills` can now reference Claude-Code plugin-provided skills** via the namespaced `global:<plugin>:<skill>` form (e.g. `global:coderabbit:code-review`). On the Claude runtime the agent's skills block emits a by-name Skill-tool load directive that resolves the plugin skill (no plugin-cache path is read); path-resolvable skills keep the existing `@`-include unchanged; on non-Claude runtimes a namespaced entry is skipped with a warning. The 22 agent_skills-consumer agents now carry the `Skill` tool so they can load plugin-provided skills.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-advisor-researcher
|
||||
description: Researches a single gray area decision and returns a structured comparison table with rationale. Spawned by discuss-phase advisor mode.
|
||||
tools: Read, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*
|
||||
tools: Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*
|
||||
color: cyan
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-assumptions-analyzer
|
||||
description: Deeply analyzes codebase for a phase and returns structured assumptions with evidence. Spawned by discuss-phase assumptions mode.
|
||||
tools: Read, Bash, Grep, Glob
|
||||
tools: Read, Bash, Grep, Glob, Skill
|
||||
color: cyan
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-code-fixer
|
||||
description: Applies fixes to code review findings from REVIEW.md. Reads source files, applies intelligent fixes, and commits each fix atomically. Spawned by /gsd:code-review --fix.
|
||||
tools: Read, Edit, Write, Bash, Grep, Glob
|
||||
tools: Read, Edit, Write, Bash, Grep, Glob, Skill
|
||||
color: green
|
||||
# hooks:
|
||||
# - before_write
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-code-reviewer
|
||||
description: Reviews source files for bugs, security issues, and code quality problems. Produces structured REVIEW.md with severity-classified findings. Spawned by /gsd:code-review.
|
||||
tools: Read, Write, Bash, Grep, Glob
|
||||
tools: Read, Write, Bash, Grep, Glob, Skill
|
||||
color: orange
|
||||
# hooks:
|
||||
# - before_write
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-codebase-mapper
|
||||
description: Explores codebase and writes structured analysis documents. Spawned by map-codebase with a focus area (tech, arch, quality, concerns). Writes documents directly to reduce orchestrator context load.
|
||||
tools: Read, Bash, Grep, Glob, Write
|
||||
tools: Read, Bash, Grep, Glob, Write, Skill
|
||||
color: cyan
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-debugger
|
||||
description: Investigates bugs using scientific method, manages debug sessions, handles checkpoints. Spawned by /gsd:debug orchestrator.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch
|
||||
color: orange
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-doc-writer
|
||||
description: Writes and updates project documentation. Spawned with a doc_assignment block specifying doc type, mode (create/update/supplement), and project context.
|
||||
tools: Read, Bash, Grep, Glob, Write, Edit
|
||||
tools: Read, Bash, Grep, Glob, Write, Edit, Skill
|
||||
color: purple
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-eval-auditor
|
||||
description: Retroactive audit of an implemented AI phase's evaluation coverage. Checks implementation against the AI-SPEC.md evaluation plan. Scores each eval dimension as COVERED/PARTIAL/MISSING. Produces a scored EVAL-REVIEW.md with findings, gaps, and remediation guidance. Spawned by /gsd:eval-review orchestrator.
|
||||
tools: Read, Write, Bash, Grep, Glob
|
||||
tools: Read, Write, Bash, Grep, Glob, Skill
|
||||
color: red
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-executor
|
||||
description: Executes GSD plans with atomic commits, deviation handling, checkpoint protocols, and state management. Spawned by execute-phase orchestrator or execute-plan command.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, mcp__context7__*
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, Skill, mcp__context7__*
|
||||
color: yellow
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-integration-checker
|
||||
description: Verifies cross-phase integration and E2E flows. Checks that phases connect properly and user workflows complete end-to-end.
|
||||
tools: Read, Bash, Grep, Glob
|
||||
tools: Read, Bash, Grep, Glob, Skill
|
||||
color: blue
|
||||
---
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ tools:
|
||||
- Bash
|
||||
- Glob
|
||||
- Grep
|
||||
- Skill
|
||||
color: purple
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-phase-researcher
|
||||
description: Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*
|
||||
color: cyan
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-plan-checker
|
||||
description: Verifies plans will achieve phase goal before execution. Goal-backward analysis of plan quality. Spawned by /gsd:plan-phase orchestrator.
|
||||
tools: Read, Bash, Glob, Grep
|
||||
tools: Read, Bash, Glob, Grep, Skill
|
||||
color: green
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-planner
|
||||
description: Creates executable phase plans with task breakdown, dependency analysis, and goal-backward verification. Spawned by /gsd:plan-phase orchestrator.
|
||||
tools: Read, Write, Edit, Bash, Glob, Grep, WebFetch, mcp__context7__*
|
||||
tools: Read, Write, Edit, Bash, Glob, Grep, Skill, WebFetch, mcp__context7__*
|
||||
color: green
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-project-researcher
|
||||
description: Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators.
|
||||
tools: Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*
|
||||
tools: Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*
|
||||
color: cyan
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-research-synthesizer
|
||||
description: Synthesizes research outputs from parallel researcher agents into SUMMARY.md. Spawned by /gsd:new-project after 4 researcher agents complete.
|
||||
tools: Read, Write, Bash
|
||||
tools: Read, Write, Bash, Skill
|
||||
color: purple
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-roadmapper
|
||||
description: Creates project roadmaps with phase breakdown, requirement mapping, success criteria derivation, and coverage validation. Spawned by /gsd:new-project orchestrator.
|
||||
tools: Read, Write, Bash, Glob, Grep
|
||||
tools: Read, Write, Bash, Glob, Grep, Skill
|
||||
color: purple
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -8,6 +8,7 @@ tools:
|
||||
- Bash
|
||||
- Glob
|
||||
- Grep
|
||||
- Skill
|
||||
color: red
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-ui-auditor
|
||||
description: Retroactive 6-pillar visual audit of implemented frontend code. Produces scored UI-REVIEW.md. Spawned by /gsd:ui-review orchestrator.
|
||||
tools: Read, Write, Bash, Grep, Glob
|
||||
tools: Read, Write, Bash, Grep, Glob, Skill
|
||||
color: pink
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-ui-checker
|
||||
description: Validates UI-SPEC.md design contracts against 6 quality dimensions. Produces BLOCK/FLAG/PASS verdicts. Spawned by /gsd:ui-phase orchestrator.
|
||||
tools: Read, Bash, Glob, Grep
|
||||
tools: Read, Bash, Glob, Grep, Skill
|
||||
color: cyan
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-ui-researcher
|
||||
description: Produces UI-SPEC.md design contract for frontend phases. Reads upstream artifacts, detects design system state, asks only unanswered questions. Spawned by /gsd:ui-phase orchestrator.
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*
|
||||
tools: Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*
|
||||
color: purple
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd-verifier
|
||||
description: Verifies phase goal achievement through goal-backward analysis. Checks codebase delivers what phase promised, not just that tasks completed. Creates VERIFICATION.md report.
|
||||
tools: Read, Write, Bash, Grep, Glob
|
||||
tools: Read, Write, Bash, Grep, Glob, Skill
|
||||
color: green
|
||||
# hooks:
|
||||
# PostToolUse:
|
||||
|
||||
@@ -405,52 +405,90 @@ Inject custom skill files into GSD subagent prompts. Skills are read by agents a
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `agent_skills` | object | `{}` | Map of agent types to skill directory paths |
|
||||
| `agent_skills` | object | `{}` | Map of agent types to arrays of skill entries |
|
||||
| `agent_skills_security.trusted_global_roots` | array of strings | `[]` | Opt-in allowlist of additional trusted directories for `global:` skills. See [Trusted global skill roots](#trusted-global-skill-roots-agent_skills_securitytrusted_global_roots) |
|
||||
|
||||
### Configuration
|
||||
|
||||
Add an `agent_skills` section to `.planning/config.json` mapping agent types to arrays of skill directory paths (relative to project root):
|
||||
Add an `agent_skills` section to `.planning/config.json` mapping agent types to arrays of skill entries:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_skills": {
|
||||
"gsd-executor": ["skills/testing-standards", "skills/api-conventions"],
|
||||
"gsd-executor": [
|
||||
"skills/testing-standards",
|
||||
"global:shared-conventions",
|
||||
"global:coderabbit:code-review"
|
||||
],
|
||||
"gsd-planner": ["skills/architecture-rules"],
|
||||
"gsd-verifier": ["skills/acceptance-criteria"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each path must be a directory containing a `SKILL.md` file. Paths are validated for safety (no traversal outside project root).
|
||||
### Skill Entry Forms
|
||||
|
||||
Each element in the array is one of three forms:
|
||||
|
||||
| Form | Example | Resolution |
|
||||
|------|---------|------------|
|
||||
| Project-relative path | `"skills/my-skill"` | Resolves to `<project>/skills/my-skill/SKILL.md`, injected as an `@`-include |
|
||||
| Global personal skill | `"global:<name>"` | Resolves to `~/.claude/skills/<name>/SKILL.md`, injected as an `@`-include |
|
||||
| Plugin-provided skill (Claude only) | `"global:<plugin>:<skill>"` | A Claude Code plugin skill, loaded by name via the Skill tool at agent spawn time |
|
||||
|
||||
**Project-relative paths** must point to a directory containing a `SKILL.md` file. Paths are validated for safety (no traversal outside the project root).
|
||||
|
||||
**Global personal skills** (`global:<name>`) resolve against the runtime's global skills directory (e.g. `~/.claude/skills/`). Symlink-escape protection applies unless the target is listed in `agent_skills_security.trusted_global_roots`.
|
||||
|
||||
**Plugin-provided skills** (`global:<plugin>:<skill>`) follow the namespaced form `seg(:seg)*`, where each segment is one or more alphanumeric characters, underscores, or hyphens joined by single colons (e.g. `global:coderabbit:code-review`). This form is **Claude-only**: on the Claude runtime GSD emits a Skill-tool load directive in the agent's `<agent_skills>` block so the agent loads the skill by name via the Skill tool, and Claude Code resolves the `plugin:skill` namespace. On all other runtimes the entry is skipped with a warning — the plugin/Skill-tool model is specific to Claude Code and has no equivalent elsewhere.
|
||||
|
||||
> **Why load by name rather than path?** Claude Code's plugin cache is versioned and ephemeral, so there is no stable filesystem path to `@`-include. Loading by namespaced name via the Skill tool lets Claude Code's own resolver locate the current version of the plugin skill at runtime.
|
||||
|
||||
The plugin must already be installed in the user's Claude Code environment (`/plugin install …`). GSD only references the skill by its namespaced name and does not read or validate the plugin cache itself.
|
||||
|
||||
### Supported Agent Types
|
||||
|
||||
Any GSD agent type can receive skills. Common types:
|
||||
Any GSD agent type can receive skills. The agent types that consume `agent_skills` are the GSD sub-agents the workflows dispatch. There are 22 consumer agents in total, including:
|
||||
|
||||
- `gsd-executor` -- executes implementation plans
|
||||
- `gsd-planner` -- creates phase plans
|
||||
- `gsd-checker` -- verifies plan quality
|
||||
- `gsd-verifier` -- post-execution verification
|
||||
- `gsd-researcher` -- phase research
|
||||
- `gsd-project-researcher` -- new-project research
|
||||
- `gsd-debugger` -- diagnostic agents
|
||||
- `gsd-codebase-mapper` -- codebase analysis
|
||||
- `gsd-advisor` -- discuss-phase advisors
|
||||
- `gsd-ui-researcher` -- UI design contract creation
|
||||
- `gsd-ui-checker` -- UI spec verification
|
||||
- `gsd-roadmapper` -- roadmap creation
|
||||
- `gsd-synthesizer` -- research synthesis
|
||||
- `gsd-executor` — executes implementation plans
|
||||
- `gsd-planner` — creates phase plans
|
||||
- `gsd-plan-checker` — verifies plan quality
|
||||
- `gsd-verifier` — post-execution verification
|
||||
- `gsd-phase-researcher` — phase research
|
||||
- `gsd-project-researcher` — new-project research
|
||||
- `gsd-debugger` — diagnostic agents
|
||||
- `gsd-codebase-mapper` — codebase analysis
|
||||
- `gsd-code-reviewer` — code review
|
||||
- `gsd-ui-researcher` — UI design contract creation
|
||||
- `gsd-ui-checker` — UI spec verification
|
||||
- `gsd-ui-auditor` — UI audit
|
||||
- `gsd-roadmapper` — roadmap creation
|
||||
- `gsd-research-synthesizer` — research synthesis
|
||||
- and others (see `tests/agent-skills.test.cjs` `CONSUMER_AGENTS` list for the full 22)
|
||||
|
||||
The `Skill` tool is granted to consumer agents deliberately and is instruction-bounded — agents use it only to load the skills listed in the `<agent_skills>` block.
|
||||
|
||||
### How It Works
|
||||
|
||||
At spawn time, workflows call `gsd-tools query agent-skills <type>` (or legacy `node gsd-tools.cjs agent-skills <type>`) to load configured skills. If skills exist for the agent type, they are injected as an `<agent_skills>` block in the Task() prompt:
|
||||
At spawn time, workflows call `gsd-tools query agent-skills <type>` (or legacy `node gsd-tools.cjs agent-skills <type>`) to load configured skills. If skills exist for the agent type, they are injected as an `<agent_skills>` block in the Task() prompt.
|
||||
|
||||
For project-relative and global personal skills, entries appear as `@`-includes:
|
||||
|
||||
```xml
|
||||
<agent_skills>
|
||||
Read these user-configured skills:
|
||||
- @skills/testing-standards/SKILL.md
|
||||
- @skills/api-conventions/SKILL.md
|
||||
- @/Users/you/.claude/skills/shared-conventions/SKILL.md
|
||||
</agent_skills>
|
||||
```
|
||||
|
||||
For a mixed config (path-resolvable and plugin-provided skills together), entries appear interleaved in config order in a single section:
|
||||
|
||||
```xml
|
||||
<agent_skills>
|
||||
Read these user-configured skills:
|
||||
- @skills/testing-standards/SKILL.md
|
||||
- Load the `coderabbit:code-review` skill via the Skill tool before proceeding (plugin-provided).
|
||||
</agent_skills>
|
||||
```
|
||||
|
||||
@@ -464,6 +502,8 @@ Set skills via the CLI:
|
||||
gsd-tools query config-set agent_skills.gsd-executor '["skills/my-skill"]'
|
||||
```
|
||||
|
||||
See [How to attach a plugin-provided skill to a GSD agent](how-to/attach-a-plugin-skill-to-a-gsd-agent.md) for a step-by-step walkthrough of the `global:plugin:skill` form.
|
||||
|
||||
---
|
||||
|
||||
## Trusted Global Skill Roots (`agent_skills_security.trusted_global_roots`)
|
||||
|
||||
@@ -17,6 +17,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md)
|
||||
|
||||
- [Install on your runtime](how-to/install-on-your-runtime.md) — runtime-specific install steps for all 16 supported runtimes
|
||||
- [Install a minimal GSD and add skills later](how-to/install-minimal-and-add-skills.md) — install only the core skills, then grow the surface with profiles and `/gsd:surface`
|
||||
- [Attach a plugin-provided skill to a GSD agent](how-to/attach-a-plugin-skill-to-a-gsd-agent.md) — use the `global:plugin:skill` entry form to load Claude Code plugin skills into agent prompts
|
||||
- [Discuss a phase](how-to/discuss-a-phase.md) — capture implementation decisions before planning begins
|
||||
- [Resolve edge-coverage findings](how-to/resolve-edge-coverage-findings.md) — turn the spec phase's surfaced domain-boundary edges into covered, dismissed, or backstopped spec decisions
|
||||
- [Resolve prohibition findings](how-to/resolve-prohibition-findings.md) — turn the spec phase's surfaced must-NOT constraints into resolved, dismissed, or deferred spec decisions
|
||||
|
||||
98
docs/how-to/attach-a-plugin-skill-to-a-gsd-agent.md
Normal file
98
docs/how-to/attach-a-plugin-skill-to-a-gsd-agent.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# How to attach a plugin-provided skill to a GSD agent
|
||||
|
||||
Use this guide when you have a Claude Code plugin that ships a skill and you want GSD agents to load it automatically at spawn time. The `global:<plugin>:<skill>` entry form makes GSD emit a Skill-tool load directive so the agent picks up the plugin skill alongside any project-local or personal global skills.
|
||||
|
||||
**This feature works on the Claude runtime only.** On other runtimes (Codex, Gemini, Cursor, etc.) the entry is skipped with a warning. Plan accordingly if your team uses multiple runtimes.
|
||||
|
||||
---
|
||||
|
||||
## 1. Install the plugin in Claude Code
|
||||
|
||||
GSD does not install or manage Claude Code plugins. Install the plugin first:
|
||||
|
||||
```
|
||||
/plugin install <plugin-source>
|
||||
```
|
||||
|
||||
Verify the plugin is active and note the skill names it exposes. The plugin's documentation or `README` will list available skill names in the format `plugin:skill` — for example, `coderabbit:code-review`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Find the namespaced skill name
|
||||
|
||||
The name you need has two colon-separated segments after `global:`:
|
||||
|
||||
```
|
||||
global:<plugin>:<skill>
|
||||
```
|
||||
|
||||
For example, if the plugin slug is `coderabbit` and it provides a skill called `code-review`, the entry is:
|
||||
|
||||
```
|
||||
global:coderabbit:code-review
|
||||
```
|
||||
|
||||
Each segment must consist of alphanumeric characters, underscores, or hyphens only. No spaces, no slashes, no dots.
|
||||
|
||||
---
|
||||
|
||||
## 3. Add the entry to `agent_skills`
|
||||
|
||||
Open `.planning/config.json` and add the namespaced skill to the array for the agent type that should receive it:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_skills": {
|
||||
"gsd-executor": [
|
||||
"skills/project-conventions",
|
||||
"global:coderabbit:code-review"
|
||||
],
|
||||
"gsd-verifier": [
|
||||
"global:coderabbit:code-review"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can mix all three entry forms freely within the same array — project-relative paths, `global:<name>` personal skills, and `global:<plugin>:<skill>` plugin skills.
|
||||
|
||||
Or set it from the CLI:
|
||||
|
||||
```bash
|
||||
gsd-tools query config-set agent_skills.gsd-executor '["skills/project-conventions","global:coderabbit:code-review"]'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Verify the injection
|
||||
|
||||
Start a phase on the Claude runtime. When the target agent is spawned, its Task() prompt will contain an `<agent_skills>` block that includes a Skill-tool load directive for the plugin skill:
|
||||
|
||||
```xml
|
||||
<agent_skills>
|
||||
Read these user-configured skills:
|
||||
- @skills/project-conventions/SKILL.md
|
||||
- Load the `coderabbit:code-review` skill via the Skill tool before proceeding (plugin-provided).
|
||||
</agent_skills>
|
||||
```
|
||||
|
||||
Both entry types appear in the same `Read these user-configured skills:` section, interleaved in config order. There is no separate header for plugin-provided skills.
|
||||
|
||||
If you see `[agent-skills] WARNING: Plugin-namespaced skill "global:coderabbit:code-review" requires a Skill-tool-capable runtime (claude) — skipping on runtime "<runtime>"` in the logs, the entry is being skipped because the active runtime is not Claude. The configuration is still valid; no change is needed.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Plugin must be pre-installed.** GSD only references the skill by namespaced name. If the plugin is not installed in Claude Code, the Skill tool call will fail at agent runtime — GSD cannot validate plugin presence at configuration time.
|
||||
- **Which agents support it.** All 22 GSD agent types that consume `agent_skills` carry the `Skill` tool, so any of them can load plugin-provided skills. See [Agent Skills Injection — Supported Agent Types](../CONFIGURATION.md#supported-agent-types) for the full list.
|
||||
- **Non-Claude runtimes.** The `global:<plugin>:<skill>` entry is silently skipped with a warning on non-Claude runtimes. Project-relative and `global:<name>` entries work on all runtimes.
|
||||
- **Security boundary.** Plugin skill content is resolved entirely by Claude Code at agent runtime. GSD does not read, cache, or validate the plugin's files.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [Configuration — Agent Skills Injection](../CONFIGURATION.md#agent-skills-injection)
|
||||
- [Install a minimal GSD and add skills later](install-minimal-and-add-skills.md)
|
||||
- [Import a capability from a URL](import-a-capability-from-a-url.md)
|
||||
@@ -24,7 +24,7 @@ const PROFILES = [
|
||||
'Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators.',
|
||||
color: 'cyan',
|
||||
tools:
|
||||
'Read, Write, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
|
||||
'Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
|
||||
requiredIncludes: [
|
||||
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
||||
'@~/.claude/gsd-core/references/research-philosophy.md',
|
||||
@@ -46,7 +46,7 @@ const PROFILES = [
|
||||
'Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator.',
|
||||
color: 'cyan',
|
||||
tools:
|
||||
'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
|
||||
'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
|
||||
requiredIncludes: [
|
||||
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
||||
'@~/.claude/gsd-core/references/research-philosophy.md',
|
||||
@@ -68,7 +68,7 @@ const PROFILES = [
|
||||
description:
|
||||
'Researches a single gray area decision and returns a structured comparison table with rationale. Spawned by discuss-phase advisor mode.',
|
||||
color: 'cyan',
|
||||
tools: 'Read, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*',
|
||||
tools: 'Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*',
|
||||
requiredIncludes: [
|
||||
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
||||
],
|
||||
@@ -117,7 +117,7 @@ const PROFILES = [
|
||||
'Produces UI-SPEC.md design contract for frontend phases. Reads upstream artifacts, detects design system state, asks only unanswered questions. Spawned by /gsd:ui-phase orchestrator.',
|
||||
color: 'purple',
|
||||
tools:
|
||||
'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
|
||||
'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
|
||||
requiredIncludes: [
|
||||
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
||||
],
|
||||
@@ -134,7 +134,7 @@ const PROFILES = [
|
||||
description:
|
||||
'Synthesizes research outputs from parallel researcher agents into SUMMARY.md. Spawned by /gsd:new-project after 4 researcher agents complete.',
|
||||
color: 'purple',
|
||||
tools: 'Read, Write, Bash',
|
||||
tools: 'Read, Write, Bash, Skill',
|
||||
requiredIncludes: [],
|
||||
requiredSeamCalls: [
|
||||
'gsd_run query commit',
|
||||
|
||||
35
src/init.cts
35
src/init.cts
@@ -1882,7 +1882,9 @@ function buildAgentSkillsBlock(
|
||||
// occurs when the caller has actually set trusted_global_roots.
|
||||
const trustedGlobalRoots = loadTrustedGlobalRoots(config);
|
||||
|
||||
const validPaths: { ref: string; display: string }[] = [];
|
||||
// Each entry is either a filesystem include ({ kind: 'include', ref, display }) or a
|
||||
// Skill-tool directive ({ kind: 'directive', name }) for plugin-provided namespaced skills.
|
||||
const validEntries: Array<{ kind: 'include'; ref: string; display: string } | { kind: 'directive'; name: string }> = [];
|
||||
for (const skillPath of skillPaths) {
|
||||
if (typeof skillPath !== 'string') continue;
|
||||
|
||||
@@ -1894,12 +1896,28 @@ function buildAgentSkillsBlock(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(skillName)) {
|
||||
// Accept: one or more [A-Za-z0-9_-]+ segments joined by single colons.
|
||||
// Rejects: empty segments (::), leading/trailing colon, dots, slashes, backslashes.
|
||||
if (!/^[A-Za-z0-9_-]+(:[A-Za-z0-9_-]+)*$/.test(skillName)) {
|
||||
process.stderr.write(
|
||||
`[agent-skills] WARNING: Invalid global skill name "${skillName}" — skipping\n`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const isNamespaced = skillName.includes(':');
|
||||
if (isNamespaced) {
|
||||
// Plugin-provided namespaced skill: no filesystem path exists locally.
|
||||
if (runtime === 'claude') {
|
||||
// Emit a natural-language Skill-tool directive (not a @-include).
|
||||
validEntries.push({ kind: 'directive', name: skillName });
|
||||
} else {
|
||||
process.stderr.write(
|
||||
`[agent-skills] WARNING: Plugin-namespaced skill "global:${skillName}" requires a Skill-tool-capable runtime (claude) — skipping on runtime "${runtime}"\n`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Non-namespaced bare name: attempt filesystem resolution as before.
|
||||
if (globalSkillsBase === null) {
|
||||
process.stderr.write(
|
||||
`[agent-skills] WARNING: Runtime "${runtime}" does not use a skills directory — "global:${skillName}" is not supported on this runtime\n`,
|
||||
@@ -1929,7 +1947,7 @@ function buildAgentSkillsBlock(
|
||||
}
|
||||
process.stderr.write(`[agent-skills] NOTE: Global skill "${skillName}" accepted via trusted_global_roots (resolves outside the default skills dir)\n`);
|
||||
}
|
||||
validPaths.push({ ref: `${globalSkillDir}/SKILL.md`, display: displayPath });
|
||||
validEntries.push({ kind: 'include', ref: `${globalSkillDir}/SKILL.md`, display: displayPath });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1949,12 +1967,17 @@ function buildAgentSkillsBlock(
|
||||
continue;
|
||||
}
|
||||
|
||||
validPaths.push({ ref: `${skillPath}/SKILL.md`, display: skillPath });
|
||||
validEntries.push({ kind: 'include', ref: `${skillPath}/SKILL.md`, display: skillPath });
|
||||
}
|
||||
|
||||
if (validPaths.length === 0) return '';
|
||||
if (validEntries.length === 0) return '';
|
||||
|
||||
const lines = validPaths.map((p) => `- @${p.ref}`).join('\n');
|
||||
const lines = validEntries.map((entry) => {
|
||||
if (entry.kind === 'directive') {
|
||||
return `- Load the \`${entry.name}\` skill via the Skill tool before proceeding (plugin-provided).`;
|
||||
}
|
||||
return `- @${entry.ref}`;
|
||||
}).join('\n');
|
||||
return `<agent_skills>\nRead these user-configured skills:\n${lines}\n</agent_skills>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
{
|
||||
"gsd-advisor-researcher.md": 4536,
|
||||
"gsd-advisor-researcher.md": 4543,
|
||||
"gsd-ai-researcher.md": 5851,
|
||||
"gsd-assumptions-analyzer.md": 4489,
|
||||
"gsd-code-fixer.md": 36499,
|
||||
"gsd-code-reviewer.md": 16773,
|
||||
"gsd-codebase-mapper.md": 21388,
|
||||
"gsd-assumptions-analyzer.md": 4496,
|
||||
"gsd-code-fixer.md": 36506,
|
||||
"gsd-code-reviewer.md": 16780,
|
||||
"gsd-codebase-mapper.md": 21395,
|
||||
"gsd-debug-session-manager.md": 14159,
|
||||
"gsd-debugger.md": 51213,
|
||||
"gsd-debugger.md": 51220,
|
||||
"gsd-doc-classifier.md": 7629,
|
||||
"gsd-doc-synthesizer.md": 9722,
|
||||
"gsd-doc-verifier.md": 12403,
|
||||
"gsd-doc-writer.md": 38827,
|
||||
"gsd-doc-writer.md": 38834,
|
||||
"gsd-domain-researcher.md": 6938,
|
||||
"gsd-eval-auditor.md": 7754,
|
||||
"gsd-eval-auditor.md": 7761,
|
||||
"gsd-eval-planner.md": 7008,
|
||||
"gsd-executor.md": 42512,
|
||||
"gsd-executor.md": 42519,
|
||||
"gsd-framework-selector.md": 6778,
|
||||
"gsd-integration-checker.md": 15141,
|
||||
"gsd-integration-checker.md": 15148,
|
||||
"gsd-intel-updater.md": 18122,
|
||||
"gsd-mempalace-curator.md": 4160,
|
||||
"gsd-nyquist-auditor.md": 7245,
|
||||
"gsd-nyquist-auditor.md": 7255,
|
||||
"gsd-pattern-mapper.md": 12487,
|
||||
"gsd-phase-researcher.md": 40611,
|
||||
"gsd-plan-checker.md": 41996,
|
||||
"gsd-planner.md": 48885,
|
||||
"gsd-project-researcher.md": 21987,
|
||||
"gsd-research-synthesizer.md": 13646,
|
||||
"gsd-roadmapper.md": 21774,
|
||||
"gsd-security-auditor.md": 6216,
|
||||
"gsd-ui-auditor.md": 17152,
|
||||
"gsd-ui-checker.md": 11081,
|
||||
"gsd-ui-researcher.md": 19265,
|
||||
"gsd-phase-researcher.md": 40618,
|
||||
"gsd-plan-checker.md": 42003,
|
||||
"gsd-planner.md": 48892,
|
||||
"gsd-project-researcher.md": 21994,
|
||||
"gsd-research-synthesizer.md": 13653,
|
||||
"gsd-roadmapper.md": 21781,
|
||||
"gsd-security-auditor.md": 6226,
|
||||
"gsd-ui-auditor.md": 17159,
|
||||
"gsd-ui-checker.md": 11088,
|
||||
"gsd-ui-researcher.md": 19272,
|
||||
"gsd-user-profiler.md": 8516,
|
||||
"gsd-verifier.md": 43339
|
||||
"gsd-verifier.md": 43346
|
||||
}
|
||||
|
||||
@@ -773,3 +773,510 @@ describe('trusted_global_roots e2e CLI (#52)', () => {
|
||||
assert.strictEqual(r.ir.block, '', 'block must be empty when "/" is the trusted root (rejected as too broad)');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── bug #1243: plugin-namespaced agent skills ─────────────────────────────────
|
||||
// allow-test-rule: source-text-is-the-product (#1243)
|
||||
|
||||
describe('bug #1243: plugin-namespaced agent skills', () => {
|
||||
let tmpDir;
|
||||
let fakeHome;
|
||||
let globalSkillsDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
fakeHome = fs.mkdtempSync(path.join(require('os').tmpdir(), 'gsd-1243-home-'));
|
||||
globalSkillsDir = path.join(fakeHome, '.claude', 'skills');
|
||||
fs.mkdirSync(globalSkillsDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
cleanup(fakeHome);
|
||||
});
|
||||
|
||||
function createGlobalSkill1243(name) {
|
||||
const skillDir = path.join(globalSkillsDir, name);
|
||||
fs.mkdirSync(skillDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), `# ${name}\nGlobal skill content.\n`);
|
||||
return skillDir;
|
||||
}
|
||||
|
||||
// ─── happy path ────────────────────────────────────────────────────────────
|
||||
|
||||
test('happy: global:coderabbit:code-review (claude) emits directive naming coderabbit:code-review, no @-line, no path', () => {
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: { 'gsd-executor': ['global:coderabbit:code-review'] },
|
||||
});
|
||||
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
// Must contain the namespaced name
|
||||
assert.ok(
|
||||
r.ir.block.includes('coderabbit:code-review'),
|
||||
`block must contain namespaced name, got: ${r.ir.block}`
|
||||
);
|
||||
// Must NOT be a @-include line
|
||||
assert.ok(
|
||||
!r.ir.block.includes('- @'),
|
||||
`block must not contain @-include line, got: ${r.ir.block}`
|
||||
);
|
||||
// Must NOT contain filesystem path or plugins/cache
|
||||
assert.ok(
|
||||
!r.ir.block.includes('plugins/cache'),
|
||||
`block must not contain plugins/cache, got: ${r.ir.block}`
|
||||
);
|
||||
// Must have the <agent_skills> wrapper
|
||||
assert.ok(
|
||||
r.ir.block.includes('<agent_skills>'),
|
||||
`block must contain <agent_skills> wrapper, got: ${r.ir.block}`
|
||||
);
|
||||
});
|
||||
|
||||
// ─── mixed: path-resolvable + namespaced ────────────────────────────────────
|
||||
|
||||
test('mixed: path-resolvable global + namespaced → @-include AND directive in block', () => {
|
||||
createGlobalSkill1243('my-local-skill');
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: {
|
||||
'gsd-executor': ['global:my-local-skill', 'global:vendor:remote-skill'],
|
||||
},
|
||||
});
|
||||
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
// The path-resolvable one must be a @-include
|
||||
assert.ok(
|
||||
r.ir.block.includes('- @') && r.ir.block.includes('my-local-skill/SKILL.md'),
|
||||
`block must contain @-include for path-resolvable skill, got: ${r.ir.block}`
|
||||
);
|
||||
// The namespaced one must be a directive, not a @-include
|
||||
assert.ok(
|
||||
r.ir.block.includes('vendor:remote-skill'),
|
||||
`block must contain namespaced directive, got: ${r.ir.block}`
|
||||
);
|
||||
});
|
||||
|
||||
// ─── precedence: bare unresolved vs resolved ─────────────────────────────────
|
||||
|
||||
test('precedence: bare global:foo not-on-disk → not found/skipped, no directive', () => {
|
||||
// foo is NOT created on disk
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: { 'gsd-executor': ['global:foo'] },
|
||||
});
|
||||
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
assert.strictEqual(r.ir.block, '', `bare unresolved name must produce empty block, got: ${r.ir.block}`);
|
||||
});
|
||||
|
||||
test('precedence: bare global:foo that resolves → @-include (existing path behavior)', () => {
|
||||
createGlobalSkill1243('foo');
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: { 'gsd-executor': ['global:foo'] },
|
||||
});
|
||||
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
assert.ok(
|
||||
r.ir.block.includes('- @') && r.ir.block.includes('foo/SKILL.md'),
|
||||
`path-resolvable bare name must produce @-include, got: ${r.ir.block}`
|
||||
);
|
||||
});
|
||||
|
||||
// ─── negative validation ─────────────────────────────────────────────────────
|
||||
|
||||
test('negative: global:../evil rejected', () => {
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: { 'gsd-executor': ['global:../evil'] },
|
||||
});
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
assert.strictEqual(r.ir.block, '', `traversal must be rejected, got: ${r.ir.block}`);
|
||||
});
|
||||
|
||||
test('negative: global:a::b rejected (empty segment)', () => {
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: { 'gsd-executor': ['global:a::b'] },
|
||||
});
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
assert.strictEqual(r.ir.block, '', `double-colon must be rejected, got: ${r.ir.block}`);
|
||||
});
|
||||
|
||||
test('negative: global::x rejected (leading colon)', () => {
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: { 'gsd-executor': ['global::x'] },
|
||||
});
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
assert.strictEqual(r.ir.block, '', `leading colon must be rejected, got: ${r.ir.block}`);
|
||||
});
|
||||
|
||||
test('negative: global:x: rejected (trailing colon)', () => {
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: { 'gsd-executor': ['global:x:'] },
|
||||
});
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
assert.strictEqual(r.ir.block, '', `trailing colon must be rejected, got: ${r.ir.block}`);
|
||||
});
|
||||
|
||||
test('negative: global:a/b rejected (slash in name)', () => {
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: { 'gsd-executor': ['global:a/b'] },
|
||||
});
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
assert.strictEqual(r.ir.block, '', `slash in name must be rejected, got: ${r.ir.block}`);
|
||||
});
|
||||
|
||||
test('negative: global: (empty name) rejected', () => {
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: { 'gsd-executor': ['global:'] },
|
||||
});
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
assert.strictEqual(r.ir.block, '', `empty name must be rejected, got: ${r.ir.block}`);
|
||||
});
|
||||
|
||||
// ─── cross-runtime ──────────────────────────────────────────────────────────
|
||||
|
||||
test('cross-runtime: namespaced + codex runtime → no directive (skipped/warned)', () => {
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'codex',
|
||||
agent_skills: { 'gsd-executor': ['global:vendor:remote-skill'] },
|
||||
});
|
||||
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
assert.strictEqual(
|
||||
r.ir.block,
|
||||
'',
|
||||
`namespaced skill on non-claude runtime must produce empty block, got: ${r.ir.block}`
|
||||
);
|
||||
});
|
||||
|
||||
test('cross-runtime: namespaced + claude runtime → directive emitted', () => {
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: { 'gsd-executor': ['global:vendor:remote-skill'] },
|
||||
});
|
||||
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
assert.ok(
|
||||
r.ir.block.includes('vendor:remote-skill'),
|
||||
`claude runtime must emit directive for namespaced skill, got: ${r.ir.block}`
|
||||
);
|
||||
});
|
||||
|
||||
// ─── regression (Hyrum) ─────────────────────────────────────────────────────
|
||||
|
||||
test('HYRUM regression: include-only block is BYTE-IDENTICAL to expected format', () => {
|
||||
// This test asserts the FULL block output is byte-identical for an include-only
|
||||
// config (path-resolvable global skill + project-relative local skill).
|
||||
// It protects the ~22 workflow consumers that depend on this exact block shape.
|
||||
createGlobalSkill1243('shadcn');
|
||||
const projectSkillDir = path.join(tmpDir, 'skills', 'local');
|
||||
fs.mkdirSync(projectSkillDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(projectSkillDir, 'SKILL.md'), '# local\n');
|
||||
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: { 'gsd-executor': ['global:shadcn', 'skills/local'] },
|
||||
});
|
||||
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
|
||||
// Compute the expected absolute path for the global skill (resolved via fakeHome)
|
||||
const expectedGlobalPath = path.join(fakeHome, '.claude', 'skills', 'shadcn', 'SKILL.md');
|
||||
// The local path is always project-relative (not absolute)
|
||||
const expectedLocalPath = 'skills/local/SKILL.md';
|
||||
|
||||
const expectedBlock = [
|
||||
'<agent_skills>',
|
||||
'Read these user-configured skills:',
|
||||
`- @${expectedGlobalPath.replace(/\\/g, '/')}`,
|
||||
`- @${expectedLocalPath}`,
|
||||
'</agent_skills>',
|
||||
].join('\n');
|
||||
|
||||
assert.strictEqual(
|
||||
r.ir.block.replace(/\\/g, '/'),
|
||||
expectedBlock,
|
||||
`HYRUM: block must be byte-identical to expected include-only format.\nExpected: ${JSON.stringify(expectedBlock)}\nGot: ${JSON.stringify(r.ir.block)}`
|
||||
);
|
||||
});
|
||||
|
||||
test('regression: empty/missing config → empty block', () => {
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
assert.strictEqual(r.ir.block, '', `missing config must produce empty block, got: ${r.ir.block}`);
|
||||
});
|
||||
|
||||
test('BYTE-IDENTICAL mixed-block: path-resolvable global + plugin-namespaced → single section, interleaved, exact format', () => {
|
||||
// Regression for code-review finding: docs previously showed a bogus two-section format
|
||||
// with a separate "Load these plugin-provided skills using the Skill tool:" header.
|
||||
// The ACTUAL emitted block is a single <agent_skills> section where @-includes and
|
||||
// plugin-provided directives are interleaved in config order under the same header.
|
||||
//
|
||||
// Config order: global:my-local-skill (path-resolvable) FIRST, then global:vendor:remote-skill (namespaced).
|
||||
createGlobalSkill1243('my-local-skill');
|
||||
writeConfig(tmpDir, {
|
||||
runtime: 'claude',
|
||||
agent_skills: {
|
||||
'gsd-executor': ['global:my-local-skill', 'global:vendor:remote-skill'],
|
||||
},
|
||||
});
|
||||
|
||||
const r = runAgentSkillsJson(
|
||||
['agent-skills', 'gsd-executor'], tmpDir, { HOME: fakeHome, USERPROFILE: fakeHome }
|
||||
);
|
||||
assert.ok(r.success, `Command failed: ${r.error}`);
|
||||
|
||||
// Compute the expected @-include path (absolute path to the resolved global skill)
|
||||
const expectedInclude = path.join(fakeHome, '.claude', 'skills', 'my-local-skill', 'SKILL.md');
|
||||
|
||||
const expectedBlock = [
|
||||
'<agent_skills>',
|
||||
'Read these user-configured skills:',
|
||||
`- @${expectedInclude.replace(/\\/g, '/')}`,
|
||||
'- Load the `vendor:remote-skill` skill via the Skill tool before proceeding (plugin-provided).',
|
||||
'</agent_skills>',
|
||||
].join('\n');
|
||||
|
||||
assert.strictEqual(
|
||||
r.ir.block.replace(/\\/g, '/'),
|
||||
expectedBlock,
|
||||
`BYTE-IDENTICAL: mixed block must be a single section with @-include and directive interleaved.\nExpected: ${JSON.stringify(expectedBlock)}\nGot: ${JSON.stringify(r.ir.block)}`
|
||||
);
|
||||
|
||||
// Structural assertions: must NOT contain any secondary header
|
||||
assert.ok(
|
||||
!r.ir.block.includes('Load these plugin-provided skills using the Skill tool:'),
|
||||
`block must NOT contain the bogus two-section header, got: ${r.ir.block}`
|
||||
);
|
||||
});
|
||||
|
||||
// ─── grant: Skill tool in consumer agent frontmatter ─────────────────────────
|
||||
|
||||
test('grant: all 22 agent_skills consumer agents have Skill in their tools frontmatter', () => {
|
||||
const CONSUMER_AGENTS = [
|
||||
'gsd-advisor-researcher',
|
||||
'gsd-assumptions-analyzer',
|
||||
'gsd-code-fixer',
|
||||
'gsd-code-reviewer',
|
||||
'gsd-codebase-mapper',
|
||||
'gsd-debugger',
|
||||
'gsd-doc-writer',
|
||||
'gsd-eval-auditor',
|
||||
'gsd-executor',
|
||||
'gsd-integration-checker',
|
||||
'gsd-nyquist-auditor',
|
||||
'gsd-phase-researcher',
|
||||
'gsd-plan-checker',
|
||||
'gsd-planner',
|
||||
'gsd-project-researcher',
|
||||
'gsd-research-synthesizer',
|
||||
'gsd-roadmapper',
|
||||
'gsd-security-auditor',
|
||||
'gsd-ui-auditor',
|
||||
'gsd-ui-checker',
|
||||
'gsd-ui-researcher',
|
||||
'gsd-verifier',
|
||||
];
|
||||
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||
|
||||
/**
|
||||
* Extract tool names from an agent file's frontmatter.
|
||||
* Handles both inline CSV format ("tools: Read, Write") and
|
||||
* YAML block sequence format ("tools:\n - Read\n - Write").
|
||||
*/
|
||||
function extractTools(content) {
|
||||
// Parse frontmatter between first pair of --- delimiters
|
||||
const lines = content.split('\n');
|
||||
let fmStart = -1;
|
||||
let fmEnd = -1;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].trim() === '---') {
|
||||
if (fmStart === -1) fmStart = i;
|
||||
else { fmEnd = i; break; }
|
||||
}
|
||||
}
|
||||
if (fmStart === -1 || fmEnd === -1) return [];
|
||||
const fmLines = lines.slice(fmStart + 1, fmEnd);
|
||||
// Find 'tools:' line
|
||||
const toolsIdx = fmLines.findIndex((l) => /^tools:/.test(l));
|
||||
if (toolsIdx === -1) return [];
|
||||
const toolsLine = fmLines[toolsIdx];
|
||||
const inlineValue = toolsLine.replace(/^tools:\s*/, '').trim();
|
||||
if (inlineValue) {
|
||||
// Inline CSV format: "tools: Read, Write, ..."
|
||||
return inlineValue.split(',').map((t) => t.trim()).filter(Boolean);
|
||||
}
|
||||
// Block sequence format: next lines starting with " - ..."
|
||||
const tools = [];
|
||||
for (let i = toolsIdx + 1; i < fmLines.length; i++) {
|
||||
const m = fmLines[i].match(/^\s+-\s+(\S.*)/);
|
||||
if (!m) break; // end of block list
|
||||
tools.push(m[1].trim());
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
|
||||
const failures = [];
|
||||
for (const agentName of CONSUMER_AGENTS) {
|
||||
const agentPath = path.join(AGENTS_DIR, agentName + '.md');
|
||||
assert.ok(fs.existsSync(agentPath), `Agent file not found: ${agentPath}`);
|
||||
const content = fs.readFileSync(agentPath, 'utf8');
|
||||
const toolsList = extractTools(content);
|
||||
if (!toolsList.includes('Skill')) {
|
||||
failures.push(`${agentName}: tools=[${toolsList.join(', ')}] — missing Skill`);
|
||||
}
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
failures,
|
||||
[],
|
||||
`These consumer agents are missing "Skill" in their tools frontmatter:\n${failures.join('\n')}`
|
||||
);
|
||||
});
|
||||
|
||||
test('grant: exact set of agents with Skill equals the 22 consumers (drift guard)', () => {
|
||||
// This test asserts that the SET of agents declaring Skill in their frontmatter
|
||||
// tools: field is EXACTLY the 22 known consumers — no more, no less.
|
||||
//
|
||||
// If a new agent legitimately needs Skill outside this set, add it to
|
||||
// KNOWN_SKILL_AGENTS with a comment explaining why.
|
||||
//
|
||||
// Empirically verified 2026-06-14: no agent outside the 22 consumers declares
|
||||
// Skill in its frontmatter tools: — KNOWN_SKILL_AGENTS is the 22 consumers only.
|
||||
const KNOWN_SKILL_AGENTS = new Set([
|
||||
// ── 22 agent_skills consumers (spawn child agents + inject skill context) ──
|
||||
'gsd-advisor-researcher',
|
||||
'gsd-assumptions-analyzer',
|
||||
'gsd-code-fixer',
|
||||
'gsd-code-reviewer',
|
||||
'gsd-codebase-mapper',
|
||||
'gsd-debugger',
|
||||
'gsd-doc-writer',
|
||||
'gsd-eval-auditor',
|
||||
'gsd-executor',
|
||||
'gsd-integration-checker',
|
||||
'gsd-nyquist-auditor',
|
||||
'gsd-phase-researcher',
|
||||
'gsd-plan-checker',
|
||||
'gsd-planner',
|
||||
'gsd-project-researcher',
|
||||
'gsd-research-synthesizer',
|
||||
'gsd-roadmapper',
|
||||
'gsd-security-auditor',
|
||||
'gsd-ui-auditor',
|
||||
'gsd-ui-checker',
|
||||
'gsd-ui-researcher',
|
||||
'gsd-verifier',
|
||||
]);
|
||||
|
||||
// allow-test-rule: source-text-is-the-product (#1243)
|
||||
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||
const agentFiles = fs.readdirSync(AGENTS_DIR).filter((f) => f.startsWith('gsd-') && f.endsWith('.md'));
|
||||
|
||||
/**
|
||||
* Extract tool names from an agent file's frontmatter (same logic as above).
|
||||
* Handles both inline CSV and YAML block-sequence forms.
|
||||
*/
|
||||
function extractToolsForDriftGuard(content) {
|
||||
const lines = content.split('\n');
|
||||
let fmStart = -1, fmEnd = -1;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].trim() === '---') {
|
||||
if (fmStart === -1) fmStart = i;
|
||||
else { fmEnd = i; break; }
|
||||
}
|
||||
}
|
||||
if (fmStart === -1 || fmEnd === -1) return [];
|
||||
const fmLines = lines.slice(fmStart + 1, fmEnd);
|
||||
const toolsIdx = fmLines.findIndex((l) => /^tools:/.test(l));
|
||||
if (toolsIdx === -1) return [];
|
||||
const toolsLine = fmLines[toolsIdx];
|
||||
const inlineValue = toolsLine.replace(/^tools:\s*/, '').trim();
|
||||
if (inlineValue) return inlineValue.split(',').map((t) => t.trim()).filter(Boolean);
|
||||
const tools = [];
|
||||
for (let i = toolsIdx + 1; i < fmLines.length; i++) {
|
||||
const m = fmLines[i].match(/^\s+-\s+(\S.*)/);
|
||||
if (!m) break;
|
||||
tools.push(m[1].trim());
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
|
||||
// Collect actual set of agents with Skill in frontmatter
|
||||
const actualSkillSet = new Set();
|
||||
for (const file of agentFiles) {
|
||||
const name = file.replace('.md', '');
|
||||
const content = fs.readFileSync(path.join(AGENTS_DIR, file), 'utf8');
|
||||
const tools = extractToolsForDriftGuard(content);
|
||||
if (tools.includes('Skill')) actualSkillSet.add(name);
|
||||
}
|
||||
|
||||
// 1. Every consumer MUST have Skill
|
||||
const missingSkill = [];
|
||||
for (const agent of KNOWN_SKILL_AGENTS) {
|
||||
if (!actualSkillSet.has(agent)) missingSkill.push(agent);
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
missingSkill,
|
||||
[],
|
||||
`These consumer agents are MISSING "Skill" in their tools frontmatter:\n${missingSkill.join('\n')}`
|
||||
);
|
||||
|
||||
// 2. The actual skill set must EQUAL the known set exactly (no extras)
|
||||
const unexpectedSkill = [];
|
||||
for (const agent of actualSkillSet) {
|
||||
if (!KNOWN_SKILL_AGENTS.has(agent)) unexpectedSkill.push(agent);
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
unexpectedSkill,
|
||||
[],
|
||||
`These agents declare "Skill" but are NOT in KNOWN_SKILL_AGENTS:\n${unexpectedSkill.join('\n')}\nIf this is intentional, add the agent to KNOWN_SKILL_AGENTS with a comment.`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user