feat: Skills execution system (Phase 2B)

- SkillTool: execute skills by name via forked sub-agent
- BundledSkills: 4 built-in skills (verify, debug, review, commit)
- SkillLoader: bundled skills auto-loaded + budget-aware prompt listing
- /skills command: detail view with /skills <name>
- Budget management: 8K char budget, 250 char per-entry cap

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pull/1/head
abel533 1 month ago
parent cb76b267f1
commit 6088678c4f
  1. 53
      src/main/java/com/claudecode/command/impl/SkillsCommand.java
  2. 8
      src/main/java/com/claudecode/config/AppConfig.java
  3. 200
      src/main/java/com/claudecode/context/BundledSkills.java
  4. 47
      src/main/java/com/claudecode/context/SkillLoader.java
  5. 198
      src/main/java/com/claudecode/tool/impl/SkillTool.java

@ -31,6 +31,24 @@ public class SkillsCommand implements SlashCommand {
SkillLoader loader = new SkillLoader(projectDir);
List<SkillLoader.Skill> skills = loader.loadAll();
// If args provided, show specific skill detail
if (args != null && !args.isBlank()) {
String query = args.strip().toLowerCase();
var match = skills.stream()
.filter(s -> s.name().equalsIgnoreCase(query))
.findFirst();
if (match.isEmpty()) {
match = skills.stream()
.filter(s -> s.name().toLowerCase().contains(query))
.findFirst();
}
if (match.isPresent()) {
return formatSkillDetail(match.get());
}
return AnsiStyle.red(" Skill not found: " + args.strip()) + "\n"
+ AnsiStyle.dim(" Use /skills to list all available skills.");
}
StringBuilder sb = new StringBuilder();
sb.append("\n");
sb.append(AnsiStyle.bold(" 🎯 Available Skills\n"));
@ -61,12 +79,43 @@ public class SkillsCommand implements SlashCommand {
if (!skill.whenToUse().isEmpty()) {
sb.append(" ").append(AnsiStyle.dim("When: " + skill.whenToUse())).append("\n");
}
sb.append(" ").append(AnsiStyle.dim("File: " + skill.filePath())).append("\n");
sb.append("\n");
}
sb.append(AnsiStyle.dim(" Total " + skills.size() + " skills\n"));
sb.append(AnsiStyle.dim(" Total " + skills.size() + " skills"));
sb.append(AnsiStyle.dim(" • Use /skills <name> for details\n"));
}
return sb.toString();
}
private String formatSkillDetail(SkillLoader.Skill skill) {
StringBuilder sb = new StringBuilder();
sb.append("\n");
sb.append(AnsiStyle.bold(" 🎯 Skill: " + skill.name())).append("\n");
sb.append(" ").append("─".repeat(50)).append("\n\n");
sb.append(" ").append(AnsiStyle.bold("Source: ")).append(skill.source()).append("\n");
if (!skill.description().isEmpty()) {
sb.append(" ").append(AnsiStyle.bold("Description: ")).append(skill.description()).append("\n");
}
if (!skill.whenToUse().isEmpty()) {
sb.append(" ").append(AnsiStyle.bold("When to use: ")).append(skill.whenToUse()).append("\n");
}
sb.append(" ").append(AnsiStyle.bold("File: ")).append(skill.filePath()).append("\n");
sb.append("\n");
// Show content preview
String content = skill.content();
if (content.length() > 500) {
content = content.substring(0, 497) + "...";
}
sb.append(AnsiStyle.dim(" Content:\n"));
for (String line : content.lines().toList()) {
sb.append(AnsiStyle.dim(" │ " + line)).append("\n");
}
sb.append("\n");
sb.append(AnsiStyle.dim(" Tip: Ask AI to execute this skill or type: /verify, /debug, etc.\n"));
return sb.toString();
}
}

@ -113,7 +113,8 @@ public class AppConfig {
new SleepTool(),
new ToolSearchTool(),
new EnterPlanModeTool(),
new ExitPlanModeTool()
new ExitPlanModeTool(),
new SkillTool()
);
// P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具)
@ -238,7 +239,7 @@ public class AppConfig {
}
@Bean
public String systemPrompt() {
public String systemPrompt(ToolContext toolContext) {
Path projectDir = Path.of(System.getProperty("user.dir"));
ClaudeMdLoader claudeLoader = new ClaudeMdLoader(projectDir);
@ -248,6 +249,9 @@ public class AppConfig {
skillLoader.loadAll();
String skillsSummary = skillLoader.buildSkillsSummary();
// Inject SkillLoader into ToolContext for SkillTool
toolContext.set(SkillTool.SKILL_LOADER_KEY, skillLoader);
GitContext gitContext = new GitContext(projectDir).collect();
String gitSummary = gitContext.buildSummary();

@ -0,0 +1,200 @@
package com.claudecode.context;
import com.claudecode.context.SkillLoader.Skill;
import java.nio.file.Path;
import java.util.List;
/**
* 内置 Skills 注册 对应 claude-code/src/skills/bundled/ 目录
* <p>
* 提供默认可用的 Skills无需用户手动创建 .md 文件
*/
public final class BundledSkills {
private BundledSkills() {}
/**
* 获取所有内置 Skills
*/
public static List<Skill> getAll() {
return List.of(
verifySkill(),
debugSkill(),
reviewSkill(),
commitSkill()
);
}
/**
* verify 代码验证 Skill
* 运行项目测试lint类型检查确保代码变更正确
*/
static Skill verifySkill() {
return new Skill(
"verify",
"Run tests and checks to verify code changes are correct",
"After making code changes that need verification",
"""
# Verify Changes
You are verifying that recent code changes are correct and don't break anything.
## Steps
1. **Identify the project type** by reading build/config files:
- Look for: package.json, pom.xml, build.gradle, Cargo.toml, go.mod, etc.
2. **Run the appropriate test command**:
- Node.js: `npm test` or `npx jest` or `npx vitest`
- Java/Maven: `mvn test -q`
- Java/Gradle: `./gradlew test`
- Python: `pytest` or `python -m pytest`
- Rust: `cargo test`
- Go: `go test ./...`
3. **Run linting if available**:
- Node.js: `npm run lint` or `npx eslint .`
- Python: `ruff check .` or `flake8`
- Java: `mvn checkstyle:check -q`
4. **Run type checking if available**:
- TypeScript: `npx tsc --noEmit`
- Python: `mypy .`
5. **Report results**:
- All checks passed
- Failures found (include specific errors)
- Warnings (include details)
If a check fails, analyze the error and suggest a fix.
""",
"bundled",
Path.of("bundled://verify")
);
}
/**
* debug 调试辅助 Skill
* 分析错误信息定位问题根因
*/
static Skill debugSkill() {
return new Skill(
"debug",
"Analyze errors and help debug issues",
"When encountering an error or unexpected behavior",
"""
# Debug Issue
You are helping debug an issue. Follow this systematic approach:
## Steps
1. **Understand the error**:
- Read the error message carefully
- Identify the error type (compile, runtime, logic, etc.)
- Note the file and line number if available
2. **Reproduce the issue**:
- Try to reproduce the error with the minimal command
- Check if the error is consistent
3. **Trace the root cause**:
- Read the file(s) mentioned in the error
- Check imports, dependencies, and configuration
- Look for recent changes that might have caused it
- Use Grep to find related code patterns
4. **Analyze and diagnose**:
- Identify the root cause (not just symptoms)
- Consider edge cases and dependencies
- Check if similar patterns exist elsewhere
5. **Suggest fix**:
- Propose a specific, minimal fix
- Explain why the fix works
- Note any side effects or risks
If the user provides an error message, start from Step 1.
If the user describes unexpected behavior, start from Step 2.
""",
"bundled",
Path.of("bundled://debug")
);
}
/**
* review 代码审查 Skill
*/
static Skill reviewSkill() {
return new Skill(
"review",
"Review code changes for quality, bugs, and best practices",
"When the user wants feedback on code changes",
"""
# Code Review
You are reviewing code changes. Focus on:
## Checklist
1. **Correctness**: Does the code do what it's supposed to?
2. **Edge cases**: Are boundary conditions handled?
3. **Error handling**: Are errors caught and handled gracefully?
4. **Security**: Any injection, auth, or data exposure risks?
5. **Performance**: Any obvious performance issues?
6. **Readability**: Is the code clear and well-structured?
7. **Tests**: Are there tests? Do they cover key cases?
## Process
1. Run `git diff` to see what changed
2. Read each changed file
3. For each issue found, note:
- Severity: 🔴 Critical / 🟡 Warning / 🔵 Suggestion
- File and line number
- What's wrong and how to fix it
4. Summarize findings
""",
"bundled",
Path.of("bundled://review")
);
}
/**
* commit 提交 Skill
*/
static Skill commitSkill() {
return new Skill(
"commit",
"Create a well-structured git commit with conventional commit message",
"When the user wants to commit changes",
"""
# Git Commit
Create a well-structured git commit.
## Steps
1. Run `git status` and `git diff --stat` to see changes
2. Analyze the changes to determine:
- Type: feat, fix, refactor, docs, test, chore, etc.
- Scope: which module/area is affected
- Breaking changes
3. Generate commit message in Conventional Commits format:
```
type(scope): brief description
Detailed body explaining what and why (not how).
Breaking changes (if any).
```
4. Stage relevant files with `git add`
5. Create the commit with `git commit -m "..."`
6. Report the commit hash
""",
"bundled",
Path.of("bundled://commit")
);
}
}

@ -46,6 +46,10 @@ public class SkillLoader {
public List<Skill> loadAll() {
skills.clear();
// 0. 内置技能
skills.addAll(BundledSkills.getAll());
log.debug("Loaded {} bundled skills", BundledSkills.getAll().size());
// 1. 用户级技能
Path userSkillsDir = Path.of(System.getProperty("user.home"), ".claude", "skills");
loadFromDirectory(userSkillsDir, "user");
@ -147,25 +151,58 @@ public class SkillLoader {
}
/**
* 构建技能上下文摘要注入系统提示词
* 构建技能上下文摘要注入系统提示词
* 支持预算控制确保不超过上下文窗口的 1%
*/
public String buildSkillsSummary() {
return buildSkillsSummary(8000); // Default 8K chars budget
}
/**
* 构建技能上下文摘要带预算控制
* 对应 TS formatCommandsWithinBudget()
*
* @param charBudget 最大字符预算
*/
public String buildSkillsSummary(int charBudget) {
if (skills.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
sb.append("# Available Skills\n\n");
sb.append("Skills can be invoked by name using the Skill tool or by typing /<name>.\n\n");
int budgetUsed = sb.length();
int perEntryMax = 250; // Per-entry cap for cache efficiency
for (Skill skill : skills) {
sb.append("- **").append(skill.name()).append("**");
StringBuilder entry = new StringBuilder();
entry.append("- **").append(skill.name()).append("**");
if (!skill.description().isEmpty()) {
sb.append(": ").append(skill.description());
String desc = skill.description();
if (desc.length() > perEntryMax - skill.name().length() - 10) {
desc = desc.substring(0, perEntryMax - skill.name().length() - 13) + "...";
}
entry.append(": ").append(desc);
}
if (!skill.whenToUse().isEmpty()) {
sb.append(" (use when: ").append(skill.whenToUse()).append(")");
entry.append(" (use when: ").append(skill.whenToUse()).append(")");
}
entry.append("\n");
// Check budget
if (budgetUsed + entry.length() > charBudget) {
// Add truncation notice
sb.append("- ... and ").append(skills.size() - skills.indexOf(skill))
.append(" more skills (use /skills to see all)\n");
break;
}
sb.append("\n");
sb.append(entry);
budgetUsed += entry.length();
}
return sb.toString();
}

@ -0,0 +1,198 @@
package com.claudecode.tool.impl;
import com.claudecode.context.SkillLoader;
import com.claudecode.context.SkillLoader.Skill;
import com.claudecode.tool.Tool;
import com.claudecode.tool.ToolContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Skill 执行工具 对应 claude-code/src/tools/SkillTool/SkillTool.ts
* <p>
* 通过名称调用已加载的 SkillSkill 会作为 forked sub-agent 执行
* 注入 Skill markdown 内容作为上下文指导
* <p>
* Skills 来源
* <ul>
* <li>用户级: ~/.claude/skills/</li>
* <li>项目级: ./.claude/skills/</li>
* <li>命令目录: ./.claude/commands/</li>
* <li>内置 Skills: verify, debug </li>
* </ul>
*/
public class SkillTool implements Tool {
private static final Logger log = LoggerFactory.getLogger(SkillTool.class);
/** ToolContext key for SkillLoader */
public static final String SKILL_LOADER_KEY = "SKILL_LOADER";
@Override
public String name() {
return "Skill";
}
@Override
public String description() {
return """
Execute a registered skill by name. Skills are reusable, structured workflows \
defined in markdown files that guide a specialized sub-agent.
When to use:
- User invokes a skill by name (e.g., /verify, /debug)
- You identify a task that matches a registered skill's "whenToUse" criteria
- Complex workflows that benefit from structured guidance
Available skills are listed in the system prompt under "Available Skills".
The skill runs as an isolated sub-agent with its own context. Provide any relevant \
arguments or context from the current conversation.
""";
}
@Override
public String inputSchema() {
return """
{
"type": "object",
"properties": {
"skill_name": {
"type": "string",
"description": "Name of the skill to execute (case-insensitive)"
},
"arguments": {
"type": "string",
"description": "Arguments or context to pass to the skill"
}
},
"required": ["skill_name"]
}
""";
}
@Override
public String execute(Map<String, Object> input, ToolContext context) {
String skillName = (String) input.get("skill_name");
String arguments = (String) input.getOrDefault("arguments", "");
if (skillName == null || skillName.isBlank()) {
return "Error: 'skill_name' is required";
}
// Get SkillLoader from context
SkillLoader skillLoader = context.get(SKILL_LOADER_KEY);
if (skillLoader == null) {
return "Error: SkillLoader not configured. No skills available.";
}
// Find skill by name
Optional<Skill> skillOpt = skillLoader.findByName(skillName);
if (skillOpt.isEmpty()) {
// Try partial match
skillOpt = findByPartialName(skillLoader.getSkills(), skillName);
}
if (skillOpt.isEmpty()) {
StringBuilder msg = new StringBuilder();
msg.append("Skill '").append(skillName).append("' not found.\n\n");
msg.append("Available skills:\n");
for (Skill s : skillLoader.getSkills()) {
msg.append(" - ").append(s.name());
if (!s.description().isEmpty()) {
msg.append(": ").append(s.description());
}
msg.append("\n");
}
return msg.toString();
}
Skill skill = skillOpt.get();
log.info("Executing skill: {} [{}]", skill.name(), skill.source());
// Build skill execution prompt
String skillPrompt = buildSkillPrompt(skill, arguments);
// Execute via agent factory (same as AgentTool)
@SuppressWarnings("unchecked")
java.util.function.Function<String, String> agentFactory =
context.getOrDefault(AgentTool.AGENT_FACTORY_KEY, null);
if (agentFactory == null) {
// Fallback: return skill content for manual execution guidance
return "⚠ Sub-agent not available. Skill content for manual execution:\n\n"
+ "# Skill: " + skill.name() + "\n"
+ skill.content();
}
try {
String result = agentFactory.apply(skillPrompt);
log.info("Skill '{}' completed, result: {} chars", skill.name(), result.length());
return result;
} catch (Exception e) {
log.debug("Skill execution failed", e);
return "Error executing skill '" + skill.name() + "': " + e.getMessage();
}
}
@Override
public boolean isReadOnly() {
return false; // Skills may modify files
}
@Override
public String activityDescription(Map<String, Object> input) {
String name = input != null ? (String) input.get("skill_name") : null;
return name != null ? "Running skill: " + name + "..." : "Running skill...";
}
/**
* Build the full prompt for skill execution.
*/
private String buildSkillPrompt(Skill skill, String arguments) {
StringBuilder sb = new StringBuilder();
sb.append("You are executing a skill: ").append(skill.name()).append("\n\n");
if (!skill.description().isEmpty()) {
sb.append("Description: ").append(skill.description()).append("\n");
}
if (!skill.whenToUse().isEmpty()) {
sb.append("When to use: ").append(skill.whenToUse()).append("\n");
}
sb.append("\n");
// Inject skill content as instructions
sb.append("## Skill Instructions\n\n");
sb.append(skill.content()).append("\n\n");
// Inject arguments
if (arguments != null && !arguments.isBlank()) {
sb.append("## User Arguments\n\n");
sb.append(arguments).append("\n\n");
}
sb.append("""
## Execution Guidelines
- Follow the skill instructions above carefully
- Use the available tools to complete the task
- Report results concisely when done
- If the skill requires user input, use AskUserQuestion
""");
return sb.toString();
}
/**
* Partial name match for skills.
*/
private Optional<Skill> findByPartialName(List<Skill> skills, String name) {
String lower = name.toLowerCase();
return skills.stream()
.filter(s -> s.name().toLowerCase().contains(lower))
.findFirst();
}
}
Loading…
Cancel
Save