enh(#1243): consume Claude plugin-provided skills in agent_skills (epic #1258 Phase B) (#1261)

* 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:
Tom Boucher
2026-06-15 00:49:12 -04:00
committed by GitHub
parent 00c447701d
commit cf68841220
30 changed files with 749 additions and 73 deletions

View 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.

View File

@@ -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
---

View File

@@ -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
---

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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
---

View File

@@ -8,6 +8,7 @@ tools:
- Bash
- Glob
- Grep
- Skill
color: purple
---

View File

@@ -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:

View File

@@ -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
---

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -8,6 +8,7 @@ tools:
- Bash
- Glob
- Grep
- Skill
color: red
---

View File

@@ -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:

View File

@@ -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
---

View File

@@ -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:

View File

@@ -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:

View File

@@ -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`)

View File

@@ -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

View 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)

View File

@@ -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',

View File

@@ -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>`;
}

View File

@@ -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
}

View File

@@ -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.`
);
});
});