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:
149
skills/do/scripts/get-context.py
Normal file
149
skills/do/scripts/get-context.py
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Get context for current task.
|
||||
|
||||
Reads the current task's jsonl files and returns context for specified agent.
|
||||
Used by inject-context hook to build agent prompts.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DIR_TASKS = ".claude/do-tasks"
|
||||
FILE_CURRENT_TASK = ".current-task"
|
||||
FILE_TASK_JSON = "task.json"
|
||||
|
||||
|
||||
def get_project_root() -> str:
|
||||
return os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
||||
|
||||
|
||||
def get_current_task(project_root: str) -> str | None:
|
||||
current_task_file = os.path.join(project_root, DIR_TASKS, FILE_CURRENT_TASK)
|
||||
if not os.path.exists(current_task_file):
|
||||
return None
|
||||
try:
|
||||
with open(current_task_file, "r", encoding="utf-8") as f:
|
||||
content = f.read().strip()
|
||||
return content if content else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def read_file_content(base_path: str, file_path: str) -> str | None:
|
||||
full_path = os.path.join(base_path, file_path)
|
||||
if os.path.exists(full_path) and os.path.isfile(full_path):
|
||||
try:
|
||||
with open(full_path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]:
|
||||
full_path = os.path.join(base_path, jsonl_path)
|
||||
if not os.path.exists(full_path):
|
||||
return []
|
||||
|
||||
results = []
|
||||
try:
|
||||
with open(full_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
item = json.loads(line)
|
||||
file_path = item.get("file") or item.get("path")
|
||||
if not file_path:
|
||||
continue
|
||||
content = read_file_content(base_path, file_path)
|
||||
if content:
|
||||
results.append((file_path, content))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
return results
|
||||
|
||||
|
||||
def get_agent_context(project_root: str, task_dir: str, agent_type: str) -> str:
|
||||
"""Get complete context for specified agent."""
|
||||
context_parts = []
|
||||
|
||||
# Read agent-specific jsonl
|
||||
agent_jsonl = os.path.join(task_dir, f"{agent_type}.jsonl")
|
||||
agent_entries = read_jsonl_entries(project_root, agent_jsonl)
|
||||
|
||||
for file_path, content in agent_entries:
|
||||
context_parts.append(f"=== {file_path} ===\n{content}")
|
||||
|
||||
# Read prd.md
|
||||
prd_content = read_file_content(project_root, os.path.join(task_dir, "prd.md"))
|
||||
if prd_content:
|
||||
context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}")
|
||||
|
||||
return "\n\n".join(context_parts)
|
||||
|
||||
|
||||
def get_task_info(project_root: str, task_dir: str) -> dict | None:
|
||||
"""Get task.json data."""
|
||||
task_json_path = os.path.join(project_root, task_dir, FILE_TASK_JSON)
|
||||
if not os.path.exists(task_json_path):
|
||||
return None
|
||||
try:
|
||||
with open(task_json_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Get context for current task")
|
||||
parser.add_argument("agent", nargs="?", choices=["implement", "check", "debug"],
|
||||
help="Agent type (optional, returns task info if not specified)")
|
||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
project_root = get_project_root()
|
||||
task_dir = get_current_task(project_root)
|
||||
|
||||
if not task_dir:
|
||||
if args.json:
|
||||
print(json.dumps({"error": "No active task"}))
|
||||
else:
|
||||
print("No active task.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
task_info = get_task_info(project_root, task_dir)
|
||||
|
||||
if not args.agent:
|
||||
if args.json:
|
||||
print(json.dumps({"task_dir": task_dir, "task_info": task_info}))
|
||||
else:
|
||||
print(f"Task: {task_dir}")
|
||||
if task_info:
|
||||
print(f"Title: {task_info.get('title', 'N/A')}")
|
||||
print(f"Phase: {task_info.get('current_phase', '?')}/{task_info.get('max_phases', 5)}")
|
||||
sys.exit(0)
|
||||
|
||||
context = get_agent_context(project_root, task_dir, args.agent)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps({
|
||||
"task_dir": task_dir,
|
||||
"agent": args.agent,
|
||||
"context": context,
|
||||
"task_info": task_info,
|
||||
}))
|
||||
else:
|
||||
print(context)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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()
|
||||
|
||||
434
skills/do/scripts/task.py
Normal file
434
skills/do/scripts/task.py
Normal file
@@ -0,0 +1,434 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Task Directory Management CLI for do skill workflow.
|
||||
|
||||
Commands:
|
||||
create <title> - Create a new task directory with task.md
|
||||
start <task-dir> - Set current task pointer
|
||||
finish - Clear current task pointer
|
||||
list - List active tasks
|
||||
status - Show current task status
|
||||
update-phase <N> - Update current phase
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Directory constants
|
||||
DIR_TASKS = ".claude/do-tasks"
|
||||
FILE_CURRENT_TASK = ".current-task"
|
||||
FILE_TASK_MD = "task.md"
|
||||
|
||||
PHASE_NAMES = {
|
||||
1: "Understand",
|
||||
2: "Clarify",
|
||||
3: "Design",
|
||||
4: "Implement",
|
||||
5: "Complete",
|
||||
}
|
||||
|
||||
|
||||
def get_project_root() -> str:
|
||||
"""Get project root from env or cwd."""
|
||||
return os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
|
||||
|
||||
|
||||
def get_tasks_dir(project_root: str) -> str:
|
||||
"""Get tasks directory path."""
|
||||
return os.path.join(project_root, DIR_TASKS)
|
||||
|
||||
|
||||
def get_current_task_file(project_root: str) -> str:
|
||||
"""Get current task pointer file path."""
|
||||
return os.path.join(project_root, DIR_TASKS, FILE_CURRENT_TASK)
|
||||
|
||||
|
||||
def generate_task_id() -> str:
|
||||
"""Generate short task ID: MMDD-XXXX format."""
|
||||
date_part = datetime.now().strftime("%m%d")
|
||||
random_part = ''.join(random.choices(string.ascii_lowercase + string.digits, k=4))
|
||||
return f"{date_part}-{random_part}"
|
||||
|
||||
|
||||
def read_task_md(task_md_path: str) -> dict | None:
|
||||
"""Read task.md and parse YAML frontmatter + body."""
|
||||
if not os.path.exists(task_md_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(task_md_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# Parse YAML frontmatter
|
||||
match = re.match(r'^---\n(.*?)\n---\n(.*)$', content, re.DOTALL)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
frontmatter_str = match.group(1)
|
||||
body = match.group(2)
|
||||
|
||||
# Simple YAML parsing (no external deps)
|
||||
frontmatter = {}
|
||||
for line in frontmatter_str.split('\n'):
|
||||
if ':' in line:
|
||||
key, value = line.split(':', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
# Handle quoted strings
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
elif value == 'true':
|
||||
value = True
|
||||
elif value == 'false':
|
||||
value = False
|
||||
elif value.isdigit():
|
||||
value = int(value)
|
||||
frontmatter[key] = value
|
||||
|
||||
return {"frontmatter": frontmatter, "body": body}
|
||||
|
||||
|
||||
def write_task_md(task_md_path: str, frontmatter: dict, body: str) -> bool:
|
||||
"""Write task.md with YAML frontmatter + body."""
|
||||
try:
|
||||
lines = ["---"]
|
||||
for key, value in frontmatter.items():
|
||||
if isinstance(value, bool):
|
||||
lines.append(f"{key}: {str(value).lower()}")
|
||||
elif isinstance(value, int):
|
||||
lines.append(f"{key}: {value}")
|
||||
elif isinstance(value, str) and ('<' in value or '>' in value or ':' in value):
|
||||
lines.append(f'{key}: "{value}"')
|
||||
else:
|
||||
lines.append(f'{key}: "{value}"' if isinstance(value, str) else f"{key}: {value}")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append(body)
|
||||
|
||||
with open(task_md_path, "w", encoding="utf-8") as f:
|
||||
f.write('\n'.join(lines))
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def create_worktree(project_root: 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_root, "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"Not a git repository: {project_root}")
|
||||
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:
|
||||
raise RuntimeError(f"Failed to create worktree: {result.stderr}")
|
||||
|
||||
return worktree_dir
|
||||
|
||||
|
||||
def create_task(title: str, use_worktree: bool = False) -> dict:
|
||||
"""Create a new task directory with task.md."""
|
||||
project_root = get_project_root()
|
||||
tasks_dir = get_tasks_dir(project_root)
|
||||
os.makedirs(tasks_dir, exist_ok=True)
|
||||
|
||||
task_id = generate_task_id()
|
||||
task_dir = os.path.join(tasks_dir, task_id)
|
||||
|
||||
os.makedirs(task_dir, exist_ok=True)
|
||||
|
||||
# Create worktree if requested
|
||||
worktree_dir = ""
|
||||
if use_worktree:
|
||||
try:
|
||||
worktree_dir = create_worktree(project_root, task_id)
|
||||
except RuntimeError as e:
|
||||
print(f"Warning: {e}", file=sys.stderr)
|
||||
use_worktree = False
|
||||
|
||||
frontmatter = {
|
||||
"id": task_id,
|
||||
"title": title,
|
||||
"status": "in_progress",
|
||||
"current_phase": 1,
|
||||
"phase_name": PHASE_NAMES[1],
|
||||
"max_phases": 5,
|
||||
"use_worktree": use_worktree,
|
||||
"worktree_dir": worktree_dir,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"completion_promise": "<promise>DO_COMPLETE</promise>",
|
||||
}
|
||||
|
||||
body = f"""# Requirements
|
||||
|
||||
{title}
|
||||
|
||||
## Context
|
||||
|
||||
## Progress
|
||||
"""
|
||||
|
||||
task_md_path = os.path.join(task_dir, FILE_TASK_MD)
|
||||
write_task_md(task_md_path, frontmatter, body)
|
||||
|
||||
current_task_file = get_current_task_file(project_root)
|
||||
relative_task_dir = os.path.relpath(task_dir, project_root)
|
||||
with open(current_task_file, "w", encoding="utf-8") as f:
|
||||
f.write(relative_task_dir)
|
||||
|
||||
return {
|
||||
"task_dir": task_dir,
|
||||
"relative_path": relative_task_dir,
|
||||
"task_data": frontmatter,
|
||||
"worktree_dir": worktree_dir,
|
||||
}
|
||||
|
||||
|
||||
def get_current_task(project_root: str) -> str | None:
|
||||
"""Read current task directory path."""
|
||||
current_task_file = get_current_task_file(project_root)
|
||||
if not os.path.exists(current_task_file):
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(current_task_file, "r", encoding="utf-8") as f:
|
||||
content = f.read().strip()
|
||||
return content if content else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def start_task(task_dir: str) -> bool:
|
||||
"""Set current task pointer."""
|
||||
project_root = get_project_root()
|
||||
tasks_dir = get_tasks_dir(project_root)
|
||||
|
||||
if os.path.isabs(task_dir):
|
||||
full_path = task_dir
|
||||
relative_path = os.path.relpath(task_dir, project_root)
|
||||
else:
|
||||
if not task_dir.startswith(DIR_TASKS):
|
||||
full_path = os.path.join(tasks_dir, task_dir)
|
||||
relative_path = os.path.join(DIR_TASKS, task_dir)
|
||||
else:
|
||||
full_path = os.path.join(project_root, task_dir)
|
||||
relative_path = task_dir
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
print(f"Error: Task directory not found: {full_path}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
current_task_file = get_current_task_file(project_root)
|
||||
os.makedirs(os.path.dirname(current_task_file), exist_ok=True)
|
||||
|
||||
with open(current_task_file, "w", encoding="utf-8") as f:
|
||||
f.write(relative_path)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def finish_task() -> bool:
|
||||
"""Clear current task pointer."""
|
||||
project_root = get_project_root()
|
||||
current_task_file = get_current_task_file(project_root)
|
||||
|
||||
if os.path.exists(current_task_file):
|
||||
os.remove(current_task_file)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def list_tasks() -> list[dict]:
|
||||
"""List all task directories."""
|
||||
project_root = get_project_root()
|
||||
tasks_dir = get_tasks_dir(project_root)
|
||||
|
||||
if not os.path.exists(tasks_dir):
|
||||
return []
|
||||
|
||||
tasks = []
|
||||
current_task = get_current_task(project_root)
|
||||
|
||||
for entry in sorted(os.listdir(tasks_dir), reverse=True):
|
||||
entry_path = os.path.join(tasks_dir, entry)
|
||||
if not os.path.isdir(entry_path):
|
||||
continue
|
||||
|
||||
task_md_path = os.path.join(entry_path, FILE_TASK_MD)
|
||||
if not os.path.exists(task_md_path):
|
||||
continue
|
||||
|
||||
parsed = read_task_md(task_md_path)
|
||||
if parsed:
|
||||
task_data = parsed["frontmatter"]
|
||||
else:
|
||||
task_data = {"id": entry, "title": entry, "status": "unknown"}
|
||||
|
||||
relative_path = os.path.join(DIR_TASKS, entry)
|
||||
task_data["path"] = relative_path
|
||||
task_data["is_current"] = current_task == relative_path
|
||||
tasks.append(task_data)
|
||||
|
||||
return tasks
|
||||
|
||||
|
||||
def get_status() -> dict | None:
|
||||
"""Get current task status."""
|
||||
project_root = get_project_root()
|
||||
current_task = get_current_task(project_root)
|
||||
|
||||
if not current_task:
|
||||
return None
|
||||
|
||||
task_dir = os.path.join(project_root, current_task)
|
||||
task_md_path = os.path.join(task_dir, FILE_TASK_MD)
|
||||
|
||||
parsed = read_task_md(task_md_path)
|
||||
if not parsed:
|
||||
return None
|
||||
|
||||
task_data = parsed["frontmatter"]
|
||||
task_data["path"] = current_task
|
||||
return task_data
|
||||
|
||||
|
||||
def update_phase(phase: int) -> bool:
|
||||
"""Update current task phase."""
|
||||
project_root = get_project_root()
|
||||
current_task = get_current_task(project_root)
|
||||
|
||||
if not current_task:
|
||||
print("Error: No active task.", file=sys.stderr)
|
||||
return False
|
||||
|
||||
task_dir = os.path.join(project_root, current_task)
|
||||
task_md_path = os.path.join(task_dir, FILE_TASK_MD)
|
||||
|
||||
parsed = read_task_md(task_md_path)
|
||||
if not parsed:
|
||||
print("Error: task.md not found or invalid.", file=sys.stderr)
|
||||
return False
|
||||
|
||||
frontmatter = parsed["frontmatter"]
|
||||
frontmatter["current_phase"] = phase
|
||||
frontmatter["phase_name"] = PHASE_NAMES.get(phase, f"Phase {phase}")
|
||||
|
||||
if not write_task_md(task_md_path, frontmatter, parsed["body"]):
|
||||
print("Error: Failed to write task.md.", file=sys.stderr)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Task directory management for do skill workflow"
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# create command
|
||||
create_parser = subparsers.add_parser("create", help="Create a new task")
|
||||
create_parser.add_argument("title", nargs="+", help="Task title")
|
||||
create_parser.add_argument("--worktree", action="store_true", help="Enable worktree mode")
|
||||
|
||||
# start command
|
||||
start_parser = subparsers.add_parser("start", help="Set current task")
|
||||
start_parser.add_argument("task_dir", help="Task directory path")
|
||||
|
||||
# finish command
|
||||
subparsers.add_parser("finish", help="Clear current task")
|
||||
|
||||
# list command
|
||||
subparsers.add_parser("list", help="List all tasks")
|
||||
|
||||
# status command
|
||||
subparsers.add_parser("status", help="Show current task status")
|
||||
|
||||
# update-phase command
|
||||
phase_parser = subparsers.add_parser("update-phase", help="Update current phase")
|
||||
phase_parser.add_argument("phase", type=int, help="Phase number (1-5)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "create":
|
||||
title = " ".join(args.title)
|
||||
result = create_task(title, args.worktree)
|
||||
print(f"Created task: {result['relative_path']}")
|
||||
print(f"Task ID: {result['task_data']['id']}")
|
||||
print(f"Phase: 1/{result['task_data']['max_phases']} (Understand)")
|
||||
print(f"Worktree: {result['task_data']['use_worktree']}")
|
||||
|
||||
elif args.command == "start":
|
||||
if start_task(args.task_dir):
|
||||
print(f"Started task: {args.task_dir}")
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
elif args.command == "finish":
|
||||
if finish_task():
|
||||
print("Task finished, current task cleared.")
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
elif args.command == "list":
|
||||
tasks = list_tasks()
|
||||
if not tasks:
|
||||
print("No tasks found.")
|
||||
else:
|
||||
for task in tasks:
|
||||
marker = "* " if task.get("is_current") else " "
|
||||
phase = task.get("current_phase", "?")
|
||||
max_phase = task.get("max_phases", 5)
|
||||
status = task.get("status", "unknown")
|
||||
print(f"{marker}{task['id']} [{status}] phase {phase}/{max_phase}")
|
||||
print(f" {task.get('title', 'No title')}")
|
||||
|
||||
elif args.command == "status":
|
||||
status = get_status()
|
||||
if not status:
|
||||
print("No active task.")
|
||||
else:
|
||||
print(f"Task: {status['id']}")
|
||||
print(f"Title: {status.get('title', 'No title')}")
|
||||
print(f"Status: {status.get('status', 'unknown')}")
|
||||
print(f"Phase: {status.get('current_phase', '?')}/{status.get('max_phases', 5)}")
|
||||
print(f"Worktree: {status.get('use_worktree', False)}")
|
||||
print(f"Path: {status['path']}")
|
||||
|
||||
elif args.command == "update-phase":
|
||||
if update_phase(args.phase):
|
||||
phase_name = PHASE_NAMES.get(args.phase, f"Phase {args.phase}")
|
||||
print(f"Updated to phase {args.phase} ({phase_name})")
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user