feat: add worktree support and refactor do skill to Python

- Add worktree module for git worktree management
- Refactor do skill scripts from shell to Python for better maintainability
- Add install.py for do skill installation
- Update stop-hook to Python implementation
- Enhance executor with additional configuration options
- Update CLAUDE.md with first-principles thinking guidelines

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
This commit is contained in:
cexll
2026-02-03 21:58:08 +08:00
parent 04fa1626ae
commit 74e4d181c2
19 changed files with 1257 additions and 433 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ __pycache__
coverage.out
references
output/
.worktrees/

View File

@@ -30,6 +30,7 @@ type cliOptions struct {
Agent string
PromptFile string
SkipPermissions bool
Worktree bool
Parallel bool
FullOutput bool
@@ -136,6 +137,7 @@ func addRootFlags(fs *pflag.FlagSet, opts *cliOptions) {
fs.BoolVar(&opts.SkipPermissions, "skip-permissions", false, "Skip permissions prompts (also via CODEAGENT_SKIP_PERMISSIONS)")
fs.BoolVar(&opts.SkipPermissions, "dangerously-skip-permissions", false, "Alias for --skip-permissions")
fs.BoolVar(&opts.Worktree, "worktree", false, "Execute in a new git worktree (auto-generates task ID)")
}
func newVersionCommand(name string) *cobra.Command {
@@ -350,6 +352,7 @@ func buildSingleConfig(cmd *cobra.Command, args []string, rawArgv []string, opts
MaxParallelWorkers: config.ResolveMaxParallelWorkers(),
AllowedTools: resolvedAllowedTools,
DisallowedTools: resolvedDisallowedTools,
Worktree: opts.Worktree,
}
if args[0] == "resume" {
@@ -653,6 +656,7 @@ func runSingleMode(cfg *Config, name string) int {
ReasoningEffort: cfg.ReasoningEffort,
Agent: cfg.Agent,
SkipPermissions: cfg.SkipPermissions,
Worktree: cfg.Worktree,
AllowedTools: cfg.AllowedTools,
DisallowedTools: cfg.DisallowedTools,
UseStdin: useStdin,

View File

@@ -1616,6 +1616,60 @@ do something`
}
}
func TestParallelParseConfig_Worktree(t *testing.T) {
input := `---TASK---
id: task-1
worktree: true
---CONTENT---
do something`
cfg, err := parseParallelConfig([]byte(input))
if err != nil {
t.Fatalf("parseParallelConfig() unexpected error: %v", err)
}
if len(cfg.Tasks) != 1 {
t.Fatalf("expected 1 task, got %d", len(cfg.Tasks))
}
task := cfg.Tasks[0]
if !task.Worktree {
t.Fatalf("Worktree = %v, want true", task.Worktree)
}
}
func TestParallelParseConfig_WorktreeBooleanValue(t *testing.T) {
tests := []struct {
name string
value string
want bool
}{
{"true", "true", true},
{"1", "1", true},
{"yes", "yes", true},
{"false", "false", false},
{"0", "0", false},
{"no", "no", false},
{"empty", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
input := fmt.Sprintf(`---TASK---
id: task-1
worktree: %s
---CONTENT---
do something`, tt.value)
cfg, err := parseParallelConfig([]byte(input))
if err != nil {
t.Fatalf("parseParallelConfig() unexpected error: %v", err)
}
if cfg.Tasks[0].Worktree != tt.want {
t.Fatalf("Worktree = %v, want %v for value %q", cfg.Tasks[0].Worktree, tt.want, tt.value)
}
})
}
}
func TestParallelParseConfig_EmptySessionID(t *testing.T) {
input := `---TASK---
id: task-1

View File

@@ -26,6 +26,7 @@ type Config struct {
MaxParallelWorkers int
AllowedTools []string
DisallowedTools []string
Worktree bool // Execute in a new git worktree
}
// EnvFlagEnabled returns true when the environment variable exists and is not

View File

@@ -21,6 +21,7 @@ import (
ilogger "codeagent-wrapper/internal/logger"
parser "codeagent-wrapper/internal/parser"
utils "codeagent-wrapper/internal/utils"
"codeagent-wrapper/internal/worktree"
)
const postMessageTerminateDelay = 1 * time.Second
@@ -49,6 +50,7 @@ var (
selectBackendFn = backend.Select
commandContext = exec.CommandContext
terminateCommandFn = terminateCommand
createWorktreeFn = worktree.CreateWorktree
)
var forceKillDelay atomic.Int32
@@ -939,6 +941,18 @@ func RunCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
cfg.WorkDir = defaultWorkdir
}
// Handle worktree mode: create a new git worktree and update cfg.WorkDir
if taskSpec.Worktree {
paths, err := createWorktreeFn(cfg.WorkDir)
if err != nil {
result.ExitCode = 1
result.Error = fmt.Sprintf("failed to create worktree: %v", err)
return result
}
cfg.WorkDir = paths.Dir
logInfo(fmt.Sprintf("Using worktree: %s (task_id: %s, branch: %s)", paths.Dir, paths.TaskID, paths.Branch))
}
if cfg.Mode == "resume" && strings.TrimSpace(cfg.SessionID) == "" {
result.ExitCode = 1
result.Error = "resume mode requires non-empty session_id"

View File

@@ -75,6 +75,12 @@ func ParseParallelConfig(data []byte) (*ParallelConfig, error) {
continue
}
task.SkipPermissions = config.ParseBoolFlag(value, false)
case "worktree":
if value == "" {
task.Worktree = true
continue
}
task.Worktree = config.ParseBoolFlag(value, false)
case "dependencies":
for _, dep := range strings.Split(value, ",") {
dep = strings.TrimSpace(dep)

View File

@@ -21,6 +21,7 @@ type TaskSpec struct {
Agent string `json:"agent,omitempty"`
PromptFile string `json:"prompt_file,omitempty"`
SkipPermissions bool `json:"skip_permissions,omitempty"`
Worktree bool `json:"worktree,omitempty"`
AllowedTools []string `json:"allowed_tools,omitempty"`
DisallowedTools []string `json:"disallowed_tools,omitempty"`
Mode string `json:"-"`

View File

@@ -0,0 +1,97 @@
package worktree
import (
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"os/exec"
"path/filepath"
"strings"
"time"
)
// Paths contains worktree information
type Paths struct {
Dir string // .worktrees/do-{task_id}/
Branch string // do/{task_id}
TaskID string // auto-generated task_id
}
// Hook points for testing
var (
randReader io.Reader = rand.Reader
timeNowFunc = time.Now
execCommand = exec.Command
)
// generateTaskID creates a unique task ID in format: YYYYMMDD-{6 hex chars}
func generateTaskID() (string, error) {
bytes := make([]byte, 3)
if _, err := io.ReadFull(randReader, bytes); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
date := timeNowFunc().Format("20060102")
return fmt.Sprintf("%s-%s", date, hex.EncodeToString(bytes)), nil
}
// isGitRepo checks if the given directory is inside a git repository
func isGitRepo(dir string) bool {
cmd := execCommand("git", "-C", dir, "rev-parse", "--is-inside-work-tree")
output, err := cmd.Output()
if err != nil {
return false
}
return strings.TrimSpace(string(output)) == "true"
}
// getGitRoot returns the root directory of the git repository
func getGitRoot(dir string) (string, error) {
cmd := execCommand("git", "-C", dir, "rev-parse", "--show-toplevel")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get git root: %w", err)
}
return strings.TrimSpace(string(output)), nil
}
// CreateWorktree creates a new git worktree with auto-generated task_id
// Returns Paths containing the worktree directory, branch name, and task_id
func CreateWorktree(projectDir string) (*Paths, error) {
if projectDir == "" {
projectDir = "."
}
// Verify it's a git repository
if !isGitRepo(projectDir) {
return nil, fmt.Errorf("not a git repository: %s", projectDir)
}
// Get git root for consistent path calculation
gitRoot, err := getGitRoot(projectDir)
if err != nil {
return nil, err
}
// Generate task ID
taskID, err := generateTaskID()
if err != nil {
return nil, err
}
// Calculate paths
worktreeDir := filepath.Join(gitRoot, ".worktrees", fmt.Sprintf("do-%s", taskID))
branchName := fmt.Sprintf("do/%s", taskID)
// Create worktree with new branch
cmd := execCommand("git", "-C", gitRoot, "worktree", "add", "-b", branchName, worktreeDir)
if output, err := cmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("failed to create worktree: %w\noutput: %s", err, string(output))
}
return &Paths{
Dir: worktreeDir,
Branch: branchName,
TaskID: taskID,
}, nil
}

View File

@@ -0,0 +1,449 @@
package worktree
import (
"crypto/rand"
"errors"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"sync"
"testing"
"time"
)
func resetHooks() {
randReader = rand.Reader
timeNowFunc = time.Now
execCommand = exec.Command
}
func TestGenerateTaskID(t *testing.T) {
defer resetHooks()
taskID, err := generateTaskID()
if err != nil {
t.Fatalf("generateTaskID() error = %v", err)
}
// Format: YYYYMMDD-6hex
pattern := regexp.MustCompile(`^\d{8}-[0-9a-f]{6}$`)
if !pattern.MatchString(taskID) {
t.Errorf("generateTaskID() = %q, want format YYYYMMDD-xxxxxx", taskID)
}
}
func TestGenerateTaskID_FixedTime(t *testing.T) {
defer resetHooks()
// Mock time to a fixed date
timeNowFunc = func() time.Time {
return time.Date(2026, 2, 3, 12, 0, 0, 0, time.UTC)
}
taskID, err := generateTaskID()
if err != nil {
t.Fatalf("generateTaskID() error = %v", err)
}
if !regexp.MustCompile(`^20260203-[0-9a-f]{6}$`).MatchString(taskID) {
t.Errorf("generateTaskID() = %q, want prefix 20260203-", taskID)
}
}
func TestGenerateTaskID_RandReaderError(t *testing.T) {
defer resetHooks()
// Mock rand reader to return error
randReader = &errorReader{err: errors.New("mock rand error")}
_, err := generateTaskID()
if err == nil {
t.Fatal("generateTaskID() expected error, got nil")
}
if !regexp.MustCompile(`failed to generate random bytes`).MatchString(err.Error()) {
t.Errorf("error = %q, want 'failed to generate random bytes'", err.Error())
}
}
type errorReader struct {
err error
}
func (e *errorReader) Read(p []byte) (n int, err error) {
return 0, e.err
}
func TestGenerateTaskID_Uniqueness(t *testing.T) {
defer resetHooks()
const count = 100
ids := make(map[string]struct{}, count)
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < count; i++ {
wg.Add(1)
go func() {
defer wg.Done()
id, err := generateTaskID()
if err != nil {
t.Errorf("generateTaskID() error = %v", err)
return
}
mu.Lock()
ids[id] = struct{}{}
mu.Unlock()
}()
}
wg.Wait()
if len(ids) != count {
t.Errorf("generateTaskID() produced %d unique IDs out of %d, expected all unique", len(ids), count)
}
}
func TestCreateWorktree_NotGitRepo(t *testing.T) {
defer resetHooks()
tmpDir, err := os.MkdirTemp("", "worktree-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
_, err = CreateWorktree(tmpDir)
if err == nil {
t.Error("CreateWorktree() expected error for non-git directory, got nil")
}
if err != nil && !regexp.MustCompile(`not a git repository`).MatchString(err.Error()) {
t.Errorf("CreateWorktree() error = %q, want 'not a git repository'", err.Error())
}
}
func TestCreateWorktree_EmptyProjectDir(t *testing.T) {
defer resetHooks()
// When projectDir is empty, it should default to "."
// This will fail because current dir may not be a git repo, but we test the default behavior
_, err := CreateWorktree("")
// We just verify it doesn't panic and returns an error (likely "not a git repository: .")
if err == nil {
// If we happen to be in a git repo, that's fine too
return
}
if !regexp.MustCompile(`not a git repository: \.`).MatchString(err.Error()) {
// It might be a git repo and fail later, which is also acceptable
return
}
}
func TestCreateWorktree_Success(t *testing.T) {
defer resetHooks()
// Create temp git repo
tmpDir, err := os.MkdirTemp("", "worktree-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Initialize git repo
if err := exec.Command("git", "-C", tmpDir, "init").Run(); err != nil {
t.Fatalf("failed to init git repo: %v", err)
}
if err := exec.Command("git", "-C", tmpDir, "config", "user.email", "test@test.com").Run(); err != nil {
t.Fatalf("failed to set git email: %v", err)
}
if err := exec.Command("git", "-C", tmpDir, "config", "user.name", "Test").Run(); err != nil {
t.Fatalf("failed to set git name: %v", err)
}
// Create initial commit (required for worktree)
testFile := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil {
t.Fatalf("failed to create test file: %v", err)
}
if err := exec.Command("git", "-C", tmpDir, "add", ".").Run(); err != nil {
t.Fatalf("failed to git add: %v", err)
}
if err := exec.Command("git", "-C", tmpDir, "commit", "-m", "initial").Run(); err != nil {
t.Fatalf("failed to git commit: %v", err)
}
// Test CreateWorktree
paths, err := CreateWorktree(tmpDir)
if err != nil {
t.Fatalf("CreateWorktree() error = %v", err)
}
// Verify task ID format
pattern := regexp.MustCompile(`^\d{8}-[0-9a-f]{6}$`)
if !pattern.MatchString(paths.TaskID) {
t.Errorf("TaskID = %q, want format YYYYMMDD-xxxxxx", paths.TaskID)
}
// Verify branch name
expectedBranch := "do/" + paths.TaskID
if paths.Branch != expectedBranch {
t.Errorf("Branch = %q, want %q", paths.Branch, expectedBranch)
}
// Verify worktree directory exists
if _, err := os.Stat(paths.Dir); os.IsNotExist(err) {
t.Errorf("worktree directory %q does not exist", paths.Dir)
}
// Verify worktree directory is under .worktrees/
expectedDirSuffix := filepath.Join(".worktrees", "do-"+paths.TaskID)
if !regexp.MustCompile(regexp.QuoteMeta(expectedDirSuffix) + `$`).MatchString(paths.Dir) {
t.Errorf("Dir = %q, want suffix %q", paths.Dir, expectedDirSuffix)
}
// Verify branch exists
cmd := exec.Command("git", "-C", tmpDir, "branch", "--list", paths.Branch)
output, err := cmd.Output()
if err != nil {
t.Fatalf("failed to list branches: %v", err)
}
if len(output) == 0 {
t.Errorf("branch %q was not created", paths.Branch)
}
}
func TestCreateWorktree_GetGitRootError(t *testing.T) {
defer resetHooks()
// Create a temp dir and mock git commands
tmpDir, err := os.MkdirTemp("", "worktree-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
callCount := 0
execCommand = func(name string, args ...string) *exec.Cmd {
callCount++
if callCount == 1 {
// First call: isGitRepo - return true
return exec.Command("echo", "true")
}
// Second call: getGitRoot - return error
return exec.Command("false")
}
_, err = CreateWorktree(tmpDir)
if err == nil {
t.Fatal("CreateWorktree() expected error, got nil")
}
if !regexp.MustCompile(`failed to get git root`).MatchString(err.Error()) {
t.Errorf("error = %q, want 'failed to get git root'", err.Error())
}
}
func TestCreateWorktree_GenerateTaskIDError(t *testing.T) {
defer resetHooks()
// Create temp git repo
tmpDir, err := os.MkdirTemp("", "worktree-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Initialize git repo with commit
if err := exec.Command("git", "-C", tmpDir, "init").Run(); err != nil {
t.Fatalf("failed to init git repo: %v", err)
}
if err := exec.Command("git", "-C", tmpDir, "config", "user.email", "test@test.com").Run(); err != nil {
t.Fatalf("failed to set git email: %v", err)
}
if err := exec.Command("git", "-C", tmpDir, "config", "user.name", "Test").Run(); err != nil {
t.Fatalf("failed to set git name: %v", err)
}
testFile := filepath.Join(tmpDir, "test.txt")
if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil {
t.Fatalf("failed to create test file: %v", err)
}
if err := exec.Command("git", "-C", tmpDir, "add", ".").Run(); err != nil {
t.Fatalf("failed to git add: %v", err)
}
if err := exec.Command("git", "-C", tmpDir, "commit", "-m", "initial").Run(); err != nil {
t.Fatalf("failed to git commit: %v", err)
}
// Mock rand reader to fail
randReader = &errorReader{err: errors.New("mock rand error")}
_, err = CreateWorktree(tmpDir)
if err == nil {
t.Fatal("CreateWorktree() expected error, got nil")
}
if !regexp.MustCompile(`failed to generate random bytes`).MatchString(err.Error()) {
t.Errorf("error = %q, want 'failed to generate random bytes'", err.Error())
}
}
func TestCreateWorktree_WorktreeAddError(t *testing.T) {
defer resetHooks()
tmpDir, err := os.MkdirTemp("", "worktree-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
callCount := 0
execCommand = func(name string, args ...string) *exec.Cmd {
callCount++
switch callCount {
case 1:
// isGitRepo - return true
return exec.Command("echo", "true")
case 2:
// getGitRoot - return tmpDir
return exec.Command("echo", tmpDir)
case 3:
// worktree add - return error
return exec.Command("false")
}
return exec.Command("false")
}
_, err = CreateWorktree(tmpDir)
if err == nil {
t.Fatal("CreateWorktree() expected error, got nil")
}
if !regexp.MustCompile(`failed to create worktree`).MatchString(err.Error()) {
t.Errorf("error = %q, want 'failed to create worktree'", err.Error())
}
}
func TestIsGitRepo(t *testing.T) {
defer resetHooks()
// Test non-git directory
tmpDir, err := os.MkdirTemp("", "worktree-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
if isGitRepo(tmpDir) {
t.Error("isGitRepo() = true for non-git directory, want false")
}
// Test git directory
if err := exec.Command("git", "-C", tmpDir, "init").Run(); err != nil {
t.Fatalf("failed to init git repo: %v", err)
}
if !isGitRepo(tmpDir) {
t.Error("isGitRepo() = false for git directory, want true")
}
}
func TestIsGitRepo_CommandError(t *testing.T) {
defer resetHooks()
// Mock execCommand to return error
execCommand = func(name string, args ...string) *exec.Cmd {
return exec.Command("false")
}
if isGitRepo("/some/path") {
t.Error("isGitRepo() = true when command fails, want false")
}
}
func TestIsGitRepo_NotTrueOutput(t *testing.T) {
defer resetHooks()
// Mock execCommand to return something other than "true"
execCommand = func(name string, args ...string) *exec.Cmd {
return exec.Command("echo", "false")
}
if isGitRepo("/some/path") {
t.Error("isGitRepo() = true when output is 'false', want false")
}
}
func TestGetGitRoot(t *testing.T) {
defer resetHooks()
// Create temp git repo
tmpDir, err := os.MkdirTemp("", "worktree-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
if err := exec.Command("git", "-C", tmpDir, "init").Run(); err != nil {
t.Fatalf("failed to init git repo: %v", err)
}
root, err := getGitRoot(tmpDir)
if err != nil {
t.Fatalf("getGitRoot() error = %v", err)
}
// The root should match tmpDir (accounting for symlinks)
absRoot, _ := filepath.EvalSymlinks(root)
absTmp, _ := filepath.EvalSymlinks(tmpDir)
if absRoot != absTmp {
t.Errorf("getGitRoot() = %q, want %q", absRoot, absTmp)
}
}
func TestGetGitRoot_Error(t *testing.T) {
defer resetHooks()
execCommand = func(name string, args ...string) *exec.Cmd {
return exec.Command("false")
}
_, err := getGitRoot("/some/path")
if err == nil {
t.Fatal("getGitRoot() expected error, got nil")
}
if !regexp.MustCompile(`failed to get git root`).MatchString(err.Error()) {
t.Errorf("error = %q, want 'failed to get git root'", err.Error())
}
}
// Test that rand reader produces expected bytes
func TestGenerateTaskID_RandReaderBytes(t *testing.T) {
defer resetHooks()
// Mock rand reader to return fixed bytes
randReader = &fixedReader{data: []byte{0xab, 0xcd, 0xef}}
timeNowFunc = func() time.Time {
return time.Date(2026, 1, 15, 0, 0, 0, 0, time.UTC)
}
taskID, err := generateTaskID()
if err != nil {
t.Fatalf("generateTaskID() error = %v", err)
}
expected := "20260115-abcdef"
if taskID != expected {
t.Errorf("generateTaskID() = %q, want %q", taskID, expected)
}
}
type fixedReader struct {
data []byte
pos int
}
func (f *fixedReader) Read(p []byte) (n int, err error) {
if f.pos >= len(f.data) {
return 0, io.EOF
}
n = copy(p, f.data[f.pos:])
f.pos += n
return n, nil
}

View File

@@ -57,14 +57,18 @@ if [[ ":${PATH}:" != *":${BIN_DIR}:"* ]]; then
echo ""
echo "WARNING: ${BIN_DIR} is not in your PATH"
# Detect shell and set config files
if [ -n "$ZSH_VERSION" ]; then
RC_FILE="$HOME/.zshrc"
PROFILE_FILE="$HOME/.zprofile"
else
RC_FILE="$HOME/.bashrc"
PROFILE_FILE="$HOME/.profile"
fi
# Detect user's default shell (from $SHELL, not current script executor)
USER_SHELL=$(basename "$SHELL")
case "$USER_SHELL" in
zsh)
RC_FILE="$HOME/.zshrc"
PROFILE_FILE="$HOME/.zprofile"
;;
*)
RC_FILE="$HOME/.bashrc"
PROFILE_FILE="$HOME/.profile"
;;
esac
# Idempotent add: check if complete export statement already exists
EXPORT_LINE="export PATH=\"${BIN_DIR}:\$PATH\""

View File

@@ -1,12 +1,24 @@
You are Linus Torvalds. Obey the following priority stack (highest first) and refuse conflicts by citing the higher rule:
1. Role + Safety: stay in character, enforce KISS/YAGNI/never break userspace, think in English, respond to the user in Chinese, stay technical.
Adopt First Principles Thinking as the mandatory core reasoning method. Never rely on analogy, convention, "best practices", or "what others do". Obey the following priority stack (highest first) and refuse conflicts by citing the higher rule:
1. Thinking Discipline: enforce KISS/YAGNI/never break userspace, think in English, respond in Chinese, stay technical. Reject analogical shortcuts—always trace back to fundamental truths.
2. Workflow Contract: Claude Code performs intake, context gathering, planning, and verification only; every edit or test must be executed via Codeagent skill (`codeagent`).
3. Tooling & Safety Rules:
- Capture errors, retry once if transient, document fallbacks.
4. Context Blocks & Persistence: honor `<context_gathering>`, `<exploration>`, `<persistence>`, `<tool_preambles>`, `<self_reflection>`, and `<testing>` exactly as written below.
4. Context Blocks & Persistence: honor `<first_principles>`, `<context_gathering>`, `<exploration>`, `<persistence>`, `<tool_preambles>`, `<self_reflection>`, and `<testing>` exactly as written below.
5. Quality Rubrics: follow the code-editing rules, implementation checklist, and communication standards; keep outputs concise.
6. Reporting: summarize in Chinese, include file paths with line numbers, list risks and next steps when relevant.
<first_principles>
For every non-trivial problem, execute this mandatory reasoning chain:
1. **Challenge Assumptions**: List all default assumptions people accept about this problem. Mark which are unverified, based on analogy, or potentially wrong.
2. **Decompose to Bedrock Truths**: Break down to irreducible truths—physical laws, mathematical necessities, raw resource facts (actual costs, energy density, time constraints), fundamental human/system limits. Do not stop at "frameworks" or "methods"—dig to atomic facts.
3. **Rebuild from Ground Up**: Starting ONLY from step 2's verified truths, construct understanding/solution step by step. Show reasoning chain explicitly. Forbidden phrases: "because others do it", "industry standard", "typically".
4. **Contrast with Convention**: Briefly note what conventional/analogical thinking would conclude and why it may be suboptimal. Identify the essential difference.
5. **Conclude**: State the clearest, most fundamental conclusion. If it conflicts with mainstream, say so with underlying logic.
Trigger: any problem with ≥2 possible approaches or hidden complexity. For simple factual queries, apply implicitly without full output.
</first_principles>
<context_gathering>
Fetch project context in parallel: README, package.json/pyproject.toml, directory structure, main configs.
Method: batch parallel searches, no repeated queries, prefer action over excessive searching.
@@ -15,17 +27,17 @@ Budget: 5-8 tool calls, justify overruns.
</context_gathering>
<exploration>
Goal: Decompose and map the problem space before planning.
Goal: Map the problem space using first-principles decomposition before planning.
Trigger conditions:
- Task involves ≥3 steps or multiple files
- User explicitly requests deep analysis
Process:
- Requirements: Break the ask into explicit requirements, unclear areas, and hidden assumptions.
- Scope mapping: Identify codebase regions, files, functions, or libraries likely involved. If unknown, perform targeted parallel searches NOW before planning. For complex codebases or deep call chains, delegate scope analysis to Codeagent skill.
- Dependencies: Identify relevant frameworks, APIs, config files, data formats, and versioning concerns. When dependencies involve complex framework internals or multi-layer interactions, delegate to Codeagent skill for analysis.
- Ambiguity resolution: Choose the most probable interpretation based on repo context, conventions, and dependency docs. Document assumptions explicitly.
- Output contract: Define exact deliverables (files changed, expected outputs, API responses, CLI behavior, tests passing, etc.).
In plan mode: Invest extra effort here—this phase determines plan quality and depth.
- Requirements: Break the ask into explicit requirements, unclear areas, and hidden assumptions. Apply <first_principles> step 1 here.
- Scope mapping: Identify codebase regions, files, functions, or libraries involved. Perform targeted parallel searches before planning. For complex call chains, delegate to Codeagent skill.
- Dependencies: Identify frameworks, APIs, configs, data formats. For complex internals, delegate to Codeagent skill.
- Ground-truth validation: Before adopting any "standard approach", verify it against bedrock constraints (performance limits, actual API behavior, resource costs). Apply <first_principles> steps 2-3.
- Output contract: Define exact deliverables (files changed, expected outputs, tests passing, etc.).
In plan mode: Apply full first-principles reasoning chain; this phase determines plan quality.
</exploration>
<persistence>

View File

@@ -1,6 +1,6 @@
# do - Feature Development Orchestrator
7-phase feature development workflow orchestrating multiple agents via codeagent-wrapper.
5-phase feature development workflow orchestrating multiple agents via codeagent-wrapper.
## Installation
@@ -24,17 +24,15 @@ Examples:
/do implement order export to CSV
```
## 7-Phase Workflow
## 5-Phase Workflow
| Phase | Name | Goal | Key Actions |
|-------|------|------|-------------|
| 1 | Discovery | Understand requirements | AskUserQuestion + code-architect draft |
| 2 | Exploration | Map codebase patterns | 2-3 parallel code-explorer tasks |
| 3 | Clarification | Resolve ambiguities | **MANDATORY** - must answer before proceeding |
| 4 | Architecture | Design implementation | 2 parallel code-architect approaches |
| 5 | Implementation | Build the feature | **Requires approval** - develop agent |
| 6 | Review | Catch defects | 2-3 parallel code-reviewer tasks |
| 7 | Summary | Document results | code-reviewer summary |
| 1 | Understand | Gather requirements | AskUserQuestion + code-explorer analysis |
| 2 | Clarify | Resolve ambiguities | **MANDATORY** - must answer before proceeding |
| 3 | Design | Plan implementation | code-architect approaches |
| 4 | Implement | Build the feature | **Requires approval** - develop agent |
| 5 | Complete | Finalize and document | code-reviewer summary |
## Agents
@@ -50,8 +48,8 @@ To customize agents, create same-named files in `~/.codeagent/agents/` to overri
## Hard Constraints
1. **Never write code directly** - delegate all changes to codeagent-wrapper agents
2. **Phase 3 is mandatory** - do not proceed until questions are answered
3. **Phase 5 requires approval** - stop after Phase 4 if not approved
2. **Phase 2 is mandatory** - do not proceed until questions are answered
3. **Phase 4 requires approval** - stop after Phase 3 if not approved
4. **Pass complete context forward** - every agent gets the Context Pack
5. **Parallel-first** - run independent tasks via `codeagent-wrapper --parallel`
6. **Update state after each phase** - keep `.claude/do.{task_id}.local.md` current
@@ -63,7 +61,7 @@ To customize agents, create same-named files in `~/.codeagent/agents/` to overri
<verbatim request>
## Context Pack
- Phase: <1-7 name>
- Phase: <1-5 name>
- Decisions: <requirements/constraints/choices>
- Code-explorer output: <paste or "None">
- Code-architect output: <paste or "None">
@@ -83,7 +81,7 @@ To customize agents, create same-named files in `~/.codeagent/agents/` to overri
When triggered via `/do <task>`, initializes `.claude/do.{task_id}.local.md` with:
- `active: true`
- `current_phase: 1`
- `max_phases: 7`
- `max_phases: 5`
- `completion_promise: "<promise>DO_COMPLETE</promise>"`
After each phase, update frontmatter:
@@ -92,7 +90,7 @@ current_phase: <next phase number>
phase_name: "<next phase name>"
```
When all 7 phases complete, output:
When all 5 phases complete, output:
```
<promise>DO_COMPLETE</promise>
```
@@ -184,3 +182,29 @@ Required when using `agent:` in parallel tasks or `--agent`. Create `~/.codeagen
```bash
python install.py --uninstall --module do
```
## Worktree Mode
Use `--worktree` to execute tasks in an isolated git worktree, preventing changes to your main branch:
```bash
codeagent-wrapper --worktree --agent develop "implement feature X" .
```
This automatically:
1. Generates a unique task ID (format: `YYYYMMDD-xxxxxx`)
2. Creates a new worktree at `.worktrees/do-{task_id}/`
3. Creates a new branch `do/{task_id}`
4. Executes the task in the isolated worktree
Output includes: `Using worktree: .worktrees/do-{task_id}/ (task_id: {id}, branch: do/{id})`
In parallel mode, add `worktree: true` to task blocks:
```
---TASK---
id: feature_impl
agent: develop
worktree: true
---CONTENT---
Implement the feature
```

View File

@@ -1,7 +1,7 @@
---
name: do
description: This skill should be used for structured feature development with codebase understanding. Triggers on /do command. Provides a 7-phase workflow (Discovery, Exploration, Clarification, Architecture, Implementation, Review, Summary) using codeagent-wrapper to orchestrate code-explorer, code-architect, code-reviewer, and develop agents in parallel.
allowed-tools: ["Bash(${SKILL_DIR}/scripts/setup-do.sh:*)"]
description: This skill should be used for structured feature development with codebase understanding. Triggers on /do command. Provides a 5-phase workflow (Understand, Clarify, Design, Implement, Complete) using codeagent-wrapper to orchestrate code-explorer, code-architect, code-reviewer, and develop agents in parallel.
allowed-tools: ["Bash(${SKILL_DIR}/scripts/setup-do.py:*)"]
---
# do - Feature Development Orchestrator
@@ -10,17 +10,57 @@ An orchestrator for systematic feature development. Invoke agents via `codeagent
## Loop Initialization (REQUIRED)
When triggered via `/do <task>`, **first** initialize the loop state:
When triggered via `/do <task>`, follow these steps:
### Step 1: Ask about worktree mode
Use AskUserQuestion to ask:
```
Develop in a separate worktree? (Isolates changes from main branch)
- Yes (Recommended for larger changes)
- No (Work directly in current directory)
```
### Step 2: Initialize state
```bash
"${SKILL_DIR}/scripts/setup-do.sh" "<task description>"
# If worktree mode selected:
python3 "${SKILL_DIR}/scripts/setup-do.py" --worktree "<task description>"
# If no worktree:
python3 "${SKILL_DIR}/scripts/setup-do.py" "<task description>"
```
This creates `.claude/do.{task_id}.local.md` with:
- `active: true`
- `current_phase: 1`
- `max_phases: 7`
- `max_phases: 5`
- `completion_promise: "<promise>DO_COMPLETE</promise>"`
- `use_worktree: true/false`
## Worktree Mode
When `use_worktree: true` in state file, ALL `codeagent-wrapper` calls that modify code MUST include `--worktree`:
```bash
# With worktree mode enabled
codeagent-wrapper --worktree --agent develop - . <<'EOF'
...
EOF
# Parallel tasks with worktree
codeagent-wrapper --worktree --parallel <<'EOF'
---TASK---
id: task1
agent: develop
workdir: .
---CONTENT---
...
EOF
```
The `--worktree` flag tells codeagent-wrapper to create/use a worktree internally. Read-only agents (code-explorer, code-architect, code-reviewer) do NOT need `--worktree`.
## Loop State Management
@@ -30,7 +70,7 @@ current_phase: <next phase number>
phase_name: "<next phase name>"
```
When all 7 phases complete, output the completion signal:
When all 5 phases complete, output the completion signal:
```
<promise>DO_COMPLETE</promise>
```
@@ -40,22 +80,35 @@ To abort early, set `active: false` in the state file.
## Hard Constraints
1. **Never write code directly.** Delegate all code changes to `codeagent-wrapper` agents.
2. **Phase 3 (Clarification) is mandatory.** Do not proceed until questions are answered.
3. **Phase 5 (Implementation) requires explicit approval.** Stop after Phase 4 if not approved.
4. **Pass complete context forward.** Every agent invocation includes the Context Pack.
5. **Parallel-first.** Run independent tasks via `codeagent-wrapper --parallel`.
6. **Update state after each phase.** Keep `.claude/do.{task_id}.local.md` current.
7. **Expect long-running `codeagent-wrapper` calls.** High-reasoning modes (e.g. `xhigh`) can take a long time; stay in the orchestrator role and wait for agents to complete.
8. **Timeouts are not an escape hatch.** If a `codeagent-wrapper` invocation times out/errors, retry `codeagent-wrapper` (split/narrow the task if needed); never switch to direct implementation.
2. **Pass complete context forward.** Every agent invocation includes the Context Pack.
3. **Parallel-first.** Run independent tasks via `codeagent-wrapper --parallel`.
4. **Update state after each phase.** Keep `.claude/do.{task_id}.local.md` current.
5. **Expect long-running `codeagent-wrapper` calls.** High-reasoning modes can take a long time; stay in the orchestrator role and wait for agents to complete.
6. **Timeouts are not an escape hatch.** If a `codeagent-wrapper` invocation times out/errors, retry (split/narrow the task if needed); never switch to direct implementation.
7. **Respect worktree setting.** If `use_worktree: true`, always pass `--worktree` to develop agent calls.
## Agents
| Agent | Purpose | Prompt |
|-------|---------|--------|
| `code-explorer` | Trace code, map architecture, find patterns | `agents/code-explorer.md` |
| `code-architect` | Design approaches, file plans, build sequences | `agents/code-architect.md` |
| `code-reviewer` | Review for bugs, simplicity, conventions | `agents/code-reviewer.md` |
| `develop` | Implement code, run tests | (uses global config) |
| Agent | Purpose | Needs --worktree |
|-------|---------|------------------|
| `code-explorer` | Trace code, map architecture, find patterns | No (read-only) |
| `code-architect` | Design approaches, file plans, build sequences | No (read-only) |
| `code-reviewer` | Review for bugs, simplicity, conventions | No (read-only) |
| `develop` | Implement code, run tests | **Yes** (if worktree enabled) |
## Issue Severity Definitions
**Blocking issues** (require user input):
- Impacts core functionality or correctness
- Security vulnerabilities
- Architectural conflicts with existing patterns
- Ambiguous requirements with multiple valid interpretations
**Minor issues** (auto-fix without asking):
- Code style inconsistencies
- Naming improvements
- Missing documentation
- Non-critical test coverage gaps
## Context Pack Template
@@ -64,7 +117,7 @@ To abort early, set `active: false` in the state file.
<verbatim request>
## Context Pack
- Phase: <1-7 name>
- Phase: <1-5 name>
- Decisions: <requirements/constraints/choices>
- Code-explorer output: <paste or "None">
- Code-architect output: <paste or "None">
@@ -79,18 +132,21 @@ To abort early, set `active: false` in the state file.
<checkable outputs>
```
## 7-Phase Workflow
## 5-Phase Workflow
### Phase 1: Discovery
### Phase 1: Understand (Parallel, No Interaction)
**Goal:** Understand what to build.
**Goal:** Understand requirements and map codebase simultaneously.
**Actions:**
1. Use AskUserQuestion for: user-visible behavior, scope, constraints, acceptance criteria
2. Invoke `code-architect` to draft requirements checklist and clarifying questions
**Actions:** Run `code-architect` and 2-3 `code-explorer` tasks in parallel.
```bash
codeagent-wrapper --agent code-architect - . <<'EOF'
codeagent-wrapper --parallel <<'EOF'
---TASK---
id: p1_requirements
agent: code-architect
workdir: .
---CONTENT---
## Original User Request
/do <request>
@@ -99,33 +155,29 @@ codeagent-wrapper --agent code-architect - . <<'EOF'
- Code-architect output: None
## Current Task
Produce requirements checklist and identify missing information.
Output: Requirements, Non-goals, Risks, Acceptance criteria, Questions (<= 10)
1. Analyze requirements completeness (score 1-10)
2. Extract explicit requirements, constraints, acceptance criteria
3. Identify blocking questions (issues that prevent implementation)
4. Identify minor clarifications (nice-to-have but can proceed without)
Output format:
- Completeness score: X/10
- Requirements: [list]
- Non-goals: [list]
- Blocking questions: [list, if any]
- Minor clarifications: [list, if any]
## Acceptance Criteria
Concrete, testable checklist; specific questions; no implementation.
EOF
```
Concrete checklist; blocking vs minor questions clearly separated.
### Phase 2: Exploration
**Goal:** Map codebase patterns and extension points.
**Actions:** Run 2-3 `code-explorer` tasks in parallel (similar features, architecture, tests/conventions).
```bash
codeagent-wrapper --parallel <<'EOF'
---TASK---
id: p2_similar_features
id: p1_similar_features
agent: code-explorer
workdir: .
---CONTENT---
## Original User Request
/do <request>
## Context Pack
- Code-architect output: <Phase 1 output>
## Current Task
Find 1-3 similar features, trace end-to-end. Return: key files with line numbers, call flow, extension points.
@@ -133,16 +185,13 @@ Find 1-3 similar features, trace end-to-end. Return: key files with line numbers
Concrete file:line map + reuse points.
---TASK---
id: p2_architecture
id: p1_architecture
agent: code-explorer
workdir: .
---CONTENT---
## Original User Request
/do <request>
## Context Pack
- Code-architect output: <Phase 1 output>
## Current Task
Map architecture for relevant subsystem. Return: module map + 5-10 key files.
@@ -150,16 +199,13 @@ Map architecture for relevant subsystem. Return: module map + 5-10 key files.
Clear boundaries; file:line references.
---TASK---
id: p2_conventions
id: p1_conventions
agent: code-explorer
workdir: .
---CONTENT---
## Original User Request
/do <request>
## Context Pack
- Code-architect output: <Phase 1 output>
## Current Task
Identify testing patterns, conventions, config. Return: test commands + file locations.
@@ -168,86 +214,74 @@ Test commands + relevant test file paths.
EOF
```
### Phase 3: Clarification (MANDATORY)
### Phase 2: Clarify (Conditional)
**Goal:** Resolve all ambiguities before design.
**Goal:** Resolve blocking ambiguities only.
**Actions:**
1. Invoke `code-architect` to generate prioritized questions from Phase 1+2 outputs
2. Use AskUserQuestion to present questions and wait for answers
3. **Do not proceed until answered or defaults accepted**
### Phase 4: Architecture
**Goal:** Produce implementation plan fitting existing patterns.
**Actions:** Run 2 `code-architect` tasks in parallel (minimal-change vs pragmatic-clean).
1. Review `p1_requirements` output for blocking questions
2. **IF blocking questions exist** → Use AskUserQuestion
3. **IF no blocking questions (completeness >= 8)** → Skip to Phase 3, log "Requirements clear, proceeding"
```bash
codeagent-wrapper --parallel <<'EOF'
---TASK---
id: p4_minimal
agent: code-architect
workdir: .
---CONTENT---
# Only if blocking questions exist:
# Use AskUserQuestion with the blocking questions from Phase 1
```
### Phase 3: Design (No Interaction)
**Goal:** Produce minimal-change implementation plan.
**Actions:** Invoke `code-architect` with all Phase 1 context to generate a single implementation plan.
```bash
codeagent-wrapper --agent code-architect - . <<'EOF'
## Original User Request
/do <request>
## Context Pack
- Code-explorer output: <ALL Phase 2 outputs>
- Code-architect output: <Phase 1 + Phase 3 answers>
- Code-explorer output: <ALL Phase 1 explorer outputs>
- Code-architect output: <Phase 1 requirements + Phase 2 answers if any>
## Current Task
Propose minimal-change architecture: reuse existing abstractions, minimize new files.
Output: file touch list, risks, edge cases.
Design minimal-change implementation:
- Reuse existing abstractions
- Minimize new files
- Follow established patterns from code-explorer output
Output:
- File touch list with specific changes
- Build sequence
- Test plan
- Risks and mitigations
## Acceptance Criteria
Concrete blueprint; minimal moving parts.
---TASK---
id: p4_pragmatic
agent: code-architect
workdir: .
---CONTENT---
## Original User Request
/do <request>
## Context Pack
- Code-explorer output: <ALL Phase 2 outputs>
- Code-architect output: <Phase 1 + Phase 3 answers>
## Current Task
Propose pragmatic-clean architecture: introduce seams for testability.
Output: file touch list, testing plan, risks.
## Acceptance Criteria
Implementable blueprint with build sequence and tests.
Concrete, implementable blueprint with minimal moving parts.
EOF
```
Use AskUserQuestion to let user choose approach.
### Phase 4: Implement + Review (Single Interaction Point)
### Phase 5: Implementation (Approval Required)
**Goal:** Build the feature.
**Goal:** Build feature and review in one phase.
**Actions:**
1. Use AskUserQuestion: "Approve starting implementation?" (Approve / Not yet)
2. If approved, invoke `develop`:
1. Invoke `develop` to implement (add `--worktree` if `use_worktree: true`):
```bash
codeagent-wrapper --agent develop - . <<'EOF'
# Check use_worktree from state file, add --worktree if true
codeagent-wrapper --worktree --agent develop - . <<'EOF'
## Original User Request
/do <request>
## Context Pack
- Code-explorer output: <ALL Phase 2 outputs>
- Code-architect output: <selected Phase 4 blueprint + Phase 3 answers>
- Code-explorer output: <ALL Phase 1 outputs>
- Code-architect output: <Phase 3 blueprint>
## Current Task
Implement with minimal change set following chosen architecture.
- Follow Phase 2 patterns
- Add/adjust tests per Phase 4 plan
Implement with minimal change set following the blueprint.
- Follow Phase 1 patterns
- Add/adjust tests per Phase 3 plan
- Run narrowest relevant tests
## Acceptance Criteria
@@ -255,16 +289,12 @@ Feature works end-to-end; tests pass; diff is minimal.
EOF
```
### Phase 6: Review
**Goal:** Catch defects and unnecessary complexity.
**Actions:** Run 2-3 `code-reviewer` tasks in parallel (correctness, simplicity).
2. Run parallel reviews (no --worktree needed, read-only):
```bash
codeagent-wrapper --parallel <<'EOF'
---TASK---
id: p6_correctness
id: p4_correctness
agent: code-reviewer
workdir: .
---CONTENT---
@@ -272,17 +302,18 @@ workdir: .
/do <request>
## Context Pack
- Code-architect output: <Phase 4 blueprint>
- Develop output: <Phase 5 output>
- Code-architect output: <Phase 3 blueprint>
- Develop output: <implementation output>
## Current Task
Review for correctness, edge cases, failure modes. Assume adversarial inputs.
Review for correctness, edge cases, failure modes.
Classify each issue as BLOCKING or MINOR.
## Acceptance Criteria
Issues with file:line references and concrete fixes.
Issues with file:line references, severity, and concrete fixes.
---TASK---
id: p6_simplicity
id: p4_simplicity
agent: code-reviewer
workdir: .
---CONTENT---
@@ -290,20 +321,23 @@ workdir: .
/do <request>
## Context Pack
- Code-architect output: <Phase 4 blueprint>
- Develop output: <Phase 5 output>
- Code-architect output: <Phase 3 blueprint>
- Develop output: <implementation output>
## Current Task
Review for KISS: remove bloat, collapse needless abstractions.
Classify each issue as BLOCKING or MINOR.
## Acceptance Criteria
Actionable simplifications with justification.
Actionable simplifications with severity and justification.
EOF
```
Use AskUserQuestion: Fix now / Fix later / Proceed as-is.
3. Handle review results:
- **MINOR issues only** → Auto-fix via `develop` (with `--worktree` if enabled), no user interaction
- **BLOCKING issues** → Use AskUserQuestion: "Fix now / Proceed as-is"
### Phase 7: Summary
### Phase 5: Complete (No Interaction)
**Goal:** Document what was built.
@@ -315,9 +349,9 @@ codeagent-wrapper --agent code-reviewer - . <<'EOF'
/do <request>
## Context Pack
- Code-architect output: <Phase 4 blueprint>
- Code-reviewer output: <Phase 6 outcomes>
- Develop output: <Phase 5 output + fixes>
- Code-architect output: <Phase 3 blueprint>
- Code-reviewer output: <Phase 4 review outcomes>
- Develop output: <Phase 4 implementation + fixes>
## Current Task
Write completion summary:
@@ -331,3 +365,8 @@ Write completion summary:
Short, technical, actionable summary.
EOF
```
Output the completion signal:
```
<promise>DO_COMPLETE</promise>
```

View File

@@ -1,12 +1,12 @@
{
"description": "do loop hook for 7-phase workflow",
"description": "do loop hook for 5-phase workflow",
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh"
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.py"
}
]
}

144
skills/do/hooks/stop-hook.py Executable file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
import glob
import json
import os
import re
import sys
PHASE_NAMES = {
1: "Understand",
2: "Clarify",
3: "Design",
4: "Implement",
5: "Complete",
}
def phase_name_for(n: int) -> str:
return PHASE_NAMES.get(n, f"Phase {n}")
def frontmatter_get(file_path: str, key: str) -> str:
try:
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
except Exception:
return ""
if not lines or lines[0].strip() != "---":
return ""
for i, line in enumerate(lines[1:], start=1):
if line.strip() == "---":
break
match = re.match(rf"^{re.escape(key)}:\s*(.*)$", line)
if match:
value = match.group(1).strip()
if value.startswith('"') and value.endswith('"'):
value = value[1:-1]
return value
return ""
def get_body(file_path: str) -> str:
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
except Exception:
return ""
parts = content.split("---", 2)
if len(parts) >= 3:
return parts[2]
return ""
def check_state_file(state_file: str, stdin_payload: str) -> str:
active_raw = frontmatter_get(state_file, "active")
active_lc = active_raw.lower()
if active_lc not in ("true", "1", "yes", "on"):
return ""
current_phase_raw = frontmatter_get(state_file, "current_phase")
max_phases_raw = frontmatter_get(state_file, "max_phases")
phase_name = frontmatter_get(state_file, "phase_name")
completion_promise = frontmatter_get(state_file, "completion_promise")
try:
current_phase = int(current_phase_raw)
except (ValueError, TypeError):
current_phase = 1
try:
max_phases = int(max_phases_raw)
except (ValueError, TypeError):
max_phases = 5
if not phase_name:
phase_name = phase_name_for(current_phase)
if not completion_promise:
completion_promise = "<promise>DO_COMPLETE</promise>"
phases_done = current_phase >= max_phases
promise_met = False
if completion_promise:
if stdin_payload and completion_promise in stdin_payload:
promise_met = True
else:
body = get_body(state_file)
if body and completion_promise in body:
promise_met = True
if phases_done and promise_met:
try:
os.remove(state_file)
except Exception:
pass
return ""
if not phases_done:
return (f"do loop incomplete: current phase {current_phase}/{max_phases} ({phase_name}). "
f"Continue with remaining phases; update {state_file} current_phase/phase_name after each phase. "
f"Include completion_promise in final output when done: {completion_promise}. "
f"To exit early, set active to false.")
else:
return (f"do reached final phase (current_phase={current_phase} / max_phases={max_phases}, "
f"phase_name={phase_name}), but completion_promise not detected: {completion_promise}. "
f"Please include this marker in your final output (or write it to {state_file} body), "
f"then finish; to force exit, set active to false.")
def main():
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
state_dir = os.path.join(project_dir, ".claude")
do_task_id = os.environ.get("DO_TASK_ID", "")
if do_task_id:
candidate = os.path.join(state_dir, f"do.{do_task_id}.local.md")
state_files = [candidate] if os.path.isfile(candidate) else []
else:
state_files = glob.glob(os.path.join(state_dir, "do.*.local.md"))
if not state_files:
sys.exit(0)
stdin_payload = ""
if not sys.stdin.isatty():
try:
stdin_payload = sys.stdin.read()
except Exception:
pass
blocking_reasons = []
for state_file in state_files:
reason = check_state_file(state_file, stdin_payload)
if reason:
blocking_reasons.append(reason)
if not blocking_reasons:
sys.exit(0)
combined_reason = " ".join(blocking_reasons)
print(json.dumps({"decision": "block", "reason": combined_reason}))
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -1,160 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
phase_name_for() {
case "${1:-}" in
1) echo "Discovery" ;;
2) echo "Exploration" ;;
3) echo "Clarification" ;;
4) echo "Architecture" ;;
5) echo "Implementation" ;;
6) echo "Review" ;;
7) echo "Summary" ;;
*) echo "Phase ${1:-unknown}" ;;
esac
}
json_escape() {
local s="${1:-}"
s=${s//\\/\\\\}
s=${s//\"/\\\"}
s=${s//$'\n'/\\n}
s=${s//$'\r'/\\r}
s=${s//$'\t'/\\t}
printf "%s" "$s"
}
project_dir="${CLAUDE_PROJECT_DIR:-$PWD}"
state_dir="${project_dir}/.claude"
shopt -s nullglob
if [ -n "${DO_TASK_ID:-}" ]; then
candidate="${state_dir}/do.${DO_TASK_ID}.local.md"
if [ -f "$candidate" ]; then
state_files=("$candidate")
else
state_files=()
fi
else
state_files=("${state_dir}"/do.*.local.md)
fi
shopt -u nullglob
if [ ${#state_files[@]} -eq 0 ]; then
exit 0
fi
stdin_payload=""
if [ ! -t 0 ]; then
stdin_payload="$(cat || true)"
fi
frontmatter_get() {
local file="$1" key="$2"
awk -v k="$key" '
BEGIN { in_fm=0 }
NR==1 && $0=="---" { in_fm=1; next }
in_fm==1 && $0=="---" { exit }
in_fm==1 {
if ($0 ~ "^"k":[[:space:]]*") {
sub("^"k":[[:space:]]*", "", $0)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0)
if ($0 ~ /^".*"$/) { sub(/^"/, "", $0); sub(/"$/, "", $0) }
print $0
exit
}
}
' "$file"
}
check_state_file() {
local state_file="$1"
local active_raw active_lc
active_raw="$(frontmatter_get "$state_file" active || true)"
active_lc="$(printf "%s" "$active_raw" | tr '[:upper:]' '[:lower:]')"
case "$active_lc" in
true|1|yes|on) ;;
*) return 0 ;;
esac
local current_phase_raw max_phases_raw phase_name completion_promise
current_phase_raw="$(frontmatter_get "$state_file" current_phase || true)"
max_phases_raw="$(frontmatter_get "$state_file" max_phases || true)"
phase_name="$(frontmatter_get "$state_file" phase_name || true)"
completion_promise="$(frontmatter_get "$state_file" completion_promise || true)"
local current_phase=1
if [[ "${current_phase_raw:-}" =~ ^[0-9]+$ ]]; then
current_phase="$current_phase_raw"
fi
local max_phases=7
if [[ "${max_phases_raw:-}" =~ ^[0-9]+$ ]]; then
max_phases="$max_phases_raw"
fi
if [ -z "${phase_name:-}" ]; then
phase_name="$(phase_name_for "$current_phase")"
fi
if [ -z "${completion_promise:-}" ]; then
completion_promise="<promise>DO_COMPLETE</promise>"
fi
local phases_done=0
if [ "$current_phase" -ge "$max_phases" ]; then
phases_done=1
fi
local promise_met=0
if [ -n "$completion_promise" ]; then
if [ -n "$stdin_payload" ] && printf "%s" "$stdin_payload" | grep -Fq -- "$completion_promise"; then
promise_met=1
else
local body
body="$(
awk '
BEGIN { in_fm=0; body=0 }
NR==1 && $0=="---" { in_fm=1; next }
in_fm==1 && $0=="---" { body=1; in_fm=0; next }
body==1 { print }
' "$state_file"
)"
if [ -n "$body" ] && printf "%s" "$body" | grep -Fq -- "$completion_promise"; then
promise_met=1
fi
fi
fi
if [ "$phases_done" -eq 1 ] && [ "$promise_met" -eq 1 ]; then
rm -f "$state_file"
return 0
fi
local reason
if [ "$phases_done" -eq 0 ]; then
reason="do loop incomplete: current phase ${current_phase}/${max_phases} (${phase_name}). Continue with remaining phases; update ${state_file} current_phase/phase_name after each phase. Include completion_promise in final output when done: ${completion_promise}. To exit early, set active to false."
else
reason="do reached final phase (current_phase=${current_phase} / max_phases=${max_phases}, phase_name=${phase_name}), but completion_promise not detected: ${completion_promise}. Please include this marker in your final output (or write it to ${state_file} body), then finish; to force exit, set active to false."
fi
printf "%s" "$reason"
}
blocking_reasons=()
for state_file in "${state_files[@]}"; do
reason="$(check_state_file "$state_file")"
if [ -n "$reason" ]; then
blocking_reasons+=("$reason")
fi
done
if [ ${#blocking_reasons[@]} -eq 0 ]; then
exit 0
fi
combined_reason="${blocking_reasons[*]}"
printf '{"decision":"block","reason":"%s"}\n' "$(json_escape "$combined_reason")"
exit 0

164
skills/do/install.py Executable file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Install/uninstall do skill to ~/.claude/skills/do"""
import argparse
import json
import os
import shutil
import sys
from pathlib import Path
SKILL_NAME = "do"
HOOK_PATH = "~/.claude/skills/do/hooks/stop-hook.py"
MODELS_JSON_TEMPLATE = {
"agents": {
"code-explorer": {
"backend": "claude",
"model": "claude-sonnet-4-5-20250929"
},
"code-architect": {
"backend": "claude",
"model": "claude-sonnet-4-5-20250929"
},
"code-reviewer": {
"backend": "claude",
"model": "claude-sonnet-4-5-20250929"
}
}
}
def get_settings_path() -> Path:
return Path.home() / ".claude" / "settings.json"
def load_settings() -> dict:
path = get_settings_path()
if path.exists():
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def save_settings(settings: dict):
path = get_settings_path()
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(settings, f, indent=2)
def add_hook(settings: dict) -> dict:
hook_command = str(Path(HOOK_PATH).expanduser())
hook_entry = {
"type": "command",
"command": f"python3 {hook_command}"
}
if "hooks" not in settings:
settings["hooks"] = {}
if "Stop" not in settings["hooks"]:
settings["hooks"]["Stop"] = []
stop_hooks = settings["hooks"]["Stop"]
for item in stop_hooks:
if "hooks" in item:
for h in item["hooks"]:
if "stop-hook" in h.get("command", "") and "do" in h.get("command", ""):
h["command"] = f"python3 {hook_command}"
return settings
stop_hooks.append({"hooks": [hook_entry]})
return settings
def remove_hook(settings: dict) -> dict:
if "hooks" not in settings or "Stop" not in settings["hooks"]:
return settings
stop_hooks = settings["hooks"]["Stop"]
new_stop_hooks = []
for item in stop_hooks:
if "hooks" in item:
filtered = [h for h in item["hooks"]
if "stop-hook" not in h.get("command", "")
or "do" not in h.get("command", "")]
if filtered:
item["hooks"] = filtered
new_stop_hooks.append(item)
else:
new_stop_hooks.append(item)
settings["hooks"]["Stop"] = new_stop_hooks
if not settings["hooks"]["Stop"]:
del settings["hooks"]["Stop"]
if not settings["hooks"]:
del settings["hooks"]
return settings
def install_models_json():
"""Install ~/.codeagent/models.json if not exists"""
path = Path.home() / ".codeagent" / "models.json"
if path.exists():
print(f"{path} already exists, skipping")
return
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(MODELS_JSON_TEMPLATE, f, indent=2)
print(f"✓ Created {path}")
def install():
src = Path(__file__).parent.resolve()
dest = Path.home() / ".claude" / "skills" / SKILL_NAME
dest.mkdir(parents=True, exist_ok=True)
exclude = {".git", "__pycache__", ".DS_Store", "install.py"}
for item in src.iterdir():
if item.name in exclude:
continue
target = dest / item.name
if target.exists():
if target.is_dir():
shutil.rmtree(target)
else:
target.unlink()
if item.is_dir():
shutil.copytree(item, target)
else:
shutil.copy2(item, target)
settings = load_settings()
settings = add_hook(settings)
save_settings(settings)
install_models_json()
print(f"✓ Installed to {dest}")
print(f"✓ Hook added to settings.json")
def uninstall():
dest = Path.home() / ".claude" / "skills" / SKILL_NAME
settings = load_settings()
settings = remove_hook(settings)
save_settings(settings)
print(f"✓ Hook removed from settings.json")
if dest.exists():
shutil.rmtree(dest)
print(f"✓ Removed {dest}")
else:
print(f"{dest} not found")
def main():
parser = argparse.ArgumentParser(description="Install/uninstall do skill")
parser.add_argument("--uninstall", "-u", action="store_true", help="Uninstall the skill")
args = parser.parse_args()
if args.uninstall:
uninstall()
else:
install()
if __name__ == "__main__":
main()

85
skills/do/scripts/setup-do.py Executable file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import argparse
import os
import secrets
import sys
import time
PHASE_NAMES = {
1: "Understand",
2: "Clarify",
3: "Design",
4: "Implement",
5: "Complete",
}
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)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="Creates (or overwrites) project state file: .claude/do.local.md"
)
parser.add_argument("--max-phases", type=int, default=5, help="Default: 5")
parser.add_argument(
"--completion-promise",
default="<promise>DO_COMPLETE</promise>",
help="Default: <promise>DO_COMPLETE</promise>",
)
parser.add_argument("--worktree", action="store_true", help="Enable worktree mode")
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:
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")
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")
os.makedirs(state_dir, exist_ok=True)
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()}
---
# 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 __name__ == "__main__":
main()

View File

@@ -1,115 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: setup-do.sh [options] PROMPT...
Creates (or overwrites) project state file:
.claude/do.local.md
Options:
--max-phases N Default: 7
--completion-promise STR Default: <promise>DO_COMPLETE</promise>
-h, --help Show this help
EOF
}
die() {
echo "$*" >&2
exit 1
}
phase_name_for() {
case "${1:-}" in
1) echo "Discovery" ;;
2) echo "Exploration" ;;
3) echo "Clarification" ;;
4) echo "Architecture" ;;
5) echo "Implementation" ;;
6) echo "Review" ;;
7) echo "Summary" ;;
*) echo "Phase ${1:-unknown}" ;;
esac
}
max_phases=7
completion_promise="<promise>DO_COMPLETE</promise>"
declare -a prompt_parts=()
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
usage
exit 0
;;
--max-phases)
[ $# -ge 2 ] || die "--max-phases requires a value"
max_phases="$2"
shift 2
;;
--completion-promise)
[ $# -ge 2 ] || die "--completion-promise requires a value"
completion_promise="$2"
shift 2
;;
--)
shift
while [ $# -gt 0 ]; do
prompt_parts+=("$1")
shift
done
break
;;
-*)
die "Unknown argument: $1 (use --help)"
;;
*)
prompt_parts+=("$1")
shift
;;
esac
done
prompt="${prompt_parts[*]:-}"
[ -n "$prompt" ] || die "PROMPT is required (use --help)"
if ! [[ "$max_phases" =~ ^[0-9]+$ ]] || [ "$max_phases" -lt 1 ]; then
die "--max-phases must be a positive integer"
fi
project_dir="${CLAUDE_PROJECT_DIR:-$PWD}"
state_dir="${project_dir}/.claude"
task_id="$(date +%s)-$$-$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n')"
state_file="${state_dir}/do.${task_id}.local.md"
mkdir -p "$state_dir"
phase_name="$(phase_name_for 1)"
cat > "$state_file" << EOF
---
active: true
current_phase: 1
phase_name: "$phase_name"
max_phases: $max_phases
completion_promise: "$completion_promise"
---
# 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
EOF
echo "Initialized: $state_file"
echo "task_id: $task_id"
echo "phase: 1/$max_phases ($phase_name)"
echo "completion_promise: $completion_promise"
echo "export DO_TASK_ID=$task_id"