diff --git a/README.md b/README.md index d46350b..7bacdfd 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,59 @@ Requirements → Architecture → Sprint Plan → Development → Review → QA --- +## Version Requirements + +### Codex CLI +**Minimum version:** Check compatibility with your installation + +The codeagent-wrapper uses these Codex CLI features: +- `codex e` - Execute commands (shorthand for `codex exec`) +- `--skip-git-repo-check` - Skip git repository validation +- `--json` - JSON stream output format +- `-C ` - Set working directory +- `resume ` - Resume previous sessions + +**Verify Codex CLI is installed:** +```bash +which codex +codex --version +``` + +### Claude CLI +**Minimum version:** Check compatibility with your installation + +Required features: +- `--output-format stream-json` - Streaming JSON output format +- `--setting-sources` - Control setting sources (prevents infinite recursion) +- `--dangerously-skip-permissions` - Skip permission prompts (use with caution) +- `-p` - Prompt input flag +- `-r ` - Resume sessions + +**Security Note:** The wrapper only adds `--dangerously-skip-permissions` for Claude when explicitly enabled (e.g. `--skip-permissions` / `CODEAGENT_SKIP_PERMISSIONS=true`). Keep it disabled unless you understand the risk. + +**Verify Claude CLI is installed:** +```bash +which claude +claude --version +``` + +### Gemini CLI +**Minimum version:** Check compatibility with your installation + +Required features: +- `-o stream-json` - JSON stream output format +- `-y` - Auto-approve prompts (non-interactive mode) +- `-r ` - Resume sessions +- `-p` - Prompt input flag + +**Verify Gemini CLI is installed:** +```bash +which gemini +gemini --version +``` + +--- + ## Installation ### Modular Installation (Recommended) @@ -163,15 +216,39 @@ python3 install.py --force ``` ~/.claude/ -├── CLAUDE.md # Core instructions and role definition -├── commands/ # Slash commands (/dev, /code, etc.) -├── agents/ # Agent definitions +├── bin/ +│ └── codeagent-wrapper # Main executable +├── CLAUDE.md # Core instructions and role definition +├── commands/ # Slash commands (/dev, /code, etc.) +├── agents/ # Agent definitions ├── skills/ │ └── codex/ -│ └── SKILL.md # Codex integration skill -└── installed_modules.json # Installation status +│ └── SKILL.md # Codex integration skill +├── config.json # Configuration +└── installed_modules.json # Installation status ``` +### Customizing Installation Directory + +By default, myclaude installs to `~/.claude`. You can customize this using the `INSTALL_DIR` environment variable: + +```bash +# Install to custom directory +INSTALL_DIR=/opt/myclaude bash install.sh + +# Update your PATH accordingly +export PATH="/opt/myclaude/bin:$PATH" +``` + +**Directory Structure:** +- `$INSTALL_DIR/bin/` - codeagent-wrapper binary +- `$INSTALL_DIR/skills/` - Skill definitions +- `$INSTALL_DIR/config.json` - Configuration file +- `$INSTALL_DIR/commands/` - Slash command definitions +- `$INSTALL_DIR/agents/` - Agent definitions + +**Note:** When using a custom installation directory, ensure that `$INSTALL_DIR/bin` is added to your `PATH` environment variable. + ### Configuration Edit `config.json` to customize: @@ -295,7 +372,7 @@ setx PATH "%USERPROFILE%\bin;%PATH%" **Codex wrapper not found:** ```bash # Check PATH -echo $PATH | grep -q "$HOME/bin" || echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc +echo $PATH | grep -q "$HOME/.claude/bin" || echo 'export PATH="$HOME/.claude/bin:$PATH"' >> ~/.zshrc # Reinstall bash install.sh @@ -315,6 +392,71 @@ cat ~/.claude/installed_modules.json python3 install.py --module dev --force ``` +### Version Compatibility Issues + +**Backend CLI not found:** +```bash +# Check if backend CLIs are installed +which codex +which claude +which gemini + +# Install missing backends +# Codex: Follow installation instructions at https://codex.docs +# Claude: Follow installation instructions at https://claude.ai/docs +# Gemini: Follow installation instructions at https://ai.google.dev/docs +``` + +**Unsupported CLI flags:** +```bash +# If you see errors like "unknown flag" or "invalid option" + +# Check backend CLI version +codex --version +claude --version +gemini --version + +# For Codex: Ensure it supports `e`, `--skip-git-repo-check`, `--json`, `-C`, and `resume` +# For Claude: Ensure it supports `--output-format stream-json`, `--setting-sources`, `-r` +# For Gemini: Ensure it supports `-o stream-json`, `-y`, `-r`, `-p` + +# Update your backend CLI to the latest version if needed +``` + +**JSON parsing errors:** +```bash +# If you see "failed to parse JSON output" errors + +# Verify the backend outputs stream-json format +codex e --json "test task" # Should output newline-delimited JSON +claude --output-format stream-json -p "test" # Should output stream JSON + +# If not, your backend CLI version may be too old or incompatible +``` + +**Infinite recursion with Claude backend:** +```bash +# The wrapper prevents this with `--setting-sources ""` flag +# If you still see recursion, ensure your Claude CLI supports this flag + +claude --help | grep "setting-sources" + +# If flag is not supported, upgrade Claude CLI +``` + +**Session resume failures:** +```bash +# Check if session ID is valid +codex history # List recent sessions +claude history + +# Ensure backend CLI supports session resumption +codex resume "test" # Should continue from previous session +claude -r "test" + +# If not supported, use new sessions instead of resume mode +``` + --- ## Documentation diff --git a/README_CN.md b/README_CN.md index b2c8f9d..0ac6de8 100644 --- a/README_CN.md +++ b/README_CN.md @@ -152,15 +152,39 @@ python3 install.py --force ``` ~/.claude/ -├── CLAUDE.md # 核心指令和角色定义 -├── commands/ # 斜杠命令 (/dev, /code 等) -├── agents/ # 智能体定义 +├── bin/ +│ └── codeagent-wrapper # 主可执行文件 +├── CLAUDE.md # 核心指令和角色定义 +├── commands/ # 斜杠命令 (/dev, /code 等) +├── agents/ # 智能体定义 ├── skills/ │ └── codex/ -│ └── SKILL.md # Codex 集成技能 -└── installed_modules.json # 安装状态 +│ └── SKILL.md # Codex 集成技能 +├── config.json # 配置文件 +└── installed_modules.json # 安装状态 ``` +### 自定义安装目录 + +默认情况下,myclaude 安装到 `~/.claude`。您可以使用 `INSTALL_DIR` 环境变量自定义安装目录: + +```bash +# 安装到自定义目录 +INSTALL_DIR=/opt/myclaude bash install.sh + +# 相应更新您的 PATH +export PATH="/opt/myclaude/bin:$PATH" +``` + +**目录结构:** +- `$INSTALL_DIR/bin/` - codeagent-wrapper 可执行文件 +- `$INSTALL_DIR/skills/` - 技能定义 +- `$INSTALL_DIR/config.json` - 配置文件 +- `$INSTALL_DIR/commands/` - 斜杠命令定义 +- `$INSTALL_DIR/agents/` - 智能体定义 + +**注意:** 使用自定义安装目录时,请确保将 `$INSTALL_DIR/bin` 添加到您的 `PATH` 环境变量中。 + ### 配置 编辑 `config.json` 自定义: @@ -284,7 +308,7 @@ setx PATH "%USERPROFILE%\bin;%PATH%" **Codex wrapper 未找到:** ```bash # 检查 PATH -echo $PATH | grep -q "$HOME/bin" || echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc +echo $PATH | grep -q "$HOME/.claude/bin" || echo 'export PATH="$HOME/.claude/bin:$PATH"' >> ~/.zshrc # 重新安装 bash install.sh diff --git a/codeagent-wrapper/backend.go b/codeagent-wrapper/backend.go index 55526a1..2e6f42d 100644 --- a/codeagent-wrapper/backend.go +++ b/codeagent-wrapper/backend.go @@ -26,15 +26,17 @@ func (ClaudeBackend) Command() string { return "claude" } func (ClaudeBackend) BuildArgs(cfg *Config, targetArg string) []string { + return buildClaudeArgs(cfg, targetArg) +} + +func buildClaudeArgs(cfg *Config, targetArg string) []string { if cfg == nil { return nil } - args := []string{"-p", "--dangerously-skip-permissions"} - - // Only skip permissions when explicitly requested - // if cfg.SkipPermissions { - // args = append(args, "--dangerously-skip-permissions") - // } + args := []string{"-p"} + if cfg.SkipPermissions { + args = append(args, "--dangerously-skip-permissions") + } // Prevent infinite recursion: disable all setting sources (user, project, local) // This ensures a clean execution environment without CLAUDE.md or skills that would trigger codeagent @@ -60,6 +62,10 @@ func (GeminiBackend) Command() string { return "gemini" } func (GeminiBackend) BuildArgs(cfg *Config, targetArg string) []string { + return buildGeminiArgs(cfg, targetArg) +} + +func buildGeminiArgs(cfg *Config, targetArg string) []string { if cfg == nil { return nil } diff --git a/codeagent-wrapper/backend_test.go b/codeagent-wrapper/backend_test.go index 2509626..9e894e9 100644 --- a/codeagent-wrapper/backend_test.go +++ b/codeagent-wrapper/backend_test.go @@ -1,6 +1,7 @@ package main import ( + "os" "reflect" "testing" ) @@ -8,16 +9,16 @@ import ( func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) { backend := ClaudeBackend{} - t.Run("new mode uses workdir without skip by default", func(t *testing.T) { + t.Run("new mode omits skip-permissions by default", func(t *testing.T) { cfg := &Config{Mode: "new", WorkDir: "/repo"} got := backend.BuildArgs(cfg, "todo") - want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "todo"} + want := []string{"-p", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "todo"} if !reflect.DeepEqual(got, want) { t.Fatalf("got %v, want %v", got, want) } }) - t.Run("new mode opt-in skip permissions with default workdir", func(t *testing.T) { + t.Run("new mode can opt-in skip-permissions", func(t *testing.T) { cfg := &Config{Mode: "new", SkipPermissions: true} got := backend.BuildArgs(cfg, "-") want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "-"} @@ -26,10 +27,10 @@ func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) { } }) - t.Run("resume mode uses session id and omits workdir", func(t *testing.T) { + t.Run("resume mode includes session id", func(t *testing.T) { cfg := &Config{Mode: "resume", SessionID: "sid-123", WorkDir: "/ignored"} got := backend.BuildArgs(cfg, "resume-task") - want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "-r", "sid-123", "--output-format", "stream-json", "--verbose", "resume-task"} + want := []string{"-p", "--setting-sources", "", "-r", "sid-123", "--output-format", "stream-json", "--verbose", "resume-task"} if !reflect.DeepEqual(got, want) { t.Fatalf("got %v, want %v", got, want) } @@ -38,7 +39,16 @@ func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) { t.Run("resume mode without session still returns base flags", func(t *testing.T) { cfg := &Config{Mode: "resume", WorkDir: "/ignored"} got := backend.BuildArgs(cfg, "follow-up") - want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "follow-up"} + want := []string{"-p", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "follow-up"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } + }) + + t.Run("resume mode can opt-in skip permissions", func(t *testing.T) { + cfg := &Config{Mode: "resume", SessionID: "sid-123", SkipPermissions: true} + got := backend.BuildArgs(cfg, "resume-task") + want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "-r", "sid-123", "--output-format", "stream-json", "--verbose", "resume-task"} if !reflect.DeepEqual(got, want) { t.Fatalf("got %v, want %v", got, want) } @@ -89,7 +99,11 @@ func TestClaudeBuildArgs_GeminiAndCodexModes(t *testing.T) { } }) - t.Run("codex build args passthrough remains intact", func(t *testing.T) { + t.Run("codex build args omits bypass flag by default", func(t *testing.T) { + const key = "CODEX_BYPASS_SANDBOX" + t.Cleanup(func() { os.Unsetenv(key) }) + os.Unsetenv(key) + backend := CodexBackend{} cfg := &Config{Mode: "new", WorkDir: "/tmp"} got := backend.BuildArgs(cfg, "task") @@ -98,6 +112,20 @@ func TestClaudeBuildArgs_GeminiAndCodexModes(t *testing.T) { t.Fatalf("got %v, want %v", got, want) } }) + + t.Run("codex build args includes bypass flag when enabled", func(t *testing.T) { + const key = "CODEX_BYPASS_SANDBOX" + t.Cleanup(func() { os.Unsetenv(key) }) + os.Setenv(key, "true") + + backend := CodexBackend{} + cfg := &Config{Mode: "new", WorkDir: "/tmp"} + got := backend.BuildArgs(cfg, "task") + want := []string{"e", "--dangerously-bypass-approvals-and-sandbox", "--skip-git-repo-check", "-C", "/tmp", "--json", "task"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } + }) } func TestClaudeBuildArgs_BackendMetadata(t *testing.T) { diff --git a/codeagent-wrapper/config.go b/codeagent-wrapper/config.go index 4d20e9a..00d5a2a 100644 --- a/codeagent-wrapper/config.go +++ b/codeagent-wrapper/config.go @@ -164,6 +164,9 @@ func parseParallelConfig(data []byte) (*ParallelConfig, error) { if content == "" { return nil, fmt.Errorf("task block #%d (%q) missing content", taskIndex, task.ID) } + if task.Mode == "resume" && strings.TrimSpace(task.SessionID) == "" { + return nil, fmt.Errorf("task block #%d (%q) has empty session_id", taskIndex, task.ID) + } if _, exists := seen[task.ID]; exists { return nil, fmt.Errorf("task block #%d has duplicate id: %s", taskIndex, task.ID) } @@ -232,7 +235,10 @@ func parseArgs() (*Config, error) { return nil, fmt.Errorf("resume mode requires: resume ") } cfg.Mode = "resume" - cfg.SessionID = args[1] + cfg.SessionID = strings.TrimSpace(args[1]) + if cfg.SessionID == "" { + return nil, fmt.Errorf("resume mode requires non-empty session_id") + } cfg.Task = args[2] cfg.ExplicitStdin = (args[2] == "-") if len(args) > 3 { diff --git a/codeagent-wrapper/executor.go b/codeagent-wrapper/executor.go index 0762f3b..c515c04 100644 --- a/codeagent-wrapper/executor.go +++ b/codeagent-wrapper/executor.go @@ -509,23 +509,43 @@ func generateFinalOutput(results []TaskResult) string { } func buildCodexArgs(cfg *Config, targetArg string) []string { - if cfg.Mode == "resume" { - return []string{ - "e", - "--skip-git-repo-check", - "--json", - "resume", - cfg.SessionID, - targetArg, + if cfg == nil { + panic("buildCodexArgs: nil config") + } + + var resumeSessionID string + isResume := cfg.Mode == "resume" + if isResume { + resumeSessionID = strings.TrimSpace(cfg.SessionID) + if resumeSessionID == "" { + logError("invalid config: resume mode requires non-empty session_id") + isResume = false } } - return []string{ - "e", - "--skip-git-repo-check", + + args := []string{"e"} + + if envFlagEnabled("CODEX_BYPASS_SANDBOX") { + logWarn("CODEX_BYPASS_SANDBOX=true: running without approval/sandbox protection") + args = append(args, "--dangerously-bypass-approvals-and-sandbox") + } + + args = append(args, "--skip-git-repo-check") + + if isResume { + return append(args, + "--json", + "resume", + resumeSessionID, + targetArg, + ) + } + + return append(args, "-C", cfg.WorkDir, "--json", targetArg, - } + ) } func runCodexTask(taskSpec TaskSpec, silent bool, timeoutSec int) TaskResult { @@ -576,6 +596,12 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe cfg.WorkDir = defaultWorkdir } + if cfg.Mode == "resume" && strings.TrimSpace(cfg.SessionID) == "" { + result.ExitCode = 1 + result.Error = "resume mode requires non-empty session_id" + return result + } + useStdin := taskSpec.UseStdin targetArg := taskSpec.Task if useStdin { @@ -745,6 +771,10 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe default: } }) + select { + case completeSeen <- struct{}{}: + default: + } parseCh <- parseResult{message: msg, threadID: tid} }() diff --git a/codeagent-wrapper/executor_concurrent_test.go b/codeagent-wrapper/executor_concurrent_test.go index eee3c80..d45d5ad 100644 --- a/codeagent-wrapper/executor_concurrent_test.go +++ b/codeagent-wrapper/executor_concurrent_test.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "sync" "sync/atomic" @@ -244,6 +245,10 @@ func TestExecutorHelperCoverage(t *testing.T) { }) t.Run("generateFinalOutputAndArgs", func(t *testing.T) { + const key = "CODEX_BYPASS_SANDBOX" + t.Cleanup(func() { os.Unsetenv(key) }) + os.Unsetenv(key) + out := generateFinalOutput([]TaskResult{ {TaskID: "ok", ExitCode: 0}, {TaskID: "fail", ExitCode: 1, Error: "boom"}, @@ -257,11 +262,11 @@ func TestExecutorHelperCoverage(t *testing.T) { } args := buildCodexArgs(&Config{Mode: "new", WorkDir: "/tmp"}, "task") - if len(args) == 0 || args[3] != "/tmp" { + if !slices.Equal(args, []string{"e", "--skip-git-repo-check", "-C", "/tmp", "--json", "task"}) { t.Fatalf("unexpected codex args: %+v", args) } args = buildCodexArgs(&Config{Mode: "resume", SessionID: "sess"}, "target") - if args[3] != "resume" || args[4] != "sess" { + if !slices.Equal(args, []string{"e", "--skip-git-repo-check", "--json", "resume", "sess", "target"}) { t.Fatalf("unexpected resume args: %+v", args) } }) @@ -298,6 +303,18 @@ func TestExecutorRunCodexTaskWithContext(t *testing.T) { origRunner := newCommandRunner defer func() { newCommandRunner = origRunner }() + t.Run("resumeMissingSessionID", func(t *testing.T) { + newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner { + t.Fatalf("unexpected command execution for invalid resume config") + return nil + } + + res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: ".", Mode: "resume"}, nil, nil, false, false, 1) + if res.ExitCode == 0 || !strings.Contains(res.Error, "session_id") { + t.Fatalf("expected validation error, got %+v", res) + } + }) + t.Run("success", func(t *testing.T) { var firstStdout *reasonReadCloser newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner { diff --git a/codeagent-wrapper/main_test.go b/codeagent-wrapper/main_test.go index 337d9f1..1aa218b 100644 --- a/codeagent-wrapper/main_test.go +++ b/codeagent-wrapper/main_test.go @@ -1038,6 +1038,8 @@ func TestBackendParseArgs_ResumeMode(t *testing.T) { }, {name: "resume missing session_id", args: []string{"codeagent-wrapper", "resume"}, wantErr: true}, {name: "resume missing task", args: []string{"codeagent-wrapper", "resume", "session-123"}, wantErr: true}, + {name: "resume empty session_id", args: []string{"codeagent-wrapper", "resume", "", "task"}, wantErr: true}, + {name: "resume whitespace session_id", args: []string{"codeagent-wrapper", "resume", " ", "task"}, wantErr: true}, } for _, tt := range tests { @@ -1254,6 +1256,18 @@ do something` } } +func TestParallelParseConfig_EmptySessionID(t *testing.T) { + input := `---TASK--- +id: task-1 +session_id: +---CONTENT--- +do something` + + if _, err := parseParallelConfig([]byte(input)); err == nil { + t.Fatalf("expected error for empty session_id, got nil") + } +} + func TestParallelParseConfig_InvalidFormat(t *testing.T) { if _, err := parseParallelConfig([]byte("invalid format")); err == nil { t.Fatalf("expected error for invalid format, got nil") @@ -1354,9 +1368,19 @@ func TestRunShouldUseStdin(t *testing.T) { } func TestRunBuildCodexArgs_NewMode(t *testing.T) { + const key = "CODEX_BYPASS_SANDBOX" + t.Cleanup(func() { os.Unsetenv(key) }) + os.Unsetenv(key) + cfg := &Config{Mode: "new", WorkDir: "/test/dir"} args := buildCodexArgs(cfg, "my task") - expected := []string{"e", "--skip-git-repo-check", "-C", "/test/dir", "--json", "my task"} + expected := []string{ + "e", + "--skip-git-repo-check", + "-C", "/test/dir", + "--json", + "my task", + } if len(args) != len(expected) { t.Fatalf("len mismatch") } @@ -1368,9 +1392,20 @@ func TestRunBuildCodexArgs_NewMode(t *testing.T) { } func TestRunBuildCodexArgs_ResumeMode(t *testing.T) { + const key = "CODEX_BYPASS_SANDBOX" + t.Cleanup(func() { os.Unsetenv(key) }) + os.Unsetenv(key) + cfg := &Config{Mode: "resume", SessionID: "session-abc"} args := buildCodexArgs(cfg, "-") - expected := []string{"e", "--skip-git-repo-check", "--json", "resume", "session-abc", "-"} + expected := []string{ + "e", + "--skip-git-repo-check", + "--json", + "resume", + "session-abc", + "-", + } if len(args) != len(expected) { t.Fatalf("len mismatch") } @@ -1381,6 +1416,61 @@ func TestRunBuildCodexArgs_ResumeMode(t *testing.T) { } } +func TestRunBuildCodexArgs_ResumeMode_EmptySessionHandledGracefully(t *testing.T) { + const key = "CODEX_BYPASS_SANDBOX" + t.Cleanup(func() { os.Unsetenv(key) }) + os.Unsetenv(key) + + cfg := &Config{Mode: "resume", SessionID: " ", WorkDir: "/test/dir"} + args := buildCodexArgs(cfg, "task") + expected := []string{"e", "--skip-git-repo-check", "-C", "/test/dir", "--json", "task"} + if len(args) != len(expected) { + t.Fatalf("len mismatch") + } + for i := range args { + if args[i] != expected[i] { + t.Fatalf("args[%d]=%s, want %s", i, args[i], expected[i]) + } + } +} + +func TestRunBuildCodexArgs_BypassSandboxEnvTrue(t *testing.T) { + defer resetTestHooks() + tempDir := t.TempDir() + t.Setenv("TMPDIR", tempDir) + + logger, err := NewLogger() + if err != nil { + t.Fatalf("NewLogger() error = %v", err) + } + setLogger(logger) + defer closeLogger() + + t.Setenv("CODEX_BYPASS_SANDBOX", "true") + + cfg := &Config{Mode: "new", WorkDir: "/test/dir"} + args := buildCodexArgs(cfg, "my task") + found := false + for _, arg := range args { + if arg == "--dangerously-bypass-approvals-and-sandbox" { + found = true + break + } + } + if !found { + t.Fatalf("expected bypass flag in args, got %v", args) + } + + logger.Flush() + data, err := os.ReadFile(logger.Path()) + if err != nil { + t.Fatalf("failed to read log file: %v", err) + } + if !strings.Contains(string(data), "CODEX_BYPASS_SANDBOX=true") { + t.Fatalf("expected bypass warning log, got: %s", string(data)) + } +} + func TestBackendSelectBackend(t *testing.T) { tests := []struct { name string @@ -1436,7 +1526,13 @@ func TestBackendBuildArgs_CodexBackend(t *testing.T) { backend := CodexBackend{} cfg := &Config{Mode: "new", WorkDir: "/test/dir"} got := backend.BuildArgs(cfg, "task") - want := []string{"e", "--skip-git-repo-check", "-C", "/test/dir", "--json", "task"} + want := []string{ + "e", + "--skip-git-repo-check", + "-C", "/test/dir", + "--json", + "task", + } if len(got) != len(want) { t.Fatalf("length mismatch") } @@ -1451,7 +1547,7 @@ func TestBackendBuildArgs_ClaudeBackend(t *testing.T) { backend := ClaudeBackend{} cfg := &Config{Mode: "new", WorkDir: defaultWorkdir} got := backend.BuildArgs(cfg, "todo") - want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "todo"} + want := []string{"-p", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "todo"} if len(got) != len(want) { t.Fatalf("length mismatch") } @@ -1472,7 +1568,7 @@ func TestClaudeBackendBuildArgs_OutputValidation(t *testing.T) { target := "ensure-flags" args := backend.BuildArgs(cfg, target) - expectedPrefix := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose"} + expectedPrefix := []string{"-p", "--setting-sources", "", "--output-format", "stream-json", "--verbose"} if len(args) != len(expectedPrefix)+1 { t.Fatalf("args length=%d, want %d", len(args), len(expectedPrefix)+1) diff --git a/dev-workflow/commands/dev.md b/dev-workflow/commands/dev.md index 77e4beb..fa07660 100644 --- a/dev-workflow/commands/dev.md +++ b/dev-workflow/commands/dev.md @@ -2,9 +2,25 @@ description: Extreme lightweight end-to-end development workflow with requirements clarification, parallel codeagent execution, and mandatory 90% test coverage --- - You are the /dev Workflow Orchestrator, an expert development workflow manager specializing in orchestrating minimal, efficient end-to-end development processes with parallel task execution and rigorous test coverage validation. +--- + +## CRITICAL CONSTRAINTS (NEVER VIOLATE) + +These rules have HIGHEST PRIORITY and override all other instructions: + +1. **NEVER use Edit, Write, or MultiEdit tools directly** - ALL code changes MUST go through codeagent-wrapper +2. **MUST use AskUserQuestion in Step 1** - Do NOT skip requirement clarification +3. **MUST use TodoWrite after Step 1** - Create task tracking list before any analysis +4. **MUST use codeagent-wrapper for Step 2 analysis** - Do NOT use Read/Glob/Grep directly for deep analysis +5. **MUST wait for user confirmation in Step 3** - Do NOT proceed to Step 4 without explicit approval +6. **MUST invoke codeagent-wrapper --parallel for Step 4 execution** - Use Bash tool, NOT Edit/Write or Task tool + +**Violation of any constraint above invalidates the entire workflow. Stop and restart if violated.** + +--- + **Core Responsibilities** - Orchestrate a streamlined 6-step development workflow: 1. Requirement clarification through targeted questioning @@ -15,14 +31,35 @@ You are the /dev Workflow Orchestrator, an expert development workflow manager s 6. Completion summary **Workflow Execution** -- **Step 1: Requirement Clarification** - - Use AskUserQuestion to clarify requirements directly +- **Step 1: Requirement Clarification [MANDATORY - DO NOT SKIP]** + - MUST use AskUserQuestion tool as the FIRST action - no exceptions - Focus questions on functional boundaries, inputs/outputs, constraints, testing, and required unit-test coverage levels - Iterate 2-3 rounds until clear; rely on judgment; keep questions concise + - After clarification complete: MUST use TodoWrite to create task tracking list with workflow steps -- **Step 2: codeagent Deep Analysis (Plan Mode Style)** +- **Step 2: codeagent-wrapper Deep Analysis (Plan Mode Style) [USE CODEAGENT-WRAPPER ONLY]** - Use codeagent Skill to perform deep analysis. codeagent should operate in "plan mode" style and must include UI detection: + MUST use Bash tool to invoke `codeagent-wrapper` for deep analysis. Do NOT use Read/Glob/Grep tools directly - delegate all exploration to codeagent-wrapper. + + **How to invoke for analysis**: + ```bash + codeagent-wrapper --backend codex - <<'EOF' + Analyze the codebase for implementing [feature name]. + + Requirements: + - [requirement 1] + - [requirement 2] + + Deliverables: + 1. Explore codebase structure and existing patterns + 2. Evaluate implementation options with trade-offs + 3. Make architectural decisions + 4. Break down into 2-5 parallelizable tasks with dependencies + 5. Determine if UI work is needed (check for .css/.tsx/.vue files) + + Output the analysis following the structure below. + EOF + ``` **When Deep Analysis is Needed** (any condition triggers): - Multiple valid approaches exist (e.g., Redis vs in-memory vs file-based caching) @@ -34,7 +71,7 @@ You are the /dev Workflow Orchestrator, an expert development workflow manager s - During analysis, output whether the task needs UI work (yes/no) and the evidence - UI criteria: presence of style assets (.css, .scss, styled-components, CSS modules, tailwindcss) OR frontend component files (.tsx, .jsx, .vue) - **What codeagent Does in Analysis Mode**: + **What the AI backend does in Analysis Mode** (when invoked via codeagent-wrapper): 1. **Explore Codebase**: Use Glob, Grep, Read to understand structure, patterns, architecture 2. **Identify Existing Patterns**: Find how similar features are implemented, reuse conventions 3. **Evaluate Options**: When multiple approaches exist, list trade-offs (complexity, performance, security, maintainability) @@ -81,27 +118,39 @@ You are the /dev Workflow Orchestrator, an expert development workflow manager s - Options: "Confirm and execute" / "Need adjustments" - If user chooses "Need adjustments", return to Step 1 or Step 2 based on feedback -- **Step 4: Parallel Development Execution** - - For each task in `dev-plan.md`, invoke codeagent skill with task brief in HEREDOC format: +- **Step 4: Parallel Development Execution [CODEAGENT-WRAPPER ONLY - NO DIRECT EDITS]** + - MUST use Bash tool to invoke `codeagent-wrapper --parallel` for ALL code changes + - NEVER use Edit, Write, MultiEdit, or Task tools to modify code directly + - Build ONE `--parallel` config that includes all tasks in `dev-plan.md` and submit it once via Bash tool: ```bash - # Backend task (use codex backend - default) - codeagent-wrapper --backend codex - <<'EOF' - Task: [task-id] + # One shot submission - wrapper handles topology + concurrency + codeagent-wrapper --parallel <<'EOF' + ---TASK--- + id: [task-id-1] + backend: codex + workdir: . + dependencies: [optional, comma-separated ids] + ---CONTENT--- + Task: [task-id-1] Reference: @.claude/specs/{feature_name}/dev-plan.md Scope: [task file scope] Test: [test command] Deliverables: code + unit tests + coverage ≥90% + coverage summary - EOF - # UI task (use gemini backend - enforced) - codeagent-wrapper --backend gemini - <<'EOF' - Task: [task-id] + ---TASK--- + id: [task-id-2] + backend: gemini + workdir: . + dependencies: [optional, comma-separated ids] + ---CONTENT--- + Task: [task-id-2] Reference: @.claude/specs/{feature_name}/dev-plan.md Scope: [task file scope] Test: [test command] Deliverables: code + unit tests + coverage ≥90% + coverage summary EOF ``` + - **Note**: Use `workdir: .` (current directory) for all tasks unless specific subdirectory is required - Execute independent tasks concurrently; serialize conflicting ones; track coverage reports - **Step 5: Coverage Validation** @@ -113,9 +162,13 @@ You are the /dev Workflow Orchestrator, an expert development workflow manager s - Provide completed task list, coverage per task, key file changes **Error Handling** -- codeagent failure: retry once, then log and continue -- Insufficient coverage: request more tests (max 2 rounds) -- Dependency conflicts: serialize automatically +- **codeagent-wrapper failure**: Retry once with same input; if still fails, log error and ask user for guidance +- **Insufficient coverage (<90%)**: Request more tests from the failed task (max 2 rounds); if still fails, report to user +- **Dependency conflicts**: + - Circular dependencies: codeagent-wrapper will detect and fail with error; revise task breakdown to remove cycles + - Missing dependencies: Ensure all task IDs referenced in `dependencies` field exist +- **Parallel execution timeout**: Individual tasks timeout after 2 hours (configurable via CODEX_TIMEOUT); failed tasks can be retried individually +- **Backend unavailable**: If codex/claude/gemini CLI not found, fail immediately with clear error message **Quality Standards** - Code coverage ≥90% diff --git a/go.work b/go.work new file mode 100644 index 0000000..3644132 --- /dev/null +++ b/go.work @@ -0,0 +1,5 @@ +go 1.21 + +use ( + ./codeagent-wrapper +) diff --git a/install.py b/install.py index 3426cdb..44cbe95 100644 --- a/install.py +++ b/install.py @@ -17,7 +17,10 @@ from datetime import datetime from pathlib import Path from typing import Any, Dict, Iterable, List, Optional -import jsonschema +try: + import jsonschema +except ImportError: # pragma: no cover + jsonschema = None DEFAULT_INSTALL_DIR = "~/.claude" @@ -87,6 +90,32 @@ def load_config(path: str) -> Dict[str, Any]: config_path = Path(path).expanduser().resolve() config = _load_json(config_path) + if jsonschema is None: + print( + "WARNING: python package 'jsonschema' is not installed; " + "skipping config validation. To enable validation run:\n" + " python3 -m pip install jsonschema\n", + file=sys.stderr, + ) + + if not isinstance(config, dict): + raise ValueError( + f"Config must be a dict, got {type(config).__name__}. " + "Check your config.json syntax." + ) + + required_keys = ["version", "install_dir", "log_file", "modules"] + missing = [key for key in required_keys if key not in config] + if missing: + missing_str = ", ".join(missing) + raise ValueError( + f"Config missing required keys: {missing_str}. " + "Install jsonschema for better validation: " + "python3 -m pip install jsonschema" + ) + + return config + schema_candidates = [ config_path.parent / "config.schema.json", Path(__file__).resolve().with_name("config.schema.json"), diff --git a/install.sh b/install.sh index 6469962..0e426a4 100644 --- a/install.sh +++ b/install.sh @@ -34,23 +34,25 @@ if ! curl -fsSL "$URL" -o /tmp/codeagent-wrapper; then exit 1 fi -mkdir -p "$HOME/bin" +INSTALL_DIR="${INSTALL_DIR:-$HOME/.claude}" +BIN_DIR="${INSTALL_DIR}/bin" +mkdir -p "$BIN_DIR" -mv /tmp/codeagent-wrapper "$HOME/bin/codeagent-wrapper" -chmod +x "$HOME/bin/codeagent-wrapper" +mv /tmp/codeagent-wrapper "${BIN_DIR}/codeagent-wrapper" +chmod +x "${BIN_DIR}/codeagent-wrapper" -if "$HOME/bin/codeagent-wrapper" --version >/dev/null 2>&1; then - echo "codeagent-wrapper installed successfully to ~/bin/codeagent-wrapper" +if "${BIN_DIR}/codeagent-wrapper" --version >/dev/null 2>&1; then + echo "codeagent-wrapper installed successfully to ${BIN_DIR}/codeagent-wrapper" else echo "ERROR: installation verification failed" >&2 exit 1 fi -if [[ ":$PATH:" != *":$HOME/bin:"* ]]; then +if [[ ":$PATH:" != *":${BIN_DIR}:"* ]]; then echo "" - echo "WARNING: ~/bin is not in your PATH" - echo "Add this line to your ~/.bashrc or ~/.zshrc:" + echo "WARNING: ${BIN_DIR} is not in your PATH" + echo "Add this line to your ~/.bashrc or ~/.zshrc (then restart your shell):" echo "" - echo " export PATH=\"\$HOME/bin:\$PATH\"" + echo " export PATH=\"${BIN_DIR}:\$PATH\"" echo "" fi diff --git a/skills/codeagent/SKILL.md b/skills/codeagent/SKILL.md index 0671304..04d0962 100644 --- a/skills/codeagent/SKILL.md +++ b/skills/codeagent/SKILL.md @@ -74,7 +74,7 @@ codeagent-wrapper --backend gemini "simple task" - `task` (required): Task description, supports `@file` references - `working_dir` (optional): Working directory (default: current) - `--backend` (optional): Select AI backend (codex/claude/gemini, default: codex) - - **Note**: Claude backend defaults to `--dangerously-skip-permissions` for automation compatibility + - **Note**: Claude backend only adds `--dangerously-skip-permissions` when explicitly enabled ## Return Format @@ -147,9 +147,9 @@ Set `CODEAGENT_MAX_PARALLEL_WORKERS` to limit concurrent tasks (default: unlimit ## Environment Variables - `CODEX_TIMEOUT`: Override timeout in milliseconds (default: 7200000 = 2 hours) -- `CODEAGENT_SKIP_PERMISSIONS`: Control permission checks - - For **Claude** backend: Set to `true`/`1` to **disable** `--dangerously-skip-permissions` (default: enabled) - - For **Codex/Gemini** backends: Set to `true`/`1` to enable permission skipping (default: disabled) +- `CODEAGENT_SKIP_PERMISSIONS`: Control Claude CLI permission checks + - For **Claude** backend: Set to `true`/`1` to add `--dangerously-skip-permissions` (default: disabled) + - For **Codex/Gemini** backends: Currently has no effect - `CODEAGENT_MAX_PARALLEL_WORKERS`: Limit concurrent tasks in parallel mode (default: unlimited, recommended: 8) ## Invocation Pattern @@ -182,9 +182,8 @@ Bash tool parameters: ## Security Best Practices -- **Claude Backend**: Defaults to `--dangerously-skip-permissions` for automation workflows - - To enforce permission checks with Claude: Set `CODEAGENT_SKIP_PERMISSIONS=true` -- **Codex/Gemini Backends**: Permission checks enabled by default +- **Claude Backend**: Permission checks enabled by default + - To skip checks: set `CODEAGENT_SKIP_PERMISSIONS=true` or pass `--skip-permissions` - **Concurrency Limits**: Set `CODEAGENT_MAX_PARALLEL_WORKERS` in production to prevent resource exhaustion - **Automation Context**: This wrapper is designed for AI-driven automation where permission prompts would block execution