mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-28 09:23:05 +08:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b204ca94e2 | ||
|
|
a39bf72bc2 | ||
|
|
f43244ec3e | ||
|
|
4c25dd8d2f | ||
|
|
19d411a6a2 | ||
|
|
791bd03724 | ||
|
|
8252b67567 | ||
|
|
207d3c5436 | ||
|
|
5fe8c24f55 | ||
|
|
1dd7b23942 | ||
|
|
664d82795a | ||
|
|
7cc7f50f46 | ||
|
|
ebd795c583 |
143
CHANGELOG.md
143
CHANGELOG.md
@@ -2,11 +2,52 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [6.7.0] - 2026-02-10
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- feat(install): per-module agent merge/unmerge for ~/.codeagent/models.json
|
||||||
|
- feat(install): post-install verification (wrapper version, PATH, backend CLIs)
|
||||||
|
- feat(install): install CLAUDE.md by default
|
||||||
|
- feat(docs): document 9 skills, 11 commands, claudekit module, OpenCode backend
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- fix(docs): correct 7-phase → 5-phase for do skill across all docs
|
||||||
|
- fix(install): best-effort default config install (never crashes main flow)
|
||||||
|
- fix(install): interactive quit no longer triggers post-install actions
|
||||||
|
- fix(install): empty parent directory cleanup on copy_file uninstall
|
||||||
|
- fix(install): agent restore on uninstall when shared by multiple modules
|
||||||
|
- fix(docs): remove non-existent on-stop hook references
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
|
||||||
|
- Updated USER_GUIDE.md with 13 CLI flags and OpenCode backend
|
||||||
|
- Updated README.md/README_CN.md with complete module and skill listings
|
||||||
|
- Added templates/models.json.example with all agent presets (do + omo)
|
||||||
|
|
||||||
|
## [6.6.0] - 2026-02-10
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- feat(skills): add per-task skill spec auto-detection and injection
|
||||||
|
- feat: add worktree support and refactor do skill to Python
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- fix(test): set USERPROFILE on Windows for skills tests
|
||||||
|
- fix(do): reuse worktree across phases via DO_WORKTREE_DIR env var
|
||||||
|
- fix(release): auto-generate release notes from git history
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
|
||||||
|
- audit and fix documentation, installation scripts, and default configuration
|
||||||
|
|
||||||
## [6.0.0] - 2026-01-26
|
## [6.0.0] - 2026-01-26
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
||||||
- support `npx github:cexll/myclaude` for installation and execution
|
- support `npx github:stellarlinkco/myclaude` for installation and execution
|
||||||
- default module changed from `dev` to `do`
|
- default module changed from `dev` to `do`
|
||||||
|
|
||||||
### 🚜 Refactor
|
### 🚜 Refactor
|
||||||
@@ -97,7 +138,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- read GEMINI_MODEL from ~/.gemini/.env ([#131](https://github.com/cexll/myclaude/issues/131))
|
- read GEMINI_MODEL from ~/.gemini/.env ([#131](https://github.com/stellarlinkco/myclaude/issues/131))
|
||||||
|
|
||||||
- validate non-empty output message before printing
|
- validate non-empty output message before printing
|
||||||
|
|
||||||
@@ -118,7 +159,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
- update release workflow build path for new directory structure
|
- update release workflow build path for new directory structure
|
||||||
|
|
||||||
- write PATH config to both profile and rc files ([#128](https://github.com/cexll/myclaude/issues/128))
|
- write PATH config to both profile and rc files ([#128](https://github.com/stellarlinkco/myclaude/issues/128))
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
||||||
@@ -143,9 +184,9 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 📚 Documentation
|
### 📚 Documentation
|
||||||
|
|
||||||
|
|
||||||
- update 'Agent Hierarchy' model for frontend-ui-ux-engineer and document-writer in README ([#127](https://github.com/cexll/myclaude/issues/127))
|
- update 'Agent Hierarchy' model for frontend-ui-ux-engineer and document-writer in README ([#127](https://github.com/stellarlinkco/myclaude/issues/127))
|
||||||
|
|
||||||
- update mappings for frontend-ui-ux-engineer and document-writer in README ([#126](https://github.com/cexll/myclaude/issues/126))
|
- update mappings for frontend-ui-ux-engineer and document-writer in README ([#126](https://github.com/stellarlinkco/myclaude/issues/126))
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
||||||
@@ -164,7 +205,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- remove extraneous dash arg for opencode stdin mode ([#124](https://github.com/cexll/myclaude/issues/124))
|
- remove extraneous dash arg for opencode stdin mode ([#124](https://github.com/stellarlinkco/myclaude/issues/124))
|
||||||
|
|
||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
@@ -177,7 +218,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- correct default models for oracle and librarian agents ([#120](https://github.com/cexll/myclaude/issues/120))
|
- correct default models for oracle and librarian agents ([#120](https://github.com/stellarlinkco/myclaude/issues/120))
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
||||||
@@ -190,7 +231,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- filter codex 0.84.0 stderr noise logs ([#122](https://github.com/cexll/myclaude/issues/122))
|
- filter codex 0.84.0 stderr noise logs ([#122](https://github.com/stellarlinkco/myclaude/issues/122))
|
||||||
|
|
||||||
- filter codex stderr noise logs
|
- filter codex stderr noise logs
|
||||||
|
|
||||||
@@ -215,11 +256,11 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- propagate SkipPermissions to parallel tasks ([#113](https://github.com/cexll/myclaude/issues/113))
|
- propagate SkipPermissions to parallel tasks ([#113](https://github.com/stellarlinkco/myclaude/issues/113))
|
||||||
|
|
||||||
- add timeout for Windows process termination
|
- add timeout for Windows process termination
|
||||||
|
|
||||||
- reject dash as workdir parameter ([#118](https://github.com/cexll/myclaude/issues/118))
|
- reject dash as workdir parameter ([#118](https://github.com/stellarlinkco/myclaude/issues/118))
|
||||||
|
|
||||||
### 📚 Documentation
|
### 📚 Documentation
|
||||||
|
|
||||||
@@ -274,14 +315,14 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- 修复 Gemini init 事件 session_id 未提取的问题 ([#111](https://github.com/cexll/myclaude/issues/111))
|
- 修复 Gemini init 事件 session_id 未提取的问题 ([#111](https://github.com/stellarlinkco/myclaude/issues/111))
|
||||||
|
|
||||||
- fix codeagent skill TaskOutput
|
- fix codeagent skill TaskOutput
|
||||||
|
|
||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- Merge branch 'master' of github.com:cexll/myclaude
|
- Merge branch 'master' of github.com:stellarlinkco/myclaude
|
||||||
|
|
||||||
- add test-cases skill
|
- add test-cases skill
|
||||||
|
|
||||||
@@ -298,7 +339,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- 修复 Windows 后端退出:taskkill 结束进程树 + turn.completed 支持 ([#108](https://github.com/cexll/myclaude/issues/108))
|
- 修复 Windows 后端退出:taskkill 结束进程树 + turn.completed 支持 ([#108](https://github.com/stellarlinkco/myclaude/issues/108))
|
||||||
|
|
||||||
## [5.4.3] - 2026-01-06
|
## [5.4.3] - 2026-01-06
|
||||||
|
|
||||||
@@ -306,7 +347,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- support model parameter for all backends, auto-inject from settings ([#105](https://github.com/cexll/myclaude/issues/105))
|
- support model parameter for all backends, auto-inject from settings ([#105](https://github.com/stellarlinkco/myclaude/issues/105))
|
||||||
|
|
||||||
### 📚 Documentation
|
### 📚 Documentation
|
||||||
|
|
||||||
@@ -326,7 +367,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- replace setx with reg add to avoid 1024-char PATH truncation ([#101](https://github.com/cexll/myclaude/issues/101))
|
- replace setx with reg add to avoid 1024-char PATH truncation ([#101](https://github.com/stellarlinkco/myclaude/issues/101))
|
||||||
|
|
||||||
## [5.4.1] - 2025-12-26
|
## [5.4.1] - 2025-12-26
|
||||||
|
|
||||||
@@ -334,21 +375,21 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- 移除未知事件格式的日志噪声 ([#96](https://github.com/cexll/myclaude/issues/96))
|
- 移除未知事件格式的日志噪声 ([#96](https://github.com/stellarlinkco/myclaude/issues/96))
|
||||||
|
|
||||||
- prevent duplicate PATH entries on reinstall ([#95](https://github.com/cexll/myclaude/issues/95))
|
- prevent duplicate PATH entries on reinstall ([#95](https://github.com/stellarlinkco/myclaude/issues/95))
|
||||||
|
|
||||||
### 📚 Documentation
|
### 📚 Documentation
|
||||||
|
|
||||||
|
|
||||||
- 添加 FAQ 常见问题章节
|
- 添加 FAQ 常见问题章节
|
||||||
|
|
||||||
- update troubleshooting with idempotent PATH commands ([#95](https://github.com/cexll/myclaude/issues/95))
|
- update troubleshooting with idempotent PATH commands ([#95](https://github.com/stellarlinkco/myclaude/issues/95))
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
||||||
|
|
||||||
- Add intelligent backend selection based on task complexity ([#61](https://github.com/cexll/myclaude/issues/61))
|
- Add intelligent backend selection based on task complexity ([#61](https://github.com/stellarlinkco/myclaude/issues/61))
|
||||||
|
|
||||||
## [5.4.0] - 2025-12-24
|
## [5.4.0] - 2025-12-24
|
||||||
|
|
||||||
@@ -363,7 +404,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
||||||
|
|
||||||
- v5.4.0 structured execution report ([#94](https://github.com/cexll/myclaude/issues/94))
|
- v5.4.0 structured execution report ([#94](https://github.com/stellarlinkco/myclaude/issues/94))
|
||||||
|
|
||||||
## [5.2.8] - 2025-12-22
|
## [5.2.8] - 2025-12-22
|
||||||
|
|
||||||
@@ -389,21 +430,21 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- allow claude backend to read env from setting.json while preventing recursion ([#92](https://github.com/cexll/myclaude/issues/92))
|
- allow claude backend to read env from setting.json while preventing recursion ([#92](https://github.com/stellarlinkco/myclaude/issues/92))
|
||||||
|
|
||||||
- comprehensive security and quality improvements for PR #85 & #87 ([#90](https://github.com/cexll/myclaude/issues/90))
|
- comprehensive security and quality improvements for PR #85 & #87 ([#90](https://github.com/stellarlinkco/myclaude/issues/90))
|
||||||
|
|
||||||
- Parser重复解析优化 + 严重bug修复 + PR #86兼容性 ([#88](https://github.com/cexll/myclaude/issues/88))
|
- Parser重复解析优化 + 严重bug修复 + PR #86兼容性 ([#88](https://github.com/stellarlinkco/myclaude/issues/88))
|
||||||
|
|
||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- Improve backend termination after message and extend timeout ([#86](https://github.com/cexll/myclaude/issues/86))
|
- Improve backend termination after message and extend timeout ([#86](https://github.com/stellarlinkco/myclaude/issues/86))
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
||||||
|
|
||||||
- add millisecond-precision timestamps to all log entries ([#91](https://github.com/cexll/myclaude/issues/91))
|
- add millisecond-precision timestamps to all log entries ([#91](https://github.com/stellarlinkco/myclaude/issues/91))
|
||||||
|
|
||||||
## [5.2.6] - 2025-12-19
|
## [5.2.6] - 2025-12-19
|
||||||
|
|
||||||
@@ -411,16 +452,16 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- filter noisy stderr output from gemini backend ([#83](https://github.com/cexll/myclaude/issues/83))
|
- filter noisy stderr output from gemini backend ([#83](https://github.com/stellarlinkco/myclaude/issues/83))
|
||||||
|
|
||||||
- 修復 wsl install.sh 格式問題 ([#78](https://github.com/cexll/myclaude/issues/78))
|
- 修復 wsl install.sh 格式問題 ([#78](https://github.com/stellarlinkco/myclaude/issues/78))
|
||||||
|
|
||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- update all readme
|
- update all readme
|
||||||
|
|
||||||
- BMADh和Requirements-Driven支持根据语义生成对应的文档 ([#82](https://github.com/cexll/myclaude/issues/82))
|
- BMADh和Requirements-Driven支持根据语义生成对应的文档 ([#82](https://github.com/stellarlinkco/myclaude/issues/82))
|
||||||
|
|
||||||
## [5.2.5] - 2025-12-17
|
## [5.2.5] - 2025-12-17
|
||||||
|
|
||||||
@@ -428,7 +469,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- 修复多 backend 并行日志 PID 混乱并移除包装格式 ([#74](https://github.com/cexll/myclaude/issues/74)) ([#76](https://github.com/cexll/myclaude/issues/76))
|
- 修复多 backend 并行日志 PID 混乱并移除包装格式 ([#74](https://github.com/stellarlinkco/myclaude/issues/74)) ([#76](https://github.com/stellarlinkco/myclaude/issues/76))
|
||||||
|
|
||||||
- replace "Codex" to "codeagent" in dev-plan-generator subagent
|
- replace "Codex" to "codeagent" in dev-plan-generator subagent
|
||||||
|
|
||||||
@@ -439,7 +480,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
- Merge pull request #71 from aliceric27/master
|
- Merge pull request #71 from aliceric27/master
|
||||||
|
|
||||||
- Merge branch 'cexll:master' into master
|
- Merge branch 'stellarlinkco:master' into master
|
||||||
|
|
||||||
- Merge pull request #72 from changxvv/master
|
- Merge pull request #72 from changxvv/master
|
||||||
|
|
||||||
@@ -467,13 +508,13 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- Merge pull request #70 from cexll/fix/prevent-codeagent-infinite-recursion
|
- Merge pull request #70 from stellarlinkco/fix/prevent-codeagent-infinite-recursion
|
||||||
|
|
||||||
- Merge pull request #69 from cexll/myclaude-master-20251215-073053-338465000
|
- Merge pull request #69 from stellarlinkco/myclaude-master-20251215-073053-338465000
|
||||||
|
|
||||||
- update CHANGELOG.md
|
- update CHANGELOG.md
|
||||||
|
|
||||||
- Merge pull request #65 from cexll/fix/issue-64-buffer-overflow
|
- Merge pull request #65 from stellarlinkco/fix/issue-64-buffer-overflow
|
||||||
|
|
||||||
## [5.2.3] - 2025-12-15
|
## [5.2.3] - 2025-12-15
|
||||||
|
|
||||||
@@ -481,7 +522,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- 修复 bufio.Scanner token too long 错误 ([#64](https://github.com/cexll/myclaude/issues/64))
|
- 修复 bufio.Scanner token too long 错误 ([#64](https://github.com/stellarlinkco/myclaude/issues/64))
|
||||||
|
|
||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
@@ -568,7 +609,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
- Merge rc/5.2 into master: v5.2.0 release improvements
|
- Merge rc/5.2 into master: v5.2.0 release improvements
|
||||||
|
|
||||||
- Merge pull request #53 from cexll/rc/5.2
|
- Merge pull request #53 from stellarlinkco/rc/5.2
|
||||||
|
|
||||||
- remove docs
|
- remove docs
|
||||||
|
|
||||||
@@ -586,7 +627,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
- Merge branch 'master' into rc/5.2
|
- Merge branch 'master' into rc/5.2
|
||||||
|
|
||||||
- Merge pull request #52 from cexll/fix/parallel-log-path-on-startup
|
- Merge pull request #52 from stellarlinkco/fix/parallel-log-path-on-startup
|
||||||
|
|
||||||
### 📚 Documentation
|
### 📚 Documentation
|
||||||
|
|
||||||
@@ -643,7 +684,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- Merge pull request #51 from cexll/fix/channel-sync-race-conditions
|
- Merge pull request #51 from stellarlinkco/fix/channel-sync-race-conditions
|
||||||
|
|
||||||
- change codex-wrapper version
|
- change codex-wrapper version
|
||||||
|
|
||||||
@@ -660,7 +701,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- Merge pull request #49 from cexll/freespace8/master
|
- Merge pull request #49 from stellarlinkco/freespace8/master
|
||||||
|
|
||||||
- resolve signal handling conflict preserving testability and Windows support
|
- resolve signal handling conflict preserving testability and Windows support
|
||||||
|
|
||||||
@@ -710,7 +751,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- Merge branch 'master' of github.com:cexll/myclaude
|
- Merge branch 'master' of github.com:stellarlinkco/myclaude
|
||||||
|
|
||||||
- Merge pull request #43 from gurdasnijor/smithery/add-badge
|
- Merge pull request #43 from gurdasnijor/smithery/add-badge
|
||||||
|
|
||||||
@@ -754,7 +795,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- Merge pull request #41 from cexll/fix-async-log
|
- Merge pull request #41 from stellarlinkco/fix-async-log
|
||||||
|
|
||||||
- remove test case 90
|
- remove test case 90
|
||||||
|
|
||||||
@@ -799,7 +840,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- Merge pull request #34 from cexll/cce-worktree-master-20251129-111802-997076000
|
- Merge pull request #34 from stellarlinkco/cce-worktree-master-20251129-111802-997076000
|
||||||
|
|
||||||
- update CLAUDE.md and codex skill
|
- update CLAUDE.md and codex skill
|
||||||
|
|
||||||
@@ -851,7 +892,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
|
||||||
- update repository URLs to cexll/myclaude
|
- update repository URLs to stellarlinkco/myclaude
|
||||||
|
|
||||||
## [4.7-alpha1] - 2025-11-27
|
## [4.7-alpha1] - 2025-11-27
|
||||||
|
|
||||||
@@ -864,7 +905,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- Merge pull request #29 from cexll/feat/codex-wrapper
|
- Merge pull request #29 from stellarlinkco/feat/codex-wrapper
|
||||||
|
|
||||||
- Add codex-wrapper Go implementation
|
- Add codex-wrapper Go implementation
|
||||||
|
|
||||||
@@ -916,9 +957,9 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
- update codex skills model config
|
- update codex skills model config
|
||||||
|
|
||||||
- Merge branch 'master' of github.com:cexll/myclaude
|
- Merge branch 'master' of github.com:stellarlinkco/myclaude
|
||||||
|
|
||||||
- Merge pull request #24 from cexll/swe-agent/23-1763544297
|
- Merge pull request #24 from stellarlinkco/swe-agent/23-1763544297
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
||||||
@@ -984,7 +1025,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
- optimize codex skills
|
- optimize codex skills
|
||||||
|
|
||||||
- Merge branch 'master' of github.com:cexll/myclaude
|
- Merge branch 'master' of github.com:stellarlinkco/myclaude
|
||||||
|
|
||||||
- Rename SKILLS.md to SKILL.md
|
- Rename SKILLS.md to SKILL.md
|
||||||
|
|
||||||
@@ -1021,9 +1062,9 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- Merge branch 'master' of github.com:cexll/myclaude
|
- Merge branch 'master' of github.com:stellarlinkco/myclaude
|
||||||
|
|
||||||
- Merge pull request #18 from cexll/swe-agent/17-1760969135
|
- Merge pull request #18 from stellarlinkco/swe-agent/17-1760969135
|
||||||
|
|
||||||
- update requirements clarity
|
- update requirements clarity
|
||||||
|
|
||||||
@@ -1051,13 +1092,13 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- Merge pull request #15 from cexll/swe-agent/13-1760944712
|
- Merge pull request #15 from stellarlinkco/swe-agent/13-1760944712
|
||||||
|
|
||||||
- Fix #13: Clean up redundant README files
|
- Fix #13: Clean up redundant README files
|
||||||
|
|
||||||
- Optimize README structure - Solution A (modular)
|
- Optimize README structure - Solution A (modular)
|
||||||
|
|
||||||
- Merge pull request #14 from cexll/swe-agent/12-1760944588
|
- Merge pull request #14 from stellarlinkco/swe-agent/12-1760944588
|
||||||
|
|
||||||
- Fix #12: Update Makefile install paths for new directory structure
|
- Fix #12: Update Makefile install paths for new directory structure
|
||||||
|
|
||||||
@@ -1067,7 +1108,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### 💼 Other
|
### 💼 Other
|
||||||
|
|
||||||
|
|
||||||
- Merge pull request #11 from cexll/swe-agent/10-1760752533
|
- Merge pull request #11 from stellarlinkco/swe-agent/10-1760752533
|
||||||
|
|
||||||
- Fix marketplace metadata references
|
- Fix marketplace metadata references
|
||||||
|
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -7,12 +7,12 @@
|
|||||||
help:
|
help:
|
||||||
@echo "Claude Code Multi-Agent Workflow - Quick Deployment"
|
@echo "Claude Code Multi-Agent Workflow - Quick Deployment"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Recommended installation: npx github:cexll/myclaude"
|
@echo "Recommended installation: npx github:stellarlinkco/myclaude"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Usage: make [target]"
|
@echo "Usage: make [target]"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@echo " install - LEGACY: install all configurations (prefer npx github:cexll/myclaude)"
|
@echo " install - LEGACY: install all configurations (prefer npx github:stellarlinkco/myclaude)"
|
||||||
@echo " deploy-bmad - Deploy BMAD workflow (bmad-pilot)"
|
@echo " deploy-bmad - Deploy BMAD workflow (bmad-pilot)"
|
||||||
@echo " deploy-requirements - Deploy Requirements workflow (requirements-pilot)"
|
@echo " deploy-requirements - Deploy Requirements workflow (requirements-pilot)"
|
||||||
@echo " deploy-essentials - Deploy Development Essentials workflow"
|
@echo " deploy-essentials - Deploy Development Essentials workflow"
|
||||||
@@ -40,7 +40,7 @@ OUTPUT_STYLES_DIR = output-styles
|
|||||||
# Install all configurations
|
# Install all configurations
|
||||||
install: deploy-all
|
install: deploy-all
|
||||||
@echo "⚠️ LEGACY PATH: make install will be removed in future versions."
|
@echo "⚠️ LEGACY PATH: make install will be removed in future versions."
|
||||||
@echo " Prefer: npx github:cexll/myclaude"
|
@echo " Prefer: npx github:stellarlinkco/myclaude"
|
||||||
@echo "✅ Installation complete!"
|
@echo "✅ Installation complete!"
|
||||||
|
|
||||||
# Deploy BMAD workflow
|
# Deploy BMAD workflow
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Claude Code plugins for this repo are defined in `.claude-plugin/marketplace.jso
|
|||||||
## Install
|
## Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/plugin marketplace add cexll/myclaude
|
/plugin marketplace add stellarlinkco/myclaude
|
||||||
/plugin list
|
/plugin list
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
58
README.md
58
README.md
@@ -2,45 +2,62 @@
|
|||||||
|
|
||||||
# Claude Code Multi-Agent Workflow System
|
# Claude Code Multi-Agent Workflow System
|
||||||
|
|
||||||
[](https://smithery.ai/skills?ns=cexll&utm_source=github&utm_medium=badge)
|
[](https://smithery.ai/skills?ns=stellarlinkco&utm_source=github&utm_medium=badge)
|
||||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||||
[](https://claude.ai/code)
|
[](https://claude.ai/code)
|
||||||
[](https://github.com/cexll/myclaude)
|
[](https://github.com/stellarlinkco/myclaude)
|
||||||
|
|
||||||
> AI-powered development automation with multi-backend execution (Codex/Claude/Gemini/OpenCode)
|
> AI-powered development automation with multi-backend execution (Codex/Claude/Gemini/OpenCode)
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx github:cexll/myclaude
|
npx github:stellarlinkco/myclaude
|
||||||
```
|
```
|
||||||
|
|
||||||
## Modules Overview
|
## Modules Overview
|
||||||
|
|
||||||
| Module | Description | Documentation |
|
| Module | Description | Documentation |
|
||||||
|--------|-------------|---------------|
|
|--------|-------------|---------------|
|
||||||
| [do](skills/do/README.md) | **Recommended** - 7-phase feature development with codeagent orchestration | `/do` command |
|
| [do](skills/do/README.md) | **Recommended** - 5-phase feature development with codeagent orchestration | `/do` command |
|
||||||
| [omo](skills/omo/README.md) | Multi-agent orchestration with intelligent routing | `/omo` command |
|
| [omo](skills/omo/README.md) | Multi-agent orchestration with intelligent routing | `/omo` command |
|
||||||
| [bmad](agents/bmad/README.md) | BMAD agile workflow with 6 specialized agents | `/bmad-pilot` command |
|
| [bmad](agents/bmad/README.md) | BMAD agile workflow with 6 specialized agents | `/bmad-pilot` command |
|
||||||
| [requirements](agents/requirements/README.md) | Lightweight requirements-to-code pipeline | `/requirements-pilot` command |
|
| [requirements](agents/requirements/README.md) | Lightweight requirements-to-code pipeline | `/requirements-pilot` command |
|
||||||
| [essentials](agents/development-essentials/README.md) | Core development commands and utilities | `/code`, `/debug`, etc. |
|
| [essentials](agents/development-essentials/README.md) | 11 core dev commands: ask, bugfix, code, debug, docs, enhance-prompt, optimize, refactor, review, test, think | `/code`, `/debug`, etc. |
|
||||||
| [sparv](skills/sparv/README.md) | SPARV workflow (Specify→Plan→Act→Review→Vault) | `/sparv` command |
|
| [sparv](skills/sparv/README.md) | SPARV workflow (Specify→Plan→Act→Review→Vault) | `/sparv` command |
|
||||||
| course | Course development (combines dev + product-requirements + test-cases) | Composite module |
|
| course | Course development (combines dev + product-requirements + test-cases) | Composite module |
|
||||||
|
| claudekit | ClaudeKit: do skill + global hooks (pre-bash, inject-spec, log-prompt) | Composite module |
|
||||||
|
|
||||||
|
### Available Skills
|
||||||
|
|
||||||
|
Individual skills can be installed separately via `npx github:stellarlinkco/myclaude --list` (skills bundled in modules like do, omo, sparv are listed above):
|
||||||
|
|
||||||
|
| Skill | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| browser | Browser automation for web testing and data extraction |
|
||||||
|
| codeagent | codeagent-wrapper invocation for multi-backend AI code tasks |
|
||||||
|
| codex | Direct Codex backend execution |
|
||||||
|
| dev | Lightweight end-to-end development workflow |
|
||||||
|
| gemini | Direct Gemini backend execution |
|
||||||
|
| product-requirements | Interactive PRD generation with quality scoring |
|
||||||
|
| prototype-prompt-generator | Structured UI/UX prototype prompt generation |
|
||||||
|
| skill-install | Install skills from GitHub with security scanning |
|
||||||
|
| test-cases | Comprehensive test case generation from requirements |
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Interactive installer (recommended)
|
# Interactive installer (recommended)
|
||||||
npx github:cexll/myclaude
|
npx github:stellarlinkco/myclaude
|
||||||
|
|
||||||
# List installable items (modules / skills / wrapper)
|
# List installable items (modules / skills / wrapper)
|
||||||
npx github:cexll/myclaude --list
|
npx github:stellarlinkco/myclaude --list
|
||||||
|
|
||||||
# Detect installed modules and update from GitHub
|
# Detect installed modules and update from GitHub
|
||||||
npx github:cexll/myclaude --update
|
npx github:stellarlinkco/myclaude --update
|
||||||
|
|
||||||
# Custom install directory / overwrite
|
# Custom install directory / overwrite
|
||||||
npx github:cexll/myclaude --install-dir ~/.claude --force
|
npx github:stellarlinkco/myclaude --install-dir ~/.claude --force
|
||||||
```
|
```
|
||||||
|
|
||||||
`--update` detects already installed modules in the target install dir (defaults to `~/.claude`, via `installed_modules.json` when present) and updates them from GitHub (latest release) by overwriting the module files.
|
`--update` detects already installed modules in the target install dir (defaults to `~/.claude`, via `installed_modules.json` when present) and updates them from GitHub (latest release) by overwriting the module files.
|
||||||
@@ -87,17 +104,20 @@ Edit `config.json` to enable/disable modules:
|
|||||||
| Codex | `codex e`, `--json`, `-C`, `resume` |
|
| Codex | `codex e`, `--json`, `-C`, `resume` |
|
||||||
| Claude | `--output-format stream-json`, `-r` |
|
| Claude | `--output-format stream-json`, `-r` |
|
||||||
| Gemini | `-o stream-json`, `-y`, `-r` |
|
| Gemini | `-o stream-json`, `-y`, `-r` |
|
||||||
|
| OpenCode | `opencode`, stdin mode |
|
||||||
|
|
||||||
## Directory Structure After Installation
|
## Directory Structure After Installation
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.claude/
|
~/.claude/
|
||||||
├── bin/codeagent-wrapper
|
├── bin/codeagent-wrapper
|
||||||
├── CLAUDE.md
|
├── CLAUDE.md (installed by default)
|
||||||
├── commands/
|
├── commands/ (from essentials module)
|
||||||
├── agents/
|
├── agents/ (from bmad/requirements modules)
|
||||||
├── skills/
|
├── skills/ (from do/omo/sparv/course modules)
|
||||||
└── config.json
|
├── hooks/ (from claudekit module)
|
||||||
|
├── settings.json (auto-generated, hooks config)
|
||||||
|
└── installed_modules.json (auto-generated, tracks modules)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
@@ -112,13 +132,13 @@ Edit `config.json` to enable/disable modules:
|
|||||||
**Codex wrapper not found:**
|
**Codex wrapper not found:**
|
||||||
```bash
|
```bash
|
||||||
# Select: codeagent-wrapper
|
# Select: codeagent-wrapper
|
||||||
npx github:cexll/myclaude
|
npx github:stellarlinkco/myclaude
|
||||||
```
|
```
|
||||||
|
|
||||||
**Module not loading:**
|
**Module not loading:**
|
||||||
```bash
|
```bash
|
||||||
cat ~/.claude/installed_modules.json
|
cat ~/.claude/installed_modules.json
|
||||||
npx github:cexll/myclaude --force
|
npx github:stellarlinkco/myclaude --force
|
||||||
```
|
```
|
||||||
|
|
||||||
**Backend CLI errors:**
|
**Backend CLI errors:**
|
||||||
@@ -136,7 +156,7 @@ which gemini && gemini --version
|
|||||||
| Gemini can't read .gitignore files | Remove from .gitignore or use different backend |
|
| Gemini can't read .gitignore files | Remove from .gitignore or use different backend |
|
||||||
| Codex permission denied | Set `approval_policy = "never"` in ~/.codex/config.yaml |
|
| Codex permission denied | Set `approval_policy = "never"` in ~/.codex/config.yaml |
|
||||||
|
|
||||||
See [GitHub Issues](https://github.com/cexll/myclaude/issues) for more.
|
See [GitHub Issues](https://github.com/stellarlinkco/myclaude/issues) for more.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -144,8 +164,8 @@ AGPL-3.0 - see [LICENSE](LICENSE)
|
|||||||
|
|
||||||
### Commercial Licensing
|
### Commercial Licensing
|
||||||
|
|
||||||
For commercial use without AGPL obligations, contact: evanxian9@gmail.com
|
For commercial use without AGPL obligations, contact: support@stellarlink.co
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
- [GitHub Issues](https://github.com/cexll/myclaude/issues)
|
- [GitHub Issues](https://github.com/stellarlinkco/myclaude/issues)
|
||||||
|
|||||||
64
README_CN.md
64
README_CN.md
@@ -2,27 +2,44 @@
|
|||||||
|
|
||||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||||
[](https://claude.ai/code)
|
[](https://claude.ai/code)
|
||||||
[](https://github.com/cexll/myclaude)
|
[](https://github.com/stellarlinkco/myclaude)
|
||||||
|
|
||||||
> AI 驱动的开发自动化 - 多后端执行架构 (Codex/Claude/Gemini/OpenCode)
|
> AI 驱动的开发自动化 - 多后端执行架构 (Codex/Claude/Gemini/OpenCode)
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx github:cexll/myclaude
|
npx github:stellarlinkco/myclaude
|
||||||
```
|
```
|
||||||
|
|
||||||
## 模块概览
|
## 模块概览
|
||||||
|
|
||||||
| 模块 | 描述 | 文档 |
|
| 模块 | 描述 | 文档 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| [do](skills/do/README.md) | **推荐** - 7 阶段功能开发 + codeagent 编排 | `/do` 命令 |
|
| [do](skills/do/README.md) | **推荐** - 5 阶段功能开发 + codeagent 编排 | `/do` 命令 |
|
||||||
| [omo](skills/omo/README.md) | 多智能体编排 + 智能路由 | `/omo` 命令 |
|
| [omo](skills/omo/README.md) | 多智能体编排 + 智能路由 | `/omo` 命令 |
|
||||||
| [bmad](agents/bmad/README.md) | BMAD 敏捷工作流 + 6 个专业智能体 | `/bmad-pilot` 命令 |
|
| [bmad](agents/bmad/README.md) | BMAD 敏捷工作流 + 6 个专业智能体 | `/bmad-pilot` 命令 |
|
||||||
| [requirements](agents/requirements/README.md) | 轻量级需求到代码流水线 | `/requirements-pilot` 命令 |
|
| [requirements](agents/requirements/README.md) | 轻量级需求到代码流水线 | `/requirements-pilot` 命令 |
|
||||||
| [essentials](agents/development-essentials/README.md) | 核心开发命令和工具 | `/code`, `/debug` 等 |
|
| [essentials](agents/development-essentials/README.md) | 11 个核心开发命令:ask、bugfix、code、debug、docs、enhance-prompt、optimize、refactor、review、test、think | `/code`, `/debug` 等 |
|
||||||
| [sparv](skills/sparv/README.md) | SPARV 工作流 (Specify→Plan→Act→Review→Vault) | `/sparv` 命令 |
|
| [sparv](skills/sparv/README.md) | SPARV 工作流 (Specify→Plan→Act→Review→Vault) | `/sparv` 命令 |
|
||||||
| course | 课程开发(组合 dev + product-requirements + test-cases) | 组合模块 |
|
| course | 课程开发(组合 dev + product-requirements + test-cases) | 组合模块 |
|
||||||
|
| claudekit | ClaudeKit:do 技能 + 全局钩子(pre-bash、inject-spec、log-prompt)| 组合模块 |
|
||||||
|
|
||||||
|
### 可用技能
|
||||||
|
|
||||||
|
可通过 `npx github:stellarlinkco/myclaude --list` 单独安装技能(模块内置技能如 do、omo、sparv 见上表):
|
||||||
|
|
||||||
|
| 技能 | 描述 |
|
||||||
|
|------|------|
|
||||||
|
| browser | 浏览器自动化测试和数据提取 |
|
||||||
|
| codeagent | codeagent-wrapper 多后端 AI 代码任务调用 |
|
||||||
|
| codex | Codex 后端直接执行 |
|
||||||
|
| dev | 轻量级端到端开发工作流 |
|
||||||
|
| gemini | Gemini 后端直接执行 |
|
||||||
|
| product-requirements | 交互式 PRD 生成(含质量评分)|
|
||||||
|
| prototype-prompt-generator | 结构化 UI/UX 原型提示词生成 |
|
||||||
|
| skill-install | 从 GitHub 安装技能(含安全扫描)|
|
||||||
|
| test-cases | 从需求生成全面测试用例 |
|
||||||
|
|
||||||
## 核心架构
|
## 核心架构
|
||||||
|
|
||||||
@@ -35,22 +52,20 @@ npx github:cexll/myclaude
|
|||||||
|
|
||||||
### do 工作流(推荐)
|
### do 工作流(推荐)
|
||||||
|
|
||||||
7 阶段功能开发,通过 codeagent-wrapper 编排多个智能体。**大多数功能开发任务的首选工作流。**
|
5 阶段功能开发,通过 codeagent-wrapper 编排多个智能体。**大多数功能开发任务的首选工作流。**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/do "添加用户登录功能"
|
/do "添加用户登录功能"
|
||||||
```
|
```
|
||||||
|
|
||||||
**7 阶段:**
|
**5 阶段:**
|
||||||
| 阶段 | 名称 | 目标 |
|
| 阶段 | 名称 | 目标 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 1 | Discovery | 理解需求 |
|
| 1 | Understand | 并行探索理解需求和映射代码库 |
|
||||||
| 2 | Exploration | 映射代码库模式 |
|
| 2 | Clarify | 解决阻塞性歧义(条件触发)|
|
||||||
| 3 | Clarification | 解决歧义(**强制**)|
|
| 3 | Design | 产出最小变更实现方案 |
|
||||||
| 4 | Architecture | 设计实现方案 |
|
| 4 | Implement + Review | 构建功能并审查 |
|
||||||
| 5 | Implementation | 构建功能(**需审批**)|
|
| 5 | Complete | 记录构建结果 |
|
||||||
| 6 | Review | 捕获缺陷 |
|
|
||||||
| 7 | Summary | 记录结果 |
|
|
||||||
|
|
||||||
**智能体:**
|
**智能体:**
|
||||||
- `code-explorer` - 代码追踪、架构映射
|
- `code-explorer` - 代码追踪、架构映射
|
||||||
@@ -162,6 +177,10 @@ npx github:cexll/myclaude
|
|||||||
| `/optimize` | 性能优化 |
|
| `/optimize` | 性能优化 |
|
||||||
| `/refactor` | 代码重构 |
|
| `/refactor` | 代码重构 |
|
||||||
| `/docs` | 编写文档 |
|
| `/docs` | 编写文档 |
|
||||||
|
| `/ask` | 提问和咨询 |
|
||||||
|
| `/bugfix` | Bug 修复 |
|
||||||
|
| `/enhance-prompt` | 提示词优化 |
|
||||||
|
| `/think` | 深度思考分析 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -169,16 +188,16 @@ npx github:cexll/myclaude
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 交互式安装器(推荐)
|
# 交互式安装器(推荐)
|
||||||
npx github:cexll/myclaude
|
npx github:stellarlinkco/myclaude
|
||||||
|
|
||||||
# 列出可安装项(module:* / skill:* / codeagent-wrapper)
|
# 列出可安装项(module:* / skill:* / codeagent-wrapper)
|
||||||
npx github:cexll/myclaude --list
|
npx github:stellarlinkco/myclaude --list
|
||||||
|
|
||||||
# 检测已安装 modules 并从 GitHub 更新
|
# 检测已安装 modules 并从 GitHub 更新
|
||||||
npx github:cexll/myclaude --update
|
npx github:stellarlinkco/myclaude --update
|
||||||
|
|
||||||
# 指定安装目录 / 强制覆盖
|
# 指定安装目录 / 强制覆盖
|
||||||
npx github:cexll/myclaude --install-dir ~/.claude --force
|
npx github:stellarlinkco/myclaude --install-dir ~/.claude --force
|
||||||
```
|
```
|
||||||
|
|
||||||
`--update` 会在目标安装目录(默认 `~/.claude`,优先读取 `installed_modules.json`)检测已安装 modules,并从 GitHub 拉取最新发布版本覆盖更新。
|
`--update` 会在目标安装目录(默认 `~/.claude`,优先读取 `installed_modules.json`)检测已安装 modules,并从 GitHub 拉取最新发布版本覆盖更新。
|
||||||
@@ -218,19 +237,20 @@ npx github:cexll/myclaude --install-dir ~/.claude --force
|
|||||||
| Codex | `codex e`, `--json`, `-C`, `resume` |
|
| Codex | `codex e`, `--json`, `-C`, `resume` |
|
||||||
| Claude | `--output-format stream-json`, `-r` |
|
| Claude | `--output-format stream-json`, `-r` |
|
||||||
| Gemini | `-o stream-json`, `-y`, `-r` |
|
| Gemini | `-o stream-json`, `-y`, `-r` |
|
||||||
|
| OpenCode | `opencode`, stdin 模式 |
|
||||||
|
|
||||||
## 故障排查
|
## 故障排查
|
||||||
|
|
||||||
**Codex wrapper 未找到:**
|
**Codex wrapper 未找到:**
|
||||||
```bash
|
```bash
|
||||||
# 选择:codeagent-wrapper
|
# 选择:codeagent-wrapper
|
||||||
npx github:cexll/myclaude
|
npx github:stellarlinkco/myclaude
|
||||||
```
|
```
|
||||||
|
|
||||||
**模块未加载:**
|
**模块未加载:**
|
||||||
```bash
|
```bash
|
||||||
cat ~/.claude/installed_modules.json
|
cat ~/.claude/installed_modules.json
|
||||||
npx github:cexll/myclaude --force
|
npx github:stellarlinkco/myclaude --force
|
||||||
```
|
```
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
@@ -241,7 +261,7 @@ npx github:cexll/myclaude --force
|
|||||||
| Gemini 无法读取 .gitignore 文件 | 从 .gitignore 移除或使用其他后端 |
|
| Gemini 无法读取 .gitignore 文件 | 从 .gitignore 移除或使用其他后端 |
|
||||||
| Codex 权限拒绝 | 在 ~/.codex/config.yaml 设置 `approval_policy = "never"` |
|
| Codex 权限拒绝 | 在 ~/.codex/config.yaml 设置 `approval_policy = "never"` |
|
||||||
|
|
||||||
更多问题请访问 [GitHub Issues](https://github.com/cexll/myclaude/issues)。
|
更多问题请访问 [GitHub Issues](https://github.com/stellarlinkco/myclaude/issues)。
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
@@ -249,8 +269,8 @@ AGPL-3.0 - 查看 [LICENSE](LICENSE)
|
|||||||
|
|
||||||
### 商业授权
|
### 商业授权
|
||||||
|
|
||||||
如需商业授权(无需遵守 AGPL 义务),请联系:evanxian9@gmail.com
|
如需商业授权(无需遵守 AGPL 义务),请联系:support@stellarlink.co
|
||||||
|
|
||||||
## 支持
|
## 支持
|
||||||
|
|
||||||
- [GitHub Issues](https://github.com/cexll/myclaude/issues)
|
- [GitHub Issues](https://github.com/stellarlinkco/myclaude/issues)
|
||||||
|
|||||||
208
bin/cli.js
208
bin/cli.js
@@ -8,13 +8,15 @@ const os = require("os");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const readline = require("readline");
|
const readline = require("readline");
|
||||||
const zlib = require("zlib");
|
const zlib = require("zlib");
|
||||||
const { spawn } = require("child_process");
|
const { spawn, spawnSync } = require("child_process");
|
||||||
|
|
||||||
const REPO = { owner: "cexll", name: "myclaude" };
|
const REPO = { owner: "stellarlinkco", name: "myclaude" };
|
||||||
const API_HEADERS = {
|
const API_HEADERS = {
|
||||||
"User-Agent": "myclaude-npx",
|
"User-Agent": "myclaude-npx",
|
||||||
Accept: "application/vnd.github+json",
|
Accept: "application/vnd.github+json",
|
||||||
};
|
};
|
||||||
|
const WRAPPER_REQUIRED_MODULES = new Set(["do", "omo"]);
|
||||||
|
const WRAPPER_REQUIRED_SKILLS = new Set(["dev"]);
|
||||||
|
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
const out = {
|
const out = {
|
||||||
@@ -58,12 +60,12 @@ function printHelp() {
|
|||||||
"myclaude (npx installer)",
|
"myclaude (npx installer)",
|
||||||
"",
|
"",
|
||||||
"Usage:",
|
"Usage:",
|
||||||
" npx github:cexll/myclaude",
|
" npx github:stellarlinkco/myclaude",
|
||||||
" npx github:cexll/myclaude --list",
|
" npx github:stellarlinkco/myclaude --list",
|
||||||
" npx github:cexll/myclaude --update",
|
" npx github:stellarlinkco/myclaude --update",
|
||||||
" npx github:cexll/myclaude --install-dir ~/.claude --force",
|
" npx github:stellarlinkco/myclaude --install-dir ~/.claude --force",
|
||||||
" npx github:cexll/myclaude uninstall",
|
" npx github:stellarlinkco/myclaude uninstall",
|
||||||
" npx github:cexll/myclaude uninstall --module bmad,do -y",
|
" npx github:stellarlinkco/myclaude uninstall --module bmad,do -y",
|
||||||
"",
|
"",
|
||||||
"Options:",
|
"Options:",
|
||||||
" --install-dir <path> Default: ~/.claude",
|
" --install-dir <path> Default: ~/.claude",
|
||||||
@@ -499,9 +501,19 @@ async function updateInstalledModules(installDir, tag, config, dryRun) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await fs.promises.mkdir(installDir, { recursive: true });
|
await fs.promises.mkdir(installDir, { recursive: true });
|
||||||
|
const installState = { wrapperInstalled: false };
|
||||||
|
|
||||||
|
async function ensureWrapperInstalled() {
|
||||||
|
if (installState.wrapperInstalled) return;
|
||||||
|
process.stdout.write("Installing codeagent-wrapper...\n");
|
||||||
|
await runInstallSh(repoRoot, installDir, tag);
|
||||||
|
installState.wrapperInstalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
for (const name of toUpdate) {
|
for (const name of toUpdate) {
|
||||||
|
if (WRAPPER_REQUIRED_MODULES.has(name)) await ensureWrapperInstalled();
|
||||||
process.stdout.write(`Updating module: ${name}\n`);
|
process.stdout.write(`Updating module: ${name}\n`);
|
||||||
const r = await applyModule(name, config, repoRoot, installDir, true, tag);
|
const r = await applyModule(name, config, repoRoot, installDir, true, tag, installState);
|
||||||
upsertModuleStatus(installDir, r);
|
upsertModuleStatus(installDir, r);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -777,7 +789,57 @@ async function rmTree(p) {
|
|||||||
await fs.promises.rmdir(p, { recursive: true });
|
await fs.promises.rmdir(p, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyModule(moduleName, config, repoRoot, installDir, force, tag) {
|
function defaultModelsConfig() {
|
||||||
|
return {
|
||||||
|
default_backend: "codex",
|
||||||
|
default_model: "gpt-4.1",
|
||||||
|
backends: {},
|
||||||
|
agents: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeModuleAgentsToModels(moduleName, mod, repoRoot) {
|
||||||
|
const moduleAgents = mod && mod.agents;
|
||||||
|
if (!isPlainObject(moduleAgents) || !Object.keys(moduleAgents).length) return false;
|
||||||
|
|
||||||
|
const modelsPath = path.join(os.homedir(), ".codeagent", "models.json");
|
||||||
|
fs.mkdirSync(path.dirname(modelsPath), { recursive: true });
|
||||||
|
|
||||||
|
let models;
|
||||||
|
if (fs.existsSync(modelsPath)) {
|
||||||
|
models = JSON.parse(fs.readFileSync(modelsPath, "utf8"));
|
||||||
|
} else {
|
||||||
|
const templatePath = path.join(repoRoot, "templates", "models.json.example");
|
||||||
|
if (fs.existsSync(templatePath)) {
|
||||||
|
models = JSON.parse(fs.readFileSync(templatePath, "utf8"));
|
||||||
|
if (!isPlainObject(models)) models = defaultModelsConfig();
|
||||||
|
models.agents = {};
|
||||||
|
} else {
|
||||||
|
models = defaultModelsConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPlainObject(models)) models = defaultModelsConfig();
|
||||||
|
if (!isPlainObject(models.agents)) models.agents = {};
|
||||||
|
|
||||||
|
let modified = false;
|
||||||
|
for (const [agentName, agentCfg] of Object.entries(moduleAgents)) {
|
||||||
|
if (!isPlainObject(agentCfg)) continue;
|
||||||
|
const existing = models.agents[agentName];
|
||||||
|
const canOverwrite = !isPlainObject(existing) || Object.prototype.hasOwnProperty.call(existing, "__module__");
|
||||||
|
if (!canOverwrite) continue;
|
||||||
|
const next = { ...agentCfg, __module__: moduleName };
|
||||||
|
if (!deepEqual(existing, next)) {
|
||||||
|
models.agents[agentName] = next;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modified) fs.writeFileSync(modelsPath, JSON.stringify(models, null, 2) + "\n", "utf8");
|
||||||
|
return modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyModule(moduleName, config, repoRoot, installDir, force, tag, installState) {
|
||||||
const mod = config && config.modules && config.modules[moduleName];
|
const mod = config && config.modules && config.modules[moduleName];
|
||||||
if (!mod) throw new Error(`Unknown module: ${moduleName}`);
|
if (!mod) throw new Error(`Unknown module: ${moduleName}`);
|
||||||
const ops = Array.isArray(mod.operations) ? mod.operations : [];
|
const ops = Array.isArray(mod.operations) ? mod.operations : [];
|
||||||
@@ -803,7 +865,12 @@ async function applyModule(moduleName, config, repoRoot, installDir, force, tag)
|
|||||||
if (cmd !== "bash install.sh") {
|
if (cmd !== "bash install.sh") {
|
||||||
throw new Error(`Refusing run_command: ${cmd || "(empty)"}`);
|
throw new Error(`Refusing run_command: ${cmd || "(empty)"}`);
|
||||||
}
|
}
|
||||||
|
if (installState && installState.wrapperInstalled) {
|
||||||
|
result.operations.push({ type, status: "success", skipped: true });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
await runInstallSh(repoRoot, installDir, tag);
|
await runInstallSh(repoRoot, installDir, tag);
|
||||||
|
if (installState) installState.wrapperInstalled = true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported operation type: ${type}`);
|
throw new Error(`Unsupported operation type: ${type}`);
|
||||||
}
|
}
|
||||||
@@ -834,6 +901,19 @@ async function applyModule(moduleName, config, repoRoot, installDir, force, tag)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (mergeModuleAgentsToModels(moduleName, mod, repoRoot)) {
|
||||||
|
result.has_agents = true;
|
||||||
|
result.operations.push({ type: "merge_agents", status: "success" });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
result.operations.push({
|
||||||
|
type: "merge_agents",
|
||||||
|
status: "failed",
|
||||||
|
error: err && err.message ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -931,6 +1011,63 @@ async function uninstallModule(moduleName, config, repoRoot, installDir, dryRun)
|
|||||||
deleteModuleStatus(installDir, moduleName);
|
deleteModuleStatus(installDir, moduleName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function installDefaultConfigs(installDir, repoRoot) {
|
||||||
|
try {
|
||||||
|
const claudeMdTarget = path.join(installDir, "CLAUDE.md");
|
||||||
|
const claudeMdSrc = path.join(repoRoot, "memorys", "CLAUDE.md");
|
||||||
|
if (!fs.existsSync(claudeMdTarget) && fs.existsSync(claudeMdSrc)) {
|
||||||
|
await fs.promises.copyFile(claudeMdSrc, claudeMdTarget);
|
||||||
|
process.stdout.write(`Installed CLAUDE.md to ${claudeMdTarget}\n`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
process.stderr.write(`Warning: could not install default configs: ${err.message}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printPostInstallInfo(installDir) {
|
||||||
|
process.stdout.write("\n");
|
||||||
|
|
||||||
|
// Check codeagent-wrapper version
|
||||||
|
const wrapperBin = path.join(installDir, "bin", "codeagent-wrapper");
|
||||||
|
let wrapperVersion = null;
|
||||||
|
try {
|
||||||
|
const r = spawnSync(wrapperBin, ["--version"], { timeout: 5000 });
|
||||||
|
if (r.status === 0 && r.stdout) {
|
||||||
|
wrapperVersion = r.stdout.toString().trim();
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Check PATH
|
||||||
|
const binDir = path.join(installDir, "bin");
|
||||||
|
const envPath = process.env.PATH || "";
|
||||||
|
const pathOk = envPath.split(path.delimiter).some((p) => {
|
||||||
|
try { return fs.realpathSync(p) === fs.realpathSync(binDir); } catch { return p === binDir; }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check backend CLIs
|
||||||
|
const whichCmd = process.platform === "win32" ? "where" : "which";
|
||||||
|
const backends = ["codex", "claude", "gemini", "opencode"];
|
||||||
|
const detected = {};
|
||||||
|
for (const name of backends) {
|
||||||
|
try {
|
||||||
|
const r = spawnSync(whichCmd, [name], { timeout: 3000 });
|
||||||
|
detected[name] = r.status === 0;
|
||||||
|
} catch {
|
||||||
|
detected[name] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write("Setup Complete!\n");
|
||||||
|
process.stdout.write(` codeagent-wrapper: ${wrapperVersion || "(not found)"} ${wrapperVersion ? "✓" : "✗"}\n`);
|
||||||
|
process.stdout.write(` PATH: ${binDir} ${pathOk ? "✓" : "✗ (not in PATH)"}\n`);
|
||||||
|
process.stdout.write("\nBackend CLIs detected:\n");
|
||||||
|
process.stdout.write(" " + backends.map((b) => `${b} ${detected[b] ? "✓" : "✗"}`).join(" | ") + "\n");
|
||||||
|
process.stdout.write("\nNext steps:\n");
|
||||||
|
process.stdout.write(" 1. Configure API keys in ~/.codeagent/models.json\n");
|
||||||
|
process.stdout.write(' 2. Try: /do "your first task"\n');
|
||||||
|
process.stdout.write("\n");
|
||||||
|
}
|
||||||
|
|
||||||
async function installSelected(picks, tag, config, installDir, force, dryRun) {
|
async function installSelected(picks, tag, config, installDir, force, dryRun) {
|
||||||
const needRepo = picks.some((p) => p.kind !== "wrapper");
|
const needRepo = picks.some((p) => p.kind !== "wrapper");
|
||||||
const needWrapper = picks.some((p) => p.kind === "wrapper");
|
const needWrapper = picks.some((p) => p.kind === "wrapper");
|
||||||
@@ -949,34 +1086,54 @@ async function installSelected(picks, tag, config, installDir, force, dryRun) {
|
|||||||
try {
|
try {
|
||||||
let repoRoot = repoRootFromHere();
|
let repoRoot = repoRootFromHere();
|
||||||
if (needRepo || needWrapper) {
|
if (needRepo || needWrapper) {
|
||||||
if (!tag) throw new Error("No tag available to download");
|
if (tag) {
|
||||||
const archive = path.join(tmp, "src.tgz");
|
const archive = path.join(tmp, "src.tgz");
|
||||||
const url = `https://codeload.github.com/${REPO.owner}/${REPO.name}/tar.gz/refs/tags/${encodeURIComponent(
|
const url = `https://codeload.github.com/${REPO.owner}/${REPO.name}/tar.gz/refs/tags/${encodeURIComponent(
|
||||||
tag
|
tag
|
||||||
)}`;
|
)}`;
|
||||||
process.stdout.write(`Downloading ${REPO.owner}/${REPO.name}@${tag}...\n`);
|
process.stdout.write(`Downloading ${REPO.owner}/${REPO.name}@${tag}...\n`);
|
||||||
await downloadToFile(url, archive);
|
await downloadToFile(url, archive);
|
||||||
process.stdout.write("Extracting...\n");
|
process.stdout.write("Extracting...\n");
|
||||||
const extracted = path.join(tmp, "src");
|
const extracted = path.join(tmp, "src");
|
||||||
await extractTarGz(archive, extracted);
|
await extractTarGz(archive, extracted);
|
||||||
repoRoot = extracted;
|
repoRoot = extracted;
|
||||||
|
} else {
|
||||||
|
process.stdout.write("Offline mode: installing from local package contents.\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.promises.mkdir(installDir, { recursive: true });
|
await fs.promises.mkdir(installDir, { recursive: true });
|
||||||
|
const installState = { wrapperInstalled: false };
|
||||||
|
|
||||||
|
async function ensureWrapperInstalled() {
|
||||||
|
if (installState.wrapperInstalled) return;
|
||||||
|
process.stdout.write("Installing codeagent-wrapper...\n");
|
||||||
|
await runInstallSh(repoRoot, installDir, tag);
|
||||||
|
installState.wrapperInstalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
for (const p of picks) {
|
for (const p of picks) {
|
||||||
if (p.kind === "wrapper") {
|
if (p.kind === "wrapper") {
|
||||||
process.stdout.write("Installing codeagent-wrapper...\n");
|
await ensureWrapperInstalled();
|
||||||
await runInstallSh(repoRoot, installDir, tag);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (p.kind === "module") {
|
if (p.kind === "module") {
|
||||||
|
if (WRAPPER_REQUIRED_MODULES.has(p.moduleName)) await ensureWrapperInstalled();
|
||||||
process.stdout.write(`Installing module: ${p.moduleName}\n`);
|
process.stdout.write(`Installing module: ${p.moduleName}\n`);
|
||||||
const r = await applyModule(p.moduleName, config, repoRoot, installDir, force, tag);
|
const r = await applyModule(
|
||||||
|
p.moduleName,
|
||||||
|
config,
|
||||||
|
repoRoot,
|
||||||
|
installDir,
|
||||||
|
force,
|
||||||
|
tag,
|
||||||
|
installState
|
||||||
|
);
|
||||||
upsertModuleStatus(installDir, r);
|
upsertModuleStatus(installDir, r);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (p.kind === "skill") {
|
if (p.kind === "skill") {
|
||||||
|
if (WRAPPER_REQUIRED_SKILLS.has(p.skillName)) await ensureWrapperInstalled();
|
||||||
process.stdout.write(`Installing skill: ${p.skillName}\n`);
|
process.stdout.write(`Installing skill: ${p.skillName}\n`);
|
||||||
await copyDirRecursive(
|
await copyDirRecursive(
|
||||||
path.join(repoRoot, "skills", p.skillName),
|
path.join(repoRoot, "skills", p.skillName),
|
||||||
@@ -985,6 +1142,9 @@ async function installSelected(picks, tag, config, installDir, force, dryRun) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await installDefaultConfigs(installDir, repoRoot);
|
||||||
|
printPostInstallInfo(installDir);
|
||||||
} finally {
|
} finally {
|
||||||
await rmTree(tmp);
|
await rmTree(tmp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ filter_unconventional = false
|
|||||||
split_commits = false
|
split_commits = false
|
||||||
# regex for preprocessing the commit messages
|
# regex for preprocessing the commit messages
|
||||||
commit_preprocessors = [
|
commit_preprocessors = [
|
||||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/cexll/myclaude/issues/${2}))" },
|
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/stellarlinkco/myclaude/issues/${2}))" },
|
||||||
]
|
]
|
||||||
# regex for parsing and grouping commits
|
# regex for parsing and grouping commits
|
||||||
commit_parsers = [
|
commit_parsers = [
|
||||||
|
|||||||
@@ -1,97 +1,158 @@
|
|||||||
# codeagent-wrapper
|
# codeagent-wrapper
|
||||||
|
|
||||||
`codeagent-wrapper` 是一个用 Go 编写的“多后端 AI 代码代理”命令行包装器:用统一的 CLI 入口封装不同的 AI 工具后端(Codex / Claude / Gemini / Opencode),并提供一致的参数、配置与会话恢复体验。
|
[English](README.md) | [中文](README_CN.md)
|
||||||
|
|
||||||
入口:`cmd/codeagent/main.go`(生成二进制名:`codeagent`)和 `cmd/codeagent-wrapper/main.go`(生成二进制名:`codeagent-wrapper`)。两者行为一致。
|
A multi-backend AI code agent CLI wrapper written in Go. Provides a unified CLI entry point wrapping different AI tool backends (Codex / Claude / Gemini / OpenCode) with consistent flags, configuration, skill injection, and session resumption.
|
||||||
|
|
||||||
## 功能特性
|
Entry point: `cmd/codeagent-wrapper/main.go` (binary: `codeagent-wrapper`).
|
||||||
|
|
||||||
- 多后端支持:`codex` / `claude` / `gemini` / `opencode`
|
## Features
|
||||||
- 统一命令行:`codeagent [flags] <task>` / `codeagent resume <session_id> <task> [workdir]`
|
|
||||||
- 自动 stdin:遇到换行/特殊字符/超长任务自动走 stdin,避免 shell quoting 地狱;也可显式使用 `-`
|
|
||||||
- 配置合并:支持配置文件与 `CODEAGENT_*` 环境变量(viper)
|
|
||||||
- Agent 预设:从 `~/.codeagent/models.json` 读取 backend/model/prompt 等预设
|
|
||||||
- 并行执行:`--parallel` 从 stdin 读取多任务配置,支持依赖拓扑并发执行
|
|
||||||
- 日志清理:`codeagent cleanup` 清理旧日志(日志写入系统临时目录)
|
|
||||||
|
|
||||||
## 安装
|
- **Multi-backend support**: `codex` / `claude` / `gemini` / `opencode`
|
||||||
|
- **Unified CLI**: `codeagent-wrapper [flags] <task>` / `codeagent-wrapper resume <session_id> <task> [workdir]`
|
||||||
|
- **Auto stdin**: Automatically pipes via stdin when task contains newlines, special characters, or exceeds length; also supports explicit `-`
|
||||||
|
- **Config merging**: Config files + `CODEAGENT_*` environment variables (viper)
|
||||||
|
- **Agent presets**: Read backend/model/prompt/reasoning/yolo/allowed_tools from `~/.codeagent/models.json`
|
||||||
|
- **Dynamic agents**: Place a `{name}.md` prompt file in `~/.codeagent/agents/` to use as an agent
|
||||||
|
- **Skill auto-injection**: `--skills` for manual specification, or auto-detect from project tech stack (Go/Rust/Python/Node.js/Vue)
|
||||||
|
- **Git worktree isolation**: `--worktree` executes tasks in an isolated git worktree with auto-generated task_id and branch
|
||||||
|
- **Parallel execution**: `--parallel` reads multi-task config from stdin with dependency-aware topological concurrent execution and structured summary reports
|
||||||
|
- **Backend config**: `backends` section in `models.json` supports per-backend `base_url` / `api_key` injection
|
||||||
|
- **Claude tool control**: `allowed_tools` / `disallowed_tools` to restrict available tools for Claude backend
|
||||||
|
- **Stderr noise filtering**: Automatically filters noisy stderr output from Gemini and Codex backends
|
||||||
|
- **Log cleanup**: `codeagent-wrapper cleanup` cleans old logs (logs written to system temp directory)
|
||||||
|
- **Cross-platform**: macOS / Linux / Windows
|
||||||
|
|
||||||
要求:Go 1.21+。
|
## Installation
|
||||||
|
|
||||||
在仓库根目录执行:
|
### Recommended (interactive installer)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install ./cmd/codeagent
|
npx github:stellarlinkco/myclaude
|
||||||
go install ./cmd/codeagent-wrapper
|
|
||||||
```
|
```
|
||||||
|
|
||||||
安装后确认:
|
Select the `codeagent-wrapper` module to install.
|
||||||
|
|
||||||
|
### Manual build
|
||||||
|
|
||||||
|
Requires: Go 1.21+.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codeagent version
|
# Build from source
|
||||||
codeagent-wrapper version
|
make build
|
||||||
|
|
||||||
|
# Or install to $GOPATH/bin
|
||||||
|
make install
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用示例
|
Verify installation:
|
||||||
|
|
||||||
最简单用法(默认后端:`codex`):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codeagent "分析 internal/app/cli.go 的入口逻辑,给出改进建议"
|
codeagent-wrapper --version
|
||||||
```
|
```
|
||||||
|
|
||||||
指定后端:
|
## Usage
|
||||||
|
|
||||||
|
Basic usage (default backend: `codex`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codeagent --backend claude "解释 internal/executor/parallel_config.go 的并行配置格式"
|
codeagent-wrapper "analyze the entry logic of internal/app/cli.go"
|
||||||
```
|
```
|
||||||
|
|
||||||
指定工作目录(第 2 个位置参数):
|
Specify backend:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codeagent "在当前 repo 下搜索潜在数据竞争" .
|
codeagent-wrapper --backend claude "explain the parallel config format in internal/executor/parallel_config.go"
|
||||||
```
|
```
|
||||||
|
|
||||||
显式从 stdin 读取 task(使用 `-`):
|
Specify working directory (2nd positional argument):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat task.txt | codeagent -
|
codeagent-wrapper "search for potential data races in this repo" .
|
||||||
```
|
```
|
||||||
|
|
||||||
恢复会话:
|
Explicit stdin (using `-`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codeagent resume <session_id> "继续上次任务"
|
cat task.txt | codeagent-wrapper -
|
||||||
```
|
```
|
||||||
|
|
||||||
并行模式(从 stdin 读取任务配置;禁止位置参数):
|
HEREDOC (recommended for multi-line tasks):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codeagent --parallel <<'EOF'
|
codeagent-wrapper --backend claude - <<'EOF'
|
||||||
|
Implement user authentication:
|
||||||
|
- JWT tokens
|
||||||
|
- bcrypt password hashing
|
||||||
|
- Session management
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Resume session:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper resume <session_id> "continue the previous task"
|
||||||
|
```
|
||||||
|
|
||||||
|
Execute in isolated git worktree:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper --worktree "refactor the auth module"
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual skill injection:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper --skills golang-base-practices "optimize database queries"
|
||||||
|
```
|
||||||
|
|
||||||
|
Parallel mode (task config from stdin):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper --parallel <<'EOF'
|
||||||
---TASK---
|
---TASK---
|
||||||
id: t1
|
id: t1
|
||||||
workdir: .
|
workdir: .
|
||||||
backend: codex
|
backend: codex
|
||||||
---CONTENT---
|
---CONTENT---
|
||||||
列出本项目的主要模块以及它们的职责。
|
List the main modules and their responsibilities.
|
||||||
---TASK---
|
---TASK---
|
||||||
id: t2
|
id: t2
|
||||||
dependencies: t1
|
dependencies: t1
|
||||||
backend: claude
|
backend: claude
|
||||||
---CONTENT---
|
---CONTENT---
|
||||||
基于 t1 的结论,提出重构风险点与建议。
|
Based on t1's findings, identify refactoring risks and suggestions.
|
||||||
EOF
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
## 配置说明
|
## CLI Flags
|
||||||
|
|
||||||
### 配置文件
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--backend <name>` | Backend selection (codex/claude/gemini/opencode) |
|
||||||
|
| `--model <name>` | Model override |
|
||||||
|
| `--agent <name>` | Agent preset name (from models.json or ~/.codeagent/agents/) |
|
||||||
|
| `--prompt-file <path>` | Read prompt from file |
|
||||||
|
| `--skills <names>` | Comma-separated skill names for spec injection |
|
||||||
|
| `--reasoning-effort <level>` | Reasoning effort (backend-specific) |
|
||||||
|
| `--skip-permissions` | Skip permission prompts |
|
||||||
|
| `--dangerously-skip-permissions` | Alias for `--skip-permissions` |
|
||||||
|
| `--worktree` | Execute in a new git worktree (auto-generates task_id) |
|
||||||
|
| `--parallel` | Parallel task mode (config from stdin) |
|
||||||
|
| `--full-output` | Full output in parallel mode (default: summary only) |
|
||||||
|
| `--config <path>` | Config file path (default: `$HOME/.codeagent/config.*`) |
|
||||||
|
| `--version`, `-v` | Print version |
|
||||||
|
| `--cleanup` | Clean up old logs |
|
||||||
|
|
||||||
默认查找路径(当 `--config` 为空时):
|
## Configuration
|
||||||
|
|
||||||
|
### Config File
|
||||||
|
|
||||||
|
Default search path (when `--config` is empty):
|
||||||
|
|
||||||
- `$HOME/.codeagent/config.(yaml|yml|json|toml|...)`
|
- `$HOME/.codeagent/config.(yaml|yml|json|toml|...)`
|
||||||
|
|
||||||
示例(YAML):
|
Example (YAML):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
backend: codex
|
backend: codex
|
||||||
@@ -99,59 +160,113 @@ model: gpt-4.1
|
|||||||
skip-permissions: false
|
skip-permissions: false
|
||||||
```
|
```
|
||||||
|
|
||||||
也可以通过 `--config /path/to/config.yaml` 显式指定。
|
Can also be specified explicitly via `--config /path/to/config.yaml`.
|
||||||
|
|
||||||
### 环境变量(`CODEAGENT_*`)
|
### Environment Variables (`CODEAGENT_*`)
|
||||||
|
|
||||||
通过 viper 读取并自动映射 `-` 为 `_`,常用项:
|
Read via viper with automatic `-` to `_` mapping:
|
||||||
|
|
||||||
- `CODEAGENT_BACKEND`(`codex|claude|gemini|opencode`)
|
| Variable | Description |
|
||||||
- `CODEAGENT_MODEL`
|
|----------|-------------|
|
||||||
- `CODEAGENT_AGENT`
|
| `CODEAGENT_BACKEND` | Backend name (codex/claude/gemini/opencode) |
|
||||||
- `CODEAGENT_PROMPT_FILE`
|
| `CODEAGENT_MODEL` | Model name |
|
||||||
- `CODEAGENT_REASONING_EFFORT`
|
| `CODEAGENT_AGENT` | Agent preset name |
|
||||||
- `CODEAGENT_SKIP_PERMISSIONS`
|
| `CODEAGENT_PROMPT_FILE` | Prompt file path |
|
||||||
- `CODEAGENT_FULL_OUTPUT`(并行模式 legacy 输出)
|
| `CODEAGENT_REASONING_EFFORT` | Reasoning effort |
|
||||||
- `CODEAGENT_MAX_PARALLEL_WORKERS`(0 表示不限制,上限 100)
|
| `CODEAGENT_SKIP_PERMISSIONS` | Skip permission prompts (default true; set `false` to disable) |
|
||||||
|
| `CODEAGENT_FULL_OUTPUT` | Full output in parallel mode |
|
||||||
|
| `CODEAGENT_MAX_PARALLEL_WORKERS` | Parallel worker count (0=unlimited, max 100) |
|
||||||
|
| `CODEAGENT_TMPDIR` | Custom temp directory (for macOS permission issues) |
|
||||||
|
| `CODEX_TIMEOUT` | Timeout in ms (default 7200000 = 2 hours) |
|
||||||
|
| `CODEX_BYPASS_SANDBOX` | Codex sandbox bypass (default true; set `false` to disable) |
|
||||||
|
| `DO_WORKTREE_DIR` | Reuse existing worktree directory (set by /do workflow) |
|
||||||
|
|
||||||
### Agent 预设(`~/.codeagent/models.json`)
|
### Agent Presets (`~/.codeagent/models.json`)
|
||||||
|
|
||||||
可在 `~/.codeagent/models.json` 定义 agent → backend/model/prompt 等映射,用 `--agent <name>` 选择:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"default_backend": "opencode",
|
"default_backend": "codex",
|
||||||
"default_model": "opencode/grok-code",
|
"default_model": "gpt-4.1",
|
||||||
|
"backends": {
|
||||||
|
"codex": { "api_key": "..." },
|
||||||
|
"claude": { "base_url": "http://localhost:23001", "api_key": "..." }
|
||||||
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"develop": {
|
"develop": {
|
||||||
"backend": "codex",
|
"backend": "codex",
|
||||||
"model": "gpt-4.1",
|
"model": "gpt-4.1",
|
||||||
"prompt_file": "~/.codeagent/prompts/develop.md",
|
"prompt_file": "~/.codeagent/prompts/develop.md",
|
||||||
"description": "Code development"
|
"reasoning": "high",
|
||||||
|
"yolo": true,
|
||||||
|
"allowed_tools": ["Read", "Write", "Bash"],
|
||||||
|
"disallowed_tools": ["WebFetch"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 支持的后端
|
Use `--agent <name>` to select a preset. Agents inherit `base_url` / `api_key` from the corresponding `backends` entry.
|
||||||
|
|
||||||
该项目本身不内置模型能力,依赖你本机安装并可在 `PATH` 中找到对应 CLI:
|
### Dynamic Agents
|
||||||
|
|
||||||
- `codex`:执行 `codex e ...`(默认会添加 `--dangerously-bypass-approvals-and-sandbox`;如需关闭请设置 `CODEX_BYPASS_SANDBOX=false`)
|
Place a `{name}.md` file in `~/.codeagent/agents/` to use it via `--agent {name}`. The Markdown file is read as the prompt, using `default_backend` and `default_model`.
|
||||||
- `claude`:执行 `claude -p ... --output-format stream-json`(默认会跳过权限提示;如需开启请设置 `CODEAGENT_SKIP_PERMISSIONS=false`)
|
|
||||||
- `gemini`:执行 `gemini ... -o stream-json`(可从 `~/.gemini/.env` 加载环境变量)
|
|
||||||
- `opencode`:执行 `opencode run --format json`
|
|
||||||
|
|
||||||
## 开发
|
### Skill Auto-Detection
|
||||||
|
|
||||||
```bash
|
When no skills are specified via `--skills`, codeagent-wrapper auto-detects the tech stack from files in the working directory:
|
||||||
make build
|
|
||||||
make test
|
| Detected Files | Injected Skills |
|
||||||
make lint
|
|----------------|-----------------|
|
||||||
make clean
|
| `go.mod` / `go.sum` | `golang-base-practices` |
|
||||||
|
| `Cargo.toml` | `rust-best-practices` |
|
||||||
|
| `pyproject.toml` / `setup.py` / `requirements.txt` | `python-best-practices` |
|
||||||
|
| `package.json` | `vercel-react-best-practices`, `frontend-design` |
|
||||||
|
| `vue.config.js` / `vite.config.ts` / `nuxt.config.ts` | `vue-web-app` |
|
||||||
|
|
||||||
|
Skill specs are read from `~/.claude/skills/{name}/SKILL.md`, subject to a 16000-character budget.
|
||||||
|
|
||||||
|
## Supported Backends
|
||||||
|
|
||||||
|
This project does not embed model capabilities. It requires the corresponding CLI tools installed and available in `PATH`:
|
||||||
|
|
||||||
|
| Backend | Command | Notes |
|
||||||
|
|---------|---------|-------|
|
||||||
|
| `codex` | `codex e ...` | Adds `--dangerously-bypass-approvals-and-sandbox` by default; set `CODEX_BYPASS_SANDBOX=false` to disable |
|
||||||
|
| `claude` | `claude -p ... --output-format stream-json` | Skips permissions and disables setting-sources to prevent recursion; set `CODEAGENT_SKIP_PERMISSIONS=false` to enable prompts; auto-reads env and model from `~/.claude/settings.json` |
|
||||||
|
| `gemini` | `gemini -o stream-json -y ...` | Auto-loads env vars from `~/.gemini/.env` (GEMINI_API_KEY, GEMINI_MODEL, etc.) |
|
||||||
|
| `opencode` | `opencode run --format json` | — |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/codeagent-wrapper/main.go # CLI entry point
|
||||||
|
internal/
|
||||||
|
app/ # CLI command definitions, argument parsing, main orchestration
|
||||||
|
backend/ # Backend abstraction and implementations (codex/claude/gemini/opencode)
|
||||||
|
config/ # Config loading, agent resolution, viper bindings
|
||||||
|
executor/ # Task execution engine: single/parallel/worktree/skill injection
|
||||||
|
logger/ # Structured logging system
|
||||||
|
parser/ # JSON stream parser
|
||||||
|
utils/ # Common utility functions
|
||||||
|
worktree/ # Git worktree management
|
||||||
```
|
```
|
||||||
|
|
||||||
## 故障排查
|
## Development
|
||||||
|
|
||||||
- macOS 下如果看到临时目录相关的 `permission denied`(例如临时可执行文件无法在 `/var/folders/.../T` 执行),可设置一个可执行的临时目录:`CODEAGENT_TMPDIR=$HOME/.codeagent/tmp`。
|
```bash
|
||||||
- `claude` 后端的 `base_url/api_key`(来自 `~/.codeagent/models.json`)会注入到子进程环境变量:`ANTHROPIC_BASE_URL` / `ANTHROPIC_API_KEY`。若 `base_url` 指向本地代理(如 `localhost:23001`),请确认代理进程在运行。
|
make build # Build binary
|
||||||
|
make test # Run tests
|
||||||
|
make lint # golangci-lint + staticcheck
|
||||||
|
make clean # Clean build artifacts
|
||||||
|
make install # Install to $GOPATH/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
CI uses GitHub Actions with Go 1.21 / 1.22 matrix testing.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- On macOS, if you see `permission denied` related to temp directories, set: `CODEAGENT_TMPDIR=$HOME/.codeagent/tmp`
|
||||||
|
- `claude` backend's `base_url` / `api_key` (from `~/.codeagent/models.json` `backends.claude`) are injected as `ANTHROPIC_BASE_URL` / `ANTHROPIC_API_KEY` env vars
|
||||||
|
- `gemini` backend's API key is loaded from `~/.gemini/.env`, injected as `GEMINI_API_KEY` with `GEMINI_API_KEY_AUTH_MECHANISM=bearer` auto-set
|
||||||
|
- Exit codes: 127 = backend not found, 124 = timeout, 130 = interrupted
|
||||||
|
- Parallel mode outputs structured summary by default; use `--full-output` for complete output when debugging
|
||||||
|
|||||||
272
codeagent-wrapper/README_CN.md
Normal file
272
codeagent-wrapper/README_CN.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# codeagent-wrapper
|
||||||
|
|
||||||
|
[English](README.md) | [中文](README_CN.md)
|
||||||
|
|
||||||
|
`codeagent-wrapper` 是一个用 Go 编写的多后端 AI 代码代理命令行包装器:用统一的 CLI 入口封装不同的 AI 工具后端(Codex / Claude / Gemini / OpenCode),并提供一致的参数、配置、技能注入与会话恢复体验。
|
||||||
|
|
||||||
|
入口:`cmd/codeagent-wrapper/main.go`(生成二进制名:`codeagent-wrapper`)。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- **多后端支持**:`codex` / `claude` / `gemini` / `opencode`
|
||||||
|
- **统一命令行**:`codeagent-wrapper [flags] <task>` / `codeagent-wrapper resume <session_id> <task> [workdir]`
|
||||||
|
- **自动 stdin**:遇到换行/特殊字符/超长任务自动走 stdin,避免 shell quoting 问题;也可显式使用 `-`
|
||||||
|
- **配置合并**:支持配置文件与 `CODEAGENT_*` 环境变量(viper)
|
||||||
|
- **Agent 预设**:从 `~/.codeagent/models.json` 读取 backend/model/prompt/reasoning/yolo/allowed_tools 等预设
|
||||||
|
- **动态 Agent**:在 `~/.codeagent/agents/{name}.md` 放置 prompt 文件即可作为 agent 使用
|
||||||
|
- **技能自动注入**:`--skills` 手动指定,或根据项目技术栈自动检测(Go/Rust/Python/Node.js/Vue)并注入对应技能规范
|
||||||
|
- **Git Worktree 隔离**:`--worktree` 在独立 git worktree 中执行任务,自动生成 task_id 和分支
|
||||||
|
- **并行执行**:`--parallel` 从 stdin 读取多任务配置,支持依赖拓扑并发执行,带结构化摘要报告
|
||||||
|
- **后端配置**:`models.json` 的 `backends` 节支持 per-backend 的 `base_url` / `api_key` 注入
|
||||||
|
- **Claude 工具控制**:`allowed_tools` / `disallowed_tools` 限制 Claude 后端可用工具
|
||||||
|
- **Stderr 降噪**:自动过滤 Gemini 和 Codex 后端的噪声 stderr 输出
|
||||||
|
- **日志清理**:`codeagent-wrapper cleanup` 清理旧日志(日志写入系统临时目录)
|
||||||
|
- **跨平台**:支持 macOS / Linux / Windows
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
### 推荐方式(交互式安装器)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx github:stellarlinkco/myclaude
|
||||||
|
```
|
||||||
|
|
||||||
|
选择 `codeagent-wrapper` 模块进行安装。
|
||||||
|
|
||||||
|
### 手动构建
|
||||||
|
|
||||||
|
要求:Go 1.21+。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 从源码构建
|
||||||
|
make build
|
||||||
|
|
||||||
|
# 或直接安装到 $GOPATH/bin
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
安装后确认:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
最简单用法(默认后端:`codex`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper "分析 internal/app/cli.go 的入口逻辑,给出改进建议"
|
||||||
|
```
|
||||||
|
|
||||||
|
指定后端:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper --backend claude "解释 internal/executor/parallel_config.go 的并行配置格式"
|
||||||
|
```
|
||||||
|
|
||||||
|
指定工作目录(第 2 个位置参数):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper "在当前 repo 下搜索潜在数据竞争" .
|
||||||
|
```
|
||||||
|
|
||||||
|
显式从 stdin 读取 task(使用 `-`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat task.txt | codeagent-wrapper -
|
||||||
|
```
|
||||||
|
|
||||||
|
使用 HEREDOC(推荐用于多行任务):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper --backend claude - <<'EOF'
|
||||||
|
实现用户认证系统:
|
||||||
|
- JWT 令牌
|
||||||
|
- bcrypt 密码哈希
|
||||||
|
- 会话管理
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
恢复会话:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper resume <session_id> "继续上次任务"
|
||||||
|
```
|
||||||
|
|
||||||
|
在 git worktree 中隔离执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper --worktree "重构认证模块"
|
||||||
|
```
|
||||||
|
|
||||||
|
手动指定技能注入:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper --skills golang-base-practices "优化数据库查询"
|
||||||
|
```
|
||||||
|
|
||||||
|
并行模式(从 stdin 读取任务配置):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codeagent-wrapper --parallel <<'EOF'
|
||||||
|
---TASK---
|
||||||
|
id: t1
|
||||||
|
workdir: .
|
||||||
|
backend: codex
|
||||||
|
---CONTENT---
|
||||||
|
列出本项目的主要模块以及它们的职责。
|
||||||
|
---TASK---
|
||||||
|
id: t2
|
||||||
|
dependencies: t1
|
||||||
|
backend: claude
|
||||||
|
---CONTENT---
|
||||||
|
基于 t1 的结论,提出重构风险点与建议。
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI 参数
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `--backend <name>` | 后端选择(codex/claude/gemini/opencode) |
|
||||||
|
| `--model <name>` | 覆盖模型 |
|
||||||
|
| `--agent <name>` | Agent 预设名(来自 models.json 或 ~/.codeagent/agents/) |
|
||||||
|
| `--prompt-file <path>` | 从文件读取 prompt |
|
||||||
|
| `--skills <names>` | 逗号分隔的技能名,注入对应规范 |
|
||||||
|
| `--reasoning-effort <level>` | 推理力度(后端相关) |
|
||||||
|
| `--skip-permissions` | 跳过权限提示 |
|
||||||
|
| `--dangerously-skip-permissions` | `--skip-permissions` 的别名 |
|
||||||
|
| `--worktree` | 在新 git worktree 中执行(自动生成 task_id) |
|
||||||
|
| `--parallel` | 并行任务模式(从 stdin 读取配置) |
|
||||||
|
| `--full-output` | 并行模式下输出完整消息(默认仅输出摘要) |
|
||||||
|
| `--config <path>` | 配置文件路径(默认:`$HOME/.codeagent/config.*`) |
|
||||||
|
| `--version`, `-v` | 打印版本号 |
|
||||||
|
| `--cleanup` | 清理旧日志 |
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### 配置文件
|
||||||
|
|
||||||
|
默认查找路径(当 `--config` 为空时):
|
||||||
|
|
||||||
|
- `$HOME/.codeagent/config.(yaml|yml|json|toml|...)`
|
||||||
|
|
||||||
|
示例(YAML):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
backend: codex
|
||||||
|
model: gpt-4.1
|
||||||
|
skip-permissions: false
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以通过 `--config /path/to/config.yaml` 显式指定。
|
||||||
|
|
||||||
|
### 环境变量(`CODEAGENT_*`)
|
||||||
|
|
||||||
|
通过 viper 读取并自动映射 `-` 为 `_`,常用项:
|
||||||
|
|
||||||
|
| 变量 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `CODEAGENT_BACKEND` | 后端名(codex/claude/gemini/opencode) |
|
||||||
|
| `CODEAGENT_MODEL` | 模型名 |
|
||||||
|
| `CODEAGENT_AGENT` | Agent 预设名 |
|
||||||
|
| `CODEAGENT_PROMPT_FILE` | Prompt 文件路径 |
|
||||||
|
| `CODEAGENT_REASONING_EFFORT` | 推理力度 |
|
||||||
|
| `CODEAGENT_SKIP_PERMISSIONS` | 跳过权限提示(默认 true;设 `false` 关闭) |
|
||||||
|
| `CODEAGENT_FULL_OUTPUT` | 并行模式完整输出 |
|
||||||
|
| `CODEAGENT_MAX_PARALLEL_WORKERS` | 并行 worker 数(0=不限制,上限 100) |
|
||||||
|
| `CODEAGENT_TMPDIR` | 自定义临时目录(macOS 权限问题时使用) |
|
||||||
|
| `CODEX_TIMEOUT` | 超时(毫秒,默认 7200000 即 2 小时) |
|
||||||
|
| `CODEX_BYPASS_SANDBOX` | Codex sandbox bypass(默认 true;设 `false` 关闭) |
|
||||||
|
| `DO_WORKTREE_DIR` | 复用已有 worktree 目录(由 /do 工作流设置) |
|
||||||
|
|
||||||
|
### Agent 预设(`~/.codeagent/models.json`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"default_backend": "codex",
|
||||||
|
"default_model": "gpt-4.1",
|
||||||
|
"backends": {
|
||||||
|
"codex": { "api_key": "..." },
|
||||||
|
"claude": { "base_url": "http://localhost:23001", "api_key": "..." }
|
||||||
|
},
|
||||||
|
"agents": {
|
||||||
|
"develop": {
|
||||||
|
"backend": "codex",
|
||||||
|
"model": "gpt-4.1",
|
||||||
|
"prompt_file": "~/.codeagent/prompts/develop.md",
|
||||||
|
"reasoning": "high",
|
||||||
|
"yolo": true,
|
||||||
|
"allowed_tools": ["Read", "Write", "Bash"],
|
||||||
|
"disallowed_tools": ["WebFetch"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
用 `--agent <name>` 选择预设,agent 会继承 `backends` 下对应后端的 `base_url` / `api_key`。
|
||||||
|
|
||||||
|
### 动态 Agent
|
||||||
|
|
||||||
|
在 `~/.codeagent/agents/` 目录放置 `{name}.md` 文件,即可通过 `--agent {name}` 使用,自动读取该 Markdown 作为 prompt,使用 `default_backend` 和 `default_model`。
|
||||||
|
|
||||||
|
### 技能自动检测
|
||||||
|
|
||||||
|
当未通过 `--skills` 显式指定技能时,codeagent-wrapper 会根据工作目录中的文件自动检测技术栈:
|
||||||
|
|
||||||
|
| 检测文件 | 注入技能 |
|
||||||
|
|----------|----------|
|
||||||
|
| `go.mod` / `go.sum` | `golang-base-practices` |
|
||||||
|
| `Cargo.toml` | `rust-best-practices` |
|
||||||
|
| `pyproject.toml` / `setup.py` / `requirements.txt` | `python-best-practices` |
|
||||||
|
| `package.json` | `vercel-react-best-practices`, `frontend-design` |
|
||||||
|
| `vue.config.js` / `vite.config.ts` / `nuxt.config.ts` | `vue-web-app` |
|
||||||
|
|
||||||
|
技能规范从 `~/.claude/skills/{name}/SKILL.md` 读取,受 16000 字符预算限制。
|
||||||
|
|
||||||
|
## 支持的后端
|
||||||
|
|
||||||
|
该项目本身不内置模型能力,依赖本机安装并可在 `PATH` 中找到对应 CLI:
|
||||||
|
|
||||||
|
| 后端 | 执行命令 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| `codex` | `codex e ...` | 默认添加 `--dangerously-bypass-approvals-and-sandbox`;设 `CODEX_BYPASS_SANDBOX=false` 关闭 |
|
||||||
|
| `claude` | `claude -p ... --output-format stream-json` | 默认跳过权限并禁用 setting-sources 防止递归;设 `CODEAGENT_SKIP_PERMISSIONS=false` 开启权限;自动读取 `~/.claude/settings.json` 中的 env 和 model |
|
||||||
|
| `gemini` | `gemini -o stream-json -y ...` | 自动从 `~/.gemini/.env` 加载环境变量(GEMINI_API_KEY, GEMINI_MODEL 等) |
|
||||||
|
| `opencode` | `opencode run --format json` | — |
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/codeagent-wrapper/main.go # CLI 入口
|
||||||
|
internal/
|
||||||
|
app/ # CLI 命令定义、参数解析、主逻辑编排
|
||||||
|
backend/ # 后端抽象与实现(codex/claude/gemini/opencode)
|
||||||
|
config/ # 配置加载、agent 解析、viper 绑定
|
||||||
|
executor/ # 任务执行引擎:单任务/并行/worktree/技能注入
|
||||||
|
logger/ # 结构化日志系统
|
||||||
|
parser/ # JSON stream 解析器
|
||||||
|
utils/ # 通用工具函数
|
||||||
|
worktree/ # Git worktree 管理
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build # 构建
|
||||||
|
make test # 运行测试
|
||||||
|
make lint # golangci-lint + staticcheck
|
||||||
|
make clean # 清理构建产物
|
||||||
|
make install # 安装到 $GOPATH/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
CI 使用 GitHub Actions,Go 1.21 / 1.22 矩阵测试。
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
- macOS 下如果看到临时目录相关的 `permission denied`,可设置:`CODEAGENT_TMPDIR=$HOME/.codeagent/tmp`
|
||||||
|
- `claude` 后端的 `base_url` / `api_key`(来自 `~/.codeagent/models.json` 的 `backends.claude`)会注入到子进程环境变量 `ANTHROPIC_BASE_URL` / `ANTHROPIC_API_KEY`
|
||||||
|
- `gemini` 后端的 API key 从 `~/.gemini/.env` 加载,注入 `GEMINI_API_KEY` 并自动设置 `GEMINI_API_KEY_AUTH_MECHANISM=bearer`
|
||||||
|
- 后端命令未找到时返回退出码 127,超时返回 124,中断返回 130
|
||||||
|
- 并行模式默认输出结构化摘要,使用 `--full-output` 查看完整输出以便调试
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# Codeagent-Wrapper User Guide
|
# Codeagent-Wrapper User Guide
|
||||||
|
|
||||||
Multi-backend AI code execution wrapper supporting Codex, Claude, and Gemini.
|
Multi-backend AI code execution wrapper supporting Codex, Claude, Gemini, and OpenCode.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
`codeagent-wrapper` is a Go-based CLI tool that provides a unified interface to multiple AI coding backends. It handles:
|
`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)
|
- Multi-backend execution (Codex, Claude, Gemini, OpenCode)
|
||||||
- JSON stream parsing and output formatting
|
- JSON stream parsing and output formatting
|
||||||
- Session management and resumption
|
- Session management and resumption
|
||||||
- Parallel task execution with dependency resolution
|
- Parallel task execution with dependency resolution
|
||||||
@@ -15,7 +15,7 @@ Multi-backend AI code execution wrapper supporting Codex, Claude, and Gemini.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Recommended: run the installer and select "codeagent-wrapper"
|
# Recommended: run the installer and select "codeagent-wrapper"
|
||||||
npx github:cexll/myclaude
|
npx github:stellarlinkco/myclaude
|
||||||
|
|
||||||
# Manual build (optional; requires repo checkout)
|
# Manual build (optional; requires repo checkout)
|
||||||
cd codeagent-wrapper
|
cd codeagent-wrapper
|
||||||
@@ -42,6 +42,24 @@ Implement user authentication:
|
|||||||
EOF
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CLI Flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--backend <name>` | Select backend (codex/claude/gemini/opencode) |
|
||||||
|
| `--model <name>` | Override model for this invocation |
|
||||||
|
| `--agent <name>` | Agent preset name (from ~/.codeagent/models.json) |
|
||||||
|
| `--config <path>` | Path to models.json config file |
|
||||||
|
| `--cleanup` | Clean up log files on startup |
|
||||||
|
| `--worktree` | Execute in a new git worktree (auto-generates task ID) |
|
||||||
|
| `--skills <names>` | Comma-separated skill names for spec injection |
|
||||||
|
| `--prompt-file <path>` | Read prompt from file |
|
||||||
|
| `--reasoning-effort <level>` | Set reasoning effort (low/medium/high) |
|
||||||
|
| `--skip-permissions` | Skip permission prompts |
|
||||||
|
| `--parallel` | Enable parallel task execution |
|
||||||
|
| `--full-output` | Show full output in parallel mode |
|
||||||
|
| `--version`, `-v` | Print version and exit |
|
||||||
|
|
||||||
### Backend Selection
|
### Backend Selection
|
||||||
|
|
||||||
| Backend | Command | Best For |
|
| Backend | Command | Best For |
|
||||||
@@ -49,6 +67,7 @@ EOF
|
|||||||
| **Codex** | `--backend codex` | General code tasks (default) |
|
| **Codex** | `--backend codex` | General code tasks (default) |
|
||||||
| **Claude** | `--backend claude` | Complex reasoning, architecture |
|
| **Claude** | `--backend claude` | Complex reasoning, architecture |
|
||||||
| **Gemini** | `--backend gemini` | Fast iteration, prototyping |
|
| **Gemini** | `--backend gemini` | Fast iteration, prototyping |
|
||||||
|
| **OpenCode** | `--backend opencode` | Open-source alternative |
|
||||||
|
|
||||||
## Core Features
|
## Core Features
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type cliOptions struct {
|
|||||||
ReasoningEffort string
|
ReasoningEffort string
|
||||||
Agent string
|
Agent string
|
||||||
PromptFile string
|
PromptFile string
|
||||||
|
Output string
|
||||||
Skills string
|
Skills string
|
||||||
SkipPermissions bool
|
SkipPermissions bool
|
||||||
Worktree bool
|
Worktree bool
|
||||||
@@ -135,6 +136,7 @@ func addRootFlags(fs *pflag.FlagSet, opts *cliOptions) {
|
|||||||
fs.StringVar(&opts.ReasoningEffort, "reasoning-effort", "", "Reasoning effort (backend-specific)")
|
fs.StringVar(&opts.ReasoningEffort, "reasoning-effort", "", "Reasoning effort (backend-specific)")
|
||||||
fs.StringVar(&opts.Agent, "agent", "", "Agent preset name (from ~/.codeagent/models.json)")
|
fs.StringVar(&opts.Agent, "agent", "", "Agent preset name (from ~/.codeagent/models.json)")
|
||||||
fs.StringVar(&opts.PromptFile, "prompt-file", "", "Prompt file path")
|
fs.StringVar(&opts.PromptFile, "prompt-file", "", "Prompt file path")
|
||||||
|
fs.StringVar(&opts.Output, "output", "", "Write structured JSON output to file")
|
||||||
fs.StringVar(&opts.Skills, "skills", "", "Comma-separated skill names for spec injection")
|
fs.StringVar(&opts.Skills, "skills", "", "Comma-separated skill names for spec injection")
|
||||||
|
|
||||||
fs.BoolVar(&opts.SkipPermissions, "skip-permissions", false, "Skip permissions prompts (also via CODEAGENT_SKIP_PERMISSIONS)")
|
fs.BoolVar(&opts.SkipPermissions, "skip-permissions", false, "Skip permissions prompts (also via CODEAGENT_SKIP_PERMISSIONS)")
|
||||||
@@ -198,10 +200,9 @@ func runWithLoggerAndCleanup(fn func() int) (exitCode int) {
|
|||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
fmt.Fprintln(os.Stderr, entry)
|
fmt.Fprintln(os.Stderr, entry)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "Log file: %s (deleted)\n", logger.Path())
|
fmt.Fprintf(os.Stderr, "Log file: %s\n", logger.Path())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = logger.RemoveLogFile()
|
|
||||||
}()
|
}()
|
||||||
defer runCleanupHook()
|
defer runCleanupHook()
|
||||||
|
|
||||||
@@ -237,6 +238,7 @@ func buildSingleConfig(cmd *cobra.Command, args []string, rawArgv []string, opts
|
|||||||
agentName := ""
|
agentName := ""
|
||||||
promptFile := ""
|
promptFile := ""
|
||||||
promptFileExplicit := false
|
promptFileExplicit := false
|
||||||
|
outputPath := ""
|
||||||
yolo := false
|
yolo := false
|
||||||
|
|
||||||
if cmd.Flags().Changed("agent") {
|
if cmd.Flags().Changed("agent") {
|
||||||
@@ -281,6 +283,15 @@ func buildSingleConfig(cmd *cobra.Command, args []string, rawArgv []string, opts
|
|||||||
promptFile = resolvedPromptFile
|
promptFile = resolvedPromptFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flags().Changed("output") {
|
||||||
|
outputPath = strings.TrimSpace(opts.Output)
|
||||||
|
if outputPath == "" {
|
||||||
|
return nil, fmt.Errorf("--output flag requires a value")
|
||||||
|
}
|
||||||
|
} else if val := strings.TrimSpace(v.GetString("output")); val != "" {
|
||||||
|
outputPath = val
|
||||||
|
}
|
||||||
|
|
||||||
agentFlagChanged := cmd.Flags().Changed("agent")
|
agentFlagChanged := cmd.Flags().Changed("agent")
|
||||||
backendFlagChanged := cmd.Flags().Changed("backend")
|
backendFlagChanged := cmd.Flags().Changed("backend")
|
||||||
if backendFlagChanged {
|
if backendFlagChanged {
|
||||||
@@ -357,6 +368,7 @@ func buildSingleConfig(cmd *cobra.Command, args []string, rawArgv []string, opts
|
|||||||
Agent: agentName,
|
Agent: agentName,
|
||||||
PromptFile: promptFile,
|
PromptFile: promptFile,
|
||||||
PromptFileExplicit: promptFileExplicit,
|
PromptFileExplicit: promptFileExplicit,
|
||||||
|
OutputPath: outputPath,
|
||||||
SkipPermissions: skipPermissions,
|
SkipPermissions: skipPermissions,
|
||||||
Yolo: yolo,
|
Yolo: yolo,
|
||||||
Model: model,
|
Model: model,
|
||||||
@@ -432,7 +444,7 @@ func runParallelMode(cmd *cobra.Command, args []string, opts *cliOptions, v *vip
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cmd.Flags().Changed("agent") || cmd.Flags().Changed("prompt-file") || cmd.Flags().Changed("reasoning-effort") || cmd.Flags().Changed("skills") {
|
if cmd.Flags().Changed("agent") || cmd.Flags().Changed("prompt-file") || cmd.Flags().Changed("reasoning-effort") || cmd.Flags().Changed("skills") {
|
||||||
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, "ERROR: --parallel reads its task configuration from stdin; only --backend, --model, --output, --full-output and --skip-permissions are allowed.")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,6 +475,17 @@ func runParallelMode(cmd *cobra.Command, args []string, opts *cliOptions, v *vip
|
|||||||
fullOutput = v.GetBool("full-output")
|
fullOutput = v.GetBool("full-output")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outputPath := ""
|
||||||
|
if cmd.Flags().Changed("output") {
|
||||||
|
outputPath = strings.TrimSpace(opts.Output)
|
||||||
|
if outputPath == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "ERROR: --output flag requires a value")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
} else if val := strings.TrimSpace(v.GetString("output")); val != "" {
|
||||||
|
outputPath = val
|
||||||
|
}
|
||||||
|
|
||||||
skipChanged := cmd.Flags().Changed("skip-permissions") || cmd.Flags().Changed("dangerously-skip-permissions")
|
skipChanged := cmd.Flags().Changed("skip-permissions") || cmd.Flags().Changed("dangerously-skip-permissions")
|
||||||
skipPermissions := false
|
skipPermissions := false
|
||||||
if skipChanged {
|
if skipChanged {
|
||||||
@@ -525,6 +548,11 @@ func runParallelMode(cmd *cobra.Command, args []string, opts *cliOptions, v *vip
|
|||||||
results[i].KeyOutput = extractKeyOutputFromLines(lines, 150)
|
results[i].KeyOutput = extractKeyOutputFromLines(lines, 150)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := writeStructuredOutput(outputPath, results); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println(generateFinalOutputWithMode(results, !fullOutput))
|
fmt.Println(generateFinalOutputWithMode(results, !fullOutput))
|
||||||
|
|
||||||
exitCode := 0
|
exitCode := 0
|
||||||
@@ -688,16 +716,32 @@ func runSingleMode(cfg *Config, name string) int {
|
|||||||
|
|
||||||
result := runTaskFn(taskSpec, false, cfg.Timeout)
|
result := runTaskFn(taskSpec, false, cfg.Timeout)
|
||||||
|
|
||||||
if result.ExitCode != 0 {
|
exitCode := result.ExitCode
|
||||||
return result.ExitCode
|
if exitCode == 0 && strings.TrimSpace(result.Message) == "" {
|
||||||
|
errMsg := fmt.Sprintf("no output message: backend=%s returned empty result.Message with exit_code=0", cfg.Backend)
|
||||||
|
logError(errMsg)
|
||||||
|
exitCode = 1
|
||||||
|
if strings.TrimSpace(result.Error) == "" {
|
||||||
|
result.Error = errMsg
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that we got a meaningful output message
|
if err := writeStructuredOutput(cfg.OutputPath, []TaskResult{result}); err != nil {
|
||||||
if strings.TrimSpace(result.Message) == "" {
|
logError(err.Error())
|
||||||
logError(fmt.Sprintf("no output message: backend=%s returned empty result.Message with exit_code=0", cfg.Backend))
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if exitCode != 0 {
|
||||||
|
// Surface any parsed backend output even on non-zero exit to avoid "(no output)" in tool runners.
|
||||||
|
if strings.TrimSpace(result.Message) != "" {
|
||||||
|
fmt.Println(result.Message)
|
||||||
|
if result.SessionID != "" {
|
||||||
|
fmt.Printf("\n---\nSESSION_ID: %s\n", result.SessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return exitCode
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println(result.Message)
|
fmt.Println(result.Message)
|
||||||
if result.SessionID != "" {
|
if result.SessionID != "" {
|
||||||
fmt.Printf("\n---\nSESSION_ID: %s\n", result.SessionID)
|
fmt.Printf("\n---\nSESSION_ID: %s\n", result.SessionID)
|
||||||
|
|||||||
@@ -169,6 +169,12 @@ func (f *execFakeRunner) Process() executor.ProcessHandle {
|
|||||||
return &execFakeProcess{pid: 1}
|
return &execFakeProcess{pid: 1}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *execFakeRunner) UnsetEnv(keys ...string) {
|
||||||
|
for _, k := range keys {
|
||||||
|
delete(f.env, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExecutorRunCodexTaskWithContext(t *testing.T) {
|
func TestExecutorRunCodexTaskWithContext(t *testing.T) {
|
||||||
defer resetTestHooks()
|
defer resetTestHooks()
|
||||||
|
|
||||||
|
|||||||
@@ -274,6 +274,10 @@ func (d *drainBlockingCmd) Process() executor.ProcessHandle {
|
|||||||
return d.inner.Process()
|
return d.inner.Process()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *drainBlockingCmd) UnsetEnv(keys ...string) {
|
||||||
|
d.inner.UnsetEnv(keys...)
|
||||||
|
}
|
||||||
|
|
||||||
type bufferWriteCloser struct {
|
type bufferWriteCloser struct {
|
||||||
buf bytes.Buffer
|
buf bytes.Buffer
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -568,6 +572,14 @@ func (f *fakeCmd) Process() executor.ProcessHandle {
|
|||||||
return f.process
|
return f.process
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeCmd) UnsetEnv(keys ...string) {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
for _, k := range keys {
|
||||||
|
delete(f.env, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeCmd) runStdoutScript() {
|
func (f *fakeCmd) runStdoutScript() {
|
||||||
if len(f.stdoutPlan) == 0 {
|
if len(f.stdoutPlan) == 0 {
|
||||||
if !f.keepStdoutOpen {
|
if !f.keepStdoutOpen {
|
||||||
@@ -1443,6 +1455,60 @@ func TestBackendParseArgs_PromptFileOverridesAgent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackendParseArgs_OutputFlag(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "output flag",
|
||||||
|
args: []string{"codeagent-wrapper", "--output", "/tmp/out.json", "task"},
|
||||||
|
want: "/tmp/out.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "output equals syntax",
|
||||||
|
args: []string{"codeagent-wrapper", "--output=/tmp/out.json", "task"},
|
||||||
|
want: "/tmp/out.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "output trimmed",
|
||||||
|
args: []string{"codeagent-wrapper", "--output", " /tmp/out.json ", "task"},
|
||||||
|
want: "/tmp/out.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "output missing value",
|
||||||
|
args: []string{"codeagent-wrapper", "--output"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "output equals missing value",
|
||||||
|
args: []string{"codeagent-wrapper", "--output=", "task"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
os.Args = tt.args
|
||||||
|
cfg, err := parseArgs()
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.OutputPath != tt.want {
|
||||||
|
t.Fatalf("OutputPath = %q, want %q", cfg.OutputPath, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBackendParseArgs_SkipPermissions(t *testing.T) {
|
func TestBackendParseArgs_SkipPermissions(t *testing.T) {
|
||||||
const envKey = "CODEAGENT_SKIP_PERMISSIONS"
|
const envKey = "CODEAGENT_SKIP_PERMISSIONS"
|
||||||
t.Setenv(envKey, "true")
|
t.Setenv(envKey, "true")
|
||||||
@@ -3739,6 +3805,245 @@ noop`)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunSingleWithOutputFile(t *testing.T) {
|
||||||
|
defer resetTestHooks()
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
outputPath := filepath.Join(tempDir, "single-output.json")
|
||||||
|
|
||||||
|
oldArgs := os.Args
|
||||||
|
t.Cleanup(func() { os.Args = oldArgs })
|
||||||
|
os.Args = []string{"codeagent-wrapper", "--output", outputPath, "task"}
|
||||||
|
|
||||||
|
stdinReader = strings.NewReader("")
|
||||||
|
isTerminalFn = func() bool { return true }
|
||||||
|
|
||||||
|
origRunTaskFn := runTaskFn
|
||||||
|
runTaskFn = func(taskSpec TaskSpec, silent bool, timeoutSec int) TaskResult {
|
||||||
|
return TaskResult{
|
||||||
|
TaskID: "single-task",
|
||||||
|
ExitCode: 0,
|
||||||
|
Message: "single-result",
|
||||||
|
SessionID: "sid-single",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { runTaskFn = origRunTaskFn })
|
||||||
|
|
||||||
|
if code := run(); code != 0 {
|
||||||
|
t.Fatalf("run exit = %d, want 0", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output file: %v", err)
|
||||||
|
}
|
||||||
|
if len(data) == 0 || data[len(data)-1] != '\n' {
|
||||||
|
t.Fatalf("output file should end with newline, got %q", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
Results []TaskResult `json:"results"`
|
||||||
|
Summary struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Success int `json:"success"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
} `json:"summary"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &payload); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal output json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Summary.Total != 1 || payload.Summary.Success != 1 || payload.Summary.Failed != 0 {
|
||||||
|
t.Fatalf("unexpected summary: %+v", payload.Summary)
|
||||||
|
}
|
||||||
|
if len(payload.Results) != 1 {
|
||||||
|
t.Fatalf("results length = %d, want 1", len(payload.Results))
|
||||||
|
}
|
||||||
|
if payload.Results[0].Message != "single-result" {
|
||||||
|
t.Fatalf("result message = %q, want %q", payload.Results[0].Message, "single-result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunSingleWithOutputFileOnFailureExitCode(t *testing.T) {
|
||||||
|
defer resetTestHooks()
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
outputPath := filepath.Join(tempDir, "single-output-failed.json")
|
||||||
|
|
||||||
|
oldArgs := os.Args
|
||||||
|
t.Cleanup(func() { os.Args = oldArgs })
|
||||||
|
os.Args = []string{"codeagent-wrapper", "--output", outputPath, "task"}
|
||||||
|
|
||||||
|
stdinReader = strings.NewReader("")
|
||||||
|
isTerminalFn = func() bool { return true }
|
||||||
|
|
||||||
|
origRunTaskFn := runTaskFn
|
||||||
|
runTaskFn = func(taskSpec TaskSpec, silent bool, timeoutSec int) TaskResult {
|
||||||
|
return TaskResult{
|
||||||
|
TaskID: "single-task",
|
||||||
|
ExitCode: 7,
|
||||||
|
Message: "failed-result",
|
||||||
|
Error: "backend error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { runTaskFn = origRunTaskFn })
|
||||||
|
|
||||||
|
if code := run(); code != 7 {
|
||||||
|
t.Fatalf("run exit = %d, want 7", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output file: %v", err)
|
||||||
|
}
|
||||||
|
if len(data) == 0 || data[len(data)-1] != '\n' {
|
||||||
|
t.Fatalf("output file should end with newline, got %q", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
Results []TaskResult `json:"results"`
|
||||||
|
Summary struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Success int `json:"success"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
} `json:"summary"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &payload); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal output json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Summary.Total != 1 || payload.Summary.Success != 0 || payload.Summary.Failed != 1 {
|
||||||
|
t.Fatalf("unexpected summary: %+v", payload.Summary)
|
||||||
|
}
|
||||||
|
if len(payload.Results) != 1 {
|
||||||
|
t.Fatalf("results length = %d, want 1", len(payload.Results))
|
||||||
|
}
|
||||||
|
if payload.Results[0].ExitCode != 7 {
|
||||||
|
t.Fatalf("result exit_code = %d, want 7", payload.Results[0].ExitCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunSingleWithOutputFileOnEmptyMessage(t *testing.T) {
|
||||||
|
defer resetTestHooks()
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
outputPath := filepath.Join(tempDir, "single-output-empty.json")
|
||||||
|
|
||||||
|
oldArgs := os.Args
|
||||||
|
t.Cleanup(func() { os.Args = oldArgs })
|
||||||
|
os.Args = []string{"codeagent-wrapper", "--output", outputPath, "task"}
|
||||||
|
|
||||||
|
stdinReader = strings.NewReader("")
|
||||||
|
isTerminalFn = func() bool { return true }
|
||||||
|
|
||||||
|
origRunTaskFn := runTaskFn
|
||||||
|
runTaskFn = func(taskSpec TaskSpec, silent bool, timeoutSec int) TaskResult {
|
||||||
|
return TaskResult{
|
||||||
|
TaskID: "single-task",
|
||||||
|
ExitCode: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { runTaskFn = origRunTaskFn })
|
||||||
|
|
||||||
|
if code := run(); code != 1 {
|
||||||
|
t.Fatalf("run exit = %d, want 1", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output file: %v", err)
|
||||||
|
}
|
||||||
|
if len(data) == 0 || data[len(data)-1] != '\n' {
|
||||||
|
t.Fatalf("output file should end with newline, got %q", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
Results []TaskResult `json:"results"`
|
||||||
|
Summary struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Success int `json:"success"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
} `json:"summary"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &payload); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal output json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Summary.Total != 1 || payload.Summary.Success != 0 || payload.Summary.Failed != 1 {
|
||||||
|
t.Fatalf("unexpected summary: %+v", payload.Summary)
|
||||||
|
}
|
||||||
|
if len(payload.Results) != 1 {
|
||||||
|
t.Fatalf("results length = %d, want 1", len(payload.Results))
|
||||||
|
}
|
||||||
|
if !strings.Contains(payload.Results[0].Error, "no output message:") {
|
||||||
|
t.Fatalf("result error = %q, want no output message", payload.Results[0].Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunParallelWithOutputFile(t *testing.T) {
|
||||||
|
defer resetTestHooks()
|
||||||
|
cleanupLogsFn = func() (CleanupStats, error) { return CleanupStats{}, nil }
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
outputPath := filepath.Join(tempDir, "parallel-output.json")
|
||||||
|
|
||||||
|
oldArgs := os.Args
|
||||||
|
t.Cleanup(func() { os.Args = oldArgs })
|
||||||
|
os.Args = []string{"codeagent-wrapper", "--parallel", "--output", outputPath}
|
||||||
|
|
||||||
|
stdinReader = strings.NewReader(`---TASK---
|
||||||
|
id: T1
|
||||||
|
---CONTENT---
|
||||||
|
noop`)
|
||||||
|
t.Cleanup(func() { stdinReader = os.Stdin })
|
||||||
|
|
||||||
|
origRunCodexTaskFn := runCodexTaskFn
|
||||||
|
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||||
|
return TaskResult{TaskID: task.ID, ExitCode: 0, Message: "parallel output marker"}
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { runCodexTaskFn = origRunCodexTaskFn })
|
||||||
|
|
||||||
|
out := captureOutput(t, func() {
|
||||||
|
if code := run(); code != 0 {
|
||||||
|
t.Fatalf("run exit = %d, want 0", code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if !strings.Contains(out, "=== Execution Report ===") {
|
||||||
|
t.Fatalf("stdout should keep summary format, got %q", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read output file: %v", err)
|
||||||
|
}
|
||||||
|
if len(data) == 0 || data[len(data)-1] != '\n' {
|
||||||
|
t.Fatalf("output file should end with newline, got %q", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
Results []TaskResult `json:"results"`
|
||||||
|
Summary struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Success int `json:"success"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
} `json:"summary"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &payload); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal output json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Summary.Total != 1 || payload.Summary.Success != 1 || payload.Summary.Failed != 0 {
|
||||||
|
t.Fatalf("unexpected summary: %+v", payload.Summary)
|
||||||
|
}
|
||||||
|
if len(payload.Results) != 1 {
|
||||||
|
t.Fatalf("results length = %d, want 1", len(payload.Results))
|
||||||
|
}
|
||||||
|
if payload.Results[0].TaskID != "T1" {
|
||||||
|
t.Fatalf("result task_id = %q, want %q", payload.Results[0].TaskID, "T1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParallelInvalidBackend(t *testing.T) {
|
func TestParallelInvalidBackend(t *testing.T) {
|
||||||
defer resetTestHooks()
|
defer resetTestHooks()
|
||||||
cleanupLogsFn = func() (CleanupStats, error) { return CleanupStats{}, nil }
|
cleanupLogsFn = func() (CleanupStats, error) { return CleanupStats{}, nil }
|
||||||
@@ -4330,9 +4635,9 @@ func TestRun_ExplicitStdinReadError(t *testing.T) {
|
|||||||
if !strings.Contains(logOutput, "Failed to read stdin: broken stdin") {
|
if !strings.Contains(logOutput, "Failed to read stdin: broken stdin") {
|
||||||
t.Fatalf("log missing read error entry, got %q", logOutput)
|
t.Fatalf("log missing read error entry, got %q", logOutput)
|
||||||
}
|
}
|
||||||
// Log file is always removed after completion (new behavior)
|
// Log file should remain for inspection; cleanup is handled via `codeagent-wrapper cleanup`.
|
||||||
if _, err := os.Stat(logPath); !os.IsNotExist(err) {
|
if _, err := os.Stat(logPath); err != nil {
|
||||||
t.Fatalf("log file should be removed after completion")
|
t.Fatalf("expected log file to exist after completion: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4348,6 +4653,51 @@ func TestRun_CommandFails(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRun_NonZeroExitPrintsParsedMessage(t *testing.T) {
|
||||||
|
defer resetTestHooks()
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
var scriptPath string
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
scriptPath = filepath.Join(tempDir, "codex.bat")
|
||||||
|
script := "@echo off\r\n" +
|
||||||
|
"echo {\"type\":\"thread.started\",\"thread_id\":\"tid\"}\r\n" +
|
||||||
|
"echo {\"type\":\"item.completed\",\"item\":{\"type\":\"agent_message\",\"text\":\"parsed-error\"}}\r\n" +
|
||||||
|
"exit /b 1\r\n"
|
||||||
|
if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to write script: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scriptPath = filepath.Join(tempDir, "codex.sh")
|
||||||
|
script := `#!/bin/sh
|
||||||
|
printf '%s\n' '{"type":"thread.started","thread_id":"tid"}'
|
||||||
|
printf '%s\n' '{"type":"item.completed","item":{"type":"agent_message","text":"parsed-error"}}'
|
||||||
|
sleep 0.05
|
||||||
|
exit 1
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to write script: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restore := withBackend(scriptPath, func(cfg *Config, targetArg string) []string { return []string{} })
|
||||||
|
defer restore()
|
||||||
|
|
||||||
|
os.Args = []string{"codeagent-wrapper", "task"}
|
||||||
|
stdinReader = strings.NewReader("")
|
||||||
|
isTerminalFn = func() bool { return true }
|
||||||
|
|
||||||
|
var exitCode int
|
||||||
|
output := captureOutput(t, func() { exitCode = run() })
|
||||||
|
if exitCode != 1 {
|
||||||
|
t.Fatalf("exit=%d, want 1", exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(output, "parsed-error") {
|
||||||
|
t.Fatalf("stdout=%q, want parsed backend message", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRun_InvalidBackend(t *testing.T) {
|
func TestRun_InvalidBackend(t *testing.T) {
|
||||||
defer resetTestHooks()
|
defer resetTestHooks()
|
||||||
os.Args = []string{"codeagent-wrapper", "--backend", "unknown", "task"}
|
os.Args = []string{"codeagent-wrapper", "--backend", "unknown", "task"}
|
||||||
@@ -4427,9 +4777,9 @@ func TestRun_PipedTaskReadError(t *testing.T) {
|
|||||||
if !strings.Contains(logOutput, "Failed to read piped stdin: read stdin: pipe failure") {
|
if !strings.Contains(logOutput, "Failed to read piped stdin: read stdin: pipe failure") {
|
||||||
t.Fatalf("log missing piped read error, got %q", logOutput)
|
t.Fatalf("log missing piped read error, got %q", logOutput)
|
||||||
}
|
}
|
||||||
// Log file is always removed after completion (new behavior)
|
// Log file should remain for inspection; cleanup is handled via `codeagent-wrapper cleanup`.
|
||||||
if _, err := os.Stat(logPath); !os.IsNotExist(err) {
|
if _, err := os.Stat(logPath); err != nil {
|
||||||
t.Fatalf("log file should be removed after completion")
|
t.Fatalf("expected log file to exist after completion: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4483,12 +4833,12 @@ func TestRun_LoggerLifecycle(t *testing.T) {
|
|||||||
if !fileExisted {
|
if !fileExisted {
|
||||||
t.Fatalf("log file was not present during run")
|
t.Fatalf("log file was not present during run")
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(logPath); !os.IsNotExist(err) {
|
if _, err := os.Stat(logPath); err != nil {
|
||||||
t.Fatalf("log file should be removed on success, but it exists")
|
t.Fatalf("expected log file to exist on success: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRun_LoggerRemovedOnSignal(t *testing.T) {
|
func TestRun_LoggerKeptOnSignal(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
t.Skip("signal-based test is not supported on Windows")
|
t.Skip("signal-based test is not supported on Windows")
|
||||||
}
|
}
|
||||||
@@ -4502,7 +4852,8 @@ func TestRun_LoggerRemovedOnSignal(t *testing.T) {
|
|||||||
defer signal.Reset(syscall.SIGINT, syscall.SIGTERM)
|
defer signal.Reset(syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
// Set shorter delays for faster test
|
// Set shorter delays for faster test
|
||||||
_ = executor.SetForceKillDelay(1)
|
restoreForceKillDelay := executor.SetForceKillDelay(1)
|
||||||
|
defer restoreForceKillDelay()
|
||||||
|
|
||||||
tempDir := setTempDirEnv(t, t.TempDir())
|
tempDir := setTempDirEnv(t, t.TempDir())
|
||||||
logPath := filepath.Join(tempDir, fmt.Sprintf("codeagent-wrapper-%d.log", os.Getpid()))
|
logPath := filepath.Join(tempDir, fmt.Sprintf("codeagent-wrapper-%d.log", os.Getpid()))
|
||||||
@@ -4525,13 +4876,19 @@ printf '%s\n' '{"type":"item.completed","item":{"type":"agent_message","text":"l
|
|||||||
exitCh := make(chan int, 1)
|
exitCh := make(chan int, 1)
|
||||||
go func() { exitCh <- run() }()
|
go func() { exitCh <- run() }()
|
||||||
|
|
||||||
deadline := time.Now().Add(1 * time.Second)
|
ready := false
|
||||||
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
for time.Now().Before(deadline) {
|
for time.Now().Before(deadline) {
|
||||||
if _, err := os.Stat(logPath); err == nil {
|
data, err := os.ReadFile(logPath)
|
||||||
|
if err == nil && strings.Contains(string(data), "Starting ") {
|
||||||
|
ready = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
if !ready {
|
||||||
|
t.Fatalf("logger did not become ready before deadline")
|
||||||
|
}
|
||||||
|
|
||||||
if proc, err := os.FindProcess(os.Getpid()); err == nil && proc != nil {
|
if proc, err := os.FindProcess(os.Getpid()); err == nil && proc != nil {
|
||||||
_ = proc.Signal(syscall.SIGINT)
|
_ = proc.Signal(syscall.SIGINT)
|
||||||
@@ -4547,9 +4904,9 @@ printf '%s\n' '{"type":"item.completed","item":{"type":"agent_message","text":"l
|
|||||||
if exitCode != 130 {
|
if exitCode != 130 {
|
||||||
t.Fatalf("exit code = %d, want 130", exitCode)
|
t.Fatalf("exit code = %d, want 130", exitCode)
|
||||||
}
|
}
|
||||||
// Log file is always removed after completion (new behavior)
|
// Log file should remain for inspection; cleanup is handled via `codeagent-wrapper cleanup`.
|
||||||
if _, err := os.Stat(logPath); !os.IsNotExist(err) {
|
if _, err := os.Stat(logPath); err != nil {
|
||||||
t.Fatalf("log file should be removed after completion")
|
t.Fatalf("expected log file to exist after completion: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4810,6 +5167,34 @@ func TestParallelLogPathInSerialMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRun_KeptLogFileOnSuccess(t *testing.T) {
|
||||||
|
defer resetTestHooks()
|
||||||
|
|
||||||
|
tempDir := setTempDirEnv(t, t.TempDir())
|
||||||
|
|
||||||
|
os.Args = []string{"codeagent-wrapper", "do-stuff"}
|
||||||
|
stdinReader = strings.NewReader("")
|
||||||
|
isTerminalFn = func() bool { return true }
|
||||||
|
codexCommand = createFakeCodexScript(t, "cli-session", "ok")
|
||||||
|
buildCodexArgsFn = func(cfg *Config, targetArg string) []string { return []string{} }
|
||||||
|
cleanupLogsFn = nil
|
||||||
|
|
||||||
|
var exitCode int
|
||||||
|
_ = captureStderr(t, func() {
|
||||||
|
_ = captureOutput(t, func() {
|
||||||
|
exitCode = run()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if exitCode != 0 {
|
||||||
|
t.Fatalf("run() exit = %d, want 0", exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedLog := filepath.Join(tempDir, fmt.Sprintf("codeagent-wrapper-%d.log", os.Getpid()))
|
||||||
|
if _, err := os.Stat(expectedLog); err != nil {
|
||||||
|
t.Fatalf("expected log file to exist: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRun_CLI_Success(t *testing.T) {
|
func TestRun_CLI_Success(t *testing.T) {
|
||||||
defer resetTestHooks()
|
defer resetTestHooks()
|
||||||
os.Args = []string{"codeagent-wrapper", "do-things"}
|
os.Args = []string{"codeagent-wrapper", "do-things"}
|
||||||
|
|||||||
65
codeagent-wrapper/internal/app/output_file.go
Normal file
65
codeagent-wrapper/internal/app/output_file.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package wrapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type outputSummary struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Success int `json:"success"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type outputPayload struct {
|
||||||
|
Results []TaskResult `json:"results"`
|
||||||
|
Summary outputSummary `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeStructuredOutput(path string, results []TaskResult) error {
|
||||||
|
path = strings.TrimSpace(path)
|
||||||
|
if path == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanPath := filepath.Clean(path)
|
||||||
|
dir := filepath.Dir(cleanPath)
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create output directory for %q: %w", cleanPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(cleanPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create output file %q: %w", cleanPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeErr := json.NewEncoder(f).Encode(outputPayload{
|
||||||
|
Results: results,
|
||||||
|
Summary: summarizeResults(results),
|
||||||
|
})
|
||||||
|
closeErr := f.Close()
|
||||||
|
|
||||||
|
if encodeErr != nil {
|
||||||
|
return fmt.Errorf("failed to write structured output to %q: %w", cleanPath, encodeErr)
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
return fmt.Errorf("failed to close output file %q: %w", cleanPath, closeErr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func summarizeResults(results []TaskResult) outputSummary {
|
||||||
|
summary := outputSummary{Total: len(results)}
|
||||||
|
for _, res := range results {
|
||||||
|
if res.ExitCode == 0 && res.Error == "" {
|
||||||
|
summary.Success++
|
||||||
|
} else {
|
||||||
|
summary.Failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return summary
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ type Config struct {
|
|||||||
Task string
|
Task string
|
||||||
SessionID string
|
SessionID string
|
||||||
WorkDir string
|
WorkDir string
|
||||||
|
OutputPath string
|
||||||
Model string
|
Model string
|
||||||
ReasoningEffort string
|
ReasoningEffort string
|
||||||
ExplicitStdin bool
|
ExplicitStdin bool
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ func (f *fakeCmd) SetEnv(env map[string]string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (f *fakeCmd) Process() processHandle { return nil }
|
func (f *fakeCmd) Process() processHandle { return nil }
|
||||||
|
func (f *fakeCmd) UnsetEnv(keys ...string) {
|
||||||
|
for _, k := range keys {
|
||||||
|
delete(f.env, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnvInjection_LogsToStderrAndMasksKey(t *testing.T) {
|
func TestEnvInjection_LogsToStderrAndMasksKey(t *testing.T) {
|
||||||
// Arrange ~/.codeagent/models.json via HOME override.
|
// Arrange ~/.codeagent/models.json via HOME override.
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ type commandRunner interface {
|
|||||||
SetStderr(io.Writer)
|
SetStderr(io.Writer)
|
||||||
SetDir(string)
|
SetDir(string)
|
||||||
SetEnv(env map[string]string)
|
SetEnv(env map[string]string)
|
||||||
|
UnsetEnv(keys ...string)
|
||||||
Process() processHandle
|
Process() processHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +222,33 @@ func (r *realCmd) SetEnv(env map[string]string) {
|
|||||||
r.cmd.Env = out
|
r.cmd.Env = out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *realCmd) UnsetEnv(keys ...string) {
|
||||||
|
if r == nil || r.cmd == nil || len(keys) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// If cmd.Env is nil, Go inherits all parent env vars.
|
||||||
|
// Populate explicitly so we can selectively remove keys.
|
||||||
|
if r.cmd.Env == nil {
|
||||||
|
r.cmd.Env = os.Environ()
|
||||||
|
}
|
||||||
|
drop := make(map[string]struct{}, len(keys))
|
||||||
|
for _, k := range keys {
|
||||||
|
drop[k] = struct{}{}
|
||||||
|
}
|
||||||
|
filtered := make([]string, 0, len(r.cmd.Env))
|
||||||
|
for _, kv := range r.cmd.Env {
|
||||||
|
idx := strings.IndexByte(kv, '=')
|
||||||
|
name := kv
|
||||||
|
if idx >= 0 {
|
||||||
|
name = kv[:idx]
|
||||||
|
}
|
||||||
|
if _, ok := drop[name]; !ok {
|
||||||
|
filtered = append(filtered, kv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.cmd.Env = filtered
|
||||||
|
}
|
||||||
|
|
||||||
func (r *realCmd) Process() processHandle {
|
func (r *realCmd) Process() processHandle {
|
||||||
if r == nil || r.cmd == nil || r.cmd.Process == nil {
|
if r == nil || r.cmd == nil || r.cmd.Process == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -1126,6 +1154,13 @@ func RunCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
|||||||
|
|
||||||
injectTempEnv(cmd)
|
injectTempEnv(cmd)
|
||||||
|
|
||||||
|
// Claude Code sets CLAUDECODE=1 in its child processes. If we don't
|
||||||
|
// remove it, the spawned `claude -p` detects the variable and refuses
|
||||||
|
// to start ("cannot be launched inside another Claude Code session").
|
||||||
|
if commandName == "claude" {
|
||||||
|
cmd.UnsetEnv("CLAUDECODE")
|
||||||
|
}
|
||||||
|
|
||||||
// For backends that don't support -C flag (claude, gemini), set working directory via cmd.Dir
|
// For backends that don't support -C flag (claude, gemini), set working directory via cmd.Dir
|
||||||
// Codex passes workdir via -C flag, so we skip setting Dir for it to avoid conflicts
|
// Codex passes workdir via -C flag, so we skip setting Dir for it to avoid conflicts
|
||||||
if cfg.Mode != "resume" && commandName != "codex" && cfg.WorkDir != "" {
|
if cfg.Mode != "resume" && commandName != "codex" && cfg.WorkDir != "" {
|
||||||
@@ -1400,6 +1435,15 @@ waitLoop:
|
|||||||
logErrorFn(fmt.Sprintf("%s exited with status %d", commandName, code))
|
logErrorFn(fmt.Sprintf("%s exited with status %d", commandName, code))
|
||||||
result.ExitCode = code
|
result.ExitCode = code
|
||||||
result.Error = attachStderr(fmt.Sprintf("%s exited with status %d", commandName, code))
|
result.Error = attachStderr(fmt.Sprintf("%s exited with status %d", commandName, code))
|
||||||
|
// Preserve parsed output when the backend exits non-zero (e.g. API error with stream-json output).
|
||||||
|
result.Message = parsed.message
|
||||||
|
result.SessionID = parsed.threadID
|
||||||
|
if stdoutLogger != nil {
|
||||||
|
stdoutLogger.Flush()
|
||||||
|
}
|
||||||
|
if stderrLogger != nil {
|
||||||
|
stderrLogger.Flush()
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
logErrorFn(commandName + " error: " + waitErr.Error())
|
logErrorFn(commandName + " error: " + waitErr.Error())
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ var geminiNoisePatterns = []string{
|
|||||||
|
|
||||||
// codexNoisePatterns contains stderr patterns to filter for codex backend
|
// codexNoisePatterns contains stderr patterns to filter for codex backend
|
||||||
var codexNoisePatterns = []string{
|
var codexNoisePatterns = []string{
|
||||||
"ERROR codex_core::codex: needs_follow_up:",
|
"ERROR codex_core::",
|
||||||
"ERROR codex_core::skills::loader:",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// filteringWriter wraps an io.Writer and filters out lines matching patterns
|
// filteringWriter wraps an io.Writer and filters out lines matching patterns
|
||||||
|
|||||||
@@ -71,3 +71,35 @@ func TestFilteringWriterPartialLines(t *testing.T) {
|
|||||||
t.Errorf("got %q, want %q", got, "Hello World\n")
|
t.Errorf("got %q, want %q", got, "Hello World\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilteringWriterCodexNoise(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "filter all codex_core errors",
|
||||||
|
input: "ERROR codex_core::rollout::list: state db missing rollout path for thread 123\nERROR codex_core::skills::loader: missing skill\nVisible output\n",
|
||||||
|
want: "Visible output\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "keep non codex_core errors",
|
||||||
|
input: "ERROR another_module::state: real failure\nERROR codex_core::codex: needs_follow_up: true\nDone\n",
|
||||||
|
want: "ERROR another_module::state: real failure\nDone\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fw := newFilteringWriter(&buf, codexNoisePatterns)
|
||||||
|
_, _ = fw.Write([]byte(tt.input))
|
||||||
|
fw.Flush()
|
||||||
|
|
||||||
|
if got := buf.String(); got != tt.want {
|
||||||
|
t.Errorf("got %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
56
config.json
56
config.json
@@ -39,6 +39,36 @@
|
|||||||
"omo": {
|
"omo": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"description": "OmO multi-agent orchestration with Sisyphus coordinator",
|
"description": "OmO multi-agent orchestration with Sisyphus coordinator",
|
||||||
|
"agents": {
|
||||||
|
"oracle": {
|
||||||
|
"backend": "claude",
|
||||||
|
"model": "claude-opus-4-5-20251101",
|
||||||
|
"yolo": true
|
||||||
|
},
|
||||||
|
"librarian": {
|
||||||
|
"backend": "claude",
|
||||||
|
"model": "claude-sonnet-4-5-20250929",
|
||||||
|
"yolo": true
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
"backend": "opencode",
|
||||||
|
"model": "opencode/grok-code"
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"backend": "codex",
|
||||||
|
"model": "gpt-5.2",
|
||||||
|
"reasoning": "xhigh",
|
||||||
|
"yolo": true
|
||||||
|
},
|
||||||
|
"frontend-ui-ux-engineer": {
|
||||||
|
"backend": "gemini",
|
||||||
|
"model": "gemini-3-pro-preview"
|
||||||
|
},
|
||||||
|
"document-writer": {
|
||||||
|
"backend": "gemini",
|
||||||
|
"model": "gemini-3-flash-preview"
|
||||||
|
}
|
||||||
|
},
|
||||||
"operations": [
|
"operations": [
|
||||||
{
|
{
|
||||||
"type": "copy_file",
|
"type": "copy_file",
|
||||||
@@ -98,7 +128,27 @@
|
|||||||
},
|
},
|
||||||
"do": {
|
"do": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"description": "7-phase feature development workflow with codeagent orchestration",
|
"description": "5-phase feature development workflow with codeagent orchestration",
|
||||||
|
"agents": {
|
||||||
|
"develop": {
|
||||||
|
"backend": "codex",
|
||||||
|
"model": "gpt-4.1",
|
||||||
|
"reasoning": "high",
|
||||||
|
"yolo": true
|
||||||
|
},
|
||||||
|
"code-explorer": {
|
||||||
|
"backend": "opencode",
|
||||||
|
"model": ""
|
||||||
|
},
|
||||||
|
"code-architect": {
|
||||||
|
"backend": "claude",
|
||||||
|
"model": ""
|
||||||
|
},
|
||||||
|
"code-reviewer": {
|
||||||
|
"backend": "claude",
|
||||||
|
"model": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"operations": [
|
"operations": [
|
||||||
{
|
{
|
||||||
"type": "copy_dir",
|
"type": "copy_dir",
|
||||||
@@ -148,7 +198,7 @@
|
|||||||
},
|
},
|
||||||
"claudekit": {
|
"claudekit": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"description": "ClaudeKit workflow: skills/do + global hooks (pre-bash, inject-spec, log-prompt, on-stop)",
|
"description": "ClaudeKit workflow: skills/do + global hooks (pre-bash, inject-spec, log-prompt)",
|
||||||
"operations": [
|
"operations": [
|
||||||
{
|
{
|
||||||
"type": "copy_dir",
|
"type": "copy_dir",
|
||||||
@@ -160,7 +210,7 @@
|
|||||||
"type": "copy_dir",
|
"type": "copy_dir",
|
||||||
"source": "hooks",
|
"source": "hooks",
|
||||||
"target": "hooks",
|
"target": "hooks",
|
||||||
"description": "Install global hooks (pre-bash, inject-spec, log-prompt, on-stop)"
|
"description": "Install global hooks (pre-bash, inject-spec, log-prompt)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://github.com/cexll/myclaude/config.schema.json",
|
"$id": "https://github.com/stellarlinkco/myclaude/config.schema.json",
|
||||||
"title": "Modular Installation Config",
|
"title": "Modular Installation Config",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
set "EXIT_CODE=0"
|
set "EXIT_CODE=0"
|
||||||
set "REPO=cexll/myclaude"
|
set "REPO=stellarlinkco/myclaude"
|
||||||
set "VERSION=latest"
|
set "VERSION=latest"
|
||||||
set "OS=windows"
|
set "OS=windows"
|
||||||
|
|
||||||
|
|||||||
254
install.py
254
install.py
@@ -24,6 +24,7 @@ except ImportError: # pragma: no cover
|
|||||||
|
|
||||||
DEFAULT_INSTALL_DIR = "~/.claude"
|
DEFAULT_INSTALL_DIR = "~/.claude"
|
||||||
SETTINGS_FILE = "settings.json"
|
SETTINGS_FILE = "settings.json"
|
||||||
|
WRAPPER_REQUIRED_MODULES = {"do", "omo"}
|
||||||
|
|
||||||
|
|
||||||
def _ensure_list(ctx: Dict[str, Any], key: str) -> List[Any]:
|
def _ensure_list(ctx: Dict[str, Any], key: str) -> List[Any]:
|
||||||
@@ -244,6 +245,112 @@ def unmerge_hooks_from_settings(module_name: str, ctx: Dict[str, Any]) -> None:
|
|||||||
write_log({"level": "INFO", "message": f"Removed hooks for module: {module_name}"}, ctx)
|
write_log({"level": "INFO", "message": f"Removed hooks for module: {module_name}"}, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def merge_agents_to_models(module_name: str, agents: Dict[str, Any], ctx: Dict[str, Any]) -> None:
|
||||||
|
"""Merge module agent configs into ~/.codeagent/models.json."""
|
||||||
|
models_path = Path.home() / ".codeagent" / "models.json"
|
||||||
|
models_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if models_path.exists():
|
||||||
|
with models_path.open("r", encoding="utf-8") as fh:
|
||||||
|
models = json.load(fh)
|
||||||
|
else:
|
||||||
|
template = ctx["config_dir"] / "templates" / "models.json.example"
|
||||||
|
if template.exists():
|
||||||
|
with template.open("r", encoding="utf-8") as fh:
|
||||||
|
models = json.load(fh)
|
||||||
|
# Clear template agents so modules populate with __module__ tags
|
||||||
|
models["agents"] = {}
|
||||||
|
else:
|
||||||
|
models = {
|
||||||
|
"default_backend": "codex",
|
||||||
|
"default_model": "gpt-4.1",
|
||||||
|
"backends": {},
|
||||||
|
"agents": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
models.setdefault("agents", {})
|
||||||
|
for agent_name, agent_cfg in agents.items():
|
||||||
|
entry = dict(agent_cfg)
|
||||||
|
entry["__module__"] = module_name
|
||||||
|
|
||||||
|
existing = models["agents"].get(agent_name, {})
|
||||||
|
if not existing or existing.get("__module__"):
|
||||||
|
models["agents"][agent_name] = entry
|
||||||
|
|
||||||
|
with models_path.open("w", encoding="utf-8") as fh:
|
||||||
|
json.dump(models, fh, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
write_log(
|
||||||
|
{
|
||||||
|
"level": "INFO",
|
||||||
|
"message": (
|
||||||
|
f"Merged {len(agents)} agent(s) from {module_name} "
|
||||||
|
"into models.json"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def unmerge_agents_from_models(module_name: str, ctx: Dict[str, Any]) -> None:
|
||||||
|
"""Remove module's agent configs from ~/.codeagent/models.json.
|
||||||
|
|
||||||
|
If another installed module also declares a removed agent, restore that
|
||||||
|
module's version so shared agents (e.g. 'develop') are not lost.
|
||||||
|
"""
|
||||||
|
models_path = Path.home() / ".codeagent" / "models.json"
|
||||||
|
if not models_path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
with models_path.open("r", encoding="utf-8") as fh:
|
||||||
|
models = json.load(fh)
|
||||||
|
|
||||||
|
agents = models.get("agents", {})
|
||||||
|
to_remove = [
|
||||||
|
name
|
||||||
|
for name, cfg in agents.items()
|
||||||
|
if isinstance(cfg, dict) and cfg.get("__module__") == module_name
|
||||||
|
]
|
||||||
|
|
||||||
|
if not to_remove:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load config to find other modules that declare the same agents
|
||||||
|
config_path = ctx["config_dir"] / "config.json"
|
||||||
|
config = _load_json(config_path) if config_path.exists() else {}
|
||||||
|
installed = load_installed_status(ctx).get("modules", {})
|
||||||
|
|
||||||
|
for name in to_remove:
|
||||||
|
del agents[name]
|
||||||
|
# Check if another installed module also declares this agent
|
||||||
|
for other_mod, other_status in installed.items():
|
||||||
|
if other_mod == module_name:
|
||||||
|
continue
|
||||||
|
if other_status.get("status") != "success":
|
||||||
|
continue
|
||||||
|
other_cfg = config.get("modules", {}).get(other_mod, {})
|
||||||
|
other_agents = other_cfg.get("agents", {})
|
||||||
|
if name in other_agents:
|
||||||
|
restored = dict(other_agents[name])
|
||||||
|
restored["__module__"] = other_mod
|
||||||
|
agents[name] = restored
|
||||||
|
break
|
||||||
|
|
||||||
|
with models_path.open("w", encoding="utf-8") as fh:
|
||||||
|
json.dump(models, fh, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
write_log(
|
||||||
|
{
|
||||||
|
"level": "INFO",
|
||||||
|
"message": (
|
||||||
|
f"Removed {len(to_remove)} agent(s) from {module_name} "
|
||||||
|
"in models.json"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _hooks_equal(hook1: Dict[str, Any], hook2: Dict[str, Any]) -> bool:
|
def _hooks_equal(hook1: Dict[str, Any], hook2: Dict[str, Any]) -> bool:
|
||||||
"""Compare two hooks ignoring the __module__ marker."""
|
"""Compare two hooks ignoring the __module__ marker."""
|
||||||
h1 = {k: v for k, v in hook1.items() if k != "__module__"}
|
h1 = {k: v for k, v in hook1.items() if k != "__module__"}
|
||||||
@@ -545,6 +652,14 @@ def uninstall_module(name: str, cfg: Dict[str, Any], ctx: Dict[str, Any]) -> Dic
|
|||||||
target.unlink()
|
target.unlink()
|
||||||
removed_paths.append(str(target))
|
removed_paths.append(str(target))
|
||||||
write_log({"level": "INFO", "message": f"Removed: {target}"}, ctx)
|
write_log({"level": "INFO", "message": f"Removed: {target}"}, ctx)
|
||||||
|
# Clean up empty parent directories up to install_dir
|
||||||
|
parent = target.parent
|
||||||
|
while parent != install_dir and parent.exists():
|
||||||
|
try:
|
||||||
|
parent.rmdir()
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
parent = parent.parent
|
||||||
elif op_type == "merge_dir":
|
elif op_type == "merge_dir":
|
||||||
if not merge_dir_files:
|
if not merge_dir_files:
|
||||||
write_log(
|
write_log(
|
||||||
@@ -604,6 +719,13 @@ def uninstall_module(name: str, cfg: Dict[str, Any], ctx: Dict[str, Any]) -> Dic
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
write_log({"level": "WARNING", "message": f"Failed to remove hooks for {name}: {exc}"}, ctx)
|
write_log({"level": "WARNING", "message": f"Failed to remove hooks for {name}: {exc}"}, ctx)
|
||||||
|
|
||||||
|
# Remove module agents from ~/.codeagent/models.json
|
||||||
|
try:
|
||||||
|
unmerge_agents_from_models(name, ctx)
|
||||||
|
result["agents_removed"] = True
|
||||||
|
except Exception as exc:
|
||||||
|
write_log({"level": "WARNING", "message": f"Failed to remove agents for {name}: {exc}"}, ctx)
|
||||||
|
|
||||||
result["removed_paths"] = removed_paths
|
result["removed_paths"] = removed_paths
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -626,7 +748,9 @@ def update_status_after_uninstall(uninstalled_modules: List[str], ctx: Dict[str,
|
|||||||
|
|
||||||
|
|
||||||
def interactive_manage(config: Dict[str, Any], ctx: Dict[str, Any]) -> int:
|
def interactive_manage(config: Dict[str, Any], ctx: Dict[str, Any]) -> int:
|
||||||
"""Interactive module management menu."""
|
"""Interactive module management menu. Returns 0 on success, 1 on error.
|
||||||
|
Sets ctx['_did_install'] = True if any module was installed."""
|
||||||
|
ctx.setdefault("_did_install", False)
|
||||||
while True:
|
while True:
|
||||||
installed_status = get_installed_modules(config, ctx)
|
installed_status = get_installed_modules(config, ctx)
|
||||||
modules = config.get("modules", {})
|
modules = config.get("modules", {})
|
||||||
@@ -695,6 +819,7 @@ def interactive_manage(config: Dict[str, Any], ctx: Dict[str, Any]) -> int:
|
|||||||
for r in results:
|
for r in results:
|
||||||
if r.get("status") == "success":
|
if r.get("status") == "success":
|
||||||
current_status.setdefault("modules", {})[r["module"]] = r
|
current_status.setdefault("modules", {})[r["module"]] = r
|
||||||
|
ctx["_did_install"] = True
|
||||||
current_status["updated_at"] = datetime.now().isoformat()
|
current_status["updated_at"] = datetime.now().isoformat()
|
||||||
with Path(ctx["status_file"]).open("w", encoding="utf-8") as fh:
|
with Path(ctx["status_file"]).open("w", encoding="utf-8") as fh:
|
||||||
json.dump(current_status, fh, indent=2, ensure_ascii=False)
|
json.dump(current_status, fh, indent=2, ensure_ascii=False)
|
||||||
@@ -774,6 +899,24 @@ def execute_module(name: str, cfg: Dict[str, Any], ctx: Dict[str, Any]) -> Dict[
|
|||||||
"installed_at": datetime.now().isoformat(),
|
"installed_at": datetime.now().isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if name in WRAPPER_REQUIRED_MODULES:
|
||||||
|
try:
|
||||||
|
ensure_wrapper_installed(ctx)
|
||||||
|
result["operations"].append({"type": "ensure_wrapper", "status": "success"})
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
result["status"] = "failed"
|
||||||
|
result["operations"].append(
|
||||||
|
{"type": "ensure_wrapper", "status": "failed", "error": str(exc)}
|
||||||
|
)
|
||||||
|
write_log(
|
||||||
|
{
|
||||||
|
"level": "ERROR",
|
||||||
|
"message": f"Module {name} failed on ensure_wrapper: {exc}",
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
for op in cfg.get("operations", []):
|
for op in cfg.get("operations", []):
|
||||||
op_type = op.get("type")
|
op_type = op.get("type")
|
||||||
try:
|
try:
|
||||||
@@ -819,6 +962,17 @@ def execute_module(name: str, cfg: Dict[str, Any], ctx: Dict[str, Any]) -> Dict[
|
|||||||
write_log({"level": "WARNING", "message": f"Failed to merge hooks for {name}: {exc}"}, ctx)
|
write_log({"level": "WARNING", "message": f"Failed to merge hooks for {name}: {exc}"}, ctx)
|
||||||
result["operations"].append({"type": "merge_hooks", "status": "failed", "error": str(exc)})
|
result["operations"].append({"type": "merge_hooks", "status": "failed", "error": str(exc)})
|
||||||
|
|
||||||
|
# Handle agents: merge module agent configs into ~/.codeagent/models.json
|
||||||
|
module_agents = cfg.get("agents", {})
|
||||||
|
if module_agents:
|
||||||
|
try:
|
||||||
|
merge_agents_to_models(name, module_agents, ctx)
|
||||||
|
result["operations"].append({"type": "merge_agents", "status": "success"})
|
||||||
|
result["has_agents"] = True
|
||||||
|
except Exception as exc:
|
||||||
|
write_log({"level": "WARNING", "message": f"Failed to merge agents for {name}: {exc}"}, ctx)
|
||||||
|
result["operations"].append({"type": "merge_agents", "status": "failed", "error": str(exc)})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -946,8 +1100,13 @@ def op_run_command(op: Dict[str, Any], ctx: Dict[str, Any]) -> None:
|
|||||||
for key, value in op.get("env", {}).items():
|
for key, value in op.get("env", {}).items():
|
||||||
env[key] = value.replace("${install_dir}", str(ctx["install_dir"]))
|
env[key] = value.replace("${install_dir}", str(ctx["install_dir"]))
|
||||||
|
|
||||||
command = op.get("command", "")
|
raw_command = str(op.get("command", "")).strip()
|
||||||
if sys.platform == "win32" and command.strip() == "bash install.sh":
|
if raw_command == "bash install.sh" and ctx.get("_wrapper_installed"):
|
||||||
|
write_log({"level": "INFO", "message": "Skip wrapper install; already installed in this run"}, ctx)
|
||||||
|
return
|
||||||
|
|
||||||
|
command = raw_command
|
||||||
|
if sys.platform == "win32" and raw_command == "bash install.sh":
|
||||||
command = "cmd /c install.bat"
|
command = "cmd /c install.bat"
|
||||||
|
|
||||||
# Stream output in real-time while capturing for logging
|
# Stream output in real-time while capturing for logging
|
||||||
@@ -1021,6 +1180,22 @@ def op_run_command(op: Dict[str, Any], ctx: Dict[str, Any]) -> None:
|
|||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise RuntimeError(f"Command failed with code {process.returncode}: {command}")
|
raise RuntimeError(f"Command failed with code {process.returncode}: {command}")
|
||||||
|
|
||||||
|
if raw_command == "bash install.sh":
|
||||||
|
ctx["_wrapper_installed"] = True
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_wrapper_installed(ctx: Dict[str, Any]) -> None:
|
||||||
|
if ctx.get("_wrapper_installed"):
|
||||||
|
return
|
||||||
|
op_run_command(
|
||||||
|
{
|
||||||
|
"type": "run_command",
|
||||||
|
"command": "bash install.sh",
|
||||||
|
"env": {"INSTALL_DIR": "${install_dir}"},
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def write_log(entry: Dict[str, Any], ctx: Dict[str, Any]) -> None:
|
def write_log(entry: Dict[str, Any], ctx: Dict[str, Any]) -> None:
|
||||||
log_path = Path(ctx["log_file"])
|
log_path = Path(ctx["log_file"])
|
||||||
@@ -1060,6 +1235,67 @@ def write_status(results: List[Dict[str, Any]], ctx: Dict[str, Any]) -> None:
|
|||||||
json.dump(status, fh, indent=2, ensure_ascii=False)
|
json.dump(status, fh, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def install_default_configs(ctx: Dict[str, Any]) -> None:
|
||||||
|
"""Copy default config files if they don't already exist. Best-effort: never raises."""
|
||||||
|
try:
|
||||||
|
install_dir = ctx["install_dir"]
|
||||||
|
config_dir = ctx["config_dir"]
|
||||||
|
|
||||||
|
# Copy memorys/CLAUDE.md -> {install_dir}/CLAUDE.md
|
||||||
|
claude_md_src = config_dir / "memorys" / "CLAUDE.md"
|
||||||
|
claude_md_dst = install_dir / "CLAUDE.md"
|
||||||
|
if not claude_md_dst.exists() and claude_md_src.exists():
|
||||||
|
shutil.copy2(claude_md_src, claude_md_dst)
|
||||||
|
print(f" Installed CLAUDE.md to {claude_md_dst}")
|
||||||
|
write_log({"level": "INFO", "message": f"Installed CLAUDE.md to {claude_md_dst}"}, ctx)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f" Warning: could not install default configs: {exc}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def print_post_install_info(ctx: Dict[str, Any]) -> None:
|
||||||
|
"""Print post-install verification and setup guidance."""
|
||||||
|
install_dir = ctx["install_dir"]
|
||||||
|
|
||||||
|
# Check codeagent-wrapper version
|
||||||
|
wrapper_bin = install_dir / "bin" / "codeagent-wrapper"
|
||||||
|
wrapper_version = None
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(wrapper_bin), "--version"],
|
||||||
|
capture_output=True, text=True, timeout=5,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
wrapper_version = result.stdout.strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check PATH
|
||||||
|
bin_dir = str(install_dir / "bin")
|
||||||
|
env_path = os.environ.get("PATH", "")
|
||||||
|
path_ok = any(
|
||||||
|
os.path.realpath(p) == os.path.realpath(bin_dir)
|
||||||
|
if os.path.exists(p) else p == bin_dir
|
||||||
|
for p in env_path.split(os.pathsep)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check backend CLIs
|
||||||
|
backends = ["codex", "claude", "gemini", "opencode"]
|
||||||
|
detected = {name: shutil.which(name) is not None for name in backends}
|
||||||
|
|
||||||
|
print("\nSetup Complete!")
|
||||||
|
v_mark = "✓" if wrapper_version else "✗"
|
||||||
|
print(f" codeagent-wrapper: {wrapper_version or '(not found)'} {v_mark}")
|
||||||
|
p_mark = "✓" if path_ok else "✗ (not in PATH)"
|
||||||
|
print(f" PATH: {bin_dir} {p_mark}")
|
||||||
|
print("\nBackend CLIs detected:")
|
||||||
|
cli_parts = [f"{b} {'✓' if detected[b] else '✗'}" for b in backends]
|
||||||
|
print(" " + " | ".join(cli_parts))
|
||||||
|
print("\nNext steps:")
|
||||||
|
print(" 1. Configure API keys in ~/.codeagent/models.json")
|
||||||
|
print(' 2. Try: /do "your first task"')
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
def prepare_status_backup(ctx: Dict[str, Any]) -> None:
|
def prepare_status_backup(ctx: Dict[str, Any]) -> None:
|
||||||
status_path = Path(ctx["status_file"])
|
status_path = Path(ctx["status_file"])
|
||||||
if status_path.exists():
|
if status_path.exists():
|
||||||
@@ -1208,6 +1444,8 @@ def main(argv: Optional[Iterable[str]] = None) -> int:
|
|||||||
failed = len(results) - success
|
failed = len(results) - success
|
||||||
if failed == 0:
|
if failed == 0:
|
||||||
print(f"\n✓ Update complete: {success} module(s) updated")
|
print(f"\n✓ Update complete: {success} module(s) updated")
|
||||||
|
install_default_configs(ctx)
|
||||||
|
print_post_install_info(ctx)
|
||||||
else:
|
else:
|
||||||
print(f"\n⚠ Update finished with errors: {success} success, {failed} failed")
|
print(f"\n⚠ Update finished with errors: {success} success, {failed} failed")
|
||||||
if not args.force:
|
if not args.force:
|
||||||
@@ -1221,7 +1459,11 @@ def main(argv: Optional[Iterable[str]] = None) -> int:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"Failed to prepare install dir: {exc}", file=sys.stderr)
|
print(f"Failed to prepare install dir: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
return interactive_manage(config, ctx)
|
result = interactive_manage(config, ctx)
|
||||||
|
if result == 0 and ctx.get("_did_install"):
|
||||||
|
install_default_configs(ctx)
|
||||||
|
print_post_install_info(ctx)
|
||||||
|
return result
|
||||||
|
|
||||||
# Install specified modules
|
# Install specified modules
|
||||||
modules = select_modules(config, args.module)
|
modules = select_modules(config, args.module)
|
||||||
@@ -1280,6 +1522,10 @@ def main(argv: Optional[Iterable[str]] = None) -> int:
|
|||||||
if not args.force:
|
if not args.force:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
if failed == 0:
|
||||||
|
install_default_configs(ctx)
|
||||||
|
print_post_install_info(ctx)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -e
|
|||||||
if [ -z "${SKIP_WARNING:-}" ]; then
|
if [ -z "${SKIP_WARNING:-}" ]; then
|
||||||
echo "⚠️ WARNING: install.sh is LEGACY and will be removed in future versions."
|
echo "⚠️ WARNING: install.sh is LEGACY and will be removed in future versions."
|
||||||
echo "Please use the new installation method:"
|
echo "Please use the new installation method:"
|
||||||
echo " npx github:cexll/myclaude"
|
echo " npx github:stellarlinkco/myclaude"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Set SKIP_WARNING=1 to bypass this message"
|
echo "Set SKIP_WARNING=1 to bypass this message"
|
||||||
echo "Continuing with legacy installation in 5 seconds..."
|
echo "Continuing with legacy installation in 5 seconds..."
|
||||||
@@ -23,7 +23,7 @@ case "$ARCH" in
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
# Build download URL
|
# Build download URL
|
||||||
REPO="cexll/myclaude"
|
REPO="stellarlinkco/myclaude"
|
||||||
VERSION="${CODEAGENT_WRAPPER_VERSION:-latest}"
|
VERSION="${CODEAGENT_WRAPPER_VERSION:-latest}"
|
||||||
BINARY_NAME="codeagent-wrapper-${OS}-${ARCH}"
|
BINARY_NAME="codeagent-wrapper-${OS}-${ARCH}"
|
||||||
if [ "$VERSION" = "latest" ]; then
|
if [ "$VERSION" = "latest" ]; then
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "myclaude",
|
"name": "myclaude",
|
||||||
"version": "0.0.0",
|
"version": "6.7.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Claude Code multi-agent workflows (npx installer)",
|
"description": "Claude Code multi-agent workflows (npx installer)",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"agents/",
|
"agents/",
|
||||||
"skills/",
|
"skills/",
|
||||||
"memorys/",
|
"memorys/",
|
||||||
|
"templates/",
|
||||||
"codeagent-wrapper/",
|
"codeagent-wrapper/",
|
||||||
"config.json",
|
"config.json",
|
||||||
"install.py",
|
"install.py",
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ This directory contains agent skills (each skill lives in its own folder with a
|
|||||||
List installable items:
|
List installable items:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx github:cexll/myclaude --list
|
npx github:stellarlinkco/myclaude --list
|
||||||
```
|
```
|
||||||
|
|
||||||
Install (interactive; pick `skill:<name>`):
|
Install (interactive; pick `skill:<name>`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx github:cexll/myclaude
|
npx github:stellarlinkco/myclaude
|
||||||
```
|
```
|
||||||
|
|
||||||
Force overwrite / custom install directory:
|
Force overwrite / custom install directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx github:cexll/myclaude --install-dir ~/.claude --force
|
npx github:stellarlinkco/myclaude --install-dir ~/.claude --force
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,334 +0,0 @@
|
|||||||
---
|
|
||||||
name: codex
|
|
||||||
description: Execute Codex CLI for code analysis, refactoring, and automated code changes. Use when you need to delegate complex code tasks to Codex AI with file references (@syntax) and structured output.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Codex CLI Integration
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Execute Codex CLI commands and parse structured JSON responses. Supports file references via `@` syntax, multiple models, and sandbox controls.
|
|
||||||
|
|
||||||
## When to Use
|
|
||||||
|
|
||||||
- Complex code analysis requiring deep understanding
|
|
||||||
- Large-scale refactoring across multiple files
|
|
||||||
- Automated code generation with safety controls
|
|
||||||
|
|
||||||
## Fallback Policy
|
|
||||||
|
|
||||||
Codex is the **primary execution method** for all code edits and tests. Direct execution is only permitted when:
|
|
||||||
|
|
||||||
1. Codex is unavailable (service down, network issues)
|
|
||||||
2. Codex fails **twice consecutively** on the same task
|
|
||||||
|
|
||||||
When falling back to direct execution:
|
|
||||||
- Log `CODEX_FALLBACK` with the reason
|
|
||||||
- Retry Codex on the next task (don't permanently switch)
|
|
||||||
- Document the fallback in the final summary
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
**Mandatory**: Run every automated invocation through the Bash tool in the foreground with **HEREDOC syntax** to avoid shell quoting issues, keeping the `timeout` parameter fixed at `7200000` milliseconds (do not change it or use any other entry point).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
codex-wrapper - [working_dir] <<'EOF'
|
|
||||||
<task content here>
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why HEREDOC?** Tasks often contain code blocks, nested quotes, shell metacharacters (`$`, `` ` ``, `\`), and multiline text. HEREDOC (Here Document) syntax passes these safely without shell interpretation, eliminating quote-escaping nightmares.
|
|
||||||
|
|
||||||
**Foreground only (no background/BashOutput)**: Never set `background: true`, never accept Claude's "Running in the background" mode, and avoid `BashOutput` streaming loops. Keep a single foreground Bash call per Codex task; if work might be long, split it into smaller foreground runs instead of offloading to background execution.
|
|
||||||
|
|
||||||
**Simple tasks** (backward compatibility):
|
|
||||||
For simple single-line tasks without special characters, you can still use direct quoting:
|
|
||||||
```bash
|
|
||||||
codex-wrapper "simple task here" [working_dir]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Resume a session with HEREDOC:**
|
|
||||||
```bash
|
|
||||||
codex-wrapper resume <session_id> - [working_dir] <<'EOF'
|
|
||||||
<task content>
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cross-platform notes:**
|
|
||||||
- **Bash/Zsh**: Use `<<'EOF'` (single quotes prevent variable expansion)
|
|
||||||
- **PowerShell 5.1+**: Use `@'` and `'@` (here-string syntax)
|
|
||||||
```powershell
|
|
||||||
codex-wrapper - @'
|
|
||||||
task content
|
|
||||||
'@
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
- **CODEX_TIMEOUT**: Override timeout in milliseconds (default: 7200000 = 2 hours)
|
|
||||||
- Example: `export CODEX_TIMEOUT=3600000` for 1 hour
|
|
||||||
|
|
||||||
## Timeout Control
|
|
||||||
|
|
||||||
- **Built-in**: Binary enforces 2-hour timeout by default
|
|
||||||
- **Override**: Set `CODEX_TIMEOUT` environment variable (in milliseconds, e.g., `CODEX_TIMEOUT=3600000` for 1 hour)
|
|
||||||
- **Behavior**: On timeout, sends SIGTERM, then SIGKILL after 5s if process doesn't exit
|
|
||||||
- **Exit code**: Returns 124 on timeout (consistent with GNU timeout)
|
|
||||||
- **Bash tool**: Always set `timeout: 7200000` parameter for double protection
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
- `task` (required): Task description, supports `@file` references
|
|
||||||
- `working_dir` (optional): Working directory (default: current)
|
|
||||||
|
|
||||||
### Return Format
|
|
||||||
|
|
||||||
Extracts `agent_message` from Codex JSON stream and appends session ID:
|
|
||||||
```
|
|
||||||
Agent response text here...
|
|
||||||
|
|
||||||
---
|
|
||||||
SESSION_ID: 019a7247-ac9d-71f3-89e2-a823dbd8fd14
|
|
||||||
```
|
|
||||||
|
|
||||||
Error format (stderr):
|
|
||||||
```
|
|
||||||
ERROR: Error message
|
|
||||||
```
|
|
||||||
|
|
||||||
Return only the final agent message and session ID—do not paste raw `BashOutput` logs or background-task chatter into the conversation.
|
|
||||||
|
|
||||||
### Invocation Pattern
|
|
||||||
|
|
||||||
All automated executions must use HEREDOC syntax through the Bash tool in the foreground, with `timeout` fixed at `7200000` (non-negotiable):
|
|
||||||
|
|
||||||
```
|
|
||||||
Bash tool parameters:
|
|
||||||
- command: codex-wrapper - [working_dir] <<'EOF'
|
|
||||||
<task content>
|
|
||||||
EOF
|
|
||||||
- timeout: 7200000
|
|
||||||
- description: <brief description of the task>
|
|
||||||
```
|
|
||||||
|
|
||||||
Run every call in the foreground—never append `&` to background it—so logs and errors stay visible for timely interruption or diagnosis.
|
|
||||||
|
|
||||||
**Important:** Use HEREDOC (`<<'EOF'`) for all but the simplest tasks. This prevents shell interpretation of quotes, variables, and special characters.
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
**Basic code analysis:**
|
|
||||||
```bash
|
|
||||||
# Recommended: with HEREDOC (handles any special characters)
|
|
||||||
codex-wrapper - <<'EOF'
|
|
||||||
explain @src/main.ts
|
|
||||||
EOF
|
|
||||||
# timeout: 7200000
|
|
||||||
|
|
||||||
# Alternative: simple direct quoting (if task is simple)
|
|
||||||
codex-wrapper "explain @src/main.ts"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Refactoring with multiline instructions:**
|
|
||||||
```bash
|
|
||||||
codex-wrapper - <<'EOF'
|
|
||||||
refactor @src/utils for performance:
|
|
||||||
- Extract duplicate code into helpers
|
|
||||||
- Use memoization for expensive calculations
|
|
||||||
- Add inline comments for non-obvious logic
|
|
||||||
EOF
|
|
||||||
# timeout: 7200000
|
|
||||||
```
|
|
||||||
|
|
||||||
**Multi-file analysis:**
|
|
||||||
```bash
|
|
||||||
codex-wrapper - "/path/to/project" <<'EOF'
|
|
||||||
analyze @. and find security issues:
|
|
||||||
1. Check for SQL injection vulnerabilities
|
|
||||||
2. Identify XSS risks in templates
|
|
||||||
3. Review authentication/authorization logic
|
|
||||||
4. Flag hardcoded credentials or secrets
|
|
||||||
EOF
|
|
||||||
# timeout: 7200000
|
|
||||||
```
|
|
||||||
|
|
||||||
**Resume previous session:**
|
|
||||||
```bash
|
|
||||||
# First session
|
|
||||||
codex-wrapper - <<'EOF'
|
|
||||||
add comments to @utils.js explaining the caching logic
|
|
||||||
EOF
|
|
||||||
# Output includes: SESSION_ID: 019a7247-ac9d-71f3-89e2-a823dbd8fd14
|
|
||||||
|
|
||||||
# Continue the conversation with more context
|
|
||||||
codex-wrapper resume 019a7247-ac9d-71f3-89e2-a823dbd8fd14 - <<'EOF'
|
|
||||||
now add TypeScript type hints and handle edge cases where cache is null
|
|
||||||
EOF
|
|
||||||
# timeout: 7200000
|
|
||||||
```
|
|
||||||
|
|
||||||
**Task with code snippets and special characters:**
|
|
||||||
```bash
|
|
||||||
codex-wrapper - <<'EOF'
|
|
||||||
Fix the bug in @app.js where the regex /\d+/ doesn't match "123"
|
|
||||||
The current code is:
|
|
||||||
const re = /\d+/;
|
|
||||||
if (re.test(input)) { ... }
|
|
||||||
Add proper escaping and handle $variables correctly.
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parallel Execution
|
|
||||||
|
|
||||||
> Important:
|
|
||||||
> - `--parallel` only reads task definitions from stdin.
|
|
||||||
> - It does not accept extra command-line arguments (no inline `workdir`, `task`, or other params).
|
|
||||||
> - Put all task metadata and content in stdin; nothing belongs after `--parallel` on the command line.
|
|
||||||
|
|
||||||
**Correct vs Incorrect Usage**
|
|
||||||
|
|
||||||
**Correct:**
|
|
||||||
```bash
|
|
||||||
# Option 1: file redirection
|
|
||||||
codex-wrapper --parallel < tasks.txt
|
|
||||||
|
|
||||||
# Option 2: heredoc (recommended for multiple tasks)
|
|
||||||
codex-wrapper --parallel <<'EOF'
|
|
||||||
---TASK---
|
|
||||||
id: task1
|
|
||||||
workdir: /path/to/dir
|
|
||||||
---CONTENT---
|
|
||||||
task content
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Option 3: pipe
|
|
||||||
echo "---TASK---..." | codex-wrapper --parallel
|
|
||||||
```
|
|
||||||
|
|
||||||
**Incorrect (will trigger shell parsing errors):**
|
|
||||||
```bash
|
|
||||||
# Bad: no extra args allowed after --parallel
|
|
||||||
codex-wrapper --parallel - /path/to/dir <<'EOF'
|
|
||||||
...
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Bad: --parallel does not take a task argument
|
|
||||||
codex-wrapper --parallel "task description"
|
|
||||||
|
|
||||||
# Bad: workdir must live inside the task config
|
|
||||||
codex-wrapper --parallel /path/to/dir < tasks.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
For multiple independent or dependent tasks, use `--parallel` mode with delimiter format:
|
|
||||||
|
|
||||||
**Typical Workflow (analyze → implement → test, chained in a single parallel call)**:
|
|
||||||
```bash
|
|
||||||
codex-wrapper --parallel <<'EOF'
|
|
||||||
---TASK---
|
|
||||||
id: analyze_1732876800
|
|
||||||
workdir: /home/user/project
|
|
||||||
---CONTENT---
|
|
||||||
analyze @spec.md and summarize API and UI requirements
|
|
||||||
---TASK---
|
|
||||||
id: implement_1732876801
|
|
||||||
workdir: /home/user/project
|
|
||||||
dependencies: analyze_1732876800
|
|
||||||
---CONTENT---
|
|
||||||
implement features from analyze_1732876800 summary in backend @services and frontend @ui
|
|
||||||
---TASK---
|
|
||||||
id: test_1732876802
|
|
||||||
workdir: /home/user/project
|
|
||||||
dependencies: implement_1732876801
|
|
||||||
---CONTENT---
|
|
||||||
add and run regression tests covering the new endpoints and UI flows
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
A single `codex-wrapper --parallel` call schedules all three stages concurrently, using `dependencies` to enforce sequential ordering without multiple invocations.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
codex-wrapper --parallel <<'EOF'
|
|
||||||
---TASK---
|
|
||||||
id: backend_1732876800
|
|
||||||
workdir: /home/user/project/backend
|
|
||||||
---CONTENT---
|
|
||||||
implement /api/orders endpoints with validation and pagination
|
|
||||||
---TASK---
|
|
||||||
id: frontend_1732876801
|
|
||||||
workdir: /home/user/project/frontend
|
|
||||||
---CONTENT---
|
|
||||||
build Orders page consuming /api/orders with loading/error states
|
|
||||||
---TASK---
|
|
||||||
id: tests_1732876802
|
|
||||||
workdir: /home/user/project/tests
|
|
||||||
dependencies: backend_1732876800, frontend_1732876801
|
|
||||||
---CONTENT---
|
|
||||||
run API contract tests and UI smoke tests (waits for backend+frontend)
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Delimiter Format**:
|
|
||||||
- `---TASK---`: Starts a new task block
|
|
||||||
- `id: <task-id>`: Required, unique task identifier
|
|
||||||
- Best practice: use `<feature>_<timestamp>` format (e.g., `auth_1732876800`, `api_test_1732876801`)
|
|
||||||
- Ensures uniqueness across runs and makes tasks traceable
|
|
||||||
- `workdir: <path>`: Optional, working directory (default: `.`)
|
|
||||||
- Best practice: use absolute paths (e.g., `/home/user/project/backend`)
|
|
||||||
- Avoids ambiguity and ensures consistent behavior across environments
|
|
||||||
- Must be specified inside each task block; do not pass `workdir` as a CLI argument to `--parallel`
|
|
||||||
- Each task can set its own `workdir` when different directories are needed
|
|
||||||
- `dependencies: <id1>, <id2>`: Optional, comma-separated task IDs
|
|
||||||
- `session_id: <uuid>`: Optional, resume a previous session
|
|
||||||
- `---CONTENT---`: Separates metadata from task content
|
|
||||||
- Task content: Any text, code, special characters (no escaping needed)
|
|
||||||
|
|
||||||
**Dependencies Best Practices**
|
|
||||||
|
|
||||||
- Avoid multiple invocations: Place "analyze then implement" in a single `codex-wrapper --parallel` call, chaining them via `dependencies`, rather than running analysis first and then launching implementation separately.
|
|
||||||
- Naming convention: Use `<action>_<timestamp>` format (e.g., `analyze_1732876800`, `implement_1732876801`), where action names map to features/stages and timestamps ensure uniqueness and sortability.
|
|
||||||
- Dependency chain design: Keep chains short; only add dependencies for tasks that truly require ordering, let others run in parallel, avoiding over-serialization that reduces throughput.
|
|
||||||
|
|
||||||
**Resume Failed Tasks**:
|
|
||||||
```bash
|
|
||||||
# Use session_id from previous output to resume
|
|
||||||
codex-wrapper --parallel <<'EOF'
|
|
||||||
---TASK---
|
|
||||||
id: T2
|
|
||||||
session_id: 019xxx-previous-session-id
|
|
||||||
---CONTENT---
|
|
||||||
fix the previous error and retry
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output**: Human-readable text format
|
|
||||||
```
|
|
||||||
=== Parallel Execution Summary ===
|
|
||||||
Total: 3 | Success: 2 | Failed: 1
|
|
||||||
|
|
||||||
--- Task: T1 ---
|
|
||||||
Status: SUCCESS
|
|
||||||
Session: 019xxx
|
|
||||||
|
|
||||||
Task output message...
|
|
||||||
|
|
||||||
--- Task: T2 ---
|
|
||||||
Status: FAILED (exit code 1)
|
|
||||||
Error: some error message
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features**:
|
|
||||||
- Automatic topological sorting based on dependencies
|
|
||||||
- Unlimited concurrency for independent tasks
|
|
||||||
- Error isolation (failed tasks don't stop others)
|
|
||||||
- Dependency blocking (dependent tasks skip if parent fails)
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- **Binary distribution**: Single Go binary, zero dependencies
|
|
||||||
- **Installation**: Download from GitHub Releases or use install.sh
|
|
||||||
- **Cross-platform compatible**: Linux (amd64/arm64), macOS (amd64/arm64)
|
|
||||||
- All automated runs must use the Bash tool with the fixed timeout to provide dual timeout protection and unified logging/exit semantics
|
|
||||||
for automation (new sessions only)
|
|
||||||
- Uses `--skip-git-repo-check` to work in any directory
|
|
||||||
- Streams progress, returns only final agent message
|
|
||||||
- Every execution returns a session ID for resuming conversations
|
|
||||||
- Requires Codex CLI installed and authenticated
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: do
|
name: do
|
||||||
description: This skill should be used for structured feature development with codebase understanding. Triggers on /do command. Provides a 5-phase workflow (Understand, Clarify, Design, Implement, Complete) using codeagent-wrapper to orchestrate code-explorer, code-architect, code-reviewer, and develop agents in parallel.
|
description: This skill should be used for structured feature development with codebase understanding. Triggers on /do command. Provides a 5-phase workflow (Understand, Clarify, Design, Implement, Complete) using codeagent-wrapper to orchestrate code-explorer, code-architect, code-reviewer, and develop agents in parallel.
|
||||||
allowed-tools: ["Bash(.claude/skills/do/scripts/setup-do.py:*)", "Bash(.claude/skills/do/scripts/task.py:*)"]
|
allowed-tools: ["Bash(python3:*/.claude/skills/do/scripts/setup-do.py*)", "Bash(python3:*/.claude/skills/do/scripts/task.py*)"]
|
||||||
---
|
---
|
||||||
|
|
||||||
# do - Feature Development Orchestrator
|
# do - Feature Development Orchestrator
|
||||||
@@ -10,57 +10,51 @@ An orchestrator for systematic feature development. Invoke agents via `codeagent
|
|||||||
|
|
||||||
## Loop Initialization (REQUIRED)
|
## Loop Initialization (REQUIRED)
|
||||||
|
|
||||||
When triggered via `/do <task>`, follow these steps:
|
When triggered via `/do <task>`, initialize the task directory immediately without asking about worktree:
|
||||||
|
|
||||||
### Step 1: Ask about worktree mode
|
|
||||||
|
|
||||||
Use AskUserQuestion to ask:
|
|
||||||
|
|
||||||
```
|
|
||||||
Develop in a separate worktree? (Isolates changes from main branch)
|
|
||||||
- Yes (Recommended for larger changes)
|
|
||||||
- No (Work directly in current directory)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Initialize task directory
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# If worktree mode selected:
|
python3 "$HOME/.claude/skills/do/scripts/setup-do.py" "<task description>"
|
||||||
python3 ".claude/skills/do/scripts/setup-do.py" --worktree "<task description>"
|
|
||||||
|
|
||||||
# If no worktree:
|
|
||||||
python3 ".claude/skills/do/scripts/setup-do.py" "<task description>"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates a task directory under `.claude/do-tasks/` with:
|
This creates a task directory under `.claude/do-tasks/` with:
|
||||||
- `task.md`: Single file containing YAML frontmatter (metadata) + Markdown body (requirements/context)
|
- `task.md`: Single file containing YAML frontmatter (metadata) + Markdown body (requirements/context)
|
||||||
|
|
||||||
|
**Worktree decision is deferred until Phase 4 (Implement).** Phases 1-3 are read-only and do not require worktree isolation.
|
||||||
|
|
||||||
## Task Directory Management
|
## Task Directory Management
|
||||||
|
|
||||||
Use `task.py` to manage task state:
|
Use `task.py` to manage task state:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Update phase
|
# Update phase
|
||||||
python3 ".claude/skills/do/scripts/task.py" update-phase 2
|
python3 "$HOME/.claude/skills/do/scripts/task.py" update-phase 2
|
||||||
|
|
||||||
# Check status
|
# Check status
|
||||||
python3 ".claude/skills/do/scripts/task.py" status
|
python3 "$HOME/.claude/skills/do/scripts/task.py" status
|
||||||
|
|
||||||
# List all tasks
|
# List all tasks
|
||||||
python3 ".claude/skills/do/scripts/task.py" list
|
python3 "$HOME/.claude/skills/do/scripts/task.py" list
|
||||||
```
|
```
|
||||||
|
|
||||||
## Worktree Mode
|
## Worktree Mode
|
||||||
|
|
||||||
When worktree mode is enabled in task.json, ALL `codeagent-wrapper` calls that modify code MUST include `--worktree`:
|
The worktree is created **only when needed** (right before Phase 4: Implement). If the user chooses worktree mode:
|
||||||
|
|
||||||
|
1. Run setup with `--worktree` flag to create the worktree:
|
||||||
|
```bash
|
||||||
|
python3 "$HOME/.claude/skills/do/scripts/setup-do.py" --worktree "<task description>"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Use the `DO_WORKTREE_DIR` environment variable to direct `codeagent-wrapper` develop agent into the worktree. **Do NOT pass `--worktree` to subsequent calls** — that creates a new worktree each time.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codeagent-wrapper --worktree --agent develop - . <<'EOF'
|
# Save the worktree path from setup output, then prefix all develop calls:
|
||||||
|
DO_WORKTREE_DIR=<worktree_dir> codeagent-wrapper --agent develop - . <<'EOF'
|
||||||
...
|
...
|
||||||
EOF
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
Read-only agents (code-explorer, code-architect, code-reviewer) do NOT need `--worktree`.
|
Read-only agents (code-explorer, code-architect, code-reviewer) do NOT need `DO_WORKTREE_DIR`.
|
||||||
|
|
||||||
## Hard Constraints
|
## Hard Constraints
|
||||||
|
|
||||||
@@ -69,7 +63,7 @@ Read-only agents (code-explorer, code-architect, code-reviewer) do NOT need `--w
|
|||||||
3. **Update phase after each phase.** Use `task.py update-phase <N>`.
|
3. **Update phase after each phase.** Use `task.py update-phase <N>`.
|
||||||
4. **Expect long-running `codeagent-wrapper` calls.** High-reasoning modes can take a long time.
|
4. **Expect long-running `codeagent-wrapper` calls.** High-reasoning modes can take a long time.
|
||||||
5. **Timeouts are not an escape hatch.** If a call times out, retry with narrower scope.
|
5. **Timeouts are not an escape hatch.** If a call times out, retry with narrower scope.
|
||||||
6. **Respect worktree setting.** If enabled, always pass `--worktree` to develop agent calls.
|
6. **Defer worktree decision until Phase 4.** Only ask about worktree mode right before implementation. If enabled, prefix develop agent calls with `DO_WORKTREE_DIR=<path>`. Never pass `--worktree` after initialization.
|
||||||
|
|
||||||
## Agents
|
## Agents
|
||||||
|
|
||||||
@@ -78,7 +72,7 @@ Read-only agents (code-explorer, code-architect, code-reviewer) do NOT need `--w
|
|||||||
| `code-explorer` | Trace code, map architecture, find patterns | No (read-only) |
|
| `code-explorer` | Trace code, map architecture, find patterns | No (read-only) |
|
||||||
| `code-architect` | Design approaches, file plans, build sequences | No (read-only) |
|
| `code-architect` | Design approaches, file plans, build sequences | No (read-only) |
|
||||||
| `code-reviewer` | Review for bugs, simplicity, conventions | No (read-only) |
|
| `code-reviewer` | Review for bugs, simplicity, conventions | No (read-only) |
|
||||||
| `develop` | Implement code, run tests | **Yes** (if worktree enabled) |
|
| `develop` | Implement code, run tests | **Yes** — use `DO_WORKTREE_DIR` env prefix |
|
||||||
|
|
||||||
## Issue Severity Definitions
|
## Issue Severity Definitions
|
||||||
|
|
||||||
@@ -175,12 +169,39 @@ EOF
|
|||||||
|
|
||||||
**Goal:** Build feature and review in one phase.
|
**Goal:** Build feature and review in one phase.
|
||||||
|
|
||||||
1. Invoke `develop` to implement. For full-stack projects, split into backend/frontend tasks with per-task `skills:` injection. Use `--parallel` when tasks can be split; use single agent when the change is small or single-domain.
|
**Step 1: Decide on worktree mode (ONLY NOW)**
|
||||||
|
|
||||||
**Single-domain example** (add `--worktree` if enabled):
|
Use AskUserQuestion to ask:
|
||||||
|
|
||||||
|
```
|
||||||
|
Develop in a separate worktree? (Isolates changes from main branch)
|
||||||
|
- Yes (Recommended for larger changes)
|
||||||
|
- No (Work directly in current directory)
|
||||||
|
```
|
||||||
|
|
||||||
|
If user chooses worktree:
|
||||||
|
```bash
|
||||||
|
python3 "$HOME/.claude/skills/do/scripts/setup-do.py" --worktree "<task description>"
|
||||||
|
# Save the worktree path from output for DO_WORKTREE_DIR
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Invoke develop agent**
|
||||||
|
|
||||||
|
For full-stack projects, split into backend/frontend tasks with per-task `skills:` injection. Use `--parallel` when tasks can be split; use single agent when the change is small or single-domain.
|
||||||
|
|
||||||
|
**Single-domain example** (prefix with `DO_WORKTREE_DIR` if worktree enabled):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codeagent-wrapper --worktree --agent develop --skills golang-base-practices - . <<'EOF'
|
# With worktree:
|
||||||
|
DO_WORKTREE_DIR=<worktree_dir> codeagent-wrapper --agent develop --skills golang-base-practices - . <<'EOF'
|
||||||
|
Implement with minimal change set following the Phase 3 blueprint.
|
||||||
|
- Follow Phase 1 patterns
|
||||||
|
- Add/adjust tests per Phase 3 plan
|
||||||
|
- Run narrowest relevant tests
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Without worktree:
|
||||||
|
codeagent-wrapper --agent develop --skills golang-base-practices - . <<'EOF'
|
||||||
Implement with minimal change set following the Phase 3 blueprint.
|
Implement with minimal change set following the Phase 3 blueprint.
|
||||||
- Follow Phase 1 patterns
|
- Follow Phase 1 patterns
|
||||||
- Add/adjust tests per Phase 3 plan
|
- Add/adjust tests per Phase 3 plan
|
||||||
@@ -191,7 +212,8 @@ EOF
|
|||||||
**Full-stack parallel example** (adapt task IDs, skills, and content based on Phase 3 design):
|
**Full-stack parallel example** (adapt task IDs, skills, and content based on Phase 3 design):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codeagent-wrapper --worktree --parallel <<'EOF'
|
# With worktree:
|
||||||
|
DO_WORKTREE_DIR=<worktree_dir> codeagent-wrapper --parallel <<'EOF'
|
||||||
---TASK---
|
---TASK---
|
||||||
id: p4_backend
|
id: p4_backend
|
||||||
agent: develop
|
agent: develop
|
||||||
@@ -213,11 +235,17 @@ Implement frontend changes following Phase 3 blueprint.
|
|||||||
- Follow Phase 1 patterns
|
- Follow Phase 1 patterns
|
||||||
- Add/adjust tests per Phase 3 plan
|
- Add/adjust tests per Phase 3 plan
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Without worktree: remove DO_WORKTREE_DIR prefix
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: Choose which skills to inject based on Phase 3 design output. Only inject skills relevant to each task's domain.
|
Note: Choose which skills to inject based on Phase 3 design output. Only inject skills relevant to each task's domain.
|
||||||
|
|
||||||
2. Run parallel reviews:
|
**Step 3: Review**
|
||||||
|
|
||||||
|
**Step 3: Review**
|
||||||
|
|
||||||
|
Run parallel reviews:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
codeagent-wrapper --parallel <<'EOF'
|
codeagent-wrapper --parallel <<'EOF'
|
||||||
@@ -239,9 +267,10 @@ Classify each issue as BLOCKING or MINOR.
|
|||||||
EOF
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Handle review results:
|
**Step 4: Handle review results**
|
||||||
- **MINOR issues only** → Auto-fix via `develop`, no user interaction
|
|
||||||
- **BLOCKING issues** → Use AskUserQuestion: "Fix now / Proceed as-is"
|
- **MINOR issues only** → Auto-fix via `develop`, no user interaction
|
||||||
|
- **BLOCKING issues** → Use AskUserQuestion: "Fix now / Proceed as-is"
|
||||||
|
|
||||||
### Phase 5: Complete (No Interaction)
|
### Phase 5: Complete (No Interaction)
|
||||||
|
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
---
|
|
||||||
name: gemini
|
|
||||||
description: Execute Gemini CLI for AI-powered code analysis and generation. Use when you need to leverage Google's Gemini models for complex reasoning tasks.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Gemini CLI Integration
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Execute Gemini CLI commands with support for multiple models and flexible prompt input. Integrates Google's Gemini AI models into Claude Code workflows.
|
|
||||||
|
|
||||||
## When to Use
|
|
||||||
|
|
||||||
- Complex reasoning tasks requiring advanced AI capabilities
|
|
||||||
- Code generation and analysis with Gemini models
|
|
||||||
- Tasks requiring Google's latest AI technology
|
|
||||||
- Alternative perspective on code problems
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
**Mandatory**: Run via uv with fixed timeout 7200000ms (foreground):
|
|
||||||
```bash
|
|
||||||
uv run ~/.claude/skills/gemini/scripts/gemini.py "<prompt>" [working_dir]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Optional** (direct execution or using Python):
|
|
||||||
```bash
|
|
||||||
~/.claude/skills/gemini/scripts/gemini.py "<prompt>" [working_dir]
|
|
||||||
# or
|
|
||||||
python3 ~/.claude/skills/gemini/scripts/gemini.py "<prompt>" [working_dir]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
- **GEMINI_MODEL**: Configure model (default: `gemini-3-pro-preview`)
|
|
||||||
- Example: `export GEMINI_MODEL=gemini-3`
|
|
||||||
|
|
||||||
## Timeout Control
|
|
||||||
|
|
||||||
- **Fixed**: 7200000 milliseconds (2 hours), immutable
|
|
||||||
- **Bash tool**: Always set `timeout: 7200000` for double protection
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
- `prompt` (required): Task prompt or question
|
|
||||||
- `working_dir` (optional): Working directory (default: current directory)
|
|
||||||
|
|
||||||
### Return Format
|
|
||||||
|
|
||||||
Plain text output from Gemini:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Model response text here...
|
|
||||||
```
|
|
||||||
|
|
||||||
Error format (stderr):
|
|
||||||
|
|
||||||
```text
|
|
||||||
ERROR: Error message
|
|
||||||
```
|
|
||||||
|
|
||||||
### Invocation Pattern
|
|
||||||
|
|
||||||
When calling via Bash tool, always include the timeout parameter:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
Bash tool parameters:
|
|
||||||
- command: uv run ~/.claude/skills/gemini/scripts/gemini.py "<prompt>"
|
|
||||||
- timeout: 7200000
|
|
||||||
- description: <brief description of the task>
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatives:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Direct execution (simplest)
|
|
||||||
- command: ~/.claude/skills/gemini/scripts/gemini.py "<prompt>"
|
|
||||||
|
|
||||||
# Using python3
|
|
||||||
- command: python3 ~/.claude/skills/gemini/scripts/gemini.py "<prompt>"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
**Basic query:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run ~/.claude/skills/gemini/scripts/gemini.py "explain quantum computing"
|
|
||||||
# timeout: 7200000
|
|
||||||
```
|
|
||||||
|
|
||||||
**Code analysis:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run ~/.claude/skills/gemini/scripts/gemini.py "review this code for security issues: $(cat app.py)"
|
|
||||||
# timeout: 7200000
|
|
||||||
```
|
|
||||||
|
|
||||||
**With specific working directory:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run ~/.claude/skills/gemini/scripts/gemini.py "analyze project structure" "/path/to/project"
|
|
||||||
# timeout: 7200000
|
|
||||||
```
|
|
||||||
|
|
||||||
**Using python3 directly (alternative):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 ~/.claude/skills/gemini/scripts/gemini.py "your prompt here"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- **Recommended**: Use `uv run` for automatic Python environment management (requires uv installed)
|
|
||||||
- **Alternative**: Direct execution `./gemini.py` (uses system Python via shebang)
|
|
||||||
- Python implementation using standard library (zero dependencies)
|
|
||||||
- Cross-platform compatible (Windows/macOS/Linux)
|
|
||||||
- PEP 723 compliant (inline script metadata)
|
|
||||||
- Requires Gemini CLI installed and authenticated
|
|
||||||
- Supports all Gemini model variants (configure via `GEMINI_MODEL` environment variable)
|
|
||||||
- Output is streamed directly from Gemini CLI
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# /// script
|
|
||||||
# requires-python = ">=3.8"
|
|
||||||
# dependencies = []
|
|
||||||
# ///
|
|
||||||
"""
|
|
||||||
Gemini CLI wrapper with cross-platform support.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
uv run gemini.py "<prompt>" [workdir]
|
|
||||||
python3 gemini.py "<prompt>"
|
|
||||||
./gemini.py "your prompt"
|
|
||||||
"""
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
DEFAULT_MODEL = os.environ.get('GEMINI_MODEL', 'gemini-3-pro-preview')
|
|
||||||
DEFAULT_WORKDIR = '.'
|
|
||||||
TIMEOUT_MS = 7_200_000 # 固定 2 小时,毫秒
|
|
||||||
DEFAULT_TIMEOUT = TIMEOUT_MS // 1000
|
|
||||||
FORCE_KILL_DELAY = 5
|
|
||||||
|
|
||||||
|
|
||||||
def log_error(message: str):
|
|
||||||
"""输出错误信息到 stderr"""
|
|
||||||
sys.stderr.write(f"ERROR: {message}\n")
|
|
||||||
|
|
||||||
|
|
||||||
def log_warn(message: str):
|
|
||||||
"""输出警告信息到 stderr"""
|
|
||||||
sys.stderr.write(f"WARN: {message}\n")
|
|
||||||
|
|
||||||
|
|
||||||
def log_info(message: str):
|
|
||||||
"""输出信息到 stderr"""
|
|
||||||
sys.stderr.write(f"INFO: {message}\n")
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
|
||||||
"""解析位置参数"""
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
log_error('Prompt required')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'prompt': sys.argv[1],
|
|
||||||
'workdir': sys.argv[2] if len(sys.argv) > 2 else DEFAULT_WORKDIR
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_gemini_args(args) -> list:
|
|
||||||
"""构建 gemini CLI 参数"""
|
|
||||||
return [
|
|
||||||
'gemini',
|
|
||||||
'-m', DEFAULT_MODEL,
|
|
||||||
'-p', args['prompt']
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
log_info('Script started')
|
|
||||||
args = parse_args()
|
|
||||||
log_info(f"Prompt length: {len(args['prompt'])}")
|
|
||||||
log_info(f"Working dir: {args['workdir']}")
|
|
||||||
gemini_args = build_gemini_args(args)
|
|
||||||
timeout_sec = DEFAULT_TIMEOUT
|
|
||||||
log_info(f"Timeout: {timeout_sec}s")
|
|
||||||
|
|
||||||
# 如果指定了工作目录,切换到该目录
|
|
||||||
if args['workdir'] != DEFAULT_WORKDIR:
|
|
||||||
try:
|
|
||||||
os.chdir(args['workdir'])
|
|
||||||
except FileNotFoundError:
|
|
||||||
log_error(f"Working directory not found: {args['workdir']}")
|
|
||||||
sys.exit(1)
|
|
||||||
except PermissionError:
|
|
||||||
log_error(f"Permission denied: {args['workdir']}")
|
|
||||||
sys.exit(1)
|
|
||||||
log_info('Changed working directory')
|
|
||||||
|
|
||||||
try:
|
|
||||||
log_info(f"Starting gemini with model {DEFAULT_MODEL}")
|
|
||||||
process = None
|
|
||||||
# 启动 gemini 子进程,直接透传 stdout 和 stderr
|
|
||||||
process = subprocess.Popen(
|
|
||||||
gemini_args,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True,
|
|
||||||
bufsize=1 # 行缓冲
|
|
||||||
)
|
|
||||||
|
|
||||||
# 实时输出 stdout
|
|
||||||
for line in process.stdout:
|
|
||||||
sys.stdout.write(line)
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
# 等待进程结束
|
|
||||||
returncode = process.wait(timeout=timeout_sec)
|
|
||||||
|
|
||||||
# 读取 stderr
|
|
||||||
stderr_output = process.stderr.read()
|
|
||||||
if stderr_output:
|
|
||||||
sys.stderr.write(stderr_output)
|
|
||||||
|
|
||||||
# 检查退出码
|
|
||||||
if returncode != 0:
|
|
||||||
log_error(f'Gemini exited with status {returncode}')
|
|
||||||
sys.exit(returncode)
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
log_error(f'Gemini execution timeout ({timeout_sec}s)')
|
|
||||||
if process is not None:
|
|
||||||
process.kill()
|
|
||||||
try:
|
|
||||||
process.wait(timeout=FORCE_KILL_DELAY)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
pass
|
|
||||||
sys.exit(124)
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
log_error("gemini command not found in PATH")
|
|
||||||
log_error("Please install Gemini CLI: https://github.com/google/generative-ai-python")
|
|
||||||
sys.exit(127)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
if process is not None:
|
|
||||||
process.terminate()
|
|
||||||
try:
|
|
||||||
process.wait(timeout=FORCE_KILL_DELAY)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
process.kill()
|
|
||||||
sys.exit(130)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
329
skills/harness/SKILL.md
Normal file
329
skills/harness/SKILL.md
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
---
|
||||||
|
name: harness
|
||||||
|
description: "This skill should be used for multi-session autonomous agent work requiring progress checkpointing, failure recovery, and task dependency management. Triggers on '/harness' command, or when a task involves many subtasks needing progress persistence, sleep/resume cycles across context windows, recovery from mid-task failures with partial state, or distributed work across multiple agent sessions. Synthesized from Anthropic and OpenAI engineering practices for long-running agents."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Harness — Long-Running Agent Framework
|
||||||
|
|
||||||
|
Executable protocol enabling any agent task to run continuously across multiple sessions with automatic progress recovery, task dependency resolution, failure rollback, and standardized error handling.
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
1. **Design for the agent, not the human** — Test output, docs, and task structure are the agent's primary interface
|
||||||
|
2. **Progress files ARE the context** — When context window resets, progress files + git history = full recovery
|
||||||
|
3. **Premature completion is the #1 failure mode** — Structured task lists with explicit completion criteria prevent declaring victory early
|
||||||
|
4. **Standardize everything grep-able** — ERROR on same line, structured timestamps, consistent prefixes
|
||||||
|
5. **Fast feedback loops** — Pre-compute stats, run smoke tests before full validation
|
||||||
|
6. **Idempotent everything** — Init scripts, task execution, environment setup must all be safe to re-run
|
||||||
|
7. **Fail safe, not fail silent** — Every failure must have an explicit recovery strategy
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
/harness init <project-path> # Initialize harness files in project
|
||||||
|
/harness run # Start/resume the infinite loop
|
||||||
|
/harness status # Show current progress and stats
|
||||||
|
/harness add "task description" # Add a task to the list
|
||||||
|
```
|
||||||
|
|
||||||
|
## Progress Persistence (Dual-File System)
|
||||||
|
|
||||||
|
Maintain two files in the project working directory:
|
||||||
|
|
||||||
|
### harness-progress.txt (Append-Only Log)
|
||||||
|
|
||||||
|
Free-text log of all agent actions across sessions. Never truncate.
|
||||||
|
|
||||||
|
```
|
||||||
|
[2025-07-01T10:00:00Z] [SESSION-1] INIT Harness initialized for project /path/to/project
|
||||||
|
[2025-07-01T10:00:05Z] [SESSION-1] INIT Environment health check: PASS
|
||||||
|
[2025-07-01T10:00:10Z] [SESSION-1] LOCK acquired (pid=12345)
|
||||||
|
[2025-07-01T10:00:11Z] [SESSION-1] Starting [task-001] Implement user authentication (base=def5678)
|
||||||
|
[2025-07-01T10:05:00Z] [SESSION-1] CHECKPOINT [task-001] step=2/4 "auth routes created, tests pending"
|
||||||
|
[2025-07-01T10:15:30Z] [SESSION-1] Completed [task-001] (commit abc1234)
|
||||||
|
[2025-07-01T10:15:31Z] [SESSION-1] Starting [task-002] Add rate limiting (base=abc1234)
|
||||||
|
[2025-07-01T10:20:00Z] [SESSION-1] ERROR [task-002] [TASK_EXEC] Redis connection refused
|
||||||
|
[2025-07-01T10:20:01Z] [SESSION-1] ROLLBACK [task-002] git reset --hard abc1234
|
||||||
|
[2025-07-01T10:20:02Z] [SESSION-1] STATS tasks_total=5 completed=1 failed=1 pending=3 blocked=0 attempts_total=2 checkpoints=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### harness-tasks.json (Structured State)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"created": "2025-07-01T10:00:00Z",
|
||||||
|
"session_config": {
|
||||||
|
"max_tasks_per_session": 20,
|
||||||
|
"max_sessions": 50
|
||||||
|
},
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "task-001",
|
||||||
|
"title": "Implement user authentication",
|
||||||
|
"status": "completed",
|
||||||
|
"priority": "P0",
|
||||||
|
"depends_on": [],
|
||||||
|
"attempts": 1,
|
||||||
|
"max_attempts": 3,
|
||||||
|
"started_at_commit": "def5678",
|
||||||
|
"validation": {
|
||||||
|
"command": "npm test -- --testPathPattern=auth",
|
||||||
|
"timeout_seconds": 300
|
||||||
|
},
|
||||||
|
"on_failure": {
|
||||||
|
"cleanup": null
|
||||||
|
},
|
||||||
|
"error_log": [],
|
||||||
|
"checkpoints": [],
|
||||||
|
"completed_at": "2025-07-01T10:15:30Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "task-002",
|
||||||
|
"title": "Add rate limiting",
|
||||||
|
"status": "failed",
|
||||||
|
"priority": "P1",
|
||||||
|
"depends_on": [],
|
||||||
|
"attempts": 1,
|
||||||
|
"max_attempts": 3,
|
||||||
|
"started_at_commit": "abc1234",
|
||||||
|
"validation": {
|
||||||
|
"command": "npm test -- --testPathPattern=rate-limit",
|
||||||
|
"timeout_seconds": 120
|
||||||
|
},
|
||||||
|
"on_failure": {
|
||||||
|
"cleanup": "docker compose down redis"
|
||||||
|
},
|
||||||
|
"error_log": ["[TASK_EXEC] Redis connection refused"],
|
||||||
|
"checkpoints": [],
|
||||||
|
"completed_at": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "task-003",
|
||||||
|
"title": "Add OAuth providers",
|
||||||
|
"status": "pending",
|
||||||
|
"priority": "P1",
|
||||||
|
"depends_on": ["task-001"],
|
||||||
|
"attempts": 0,
|
||||||
|
"max_attempts": 3,
|
||||||
|
"started_at_commit": null,
|
||||||
|
"validation": {
|
||||||
|
"command": "npm test -- --testPathPattern=oauth",
|
||||||
|
"timeout_seconds": 180
|
||||||
|
},
|
||||||
|
"on_failure": {
|
||||||
|
"cleanup": null
|
||||||
|
},
|
||||||
|
"error_log": [],
|
||||||
|
"checkpoints": [],
|
||||||
|
"completed_at": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"session_count": 1,
|
||||||
|
"last_session": "2025-07-01T10:20:02Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Task statuses: `pending` → `in_progress` (transient, set only during active execution) → `completed` or `failed`. A task found as `in_progress` at session start means the previous session was interrupted — handle via Context Window Recovery Protocol.
|
||||||
|
|
||||||
|
**Session boundary**: A session starts when the agent begins executing the Session Start protocol and ends when a Stopping Condition is met or the context window resets. Each session gets a unique `SESSION-N` identifier (N = `session_count` after increment).
|
||||||
|
|
||||||
|
## Concurrency Control
|
||||||
|
|
||||||
|
Before modifying `harness-tasks.json`, acquire an exclusive lock using portable `mkdir` (atomic on all POSIX systems, works on both macOS and Linux):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Acquire lock (fail fast if another agent is running)
|
||||||
|
LOCKDIR="/tmp/harness-$(printf '%s' "$(pwd)" | shasum -a 256 2>/dev/null || sha256sum | cut -c1-8).lock"
|
||||||
|
if ! mkdir "$LOCKDIR" 2>/dev/null; then
|
||||||
|
# Check if lock holder is still alive
|
||||||
|
LOCK_PID=$(cat "$LOCKDIR/pid" 2>/dev/null)
|
||||||
|
if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
|
||||||
|
echo "ERROR: Another harness session is active (pid=$LOCK_PID)"; exit 1
|
||||||
|
fi
|
||||||
|
# Stale lock — atomically reclaim via mv to avoid TOCTOU race
|
||||||
|
STALE="$LOCKDIR.stale.$$"
|
||||||
|
if mv "$LOCKDIR" "$STALE" 2>/dev/null; then
|
||||||
|
rm -rf "$STALE"
|
||||||
|
mkdir "$LOCKDIR" || { echo "ERROR: Lock contention"; exit 1; }
|
||||||
|
echo "WARN: Removed stale lock${LOCK_PID:+ from pid=$LOCK_PID}"
|
||||||
|
else
|
||||||
|
echo "ERROR: Another agent reclaimed the lock"; exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "$$" > "$LOCKDIR/pid"
|
||||||
|
trap 'rm -rf "$LOCKDIR"' EXIT
|
||||||
|
```
|
||||||
|
|
||||||
|
Log lock acquisition: `[timestamp] [SESSION-N] LOCK acquired (pid=<PID>)`
|
||||||
|
Log lock release: `[timestamp] [SESSION-N] LOCK released`
|
||||||
|
|
||||||
|
The lock is held for the entire session. The `trap EXIT` handler releases it automatically on normal exit, errors, or signals. Never release the lock between tasks within a session.
|
||||||
|
|
||||||
|
## Infinite Loop Protocol
|
||||||
|
|
||||||
|
### Session Start (Execute Every Time)
|
||||||
|
|
||||||
|
1. **Read state**: Read last 200 lines of `harness-progress.txt` + full `harness-tasks.json`. If JSON is unparseable, see JSON corruption recovery in Error Handling.
|
||||||
|
2. **Read git**: Run `git log --oneline -20` and `git diff --stat` to detect uncommitted work
|
||||||
|
3. **Acquire lock**: Fail if another session is active
|
||||||
|
4. **Recover interrupted tasks** (see Context Window Recovery below)
|
||||||
|
5. **Health check**: Run `harness-init.sh` if it exists
|
||||||
|
6. **Track session**: Increment `session_count` in JSON. Check `session_count` against `max_sessions` — if reached, log STATS and STOP. Initialize per-session task counter to 0.
|
||||||
|
7. **Pick next task** using Task Selection Algorithm below
|
||||||
|
|
||||||
|
### Task Selection Algorithm
|
||||||
|
|
||||||
|
Before selecting, run dependency validation:
|
||||||
|
|
||||||
|
1. **Cycle detection**: For each non-completed task, walk `depends_on` transitively. If any task appears in its own chain, mark it `failed` with `[DEPENDENCY] Circular dependency detected: task-A -> task-B -> task-A`. Self-references (`depends_on` includes own id) are also cycles.
|
||||||
|
2. **Blocked propagation**: If a task's `depends_on` includes a task that is `failed` and will never be retried (either `attempts >= max_attempts` OR its `error_log` contains a `[DEPENDENCY]` entry), mark the blocked task as `failed` with `[DEPENDENCY] Blocked by failed task-XXX`. Repeat until no more tasks can be propagated.
|
||||||
|
|
||||||
|
Then pick the next task in this priority order:
|
||||||
|
|
||||||
|
1. Tasks with `status: "pending"` where ALL `depends_on` tasks are `completed` — sorted by `priority` (P0 > P1 > P2), then by `id` (lowest first)
|
||||||
|
2. Tasks with `status: "failed"` where `attempts < max_attempts` and ALL `depends_on` are `completed` — sorted by priority, then oldest failure first
|
||||||
|
3. If no eligible tasks remain → log final STATS → STOP
|
||||||
|
|
||||||
|
### Task Execution Cycle
|
||||||
|
|
||||||
|
For each task, execute this exact sequence:
|
||||||
|
|
||||||
|
1. **Claim**: Record `started_at_commit` = current HEAD hash. Set status to `in_progress`, log `Starting [<task-id>] <title> (base=<hash>)`
|
||||||
|
2. **Execute with checkpoints**: Perform the work. After each significant step, log:
|
||||||
|
```
|
||||||
|
[timestamp] [SESSION-N] CHECKPOINT [task-id] step=M/N "description of what was done"
|
||||||
|
```
|
||||||
|
Also append to the task's `checkpoints` array: `{ "step": M, "total": N, "description": "...", "timestamp": "ISO" }`
|
||||||
|
3. **Validate**: Run the task's `validation.command` wrapped with `timeout`: `timeout <timeout_seconds> <command>`. If no validation command, skip. Before running, verify the command exists (e.g., `command -v <binary>`) — if missing, treat as `ENV_SETUP` error.
|
||||||
|
- Command exits 0 → PASS
|
||||||
|
- Command exits non-zero → FAIL
|
||||||
|
- Command exceeds timeout → TIMEOUT
|
||||||
|
4. **Record outcome**:
|
||||||
|
- **Success**: status=`completed`, set `completed_at`, log `Completed [<task-id>] (commit <hash>)`, git commit
|
||||||
|
- **Failure**: increment `attempts`, append error to `error_log`. Verify `started_at_commit` exists via `git cat-file -t <hash>` — if missing, mark failed at max_attempts. Otherwise execute `git reset --hard <started_at_commit>` and `git clean -fd` to rollback ALL commits and remove untracked files. Execute `on_failure.cleanup` if defined. Log `ERROR [<task-id>] [<category>] <message>`. Set status=`failed` (Task Selection Algorithm pass 2 handles retries when attempts < max_attempts)
|
||||||
|
5. **Track**: Increment per-session task counter. If `max_tasks_per_session` reached, log STATS and STOP.
|
||||||
|
6. **Continue**: Immediately pick next task (zero idle time)
|
||||||
|
|
||||||
|
### Stopping Conditions
|
||||||
|
|
||||||
|
- All tasks `completed`
|
||||||
|
- All remaining tasks `failed` at max_attempts or blocked by failed dependencies
|
||||||
|
- `session_config.max_tasks_per_session` reached for this session
|
||||||
|
- `session_config.max_sessions` reached across all sessions
|
||||||
|
- User interrupts
|
||||||
|
|
||||||
|
## Context Window Recovery Protocol
|
||||||
|
|
||||||
|
When a new session starts and finds a task with `status: "in_progress"`:
|
||||||
|
|
||||||
|
1. **Check git state**:
|
||||||
|
```bash
|
||||||
|
git diff --stat # Uncommitted changes?
|
||||||
|
git log --oneline -5 # Recent commits since task started?
|
||||||
|
git stash list # Any stashed work?
|
||||||
|
```
|
||||||
|
2. **Check checkpoints**: Read the task's `checkpoints` array to determine last completed step
|
||||||
|
3. **Decision matrix** (verify recent commits belong to this task by checking commit messages for the task-id):
|
||||||
|
|
||||||
|
| Uncommitted? | Recent task commits? | Checkpoints? | Action |
|
||||||
|
|---|---|---|---|
|
||||||
|
| No | No | None | Mark `failed` with `[SESSION_TIMEOUT] No progress detected`, increment attempts |
|
||||||
|
| No | No | Some | Verify file state matches checkpoint claims. If files reflect checkpoint progress, resume from last step. If not, mark `failed` — work was lost |
|
||||||
|
| No | Yes | Any | Run `validation.command`. If passes → mark `completed`. If fails → `git reset --hard <started_at_commit>`, mark `failed` |
|
||||||
|
| Yes | No | Any | Run validation WITH uncommitted changes present. If passes → commit, mark `completed`. If fails → `git reset --hard <started_at_commit>` + `git clean -fd`, mark `failed` |
|
||||||
|
| Yes | Yes | Any | Commit uncommitted changes, run `validation.command`. If passes → mark `completed`. If fails → `git reset --hard <started_at_commit>` + `git clean -fd`, mark `failed` |
|
||||||
|
|
||||||
|
4. **Log recovery**: `[timestamp] [SESSION-N] RECOVERY [task-id] action="<action taken>" reason="<reason>"`
|
||||||
|
|
||||||
|
## Error Handling & Recovery Strategies
|
||||||
|
|
||||||
|
Each error category has a default recovery strategy:
|
||||||
|
|
||||||
|
| Category | Default Recovery | Agent Action |
|
||||||
|
|----------|-----------------|--------------|
|
||||||
|
| `ENV_SETUP` | Re-run init, then STOP if still failing | Run `harness-init.sh` again immediately. If fails twice, log and stop — environment is broken |
|
||||||
|
| `TASK_EXEC` | Rollback via `git reset --hard <started_at_commit>`, retry | Verify `started_at_commit` exists (`git cat-file -t <hash>`). If missing, mark failed at max_attempts. Otherwise reset, run `on_failure.cleanup` if defined, retry if attempts < max_attempts |
|
||||||
|
| `TEST_FAIL` | Rollback via `git reset --hard <started_at_commit>`, retry | Reset to `started_at_commit`, analyze test output to identify fix, retry with targeted changes |
|
||||||
|
| `TIMEOUT` | Kill process, execute cleanup, retry | Wrap validation with `timeout <seconds> <command>`. On timeout, run `on_failure.cleanup`, retry (consider splitting task if repeated) |
|
||||||
|
| `DEPENDENCY` | Skip task, mark blocked | Log which dependency failed, mark task as `failed` with dependency reason |
|
||||||
|
| `SESSION_TIMEOUT` | Use Context Window Recovery Protocol | New session assesses partial progress via Recovery Protocol — may result in completion or failure depending on validation |
|
||||||
|
|
||||||
|
**JSON corruption**: If `harness-tasks.json` cannot be parsed, check for `harness-tasks.json.bak` (written before each modification). If backup exists and is valid, restore from it. If no valid backup, log `ERROR [ENV_SETUP] harness-tasks.json corrupted and unrecoverable` and STOP — task metadata (validation commands, dependencies, cleanup) cannot be reconstructed from logs alone.
|
||||||
|
|
||||||
|
**Backup protocol**: Before every write to `harness-tasks.json`, copy the current file to `harness-tasks.json.bak`.
|
||||||
|
|
||||||
|
## Environment Initialization
|
||||||
|
|
||||||
|
If `harness-init.sh` exists in the project root, run it at every session start. The script must be idempotent.
|
||||||
|
|
||||||
|
Example `harness-init.sh`:
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
npm install 2>/dev/null || pip install -r requirements.txt 2>/dev/null || true
|
||||||
|
curl -sf http://localhost:5432 >/dev/null 2>&1 || echo "WARN: DB not reachable"
|
||||||
|
npm test -- --bail --silent 2>/dev/null || echo "WARN: Smoke test failed"
|
||||||
|
echo "Environment health check complete"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Standardized Log Format
|
||||||
|
|
||||||
|
All log entries use grep-friendly format on a single line:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ISO-timestamp] [SESSION-N] <TYPE> [task-id]? [category]? message
|
||||||
|
```
|
||||||
|
|
||||||
|
`[task-id]` and `[category]` are included when applicable (task-scoped entries). Session-level entries (`INIT`, `LOCK`, `STATS`) omit them.
|
||||||
|
|
||||||
|
Types: `INIT`, `Starting`, `Completed`, `ERROR`, `CHECKPOINT`, `ROLLBACK`, `RECOVERY`, `STATS`, `LOCK`, `WARN`
|
||||||
|
|
||||||
|
Error categories: `ENV_SETUP`, `TASK_EXEC`, `TEST_FAIL`, `TIMEOUT`, `DEPENDENCY`, `SESSION_TIMEOUT`
|
||||||
|
|
||||||
|
Filtering:
|
||||||
|
```bash
|
||||||
|
grep "ERROR" harness-progress.txt # All errors
|
||||||
|
grep "ERROR" harness-progress.txt | grep "TASK_EXEC" # Execution errors only
|
||||||
|
grep "SESSION-3" harness-progress.txt # All session 3 activity
|
||||||
|
grep "STATS" harness-progress.txt # All session summaries
|
||||||
|
grep "CHECKPOINT" harness-progress.txt # All checkpoints
|
||||||
|
grep "RECOVERY" harness-progress.txt # All recovery actions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Session Statistics
|
||||||
|
|
||||||
|
At session end, update `harness-tasks.json`: increment `session_count`, set `last_session` to current timestamp. Then append:
|
||||||
|
|
||||||
|
```
|
||||||
|
[timestamp] [SESSION-N] STATS tasks_total=10 completed=7 failed=1 pending=2 blocked=0 attempts_total=12 checkpoints=23
|
||||||
|
```
|
||||||
|
|
||||||
|
`blocked` is computed at stats time: count of pending tasks whose `depends_on` includes a permanently failed task. It is not a stored status value.
|
||||||
|
|
||||||
|
## Init Command (`/harness init`)
|
||||||
|
|
||||||
|
1. Create `harness-progress.txt` with initialization entry
|
||||||
|
2. Create `harness-tasks.json` with empty task list and default `session_config`
|
||||||
|
3. Optionally create `harness-init.sh` template (chmod +x)
|
||||||
|
4. Ask user: add harness files to `.gitignore`?
|
||||||
|
|
||||||
|
## Status Command (`/harness status`)
|
||||||
|
|
||||||
|
Read `harness-tasks.json` and `harness-progress.txt`, then display:
|
||||||
|
|
||||||
|
1. Task summary: count by status (completed, failed, pending, blocked). `blocked` = pending tasks whose `depends_on` includes a permanently failed task (computed, not a stored status).
|
||||||
|
2. Per-task one-liner: `[status] task-id: title (attempts/max_attempts)`
|
||||||
|
3. Last 5 lines from `harness-progress.txt`
|
||||||
|
4. Session count and last session timestamp
|
||||||
|
|
||||||
|
Does NOT acquire the lock (read-only operation).
|
||||||
|
|
||||||
|
## Add Command (`/harness add`)
|
||||||
|
|
||||||
|
Append a new task to `harness-tasks.json` with auto-incremented id (`task-NNN`), status `pending`, default `max_attempts: 3`, empty `depends_on`, and no validation command. Prompt user for optional fields: `priority`, `depends_on`, `validation.command`, `timeout_seconds`. Requires lock acquisition (modifies JSON).
|
||||||
|
|
||||||
|
## Tool Dependencies
|
||||||
|
|
||||||
|
Requires: Bash, file read/write, git. All harness operations must be executed from the project root directory.
|
||||||
|
Does NOT require: specific MCP servers, programming languages, or test frameworks.
|
||||||
52
templates/models.json.example
Normal file
52
templates/models.json.example
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"default_backend": "codex",
|
||||||
|
"default_model": "gpt-5.2",
|
||||||
|
"backends": {
|
||||||
|
"codex": { "api_key": "" },
|
||||||
|
"claude": { "api_key": "" },
|
||||||
|
"gemini": { "api_key": "" },
|
||||||
|
"opencode": { "api_key": "" }
|
||||||
|
},
|
||||||
|
"agents": {
|
||||||
|
"develop": {
|
||||||
|
"backend": "codex",
|
||||||
|
"model": "gpt-5.2",
|
||||||
|
"reasoning": "xhigh",
|
||||||
|
"yolo": true
|
||||||
|
},
|
||||||
|
"code-explorer": {
|
||||||
|
"backend": "opencode",
|
||||||
|
"model": ""
|
||||||
|
},
|
||||||
|
"code-architect": {
|
||||||
|
"backend": "claude",
|
||||||
|
"model": ""
|
||||||
|
},
|
||||||
|
"code-reviewer": {
|
||||||
|
"backend": "claude",
|
||||||
|
"model": ""
|
||||||
|
},
|
||||||
|
"oracle": {
|
||||||
|
"backend": "claude",
|
||||||
|
"model": "claude-opus-4-5-20251101",
|
||||||
|
"yolo": true
|
||||||
|
},
|
||||||
|
"librarian": {
|
||||||
|
"backend": "claude",
|
||||||
|
"model": "claude-sonnet-4-5-20250929",
|
||||||
|
"yolo": true
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
"backend": "opencode",
|
||||||
|
"model": "opencode/grok-code"
|
||||||
|
},
|
||||||
|
"frontend-ui-ux-engineer": {
|
||||||
|
"backend": "gemini",
|
||||||
|
"model": "gemini-3-pro-preview"
|
||||||
|
},
|
||||||
|
"document-writer": {
|
||||||
|
"backend": "gemini",
|
||||||
|
"model": "gemini-3-flash-preview"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user