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>
This commit is contained in:
@@ -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(")");
|
||||
}
|
||||
sb.append("\n");
|
||||
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(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>
|
||||
* 通过名称调用已加载的 Skill。Skill 会作为 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user