fix: comprehensive security and quality improvements for PR #85 & #87 (#90)

Co-authored-by: tytsxai <tytsxai@users.noreply.github.com>
This commit is contained in:
ben
2025-12-21 17:55:16 +08:00
committed by cexll
parent 0f359b048f
commit 1f42bcc1c6
13 changed files with 517 additions and 80 deletions

154
README.md
View File

@@ -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 <workdir>` - Set working directory
- `resume <session_id>` - 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 <session_id>` - 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 <session_id>` - Resume sessions
- `-p` - Prompt input flag
**Verify Gemini CLI is installed:**
```bash
which gemini
gemini --version
```
---
## Installation ## Installation
### Modular Installation (Recommended) ### Modular Installation (Recommended)
@@ -163,15 +216,39 @@ python3 install.py --force
``` ```
~/.claude/ ~/.claude/
├── CLAUDE.md # Core instructions and role definition ├── bin/
├── commands/ # Slash commands (/dev, /code, etc.) │ └── codeagent-wrapper # Main executable
├── agents/ # Agent definitions ├── CLAUDE.md # Core instructions and role definition
├── commands/ # Slash commands (/dev, /code, etc.)
├── agents/ # Agent definitions
├── skills/ ├── skills/
│ └── codex/ │ └── codex/
│ └── SKILL.md # Codex integration skill │ └── SKILL.md # Codex integration skill
── installed_modules.json # Installation status ── 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 ### Configuration
Edit `config.json` to customize: Edit `config.json` to customize:
@@ -295,7 +372,7 @@ setx PATH "%USERPROFILE%\bin;%PATH%"
**Codex wrapper not found:** **Codex wrapper not found:**
```bash ```bash
# Check PATH # 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 # Reinstall
bash install.sh bash install.sh
@@ -315,6 +392,71 @@ cat ~/.claude/installed_modules.json
python3 install.py --module dev --force 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 <session_id> "test" # Should continue from previous session
claude -r <session_id> "test"
# If not supported, use new sessions instead of resume mode
```
--- ---
## Documentation ## Documentation

View File

@@ -152,15 +152,39 @@ python3 install.py --force
``` ```
~/.claude/ ~/.claude/
├── CLAUDE.md # 核心指令和角色定义 ├── bin/
├── commands/ # 斜杠命令 (/dev, /code 等) │ └── codeagent-wrapper # 主可执行文件
├── agents/ # 智能体定义 ├── CLAUDE.md # 核心指令和角色定义
├── commands/ # 斜杠命令 (/dev, /code 等)
├── agents/ # 智能体定义
├── skills/ ├── skills/
│ └── codex/ │ └── codex/
│ └── SKILL.md # Codex 集成技能 │ └── SKILL.md # Codex 集成技能
── installed_modules.json # 安装状态 ── 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` 自定义: 编辑 `config.json` 自定义:
@@ -284,7 +308,7 @@ setx PATH "%USERPROFILE%\bin;%PATH%"
**Codex wrapper 未找到:** **Codex wrapper 未找到:**
```bash ```bash
# 检查 PATH # 检查 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 bash install.sh

View File

@@ -26,15 +26,17 @@ func (ClaudeBackend) Command() string {
return "claude" return "claude"
} }
func (ClaudeBackend) BuildArgs(cfg *Config, targetArg string) []string { func (ClaudeBackend) BuildArgs(cfg *Config, targetArg string) []string {
return buildClaudeArgs(cfg, targetArg)
}
func buildClaudeArgs(cfg *Config, targetArg string) []string {
if cfg == nil { if cfg == nil {
return nil return nil
} }
args := []string{"-p", "--dangerously-skip-permissions"} args := []string{"-p"}
if cfg.SkipPermissions {
// Only skip permissions when explicitly requested args = append(args, "--dangerously-skip-permissions")
// if cfg.SkipPermissions { }
// args = append(args, "--dangerously-skip-permissions")
// }
// Prevent infinite recursion: disable all setting sources (user, project, local) // 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 // 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" return "gemini"
} }
func (GeminiBackend) BuildArgs(cfg *Config, targetArg string) []string { func (GeminiBackend) BuildArgs(cfg *Config, targetArg string) []string {
return buildGeminiArgs(cfg, targetArg)
}
func buildGeminiArgs(cfg *Config, targetArg string) []string {
if cfg == nil { if cfg == nil {
return nil return nil
} }

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"os"
"reflect" "reflect"
"testing" "testing"
) )
@@ -8,16 +9,16 @@ import (
func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) { func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
backend := ClaudeBackend{} 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"} cfg := &Config{Mode: "new", WorkDir: "/repo"}
got := backend.BuildArgs(cfg, "todo") 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) { if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", 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} cfg := &Config{Mode: "new", SkipPermissions: true}
got := backend.BuildArgs(cfg, "-") got := backend.BuildArgs(cfg, "-")
want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "-"} 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"} cfg := &Config{Mode: "resume", SessionID: "sid-123", WorkDir: "/ignored"}
got := backend.BuildArgs(cfg, "resume-task") 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) { if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", 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) { t.Run("resume mode without session still returns base flags", func(t *testing.T) {
cfg := &Config{Mode: "resume", WorkDir: "/ignored"} cfg := &Config{Mode: "resume", WorkDir: "/ignored"}
got := backend.BuildArgs(cfg, "follow-up") 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) { if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", 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{} backend := CodexBackend{}
cfg := &Config{Mode: "new", WorkDir: "/tmp"} cfg := &Config{Mode: "new", WorkDir: "/tmp"}
got := backend.BuildArgs(cfg, "task") got := backend.BuildArgs(cfg, "task")
@@ -98,6 +112,20 @@ func TestClaudeBuildArgs_GeminiAndCodexModes(t *testing.T) {
t.Fatalf("got %v, want %v", got, want) 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) { func TestClaudeBuildArgs_BackendMetadata(t *testing.T) {

View File

@@ -164,6 +164,9 @@ func parseParallelConfig(data []byte) (*ParallelConfig, error) {
if content == "" { if content == "" {
return nil, fmt.Errorf("task block #%d (%q) missing content", taskIndex, task.ID) 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 { if _, exists := seen[task.ID]; exists {
return nil, fmt.Errorf("task block #%d has duplicate id: %s", taskIndex, task.ID) 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 <session_id> <task>") return nil, fmt.Errorf("resume mode requires: resume <session_id> <task>")
} }
cfg.Mode = "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.Task = args[2]
cfg.ExplicitStdin = (args[2] == "-") cfg.ExplicitStdin = (args[2] == "-")
if len(args) > 3 { if len(args) > 3 {

View File

@@ -509,23 +509,43 @@ func generateFinalOutput(results []TaskResult) string {
} }
func buildCodexArgs(cfg *Config, targetArg string) []string { func buildCodexArgs(cfg *Config, targetArg string) []string {
if cfg.Mode == "resume" { if cfg == nil {
return []string{ panic("buildCodexArgs: nil config")
"e", }
"--skip-git-repo-check",
"--json", var resumeSessionID string
"resume", isResume := cfg.Mode == "resume"
cfg.SessionID, if isResume {
targetArg, resumeSessionID = strings.TrimSpace(cfg.SessionID)
if resumeSessionID == "" {
logError("invalid config: resume mode requires non-empty session_id")
isResume = false
} }
} }
return []string{
"e", args := []string{"e"}
"--skip-git-repo-check",
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, "-C", cfg.WorkDir,
"--json", "--json",
targetArg, targetArg,
} )
} }
func runCodexTask(taskSpec TaskSpec, silent bool, timeoutSec int) TaskResult { func runCodexTask(taskSpec TaskSpec, silent bool, timeoutSec int) TaskResult {
@@ -576,6 +596,12 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
cfg.WorkDir = defaultWorkdir 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 useStdin := taskSpec.UseStdin
targetArg := taskSpec.Task targetArg := taskSpec.Task
if useStdin { if useStdin {
@@ -745,6 +771,10 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
default: default:
} }
}) })
select {
case completeSeen <- struct{}{}:
default:
}
parseCh <- parseResult{message: msg, threadID: tid} parseCh <- parseResult{message: msg, threadID: tid}
}() }()

View File

@@ -10,6 +10,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -244,6 +245,10 @@ func TestExecutorHelperCoverage(t *testing.T) {
}) })
t.Run("generateFinalOutputAndArgs", func(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{ out := generateFinalOutput([]TaskResult{
{TaskID: "ok", ExitCode: 0}, {TaskID: "ok", ExitCode: 0},
{TaskID: "fail", ExitCode: 1, Error: "boom"}, {TaskID: "fail", ExitCode: 1, Error: "boom"},
@@ -257,11 +262,11 @@ func TestExecutorHelperCoverage(t *testing.T) {
} }
args := buildCodexArgs(&Config{Mode: "new", WorkDir: "/tmp"}, "task") 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) t.Fatalf("unexpected codex args: %+v", args)
} }
args = buildCodexArgs(&Config{Mode: "resume", SessionID: "sess"}, "target") 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) t.Fatalf("unexpected resume args: %+v", args)
} }
}) })
@@ -298,6 +303,18 @@ func TestExecutorRunCodexTaskWithContext(t *testing.T) {
origRunner := newCommandRunner origRunner := newCommandRunner
defer func() { newCommandRunner = origRunner }() 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) { t.Run("success", func(t *testing.T) {
var firstStdout *reasonReadCloser var firstStdout *reasonReadCloser
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner { newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {

View File

@@ -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 session_id", args: []string{"codeagent-wrapper", "resume"}, wantErr: true},
{name: "resume missing task", args: []string{"codeagent-wrapper", "resume", "session-123"}, 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 { 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) { func TestParallelParseConfig_InvalidFormat(t *testing.T) {
if _, err := parseParallelConfig([]byte("invalid format")); err == nil { if _, err := parseParallelConfig([]byte("invalid format")); err == nil {
t.Fatalf("expected error for invalid format, got 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) { 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"} cfg := &Config{Mode: "new", WorkDir: "/test/dir"}
args := buildCodexArgs(cfg, "my task") 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) { if len(args) != len(expected) {
t.Fatalf("len mismatch") t.Fatalf("len mismatch")
} }
@@ -1368,9 +1392,20 @@ func TestRunBuildCodexArgs_NewMode(t *testing.T) {
} }
func TestRunBuildCodexArgs_ResumeMode(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"} cfg := &Config{Mode: "resume", SessionID: "session-abc"}
args := buildCodexArgs(cfg, "-") 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) { if len(args) != len(expected) {
t.Fatalf("len mismatch") 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) { func TestBackendSelectBackend(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -1436,7 +1526,13 @@ func TestBackendBuildArgs_CodexBackend(t *testing.T) {
backend := CodexBackend{} backend := CodexBackend{}
cfg := &Config{Mode: "new", WorkDir: "/test/dir"} cfg := &Config{Mode: "new", WorkDir: "/test/dir"}
got := backend.BuildArgs(cfg, "task") 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) { if len(got) != len(want) {
t.Fatalf("length mismatch") t.Fatalf("length mismatch")
} }
@@ -1451,7 +1547,7 @@ func TestBackendBuildArgs_ClaudeBackend(t *testing.T) {
backend := ClaudeBackend{} backend := ClaudeBackend{}
cfg := &Config{Mode: "new", WorkDir: defaultWorkdir} cfg := &Config{Mode: "new", WorkDir: defaultWorkdir}
got := backend.BuildArgs(cfg, "todo") 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) { if len(got) != len(want) {
t.Fatalf("length mismatch") t.Fatalf("length mismatch")
} }
@@ -1472,7 +1568,7 @@ func TestClaudeBackendBuildArgs_OutputValidation(t *testing.T) {
target := "ensure-flags" target := "ensure-flags"
args := backend.BuildArgs(cfg, target) 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 { if len(args) != len(expectedPrefix)+1 {
t.Fatalf("args length=%d, want %d", len(args), len(expectedPrefix)+1) t.Fatalf("args length=%d, want %d", len(args), len(expectedPrefix)+1)

View File

@@ -2,9 +2,25 @@
description: Extreme lightweight end-to-end development workflow with requirements clarification, parallel codeagent execution, and mandatory 90% test coverage 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. 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** **Core Responsibilities**
- Orchestrate a streamlined 6-step development workflow: - Orchestrate a streamlined 6-step development workflow:
1. Requirement clarification through targeted questioning 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 6. Completion summary
**Workflow Execution** **Workflow Execution**
- **Step 1: Requirement Clarification** - **Step 1: Requirement Clarification [MANDATORY - DO NOT SKIP]**
- Use AskUserQuestion to clarify requirements directly - 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 - 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 - 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): **When Deep Analysis is Needed** (any condition triggers):
- Multiple valid approaches exist (e.g., Redis vs in-memory vs file-based caching) - 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 - 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) - 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 1. **Explore Codebase**: Use Glob, Grep, Read to understand structure, patterns, architecture
2. **Identify Existing Patterns**: Find how similar features are implemented, reuse conventions 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) 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" - Options: "Confirm and execute" / "Need adjustments"
- If user chooses "Need adjustments", return to Step 1 or Step 2 based on feedback - If user chooses "Need adjustments", return to Step 1 or Step 2 based on feedback
- **Step 4: Parallel Development Execution** - **Step 4: Parallel Development Execution [CODEAGENT-WRAPPER ONLY - NO DIRECT EDITS]**
- For each task in `dev-plan.md`, invoke codeagent skill with task brief in HEREDOC format: - 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 ```bash
# Backend task (use codex backend - default) # One shot submission - wrapper handles topology + concurrency
codeagent-wrapper --backend codex - <<'EOF' codeagent-wrapper --parallel <<'EOF'
Task: [task-id] ---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 Reference: @.claude/specs/{feature_name}/dev-plan.md
Scope: [task file scope] Scope: [task file scope]
Test: [test command] Test: [test command]
Deliverables: code + unit tests + coverage ≥90% + coverage summary Deliverables: code + unit tests + coverage ≥90% + coverage summary
EOF
# UI task (use gemini backend - enforced) ---TASK---
codeagent-wrapper --backend gemini - <<'EOF' id: [task-id-2]
Task: [task-id] backend: gemini
workdir: .
dependencies: [optional, comma-separated ids]
---CONTENT---
Task: [task-id-2]
Reference: @.claude/specs/{feature_name}/dev-plan.md Reference: @.claude/specs/{feature_name}/dev-plan.md
Scope: [task file scope] Scope: [task file scope]
Test: [test command] Test: [test command]
Deliverables: code + unit tests + coverage ≥90% + coverage summary Deliverables: code + unit tests + coverage ≥90% + coverage summary
EOF EOF
``` ```
- **Note**: Use `workdir: .` (current directory) for all tasks unless specific subdirectory is required
- Execute independent tasks concurrently; serialize conflicting ones; track coverage reports - Execute independent tasks concurrently; serialize conflicting ones; track coverage reports
- **Step 5: Coverage Validation** - **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 - Provide completed task list, coverage per task, key file changes
**Error Handling** **Error Handling**
- codeagent failure: retry once, then log and continue - **codeagent-wrapper failure**: Retry once with same input; if still fails, log error and ask user for guidance
- Insufficient coverage: request more tests (max 2 rounds) - **Insufficient coverage (<90%)**: Request more tests from the failed task (max 2 rounds); if still fails, report to user
- Dependency conflicts: serialize automatically - **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** **Quality Standards**
- Code coverage ≥90% - Code coverage ≥90%

5
go.work Normal file
View File

@@ -0,0 +1,5 @@
go 1.21
use (
./codeagent-wrapper
)

View File

@@ -17,7 +17,10 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional from typing import Any, Dict, Iterable, List, Optional
import jsonschema try:
import jsonschema
except ImportError: # pragma: no cover
jsonschema = None
DEFAULT_INSTALL_DIR = "~/.claude" DEFAULT_INSTALL_DIR = "~/.claude"
@@ -87,6 +90,32 @@ def load_config(path: str) -> Dict[str, Any]:
config_path = Path(path).expanduser().resolve() config_path = Path(path).expanduser().resolve()
config = _load_json(config_path) 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 = [ schema_candidates = [
config_path.parent / "config.schema.json", config_path.parent / "config.schema.json",
Path(__file__).resolve().with_name("config.schema.json"), Path(__file__).resolve().with_name("config.schema.json"),

View File

@@ -34,23 +34,25 @@ if ! curl -fsSL "$URL" -o /tmp/codeagent-wrapper; then
exit 1 exit 1
fi 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" mv /tmp/codeagent-wrapper "${BIN_DIR}/codeagent-wrapper"
chmod +x "$HOME/bin/codeagent-wrapper" chmod +x "${BIN_DIR}/codeagent-wrapper"
if "$HOME/bin/codeagent-wrapper" --version >/dev/null 2>&1; then if "${BIN_DIR}/codeagent-wrapper" --version >/dev/null 2>&1; then
echo "codeagent-wrapper installed successfully to ~/bin/codeagent-wrapper" echo "codeagent-wrapper installed successfully to ${BIN_DIR}/codeagent-wrapper"
else else
echo "ERROR: installation verification failed" >&2 echo "ERROR: installation verification failed" >&2
exit 1 exit 1
fi fi
if [[ ":$PATH:" != *":$HOME/bin:"* ]]; then if [[ ":$PATH:" != *":${BIN_DIR}:"* ]]; then
echo "" echo ""
echo "WARNING: ~/bin is not in your PATH" echo "WARNING: ${BIN_DIR} is not in your PATH"
echo "Add this line to your ~/.bashrc or ~/.zshrc:" echo "Add this line to your ~/.bashrc or ~/.zshrc (then restart your shell):"
echo "" echo ""
echo " export PATH=\"\$HOME/bin:\$PATH\"" echo " export PATH=\"${BIN_DIR}:\$PATH\""
echo "" echo ""
fi fi

View File

@@ -74,7 +74,7 @@ codeagent-wrapper --backend gemini "simple task"
- `task` (required): Task description, supports `@file` references - `task` (required): Task description, supports `@file` references
- `working_dir` (optional): Working directory (default: current) - `working_dir` (optional): Working directory (default: current)
- `--backend` (optional): Select AI backend (codex/claude/gemini, default: codex) - `--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 ## Return Format
@@ -147,9 +147,9 @@ Set `CODEAGENT_MAX_PARALLEL_WORKERS` to limit concurrent tasks (default: unlimit
## Environment Variables ## Environment Variables
- `CODEX_TIMEOUT`: Override timeout in milliseconds (default: 7200000 = 2 hours) - `CODEX_TIMEOUT`: Override timeout in milliseconds (default: 7200000 = 2 hours)
- `CODEAGENT_SKIP_PERMISSIONS`: Control permission checks - `CODEAGENT_SKIP_PERMISSIONS`: Control Claude CLI permission checks
- For **Claude** backend: Set to `true`/`1` to **disable** `--dangerously-skip-permissions` (default: enabled) - For **Claude** backend: Set to `true`/`1` to add `--dangerously-skip-permissions` (default: disabled)
- For **Codex/Gemini** backends: Set to `true`/`1` to enable permission skipping (default: disabled) - For **Codex/Gemini** backends: Currently has no effect
- `CODEAGENT_MAX_PARALLEL_WORKERS`: Limit concurrent tasks in parallel mode (default: unlimited, recommended: 8) - `CODEAGENT_MAX_PARALLEL_WORKERS`: Limit concurrent tasks in parallel mode (default: unlimited, recommended: 8)
## Invocation Pattern ## Invocation Pattern
@@ -182,9 +182,8 @@ Bash tool parameters:
## Security Best Practices ## Security Best Practices
- **Claude Backend**: Defaults to `--dangerously-skip-permissions` for automation workflows - **Claude Backend**: Permission checks enabled by default
- To enforce permission checks with Claude: Set `CODEAGENT_SKIP_PERMISSIONS=true` - To skip checks: set `CODEAGENT_SKIP_PERMISSIONS=true` or pass `--skip-permissions`
- **Codex/Gemini Backends**: Permission checks enabled by default
- **Concurrency Limits**: Set `CODEAGENT_MAX_PARALLEL_WORKERS` in production to prevent resource exhaustion - **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 - **Automation Context**: This wrapper is designed for AI-driven automation where permission prompts would block execution