Merge pull request #1366 from gsd-build/feat/agent-skill-injection

feat: agent skill injection via config (#1355)
This commit is contained in:
Tom Boucher
2026-03-24 16:50:03 -04:00
committed by GitHub
23 changed files with 494 additions and 14 deletions

View File

@@ -6,3 +6,6 @@
# Examples:
# tests/fixtures/fake-credentials.json
# docs/examples/sample-config.yml
# plan-phase.md contains illustrative DATABASE_URL/REDIS_URL examples
get-shit-done/workflows/plan-phase.md

View File

@@ -66,7 +66,8 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd:new
"safety": {
"always_confirm_destructive": true,
"always_confirm_external_services": true
}
},
"agent_skills": {}
}
```
@@ -146,6 +147,72 @@ To keep planning artifacts out of git:
---
## Agent Skills Injection
Inject custom skill files into GSD subagent prompts. Skills are read by agents at spawn time, giving them project-specific instructions beyond what CLAUDE.md provides.
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `agent_skills` | object | `{}` | Map of agent types to skill directory paths |
### Configuration
Add an `agent_skills` section to `.planning/config.json` mapping agent types to arrays of skill directory paths (relative to project root):
```json
{
"agent_skills": {
"gsd-executor": ["skills/testing-standards", "skills/api-conventions"],
"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).
### Supported Agent Types
Any GSD agent type can receive skills. Common types:
- `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
### How It Works
At spawn time, workflows call `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:
```xml
<agent_skills>
Read these user-configured skills:
- @skills/testing-standards/SKILL.md
- @skills/api-conventions/SKILL.md
</agent_skills>
```
If no skills are configured, the block is omitted (zero overhead).
### CLI
Set skills via the CLI:
```bash
node gsd-tools.cjs config-set agent_skills.gsd-executor '["skills/my-skill"]'
```
---
## Parallelization Settings
| Setting | Type | Default | Description |

View File

@@ -548,6 +548,11 @@ async function runCommand(command, args, cwd, raw) {
break;
}
case 'agent-skills': {
init.cmdAgentSkills(cwd, args[1], raw);
break;
}
case 'history-digest': {
commands.cmdHistoryDigest(cwd, raw);
break;

View File

@@ -27,6 +27,18 @@ const VALID_CONFIG_KEYS = new Set([
'hooks.context_warnings',
]);
/**
* Check whether a config key path is valid.
* Supports exact matches from VALID_CONFIG_KEYS plus dynamic patterns
* like `agent_skills.<agent-type>` where the sub-key is freeform.
*/
function isValidConfigKey(keyPath) {
if (VALID_CONFIG_KEYS.has(keyPath)) return true;
// Allow agent_skills.<agent-type> with any agent type string
if (/^agent_skills\.[a-zA-Z0-9_-]+$/.test(keyPath)) return true;
return false;
}
const CONFIG_KEY_SUGGESTIONS = {
'workflow.nyquist_validation_enabled': 'workflow.nyquist_validation',
'agents.nyquist_validation_enabled': 'workflow.nyquist_validation',
@@ -120,6 +132,7 @@ function buildNewProjectConfig(userChoices) {
hooks: {
context_warnings: true,
},
agent_skills: {},
};
// Three-level deep merge: hardcoded <- userDefaults <- choices
@@ -142,6 +155,11 @@ function buildNewProjectConfig(userChoices) {
...(userDefaults.hooks || {}),
...(choices.hooks || {}),
},
agent_skills: {
...hardcoded.agent_skills,
...(userDefaults.agent_skills || {}),
...(choices.agent_skills || {}),
},
};
}
@@ -298,15 +316,18 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
validateKnownConfigKeyPath(keyPath);
if (!VALID_CONFIG_KEYS.has(keyPath)) {
error(`Unknown config key: "${keyPath}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}`);
if (!isValidConfigKey(keyPath)) {
error(`Unknown config key: "${keyPath}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}, agent_skills.<agent-type>`);
}
// Parse value (handle booleans and numbers)
// Parse value (handle booleans, numbers, and JSON arrays/objects)
let parsedValue = value;
if (value === 'true') parsedValue = true;
else if (value === 'false') parsedValue = false;
else if (!isNaN(value) && value !== '') parsedValue = Number(value);
else if (typeof value === 'string' && (value.startsWith('[') || value.startsWith('{'))) {
try { parsedValue = JSON.parse(value); } catch { /* keep as string */ }
}
const setConfigValueResult = setConfigValue(cwd, keyPath, parsedValue);
output(setConfigValueResult, raw, `${keyPath}=${parsedValue}`);

View File

@@ -309,6 +309,7 @@ function loadConfig(cwd) {
context_window: get('context_window') ?? defaults.context_window,
phase_naming: get('phase_naming') ?? defaults.phase_naming,
model_overrides: parsed.model_overrides || null,
agent_skills: parsed.agent_skills || {},
};
} catch {
return defaults;

View File

@@ -1342,6 +1342,77 @@ function cmdInitRemoveWorkspace(cwd, name, raw) {
output(result, raw);
}
/**
* Build a formatted agent skills block for injection into Task() prompts.
*
* Reads `config.agent_skills[agentType]` and validates each skill path exists
* within the project root. Returns a formatted `<agent_skills>` block or empty
* string if no skills are configured.
*
* @param {object} config - Loaded project config
* @param {string} agentType - The agent type (e.g., 'gsd-executor', 'gsd-planner')
* @param {string} projectRoot - Absolute path to project root (for path validation)
* @returns {string} Formatted skills block or empty string
*/
function buildAgentSkillsBlock(config, agentType, projectRoot) {
const { validatePath } = require('./security.cjs');
if (!config || !config.agent_skills || !agentType) return '';
let skillPaths = config.agent_skills[agentType];
if (!skillPaths) return '';
// Normalize single string to array
if (typeof skillPaths === 'string') skillPaths = [skillPaths];
if (!Array.isArray(skillPaths) || skillPaths.length === 0) return '';
const validPaths = [];
for (const skillPath of skillPaths) {
if (typeof skillPath !== 'string') continue;
// Validate path safety — must resolve within project root
const pathCheck = validatePath(skillPath, projectRoot);
if (!pathCheck.safe) {
process.stderr.write(`[agent-skills] WARNING: Skipping unsafe path "${skillPath}": ${pathCheck.error}\n`);
continue;
}
// Check that the skill directory and SKILL.md exist
const skillMdPath = path.join(projectRoot, skillPath, 'SKILL.md');
if (!fs.existsSync(skillMdPath)) {
process.stderr.write(`[agent-skills] WARNING: Skill not found at "${skillPath}/SKILL.md" — skipping\n`);
continue;
}
validPaths.push(skillPath);
}
if (validPaths.length === 0) return '';
const lines = validPaths.map(p => `- @${p}/SKILL.md`).join('\n');
return `<agent_skills>\nRead these user-configured skills:\n${lines}\n</agent_skills>`;
}
/**
* Command: output the agent skills block for a given agent type.
* Used by workflows: SKILLS=$(node "$TOOLS" agent-skills gsd-executor 2>/dev/null)
*/
function cmdAgentSkills(cwd, agentType, raw) {
if (!agentType) {
// No agent type — output empty string silently
output('', raw, '');
return;
}
const config = loadConfig(cwd);
const block = buildAgentSkillsBlock(config, agentType, cwd);
// Output raw text (not JSON) so workflows can embed it directly
if (block) {
process.stdout.write(block);
}
process.exit(0);
}
module.exports = {
cmdInitExecutePhase,
cmdInitPlanPhase,
@@ -1360,4 +1431,6 @@ module.exports = {
cmdInitListWorkspaces,
cmdInitRemoveWorkspace,
detectChildRepos,
buildAgentSkillsBlock,
cmdAgentSkills,
};

View File

@@ -39,5 +39,6 @@
},
"hooks": {
"context_warnings": true
}
},
"agent_skills": {}
}

View File

@@ -18,6 +18,7 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init milestone-op)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_CHECKER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-integration-checker 2>/dev/null)
```
Extract from init JSON: `milestone_version`, `milestone_name`, `phase_count`, `completed_phases`, `commit_docs`.
@@ -78,7 +79,8 @@ Milestone Requirements:
MUST map each integration finding to affected requirement IDs where applicable.
Verify cross-phase wiring and E2E user flows.",
Verify cross-phase wiring and E2E user flows.
${AGENT_SKILLS_CHECKER}",
subagent_type="gsd-integration-checker",
model="{integration_checker_model}"
)

View File

@@ -78,13 +78,19 @@ This runs in parallel - all gaps investigated simultaneously.
</step>
<step name="spawn_agents">
**Load agent skills:**
```bash
AGENT_SKILLS_DEBUGGER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-debugger 2>/dev/null)
```
**Spawn debug agents in parallel:**
For each gap, fill the debug-subagent-prompt template and spawn:
```
Task(
prompt=filled_debug_subagent_prompt + "\n\n<files_to_read>\n- {phase_dir}/{phase_num}-UAT.md\n- .planning/STATE.md\n</files_to_read>",
prompt=filled_debug_subagent_prompt + "\n\n<files_to_read>\n- {phase_dir}/{phase_num}-UAT.md\n- .planning/STATE.md\n</files_to_read>\n${AGENT_SKILLS_DEBUGGER}",
subagent_type="gsd-debugger",
isolation="worktree",
description="Debug: {truth_short}"

View File

@@ -66,6 +66,7 @@ Phase number from argument (required).
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_ANALYZER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-assumptions-analyzer 2>/dev/null)
```
Parse JSON for: `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`,
@@ -270,6 +271,8 @@ Return EXACTLY this structure:
## Needs External Research
[Topics where codebase alone is insufficient — library version compatibility,
ecosystem best practices, etc. Leave empty if codebase provides enough evidence.]
${AGENT_SKILLS_ANALYZER}
""")
```

View File

@@ -134,6 +134,7 @@ Phase number from argument (required).
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_ADVISOR=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-advisor 2>/dev/null)
```
Parse JSON for: `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_plans`, `has_verification`, `plan_count`, `roadmap_exists`, `planning_exists`.
@@ -498,7 +499,8 @@ After user selects gray areas in present_gray_areas, spawn parallel research age
<project_context>{project name and brief description from PROJECT.md}</project_context>
<calibration_tier>{resolved calibration tier: full_maturity | standard | minimal_decisive}</calibration_tier>
Research this gray area and return a structured comparison table with rationale.",
Research this gray area and return a structured comparison table with rationale.
${AGENT_SKILLS_ADVISOR}",
subagent_type="general-purpose",
model="{ADVISOR_MODEL}",
description="Research: {area_name}"

View File

@@ -63,6 +63,7 @@ Load all context in one call:
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init execute-phase "${PHASE_ARG}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-executor 2>/dev/null)
```
Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelization`, `branching_strategy`, `branch_name`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `plans`, `incomplete_plans`, `plan_count`, `incomplete_count`, `state_exists`, `roadmap_exists`, `phase_req_ids`.
@@ -258,6 +259,8 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT
- .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation)
</files_to_read>
${AGENT_SKILLS}
<mcp_tools>
If CLAUDE.md or project instructions reference MCP tools (e.g. jCodeMunch, context7,
or other MCP servers), prefer those tools over Grep/Glob for code navigation when available.
@@ -580,6 +583,10 @@ Use AskUserQuestion to present the options.
<step name="verify_phase_goal">
Verify phase achieved its GOAL, not just completed tasks.
```bash
VERIFIER_SKILLS=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-verifier 2>/dev/null)
```
```
Task(
prompt="Verify phase {phase_number} goal achievement.
@@ -588,7 +595,8 @@ Phase goal: {goal from ROADMAP.md}
Phase requirement IDs: {phase_req_ids}
Check must_haves against actual codebase.
Cross-reference requirement IDs from PLAN frontmatter against REQUIREMENTS.md — every ID MUST be accounted for.
Create VERIFICATION.md.",
Create VERIFICATION.md.
${VERIFIER_SKILLS}",
subagent_type="gsd-verifier",
model="{verifier_model}"
)

View File

@@ -33,6 +33,7 @@ Load codebase mapping context:
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init map-codebase)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_MAPPER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-codebase-mapper 2>/dev/null)
```
Extract from init JSON: `mapper_model`, `commit_docs`, `codebase_dir`, `existing_maps`, `has_maps`, `codebase_dir_exists`.
@@ -120,7 +121,8 @@ Write these documents to .planning/codebase/:
- STACK.md - Languages, runtime, frameworks, dependencies, configuration
- INTEGRATIONS.md - External APIs, databases, auth providers, webhooks
Explore thoroughly. Write documents directly using templates. Return confirmation only."
Explore thoroughly. Write documents directly using templates. Return confirmation only.
${AGENT_SKILLS_MAPPER}"
)
```
@@ -140,7 +142,8 @@ Write these documents to .planning/codebase/:
- ARCHITECTURE.md - Pattern, layers, data flow, abstractions, entry points
- STRUCTURE.md - Directory layout, key locations, naming conventions
Explore thoroughly. Write documents directly using templates. Return confirmation only."
Explore thoroughly. Write documents directly using templates. Return confirmation only.
${AGENT_SKILLS_MAPPER}"
)
```
@@ -160,7 +163,8 @@ Write these documents to .planning/codebase/:
- CONVENTIONS.md - Code style, naming, patterns, error handling
- TESTING.md - Framework, structure, mocking, coverage
Explore thoroughly. Write documents directly using templates. Return confirmation only."
Explore thoroughly. Write documents directly using templates. Return confirmation only.
${AGENT_SKILLS_MAPPER}"
)
```
@@ -179,7 +183,8 @@ Analyze this codebase for technical debt, known issues, and areas of concern.
Write this document to .planning/codebase/:
- CONCERNS.md - Tech debt, bugs, security, performance, fragile areas
Explore thoroughly. Write document directly using template. Return confirmation only."
Explore thoroughly. Write document directly using template. Return confirmation only.
${AGENT_SKILLS_MAPPER}"
)
```

View File

@@ -146,6 +146,9 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: start milesto
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init new-milestone)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_RESEARCHER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-project-researcher 2>/dev/null)
AGENT_SKILLS_SYNTHESIZER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-synthesizer 2>/dev/null)
AGENT_SKILLS_ROADMAPPER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-roadmapper 2>/dev/null)
```
Extract from init JSON: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `research_enabled`, `current_milestone`, `project_exists`, `roadmap_exists`, `latest_completed_milestone`, `phase_dir_count`, `phase_archive_path`.
@@ -222,6 +225,8 @@ Focus ONLY on what's needed for the NEW features.
- .planning/PROJECT.md (Project context)
</files_to_read>
${AGENT_SKILLS_RESEARCHER}
<downstream_consumer>{CONSUMER}</downstream_consumer>
<quality_gate>{GATES}</quality_gate>
@@ -256,6 +261,8 @@ Synthesize research outputs into SUMMARY.md.
- .planning/research/PITFALLS.md
</files_to_read>
${AGENT_SKILLS_SYNTHESIZER}
Write to: .planning/research/SUMMARY.md
Use template: ~/.claude/get-shit-done/templates/research-project/SUMMARY.md
Commit after writing.
@@ -370,6 +377,9 @@ Task(prompt="
- .planning/config.json
- .planning/MILESTONES.md
</files_to_read>
${AGENT_SKILLS_ROADMAPPER}
</planning_context>
<instructions>

View File

@@ -59,6 +59,9 @@ The document should describe what you want to build.
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init new-project)
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_RESEARCHER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-project-researcher 2>/dev/null)
AGENT_SKILLS_SYNTHESIZER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-synthesizer 2>/dev/null)
AGENT_SKILLS_ROADMAPPER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-roadmapper 2>/dev/null)
```
Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `project_exists`, `has_codebase_map`, `planning_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `needs_codebase_map`, `has_git`, `project_path`.
@@ -630,6 +633,8 @@ What's the standard 2025 stack for [domain]?
- {project_path} (Project context and goals)
</files_to_read>
${AGENT_SKILLS_RESEARCHER}
<downstream_consumer>
Your STACK.md feeds into roadmap creation. Be prescriptive:
- Specific libraries with versions
@@ -668,6 +673,8 @@ What features do [domain] products have? What's table stakes vs differentiating?
- {project_path} (Project context)
</files_to_read>
${AGENT_SKILLS_RESEARCHER}
<downstream_consumer>
Your FEATURES.md feeds into requirements definition. Categorize clearly:
- Table stakes (must have or users leave)
@@ -706,6 +713,8 @@ How are [domain] systems typically structured? What are major components?
- {project_path} (Project context)
</files_to_read>
${AGENT_SKILLS_RESEARCHER}
<downstream_consumer>
Your ARCHITECTURE.md informs phase structure in roadmap. Include:
- Component boundaries (what talks to what)
@@ -744,6 +753,8 @@ What do [domain] projects commonly get wrong? Critical mistakes?
- {project_path} (Project context)
</files_to_read>
${AGENT_SKILLS_RESEARCHER}
<downstream_consumer>
Your PITFALLS.md prevents mistakes in roadmap/planning. For each pitfall:
- Warning signs (how to detect early)
@@ -779,6 +790,8 @@ Synthesize research outputs into SUMMARY.md.
- .planning/research/PITFALLS.md
</files_to_read>
${AGENT_SKILLS_SYNTHESIZER}
<output>
Write to: .planning/research/SUMMARY.md
Use template: ~/.claude/get-shit-done/templates/research-project/SUMMARY.md
@@ -984,6 +997,8 @@ Task(prompt="
- .planning/config.json (Granularity and mode settings)
</files_to_read>
${AGENT_SKILLS_ROADMAPPER}
</planning_context>
<instructions>
@@ -1078,6 +1093,8 @@ Use AskUserQuestion:
- .planning/ROADMAP.md (Current roadmap to revise)
</files_to_read>
${AGENT_SKILLS_ROADMAPPER}
Update the roadmap based on feedback. Edit files in place.
Return ROADMAP REVISED with changes made.
</revision>

View File

@@ -24,6 +24,9 @@ Load all context in one call (paths only to minimize orchestrator context):
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init plan-phase "$PHASE")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_RESEARCHER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-researcher 2>/dev/null)
AGENT_SKILLS_PLANNER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-planner 2>/dev/null)
AGENT_SKILLS_CHECKER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-checker 2>/dev/null)
```
Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_enabled`, `plan_checker_enabled`, `nyquist_validation_enabled`, `commit_docs`, `text_mode`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_reviews`, `has_plans`, `plan_count`, `planning_exists`, `roadmap_exists`, `phase_req_ids`.
@@ -298,6 +301,8 @@ Answer: "What do I need to know to PLAN this phase well?"
- {state_path} (Project decisions and history)
</files_to_read>
${AGENT_SKILLS_RESEARCHER}
<additional_context>
**Phase description:** {phase_description}
**Phase requirement IDs (MUST address):** {phase_req_ids}
@@ -485,6 +490,8 @@ Planner prompt:
- {UI_SPEC_PATH} (UI Design Contract — visual/interaction specs, if exists)
</files_to_read>
${AGENT_SKILLS_PLANNER}
**Phase requirement IDs (every ID MUST appear in a plan's `requirements` field):** {phase_req_ids}
**Project instructions:** Read ./CLAUDE.md if exists — follow project-specific guidelines
@@ -582,6 +589,8 @@ Checker prompt:
- {research_path} (Technical Research — includes Validation Architecture)
</files_to_read>
${AGENT_SKILLS_CHECKER}
**Phase requirement IDs (MUST ALL be covered):** {phase_req_ids}
**Project instructions:** Read ./CLAUDE.md if exists — verify plans honor project guidelines
@@ -628,6 +637,8 @@ Revision prompt:
- {context_path} (USER DECISIONS from /gsd:discuss-phase)
</files_to_read>
${AGENT_SKILLS_PLANNER}
**Checker issues:** {structured_issues_from_checker}
</revision_context>

View File

@@ -118,6 +118,10 @@ If `$FULL_MODE` only:
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init quick "$DESCRIPTION")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_PLANNER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-planner 2>/dev/null)
AGENT_SKILLS_EXECUTOR=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-executor 2>/dev/null)
AGENT_SKILLS_CHECKER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-checker 2>/dev/null)
AGENT_SKILLS_VERIFIER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-verifier 2>/dev/null)
```
Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `branch_name`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`.
@@ -327,6 +331,8 @@ Task(
${DISCUSS_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-CONTEXT.md (User decisions — research should align with these)' : ''}
</files_to_read>
${AGENT_SKILLS_PLANNER}
</research_context>
<focus>
@@ -381,6 +387,8 @@ ${DISCUSS_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-CONTEXT.md (User decision
${RESEARCH_MODE ? '- ' + QUICK_DIR + '/' + quick_id + '-RESEARCH.md (Research findings — use to inform implementation choices)' : ''}
</files_to_read>
${AGENT_SKILLS_PLANNER}
**Project skills:** Check .claude/skills/ or .agents/skills/ directory (if either exists) — read SKILL.md files, plans should account for project skill rules
</planning_context>
@@ -438,6 +446,8 @@ Checker prompt:
- ${QUICK_DIR}/${quick_id}-PLAN.md (Plan to verify)
</files_to_read>
${AGENT_SKILLS_CHECKER}
**Scope:** This is a quick task, not a full phase. Skip checks that require a ROADMAP phase goal.
</verification_context>
@@ -490,6 +500,8 @@ Revision prompt:
- ${QUICK_DIR}/${quick_id}-PLAN.md (Existing plan)
</files_to_read>
${AGENT_SKILLS_PLANNER}
**Checker issues:** ${structured_issues_from_checker}
</revision_context>
@@ -536,6 +548,8 @@ Execute quick task ${quick_id}.
- .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation)
</files_to_read>
${AGENT_SKILLS_EXECUTOR}
<constraints>
- Execute all tasks in the plan
- Commit each task atomically
@@ -586,6 +600,8 @@ Task goal: ${DESCRIPTION}
- ${QUICK_DIR}/${quick_id}-PLAN.md (Plan)
</files_to_read>
${AGENT_SKILLS_VERIFIER}
Check must_haves against actual codebase. Create VERIFICATION.md at ${QUICK_DIR}/${quick_id}-VERIFICATION.md.",
subagent_type="gsd-verifier",
model="{verifier_model}",

View File

@@ -42,6 +42,7 @@ If exists: Offer update/view/skip options.
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
# Extract: phase_dir, padded_phase, phase_number, state_path, requirements_path, context_path
AGENT_SKILLS_RESEARCHER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-researcher 2>/dev/null)
```
## Step 4: Spawn Researcher
@@ -58,6 +59,8 @@ Research implementation approach for Phase {phase}: {name}
- {state_path} (Project decisions and history)
</files_to_read>
${AGENT_SKILLS_RESEARCHER}
<additional_context>
Phase description: {description}
</additional_context>

View File

@@ -21,6 +21,8 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init plan-phase "$PHASE")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_UI=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-ui-researcher 2>/dev/null)
AGENT_SKILLS_UI_CHECKER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-ui-checker 2>/dev/null)
```
Parse JSON for: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_context`, `has_research`, `commit_docs`.
@@ -122,6 +124,8 @@ Answer: "What visual and interaction contracts does this phase need?"
- {research_path} (Technical Research — stack decisions)
</files_to_read>
${AGENT_SKILLS_UI}
<output>
Write to: {phase_dir}/{padded_phase}-UI-SPEC.md
Template: ~/.claude/get-shit-done/templates/UI-SPEC.md
@@ -180,6 +184,8 @@ Check all 6 dimensions. Return APPROVED or BLOCKED.
- {research_path} (Technical Research — check stack alignment)
</files_to_read>
${AGENT_SKILLS_UI_CHECKER}
<config>
ui_safety_gate: {ui_safety_gate config value}
</config>

View File

@@ -18,6 +18,7 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_UI_REVIEWER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-ui-reviewer 2>/dev/null)
```
Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `commit_docs`.
@@ -85,6 +86,8 @@ Conduct 6-pillar visual audit of Phase {phase_number}: {phase_name}
- {context_path} (User decisions, if exists)
</files_to_read>
${AGENT_SKILLS_UI_REVIEWER}
<config>
phase_dir: {phase_dir}
padded_phase: {padded_phase}

View File

@@ -18,6 +18,7 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_AUDITOR=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-nyquist-auditor 2>/dev/null)
```
Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`.
@@ -95,7 +96,8 @@ Task(
"<files_to_read>{PLAN, SUMMARY, impl files, VALIDATION.md}</files_to_read>" +
"<gaps>{gap list}</gaps>" +
"<test_infrastructure>{framework, config, commands}</test_infrastructure>" +
"<constraints>Never modify impl files. Max 3 debug iterations. Escalate impl bugs.</constraints>",
"<constraints>Never modify impl files. Max 3 debug iterations. Escalate impl bugs.</constraints>" +
"${AGENT_SKILLS_AUDITOR}",
subagent_type="gsd-nyquist-auditor",
model="{AUDITOR_MODEL}",
description="Fill validation gaps for Phase {N}"

View File

@@ -32,6 +32,8 @@ If $ARGUMENTS contains a phase number, load context:
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init verify-work "${PHASE_ARG}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_PLANNER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-planner 2>/dev/null)
AGENT_SKILLS_CHECKER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-checker 2>/dev/null)
```
Parse JSON for: `planner_model`, `checker_model`, `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `has_verification`, `uat_path`.
@@ -431,6 +433,8 @@ Task(
- .planning/ROADMAP.md (Roadmap)
</files_to_read>
${AGENT_SKILLS_PLANNER}
</planning_context>
<downstream_consumer>
@@ -477,6 +481,8 @@ Task(
- {phase_dir}/*-PLAN.md (Plans to verify)
</files_to_read>
${AGENT_SKILLS_CHECKER}
</verification_context>
<expected_output>
@@ -517,6 +523,8 @@ Task(
- {phase_dir}/*-PLAN.md (Existing plans)
</files_to_read>
${AGENT_SKILLS_PLANNER}
**Checker issues:**
{structured_issues_from_checker}

207
tests/agent-skills.test.cjs Normal file
View File

@@ -0,0 +1,207 @@
/**
* GSD Tools Tests - Agent Skills Injection
*
* CLI integration tests for the `agent-skills` command that reads
* `agent_skills` from .planning/config.json and returns a formatted
* skills block for injection into Task() prompts.
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
// ─── helpers ──────────────────────────────────────────────────────────────────
function writeConfig(tmpDir, obj) {
const configPath = path.join(tmpDir, '.planning', 'config.json');
fs.writeFileSync(configPath, JSON.stringify(obj, null, 2), 'utf-8');
}
function readConfig(tmpDir) {
const configPath = path.join(tmpDir, '.planning', 'config.json');
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
// ─── agent-skills command ────────────────────────────────────────────────────
describe('agent-skills command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('returns empty when no config exists', () => {
// No config.json at all
const result = runGsdTools(['agent-skills', 'gsd-executor'], tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
// Should succeed with empty output (no skills configured)
assert.strictEqual(result.output, '');
});
test('returns empty when config has no agent_skills section', () => {
writeConfig(tmpDir, { model_profile: 'balanced' });
const result = runGsdTools(['agent-skills', 'gsd-executor'], tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.strictEqual(result.output, '');
});
test('returns empty for unconfigured agent type', () => {
writeConfig(tmpDir, {
agent_skills: {
'gsd-executor': ['skills/test-skill'],
},
});
const result = runGsdTools(['agent-skills', 'gsd-planner'], tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.strictEqual(result.output, '');
});
test('returns formatted block for configured agent with array of paths', () => {
// Create the skill directories with SKILL.md files
const skillDir = path.join(tmpDir, 'skills', 'test-skill');
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# Test Skill\n');
writeConfig(tmpDir, {
agent_skills: {
'gsd-executor': ['skills/test-skill'],
},
});
const result = runGsdTools(['agent-skills', 'gsd-executor'], tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
assert.ok(result.output.includes('<agent_skills>'), 'Should contain <agent_skills> tag');
assert.ok(result.output.includes('</agent_skills>'), 'Should contain closing tag');
assert.ok(result.output.includes('skills/test-skill/SKILL.md'), 'Should contain skill path');
});
test('returns formatted block for configured agent with single string path', () => {
const skillDir = path.join(tmpDir, 'skills', 'my-skill');
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# My Skill\n');
writeConfig(tmpDir, {
agent_skills: {
'gsd-executor': 'skills/my-skill',
},
});
const result = runGsdTools(['agent-skills', 'gsd-executor'], tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
assert.ok(result.output.includes('skills/my-skill/SKILL.md'), 'Should contain skill path');
});
test('handles multiple skill paths', () => {
const skill1 = path.join(tmpDir, 'skills', 'skill-a');
const skill2 = path.join(tmpDir, 'skills', 'skill-b');
fs.mkdirSync(skill1, { recursive: true });
fs.mkdirSync(skill2, { recursive: true });
fs.writeFileSync(path.join(skill1, 'SKILL.md'), '# Skill A\n');
fs.writeFileSync(path.join(skill2, 'SKILL.md'), '# Skill B\n');
writeConfig(tmpDir, {
agent_skills: {
'gsd-executor': ['skills/skill-a', 'skills/skill-b'],
},
});
const result = runGsdTools(['agent-skills', 'gsd-executor'], tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
assert.ok(result.output.includes('skills/skill-a/SKILL.md'), 'Should contain first skill');
assert.ok(result.output.includes('skills/skill-b/SKILL.md'), 'Should contain second skill');
});
test('warns for nonexistent skill path but does not error', () => {
writeConfig(tmpDir, {
agent_skills: {
'gsd-executor': ['skills/nonexistent'],
},
});
const result = runGsdTools(['agent-skills', 'gsd-executor'], tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
// Should not crash — returns empty output (the missing skill is skipped)
assert.ok(result.success, 'Command should succeed even with missing skill paths');
// Should not include the missing skill in the output
assert.ok(!result.output.includes('skills/nonexistent/SKILL.md'),
'Should not include nonexistent skill in output');
});
test('validates path safety — rejects traversal attempts', () => {
writeConfig(tmpDir, {
agent_skills: {
'gsd-executor': ['../../../etc/passwd'],
},
});
const result = runGsdTools(['agent-skills', 'gsd-executor'], tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
// Should not include traversal path in output
assert.ok(!result.output.includes('/etc/passwd'), 'Should not include traversal path');
});
test('returns empty when no agent type argument provided', () => {
const result = runGsdTools(['agent-skills'], tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
// Should succeed with empty output — no agent type means no skills to return
assert.ok(result.success, 'Command should succeed');
const parsed = JSON.parse(result.output);
assert.strictEqual(parsed, '', 'Should return empty string');
});
});
// ─── config-ensure-section includes agent_skills ────────────────────────────
describe('config-ensure-section with agent_skills', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('new configs include agent_skills key', () => {
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.ok('agent_skills' in config, 'config should have agent_skills key');
assert.deepStrictEqual(config.agent_skills, {}, 'agent_skills should default to empty object');
});
});
// ─── config-set agent_skills ─────────────────────────────────────────────────
describe('config-set agent_skills', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
// Ensure config exists first
runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
});
afterEach(() => {
cleanup(tmpDir);
});
test('can set agent_skills via dot notation', () => {
const result = runGsdTools(
['config-set', 'agent_skills.gsd-executor', '["skills/my-skill"]'],
tmpDir,
{ HOME: tmpDir, USERPROFILE: tmpDir }
);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.deepStrictEqual(
config.agent_skills['gsd-executor'],
['skills/my-skill'],
'Should store array of skill paths'
);
});
});