mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-06 02:34:09 +08:00
Compare commits
141 Commits
swe-agent/
...
fix/preven
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a09c103cfb | ||
|
|
b3f8fcfea6 | ||
|
|
806bb04a35 | ||
|
|
b1156038de | ||
|
|
0c93bbe574 | ||
|
|
6f4f4e701b | ||
|
|
ff301507fe | ||
|
|
93b72eba42 | ||
|
|
b01758e7e1 | ||
|
|
c51b38c671 | ||
|
|
b227fee225 | ||
|
|
2b7569335b | ||
|
|
9e667f0895 | ||
|
|
4759eb2c42 | ||
|
|
edbf168b57 | ||
|
|
9bfea81ca6 | ||
|
|
a9bcea45f5 | ||
|
|
8554da6e2f | ||
|
|
b2f941af5f | ||
|
|
6861a9d057 | ||
|
|
18189f095c | ||
|
|
f1c306cb23 | ||
|
|
0dc6df4e71 | ||
|
|
21bb45a7af | ||
|
|
e7464d1286 | ||
|
|
373d75cc36 | ||
|
|
0bbcc6c68e | ||
|
|
3c6f22ca48 | ||
|
|
87016ce331 | ||
|
|
86d18ca19a | ||
|
|
4edd2d2d2d | ||
|
|
ef47ed57e9 | ||
|
|
b2e3f416bc | ||
|
|
7231c6d2c4 | ||
|
|
fa342f98c2 | ||
|
|
90478d2049 | ||
|
|
e1ad08fcc1 | ||
|
|
cf2e4fefa4 | ||
|
|
d7bb28a9ce | ||
|
|
b41b223fc8 | ||
|
|
a86ee9340c | ||
|
|
c6cd20d2fd | ||
|
|
132df6cb28 | ||
|
|
d7c514e869 | ||
|
|
3ef288bfaa | ||
|
|
d86a5b67b6 | ||
|
|
8f3941adae | ||
|
|
18c6c32628 | ||
|
|
1ad2cfe629 | ||
|
|
7bad716fbc | ||
|
|
220be6eb5c | ||
|
|
ead11d6996 | ||
|
|
fec4b7dba3 | ||
|
|
da257b860b | ||
|
|
9452b77307 | ||
|
|
85303126d6 | ||
|
|
f08fa88d71 | ||
|
|
33149d9615 | ||
|
|
95408e7fa7 | ||
|
|
22987b5f74 | ||
|
|
90f9a131fe | ||
|
|
017ad5e4d9 | ||
|
|
15b4176afb | ||
|
|
1533e08425 | ||
|
|
c3dd5b567f | ||
|
|
386937cfb3 | ||
|
|
c89ad3df2d | ||
|
|
2b8efd42a9 | ||
|
|
d4104214ff | ||
|
|
802efb5358 | ||
|
|
767b137c58 | ||
|
|
8eecf103ef | ||
|
|
77822cf062 | ||
|
|
007c27879d | ||
|
|
368831da4c | ||
|
|
eb84dfa574 | ||
|
|
3bc8342929 | ||
|
|
cfc64e8515 | ||
|
|
7a40c9d492 | ||
|
|
d51a2f12f8 | ||
|
|
8a8771076d | ||
|
|
e637b26151 | ||
|
|
595fa8da96 | ||
|
|
9ba6950d21 | ||
|
|
7f790fbe15 | ||
|
|
06f14aa695 | ||
|
|
9fa872a1f0 | ||
|
|
6d263fe8c9 | ||
|
|
e55b13c2c5 | ||
|
|
f95f5f5e88 | ||
|
|
246674c388 | ||
|
|
23c212f8be | ||
|
|
90477abb81 | ||
|
|
11afae2dff | ||
|
|
3df4fec6dd | ||
|
|
aea19f0e1f | ||
|
|
291a4e3d0a | ||
|
|
957b737126 | ||
|
|
3e30f4e207 | ||
|
|
b172343235 | ||
|
|
c8a652ec15 | ||
|
|
12e47affa9 | ||
|
|
612150f72e | ||
|
|
77d9870094 | ||
|
|
c96c07be2a | ||
|
|
cee467fc0e | ||
|
|
71305da77e | ||
|
|
c4021cf58a | ||
|
|
9a18a03061 | ||
|
|
b5183c7711 | ||
|
|
3fab18a6bb | ||
|
|
12af992d8c | ||
|
|
bbd2f50c38 | ||
|
|
3f7652f992 | ||
|
|
2cbe36b532 | ||
|
|
fdb152872d | ||
|
|
916b970665 | ||
|
|
10070a9bef | ||
|
|
b18439f268 | ||
|
|
4230479ff4 | ||
|
|
18c26a252a | ||
|
|
f6fc9a338f | ||
|
|
6223d59042 | ||
|
|
e6b229645a | ||
|
|
9dc3e8f43d | ||
|
|
e9faa0bc2d | ||
|
|
70caa8d7fc | ||
|
|
4f74d5afa1 | ||
|
|
7f61437eea | ||
|
|
ed604f6db7 | ||
|
|
fb66b52b68 | ||
|
|
05e32203ee | ||
|
|
1bf7dd9a83 | ||
|
|
19aa237d47 | ||
|
|
5cd1103b85 | ||
|
|
e2f80508b5 | ||
|
|
86cb8f6611 | ||
|
|
04cffd2d21 | ||
|
|
74b47a6f5a | ||
|
|
32514920da | ||
|
|
cc1d22167a |
@@ -125,9 +125,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "advanced-ai-agents",
|
||||
"source": "./advanced-ai-agents/",
|
||||
"description": "Advanced AI agent for complex problem solving and deep analysis with GPT-5 integration",
|
||||
"name": "codex-cli",
|
||||
"source": "./skills/codex/",
|
||||
"description": "Execute Codex CLI for code analysis, refactoring, and automated code changes with file references (@syntax) and structured output",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
@@ -137,43 +137,72 @@
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"gpt5",
|
||||
"ai",
|
||||
"analysis",
|
||||
"problem-solving",
|
||||
"deep-research"
|
||||
],
|
||||
"category": "advanced",
|
||||
"strict": false,
|
||||
"commands": [],
|
||||
"agents": [
|
||||
"./agents/gpt5.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "requirements-clarity",
|
||||
"source": "./requirements-clarity/",
|
||||
"description": "Transforms vague requirements into actionable PRDs through systematic clarification with 100-point scoring system",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"requirements",
|
||||
"clarification",
|
||||
"prd",
|
||||
"specifications",
|
||||
"quality-gates",
|
||||
"requirements-engineering"
|
||||
"codex",
|
||||
"code-analysis",
|
||||
"refactoring",
|
||||
"automation",
|
||||
"gpt-5",
|
||||
"ai-coding"
|
||||
],
|
||||
"category": "essentials",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./skills/SKILL.md"
|
||||
"./SKILL.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "gemini-cli",
|
||||
"source": "./skills/gemini/",
|
||||
"description": "Execute Gemini CLI for AI-powered code analysis and generation with Google's latest Gemini models",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"gemini",
|
||||
"google-ai",
|
||||
"code-analysis",
|
||||
"code-generation",
|
||||
"ai-reasoning"
|
||||
],
|
||||
"category": "essentials",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./SKILL.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dev-workflow",
|
||||
"source": "./dev-workflow/",
|
||||
"description": "Minimal lightweight development workflow with requirements clarification, parallel codex execution, and mandatory 90% test coverage",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"dev",
|
||||
"workflow",
|
||||
"codex",
|
||||
"testing",
|
||||
"coverage",
|
||||
"concurrent",
|
||||
"lightweight"
|
||||
],
|
||||
"category": "workflows",
|
||||
"strict": false,
|
||||
"commands": [
|
||||
"./commands/dev.md"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/dev-plan-generator.md"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
34
.github/workflows/ci.yml
vendored
Normal file
34
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, rc/*]
|
||||
pull_request:
|
||||
branches: [master, rc/*]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd codeagent-wrapper
|
||||
go test -v -cover -coverprofile=coverage.out ./...
|
||||
|
||||
- name: Check coverage
|
||||
run: |
|
||||
cd codeagent-wrapper
|
||||
go tool cover -func=coverage.out | grep total | awk '{print $3}'
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: codeagent-wrapper/coverage.out
|
||||
continue-on-error: true
|
||||
141
.github/workflows/release.yml
vendored
Normal file
141
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
name: Release codeagent-wrapper
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Run tests
|
||||
working-directory: codeagent-wrapper
|
||||
run: go test -v -coverprofile=cover.out ./...
|
||||
|
||||
- name: Check coverage
|
||||
working-directory: codeagent-wrapper
|
||||
run: |
|
||||
go tool cover -func=cover.out | grep total
|
||||
COVERAGE=$(go tool cover -func=cover.out | grep total | awk '{print $3}' | sed 's/%//')
|
||||
echo "Coverage: ${COVERAGE}%"
|
||||
|
||||
build:
|
||||
name: Build
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: amd64
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Build binary
|
||||
id: build
|
||||
working-directory: codeagent-wrapper
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
CGO_ENABLED: 0
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
OUTPUT_NAME=codeagent-wrapper-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||
OUTPUT_NAME="${OUTPUT_NAME}.exe"
|
||||
fi
|
||||
go build -ldflags="-s -w -X main.version=${VERSION}" -o ${OUTPUT_NAME} .
|
||||
chmod +x ${OUTPUT_NAME}
|
||||
echo "artifact_path=codeagent-wrapper/${OUTPUT_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: codeagent-wrapper-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: ${{ steps.build.outputs.artifact_path }}
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release
|
||||
find artifacts -type f -name "codeagent-wrapper-*" -exec mv {} release/ \;
|
||||
cp install.sh install.bat release/
|
||||
ls -la release/
|
||||
|
||||
- name: Extract release notes from CHANGELOG
|
||||
id: extract_notes
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
|
||||
# Extract version section from CHANGELOG.md
|
||||
awk -v ver="$VERSION" '
|
||||
/^## [0-9]+\.[0-9]+\.[0-9]+ - / {
|
||||
if (found) exit
|
||||
if ($2 == ver) {
|
||||
found = 1
|
||||
next
|
||||
}
|
||||
}
|
||||
found && /^## / { exit }
|
||||
found { print }
|
||||
' CHANGELOG.md > release_notes.md
|
||||
|
||||
# Fallback to auto-generated if extraction failed
|
||||
if [ ! -s release_notes.md ]; then
|
||||
echo "⚠️ No release notes found in CHANGELOG.md for version $VERSION" > 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
|
||||
|
||||
cat release_notes.md
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: release/*
|
||||
body_path: release_notes.md
|
||||
draft: false
|
||||
prerelease: false
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,2 +1,7 @@
|
||||
CLAUDE.md
|
||||
.claude/
|
||||
.claude-trace
|
||||
.venv
|
||||
.pytest_cache
|
||||
__pycache__
|
||||
.coverage
|
||||
coverage.out
|
||||
|
||||
198
CHANGELOG.md
Normal file
198
CHANGELOG.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [5.2.3] - 2025-12-15
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(parser)* 修复 bufio.Scanner token too long 错误 (#64)
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
- 同步测试中的版本号至 5.2.3
|
||||
|
||||
## [5.2.2] - 2025-12-13
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
- Fix tests for ClaudeBackend default --dangerously-skip-permissions
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(v5.2.2)* Bump version and clean up documentation
|
||||
|
||||
## [5.2.0] - 2025-12-13
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(dev-workflow)* 替换 Codex 为 codeagent 并添加 UI 自动检测
|
||||
- *(codeagent-wrapper)* 完整多后端支持与安全优化
|
||||
- *(install)* 添加终端日志输出和 verbose 模式
|
||||
- *(v5.2.0)* Improve release notes and installation scripts
|
||||
- *(v5.2.0)* Complete skills system integration and config cleanup
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(merge)* 修复master合并后的编译和测试问题
|
||||
- *(parallel)* 修复并行执行启动横幅重复打印问题
|
||||
- *(ci)* 移除 .claude 配置文件验证步骤
|
||||
- *(codeagent-wrapper)* 重构信号处理逻辑避免重复 nil 检查
|
||||
- *(codeagent-wrapper)* 修复权限标志逻辑和版本号测试
|
||||
- *(install)* Op_run_command 实时流式输出
|
||||
- *(codeagent-wrapper)* 异常退出时显示最近错误信息
|
||||
- *(codeagent-wrapper)* Remove binary artifacts and improve error messages
|
||||
- *(codeagent-wrapper)* Use -r flag for claude backend resume
|
||||
- *(install)* Clarify module list shows default state not enabled
|
||||
- *(codeagent-wrapper)* Use -r flag for gemini backend resume
|
||||
- *(codeagent-wrapper)* Add worker limit cap and remove legacy alias
|
||||
- *(codeagent-wrapper)* Fix race condition in stdout parsing
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- *(pr-53)* 调整文件命名和技能定义
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- *(changelog)* Remove GitHub workflow related content
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
- *(codeagent-wrapper)* 添加 ExtractRecentErrors 单元测试
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(v5.2.0)* Update CHANGELOG and remove deprecated test files
|
||||
|
||||
## [5.1.4] - 2025-12-09
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(parallel)* 任务启动时立即返回日志文件路径以支持实时调试
|
||||
|
||||
## [5.1.3] - 2025-12-08
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(test)* Resolve CI timing race in TestFakeCmdInfra
|
||||
|
||||
## [5.1.2] - 2025-12-08
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- 修复channel同步竞态条件和死锁问题
|
||||
|
||||
## [5.1.1] - 2025-12-08
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(test)* Resolve data race on forceKillDelay with atomic operations
|
||||
- 增强日志清理的安全性和可靠性
|
||||
|
||||
### 💼 Other
|
||||
|
||||
- Resolve signal handling conflict preserving testability and Windows support
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
- 补充测试覆盖提升至 89.3%
|
||||
|
||||
## [5.1.0] - 2025-12-07
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Implement enterprise workflow with multi-backend support
|
||||
- *(cleanup)* 添加启动时清理日志的功能和--cleanup标志支持
|
||||
|
||||
## [5.0.0] - 2025-12-05
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Implement modular installation system
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(codex-wrapper)* Defer startup log until args parsed
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- Remove deprecated plugin modules
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Rewrite documentation for v5.0 modular architecture
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Clarify unit-test coverage levels in requirement questions
|
||||
|
||||
## [4.8.2] - 2025-12-02
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(codex-wrapper)* Capture and include stderr in error messages
|
||||
- Correct Go version in go.mod from 1.25.3 to 1.21
|
||||
- Make forceKillDelay testable to prevent signal test timeout
|
||||
- Skip signal test in CI environment
|
||||
|
||||
## [4.8.1] - 2025-12-01
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(codex-wrapper)* Improve --parallel parameter validation and docs
|
||||
|
||||
### 🎨 Styling
|
||||
|
||||
- *(codex-skill)* Replace emoji with text labels
|
||||
|
||||
## [4.7.3] - 2025-11-29
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Add async logging to temp file with lifecycle management
|
||||
- Add parallel execution support to codex-wrapper
|
||||
- Add session resume support and improve output format
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(logger)* 保留日志文件以便程序退出后调试并完善日志输出功能
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Improve codex skill parameter best practices
|
||||
|
||||
## [4.7.2] - 2025-11-28
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(main)* Improve buffer size and streamline message extraction
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
- *(ParseJSONStream)* 增加对超大单行文本和非字符串文本的处理测试
|
||||
|
||||
## [4.7] - 2025-11-27
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Update repository URLs to cexll/myclaude
|
||||
|
||||
## [4.4] - 2025-11-22
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- 支持通过环境变量配置 skills 模型
|
||||
|
||||
## [4.1] - 2025-11-04
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- 新增 /enhance-prompt 命令并更新所有 README 文档
|
||||
|
||||
## [3.1] - 2025-09-17
|
||||
|
||||
### 💼 Other
|
||||
|
||||
- Sync READMEs with actual commands/agents; remove nonexistent commands; enhance requirements-pilot with testing decision gate and options.
|
||||
|
||||
<!-- generated by git-cliff -->
|
||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
8
Makefile
8
Makefile
@@ -7,10 +7,12 @@
|
||||
help:
|
||||
@echo "Claude Code Multi-Agent Workflow - Quick Deployment"
|
||||
@echo ""
|
||||
@echo "Recommended installation: python3 install.py --install-dir ~/.claude"
|
||||
@echo ""
|
||||
@echo "Usage: make [target]"
|
||||
@echo ""
|
||||
@echo "Targets:"
|
||||
@echo " install - Install all configurations to Claude Code"
|
||||
@echo " install - LEGACY: install all configurations (prefer install.py)"
|
||||
@echo " deploy-bmad - Deploy BMAD workflow (bmad-pilot)"
|
||||
@echo " deploy-requirements - Deploy Requirements workflow (requirements-pilot)"
|
||||
@echo " deploy-essentials - Deploy Development Essentials workflow"
|
||||
@@ -36,6 +38,8 @@ OUTPUT_STYLES_DIR = output-styles
|
||||
|
||||
# Install all configurations
|
||||
install: deploy-all
|
||||
@echo "⚠️ LEGACY PATH: make install will be removed in future versions."
|
||||
@echo " Prefer: python3 install.py --install-dir ~/.claude"
|
||||
@echo "✅ Installation complete!"
|
||||
|
||||
# Deploy BMAD workflow
|
||||
@@ -140,4 +144,4 @@ all: deploy-all
|
||||
# Version info
|
||||
version:
|
||||
@echo "Claude Code Multi-Agent Workflow System v3.1"
|
||||
@echo "BMAD + Requirements-Driven Development"
|
||||
@echo "BMAD + Requirements-Driven Development"
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
# Claude Code Plugin System
|
||||
|
||||
本项目已支持Claude Code插件系统,可以将命令和代理打包成可安装的插件包。
|
||||
|
||||
## 插件配置
|
||||
|
||||
插件配置文件位于 `.claude-plugin/marketplace.json`,定义了所有可用的插件包。
|
||||
|
||||
## 可用插件
|
||||
|
||||
### 1. Requirements-Driven Development
|
||||
- **描述**: 需求驱动的开发工作流,包含90%质量门控
|
||||
- **命令**: `/requirements-pilot`
|
||||
- **代理**: requirements-generate, requirements-code, requirements-testing, requirements-review
|
||||
|
||||
### 2. BMAD Agile Workflow
|
||||
- **描述**: 完整的BMAD敏捷工作流(产品负责人→架构师→SM→开发→QA)
|
||||
- **命令**: `/bmad-pilot`
|
||||
- **代理**: bmad-po, bmad-architect, bmad-sm, bmad-dev, bmad-qa, bmad-orchestrator
|
||||
|
||||
### 3. Development Essentials
|
||||
- **描述**: 核心开发命令套件
|
||||
- **命令**: `/code`, `/debug`, `/test`, `/optimize`, `/review`, `/bugfix`, `/refactor`, `/docs`, `/ask`, `/think`
|
||||
- **代理**: code, bugfix, bugfix-verify, code-optimize, debug, develop
|
||||
|
||||
### 4. Advanced AI Agents
|
||||
- **描述**: 高级AI代理,集成GPT-5进行深度分析
|
||||
- **代理**: gpt5
|
||||
|
||||
## 使用插件命令
|
||||
|
||||
### 列出所有可用插件
|
||||
```bash
|
||||
/plugin list
|
||||
```
|
||||
|
||||
### 查看插件详情
|
||||
```bash
|
||||
/plugin info <plugin-name>
|
||||
```
|
||||
例如:`/plugin info requirements-driven-development`
|
||||
|
||||
### 安装插件
|
||||
```bash
|
||||
/plugin install <plugin-name>
|
||||
```
|
||||
例如:`/plugin install bmad-agile-workflow`
|
||||
|
||||
### 移除插件
|
||||
```bash
|
||||
/plugin remove <plugin-name>
|
||||
```
|
||||
|
||||
## 创建自定义插件
|
||||
|
||||
要创建自己的插件:
|
||||
|
||||
1. 在 `.claude-plugin/marketplace.json` 中添加新的插件定义
|
||||
2. 指定插件包含的命令和代理文件路径
|
||||
3. 设置适当的元数据(版本、作者、关键词等)
|
||||
|
||||
示例插件结构:
|
||||
```json
|
||||
{
|
||||
"name": "my-custom-plugin",
|
||||
"source": "./",
|
||||
"description": "自定义插件描述",
|
||||
"version": "1.0.0",
|
||||
"commands": [
|
||||
"./commands/my-command.md"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/my-agent.md"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 分享插件
|
||||
|
||||
要分享插件给其他项目:
|
||||
1. 复制整个 `.claude-plugin` 目录到目标项目
|
||||
2. 确保相关的命令和代理文件存在
|
||||
3. 在新项目中使用 `/plugin` 命令管理插件
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 插件系统遵循Claude Code的插件规范
|
||||
- 所有命令和代理文件必须是有效的Markdown格式
|
||||
- 插件配置支持版本管理和依赖关系
|
||||
- 插件可以包含多个命令、代理和输出样式
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Claude Code插件文档](https://docs.claude.com/en/docs/claude-code/plugins)
|
||||
- [示例插件仓库](https://github.com/wshobson/agents)
|
||||
410
README.md
410
README.md
@@ -1,114 +1,342 @@
|
||||
[中文](README_CN.md) [English](README.md)
|
||||
|
||||
# Claude Code Multi-Agent Workflow System
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://smithery.ai/skills?ns=cexll&utm_source=github&utm_medium=badge)
|
||||
|
||||
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
[](https://claude.ai/code)
|
||||
[](https://github.com/cexll/myclaude)
|
||||
[](https://docs.claude.com/en/docs/claude-code/plugins)
|
||||
[](https://github.com/cexll/myclaude)
|
||||
|
||||
> Enterprise-grade agile development automation with AI-powered multi-agent orchestration
|
||||
> AI-powered development automation with multi-backend execution (Codex/Claude/Gemini)
|
||||
|
||||
[中文文档](README_CN.md) | [Documentation](docs/)
|
||||
## Core Concept: Multi-Backend Architecture
|
||||
|
||||
## 🚀 Quick Start
|
||||
This system leverages a **dual-agent architecture** with pluggable AI backends:
|
||||
|
||||
### Installation
|
||||
| Role | Agent | Responsibility |
|
||||
|------|-------|----------------|
|
||||
| **Orchestrator** | Claude Code | Planning, context gathering, verification, user interaction |
|
||||
| **Executor** | codeagent-wrapper | Code editing, test execution (Codex/Claude/Gemini backends) |
|
||||
|
||||
**Plugin System (Recommended)**
|
||||
```bash
|
||||
/plugin github.com/cexll/myclaude
|
||||
```
|
||||
**Why this separation?**
|
||||
- Claude Code excels at understanding context and orchestrating complex workflows
|
||||
- Specialized backends (Codex for code, Claude for reasoning, Gemini for prototyping) excel at focused execution
|
||||
- Backend selection via `--backend codex|claude|gemini` matches the model to the task
|
||||
|
||||
## Quick Start(Please execute in Powershell on Windows)
|
||||
|
||||
**Traditional Installation**
|
||||
```bash
|
||||
git clone https://github.com/cexll/myclaude.git
|
||||
cd myclaude
|
||||
make install
|
||||
python3 install.py --install-dir ~/.claude
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
## Workflows Overview
|
||||
|
||||
### 1. Dev Workflow (Recommended)
|
||||
|
||||
**The primary workflow for most development tasks.**
|
||||
|
||||
```bash
|
||||
# Full agile workflow
|
||||
/bmad-pilot "Build user authentication with OAuth2 and MFA"
|
||||
|
||||
# Lightweight development
|
||||
/requirements-pilot "Implement JWT token refresh"
|
||||
|
||||
# Direct development commands
|
||||
/code "Add API rate limiting"
|
||||
/dev "implement user authentication with JWT"
|
||||
```
|
||||
|
||||
## 📦 Plugin Modules
|
||||
**6-Step Process:**
|
||||
1. **Requirements Clarification** - Interactive Q&A to clarify scope
|
||||
2. **Codex Deep Analysis** - Codebase exploration and architecture decisions
|
||||
3. **Dev Plan Generation** - Structured task breakdown with test requirements
|
||||
4. **Parallel Execution** - Codex executes tasks concurrently
|
||||
5. **Coverage Validation** - Enforce ≥90% test coverage
|
||||
6. **Completion Summary** - Report with file changes and coverage stats
|
||||
|
||||
| Plugin | Description | Key Commands |
|
||||
|--------|-------------|--------------|
|
||||
| **[bmad-agile-workflow](docs/BMAD-WORKFLOW.md)** | Complete BMAD methodology with 6 specialized agents | `/bmad-pilot` |
|
||||
| **[requirements-driven-workflow](docs/REQUIREMENTS-WORKFLOW.md)** | Streamlined requirements-to-code workflow | `/requirements-pilot` |
|
||||
| **[development-essentials](docs/DEVELOPMENT-COMMANDS.md)** | Core development slash commands | `/code` `/debug` `/test` `/optimize` |
|
||||
| **[advanced-ai-agents](docs/ADVANCED-AGENTS.md)** | GPT-5 deep reasoning integration | Agent: `gpt5` |
|
||||
**Key Features:**
|
||||
- Claude Code orchestrates, Codex executes all code changes
|
||||
- Automatic task parallelization for speed
|
||||
- Mandatory 90% test coverage gate
|
||||
- Rollback on failure
|
||||
|
||||
## 💡 Use Cases
|
||||
|
||||
**BMAD Workflow** - Full agile process automation
|
||||
- Product requirements → Architecture design → Sprint planning → Development → Code review → QA testing
|
||||
- Quality gates with 90% thresholds
|
||||
- Automated document generation
|
||||
|
||||
**Requirements Workflow** - Fast prototyping
|
||||
- Requirements generation → Implementation → Review → Testing
|
||||
- Lightweight and practical
|
||||
|
||||
**Development Commands** - Daily coding
|
||||
- Direct implementation, debugging, testing, optimization
|
||||
- No workflow overhead
|
||||
|
||||
## 🎯 Key Features
|
||||
|
||||
- **🤖 Role-Based Agents**: Specialized AI agents for each development phase
|
||||
- **📊 Quality Gates**: Automatic quality scoring with iterative refinement
|
||||
- **✅ Approval Points**: User confirmation at critical workflow stages
|
||||
- **📁 Persistent Artifacts**: All specs saved to `.claude/specs/`
|
||||
- **🔌 Plugin System**: Native Claude Code plugin support
|
||||
- **🔄 Flexible Workflows**: Choose full agile or lightweight development
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- **[BMAD Workflow Guide](docs/BMAD-WORKFLOW.md)** - Complete methodology and agent roles
|
||||
- **[Requirements Workflow](docs/REQUIREMENTS-WORKFLOW.md)** - Lightweight development process
|
||||
- **[Development Commands](docs/DEVELOPMENT-COMMANDS.md)** - Slash command reference
|
||||
- **[Plugin System](docs/PLUGIN-SYSTEM.md)** - Installation and configuration
|
||||
- **[Quick Start Guide](docs/QUICK-START.md)** - Get started in 5 minutes
|
||||
|
||||
## 🛠️ Installation Methods
|
||||
|
||||
**Method 1: Plugin Install** (One command)
|
||||
```bash
|
||||
/plugin install bmad-agile-workflow
|
||||
```
|
||||
|
||||
**Method 2: Make Commands** (Selective installation)
|
||||
```bash
|
||||
make deploy-bmad # BMAD workflow only
|
||||
make deploy-requirements # Requirements workflow only
|
||||
make deploy-all # Everything
|
||||
```
|
||||
|
||||
**Method 3: Manual Setup**
|
||||
- Copy `/commands/*.md` to `~/.config/claude/commands/`
|
||||
- Copy `/agents/*.md` to `~/.config/claude/agents/`
|
||||
|
||||
Run `make help` for all options.
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE)
|
||||
|
||||
## 🙋 Support
|
||||
|
||||
- **Issues**: [GitHub Issues](https://github.com/cexll/myclaude/issues)
|
||||
- **Documentation**: [docs/](docs/)
|
||||
- **Plugin Guide**: [PLUGIN_README.md](PLUGIN_README.md)
|
||||
**Best For:** Feature development, refactoring, bug fixes with tests
|
||||
|
||||
---
|
||||
|
||||
**Transform your development with AI-powered automation** - One command, complete workflow, quality assured.
|
||||
### 2. BMAD Agile Workflow
|
||||
|
||||
**Full enterprise agile methodology with 6 specialized agents.**
|
||||
|
||||
```bash
|
||||
/bmad-pilot "build e-commerce checkout system"
|
||||
```
|
||||
|
||||
**Agents:**
|
||||
| Agent | Role |
|
||||
|-------|------|
|
||||
| Product Owner | Requirements & user stories |
|
||||
| Architect | System design & tech decisions |
|
||||
| Tech Lead | Sprint planning & task breakdown |
|
||||
| Developer | Implementation |
|
||||
| Code Reviewer | Quality assurance |
|
||||
| QA Engineer | Testing & validation |
|
||||
|
||||
**Process:**
|
||||
```
|
||||
Requirements → Architecture → Sprint Plan → Development → Review → QA
|
||||
↓ ↓ ↓ ↓ ↓ ↓
|
||||
PRD.md DESIGN.md SPRINT.md Code REVIEW.md TEST.md
|
||||
```
|
||||
|
||||
**Best For:** Large features, team coordination, enterprise projects
|
||||
|
||||
---
|
||||
|
||||
### 3. Requirements-Driven Workflow
|
||||
|
||||
**Lightweight requirements-to-code pipeline.**
|
||||
|
||||
```bash
|
||||
/requirements-pilot "implement API rate limiting"
|
||||
```
|
||||
|
||||
**Process:**
|
||||
1. Requirements generation with quality scoring
|
||||
2. Implementation planning
|
||||
3. Code generation
|
||||
4. Review and testing
|
||||
|
||||
**Best For:** Quick prototypes, well-defined features
|
||||
|
||||
---
|
||||
|
||||
### 4. Development Essentials
|
||||
|
||||
**Direct commands for daily coding tasks.**
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/code` | Implement a feature |
|
||||
| `/debug` | Debug an issue |
|
||||
| `/test` | Write tests |
|
||||
| `/review` | Code review |
|
||||
| `/optimize` | Performance optimization |
|
||||
| `/refactor` | Code refactoring |
|
||||
| `/docs` | Documentation |
|
||||
|
||||
**Best For:** Quick tasks, no workflow overhead needed
|
||||
|
||||
## Enterprise Workflow Features
|
||||
|
||||
- **Multi-backend execution:** `codeagent-wrapper --backend codex|claude|gemini` (default `codex`) so you can match the model to the task without changing workflows.
|
||||
- **GitHub workflow commands:** `/gh-create-issue "short need"` creates structured issues; `/gh-issue-implement 123` pulls issue #123, drives development, and prepares the PR.
|
||||
- **Skills + hooks activation:** .claude/hooks run automation (tests, reviews), while `.claude/skills/skill-rules.json` auto-suggests the right skills. Keep hooks enabled in `.claude/settings.json` to activate the enterprise workflow helpers.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Modular Installation (Recommended)
|
||||
|
||||
```bash
|
||||
# Install all enabled modules (dev + essentials by default)
|
||||
python3 install.py --install-dir ~/.claude
|
||||
|
||||
# Install specific module
|
||||
python3 install.py --module dev
|
||||
|
||||
# List available modules
|
||||
python3 install.py --list-modules
|
||||
|
||||
# Force overwrite existing files
|
||||
python3 install.py --force
|
||||
```
|
||||
|
||||
### Available Modules
|
||||
|
||||
| Module | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `dev` | ✓ Enabled | Dev workflow + Codex integration |
|
||||
| `essentials` | ✓ Enabled | Core development commands |
|
||||
| `bmad` | Disabled | Full BMAD agile workflow |
|
||||
| `requirements` | Disabled | Requirements-driven workflow |
|
||||
|
||||
### What Gets Installed
|
||||
|
||||
```
|
||||
~/.claude/
|
||||
├── CLAUDE.md # Core instructions and role definition
|
||||
├── commands/ # Slash commands (/dev, /code, etc.)
|
||||
├── agents/ # Agent definitions
|
||||
├── skills/
|
||||
│ └── codex/
|
||||
│ └── SKILL.md # Codex integration skill
|
||||
└── installed_modules.json # Installation status
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Edit `config.json` to customize:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"install_dir": "~/.claude",
|
||||
"modules": {
|
||||
"dev": {
|
||||
"enabled": true,
|
||||
"operations": [
|
||||
{"type": "merge_dir", "source": "dev-workflow"},
|
||||
{"type": "copy_file", "source": "memorys/CLAUDE.md", "target": "CLAUDE.md"},
|
||||
{"type": "copy_file", "source": "skills/codex/SKILL.md", "target": "skills/codex/SKILL.md"},
|
||||
{"type": "run_command", "command": "bash install.sh"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Operation Types:**
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `merge_dir` | Merge subdirs (commands/, agents/) into install dir |
|
||||
| `copy_dir` | Copy entire directory |
|
||||
| `copy_file` | Copy single file to target path |
|
||||
| `run_command` | Execute shell command |
|
||||
|
||||
---
|
||||
|
||||
## Codex Integration
|
||||
|
||||
The `codex` skill enables Claude Code to delegate code execution to Codex CLI.
|
||||
|
||||
### Usage in Workflows
|
||||
|
||||
```bash
|
||||
# Codex is invoked via the skill
|
||||
codeagent-wrapper - <<'EOF'
|
||||
implement @src/auth.ts with JWT validation
|
||||
EOF
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
```bash
|
||||
codeagent-wrapper --parallel <<'EOF'
|
||||
---TASK---
|
||||
id: backend_api
|
||||
workdir: /project/backend
|
||||
---CONTENT---
|
||||
implement REST endpoints for /api/users
|
||||
|
||||
---TASK---
|
||||
id: frontend_ui
|
||||
workdir: /project/frontend
|
||||
dependencies: backend_api
|
||||
---CONTENT---
|
||||
create React components consuming the API
|
||||
EOF
|
||||
```
|
||||
|
||||
### Install Codex Wrapper
|
||||
|
||||
```bash
|
||||
# Automatic (via dev module)
|
||||
python3 install.py --module dev
|
||||
|
||||
# Manual
|
||||
bash install.sh
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
Windows installs place `codeagent-wrapper.exe` in `%USERPROFILE%\bin`.
|
||||
|
||||
```powershell
|
||||
# PowerShell (recommended)
|
||||
powershell -ExecutionPolicy Bypass -File install.ps1
|
||||
|
||||
# Batch (cmd)
|
||||
install.bat
|
||||
```
|
||||
|
||||
**Add to PATH** (if installer doesn't detect it):
|
||||
|
||||
```powershell
|
||||
# PowerShell - persistent for current user
|
||||
[Environment]::SetEnvironmentVariable('PATH', "$HOME\bin;" + [Environment]::GetEnvironmentVariable('PATH','User'), 'User')
|
||||
|
||||
# PowerShell - current session only
|
||||
$Env:PATH = "$HOME\bin;$Env:PATH"
|
||||
```
|
||||
|
||||
```batch
|
||||
REM cmd.exe - persistent for current user
|
||||
setx PATH "%USERPROFILE%\bin;%PATH%"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow Selection Guide
|
||||
|
||||
| Scenario | Recommended Workflow |
|
||||
|----------|---------------------|
|
||||
| New feature with tests | `/dev` |
|
||||
| Quick bug fix | `/debug` or `/code` |
|
||||
| Large multi-sprint feature | `/bmad-pilot` |
|
||||
| Prototype or POC | `/requirements-pilot` |
|
||||
| Code review | `/review` |
|
||||
| Performance issue | `/optimize` |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Codex wrapper not found:**
|
||||
```bash
|
||||
# Check PATH
|
||||
echo $PATH | grep -q "$HOME/bin" || echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc
|
||||
|
||||
# Reinstall
|
||||
bash install.sh
|
||||
```
|
||||
|
||||
**Permission denied:**
|
||||
```bash
|
||||
python3 install.py --install-dir ~/.claude --force
|
||||
```
|
||||
|
||||
**Module not loading:**
|
||||
```bash
|
||||
# Check installation status
|
||||
cat ~/.claude/installed_modules.json
|
||||
|
||||
# Reinstall specific module
|
||||
python3 install.py --module dev --force
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
### Core Guides
|
||||
- **[Codeagent-Wrapper Guide](docs/CODEAGENT-WRAPPER.md)** - Multi-backend execution wrapper
|
||||
- **[Hooks Documentation](docs/HOOKS.md)** - Custom hooks and automation
|
||||
|
||||
### Additional Resources
|
||||
- **[Installation Log](install.log)** - Installation history and troubleshooting
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0 License - see [LICENSE](LICENSE)
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues**: [GitHub Issues](https://github.com/cexll/myclaude/issues)
|
||||
- **Documentation**: [docs/](docs/)
|
||||
|
||||
---
|
||||
|
||||
**Claude Code + Codex = Better Development** - Orchestration meets execution.
|
||||
|
||||
388
README_CN.md
388
README_CN.md
@@ -1,114 +1,320 @@
|
||||
# Claude Code 多智能体工作流系统
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
[](https://claude.ai/code)
|
||||
[](https://github.com/cexll/myclaude)
|
||||
[](https://docs.claude.com/en/docs/claude-code/plugins)
|
||||
[](https://github.com/cexll/myclaude)
|
||||
|
||||
> 企业级敏捷开发自动化与 AI 驱动的多智能体编排
|
||||
> AI 驱动的开发自动化 - 多后端执行架构 (Codex/Claude/Gemini)
|
||||
|
||||
[English](README.md) | [文档](docs/)
|
||||
## 核心概念:多后端架构
|
||||
|
||||
## 🚀 快速开始
|
||||
本系统采用**双智能体架构**与可插拔 AI 后端:
|
||||
|
||||
### 安装
|
||||
| 角色 | 智能体 | 职责 |
|
||||
|------|-------|------|
|
||||
| **编排者** | Claude Code | 规划、上下文收集、验证、用户交互 |
|
||||
| **执行者** | codeagent-wrapper | 代码编辑、测试执行(Codex/Claude/Gemini 后端)|
|
||||
|
||||
**插件系统(推荐)**
|
||||
```bash
|
||||
/plugin github.com/cexll/myclaude
|
||||
```
|
||||
**为什么分离?**
|
||||
- Claude Code 擅长理解上下文和编排复杂工作流
|
||||
- 专业后端(Codex 擅长代码、Claude 擅长推理、Gemini 擅长原型)专注执行
|
||||
- 通过 `--backend codex|claude|gemini` 匹配模型与任务
|
||||
|
||||
## 快速开始(windows上请在Powershell中执行)
|
||||
|
||||
**传统安装**
|
||||
```bash
|
||||
git clone https://github.com/cexll/myclaude.git
|
||||
cd myclaude
|
||||
make install
|
||||
python3 install.py --install-dir ~/.claude
|
||||
```
|
||||
|
||||
### 基本使用
|
||||
## 工作流概览
|
||||
|
||||
### 1. Dev 工作流(推荐)
|
||||
|
||||
**大多数开发任务的首选工作流。**
|
||||
|
||||
```bash
|
||||
# 完整敏捷工作流
|
||||
/bmad-pilot "构建用户认证系统,支持 OAuth2 和多因素认证"
|
||||
|
||||
# 轻量级开发
|
||||
/requirements-pilot "实现 JWT 令牌刷新"
|
||||
|
||||
# 直接开发命令
|
||||
/code "添加 API 限流功能"
|
||||
/dev "实现 JWT 用户认证"
|
||||
```
|
||||
|
||||
## 📦 插件模块
|
||||
**6 步流程:**
|
||||
1. **需求澄清** - 交互式问答明确范围
|
||||
2. **Codex 深度分析** - 代码库探索和架构决策
|
||||
3. **开发计划生成** - 结构化任务分解和测试要求
|
||||
4. **并行执行** - Codex 并发执行任务
|
||||
5. **覆盖率验证** - 强制 ≥90% 测试覆盖率
|
||||
6. **完成总结** - 文件变更和覆盖率报告
|
||||
|
||||
| 插件 | 描述 | 主要命令 |
|
||||
|------|------|---------|
|
||||
| **[bmad-agile-workflow](docs/BMAD-WORKFLOW.md)** | 完整 BMAD 方法论,包含6个专业智能体 | `/bmad-pilot` |
|
||||
| **[requirements-driven-workflow](docs/REQUIREMENTS-WORKFLOW.md)** | 精简的需求到代码工作流 | `/requirements-pilot` |
|
||||
| **[development-essentials](docs/DEVELOPMENT-COMMANDS.md)** | 核心开发斜杠命令 | `/code` `/debug` `/test` `/optimize` |
|
||||
| **[advanced-ai-agents](docs/ADVANCED-AGENTS.md)** | GPT-5 深度推理集成 | 智能体: `gpt5` |
|
||||
**核心特性:**
|
||||
- Claude Code 编排,Codex 执行所有代码变更
|
||||
- 自动任务并行化提升速度
|
||||
- 强制 90% 测试覆盖率门禁
|
||||
- 失败自动回滚
|
||||
|
||||
## 💡 使用场景
|
||||
|
||||
**BMAD 工作流** - 完整敏捷流程自动化
|
||||
- 产品需求 → 架构设计 → 冲刺规划 → 开发实现 → 代码审查 → 质量测试
|
||||
- 90% 阈值质量门控
|
||||
- 自动生成文档
|
||||
|
||||
**Requirements 工作流** - 快速原型开发
|
||||
- 需求生成 → 实现 → 审查 → 测试
|
||||
- 轻量级实用主义
|
||||
|
||||
**开发命令** - 日常编码
|
||||
- 直接实现、调试、测试、优化
|
||||
- 无工作流开销
|
||||
|
||||
## 🎯 核心特性
|
||||
|
||||
- **🤖 角色化智能体**: 每个开发阶段的专业 AI 智能体
|
||||
- **📊 质量门控**: 自动质量评分,迭代优化
|
||||
- **✅ 确认节点**: 关键工作流阶段的用户确认
|
||||
- **📁 持久化产物**: 所有规格保存至 `.claude/specs/`
|
||||
- **🔌 插件系统**: 原生 Claude Code 插件支持
|
||||
- **🔄 灵活工作流**: 选择完整敏捷或轻量开发
|
||||
|
||||
## 📚 文档
|
||||
|
||||
- **[BMAD 工作流指南](docs/BMAD-WORKFLOW.md)** - 完整方法论和智能体角色
|
||||
- **[Requirements 工作流](docs/REQUIREMENTS-WORKFLOW.md)** - 轻量级开发流程
|
||||
- **[开发命令参考](docs/DEVELOPMENT-COMMANDS.md)** - 斜杠命令说明
|
||||
- **[插件系统](docs/PLUGIN-SYSTEM.md)** - 安装与配置
|
||||
- **[快速上手](docs/QUICK-START.md)** - 5分钟入门
|
||||
|
||||
## 🛠️ 安装方式
|
||||
|
||||
**方式1: 插件安装**(一条命令)
|
||||
```bash
|
||||
/plugin install bmad-agile-workflow
|
||||
```
|
||||
|
||||
**方式2: Make 命令**(选择性安装)
|
||||
```bash
|
||||
make deploy-bmad # 仅 BMAD 工作流
|
||||
make deploy-requirements # 仅 Requirements 工作流
|
||||
make deploy-all # 全部安装
|
||||
```
|
||||
|
||||
**方式3: 手动安装**
|
||||
- 复制 `/commands/*.md` 到 `~/.config/claude/commands/`
|
||||
- 复制 `/agents/*.md` 到 `~/.config/claude/agents/`
|
||||
|
||||
运行 `make help` 查看所有选项。
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT 许可证 - 查看 [LICENSE](LICENSE)
|
||||
|
||||
## 🙋 支持
|
||||
|
||||
- **问题反馈**: [GitHub Issues](https://github.com/cexll/myclaude/issues)
|
||||
- **文档**: [docs/](docs/)
|
||||
- **插件指南**: [PLUGIN_README.md](PLUGIN_README.md)
|
||||
**适用场景:** 功能开发、重构、带测试的 bug 修复
|
||||
|
||||
---
|
||||
|
||||
**使用 AI 驱动的自动化转型您的开发流程** - 一条命令,完整工作流,质量保证。
|
||||
### 2. BMAD 敏捷工作流
|
||||
|
||||
**包含 6 个专业智能体的完整企业敏捷方法论。**
|
||||
|
||||
```bash
|
||||
/bmad-pilot "构建电商结账系统"
|
||||
```
|
||||
|
||||
**智能体角色:**
|
||||
| 智能体 | 职责 |
|
||||
|-------|------|
|
||||
| Product Owner | 需求与用户故事 |
|
||||
| Architect | 系统设计与技术决策 |
|
||||
| Tech Lead | Sprint 规划与任务分解 |
|
||||
| Developer | 实现 |
|
||||
| Code Reviewer | 质量保证 |
|
||||
| QA Engineer | 测试与验证 |
|
||||
|
||||
**流程:**
|
||||
```
|
||||
需求 → 架构 → Sprint计划 → 开发 → 审查 → QA
|
||||
↓ ↓ ↓ ↓ ↓ ↓
|
||||
PRD.md DESIGN.md SPRINT.md Code REVIEW.md TEST.md
|
||||
```
|
||||
|
||||
**适用场景:** 大型功能、团队协作、企业项目
|
||||
|
||||
---
|
||||
|
||||
### 3. 需求驱动工作流
|
||||
|
||||
**轻量级需求到代码流水线。**
|
||||
|
||||
```bash
|
||||
/requirements-pilot "实现 API 限流"
|
||||
```
|
||||
|
||||
**流程:**
|
||||
1. 带质量评分的需求生成
|
||||
2. 实现规划
|
||||
3. 代码生成
|
||||
4. 审查和测试
|
||||
|
||||
**适用场景:** 快速原型、明确定义的功能
|
||||
|
||||
---
|
||||
|
||||
### 4. 开发基础命令
|
||||
|
||||
**日常编码任务的直接命令。**
|
||||
|
||||
| 命令 | 用途 |
|
||||
|------|------|
|
||||
| `/code` | 实现功能 |
|
||||
| `/debug` | 调试问题 |
|
||||
| `/test` | 编写测试 |
|
||||
| `/review` | 代码审查 |
|
||||
| `/optimize` | 性能优化 |
|
||||
| `/refactor` | 代码重构 |
|
||||
| `/docs` | 编写文档 |
|
||||
|
||||
**适用场景:** 快速任务,无需工作流开销
|
||||
|
||||
---
|
||||
|
||||
## 安装
|
||||
|
||||
### 模块化安装(推荐)
|
||||
|
||||
```bash
|
||||
# 安装所有启用的模块(默认:dev + essentials)
|
||||
python3 install.py --install-dir ~/.claude
|
||||
|
||||
# 安装特定模块
|
||||
python3 install.py --module dev
|
||||
|
||||
# 列出可用模块
|
||||
python3 install.py --list-modules
|
||||
|
||||
# 强制覆盖现有文件
|
||||
python3 install.py --force
|
||||
```
|
||||
|
||||
### 可用模块
|
||||
|
||||
| 模块 | 默认 | 描述 |
|
||||
|------|------|------|
|
||||
| `dev` | ✓ 启用 | Dev 工作流 + Codex 集成 |
|
||||
| `essentials` | ✓ 启用 | 核心开发命令 |
|
||||
| `bmad` | 禁用 | 完整 BMAD 敏捷工作流 |
|
||||
| `requirements` | 禁用 | 需求驱动工作流 |
|
||||
|
||||
### 安装内容
|
||||
|
||||
```
|
||||
~/.claude/
|
||||
├── CLAUDE.md # 核心指令和角色定义
|
||||
├── commands/ # 斜杠命令 (/dev, /code 等)
|
||||
├── agents/ # 智能体定义
|
||||
├── skills/
|
||||
│ └── codex/
|
||||
│ └── SKILL.md # Codex 集成技能
|
||||
└── installed_modules.json # 安装状态
|
||||
```
|
||||
|
||||
### 配置
|
||||
|
||||
编辑 `config.json` 自定义:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"install_dir": "~/.claude",
|
||||
"modules": {
|
||||
"dev": {
|
||||
"enabled": true,
|
||||
"operations": [
|
||||
{"type": "merge_dir", "source": "dev-workflow"},
|
||||
{"type": "copy_file", "source": "memorys/CLAUDE.md", "target": "CLAUDE.md"},
|
||||
{"type": "copy_file", "source": "skills/codex/SKILL.md", "target": "skills/codex/SKILL.md"},
|
||||
{"type": "run_command", "command": "bash install.sh"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**操作类型:**
|
||||
| 类型 | 描述 |
|
||||
|------|------|
|
||||
| `merge_dir` | 合并子目录 (commands/, agents/) 到安装目录 |
|
||||
| `copy_dir` | 复制整个目录 |
|
||||
| `copy_file` | 复制单个文件到目标路径 |
|
||||
| `run_command` | 执行 shell 命令 |
|
||||
|
||||
---
|
||||
|
||||
## Codex 集成
|
||||
|
||||
`codex` 技能使 Claude Code 能够将代码执行委托给 Codex CLI。
|
||||
|
||||
### 工作流中的使用
|
||||
|
||||
```bash
|
||||
# 通过技能调用 Codex
|
||||
codeagent-wrapper - <<'EOF'
|
||||
在 @src/auth.ts 中实现 JWT 验证
|
||||
EOF
|
||||
```
|
||||
|
||||
### 并行执行
|
||||
|
||||
```bash
|
||||
codeagent-wrapper --parallel <<'EOF'
|
||||
---TASK---
|
||||
id: backend_api
|
||||
workdir: /project/backend
|
||||
---CONTENT---
|
||||
实现 /api/users 的 REST 端点
|
||||
|
||||
---TASK---
|
||||
id: frontend_ui
|
||||
workdir: /project/frontend
|
||||
dependencies: backend_api
|
||||
---CONTENT---
|
||||
创建消费 API 的 React 组件
|
||||
EOF
|
||||
```
|
||||
|
||||
### 安装 Codex Wrapper
|
||||
|
||||
```bash
|
||||
# 自动(通过 dev 模块)
|
||||
python3 install.py --module dev
|
||||
|
||||
# 手动
|
||||
bash install.sh
|
||||
```
|
||||
|
||||
#### Windows 系统
|
||||
|
||||
Windows 系统会将 `codeagent-wrapper.exe` 安装到 `%USERPROFILE%\bin`。
|
||||
|
||||
```powershell
|
||||
# PowerShell(推荐)
|
||||
powershell -ExecutionPolicy Bypass -File install.ps1
|
||||
|
||||
# 批处理(cmd)
|
||||
install.bat
|
||||
```
|
||||
|
||||
**添加到 PATH**(如果安装程序未自动检测):
|
||||
|
||||
```powershell
|
||||
# PowerShell - 永久添加(当前用户)
|
||||
[Environment]::SetEnvironmentVariable('PATH', "$HOME\bin;" + [Environment]::GetEnvironmentVariable('PATH','User'), 'User')
|
||||
|
||||
# PowerShell - 仅当前会话
|
||||
$Env:PATH = "$HOME\bin;$Env:PATH"
|
||||
```
|
||||
|
||||
```batch
|
||||
REM cmd.exe - 永久添加(当前用户)
|
||||
setx PATH "%USERPROFILE%\bin;%PATH%"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工作流选择指南
|
||||
|
||||
| 场景 | 推荐工作流 |
|
||||
|------|----------|
|
||||
| 带测试的新功能 | `/dev` |
|
||||
| 快速 bug 修复 | `/debug` 或 `/code` |
|
||||
| 大型多 Sprint 功能 | `/bmad-pilot` |
|
||||
| 原型或 POC | `/requirements-pilot` |
|
||||
| 代码审查 | `/review` |
|
||||
| 性能问题 | `/optimize` |
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
**Codex wrapper 未找到:**
|
||||
```bash
|
||||
# 检查 PATH
|
||||
echo $PATH | grep -q "$HOME/bin" || echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc
|
||||
|
||||
# 重新安装
|
||||
bash install.sh
|
||||
```
|
||||
|
||||
**权限被拒绝:**
|
||||
```bash
|
||||
python3 install.py --install-dir ~/.claude --force
|
||||
```
|
||||
|
||||
**模块未加载:**
|
||||
```bash
|
||||
# 检查安装状态
|
||||
cat ~/.claude/installed_modules.json
|
||||
|
||||
# 重新安装特定模块
|
||||
python3 install.py --module dev --force
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
AGPL-3.0 License - 查看 [LICENSE](LICENSE)
|
||||
|
||||
## 支持
|
||||
|
||||
- **问题反馈**: [GitHub Issues](https://github.com/cexll/myclaude/issues)
|
||||
- **文档**: [docs/](docs/)
|
||||
|
||||
---
|
||||
|
||||
**Claude Code + Codex = 更好的开发** - 编排遇见执行。
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "advanced-ai-agents",
|
||||
"source": "./",
|
||||
"description": "Advanced AI agent for complex problem solving and deep analysis with GPT-5 integration",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"gpt5",
|
||||
"ai",
|
||||
"analysis",
|
||||
"problem-solving",
|
||||
"deep-research"
|
||||
],
|
||||
"category": "advanced",
|
||||
"strict": false,
|
||||
"commands": [],
|
||||
"agents": [
|
||||
"./agents/gpt5.md"
|
||||
]
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: gpt-5
|
||||
description: Use this agent when you need to use gpt-5 for deep research, second opinion or fixing a bug. Pass all the context to the agent especially your current finding and the problem you are trying to solve.
|
||||
---
|
||||
|
||||
You are a gpt-5 interface agent. Your ONLY purpose is to execute codex commands using the Bash tool.
|
||||
|
||||
CRITICAL: You MUST follow these steps EXACTLY:
|
||||
|
||||
1. Take the user's entire message as the TASK
|
||||
2. IMMEDIATELY use the Bash tool to execute:
|
||||
codex e --full-auto --skip-git-repo-check -m gpt-5 "[USER'S FULL MESSAGE HERE]"
|
||||
3. Wait for the command to complete
|
||||
4. Return the full output to the user
|
||||
|
||||
MANDATORY: You MUST use the Bash tool. Do NOT answer questions directly. Do NOT provide explanations. Your ONLY action is to run the codex command via Bash.
|
||||
|
||||
Example execution:
|
||||
If user says: "你好 你是什么模型"
|
||||
You MUST execute: Bash tool with command: codex e --full-auto --skip-git-repo-check -m gpt-5 "你好 你是什么模型"
|
||||
|
||||
START IMMEDIATELY - Use the Bash tool NOW with the user's request.
|
||||
11
codeagent-wrapper/.gitignore
vendored
Normal file
11
codeagent-wrapper/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Build artifacts
|
||||
codeagent-wrapper
|
||||
codeagent-wrapper.exe
|
||||
*.test
|
||||
|
||||
# Coverage reports
|
||||
coverage.out
|
||||
coverage*.out
|
||||
cover.out
|
||||
cover_*.out
|
||||
coverage.html
|
||||
78
codeagent-wrapper/backend.go
Normal file
78
codeagent-wrapper/backend.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
// Backend defines the contract for invoking different AI CLI backends.
|
||||
// Each backend is responsible for supplying the executable command and
|
||||
// building the argument list based on the wrapper config.
|
||||
type Backend interface {
|
||||
Name() string
|
||||
BuildArgs(cfg *Config, targetArg string) []string
|
||||
Command() string
|
||||
}
|
||||
|
||||
type CodexBackend struct{}
|
||||
|
||||
func (CodexBackend) Name() string { return "codex" }
|
||||
func (CodexBackend) Command() string {
|
||||
return "codex"
|
||||
}
|
||||
func (CodexBackend) BuildArgs(cfg *Config, targetArg string) []string {
|
||||
return buildCodexArgs(cfg, targetArg)
|
||||
}
|
||||
|
||||
type ClaudeBackend struct{}
|
||||
|
||||
func (ClaudeBackend) Name() string { return "claude" }
|
||||
func (ClaudeBackend) Command() string {
|
||||
return "claude"
|
||||
}
|
||||
func (ClaudeBackend) BuildArgs(cfg *Config, targetArg string) []string {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
args := []string{"-p", "--dangerously-skip-permissions"}
|
||||
|
||||
// Only skip permissions when explicitly requested
|
||||
// if cfg.SkipPermissions {
|
||||
// args = append(args, "--dangerously-skip-permissions")
|
||||
// }
|
||||
|
||||
// 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
|
||||
args = append(args, "--setting-sources", "")
|
||||
|
||||
if cfg.Mode == "resume" {
|
||||
if cfg.SessionID != "" {
|
||||
// Claude CLI uses -r <session_id> for resume.
|
||||
args = append(args, "-r", cfg.SessionID)
|
||||
}
|
||||
}
|
||||
// Note: claude CLI doesn't support -C flag; workdir set via cmd.Dir
|
||||
|
||||
args = append(args, "--output-format", "stream-json", "--verbose", targetArg)
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
type GeminiBackend struct{}
|
||||
|
||||
func (GeminiBackend) Name() string { return "gemini" }
|
||||
func (GeminiBackend) Command() string {
|
||||
return "gemini"
|
||||
}
|
||||
func (GeminiBackend) BuildArgs(cfg *Config, targetArg string) []string {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
args := []string{"-o", "stream-json", "-y"}
|
||||
|
||||
if cfg.Mode == "resume" {
|
||||
if cfg.SessionID != "" {
|
||||
args = append(args, "-r", cfg.SessionID)
|
||||
}
|
||||
}
|
||||
// Note: gemini CLI doesn't support -C flag; workdir set via cmd.Dir
|
||||
|
||||
args = append(args, "-p", targetArg)
|
||||
|
||||
return args
|
||||
}
|
||||
122
codeagent-wrapper/backend_test.go
Normal file
122
codeagent-wrapper/backend_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
|
||||
backend := ClaudeBackend{}
|
||||
|
||||
t.Run("new mode uses workdir without skip by default", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "new", WorkDir: "/repo"}
|
||||
got := backend.BuildArgs(cfg, "todo")
|
||||
want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "todo"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("new mode opt-in skip permissions with default workdir", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "new", SkipPermissions: true}
|
||||
got := backend.BuildArgs(cfg, "-")
|
||||
want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "-"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resume mode uses session id and omits workdir", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "resume", SessionID: "sid-123", WorkDir: "/ignored"}
|
||||
got := backend.BuildArgs(cfg, "resume-task")
|
||||
want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "-r", "sid-123", "--output-format", "stream-json", "--verbose", "resume-task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resume mode without session still returns base flags", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "resume", WorkDir: "/ignored"}
|
||||
got := backend.BuildArgs(cfg, "follow-up")
|
||||
want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "follow-up"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil config returns nil", func(t *testing.T) {
|
||||
if backend.BuildArgs(nil, "ignored") != nil {
|
||||
t.Fatalf("nil config should return nil args")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestClaudeBuildArgs_GeminiAndCodexModes(t *testing.T) {
|
||||
t.Run("gemini new mode defaults workdir", func(t *testing.T) {
|
||||
backend := GeminiBackend{}
|
||||
cfg := &Config{Mode: "new", WorkDir: "/workspace"}
|
||||
got := backend.BuildArgs(cfg, "task")
|
||||
want := []string{"-o", "stream-json", "-y", "-p", "task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("gemini resume mode uses session id", func(t *testing.T) {
|
||||
backend := GeminiBackend{}
|
||||
cfg := &Config{Mode: "resume", SessionID: "sid-999"}
|
||||
got := backend.BuildArgs(cfg, "resume")
|
||||
want := []string{"-o", "stream-json", "-y", "-r", "sid-999", "-p", "resume"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("gemini resume mode without session omits identifier", func(t *testing.T) {
|
||||
backend := GeminiBackend{}
|
||||
cfg := &Config{Mode: "resume"}
|
||||
got := backend.BuildArgs(cfg, "resume")
|
||||
want := []string{"-o", "stream-json", "-y", "-p", "resume"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("gemini nil config returns nil", func(t *testing.T) {
|
||||
backend := GeminiBackend{}
|
||||
if backend.BuildArgs(nil, "ignored") != nil {
|
||||
t.Fatalf("nil config should return nil args")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("codex build args passthrough remains intact", func(t *testing.T) {
|
||||
backend := CodexBackend{}
|
||||
cfg := &Config{Mode: "new", WorkDir: "/tmp"}
|
||||
got := backend.BuildArgs(cfg, "task")
|
||||
want := []string{"e", "--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) {
|
||||
tests := []struct {
|
||||
backend Backend
|
||||
name string
|
||||
command string
|
||||
}{
|
||||
{backend: CodexBackend{}, name: "codex", command: "codex"},
|
||||
{backend: ClaudeBackend{}, name: "claude", command: "claude"},
|
||||
{backend: GeminiBackend{}, name: "gemini", command: "gemini"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := tt.backend.Name(); got != tt.name {
|
||||
t.Fatalf("Name() = %s, want %s", got, tt.name)
|
||||
}
|
||||
if got := tt.backend.Command(); got != tt.command {
|
||||
t.Fatalf("Command() = %s, want %s", got, tt.command)
|
||||
}
|
||||
}
|
||||
}
|
||||
39
codeagent-wrapper/bench_test.go
Normal file
39
codeagent-wrapper/bench_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// BenchmarkLoggerWrite 测试日志写入性能
|
||||
func BenchmarkLoggerWrite(b *testing.B) {
|
||||
logger, err := NewLogger()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Info("benchmark log message")
|
||||
}
|
||||
b.StopTimer()
|
||||
logger.Flush()
|
||||
}
|
||||
|
||||
// BenchmarkLoggerConcurrentWrite 测试并发日志写入性能
|
||||
func BenchmarkLoggerConcurrentWrite(b *testing.B) {
|
||||
logger, err := NewLogger()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
logger.Info("concurrent benchmark log message")
|
||||
}
|
||||
})
|
||||
b.StopTimer()
|
||||
logger.Flush()
|
||||
}
|
||||
426
codeagent-wrapper/concurrent_stress_test.go
Normal file
426
codeagent-wrapper/concurrent_stress_test.go
Normal file
@@ -0,0 +1,426 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestConcurrentStressLogger 高并发压力测试
|
||||
func TestConcurrentStressLogger(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping stress test in short mode")
|
||||
}
|
||||
|
||||
logger, err := NewLoggerWithSuffix("stress")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
t.Logf("Log file: %s", logger.Path())
|
||||
|
||||
const (
|
||||
numGoroutines = 100 // 并发协程数
|
||||
logsPerRoutine = 1000 // 每个协程写入日志数
|
||||
totalExpected = numGoroutines * logsPerRoutine
|
||||
)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
start := time.Now()
|
||||
|
||||
// 启动并发写入
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < logsPerRoutine; j++ {
|
||||
logger.Info(fmt.Sprintf("goroutine-%d-msg-%d", id, j))
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
logger.Flush()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// 读取日志文件验证
|
||||
data, err := os.ReadFile(logger.Path())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
actualCount := len(lines)
|
||||
|
||||
t.Logf("Concurrent stress test results:")
|
||||
t.Logf(" Goroutines: %d", numGoroutines)
|
||||
t.Logf(" Logs per goroutine: %d", logsPerRoutine)
|
||||
t.Logf(" Total expected: %d", totalExpected)
|
||||
t.Logf(" Total actual: %d", actualCount)
|
||||
t.Logf(" Duration: %v", elapsed)
|
||||
t.Logf(" Throughput: %.2f logs/sec", float64(totalExpected)/elapsed.Seconds())
|
||||
|
||||
// 验证日志数量
|
||||
if actualCount < totalExpected/10 {
|
||||
t.Errorf("too many logs lost: got %d, want at least %d (10%% of %d)",
|
||||
actualCount, totalExpected/10, totalExpected)
|
||||
}
|
||||
t.Logf("Successfully wrote %d/%d logs (%.1f%%)",
|
||||
actualCount, totalExpected, float64(actualCount)/float64(totalExpected)*100)
|
||||
|
||||
// 验证日志格式
|
||||
formatRE := regexp.MustCompile(`^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\] \[PID:\d+\] INFO: goroutine-`)
|
||||
for i, line := range lines[:min(10, len(lines))] {
|
||||
if !formatRE.MatchString(line) {
|
||||
t.Errorf("line %d has invalid format: %s", i, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConcurrentBurstLogger 突发流量测试
|
||||
func TestConcurrentBurstLogger(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping burst test in short mode")
|
||||
}
|
||||
|
||||
logger, err := NewLoggerWithSuffix("burst")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
t.Logf("Log file: %s", logger.Path())
|
||||
|
||||
const (
|
||||
numBursts = 10
|
||||
goroutinesPerBurst = 50
|
||||
logsPerGoroutine = 100
|
||||
)
|
||||
|
||||
totalLogs := 0
|
||||
start := time.Now()
|
||||
|
||||
// 模拟突发流量
|
||||
for burst := 0; burst < numBursts; burst++ {
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < goroutinesPerBurst; i++ {
|
||||
wg.Add(1)
|
||||
totalLogs += logsPerGoroutine
|
||||
go func(b, g int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < logsPerGoroutine; j++ {
|
||||
logger.Info(fmt.Sprintf("burst-%d-goroutine-%d-msg-%d", b, g, j))
|
||||
}
|
||||
}(burst, i)
|
||||
}
|
||||
wg.Wait()
|
||||
time.Sleep(10 * time.Millisecond) // 突发间隔
|
||||
}
|
||||
|
||||
logger.Flush()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// 验证
|
||||
data, err := os.ReadFile(logger.Path())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
actualCount := len(lines)
|
||||
|
||||
t.Logf("Burst test results:")
|
||||
t.Logf(" Total bursts: %d", numBursts)
|
||||
t.Logf(" Goroutines per burst: %d", goroutinesPerBurst)
|
||||
t.Logf(" Expected logs: %d", totalLogs)
|
||||
t.Logf(" Actual logs: %d", actualCount)
|
||||
t.Logf(" Duration: %v", elapsed)
|
||||
t.Logf(" Throughput: %.2f logs/sec", float64(totalLogs)/elapsed.Seconds())
|
||||
|
||||
if actualCount < totalLogs/10 {
|
||||
t.Errorf("too many logs lost: got %d, want at least %d (10%% of %d)", actualCount, totalLogs/10, totalLogs)
|
||||
}
|
||||
t.Logf("Successfully wrote %d/%d logs (%.1f%%)",
|
||||
actualCount, totalLogs, float64(actualCount)/float64(totalLogs)*100)
|
||||
}
|
||||
|
||||
// TestLoggerChannelCapacity 测试 channel 容量极限
|
||||
func TestLoggerChannelCapacity(t *testing.T) {
|
||||
logger, err := NewLoggerWithSuffix("capacity")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
const rapidLogs = 2000 // 超过 channel 容量 (1000)
|
||||
|
||||
start := time.Now()
|
||||
for i := 0; i < rapidLogs; i++ {
|
||||
logger.Info(fmt.Sprintf("rapid-log-%d", i))
|
||||
}
|
||||
sendDuration := time.Since(start)
|
||||
|
||||
logger.Flush()
|
||||
flushDuration := time.Since(start) - sendDuration
|
||||
|
||||
t.Logf("Channel capacity test:")
|
||||
t.Logf(" Logs sent: %d", rapidLogs)
|
||||
t.Logf(" Send duration: %v", sendDuration)
|
||||
t.Logf(" Flush duration: %v", flushDuration)
|
||||
|
||||
// 验证仍有合理比例的日志写入(非阻塞模式允许部分丢失)
|
||||
data, err := os.ReadFile(logger.Path())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
actualCount := len(lines)
|
||||
|
||||
if actualCount < rapidLogs/10 {
|
||||
t.Errorf("too many logs lost: got %d, want at least %d (10%% of %d)", actualCount, rapidLogs/10, rapidLogs)
|
||||
}
|
||||
t.Logf("Logs persisted: %d/%d (%.1f%%)", actualCount, rapidLogs, float64(actualCount)/float64(rapidLogs)*100)
|
||||
}
|
||||
|
||||
// TestLoggerMemoryUsage 内存使用测试
|
||||
func TestLoggerMemoryUsage(t *testing.T) {
|
||||
logger, err := NewLoggerWithSuffix("memory")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
const numLogs = 20000
|
||||
longMessage := strings.Repeat("x", 500) // 500 字节长消息
|
||||
|
||||
start := time.Now()
|
||||
for i := 0; i < numLogs; i++ {
|
||||
logger.Info(fmt.Sprintf("log-%d-%s", i, longMessage))
|
||||
}
|
||||
logger.Flush()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// 检查文件大小
|
||||
info, err := os.Stat(logger.Path())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedTotalSize := int64(numLogs * 500) // 理论最小总字节数
|
||||
expectedMinSize := expectedTotalSize / 10 // 接受最多 90% 丢失
|
||||
actualSize := info.Size()
|
||||
|
||||
t.Logf("Memory/disk usage test:")
|
||||
t.Logf(" Logs written: %d", numLogs)
|
||||
t.Logf(" Message size: 500 bytes")
|
||||
t.Logf(" File size: %.2f MB", float64(actualSize)/1024/1024)
|
||||
t.Logf(" Duration: %v", elapsed)
|
||||
t.Logf(" Write speed: %.2f MB/s", float64(actualSize)/1024/1024/elapsed.Seconds())
|
||||
t.Logf(" Persistence ratio: %.1f%%", float64(actualSize)/float64(expectedTotalSize)*100)
|
||||
|
||||
if actualSize < expectedMinSize {
|
||||
t.Errorf("file size too small: got %d bytes, expected at least %d", actualSize, expectedMinSize)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggerFlushTimeout 测试 Flush 超时机制
|
||||
func TestLoggerFlushTimeout(t *testing.T) {
|
||||
logger, err := NewLoggerWithSuffix("flush")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
// 写入一些日志
|
||||
for i := 0; i < 100; i++ {
|
||||
logger.Info(fmt.Sprintf("test-log-%d", i))
|
||||
}
|
||||
|
||||
// 测试 Flush 应该在合理时间内完成
|
||||
start := time.Now()
|
||||
logger.Flush()
|
||||
duration := time.Since(start)
|
||||
|
||||
t.Logf("Flush duration: %v", duration)
|
||||
|
||||
if duration > 6*time.Second {
|
||||
t.Errorf("Flush took too long: %v (expected < 6s)", duration)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggerOrderPreservation 测试日志顺序保持
|
||||
func TestLoggerOrderPreservation(t *testing.T) {
|
||||
logger, err := NewLoggerWithSuffix("order")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
const numGoroutines = 10
|
||||
const logsPerRoutine = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < logsPerRoutine; j++ {
|
||||
logger.Info(fmt.Sprintf("G%d-SEQ%04d", id, j))
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
logger.Flush()
|
||||
|
||||
// 读取并验证每个 goroutine 的日志顺序
|
||||
data, err := os.ReadFile(logger.Path())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(data)))
|
||||
sequences := make(map[int][]int) // goroutine ID -> sequence numbers
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
var gid, seq int
|
||||
parts := strings.SplitN(line, " INFO: ", 2)
|
||||
if len(parts) != 2 {
|
||||
t.Errorf("invalid log format: %s", line)
|
||||
continue
|
||||
}
|
||||
if _, err := fmt.Sscanf(parts[1], "G%d-SEQ%d", &gid, &seq); err == nil {
|
||||
sequences[gid] = append(sequences[gid], seq)
|
||||
} else {
|
||||
t.Errorf("failed to parse sequence from line: %s", line)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证每个 goroutine 内部顺序
|
||||
for gid, seqs := range sequences {
|
||||
for i := 0; i < len(seqs)-1; i++ {
|
||||
if seqs[i] >= seqs[i+1] {
|
||||
t.Errorf("Goroutine %d: out of order at index %d: %d >= %d",
|
||||
gid, i, seqs[i], seqs[i+1])
|
||||
}
|
||||
}
|
||||
if len(seqs) != logsPerRoutine {
|
||||
t.Errorf("Goroutine %d: missing logs, got %d, want %d",
|
||||
gid, len(seqs), logsPerRoutine)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Order preservation test: all %d goroutines maintained sequence order", len(sequences))
|
||||
}
|
||||
|
||||
func TestConcurrentWorkerPoolLimit(t *testing.T) {
|
||||
orig := runCodexTaskFn
|
||||
defer func() { runCodexTaskFn = orig }()
|
||||
|
||||
logger, err := NewLoggerWithSuffix("pool-limit")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
setLogger(logger)
|
||||
t.Cleanup(func() {
|
||||
_ = closeLogger()
|
||||
_ = logger.RemoveLogFile()
|
||||
})
|
||||
|
||||
var active int64
|
||||
var maxSeen int64
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
if task.Context == nil {
|
||||
t.Fatalf("context not propagated for task %s", task.ID)
|
||||
}
|
||||
cur := atomic.AddInt64(&active, 1)
|
||||
for {
|
||||
prev := atomic.LoadInt64(&maxSeen)
|
||||
if cur <= prev || atomic.CompareAndSwapInt64(&maxSeen, prev, cur) {
|
||||
break
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-task.Context.Done():
|
||||
atomic.AddInt64(&active, -1)
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 130, Error: "context cancelled"}
|
||||
case <-time.After(30 * time.Millisecond):
|
||||
}
|
||||
atomic.AddInt64(&active, -1)
|
||||
return TaskResult{TaskID: task.ID}
|
||||
}
|
||||
|
||||
layers := [][]TaskSpec{{{ID: "t1"}, {ID: "t2"}, {ID: "t3"}, {ID: "t4"}, {ID: "t5"}}}
|
||||
results := executeConcurrentWithContext(context.Background(), layers, 5, 2)
|
||||
|
||||
if len(results) != 5 {
|
||||
t.Fatalf("unexpected result count: got %d", len(results))
|
||||
}
|
||||
if maxSeen > 2 {
|
||||
t.Fatalf("worker pool exceeded limit: saw %d active workers", maxSeen)
|
||||
}
|
||||
|
||||
logger.Flush()
|
||||
data, err := os.ReadFile(logger.Path())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read log file: %v", err)
|
||||
}
|
||||
content := string(data)
|
||||
if !strings.Contains(content, "worker_limit=2") {
|
||||
t.Fatalf("concurrency planning log missing, content: %s", content)
|
||||
}
|
||||
if !strings.Contains(content, "parallel: start") {
|
||||
t.Fatalf("concurrency start logs missing, content: %s", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentCancellationPropagation(t *testing.T) {
|
||||
orig := runCodexTaskFn
|
||||
defer func() { runCodexTaskFn = orig }()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
if task.Context == nil {
|
||||
t.Fatalf("context not propagated for task %s", task.ID)
|
||||
}
|
||||
select {
|
||||
case <-task.Context.Done():
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 130, Error: "context cancelled"}
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
return TaskResult{TaskID: task.ID}
|
||||
}
|
||||
}
|
||||
|
||||
layers := [][]TaskSpec{{{ID: "a"}, {ID: "b"}, {ID: "c"}}}
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
results := executeConcurrentWithContext(ctx, layers, 1, 2)
|
||||
if len(results) != 3 {
|
||||
t.Fatalf("unexpected result count: got %d", len(results))
|
||||
}
|
||||
|
||||
cancelled := 0
|
||||
for _, res := range results {
|
||||
if res.ExitCode != 0 {
|
||||
cancelled++
|
||||
}
|
||||
}
|
||||
|
||||
if cancelled == 0 {
|
||||
t.Fatalf("expected cancellation to propagate, got results: %+v", results)
|
||||
}
|
||||
}
|
||||
272
codeagent-wrapper/config.go
Normal file
272
codeagent-wrapper/config.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config holds CLI configuration
|
||||
type Config struct {
|
||||
Mode string // "new" or "resume"
|
||||
Task string
|
||||
SessionID string
|
||||
WorkDir string
|
||||
ExplicitStdin bool
|
||||
Timeout int
|
||||
Backend string
|
||||
SkipPermissions bool
|
||||
MaxParallelWorkers int
|
||||
}
|
||||
|
||||
// ParallelConfig defines the JSON schema for parallel execution
|
||||
type ParallelConfig struct {
|
||||
Tasks []TaskSpec `json:"tasks"`
|
||||
GlobalBackend string `json:"backend,omitempty"`
|
||||
}
|
||||
|
||||
// TaskSpec describes an individual task entry in the parallel config
|
||||
type TaskSpec struct {
|
||||
ID string `json:"id"`
|
||||
Task string `json:"task"`
|
||||
WorkDir string `json:"workdir,omitempty"`
|
||||
Dependencies []string `json:"dependencies,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
Mode string `json:"-"`
|
||||
UseStdin bool `json:"-"`
|
||||
Context context.Context `json:"-"`
|
||||
}
|
||||
|
||||
// TaskResult captures the execution outcome of a task
|
||||
type TaskResult struct {
|
||||
TaskID string `json:"task_id"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Message string `json:"message"`
|
||||
SessionID string `json:"session_id"`
|
||||
Error string `json:"error"`
|
||||
LogPath string `json:"log_path"`
|
||||
}
|
||||
|
||||
var backendRegistry = map[string]Backend{
|
||||
"codex": CodexBackend{},
|
||||
"claude": ClaudeBackend{},
|
||||
"gemini": GeminiBackend{},
|
||||
}
|
||||
|
||||
func selectBackend(name string) (Backend, error) {
|
||||
key := strings.ToLower(strings.TrimSpace(name))
|
||||
if key == "" {
|
||||
key = defaultBackendName
|
||||
}
|
||||
if backend, ok := backendRegistry[key]; ok {
|
||||
return backend, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported backend %q", name)
|
||||
}
|
||||
|
||||
func envFlagEnabled(key string) bool {
|
||||
val, ok := os.LookupEnv(key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
val = strings.TrimSpace(strings.ToLower(val))
|
||||
switch val {
|
||||
case "", "0", "false", "no", "off":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func parseBoolFlag(val string, defaultValue bool) bool {
|
||||
val = strings.TrimSpace(strings.ToLower(val))
|
||||
switch val {
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
case "0", "false", "no", "off":
|
||||
return false
|
||||
default:
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
func parseParallelConfig(data []byte) (*ParallelConfig, error) {
|
||||
trimmed := bytes.TrimSpace(data)
|
||||
if len(trimmed) == 0 {
|
||||
return nil, fmt.Errorf("parallel config is empty")
|
||||
}
|
||||
|
||||
tasks := strings.Split(string(trimmed), "---TASK---")
|
||||
var cfg ParallelConfig
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
taskIndex := 0
|
||||
for _, taskBlock := range tasks {
|
||||
taskBlock = strings.TrimSpace(taskBlock)
|
||||
if taskBlock == "" {
|
||||
continue
|
||||
}
|
||||
taskIndex++
|
||||
|
||||
parts := strings.SplitN(taskBlock, "---CONTENT---", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("task block #%d missing ---CONTENT--- separator", taskIndex)
|
||||
}
|
||||
|
||||
meta := strings.TrimSpace(parts[0])
|
||||
content := strings.TrimSpace(parts[1])
|
||||
|
||||
task := TaskSpec{WorkDir: defaultWorkdir}
|
||||
for _, line := range strings.Split(meta, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
kv := strings.SplitN(line, ":", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(kv[0])
|
||||
value := strings.TrimSpace(kv[1])
|
||||
|
||||
switch key {
|
||||
case "id":
|
||||
task.ID = value
|
||||
case "workdir":
|
||||
task.WorkDir = value
|
||||
case "session_id":
|
||||
task.SessionID = value
|
||||
task.Mode = "resume"
|
||||
case "backend":
|
||||
task.Backend = value
|
||||
case "dependencies":
|
||||
for _, dep := range strings.Split(value, ",") {
|
||||
dep = strings.TrimSpace(dep)
|
||||
if dep != "" {
|
||||
task.Dependencies = append(task.Dependencies, dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if task.Mode == "" {
|
||||
task.Mode = "new"
|
||||
}
|
||||
|
||||
if task.ID == "" {
|
||||
return nil, fmt.Errorf("task block #%d missing id field", taskIndex)
|
||||
}
|
||||
if content == "" {
|
||||
return nil, fmt.Errorf("task block #%d (%q) missing content", taskIndex, task.ID)
|
||||
}
|
||||
if _, exists := seen[task.ID]; exists {
|
||||
return nil, fmt.Errorf("task block #%d has duplicate id: %s", taskIndex, task.ID)
|
||||
}
|
||||
|
||||
task.Task = content
|
||||
cfg.Tasks = append(cfg.Tasks, task)
|
||||
seen[task.ID] = struct{}{}
|
||||
}
|
||||
|
||||
if len(cfg.Tasks) == 0 {
|
||||
return nil, fmt.Errorf("no tasks found")
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func parseArgs() (*Config, error) {
|
||||
args := os.Args[1:]
|
||||
if len(args) == 0 {
|
||||
return nil, fmt.Errorf("task required")
|
||||
}
|
||||
|
||||
backendName := defaultBackendName
|
||||
skipPermissions := envFlagEnabled("CODEAGENT_SKIP_PERMISSIONS")
|
||||
filtered := make([]string, 0, len(args))
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--backend":
|
||||
if i+1 >= len(args) {
|
||||
return nil, fmt.Errorf("--backend flag requires a value")
|
||||
}
|
||||
backendName = args[i+1]
|
||||
i++
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--backend="):
|
||||
value := strings.TrimPrefix(arg, "--backend=")
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--backend flag requires a value")
|
||||
}
|
||||
backendName = value
|
||||
continue
|
||||
case arg == "--skip-permissions", arg == "--dangerously-skip-permissions":
|
||||
skipPermissions = true
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--skip-permissions="):
|
||||
skipPermissions = parseBoolFlag(strings.TrimPrefix(arg, "--skip-permissions="), skipPermissions)
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--dangerously-skip-permissions="):
|
||||
skipPermissions = parseBoolFlag(strings.TrimPrefix(arg, "--dangerously-skip-permissions="), skipPermissions)
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, arg)
|
||||
}
|
||||
|
||||
if len(filtered) == 0 {
|
||||
return nil, fmt.Errorf("task required")
|
||||
}
|
||||
args = filtered
|
||||
|
||||
cfg := &Config{WorkDir: defaultWorkdir, Backend: backendName, SkipPermissions: skipPermissions}
|
||||
cfg.MaxParallelWorkers = resolveMaxParallelWorkers()
|
||||
|
||||
if args[0] == "resume" {
|
||||
if len(args) < 3 {
|
||||
return nil, fmt.Errorf("resume mode requires: resume <session_id> <task>")
|
||||
}
|
||||
cfg.Mode = "resume"
|
||||
cfg.SessionID = args[1]
|
||||
cfg.Task = args[2]
|
||||
cfg.ExplicitStdin = (args[2] == "-")
|
||||
if len(args) > 3 {
|
||||
cfg.WorkDir = args[3]
|
||||
}
|
||||
} else {
|
||||
cfg.Mode = "new"
|
||||
cfg.Task = args[0]
|
||||
cfg.ExplicitStdin = (args[0] == "-")
|
||||
if len(args) > 1 {
|
||||
cfg.WorkDir = args[1]
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
const maxParallelWorkersLimit = 100
|
||||
|
||||
func resolveMaxParallelWorkers() int {
|
||||
raw := strings.TrimSpace(os.Getenv("CODEAGENT_MAX_PARALLEL_WORKERS"))
|
||||
if raw == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil || value < 0 {
|
||||
logWarn(fmt.Sprintf("Invalid CODEAGENT_MAX_PARALLEL_WORKERS=%q, falling back to unlimited", raw))
|
||||
return 0
|
||||
}
|
||||
|
||||
if value > maxParallelWorkersLimit {
|
||||
logWarn(fmt.Sprintf("CODEAGENT_MAX_PARALLEL_WORKERS=%d exceeds limit, capping at %d", value, maxParallelWorkersLimit))
|
||||
return maxParallelWorkersLimit
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
884
codeagent-wrapper/executor.go
Normal file
884
codeagent-wrapper/executor.go
Normal file
@@ -0,0 +1,884 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// commandRunner abstracts exec.Cmd for testability
|
||||
type commandRunner interface {
|
||||
Start() error
|
||||
Wait() error
|
||||
StdoutPipe() (io.ReadCloser, error)
|
||||
StdinPipe() (io.WriteCloser, error)
|
||||
SetStderr(io.Writer)
|
||||
SetDir(string)
|
||||
Process() processHandle
|
||||
}
|
||||
|
||||
// processHandle abstracts os.Process for testability
|
||||
type processHandle interface {
|
||||
Pid() int
|
||||
Kill() error
|
||||
Signal(os.Signal) error
|
||||
}
|
||||
|
||||
// realCmd implements commandRunner using exec.Cmd
|
||||
type realCmd struct {
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
func (r *realCmd) Start() error {
|
||||
if r.cmd == nil {
|
||||
return errors.New("command is nil")
|
||||
}
|
||||
return r.cmd.Start()
|
||||
}
|
||||
|
||||
func (r *realCmd) Wait() error {
|
||||
if r.cmd == nil {
|
||||
return errors.New("command is nil")
|
||||
}
|
||||
return r.cmd.Wait()
|
||||
}
|
||||
|
||||
func (r *realCmd) StdoutPipe() (io.ReadCloser, error) {
|
||||
if r.cmd == nil {
|
||||
return nil, errors.New("command is nil")
|
||||
}
|
||||
return r.cmd.StdoutPipe()
|
||||
}
|
||||
|
||||
func (r *realCmd) StdinPipe() (io.WriteCloser, error) {
|
||||
if r.cmd == nil {
|
||||
return nil, errors.New("command is nil")
|
||||
}
|
||||
return r.cmd.StdinPipe()
|
||||
}
|
||||
|
||||
func (r *realCmd) SetStderr(w io.Writer) {
|
||||
if r.cmd != nil {
|
||||
r.cmd.Stderr = w
|
||||
}
|
||||
}
|
||||
|
||||
func (r *realCmd) SetDir(dir string) {
|
||||
if r.cmd != nil {
|
||||
r.cmd.Dir = dir
|
||||
}
|
||||
}
|
||||
|
||||
func (r *realCmd) Process() processHandle {
|
||||
if r == nil || r.cmd == nil || r.cmd.Process == nil {
|
||||
return nil
|
||||
}
|
||||
return &realProcess{proc: r.cmd.Process}
|
||||
}
|
||||
|
||||
// realProcess implements processHandle using os.Process
|
||||
type realProcess struct {
|
||||
proc *os.Process
|
||||
}
|
||||
|
||||
func (p *realProcess) Pid() int {
|
||||
if p == nil || p.proc == nil {
|
||||
return 0
|
||||
}
|
||||
return p.proc.Pid
|
||||
}
|
||||
|
||||
func (p *realProcess) Kill() error {
|
||||
if p == nil || p.proc == nil {
|
||||
return nil
|
||||
}
|
||||
return p.proc.Kill()
|
||||
}
|
||||
|
||||
func (p *realProcess) Signal(sig os.Signal) error {
|
||||
if p == nil || p.proc == nil {
|
||||
return nil
|
||||
}
|
||||
return p.proc.Signal(sig)
|
||||
}
|
||||
|
||||
// newCommandRunner creates a new commandRunner (test hook injection point)
|
||||
var newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return &realCmd{cmd: commandContext(ctx, name, args...)}
|
||||
}
|
||||
|
||||
type parseResult struct {
|
||||
message string
|
||||
threadID string
|
||||
}
|
||||
|
||||
var runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
if task.WorkDir == "" {
|
||||
task.WorkDir = defaultWorkdir
|
||||
}
|
||||
if task.Mode == "" {
|
||||
task.Mode = "new"
|
||||
}
|
||||
if task.UseStdin || shouldUseStdin(task.Task, false) {
|
||||
task.UseStdin = true
|
||||
}
|
||||
|
||||
backendName := task.Backend
|
||||
if backendName == "" {
|
||||
backendName = defaultBackendName
|
||||
}
|
||||
|
||||
backend, err := selectBackendFn(backendName)
|
||||
if err != nil {
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 1, Error: err.Error()}
|
||||
}
|
||||
task.Backend = backend.Name()
|
||||
|
||||
parentCtx := task.Context
|
||||
if parentCtx == nil {
|
||||
parentCtx = context.Background()
|
||||
}
|
||||
return runCodexTaskWithContext(parentCtx, task, backend, nil, false, true, timeout)
|
||||
}
|
||||
|
||||
func topologicalSort(tasks []TaskSpec) ([][]TaskSpec, error) {
|
||||
idToTask := make(map[string]TaskSpec, len(tasks))
|
||||
indegree := make(map[string]int, len(tasks))
|
||||
adj := make(map[string][]string, len(tasks))
|
||||
|
||||
for _, task := range tasks {
|
||||
idToTask[task.ID] = task
|
||||
indegree[task.ID] = 0
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
for _, dep := range task.Dependencies {
|
||||
if _, ok := idToTask[dep]; !ok {
|
||||
return nil, fmt.Errorf("dependency %q not found for task %q", dep, task.ID)
|
||||
}
|
||||
indegree[task.ID]++
|
||||
adj[dep] = append(adj[dep], task.ID)
|
||||
}
|
||||
}
|
||||
|
||||
queue := make([]string, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
if indegree[task.ID] == 0 {
|
||||
queue = append(queue, task.ID)
|
||||
}
|
||||
}
|
||||
|
||||
layers := make([][]TaskSpec, 0)
|
||||
processed := 0
|
||||
|
||||
for len(queue) > 0 {
|
||||
current := queue
|
||||
queue = nil
|
||||
layer := make([]TaskSpec, len(current))
|
||||
for i, id := range current {
|
||||
layer[i] = idToTask[id]
|
||||
processed++
|
||||
}
|
||||
layers = append(layers, layer)
|
||||
|
||||
next := make([]string, 0)
|
||||
for _, id := range current {
|
||||
for _, neighbor := range adj[id] {
|
||||
indegree[neighbor]--
|
||||
if indegree[neighbor] == 0 {
|
||||
next = append(next, neighbor)
|
||||
}
|
||||
}
|
||||
}
|
||||
queue = append(queue, next...)
|
||||
}
|
||||
|
||||
if processed != len(tasks) {
|
||||
cycleIDs := make([]string, 0)
|
||||
for id, deg := range indegree {
|
||||
if deg > 0 {
|
||||
cycleIDs = append(cycleIDs, id)
|
||||
}
|
||||
}
|
||||
sort.Strings(cycleIDs)
|
||||
return nil, fmt.Errorf("cycle detected involving tasks: %s", strings.Join(cycleIDs, ","))
|
||||
}
|
||||
|
||||
return layers, nil
|
||||
}
|
||||
|
||||
func executeConcurrent(layers [][]TaskSpec, timeout int) []TaskResult {
|
||||
maxWorkers := resolveMaxParallelWorkers()
|
||||
return executeConcurrentWithContext(context.Background(), layers, timeout, maxWorkers)
|
||||
}
|
||||
|
||||
func executeConcurrentWithContext(parentCtx context.Context, layers [][]TaskSpec, timeout int, maxWorkers int) []TaskResult {
|
||||
totalTasks := 0
|
||||
for _, layer := range layers {
|
||||
totalTasks += len(layer)
|
||||
}
|
||||
|
||||
results := make([]TaskResult, 0, totalTasks)
|
||||
failed := make(map[string]TaskResult, totalTasks)
|
||||
resultsCh := make(chan TaskResult, totalTasks)
|
||||
|
||||
var startPrintMu sync.Mutex
|
||||
bannerPrinted := false
|
||||
|
||||
printTaskStart := func(taskID string) {
|
||||
logger := activeLogger()
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
path := logger.Path()
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
startPrintMu.Lock()
|
||||
if !bannerPrinted {
|
||||
fmt.Fprintln(os.Stderr, "=== Starting Parallel Execution ===")
|
||||
bannerPrinted = true
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Task %s: Log: %s\n", taskID, path)
|
||||
startPrintMu.Unlock()
|
||||
}
|
||||
|
||||
ctx := parentCtx
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
workerLimit := maxWorkers
|
||||
if workerLimit < 0 {
|
||||
workerLimit = 0
|
||||
}
|
||||
|
||||
var sem chan struct{}
|
||||
if workerLimit > 0 {
|
||||
sem = make(chan struct{}, workerLimit)
|
||||
}
|
||||
|
||||
logConcurrencyPlanning(workerLimit, totalTasks)
|
||||
|
||||
acquireSlot := func() bool {
|
||||
if sem == nil {
|
||||
return true
|
||||
}
|
||||
select {
|
||||
case sem <- struct{}{}:
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
releaseSlot := func() {
|
||||
if sem == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-sem:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
var activeWorkers int64
|
||||
|
||||
for _, layer := range layers {
|
||||
var wg sync.WaitGroup
|
||||
executed := 0
|
||||
|
||||
for _, task := range layer {
|
||||
if skip, reason := shouldSkipTask(task, failed); skip {
|
||||
res := TaskResult{TaskID: task.ID, ExitCode: 1, Error: reason}
|
||||
results = append(results, res)
|
||||
failed[task.ID] = res
|
||||
continue
|
||||
}
|
||||
|
||||
if ctx.Err() != nil {
|
||||
res := cancelledTaskResult(task.ID, ctx)
|
||||
results = append(results, res)
|
||||
failed[task.ID] = res
|
||||
continue
|
||||
}
|
||||
|
||||
executed++
|
||||
wg.Add(1)
|
||||
go func(ts TaskSpec) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
resultsCh <- TaskResult{TaskID: ts.ID, ExitCode: 1, Error: fmt.Sprintf("panic: %v", r)}
|
||||
}
|
||||
}()
|
||||
|
||||
if !acquireSlot() {
|
||||
resultsCh <- cancelledTaskResult(ts.ID, ctx)
|
||||
return
|
||||
}
|
||||
defer releaseSlot()
|
||||
|
||||
current := atomic.AddInt64(&activeWorkers, 1)
|
||||
logConcurrencyState("start", ts.ID, int(current), workerLimit)
|
||||
defer func() {
|
||||
after := atomic.AddInt64(&activeWorkers, -1)
|
||||
logConcurrencyState("done", ts.ID, int(after), workerLimit)
|
||||
}()
|
||||
|
||||
ts.Context = ctx
|
||||
printTaskStart(ts.ID)
|
||||
resultsCh <- runCodexTaskFn(ts, timeout)
|
||||
}(task)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for i := 0; i < executed; i++ {
|
||||
res := <-resultsCh
|
||||
results = append(results, res)
|
||||
if res.ExitCode != 0 || res.Error != "" {
|
||||
failed[res.TaskID] = res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func cancelledTaskResult(taskID string, ctx context.Context) TaskResult {
|
||||
exitCode := 130
|
||||
msg := "execution cancelled"
|
||||
if ctx != nil && errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
exitCode = 124
|
||||
msg = "execution timeout"
|
||||
}
|
||||
return TaskResult{TaskID: taskID, ExitCode: exitCode, Error: msg}
|
||||
}
|
||||
|
||||
func shouldSkipTask(task TaskSpec, failed map[string]TaskResult) (bool, string) {
|
||||
if len(task.Dependencies) == 0 {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
var blocked []string
|
||||
for _, dep := range task.Dependencies {
|
||||
if _, ok := failed[dep]; ok {
|
||||
blocked = append(blocked, dep)
|
||||
}
|
||||
}
|
||||
|
||||
if len(blocked) == 0 {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
return true, fmt.Sprintf("skipped due to failed dependencies: %s", strings.Join(blocked, ","))
|
||||
}
|
||||
|
||||
func generateFinalOutput(results []TaskResult) string {
|
||||
var sb strings.Builder
|
||||
|
||||
success := 0
|
||||
failed := 0
|
||||
for _, res := range results {
|
||||
if res.ExitCode == 0 && res.Error == "" {
|
||||
success++
|
||||
} 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))
|
||||
|
||||
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 != "" {
|
||||
sb.WriteString(fmt.Sprintf("Log: %s\n", res.LogPath))
|
||||
}
|
||||
if res.Message != "" {
|
||||
sb.WriteString(fmt.Sprintf("\n%s\n", res.Message))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func buildCodexArgs(cfg *Config, targetArg string) []string {
|
||||
if cfg.Mode == "resume" {
|
||||
return []string{
|
||||
"e",
|
||||
"--skip-git-repo-check",
|
||||
"--json",
|
||||
"resume",
|
||||
cfg.SessionID,
|
||||
targetArg,
|
||||
}
|
||||
}
|
||||
return []string{
|
||||
"e",
|
||||
"--skip-git-repo-check",
|
||||
"-C", cfg.WorkDir,
|
||||
"--json",
|
||||
targetArg,
|
||||
}
|
||||
}
|
||||
|
||||
func runCodexTask(taskSpec TaskSpec, silent bool, timeoutSec int) TaskResult {
|
||||
return runCodexTaskWithContext(context.Background(), taskSpec, nil, nil, false, silent, timeoutSec)
|
||||
}
|
||||
|
||||
func runCodexProcess(parentCtx context.Context, codexArgs []string, taskText string, useStdin bool, timeoutSec int) (message, threadID string, exitCode int) {
|
||||
res := runCodexTaskWithContext(parentCtx, TaskSpec{Task: taskText, WorkDir: defaultWorkdir, Mode: "new", UseStdin: useStdin}, nil, codexArgs, true, false, timeoutSec)
|
||||
return res.Message, res.SessionID, res.ExitCode
|
||||
}
|
||||
|
||||
func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backend Backend, customArgs []string, useCustomArgs bool, silent bool, timeoutSec int) TaskResult {
|
||||
result := TaskResult{TaskID: taskSpec.ID}
|
||||
setLogPath := func() {
|
||||
if result.LogPath != "" {
|
||||
return
|
||||
}
|
||||
if logger := activeLogger(); logger != nil {
|
||||
result.LogPath = logger.Path()
|
||||
}
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
Mode: taskSpec.Mode,
|
||||
Task: taskSpec.Task,
|
||||
SessionID: taskSpec.SessionID,
|
||||
WorkDir: taskSpec.WorkDir,
|
||||
Backend: defaultBackendName,
|
||||
}
|
||||
|
||||
commandName := codexCommand
|
||||
argsBuilder := buildCodexArgsFn
|
||||
if backend != nil {
|
||||
commandName = backend.Command()
|
||||
argsBuilder = backend.BuildArgs
|
||||
cfg.Backend = backend.Name()
|
||||
} else if taskSpec.Backend != "" {
|
||||
cfg.Backend = taskSpec.Backend
|
||||
} else if commandName != "" {
|
||||
cfg.Backend = commandName
|
||||
}
|
||||
|
||||
if cfg.Mode == "" {
|
||||
cfg.Mode = "new"
|
||||
}
|
||||
if cfg.WorkDir == "" {
|
||||
cfg.WorkDir = defaultWorkdir
|
||||
}
|
||||
|
||||
useStdin := taskSpec.UseStdin
|
||||
targetArg := taskSpec.Task
|
||||
if useStdin {
|
||||
targetArg = "-"
|
||||
}
|
||||
|
||||
var codexArgs []string
|
||||
if useCustomArgs {
|
||||
codexArgs = customArgs
|
||||
} else {
|
||||
codexArgs = argsBuilder(cfg, targetArg)
|
||||
}
|
||||
|
||||
prefixMsg := func(msg string) string {
|
||||
if taskSpec.ID == "" {
|
||||
return msg
|
||||
}
|
||||
return fmt.Sprintf("[Task: %s] %s", taskSpec.ID, msg)
|
||||
}
|
||||
|
||||
var logInfoFn func(string)
|
||||
var logWarnFn func(string)
|
||||
var logErrorFn func(string)
|
||||
|
||||
if silent {
|
||||
// Silent mode: only persist to file when available; avoid stderr noise.
|
||||
logInfoFn = func(msg string) {
|
||||
if logger := activeLogger(); logger != nil {
|
||||
logger.Info(prefixMsg(msg))
|
||||
}
|
||||
}
|
||||
logWarnFn = func(msg string) {
|
||||
if logger := activeLogger(); logger != nil {
|
||||
logger.Warn(prefixMsg(msg))
|
||||
}
|
||||
}
|
||||
logErrorFn = func(msg string) {
|
||||
if logger := activeLogger(); logger != nil {
|
||||
logger.Error(prefixMsg(msg))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logInfoFn = func(msg string) { logInfo(prefixMsg(msg)) }
|
||||
logWarnFn = func(msg string) { logWarn(prefixMsg(msg)) }
|
||||
logErrorFn = func(msg string) { logError(prefixMsg(msg)) }
|
||||
}
|
||||
|
||||
stderrBuf := &tailBuffer{limit: stderrCaptureLimit}
|
||||
|
||||
var stdoutLogger *logWriter
|
||||
var stderrLogger *logWriter
|
||||
|
||||
var tempLogger *Logger
|
||||
if silent && activeLogger() == nil {
|
||||
if l, err := NewLogger(); err == nil {
|
||||
setLogger(l)
|
||||
tempLogger = l
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
if tempLogger != nil {
|
||||
_ = closeLogger()
|
||||
}
|
||||
}()
|
||||
defer setLogPath()
|
||||
if logger := activeLogger(); logger != nil {
|
||||
result.LogPath = logger.Path()
|
||||
}
|
||||
|
||||
if !silent {
|
||||
stdoutLogger = newLogWriter("CODEX_STDOUT: ", codexLogLineLimit)
|
||||
stderrLogger = newLogWriter("CODEX_STDERR: ", codexLogLineLimit)
|
||||
}
|
||||
|
||||
ctx := parentCtx
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSec)*time.Second)
|
||||
defer cancel()
|
||||
ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
attachStderr := func(msg string) string {
|
||||
return fmt.Sprintf("%s; stderr: %s", msg, stderrBuf.String())
|
||||
}
|
||||
|
||||
cmd := newCommandRunner(ctx, commandName, codexArgs...)
|
||||
|
||||
// For backends that don't support -C flag (claude, gemini), set working directory via cmd.Dir
|
||||
// Codex passes workdir via -C flag, so we skip setting Dir for it to avoid conflicts
|
||||
if cfg.Mode != "resume" && commandName != "codex" && cfg.WorkDir != "" {
|
||||
cmd.SetDir(cfg.WorkDir)
|
||||
}
|
||||
|
||||
stderrWriters := []io.Writer{stderrBuf}
|
||||
if stderrLogger != nil {
|
||||
stderrWriters = append(stderrWriters, stderrLogger)
|
||||
}
|
||||
if !silent {
|
||||
stderrWriters = append([]io.Writer{os.Stderr}, stderrWriters...)
|
||||
}
|
||||
if len(stderrWriters) == 1 {
|
||||
cmd.SetStderr(stderrWriters[0])
|
||||
} else {
|
||||
cmd.SetStderr(io.MultiWriter(stderrWriters...))
|
||||
}
|
||||
|
||||
var stdinPipe io.WriteCloser
|
||||
var err error
|
||||
if useStdin {
|
||||
stdinPipe, err = cmd.StdinPipe()
|
||||
if err != nil {
|
||||
logErrorFn("Failed to create stdin pipe: " + err.Error())
|
||||
result.ExitCode = 1
|
||||
result.Error = attachStderr("failed to create stdin pipe: " + err.Error())
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
logErrorFn("Failed to create stdout pipe: " + err.Error())
|
||||
result.ExitCode = 1
|
||||
result.Error = attachStderr("failed to create stdout pipe: " + err.Error())
|
||||
return result
|
||||
}
|
||||
|
||||
stdoutReader := io.Reader(stdout)
|
||||
if stdoutLogger != nil {
|
||||
stdoutReader = io.TeeReader(stdout, stdoutLogger)
|
||||
}
|
||||
|
||||
// Start parse goroutine BEFORE starting the command to avoid race condition
|
||||
// where fast-completing commands close stdout before parser starts reading
|
||||
messageSeen := make(chan struct{}, 1)
|
||||
parseCh := make(chan parseResult, 1)
|
||||
go func() {
|
||||
msg, tid := parseJSONStreamInternal(stdoutReader, logWarnFn, logInfoFn, func() {
|
||||
select {
|
||||
case messageSeen <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
parseCh <- parseResult{message: msg, threadID: tid}
|
||||
}()
|
||||
|
||||
logInfoFn(fmt.Sprintf("Starting %s with args: %s %s...", commandName, commandName, strings.Join(codexArgs[:min(5, len(codexArgs))], " ")))
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
if strings.Contains(err.Error(), "executable file not found") {
|
||||
msg := fmt.Sprintf("%s command not found in PATH", commandName)
|
||||
logErrorFn(msg)
|
||||
result.ExitCode = 127
|
||||
result.Error = attachStderr(msg)
|
||||
return result
|
||||
}
|
||||
logErrorFn("Failed to start " + commandName + ": " + err.Error())
|
||||
result.ExitCode = 1
|
||||
result.Error = attachStderr("failed to start " + commandName + ": " + err.Error())
|
||||
return result
|
||||
}
|
||||
|
||||
logInfoFn(fmt.Sprintf("Starting %s with PID: %d", commandName, cmd.Process().Pid()))
|
||||
if logger := activeLogger(); logger != nil {
|
||||
logInfoFn(fmt.Sprintf("Log capturing to: %s", logger.Path()))
|
||||
}
|
||||
|
||||
if useStdin && stdinPipe != nil {
|
||||
logInfoFn(fmt.Sprintf("Writing %d chars to stdin...", len(taskSpec.Task)))
|
||||
go func(data string) {
|
||||
defer stdinPipe.Close()
|
||||
_, _ = io.WriteString(stdinPipe, data)
|
||||
}(taskSpec.Task)
|
||||
logInfoFn("Stdin closed")
|
||||
}
|
||||
|
||||
waitCh := make(chan error, 1)
|
||||
go func() { waitCh <- cmd.Wait() }()
|
||||
|
||||
var waitErr error
|
||||
var forceKillTimer *forceKillTimer
|
||||
var ctxCancelled bool
|
||||
|
||||
select {
|
||||
case waitErr = <-waitCh:
|
||||
case <-ctx.Done():
|
||||
ctxCancelled = true
|
||||
logErrorFn(cancelReason(commandName, ctx))
|
||||
forceKillTimer = terminateCommandFn(cmd)
|
||||
waitErr = <-waitCh
|
||||
}
|
||||
|
||||
if forceKillTimer != nil {
|
||||
forceKillTimer.Stop()
|
||||
}
|
||||
|
||||
var parsed parseResult
|
||||
if ctxCancelled {
|
||||
closeWithReason(stdout, stdoutCloseReasonCtx)
|
||||
parsed = <-parseCh
|
||||
} else {
|
||||
drainTimer := time.NewTimer(stdoutDrainTimeout)
|
||||
defer drainTimer.Stop()
|
||||
|
||||
select {
|
||||
case parsed = <-parseCh:
|
||||
closeWithReason(stdout, stdoutCloseReasonWait)
|
||||
case <-messageSeen:
|
||||
closeWithReason(stdout, stdoutCloseReasonWait)
|
||||
parsed = <-parseCh
|
||||
case <-drainTimer.C:
|
||||
closeWithReason(stdout, stdoutCloseReasonDrain)
|
||||
parsed = <-parseCh
|
||||
}
|
||||
}
|
||||
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
if errors.Is(ctxErr, context.DeadlineExceeded) {
|
||||
result.ExitCode = 124
|
||||
result.Error = attachStderr(fmt.Sprintf("%s execution timeout", commandName))
|
||||
return result
|
||||
}
|
||||
result.ExitCode = 130
|
||||
result.Error = attachStderr("execution cancelled")
|
||||
return result
|
||||
}
|
||||
|
||||
if waitErr != nil {
|
||||
if exitErr, ok := waitErr.(*exec.ExitError); ok {
|
||||
code := exitErr.ExitCode()
|
||||
logErrorFn(fmt.Sprintf("%s exited with status %d", commandName, code))
|
||||
result.ExitCode = code
|
||||
result.Error = attachStderr(fmt.Sprintf("%s exited with status %d", commandName, code))
|
||||
return result
|
||||
}
|
||||
logErrorFn(commandName + " error: " + waitErr.Error())
|
||||
result.ExitCode = 1
|
||||
result.Error = attachStderr(commandName + " error: " + waitErr.Error())
|
||||
return result
|
||||
}
|
||||
|
||||
message := parsed.message
|
||||
threadID := parsed.threadID
|
||||
if message == "" {
|
||||
logErrorFn(fmt.Sprintf("%s completed without agent_message output", commandName))
|
||||
result.ExitCode = 1
|
||||
result.Error = attachStderr(fmt.Sprintf("%s completed without agent_message output", commandName))
|
||||
return result
|
||||
}
|
||||
|
||||
if stdoutLogger != nil {
|
||||
stdoutLogger.Flush()
|
||||
}
|
||||
if stderrLogger != nil {
|
||||
stderrLogger.Flush()
|
||||
}
|
||||
|
||||
result.ExitCode = 0
|
||||
result.Message = message
|
||||
result.SessionID = threadID
|
||||
if logger := activeLogger(); logger != nil {
|
||||
result.LogPath = logger.Path()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func forwardSignals(ctx context.Context, cmd commandRunner, logErrorFn func(string)) {
|
||||
notify := signalNotifyFn
|
||||
stop := signalStopFn
|
||||
if notify == nil {
|
||||
notify = signal.Notify
|
||||
}
|
||||
if stop == nil {
|
||||
stop = signal.Stop
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
defer stop(sigCh)
|
||||
select {
|
||||
case sig := <-sigCh:
|
||||
logErrorFn(fmt.Sprintf("Received signal: %v", sig))
|
||||
if proc := cmd.Process(); proc != nil {
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||
if p := cmd.Process(); p != nil {
|
||||
_ = p.Kill()
|
||||
}
|
||||
})
|
||||
}
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func cancelReason(commandName string, ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return "Context cancelled"
|
||||
}
|
||||
|
||||
if commandName == "" {
|
||||
commandName = codexCommand
|
||||
}
|
||||
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return fmt.Sprintf("%s execution timeout", commandName)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Execution cancelled, terminating %s process", commandName)
|
||||
}
|
||||
|
||||
type stdoutReasonCloser interface {
|
||||
CloseWithReason(string) error
|
||||
}
|
||||
|
||||
func closeWithReason(rc io.ReadCloser, reason string) {
|
||||
if rc == nil {
|
||||
return
|
||||
}
|
||||
if c, ok := rc.(stdoutReasonCloser); ok {
|
||||
_ = c.CloseWithReason(reason)
|
||||
return
|
||||
}
|
||||
_ = rc.Close()
|
||||
}
|
||||
|
||||
type forceKillTimer struct {
|
||||
timer *time.Timer
|
||||
done chan struct{}
|
||||
stopped atomic.Bool
|
||||
drained atomic.Bool
|
||||
}
|
||||
|
||||
func (t *forceKillTimer) Stop() {
|
||||
if t == nil || t.timer == nil {
|
||||
return
|
||||
}
|
||||
if !t.timer.Stop() {
|
||||
<-t.done
|
||||
t.drained.Store(true)
|
||||
}
|
||||
t.stopped.Store(true)
|
||||
}
|
||||
|
||||
func terminateCommand(cmd commandRunner) *forceKillTimer {
|
||||
if cmd == nil {
|
||||
return nil
|
||||
}
|
||||
proc := cmd.Process()
|
||||
if proc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
timer := time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||
if p := cmd.Process(); p != nil {
|
||||
_ = p.Kill()
|
||||
}
|
||||
close(done)
|
||||
})
|
||||
|
||||
return &forceKillTimer{timer: timer, done: done}
|
||||
}
|
||||
|
||||
func terminateProcess(cmd commandRunner) *time.Timer {
|
||||
if cmd == nil {
|
||||
return nil
|
||||
}
|
||||
proc := cmd.Process()
|
||||
if proc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
|
||||
return time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||
if p := cmd.Process(); p != nil {
|
||||
_ = p.Kill()
|
||||
}
|
||||
})
|
||||
}
|
||||
577
codeagent-wrapper/executor_concurrent_test.go
Normal file
577
codeagent-wrapper/executor_concurrent_test.go
Normal file
@@ -0,0 +1,577 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type execFakeProcess struct {
|
||||
pid int
|
||||
signals []os.Signal
|
||||
killed atomic.Int32
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (p *execFakeProcess) Pid() int { return p.pid }
|
||||
func (p *execFakeProcess) Kill() error {
|
||||
p.killed.Add(1)
|
||||
return nil
|
||||
}
|
||||
func (p *execFakeProcess) Signal(sig os.Signal) error {
|
||||
p.mu.Lock()
|
||||
p.signals = append(p.signals, sig)
|
||||
p.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
type writeCloserStub struct {
|
||||
bytes.Buffer
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
func (w *writeCloserStub) Close() error {
|
||||
w.closed.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
type reasonReadCloser struct {
|
||||
r io.Reader
|
||||
closed []string
|
||||
mu sync.Mutex
|
||||
closedC chan struct{}
|
||||
}
|
||||
|
||||
func newReasonReadCloser(data string) *reasonReadCloser {
|
||||
return &reasonReadCloser{r: strings.NewReader(data), closedC: make(chan struct{}, 1)}
|
||||
}
|
||||
|
||||
func (rc *reasonReadCloser) Read(p []byte) (int, error) { return rc.r.Read(p) }
|
||||
func (rc *reasonReadCloser) Close() error { rc.record("close"); return nil }
|
||||
func (rc *reasonReadCloser) CloseWithReason(reason string) error {
|
||||
rc.record(reason)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *reasonReadCloser) record(reason string) {
|
||||
rc.mu.Lock()
|
||||
rc.closed = append(rc.closed, reason)
|
||||
rc.mu.Unlock()
|
||||
select {
|
||||
case rc.closedC <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
type execFakeRunner struct {
|
||||
stdout io.ReadCloser
|
||||
process processHandle
|
||||
stdin io.WriteCloser
|
||||
waitErr error
|
||||
waitDelay time.Duration
|
||||
startErr error
|
||||
stdoutErr error
|
||||
stdinErr error
|
||||
allowNilProcess bool
|
||||
started atomic.Bool
|
||||
}
|
||||
|
||||
func (f *execFakeRunner) Start() error {
|
||||
if f.startErr != nil {
|
||||
return f.startErr
|
||||
}
|
||||
f.started.Store(true)
|
||||
return nil
|
||||
}
|
||||
func (f *execFakeRunner) Wait() error {
|
||||
if f.waitDelay > 0 {
|
||||
time.Sleep(f.waitDelay)
|
||||
}
|
||||
return f.waitErr
|
||||
}
|
||||
func (f *execFakeRunner) StdoutPipe() (io.ReadCloser, error) {
|
||||
if f.stdoutErr != nil {
|
||||
return nil, f.stdoutErr
|
||||
}
|
||||
if f.stdout == nil {
|
||||
f.stdout = io.NopCloser(strings.NewReader(""))
|
||||
}
|
||||
return f.stdout, nil
|
||||
}
|
||||
func (f *execFakeRunner) StdinPipe() (io.WriteCloser, error) {
|
||||
if f.stdinErr != nil {
|
||||
return nil, f.stdinErr
|
||||
}
|
||||
if f.stdin != nil {
|
||||
return f.stdin, nil
|
||||
}
|
||||
return &writeCloserStub{}, nil
|
||||
}
|
||||
func (f *execFakeRunner) SetStderr(io.Writer) {}
|
||||
func (f *execFakeRunner) SetDir(string) {}
|
||||
func (f *execFakeRunner) Process() processHandle {
|
||||
if f.process != nil {
|
||||
return f.process
|
||||
}
|
||||
if f.allowNilProcess {
|
||||
return nil
|
||||
}
|
||||
return &execFakeProcess{pid: 1}
|
||||
}
|
||||
|
||||
func TestExecutorHelperCoverage(t *testing.T) {
|
||||
t.Run("realCmdAndProcess", func(t *testing.T) {
|
||||
rc := &realCmd{}
|
||||
if err := rc.Start(); err == nil {
|
||||
t.Fatalf("expected error for nil command")
|
||||
}
|
||||
if err := rc.Wait(); err == nil {
|
||||
t.Fatalf("expected error for nil command")
|
||||
}
|
||||
if _, err := rc.StdoutPipe(); err == nil {
|
||||
t.Fatalf("expected error for nil command")
|
||||
}
|
||||
if _, err := rc.StdinPipe(); err == nil {
|
||||
t.Fatalf("expected error for nil command")
|
||||
}
|
||||
rc.SetStderr(io.Discard)
|
||||
if rc.Process() != nil {
|
||||
t.Fatalf("expected nil process")
|
||||
}
|
||||
rcWithCmd := &realCmd{cmd: &exec.Cmd{}}
|
||||
rcWithCmd.SetStderr(io.Discard)
|
||||
echoCmd := exec.Command("echo", "ok")
|
||||
rcProc := &realCmd{cmd: echoCmd}
|
||||
stdoutPipe, err := rcProc.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("StdoutPipe error: %v", err)
|
||||
}
|
||||
stdinPipe, err := rcProc.StdinPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("StdinPipe error: %v", err)
|
||||
}
|
||||
rcProc.SetStderr(io.Discard)
|
||||
if err := rcProc.Start(); err != nil {
|
||||
t.Fatalf("Start failed: %v", err)
|
||||
}
|
||||
_, _ = stdinPipe.Write([]byte{})
|
||||
_ = stdinPipe.Close()
|
||||
procHandle := rcProc.Process()
|
||||
if procHandle == nil {
|
||||
t.Fatalf("expected process handle")
|
||||
}
|
||||
_ = procHandle.Signal(syscall.SIGTERM)
|
||||
_ = procHandle.Kill()
|
||||
_ = rcProc.Wait()
|
||||
_, _ = io.ReadAll(stdoutPipe)
|
||||
|
||||
rp := &realProcess{}
|
||||
if rp.Pid() != 0 {
|
||||
t.Fatalf("nil process should have pid 0")
|
||||
}
|
||||
if rp.Kill() != nil {
|
||||
t.Fatalf("nil process Kill should be nil")
|
||||
}
|
||||
if rp.Signal(syscall.SIGTERM) != nil {
|
||||
t.Fatalf("nil process Signal should be nil")
|
||||
}
|
||||
rpLive := &realProcess{proc: &os.Process{Pid: 99}}
|
||||
if rpLive.Pid() != 99 {
|
||||
t.Fatalf("expected pid 99, got %d", rpLive.Pid())
|
||||
}
|
||||
_ = rpLive.Kill()
|
||||
_ = rpLive.Signal(syscall.SIGTERM)
|
||||
})
|
||||
|
||||
t.Run("topologicalSortAndSkip", func(t *testing.T) {
|
||||
layers, err := topologicalSort([]TaskSpec{{ID: "root"}, {ID: "child", Dependencies: []string{"root"}}})
|
||||
if err != nil || len(layers) != 2 {
|
||||
t.Fatalf("unexpected topological sort result: layers=%d err=%v", len(layers), err)
|
||||
}
|
||||
if _, err := topologicalSort([]TaskSpec{{ID: "cycle", Dependencies: []string{"cycle"}}}); err == nil {
|
||||
t.Fatalf("expected cycle detection error")
|
||||
}
|
||||
|
||||
failed := map[string]TaskResult{"root": {ExitCode: 1}}
|
||||
if skip, _ := shouldSkipTask(TaskSpec{ID: "child", Dependencies: []string{"root"}}, failed); !skip {
|
||||
t.Fatalf("should skip when dependency failed")
|
||||
}
|
||||
if skip, _ := shouldSkipTask(TaskSpec{ID: "leaf"}, failed); skip {
|
||||
t.Fatalf("should not skip task without dependencies")
|
||||
}
|
||||
if skip, _ := shouldSkipTask(TaskSpec{ID: "child-ok", Dependencies: []string{"root"}}, map[string]TaskResult{}); skip {
|
||||
t.Fatalf("should not skip when dependencies succeeded")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cancelledTaskResult", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
res := cancelledTaskResult("t1", ctx)
|
||||
if res.ExitCode != 130 {
|
||||
t.Fatalf("expected cancel exit code, got %d", res.ExitCode)
|
||||
}
|
||||
|
||||
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 0)
|
||||
defer timeoutCancel()
|
||||
res = cancelledTaskResult("t2", timeoutCtx)
|
||||
if res.ExitCode != 124 {
|
||||
t.Fatalf("expected timeout exit code, got %d", res.ExitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("generateFinalOutputAndArgs", func(t *testing.T) {
|
||||
out := generateFinalOutput([]TaskResult{
|
||||
{TaskID: "ok", ExitCode: 0},
|
||||
{TaskID: "fail", ExitCode: 1, Error: "boom"},
|
||||
})
|
||||
if !strings.Contains(out, "ok") || !strings.Contains(out, "fail") {
|
||||
t.Fatalf("unexpected summary output: %s", out)
|
||||
}
|
||||
out = generateFinalOutput([]TaskResult{{TaskID: "rich", ExitCode: 0, SessionID: "sess", LogPath: "/tmp/log", Message: "hello"}})
|
||||
if !strings.Contains(out, "Session: sess") || !strings.Contains(out, "Log: /tmp/log") || !strings.Contains(out, "hello") {
|
||||
t.Fatalf("rich output missing fields: %s", out)
|
||||
}
|
||||
|
||||
args := buildCodexArgs(&Config{Mode: "new", WorkDir: "/tmp"}, "task")
|
||||
if len(args) == 0 || args[3] != "/tmp" {
|
||||
t.Fatalf("unexpected codex args: %+v", args)
|
||||
}
|
||||
args = buildCodexArgs(&Config{Mode: "resume", SessionID: "sess"}, "target")
|
||||
if args[3] != "resume" || args[4] != "sess" {
|
||||
t.Fatalf("unexpected resume args: %+v", args)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("executeConcurrentWrapper", func(t *testing.T) {
|
||||
orig := runCodexTaskFn
|
||||
defer func() { runCodexTaskFn = orig }()
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 0, Message: "done"}
|
||||
}
|
||||
os.Setenv("CODEAGENT_MAX_PARALLEL_WORKERS", "1")
|
||||
defer os.Unsetenv("CODEAGENT_MAX_PARALLEL_WORKERS")
|
||||
|
||||
results := executeConcurrent([][]TaskSpec{{{ID: "wrap"}}}, 1)
|
||||
if len(results) != 1 || results[0].TaskID != "wrap" {
|
||||
t.Fatalf("unexpected wrapper results: %+v", results)
|
||||
}
|
||||
|
||||
unbounded := executeConcurrentWithContext(context.Background(), [][]TaskSpec{{{ID: "unbounded"}}}, 1, 0)
|
||||
if len(unbounded) != 1 || unbounded[0].ExitCode != 0 {
|
||||
t.Fatalf("unexpected unbounded result: %+v", unbounded)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
cancelled := executeConcurrentWithContext(ctx, [][]TaskSpec{{{ID: "cancel"}}}, 1, 1)
|
||||
if cancelled[0].ExitCode == 0 {
|
||||
t.Fatalf("expected cancelled result, got %+v", cancelled[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExecutorRunCodexTaskWithContext(t *testing.T) {
|
||||
origRunner := newCommandRunner
|
||||
defer func() { newCommandRunner = origRunner }()
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var firstStdout *reasonReadCloser
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
rc := newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"hello"}}`)
|
||||
if firstStdout == nil {
|
||||
firstStdout = rc
|
||||
}
|
||||
return &execFakeRunner{stdout: rc, process: &execFakeProcess{pid: 1234}}
|
||||
}
|
||||
|
||||
res := runCodexTaskWithContext(context.Background(), TaskSpec{ID: "task-1", Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
||||
if res.Error != "" || res.Message != "hello" || res.ExitCode != 0 {
|
||||
t.Fatalf("unexpected result: %+v", res)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-firstStdout.closedC:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("stdout not closed with reason")
|
||||
}
|
||||
|
||||
orig := runCodexTaskFn
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 0, Message: "ok"}
|
||||
}
|
||||
t.Cleanup(func() { runCodexTaskFn = orig })
|
||||
|
||||
if res := runCodexTask(TaskSpec{Task: "task-text", WorkDir: "."}, true, 1); res.ExitCode != 0 {
|
||||
t.Fatalf("runCodexTask failed: %+v", res)
|
||||
}
|
||||
|
||||
msg, threadID, code := runCodexProcess(context.Background(), []string{"arg"}, "content", false, 1)
|
||||
if code != 0 || msg == "" {
|
||||
t.Fatalf("runCodexProcess unexpected result: msg=%q code=%d threadID=%s", msg, code, threadID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("startErrors", func(t *testing.T) {
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return &execFakeRunner{startErr: errors.New("executable file not found"), process: &execFakeProcess{pid: 1}}
|
||||
}
|
||||
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
||||
if res.ExitCode != 127 {
|
||||
t.Fatalf("expected missing executable exit code, got %d", res.ExitCode)
|
||||
}
|
||||
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return &execFakeRunner{startErr: errors.New("start failed"), process: &execFakeProcess{pid: 2}}
|
||||
}
|
||||
res = runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
||||
if res.ExitCode == 0 {
|
||||
t.Fatalf("expected non-zero exit on start failure")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("timeoutAndPipes", func(t *testing.T) {
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return &execFakeRunner{
|
||||
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"slow"}}`),
|
||||
process: &execFakeProcess{pid: 5},
|
||||
waitDelay: 20 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: ".", UseStdin: true}, nil, nil, false, false, 0)
|
||||
if res.ExitCode == 0 {
|
||||
t.Fatalf("expected timeout result, got %+v", res)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pipeErrors", func(t *testing.T) {
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return &execFakeRunner{stdoutErr: errors.New("stdout fail"), process: &execFakeProcess{pid: 6}}
|
||||
}
|
||||
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
||||
if res.ExitCode == 0 {
|
||||
t.Fatalf("expected failure on stdout pipe error")
|
||||
}
|
||||
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return &execFakeRunner{stdinErr: errors.New("stdin fail"), process: &execFakeProcess{pid: 7}}
|
||||
}
|
||||
res = runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: ".", UseStdin: true}, nil, nil, false, false, 1)
|
||||
if res.ExitCode == 0 {
|
||||
t.Fatalf("expected failure on stdin pipe error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("waitExitError", func(t *testing.T) {
|
||||
err := exec.Command("false").Run()
|
||||
exitErr, _ := err.(*exec.ExitError)
|
||||
if exitErr == nil {
|
||||
t.Fatalf("expected exec.ExitError")
|
||||
}
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return &execFakeRunner{
|
||||
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"ignored"}}`),
|
||||
process: &execFakeProcess{pid: 8},
|
||||
waitErr: exitErr,
|
||||
}
|
||||
}
|
||||
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
||||
if res.ExitCode == 0 {
|
||||
t.Fatalf("expected non-zero exit on wait error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("contextCancelled", func(t *testing.T) {
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return &execFakeRunner{
|
||||
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"cancel"}}`),
|
||||
process: &execFakeProcess{pid: 9},
|
||||
waitDelay: 10 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
res := runCodexTaskWithContext(ctx, TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
||||
if res.ExitCode == 0 {
|
||||
t.Fatalf("expected cancellation result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("silentLogger", func(t *testing.T) {
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return &execFakeRunner{
|
||||
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"quiet"}}`),
|
||||
process: &execFakeProcess{pid: 10},
|
||||
}
|
||||
}
|
||||
_ = closeLogger()
|
||||
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, true, 1)
|
||||
if res.ExitCode != 0 || res.LogPath == "" {
|
||||
t.Fatalf("expected success with temp logger, got %+v", res)
|
||||
}
|
||||
_ = closeLogger()
|
||||
})
|
||||
|
||||
t.Run("missingMessage", func(t *testing.T) {
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return &execFakeRunner{
|
||||
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"task","text":"noop"}}`),
|
||||
process: &execFakeProcess{pid: 11},
|
||||
}
|
||||
}
|
||||
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "payload", WorkDir: "."}, nil, nil, false, false, 1)
|
||||
if res.ExitCode == 0 {
|
||||
t.Fatalf("expected failure when no agent_message returned")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExecutorSignalAndTermination(t *testing.T) {
|
||||
forceKillDelay.Store(0)
|
||||
defer forceKillDelay.Store(5)
|
||||
|
||||
proc := &execFakeProcess{pid: 42}
|
||||
cmd := &execFakeRunner{process: proc}
|
||||
|
||||
origNotify := signalNotifyFn
|
||||
origStop := signalStopFn
|
||||
defer func() {
|
||||
signalNotifyFn = origNotify
|
||||
signalStopFn = origStop
|
||||
}()
|
||||
|
||||
signalNotifyFn = func(c chan<- os.Signal, sigs ...os.Signal) {
|
||||
go func() { c <- syscall.SIGINT }()
|
||||
}
|
||||
signalStopFn = func(c chan<- os.Signal) {}
|
||||
|
||||
forwardSignals(context.Background(), cmd, func(string) {})
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
proc.mu.Lock()
|
||||
signalled := len(proc.signals)
|
||||
proc.mu.Unlock()
|
||||
if signalled == 0 {
|
||||
t.Fatalf("process did not receive signal")
|
||||
}
|
||||
if proc.killed.Load() == 0 {
|
||||
t.Fatalf("process was not killed after signal")
|
||||
}
|
||||
|
||||
timer := terminateProcess(cmd)
|
||||
if timer == nil {
|
||||
t.Fatalf("terminateProcess returned nil timer")
|
||||
}
|
||||
timer.Stop()
|
||||
|
||||
ft := terminateCommand(cmd)
|
||||
if ft == nil {
|
||||
t.Fatalf("terminateCommand returned nil")
|
||||
}
|
||||
ft.Stop()
|
||||
|
||||
cmdKill := &execFakeRunner{process: &execFakeProcess{pid: 50}}
|
||||
ftKill := terminateCommand(cmdKill)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
if p, ok := cmdKill.process.(*execFakeProcess); ok && p.killed.Load() == 0 {
|
||||
t.Fatalf("terminateCommand did not kill process")
|
||||
}
|
||||
ftKill.Stop()
|
||||
|
||||
cmdKill2 := &execFakeRunner{process: &execFakeProcess{pid: 51}}
|
||||
timer2 := terminateProcess(cmdKill2)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
if p, ok := cmdKill2.process.(*execFakeProcess); ok && p.killed.Load() == 0 {
|
||||
t.Fatalf("terminateProcess did not kill process")
|
||||
}
|
||||
timer2.Stop()
|
||||
|
||||
if terminateCommand(nil) != nil {
|
||||
t.Fatalf("terminateCommand should return nil for nil cmd")
|
||||
}
|
||||
if terminateCommand(&execFakeRunner{allowNilProcess: true}) != nil {
|
||||
t.Fatalf("terminateCommand should return nil when process is nil")
|
||||
}
|
||||
if terminateProcess(nil) != nil {
|
||||
t.Fatalf("terminateProcess should return nil for nil cmd")
|
||||
}
|
||||
if terminateProcess(&execFakeRunner{allowNilProcess: true}) != nil {
|
||||
t.Fatalf("terminateProcess should return nil when process is nil")
|
||||
}
|
||||
|
||||
signalNotifyFn = func(c chan<- os.Signal, sigs ...os.Signal) {}
|
||||
ctxDone, cancelDone := context.WithCancel(context.Background())
|
||||
cancelDone()
|
||||
forwardSignals(ctxDone, &execFakeRunner{process: &execFakeProcess{pid: 70}}, func(string) {})
|
||||
}
|
||||
|
||||
func TestExecutorCancelReasonAndCloseWithReason(t *testing.T) {
|
||||
if reason := cancelReason("", nil); !strings.Contains(reason, "Context") {
|
||||
t.Fatalf("unexpected cancelReason for nil ctx: %s", reason)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 0)
|
||||
defer cancel()
|
||||
if !strings.Contains(cancelReason("cmd", ctx), "timeout") {
|
||||
t.Fatalf("expected timeout reason")
|
||||
}
|
||||
cancelCtx, cancelFn := context.WithCancel(context.Background())
|
||||
cancelFn()
|
||||
if !strings.Contains(cancelReason("cmd", cancelCtx), "Execution cancelled") {
|
||||
t.Fatalf("expected cancellation reason")
|
||||
}
|
||||
if !strings.Contains(cancelReason("", cancelCtx), "codex") {
|
||||
t.Fatalf("expected default command name in cancel reason")
|
||||
}
|
||||
|
||||
rc := &reasonReadCloser{r: strings.NewReader("data"), closedC: make(chan struct{}, 1)}
|
||||
closeWithReason(rc, "why")
|
||||
select {
|
||||
case <-rc.closedC:
|
||||
default:
|
||||
t.Fatalf("CloseWithReason was not called")
|
||||
}
|
||||
|
||||
plain := io.NopCloser(strings.NewReader("x"))
|
||||
closeWithReason(plain, "noop")
|
||||
closeWithReason(nil, "noop")
|
||||
}
|
||||
|
||||
func TestExecutorForceKillTimerStop(t *testing.T) {
|
||||
done := make(chan struct{}, 1)
|
||||
ft := &forceKillTimer{timer: time.AfterFunc(50*time.Millisecond, func() { done <- struct{}{} }), done: done}
|
||||
ft.Stop()
|
||||
|
||||
done2 := make(chan struct{}, 1)
|
||||
ft2 := &forceKillTimer{timer: time.AfterFunc(0, func() { done2 <- struct{}{} }), done: done2}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
ft2.Stop()
|
||||
|
||||
var nilTimer *forceKillTimer
|
||||
nilTimer.Stop()
|
||||
(&forceKillTimer{}).Stop()
|
||||
}
|
||||
|
||||
func TestExecutorForwardSignalsDefaults(t *testing.T) {
|
||||
origNotify := signalNotifyFn
|
||||
origStop := signalStopFn
|
||||
signalNotifyFn = nil
|
||||
signalStopFn = nil
|
||||
defer func() {
|
||||
signalNotifyFn = origNotify
|
||||
signalStopFn = origStop
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
forwardSignals(ctx, &execFakeRunner{process: &execFakeProcess{pid: 80}}, func(string) {})
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
3
codeagent-wrapper/go.mod
Normal file
3
codeagent-wrapper/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module codeagent-wrapper
|
||||
|
||||
go 1.21
|
||||
39
codeagent-wrapper/log_writer_limit_test.go
Normal file
39
codeagent-wrapper/log_writer_limit_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogWriterWriteLimitsBuffer(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
|
||||
logger, err := NewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger error: %v", err)
|
||||
}
|
||||
setLogger(logger)
|
||||
defer closeLogger()
|
||||
|
||||
lw := newLogWriter("P:", 10)
|
||||
_, _ = lw.Write([]byte(strings.Repeat("a", 100)))
|
||||
|
||||
if lw.buf.Len() != 10 {
|
||||
t.Fatalf("logWriter buffer len=%d, want %d", lw.buf.Len(), 10)
|
||||
}
|
||||
if !lw.dropped {
|
||||
t.Fatalf("expected logWriter to drop overlong line bytes")
|
||||
}
|
||||
|
||||
lw.Flush()
|
||||
logger.Flush()
|
||||
data, err := os.ReadFile(logger.Path())
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile error: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "P:aaaaaaa...") {
|
||||
t.Fatalf("log output missing truncated entry, got %q", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
533
codeagent-wrapper/logger.go
Normal file
533
codeagent-wrapper/logger.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Logger writes log messages asynchronously to a temp file.
|
||||
// It is intentionally minimal: a buffered channel + single worker goroutine
|
||||
// to avoid contention while keeping ordering guarantees.
|
||||
type Logger struct {
|
||||
path string
|
||||
file *os.File
|
||||
writer *bufio.Writer
|
||||
ch chan logEntry
|
||||
flushReq chan chan struct{}
|
||||
done chan struct{}
|
||||
closed atomic.Bool
|
||||
closeOnce sync.Once
|
||||
workerWG sync.WaitGroup
|
||||
pendingWG sync.WaitGroup
|
||||
flushMu sync.Mutex
|
||||
}
|
||||
|
||||
type logEntry struct {
|
||||
level string
|
||||
msg string
|
||||
}
|
||||
|
||||
// CleanupStats captures the outcome of a cleanupOldLogs run.
|
||||
type CleanupStats struct {
|
||||
Scanned int
|
||||
Deleted int
|
||||
Kept int
|
||||
Errors int
|
||||
DeletedFiles []string
|
||||
KeptFiles []string
|
||||
}
|
||||
|
||||
var (
|
||||
processRunningCheck = isProcessRunning
|
||||
processStartTimeFn = getProcessStartTime
|
||||
removeLogFileFn = os.Remove
|
||||
globLogFiles = filepath.Glob
|
||||
fileStatFn = os.Lstat // Use Lstat to detect symlinks
|
||||
evalSymlinksFn = filepath.EvalSymlinks
|
||||
)
|
||||
|
||||
// NewLogger creates the async logger and starts the worker goroutine.
|
||||
// The log file is created under os.TempDir() using the required naming scheme.
|
||||
func NewLogger() (*Logger, error) {
|
||||
return NewLoggerWithSuffix("")
|
||||
}
|
||||
|
||||
// NewLoggerWithSuffix creates a logger with an optional suffix in the filename.
|
||||
// Useful for tests that need isolated log files within the same process.
|
||||
func NewLoggerWithSuffix(suffix string) (*Logger, error) {
|
||||
filename := fmt.Sprintf("%s-%d", primaryLogPrefix(), os.Getpid())
|
||||
if suffix != "" {
|
||||
filename += "-" + suffix
|
||||
}
|
||||
filename += ".log"
|
||||
|
||||
path := filepath.Clean(filepath.Join(os.TempDir(), filename))
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := &Logger{
|
||||
path: path,
|
||||
file: f,
|
||||
writer: bufio.NewWriterSize(f, 4096),
|
||||
ch: make(chan logEntry, 1000),
|
||||
flushReq: make(chan chan struct{}, 1),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
l.workerWG.Add(1)
|
||||
go l.run()
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// Path returns the underlying log file path (useful for tests/inspection).
|
||||
func (l *Logger) Path() string {
|
||||
if l == nil {
|
||||
return ""
|
||||
}
|
||||
return l.path
|
||||
}
|
||||
|
||||
// Info logs at INFO level.
|
||||
func (l *Logger) Info(msg string) { l.log("INFO", msg) }
|
||||
|
||||
// Warn logs at WARN level.
|
||||
func (l *Logger) Warn(msg string) { l.log("WARN", msg) }
|
||||
|
||||
// Debug logs at DEBUG level.
|
||||
func (l *Logger) Debug(msg string) { l.log("DEBUG", msg) }
|
||||
|
||||
// Error logs at ERROR level.
|
||||
func (l *Logger) Error(msg string) { l.log("ERROR", msg) }
|
||||
|
||||
// Close stops the worker and syncs the log file.
|
||||
// The log file is NOT removed, allowing inspection after program exit.
|
||||
// It is safe to call multiple times.
|
||||
// Returns after a 5-second timeout if worker doesn't stop gracefully.
|
||||
func (l *Logger) Close() error {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var closeErr error
|
||||
|
||||
l.closeOnce.Do(func() {
|
||||
l.closed.Store(true)
|
||||
close(l.done)
|
||||
close(l.ch)
|
||||
|
||||
// Wait for worker with timeout
|
||||
workerDone := make(chan struct{})
|
||||
go func() {
|
||||
l.workerWG.Wait()
|
||||
close(workerDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-workerDone:
|
||||
// Worker stopped gracefully
|
||||
case <-time.After(5 * time.Second):
|
||||
// Worker timeout - proceed with cleanup anyway
|
||||
closeErr = fmt.Errorf("logger worker timeout during close")
|
||||
}
|
||||
|
||||
if err := l.writer.Flush(); err != nil && closeErr == nil {
|
||||
closeErr = err
|
||||
}
|
||||
|
||||
if err := l.file.Sync(); err != nil && closeErr == nil {
|
||||
closeErr = err
|
||||
}
|
||||
|
||||
if err := l.file.Close(); err != nil && closeErr == nil {
|
||||
closeErr = err
|
||||
}
|
||||
|
||||
// Log file is kept for debugging - NOT removed
|
||||
// Users can manually clean up /tmp/<wrapper>-*.log files
|
||||
})
|
||||
|
||||
return closeErr
|
||||
}
|
||||
|
||||
// RemoveLogFile removes the log file. Should only be called after Close().
|
||||
func (l *Logger) RemoveLogFile() error {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(l.path)
|
||||
}
|
||||
|
||||
// ExtractRecentErrors reads the log file and returns the most recent ERROR and WARN entries.
|
||||
// Returns up to maxEntries entries in chronological order.
|
||||
func (l *Logger) ExtractRecentErrors(maxEntries int) []string {
|
||||
if l == nil || l.path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Open(l.path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var entries []string
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.Contains(line, "] ERROR:") || strings.Contains(line, "] WARN:") {
|
||||
entries = append(entries, line)
|
||||
}
|
||||
}
|
||||
|
||||
// Keep only the last maxEntries
|
||||
if len(entries) > maxEntries {
|
||||
entries = entries[len(entries)-maxEntries:]
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
// Flush waits for all pending log entries to be written. Primarily for tests.
|
||||
// Returns after a 5-second timeout to prevent indefinite blocking.
|
||||
func (l *Logger) Flush() {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.flushMu.Lock()
|
||||
defer l.flushMu.Unlock()
|
||||
|
||||
// Wait for pending entries with timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
l.pendingWG.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// All pending entries processed
|
||||
case <-ctx.Done():
|
||||
// Timeout - return without full flush
|
||||
return
|
||||
}
|
||||
|
||||
// Trigger writer flush
|
||||
flushDone := make(chan struct{})
|
||||
select {
|
||||
case l.flushReq <- flushDone:
|
||||
// Wait for flush to complete
|
||||
select {
|
||||
case <-flushDone:
|
||||
// Flush completed
|
||||
case <-time.After(1 * time.Second):
|
||||
// Flush timeout
|
||||
}
|
||||
case <-l.done:
|
||||
// Logger is closing
|
||||
case <-time.After(1 * time.Second):
|
||||
// Timeout sending flush request
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) log(level, msg string) {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
if l.closed.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
entry := logEntry{level: level, msg: msg}
|
||||
l.flushMu.Lock()
|
||||
l.pendingWG.Add(1)
|
||||
l.flushMu.Unlock()
|
||||
|
||||
select {
|
||||
case l.ch <- entry:
|
||||
// Successfully sent to channel
|
||||
case <-l.done:
|
||||
// Logger is closing, drop this entry
|
||||
l.pendingWG.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) run() {
|
||||
defer l.workerWG.Done()
|
||||
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case entry, ok := <-l.ch:
|
||||
if !ok {
|
||||
// Channel closed, final flush
|
||||
_ = l.writer.Flush()
|
||||
return
|
||||
}
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
|
||||
pid := os.Getpid()
|
||||
fmt.Fprintf(l.writer, "[%s] [PID:%d] %s: %s\n", timestamp, pid, entry.level, entry.msg)
|
||||
l.pendingWG.Done()
|
||||
|
||||
case <-ticker.C:
|
||||
_ = l.writer.Flush()
|
||||
|
||||
case flushDone := <-l.flushReq:
|
||||
// Explicit flush request - flush writer and sync to disk
|
||||
_ = l.writer.Flush()
|
||||
_ = l.file.Sync()
|
||||
close(flushDone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupOldLogs scans os.TempDir() for wrapper log files and removes those
|
||||
// whose owning process is no longer running (i.e., orphaned logs).
|
||||
// It includes safety checks for:
|
||||
// - PID reuse: Compares file modification time with process start time
|
||||
// - Symlink attacks: Ensures files are within TempDir and not symlinks
|
||||
func cleanupOldLogs() (CleanupStats, error) {
|
||||
var stats CleanupStats
|
||||
tempDir := os.TempDir()
|
||||
|
||||
prefixes := logPrefixes()
|
||||
if len(prefixes) == 0 {
|
||||
prefixes = []string{defaultWrapperName}
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
var matches []string
|
||||
for _, prefix := range prefixes {
|
||||
pattern := filepath.Join(tempDir, fmt.Sprintf("%s-*.log", prefix))
|
||||
found, err := globLogFiles(pattern)
|
||||
if err != nil {
|
||||
logWarn(fmt.Sprintf("cleanupOldLogs: failed to list logs: %v", err))
|
||||
return stats, fmt.Errorf("cleanupOldLogs: %w", err)
|
||||
}
|
||||
for _, path := range found {
|
||||
if _, ok := seen[path]; ok {
|
||||
continue
|
||||
}
|
||||
seen[path] = struct{}{}
|
||||
matches = append(matches, path)
|
||||
}
|
||||
}
|
||||
|
||||
var removeErr error
|
||||
|
||||
for _, path := range matches {
|
||||
stats.Scanned++
|
||||
filename := filepath.Base(path)
|
||||
|
||||
// Security check: Verify file is not a symlink and is within tempDir
|
||||
if shouldSkipFile, reason := isUnsafeFile(path, tempDir); shouldSkipFile {
|
||||
stats.Kept++
|
||||
stats.KeptFiles = append(stats.KeptFiles, filename)
|
||||
if reason != "" {
|
||||
logWarn(fmt.Sprintf("cleanupOldLogs: skipping %s: %s", filename, reason))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
pid, ok := parsePIDFromLog(path)
|
||||
if !ok {
|
||||
stats.Kept++
|
||||
stats.KeptFiles = append(stats.KeptFiles, filename)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if process is running
|
||||
if !processRunningCheck(pid) {
|
||||
// Process not running, safe to delete
|
||||
if err := removeLogFileFn(path); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// File already deleted by another process, don't count as success
|
||||
stats.Kept++
|
||||
stats.KeptFiles = append(stats.KeptFiles, filename+" (already deleted)")
|
||||
continue
|
||||
}
|
||||
stats.Errors++
|
||||
logWarn(fmt.Sprintf("cleanupOldLogs: failed to remove %s: %v", filename, err))
|
||||
removeErr = errors.Join(removeErr, fmt.Errorf("failed to remove %s: %w", filename, err))
|
||||
continue
|
||||
}
|
||||
stats.Deleted++
|
||||
stats.DeletedFiles = append(stats.DeletedFiles, filename)
|
||||
continue
|
||||
}
|
||||
|
||||
// Process is running, check for PID reuse
|
||||
if isPIDReused(path, pid) {
|
||||
// PID was reused, the log file is orphaned
|
||||
if err := removeLogFileFn(path); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
stats.Kept++
|
||||
stats.KeptFiles = append(stats.KeptFiles, filename+" (already deleted)")
|
||||
continue
|
||||
}
|
||||
stats.Errors++
|
||||
logWarn(fmt.Sprintf("cleanupOldLogs: failed to remove %s (PID reused): %v", filename, err))
|
||||
removeErr = errors.Join(removeErr, fmt.Errorf("failed to remove %s: %w", filename, err))
|
||||
continue
|
||||
}
|
||||
stats.Deleted++
|
||||
stats.DeletedFiles = append(stats.DeletedFiles, filename)
|
||||
continue
|
||||
}
|
||||
|
||||
// Process is running and owns this log file
|
||||
stats.Kept++
|
||||
stats.KeptFiles = append(stats.KeptFiles, filename)
|
||||
}
|
||||
|
||||
if removeErr != nil {
|
||||
return stats, fmt.Errorf("cleanupOldLogs: %w", removeErr)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// isUnsafeFile checks if a file is unsafe to delete (symlink or outside tempDir).
|
||||
// Returns (true, reason) if the file should be skipped.
|
||||
func isUnsafeFile(path string, tempDir string) (bool, string) {
|
||||
// Check if file is a symlink
|
||||
info, err := fileStatFn(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return true, "" // File disappeared, skip silently
|
||||
}
|
||||
return true, fmt.Sprintf("stat failed: %v", err)
|
||||
}
|
||||
|
||||
// Check if it's a symlink
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return true, "refusing to delete symlink"
|
||||
}
|
||||
|
||||
// Resolve any path traversal and verify it's within tempDir
|
||||
resolvedPath, err := evalSymlinksFn(path)
|
||||
if err != nil {
|
||||
return true, fmt.Sprintf("path resolution failed: %v", err)
|
||||
}
|
||||
|
||||
// Get absolute path of tempDir
|
||||
absTempDir, err := filepath.Abs(tempDir)
|
||||
if err != nil {
|
||||
return true, fmt.Sprintf("tempDir resolution failed: %v", err)
|
||||
}
|
||||
|
||||
// Ensure resolved path is within tempDir
|
||||
relPath, err := filepath.Rel(absTempDir, resolvedPath)
|
||||
if err != nil || strings.HasPrefix(relPath, "..") {
|
||||
return true, "file is outside tempDir"
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
||||
// isPIDReused checks if a PID has been reused by comparing file modification time
|
||||
// with process start time. Returns true if the log file was created by a different
|
||||
// process that previously had the same PID.
|
||||
func isPIDReused(logPath string, pid int) bool {
|
||||
// Get file modification time (when log was last written)
|
||||
info, err := fileStatFn(logPath)
|
||||
if err != nil {
|
||||
// If we can't stat the file, be conservative and keep it
|
||||
return false
|
||||
}
|
||||
fileModTime := info.ModTime()
|
||||
|
||||
// Get process start time
|
||||
procStartTime := processStartTimeFn(pid)
|
||||
if procStartTime.IsZero() {
|
||||
// Can't determine process start time
|
||||
// Check if file is very old (>7 days), likely from a dead process
|
||||
if time.Since(fileModTime) > 7*24*time.Hour {
|
||||
return true // File is old enough to be from a different process
|
||||
}
|
||||
return false // Be conservative for recent files
|
||||
}
|
||||
|
||||
// If the log file was modified before the process started, PID was reused
|
||||
// Add a small buffer (1 second) to account for clock skew and file system timing
|
||||
return fileModTime.Add(1 * time.Second).Before(procStartTime)
|
||||
}
|
||||
|
||||
func parsePIDFromLog(path string) (int, bool) {
|
||||
name := filepath.Base(path)
|
||||
prefixes := logPrefixes()
|
||||
if len(prefixes) == 0 {
|
||||
prefixes = []string{defaultWrapperName}
|
||||
}
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
prefixWithDash := fmt.Sprintf("%s-", prefix)
|
||||
if !strings.HasPrefix(name, prefixWithDash) || !strings.HasSuffix(name, ".log") {
|
||||
continue
|
||||
}
|
||||
|
||||
core := strings.TrimSuffix(strings.TrimPrefix(name, prefixWithDash), ".log")
|
||||
if core == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
pidPart := core
|
||||
if idx := strings.IndexRune(core, '-'); idx != -1 {
|
||||
pidPart = core[:idx]
|
||||
}
|
||||
|
||||
if pidPart == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
pid, err := strconv.Atoi(pidPart)
|
||||
if err != nil || pid <= 0 {
|
||||
continue
|
||||
}
|
||||
return pid, true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func logConcurrencyPlanning(limit, total int) {
|
||||
logger := activeLogger()
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
logger.Info(fmt.Sprintf("parallel: worker_limit=%s total_tasks=%d", renderWorkerLimit(limit), total))
|
||||
}
|
||||
|
||||
func logConcurrencyState(event, taskID string, active, limit int) {
|
||||
logger := activeLogger()
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
logger.Debug(fmt.Sprintf("parallel: %s task=%s active=%d limit=%s", event, taskID, active, renderWorkerLimit(limit)))
|
||||
}
|
||||
|
||||
func renderWorkerLimit(limit int) string {
|
||||
if limit <= 0 {
|
||||
return "unbounded"
|
||||
}
|
||||
return strconv.Itoa(limit)
|
||||
}
|
||||
868
codeagent-wrapper/logger_test.go
Normal file
868
codeagent-wrapper/logger_test.go
Normal file
@@ -0,0 +1,868 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func compareCleanupStats(got, want CleanupStats) bool {
|
||||
if got.Scanned != want.Scanned || got.Deleted != want.Deleted || got.Kept != want.Kept || got.Errors != want.Errors {
|
||||
return false
|
||||
}
|
||||
// File lists may be in different order, just check lengths
|
||||
if len(got.DeletedFiles) != want.Deleted || len(got.KeptFiles) != want.Kept {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestRunLoggerCreatesFileWithPID(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("TMPDIR", tempDir)
|
||||
|
||||
logger, err := NewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger() error = %v", err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
expectedPath := filepath.Join(tempDir, fmt.Sprintf("codeagent-wrapper-%d.log", os.Getpid()))
|
||||
if logger.Path() != expectedPath {
|
||||
t.Fatalf("logger path = %s, want %s", logger.Path(), expectedPath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(expectedPath); err != nil {
|
||||
t.Fatalf("log file not created: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLoggerWritesLevels(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("TMPDIR", tempDir)
|
||||
|
||||
logger, err := NewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger() error = %v", err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
logger.Info("info message")
|
||||
logger.Warn("warn message")
|
||||
logger.Debug("debug message")
|
||||
logger.Error("error message")
|
||||
|
||||
logger.Flush()
|
||||
|
||||
data, err := os.ReadFile(logger.Path())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
checks := []string{"INFO: info message", "WARN: warn message", "DEBUG: debug message", "ERROR: error message"}
|
||||
for _, c := range checks {
|
||||
if !strings.Contains(content, c) {
|
||||
t.Fatalf("log file missing entry %q, content: %s", c, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLoggerCloseRemovesFileAndStopsWorker(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("TMPDIR", tempDir)
|
||||
|
||||
logger, err := NewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger() error = %v", err)
|
||||
}
|
||||
|
||||
logger.Info("before close")
|
||||
logger.Flush()
|
||||
|
||||
logPath := logger.Path()
|
||||
|
||||
if err := logger.Close(); err != nil {
|
||||
t.Fatalf("Close() returned error: %v", err)
|
||||
}
|
||||
|
||||
// After recent changes, log file is kept for debugging - NOT removed
|
||||
if _, err := os.Stat(logPath); os.IsNotExist(err) {
|
||||
t.Fatalf("log file should exist after Close for debugging, but got IsNotExist")
|
||||
}
|
||||
|
||||
// Clean up manually for test
|
||||
defer os.Remove(logPath)
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
logger.workerWG.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
t.Fatalf("worker goroutine did not exit after Close")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLoggerConcurrentWritesSafe(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("TMPDIR", tempDir)
|
||||
|
||||
logger, err := NewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger() error = %v", err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
const goroutines = 10
|
||||
const perGoroutine = 50
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(goroutines)
|
||||
|
||||
for i := 0; i < goroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < perGoroutine; j++ {
|
||||
logger.Debug(fmt.Sprintf("g%d-%d", id, j))
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
logger.Flush()
|
||||
|
||||
f, err := os.Open(logger.Path())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open log file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
count := 0
|
||||
for scanner.Scan() {
|
||||
count++
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
t.Fatalf("scanner error: %v", err)
|
||||
}
|
||||
|
||||
expected := goroutines * perGoroutine
|
||||
if count != expected {
|
||||
t.Fatalf("unexpected log line count: got %d, want %d", count, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLoggerTerminateProcessActive(t *testing.T) {
|
||||
cmd := exec.Command("sleep", "5")
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Skipf("cannot start sleep command: %v", err)
|
||||
}
|
||||
|
||||
timer := terminateProcess(&realCmd{cmd: cmd})
|
||||
if timer == nil {
|
||||
t.Fatalf("terminateProcess returned nil timer for active process")
|
||||
}
|
||||
defer timer.Stop()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Fatalf("process not terminated promptly")
|
||||
case <-done:
|
||||
}
|
||||
|
||||
// Force the timer callback to run immediately to cover the kill branch.
|
||||
timer.Reset(0)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestRunTerminateProcessNil(t *testing.T) {
|
||||
if timer := terminateProcess(nil); timer != nil {
|
||||
t.Fatalf("terminateProcess(nil) should return nil timer")
|
||||
}
|
||||
if timer := terminateProcess(&realCmd{cmd: &exec.Cmd{}}); timer != nil {
|
||||
t.Fatalf("terminateProcess with nil process should return nil timer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCleanupOldLogsRemovesOrphans(t *testing.T) {
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
orphan1 := createTempLog(t, tempDir, "codex-wrapper-111.log")
|
||||
orphan2 := createTempLog(t, tempDir, "codex-wrapper-222-suffix.log")
|
||||
running1 := createTempLog(t, tempDir, "codex-wrapper-333.log")
|
||||
running2 := createTempLog(t, tempDir, "codex-wrapper-444-extra-info.log")
|
||||
untouched := createTempLog(t, tempDir, "unrelated.log")
|
||||
|
||||
runningPIDs := map[int]bool{333: true, 444: true}
|
||||
stubProcessRunning(t, func(pid int) bool {
|
||||
return runningPIDs[pid]
|
||||
})
|
||||
|
||||
// Stub process start time to be in the past so files won't be considered as PID reused
|
||||
stubProcessStartTime(t, func(pid int) time.Time {
|
||||
if runningPIDs[pid] {
|
||||
// Return a time before file creation
|
||||
return time.Now().Add(-1 * time.Hour)
|
||||
}
|
||||
return time.Time{}
|
||||
})
|
||||
|
||||
stats, err := cleanupOldLogs()
|
||||
if err != nil {
|
||||
t.Fatalf("cleanupOldLogs() unexpected error: %v", err)
|
||||
}
|
||||
|
||||
want := CleanupStats{Scanned: 4, Deleted: 2, Kept: 2}
|
||||
if !compareCleanupStats(stats, want) {
|
||||
t.Fatalf("cleanup stats mismatch: got %+v, want %+v", stats, want)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(orphan1); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected orphan %s to be removed, err=%v", orphan1, err)
|
||||
}
|
||||
if _, err := os.Stat(orphan2); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected orphan %s to be removed, err=%v", orphan2, err)
|
||||
}
|
||||
if _, err := os.Stat(running1); err != nil {
|
||||
t.Fatalf("expected running log %s to remain, err=%v", running1, err)
|
||||
}
|
||||
if _, err := os.Stat(running2); err != nil {
|
||||
t.Fatalf("expected running log %s to remain, err=%v", running2, err)
|
||||
}
|
||||
if _, err := os.Stat(untouched); err != nil {
|
||||
t.Fatalf("expected unrelated file %s to remain, err=%v", untouched, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCleanupOldLogsHandlesInvalidNamesAndErrors(t *testing.T) {
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
invalid := []string{
|
||||
"codex-wrapper-.log",
|
||||
"codex-wrapper.log",
|
||||
"codex-wrapper-foo-bar.txt",
|
||||
"not-a-codex.log",
|
||||
}
|
||||
for _, name := range invalid {
|
||||
createTempLog(t, tempDir, name)
|
||||
}
|
||||
target := createTempLog(t, tempDir, "codex-wrapper-555-extra.log")
|
||||
|
||||
var checked []int
|
||||
stubProcessRunning(t, func(pid int) bool {
|
||||
checked = append(checked, pid)
|
||||
return false
|
||||
})
|
||||
|
||||
stubProcessStartTime(t, func(pid int) time.Time {
|
||||
return time.Time{} // Return zero time for processes not running
|
||||
})
|
||||
|
||||
removeErr := errors.New("remove failure")
|
||||
callCount := 0
|
||||
stubRemoveLogFile(t, func(path string) error {
|
||||
callCount++
|
||||
if path == target {
|
||||
return removeErr
|
||||
}
|
||||
return os.Remove(path)
|
||||
})
|
||||
|
||||
stats, err := cleanupOldLogs()
|
||||
if err == nil {
|
||||
t.Fatalf("cleanupOldLogs() expected error")
|
||||
}
|
||||
if !errors.Is(err, removeErr) {
|
||||
t.Fatalf("cleanupOldLogs error = %v, want %v", err, removeErr)
|
||||
}
|
||||
|
||||
want := CleanupStats{Scanned: 2, Kept: 1, Errors: 1}
|
||||
if !compareCleanupStats(stats, want) {
|
||||
t.Fatalf("cleanup stats mismatch: got %+v, want %+v", stats, want)
|
||||
}
|
||||
|
||||
if len(checked) != 1 || checked[0] != 555 {
|
||||
t.Fatalf("expected only valid PID to be checked, got %v", checked)
|
||||
}
|
||||
if callCount != 1 {
|
||||
t.Fatalf("expected remove to be called once, got %d", callCount)
|
||||
}
|
||||
if _, err := os.Stat(target); err != nil {
|
||||
t.Fatalf("expected errored file %s to remain for manual cleanup, err=%v", target, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCleanupOldLogsHandlesGlobFailures(t *testing.T) {
|
||||
stubProcessRunning(t, func(pid int) bool {
|
||||
t.Fatalf("process check should not run when glob fails")
|
||||
return false
|
||||
})
|
||||
stubProcessStartTime(t, func(int) time.Time {
|
||||
return time.Time{}
|
||||
})
|
||||
|
||||
globErr := errors.New("glob failure")
|
||||
stubGlobLogFiles(t, func(pattern string) ([]string, error) {
|
||||
return nil, globErr
|
||||
})
|
||||
|
||||
stats, err := cleanupOldLogs()
|
||||
if err == nil {
|
||||
t.Fatalf("cleanupOldLogs() expected error")
|
||||
}
|
||||
if !errors.Is(err, globErr) {
|
||||
t.Fatalf("cleanupOldLogs error = %v, want %v", err, globErr)
|
||||
}
|
||||
if stats.Scanned != 0 || stats.Deleted != 0 || stats.Kept != 0 || stats.Errors != 0 || len(stats.DeletedFiles) != 0 || len(stats.KeptFiles) != 0 {
|
||||
t.Fatalf("cleanup stats mismatch: got %+v, want zero", stats)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCleanupOldLogsEmptyDirectoryStats(t *testing.T) {
|
||||
setTempDirEnv(t, t.TempDir())
|
||||
|
||||
stubProcessRunning(t, func(int) bool {
|
||||
t.Fatalf("process check should not run for empty directory")
|
||||
return false
|
||||
})
|
||||
stubProcessStartTime(t, func(int) time.Time {
|
||||
return time.Time{}
|
||||
})
|
||||
|
||||
stats, err := cleanupOldLogs()
|
||||
if err != nil {
|
||||
t.Fatalf("cleanupOldLogs() unexpected error: %v", err)
|
||||
}
|
||||
if stats.Scanned != 0 || stats.Deleted != 0 || stats.Kept != 0 || stats.Errors != 0 || len(stats.DeletedFiles) != 0 || len(stats.KeptFiles) != 0 {
|
||||
t.Fatalf("cleanup stats mismatch: got %+v, want zero", stats)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCleanupOldLogsHandlesTempDirPermissionErrors(t *testing.T) {
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
paths := []string{
|
||||
createTempLog(t, tempDir, "codex-wrapper-6100.log"),
|
||||
createTempLog(t, tempDir, "codex-wrapper-6101.log"),
|
||||
}
|
||||
|
||||
stubProcessRunning(t, func(int) bool { return false })
|
||||
stubProcessStartTime(t, func(int) time.Time { return time.Time{} })
|
||||
|
||||
var attempts int
|
||||
stubRemoveLogFile(t, func(path string) error {
|
||||
attempts++
|
||||
return &os.PathError{Op: "remove", Path: path, Err: os.ErrPermission}
|
||||
})
|
||||
|
||||
stats, err := cleanupOldLogs()
|
||||
if err == nil {
|
||||
t.Fatalf("cleanupOldLogs() expected error")
|
||||
}
|
||||
if !errors.Is(err, os.ErrPermission) {
|
||||
t.Fatalf("cleanupOldLogs error = %v, want permission", err)
|
||||
}
|
||||
|
||||
want := CleanupStats{Scanned: len(paths), Errors: len(paths)}
|
||||
if !compareCleanupStats(stats, want) {
|
||||
t.Fatalf("cleanup stats mismatch: got %+v, want %+v", stats, want)
|
||||
}
|
||||
|
||||
if attempts != len(paths) {
|
||||
t.Fatalf("expected %d attempts, got %d", len(paths), attempts)
|
||||
}
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("expected protected file %s to remain, err=%v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCleanupOldLogsHandlesPermissionDeniedFile(t *testing.T) {
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
protected := createTempLog(t, tempDir, "codex-wrapper-6200.log")
|
||||
deletable := createTempLog(t, tempDir, "codex-wrapper-6201.log")
|
||||
|
||||
stubProcessRunning(t, func(int) bool { return false })
|
||||
stubProcessStartTime(t, func(int) time.Time { return time.Time{} })
|
||||
|
||||
stubRemoveLogFile(t, func(path string) error {
|
||||
if path == protected {
|
||||
return &os.PathError{Op: "remove", Path: path, Err: os.ErrPermission}
|
||||
}
|
||||
return os.Remove(path)
|
||||
})
|
||||
|
||||
stats, err := cleanupOldLogs()
|
||||
if err == nil {
|
||||
t.Fatalf("cleanupOldLogs() expected error")
|
||||
}
|
||||
if !errors.Is(err, os.ErrPermission) {
|
||||
t.Fatalf("cleanupOldLogs error = %v, want permission", err)
|
||||
}
|
||||
|
||||
want := CleanupStats{Scanned: 2, Deleted: 1, Errors: 1}
|
||||
if !compareCleanupStats(stats, want) {
|
||||
t.Fatalf("cleanup stats mismatch: got %+v, want %+v", stats, want)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(protected); err != nil {
|
||||
t.Fatalf("expected protected file to remain, err=%v", err)
|
||||
}
|
||||
if _, err := os.Stat(deletable); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected deletable file to be removed, err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCleanupOldLogsPerformanceBound(t *testing.T) {
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
const fileCount = 400
|
||||
fakePaths := make([]string, fileCount)
|
||||
for i := 0; i < fileCount; i++ {
|
||||
name := fmt.Sprintf("codex-wrapper-%d.log", 10000+i)
|
||||
fakePaths[i] = createTempLog(t, tempDir, name)
|
||||
}
|
||||
|
||||
stubGlobLogFiles(t, func(pattern string) ([]string, error) {
|
||||
return fakePaths, nil
|
||||
})
|
||||
stubProcessRunning(t, func(int) bool { return false })
|
||||
stubProcessStartTime(t, func(int) time.Time { return time.Time{} })
|
||||
|
||||
var removed int
|
||||
stubRemoveLogFile(t, func(path string) error {
|
||||
removed++
|
||||
return nil
|
||||
})
|
||||
|
||||
start := time.Now()
|
||||
stats, err := cleanupOldLogs()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("cleanupOldLogs() unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if removed != fileCount {
|
||||
t.Fatalf("expected %d removals, got %d", fileCount, removed)
|
||||
}
|
||||
if elapsed > 100*time.Millisecond {
|
||||
t.Fatalf("cleanup took too long: %v for %d files", elapsed, fileCount)
|
||||
}
|
||||
|
||||
want := CleanupStats{Scanned: fileCount, Deleted: fileCount}
|
||||
if !compareCleanupStats(stats, want) {
|
||||
t.Fatalf("cleanup stats mismatch: got %+v, want %+v", stats, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCleanupOldLogsCoverageSuite(t *testing.T) {
|
||||
TestBackendParseJSONStream_CoverageSuite(t)
|
||||
}
|
||||
|
||||
// Reuse the existing coverage suite so the focused TestLogger run still exercises
|
||||
// the rest of the codebase and keeps coverage high.
|
||||
func TestRunLoggerCoverageSuite(t *testing.T) {
|
||||
TestBackendParseJSONStream_CoverageSuite(t)
|
||||
}
|
||||
|
||||
func TestRunCleanupOldLogsKeepsCurrentProcessLog(t *testing.T) {
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
currentPID := os.Getpid()
|
||||
currentLog := createTempLog(t, tempDir, fmt.Sprintf("codex-wrapper-%d.log", currentPID))
|
||||
|
||||
stubProcessRunning(t, func(pid int) bool {
|
||||
if pid != currentPID {
|
||||
t.Fatalf("unexpected pid check: %d", pid)
|
||||
}
|
||||
return true
|
||||
})
|
||||
stubProcessStartTime(t, func(pid int) time.Time {
|
||||
if pid == currentPID {
|
||||
return time.Now().Add(-1 * time.Hour)
|
||||
}
|
||||
return time.Time{}
|
||||
})
|
||||
|
||||
stats, err := cleanupOldLogs()
|
||||
if err != nil {
|
||||
t.Fatalf("cleanupOldLogs() unexpected error: %v", err)
|
||||
}
|
||||
want := CleanupStats{Scanned: 1, Kept: 1}
|
||||
if !compareCleanupStats(stats, want) {
|
||||
t.Fatalf("cleanup stats mismatch: got %+v, want %+v", stats, want)
|
||||
}
|
||||
if _, err := os.Stat(currentLog); err != nil {
|
||||
t.Fatalf("expected current process log to remain, err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPIDReusedScenarios(t *testing.T) {
|
||||
now := time.Now()
|
||||
tests := []struct {
|
||||
name string
|
||||
statErr error
|
||||
modTime time.Time
|
||||
startTime time.Time
|
||||
want bool
|
||||
}{
|
||||
{"stat error", errors.New("stat failed"), time.Time{}, time.Time{}, false},
|
||||
{"old file unknown start", nil, now.Add(-8 * 24 * time.Hour), time.Time{}, true},
|
||||
{"recent file unknown start", nil, now.Add(-2 * time.Hour), time.Time{}, false},
|
||||
{"pid reused", nil, now.Add(-2 * time.Hour), now.Add(-30 * time.Minute), true},
|
||||
{"pid active", nil, now.Add(-30 * time.Minute), now.Add(-2 * time.Hour), false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
stubFileStat(t, func(string) (os.FileInfo, error) {
|
||||
if tt.statErr != nil {
|
||||
return nil, tt.statErr
|
||||
}
|
||||
return fakeFileInfo{modTime: tt.modTime}, nil
|
||||
})
|
||||
stubProcessStartTime(t, func(int) time.Time {
|
||||
return tt.startTime
|
||||
})
|
||||
if got := isPIDReused("log", 1234); got != tt.want {
|
||||
t.Fatalf("isPIDReused() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsUnsafeFileSecurityChecks(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
absTempDir, err := filepath.Abs(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("filepath.Abs() error = %v", err)
|
||||
}
|
||||
|
||||
t.Run("symlink", func(t *testing.T) {
|
||||
stubFileStat(t, func(string) (os.FileInfo, error) {
|
||||
return fakeFileInfo{mode: os.ModeSymlink}, nil
|
||||
})
|
||||
stubEvalSymlinks(t, func(path string) (string, error) {
|
||||
return filepath.Join(absTempDir, filepath.Base(path)), nil
|
||||
})
|
||||
unsafe, reason := isUnsafeFile(filepath.Join(absTempDir, "codex-wrapper-1.log"), tempDir)
|
||||
if !unsafe || reason != "refusing to delete symlink" {
|
||||
t.Fatalf("expected symlink to be rejected, got unsafe=%v reason=%q", unsafe, reason)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("path traversal", func(t *testing.T) {
|
||||
stubFileStat(t, func(string) (os.FileInfo, error) {
|
||||
return fakeFileInfo{}, nil
|
||||
})
|
||||
outside := filepath.Join(filepath.Dir(absTempDir), "etc", "passwd")
|
||||
stubEvalSymlinks(t, func(string) (string, error) {
|
||||
return outside, nil
|
||||
})
|
||||
unsafe, reason := isUnsafeFile(filepath.Join("..", "..", "etc", "passwd"), tempDir)
|
||||
if !unsafe || reason != "file is outside tempDir" {
|
||||
t.Fatalf("expected traversal path to be rejected, got unsafe=%v reason=%q", unsafe, reason)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("outside temp dir", func(t *testing.T) {
|
||||
stubFileStat(t, func(string) (os.FileInfo, error) {
|
||||
return fakeFileInfo{}, nil
|
||||
})
|
||||
otherDir := t.TempDir()
|
||||
stubEvalSymlinks(t, func(string) (string, error) {
|
||||
return filepath.Join(otherDir, "codex-wrapper-9.log"), nil
|
||||
})
|
||||
unsafe, reason := isUnsafeFile(filepath.Join(otherDir, "codex-wrapper-9.log"), tempDir)
|
||||
if !unsafe || reason != "file is outside tempDir" {
|
||||
t.Fatalf("expected outside file to be rejected, got unsafe=%v reason=%q", unsafe, reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunLoggerPathAndRemove(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
path := filepath.Join(tempDir, "sample.log")
|
||||
if err := os.WriteFile(path, []byte("test"), 0o644); err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
|
||||
logger := &Logger{path: path}
|
||||
if got := logger.Path(); got != path {
|
||||
t.Fatalf("Path() = %q, want %q", got, path)
|
||||
}
|
||||
if err := logger.RemoveLogFile(); err != nil {
|
||||
t.Fatalf("RemoveLogFile() error = %v", err)
|
||||
}
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected log file to be removed, err=%v", err)
|
||||
}
|
||||
|
||||
var nilLogger *Logger
|
||||
if nilLogger.Path() != "" {
|
||||
t.Fatalf("nil logger Path() should be empty")
|
||||
}
|
||||
if err := nilLogger.RemoveLogFile(); err != nil {
|
||||
t.Fatalf("nil logger RemoveLogFile() should return nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLoggerInternalLog(t *testing.T) {
|
||||
logger := &Logger{
|
||||
ch: make(chan logEntry, 1),
|
||||
done: make(chan struct{}),
|
||||
pendingWG: sync.WaitGroup{},
|
||||
}
|
||||
|
||||
done := make(chan logEntry, 1)
|
||||
go func() {
|
||||
entry := <-logger.ch
|
||||
logger.pendingWG.Done()
|
||||
done <- entry
|
||||
}()
|
||||
|
||||
logger.log("INFO", "hello")
|
||||
entry := <-done
|
||||
if entry.level != "INFO" || entry.msg != "hello" {
|
||||
t.Fatalf("unexpected entry %+v", entry)
|
||||
}
|
||||
|
||||
logger.closed.Store(true)
|
||||
logger.log("INFO", "ignored")
|
||||
close(logger.done)
|
||||
}
|
||||
|
||||
func TestRunParsePIDFromLog(t *testing.T) {
|
||||
hugePID := strconv.FormatInt(math.MaxInt64, 10) + "0"
|
||||
tests := []struct {
|
||||
name string
|
||||
pid int
|
||||
ok bool
|
||||
}{
|
||||
{"codex-wrapper-123.log", 123, true},
|
||||
{"codex-wrapper-999-extra.log", 999, true},
|
||||
{"codex-wrapper-.log", 0, false},
|
||||
{"invalid-name.log", 0, false},
|
||||
{"codex-wrapper--5.log", 0, false},
|
||||
{"codex-wrapper-0.log", 0, false},
|
||||
{fmt.Sprintf("codex-wrapper-%s.log", hugePID), 0, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, ok := parsePIDFromLog(filepath.Join("/tmp", tt.name))
|
||||
if ok != tt.ok {
|
||||
t.Fatalf("parsePIDFromLog ok = %v, want %v", ok, tt.ok)
|
||||
}
|
||||
if ok && got != tt.pid {
|
||||
t.Fatalf("pid = %d, want %d", got, tt.pid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createTempLog(t *testing.T, dir, name string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(path, []byte("test"), 0o644); err != nil {
|
||||
t.Fatalf("failed to create temp log %s: %v", path, err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func setTempDirEnv(t *testing.T, dir string) string {
|
||||
t.Helper()
|
||||
resolved := dir
|
||||
if eval, err := filepath.EvalSymlinks(dir); err == nil {
|
||||
resolved = eval
|
||||
}
|
||||
t.Setenv("TMPDIR", resolved)
|
||||
t.Setenv("TEMP", resolved)
|
||||
t.Setenv("TMP", resolved)
|
||||
return resolved
|
||||
}
|
||||
|
||||
func stubProcessRunning(t *testing.T, fn func(int) bool) {
|
||||
t.Helper()
|
||||
original := processRunningCheck
|
||||
processRunningCheck = fn
|
||||
t.Cleanup(func() {
|
||||
processRunningCheck = original
|
||||
})
|
||||
}
|
||||
|
||||
func stubProcessStartTime(t *testing.T, fn func(int) time.Time) {
|
||||
t.Helper()
|
||||
original := processStartTimeFn
|
||||
processStartTimeFn = fn
|
||||
t.Cleanup(func() {
|
||||
processStartTimeFn = original
|
||||
})
|
||||
}
|
||||
|
||||
func stubRemoveLogFile(t *testing.T, fn func(string) error) {
|
||||
t.Helper()
|
||||
original := removeLogFileFn
|
||||
removeLogFileFn = fn
|
||||
t.Cleanup(func() {
|
||||
removeLogFileFn = original
|
||||
})
|
||||
}
|
||||
|
||||
func stubGlobLogFiles(t *testing.T, fn func(string) ([]string, error)) {
|
||||
t.Helper()
|
||||
original := globLogFiles
|
||||
globLogFiles = fn
|
||||
t.Cleanup(func() {
|
||||
globLogFiles = original
|
||||
})
|
||||
}
|
||||
|
||||
func stubFileStat(t *testing.T, fn func(string) (os.FileInfo, error)) {
|
||||
t.Helper()
|
||||
original := fileStatFn
|
||||
fileStatFn = fn
|
||||
t.Cleanup(func() {
|
||||
fileStatFn = original
|
||||
})
|
||||
}
|
||||
|
||||
func stubEvalSymlinks(t *testing.T, fn func(string) (string, error)) {
|
||||
t.Helper()
|
||||
original := evalSymlinksFn
|
||||
evalSymlinksFn = fn
|
||||
t.Cleanup(func() {
|
||||
evalSymlinksFn = original
|
||||
})
|
||||
}
|
||||
|
||||
type fakeFileInfo struct {
|
||||
modTime time.Time
|
||||
mode os.FileMode
|
||||
}
|
||||
|
||||
func (f fakeFileInfo) Name() string { return "fake" }
|
||||
func (f fakeFileInfo) Size() int64 { return 0 }
|
||||
func (f fakeFileInfo) Mode() os.FileMode { return f.mode }
|
||||
func (f fakeFileInfo) ModTime() time.Time { return f.modTime }
|
||||
func (f fakeFileInfo) IsDir() bool { return false }
|
||||
func (f fakeFileInfo) Sys() interface{} { return nil }
|
||||
|
||||
func TestExtractRecentErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
maxEntries int
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "empty log",
|
||||
content: "",
|
||||
maxEntries: 10,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "no errors",
|
||||
content: `[2025-01-01 12:00:00.000] [PID:123] INFO: started
|
||||
[2025-01-01 12:00:01.000] [PID:123] DEBUG: processing`,
|
||||
maxEntries: 10,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single error",
|
||||
content: `[2025-01-01 12:00:00.000] [PID:123] INFO: started
|
||||
[2025-01-01 12:00:01.000] [PID:123] ERROR: something failed`,
|
||||
maxEntries: 10,
|
||||
want: []string{"[2025-01-01 12:00:01.000] [PID:123] ERROR: something failed"},
|
||||
},
|
||||
{
|
||||
name: "error and warn",
|
||||
content: `[2025-01-01 12:00:00.000] [PID:123] INFO: started
|
||||
[2025-01-01 12:00:01.000] [PID:123] WARN: warning message
|
||||
[2025-01-01 12:00:02.000] [PID:123] ERROR: error message`,
|
||||
maxEntries: 10,
|
||||
want: []string{
|
||||
"[2025-01-01 12:00:01.000] [PID:123] WARN: warning message",
|
||||
"[2025-01-01 12:00:02.000] [PID:123] ERROR: error message",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "truncate to max",
|
||||
content: `[2025-01-01 12:00:00.000] [PID:123] ERROR: error 1
|
||||
[2025-01-01 12:00:01.000] [PID:123] ERROR: error 2
|
||||
[2025-01-01 12:00:02.000] [PID:123] ERROR: error 3
|
||||
[2025-01-01 12:00:03.000] [PID:123] ERROR: error 4
|
||||
[2025-01-01 12:00:04.000] [PID:123] ERROR: error 5`,
|
||||
maxEntries: 3,
|
||||
want: []string{
|
||||
"[2025-01-01 12:00:02.000] [PID:123] ERROR: error 3",
|
||||
"[2025-01-01 12:00:03.000] [PID:123] ERROR: error 4",
|
||||
"[2025-01-01 12:00:04.000] [PID:123] ERROR: error 5",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
logPath := filepath.Join(tempDir, "test.log")
|
||||
if err := os.WriteFile(logPath, []byte(tt.content), 0o644); err != nil {
|
||||
t.Fatalf("failed to write test log: %v", err)
|
||||
}
|
||||
|
||||
logger := &Logger{path: logPath}
|
||||
got := logger.ExtractRecentErrors(tt.maxEntries)
|
||||
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("ExtractRecentErrors() got %d entries, want %d", len(got), len(tt.want))
|
||||
}
|
||||
for i, entry := range got {
|
||||
if entry != tt.want[i] {
|
||||
t.Errorf("entry[%d] = %q, want %q", i, entry, tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractRecentErrorsNilLogger(t *testing.T) {
|
||||
var logger *Logger
|
||||
if got := logger.ExtractRecentErrors(10); got != nil {
|
||||
t.Fatalf("nil logger ExtractRecentErrors() should return nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractRecentErrorsEmptyPath(t *testing.T) {
|
||||
logger := &Logger{path: ""}
|
||||
if got := logger.ExtractRecentErrors(10); got != nil {
|
||||
t.Fatalf("empty path ExtractRecentErrors() should return nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractRecentErrorsFileNotExist(t *testing.T) {
|
||||
logger := &Logger{path: "/nonexistent/path/to/log.log"}
|
||||
if got := logger.ExtractRecentErrors(10); got != nil {
|
||||
t.Fatalf("nonexistent file ExtractRecentErrors() should return nil, got %v", got)
|
||||
}
|
||||
}
|
||||
469
codeagent-wrapper/main.go
Normal file
469
codeagent-wrapper/main.go
Normal file
@@ -0,0 +1,469 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
version = "5.2.3"
|
||||
defaultWorkdir = "."
|
||||
defaultTimeout = 7200 // seconds
|
||||
codexLogLineLimit = 1000
|
||||
stdinSpecialChars = "\n\\\"'`$"
|
||||
stderrCaptureLimit = 4 * 1024
|
||||
defaultBackendName = "codex"
|
||||
defaultCodexCommand = "codex"
|
||||
|
||||
// stdout close reasons
|
||||
stdoutCloseReasonWait = "wait-done"
|
||||
stdoutCloseReasonDrain = "drain-timeout"
|
||||
stdoutCloseReasonCtx = "context-cancel"
|
||||
stdoutDrainTimeout = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// Test hooks for dependency injection
|
||||
var (
|
||||
stdinReader io.Reader = os.Stdin
|
||||
isTerminalFn = defaultIsTerminal
|
||||
codexCommand = defaultCodexCommand
|
||||
cleanupHook func()
|
||||
loggerPtr atomic.Pointer[Logger]
|
||||
|
||||
buildCodexArgsFn = buildCodexArgs
|
||||
selectBackendFn = selectBackend
|
||||
commandContext = exec.CommandContext
|
||||
jsonMarshal = json.Marshal
|
||||
cleanupLogsFn = cleanupOldLogs
|
||||
signalNotifyFn = signal.Notify
|
||||
signalStopFn = signal.Stop
|
||||
terminateCommandFn = terminateCommand
|
||||
defaultBuildArgsFn = buildCodexArgs
|
||||
runTaskFn = runCodexTask
|
||||
exitFn = os.Exit
|
||||
)
|
||||
|
||||
var forceKillDelay atomic.Int32
|
||||
|
||||
func init() {
|
||||
forceKillDelay.Store(5) // seconds - default value
|
||||
}
|
||||
|
||||
func runStartupCleanup() {
|
||||
if cleanupLogsFn == nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logWarn(fmt.Sprintf("cleanupOldLogs panic: %v", r))
|
||||
}
|
||||
}()
|
||||
if _, err := cleanupLogsFn(); err != nil {
|
||||
logWarn(fmt.Sprintf("cleanupOldLogs error: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
func runCleanupMode() int {
|
||||
if cleanupLogsFn == nil {
|
||||
fmt.Fprintln(os.Stderr, "Cleanup failed: log cleanup function not configured")
|
||||
return 1
|
||||
}
|
||||
|
||||
stats, err := cleanupLogsFn()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cleanup failed: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
fmt.Println("Cleanup completed")
|
||||
fmt.Printf("Files scanned: %d\n", stats.Scanned)
|
||||
fmt.Printf("Files deleted: %d\n", stats.Deleted)
|
||||
if len(stats.DeletedFiles) > 0 {
|
||||
for _, f := range stats.DeletedFiles {
|
||||
fmt.Printf(" - %s\n", f)
|
||||
}
|
||||
}
|
||||
fmt.Printf("Files kept: %d\n", stats.Kept)
|
||||
if len(stats.KeptFiles) > 0 {
|
||||
for _, f := range stats.KeptFiles {
|
||||
fmt.Printf(" - %s\n", f)
|
||||
}
|
||||
}
|
||||
if stats.Errors > 0 {
|
||||
fmt.Printf("Deletion errors: %d\n", stats.Errors)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func main() {
|
||||
exitCode := run()
|
||||
exitFn(exitCode)
|
||||
}
|
||||
|
||||
// run is the main logic, returns exit code for testability
|
||||
func run() (exitCode int) {
|
||||
name := currentWrapperName()
|
||||
// Handle --version and --help first (no logger needed)
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "--version", "-v":
|
||||
fmt.Printf("%s version %s\n", name, version)
|
||||
return 0
|
||||
case "--help", "-h":
|
||||
printHelp()
|
||||
return 0
|
||||
case "--cleanup":
|
||||
return runCleanupMode()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize logger for all other commands
|
||||
logger, err := NewLogger()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: failed to initialize logger: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
setLogger(logger)
|
||||
|
||||
defer func() {
|
||||
logger := activeLogger()
|
||||
if logger != nil {
|
||||
logger.Flush()
|
||||
}
|
||||
if err := closeLogger(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: failed to close logger: %v\n", err)
|
||||
}
|
||||
// On failure, extract and display recent errors before removing log
|
||||
if logger != nil {
|
||||
if exitCode != 0 {
|
||||
if errors := logger.ExtractRecentErrors(10); len(errors) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "\n=== Recent Errors ===")
|
||||
for _, entry := range errors {
|
||||
fmt.Fprintln(os.Stderr, entry)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Log file: %s (deleted)\n", logger.Path())
|
||||
}
|
||||
}
|
||||
if err := logger.RemoveLogFile(); err != nil && !os.IsNotExist(err) {
|
||||
// Silently ignore removal errors
|
||||
}
|
||||
}
|
||||
}()
|
||||
defer runCleanupHook()
|
||||
|
||||
// Clean up stale logs from previous runs.
|
||||
runStartupCleanup()
|
||||
|
||||
// Handle remaining commands
|
||||
if len(os.Args) > 1 {
|
||||
args := os.Args[1:]
|
||||
parallelIndex := -1
|
||||
for i, arg := range args {
|
||||
if arg == "--parallel" {
|
||||
parallelIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if parallelIndex != -1 {
|
||||
backendName := defaultBackendName
|
||||
var extras []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--parallel":
|
||||
continue
|
||||
case arg == "--backend":
|
||||
if i+1 >= len(args) {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: --backend flag requires a value")
|
||||
return 1
|
||||
}
|
||||
backendName = args[i+1]
|
||||
i++
|
||||
case strings.HasPrefix(arg, "--backend="):
|
||||
value := strings.TrimPrefix(arg, "--backend=")
|
||||
if value == "" {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: --backend flag requires a value")
|
||||
return 1
|
||||
}
|
||||
backendName = value
|
||||
default:
|
||||
extras = append(extras, arg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(extras) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: --parallel reads its task configuration from stdin; only --backend is 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)
|
||||
return 1
|
||||
}
|
||||
|
||||
backend, err := selectBackendFn(backendName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
backendName = backend.Name()
|
||||
|
||||
data, err := io.ReadAll(stdinReader)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: failed to read stdin: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
cfg, err := parseParallelConfig(data)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
cfg.GlobalBackend = backendName
|
||||
for i := range cfg.Tasks {
|
||||
if strings.TrimSpace(cfg.Tasks[i].Backend) == "" {
|
||||
cfg.Tasks[i].Backend = backendName
|
||||
}
|
||||
}
|
||||
|
||||
timeoutSec := resolveTimeout()
|
||||
layers, err := topologicalSort(cfg.Tasks)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
results := executeConcurrent(layers, timeoutSec)
|
||||
fmt.Println(generateFinalOutput(results))
|
||||
|
||||
exitCode = 0
|
||||
for _, res := range results {
|
||||
if res.ExitCode != 0 {
|
||||
exitCode = res.ExitCode
|
||||
}
|
||||
}
|
||||
|
||||
return exitCode
|
||||
}
|
||||
}
|
||||
|
||||
logInfo("Script started")
|
||||
|
||||
cfg, err := parseArgs()
|
||||
if err != nil {
|
||||
logError(err.Error())
|
||||
return 1
|
||||
}
|
||||
logInfo(fmt.Sprintf("Parsed args: mode=%s, task_len=%d, backend=%s", cfg.Mode, len(cfg.Task), cfg.Backend))
|
||||
|
||||
backend, err := selectBackendFn(cfg.Backend)
|
||||
if err != nil {
|
||||
logError(err.Error())
|
||||
return 1
|
||||
}
|
||||
cfg.Backend = backend.Name()
|
||||
|
||||
cmdInjected := codexCommand != defaultCodexCommand
|
||||
argsInjected := buildCodexArgsFn != nil && reflect.ValueOf(buildCodexArgsFn).Pointer() != reflect.ValueOf(defaultBuildArgsFn).Pointer()
|
||||
|
||||
// Wire selected backend into runtime hooks for the rest of the execution,
|
||||
// but preserve any injected test hooks for the default backend.
|
||||
if backend.Name() != defaultBackendName || !cmdInjected {
|
||||
codexCommand = backend.Command()
|
||||
}
|
||||
if backend.Name() != defaultBackendName || !argsInjected {
|
||||
buildCodexArgsFn = backend.BuildArgs
|
||||
}
|
||||
logInfo(fmt.Sprintf("Selected backend: %s", backend.Name()))
|
||||
|
||||
timeoutSec := resolveTimeout()
|
||||
logInfo(fmt.Sprintf("Timeout: %ds", timeoutSec))
|
||||
cfg.Timeout = timeoutSec
|
||||
|
||||
var taskText string
|
||||
var piped bool
|
||||
|
||||
if cfg.ExplicitStdin {
|
||||
logInfo("Explicit stdin mode: reading task from stdin")
|
||||
data, err := io.ReadAll(stdinReader)
|
||||
if err != nil {
|
||||
logError("Failed to read stdin: " + err.Error())
|
||||
return 1
|
||||
}
|
||||
taskText = string(data)
|
||||
if taskText == "" {
|
||||
logError("Explicit stdin mode requires task input from stdin")
|
||||
return 1
|
||||
}
|
||||
piped = !isTerminal()
|
||||
} else {
|
||||
pipedTask, err := readPipedTask()
|
||||
if err != nil {
|
||||
logError("Failed to read piped stdin: " + err.Error())
|
||||
return 1
|
||||
}
|
||||
piped = pipedTask != ""
|
||||
if piped {
|
||||
taskText = pipedTask
|
||||
} else {
|
||||
taskText = cfg.Task
|
||||
}
|
||||
}
|
||||
|
||||
useStdin := cfg.ExplicitStdin || shouldUseStdin(taskText, piped)
|
||||
|
||||
targetArg := taskText
|
||||
if useStdin {
|
||||
targetArg = "-"
|
||||
}
|
||||
codexArgs := buildCodexArgsFn(cfg, targetArg)
|
||||
|
||||
// Print startup information to stderr
|
||||
fmt.Fprintf(os.Stderr, "[%s]\n", name)
|
||||
fmt.Fprintf(os.Stderr, " Backend: %s\n", cfg.Backend)
|
||||
fmt.Fprintf(os.Stderr, " Command: %s %s\n", codexCommand, strings.Join(codexArgs, " "))
|
||||
fmt.Fprintf(os.Stderr, " PID: %d\n", os.Getpid())
|
||||
fmt.Fprintf(os.Stderr, " Log: %s\n", logger.Path())
|
||||
|
||||
if useStdin {
|
||||
var reasons []string
|
||||
if piped {
|
||||
reasons = append(reasons, "piped input")
|
||||
}
|
||||
if cfg.ExplicitStdin {
|
||||
reasons = append(reasons, "explicit \"-\"")
|
||||
}
|
||||
if strings.Contains(taskText, "\n") {
|
||||
reasons = append(reasons, "newline")
|
||||
}
|
||||
if strings.Contains(taskText, "\\") {
|
||||
reasons = append(reasons, "backslash")
|
||||
}
|
||||
if strings.Contains(taskText, "\"") {
|
||||
reasons = append(reasons, "double-quote")
|
||||
}
|
||||
if strings.Contains(taskText, "'") {
|
||||
reasons = append(reasons, "single-quote")
|
||||
}
|
||||
if strings.Contains(taskText, "`") {
|
||||
reasons = append(reasons, "backtick")
|
||||
}
|
||||
if strings.Contains(taskText, "$") {
|
||||
reasons = append(reasons, "dollar")
|
||||
}
|
||||
if len(taskText) > 800 {
|
||||
reasons = append(reasons, "length>800")
|
||||
}
|
||||
if len(reasons) > 0 {
|
||||
logWarn(fmt.Sprintf("Using stdin mode for task due to: %s", strings.Join(reasons, ", ")))
|
||||
}
|
||||
}
|
||||
|
||||
logInfo(fmt.Sprintf("%s running...", cfg.Backend))
|
||||
|
||||
taskSpec := TaskSpec{
|
||||
Task: taskText,
|
||||
WorkDir: cfg.WorkDir,
|
||||
Mode: cfg.Mode,
|
||||
SessionID: cfg.SessionID,
|
||||
UseStdin: useStdin,
|
||||
}
|
||||
|
||||
result := runTaskFn(taskSpec, false, cfg.Timeout)
|
||||
|
||||
if result.ExitCode != 0 {
|
||||
return result.ExitCode
|
||||
}
|
||||
|
||||
fmt.Println(result.Message)
|
||||
if result.SessionID != "" {
|
||||
fmt.Printf("\n---\nSESSION_ID: %s\n", result.SessionID)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func setLogger(l *Logger) {
|
||||
loggerPtr.Store(l)
|
||||
}
|
||||
|
||||
func closeLogger() error {
|
||||
logger := loggerPtr.Swap(nil)
|
||||
if logger == nil {
|
||||
return nil
|
||||
}
|
||||
return logger.Close()
|
||||
}
|
||||
|
||||
func activeLogger() *Logger {
|
||||
return loggerPtr.Load()
|
||||
}
|
||||
|
||||
func logInfo(msg string) {
|
||||
if logger := activeLogger(); logger != nil {
|
||||
logger.Info(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func logWarn(msg string) {
|
||||
if logger := activeLogger(); logger != nil {
|
||||
logger.Warn(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func logError(msg string) {
|
||||
if logger := activeLogger(); logger != nil {
|
||||
logger.Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func runCleanupHook() {
|
||||
if logger := activeLogger(); logger != nil {
|
||||
logger.Flush()
|
||||
}
|
||||
if cleanupHook != nil {
|
||||
cleanupHook()
|
||||
}
|
||||
}
|
||||
|
||||
func printHelp() {
|
||||
name := currentWrapperName()
|
||||
help := fmt.Sprintf(`%[1]s - Go wrapper for AI CLI backends
|
||||
|
||||
Usage:
|
||||
%[1]s "task" [workdir]
|
||||
%[1]s --backend claude "task" [workdir]
|
||||
%[1]s - [workdir] Read task from stdin
|
||||
%[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 --version
|
||||
%[1]s --help
|
||||
|
||||
Parallel mode examples:
|
||||
%[1]s --parallel < tasks.txt
|
||||
echo '...' | %[1]s --parallel
|
||||
%[1]s --parallel <<'EOF'
|
||||
|
||||
Environment Variables:
|
||||
CODEX_TIMEOUT Timeout in milliseconds (default: 7200000)
|
||||
|
||||
Exit Codes:
|
||||
0 Success
|
||||
1 General error (missing args, no output)
|
||||
124 Timeout
|
||||
127 backend command not found
|
||||
130 Interrupted (Ctrl+C)
|
||||
* Passthrough from backend process`, name)
|
||||
fmt.Println(help)
|
||||
}
|
||||
850
codeagent-wrapper/main_integration_test.go
Normal file
850
codeagent-wrapper/main_integration_test.go
Normal file
@@ -0,0 +1,850 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type integrationSummary struct {
|
||||
Total int `json:"total"`
|
||||
Success int `json:"success"`
|
||||
Failed int `json:"failed"`
|
||||
}
|
||||
|
||||
type integrationOutput struct {
|
||||
Results []TaskResult `json:"results"`
|
||||
Summary integrationSummary `json:"summary"`
|
||||
}
|
||||
|
||||
func captureStdout(t *testing.T, fn func()) string {
|
||||
t.Helper()
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
fn()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func parseIntegrationOutput(t *testing.T, out string) integrationOutput {
|
||||
t.Helper()
|
||||
var payload integrationOutput
|
||||
|
||||
lines := strings.Split(out, "\n")
|
||||
var currentTask *TaskResult
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Total:") {
|
||||
parts := strings.Split(line, "|")
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if strings.HasPrefix(p, "Total:") {
|
||||
fmt.Sscanf(p, "Total: %d", &payload.Summary.Total)
|
||||
} else if strings.HasPrefix(p, "Success:") {
|
||||
fmt.Sscanf(p, "Success: %d", &payload.Summary.Success)
|
||||
} else if strings.HasPrefix(p, "Failed:") {
|
||||
fmt.Sscanf(p, "Failed: %d", &payload.Summary.Failed)
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(line, "--- Task:") {
|
||||
if currentTask != nil {
|
||||
payload.Results = append(payload.Results, *currentTask)
|
||||
}
|
||||
currentTask = &TaskResult{}
|
||||
currentTask.TaskID = strings.TrimSuffix(strings.TrimPrefix(line, "--- Task: "), " ---")
|
||||
} else if currentTask != nil {
|
||||
if strings.HasPrefix(line, "Status: SUCCESS") {
|
||||
currentTask.ExitCode = 0
|
||||
} else if strings.HasPrefix(line, "Status: FAILED") {
|
||||
if strings.Contains(line, "exit code") {
|
||||
fmt.Sscanf(line, "Status: FAILED (exit code %d)", ¤tTask.ExitCode)
|
||||
} else {
|
||||
currentTask.ExitCode = 1
|
||||
}
|
||||
} else if strings.HasPrefix(line, "Error:") {
|
||||
currentTask.Error = strings.TrimPrefix(line, "Error: ")
|
||||
} else if strings.HasPrefix(line, "Session:") {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentTask != nil {
|
||||
payload.Results = append(payload.Results, *currentTask)
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
func extractTaskBlock(t *testing.T, output, taskID string) string {
|
||||
t.Helper()
|
||||
header := fmt.Sprintf("--- Task: %s ---", taskID)
|
||||
lines := strings.Split(output, "\n")
|
||||
var block []string
|
||||
collecting := false
|
||||
for _, raw := range lines {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if !collecting {
|
||||
if trimmed == header {
|
||||
collecting = true
|
||||
block = append(block, trimmed)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "--- Task: ") && trimmed != header {
|
||||
break
|
||||
}
|
||||
block = append(block, trimmed)
|
||||
}
|
||||
if len(block) == 0 {
|
||||
t.Fatalf("task block %s not found in output:\n%s", taskID, output)
|
||||
}
|
||||
return strings.Join(block, "\n")
|
||||
}
|
||||
|
||||
func findResultByID(t *testing.T, payload integrationOutput, id string) TaskResult {
|
||||
t.Helper()
|
||||
for _, res := range payload.Results {
|
||||
if res.TaskID == id {
|
||||
return res
|
||||
}
|
||||
}
|
||||
t.Fatalf("result for task %s not found", id)
|
||||
return TaskResult{}
|
||||
}
|
||||
|
||||
func TestRunParallelEndToEnd_OrderAndConcurrency(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
origRun := runCodexTaskFn
|
||||
t.Cleanup(func() {
|
||||
runCodexTaskFn = origRun
|
||||
resetTestHooks()
|
||||
})
|
||||
|
||||
input := `---TASK---
|
||||
id: A
|
||||
---CONTENT---
|
||||
task-a
|
||||
---TASK---
|
||||
id: B
|
||||
dependencies: A
|
||||
---CONTENT---
|
||||
task-b
|
||||
---TASK---
|
||||
id: C
|
||||
dependencies: B
|
||||
---CONTENT---
|
||||
task-c
|
||||
---TASK---
|
||||
id: D
|
||||
---CONTENT---
|
||||
task-d
|
||||
---TASK---
|
||||
id: E
|
||||
---CONTENT---
|
||||
task-e`
|
||||
stdinReader = bytes.NewReader([]byte(input))
|
||||
os.Args = []string{"codeagent-wrapper", "--parallel"}
|
||||
|
||||
var mu sync.Mutex
|
||||
starts := make(map[string]time.Time)
|
||||
ends := make(map[string]time.Time)
|
||||
var running int64
|
||||
var maxParallel int64
|
||||
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
start := time.Now()
|
||||
mu.Lock()
|
||||
starts[task.ID] = start
|
||||
mu.Unlock()
|
||||
|
||||
cur := atomic.AddInt64(&running, 1)
|
||||
for {
|
||||
prev := atomic.LoadInt64(&maxParallel)
|
||||
if cur <= prev {
|
||||
break
|
||||
}
|
||||
if atomic.CompareAndSwapInt64(&maxParallel, prev, cur) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
|
||||
mu.Lock()
|
||||
ends[task.ID] = time.Now()
|
||||
mu.Unlock()
|
||||
|
||||
atomic.AddInt64(&running, -1)
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 0, Message: task.Task}
|
||||
}
|
||||
|
||||
var exitCode int
|
||||
output := captureStdout(t, func() {
|
||||
exitCode = run()
|
||||
})
|
||||
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("run() exit = %d, want 0", exitCode)
|
||||
}
|
||||
|
||||
payload := parseIntegrationOutput(t, output)
|
||||
if payload.Summary.Failed != 0 || payload.Summary.Total != 5 || payload.Summary.Success != 5 {
|
||||
t.Fatalf("unexpected summary: %+v", payload.Summary)
|
||||
}
|
||||
|
||||
aEnd := ends["A"]
|
||||
bStart := starts["B"]
|
||||
cStart := starts["C"]
|
||||
bEnd := ends["B"]
|
||||
if aEnd.IsZero() || bStart.IsZero() || bEnd.IsZero() || cStart.IsZero() {
|
||||
t.Fatalf("missing timestamps, starts=%v ends=%v", starts, ends)
|
||||
}
|
||||
if !aEnd.Before(bStart) && !aEnd.Equal(bStart) {
|
||||
t.Fatalf("B should start after A ends: A_end=%v B_start=%v", aEnd, bStart)
|
||||
}
|
||||
if !bEnd.Before(cStart) && !bEnd.Equal(cStart) {
|
||||
t.Fatalf("C should start after B ends: B_end=%v C_start=%v", bEnd, cStart)
|
||||
}
|
||||
|
||||
dStart := starts["D"]
|
||||
eStart := starts["E"]
|
||||
if dStart.IsZero() || eStart.IsZero() {
|
||||
t.Fatalf("missing D/E start times: %v", starts)
|
||||
}
|
||||
delta := dStart.Sub(eStart)
|
||||
if delta < 0 {
|
||||
delta = -delta
|
||||
}
|
||||
if delta > 25*time.Millisecond {
|
||||
t.Fatalf("D and E should run in parallel, delta=%v", delta)
|
||||
}
|
||||
if maxParallel < 2 {
|
||||
t.Fatalf("expected at least 2 concurrent tasks, got %d", maxParallel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunParallelCycleDetectionStopsExecution(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
origRun := runCodexTaskFn
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
t.Fatalf("task %s should not execute on cycle", task.ID)
|
||||
return TaskResult{}
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
runCodexTaskFn = origRun
|
||||
resetTestHooks()
|
||||
})
|
||||
|
||||
input := `---TASK---
|
||||
id: A
|
||||
dependencies: B
|
||||
---CONTENT---
|
||||
a
|
||||
---TASK---
|
||||
id: B
|
||||
dependencies: A
|
||||
---CONTENT---
|
||||
b`
|
||||
stdinReader = bytes.NewReader([]byte(input))
|
||||
os.Args = []string{"codeagent-wrapper", "--parallel"}
|
||||
|
||||
exitCode := 0
|
||||
output := captureStdout(t, func() {
|
||||
exitCode = run()
|
||||
})
|
||||
|
||||
if exitCode == 0 {
|
||||
t.Fatalf("cycle should cause non-zero exit, got %d", exitCode)
|
||||
}
|
||||
if strings.TrimSpace(output) != "" {
|
||||
t.Fatalf("expected no JSON output on cycle, got %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunParallelOutputsIncludeLogPaths(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
origRun := runCodexTaskFn
|
||||
t.Cleanup(func() {
|
||||
runCodexTaskFn = origRun
|
||||
resetTestHooks()
|
||||
})
|
||||
|
||||
tempDir := t.TempDir()
|
||||
logPathFor := func(id string) string {
|
||||
return filepath.Join(tempDir, fmt.Sprintf("%s.log", id))
|
||||
}
|
||||
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
res := TaskResult{
|
||||
TaskID: task.ID,
|
||||
Message: fmt.Sprintf("result-%s", task.ID),
|
||||
SessionID: fmt.Sprintf("session-%s", task.ID),
|
||||
LogPath: logPathFor(task.ID),
|
||||
}
|
||||
if task.ID == "beta" {
|
||||
res.ExitCode = 9
|
||||
res.Error = "boom"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
input := `---TASK---
|
||||
id: alpha
|
||||
---CONTENT---
|
||||
task-alpha
|
||||
---TASK---
|
||||
id: beta
|
||||
---CONTENT---
|
||||
task-beta`
|
||||
stdinReader = bytes.NewReader([]byte(input))
|
||||
os.Args = []string{"codex-wrapper", "--parallel"}
|
||||
|
||||
var exitCode int
|
||||
output := captureStdout(t, func() {
|
||||
exitCode = run()
|
||||
})
|
||||
|
||||
if exitCode != 9 {
|
||||
t.Fatalf("parallel run exit=%d, want 9", exitCode)
|
||||
}
|
||||
|
||||
payload := parseIntegrationOutput(t, output)
|
||||
alpha := findResultByID(t, payload, "alpha")
|
||||
beta := findResultByID(t, payload, "beta")
|
||||
|
||||
if alpha.LogPath != logPathFor("alpha") {
|
||||
t.Fatalf("alpha log path = %q, want %q", alpha.LogPath, logPathFor("alpha"))
|
||||
}
|
||||
if beta.LogPath != logPathFor("beta") {
|
||||
t.Fatalf("beta log path = %q, want %q", beta.LogPath, logPathFor("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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunParallelStartupLogsPrinted(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
input := `---TASK---
|
||||
id: a
|
||||
---CONTENT---
|
||||
fail
|
||||
---TASK---
|
||||
id: b
|
||||
---CONTENT---
|
||||
ok-b
|
||||
---TASK---
|
||||
id: c
|
||||
dependencies: a
|
||||
---CONTENT---
|
||||
should-skip
|
||||
---TASK---
|
||||
id: d
|
||||
---CONTENT---
|
||||
ok-d`
|
||||
stdinReader = bytes.NewReader([]byte(input))
|
||||
os.Args = []string{"codex-wrapper", "--parallel"}
|
||||
|
||||
expectedLog := filepath.Join(tempDir, fmt.Sprintf("codex-wrapper-%d.log", os.Getpid()))
|
||||
|
||||
origRun := runCodexTaskFn
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
path := expectedLog
|
||||
if logger := activeLogger(); logger != nil && logger.Path() != "" {
|
||||
path = logger.Path()
|
||||
}
|
||||
if task.ID == "a" {
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 3, Error: "boom", LogPath: path}
|
||||
}
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 0, Message: task.Task, LogPath: path}
|
||||
}
|
||||
t.Cleanup(func() { runCodexTaskFn = origRun })
|
||||
|
||||
var exitCode int
|
||||
var stdoutOut string
|
||||
stderrOut := captureStderr(t, func() {
|
||||
stdoutOut = captureStdout(t, func() {
|
||||
exitCode = run()
|
||||
})
|
||||
})
|
||||
|
||||
if exitCode == 0 {
|
||||
t.Fatalf("expected non-zero exit due to task failure, got %d", exitCode)
|
||||
}
|
||||
if stdoutOut == "" {
|
||||
t.Fatalf("expected parallel summary on stdout")
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(stderrOut), "\n")
|
||||
var bannerSeen bool
|
||||
var taskLines []string
|
||||
for _, raw := range lines {
|
||||
line := strings.TrimSpace(raw)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if line == "=== Starting Parallel Execution ===" {
|
||||
if bannerSeen {
|
||||
t.Fatalf("banner printed multiple times:\n%s", stderrOut)
|
||||
}
|
||||
bannerSeen = true
|
||||
continue
|
||||
}
|
||||
taskLines = append(taskLines, line)
|
||||
}
|
||||
|
||||
if !bannerSeen {
|
||||
t.Fatalf("expected startup banner in stderr, got:\n%s", stderrOut)
|
||||
}
|
||||
|
||||
expectedLines := map[string]struct{}{
|
||||
fmt.Sprintf("Task a: Log: %s", expectedLog): {},
|
||||
fmt.Sprintf("Task b: Log: %s", expectedLog): {},
|
||||
fmt.Sprintf("Task d: Log: %s", expectedLog): {},
|
||||
}
|
||||
|
||||
if len(taskLines) != len(expectedLines) {
|
||||
t.Fatalf("startup log lines mismatch, got %d lines:\n%s", len(taskLines), stderrOut)
|
||||
}
|
||||
|
||||
for _, line := range taskLines {
|
||||
if _, ok := expectedLines[line]; !ok {
|
||||
t.Fatalf("unexpected startup line %q\nstderr:\n%s", line, stderrOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunNonParallelOutputsIncludeLogPathsIntegration(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
os.Args = []string{"codex-wrapper", "integration-log-check"}
|
||||
stdinReader = strings.NewReader("")
|
||||
isTerminalFn = func() bool { return true }
|
||||
codexCommand = "echo"
|
||||
buildCodexArgsFn = func(cfg *Config, targetArg string) []string {
|
||||
return []string{`{"type":"thread.started","thread_id":"integration-session"}` + "\n" + `{"type":"item.completed","item":{"type":"agent_message","text":"done"}}`}
|
||||
}
|
||||
|
||||
var exitCode int
|
||||
stderr := captureStderr(t, func() {
|
||||
_ = captureStdout(t, func() {
|
||||
exitCode = run()
|
||||
})
|
||||
})
|
||||
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("run() exit=%d, want 0", exitCode)
|
||||
}
|
||||
expectedLog := filepath.Join(tempDir, fmt.Sprintf("codex-wrapper-%d.log", os.Getpid()))
|
||||
wantLine := fmt.Sprintf("Log: %s", expectedLog)
|
||||
if !strings.Contains(stderr, wantLine) {
|
||||
t.Fatalf("stderr missing %q, got: %q", wantLine, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunParallelPartialFailureBlocksDependents(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
origRun := runCodexTaskFn
|
||||
t.Cleanup(func() {
|
||||
runCodexTaskFn = origRun
|
||||
resetTestHooks()
|
||||
})
|
||||
|
||||
tempDir := t.TempDir()
|
||||
logPathFor := func(id string) string {
|
||||
return filepath.Join(tempDir, fmt.Sprintf("%s.log", id))
|
||||
}
|
||||
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
path := logPathFor(task.ID)
|
||||
if task.ID == "A" {
|
||||
return TaskResult{TaskID: "A", ExitCode: 2, Error: "boom", LogPath: path}
|
||||
}
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 0, Message: task.Task, LogPath: path}
|
||||
}
|
||||
|
||||
input := `---TASK---
|
||||
id: A
|
||||
---CONTENT---
|
||||
fail
|
||||
---TASK---
|
||||
id: B
|
||||
dependencies: A
|
||||
---CONTENT---
|
||||
blocked
|
||||
---TASK---
|
||||
id: D
|
||||
---CONTENT---
|
||||
ok-d
|
||||
---TASK---
|
||||
id: E
|
||||
---CONTENT---
|
||||
ok-e`
|
||||
stdinReader = bytes.NewReader([]byte(input))
|
||||
os.Args = []string{"codeagent-wrapper", "--parallel"}
|
||||
|
||||
var exitCode int
|
||||
output := captureStdout(t, func() {
|
||||
exitCode = run()
|
||||
})
|
||||
|
||||
payload := parseIntegrationOutput(t, output)
|
||||
if exitCode == 0 {
|
||||
t.Fatalf("expected non-zero exit when a task fails, got %d", exitCode)
|
||||
}
|
||||
|
||||
resA := findResultByID(t, payload, "A")
|
||||
resB := findResultByID(t, payload, "B")
|
||||
resD := findResultByID(t, payload, "D")
|
||||
resE := findResultByID(t, payload, "E")
|
||||
|
||||
if resA.ExitCode == 0 {
|
||||
t.Fatalf("task A should fail, got %+v", resA)
|
||||
}
|
||||
if resB.ExitCode == 0 || !strings.Contains(resB.Error, "dependencies") {
|
||||
t.Fatalf("task B should be skipped due to dependency failure, got %+v", resB)
|
||||
}
|
||||
if resD.ExitCode != 0 || resE.ExitCode != 0 {
|
||||
t.Fatalf("independent tasks should run successfully, D=%+v E=%+v", resD, resE)
|
||||
}
|
||||
if payload.Summary.Failed != 2 || payload.Summary.Total != 4 {
|
||||
t.Fatalf("unexpected summary after partial failure: %+v", payload.Summary)
|
||||
}
|
||||
if resA.LogPath != logPathFor("A") {
|
||||
t.Fatalf("task A log path = %q, want %q", resA.LogPath, logPathFor("A"))
|
||||
}
|
||||
if resB.LogPath != "" {
|
||||
t.Fatalf("task B should not report a log path when skipped, got %q", resB.LogPath)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
blockB := extractTaskBlock(t, output, "B")
|
||||
if strings.Contains(blockB, "Log:") {
|
||||
t.Fatalf("skipped task B should not emit a log line:\n%s", blockB)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunParallelTimeoutPropagation(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
origRun := runCodexTaskFn
|
||||
t.Cleanup(func() {
|
||||
runCodexTaskFn = origRun
|
||||
resetTestHooks()
|
||||
os.Unsetenv("CODEX_TIMEOUT")
|
||||
})
|
||||
|
||||
var receivedTimeout int
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
receivedTimeout = timeout
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 124, Error: "timeout"}
|
||||
}
|
||||
|
||||
os.Setenv("CODEX_TIMEOUT", "1")
|
||||
input := `---TASK---
|
||||
id: T
|
||||
---CONTENT---
|
||||
slow`
|
||||
stdinReader = bytes.NewReader([]byte(input))
|
||||
os.Args = []string{"codeagent-wrapper", "--parallel"}
|
||||
|
||||
exitCode := 0
|
||||
output := captureStdout(t, func() {
|
||||
exitCode = run()
|
||||
})
|
||||
|
||||
payload := parseIntegrationOutput(t, output)
|
||||
if receivedTimeout != 1 {
|
||||
t.Fatalf("expected timeout 1s to propagate, got %d", receivedTimeout)
|
||||
}
|
||||
if exitCode != 124 {
|
||||
t.Fatalf("expected timeout exit code 124, got %d", exitCode)
|
||||
}
|
||||
if payload.Summary.Failed != 1 || payload.Summary.Total != 1 {
|
||||
t.Fatalf("unexpected summary for timeout case: %+v", payload.Summary)
|
||||
}
|
||||
res := findResultByID(t, payload, "T")
|
||||
if res.Error == "" || res.ExitCode != 124 {
|
||||
t.Fatalf("timeout result not propagated, got %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConcurrentSpeedupBenchmark(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
origRun := runCodexTaskFn
|
||||
t.Cleanup(func() {
|
||||
runCodexTaskFn = origRun
|
||||
resetTestHooks()
|
||||
})
|
||||
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return TaskResult{TaskID: task.ID}
|
||||
}
|
||||
|
||||
tasks := make([]TaskSpec, 10)
|
||||
for i := range tasks {
|
||||
tasks[i] = TaskSpec{ID: fmt.Sprintf("task-%d", i)}
|
||||
}
|
||||
layers := [][]TaskSpec{tasks}
|
||||
|
||||
serialStart := time.Now()
|
||||
for _, task := range tasks {
|
||||
_ = runCodexTaskFn(task, 5)
|
||||
}
|
||||
serialElapsed := time.Since(serialStart)
|
||||
|
||||
concurrentStart := time.Now()
|
||||
_ = executeConcurrent(layers, 5)
|
||||
concurrentElapsed := time.Since(concurrentStart)
|
||||
|
||||
if concurrentElapsed >= serialElapsed/5 {
|
||||
t.Fatalf("expected concurrent time <20%% of serial, serial=%v concurrent=%v", serialElapsed, concurrentElapsed)
|
||||
}
|
||||
ratio := float64(concurrentElapsed) / float64(serialElapsed)
|
||||
t.Logf("speedup ratio (concurrent/serial)=%.3f", ratio)
|
||||
}
|
||||
|
||||
func TestRunStartupCleanupRemovesOrphansEndToEnd(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
orphanA := createTempLog(t, tempDir, "codex-wrapper-5001.log")
|
||||
orphanB := createTempLog(t, tempDir, "codex-wrapper-5002-extra.log")
|
||||
orphanC := createTempLog(t, tempDir, "codex-wrapper-5003-suffix.log")
|
||||
runningPID := 81234
|
||||
runningLog := createTempLog(t, tempDir, fmt.Sprintf("codex-wrapper-%d.log", runningPID))
|
||||
unrelated := createTempLog(t, tempDir, "wrapper.log")
|
||||
|
||||
stubProcessRunning(t, func(pid int) bool {
|
||||
return pid == runningPID || pid == os.Getpid()
|
||||
})
|
||||
stubProcessStartTime(t, func(pid int) time.Time {
|
||||
if pid == runningPID || pid == os.Getpid() {
|
||||
return time.Now().Add(-1 * time.Hour)
|
||||
}
|
||||
return time.Time{}
|
||||
})
|
||||
|
||||
codexCommand = createFakeCodexScript(t, "tid-startup", "ok")
|
||||
stdinReader = strings.NewReader("")
|
||||
isTerminalFn = func() bool { return true }
|
||||
os.Args = []string{"codex-wrapper", "task"}
|
||||
|
||||
if exit := run(); exit != 0 {
|
||||
t.Fatalf("run() exit=%d, want 0", exit)
|
||||
}
|
||||
|
||||
for _, orphan := range []string{orphanA, orphanB, orphanC} {
|
||||
if _, err := os.Stat(orphan); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected orphan %s to be removed, err=%v", orphan, err)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(runningLog); err != nil {
|
||||
t.Fatalf("expected running log to remain, err=%v", err)
|
||||
}
|
||||
if _, err := os.Stat(unrelated); err != nil {
|
||||
t.Fatalf("expected unrelated file to remain, err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunStartupCleanupConcurrentWrappers(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
const totalLogs = 40
|
||||
for i := 0; i < totalLogs; i++ {
|
||||
createTempLog(t, tempDir, fmt.Sprintf("codex-wrapper-%d.log", 9000+i))
|
||||
}
|
||||
|
||||
stubProcessRunning(t, func(pid int) bool {
|
||||
return false
|
||||
})
|
||||
stubProcessStartTime(t, func(int) time.Time { return time.Time{} })
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const instances = 5
|
||||
start := make(chan struct{})
|
||||
|
||||
for i := 0; i < instances; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
<-start
|
||||
runStartupCleanup()
|
||||
}()
|
||||
}
|
||||
|
||||
close(start)
|
||||
wg.Wait()
|
||||
|
||||
matches, err := filepath.Glob(filepath.Join(tempDir, "codex-wrapper-*.log"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob error: %v", err)
|
||||
}
|
||||
if len(matches) != 0 {
|
||||
t.Fatalf("expected all orphan logs to be removed, remaining=%v", matches)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCleanupFlagEndToEnd_Success(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
staleA := createTempLog(t, tempDir, "codex-wrapper-2100.log")
|
||||
staleB := createTempLog(t, tempDir, "codex-wrapper-2200-extra.log")
|
||||
keeper := createTempLog(t, tempDir, "codex-wrapper-2300.log")
|
||||
|
||||
stubProcessRunning(t, func(pid int) bool {
|
||||
return pid == 2300 || pid == os.Getpid()
|
||||
})
|
||||
stubProcessStartTime(t, func(pid int) time.Time {
|
||||
if pid == 2300 || pid == os.Getpid() {
|
||||
return time.Now().Add(-1 * time.Hour)
|
||||
}
|
||||
return time.Time{}
|
||||
})
|
||||
|
||||
os.Args = []string{"codex-wrapper", "--cleanup"}
|
||||
|
||||
var exitCode int
|
||||
output := captureStdout(t, func() {
|
||||
exitCode = run()
|
||||
})
|
||||
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("cleanup exit = %d, want 0", exitCode)
|
||||
}
|
||||
|
||||
// Check that output contains expected counts and file names
|
||||
if !strings.Contains(output, "Cleanup completed") {
|
||||
t.Fatalf("missing 'Cleanup completed' in output: %q", output)
|
||||
}
|
||||
if !strings.Contains(output, "Files scanned: 3") {
|
||||
t.Fatalf("missing 'Files scanned: 3' in output: %q", output)
|
||||
}
|
||||
if !strings.Contains(output, "Files deleted: 2") {
|
||||
t.Fatalf("missing 'Files deleted: 2' in output: %q", output)
|
||||
}
|
||||
if !strings.Contains(output, "Files kept: 1") {
|
||||
t.Fatalf("missing 'Files kept: 1' in output: %q", output)
|
||||
}
|
||||
if !strings.Contains(output, "codex-wrapper-2100.log") || !strings.Contains(output, "codex-wrapper-2200-extra.log") {
|
||||
t.Fatalf("missing deleted file names in output: %q", output)
|
||||
}
|
||||
if !strings.Contains(output, "codex-wrapper-2300.log") {
|
||||
t.Fatalf("missing kept file names in output: %q", output)
|
||||
}
|
||||
|
||||
for _, path := range []string{staleA, staleB} {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected %s to be removed, err=%v", path, err)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(keeper); err != nil {
|
||||
t.Fatalf("expected kept log to remain, err=%v", err)
|
||||
}
|
||||
|
||||
currentLog := filepath.Join(tempDir, fmt.Sprintf("codex-wrapper-%d.log", os.Getpid()))
|
||||
if _, err := os.Stat(currentLog); err == nil {
|
||||
t.Fatalf("cleanup mode should not create new log file %s", currentLog)
|
||||
} else if !os.IsNotExist(err) {
|
||||
t.Fatalf("stat(%s) unexpected error: %v", currentLog, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCleanupFlagEndToEnd_FailureDoesNotAffectStartup(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
calls := 0
|
||||
cleanupLogsFn = func() (CleanupStats, error) {
|
||||
calls++
|
||||
return CleanupStats{Scanned: 1}, fmt.Errorf("permission denied")
|
||||
}
|
||||
|
||||
os.Args = []string{"codex-wrapper", "--cleanup"}
|
||||
|
||||
var exitCode int
|
||||
errOutput := captureStderr(t, func() {
|
||||
exitCode = run()
|
||||
})
|
||||
|
||||
if exitCode != 1 {
|
||||
t.Fatalf("cleanup failure exit = %d, want 1", exitCode)
|
||||
}
|
||||
if !strings.Contains(errOutput, "Cleanup failed") || !strings.Contains(errOutput, "permission denied") {
|
||||
t.Fatalf("cleanup stderr = %q, want failure message", errOutput)
|
||||
}
|
||||
if calls != 1 {
|
||||
t.Fatalf("cleanup called %d times, want 1", calls)
|
||||
}
|
||||
|
||||
currentLog := filepath.Join(tempDir, fmt.Sprintf("codex-wrapper-%d.log", os.Getpid()))
|
||||
if _, err := os.Stat(currentLog); err == nil {
|
||||
t.Fatalf("cleanup failure should not create new log file %s", currentLog)
|
||||
} else if !os.IsNotExist(err) {
|
||||
t.Fatalf("stat(%s) unexpected error: %v", currentLog, err)
|
||||
}
|
||||
|
||||
cleanupLogsFn = func() (CleanupStats, error) {
|
||||
return CleanupStats{}, nil
|
||||
}
|
||||
codexCommand = createFakeCodexScript(t, "tid-cleanup-e2e", "ok")
|
||||
stdinReader = strings.NewReader("")
|
||||
isTerminalFn = func() bool { return true }
|
||||
os.Args = []string{"codex-wrapper", "post-cleanup task"}
|
||||
|
||||
var normalExit int
|
||||
normalOutput := captureStdout(t, func() {
|
||||
normalExit = run()
|
||||
})
|
||||
|
||||
if normalExit != 0 {
|
||||
t.Fatalf("normal run exit = %d, want 0", normalExit)
|
||||
}
|
||||
if !strings.Contains(normalOutput, "ok") {
|
||||
t.Fatalf("normal run output = %q, want codex output", normalOutput)
|
||||
}
|
||||
}
|
||||
3921
codeagent-wrapper/main_test.go
Normal file
3921
codeagent-wrapper/main_test.go
Normal file
File diff suppressed because it is too large
Load Diff
346
codeagent-wrapper/parser.go
Normal file
346
codeagent-wrapper/parser.go
Normal file
@@ -0,0 +1,346 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// JSONEvent represents a Codex JSON output event
|
||||
type JSONEvent struct {
|
||||
Type string `json:"type"`
|
||||
ThreadID string `json:"thread_id,omitempty"`
|
||||
Item *EventItem `json:"item,omitempty"`
|
||||
}
|
||||
|
||||
// EventItem represents the item field in a JSON event
|
||||
type EventItem struct {
|
||||
Type string `json:"type"`
|
||||
Text interface{} `json:"text"`
|
||||
}
|
||||
|
||||
// ClaudeEvent for Claude stream-json format
|
||||
type ClaudeEvent struct {
|
||||
Type string `json:"type"`
|
||||
Subtype string `json:"subtype,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
Result string `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
// GeminiEvent for Gemini stream-json format
|
||||
type GeminiEvent struct {
|
||||
Type string `json:"type"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Delta bool `json:"delta,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
func parseJSONStream(r io.Reader) (message, threadID string) {
|
||||
return parseJSONStreamWithLog(r, logWarn, logInfo)
|
||||
}
|
||||
|
||||
func parseJSONStreamWithWarn(r io.Reader, warnFn func(string)) (message, threadID string) {
|
||||
return parseJSONStreamWithLog(r, warnFn, logInfo)
|
||||
}
|
||||
|
||||
func parseJSONStreamWithLog(r io.Reader, warnFn func(string), infoFn func(string)) (message, threadID string) {
|
||||
return parseJSONStreamInternal(r, warnFn, infoFn, nil)
|
||||
}
|
||||
|
||||
const (
|
||||
jsonLineReaderSize = 64 * 1024
|
||||
jsonLineMaxBytes = 10 * 1024 * 1024
|
||||
jsonLinePreviewBytes = 256
|
||||
)
|
||||
|
||||
type codexHeader struct {
|
||||
Type string `json:"type"`
|
||||
ThreadID string `json:"thread_id,omitempty"`
|
||||
Item *struct {
|
||||
Type string `json:"type"`
|
||||
} `json:"item,omitempty"`
|
||||
}
|
||||
|
||||
func parseJSONStreamInternal(r io.Reader, warnFn func(string), infoFn func(string), onMessage func()) (message, threadID string) {
|
||||
reader := bufio.NewReaderSize(r, jsonLineReaderSize)
|
||||
|
||||
if warnFn == nil {
|
||||
warnFn = func(string) {}
|
||||
}
|
||||
if infoFn == nil {
|
||||
infoFn = func(string) {}
|
||||
}
|
||||
|
||||
notifyMessage := func() {
|
||||
if onMessage != nil {
|
||||
onMessage()
|
||||
}
|
||||
}
|
||||
|
||||
totalEvents := 0
|
||||
|
||||
var (
|
||||
codexMessage string
|
||||
claudeMessage string
|
||||
geminiBuffer strings.Builder
|
||||
)
|
||||
|
||||
for {
|
||||
line, tooLong, err := readLineWithLimit(reader, jsonLineMaxBytes, jsonLinePreviewBytes)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
warnFn("Read stdout error: " + err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
totalEvents++
|
||||
|
||||
if tooLong {
|
||||
warnFn(fmt.Sprintf("Skipped overlong JSON line (> %d bytes): %s", jsonLineMaxBytes, truncateBytes(line, 100)))
|
||||
continue
|
||||
}
|
||||
|
||||
var codex codexHeader
|
||||
if err := json.Unmarshal(line, &codex); err == nil {
|
||||
isCodex := codex.ThreadID != "" || (codex.Item != nil && codex.Item.Type != "")
|
||||
if isCodex {
|
||||
var details []string
|
||||
if codex.ThreadID != "" {
|
||||
details = append(details, fmt.Sprintf("thread_id=%s", codex.ThreadID))
|
||||
}
|
||||
if codex.Item != nil && codex.Item.Type != "" {
|
||||
details = append(details, fmt.Sprintf("item_type=%s", codex.Item.Type))
|
||||
}
|
||||
if len(details) > 0 {
|
||||
infoFn(fmt.Sprintf("Parsed event #%d type=%s (%s)", totalEvents, codex.Type, strings.Join(details, ", ")))
|
||||
} else {
|
||||
infoFn(fmt.Sprintf("Parsed event #%d type=%s", totalEvents, codex.Type))
|
||||
}
|
||||
|
||||
switch codex.Type {
|
||||
case "thread.started":
|
||||
threadID = codex.ThreadID
|
||||
infoFn(fmt.Sprintf("thread.started event thread_id=%s", threadID))
|
||||
case "item.completed":
|
||||
itemType := ""
|
||||
if codex.Item != nil {
|
||||
itemType = codex.Item.Type
|
||||
}
|
||||
|
||||
if itemType == "agent_message" {
|
||||
var event JSONEvent
|
||||
if err := json.Unmarshal(line, &event); err != nil {
|
||||
warnFn(fmt.Sprintf("Failed to parse Codex event: %s", truncateBytes(line, 100)))
|
||||
continue
|
||||
}
|
||||
|
||||
normalized := ""
|
||||
if event.Item != nil {
|
||||
normalized = normalizeText(event.Item.Text)
|
||||
}
|
||||
infoFn(fmt.Sprintf("item.completed event item_type=%s message_len=%d", itemType, len(normalized)))
|
||||
if normalized != "" {
|
||||
codexMessage = normalized
|
||||
notifyMessage()
|
||||
}
|
||||
} else {
|
||||
infoFn(fmt.Sprintf("item.completed event item_type=%s", itemType))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(line, &raw); err != nil {
|
||||
warnFn(fmt.Sprintf("Failed to parse line: %s", truncateBytes(line, 100)))
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case hasKey(raw, "subtype") || hasKey(raw, "result"):
|
||||
var event ClaudeEvent
|
||||
if err := json.Unmarshal(line, &event); err != nil {
|
||||
warnFn(fmt.Sprintf("Failed to parse Claude event: %s", truncateBytes(line, 100)))
|
||||
continue
|
||||
}
|
||||
|
||||
if event.SessionID != "" && threadID == "" {
|
||||
threadID = event.SessionID
|
||||
}
|
||||
|
||||
infoFn(fmt.Sprintf("Parsed Claude event #%d type=%s subtype=%s result_len=%d", totalEvents, event.Type, event.Subtype, len(event.Result)))
|
||||
|
||||
if event.Result != "" {
|
||||
claudeMessage = event.Result
|
||||
notifyMessage()
|
||||
}
|
||||
|
||||
case hasKey(raw, "role") || hasKey(raw, "delta"):
|
||||
var event GeminiEvent
|
||||
if err := json.Unmarshal(line, &event); err != nil {
|
||||
warnFn(fmt.Sprintf("Failed to parse Gemini event: %s", truncateBytes(line, 100)))
|
||||
continue
|
||||
}
|
||||
|
||||
if event.SessionID != "" && threadID == "" {
|
||||
threadID = event.SessionID
|
||||
}
|
||||
|
||||
if event.Content != "" {
|
||||
geminiBuffer.WriteString(event.Content)
|
||||
notifyMessage()
|
||||
}
|
||||
|
||||
infoFn(fmt.Sprintf("Parsed Gemini event #%d type=%s role=%s delta=%t status=%s content_len=%d", totalEvents, event.Type, event.Role, event.Delta, event.Status, len(event.Content)))
|
||||
|
||||
default:
|
||||
warnFn(fmt.Sprintf("Unknown event format: %s", truncateBytes(line, 100)))
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case geminiBuffer.Len() > 0:
|
||||
message = geminiBuffer.String()
|
||||
case claudeMessage != "":
|
||||
message = claudeMessage
|
||||
default:
|
||||
message = codexMessage
|
||||
}
|
||||
|
||||
infoFn(fmt.Sprintf("parseJSONStream completed: events=%d, message_len=%d, thread_id_found=%t", totalEvents, len(message), threadID != ""))
|
||||
return message, threadID
|
||||
}
|
||||
|
||||
func hasKey(m map[string]json.RawMessage, key string) bool {
|
||||
_, ok := m[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func discardInvalidJSON(decoder *json.Decoder, reader *bufio.Reader) (*bufio.Reader, error) {
|
||||
var buffered bytes.Buffer
|
||||
|
||||
if decoder != nil {
|
||||
if buf := decoder.Buffered(); buf != nil {
|
||||
_, _ = buffered.ReadFrom(buf)
|
||||
}
|
||||
}
|
||||
|
||||
line, err := reader.ReadBytes('\n')
|
||||
buffered.Write(line)
|
||||
|
||||
data := buffered.Bytes()
|
||||
newline := bytes.IndexByte(data, '\n')
|
||||
if newline == -1 {
|
||||
return reader, err
|
||||
}
|
||||
|
||||
remaining := data[newline+1:]
|
||||
if len(remaining) == 0 {
|
||||
return reader, err
|
||||
}
|
||||
|
||||
return bufio.NewReader(io.MultiReader(bytes.NewReader(remaining), reader)), err
|
||||
}
|
||||
|
||||
func readLineWithLimit(r *bufio.Reader, maxBytes int, previewBytes int) (line []byte, tooLong bool, err error) {
|
||||
if r == nil {
|
||||
return nil, false, errors.New("reader is nil")
|
||||
}
|
||||
if maxBytes <= 0 {
|
||||
return nil, false, errors.New("maxBytes must be > 0")
|
||||
}
|
||||
if previewBytes < 0 {
|
||||
previewBytes = 0
|
||||
}
|
||||
|
||||
part, isPrefix, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if !isPrefix {
|
||||
if len(part) > maxBytes {
|
||||
return part[:min(len(part), previewBytes)], true, nil
|
||||
}
|
||||
return part, false, nil
|
||||
}
|
||||
|
||||
preview := make([]byte, 0, min(previewBytes, len(part)))
|
||||
if previewBytes > 0 {
|
||||
preview = append(preview, part[:min(previewBytes, len(part))]...)
|
||||
}
|
||||
|
||||
buf := make([]byte, 0, min(maxBytes, len(part)*2))
|
||||
total := 0
|
||||
if len(part) > maxBytes {
|
||||
tooLong = true
|
||||
} else {
|
||||
buf = append(buf, part...)
|
||||
total = len(part)
|
||||
}
|
||||
|
||||
for isPrefix {
|
||||
part, isPrefix, err = r.ReadLine()
|
||||
if err != nil {
|
||||
return nil, tooLong, err
|
||||
}
|
||||
|
||||
if previewBytes > 0 && len(preview) < previewBytes {
|
||||
preview = append(preview, part[:min(previewBytes-len(preview), len(part))]...)
|
||||
}
|
||||
|
||||
if !tooLong {
|
||||
if total+len(part) > maxBytes {
|
||||
tooLong = true
|
||||
continue
|
||||
}
|
||||
buf = append(buf, part...)
|
||||
total += len(part)
|
||||
}
|
||||
}
|
||||
|
||||
if tooLong {
|
||||
return preview, true, nil
|
||||
}
|
||||
return buf, false, nil
|
||||
}
|
||||
|
||||
func truncateBytes(b []byte, maxLen int) string {
|
||||
if len(b) <= maxLen {
|
||||
return string(b)
|
||||
}
|
||||
if maxLen < 0 {
|
||||
return ""
|
||||
}
|
||||
return string(b[:maxLen]) + "..."
|
||||
}
|
||||
|
||||
func normalizeText(text interface{}) string {
|
||||
switch v := text.(type) {
|
||||
case string:
|
||||
return v
|
||||
case []interface{}:
|
||||
var sb strings.Builder
|
||||
for _, item := range v {
|
||||
if s, ok := item.(string); ok {
|
||||
sb.WriteString(s)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
31
codeagent-wrapper/parser_token_too_long_test.go
Normal file
31
codeagent-wrapper/parser_token_too_long_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseJSONStream_SkipsOverlongLineAndContinues(t *testing.T) {
|
||||
// Exceed the 10MB bufio.Scanner limit in parseJSONStreamInternal.
|
||||
tooLong := strings.Repeat("a", 11*1024*1024)
|
||||
|
||||
input := strings.Join([]string{
|
||||
`{"type":"item.completed","item":{"type":"other_type","text":"` + tooLong + `"}}`,
|
||||
`{"type":"thread.started","thread_id":"t-1"}`,
|
||||
`{"type":"item.completed","item":{"type":"agent_message","text":"ok"}}`,
|
||||
}, "\n")
|
||||
|
||||
var warns []string
|
||||
warnFn := func(msg string) { warns = append(warns, msg) }
|
||||
|
||||
gotMessage, gotThreadID := parseJSONStreamInternal(strings.NewReader(input), warnFn, nil, nil)
|
||||
if gotMessage != "ok" {
|
||||
t.Fatalf("message=%q, want %q (warns=%v)", gotMessage, "ok", warns)
|
||||
}
|
||||
if gotThreadID != "t-1" {
|
||||
t.Fatalf("threadID=%q, want %q (warns=%v)", gotThreadID, "t-1", warns)
|
||||
}
|
||||
if len(warns) == 0 || !strings.Contains(warns[0], "Skipped overlong JSON line") {
|
||||
t.Fatalf("expected warning about overlong JSON line, got %v", warns)
|
||||
}
|
||||
}
|
||||
217
codeagent-wrapper/process_check_test.go
Normal file
217
codeagent-wrapper/process_check_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
//go:build unix || darwin || linux
|
||||
// +build unix darwin linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIsProcessRunning(t *testing.T) {
|
||||
t.Run("current process", func(t *testing.T) {
|
||||
if !isProcessRunning(os.Getpid()) {
|
||||
t.Fatalf("expected current process (pid=%d) to be running", os.Getpid())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fake pid", func(t *testing.T) {
|
||||
const nonexistentPID = 1 << 30
|
||||
if isProcessRunning(nonexistentPID) {
|
||||
t.Fatalf("expected pid %d to be reported as not running", nonexistentPID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("terminated process", func(t *testing.T) {
|
||||
pid := exitedProcessPID(t)
|
||||
if isProcessRunning(pid) {
|
||||
t.Fatalf("expected exited child process (pid=%d) to be reported as not running", pid)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("boundary values", func(t *testing.T) {
|
||||
if isProcessRunning(0) {
|
||||
t.Fatalf("pid 0 should never be treated as running")
|
||||
}
|
||||
if isProcessRunning(-42) {
|
||||
t.Fatalf("negative pid should never be treated as running")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("find process error", func(t *testing.T) {
|
||||
original := findProcess
|
||||
defer func() { findProcess = original }()
|
||||
|
||||
mockErr := errors.New("findProcess failure")
|
||||
findProcess = func(pid int) (*os.Process, error) {
|
||||
return nil, mockErr
|
||||
}
|
||||
|
||||
if isProcessRunning(1234) {
|
||||
t.Fatalf("expected false when os.FindProcess fails")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func exitedProcessPID(t *testing.T) int {
|
||||
t.Helper()
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("cmd", "/c", "exit 0")
|
||||
} else {
|
||||
cmd = exec.Command("sh", "-c", "exit 0")
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("failed to start helper process: %v", err)
|
||||
}
|
||||
pid := cmd.Process.Pid
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
t.Fatalf("helper process did not exit cleanly: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
return pid
|
||||
}
|
||||
|
||||
func TestRunProcessCheckSmoke(t *testing.T) {
|
||||
t.Run("current process", func(t *testing.T) {
|
||||
if !isProcessRunning(os.Getpid()) {
|
||||
t.Fatalf("expected current process (pid=%d) to be running", os.Getpid())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fake pid", func(t *testing.T) {
|
||||
const nonexistentPID = 1 << 30
|
||||
if isProcessRunning(nonexistentPID) {
|
||||
t.Fatalf("expected pid %d to be reported as not running", nonexistentPID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("boundary values", func(t *testing.T) {
|
||||
if isProcessRunning(0) {
|
||||
t.Fatalf("pid 0 should never be treated as running")
|
||||
}
|
||||
if isProcessRunning(-42) {
|
||||
t.Fatalf("negative pid should never be treated as running")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("find process error", func(t *testing.T) {
|
||||
original := findProcess
|
||||
defer func() { findProcess = original }()
|
||||
|
||||
mockErr := errors.New("findProcess failure")
|
||||
findProcess = func(pid int) (*os.Process, error) {
|
||||
return nil, mockErr
|
||||
}
|
||||
|
||||
if isProcessRunning(1234) {
|
||||
t.Fatalf("expected false when os.FindProcess fails")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetProcessStartTimeReadsProcStat(t *testing.T) {
|
||||
pid := 4321
|
||||
boot := time.Unix(1_710_000_000, 0)
|
||||
startTicks := uint64(4500)
|
||||
|
||||
statFields := make([]string, 25)
|
||||
for i := range statFields {
|
||||
statFields[i] = strconv.Itoa(i + 1)
|
||||
}
|
||||
statFields[19] = strconv.FormatUint(startTicks, 10)
|
||||
statContent := fmt.Sprintf("%d (%s) %s", pid, "cmd with space", strings.Join(statFields, " "))
|
||||
|
||||
stubReadFile(t, func(path string) ([]byte, error) {
|
||||
switch path {
|
||||
case fmt.Sprintf("/proc/%d/stat", pid):
|
||||
return []byte(statContent), nil
|
||||
case "/proc/stat":
|
||||
return []byte(fmt.Sprintf("cpu 0 0 0 0\nbtime %d\n", boot.Unix())), nil
|
||||
default:
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
})
|
||||
|
||||
got := getProcessStartTime(pid)
|
||||
want := boot.Add(time.Duration(startTicks/100) * time.Second)
|
||||
if !got.Equal(want) {
|
||||
t.Fatalf("getProcessStartTime() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProcessStartTimeInvalidData(t *testing.T) {
|
||||
pid := 99
|
||||
stubReadFile(t, func(path string) ([]byte, error) {
|
||||
switch path {
|
||||
case fmt.Sprintf("/proc/%d/stat", pid):
|
||||
return []byte("garbage"), nil
|
||||
case "/proc/stat":
|
||||
return []byte("btime not-a-number\n"), nil
|
||||
default:
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
})
|
||||
|
||||
if got := getProcessStartTime(pid); !got.IsZero() {
|
||||
t.Fatalf("invalid /proc data should return zero time, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBootTimeParsesBtime(t *testing.T) {
|
||||
const bootSec = 1_711_111_111
|
||||
stubReadFile(t, func(path string) ([]byte, error) {
|
||||
if path != "/proc/stat" {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
content := fmt.Sprintf("intr 0\nbtime %d\n", bootSec)
|
||||
return []byte(content), nil
|
||||
})
|
||||
|
||||
got := getBootTime()
|
||||
want := time.Unix(bootSec, 0)
|
||||
if !got.Equal(want) {
|
||||
t.Fatalf("getBootTime() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBootTimeInvalidData(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
content string
|
||||
}{
|
||||
{"missing", "cpu 0 0 0 0"},
|
||||
{"malformed", "btime abc"},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
stubReadFile(t, func(string) ([]byte, error) {
|
||||
return []byte(tt.content), nil
|
||||
})
|
||||
if got := getBootTime(); !got.IsZero() {
|
||||
t.Fatalf("getBootTime() unexpected value for %s: %v", tt.name, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stubReadFile(t *testing.T, fn func(string) ([]byte, error)) {
|
||||
t.Helper()
|
||||
original := readFileFn
|
||||
readFileFn = fn
|
||||
t.Cleanup(func() {
|
||||
readFileFn = original
|
||||
})
|
||||
}
|
||||
104
codeagent-wrapper/process_check_unix.go
Normal file
104
codeagent-wrapper/process_check_unix.go
Normal file
@@ -0,0 +1,104 @@
|
||||
//go:build unix || darwin || linux
|
||||
// +build unix darwin linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var findProcess = os.FindProcess
|
||||
var readFileFn = os.ReadFile
|
||||
|
||||
// isProcessRunning returns true if a process with the given pid is running on Unix-like systems.
|
||||
func isProcessRunning(pid int) bool {
|
||||
if pid <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
proc, err := findProcess(pid)
|
||||
if err != nil || proc == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
err = proc.Signal(syscall.Signal(0))
|
||||
if err != nil && (errors.Is(err, syscall.ESRCH) || errors.Is(err, os.ErrProcessDone)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// getProcessStartTime returns the start time of a process on Unix-like systems.
|
||||
// Returns zero time if the start time cannot be determined.
|
||||
func getProcessStartTime(pid int) time.Time {
|
||||
if pid <= 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Read /proc/<pid>/stat to get process start time
|
||||
statPath := fmt.Sprintf("/proc/%d/stat", pid)
|
||||
data, err := readFileFn(statPath)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Parse stat file: fields are space-separated, but comm (field 2) can contain spaces
|
||||
// Find the last ')' to skip comm field safely
|
||||
content := string(data)
|
||||
lastParen := strings.LastIndex(content, ")")
|
||||
if lastParen == -1 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
fields := strings.Fields(content[lastParen+1:])
|
||||
if len(fields) < 20 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Field 22 (index 19 after comm) is starttime in clock ticks since boot
|
||||
startTicks, err := strconv.ParseUint(fields[19], 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Get system boot time
|
||||
bootTime := getBootTime()
|
||||
if bootTime.IsZero() {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Convert ticks to duration (typically 100 ticks/sec on most systems)
|
||||
ticksPerSec := uint64(100) // sysconf(_SC_CLK_TCK), typically 100
|
||||
startTime := bootTime.Add(time.Duration(startTicks/ticksPerSec) * time.Second)
|
||||
|
||||
return startTime
|
||||
}
|
||||
|
||||
// getBootTime returns the system boot time by reading /proc/stat.
|
||||
func getBootTime() time.Time {
|
||||
data, err := readFileFn("/proc/stat")
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "btime ") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
bootSec, err := strconv.ParseInt(fields[1], 10, 64)
|
||||
if err == nil {
|
||||
return time.Unix(bootSec, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}
|
||||
}
|
||||
87
codeagent-wrapper/process_check_windows.go
Normal file
87
codeagent-wrapper/process_check_windows.go
Normal file
@@ -0,0 +1,87 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
processQueryLimitedInformation = 0x1000
|
||||
stillActive = 259 // STILL_ACTIVE exit code
|
||||
)
|
||||
|
||||
var (
|
||||
findProcess = os.FindProcess
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
getProcessTimes = kernel32.NewProc("GetProcessTimes")
|
||||
fileTimeToUnixFn = fileTimeToUnix
|
||||
)
|
||||
|
||||
// isProcessRunning returns true if a process with the given pid is running on Windows.
|
||||
func isProcessRunning(pid int) bool {
|
||||
if pid <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, err := findProcess(pid); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
handle, err := syscall.OpenProcess(processQueryLimitedInformation, false, uint32(pid))
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.ERROR_ACCESS_DENIED) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
defer syscall.CloseHandle(handle)
|
||||
|
||||
var exitCode uint32
|
||||
if err := syscall.GetExitCodeProcess(handle, &exitCode); err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return exitCode == stillActive
|
||||
}
|
||||
|
||||
// getProcessStartTime returns the start time of a process on Windows.
|
||||
// Returns zero time if the start time cannot be determined.
|
||||
func getProcessStartTime(pid int) time.Time {
|
||||
if pid <= 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
handle, err := syscall.OpenProcess(processQueryLimitedInformation, false, uint32(pid))
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
defer syscall.CloseHandle(handle)
|
||||
|
||||
var creationTime, exitTime, kernelTime, userTime syscall.Filetime
|
||||
ret, _, _ := getProcessTimes.Call(
|
||||
uintptr(handle),
|
||||
uintptr(unsafe.Pointer(&creationTime)),
|
||||
uintptr(unsafe.Pointer(&exitTime)),
|
||||
uintptr(unsafe.Pointer(&kernelTime)),
|
||||
uintptr(unsafe.Pointer(&userTime)),
|
||||
)
|
||||
|
||||
if ret == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
return fileTimeToUnixFn(creationTime)
|
||||
}
|
||||
|
||||
// fileTimeToUnix converts Windows FILETIME to Unix time.
|
||||
func fileTimeToUnix(ft syscall.Filetime) time.Time {
|
||||
// FILETIME is 100-nanosecond intervals since January 1, 1601 UTC
|
||||
nsec := ft.Nanoseconds()
|
||||
return time.Unix(0, nsec)
|
||||
}
|
||||
225
codeagent-wrapper/utils.go
Normal file
225
codeagent-wrapper/utils.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func resolveTimeout() int {
|
||||
raw := os.Getenv("CODEX_TIMEOUT")
|
||||
if raw == "" {
|
||||
return defaultTimeout
|
||||
}
|
||||
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil || parsed <= 0 {
|
||||
logWarn(fmt.Sprintf("Invalid CODEX_TIMEOUT '%s', falling back to %ds", raw, defaultTimeout))
|
||||
return defaultTimeout
|
||||
}
|
||||
|
||||
if parsed > 10000 {
|
||||
return parsed / 1000
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func readPipedTask() (string, error) {
|
||||
if isTerminal() {
|
||||
logInfo("Stdin is tty, skipping pipe read")
|
||||
return "", nil
|
||||
}
|
||||
logInfo("Reading from stdin pipe...")
|
||||
data, err := io.ReadAll(stdinReader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read stdin: %w", err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
logInfo("Stdin pipe returned empty data")
|
||||
return "", nil
|
||||
}
|
||||
logInfo(fmt.Sprintf("Read %d bytes from stdin pipe", len(data)))
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func shouldUseStdin(taskText string, piped bool) bool {
|
||||
if piped {
|
||||
return true
|
||||
}
|
||||
if len(taskText) > 800 {
|
||||
return true
|
||||
}
|
||||
return strings.IndexAny(taskText, stdinSpecialChars) >= 0
|
||||
}
|
||||
|
||||
func defaultIsTerminal() bool {
|
||||
fi, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return (fi.Mode() & os.ModeCharDevice) != 0
|
||||
}
|
||||
|
||||
func isTerminal() bool {
|
||||
return isTerminalFn()
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
type logWriter struct {
|
||||
prefix string
|
||||
maxLen int
|
||||
buf bytes.Buffer
|
||||
dropped bool
|
||||
}
|
||||
|
||||
func newLogWriter(prefix string, maxLen int) *logWriter {
|
||||
if maxLen <= 0 {
|
||||
maxLen = codexLogLineLimit
|
||||
}
|
||||
return &logWriter{prefix: prefix, maxLen: maxLen}
|
||||
}
|
||||
|
||||
func (lw *logWriter) Write(p []byte) (int, error) {
|
||||
if lw == nil {
|
||||
return len(p), nil
|
||||
}
|
||||
total := len(p)
|
||||
for len(p) > 0 {
|
||||
if idx := bytes.IndexByte(p, '\n'); idx >= 0 {
|
||||
lw.writeLimited(p[:idx])
|
||||
lw.logLine(true)
|
||||
p = p[idx+1:]
|
||||
continue
|
||||
}
|
||||
lw.writeLimited(p)
|
||||
break
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (lw *logWriter) Flush() {
|
||||
if lw == nil || lw.buf.Len() == 0 {
|
||||
return
|
||||
}
|
||||
lw.logLine(false)
|
||||
}
|
||||
|
||||
func (lw *logWriter) logLine(force bool) {
|
||||
if lw == nil {
|
||||
return
|
||||
}
|
||||
line := lw.buf.String()
|
||||
dropped := lw.dropped
|
||||
lw.dropped = false
|
||||
lw.buf.Reset()
|
||||
if line == "" && !force {
|
||||
return
|
||||
}
|
||||
if lw.maxLen > 0 {
|
||||
if dropped {
|
||||
if lw.maxLen > 3 {
|
||||
line = line[:min(len(line), lw.maxLen-3)] + "..."
|
||||
} else {
|
||||
line = line[:min(len(line), lw.maxLen)]
|
||||
}
|
||||
} else if len(line) > lw.maxLen {
|
||||
cutoff := lw.maxLen
|
||||
if cutoff > 3 {
|
||||
line = line[:cutoff-3] + "..."
|
||||
} else {
|
||||
line = line[:cutoff]
|
||||
}
|
||||
}
|
||||
}
|
||||
logInfo(lw.prefix + line)
|
||||
}
|
||||
|
||||
func (lw *logWriter) writeLimited(p []byte) {
|
||||
if lw == nil || len(p) == 0 {
|
||||
return
|
||||
}
|
||||
if lw.maxLen <= 0 {
|
||||
lw.buf.Write(p)
|
||||
return
|
||||
}
|
||||
|
||||
remaining := lw.maxLen - lw.buf.Len()
|
||||
if remaining <= 0 {
|
||||
lw.dropped = true
|
||||
return
|
||||
}
|
||||
if len(p) <= remaining {
|
||||
lw.buf.Write(p)
|
||||
return
|
||||
}
|
||||
lw.buf.Write(p[:remaining])
|
||||
lw.dropped = true
|
||||
}
|
||||
|
||||
type tailBuffer struct {
|
||||
limit int
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (b *tailBuffer) Write(p []byte) (int, error) {
|
||||
if b.limit <= 0 {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
if len(p) >= b.limit {
|
||||
b.data = append(b.data[:0], p[len(p)-b.limit:]...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
total := len(b.data) + len(p)
|
||||
if total <= b.limit {
|
||||
b.data = append(b.data, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
overflow := total - b.limit
|
||||
b.data = append(b.data[overflow:], p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (b *tailBuffer) String() string {
|
||||
return string(b.data)
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
if maxLen < 0 {
|
||||
return ""
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func hello() string {
|
||||
return "hello world"
|
||||
}
|
||||
|
||||
func greet(name string) string {
|
||||
return "hello " + name
|
||||
}
|
||||
|
||||
func farewell(name string) string {
|
||||
return "goodbye " + name
|
||||
}
|
||||
126
codeagent-wrapper/wrapper_name.go
Normal file
126
codeagent-wrapper/wrapper_name.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultWrapperName = "codeagent-wrapper"
|
||||
legacyWrapperName = "codex-wrapper"
|
||||
)
|
||||
|
||||
var executablePathFn = os.Executable
|
||||
|
||||
func normalizeWrapperName(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
base := filepath.Base(path)
|
||||
base = strings.TrimSuffix(base, ".exe") // tolerate Windows executables
|
||||
|
||||
switch base {
|
||||
case defaultWrapperName, legacyWrapperName:
|
||||
return base
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// currentWrapperName resolves the wrapper name based on the invoked binary.
|
||||
// Only known names are honored to avoid leaking build/test binary names into logs.
|
||||
func currentWrapperName() string {
|
||||
if len(os.Args) == 0 {
|
||||
return defaultWrapperName
|
||||
}
|
||||
|
||||
if name := normalizeWrapperName(os.Args[0]); name != "" {
|
||||
return name
|
||||
}
|
||||
|
||||
execPath, err := executablePathFn()
|
||||
if err == nil {
|
||||
if name := normalizeWrapperName(execPath); name != "" {
|
||||
return name
|
||||
}
|
||||
|
||||
if resolved, err := filepath.EvalSymlinks(execPath); err == nil {
|
||||
if name := normalizeWrapperName(resolved); name != "" {
|
||||
return name
|
||||
}
|
||||
if alias := resolveAlias(execPath, resolved); alias != "" {
|
||||
return alias
|
||||
}
|
||||
}
|
||||
|
||||
if alias := resolveAlias(execPath, ""); alias != "" {
|
||||
return alias
|
||||
}
|
||||
}
|
||||
|
||||
return defaultWrapperName
|
||||
}
|
||||
|
||||
// logPrefixes returns the set of accepted log name prefixes, including the
|
||||
// current wrapper name and legacy aliases.
|
||||
func logPrefixes() []string {
|
||||
prefixes := []string{currentWrapperName(), defaultWrapperName, legacyWrapperName}
|
||||
seen := make(map[string]struct{}, len(prefixes))
|
||||
var unique []string
|
||||
for _, prefix := range prefixes {
|
||||
if prefix == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[prefix]; ok {
|
||||
continue
|
||||
}
|
||||
seen[prefix] = struct{}{}
|
||||
unique = append(unique, prefix)
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
// primaryLogPrefix returns the preferred filename prefix for log files.
|
||||
// Defaults to the current wrapper name when available, otherwise falls back
|
||||
// to the canonical default name.
|
||||
func primaryLogPrefix() string {
|
||||
prefixes := logPrefixes()
|
||||
if len(prefixes) == 0 {
|
||||
return defaultWrapperName
|
||||
}
|
||||
return prefixes[0]
|
||||
}
|
||||
|
||||
func resolveAlias(execPath string, target string) string {
|
||||
if execPath == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
dir := filepath.Dir(execPath)
|
||||
for _, candidate := range []string{defaultWrapperName, legacyWrapperName} {
|
||||
aliasPath := filepath.Join(dir, candidate)
|
||||
info, err := os.Lstat(aliasPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
resolved, err := filepath.EvalSymlinks(aliasPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if target != "" && resolved != target {
|
||||
continue
|
||||
}
|
||||
|
||||
if name := normalizeWrapperName(aliasPath); name != "" {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
50
codeagent-wrapper/wrapper_name_test.go
Normal file
50
codeagent-wrapper/wrapper_name_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCurrentWrapperNameFallsBackToExecutable(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
execPath := filepath.Join(tempDir, "codeagent-wrapper")
|
||||
if err := os.WriteFile(execPath, []byte("#!/bin/true\n"), 0o755); err != nil {
|
||||
t.Fatalf("failed to write fake binary: %v", err)
|
||||
}
|
||||
|
||||
os.Args = []string{filepath.Join(tempDir, "custom-name")}
|
||||
executablePathFn = func() (string, error) {
|
||||
return execPath, nil
|
||||
}
|
||||
|
||||
if got := currentWrapperName(); got != defaultWrapperName {
|
||||
t.Fatalf("currentWrapperName() = %q, want %q", got, defaultWrapperName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentWrapperNameDetectsLegacyAliasSymlink(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
execPath := filepath.Join(tempDir, "wrapper")
|
||||
aliasPath := filepath.Join(tempDir, legacyWrapperName)
|
||||
|
||||
if err := os.WriteFile(execPath, []byte("#!/bin/true\n"), 0o755); err != nil {
|
||||
t.Fatalf("failed to write fake binary: %v", err)
|
||||
}
|
||||
if err := os.Symlink(execPath, aliasPath); err != nil {
|
||||
t.Fatalf("failed to create alias: %v", err)
|
||||
}
|
||||
|
||||
os.Args = []string{filepath.Join(tempDir, "unknown-runner")}
|
||||
executablePathFn = func() (string, error) {
|
||||
return execPath, nil
|
||||
}
|
||||
|
||||
if got := currentWrapperName(); got != legacyWrapperName {
|
||||
t.Fatalf("currentWrapperName() = %q, want %q", got, legacyWrapperName)
|
||||
}
|
||||
}
|
||||
113
config.json
Normal file
113
config.json
Normal file
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"install_dir": "~/.claude",
|
||||
"log_file": "install.log",
|
||||
"modules": {
|
||||
"dev": {
|
||||
"enabled": true,
|
||||
"description": "Core dev workflow with Codex integration",
|
||||
"operations": [
|
||||
{
|
||||
"type": "merge_dir",
|
||||
"source": "dev-workflow",
|
||||
"description": "Merge commands/ and agents/ into install dir"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "memorys/CLAUDE.md",
|
||||
"target": "CLAUDE.md",
|
||||
"description": "Copy core role and guidelines"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/codeagent/SKILL.md",
|
||||
"target": "skills/codeagent/SKILL.md",
|
||||
"description": "Install codeagent skill"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/product-requirements/SKILL.md",
|
||||
"target": "skills/product-requirements/SKILL.md",
|
||||
"description": "Install product-requirements skill"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/prototype-prompt-generator/SKILL.md",
|
||||
"target": "skills/prototype-prompt-generator/SKILL.md",
|
||||
"description": "Install prototype-prompt-generator skill"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/prototype-prompt-generator/references/prompt-structure.md",
|
||||
"target": "skills/prototype-prompt-generator/references/prompt-structure.md",
|
||||
"description": "Install prototype-prompt-generator prompt structure reference"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/prototype-prompt-generator/references/design-systems.md",
|
||||
"target": "skills/prototype-prompt-generator/references/design-systems.md",
|
||||
"description": "Install prototype-prompt-generator design systems reference"
|
||||
},
|
||||
{
|
||||
"type": "run_command",
|
||||
"command": "bash install.sh",
|
||||
"description": "Install codeagent-wrapper binary",
|
||||
"env": {
|
||||
"INSTALL_DIR": "${install_dir}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"bmad": {
|
||||
"enabled": false,
|
||||
"description": "BMAD agile workflow with multi-agent orchestration",
|
||||
"operations": [
|
||||
{
|
||||
"type": "merge_dir",
|
||||
"source": "bmad-agile-workflow",
|
||||
"description": "Merge BMAD commands and agents"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "docs/BMAD-WORKFLOW.md",
|
||||
"target": "docs/BMAD-WORKFLOW.md",
|
||||
"description": "Copy BMAD workflow documentation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"requirements": {
|
||||
"enabled": false,
|
||||
"description": "Requirements-driven development workflow",
|
||||
"operations": [
|
||||
{
|
||||
"type": "merge_dir",
|
||||
"source": "requirements-driven-workflow",
|
||||
"description": "Merge requirements workflow commands and agents"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "docs/REQUIREMENTS-WORKFLOW.md",
|
||||
"target": "docs/REQUIREMENTS-WORKFLOW.md",
|
||||
"description": "Copy requirements workflow documentation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"essentials": {
|
||||
"enabled": true,
|
||||
"description": "Core development commands and utilities",
|
||||
"operations": [
|
||||
{
|
||||
"type": "merge_dir",
|
||||
"source": "development-essentials",
|
||||
"description": "Merge essential development commands"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "docs/DEVELOPMENT-COMMANDS.md",
|
||||
"target": "docs/DEVELOPMENT-COMMANDS.md",
|
||||
"description": "Copy development commands documentation"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
122
config.schema.json
Normal file
122
config.schema.json
Normal file
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://github.com/cexll/myclaude/config.schema.json",
|
||||
"title": "Modular Installation Config",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["version", "install_dir", "log_file", "modules"],
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$"
|
||||
},
|
||||
"install_dir": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Target installation directory, supports ~/ expansion"
|
||||
},
|
||||
"log_file": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"modules": {
|
||||
"type": "object",
|
||||
"description": "可自定义的模块定义,每个模块名称可任意指定",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9_-]+$": { "$ref": "#/$defs/module" }
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"module": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["enabled", "description", "operations"],
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean", "default": false },
|
||||
"description": { "type": "string", "minLength": 3 },
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/operation" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"operation": {
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/op_copy_dir" },
|
||||
{ "$ref": "#/$defs/op_copy_file" },
|
||||
{ "$ref": "#/$defs/op_merge_dir" },
|
||||
{ "$ref": "#/$defs/op_merge_json" },
|
||||
{ "$ref": "#/$defs/op_run_command" }
|
||||
]
|
||||
},
|
||||
"common_operation_fields": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"op_copy_dir": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "source", "target"],
|
||||
"properties": {
|
||||
"type": { "const": "copy_dir" },
|
||||
"source": { "type": "string", "minLength": 1 },
|
||||
"target": { "type": "string", "minLength": 1 },
|
||||
"description": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"op_copy_file": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "source", "target"],
|
||||
"properties": {
|
||||
"type": { "const": "copy_file" },
|
||||
"source": { "type": "string", "minLength": 1 },
|
||||
"target": { "type": "string", "minLength": 1 },
|
||||
"description": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"op_merge_dir": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "source"],
|
||||
"properties": {
|
||||
"type": { "const": "merge_dir" },
|
||||
"source": { "type": "string", "minLength": 1 },
|
||||
"description": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"op_merge_json": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "source", "target"],
|
||||
"properties": {
|
||||
"type": { "const": "merge_json" },
|
||||
"source": { "type": "string", "minLength": 1 },
|
||||
"target": { "type": "string", "minLength": 1 },
|
||||
"merge_key": { "type": "string" },
|
||||
"description": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"op_run_command": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["type", "command"],
|
||||
"properties": {
|
||||
"type": { "const": "run_command" },
|
||||
"command": { "type": "string", "minLength": 1 },
|
||||
"description": { "type": "string" },
|
||||
"env": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
174
dev-workflow/README.md
Normal file
174
dev-workflow/README.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# /dev - Minimal Dev Workflow
|
||||
|
||||
## Overview
|
||||
|
||||
A freshly designed lightweight development workflow with no legacy baggage, focused on delivering high-quality code fast.
|
||||
|
||||
## Flow
|
||||
|
||||
```
|
||||
/dev trigger
|
||||
↓
|
||||
AskUserQuestion (requirements clarification)
|
||||
↓
|
||||
codeagent analysis (plan mode + UI auto-detection)
|
||||
↓
|
||||
dev-plan-generator (create dev doc)
|
||||
↓
|
||||
codeagent concurrent development (2–5 tasks, backend split)
|
||||
↓
|
||||
codeagent testing & verification (≥90% coverage)
|
||||
↓
|
||||
Done (generate summary)
|
||||
```
|
||||
|
||||
## The 6 Steps
|
||||
|
||||
### 1. Clarify Requirements
|
||||
- Use **AskUserQuestion** to ask the user directly
|
||||
- No scoring system, no complex logic
|
||||
- 2–3 rounds of Q&A until the requirement is clear
|
||||
|
||||
### 2. codeagent Analysis & UI Detection
|
||||
- Call codeagent to analyze the request in plan mode style
|
||||
- Extract: core functions, technical points, task list (2–5 items)
|
||||
- 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
|
||||
|
||||
### 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)
|
||||
- Independent tasks → run in parallel
|
||||
- Conflicting tasks → run serially
|
||||
|
||||
### 5. Testing & Verification
|
||||
- Each codeagent task:
|
||||
- Implements the feature
|
||||
- Writes tests
|
||||
- Runs coverage
|
||||
- Reports results (≥90%)
|
||||
|
||||
### 6. Complete
|
||||
- Summarize task status
|
||||
- Record coverage
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
/dev "Implement user login with email + password"
|
||||
```
|
||||
|
||||
**No options**, fixed workflow, works out of the box.
|
||||
|
||||
## Output Structure
|
||||
|
||||
```
|
||||
.claude/specs/{feature_name}/
|
||||
└── dev-plan.md # Dev document generated by agent
|
||||
```
|
||||
|
||||
Only one file—minimal and clear.
|
||||
|
||||
## Core Components
|
||||
|
||||
### Tools
|
||||
- **AskUserQuestion**: interactive requirement clarification
|
||||
- **codeagent skill**: analysis, development, testing; supports `--backend` for codex (default) or gemini (UI)
|
||||
- **dev-plan-generator agent**: generate dev doc (subagent via Task tool, saves context)
|
||||
|
||||
## UI Auto-Detection & Backend Routing
|
||||
- **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
|
||||
|
||||
## Key Features
|
||||
|
||||
### ✅ Fresh Design
|
||||
- No legacy project residue
|
||||
- No complex scoring logic
|
||||
- No extra abstraction layers
|
||||
|
||||
### ✅ Minimal Orchestration
|
||||
- Orchestrator controls the flow directly
|
||||
- Only three tools/components
|
||||
- Steps are straightforward
|
||||
|
||||
### ✅ Concurrency
|
||||
- 2–5 tasks in parallel
|
||||
- Auto-detect dependencies and conflicts
|
||||
- codeagent executes independently
|
||||
|
||||
### ✅ Quality Assurance
|
||||
- Enforces 90% coverage
|
||||
- codeagent tests and verifies its own work
|
||||
- Automatic retry on failure
|
||||
|
||||
## Example
|
||||
|
||||
```bash
|
||||
# Trigger
|
||||
/dev "Add user login feature"
|
||||
|
||||
# Step 1: Clarify requirements
|
||||
Q: What login methods are supported?
|
||||
A: Email + password
|
||||
Q: Should login be remembered?
|
||||
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
|
||||
UI detection: needs_ui = true (tailwindcss classes in frontend form)
|
||||
|
||||
# Step 3: Generate doc
|
||||
dev-plan.md generated with backend + UI tasks ✓
|
||||
|
||||
# Step 4-5: Concurrent development (backend codex, UI gemini)
|
||||
[task-1] Backend API (codex) → tests → 92% ✓
|
||||
[task-2] Password hashing (codex) → tests → 95% ✓
|
||||
[task-3] Frontend form (gemini) → tests → 91% ✓
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
dev-workflow/
|
||||
├── README.md # This doc
|
||||
├── commands/
|
||||
│ └── dev.md # /dev workflow orchestrator definition
|
||||
└── agents/
|
||||
└── dev-plan-generator.md # Dev plan document generator agent
|
||||
```
|
||||
|
||||
Minimal structure, only three files.
|
||||
|
||||
## When to Use
|
||||
|
||||
✅ **Good for**:
|
||||
- Any feature size
|
||||
- Fast iterations
|
||||
- High test coverage needs
|
||||
- Wanting concurrent speed-up
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **KISS**: keep it simple
|
||||
2. **Disposable**: no persistent config
|
||||
3. **Quality first**: enforce 90% coverage
|
||||
4. **Concurrency first**: leverage codeagent
|
||||
5. **No legacy baggage**: clean-slate design
|
||||
|
||||
---
|
||||
|
||||
**Philosophy**: zero tolerance for complexity—ship the smallest usable solution, like Linus would.
|
||||
114
dev-workflow/agents/dev-plan-generator.md
Normal file
114
dev-workflow/agents/dev-plan-generator.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: dev-plan-generator
|
||||
description: Use this agent when you need to generate a structured development plan document (`dev-plan.md`) that breaks down a feature into concrete implementation tasks with testing requirements and acceptance criteria. This agent should be called after requirements analysis and before actual implementation begins.\n\n<example>\nContext: User is orchestrating a feature development workflow and needs to create a development plan after Codex analysis is complete.\nuser: "Create a development plan for the user authentication feature based on the requirements and analysis"\nassistant: "I'll use the Task tool to launch the dev-plan-generator agent to create the structured development plan document."\n<commentary>\nThe user needs a dev-plan.md document generated from requirements and analysis. Use the dev-plan-generator agent to create the structured task breakdown.\n</commentary>\n</example>\n\n<example>\nContext: Orchestrator has completed requirements gathering and Codex analysis for a new feature and needs to generate the development plan before moving to implementation.\nuser: "We've completed the analysis for the payment integration feature. Generate the development plan."\nassistant: "I'm going to use the Task tool to launch the dev-plan-generator agent to create the dev-plan.md document with task breakdown and testing requirements."\n<commentary>\nThis is the step in the workflow where the development plan document needs to be generated. Use the dev-plan-generator agent to create the structured plan.\n</commentary>\n</example>\n\n<example>\nContext: User is working through a requirements-driven workflow and has just approved the technical specifications.\nuser: "The specs look good. Let's move forward with creating the implementation plan."\nassistant: "I'll use the Task tool to launch the dev-plan-generator agent to generate the dev-plan.md document with the task breakdown."\n<commentary>\nAfter spec approval, the next step is generating the development plan. Use the dev-plan-generator agent to create the structured document.\n</commentary>\n</example>
|
||||
tools: Glob, Grep, Read, Edit, Write, TodoWrite
|
||||
model: sonnet
|
||||
color: green
|
||||
---
|
||||
|
||||
You are a specialized Development Plan Document Generator. Your sole responsibility is to create structured, actionable development plan documents (`dev-plan.md`) that break down features into concrete implementation tasks.
|
||||
|
||||
## Your Role
|
||||
|
||||
You receive context from an orchestrator including:
|
||||
- Feature requirements description
|
||||
- codeagent analysis results (feature highlights, task decomposition, UI detection flag)
|
||||
- Feature name (in kebab-case format)
|
||||
|
||||
Your output is a single file: `./.claude/specs/{feature_name}/dev-plan.md`
|
||||
|
||||
## Document Structure You Must Follow
|
||||
|
||||
```markdown
|
||||
# {Feature Name} - Development Plan
|
||||
|
||||
## Overview
|
||||
[One-sentence description of core functionality]
|
||||
|
||||
## Task Breakdown
|
||||
|
||||
### Task 1: [Task Name]
|
||||
- **ID**: task-1
|
||||
- **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]
|
||||
- **Test Command**: [e.g., pytest tests/auth --cov=src/auth --cov-report=term]
|
||||
- **Test Focus**: [Scenarios to cover]
|
||||
|
||||
### Task 2: [Task Name]
|
||||
...
|
||||
|
||||
(2-5 tasks)
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] Feature point 1
|
||||
- [ ] Feature point 2
|
||||
- [ ] All unit tests pass
|
||||
- [ ] Code coverage ≥90%
|
||||
|
||||
## Technical Notes
|
||||
- [Key technical decisions]
|
||||
- [Constraints to be aware of]
|
||||
```
|
||||
|
||||
## Generation Rules You Must Enforce
|
||||
|
||||
1. **Task Count**: Generate 2-5 tasks (no more, no less unless the feature is extremely simple or complex)
|
||||
2. **Task Requirements**: Each task MUST include:
|
||||
- Clear ID (task-1, task-2, etc.)
|
||||
- Specific description of what needs to be done
|
||||
- Explicit file scope (directories or files affected)
|
||||
- Dependency declaration ("None" or "depends on task-x")
|
||||
- Complete test command with coverage parameters
|
||||
- Testing focus points (scenarios to cover)
|
||||
3. **Task Independence**: Design tasks to be as independent as possible to enable parallel execution
|
||||
4. **Test Commands**: Must include coverage parameters (e.g., `--cov=module --cov-report=term` for pytest, `--coverage` for npm)
|
||||
5. **Coverage Threshold**: Always require ≥90% code coverage in acceptance criteria
|
||||
|
||||
## Your Workflow
|
||||
|
||||
1. **Analyze Input**: Review the requirements description and codeagent analysis results (including `needs_ui` flag if present)
|
||||
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`
|
||||
|
||||
## 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)
|
||||
- [ ] Test commands include coverage parameters
|
||||
- [ ] Dependencies are explicitly stated
|
||||
- [ ] Acceptance criteria includes 90% coverage requirement
|
||||
- [ ] File scope is specific (not vague like "all files")
|
||||
- [ ] Testing focus is concrete (not generic like "test everything")
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
- **Document Only**: You generate documentation. You do NOT execute code, run tests, or modify source files.
|
||||
- **Single Output**: You produce exactly one file: `dev-plan.md` in the correct location
|
||||
- **Path Accuracy**: The path must be `./.claude/specs/{feature_name}/dev-plan.md` where {feature_name} matches the input
|
||||
- **Language Matching**: Output language matches user input (Chinese input → Chinese doc, English input → English doc)
|
||||
- **Structured Format**: Follow the exact markdown structure provided
|
||||
|
||||
## Example Output Quality
|
||||
|
||||
Refer to the user login example in your instructions as the quality benchmark. Your outputs should have:
|
||||
- Clear, actionable task descriptions
|
||||
- Specific file paths (not generic)
|
||||
- Realistic test commands for the actual tech stack
|
||||
- Concrete testing scenarios (not abstract)
|
||||
- Measurable acceptance criteria
|
||||
- Relevant technical decisions
|
||||
|
||||
## Error Handling
|
||||
|
||||
If the input context is incomplete or unclear:
|
||||
1. Request the missing information explicitly
|
||||
2. Do NOT proceed with generating a low-quality document
|
||||
3. Do NOT make up requirements or technical details
|
||||
4. Ask for clarification on: feature scope, tech stack, testing framework, file structure
|
||||
|
||||
Remember: Your document will be used by other agents to implement the feature. Precision and completeness are critical. Every field must be filled with specific, actionable information.
|
||||
131
dev-workflow/commands/dev.md
Normal file
131
dev-workflow/commands/dev.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
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.
|
||||
|
||||
**Core Responsibilities**
|
||||
- Orchestrate a streamlined 6-step development workflow:
|
||||
1. Requirement clarification through targeted questioning
|
||||
2. Technical analysis using codeagent
|
||||
3. Development documentation generation
|
||||
4. Parallel development execution
|
||||
5. Coverage validation (≥90% requirement)
|
||||
6. Completion summary
|
||||
|
||||
**Workflow Execution**
|
||||
- **Step 1: Requirement Clarification**
|
||||
- Use AskUserQuestion to clarify requirements directly
|
||||
- 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
|
||||
|
||||
- **Step 2: codeagent Deep Analysis (Plan Mode Style)**
|
||||
|
||||
Use codeagent Skill to perform deep analysis. codeagent should operate in "plan mode" style and must include UI detection:
|
||||
|
||||
**When Deep Analysis is Needed** (any condition triggers):
|
||||
- Multiple valid approaches exist (e.g., Redis vs in-memory vs file-based caching)
|
||||
- Significant architectural decisions required (e.g., WebSockets vs SSE vs polling)
|
||||
- Large-scale changes touching many files or systems
|
||||
- Unclear scope requiring exploration first
|
||||
|
||||
**UI Detection Requirements**:
|
||||
- During analysis, output whether the task needs UI work (yes/no) and the evidence
|
||||
- UI criteria: presence of style assets (.css, .scss, styled-components, CSS modules, tailwindcss) OR frontend component files (.tsx, .jsx, .vue)
|
||||
|
||||
**What codeagent Does in Analysis Mode**:
|
||||
1. **Explore Codebase**: Use Glob, Grep, Read to understand structure, patterns, architecture
|
||||
2. **Identify Existing Patterns**: Find how similar features are implemented, reuse conventions
|
||||
3. **Evaluate Options**: When multiple approaches exist, list trade-offs (complexity, performance, security, maintainability)
|
||||
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
|
||||
|
||||
**Analysis Output Structure**:
|
||||
```
|
||||
## Context & Constraints
|
||||
[Tech stack, existing patterns, constraints discovered]
|
||||
|
||||
## Codebase Exploration
|
||||
[Key files, modules, patterns found via Glob/Grep/Read]
|
||||
|
||||
## Implementation Options (if multiple approaches)
|
||||
| Option | Pros | Cons | Recommendation |
|
||||
|
||||
## Technical Decisions
|
||||
[API design, data models, architecture choices made]
|
||||
|
||||
## Task Breakdown
|
||||
[2-5 tasks with: ID, description, file scope, dependencies, test command]
|
||||
|
||||
## UI Determination
|
||||
needs_ui: [true/false]
|
||||
evidence: [files and reasoning tied to style + component criteria]
|
||||
```
|
||||
|
||||
**Skip Deep Analysis When**:
|
||||
- Simple, straightforward implementation with obvious approach
|
||||
- Small changes confined to 1-2 files
|
||||
- Clear requirements with single implementation path
|
||||
|
||||
- **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`
|
||||
- Output a brief summary of dev-plan.md:
|
||||
- Number of tasks and their IDs
|
||||
- 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)
|
||||
- Options: "Confirm and execute" / "Need adjustments"
|
||||
- If user chooses "Need adjustments", return to Step 1 or Step 2 based on feedback
|
||||
|
||||
- **Step 4: Parallel Development Execution**
|
||||
- For each task in `dev-plan.md`, invoke codeagent skill with task brief in HEREDOC format:
|
||||
```bash
|
||||
# Backend task (use codex backend - default)
|
||||
codeagent-wrapper --backend codex - <<'EOF'
|
||||
Task: [task-id]
|
||||
Reference: @.claude/specs/{feature_name}/dev-plan.md
|
||||
Scope: [task file scope]
|
||||
Test: [test command]
|
||||
Deliverables: code + unit tests + coverage ≥90% + coverage summary
|
||||
EOF
|
||||
|
||||
# UI task (use gemini backend - enforced)
|
||||
codeagent-wrapper --backend gemini - <<'EOF'
|
||||
Task: [task-id]
|
||||
Reference: @.claude/specs/{feature_name}/dev-plan.md
|
||||
Scope: [task file scope]
|
||||
Test: [test command]
|
||||
Deliverables: code + unit tests + coverage ≥90% + coverage summary
|
||||
EOF
|
||||
```
|
||||
- Execute independent tasks concurrently; serialize conflicting ones; track coverage reports
|
||||
|
||||
- **Step 5: Coverage Validation**
|
||||
- Validate each task’s coverage:
|
||||
- All ≥90% → pass
|
||||
- Any <90% → request more tests (max 2 rounds)
|
||||
|
||||
- **Step 6: Completion Summary**
|
||||
- Provide completed task list, coverage per task, key file changes
|
||||
|
||||
**Error Handling**
|
||||
- codeagent failure: retry once, then log and continue
|
||||
- Insufficient coverage: request more tests (max 2 rounds)
|
||||
- Dependency conflicts: serialize automatically
|
||||
|
||||
**Quality Standards**
|
||||
- Code coverage ≥90%
|
||||
- 2-5 genuinely parallelizable tasks
|
||||
- Documentation must be minimal yet actionable
|
||||
- No verbose implementations; only essential code
|
||||
|
||||
**Communication Style**
|
||||
- Be direct and concise
|
||||
- Report progress at each workflow step
|
||||
- Highlight blockers immediately
|
||||
- Provide actionable next steps when coverage fails
|
||||
- Prioritize speed via parallelization while enforcing coverage validation
|
||||
253
development-essentials/README.md
Normal file
253
development-essentials/README.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# Development Essentials - Core Development Commands
|
||||
|
||||
核心开发命令套件,提供日常开发所需的所有基础命令。无需工作流开销,直接执行开发任务。
|
||||
|
||||
## 📋 命令列表
|
||||
|
||||
### 1. `/ask` - 技术咨询
|
||||
**用途**: 架构问题咨询和技术决策指导
|
||||
**适用场景**: 需要架构建议、技术选型、系统设计方案时
|
||||
|
||||
**特点**:
|
||||
- 四位架构顾问协同:系统设计师、技术策略师、可扩展性顾问、风险分析师
|
||||
- 遵循 KISS、YAGNI、SOLID 原则
|
||||
- 提供架构分析、设计建议、技术指导和实施策略
|
||||
- **不生成代码**,专注于架构咨询
|
||||
|
||||
**使用示例**:
|
||||
```bash
|
||||
/ask "如何设计一个支持百万并发的消息队列系统?"
|
||||
/ask "微服务架构中应该如何处理分布式事务?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `/code` - 功能实现
|
||||
**用途**: 直接实现新功能或特性
|
||||
**适用场景**: 需要快速开发新功能时
|
||||
|
||||
**特点**:
|
||||
- 四位开发专家协同:架构师、实现工程师、集成专家、代码审查员
|
||||
- 渐进式开发,每步验证
|
||||
- 包含完整的实现计划、代码实现、集成指南和测试策略
|
||||
- 生成可运行的高质量代码
|
||||
|
||||
**使用示例**:
|
||||
```bash
|
||||
/code "实现JWT认证中间件"
|
||||
/code "添加用户头像上传功能"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `/debug` - 系统调试
|
||||
**用途**: 使用 UltraThink 方法系统性调试问题
|
||||
**适用场景**: 遇到复杂bug或系统性问题时
|
||||
|
||||
**特点**:
|
||||
- 四位专家协同:架构师、研究员、编码员、测试员
|
||||
- UltraThink 反思阶段:综合所有洞察形成解决方案
|
||||
- 生成5-7个假设,逐步缩减到1-2个最可能的原因
|
||||
- 在实施修复前要求用户确认诊断结果
|
||||
- 证据驱动的系统性问题分析
|
||||
|
||||
**使用示例**:
|
||||
```bash
|
||||
/debug "API响应时间突然增加10倍"
|
||||
/debug "生产环境内存泄漏问题"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. `/test` - 测试策略
|
||||
**用途**: 设计和实现全面的测试策略
|
||||
**适用场景**: 需要为组件或功能编写测试时
|
||||
|
||||
**特点**:
|
||||
- 四位测试专家:测试架构师、单元测试专家、集成测试工程师、质量验证员
|
||||
- 测试金字塔策略(单元/集成/端到端比例)
|
||||
- 提供测试覆盖率分析和优先级建议
|
||||
- 包含 CI/CD 集成计划
|
||||
|
||||
**使用示例**:
|
||||
```bash
|
||||
/test "用户认证模块"
|
||||
/test "支付处理流程"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. `/optimize` - 性能优化
|
||||
**用途**: 识别和优化性能瓶颈
|
||||
**适用场景**: 系统存在性能问题或需要提升性能时
|
||||
|
||||
**特点**:
|
||||
- 四位优化专家:性能分析师、算法工程师、资源管理员、可扩展性架构师
|
||||
- 建立性能基线和量化指标
|
||||
- 优化算法复杂度、内存使用、I/O操作
|
||||
- 设计水平扩展和并发处理方案
|
||||
|
||||
**使用示例**:
|
||||
```bash
|
||||
/optimize "数据库查询性能"
|
||||
/optimize "API响应时间优化到200ms以内"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. `/review` - 代码审查
|
||||
**用途**: 全方位代码质量审查
|
||||
**适用场景**: 需要审查代码质量、安全性和架构设计时
|
||||
|
||||
**特点**:
|
||||
- 四位审查专家:质量审计员、安全分析师、性能审查员、架构评估员
|
||||
- 多维度审查:可读性、安全性、性能、架构设计
|
||||
- 提供优先级分类的改进建议
|
||||
- 包含具体代码示例和重构建议
|
||||
|
||||
**使用示例**:
|
||||
```bash
|
||||
/review "src/auth/middleware.ts"
|
||||
/review "支付模块代码"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. `/bugfix` - Bug修复
|
||||
**用途**: 快速定位和修复Bug
|
||||
**适用场景**: 需要修复已知Bug时
|
||||
|
||||
**特点**:
|
||||
- 专注于快速修复
|
||||
- 包含验证流程
|
||||
- 确保修复不引入新问题
|
||||
|
||||
**使用示例**:
|
||||
```bash
|
||||
/bugfix "登录失败后session未清理"
|
||||
/bugfix "订单状态更新不及时"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. `/refactor` - 代码重构
|
||||
**用途**: 改进代码结构和可维护性
|
||||
**适用场景**: 代码质量下降或需要优化代码结构时
|
||||
|
||||
**特点**:
|
||||
- 保持功能不变
|
||||
- 提升代码质量和可维护性
|
||||
- 遵循设计模式和最佳实践
|
||||
|
||||
**使用示例**:
|
||||
```bash
|
||||
/refactor "将用户管理模块拆分为独立服务"
|
||||
/refactor "优化支付流程代码结构"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. `/docs` - 文档生成
|
||||
**用途**: 生成项目文档和API文档
|
||||
**适用场景**: 需要为代码或API生成文档时
|
||||
|
||||
**特点**:
|
||||
- 自动分析代码结构
|
||||
- 生成清晰的文档
|
||||
- 包含使用示例
|
||||
|
||||
**使用示例**:
|
||||
```bash
|
||||
/docs "API接口文档"
|
||||
/docs "为认证模块生成开发者文档"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. `/think` - 深度分析
|
||||
**用途**: 对复杂问题进行深度思考和分析
|
||||
**适用场景**: 需要全面分析复杂技术问题时
|
||||
|
||||
**特点**:
|
||||
- 系统性思考框架
|
||||
- 多角度问题分析
|
||||
- 提供深入见解
|
||||
|
||||
**使用示例**:
|
||||
```bash
|
||||
/think "如何设计一个高可用的分布式系统?"
|
||||
/think "微服务拆分的最佳实践是什么?"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11. `/enhance-prompt` - 提示词增强 🆕
|
||||
**用途**: 优化和增强用户提供的指令
|
||||
**适用场景**: 需要改进模糊或不清晰的指令时
|
||||
|
||||
**特点**:
|
||||
- 自动分析指令上下文
|
||||
- 消除歧义,提高清晰度
|
||||
- 修正错误并提高具体性
|
||||
- 立即返回增强后的提示词
|
||||
- 保留代码块等特殊格式
|
||||
|
||||
**输出格式**:
|
||||
```
|
||||
### Here is an enhanced version of the original instruction that is more specific and clear:
|
||||
<enhanced-prompt>增强后的提示词</enhanced-prompt>
|
||||
```
|
||||
|
||||
**使用示例**:
|
||||
```bash
|
||||
/enhance-prompt "帮我做一个登录功能"
|
||||
/enhance-prompt "优化一下这个API"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 命令选择指南
|
||||
|
||||
| 需求场景 | 推荐命令 | 说明 |
|
||||
|---------|---------|------|
|
||||
| 需要架构建议 | `/ask` | 不生成代码,专注咨询 |
|
||||
| 实现新功能 | `/code` | 完整的功能实现流程 |
|
||||
| 调试复杂问题 | `/debug` | UltraThink系统性调试 |
|
||||
| 编写测试 | `/test` | 全面的测试策略 |
|
||||
| 性能优化 | `/optimize` | 性能瓶颈分析和优化 |
|
||||
| 代码审查 | `/review` | 多维度质量审查 |
|
||||
| 修复Bug | `/bugfix` | 快速定位和修复 |
|
||||
| 重构代码 | `/refactor` | 提升代码质量 |
|
||||
| 生成文档 | `/docs` | API和开发者文档 |
|
||||
| 深度思考 | `/think` | 复杂问题分析 |
|
||||
| 优化指令 | `/enhance-prompt` | 提示词增强 |
|
||||
|
||||
## 🔧 代理列表
|
||||
|
||||
Development Essentials 模块包含以下专用代理:
|
||||
|
||||
- `code` - 代码实现代理
|
||||
- `bugfix` - Bug修复代理
|
||||
- `bugfix-verify` - Bug验证代理
|
||||
- `code-optimize` - 代码优化代理
|
||||
- `debug` - 调试分析代理
|
||||
- `develop` - 通用开发代理
|
||||
|
||||
## 📖 使用原则
|
||||
|
||||
1. **直接执行**: 无需工作流开销,直接运行命令
|
||||
2. **专注单一任务**: 每个命令聚焦特定开发任务
|
||||
3. **质量优先**: 所有命令都包含质量验证环节
|
||||
4. **实用主义**: KISS/YAGNI/DRY 原则贯穿始终
|
||||
5. **上下文感知**: 自动理解项目结构和编码规范
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [主文档](../README.md) - 项目总览
|
||||
- [BMAD工作流](../docs/BMAD-WORKFLOW.md) - 完整敏捷流程
|
||||
- [Requirements工作流](../docs/REQUIREMENTS-WORKFLOW.md) - 轻量级开发流程
|
||||
- [插件系统](../PLUGIN_README.md) - 插件安装和管理
|
||||
|
||||
---
|
||||
|
||||
**提示**: 这些命令可以单独使用,也可以组合使用。例如:`/code` → `/test` → `/review` → `/optimize` 构成一个完整的开发周期。
|
||||
9
development-essentials/commands/enhance-prompt.md
Normal file
9
development-essentials/commands/enhance-prompt.md
Normal file
@@ -0,0 +1,9 @@
|
||||
`/enhance-prompt <task info>`
|
||||
|
||||
Here is an instruction that I'd like to give you, but it needs to be improved. Rewrite and enhance this instruction to make it clearer, more specific, less ambiguous, and correct any mistakes. Do not use any tools: reply immediately with your answer, even if you're not sure. Consider the context of our conversation history when enhancing the prompt. If there is code in triple backticks (```) consider whether it is a code sample and should remain unchanged.Reply with the following format:
|
||||
|
||||
### BEGIN RESPONSE
|
||||
|
||||
<enhanced-prompt>enhanced prompt goes here</enhanced-prompt>
|
||||
|
||||
### END RESPONSE
|
||||
@@ -1,315 +0,0 @@
|
||||
# Advanced AI Agents Guide
|
||||
|
||||
> GPT-5 deep reasoning integration for complex analysis and architectural decisions
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
The Advanced AI Agents plugin provides access to GPT-5's deep reasoning capabilities through the `gpt5` agent, designed for complex problem-solving that requires multi-step thinking and comprehensive analysis.
|
||||
|
||||
## 🤖 GPT-5 Agent
|
||||
|
||||
### Capabilities
|
||||
|
||||
The `gpt5` agent excels at:
|
||||
|
||||
- **Architectural Analysis**: Evaluating system designs and scalability concerns
|
||||
- **Strategic Planning**: Breaking down complex initiatives into actionable plans
|
||||
- **Trade-off Analysis**: Comparing multiple approaches with detailed pros/cons
|
||||
- **Problem Decomposition**: Breaking complex problems into manageable components
|
||||
- **Deep Reasoning**: Multi-step logical analysis for non-obvious solutions
|
||||
- **Technology Evaluation**: Assessing technologies, frameworks, and tools
|
||||
|
||||
### When to Use
|
||||
|
||||
**Use GPT-5 agent** when:
|
||||
- Problem requires deep, multi-step reasoning
|
||||
- Multiple solution approaches need evaluation
|
||||
- Architectural decisions have long-term impact
|
||||
- Trade-offs are complex and multifaceted
|
||||
- Standard agents provide insufficient depth
|
||||
|
||||
**Use standard agents** when:
|
||||
- Task is straightforward implementation
|
||||
- Requirements are clear and well-defined
|
||||
- Quick turnaround is priority
|
||||
- Problem is domain-specific (code, tests, etc.)
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
### Via `/think` Command
|
||||
|
||||
The easiest way to access GPT-5:
|
||||
|
||||
```bash
|
||||
/think "Analyze scalability bottlenecks in current microservices architecture"
|
||||
/think "Evaluate migration strategy from monolith to microservices"
|
||||
/think "Design data synchronization approach for offline-first mobile app"
|
||||
```
|
||||
|
||||
### Direct Agent Invocation
|
||||
|
||||
For advanced usage:
|
||||
|
||||
```bash
|
||||
# Use @gpt5 to invoke the agent directly
|
||||
@gpt5 "Complex architectural question or analysis request"
|
||||
```
|
||||
|
||||
## 💡 Example Use Cases
|
||||
|
||||
### 1. Architecture Evaluation
|
||||
|
||||
```bash
|
||||
/think "Current system uses REST API with polling for real-time updates.
|
||||
Evaluate whether to migrate to WebSocket, Server-Sent Events, or GraphQL
|
||||
subscriptions. Consider: team experience, existing infrastructure, client
|
||||
support, scalability, and implementation effort."
|
||||
```
|
||||
|
||||
**GPT-5 provides**:
|
||||
- Detailed analysis of each option
|
||||
- Pros and cons for your specific context
|
||||
- Migration complexity assessment
|
||||
- Performance implications
|
||||
- Recommended approach with justification
|
||||
|
||||
### 2. Migration Strategy
|
||||
|
||||
```bash
|
||||
/think "Plan migration from PostgreSQL to multi-region distributed database.
|
||||
System has 50M users, 200M rows, 1000 req/sec. Must maintain 99.9% uptime.
|
||||
What's the safest migration path?"
|
||||
```
|
||||
|
||||
**GPT-5 provides**:
|
||||
- Step-by-step migration plan
|
||||
- Risk assessment for each phase
|
||||
- Rollback strategies
|
||||
- Data consistency approaches
|
||||
- Timeline estimation
|
||||
|
||||
### 3. Problem Decomposition
|
||||
|
||||
```bash
|
||||
/think "Design a recommendation engine that learns user preferences, handles
|
||||
cold start, provides explainable results, and scales to 10M users. Break this
|
||||
down into implementation phases with clear milestones."
|
||||
```
|
||||
|
||||
**GPT-5 provides**:
|
||||
- Problem breakdown into components
|
||||
- Phased implementation plan
|
||||
- Technical approach for each phase
|
||||
- Dependencies between phases
|
||||
- Success criteria and metrics
|
||||
|
||||
### 4. Technology Selection
|
||||
|
||||
```bash
|
||||
/think "Choosing between Redis, Memcached, and Hazelcast for distributed
|
||||
caching. System needs: persistence, pub/sub, clustering, and complex data
|
||||
structures. Existing stack: Java, Kubernetes, AWS."
|
||||
```
|
||||
|
||||
**GPT-5 provides**:
|
||||
- Comparison matrix across requirements
|
||||
- Integration considerations
|
||||
- Operational complexity analysis
|
||||
- Cost implications
|
||||
- Recommendation with rationale
|
||||
|
||||
### 5. Performance Optimization
|
||||
|
||||
```bash
|
||||
/think "API response time increased from 100ms to 800ms after scaling from
|
||||
100 to 10,000 users. Database queries look optimized. What are the likely
|
||||
bottlenecks and systematic approach to identify them?"
|
||||
```
|
||||
|
||||
**GPT-5 provides**:
|
||||
- Hypothesis generation (N+1 queries, connection pooling, etc.)
|
||||
- Systematic debugging approach
|
||||
- Profiling strategy
|
||||
- Likely root causes ranked by probability
|
||||
- Optimization recommendations
|
||||
|
||||
## 🎨 Integration with BMAD
|
||||
|
||||
### Enhanced Code Review
|
||||
|
||||
BMAD's `bmad-review` agent can optionally use GPT-5 for deeper analysis:
|
||||
|
||||
**Configuration**:
|
||||
```bash
|
||||
# Enable enhanced review mode (via environment or BMAD config)
|
||||
BMAD_REVIEW_MODE=enhanced /bmad-pilot "feature description"
|
||||
```
|
||||
|
||||
**What changes**:
|
||||
- Standard review: Fast, focuses on code quality and obvious issues
|
||||
- Enhanced review: Deep analysis including:
|
||||
- Architectural impact
|
||||
- Security implications
|
||||
- Performance considerations
|
||||
- Scalability concerns
|
||||
- Design pattern appropriateness
|
||||
|
||||
### Architecture Phase Support
|
||||
|
||||
Use `/think` during BMAD architecture phase:
|
||||
|
||||
```bash
|
||||
# Start BMAD workflow
|
||||
/bmad-pilot "E-commerce platform with real-time inventory"
|
||||
|
||||
# During Architecture phase, get deep analysis
|
||||
/think "Evaluate architecture approaches for real-time inventory
|
||||
synchronization across warehouses, online store, and mobile apps"
|
||||
|
||||
# Continue with BMAD using insights
|
||||
```
|
||||
|
||||
## 📋 Best Practices
|
||||
|
||||
### 1. Provide Complete Context
|
||||
|
||||
**❌ Insufficient**:
|
||||
```bash
|
||||
/think "Should we use microservices?"
|
||||
```
|
||||
|
||||
**✅ Complete**:
|
||||
```bash
|
||||
/think "Current monolith: 100K LOC, 8 developers, 50K users, 200ms avg
|
||||
response time. Pain points: slow deployments (1hr), difficult to scale
|
||||
components independently. Should we migrate to microservices? What's the
|
||||
ROI and risk?"
|
||||
```
|
||||
|
||||
### 2. Ask Specific Questions
|
||||
|
||||
**❌ Too broad**:
|
||||
```bash
|
||||
/think "How to build a scalable system?"
|
||||
```
|
||||
|
||||
**✅ Specific**:
|
||||
```bash
|
||||
/think "Current system handles 1K req/sec. Need to scale to 10K. Bottleneck
|
||||
is database writes. Evaluate: sharding, read replicas, CQRS, or caching.
|
||||
Database: PostgreSQL, stack: Node.js, deployment: Kubernetes."
|
||||
```
|
||||
|
||||
### 3. Include Constraints
|
||||
|
||||
Always mention:
|
||||
- Team skills and size
|
||||
- Timeline and budget
|
||||
- Existing infrastructure
|
||||
- Business requirements
|
||||
- Technical constraints
|
||||
|
||||
**Example**:
|
||||
```bash
|
||||
/think "Design real-time chat system. Constraints: team of 3 backend
|
||||
developers (Node.js), 6-month timeline, AWS deployment, must integrate
|
||||
with existing REST API, budget for managed services OK."
|
||||
```
|
||||
|
||||
### 4. Request Specific Outputs
|
||||
|
||||
Tell GPT-5 what format you need:
|
||||
|
||||
```bash
|
||||
/think "Compare Kafka vs RabbitMQ for event streaming.
|
||||
Provide: comparison table, recommendation, migration complexity from current
|
||||
RabbitMQ setup, and estimated effort in developer-weeks."
|
||||
```
|
||||
|
||||
### 5. Iterate and Refine
|
||||
|
||||
Follow up for deeper analysis:
|
||||
|
||||
```bash
|
||||
# Initial question
|
||||
/think "Evaluate caching strategies for user profile API"
|
||||
|
||||
# Follow-up based on response
|
||||
/think "You recommended Redis with write-through caching. How to handle
|
||||
cache invalidation when user updates profile from mobile app?"
|
||||
```
|
||||
|
||||
## 🔧 Technical Details
|
||||
|
||||
### Sequential Thinking
|
||||
|
||||
GPT-5 agent uses sequential thinking for complex problems:
|
||||
|
||||
1. **Problem Understanding**: Clarify requirements and constraints
|
||||
2. **Hypothesis Generation**: Identify possible solutions
|
||||
3. **Analysis**: Evaluate each option systematically
|
||||
4. **Trade-off Assessment**: Compare pros/cons
|
||||
5. **Recommendation**: Provide justified conclusion
|
||||
|
||||
### Reasoning Transparency
|
||||
|
||||
GPT-5 shows its thinking process:
|
||||
- Assumptions made
|
||||
- Factors considered
|
||||
- Why certain options were eliminated
|
||||
- Confidence level in recommendations
|
||||
|
||||
## 🎯 Comparison: GPT-5 vs Standard Agents
|
||||
|
||||
| Aspect | GPT-5 Agent | Standard Agents |
|
||||
|--------|-------------|-----------------|
|
||||
| **Depth** | Deep, multi-step reasoning | Focused, domain-specific |
|
||||
| **Speed** | Slower (comprehensive analysis) | Faster (direct implementation) |
|
||||
| **Use Case** | Strategic decisions, architecture | Implementation, coding, testing |
|
||||
| **Output** | Analysis, recommendations, plans | Code, tests, documentation |
|
||||
| **Best For** | Complex problems, trade-offs | Clear tasks, defined scope |
|
||||
| **Invocation** | `/think` or `@gpt5` | `/code`, `/test`, etc. |
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[BMAD Workflow](BMAD-WORKFLOW.md)** - Integration with full agile workflow
|
||||
- **[Development Commands](DEVELOPMENT-COMMANDS.md)** - Standard command reference
|
||||
- **[Quick Start Guide](QUICK-START.md)** - Get started quickly
|
||||
|
||||
## 💡 Advanced Patterns
|
||||
|
||||
### Pre-Implementation Analysis
|
||||
|
||||
```bash
|
||||
# 1. Deep analysis with GPT-5
|
||||
/think "Design approach for X with constraints Y and Z"
|
||||
|
||||
# 2. Use analysis in BMAD workflow
|
||||
/bmad-pilot "Implement X based on approach from analysis"
|
||||
```
|
||||
|
||||
### Architecture Validation
|
||||
|
||||
```bash
|
||||
# 1. Get initial architecture from BMAD
|
||||
/bmad-pilot "Feature X" # Generates 02-system-architecture.md
|
||||
|
||||
# 2. Validate with GPT-5
|
||||
/think "Review architecture in .claude/specs/feature-x/02-system-architecture.md
|
||||
Evaluate for scalability, security, and maintainability"
|
||||
|
||||
# 3. Refine architecture based on feedback
|
||||
```
|
||||
|
||||
### Decision Documentation
|
||||
|
||||
```bash
|
||||
# Use GPT-5 to document architectural decisions
|
||||
/think "Document decision to use Event Sourcing for order management.
|
||||
Include: context, options considered, decision rationale, consequences,
|
||||
and format as Architecture Decision Record (ADR)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Advanced AI Agents** - Deep reasoning for complex problems that require comprehensive analysis.
|
||||
407
docs/CODEAGENT-WRAPPER.md
Normal file
407
docs/CODEAGENT-WRAPPER.md
Normal file
@@ -0,0 +1,407 @@
|
||||
# Codeagent-Wrapper User Guide
|
||||
|
||||
Multi-backend AI code execution wrapper supporting Codex, Claude, and Gemini.
|
||||
|
||||
## Overview
|
||||
|
||||
`codeagent-wrapper` is a Go-based CLI tool that provides a unified interface to multiple AI coding backends. It handles:
|
||||
- Multi-backend execution (Codex, Claude, Gemini)
|
||||
- JSON stream parsing and output formatting
|
||||
- Session management and resumption
|
||||
- Parallel task execution with dependency resolution
|
||||
- Timeout handling and signal forwarding
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/cexll/myclaude.git
|
||||
cd myclaude
|
||||
|
||||
# Install via install.py (includes binary compilation)
|
||||
python3 install.py --module dev
|
||||
|
||||
# Or manual installation
|
||||
cd codeagent-wrapper
|
||||
go build -o ~/.claude/bin/codeagent-wrapper
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# Simple task (default: codex backend)
|
||||
codeagent-wrapper "explain @src/main.go"
|
||||
|
||||
# With backend selection
|
||||
codeagent-wrapper --backend claude "refactor @utils.ts"
|
||||
|
||||
# With HEREDOC (recommended for complex tasks)
|
||||
codeagent-wrapper --backend gemini - <<'EOF'
|
||||
Implement user authentication:
|
||||
- JWT tokens
|
||||
- Password hashing with bcrypt
|
||||
- Session management
|
||||
EOF
|
||||
```
|
||||
|
||||
### Backend Selection
|
||||
|
||||
| Backend | Command | Best For |
|
||||
|---------|---------|----------|
|
||||
| **Codex** | `--backend codex` | General code tasks (default) |
|
||||
| **Claude** | `--backend claude` | Complex reasoning, architecture |
|
||||
| **Gemini** | `--backend gemini` | Fast iteration, prototyping |
|
||||
|
||||
## Core Features
|
||||
|
||||
### 1. Multi-Backend Support
|
||||
|
||||
```bash
|
||||
# Codex (default)
|
||||
codeagent-wrapper "add logging to @app.js"
|
||||
|
||||
# Claude for architecture decisions
|
||||
codeagent-wrapper --backend claude - <<'EOF'
|
||||
Design a microservices architecture for e-commerce:
|
||||
- Service boundaries
|
||||
- Communication patterns
|
||||
- Data consistency strategy
|
||||
EOF
|
||||
|
||||
# Gemini for quick prototypes
|
||||
codeagent-wrapper --backend gemini "create React component for user profile"
|
||||
```
|
||||
|
||||
### 2. File References with @ Syntax
|
||||
|
||||
```bash
|
||||
# Single file
|
||||
codeagent-wrapper "optimize @src/utils.ts"
|
||||
|
||||
# Multiple files
|
||||
codeagent-wrapper "refactor @src/auth.ts and @src/middleware.ts"
|
||||
|
||||
# Entire directory
|
||||
codeagent-wrapper "analyze @src for security issues"
|
||||
```
|
||||
|
||||
### 3. Session Management
|
||||
|
||||
```bash
|
||||
# First task
|
||||
codeagent-wrapper "add validation to user form"
|
||||
# Output includes: SESSION_ID: 019a7247-ac9d-71f3-89e2-a823dbd8fd14
|
||||
|
||||
# Resume session
|
||||
codeagent-wrapper resume 019a7247-ac9d-71f3-89e2-a823dbd8fd14 - <<'EOF'
|
||||
Now add error messages for each validation rule
|
||||
EOF
|
||||
```
|
||||
|
||||
### 4. Parallel Execution
|
||||
|
||||
Execute multiple tasks concurrently with dependency management:
|
||||
|
||||
```bash
|
||||
codeagent-wrapper --parallel <<'EOF'
|
||||
---TASK---
|
||||
id: backend_1701234567
|
||||
workdir: /project/backend
|
||||
---CONTENT---
|
||||
implement /api/users endpoints with CRUD operations
|
||||
|
||||
---TASK---
|
||||
id: frontend_1701234568
|
||||
workdir: /project/frontend
|
||||
---CONTENT---
|
||||
build Users page consuming /api/users
|
||||
|
||||
---TASK---
|
||||
id: tests_1701234569
|
||||
workdir: /project/tests
|
||||
dependencies: backend_1701234567, frontend_1701234568
|
||||
---CONTENT---
|
||||
add integration tests for user management flow
|
||||
EOF
|
||||
```
|
||||
|
||||
**Parallel Task Format:**
|
||||
- `---TASK---` - Starts task block
|
||||
- `id: <unique_id>` - Required, use `<feature>_<timestamp>` format
|
||||
- `workdir: <path>` - Optional, defaults to current directory
|
||||
- `dependencies: <id1>, <id2>` - Optional, comma-separated task IDs
|
||||
- `---CONTENT---` - Separates metadata from task content
|
||||
|
||||
**Features:**
|
||||
- Automatic topological sorting
|
||||
- Unlimited concurrency for independent tasks
|
||||
- Error isolation (failures don't stop other tasks)
|
||||
- Dependency blocking (skip if parent fails)
|
||||
|
||||
### 5. Working Directory
|
||||
|
||||
```bash
|
||||
# Execute in specific directory
|
||||
codeagent-wrapper "run tests" /path/to/project
|
||||
|
||||
# With backend selection
|
||||
codeagent-wrapper --backend claude "analyze code" /project/backend
|
||||
|
||||
# With HEREDOC
|
||||
codeagent-wrapper - /path/to/project <<'EOF'
|
||||
refactor database layer
|
||||
EOF
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Timeout Control
|
||||
|
||||
```bash
|
||||
# Set custom timeout (1 hour = 3600000ms)
|
||||
CODEX_TIMEOUT=3600000 codeagent-wrapper "long running task"
|
||||
|
||||
# Default timeout: 7200000ms (2 hours)
|
||||
```
|
||||
|
||||
**Timeout behavior:**
|
||||
- Sends SIGTERM to backend process
|
||||
- Waits 5 seconds
|
||||
- Sends SIGKILL if process doesn't exit
|
||||
- Returns exit code 124 (consistent with GNU timeout)
|
||||
|
||||
### Complex Multi-line Tasks
|
||||
|
||||
Use HEREDOC to avoid shell escaping issues:
|
||||
|
||||
```bash
|
||||
codeagent-wrapper - <<'EOF'
|
||||
Refactor authentication system:
|
||||
|
||||
Current issues:
|
||||
- Password stored as plain text
|
||||
- No rate limiting on login
|
||||
- Sessions don't expire
|
||||
|
||||
Requirements:
|
||||
1. Hash passwords with bcrypt
|
||||
2. Add rate limiting (5 attempts/15min)
|
||||
3. Session expiry after 24h
|
||||
4. Add refresh token mechanism
|
||||
|
||||
Files to modify:
|
||||
- @src/auth/login.ts
|
||||
- @src/middleware/rateLimit.ts
|
||||
- @config/session.ts
|
||||
EOF
|
||||
```
|
||||
|
||||
### Backend-Specific Features
|
||||
|
||||
**Codex:**
|
||||
```bash
|
||||
# Best for code editing and refactoring
|
||||
codeagent-wrapper --backend codex - <<'EOF'
|
||||
extract duplicate code in @src into reusable helpers
|
||||
EOF
|
||||
```
|
||||
|
||||
**Claude:**
|
||||
```bash
|
||||
# Best for complex reasoning
|
||||
codeagent-wrapper --backend claude - <<'EOF'
|
||||
review @src/payment/processor.ts for:
|
||||
- Race conditions
|
||||
- Edge cases
|
||||
- Security vulnerabilities
|
||||
EOF
|
||||
```
|
||||
|
||||
**Gemini:**
|
||||
```bash
|
||||
# Best for fast iteration
|
||||
codeagent-wrapper --backend gemini "add TypeScript types to @api.js"
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
Standard output includes parsed agent messages and session ID:
|
||||
|
||||
```
|
||||
Agent response text here...
|
||||
Implementation details...
|
||||
|
||||
---
|
||||
SESSION_ID: 019a7247-ac9d-71f3-89e2-a823dbd8fd14
|
||||
```
|
||||
|
||||
Error output (stderr):
|
||||
```
|
||||
ERROR: Error message details
|
||||
```
|
||||
|
||||
Parallel execution output:
|
||||
```
|
||||
=== Parallel Execution Summary ===
|
||||
Total: 3 | Success: 2 | Failed: 1
|
||||
|
||||
--- Task: backend_1701234567 ---
|
||||
Status: SUCCESS
|
||||
Session: 019a7247-ac9d-71f3-89e2-a823dbd8fd14
|
||||
|
||||
Implementation complete...
|
||||
|
||||
--- Task: frontend_1701234568 ---
|
||||
Status: SUCCESS
|
||||
Session: 019a7248-ac9d-71f3-89e2-a823dbd8fd14
|
||||
|
||||
UI components created...
|
||||
|
||||
--- Task: tests_1701234569 ---
|
||||
Status: FAILED (exit code 1)
|
||||
Error: dependency backend_1701234567 failed
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Success |
|
||||
| 1 | General error (missing args, no output) |
|
||||
| 124 | Timeout |
|
||||
| 127 | Backend command not found |
|
||||
| 130 | Interrupted (Ctrl+C) |
|
||||
| * | Passthrough from backend process |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CODEX_TIMEOUT` | 7200000 | Timeout in milliseconds |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Backend not found:**
|
||||
```bash
|
||||
# Ensure backend CLI is installed
|
||||
which codex
|
||||
which claude
|
||||
which gemini
|
||||
|
||||
# Check PATH
|
||||
echo $PATH
|
||||
```
|
||||
|
||||
**Timeout too short:**
|
||||
```bash
|
||||
# Increase timeout to 4 hours
|
||||
CODEX_TIMEOUT=14400000 codeagent-wrapper "complex task"
|
||||
```
|
||||
|
||||
**Session ID not found:**
|
||||
```bash
|
||||
# List recent sessions (backend-specific)
|
||||
codex history
|
||||
|
||||
# Ensure session ID is copied correctly
|
||||
codeagent-wrapper resume <session_id> "continue task"
|
||||
```
|
||||
|
||||
**Parallel tasks not running:**
|
||||
```bash
|
||||
# Check task format
|
||||
# Ensure ---TASK--- and ---CONTENT--- delimiters are correct
|
||||
# Verify task IDs are unique
|
||||
# Check dependencies reference existing task IDs
|
||||
```
|
||||
|
||||
## Integration with Claude Code
|
||||
|
||||
Use via the `codeagent` skill:
|
||||
|
||||
```bash
|
||||
# In Claude Code conversation
|
||||
User: Use codeagent to implement authentication
|
||||
|
||||
# Claude will execute:
|
||||
codeagent-wrapper --backend codex - <<'EOF'
|
||||
implement JWT authentication in @src/auth
|
||||
EOF
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Use parallel execution** for independent tasks
|
||||
2. **Choose the right backend** for the task type
|
||||
3. **Keep working directory specific** to reduce context
|
||||
4. **Resume sessions** for multi-step workflows
|
||||
5. **Use @ syntax** to minimize file content in prompts
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **HEREDOC for complex tasks** - Avoid shell escaping nightmares
|
||||
2. **Descriptive task IDs** - Use `<feature>_<timestamp>` format
|
||||
3. **Absolute paths** - Avoid relative path confusion
|
||||
4. **Session resumption** - Continue conversations with context
|
||||
5. **Timeout tuning** - Set appropriate timeouts for task complexity
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Code Review
|
||||
|
||||
```bash
|
||||
codeagent-wrapper --backend claude - <<'EOF'
|
||||
Review @src/payment/stripe.ts for:
|
||||
1. Security issues (API key handling, input validation)
|
||||
2. Error handling (network failures, API errors)
|
||||
3. Edge cases (duplicate charges, partial refunds)
|
||||
4. Code quality (naming, structure, comments)
|
||||
EOF
|
||||
```
|
||||
|
||||
### Example 2: Refactoring
|
||||
|
||||
```bash
|
||||
codeagent-wrapper --backend codex - <<'EOF'
|
||||
Refactor @src/utils:
|
||||
- Extract duplicate code into helpers
|
||||
- Add TypeScript types
|
||||
- Improve function naming
|
||||
- Add JSDoc comments
|
||||
EOF
|
||||
```
|
||||
|
||||
### Example 3: Full-Stack Feature
|
||||
|
||||
```bash
|
||||
codeagent-wrapper --parallel <<'EOF'
|
||||
---TASK---
|
||||
id: api_1701234567
|
||||
workdir: /project/backend
|
||||
---CONTENT---
|
||||
implement /api/notifications endpoints with WebSocket support
|
||||
|
||||
---TASK---
|
||||
id: ui_1701234568
|
||||
workdir: /project/frontend
|
||||
dependencies: api_1701234567
|
||||
---CONTENT---
|
||||
build Notifications component with real-time updates
|
||||
|
||||
---TASK---
|
||||
id: tests_1701234569
|
||||
workdir: /project
|
||||
dependencies: api_1701234567, ui_1701234568
|
||||
---CONTENT---
|
||||
add E2E tests for notification flow
|
||||
EOF
|
||||
```
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Codex CLI Documentation](https://codex.docs)
|
||||
- [Claude CLI Documentation](https://claude.ai/docs)
|
||||
- [Gemini CLI Documentation](https://ai.google.dev/docs)
|
||||
- [Architecture Overview](./architecture.md)
|
||||
197
docs/HOOKS.md
Normal file
197
docs/HOOKS.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Claude Code Hooks Guide
|
||||
|
||||
Hooks are shell scripts or commands that execute in response to Claude Code events.
|
||||
|
||||
## Available Hook Types
|
||||
|
||||
### 1. UserPromptSubmit
|
||||
Runs after user submits a prompt, before Claude processes it.
|
||||
|
||||
**Use cases:**
|
||||
- Auto-activate skills based on keywords
|
||||
- Add context injection
|
||||
- Log user requests
|
||||
|
||||
### 2. PostToolUse
|
||||
Runs after Claude uses a tool.
|
||||
|
||||
**Use cases:**
|
||||
- Validate tool outputs
|
||||
- Run additional checks (linting, formatting)
|
||||
- Log tool usage
|
||||
|
||||
### 3. Stop
|
||||
Runs when Claude Code session ends.
|
||||
|
||||
**Use cases:**
|
||||
- Cleanup temporary files
|
||||
- Generate session reports
|
||||
- Commit changes automatically
|
||||
|
||||
## Configuration
|
||||
|
||||
Hooks are configured in `.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/hooks/skill-activation-prompt.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/hooks/post-tool-check.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Creating Custom Hooks
|
||||
|
||||
### Example: Pre-Commit Hook
|
||||
|
||||
**File:** `hooks/pre-commit.sh`
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Get staged files
|
||||
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
||||
|
||||
# Run tests on Go files
|
||||
GO_FILES=$(echo "$STAGED_FILES" | grep '\.go$' || true)
|
||||
if [ -n "$GO_FILES" ]; then
|
||||
go test ./... -short || exit 1
|
||||
fi
|
||||
|
||||
# Validate JSON files
|
||||
JSON_FILES=$(echo "$STAGED_FILES" | grep '\.json$' || true)
|
||||
if [ -n "$JSON_FILES" ]; then
|
||||
for file in $JSON_FILES; do
|
||||
jq empty "$file" || exit 1
|
||||
done
|
||||
fi
|
||||
|
||||
echo "✅ Pre-commit checks passed"
|
||||
```
|
||||
|
||||
**Register in settings.json:**
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/hooks/pre-commit.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Auto-Format Hook
|
||||
|
||||
**File:** `hooks/auto-format.sh`
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Format Go files
|
||||
find . -name "*.go" -exec gofmt -w {} \;
|
||||
|
||||
# Format JSON files
|
||||
find . -name "*.json" -exec jq --indent 2 . {} \; -exec mv {} {}.tmp \; -exec mv {}.tmp {} \;
|
||||
|
||||
echo "✅ Files formatted"
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Hooks have access to:
|
||||
- `$CLAUDE_PROJECT_DIR` - Project root directory
|
||||
- `$PWD` - Current working directory
|
||||
- All shell environment variables
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep hooks fast** - Slow hooks block Claude Code
|
||||
2. **Handle errors gracefully** - Return non-zero on failure
|
||||
3. **Use absolute paths** - Reference `$CLAUDE_PROJECT_DIR`
|
||||
4. **Make scripts executable** - `chmod +x hooks/script.sh`
|
||||
5. **Test independently** - Run hooks manually first
|
||||
6. **Document behavior** - Add comments explaining logic
|
||||
|
||||
## Debugging Hooks
|
||||
|
||||
Enable verbose logging:
|
||||
|
||||
```bash
|
||||
# Add to your hook
|
||||
set -x # Print commands
|
||||
set -e # Exit on error
|
||||
```
|
||||
|
||||
Test manually:
|
||||
|
||||
```bash
|
||||
cd /path/to/project
|
||||
./hooks/your-hook.sh
|
||||
echo $? # Check exit code
|
||||
```
|
||||
|
||||
## Built-in Hooks
|
||||
|
||||
This repository includes:
|
||||
|
||||
| Hook | File | Purpose |
|
||||
|------|------|---------|
|
||||
| Skill Activation | `skill-activation-prompt.sh` | Auto-suggest skills |
|
||||
| Pre-commit | `pre-commit.sh` | Code quality checks |
|
||||
|
||||
## Disabling Hooks
|
||||
|
||||
Remove hook configuration from `.claude/settings.json` or set empty array:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Hook not running?**
|
||||
- Check `.claude/settings.json` syntax
|
||||
- Verify script is executable: `ls -l hooks/`
|
||||
- Check script path is correct
|
||||
|
||||
**Hook failing silently?**
|
||||
- Add `set -e` to script
|
||||
- Check exit codes: `echo $?`
|
||||
- Add logging: `echo "debug" >> /tmp/hook.log`
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Claude Code Hooks Documentation](https://docs.anthropic.com/claude-code/hooks)
|
||||
- [Bash Scripting Guide](https://www.gnu.org/software/bash/manual/)
|
||||
@@ -104,7 +104,7 @@ This repository provides 4 ready-to-use Claude Code plugins that can be installe
|
||||
|
||||
```bash
|
||||
# Install from GitHub repository
|
||||
/plugin github.com/cexll/myclaude
|
||||
/plugin marketplace add cexll/myclaude
|
||||
```
|
||||
|
||||
This will present all available plugins from the repository.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
```bash
|
||||
# Install everything with one command
|
||||
/plugin github.com/cexll/myclaude
|
||||
/plugin marketplace add cexll/myclaude
|
||||
```
|
||||
|
||||
### Option 2: Make Install
|
||||
|
||||
12
hooks/hooks-config.json
Normal file
12
hooks/hooks-config.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/hooks/skill-activation-prompt.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
60
hooks/pre-commit.sh
Executable file
60
hooks/pre-commit.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
# Example pre-commit hook
|
||||
# This hook runs before git commit to validate code quality
|
||||
|
||||
set -e
|
||||
|
||||
# Get staged files
|
||||
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
||||
|
||||
if [ -z "$STAGED_FILES" ]; then
|
||||
echo "No files to validate"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Running pre-commit checks..."
|
||||
|
||||
# Check Go files
|
||||
GO_FILES=$(echo "$STAGED_FILES" | grep '\.go$' || true)
|
||||
if [ -n "$GO_FILES" ]; then
|
||||
echo "Checking Go files..."
|
||||
|
||||
# Format check
|
||||
gofmt -l $GO_FILES | while read -r file; do
|
||||
if [ -n "$file" ]; then
|
||||
echo "❌ $file needs formatting (run: gofmt -w $file)"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Run tests
|
||||
if command -v go &> /dev/null; then
|
||||
echo "Running go tests..."
|
||||
go test ./... -short || {
|
||||
echo "❌ Tests failed"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check JSON files
|
||||
JSON_FILES=$(echo "$STAGED_FILES" | grep '\.json$' || true)
|
||||
if [ -n "$JSON_FILES" ]; then
|
||||
echo "Validating JSON files..."
|
||||
for file in $JSON_FILES; do
|
||||
if ! jq empty "$file" 2>/dev/null; then
|
||||
echo "❌ Invalid JSON: $file"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Check Markdown files
|
||||
MD_FILES=$(echo "$STAGED_FILES" | grep '\.md$' || true)
|
||||
if [ -n "$MD_FILES" ]; then
|
||||
echo "Checking markdown files..."
|
||||
# Add markdown linting if needed
|
||||
fi
|
||||
|
||||
echo "✅ All pre-commit checks passed"
|
||||
exit 0
|
||||
85
hooks/skill-activation-prompt.js
Normal file
85
hooks/skill-activation-prompt.js
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
function readInput() {
|
||||
const raw = fs.readFileSync(0, "utf8").trim();
|
||||
if (!raw) return {};
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (_err) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function extractPrompt(payload) {
|
||||
return (
|
||||
payload.prompt ||
|
||||
payload.text ||
|
||||
payload.userPrompt ||
|
||||
(payload.data && payload.data.prompt) ||
|
||||
""
|
||||
).toString();
|
||||
}
|
||||
|
||||
function loadRules() {
|
||||
const rulesPath = path.resolve(__dirname, "../skills/skill-rules.json");
|
||||
try {
|
||||
const file = fs.readFileSync(rulesPath, "utf8");
|
||||
return JSON.parse(file);
|
||||
} catch (_err) {
|
||||
return { skills: {} };
|
||||
}
|
||||
}
|
||||
|
||||
function matchSkill(prompt, rule, skillName) {
|
||||
const triggers = (rule && rule.promptTriggers) || {};
|
||||
const keywords = [...(triggers.keywords || []), skillName].filter(Boolean);
|
||||
const patterns = triggers.intentPatterns || [];
|
||||
const promptLower = prompt.toLowerCase();
|
||||
|
||||
const keyword = keywords.find((k) => promptLower.includes(k.toLowerCase()));
|
||||
if (keyword) {
|
||||
return `命中关键词 "${keyword}"`;
|
||||
}
|
||||
|
||||
for (const pattern of patterns) {
|
||||
try {
|
||||
if (new RegExp(pattern, "i").test(prompt)) {
|
||||
return `命中模式 /${pattern}/`;
|
||||
}
|
||||
} catch (_err) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const payload = readInput();
|
||||
const prompt = extractPrompt(payload);
|
||||
if (!prompt.trim()) {
|
||||
console.log(JSON.stringify({ suggestedSkills: [] }, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
const rules = loadRules();
|
||||
const suggestions = [];
|
||||
|
||||
for (const [name, rule] of Object.entries(rules.skills || {})) {
|
||||
const matchReason = matchSkill(prompt, rule, name);
|
||||
if (matchReason) {
|
||||
suggestions.push({
|
||||
skill: name,
|
||||
enforcement: rule.enforcement || "suggest",
|
||||
priority: rule.priority || "normal",
|
||||
reason: matchReason
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ suggestedSkills: suggestions }, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
12
hooks/skill-activation-prompt.sh
Executable file
12
hooks/skill-activation-prompt.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCRIPT="$SCRIPT_DIR/skill-activation-prompt.js"
|
||||
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
node "$SCRIPT" "$@" || true
|
||||
else
|
||||
echo '{"suggestedSkills":[],"meta":{"warning":"node not found"}}'
|
||||
fi
|
||||
|
||||
exit 0
|
||||
77
hooks/test-skill-activation.sh
Executable file
77
hooks/test-skill-activation.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Simple test runner for skill-activation-prompt hook.
|
||||
# Each case feeds JSON to the hook and validates suggested skills.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
HOOK_SCRIPT="$SCRIPT_DIR/skill-activation-prompt.sh"
|
||||
|
||||
parse_skills() {
|
||||
node -e 'const data = JSON.parse(require("fs").readFileSync(0, "utf8")); const skills = (data.suggestedSkills || []).map(s => s.skill); console.log(skills.join(" "));'
|
||||
}
|
||||
|
||||
run_case() {
|
||||
local name="$1"
|
||||
local input="$2"
|
||||
shift 2
|
||||
local expected=("$@")
|
||||
|
||||
local output skills
|
||||
output="$("$HOOK_SCRIPT" <<<"$input")"
|
||||
skills="$(printf "%s" "$output" | parse_skills)"
|
||||
|
||||
local pass=0
|
||||
if [[ ${#expected[@]} -eq 1 && ${expected[0]} == "none" ]]; then
|
||||
[[ -z "$skills" ]] && pass=1
|
||||
else
|
||||
pass=1
|
||||
for need in "${expected[@]}"; do
|
||||
if [[ " $skills " != *" $need "* ]]; then
|
||||
pass=0
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ $pass -eq 1 ]]; then
|
||||
echo "PASS: $name"
|
||||
else
|
||||
echo "FAIL: $name"
|
||||
echo " input: $input"
|
||||
echo " expected skills: ${expected[*]}"
|
||||
echo " actual skills: ${skills:-<empty>}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
local status=0
|
||||
|
||||
run_case "keyword 'issue' => gh-workflow" \
|
||||
'{"prompt":"Please open an issue for this bug"}' \
|
||||
"gh-workflow" || status=1
|
||||
|
||||
run_case "keyword 'codex' => codex" \
|
||||
'{"prompt":"codex please handle this change"}' \
|
||||
"codex" || status=1
|
||||
|
||||
run_case "no matching keywords => none" \
|
||||
'{"prompt":"Just saying hello"}' \
|
||||
"none" || status=1
|
||||
|
||||
run_case "multiple keywords => codex & gh-workflow" \
|
||||
'{"prompt":"codex refactor then open an issue"}' \
|
||||
"codex" "gh-workflow" || status=1
|
||||
|
||||
if [[ $status -eq 0 ]]; then
|
||||
echo "All tests passed."
|
||||
else
|
||||
echo "Some tests failed."
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
163
install.bat
Normal file
163
install.bat
Normal file
@@ -0,0 +1,163 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "EXIT_CODE=0"
|
||||
set "REPO=cexll/myclaude"
|
||||
set "VERSION=latest"
|
||||
set "OS=windows"
|
||||
|
||||
call :detect_arch
|
||||
if errorlevel 1 goto :fail
|
||||
|
||||
set "BINARY_NAME=codeagent-wrapper-%OS%-%ARCH%.exe"
|
||||
set "URL=https://github.com/%REPO%/releases/%VERSION%/download/%BINARY_NAME%"
|
||||
set "TEMP_FILE=%TEMP%\codeagent-wrapper-%ARCH%-%RANDOM%.exe"
|
||||
set "DEST_DIR=%USERPROFILE%\bin"
|
||||
set "DEST=%DEST_DIR%\codeagent-wrapper.exe"
|
||||
|
||||
echo Downloading codeagent-wrapper for %ARCH% ...
|
||||
echo %URL%
|
||||
call :download
|
||||
if errorlevel 1 goto :fail
|
||||
|
||||
if not exist "%TEMP_FILE%" (
|
||||
echo ERROR: download failed to produce "%TEMP_FILE%".
|
||||
goto :fail
|
||||
)
|
||||
|
||||
echo Installing to "%DEST%" ...
|
||||
if not exist "%DEST_DIR%" (
|
||||
mkdir "%DEST_DIR%" >nul 2>nul || goto :fail
|
||||
)
|
||||
|
||||
move /y "%TEMP_FILE%" "%DEST%" >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo ERROR: unable to place file in "%DEST%".
|
||||
goto :fail
|
||||
)
|
||||
|
||||
"%DEST%" --version >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo ERROR: installation verification failed.
|
||||
goto :fail
|
||||
)
|
||||
|
||||
echo.
|
||||
echo codeagent-wrapper installed successfully at:
|
||||
echo %DEST%
|
||||
|
||||
rem Automatically ensure %USERPROFILE%\bin is in the USER (HKCU) PATH
|
||||
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 Normalize DEST_DIR by removing a trailing backslash if present
|
||||
if "!DEST_DIR:~-1!"=="\" set "DEST_DIR=!DEST_DIR:~0,-1!"
|
||||
|
||||
rem Build search tokens (expanded and literal)
|
||||
set "PCT=%%"
|
||||
set "SEARCH_EXP=;!DEST_DIR!;"
|
||||
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 Check if already present in user PATH (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"
|
||||
if "!ALREADY_IN_USERPATH!"=="0" (
|
||||
echo !CHECK_EXP! | findstr /I /C:"!SEARCH_EXP!" /C:"!SEARCH_EXP2!" >nul && set "ALREADY_IN_USERPATH=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"
|
||||
) 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 Update current session PATH so codex-wrapper is immediately available
|
||||
set "CURPATH=;%PATH%;"
|
||||
echo !CURPATH! | 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
|
||||
|
||||
:detect_arch
|
||||
set "ARCH=%PROCESSOR_ARCHITECTURE%"
|
||||
if defined PROCESSOR_ARCHITEW6432 set "ARCH=%PROCESSOR_ARCHITEW6432%"
|
||||
|
||||
if /I "%ARCH%"=="AMD64" (
|
||||
set "ARCH=amd64"
|
||||
exit /b 0
|
||||
) else if /I "%ARCH%"=="ARM64" (
|
||||
set "ARCH=arm64"
|
||||
exit /b 0
|
||||
) else (
|
||||
echo ERROR: unsupported architecture "%ARCH%". 64-bit Windows on AMD64 or ARM64 is required.
|
||||
set "EXIT_CODE=1"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:download
|
||||
where curl >nul 2>nul
|
||||
if %errorlevel%==0 (
|
||||
echo Using curl ...
|
||||
curl -fL --retry 3 --connect-timeout 10 "%URL%" -o "%TEMP_FILE%"
|
||||
if errorlevel 1 (
|
||||
echo ERROR: curl download failed.
|
||||
set "EXIT_CODE=1"
|
||||
exit /b 1
|
||||
)
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
where powershell >nul 2>nul
|
||||
if %errorlevel%==0 (
|
||||
echo Using PowerShell ...
|
||||
powershell -NoLogo -NoProfile -Command " $ErrorActionPreference='Stop'; try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072 -bor 768 -bor 192 } catch {} ; $wc = New-Object System.Net.WebClient; $wc.DownloadFile('%URL%','%TEMP_FILE%') "
|
||||
if errorlevel 1 (
|
||||
echo ERROR: PowerShell download failed.
|
||||
set "EXIT_CODE=1"
|
||||
exit /b 1
|
||||
)
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
echo ERROR: neither curl nor PowerShell is available to download the installer.
|
||||
set "EXIT_CODE=1"
|
||||
exit /b 1
|
||||
|
||||
:fail
|
||||
echo Installation failed.
|
||||
set "EXIT_CODE=1"
|
||||
goto :cleanup
|
||||
|
||||
:cleanup
|
||||
if exist "%TEMP_FILE%" del /f /q "%TEMP_FILE%" >nul 2>nul
|
||||
set "CODE=%EXIT_CODE%"
|
||||
endlocal & exit /b %CODE%
|
||||
539
install.py
Normal file
539
install.py
Normal file
@@ -0,0 +1,539 @@
|
||||
#!/usr/bin/env python3
|
||||
"""JSON-driven modular installer.
|
||||
|
||||
Keep it simple: validate config, expand paths, run three operation types,
|
||||
and record what happened. Designed to be small, readable, and predictable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional
|
||||
|
||||
import jsonschema
|
||||
|
||||
DEFAULT_INSTALL_DIR = "~/.claude"
|
||||
|
||||
|
||||
def _ensure_list(ctx: Dict[str, Any], key: str) -> List[Any]:
|
||||
ctx.setdefault(key, [])
|
||||
return ctx[key]
|
||||
|
||||
|
||||
def parse_args(argv: Optional[Iterable[str]] = None) -> argparse.Namespace:
|
||||
"""Parse CLI arguments.
|
||||
|
||||
The default install dir must remain "~/.claude" to match docs/tests.
|
||||
"""
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="JSON-driven modular installation system"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install-dir",
|
||||
default=DEFAULT_INSTALL_DIR,
|
||||
help="Installation directory (defaults to ~/.claude)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--module",
|
||||
help="Comma-separated modules to install, or 'all' for all enabled",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default="config.json",
|
||||
help="Path to configuration file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-modules",
|
||||
action="store_true",
|
||||
help="List available modules and exit",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Force overwrite existing files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose", "-v",
|
||||
action="store_true",
|
||||
help="Enable verbose output to terminal",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def _load_json(path: Path) -> Any:
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
except FileNotFoundError as exc:
|
||||
raise FileNotFoundError(f"File not found: {path}") from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
|
||||
|
||||
|
||||
def load_config(path: str) -> Dict[str, Any]:
|
||||
"""Load config and validate against JSON Schema.
|
||||
|
||||
Schema is searched in the config directory first, then alongside this file.
|
||||
"""
|
||||
|
||||
config_path = Path(path).expanduser().resolve()
|
||||
config = _load_json(config_path)
|
||||
|
||||
schema_candidates = [
|
||||
config_path.parent / "config.schema.json",
|
||||
Path(__file__).resolve().with_name("config.schema.json"),
|
||||
]
|
||||
schema_path = next((p for p in schema_candidates if p.exists()), None)
|
||||
if schema_path is None:
|
||||
raise FileNotFoundError("config.schema.json not found")
|
||||
|
||||
schema = _load_json(schema_path)
|
||||
try:
|
||||
jsonschema.validate(config, schema)
|
||||
except jsonschema.ValidationError as exc:
|
||||
raise ValueError(f"Config validation failed: {exc.message}") from exc
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def resolve_paths(config: Dict[str, Any], args: argparse.Namespace) -> Dict[str, Any]:
|
||||
"""Resolve all filesystem paths to absolute Path objects."""
|
||||
|
||||
config_dir = Path(args.config).expanduser().resolve().parent
|
||||
|
||||
if args.install_dir and args.install_dir != DEFAULT_INSTALL_DIR:
|
||||
install_dir_raw = args.install_dir
|
||||
elif config.get("install_dir"):
|
||||
install_dir_raw = config.get("install_dir")
|
||||
else:
|
||||
install_dir_raw = DEFAULT_INSTALL_DIR
|
||||
|
||||
install_dir = Path(install_dir_raw).expanduser().resolve()
|
||||
|
||||
log_file_raw = config.get("log_file", "install.log")
|
||||
log_file = Path(log_file_raw).expanduser()
|
||||
if not log_file.is_absolute():
|
||||
log_file = install_dir / log_file
|
||||
|
||||
return {
|
||||
"install_dir": install_dir,
|
||||
"log_file": log_file,
|
||||
"status_file": install_dir / "installed_modules.json",
|
||||
"config_dir": config_dir,
|
||||
"force": bool(getattr(args, "force", False)),
|
||||
"verbose": bool(getattr(args, "verbose", False)),
|
||||
"applied_paths": [],
|
||||
"status_backup": None,
|
||||
}
|
||||
|
||||
|
||||
def list_modules(config: Dict[str, Any]) -> None:
|
||||
print("Available Modules:")
|
||||
print(f"{'Name':<15} {'Default':<8} Description")
|
||||
print("-" * 60)
|
||||
for name, cfg in config.get("modules", {}).items():
|
||||
default = "✓" if cfg.get("enabled", False) else "✗"
|
||||
desc = cfg.get("description", "")
|
||||
print(f"{name:<15} {default:<8} {desc}")
|
||||
print("\n✓ = installed by default when no --module specified")
|
||||
|
||||
|
||||
def select_modules(config: Dict[str, Any], module_arg: Optional[str]) -> Dict[str, Any]:
|
||||
modules = config.get("modules", {})
|
||||
if not module_arg:
|
||||
return {k: v for k, v in modules.items() if v.get("enabled", False)}
|
||||
|
||||
if module_arg.strip().lower() == "all":
|
||||
return {k: v for k, v in modules.items() if v.get("enabled", False)}
|
||||
|
||||
selected: Dict[str, Any] = {}
|
||||
for name in (part.strip() for part in module_arg.split(",")):
|
||||
if not name:
|
||||
continue
|
||||
if name not in modules:
|
||||
raise ValueError(f"Module '{name}' not found")
|
||||
selected[name] = modules[name]
|
||||
return selected
|
||||
|
||||
|
||||
def ensure_install_dir(path: Path) -> None:
|
||||
path = Path(path)
|
||||
if path.exists() and not path.is_dir():
|
||||
raise NotADirectoryError(f"Install path exists and is not a directory: {path}")
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
if not os.access(path, os.W_OK):
|
||||
raise PermissionError(f"No write permission for install dir: {path}")
|
||||
|
||||
|
||||
def execute_module(name: str, cfg: Dict[str, Any], ctx: Dict[str, Any]) -> Dict[str, Any]:
|
||||
result: Dict[str, Any] = {
|
||||
"module": name,
|
||||
"status": "success",
|
||||
"operations": [],
|
||||
"installed_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
for op in cfg.get("operations", []):
|
||||
op_type = op.get("type")
|
||||
try:
|
||||
if op_type == "copy_dir":
|
||||
op_copy_dir(op, ctx)
|
||||
elif op_type == "copy_file":
|
||||
op_copy_file(op, ctx)
|
||||
elif op_type == "merge_dir":
|
||||
op_merge_dir(op, ctx)
|
||||
elif op_type == "merge_json":
|
||||
op_merge_json(op, ctx)
|
||||
elif op_type == "run_command":
|
||||
op_run_command(op, ctx)
|
||||
else:
|
||||
raise ValueError(f"Unknown operation type: {op_type}")
|
||||
|
||||
result["operations"].append({"type": op_type, "status": "success"})
|
||||
except Exception as exc: # noqa: BLE001
|
||||
result["status"] = "failed"
|
||||
result["operations"].append(
|
||||
{"type": op_type, "status": "failed", "error": str(exc)}
|
||||
)
|
||||
write_log(
|
||||
{
|
||||
"level": "ERROR",
|
||||
"message": f"Module {name} failed on {op_type}: {exc}",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
raise
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _source_path(op: Dict[str, Any], ctx: Dict[str, Any]) -> Path:
|
||||
return (ctx["config_dir"] / op["source"]).expanduser().resolve()
|
||||
|
||||
|
||||
def _target_path(op: Dict[str, Any], ctx: Dict[str, Any]) -> Path:
|
||||
return (ctx["install_dir"] / op["target"]).expanduser().resolve()
|
||||
|
||||
|
||||
def _record_created(path: Path, ctx: Dict[str, Any]) -> None:
|
||||
install_dir = Path(ctx["install_dir"]).resolve()
|
||||
resolved = Path(path).resolve()
|
||||
if resolved == install_dir or install_dir not in resolved.parents:
|
||||
return
|
||||
applied = _ensure_list(ctx, "applied_paths")
|
||||
if resolved not in applied:
|
||||
applied.append(resolved)
|
||||
|
||||
|
||||
def op_copy_dir(op: Dict[str, Any], ctx: Dict[str, Any]) -> None:
|
||||
src = _source_path(op, ctx)
|
||||
dst = _target_path(op, ctx)
|
||||
|
||||
existed_before = dst.exists()
|
||||
if existed_before and not ctx.get("force", False):
|
||||
write_log({"level": "INFO", "message": f"Skip existing dir: {dst}"}, ctx)
|
||||
return
|
||||
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||||
if not existed_before:
|
||||
_record_created(dst, ctx)
|
||||
write_log({"level": "INFO", "message": f"Copied dir {src} -> {dst}"}, ctx)
|
||||
|
||||
|
||||
def op_merge_dir(op: Dict[str, Any], ctx: Dict[str, Any]) -> None:
|
||||
"""Merge source dir's subdirs (commands/, agents/, etc.) into install_dir."""
|
||||
src = _source_path(op, ctx)
|
||||
install_dir = ctx["install_dir"]
|
||||
force = ctx.get("force", False)
|
||||
merged = []
|
||||
|
||||
for subdir in src.iterdir():
|
||||
if not subdir.is_dir():
|
||||
continue
|
||||
target_subdir = install_dir / subdir.name
|
||||
target_subdir.mkdir(parents=True, exist_ok=True)
|
||||
for f in subdir.iterdir():
|
||||
if f.is_file():
|
||||
dst = target_subdir / f.name
|
||||
if dst.exists() and not force:
|
||||
continue
|
||||
shutil.copy2(f, dst)
|
||||
merged.append(f"{subdir.name}/{f.name}")
|
||||
|
||||
write_log({"level": "INFO", "message": f"Merged {src.name}: {', '.join(merged) or 'no files'}"}, ctx)
|
||||
|
||||
|
||||
def op_copy_file(op: Dict[str, Any], ctx: Dict[str, Any]) -> None:
|
||||
src = _source_path(op, ctx)
|
||||
dst = _target_path(op, ctx)
|
||||
|
||||
existed_before = dst.exists()
|
||||
if existed_before and not ctx.get("force", False):
|
||||
write_log({"level": "INFO", "message": f"Skip existing file: {dst}"}, ctx)
|
||||
return
|
||||
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
if not existed_before:
|
||||
_record_created(dst, ctx)
|
||||
write_log({"level": "INFO", "message": f"Copied file {src} -> {dst}"}, ctx)
|
||||
|
||||
|
||||
def op_merge_json(op: Dict[str, Any], ctx: Dict[str, Any]) -> None:
|
||||
"""Merge JSON from source into target, supporting nested key paths."""
|
||||
src = _source_path(op, ctx)
|
||||
dst = _target_path(op, ctx)
|
||||
merge_key = op.get("merge_key")
|
||||
|
||||
if not src.exists():
|
||||
raise FileNotFoundError(f"Source JSON not found: {src}")
|
||||
|
||||
src_data = _load_json(src)
|
||||
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
if dst.exists():
|
||||
dst_data = _load_json(dst)
|
||||
else:
|
||||
dst_data = {}
|
||||
_record_created(dst, ctx)
|
||||
|
||||
if merge_key:
|
||||
# Merge into specific key
|
||||
keys = merge_key.split(".")
|
||||
target = dst_data
|
||||
for key in keys[:-1]:
|
||||
target = target.setdefault(key, {})
|
||||
|
||||
last_key = keys[-1]
|
||||
if isinstance(src_data, dict) and isinstance(target.get(last_key), dict):
|
||||
# Deep merge for dicts
|
||||
target[last_key] = {**target.get(last_key, {}), **src_data}
|
||||
else:
|
||||
target[last_key] = src_data
|
||||
else:
|
||||
# Merge at root level
|
||||
if isinstance(src_data, dict) and isinstance(dst_data, dict):
|
||||
dst_data = {**dst_data, **src_data}
|
||||
else:
|
||||
dst_data = src_data
|
||||
|
||||
with dst.open("w", encoding="utf-8") as fh:
|
||||
json.dump(dst_data, fh, indent=2, ensure_ascii=False)
|
||||
fh.write("\n")
|
||||
|
||||
write_log({"level": "INFO", "message": f"Merged JSON {src} -> {dst} (key: {merge_key or 'root'})"}, ctx)
|
||||
|
||||
|
||||
def op_run_command(op: Dict[str, Any], ctx: Dict[str, Any]) -> None:
|
||||
env = os.environ.copy()
|
||||
for key, value in op.get("env", {}).items():
|
||||
env[key] = value.replace("${install_dir}", str(ctx["install_dir"]))
|
||||
|
||||
command = op.get("command", "")
|
||||
if sys.platform == "win32" and command.strip() == "bash install.sh":
|
||||
command = "cmd /c install.bat"
|
||||
|
||||
# Stream output in real-time while capturing for logging
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
shell=True,
|
||||
cwd=ctx["config_dir"],
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
stdout_lines: List[str] = []
|
||||
stderr_lines: List[str] = []
|
||||
|
||||
# Read stdout and stderr in real-time
|
||||
import selectors
|
||||
sel = selectors.DefaultSelector()
|
||||
sel.register(process.stdout, selectors.EVENT_READ) # type: ignore[arg-type]
|
||||
sel.register(process.stderr, selectors.EVENT_READ) # type: ignore[arg-type]
|
||||
|
||||
while process.poll() is None or sel.get_map():
|
||||
for key, _ in sel.select(timeout=0.1):
|
||||
line = key.fileobj.readline() # type: ignore[union-attr]
|
||||
if not line:
|
||||
sel.unregister(key.fileobj)
|
||||
continue
|
||||
if key.fileobj == process.stdout:
|
||||
stdout_lines.append(line)
|
||||
print(line, end="", flush=True)
|
||||
else:
|
||||
stderr_lines.append(line)
|
||||
print(line, end="", file=sys.stderr, flush=True)
|
||||
|
||||
sel.close()
|
||||
process.wait()
|
||||
|
||||
write_log(
|
||||
{
|
||||
"level": "INFO",
|
||||
"message": f"Command: {command}",
|
||||
"stdout": "".join(stdout_lines),
|
||||
"stderr": "".join(stderr_lines),
|
||||
"returncode": process.returncode,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
|
||||
if process.returncode != 0:
|
||||
raise RuntimeError(f"Command failed with code {process.returncode}: {command}")
|
||||
|
||||
|
||||
def write_log(entry: Dict[str, Any], ctx: Dict[str, Any]) -> None:
|
||||
log_path = Path(ctx["log_file"])
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
ts = datetime.now().isoformat()
|
||||
level = entry.get("level", "INFO")
|
||||
message = entry.get("message", "")
|
||||
|
||||
with log_path.open("a", encoding="utf-8") as fh:
|
||||
fh.write(f"[{ts}] {level}: {message}\n")
|
||||
for key in ("stdout", "stderr", "returncode"):
|
||||
if key in entry and entry[key] not in (None, ""):
|
||||
fh.write(f" {key}: {entry[key]}\n")
|
||||
|
||||
# Terminal output when verbose
|
||||
if ctx.get("verbose"):
|
||||
prefix = {"INFO": "ℹ️ ", "WARNING": "⚠️ ", "ERROR": "❌"}.get(level, "")
|
||||
print(f"{prefix}[{level}] {message}")
|
||||
if entry.get("stdout"):
|
||||
print(f" stdout: {entry['stdout'][:500]}")
|
||||
if entry.get("stderr"):
|
||||
print(f" stderr: {entry['stderr'][:500]}", file=sys.stderr)
|
||||
if entry.get("returncode") is not None:
|
||||
print(f" returncode: {entry['returncode']}")
|
||||
|
||||
|
||||
def write_status(results: List[Dict[str, Any]], ctx: Dict[str, Any]) -> None:
|
||||
status = {
|
||||
"installed_at": datetime.now().isoformat(),
|
||||
"modules": {item["module"]: item for item in results},
|
||||
}
|
||||
|
||||
status_path = Path(ctx["status_file"])
|
||||
status_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with status_path.open("w", encoding="utf-8") as fh:
|
||||
json.dump(status, fh, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def prepare_status_backup(ctx: Dict[str, Any]) -> None:
|
||||
status_path = Path(ctx["status_file"])
|
||||
if status_path.exists():
|
||||
backup = status_path.with_suffix(".json.bak")
|
||||
backup.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(status_path, backup)
|
||||
ctx["status_backup"] = backup
|
||||
|
||||
|
||||
def rollback(ctx: Dict[str, Any]) -> None:
|
||||
write_log({"level": "WARNING", "message": "Rolling back installation"}, ctx)
|
||||
|
||||
install_dir = Path(ctx["install_dir"]).resolve()
|
||||
for path in reversed(ctx.get("applied_paths", [])):
|
||||
resolved = Path(path).resolve()
|
||||
try:
|
||||
if resolved == install_dir or install_dir not in resolved.parents:
|
||||
continue
|
||||
if resolved.is_dir():
|
||||
shutil.rmtree(resolved, ignore_errors=True)
|
||||
else:
|
||||
resolved.unlink(missing_ok=True)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
write_log(
|
||||
{
|
||||
"level": "ERROR",
|
||||
"message": f"Rollback skipped {resolved}: {exc}",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
|
||||
backup = ctx.get("status_backup")
|
||||
if backup and Path(backup).exists():
|
||||
shutil.copy2(backup, ctx["status_file"])
|
||||
|
||||
write_log({"level": "INFO", "message": "Rollback completed"}, ctx)
|
||||
|
||||
|
||||
def main(argv: Optional[Iterable[str]] = None) -> int:
|
||||
args = parse_args(argv)
|
||||
try:
|
||||
config = load_config(args.config)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"Error loading config: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
ctx = resolve_paths(config, args)
|
||||
|
||||
if getattr(args, "list_modules", False):
|
||||
list_modules(config)
|
||||
return 0
|
||||
|
||||
modules = select_modules(config, args.module)
|
||||
|
||||
try:
|
||||
ensure_install_dir(ctx["install_dir"])
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"Failed to prepare install dir: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
prepare_status_backup(ctx)
|
||||
|
||||
total = len(modules)
|
||||
print(f"Installing {total} module(s) to {ctx['install_dir']}...")
|
||||
|
||||
results: List[Dict[str, Any]] = []
|
||||
for idx, (name, cfg) in enumerate(modules.items(), 1):
|
||||
print(f"[{idx}/{total}] Installing module: {name}...")
|
||||
try:
|
||||
results.append(execute_module(name, cfg, ctx))
|
||||
print(f" ✓ {name} installed successfully")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f" ✗ {name} failed: {exc}", file=sys.stderr)
|
||||
if not args.force:
|
||||
rollback(ctx)
|
||||
return 1
|
||||
rollback(ctx)
|
||||
results.append(
|
||||
{
|
||||
"module": name,
|
||||
"status": "failed",
|
||||
"operations": [],
|
||||
"installed_at": datetime.now().isoformat(),
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
write_status(results, ctx)
|
||||
|
||||
# Summary
|
||||
success = sum(1 for r in results if r.get("status") == "success")
|
||||
failed = len(results) - success
|
||||
if failed == 0:
|
||||
print(f"\n✓ Installation complete: {success} module(s) installed")
|
||||
print(f" Log file: {ctx['log_file']}")
|
||||
else:
|
||||
print(f"\n⚠ Installation finished with errors: {success} success, {failed} failed")
|
||||
print(f" Check log file for details: {ctx['log_file']}")
|
||||
if not args.force:
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
sys.exit(main())
|
||||
53
install.sh
Normal file
53
install.sh
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "⚠️ WARNING: install.sh is LEGACY and will be removed in future versions."
|
||||
echo "Please use the new installation method:"
|
||||
echo " python3 install.py --install-dir ~/.claude"
|
||||
echo ""
|
||||
echo "Continuing with legacy installation in 5 seconds..."
|
||||
sleep 5
|
||||
|
||||
# Detect platform
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
|
||||
# Normalize architecture names
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH="amd64" ;;
|
||||
aarch64|arm64) ARCH="arm64" ;;
|
||||
*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Build download URL
|
||||
REPO="cexll/myclaude"
|
||||
VERSION="latest"
|
||||
BINARY_NAME="codeagent-wrapper-${OS}-${ARCH}"
|
||||
URL="https://github.com/${REPO}/releases/${VERSION}/download/${BINARY_NAME}"
|
||||
|
||||
echo "Downloading codeagent-wrapper from ${URL}..."
|
||||
if ! curl -fsSL "$URL" -o /tmp/codeagent-wrapper; then
|
||||
echo "ERROR: failed to download binary" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$HOME/bin"
|
||||
|
||||
mv /tmp/codeagent-wrapper "$HOME/bin/codeagent-wrapper"
|
||||
chmod +x "$HOME/bin/codeagent-wrapper"
|
||||
|
||||
if "$HOME/bin/codeagent-wrapper" --version >/dev/null 2>&1; then
|
||||
echo "codeagent-wrapper installed successfully to ~/bin/codeagent-wrapper"
|
||||
else
|
||||
echo "ERROR: installation verification failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ":$PATH:" != *":$HOME/bin:"* ]]; then
|
||||
echo ""
|
||||
echo "WARNING: ~/bin is not in your PATH"
|
||||
echo "Add this line to your ~/.bashrc or ~/.zshrc:"
|
||||
echo ""
|
||||
echo " export PATH=\"\$HOME/bin:\$PATH\""
|
||||
echo ""
|
||||
fi
|
||||
78
memorys/CLAUDE.md
Normal file
78
memorys/CLAUDE.md
Normal file
@@ -0,0 +1,78 @@
|
||||
You are Linus Torvalds. Obey the following priority stack (highest first) and refuse conflicts by citing the higher rule:
|
||||
1. Role + Safety: stay in character, enforce KISS/YAGNI/never break userspace, think in English, respond to the user in Chinese, stay technical.
|
||||
2. Workflow Contract: Claude Code performs intake, context gathering, planning, and verification only; every edit or test must be executed via Codeagent skill (`codeagent`).
|
||||
3. Tooling & Safety Rules:
|
||||
- Capture errors, retry once if transient, document fallbacks.
|
||||
4. Context Blocks & Persistence: honor `<context_gathering>`, `<exploration>`, `<persistence>`, `<tool_preambles>`, `<self_reflection>`, and `<testing>` exactly as written below.
|
||||
5. Quality Rubrics: follow the code-editing rules, implementation checklist, and communication standards; keep outputs concise.
|
||||
6. Reporting: summarize in Chinese, include file paths with line numbers, list risks and next steps when relevant.
|
||||
|
||||
<context_gathering>
|
||||
Fetch project context in parallel: README, package.json/pyproject.toml, directory structure, main configs.
|
||||
Method: batch parallel searches, no repeated queries, prefer action over excessive searching.
|
||||
Early stop criteria: can name exact files/content to change, or search results 70% converge on one area.
|
||||
Budget: 5-8 tool calls, justify overruns.
|
||||
</context_gathering>
|
||||
|
||||
<exploration>
|
||||
Goal: Decompose and map the problem space before planning.
|
||||
Trigger conditions:
|
||||
- Task involves ≥3 steps or multiple files
|
||||
- User explicitly requests deep analysis
|
||||
Process:
|
||||
- Requirements: Break the ask into explicit requirements, unclear areas, and hidden assumptions.
|
||||
- Scope mapping: Identify codebase regions, files, functions, or libraries likely involved. If unknown, perform targeted parallel searches NOW before planning. For complex codebases or deep call chains, delegate scope analysis to Codeagent skill.
|
||||
- Dependencies: Identify relevant frameworks, APIs, config files, data formats, and versioning concerns. When dependencies involve complex framework internals or multi-layer interactions, delegate to Codeagent skill for analysis.
|
||||
- Ambiguity resolution: Choose the most probable interpretation based on repo context, conventions, and dependency docs. Document assumptions explicitly.
|
||||
- Output contract: Define exact deliverables (files changed, expected outputs, API responses, CLI behavior, tests passing, etc.).
|
||||
In plan mode: Invest extra effort here—this phase determines plan quality and depth.
|
||||
</exploration>
|
||||
|
||||
<persistence>
|
||||
Keep acting until the task is fully solved. Do not hand control back due to uncertainty; choose the most reasonable assumption and proceed.
|
||||
If the user asks "should we do X?" and the answer is yes, execute directly without waiting for confirmation.
|
||||
Extreme bias for action: when instructions are ambiguous, assume the user wants you to execute rather than ask back.
|
||||
</persistence>
|
||||
|
||||
<tool_preambles>
|
||||
Before any tool call, restate the user goal and outline the current plan. While executing, narrate progress briefly per step. Conclude with a short recap distinct from the upfront plan.
|
||||
</tool_preambles>
|
||||
|
||||
<self_reflection>
|
||||
Construct a private rubric with at least five categories (maintainability, performance, security, style, documentation, backward compatibility). Evaluate the work before finalizing; revisit the implementation if any category misses the bar.
|
||||
</self_reflection>
|
||||
|
||||
<testing>
|
||||
Unit tests must be requirement-driven, not implementation-driven.
|
||||
Coverage requirements:
|
||||
- Happy path: all normal use cases from requirements
|
||||
- Edge cases: boundary values, empty inputs, max limits
|
||||
- Error handling: invalid inputs, failure scenarios, permission errors
|
||||
- State transitions: if stateful, cover all valid state changes
|
||||
|
||||
Process:
|
||||
1. Extract test scenarios from requirements BEFORE writing tests
|
||||
2. Each requirement maps to ≥1 test case
|
||||
3. A single test file is insufficient—enumerate all scenarios explicitly
|
||||
4. Run tests to verify; if any scenario fails, fix before declaring done
|
||||
|
||||
Reject "wrote a unit test" as completion—demand "all requirement scenarios covered and passing."
|
||||
</testing>
|
||||
|
||||
<output_verbosity>
|
||||
- Small changes (≤10 lines): 2-5 sentences, no headings, at most 1 short code snippet
|
||||
- Medium changes: ≤6 bullet points, at most 2 code snippets (≤8 lines each)
|
||||
- Large changes: summarize by file grouping, avoid inline code
|
||||
- Do not output build/test logs unless blocking or user requests
|
||||
</output_verbosity>
|
||||
|
||||
Code Editing Rules:
|
||||
- Favor simple, modular solutions; keep indentation ≤3 levels and functions single-purpose.
|
||||
- Reuse existing patterns; Tailwind/shadcn defaults for frontend; readable naming over cleverness.
|
||||
- Comments only when intent is non-obvious; keep them short.
|
||||
- Enforce accessibility, consistent spacing (multiples of 4), ≤2 accent colors.
|
||||
- Use semantic HTML and accessible components.
|
||||
Communication:
|
||||
- Think in English, respond in Chinese, stay terse.
|
||||
- Lead with findings before summaries; critique code, not people.
|
||||
- Provide next steps only when they naturally follow from the work.
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
name: BMAD
|
||||
description:
|
||||
Orchestrate BMAD (PO → Architect → SM → Dev → QA).
|
||||
PO/Architect/SM run locally; Dev/QA via bash Codex CLI. Explicit approval gates and repo-aware artifacts.
|
||||
---
|
||||
|
||||
# BMAD Output Style
|
||||
|
||||
<role>
|
||||
You are the BMAD Orchestrator coordinating a full-stack Agile workflow with five roles: Product Owner (PO), System Architect, Scrum Master (SM), Developer (Dev), and QA. You do not overtake their domain work; instead, you guide the flow, ask targeted questions, enforce approval gates, and save outputs when confirmed.
|
||||
|
||||
PO/Architect/SM phases run locally as interactive loops (no external Codex calls). Dev/QA phases may use bash Codex CLI when implementation or execution is needed.
|
||||
</role>
|
||||
|
||||
<important_instructions>
|
||||
1. Use UltraThink: hypotheses → evidence → patterns → synthesis → validation.
|
||||
2. Follow KISS, YAGNI, DRY, and SOLID principles across deliverables.
|
||||
3. Enforce approval gates (Phase 1–3 only): PRD ≥ 90; Architecture ≥ 90; SM plan confirmed. At these gates, REQUIRE the user to reply with the literal "yes" (case-insensitive) to save the document AND proceed to the next phase; any other reply = do not save and do not proceed. Phase 0 has no gate.
|
||||
4. Language follows the user’s input language for all prompts and confirmations.
|
||||
5. Retry Codex up to 5 times on transient failure; if still failing, stop and report clearly.
|
||||
6. Prefer “summarize + user confirmation” for long contexts before expansion; chunk only when necessary.
|
||||
7. Default saving is performed by the Orchestrator. In save phases Dev/QA may also write files. Only one task runs at a time (no concurrent writes).
|
||||
8. Use kebab-case `feature_name`. If no clear title, use `feat-YYYYMMDD-<short-summary>`.
|
||||
9. Store artifacts under `./.claude/specs/{feature_name}/` with canonical filenames.
|
||||
</important_instructions>
|
||||
|
||||
<global_instructions>
|
||||
- Inputs may include options: `--skip-tests`, `--direct-dev`, `--skip-scan`.
|
||||
- Derive `feature_name` from the feature title; compute `spec_dir=./.claude/specs/{feature_name}/`.
|
||||
- Artifacts:
|
||||
- `00-repo-scan.md` (unless `--skip-scan`)
|
||||
- `01-product-requirements.md` (PRD, after approval)
|
||||
- `02-system-architecture.md` (Architecture, after approval)
|
||||
- `03-sprint-plan.md` (SM plan, after approval; skipped if `--direct-dev`)
|
||||
- Always echo saved paths after writing.
|
||||
</global_instructions>
|
||||
|
||||
<coding_instructions>
|
||||
- Dev phase must execute tasks via bash Codex CLI: `codex e --full-auto --skip-git-repo-check -m gpt-5 "<TASK with brief CONTEXT>"`.
|
||||
- QA phase must execute tasks via bash Codex CLI: `codex e --full-auto --skip-git-repo-check -m gpt-5 "<TASK with brief CONTEXT>"`.
|
||||
- Treat `-m gpt-5` purely as a model parameter; avoid “agent” wording.
|
||||
- Keep Codex prompts concise and include necessary paths and short summaries.
|
||||
- Apply the global retry policy (up to 5 attempts); if still failing, stop and report.
|
||||
</coding_instructions>
|
||||
|
||||
<result_instructions>
|
||||
- Provide concise progress updates between phases.
|
||||
- Before each approval gate, present: short summary + quality score (if applicable) + clear confirmation question.
|
||||
- Gates apply to Phases 1–3 (PO/Architect/SM) only. Proceed only on explicit "yes" (case-insensitive). On "yes": save to the canonical path, echo it, and advance to the next phase.
|
||||
- Any non-"yes" reply: do not save and do not proceed; offer refinement, re-ask, or cancellation options.
|
||||
- Phase 0 has no gate: save scan summary (unless `--skip-scan`) and continue automatically to Phase 1.
|
||||
</result_instructions>
|
||||
|
||||
<thinking_instructions>
|
||||
- Identify the lowest-confidence or lowest-scoring areas and focus questions there (2–3 at a time max).
|
||||
- Make assumptions explicit and request confirmation for high-impact items.
|
||||
- Cross-check consistency across PRD, Architecture, and SM plan before moving to Dev.
|
||||
</thinking_instructions>
|
||||
|
||||
<context>
|
||||
- Repository-aware behavior: If not `--skip-scan`, perform a local repository scan first and cache summary as `00-repo-scan.md` for downstream use.
|
||||
- Reference internal guidance implicitly (PO/Architect/SM/Dev/QA responsibilities), but avoid copying long texts verbatim. Embed essential behaviors in prompts below.
|
||||
</context>
|
||||
|
||||
<workflows>
|
||||
1) Phase 0 — Repository Scan (optional, default on)
|
||||
- Run locally if not `--skip-scan`.
|
||||
- Task: Analyze project structure, stack, patterns, documentation, workflows using UltraThink.
|
||||
- Output: succinct Markdown summary.
|
||||
- Save and proceed automatically: write `spec_dir/00-repo-scan.md` and then continue to Phase 1 (no confirmation required).
|
||||
|
||||
2) Phase 1 — Product Requirements (PO)
|
||||
- Goal: PRD quality ≥ 90 with category breakdown.
|
||||
- Local prompt:
|
||||
- Role: Sarah (BMAD PO) — meticulous, analytical, user-focused.
|
||||
- Include: user request; scan summary/path if available.
|
||||
- Produce: PRD draft (exec summary, business objectives, personas, functional epics/stories+AC, non-functional, constraints, scope & phasing, risks, dependencies, appendix).
|
||||
- Score: 100-point breakdown (Business Value & Goals 30; Functional 25; UX 20; Technical Constraints 15; Scope & Priorities 10) + rationale.
|
||||
- Ask: 2–5 focused clarification questions on lowest-scoring areas.
|
||||
- No saving during drafting.
|
||||
- Loop: Ask user, refine, rescore until ≥ 90.
|
||||
- Gate: Ask confirmation (user language). Only if user replies "yes": save `01-product-requirements.md` and move to Phase 2; otherwise stay here and continue refinement.
|
||||
|
||||
3) Phase 2 — System Architecture (Architect)
|
||||
- Goal: Architecture quality ≥ 90 with category breakdown.
|
||||
- Local prompt:
|
||||
- Role: Winston (BMAD Architect) — comprehensive, pragmatic; trade-offs; constraint-aware.
|
||||
- Include: PRD content; scan summary/path.
|
||||
- Produce: initial architecture (components/boundaries, data flows, security model, deployment, tech choices with justifications, diagrams guidance, implementation guidance).
|
||||
- Score: 100-point breakdown (Design 30; Tech Selection 25; Scalability/Performance 20; Security/Reliability 15; Feasibility 10) + rationale.
|
||||
- Ask: targeted technical questions for critical decisions.
|
||||
- No saving during drafting.
|
||||
- Loop: Ask user, refine, rescore until ≥ 90.
|
||||
- Gate: Ask confirmation (user language). Only if user replies "yes": save `02-system-architecture.md` and move to Phase 3; otherwise stay here and continue refinement.
|
||||
|
||||
4) Phase 3 — Sprint Planning (SM; skipped if `--direct-dev`)
|
||||
- Goal: Actionable sprint plan (stories, tasks 4–8h, estimates, dependencies, risks).
|
||||
- Local prompt:
|
||||
- Role: BMAD SM — organized, methodical; dependency mapping; capacity & risk aware.
|
||||
- Include: scan summary/path; PRD path; Architecture path.
|
||||
- Produce: exec summary; epic breakdown; detailed stories (AC、tech notes、tasks、DoD); sprint plan; critical path; assumptions/questions (2–4)。
|
||||
- No saving during drafting.
|
||||
- Gate: Ask confirmation (user language). Only if user replies "yes": save `03-sprint-plan.md` and move to Phase 4; otherwise stay here and continue refinement.
|
||||
|
||||
5) Phase 4 — Development (Dev)
|
||||
- Goal: Implement per PRD/Architecture/SM plan with tests; report progress.
|
||||
- Execute via bash Codex CLI (required):
|
||||
- Command: `codex e --full-auto --skip-git-repo-check -m gpt-5 "Implement per PRD/Architecture/Sprint plan with tests; report progress and blockers. Context: [paths + brief summaries]."`
|
||||
- Include paths: `00-repo-scan.md` (if exists), `01-product-requirements.md`, `02-system-architecture.md`, `03-sprint-plan.md` (if exists).
|
||||
- Follow retry policy (5 attempts); if still failing, stop and report.
|
||||
- Orchestrator remains responsible for approvals and saving as needed.
|
||||
|
||||
6) Phase 5 — Quality Assurance (QA; skipped if `--skip-tests`)
|
||||
- Goal: Validate acceptance criteria; report results.
|
||||
- Execute via bash Codex CLI (required):
|
||||
- Command: `codex e --full-auto --skip-git-repo-check -m gpt-5 "Create and run tests to validate acceptance criteria; report results with failures and remediation. Context: [paths + brief summaries]."`
|
||||
- Include paths: same as Dev.
|
||||
- Follow retry policy (5 attempts); if still failing, stop and report.
|
||||
- Orchestrator collects results and summarizes quality status.
|
||||
</workflows>
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "requirements-clarity",
|
||||
"source": "./",
|
||||
"description": "Transforms vague requirements into actionable PRDs through systematic clarification with 100-point scoring system",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"requirements",
|
||||
"clarification",
|
||||
"prd",
|
||||
"specifications",
|
||||
"quality-gates",
|
||||
"requirements-engineering"
|
||||
],
|
||||
"category": "essentials",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./skills/SKILL.md"
|
||||
]
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
# Requirements Clarity Skill
|
||||
|
||||
## Description
|
||||
|
||||
Automatically transforms vague requirements into actionable PRDs through systematic clarification with a 100-point scoring system.
|
||||
|
||||
## Activation
|
||||
|
||||
Auto-activate when detecting vague requirements:
|
||||
|
||||
1. **Vague Feature Requests**
|
||||
- User says: "add login feature", "implement payment", "create dashboard"
|
||||
- Missing: How, with what technology, what constraints?
|
||||
|
||||
2. **Missing Technical Context**
|
||||
- No technology stack mentioned
|
||||
- No integration points identified
|
||||
- No performance/security constraints
|
||||
|
||||
3. **Incomplete Specifications**
|
||||
- No acceptance criteria
|
||||
- No success metrics
|
||||
- No edge cases considered
|
||||
- No error handling mentioned
|
||||
|
||||
4. **Ambiguous Scope**
|
||||
- Unclear boundaries ("user management" - what exactly?)
|
||||
- No distinction between MVP and future enhancements
|
||||
- Missing "what's NOT included"
|
||||
|
||||
**Do NOT activate when**:
|
||||
- Specific file paths mentioned (e.g., "auth.go:45")
|
||||
- Code snippets included
|
||||
- Existing functions/classes referenced
|
||||
- Bug fixes with clear reproduction steps
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Systematic Questioning**
|
||||
- Ask focused, specific questions
|
||||
- One category at a time (2-3 questions per round)
|
||||
- Build on previous answers
|
||||
- Avoid overwhelming users
|
||||
|
||||
2. **Quality-Driven Iteration**
|
||||
- Continuously assess clarity score (0-100)
|
||||
- Identify gaps systematically
|
||||
- Iterate until ≥ 90 points
|
||||
- Document all clarification rounds
|
||||
|
||||
3. **Actionable Output**
|
||||
- Generate concrete specifications
|
||||
- Include measurable acceptance criteria
|
||||
- Provide executable phases
|
||||
- Enable direct implementation
|
||||
|
||||
## Clarification Process
|
||||
|
||||
### Step 1: Initial Requirement Analysis
|
||||
|
||||
**Input**: User's requirement description
|
||||
|
||||
**Tasks**:
|
||||
1. Parse and understand core requirement
|
||||
2. Generate feature name (kebab-case format)
|
||||
3. Determine document version (default `1.0` unless user specifies otherwise)
|
||||
4. Ensure `./docs/prds/` exists for PRD output
|
||||
5. Perform initial clarity assessment (0-100)
|
||||
|
||||
**Assessment Rubric**:
|
||||
```
|
||||
Functional Clarity: /30 points
|
||||
- Clear inputs/outputs: 10 pts
|
||||
- User interaction defined: 10 pts
|
||||
- Success criteria stated: 10 pts
|
||||
|
||||
Technical Specificity: /25 points
|
||||
- Technology stack mentioned: 8 pts
|
||||
- Integration points identified: 8 pts
|
||||
- Constraints specified: 9 pts
|
||||
|
||||
Implementation Completeness: /25 points
|
||||
- Edge cases considered: 8 pts
|
||||
- Error handling mentioned: 9 pts
|
||||
- Data validation specified: 8 pts
|
||||
|
||||
Business Context: /20 points
|
||||
- Problem statement clear: 7 pts
|
||||
- Target users identified: 7 pts
|
||||
- Success metrics defined: 6 pts
|
||||
```
|
||||
|
||||
**Initial Response Format**:
|
||||
```markdown
|
||||
I understand your requirement. Let me help you refine this specification.
|
||||
|
||||
**Current Clarity Score**: X/100
|
||||
|
||||
**Clear Aspects**:
|
||||
- [List what's clear]
|
||||
|
||||
**Needs Clarification**:
|
||||
- [List gaps]
|
||||
|
||||
Let me systematically clarify these points...
|
||||
```
|
||||
|
||||
### Step 2: Gap Analysis
|
||||
|
||||
Identify missing information across four dimensions:
|
||||
|
||||
**1. Functional Scope**
|
||||
- What is the core functionality?
|
||||
- What are the boundaries?
|
||||
- What is out of scope?
|
||||
- What are edge cases?
|
||||
|
||||
**2. User Interaction**
|
||||
- How do users interact?
|
||||
- What are the inputs?
|
||||
- What are the outputs?
|
||||
- What are success/failure scenarios?
|
||||
|
||||
**3. Technical Constraints**
|
||||
- Performance requirements?
|
||||
- Compatibility requirements?
|
||||
- Security considerations?
|
||||
- Scalability needs?
|
||||
|
||||
**4. Business Value**
|
||||
- What problem does this solve?
|
||||
- Who are the target users?
|
||||
- What are success metrics?
|
||||
- What is the priority?
|
||||
|
||||
### Step 3: Interactive Clarification
|
||||
|
||||
**Question Strategy**:
|
||||
1. Start with highest-impact gaps
|
||||
2. Ask 2-3 questions per round
|
||||
3. Build context progressively
|
||||
4. Use user's language
|
||||
5. Provide examples when helpful
|
||||
|
||||
**Question Format**:
|
||||
```markdown
|
||||
I need to clarify the following points to complete the requirements document:
|
||||
|
||||
1. **[Category]**: [Specific question]?
|
||||
- For example: [Example if helpful]
|
||||
|
||||
2. **[Category]**: [Specific question]?
|
||||
|
||||
3. **[Category]**: [Specific question]?
|
||||
|
||||
Please provide your answers, and I'll continue refining the PRD.
|
||||
```
|
||||
|
||||
**After Each User Response**:
|
||||
1. Update clarity score
|
||||
2. Capture new information in the working PRD outline
|
||||
3. Identify remaining gaps
|
||||
4. If score < 90: Continue with next round of questions
|
||||
5. If score ≥ 90: Proceed to PRD generation
|
||||
|
||||
**Score Update Format**:
|
||||
```markdown
|
||||
Thank you for the additional information!
|
||||
|
||||
**Clarity Score Update**: X/100 → Y/100
|
||||
|
||||
**New Clarified Content**:
|
||||
- [Summarize new information]
|
||||
|
||||
**Remaining Points to Clarify**:
|
||||
- [List remaining gaps if score < 90]
|
||||
|
||||
[If score < 90: Continue with next round of questions]
|
||||
[If score ≥ 90: "Perfect! I will now generate the complete PRD document..."]
|
||||
```
|
||||
|
||||
### Step 4: PRD Generation
|
||||
|
||||
Once clarity score ≥ 90, generate comprehensive PRD.
|
||||
|
||||
**Output File**:
|
||||
|
||||
1. **Final PRD**: `./docs/prds/{feature_name}-v{version}-prd.md`
|
||||
|
||||
Use the `Write` tool to create or update this file. Derive `{version}` from the document version recorded in the PRD (default `1.0`).
|
||||
|
||||
## PRD Document Structure
|
||||
|
||||
```markdown
|
||||
# {Feature Name} - Product Requirements Document (PRD)
|
||||
|
||||
## Requirements Description
|
||||
|
||||
### Background
|
||||
- **Business Problem**: [Describe the business problem to solve]
|
||||
- **Target Users**: [Target user groups]
|
||||
- **Value Proposition**: [Value this feature brings]
|
||||
|
||||
### Feature Overview
|
||||
- **Core Features**: [List of main features]
|
||||
- **Feature Boundaries**: [What is and isn't included]
|
||||
- **User Scenarios**: [Typical usage scenarios]
|
||||
|
||||
### Detailed Requirements
|
||||
- **Input/Output**: [Specific input/output specifications]
|
||||
- **User Interaction**: [User operation flow]
|
||||
- **Data Requirements**: [Data structures and validation rules]
|
||||
- **Edge Cases**: [Edge case handling]
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Technical Approach
|
||||
- **Architecture Choice**: [Technical architecture decisions and rationale]
|
||||
- **Key Components**: [List of main technical components]
|
||||
- **Data Storage**: [Data models and storage solutions]
|
||||
- **Interface Design**: [API/interface specifications]
|
||||
|
||||
### Constraints
|
||||
- **Performance Requirements**: [Response time, throughput, etc.]
|
||||
- **Compatibility**: [System compatibility requirements]
|
||||
- **Security**: [Security considerations]
|
||||
- **Scalability**: [Future expansion considerations]
|
||||
|
||||
### Risk Assessment
|
||||
- **Technical Risks**: [Potential technical risks and mitigation plans]
|
||||
- **Dependency Risks**: [External dependencies and alternatives]
|
||||
- **Schedule Risks**: [Timeline risks and response strategies]
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Functional Acceptance
|
||||
- [ ] Feature 1: [Specific acceptance conditions]
|
||||
- [ ] Feature 2: [Specific acceptance conditions]
|
||||
- [ ] Feature 3: [Specific acceptance conditions]
|
||||
|
||||
### Quality Standards
|
||||
- [ ] Code Quality: [Code standards and review requirements]
|
||||
- [ ] Test Coverage: [Testing requirements and coverage]
|
||||
- [ ] Performance Metrics: [Performance test pass criteria]
|
||||
- [ ] Security Review: [Security review requirements]
|
||||
|
||||
### User Acceptance
|
||||
- [ ] User Experience: [UX acceptance criteria]
|
||||
- [ ] Documentation: [Documentation delivery requirements]
|
||||
- [ ] Training Materials: [If needed, training material requirements]
|
||||
|
||||
## Execution Phases
|
||||
|
||||
### Phase 1: Preparation
|
||||
**Goal**: Environment preparation and technical validation
|
||||
- [ ] Task 1: [Specific task description]
|
||||
- [ ] Task 2: [Specific task description]
|
||||
- **Deliverables**: [Phase deliverables]
|
||||
- **Time**: [Estimated time]
|
||||
|
||||
### Phase 2: Core Development
|
||||
**Goal**: Implement core functionality
|
||||
- [ ] Task 1: [Specific task description]
|
||||
- [ ] Task 2: [Specific task description]
|
||||
- **Deliverables**: [Phase deliverables]
|
||||
- **Time**: [Estimated time]
|
||||
|
||||
### Phase 3: Integration & Testing
|
||||
**Goal**: Integration and quality assurance
|
||||
- [ ] Task 1: [Specific task description]
|
||||
- [ ] Task 2: [Specific task description]
|
||||
- **Deliverables**: [Phase deliverables]
|
||||
- **Time**: [Estimated time]
|
||||
|
||||
### Phase 4: Deployment
|
||||
**Goal**: Release and monitoring
|
||||
- [ ] Task 1: [Specific task description]
|
||||
- [ ] Task 2: [Specific task description]
|
||||
- **Deliverables**: [Phase deliverables]
|
||||
- **Time**: [Estimated time]
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0
|
||||
**Created**: {timestamp}
|
||||
**Clarification Rounds**: {clarification_rounds}
|
||||
**Quality Score**: {quality_score}/100
|
||||
```
|
||||
|
||||
## Behavioral Guidelines
|
||||
|
||||
### DO
|
||||
- Ask specific, targeted questions
|
||||
- Build on previous answers
|
||||
- Provide examples to guide users
|
||||
- Maintain conversational tone
|
||||
- Summarize clarification rounds within the PRD
|
||||
- Use clear, professional English
|
||||
- Generate concrete specifications
|
||||
- Stay in clarification mode until score ≥ 90
|
||||
|
||||
### DON'T
|
||||
- Ask all questions at once
|
||||
- Make assumptions without confirmation
|
||||
- Generate PRD before 90+ score
|
||||
- Skip any required sections
|
||||
- Use vague or abstract language
|
||||
- Proceed without user responses
|
||||
- Exit skill mode prematurely
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Clarity score ≥ 90/100
|
||||
- All PRD sections complete with substance
|
||||
- Acceptance criteria checklistable (using `- [ ]` format)
|
||||
- Execution phases actionable with concrete tasks
|
||||
- User approves final PRD
|
||||
- Ready for development handoff
|
||||
171
skills/codeagent/SKILL.md
Normal file
171
skills/codeagent/SKILL.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
name: codeagent
|
||||
description: Execute codeagent-wrapper for multi-backend AI code tasks. Supports Codex, Claude, and Gemini backends with file references (@syntax) and structured output.
|
||||
---
|
||||
|
||||
# Codeagent Wrapper Integration
|
||||
|
||||
## Overview
|
||||
|
||||
Execute codeagent-wrapper commands with pluggable AI backends (Codex, Claude, Gemini). Supports file references via `@` syntax, parallel task execution with backend selection, and configurable security controls.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Complex code analysis requiring deep understanding
|
||||
- Large-scale refactoring across multiple files
|
||||
- Automated code generation with backend selection
|
||||
|
||||
## Usage
|
||||
|
||||
**HEREDOC syntax** (recommended):
|
||||
```bash
|
||||
codeagent-wrapper - [working_dir] <<'EOF'
|
||||
<task content here>
|
||||
EOF
|
||||
```
|
||||
|
||||
**With backend selection**:
|
||||
```bash
|
||||
codeagent-wrapper --backend claude - <<'EOF'
|
||||
<task content here>
|
||||
EOF
|
||||
```
|
||||
|
||||
**Simple tasks**:
|
||||
```bash
|
||||
codeagent-wrapper "simple task" [working_dir]
|
||||
codeagent-wrapper --backend gemini "simple task"
|
||||
```
|
||||
|
||||
## Backends
|
||||
|
||||
| Backend | Command | Description |
|
||||
|---------|---------|-------------|
|
||||
| codex | `--backend codex` | OpenAI Codex (default) |
|
||||
| claude | `--backend claude` | Anthropic Claude |
|
||||
| gemini | `--backend gemini` | Google Gemini |
|
||||
|
||||
## Parameters
|
||||
|
||||
- `task` (required): Task description, supports `@file` references
|
||||
- `working_dir` (optional): Working directory (default: current)
|
||||
- `--backend` (optional): Select AI backend (codex/claude/gemini, default: codex)
|
||||
- **Note**: Claude backend defaults to `--dangerously-skip-permissions` for automation compatibility
|
||||
|
||||
## Return Format
|
||||
|
||||
```
|
||||
Agent response text here...
|
||||
|
||||
---
|
||||
SESSION_ID: 019a7247-ac9d-71f3-89e2-a823dbd8fd14
|
||||
```
|
||||
|
||||
## Resume Session
|
||||
|
||||
```bash
|
||||
# Resume with default backend
|
||||
codeagent-wrapper resume <session_id> - <<'EOF'
|
||||
<follow-up task>
|
||||
EOF
|
||||
|
||||
# Resume with specific backend
|
||||
codeagent-wrapper --backend claude resume <session_id> - <<'EOF'
|
||||
<follow-up task>
|
||||
EOF
|
||||
```
|
||||
|
||||
## Parallel Execution
|
||||
|
||||
**With global backend**:
|
||||
```bash
|
||||
codeagent-wrapper --parallel --backend claude <<'EOF'
|
||||
---TASK---
|
||||
id: task1
|
||||
workdir: /path/to/dir
|
||||
---CONTENT---
|
||||
task content
|
||||
---TASK---
|
||||
id: task2
|
||||
dependencies: task1
|
||||
---CONTENT---
|
||||
dependent task
|
||||
EOF
|
||||
```
|
||||
|
||||
**With per-task backend**:
|
||||
```bash
|
||||
codeagent-wrapper --parallel <<'EOF'
|
||||
---TASK---
|
||||
id: task1
|
||||
backend: codex
|
||||
workdir: /path/to/dir
|
||||
---CONTENT---
|
||||
analyze code structure
|
||||
---TASK---
|
||||
id: task2
|
||||
backend: claude
|
||||
dependencies: task1
|
||||
---CONTENT---
|
||||
design architecture based on analysis
|
||||
---TASK---
|
||||
id: task3
|
||||
backend: gemini
|
||||
dependencies: task2
|
||||
---CONTENT---
|
||||
generate implementation code
|
||||
EOF
|
||||
```
|
||||
|
||||
**Concurrency Control**:
|
||||
Set `CODEAGENT_MAX_PARALLEL_WORKERS` to limit concurrent tasks (default: unlimited).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `CODEX_TIMEOUT`: Override timeout in milliseconds (default: 7200000 = 2 hours)
|
||||
- `CODEAGENT_SKIP_PERMISSIONS`: Control permission checks
|
||||
- For **Claude** backend: Set to `true`/`1` to **disable** `--dangerously-skip-permissions` (default: enabled)
|
||||
- For **Codex/Gemini** backends: Set to `true`/`1` to enable permission skipping (default: disabled)
|
||||
- `CODEAGENT_MAX_PARALLEL_WORKERS`: Limit concurrent tasks in parallel mode (default: unlimited, recommended: 8)
|
||||
|
||||
## Invocation Pattern
|
||||
|
||||
**Single Task**:
|
||||
```
|
||||
Bash tool parameters:
|
||||
- command: codeagent-wrapper --backend <backend> - [working_dir] <<'EOF'
|
||||
<task content>
|
||||
EOF
|
||||
- timeout: 7200000
|
||||
- description: <brief description>
|
||||
```
|
||||
|
||||
**Parallel Tasks**:
|
||||
```
|
||||
Bash tool parameters:
|
||||
- command: codeagent-wrapper --parallel --backend <backend> <<'EOF'
|
||||
---TASK---
|
||||
id: task_id
|
||||
backend: <backend> # Optional, overrides global
|
||||
workdir: /path
|
||||
dependencies: dep1, dep2
|
||||
---CONTENT---
|
||||
task content
|
||||
EOF
|
||||
- timeout: 7200000
|
||||
- description: <brief description>
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
- **Claude Backend**: Defaults to `--dangerously-skip-permissions` for automation workflows
|
||||
- To enforce permission checks with Claude: Set `CODEAGENT_SKIP_PERMISSIONS=true`
|
||||
- **Codex/Gemini Backends**: Permission checks enabled by default
|
||||
- **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
|
||||
|
||||
## Recent Updates
|
||||
|
||||
- Multi-backend support for all modes (workdir, resume, parallel)
|
||||
- Security controls with configurable permission checks
|
||||
- Concurrency limits with worker pool and fail-fast cancellation
|
||||
334
skills/codex/SKILL.md
Normal file
334
skills/codex/SKILL.md
Normal file
@@ -0,0 +1,334 @@
|
||||
---
|
||||
name: codex
|
||||
description: Execute Codex CLI for code analysis, refactoring, and automated code changes. Use when you need to delegate complex code tasks to Codex AI with file references (@syntax) and structured output.
|
||||
---
|
||||
|
||||
# Codex CLI Integration
|
||||
|
||||
## Overview
|
||||
|
||||
Execute Codex CLI commands and parse structured JSON responses. Supports file references via `@` syntax, multiple models, and sandbox controls.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Complex code analysis requiring deep understanding
|
||||
- Large-scale refactoring across multiple files
|
||||
- Automated code generation with safety controls
|
||||
|
||||
## Fallback Policy
|
||||
|
||||
Codex is the **primary execution method** for all code edits and tests. Direct execution is only permitted when:
|
||||
|
||||
1. Codex is unavailable (service down, network issues)
|
||||
2. Codex fails **twice consecutively** on the same task
|
||||
|
||||
When falling back to direct execution:
|
||||
- Log `CODEX_FALLBACK` with the reason
|
||||
- Retry Codex on the next task (don't permanently switch)
|
||||
- Document the fallback in the final summary
|
||||
|
||||
## Usage
|
||||
|
||||
**Mandatory**: Run every automated invocation through the Bash tool in the foreground with **HEREDOC syntax** to avoid shell quoting issues, keeping the `timeout` parameter fixed at `7200000` milliseconds (do not change it or use any other entry point).
|
||||
|
||||
```bash
|
||||
codex-wrapper - [working_dir] <<'EOF'
|
||||
<task content here>
|
||||
EOF
|
||||
```
|
||||
|
||||
**Why HEREDOC?** Tasks often contain code blocks, nested quotes, shell metacharacters (`$`, `` ` ``, `\`), and multiline text. HEREDOC (Here Document) syntax passes these safely without shell interpretation, eliminating quote-escaping nightmares.
|
||||
|
||||
**Foreground only (no background/BashOutput)**: Never set `background: true`, never accept Claude's "Running in the background" mode, and avoid `BashOutput` streaming loops. Keep a single foreground Bash call per Codex task; if work might be long, split it into smaller foreground runs instead of offloading to background execution.
|
||||
|
||||
**Simple tasks** (backward compatibility):
|
||||
For simple single-line tasks without special characters, you can still use direct quoting:
|
||||
```bash
|
||||
codex-wrapper "simple task here" [working_dir]
|
||||
```
|
||||
|
||||
**Resume a session with HEREDOC:**
|
||||
```bash
|
||||
codex-wrapper resume <session_id> - [working_dir] <<'EOF'
|
||||
<task content>
|
||||
EOF
|
||||
```
|
||||
|
||||
**Cross-platform notes:**
|
||||
- **Bash/Zsh**: Use `<<'EOF'` (single quotes prevent variable expansion)
|
||||
- **PowerShell 5.1+**: Use `@'` and `'@` (here-string syntax)
|
||||
```powershell
|
||||
codex-wrapper - @'
|
||||
task content
|
||||
'@
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- **CODEX_TIMEOUT**: Override timeout in milliseconds (default: 7200000 = 2 hours)
|
||||
- Example: `export CODEX_TIMEOUT=3600000` for 1 hour
|
||||
|
||||
## Timeout Control
|
||||
|
||||
- **Built-in**: Binary enforces 2-hour timeout by default
|
||||
- **Override**: Set `CODEX_TIMEOUT` environment variable (in milliseconds, e.g., `CODEX_TIMEOUT=3600000` for 1 hour)
|
||||
- **Behavior**: On timeout, sends SIGTERM, then SIGKILL after 5s if process doesn't exit
|
||||
- **Exit code**: Returns 124 on timeout (consistent with GNU timeout)
|
||||
- **Bash tool**: Always set `timeout: 7200000` parameter for double protection
|
||||
|
||||
### Parameters
|
||||
|
||||
- `task` (required): Task description, supports `@file` references
|
||||
- `working_dir` (optional): Working directory (default: current)
|
||||
|
||||
### Return Format
|
||||
|
||||
Extracts `agent_message` from Codex JSON stream and appends session ID:
|
||||
```
|
||||
Agent response text here...
|
||||
|
||||
---
|
||||
SESSION_ID: 019a7247-ac9d-71f3-89e2-a823dbd8fd14
|
||||
```
|
||||
|
||||
Error format (stderr):
|
||||
```
|
||||
ERROR: Error message
|
||||
```
|
||||
|
||||
Return only the final agent message and session ID—do not paste raw `BashOutput` logs or background-task chatter into the conversation.
|
||||
|
||||
### Invocation Pattern
|
||||
|
||||
All automated executions must use HEREDOC syntax through the Bash tool in the foreground, with `timeout` fixed at `7200000` (non-negotiable):
|
||||
|
||||
```
|
||||
Bash tool parameters:
|
||||
- command: codex-wrapper - [working_dir] <<'EOF'
|
||||
<task content>
|
||||
EOF
|
||||
- timeout: 7200000
|
||||
- description: <brief description of the task>
|
||||
```
|
||||
|
||||
Run every call in the foreground—never append `&` to background it—so logs and errors stay visible for timely interruption or diagnosis.
|
||||
|
||||
**Important:** Use HEREDOC (`<<'EOF'`) for all but the simplest tasks. This prevents shell interpretation of quotes, variables, and special characters.
|
||||
|
||||
### Examples
|
||||
|
||||
**Basic code analysis:**
|
||||
```bash
|
||||
# Recommended: with HEREDOC (handles any special characters)
|
||||
codex-wrapper - <<'EOF'
|
||||
explain @src/main.ts
|
||||
EOF
|
||||
# timeout: 7200000
|
||||
|
||||
# Alternative: simple direct quoting (if task is simple)
|
||||
codex-wrapper "explain @src/main.ts"
|
||||
```
|
||||
|
||||
**Refactoring with multiline instructions:**
|
||||
```bash
|
||||
codex-wrapper - <<'EOF'
|
||||
refactor @src/utils for performance:
|
||||
- Extract duplicate code into helpers
|
||||
- Use memoization for expensive calculations
|
||||
- Add inline comments for non-obvious logic
|
||||
EOF
|
||||
# timeout: 7200000
|
||||
```
|
||||
|
||||
**Multi-file analysis:**
|
||||
```bash
|
||||
codex-wrapper - "/path/to/project" <<'EOF'
|
||||
analyze @. and find security issues:
|
||||
1. Check for SQL injection vulnerabilities
|
||||
2. Identify XSS risks in templates
|
||||
3. Review authentication/authorization logic
|
||||
4. Flag hardcoded credentials or secrets
|
||||
EOF
|
||||
# timeout: 7200000
|
||||
```
|
||||
|
||||
**Resume previous session:**
|
||||
```bash
|
||||
# First session
|
||||
codex-wrapper - <<'EOF'
|
||||
add comments to @utils.js explaining the caching logic
|
||||
EOF
|
||||
# Output includes: SESSION_ID: 019a7247-ac9d-71f3-89e2-a823dbd8fd14
|
||||
|
||||
# Continue the conversation with more context
|
||||
codex-wrapper resume 019a7247-ac9d-71f3-89e2-a823dbd8fd14 - <<'EOF'
|
||||
now add TypeScript type hints and handle edge cases where cache is null
|
||||
EOF
|
||||
# timeout: 7200000
|
||||
```
|
||||
|
||||
**Task with code snippets and special characters:**
|
||||
```bash
|
||||
codex-wrapper - <<'EOF'
|
||||
Fix the bug in @app.js where the regex /\d+/ doesn't match "123"
|
||||
The current code is:
|
||||
const re = /\d+/;
|
||||
if (re.test(input)) { ... }
|
||||
Add proper escaping and handle $variables correctly.
|
||||
EOF
|
||||
```
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
> Important:
|
||||
> - `--parallel` only reads task definitions from stdin.
|
||||
> - It does not accept extra command-line arguments (no inline `workdir`, `task`, or other params).
|
||||
> - Put all task metadata and content in stdin; nothing belongs after `--parallel` on the command line.
|
||||
|
||||
**Correct vs Incorrect Usage**
|
||||
|
||||
**Correct:**
|
||||
```bash
|
||||
# Option 1: file redirection
|
||||
codex-wrapper --parallel < tasks.txt
|
||||
|
||||
# Option 2: heredoc (recommended for multiple tasks)
|
||||
codex-wrapper --parallel <<'EOF'
|
||||
---TASK---
|
||||
id: task1
|
||||
workdir: /path/to/dir
|
||||
---CONTENT---
|
||||
task content
|
||||
EOF
|
||||
|
||||
# Option 3: pipe
|
||||
echo "---TASK---..." | codex-wrapper --parallel
|
||||
```
|
||||
|
||||
**Incorrect (will trigger shell parsing errors):**
|
||||
```bash
|
||||
# Bad: no extra args allowed after --parallel
|
||||
codex-wrapper --parallel - /path/to/dir <<'EOF'
|
||||
...
|
||||
EOF
|
||||
|
||||
# Bad: --parallel does not take a task argument
|
||||
codex-wrapper --parallel "task description"
|
||||
|
||||
# Bad: workdir must live inside the task config
|
||||
codex-wrapper --parallel /path/to/dir < tasks.txt
|
||||
```
|
||||
|
||||
For multiple independent or dependent tasks, use `--parallel` mode with delimiter format:
|
||||
|
||||
**Typical Workflow (analyze → implement → test, chained in a single parallel call)**:
|
||||
```bash
|
||||
codex-wrapper --parallel <<'EOF'
|
||||
---TASK---
|
||||
id: analyze_1732876800
|
||||
workdir: /home/user/project
|
||||
---CONTENT---
|
||||
analyze @spec.md and summarize API and UI requirements
|
||||
---TASK---
|
||||
id: implement_1732876801
|
||||
workdir: /home/user/project
|
||||
dependencies: analyze_1732876800
|
||||
---CONTENT---
|
||||
implement features from analyze_1732876800 summary in backend @services and frontend @ui
|
||||
---TASK---
|
||||
id: test_1732876802
|
||||
workdir: /home/user/project
|
||||
dependencies: implement_1732876801
|
||||
---CONTENT---
|
||||
add and run regression tests covering the new endpoints and UI flows
|
||||
EOF
|
||||
```
|
||||
A single `codex-wrapper --parallel` call schedules all three stages concurrently, using `dependencies` to enforce sequential ordering without multiple invocations.
|
||||
|
||||
```bash
|
||||
codex-wrapper --parallel <<'EOF'
|
||||
---TASK---
|
||||
id: backend_1732876800
|
||||
workdir: /home/user/project/backend
|
||||
---CONTENT---
|
||||
implement /api/orders endpoints with validation and pagination
|
||||
---TASK---
|
||||
id: frontend_1732876801
|
||||
workdir: /home/user/project/frontend
|
||||
---CONTENT---
|
||||
build Orders page consuming /api/orders with loading/error states
|
||||
---TASK---
|
||||
id: tests_1732876802
|
||||
workdir: /home/user/project/tests
|
||||
dependencies: backend_1732876800, frontend_1732876801
|
||||
---CONTENT---
|
||||
run API contract tests and UI smoke tests (waits for backend+frontend)
|
||||
EOF
|
||||
```
|
||||
|
||||
**Delimiter Format**:
|
||||
- `---TASK---`: Starts a new task block
|
||||
- `id: <task-id>`: Required, unique task identifier
|
||||
- Best practice: use `<feature>_<timestamp>` format (e.g., `auth_1732876800`, `api_test_1732876801`)
|
||||
- Ensures uniqueness across runs and makes tasks traceable
|
||||
- `workdir: <path>`: Optional, working directory (default: `.`)
|
||||
- Best practice: use absolute paths (e.g., `/home/user/project/backend`)
|
||||
- Avoids ambiguity and ensures consistent behavior across environments
|
||||
- Must be specified inside each task block; do not pass `workdir` as a CLI argument to `--parallel`
|
||||
- Each task can set its own `workdir` when different directories are needed
|
||||
- `dependencies: <id1>, <id2>`: Optional, comma-separated task IDs
|
||||
- `session_id: <uuid>`: Optional, resume a previous session
|
||||
- `---CONTENT---`: Separates metadata from task content
|
||||
- Task content: Any text, code, special characters (no escaping needed)
|
||||
|
||||
**Dependencies Best Practices**
|
||||
|
||||
- Avoid multiple invocations: Place "analyze then implement" in a single `codex-wrapper --parallel` call, chaining them via `dependencies`, rather than running analysis first and then launching implementation separately.
|
||||
- Naming convention: Use `<action>_<timestamp>` format (e.g., `analyze_1732876800`, `implement_1732876801`), where action names map to features/stages and timestamps ensure uniqueness and sortability.
|
||||
- Dependency chain design: Keep chains short; only add dependencies for tasks that truly require ordering, let others run in parallel, avoiding over-serialization that reduces throughput.
|
||||
|
||||
**Resume Failed Tasks**:
|
||||
```bash
|
||||
# Use session_id from previous output to resume
|
||||
codex-wrapper --parallel <<'EOF'
|
||||
---TASK---
|
||||
id: T2
|
||||
session_id: 019xxx-previous-session-id
|
||||
---CONTENT---
|
||||
fix the previous error and retry
|
||||
EOF
|
||||
```
|
||||
|
||||
**Output**: Human-readable text format
|
||||
```
|
||||
=== Parallel Execution Summary ===
|
||||
Total: 3 | Success: 2 | Failed: 1
|
||||
|
||||
--- Task: T1 ---
|
||||
Status: SUCCESS
|
||||
Session: 019xxx
|
||||
|
||||
Task output message...
|
||||
|
||||
--- Task: T2 ---
|
||||
Status: FAILED (exit code 1)
|
||||
Error: some error message
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- Automatic topological sorting based on dependencies
|
||||
- Unlimited concurrency for independent tasks
|
||||
- Error isolation (failed tasks don't stop others)
|
||||
- Dependency blocking (dependent tasks skip if parent fails)
|
||||
|
||||
## Notes
|
||||
|
||||
- **Binary distribution**: Single Go binary, zero dependencies
|
||||
- **Installation**: Download from GitHub Releases or use install.sh
|
||||
- **Cross-platform compatible**: Linux (amd64/arm64), macOS (amd64/arm64)
|
||||
- All automated runs must use the Bash tool with the fixed timeout to provide dual timeout protection and unified logging/exit semantics
|
||||
for automation (new sessions only)
|
||||
- Uses `--skip-git-repo-check` to work in any directory
|
||||
- Streams progress, returns only final agent message
|
||||
- Every execution returns a session ID for resuming conversations
|
||||
- Requires Codex CLI installed and authenticated
|
||||
120
skills/gemini/SKILL.md
Normal file
120
skills/gemini/SKILL.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
name: gemini
|
||||
description: Execute Gemini CLI for AI-powered code analysis and generation. Use when you need to leverage Google's Gemini models for complex reasoning tasks.
|
||||
---
|
||||
|
||||
# Gemini CLI Integration
|
||||
|
||||
## Overview
|
||||
|
||||
Execute Gemini CLI commands with support for multiple models and flexible prompt input. Integrates Google's Gemini AI models into Claude Code workflows.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Complex reasoning tasks requiring advanced AI capabilities
|
||||
- Code generation and analysis with Gemini models
|
||||
- Tasks requiring Google's latest AI technology
|
||||
- Alternative perspective on code problems
|
||||
|
||||
## Usage
|
||||
**Mandatory**: Run via uv with fixed timeout 7200000ms (foreground):
|
||||
```bash
|
||||
uv run ~/.claude/skills/gemini/scripts/gemini.py "<prompt>" [working_dir]
|
||||
```
|
||||
|
||||
**Optional** (direct execution or using Python):
|
||||
```bash
|
||||
~/.claude/skills/gemini/scripts/gemini.py "<prompt>" [working_dir]
|
||||
# or
|
||||
python3 ~/.claude/skills/gemini/scripts/gemini.py "<prompt>" [working_dir]
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- **GEMINI_MODEL**: Configure model (default: `gemini-3-pro-preview`)
|
||||
- Example: `export GEMINI_MODEL=gemini-3`
|
||||
|
||||
## Timeout Control
|
||||
|
||||
- **Fixed**: 7200000 milliseconds (2 hours), immutable
|
||||
- **Bash tool**: Always set `timeout: 7200000` for double protection
|
||||
|
||||
### Parameters
|
||||
|
||||
- `prompt` (required): Task prompt or question
|
||||
- `working_dir` (optional): Working directory (default: current directory)
|
||||
|
||||
### Return Format
|
||||
|
||||
Plain text output from Gemini:
|
||||
|
||||
```text
|
||||
Model response text here...
|
||||
```
|
||||
|
||||
Error format (stderr):
|
||||
|
||||
```text
|
||||
ERROR: Error message
|
||||
```
|
||||
|
||||
### Invocation Pattern
|
||||
|
||||
When calling via Bash tool, always include the timeout parameter:
|
||||
|
||||
```yaml
|
||||
Bash tool parameters:
|
||||
- command: uv run ~/.claude/skills/gemini/scripts/gemini.py "<prompt>"
|
||||
- timeout: 7200000
|
||||
- description: <brief description of the task>
|
||||
```
|
||||
|
||||
Alternatives:
|
||||
|
||||
```yaml
|
||||
# Direct execution (simplest)
|
||||
- command: ~/.claude/skills/gemini/scripts/gemini.py "<prompt>"
|
||||
|
||||
# Using python3
|
||||
- command: python3 ~/.claude/skills/gemini/scripts/gemini.py "<prompt>"
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
**Basic query:**
|
||||
|
||||
```bash
|
||||
uv run ~/.claude/skills/gemini/scripts/gemini.py "explain quantum computing"
|
||||
# timeout: 7200000
|
||||
```
|
||||
|
||||
**Code analysis:**
|
||||
|
||||
```bash
|
||||
uv run ~/.claude/skills/gemini/scripts/gemini.py "review this code for security issues: $(cat app.py)"
|
||||
# timeout: 7200000
|
||||
```
|
||||
|
||||
**With specific working directory:**
|
||||
|
||||
```bash
|
||||
uv run ~/.claude/skills/gemini/scripts/gemini.py "analyze project structure" "/path/to/project"
|
||||
# timeout: 7200000
|
||||
```
|
||||
|
||||
**Using python3 directly (alternative):**
|
||||
|
||||
```bash
|
||||
python3 ~/.claude/skills/gemini/scripts/gemini.py "your prompt here"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- **Recommended**: Use `uv run` for automatic Python environment management (requires uv installed)
|
||||
- **Alternative**: Direct execution `./gemini.py` (uses system Python via shebang)
|
||||
- Python implementation using standard library (zero dependencies)
|
||||
- Cross-platform compatible (Windows/macOS/Linux)
|
||||
- PEP 723 compliant (inline script metadata)
|
||||
- Requires Gemini CLI installed and authenticated
|
||||
- Supports all Gemini model variants (configure via `GEMINI_MODEL` environment variable)
|
||||
- Output is streamed directly from Gemini CLI
|
||||
140
skills/gemini/scripts/gemini.py
Executable file
140
skills/gemini/scripts/gemini.py
Executable file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.8"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""
|
||||
Gemini CLI wrapper with cross-platform support.
|
||||
|
||||
Usage:
|
||||
uv run gemini.py "<prompt>" [workdir]
|
||||
python3 gemini.py "<prompt>"
|
||||
./gemini.py "your prompt"
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
DEFAULT_MODEL = os.environ.get('GEMINI_MODEL', 'gemini-3-pro-preview')
|
||||
DEFAULT_WORKDIR = '.'
|
||||
TIMEOUT_MS = 7_200_000 # 固定 2 小时,毫秒
|
||||
DEFAULT_TIMEOUT = TIMEOUT_MS // 1000
|
||||
FORCE_KILL_DELAY = 5
|
||||
|
||||
|
||||
def log_error(message: str):
|
||||
"""输出错误信息到 stderr"""
|
||||
sys.stderr.write(f"ERROR: {message}\n")
|
||||
|
||||
|
||||
def log_warn(message: str):
|
||||
"""输出警告信息到 stderr"""
|
||||
sys.stderr.write(f"WARN: {message}\n")
|
||||
|
||||
|
||||
def log_info(message: str):
|
||||
"""输出信息到 stderr"""
|
||||
sys.stderr.write(f"INFO: {message}\n")
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""解析位置参数"""
|
||||
if len(sys.argv) < 2:
|
||||
log_error('Prompt required')
|
||||
sys.exit(1)
|
||||
|
||||
return {
|
||||
'prompt': sys.argv[1],
|
||||
'workdir': sys.argv[2] if len(sys.argv) > 2 else DEFAULT_WORKDIR
|
||||
}
|
||||
|
||||
|
||||
def build_gemini_args(args) -> list:
|
||||
"""构建 gemini CLI 参数"""
|
||||
return [
|
||||
'gemini',
|
||||
'-m', DEFAULT_MODEL,
|
||||
'-p', args['prompt']
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
log_info('Script started')
|
||||
args = parse_args()
|
||||
log_info(f"Prompt length: {len(args['prompt'])}")
|
||||
log_info(f"Working dir: {args['workdir']}")
|
||||
gemini_args = build_gemini_args(args)
|
||||
timeout_sec = DEFAULT_TIMEOUT
|
||||
log_info(f"Timeout: {timeout_sec}s")
|
||||
|
||||
# 如果指定了工作目录,切换到该目录
|
||||
if args['workdir'] != DEFAULT_WORKDIR:
|
||||
try:
|
||||
os.chdir(args['workdir'])
|
||||
except FileNotFoundError:
|
||||
log_error(f"Working directory not found: {args['workdir']}")
|
||||
sys.exit(1)
|
||||
except PermissionError:
|
||||
log_error(f"Permission denied: {args['workdir']}")
|
||||
sys.exit(1)
|
||||
log_info('Changed working directory')
|
||||
|
||||
try:
|
||||
log_info(f"Starting gemini with model {DEFAULT_MODEL}")
|
||||
process = None
|
||||
# 启动 gemini 子进程,直接透传 stdout 和 stderr
|
||||
process = subprocess.Popen(
|
||||
gemini_args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1 # 行缓冲
|
||||
)
|
||||
|
||||
# 实时输出 stdout
|
||||
for line in process.stdout:
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.flush()
|
||||
|
||||
# 等待进程结束
|
||||
returncode = process.wait(timeout=timeout_sec)
|
||||
|
||||
# 读取 stderr
|
||||
stderr_output = process.stderr.read()
|
||||
if stderr_output:
|
||||
sys.stderr.write(stderr_output)
|
||||
|
||||
# 检查退出码
|
||||
if returncode != 0:
|
||||
log_error(f'Gemini exited with status {returncode}')
|
||||
sys.exit(returncode)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
log_error(f'Gemini execution timeout ({timeout_sec}s)')
|
||||
if process is not None:
|
||||
process.kill()
|
||||
try:
|
||||
process.wait(timeout=FORCE_KILL_DELAY)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
sys.exit(124)
|
||||
|
||||
except FileNotFoundError:
|
||||
log_error("gemini command not found in PATH")
|
||||
log_error("Please install Gemini CLI: https://github.com/google/generative-ai-python")
|
||||
sys.exit(127)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
if process is not None:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=FORCE_KILL_DELAY)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
sys.exit(130)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
362
skills/product-requirements/SKILL.md
Normal file
362
skills/product-requirements/SKILL.md
Normal file
@@ -0,0 +1,362 @@
|
||||
---
|
||||
name: product-requirements
|
||||
description: Interactive Product Owner skill for requirements gathering, analysis, and PRD generation. Triggers when users request product requirements, feature specification, PRD creation, or need help understanding and documenting project requirements. Uses quality scoring and iterative dialogue to ensure comprehensive requirements before generating professional PRD documents.
|
||||
---
|
||||
|
||||
# Product Requirements Skill
|
||||
|
||||
## Overview
|
||||
|
||||
Transform user requirements into professional Product Requirements Documents (PRDs) through interactive dialogue, quality scoring, and iterative refinement. Act as Sarah, a meticulous Product Owner who ensures requirements are clear, testable, and actionable before documentation.
|
||||
|
||||
## Core Identity
|
||||
|
||||
- **Role**: Technical Product Owner & Requirements Specialist
|
||||
- **Approach**: Systematic, quality-driven, user-focused
|
||||
- **Method**: Quality scoring (100-point scale) with 90+ threshold for PRD generation
|
||||
- **Output**: Professional yet concise PRDs saved to `docs/{feature-name}-prd.md`
|
||||
|
||||
## Interactive Process
|
||||
|
||||
### Step 1: Initial Understanding & Context Gathering
|
||||
|
||||
Greet as Sarah and immediately gather project context:
|
||||
|
||||
```
|
||||
"Hi! I'm Sarah, your Product Owner. I'll help define clear requirements for your feature.
|
||||
|
||||
Let me first understand your project context..."
|
||||
```
|
||||
|
||||
**Context gathering actions:**
|
||||
1. Read project README, package.json/pyproject.toml in parallel
|
||||
2. Understand tech stack, existing architecture, and conventions
|
||||
3. Present initial interpretation of the user's request within project context
|
||||
4. Ask: "Is this understanding correct? What would you like to add?"
|
||||
|
||||
**Early stop**: Once you can articulate the feature request clearly within the project's context, proceed to quality assessment.
|
||||
|
||||
### Step 2: Quality Assessment (100-Point System)
|
||||
|
||||
Evaluate requirements across five dimensions:
|
||||
|
||||
#### Scoring Breakdown:
|
||||
|
||||
**Business Value & Goals (30 points)**
|
||||
- 10 pts: Clear problem statement and business need
|
||||
- 10 pts: Measurable success metrics and KPIs
|
||||
- 10 pts: Expected outcomes and ROI justification
|
||||
|
||||
**Functional Requirements (25 points)**
|
||||
- 10 pts: Complete user stories with acceptance criteria
|
||||
- 10 pts: Clear feature descriptions and workflows
|
||||
- 5 pts: Edge cases and error handling defined
|
||||
|
||||
**User Experience (20 points)**
|
||||
- 8 pts: Well-defined user personas
|
||||
- 7 pts: User journey and interaction flows
|
||||
- 5 pts: UI/UX preferences and constraints
|
||||
|
||||
**Technical Constraints (15 points)**
|
||||
- 5 pts: Performance requirements
|
||||
- 5 pts: Security and compliance needs
|
||||
- 5 pts: Integration requirements
|
||||
|
||||
**Scope & Priorities (10 points)**
|
||||
- 5 pts: Clear MVP definition
|
||||
- 3 pts: Phased delivery plan
|
||||
- 2 pts: Priority rankings
|
||||
|
||||
**Display format:**
|
||||
```
|
||||
📊 Requirements Quality Score: [TOTAL]/100
|
||||
|
||||
Breakdown:
|
||||
- Business Value & Goals: [X]/30
|
||||
- Functional Requirements: [X]/25
|
||||
- User Experience: [X]/20
|
||||
- Technical Constraints: [X]/15
|
||||
- Scope & Priorities: [X]/10
|
||||
|
||||
[If < 90]: Let me ask targeted questions to improve clarity...
|
||||
[If ≥ 90]: Excellent! Ready to generate PRD.
|
||||
```
|
||||
|
||||
### Step 3: Targeted Clarification
|
||||
|
||||
**If score < 90**, use `AskUserQuestion` tool to clarify gaps. Focus on the lowest-scoring area first.
|
||||
|
||||
**Question categories by dimension:**
|
||||
|
||||
**Business Value (if <24/30):**
|
||||
- "What specific business problem are we solving?"
|
||||
- "How will we measure success?"
|
||||
- "What happens if we don't build this?"
|
||||
|
||||
**Functional Requirements (if <20/25):**
|
||||
- "Can you walk me through the main user workflows?"
|
||||
- "What should happen when [specific edge case]?"
|
||||
- "What are the must-have vs. nice-to-have features?"
|
||||
|
||||
**User Experience (if <16/20):**
|
||||
- "Who are the primary users?"
|
||||
- "What are their goals and pain points?"
|
||||
- "Can you describe the ideal user experience?"
|
||||
|
||||
**Technical Constraints (if <12/15):**
|
||||
- "What performance expectations do you have?"
|
||||
- "Are there security or compliance requirements?"
|
||||
- "What systems need to integrate with this?"
|
||||
|
||||
**Scope & Priorities (if <8/10):**
|
||||
- "What's the minimum viable product (MVP)?"
|
||||
- "How should we phase the delivery?"
|
||||
- "What are the top 3 priorities?"
|
||||
|
||||
**Ask 2-3 questions at a time** using `AskUserQuestion` tool. Don't overwhelm.
|
||||
|
||||
### Step 4: Iterative Refinement
|
||||
|
||||
After each user response:
|
||||
1. Update understanding
|
||||
2. Recalculate quality score
|
||||
3. Show progress: "Great! That improved [area] from X to Y."
|
||||
4. Continue until 90+ threshold met
|
||||
|
||||
### Step 5: Final Confirmation & PRD Generation
|
||||
|
||||
When score ≥ 90:
|
||||
|
||||
```
|
||||
"Excellent! Here's the final PRD summary:
|
||||
|
||||
[2-3 sentence executive summary]
|
||||
|
||||
📊 Final Quality Score: [SCORE]/100
|
||||
|
||||
Generating professional PRD at docs/{feature-name}-prd.md..."
|
||||
```
|
||||
|
||||
Generate PRD using template below, then confirm:
|
||||
```
|
||||
"✅ PRD saved to docs/{feature-name}-prd.md
|
||||
|
||||
Review the document and let me know if any adjustments are needed."
|
||||
```
|
||||
|
||||
## PRD Template (Streamlined Professional Version)
|
||||
|
||||
Save to: `docs/{feature-name}-prd.md`
|
||||
|
||||
```markdown
|
||||
# Product Requirements Document: [Feature Name]
|
||||
|
||||
**Version**: 1.0
|
||||
**Date**: [YYYY-MM-DD]
|
||||
**Author**: Sarah (Product Owner)
|
||||
**Quality Score**: [SCORE]/100
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
[2-3 paragraphs covering: what problem this solves, who it helps, and expected impact. Include business context and why this feature matters now.]
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
**Current Situation**: [Describe current pain points or limitations]
|
||||
|
||||
**Proposed Solution**: [High-level description of the feature]
|
||||
|
||||
**Business Impact**: [Quantifiable or qualitative expected outcomes]
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
**Primary KPIs:**
|
||||
- [Metric 1]: [Target value and measurement method]
|
||||
- [Metric 2]: [Target value and measurement method]
|
||||
- [Metric 3]: [Target value and measurement method]
|
||||
|
||||
**Validation**: [How and when we'll measure these metrics]
|
||||
|
||||
---
|
||||
|
||||
## User Personas
|
||||
|
||||
### Primary: [Persona Name]
|
||||
- **Role**: [User type]
|
||||
- **Goals**: [What they want to achieve]
|
||||
- **Pain Points**: [Current frustrations]
|
||||
- **Technical Level**: [Novice/Intermediate/Advanced]
|
||||
|
||||
[Add secondary persona if relevant]
|
||||
|
||||
---
|
||||
|
||||
## User Stories & Acceptance Criteria
|
||||
|
||||
### Story 1: [Story Title]
|
||||
|
||||
**As a** [persona]
|
||||
**I want to** [action]
|
||||
**So that** [benefit]
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] [Specific, testable criterion]
|
||||
- [ ] [Another criterion covering happy path]
|
||||
- [ ] [Edge case or error handling criterion]
|
||||
|
||||
### Story 2: [Story Title]
|
||||
|
||||
[Repeat structure]
|
||||
|
||||
[Continue for all core user stories - typically 3-5 for MVP]
|
||||
|
||||
---
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
### Core Features
|
||||
|
||||
**Feature 1: [Name]**
|
||||
- Description: [Clear explanation of functionality]
|
||||
- User flow: [Step-by-step interaction]
|
||||
- Edge cases: [What happens when...]
|
||||
- Error handling: [How system responds to failures]
|
||||
|
||||
**Feature 2: [Name]**
|
||||
[Repeat structure]
|
||||
|
||||
### Out of Scope
|
||||
- [Explicitly list what's NOT included in this release]
|
||||
- [Helps prevent scope creep]
|
||||
|
||||
---
|
||||
|
||||
## Technical Constraints
|
||||
|
||||
### Performance
|
||||
- [Response time requirements: e.g., "API calls < 200ms"]
|
||||
- [Scalability: e.g., "Support 10k concurrent users"]
|
||||
|
||||
### Security
|
||||
- [Authentication/authorization requirements]
|
||||
- [Data protection and privacy considerations]
|
||||
- [Compliance requirements: GDPR, SOC2, etc.]
|
||||
|
||||
### Integration
|
||||
- **[System 1]**: [Integration details and dependencies]
|
||||
- **[System 2]**: [Integration details]
|
||||
|
||||
### Technology Stack
|
||||
- [Required frameworks, libraries, or platforms]
|
||||
- [Compatibility requirements: browsers, devices, OS]
|
||||
- [Infrastructure constraints: cloud provider, database, etc.]
|
||||
|
||||
---
|
||||
|
||||
## MVP Scope & Phasing
|
||||
|
||||
### Phase 1: MVP (Required for Initial Launch)
|
||||
- [Core feature 1]
|
||||
- [Core feature 2]
|
||||
- [Core feature 3]
|
||||
|
||||
**MVP Definition**: [What's the minimum that delivers value?]
|
||||
|
||||
### Phase 2: Enhancements (Post-Launch)
|
||||
- [Enhancement 1]
|
||||
- [Enhancement 2]
|
||||
|
||||
### Future Considerations
|
||||
- [Potential future feature 1]
|
||||
- [Potential future feature 2]
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Probability | Impact | Mitigation Strategy |
|
||||
|------|------------|--------|---------------------|
|
||||
| [Risk 1: e.g., API rate limits] | High/Med/Low | High/Med/Low | [Specific mitigation plan] |
|
||||
| [Risk 2: e.g., User adoption] | High/Med/Low | High/Med/Low | [Mitigation plan] |
|
||||
| [Risk 3: e.g., Technical debt] | High/Med/Low | High/Med/Low | [Mitigation plan] |
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Blockers
|
||||
|
||||
**Dependencies:**
|
||||
- [Dependency 1]: [Description and owner]
|
||||
- [Dependency 2]: [Description]
|
||||
|
||||
**Known Blockers:**
|
||||
- [Blocker 1]: [Description and resolution plan]
|
||||
|
||||
---
|
||||
|
||||
## Appendix
|
||||
|
||||
### Glossary
|
||||
- **[Term]**: [Definition]
|
||||
- **[Term]**: [Definition]
|
||||
|
||||
### References
|
||||
- [Link to design mockups]
|
||||
- [Related documentation]
|
||||
- [Technical specs or API docs]
|
||||
|
||||
---
|
||||
|
||||
*This PRD was created through interactive requirements gathering with quality scoring to ensure comprehensive coverage of business, functional, UX, and technical dimensions.*
|
||||
```
|
||||
|
||||
## Communication Guidelines
|
||||
|
||||
### Tone
|
||||
- Professional yet approachable
|
||||
- Clear, jargon-free language
|
||||
- Collaborative and respectful
|
||||
|
||||
### Show Progress
|
||||
- Celebrate improvements: "Great! That really clarifies things."
|
||||
- Acknowledge complexity: "This is a complex requirement, let's break it down."
|
||||
- Be transparent: "I need more information about X to ensure quality."
|
||||
|
||||
### Handle Uncertainty
|
||||
- If user is unsure: "That's okay, let's explore some options..."
|
||||
- For assumptions: "I'll assume X based on typical patterns, but we can adjust."
|
||||
|
||||
## Important Behaviors
|
||||
|
||||
### DO:
|
||||
- Start with greeting and context gathering
|
||||
- Show quality scores transparently after assessment
|
||||
- Use `AskUserQuestion` tool for clarification (2-3 questions max per round)
|
||||
- Iterate until 90+ quality threshold
|
||||
- Generate PRD with proper feature name in filename
|
||||
- Maintain focus on actionable, testable requirements
|
||||
|
||||
### DON'T:
|
||||
- Skip context gathering phase
|
||||
- Accept vague requirements (iterate to 90+)
|
||||
- Overwhelm with too many questions at once
|
||||
- Proceed without quality threshold
|
||||
- Make assumptions without validation
|
||||
- Use overly technical jargon
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- ✅ Achieve 90+ quality score through systematic dialogue
|
||||
- ✅ Create concise, actionable PRD (not bloated documentation)
|
||||
- ✅ Save to `docs/{feature-name}-prd.md` with proper naming
|
||||
- ✅ Enable smooth handoff to development phase
|
||||
- ✅ Maintain positive, collaborative user engagement
|
||||
|
||||
---
|
||||
|
||||
**Remember**: Think in English, respond to user in Chinese. Quality over speed—iterate until requirements are truly clear.
|
||||
497
skills/prototype-prompt-generator/SKILL.md
Normal file
497
skills/prototype-prompt-generator/SKILL.md
Normal file
@@ -0,0 +1,497 @@
|
||||
---
|
||||
name: prototype-prompt-generator
|
||||
description: This skill should be used when users need to generate detailed, structured prompts for creating UI/UX prototypes. Trigger when users request help with "create a prototype prompt", "design a mobile app", "generate UI specifications", or need comprehensive design documentation for web/mobile applications. Works with multiple design systems including WeChat Work, iOS Native, Material Design, and Ant Design Mobile.
|
||||
---
|
||||
|
||||
# Prototype Prompt Generator
|
||||
|
||||
## Overview
|
||||
|
||||
Generate comprehensive, production-ready prompts for UI/UX prototype creation. Transform user requirements into detailed technical specifications that include design systems, color palettes, component specifications, layout structures, and implementation guidelines. Output prompts are structured for optimal consumption by AI tools or human developers building HTML/CSS/React prototypes.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Gather Requirements
|
||||
|
||||
Begin by collecting essential information from the user. Ask targeted questions to understand:
|
||||
|
||||
**Application Type & Purpose:**
|
||||
- What kind of application? (e.g., enterprise tool, e-commerce, social media, dashboard)
|
||||
- Who are the target users?
|
||||
- What are the primary use cases and workflows?
|
||||
|
||||
**Platform & Context:**
|
||||
- Target platform: iOS, Android, Web, WeChat Mini Program, or cross-platform?
|
||||
- Device: Mobile phone, tablet, desktop, or responsive?
|
||||
- Viewport dimensions if known (e.g., 375px for iPhone, 1200px for desktop)
|
||||
|
||||
**Design Preferences:**
|
||||
- Design style: WeChat Work, iOS Native, Material Design, Ant Design Mobile, or custom?
|
||||
- Brand colors or visual preferences?
|
||||
- Any design references or inspiration?
|
||||
|
||||
**Feature Requirements:**
|
||||
- Key pages and features needed
|
||||
- Navigation structure (tabs, drawer, stack navigation)
|
||||
- Data to display (metrics, lists, forms, media)
|
||||
- User interactions (tap, swipe, long-press, etc.)
|
||||
|
||||
**Content & Data:**
|
||||
- Actual content to display (realistic text, numbers, names)
|
||||
- Empty states, error states, loading states
|
||||
- Any specific business logic or rules
|
||||
|
||||
**Technical Constraints:**
|
||||
- Framework preference: Plain HTML, React, Vue, or framework-agnostic?
|
||||
- CSS approach: Tailwind CSS, CSS Modules, styled-components?
|
||||
- Image assets: Real images, placeholders, or specific sources?
|
||||
- CDN dependencies or version requirements?
|
||||
|
||||
**Ask questions incrementally** (2-3 at a time) to avoid overwhelming the user. Many details can be inferred from context or filled with sensible defaults.
|
||||
|
||||
### Step 2: Select Design System
|
||||
|
||||
Based on the gathered requirements, choose the appropriate design system from `references/design-systems.md`:
|
||||
|
||||
**WeChat Work Style:**
|
||||
- **When to use**: Chinese enterprise applications, work management tools, B2B platforms, internal business systems
|
||||
- **Characteristics**: Simple and professional, tech blue primary color, clear information hierarchy
|
||||
- **Key audience**: Chinese business users, corporate environments
|
||||
|
||||
**iOS Native Style:**
|
||||
- **When to use**: iOS-specific apps, Apple ecosystem integration, apps targeting iPhone/iPad users
|
||||
- **Characteristics**: Minimalist, spacious layouts, San Francisco font, system colors
|
||||
- **Key audience**: Apple users, consumer apps, content-focused applications
|
||||
|
||||
**Material Design Style:**
|
||||
- **When to use**: Android-first apps, Google ecosystem integration, cross-platform with Material UI
|
||||
- **Characteristics**: Bold graphics, elevation system, ripple effects, Roboto font
|
||||
- **Key audience**: Android users, Google services, developer tools
|
||||
|
||||
**Ant Design Mobile Style:**
|
||||
- **When to use**: Enterprise mobile applications with complex data entry and forms
|
||||
- **Characteristics**: Efficiency-oriented, consistent components, suitable for business applications
|
||||
- **Key audience**: Business users, enterprise mobile apps, data-heavy interfaces
|
||||
|
||||
**If the user hasn't specified a design system**, recommend one based on:
|
||||
- Geographic location: Chinese users → WeChat Work, Western users → iOS/Material
|
||||
- Platform: iOS → iOS Native, Android → Material Design
|
||||
- Application type: Enterprise B2B → WeChat Work or Ant Design, Consumer app → iOS or Material
|
||||
|
||||
Load the complete design system specifications from `references/design-systems.md` to ensure accurate color codes, component dimensions, and interaction patterns.
|
||||
|
||||
### Step 3: Structure the Prompt
|
||||
|
||||
Using the template from `references/prompt-structure.md`, construct a comprehensive prompt with these sections:
|
||||
|
||||
**1. Role Definition**
|
||||
Define expertise relevant to the prototype:
|
||||
```
|
||||
# Role
|
||||
You are a world-class UI/UX engineer and frontend developer, specializing in [specific domain] using [technologies].
|
||||
```
|
||||
|
||||
**2. Task Description**
|
||||
State clearly what to build and the design style:
|
||||
```
|
||||
# Task
|
||||
Create a [type] prototype for [application description].
|
||||
Design style must strictly follow [design system], with core keywords: [3-5 key attributes].
|
||||
```
|
||||
|
||||
**3. Tech Stack Specifications**
|
||||
List all technologies, frameworks, and resources:
|
||||
- File structure (single HTML, multi-page, component-based)
|
||||
- Framework and version (e.g., Tailwind CSS CDN)
|
||||
- Device simulation (viewport size, device chrome)
|
||||
- Asset sources (Unsplash, Pexels, real images)
|
||||
- Icon libraries (FontAwesome, Material Icons)
|
||||
- Custom configuration (Tailwind config, theme variables)
|
||||
|
||||
**4. Visual Design Requirements**
|
||||
Provide detailed specifications:
|
||||
|
||||
**(a) Color Palette:**
|
||||
Include all colors with hex codes:
|
||||
- Background colors (main, section, card)
|
||||
- Primary and accent colors with usage
|
||||
- Status colors (success, warning, error)
|
||||
- Text colors (title, body, secondary, disabled)
|
||||
- UI element colors (borders, dividers)
|
||||
|
||||
**(b) UI Style Characteristics:**
|
||||
Specify for each component type:
|
||||
- Cards: background, radius, shadow, border, padding
|
||||
- Buttons: variants (primary, secondary, ghost), dimensions, states
|
||||
- Icons: style, sizes, colors, containers
|
||||
- List items: layout, height, divider style, active state
|
||||
- Shadows: type and usage
|
||||
|
||||
**(c) Layout Structure:**
|
||||
Describe each major section:
|
||||
- Top navigation bar: height, title style, icons, background
|
||||
- Content areas: grids, cards, lists, spacing
|
||||
- Quick access areas: icon grids, layouts
|
||||
- Data display cards: metrics, layout, styling
|
||||
- Feature lists: structure, icons, interactions
|
||||
- Bottom tab bar: height, tabs, active/inactive states, badges
|
||||
|
||||
**(d) Specific Page Content:**
|
||||
Provide actual content, not placeholders:
|
||||
- Real page titles and section headings
|
||||
- Actual data points (numbers, names, dates)
|
||||
- Feature names and descriptions
|
||||
- Button labels and link text
|
||||
- Sample list items with realistic content
|
||||
|
||||
**5. Implementation Details**
|
||||
Cover technical specifics:
|
||||
- Page width and centering approach
|
||||
- Layout systems (Flexbox, Grid, or both)
|
||||
- Fixed/sticky positioning for navigation
|
||||
- Spacing scale (margins, padding, gaps)
|
||||
- Typography (font family, sizes, weights)
|
||||
- Interactive states (hover, active, focus, disabled)
|
||||
- Icon sources and usage
|
||||
- Border and divider styling
|
||||
|
||||
**6. Tailwind Configuration**
|
||||
If using Tailwind CSS, provide custom config:
|
||||
```javascript
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'brand-primary': '#3478F6',
|
||||
// ... all custom colors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**7. Content Structure & Hierarchy**
|
||||
Visualize the page structure as a tree:
|
||||
```
|
||||
Page Name
|
||||
├─ Section 1
|
||||
│ ├─ Element 1
|
||||
│ └─ Element 2
|
||||
├─ Section 2
|
||||
│ ├─ Subsection A
|
||||
│ │ ├─ Item 1
|
||||
│ │ └─ Item 2
|
||||
│ └─ Subsection B
|
||||
└─ Section 3
|
||||
```
|
||||
|
||||
**8. Special Requirements**
|
||||
Highlight unique considerations:
|
||||
- Design system-specific guidelines
|
||||
- Primary color application scenarios
|
||||
- Interaction details (tap feedback, animations, gestures)
|
||||
- Accessibility requirements (contrast, touch targets, ARIA)
|
||||
- Performance considerations (image optimization, lazy loading)
|
||||
|
||||
**9. Output Format**
|
||||
Specify the exact deliverable:
|
||||
```
|
||||
# Output Format
|
||||
|
||||
Please output complete [file type] code, ensuring:
|
||||
1. [Requirement 1]
|
||||
2. [Requirement 2]
|
||||
...
|
||||
|
||||
The output should be production-ready and viewable at [viewport] on [device].
|
||||
```
|
||||
|
||||
### Step 4: Populate with Specifics
|
||||
|
||||
Replace all template placeholders with concrete values:
|
||||
|
||||
**Replace vague terms with precise specifications:**
|
||||
- ❌ "Use blue colors" → ✅ "Primary: #3478F6 (tech blue), Link: #576B95 (link blue)"
|
||||
- ❌ "Make buttons rounded" → ✅ "Border radius: 4px (Tailwind: rounded)"
|
||||
- ❌ "Add some spacing" → ✅ "Card spacing: 12px, page margins: 16px"
|
||||
- ❌ "Display user info" → ✅ "Show username (15px bold), email (13px gray), avatar (48px circle)"
|
||||
|
||||
**Use real content, not placeholders:**
|
||||
- ❌ "Lorem ipsum dolor sit amet" → ✅ "Customer Total: 14, Today's New Customers: 1, Today's Revenue: ¥0.00"
|
||||
- ❌ "[Company Name]" → ✅ "Acme Insurance Co."
|
||||
- ❌ "Feature 1, Feature 2, Feature 3" → ✅ "Customer Contact, Customer Moments, Customer Groups"
|
||||
|
||||
**Specify all measurements:**
|
||||
- Component heights (44px, 50px, 64px)
|
||||
- Font sizes (13px, 15px, 16px, 18px)
|
||||
- Spacing values (8px, 12px, 16px, 24px)
|
||||
- Icon sizes (24px, 32px, 48px)
|
||||
- Border radius (4px, 8px, 10px)
|
||||
|
||||
**Define all states:**
|
||||
- Normal: base colors and styles
|
||||
- Hover: if applicable (desktop)
|
||||
- Active/Pressed: opacity or background changes
|
||||
- Disabled: grayed out with reduced opacity
|
||||
- Selected: highlight color (often primary brand color)
|
||||
|
||||
**Include all colors:**
|
||||
Every color mentioned must have a hex code. Reference the chosen design system from `references/design-systems.md` for accurate values.
|
||||
|
||||
### Step 5: Quality Assurance
|
||||
|
||||
Before presenting the final prompt, verify against the checklist in `references/prompt-structure.md`:
|
||||
|
||||
**Completeness Check:**
|
||||
- [ ] Role clearly defined with relevant expertise
|
||||
- [ ] Task explicitly states what to build and design style
|
||||
- [ ] All tech stack components listed with versions/CDNs
|
||||
- [ ] Complete color palette with hex codes for all colors
|
||||
- [ ] All UI components specified with exact dimensions and styles
|
||||
- [ ] Page layout fully described with precise measurements
|
||||
- [ ] Actual, realistic content provided (no placeholders like "Lorem Ipsum" or "[Name]")
|
||||
- [ ] Implementation details cover all technical requirements
|
||||
- [ ] Tailwind config included if using Tailwind CSS
|
||||
- [ ] Content hierarchy visualized as a tree structure
|
||||
- [ ] Special requirements and interactions documented
|
||||
- [ ] Output format clearly defined with all deliverables
|
||||
|
||||
**Clarity Check:**
|
||||
- [ ] No ambiguous terms or vague descriptions (e.g., "some padding", "nice colors")
|
||||
- [ ] All measurements specified with units (px, rem, %, vh, etc.)
|
||||
- [ ] All colors defined with hex codes (e.g., #3478F6, not just "blue")
|
||||
- [ ] Component states described (normal, hover, active, disabled, selected)
|
||||
- [ ] Layout relationships clear (parent-child, spacing, alignment, z-index)
|
||||
|
||||
**Specificity Check:**
|
||||
- [ ] Design system explicitly named (WeChat Work, iOS Native, Material Design, etc.)
|
||||
- [ ] Viewport dimensions provided (e.g., 375px × 812px for iPhone)
|
||||
- [ ] Typography scale defined (sizes, weights, line heights)
|
||||
- [ ] Interactive behaviors documented with timing if animated
|
||||
- [ ] Edge cases considered (long text overflow, empty states, loading, errors)
|
||||
|
||||
**Realism Check:**
|
||||
- [ ] Real content examples, not Latin placeholder text
|
||||
- [ ] Authentic data points (realistic numbers, names, dates, amounts)
|
||||
- [ ] Practical feature set (not overengineered or underspecified)
|
||||
- [ ] Appropriate complexity for the stated use case
|
||||
|
||||
**Technical Accuracy Check:**
|
||||
- [ ] Valid Tailwind class names (if using Tailwind)
|
||||
- [ ] Correct CDN links with versions (e.g., https://cdn.tailwindcss.com)
|
||||
- [ ] Proper HTML structure implied (semantic elements, hierarchy)
|
||||
- [ ] Feasible layout techniques (Flexbox/Grid patterns that work)
|
||||
- [ ] Accessible markup considerations (touch targets ≥44px, color contrast)
|
||||
|
||||
If any checks fail, refine the prompt before proceeding.
|
||||
|
||||
### Step 6: Present and Iterate
|
||||
|
||||
**Present the generated prompt to the user** with a brief explanation:
|
||||
- What design system was selected and why
|
||||
- Key design decisions made
|
||||
- Any assumptions or defaults applied
|
||||
- How to use the prompt (copy and provide to another AI tool or developer)
|
||||
|
||||
**Offer refinement options:**
|
||||
- "Would you like to adjust any colors or spacing?"
|
||||
- "Should we add more pages or features?"
|
||||
- "Do you want to change the design system?"
|
||||
- "Any specific interactions or animations to emphasize?"
|
||||
|
||||
**Iterate based on feedback:**
|
||||
If the user requests changes:
|
||||
1. Update the relevant sections of the prompt
|
||||
2. Maintain consistency across all sections
|
||||
3. Re-verify against the quality checklist
|
||||
4. Present the updated prompt
|
||||
|
||||
**Save or Export:**
|
||||
Offer to save the prompt to a file:
|
||||
- Markdown file for documentation
|
||||
- Text file for easy copying
|
||||
- Include as a code block for immediate use
|
||||
|
||||
## Best Practices
|
||||
|
||||
**1. Default to High Quality:**
|
||||
Even if the user provides minimal requirements, generate a comprehensive prompt. It's easier to remove details than to add them later. Include:
|
||||
- Complete color palettes (8-12 colors minimum)
|
||||
- All common UI components (buttons, cards, lists, inputs)
|
||||
- Multiple component states (normal, active, disabled)
|
||||
- Responsive considerations
|
||||
- Accessibility basics (contrast, touch targets)
|
||||
|
||||
**2. Use Design System Defaults Intelligently:**
|
||||
When user requirements are vague:
|
||||
- Apply the full design system consistently
|
||||
- Use standard component dimensions from the design system
|
||||
- Follow established patterns (e.g., WeChat Work's 64px list items)
|
||||
- Include typical interaction patterns for the platform
|
||||
|
||||
**3. Prioritize Clarity Over Brevity:**
|
||||
Longer, detailed prompts produce better prototypes than short, vague ones. Include:
|
||||
- Exact hex codes instead of color names
|
||||
- Precise measurements instead of relative terms
|
||||
- Specific component layouts instead of general descriptions
|
||||
- Actual content instead of placeholder text
|
||||
|
||||
**4. Think Mobile-First:**
|
||||
For mobile applications, always consider:
|
||||
- Safe areas (iOS notch, Android gesture bar)
|
||||
- Touch target sizes (minimum 44px × 44px)
|
||||
- Thumb-reachable zones (bottom navigation over top)
|
||||
- Portrait orientation primarily (landscape as secondary)
|
||||
- One-handed operation where possible
|
||||
|
||||
**5. Balance Flexibility and Specificity:**
|
||||
- Be specific about core design elements (colors, typography, key components)
|
||||
- Allow flexibility in implementation details (exact animation timing, minor spacing adjustments)
|
||||
- Specify "must-haves" clearly, mark "nice-to-haves" as optional
|
||||
|
||||
**6. Consider the Full User Journey:**
|
||||
Include specifications for:
|
||||
- Entry points (splash screen, onboarding if applicable)
|
||||
- Primary workflows (happy path through key features)
|
||||
- Edge cases (empty states, error states, loading states)
|
||||
- Exit points (logout, back navigation, completion states)
|
||||
|
||||
**7. Provide Context, Not Just Specs:**
|
||||
Explain the "why" behind design decisions:
|
||||
- "Tech blue (#3478F6) for trust and professionalism in enterprise context"
|
||||
- "64px list item height for comfortable thumb tapping on mobile"
|
||||
- "Fixed bottom tab bar for quick access to primary features"
|
||||
|
||||
**8. Validate Technical Feasibility:**
|
||||
Before finalizing the prompt:
|
||||
- Ensure CSS/Tailwind classes can achieve the described design
|
||||
- Verify that layout patterns work with the stated grid/flexbox approach
|
||||
- Confirm that the specified viewport can accommodate all content
|
||||
- Check that CDN links and versions are correct and available
|
||||
|
||||
**9. Make It Actionable:**
|
||||
The prompt should enable immediate implementation:
|
||||
- Include all necessary CDN links and imports
|
||||
- Provide complete Tailwind config (no "...add more as needed")
|
||||
- Specify file structure and organization
|
||||
- Define clear deliverables (HTML file, React components, etc.)
|
||||
|
||||
**10. Anticipate Questions:**
|
||||
Address common uncertainties in the prompt:
|
||||
- Font fallbacks (e.g., "sans-serif" system font stack)
|
||||
- Image dimensions and aspect ratios
|
||||
- Icon usage (when to use FontAwesome vs SVG vs emoji)
|
||||
- Z-index layering (what's on top)
|
||||
- Overflow behavior (scroll, truncate, wrap)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Enterprise Work Dashboard (WeChat Work Style)
|
||||
**Typical Structure:**
|
||||
- Top navigation bar (44px, title + search/menu icons)
|
||||
- Quick access grid (4-column icon grid)
|
||||
- Data summary cards (key metrics in horizontal layout)
|
||||
- Feature list (icon + text rows, 64px height each)
|
||||
- Bottom tab bar (5 tabs, 50px height)
|
||||
|
||||
**Key Elements:**
|
||||
- Tech blue (#3478F6) for primary actions and active states
|
||||
- White cards with subtle shadows on light gray background
|
||||
- 48px icons with rounded-lg containers
|
||||
- Right arrow indicators for navigation
|
||||
|
||||
### Pattern 2: iOS Consumer App (iOS Native Style)
|
||||
**Typical Structure:**
|
||||
- Large title navigation bar (96px when expanded)
|
||||
- Card-based content sections
|
||||
- System standard lists (44px minimum row height)
|
||||
- Tab bar with SF Symbols icons
|
||||
|
||||
**Key Elements:**
|
||||
- System blue (#007AFF) for interactive elements
|
||||
- Generous whitespace (20px margins, 16px padding)
|
||||
- Subtle dividers with left inset
|
||||
- Translucent blur effects on navigation
|
||||
|
||||
### Pattern 3: Android App (Material Design Style)
|
||||
**Typical Structure:**
|
||||
- Top app bar (56px on mobile, 64px on tablet)
|
||||
- FAB (Floating Action Button) for primary action
|
||||
- Card-based content with elevation
|
||||
- Bottom navigation or navigation drawer
|
||||
|
||||
**Key Elements:**
|
||||
- Bold primary color (#6200EE) with elevation shadows
|
||||
- Ripple effects on tap
|
||||
- 16dp grid system
|
||||
- Material icons (24px)
|
||||
|
||||
### Pattern 4: Enterprise Form App (Ant Design Mobile)
|
||||
**Typical Structure:**
|
||||
- Simple navigation bar (45px)
|
||||
- Form sections with grouped inputs
|
||||
- List views with detailed information
|
||||
- Fixed bottom action bar with primary button
|
||||
|
||||
**Key Elements:**
|
||||
- Professional blue (#108EE9) for actions
|
||||
- Dense information layout
|
||||
- Clear form field labels and validation
|
||||
- Breadcrumb or step indicators for multi-step flows
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue: User requirements are too vague**
|
||||
**Solution:** Ask focused questions, provide examples of similar apps, suggest design systems to choose from, or create a default prompt and offer iteration.
|
||||
|
||||
**Issue: User wants multiple design styles mixed**
|
||||
**Solution:** Pick a primary design system for overall structure and consistency, then incorporate specific elements from other systems as accent features. Explain trade-offs.
|
||||
|
||||
**Issue: User specifies impossible or conflicting requirements**
|
||||
**Solution:** Identify the conflict, explain why it's problematic (e.g., "64px icons won't fit in a 44px navigation bar"), suggest alternatives, and seek clarification.
|
||||
|
||||
**Issue: Too many features for one prompt**
|
||||
**Solution:** Focus on the primary page/workflow first, generate that prompt, then create separate prompts for additional features. Maintain consistency across prompts.
|
||||
|
||||
**Issue: User lacks technical knowledge**
|
||||
**Solution:** Avoid jargon, explain design decisions in plain language, provide visual descriptions instead of technical terms, and include helpful comments in the prompt.
|
||||
|
||||
**Issue: Prototype prompt doesn't produce good results**
|
||||
**Solution:** Review against the quality checklist, ensure all colors have hex codes, verify all measurements are specified, add more specific content examples, check for ambiguous language.
|
||||
|
||||
## Resources
|
||||
|
||||
This skill includes reference documentation to support prompt generation:
|
||||
|
||||
### references/design-systems.md
|
||||
Comprehensive specifications for major design systems:
|
||||
- **WeChat Work Style**: Chinese enterprise applications
|
||||
- **iOS Native Style**: Apple ecosystem apps
|
||||
- **Material Design**: Google/Android apps
|
||||
- **Ant Design Mobile**: Enterprise mobile apps
|
||||
|
||||
Each design system includes:
|
||||
- Complete color palettes with hex codes
|
||||
- Component specifications (dimensions, spacing, states)
|
||||
- Typography scales (sizes, weights, line heights)
|
||||
- Interaction patterns (animations, gestures, feedback)
|
||||
- Layout guidelines (grids, spacing, safe areas)
|
||||
- Code examples (Tailwind classes, CSS snippets)
|
||||
|
||||
**When to reference:** Always load this file when generating a prompt to ensure accurate design system specifications. Use it to populate color values, component dimensions, and interaction patterns.
|
||||
|
||||
### references/prompt-structure.md
|
||||
Detailed template and guidelines for prompt construction:
|
||||
- Standard prompt structure (9 sections)
|
||||
- Template syntax with placeholders
|
||||
- Examples for each section
|
||||
- Quality checklist (completeness, clarity, specificity)
|
||||
- Workflow guidance (requirements → prompt → iteration)
|
||||
- Tips for effective prompts
|
||||
- Common pitfalls to avoid
|
||||
|
||||
**When to reference:** Use this as the skeleton for every generated prompt. It ensures consistency and completeness across all prompts you create.
|
||||
|
||||
---
|
||||
|
||||
**Note:** This skill generates prompts for prototype creation—it does not create the prototypes themselves. The output is a comprehensive text prompt that can be provided to another AI tool, developer, or design tool to generate the actual HTML/CSS/React code.
|
||||
388
skills/prototype-prompt-generator/references/design-systems.md
Normal file
388
skills/prototype-prompt-generator/references/design-systems.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# Design Systems Reference
|
||||
|
||||
This document provides detailed specifications for common design systems used in mobile and web applications.
|
||||
|
||||
## 企业微信 (WeChat Work) Style
|
||||
|
||||
### Core Characteristics
|
||||
- **Simplicity & Professionalism**: Clean interface with clear information hierarchy
|
||||
- **Operational Clarity**: Clear action paths with explicit next steps
|
||||
- **Tech Blue**: Primary color scheme emphasizing efficiency and trust
|
||||
|
||||
### Color Palette
|
||||
```
|
||||
Primary Colors:
|
||||
- Tech Blue: #3478F6 (buttons, icons, emphasis)
|
||||
- Link Blue: #576B95 (hyperlinks)
|
||||
- Alert Red: #FA5151 (warnings, errors)
|
||||
- Warning Orange: #FF976A (cautions)
|
||||
|
||||
Neutral Colors:
|
||||
- Title Black: #191919 (headings)
|
||||
- Text Gray: #333333 (body text)
|
||||
- Light Text: #999999 (secondary text)
|
||||
- Divider: #E5E5E5 (borders, separators)
|
||||
- Background Area: #F7F8FA (section backgrounds)
|
||||
- White: #FFFFFF (card backgrounds)
|
||||
```
|
||||
|
||||
### Component Specifications
|
||||
|
||||
#### Cards
|
||||
- Background: White (#FFFFFF)
|
||||
- Border Radius: 8px (rounded-lg)
|
||||
- Shadow: Subtle (shadow-sm)
|
||||
- No thick borders
|
||||
- Spacing: 12px between cards
|
||||
|
||||
#### Buttons
|
||||
- Primary: Blue background (#3478F6), white text
|
||||
- Height: 44px
|
||||
- Border Radius: 4px (rounded)
|
||||
- Active State: 90% opacity (active:opacity-90)
|
||||
|
||||
#### Icons
|
||||
- Style: Rounded square background (rounded-lg) or pure icon
|
||||
- Primary Icons: Tech Blue (#3478F6)
|
||||
- Sizes: 24px / 32px / 48px
|
||||
|
||||
#### List Items
|
||||
- Layout: Left icon/avatar (48px) + Title (15px bold) + Subtitle (13px gray) + Right arrow
|
||||
- Height: 64px
|
||||
- Divider: Left indent 64px (aligned with content)
|
||||
- Active State: Gray background (active:bg-gray-50)
|
||||
|
||||
#### Navigation Bar (Top)
|
||||
- Height: 44px
|
||||
- Title: Centered, 16px bold, deep black
|
||||
- Background: White with 1px bottom border (border-gray-200)
|
||||
- Icons: Search and menu icons on right
|
||||
|
||||
#### Tab Bar (Bottom)
|
||||
- Height: 50px
|
||||
- Layout: Icon + Text (vertical)
|
||||
- Selected: Tech Blue (#3478F6)
|
||||
- Unselected: Gray
|
||||
- Position: Fixed bottom (fixed bottom-0)
|
||||
- Badge Support: Red circular badge with white text
|
||||
|
||||
### Typography
|
||||
- System Default: Sans-serif
|
||||
- Headings: font-semibold or font-bold
|
||||
- Title: 16px bold
|
||||
- Body: 15px
|
||||
- Caption: 13px
|
||||
- Helper Text: 13px gray
|
||||
|
||||
### Spacing & Layout
|
||||
- Page Margins: 16px (left/right)
|
||||
- Card Spacing: 12px
|
||||
- Content Padding: 16px
|
||||
- Mobile Width: 375px (max-w-[375px])
|
||||
|
||||
### Interaction Patterns
|
||||
- Tap Feedback: Background changes to light gray (bg-gray-50)
|
||||
- Button Press: Opacity reduces to 90%
|
||||
- Card Elevation: Subtle shadow (shadow-sm)
|
||||
- List Navigation: Right arrow indicator
|
||||
|
||||
---
|
||||
|
||||
## iOS Native Style
|
||||
|
||||
### Core Characteristics
|
||||
- **Minimalism**: Clean, spacious layouts with generous whitespace
|
||||
- **Clarity**: Clear visual hierarchy and focus on content
|
||||
- **Depth**: Subtle shadows and layering to create depth
|
||||
|
||||
### Color Palette
|
||||
```
|
||||
System Colors:
|
||||
- Blue: #007AFF (primary actions, links)
|
||||
- Green: #34C759 (success, positive actions)
|
||||
- Red: #FF3B30 (destructive actions, errors)
|
||||
- Orange: #FF9500 (warnings)
|
||||
- Yellow: #FFCC00 (cautions)
|
||||
- Gray: #8E8E93 (secondary text)
|
||||
|
||||
Background Colors:
|
||||
- System Background: #FFFFFF (light mode), #000000 (dark mode)
|
||||
- Secondary Background: #F2F2F7 (light mode), #1C1C1E (dark mode)
|
||||
- Tertiary Background: #FFFFFF (light mode), #2C2C2E (dark mode)
|
||||
|
||||
Text Colors:
|
||||
- Primary: #000000 (light mode), #FFFFFF (dark mode)
|
||||
- Secondary: #3C3C43 (60% opacity)
|
||||
- Tertiary: #3C3C43 (30% opacity)
|
||||
```
|
||||
|
||||
### Component Specifications
|
||||
|
||||
#### Navigation Bar
|
||||
- Height: 44px (compact), 96px (large title)
|
||||
- Title: 34px bold (large), 17px semibold (inline)
|
||||
- Background: Translucent blur effect
|
||||
- Buttons: Text or icon, 17px regular
|
||||
|
||||
#### Tab Bar
|
||||
- Height: 49px
|
||||
- Layout: Icon + Text (vertical, 10pt text)
|
||||
- Selected: Blue (#007AFF)
|
||||
- Unselected: Gray (#8E8E93)
|
||||
- Position: Fixed bottom
|
||||
- Blur Effect: Translucent background
|
||||
|
||||
#### List/Table View
|
||||
- Cell Height: 44px minimum
|
||||
- Layout: Left accessory + Title + Detail + Right accessory/chevron
|
||||
- Separator: 0.5px hairline, inset from left
|
||||
- Active State: Gray highlight (bg-gray-100)
|
||||
|
||||
#### Buttons
|
||||
- Primary: Blue background (#007AFF), white text, 50px height, rounded corners (10px)
|
||||
- Secondary: No background, blue text
|
||||
- Destructive: Red text
|
||||
- Corner Radius: 10px (rounded-lg)
|
||||
|
||||
#### Cards
|
||||
- Border Radius: 10px (rounded-lg)
|
||||
- Background: White (light mode), #1C1C1E (dark mode)
|
||||
- Shadow: Subtle (0 1px 3px rgba(0,0,0,0.1))
|
||||
- Padding: 16px
|
||||
|
||||
### Typography (San Francisco Font)
|
||||
- Large Title: 34px bold
|
||||
- Title 1: 28px regular
|
||||
- Title 2: 22px regular
|
||||
- Title 3: 20px regular
|
||||
- Headline: 17px semibold
|
||||
- Body: 17px regular
|
||||
- Callout: 16px regular
|
||||
- Subheadline: 15px regular
|
||||
- Footnote: 13px regular
|
||||
- Caption 1: 12px regular
|
||||
- Caption 2: 11px regular
|
||||
|
||||
### Spacing
|
||||
- Edge Margins: 16px (standard), 20px (large screens)
|
||||
- Inter-Element Spacing: 8px / 16px / 24px
|
||||
- Section Spacing: 32px
|
||||
|
||||
### Interaction Patterns
|
||||
- Tap Feedback: Subtle highlight (no ripple effect)
|
||||
- Swipe Gestures: Swipe to delete, swipe back navigation
|
||||
- Pull to Refresh: System spinner at top
|
||||
- Haptic Feedback: Light impact on selection
|
||||
|
||||
---
|
||||
|
||||
## Material Design (Google) Style
|
||||
|
||||
### Core Characteristics
|
||||
- **Material Metaphor**: Physical surfaces and realistic motion
|
||||
- **Bold Graphics**: Intentional use of color, imagery, typography
|
||||
- **Motion**: Meaningful animations that provide feedback
|
||||
|
||||
### Color Palette
|
||||
```
|
||||
Primary Colors:
|
||||
- Primary: #6200EE (brand color, main actions)
|
||||
- Primary Variant: #3700B3 (darker shade)
|
||||
- Secondary: #03DAC6 (accent color, floating actions)
|
||||
- Secondary Variant: #018786 (darker accent)
|
||||
|
||||
Status Colors:
|
||||
- Error: #B00020 (errors, alerts)
|
||||
- Success: #4CAF50 (success states)
|
||||
- Warning: #FF9800 (warnings)
|
||||
- Info: #2196F3 (informational)
|
||||
|
||||
Background/Surface:
|
||||
- Background: #FFFFFF (light), #121212 (dark)
|
||||
- Surface: #FFFFFF (light), #121212 (dark)
|
||||
- Surface Variant: #F5F5F5 (light), #1E1E1E (dark)
|
||||
|
||||
Text Colors:
|
||||
- High Emphasis: #000000 (87% opacity)
|
||||
- Medium Emphasis: #000000 (60% opacity)
|
||||
- Disabled: #000000 (38% opacity)
|
||||
```
|
||||
|
||||
### Component Specifications
|
||||
|
||||
#### App Bar (Top)
|
||||
- Height: 56px (mobile), 64px (desktop)
|
||||
- Title: 20px medium weight
|
||||
- Background: Primary color or white
|
||||
- Icons: 24px, white or primary color
|
||||
- Elevation: 4dp
|
||||
|
||||
#### Bottom Navigation
|
||||
- Height: 56px
|
||||
- Layout: Icon (24px) + Label (12px)
|
||||
- Active: Primary color with ripple effect
|
||||
- Inactive: 60% opacity
|
||||
- Elevation: 8dp
|
||||
|
||||
#### Cards
|
||||
- Border Radius: 4px (rounded)
|
||||
- Background: White (light), #1E1E1E (dark)
|
||||
- Elevation: 1dp (resting), 8dp (raised)
|
||||
- Padding: 16px
|
||||
|
||||
#### Buttons
|
||||
- Contained: Primary color background, white text, 36px height, 4px radius
|
||||
- Outlined: 1px border, primary color text, transparent background
|
||||
- Text: No border/background, primary color text
|
||||
- Corner Radius: 4px (rounded)
|
||||
- Ripple Effect: Material ripple on tap
|
||||
|
||||
#### Lists
|
||||
- Item Height: 48px (single-line), 64px (two-line), 88px (three-line)
|
||||
- Left Icon: 24px (40px container)
|
||||
- Right Icon: 24px
|
||||
- Divider: 1px, 87% opacity
|
||||
- Ripple Effect: On tap
|
||||
|
||||
#### Floating Action Button (FAB)
|
||||
- Size: 56px (standard), 40px (mini)
|
||||
- Shape: Circular
|
||||
- Color: Secondary color
|
||||
- Elevation: 6dp (resting), 12dp (pressed)
|
||||
- Icon: 24px white
|
||||
|
||||
### Typography (Roboto Font)
|
||||
- H1: 96px light
|
||||
- H2: 60px light
|
||||
- H3: 48px regular
|
||||
- H4: 34px regular
|
||||
- H5: 24px regular
|
||||
- H6: 20px medium
|
||||
- Subtitle 1: 16px regular
|
||||
- Subtitle 2: 14px medium
|
||||
- Body 1: 16px regular
|
||||
- Body 2: 14px regular
|
||||
- Button: 14px medium, uppercase
|
||||
- Caption: 12px regular
|
||||
- Overline: 10px regular, uppercase
|
||||
|
||||
### Spacing & Grid
|
||||
- Base Unit: 8dp
|
||||
- Layout Grid: 8dp increments
|
||||
- Margins: 16dp (mobile), 24dp (tablet)
|
||||
- Gutters: 16dp (mobile), 24dp (tablet)
|
||||
- Touch Target: 48dp minimum
|
||||
|
||||
### Elevation System
|
||||
- Level 0: 0dp (background)
|
||||
- Level 1: 1dp (cards at rest)
|
||||
- Level 2: 2dp (buttons at rest)
|
||||
- Level 3: 3dp (refresh indicator)
|
||||
- Level 4: 4dp (app bar)
|
||||
- Level 6: 6dp (FAB at rest)
|
||||
- Level 8: 8dp (bottom nav, menus)
|
||||
- Level 12: 12dp (FAB pressed)
|
||||
- Level 16: 16dp (nav drawer)
|
||||
- Level 24: 24dp (dialog, picker)
|
||||
|
||||
### Interaction Patterns
|
||||
- Ripple Effect: Circular expanding animation from tap point
|
||||
- State Changes: Smooth color transitions (300ms)
|
||||
- Entry/Exit Animations: Fade + scale/slide
|
||||
- Touch Feedback: Immediate visual response
|
||||
|
||||
---
|
||||
|
||||
## Ant Design Mobile Style
|
||||
|
||||
### Core Characteristics
|
||||
- **Enterprise-Grade**: Professional UI for business applications
|
||||
- **Efficiency-Oriented**: Optimized for quick task completion
|
||||
- **Consistent**: Unified design language across platforms
|
||||
|
||||
### Color Palette
|
||||
```
|
||||
Primary Colors:
|
||||
- Brand Blue: #108EE9 (primary actions)
|
||||
- Link Blue: #108EE9 (links, emphasis)
|
||||
- Success: #00A854 (success states)
|
||||
- Warning: #FFBF00 (warnings)
|
||||
- Error: #F04134 (errors, destructive actions)
|
||||
|
||||
Neutral Colors:
|
||||
- Heading: #000000 (85% opacity)
|
||||
- Body Text: #000000 (65% opacity)
|
||||
- Secondary Text: #000000 (45% opacity)
|
||||
- Disabled: #000000 (25% opacity)
|
||||
- Border: #E9E9E9 (borders, dividers)
|
||||
- Background: #F5F5F5 (page background)
|
||||
- White: #FFFFFF (component background)
|
||||
```
|
||||
|
||||
### Component Specifications
|
||||
|
||||
#### Navigation Bar
|
||||
- Height: 45px
|
||||
- Title: 18px bold, centered
|
||||
- Background: #108EE9 or white
|
||||
- Icons: 22px
|
||||
- Border Bottom: 1px (#E9E9E9)
|
||||
|
||||
#### Tab Bar
|
||||
- Height: 50px
|
||||
- Layout: Icon (22px) + Text (10px)
|
||||
- Active: Brand blue (#108EE9)
|
||||
- Inactive: #888888
|
||||
- Badge: Red dot or number
|
||||
|
||||
#### List
|
||||
- Item Height: 44px minimum
|
||||
- Left Icon: 22px (44px container)
|
||||
- Right Arrow: 16px
|
||||
- Divider: 1px, inset 15px from left
|
||||
- Active State: #DDDDDD background
|
||||
|
||||
#### Buttons
|
||||
- Default: 47px height, 5px radius
|
||||
- Primary: Blue background (#108EE9), white text
|
||||
- Ghost: Transparent background, blue border and text
|
||||
- Disabled: Gray with 60% opacity
|
||||
|
||||
#### Cards
|
||||
- Border Radius: 2px (rounded-sm)
|
||||
- Background: White
|
||||
- Border: 1px (#E9E9E9) or none
|
||||
- Shadow: Optional subtle shadow
|
||||
- Padding: 15px
|
||||
|
||||
### Typography (System Default)
|
||||
- Heading: 18px bold
|
||||
- Subheading: 15px bold
|
||||
- Body: 14px regular
|
||||
- Caption: 12px regular
|
||||
- Button: 16px medium
|
||||
|
||||
### Spacing
|
||||
- Base Unit: 5px
|
||||
- Standard Spacing: 15px
|
||||
- Edge Margins: 15px
|
||||
- Component Padding: 15px
|
||||
|
||||
---
|
||||
|
||||
## Usage Guidelines
|
||||
|
||||
When generating a prototype prompt, reference the appropriate design system based on user requirements:
|
||||
|
||||
1. **WeChat Work Style**: Use for Chinese enterprise applications, work management tools, B2B platforms
|
||||
2. **iOS Native Style**: Use when user requests iOS-specific design or mentions Apple guidelines
|
||||
3. **Material Design**: Use for Android-first apps, Google ecosystem apps, or when cross-platform Material UI is requested
|
||||
4. **Ant Design Mobile**: Use for enterprise mobile applications with complex data and forms
|
||||
|
||||
For each design system, include:
|
||||
- Complete color palette with hex codes
|
||||
- Component specifications (dimensions, spacing, states)
|
||||
- Typography scale (sizes, weights, line heights)
|
||||
- Interaction patterns (tap feedback, animations)
|
||||
- Accessibility considerations
|
||||
- Code examples (Tailwind classes or CSS)
|
||||
530
skills/prototype-prompt-generator/references/prompt-structure.md
Normal file
530
skills/prototype-prompt-generator/references/prompt-structure.md
Normal file
@@ -0,0 +1,530 @@
|
||||
# Prototype Prompt Structure Template
|
||||
|
||||
This document provides a comprehensive template for generating high-quality prototype prompts. Each section includes guidelines and examples.
|
||||
|
||||
---
|
||||
|
||||
## Standard Prompt Structure
|
||||
|
||||
A well-structured prototype prompt should contain the following sections in order:
|
||||
|
||||
### 1. Role Definition
|
||||
Define the expertise and perspective Claude should adopt.
|
||||
|
||||
**Template:**
|
||||
```
|
||||
# Role
|
||||
You are a world-class [domain] engineer specializing in [specific skills]. You excel at [key capabilities].
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
# Role
|
||||
You are a world-class UI/UX engineer and frontend developer, specializing in creating professional, efficient enterprise-grade mobile application interfaces using Tailwind CSS.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Task Description
|
||||
Clearly state what needs to be created, with emphasis on key quality attributes.
|
||||
|
||||
**Template:**
|
||||
```
|
||||
# Task
|
||||
Create a [type of prototype] for [application description].
|
||||
Design style must strictly follow [design system/style guide], with core keywords: [3-5 key attributes].
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
# Task
|
||||
Create a high-fidelity prototype (HTML + Tailwind CSS) for an insurance agent work application.
|
||||
Design style must strictly follow WeChat Work style, with core keywords: simple and professional, clear information hierarchy, explicit action paths, tech blue primary color.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Tech Stack Specifications
|
||||
List all technologies, frameworks, and resources required.
|
||||
|
||||
**Template:**
|
||||
```
|
||||
# Tech Stack
|
||||
- File Structure: [describe output file organization]
|
||||
- Frameworks: [CSS frameworks, JS libraries, etc.]
|
||||
- Device Simulation: [viewport size, device features]
|
||||
- Asset Sources: [image sources, icon libraries]
|
||||
- CDN Resources: [external dependencies]
|
||||
- Custom Configuration: [theme extensions, variables]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
# Tech Stack
|
||||
- Each page outputs as an independent HTML file (e.g., home.html, discover.html)
|
||||
- index.html is the entry point, using iframe to display all pages
|
||||
- Simulate iPhone 16 Pro dimensions with rounded corners, including iOS status bar and bottom navigation
|
||||
- Use real UI image assets (from Unsplash, Pexels, Apple UI resources), no placeholders
|
||||
- Include Tailwind CSS (CDN: <script src="https://cdn.tailwindcss.com"></script>)
|
||||
- Use FontAwesome (CDN) for icons
|
||||
- Custom Tailwind config with brand colors: Tech Blue #3478F6, Link Blue #576B95, Alert Red #FA5151
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Visual Design Requirements
|
||||
Provide detailed specifications for visual design elements.
|
||||
|
||||
**Sections to include:**
|
||||
1. **Color Palette**: Primary, secondary, neutral, and status colors with hex codes
|
||||
2. **UI Style Characteristics**: Card design, buttons, icons, list items, shadows
|
||||
3. **Layout Structure**: Viewport dimensions, navigation bars, content areas, tab bars
|
||||
4. **Specific Page Content**: Actual text, data, and functional elements
|
||||
|
||||
**Template:**
|
||||
|
||||
```
|
||||
# Visual Design Requirements
|
||||
|
||||
## 1. Color Palette
|
||||
**Background Colors:**
|
||||
- Main Background: [color name] [hex code]
|
||||
- Section Background: [color name] [hex code]
|
||||
|
||||
**Primary Colors:**
|
||||
- Main Color: [color name] [hex code] (usage: [buttons, icons, emphasis])
|
||||
- Accent Color: [color name] [hex code] (usage: [links, secondary actions])
|
||||
|
||||
**Status Colors:**
|
||||
- Success: [hex code]
|
||||
- Warning: [hex code]
|
||||
- Error: [hex code]
|
||||
|
||||
**Text Colors:**
|
||||
- Title: [hex code]
|
||||
- Body: [hex code]
|
||||
- Secondary: [hex code]
|
||||
|
||||
**UI Elements:**
|
||||
- Divider: [hex code]
|
||||
- Border: [hex code]
|
||||
|
||||
## 2. UI Style Characteristics
|
||||
|
||||
**Card Design:**
|
||||
- Background: [color]
|
||||
- Border Radius: [size] ([Tailwind class])
|
||||
- Shadow: [type] ([Tailwind class])
|
||||
- Border: [style]
|
||||
- Spacing: [measurements]
|
||||
|
||||
**Buttons:**
|
||||
- Primary Button: [background] + [text color], height [px], radius [size] ([Tailwind class])
|
||||
- Active State: [opacity/color] ([Tailwind class])
|
||||
- Additional variants: [outline, ghost, text, etc.]
|
||||
|
||||
**Icons:**
|
||||
- Style: [rounded/square/circular] ([Tailwind class])
|
||||
- Primary Icon Color: [color]
|
||||
- Sizes: [24px / 32px / 48px]
|
||||
- Container: [if applicable]
|
||||
|
||||
**List Items:**
|
||||
- Layout: [Left element] + [Title size/weight] + [Subtitle size/color] + [Right element]
|
||||
- Height: [px]
|
||||
- Divider: [position, style]
|
||||
- Active State: [interaction feedback]
|
||||
|
||||
**Shadows:**
|
||||
- Style: [subtle/prominent] ([Tailwind classes])
|
||||
- Usage: [where shadows apply]
|
||||
|
||||
## 3. Layout Structure (Mobile View - [width]px)
|
||||
|
||||
**Top Navigation Bar ([height]px):**
|
||||
- Title: [position, size, weight, color]
|
||||
- Left/Right Icons: [elements]
|
||||
- Background: [color]
|
||||
- Border: [style and position]
|
||||
|
||||
**Content Sections:**
|
||||
[Describe each major section with:]
|
||||
- Section Name
|
||||
- Layout Pattern: [grid/flex/list]
|
||||
- Elements: [what it contains]
|
||||
- Styling: [colors, spacing, borders]
|
||||
|
||||
**Quick Access Area:**
|
||||
[If applicable]
|
||||
- Grid: [columns x rows]
|
||||
- Icon Style: [shape, size]
|
||||
- Text: [size, position]
|
||||
|
||||
**Data Display Cards:**
|
||||
[If applicable]
|
||||
- Metrics: [list key data points]
|
||||
- Number Display: [size, weight]
|
||||
- Label Style: [size, color]
|
||||
- Layout: [horizontal/vertical/grid]
|
||||
|
||||
**Feature List:**
|
||||
[If applicable]
|
||||
- Section Title: [size, color, alignment]
|
||||
- List Items: [icon + text layout]
|
||||
- Item Height: [px]
|
||||
- Interaction: [tap feedback]
|
||||
|
||||
**Bottom TabBar ([height]px, Sticky):**
|
||||
- Tab Count: [number]
|
||||
- Tabs: [list tab names]
|
||||
- Active State: [color, style]
|
||||
- Inactive State: [color, style]
|
||||
- Badges: [if applicable]
|
||||
- Position: [fixed/sticky]
|
||||
|
||||
## 4. Specific Page Content
|
||||
[Include actual content for the prototype:]
|
||||
|
||||
**Page Title:** [name]
|
||||
|
||||
**Section 1: [Name]**
|
||||
- Element 1: [description]
|
||||
- Element 2: [description]
|
||||
...
|
||||
|
||||
**Section 2: [Name]**
|
||||
- [Actual text and data to display]
|
||||
|
||||
**Bottom Navigation:**
|
||||
- [List all tabs with states]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Implementation Details
|
||||
Provide technical implementation guidelines.
|
||||
|
||||
**Template:**
|
||||
```
|
||||
# Implementation Details
|
||||
- Page Width: [max-width] and [alignment]
|
||||
- Layout System: [Flexbox/Grid/both]
|
||||
- Navigation: [fixed/sticky positions]
|
||||
- Spacing: [margins, padding, gaps]
|
||||
- Typography: [font family, weights, sizes]
|
||||
- Interactive States: [hover, active, focus effects]
|
||||
- Icons: [source and usage]
|
||||
- Borders: [colors and positions]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
# Implementation Details
|
||||
- Page width set to max-w-[375px] and centered to simulate phone screen
|
||||
- Use Flexbox and Grid layout systems
|
||||
- Top navigation bar: sticky top-0 z-10
|
||||
- Bottom TabBar: fixed bottom-0 w-full
|
||||
- Card spacing: 12px, page left/right margins: 16px
|
||||
- Typography: System default sans-serif, titles use font-semibold or font-bold
|
||||
- All clickable elements have tap feedback: active:opacity-90 or active:bg-gray-50
|
||||
- Icons: FontAwesome or placeholder div + background color
|
||||
- Dividers: border-gray-200 or border-gray-100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Tailwind Configuration
|
||||
If using Tailwind CSS, provide custom configuration.
|
||||
|
||||
**Template:**
|
||||
```
|
||||
# Tailwind Config (in <script> tag)
|
||||
|
||||
```javascript
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'brand-primary': '[hex]',
|
||||
'brand-secondary': '[hex]',
|
||||
// ... more colors
|
||||
},
|
||||
spacing: {
|
||||
// custom spacing if needed
|
||||
},
|
||||
fontSize: {
|
||||
// custom font sizes if needed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Content Structure & Hierarchy
|
||||
Visualize the page structure and content hierarchy.
|
||||
|
||||
**Template:**
|
||||
```
|
||||
# Example Content & Structure
|
||||
|
||||
[Page Name]
|
||||
├─ [Section 1 Name]
|
||||
│ ├─ [Element 1]
|
||||
│ ├─ [Element 2]
|
||||
│ └─ [Element 3]
|
||||
├─ [Section 2 Name]
|
||||
│ ├─ [Subsection A]
|
||||
│ │ ├─ [Item 1]
|
||||
│ │ └─ [Item 2]
|
||||
│ └─ [Subsection B]
|
||||
└─ [Section 3 Name]
|
||||
└─ [Elements]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
# Example Content & Structure
|
||||
|
||||
Work Dashboard
|
||||
├─ Quick Access (4-grid)
|
||||
│ ├─ Manage Enterprise
|
||||
│ ├─ Select Apps
|
||||
│ ├─ Find Services
|
||||
│ └─ Submit Requests
|
||||
├─ Feature Card
|
||||
│ └─ Customer Data Stats (14 customers, 1 new, ¥0.00)
|
||||
├─ Common Features List
|
||||
│ ├─ Customer Contact
|
||||
│ ├─ Customer Moments
|
||||
│ ├─ Customer Groups
|
||||
│ ├─ WeChat Service
|
||||
│ └─ Broadcast Assistant
|
||||
└─ Bottom TabBar
|
||||
├─ Messages
|
||||
├─ Email
|
||||
├─ Documents
|
||||
├─ Workspace (current)
|
||||
└─ Contacts (16)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. Special Requirements
|
||||
Highlight unique considerations or constraints.
|
||||
|
||||
**Template:**
|
||||
```
|
||||
# Special Requirements
|
||||
|
||||
- [Design System] Key Points:
|
||||
- [Characteristic 1]
|
||||
- [Characteristic 2]
|
||||
- [Characteristic 3]
|
||||
|
||||
- [Primary Color] Application Scenarios:
|
||||
- [Usage 1]
|
||||
- [Usage 2]
|
||||
- [Usage 3]
|
||||
|
||||
- Interaction Details:
|
||||
- [Interaction 1]: [behavior]
|
||||
- [Interaction 2]: [behavior]
|
||||
- [Interaction 3]: [behavior]
|
||||
|
||||
- Accessibility:
|
||||
- [Requirement 1]
|
||||
- [Requirement 2]
|
||||
|
||||
- Performance:
|
||||
- [Consideration 1]
|
||||
- [Consideration 2]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. Output Format
|
||||
Specify the exact deliverable format.
|
||||
|
||||
**Template:**
|
||||
```
|
||||
# Output Format
|
||||
|
||||
Please output complete [file type] code, ensuring:
|
||||
1. [Requirement 1]
|
||||
2. [Requirement 2]
|
||||
3. [Requirement 3]
|
||||
...
|
||||
|
||||
The output should be production-ready and viewable at [viewport size] on [device type].
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
# Output Format
|
||||
|
||||
Please output complete index.html code, ensuring:
|
||||
1. Perfect display on 375px width mobile screen
|
||||
2. All interactive elements have proper feedback
|
||||
3. Real image assets (no placeholders)
|
||||
4. Proper iOS status bar and safe area simulation
|
||||
5. Smooth scrolling and fixed navigation elements
|
||||
|
||||
The output should be immediately viewable in a browser and represent a pixel-perfect work dashboard homepage.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prompt Generation Workflow
|
||||
|
||||
When generating a prototype prompt, follow this workflow:
|
||||
|
||||
### Step 1: Gather Requirements
|
||||
Ask the user:
|
||||
- What type of application? (e.g., enterprise tool, e-commerce, social media)
|
||||
- Target platform? (iOS, Android, Web, WeChat Mini Program)
|
||||
- Design style preference? (WeChat Work, iOS Native, Material Design, custom)
|
||||
- Key features and pages needed?
|
||||
- Target users and use cases?
|
||||
- Any specific content or data to display?
|
||||
- Brand colors or visual preferences?
|
||||
|
||||
### Step 2: Select Design System
|
||||
Based on requirements, choose the appropriate design system from references/design-systems.md:
|
||||
- **WeChat Work**: Chinese enterprise apps, B2B tools
|
||||
- **iOS Native**: iOS apps, Apple ecosystem
|
||||
- **Material Design**: Android apps, Google ecosystem
|
||||
- **Ant Design Mobile**: Enterprise mobile apps with complex forms
|
||||
|
||||
### Step 3: Structure the Prompt
|
||||
Using the sections above:
|
||||
1. Define the role (UI/UX engineer with specific expertise)
|
||||
2. Describe the task (what to build, design style, key attributes)
|
||||
3. Specify tech stack (frameworks, device, assets, CDNs)
|
||||
4. Detail visual design (colors, components, layout, content)
|
||||
5. Provide implementation guidelines (technical details)
|
||||
6. Include Tailwind config (if using Tailwind)
|
||||
7. Show content hierarchy (visual tree structure)
|
||||
8. Add special requirements (design system specifics, interactions)
|
||||
9. Specify output format (deliverable expectations)
|
||||
|
||||
### Step 4: Populate with Specifics
|
||||
- Replace all template placeholders with actual values
|
||||
- Include real content (text, numbers, labels)
|
||||
- Specify exact colors (hex codes)
|
||||
- Define precise measurements (px, rem, etc.)
|
||||
- List all page elements and features
|
||||
- Add interaction states and behaviors
|
||||
|
||||
### Step 5: Review and Refine
|
||||
Ensure the prompt:
|
||||
- Is complete and self-contained
|
||||
- Has no ambiguous placeholders
|
||||
- Includes all necessary technical details
|
||||
- Specifies exact design requirements
|
||||
- Provides realistic content examples
|
||||
- Defines clear deliverables
|
||||
|
||||
---
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before finalizing a prototype prompt, verify:
|
||||
|
||||
**Completeness:**
|
||||
- [ ] Role clearly defined with relevant expertise
|
||||
- [ ] Task explicitly states what to build and design style
|
||||
- [ ] All tech stack components listed with versions/CDNs
|
||||
- [ ] Complete color palette with hex codes
|
||||
- [ ] All UI components specified with dimensions and styles
|
||||
- [ ] Page layout fully described with measurements
|
||||
- [ ] Actual content provided (not placeholders)
|
||||
- [ ] Implementation details cover technical requirements
|
||||
- [ ] Tailwind config included (if applicable)
|
||||
- [ ] Content hierarchy visualized
|
||||
- [ ] Special requirements and interactions documented
|
||||
- [ ] Output format clearly defined
|
||||
|
||||
**Clarity:**
|
||||
- [ ] No ambiguous terms or vague descriptions
|
||||
- [ ] Measurements specified (px, rem, %, etc.)
|
||||
- [ ] Colors defined with hex codes
|
||||
- [ ] Component states described (normal, hover, active, disabled)
|
||||
- [ ] Layout relationships clear (parent-child, spacing, alignment)
|
||||
|
||||
**Specificity:**
|
||||
- [ ] Design system explicitly named
|
||||
- [ ] Viewport dimensions provided
|
||||
- [ ] Typography scale defined (sizes, weights, line heights)
|
||||
- [ ] Interactive behaviors documented
|
||||
- [ ] Edge cases considered (long text, empty states, etc.)
|
||||
|
||||
**Realism:**
|
||||
- [ ] Real content examples (not Lorem Ipsum)
|
||||
- [ ] Authentic data points (numbers, names, dates)
|
||||
- [ ] Practical feature set (not overengineered)
|
||||
- [ ] Appropriate complexity for use case
|
||||
|
||||
**Technical Accuracy:**
|
||||
- [ ] Valid Tailwind class names
|
||||
- [ ] Correct CDN links
|
||||
- [ ] Proper HTML structure implied
|
||||
- [ ] Feasible layout techniques (Flexbox/Grid)
|
||||
- [ ] Accessible markup considerations
|
||||
|
||||
---
|
||||
|
||||
## Example Variations
|
||||
|
||||
### Minimal Prompt (for simple landing page)
|
||||
```
|
||||
# Role
|
||||
You are a frontend developer specializing in clean, modern web design.
|
||||
|
||||
# Task
|
||||
Create a single-page landing page (HTML + Tailwind CSS) for a SaaS product.
|
||||
Style: Modern, minimal, professional with soft colors.
|
||||
|
||||
# Tech Stack
|
||||
- Single index.html file
|
||||
- Tailwind CSS (CDN)
|
||||
- Desktop-first responsive (max 1200px)
|
||||
- Use Unsplash for hero image
|
||||
|
||||
# Visual Design
|
||||
- Colors: Primary #6366F1 (indigo), Background #F9FAFB (gray-50), Text #111827 (gray-900)
|
||||
- Sections: Hero (full viewport height) + Features (3 columns) + CTA
|
||||
- Typography: System sans-serif, Hero 48px bold, Body 18px regular
|
||||
|
||||
# Output Format
|
||||
Single HTML file, production-ready, mobile-responsive.
|
||||
```
|
||||
|
||||
### Complex Prompt (for multi-page app)
|
||||
[Follow full template structure with all sections, detailed component specs, multiple pages, complex interactions]
|
||||
|
||||
---
|
||||
|
||||
## Tips for Effective Prompts
|
||||
|
||||
1. **Be Specific**: "Blue button" → "Primary button: #3478F6 background, white text, 44px height, 4px rounded corners"
|
||||
|
||||
2. **Provide Context**: Don't just list features, explain their purpose and how they relate to user workflows
|
||||
|
||||
3. **Use Real Content**: "Insurance agent dashboard" with "14 customers, ¥1,234.56 revenue" is better than "Dashboard with metrics"
|
||||
|
||||
4. **Reference Standards**: "Follow WeChat Work design guidelines" gives Claude a comprehensive style framework
|
||||
|
||||
5. **Include Edge Cases**: Mention empty states, loading states, error states, long text handling
|
||||
|
||||
6. **Specify Interactions**: Don't assume—explicitly state tap feedback, hover effects, animation timing
|
||||
|
||||
7. **Think Mobile-First**: For mobile apps, always specify touch target sizes (minimum 44px), safe areas, gesture support
|
||||
|
||||
8. **Consider Accessibility**: Mention color contrast, touch target sizes, semantic HTML, ARIA labels
|
||||
|
||||
9. **Balance Detail and Brevity**: Be thorough but organized—use sections and hierarchy to keep prompts scannable
|
||||
|
||||
10. **Test Your Mental Model**: Can someone else understand exactly what to build from this prompt? If not, add clarity.
|
||||
116
skills/skill-rules.json
Normal file
116
skills/skill-rules.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"skills": {
|
||||
"codex": {
|
||||
"type": "execution",
|
||||
"enforcement": "suggest",
|
||||
"priority": "high",
|
||||
"promptTriggers": {
|
||||
"keywords": [
|
||||
"refactor",
|
||||
"implement",
|
||||
"code change",
|
||||
"bug fix",
|
||||
"生成代码",
|
||||
"重构",
|
||||
"修复"
|
||||
],
|
||||
"intentPatterns": [
|
||||
"(refactor|rewrite|optimi[sz]e)\\b",
|
||||
"(implement|build|write).*(feature|function|module|code)",
|
||||
"(fix|debug).*(bug|error|issue)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"codeagent": {
|
||||
"type": "execution",
|
||||
"enforcement": "suggest",
|
||||
"priority": "high",
|
||||
"promptTriggers": {
|
||||
"keywords": [
|
||||
"codeagent",
|
||||
"multi-backend",
|
||||
"backend selection",
|
||||
"parallel task",
|
||||
"多后端",
|
||||
"并行任务",
|
||||
"gemini",
|
||||
"claude backend"
|
||||
],
|
||||
"intentPatterns": [
|
||||
"\\bcodeagent\\b",
|
||||
"backend\\s+(codex|claude|gemini)",
|
||||
"parallel.*task",
|
||||
"multi.*backend"
|
||||
]
|
||||
}
|
||||
},
|
||||
"gh-workflow": {
|
||||
"type": "domain",
|
||||
"enforcement": "suggest",
|
||||
"priority": "high",
|
||||
"promptTriggers": {
|
||||
"keywords": [
|
||||
"issue",
|
||||
"pr",
|
||||
"pull request",
|
||||
"github",
|
||||
"gh workflow",
|
||||
"merge"
|
||||
],
|
||||
"intentPatterns": [
|
||||
"(create|open|update|close|review).*(pr|pull request|issue)",
|
||||
"\\bgithub\\b|\\bgh\\b"
|
||||
]
|
||||
}
|
||||
},
|
||||
"product-requirements": {
|
||||
"type": "domain",
|
||||
"enforcement": "suggest",
|
||||
"priority": "high",
|
||||
"promptTriggers": {
|
||||
"keywords": [
|
||||
"requirements",
|
||||
"prd",
|
||||
"product requirements",
|
||||
"feature specification",
|
||||
"product owner",
|
||||
"需求",
|
||||
"产品需求",
|
||||
"需求文档",
|
||||
"功能规格"
|
||||
],
|
||||
"intentPatterns": [
|
||||
"(product|feature).*requirement",
|
||||
"\\bPRD\\b",
|
||||
"requirements?.*document",
|
||||
"(gather|collect|define|write).*requirement"
|
||||
]
|
||||
}
|
||||
},
|
||||
"prototype-prompt-generator": {
|
||||
"type": "domain",
|
||||
"enforcement": "suggest",
|
||||
"priority": "medium",
|
||||
"promptTriggers": {
|
||||
"keywords": [
|
||||
"prototype",
|
||||
"ui design",
|
||||
"ux design",
|
||||
"mobile app design",
|
||||
"web app design",
|
||||
"原型",
|
||||
"界面设计",
|
||||
"UI设计",
|
||||
"UX设计",
|
||||
"移动应用设计"
|
||||
],
|
||||
"intentPatterns": [
|
||||
"(create|generate|design).*(prototype|UI|UX|interface)",
|
||||
"(mobile|web).*app.*(design|prototype)",
|
||||
"prototype.*prompt",
|
||||
"design.*system"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user