From db3eeb8fe4145c6ef46ce6dc5e49c6c68acc70a5 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 24 Mar 2026 16:26:13 -0400 Subject: [PATCH 1/2] feat: agent skill injection via config (#1355) Add agent_skills config section that maps agent types to skill directory paths. At spawn time, workflows load configured skills and inject them as blocks in Task() prompts, giving subagents access to project-specific skill files. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/CONFIGURATION.md | 69 +++++- get-shit-done/bin/gsd-tools.cjs | 5 + get-shit-done/bin/lib/config.cjs | 27 ++- get-shit-done/bin/lib/core.cjs | 1 + get-shit-done/bin/lib/init.cjs | 73 ++++++ get-shit-done/templates/config.json | 3 +- get-shit-done/workflows/audit-milestone.md | 4 +- get-shit-done/workflows/diagnose-issues.md | 8 +- .../workflows/discuss-phase-assumptions.md | 3 + get-shit-done/workflows/discuss-phase.md | 4 +- get-shit-done/workflows/execute-phase.md | 10 +- get-shit-done/workflows/map-codebase.md | 13 +- get-shit-done/workflows/new-milestone.md | 10 + get-shit-done/workflows/new-project.md | 17 ++ get-shit-done/workflows/plan-phase.md | 11 + get-shit-done/workflows/quick.md | 16 ++ get-shit-done/workflows/research-phase.md | 3 + get-shit-done/workflows/ui-phase.md | 6 + get-shit-done/workflows/ui-review.md | 3 + get-shit-done/workflows/validate-phase.md | 4 +- get-shit-done/workflows/verify-work.md | 8 + tests/agent-skills.test.cjs | 207 ++++++++++++++++++ 22 files changed, 491 insertions(+), 14 deletions(-) create mode 100644 tests/agent-skills.test.cjs diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index ad5d5ca0a..5c4491d74 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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 ` to load configured skills. If skills exist for the agent type, they are injected as an `` block in the Task() prompt: + +```xml + +Read these user-configured skills: +- @skills/testing-standards/SKILL.md +- @skills/api-conventions/SKILL.md + +``` + +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 | diff --git a/get-shit-done/bin/gsd-tools.cjs b/get-shit-done/bin/gsd-tools.cjs index 51e340cea..1214a8260 100755 --- a/get-shit-done/bin/gsd-tools.cjs +++ b/get-shit-done/bin/gsd-tools.cjs @@ -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; diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index 9d5327917..67fe5a6b9 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -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.` where the sub-key is freeform. + */ +function isValidConfigKey(keyPath) { + if (VALID_CONFIG_KEYS.has(keyPath)) return true; + // Allow agent_skills. 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.`); } - // 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}`); diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index b40db3d86..e1b2262f7 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -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; diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index fd984a64a..005cb69d3 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -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 `` 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 `\nRead these user-configured skills:\n${lines}\n`; +} + +/** + * 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, }; diff --git a/get-shit-done/templates/config.json b/get-shit-done/templates/config.json index 31ed8dea7..fd147b6f4 100644 --- a/get-shit-done/templates/config.json +++ b/get-shit-done/templates/config.json @@ -39,5 +39,6 @@ }, "hooks": { "context_warnings": true - } + }, + "agent_skills": {} } diff --git a/get-shit-done/workflows/audit-milestone.md b/get-shit-done/workflows/audit-milestone.md index 634c437f6..05b9974c6 100644 --- a/get-shit-done/workflows/audit-milestone.md +++ b/get-shit-done/workflows/audit-milestone.md @@ -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}" ) diff --git a/get-shit-done/workflows/diagnose-issues.md b/get-shit-done/workflows/diagnose-issues.md index be5c438a3..ea53fe26e 100644 --- a/get-shit-done/workflows/diagnose-issues.md +++ b/get-shit-done/workflows/diagnose-issues.md @@ -78,13 +78,19 @@ This runs in parallel - all gaps investigated simultaneously. +**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\n- {phase_dir}/{phase_num}-UAT.md\n- .planning/STATE.md\n", + prompt=filled_debug_subagent_prompt + "\n\n\n- {phase_dir}/{phase_num}-UAT.md\n- .planning/STATE.md\n\n${AGENT_SKILLS_DEBUGGER}", subagent_type="gsd-debugger", isolation="worktree", description="Debug: {truth_short}" diff --git a/get-shit-done/workflows/discuss-phase-assumptions.md b/get-shit-done/workflows/discuss-phase-assumptions.md index 22af28dc5..3073807a9 100644 --- a/get-shit-done/workflows/discuss-phase-assumptions.md +++ b/get-shit-done/workflows/discuss-phase-assumptions.md @@ -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} """) ``` diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md index da17dc700..1d268b1ba 100644 --- a/get-shit-done/workflows/discuss-phase.md +++ b/get-shit-done/workflows/discuss-phase.md @@ -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 name and brief description from PROJECT.md} {resolved calibration tier: full_maturity | standard | minimal_decisive} - 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}" diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index f12596849..0bdb11174 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -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) + ${AGENT_SKILLS} + 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. 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}" ) diff --git a/get-shit-done/workflows/map-codebase.md b/get-shit-done/workflows/map-codebase.md index f67527a7c..270ad090a 100644 --- a/get-shit-done/workflows/map-codebase.md +++ b/get-shit-done/workflows/map-codebase.md @@ -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}" ) ``` diff --git a/get-shit-done/workflows/new-milestone.md b/get-shit-done/workflows/new-milestone.md index 7a8e6fff5..6427747c4 100644 --- a/get-shit-done/workflows/new-milestone.md +++ b/get-shit-done/workflows/new-milestone.md @@ -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) +${AGENT_SKILLS_RESEARCHER} + {CONSUMER} {GATES} @@ -256,6 +261,8 @@ Synthesize research outputs into SUMMARY.md. - .planning/research/PITFALLS.md +${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 + +${AGENT_SKILLS_ROADMAPPER} + diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index fd27f93c0..a5f50f8b9 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -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) +${AGENT_SKILLS_RESEARCHER} + 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) +${AGENT_SKILLS_RESEARCHER} + 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) +${AGENT_SKILLS_RESEARCHER} + 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) +${AGENT_SKILLS_RESEARCHER} + 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 +${AGENT_SKILLS_SYNTHESIZER} + 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) +${AGENT_SKILLS_ROADMAPPER} + @@ -1078,6 +1093,8 @@ Use AskUserQuestion: - .planning/ROADMAP.md (Current roadmap to revise) + ${AGENT_SKILLS_ROADMAPPER} + Update the roadmap based on feedback. Edit files in place. Return ROADMAP REVISED with changes made. diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index dfc9eb1be..b13dc0343 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -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) +${AGENT_SKILLS_RESEARCHER} + **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) +${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) +${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) +${AGENT_SKILLS_PLANNER} + **Checker issues:** {structured_issues_from_checker} diff --git a/get-shit-done/workflows/quick.md b/get-shit-done/workflows/quick.md index b2a3e9392..89441f965 100644 --- a/get-shit-done/workflows/quick.md +++ b/get-shit-done/workflows/quick.md @@ -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)' : ''} +${AGENT_SKILLS_PLANNER} + @@ -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)' : ''} +${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 @@ -438,6 +446,8 @@ Checker prompt: - ${QUICK_DIR}/${quick_id}-PLAN.md (Plan to verify) +${AGENT_SKILLS_CHECKER} + **Scope:** This is a quick task, not a full phase. Skip checks that require a ROADMAP phase goal. @@ -490,6 +500,8 @@ Revision prompt: - ${QUICK_DIR}/${quick_id}-PLAN.md (Existing plan) +${AGENT_SKILLS_PLANNER} + **Checker issues:** ${structured_issues_from_checker} @@ -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) +${AGENT_SKILLS_EXECUTOR} + - Execute all tasks in the plan - Commit each task atomically @@ -586,6 +600,8 @@ Task goal: ${DESCRIPTION} - ${QUICK_DIR}/${quick_id}-PLAN.md (Plan) +${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}", diff --git a/get-shit-done/workflows/research-phase.md b/get-shit-done/workflows/research-phase.md index 85ffeb9b9..32eb39b24 100644 --- a/get-shit-done/workflows/research-phase.md +++ b/get-shit-done/workflows/research-phase.md @@ -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) +${AGENT_SKILLS_RESEARCHER} + Phase description: {description} diff --git a/get-shit-done/workflows/ui-phase.md b/get-shit-done/workflows/ui-phase.md index 0480d6474..0463ad08a 100644 --- a/get-shit-done/workflows/ui-phase.md +++ b/get-shit-done/workflows/ui-phase.md @@ -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) +${AGENT_SKILLS_UI} + 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) +${AGENT_SKILLS_UI_CHECKER} + ui_safety_gate: {ui_safety_gate config value} diff --git a/get-shit-done/workflows/ui-review.md b/get-shit-done/workflows/ui-review.md index cf6d870d3..2d23f166f 100644 --- a/get-shit-done/workflows/ui-review.md +++ b/get-shit-done/workflows/ui-review.md @@ -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) +${AGENT_SKILLS_UI_REVIEWER} + phase_dir: {phase_dir} padded_phase: {padded_phase} diff --git a/get-shit-done/workflows/validate-phase.md b/get-shit-done/workflows/validate-phase.md index 08849ff14..751e92974 100644 --- a/get-shit-done/workflows/validate-phase.md +++ b/get-shit-done/workflows/validate-phase.md @@ -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( "{PLAN, SUMMARY, impl files, VALIDATION.md}" + "{gap list}" + "{framework, config, commands}" + - "Never modify impl files. Max 3 debug iterations. Escalate impl bugs.", + "Never modify impl files. Max 3 debug iterations. Escalate impl bugs." + + "${AGENT_SKILLS_AUDITOR}", subagent_type="gsd-nyquist-auditor", model="{AUDITOR_MODEL}", description="Fill validation gaps for Phase {N}" diff --git a/get-shit-done/workflows/verify-work.md b/get-shit-done/workflows/verify-work.md index a0cd085c2..c21dc9ea3 100644 --- a/get-shit-done/workflows/verify-work.md +++ b/get-shit-done/workflows/verify-work.md @@ -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) +${AGENT_SKILLS_PLANNER} + @@ -477,6 +481,8 @@ Task( - {phase_dir}/*-PLAN.md (Plans to verify) +${AGENT_SKILLS_CHECKER} + @@ -517,6 +523,8 @@ Task( - {phase_dir}/*-PLAN.md (Existing plans) +${AGENT_SKILLS_PLANNER} + **Checker issues:** {structured_issues_from_checker} diff --git a/tests/agent-skills.test.cjs b/tests/agent-skills.test.cjs new file mode 100644 index 000000000..c4438222b --- /dev/null +++ b/tests/agent-skills.test.cjs @@ -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(''), 'Should contain tag'); + assert.ok(result.output.includes(''), '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' + ); + }); +}); From 0a26f815da6d92c62fc88489defd1c1b94679037 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 24 Mar 2026 16:36:32 -0400 Subject: [PATCH 2/2] ci: add .secretscanignore for plan-phase.md false positive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plan-phase.md contains illustrative DATABASE_URL/REDIS_URL examples in documentation text, not real credentials. The secret-scan.sh script already supports .secretscanignore — this file activates it. Co-Authored-By: Claude Opus 4.6 (1M context) --- .secretscanignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.secretscanignore b/.secretscanignore index 878c795ad..681532e13 100644 --- a/.secretscanignore +++ b/.secretscanignore @@ -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