From cf688412201e93fb8a70905855768adae8491e73 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 15 Jun 2026 00:49:12 -0400 Subject: [PATCH] enh(#1243): consume Claude plugin-provided skills in agent_skills (epic #1258 Phase B) (#1261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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:, global::), 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 * 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:" requires a Skill-tool-capable runtime (claude) — skipping on 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 * 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 --------- Co-authored-by: Claude Sonnet 4.6 --- .changeset/rapid-pumas-fly.md | 5 + agents/gsd-advisor-researcher.md | 2 +- agents/gsd-assumptions-analyzer.md | 2 +- agents/gsd-code-fixer.md | 2 +- agents/gsd-code-reviewer.md | 2 +- agents/gsd-codebase-mapper.md | 2 +- agents/gsd-debugger.md | 2 +- agents/gsd-doc-writer.md | 2 +- agents/gsd-eval-auditor.md | 2 +- agents/gsd-executor.md | 2 +- agents/gsd-integration-checker.md | 2 +- agents/gsd-nyquist-auditor.md | 1 + agents/gsd-phase-researcher.md | 2 +- agents/gsd-plan-checker.md | 2 +- agents/gsd-planner.md | 2 +- agents/gsd-project-researcher.md | 2 +- agents/gsd-research-synthesizer.md | 2 +- agents/gsd-roadmapper.md | 2 +- agents/gsd-security-auditor.md | 1 + agents/gsd-ui-auditor.md | 2 +- agents/gsd-ui-checker.md | 2 +- agents/gsd-ui-researcher.md | 2 +- agents/gsd-verifier.md | 2 +- docs/CONFIGURATION.md | 80 ++- docs/README.md | 1 + .../attach-a-plugin-skill-to-a-gsd-agent.md | 98 ++++ scripts/research-profiles.cjs | 10 +- src/init.cts | 35 +- tests/agent-size-baseline.json | 44 +- tests/agent-skills.test.cjs | 507 ++++++++++++++++++ 30 files changed, 749 insertions(+), 73 deletions(-) create mode 100644 .changeset/rapid-pumas-fly.md create mode 100644 docs/how-to/attach-a-plugin-skill-to-a-gsd-agent.md diff --git a/.changeset/rapid-pumas-fly.md b/.changeset/rapid-pumas-fly.md new file mode 100644 index 000000000..a863ebdaa --- /dev/null +++ b/.changeset/rapid-pumas-fly.md @@ -0,0 +1,5 @@ +--- +type: Added +pr: 1261 +--- +**`agent_skills` can now reference Claude-Code plugin-provided skills** via the namespaced `global::` 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. diff --git a/agents/gsd-advisor-researcher.md b/agents/gsd-advisor-researcher.md index 9218b97b8..069e98e38 100644 --- a/agents/gsd-advisor-researcher.md +++ b/agents/gsd-advisor-researcher.md @@ -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 --- diff --git a/agents/gsd-assumptions-analyzer.md b/agents/gsd-assumptions-analyzer.md index 5531fc4a9..91d7cde1f 100644 --- a/agents/gsd-assumptions-analyzer.md +++ b/agents/gsd-assumptions-analyzer.md @@ -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 --- diff --git a/agents/gsd-code-fixer.md b/agents/gsd-code-fixer.md index e742cb1f1..1b89dd79c 100644 --- a/agents/gsd-code-fixer.md +++ b/agents/gsd-code-fixer.md @@ -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 diff --git a/agents/gsd-code-reviewer.md b/agents/gsd-code-reviewer.md index 772c22aeb..0ecc81549 100644 --- a/agents/gsd-code-reviewer.md +++ b/agents/gsd-code-reviewer.md @@ -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 diff --git a/agents/gsd-codebase-mapper.md b/agents/gsd-codebase-mapper.md index cc1eb1c82..4897c3040 100644 --- a/agents/gsd-codebase-mapper.md +++ b/agents/gsd-codebase-mapper.md @@ -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: diff --git a/agents/gsd-debugger.md b/agents/gsd-debugger.md index be505e384..565ea6508 100644 --- a/agents/gsd-debugger.md +++ b/agents/gsd-debugger.md @@ -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: diff --git a/agents/gsd-doc-writer.md b/agents/gsd-doc-writer.md index 620fe96b1..d594b7bb3 100644 --- a/agents/gsd-doc-writer.md +++ b/agents/gsd-doc-writer.md @@ -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: diff --git a/agents/gsd-eval-auditor.md b/agents/gsd-eval-auditor.md index d55503831..b0608810c 100644 --- a/agents/gsd-eval-auditor.md +++ b/agents/gsd-eval-auditor.md @@ -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: diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index 8735b4a7c..90739fc76 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -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: diff --git a/agents/gsd-integration-checker.md b/agents/gsd-integration-checker.md index cd40576c5..ab324b012 100644 --- a/agents/gsd-integration-checker.md +++ b/agents/gsd-integration-checker.md @@ -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 --- diff --git a/agents/gsd-nyquist-auditor.md b/agents/gsd-nyquist-auditor.md index 35279b10b..862fef52a 100644 --- a/agents/gsd-nyquist-auditor.md +++ b/agents/gsd-nyquist-auditor.md @@ -8,6 +8,7 @@ tools: - Bash - Glob - Grep + - Skill color: purple --- diff --git a/agents/gsd-phase-researcher.md b/agents/gsd-phase-researcher.md index 70b0eaf7c..e78443aa7 100644 --- a/agents/gsd-phase-researcher.md +++ b/agents/gsd-phase-researcher.md @@ -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: diff --git a/agents/gsd-plan-checker.md b/agents/gsd-plan-checker.md index d4de9234a..d1ac46e34 100644 --- a/agents/gsd-plan-checker.md +++ b/agents/gsd-plan-checker.md @@ -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 --- diff --git a/agents/gsd-planner.md b/agents/gsd-planner.md index 82d3855c5..89717878a 100644 --- a/agents/gsd-planner.md +++ b/agents/gsd-planner.md @@ -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: diff --git a/agents/gsd-project-researcher.md b/agents/gsd-project-researcher.md index 76b6e6967..8f5286bc5 100644 --- a/agents/gsd-project-researcher.md +++ b/agents/gsd-project-researcher.md @@ -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: diff --git a/agents/gsd-research-synthesizer.md b/agents/gsd-research-synthesizer.md index 4b8d13baa..b29b124d5 100644 --- a/agents/gsd-research-synthesizer.md +++ b/agents/gsd-research-synthesizer.md @@ -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: diff --git a/agents/gsd-roadmapper.md b/agents/gsd-roadmapper.md index 00ad0d6c4..02195ef4b 100644 --- a/agents/gsd-roadmapper.md +++ b/agents/gsd-roadmapper.md @@ -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: diff --git a/agents/gsd-security-auditor.md b/agents/gsd-security-auditor.md index 209ce4683..e668a64b8 100644 --- a/agents/gsd-security-auditor.md +++ b/agents/gsd-security-auditor.md @@ -8,6 +8,7 @@ tools: - Bash - Glob - Grep + - Skill color: red --- diff --git a/agents/gsd-ui-auditor.md b/agents/gsd-ui-auditor.md index 3ed6cc9e6..f30d28a35 100644 --- a/agents/gsd-ui-auditor.md +++ b/agents/gsd-ui-auditor.md @@ -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: diff --git a/agents/gsd-ui-checker.md b/agents/gsd-ui-checker.md index adb0e67a3..b59e4f85e 100644 --- a/agents/gsd-ui-checker.md +++ b/agents/gsd-ui-checker.md @@ -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 --- diff --git a/agents/gsd-ui-researcher.md b/agents/gsd-ui-researcher.md index 08a909b4a..8a4e9263d 100644 --- a/agents/gsd-ui-researcher.md +++ b/agents/gsd-ui-researcher.md @@ -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: diff --git a/agents/gsd-verifier.md b/agents/gsd-verifier.md index 4f7e8ad74..4c0f1a9ab 100644 --- a/agents/gsd-verifier.md +++ b/agents/gsd-verifier.md @@ -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: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e98a22494..efce61aed 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 `/skills/my-skill/SKILL.md`, injected as an `@`-include | +| Global personal skill | `"global:"` | Resolves to `~/.claude/skills//SKILL.md`, injected as an `@`-include | +| Plugin-provided skill (Claude only) | `"global::"` | 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:`) 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::`) 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 `` 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 `` block. ### How It Works -At spawn time, workflows call `gsd-tools query agent-skills ` (or legacy `node gsd-tools.cjs agent-skills `) to load configured skills. If skills exist for the agent type, they are injected as an `` block in the Task() prompt: +At spawn time, workflows call `gsd-tools query agent-skills ` (or legacy `node gsd-tools.cjs agent-skills `) to load configured skills. If skills exist for the agent type, they are injected as an `` block in the Task() prompt. + +For project-relative and global personal skills, entries appear as `@`-includes: ```xml Read these user-configured skills: - @skills/testing-standards/SKILL.md -- @skills/api-conventions/SKILL.md +- @/Users/you/.claude/skills/shared-conventions/SKILL.md + +``` + +For a mixed config (path-resolvable and plugin-provided skills together), entries appear interleaved in config order in a single section: + +```xml + +Read these user-configured skills: +- @skills/testing-standards/SKILL.md +- Load the `coderabbit:code-review` skill via the Skill tool before proceeding (plugin-provided). ``` @@ -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`) diff --git a/docs/README.md b/docs/README.md index edeaa1111..471a8832a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 diff --git a/docs/how-to/attach-a-plugin-skill-to-a-gsd-agent.md b/docs/how-to/attach-a-plugin-skill-to-a-gsd-agent.md new file mode 100644 index 000000000..e4c3d305b --- /dev/null +++ b/docs/how-to/attach-a-plugin-skill-to-a-gsd-agent.md @@ -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::` 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 +``` + +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:: +``` + +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:` personal skills, and `global::` 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 `` block that includes a Skill-tool load directive for the plugin skill: + +```xml + +Read these user-configured skills: +- @skills/project-conventions/SKILL.md +- Load the `coderabbit:code-review` skill via the Skill tool before proceeding (plugin-provided). + +``` + +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 ""` 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::` entry is silently skipped with a warning on non-Claude runtimes. Project-relative and `global:` 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) diff --git a/scripts/research-profiles.cjs b/scripts/research-profiles.cjs index 111fd317e..afe801e17 100644 --- a/scripts/research-profiles.cjs +++ b/scripts/research-profiles.cjs @@ -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', diff --git a/src/init.cts b/src/init.cts index 697428162..286921dca 100644 --- a/src/init.cts +++ b/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 `\nRead these user-configured skills:\n${lines}\n`; } diff --git a/tests/agent-size-baseline.json b/tests/agent-size-baseline.json index 02a2086d3..7b479690a 100644 --- a/tests/agent-size-baseline.json +++ b/tests/agent-size-baseline.json @@ -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 } diff --git a/tests/agent-skills.test.cjs b/tests/agent-skills.test.cjs index 8b058a895..7aff32d6a 100644 --- a/tests/agent-skills.test.cjs +++ b/tests/agent-skills.test.cjs @@ -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 wrapper + assert.ok( + r.ir.block.includes(''), + `block must contain 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 = [ + '', + 'Read these user-configured skills:', + `- @${expectedGlobalPath.replace(/\\/g, '/')}`, + `- @${expectedLocalPath}`, + '', + ].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 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 = [ + '', + 'Read these user-configured skills:', + `- @${expectedInclude.replace(/\\/g, '/')}`, + '- Load the `vendor:remote-skill` skill via the Skill tool before proceeding (plugin-provided).', + '', + ].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.` + ); + }); +});