mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-05 02:30:26 +08:00
Compare commits
238 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7240e08900 | ||
|
|
e122d8ff25 | ||
|
|
6985a30a6a | ||
|
|
dd4c12b8e2 | ||
|
|
a88315d92d | ||
|
|
d1f13b3379 | ||
|
|
5d362852ab | ||
|
|
238c7b9a13 | ||
|
|
0986fa82ee | ||
|
|
a989ce343c | ||
|
|
abe0839249 | ||
|
|
d75c973f32 | ||
|
|
e7f329940b | ||
|
|
0fc5eaaa2d | ||
|
|
420eb857ff | ||
|
|
661656c587 | ||
|
|
ed4b088631 | ||
|
|
55a574280a | ||
|
|
8f05626075 | ||
|
|
4395c5785d | ||
|
|
b0d7a09ff2 | ||
|
|
f7aeaa5c7e | ||
|
|
c8f75faf84 | ||
|
|
b8b06257ff | ||
|
|
369a3319f9 | ||
|
|
75f08ab81f | ||
|
|
23282ef460 | ||
|
|
c7cb28a1da | ||
|
|
0a4982e96d | ||
|
|
17e52d78d2 | ||
|
|
55246ce9c4 | ||
|
|
890fec81bf | ||
|
|
81f298c2ea | ||
|
|
8ea6d10be5 | ||
|
|
bdf62d0f1c | ||
|
|
40e2d00d35 | ||
|
|
13465b12e5 | ||
|
|
cf93a0ada9 | ||
|
|
b81953a1d7 | ||
|
|
1d2f28101a | ||
|
|
81e95777a8 | ||
|
|
993249acb1 | ||
|
|
0d28e70026 | ||
|
|
7560ce1976 | ||
|
|
683d18e6bb | ||
|
|
a7147f692c | ||
|
|
b71d74f01f | ||
|
|
af1c860f54 | ||
|
|
70b1896011 | ||
|
|
3fd3c67749 | ||
|
|
156a072a0b | ||
|
|
0ceb819419 | ||
|
|
4d69c8aef1 | ||
|
|
eec844d850 | ||
|
|
1f42bcc1c6 | ||
|
|
0f359b048f | ||
|
|
4e2df6a80e | ||
|
|
a30f434b5d | ||
|
|
41f4e21268 | ||
|
|
a67aa00c9a | ||
|
|
d61a0f9ffd | ||
|
|
fe5508228f | ||
|
|
50093036c3 | ||
|
|
0cae0ede08 | ||
|
|
4613b57240 | ||
|
|
7535a7b101 | ||
|
|
f6bb97eba9 | ||
|
|
78a411462b | ||
|
|
9471a981e3 | ||
|
|
3d27d44676 | ||
|
|
6a66c9741f | ||
|
|
a09c103cfb | ||
|
|
1dec763e26 | ||
|
|
f57ea2df59 | ||
|
|
d215c33549 | ||
|
|
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 | ||
|
|
a36b37c66d | ||
|
|
b4a80f833a | ||
|
|
cc1d22167a | ||
|
|
c080eea98c | ||
|
|
95b43c68fe | ||
|
|
6b06403014 | ||
|
|
4d3789d0dc | ||
|
|
4110ee4600 | ||
|
|
9d16cb4406 | ||
|
|
daa50177f3 | ||
|
|
9c2c91bb1a | ||
|
|
34f1557f83 | ||
|
|
41d776c09e | ||
|
|
9dea5d37ef | ||
|
|
656bdd27c5 | ||
|
|
5b1190f8a3 | ||
|
|
32f2e4c2cb | ||
|
|
394013fb2e | ||
|
|
c344a2f544 | ||
|
|
5532652383 | ||
|
|
44b96c0498 | ||
|
|
9304573cc6 | ||
|
|
8a84a05fa0 | ||
|
|
894952decd |
@@ -1,158 +1,54 @@
|
||||
{
|
||||
"name": "claude-code-dev-workflows",
|
||||
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||
"name": "myclaude",
|
||||
"version": "5.6.1",
|
||||
"description": "Professional multi-agent development workflows with OmO orchestration, Requirements-Driven and BMAD methodologies",
|
||||
"owner": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"email": "contact@example.com",
|
||||
"url": "https://github.com/yourusername/myclaude"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Professional multi-agent development workflows with Requirements-Driven and BMAD methodologies, featuring 16+ specialized agents and 12+ commands",
|
||||
"version": "1.0.0"
|
||||
"name": "cexll",
|
||||
"email": "evanxian9@gmail.com"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "requirements-driven-development",
|
||||
"source": "./",
|
||||
"description": "Streamlined requirements-driven development workflow with 90% quality gates for practical feature implementation",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/yourusername/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/yourusername/myclaude",
|
||||
"repository": "https://github.com/yourusername/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"requirements",
|
||||
"workflow",
|
||||
"automation",
|
||||
"quality-gates",
|
||||
"feature-development",
|
||||
"agile",
|
||||
"specifications"
|
||||
],
|
||||
"category": "workflows",
|
||||
"strict": false,
|
||||
"commands": [
|
||||
"./commands/requirements-pilot.md"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/requirements-generate.md",
|
||||
"./agents/requirements-code.md",
|
||||
"./agents/requirements-testing.md",
|
||||
"./agents/requirements-review.md"
|
||||
]
|
||||
"name": "omo",
|
||||
"description": "Multi-agent orchestration for code analysis, bug investigation, fix planning, and implementation with intelligent routing to specialized agents",
|
||||
"version": "5.6.1",
|
||||
"source": "./skills/omo",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "bmad-agile-workflow",
|
||||
"source": "./",
|
||||
"name": "dev",
|
||||
"description": "Lightweight development workflow with requirements clarification, parallel codex execution, and mandatory 90% test coverage",
|
||||
"version": "5.6.1",
|
||||
"source": "./dev-workflow",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "requirements",
|
||||
"description": "Requirements-driven development workflow with quality gates for practical feature implementation",
|
||||
"version": "5.6.1",
|
||||
"source": "./requirements-driven-workflow",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "bmad",
|
||||
"description": "Full BMAD agile workflow with role-based agents (PO, Architect, SM, Dev, QA) and interactive approval gates",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/yourusername/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/yourusername/myclaude",
|
||||
"repository": "https://github.com/yourusername/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"bmad",
|
||||
"agile",
|
||||
"scrum",
|
||||
"product-owner",
|
||||
"architect",
|
||||
"developer",
|
||||
"qa",
|
||||
"workflow-orchestration"
|
||||
],
|
||||
"category": "workflows",
|
||||
"strict": false,
|
||||
"commands": [
|
||||
"./commands/bmad-pilot.md"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/bmad-po.md",
|
||||
"./agents/bmad-architect.md",
|
||||
"./agents/bmad-sm.md",
|
||||
"./agents/bmad-dev.md",
|
||||
"./agents/bmad-qa.md",
|
||||
"./agents/bmad-orchestrator.md",
|
||||
"./agents/bmad-review.md"
|
||||
],
|
||||
"output-styles": [
|
||||
"./output-styles/bmad.md"
|
||||
]
|
||||
"version": "5.6.1",
|
||||
"source": "./bmad-agile-workflow",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "development-essentials",
|
||||
"source": "./",
|
||||
"name": "dev-kit",
|
||||
"description": "Essential development commands for coding, debugging, testing, optimization, and documentation",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/yourusername/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/yourusername/myclaude",
|
||||
"repository": "https://github.com/yourusername/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"code",
|
||||
"debug",
|
||||
"test",
|
||||
"optimize",
|
||||
"review",
|
||||
"bugfix",
|
||||
"refactor",
|
||||
"documentation"
|
||||
],
|
||||
"category": "essentials",
|
||||
"strict": false,
|
||||
"commands": [
|
||||
"./commands/code.md",
|
||||
"./commands/debug.md",
|
||||
"./commands/test.md",
|
||||
"./commands/optimize.md",
|
||||
"./commands/review.md",
|
||||
"./commands/bugfix.md",
|
||||
"./commands/refactor.md",
|
||||
"./commands/docs.md",
|
||||
"./commands/ask.md",
|
||||
"./commands/think.md"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/code.md",
|
||||
"./agents/bugfix.md",
|
||||
"./agents/bugfix-verify.md",
|
||||
"./agents/code-optimize.md",
|
||||
"./agents/debug.md",
|
||||
"./agents/develop.md"
|
||||
]
|
||||
"version": "5.6.1",
|
||||
"source": "./development-essentials",
|
||||
"category": "productivity"
|
||||
},
|
||||
{
|
||||
"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/yourusername/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/yourusername/myclaude",
|
||||
"repository": "https://github.com/yourusername/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"gpt5",
|
||||
"ai",
|
||||
"analysis",
|
||||
"problem-solving",
|
||||
"deep-research"
|
||||
],
|
||||
"category": "advanced",
|
||||
"strict": false,
|
||||
"commands": [],
|
||||
"agents": [
|
||||
"./agents/gpt5.md"
|
||||
]
|
||||
"name": "sparv",
|
||||
"description": "Minimal SPARV workflow (Specify→Plan→Act→Review→Vault) with 10-point spec gate, unified journal, 2-action saves, 3-failure protocol, and EHRB risk detection",
|
||||
"version": "1.1.0",
|
||||
"source": "./skills/sparv",
|
||||
"category": "development"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
22
.gitattributes
vendored
Normal file
22
.gitattributes
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Ensure shell scripts always use LF line endings on all platforms
|
||||
*.sh text eol=lf
|
||||
|
||||
# Ensure Python files use LF line endings
|
||||
*.py text eol=lf
|
||||
|
||||
# Auto-detect text files and normalize line endings to LF
|
||||
* text=auto eol=lf
|
||||
|
||||
# Explicitly declare files that should always be treated as binary
|
||||
*.exe binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.mov binary
|
||||
*.mp4 binary
|
||||
*.mp3 binary
|
||||
*.zip binary
|
||||
*.gz binary
|
||||
*.tar binary
|
||||
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
|
||||
113
.github/workflows/release.yml
vendored
Normal file
113
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
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: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: release/*
|
||||
generate_release_notes: true
|
||||
draft: false
|
||||
prerelease: false
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,3 +1,9 @@
|
||||
CLAUDE.md
|
||||
|
||||
.claude/
|
||||
.claude-trace
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
.venv
|
||||
.pytest_cache
|
||||
__pycache__
|
||||
.coverage
|
||||
coverage.out
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
# BMAD Pilot 使用指南
|
||||
|
||||
本指南介绍如何使用 BMAD Pilot 工作流,编排一组协作 AI 角色(PO/Architect/SM/Dev/QA)在仓库上下文中完成:01 产品需求文档、02 系统设计规范、03 冲刺计划,并自动进入开发与测试,整个过程包含多次用户确认门与质量评分。
|
||||
|
||||
参考阅读:BMAD-README.md(BMAD 方法概览)、BMAD-INTEGRATION-GUIDE.md(进阶集成)。
|
||||
|
||||
---
|
||||
|
||||
## 命令总览
|
||||
|
||||
- 命令:`/bmad-pilot <PROJECT_DESCRIPTION> [OPTIONS]`
|
||||
- 作用:在仓库上下文中,按阶段编排 `bmad-po → bmad-architect → bmad-sm → bmad-dev → bmad-qa`。
|
||||
- Orchestrator:由工作流统一编排(使用 bmad-orchestrator 进行仓库扫描)。
|
||||
|
||||
### Options
|
||||
- `--skip-tests`:跳过 QA 阶段
|
||||
- `--direct-dev`:跳过 SM 冲刺计划,架构后直接进入开发
|
||||
- `--skip-scan`:跳过初始仓库扫描(不推荐)
|
||||
|
||||
### 输出目录
|
||||
- 所有产出归档在:`./.claude/specs/{feature_name}/`
|
||||
- `00-repo-scan.md` — 仓库扫描摘要(自动生成)
|
||||
- `01-product-requirements.md` — 产品需求文档(确认后保存)
|
||||
- `02-system-architecture.md` — 系统设计规范(确认后保存)
|
||||
- `03-sprint-plan.md` — 冲刺计划(确认后保存;`--direct-dev` 时跳过)
|
||||
|
||||
`{feature_name}` 由 `<PROJECT_DESCRIPTION>` 生成(kebab-case:小写,空格/标点转 `-`,连续合并,首尾去除)。
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
1) 执行 Pilot:
|
||||
```
|
||||
/bmad-pilot 为现有项目新增看板模块,支持多用户权限与移动端适配
|
||||
```
|
||||
2) 与 PO 交互澄清,直至 PRD ≥ 90 分 → 确认保存。
|
||||
3) 与 Architect 讨论技术决策,直至架构 ≥ 90 分 → 确认保存。
|
||||
4) 审阅并确认 SM 的冲刺计划(或使用 `--direct-dev` 跳过该阶段)。
|
||||
5) Dev 基于文档实现;QA 基于文档与实现测试(除非 `--skip-tests`)。
|
||||
6) 查看产出目录:`./.claude/specs/{feature_name}/`。
|
||||
|
||||
---
|
||||
|
||||
## 工作流阶段
|
||||
|
||||
- Phase 0:仓库扫描(自动,除非 `--skip-scan`)
|
||||
- Agent:`bmad-orchestrator`
|
||||
- 结果:扫描摘要返回并写入 `00-repo-scan.md`
|
||||
- 内容:项目类型、技术栈、代码组织、惯例、集成点、约束与注意事项
|
||||
|
||||
- Phase 1:产品需求(交互)
|
||||
- Agent:`bmad-po`
|
||||
- 循环:澄清问题 → 更新 PRD → 评分(目标 ≥ 90)
|
||||
- 确认门:PRD ≥ 90 分后,需要用户明确确认再继续
|
||||
- 保存:`01-product-requirements.md`
|
||||
|
||||
- Phase 2:系统架构(交互)
|
||||
- Agent:`bmad-architect`
|
||||
- 循环:技术选型与设计澄清 → 更新架构 → 评分(目标 ≥ 90)
|
||||
- 确认门:架构 ≥ 90 分后,需要用户明确确认再继续
|
||||
- 保存:`02-system-architecture.md`
|
||||
|
||||
- Phase 3:冲刺计划(交互,除非 `--direct-dev`)
|
||||
- Agent:`bmad-sm`
|
||||
- 循环:计划要点与问题澄清 → 更新计划 → 确认保存
|
||||
- 保存:`03-sprint-plan.md`
|
||||
|
||||
- Phase 4:开发实现(自动)
|
||||
- Agent:`bmad-dev`
|
||||
- 输入:PRD、架构、冲刺计划、`00-repo-scan.md`
|
||||
|
||||
- Phase 5:质量保障(自动,除非 `--skip-tests`)
|
||||
- Agent:`bmad-qa`
|
||||
- 输入:PRD、架构、冲刺计划、实现、`00-repo-scan.md`
|
||||
|
||||
---
|
||||
|
||||
## 交互与质量门
|
||||
|
||||
- 质控阈值:PRD 与架构质量评分需达到 ≥ 90 分。
|
||||
- 强制确认门:每个关键阶段完成后,Orchestrator 会停下等待你的“继续/确认”。
|
||||
- 迭代澄清:PO/Architect/SM 会提出 2-5 个精准问题,Orchestrator 转述并汇总你的回答以供下一轮完善。
|
||||
|
||||
---
|
||||
|
||||
## 仓库上下文
|
||||
|
||||
- 首次扫描:由工作流触发的 orchestrator 扫描(`bmad-orchestrator`)自动分析当前仓库(`--skip-scan` 可跳过)。
|
||||
- 缓存路径:`./.claude/specs/{feature_name}/00-repo-scan.md`(供所有后续 Agent 引用)。
|
||||
- 作用:提供技术栈识别、约定、测试模式、集成点,避免上下文丢失并保持一致性。
|
||||
|
||||
---
|
||||
|
||||
## 角色职责
|
||||
|
||||
- `bmad-po`:需求澄清与 PRD 产出,评分与问题驱动迭代。
|
||||
- `bmad-architect`:技术架构与关键决策,评分与问题驱动迭代。
|
||||
- `bmad-sm`:冲刺计划、任务拆分、依赖/风险/节奏规划。
|
||||
- `bmad-dev`:按文档实现、测试、日志/安全/性能与同构风格。
|
||||
- `bmad-qa`:基于需求与实现的全维度测试(单测/集成/E2E/性能/安全)。
|
||||
|
||||
---
|
||||
|
||||
## 示例
|
||||
|
||||
- 基础运行:
|
||||
```
|
||||
/bmad-pilot 在线商城结算流程升级,支持优惠券与发票
|
||||
```
|
||||
|
||||
- 跳过测试:
|
||||
```
|
||||
/bmad-pilot H5 活动页生成器 --skip-tests
|
||||
```
|
||||
|
||||
- 直接从架构进入开发(跳过 SM):
|
||||
```
|
||||
/bmad-pilot 小程序客服模块重构 --direct-dev
|
||||
```
|
||||
|
||||
- 跳过扫描(不推荐):
|
||||
```
|
||||
/bmad-pilot 部署流水线可视化 --skip-scan
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
.claude/
|
||||
specs/
|
||||
{feature_name}/
|
||||
00-repo-scan.md
|
||||
01-product-requirements.md
|
||||
02-system-architecture.md
|
||||
03-sprint-plan.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tips & 常见问题
|
||||
|
||||
- 分数上不去:优先补齐评分分项的缺口(业务指标、关键流程、性能/安全约束等)。
|
||||
- 上下文不一致:检查并引用 `00-repo-scan.md` 的关键约定与模式,保证 PRD/架构/计划一致。
|
||||
- 依赖/网络受限:Dev/QA 的实际执行受环境影响;请在项目内准备依赖与测试环境,或先提交伪实现/测试策略。
|
||||
- 文档路径:确保在项目根目录执行,Pilot 会将文件写入 `./.claude/specs/{feature_name}/`。
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
- 小步快跑:每轮补充最关键信息,快速达成 ≥ 90 分文档。
|
||||
- 统一术语:在 PRD 固定术语词表;架构与代码沿用同名。
|
||||
- 用例先行:PRD 的验收标准应转化为 QA 的关键测试用例。
|
||||
- 复用模式:尽量沿用扫描识别的现有代码/测试模式,减少偏差。
|
||||
|
||||
---
|
||||
|
||||
## 版本记录
|
||||
|
||||
- 2025-08-11:新增仓库扫描摘要缓存 `00-repo-scan.md`,统一路径与跨阶段引用;明确确认门与目录预创建说明。
|
||||
339
BMAD-README.md
339
BMAD-README.md
@@ -1,339 +0,0 @@
|
||||
# BMAD方法论 Claude Code 使用指南
|
||||
|
||||
[](https://github.com/bmadcode/BMAD-METHOD)
|
||||
[](https://claude.ai/code)
|
||||
|
||||
> 从产品理念到代码实现的完整AI驱动敏捷开发工作流
|
||||
|
||||
## 🎯 什么是BMAD方法论?
|
||||
|
||||
BMAD (Business, Market, Architecture, Development) 是一个AI驱动的敏捷开发方法论,通过专业化代理团队实现从商业需求到技术实现的完整工作流程。
|
||||
|
||||
### 核心理念
|
||||
- **智能体规划**: 专门代理协作创建详细、一致的PRD和架构文档
|
||||
- **上下文工程开发**: 将详细计划转换为超详细的开发故事
|
||||
- **角色专业化**: 每个代理专注特定领域,避免角色切换导致的质量下降
|
||||
|
||||
## 🏗️ BMAD代理体系
|
||||
|
||||
### 代理角色说明
|
||||
- **PO (Product Owner)** - 产品负责人Sarah:需求分析、用户故事、验收标准
|
||||
- **Analyst** - 业务分析师Mary:市场研究、竞争分析、商业案例
|
||||
- **Architect** - 系统架构师Winston:技术架构、系统设计、技术选择
|
||||
- **SM (Scrum Master)** - 敏捷教练:任务分解、冲刺规划、流程协调
|
||||
- **Dev (Developer)** - 开发工程师:代码实现、技术文档
|
||||
- **QA (Quality Assurance)** - 质量保证:测试策略、质量验证
|
||||
- **UX Expert** - 用户体验专家:交互设计、可用性测试
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 安装配置
|
||||
BMAD方法论已集成到您的Claude Code系统中,无需额外安装。
|
||||
|
||||
### 基本使用方法
|
||||
|
||||
#### 1. 完整BMAD工作流
|
||||
```bash
|
||||
# 一键执行完整开发流程
|
||||
/bmad-pilot "实现企业级用户管理系统,支持RBAC权限控制和LDAP集成"
|
||||
|
||||
# 执行流程:PO → Architect → SM → Dev → QA
|
||||
```
|
||||
|
||||
#### 2. 常用选项
|
||||
```bash
|
||||
# 跳过测试(PO → Architect → SM → Dev)
|
||||
/bmad-pilot "实现支付网关API" --skip-tests
|
||||
|
||||
# 直接从架构进入开发(跳过 SM 规划)
|
||||
/bmad-pilot "设计微服务电商平台" --direct-dev
|
||||
|
||||
# 跳过仓库扫描(不推荐)
|
||||
/bmad-pilot "用户界面优化" --skip-scan
|
||||
```
|
||||
|
||||
#### 3. 直接开发与部分流程
|
||||
```bash
|
||||
# 技术焦点(架构后直接进入开发与测试)
|
||||
/bmad-pilot "API网关实现" --direct-dev
|
||||
|
||||
# 完整设计流程(需求→架构→规划→开发→测试)
|
||||
/bmad-pilot "系统重构规划"
|
||||
|
||||
# 仅业务相关分析 → 请使用下方“独立代理使用”中的 /bmad-po 与 /bmad-analyst
|
||||
```
|
||||
|
||||
#### 4. 独立代理使用
|
||||
```bash
|
||||
# 产品需求分析
|
||||
/bmad-po "企业CRM系统功能需求定义"
|
||||
|
||||
# 市场调研分析
|
||||
/bmad-analyst "SaaS市场竞争格局和机会分析"
|
||||
|
||||
# 系统架构设计
|
||||
/bmad-architect "高并发分布式系统架构设计"
|
||||
|
||||
# 主协调器(可转换为任意代理)
|
||||
/bmad-orchestrator "协调多代理完成复杂项目"
|
||||
```
|
||||
|
||||
## 📋 详细命令说明
|
||||
|
||||
### `/bmad-pilot` - 完整工作流执行
|
||||
**用法**: `/bmad-pilot <项目描述> [选项]`
|
||||
|
||||
**选项**:
|
||||
- `--skip-tests`: 跳过 QA 阶段
|
||||
- `--direct-dev`: 跳过 SM 冲刺计划,架构后直接进入开发
|
||||
- `--skip-scan`: 跳过初始仓库扫描(不推荐)
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
/bmad-pilot "构建在线教育平台,支持直播、录播、作业系统"
|
||||
/bmad-pilot "API网关设计" --direct-dev
|
||||
/bmad-pilot "支付模块" --skip-tests
|
||||
```
|
||||
|
||||
### `/bmad-po` - 产品负责人
|
||||
**角色**: Sarah - 技术产品负责人 & 流程管家
|
||||
**专长**: 需求分析、用户故事、验收标准、冲刺规划
|
||||
|
||||
**用法**: `/bmad-po <需求描述>`
|
||||
|
||||
**工作流程**:
|
||||
1. 需求分解和功能点识别
|
||||
2. 用户故事创建(As a... I want... So that...)
|
||||
3. 验收标准定义和优先级排序
|
||||
4. 利益相关者验证和签署
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
/bmad-po "设计企业级权限管理系统,支持多租户和细粒度权限控制"
|
||||
/bmad-po "移动端电商APP功能需求分析"
|
||||
```
|
||||
|
||||
### `/bmad-analyst` - 业务分析师
|
||||
**角色**: Mary - 洞察分析师 & 战略合作伙伴
|
||||
**专长**: 市场研究、竞争分析、商业案例开发、利益相关者分析
|
||||
|
||||
**用法**: `/bmad-analyst <分析主题>`
|
||||
|
||||
**工作流程**:
|
||||
1. 市场格局和竞争对手分析
|
||||
2. 商业案例开发和ROI分析
|
||||
3. 利益相关者分析和需求收集
|
||||
4. 项目简报和战略建议
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
/bmad-analyst "企业级认证市场分析,JWT vs OAuth2.0 vs SAML"
|
||||
/bmad-analyst "云原生架构迁移的商业价值和风险评估"
|
||||
```
|
||||
|
||||
### `/bmad-architect` - 系统架构师
|
||||
**角色**: Winston - 全栈系统架构师 & 技术领导者
|
||||
**专长**: 系统设计、技术选择、API设计、基础架构规划
|
||||
|
||||
**用法**: `/bmad-architect <系统设计需求>`
|
||||
|
||||
**工作流程**:
|
||||
1. 系统需求和约束分析
|
||||
2. 技术栈和架构模式选择
|
||||
3. 组件设计和系统架构图
|
||||
4. 实施策略和开发指导
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
/bmad-architect "微服务架构设计,支持事件驱动和最终一致性"
|
||||
/bmad-architect "高可用API网关架构,支持限流、熔断、监控"
|
||||
```
|
||||
|
||||
### `/bmad-orchestrator` - 主协调器
|
||||
**角色**: BMAD主协调器
|
||||
**专长**: 工作流协调、代理转换、多代理任务管理
|
||||
|
||||
**用法**: `/bmad-orchestrator [命令] [参数]`
|
||||
|
||||
**功能**:
|
||||
- 动态转换为任意专门代理
|
||||
- 协调复杂多代理工作流
|
||||
- 管理代理间的上下文传递
|
||||
- 提供工作流指导和建议
|
||||
|
||||
## 🔄 与现有系统集成
|
||||
|
||||
### 现有系统 vs BMAD方法论
|
||||
|
||||
| 特性 | Requirements-Pilot | BMAD方法论 |
|
||||
|------|-------------------|-----------|
|
||||
| **执行时间** | 30分钟 | 1-2小时 |
|
||||
| **适用场景** | 快速功能开发 | 企业级项目 |
|
||||
| **覆盖范围** | 技术实现 | 商业+技术全流程 |
|
||||
| **质量门控** | 90%技术质量 | 多维度质量验证 |
|
||||
| **代理数量** | 4个技术代理 | 7个全角色代理 |
|
||||
|
||||
### 使用场景建议
|
||||
|
||||
#### 🚅 快速开发(推荐现有系统)
|
||||
```bash
|
||||
# 简单功能快速实现
|
||||
/requirements-pilot "添加用户登录功能"
|
||||
/requirements-pilot "实现数据导出API"
|
||||
```
|
||||
|
||||
#### 🏢 企业级项目(推荐BMAD)
|
||||
```bash
|
||||
# 复杂系统完整流程
|
||||
/bmad-pilot "构建企业级ERP系统,集成财务、人事、项目管理模块"
|
||||
/bmad-pilot "设计多租户SaaS平台,支持自定义配置和第三方集成"
|
||||
```
|
||||
|
||||
#### 🔄 混合模式(规划+实现)
|
||||
```bash
|
||||
# 先用BMAD做规划(在 PRD/架构确认门停留)
|
||||
/bmad-pilot "电商平台架构设计"
|
||||
|
||||
# 再用现有系统快速实现
|
||||
/requirements-pilot "基于架构规格实现用户服务模块"
|
||||
/requirements-pilot "基于架构规格实现订单服务模块"
|
||||
```
|
||||
|
||||
## 🎯 典型工作流示例
|
||||
|
||||
### 示例1: 企业级认证系统
|
||||
```bash
|
||||
# 完整BMAD流程
|
||||
/bmad-pilot "企业级JWT认证系统,支持RBAC权限控制、LDAP集成、审计日志、高可用部署"
|
||||
|
||||
# 预期输出:
|
||||
# 1. PO: 详细用户故事和验收标准
|
||||
# 2. Architect: 完整系统架构和技术选择
|
||||
# 3. SM: 开发任务分解和冲刺计划
|
||||
# 4. Dev: 生产就绪代码实现
|
||||
# 5. QA: 测试策略与用例并执行(可选)
|
||||
```
|
||||
|
||||
### 示例2: API网关开发
|
||||
```bash
|
||||
# 技术焦点流程(跳过SM,架构后直接进入开发)
|
||||
/bmad-pilot "高性能API网关,支持限流、熔断、监控、服务发现" --direct-dev
|
||||
|
||||
# 执行流程:
|
||||
# 1. Architect: 系统架构设计
|
||||
# 2. Dev: 代码实现
|
||||
# 3. QA: 性能测试和质量验证
|
||||
```
|
||||
|
||||
### 示例3: 产品市场分析
|
||||
```bash
|
||||
# 业务分析流程(使用独立代理)
|
||||
/bmad-po "云原生数据库市场机会分析的产品需求假设与范围界定"
|
||||
/bmad-analyst "云原生数据库市场机会分析"
|
||||
|
||||
# 执行流程:
|
||||
# 1. PO: 产品需求定义
|
||||
# 2. Analyst: 市场研究和竞争分析
|
||||
```
|
||||
|
||||
## 📊 质量保证体系
|
||||
|
||||
### BMAD质量标准
|
||||
- **需求完整性**: 90+ 分需求清晰度评分
|
||||
- **商业对齐**: 明确的价值主张和市场定位
|
||||
- **架构完善**: 全面的系统设计和技术选择
|
||||
- **实现就绪**: 可执行的开发规格和质量标准
|
||||
|
||||
### 集成现有质量门控
|
||||
- 保持90%技术质量阈值
|
||||
- 增加商业价值验证维度
|
||||
- 多代理交叉验证机制
|
||||
- 自动化质量反馈循环
|
||||
|
||||
## 🔧 高级用法和最佳实践
|
||||
|
||||
### 1. 渐进式复杂度管理
|
||||
```bash
|
||||
# MVP阶段
|
||||
/bmad-workflow "用户管理系统MVP版本" --phase=development
|
||||
|
||||
# 功能增强阶段
|
||||
/bmad-analyst "用户反馈分析和功能增强建议"
|
||||
/requirements-pilot "基于反馈实现增强功能"
|
||||
|
||||
# 企业级增强
|
||||
/bmad-workflow "企业级安全增强和合规支持" --agents=architect,dev,qa
|
||||
```
|
||||
|
||||
### 2. 跨项目知识管理
|
||||
```bash
|
||||
# 项目文档化
|
||||
/bmad-orchestrator "将当前项目架构文档化,便于后续项目参考"
|
||||
|
||||
# 最佳实践提取
|
||||
/bmad-architect "基于项目经验总结微服务架构最佳实践"
|
||||
```
|
||||
|
||||
### 3. 团队协作优化
|
||||
```bash
|
||||
# 团队能力评估
|
||||
/bmad-analyst "评估团队技术栈和能力匹配度"
|
||||
|
||||
# 开发计划调整
|
||||
/bmad-po "根据团队能力调整功能优先级和实现计划"
|
||||
```
|
||||
|
||||
## 🚦 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
**Q: BMAD工作流执行时间较长,如何优化?**
|
||||
A:
|
||||
- 简单功能使用 `/requirements-pilot`
|
||||
- 复杂项目使用分阶段执行 `--phase=planning`
|
||||
- 使用自定义代理序列减少不必要的步骤
|
||||
|
||||
**Q: 如何在BMAD和现有系统间选择?**
|
||||
A:
|
||||
- 项目复杂度 < 中等:使用 `/requirements-pilot`
|
||||
- 项目复杂度 ≥ 高:使用 `/bmad-workflow`
|
||||
- 需要商业分析:必须使用BMAD
|
||||
- 纯技术实现:可选择任一系统
|
||||
|
||||
**Q: 代理输出质量不符合预期怎么办?**
|
||||
A:
|
||||
- 提供更详细的项目描述
|
||||
- 使用分阶段执行,逐步细化
|
||||
- 结合独立代理使用进行专项优化
|
||||
|
||||
## 🎉 开始你的BMAD之旅
|
||||
|
||||
### 第一次使用
|
||||
```bash
|
||||
# 体验完整BMAD工作流
|
||||
/bmad-workflow "构建一个简单的博客系统,支持文章发布、评论、用户管理"
|
||||
```
|
||||
|
||||
### 学习不同代理角色
|
||||
```bash
|
||||
# 产品思维
|
||||
/bmad-po "分析博客系统的用户需求和使用场景"
|
||||
|
||||
# 商业思维
|
||||
/bmad-analyst "个人博客vs企业CMS市场定位分析"
|
||||
|
||||
# 技术思维
|
||||
/bmad-architect "可扩展博客系统架构设计"
|
||||
```
|
||||
|
||||
## 📚 进阶学习资源
|
||||
|
||||
- [BMAD-METHOD原理](https://github.com/bmadcode/BMAD-METHOD)
|
||||
- [Claude Code文档](https://docs.anthropic.com/en/docs/claude-code)
|
||||
- [敏捷开发最佳实践](https://agilemanifesto.org/)
|
||||
|
||||
---
|
||||
|
||||
**BMAD方法论 + Claude Code = 从理念到代码的完整AI开发工作流** 🚀
|
||||
|
||||
开始使用BMAD方法论,体验专业化AI代理团队带来的开发效率和质量提升!
|
||||
772
CHANGELOG.md
Normal file
772
CHANGELOG.md
Normal file
@@ -0,0 +1,772 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [5.6.4] - 2026-01-15
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- add reasoning effort config for codex backend
|
||||
- default to skip-permissions and bypass-sandbox
|
||||
- add multi-agent support with yolo mode
|
||||
- add omo module for multi-agent orchestration
|
||||
- add intelligent backend selection based on task complexity (#61)
|
||||
- v5.4.0 structured execution report (#94)
|
||||
- add millisecond-precision timestamps to all log entries (#91)
|
||||
- skill-install install script and security scan
|
||||
- add uninstall scripts with selective module removal
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- filter codex stderr noise logs
|
||||
- use config override for codex reasoning effort
|
||||
- propagate SkipPermissions to parallel tasks (#113)
|
||||
- add timeout for Windows process termination
|
||||
- reject dash as workdir parameter (#118)
|
||||
- add sleep in fake script to prevent CI race condition
|
||||
- fix gemini env load
|
||||
- fix omo
|
||||
- fix codeagent skill TaskOutput
|
||||
- 修复 Gemini init 事件 session_id 未提取的问题 (#111)
|
||||
- Windows 后端退出:taskkill 结束进程树 + turn.completed 支持 (#108)
|
||||
- support model parameter for all backends, auto-inject from settings (#105)
|
||||
- replace setx with reg add to avoid 1024-char PATH truncation (#101)
|
||||
- 移除未知事件格式的日志噪声 (#96)
|
||||
- prevent duplicate PATH entries on reinstall (#95)
|
||||
- Minor issues #12 and #13 - ASCII mode and performance optimization
|
||||
- correct settings.json filename and bump version to v5.2.8
|
||||
- allow claude backend to read env from setting.json while preventing recursion (#92)
|
||||
- comprehensive security and quality improvements for PR #85 & #87 (#90)
|
||||
- Improve backend termination after message and extend timeout (#86)
|
||||
- Parser重复解析优化 + 严重bug修复 + PR #86兼容性 (#88)
|
||||
- filter noisy stderr output from gemini backend (#83)
|
||||
- 修復 wsl install.sh 格式問題 (#78)
|
||||
- 修复多 backend 并行日志 PID 混乱并移除包装格式 (#74) (#76)
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- remove sisyphus agent and unused code
|
||||
- streamline agent documentation and remove sisyphus
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- add OmO workflow to README and fix plugin marketplace structure
|
||||
- update FAQ for default bypass/skip-permissions behavior
|
||||
- 添加 FAQ 常见问题章节
|
||||
- update troubleshooting with idempotent PATH commands (#95)
|
||||
|
||||
### 💼 Other
|
||||
|
||||
- add test-cases skill
|
||||
- add browser skill
|
||||
- BMADh和Requirements-Driven支持根据语义生成对应的文档 (#82)
|
||||
- update all readme
|
||||
|
||||
## [5.2.4] - 2025-12-16
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
|
||||
- integrate git-cliff for automated changelog generation
|
||||
|
||||
- bump version to 5.2.4
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- 防止 Claude backend 无限递归调用
|
||||
|
||||
- isolate log files per task in parallel mode
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge pull request #70 from cexll/fix/prevent-codeagent-infinite-recursion
|
||||
|
||||
- Merge pull request #69 from cexll/myclaude-master-20251215-073053-338465000
|
||||
|
||||
- update CHANGELOG.md
|
||||
|
||||
- Merge pull request #65 from cexll/fix/issue-64-buffer-overflow
|
||||
|
||||
## [5.2.3] - 2025-12-15
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- 修复 bufio.Scanner token too long 错误 ([#64](https://github.com/cexll/myclaude/issues/64))
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- change version
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
|
||||
- 同步测试中的版本号至 5.2.3
|
||||
|
||||
## [5.2.2] - 2025-12-13
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
|
||||
- Bump version and clean up documentation
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- fix codeagent backend claude no auto
|
||||
|
||||
- fix install.py dev fail
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
|
||||
- Fix tests for ClaudeBackend default --dangerously-skip-permissions
|
||||
|
||||
## [5.2.1] - 2025-12-13
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- fix codeagent claude and gemini root dir
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- update readme
|
||||
|
||||
## [5.2.0] - 2025-12-13
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
|
||||
- Update CHANGELOG and remove deprecated test files
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- fix race condition in stdout parsing
|
||||
|
||||
- add worker limit cap and remove legacy alias
|
||||
|
||||
- use -r flag for gemini backend resume
|
||||
|
||||
- clarify module list shows default state not enabled
|
||||
|
||||
- use -r flag for claude backend resume
|
||||
|
||||
- remove binary artifacts and improve error messages
|
||||
|
||||
- 异常退出时显示最近错误信息
|
||||
|
||||
- op_run_command 实时流式输出
|
||||
|
||||
- 修复权限标志逻辑和版本号测试
|
||||
|
||||
- 重构信号处理逻辑避免重复 nil 检查
|
||||
|
||||
- 移除 .claude 配置文件验证步骤
|
||||
|
||||
- 修复并行执行启动横幅重复打印问题
|
||||
|
||||
- 修复master合并后的编译和测试问题
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge rc/5.2 into master: v5.2.0 release improvements
|
||||
|
||||
- Merge pull request #53 from cexll/rc/5.2
|
||||
|
||||
- remove docs
|
||||
|
||||
- remove docs
|
||||
|
||||
- add prototype prompt skill
|
||||
|
||||
- add prd skill
|
||||
|
||||
- update memory claude
|
||||
|
||||
- remove command gh flow
|
||||
|
||||
- update license
|
||||
|
||||
- Merge branch 'master' into rc/5.2
|
||||
|
||||
- Merge pull request #52 from cexll/fix/parallel-log-path-on-startup
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
|
||||
- remove GitHub workflow related content
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
|
||||
- Complete skills system integration and config cleanup
|
||||
|
||||
- Improve release notes and installation scripts
|
||||
|
||||
- 添加终端日志输出和 verbose 模式
|
||||
|
||||
- 完整多后端支持与安全优化
|
||||
|
||||
- 替换 Codex 为 codeagent 并添加 UI 自动检测
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
|
||||
- 调整文件命名和技能定义
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
|
||||
- 添加 ExtractRecentErrors 单元测试
|
||||
|
||||
## [5.1.4] - 2025-12-09
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- 任务启动时立即返回日志文件路径以支持实时调试
|
||||
|
||||
## [5.1.3] - 2025-12-08
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- resolve CI timing race in TestFakeCmdInfra
|
||||
|
||||
## [5.1.2] - 2025-12-08
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- 修复channel同步竞态条件和死锁问题
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge pull request #51 from cexll/fix/channel-sync-race-conditions
|
||||
|
||||
- change codex-wrapper version
|
||||
|
||||
## [5.1.1] - 2025-12-08
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- 增强日志清理的安全性和可靠性
|
||||
|
||||
- resolve data race on forceKillDelay with atomic operations
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge pull request #49 from cexll/freespace8/master
|
||||
|
||||
- resolve signal handling conflict preserving testability and Windows support
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
|
||||
- 补充测试覆盖提升至 89.3%
|
||||
|
||||
## [5.1.0] - 2025-12-07
|
||||
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge pull request #45 from Michaelxwb/master
|
||||
|
||||
- 修改windows安装说明
|
||||
|
||||
- 修改打包脚本
|
||||
|
||||
- 支持windows系统的安装
|
||||
|
||||
- Merge pull request #1 from Michaelxwb/feature-win
|
||||
|
||||
- 支持window
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
|
||||
- 添加启动时清理日志的功能和--cleanup标志支持
|
||||
|
||||
- implement enterprise workflow with multi-backend support
|
||||
|
||||
## [5.0.0] - 2025-12-05
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
|
||||
- clarify unit-test coverage levels in requirement questions
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- defer startup log until args parsed
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge branch 'master' of github.com:cexll/myclaude
|
||||
|
||||
- Merge pull request #43 from gurdasnijor/smithery/add-badge
|
||||
|
||||
- Add Smithery badge
|
||||
|
||||
- Merge pull request #42 from freespace8/master
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
|
||||
- rewrite documentation for v5.0 modular architecture
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
|
||||
- feat install.py
|
||||
|
||||
- implement modular installation system
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
|
||||
- remove deprecated plugin modules
|
||||
|
||||
## [4.8.2] - 2025-12-02
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- skip signal test in CI environment
|
||||
|
||||
- make forceKillDelay testable to prevent signal test timeout
|
||||
|
||||
- correct Go version in go.mod from 1.25.3 to 1.21
|
||||
|
||||
- fix codex wrapper async log
|
||||
|
||||
- capture and include stderr in error messages
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge pull request #41 from cexll/fix-async-log
|
||||
|
||||
- remove test case 90
|
||||
|
||||
- optimize codex-wrapper
|
||||
|
||||
- Merge branch 'master' into fix-async-log
|
||||
|
||||
## [4.8.1] - 2025-12-01
|
||||
|
||||
|
||||
### 🎨 Styling
|
||||
|
||||
|
||||
- replace emoji with text labels
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- improve --parallel parameter validation and docs
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- remove codex-wrapper bin
|
||||
|
||||
## [4.8.0] - 2025-11-30
|
||||
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- update codex skill dependencies
|
||||
|
||||
## [4.7.3] - 2025-11-29
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- 保留日志文件以便程序退出后调试并完善日志输出功能
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge pull request #34 from cexll/cce-worktree-master-20251129-111802-997076000
|
||||
|
||||
- update CLAUDE.md and codex skill
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
|
||||
- improve codex skill parameter best practices
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
|
||||
- add session resume support and improve output format
|
||||
|
||||
- add parallel execution support to codex-wrapper
|
||||
|
||||
- add async logging to temp file with lifecycle management
|
||||
|
||||
## [4.7.2] - 2025-11-28
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- improve buffer size and streamline message extraction
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge pull request #32 from freespace8/master
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
|
||||
- 增加对超大单行文本和非字符串文本的处理测试
|
||||
|
||||
## [4.7.1] - 2025-11-27
|
||||
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- optimize dev pipline
|
||||
|
||||
- Merge feat/codex-wrapper: fix repository URLs
|
||||
|
||||
## [4.7] - 2025-11-27
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- update repository URLs to cexll/myclaude
|
||||
|
||||
## [4.7-alpha1] - 2025-11-27
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- fix marketplace schema validation error in dev-workflow plugin
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge pull request #29 from cexll/feat/codex-wrapper
|
||||
|
||||
- Add codex-wrapper Go implementation
|
||||
|
||||
- update readme
|
||||
|
||||
- update readme
|
||||
|
||||
## [4.6] - 2025-11-25
|
||||
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- update dev workflow
|
||||
|
||||
- update dev workflow
|
||||
|
||||
## [4.5] - 2025-11-25
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- fix codex skill eof
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- update dev workflow plugin
|
||||
|
||||
- update readme
|
||||
|
||||
## [4.4] - 2025-11-22
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- fix codex skill timeout and add more log
|
||||
|
||||
- fix codex skill
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- update gemini skills
|
||||
|
||||
- update dev workflow
|
||||
|
||||
- update codex skills model config
|
||||
|
||||
- Merge branch 'master' of github.com:cexll/myclaude
|
||||
|
||||
- Merge pull request #24 from cexll/swe-agent/23-1763544297
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
|
||||
- 支持通过环境变量配置 skills 模型
|
||||
|
||||
## [4.3] - 2025-11-19
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- fix codex skills running
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- update skills plugin
|
||||
|
||||
- update gemini
|
||||
|
||||
- update doc
|
||||
|
||||
- Add Gemini CLI integration skill
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
|
||||
- feat simple dev workflow
|
||||
|
||||
## [4.2.2] - 2025-11-15
|
||||
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- update codex skills
|
||||
|
||||
## [4.2.1] - 2025-11-14
|
||||
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge pull request #21 from Tshoiasc/master
|
||||
|
||||
- Merge branch 'master' into master
|
||||
|
||||
- Change default model to gpt-5.1-codex
|
||||
|
||||
- Enhance codex.py to auto-detect long inputs and switch to stdin mode, improving handling of shell argument issues. Updated build_codex_args to support stdin and added relevant logging for task length warnings.
|
||||
|
||||
## [4.2] - 2025-11-13
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- fix codex.py wsl run err
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- optimize codex skills
|
||||
|
||||
- Merge branch 'master' of github.com:cexll/myclaude
|
||||
|
||||
- Rename SKILLS.md to SKILL.md
|
||||
|
||||
- optimize codex skills
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
|
||||
- feat codex skills
|
||||
|
||||
## [4.1] - 2025-11-04
|
||||
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- update enhance-prompt.md response
|
||||
|
||||
- update readme
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
|
||||
- 新增 /enhance-prompt 命令并更新所有 README 文档
|
||||
|
||||
## [4.0] - 2025-10-22
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- fix skills format
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge branch 'master' of github.com:cexll/myclaude
|
||||
|
||||
- Merge pull request #18 from cexll/swe-agent/17-1760969135
|
||||
|
||||
- update requirements clarity
|
||||
|
||||
- update .gitignore
|
||||
|
||||
- Fix #17: Update root marketplace.json to use skills array
|
||||
|
||||
- Fix #17: Convert requirements-clarity to correct plugin directory format
|
||||
|
||||
- Fix #17: Convert requirements-clarity to correct plugin directory format
|
||||
|
||||
- Convert requirements-clarity to plugin format with English prompts
|
||||
|
||||
- Translate requirements-clarity skill to English for plugin compatibility
|
||||
|
||||
- Add requirements-clarity Claude Skill
|
||||
|
||||
- Add requirements clarification command
|
||||
|
||||
- update
|
||||
|
||||
## [3.5] - 2025-10-20
|
||||
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge pull request #15 from cexll/swe-agent/13-1760944712
|
||||
|
||||
- Fix #13: Clean up redundant README files
|
||||
|
||||
- Optimize README structure - Solution A (modular)
|
||||
|
||||
- Merge pull request #14 from cexll/swe-agent/12-1760944588
|
||||
|
||||
- Fix #12: Update Makefile install paths for new directory structure
|
||||
|
||||
## [3.4] - 2025-10-20
|
||||
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Merge pull request #11 from cexll/swe-agent/10-1760752533
|
||||
|
||||
- Fix marketplace metadata references
|
||||
|
||||
- Fix plugin configuration: rename to marketplace.json and update repository URLs
|
||||
|
||||
- Fix #10: Restructure plugin directories to ensure proper command isolation
|
||||
|
||||
## [3.3] - 2025-10-15
|
||||
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Update README-zh.md
|
||||
|
||||
- Update README.md
|
||||
|
||||
- Update marketplace.json
|
||||
|
||||
- Update Chinese README with v3.2 plugin system documentation
|
||||
|
||||
- Update README with v3.2 plugin system documentation
|
||||
|
||||
## [3.2] - 2025-10-10
|
||||
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- Add Claude Code plugin system support
|
||||
|
||||
- update readme
|
||||
|
||||
- Add Makefile for quick deployment and update READMEs
|
||||
|
||||
## [3.1] - 2025-09-17
|
||||
|
||||
|
||||
### ◀️ Revert
|
||||
|
||||
|
||||
- revert
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
- fixed bmad-orchestrator not fund
|
||||
|
||||
- fix bmad
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
||||
- update bmad review with codex support
|
||||
|
||||
- 优化 BMAD 工作流和代理配置
|
||||
|
||||
- update gpt5
|
||||
|
||||
- support bmad output-style
|
||||
|
||||
- update bmad user guide
|
||||
|
||||
- update bmad readme
|
||||
|
||||
- optimize requirements pilot
|
||||
|
||||
- add use gpt5 codex
|
||||
|
||||
- add bmad pilot
|
||||
|
||||
- sync READMEs with actual commands/agents; remove nonexistent commands; enhance requirements-pilot with testing decision gate and options.
|
||||
|
||||
- Update Chinese README and requirements-pilot command to align with latest workflow
|
||||
|
||||
- update readme
|
||||
|
||||
- update agent
|
||||
|
||||
- update bugfix sub agents
|
||||
|
||||
- Update ask support KISS YAGNI SOLID
|
||||
|
||||
- Add comprehensive documentation and multi-agent workflow system
|
||||
|
||||
- update commands
|
||||
<!-- 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/>.
|
||||
91
Makefile
91
Makefile
@@ -1,35 +1,46 @@
|
||||
# Claude Code Multi-Agent Workflow System Makefile
|
||||
# Quick deployment for BMAD and Requirements workflows
|
||||
|
||||
.PHONY: help install deploy-bmad deploy-requirements deploy-all clean test
|
||||
.PHONY: help install deploy-bmad deploy-requirements deploy-essentials deploy-advanced deploy-all deploy-commands deploy-agents clean test changelog
|
||||
|
||||
# Default target
|
||||
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 " deploy-bmad - Deploy BMAD workflow (bmad-pilot)"
|
||||
@echo " deploy-requirements - Deploy Requirements workflow (requirements-pilot)"
|
||||
@echo " deploy-commands - Deploy all slash commands"
|
||||
@echo " deploy-agents - Deploy all agent configurations"
|
||||
@echo " deploy-all - Deploy everything (commands + agents)"
|
||||
@echo " test-bmad - Test BMAD workflow with sample"
|
||||
@echo " test-requirements - Test Requirements workflow with sample"
|
||||
@echo " clean - Clean generated artifacts"
|
||||
@echo " help - Show this help message"
|
||||
@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"
|
||||
@echo " deploy-advanced - Deploy Advanced AI Agents"
|
||||
@echo " deploy-commands - Deploy all slash commands"
|
||||
@echo " deploy-agents - Deploy all agent configurations"
|
||||
@echo " deploy-all - Deploy everything (commands + agents)"
|
||||
@echo " test-bmad - Test BMAD workflow with sample"
|
||||
@echo " test-requirements - Test Requirements workflow with sample"
|
||||
@echo " changelog - Update CHANGELOG.md using git-cliff"
|
||||
@echo " clean - Clean generated artifacts"
|
||||
@echo " help - Show this help message"
|
||||
|
||||
# Configuration paths
|
||||
CLAUDE_CONFIG_DIR = ~/.claude
|
||||
COMMANDS_DIR = commands
|
||||
AGENTS_DIR = agents
|
||||
OUTPUT_STYLES_DIR = output-styles
|
||||
SPECS_DIR = .claude/specs
|
||||
|
||||
# Workflow directories
|
||||
BMAD_DIR = bmad-agile-workflow
|
||||
REQUIREMENTS_DIR = requirements-driven-workflow
|
||||
ESSENTIALS_DIR = development-essentials
|
||||
ADVANCED_DIR = advanced-ai-agents
|
||||
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
|
||||
@@ -38,8 +49,8 @@ deploy-bmad:
|
||||
@mkdir -p $(CLAUDE_CONFIG_DIR)/commands
|
||||
@mkdir -p $(CLAUDE_CONFIG_DIR)/agents
|
||||
@mkdir -p $(CLAUDE_CONFIG_DIR)/output-styles
|
||||
@cp $(COMMANDS_DIR)/bmad-pilot.md $(CLAUDE_CONFIG_DIR)/commands/
|
||||
@cp $(AGENTS_DIR)/bmad-*.md $(CLAUDE_CONFIG_DIR)/agents/
|
||||
@cp $(BMAD_DIR)/commands/bmad-pilot.md $(CLAUDE_CONFIG_DIR)/commands/
|
||||
@cp $(BMAD_DIR)/agents/*.md $(CLAUDE_CONFIG_DIR)/agents/
|
||||
@cp $(OUTPUT_STYLES_DIR)/bmad.md $(CLAUDE_CONFIG_DIR)/output-styles/ 2>/dev/null || true
|
||||
@echo "✅ BMAD workflow deployed successfully!"
|
||||
@echo " Usage: /bmad-pilot \"your feature description\""
|
||||
@@ -49,16 +60,35 @@ deploy-requirements:
|
||||
@echo "🚀 Deploying Requirements workflow..."
|
||||
@mkdir -p $(CLAUDE_CONFIG_DIR)/commands
|
||||
@mkdir -p $(CLAUDE_CONFIG_DIR)/agents
|
||||
@cp $(COMMANDS_DIR)/requirements-pilot.md $(CLAUDE_CONFIG_DIR)/commands/
|
||||
@cp $(AGENTS_DIR)/requirements-*.md $(CLAUDE_CONFIG_DIR)/agents/
|
||||
@cp $(REQUIREMENTS_DIR)/commands/requirements-pilot.md $(CLAUDE_CONFIG_DIR)/commands/
|
||||
@cp $(REQUIREMENTS_DIR)/agents/*.md $(CLAUDE_CONFIG_DIR)/agents/
|
||||
@echo "✅ Requirements workflow deployed successfully!"
|
||||
@echo " Usage: /requirements-pilot \"your feature description\""
|
||||
|
||||
# Deploy Development Essentials workflow
|
||||
deploy-essentials:
|
||||
@echo "🚀 Deploying Development Essentials workflow..."
|
||||
@mkdir -p $(CLAUDE_CONFIG_DIR)/commands
|
||||
@mkdir -p $(CLAUDE_CONFIG_DIR)/agents
|
||||
@cp $(ESSENTIALS_DIR)/commands/*.md $(CLAUDE_CONFIG_DIR)/commands/
|
||||
@cp $(ESSENTIALS_DIR)/agents/*.md $(CLAUDE_CONFIG_DIR)/agents/
|
||||
@echo "✅ Development Essentials deployed successfully!"
|
||||
@echo " Available commands: /ask, /code, /debug, /test, /review, /optimize, /bugfix, /refactor, /docs, /think"
|
||||
|
||||
# Deploy Advanced AI Agents
|
||||
deploy-advanced:
|
||||
@echo "🚀 Deploying Advanced AI Agents..."
|
||||
@mkdir -p $(CLAUDE_CONFIG_DIR)/agents
|
||||
@cp $(ADVANCED_DIR)/agents/*.md $(CLAUDE_CONFIG_DIR)/agents/
|
||||
@echo "✅ Advanced AI Agents deployed successfully!"
|
||||
|
||||
# Deploy all commands
|
||||
deploy-commands:
|
||||
@echo "📦 Deploying all slash commands..."
|
||||
@mkdir -p $(CLAUDE_CONFIG_DIR)/commands
|
||||
@cp $(COMMANDS_DIR)/*.md $(CLAUDE_CONFIG_DIR)/commands/
|
||||
@cp $(BMAD_DIR)/commands/*.md $(CLAUDE_CONFIG_DIR)/commands/
|
||||
@cp $(REQUIREMENTS_DIR)/commands/*.md $(CLAUDE_CONFIG_DIR)/commands/
|
||||
@cp $(ESSENTIALS_DIR)/commands/*.md $(CLAUDE_CONFIG_DIR)/commands/
|
||||
@echo "✅ All commands deployed!"
|
||||
@echo " Available commands:"
|
||||
@echo " - /bmad-pilot (Full agile workflow)"
|
||||
@@ -70,7 +100,10 @@ deploy-commands:
|
||||
deploy-agents:
|
||||
@echo "🤖 Deploying all agents..."
|
||||
@mkdir -p $(CLAUDE_CONFIG_DIR)/agents
|
||||
@cp $(AGENTS_DIR)/*.md $(CLAUDE_CONFIG_DIR)/agents/
|
||||
@cp $(BMAD_DIR)/agents/*.md $(CLAUDE_CONFIG_DIR)/agents/
|
||||
@cp $(REQUIREMENTS_DIR)/agents/*.md $(CLAUDE_CONFIG_DIR)/agents/
|
||||
@cp $(ESSENTIALS_DIR)/agents/*.md $(CLAUDE_CONFIG_DIR)/agents/
|
||||
@cp $(ADVANCED_DIR)/agents/*.md $(CLAUDE_CONFIG_DIR)/agents/
|
||||
@echo "✅ All agents deployed!"
|
||||
|
||||
# Deploy everything
|
||||
@@ -105,9 +138,25 @@ clean:
|
||||
# Quick deployment shortcuts
|
||||
bmad: deploy-bmad
|
||||
requirements: deploy-requirements
|
||||
essentials: deploy-essentials
|
||||
advanced: deploy-advanced
|
||||
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"
|
||||
|
||||
# Update CHANGELOG.md using git-cliff
|
||||
changelog:
|
||||
@echo "📝 Updating CHANGELOG.md with git-cliff..."
|
||||
@if ! command -v git-cliff > /dev/null 2>&1; then \
|
||||
echo "❌ git-cliff not found. Installing via Homebrew..."; \
|
||||
brew install git-cliff; \
|
||||
fi
|
||||
@git-cliff -o CHANGELOG.md
|
||||
@echo "✅ CHANGELOG.md updated successfully!"
|
||||
@echo ""
|
||||
@echo "Preview the changes:"
|
||||
@echo " git diff CHANGELOG.md"
|
||||
|
||||
|
||||
@@ -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)
|
||||
238
README-zh.md
238
README-zh.md
@@ -1,238 +0,0 @@
|
||||
# Claude Code 多智能体工作流系统
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://claude.ai/code)
|
||||
[](https://github.com/)
|
||||
|
||||
> 企业级敏捷开发工作流自动化与多智能体编排
|
||||
|
||||
[English](README.md)
|
||||
|
||||
## 🚀 BMAD 方法论:敏捷开发自动化
|
||||
|
||||
**BMAD (Business-Minded Agile Development)** 将您的开发流程转换为全自动化的敏捷工作流,配备角色化 AI 智能体和质量门控。
|
||||
|
||||
### 一条命令,完整工作流
|
||||
|
||||
```bash
|
||||
/bmad-pilot "构建电商结账系统,集成支付功能"
|
||||
# 自动化:产品 → 架构 → 冲刺 → 开发 → 审查 → 测试
|
||||
```
|
||||
|
||||
## 🎯 BMAD 工作流架构
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
PO[产品负责人] -->|PRD 90+| Architect[架构师]
|
||||
Architect -->|设计 90+| SM[Scrum Master]
|
||||
SM -->|冲刺计划| Dev[开发者]
|
||||
Dev -->|代码| Review[审查]
|
||||
Review -->|Pass/Fail| QA[测试]
|
||||
QA -->|测试| Done[完成]
|
||||
```
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **🤖 6个专业智能体**:PO、Architect、SM、Dev、Review、QA
|
||||
- **📊 质量门控**:90% 阈值自动优化
|
||||
- **✅ 确认节点**:关键阶段用户确认
|
||||
- **📁 持久化产物**:所有文档保存至 `./.claude/specs/`
|
||||
- **🔄 迭代优化**:自动改进直至质量达标
|
||||
|
||||
## 📋 BMAD 智能体与角色
|
||||
|
||||
| 智能体 | 角色 | 质量门控 | 输出 |
|
||||
|--------|------|----------|------|
|
||||
| **bmad-po** (Sarah) | 产品需求收集 | 90/100 PRD 评分 | `01-product-requirements.md` |
|
||||
| **bmad-architect** (Winston) | 技术设计与架构 | 90/100 设计评分 | `02-system-architecture.md` |
|
||||
| **bmad-sm** (Mike) | 冲刺计划与任务分解 | 用户确认 | `03-sprint-plan.md` |
|
||||
| **bmad-dev** (Alex) | 功能实现 | 代码完成 | 实现文件 |
|
||||
| **bmad-review** | 独立代码审查 | Pass/Risk/Fail | `04-dev-reviewed.md` |
|
||||
| **bmad-qa** (Emma) | 测试与质量保证 | 测试执行 | `05-qa-report.md` |
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 一键安装
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/your-repo/claude-code-workflows.git
|
||||
cd claude-code-workflows
|
||||
|
||||
# 使用 make 安装所有配置
|
||||
make install
|
||||
|
||||
# 或部署特定工作流
|
||||
make deploy-bmad # 仅部署 BMAD 工作流
|
||||
make deploy-requirements # 仅部署 Requirements 工作流
|
||||
make deploy-all # 部署所有命令和智能体
|
||||
```
|
||||
|
||||
### 基本 BMAD 工作流
|
||||
|
||||
```bash
|
||||
# 完整敏捷工作流(所有阶段)
|
||||
/bmad-pilot "用户认证系统,支持 OAuth2 和多因素认证"
|
||||
|
||||
# 快速原型(跳过测试)
|
||||
/bmad-pilot "管理后台" --skip-tests
|
||||
|
||||
# 直接开发(跳过冲刺计划)
|
||||
/bmad-pilot "修复登录问题" --direct-dev
|
||||
|
||||
# 跳过仓库扫描(使用现有上下文)
|
||||
/bmad-pilot "添加功能" --skip-scan
|
||||
```
|
||||
|
||||
### 工作流产物
|
||||
|
||||
每次 BMAD 运行创建结构化文档:
|
||||
|
||||
```
|
||||
.claude/specs/user-authentication/
|
||||
├── 00-repository-context.md # 仓库分析
|
||||
├── 01-product-requirements.md # PRD 及业务目标
|
||||
├── 02-system-architecture.md # 技术设计
|
||||
├── 03-sprint-plan.md # 冲刺任务
|
||||
├── 04-dev-reviewed.md # 代码审查报告(v3.1 新增)
|
||||
└── 05-qa-report.md # 测试结果
|
||||
```
|
||||
|
||||
## 🎨 BMAD 输出样式
|
||||
|
||||
BMAD 工作流使用专门的输出样式:
|
||||
- 创建阶段隔离的上下文
|
||||
- 管理智能体交接
|
||||
- 跟踪质量评分
|
||||
- 处理确认门控
|
||||
- 支持 Codex CLI 集成
|
||||
|
||||
## ⚡ v3.1 新特性
|
||||
|
||||
### 独立代码审查智能体
|
||||
- **bmad-review**:Dev 和 QA 之间的自动审查
|
||||
- **双版本支持**:
|
||||
- 标准版:Claude Code 原生审查
|
||||
- 增强版:通过 Codex CLI 调用 GPT-5
|
||||
- **三级状态**:Pass / Pass with Risk / Fail
|
||||
|
||||
### 增强工作流
|
||||
- Dev → Review → QA 质量链
|
||||
- 自动更新冲刺计划
|
||||
- 针对性 QA 测试建议
|
||||
|
||||
## 📊 质量评分系统
|
||||
|
||||
### PRD 质量(100分)
|
||||
- 业务价值:30
|
||||
- 功能需求:25
|
||||
- 用户体验:20
|
||||
- 技术约束:15
|
||||
- 范围与优先级:10
|
||||
|
||||
### 架构质量(100分)
|
||||
- 设计质量:30
|
||||
- 技术选型:25
|
||||
- 可扩展性:20
|
||||
- 安全性:15
|
||||
- 可行性:10
|
||||
|
||||
### 审查状态
|
||||
- **Pass**:无问题,进入 QA
|
||||
- **Pass with Risk**:非关键问题
|
||||
- **Fail**:必须返回 Dev
|
||||
|
||||
## 🔧 高级用法
|
||||
|
||||
### 仓库上下文
|
||||
BMAD 自动扫描仓库了解:
|
||||
- 技术栈
|
||||
- 项目结构
|
||||
- 现有模式
|
||||
- 依赖关系
|
||||
- 编码规范
|
||||
|
||||
### 交互式优化
|
||||
每个阶段支持迭代改进:
|
||||
```
|
||||
PO: "这是 PRD(评分:75/100)"
|
||||
用户: "添加移动端支持和离线模式"
|
||||
PO: "更新的 PRD(评分:92/100)✅"
|
||||
```
|
||||
|
||||
### 确认门控
|
||||
关键阶段需要明确确认:
|
||||
```
|
||||
架构师: "技术设计完成(评分:93/100)"
|
||||
系统: "准备继续?(yes/no)"
|
||||
用户: yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏭 Requirements-Driven 工作流
|
||||
|
||||
适用于简单项目的轻量级替代方案:
|
||||
|
||||
```bash
|
||||
/requirements-pilot "实现 JWT 认证"
|
||||
# 自动化:需求 → 代码 → 审查 → 测试
|
||||
```
|
||||
|
||||
### 特性
|
||||
- 90% 质量门控
|
||||
- 自动优化循环
|
||||
- 实现导向规格
|
||||
- 实用主义优先
|
||||
|
||||
## 🛠️ 其他命令
|
||||
|
||||
### 开发命令
|
||||
- `/ask` - 技术咨询
|
||||
- `/code` - 直接实现
|
||||
- `/debug` - 系统化调试
|
||||
- `/test` - 测试策略
|
||||
- `/review` - 代码验证
|
||||
- `/optimize` - 性能优化
|
||||
- `/bugfix` - 错误解决
|
||||
- `/refactor` - 代码改进
|
||||
- `/docs` - 文档生成
|
||||
- `/think` - 高级分析
|
||||
|
||||
### 手动工作流示例
|
||||
```bash
|
||||
/ask "实时消息的设计模式"
|
||||
/code "实现 WebSocket 服务器"
|
||||
/test "创建集成测试"
|
||||
/review "验证安全性"
|
||||
```
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT 许可证 - 查看 [LICENSE](LICENSE) 文件
|
||||
|
||||
## 🙋 支持
|
||||
|
||||
- **文档**:查看 `/commands/` 和 `/agents/` 目录
|
||||
- **问题**:GitHub issues 用于报告 bug 和功能请求
|
||||
- **Makefile 帮助**:运行 `make help` 查看所有部署选项
|
||||
|
||||
### 可用的 Make 命令
|
||||
|
||||
```bash
|
||||
make install # 安装所有配置到 Claude Code
|
||||
make deploy-bmad # 仅部署 BMAD 工作流
|
||||
make deploy-requirements # 仅部署 Requirements 工作流
|
||||
make deploy-commands # 部署所有斜杠命令
|
||||
make deploy-agents # 部署所有智能体配置
|
||||
make test-bmad # 测试 BMAD 工作流示例
|
||||
make test-requirements # 测试 Requirements 工作流示例
|
||||
make clean # 清理生成的文件
|
||||
make help # 显示所有可用命令
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**使用 BMAD 转型您的开发** - 一条命令,完整敏捷工作流,质量保证。
|
||||
|
||||
*让专业的 AI 智能体处理专业工作。*
|
||||
753
README.md
753
README.md
@@ -1,238 +1,621 @@
|
||||
[中文](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/)
|
||||
[](https://github.com/cexll/myclaude)
|
||||
|
||||
> Enterprise-grade agile development workflow automation with multi-agent orchestration
|
||||
> AI-powered development automation with multi-backend execution (Codex/Claude/Gemini)
|
||||
|
||||
[中文](README-zh.md)
|
||||
## Core Concept: Multi-Backend Architecture
|
||||
|
||||
## 🚀 BMAD Methodology: Agile Development Automation
|
||||
This system leverages a **dual-agent architecture** with pluggable AI backends:
|
||||
|
||||
**BMAD (Business-Minded Agile Development)** transforms your development process into a fully automated agile workflow with role-based AI agents and quality gates.
|
||||
| Role | Agent | Responsibility |
|
||||
|------|-------|----------------|
|
||||
| **Orchestrator** | Claude Code | Planning, context gathering, verification, user interaction |
|
||||
| **Executor** | codeagent-wrapper | Code editing, test execution (Codex/Claude/Gemini backends) |
|
||||
|
||||
### One Command, Complete Workflow
|
||||
**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)
|
||||
|
||||
```bash
|
||||
/bmad-pilot "Build e-commerce checkout system with payment integration"
|
||||
# Automated: Product → Architecture → Sprint → Dev → Review → QA
|
||||
git clone https://github.com/cexll/myclaude.git
|
||||
cd myclaude
|
||||
python3 install.py --install-dir ~/.claude
|
||||
```
|
||||
|
||||
## 🎯 BMAD Workflow Architecture
|
||||
## Workflows Overview
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
PO[Product Owner] -->|PRD 90+| Architect
|
||||
Architect -->|Design 90+| SM[Scrum Master]
|
||||
SM -->|Sprint Plan| Dev
|
||||
Dev -->|Code| Review
|
||||
Review -->|Pass/Fail| QA
|
||||
QA -->|Tests| Done
|
||||
```
|
||||
### 0. OmO Multi-Agent Orchestrator (Recommended for Complex Tasks)
|
||||
|
||||
### Key Features
|
||||
|
||||
- **🤖 6 Specialized Agents**: PO, Architect, SM, Dev, Review, QA
|
||||
- **📊 Quality Gates**: 90% thresholds with automatic optimization
|
||||
- **✅ Approval Points**: User confirmation at critical phases
|
||||
- **📁 Persistent Artifacts**: All documents saved to `./.claude/specs/`
|
||||
- **🔄 Iterative Refinement**: Automatic improvement until quality met
|
||||
|
||||
## 📋 BMAD Agents & Roles
|
||||
|
||||
| Agent | Role | Quality Gate | Output |
|
||||
|-------|------|--------------|--------|
|
||||
| **bmad-po** (Sarah) | Product requirements gathering | 90/100 PRD score | `01-product-requirements.md` |
|
||||
| **bmad-architect** (Winston) | Technical design & architecture | 90/100 design score | `02-system-architecture.md` |
|
||||
| **bmad-sm** (Mike) | Sprint planning & task breakdown | User approval | `03-sprint-plan.md` |
|
||||
| **bmad-dev** (Alex) | Feature implementation | Code completion | Implementation files |
|
||||
| **bmad-review** | Independent code review | Pass/Risk/Fail | `04-dev-reviewed.md` |
|
||||
| **bmad-qa** (Emma) | Testing & quality assurance | Test execution | `05-qa-report.md` |
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### One-Command Installation
|
||||
**Intelligent multi-agent orchestration that routes tasks to specialized agents based on risk signals.**
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/your-repo/claude-code-workflows.git
|
||||
cd claude-code-workflows
|
||||
|
||||
# Install everything with make
|
||||
make install
|
||||
|
||||
# Or deploy specific workflows
|
||||
make deploy-bmad # Deploy BMAD workflow only
|
||||
make deploy-requirements # Deploy Requirements workflow only
|
||||
make deploy-all # Deploy all commands and agents
|
||||
/omo "analyze and fix this authentication bug"
|
||||
```
|
||||
|
||||
### Basic BMAD Workflow
|
||||
**Agent Hierarchy:**
|
||||
| Agent | Role | Backend | Model |
|
||||
|-------|------|---------|-------|
|
||||
| `oracle` | Technical advisor | Claude | claude-opus-4-5 |
|
||||
| `librarian` | External research | Claude | claude-sonnet-4-5 |
|
||||
| `explore` | Codebase search | OpenCode | grok-code |
|
||||
| `develop` | Code implementation | Codex | gpt-5.2 |
|
||||
| `frontend-ui-ux-engineer` | UI/UX specialist | Gemini | gemini-3-pro |
|
||||
| `document-writer` | Documentation | Gemini | gemini-3-flash |
|
||||
|
||||
**Routing Signals (Not Fixed Pipeline):**
|
||||
- Code location unclear → `explore`
|
||||
- External library/API → `librarian`
|
||||
- Risky/multi-file change → `oracle`
|
||||
- Implementation needed → `develop` / `frontend-ui-ux-engineer`
|
||||
|
||||
**Common Recipes:**
|
||||
- Explain code: `explore`
|
||||
- Small fix with known location: `develop` directly
|
||||
- Bug fix, location unknown: `explore → develop`
|
||||
- Cross-cutting refactor: `explore → oracle → develop`
|
||||
- External API integration: `explore + librarian → oracle → develop`
|
||||
|
||||
**Best For:** Complex bug investigation, multi-file refactoring, architecture decisions
|
||||
|
||||
---
|
||||
|
||||
### 1. Dev Workflow (Recommended)
|
||||
|
||||
**The primary workflow for most development tasks.**
|
||||
|
||||
```bash
|
||||
# Full agile workflow with all phases
|
||||
/bmad-pilot "User authentication system with OAuth2 and MFA"
|
||||
|
||||
# Skip testing for quick prototypes
|
||||
/bmad-pilot "Admin dashboard" --skip-tests
|
||||
|
||||
# Direct development (skip sprint planning)
|
||||
/bmad-pilot "Bug fix for login issue" --direct-dev
|
||||
|
||||
# Skip repository scanning (use existing context)
|
||||
/bmad-pilot "Add feature" --skip-scan
|
||||
/dev "implement user authentication with JWT"
|
||||
```
|
||||
|
||||
### Workflow Artifacts
|
||||
**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
|
||||
|
||||
Each BMAD run creates structured documentation:
|
||||
**Key Features:**
|
||||
- Claude Code orchestrates, Codex executes all code changes
|
||||
- Automatic task parallelization for speed
|
||||
- Mandatory 90% test coverage gate
|
||||
- Rollback on failure
|
||||
|
||||
```
|
||||
.claude/specs/user-authentication/
|
||||
├── 00-repository-context.md # Repository analysis
|
||||
├── 01-product-requirements.md # PRD with business goals
|
||||
├── 02-system-architecture.md # Technical design
|
||||
├── 03-sprint-plan.md # Sprint tasks
|
||||
├── 04-dev-reviewed.md # Code review report (NEW v3.1)
|
||||
└── 05-qa-report.md # Test results
|
||||
**Best For:** Feature development, refactoring, bug fixes with tests
|
||||
|
||||
---
|
||||
|
||||
### 2. BMAD Agile Workflow
|
||||
|
||||
**Full enterprise agile methodology with 6 specialized agents.**
|
||||
|
||||
```bash
|
||||
/bmad-pilot "build e-commerce checkout system"
|
||||
```
|
||||
|
||||
## 🎨 BMAD Output Style
|
||||
**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 |
|
||||
|
||||
The BMAD workflow uses a specialized output style that:
|
||||
- Creates phase-separated contexts
|
||||
- Manages agent handoffs
|
||||
- Tracks quality scores
|
||||
- Handles approval gates
|
||||
- Supports Codex CLI integration
|
||||
|
||||
## ⚡ v3.1 New Features
|
||||
|
||||
### Independent Code Review Agent
|
||||
- **bmad-review**: Automated review between Dev and QA
|
||||
- **Dual Version Support**:
|
||||
- Standard: Native Claude Code review
|
||||
- Enhanced: GPT-5 via Codex CLI
|
||||
- **Three-tier Status**: Pass / Pass with Risk / Fail
|
||||
|
||||
### Enhanced Workflow
|
||||
- Dev → Review → QA quality chain
|
||||
- Automatic Sprint plan updates
|
||||
- Targeted QA test recommendations
|
||||
|
||||
## 📊 Quality Scoring Systems
|
||||
|
||||
### PRD Quality (100 points)
|
||||
- Business Value: 30
|
||||
- Functional Requirements: 25
|
||||
- User Experience: 20
|
||||
- Technical Constraints: 15
|
||||
- Scope & Priorities: 10
|
||||
|
||||
### Architecture Quality (100 points)
|
||||
- Design Quality: 30
|
||||
- Technology Selection: 25
|
||||
- Scalability: 20
|
||||
- Security: 15
|
||||
- Feasibility: 10
|
||||
|
||||
### Review Status
|
||||
- **Pass**: No issues, proceed to QA
|
||||
- **Pass with Risk**: Non-critical issues
|
||||
- **Fail**: Must return to Dev
|
||||
|
||||
## 🔧 Advanced Usage
|
||||
|
||||
### Repository Context
|
||||
BMAD automatically scans your repository to understand:
|
||||
- Technology stack
|
||||
- Project structure
|
||||
- Existing patterns
|
||||
- Dependencies
|
||||
- Conventions
|
||||
|
||||
### Interactive Refinement
|
||||
Each phase supports iterative improvement:
|
||||
**Process:**
|
||||
```
|
||||
PO: "Here's the PRD (Score: 75/100)"
|
||||
User: "Add mobile support and offline mode"
|
||||
PO: "Updated PRD (Score: 92/100) ✅"
|
||||
Requirements → Architecture → Sprint Plan → Development → Review → QA
|
||||
↓ ↓ ↓ ↓ ↓ ↓
|
||||
PRD.md DESIGN.md SPRINT.md Code REVIEW.md TEST.md
|
||||
```
|
||||
|
||||
### Approval Gates
|
||||
Critical phases require explicit confirmation:
|
||||
**Best For:** Large features, team coordination, enterprise projects
|
||||
|
||||
---
|
||||
|
||||
### 3. Requirements-Driven Workflow
|
||||
|
||||
**Lightweight requirements-to-code pipeline.**
|
||||
|
||||
```bash
|
||||
/requirements-pilot "implement API rate limiting"
|
||||
```
|
||||
Architect: "Technical design complete (Score: 93/100)"
|
||||
System: "Ready to proceed? (yes/no)"
|
||||
User: yes
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## Version Requirements
|
||||
|
||||
### Codex CLI
|
||||
**Minimum version:** Check compatibility with your installation
|
||||
|
||||
The codeagent-wrapper uses these Codex CLI features:
|
||||
- `codex e` - Execute commands (shorthand for `codex exec`)
|
||||
- `--skip-git-repo-check` - Skip git repository validation
|
||||
- `--json` - JSON stream output format
|
||||
- `-C <workdir>` - Set working directory
|
||||
- `resume <session_id>` - Resume previous sessions
|
||||
|
||||
**Verify Codex CLI is installed:**
|
||||
```bash
|
||||
which codex
|
||||
codex --version
|
||||
```
|
||||
|
||||
### Claude CLI
|
||||
**Minimum version:** Check compatibility with your installation
|
||||
|
||||
Required features:
|
||||
- `--output-format stream-json` - Streaming JSON output format
|
||||
- `--setting-sources` - Control setting sources (prevents infinite recursion)
|
||||
- `--dangerously-skip-permissions` - Skip permission prompts (use with caution)
|
||||
- `-p` - Prompt input flag
|
||||
- `-r <session_id>` - Resume sessions
|
||||
|
||||
**Security Note:** The wrapper adds `--dangerously-skip-permissions` for Claude by default. Set `CODEAGENT_SKIP_PERMISSIONS=false` to disable if you need permission prompts.
|
||||
|
||||
**Verify Claude CLI is installed:**
|
||||
```bash
|
||||
which claude
|
||||
claude --version
|
||||
```
|
||||
|
||||
### Gemini CLI
|
||||
**Minimum version:** Check compatibility with your installation
|
||||
|
||||
Required features:
|
||||
- `-o stream-json` - JSON stream output format
|
||||
- `-y` - Auto-approve prompts (non-interactive mode)
|
||||
- `-r <session_id>` - Resume sessions
|
||||
- `-p` - Prompt input flag
|
||||
|
||||
**Verify Gemini CLI is installed:**
|
||||
```bash
|
||||
which gemini
|
||||
gemini --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏭 Requirements-Driven Workflow
|
||||
## Installation
|
||||
|
||||
An alternative lightweight workflow for simpler projects:
|
||||
### Modular Installation (Recommended)
|
||||
|
||||
```bash
|
||||
/requirements-pilot "Implement JWT authentication"
|
||||
# Automated: Requirements → Code → Review → Test
|
||||
# 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
|
||||
```
|
||||
|
||||
### Features
|
||||
- 90% quality gates
|
||||
- Automatic optimization loops
|
||||
- Implementation-focused specs
|
||||
- Pragmatic over architectural
|
||||
### Available Modules
|
||||
|
||||
## 🛠️ Other Commands
|
||||
| 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 |
|
||||
|
||||
### Development Commands
|
||||
- `/ask` - Technical consultation
|
||||
- `/code` - Direct implementation
|
||||
- `/debug` - Systematic debugging
|
||||
- `/test` - Testing strategies
|
||||
- `/review` - Code validation
|
||||
- `/optimize` - Performance tuning
|
||||
- `/bugfix` - Bug resolution
|
||||
- `/refactor` - Code improvement
|
||||
- `/docs` - Documentation
|
||||
- `/think` - Advanced analysis
|
||||
### What Gets Installed
|
||||
|
||||
### Manual Workflow Example
|
||||
```bash
|
||||
/ask "Design patterns for real-time messaging"
|
||||
/code "Implement WebSocket server"
|
||||
/test "Create integration tests"
|
||||
/review "Validate security"
|
||||
```
|
||||
~/.claude/
|
||||
├── bin/
|
||||
│ └── codeagent-wrapper # Main executable
|
||||
├── CLAUDE.md # Core instructions and role definition
|
||||
├── commands/ # Slash commands (/dev, /code, etc.)
|
||||
├── agents/ # Agent definitions
|
||||
├── skills/
|
||||
│ └── codex/
|
||||
│ └── SKILL.md # Codex integration skill
|
||||
├── config.json # Configuration
|
||||
└── installed_modules.json # Installation status
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
### Customizing Installation Directory
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) file
|
||||
|
||||
## 🙋 Support
|
||||
|
||||
- **Documentation**: Check `/commands/` and `/agents/` directories
|
||||
- **Issues**: GitHub issues for bugs and features
|
||||
- **Makefile Help**: Run `make help` for all deployment options
|
||||
|
||||
### Available Make Commands
|
||||
By default, myclaude installs to `~/.claude`. You can customize this using the `INSTALL_DIR` environment variable:
|
||||
|
||||
```bash
|
||||
make install # Install everything to Claude Code
|
||||
make deploy-bmad # Deploy BMAD workflow only
|
||||
make deploy-requirements # Deploy Requirements workflow only
|
||||
make deploy-commands # Deploy all slash commands
|
||||
make deploy-agents # Deploy all agent configurations
|
||||
make test-bmad # Test BMAD workflow sample
|
||||
make test-requirements # Test Requirements workflow sample
|
||||
make clean # Clean generated artifacts
|
||||
make help # Show all available commands
|
||||
# Install to custom directory
|
||||
INSTALL_DIR=/opt/myclaude bash install.sh
|
||||
|
||||
# Update your PATH accordingly
|
||||
export PATH="/opt/myclaude/bin:$PATH"
|
||||
```
|
||||
|
||||
**Directory Structure:**
|
||||
- `$INSTALL_DIR/bin/` - codeagent-wrapper binary
|
||||
- `$INSTALL_DIR/skills/` - Skill definitions
|
||||
- `$INSTALL_DIR/config.json` - Configuration file
|
||||
- `$INSTALL_DIR/commands/` - Slash command definitions
|
||||
- `$INSTALL_DIR/agents/` - Agent definitions
|
||||
|
||||
**Note:** When using a custom installation directory, ensure that `$INSTALL_DIR/bin` is added to your `PATH` environment variable.
|
||||
|
||||
### Configuration
|
||||
|
||||
Edit `config.json` to customize:
|
||||
|
||||
```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 (use PowerShell method above instead)
|
||||
REM WARNING: This expands %PATH% which includes system PATH, causing duplication
|
||||
REM Note: Using reg add instead of setx to avoid 1024-character truncation limit
|
||||
reg add "HKCU\Environment" /v Path /t REG_EXPAND_SZ /d "%USERPROFILE%\bin;%PATH%" /f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Transform your development with BMAD** - One command, complete agile workflow, quality assured.
|
||||
## Workflow Selection Guide
|
||||
|
||||
*Let specialized AI agents handle specialized work.*
|
||||
| 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
|
||||
# Installer auto-adds PATH, check if configured
|
||||
if [[ ":$PATH:" != *":$HOME/.claude/bin:"* ]]; then
|
||||
echo "PATH not configured. Reinstalling..."
|
||||
bash install.sh
|
||||
fi
|
||||
|
||||
# Or manually add (idempotent command)
|
||||
[[ ":$PATH:" != *":$HOME/.claude/bin:"* ]] && echo 'export PATH="$HOME/.claude/bin:$PATH"' >> ~/.zshrc
|
||||
```
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
### Version Compatibility Issues
|
||||
|
||||
**Backend CLI not found:**
|
||||
```bash
|
||||
# Check if backend CLIs are installed
|
||||
which codex
|
||||
which claude
|
||||
which gemini
|
||||
|
||||
# Install missing backends
|
||||
# Codex: Follow installation instructions at https://codex.docs
|
||||
# Claude: Follow installation instructions at https://claude.ai/docs
|
||||
# Gemini: Follow installation instructions at https://ai.google.dev/docs
|
||||
```
|
||||
|
||||
**Unsupported CLI flags:**
|
||||
```bash
|
||||
# If you see errors like "unknown flag" or "invalid option"
|
||||
|
||||
# Check backend CLI version
|
||||
codex --version
|
||||
claude --version
|
||||
gemini --version
|
||||
|
||||
# For Codex: Ensure it supports `e`, `--skip-git-repo-check`, `--json`, `-C`, and `resume`
|
||||
# For Claude: Ensure it supports `--output-format stream-json`, `--setting-sources`, `-r`
|
||||
# For Gemini: Ensure it supports `-o stream-json`, `-y`, `-r`, `-p`
|
||||
|
||||
# Update your backend CLI to the latest version if needed
|
||||
```
|
||||
|
||||
**JSON parsing errors:**
|
||||
```bash
|
||||
# If you see "failed to parse JSON output" errors
|
||||
|
||||
# Verify the backend outputs stream-json format
|
||||
codex e --json "test task" # Should output newline-delimited JSON
|
||||
claude --output-format stream-json -p "test" # Should output stream JSON
|
||||
|
||||
# If not, your backend CLI version may be too old or incompatible
|
||||
```
|
||||
|
||||
**Infinite recursion with Claude backend:**
|
||||
```bash
|
||||
# The wrapper prevents this with `--setting-sources ""` flag
|
||||
# If you still see recursion, ensure your Claude CLI supports this flag
|
||||
|
||||
claude --help | grep "setting-sources"
|
||||
|
||||
# If flag is not supported, upgrade Claude CLI
|
||||
```
|
||||
|
||||
**Session resume failures:**
|
||||
```bash
|
||||
# Check if session ID is valid
|
||||
codex history # List recent sessions
|
||||
claude history
|
||||
|
||||
# Ensure backend CLI supports session resumption
|
||||
codex resume <session_id> "test" # Should continue from previous session
|
||||
claude -r <session_id> "test"
|
||||
|
||||
# If not supported, use new sessions instead of resume mode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ (Frequently Asked Questions)
|
||||
|
||||
### Q1: `codeagent-wrapper` execution fails with "Unknown event format"
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
Unknown event format: {"type":"turn.started"}
|
||||
Unknown event format: {"type":"assistant", ...}
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
This is a logging event format display issue and does not affect actual functionality. It will be fixed in the next version. You can ignore these log outputs.
|
||||
|
||||
**Related Issue:** [#96](https://github.com/cexll/myclaude/issues/96)
|
||||
|
||||
---
|
||||
|
||||
### Q2: Gemini cannot read files ignored by `.gitignore`
|
||||
|
||||
**Problem:**
|
||||
When using `codeagent-wrapper --backend gemini`, files in directories like `.claude/` that are ignored by `.gitignore` cannot be read.
|
||||
|
||||
**Solution:**
|
||||
- **Option 1:** Remove `.claude/` from your `.gitignore` file
|
||||
- **Option 2:** Ensure files that need to be read are not in `.gitignore` list
|
||||
|
||||
**Related Issue:** [#75](https://github.com/cexll/myclaude/issues/75)
|
||||
|
||||
---
|
||||
|
||||
### Q3: `/dev` command parallel execution is very slow
|
||||
|
||||
**Problem:**
|
||||
Using `/dev` command for simple features takes too long (over 30 minutes) with no visibility into task progress.
|
||||
|
||||
**Solution:**
|
||||
1. **Check logs:** Review `C:\Users\User\AppData\Local\Temp\codeagent-wrapper-*.log` to identify bottlenecks
|
||||
2. **Adjust backend:**
|
||||
- Try faster models like `gpt-5.1-codex-max`
|
||||
- Running in WSL may be significantly faster
|
||||
3. **Workspace:** Use a single repository instead of monorepo with multiple sub-projects
|
||||
|
||||
**Related Issue:** [#77](https://github.com/cexll/myclaude/issues/77)
|
||||
|
||||
---
|
||||
|
||||
### Q4: Codex permission denied with new Go version
|
||||
|
||||
**Problem:**
|
||||
After upgrading to the new Go-based Codex implementation, execution fails with permission denied errors.
|
||||
|
||||
**Solution:**
|
||||
Add the following configuration to `~/.codex/config.yaml` (Windows: `c:\user\.codex\config.toml`):
|
||||
```yaml
|
||||
model = "gpt-5.1-codex-max"
|
||||
model_reasoning_effort = "high"
|
||||
model_reasoning_summary = "detailed"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "workspace-write"
|
||||
disable_response_storage = true
|
||||
network_access = true
|
||||
```
|
||||
|
||||
**Key settings:**
|
||||
- `approval_policy = "never"` - Remove approval restrictions
|
||||
- `sandbox_mode = "workspace-write"` - Allow workspace write access
|
||||
- `network_access = true` - Enable network access
|
||||
|
||||
**Related Issue:** [#31](https://github.com/cexll/myclaude/issues/31)
|
||||
|
||||
---
|
||||
|
||||
### Q5: How to disable default bypass/skip-permissions mode
|
||||
|
||||
**Background:**
|
||||
By default, codeagent-wrapper enables bypass mode for both Codex and Claude backends:
|
||||
- `CODEX_BYPASS_SANDBOX=true` - Bypasses Codex sandbox restrictions
|
||||
- `CODEAGENT_SKIP_PERMISSIONS=true` - Skips Claude permission prompts
|
||||
|
||||
**To disable (if you need sandbox/permission protection):**
|
||||
```bash
|
||||
export CODEX_BYPASS_SANDBOX=false
|
||||
export CODEAGENT_SKIP_PERMISSIONS=false
|
||||
```
|
||||
|
||||
Or add to your shell profile (`~/.zshrc` or `~/.bashrc`):
|
||||
```bash
|
||||
echo 'export CODEX_BYPASS_SANDBOX=false' >> ~/.zshrc
|
||||
echo 'export CODEAGENT_SKIP_PERMISSIONS=false' >> ~/.zshrc
|
||||
```
|
||||
|
||||
**Note:** Disabling bypass mode will require manual approval for certain operations.
|
||||
|
||||
---
|
||||
|
||||
**Still having issues?** Visit [GitHub Issues](https://github.com/cexll/myclaude/issues) to search or report new issues.
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
- **[Codeagent-Wrapper Guide](docs/CODEAGENT-WRAPPER.md)** - Multi-backend execution wrapper
|
||||
- **[Hooks Documentation](docs/HOOKS.md)** - Custom hooks and automation
|
||||
|
||||
### 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.
|
||||
|
||||
483
README_CN.md
Normal file
483
README_CN.md
Normal file
@@ -0,0 +1,483 @@
|
||||
# Claude Code 多智能体工作流系统
|
||||
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
[](https://claude.ai/code)
|
||||
[](https://github.com/cexll/myclaude)
|
||||
|
||||
> AI 驱动的开发自动化 - 多后端执行架构 (Codex/Claude/Gemini)
|
||||
|
||||
## 核心概念:多后端架构
|
||||
|
||||
本系统采用**双智能体架构**与可插拔 AI 后端:
|
||||
|
||||
| 角色 | 智能体 | 职责 |
|
||||
|------|-------|------|
|
||||
| **编排者** | Claude Code | 规划、上下文收集、验证、用户交互 |
|
||||
| **执行者** | codeagent-wrapper | 代码编辑、测试执行(Codex/Claude/Gemini 后端)|
|
||||
|
||||
**为什么分离?**
|
||||
- Claude Code 擅长理解上下文和编排复杂工作流
|
||||
- 专业后端(Codex 擅长代码、Claude 擅长推理、Gemini 擅长原型)专注执行
|
||||
- 通过 `--backend codex|claude|gemini` 匹配模型与任务
|
||||
|
||||
## 快速开始(windows上请在Powershell中执行)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/cexll/myclaude.git
|
||||
cd myclaude
|
||||
python3 install.py --install-dir ~/.claude
|
||||
```
|
||||
|
||||
## 工作流概览
|
||||
|
||||
### 0. OmO 多智能体编排器(复杂任务推荐)
|
||||
|
||||
**基于风险信号智能路由任务到专业智能体的多智能体编排系统。**
|
||||
|
||||
```bash
|
||||
/omo "分析并修复这个认证 bug"
|
||||
```
|
||||
|
||||
**智能体层级:**
|
||||
| 智能体 | 角色 | 后端 | 模型 |
|
||||
|-------|------|------|------|
|
||||
| `oracle` | 技术顾问 | Claude | claude-opus-4-5 |
|
||||
| `librarian` | 外部研究 | Claude | claude-sonnet-4-5 |
|
||||
| `explore` | 代码库搜索 | OpenCode | grok-code |
|
||||
| `develop` | 代码实现 | Codex | gpt-5.2 |
|
||||
| `frontend-ui-ux-engineer` | UI/UX 专家 | Gemini | gemini-3-pro |
|
||||
| `document-writer` | 文档撰写 | Gemini | gemini-3-flash |
|
||||
|
||||
**路由信号(非固定流水线):**
|
||||
- 代码位置不明确 → `explore`
|
||||
- 外部库/API → `librarian`
|
||||
- 高风险/多文件变更 → `oracle`
|
||||
- 需要实现 → `develop` / `frontend-ui-ux-engineer`
|
||||
|
||||
**常用配方:**
|
||||
- 解释代码:`explore`
|
||||
- 位置已知的小修复:直接 `develop`
|
||||
- Bug 修复,位置未知:`explore → develop`
|
||||
- 跨模块重构:`explore → oracle → develop`
|
||||
- 外部 API 集成:`explore + librarian → oracle → develop`
|
||||
|
||||
**适用场景:** 复杂 bug 调查、多文件重构、架构决策
|
||||
|
||||
---
|
||||
|
||||
### 1. Dev 工作流(推荐)
|
||||
|
||||
**大多数开发任务的首选工作流。**
|
||||
|
||||
```bash
|
||||
/dev "实现 JWT 用户认证"
|
||||
```
|
||||
|
||||
**6 步流程:**
|
||||
1. **需求澄清** - 交互式问答明确范围
|
||||
2. **Codex 深度分析** - 代码库探索和架构决策
|
||||
3. **开发计划生成** - 结构化任务分解和测试要求
|
||||
4. **并行执行** - Codex 并发执行任务
|
||||
5. **覆盖率验证** - 强制 ≥90% 测试覆盖率
|
||||
6. **完成总结** - 文件变更和覆盖率报告
|
||||
|
||||
**核心特性:**
|
||||
- Claude Code 编排,Codex 执行所有代码变更
|
||||
- 自动任务并行化提升速度
|
||||
- 强制 90% 测试覆盖率门禁
|
||||
- 失败自动回滚
|
||||
|
||||
**适用场景:** 功能开发、重构、带测试的 bug 修复
|
||||
|
||||
---
|
||||
|
||||
### 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/
|
||||
├── bin/
|
||||
│ └── codeagent-wrapper # 主可执行文件
|
||||
├── CLAUDE.md # 核心指令和角色定义
|
||||
├── commands/ # 斜杠命令 (/dev, /code 等)
|
||||
├── agents/ # 智能体定义
|
||||
├── skills/
|
||||
│ └── codex/
|
||||
│ └── SKILL.md # Codex 集成技能
|
||||
├── config.json # 配置文件
|
||||
└── installed_modules.json # 安装状态
|
||||
```
|
||||
|
||||
### 自定义安装目录
|
||||
|
||||
默认情况下,myclaude 安装到 `~/.claude`。您可以使用 `INSTALL_DIR` 环境变量自定义安装目录:
|
||||
|
||||
```bash
|
||||
# 安装到自定义目录
|
||||
INSTALL_DIR=/opt/myclaude bash install.sh
|
||||
|
||||
# 相应更新您的 PATH
|
||||
export PATH="/opt/myclaude/bin:$PATH"
|
||||
```
|
||||
|
||||
**目录结构:**
|
||||
- `$INSTALL_DIR/bin/` - codeagent-wrapper 可执行文件
|
||||
- `$INSTALL_DIR/skills/` - 技能定义
|
||||
- `$INSTALL_DIR/config.json` - 配置文件
|
||||
- `$INSTALL_DIR/commands/` - 斜杠命令定义
|
||||
- `$INSTALL_DIR/agents/` - 智能体定义
|
||||
|
||||
**注意:** 使用自定义安装目录时,请确保将 `$INSTALL_DIR/bin` 添加到您的 `PATH` 环境变量中。
|
||||
|
||||
### 配置
|
||||
|
||||
编辑 `config.json` 自定义:
|
||||
|
||||
```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 - 永久添加(当前用户)(建议使用上面的 PowerShell 方法)
|
||||
REM 警告:此命令会展开 %PATH% 包含系统 PATH,导致重复
|
||||
REM 注意:使用 reg add 而非 setx 以避免 1024 字符截断限制
|
||||
reg add "HKCU\Environment" /v Path /t REG_EXPAND_SZ /d "%USERPROFILE%\bin;%PATH%" /f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工作流选择指南
|
||||
|
||||
| 场景 | 推荐工作流 |
|
||||
|------|----------|
|
||||
| 带测试的新功能 | `/dev` |
|
||||
| 快速 bug 修复 | `/debug` 或 `/code` |
|
||||
| 大型多 Sprint 功能 | `/bmad-pilot` |
|
||||
| 原型或 POC | `/requirements-pilot` |
|
||||
| 代码审查 | `/review` |
|
||||
| 性能问题 | `/optimize` |
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
**Codex wrapper 未找到:**
|
||||
```bash
|
||||
# 安装程序会自动添加 PATH,检查是否已添加
|
||||
if [[ ":$PATH:" != *":$HOME/.claude/bin:"* ]]; then
|
||||
echo "PATH not configured. Reinstalling..."
|
||||
bash install.sh
|
||||
fi
|
||||
|
||||
# 或手动添加(幂等性命令)
|
||||
[[ ":$PATH:" != *":$HOME/.claude/bin:"* ]] && echo 'export PATH="$HOME/.claude/bin:$PATH"' >> ~/.zshrc
|
||||
```
|
||||
|
||||
**权限被拒绝:**
|
||||
```bash
|
||||
python3 install.py --install-dir ~/.claude --force
|
||||
```
|
||||
|
||||
**模块未加载:**
|
||||
```bash
|
||||
# 检查安装状态
|
||||
cat ~/.claude/installed_modules.json
|
||||
|
||||
# 重新安装特定模块
|
||||
python3 install.py --module dev --force
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题 (FAQ)
|
||||
|
||||
### Q1: `codeagent-wrapper` 执行时报错 "Unknown event format"
|
||||
|
||||
**问题描述:**
|
||||
执行 `codeagent-wrapper` 时出现错误:
|
||||
```
|
||||
Unknown event format: {"type":"turn.started"}
|
||||
Unknown event format: {"type":"assistant", ...}
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
这是日志事件流的显示问题,不影响实际功能执行。预计在下个版本中修复。如需排查其他问题,可忽略此日志输出。
|
||||
|
||||
**相关 Issue:** [#96](https://github.com/cexll/myclaude/issues/96)
|
||||
|
||||
---
|
||||
|
||||
### Q2: Gemini 无法读取 `.gitignore` 忽略的文件
|
||||
|
||||
**问题描述:**
|
||||
使用 `codeagent-wrapper --backend gemini` 时,无法读取 `.claude/` 等被 `.gitignore` 忽略的目录中的文件。
|
||||
|
||||
**解决方案:**
|
||||
- **方案一:** 在项目根目录的 `.gitignore` 中取消对 `.claude/` 的忽略
|
||||
- **方案二:** 确保需要读取的文件不在 `.gitignore` 忽略列表中
|
||||
|
||||
**相关 Issue:** [#75](https://github.com/cexll/myclaude/issues/75)
|
||||
|
||||
---
|
||||
|
||||
### Q3: `/dev` 命令并行执行特别慢
|
||||
|
||||
**问题描述:**
|
||||
使用 `/dev` 命令开发简单功能耗时过长(超过30分钟),无法了解任务执行状态。
|
||||
|
||||
**解决方案:**
|
||||
1. **检查日志:** 查看 `C:\Users\User\AppData\Local\Temp\codeagent-wrapper-*.log` 分析瓶颈
|
||||
2. **调整后端:**
|
||||
- 尝试使用 `gpt-5.1-codex-max` 等更快的模型
|
||||
- 在 WSL 环境下运行速度可能更快
|
||||
3. **工作区选择:** 使用独立的代码仓库而非包含多个子项目的 monorepo
|
||||
|
||||
**相关 Issue:** [#77](https://github.com/cexll/myclaude/issues/77)
|
||||
|
||||
---
|
||||
|
||||
### Q4: 新版 Go 实现的 Codex 权限不足
|
||||
|
||||
**问题描述:**
|
||||
升级到新版 Go 实现的 Codex 后,出现权限不足的错误。
|
||||
|
||||
**解决方案:**
|
||||
在 `~/.codex/config.yaml` 中添加以下配置(Windows: `c:\user\.codex\config.toml`):
|
||||
```yaml
|
||||
model = "gpt-5.1-codex-max"
|
||||
model_reasoning_effort = "high"
|
||||
model_reasoning_summary = "detailed"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "workspace-write"
|
||||
disable_response_storage = true
|
||||
network_access = true
|
||||
```
|
||||
|
||||
**关键配置说明:**
|
||||
- `approval_policy = "never"` - 移除审批限制
|
||||
- `sandbox_mode = "workspace-write"` - 允许工作区写入权限
|
||||
- `network_access = true` - 启用网络访问
|
||||
|
||||
**相关 Issue:** [#31](https://github.com/cexll/myclaude/issues/31)
|
||||
|
||||
---
|
||||
|
||||
### Q5: 执行时遇到权限拒绝或沙箱限制
|
||||
|
||||
**问题描述:**
|
||||
运行 codeagent-wrapper 时出现权限错误或沙箱限制。
|
||||
|
||||
**解决方案:**
|
||||
设置以下环境变量:
|
||||
```bash
|
||||
export CODEX_BYPASS_SANDBOX=true
|
||||
export CODEAGENT_SKIP_PERMISSIONS=true
|
||||
```
|
||||
|
||||
或添加到 shell 配置文件(`~/.zshrc` 或 `~/.bashrc`):
|
||||
```bash
|
||||
echo 'export CODEX_BYPASS_SANDBOX=true' >> ~/.zshrc
|
||||
echo 'export CODEAGENT_SKIP_PERMISSIONS=true' >> ~/.zshrc
|
||||
```
|
||||
|
||||
**注意:** 这些设置会绕过安全限制,请仅在可信环境中使用。
|
||||
|
||||
---
|
||||
|
||||
**仍有疑问?** 请访问 [GitHub Issues](https://github.com/cexll/myclaude/issues) 搜索或提交新问题。
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
AGPL-3.0 License - 查看 [LICENSE](LICENSE)
|
||||
|
||||
## 支持
|
||||
|
||||
- **问题反馈**: [GitHub Issues](https://github.com/cexll/myclaude/issues)
|
||||
- **文档**: [docs/](docs/)
|
||||
|
||||
---
|
||||
|
||||
**Claude Code + Codex = 更好的开发** - 编排遇见执行。
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
name: bmad-review
|
||||
description: Independent code review agent
|
||||
---
|
||||
|
||||
# BMAD Review Agent
|
||||
|
||||
You are an independent code review agent responsible for conducting reviews between Dev and QA phases.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Load Context**
|
||||
- Read PRD from `./.claude/specs/{feature_name}/01-product-requirements.md`
|
||||
- Read Architecture from `./.claude/specs/{feature_name}/02-system-architecture.md`
|
||||
- Read Sprint Plan from `./.claude/specs/{feature_name}/03-sprint-plan.md`
|
||||
- Analyze the code changes and implementation
|
||||
|
||||
2. **Execute Review**
|
||||
Use Bash to call codex with an optimized prompt:
|
||||
```bash
|
||||
codex exec --skip-git-repo-check -m gpt-5 "[Your optimized review prompt here]"
|
||||
```
|
||||
|
||||
When constructing the prompt, follow these principles:
|
||||
- Use structured XML tags for organization
|
||||
- Include clear role definition
|
||||
- Add thinking sections for analysis
|
||||
- Specify detailed output format
|
||||
- Include QA testing guidance
|
||||
|
||||
3. **Generate Report**
|
||||
Write the review results to `./.claude/specs/{feature_name}/04-dev-reviewed.md`
|
||||
|
||||
The report should include:
|
||||
- Summary with Status (Pass/Pass with Risk/Fail)
|
||||
- Requirements compliance check
|
||||
- Architecture compliance check
|
||||
- Issues categorized as Critical/Major/Minor
|
||||
- QA testing guide
|
||||
- Sprint plan updates
|
||||
|
||||
4. **Update Status**
|
||||
Based on the review status:
|
||||
- If Pass or Pass with Risk: Mark review as completed in sprint plan
|
||||
- If Fail: Keep as pending and indicate Dev needs to address issues
|
||||
|
||||
## Key Principles
|
||||
- Maintain independence from Dev context
|
||||
- Focus on actionable findings
|
||||
- Provide specific QA guidance
|
||||
- Use clear, parseable output format
|
||||
@@ -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.
|
||||
9
bmad-agile-workflow/.claude-plugin/plugin.json
Normal file
9
bmad-agile-workflow/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "bmad",
|
||||
"description": "Full BMAD agile workflow with role-based agents (PO, Architect, SM, Dev, QA) and interactive approval gates",
|
||||
"version": "5.6.1",
|
||||
"author": {
|
||||
"name": "cexll",
|
||||
"email": "cexll@cexll.com"
|
||||
}
|
||||
}
|
||||
@@ -427,6 +427,10 @@ Generate architecture document at `./.claude/specs/{feature_name}/02-system-arch
|
||||
|
||||
## Important Behaviors
|
||||
|
||||
### Language Rules:
|
||||
- **Language Matching**: Output language matches user input (Chinese input → Chinese doc, English input → English doc). When language is ambiguous, default to Chinese.
|
||||
- **Technical Terms**: Keep technical terms (API, REST, GraphQL, JWT, RBAC, etc.) in English; translate explanatory text only.
|
||||
|
||||
### DO:
|
||||
- Start by reviewing and referencing the PRD
|
||||
- Present initial architecture based on requirements
|
||||
@@ -419,6 +419,10 @@ logger.info('User created', {
|
||||
|
||||
## Important Implementation Rules
|
||||
|
||||
### Language Rules:
|
||||
- **Language Matching**: Output language matches user input (Chinese input → Chinese doc, English input → English doc). When language is ambiguous, default to Chinese.
|
||||
- **Technical Terms**: Keep technical terms (API, CRUD, JWT, SQL, etc.) in English; translate explanatory text only.
|
||||
|
||||
### DO:
|
||||
- Follow architecture specifications exactly
|
||||
- Implement all acceptance criteria from PRD
|
||||
@@ -22,6 +22,10 @@ You are the BMAD Orchestrator. Your core focus is repository analysis, workflow
|
||||
- Consistency: ensure conventions and patterns discovered in scan are preserved downstream
|
||||
- Explicit handoffs: clearly document assumptions, risks, and integration points for other agents
|
||||
|
||||
### Language Rules:
|
||||
- **Language Matching**: Output language matches user input (Chinese input → Chinese doc, English input → English doc). When language is ambiguous, default to Chinese.
|
||||
- **Technical Terms**: Keep technical terms (API, PRD, Sprint, etc.) in English; translate explanatory text only.
|
||||
|
||||
## UltraThink Repository Scan
|
||||
|
||||
When asked to analyze the repository, follow this structure and return a clear, actionable summary.
|
||||
@@ -313,6 +313,10 @@ Generate PRD at `./.claude/specs/{feature_name}/01-product-requirements.md`:
|
||||
|
||||
## Important Behaviors
|
||||
|
||||
### Language Rules:
|
||||
- **Language Matching**: Output language matches user input (Chinese input → Chinese doc, English input → English doc). When language is ambiguous, default to Chinese.
|
||||
- **Technical Terms**: Keep technical terms (API, Sprint, PRD, KPI, MVP, etc.) in English; translate explanatory text only.
|
||||
|
||||
### DO:
|
||||
- Start immediately with greeting and initial understanding
|
||||
- Show quality scores transparently
|
||||
@@ -478,6 +478,10 @@ module.exports = {
|
||||
|
||||
## Important Testing Rules
|
||||
|
||||
### Language Rules:
|
||||
- **Language Matching**: Output language matches user input (Chinese input → Chinese doc, English input → English doc). When language is ambiguous, default to Chinese.
|
||||
- **Technical Terms**: Keep technical terms (API, E2E, CI/CD, Mock, etc.) in English; translate explanatory text only.
|
||||
|
||||
### DO:
|
||||
- Test all acceptance criteria from PRD
|
||||
- Cover happy path, edge cases, and error scenarios
|
||||
@@ -45,3 +45,7 @@ You are an independent code review agent responsible for conducting reviews betw
|
||||
- Focus on actionable findings
|
||||
- Provide specific QA guidance
|
||||
- Use clear, parseable output format
|
||||
|
||||
### Language Rules:
|
||||
- **Language Matching**: Output language matches user input (Chinese input → Chinese doc, English input → English doc). When language is ambiguous, default to Chinese.
|
||||
- **Technical Terms**: Keep technical terms (API, PRD, Sprint, etc.) in English; translate explanatory text only.
|
||||
@@ -351,6 +351,10 @@ So that [benefit]
|
||||
|
||||
## Important Behaviors
|
||||
|
||||
### Language Rules:
|
||||
- **Language Matching**: Output language matches user input (Chinese input → Chinese doc, English input → English doc). When language is ambiguous, default to Chinese.
|
||||
- **Technical Terms**: Keep technical terms (Sprint, Epic, Story, Backlog, Velocity, etc.) in English; translate explanatory text only.
|
||||
|
||||
### DO:
|
||||
- Read both PRD and Architecture documents thoroughly
|
||||
- Create comprehensive task breakdown
|
||||
72
cliff.toml
Normal file
72
cliff.toml
Normal file
@@ -0,0 +1,72 @@
|
||||
# git-cliff configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
"""
|
||||
# template for the changelog body
|
||||
body = """
|
||||
{% if version %}
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}
|
||||
## Unreleased
|
||||
{% endif %}
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group }}
|
||||
|
||||
{% for commit in commits %}
|
||||
- {{ commit.message | split(pat="\n") | first }}
|
||||
{% endfor -%}
|
||||
{% endfor -%}
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = false
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/cexll/myclaude/issues/${2}))" },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "🚀 Features" },
|
||||
{ message = "^fix", group = "🐛 Bug Fixes" },
|
||||
{ message = "^doc", group = "📚 Documentation" },
|
||||
{ message = "^perf", group = "⚡ Performance" },
|
||||
{ message = "^refactor", group = "🚜 Refactor" },
|
||||
{ message = "^style", group = "🎨 Styling" },
|
||||
{ message = "^test", group = "🧪 Testing" },
|
||||
{ message = "^chore\\(release\\):", skip = true },
|
||||
{ message = "^chore", group = "⚙️ Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "🛡️ Security" },
|
||||
{ message = "^revert", group = "◀️ Revert" },
|
||||
{ message = ".*", group = "💼 Other" },
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-beta.1"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "newest"
|
||||
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
|
||||
79
codeagent-wrapper/agent_config.go
Normal file
79
codeagent-wrapper/agent_config.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type AgentModelConfig struct {
|
||||
Backend string `json:"backend"`
|
||||
Model string `json:"model"`
|
||||
PromptFile string `json:"prompt_file,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Yolo bool `json:"yolo,omitempty"`
|
||||
Reasoning string `json:"reasoning,omitempty"`
|
||||
}
|
||||
|
||||
type ModelsConfig struct {
|
||||
DefaultBackend string `json:"default_backend"`
|
||||
DefaultModel string `json:"default_model"`
|
||||
Agents map[string]AgentModelConfig `json:"agents"`
|
||||
}
|
||||
|
||||
var defaultModelsConfig = ModelsConfig{
|
||||
DefaultBackend: "opencode",
|
||||
DefaultModel: "opencode/grok-code",
|
||||
Agents: map[string]AgentModelConfig{
|
||||
"oracle": {Backend: "claude", Model: "claude-opus-4-5-20251101", PromptFile: "~/.claude/skills/omo/references/oracle.md", Description: "Technical advisor"},
|
||||
"librarian": {Backend: "claude", Model: "claude-sonnet-4-5-20250929", PromptFile: "~/.claude/skills/omo/references/librarian.md", Description: "Researcher"},
|
||||
"explore": {Backend: "opencode", Model: "opencode/grok-code", PromptFile: "~/.claude/skills/omo/references/explore.md", Description: "Code search"},
|
||||
"develop": {Backend: "codex", Model: "", PromptFile: "~/.claude/skills/omo/references/develop.md", Description: "Code development"},
|
||||
"frontend-ui-ux-engineer": {Backend: "gemini", Model: "", PromptFile: "~/.claude/skills/omo/references/frontend-ui-ux-engineer.md", Description: "Frontend engineer"},
|
||||
"document-writer": {Backend: "gemini", Model: "", PromptFile: "~/.claude/skills/omo/references/document-writer.md", Description: "Documentation"},
|
||||
},
|
||||
}
|
||||
|
||||
func loadModelsConfig() *ModelsConfig {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
logWarn(fmt.Sprintf("Failed to resolve home directory for models config: %v; using defaults", err))
|
||||
return &defaultModelsConfig
|
||||
}
|
||||
|
||||
configPath := filepath.Join(home, ".codeagent", "models.json")
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
logWarn(fmt.Sprintf("Failed to read models config %s: %v; using defaults", configPath, err))
|
||||
}
|
||||
return &defaultModelsConfig
|
||||
}
|
||||
|
||||
var cfg ModelsConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
logWarn(fmt.Sprintf("Failed to parse models config %s: %v; using defaults", configPath, err))
|
||||
return &defaultModelsConfig
|
||||
}
|
||||
|
||||
// Merge with defaults
|
||||
for name, agent := range defaultModelsConfig.Agents {
|
||||
if _, exists := cfg.Agents[name]; !exists {
|
||||
if cfg.Agents == nil {
|
||||
cfg.Agents = make(map[string]AgentModelConfig)
|
||||
}
|
||||
cfg.Agents[name] = agent
|
||||
}
|
||||
}
|
||||
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func resolveAgentConfig(agentName string) (backend, model, promptFile, reasoning string, yolo bool) {
|
||||
cfg := loadModelsConfig()
|
||||
if agent, ok := cfg.Agents[agentName]; ok {
|
||||
return agent.Backend, agent.Model, agent.PromptFile, agent.Reasoning, agent.Yolo
|
||||
}
|
||||
return cfg.DefaultBackend, cfg.DefaultModel, "", "", false
|
||||
}
|
||||
217
codeagent-wrapper/agent_config_test.go
Normal file
217
codeagent-wrapper/agent_config_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveAgentConfig_Defaults(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
// Test that default agents resolve correctly without config file
|
||||
tests := []struct {
|
||||
agent string
|
||||
wantBackend string
|
||||
wantModel string
|
||||
wantPromptFile string
|
||||
}{
|
||||
{"oracle", "claude", "claude-opus-4-5-20251101", "~/.claude/skills/omo/references/oracle.md"},
|
||||
{"librarian", "claude", "claude-sonnet-4-5-20250929", "~/.claude/skills/omo/references/librarian.md"},
|
||||
{"explore", "opencode", "opencode/grok-code", "~/.claude/skills/omo/references/explore.md"},
|
||||
{"frontend-ui-ux-engineer", "gemini", "", "~/.claude/skills/omo/references/frontend-ui-ux-engineer.md"},
|
||||
{"document-writer", "gemini", "", "~/.claude/skills/omo/references/document-writer.md"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.agent, func(t *testing.T) {
|
||||
backend, model, promptFile, _, _ := resolveAgentConfig(tt.agent)
|
||||
if backend != tt.wantBackend {
|
||||
t.Errorf("backend = %q, want %q", backend, tt.wantBackend)
|
||||
}
|
||||
if model != tt.wantModel {
|
||||
t.Errorf("model = %q, want %q", model, tt.wantModel)
|
||||
}
|
||||
if promptFile != tt.wantPromptFile {
|
||||
t.Errorf("promptFile = %q, want %q", promptFile, tt.wantPromptFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveAgentConfig_UnknownAgent(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
backend, model, promptFile, _, _ := resolveAgentConfig("unknown-agent")
|
||||
if backend != "opencode" {
|
||||
t.Errorf("unknown agent backend = %q, want %q", backend, "opencode")
|
||||
}
|
||||
if model != "opencode/grok-code" {
|
||||
t.Errorf("unknown agent model = %q, want %q", model, "opencode/grok-code")
|
||||
}
|
||||
if promptFile != "" {
|
||||
t.Errorf("unknown agent promptFile = %q, want empty", promptFile)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadModelsConfig_NoFile(t *testing.T) {
|
||||
home := "/nonexistent/path/that/does/not/exist"
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
cfg := loadModelsConfig()
|
||||
if cfg.DefaultBackend != "opencode" {
|
||||
t.Errorf("DefaultBackend = %q, want %q", cfg.DefaultBackend, "opencode")
|
||||
}
|
||||
if len(cfg.Agents) != 6 {
|
||||
t.Errorf("len(Agents) = %d, want 6", len(cfg.Agents))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadModelsConfig_WithFile(t *testing.T) {
|
||||
// Create temp dir and config file
|
||||
tmpDir := t.TempDir()
|
||||
configDir := filepath.Join(tmpDir, ".codeagent")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
configContent := `{
|
||||
"default_backend": "claude",
|
||||
"default_model": "claude-opus-4",
|
||||
"agents": {
|
||||
"custom-agent": {
|
||||
"backend": "codex",
|
||||
"model": "gpt-4o",
|
||||
"description": "Custom agent"
|
||||
}
|
||||
}
|
||||
}`
|
||||
configPath := filepath.Join(configDir, "models.json")
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Setenv("HOME", tmpDir)
|
||||
t.Setenv("USERPROFILE", tmpDir)
|
||||
|
||||
cfg := loadModelsConfig()
|
||||
|
||||
if cfg.DefaultBackend != "claude" {
|
||||
t.Errorf("DefaultBackend = %q, want %q", cfg.DefaultBackend, "claude")
|
||||
}
|
||||
if cfg.DefaultModel != "claude-opus-4" {
|
||||
t.Errorf("DefaultModel = %q, want %q", cfg.DefaultModel, "claude-opus-4")
|
||||
}
|
||||
|
||||
// Check custom agent
|
||||
if agent, ok := cfg.Agents["custom-agent"]; !ok {
|
||||
t.Error("custom-agent not found")
|
||||
} else {
|
||||
if agent.Backend != "codex" {
|
||||
t.Errorf("custom-agent.Backend = %q, want %q", agent.Backend, "codex")
|
||||
}
|
||||
if agent.Model != "gpt-4o" {
|
||||
t.Errorf("custom-agent.Model = %q, want %q", agent.Model, "gpt-4o")
|
||||
}
|
||||
}
|
||||
|
||||
// Check that defaults are merged
|
||||
if _, ok := cfg.Agents["oracle"]; !ok {
|
||||
t.Error("default agent oracle should be merged")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadModelsConfig_InvalidJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configDir := filepath.Join(tmpDir, ".codeagent")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write invalid JSON
|
||||
configPath := filepath.Join(configDir, "models.json")
|
||||
if err := os.WriteFile(configPath, []byte("invalid json {"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Setenv("HOME", tmpDir)
|
||||
t.Setenv("USERPROFILE", tmpDir)
|
||||
|
||||
cfg := loadModelsConfig()
|
||||
// Should fall back to defaults
|
||||
if cfg.DefaultBackend != "opencode" {
|
||||
t.Errorf("invalid JSON should fallback, got DefaultBackend = %q", cfg.DefaultBackend)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeBackend_BuildArgs(t *testing.T) {
|
||||
backend := OpencodeBackend{}
|
||||
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "new"}
|
||||
got := backend.BuildArgs(cfg, "hello")
|
||||
want := []string{"run", "--format", "json", "hello"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with model", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "new", Model: "opencode/grok-code"}
|
||||
got := backend.BuildArgs(cfg, "task")
|
||||
want := []string{"run", "-m", "opencode/grok-code", "--format", "json", "task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resume mode", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "resume", SessionID: "ses_123", Model: "opencode/grok-code"}
|
||||
got := backend.BuildArgs(cfg, "follow-up")
|
||||
want := []string{"run", "-m", "opencode/grok-code", "-s", "ses_123", "--format", "json", "follow-up"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resume without session", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "resume"}
|
||||
got := backend.BuildArgs(cfg, "task")
|
||||
want := []string{"run", "--format", "json", "task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("stdin mode omits dash", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "new"}
|
||||
got := backend.BuildArgs(cfg, "-")
|
||||
want := []string{"run", "--format", "json"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpencodeBackend_Interface(t *testing.T) {
|
||||
backend := OpencodeBackend{}
|
||||
|
||||
if backend.Name() != "opencode" {
|
||||
t.Errorf("Name() = %q, want %q", backend.Name(), "opencode")
|
||||
}
|
||||
if backend.Command() != "opencode" {
|
||||
t.Errorf("Command() = %q, want %q", backend.Command(), "opencode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendRegistry_IncludesOpencode(t *testing.T) {
|
||||
if _, ok := backendRegistry["opencode"]; !ok {
|
||||
t.Error("backendRegistry should include opencode")
|
||||
}
|
||||
}
|
||||
147
codeagent-wrapper/agent_validation_test.go
Normal file
147
codeagent-wrapper/agent_validation_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestValidateAgentName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "simple", input: "develop", wantErr: false},
|
||||
{name: "upper", input: "ABC", wantErr: false},
|
||||
{name: "digits", input: "a1", wantErr: false},
|
||||
{name: "dash underscore", input: "a-b_c", wantErr: false},
|
||||
{name: "empty", input: "", wantErr: true},
|
||||
{name: "space", input: "a b", wantErr: true},
|
||||
{name: "slash", input: "a/b", wantErr: true},
|
||||
{name: "dotdot", input: "../evil", wantErr: true},
|
||||
{name: "unicode", input: "中文", wantErr: true},
|
||||
{name: "symbol", input: "a$b", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateAgentName(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("validateAgentName(%q) err=%v, wantErr=%v", tt.input, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArgs_InvalidAgentNameRejected(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
os.Args = []string{"codeagent-wrapper", "--agent", "../evil", "task"}
|
||||
if _, err := parseArgs(); err == nil {
|
||||
t.Fatalf("expected parseArgs to reject invalid agent name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseParallelConfig_InvalidAgentNameRejected(t *testing.T) {
|
||||
input := `---TASK---
|
||||
id: task-1
|
||||
agent: ../evil
|
||||
---CONTENT---
|
||||
do something`
|
||||
if _, err := parseParallelConfig([]byte(input)); err == nil {
|
||||
t.Fatalf("expected parseParallelConfig to reject invalid agent name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseParallelConfig_ResolvesAgentPromptFile(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
configDir := filepath.Join(home, ".codeagent")
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(configDir, "models.json"), []byte(`{
|
||||
"default_backend": "codex",
|
||||
"default_model": "gpt-test",
|
||||
"agents": {
|
||||
"custom-agent": {
|
||||
"backend": "codex",
|
||||
"model": "gpt-test",
|
||||
"prompt_file": "~/.claude/prompt.md"
|
||||
}
|
||||
}
|
||||
}`), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
input := `---TASK---
|
||||
id: task-1
|
||||
agent: custom-agent
|
||||
---CONTENT---
|
||||
do something`
|
||||
cfg, err := parseParallelConfig([]byte(input))
|
||||
if err != nil {
|
||||
t.Fatalf("parseParallelConfig() unexpected error: %v", err)
|
||||
}
|
||||
if len(cfg.Tasks) != 1 {
|
||||
t.Fatalf("expected 1 task, got %d", len(cfg.Tasks))
|
||||
}
|
||||
if got := cfg.Tasks[0].PromptFile; got != "~/.claude/prompt.md" {
|
||||
t.Fatalf("PromptFile = %q, want %q", got, "~/.claude/prompt.md")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRunCodexTaskFn_AppliesAgentPromptFile(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
claudeDir := filepath.Join(home, ".claude")
|
||||
if err := os.MkdirAll(claudeDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(claudeDir, "prompt.md"), []byte("P\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
fake := newFakeCmd(fakeCmdConfig{
|
||||
StdoutPlan: []fakeStdoutEvent{
|
||||
{Data: `{"type":"item.completed","item":{"type":"agent_message","text":"ok"}}` + "\n"},
|
||||
},
|
||||
WaitDelay: 2 * time.Millisecond,
|
||||
})
|
||||
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return fake
|
||||
}
|
||||
selectBackendFn = func(name string) (Backend, error) {
|
||||
return testBackend{
|
||||
name: name,
|
||||
command: "fake-cmd",
|
||||
argsFn: func(cfg *Config, targetArg string) []string {
|
||||
return []string{targetArg}
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
res := defaultRunCodexTaskFn(TaskSpec{
|
||||
ID: "t",
|
||||
Task: "do",
|
||||
Backend: "codex",
|
||||
PromptFile: "~/.claude/prompt.md",
|
||||
}, 5)
|
||||
if res.ExitCode != 0 {
|
||||
t.Fatalf("unexpected result: %+v", res)
|
||||
}
|
||||
|
||||
want := "<agent-prompt>\nP\n</agent-prompt>\n\ndo"
|
||||
if got := fake.StdinContents(); got != want {
|
||||
t.Fatalf("stdin mismatch:\n got=%q\nwant=%q", got, want)
|
||||
}
|
||||
}
|
||||
240
codeagent-wrapper/backend.go
Normal file
240
codeagent-wrapper/backend.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
return buildClaudeArgs(cfg, targetArg)
|
||||
}
|
||||
|
||||
const maxClaudeSettingsBytes = 1 << 20 // 1MB
|
||||
|
||||
type minimalClaudeSettings struct {
|
||||
Env map[string]string
|
||||
Model string
|
||||
}
|
||||
|
||||
// loadMinimalClaudeSettings 从 ~/.claude/settings.json 只提取安全的最小子集:
|
||||
// - env: 只接受字符串类型的值
|
||||
// - model: 只接受字符串类型的值
|
||||
// 文件缺失/解析失败/超限都返回空。
|
||||
func loadMinimalClaudeSettings() minimalClaudeSettings {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
return minimalClaudeSettings{}
|
||||
}
|
||||
|
||||
settingPath := filepath.Join(home, ".claude", "settings.json")
|
||||
info, err := os.Stat(settingPath)
|
||||
if err != nil || info.Size() > maxClaudeSettingsBytes {
|
||||
return minimalClaudeSettings{}
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(settingPath)
|
||||
if err != nil {
|
||||
return minimalClaudeSettings{}
|
||||
}
|
||||
|
||||
var cfg struct {
|
||||
Env map[string]any `json:"env"`
|
||||
Model any `json:"model"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return minimalClaudeSettings{}
|
||||
}
|
||||
|
||||
out := minimalClaudeSettings{}
|
||||
|
||||
if model, ok := cfg.Model.(string); ok {
|
||||
out.Model = strings.TrimSpace(model)
|
||||
}
|
||||
|
||||
if len(cfg.Env) == 0 {
|
||||
return out
|
||||
}
|
||||
|
||||
env := make(map[string]string, len(cfg.Env))
|
||||
for k, v := range cfg.Env {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
env[k] = s
|
||||
}
|
||||
if len(env) == 0 {
|
||||
return out
|
||||
}
|
||||
out.Env = env
|
||||
return out
|
||||
}
|
||||
|
||||
// loadMinimalEnvSettings is kept for backwards tests; prefer loadMinimalClaudeSettings.
|
||||
func loadMinimalEnvSettings() map[string]string {
|
||||
settings := loadMinimalClaudeSettings()
|
||||
if len(settings.Env) == 0 {
|
||||
return nil
|
||||
}
|
||||
return settings.Env
|
||||
}
|
||||
|
||||
// loadGeminiEnv loads environment variables from ~/.gemini/.env
|
||||
// Supports GEMINI_API_KEY, GEMINI_MODEL, GOOGLE_GEMINI_BASE_URL
|
||||
// Also sets GEMINI_API_KEY_AUTH_MECHANISM=bearer for third-party API compatibility
|
||||
func loadGeminiEnv() map[string]string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
envPath := filepath.Join(home, ".gemini", ".env")
|
||||
data, err := os.ReadFile(envPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
env := make(map[string]string)
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
idx := strings.IndexByte(line, '=')
|
||||
if idx <= 0 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(line[:idx])
|
||||
value := strings.TrimSpace(line[idx+1:])
|
||||
if key != "" && value != "" {
|
||||
env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Set bearer auth mechanism for third-party API compatibility
|
||||
if _, ok := env["GEMINI_API_KEY"]; ok {
|
||||
if _, hasAuth := env["GEMINI_API_KEY_AUTH_MECHANISM"]; !hasAuth {
|
||||
env["GEMINI_API_KEY_AUTH_MECHANISM"] = "bearer"
|
||||
}
|
||||
}
|
||||
|
||||
if len(env) == 0 {
|
||||
return nil
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func buildClaudeArgs(cfg *Config, targetArg string) []string {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
args := []string{"-p"}
|
||||
// Default to skip permissions unless CODEAGENT_SKIP_PERMISSIONS=false
|
||||
if cfg.SkipPermissions || cfg.Yolo || envFlagDefaultTrue("CODEAGENT_SKIP_PERMISSIONS") {
|
||||
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 model := strings.TrimSpace(cfg.Model); model != "" {
|
||||
args = append(args, "--model", model)
|
||||
}
|
||||
|
||||
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 {
|
||||
return buildGeminiArgs(cfg, targetArg)
|
||||
}
|
||||
|
||||
type OpencodeBackend struct{}
|
||||
|
||||
func (OpencodeBackend) Name() string { return "opencode" }
|
||||
func (OpencodeBackend) Command() string { return "opencode" }
|
||||
func (OpencodeBackend) BuildArgs(cfg *Config, targetArg string) []string {
|
||||
args := []string{"run"}
|
||||
if model := strings.TrimSpace(cfg.Model); model != "" {
|
||||
args = append(args, "-m", model)
|
||||
}
|
||||
if cfg.Mode == "resume" && cfg.SessionID != "" {
|
||||
args = append(args, "-s", cfg.SessionID)
|
||||
}
|
||||
args = append(args, "--format", "json")
|
||||
if targetArg != "-" {
|
||||
args = append(args, targetArg)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func buildGeminiArgs(cfg *Config, targetArg string) []string {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
args := []string{"-o", "stream-json", "-y"}
|
||||
|
||||
if model := strings.TrimSpace(cfg.Model); model != "" {
|
||||
args = append(args, "-m", model)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Use positional argument instead of deprecated -p flag
|
||||
// For stdin mode ("-"), use -p to read from stdin
|
||||
if targetArg == "-" {
|
||||
args = append(args, "-p", targetArg)
|
||||
} else {
|
||||
args = append(args, targetArg)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
260
codeagent-wrapper/backend_test.go
Normal file
260
codeagent-wrapper/backend_test.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
|
||||
backend := ClaudeBackend{}
|
||||
|
||||
t.Run("new mode omits skip-permissions when env disabled", func(t *testing.T) {
|
||||
t.Setenv("CODEAGENT_SKIP_PERMISSIONS", "false")
|
||||
cfg := &Config{Mode: "new", WorkDir: "/repo"}
|
||||
got := backend.BuildArgs(cfg, "todo")
|
||||
want := []string{"-p", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "todo"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("new mode includes skip-permissions by default", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "new", SkipPermissions: false}
|
||||
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 includes session id", func(t *testing.T) {
|
||||
t.Setenv("CODEAGENT_SKIP_PERMISSIONS", "false")
|
||||
cfg := &Config{Mode: "resume", SessionID: "sid-123", WorkDir: "/ignored"}
|
||||
got := backend.BuildArgs(cfg, "resume-task")
|
||||
want := []string{"-p", "--setting-sources", "", "-r", "sid-123", "--output-format", "stream-json", "--verbose", "resume-task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resume mode without session still returns base flags", func(t *testing.T) {
|
||||
t.Setenv("CODEAGENT_SKIP_PERMISSIONS", "false")
|
||||
cfg := &Config{Mode: "resume", WorkDir: "/ignored"}
|
||||
got := backend.BuildArgs(cfg, "follow-up")
|
||||
want := []string{"-p", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "follow-up"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resume mode can opt-in skip permissions", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "resume", SessionID: "sid-123", SkipPermissions: true}
|
||||
got := backend.BuildArgs(cfg, "resume-task")
|
||||
want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "-r", "sid-123", "--output-format", "stream-json", "--verbose", "resume-task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
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 TestBackendBuildArgs_Model(t *testing.T) {
|
||||
t.Run("claude includes --model when set", func(t *testing.T) {
|
||||
t.Setenv("CODEAGENT_SKIP_PERMISSIONS", "false")
|
||||
backend := ClaudeBackend{}
|
||||
cfg := &Config{Mode: "new", Model: "opus"}
|
||||
got := backend.BuildArgs(cfg, "todo")
|
||||
want := []string{"-p", "--setting-sources", "", "--model", "opus", "--output-format", "stream-json", "--verbose", "todo"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("gemini includes -m when set", func(t *testing.T) {
|
||||
backend := GeminiBackend{}
|
||||
cfg := &Config{Mode: "new", Model: "gemini-3-pro-preview"}
|
||||
got := backend.BuildArgs(cfg, "task")
|
||||
want := []string{"-o", "stream-json", "-y", "-m", "gemini-3-pro-preview", "task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("codex includes --model when set", func(t *testing.T) {
|
||||
const key = "CODEX_BYPASS_SANDBOX"
|
||||
t.Setenv(key, "false")
|
||||
|
||||
backend := CodexBackend{}
|
||||
cfg := &Config{Mode: "new", WorkDir: "/tmp", Model: "o3"}
|
||||
got := backend.BuildArgs(cfg, "task")
|
||||
want := []string{"e", "--model", "o3", "--skip-git-repo-check", "-C", "/tmp", "--json", "task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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", "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", "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", "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("gemini stdin mode uses -p flag", func(t *testing.T) {
|
||||
backend := GeminiBackend{}
|
||||
cfg := &Config{Mode: "new"}
|
||||
got := backend.BuildArgs(cfg, "-")
|
||||
want := []string{"-o", "stream-json", "-y", "-p", "-"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("codex build args omits bypass flag by default", func(t *testing.T) {
|
||||
const key = "CODEX_BYPASS_SANDBOX"
|
||||
t.Setenv(key, "false")
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("codex build args includes bypass flag when enabled", func(t *testing.T) {
|
||||
const key = "CODEX_BYPASS_SANDBOX"
|
||||
t.Setenv(key, "true")
|
||||
|
||||
backend := CodexBackend{}
|
||||
cfg := &Config{Mode: "new", WorkDir: "/tmp"}
|
||||
got := backend.BuildArgs(cfg, "task")
|
||||
want := []string{"e", "--dangerously-bypass-approvals-and-sandbox", "--skip-git-repo-check", "-C", "/tmp", "--json", "task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestClaudeBuildArgs_BackendMetadata(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMinimalEnvSettings(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
t.Run("missing file returns empty", func(t *testing.T) {
|
||||
if got := loadMinimalEnvSettings(); len(got) != 0 {
|
||||
t.Fatalf("got %v, want empty", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid env returns string map", func(t *testing.T) {
|
||||
dir := filepath.Join(home, ".claude")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
path := filepath.Join(dir, "settings.json")
|
||||
data := []byte(`{"env":{"ANTHROPIC_API_KEY":"secret","FOO":"bar"}}`)
|
||||
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
got := loadMinimalEnvSettings()
|
||||
if got["ANTHROPIC_API_KEY"] != "secret" || got["FOO"] != "bar" {
|
||||
t.Fatalf("got %v, want keys present", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-string values are ignored", func(t *testing.T) {
|
||||
dir := filepath.Join(home, ".claude")
|
||||
path := filepath.Join(dir, "settings.json")
|
||||
data := []byte(`{"env":{"GOOD":"ok","BAD":123,"ALSO_BAD":true}}`)
|
||||
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
got := loadMinimalEnvSettings()
|
||||
if got["GOOD"] != "ok" {
|
||||
t.Fatalf("got %v, want GOOD=ok", got)
|
||||
}
|
||||
if _, ok := got["BAD"]; ok {
|
||||
t.Fatalf("got %v, want BAD omitted", got)
|
||||
}
|
||||
if _, ok := got["ALSO_BAD"]; ok {
|
||||
t.Fatalf("got %v, want ALSO_BAD omitted", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("oversized file returns empty", func(t *testing.T) {
|
||||
dir := filepath.Join(home, ".claude")
|
||||
path := filepath.Join(dir, "settings.json")
|
||||
data := bytes.Repeat([]byte("a"), maxClaudeSettingsBytes+1)
|
||||
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
if got := loadMinimalEnvSettings(); len(got) != 0 {
|
||||
t.Fatalf("got %v, want empty", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
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()
|
||||
}
|
||||
434
codeagent-wrapper/concurrent_stress_test.go
Normal file
434
codeagent-wrapper/concurrent_stress_test.go
Normal file
@@ -0,0 +1,434 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func stripTimestampPrefix(line string) string {
|
||||
if !strings.HasPrefix(line, "[") {
|
||||
return line
|
||||
}
|
||||
if idx := strings.Index(line, "] "); idx >= 0 {
|
||||
return line[idx+2:]
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// 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(`^goroutine-\d+-msg-\d+$`)
|
||||
for i, line := range lines[:min(10, len(lines))] {
|
||||
msg := stripTimestampPrefix(line)
|
||||
if !formatRE.MatchString(msg) {
|
||||
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 := stripTimestampPrefix(scanner.Text())
|
||||
var gid, seq int
|
||||
// Parse format: G0-SEQ0001 (without INFO: prefix)
|
||||
_, err := fmt.Sscanf(line, "G%d-SEQ%04d", &gid, &seq)
|
||||
if err != nil {
|
||||
t.Errorf("invalid log format: %s (error: %v)", line, err)
|
||||
continue
|
||||
}
|
||||
sequences[gid] = append(sequences[gid], seq)
|
||||
}
|
||||
|
||||
// 验证每个 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)
|
||||
}
|
||||
}
|
||||
473
codeagent-wrapper/config.go
Normal file
473
codeagent-wrapper/config.go
Normal file
@@ -0,0 +1,473 @@
|
||||
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
|
||||
Model string
|
||||
ReasoningEffort string
|
||||
ExplicitStdin bool
|
||||
Timeout int
|
||||
Backend string
|
||||
Agent string
|
||||
PromptFile string
|
||||
PromptFileExplicit bool
|
||||
SkipPermissions bool
|
||||
Yolo 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"`
|
||||
Model string `json:"model,omitempty"`
|
||||
ReasoningEffort string `json:"reasoning_effort,omitempty"`
|
||||
Agent string `json:"agent,omitempty"`
|
||||
PromptFile string `json:"prompt_file,omitempty"`
|
||||
SkipPermissions bool `json:"skip_permissions,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"`
|
||||
// Structured report fields
|
||||
Coverage string `json:"coverage,omitempty"` // extracted coverage percentage (e.g., "92%")
|
||||
CoverageNum float64 `json:"coverage_num,omitempty"` // numeric coverage for comparison
|
||||
CoverageTarget float64 `json:"coverage_target,omitempty"` // target coverage (default 90)
|
||||
FilesChanged []string `json:"files_changed,omitempty"` // list of changed files
|
||||
KeyOutput string `json:"key_output,omitempty"` // brief summary of what was done
|
||||
TestsPassed int `json:"tests_passed,omitempty"` // number of tests passed
|
||||
TestsFailed int `json:"tests_failed,omitempty"` // number of tests failed
|
||||
sharedLog bool
|
||||
}
|
||||
|
||||
var backendRegistry = map[string]Backend{
|
||||
"codex": CodexBackend{},
|
||||
"claude": ClaudeBackend{},
|
||||
"gemini": GeminiBackend{},
|
||||
"opencode": OpencodeBackend{},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// envFlagDefaultTrue returns true unless the env var is explicitly set to false/0/no/off.
|
||||
func envFlagDefaultTrue(key string) bool {
|
||||
val, ok := os.LookupEnv(key)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return parseBoolFlag(val, true)
|
||||
}
|
||||
|
||||
func validateAgentName(name string) error {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return fmt.Errorf("agent name is empty")
|
||||
}
|
||||
for _, r := range name {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
case r >= 'A' && r <= 'Z':
|
||||
case r >= '0' && r <= '9':
|
||||
case r == '-', r == '_':
|
||||
default:
|
||||
return fmt.Errorf("agent name %q contains invalid character %q", name, r)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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}
|
||||
agentSpecified := false
|
||||
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":
|
||||
// Validate workdir: "-" is not a valid directory
|
||||
if value == "-" {
|
||||
return nil, fmt.Errorf("task block #%d has invalid workdir: '-' is not a valid directory path", taskIndex)
|
||||
}
|
||||
task.WorkDir = value
|
||||
case "session_id":
|
||||
task.SessionID = value
|
||||
task.Mode = "resume"
|
||||
case "backend":
|
||||
task.Backend = value
|
||||
case "model":
|
||||
task.Model = value
|
||||
case "reasoning_effort":
|
||||
task.ReasoningEffort = value
|
||||
case "agent":
|
||||
agentSpecified = true
|
||||
task.Agent = value
|
||||
case "skip_permissions", "skip-permissions":
|
||||
if value == "" {
|
||||
task.SkipPermissions = true
|
||||
continue
|
||||
}
|
||||
task.SkipPermissions = parseBoolFlag(value, false)
|
||||
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 agentSpecified {
|
||||
if strings.TrimSpace(task.Agent) == "" {
|
||||
return nil, fmt.Errorf("task block #%d has empty agent field", taskIndex)
|
||||
}
|
||||
if err := validateAgentName(task.Agent); err != nil {
|
||||
return nil, fmt.Errorf("task block #%d invalid agent name: %w", taskIndex, err)
|
||||
}
|
||||
backend, model, promptFile, reasoning, _ := resolveAgentConfig(task.Agent)
|
||||
if task.Backend == "" {
|
||||
task.Backend = backend
|
||||
}
|
||||
if task.Model == "" {
|
||||
task.Model = model
|
||||
}
|
||||
if task.ReasoningEffort == "" {
|
||||
task.ReasoningEffort = reasoning
|
||||
}
|
||||
task.PromptFile = promptFile
|
||||
}
|
||||
|
||||
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 task.Mode == "resume" && strings.TrimSpace(task.SessionID) == "" {
|
||||
return nil, fmt.Errorf("task block #%d (%q) has empty session_id", taskIndex, task.ID)
|
||||
}
|
||||
if _, exists := seen[task.ID]; exists {
|
||||
return nil, fmt.Errorf("task block #%d has duplicate id: %s", taskIndex, task.ID)
|
||||
}
|
||||
|
||||
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
|
||||
model := ""
|
||||
reasoningEffort := ""
|
||||
agentName := ""
|
||||
promptFile := ""
|
||||
promptFileExplicit := false
|
||||
yolo := false
|
||||
skipPermissions := envFlagEnabled("CODEAGENT_SKIP_PERMISSIONS")
|
||||
filtered := make([]string, 0, len(args))
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--agent":
|
||||
if i+1 >= len(args) {
|
||||
return nil, fmt.Errorf("--agent flag requires a value")
|
||||
}
|
||||
value := strings.TrimSpace(args[i+1])
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--agent flag requires a value")
|
||||
}
|
||||
if err := validateAgentName(value); err != nil {
|
||||
return nil, fmt.Errorf("--agent flag invalid value: %w", err)
|
||||
}
|
||||
resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning, resolvedYolo := resolveAgentConfig(value)
|
||||
backendName = resolvedBackend
|
||||
model = resolvedModel
|
||||
if !promptFileExplicit {
|
||||
promptFile = resolvedPromptFile
|
||||
}
|
||||
if reasoningEffort == "" {
|
||||
reasoningEffort = resolvedReasoning
|
||||
}
|
||||
yolo = resolvedYolo
|
||||
agentName = value
|
||||
i++
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--agent="):
|
||||
value := strings.TrimSpace(strings.TrimPrefix(arg, "--agent="))
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--agent flag requires a value")
|
||||
}
|
||||
if err := validateAgentName(value); err != nil {
|
||||
return nil, fmt.Errorf("--agent flag invalid value: %w", err)
|
||||
}
|
||||
resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning, resolvedYolo := resolveAgentConfig(value)
|
||||
backendName = resolvedBackend
|
||||
model = resolvedModel
|
||||
if !promptFileExplicit {
|
||||
promptFile = resolvedPromptFile
|
||||
}
|
||||
if reasoningEffort == "" {
|
||||
reasoningEffort = resolvedReasoning
|
||||
}
|
||||
yolo = resolvedYolo
|
||||
agentName = value
|
||||
continue
|
||||
case arg == "--prompt-file":
|
||||
if i+1 >= len(args) {
|
||||
return nil, fmt.Errorf("--prompt-file flag requires a value")
|
||||
}
|
||||
value := strings.TrimSpace(args[i+1])
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--prompt-file flag requires a value")
|
||||
}
|
||||
promptFile = value
|
||||
promptFileExplicit = true
|
||||
i++
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--prompt-file="):
|
||||
value := strings.TrimSpace(strings.TrimPrefix(arg, "--prompt-file="))
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--prompt-file flag requires a value")
|
||||
}
|
||||
promptFile = value
|
||||
promptFileExplicit = true
|
||||
continue
|
||||
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 arg == "--model":
|
||||
if i+1 >= len(args) {
|
||||
return nil, fmt.Errorf("--model flag requires a value")
|
||||
}
|
||||
model = args[i+1]
|
||||
i++
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--model="):
|
||||
value := strings.TrimPrefix(arg, "--model=")
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--model flag requires a value")
|
||||
}
|
||||
model = value
|
||||
continue
|
||||
case arg == "--reasoning-effort":
|
||||
if i+1 >= len(args) {
|
||||
return nil, fmt.Errorf("--reasoning-effort flag requires a value")
|
||||
}
|
||||
value := strings.TrimSpace(args[i+1])
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--reasoning-effort flag requires a value")
|
||||
}
|
||||
reasoningEffort = value
|
||||
i++
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--reasoning-effort="):
|
||||
value := strings.TrimSpace(strings.TrimPrefix(arg, "--reasoning-effort="))
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--reasoning-effort flag requires a value")
|
||||
}
|
||||
reasoningEffort = value
|
||||
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, Agent: agentName, PromptFile: promptFile, PromptFileExplicit: promptFileExplicit, SkipPermissions: skipPermissions, Yolo: yolo, Model: strings.TrimSpace(model), ReasoningEffort: strings.TrimSpace(reasoningEffort)}
|
||||
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 = strings.TrimSpace(args[1])
|
||||
if cfg.SessionID == "" {
|
||||
return nil, fmt.Errorf("resume mode requires non-empty session_id")
|
||||
}
|
||||
cfg.Task = args[2]
|
||||
cfg.ExplicitStdin = (args[2] == "-")
|
||||
if len(args) > 3 {
|
||||
// Validate workdir: "-" is not a valid directory
|
||||
if args[3] == "-" {
|
||||
return nil, fmt.Errorf("invalid workdir: '-' is not a valid directory path")
|
||||
}
|
||||
cfg.WorkDir = args[3]
|
||||
}
|
||||
} else {
|
||||
cfg.Mode = "new"
|
||||
cfg.Task = args[0]
|
||||
cfg.ExplicitStdin = (args[0] == "-")
|
||||
if len(args) > 1 {
|
||||
// Validate workdir: "-" is not a valid directory
|
||||
if args[1] == "-" {
|
||||
return nil, fmt.Errorf("invalid workdir: '-' is not a valid directory path")
|
||||
}
|
||||
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
|
||||
}
|
||||
1397
codeagent-wrapper/executor.go
Normal file
1397
codeagent-wrapper/executor.go
Normal file
File diff suppressed because it is too large
Load Diff
1477
codeagent-wrapper/executor_concurrent_test.go
Normal file
1477
codeagent-wrapper/executor_concurrent_test.go
Normal file
File diff suppressed because it is too large
Load Diff
72
codeagent-wrapper/filter.go
Normal file
72
codeagent-wrapper/filter.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// geminiNoisePatterns contains stderr patterns to filter for gemini backend
|
||||
var geminiNoisePatterns = []string{
|
||||
"[STARTUP]",
|
||||
"Session cleanup disabled",
|
||||
"Warning:",
|
||||
"(node:",
|
||||
"(Use `node --trace-warnings",
|
||||
"Loaded cached credentials",
|
||||
"Loading extension:",
|
||||
"YOLO mode is enabled",
|
||||
}
|
||||
|
||||
// codexNoisePatterns contains stderr patterns to filter for codex backend
|
||||
var codexNoisePatterns = []string{
|
||||
"ERROR codex_core::codex: needs_follow_up:",
|
||||
"ERROR codex_core::skills::loader:",
|
||||
}
|
||||
|
||||
// filteringWriter wraps an io.Writer and filters out lines matching patterns
|
||||
type filteringWriter struct {
|
||||
w io.Writer
|
||||
patterns []string
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func newFilteringWriter(w io.Writer, patterns []string) *filteringWriter {
|
||||
return &filteringWriter{w: w, patterns: patterns}
|
||||
}
|
||||
|
||||
func (f *filteringWriter) Write(p []byte) (n int, err error) {
|
||||
f.buf.Write(p)
|
||||
for {
|
||||
line, err := f.buf.ReadString('\n')
|
||||
if err != nil {
|
||||
// incomplete line, put it back
|
||||
f.buf.WriteString(line)
|
||||
break
|
||||
}
|
||||
if !f.shouldFilter(line) {
|
||||
f.w.Write([]byte(line))
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (f *filteringWriter) shouldFilter(line string) bool {
|
||||
for _, pattern := range f.patterns {
|
||||
if strings.Contains(line, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Flush writes any remaining buffered content
|
||||
func (f *filteringWriter) Flush() {
|
||||
if f.buf.Len() > 0 {
|
||||
remaining := f.buf.String()
|
||||
if !f.shouldFilter(remaining) {
|
||||
f.w.Write([]byte(remaining))
|
||||
}
|
||||
f.buf.Reset()
|
||||
}
|
||||
}
|
||||
73
codeagent-wrapper/filter_test.go
Normal file
73
codeagent-wrapper/filter_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFilteringWriter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
patterns []string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "filter STARTUP lines",
|
||||
patterns: geminiNoisePatterns,
|
||||
input: "[STARTUP] Recording metric\nHello World\n[STARTUP] Another line\n",
|
||||
want: "Hello World\n",
|
||||
},
|
||||
{
|
||||
name: "filter Warning lines",
|
||||
patterns: geminiNoisePatterns,
|
||||
input: "Warning: something bad\nActual output\n",
|
||||
want: "Actual output\n",
|
||||
},
|
||||
{
|
||||
name: "filter multiple patterns",
|
||||
patterns: geminiNoisePatterns,
|
||||
input: "YOLO mode is enabled\nSession cleanup disabled\nReal content\nLoading extension: foo\n",
|
||||
want: "Real content\n",
|
||||
},
|
||||
{
|
||||
name: "no filtering needed",
|
||||
patterns: geminiNoisePatterns,
|
||||
input: "Line 1\nLine 2\nLine 3\n",
|
||||
want: "Line 1\nLine 2\nLine 3\n",
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
patterns: geminiNoisePatterns,
|
||||
input: "",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
fw := newFilteringWriter(&buf, tt.patterns)
|
||||
fw.Write([]byte(tt.input))
|
||||
fw.Flush()
|
||||
|
||||
if got := buf.String(); got != tt.want {
|
||||
t.Errorf("got %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilteringWriterPartialLines(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
fw := newFilteringWriter(&buf, geminiNoisePatterns)
|
||||
|
||||
// Write partial line
|
||||
fw.Write([]byte("Hello "))
|
||||
fw.Write([]byte("World\n"))
|
||||
fw.Flush()
|
||||
|
||||
if got := buf.String(); got != "Hello World\n" {
|
||||
t.Errorf("got %q, want %q", got, "Hello World\n")
|
||||
}
|
||||
}
|
||||
3
codeagent-wrapper/go.mod
Normal file
3
codeagent-wrapper/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module codeagent-wrapper
|
||||
|
||||
go 1.21
|
||||
38
codeagent-wrapper/log_writer_limit_test.go
Normal file
38
codeagent-wrapper/log_writer_limit_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
663
codeagent-wrapper/logger.go
Normal file
663
codeagent-wrapper/logger.go
Normal file
@@ -0,0 +1,663 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"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
|
||||
workerErr error
|
||||
errorEntries []string // Cache of recent ERROR/WARN entries
|
||||
errorMu sync.Mutex
|
||||
}
|
||||
|
||||
type logEntry struct {
|
||||
msg string
|
||||
isError bool // true for ERROR or WARN levels
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
const maxLogSuffixLen = 64
|
||||
|
||||
var logSuffixCounter atomic.Uint64
|
||||
|
||||
// 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) {
|
||||
pid := os.Getpid()
|
||||
filename := fmt.Sprintf("%s-%d", primaryLogPrefix(), pid)
|
||||
var safeSuffix string
|
||||
if suffix != "" {
|
||||
safeSuffix = sanitizeLogSuffix(suffix)
|
||||
}
|
||||
if safeSuffix != "" {
|
||||
filename += "-" + safeSuffix
|
||||
}
|
||||
filename += ".log"
|
||||
|
||||
path := filepath.Clean(filepath.Join(os.TempDir(), filename))
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func sanitizeLogSuffix(raw string) string {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return fallbackLogSuffix()
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
changed := false
|
||||
for _, r := range trimmed {
|
||||
if isSafeLogRune(r) {
|
||||
b.WriteRune(r)
|
||||
} else {
|
||||
changed = true
|
||||
b.WriteByte('-')
|
||||
}
|
||||
if b.Len() >= maxLogSuffixLen {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sanitized := strings.Trim(b.String(), "-.")
|
||||
if sanitized != b.String() {
|
||||
changed = true // Mark if trim removed any characters
|
||||
}
|
||||
if sanitized == "" {
|
||||
return fallbackLogSuffix()
|
||||
}
|
||||
|
||||
if changed || len(sanitized) > maxLogSuffixLen {
|
||||
hash := crc32.ChecksumIEEE([]byte(trimmed))
|
||||
hashStr := fmt.Sprintf("%x", hash)
|
||||
|
||||
maxPrefix := maxLogSuffixLen - len(hashStr) - 1
|
||||
if maxPrefix < 1 {
|
||||
maxPrefix = 1
|
||||
}
|
||||
if len(sanitized) > maxPrefix {
|
||||
sanitized = sanitized[:maxPrefix]
|
||||
}
|
||||
|
||||
sanitized = fmt.Sprintf("%s-%s", sanitized, hashStr)
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
func fallbackLogSuffix() string {
|
||||
next := logSuffixCounter.Add(1)
|
||||
return fmt.Sprintf("task-%d", next)
|
||||
}
|
||||
|
||||
func isSafeLogRune(r rune) bool {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
return true
|
||||
case r >= 'A' && r <= 'Z':
|
||||
return true
|
||||
case r >= '0' && r <= '9':
|
||||
return true
|
||||
case r == '-', r == '_', r == '.':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 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 signals the worker to flush and close the log file.
|
||||
// The log file is NOT removed, allowing inspection after program exit.
|
||||
// It is safe to call multiple times.
|
||||
// Waits up to CODEAGENT_LOGGER_CLOSE_TIMEOUT_MS (default: 5000) for shutdown; set to 0 to wait indefinitely.
|
||||
// Returns an error if shutdown doesn't complete within the timeout.
|
||||
func (l *Logger) Close() error {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var closeErr error
|
||||
|
||||
l.closeOnce.Do(func() {
|
||||
l.closed.Store(true)
|
||||
close(l.done)
|
||||
|
||||
timeout := loggerCloseTimeout()
|
||||
workerDone := make(chan struct{})
|
||||
go func() {
|
||||
l.workerWG.Wait()
|
||||
close(workerDone)
|
||||
}()
|
||||
|
||||
if timeout > 0 {
|
||||
select {
|
||||
case <-workerDone:
|
||||
// Worker stopped gracefully
|
||||
case <-time.After(timeout):
|
||||
closeErr = fmt.Errorf("logger worker timeout during close")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
<-workerDone
|
||||
}
|
||||
|
||||
if l.workerErr != nil && closeErr == nil {
|
||||
closeErr = l.workerErr
|
||||
}
|
||||
})
|
||||
|
||||
return closeErr
|
||||
}
|
||||
|
||||
func loggerCloseTimeout() time.Duration {
|
||||
const defaultTimeout = 5 * time.Second
|
||||
|
||||
raw := strings.TrimSpace(os.Getenv("CODEAGENT_LOGGER_CLOSE_TIMEOUT_MS"))
|
||||
if raw == "" {
|
||||
return defaultTimeout
|
||||
}
|
||||
ms, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return defaultTimeout
|
||||
}
|
||||
if ms <= 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(ms) * time.Millisecond
|
||||
}
|
||||
|
||||
// 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 returns the most recent ERROR and WARN entries from memory cache.
|
||||
// Returns up to maxEntries entries in chronological order.
|
||||
func (l *Logger) ExtractRecentErrors(maxEntries int) []string {
|
||||
if l == nil || maxEntries <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
l.errorMu.Lock()
|
||||
defer l.errorMu.Unlock()
|
||||
|
||||
if len(l.errorEntries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return last N entries
|
||||
start := 0
|
||||
if len(l.errorEntries) > maxEntries {
|
||||
start = len(l.errorEntries) - maxEntries
|
||||
}
|
||||
|
||||
result := make([]string, len(l.errorEntries)-start)
|
||||
copy(result, l.errorEntries[start:])
|
||||
return result
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
isError := level == "WARN" || level == "ERROR"
|
||||
entry := logEntry{msg: msg, isError: isError}
|
||||
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()
|
||||
|
||||
writeEntry := func(entry logEntry) {
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
|
||||
fmt.Fprintf(l.writer, "[%s] %s\n", timestamp, entry.msg)
|
||||
|
||||
// Cache error/warn entries in memory for fast extraction
|
||||
if entry.isError {
|
||||
l.errorMu.Lock()
|
||||
l.errorEntries = append(l.errorEntries, entry.msg)
|
||||
if len(l.errorEntries) > 100 { // Keep last 100
|
||||
l.errorEntries = l.errorEntries[1:]
|
||||
}
|
||||
l.errorMu.Unlock()
|
||||
}
|
||||
|
||||
l.pendingWG.Done()
|
||||
}
|
||||
|
||||
finalize := func() {
|
||||
if err := l.writer.Flush(); err != nil && l.workerErr == nil {
|
||||
l.workerErr = err
|
||||
}
|
||||
if err := l.file.Sync(); err != nil && l.workerErr == nil {
|
||||
l.workerErr = err
|
||||
}
|
||||
if err := l.file.Close(); err != nil && l.workerErr == nil {
|
||||
l.workerErr = err
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case entry, ok := <-l.ch:
|
||||
if !ok {
|
||||
finalize()
|
||||
return
|
||||
}
|
||||
writeEntry(entry)
|
||||
|
||||
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)
|
||||
|
||||
case <-l.done:
|
||||
for {
|
||||
select {
|
||||
case entry, ok := <-l.ch:
|
||||
if !ok {
|
||||
finalize()
|
||||
return
|
||||
}
|
||||
writeEntry(entry)
|
||||
default:
|
||||
finalize()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
158
codeagent-wrapper/logger_additional_coverage_test.go
Normal file
158
codeagent-wrapper/logger_additional_coverage_test.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoggerNilReceiverNoop(t *testing.T) {
|
||||
var logger *Logger
|
||||
logger.Info("info")
|
||||
logger.Warn("warn")
|
||||
logger.Debug("debug")
|
||||
logger.Error("error")
|
||||
logger.Flush()
|
||||
if err := logger.Close(); err != nil {
|
||||
t.Fatalf("Close() on nil logger should return nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerConcurrencyLogHelpers(t *testing.T) {
|
||||
setTempDirEnv(t, t.TempDir())
|
||||
|
||||
logger, err := NewLoggerWithSuffix("concurrency")
|
||||
if err != nil {
|
||||
t.Fatalf("NewLoggerWithSuffix error: %v", err)
|
||||
}
|
||||
setLogger(logger)
|
||||
defer closeLogger()
|
||||
|
||||
logConcurrencyPlanning(0, 2)
|
||||
logConcurrencyPlanning(3, 2)
|
||||
logConcurrencyState("start", "task-1", 1, 0)
|
||||
logConcurrencyState("done", "task-1", 0, 3)
|
||||
logger.Flush()
|
||||
|
||||
data, err := os.ReadFile(logger.Path())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read log file: %v", err)
|
||||
}
|
||||
output := string(data)
|
||||
|
||||
checks := []string{
|
||||
"parallel: worker_limit=unbounded total_tasks=2",
|
||||
"parallel: worker_limit=3 total_tasks=2",
|
||||
"parallel: start task=task-1 active=1 limit=unbounded",
|
||||
"parallel: done task=task-1 active=0 limit=3",
|
||||
}
|
||||
for _, c := range checks {
|
||||
if !strings.Contains(output, c) {
|
||||
t.Fatalf("log output missing %q, got: %s", c, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerConcurrencyLogHelpersNoopWithoutActiveLogger(t *testing.T) {
|
||||
_ = closeLogger()
|
||||
logConcurrencyPlanning(1, 1)
|
||||
logConcurrencyState("start", "task-1", 0, 1)
|
||||
}
|
||||
|
||||
func TestLoggerCleanupOldLogsSkipsUnsafeAndHandlesAlreadyDeleted(t *testing.T) {
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
unsafePath := createTempLog(t, tempDir, fmt.Sprintf("%s-%d.log", primaryLogPrefix(), 222))
|
||||
orphanPath := createTempLog(t, tempDir, fmt.Sprintf("%s-%d.log", primaryLogPrefix(), 111))
|
||||
|
||||
stubFileStat(t, func(path string) (os.FileInfo, error) {
|
||||
if path == unsafePath {
|
||||
return fakeFileInfo{mode: os.ModeSymlink}, nil
|
||||
}
|
||||
return os.Lstat(path)
|
||||
})
|
||||
|
||||
stubProcessRunning(t, func(pid int) bool {
|
||||
if pid == 111 {
|
||||
_ = os.Remove(orphanPath)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
stats, err := cleanupOldLogs()
|
||||
if err != nil {
|
||||
t.Fatalf("cleanupOldLogs() unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if stats.Scanned != 2 {
|
||||
t.Fatalf("scanned = %d, want %d", stats.Scanned, 2)
|
||||
}
|
||||
if stats.Deleted != 0 {
|
||||
t.Fatalf("deleted = %d, want %d", stats.Deleted, 0)
|
||||
}
|
||||
if stats.Kept != 2 {
|
||||
t.Fatalf("kept = %d, want %d", stats.Kept, 2)
|
||||
}
|
||||
if stats.Errors != 0 {
|
||||
t.Fatalf("errors = %d, want %d", stats.Errors, 0)
|
||||
}
|
||||
|
||||
hasSkip := false
|
||||
hasAlreadyDeleted := false
|
||||
for _, name := range stats.KeptFiles {
|
||||
if strings.Contains(name, "already deleted") {
|
||||
hasAlreadyDeleted = true
|
||||
}
|
||||
if strings.Contains(name, filepath.Base(unsafePath)) {
|
||||
hasSkip = true
|
||||
}
|
||||
}
|
||||
if !hasSkip {
|
||||
t.Fatalf("expected kept files to include unsafe log %q, got %+v", filepath.Base(unsafePath), stats.KeptFiles)
|
||||
}
|
||||
if !hasAlreadyDeleted {
|
||||
t.Fatalf("expected kept files to include already deleted marker, got %+v", stats.KeptFiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerIsUnsafeFileErrorPaths(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
t.Run("stat ErrNotExist", func(t *testing.T) {
|
||||
stubFileStat(t, func(string) (os.FileInfo, error) {
|
||||
return nil, os.ErrNotExist
|
||||
})
|
||||
|
||||
unsafe, reason := isUnsafeFile("missing.log", tempDir)
|
||||
if !unsafe || reason != "" {
|
||||
t.Fatalf("expected missing file to be skipped silently, got unsafe=%v reason=%q", unsafe, reason)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("stat error", func(t *testing.T) {
|
||||
stubFileStat(t, func(string) (os.FileInfo, error) {
|
||||
return nil, fmt.Errorf("boom")
|
||||
})
|
||||
|
||||
unsafe, reason := isUnsafeFile("broken.log", tempDir)
|
||||
if !unsafe || !strings.Contains(reason, "stat failed") {
|
||||
t.Fatalf("expected stat failure to be unsafe, got unsafe=%v reason=%q", unsafe, reason)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EvalSymlinks error", func(t *testing.T) {
|
||||
stubFileStat(t, func(string) (os.FileInfo, error) {
|
||||
return fakeFileInfo{}, nil
|
||||
})
|
||||
stubEvalSymlinks(t, func(string) (string, error) {
|
||||
return "", fmt.Errorf("resolve failed")
|
||||
})
|
||||
|
||||
unsafe, reason := isUnsafeFile("cannot-resolve.log", tempDir)
|
||||
if !unsafe || !strings.Contains(reason, "path resolution failed") {
|
||||
t.Fatalf("expected resolution failure to be unsafe, got unsafe=%v reason=%q", unsafe, reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
115
codeagent-wrapper/logger_suffix_test.go
Normal file
115
codeagent-wrapper/logger_suffix_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoggerWithSuffixNamingAndIsolation(t *testing.T) {
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
taskA := "task-1"
|
||||
taskB := "task-2"
|
||||
|
||||
loggerA, err := NewLoggerWithSuffix(taskA)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLoggerWithSuffix(%q) error = %v", taskA, err)
|
||||
}
|
||||
defer loggerA.Close()
|
||||
|
||||
loggerB, err := NewLoggerWithSuffix(taskB)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLoggerWithSuffix(%q) error = %v", taskB, err)
|
||||
}
|
||||
defer loggerB.Close()
|
||||
|
||||
wantA := filepath.Join(tempDir, fmt.Sprintf("%s-%d-%s.log", primaryLogPrefix(), os.Getpid(), taskA))
|
||||
if loggerA.Path() != wantA {
|
||||
t.Fatalf("loggerA path = %q, want %q", loggerA.Path(), wantA)
|
||||
}
|
||||
|
||||
wantB := filepath.Join(tempDir, fmt.Sprintf("%s-%d-%s.log", primaryLogPrefix(), os.Getpid(), taskB))
|
||||
if loggerB.Path() != wantB {
|
||||
t.Fatalf("loggerB path = %q, want %q", loggerB.Path(), wantB)
|
||||
}
|
||||
|
||||
if loggerA.Path() == loggerB.Path() {
|
||||
t.Fatalf("expected different log files, got %q", loggerA.Path())
|
||||
}
|
||||
|
||||
loggerA.Info("from taskA")
|
||||
loggerB.Info("from taskB")
|
||||
loggerA.Flush()
|
||||
loggerB.Flush()
|
||||
|
||||
dataA, err := os.ReadFile(loggerA.Path())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read loggerA file: %v", err)
|
||||
}
|
||||
dataB, err := os.ReadFile(loggerB.Path())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read loggerB file: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(dataA), "from taskA") {
|
||||
t.Fatalf("loggerA missing its message, got: %q", string(dataA))
|
||||
}
|
||||
if strings.Contains(string(dataA), "from taskB") {
|
||||
t.Fatalf("loggerA contains loggerB message, got: %q", string(dataA))
|
||||
}
|
||||
if !strings.Contains(string(dataB), "from taskB") {
|
||||
t.Fatalf("loggerB missing its message, got: %q", string(dataB))
|
||||
}
|
||||
if strings.Contains(string(dataB), "from taskA") {
|
||||
t.Fatalf("loggerB contains loggerA message, got: %q", string(dataB))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerWithSuffixReturnsErrorWhenTempDirNotWritable(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
noWrite := filepath.Join(base, "ro")
|
||||
if err := os.Mkdir(noWrite, 0o500); err != nil {
|
||||
t.Fatalf("failed to create read-only temp dir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Chmod(noWrite, 0o700) })
|
||||
setTempDirEnv(t, noWrite)
|
||||
|
||||
logger, err := NewLoggerWithSuffix("task-err")
|
||||
if err == nil {
|
||||
_ = logger.Close()
|
||||
t.Fatalf("expected error when temp dir is not writable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerWithSuffixSanitizesUnsafeSuffix(t *testing.T) {
|
||||
tempDir := setTempDirEnv(t, t.TempDir())
|
||||
|
||||
raw := "../bad id/with?chars"
|
||||
safe := sanitizeLogSuffix(raw)
|
||||
if safe == "" {
|
||||
t.Fatalf("sanitizeLogSuffix returned empty string")
|
||||
}
|
||||
if strings.ContainsAny(safe, "/\\") {
|
||||
t.Fatalf("sanitized suffix should not contain path separators, got %q", safe)
|
||||
}
|
||||
|
||||
logger, err := NewLoggerWithSuffix(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLoggerWithSuffix(%q) error = %v", raw, err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = logger.Close()
|
||||
_ = os.Remove(logger.Path())
|
||||
})
|
||||
|
||||
wantBase := fmt.Sprintf("%s-%d-%s.log", primaryLogPrefix(), os.Getpid(), safe)
|
||||
if gotBase := filepath.Base(logger.Path()); gotBase != wantBase {
|
||||
t.Fatalf("log filename = %q, want %q", gotBase, wantBase)
|
||||
}
|
||||
if dir := filepath.Dir(logger.Path()); dir != tempDir {
|
||||
t.Fatalf("logger path dir = %q, want %q", dir, tempDir)
|
||||
}
|
||||
}
|
||||
1128
codeagent-wrapper/logger_test.go
Normal file
1128
codeagent-wrapper/logger_test.go
Normal file
File diff suppressed because it is too large
Load Diff
627
codeagent-wrapper/main.go
Normal file
627
codeagent-wrapper/main.go
Normal file
@@ -0,0 +1,627 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
version = "5.6.4"
|
||||
defaultWorkdir = "."
|
||||
defaultTimeout = 7200 // seconds (2 hours)
|
||||
defaultCoverageTarget = 90.0
|
||||
codexLogLineLimit = 1000
|
||||
stdinSpecialChars = "\n\\\"'`$"
|
||||
stderrCaptureLimit = 4 * 1024
|
||||
defaultBackendName = "codex"
|
||||
defaultCodexCommand = "codex"
|
||||
|
||||
// stdout close reasons
|
||||
stdoutCloseReasonWait = "wait-done"
|
||||
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
|
||||
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
|
||||
model := ""
|
||||
fullOutput := false
|
||||
skipPermissions := envFlagEnabled("CODEAGENT_SKIP_PERMISSIONS")
|
||||
var extras []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--parallel":
|
||||
continue
|
||||
case arg == "--full-output":
|
||||
fullOutput = true
|
||||
case arg == "--backend":
|
||||
if i+1 >= len(args) {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: --backend flag requires a value")
|
||||
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
|
||||
case arg == "--model":
|
||||
if i+1 >= len(args) {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: --model flag requires a value")
|
||||
return 1
|
||||
}
|
||||
model = args[i+1]
|
||||
i++
|
||||
case strings.HasPrefix(arg, "--model="):
|
||||
value := strings.TrimPrefix(arg, "--model=")
|
||||
if value == "" {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: --model flag requires a value")
|
||||
return 1
|
||||
}
|
||||
model = value
|
||||
case arg == "--skip-permissions", arg == "--dangerously-skip-permissions":
|
||||
skipPermissions = true
|
||||
case strings.HasPrefix(arg, "--skip-permissions="):
|
||||
skipPermissions = parseBoolFlag(strings.TrimPrefix(arg, "--skip-permissions="), skipPermissions)
|
||||
case strings.HasPrefix(arg, "--dangerously-skip-permissions="):
|
||||
skipPermissions = parseBoolFlag(strings.TrimPrefix(arg, "--dangerously-skip-permissions="), skipPermissions)
|
||||
default:
|
||||
extras = append(extras, arg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(extras) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: --parallel reads its task configuration from stdin; only --backend, --model, --full-output and --skip-permissions are allowed.")
|
||||
fmt.Fprintln(os.Stderr, "Usage examples:")
|
||||
fmt.Fprintf(os.Stderr, " %s --parallel < tasks.txt\n", name)
|
||||
fmt.Fprintf(os.Stderr, " echo '...' | %s --parallel\n", name)
|
||||
fmt.Fprintf(os.Stderr, " %s --parallel <<'EOF'\n", name)
|
||||
fmt.Fprintf(os.Stderr, " %s --parallel --full-output <<'EOF' # include full task output\n", name)
|
||||
return 1
|
||||
}
|
||||
|
||||
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
|
||||
model = strings.TrimSpace(model)
|
||||
for i := range cfg.Tasks {
|
||||
if strings.TrimSpace(cfg.Tasks[i].Backend) == "" {
|
||||
cfg.Tasks[i].Backend = backendName
|
||||
}
|
||||
if strings.TrimSpace(cfg.Tasks[i].Model) == "" && model != "" {
|
||||
cfg.Tasks[i].Model = model
|
||||
}
|
||||
cfg.Tasks[i].SkipPermissions = cfg.Tasks[i].SkipPermissions || skipPermissions
|
||||
}
|
||||
|
||||
timeoutSec := resolveTimeout()
|
||||
layers, err := topologicalSort(cfg.Tasks)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
results := executeConcurrent(layers, timeoutSec)
|
||||
|
||||
// Extract structured report fields from each result
|
||||
for i := range results {
|
||||
results[i].CoverageTarget = defaultCoverageTarget
|
||||
if results[i].Message == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
lines := strings.Split(results[i].Message, "\n")
|
||||
|
||||
// Coverage extraction
|
||||
results[i].Coverage = extractCoverageFromLines(lines)
|
||||
results[i].CoverageNum = extractCoverageNum(results[i].Coverage)
|
||||
|
||||
// Files changed
|
||||
results[i].FilesChanged = extractFilesChangedFromLines(lines)
|
||||
|
||||
// Test results
|
||||
results[i].TestsPassed, results[i].TestsFailed = extractTestResultsFromLines(lines)
|
||||
|
||||
// Key output summary
|
||||
results[i].KeyOutput = extractKeyOutputFromLines(lines, 150)
|
||||
}
|
||||
|
||||
// Default: summary mode (context-efficient)
|
||||
// --full-output: legacy full output mode
|
||||
fmt.Println(generateFinalOutputWithMode(results, !fullOutput))
|
||||
|
||||
exitCode = 0
|
||||
for _, res := range results {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(cfg.PromptFile) != "" {
|
||||
prompt, err := readAgentPromptFile(cfg.PromptFile, cfg.PromptFileExplicit)
|
||||
if err != nil {
|
||||
logError("Failed to read prompt file: " + err.Error())
|
||||
return 1
|
||||
}
|
||||
taskText = wrapTaskWithAgentPrompt(prompt, taskText)
|
||||
}
|
||||
|
||||
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,
|
||||
Model: cfg.Model,
|
||||
ReasoningEffort: cfg.ReasoningEffort,
|
||||
SkipPermissions: cfg.SkipPermissions,
|
||||
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 readAgentPromptFile(path string, allowOutsideClaudeDir bool) (string, error) {
|
||||
raw := strings.TrimSpace(path)
|
||||
if raw == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
expanded := raw
|
||||
if raw == "~" || strings.HasPrefix(raw, "~/") || strings.HasPrefix(raw, "~\\") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if raw == "~" {
|
||||
expanded = home
|
||||
} else {
|
||||
expanded = home + raw[1:]
|
||||
}
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(expanded)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
absPath = filepath.Clean(absPath)
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
if !allowOutsideClaudeDir {
|
||||
return "", err
|
||||
}
|
||||
logWarn(fmt.Sprintf("Failed to resolve home directory for prompt file validation: %v; proceeding without restriction", err))
|
||||
} else {
|
||||
allowedDir := filepath.Clean(filepath.Join(home, ".claude"))
|
||||
allowedAbs, err := filepath.Abs(allowedDir)
|
||||
if err == nil {
|
||||
allowedDir = filepath.Clean(allowedAbs)
|
||||
}
|
||||
|
||||
isWithinDir := func(path, dir string) bool {
|
||||
rel, err := filepath.Rel(dir, path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
rel = filepath.Clean(rel)
|
||||
if rel == "." {
|
||||
return true
|
||||
}
|
||||
if rel == ".." {
|
||||
return false
|
||||
}
|
||||
prefix := ".." + string(os.PathSeparator)
|
||||
return !strings.HasPrefix(rel, prefix)
|
||||
}
|
||||
|
||||
if !allowOutsideClaudeDir {
|
||||
if !isWithinDir(absPath, allowedDir) {
|
||||
logWarn(fmt.Sprintf("Refusing to read prompt file outside %s: %s", allowedDir, absPath))
|
||||
return "", fmt.Errorf("prompt file must be under %s", allowedDir)
|
||||
}
|
||||
resolvedPath, errPath := filepath.EvalSymlinks(absPath)
|
||||
resolvedBase, errBase := filepath.EvalSymlinks(allowedDir)
|
||||
if errPath == nil && errBase == nil {
|
||||
resolvedPath = filepath.Clean(resolvedPath)
|
||||
resolvedBase = filepath.Clean(resolvedBase)
|
||||
if !isWithinDir(resolvedPath, resolvedBase) {
|
||||
logWarn(fmt.Sprintf("Refusing to read prompt file outside %s (resolved): %s", resolvedBase, resolvedPath))
|
||||
return "", fmt.Errorf("prompt file must be under %s", resolvedBase)
|
||||
}
|
||||
}
|
||||
} else if !isWithinDir(absPath, allowedDir) {
|
||||
logWarn(fmt.Sprintf("Reading prompt file outside %s: %s", allowedDir, absPath))
|
||||
}
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimRight(string(data), "\r\n"), nil
|
||||
}
|
||||
|
||||
func wrapTaskWithAgentPrompt(prompt string, task string) string {
|
||||
return "<agent-prompt>\n" + prompt + "\n</agent-prompt>\n\n" + task
|
||||
}
|
||||
|
||||
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 --prompt-file /path/to/prompt.md "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 --parallel --full-output Run tasks in parallel with full output (legacy)
|
||||
%[1]s --version
|
||||
%[1]s --help
|
||||
|
||||
Parallel mode examples:
|
||||
%[1]s --parallel < tasks.txt
|
||||
echo '...' | %[1]s --parallel
|
||||
%[1]s --parallel --full-output < tasks.txt
|
||||
%[1]s --parallel <<'EOF'
|
||||
|
||||
Environment Variables:
|
||||
CODEX_TIMEOUT Timeout in milliseconds (default: 7200000)
|
||||
CODEAGENT_ASCII_MODE Use ASCII symbols instead of Unicode (PASS/WARN/FAIL)
|
||||
|
||||
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)
|
||||
}
|
||||
896
codeagent-wrapper/main_integration_test.go
Normal file
896
codeagent-wrapper/main_integration_test.go
Normal file
@@ -0,0 +1,896 @@
|
||||
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
|
||||
inTaskResults := false
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Parse new format header: "X tasks | Y passed | Z failed"
|
||||
if strings.Contains(line, "tasks |") && strings.Contains(line, "passed |") {
|
||||
parts := strings.Split(line, "|")
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if strings.HasSuffix(p, "tasks") {
|
||||
fmt.Sscanf(p, "%d tasks", &payload.Summary.Total)
|
||||
} else if strings.HasSuffix(p, "passed") {
|
||||
fmt.Sscanf(p, "%d passed", &payload.Summary.Success)
|
||||
} else if strings.HasSuffix(p, "failed") {
|
||||
fmt.Sscanf(p, "%d failed", &payload.Summary.Failed)
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(line, "Total:") {
|
||||
// Legacy format: "Total: X | Success: Y | Failed: Z"
|
||||
parts := strings.Split(line, "|")
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
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 line == "## Task Results" {
|
||||
inTaskResults = true
|
||||
} else if line == "## Summary" {
|
||||
// End of task results section
|
||||
if currentTask != nil {
|
||||
payload.Results = append(payload.Results, *currentTask)
|
||||
currentTask = nil
|
||||
}
|
||||
inTaskResults = false
|
||||
} else if inTaskResults && strings.HasPrefix(line, "### ") {
|
||||
// New task: ### task-id ✓ 92% or ### task-id PASS 92% (ASCII mode)
|
||||
if currentTask != nil {
|
||||
payload.Results = append(payload.Results, *currentTask)
|
||||
}
|
||||
currentTask = &TaskResult{}
|
||||
|
||||
taskLine := strings.TrimPrefix(line, "### ")
|
||||
success, warning, failed := getStatusSymbols()
|
||||
// Parse different formats
|
||||
if strings.Contains(taskLine, " "+success) {
|
||||
parts := strings.Split(taskLine, " "+success)
|
||||
currentTask.TaskID = strings.TrimSpace(parts[0])
|
||||
currentTask.ExitCode = 0
|
||||
// Extract coverage if present
|
||||
if len(parts) > 1 {
|
||||
coveragePart := strings.TrimSpace(parts[1])
|
||||
if strings.HasSuffix(coveragePart, "%") {
|
||||
currentTask.Coverage = coveragePart
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(taskLine, " "+warning) {
|
||||
parts := strings.Split(taskLine, " "+warning)
|
||||
currentTask.TaskID = strings.TrimSpace(parts[0])
|
||||
currentTask.ExitCode = 0
|
||||
} else if strings.Contains(taskLine, " "+failed) {
|
||||
parts := strings.Split(taskLine, " "+failed)
|
||||
currentTask.TaskID = strings.TrimSpace(parts[0])
|
||||
currentTask.ExitCode = 1
|
||||
} else {
|
||||
currentTask.TaskID = taskLine
|
||||
}
|
||||
} else if currentTask != nil && inTaskResults {
|
||||
// Parse task details
|
||||
if strings.HasPrefix(line, "Exit code:") {
|
||||
fmt.Sscanf(line, "Exit code: %d", ¤tTask.ExitCode)
|
||||
} else if strings.HasPrefix(line, "Error:") {
|
||||
currentTask.Error = strings.TrimPrefix(line, "Error: ")
|
||||
} else if strings.HasPrefix(line, "Log:") {
|
||||
currentTask.LogPath = strings.TrimSpace(strings.TrimPrefix(line, "Log:"))
|
||||
} else if strings.HasPrefix(line, "Did:") {
|
||||
currentTask.KeyOutput = strings.TrimSpace(strings.TrimPrefix(line, "Did:"))
|
||||
} else if strings.HasPrefix(line, "Detail:") {
|
||||
// Error detail for failed tasks
|
||||
if currentTask.Message == "" {
|
||||
currentTask.Message = strings.TrimSpace(strings.TrimPrefix(line, "Detail:"))
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(line, "--- Task:") {
|
||||
// Legacy full output format
|
||||
if currentTask != nil {
|
||||
payload.Results = append(payload.Results, *currentTask)
|
||||
}
|
||||
currentTask = &TaskResult{}
|
||||
currentTask.TaskID = strings.TrimSuffix(strings.TrimPrefix(line, "--- Task: "), " ---")
|
||||
} else if currentTask != nil && !inTaskResults {
|
||||
// Legacy format parsing
|
||||
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:"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle last task
|
||||
if currentTask != nil {
|
||||
payload.Results = append(payload.Results, *currentTask)
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
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"} {
|
||||
// Summary mode shows log paths in table format, not "Log: xxx"
|
||||
logPath := logPathFor(id)
|
||||
if !strings.Contains(output, logPath) {
|
||||
t.Fatalf("parallel output missing log path %q for %s:\n%s", logPath, id, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// After parallel log isolation fix, each task has its own log file
|
||||
expectedLines := map[string]struct{}{
|
||||
fmt.Sprintf("Task a: Log: %s", filepath.Join(tempDir, fmt.Sprintf("codex-wrapper-%d-a.log", os.Getpid()))): {},
|
||||
fmt.Sprintf("Task b: Log: %s", filepath.Join(tempDir, fmt.Sprintf("codex-wrapper-%d-b.log", os.Getpid()))): {},
|
||||
fmt.Sprintf("Task d: Log: %s", filepath.Join(tempDir, fmt.Sprintf("codex-wrapper-%d-d.log", os.Getpid()))): {},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// Summary mode shows log paths in table, verify they appear in output
|
||||
for _, id := range []string{"A", "D", "E"} {
|
||||
logPath := logPathFor(id)
|
||||
if !strings.Contains(output, logPath) {
|
||||
t.Fatalf("task %s log path %q not found in output:\n%s", id, logPath, output)
|
||||
}
|
||||
}
|
||||
// Task B was skipped, should have "-" or empty log path in table
|
||||
if resB.LogPath != "" {
|
||||
t.Fatalf("skipped task B should have empty log path, got %q", resB.LogPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunParallelTimeoutPropagation(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
origRun := runCodexTaskFn
|
||||
t.Cleanup(func() {
|
||||
runCodexTaskFn = origRun
|
||||
resetTestHooks()
|
||||
})
|
||||
|
||||
var receivedTimeout int
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
receivedTimeout = timeout
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 124, Error: "timeout"}
|
||||
}
|
||||
|
||||
t.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)
|
||||
}
|
||||
}
|
||||
4997
codeagent-wrapper/main_test.go
Normal file
4997
codeagent-wrapper/main_test.go
Normal file
File diff suppressed because it is too large
Load Diff
456
codeagent-wrapper/parser.go
Normal file
456
codeagent-wrapper/parser.go
Normal file
@@ -0,0 +1,456 @@
|
||||
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, nil)
|
||||
}
|
||||
|
||||
const (
|
||||
jsonLineReaderSize = 64 * 1024
|
||||
jsonLineMaxBytes = 10 * 1024 * 1024
|
||||
jsonLinePreviewBytes = 256
|
||||
)
|
||||
|
||||
// UnifiedEvent combines all backend event formats into a single structure
|
||||
// to avoid multiple JSON unmarshal operations per event
|
||||
type UnifiedEvent struct {
|
||||
// Common fields
|
||||
Type string `json:"type"`
|
||||
|
||||
// Codex-specific fields
|
||||
ThreadID string `json:"thread_id,omitempty"`
|
||||
Item json.RawMessage `json:"item,omitempty"` // Lazy parse
|
||||
|
||||
// Claude-specific fields
|
||||
Subtype string `json:"subtype,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
Result string `json:"result,omitempty"`
|
||||
|
||||
// Gemini-specific fields
|
||||
Role string `json:"role,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Delta *bool `json:"delta,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
// Opencode-specific fields (camelCase sessionID)
|
||||
OpencodeSessionID string `json:"sessionID,omitempty"`
|
||||
Part json.RawMessage `json:"part,omitempty"`
|
||||
}
|
||||
|
||||
// OpencodePart represents the part field in opencode events
|
||||
type OpencodePart struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
SessionID string `json:"sessionID,omitempty"`
|
||||
}
|
||||
|
||||
// ItemContent represents the parsed item.text field for Codex events
|
||||
type ItemContent struct {
|
||||
Type string `json:"type"`
|
||||
Text interface{} `json:"text"`
|
||||
}
|
||||
|
||||
func parseJSONStreamInternal(r io.Reader, warnFn func(string), infoFn func(string), onMessage func(), onComplete 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()
|
||||
}
|
||||
}
|
||||
|
||||
notifyComplete := func() {
|
||||
if onComplete != nil {
|
||||
onComplete()
|
||||
}
|
||||
}
|
||||
|
||||
totalEvents := 0
|
||||
|
||||
var (
|
||||
codexMessage string
|
||||
claudeMessage string
|
||||
geminiBuffer strings.Builder
|
||||
opencodeMessage 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
|
||||
}
|
||||
|
||||
// Single unmarshal for all backend types
|
||||
var event UnifiedEvent
|
||||
if err := json.Unmarshal(line, &event); err != nil {
|
||||
warnFn(fmt.Sprintf("Failed to parse event: %s", truncateBytes(line, 100)))
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect backend type by field presence
|
||||
isCodex := event.ThreadID != ""
|
||||
if !isCodex && len(event.Item) > 0 {
|
||||
var itemHeader struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if json.Unmarshal(event.Item, &itemHeader) == nil && itemHeader.Type != "" {
|
||||
isCodex = true
|
||||
}
|
||||
}
|
||||
// Codex-specific event types without thread_id or item
|
||||
if !isCodex && (event.Type == "turn.started" || event.Type == "turn.completed") {
|
||||
isCodex = true
|
||||
}
|
||||
isClaude := event.Subtype != "" || event.Result != ""
|
||||
if !isClaude && event.Type == "result" && event.SessionID != "" && event.Status == "" {
|
||||
isClaude = true
|
||||
}
|
||||
isGemini := (event.Type == "init" && event.SessionID != "") || event.Role != "" || event.Delta != nil || event.Status != ""
|
||||
isOpencode := event.OpencodeSessionID != "" && len(event.Part) > 0
|
||||
|
||||
// Handle Opencode events first (most specific detection)
|
||||
if isOpencode {
|
||||
if threadID == "" {
|
||||
threadID = event.OpencodeSessionID
|
||||
}
|
||||
|
||||
var part OpencodePart
|
||||
if err := json.Unmarshal(event.Part, &part); err != nil {
|
||||
warnFn(fmt.Sprintf("Failed to parse opencode part: %s", err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract sessionID from part if available
|
||||
if part.SessionID != "" && threadID == "" {
|
||||
threadID = part.SessionID
|
||||
}
|
||||
|
||||
infoFn(fmt.Sprintf("Parsed Opencode event #%d type=%s part_type=%s", totalEvents, event.Type, part.Type))
|
||||
|
||||
if event.Type == "text" && part.Text != "" {
|
||||
opencodeMessage.WriteString(part.Text)
|
||||
notifyMessage()
|
||||
}
|
||||
|
||||
if part.Type == "step-finish" && part.Reason == "stop" {
|
||||
notifyComplete()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle Codex events
|
||||
if isCodex {
|
||||
var details []string
|
||||
if event.ThreadID != "" {
|
||||
details = append(details, fmt.Sprintf("thread_id=%s", event.ThreadID))
|
||||
}
|
||||
|
||||
if len(details) > 0 {
|
||||
infoFn(fmt.Sprintf("Parsed event #%d type=%s (%s)", totalEvents, event.Type, strings.Join(details, ", ")))
|
||||
} else {
|
||||
infoFn(fmt.Sprintf("Parsed event #%d type=%s", totalEvents, event.Type))
|
||||
}
|
||||
|
||||
switch event.Type {
|
||||
case "thread.started":
|
||||
threadID = event.ThreadID
|
||||
infoFn(fmt.Sprintf("thread.started event thread_id=%s", threadID))
|
||||
|
||||
case "thread.completed":
|
||||
if event.ThreadID != "" && threadID == "" {
|
||||
threadID = event.ThreadID
|
||||
}
|
||||
infoFn(fmt.Sprintf("thread.completed event thread_id=%s", event.ThreadID))
|
||||
notifyComplete()
|
||||
|
||||
case "turn.completed":
|
||||
infoFn("turn.completed event")
|
||||
notifyComplete()
|
||||
|
||||
case "item.completed":
|
||||
var itemType string
|
||||
if len(event.Item) > 0 {
|
||||
var itemHeader struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if err := json.Unmarshal(event.Item, &itemHeader); err == nil {
|
||||
itemType = itemHeader.Type
|
||||
}
|
||||
}
|
||||
|
||||
if itemType == "agent_message" && len(event.Item) > 0 {
|
||||
// Lazy parse: only parse item content when needed
|
||||
var item ItemContent
|
||||
if err := json.Unmarshal(event.Item, &item); err == nil {
|
||||
normalized := normalizeText(item.Text)
|
||||
infoFn(fmt.Sprintf("item.completed event item_type=%s message_len=%d", itemType, len(normalized)))
|
||||
if normalized != "" {
|
||||
codexMessage = normalized
|
||||
notifyMessage()
|
||||
}
|
||||
} else {
|
||||
warnFn(fmt.Sprintf("Failed to parse item content: %s", err.Error()))
|
||||
}
|
||||
} else {
|
||||
infoFn(fmt.Sprintf("item.completed event item_type=%s", itemType))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle Claude events
|
||||
if isClaude {
|
||||
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()
|
||||
}
|
||||
|
||||
if event.Type == "result" {
|
||||
notifyComplete()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle Gemini events
|
||||
if isGemini {
|
||||
if event.SessionID != "" && threadID == "" {
|
||||
threadID = event.SessionID
|
||||
}
|
||||
|
||||
if event.Content != "" {
|
||||
geminiBuffer.WriteString(event.Content)
|
||||
}
|
||||
|
||||
if event.Status != "" {
|
||||
notifyMessage()
|
||||
|
||||
if event.Type == "result" && (event.Status == "success" || event.Status == "error" || event.Status == "complete" || event.Status == "failed") {
|
||||
notifyComplete()
|
||||
}
|
||||
}
|
||||
|
||||
delta := false
|
||||
if event.Delta != nil {
|
||||
delta = *event.Delta
|
||||
}
|
||||
|
||||
infoFn(fmt.Sprintf("Parsed Gemini event #%d type=%s role=%s delta=%t status=%s content_len=%d", totalEvents, event.Type, event.Role, delta, event.Status, len(event.Content)))
|
||||
continue
|
||||
}
|
||||
|
||||
// Unknown event format from other backends (turn.started/assistant/user); ignore.
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case opencodeMessage.Len() > 0:
|
||||
message = opencodeMessage.String()
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
50
codeagent-wrapper/parser_opencode_test.go
Normal file
50
codeagent-wrapper/parser_opencode_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseJSONStream_Opencode(t *testing.T) {
|
||||
input := `{"type":"step_start","timestamp":1768187730683,"sessionID":"ses_44fced3c7ffe83sZpzY1rlQka3","part":{"id":"prt_bb0339afa001NTqoJ2NS8x91zP","sessionID":"ses_44fced3c7ffe83sZpzY1rlQka3","messageID":"msg_bb033866f0011oZxTqvfy0TKtS","type":"step-start","snapshot":"904f0fd58c125b79e60f0993e38f9d9f6200bf47"}}
|
||||
{"type":"text","timestamp":1768187744432,"sessionID":"ses_44fced3c7ffe83sZpzY1rlQka3","part":{"id":"prt_bb0339cb5001QDd0Lh0PzFZpa3","sessionID":"ses_44fced3c7ffe83sZpzY1rlQka3","messageID":"msg_bb033866f0011oZxTqvfy0TKtS","type":"text","text":"Hello from opencode"}}
|
||||
{"type":"step_finish","timestamp":1768187744471,"sessionID":"ses_44fced3c7ffe83sZpzY1rlQka3","part":{"id":"prt_bb033d0af0019VRZzpO2OVW1na","sessionID":"ses_44fced3c7ffe83sZpzY1rlQka3","messageID":"msg_bb033866f0011oZxTqvfy0TKtS","type":"step-finish","reason":"stop","snapshot":"904f0fd58c125b79e60f0993e38f9d9f6200bf47","cost":0}}`
|
||||
|
||||
message, threadID := parseJSONStream(strings.NewReader(input))
|
||||
|
||||
if threadID != "ses_44fced3c7ffe83sZpzY1rlQka3" {
|
||||
t.Errorf("threadID = %q, want %q", threadID, "ses_44fced3c7ffe83sZpzY1rlQka3")
|
||||
}
|
||||
if message != "Hello from opencode" {
|
||||
t.Errorf("message = %q, want %q", message, "Hello from opencode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJSONStream_Opencode_MultipleTextEvents(t *testing.T) {
|
||||
input := `{"type":"text","sessionID":"ses_123","part":{"type":"text","text":"Part 1"}}
|
||||
{"type":"text","sessionID":"ses_123","part":{"type":"text","text":" Part 2"}}
|
||||
{"type":"step_finish","sessionID":"ses_123","part":{"type":"step-finish","reason":"stop"}}`
|
||||
|
||||
message, threadID := parseJSONStream(strings.NewReader(input))
|
||||
|
||||
if threadID != "ses_123" {
|
||||
t.Errorf("threadID = %q, want %q", threadID, "ses_123")
|
||||
}
|
||||
if message != "Part 1 Part 2" {
|
||||
t.Errorf("message = %q, want %q", message, "Part 1 Part 2")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJSONStream_Opencode_NoStopReason(t *testing.T) {
|
||||
input := `{"type":"text","sessionID":"ses_456","part":{"type":"text","text":"Content"}}
|
||||
{"type":"step_finish","sessionID":"ses_456","part":{"type":"step-finish","reason":"tool-calls"}}`
|
||||
|
||||
message, threadID := parseJSONStream(strings.NewReader(input))
|
||||
|
||||
if threadID != "ses_456" {
|
||||
t.Errorf("threadID = %q, want %q", threadID, "ses_456")
|
||||
}
|
||||
if message != "Content" {
|
||||
t.Errorf("message = %q, want %q", message, "Content")
|
||||
}
|
||||
}
|
||||
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, 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)
|
||||
}
|
||||
}
|
||||
32
codeagent-wrapper/parser_unknown_event_test.go
Normal file
32
codeagent-wrapper/parser_unknown_event_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBackendParseJSONStream_UnknownEventsAreSilent(t *testing.T) {
|
||||
input := strings.Join([]string{
|
||||
`{"type":"turn.started"}`,
|
||||
`{"type":"assistant","text":"hi"}`,
|
||||
`{"type":"user","text":"yo"}`,
|
||||
`{"type":"item.completed","item":{"type":"agent_message","text":"ok"}}`,
|
||||
}, "\n")
|
||||
|
||||
var infos []string
|
||||
infoFn := func(msg string) { infos = append(infos, msg) }
|
||||
|
||||
message, threadID := parseJSONStreamInternal(strings.NewReader(input), nil, infoFn, nil, nil)
|
||||
if message != "ok" {
|
||||
t.Fatalf("message=%q, want %q (infos=%v)", message, "ok", infos)
|
||||
}
|
||||
if threadID != "" {
|
||||
t.Fatalf("threadID=%q, want empty (infos=%v)", threadID, infos)
|
||||
}
|
||||
|
||||
for _, msg := range infos {
|
||||
if strings.Contains(msg, "Agent event:") {
|
||||
t.Fatalf("unexpected log for unknown event: %q", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
64
codeagent-wrapper/process_check_windows_test.go
Normal file
64
codeagent-wrapper/process_check_windows_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIsProcessRunning(t *testing.T) {
|
||||
t.Run("boundary values", func(t *testing.T) {
|
||||
if isProcessRunning(0) {
|
||||
t.Fatalf("expected pid 0 to be reported as not running")
|
||||
}
|
||||
if isProcessRunning(-1) {
|
||||
t.Fatalf("expected pid -1 to be reported as not running")
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetProcessStartTimeReadsProcStat(t *testing.T) {
|
||||
start := getProcessStartTime(os.Getpid())
|
||||
if start.IsZero() {
|
||||
t.Fatalf("expected non-zero start time for current process")
|
||||
}
|
||||
if start.After(time.Now().Add(5 * time.Second)) {
|
||||
t.Fatalf("start time is unexpectedly in the future: %v", start)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProcessStartTimeInvalidData(t *testing.T) {
|
||||
if !getProcessStartTime(0).IsZero() {
|
||||
t.Fatalf("expected zero time for pid 0")
|
||||
}
|
||||
if !getProcessStartTime(-1).IsZero() {
|
||||
t.Fatalf("expected zero time for negative pid")
|
||||
}
|
||||
if !getProcessStartTime(1 << 30).IsZero() {
|
||||
t.Fatalf("expected zero time for non-existent pid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBootTimeParsesBtime(t *testing.T) {
|
||||
t.Skip("getBootTime is only implemented on Unix-like systems")
|
||||
}
|
||||
|
||||
func TestGetBootTimeInvalidData(t *testing.T) {
|
||||
t.Skip("getBootTime is only implemented on Unix-like systems")
|
||||
}
|
||||
163
codeagent-wrapper/prompt_file_test.go
Normal file
163
codeagent-wrapper/prompt_file_test.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWrapTaskWithAgentPrompt(t *testing.T) {
|
||||
got := wrapTaskWithAgentPrompt("P", "do")
|
||||
want := "<agent-prompt>\nP\n</agent-prompt>\n\ndo"
|
||||
if got != want {
|
||||
t.Fatalf("wrapTaskWithAgentPrompt mismatch:\n got=%q\nwant=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_EmptyPath(t *testing.T) {
|
||||
for _, allowOutside := range []bool{false, true} {
|
||||
got, err := readAgentPromptFile(" ", allowOutside)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error (allowOutside=%v): %v", allowOutside, err)
|
||||
}
|
||||
if got != "" {
|
||||
t.Fatalf("expected empty result (allowOutside=%v), got %q", allowOutside, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_ExplicitAbsolutePath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "prompt.md")
|
||||
if err := os.WriteFile(path, []byte("LINE1\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
got, err := readAgentPromptFile(path, true)
|
||||
if err != nil {
|
||||
t.Fatalf("readAgentPromptFile error: %v", err)
|
||||
}
|
||||
if got != "LINE1" {
|
||||
t.Fatalf("got %q, want %q", got, "LINE1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_ExplicitTildeExpansion(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
path := filepath.Join(home, "prompt.md")
|
||||
if err := os.WriteFile(path, []byte("P\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
got, err := readAgentPromptFile("~/prompt.md", true)
|
||||
if err != nil {
|
||||
t.Fatalf("readAgentPromptFile error: %v", err)
|
||||
}
|
||||
if got != "P" {
|
||||
t.Fatalf("got %q, want %q", got, "P")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_RestrictedAllowsClaudeDir(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
claudeDir := filepath.Join(home, ".claude")
|
||||
if err := os.MkdirAll(claudeDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
path := filepath.Join(claudeDir, "prompt.md")
|
||||
if err := os.WriteFile(path, []byte("OK\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
got, err := readAgentPromptFile("~/.claude/prompt.md", false)
|
||||
if err != nil {
|
||||
t.Fatalf("readAgentPromptFile error: %v", err)
|
||||
}
|
||||
if got != "OK" {
|
||||
t.Fatalf("got %q, want %q", got, "OK")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_RestrictedRejectsOutsideClaudeDir(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
path := filepath.Join(home, "prompt.md")
|
||||
if err := os.WriteFile(path, []byte("NO\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
if _, err := readAgentPromptFile("~/prompt.md", false); err == nil {
|
||||
t.Fatalf("expected error for prompt file outside ~/.claude, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_RestrictedRejectsTraversal(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
path := filepath.Join(home, "secret.md")
|
||||
if err := os.WriteFile(path, []byte("SECRET\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
if _, err := readAgentPromptFile("~/.claude/../secret.md", false); err == nil {
|
||||
t.Fatalf("expected traversal to be rejected, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_NotFound(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
claudeDir := filepath.Join(home, ".claude")
|
||||
if err := os.MkdirAll(claudeDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
|
||||
_, err := readAgentPromptFile("~/.claude/missing.md", false)
|
||||
if err == nil || !os.IsNotExist(err) {
|
||||
t.Fatalf("expected not-exist error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_PermissionDenied(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("chmod-based permission test is not reliable on Windows")
|
||||
}
|
||||
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
claudeDir := filepath.Join(home, ".claude")
|
||||
if err := os.MkdirAll(claudeDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
path := filepath.Join(claudeDir, "private.md")
|
||||
if err := os.WriteFile(path, []byte("PRIVATE\n"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
if err := os.Chmod(path, 0o000); err != nil {
|
||||
t.Fatalf("Chmod: %v", err)
|
||||
}
|
||||
|
||||
_, err := readAgentPromptFile("~/.claude/private.md", false)
|
||||
if err == nil {
|
||||
t.Fatalf("expected permission error, got nil")
|
||||
}
|
||||
if !os.IsPermission(err) && !strings.Contains(strings.ToLower(err.Error()), "permission") {
|
||||
t.Fatalf("expected permission denied, got: %v", err)
|
||||
}
|
||||
}
|
||||
16
codeagent-wrapper/signal_unix.go
Normal file
16
codeagent-wrapper/signal_unix.go
Normal file
@@ -0,0 +1,16 @@
|
||||
//go:build unix || darwin || linux
|
||||
// +build unix darwin linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// sendTermSignal sends SIGTERM for graceful shutdown on Unix.
|
||||
func sendTermSignal(proc processHandle) error {
|
||||
if proc == nil {
|
||||
return nil
|
||||
}
|
||||
return proc.Signal(syscall.SIGTERM)
|
||||
}
|
||||
87
codeagent-wrapper/signal_windows.go
Normal file
87
codeagent-wrapper/signal_windows.go
Normal file
@@ -0,0 +1,87 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// sendTermSignal on Windows directly kills the process.
|
||||
// SIGTERM is not supported on Windows.
|
||||
func sendTermSignal(proc processHandle) error {
|
||||
if proc == nil {
|
||||
return nil
|
||||
}
|
||||
pid := proc.Pid()
|
||||
if pid > 0 {
|
||||
// Kill the whole process tree to avoid leaving inheriting child processes around.
|
||||
// This also helps prevent exec.Cmd.Wait() from blocking on stderr/stdout pipes held open by children.
|
||||
taskkill := "taskkill"
|
||||
if root := os.Getenv("SystemRoot"); root != "" {
|
||||
taskkill = filepath.Join(root, "System32", "taskkill.exe")
|
||||
}
|
||||
cmd := exec.Command(taskkill, "/PID", strconv.Itoa(pid), "/T", "/F")
|
||||
cmd.Stdout = io.Discard
|
||||
cmd.Stderr = io.Discard
|
||||
if err := cmd.Run(); err == nil {
|
||||
return nil
|
||||
}
|
||||
if err := killProcessTree(pid); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return proc.Kill()
|
||||
}
|
||||
|
||||
func killProcessTree(pid int) error {
|
||||
if pid <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
wmic := "wmic"
|
||||
if root := os.Getenv("SystemRoot"); root != "" {
|
||||
wmic = filepath.Join(root, "System32", "wbem", "WMIC.exe")
|
||||
}
|
||||
|
||||
queryChildren := "(ParentProcessId=" + strconv.Itoa(pid) + ")"
|
||||
listCmd := exec.Command(wmic, "process", "where", queryChildren, "get", "ProcessId", "/VALUE")
|
||||
listCmd.Stderr = io.Discard
|
||||
out, err := listCmd.Output()
|
||||
if err == nil {
|
||||
for _, childPID := range parseWMICPIDs(out) {
|
||||
_ = killProcessTree(childPID)
|
||||
}
|
||||
}
|
||||
|
||||
querySelf := "(ProcessId=" + strconv.Itoa(pid) + ")"
|
||||
termCmd := exec.Command(wmic, "process", "where", querySelf, "call", "terminate")
|
||||
termCmd.Stdout = io.Discard
|
||||
termCmd.Stderr = io.Discard
|
||||
if termErr := termCmd.Run(); termErr != nil && err == nil {
|
||||
err = termErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func parseWMICPIDs(out []byte) []int {
|
||||
const prefix = "ProcessId="
|
||||
var pids []int
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, prefix) {
|
||||
continue
|
||||
}
|
||||
n, err := strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(line, prefix)))
|
||||
if err != nil || n <= 0 {
|
||||
continue
|
||||
}
|
||||
pids = append(pids, n)
|
||||
}
|
||||
return pids
|
||||
}
|
||||
682
codeagent-wrapper/utils.go
Normal file
682
codeagent-wrapper/utils.go
Normal file
@@ -0,0 +1,682 @@
|
||||
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] + "..."
|
||||
}
|
||||
|
||||
// safeTruncate safely truncates string to maxLen, avoiding panic and UTF-8 corruption.
|
||||
func safeTruncate(s string, maxLen int) string {
|
||||
if maxLen <= 0 || s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
runes := []rune(s)
|
||||
if len(runes) <= maxLen {
|
||||
return s
|
||||
}
|
||||
|
||||
if maxLen < 4 {
|
||||
return string(runes[:1])
|
||||
}
|
||||
|
||||
cutoff := maxLen - 3
|
||||
if cutoff <= 0 {
|
||||
return string(runes[:1])
|
||||
}
|
||||
if len(runes) <= cutoff {
|
||||
return s
|
||||
}
|
||||
return string(runes[:cutoff]) + "..."
|
||||
}
|
||||
|
||||
// sanitizeOutput removes ANSI escape sequences and control characters.
|
||||
func sanitizeOutput(s string) string {
|
||||
var result strings.Builder
|
||||
inEscape := false
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\x1b' && i+1 < len(s) && s[i+1] == '[' {
|
||||
inEscape = true
|
||||
i++ // skip '['
|
||||
continue
|
||||
}
|
||||
if inEscape {
|
||||
if (s[i] >= 'A' && s[i] <= 'Z') || (s[i] >= 'a' && s[i] <= 'z') {
|
||||
inEscape = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Keep printable chars and common whitespace.
|
||||
if s[i] >= 32 || s[i] == '\n' || s[i] == '\t' {
|
||||
result.WriteByte(s[i])
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func hello() string {
|
||||
return "hello world"
|
||||
}
|
||||
|
||||
func greet(name string) string {
|
||||
return "hello " + name
|
||||
}
|
||||
|
||||
func farewell(name string) string {
|
||||
return "goodbye " + name
|
||||
}
|
||||
|
||||
// extractCoverageFromLines extracts coverage from pre-split lines.
|
||||
func extractCoverageFromLines(lines []string) string {
|
||||
if len(lines) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
end := len(lines)
|
||||
for end > 0 && strings.TrimSpace(lines[end-1]) == "" {
|
||||
end--
|
||||
}
|
||||
|
||||
if end == 1 {
|
||||
trimmed := strings.TrimSpace(lines[0])
|
||||
if strings.HasSuffix(trimmed, "%") {
|
||||
if num, err := strconv.ParseFloat(strings.TrimSuffix(trimmed, "%"), 64); err == nil && num >= 0 && num <= 100 {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coverageKeywords := []string{"file", "stmt", "branch", "line", "coverage", "total"}
|
||||
|
||||
for _, line := range lines[:end] {
|
||||
lower := strings.ToLower(line)
|
||||
|
||||
hasKeyword := false
|
||||
tokens := strings.FieldsFunc(lower, func(r rune) bool { return r < 'a' || r > 'z' })
|
||||
for _, token := range tokens {
|
||||
for _, kw := range coverageKeywords {
|
||||
if strings.HasPrefix(token, kw) {
|
||||
hasKeyword = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasKeyword {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasKeyword {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(line, "%") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract percentage pattern: number followed by %
|
||||
for i := 0; i < len(line); i++ {
|
||||
if line[i] == '%' && i > 0 {
|
||||
// Walk back to find the number
|
||||
j := i - 1
|
||||
for j >= 0 && (line[j] == '.' || (line[j] >= '0' && line[j] <= '9')) {
|
||||
j--
|
||||
}
|
||||
if j < i-1 {
|
||||
numStr := line[j+1 : i]
|
||||
// Validate it's a reasonable percentage
|
||||
if num, err := strconv.ParseFloat(numStr, 64); err == nil && num >= 0 && num <= 100 {
|
||||
return numStr + "%"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractCoverage extracts coverage percentage from task output
|
||||
// Supports common formats: "Coverage: 92%", "92% coverage", "coverage 92%", "TOTAL 92%"
|
||||
func extractCoverage(message string) string {
|
||||
if message == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return extractCoverageFromLines(strings.Split(message, "\n"))
|
||||
}
|
||||
|
||||
// extractCoverageNum extracts coverage as a numeric value for comparison
|
||||
func extractCoverageNum(coverage string) float64 {
|
||||
if coverage == "" {
|
||||
return 0
|
||||
}
|
||||
// Remove % sign and parse
|
||||
numStr := strings.TrimSuffix(coverage, "%")
|
||||
if num, err := strconv.ParseFloat(numStr, 64); err == nil {
|
||||
return num
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// extractFilesChangedFromLines extracts files from pre-split lines.
|
||||
func extractFilesChangedFromLines(lines []string) []string {
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var files []string
|
||||
seen := make(map[string]bool)
|
||||
exts := []string{".ts", ".tsx", ".js", ".jsx", ".go", ".py", ".rs", ".java", ".vue", ".css", ".scss", ".md", ".json", ".yaml", ".yml", ".toml"}
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Pattern 1: "Modified: path/to/file.ts" or "Created: path/to/file.ts"
|
||||
matchedPrefix := false
|
||||
for _, prefix := range []string{"Modified:", "Created:", "Updated:", "Edited:", "Wrote:", "Changed:"} {
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
file := strings.TrimSpace(strings.TrimPrefix(line, prefix))
|
||||
file = strings.Trim(file, "`,\"'()[],:")
|
||||
file = strings.TrimPrefix(file, "@")
|
||||
if file != "" && !seen[file] {
|
||||
files = append(files, file)
|
||||
seen[file] = true
|
||||
}
|
||||
matchedPrefix = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matchedPrefix {
|
||||
continue
|
||||
}
|
||||
|
||||
// Pattern 2: Tokens that look like file paths (allow root files, strip @ prefix).
|
||||
parts := strings.Fields(line)
|
||||
for _, part := range parts {
|
||||
part = strings.Trim(part, "`,\"'()[],:")
|
||||
part = strings.TrimPrefix(part, "@")
|
||||
for _, ext := range exts {
|
||||
if strings.HasSuffix(part, ext) && !seen[part] {
|
||||
files = append(files, part)
|
||||
seen[part] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Limit to first 10 files to avoid bloat
|
||||
if len(files) > 10 {
|
||||
files = files[:10]
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// extractFilesChanged extracts list of changed files from task output
|
||||
// Looks for common patterns like "Modified: file.ts", "Created: file.ts", file paths in output
|
||||
func extractFilesChanged(message string) []string {
|
||||
if message == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return extractFilesChangedFromLines(strings.Split(message, "\n"))
|
||||
}
|
||||
|
||||
// extractTestResultsFromLines extracts test results from pre-split lines.
|
||||
func extractTestResultsFromLines(lines []string) (passed, failed int) {
|
||||
if len(lines) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// Common patterns:
|
||||
// pytest: "12 passed, 2 failed"
|
||||
// jest: "Tests: 2 failed, 12 passed"
|
||||
// go: "ok ... 12 tests"
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.ToLower(line)
|
||||
|
||||
// Look for test result lines
|
||||
if !strings.Contains(line, "pass") && !strings.Contains(line, "fail") && !strings.Contains(line, "test") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract numbers near "passed" or "pass"
|
||||
if idx := strings.Index(line, "pass"); idx != -1 {
|
||||
// Look for number before "pass"
|
||||
num := extractNumberBefore(line, idx)
|
||||
if num > 0 {
|
||||
passed = num
|
||||
}
|
||||
}
|
||||
|
||||
// Extract numbers near "failed" or "fail"
|
||||
if idx := strings.Index(line, "fail"); idx != -1 {
|
||||
num := extractNumberBefore(line, idx)
|
||||
if num > 0 {
|
||||
failed = num
|
||||
}
|
||||
}
|
||||
|
||||
// go test style: "ok ... 12 tests"
|
||||
if passed == 0 {
|
||||
if idx := strings.Index(line, "test"); idx != -1 {
|
||||
num := extractNumberBefore(line, idx)
|
||||
if num > 0 {
|
||||
passed = num
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we found both, stop
|
||||
if passed > 0 && failed > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return passed, failed
|
||||
}
|
||||
|
||||
// extractTestResults extracts test pass/fail counts from task output
|
||||
func extractTestResults(message string) (passed, failed int) {
|
||||
if message == "" {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
return extractTestResultsFromLines(strings.Split(message, "\n"))
|
||||
}
|
||||
|
||||
// extractNumberBefore extracts a number that appears before the given index
|
||||
func extractNumberBefore(s string, idx int) int {
|
||||
if idx <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Walk backwards to find digits
|
||||
end := idx - 1
|
||||
for end >= 0 && (s[end] == ' ' || s[end] == ':' || s[end] == ',') {
|
||||
end--
|
||||
}
|
||||
if end < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
start := end
|
||||
for start >= 0 && s[start] >= '0' && s[start] <= '9' {
|
||||
start--
|
||||
}
|
||||
start++
|
||||
|
||||
if start > end {
|
||||
return 0
|
||||
}
|
||||
|
||||
numStr := s[start : end+1]
|
||||
if num, err := strconv.Atoi(numStr); err == nil {
|
||||
return num
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// extractKeyOutputFromLines extracts key output from pre-split lines.
|
||||
func extractKeyOutputFromLines(lines []string, maxLen int) string {
|
||||
if len(lines) == 0 || maxLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Priority 1: Look for explicit summary lines
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
lower := strings.ToLower(line)
|
||||
if strings.HasPrefix(lower, "summary:") || strings.HasPrefix(lower, "completed:") ||
|
||||
strings.HasPrefix(lower, "implemented:") || strings.HasPrefix(lower, "added:") ||
|
||||
strings.HasPrefix(lower, "created:") || strings.HasPrefix(lower, "fixed:") {
|
||||
content := line
|
||||
for _, prefix := range []string{"Summary:", "Completed:", "Implemented:", "Added:", "Created:", "Fixed:",
|
||||
"summary:", "completed:", "implemented:", "added:", "created:", "fixed:"} {
|
||||
content = strings.TrimPrefix(content, prefix)
|
||||
}
|
||||
content = strings.TrimSpace(content)
|
||||
if len(content) > 0 {
|
||||
return safeTruncate(content, maxLen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: First meaningful line (skip noise)
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "```") || strings.HasPrefix(line, "---") ||
|
||||
strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
|
||||
continue
|
||||
}
|
||||
// Skip very short lines (likely headers or markers)
|
||||
if len(line) < 20 {
|
||||
continue
|
||||
}
|
||||
return safeTruncate(line, maxLen)
|
||||
}
|
||||
|
||||
// Fallback: truncate entire message
|
||||
clean := strings.TrimSpace(strings.Join(lines, "\n"))
|
||||
return safeTruncate(clean, maxLen)
|
||||
}
|
||||
|
||||
// extractCoverageGap extracts what's missing from coverage reports
|
||||
// Looks for uncovered lines, branches, or functions
|
||||
func extractCoverageGap(message string) string {
|
||||
if message == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
lower := strings.ToLower(message)
|
||||
lines := strings.Split(message, "\n")
|
||||
|
||||
// Look for uncovered/missing patterns
|
||||
for _, line := range lines {
|
||||
lineLower := strings.ToLower(line)
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Common patterns for uncovered code
|
||||
if strings.Contains(lineLower, "uncovered") ||
|
||||
strings.Contains(lineLower, "not covered") ||
|
||||
strings.Contains(lineLower, "missing coverage") ||
|
||||
strings.Contains(lineLower, "lines not covered") {
|
||||
if len(line) > 100 {
|
||||
return line[:97] + "..."
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// Look for specific file:line patterns in coverage reports
|
||||
if strings.Contains(lineLower, "branch") && strings.Contains(lineLower, "not taken") {
|
||||
if len(line) > 100 {
|
||||
return line[:97] + "..."
|
||||
}
|
||||
return line
|
||||
}
|
||||
}
|
||||
|
||||
// Look for function names that aren't covered
|
||||
if strings.Contains(lower, "function") && strings.Contains(lower, "0%") {
|
||||
for _, line := range lines {
|
||||
if strings.Contains(strings.ToLower(line), "0%") && strings.Contains(line, "function") {
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) > 100 {
|
||||
return line[:97] + "..."
|
||||
}
|
||||
return line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractErrorDetail extracts meaningful error context from task output
|
||||
// Returns the most relevant error information up to maxLen characters
|
||||
func extractErrorDetail(message string, maxLen int) string {
|
||||
if message == "" || maxLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := strings.Split(message, "\n")
|
||||
var errorLines []string
|
||||
|
||||
// Look for error-related lines
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
lower := strings.ToLower(line)
|
||||
|
||||
// Skip noise lines
|
||||
if strings.HasPrefix(line, "at ") && strings.Contains(line, "(") {
|
||||
// Stack trace line - only keep first one
|
||||
if len(errorLines) > 0 && strings.HasPrefix(strings.ToLower(errorLines[len(errorLines)-1]), "at ") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Prioritize error/fail lines
|
||||
if strings.Contains(lower, "error") ||
|
||||
strings.Contains(lower, "fail") ||
|
||||
strings.Contains(lower, "exception") ||
|
||||
strings.Contains(lower, "assert") ||
|
||||
strings.Contains(lower, "expected") ||
|
||||
strings.Contains(lower, "timeout") ||
|
||||
strings.Contains(lower, "not found") ||
|
||||
strings.Contains(lower, "cannot") ||
|
||||
strings.Contains(lower, "undefined") ||
|
||||
strings.HasPrefix(line, "FAIL") ||
|
||||
strings.HasPrefix(line, "●") {
|
||||
errorLines = append(errorLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errorLines) == 0 {
|
||||
// No specific error lines found, take last few lines
|
||||
start := len(lines) - 5
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
for _, line := range lines[start:] {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
errorLines = append(errorLines, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Join and truncate
|
||||
result := strings.Join(errorLines, " | ")
|
||||
return safeTruncate(result, maxLen)
|
||||
}
|
||||
143
codeagent-wrapper/utils_test.go
Normal file
143
codeagent-wrapper/utils_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractCoverage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"bare int", "92%", "92%"},
|
||||
{"bare float", "92.5%", "92.5%"},
|
||||
{"coverage prefix", "coverage: 92%", "92%"},
|
||||
{"total prefix", "TOTAL 92%", "92%"},
|
||||
{"all files", "All files 92%", "92%"},
|
||||
{"empty", "", ""},
|
||||
{"no number", "coverage: N/A", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := extractCoverage(tt.in); got != tt.want {
|
||||
t.Fatalf("extractCoverage(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTestResults(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
wantPassed int
|
||||
wantFailed int
|
||||
}{
|
||||
{"pytest one line", "12 passed, 2 failed", 12, 2},
|
||||
{"pytest split lines", "12 passed\n2 failed", 12, 2},
|
||||
{"jest format", "Tests: 2 failed, 12 passed, 14 total", 12, 2},
|
||||
{"go test style count", "ok\texample.com/foo\t0.12s\t12 tests", 12, 0},
|
||||
{"zero counts", "0 passed, 0 failed", 0, 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
passed, failed := extractTestResults(tt.in)
|
||||
if passed != tt.wantPassed || failed != tt.wantFailed {
|
||||
t.Fatalf("extractTestResults(%q) = (%d, %d), want (%d, %d)", tt.in, passed, failed, tt.wantPassed, tt.wantFailed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFilesChanged(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want []string
|
||||
}{
|
||||
{"root file", "Modified: main.go\n", []string{"main.go"}},
|
||||
{"path file", "Created: codeagent-wrapper/utils.go\n", []string{"codeagent-wrapper/utils.go"}},
|
||||
{"at prefix", "Updated: @codeagent-wrapper/main.go\n", []string{"codeagent-wrapper/main.go"}},
|
||||
{"token scan", "Files: @main.go, @codeagent-wrapper/utils.go\n", []string{"main.go", "codeagent-wrapper/utils.go"}},
|
||||
{"space path", "Modified: dir/with space/file.go\n", []string{"dir/with space/file.go"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := extractFilesChanged(tt.in); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("extractFilesChanged(%q) = %#v, want %#v", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("limits to first 10", func(t *testing.T) {
|
||||
var b strings.Builder
|
||||
for i := 0; i < 12; i++ {
|
||||
fmt.Fprintf(&b, "Modified: file%d.go\n", i)
|
||||
}
|
||||
got := extractFilesChanged(b.String())
|
||||
if len(got) != 10 {
|
||||
t.Fatalf("len(files)=%d, want 10: %#v", len(got), got)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
want := fmt.Sprintf("file%d.go", i)
|
||||
if got[i] != want {
|
||||
t.Fatalf("files[%d]=%q, want %q", i, got[i], want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSafeTruncate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
maxLen int
|
||||
want string
|
||||
}{
|
||||
{"empty", "", 4, ""},
|
||||
{"zero maxLen", "hello", 0, ""},
|
||||
{"one rune", "你好", 1, "你"},
|
||||
{"two runes no truncate", "你好", 2, "你好"},
|
||||
{"three runes no truncate", "你好", 3, "你好"},
|
||||
{"two runes truncates long", "你好世界", 2, "你"},
|
||||
{"three runes truncates long", "你好世界", 3, "你"},
|
||||
{"four with ellipsis", "你好世界啊", 4, "你..."},
|
||||
{"emoji", "🙂🙂🙂🙂🙂", 4, "🙂..."},
|
||||
{"no truncate", "你好世界", 4, "你好世界"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := safeTruncate(tt.in, tt.maxLen); got != tt.want {
|
||||
t.Fatalf("safeTruncate(%q, %d) = %q, want %q", tt.in, tt.maxLen, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"ansi", "\x1b[31mred\x1b[0m", "red"},
|
||||
{"control chars", "a\x07b\r\nc\t", "ab\nc\t"},
|
||||
{"normal", "hello\nworld\t!", "hello\nworld\t!"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := sanitizeOutput(tt.in); got != tt.want {
|
||||
t.Fatalf("sanitizeOutput(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
173
config.json
Normal file
173
config.json
Normal file
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"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": false,
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"omo": {
|
||||
"enabled": false,
|
||||
"description": "OmO multi-agent orchestration with Sisyphus coordinator",
|
||||
"operations": [
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/SKILL.md",
|
||||
"target": "skills/omo/SKILL.md",
|
||||
"description": "Install omo skill"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/references/oracle.md",
|
||||
"target": "skills/omo/references/oracle.md",
|
||||
"description": "Install oracle agent prompt"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/references/librarian.md",
|
||||
"target": "skills/omo/references/librarian.md",
|
||||
"description": "Install librarian agent prompt"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/references/explore.md",
|
||||
"target": "skills/omo/references/explore.md",
|
||||
"description": "Install explore agent prompt"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/references/frontend-ui-ux-engineer.md",
|
||||
"target": "skills/omo/references/frontend-ui-ux-engineer.md",
|
||||
"description": "Install frontend-ui-ux-engineer agent prompt"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/references/document-writer.md",
|
||||
"target": "skills/omo/references/document-writer.md",
|
||||
"description": "Install document-writer agent prompt"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/references/develop.md",
|
||||
"target": "skills/omo/references/develop.md",
|
||||
"description": "Install develop agent prompt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sparv": {
|
||||
"enabled": false,
|
||||
"description": "SPARV workflow (Specify→Plan→Act→Review→Vault) with 10-point gate",
|
||||
"operations": [
|
||||
{
|
||||
"type": "copy_dir",
|
||||
"source": "skills/sparv",
|
||||
"target": "skills/sparv",
|
||||
"description": "Install sparv skill with all scripts and hooks"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
dev-workflow/.claude-plugin/plugin.json
Normal file
9
dev-workflow/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "dev",
|
||||
"description": "Lightweight development workflow with requirements clarification, parallel codex execution, and mandatory 90% test coverage",
|
||||
"version": "5.6.1",
|
||||
"author": {
|
||||
"name": "cexll",
|
||||
"email": "cexll@cexll.com"
|
||||
}
|
||||
}
|
||||
192
dev-workflow/README.md
Normal file
192
dev-workflow/README.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# /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 (backend selection)
|
||||
↓
|
||||
AskUserQuestion (requirements clarification)
|
||||
↓
|
||||
codeagent analysis (plan mode + task typing + UI auto-detection)
|
||||
↓
|
||||
dev-plan-generator (create dev doc)
|
||||
↓
|
||||
codeagent concurrent development (2–5 tasks, backend routing)
|
||||
↓
|
||||
codeagent testing & verification (≥90% coverage)
|
||||
↓
|
||||
Done (generate summary)
|
||||
```
|
||||
|
||||
## Step 0 + The 6 Steps
|
||||
|
||||
### 0. Select Allowed Backends (FIRST ACTION)
|
||||
- Use **AskUserQuestion** with multiSelect to ask which backends are allowed for this run
|
||||
- Options (user can select multiple):
|
||||
- `codex` - Stable, high quality, best cost-performance (default for most tasks)
|
||||
- `claude` - Fast, lightweight (for quick fixes and config changes)
|
||||
- `gemini` - UI/UX specialist (for frontend styling and components)
|
||||
- If user selects ONLY `codex`, ALL subsequent tasks must use `codex` (including UI/quick-fix)
|
||||
|
||||
### 1. Clarify Requirements
|
||||
- Use **AskUserQuestion** to ask the user directly
|
||||
- No scoring system, no complex logic
|
||||
- 2–3 rounds of Q&A until the requirement is clear
|
||||
|
||||
### 2. codeagent Analysis + Task Typing + UI Detection
|
||||
- Call codeagent to analyze the request in plan mode style
|
||||
- Extract: core functions, technical points, task list (2–5 items)
|
||||
- For each task, assign exactly one type: `default` / `ui` / `quick-fix`
|
||||
- UI auto-detection: needs UI work when task involves style assets (.css, .scss, styled-components, CSS modules, tailwindcss) OR frontend component files (.tsx, .jsx, .vue); output yes/no plus evidence
|
||||
|
||||
### 3. Generate Dev Doc
|
||||
- Call the **dev-plan-generator** agent
|
||||
- Produce a single `dev-plan.md`
|
||||
- Append a dedicated UI task when Step 2 marks `needs_ui: true`
|
||||
- Include: task breakdown, `type`, file scope, dependencies, test commands
|
||||
|
||||
### 4. Concurrent Development
|
||||
- Work from the task list in dev-plan.md
|
||||
- Route backend per task type (with user constraints + fallback):
|
||||
- `default` → `codex`
|
||||
- `ui` → `gemini` (enforced when allowed)
|
||||
- `quick-fix` → `claude`
|
||||
- Missing `type` → treat as `default`
|
||||
- If the preferred backend is not allowed, fallback to an allowed backend by priority: `codex` → `claude` → `gemini`
|
||||
- Independent tasks → run in parallel
|
||||
- Conflicting tasks → run serially
|
||||
|
||||
### 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 CLI flags required; workflow starts with an interactive backend selection.
|
||||
|
||||
## 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` / `claude` / `gemini`
|
||||
- **dev-plan-generator agent**: generate dev doc (subagent via Task tool, saves context)
|
||||
|
||||
## Backend Selection & Routing
|
||||
- **Step 0**: user selects allowed backends; if `仅 codex`, all tasks use codex
|
||||
- **UI detection standard**: style files (.css, .scss, styled-components, CSS modules, tailwindcss) OR frontend component code (.tsx, .jsx, .vue) trigger `needs_ui: true`
|
||||
- **Task type field**: each task in `dev-plan.md` must have `type: default|ui|quick-fix`
|
||||
- **Routing**: `default`→codex, `ui`→gemini, `quick-fix`→claude; if disallowed, fallback to an allowed backend by priority: codex→claude→gemini
|
||||
|
||||
## Key Features
|
||||
|
||||
### ✅ 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
|
||||
- Tasks split based on natural functional boundaries
|
||||
- Auto-detect dependencies and conflicts
|
||||
- codeagent executes independently with optimal backend
|
||||
|
||||
### ✅ 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 0: Select backends
|
||||
Q: Which backends are allowed? (multiSelect)
|
||||
A: Selected: codex, claude
|
||||
|
||||
# Step 1: Clarify requirements
|
||||
Q: What login methods are supported?
|
||||
A: Email + password
|
||||
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 (type=default)
|
||||
- Task 2: Password hashing (type=default)
|
||||
- Task 3: Frontend form (type=ui)
|
||||
UI detection: needs_ui = true (tailwindcss classes in frontend form)
|
||||
|
||||
# Step 3: Generate doc
|
||||
dev-plan.md generated with typed tasks ✓
|
||||
|
||||
# Step 4-5: Concurrent development (routing + fallback)
|
||||
[task-1] Backend API (codex) → tests → 92% ✓
|
||||
[task-2] Password hashing (codex) → tests → 95% ✓
|
||||
[task-3] Frontend form (fallback to codex; gemini not allowed) → 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.
|
||||
124
dev-workflow/agents/dev-plan-generator.md
Normal file
124
dev-workflow/agents/dev-plan-generator.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
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 codeagent 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 codeagent 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, and task typing hints)
|
||||
- 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
|
||||
- **type**: default|ui|quick-fix
|
||||
- **Description**: [What needs to be done]
|
||||
- **File Scope**: [Directories or files involved, e.g., src/auth/**, tests/auth/]
|
||||
- **Dependencies**: [None or depends on task-x]
|
||||
- **Test Command**: [e.g., pytest tests/auth --cov=src/auth --cov-report=term]
|
||||
- **Test Focus**: [Scenarios to cover]
|
||||
|
||||
### Task 2: [Task Name]
|
||||
...
|
||||
|
||||
(Tasks based on natural functional boundaries, typically 2-5)
|
||||
|
||||
## 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 tasks based on natural functional boundaries (no artificial limits)
|
||||
- Typical range: 2-5 tasks
|
||||
- Quality over quantity: prefer fewer well-scoped tasks over excessive fragmentation
|
||||
- Each task should be independently completable by one agent
|
||||
2. **Task Requirements**: Each task MUST include:
|
||||
- Clear ID (task-1, task-2, etc.)
|
||||
- A single task type field: `type: default|ui|quick-fix`
|
||||
- Specific description of what needs to be done
|
||||
- Explicit file scope (directories or files affected)
|
||||
- Dependency declaration ("None" or "depends on task-x")
|
||||
- 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` and any task typing hints)
|
||||
2. **Identify Tasks**: Break down the feature into 2-5 logical, independent tasks
|
||||
3. **Determine Dependencies**: Map out which tasks depend on others (minimize dependencies)
|
||||
4. **Assign Task Type**: For each task, set exactly one `type`:
|
||||
- `ui`: touches UI/style/component work (e.g., .css/.scss/.tsx/.jsx/.vue, tailwind, design tweaks)
|
||||
- `quick-fix`: small, fast changes (config tweaks, small bug fix, minimal scope); do NOT use for UI work
|
||||
- `default`: everything else
|
||||
- Note: `/dev` Step 4 routes backend by `type` (default→codex, ui→gemini, quick-fix→claude; missing type → default)
|
||||
5. **Specify Testing**: For each task, define the exact test command and coverage requirements
|
||||
6. **Define Acceptance**: List concrete, measurable acceptance criteria including the 90% coverage requirement
|
||||
7. **Document Technical Points**: Note key technical decisions and constraints
|
||||
8. **Write File**: Use the Write tool to create `./.claude/specs/{feature_name}/dev-plan.md`
|
||||
|
||||
## Quality Checks Before Writing
|
||||
|
||||
- [ ] Task count is between 2-5
|
||||
- [ ] Every task has all required fields (ID, type, Description, File Scope, Dependencies, Test Command, Test Focus)
|
||||
- [ ] Test commands include coverage parameters
|
||||
- [ ] Dependencies are explicitly stated
|
||||
- [ ] Acceptance criteria includes 90% coverage requirement
|
||||
- [ ] 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.
|
||||
213
dev-workflow/commands/dev.md
Normal file
213
dev-workflow/commands/dev.md
Normal file
@@ -0,0 +1,213 @@
|
||||
---
|
||||
description: Extreme lightweight end-to-end development workflow with requirements clarification, intelligent backend selection, parallel codeagent execution, and mandatory 90% test coverage
|
||||
---
|
||||
|
||||
You are the /dev Workflow Orchestrator, an expert development workflow manager specializing in orchestrating minimal, efficient end-to-end development processes with parallel task execution and rigorous test coverage validation.
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL CONSTRAINTS (NEVER VIOLATE)
|
||||
|
||||
These rules have HIGHEST PRIORITY and override all other instructions:
|
||||
|
||||
1. **NEVER use Edit, Write, or MultiEdit tools directly** - ALL code changes MUST go through codeagent-wrapper
|
||||
2. **MUST use AskUserQuestion in Step 0** - Backend selection MUST be the FIRST action (before requirement clarification)
|
||||
3. **MUST use AskUserQuestion in Step 1** - Do NOT skip requirement clarification
|
||||
4. **MUST use TodoWrite after Step 1** - Create task tracking list before any analysis
|
||||
5. **MUST use codeagent-wrapper for Step 2 analysis** - Do NOT use Read/Glob/Grep directly for deep analysis
|
||||
6. **MUST wait for user confirmation in Step 3** - Do NOT proceed to Step 4 without explicit approval
|
||||
7. **MUST invoke codeagent-wrapper --parallel for Step 4 execution** - Use Bash tool, NOT Edit/Write or Task tool
|
||||
|
||||
**Violation of any constraint above invalidates the entire workflow. Stop and restart if violated.**
|
||||
|
||||
---
|
||||
|
||||
**Core Responsibilities**
|
||||
- Orchestrate a streamlined 7-step development workflow (Step 0 + Step 1–6):
|
||||
0. Backend selection (user constrained)
|
||||
1. Requirement clarification through targeted questioning
|
||||
2. Technical analysis using codeagent-wrapper
|
||||
3. Development documentation generation
|
||||
4. Parallel development execution (backend routing per task type)
|
||||
5. Coverage validation (≥90% requirement)
|
||||
6. Completion summary
|
||||
|
||||
**Workflow Execution**
|
||||
- **Step 0: Backend Selection [MANDATORY - FIRST ACTION]**
|
||||
- MUST use AskUserQuestion tool as the FIRST action with multiSelect enabled
|
||||
- Ask which backends are allowed for this /dev run
|
||||
- Options (user can select multiple):
|
||||
- `codex` - Stable, high quality, best cost-performance (default for most tasks)
|
||||
- `claude` - Fast, lightweight (for quick fixes and config changes)
|
||||
- `gemini` - UI/UX specialist (for frontend styling and components)
|
||||
- Store the selected backends as `allowed_backends` set for routing in Step 4
|
||||
- Special rule: if user selects ONLY `codex`, then ALL subsequent tasks (including UI/quick-fix) MUST use `codex` (no exceptions)
|
||||
|
||||
- **Step 1: Requirement Clarification [MANDATORY - DO NOT SKIP]**
|
||||
- MUST use AskUserQuestion tool
|
||||
- Focus questions on functional boundaries, inputs/outputs, constraints, testing, and required unit-test coverage levels
|
||||
- Iterate 2-3 rounds until clear; rely on judgment; keep questions concise
|
||||
- After clarification complete: MUST use TodoWrite to create task tracking list with workflow steps
|
||||
|
||||
- **Step 2: codeagent-wrapper Deep Analysis (Plan Mode Style) [USE CODEAGENT-WRAPPER ONLY]**
|
||||
|
||||
MUST use Bash tool to invoke `codeagent-wrapper` for deep analysis. Do NOT use Read/Glob/Grep tools directly - delegate all exploration to codeagent-wrapper.
|
||||
|
||||
**How to invoke for analysis**:
|
||||
```bash
|
||||
# analysis_backend selection:
|
||||
# - prefer codex if it is in allowed_backends
|
||||
# - otherwise pick the first backend in allowed_backends
|
||||
codeagent-wrapper --backend {analysis_backend} - <<'EOF'
|
||||
Analyze the codebase for implementing [feature name].
|
||||
|
||||
Requirements:
|
||||
- [requirement 1]
|
||||
- [requirement 2]
|
||||
|
||||
Deliverables:
|
||||
1. Explore codebase structure and existing patterns
|
||||
2. Evaluate implementation options with trade-offs
|
||||
3. Make architectural decisions
|
||||
4. Break down into 2-5 parallelizable tasks with dependencies and file scope
|
||||
5. Classify each task with a single `type`: `default` / `ui` / `quick-fix`
|
||||
6. Determine if UI work is needed (check for .css/.tsx/.vue files)
|
||||
|
||||
Output the analysis following the structure below.
|
||||
EOF
|
||||
```
|
||||
|
||||
**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 the AI backend does in Analysis Mode** (when invoked via codeagent-wrapper):
|
||||
1. **Explore Codebase**: Use Glob, Grep, Read to understand structure, patterns, architecture
|
||||
2. **Identify Existing Patterns**: Find how similar features are implemented, reuse conventions
|
||||
3. **Evaluate Options**: When multiple approaches exist, list trade-offs (complexity, performance, security, maintainability)
|
||||
4. **Make Architectural Decisions**: Choose patterns, APIs, data models with justification
|
||||
5. **Design Task Breakdown**: Produce parallelizable tasks based on natural functional boundaries 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, type(default|ui|quick-fix)]
|
||||
|
||||
## 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`, ensure every task has `type: default|ui|quick-fix`
|
||||
- Append a dedicated UI task if Step 2 marked `needs_ui: true` but no UI task exists
|
||||
- Output a brief summary of dev-plan.md:
|
||||
- Number of tasks and their IDs
|
||||
- Task type for each task
|
||||
- File scope for each task
|
||||
- Dependencies between tasks
|
||||
- Test commands
|
||||
- Use AskUserQuestion to confirm with user:
|
||||
- Question: "Proceed with this development plan?" (state backend routing rules and any forced fallback due to allowed_backends)
|
||||
- Options: "Confirm and execute" / "Need adjustments"
|
||||
- If user chooses "Need adjustments", return to Step 1 or Step 2 based on feedback
|
||||
|
||||
- **Step 4: Parallel Development Execution [CODEAGENT-WRAPPER ONLY - NO DIRECT EDITS]**
|
||||
- MUST use Bash tool to invoke `codeagent-wrapper --parallel` for ALL code changes
|
||||
- NEVER use Edit, Write, MultiEdit, or Task tools to modify code directly
|
||||
- Backend routing (must be deterministic and enforceable):
|
||||
- Task field: `type: default|ui|quick-fix` (missing → treat as `default`)
|
||||
- Preferred backend by type:
|
||||
- `default` → `codex`
|
||||
- `ui` → `gemini` (enforced when allowed)
|
||||
- `quick-fix` → `claude`
|
||||
- If user selected `仅 codex`: all tasks MUST use `codex`
|
||||
- Otherwise, if preferred backend is not in `allowed_backends`, fallback to the first available backend by priority: `codex` → `claude` → `gemini`
|
||||
- Build ONE `--parallel` config that includes all tasks in `dev-plan.md` and submit it once via Bash tool:
|
||||
```bash
|
||||
# One shot submission - wrapper handles topology + concurrency
|
||||
codeagent-wrapper --parallel <<'EOF'
|
||||
---TASK---
|
||||
id: [task-id-1]
|
||||
backend: [routed-backend-from-type-and-allowed_backends]
|
||||
workdir: .
|
||||
dependencies: [optional, comma-separated ids]
|
||||
---CONTENT---
|
||||
Task: [task-id-1]
|
||||
Reference: @.claude/specs/{feature_name}/dev-plan.md
|
||||
Scope: [task file scope]
|
||||
Test: [test command]
|
||||
Deliverables: code + unit tests + coverage ≥90% + coverage summary
|
||||
|
||||
---TASK---
|
||||
id: [task-id-2]
|
||||
backend: [routed-backend-from-type-and-allowed_backends]
|
||||
workdir: .
|
||||
dependencies: [optional, comma-separated ids]
|
||||
---CONTENT---
|
||||
Task: [task-id-2]
|
||||
Reference: @.claude/specs/{feature_name}/dev-plan.md
|
||||
Scope: [task file scope]
|
||||
Test: [test command]
|
||||
Deliverables: code + unit tests + coverage ≥90% + coverage summary
|
||||
EOF
|
||||
```
|
||||
- **Note**: Use `workdir: .` (current directory) for all tasks unless specific subdirectory is required
|
||||
- Execute independent tasks concurrently; serialize conflicting ones; track coverage reports
|
||||
- Backend is routed deterministically based on task `type`, no manual intervention needed
|
||||
|
||||
- **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-wrapper failure**: Retry once with same input; if still fails, log error and ask user for guidance
|
||||
- **Insufficient coverage (<90%)**: Request more tests from the failed task (max 2 rounds); if still fails, report to user
|
||||
- **Dependency conflicts**:
|
||||
- Circular dependencies: codeagent-wrapper will detect and fail with error; revise task breakdown to remove cycles
|
||||
- Missing dependencies: Ensure all task IDs referenced in `dependencies` field exist
|
||||
- **Parallel execution timeout**: Individual tasks timeout after 2 hours (configurable via CODEX_TIMEOUT); failed tasks can be retried individually
|
||||
- **Backend unavailable**: If a routed backend is unavailable, fallback to another backend in `allowed_backends` (priority: codex → claude → gemini); if none works, fail with a clear error message
|
||||
|
||||
**Quality Standards**
|
||||
- Code coverage ≥90%
|
||||
- Tasks based on natural functional boundaries (typically 2-5)
|
||||
- Each task has exactly one `type: default|ui|quick-fix`
|
||||
- Backend routed by `type`: `default`→codex, `ui`→gemini, `quick-fix`→claude (with allowed_backends fallback)
|
||||
- Documentation must be minimal yet actionable
|
||||
- No verbose implementations; only essential code
|
||||
|
||||
**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
|
||||
9
development-essentials/.claude-plugin/plugin.json
Normal file
9
development-essentials/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "essentials",
|
||||
"description": "Essential development commands for coding, debugging, testing, optimization, and documentation",
|
||||
"version": "5.6.1",
|
||||
"author": {
|
||||
"name": "cexll",
|
||||
"email": "cexll@cexll.com"
|
||||
}
|
||||
}
|
||||
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
|
||||
258
docs/BMAD-WORKFLOW.md
Normal file
258
docs/BMAD-WORKFLOW.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# BMAD Workflow Complete Guide
|
||||
|
||||
> **BMAD (Business-Minded Agile Development)** - AI-driven agile development automation with role-based agents
|
||||
|
||||
## 🎯 What is BMAD?
|
||||
|
||||
BMAD is an enterprise-grade agile development methodology that transforms your development process into a fully automated workflow with 6 specialized AI agents and quality gates.
|
||||
|
||||
### Core Principles
|
||||
|
||||
- **Agent Planning**: Specialized agents collaborate to create detailed, consistent PRDs and architecture documents
|
||||
- **Context-Driven Development**: Transform detailed plans into ultra-detailed development stories
|
||||
- **Role Specialization**: Each agent focuses on specific domains, avoiding quality degradation from role switching
|
||||
|
||||
## 🤖 BMAD Agent System
|
||||
|
||||
### Agent Roles
|
||||
|
||||
| Agent | Role | Quality Gate | Artifacts |
|
||||
|-------|------|--------------|-----------|
|
||||
| **bmad-po** (Sarah) | Product Owner - requirements gathering, user stories | PRD ≥ 90/100 | `01-product-requirements.md` |
|
||||
| **bmad-architect** (Winston) | System Architect - technical design, system architecture | Design ≥ 90/100 | `02-system-architecture.md` |
|
||||
| **bmad-sm** (Mike) | Scrum Master - task breakdown, sprint planning | User approval | `03-sprint-plan.md` |
|
||||
| **bmad-dev** (Alex) | Developer - code implementation, technical docs | Code completion | Implementation files |
|
||||
| **bmad-review** | Code Reviewer - independent review between Dev and QA | Pass/Risk/Fail | `04-dev-reviewed.md` |
|
||||
| **bmad-qa** (Emma) | QA Engineer - testing strategy, quality assurance | Test execution | `05-qa-report.md` |
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Command Overview
|
||||
|
||||
```bash
|
||||
# Full BMAD workflow
|
||||
/bmad-pilot "Build e-commerce checkout system with payment integration"
|
||||
|
||||
# Workflow: PO → Architect → SM → Dev → Review → QA
|
||||
```
|
||||
|
||||
### Command Options
|
||||
|
||||
```bash
|
||||
# Skip testing phase
|
||||
/bmad-pilot "Admin dashboard" --skip-tests
|
||||
|
||||
# Skip sprint planning (architecture → dev directly)
|
||||
/bmad-pilot "API gateway implementation" --direct-dev
|
||||
|
||||
# Skip repository scan (not recommended)
|
||||
/bmad-pilot "Add feature" --skip-scan
|
||||
```
|
||||
|
||||
### Individual Agent Usage
|
||||
|
||||
```bash
|
||||
# Product requirements analysis only
|
||||
/bmad-po "Enterprise CRM system requirements"
|
||||
|
||||
# Technical architecture design only
|
||||
/bmad-architect "High-concurrency distributed system design"
|
||||
|
||||
# Orchestrator (can transform into any agent)
|
||||
/bmad-orchestrator "Coordinate multi-agent complex project"
|
||||
```
|
||||
|
||||
## 📋 Workflow Phases
|
||||
|
||||
### Phase 0: Repository Scan (Automatic)
|
||||
- **Agent**: `bmad-orchestrator`
|
||||
- **Output**: `00-repository-context.md`
|
||||
- **Content**: Project type, tech stack, code organization, conventions, integration points
|
||||
|
||||
### Phase 1: Product Requirements (PO)
|
||||
- **Agent**: `bmad-po` (Sarah - Product Owner)
|
||||
- **Quality Gate**: PRD score ≥ 90/100
|
||||
- **Output**: `01-product-requirements.md`
|
||||
- **Process**:
|
||||
1. PO generates initial PRD
|
||||
2. System calculates quality score (100-point scale)
|
||||
3. If < 90: User provides feedback → PO revises → Recalculate
|
||||
4. If ≥ 90: User confirms → Save artifact → Next phase
|
||||
|
||||
### Phase 2: System Architecture (Architect)
|
||||
- **Agent**: `bmad-architect` (Winston - System Architect)
|
||||
- **Quality Gate**: Design score ≥ 90/100
|
||||
- **Output**: `02-system-architecture.md`
|
||||
- **Process**:
|
||||
1. Architect reads PRD + repo context
|
||||
2. Generates technical design document
|
||||
3. System calculates design quality score
|
||||
4. If < 90: User provides feedback → Architect revises
|
||||
5. If ≥ 90: User confirms → Save artifact → Next phase
|
||||
|
||||
### Phase 3: Sprint Planning (SM)
|
||||
- **Agent**: `bmad-sm` (Mike - Scrum Master)
|
||||
- **Quality Gate**: User approval
|
||||
- **Output**: `03-sprint-plan.md`
|
||||
- **Process**:
|
||||
1. SM reads PRD + Architecture
|
||||
2. Breaks down tasks with story points
|
||||
3. User reviews sprint plan
|
||||
4. User confirms → Save artifact → Next phase
|
||||
- **Skip**: Use `--direct-dev` to skip this phase
|
||||
|
||||
### Phase 4: Development (Dev)
|
||||
- **Agent**: `bmad-dev` (Alex - Developer)
|
||||
- **Quality Gate**: Code completion
|
||||
- **Output**: Implementation files
|
||||
- **Process**:
|
||||
1. Dev reads all previous artifacts
|
||||
2. Implements features following sprint plan
|
||||
3. Creates or modifies code files
|
||||
4. Completes implementation → Next phase
|
||||
|
||||
### Phase 5: Code Review (Review)
|
||||
- **Agent**: `bmad-review` (Independent Reviewer)
|
||||
- **Quality Gate**: Pass / Pass with Risk / Fail
|
||||
- **Output**: `04-dev-reviewed.md`
|
||||
- **Process**:
|
||||
1. Review reads implementation + all specs
|
||||
2. Performs comprehensive code review
|
||||
3. Generates review report with status:
|
||||
- **Pass**: No issues, proceed to QA
|
||||
- **Pass with Risk**: Non-critical issues noted
|
||||
- **Fail**: Critical issues, return to Dev
|
||||
4. Updates sprint plan with review findings
|
||||
|
||||
**Enhanced Review (Optional)**:
|
||||
- Use GPT-5 via Codex CLI for deeper analysis
|
||||
- Set via `BMAD_REVIEW_MODE=enhanced` environment variable
|
||||
|
||||
### Phase 6: Quality Assurance (QA)
|
||||
- **Agent**: `bmad-qa` (Emma - QA Engineer)
|
||||
- **Quality Gate**: Test execution
|
||||
- **Output**: `05-qa-report.md`
|
||||
- **Process**:
|
||||
1. QA reads implementation + review + all specs
|
||||
2. Creates targeted test strategy
|
||||
3. Executes tests
|
||||
4. Generates QA report
|
||||
5. Workflow complete
|
||||
- **Skip**: Use `--skip-tests` to skip this phase
|
||||
|
||||
## 📊 Quality Scoring System
|
||||
|
||||
### PRD Quality (100 points)
|
||||
- **Business Value** (30): Clear value proposition, user benefits
|
||||
- **Functional Requirements** (25): Complete, unambiguous requirements
|
||||
- **User Experience** (20): User flows, interaction patterns
|
||||
- **Technical Constraints** (15): Performance, security, scalability
|
||||
- **Scope & Priorities** (10): Clear boundaries, must-have vs nice-to-have
|
||||
|
||||
### Architecture Quality (100 points)
|
||||
- **Design Quality** (30): Modularity, maintainability, clarity
|
||||
- **Technology Selection** (25): Appropriate tech stack, justification
|
||||
- **Scalability** (20): Growth handling, performance considerations
|
||||
- **Security** (15): Authentication, authorization, data protection
|
||||
- **Feasibility** (10): Realistic implementation, resource alignment
|
||||
|
||||
### Review Status (3 levels)
|
||||
- **Pass**: No critical issues, code meets standards
|
||||
- **Pass with Risk**: Non-critical issues, recommendations included
|
||||
- **Fail**: Critical issues, requires Dev iteration
|
||||
|
||||
## 📁 Workflow Artifacts
|
||||
|
||||
All documents are saved to `.claude/specs/{feature-name}/`:
|
||||
|
||||
```
|
||||
.claude/specs/e-commerce-checkout/
|
||||
├── 00-repository-context.md # Repo analysis (auto)
|
||||
├── 01-product-requirements.md # PRD (PO, score ≥ 90)
|
||||
├── 02-system-architecture.md # Design (Architect, score ≥ 90)
|
||||
├── 03-sprint-plan.md # Sprint plan (SM, user approved)
|
||||
├── 04-dev-reviewed.md # Code review (Review, Pass/Risk/Fail)
|
||||
└── 05-qa-report.md # Test report (QA, tests executed)
|
||||
```
|
||||
|
||||
Feature name generated from project description (kebab-case: lowercase, spaces/punctuation → `-`).
|
||||
|
||||
## 🔧 Advanced Usage
|
||||
|
||||
### Approval Gates
|
||||
|
||||
Critical phases require explicit user confirmation:
|
||||
|
||||
```
|
||||
Architect: "Technical design complete (Score: 93/100)"
|
||||
System: "Ready to proceed to sprint planning? (yes/no)"
|
||||
User: yes
|
||||
```
|
||||
|
||||
### Iterative Refinement
|
||||
|
||||
Each phase supports feedback loops:
|
||||
|
||||
```
|
||||
PO: "Here's the PRD (Score: 75/100)"
|
||||
User: "Add mobile support and offline mode"
|
||||
PO: "Updated PRD (Score: 92/100) ✅"
|
||||
```
|
||||
|
||||
### Repository Context
|
||||
|
||||
BMAD automatically scans your repository to understand:
|
||||
- Technology stack (languages, frameworks, libraries)
|
||||
- Project structure (directories, modules, patterns)
|
||||
- Existing conventions (naming, formatting, architecture)
|
||||
- Dependencies (package managers, external services)
|
||||
- Integration points (APIs, databases, third-party services)
|
||||
|
||||
### Workflow Variations
|
||||
|
||||
**Fast Prototyping** - Skip non-essential phases:
|
||||
```bash
|
||||
/bmad-pilot "Quick admin UI" --skip-tests --direct-dev
|
||||
# Workflow: PO → Architect → Dev
|
||||
```
|
||||
|
||||
**Architecture-First** - Focus on design:
|
||||
```bash
|
||||
/bmad-architect "Microservices architecture for e-commerce"
|
||||
# Only runs Architect agent
|
||||
```
|
||||
|
||||
**Full Rigor** - All phases with maximum quality:
|
||||
```bash
|
||||
/bmad-pilot "Enterprise payment gateway with PCI compliance"
|
||||
# Workflow: Scan → PO → Architect → SM → Dev → Review → QA
|
||||
```
|
||||
|
||||
## 🎨 Output Style
|
||||
|
||||
BMAD workflow uses a specialized output style that:
|
||||
- Creates phase-separated contexts
|
||||
- Manages agent handoffs with clear boundaries
|
||||
- Tracks quality scores across phases
|
||||
- Handles approval gates with user prompts
|
||||
- Supports Codex CLI integration for enhanced reviews
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[Quick Start Guide](QUICK-START.md)** - Get started in 5 minutes
|
||||
- **[Plugin System](PLUGIN-SYSTEM.md)** - Installation and configuration
|
||||
- **[Development Commands](DEVELOPMENT-COMMANDS.md)** - Alternative workflows
|
||||
- **[Requirements Workflow](REQUIREMENTS-WORKFLOW.md)** - Lightweight alternative
|
||||
|
||||
## 💡 Best Practices
|
||||
|
||||
1. **Don't skip repository scan** - Helps agents understand your project context
|
||||
2. **Provide detailed descriptions** - Better input → better output
|
||||
3. **Engage with agents** - Provide feedback during quality gates
|
||||
4. **Review artifacts** - Check generated documents before confirming
|
||||
5. **Use appropriate workflows** - Full BMAD for complex features, lightweight for simple tasks
|
||||
6. **Keep artifacts** - They serve as project documentation and context for future work
|
||||
|
||||
---
|
||||
|
||||
**Transform your development with BMAD** - One command, complete agile workflow, quality assured.
|
||||
451
docs/CODEAGENT-WRAPPER.md
Normal file
451
docs/CODEAGENT-WRAPPER.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# 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
|
||||
# Default: summary output (context-efficient, recommended)
|
||||
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
|
||||
|
||||
# Full output mode (for debugging, includes complete task messages)
|
||||
codeagent-wrapper --parallel --full-output <<'EOF'
|
||||
...
|
||||
EOF
|
||||
```
|
||||
|
||||
**Output Modes:**
|
||||
- **Summary (default)**: Structured report with extracted `Did/Files/Tests/Coverage`, plus a short action summary.
|
||||
- **Full (`--full-output`)**: Complete task messages included. Use only for debugging.
|
||||
|
||||
**Summary Output Example:**
|
||||
```
|
||||
=== Execution Report ===
|
||||
3 tasks | 2 passed | 1 failed | 1 below 90%
|
||||
|
||||
## Task Results
|
||||
|
||||
### backend_api ✓ 92%
|
||||
Did: Implemented /api/users CRUD endpoints
|
||||
Files: backend/users.go, backend/router.go
|
||||
Tests: 12 passed
|
||||
Log: /tmp/codeagent-xxx.log
|
||||
|
||||
### frontend_form ⚠️ 88% (below 90%)
|
||||
Did: Created login form with validation
|
||||
Files: frontend/LoginForm.tsx
|
||||
Tests: 8 passed
|
||||
Gap: lines not covered: frontend/LoginForm.tsx:42-47
|
||||
Log: /tmp/codeagent-yyy.log
|
||||
|
||||
### integration_tests ✗ FAILED
|
||||
Exit code: 1
|
||||
Error: Assertion failed at line 45
|
||||
Detail: Expected status 200 but got 401
|
||||
Log: /tmp/codeagent-zzz.log
|
||||
|
||||
## Summary
|
||||
- 2/3 completed successfully
|
||||
- Fix: integration_tests (Assertion failed at line 45)
|
||||
- Coverage: frontend_form
|
||||
```
|
||||
|
||||
**Parallel Task Format:**
|
||||
- `---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 |
|
||||
| `CODEX_BYPASS_SANDBOX` | true | Bypass Codex sandbox/approval. Set `false` to disable |
|
||||
| `CODEAGENT_SKIP_PERMISSIONS` | true | Skip Claude permission prompts. Set `false` to disable |
|
||||
|
||||
## 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)
|
||||
321
docs/DEVELOPMENT-COMMANDS.md
Normal file
321
docs/DEVELOPMENT-COMMANDS.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# Development Commands Reference
|
||||
|
||||
> Direct slash commands for daily coding tasks without workflow overhead
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
Development Essentials provides focused slash commands for common development tasks. Use these when you need direct implementation without the full workflow structure.
|
||||
|
||||
## 📋 Available Commands
|
||||
|
||||
### `/code` - Direct Implementation
|
||||
|
||||
Implement features, add functionality, or write code directly.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
/code "Add input validation for email fields"
|
||||
/code "Implement pagination for user list API"
|
||||
/code "Create database migration for orders table"
|
||||
```
|
||||
|
||||
**Agent**: `code`
|
||||
|
||||
**Best for**:
|
||||
- Clear, well-defined tasks
|
||||
- Quick implementations
|
||||
- Following existing patterns
|
||||
- Adding straightforward features
|
||||
|
||||
### `/debug` - Systematic Debugging
|
||||
|
||||
Analyze and fix bugs with structured debugging approach.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
/debug "Login fails with 500 error on invalid credentials"
|
||||
/debug "Memory leak in background worker process"
|
||||
/debug "Race condition in order processing"
|
||||
```
|
||||
|
||||
**Agent**: `debug`
|
||||
|
||||
**Approach**:
|
||||
1. Reproduce the issue
|
||||
2. Analyze root cause
|
||||
3. Propose solution
|
||||
4. Implement fix
|
||||
5. Verify resolution
|
||||
|
||||
### `/test` - Testing Strategy
|
||||
|
||||
Create tests, improve test coverage, or test existing code.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
/test "Add unit tests for authentication service"
|
||||
/test "Create integration tests for payment flow"
|
||||
/test "Test edge cases for date parser"
|
||||
```
|
||||
|
||||
**Agent**: `develop` (testing mode)
|
||||
|
||||
**Covers**:
|
||||
- Unit tests
|
||||
- Integration tests
|
||||
- Edge cases
|
||||
- Error scenarios
|
||||
- Test data setup
|
||||
|
||||
### `/optimize` - Performance Tuning
|
||||
|
||||
Improve performance, reduce resource usage, or optimize algorithms.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
/optimize "Reduce database queries in dashboard endpoint"
|
||||
/optimize "Speed up report generation process"
|
||||
/optimize "Improve memory usage in data processing pipeline"
|
||||
```
|
||||
|
||||
**Agent**: `develop` (optimization mode)
|
||||
|
||||
**Focus areas**:
|
||||
- Algorithm efficiency
|
||||
- Database query optimization
|
||||
- Caching strategies
|
||||
- Resource utilization
|
||||
- Load time reduction
|
||||
|
||||
### `/bugfix` - Bug Resolution
|
||||
|
||||
Fix specific bugs with focused approach.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
/bugfix "Users can't reset password with special characters"
|
||||
/bugfix "Session expires too quickly on mobile"
|
||||
/bugfix "File upload fails for large files"
|
||||
```
|
||||
|
||||
**Agent**: `bugfix`
|
||||
|
||||
**Process**:
|
||||
1. Understand the bug
|
||||
2. Locate problematic code
|
||||
3. Implement fix
|
||||
4. Add regression tests
|
||||
5. Verify fix
|
||||
|
||||
### `/refactor` - Code Improvement
|
||||
|
||||
Improve code structure, readability, or maintainability without changing behavior.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
/refactor "Extract user validation logic into separate module"
|
||||
/refactor "Simplify nested conditionals in order processing"
|
||||
/refactor "Remove code duplication in API handlers"
|
||||
```
|
||||
|
||||
**Agent**: `develop` (refactor mode)
|
||||
|
||||
**Goals**:
|
||||
- Improve readability
|
||||
- Reduce complexity
|
||||
- Eliminate duplication
|
||||
- Enhance maintainability
|
||||
- Follow best practices
|
||||
|
||||
### `/review` - Code Validation
|
||||
|
||||
Review code for quality, security, and best practices.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
/review "Check authentication implementation for security issues"
|
||||
/review "Validate API error handling patterns"
|
||||
/review "Assess database schema design"
|
||||
```
|
||||
|
||||
**Agent**: Independent reviewer
|
||||
|
||||
**Review criteria**:
|
||||
- Code quality
|
||||
- Security vulnerabilities
|
||||
- Performance issues
|
||||
- Best practices compliance
|
||||
- Maintainability
|
||||
|
||||
### `/ask` - Technical Consultation
|
||||
|
||||
Get technical advice, design patterns, or implementation guidance.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
/ask "Best approach for real-time notifications in React"
|
||||
/ask "How to handle database migrations in production"
|
||||
/ask "Design pattern for plugin system"
|
||||
```
|
||||
|
||||
**Agent**: Technical consultant
|
||||
|
||||
**Provides**:
|
||||
- Architecture guidance
|
||||
- Technology recommendations
|
||||
- Design patterns
|
||||
- Best practices
|
||||
- Trade-off analysis
|
||||
|
||||
### `/docs` - Documentation
|
||||
|
||||
Generate or improve documentation.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
/docs "Create API documentation for user endpoints"
|
||||
/docs "Add JSDoc comments to utility functions"
|
||||
/docs "Write README for authentication module"
|
||||
```
|
||||
|
||||
**Agent**: Documentation writer
|
||||
|
||||
**Creates**:
|
||||
- Code comments
|
||||
- API documentation
|
||||
- README files
|
||||
- Usage examples
|
||||
- Architecture docs
|
||||
|
||||
### `/think` - Advanced Analysis
|
||||
|
||||
Deep reasoning and analysis for complex problems.
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
/think "Analyze scalability bottlenecks in current architecture"
|
||||
/think "Evaluate different approaches for data synchronization"
|
||||
/think "Design migration strategy from monolith to microservices"
|
||||
```
|
||||
|
||||
**Agent**: `gpt5` (deep reasoning)
|
||||
|
||||
**Best for**:
|
||||
- Complex architectural decisions
|
||||
- Multi-faceted problems
|
||||
- Trade-off analysis
|
||||
- Strategic planning
|
||||
- System design
|
||||
|
||||
## 🔄 Command Workflows
|
||||
|
||||
### Simple Feature Development
|
||||
|
||||
```bash
|
||||
# 1. Ask for guidance
|
||||
/ask "Best way to implement rate limiting in Express"
|
||||
|
||||
# 2. Implement the feature
|
||||
/code "Add rate limiting middleware to API routes"
|
||||
|
||||
# 3. Add tests
|
||||
/test "Create tests for rate limiting behavior"
|
||||
|
||||
# 4. Review implementation
|
||||
/review "Validate rate limiting implementation"
|
||||
```
|
||||
|
||||
### Bug Investigation and Fix
|
||||
|
||||
```bash
|
||||
# 1. Debug the issue
|
||||
/debug "API returns 500 on concurrent requests"
|
||||
|
||||
# 2. Fix the bug
|
||||
/bugfix "Add mutex lock to prevent race condition"
|
||||
|
||||
# 3. Add regression tests
|
||||
/test "Test concurrent request handling"
|
||||
```
|
||||
|
||||
### Code Quality Improvement
|
||||
|
||||
```bash
|
||||
# 1. Review current code
|
||||
/review "Analyze user service for improvements"
|
||||
|
||||
# 2. Refactor based on findings
|
||||
/refactor "Simplify user validation logic"
|
||||
|
||||
# 3. Optimize performance
|
||||
/optimize "Cache frequently accessed user data"
|
||||
|
||||
# 4. Update documentation
|
||||
/docs "Document user service API"
|
||||
```
|
||||
|
||||
## 🎯 When to Use What
|
||||
|
||||
### Use Direct Commands When:
|
||||
- Task is clear and well-defined
|
||||
- No complex planning needed
|
||||
- Fast iteration is priority
|
||||
- Working within existing patterns
|
||||
|
||||
### Use Requirements Workflow When:
|
||||
- Feature has unclear requirements
|
||||
- Need documented specifications
|
||||
- Multiple implementation approaches possible
|
||||
- Quality gates desired
|
||||
|
||||
### Use BMAD Workflow When:
|
||||
- Complex business requirements
|
||||
- Architecture design needed
|
||||
- Sprint planning required
|
||||
- Multiple stakeholders involved
|
||||
|
||||
## 💡 Best Practices
|
||||
|
||||
1. **Be Specific**: Provide clear, detailed descriptions
|
||||
- ❌ `/code "fix the bug"`
|
||||
- ✅ `/code "Fix null pointer exception in user login when email is missing"`
|
||||
|
||||
2. **One Task Per Command**: Keep commands focused
|
||||
- ❌ `/code "Add feature X, fix bug Y, refactor module Z"`
|
||||
- ✅ `/code "Add email validation to registration form"`
|
||||
|
||||
3. **Provide Context**: Include relevant details
|
||||
- ✅ `/debug "Login API returns 401 after password change, only on Safari"`
|
||||
|
||||
4. **Use Appropriate Command**: Match command to task type
|
||||
- Use `/bugfix` for bugs, not `/code`
|
||||
- Use `/refactor` for restructuring, not `/optimize`
|
||||
- Use `/think` for complex analysis, not `/ask`
|
||||
|
||||
5. **Chain Commands**: Break complex tasks into steps
|
||||
```bash
|
||||
/ask "How to implement OAuth2"
|
||||
/code "Implement OAuth2 authorization flow"
|
||||
/test "Add OAuth2 integration tests"
|
||||
/review "Validate OAuth2 security"
|
||||
/docs "Document OAuth2 setup process"
|
||||
```
|
||||
|
||||
## 🔌 Agent Configuration
|
||||
|
||||
All commands use specialized agents configured in:
|
||||
- `development-essentials/agents/`
|
||||
- Agent prompt templates
|
||||
- Tool access permissions
|
||||
- Output formatting
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[BMAD Workflow](BMAD-WORKFLOW.md)** - Full agile methodology
|
||||
- **[Requirements Workflow](REQUIREMENTS-WORKFLOW.md)** - Lightweight workflow
|
||||
- **[Quick Start Guide](QUICK-START.md)** - Get started quickly
|
||||
- **[Plugin System](PLUGIN-SYSTEM.md)** - Installation and configuration
|
||||
|
||||
---
|
||||
|
||||
**Development Essentials** - Direct commands for productive coding without workflow overhead.
|
||||
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/)
|
||||
348
docs/PLUGIN-SYSTEM.md
Normal file
348
docs/PLUGIN-SYSTEM.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# Plugin System Guide
|
||||
|
||||
> Native Claude Code plugin support for modular workflow installation
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
This repository provides 4 ready-to-use Claude Code plugins that can be installed individually or as a complete suite.
|
||||
|
||||
## 📦 Available Plugins
|
||||
|
||||
### 1. bmad-agile-workflow
|
||||
|
||||
**Complete BMAD methodology with 6 specialized agents**
|
||||
|
||||
**Commands**:
|
||||
- `/bmad-pilot` - Full agile workflow orchestration
|
||||
|
||||
**Agents**:
|
||||
- `bmad-po` - Product Owner (Sarah)
|
||||
- `bmad-architect` - System Architect (Winston)
|
||||
- `bmad-sm` - Scrum Master (Mike)
|
||||
- `bmad-dev` - Developer (Alex)
|
||||
- `bmad-review` - Code Reviewer
|
||||
- `bmad-qa` - QA Engineer (Emma)
|
||||
- `bmad-orchestrator` - Main orchestrator
|
||||
|
||||
**Use for**: Enterprise projects, complex features, full agile process
|
||||
|
||||
### 2. requirements-driven-workflow
|
||||
|
||||
**Streamlined requirements-to-code workflow**
|
||||
|
||||
**Commands**:
|
||||
- `/requirements-pilot` - Requirements-driven development flow
|
||||
|
||||
**Agents**:
|
||||
- `requirements-generate` - Requirements generation
|
||||
- `requirements-code` - Code implementation
|
||||
- `requirements-review` - Code review
|
||||
- `requirements-testing` - Testing strategy
|
||||
|
||||
**Use for**: Quick prototyping, simple features, rapid development
|
||||
|
||||
### 3. development-essentials
|
||||
|
||||
**Core development slash commands**
|
||||
|
||||
**Commands**:
|
||||
- `/code` - Direct implementation
|
||||
- `/debug` - Systematic debugging
|
||||
- `/test` - Testing strategy
|
||||
- `/optimize` - Performance tuning
|
||||
- `/bugfix` - Bug resolution
|
||||
- `/refactor` - Code improvement
|
||||
- `/review` - Code validation
|
||||
- `/ask` - Technical consultation
|
||||
- `/docs` - Documentation
|
||||
- `/think` - Advanced analysis
|
||||
|
||||
**Agents**:
|
||||
- `code` - Code implementation
|
||||
- `bugfix` - Bug fixing
|
||||
- `debug` - Debugging
|
||||
- `develop` - General development
|
||||
|
||||
**Use for**: Daily coding tasks, quick implementations
|
||||
|
||||
### 4. advanced-ai-agents
|
||||
|
||||
**GPT-5 deep reasoning integration**
|
||||
|
||||
**Commands**: None (agent-only)
|
||||
|
||||
**Agents**:
|
||||
- `gpt5` - Deep reasoning and analysis
|
||||
|
||||
**Use for**: Complex architectural decisions, strategic planning
|
||||
|
||||
## 🚀 Installation Methods
|
||||
|
||||
### Method 1: Plugin Commands (Recommended)
|
||||
|
||||
```bash
|
||||
# List all available plugins
|
||||
/plugin list
|
||||
|
||||
# Get detailed information about a plugin
|
||||
/plugin info bmad-agile-workflow
|
||||
|
||||
# Install a specific plugin
|
||||
/plugin install bmad-agile-workflow
|
||||
|
||||
# Install all plugins
|
||||
/plugin install bmad-agile-workflow
|
||||
/plugin install requirements-driven-workflow
|
||||
/plugin install development-essentials
|
||||
/plugin install advanced-ai-agents
|
||||
|
||||
# Remove an installed plugin
|
||||
/plugin remove development-essentials
|
||||
```
|
||||
|
||||
### Method 2: Repository Reference
|
||||
|
||||
```bash
|
||||
# Install from GitHub repository
|
||||
/plugin marketplace add cexll/myclaude
|
||||
```
|
||||
|
||||
This will present all available plugins from the repository.
|
||||
|
||||
### Method 3: Make Commands
|
||||
|
||||
For traditional installation or selective deployment:
|
||||
|
||||
```bash
|
||||
# Install everything
|
||||
make install
|
||||
|
||||
# Deploy specific workflows
|
||||
make deploy-bmad # BMAD workflow only
|
||||
make deploy-requirements # Requirements workflow only
|
||||
make deploy-commands # All slash commands
|
||||
make deploy-agents # All agents
|
||||
|
||||
# Deploy everything
|
||||
make deploy-all
|
||||
|
||||
# View all options
|
||||
make help
|
||||
```
|
||||
|
||||
### Method 4: Manual Installation
|
||||
|
||||
Copy files to Claude Code configuration directories:
|
||||
|
||||
**Commands**:
|
||||
```bash
|
||||
cp bmad-agile-workflow/commands/*.md ~/.config/claude/commands/
|
||||
cp requirements-driven-workflow/commands/*.md ~/.config/claude/commands/
|
||||
cp development-essentials/commands/*.md ~/.config/claude/commands/
|
||||
```
|
||||
|
||||
**Agents**:
|
||||
```bash
|
||||
cp bmad-agile-workflow/agents/*.md ~/.config/claude/agents/
|
||||
cp requirements-driven-workflow/agents/*.md ~/.config/claude/agents/
|
||||
cp development-essentials/agents/*.md ~/.config/claude/agents/
|
||||
cp advanced-ai-agents/agents/*.md ~/.config/claude/agents/
|
||||
```
|
||||
|
||||
**Output Styles** (optional):
|
||||
```bash
|
||||
cp output-styles/*.md ~/.config/claude/output-styles/
|
||||
```
|
||||
|
||||
## 📋 Plugin Configuration
|
||||
|
||||
Plugins are defined in `.claude-plugin/marketplace.json` following the Claude Code plugin specification.
|
||||
|
||||
### Plugin Metadata Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "plugin-name",
|
||||
"displayName": "Human Readable Name",
|
||||
"description": "Plugin description",
|
||||
"version": "1.0.0",
|
||||
"author": "Author Name",
|
||||
"category": "workflow|development|analysis",
|
||||
"keywords": ["keyword1", "keyword2"],
|
||||
"commands": ["command1", "command2"],
|
||||
"agents": ["agent1", "agent2"]
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Plugin Management
|
||||
|
||||
### Check Installed Plugins
|
||||
|
||||
```bash
|
||||
/plugin list
|
||||
```
|
||||
|
||||
Shows all installed plugins with their status.
|
||||
|
||||
### Plugin Information
|
||||
|
||||
```bash
|
||||
/plugin info <plugin-name>
|
||||
```
|
||||
|
||||
Displays detailed information:
|
||||
- Description
|
||||
- Version
|
||||
- Commands provided
|
||||
- Agents included
|
||||
- Author and keywords
|
||||
|
||||
### Update Plugins
|
||||
|
||||
Plugins are updated when you pull the latest repository changes:
|
||||
|
||||
```bash
|
||||
git pull origin main
|
||||
make install
|
||||
```
|
||||
|
||||
### Uninstall Plugins
|
||||
|
||||
```bash
|
||||
/plugin remove <plugin-name>
|
||||
```
|
||||
|
||||
Or manually remove files:
|
||||
|
||||
```bash
|
||||
# Remove commands
|
||||
rm ~/.config/claude/commands/<command-name>.md
|
||||
|
||||
# Remove agents
|
||||
rm ~/.config/claude/agents/<agent-name>.md
|
||||
```
|
||||
|
||||
## 🎯 Plugin Selection Guide
|
||||
|
||||
### Install Everything (Recommended for New Users)
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
Provides complete functionality with all workflows and commands.
|
||||
|
||||
### Selective Installation
|
||||
|
||||
**For Agile Teams**:
|
||||
```bash
|
||||
/plugin install bmad-agile-workflow
|
||||
```
|
||||
|
||||
**For Rapid Development**:
|
||||
```bash
|
||||
/plugin install requirements-driven-workflow
|
||||
/plugin install development-essentials
|
||||
```
|
||||
|
||||
**For Individual Developers**:
|
||||
```bash
|
||||
/plugin install development-essentials
|
||||
/plugin install advanced-ai-agents
|
||||
```
|
||||
|
||||
**For Code Quality Focus**:
|
||||
```bash
|
||||
/plugin install development-essentials # Includes /review
|
||||
/plugin install bmad-agile-workflow # Includes bmad-review
|
||||
```
|
||||
|
||||
## 📁 Directory Structure
|
||||
|
||||
```
|
||||
myclaude/
|
||||
├── .claude-plugin/
|
||||
│ └── marketplace.json # Plugin registry
|
||||
├── bmad-agile-workflow/
|
||||
│ ├── commands/
|
||||
│ │ └── bmad-pilot.md
|
||||
│ └── agents/
|
||||
│ ├── bmad-po.md
|
||||
│ ├── bmad-architect.md
|
||||
│ ├── bmad-sm.md
|
||||
│ ├── bmad-dev.md
|
||||
│ ├── bmad-review.md
|
||||
│ ├── bmad-qa.md
|
||||
│ └── bmad-orchestrator.md
|
||||
├── requirements-driven-workflow/
|
||||
│ ├── commands/
|
||||
│ │ └── requirements-pilot.md
|
||||
│ └── agents/
|
||||
│ ├── requirements-generate.md
|
||||
│ ├── requirements-code.md
|
||||
│ ├── requirements-review.md
|
||||
│ └── requirements-testing.md
|
||||
├── development-essentials/
|
||||
│ ├── commands/
|
||||
│ │ ├── code.md
|
||||
│ │ ├── debug.md
|
||||
│ │ ├── test.md
|
||||
│ │ └── ... (more commands)
|
||||
│ └── agents/
|
||||
│ ├── code.md
|
||||
│ ├── bugfix.md
|
||||
│ ├── debug.md
|
||||
│ └── develop.md
|
||||
├── advanced-ai-agents/
|
||||
│ └── agents/
|
||||
│ └── gpt5.md
|
||||
└── output-styles/
|
||||
└── bmad-phase-context.md
|
||||
```
|
||||
|
||||
## 🔄 Plugin Dependencies
|
||||
|
||||
**No Dependencies**: All plugins work independently
|
||||
|
||||
**Complementary Combinations**:
|
||||
- BMAD + Advanced Agents (enhanced reviews)
|
||||
- Requirements + Development Essentials (complete toolkit)
|
||||
- All four plugins (full suite)
|
||||
|
||||
## 🛠️ Makefile Reference
|
||||
|
||||
```bash
|
||||
# Installation
|
||||
make install # Install all plugins
|
||||
make deploy-all # Deploy all configurations
|
||||
|
||||
# Selective Deployment
|
||||
make deploy-bmad # BMAD workflow only
|
||||
make deploy-requirements # Requirements workflow only
|
||||
make deploy-commands # All slash commands only
|
||||
make deploy-agents # All agents only
|
||||
|
||||
# Testing
|
||||
make test-bmad # Test BMAD workflow
|
||||
make test-requirements # Test Requirements workflow
|
||||
|
||||
# Cleanup
|
||||
make clean # Remove generated artifacts
|
||||
make help # Show all available commands
|
||||
```
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[BMAD Workflow](BMAD-WORKFLOW.md)** - Complete BMAD guide
|
||||
- **[Requirements Workflow](REQUIREMENTS-WORKFLOW.md)** - Lightweight workflow guide
|
||||
- **[Development Commands](DEVELOPMENT-COMMANDS.md)** - Command reference
|
||||
- **[Quick Start Guide](QUICK-START.md)** - Get started quickly
|
||||
|
||||
## 🔗 External Resources
|
||||
|
||||
- **[Claude Code Plugin Docs](https://docs.claude.com/en/docs/claude-code/plugins)** - Official plugin documentation
|
||||
- **[Claude Code CLI](https://claude.ai/code)** - Claude Code interface
|
||||
|
||||
---
|
||||
|
||||
**Modular Installation** - Install only what you need, when you need it.
|
||||
326
docs/QUICK-START.md
Normal file
326
docs/QUICK-START.md
Normal file
@@ -0,0 +1,326 @@
|
||||
# Quick Start Guide
|
||||
|
||||
> Get started with Claude Code Multi-Agent Workflow System in 5 minutes
|
||||
|
||||
## 🚀 Installation (2 minutes)
|
||||
|
||||
### Option 1: Plugin System (Fastest)
|
||||
|
||||
```bash
|
||||
# Install everything with one command
|
||||
/plugin marketplace add cexll/myclaude
|
||||
```
|
||||
|
||||
### Option 2: Make Install
|
||||
|
||||
```bash
|
||||
git clone https://github.com/cexll/myclaude.git
|
||||
cd myclaude
|
||||
make install
|
||||
```
|
||||
|
||||
### Option 3: Selective Install
|
||||
|
||||
```bash
|
||||
# Install only what you need
|
||||
/plugin install bmad-agile-workflow # Full agile workflow
|
||||
/plugin install development-essentials # Daily coding commands
|
||||
```
|
||||
|
||||
## 🎯 Your First Workflow (3 minutes)
|
||||
|
||||
### Try BMAD Workflow
|
||||
|
||||
Complete agile development automation:
|
||||
|
||||
```bash
|
||||
/bmad-pilot "Build a simple todo list API with CRUD operations"
|
||||
```
|
||||
|
||||
**What happens**:
|
||||
1. **Product Owner** generates requirements (PRD)
|
||||
2. **Architect** designs system architecture
|
||||
3. **Scrum Master** creates sprint plan
|
||||
4. **Developer** implements code
|
||||
5. **Reviewer** performs code review
|
||||
6. **QA** runs tests
|
||||
|
||||
All documents saved to `.claude/specs/todo-list-api/`
|
||||
|
||||
### Try Requirements Workflow
|
||||
|
||||
Fast prototyping:
|
||||
|
||||
```bash
|
||||
/requirements-pilot "Add user authentication to existing API"
|
||||
```
|
||||
|
||||
**What happens**:
|
||||
1. Generate functional requirements
|
||||
2. Implement code
|
||||
3. Review implementation
|
||||
4. Create tests
|
||||
|
||||
### Try Direct Commands
|
||||
|
||||
Quick coding without workflow:
|
||||
|
||||
```bash
|
||||
# Implement a feature
|
||||
/code "Add input validation for email fields"
|
||||
|
||||
# Debug an issue
|
||||
/debug "API returns 500 on missing parameters"
|
||||
|
||||
# Add tests
|
||||
/test "Create unit tests for validation logic"
|
||||
```
|
||||
|
||||
## 📋 Common Use Cases
|
||||
|
||||
### 1. New Feature Development
|
||||
|
||||
**Complex Feature** (use BMAD):
|
||||
```bash
|
||||
/bmad-pilot "User authentication system with OAuth2, MFA, and role-based access control"
|
||||
```
|
||||
|
||||
**Simple Feature** (use Requirements):
|
||||
```bash
|
||||
/requirements-pilot "Add pagination to user list endpoint"
|
||||
```
|
||||
|
||||
**Tiny Feature** (use direct command):
|
||||
```bash
|
||||
/code "Add created_at timestamp to user model"
|
||||
```
|
||||
|
||||
### 2. Bug Fixing
|
||||
|
||||
**Complex Bug** (use debug):
|
||||
```bash
|
||||
/debug "Memory leak in background job processor"
|
||||
```
|
||||
|
||||
**Simple Bug** (use bugfix):
|
||||
```bash
|
||||
/bugfix "Login button not working on mobile Safari"
|
||||
```
|
||||
|
||||
### 3. Code Quality
|
||||
|
||||
**Full Review**:
|
||||
```bash
|
||||
/review "Review authentication module for security issues"
|
||||
```
|
||||
|
||||
**Refactoring**:
|
||||
```bash
|
||||
/refactor "Simplify user validation logic and remove duplication"
|
||||
```
|
||||
|
||||
**Optimization**:
|
||||
```bash
|
||||
/optimize "Reduce database queries in dashboard API"
|
||||
```
|
||||
|
||||
## 🎨 Workflow Selection Guide
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Choose Your Workflow │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
Complex Business Feature + Architecture Needed
|
||||
↓
|
||||
🏢 Use BMAD Workflow
|
||||
/bmad-pilot "description"
|
||||
• 6 specialized agents
|
||||
• Quality gates (PRD ≥90, Design ≥90)
|
||||
• Complete documentation
|
||||
• Sprint planning included
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
|
||||
Clear Requirements + Fast Iteration Needed
|
||||
↓
|
||||
⚡ Use Requirements Workflow
|
||||
/requirements-pilot "description"
|
||||
• 4 phases: Requirements → Code → Review → Test
|
||||
• Quality gate (Requirements ≥90)
|
||||
• Minimal documentation
|
||||
• Direct to implementation
|
||||
|
||||
────────────────────────────────────────────────────────
|
||||
|
||||
Well-Defined Task + No Workflow Overhead
|
||||
↓
|
||||
🔧 Use Direct Commands
|
||||
/code | /debug | /test | /optimize
|
||||
• Single-purpose commands
|
||||
• Immediate execution
|
||||
• No documentation overhead
|
||||
• Perfect for daily tasks
|
||||
```
|
||||
|
||||
## 💡 Tips for Success
|
||||
|
||||
### 1. Be Specific
|
||||
|
||||
**❌ Bad**:
|
||||
```bash
|
||||
/bmad-pilot "Build an app"
|
||||
```
|
||||
|
||||
**✅ Good**:
|
||||
```bash
|
||||
/bmad-pilot "Build a task management API with user authentication, task CRUD,
|
||||
task assignment, and real-time notifications via WebSocket"
|
||||
```
|
||||
|
||||
### 2. Provide Context
|
||||
|
||||
Include relevant technical details:
|
||||
```bash
|
||||
/code "Add Redis caching to user profile endpoint, cache TTL 5 minutes,
|
||||
invalidate on profile update"
|
||||
```
|
||||
|
||||
### 3. Engage with Agents
|
||||
|
||||
During BMAD workflow, provide feedback at quality gates:
|
||||
|
||||
```
|
||||
PO: "Here's the PRD (Score: 85/100)"
|
||||
You: "Add mobile app support and offline mode requirements"
|
||||
PO: "Updated PRD (Score: 94/100) ✅"
|
||||
```
|
||||
|
||||
### 4. Review Generated Artifacts
|
||||
|
||||
Check documents before confirming:
|
||||
- `.claude/specs/{feature}/01-product-requirements.md`
|
||||
- `.claude/specs/{feature}/02-system-architecture.md`
|
||||
- `.claude/specs/{feature}/03-sprint-plan.md`
|
||||
|
||||
### 5. Chain Commands for Complex Tasks
|
||||
|
||||
Break down complex work:
|
||||
```bash
|
||||
/ask "Best approach for implementing real-time chat"
|
||||
/bmad-pilot "Real-time chat system with message history and typing indicators"
|
||||
/test "Add integration tests for chat message delivery"
|
||||
/docs "Document chat API endpoints and WebSocket events"
|
||||
```
|
||||
|
||||
## 🎓 Learning Path
|
||||
|
||||
**Day 1**: Try direct commands
|
||||
```bash
|
||||
/code "simple task"
|
||||
/test "add some tests"
|
||||
/review "check my code"
|
||||
```
|
||||
|
||||
**Day 2**: Try Requirements workflow
|
||||
```bash
|
||||
/requirements-pilot "small feature"
|
||||
```
|
||||
|
||||
**Week 2**: Try BMAD workflow
|
||||
```bash
|
||||
/bmad-pilot "larger feature"
|
||||
```
|
||||
|
||||
**Week 3**: Combine workflows
|
||||
```bash
|
||||
# Use BMAD for planning
|
||||
/bmad-pilot "new module" --direct-dev
|
||||
|
||||
# Use Requirements for sprint tasks
|
||||
/requirements-pilot "individual task from sprint"
|
||||
|
||||
# Use commands for daily work
|
||||
/code "quick fix"
|
||||
/test "add test"
|
||||
```
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
### Explore Documentation
|
||||
|
||||
- **[BMAD Workflow Guide](BMAD-WORKFLOW.md)** - Deep dive into full agile workflow
|
||||
- **[Requirements Workflow Guide](REQUIREMENTS-WORKFLOW.md)** - Learn lightweight development
|
||||
- **[Development Commands Reference](DEVELOPMENT-COMMANDS.md)** - All command details
|
||||
- **[Plugin System Guide](PLUGIN-SYSTEM.md)** - Plugin management
|
||||
|
||||
### Try Advanced Features
|
||||
|
||||
**BMAD Options**:
|
||||
```bash
|
||||
# Skip testing for prototype
|
||||
/bmad-pilot "prototype" --skip-tests
|
||||
|
||||
# Skip sprint planning for quick dev
|
||||
/bmad-pilot "feature" --direct-dev
|
||||
|
||||
# Skip repo scan (if context exists)
|
||||
/bmad-pilot "feature" --skip-scan
|
||||
```
|
||||
|
||||
**Individual Agents**:
|
||||
```bash
|
||||
# Just requirements
|
||||
/bmad-po "feature requirements"
|
||||
|
||||
# Just architecture
|
||||
/bmad-architect "system design"
|
||||
|
||||
# Just orchestration
|
||||
/bmad-orchestrator "complex project coordination"
|
||||
```
|
||||
|
||||
### Check Quality
|
||||
|
||||
Run tests and validation:
|
||||
```bash
|
||||
make test-bmad # Test BMAD workflow
|
||||
make test-requirements # Test Requirements workflow
|
||||
```
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
**Commands not found**?
|
||||
```bash
|
||||
# Verify installation
|
||||
/plugin list
|
||||
|
||||
# Reinstall if needed
|
||||
make install
|
||||
```
|
||||
|
||||
**Agents not working**?
|
||||
```bash
|
||||
# Check agent configuration
|
||||
ls ~/.config/claude/agents/
|
||||
|
||||
# Redeploy agents
|
||||
make deploy-agents
|
||||
```
|
||||
|
||||
**Output styles missing**?
|
||||
```bash
|
||||
# Deploy output styles
|
||||
cp output-styles/*.md ~/.config/claude/output-styles/
|
||||
```
|
||||
|
||||
## 📞 Get Help
|
||||
|
||||
- **Issues**: [GitHub Issues](https://github.com/cexll/myclaude/issues)
|
||||
- **Documentation**: [docs/](.)
|
||||
- **Examples**: Check `.claude/specs/` after running workflows
|
||||
- **Make Help**: Run `make help` for all commands
|
||||
|
||||
---
|
||||
|
||||
**You're ready!** Start with `/code "your first task"` and explore from there.
|
||||
259
docs/REQUIREMENTS-WORKFLOW.md
Normal file
259
docs/REQUIREMENTS-WORKFLOW.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Requirements-Driven Workflow Guide
|
||||
|
||||
> Lightweight alternative to BMAD for rapid prototyping and simple feature development
|
||||
|
||||
## 🎯 What is Requirements Workflow?
|
||||
|
||||
A streamlined 4-phase workflow that focuses on getting from requirements to working code quickly:
|
||||
|
||||
**Requirements → Implementation → Review → Testing**
|
||||
|
||||
Perfect for:
|
||||
- Quick prototypes
|
||||
- Small features
|
||||
- Bug fixes with clear scope
|
||||
- Projects without complex architecture needs
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Basic Command
|
||||
|
||||
```bash
|
||||
/requirements-pilot "Implement JWT authentication with refresh tokens"
|
||||
|
||||
# Automated workflow:
|
||||
# 1. Requirements generation (90% quality gate)
|
||||
# 2. Code implementation
|
||||
# 3. Code review
|
||||
# 4. Testing strategy
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Use Requirements Workflow** when:
|
||||
- Feature scope is clear and simple
|
||||
- No complex architecture design needed
|
||||
- Fast iteration is priority
|
||||
- You want minimal workflow overhead
|
||||
|
||||
**Use BMAD Workflow** when:
|
||||
- Complex business requirements
|
||||
- Multiple systems integration
|
||||
- Architecture design is critical
|
||||
- Need detailed sprint planning
|
||||
|
||||
## 📋 Workflow Phases
|
||||
|
||||
### Phase 1: Requirements Generation
|
||||
- **Agent**: `requirements-generate`
|
||||
- **Quality Gate**: Requirements score ≥ 90/100
|
||||
- **Output**: Functional requirements document
|
||||
- **Focus**:
|
||||
- Clear functional requirements
|
||||
- Acceptance criteria
|
||||
- Technical constraints
|
||||
- Implementation notes
|
||||
|
||||
**Quality Criteria (100 points)**:
|
||||
- Clarity (30): Unambiguous, well-defined
|
||||
- Completeness (25): All aspects covered
|
||||
- Testability (20): Clear verification points
|
||||
- Technical Feasibility (15): Realistic implementation
|
||||
- Scope Definition (10): Clear boundaries
|
||||
|
||||
### Phase 2: Code Implementation
|
||||
- **Agent**: `requirements-code`
|
||||
- **Quality Gate**: Code completion
|
||||
- **Output**: Implementation files
|
||||
- **Process**:
|
||||
1. Read requirements + repository context
|
||||
2. Implement features following requirements
|
||||
3. Create or modify code files
|
||||
4. Follow existing code conventions
|
||||
|
||||
### Phase 3: Code Review
|
||||
- **Agent**: `requirements-review`
|
||||
- **Quality Gate**: Pass / Pass with Risk / Fail
|
||||
- **Output**: Review report
|
||||
- **Focus**:
|
||||
- Code quality
|
||||
- Requirements alignment
|
||||
- Security concerns
|
||||
- Performance issues
|
||||
- Best practices compliance
|
||||
|
||||
**Review Status**:
|
||||
- **Pass**: Meets standards, ready for testing
|
||||
- **Pass with Risk**: Minor issues noted
|
||||
- **Fail**: Requires implementation revision
|
||||
|
||||
### Phase 4: Testing Strategy
|
||||
- **Agent**: `requirements-testing`
|
||||
- **Quality Gate**: Test execution
|
||||
- **Output**: Test report
|
||||
- **Process**:
|
||||
1. Create test strategy from requirements
|
||||
2. Generate test cases
|
||||
3. Execute tests (unit, integration)
|
||||
4. Report results
|
||||
|
||||
## 📁 Workflow Artifacts
|
||||
|
||||
Generated in `.claude/requirements/{feature-name}/`:
|
||||
|
||||
```
|
||||
.claude/requirements/jwt-authentication/
|
||||
├── 01-requirements.md # Functional requirements (score ≥ 90)
|
||||
├── 02-implementation.md # Implementation summary
|
||||
├── 03-review.md # Code review report
|
||||
└── 04-testing.md # Test strategy and results
|
||||
```
|
||||
|
||||
## 🔧 Command Options
|
||||
|
||||
```bash
|
||||
# Standard workflow
|
||||
/requirements-pilot "Add API rate limiting"
|
||||
|
||||
# With specific technology
|
||||
/requirements-pilot "Redis caching layer with TTL management"
|
||||
|
||||
# Bug fix with requirements
|
||||
/requirements-pilot "Fix login session timeout issue"
|
||||
```
|
||||
|
||||
## 📊 Quality Scoring
|
||||
|
||||
### Requirements Score (100 points)
|
||||
|
||||
| Category | Points | Description |
|
||||
|----------|--------|-------------|
|
||||
| Clarity | 30 | Unambiguous, well-defined requirements |
|
||||
| Completeness | 25 | All functional aspects covered |
|
||||
| Testability | 20 | Clear acceptance criteria |
|
||||
| Technical Feasibility | 15 | Realistic implementation plan |
|
||||
| Scope Definition | 10 | Clear feature boundaries |
|
||||
|
||||
**Threshold**: ≥ 90 points to proceed
|
||||
|
||||
### Automatic Optimization
|
||||
|
||||
If initial score < 90:
|
||||
1. User provides feedback
|
||||
2. Agent revises requirements
|
||||
3. System recalculates score
|
||||
4. Repeat until ≥ 90
|
||||
5. User confirms → Save → Next phase
|
||||
|
||||
## 🎯 Comparison: Requirements vs BMAD
|
||||
|
||||
| Aspect | Requirements Workflow | BMAD Workflow |
|
||||
|--------|----------------------|---------------|
|
||||
| **Phases** | 4 (Requirements → Code → Review → Test) | 6 (PO → Arch → SM → Dev → Review → QA) |
|
||||
| **Duration** | Fast (hours) | Thorough (days) |
|
||||
| **Documentation** | Minimal | Comprehensive |
|
||||
| **Quality Gates** | 1 (Requirements ≥ 90) | 2 (PRD ≥ 90, Design ≥ 90) |
|
||||
| **Approval Points** | None | Multiple (after PRD, Architecture, Sprint Plan) |
|
||||
| **Best For** | Simple features, prototypes | Complex features, enterprise projects |
|
||||
| **Artifacts** | 4 documents | 6 documents |
|
||||
| **Planning** | Direct implementation | Sprint planning included |
|
||||
| **Architecture** | Implicit in requirements | Explicit design phase |
|
||||
|
||||
## 💡 Usage Examples
|
||||
|
||||
### Example 1: API Feature
|
||||
|
||||
```bash
|
||||
/requirements-pilot "REST API endpoint for user profile updates with validation"
|
||||
|
||||
# Generated requirements include:
|
||||
# - Endpoint specification (PUT /api/users/:id/profile)
|
||||
# - Request/response schemas
|
||||
# - Validation rules
|
||||
# - Error handling
|
||||
# - Authentication requirements
|
||||
|
||||
# Implementation follows directly
|
||||
# Review checks API best practices
|
||||
# Testing includes endpoint testing
|
||||
```
|
||||
|
||||
### Example 2: Database Schema
|
||||
|
||||
```bash
|
||||
/requirements-pilot "Add audit logging table for user actions"
|
||||
|
||||
# Generated requirements include:
|
||||
# - Table schema definition
|
||||
# - Indexing strategy
|
||||
# - Retention policy
|
||||
# - Query patterns
|
||||
|
||||
# Implementation creates migration
|
||||
# Review checks schema design
|
||||
# Testing verifies logging behavior
|
||||
```
|
||||
|
||||
### Example 3: Bug Fix
|
||||
|
||||
```bash
|
||||
/requirements-pilot "Fix race condition in order processing queue"
|
||||
|
||||
# Generated requirements include:
|
||||
# - Problem description
|
||||
# - Root cause analysis
|
||||
# - Solution approach
|
||||
# - Verification steps
|
||||
|
||||
# Implementation applies fix
|
||||
# Review checks concurrency handling
|
||||
# Testing includes stress tests
|
||||
```
|
||||
|
||||
## 🔄 Iterative Refinement
|
||||
|
||||
Each phase supports feedback:
|
||||
|
||||
```
|
||||
Agent: "Requirements complete (Score: 85/100)"
|
||||
User: "Add error handling for network failures"
|
||||
Agent: "Updated requirements (Score: 93/100) ✅"
|
||||
```
|
||||
|
||||
## 🚀 Advanced Usage
|
||||
|
||||
### Combining with Individual Commands
|
||||
|
||||
```bash
|
||||
# Generate requirements only
|
||||
/requirements-generate "OAuth2 integration requirements"
|
||||
|
||||
# Just code implementation (requires existing requirements)
|
||||
/requirements-code "Implement based on requirements.md"
|
||||
|
||||
# Standalone review
|
||||
/requirements-review "Review current implementation"
|
||||
```
|
||||
|
||||
### Integration with BMAD
|
||||
|
||||
Use Requirements Workflow for sub-tasks within BMAD sprints:
|
||||
|
||||
```bash
|
||||
# BMAD creates sprint plan
|
||||
/bmad-pilot "E-commerce platform"
|
||||
|
||||
# Use Requirements for individual sprint tasks
|
||||
/requirements-pilot "Shopping cart session management"
|
||||
/requirements-pilot "Payment webhook handling"
|
||||
```
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- **[BMAD Workflow](BMAD-WORKFLOW.md)** - Full agile methodology
|
||||
- **[Development Commands](DEVELOPMENT-COMMANDS.md)** - Direct coding commands
|
||||
- **[Quick Start Guide](QUICK-START.md)** - Get started quickly
|
||||
|
||||
---
|
||||
|
||||
**Requirements-Driven Development** - From requirements to working code in hours, not days.
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
82
hooks/pre-commit.sh
Executable file
82
hooks/pre-commit.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/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="$(printf '%s\n' "$STAGED_FILES" | grep '\.go$' || true)"
|
||||
if [ -n "$GO_FILES" ]; then
|
||||
echo "Checking Go files..."
|
||||
|
||||
if ! command -v gofmt &> /dev/null; then
|
||||
echo "❌ gofmt not found. Please install Go (gofmt is included with the Go toolchain)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Format check
|
||||
GO_FILE_ARGS=()
|
||||
while IFS= read -r file; do
|
||||
if [ -n "$file" ]; then
|
||||
GO_FILE_ARGS+=("$file")
|
||||
fi
|
||||
done <<< "$GO_FILES"
|
||||
|
||||
if [ "${#GO_FILE_ARGS[@]}" -gt 0 ]; then
|
||||
UNFORMATTED="$(gofmt -l "${GO_FILE_ARGS[@]}")"
|
||||
if [ -n "$UNFORMATTED" ]; then
|
||||
echo "❌ The following files need formatting:"
|
||||
echo "$UNFORMATTED"
|
||||
echo "Run: gofmt -w <file>"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 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="$(printf '%s\n' "$STAGED_FILES" | grep '\.json$' || true)"
|
||||
if [ -n "$JSON_FILES" ]; then
|
||||
echo "Validating JSON files..."
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "❌ jq not found. Please install jq to validate JSON files."
|
||||
exit 1
|
||||
fi
|
||||
while IFS= read -r file; do
|
||||
if [ -z "$file" ]; then
|
||||
continue
|
||||
fi
|
||||
if ! jq empty "$file" 2>/dev/null; then
|
||||
echo "❌ Invalid JSON: $file"
|
||||
exit 1
|
||||
fi
|
||||
done <<< "$JSON_FILES"
|
||||
fi
|
||||
|
||||
# Check Markdown files
|
||||
MD_FILES="$(printf '%s\n' "$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 "$@"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user