Compare commits

...

6 Commits

Author SHA1 Message Date
cnzgray
33a94d2bc4 fix(executor): isolate CLAUDE_CODE_TMPDIR for nested claude to fix (no output) (#154)
* fix(executor): isolate CLAUDE_CODE_TMPDIR for nested claude to fix (no output)

Claude 2.1.45+ calls Nz7() in preAction to clean its tasks directory on
startup. When claude runs as a nested subprocess, it deletes the parent
session's *.output files, causing the parent to read an empty string and
display "(no output)".

Fix: assign each nested claude process its own unique CLAUDE_CODE_TMPDIR
(os.TempDir()/cc-nested-<pid>-<ns>) so it only cleans its own tasks
directory and never touches the parent's output files.

* fix(executor): use MkdirTemp for nested tmpdir

---------

Co-authored-by: cexll <evanxian9@gmail.com>
2026-02-27 22:15:19 +08:00
ben
b204ca94e2 fix(codeagent-wrapper): keep logs + surface parsed error output (#152)
Fixes #150
2026-02-27 10:05:58 +08:00
Lucas Men
a39bf72bc2 fix(do-skill): use absolute paths for script references in SKILL.md (#153)
* fix(do-skill): use absolute paths for script references in SKILL.md

Relative paths cause FileNotFoundError when running /do from any
project directory outside $HOME, since skills are installed at
~/.claude/skills/do/ not .claude/skills/do/ relative to project.

* fix(do-skill): fix home path expansion

---------

Co-authored-by: menft <17554333016@163.com>
Co-authored-by: cexll <evanxian9@gmail.com>
2026-02-27 10:03:11 +08:00
DanielLi
f43244ec3e feat(codeagent-wrapper): add --output structured JSON file (#151)
* feat(codeagent-wrapper): add --output structured JSON file

* fix(codeagent-wrapper): write --output on failure

---------

Co-authored-by: danielee.eth <danielee.eth@gmail.com>
Co-authored-by: cexll <evanxian9@gmail.com>
2026-02-27 00:46:24 +08:00
cexll
4c25dd8d2f chore: update repo owner and support email 2026-02-23 13:41:48 +08:00
cexll
19d411a6a2 fix installer bootstrap for do/omo/dev initialization 2026-02-22 18:19:29 +08:00
22 changed files with 786 additions and 138 deletions

View File

@@ -47,7 +47,7 @@ All notable changes to this project will be documented in this file.
### 🚀 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
@@ -138,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
@@ -159,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
@@ -184,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
@@ -205,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
@@ -218,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
@@ -231,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
@@ -256,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
@@ -315,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
@@ -339,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
@@ -347,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
@@ -367,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
@@ -375,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
@@ -404,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
@@ -430,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
@@ -452,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
@@ -469,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
@@ -480,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
@@ -508,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
@@ -522,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
@@ -609,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
@@ -627,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
@@ -684,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
@@ -701,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
@@ -751,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
@@ -795,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
@@ -840,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
@@ -892,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
@@ -905,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
@@ -957,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
@@ -1025,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
@@ -1062,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
@@ -1092,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
@@ -1108,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

View File

@@ -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

View File

@@ -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
``` ```

View File

@@ -2,17 +2,17 @@
# Claude Code Multi-Agent Workflow System # Claude Code Multi-Agent Workflow System
[![Run in Smithery](https://smithery.ai/badge/skills/cexll)](https://smithery.ai/skills?ns=cexll&utm_source=github&utm_medium=badge) [![Run in Smithery](https://smithery.ai/badge/skills/stellarlinkco)](https://smithery.ai/skills?ns=stellarlinkco&utm_source=github&utm_medium=badge)
[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![Claude Code](https://img.shields.io/badge/Claude-Code-blue)](https://claude.ai/code) [![Claude Code](https://img.shields.io/badge/Claude-Code-blue)](https://claude.ai/code)
[![Version](https://img.shields.io/badge/Version-6.x-green)](https://github.com/cexll/myclaude) [![Version](https://img.shields.io/badge/Version-6.x-green)](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
@@ -30,7 +30,7 @@ npx github:cexll/myclaude
### Available Skills ### Available Skills
Individual skills can be installed separately via `npx github:cexll/myclaude --list` (skills bundled in modules like do, omo, sparv are listed above): 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 | | Skill | Description |
|-------|-------------| |-------|-------------|
@@ -48,16 +48,16 @@ Individual skills can be installed separately via `npx github:cexll/myclaude --l
```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.
@@ -132,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:**
@@ -156,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
@@ -164,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)

View File

@@ -2,14 +2,14 @@
[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![Claude Code](https://img.shields.io/badge/Claude-Code-blue)](https://claude.ai/code) [![Claude Code](https://img.shields.io/badge/Claude-Code-blue)](https://claude.ai/code)
[![Version](https://img.shields.io/badge/Version-6.x-green)](https://github.com/cexll/myclaude) [![Version](https://img.shields.io/badge/Version-6.x-green)](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
``` ```
## 模块概览 ## 模块概览
@@ -27,7 +27,7 @@ npx github:cexll/myclaude
### 可用技能 ### 可用技能
可通过 `npx github:cexll/myclaude --list` 单独安装技能(模块内置技能如 do、omo、sparv 见上表): 可通过 `npx github:stellarlinkco/myclaude --list` 单独安装技能(模块内置技能如 do、omo、sparv 见上表):
| 技能 | 描述 | | 技能 | 描述 |
|------|------| |------|------|
@@ -188,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 拉取最新发布版本覆盖更新。
@@ -244,13 +244,13 @@ npx github:cexll/myclaude --install-dir ~/.claude --force
**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
@@ -261,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)。
## 许可证 ## 许可证
@@ -269,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)

View File

@@ -10,11 +10,13 @@ const readline = require("readline");
const zlib = require("zlib"); const zlib = require("zlib");
const { spawn, spawnSync } = 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;
} }
@@ -1023,20 +1103,37 @@ async function installSelected(picks, tag, config, installDir, force, 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 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),

View File

@@ -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 = [

View File

@@ -28,7 +28,7 @@ Entry point: `cmd/codeagent-wrapper/main.go` (binary: `codeagent-wrapper`).
### Recommended (interactive installer) ### Recommended (interactive installer)
```bash ```bash
npx github:cexll/myclaude npx github:stellarlinkco/myclaude
``` ```
Select the `codeagent-wrapper` module to install. Select the `codeagent-wrapper` module to install.

View File

@@ -28,7 +28,7 @@
### 推荐方式(交互式安装器) ### 推荐方式(交互式安装器)
```bash ```bash
npx github:cexll/myclaude npx github:stellarlinkco/myclaude
``` ```
选择 `codeagent-wrapper` 模块进行安装。 选择 `codeagent-wrapper` 模块进行安装。

View File

@@ -15,7 +15,7 @@ Multi-backend AI code execution wrapper supporting Codex, Claude, Gemini, and Op
```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

View File

@@ -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)

View File

@@ -1455,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")
@@ -3751,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 }
@@ -4342,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)
} }
} }
@@ -4360,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"}
@@ -4439,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)
} }
} }
@@ -4495,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")
} }
@@ -4514,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()))
@@ -4537,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)
@@ -4559,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)
} }
} }
@@ -4822,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"}

View 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
}

View File

@@ -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

View File

@@ -125,6 +125,9 @@ func TestEnvInjection_LogsToStderrAndMasksKey(t *testing.T) {
if cmd.env["ANTHROPIC_API_KEY"] != apiKey { if cmd.env["ANTHROPIC_API_KEY"] != apiKey {
t.Fatalf("ANTHROPIC_API_KEY=%q, want %q", cmd.env["ANTHROPIC_API_KEY"], apiKey) t.Fatalf("ANTHROPIC_API_KEY=%q, want %q", cmd.env["ANTHROPIC_API_KEY"], apiKey)
} }
if cmd.env["CLAUDE_CODE_TMPDIR"] == "" {
t.Fatalf("expected CLAUDE_CODE_TMPDIR to be set for nested claude, got empty")
}
if !strings.Contains(got, "Env: ANTHROPIC_BASE_URL="+baseURL) { if !strings.Contains(got, "Env: ANTHROPIC_BASE_URL="+baseURL) {
t.Fatalf("stderr missing base URL env log; stderr=%q", got) t.Fatalf("stderr missing base URL env log; stderr=%q", got)
@@ -132,4 +135,7 @@ func TestEnvInjection_LogsToStderrAndMasksKey(t *testing.T) {
if !strings.Contains(got, "Env: ANTHROPIC_API_KEY=eyJh****test") { if !strings.Contains(got, "Env: ANTHROPIC_API_KEY=eyJh****test") {
t.Fatalf("stderr missing masked API key log; stderr=%q", got) t.Fatalf("stderr missing masked API key log; stderr=%q", got)
} }
if !strings.Contains(got, "CLAUDE_CODE_TMPDIR: ") {
t.Fatalf("stderr missing CLAUDE_CODE_TMPDIR log; stderr=%q", got)
}
} }

View File

@@ -1154,10 +1154,23 @@ func RunCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
injectTempEnv(cmd) injectTempEnv(cmd)
if commandName == "claude" {
// Claude 2.1.45+ calls Nz7() on startup to clean its tasks directory,
// which deletes the parent session's *.output files and causes "(no output)".
// Assign each nested claude its own isolated tmpdir so it only cleans its own files.
nestedTmpDir, err := os.MkdirTemp("", fmt.Sprintf("cc-nested-%d-", os.Getpid()))
if err != nil {
logWarnFn("Failed to create isolated CLAUDE_CODE_TMPDIR: " + err.Error())
} else {
cmd.SetEnv(map[string]string{"CLAUDE_CODE_TMPDIR": nestedTmpDir})
defer os.RemoveAll(nestedTmpDir) //nolint:errcheck
logInfoFn("CLAUDE_CODE_TMPDIR: " + nestedTmpDir)
fmt.Fprintln(os.Stderr, " CLAUDE_CODE_TMPDIR: "+nestedTmpDir)
}
// Claude Code sets CLAUDECODE=1 in its child processes. If we don't // Claude Code sets CLAUDECODE=1 in its child processes. If we don't
// remove it, the spawned `claude -p` detects the variable and refuses // remove it, the spawned `claude -p` detects the variable and refuses
// to start ("cannot be launched inside another Claude Code session"). // to start ("cannot be launched inside another Claude Code session").
if commandName == "claude" {
cmd.UnsetEnv("CLAUDECODE") cmd.UnsetEnv("CLAUDECODE")
} }
@@ -1435,6 +1448,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())

View File

@@ -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,

View File

@@ -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"

View File

@@ -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]:
@@ -898,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:
@@ -1081,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
@@ -1156,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"])

View File

@@ -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

View File

@@ -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
``` ```

View File

@@ -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
@@ -13,7 +13,7 @@ An orchestrator for systematic feature development. Invoke agents via `codeagent
When triggered via `/do <task>`, initialize the task directory immediately without asking about worktree: When triggered via `/do <task>`, initialize the task directory immediately without asking about worktree:
```bash ```bash
python3 ".claude/skills/do/scripts/setup-do.py" "<task description>" python3 "$HOME/.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:
@@ -27,13 +27,13 @@ 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
@@ -42,7 +42,7 @@ The worktree is created **only when needed** (right before Phase 4: Implement).
1. Run setup with `--worktree` flag to create the worktree: 1. Run setup with `--worktree` flag to create the worktree:
```bash ```bash
python3 ".claude/skills/do/scripts/setup-do.py" --worktree "<task description>" 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. 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.
@@ -181,7 +181,7 @@ Develop in a separate worktree? (Isolates changes from main branch)
If user chooses worktree: If user chooses worktree:
```bash ```bash
python3 ".claude/skills/do/scripts/setup-do.py" --worktree "<task description>" python3 "$HOME/.claude/skills/do/scripts/setup-do.py" --worktree "<task description>"
# Save the worktree path from output for DO_WORKTREE_DIR # Save the worktree path from output for DO_WORKTREE_DIR
``` ```