Compare commits

..

11 Commits

Author SHA1 Message Date
cexll
81e95777a8 fix: replace setx with reg add to avoid 1024-char PATH truncation (#101)
- Use reg add instead of setx to bypass Windows 1024-character limit
- Add safety check for quotes/exclamation marks in PATH to prevent injection
- Preserve stderr output for better error diagnostics
- Update documentation with warnings about cmd PATH duplication
- Add test script for PATH update validation

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2025-12-31 14:40:34 +08:00
cexll
993249acb1 docs: 添加 FAQ 常见问题章节
添加 4 个高频问题的解决方案:
- Q1: codeagent-wrapper "Unknown event format" 日志问题 (#96)
- Q2: Gemini 无法读取 .gitignore 文件 (#75)
- Q3: /dev 命令并行执行性能优化建议 (#77)
- Q4: Go 版 Codex 权限配置指南 (#31)

提升用户自助排障能力,减少重复问题咨询。

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2025-12-26 15:03:43 +08:00
ben
0d28e70026 feat(dev-workflow): Add intelligent backend selection based on task complexity (#61)
合并智能 backend 选择功能。包含:multiSelect backend 选择、type 字段任务分类(default|ui|quick-fix)、智能路由策略
2025-12-26 15:01:58 +08:00
cexll
7560ce1976 fix: 移除未知事件格式的日志噪声 (#96)
问题:
- codeagent-wrapper 在处理 Claude Code 等其他后端的事件流时
- 对无法识别的事件格式(turn.started/assistant/user)打印警告日志
- 造成输出噪声,影响用户体验

修复:
- parser.go:274 - 移除对未知事件的 warnFn 日志打印
- 改为静默 continue,直接跳过这些事件
- 添加注释说明这些事件来自其他后端,无需处理

测试:
- 新增 parser_unknown_event_test.go 回归测试
- 验证未知事件不产生 "Agent event:" 日志
- 确保 Codex/Claude/Gemini 事件解析不受影响
- 所有测试通过

Closes #96

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2025-12-26 14:51:38 +08:00
cexll
683d18e6bb docs: update troubleshooting with idempotent PATH commands (#95)
- Use correct PATH pattern matching syntax
- Explain installer auto-adds PATH
- Provide idempotent command for manual use

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2025-12-25 11:40:53 +08:00
cexll
a7147f692c fix: prevent duplicate PATH entries on reinstall (#95)
- install.sh: Auto-detect shell and add PATH with idempotency check
- install.bat: Improve PATH detection with system PATH check
- Fix PATH variable quoting in pattern matching

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2025-12-25 11:38:42 +08:00
cexll
b71d74f01f fix: Minor issues #12 and #13 - ASCII mode and performance optimization
This commit addresses the remaining Minor issues from PR #94 code review:

Minor #12: Unicode Symbol Compatibility
- Added CODEAGENT_ASCII_MODE environment variable support
- When set to "true", uses ASCII symbols: PASS/WARN/FAIL
- Default behavior (unset or "false"): Unicode symbols ✓/⚠️/✗
- Updated help text to document the environment variable
- Added tests for both ASCII and Unicode modes

Implementation:
- executor.go:514: New getStatusSymbols() function
- executor.go:531: Dynamic symbol selection in generateFinalOutputWithMode
- main.go:34: useASCIIMode variable declaration
- main.go:495: Environment variable documentation in help
- executor_concurrent_test.go:292: Tests for ASCII mode
- main_integration_test.go:89: Parser updated for both symbol formats

Minor #13: Performance Optimization - Reduce Repeated String Operations
- Optimized Message parsing to split only once per task result
- Added *FromLines() variants of all extractor functions
- Original extract*() functions now wrap *FromLines() for compatibility
- Reduces memory allocations and CPU usage in parallel execution

Implementation:
- utils.go:300: extractCoverageFromLines()
- utils.go:390: extractFilesChangedFromLines()
- utils.go:455: extractTestResultsFromLines()
- utils.go:551: extractKeyOutputFromLines()
- main.go:255: Single split with reuse: lines := strings.Split(...)

Backward Compatibility:
- All original extract*() functions preserved
- Tests updated to handle both symbol formats
- No breaking changes to public API

Test Results:
- All tests pass: go test ./... (40.164s)
- ASCII mode verified: PASS/WARN/FAIL symbols display correctly
- Unicode mode verified: ✓/⚠️/✗ symbols remain default
- Performance: Single split per Message instead of 4+

Usage Examples:
  # Unicode mode (default)
  ./codeagent-wrapper --parallel < tasks.txt

  # ASCII mode (for terminals without Unicode support)
  CODEAGENT_ASCII_MODE=true ./codeagent-wrapper --parallel < tasks.txt

Benefits:
- Improved terminal compatibility across different environments
- Reduced memory allocations in parallel execution
- Better performance for large-scale parallel tasks
- User choice between Unicode aesthetics and ASCII compatibility

Related: #94

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2025-12-24 11:59:00 +08:00
cexll
af1c860f54 fix: code review fixes for PR #94 - all critical and major issues resolved
This commit addresses all Critical and Major issues identified in the code review:

Critical Issues Fixed:
- #1: Test statistics data loss (utils.go:480) - Changed exit condition from || to &&
- #2: Below-target header showing "below 0%" - Added defaultCoverageTarget constant

Major Issues Fixed:
- #3: Coverage extraction not robust - Relaxed trigger conditions for various formats
- #4: 0% coverage ignored - Changed from CoverageNum>0 to Coverage!="" check
- #5: File change extraction incomplete - Support root files and @ prefix
- #6: String truncation panic risk - Added safeTruncate() with rune-based truncation
- #7: Breaking change documentation missing - Updated help text and docs
- #8: .DS_Store garbage files - Removed files and updated .gitignore
- #9: Test coverage insufficient - Added 29+ test cases in utils_test.go
- #10: Terminal escape injection risk - Added sanitizeOutput() for ANSI cleaning
- #11: Redundant code - Removed unused patterns variable

Test Results:
- All tests pass: go test ./... (34.283s)
- Test coverage: 88.4% (up from ~85%)
- New test file: codeagent-wrapper/utils_test.go
- No breaking changes to existing functionality

Files Modified:
- codeagent-wrapper/utils.go (+166 lines) - Core fixes and new functions
- codeagent-wrapper/executor.go (+111 lines) - Output format fixes
- codeagent-wrapper/main.go (+45 lines) - Configuration updates
- codeagent-wrapper/main_test.go (+40 lines) - New integration tests
- codeagent-wrapper/utils_test.go (new file) - Complete extractor tests
- docs/CODEAGENT-WRAPPER.md (+38 lines) - Documentation updates
- .gitignore (+2 lines) - Added .DS_Store patterns
- Deleted 5 .DS_Store files

Verification:
- Binary compiles successfully (v5.4.0)
- All extractors validated with real-world test cases
- Security vulnerabilities patched
- Performance maintained (90% token reduction preserved)

Related: #94

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2025-12-24 09:55:39 +08:00
tytsxai
70b1896011 feat(codeagent-wrapper): v5.4.0 structured execution report (#94)
Merging PR #94 with code review fixes applied.

All Critical and Major issues from code review have been addressed:
- 11/13 issues fixed (2 minor optimizations deferred)
- Test coverage: 88.4%
- All tests passing
- Security vulnerabilities patched
- Documentation updated

The code review fixes have been committed to pr-94 branch and are ready for integration.
2025-12-24 09:53:58 +08:00
cexll
3fd3c67749 fix: correct settings.json filename and bump version to v5.2.8
- Fix incorrect filename reference from setting.json to settings.json in backend.go
- Update corresponding test fixtures to use correct filename
- Bump version from 5.2.7 to 5.2.8

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2025-12-22 10:32:44 +08:00
cexll
156a072a0b chore: simplify release workflow to use GitHub auto-generated notes
- Remove git-cliff dependency and node.js setup
- Use generate_release_notes: true for automatic PR/commit listing
- Maintains all binary builds and artifact uploads
- Release notes can still be manually edited after creation

Benefits:
- Simpler workflow with fewer dependencies
- Automatic PR titles and contributor attribution
- Easier to maintain and debug

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2025-12-21 20:37:11 +08:00
24 changed files with 1617 additions and 196 deletions

View File

@@ -97,11 +97,6 @@ jobs:
with:
path: artifacts
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Prepare release files
run: |
mkdir -p release
@@ -109,26 +104,10 @@ jobs:
cp install.sh install.bat release/
ls -la release/
- name: Generate release notes with git-cliff
run: |
# Install git-cliff via npx
npx git-cliff@latest --current --strip all -o release_notes.md
# Fallback if generation failed
if [ ! -s release_notes.md ]; then
echo "⚠️ Failed to generate release notes with git-cliff" > release_notes.md
echo "" >> release_notes.md
echo "## What's Changed" >> release_notes.md
echo "See commits in this release for details." >> release_notes.md
fi
echo "--- Generated Release Notes ---"
cat release_notes.md
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: release/*
body_path: release_notes.md
generate_release_notes: true
draft: false
prerelease: false

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
.claude/
.claude-trace
.DS_Store
**/.DS_Store
.venv
.pytest_cache
__pycache__

View File

@@ -346,8 +346,10 @@ $Env:PATH = "$HOME\bin;$Env:PATH"
```
```batch
REM cmd.exe - persistent for current user
setx PATH "%USERPROFILE%\bin;%PATH%"
REM cmd.exe - persistent for current user (use PowerShell method above instead)
REM WARNING: This expands %PATH% which includes system PATH, causing duplication
REM Note: Using reg add instead of setx to avoid 1024-character truncation limit
reg add "HKCU\Environment" /v Path /t REG_EXPAND_SZ /d "%USERPROFILE%\bin;%PATH%" /f
```
---
@@ -371,11 +373,14 @@ setx PATH "%USERPROFILE%\bin;%PATH%"
**Codex wrapper not found:**
```bash
# Check PATH
echo $PATH | grep -q "$HOME/.claude/bin" || echo 'export PATH="$HOME/.claude/bin:$PATH"' >> ~/.zshrc
# Installer auto-adds PATH, check if configured
if [[ ":$PATH:" != *":$HOME/.claude/bin:"* ]]; then
echo "PATH not configured. Reinstalling..."
bash install.sh
fi
# Reinstall
bash install.sh
# Or manually add (idempotent command)
[[ ":$PATH:" != *":$HOME/.claude/bin:"* ]] && echo 'export PATH="$HOME/.claude/bin:$PATH"' >> ~/.zshrc
```
**Permission denied:**
@@ -459,9 +464,83 @@ claude -r <session_id> "test"
---
## Documentation
## FAQ (Frequently Asked Questions)
### Core Guides
### Q1: `codeagent-wrapper` execution fails with "Unknown event format"
**Problem:**
```
Unknown event format: {"type":"turn.started"}
Unknown event format: {"type":"assistant", ...}
```
**Solution:**
This is a logging event format display issue and does not affect actual functionality. It will be fixed in the next version. You can ignore these log outputs.
**Related Issue:** [#96](https://github.com/cexll/myclaude/issues/96)
---
### Q2: Gemini cannot read files ignored by `.gitignore`
**Problem:**
When using `codeagent-wrapper --backend gemini`, files in directories like `.claude/` that are ignored by `.gitignore` cannot be read.
**Solution:**
- **Option 1:** Remove `.claude/` from your `.gitignore` file
- **Option 2:** Ensure files that need to be read are not in `.gitignore` list
**Related Issue:** [#75](https://github.com/cexll/myclaude/issues/75)
---
### Q3: `/dev` command parallel execution is very slow
**Problem:**
Using `/dev` command for simple features takes too long (over 30 minutes) with no visibility into task progress.
**Solution:**
1. **Check logs:** Review `C:\Users\User\AppData\Local\Temp\codeagent-wrapper-*.log` to identify bottlenecks
2. **Adjust backend:**
- Try faster models like `gpt-5.1-codex-max`
- Running in WSL may be significantly faster
3. **Workspace:** Use a single repository instead of monorepo with multiple sub-projects
**Related Issue:** [#77](https://github.com/cexll/myclaude/issues/77)
---
### Q4: Codex permission denied with new Go version
**Problem:**
After upgrading to the new Go-based Codex implementation, execution fails with permission denied errors.
**Solution:**
Add the following configuration to `~/.codex/config.yaml` (Windows: `c:\user\.codex\config.toml`):
```yaml
model = "gpt-5.1-codex-max"
model_reasoning_effort = "high"
model_reasoning_summary = "detailed"
approval_policy = "never"
sandbox_mode = "workspace-write"
disable_response_storage = true
network_access = true
```
**Key settings:**
- `approval_policy = "never"` - Remove approval restrictions
- `sandbox_mode = "workspace-write"` - Allow workspace write access
- `network_access = true` - Enable network access
**Related Issue:** [#31](https://github.com/cexll/myclaude/issues/31)
---
**Still having issues?** Visit [GitHub Issues](https://github.com/cexll/myclaude/issues) to search or report new issues.
---
## Documentation
- **[Codeagent-Wrapper Guide](docs/CODEAGENT-WRAPPER.md)** - Multi-backend execution wrapper
- **[Hooks Documentation](docs/HOOKS.md)** - Custom hooks and automation

View File

@@ -282,8 +282,10 @@ $Env:PATH = "$HOME\bin;$Env:PATH"
```
```batch
REM cmd.exe - 永久添加(当前用户)
setx PATH "%USERPROFILE%\bin;%PATH%"
REM cmd.exe - 永久添加(当前用户)(建议使用上面的 PowerShell 方法)
REM 警告:此命令会展开 %PATH% 包含系统 PATH导致重复
REM 注意:使用 reg add 而非 setx 以避免 1024 字符截断限制
reg add "HKCU\Environment" /v Path /t REG_EXPAND_SZ /d "%USERPROFILE%\bin;%PATH%" /f
```
---
@@ -307,11 +309,14 @@ setx PATH "%USERPROFILE%\bin;%PATH%"
**Codex wrapper 未找到:**
```bash
# 检查 PATH
echo $PATH | grep -q "$HOME/.claude/bin" || echo 'export PATH="$HOME/.claude/bin:$PATH"' >> ~/.zshrc
# 安装程序会自动添加 PATH检查是否已添加
if [[ ":$PATH:" != *":$HOME/.claude/bin:"* ]]; then
echo "PATH not configured. Reinstalling..."
bash install.sh
fi
# 重新安装
bash install.sh
# 或手动添加(幂等性命令)
[[ ":$PATH:" != *":$HOME/.claude/bin:"* ]] && echo 'export PATH="$HOME/.claude/bin:$PATH"' >> ~/.zshrc
```
**权限被拒绝:**
@@ -330,6 +335,83 @@ python3 install.py --module dev --force
---
## 常见问题 (FAQ)
### Q1: `codeagent-wrapper` 执行时报错 "Unknown event format"
**问题描述:**
执行 `codeagent-wrapper` 时出现错误:
```
Unknown event format: {"type":"turn.started"}
Unknown event format: {"type":"assistant", ...}
```
**解决方案:**
这是日志事件流的显示问题,不影响实际功能执行。预计在下个版本中修复。如需排查其他问题,可忽略此日志输出。
**相关 Issue** [#96](https://github.com/cexll/myclaude/issues/96)
---
### Q2: Gemini 无法读取 `.gitignore` 忽略的文件
**问题描述:**
使用 `codeagent-wrapper --backend gemini` 时,无法读取 `.claude/` 等被 `.gitignore` 忽略的目录中的文件。
**解决方案:**
- **方案一:** 在项目根目录的 `.gitignore` 中取消对 `.claude/` 的忽略
- **方案二:** 确保需要读取的文件不在 `.gitignore` 忽略列表中
**相关 Issue** [#75](https://github.com/cexll/myclaude/issues/75)
---
### Q3: `/dev` 命令并行执行特别慢
**问题描述:**
使用 `/dev` 命令开发简单功能耗时过长超过30分钟无法了解任务执行状态。
**解决方案:**
1. **检查日志:** 查看 `C:\Users\User\AppData\Local\Temp\codeagent-wrapper-*.log` 分析瓶颈
2. **调整后端:**
- 尝试使用 `gpt-5.1-codex-max` 等更快的模型
- 在 WSL 环境下运行速度可能更快
3. **工作区选择:** 使用独立的代码仓库而非包含多个子项目的 monorepo
**相关 Issue** [#77](https://github.com/cexll/myclaude/issues/77)
---
### Q4: 新版 Go 实现的 Codex 权限不足
**问题描述:**
升级到新版 Go 实现的 Codex 后,出现权限不足的错误。
**解决方案:**
`~/.codex/config.yaml` 中添加以下配置Windows: `c:\user\.codex\config.toml`
```yaml
model = "gpt-5.1-codex-max"
model_reasoning_effort = "high"
model_reasoning_summary = "detailed"
approval_policy = "never"
sandbox_mode = "workspace-write"
disable_response_storage = true
network_access = true
```
**关键配置说明:**
- `approval_policy = "never"` - 移除审批限制
- `sandbox_mode = "workspace-write"` - 允许工作区写入权限
- `network_access = true` - 启用网络访问
**相关 Issue** [#31](https://github.com/cexll/myclaude/issues/31)
---
**仍有疑问?** 请访问 [GitHub Issues](https://github.com/cexll/myclaude/issues) 搜索或提交新问题。
---
## 许可证
AGPL-3.0 License - 查看 [LICENSE](LICENSE)

View File

@@ -37,7 +37,7 @@ func (ClaudeBackend) BuildArgs(cfg *Config, targetArg string) []string {
const maxClaudeSettingsBytes = 1 << 20 // 1MB
// loadMinimalEnvSettings 从 ~/.claude/setting.json 只提取 env 配置。
// loadMinimalEnvSettings 从 ~/.claude/settings.json 只提取 env 配置。
// 只接受字符串类型的值;文件缺失/解析失败/超限都返回空。
func loadMinimalEnvSettings() map[string]string {
home, err := os.UserHomeDir()
@@ -45,7 +45,7 @@ func loadMinimalEnvSettings() map[string]string {
return nil
}
settingPath := filepath.Join(home, ".claude", "setting.json")
settingPath := filepath.Join(home, ".claude", "settings.json")
info, err := os.Stat(settingPath)
if err != nil || info.Size() > maxClaudeSettingsBytes {
return nil

View File

@@ -167,7 +167,7 @@ func TestLoadMinimalEnvSettings(t *testing.T) {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
path := filepath.Join(dir, "setting.json")
path := filepath.Join(dir, "settings.json")
data := []byte(`{"env":{"ANTHROPIC_API_KEY":"secret","FOO":"bar"}}`)
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
@@ -181,7 +181,7 @@ func TestLoadMinimalEnvSettings(t *testing.T) {
t.Run("non-string values are ignored", func(t *testing.T) {
dir := filepath.Join(home, ".claude")
path := filepath.Join(dir, "setting.json")
path := filepath.Join(dir, "settings.json")
data := []byte(`{"env":{"GOOD":"ok","BAD":123,"ALSO_BAD":true}}`)
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
@@ -201,7 +201,7 @@ func TestLoadMinimalEnvSettings(t *testing.T) {
t.Run("oversized file returns empty", func(t *testing.T) {
dir := filepath.Join(home, ".claude")
path := filepath.Join(dir, "setting.json")
path := filepath.Join(dir, "settings.json")
data := bytes.Repeat([]byte("a"), maxClaudeSettingsBytes+1)
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)

View File

@@ -49,7 +49,15 @@ type TaskResult struct {
SessionID string `json:"session_id"`
Error string `json:"error"`
LogPath string `json:"log_path"`
sharedLog bool
// Structured report fields
Coverage string `json:"coverage,omitempty"` // extracted coverage percentage (e.g., "92%")
CoverageNum float64 `json:"coverage_num,omitempty"` // numeric coverage for comparison
CoverageTarget float64 `json:"coverage_target,omitempty"` // target coverage (default 90)
FilesChanged []string `json:"files_changed,omitempty"` // list of changed files
KeyOutput string `json:"key_output,omitempty"` // brief summary of what was done
TestsPassed int `json:"tests_passed,omitempty"` // number of tests passed
TestsFailed int `json:"tests_failed,omitempty"` // number of tests failed
sharedLog bool
}
var backendRegistry = map[string]Backend{

View File

@@ -511,45 +511,212 @@ func shouldSkipTask(task TaskSpec, failed map[string]TaskResult) (bool, string)
return true, fmt.Sprintf("skipped due to failed dependencies: %s", strings.Join(blocked, ","))
}
func generateFinalOutput(results []TaskResult) string {
var sb strings.Builder
// getStatusSymbols returns status symbols based on ASCII mode.
func getStatusSymbols() (success, warning, failed string) {
if os.Getenv("CODEAGENT_ASCII_MODE") == "true" {
return "PASS", "WARN", "FAIL"
}
return "✓", "⚠️", "✗"
}
func generateFinalOutput(results []TaskResult) string {
return generateFinalOutputWithMode(results, true) // default to summary mode
}
// generateFinalOutputWithMode generates output based on mode
// summaryOnly=true: structured report - every token has value
// summaryOnly=false: full output with complete messages (legacy behavior)
func generateFinalOutputWithMode(results []TaskResult, summaryOnly bool) string {
var sb strings.Builder
successSymbol, warningSymbol, failedSymbol := getStatusSymbols()
reportCoverageTarget := defaultCoverageTarget
for _, res := range results {
if res.CoverageTarget > 0 {
reportCoverageTarget = res.CoverageTarget
break
}
}
// Count results by status
success := 0
failed := 0
belowTarget := 0
for _, res := range results {
if res.ExitCode == 0 && res.Error == "" {
success++
target := res.CoverageTarget
if target <= 0 {
target = reportCoverageTarget
}
if res.Coverage != "" && target > 0 && res.CoverageNum < target {
belowTarget++
}
} else {
failed++
}
}
sb.WriteString(fmt.Sprintf("=== Parallel Execution Summary ===\n"))
sb.WriteString(fmt.Sprintf("Total: %d | Success: %d | Failed: %d\n\n", len(results), success, failed))
if summaryOnly {
// Header
sb.WriteString("=== Execution Report ===\n")
sb.WriteString(fmt.Sprintf("%d tasks | %d passed | %d failed", len(results), success, failed))
if belowTarget > 0 {
sb.WriteString(fmt.Sprintf(" | %d below %.0f%%", belowTarget, reportCoverageTarget))
}
sb.WriteString("\n\n")
// Task Results - each task gets: Did + Files + Tests + Coverage
sb.WriteString("## Task Results\n")
for _, res := range results {
taskID := sanitizeOutput(res.TaskID)
coverage := sanitizeOutput(res.Coverage)
keyOutput := sanitizeOutput(res.KeyOutput)
logPath := sanitizeOutput(res.LogPath)
filesChanged := sanitizeOutput(strings.Join(res.FilesChanged, ", "))
target := res.CoverageTarget
if target <= 0 {
target = reportCoverageTarget
}
isSuccess := res.ExitCode == 0 && res.Error == ""
isBelowTarget := isSuccess && coverage != "" && target > 0 && res.CoverageNum < target
if isSuccess && !isBelowTarget {
// Passed task: one block with Did/Files/Tests
sb.WriteString(fmt.Sprintf("\n### %s %s", taskID, successSymbol))
if coverage != "" {
sb.WriteString(fmt.Sprintf(" %s", coverage))
}
sb.WriteString("\n")
if keyOutput != "" {
sb.WriteString(fmt.Sprintf("Did: %s\n", keyOutput))
}
if len(res.FilesChanged) > 0 {
sb.WriteString(fmt.Sprintf("Files: %s\n", filesChanged))
}
if res.TestsPassed > 0 {
sb.WriteString(fmt.Sprintf("Tests: %d passed\n", res.TestsPassed))
}
if logPath != "" {
sb.WriteString(fmt.Sprintf("Log: %s\n", logPath))
}
} else if isSuccess && isBelowTarget {
// Below target: add Gap info
sb.WriteString(fmt.Sprintf("\n### %s %s %s (below %.0f%%)\n", taskID, warningSymbol, coverage, target))
if keyOutput != "" {
sb.WriteString(fmt.Sprintf("Did: %s\n", keyOutput))
}
if len(res.FilesChanged) > 0 {
sb.WriteString(fmt.Sprintf("Files: %s\n", filesChanged))
}
if res.TestsPassed > 0 {
sb.WriteString(fmt.Sprintf("Tests: %d passed\n", res.TestsPassed))
}
// Extract what's missing from coverage
gap := sanitizeOutput(extractCoverageGap(res.Message))
if gap != "" {
sb.WriteString(fmt.Sprintf("Gap: %s\n", gap))
}
if logPath != "" {
sb.WriteString(fmt.Sprintf("Log: %s\n", logPath))
}
for _, res := range results {
sb.WriteString(fmt.Sprintf("--- Task: %s ---\n", res.TaskID))
if res.Error != "" {
sb.WriteString(fmt.Sprintf("Status: FAILED (exit code %d)\nError: %s\n", res.ExitCode, res.Error))
} else if res.ExitCode != 0 {
sb.WriteString(fmt.Sprintf("Status: FAILED (exit code %d)\n", res.ExitCode))
} else {
sb.WriteString("Status: SUCCESS\n")
}
if res.SessionID != "" {
sb.WriteString(fmt.Sprintf("Session: %s\n", res.SessionID))
}
if res.LogPath != "" {
if res.sharedLog {
sb.WriteString(fmt.Sprintf("Log: %s (shared)\n", res.LogPath))
} else {
sb.WriteString(fmt.Sprintf("Log: %s\n", res.LogPath))
// Failed task: show error detail
sb.WriteString(fmt.Sprintf("\n### %s %s FAILED\n", taskID, failedSymbol))
sb.WriteString(fmt.Sprintf("Exit code: %d\n", res.ExitCode))
if errText := sanitizeOutput(res.Error); errText != "" {
sb.WriteString(fmt.Sprintf("Error: %s\n", errText))
}
// Show context from output (last meaningful lines)
detail := sanitizeOutput(extractErrorDetail(res.Message, 300))
if detail != "" {
sb.WriteString(fmt.Sprintf("Detail: %s\n", detail))
}
if logPath != "" {
sb.WriteString(fmt.Sprintf("Log: %s\n", logPath))
}
}
}
if res.Message != "" {
sb.WriteString(fmt.Sprintf("\n%s\n", res.Message))
// Summary section
sb.WriteString("\n## Summary\n")
sb.WriteString(fmt.Sprintf("- %d/%d completed successfully\n", success, len(results)))
if belowTarget > 0 || failed > 0 {
var needFix []string
var needCoverage []string
for _, res := range results {
if res.ExitCode != 0 || res.Error != "" {
taskID := sanitizeOutput(res.TaskID)
reason := sanitizeOutput(res.Error)
if reason == "" && res.ExitCode != 0 {
reason = fmt.Sprintf("exit code %d", res.ExitCode)
}
reason = safeTruncate(reason, 50)
needFix = append(needFix, fmt.Sprintf("%s (%s)", taskID, reason))
continue
}
target := res.CoverageTarget
if target <= 0 {
target = reportCoverageTarget
}
if res.Coverage != "" && target > 0 && res.CoverageNum < target {
needCoverage = append(needCoverage, sanitizeOutput(res.TaskID))
}
}
if len(needFix) > 0 {
sb.WriteString(fmt.Sprintf("- Fix: %s\n", strings.Join(needFix, ", ")))
}
if len(needCoverage) > 0 {
sb.WriteString(fmt.Sprintf("- Coverage: %s\n", strings.Join(needCoverage, ", ")))
}
}
} else {
// Legacy full output mode
sb.WriteString("=== Parallel Execution Summary ===\n")
sb.WriteString(fmt.Sprintf("Total: %d | Success: %d | Failed: %d\n\n", len(results), success, failed))
for _, res := range results {
taskID := sanitizeOutput(res.TaskID)
sb.WriteString(fmt.Sprintf("--- Task: %s ---\n", taskID))
if res.Error != "" {
sb.WriteString(fmt.Sprintf("Status: FAILED (exit code %d)\nError: %s\n", res.ExitCode, sanitizeOutput(res.Error)))
} else if res.ExitCode != 0 {
sb.WriteString(fmt.Sprintf("Status: FAILED (exit code %d)\n", res.ExitCode))
} else {
sb.WriteString("Status: SUCCESS\n")
}
if res.Coverage != "" {
sb.WriteString(fmt.Sprintf("Coverage: %s\n", sanitizeOutput(res.Coverage)))
}
if res.SessionID != "" {
sb.WriteString(fmt.Sprintf("Session: %s\n", sanitizeOutput(res.SessionID)))
}
if res.LogPath != "" {
logPath := sanitizeOutput(res.LogPath)
if res.sharedLog {
sb.WriteString(fmt.Sprintf("Log: %s (shared)\n", logPath))
} else {
sb.WriteString(fmt.Sprintf("Log: %s\n", logPath))
}
}
if res.Message != "" {
message := sanitizeOutput(res.Message)
if message != "" {
sb.WriteString(fmt.Sprintf("\n%s\n", message))
}
}
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return sb.String()

View File

@@ -268,9 +268,15 @@ func TestExecutorHelperCoverage(t *testing.T) {
if !strings.Contains(out, "ok") || !strings.Contains(out, "fail") {
t.Fatalf("unexpected summary output: %s", out)
}
// Test summary mode (default) - should have new format with ### headers
out = generateFinalOutput([]TaskResult{{TaskID: "rich", ExitCode: 0, SessionID: "sess", LogPath: "/tmp/log", Message: "hello"}})
if !strings.Contains(out, "### rich") {
t.Fatalf("summary output missing task header: %s", out)
}
// Test full output mode - should have Session and Message
out = generateFinalOutputWithMode([]TaskResult{{TaskID: "rich", ExitCode: 0, SessionID: "sess", LogPath: "/tmp/log", Message: "hello"}}, false)
if !strings.Contains(out, "Session: sess") || !strings.Contains(out, "Log: /tmp/log") || !strings.Contains(out, "hello") {
t.Fatalf("rich output missing fields: %s", out)
t.Fatalf("full output missing fields: %s", out)
}
args := buildCodexArgs(&Config{Mode: "new", WorkDir: "/tmp"}, "task")
@@ -283,6 +289,45 @@ func TestExecutorHelperCoverage(t *testing.T) {
}
})
t.Run("generateFinalOutputASCIIMode", func(t *testing.T) {
t.Setenv("CODEAGENT_ASCII_MODE", "true")
results := []TaskResult{
{TaskID: "ok", ExitCode: 0, Coverage: "92%", CoverageNum: 92, CoverageTarget: 90, KeyOutput: "done"},
{TaskID: "warn", ExitCode: 0, Coverage: "80%", CoverageNum: 80, CoverageTarget: 90, KeyOutput: "did"},
{TaskID: "bad", ExitCode: 2, Error: "boom"},
}
out := generateFinalOutput(results)
for _, sym := range []string{"PASS", "WARN", "FAIL"} {
if !strings.Contains(out, sym) {
t.Fatalf("ASCII mode should include %q, got: %s", sym, out)
}
}
for _, sym := range []string{"✓", "⚠️", "✗"} {
if strings.Contains(out, sym) {
t.Fatalf("ASCII mode should not include %q, got: %s", sym, out)
}
}
})
t.Run("generateFinalOutputUnicodeMode", func(t *testing.T) {
t.Setenv("CODEAGENT_ASCII_MODE", "false")
results := []TaskResult{
{TaskID: "ok", ExitCode: 0, Coverage: "92%", CoverageNum: 92, CoverageTarget: 90, KeyOutput: "done"},
{TaskID: "warn", ExitCode: 0, Coverage: "80%", CoverageNum: 80, CoverageTarget: 90, KeyOutput: "did"},
{TaskID: "bad", ExitCode: 2, Error: "boom"},
}
out := generateFinalOutput(results)
for _, sym := range []string{"✓", "⚠️", "✗"} {
if !strings.Contains(out, sym) {
t.Fatalf("Unicode mode should include %q, got: %s", sym, out)
}
}
})
t.Run("executeConcurrentWrapper", func(t *testing.T) {
orig := runCodexTaskFn
defer func() { runCodexTaskFn = orig }()
@@ -1111,9 +1156,10 @@ func TestExecutorExecuteConcurrentWithContextBranches(t *testing.T) {
}
}
summary := generateFinalOutput(results)
// Test full output mode for shared marker (summary mode doesn't show it)
summary := generateFinalOutputWithMode(results, false)
if !strings.Contains(summary, "(shared)") {
t.Fatalf("summary missing shared marker: %s", summary)
t.Fatalf("full output missing shared marker: %s", summary)
}
mainLogger.Flush()

View File

@@ -14,14 +14,15 @@ import (
)
const (
version = "5.2.7"
defaultWorkdir = "."
defaultTimeout = 7200 // seconds (2 hours)
codexLogLineLimit = 1000
stdinSpecialChars = "\n\\\"'`$"
stderrCaptureLimit = 4 * 1024
defaultBackendName = "codex"
defaultCodexCommand = "codex"
version = "5.4.0"
defaultWorkdir = "."
defaultTimeout = 7200 // seconds (2 hours)
defaultCoverageTarget = 90.0
codexLogLineLimit = 1000
stdinSpecialChars = "\n\\\"'`$"
stderrCaptureLimit = 4 * 1024
defaultBackendName = "codex"
defaultCodexCommand = "codex"
// stdout close reasons
stdoutCloseReasonWait = "wait-done"
@@ -30,6 +31,8 @@ const (
stdoutDrainTimeout = 100 * time.Millisecond
)
var useASCIIMode = os.Getenv("CODEAGENT_ASCII_MODE") == "true"
// Test hooks for dependency injection
var (
stdinReader io.Reader = os.Stdin
@@ -175,6 +178,7 @@ func run() (exitCode int) {
if parallelIndex != -1 {
backendName := defaultBackendName
fullOutput := false
var extras []string
for i := 0; i < len(args); i++ {
@@ -182,6 +186,8 @@ func run() (exitCode int) {
switch {
case arg == "--parallel":
continue
case arg == "--full-output":
fullOutput = true
case arg == "--backend":
if i+1 >= len(args) {
fmt.Fprintln(os.Stderr, "ERROR: --backend flag requires a value")
@@ -202,11 +208,12 @@ func run() (exitCode int) {
}
if len(extras) > 0 {
fmt.Fprintln(os.Stderr, "ERROR: --parallel reads its task configuration from stdin; only --backend is allowed.")
fmt.Fprintln(os.Stderr, "ERROR: --parallel reads its task configuration from stdin; only --backend and --full-output are allowed.")
fmt.Fprintln(os.Stderr, "Usage examples:")
fmt.Fprintf(os.Stderr, " %s --parallel < tasks.txt\n", name)
fmt.Fprintf(os.Stderr, " echo '...' | %s --parallel\n", name)
fmt.Fprintf(os.Stderr, " %s --parallel <<'EOF'\n", name)
fmt.Fprintf(os.Stderr, " %s --parallel --full-output <<'EOF' # include full task output\n", name)
return 1
}
@@ -244,7 +251,33 @@ func run() (exitCode int) {
}
results := executeConcurrent(layers, timeoutSec)
fmt.Println(generateFinalOutput(results))
// Extract structured report fields from each result
for i := range results {
results[i].CoverageTarget = defaultCoverageTarget
if results[i].Message == "" {
continue
}
lines := strings.Split(results[i].Message, "\n")
// Coverage extraction
results[i].Coverage = extractCoverageFromLines(lines)
results[i].CoverageNum = extractCoverageNum(results[i].Coverage)
// Files changed
results[i].FilesChanged = extractFilesChangedFromLines(lines)
// Test results
results[i].TestsPassed, results[i].TestsFailed = extractTestResultsFromLines(lines)
// Key output summary
results[i].KeyOutput = extractKeyOutputFromLines(lines, 150)
}
// Default: summary mode (context-efficient)
// --full-output: legacy full output mode
fmt.Println(generateFinalOutputWithMode(results, !fullOutput))
exitCode = 0
for _, res := range results {
@@ -447,16 +480,19 @@ Usage:
%[1]s resume <session_id> "task" [workdir]
%[1]s resume <session_id> - [workdir]
%[1]s --parallel Run tasks in parallel (config from stdin)
%[1]s --parallel --full-output Run tasks in parallel with full output (legacy)
%[1]s --version
%[1]s --help
Parallel mode examples:
%[1]s --parallel < tasks.txt
echo '...' | %[1]s --parallel
%[1]s --parallel --full-output < tasks.txt
%[1]s --parallel <<'EOF'
Environment Variables:
CODEX_TIMEOUT Timeout in milliseconds (default: 7200000)
CODEX_TIMEOUT Timeout in milliseconds (default: 7200000)
CODEAGENT_ASCII_MODE Use ASCII symbols instead of Unicode (PASS/WARN/FAIL)
Exit Codes:
0 Success

View File

@@ -46,10 +46,26 @@ func parseIntegrationOutput(t *testing.T, out string) integrationOutput {
lines := strings.Split(out, "\n")
var currentTask *TaskResult
inTaskResults := false
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Total:") {
// Parse new format header: "X tasks | Y passed | Z failed"
if strings.Contains(line, "tasks |") && strings.Contains(line, "passed |") {
parts := strings.Split(line, "|")
for _, p := range parts {
p = strings.TrimSpace(p)
if strings.HasSuffix(p, "tasks") {
fmt.Sscanf(p, "%d tasks", &payload.Summary.Total)
} else if strings.HasSuffix(p, "passed") {
fmt.Sscanf(p, "%d passed", &payload.Summary.Success)
} else if strings.HasSuffix(p, "failed") {
fmt.Sscanf(p, "%d failed", &payload.Summary.Failed)
}
}
} else if strings.HasPrefix(line, "Total:") {
// Legacy format: "Total: X | Success: Y | Failed: Z"
parts := strings.Split(line, "|")
for _, p := range parts {
p = strings.TrimSpace(p)
@@ -61,13 +77,72 @@ func parseIntegrationOutput(t *testing.T, out string) integrationOutput {
fmt.Sscanf(p, "Failed: %d", &payload.Summary.Failed)
}
}
} else if line == "## Task Results" {
inTaskResults = true
} else if line == "## Summary" {
// End of task results section
if currentTask != nil {
payload.Results = append(payload.Results, *currentTask)
currentTask = nil
}
inTaskResults = false
} else if inTaskResults && strings.HasPrefix(line, "### ") {
// New task: ### task-id ✓ 92% or ### task-id PASS 92% (ASCII mode)
if currentTask != nil {
payload.Results = append(payload.Results, *currentTask)
}
currentTask = &TaskResult{}
taskLine := strings.TrimPrefix(line, "### ")
success, warning, failed := getStatusSymbols()
// Parse different formats
if strings.Contains(taskLine, " "+success) {
parts := strings.Split(taskLine, " "+success)
currentTask.TaskID = strings.TrimSpace(parts[0])
currentTask.ExitCode = 0
// Extract coverage if present
if len(parts) > 1 {
coveragePart := strings.TrimSpace(parts[1])
if strings.HasSuffix(coveragePart, "%") {
currentTask.Coverage = coveragePart
}
}
} else if strings.Contains(taskLine, " "+warning) {
parts := strings.Split(taskLine, " "+warning)
currentTask.TaskID = strings.TrimSpace(parts[0])
currentTask.ExitCode = 0
} else if strings.Contains(taskLine, " "+failed) {
parts := strings.Split(taskLine, " "+failed)
currentTask.TaskID = strings.TrimSpace(parts[0])
currentTask.ExitCode = 1
} else {
currentTask.TaskID = taskLine
}
} else if currentTask != nil && inTaskResults {
// Parse task details
if strings.HasPrefix(line, "Exit code:") {
fmt.Sscanf(line, "Exit code: %d", &currentTask.ExitCode)
} else if strings.HasPrefix(line, "Error:") {
currentTask.Error = strings.TrimPrefix(line, "Error: ")
} else if strings.HasPrefix(line, "Log:") {
currentTask.LogPath = strings.TrimSpace(strings.TrimPrefix(line, "Log:"))
} else if strings.HasPrefix(line, "Did:") {
currentTask.KeyOutput = strings.TrimSpace(strings.TrimPrefix(line, "Did:"))
} else if strings.HasPrefix(line, "Detail:") {
// Error detail for failed tasks
if currentTask.Message == "" {
currentTask.Message = strings.TrimSpace(strings.TrimPrefix(line, "Detail:"))
}
}
} else if strings.HasPrefix(line, "--- Task:") {
// Legacy full output format
if currentTask != nil {
payload.Results = append(payload.Results, *currentTask)
}
currentTask = &TaskResult{}
currentTask.TaskID = strings.TrimSuffix(strings.TrimPrefix(line, "--- Task: "), " ---")
} else if currentTask != nil {
} else if currentTask != nil && !inTaskResults {
// Legacy format parsing
if strings.HasPrefix(line, "Status: SUCCESS") {
currentTask.ExitCode = 0
} else if strings.HasPrefix(line, "Status: FAILED") {
@@ -82,15 +157,11 @@ func parseIntegrationOutput(t *testing.T, out string) integrationOutput {
currentTask.SessionID = strings.TrimPrefix(line, "Session: ")
} else if strings.HasPrefix(line, "Log:") {
currentTask.LogPath = strings.TrimSpace(strings.TrimPrefix(line, "Log:"))
} else if line != "" && !strings.HasPrefix(line, "===") && !strings.HasPrefix(line, "---") {
if currentTask.Message != "" {
currentTask.Message += "\n"
}
currentTask.Message += line
}
}
}
// Handle last task
if currentTask != nil {
payload.Results = append(payload.Results, *currentTask)
}
@@ -343,9 +414,10 @@ task-beta`
}
for _, id := range []string{"alpha", "beta"} {
want := fmt.Sprintf("Log: %s", logPathFor(id))
if !strings.Contains(output, want) {
t.Fatalf("parallel output missing %q for %s:\n%s", want, id, output)
// Summary mode shows log paths in table format, not "Log: xxx"
logPath := logPathFor(id)
if !strings.Contains(output, logPath) {
t.Fatalf("parallel output missing log path %q for %s:\n%s", logPath, id, output)
}
}
}
@@ -550,16 +622,16 @@ ok-e`
if resD.LogPath != logPathFor("D") || resE.LogPath != logPathFor("E") {
t.Fatalf("expected log paths for D/E, got D=%q E=%q", resD.LogPath, resE.LogPath)
}
// Summary mode shows log paths in table, verify they appear in output
for _, id := range []string{"A", "D", "E"} {
block := extractTaskBlock(t, output, id)
want := fmt.Sprintf("Log: %s", logPathFor(id))
if !strings.Contains(block, want) {
t.Fatalf("task %s block missing %q:\n%s", id, want, block)
logPath := logPathFor(id)
if !strings.Contains(output, logPath) {
t.Fatalf("task %s log path %q not found in output:\n%s", id, logPath, output)
}
}
blockB := extractTaskBlock(t, output, "B")
if strings.Contains(blockB, "Log:") {
t.Fatalf("skipped task B should not emit a log line:\n%s", blockB)
// Task B was skipped, should have "-" or empty log path in table
if resB.LogPath != "" {
t.Fatalf("skipped task B should have empty log path, got %q", resB.LogPath)
}
}

View File

@@ -2633,14 +2633,17 @@ func TestRunGenerateFinalOutput(t *testing.T) {
if out == "" {
t.Fatalf("generateFinalOutput() returned empty string")
}
if !strings.Contains(out, "Total: 3") || !strings.Contains(out, "Success: 2") || !strings.Contains(out, "Failed: 1") {
// New format: "X tasks | Y passed | Z failed"
if !strings.Contains(out, "3 tasks") || !strings.Contains(out, "2 passed") || !strings.Contains(out, "1 failed") {
t.Fatalf("summary missing, got %q", out)
}
if !strings.Contains(out, "Task: a") || !strings.Contains(out, "Task: b") {
t.Fatalf("task entries missing")
// New format uses ### task-id for each task
if !strings.Contains(out, "### a") || !strings.Contains(out, "### b") {
t.Fatalf("task entries missing in structured format")
}
if strings.Contains(out, "Log:") {
t.Fatalf("unexpected log line when LogPath empty, got %q", out)
// Should have Summary section
if !strings.Contains(out, "## Summary") {
t.Fatalf("Summary section missing, got %q", out)
}
}
@@ -2660,12 +2663,18 @@ func TestRunGenerateFinalOutput_LogPath(t *testing.T) {
LogPath: "/tmp/log-b",
},
}
// Test summary mode (default) - should contain log paths
out := generateFinalOutput(results)
if !strings.Contains(out, "Session: sid\nLog: /tmp/log-a") {
t.Fatalf("output missing log line after session: %q", out)
if !strings.Contains(out, "/tmp/log-b") {
t.Fatalf("summary output missing log path for failed task: %q", out)
}
// Test full output mode - shows Session: and Log: lines
out = generateFinalOutputWithMode(results, false)
if !strings.Contains(out, "Session: sid") || !strings.Contains(out, "Log: /tmp/log-a") {
t.Fatalf("full output missing log line after session: %q", out)
}
if !strings.Contains(out, "Log: /tmp/log-b") {
t.Fatalf("output missing log line for failed task: %q", out)
t.Fatalf("full output missing log line for failed task: %q", out)
}
}
@@ -2963,6 +2972,46 @@ test`
}
}
func TestRunParallelWithFullOutput(t *testing.T) {
defer resetTestHooks()
cleanupLogsFn = func() (CleanupStats, error) { return CleanupStats{}, nil }
oldArgs := os.Args
t.Cleanup(func() { os.Args = oldArgs })
os.Args = []string{"codeagent-wrapper", "--parallel", "--full-output"}
stdinReader = strings.NewReader(`---TASK---
id: T1
---CONTENT---
noop`)
t.Cleanup(func() { stdinReader = os.Stdin })
orig := runCodexTaskFn
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
return TaskResult{TaskID: task.ID, ExitCode: 0, Message: "full output marker"}
}
t.Cleanup(func() { runCodexTaskFn = orig })
out := captureOutput(t, func() {
if code := run(); code != 0 {
t.Fatalf("run exit = %d, want 0", code)
}
})
if !strings.Contains(out, "=== Parallel Execution Summary ===") {
t.Fatalf("output missing full-output header, got %q", out)
}
if !strings.Contains(out, "--- Task: T1 ---") {
t.Fatalf("output missing task block, got %q", out)
}
if !strings.Contains(out, "full output marker") {
t.Fatalf("output missing task message, got %q", out)
}
if strings.Contains(out, "=== Execution Report ===") {
t.Fatalf("output should not include summary-only header, got %q", out)
}
}
func TestParallelInvalidBackend(t *testing.T) {
defer resetTestHooks()
cleanupLogsFn = func() (CleanupStats, error) { return CleanupStats{}, nil }
@@ -3017,7 +3066,9 @@ func TestVersionFlag(t *testing.T) {
t.Errorf("exit = %d, want 0", code)
}
})
want := "codeagent-wrapper version 5.2.7\n"
want := "codeagent-wrapper version 5.4.0\n"
if output != want {
t.Fatalf("output = %q, want %q", output, want)
}
@@ -3031,7 +3082,9 @@ func TestVersionShortFlag(t *testing.T) {
t.Errorf("exit = %d, want 0", code)
}
})
want := "codeagent-wrapper version 5.2.7\n"
want := "codeagent-wrapper version 5.4.0\n"
if output != want {
t.Fatalf("output = %q, want %q", output, want)
}
@@ -3045,7 +3098,9 @@ func TestVersionLegacyAlias(t *testing.T) {
t.Errorf("exit = %d, want 0", code)
}
})
want := "codex-wrapper version 5.2.7\n"
want := "codex-wrapper version 5.4.0\n"
if output != want {
t.Fatalf("output = %q, want %q", output, want)
}

View File

@@ -271,8 +271,8 @@ func parseJSONStreamInternal(r io.Reader, warnFn func(string), infoFn func(strin
continue
}
// Unknown event format
warnFn(fmt.Sprintf("Unknown event format: %s", truncateBytes(line, 100)))
// Unknown event format from other backends (turn.started/assistant/user); ignore.
continue
}
switch {

View File

@@ -0,0 +1,33 @@
package main
import (
"strings"
"testing"
)
func TestBackendParseJSONStream_UnknownEventsAreSilent(t *testing.T) {
input := strings.Join([]string{
`{"type":"turn.started"}`,
`{"type":"assistant","text":"hi"}`,
`{"type":"user","text":"yo"}`,
`{"type":"item.completed","item":{"type":"agent_message","text":"ok"}}`,
}, "\n")
var infos []string
infoFn := func(msg string) { infos = append(infos, msg) }
message, threadID := parseJSONStreamInternal(strings.NewReader(input), nil, infoFn, nil, nil)
if message != "ok" {
t.Fatalf("message=%q, want %q (infos=%v)", message, "ok", infos)
}
if threadID != "" {
t.Fatalf("threadID=%q, want empty (infos=%v)", threadID, infos)
}
for _, msg := range infos {
if strings.Contains(msg, "Agent event:") {
t.Fatalf("unexpected log for unknown event: %q", msg)
}
}
}

View File

@@ -75,9 +75,9 @@ func getEnv(key, defaultValue string) string {
}
type logWriter struct {
prefix string
maxLen int
buf bytes.Buffer
prefix string
maxLen int
buf bytes.Buffer
dropped bool
}
@@ -205,6 +205,55 @@ func truncate(s string, maxLen int) string {
return s[:maxLen] + "..."
}
// safeTruncate safely truncates string to maxLen, avoiding panic and UTF-8 corruption.
func safeTruncate(s string, maxLen int) string {
if maxLen <= 0 || s == "" {
return ""
}
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
if maxLen < 4 {
return string(runes[:1])
}
cutoff := maxLen - 3
if cutoff <= 0 {
return string(runes[:1])
}
if len(runes) <= cutoff {
return s
}
return string(runes[:cutoff]) + "..."
}
// sanitizeOutput removes ANSI escape sequences and control characters.
func sanitizeOutput(s string) string {
var result strings.Builder
inEscape := false
for i := 0; i < len(s); i++ {
if s[i] == '\x1b' && i+1 < len(s) && s[i+1] == '[' {
inEscape = true
i++ // skip '['
continue
}
if inEscape {
if (s[i] >= 'A' && s[i] <= 'Z') || (s[i] >= 'a' && s[i] <= 'z') {
inEscape = false
}
continue
}
// Keep printable chars and common whitespace.
if s[i] >= 32 || s[i] == '\n' || s[i] == '\t' {
result.WriteByte(s[i])
}
}
return result.String()
}
func min(a, b int) int {
if a < b {
return a
@@ -223,3 +272,444 @@ func greet(name string) string {
func farewell(name string) string {
return "goodbye " + name
}
// extractMessageSummary extracts a brief summary from task output
// Returns first meaningful line or truncated content up to maxLen chars
func extractMessageSummary(message string, maxLen int) string {
if message == "" || maxLen <= 0 {
return ""
}
// Try to find a meaningful summary line
lines := strings.Split(message, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
// Skip empty lines and common noise
if line == "" || strings.HasPrefix(line, "```") || strings.HasPrefix(line, "---") {
continue
}
// Found a meaningful line
return safeTruncate(line, maxLen)
}
// Fallback: truncate entire message
clean := strings.TrimSpace(message)
return safeTruncate(clean, maxLen)
}
// extractCoverageFromLines extracts coverage from pre-split lines.
func extractCoverageFromLines(lines []string) string {
if len(lines) == 0 {
return ""
}
end := len(lines)
for end > 0 && strings.TrimSpace(lines[end-1]) == "" {
end--
}
if end == 1 {
trimmed := strings.TrimSpace(lines[0])
if strings.HasSuffix(trimmed, "%") {
if num, err := strconv.ParseFloat(strings.TrimSuffix(trimmed, "%"), 64); err == nil && num >= 0 && num <= 100 {
return trimmed
}
}
}
coverageKeywords := []string{"file", "stmt", "branch", "line", "coverage", "total"}
for _, line := range lines[:end] {
lower := strings.ToLower(line)
hasKeyword := false
tokens := strings.FieldsFunc(lower, func(r rune) bool { return r < 'a' || r > 'z' })
for _, token := range tokens {
for _, kw := range coverageKeywords {
if strings.HasPrefix(token, kw) {
hasKeyword = true
break
}
}
if hasKeyword {
break
}
}
if !hasKeyword {
continue
}
if !strings.Contains(line, "%") {
continue
}
// Extract percentage pattern: number followed by %
for i := 0; i < len(line); i++ {
if line[i] == '%' && i > 0 {
// Walk back to find the number
j := i - 1
for j >= 0 && (line[j] == '.' || (line[j] >= '0' && line[j] <= '9')) {
j--
}
if j < i-1 {
numStr := line[j+1 : i]
// Validate it's a reasonable percentage
if num, err := strconv.ParseFloat(numStr, 64); err == nil && num >= 0 && num <= 100 {
return numStr + "%"
}
}
}
}
}
return ""
}
// extractCoverage extracts coverage percentage from task output
// Supports common formats: "Coverage: 92%", "92% coverage", "coverage 92%", "TOTAL 92%"
func extractCoverage(message string) string {
if message == "" {
return ""
}
return extractCoverageFromLines(strings.Split(message, "\n"))
}
// extractCoverageNum extracts coverage as a numeric value for comparison
func extractCoverageNum(coverage string) float64 {
if coverage == "" {
return 0
}
// Remove % sign and parse
numStr := strings.TrimSuffix(coverage, "%")
if num, err := strconv.ParseFloat(numStr, 64); err == nil {
return num
}
return 0
}
// extractFilesChangedFromLines extracts files from pre-split lines.
func extractFilesChangedFromLines(lines []string) []string {
if len(lines) == 0 {
return nil
}
var files []string
seen := make(map[string]bool)
exts := []string{".ts", ".tsx", ".js", ".jsx", ".go", ".py", ".rs", ".java", ".vue", ".css", ".scss", ".md", ".json", ".yaml", ".yml", ".toml"}
for _, line := range lines {
line = strings.TrimSpace(line)
// Pattern 1: "Modified: path/to/file.ts" or "Created: path/to/file.ts"
matchedPrefix := false
for _, prefix := range []string{"Modified:", "Created:", "Updated:", "Edited:", "Wrote:", "Changed:"} {
if strings.HasPrefix(line, prefix) {
file := strings.TrimSpace(strings.TrimPrefix(line, prefix))
file = strings.Trim(file, "`,\"'()[],:")
file = strings.TrimPrefix(file, "@")
if file != "" && !seen[file] {
files = append(files, file)
seen[file] = true
}
matchedPrefix = true
break
}
}
if matchedPrefix {
continue
}
// Pattern 2: Tokens that look like file paths (allow root files, strip @ prefix).
parts := strings.Fields(line)
for _, part := range parts {
part = strings.Trim(part, "`,\"'()[],:")
part = strings.TrimPrefix(part, "@")
for _, ext := range exts {
if strings.HasSuffix(part, ext) && !seen[part] {
files = append(files, part)
seen[part] = true
break
}
}
}
}
// Limit to first 10 files to avoid bloat
if len(files) > 10 {
files = files[:10]
}
return files
}
// extractFilesChanged extracts list of changed files from task output
// Looks for common patterns like "Modified: file.ts", "Created: file.ts", file paths in output
func extractFilesChanged(message string) []string {
if message == "" {
return nil
}
return extractFilesChangedFromLines(strings.Split(message, "\n"))
}
// extractTestResultsFromLines extracts test results from pre-split lines.
func extractTestResultsFromLines(lines []string) (passed, failed int) {
if len(lines) == 0 {
return 0, 0
}
// Common patterns:
// pytest: "12 passed, 2 failed"
// jest: "Tests: 2 failed, 12 passed"
// go: "ok ... 12 tests"
for _, line := range lines {
line = strings.ToLower(line)
// Look for test result lines
if !strings.Contains(line, "pass") && !strings.Contains(line, "fail") && !strings.Contains(line, "test") {
continue
}
// Extract numbers near "passed" or "pass"
if idx := strings.Index(line, "pass"); idx != -1 {
// Look for number before "pass"
num := extractNumberBefore(line, idx)
if num > 0 {
passed = num
}
}
// Extract numbers near "failed" or "fail"
if idx := strings.Index(line, "fail"); idx != -1 {
num := extractNumberBefore(line, idx)
if num > 0 {
failed = num
}
}
// go test style: "ok ... 12 tests"
if passed == 0 {
if idx := strings.Index(line, "test"); idx != -1 {
num := extractNumberBefore(line, idx)
if num > 0 {
passed = num
}
}
}
// If we found both, stop
if passed > 0 && failed > 0 {
break
}
}
return passed, failed
}
// extractTestResults extracts test pass/fail counts from task output
func extractTestResults(message string) (passed, failed int) {
if message == "" {
return 0, 0
}
return extractTestResultsFromLines(strings.Split(message, "\n"))
}
// extractNumberBefore extracts a number that appears before the given index
func extractNumberBefore(s string, idx int) int {
if idx <= 0 {
return 0
}
// Walk backwards to find digits
end := idx - 1
for end >= 0 && (s[end] == ' ' || s[end] == ':' || s[end] == ',') {
end--
}
if end < 0 {
return 0
}
start := end
for start >= 0 && s[start] >= '0' && s[start] <= '9' {
start--
}
start++
if start > end {
return 0
}
numStr := s[start : end+1]
if num, err := strconv.Atoi(numStr); err == nil {
return num
}
return 0
}
// extractKeyOutputFromLines extracts key output from pre-split lines.
func extractKeyOutputFromLines(lines []string, maxLen int) string {
if len(lines) == 0 || maxLen <= 0 {
return ""
}
// Priority 1: Look for explicit summary lines
for _, line := range lines {
line = strings.TrimSpace(line)
lower := strings.ToLower(line)
if strings.HasPrefix(lower, "summary:") || strings.HasPrefix(lower, "completed:") ||
strings.HasPrefix(lower, "implemented:") || strings.HasPrefix(lower, "added:") ||
strings.HasPrefix(lower, "created:") || strings.HasPrefix(lower, "fixed:") {
content := line
for _, prefix := range []string{"Summary:", "Completed:", "Implemented:", "Added:", "Created:", "Fixed:",
"summary:", "completed:", "implemented:", "added:", "created:", "fixed:"} {
content = strings.TrimPrefix(content, prefix)
}
content = strings.TrimSpace(content)
if len(content) > 0 {
return safeTruncate(content, maxLen)
}
}
}
// Priority 2: First meaningful line (skip noise)
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "```") || strings.HasPrefix(line, "---") ||
strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
continue
}
// Skip very short lines (likely headers or markers)
if len(line) < 20 {
continue
}
return safeTruncate(line, maxLen)
}
// Fallback: truncate entire message
clean := strings.TrimSpace(strings.Join(lines, "\n"))
return safeTruncate(clean, maxLen)
}
// extractKeyOutput extracts a brief summary of what the task accomplished
// Looks for summary lines, first meaningful sentence, or truncates message
func extractKeyOutput(message string, maxLen int) string {
if message == "" || maxLen <= 0 {
return ""
}
return extractKeyOutputFromLines(strings.Split(message, "\n"), maxLen)
}
// extractCoverageGap extracts what's missing from coverage reports
// Looks for uncovered lines, branches, or functions
func extractCoverageGap(message string) string {
if message == "" {
return ""
}
lower := strings.ToLower(message)
lines := strings.Split(message, "\n")
// Look for uncovered/missing patterns
for _, line := range lines {
lineLower := strings.ToLower(line)
line = strings.TrimSpace(line)
// Common patterns for uncovered code
if strings.Contains(lineLower, "uncovered") ||
strings.Contains(lineLower, "not covered") ||
strings.Contains(lineLower, "missing coverage") ||
strings.Contains(lineLower, "lines not covered") {
if len(line) > 100 {
return line[:97] + "..."
}
return line
}
// Look for specific file:line patterns in coverage reports
if strings.Contains(lineLower, "branch") && strings.Contains(lineLower, "not taken") {
if len(line) > 100 {
return line[:97] + "..."
}
return line
}
}
// Look for function names that aren't covered
if strings.Contains(lower, "function") && strings.Contains(lower, "0%") {
for _, line := range lines {
if strings.Contains(strings.ToLower(line), "0%") && strings.Contains(line, "function") {
line = strings.TrimSpace(line)
if len(line) > 100 {
return line[:97] + "..."
}
return line
}
}
}
return ""
}
// extractErrorDetail extracts meaningful error context from task output
// Returns the most relevant error information up to maxLen characters
func extractErrorDetail(message string, maxLen int) string {
if message == "" || maxLen <= 0 {
return ""
}
lines := strings.Split(message, "\n")
var errorLines []string
// Look for error-related lines
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
lower := strings.ToLower(line)
// Skip noise lines
if strings.HasPrefix(line, "at ") && strings.Contains(line, "(") {
// Stack trace line - only keep first one
if len(errorLines) > 0 && strings.HasPrefix(strings.ToLower(errorLines[len(errorLines)-1]), "at ") {
continue
}
}
// Prioritize error/fail lines
if strings.Contains(lower, "error") ||
strings.Contains(lower, "fail") ||
strings.Contains(lower, "exception") ||
strings.Contains(lower, "assert") ||
strings.Contains(lower, "expected") ||
strings.Contains(lower, "timeout") ||
strings.Contains(lower, "not found") ||
strings.Contains(lower, "cannot") ||
strings.Contains(lower, "undefined") ||
strings.HasPrefix(line, "FAIL") ||
strings.HasPrefix(line, "●") {
errorLines = append(errorLines, line)
}
}
if len(errorLines) == 0 {
// No specific error lines found, take last few lines
start := len(lines) - 5
if start < 0 {
start = 0
}
for _, line := range lines[start:] {
line = strings.TrimSpace(line)
if line != "" {
errorLines = append(errorLines, line)
}
}
}
// Join and truncate
result := strings.Join(errorLines, " | ")
return safeTruncate(result, maxLen)
}

View File

@@ -0,0 +1,143 @@
package main
import (
"fmt"
"reflect"
"strings"
"testing"
)
func TestExtractCoverage(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{"bare int", "92%", "92%"},
{"bare float", "92.5%", "92.5%"},
{"coverage prefix", "coverage: 92%", "92%"},
{"total prefix", "TOTAL 92%", "92%"},
{"all files", "All files 92%", "92%"},
{"empty", "", ""},
{"no number", "coverage: N/A", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := extractCoverage(tt.in); got != tt.want {
t.Fatalf("extractCoverage(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
func TestExtractTestResults(t *testing.T) {
tests := []struct {
name string
in string
wantPassed int
wantFailed int
}{
{"pytest one line", "12 passed, 2 failed", 12, 2},
{"pytest split lines", "12 passed\n2 failed", 12, 2},
{"jest format", "Tests: 2 failed, 12 passed, 14 total", 12, 2},
{"go test style count", "ok\texample.com/foo\t0.12s\t12 tests", 12, 0},
{"zero counts", "0 passed, 0 failed", 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
passed, failed := extractTestResults(tt.in)
if passed != tt.wantPassed || failed != tt.wantFailed {
t.Fatalf("extractTestResults(%q) = (%d, %d), want (%d, %d)", tt.in, passed, failed, tt.wantPassed, tt.wantFailed)
}
})
}
}
func TestExtractFilesChanged(t *testing.T) {
tests := []struct {
name string
in string
want []string
}{
{"root file", "Modified: main.go\n", []string{"main.go"}},
{"path file", "Created: codeagent-wrapper/utils.go\n", []string{"codeagent-wrapper/utils.go"}},
{"at prefix", "Updated: @codeagent-wrapper/main.go\n", []string{"codeagent-wrapper/main.go"}},
{"token scan", "Files: @main.go, @codeagent-wrapper/utils.go\n", []string{"main.go", "codeagent-wrapper/utils.go"}},
{"space path", "Modified: dir/with space/file.go\n", []string{"dir/with space/file.go"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := extractFilesChanged(tt.in); !reflect.DeepEqual(got, tt.want) {
t.Fatalf("extractFilesChanged(%q) = %#v, want %#v", tt.in, got, tt.want)
}
})
}
t.Run("limits to first 10", func(t *testing.T) {
var b strings.Builder
for i := 0; i < 12; i++ {
fmt.Fprintf(&b, "Modified: file%d.go\n", i)
}
got := extractFilesChanged(b.String())
if len(got) != 10 {
t.Fatalf("len(files)=%d, want 10: %#v", len(got), got)
}
for i := 0; i < 10; i++ {
want := fmt.Sprintf("file%d.go", i)
if got[i] != want {
t.Fatalf("files[%d]=%q, want %q", i, got[i], want)
}
}
})
}
func TestSafeTruncate(t *testing.T) {
tests := []struct {
name string
in string
maxLen int
want string
}{
{"empty", "", 4, ""},
{"zero maxLen", "hello", 0, ""},
{"one rune", "你好", 1, "你"},
{"two runes no truncate", "你好", 2, "你好"},
{"three runes no truncate", "你好", 3, "你好"},
{"two runes truncates long", "你好世界", 2, "你"},
{"three runes truncates long", "你好世界", 3, "你"},
{"four with ellipsis", "你好世界啊", 4, "你..."},
{"emoji", "🙂🙂🙂🙂🙂", 4, "🙂..."},
{"no truncate", "你好世界", 4, "你好世界"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := safeTruncate(tt.in, tt.maxLen); got != tt.want {
t.Fatalf("safeTruncate(%q, %d) = %q, want %q", tt.in, tt.maxLen, got, tt.want)
}
})
}
}
func TestSanitizeOutput(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{"ansi", "\x1b[31mred\x1b[0m", "red"},
{"control chars", "a\x07b\r\nc\t", "ab\nc\t"},
{"normal", "hello\nworld\t!", "hello\nworld\t!"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := sanitizeOutput(tt.in); got != tt.want {
t.Fatalf("sanitizeOutput(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}

View File

@@ -9,42 +9,56 @@ A freshly designed lightweight development workflow with no legacy baggage, focu
```
/dev trigger
AskUserQuestion (backend selection)
AskUserQuestion (requirements clarification)
codeagent analysis (plan mode + UI auto-detection)
codeagent analysis (plan mode + task typing + UI auto-detection)
dev-plan-generator (create dev doc)
codeagent concurrent development (25 tasks, backend split)
codeagent concurrent development (25 tasks, backend routing)
codeagent testing & verification (≥90% coverage)
Done (generate summary)
```
## The 6 Steps
## Step 0 + The 6 Steps
### 0. Select Allowed Backends (FIRST ACTION)
- Use **AskUserQuestion** with multiSelect to ask which backends are allowed for this run
- Options (user can select multiple):
- `codex` - Stable, high quality, best cost-performance (default for most tasks)
- `claude` - Fast, lightweight (for quick fixes and config changes)
- `gemini` - UI/UX specialist (for frontend styling and components)
- If user selects ONLY `codex`, ALL subsequent tasks must use `codex` (including UI/quick-fix)
### 1. Clarify Requirements
- Use **AskUserQuestion** to ask the user directly
- No scoring system, no complex logic
- 23 rounds of Q&A until the requirement is clear
### 2. codeagent Analysis & UI Detection
### 2. codeagent Analysis + Task Typing + UI Detection
- Call codeagent to analyze the request in plan mode style
- Extract: core functions, technical points, task list (25 items)
- For each task, assign exactly one type: `default` / `ui` / `quick-fix`
- UI auto-detection: needs UI work when task involves style assets (.css, .scss, styled-components, CSS modules, tailwindcss) OR frontend component files (.tsx, .jsx, .vue); output yes/no plus evidence
### 3. Generate Dev Doc
- Call the **dev-plan-generator** agent
- Produce a single `dev-plan.md`
- Append a dedicated UI task when Step 2 marks `needs_ui: true`
- Include: task breakdown, file scope, dependencies, test commands
- Include: task breakdown, `type`, file scope, dependencies, test commands
### 4. Concurrent Development
- Work from the task list in dev-plan.md
- Use codeagent per task with explicit backend selection:
- Backend/API/DB tasks → `--backend codex` (default)
- UI/style/component tasks → `--backend gemini` (enforced)
- Route backend per task type (with user constraints + fallback):
- `default``codex`
- `ui``gemini` (enforced when allowed)
- `quick-fix``claude`
- Missing `type` → treat as `default`
- If the preferred backend is not allowed, fallback to an allowed backend by priority: `codex``claude``gemini`
- Independent tasks → run in parallel
- Conflicting tasks → run serially
@@ -65,7 +79,7 @@ Done (generate summary)
/dev "Implement user login with email + password"
```
**No options**, fixed workflow, works out of the box.
No CLI flags required; workflow starts with an interactive backend selection.
## Output Structure
@@ -80,14 +94,14 @@ Only one file—minimal and clear.
### Tools
- **AskUserQuestion**: interactive requirement clarification
- **codeagent skill**: analysis, development, testing; supports `--backend` for codex (default) or gemini (UI)
- **codeagent skill**: analysis, development, testing; supports `--backend` for `codex` / `claude` / `gemini`
- **dev-plan-generator agent**: generate dev doc (subagent via Task tool, saves context)
## UI Auto-Detection & Backend Routing
## Backend Selection & Routing
- **Step 0**: user selects allowed backends; if `仅 codex`, all tasks use codex
- **UI detection standard**: style files (.css, .scss, styled-components, CSS modules, tailwindcss) OR frontend component code (.tsx, .jsx, .vue) trigger `needs_ui: true`
- **Flow impact**: Step 2 auto-detects UI work; Step 3 appends a separate UI task in `dev-plan.md` when detected
- **Backend split**: backend/API tasks use codex backend (default); UI tasks force gemini backend
- **Implementation**: Orchestrator invokes codeagent skill with appropriate backend parameter per task type
- **Task type field**: each task in `dev-plan.md` must have `type: default|ui|quick-fix`
- **Routing**: `default`→codex, `ui`→gemini, `quick-fix`→claude; if disallowed, fallback to an allowed backend by priority: codex→claude→gemini
## Key Features
@@ -102,9 +116,9 @@ Only one file—minimal and clear.
- Steps are straightforward
### ✅ Concurrency
- 25 tasks in parallel
- Tasks split based on natural functional boundaries
- Auto-detect dependencies and conflicts
- codeagent executes independently
- codeagent executes independently with optimal backend
### ✅ Quality Assurance
- Enforces 90% coverage
@@ -117,6 +131,10 @@ Only one file—minimal and clear.
# Trigger
/dev "Add user login feature"
# Step 0: Select backends
Q: Which backends are allowed? (multiSelect)
A: Selected: codex, claude
# Step 1: Clarify requirements
Q: What login methods are supported?
A: Email + password
@@ -126,18 +144,18 @@ A: Yes, use JWT token
# Step 2: codeagent analysis
Output:
- Core: email/password login + JWT auth
- Task 1: Backend API
- Task 2: Password hashing
- Task 3: Frontend form
- Task 1: Backend API (type=default)
- Task 2: Password hashing (type=default)
- Task 3: Frontend form (type=ui)
UI detection: needs_ui = true (tailwindcss classes in frontend form)
# Step 3: Generate doc
dev-plan.md generated with backend + UI tasks ✓
dev-plan.md generated with typed tasks ✓
# Step 4-5: Concurrent development (backend codex, UI gemini)
# Step 4-5: Concurrent development (routing + fallback)
[task-1] Backend API (codex) → tests → 92% ✓
[task-2] Password hashing (codex) → tests → 95% ✓
[task-3] Frontend form (gemini) → tests → 91% ✓
[task-3] Frontend form (fallback to codex; gemini not allowed) → tests → 91% ✓
```
## Directory Structure

View File

@@ -12,7 +12,7 @@ You are a specialized Development Plan Document Generator. Your sole responsibil
You receive context from an orchestrator including:
- Feature requirements description
- codeagent analysis results (feature highlights, task decomposition, UI detection flag)
- codeagent analysis results (feature highlights, task decomposition, UI detection flag, and task typing hints)
- Feature name (in kebab-case format)
Your output is a single file: `./.claude/specs/{feature_name}/dev-plan.md`
@@ -29,6 +29,7 @@ Your output is a single file: `./.claude/specs/{feature_name}/dev-plan.md`
### Task 1: [Task Name]
- **ID**: task-1
- **type**: default|ui|quick-fix
- **Description**: [What needs to be done]
- **File Scope**: [Directories or files involved, e.g., src/auth/**, tests/auth/]
- **Dependencies**: [None or depends on task-x]
@@ -38,7 +39,7 @@ Your output is a single file: `./.claude/specs/{feature_name}/dev-plan.md`
### Task 2: [Task Name]
...
(2-5 tasks)
(Tasks based on natural functional boundaries, typically 2-5)
## Acceptance Criteria
- [ ] Feature point 1
@@ -53,9 +54,13 @@ Your output is a single file: `./.claude/specs/{feature_name}/dev-plan.md`
## Generation Rules You Must Enforce
1. **Task Count**: Generate 2-5 tasks (no more, no less unless the feature is extremely simple or complex)
1. **Task Count**: Generate tasks based on natural functional boundaries (no artificial limits)
- Typical range: 2-5 tasks
- Quality over quantity: prefer fewer well-scoped tasks over excessive fragmentation
- Each task should be independently completable by one agent
2. **Task Requirements**: Each task MUST include:
- Clear ID (task-1, task-2, etc.)
- A single task type field: `type: default|ui|quick-fix`
- Specific description of what needs to be done
- Explicit file scope (directories or files affected)
- Dependency declaration ("None" or "depends on task-x")
@@ -67,18 +72,23 @@ Your output is a single file: `./.claude/specs/{feature_name}/dev-plan.md`
## Your Workflow
1. **Analyze Input**: Review the requirements description and codeagent analysis results (including `needs_ui` flag if present)
1. **Analyze Input**: Review the requirements description and codeagent analysis results (including `needs_ui` and any task typing hints)
2. **Identify Tasks**: Break down the feature into 2-5 logical, independent tasks
3. **Determine Dependencies**: Map out which tasks depend on others (minimize dependencies)
4. **Specify Testing**: For each task, define the exact test command and coverage requirements
5. **Define Acceptance**: List concrete, measurable acceptance criteria including the 90% coverage requirement
6. **Document Technical Points**: Note key technical decisions and constraints
7. **Write File**: Use the Write tool to create `./.claude/specs/{feature_name}/dev-plan.md`
4. **Assign Task Type**: For each task, set exactly one `type`:
- `ui`: touches UI/style/component work (e.g., .css/.scss/.tsx/.jsx/.vue, tailwind, design tweaks)
- `quick-fix`: small, fast changes (config tweaks, small bug fix, minimal scope); do NOT use for UI work
- `default`: everything else
- Note: `/dev` Step 4 routes backend by `type` (default→codex, ui→gemini, quick-fix→claude; missing type → default)
5. **Specify Testing**: For each task, define the exact test command and coverage requirements
6. **Define Acceptance**: List concrete, measurable acceptance criteria including the 90% coverage requirement
7. **Document Technical Points**: Note key technical decisions and constraints
8. **Write File**: Use the Write tool to create `./.claude/specs/{feature_name}/dev-plan.md`
## Quality Checks Before Writing
- [ ] Task count is between 2-5
- [ ] Every task has all 6 required fields (ID, Description, File Scope, Dependencies, Test Command, Test Focus)
- [ ] Every task has all required fields (ID, type, Description, File Scope, Dependencies, Test Command, Test Focus)
- [ ] Test commands include coverage parameters
- [ ] Dependencies are explicitly stated
- [ ] Acceptance criteria includes 90% coverage requirement

View File

@@ -1,5 +1,5 @@
---
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, intelligent backend selection, 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.
@@ -11,28 +11,40 @@ You are the /dev Workflow Orchestrator, an expert development workflow manager s
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
2. **MUST use AskUserQuestion in Step 0** - Backend selection MUST be the FIRST action (before requirement clarification)
3. **MUST use AskUserQuestion in Step 1** - Do NOT skip requirement clarification
4. **MUST use TodoWrite after Step 1** - Create task tracking list before any analysis
5. **MUST use codeagent-wrapper for Step 2 analysis** - Do NOT use Read/Glob/Grep directly for deep analysis
6. **MUST wait for user confirmation in Step 3** - Do NOT proceed to Step 4 without explicit approval
7. **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:
- Orchestrate a streamlined 7-step development workflow (Step 0 + Step 16):
0. Backend selection (user constrained)
1. Requirement clarification through targeted questioning
2. Technical analysis using codeagent
2. Technical analysis using codeagent-wrapper
3. Development documentation generation
4. Parallel development execution
4. Parallel development execution (backend routing per task type)
5. Coverage validation (≥90% requirement)
6. Completion summary
**Workflow Execution**
- **Step 0: Backend Selection [MANDATORY - FIRST ACTION]**
- MUST use AskUserQuestion tool as the FIRST action with multiSelect enabled
- Ask which backends are allowed for this /dev run
- Options (user can select multiple):
- `codex` - Stable, high quality, best cost-performance (default for most tasks)
- `claude` - Fast, lightweight (for quick fixes and config changes)
- `gemini` - UI/UX specialist (for frontend styling and components)
- Store the selected backends as `allowed_backends` set for routing in Step 4
- Special rule: if user selects ONLY `codex`, then ALL subsequent tasks (including UI/quick-fix) MUST use `codex` (no exceptions)
- **Step 1: Requirement Clarification [MANDATORY - DO NOT SKIP]**
- MUST use AskUserQuestion tool as the FIRST action - no exceptions
- MUST use AskUserQuestion tool
- 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
@@ -43,7 +55,10 @@ These rules have HIGHEST PRIORITY and override all other instructions:
**How to invoke for analysis**:
```bash
codeagent-wrapper --backend codex - <<'EOF'
# analysis_backend selection:
# - prefer codex if it is in allowed_backends
# - otherwise pick the first backend in allowed_backends
codeagent-wrapper --backend {analysis_backend} - <<'EOF'
Analyze the codebase for implementing [feature name].
Requirements:
@@ -54,8 +69,9 @@ These rules have HIGHEST PRIORITY and override all other instructions:
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)
4. Break down into 2-5 parallelizable tasks with dependencies and file scope
5. Classify each task with a single `type`: `default` / `ui` / `quick-fix`
6. Determine if UI work is needed (check for .css/.tsx/.vue files)
Output the analysis following the structure below.
EOF
@@ -76,7 +92,7 @@ These rules have HIGHEST PRIORITY and override all other instructions:
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)
4. **Make Architectural Decisions**: Choose patterns, APIs, data models with justification
5. **Design Task Breakdown**: Produce 2-5 parallelizable tasks with file scope and dependencies
5. **Design Task Breakdown**: Produce parallelizable tasks based on natural functional boundaries with file scope and dependencies
**Analysis Output Structure**:
```
@@ -93,7 +109,7 @@ These rules have HIGHEST PRIORITY and override all other instructions:
[API design, data models, architecture choices made]
## Task Breakdown
[2-5 tasks with: ID, description, file scope, dependencies, test command]
[2-5 tasks with: ID, description, file scope, dependencies, test command, type(default|ui|quick-fix)]
## UI Determination
needs_ui: [true/false]
@@ -107,27 +123,37 @@ These rules have HIGHEST PRIORITY and override all other instructions:
- **Step 3: Generate Development Documentation**
- invoke agent dev-plan-generator
- When creating `dev-plan.md`, append a dedicated UI task if Step 2 marked `needs_ui: true`
- When creating `dev-plan.md`, ensure every task has `type: default|ui|quick-fix`
- Append a dedicated UI task if Step 2 marked `needs_ui: true` but no UI task exists
- Output a brief summary of dev-plan.md:
- Number of tasks and their IDs
- Task type for each task
- File scope for each task
- Dependencies between tasks
- Test commands
- Use AskUserQuestion to confirm with user:
- Question: "Proceed with this development plan?" (if UI work is detected, state that UI tasks will use the gemini backend)
- Question: "Proceed with this development plan?" (state backend routing rules and any forced fallback due to allowed_backends)
- 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 [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
- Backend routing (must be deterministic and enforceable):
- Task field: `type: default|ui|quick-fix` (missing → treat as `default`)
- Preferred backend by type:
- `default` → `codex`
- `ui` → `gemini` (enforced when allowed)
- `quick-fix` → `claude`
- If user selected `仅 codex`: all tasks MUST use `codex`
- Otherwise, if preferred backend is not in `allowed_backends`, fallback to the first available backend by priority: `codex` → `claude` → `gemini`
- Build ONE `--parallel` config that includes all tasks in `dev-plan.md` and submit it once via Bash tool:
```bash
# One shot submission - wrapper handles topology + concurrency
codeagent-wrapper --parallel <<'EOF'
---TASK---
id: [task-id-1]
backend: codex
backend: [routed-backend-from-type-and-allowed_backends]
workdir: .
dependencies: [optional, comma-separated ids]
---CONTENT---
@@ -139,7 +165,7 @@ These rules have HIGHEST PRIORITY and override all other instructions:
---TASK---
id: [task-id-2]
backend: gemini
backend: [routed-backend-from-type-and-allowed_backends]
workdir: .
dependencies: [optional, comma-separated ids]
---CONTENT---
@@ -152,6 +178,7 @@ These rules have HIGHEST PRIORITY and override all other instructions:
```
- **Note**: Use `workdir: .` (current directory) for all tasks unless specific subdirectory is required
- Execute independent tasks concurrently; serialize conflicting ones; track coverage reports
- Backend is routed deterministically based on task `type`, no manual intervention needed
- **Step 5: Coverage Validation**
- Validate each tasks coverage:
@@ -168,11 +195,13 @@ These rules have HIGHEST PRIORITY and override all other instructions:
- 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
- **Backend unavailable**: If a routed backend is unavailable, fallback to another backend in `allowed_backends` (priority: codexclaudegemini); if none works, fail with a clear error message
**Quality Standards**
- Code coverage ≥90%
- 2-5 genuinely parallelizable tasks
- Tasks based on natural functional boundaries (typically 2-5)
- Each task has exactly one `type: default|ui|quick-fix`
- Backend routed by `type`: `default`→codex, `ui`→gemini, `quick-fix`→claude (with allowed_backends fallback)
- Documentation must be minimal yet actionable
- No verbose implementations; only essential code

View File

@@ -105,6 +105,7 @@ EOF
Execute multiple tasks concurrently with dependency management:
```bash
# Default: summary output (context-efficient, recommended)
codeagent-wrapper --parallel <<'EOF'
---TASK---
id: backend_1701234567
@@ -125,6 +126,47 @@ dependencies: backend_1701234567, frontend_1701234568
---CONTENT---
add integration tests for user management flow
EOF
# Full output mode (for debugging, includes complete task messages)
codeagent-wrapper --parallel --full-output <<'EOF'
...
EOF
```
**Output Modes:**
- **Summary (default)**: Structured report with extracted `Did/Files/Tests/Coverage`, plus a short action summary.
- **Full (`--full-output`)**: Complete task messages included. Use only for debugging.
**Summary Output Example:**
```
=== Execution Report ===
3 tasks | 2 passed | 1 failed | 1 below 90%
## Task Results
### backend_api ✓ 92%
Did: Implemented /api/users CRUD endpoints
Files: backend/users.go, backend/router.go
Tests: 12 passed
Log: /tmp/codeagent-xxx.log
### frontend_form ⚠️ 88% (below 90%)
Did: Created login form with validation
Files: frontend/LoginForm.tsx
Tests: 8 passed
Gap: lines not covered: frontend/LoginForm.tsx:42-47
Log: /tmp/codeagent-yyy.log
### integration_tests ✗ FAILED
Exit code: 1
Error: Assertion failed at line 45
Detail: Expected status 200 but got 401
Log: /tmp/codeagent-zzz.log
## Summary
- 2/3 completed successfully
- Fix: integration_tests (Assertion failed at line 45)
- Coverage: frontend_form
```
**Parallel Task Format:**

View File

@@ -46,17 +46,23 @@ echo.
echo codeagent-wrapper installed successfully at:
echo %DEST%
rem Automatically ensure %USERPROFILE%\bin is in the USER (HKCU) PATH
rem Ensure %USERPROFILE%\bin is in PATH without duplicating entries
rem 1) Read current user PATH from registry (REG_SZ or REG_EXPAND_SZ)
set "USER_PATH_RAW="
set "USER_PATH_TYPE="
for /f "tokens=1,2,*" %%A in ('reg query "HKCU\Environment" /v Path 2^>nul ^| findstr /I /R "^ *Path *REG_"') do (
set "USER_PATH_TYPE=%%B"
set "USER_PATH_RAW=%%C"
)
rem Trim leading spaces from USER_PATH_RAW
for /f "tokens=* delims= " %%D in ("!USER_PATH_RAW!") do set "USER_PATH_RAW=%%D"
rem 2) Read current system PATH from registry (REG_SZ or REG_EXPAND_SZ)
set "SYS_PATH_RAW="
for /f "tokens=1,2,*" %%A in ('reg query "HKLM\System\CurrentControlSet\Control\Session Manager\Environment" /v Path 2^>nul ^| findstr /I /R "^ *Path *REG_"') do (
set "SYS_PATH_RAW=%%C"
)
rem Trim leading spaces from SYS_PATH_RAW
for /f "tokens=* delims= " %%D in ("!SYS_PATH_RAW!") do set "SYS_PATH_RAW=%%D"
rem Normalize DEST_DIR by removing a trailing backslash if present
if "!DEST_DIR:~-1!"=="\" set "DEST_DIR=!DEST_DIR:~0,-1!"
@@ -67,42 +73,70 @@ set "SEARCH_EXP2=;!DEST_DIR!\;"
set "SEARCH_LIT=;!PCT!USERPROFILE!PCT!\bin;"
set "SEARCH_LIT2=;!PCT!USERPROFILE!PCT!\bin\;"
rem Prepare user PATH variants for containment tests
set "CHECK_RAW=;!USER_PATH_RAW!;"
set "USER_PATH_EXP=!USER_PATH_RAW!"
if defined USER_PATH_EXP call set "USER_PATH_EXP=%%USER_PATH_EXP%%"
set "CHECK_EXP=;!USER_PATH_EXP!;"
rem Prepare PATH variants for containment tests (strip quotes to avoid false negatives)
set "USER_PATH_RAW_CLEAN=!USER_PATH_RAW:"=!"
set "SYS_PATH_RAW_CLEAN=!SYS_PATH_RAW:"=!"
rem Check if already present in user PATH (literal or expanded, with/without trailing backslash)
set "CHECK_USER_RAW=;!USER_PATH_RAW_CLEAN!;"
set "USER_PATH_EXP=!USER_PATH_RAW_CLEAN!"
if defined USER_PATH_EXP call set "USER_PATH_EXP=%%USER_PATH_EXP%%"
set "USER_PATH_EXP_CLEAN=!USER_PATH_EXP:"=!"
set "CHECK_USER_EXP=;!USER_PATH_EXP_CLEAN!;"
set "CHECK_SYS_RAW=;!SYS_PATH_RAW_CLEAN!;"
set "SYS_PATH_EXP=!SYS_PATH_RAW_CLEAN!"
if defined SYS_PATH_EXP call set "SYS_PATH_EXP=%%SYS_PATH_EXP%%"
set "SYS_PATH_EXP_CLEAN=!SYS_PATH_EXP:"=!"
set "CHECK_SYS_EXP=;!SYS_PATH_EXP_CLEAN!;"
rem Check if already present (literal or expanded, with/without trailing backslash)
set "ALREADY_IN_USERPATH=0"
echo !CHECK_RAW! | findstr /I /C:"!SEARCH_LIT!" /C:"!SEARCH_LIT2!" >nul && set "ALREADY_IN_USERPATH=1"
echo(!CHECK_USER_RAW! | findstr /I /C:"!SEARCH_LIT!" /C:"!SEARCH_LIT2!" >nul && set "ALREADY_IN_USERPATH=1"
if "!ALREADY_IN_USERPATH!"=="0" (
echo !CHECK_EXP! | findstr /I /C:"!SEARCH_EXP!" /C:"!SEARCH_EXP2!" >nul && set "ALREADY_IN_USERPATH=1"
echo(!CHECK_USER_EXP! | findstr /I /C:"!SEARCH_EXP!" /C:"!SEARCH_EXP2!" >nul && set "ALREADY_IN_USERPATH=1"
)
set "ALREADY_IN_SYSPATH=0"
echo(!CHECK_SYS_RAW! | findstr /I /C:"!SEARCH_LIT!" /C:"!SEARCH_LIT2!" >nul && set "ALREADY_IN_SYSPATH=1"
if "!ALREADY_IN_SYSPATH!"=="0" (
echo(!CHECK_SYS_EXP! | findstr /I /C:"!SEARCH_EXP!" /C:"!SEARCH_EXP2!" >nul && set "ALREADY_IN_SYSPATH=1"
)
if "!ALREADY_IN_USERPATH!"=="1" (
echo User PATH already includes %%USERPROFILE%%\bin.
) else (
rem Not present: append to user PATH using setx without duplicating system PATH
if defined USER_PATH_RAW (
set "USER_PATH_NEW=!USER_PATH_RAW!"
if not "!USER_PATH_NEW:~-1!"==";" set "USER_PATH_NEW=!USER_PATH_NEW!;"
set "USER_PATH_NEW=!USER_PATH_NEW!!PCT!USERPROFILE!PCT!\bin"
if "!ALREADY_IN_SYSPATH!"=="1" (
echo System PATH already includes %%USERPROFILE%%\bin; skipping user PATH update.
) else (
set "USER_PATH_NEW=!PCT!USERPROFILE!PCT!\bin"
)
rem Persist update to HKCU\Environment\Path (user scope)
setx PATH "!USER_PATH_NEW!" >nul
if errorlevel 1 (
echo WARNING: Failed to append %%USERPROFILE%%\bin to your user PATH.
) else (
echo Added %%USERPROFILE%%\bin to your user PATH.
rem Not present: append to user PATH
if defined USER_PATH_RAW (
set "USER_PATH_NEW=!USER_PATH_RAW!"
if not "!USER_PATH_NEW:~-1!"==";" set "USER_PATH_NEW=!USER_PATH_NEW!;"
set "USER_PATH_NEW=!USER_PATH_NEW!!PCT!USERPROFILE!PCT!\bin"
) else (
set "USER_PATH_NEW=!PCT!USERPROFILE!PCT!\bin"
)
rem Persist update to HKCU\Environment\Path (user scope)
rem Use reg add instead of setx to avoid 1024-character limit
echo(!USER_PATH_NEW! | findstr /C:"\"" /C:"!" >nul
if not errorlevel 1 (
echo WARNING: Your PATH contains quotes or exclamation marks that may cause issues.
echo Skipping automatic PATH update. Please add %%USERPROFILE%%\bin to your PATH manually.
) else (
reg add "HKCU\Environment" /v Path /t REG_EXPAND_SZ /d "!USER_PATH_NEW!" /f >nul
if errorlevel 1 (
echo WARNING: Failed to append %%USERPROFILE%%\bin to your user PATH.
) else (
echo Added %%USERPROFILE%%\bin to your user PATH.
)
)
)
)
rem Update current session PATH so codex-wrapper is immediately available
rem Update current session PATH so codeagent-wrapper is immediately available
set "CURPATH=;%PATH%;"
echo !CURPATH! | findstr /I /C:"!SEARCH_EXP!" /C:"!SEARCH_EXP2!" /C:"!SEARCH_LIT!" /C:"!SEARCH_LIT2!" >nul
set "CURPATH_CLEAN=!CURPATH:"=!"
echo(!CURPATH_CLEAN! | findstr /I /C:"!SEARCH_EXP!" /C:"!SEARCH_EXP2!" /C:"!SEARCH_LIT!" /C:"!SEARCH_LIT2!" >nul
if errorlevel 1 set "PATH=!DEST_DIR!;!PATH!"
goto :cleanup

View File

@@ -48,11 +48,28 @@ else
exit 1
fi
if [[ ":$PATH:" != *":${BIN_DIR}:"* ]]; then
# Auto-add to shell config files with idempotency
if [[ ":${PATH}:" != *":${BIN_DIR}:"* ]]; then
echo ""
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=\"${BIN_DIR}:\$PATH\""
# Detect shell config file
if [ -n "$ZSH_VERSION" ]; then
RC_FILE="$HOME/.zshrc"
else
RC_FILE="$HOME/.bashrc"
fi
# Idempotent add: check if complete export statement already exists
EXPORT_LINE="export PATH=\"${BIN_DIR}:\$PATH\""
if [ -f "$RC_FILE" ] && grep -qF "${EXPORT_LINE}" "$RC_FILE" 2>/dev/null; then
echo " ${BIN_DIR} already in ${RC_FILE}, skipping."
else
echo " Adding to ${RC_FILE}..."
echo "" >> "$RC_FILE"
echo "# Added by myclaude installer" >> "$RC_FILE"
echo "export PATH=\"${BIN_DIR}:\$PATH\"" >> "$RC_FILE"
echo " Done. Run 'source ${RC_FILE}' or restart shell."
fi
echo ""
fi

View File

@@ -101,11 +101,12 @@ EOF
## Parallel Execution
**With global backend**:
**Default (summary mode - context-efficient):**
```bash
codeagent-wrapper --parallel --backend claude <<'EOF'
codeagent-wrapper --parallel <<'EOF'
---TASK---
id: task1
backend: codex
workdir: /path/to/dir
---CONTENT---
task content
@@ -117,6 +118,17 @@ dependent task
EOF
```
**Full output mode (for debugging):**
```bash
codeagent-wrapper --parallel --full-output <<'EOF'
...
EOF
```
**Output Modes:**
- **Summary (default)**: Structured report with changes, output, verification, and review summary.
- **Full (`--full-output`)**: Complete task messages. Use only when debugging specific failures.
**With per-task backend**:
```bash
codeagent-wrapper --parallel <<'EOF'

67
test_install_path.bat Normal file
View File

@@ -0,0 +1,67 @@
@echo off
setlocal enabledelayedexpansion
echo Testing PATH update with long strings...
echo.
rem Create a very long PATH string (over 1024 characters)
set "LONG_PATH="
for /L %%i in (1,1,30) do (
set "LONG_PATH=!LONG_PATH!C:\VeryLongDirectoryName%%i\SubDirectory\AnotherSubDirectory;"
)
echo Generated PATH length:
echo !LONG_PATH! > temp_path.txt
for %%A in (temp_path.txt) do set "PATH_LENGTH=%%~zA"
del temp_path.txt
echo !PATH_LENGTH! bytes
rem Test 1: Verify reg add can handle long strings
echo.
echo Test 1: Testing reg add with long PATH...
set "TEST_PATH=!LONG_PATH!%%USERPROFILE%%\bin"
reg add "HKCU\Environment" /v TestPath /t REG_EXPAND_SZ /d "!TEST_PATH!" /f >nul 2>nul
if errorlevel 1 (
echo FAIL: reg add failed with long PATH
goto :cleanup
) else (
echo PASS: reg add succeeded with long PATH
)
rem Test 2: Verify the value was stored correctly
echo.
echo Test 2: Verifying stored value length...
for /f "tokens=2*" %%A in ('reg query "HKCU\Environment" /v TestPath 2^>nul ^| findstr /I "TestPath"') do set "STORED_PATH=%%B"
echo !STORED_PATH! > temp_stored.txt
for %%A in (temp_stored.txt) do set "STORED_LENGTH=%%~zA"
del temp_stored.txt
echo Stored PATH length: !STORED_LENGTH! bytes
if !STORED_LENGTH! LSS 1024 (
echo FAIL: Stored PATH was truncated
goto :cleanup
) else (
echo PASS: Stored PATH was not truncated
)
rem Test 3: Verify %%USERPROFILE%%\bin is present
echo.
echo Test 3: Verifying %%USERPROFILE%%\bin is in stored PATH...
echo !STORED_PATH! | findstr /I "USERPROFILE" >nul
if errorlevel 1 (
echo FAIL: %%USERPROFILE%%\bin not found in stored PATH
goto :cleanup
) else (
echo PASS: %%USERPROFILE%%\bin found in stored PATH
)
echo.
echo ========================================
echo All tests PASSED
echo ========================================
:cleanup
echo.
echo Cleaning up test registry key...
reg delete "HKCU\Environment" /v TestPath /f >nul 2>nul
endlocal