mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-13 03:31:49 +08:00
feat(skills): add per-task skill spec auto-detection and injection
Replace external inject-spec.py hook with built-in zero-config skill detection in codeagent-wrapper. The system auto-detects project type from fingerprint files (go.mod, package.json, etc.), maps to installed skills, and injects SKILL.md content directly into sub-agent prompts. Key changes: - Add DetectProjectSkills/ResolveSkillContent in executor/prompt.go - Add Skills field to TaskSpec with parallel config parsing - Add --skills CLI flag for explicit override - Update /do SKILL.md Phase 4 with per-task skill examples - Remove on-stop.py global hook (not needed) - Replace inject-spec.py with no-op (detection now internal) - Add 20 unit tests covering detection, resolution, budget, security Security: path traversal protection via validSkillName regex, 16K char budget with tag overhead accounting, CRLF normalization. Generated with SWE-Agent.ai Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
This commit is contained in:
@@ -1,57 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Initialize do skill workflow - wrapper around task.py.
|
||||
|
||||
Creates a task directory under .claude/do-tasks/ with:
|
||||
- task.md: Task metadata (YAML frontmatter) + requirements (Markdown body)
|
||||
|
||||
If --worktree is specified, also creates a git worktree for isolated development.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
PHASE_NAMES = {
|
||||
1: "Understand",
|
||||
2: "Clarify",
|
||||
3: "Design",
|
||||
4: "Implement",
|
||||
5: "Complete",
|
||||
}
|
||||
from task import create_task, PHASE_NAMES
|
||||
|
||||
def phase_name_for(n: int) -> str:
|
||||
return PHASE_NAMES.get(n, f"Phase {n}")
|
||||
|
||||
def die(msg: str):
|
||||
print(f"❌ {msg}", file=sys.stderr)
|
||||
print(f"Error: {msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def create_worktree(project_dir: str, task_id: str) -> str:
|
||||
"""Create a git worktree for the task. Returns the worktree directory path."""
|
||||
# Get git root
|
||||
result = subprocess.run(
|
||||
["git", "-C", project_dir, "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
die(f"Not a git repository: {project_dir}")
|
||||
git_root = result.stdout.strip()
|
||||
|
||||
# Calculate paths
|
||||
worktree_dir = os.path.join(git_root, ".worktrees", f"do-{task_id}")
|
||||
branch_name = f"do/{task_id}"
|
||||
|
||||
# Create worktree with new branch
|
||||
result = subprocess.run(
|
||||
["git", "-C", git_root, "worktree", "add", "-b", branch_name, worktree_dir],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
die(f"Failed to create worktree: {result.stderr}")
|
||||
|
||||
return worktree_dir
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Creates (or overwrites) project state file: .claude/do.local.md"
|
||||
description="Initialize do skill workflow with task directory"
|
||||
)
|
||||
parser.add_argument("--max-phases", type=int, default=5, help="Default: 5")
|
||||
parser.add_argument(
|
||||
@@ -63,61 +33,26 @@ def main():
|
||||
parser.add_argument("prompt", nargs="+", help="Task description")
|
||||
args = parser.parse_args()
|
||||
|
||||
max_phases = args.max_phases
|
||||
completion_promise = args.completion_promise
|
||||
use_worktree = args.worktree
|
||||
prompt = " ".join(args.prompt)
|
||||
|
||||
if max_phases < 1:
|
||||
if args.max_phases < 1:
|
||||
die("--max-phases must be a positive integer")
|
||||
|
||||
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
||||
state_dir = os.path.join(project_dir, ".claude")
|
||||
prompt = " ".join(args.prompt)
|
||||
result = create_task(title=prompt, use_worktree=args.worktree)
|
||||
|
||||
task_id = f"{int(time.time())}-{os.getpid()}-{secrets.token_hex(4)}"
|
||||
state_file = os.path.join(state_dir, f"do.{task_id}.local.md")
|
||||
task_data = result["task_data"]
|
||||
worktree_dir = result.get("worktree_dir", "")
|
||||
|
||||
os.makedirs(state_dir, exist_ok=True)
|
||||
print(f"Initialized: {result['relative_path']}")
|
||||
print(f"task_id: {task_data['id']}")
|
||||
print(f"phase: 1/{task_data['max_phases']} ({PHASE_NAMES[1]})")
|
||||
print(f"completion_promise: {task_data['completion_promise']}")
|
||||
print(f"use_worktree: {task_data['use_worktree']}")
|
||||
print(f"export DO_TASK_DIR={result['relative_path']}")
|
||||
|
||||
# Create worktree if requested (before writing state file)
|
||||
worktree_dir = ""
|
||||
if use_worktree:
|
||||
worktree_dir = create_worktree(project_dir, task_id)
|
||||
|
||||
phase_name = phase_name_for(1)
|
||||
|
||||
content = f"""---
|
||||
active: true
|
||||
current_phase: 1
|
||||
phase_name: "{phase_name}"
|
||||
max_phases: {max_phases}
|
||||
completion_promise: "{completion_promise}"
|
||||
use_worktree: {str(use_worktree).lower()}
|
||||
worktree_dir: "{worktree_dir}"
|
||||
---
|
||||
|
||||
# do loop state
|
||||
|
||||
## Prompt
|
||||
{prompt}
|
||||
|
||||
## Notes
|
||||
- Update frontmatter current_phase/phase_name as you progress
|
||||
- When complete, include the frontmatter completion_promise in your final output
|
||||
"""
|
||||
|
||||
with open(state_file, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"Initialized: {state_file}")
|
||||
print(f"task_id: {task_id}")
|
||||
print(f"phase: 1/{max_phases} ({phase_name})")
|
||||
print(f"completion_promise: {completion_promise}")
|
||||
print(f"use_worktree: {use_worktree}")
|
||||
print(f"export DO_TASK_ID={task_id}")
|
||||
if worktree_dir:
|
||||
print(f"worktree_dir: {worktree_dir}")
|
||||
print(f"export DO_WORKTREE_DIR={worktree_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user