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' + ); + }); +});