mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-05 02:30:26 +08:00
Compare commits
3 Commits
v5.6.3
...
feat/intel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61536d04e2 | ||
|
|
2856bf0c29 | ||
|
|
19facf3385 |
@@ -1,47 +1,209 @@
|
||||
{
|
||||
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||
"name": "myclaude",
|
||||
"version": "5.6.1",
|
||||
"description": "Professional multi-agent development workflows with OmO orchestration, Requirements-Driven and BMAD methodologies",
|
||||
"name": "claude-code-dev-workflows",
|
||||
"owner": {
|
||||
"name": "cexll",
|
||||
"email": "evanxian9@gmail.com"
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"email": "contact@example.com",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Professional multi-agent development workflows with Requirements-Driven and BMAD methodologies, featuring 16+ specialized agents and 12+ commands",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "omo",
|
||||
"description": "Multi-agent orchestration for code analysis, bug investigation, fix planning, and implementation with intelligent routing to specialized agents",
|
||||
"version": "5.6.1",
|
||||
"source": "./skills/omo",
|
||||
"category": "development"
|
||||
"name": "requirements-driven-development",
|
||||
"source": "./requirements-driven-workflow/",
|
||||
"description": "Streamlined requirements-driven development workflow with 90% quality gates for practical feature implementation",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"requirements",
|
||||
"workflow",
|
||||
"automation",
|
||||
"quality-gates",
|
||||
"feature-development",
|
||||
"agile",
|
||||
"specifications"
|
||||
],
|
||||
"category": "workflows",
|
||||
"strict": false,
|
||||
"commands": [
|
||||
"./commands/requirements-pilot.md"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/requirements-generate.md",
|
||||
"./agents/requirements-code.md",
|
||||
"./agents/requirements-testing.md",
|
||||
"./agents/requirements-review.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dev",
|
||||
"description": "Lightweight development workflow with requirements clarification, parallel codex execution, and mandatory 90% test coverage",
|
||||
"version": "5.6.1",
|
||||
"source": "./dev-workflow",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "requirements",
|
||||
"description": "Requirements-driven development workflow with quality gates for practical feature implementation",
|
||||
"version": "5.6.1",
|
||||
"source": "./requirements-driven-workflow",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "bmad",
|
||||
"name": "bmad-agile-workflow",
|
||||
"source": "./bmad-agile-workflow/",
|
||||
"description": "Full BMAD agile workflow with role-based agents (PO, Architect, SM, Dev, QA) and interactive approval gates",
|
||||
"version": "5.6.1",
|
||||
"source": "./bmad-agile-workflow",
|
||||
"category": "development"
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"bmad",
|
||||
"agile",
|
||||
"scrum",
|
||||
"product-owner",
|
||||
"architect",
|
||||
"developer",
|
||||
"qa",
|
||||
"workflow-orchestration"
|
||||
],
|
||||
"category": "workflows",
|
||||
"strict": false,
|
||||
"commands": [
|
||||
"./commands/bmad-pilot.md"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/bmad-po.md",
|
||||
"./agents/bmad-architect.md",
|
||||
"./agents/bmad-sm.md",
|
||||
"./agents/bmad-dev.md",
|
||||
"./agents/bmad-qa.md",
|
||||
"./agents/bmad-orchestrator.md",
|
||||
"./agents/bmad-review.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dev-kit",
|
||||
"name": "development-essentials",
|
||||
"source": "./development-essentials/",
|
||||
"description": "Essential development commands for coding, debugging, testing, optimization, and documentation",
|
||||
"version": "5.6.1",
|
||||
"source": "./development-essentials",
|
||||
"category": "productivity"
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"code",
|
||||
"debug",
|
||||
"test",
|
||||
"optimize",
|
||||
"review",
|
||||
"bugfix",
|
||||
"refactor",
|
||||
"documentation"
|
||||
],
|
||||
"category": "essentials",
|
||||
"strict": false,
|
||||
"commands": [
|
||||
"./commands/code.md",
|
||||
"./commands/debug.md",
|
||||
"./commands/test.md",
|
||||
"./commands/optimize.md",
|
||||
"./commands/review.md",
|
||||
"./commands/bugfix.md",
|
||||
"./commands/refactor.md",
|
||||
"./commands/docs.md",
|
||||
"./commands/ask.md",
|
||||
"./commands/think.md"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/code.md",
|
||||
"./agents/bugfix.md",
|
||||
"./agents/bugfix-verify.md",
|
||||
"./agents/optimize.md",
|
||||
"./agents/debug.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "codex-cli",
|
||||
"source": "./skills/codex/",
|
||||
"description": "Execute Codex CLI for code analysis, refactoring, and automated code changes with file references (@syntax) and structured output",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"codex",
|
||||
"code-analysis",
|
||||
"refactoring",
|
||||
"automation",
|
||||
"gpt-5",
|
||||
"ai-coding"
|
||||
],
|
||||
"category": "essentials",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./SKILL.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "gemini-cli",
|
||||
"source": "./skills/gemini/",
|
||||
"description": "Execute Gemini CLI for AI-powered code analysis and generation with Google's latest Gemini models",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"gemini",
|
||||
"google-ai",
|
||||
"code-analysis",
|
||||
"code-generation",
|
||||
"ai-reasoning"
|
||||
],
|
||||
"category": "essentials",
|
||||
"strict": false,
|
||||
"skills": [
|
||||
"./SKILL.md"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dev-workflow",
|
||||
"source": "./dev-workflow/",
|
||||
"description": "Minimal lightweight development workflow with requirements clarification, parallel codex execution, and mandatory 90% test coverage",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"dev",
|
||||
"workflow",
|
||||
"codex",
|
||||
"testing",
|
||||
"coverage",
|
||||
"concurrent",
|
||||
"lightweight"
|
||||
],
|
||||
"category": "workflows",
|
||||
"strict": false,
|
||||
"commands": [
|
||||
"./commands/dev.md"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/dev-plan-generator.md"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
144
README.md
144
README.md
@@ -35,41 +35,6 @@ python3 install.py --install-dir ~/.claude
|
||||
|
||||
## Workflows Overview
|
||||
|
||||
### 0. OmO Multi-Agent Orchestrator (Recommended for Complex Tasks)
|
||||
|
||||
**Intelligent multi-agent orchestration that routes tasks to specialized agents based on risk signals.**
|
||||
|
||||
```bash
|
||||
/omo "analyze and fix this authentication bug"
|
||||
```
|
||||
|
||||
**Agent Hierarchy:**
|
||||
| Agent | Role | Backend | Model |
|
||||
|-------|------|---------|-------|
|
||||
| `oracle` | Technical advisor | Claude | claude-opus-4-5 |
|
||||
| `librarian` | External research | Claude | claude-sonnet-4-5 |
|
||||
| `explore` | Codebase search | OpenCode | grok-code |
|
||||
| `develop` | Code implementation | Codex | gpt-5.2 |
|
||||
| `frontend-ui-ux-engineer` | UI/UX specialist | Gemini | gemini-3-pro |
|
||||
| `document-writer` | Documentation | Gemini | gemini-3-flash |
|
||||
|
||||
**Routing Signals (Not Fixed Pipeline):**
|
||||
- Code location unclear → `explore`
|
||||
- External library/API → `librarian`
|
||||
- Risky/multi-file change → `oracle`
|
||||
- Implementation needed → `develop` / `frontend-ui-ux-engineer`
|
||||
|
||||
**Common Recipes:**
|
||||
- Explain code: `explore`
|
||||
- Small fix with known location: `develop` directly
|
||||
- Bug fix, location unknown: `explore → develop`
|
||||
- Cross-cutting refactor: `explore → oracle → develop`
|
||||
- External API integration: `explore + librarian → oracle → develop`
|
||||
|
||||
**Best For:** Complex bug investigation, multi-file refactoring, architecture decisions
|
||||
|
||||
---
|
||||
|
||||
### 1. Dev Workflow (Recommended)
|
||||
|
||||
**The primary workflow for most development tasks.**
|
||||
@@ -195,7 +160,7 @@ Required features:
|
||||
- `-p` - Prompt input flag
|
||||
- `-r <session_id>` - Resume sessions
|
||||
|
||||
**Security Note:** The wrapper adds `--dangerously-skip-permissions` for Claude by default. Set `CODEAGENT_SKIP_PERMISSIONS=false` to disable if you need permission prompts.
|
||||
**Security Note:** The wrapper only adds `--dangerously-skip-permissions` for Claude when explicitly enabled (e.g. `--skip-permissions` / `CODEAGENT_SKIP_PERMISSIONS=true`). Keep it disabled unless you understand the risk.
|
||||
|
||||
**Verify Claude CLI is installed:**
|
||||
```bash
|
||||
@@ -381,10 +346,8 @@ $Env:PATH = "$HOME\bin;$Env:PATH"
|
||||
```
|
||||
|
||||
```batch
|
||||
REM cmd.exe - persistent for current user (use PowerShell method above instead)
|
||||
REM WARNING: This expands %PATH% which includes system PATH, causing duplication
|
||||
REM Note: Using reg add instead of setx to avoid 1024-character truncation limit
|
||||
reg add "HKCU\Environment" /v Path /t REG_EXPAND_SZ /d "%USERPROFILE%\bin;%PATH%" /f
|
||||
REM cmd.exe - persistent for current user
|
||||
setx PATH "%USERPROFILE%\bin;%PATH%"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -499,106 +462,9 @@ claude -r <session_id> "test"
|
||||
|
||||
---
|
||||
|
||||
## FAQ (Frequently Asked Questions)
|
||||
|
||||
### Q1: `codeagent-wrapper` execution fails with "Unknown event format"
|
||||
|
||||
**Problem:**
|
||||
```
|
||||
Unknown event format: {"type":"turn.started"}
|
||||
Unknown event format: {"type":"assistant", ...}
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
This is a logging event format display issue and does not affect actual functionality. It will be fixed in the next version. You can ignore these log outputs.
|
||||
|
||||
**Related Issue:** [#96](https://github.com/cexll/myclaude/issues/96)
|
||||
|
||||
---
|
||||
|
||||
### Q2: Gemini cannot read files ignored by `.gitignore`
|
||||
|
||||
**Problem:**
|
||||
When using `codeagent-wrapper --backend gemini`, files in directories like `.claude/` that are ignored by `.gitignore` cannot be read.
|
||||
|
||||
**Solution:**
|
||||
- **Option 1:** Remove `.claude/` from your `.gitignore` file
|
||||
- **Option 2:** Ensure files that need to be read are not in `.gitignore` list
|
||||
|
||||
**Related Issue:** [#75](https://github.com/cexll/myclaude/issues/75)
|
||||
|
||||
---
|
||||
|
||||
### Q3: `/dev` command parallel execution is very slow
|
||||
|
||||
**Problem:**
|
||||
Using `/dev` command for simple features takes too long (over 30 minutes) with no visibility into task progress.
|
||||
|
||||
**Solution:**
|
||||
1. **Check logs:** Review `C:\Users\User\AppData\Local\Temp\codeagent-wrapper-*.log` to identify bottlenecks
|
||||
2. **Adjust backend:**
|
||||
- Try faster models like `gpt-5.1-codex-max`
|
||||
- Running in WSL may be significantly faster
|
||||
3. **Workspace:** Use a single repository instead of monorepo with multiple sub-projects
|
||||
|
||||
**Related Issue:** [#77](https://github.com/cexll/myclaude/issues/77)
|
||||
|
||||
---
|
||||
|
||||
### Q4: Codex permission denied with new Go version
|
||||
|
||||
**Problem:**
|
||||
After upgrading to the new Go-based Codex implementation, execution fails with permission denied errors.
|
||||
|
||||
**Solution:**
|
||||
Add the following configuration to `~/.codex/config.yaml` (Windows: `c:\user\.codex\config.toml`):
|
||||
```yaml
|
||||
model = "gpt-5.1-codex-max"
|
||||
model_reasoning_effort = "high"
|
||||
model_reasoning_summary = "detailed"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "workspace-write"
|
||||
disable_response_storage = true
|
||||
network_access = true
|
||||
```
|
||||
|
||||
**Key settings:**
|
||||
- `approval_policy = "never"` - Remove approval restrictions
|
||||
- `sandbox_mode = "workspace-write"` - Allow workspace write access
|
||||
- `network_access = true` - Enable network access
|
||||
|
||||
**Related Issue:** [#31](https://github.com/cexll/myclaude/issues/31)
|
||||
|
||||
---
|
||||
|
||||
### Q5: How to disable default bypass/skip-permissions mode
|
||||
|
||||
**Background:**
|
||||
By default, codeagent-wrapper enables bypass mode for both Codex and Claude backends:
|
||||
- `CODEX_BYPASS_SANDBOX=true` - Bypasses Codex sandbox restrictions
|
||||
- `CODEAGENT_SKIP_PERMISSIONS=true` - Skips Claude permission prompts
|
||||
|
||||
**To disable (if you need sandbox/permission protection):**
|
||||
```bash
|
||||
export CODEX_BYPASS_SANDBOX=false
|
||||
export CODEAGENT_SKIP_PERMISSIONS=false
|
||||
```
|
||||
|
||||
Or add to your shell profile (`~/.zshrc` or `~/.bashrc`):
|
||||
```bash
|
||||
echo 'export CODEX_BYPASS_SANDBOX=false' >> ~/.zshrc
|
||||
echo 'export CODEAGENT_SKIP_PERMISSIONS=false' >> ~/.zshrc
|
||||
```
|
||||
|
||||
**Note:** Disabling bypass mode will require manual approval for certain operations.
|
||||
|
||||
---
|
||||
|
||||
**Still having issues?** Visit [GitHub Issues](https://github.com/cexll/myclaude/issues) to search or report new issues.
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
### Core Guides
|
||||
- **[Codeagent-Wrapper Guide](docs/CODEAGENT-WRAPPER.md)** - Multi-backend execution wrapper
|
||||
- **[Hooks Documentation](docs/HOOKS.md)** - Custom hooks and automation
|
||||
|
||||
|
||||
140
README_CN.md
140
README_CN.md
@@ -30,41 +30,6 @@ python3 install.py --install-dir ~/.claude
|
||||
|
||||
## 工作流概览
|
||||
|
||||
### 0. OmO 多智能体编排器(复杂任务推荐)
|
||||
|
||||
**基于风险信号智能路由任务到专业智能体的多智能体编排系统。**
|
||||
|
||||
```bash
|
||||
/omo "分析并修复这个认证 bug"
|
||||
```
|
||||
|
||||
**智能体层级:**
|
||||
| 智能体 | 角色 | 后端 | 模型 |
|
||||
|-------|------|------|------|
|
||||
| `oracle` | 技术顾问 | Claude | claude-opus-4-5 |
|
||||
| `librarian` | 外部研究 | Claude | claude-sonnet-4-5 |
|
||||
| `explore` | 代码库搜索 | OpenCode | grok-code |
|
||||
| `develop` | 代码实现 | Codex | gpt-5.2 |
|
||||
| `frontend-ui-ux-engineer` | UI/UX 专家 | Gemini | gemini-3-pro |
|
||||
| `document-writer` | 文档撰写 | Gemini | gemini-3-flash |
|
||||
|
||||
**路由信号(非固定流水线):**
|
||||
- 代码位置不明确 → `explore`
|
||||
- 外部库/API → `librarian`
|
||||
- 高风险/多文件变更 → `oracle`
|
||||
- 需要实现 → `develop` / `frontend-ui-ux-engineer`
|
||||
|
||||
**常用配方:**
|
||||
- 解释代码:`explore`
|
||||
- 位置已知的小修复:直接 `develop`
|
||||
- Bug 修复,位置未知:`explore → develop`
|
||||
- 跨模块重构:`explore → oracle → develop`
|
||||
- 外部 API 集成:`explore + librarian → oracle → develop`
|
||||
|
||||
**适用场景:** 复杂 bug 调查、多文件重构、架构决策
|
||||
|
||||
---
|
||||
|
||||
### 1. Dev 工作流(推荐)
|
||||
|
||||
**大多数开发任务的首选工作流。**
|
||||
@@ -317,10 +282,8 @@ $Env:PATH = "$HOME\bin;$Env:PATH"
|
||||
```
|
||||
|
||||
```batch
|
||||
REM cmd.exe - 永久添加(当前用户)(建议使用上面的 PowerShell 方法)
|
||||
REM 警告:此命令会展开 %PATH% 包含系统 PATH,导致重复
|
||||
REM 注意:使用 reg add 而非 setx 以避免 1024 字符截断限制
|
||||
reg add "HKCU\Environment" /v Path /t REG_EXPAND_SZ /d "%USERPROFILE%\bin;%PATH%" /f
|
||||
REM cmd.exe - 永久添加(当前用户)
|
||||
setx PATH "%USERPROFILE%\bin;%PATH%"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -370,105 +333,6 @@ python3 install.py --module dev --force
|
||||
|
||||
---
|
||||
|
||||
## 常见问题 (FAQ)
|
||||
|
||||
### Q1: `codeagent-wrapper` 执行时报错 "Unknown event format"
|
||||
|
||||
**问题描述:**
|
||||
执行 `codeagent-wrapper` 时出现错误:
|
||||
```
|
||||
Unknown event format: {"type":"turn.started"}
|
||||
Unknown event format: {"type":"assistant", ...}
|
||||
```
|
||||
|
||||
**解决方案:**
|
||||
这是日志事件流的显示问题,不影响实际功能执行。预计在下个版本中修复。如需排查其他问题,可忽略此日志输出。
|
||||
|
||||
**相关 Issue:** [#96](https://github.com/cexll/myclaude/issues/96)
|
||||
|
||||
---
|
||||
|
||||
### Q2: Gemini 无法读取 `.gitignore` 忽略的文件
|
||||
|
||||
**问题描述:**
|
||||
使用 `codeagent-wrapper --backend gemini` 时,无法读取 `.claude/` 等被 `.gitignore` 忽略的目录中的文件。
|
||||
|
||||
**解决方案:**
|
||||
- **方案一:** 在项目根目录的 `.gitignore` 中取消对 `.claude/` 的忽略
|
||||
- **方案二:** 确保需要读取的文件不在 `.gitignore` 忽略列表中
|
||||
|
||||
**相关 Issue:** [#75](https://github.com/cexll/myclaude/issues/75)
|
||||
|
||||
---
|
||||
|
||||
### Q3: `/dev` 命令并行执行特别慢
|
||||
|
||||
**问题描述:**
|
||||
使用 `/dev` 命令开发简单功能耗时过长(超过30分钟),无法了解任务执行状态。
|
||||
|
||||
**解决方案:**
|
||||
1. **检查日志:** 查看 `C:\Users\User\AppData\Local\Temp\codeagent-wrapper-*.log` 分析瓶颈
|
||||
2. **调整后端:**
|
||||
- 尝试使用 `gpt-5.1-codex-max` 等更快的模型
|
||||
- 在 WSL 环境下运行速度可能更快
|
||||
3. **工作区选择:** 使用独立的代码仓库而非包含多个子项目的 monorepo
|
||||
|
||||
**相关 Issue:** [#77](https://github.com/cexll/myclaude/issues/77)
|
||||
|
||||
---
|
||||
|
||||
### Q4: 新版 Go 实现的 Codex 权限不足
|
||||
|
||||
**问题描述:**
|
||||
升级到新版 Go 实现的 Codex 后,出现权限不足的错误。
|
||||
|
||||
**解决方案:**
|
||||
在 `~/.codex/config.yaml` 中添加以下配置(Windows: `c:\user\.codex\config.toml`):
|
||||
```yaml
|
||||
model = "gpt-5.1-codex-max"
|
||||
model_reasoning_effort = "high"
|
||||
model_reasoning_summary = "detailed"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "workspace-write"
|
||||
disable_response_storage = true
|
||||
network_access = true
|
||||
```
|
||||
|
||||
**关键配置说明:**
|
||||
- `approval_policy = "never"` - 移除审批限制
|
||||
- `sandbox_mode = "workspace-write"` - 允许工作区写入权限
|
||||
- `network_access = true` - 启用网络访问
|
||||
|
||||
**相关 Issue:** [#31](https://github.com/cexll/myclaude/issues/31)
|
||||
|
||||
---
|
||||
|
||||
### Q5: 执行时遇到权限拒绝或沙箱限制
|
||||
|
||||
**问题描述:**
|
||||
运行 codeagent-wrapper 时出现权限错误或沙箱限制。
|
||||
|
||||
**解决方案:**
|
||||
设置以下环境变量:
|
||||
```bash
|
||||
export CODEX_BYPASS_SANDBOX=true
|
||||
export CODEAGENT_SKIP_PERMISSIONS=true
|
||||
```
|
||||
|
||||
或添加到 shell 配置文件(`~/.zshrc` 或 `~/.bashrc`):
|
||||
```bash
|
||||
echo 'export CODEX_BYPASS_SANDBOX=true' >> ~/.zshrc
|
||||
echo 'export CODEAGENT_SKIP_PERMISSIONS=true' >> ~/.zshrc
|
||||
```
|
||||
|
||||
**注意:** 这些设置会绕过安全限制,请仅在可信环境中使用。
|
||||
|
||||
---
|
||||
|
||||
**仍有疑问?** 请访问 [GitHub Issues](https://github.com/cexll/myclaude/issues) 搜索或提交新问题。
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
AGPL-3.0 License - 查看 [LICENSE](LICENSE)
|
||||
|
||||
37
bmad-agile-workflow/.claude-plugin/marketplace.json
Normal file
37
bmad-agile-workflow/.claude-plugin/marketplace.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "bmad-agile-workflow",
|
||||
"source": "./",
|
||||
"description": "Full BMAD agile workflow with role-based agents (PO, Architect, SM, Dev, QA) and interactive approval gates",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"bmad",
|
||||
"agile",
|
||||
"scrum",
|
||||
"product-owner",
|
||||
"architect",
|
||||
"developer",
|
||||
"qa",
|
||||
"workflow-orchestration"
|
||||
],
|
||||
"category": "workflows",
|
||||
"strict": false,
|
||||
"commands": [
|
||||
"./commands/bmad-pilot.md"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/bmad-po.md",
|
||||
"./agents/bmad-architect.md",
|
||||
"./agents/bmad-sm.md",
|
||||
"./agents/bmad-dev.md",
|
||||
"./agents/bmad-qa.md",
|
||||
"./agents/bmad-orchestrator.md",
|
||||
"./agents/bmad-review.md"
|
||||
]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "bmad",
|
||||
"description": "Full BMAD agile workflow with role-based agents (PO, Architect, SM, Dev, QA) and interactive approval gates",
|
||||
"version": "5.6.1",
|
||||
"author": {
|
||||
"name": "cexll",
|
||||
"email": "cexll@cexll.com"
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type AgentModelConfig struct {
|
||||
Backend string `json:"backend"`
|
||||
Model string `json:"model"`
|
||||
PromptFile string `json:"prompt_file,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Yolo bool `json:"yolo,omitempty"`
|
||||
Reasoning string `json:"reasoning,omitempty"`
|
||||
}
|
||||
|
||||
type ModelsConfig struct {
|
||||
DefaultBackend string `json:"default_backend"`
|
||||
DefaultModel string `json:"default_model"`
|
||||
Agents map[string]AgentModelConfig `json:"agents"`
|
||||
}
|
||||
|
||||
var defaultModelsConfig = ModelsConfig{
|
||||
DefaultBackend: "opencode",
|
||||
DefaultModel: "opencode/grok-code",
|
||||
Agents: map[string]AgentModelConfig{
|
||||
"oracle": {Backend: "claude", Model: "claude-sonnet-4-20250514", PromptFile: "~/.claude/skills/omo/references/oracle.md", Description: "Technical advisor"},
|
||||
"librarian": {Backend: "claude", Model: "claude-sonnet-4-5-20250514", PromptFile: "~/.claude/skills/omo/references/librarian.md", Description: "Researcher"},
|
||||
"explore": {Backend: "opencode", Model: "opencode/grok-code", PromptFile: "~/.claude/skills/omo/references/explore.md", Description: "Code search"},
|
||||
"develop": {Backend: "codex", Model: "", PromptFile: "~/.claude/skills/omo/references/develop.md", Description: "Code development"},
|
||||
"frontend-ui-ux-engineer": {Backend: "gemini", Model: "", PromptFile: "~/.claude/skills/omo/references/frontend-ui-ux-engineer.md", Description: "Frontend engineer"},
|
||||
"document-writer": {Backend: "gemini", Model: "", PromptFile: "~/.claude/skills/omo/references/document-writer.md", Description: "Documentation"},
|
||||
},
|
||||
}
|
||||
|
||||
func loadModelsConfig() *ModelsConfig {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
logWarn(fmt.Sprintf("Failed to resolve home directory for models config: %v; using defaults", err))
|
||||
return &defaultModelsConfig
|
||||
}
|
||||
|
||||
configPath := filepath.Join(home, ".codeagent", "models.json")
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
logWarn(fmt.Sprintf("Failed to read models config %s: %v; using defaults", configPath, err))
|
||||
}
|
||||
return &defaultModelsConfig
|
||||
}
|
||||
|
||||
var cfg ModelsConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
logWarn(fmt.Sprintf("Failed to parse models config %s: %v; using defaults", configPath, err))
|
||||
return &defaultModelsConfig
|
||||
}
|
||||
|
||||
// Merge with defaults
|
||||
for name, agent := range defaultModelsConfig.Agents {
|
||||
if _, exists := cfg.Agents[name]; !exists {
|
||||
if cfg.Agents == nil {
|
||||
cfg.Agents = make(map[string]AgentModelConfig)
|
||||
}
|
||||
cfg.Agents[name] = agent
|
||||
}
|
||||
}
|
||||
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func resolveAgentConfig(agentName string) (backend, model, promptFile, reasoning string, yolo bool) {
|
||||
cfg := loadModelsConfig()
|
||||
if agent, ok := cfg.Agents[agentName]; ok {
|
||||
return agent.Backend, agent.Model, agent.PromptFile, agent.Reasoning, agent.Yolo
|
||||
}
|
||||
return cfg.DefaultBackend, cfg.DefaultModel, "", "", false
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveAgentConfig_Defaults(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
// Test that default agents resolve correctly without config file
|
||||
tests := []struct {
|
||||
agent string
|
||||
wantBackend string
|
||||
wantModel string
|
||||
wantPromptFile string
|
||||
}{
|
||||
{"oracle", "claude", "claude-sonnet-4-20250514", "~/.claude/skills/omo/references/oracle.md"},
|
||||
{"librarian", "claude", "claude-sonnet-4-5-20250514", "~/.claude/skills/omo/references/librarian.md"},
|
||||
{"explore", "opencode", "opencode/grok-code", "~/.claude/skills/omo/references/explore.md"},
|
||||
{"frontend-ui-ux-engineer", "gemini", "", "~/.claude/skills/omo/references/frontend-ui-ux-engineer.md"},
|
||||
{"document-writer", "gemini", "", "~/.claude/skills/omo/references/document-writer.md"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.agent, func(t *testing.T) {
|
||||
backend, model, promptFile, _, _ := resolveAgentConfig(tt.agent)
|
||||
if backend != tt.wantBackend {
|
||||
t.Errorf("backend = %q, want %q", backend, tt.wantBackend)
|
||||
}
|
||||
if model != tt.wantModel {
|
||||
t.Errorf("model = %q, want %q", model, tt.wantModel)
|
||||
}
|
||||
if promptFile != tt.wantPromptFile {
|
||||
t.Errorf("promptFile = %q, want %q", promptFile, tt.wantPromptFile)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveAgentConfig_UnknownAgent(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
backend, model, promptFile, _, _ := resolveAgentConfig("unknown-agent")
|
||||
if backend != "opencode" {
|
||||
t.Errorf("unknown agent backend = %q, want %q", backend, "opencode")
|
||||
}
|
||||
if model != "opencode/grok-code" {
|
||||
t.Errorf("unknown agent model = %q, want %q", model, "opencode/grok-code")
|
||||
}
|
||||
if promptFile != "" {
|
||||
t.Errorf("unknown agent promptFile = %q, want empty", promptFile)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadModelsConfig_NoFile(t *testing.T) {
|
||||
home := "/nonexistent/path/that/does/not/exist"
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
cfg := loadModelsConfig()
|
||||
if cfg.DefaultBackend != "opencode" {
|
||||
t.Errorf("DefaultBackend = %q, want %q", cfg.DefaultBackend, "opencode")
|
||||
}
|
||||
if len(cfg.Agents) != 6 {
|
||||
t.Errorf("len(Agents) = %d, want 6", len(cfg.Agents))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadModelsConfig_WithFile(t *testing.T) {
|
||||
// Create temp dir and config file
|
||||
tmpDir := t.TempDir()
|
||||
configDir := filepath.Join(tmpDir, ".codeagent")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
configContent := `{
|
||||
"default_backend": "claude",
|
||||
"default_model": "claude-opus-4",
|
||||
"agents": {
|
||||
"custom-agent": {
|
||||
"backend": "codex",
|
||||
"model": "gpt-4o",
|
||||
"description": "Custom agent"
|
||||
}
|
||||
}
|
||||
}`
|
||||
configPath := filepath.Join(configDir, "models.json")
|
||||
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Setenv("HOME", tmpDir)
|
||||
t.Setenv("USERPROFILE", tmpDir)
|
||||
|
||||
cfg := loadModelsConfig()
|
||||
|
||||
if cfg.DefaultBackend != "claude" {
|
||||
t.Errorf("DefaultBackend = %q, want %q", cfg.DefaultBackend, "claude")
|
||||
}
|
||||
if cfg.DefaultModel != "claude-opus-4" {
|
||||
t.Errorf("DefaultModel = %q, want %q", cfg.DefaultModel, "claude-opus-4")
|
||||
}
|
||||
|
||||
// Check custom agent
|
||||
if agent, ok := cfg.Agents["custom-agent"]; !ok {
|
||||
t.Error("custom-agent not found")
|
||||
} else {
|
||||
if agent.Backend != "codex" {
|
||||
t.Errorf("custom-agent.Backend = %q, want %q", agent.Backend, "codex")
|
||||
}
|
||||
if agent.Model != "gpt-4o" {
|
||||
t.Errorf("custom-agent.Model = %q, want %q", agent.Model, "gpt-4o")
|
||||
}
|
||||
}
|
||||
|
||||
// Check that defaults are merged
|
||||
if _, ok := cfg.Agents["oracle"]; !ok {
|
||||
t.Error("default agent oracle should be merged")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadModelsConfig_InvalidJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configDir := filepath.Join(tmpDir, ".codeagent")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write invalid JSON
|
||||
configPath := filepath.Join(configDir, "models.json")
|
||||
if err := os.WriteFile(configPath, []byte("invalid json {"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Setenv("HOME", tmpDir)
|
||||
t.Setenv("USERPROFILE", tmpDir)
|
||||
|
||||
cfg := loadModelsConfig()
|
||||
// Should fall back to defaults
|
||||
if cfg.DefaultBackend != "opencode" {
|
||||
t.Errorf("invalid JSON should fallback, got DefaultBackend = %q", cfg.DefaultBackend)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpencodeBackend_BuildArgs(t *testing.T) {
|
||||
backend := OpencodeBackend{}
|
||||
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "new"}
|
||||
got := backend.BuildArgs(cfg, "hello")
|
||||
want := []string{"run", "--format", "json", "hello"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with model", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "new", Model: "opencode/grok-code"}
|
||||
got := backend.BuildArgs(cfg, "task")
|
||||
want := []string{"run", "-m", "opencode/grok-code", "--format", "json", "task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resume mode", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "resume", SessionID: "ses_123", Model: "opencode/grok-code"}
|
||||
got := backend.BuildArgs(cfg, "follow-up")
|
||||
want := []string{"run", "-m", "opencode/grok-code", "-s", "ses_123", "--format", "json", "follow-up"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resume without session", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "resume"}
|
||||
got := backend.BuildArgs(cfg, "task")
|
||||
want := []string{"run", "--format", "json", "task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpencodeBackend_Interface(t *testing.T) {
|
||||
backend := OpencodeBackend{}
|
||||
|
||||
if backend.Name() != "opencode" {
|
||||
t.Errorf("Name() = %q, want %q", backend.Name(), "opencode")
|
||||
}
|
||||
if backend.Command() != "opencode" {
|
||||
t.Errorf("Command() = %q, want %q", backend.Command(), "opencode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendRegistry_IncludesOpencode(t *testing.T) {
|
||||
if _, ok := backendRegistry["opencode"]; !ok {
|
||||
t.Error("backendRegistry should include opencode")
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestValidateAgentName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "simple", input: "develop", wantErr: false},
|
||||
{name: "upper", input: "ABC", wantErr: false},
|
||||
{name: "digits", input: "a1", wantErr: false},
|
||||
{name: "dash underscore", input: "a-b_c", wantErr: false},
|
||||
{name: "empty", input: "", wantErr: true},
|
||||
{name: "space", input: "a b", wantErr: true},
|
||||
{name: "slash", input: "a/b", wantErr: true},
|
||||
{name: "dotdot", input: "../evil", wantErr: true},
|
||||
{name: "unicode", input: "中文", wantErr: true},
|
||||
{name: "symbol", input: "a$b", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateAgentName(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("validateAgentName(%q) err=%v, wantErr=%v", tt.input, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArgs_InvalidAgentNameRejected(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
os.Args = []string{"codeagent-wrapper", "--agent", "../evil", "task"}
|
||||
if _, err := parseArgs(); err == nil {
|
||||
t.Fatalf("expected parseArgs to reject invalid agent name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseParallelConfig_InvalidAgentNameRejected(t *testing.T) {
|
||||
input := `---TASK---
|
||||
id: task-1
|
||||
agent: ../evil
|
||||
---CONTENT---
|
||||
do something`
|
||||
if _, err := parseParallelConfig([]byte(input)); err == nil {
|
||||
t.Fatalf("expected parseParallelConfig to reject invalid agent name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseParallelConfig_ResolvesAgentPromptFile(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
configDir := filepath.Join(home, ".codeagent")
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(configDir, "models.json"), []byte(`{
|
||||
"default_backend": "codex",
|
||||
"default_model": "gpt-test",
|
||||
"agents": {
|
||||
"custom-agent": {
|
||||
"backend": "codex",
|
||||
"model": "gpt-test",
|
||||
"prompt_file": "~/.claude/prompt.md"
|
||||
}
|
||||
}
|
||||
}`), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
input := `---TASK---
|
||||
id: task-1
|
||||
agent: custom-agent
|
||||
---CONTENT---
|
||||
do something`
|
||||
cfg, err := parseParallelConfig([]byte(input))
|
||||
if err != nil {
|
||||
t.Fatalf("parseParallelConfig() unexpected error: %v", err)
|
||||
}
|
||||
if len(cfg.Tasks) != 1 {
|
||||
t.Fatalf("expected 1 task, got %d", len(cfg.Tasks))
|
||||
}
|
||||
if got := cfg.Tasks[0].PromptFile; got != "~/.claude/prompt.md" {
|
||||
t.Fatalf("PromptFile = %q, want %q", got, "~/.claude/prompt.md")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRunCodexTaskFn_AppliesAgentPromptFile(t *testing.T) {
|
||||
defer resetTestHooks()
|
||||
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
claudeDir := filepath.Join(home, ".claude")
|
||||
if err := os.MkdirAll(claudeDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(claudeDir, "prompt.md"), []byte("P\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
fake := newFakeCmd(fakeCmdConfig{
|
||||
StdoutPlan: []fakeStdoutEvent{
|
||||
{Data: `{"type":"item.completed","item":{"type":"agent_message","text":"ok"}}` + "\n"},
|
||||
},
|
||||
WaitDelay: 2 * time.Millisecond,
|
||||
})
|
||||
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return fake
|
||||
}
|
||||
selectBackendFn = func(name string) (Backend, error) {
|
||||
return testBackend{
|
||||
name: name,
|
||||
command: "fake-cmd",
|
||||
argsFn: func(cfg *Config, targetArg string) []string {
|
||||
return []string{targetArg}
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
res := defaultRunCodexTaskFn(TaskSpec{
|
||||
ID: "t",
|
||||
Task: "do",
|
||||
Backend: "codex",
|
||||
PromptFile: "~/.claude/prompt.md",
|
||||
}, 5)
|
||||
if res.ExitCode != 0 {
|
||||
t.Fatalf("unexpected result: %+v", res)
|
||||
}
|
||||
|
||||
want := "<agent-prompt>\nP\n</agent-prompt>\n\ndo"
|
||||
if got := fake.StdinContents(); got != want {
|
||||
t.Fatalf("stdin mismatch:\n got=%q\nwant=%q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Backend defines the contract for invoking different AI CLI backends.
|
||||
@@ -38,48 +37,33 @@ func (ClaudeBackend) BuildArgs(cfg *Config, targetArg string) []string {
|
||||
|
||||
const maxClaudeSettingsBytes = 1 << 20 // 1MB
|
||||
|
||||
type minimalClaudeSettings struct {
|
||||
Env map[string]string
|
||||
Model string
|
||||
}
|
||||
|
||||
// loadMinimalClaudeSettings 从 ~/.claude/settings.json 只提取安全的最小子集:
|
||||
// - env: 只接受字符串类型的值
|
||||
// - model: 只接受字符串类型的值
|
||||
// 文件缺失/解析失败/超限都返回空。
|
||||
func loadMinimalClaudeSettings() minimalClaudeSettings {
|
||||
// loadMinimalEnvSettings 从 ~/.claude/settings.json 只提取 env 配置。
|
||||
// 只接受字符串类型的值;文件缺失/解析失败/超限都返回空。
|
||||
func loadMinimalEnvSettings() map[string]string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
return minimalClaudeSettings{}
|
||||
return nil
|
||||
}
|
||||
|
||||
settingPath := filepath.Join(home, ".claude", "settings.json")
|
||||
info, err := os.Stat(settingPath)
|
||||
if err != nil || info.Size() > maxClaudeSettingsBytes {
|
||||
return minimalClaudeSettings{}
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(settingPath)
|
||||
if err != nil {
|
||||
return minimalClaudeSettings{}
|
||||
return nil
|
||||
}
|
||||
|
||||
var cfg struct {
|
||||
Env map[string]any `json:"env"`
|
||||
Model any `json:"model"`
|
||||
Env map[string]any `json:"env"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return minimalClaudeSettings{}
|
||||
return nil
|
||||
}
|
||||
|
||||
out := minimalClaudeSettings{}
|
||||
|
||||
if model, ok := cfg.Model.(string); ok {
|
||||
out.Model = strings.TrimSpace(model)
|
||||
}
|
||||
|
||||
if len(cfg.Env) == 0 {
|
||||
return out
|
||||
return nil
|
||||
}
|
||||
|
||||
env := make(map[string]string, len(cfg.Env))
|
||||
@@ -90,61 +74,6 @@ func loadMinimalClaudeSettings() minimalClaudeSettings {
|
||||
}
|
||||
env[k] = s
|
||||
}
|
||||
if len(env) == 0 {
|
||||
return out
|
||||
}
|
||||
out.Env = env
|
||||
return out
|
||||
}
|
||||
|
||||
// loadMinimalEnvSettings is kept for backwards tests; prefer loadMinimalClaudeSettings.
|
||||
func loadMinimalEnvSettings() map[string]string {
|
||||
settings := loadMinimalClaudeSettings()
|
||||
if len(settings.Env) == 0 {
|
||||
return nil
|
||||
}
|
||||
return settings.Env
|
||||
}
|
||||
|
||||
// loadGeminiEnv loads environment variables from ~/.gemini/.env
|
||||
// Supports GEMINI_API_KEY, GEMINI_MODEL, GOOGLE_GEMINI_BASE_URL
|
||||
// Also sets GEMINI_API_KEY_AUTH_MECHANISM=bearer for third-party API compatibility
|
||||
func loadGeminiEnv() map[string]string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
envPath := filepath.Join(home, ".gemini", ".env")
|
||||
data, err := os.ReadFile(envPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
env := make(map[string]string)
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
idx := strings.IndexByte(line, '=')
|
||||
if idx <= 0 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(line[:idx])
|
||||
value := strings.TrimSpace(line[idx+1:])
|
||||
if key != "" && value != "" {
|
||||
env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Set bearer auth mechanism for third-party API compatibility
|
||||
if _, ok := env["GEMINI_API_KEY"]; ok {
|
||||
if _, hasAuth := env["GEMINI_API_KEY_AUTH_MECHANISM"]; !hasAuth {
|
||||
env["GEMINI_API_KEY_AUTH_MECHANISM"] = "bearer"
|
||||
}
|
||||
}
|
||||
|
||||
if len(env) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -156,8 +85,7 @@ func buildClaudeArgs(cfg *Config, targetArg string) []string {
|
||||
return nil
|
||||
}
|
||||
args := []string{"-p"}
|
||||
// Default to skip permissions unless CODEAGENT_SKIP_PERMISSIONS=false
|
||||
if cfg.SkipPermissions || cfg.Yolo || envFlagDefaultTrue("CODEAGENT_SKIP_PERMISSIONS") {
|
||||
if cfg.SkipPermissions {
|
||||
args = append(args, "--dangerously-skip-permissions")
|
||||
}
|
||||
|
||||
@@ -165,10 +93,6 @@ func buildClaudeArgs(cfg *Config, targetArg string) []string {
|
||||
// This ensures a clean execution environment without CLAUDE.md or skills that would trigger codeagent
|
||||
args = append(args, "--setting-sources", "")
|
||||
|
||||
if model := strings.TrimSpace(cfg.Model); model != "" {
|
||||
args = append(args, "--model", model)
|
||||
}
|
||||
|
||||
if cfg.Mode == "resume" {
|
||||
if cfg.SessionID != "" {
|
||||
// Claude CLI uses -r <session_id> for resume.
|
||||
@@ -192,32 +116,12 @@ func (GeminiBackend) BuildArgs(cfg *Config, targetArg string) []string {
|
||||
return buildGeminiArgs(cfg, targetArg)
|
||||
}
|
||||
|
||||
type OpencodeBackend struct{}
|
||||
|
||||
func (OpencodeBackend) Name() string { return "opencode" }
|
||||
func (OpencodeBackend) Command() string { return "opencode" }
|
||||
func (OpencodeBackend) BuildArgs(cfg *Config, targetArg string) []string {
|
||||
args := []string{"run"}
|
||||
if model := strings.TrimSpace(cfg.Model); model != "" {
|
||||
args = append(args, "-m", model)
|
||||
}
|
||||
if cfg.Mode == "resume" && cfg.SessionID != "" {
|
||||
args = append(args, "-s", cfg.SessionID)
|
||||
}
|
||||
args = append(args, "--format", "json", targetArg)
|
||||
return args
|
||||
}
|
||||
|
||||
func buildGeminiArgs(cfg *Config, targetArg string) []string {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
args := []string{"-o", "stream-json", "-y"}
|
||||
|
||||
if model := strings.TrimSpace(cfg.Model); model != "" {
|
||||
args = append(args, "-m", model)
|
||||
}
|
||||
|
||||
if cfg.Mode == "resume" {
|
||||
if cfg.SessionID != "" {
|
||||
args = append(args, "-r", cfg.SessionID)
|
||||
@@ -225,13 +129,7 @@ func buildGeminiArgs(cfg *Config, targetArg string) []string {
|
||||
}
|
||||
// Note: gemini CLI doesn't support -C flag; workdir set via cmd.Dir
|
||||
|
||||
// Use positional argument instead of deprecated -p flag
|
||||
// For stdin mode ("-"), use -p to read from stdin
|
||||
if targetArg == "-" {
|
||||
args = append(args, "-p", targetArg)
|
||||
} else {
|
||||
args = append(args, targetArg)
|
||||
}
|
||||
args = append(args, "-p", targetArg)
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ import (
|
||||
func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
|
||||
backend := ClaudeBackend{}
|
||||
|
||||
t.Run("new mode omits skip-permissions when env disabled", func(t *testing.T) {
|
||||
t.Setenv("CODEAGENT_SKIP_PERMISSIONS", "false")
|
||||
t.Run("new mode omits skip-permissions by default", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "new", WorkDir: "/repo"}
|
||||
got := backend.BuildArgs(cfg, "todo")
|
||||
want := []string{"-p", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "todo"}
|
||||
@@ -21,8 +20,8 @@ func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("new mode includes skip-permissions by default", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "new", SkipPermissions: false}
|
||||
t.Run("new mode can opt-in skip-permissions", func(t *testing.T) {
|
||||
cfg := &Config{Mode: "new", SkipPermissions: true}
|
||||
got := backend.BuildArgs(cfg, "-")
|
||||
want := []string{"-p", "--dangerously-skip-permissions", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "-"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
@@ -31,7 +30,6 @@ func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("resume mode includes session id", func(t *testing.T) {
|
||||
t.Setenv("CODEAGENT_SKIP_PERMISSIONS", "false")
|
||||
cfg := &Config{Mode: "resume", SessionID: "sid-123", WorkDir: "/ignored"}
|
||||
got := backend.BuildArgs(cfg, "resume-task")
|
||||
want := []string{"-p", "--setting-sources", "", "-r", "sid-123", "--output-format", "stream-json", "--verbose", "resume-task"}
|
||||
@@ -41,7 +39,6 @@ func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("resume mode without session still returns base flags", func(t *testing.T) {
|
||||
t.Setenv("CODEAGENT_SKIP_PERMISSIONS", "false")
|
||||
cfg := &Config{Mode: "resume", WorkDir: "/ignored"}
|
||||
got := backend.BuildArgs(cfg, "follow-up")
|
||||
want := []string{"-p", "--setting-sources", "", "--output-format", "stream-json", "--verbose", "follow-up"}
|
||||
@@ -66,48 +63,12 @@ func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBackendBuildArgs_Model(t *testing.T) {
|
||||
t.Run("claude includes --model when set", func(t *testing.T) {
|
||||
t.Setenv("CODEAGENT_SKIP_PERMISSIONS", "false")
|
||||
backend := ClaudeBackend{}
|
||||
cfg := &Config{Mode: "new", Model: "opus"}
|
||||
got := backend.BuildArgs(cfg, "todo")
|
||||
want := []string{"-p", "--setting-sources", "", "--model", "opus", "--output-format", "stream-json", "--verbose", "todo"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("gemini includes -m when set", func(t *testing.T) {
|
||||
backend := GeminiBackend{}
|
||||
cfg := &Config{Mode: "new", Model: "gemini-3-pro-preview"}
|
||||
got := backend.BuildArgs(cfg, "task")
|
||||
want := []string{"-o", "stream-json", "-y", "-m", "gemini-3-pro-preview", "task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("codex includes --model when set", func(t *testing.T) {
|
||||
const key = "CODEX_BYPASS_SANDBOX"
|
||||
t.Setenv(key, "false")
|
||||
|
||||
backend := CodexBackend{}
|
||||
cfg := &Config{Mode: "new", WorkDir: "/tmp", Model: "o3"}
|
||||
got := backend.BuildArgs(cfg, "task")
|
||||
want := []string{"e", "--model", "o3", "--skip-git-repo-check", "-C", "/tmp", "--json", "task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestClaudeBuildArgs_GeminiAndCodexModes(t *testing.T) {
|
||||
t.Run("gemini new mode defaults workdir", func(t *testing.T) {
|
||||
backend := GeminiBackend{}
|
||||
cfg := &Config{Mode: "new", WorkDir: "/workspace"}
|
||||
got := backend.BuildArgs(cfg, "task")
|
||||
want := []string{"-o", "stream-json", "-y", "task"}
|
||||
want := []string{"-o", "stream-json", "-y", "-p", "task"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
@@ -117,7 +78,7 @@ func TestClaudeBuildArgs_GeminiAndCodexModes(t *testing.T) {
|
||||
backend := GeminiBackend{}
|
||||
cfg := &Config{Mode: "resume", SessionID: "sid-999"}
|
||||
got := backend.BuildArgs(cfg, "resume")
|
||||
want := []string{"-o", "stream-json", "-y", "-r", "sid-999", "resume"}
|
||||
want := []string{"-o", "stream-json", "-y", "-r", "sid-999", "-p", "resume"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
@@ -127,7 +88,7 @@ func TestClaudeBuildArgs_GeminiAndCodexModes(t *testing.T) {
|
||||
backend := GeminiBackend{}
|
||||
cfg := &Config{Mode: "resume"}
|
||||
got := backend.BuildArgs(cfg, "resume")
|
||||
want := []string{"-o", "stream-json", "-y", "resume"}
|
||||
want := []string{"-o", "stream-json", "-y", "-p", "resume"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
@@ -140,19 +101,10 @@ func TestClaudeBuildArgs_GeminiAndCodexModes(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("gemini stdin mode uses -p flag", func(t *testing.T) {
|
||||
backend := GeminiBackend{}
|
||||
cfg := &Config{Mode: "new"}
|
||||
got := backend.BuildArgs(cfg, "-")
|
||||
want := []string{"-o", "stream-json", "-y", "-p", "-"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("codex build args omits bypass flag by default", func(t *testing.T) {
|
||||
const key = "CODEX_BYPASS_SANDBOX"
|
||||
t.Setenv(key, "false")
|
||||
t.Cleanup(func() { os.Unsetenv(key) })
|
||||
os.Unsetenv(key)
|
||||
|
||||
backend := CodexBackend{}
|
||||
cfg := &Config{Mode: "new", WorkDir: "/tmp"}
|
||||
@@ -165,7 +117,8 @@ func TestClaudeBuildArgs_GeminiAndCodexModes(t *testing.T) {
|
||||
|
||||
t.Run("codex build args includes bypass flag when enabled", func(t *testing.T) {
|
||||
const key = "CODEX_BYPASS_SANDBOX"
|
||||
t.Setenv(key, "true")
|
||||
t.Cleanup(func() { os.Unsetenv(key) })
|
||||
os.Setenv(key, "true")
|
||||
|
||||
backend := CodexBackend{}
|
||||
cfg := &Config{Mode: "new", WorkDir: "/tmp"}
|
||||
|
||||
@@ -15,16 +15,10 @@ type Config struct {
|
||||
Task string
|
||||
SessionID string
|
||||
WorkDir string
|
||||
Model string
|
||||
ReasoningEffort string
|
||||
ExplicitStdin bool
|
||||
Timeout int
|
||||
Backend string
|
||||
Agent string
|
||||
PromptFile string
|
||||
PromptFileExplicit bool
|
||||
SkipPermissions bool
|
||||
Yolo bool
|
||||
MaxParallelWorkers int
|
||||
}
|
||||
|
||||
@@ -36,20 +30,15 @@ type ParallelConfig struct {
|
||||
|
||||
// TaskSpec describes an individual task entry in the parallel config
|
||||
type TaskSpec struct {
|
||||
ID string `json:"id"`
|
||||
Task string `json:"task"`
|
||||
WorkDir string `json:"workdir,omitempty"`
|
||||
Dependencies []string `json:"dependencies,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
ReasoningEffort string `json:"reasoning_effort,omitempty"`
|
||||
Agent string `json:"agent,omitempty"`
|
||||
PromptFile string `json:"prompt_file,omitempty"`
|
||||
SkipPermissions bool `json:"skip_permissions,omitempty"`
|
||||
Mode string `json:"-"`
|
||||
UseStdin bool `json:"-"`
|
||||
Context context.Context `json:"-"`
|
||||
ID string `json:"id"`
|
||||
Task string `json:"task"`
|
||||
WorkDir string `json:"workdir,omitempty"`
|
||||
Dependencies []string `json:"dependencies,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
Mode string `json:"-"`
|
||||
UseStdin bool `json:"-"`
|
||||
Context context.Context `json:"-"`
|
||||
}
|
||||
|
||||
// TaskResult captures the execution outcome of a task
|
||||
@@ -72,10 +61,9 @@ type TaskResult struct {
|
||||
}
|
||||
|
||||
var backendRegistry = map[string]Backend{
|
||||
"codex": CodexBackend{},
|
||||
"claude": ClaudeBackend{},
|
||||
"gemini": GeminiBackend{},
|
||||
"opencode": OpencodeBackend{},
|
||||
"codex": CodexBackend{},
|
||||
"claude": ClaudeBackend{},
|
||||
"gemini": GeminiBackend{},
|
||||
}
|
||||
|
||||
func selectBackend(name string) (Backend, error) {
|
||||
@@ -115,32 +103,6 @@ func parseBoolFlag(val string, defaultValue bool) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// envFlagDefaultTrue returns true unless the env var is explicitly set to false/0/no/off.
|
||||
func envFlagDefaultTrue(key string) bool {
|
||||
val, ok := os.LookupEnv(key)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
return parseBoolFlag(val, true)
|
||||
}
|
||||
|
||||
func validateAgentName(name string) error {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return fmt.Errorf("agent name is empty")
|
||||
}
|
||||
for _, r := range name {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
case r >= 'A' && r <= 'Z':
|
||||
case r >= '0' && r <= '9':
|
||||
case r == '-', r == '_':
|
||||
default:
|
||||
return fmt.Errorf("agent name %q contains invalid character %q", name, r)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseParallelConfig(data []byte) (*ParallelConfig, error) {
|
||||
trimmed := bytes.TrimSpace(data)
|
||||
if len(trimmed) == 0 {
|
||||
@@ -168,7 +130,6 @@ func parseParallelConfig(data []byte) (*ParallelConfig, error) {
|
||||
content := strings.TrimSpace(parts[1])
|
||||
|
||||
task := TaskSpec{WorkDir: defaultWorkdir}
|
||||
agentSpecified := false
|
||||
for _, line := range strings.Split(meta, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
@@ -185,29 +146,12 @@ func parseParallelConfig(data []byte) (*ParallelConfig, error) {
|
||||
case "id":
|
||||
task.ID = value
|
||||
case "workdir":
|
||||
// Validate workdir: "-" is not a valid directory
|
||||
if value == "-" {
|
||||
return nil, fmt.Errorf("task block #%d has invalid workdir: '-' is not a valid directory path", taskIndex)
|
||||
}
|
||||
task.WorkDir = value
|
||||
case "session_id":
|
||||
task.SessionID = value
|
||||
task.Mode = "resume"
|
||||
case "backend":
|
||||
task.Backend = value
|
||||
case "model":
|
||||
task.Model = value
|
||||
case "reasoning_effort":
|
||||
task.ReasoningEffort = value
|
||||
case "agent":
|
||||
agentSpecified = true
|
||||
task.Agent = value
|
||||
case "skip_permissions", "skip-permissions":
|
||||
if value == "" {
|
||||
task.SkipPermissions = true
|
||||
continue
|
||||
}
|
||||
task.SkipPermissions = parseBoolFlag(value, false)
|
||||
case "dependencies":
|
||||
for _, dep := range strings.Split(value, ",") {
|
||||
dep = strings.TrimSpace(dep)
|
||||
@@ -222,26 +166,6 @@ func parseParallelConfig(data []byte) (*ParallelConfig, error) {
|
||||
task.Mode = "new"
|
||||
}
|
||||
|
||||
if agentSpecified {
|
||||
if strings.TrimSpace(task.Agent) == "" {
|
||||
return nil, fmt.Errorf("task block #%d has empty agent field", taskIndex)
|
||||
}
|
||||
if err := validateAgentName(task.Agent); err != nil {
|
||||
return nil, fmt.Errorf("task block #%d invalid agent name: %w", taskIndex, err)
|
||||
}
|
||||
backend, model, promptFile, reasoning, _ := resolveAgentConfig(task.Agent)
|
||||
if task.Backend == "" {
|
||||
task.Backend = backend
|
||||
}
|
||||
if task.Model == "" {
|
||||
task.Model = model
|
||||
}
|
||||
if task.ReasoningEffort == "" {
|
||||
task.ReasoningEffort = reasoning
|
||||
}
|
||||
task.PromptFile = promptFile
|
||||
}
|
||||
|
||||
if task.ID == "" {
|
||||
return nil, fmt.Errorf("task block #%d missing id field", taskIndex)
|
||||
}
|
||||
@@ -274,81 +198,11 @@ func parseArgs() (*Config, error) {
|
||||
}
|
||||
|
||||
backendName := defaultBackendName
|
||||
model := ""
|
||||
reasoningEffort := ""
|
||||
agentName := ""
|
||||
promptFile := ""
|
||||
promptFileExplicit := false
|
||||
yolo := false
|
||||
skipPermissions := envFlagEnabled("CODEAGENT_SKIP_PERMISSIONS")
|
||||
filtered := make([]string, 0, len(args))
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--agent":
|
||||
if i+1 >= len(args) {
|
||||
return nil, fmt.Errorf("--agent flag requires a value")
|
||||
}
|
||||
value := strings.TrimSpace(args[i+1])
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--agent flag requires a value")
|
||||
}
|
||||
if err := validateAgentName(value); err != nil {
|
||||
return nil, fmt.Errorf("--agent flag invalid value: %w", err)
|
||||
}
|
||||
resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning, resolvedYolo := resolveAgentConfig(value)
|
||||
backendName = resolvedBackend
|
||||
model = resolvedModel
|
||||
if !promptFileExplicit {
|
||||
promptFile = resolvedPromptFile
|
||||
}
|
||||
if reasoningEffort == "" {
|
||||
reasoningEffort = resolvedReasoning
|
||||
}
|
||||
yolo = resolvedYolo
|
||||
agentName = value
|
||||
i++
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--agent="):
|
||||
value := strings.TrimSpace(strings.TrimPrefix(arg, "--agent="))
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--agent flag requires a value")
|
||||
}
|
||||
if err := validateAgentName(value); err != nil {
|
||||
return nil, fmt.Errorf("--agent flag invalid value: %w", err)
|
||||
}
|
||||
resolvedBackend, resolvedModel, resolvedPromptFile, resolvedReasoning, resolvedYolo := resolveAgentConfig(value)
|
||||
backendName = resolvedBackend
|
||||
model = resolvedModel
|
||||
if !promptFileExplicit {
|
||||
promptFile = resolvedPromptFile
|
||||
}
|
||||
if reasoningEffort == "" {
|
||||
reasoningEffort = resolvedReasoning
|
||||
}
|
||||
yolo = resolvedYolo
|
||||
agentName = value
|
||||
continue
|
||||
case arg == "--prompt-file":
|
||||
if i+1 >= len(args) {
|
||||
return nil, fmt.Errorf("--prompt-file flag requires a value")
|
||||
}
|
||||
value := strings.TrimSpace(args[i+1])
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--prompt-file flag requires a value")
|
||||
}
|
||||
promptFile = value
|
||||
promptFileExplicit = true
|
||||
i++
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--prompt-file="):
|
||||
value := strings.TrimSpace(strings.TrimPrefix(arg, "--prompt-file="))
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--prompt-file flag requires a value")
|
||||
}
|
||||
promptFile = value
|
||||
promptFileExplicit = true
|
||||
continue
|
||||
case arg == "--backend":
|
||||
if i+1 >= len(args) {
|
||||
return nil, fmt.Errorf("--backend flag requires a value")
|
||||
@@ -366,38 +220,6 @@ func parseArgs() (*Config, error) {
|
||||
case arg == "--skip-permissions", arg == "--dangerously-skip-permissions":
|
||||
skipPermissions = true
|
||||
continue
|
||||
case arg == "--model":
|
||||
if i+1 >= len(args) {
|
||||
return nil, fmt.Errorf("--model flag requires a value")
|
||||
}
|
||||
model = args[i+1]
|
||||
i++
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--model="):
|
||||
value := strings.TrimPrefix(arg, "--model=")
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--model flag requires a value")
|
||||
}
|
||||
model = value
|
||||
continue
|
||||
case arg == "--reasoning-effort":
|
||||
if i+1 >= len(args) {
|
||||
return nil, fmt.Errorf("--reasoning-effort flag requires a value")
|
||||
}
|
||||
value := strings.TrimSpace(args[i+1])
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--reasoning-effort flag requires a value")
|
||||
}
|
||||
reasoningEffort = value
|
||||
i++
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--reasoning-effort="):
|
||||
value := strings.TrimSpace(strings.TrimPrefix(arg, "--reasoning-effort="))
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("--reasoning-effort flag requires a value")
|
||||
}
|
||||
reasoningEffort = value
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--skip-permissions="):
|
||||
skipPermissions = parseBoolFlag(strings.TrimPrefix(arg, "--skip-permissions="), skipPermissions)
|
||||
continue
|
||||
@@ -413,7 +235,7 @@ func parseArgs() (*Config, error) {
|
||||
}
|
||||
args = filtered
|
||||
|
||||
cfg := &Config{WorkDir: defaultWorkdir, Backend: backendName, Agent: agentName, PromptFile: promptFile, PromptFileExplicit: promptFileExplicit, SkipPermissions: skipPermissions, Yolo: yolo, Model: strings.TrimSpace(model), ReasoningEffort: strings.TrimSpace(reasoningEffort)}
|
||||
cfg := &Config{WorkDir: defaultWorkdir, Backend: backendName, SkipPermissions: skipPermissions}
|
||||
cfg.MaxParallelWorkers = resolveMaxParallelWorkers()
|
||||
|
||||
if args[0] == "resume" {
|
||||
@@ -428,10 +250,6 @@ func parseArgs() (*Config, error) {
|
||||
cfg.Task = args[2]
|
||||
cfg.ExplicitStdin = (args[2] == "-")
|
||||
if len(args) > 3 {
|
||||
// Validate workdir: "-" is not a valid directory
|
||||
if args[3] == "-" {
|
||||
return nil, fmt.Errorf("invalid workdir: '-' is not a valid directory path")
|
||||
}
|
||||
cfg.WorkDir = args[3]
|
||||
}
|
||||
} else {
|
||||
@@ -439,10 +257,6 @@ func parseArgs() (*Config, error) {
|
||||
cfg.Task = args[0]
|
||||
cfg.ExplicitStdin = (args[0] == "-")
|
||||
if len(args) > 1 {
|
||||
// Validate workdir: "-" is not a valid directory
|
||||
if args[1] == "-" {
|
||||
return nil, fmt.Errorf("invalid workdir: '-' is not a valid directory path")
|
||||
}
|
||||
cfg.WorkDir = args[1]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,12 @@ import (
|
||||
)
|
||||
|
||||
const postMessageTerminateDelay = 1 * time.Second
|
||||
const forceKillWaitTimeout = 5 * time.Second
|
||||
|
||||
// commandRunner abstracts exec.Cmd for testability
|
||||
type commandRunner interface {
|
||||
Start() error
|
||||
Wait() error
|
||||
StdoutPipe() (io.ReadCloser, error)
|
||||
StderrPipe() (io.ReadCloser, error)
|
||||
StdinPipe() (io.WriteCloser, error)
|
||||
SetStderr(io.Writer)
|
||||
SetDir(string)
|
||||
@@ -65,13 +63,6 @@ func (r *realCmd) StdoutPipe() (io.ReadCloser, error) {
|
||||
return r.cmd.StdoutPipe()
|
||||
}
|
||||
|
||||
func (r *realCmd) StderrPipe() (io.ReadCloser, error) {
|
||||
if r.cmd == nil {
|
||||
return nil, errors.New("command is nil")
|
||||
}
|
||||
return r.cmd.StderrPipe()
|
||||
}
|
||||
|
||||
func (r *realCmd) StdinPipe() (io.WriteCloser, error) {
|
||||
if r.cmd == nil {
|
||||
return nil, errors.New("command is nil")
|
||||
@@ -237,13 +228,6 @@ func defaultRunCodexTaskFn(task TaskSpec, timeout int) TaskResult {
|
||||
if task.Mode == "" {
|
||||
task.Mode = "new"
|
||||
}
|
||||
if strings.TrimSpace(task.PromptFile) != "" {
|
||||
prompt, err := readAgentPromptFile(task.PromptFile, false)
|
||||
if err != nil {
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 1, Error: "failed to read prompt file: " + err.Error()}
|
||||
}
|
||||
task.Task = wrapTaskWithAgentPrompt(prompt, task.Task)
|
||||
}
|
||||
if task.UseStdin || shouldUseStdin(task.Task, false) {
|
||||
task.UseStdin = true
|
||||
}
|
||||
@@ -755,20 +739,11 @@ func buildCodexArgs(cfg *Config, targetArg string) []string {
|
||||
|
||||
args := []string{"e"}
|
||||
|
||||
// Default to bypass sandbox unless CODEX_BYPASS_SANDBOX=false
|
||||
if cfg.Yolo || envFlagDefaultTrue("CODEX_BYPASS_SANDBOX") {
|
||||
logWarn("YOLO mode or CODEX_BYPASS_SANDBOX enabled: running without approval/sandbox protection")
|
||||
if envFlagEnabled("CODEX_BYPASS_SANDBOX") {
|
||||
logWarn("CODEX_BYPASS_SANDBOX=true: running without approval/sandbox protection")
|
||||
args = append(args, "--dangerously-bypass-approvals-and-sandbox")
|
||||
}
|
||||
|
||||
if model := strings.TrimSpace(cfg.Model); model != "" {
|
||||
args = append(args, "--model", model)
|
||||
}
|
||||
|
||||
if reasoningEffort := strings.TrimSpace(cfg.ReasoningEffort); reasoningEffort != "" {
|
||||
args = append(args, "-c", "model_reasoning_effort="+reasoningEffort)
|
||||
}
|
||||
|
||||
args = append(args, "--skip-git-repo-check")
|
||||
|
||||
if isResume {
|
||||
@@ -809,14 +784,11 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
||||
logger := injectedLogger
|
||||
|
||||
cfg := &Config{
|
||||
Mode: taskSpec.Mode,
|
||||
Task: taskSpec.Task,
|
||||
SessionID: taskSpec.SessionID,
|
||||
WorkDir: taskSpec.WorkDir,
|
||||
Model: taskSpec.Model,
|
||||
ReasoningEffort: taskSpec.ReasoningEffort,
|
||||
SkipPermissions: taskSpec.SkipPermissions,
|
||||
Backend: defaultBackendName,
|
||||
Mode: taskSpec.Mode,
|
||||
Task: taskSpec.Task,
|
||||
SessionID: taskSpec.SessionID,
|
||||
WorkDir: taskSpec.WorkDir,
|
||||
Backend: defaultBackendName,
|
||||
}
|
||||
|
||||
commandName := codexCommand
|
||||
@@ -844,21 +816,6 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
||||
return result
|
||||
}
|
||||
|
||||
var claudeEnv map[string]string
|
||||
if cfg.Backend == "claude" {
|
||||
settings := loadMinimalClaudeSettings()
|
||||
claudeEnv = settings.Env
|
||||
if cfg.Mode != "resume" && strings.TrimSpace(cfg.Model) == "" && settings.Model != "" {
|
||||
cfg.Model = settings.Model
|
||||
}
|
||||
}
|
||||
|
||||
// Load gemini env from ~/.gemini/.env if exists
|
||||
var geminiEnv map[string]string
|
||||
if cfg.Backend == "gemini" {
|
||||
geminiEnv = loadGeminiEnv()
|
||||
}
|
||||
|
||||
useStdin := taskSpec.UseStdin
|
||||
targetArg := taskSpec.Task
|
||||
if useStdin {
|
||||
@@ -958,11 +915,10 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
||||
|
||||
cmd := newCommandRunner(ctx, commandName, codexArgs...)
|
||||
|
||||
if cfg.Backend == "claude" && len(claudeEnv) > 0 {
|
||||
cmd.SetEnv(claudeEnv)
|
||||
}
|
||||
if cfg.Backend == "gemini" && len(geminiEnv) > 0 {
|
||||
cmd.SetEnv(geminiEnv)
|
||||
if cfg.Backend == "claude" {
|
||||
if env := loadMinimalEnvSettings(); len(env) > 0 {
|
||||
cmd.SetEnv(env)
|
||||
}
|
||||
}
|
||||
|
||||
// For backends that don't support -C flag (claude, gemini), set working directory via cmd.Dir
|
||||
@@ -983,40 +939,33 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
||||
if cfg.Backend == "gemini" {
|
||||
stderrFilter = newFilteringWriter(os.Stderr, geminiNoisePatterns)
|
||||
stderrOut = stderrFilter
|
||||
defer stderrFilter.Flush()
|
||||
}
|
||||
stderrWriters = append([]io.Writer{stderrOut}, stderrWriters...)
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
logErrorFn("Failed to create stderr pipe: " + err.Error())
|
||||
result.ExitCode = 1
|
||||
result.Error = attachStderr("failed to create stderr pipe: " + err.Error())
|
||||
return result
|
||||
if len(stderrWriters) == 1 {
|
||||
cmd.SetStderr(stderrWriters[0])
|
||||
} else {
|
||||
cmd.SetStderr(io.MultiWriter(stderrWriters...))
|
||||
}
|
||||
|
||||
var stdinPipe io.WriteCloser
|
||||
var err error
|
||||
if useStdin {
|
||||
stdinPipe, err = cmd.StdinPipe()
|
||||
if err != nil {
|
||||
logErrorFn("Failed to create stdin pipe: " + err.Error())
|
||||
result.ExitCode = 1
|
||||
result.Error = attachStderr("failed to create stdin pipe: " + err.Error())
|
||||
closeWithReason(stderr, "stdin-pipe-failed")
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
stderrDone := make(chan error, 1)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
logErrorFn("Failed to create stdout pipe: " + err.Error())
|
||||
result.ExitCode = 1
|
||||
result.Error = attachStderr("failed to create stdout pipe: " + err.Error())
|
||||
closeWithReason(stderr, "stdout-pipe-failed")
|
||||
if stdinPipe != nil {
|
||||
_ = stdinPipe.Close()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1052,11 +1001,6 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
||||
logInfoFn(fmt.Sprintf("Starting %s with args: %s %s...", commandName, commandName, strings.Join(codexArgs[:min(5, len(codexArgs))], " ")))
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
closeWithReason(stdout, "start-failed")
|
||||
closeWithReason(stderr, "start-failed")
|
||||
if stdinPipe != nil {
|
||||
_ = stdinPipe.Close()
|
||||
}
|
||||
if strings.Contains(err.Error(), "executable file not found") {
|
||||
msg := fmt.Sprintf("%s command not found in PATH", commandName)
|
||||
logErrorFn(msg)
|
||||
@@ -1075,15 +1019,6 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
||||
logInfoFn(fmt.Sprintf("Log capturing to: %s", logger.Path()))
|
||||
}
|
||||
|
||||
// Start stderr drain AFTER we know the command started, but BEFORE cmd.Wait can close the pipe.
|
||||
go func() {
|
||||
_, copyErr := io.Copy(io.MultiWriter(stderrWriters...), stderr)
|
||||
if stderrFilter != nil {
|
||||
stderrFilter.Flush()
|
||||
}
|
||||
stderrDone <- copyErr
|
||||
}()
|
||||
|
||||
if useStdin && stdinPipe != nil {
|
||||
logInfoFn(fmt.Sprintf("Writing %d chars to stdin...", len(taskSpec.Task)))
|
||||
go func(data string) {
|
||||
@@ -1111,8 +1046,7 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
||||
waitLoop:
|
||||
for {
|
||||
select {
|
||||
case err := <-waitCh:
|
||||
waitErr = err
|
||||
case waitErr = <-waitCh:
|
||||
break waitLoop
|
||||
case <-ctx.Done():
|
||||
ctxCancelled = true
|
||||
@@ -1123,17 +1057,8 @@ waitLoop:
|
||||
terminated = true
|
||||
}
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case err := <-waitCh:
|
||||
waitErr = err
|
||||
break waitLoop
|
||||
case <-time.After(forceKillWaitTimeout):
|
||||
if proc := cmd.Process(); proc != nil {
|
||||
_ = proc.Kill()
|
||||
}
|
||||
}
|
||||
}
|
||||
waitErr = <-waitCh
|
||||
break waitLoop
|
||||
case <-messageTimerCh:
|
||||
forcedAfterComplete = true
|
||||
messageTimerCh = nil
|
||||
@@ -1144,20 +1069,6 @@ waitLoop:
|
||||
terminated = true
|
||||
}
|
||||
}
|
||||
// Close pipes to unblock stream readers, then wait for process exit.
|
||||
closeWithReason(stdout, "terminate")
|
||||
closeWithReason(stderr, "terminate")
|
||||
for {
|
||||
select {
|
||||
case err := <-waitCh:
|
||||
waitErr = err
|
||||
break waitLoop
|
||||
case <-time.After(forceKillWaitTimeout):
|
||||
if proc := cmd.Process(); proc != nil {
|
||||
_ = proc.Kill()
|
||||
}
|
||||
}
|
||||
}
|
||||
case <-completeSeen:
|
||||
completeSeenObserved = true
|
||||
if messageTimer != nil {
|
||||
@@ -1212,12 +1123,6 @@ waitLoop:
|
||||
}
|
||||
}
|
||||
|
||||
closeWithReason(stderr, stdoutCloseReasonWait)
|
||||
// Wait for stderr drain so stderrBuf / stderrLogger are not accessed concurrently.
|
||||
// Important: cmd.Wait can block on internal stderr copying if cmd.Stderr is a non-file writer.
|
||||
// We use StderrPipe and drain ourselves to avoid that deadlock class (common when children inherit pipes).
|
||||
<-stderrDone
|
||||
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
if errors.Is(ctxErr, context.DeadlineExceeded) {
|
||||
result.ExitCode = 124
|
||||
@@ -1292,7 +1197,7 @@ func forwardSignals(ctx context.Context, cmd commandRunner, logErrorFn func(stri
|
||||
case sig := <-sigCh:
|
||||
logErrorFn(fmt.Sprintf("Received signal: %v", sig))
|
||||
if proc := cmd.Process(); proc != nil {
|
||||
_ = sendTermSignal(proc)
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||
if p := cmd.Process(); p != nil {
|
||||
_ = p.Kill()
|
||||
@@ -1362,7 +1267,7 @@ func terminateCommand(cmd commandRunner) *forceKillTimer {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = sendTermSignal(proc)
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
|
||||
done := make(chan struct{}, 1)
|
||||
timer := time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||
@@ -1384,7 +1289,7 @@ func terminateProcess(cmd commandRunner) *time.Timer {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = sendTermSignal(proc)
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
|
||||
return time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||
if p := cmd.Process(); p != nil {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -33,12 +32,7 @@ type execFakeProcess struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (p *execFakeProcess) Pid() int {
|
||||
if runtime.GOOS == "windows" {
|
||||
return 0
|
||||
}
|
||||
return p.pid
|
||||
}
|
||||
func (p *execFakeProcess) Pid() int { return p.pid }
|
||||
func (p *execFakeProcess) Kill() error {
|
||||
p.killed.Add(1)
|
||||
return nil
|
||||
@@ -90,7 +84,6 @@ func (rc *reasonReadCloser) record(reason string) {
|
||||
|
||||
type execFakeRunner struct {
|
||||
stdout io.ReadCloser
|
||||
stderr io.ReadCloser
|
||||
process processHandle
|
||||
stdin io.WriteCloser
|
||||
dir string
|
||||
@@ -99,7 +92,6 @@ type execFakeRunner struct {
|
||||
waitDelay time.Duration
|
||||
startErr error
|
||||
stdoutErr error
|
||||
stderrErr error
|
||||
stdinErr error
|
||||
allowNilProcess bool
|
||||
started atomic.Bool
|
||||
@@ -127,15 +119,6 @@ func (f *execFakeRunner) StdoutPipe() (io.ReadCloser, error) {
|
||||
}
|
||||
return f.stdout, nil
|
||||
}
|
||||
func (f *execFakeRunner) StderrPipe() (io.ReadCloser, error) {
|
||||
if f.stderrErr != nil {
|
||||
return nil, f.stderrErr
|
||||
}
|
||||
if f.stderr == nil {
|
||||
f.stderr = io.NopCloser(strings.NewReader(""))
|
||||
}
|
||||
return f.stderr, nil
|
||||
}
|
||||
func (f *execFakeRunner) StdinPipe() (io.WriteCloser, error) {
|
||||
if f.stdinErr != nil {
|
||||
return nil, f.stdinErr
|
||||
@@ -180,9 +163,6 @@ func TestExecutorHelperCoverage(t *testing.T) {
|
||||
if _, err := rc.StdoutPipe(); err == nil {
|
||||
t.Fatalf("expected error for nil command")
|
||||
}
|
||||
if _, err := rc.StderrPipe(); err == nil {
|
||||
t.Fatalf("expected error for nil command")
|
||||
}
|
||||
if _, err := rc.StdinPipe(); err == nil {
|
||||
t.Fatalf("expected error for nil command")
|
||||
}
|
||||
@@ -202,14 +182,11 @@ func TestExecutorHelperCoverage(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("StdoutPipe error: %v", err)
|
||||
}
|
||||
stderrPipe, err := rcProc.StderrPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("StderrPipe error: %v", err)
|
||||
}
|
||||
stdinPipe, err := rcProc.StdinPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("StdinPipe error: %v", err)
|
||||
}
|
||||
rcProc.SetStderr(io.Discard)
|
||||
if err := rcProc.Start(); err != nil {
|
||||
t.Fatalf("Start failed: %v", err)
|
||||
}
|
||||
@@ -223,7 +200,6 @@ func TestExecutorHelperCoverage(t *testing.T) {
|
||||
_ = procHandle.Kill()
|
||||
_ = rcProc.Wait()
|
||||
_, _ = io.ReadAll(stdoutPipe)
|
||||
_, _ = io.ReadAll(stderrPipe)
|
||||
|
||||
rp := &realProcess{}
|
||||
if rp.Pid() != 0 {
|
||||
@@ -282,7 +258,8 @@ func TestExecutorHelperCoverage(t *testing.T) {
|
||||
|
||||
t.Run("generateFinalOutputAndArgs", func(t *testing.T) {
|
||||
const key = "CODEX_BYPASS_SANDBOX"
|
||||
t.Setenv(key, "false")
|
||||
t.Cleanup(func() { os.Unsetenv(key) })
|
||||
os.Unsetenv(key)
|
||||
|
||||
out := generateFinalOutput([]TaskResult{
|
||||
{TaskID: "ok", ExitCode: 0},
|
||||
@@ -357,7 +334,8 @@ func TestExecutorHelperCoverage(t *testing.T) {
|
||||
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 0, Message: "done"}
|
||||
}
|
||||
t.Setenv("CODEAGENT_MAX_PARALLEL_WORKERS", "1")
|
||||
os.Setenv("CODEAGENT_MAX_PARALLEL_WORKERS", "1")
|
||||
defer os.Unsetenv("CODEAGENT_MAX_PARALLEL_WORKERS")
|
||||
|
||||
results := executeConcurrent([][]TaskSpec{{{ID: "wrap"}}}, 1)
|
||||
if len(results) != 1 || results[0].TaskID != "wrap" {
|
||||
@@ -625,27 +603,6 @@ func TestExecutorRunCodexTaskWithContext(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("claudeSkipPermissionsPropagatesFromTaskSpec", func(t *testing.T) {
|
||||
t.Setenv("CODEAGENT_SKIP_PERMISSIONS", "false")
|
||||
var gotArgs []string
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
gotArgs = append([]string(nil), args...)
|
||||
return &execFakeRunner{
|
||||
stdout: newReasonReadCloser(`{"type":"item.completed","item":{"type":"agent_message","text":"ok"}}`),
|
||||
process: &execFakeProcess{pid: 15},
|
||||
}
|
||||
}
|
||||
|
||||
_ = closeLogger()
|
||||
res := runCodexTaskWithContext(context.Background(), TaskSpec{ID: "task-skip", Task: "payload", WorkDir: ".", SkipPermissions: true}, ClaudeBackend{}, nil, false, false, 1)
|
||||
if res.ExitCode != 0 || res.Error != "" {
|
||||
t.Fatalf("unexpected result: %+v", res)
|
||||
}
|
||||
if !slices.Contains(gotArgs, "--dangerously-skip-permissions") {
|
||||
t.Fatalf("expected --dangerously-skip-permissions in args, got %v", gotArgs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missingMessage", func(t *testing.T) {
|
||||
newCommandRunner = func(ctx context.Context, name string, args ...string) commandRunner {
|
||||
return &execFakeRunner{
|
||||
@@ -1293,7 +1250,7 @@ func TestExecutorSignalAndTermination(t *testing.T) {
|
||||
proc.mu.Lock()
|
||||
signalled := len(proc.signals)
|
||||
proc.mu.Unlock()
|
||||
if runtime.GOOS != "windows" && signalled == 0 {
|
||||
if signalled == 0 {
|
||||
t.Fatalf("process did not receive signal")
|
||||
}
|
||||
if proc.killed.Load() == 0 {
|
||||
|
||||
@@ -36,3 +36,4 @@ func TestLogWriterWriteLimitsBuffer(t *testing.T) {
|
||||
t.Fatalf("log output missing truncated entry, got %q", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
version = "5.6.3"
|
||||
version = "5.4.0"
|
||||
defaultWorkdir = "."
|
||||
defaultTimeout = 7200 // seconds (2 hours)
|
||||
defaultCoverageTarget = 90.0
|
||||
@@ -31,6 +31,8 @@ const (
|
||||
stdoutDrainTimeout = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
var useASCIIMode = os.Getenv("CODEAGENT_ASCII_MODE") == "true"
|
||||
|
||||
// Test hooks for dependency injection
|
||||
var (
|
||||
stdinReader io.Reader = os.Stdin
|
||||
@@ -42,6 +44,7 @@ var (
|
||||
buildCodexArgsFn = buildCodexArgs
|
||||
selectBackendFn = selectBackend
|
||||
commandContext = exec.CommandContext
|
||||
jsonMarshal = json.Marshal
|
||||
cleanupLogsFn = cleanupOldLogs
|
||||
signalNotifyFn = signal.Notify
|
||||
signalStopFn = signal.Stop
|
||||
@@ -175,9 +178,7 @@ func run() (exitCode int) {
|
||||
|
||||
if parallelIndex != -1 {
|
||||
backendName := defaultBackendName
|
||||
model := ""
|
||||
fullOutput := false
|
||||
skipPermissions := envFlagEnabled("CODEAGENT_SKIP_PERMISSIONS")
|
||||
var extras []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
@@ -201,33 +202,13 @@ func run() (exitCode int) {
|
||||
return 1
|
||||
}
|
||||
backendName = value
|
||||
case arg == "--model":
|
||||
if i+1 >= len(args) {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: --model flag requires a value")
|
||||
return 1
|
||||
}
|
||||
model = args[i+1]
|
||||
i++
|
||||
case strings.HasPrefix(arg, "--model="):
|
||||
value := strings.TrimPrefix(arg, "--model=")
|
||||
if value == "" {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: --model flag requires a value")
|
||||
return 1
|
||||
}
|
||||
model = value
|
||||
case arg == "--skip-permissions", arg == "--dangerously-skip-permissions":
|
||||
skipPermissions = true
|
||||
case strings.HasPrefix(arg, "--skip-permissions="):
|
||||
skipPermissions = parseBoolFlag(strings.TrimPrefix(arg, "--skip-permissions="), skipPermissions)
|
||||
case strings.HasPrefix(arg, "--dangerously-skip-permissions="):
|
||||
skipPermissions = parseBoolFlag(strings.TrimPrefix(arg, "--dangerously-skip-permissions="), skipPermissions)
|
||||
default:
|
||||
extras = append(extras, arg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(extras) > 0 {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: --parallel reads its task configuration from stdin; only --backend, --model, --full-output and --skip-permissions are allowed.")
|
||||
fmt.Fprintln(os.Stderr, "ERROR: --parallel reads its task configuration from stdin; only --backend and --full-output are allowed.")
|
||||
fmt.Fprintln(os.Stderr, "Usage examples:")
|
||||
fmt.Fprintf(os.Stderr, " %s --parallel < tasks.txt\n", name)
|
||||
fmt.Fprintf(os.Stderr, " echo '...' | %s --parallel\n", name)
|
||||
@@ -256,15 +237,10 @@ func run() (exitCode int) {
|
||||
}
|
||||
|
||||
cfg.GlobalBackend = backendName
|
||||
model = strings.TrimSpace(model)
|
||||
for i := range cfg.Tasks {
|
||||
if strings.TrimSpace(cfg.Tasks[i].Backend) == "" {
|
||||
cfg.Tasks[i].Backend = backendName
|
||||
}
|
||||
if strings.TrimSpace(cfg.Tasks[i].Model) == "" && model != "" {
|
||||
cfg.Tasks[i].Model = model
|
||||
}
|
||||
cfg.Tasks[i].SkipPermissions = cfg.Tasks[i].SkipPermissions || skipPermissions
|
||||
}
|
||||
|
||||
timeoutSec := resolveTimeout()
|
||||
@@ -377,15 +353,6 @@ func run() (exitCode int) {
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(cfg.PromptFile) != "" {
|
||||
prompt, err := readAgentPromptFile(cfg.PromptFile, cfg.PromptFileExplicit)
|
||||
if err != nil {
|
||||
logError("Failed to read prompt file: " + err.Error())
|
||||
return 1
|
||||
}
|
||||
taskText = wrapTaskWithAgentPrompt(prompt, taskText)
|
||||
}
|
||||
|
||||
useStdin := cfg.ExplicitStdin || shouldUseStdin(taskText, piped)
|
||||
|
||||
targetArg := taskText
|
||||
@@ -438,14 +405,11 @@ func run() (exitCode int) {
|
||||
logInfo(fmt.Sprintf("%s running...", cfg.Backend))
|
||||
|
||||
taskSpec := TaskSpec{
|
||||
Task: taskText,
|
||||
WorkDir: cfg.WorkDir,
|
||||
Mode: cfg.Mode,
|
||||
SessionID: cfg.SessionID,
|
||||
Model: cfg.Model,
|
||||
ReasoningEffort: cfg.ReasoningEffort,
|
||||
SkipPermissions: cfg.SkipPermissions,
|
||||
UseStdin: useStdin,
|
||||
Task: taskText,
|
||||
WorkDir: cfg.WorkDir,
|
||||
Mode: cfg.Mode,
|
||||
SessionID: cfg.SessionID,
|
||||
UseStdin: useStdin,
|
||||
}
|
||||
|
||||
result := runTaskFn(taskSpec, false, cfg.Timeout)
|
||||
@@ -462,91 +426,6 @@ func run() (exitCode int) {
|
||||
return 0
|
||||
}
|
||||
|
||||
func readAgentPromptFile(path string, allowOutsideClaudeDir bool) (string, error) {
|
||||
raw := strings.TrimSpace(path)
|
||||
if raw == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
expanded := raw
|
||||
if raw == "~" || strings.HasPrefix(raw, "~/") || strings.HasPrefix(raw, "~\\") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if raw == "~" {
|
||||
expanded = home
|
||||
} else {
|
||||
expanded = home + raw[1:]
|
||||
}
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(expanded)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
absPath = filepath.Clean(absPath)
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
if !allowOutsideClaudeDir {
|
||||
return "", err
|
||||
}
|
||||
logWarn(fmt.Sprintf("Failed to resolve home directory for prompt file validation: %v; proceeding without restriction", err))
|
||||
} else {
|
||||
allowedDir := filepath.Clean(filepath.Join(home, ".claude"))
|
||||
allowedAbs, err := filepath.Abs(allowedDir)
|
||||
if err == nil {
|
||||
allowedDir = filepath.Clean(allowedAbs)
|
||||
}
|
||||
|
||||
isWithinDir := func(path, dir string) bool {
|
||||
rel, err := filepath.Rel(dir, path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
rel = filepath.Clean(rel)
|
||||
if rel == "." {
|
||||
return true
|
||||
}
|
||||
if rel == ".." {
|
||||
return false
|
||||
}
|
||||
prefix := ".." + string(os.PathSeparator)
|
||||
return !strings.HasPrefix(rel, prefix)
|
||||
}
|
||||
|
||||
if !allowOutsideClaudeDir {
|
||||
if !isWithinDir(absPath, allowedDir) {
|
||||
logWarn(fmt.Sprintf("Refusing to read prompt file outside %s: %s", allowedDir, absPath))
|
||||
return "", fmt.Errorf("prompt file must be under %s", allowedDir)
|
||||
}
|
||||
resolvedPath, errPath := filepath.EvalSymlinks(absPath)
|
||||
resolvedBase, errBase := filepath.EvalSymlinks(allowedDir)
|
||||
if errPath == nil && errBase == nil {
|
||||
resolvedPath = filepath.Clean(resolvedPath)
|
||||
resolvedBase = filepath.Clean(resolvedBase)
|
||||
if !isWithinDir(resolvedPath, resolvedBase) {
|
||||
logWarn(fmt.Sprintf("Refusing to read prompt file outside %s (resolved): %s", resolvedBase, resolvedPath))
|
||||
return "", fmt.Errorf("prompt file must be under %s", resolvedBase)
|
||||
}
|
||||
}
|
||||
} else if !isWithinDir(absPath, allowedDir) {
|
||||
logWarn(fmt.Sprintf("Reading prompt file outside %s: %s", allowedDir, absPath))
|
||||
}
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimRight(string(data), "\r\n"), nil
|
||||
}
|
||||
|
||||
func wrapTaskWithAgentPrompt(prompt string, task string) string {
|
||||
return "<agent-prompt>\n" + prompt + "\n</agent-prompt>\n\n" + task
|
||||
}
|
||||
|
||||
func setLogger(l *Logger) {
|
||||
loggerPtr.Store(l)
|
||||
}
|
||||
@@ -597,7 +476,6 @@ func printHelp() {
|
||||
Usage:
|
||||
%[1]s "task" [workdir]
|
||||
%[1]s --backend claude "task" [workdir]
|
||||
%[1]s --prompt-file /path/to/prompt.md "task" [workdir]
|
||||
%[1]s - [workdir] Read task from stdin
|
||||
%[1]s resume <session_id> "task" [workdir]
|
||||
%[1]s resume <session_id> - [workdir]
|
||||
|
||||
@@ -169,6 +169,32 @@ func parseIntegrationOutput(t *testing.T, out string) integrationOutput {
|
||||
return payload
|
||||
}
|
||||
|
||||
func extractTaskBlock(t *testing.T, output, taskID string) string {
|
||||
t.Helper()
|
||||
header := fmt.Sprintf("--- Task: %s ---", taskID)
|
||||
lines := strings.Split(output, "\n")
|
||||
var block []string
|
||||
collecting := false
|
||||
for _, raw := range lines {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if !collecting {
|
||||
if trimmed == header {
|
||||
collecting = true
|
||||
block = append(block, trimmed)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "--- Task: ") && trimmed != header {
|
||||
break
|
||||
}
|
||||
block = append(block, trimmed)
|
||||
}
|
||||
if len(block) == 0 {
|
||||
t.Fatalf("task block %s not found in output:\n%s", taskID, output)
|
||||
}
|
||||
return strings.Join(block, "\n")
|
||||
}
|
||||
|
||||
func findResultByID(t *testing.T, payload integrationOutput, id string) TaskResult {
|
||||
t.Helper()
|
||||
for _, res := range payload.Results {
|
||||
@@ -615,6 +641,7 @@ func TestRunParallelTimeoutPropagation(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
runCodexTaskFn = origRun
|
||||
resetTestHooks()
|
||||
os.Unsetenv("CODEX_TIMEOUT")
|
||||
})
|
||||
|
||||
var receivedTimeout int
|
||||
@@ -623,7 +650,7 @@ func TestRunParallelTimeoutPropagation(t *testing.T) {
|
||||
return TaskResult{TaskID: task.ID, ExitCode: 124, Error: "timeout"}
|
||||
}
|
||||
|
||||
t.Setenv("CODEX_TIMEOUT", "1")
|
||||
os.Setenv("CODEX_TIMEOUT", "1")
|
||||
input := `---TASK---
|
||||
id: T
|
||||
---CONTENT---
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,14 @@ const (
|
||||
jsonLinePreviewBytes = 256
|
||||
)
|
||||
|
||||
type codexHeader struct {
|
||||
Type string `json:"type"`
|
||||
ThreadID string `json:"thread_id,omitempty"`
|
||||
Item *struct {
|
||||
Type string `json:"type"`
|
||||
} `json:"item,omitempty"`
|
||||
}
|
||||
|
||||
// UnifiedEvent combines all backend event formats into a single structure
|
||||
// to avoid multiple JSON unmarshal operations per event
|
||||
type UnifiedEvent struct {
|
||||
@@ -79,18 +87,6 @@ type UnifiedEvent struct {
|
||||
Content string `json:"content,omitempty"`
|
||||
Delta *bool `json:"delta,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
|
||||
// Opencode-specific fields (camelCase sessionID)
|
||||
OpencodeSessionID string `json:"sessionID,omitempty"`
|
||||
Part json.RawMessage `json:"part,omitempty"`
|
||||
}
|
||||
|
||||
// OpencodePart represents the part field in opencode events
|
||||
type OpencodePart struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
SessionID string `json:"sessionID,omitempty"`
|
||||
}
|
||||
|
||||
// ItemContent represents the parsed item.text field for Codex events
|
||||
@@ -124,10 +120,9 @@ func parseJSONStreamInternal(r io.Reader, warnFn func(string), infoFn func(strin
|
||||
totalEvents := 0
|
||||
|
||||
var (
|
||||
codexMessage string
|
||||
claudeMessage string
|
||||
geminiBuffer strings.Builder
|
||||
opencodeMessage strings.Builder
|
||||
codexMessage string
|
||||
claudeMessage string
|
||||
geminiBuffer strings.Builder
|
||||
)
|
||||
|
||||
for {
|
||||
@@ -168,46 +163,11 @@ func parseJSONStreamInternal(r io.Reader, warnFn func(string), infoFn func(strin
|
||||
isCodex = true
|
||||
}
|
||||
}
|
||||
// Codex-specific event types without thread_id or item
|
||||
if !isCodex && (event.Type == "turn.started" || event.Type == "turn.completed") {
|
||||
isCodex = true
|
||||
}
|
||||
isClaude := event.Subtype != "" || event.Result != ""
|
||||
if !isClaude && event.Type == "result" && event.SessionID != "" && event.Status == "" {
|
||||
isClaude = true
|
||||
}
|
||||
isGemini := (event.Type == "init" && event.SessionID != "") || event.Role != "" || event.Delta != nil || event.Status != ""
|
||||
isOpencode := event.OpencodeSessionID != "" && len(event.Part) > 0
|
||||
|
||||
// Handle Opencode events first (most specific detection)
|
||||
if isOpencode {
|
||||
if threadID == "" {
|
||||
threadID = event.OpencodeSessionID
|
||||
}
|
||||
|
||||
var part OpencodePart
|
||||
if err := json.Unmarshal(event.Part, &part); err != nil {
|
||||
warnFn(fmt.Sprintf("Failed to parse opencode part: %s", err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract sessionID from part if available
|
||||
if part.SessionID != "" && threadID == "" {
|
||||
threadID = part.SessionID
|
||||
}
|
||||
|
||||
infoFn(fmt.Sprintf("Parsed Opencode event #%d type=%s part_type=%s", totalEvents, event.Type, part.Type))
|
||||
|
||||
if event.Type == "text" && part.Text != "" {
|
||||
opencodeMessage.WriteString(part.Text)
|
||||
notifyMessage()
|
||||
}
|
||||
|
||||
if part.Type == "step-finish" && part.Reason == "stop" {
|
||||
notifyComplete()
|
||||
}
|
||||
continue
|
||||
}
|
||||
isGemini := event.Role != "" || event.Delta != nil || event.Status != ""
|
||||
|
||||
// Handle Codex events
|
||||
if isCodex {
|
||||
@@ -234,10 +194,6 @@ func parseJSONStreamInternal(r io.Reader, warnFn func(string), infoFn func(strin
|
||||
infoFn(fmt.Sprintf("thread.completed event thread_id=%s", event.ThreadID))
|
||||
notifyComplete()
|
||||
|
||||
case "turn.completed":
|
||||
infoFn("turn.completed event")
|
||||
notifyComplete()
|
||||
|
||||
case "item.completed":
|
||||
var itemType string
|
||||
if len(event.Item) > 0 {
|
||||
@@ -315,13 +271,11 @@ func parseJSONStreamInternal(r io.Reader, warnFn func(string), infoFn func(strin
|
||||
continue
|
||||
}
|
||||
|
||||
// Unknown event format from other backends (turn.started/assistant/user); ignore.
|
||||
continue
|
||||
// Unknown event format
|
||||
warnFn(fmt.Sprintf("Unknown event format: %s", truncateBytes(line, 100)))
|
||||
}
|
||||
|
||||
switch {
|
||||
case opencodeMessage.Len() > 0:
|
||||
message = opencodeMessage.String()
|
||||
case geminiBuffer.Len() > 0:
|
||||
message = geminiBuffer.String()
|
||||
case claudeMessage != "":
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseJSONStream_Opencode(t *testing.T) {
|
||||
input := `{"type":"step_start","timestamp":1768187730683,"sessionID":"ses_44fced3c7ffe83sZpzY1rlQka3","part":{"id":"prt_bb0339afa001NTqoJ2NS8x91zP","sessionID":"ses_44fced3c7ffe83sZpzY1rlQka3","messageID":"msg_bb033866f0011oZxTqvfy0TKtS","type":"step-start","snapshot":"904f0fd58c125b79e60f0993e38f9d9f6200bf47"}}
|
||||
{"type":"text","timestamp":1768187744432,"sessionID":"ses_44fced3c7ffe83sZpzY1rlQka3","part":{"id":"prt_bb0339cb5001QDd0Lh0PzFZpa3","sessionID":"ses_44fced3c7ffe83sZpzY1rlQka3","messageID":"msg_bb033866f0011oZxTqvfy0TKtS","type":"text","text":"Hello from opencode"}}
|
||||
{"type":"step_finish","timestamp":1768187744471,"sessionID":"ses_44fced3c7ffe83sZpzY1rlQka3","part":{"id":"prt_bb033d0af0019VRZzpO2OVW1na","sessionID":"ses_44fced3c7ffe83sZpzY1rlQka3","messageID":"msg_bb033866f0011oZxTqvfy0TKtS","type":"step-finish","reason":"stop","snapshot":"904f0fd58c125b79e60f0993e38f9d9f6200bf47","cost":0}}`
|
||||
|
||||
message, threadID := parseJSONStream(strings.NewReader(input))
|
||||
|
||||
if threadID != "ses_44fced3c7ffe83sZpzY1rlQka3" {
|
||||
t.Errorf("threadID = %q, want %q", threadID, "ses_44fced3c7ffe83sZpzY1rlQka3")
|
||||
}
|
||||
if message != "Hello from opencode" {
|
||||
t.Errorf("message = %q, want %q", message, "Hello from opencode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJSONStream_Opencode_MultipleTextEvents(t *testing.T) {
|
||||
input := `{"type":"text","sessionID":"ses_123","part":{"type":"text","text":"Part 1"}}
|
||||
{"type":"text","sessionID":"ses_123","part":{"type":"text","text":" Part 2"}}
|
||||
{"type":"step_finish","sessionID":"ses_123","part":{"type":"step-finish","reason":"stop"}}`
|
||||
|
||||
message, threadID := parseJSONStream(strings.NewReader(input))
|
||||
|
||||
if threadID != "ses_123" {
|
||||
t.Errorf("threadID = %q, want %q", threadID, "ses_123")
|
||||
}
|
||||
if message != "Part 1 Part 2" {
|
||||
t.Errorf("message = %q, want %q", message, "Part 1 Part 2")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJSONStream_Opencode_NoStopReason(t *testing.T) {
|
||||
input := `{"type":"text","sessionID":"ses_456","part":{"type":"text","text":"Content"}}
|
||||
{"type":"step_finish","sessionID":"ses_456","part":{"type":"step-finish","reason":"tool-calls"}}`
|
||||
|
||||
message, threadID := parseJSONStream(strings.NewReader(input))
|
||||
|
||||
if threadID != "ses_456" {
|
||||
t.Errorf("threadID = %q, want %q", threadID, "ses_456")
|
||||
}
|
||||
if message != "Content" {
|
||||
t.Errorf("message = %q, want %q", message, "Content")
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBackendParseJSONStream_UnknownEventsAreSilent(t *testing.T) {
|
||||
input := strings.Join([]string{
|
||||
`{"type":"turn.started"}`,
|
||||
`{"type":"assistant","text":"hi"}`,
|
||||
`{"type":"user","text":"yo"}`,
|
||||
`{"type":"item.completed","item":{"type":"agent_message","text":"ok"}}`,
|
||||
}, "\n")
|
||||
|
||||
var infos []string
|
||||
infoFn := func(msg string) { infos = append(infos, msg) }
|
||||
|
||||
message, threadID := parseJSONStreamInternal(strings.NewReader(input), nil, infoFn, nil, nil)
|
||||
if message != "ok" {
|
||||
t.Fatalf("message=%q, want %q (infos=%v)", message, "ok", infos)
|
||||
}
|
||||
if threadID != "" {
|
||||
t.Fatalf("threadID=%q, want empty (infos=%v)", threadID, infos)
|
||||
}
|
||||
|
||||
for _, msg := range infos {
|
||||
if strings.Contains(msg, "Agent event:") {
|
||||
t.Fatalf("unexpected log for unknown event: %q", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,10 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
findProcess = os.FindProcess
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
getProcessTimes = kernel32.NewProc("GetProcessTimes")
|
||||
fileTimeToUnixFn = fileTimeToUnix
|
||||
findProcess = os.FindProcess
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
getProcessTimes = kernel32.NewProc("GetProcessTimes")
|
||||
fileTimeToUnixFn = fileTimeToUnix
|
||||
)
|
||||
|
||||
// isProcessRunning returns true if a process with the given pid is running on Windows.
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIsProcessRunning(t *testing.T) {
|
||||
t.Run("boundary values", func(t *testing.T) {
|
||||
if isProcessRunning(0) {
|
||||
t.Fatalf("expected pid 0 to be reported as not running")
|
||||
}
|
||||
if isProcessRunning(-1) {
|
||||
t.Fatalf("expected pid -1 to be reported as not running")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("current process", func(t *testing.T) {
|
||||
if !isProcessRunning(os.Getpid()) {
|
||||
t.Fatalf("expected current process (pid=%d) to be running", os.Getpid())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fake pid", func(t *testing.T) {
|
||||
const nonexistentPID = 1 << 30
|
||||
if isProcessRunning(nonexistentPID) {
|
||||
t.Fatalf("expected pid %d to be reported as not running", nonexistentPID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetProcessStartTimeReadsProcStat(t *testing.T) {
|
||||
start := getProcessStartTime(os.Getpid())
|
||||
if start.IsZero() {
|
||||
t.Fatalf("expected non-zero start time for current process")
|
||||
}
|
||||
if start.After(time.Now().Add(5 * time.Second)) {
|
||||
t.Fatalf("start time is unexpectedly in the future: %v", start)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProcessStartTimeInvalidData(t *testing.T) {
|
||||
if !getProcessStartTime(0).IsZero() {
|
||||
t.Fatalf("expected zero time for pid 0")
|
||||
}
|
||||
if !getProcessStartTime(-1).IsZero() {
|
||||
t.Fatalf("expected zero time for negative pid")
|
||||
}
|
||||
if !getProcessStartTime(1 << 30).IsZero() {
|
||||
t.Fatalf("expected zero time for non-existent pid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBootTimeParsesBtime(t *testing.T) {
|
||||
t.Skip("getBootTime is only implemented on Unix-like systems")
|
||||
}
|
||||
|
||||
func TestGetBootTimeInvalidData(t *testing.T) {
|
||||
t.Skip("getBootTime is only implemented on Unix-like systems")
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWrapTaskWithAgentPrompt(t *testing.T) {
|
||||
got := wrapTaskWithAgentPrompt("P", "do")
|
||||
want := "<agent-prompt>\nP\n</agent-prompt>\n\ndo"
|
||||
if got != want {
|
||||
t.Fatalf("wrapTaskWithAgentPrompt mismatch:\n got=%q\nwant=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_EmptyPath(t *testing.T) {
|
||||
for _, allowOutside := range []bool{false, true} {
|
||||
got, err := readAgentPromptFile(" ", allowOutside)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error (allowOutside=%v): %v", allowOutside, err)
|
||||
}
|
||||
if got != "" {
|
||||
t.Fatalf("expected empty result (allowOutside=%v), got %q", allowOutside, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_ExplicitAbsolutePath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "prompt.md")
|
||||
if err := os.WriteFile(path, []byte("LINE1\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
got, err := readAgentPromptFile(path, true)
|
||||
if err != nil {
|
||||
t.Fatalf("readAgentPromptFile error: %v", err)
|
||||
}
|
||||
if got != "LINE1" {
|
||||
t.Fatalf("got %q, want %q", got, "LINE1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_ExplicitTildeExpansion(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
path := filepath.Join(home, "prompt.md")
|
||||
if err := os.WriteFile(path, []byte("P\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
got, err := readAgentPromptFile("~/prompt.md", true)
|
||||
if err != nil {
|
||||
t.Fatalf("readAgentPromptFile error: %v", err)
|
||||
}
|
||||
if got != "P" {
|
||||
t.Fatalf("got %q, want %q", got, "P")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_RestrictedAllowsClaudeDir(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
claudeDir := filepath.Join(home, ".claude")
|
||||
if err := os.MkdirAll(claudeDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
path := filepath.Join(claudeDir, "prompt.md")
|
||||
if err := os.WriteFile(path, []byte("OK\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
got, err := readAgentPromptFile("~/.claude/prompt.md", false)
|
||||
if err != nil {
|
||||
t.Fatalf("readAgentPromptFile error: %v", err)
|
||||
}
|
||||
if got != "OK" {
|
||||
t.Fatalf("got %q, want %q", got, "OK")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_RestrictedRejectsOutsideClaudeDir(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
path := filepath.Join(home, "prompt.md")
|
||||
if err := os.WriteFile(path, []byte("NO\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
if _, err := readAgentPromptFile("~/prompt.md", false); err == nil {
|
||||
t.Fatalf("expected error for prompt file outside ~/.claude, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_RestrictedRejectsTraversal(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
path := filepath.Join(home, "secret.md")
|
||||
if err := os.WriteFile(path, []byte("SECRET\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
|
||||
if _, err := readAgentPromptFile("~/.claude/../secret.md", false); err == nil {
|
||||
t.Fatalf("expected traversal to be rejected, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_NotFound(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
claudeDir := filepath.Join(home, ".claude")
|
||||
if err := os.MkdirAll(claudeDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
|
||||
_, err := readAgentPromptFile("~/.claude/missing.md", false)
|
||||
if err == nil || !os.IsNotExist(err) {
|
||||
t.Fatalf("expected not-exist error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAgentPromptFile_PermissionDenied(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("chmod-based permission test is not reliable on Windows")
|
||||
}
|
||||
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
claudeDir := filepath.Join(home, ".claude")
|
||||
if err := os.MkdirAll(claudeDir, 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
}
|
||||
path := filepath.Join(claudeDir, "private.md")
|
||||
if err := os.WriteFile(path, []byte("PRIVATE\n"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
if err := os.Chmod(path, 0o000); err != nil {
|
||||
t.Fatalf("Chmod: %v", err)
|
||||
}
|
||||
|
||||
_, err := readAgentPromptFile("~/.claude/private.md", false)
|
||||
if err == nil {
|
||||
t.Fatalf("expected permission error, got nil")
|
||||
}
|
||||
if !os.IsPermission(err) && !strings.Contains(strings.ToLower(err.Error()), "permission") {
|
||||
t.Fatalf("expected permission denied, got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//go:build unix || darwin || linux
|
||||
// +build unix darwin linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// sendTermSignal sends SIGTERM for graceful shutdown on Unix.
|
||||
func sendTermSignal(proc processHandle) error {
|
||||
if proc == nil {
|
||||
return nil
|
||||
}
|
||||
return proc.Signal(syscall.SIGTERM)
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// sendTermSignal on Windows directly kills the process.
|
||||
// SIGTERM is not supported on Windows.
|
||||
func sendTermSignal(proc processHandle) error {
|
||||
if proc == nil {
|
||||
return nil
|
||||
}
|
||||
pid := proc.Pid()
|
||||
if pid > 0 {
|
||||
// Kill the whole process tree to avoid leaving inheriting child processes around.
|
||||
// This also helps prevent exec.Cmd.Wait() from blocking on stderr/stdout pipes held open by children.
|
||||
taskkill := "taskkill"
|
||||
if root := os.Getenv("SystemRoot"); root != "" {
|
||||
taskkill = filepath.Join(root, "System32", "taskkill.exe")
|
||||
}
|
||||
cmd := exec.Command(taskkill, "/PID", strconv.Itoa(pid), "/T", "/F")
|
||||
cmd.Stdout = io.Discard
|
||||
cmd.Stderr = io.Discard
|
||||
if err := cmd.Run(); err == nil {
|
||||
return nil
|
||||
}
|
||||
if err := killProcessTree(pid); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return proc.Kill()
|
||||
}
|
||||
|
||||
func killProcessTree(pid int) error {
|
||||
if pid <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
wmic := "wmic"
|
||||
if root := os.Getenv("SystemRoot"); root != "" {
|
||||
wmic = filepath.Join(root, "System32", "wbem", "WMIC.exe")
|
||||
}
|
||||
|
||||
queryChildren := "(ParentProcessId=" + strconv.Itoa(pid) + ")"
|
||||
listCmd := exec.Command(wmic, "process", "where", queryChildren, "get", "ProcessId", "/VALUE")
|
||||
listCmd.Stderr = io.Discard
|
||||
out, err := listCmd.Output()
|
||||
if err == nil {
|
||||
for _, childPID := range parseWMICPIDs(out) {
|
||||
_ = killProcessTree(childPID)
|
||||
}
|
||||
}
|
||||
|
||||
querySelf := "(ProcessId=" + strconv.Itoa(pid) + ")"
|
||||
termCmd := exec.Command(wmic, "process", "where", querySelf, "call", "terminate")
|
||||
termCmd.Stdout = io.Discard
|
||||
termCmd.Stderr = io.Discard
|
||||
if termErr := termCmd.Run(); termErr != nil && err == nil {
|
||||
err = termErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func parseWMICPIDs(out []byte) []int {
|
||||
const prefix = "ProcessId="
|
||||
var pids []int
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, prefix) {
|
||||
continue
|
||||
}
|
||||
n, err := strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(line, prefix)))
|
||||
if err != nil || n <= 0 {
|
||||
continue
|
||||
}
|
||||
pids = append(pids, n)
|
||||
}
|
||||
return pids
|
||||
}
|
||||
@@ -273,6 +273,30 @@ func farewell(name string) string {
|
||||
return "goodbye " + name
|
||||
}
|
||||
|
||||
// extractMessageSummary extracts a brief summary from task output
|
||||
// Returns first meaningful line or truncated content up to maxLen chars
|
||||
func extractMessageSummary(message string, maxLen int) string {
|
||||
if message == "" || maxLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Try to find a meaningful summary line
|
||||
lines := strings.Split(message, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
// Skip empty lines and common noise
|
||||
if line == "" || strings.HasPrefix(line, "```") || strings.HasPrefix(line, "---") {
|
||||
continue
|
||||
}
|
||||
// Found a meaningful line
|
||||
return safeTruncate(line, maxLen)
|
||||
}
|
||||
|
||||
// Fallback: truncate entire message
|
||||
clean := strings.TrimSpace(message)
|
||||
return safeTruncate(clean, maxLen)
|
||||
}
|
||||
|
||||
// extractCoverageFromLines extracts coverage from pre-split lines.
|
||||
func extractCoverageFromLines(lines []string) string {
|
||||
if len(lines) == 0 {
|
||||
@@ -568,6 +592,15 @@ func extractKeyOutputFromLines(lines []string, maxLen int) string {
|
||||
return safeTruncate(clean, maxLen)
|
||||
}
|
||||
|
||||
// extractKeyOutput extracts a brief summary of what the task accomplished
|
||||
// Looks for summary lines, first meaningful sentence, or truncates message
|
||||
func extractKeyOutput(message string, maxLen int) string {
|
||||
if message == "" || maxLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
return extractKeyOutputFromLines(strings.Split(message, "\n"), maxLen)
|
||||
}
|
||||
|
||||
// extractCoverageGap extracts what's missing from coverage reports
|
||||
// Looks for uncovered lines, branches, or functions
|
||||
func extractCoverageGap(message string) string {
|
||||
|
||||
48
config.json
48
config.json
@@ -108,54 +108,6 @@
|
||||
"description": "Copy development commands documentation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"omo": {
|
||||
"enabled": false,
|
||||
"description": "OmO multi-agent orchestration with Sisyphus coordinator",
|
||||
"operations": [
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/SKILL.md",
|
||||
"target": "skills/omo/SKILL.md",
|
||||
"description": "Install omo skill"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/references/oracle.md",
|
||||
"target": "skills/omo/references/oracle.md",
|
||||
"description": "Install oracle agent prompt"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/references/librarian.md",
|
||||
"target": "skills/omo/references/librarian.md",
|
||||
"description": "Install librarian agent prompt"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/references/explore.md",
|
||||
"target": "skills/omo/references/explore.md",
|
||||
"description": "Install explore agent prompt"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/references/frontend-ui-ux-engineer.md",
|
||||
"target": "skills/omo/references/frontend-ui-ux-engineer.md",
|
||||
"description": "Install frontend-ui-ux-engineer agent prompt"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/references/document-writer.md",
|
||||
"target": "skills/omo/references/document-writer.md",
|
||||
"description": "Install document-writer agent prompt"
|
||||
},
|
||||
{
|
||||
"type": "copy_file",
|
||||
"source": "skills/omo/references/develop.md",
|
||||
"target": "skills/omo/references/develop.md",
|
||||
"description": "Install develop agent prompt"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "dev",
|
||||
"description": "Lightweight development workflow with requirements clarification, parallel codex execution, and mandatory 90% test coverage",
|
||||
"version": "5.6.1",
|
||||
"author": {
|
||||
"name": "cexll",
|
||||
"email": "cexll@cexll.com"
|
||||
}
|
||||
}
|
||||
44
development-essentials/.claude-plugin/marketplace.json
Normal file
44
development-essentials/.claude-plugin/marketplace.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "development-essentials",
|
||||
"source": "./",
|
||||
"description": "Essential development commands for coding, debugging, testing, optimization, and documentation",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"code",
|
||||
"debug",
|
||||
"test",
|
||||
"optimize",
|
||||
"review",
|
||||
"bugfix",
|
||||
"refactor",
|
||||
"documentation"
|
||||
],
|
||||
"category": "essentials",
|
||||
"strict": false,
|
||||
"commands": [
|
||||
"./commands/code.md",
|
||||
"./commands/debug.md",
|
||||
"./commands/test.md",
|
||||
"./commands/optimize.md",
|
||||
"./commands/review.md",
|
||||
"./commands/bugfix.md",
|
||||
"./commands/refactor.md",
|
||||
"./commands/docs.md",
|
||||
"./commands/ask.md",
|
||||
"./commands/think.md"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/code.md",
|
||||
"./agents/bugfix.md",
|
||||
"./agents/bugfix-verify.md",
|
||||
"./agents/optimize.md",
|
||||
"./agents/debug.md"
|
||||
]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "essentials",
|
||||
"description": "Essential development commands for coding, debugging, testing, optimization, and documentation",
|
||||
"version": "5.6.1",
|
||||
"author": {
|
||||
"name": "cexll",
|
||||
"email": "cexll@cexll.com"
|
||||
}
|
||||
}
|
||||
@@ -322,8 +322,6 @@ Error: dependency backend_1701234567 failed
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CODEX_TIMEOUT` | 7200000 | Timeout in milliseconds |
|
||||
| `CODEX_BYPASS_SANDBOX` | true | Bypass Codex sandbox/approval. Set `false` to disable |
|
||||
| `CODEAGENT_SKIP_PERMISSIONS` | true | Skip Claude permission prompts. Set `false` to disable |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
15
install.bat
15
install.bat
@@ -117,18 +117,11 @@ if "!ALREADY_IN_USERPATH!"=="1" (
|
||||
set "USER_PATH_NEW=!PCT!USERPROFILE!PCT!\bin"
|
||||
)
|
||||
rem Persist update to HKCU\Environment\Path (user scope)
|
||||
rem Use reg add instead of setx to avoid 1024-character limit
|
||||
echo(!USER_PATH_NEW! | findstr /C:"\"" /C:"!" >nul
|
||||
if not errorlevel 1 (
|
||||
echo WARNING: Your PATH contains quotes or exclamation marks that may cause issues.
|
||||
echo Skipping automatic PATH update. Please add %%USERPROFILE%%\bin to your PATH manually.
|
||||
setx Path "!USER_PATH_NEW!" >nul
|
||||
if errorlevel 1 (
|
||||
echo WARNING: Failed to append %%USERPROFILE%%\bin to your user PATH.
|
||||
) else (
|
||||
reg add "HKCU\Environment" /v Path /t REG_EXPAND_SZ /d "!USER_PATH_NEW!" /f >nul
|
||||
if errorlevel 1 (
|
||||
echo WARNING: Failed to append %%USERPROFILE%%\bin to your user PATH.
|
||||
) else (
|
||||
echo Added %%USERPROFILE%%\bin to your user PATH.
|
||||
)
|
||||
echo Added %%USERPROFILE%%\bin to your user PATH.
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
33
requirements-driven-workflow/.claude-plugin/marketplace.json
Normal file
33
requirements-driven-workflow/.claude-plugin/marketplace.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "requirements-driven-development",
|
||||
"source": "./",
|
||||
"description": "Streamlined requirements-driven development workflow with 90% quality gates for practical feature implementation",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Claude Code Dev Workflows",
|
||||
"url": "https://github.com/cexll/myclaude"
|
||||
},
|
||||
"homepage": "https://github.com/cexll/myclaude",
|
||||
"repository": "https://github.com/cexll/myclaude",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"requirements",
|
||||
"workflow",
|
||||
"automation",
|
||||
"quality-gates",
|
||||
"feature-development",
|
||||
"agile",
|
||||
"specifications"
|
||||
],
|
||||
"category": "workflows",
|
||||
"strict": false,
|
||||
"commands": [
|
||||
"./commands/requirements-pilot.md"
|
||||
],
|
||||
"agents": [
|
||||
"./agents/requirements-generate.md",
|
||||
"./agents/requirements-code.md",
|
||||
"./agents/requirements-testing.md",
|
||||
"./agents/requirements-review.md"
|
||||
]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "requirements",
|
||||
"description": "Requirements-driven development workflow with quality gates for practical feature implementation",
|
||||
"version": "5.6.1",
|
||||
"author": {
|
||||
"name": "cexll",
|
||||
"email": "cexll@cexll.com"
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
name: browser
|
||||
description: This skill should be used for browser automation tasks using Chrome DevTools Protocol (CDP). Triggers when users need to launch Chrome with remote debugging, navigate pages, execute JavaScript in browser context, capture screenshots, or interactively select DOM elements. No MCP server required.
|
||||
---
|
||||
|
||||
# Browser Automation
|
||||
|
||||
Minimal Chrome DevTools Protocol (CDP) helpers for browser automation without MCP server setup.
|
||||
|
||||
## Setup
|
||||
|
||||
Install dependencies before first use:
|
||||
|
||||
```bash
|
||||
npm install --prefix ~/.claude/skills/browser/browser ws
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
All scripts connect to Chrome on `localhost:9222`.
|
||||
|
||||
### start.js - Launch Chrome
|
||||
|
||||
```bash
|
||||
scripts/start.js # Fresh profile
|
||||
scripts/start.js --profile # Use persistent profile (keeps cookies/auth)
|
||||
```
|
||||
|
||||
### nav.js - Navigate
|
||||
|
||||
```bash
|
||||
scripts/nav.js https://example.com # Navigate current tab
|
||||
scripts/nav.js https://example.com --new # Open in new tab
|
||||
```
|
||||
|
||||
### eval.js - Execute JavaScript
|
||||
|
||||
```bash
|
||||
scripts/eval.js 'document.title'
|
||||
scripts/eval.js '(() => { const x = 1; return x + 1; })()'
|
||||
```
|
||||
|
||||
Use single expressions or IIFE for multiple statements.
|
||||
|
||||
### screenshot.js - Capture Screenshot
|
||||
|
||||
```bash
|
||||
scripts/screenshot.js
|
||||
```
|
||||
|
||||
Returns `{ path, filename }` of saved PNG in temp directory.
|
||||
|
||||
### pick.js - Visual Element Picker
|
||||
|
||||
```bash
|
||||
scripts/pick.js "Click the submit button"
|
||||
```
|
||||
|
||||
Returns element metadata: tag, id, classes, text, href, selector, rect.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Launch Chrome: `scripts/start.js --profile` for authenticated sessions
|
||||
2. Navigate: `scripts/nav.js <url>`
|
||||
3. Inspect: `scripts/eval.js 'document.querySelector(...)'`
|
||||
4. Capture: `scripts/screenshot.js` or `scripts/pick.js`
|
||||
5. Return gathered data
|
||||
|
||||
## Key Points
|
||||
|
||||
- All operations run locally - credentials never leave the machine
|
||||
- Use `--profile` flag to preserve cookies and auth tokens
|
||||
- Scripts return structured JSON for agent consumption
|
||||
Binary file not shown.
33
skills/browser/package-lock.json
generated
33
skills/browser/package-lock.json
generated
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "browser",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Execute JavaScript in the active browser tab
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const code = process.argv[2];
|
||||
if (!code) {
|
||||
console.error('Usage: eval.js <javascript-expression>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function getTargets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get('http://localhost:9222/json', res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(JSON.parse(data)));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const targets = await getTargets();
|
||||
const page = targets.find(t => t.type === 'page');
|
||||
if (!page) throw new Error('No active page found');
|
||||
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
|
||||
ws.on('open', () => {
|
||||
ws.send(JSON.stringify({
|
||||
id: 1,
|
||||
method: 'Runtime.evaluate',
|
||||
params: {
|
||||
expression: code,
|
||||
returnByValue: true,
|
||||
awaitPromise: true
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', data => {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.id === 1) {
|
||||
ws.close();
|
||||
if (msg.result.exceptionDetails) {
|
||||
console.error('Error:', msg.result.exceptionDetails.text);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(JSON.stringify(msg.result.result.value ?? msg.result.result));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', e => {
|
||||
console.error('WebSocket error:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Navigate to URL in current or new tab
|
||||
const http = require('http');
|
||||
|
||||
const url = process.argv[2];
|
||||
const newTab = process.argv.includes('--new');
|
||||
|
||||
if (!url) {
|
||||
console.error('Usage: nav.js <url> [--new]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function getTargets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get('http://localhost:9222/json', res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(JSON.parse(data)));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function createTab(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`http://localhost:9222/json/new?${encodeURIComponent(url)}`, res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(JSON.parse(data)));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function navigate(targetId, url) {
|
||||
const WebSocket = require('ws');
|
||||
const targets = await getTargets();
|
||||
const target = targets.find(t => t.id === targetId);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
ws.on('open', () => {
|
||||
ws.send(JSON.stringify({ id: 1, method: 'Page.navigate', params: { url } }));
|
||||
});
|
||||
ws.on('message', data => {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.id === 1) {
|
||||
ws.close();
|
||||
resolve(msg.result);
|
||||
}
|
||||
});
|
||||
ws.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
if (newTab) {
|
||||
const tab = await createTab(url);
|
||||
console.log(JSON.stringify({ action: 'created', tabId: tab.id, url }));
|
||||
} else {
|
||||
const targets = await getTargets();
|
||||
const page = targets.find(t => t.type === 'page');
|
||||
if (!page) throw new Error('No active page found');
|
||||
await navigate(page.id, url);
|
||||
console.log(JSON.stringify({ action: 'navigated', tabId: page.id, url }));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Visual element picker - click to select DOM nodes
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const hint = process.argv[2] || 'Click an element to select it';
|
||||
|
||||
async function getTargets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get('http://localhost:9222/json', res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(JSON.parse(data)));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
const pickerScript = `
|
||||
(function(hint) {
|
||||
return new Promise(resolve => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;z-index:999999;cursor:crosshair;';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.textContent = hint;
|
||||
label.style.cssText = 'position:fixed;top:10px;left:50%;transform:translateX(-50%);background:#333;color:#fff;padding:8px 16px;border-radius:4px;z-index:1000000;font:14px sans-serif;';
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
document.body.appendChild(label);
|
||||
|
||||
overlay.onclick = e => {
|
||||
overlay.remove();
|
||||
label.remove();
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!el) return resolve(null);
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
resolve({
|
||||
tag: el.tagName.toLowerCase(),
|
||||
id: el.id || null,
|
||||
classes: [...el.classList],
|
||||
text: el.textContent?.slice(0, 100)?.trim() || null,
|
||||
href: el.href || null,
|
||||
selector: el.id ? '#' + el.id : el.className ? el.tagName.toLowerCase() + '.' + [...el.classList].join('.') : el.tagName.toLowerCase(),
|
||||
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
|
||||
});
|
||||
};
|
||||
});
|
||||
})`;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const targets = await getTargets();
|
||||
const page = targets.find(t => t.type === 'page');
|
||||
if (!page) throw new Error('No active page found');
|
||||
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
|
||||
ws.on('open', () => {
|
||||
ws.send(JSON.stringify({
|
||||
id: 1,
|
||||
method: 'Runtime.evaluate',
|
||||
params: {
|
||||
expression: `${pickerScript}(${JSON.stringify(hint)})`,
|
||||
returnByValue: true,
|
||||
awaitPromise: true
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', data => {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.id === 1) {
|
||||
ws.close();
|
||||
console.log(JSON.stringify(msg.result.result.value, null, 2));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', e => {
|
||||
console.error('WebSocket error:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Capture screenshot of the active browser tab
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
async function getTargets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get('http://localhost:9222/json', res => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(JSON.parse(data)));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const targets = await getTargets();
|
||||
const page = targets.find(t => t.type === 'page');
|
||||
if (!page) throw new Error('No active page found');
|
||||
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
|
||||
ws.on('open', () => {
|
||||
ws.send(JSON.stringify({
|
||||
id: 1,
|
||||
method: 'Page.captureScreenshot',
|
||||
params: { format: 'png' }
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', data => {
|
||||
const msg = JSON.parse(data);
|
||||
if (msg.id === 1) {
|
||||
ws.close();
|
||||
const filename = `screenshot-${Date.now()}.png`;
|
||||
const filepath = path.join(os.tmpdir(), filename);
|
||||
fs.writeFileSync(filepath, Buffer.from(msg.result.data, 'base64'));
|
||||
console.log(JSON.stringify({ path: filepath, filename }));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', e => {
|
||||
console.error('WebSocket error:', e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Launch Chrome with remote debugging on port 9222
|
||||
const { execSync, spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
const useProfile = process.argv.includes('--profile');
|
||||
const port = 9222;
|
||||
|
||||
// Find Chrome executable
|
||||
const chromePaths = {
|
||||
darwin: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
linux: '/usr/bin/google-chrome',
|
||||
win32: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
|
||||
};
|
||||
const chromePath = chromePaths[process.platform];
|
||||
|
||||
// Build args
|
||||
const args = [
|
||||
`--remote-debugging-port=${port}`,
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check'
|
||||
];
|
||||
|
||||
if (useProfile) {
|
||||
const profileDir = path.join(os.homedir(), '.chrome-debug-profile');
|
||||
args.push(`--user-data-dir=${profileDir}`);
|
||||
} else {
|
||||
args.push(`--user-data-dir=${path.join(os.tmpdir(), 'chrome-debug-' + Date.now())}`);
|
||||
}
|
||||
|
||||
console.log(`Starting Chrome on port ${port}${useProfile ? ' (with profile)' : ''}...`);
|
||||
const chrome = spawn(chromePath, args, { detached: true, stdio: 'ignore' });
|
||||
chrome.unref();
|
||||
console.log(`Chrome launched (PID: ${chrome.pid})`);
|
||||
@@ -19,22 +19,22 @@ Execute codeagent-wrapper commands with pluggable AI backends (Codex, Claude, Ge
|
||||
|
||||
**HEREDOC syntax** (recommended):
|
||||
```bash
|
||||
codeagent-wrapper --backend codex - [working_dir] <<'EOF'
|
||||
codeagent-wrapper - [working_dir] <<'EOF'
|
||||
<task content here>
|
||||
EOF
|
||||
```
|
||||
|
||||
**With backend selection**:
|
||||
```bash
|
||||
codeagent-wrapper --backend claude - . <<'EOF'
|
||||
codeagent-wrapper --backend claude - <<'EOF'
|
||||
<task content here>
|
||||
EOF
|
||||
```
|
||||
|
||||
**Simple tasks**:
|
||||
```bash
|
||||
codeagent-wrapper --backend codex "simple task" [working_dir]
|
||||
codeagent-wrapper --backend gemini "simple task" [working_dir]
|
||||
codeagent-wrapper "simple task" [working_dir]
|
||||
codeagent-wrapper --backend gemini "simple task"
|
||||
```
|
||||
|
||||
## Backends
|
||||
@@ -73,7 +73,7 @@ codeagent-wrapper --backend gemini "simple task" [working_dir]
|
||||
|
||||
- `task` (required): Task description, supports `@file` references
|
||||
- `working_dir` (optional): Working directory (default: current)
|
||||
- `--backend` (required): Select AI backend (codex/claude/gemini)
|
||||
- `--backend` (optional): Select AI backend (codex/claude/gemini, default: codex)
|
||||
- **Note**: Claude backend only adds `--dangerously-skip-permissions` when explicitly enabled
|
||||
|
||||
## Return Format
|
||||
@@ -88,8 +88,8 @@ SESSION_ID: 019a7247-ac9d-71f3-89e2-a823dbd8fd14
|
||||
## Resume Session
|
||||
|
||||
```bash
|
||||
# Resume with codex backend
|
||||
codeagent-wrapper --backend codex resume <session_id> - <<'EOF'
|
||||
# Resume with default backend
|
||||
codeagent-wrapper resume <session_id> - <<'EOF'
|
||||
<follow-up task>
|
||||
EOF
|
||||
|
||||
@@ -174,8 +174,6 @@ Bash tool parameters:
|
||||
EOF
|
||||
- timeout: 7200000
|
||||
- description: <brief description>
|
||||
|
||||
Note: --backend is required (codex/claude/gemini)
|
||||
```
|
||||
|
||||
**Parallel Tasks**:
|
||||
@@ -192,36 +190,8 @@ Bash tool parameters:
|
||||
EOF
|
||||
- timeout: 7200000
|
||||
- description: <brief description>
|
||||
|
||||
Note: Global --backend is required; per-task backend is optional
|
||||
```
|
||||
|
||||
## Critical Rules
|
||||
|
||||
**NEVER kill codeagent processes.** Long-running tasks are normal. Instead:
|
||||
|
||||
1. **Check task status via log file**:
|
||||
```bash
|
||||
# View real-time output
|
||||
tail -f /tmp/claude/<workdir>/tasks/<task_id>.output
|
||||
|
||||
# Check if task is still running
|
||||
cat /tmp/claude/<workdir>/tasks/<task_id>.output | tail -50
|
||||
```
|
||||
|
||||
2. **Wait with timeout**:
|
||||
```bash
|
||||
# Use TaskOutput tool with block=true and timeout
|
||||
TaskOutput(task_id="<id>", block=true, timeout=300000)
|
||||
```
|
||||
|
||||
3. **Check process without killing**:
|
||||
```bash
|
||||
ps aux | grep codeagent-wrapper | grep -v grep
|
||||
```
|
||||
|
||||
**Why:** codeagent tasks often take 2-10 minutes. Killing them wastes API costs and loses progress.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
- **Claude Backend**: Permission checks enabled by default
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "omo",
|
||||
"description": "Multi-agent orchestration for code analysis, bug investigation, fix planning, and implementation with intelligent routing to specialized agents",
|
||||
"version": "5.6.1",
|
||||
"author": {
|
||||
"name": "cexll",
|
||||
"email": "cexll@cexll.com"
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
# OmO Multi-Agent Orchestration
|
||||
|
||||
OmO (Oh-My-OpenCode) is a multi-agent orchestration skill that delegates tasks to specialized agents based on routing signals.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
python3 install.py --module omo
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```
|
||||
/omo <your task>
|
||||
```
|
||||
|
||||
## Agent Hierarchy
|
||||
|
||||
| Agent | Role | Backend | Model |
|
||||
|-------|------|---------|-------|
|
||||
| oracle | Technical advisor | claude | claude-opus-4-5-20251101 |
|
||||
| librarian | External research | claude | claude-sonnet-4-5-20250929 |
|
||||
| explore | Codebase search | opencode | opencode/grok-code |
|
||||
| develop | Code implementation | codex | gpt-5.2 |
|
||||
| frontend-ui-ux-engineer | UI/UX specialist | gemini | gemini-3-pro-high |
|
||||
| document-writer | Documentation | gemini | gemini-3-flash |
|
||||
|
||||
## How It Works
|
||||
|
||||
1. `/omo` analyzes your request via routing signals
|
||||
2. Based on task type, it either:
|
||||
- Answers directly (analysis/explanation tasks - no code changes)
|
||||
- Delegates to specialized agents (implementation tasks)
|
||||
- Fires parallel agents (exploration + research)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Refactoring
|
||||
/omo Help me refactor this authentication module
|
||||
|
||||
# Feature development
|
||||
/omo I need to add a new payment feature with frontend UI and backend API
|
||||
|
||||
# Research
|
||||
/omo What authentication scheme does this project use?
|
||||
```
|
||||
|
||||
## Agent Delegation
|
||||
|
||||
Delegates via codeagent-wrapper with full Context Pack:
|
||||
|
||||
```bash
|
||||
codeagent-wrapper --agent oracle - . <<'EOF'
|
||||
## Original User Request
|
||||
Analyze the authentication architecture and recommend improvements.
|
||||
|
||||
## Context Pack (include anything relevant; write "None" if absent)
|
||||
- Explore output: [paste explore output if available]
|
||||
- Librarian output: None
|
||||
- Oracle output: None
|
||||
|
||||
## Current Task
|
||||
Review auth architecture, identify risks, propose minimal improvements.
|
||||
|
||||
## Acceptance Criteria
|
||||
Output: recommendation, action plan, risk assessment, effort estimate.
|
||||
EOF
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Agent-model mappings are configured in `~/.codeagent/models.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"default_backend": "codex",
|
||||
"default_model": "gpt-5.2",
|
||||
"agents": {
|
||||
"oracle": {
|
||||
"backend": "claude",
|
||||
"model": "claude-opus-4-5-20251101",
|
||||
"description": "Technical advisor",
|
||||
"yolo": true
|
||||
},
|
||||
"librarian": {
|
||||
"backend": "claude",
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"description": "Researcher",
|
||||
"yolo": true
|
||||
},
|
||||
"explore": {
|
||||
"backend": "opencode",
|
||||
"model": "opencode/grok-code",
|
||||
"description": "Code search"
|
||||
},
|
||||
"frontend-ui-ux-engineer": {
|
||||
"backend": "gemini",
|
||||
"model": "gemini-3-pro-high",
|
||||
"description": "Frontend engineer"
|
||||
},
|
||||
"document-writer": {
|
||||
"backend": "gemini",
|
||||
"model": "gemini-3-flash",
|
||||
"description": "Documentation"
|
||||
},
|
||||
"develop": {
|
||||
"backend": "codex",
|
||||
"model": "gpt-5.2",
|
||||
"description": "codex develop",
|
||||
"yolo": true,
|
||||
"reasoning": "xhigh"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- codeagent-wrapper with `--agent` support
|
||||
- Backend CLIs: claude, opencode, codex, gemini
|
||||
@@ -1,279 +0,0 @@
|
||||
---
|
||||
name: omo
|
||||
description: Use this skill when you see `/omo`. Multi-agent orchestration for "code analysis / bug investigation / fix planning / implementation". Choose the minimal agent set and order based on task type + risk; recipes below show common patterns.
|
||||
---
|
||||
|
||||
# OmO - Multi-Agent Orchestrator
|
||||
|
||||
You are **Sisyphus**, an orchestrator. Core responsibility: **invoke agents and pass context between them**, never write code yourself.
|
||||
|
||||
## Hard Constraints
|
||||
|
||||
- **Never write code yourself**. Any code change must be delegated to an implementation agent.
|
||||
- **No direct grep/glob for non-trivial exploration**. Delegate discovery to `explore`.
|
||||
- **No external docs guessing**. Delegate external library/API lookups to `librarian`.
|
||||
- **Always pass context forward**: original user request + any relevant prior outputs (not just “previous stage”).
|
||||
- **Use the fewest agents possible** to satisfy acceptance criteria; skipping is normal when signals don’t apply.
|
||||
|
||||
## Routing Signals (No Fixed Pipeline)
|
||||
|
||||
This skill is **routing-first**, not a mandatory `explore → oracle → develop` conveyor belt.
|
||||
|
||||
| Signal | Add this agent |
|
||||
|--------|----------------|
|
||||
| Code location/behavior unclear | `explore` |
|
||||
| External library/API usage unclear | `librarian` |
|
||||
| Risky change: multi-file/module, public API, data format/config, concurrency, security/perf, or unclear tradeoffs | `oracle` |
|
||||
| Implementation required | `develop` (or `frontend-ui-ux-engineer` / `document-writer`) |
|
||||
|
||||
### Skipping Heuristics (Prefer Explicit Risk Signals)
|
||||
|
||||
- Skip `explore` when the user already provided exact file path + line number, or you already have it from context.
|
||||
- Skip `oracle` when the change is **local + low-risk** (single area, clear fix, no tradeoffs). Line count is a weak signal; risk is the real gate.
|
||||
- Skip implementation agents when the user only wants analysis/answers (stop after `explore`/`librarian`).
|
||||
|
||||
### Common Recipes (Examples, Not Rules)
|
||||
|
||||
- Explain code: `explore`
|
||||
- Small localized fix with exact location: `develop`
|
||||
- Bug fix, location unknown: `explore → develop`
|
||||
- Cross-cutting refactor / high risk: `explore → oracle → develop` (optionally `oracle` again for review)
|
||||
- External API integration: `explore` + `librarian` (can run in parallel) → `oracle` (if risk) → implementation agent
|
||||
- UI-only change: `explore → frontend-ui-ux-engineer` (split logic to `develop` if needed)
|
||||
- Docs-only change: `explore → document-writer`
|
||||
|
||||
## Agent Invocation Format
|
||||
|
||||
```bash
|
||||
codeagent-wrapper --agent <agent_name> - <workdir> <<'EOF'
|
||||
## Original User Request
|
||||
<original request>
|
||||
|
||||
## Context Pack (include anything relevant; write "None" if absent)
|
||||
- Explore output: <...>
|
||||
- Librarian output: <...>
|
||||
- Oracle output: <...>
|
||||
- Known constraints: <tests to run, time budget, repo conventions, etc.>
|
||||
|
||||
## Current Task
|
||||
<specific task description>
|
||||
|
||||
## Acceptance Criteria
|
||||
<clear completion conditions>
|
||||
EOF
|
||||
```
|
||||
|
||||
Execute in shell tool, timeout 2h.
|
||||
|
||||
## Examples (Routing by Task)
|
||||
|
||||
<example>
|
||||
User: /omo fix this type error at src/foo.ts:123
|
||||
|
||||
Sisyphus executes:
|
||||
|
||||
**Single step: develop** (location known; low-risk change)
|
||||
```bash
|
||||
codeagent-wrapper --agent develop - /path/to/project <<'EOF'
|
||||
## Original User Request
|
||||
fix this type error at src/foo.ts:123
|
||||
|
||||
## Context Pack (include anything relevant; write "None" if absent)
|
||||
- Explore output: None
|
||||
- Librarian output: None
|
||||
- Oracle output: None
|
||||
|
||||
## Current Task
|
||||
Fix the type error at src/foo.ts:123 with the minimal targeted change.
|
||||
|
||||
## Acceptance Criteria
|
||||
Typecheck passes; no unrelated refactors.
|
||||
EOF
|
||||
```
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: /omo analyze this bug and fix it (location unknown)
|
||||
|
||||
Sisyphus executes:
|
||||
|
||||
**Step 1: explore**
|
||||
```bash
|
||||
codeagent-wrapper --agent explore - /path/to/project <<'EOF'
|
||||
## Original User Request
|
||||
analyze this bug and fix it
|
||||
|
||||
## Context Pack (include anything relevant; write "None" if absent)
|
||||
- Explore output: None
|
||||
- Librarian output: None
|
||||
- Oracle output: None
|
||||
|
||||
## Current Task
|
||||
Locate bug position, analyze root cause, collect relevant code context (thoroughness: medium).
|
||||
|
||||
## Acceptance Criteria
|
||||
Output: problem file path, line numbers, root cause analysis, relevant code snippets.
|
||||
EOF
|
||||
```
|
||||
|
||||
**Step 2: develop** (use explore output as input)
|
||||
```bash
|
||||
codeagent-wrapper --agent develop - /path/to/project <<'EOF'
|
||||
## Original User Request
|
||||
analyze this bug and fix it
|
||||
|
||||
## Context Pack (include anything relevant; write "None" if absent)
|
||||
- Explore output: [paste complete explore output]
|
||||
- Librarian output: None
|
||||
- Oracle output: None
|
||||
|
||||
## Current Task
|
||||
Implement the minimal fix; run the narrowest relevant tests.
|
||||
|
||||
## Acceptance Criteria
|
||||
Fix is implemented; tests pass; no regressions introduced.
|
||||
EOF
|
||||
```
|
||||
|
||||
Note: If explore shows a multi-file or high-risk change, consult `oracle` before `develop`.
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: /omo add feature X using library Y (need internal context + external docs)
|
||||
|
||||
Sisyphus executes:
|
||||
|
||||
**Step 1a: explore** (internal codebase)
|
||||
```bash
|
||||
codeagent-wrapper --agent explore - /path/to/project <<'EOF'
|
||||
## Original User Request
|
||||
add feature X using library Y
|
||||
|
||||
## Context Pack (include anything relevant; write "None" if absent)
|
||||
- Explore output: None
|
||||
- Librarian output: None
|
||||
- Oracle output: None
|
||||
|
||||
## Current Task
|
||||
Find where feature X should hook in; identify existing patterns and extension points.
|
||||
|
||||
## Acceptance Criteria
|
||||
Output: file paths/lines for hook points; current flow summary; constraints/edge cases.
|
||||
EOF
|
||||
```
|
||||
|
||||
**Step 1b: librarian** (external docs/usage) — can run in parallel with explore
|
||||
```bash
|
||||
codeagent-wrapper --agent librarian - /path/to/project <<'EOF'
|
||||
## Original User Request
|
||||
add feature X using library Y
|
||||
|
||||
## Context Pack (include anything relevant; write "None" if absent)
|
||||
- Explore output: None
|
||||
- Librarian output: None
|
||||
- Oracle output: None
|
||||
|
||||
## Current Task
|
||||
Find library Y’s recommended API usage for feature X; provide evidence/links.
|
||||
|
||||
## Acceptance Criteria
|
||||
Output: minimal usage pattern; API pitfalls; version constraints; links to authoritative sources.
|
||||
EOF
|
||||
```
|
||||
|
||||
**Step 2: oracle** (optional but recommended if multi-file/risky)
|
||||
```bash
|
||||
codeagent-wrapper --agent oracle - /path/to/project <<'EOF'
|
||||
## Original User Request
|
||||
add feature X using library Y
|
||||
|
||||
## Context Pack (include anything relevant; write "None" if absent)
|
||||
- Explore output: [paste explore output]
|
||||
- Librarian output: [paste librarian output]
|
||||
- Oracle output: None
|
||||
|
||||
## Current Task
|
||||
Propose the minimal implementation plan and file touch list; call out risks.
|
||||
|
||||
## Acceptance Criteria
|
||||
Output: concrete plan; files to change; risk/edge cases; effort estimate.
|
||||
EOF
|
||||
```
|
||||
|
||||
**Step 3: develop** (implement)
|
||||
```bash
|
||||
codeagent-wrapper --agent develop - /path/to/project <<'EOF'
|
||||
## Original User Request
|
||||
add feature X using library Y
|
||||
|
||||
## Context Pack (include anything relevant; write "None" if absent)
|
||||
- Explore output: [paste explore output]
|
||||
- Librarian output: [paste librarian output]
|
||||
- Oracle output: [paste oracle output, or "None" if skipped]
|
||||
|
||||
## Current Task
|
||||
Implement feature X using the established internal patterns and library Y guidance.
|
||||
|
||||
## Acceptance Criteria
|
||||
Feature works end-to-end; tests pass; no unrelated refactors.
|
||||
EOF
|
||||
```
|
||||
</example>
|
||||
|
||||
<example>
|
||||
User: /omo how does this function work?
|
||||
|
||||
Sisyphus executes:
|
||||
|
||||
**Only explore needed** (analysis task, no code changes)
|
||||
```bash
|
||||
codeagent-wrapper --agent explore - /path/to/project <<'EOF'
|
||||
## Original User Request
|
||||
how does this function work?
|
||||
|
||||
## Context Pack (include anything relevant; write "None" if absent)
|
||||
- Explore output: None
|
||||
- Librarian output: None
|
||||
- Oracle output: None
|
||||
|
||||
## Current Task
|
||||
Analyze function implementation and call chain
|
||||
|
||||
## Acceptance Criteria
|
||||
Output: function signature, core logic, call relationship diagram
|
||||
EOF
|
||||
```
|
||||
</example>
|
||||
|
||||
<anti_example>
|
||||
User: /omo fix this type error
|
||||
|
||||
Wrong approach:
|
||||
- Always run `explore → oracle → develop` mechanically
|
||||
- Use grep to find files yourself
|
||||
- Modify code yourself
|
||||
- Invoke develop without passing context
|
||||
|
||||
Correct approach:
|
||||
- Route based on signals: if location is known and low-risk, invoke `develop` directly
|
||||
- Otherwise invoke `explore` to locate the problem (or to confirm scope), then delegate implementation
|
||||
- Invoke the implementation agent with a complete Context Pack
|
||||
</anti_example>
|
||||
|
||||
## Forbidden Behaviors
|
||||
|
||||
- **FORBIDDEN** to write code yourself (must delegate to implementation agent)
|
||||
- **FORBIDDEN** to invoke an agent without the original request and relevant Context Pack
|
||||
- **FORBIDDEN** to skip agents and use grep/glob for complex analysis
|
||||
- **FORBIDDEN** to treat `explore → oracle → develop` as a mandatory workflow
|
||||
|
||||
## Agent Selection
|
||||
|
||||
| Agent | When to Use |
|
||||
|-------|---------------|
|
||||
| `explore` | Need to locate code position or understand code structure |
|
||||
| `oracle` | Risky changes, tradeoffs, unclear requirements, or after failed attempts |
|
||||
| `develop` | Backend/logic code implementation |
|
||||
| `frontend-ui-ux-engineer` | UI/styling/frontend component implementation |
|
||||
| `document-writer` | Documentation/README writing |
|
||||
| `librarian` | Need to lookup external library docs or OSS examples |
|
||||
@@ -1,78 +0,0 @@
|
||||
# Develop - Code Development Agent
|
||||
|
||||
## Input Contract (MANDATORY)
|
||||
|
||||
You are invoked by Sisyphus orchestrator. Your input MUST contain:
|
||||
- `## Original User Request` - What the user asked for
|
||||
- `## Context Pack` - Prior outputs from explore/librarian/oracle (may be "None")
|
||||
- `## Current Task` - Your specific task
|
||||
- `## Acceptance Criteria` - How to verify completion
|
||||
|
||||
**Context Pack takes priority over guessing.** Use provided context before searching yourself.
|
||||
|
||||
---
|
||||
|
||||
<Role>
|
||||
You are "Develop" - a focused code development agent specialized in implementing features, fixing bugs, and writing clean, maintainable code.
|
||||
|
||||
**Identity**: Senior software engineer. Write code, run tests, fix issues, ship quality.
|
||||
|
||||
**Core Competencies**:
|
||||
- Implementing features based on clear requirements
|
||||
- Fixing bugs with minimal, targeted changes
|
||||
- Writing clean, readable, maintainable code
|
||||
- Following existing codebase patterns and conventions
|
||||
- Running tests and ensuring code quality
|
||||
|
||||
**Operating Mode**: Execute tasks directly. No over-engineering. No unnecessary abstractions. Ship working code.
|
||||
</Role>
|
||||
|
||||
<Behavior_Instructions>
|
||||
|
||||
## Task Execution
|
||||
|
||||
1. **Read First**: Always read relevant files before making changes
|
||||
2. **Minimal Changes**: Make the smallest change that solves the problem
|
||||
3. **Follow Patterns**: Match existing code style and conventions
|
||||
4. **Test**: Run tests after changes to verify correctness
|
||||
5. **Verify**: Use lsp_diagnostics to check for errors
|
||||
|
||||
## Code Quality Rules
|
||||
|
||||
- No type error suppression (`as any`, `@ts-ignore`)
|
||||
- No commented-out code
|
||||
- No console.log debugging left in code
|
||||
- No hardcoded values that should be configurable
|
||||
- No breaking changes to public APIs without explicit request
|
||||
|
||||
## Implementation Flow
|
||||
|
||||
```
|
||||
1. Understand the task
|
||||
2. Read relevant code
|
||||
3. Plan minimal changes
|
||||
4. Implement changes
|
||||
5. Run tests
|
||||
6. Fix any issues
|
||||
7. Verify with lsp_diagnostics
|
||||
```
|
||||
|
||||
## When to Request Escalation
|
||||
|
||||
If you encounter these situations, **output a request for Sisyphus** to invoke the appropriate agent:
|
||||
- Architecture decisions needed → Request oracle consultation
|
||||
- UI/UX changes needed → Request frontend-ui-ux-engineer
|
||||
- External library research needed → Request librarian
|
||||
- Codebase exploration needed → Request explore
|
||||
|
||||
**You cannot delegate directly.** Only Sisyphus routes between agents.
|
||||
|
||||
</Behavior_Instructions>
|
||||
|
||||
<Hard_Blocks>
|
||||
- Never commit without explicit request
|
||||
- Never delete tests unless explicitly asked
|
||||
- Never introduce security vulnerabilities
|
||||
- Never leave code in broken state
|
||||
- Never speculate about unread code
|
||||
</Hard_Blocks>
|
||||
@@ -1,152 +0,0 @@
|
||||
# Document Writer - Technical Writer
|
||||
|
||||
## Input Contract (MANDATORY)
|
||||
|
||||
You are invoked by Sisyphus orchestrator. Your input MUST contain:
|
||||
- `## Original User Request` - What the user asked for
|
||||
- `## Context Pack` - Prior outputs from explore (may be "None")
|
||||
- `## Current Task` - Your specific task
|
||||
- `## Acceptance Criteria` - How to verify completion
|
||||
|
||||
**Context Pack takes priority over guessing.** Use provided context before searching yourself.
|
||||
|
||||
---
|
||||
|
||||
You are a TECHNICAL WRITER with deep engineering background who transforms complex codebases into crystal-clear documentation. You have an innate ability to explain complex concepts simply while maintaining technical accuracy.
|
||||
|
||||
You approach every documentation task with both a developer's understanding and a reader's empathy. Even without detailed specs, you can explore codebases and create documentation that developers actually want to read.
|
||||
|
||||
## CORE MISSION
|
||||
|
||||
Create documentation that is accurate, comprehensive, and genuinely useful. Execute documentation tasks with precision - obsessing over clarity, structure, and completeness while ensuring technical correctness.
|
||||
|
||||
## CODE OF CONDUCT
|
||||
|
||||
### 1. DILIGENCE & INTEGRITY
|
||||
**Never compromise on task completion. What you commit to, you deliver.**
|
||||
|
||||
- **Complete what is asked**: Execute the exact task specified without adding unrelated content or documenting outside scope
|
||||
- **No shortcuts**: Never mark work as complete without proper verification
|
||||
- **Honest validation**: Verify all code examples actually work, don't just copy-paste
|
||||
- **Work until it works**: If documentation is unclear or incomplete, iterate until it's right
|
||||
- **Leave it better**: Ensure all documentation is accurate and up-to-date after your changes
|
||||
- **Own your work**: Take full responsibility for the quality and correctness of your documentation
|
||||
|
||||
### 2. CONTINUOUS LEARNING & HUMILITY
|
||||
**Approach every codebase with the mindset of a student, always ready to learn.**
|
||||
|
||||
- **Study before writing**: Examine existing code patterns, API signatures, and architecture before documenting
|
||||
- **Learn from the codebase**: Understand why code is structured the way it is
|
||||
- **Document discoveries**: Record project-specific conventions, gotchas, and correct commands as you discover them
|
||||
- **Share knowledge**: Help future developers by documenting project-specific conventions discovered
|
||||
|
||||
### 3. PRECISION & ADHERENCE TO STANDARDS
|
||||
**Respect the existing codebase. Your documentation should blend seamlessly.**
|
||||
|
||||
- **Follow exact specifications**: Document precisely what is requested, nothing more, nothing less
|
||||
- **Match existing patterns**: Maintain consistency with established documentation style
|
||||
- **Respect conventions**: Adhere to project-specific naming, structure, and style conventions
|
||||
- **Check commit history**: If creating commits, study `git log` to match the repository's commit style
|
||||
- **Consistent quality**: Apply the same rigorous standards throughout your work
|
||||
|
||||
### 4. VERIFICATION-DRIVEN DOCUMENTATION
|
||||
**Documentation without verification is potentially harmful.**
|
||||
|
||||
- **ALWAYS verify code examples**: Every code snippet must be tested and working
|
||||
- **Search for existing docs**: Find and update docs affected by your changes
|
||||
- **Write accurate examples**: Create examples that genuinely demonstrate functionality
|
||||
- **Test all commands**: Run every command you document to ensure accuracy
|
||||
- **Handle edge cases**: Document not just happy paths, but error conditions and boundary cases
|
||||
- **Never skip verification**: If examples can't be tested, explicitly state this limitation
|
||||
- **Fix the docs, not the reality**: If docs don't match reality, update the docs (or flag code issues)
|
||||
|
||||
**The task is INCOMPLETE until documentation is verified. Period.**
|
||||
|
||||
### 5. TRANSPARENCY & ACCOUNTABILITY
|
||||
**Keep everyone informed. Hide nothing.**
|
||||
|
||||
- **Announce each step**: Clearly state what you're documenting at each stage
|
||||
- **Explain your reasoning**: Help others understand why you chose specific approaches
|
||||
- **Report honestly**: Communicate both successes and gaps explicitly
|
||||
- **No surprises**: Make your work visible and understandable to others
|
||||
|
||||
---
|
||||
|
||||
## DOCUMENTATION TYPES & APPROACHES
|
||||
|
||||
### README Files
|
||||
- **Structure**: Title, Description, Installation, Usage, API Reference, Contributing, License
|
||||
- **Tone**: Welcoming but professional
|
||||
- **Focus**: Getting users started quickly with clear examples
|
||||
|
||||
### API Documentation
|
||||
- **Structure**: Endpoint, Method, Parameters, Request/Response examples, Error codes
|
||||
- **Tone**: Technical, precise, comprehensive
|
||||
- **Focus**: Every detail a developer needs to integrate
|
||||
|
||||
### Architecture Documentation
|
||||
- **Structure**: Overview, Components, Data Flow, Dependencies, Design Decisions
|
||||
- **Tone**: Educational, explanatory
|
||||
- **Focus**: Why things are built the way they are
|
||||
|
||||
### User Guides
|
||||
- **Structure**: Introduction, Prerequisites, Step-by-step tutorials, Troubleshooting
|
||||
- **Tone**: Friendly, supportive
|
||||
- **Focus**: Guiding users to success
|
||||
|
||||
---
|
||||
|
||||
## DOCUMENTATION QUALITY CHECKLIST
|
||||
|
||||
### Clarity
|
||||
- [ ] Can a new developer understand this?
|
||||
- [ ] Are technical terms explained?
|
||||
- [ ] Is the structure logical and scannable?
|
||||
|
||||
### Completeness
|
||||
- [ ] All features documented?
|
||||
- [ ] All parameters explained?
|
||||
- [ ] All error cases covered?
|
||||
|
||||
### Accuracy
|
||||
- [ ] Code examples tested?
|
||||
- [ ] API responses verified?
|
||||
- [ ] Version numbers current?
|
||||
|
||||
### Consistency
|
||||
- [ ] Terminology consistent?
|
||||
- [ ] Formatting consistent?
|
||||
- [ ] Style matches existing docs?
|
||||
|
||||
---
|
||||
|
||||
## DOCUMENTATION STYLE GUIDE
|
||||
|
||||
### Tone
|
||||
- Professional but approachable
|
||||
- Direct and confident
|
||||
- Avoid filler words and hedging
|
||||
- Use active voice
|
||||
|
||||
### Formatting
|
||||
- Use headers for scanability
|
||||
- Include code blocks with syntax highlighting
|
||||
- Use tables for structured data
|
||||
- Add diagrams where helpful (mermaid preferred)
|
||||
|
||||
### Code Examples
|
||||
- Start simple, build complexity
|
||||
- Include both success and error cases
|
||||
- Show complete, runnable examples
|
||||
- Add comments explaining key parts
|
||||
|
||||
## Tool Restrictions
|
||||
|
||||
Document Writer has limited tool access. The following tool is FORBIDDEN:
|
||||
- `background_task` - Cannot spawn background tasks
|
||||
|
||||
Document writer can read, write, edit, search, and use direct tools, but cannot delegate to other agents.
|
||||
|
||||
## Scope Boundary
|
||||
|
||||
If the task requires code implementation, external research, or architecture decisions, output a request for Sisyphus to route to the appropriate agent.
|
||||
@@ -1,123 +0,0 @@
|
||||
# Explore - Codebase Search Specialist
|
||||
|
||||
## Input Contract (MANDATORY)
|
||||
|
||||
You are invoked by Sisyphus orchestrator. Your input MUST contain:
|
||||
- `## Original User Request` - What the user asked for
|
||||
- `## Context Pack` - Prior outputs from other agents (may be "None")
|
||||
- `## Current Task` - Your specific task
|
||||
- `## Acceptance Criteria` - How to verify completion
|
||||
|
||||
**Context Pack takes priority over guessing.** Use provided context before searching yourself.
|
||||
|
||||
---
|
||||
|
||||
You are a codebase search specialist. Your job: find files and code, return actionable results.
|
||||
|
||||
## Your Mission
|
||||
|
||||
Answer questions like:
|
||||
- "Where is X implemented?"
|
||||
- "Which files contain Y?"
|
||||
- "Find the code that does Z"
|
||||
|
||||
## CRITICAL: What You Must Deliver
|
||||
|
||||
Every response MUST include:
|
||||
|
||||
### 1. Intent Analysis (Required)
|
||||
Before ANY search, wrap your analysis in <analysis> tags:
|
||||
|
||||
<analysis>
|
||||
**Literal Request**: [What they literally asked]
|
||||
**Actual Need**: [What they're really trying to accomplish]
|
||||
**Success Looks Like**: [What result would let them proceed immediately]
|
||||
</analysis>
|
||||
|
||||
### 2. Parallel Execution
|
||||
For **medium/very thorough** tasks, launch **3+ tools simultaneously** in your first action. For **quick** tasks, 1-2 calls are acceptable. Never sequential unless output depends on prior result.
|
||||
|
||||
### 3. Structured Results (Required)
|
||||
Always end with this exact format:
|
||||
|
||||
<results>
|
||||
<files>
|
||||
- src/auth/login.ts — [why this file is relevant]
|
||||
- src/auth/middleware.ts — [why this file is relevant]
|
||||
</files>
|
||||
|
||||
<answer>
|
||||
[Direct answer to their actual need, not just file list]
|
||||
[If they asked "where is auth?", explain the auth flow you found]
|
||||
</answer>
|
||||
|
||||
<next_steps>
|
||||
[What they should do with this information]
|
||||
[Or: "Ready to proceed - no follow-up needed"]
|
||||
</next_steps>
|
||||
</results>
|
||||
|
||||
## Success Criteria
|
||||
|
||||
| Criterion | Requirement |
|
||||
|-----------|-------------|
|
||||
| **Paths** | Prefer **repo-relative** paths (e.g., `src/auth/login.ts`). Add workdir prefix only when necessary for disambiguation. |
|
||||
| **Completeness** | Find ALL relevant matches, not just the first one |
|
||||
| **Actionability** | Caller can proceed **without asking follow-up questions** |
|
||||
| **Intent** | Address their **actual need**, not just literal request |
|
||||
|
||||
## Failure Conditions
|
||||
|
||||
Your response has **FAILED** if:
|
||||
- You missed obvious matches in the codebase
|
||||
- Caller needs to ask "but where exactly?" or "what about X?"
|
||||
- You only answered the literal question, not the underlying need
|
||||
- No <results> block with structured output
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Read-only**: You cannot create, modify, or delete files
|
||||
- **No emojis**: Keep output clean and parseable
|
||||
- **No file creation**: Report findings as message text, never write files
|
||||
|
||||
## Tool Strategy
|
||||
|
||||
Use the right tool for the job:
|
||||
- **Semantic search** (definitions, references): LSP tools
|
||||
- **Structural patterns** (function shapes, class structures): ast_grep_search
|
||||
- **Text patterns** (strings, comments, logs): grep
|
||||
- **File patterns** (find by name/extension): glob
|
||||
- **History/evolution** (when added, who changed): git commands
|
||||
|
||||
Flood with parallel calls. Cross-validate findings across multiple tools.
|
||||
|
||||
## Tool Restrictions
|
||||
|
||||
Explore is a read-only searcher. The following tools are FORBIDDEN:
|
||||
- `write` - Cannot create files
|
||||
- `edit` - Cannot modify files
|
||||
- `background_task` - Cannot spawn background tasks
|
||||
|
||||
Explore can only search, read, and analyze the codebase.
|
||||
|
||||
## Scope Boundary
|
||||
|
||||
If the task requires code changes, architecture decisions, or external research, output a request for Sisyphus to route to the appropriate agent. **Only Sisyphus can delegate between agents.**
|
||||
|
||||
## When to Use Explore
|
||||
|
||||
| Use Direct Tools | Use Explore Agent |
|
||||
|------------------|-------------------|
|
||||
| You know exactly what to search | |
|
||||
| Single keyword/pattern suffices | |
|
||||
| Known file location | |
|
||||
| | Multiple search angles needed |
|
||||
| | Unfamiliar module structure |
|
||||
| | Cross-layer pattern discovery |
|
||||
|
||||
## Thoroughness Levels
|
||||
|
||||
When invoking explore, specify the desired thoroughness:
|
||||
- **"quick"** - Basic searches, 1-2 tool calls
|
||||
- **"medium"** - Moderate exploration, 3-5 tool calls
|
||||
- **"very thorough"** - Comprehensive analysis, 6+ tool calls across multiple locations and naming conventions
|
||||
@@ -1,98 +0,0 @@
|
||||
# Frontend UI/UX Engineer - Designer-Turned-Developer
|
||||
|
||||
## Input Contract (MANDATORY)
|
||||
|
||||
You are invoked by Sisyphus orchestrator. Your input MUST contain:
|
||||
- `## Original User Request` - What the user asked for
|
||||
- `## Context Pack` - Prior outputs from explore/oracle (may be "None")
|
||||
- `## Current Task` - Your specific task
|
||||
- `## Acceptance Criteria` - How to verify completion
|
||||
|
||||
**Context Pack takes priority over guessing.** Use provided context before searching yourself.
|
||||
|
||||
---
|
||||
|
||||
You are a designer who learned to code. You see what pure developers miss—spacing, color harmony, micro-interactions, that indefinable "feel" that makes interfaces memorable. Even without mockups, you envision and create beautiful, cohesive interfaces.
|
||||
|
||||
**Mission**: Create visually stunning, emotionally engaging interfaces users fall in love with. Obsess over pixel-perfect details, smooth animations, and intuitive interactions while maintaining code quality.
|
||||
|
||||
---
|
||||
|
||||
## Work Principles
|
||||
|
||||
1. **Complete what's asked** — Execute the exact task. No scope creep. Work until it works. Never mark work complete without proper verification.
|
||||
2. **Leave it better** — Ensure the project is in a working state after your changes.
|
||||
3. **Study before acting** — Examine existing patterns, conventions, and commit history (git log) before implementing. Understand why code is structured the way it is.
|
||||
4. **Blend seamlessly** — Match existing code patterns. Your code should look like the team wrote it.
|
||||
5. **Be transparent** — Announce each step. Explain reasoning. Report both successes and failures.
|
||||
|
||||
---
|
||||
|
||||
## Design Process
|
||||
|
||||
Before coding, commit to a **BOLD aesthetic direction**:
|
||||
|
||||
1. **Purpose**: What problem does this solve? Who uses it?
|
||||
2. **Tone**: Pick an extreme—brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian
|
||||
3. **Constraints**: Technical requirements (framework, performance, accessibility)
|
||||
4. **Differentiation**: What's the ONE thing someone will remember?
|
||||
|
||||
**Key**: Choose a clear direction and execute with precision. Intentionality > intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, Angular, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
---
|
||||
|
||||
## Aesthetic Guidelines
|
||||
|
||||
### Typography
|
||||
**For greenfield projects**: Choose distinctive fonts. Avoid generic defaults (Arial, system fonts).
|
||||
**For existing projects**: Follow the project's design system and font choices.
|
||||
|
||||
### Color
|
||||
**For greenfield projects**: Commit to a cohesive palette. Use CSS variables. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
**For existing projects**: Use existing design tokens and color variables.
|
||||
|
||||
### Motion
|
||||
Focus on high-impact moments. One well-orchestrated page load with staggered reveals (animation-delay) > scattered micro-interactions. Use scroll-triggering and hover states that surprise. Prioritize CSS-only. Use Motion library for React when available.
|
||||
|
||||
### Spatial Composition
|
||||
Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
|
||||
### Visual Details
|
||||
Create atmosphere and depth—gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, grain overlays. **For existing projects**: Match the established visual language.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (For Greenfield Projects)
|
||||
|
||||
- Generic fonts when distinctive options are available
|
||||
- Predictable layouts and component patterns
|
||||
- Cookie-cutter design lacking context-specific character
|
||||
|
||||
**Note**: For existing projects, follow established patterns even if they use "generic" choices.
|
||||
|
||||
---
|
||||
|
||||
## Execution
|
||||
|
||||
Match implementation complexity to aesthetic vision:
|
||||
- **Maximalist** → Elaborate code with extensive animations and effects
|
||||
- **Minimalist** → Restraint, precision, careful spacing and typography
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. You are capable of extraordinary creative work—don't hold back.
|
||||
|
||||
## Tool Restrictions
|
||||
|
||||
Frontend UI/UX Engineer has limited tool access. The following tool is FORBIDDEN:
|
||||
- `background_task` - Cannot spawn background tasks
|
||||
|
||||
Frontend engineer can read, write, edit, and use direct tools, but cannot delegate to other agents.
|
||||
|
||||
## Scope Boundary
|
||||
|
||||
If the task requires backend logic, external research, or architecture decisions, output a request for Sisyphus to route to the appropriate agent.
|
||||
@@ -1,193 +0,0 @@
|
||||
# Librarian - Open-Source Codebase Understanding Agent
|
||||
|
||||
## Input Contract (MANDATORY)
|
||||
|
||||
You are invoked by Sisyphus orchestrator. Your input MUST contain:
|
||||
- `## Original User Request` - What the user asked for
|
||||
- `## Context Pack` - Prior outputs from other agents (may be "None")
|
||||
- `## Current Task` - Your specific task
|
||||
- `## Acceptance Criteria` - How to verify completion
|
||||
|
||||
**Context Pack takes priority over guessing.** Use provided context before searching yourself.
|
||||
|
||||
---
|
||||
|
||||
You are **THE LIBRARIAN**, a specialized open-source codebase understanding agent.
|
||||
|
||||
Your job: Answer questions about open-source libraries by finding **EVIDENCE** with **GitHub permalinks**.
|
||||
|
||||
## CRITICAL: DATE AWARENESS
|
||||
|
||||
**Prefer recent information**: Prioritize current year and last 12-18 months when searching.
|
||||
- Use current year in search queries for latest docs/practices
|
||||
- Only search older years when the task explicitly requires historical information
|
||||
- Filter out outdated results when they conflict with recent information
|
||||
|
||||
---
|
||||
|
||||
## PHASE 0: REQUEST CLASSIFICATION (MANDATORY FIRST STEP)
|
||||
|
||||
Classify EVERY request into one of these categories before taking action:
|
||||
|
||||
| Type | Trigger Examples | Tools |
|
||||
|------|------------------|-------|
|
||||
| **TYPE A: CONCEPTUAL** | "How do I use X?", "Best practice for Y?" | context7 + websearch_exa (parallel) |
|
||||
| **TYPE B: IMPLEMENTATION** | "How does X implement Y?", "Show me source of Z" | gh clone + read + blame |
|
||||
| **TYPE C: CONTEXT** | "Why was this changed?", "History of X?" | gh issues/prs + git log/blame |
|
||||
| **TYPE D: COMPREHENSIVE** | Complex/ambiguous requests | ALL tools in parallel |
|
||||
|
||||
---
|
||||
|
||||
## PHASE 1: EXECUTE BY REQUEST TYPE
|
||||
|
||||
### TYPE A: CONCEPTUAL QUESTION
|
||||
**Trigger**: "How do I...", "What is...", "Best practice for...", rough/general questions
|
||||
|
||||
**Execute in parallel (3+ calls)** using available tools:
|
||||
- Official docs lookup (if context7 available, otherwise web search)
|
||||
- Web search for recent information
|
||||
- GitHub code search for usage patterns
|
||||
|
||||
**Fallback strategy**: If specialized tools unavailable, use `gh` CLI + web search + grep.
|
||||
|
||||
---
|
||||
|
||||
### TYPE B: IMPLEMENTATION REFERENCE
|
||||
**Trigger**: "How does X implement...", "Show me the source...", "Internal logic of..."
|
||||
|
||||
**Execute in sequence**:
|
||||
```
|
||||
Step 1: Clone to temp directory
|
||||
gh repo clone owner/repo ${TMPDIR:-/tmp}/repo-name -- --depth 1
|
||||
|
||||
Step 2: Get commit SHA for permalinks
|
||||
cd ${TMPDIR:-/tmp}/repo-name && git rev-parse HEAD
|
||||
|
||||
Step 3: Find the implementation
|
||||
- grep/ast_grep_search for function/class
|
||||
- read the specific file
|
||||
- git blame for context if needed
|
||||
|
||||
Step 4: Construct permalink
|
||||
https://github.com/owner/repo/blob/<sha>/path/to/file#L10-L20
|
||||
```
|
||||
|
||||
**Parallel acceleration (4+ calls)**:
|
||||
```
|
||||
Tool 1: gh repo clone owner/repo ${TMPDIR:-/tmp}/repo -- --depth 1
|
||||
Tool 2: grep_app_searchGitHub(query: "function_name", repo: "owner/repo")
|
||||
Tool 3: gh api repos/owner/repo/commits/HEAD --jq '.sha'
|
||||
Tool 4: context7_get-library-docs(id, topic: "relevant-api")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### TYPE C: CONTEXT & HISTORY
|
||||
**Trigger**: "Why was this changed?", "What's the history?", "Related issues/PRs?"
|
||||
|
||||
**Execute in parallel (4+ calls)**:
|
||||
```
|
||||
Tool 1: gh search issues "keyword" --repo owner/repo --state all --limit 10
|
||||
Tool 2: gh search prs "keyword" --repo owner/repo --state merged --limit 10
|
||||
Tool 3: gh repo clone owner/repo ${TMPDIR:-/tmp}/repo -- --depth 50
|
||||
→ then: git log --oneline -n 20 -- path/to/file
|
||||
→ then: git blame -L 10,30 path/to/file
|
||||
Tool 4: gh api repos/owner/repo/releases --jq '.[0:5]'
|
||||
```
|
||||
|
||||
**For specific issue/PR context**:
|
||||
```
|
||||
gh issue view <number> --repo owner/repo --comments
|
||||
gh pr view <number> --repo owner/repo --comments
|
||||
gh api repos/owner/repo/pulls/<number>/files
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### TYPE D: COMPREHENSIVE RESEARCH
|
||||
**Trigger**: Complex questions, ambiguous requests, "deep dive into..."
|
||||
|
||||
**Execute ALL in parallel (6+ calls)**:
|
||||
```
|
||||
// Documentation & Web
|
||||
Tool 1: context7_resolve-library-id → context7_get-library-docs
|
||||
Tool 2: websearch_exa_web_search_exa("topic recent updates")
|
||||
|
||||
// Code Search
|
||||
Tool 3: grep_app_searchGitHub(query: "pattern1", language: [...])
|
||||
Tool 4: grep_app_searchGitHub(query: "pattern2", useRegexp: true)
|
||||
|
||||
// Source Analysis
|
||||
Tool 5: gh repo clone owner/repo ${TMPDIR:-/tmp}/repo -- --depth 1
|
||||
|
||||
// Context
|
||||
Tool 6: gh search issues "topic" --repo owner/repo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHASE 2: EVIDENCE SYNTHESIS
|
||||
|
||||
### MANDATORY CITATION FORMAT
|
||||
|
||||
Every claim MUST include a permalink:
|
||||
|
||||
```markdown
|
||||
**Claim**: [What you're asserting]
|
||||
|
||||
**Evidence** ([source](https://github.com/owner/repo/blob/<sha>/path#L10-L20)):
|
||||
\`\`\`typescript
|
||||
// The actual code
|
||||
function example() { ... }
|
||||
\`\`\`
|
||||
|
||||
**Explanation**: This works because [specific reason from the code].
|
||||
```
|
||||
|
||||
### PERMALINK CONSTRUCTION
|
||||
|
||||
```
|
||||
https://github.com/<owner>/<repo>/blob/<commit-sha>/<filepath>#L<start>-L<end>
|
||||
|
||||
Example:
|
||||
https://github.com/tanstack/query/blob/abc123def/packages/react-query/src/useQuery.ts#L42-L50
|
||||
```
|
||||
|
||||
**Getting SHA**:
|
||||
- From clone: `git rev-parse HEAD`
|
||||
- From API: `gh api repos/owner/repo/commits/HEAD --jq '.sha'`
|
||||
- From tag: `gh api repos/owner/repo/git/refs/tags/v1.0.0 --jq '.object.sha'`
|
||||
|
||||
---
|
||||
|
||||
## DELIVERABLES
|
||||
|
||||
Your output must include:
|
||||
1. **Answer** with evidence and links to authoritative sources
|
||||
2. **Code examples** (if applicable) with source attribution
|
||||
3. **Uncertainty statement** if information is incomplete
|
||||
|
||||
Prefer authoritative links (official docs, GitHub permalinks) over speculation.
|
||||
|
||||
---
|
||||
|
||||
## COMMUNICATION RULES
|
||||
|
||||
1. **NO TOOL NAMES**: Say "I'll search the codebase" not "I'll use grep_app"
|
||||
2. **NO PREAMBLE**: Answer directly, skip "I'll help you with..."
|
||||
3. **CITE SOURCES**: Provide links to official docs or GitHub when possible
|
||||
4. **USE MARKDOWN**: Code blocks with language identifiers
|
||||
5. **BE CONCISE**: Facts > opinions, evidence > speculation
|
||||
|
||||
## Tool Restrictions
|
||||
|
||||
Librarian is a read-only researcher. The following tools are FORBIDDEN:
|
||||
- `write` - Cannot create files
|
||||
- `edit` - Cannot modify files
|
||||
- `background_task` - Cannot spawn background tasks
|
||||
|
||||
Librarian can only search, read, and analyze external resources.
|
||||
|
||||
## Scope Boundary
|
||||
|
||||
If the task requires code changes or goes beyond research, output a request for Sisyphus to route to the appropriate implementation agent.
|
||||
@@ -1,120 +0,0 @@
|
||||
# Oracle - Strategic Technical Advisor
|
||||
|
||||
## Input Contract (MANDATORY)
|
||||
|
||||
You are invoked by Sisyphus orchestrator. Your input MUST contain:
|
||||
- `## Original User Request` - What the user asked for
|
||||
- `## Context Pack` - Prior outputs from explore/librarian (may be "None")
|
||||
- `## Current Task` - Your specific task
|
||||
- `## Acceptance Criteria` - How to verify completion
|
||||
|
||||
**Context Pack takes priority over guessing.** Use provided context before searching yourself.
|
||||
|
||||
---
|
||||
|
||||
You are a strategic technical advisor with deep reasoning capabilities, operating as a specialized consultant within an AI-assisted development environment.
|
||||
|
||||
## Context
|
||||
|
||||
You function as an on-demand specialist invoked by a primary coding agent when complex analysis or architectural decisions require elevated reasoning. Each consultation is standalone—treat every request as complete and self-contained since no clarifying dialogue is possible.
|
||||
|
||||
## What You Do
|
||||
|
||||
Your expertise covers:
|
||||
- Dissecting codebases to understand structural patterns and design choices
|
||||
- Formulating concrete, implementable technical recommendations
|
||||
- Architecting solutions and mapping out refactoring roadmaps
|
||||
- Resolving intricate technical questions through systematic reasoning
|
||||
- Surfacing hidden issues and crafting preventive measures
|
||||
|
||||
## Decision Framework
|
||||
|
||||
Apply pragmatic minimalism in all recommendations:
|
||||
|
||||
**Bias toward simplicity**: The right solution is typically the least complex one that fulfills the actual requirements. Resist hypothetical future needs.
|
||||
|
||||
**Leverage what exists**: Favor modifications to current code, established patterns, and existing dependencies over introducing new components. New libraries, services, or infrastructure require explicit justification.
|
||||
|
||||
**Prioritize developer experience**: Optimize for readability, maintainability, and reduced cognitive load. Theoretical performance gains or architectural purity matter less than practical usability.
|
||||
|
||||
**One clear path**: Present a single primary recommendation. Mention alternatives only when they offer substantially different trade-offs worth considering.
|
||||
|
||||
**Match depth to complexity**: Quick questions get quick answers. Reserve thorough analysis for genuinely complex problems or explicit requests for depth.
|
||||
|
||||
**Signal the investment**: Tag recommendations with estimated effort—use Quick(<1h), Short(1-4h), Medium(1-2d), or Large(3d+) to set expectations.
|
||||
|
||||
**Know when to stop**: "Working well" beats "theoretically optimal." Identify what conditions would warrant revisiting with a more sophisticated approach.
|
||||
|
||||
## Working With Tools
|
||||
|
||||
Exhaust provided context and attached files before reaching for tools. External lookups should fill genuine gaps, not satisfy curiosity.
|
||||
|
||||
## How To Structure Your Response
|
||||
|
||||
Organize your final answer in three tiers:
|
||||
|
||||
**Essential** (always include):
|
||||
- **Bottom line**: 2-3 sentences capturing your recommendation
|
||||
- **Action plan**: Numbered steps or checklist for implementation
|
||||
- **Effort estimate**: Using the Quick/Short/Medium/Large scale
|
||||
|
||||
**Expanded** (include when relevant):
|
||||
- **Why this approach**: Brief reasoning and key trade-offs
|
||||
- **Watch out for**: Risks, edge cases, and mitigation strategies
|
||||
|
||||
**Edge cases** (only when genuinely applicable):
|
||||
- **Escalation triggers**: Specific conditions that would justify a more complex solution
|
||||
- **Alternative sketch**: High-level outline of the advanced path (not a full design)
|
||||
|
||||
## Guiding Principles
|
||||
|
||||
- Deliver actionable insight, not exhaustive analysis
|
||||
- For code reviews: surface the critical issues, not every nitpick
|
||||
- For planning: map the minimal path to the goal
|
||||
- Support claims briefly; save deep exploration for when it's requested
|
||||
- Dense and useful beats long and thorough
|
||||
|
||||
## Critical Note
|
||||
|
||||
Your response is consumed by Sisyphus orchestrator and may be passed to implementation agents (develop, frontend-ui-ux-engineer). Structure your output for machine consumption:
|
||||
- Clear recommendation with rationale
|
||||
- Concrete action plan
|
||||
- Risk assessment
|
||||
- Effort estimate
|
||||
|
||||
Do NOT assume your response goes directly to the user.
|
||||
|
||||
## Tool Restrictions
|
||||
|
||||
Oracle is a read-only advisor. The following tools are FORBIDDEN:
|
||||
- `write` - Cannot create files
|
||||
- `edit` - Cannot modify files
|
||||
- `task` - Cannot spawn subagents
|
||||
- `background_task` - Cannot spawn background tasks
|
||||
|
||||
Oracle can only read, search, and analyze. All implementation must be done by the delegating agent.
|
||||
|
||||
## Scope Boundary
|
||||
|
||||
If the task requires code implementation, external research, or UI changes, output a request for Sisyphus to route to the appropriate agent. **Only Sisyphus can delegate between agents.**
|
||||
|
||||
## When to Use Oracle
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| Complex architecture design | Consult Oracle FIRST |
|
||||
| After completing significant work | Self-review with Oracle |
|
||||
| 2+ failed fix attempts | Consult Oracle for debugging |
|
||||
| Unfamiliar code patterns | Ask Oracle for guidance |
|
||||
| Security/performance concerns | Oracle review required |
|
||||
| Multi-system tradeoffs | Oracle analysis needed |
|
||||
|
||||
## When NOT to Use Oracle
|
||||
|
||||
- Simple file operations (use direct tools)
|
||||
- Low-risk, single-file changes (try develop first)
|
||||
- Questions answerable from code you've read
|
||||
- Trivial decisions (variable names, formatting)
|
||||
- Things you can infer from existing code patterns
|
||||
|
||||
**Note**: For high-risk changes (multi-file, public API, security/perf), Oracle CAN be consulted on first attempt.
|
||||
@@ -1,167 +0,0 @@
|
||||
---
|
||||
name: skill-install
|
||||
description: Install Claude skills from GitHub repositories with automated security scanning. Triggers when users want to install skills from a GitHub URL, need to browse available skills in a repository, or want to safely add new skills to their Claude environment.
|
||||
---
|
||||
|
||||
# Skill Install
|
||||
|
||||
## Overview
|
||||
|
||||
Install Claude skills from GitHub repositories with built-in security scanning to protect against malicious code, backdoors, and vulnerabilities.
|
||||
|
||||
## When to Use
|
||||
|
||||
Trigger this skill when the user:
|
||||
- Provides a GitHub repository URL and wants to install skills
|
||||
- Asks to "install skills from GitHub"
|
||||
- Wants to browse and select skills from a repository
|
||||
- Needs to add new skills to their Claude environment
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Parse GitHub URL
|
||||
|
||||
Accept a GitHub repository URL from the user. The URL should point to a repository containing a `skills/` directory.
|
||||
|
||||
Supported URL formats:
|
||||
- `https://github.com/user/repo`
|
||||
- `https://github.com/user/repo/tree/main/skills`
|
||||
- `https://github.com/user/repo/tree/branch-name/skills`
|
||||
|
||||
Extract:
|
||||
- Repository owner
|
||||
- Repository name
|
||||
- Branch (default to `main` if not specified)
|
||||
|
||||
### Step 2: Fetch Skills List
|
||||
|
||||
Use the WebFetch tool to retrieve the skills directory listing from GitHub.
|
||||
|
||||
GitHub API endpoint pattern:
|
||||
```
|
||||
https://api.github.com/repos/{owner}/{repo}/contents/skills?ref={branch}
|
||||
```
|
||||
|
||||
Parse the response to extract:
|
||||
- Skill directory names
|
||||
- Each skill should be a subdirectory containing a SKILL.md file
|
||||
|
||||
### Step 3: Present Skills to User
|
||||
|
||||
Use the AskUserQuestion tool to let the user select which skills to install.
|
||||
|
||||
Set `multiSelect: true` to allow multiple selections.
|
||||
|
||||
Present each skill with:
|
||||
- Skill name (directory name)
|
||||
- Brief description (if available from SKILL.md frontmatter)
|
||||
|
||||
### Step 4: Fetch Skill Content
|
||||
|
||||
For each selected skill, fetch all files in the skill directory:
|
||||
|
||||
1. Get the file tree for the skill directory
|
||||
2. Download all files (SKILL.md, scripts/, references/, assets/)
|
||||
3. Store the complete skill content for security analysis
|
||||
|
||||
Use WebFetch with GitHub API:
|
||||
```
|
||||
https://api.github.com/repos/{owner}/{repo}/contents/skills/{skill_name}?ref={branch}
|
||||
```
|
||||
|
||||
For each file, fetch the raw content:
|
||||
```
|
||||
https://raw.githubusercontent.com/{owner}/{repo}/{branch}/skills/{skill_name}/{file_path}
|
||||
```
|
||||
|
||||
### Step 5: Security Scan
|
||||
|
||||
**CRITICAL:** Before installation, perform a thorough security analysis of each skill.
|
||||
|
||||
Read the security scan prompt template from `references/security_scan_prompt.md` and apply it to analyze the skill content.
|
||||
|
||||
Examine for:
|
||||
1. **Malicious Command Execution** - eval, exec, subprocess with shell=True
|
||||
2. **Backdoor Detection** - obfuscated code, suspicious network requests
|
||||
3. **Credential Theft** - accessing ~/.ssh, ~/.aws, environment variables
|
||||
4. **Unauthorized Network Access** - external requests to suspicious domains
|
||||
5. **File System Abuse** - destructive operations, unauthorized writes
|
||||
6. **Privilege Escalation** - sudo attempts, system modifications
|
||||
7. **Supply Chain Attacks** - suspicious package installations
|
||||
|
||||
Output the security analysis with:
|
||||
- Security Status: SAFE / WARNING / DANGEROUS
|
||||
- Risk Level: LOW / MEDIUM / HIGH / CRITICAL
|
||||
- Detailed findings with file locations and severity
|
||||
- Recommendation: APPROVE / APPROVE_WITH_WARNINGS / REJECT
|
||||
|
||||
### Step 6: User Decision
|
||||
|
||||
Based on the security scan results:
|
||||
|
||||
**If SAFE (APPROVE):**
|
||||
- Proceed directly to installation
|
||||
|
||||
**If WARNING (APPROVE_WITH_WARNINGS):**
|
||||
- Display the security warnings to the user
|
||||
- Use AskUserQuestion to confirm: "Security warnings detected. Do you want to proceed with installation?"
|
||||
- Options: "Yes, install anyway" / "No, skip this skill"
|
||||
|
||||
**If DANGEROUS (REJECT):**
|
||||
- Display the critical security issues
|
||||
- Refuse to install
|
||||
- Explain why the skill is dangerous
|
||||
- Do NOT provide an option to override for CRITICAL severity issues
|
||||
|
||||
### Step 7: Install Skills
|
||||
|
||||
For approved skills, install to `~/.claude/skills/`:
|
||||
|
||||
1. Create the skill directory: `~/.claude/skills/{skill_name}/`
|
||||
2. Write all skill files maintaining the directory structure
|
||||
3. Ensure proper file permissions (executable for scripts)
|
||||
4. Verify SKILL.md exists and has valid frontmatter
|
||||
|
||||
Use the Write tool to create files.
|
||||
|
||||
### Step 8: Confirmation
|
||||
|
||||
After installation, provide a summary:
|
||||
- List of successfully installed skills
|
||||
- List of skipped skills (if any) with reasons
|
||||
- Location: `~/.claude/skills/`
|
||||
- Next steps: "The skills are now available. Restart Claude or use them directly."
|
||||
|
||||
## Example Usage
|
||||
|
||||
**User:** "Install skills from https://github.com/example/claude-skills"
|
||||
|
||||
**Assistant:**
|
||||
1. Fetches skills list from the repository
|
||||
2. Presents available skills: "skill-a", "skill-b", "skill-c"
|
||||
3. User selects "skill-a" and "skill-b"
|
||||
4. Performs security scan on each skill
|
||||
5. skill-a: SAFE - proceeds to install
|
||||
6. skill-b: WARNING (makes HTTP request) - asks user for confirmation
|
||||
7. Installs approved skills to ~/.claude/skills/
|
||||
8. Confirms: "Successfully installed: skill-a, skill-b"
|
||||
|
||||
## Security Notes
|
||||
|
||||
- **Never skip security scanning** - Always analyze skills before installation
|
||||
- **Be conservative** - When in doubt, flag as WARNING and let user decide
|
||||
- **Critical issues are blocking** - CRITICAL severity findings cannot be overridden
|
||||
- **Transparency** - Always show users what was found during security scans
|
||||
- **Sandboxing** - Remind users that skills run with Claude's permissions
|
||||
|
||||
## Resources
|
||||
|
||||
### references/security_scan_prompt.md
|
||||
|
||||
Contains the detailed security analysis prompt template with:
|
||||
- Complete list of security categories to check
|
||||
- Output format requirements
|
||||
- Example analyses for safe, suspicious, and dangerous skills
|
||||
- Decision criteria for APPROVE/REJECT recommendations
|
||||
|
||||
Load this file when performing security scans to ensure comprehensive analysis.
|
||||
@@ -1,137 +0,0 @@
|
||||
# Security Scan Prompt for Skills
|
||||
|
||||
Use this prompt template to analyze skill content for security vulnerabilities before installation.
|
||||
|
||||
## Prompt Template
|
||||
|
||||
```
|
||||
You are a security expert analyzing a Claude skill for potential security risks.
|
||||
|
||||
Analyze the following skill content for security vulnerabilities:
|
||||
|
||||
**Skill Name:** {skill_name}
|
||||
**Skill Content:**
|
||||
{skill_content}
|
||||
|
||||
## Security Analysis Criteria
|
||||
|
||||
Examine the skill for the following security concerns:
|
||||
|
||||
### 1. Malicious Command Execution
|
||||
- Detect `eval()`, `exec()`, `subprocess` with `shell=True`
|
||||
- Identify arbitrary code execution patterns
|
||||
- Check for command injection vulnerabilities
|
||||
|
||||
### 2. Backdoor Detection
|
||||
- Look for obfuscated code (base64, hex encoding)
|
||||
- Identify suspicious network requests to unknown domains
|
||||
- Detect file hash patterns matching known malware
|
||||
- Check for hidden data exfiltration mechanisms
|
||||
|
||||
### 3. Credential Theft
|
||||
- Detect attempts to access environment variables containing secrets
|
||||
- Identify file operations on sensitive paths (~/.ssh, ~/.aws, ~/.netrc)
|
||||
- Check for credential harvesting patterns
|
||||
- Look for keylogging or clipboard monitoring
|
||||
|
||||
### 4. Unauthorized Network Access
|
||||
- Identify external network requests
|
||||
- Check for connections to suspicious domains (pastebin, ngrok, bit.ly, etc.)
|
||||
- Detect data exfiltration via HTTP/HTTPS
|
||||
- Look for reverse shell patterns
|
||||
|
||||
### 5. File System Abuse
|
||||
- Detect destructive file operations (rm -rf, shutil.rmtree)
|
||||
- Identify unauthorized file writes to system directories
|
||||
- Check for file permission modifications
|
||||
- Look for attempts to modify critical system files
|
||||
|
||||
### 6. Privilege Escalation
|
||||
- Detect sudo or privilege escalation attempts
|
||||
- Identify attempts to modify system configurations
|
||||
- Check for container escape patterns
|
||||
|
||||
### 7. Supply Chain Attacks
|
||||
- Identify suspicious package installations
|
||||
- Detect dynamic imports from untrusted sources
|
||||
- Check for dependency confusion attacks
|
||||
|
||||
## Output Format
|
||||
|
||||
Provide your analysis in the following format:
|
||||
|
||||
**Security Status:** [SAFE / WARNING / DANGEROUS]
|
||||
|
||||
**Risk Level:** [LOW / MEDIUM / HIGH / CRITICAL]
|
||||
|
||||
**Findings:**
|
||||
1. [Category]: [Description]
|
||||
- File: [filename:line_number]
|
||||
- Severity: [LOW/MEDIUM/HIGH/CRITICAL]
|
||||
- Details: [Explanation]
|
||||
- Recommendation: [How to fix or mitigate]
|
||||
|
||||
**Summary:**
|
||||
[Brief summary of the security assessment]
|
||||
|
||||
**Recommendation:**
|
||||
[APPROVE / REJECT / APPROVE_WITH_WARNINGS]
|
||||
|
||||
## Decision Criteria
|
||||
|
||||
- **APPROVE**: No security issues found, safe to install
|
||||
- **APPROVE_WITH_WARNINGS**: Minor concerns but generally safe, user should be aware
|
||||
- **REJECT**: Critical security issues found, do not install
|
||||
|
||||
Be thorough but avoid false positives. Consider the context and legitimate use cases.
|
||||
```
|
||||
|
||||
## Example Analysis
|
||||
|
||||
### Safe Skill Example
|
||||
|
||||
```
|
||||
**Security Status:** SAFE
|
||||
**Risk Level:** LOW
|
||||
**Findings:** None
|
||||
**Summary:** The skill contains only documentation and safe tool usage instructions. No executable code or suspicious patterns detected.
|
||||
**Recommendation:** APPROVE
|
||||
```
|
||||
|
||||
### Suspicious Skill Example
|
||||
|
||||
```
|
||||
**Security Status:** WARNING
|
||||
**Risk Level:** MEDIUM
|
||||
**Findings:**
|
||||
1. [Network Access]: External HTTP request detected
|
||||
- File: scripts/helper.py:42
|
||||
- Severity: MEDIUM
|
||||
- Details: Script makes HTTP request to api.example.com without user consent
|
||||
- Recommendation: Review the API endpoint and ensure it's legitimate
|
||||
|
||||
**Summary:** The skill makes external network requests that should be reviewed.
|
||||
**Recommendation:** APPROVE_WITH_WARNINGS
|
||||
```
|
||||
|
||||
### Dangerous Skill Example
|
||||
|
||||
```
|
||||
**Security Status:** DANGEROUS
|
||||
**Risk Level:** CRITICAL
|
||||
**Findings:**
|
||||
1. [Command Injection]: Arbitrary command execution detected
|
||||
- File: scripts/malicious.py:15
|
||||
- Severity: CRITICAL
|
||||
- Details: Uses subprocess.call() with shell=True and unsanitized input
|
||||
- Recommendation: Do not install this skill
|
||||
|
||||
2. [Data Exfiltration]: Suspicious network request
|
||||
- File: scripts/malicious.py:28
|
||||
- Severity: HIGH
|
||||
- Details: Sends data to pastebin.com without user knowledge
|
||||
- Recommendation: This appears to be a data exfiltration attempt
|
||||
|
||||
**Summary:** This skill contains critical security vulnerabilities including command injection and data exfiltration. It appears to be malicious.
|
||||
**Recommendation:** REJECT
|
||||
```
|
||||
@@ -1,199 +0,0 @@
|
||||
---
|
||||
name: test-cases
|
||||
description: This skill should be used when generating comprehensive test cases from PRD documents or user requirements. Triggers when users request test case generation, QA planning, test scenario creation, or need structured test documentation. Produces detailed test cases covering functional, edge case, error handling, and state transition scenarios.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Test Cases Generator
|
||||
|
||||
This skill generates comprehensive, requirement-driven test cases from PRD documents or user requirements.
|
||||
|
||||
## Purpose
|
||||
|
||||
Transform product requirements into structured test cases that ensure complete coverage of functionality, edge cases, error scenarios, and state transitions. The skill follows a pragmatic testing philosophy: test what matters, ensure every requirement has corresponding test coverage, and maintain test quality over quantity.
|
||||
|
||||
## When to Use
|
||||
|
||||
Trigger this skill when:
|
||||
- User provides a PRD or requirements document and requests test cases
|
||||
- User asks to "generate test cases", "create test scenarios", or "plan QA"
|
||||
- User mentions testing coverage for a feature or requirement
|
||||
- User needs structured test documentation in markdown format
|
||||
|
||||
## Core Testing Principles
|
||||
|
||||
Follow these principles when generating test cases:
|
||||
|
||||
1. **Requirement-driven, not implementation-driven** - Test cases must map directly to requirements, not implementation details
|
||||
2. **Complete coverage** - Every requirement must have at least one test case covering:
|
||||
- Happy path (normal use cases)
|
||||
- Edge cases (boundary values, empty inputs, max limits)
|
||||
- Error handling (invalid inputs, failure scenarios, permission errors)
|
||||
- State transitions (if stateful, cover all valid state changes)
|
||||
3. **Clear and actionable** - Each test case must be executable by a QA engineer without ambiguity
|
||||
4. **Traceable** - Maintain clear mapping between requirements and test cases
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Gather Requirements
|
||||
|
||||
First, identify the source of requirements:
|
||||
|
||||
1. If user provides a file path to a PRD, read it using the Read tool
|
||||
2. If user describes requirements verbally, capture them
|
||||
3. If requirements are unclear or incomplete, use AskUserQuestion to clarify:
|
||||
- What are the core user flows?
|
||||
- What are the acceptance criteria?
|
||||
- What are the edge cases or error scenarios to consider?
|
||||
- Are there any state transitions or workflows?
|
||||
- What platforms or environments need testing?
|
||||
|
||||
### Step 2: Extract Test Scenarios
|
||||
|
||||
Analyze requirements and extract test scenarios:
|
||||
|
||||
1. **Functional scenarios** - Normal use cases from requirements
|
||||
2. **Edge case scenarios** - Boundary conditions, empty states, maximum limits
|
||||
3. **Error scenarios** - Invalid inputs, permission failures, network errors
|
||||
4. **State transition scenarios** - If the feature involves state, map all transitions
|
||||
|
||||
For each requirement, identify:
|
||||
- Preconditions (what must be true before testing)
|
||||
- Test steps (actions to perform)
|
||||
- Expected results (what should happen)
|
||||
- Postconditions (state after test completes)
|
||||
|
||||
### Step 3: Structure Test Cases
|
||||
|
||||
Organize test cases using this structure:
|
||||
|
||||
```markdown
|
||||
# Test Cases: [Feature Name]
|
||||
|
||||
## Overview
|
||||
- **Feature**: [Feature name]
|
||||
- **Requirements Source**: [PRD file path or description]
|
||||
- **Test Coverage**: [Summary of what's covered]
|
||||
- **Last Updated**: [Date]
|
||||
|
||||
## Test Case Categories
|
||||
|
||||
### 1. Functional Tests
|
||||
Test cases covering normal user flows and core functionality.
|
||||
|
||||
#### TC-F-001: [Test Case Title]
|
||||
- **Requirement**: [Link to specific requirement]
|
||||
- **Priority**: [High/Medium/Low]
|
||||
- **Preconditions**:
|
||||
- [Condition 1]
|
||||
- [Condition 2]
|
||||
- **Test Steps**:
|
||||
1. [Step 1]
|
||||
2. [Step 2]
|
||||
3. [Step 3]
|
||||
- **Expected Results**:
|
||||
- [Expected result 1]
|
||||
- [Expected result 2]
|
||||
- **Postconditions**: [State after test]
|
||||
|
||||
### 2. Edge Case Tests
|
||||
Test cases covering boundary conditions and unusual inputs.
|
||||
|
||||
#### TC-E-001: [Test Case Title]
|
||||
[Same structure as above]
|
||||
|
||||
### 3. Error Handling Tests
|
||||
Test cases covering error scenarios and failure modes.
|
||||
|
||||
#### TC-ERR-001: [Test Case Title]
|
||||
[Same structure as above]
|
||||
|
||||
### 4. State Transition Tests
|
||||
Test cases covering state changes and workflows (if applicable).
|
||||
|
||||
#### TC-ST-001: [Test Case Title]
|
||||
[Same structure as above]
|
||||
|
||||
## Test Coverage Matrix
|
||||
|
||||
| Requirement ID | Test Cases | Coverage Status |
|
||||
|---------------|------------|-----------------|
|
||||
| REQ-001 | TC-F-001, TC-E-001 | ✓ Complete |
|
||||
| REQ-002 | TC-F-002 | ⚠ Partial |
|
||||
|
||||
## Notes
|
||||
- [Any additional testing considerations]
|
||||
- [Known limitations or assumptions]
|
||||
```
|
||||
|
||||
### Step 4: Generate Test Cases
|
||||
|
||||
For each identified scenario, create a detailed test case following the structure above. Ensure:
|
||||
|
||||
1. **Unique IDs** - Use prefixes: TC-F (functional), TC-E (edge), TC-ERR (error), TC-ST (state)
|
||||
2. **Clear titles** - Descriptive titles that explain what's being tested
|
||||
3. **Requirement traceability** - Link each test case to specific requirements
|
||||
4. **Priority assignment** - Mark critical paths as High priority
|
||||
5. **Executable steps** - Steps must be clear enough for any QA engineer to execute
|
||||
6. **Measurable results** - Expected results must be verifiable
|
||||
|
||||
### Step 5: Validate Coverage
|
||||
|
||||
Before finalizing, verify:
|
||||
|
||||
1. Every requirement has at least one test case
|
||||
2. Happy path is covered for all user flows
|
||||
3. Edge cases are identified for boundary conditions
|
||||
4. Error scenarios are covered for failure modes
|
||||
5. State transitions are tested if feature is stateful
|
||||
|
||||
If coverage gaps exist, generate additional test cases.
|
||||
|
||||
### Step 6: Output Test Cases
|
||||
|
||||
Write the test cases to `tests/<name>-test-cases.md` where `<name>` is derived from:
|
||||
- The feature name from the PRD
|
||||
- The user's specified name
|
||||
- A sanitized version of the requirement title
|
||||
|
||||
Use the Write tool to create the file with the structured test cases.
|
||||
|
||||
### Step 7: Summary
|
||||
|
||||
After generating test cases, provide a brief summary in Chinese:
|
||||
- Total number of test cases generated
|
||||
- Coverage breakdown (functional, edge, error, state)
|
||||
- Any assumptions made or areas needing clarification
|
||||
- File path where test cases were saved
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before finalizing test cases, verify:
|
||||
|
||||
- [ ] Every requirement has corresponding test cases
|
||||
- [ ] Happy path scenarios are covered
|
||||
- [ ] Edge cases include boundary values, empty inputs, max limits
|
||||
- [ ] Error handling covers invalid inputs and failure scenarios
|
||||
- [ ] State transitions are tested if applicable
|
||||
- [ ] Test case IDs are unique and follow naming convention
|
||||
- [ ] Test steps are clear and executable
|
||||
- [ ] Expected results are measurable and verifiable
|
||||
- [ ] Coverage matrix shows complete coverage
|
||||
- [ ] File is written to tests/<name>-test-cases.md
|
||||
|
||||
## Example Usage
|
||||
|
||||
**User**: "Generate test cases for the user authentication feature in docs/auth-prd.md"
|
||||
|
||||
**Process**:
|
||||
1. Read docs/auth-prd.md
|
||||
2. Extract requirements: login, logout, password reset, session management
|
||||
3. Identify scenarios: successful login, invalid credentials, expired session, etc.
|
||||
4. Generate test cases covering all scenarios
|
||||
5. Write to tests/auth-test-cases.md
|
||||
6. Summarize coverage in Chinese
|
||||
|
||||
## References
|
||||
|
||||
For detailed testing methodologies and best practices, see:
|
||||
- `references/testing-principles.md` - Core testing principles and patterns
|
||||
@@ -1,224 +0,0 @@
|
||||
# Testing Principles and Best Practices
|
||||
|
||||
## Core Philosophy
|
||||
|
||||
**Test what matters** - Focus on functionality that impacts users: behavior, performance, data integrity, and user experience. Avoid testing implementation details that can change without affecting outcomes.
|
||||
|
||||
**Requirement-driven testing** - Every test must trace back to a specific requirement. If a requirement exists without tests, coverage is incomplete. If a test exists without a requirement, it may be testing implementation rather than behavior.
|
||||
|
||||
**Quality over quantity** - A small set of stable, meaningful tests is more valuable than extensive flaky tests. Flaky tests erode trust and waste time. Every shipped bug represents a process failure.
|
||||
|
||||
## Coverage Requirements
|
||||
|
||||
### 1. Happy Path Coverage
|
||||
Test all normal use cases from requirements:
|
||||
- Primary user flows
|
||||
- Expected inputs and outputs
|
||||
- Standard workflows
|
||||
- Common scenarios
|
||||
|
||||
**Example**: For a login feature, test successful login with valid credentials.
|
||||
|
||||
### 2. Edge Case Coverage
|
||||
Test boundary conditions and unusual inputs:
|
||||
- Empty inputs (null, undefined, empty string, empty array)
|
||||
- Boundary values (min, max, zero, negative)
|
||||
- Maximum limits (character limits, file size limits, array lengths)
|
||||
- Special characters and encoding
|
||||
- Concurrent operations
|
||||
|
||||
**Example**: For a login feature, test with empty username, maximum length password, special characters in credentials.
|
||||
|
||||
### 3. Error Handling Coverage
|
||||
Test failure scenarios and error conditions:
|
||||
- Invalid inputs (wrong type, format, range)
|
||||
- Permission errors (unauthorized access, insufficient privileges)
|
||||
- Network failures (timeout, connection lost, server error)
|
||||
- Resource exhaustion (out of memory, disk full)
|
||||
- Dependency failures (database down, API unavailable)
|
||||
|
||||
**Example**: For a login feature, test with invalid credentials, account locked, server timeout.
|
||||
|
||||
### 4. State Transition Coverage
|
||||
If the feature involves state, test all valid state changes:
|
||||
- Initial state to each possible next state
|
||||
- All valid state transitions
|
||||
- Invalid state transitions (should be rejected)
|
||||
- State persistence across sessions
|
||||
- Concurrent state modifications
|
||||
|
||||
**Example**: For a login feature, test transitions: logged out → logging in → logged in → logging out → logged out.
|
||||
|
||||
## Test Case Structure
|
||||
|
||||
### Essential Components
|
||||
|
||||
Every test case must include:
|
||||
|
||||
1. **Unique ID** - Consistent naming convention (TC-F-001, TC-E-001, etc.)
|
||||
2. **Title** - Clear, descriptive name explaining what's being tested
|
||||
3. **Requirement Link** - Traceability to specific requirement
|
||||
4. **Priority** - High/Medium/Low based on user impact
|
||||
5. **Preconditions** - State that must exist before test execution
|
||||
6. **Test Steps** - Clear, numbered, executable actions
|
||||
7. **Expected Results** - Measurable, verifiable outcomes
|
||||
8. **Postconditions** - State after test completion
|
||||
|
||||
### Test Case Naming Convention
|
||||
|
||||
Use prefixes to categorize test cases:
|
||||
- **TC-F-XXX**: Functional tests (happy path)
|
||||
- **TC-E-XXX**: Edge case tests (boundaries)
|
||||
- **TC-ERR-XXX**: Error handling tests (failures)
|
||||
- **TC-ST-XXX**: State transition tests (workflows)
|
||||
- **TC-PERF-XXX**: Performance tests (speed, load)
|
||||
- **TC-SEC-XXX**: Security tests (auth, permissions)
|
||||
|
||||
## Test Design Patterns
|
||||
|
||||
### Pattern 1: Arrange-Act-Assert (AAA)
|
||||
|
||||
Structure test steps using AAA pattern:
|
||||
1. **Arrange** - Set up preconditions and test data
|
||||
2. **Act** - Execute the action being tested
|
||||
3. **Assert** - Verify expected results
|
||||
|
||||
**Example**:
|
||||
```
|
||||
Preconditions:
|
||||
- User account exists with username "testuser"
|
||||
- User is logged out
|
||||
|
||||
Test Steps:
|
||||
1. Navigate to login page (Arrange)
|
||||
2. Enter username "testuser" and password "password123" (Arrange)
|
||||
3. Click "Login" button (Act)
|
||||
4. Verify user is redirected to dashboard (Assert)
|
||||
5. Verify welcome message displays "Welcome, testuser" (Assert)
|
||||
```
|
||||
|
||||
### Pattern 2: Equivalence Partitioning
|
||||
|
||||
Group inputs into equivalence classes and test one representative from each class:
|
||||
- Valid equivalence class
|
||||
- Invalid equivalence classes
|
||||
- Boundary values
|
||||
|
||||
**Example**: For age input (valid range 18-100):
|
||||
- Valid class: 18, 50, 100
|
||||
- Invalid class: 17, 101, -1, "abc"
|
||||
- Boundaries: 17, 18, 100, 101
|
||||
|
||||
### Pattern 3: State Transition Testing
|
||||
|
||||
For stateful features, create a state transition table and test each transition:
|
||||
|
||||
| Current State | Action | Next State | Test Case |
|
||||
|--------------|--------|------------|-----------|
|
||||
| Logged Out | Login Success | Logged In | TC-ST-001 |
|
||||
| Logged Out | Login Failure | Logged Out | TC-ST-002 |
|
||||
| Logged In | Logout | Logged Out | TC-ST-003 |
|
||||
| Logged In | Session Timeout | Logged Out | TC-ST-004 |
|
||||
|
||||
## Test Prioritization
|
||||
|
||||
Prioritize test cases based on:
|
||||
|
||||
1. **High Priority**
|
||||
- Core user flows (login, checkout, data submission)
|
||||
- Data integrity (create, update, delete operations)
|
||||
- Security-critical paths (authentication, authorization)
|
||||
- Revenue-impacting features (payment, subscription)
|
||||
|
||||
2. **Medium Priority**
|
||||
- Secondary user flows
|
||||
- Edge cases for high-priority features
|
||||
- Error handling for common failures
|
||||
- Performance-sensitive operations
|
||||
|
||||
3. **Low Priority**
|
||||
- Rare edge cases
|
||||
- Cosmetic issues
|
||||
- Nice-to-have features
|
||||
- Non-critical error scenarios
|
||||
|
||||
## Test Quality Indicators
|
||||
|
||||
### Good Test Cases
|
||||
- ✓ Maps directly to a requirement
|
||||
- ✓ Tests behavior, not implementation
|
||||
- ✓ Has clear, executable steps
|
||||
- ✓ Has measurable expected results
|
||||
- ✓ Is independent of other tests
|
||||
- ✓ Is repeatable and deterministic
|
||||
- ✓ Fails only when behavior is broken
|
||||
|
||||
### Poor Test Cases
|
||||
- ✗ Tests implementation details
|
||||
- ✗ Has vague or ambiguous steps
|
||||
- ✗ Has unmeasurable expected results
|
||||
- ✗ Depends on execution order
|
||||
- ✗ Is flaky or non-deterministic
|
||||
- ✗ Fails due to environment issues
|
||||
|
||||
## Coverage Validation
|
||||
|
||||
Before finalizing test cases, verify:
|
||||
|
||||
1. **Requirement Coverage**
|
||||
- Every requirement has at least one test case
|
||||
- Critical requirements have multiple test cases
|
||||
- Coverage matrix shows complete mapping
|
||||
|
||||
2. **Scenario Coverage**
|
||||
- Happy path: All normal flows covered
|
||||
- Edge cases: Boundaries and limits covered
|
||||
- Error handling: Failure modes covered
|
||||
- State transitions: All valid transitions covered
|
||||
|
||||
3. **Risk Coverage**
|
||||
- High-risk areas have comprehensive coverage
|
||||
- Security-sensitive features are thoroughly tested
|
||||
- Data integrity operations are validated
|
||||
|
||||
## Common Pitfalls to Avoid
|
||||
|
||||
1. **Testing implementation instead of behavior** - Focus on what the system does, not how it does it
|
||||
2. **Incomplete edge case coverage** - Don't forget empty inputs, boundaries, and limits
|
||||
3. **Missing error scenarios** - Test failure modes, not just success paths
|
||||
4. **Vague expected results** - Make results measurable and verifiable
|
||||
5. **Test interdependencies** - Each test should be independent
|
||||
6. **Ignoring state transitions** - For stateful features, test all transitions
|
||||
7. **Over-testing trivial code** - Focus on logic that matters to users
|
||||
|
||||
## Test Documentation Standards
|
||||
|
||||
### File Organization
|
||||
```
|
||||
tests/
|
||||
├── <feature>-test-cases.md # Test cases for specific feature
|
||||
├── <module>-test-cases.md # Test cases for specific module
|
||||
└── integration-test-cases.md # Cross-feature integration tests
|
||||
```
|
||||
|
||||
### Markdown Structure
|
||||
- Use clear headings for test categories
|
||||
- Use tables for coverage matrices
|
||||
- Use code blocks for test data examples
|
||||
- Use checkboxes for test execution tracking
|
||||
- Include metadata (feature, date, version)
|
||||
|
||||
### Maintenance
|
||||
- Update test cases when requirements change
|
||||
- Remove obsolete test cases
|
||||
- Add new test cases for bug fixes
|
||||
- Review coverage regularly
|
||||
- Keep test cases synchronized with implementation
|
||||
|
||||
## References
|
||||
|
||||
These principles are derived from:
|
||||
- Industry-standard QA practices
|
||||
- Game QA methodologies (Unity Test Framework, Unreal Automation, Godot GUT)
|
||||
- Pragmatic testing philosophy: "Test what matters"
|
||||
- Requirement-driven testing approach from CLAUDE.md context
|
||||
@@ -1,67 +0,0 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
echo Testing PATH update with long strings...
|
||||
echo.
|
||||
|
||||
rem Create a very long PATH string (over 1024 characters)
|
||||
set "LONG_PATH="
|
||||
for /L %%i in (1,1,30) do (
|
||||
set "LONG_PATH=!LONG_PATH!C:\VeryLongDirectoryName%%i\SubDirectory\AnotherSubDirectory;"
|
||||
)
|
||||
|
||||
echo Generated PATH length:
|
||||
echo !LONG_PATH! > temp_path.txt
|
||||
for %%A in (temp_path.txt) do set "PATH_LENGTH=%%~zA"
|
||||
del temp_path.txt
|
||||
echo !PATH_LENGTH! bytes
|
||||
|
||||
rem Test 1: Verify reg add can handle long strings
|
||||
echo.
|
||||
echo Test 1: Testing reg add with long PATH...
|
||||
set "TEST_PATH=!LONG_PATH!%%USERPROFILE%%\bin"
|
||||
reg add "HKCU\Environment" /v TestPath /t REG_EXPAND_SZ /d "!TEST_PATH!" /f >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo FAIL: reg add failed with long PATH
|
||||
goto :cleanup
|
||||
) else (
|
||||
echo PASS: reg add succeeded with long PATH
|
||||
)
|
||||
|
||||
rem Test 2: Verify the value was stored correctly
|
||||
echo.
|
||||
echo Test 2: Verifying stored value length...
|
||||
for /f "tokens=2*" %%A in ('reg query "HKCU\Environment" /v TestPath 2^>nul ^| findstr /I "TestPath"') do set "STORED_PATH=%%B"
|
||||
echo !STORED_PATH! > temp_stored.txt
|
||||
for %%A in (temp_stored.txt) do set "STORED_LENGTH=%%~zA"
|
||||
del temp_stored.txt
|
||||
echo Stored PATH length: !STORED_LENGTH! bytes
|
||||
|
||||
if !STORED_LENGTH! LSS 1024 (
|
||||
echo FAIL: Stored PATH was truncated
|
||||
goto :cleanup
|
||||
) else (
|
||||
echo PASS: Stored PATH was not truncated
|
||||
)
|
||||
|
||||
rem Test 3: Verify %%USERPROFILE%%\bin is present
|
||||
echo.
|
||||
echo Test 3: Verifying %%USERPROFILE%%\bin is in stored PATH...
|
||||
echo !STORED_PATH! | findstr /I "USERPROFILE" >nul
|
||||
if errorlevel 1 (
|
||||
echo FAIL: %%USERPROFILE%%\bin not found in stored PATH
|
||||
goto :cleanup
|
||||
) else (
|
||||
echo PASS: %%USERPROFILE%%\bin found in stored PATH
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo All tests PASSED
|
||||
echo ========================================
|
||||
|
||||
:cleanup
|
||||
echo.
|
||||
echo Cleaning up test registry key...
|
||||
reg delete "HKCU\Environment" /v TestPath /f >nul 2>nul
|
||||
endlocal
|
||||
302
uninstall.py
302
uninstall.py
@@ -1,302 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Uninstaller for myclaude - reads installed_modules.json for precise removal."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
DEFAULT_INSTALL_DIR = "~/.claude"
|
||||
|
||||
# Files created by installer itself (not by modules)
|
||||
INSTALLER_FILES = ["install.log", "installed_modules.json", "installed_modules.json.bak"]
|
||||
|
||||
|
||||
def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Uninstall myclaude")
|
||||
parser.add_argument(
|
||||
"--install-dir",
|
||||
default=DEFAULT_INSTALL_DIR,
|
||||
help="Installation directory (defaults to ~/.claude)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--module",
|
||||
help="Comma-separated modules to uninstall (default: all installed)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list",
|
||||
action="store_true",
|
||||
help="List installed modules and exit",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Show what would be removed without actually removing",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--purge",
|
||||
action="store_true",
|
||||
help="Remove entire install directory (DANGEROUS: removes user files too)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-y", "--yes",
|
||||
action="store_true",
|
||||
help="Skip confirmation prompt",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def load_installed_modules(install_dir: Path) -> Dict[str, Any]:
|
||||
"""Load installed_modules.json to know what was installed."""
|
||||
status_file = install_dir / "installed_modules.json"
|
||||
if not status_file.exists():
|
||||
return {}
|
||||
try:
|
||||
with status_file.open("r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return {}
|
||||
|
||||
|
||||
def load_config(install_dir: Path) -> Dict[str, Any]:
|
||||
"""Try to load config.json from source repo to understand module structure."""
|
||||
# Look for config.json in common locations
|
||||
candidates = [
|
||||
Path(__file__).parent / "config.json",
|
||||
install_dir / "config.json",
|
||||
]
|
||||
for path in candidates:
|
||||
if path.exists():
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
continue
|
||||
return {}
|
||||
|
||||
|
||||
def get_module_files(module_name: str, config: Dict[str, Any]) -> Set[str]:
|
||||
"""Extract files/dirs that a module installs based on config.json operations."""
|
||||
files: Set[str] = set()
|
||||
modules = config.get("modules", {})
|
||||
module_cfg = modules.get(module_name, {})
|
||||
|
||||
for op in module_cfg.get("operations", []):
|
||||
op_type = op.get("type", "")
|
||||
target = op.get("target", "")
|
||||
|
||||
if op_type == "copy_file" and target:
|
||||
files.add(target)
|
||||
elif op_type == "copy_dir" and target:
|
||||
files.add(target)
|
||||
elif op_type == "merge_dir":
|
||||
# merge_dir merges subdirs like commands/, agents/ into install_dir
|
||||
source = op.get("source", "")
|
||||
source_path = Path(__file__).parent / source
|
||||
if source_path.exists():
|
||||
for subdir in source_path.iterdir():
|
||||
if subdir.is_dir():
|
||||
files.add(subdir.name)
|
||||
elif op_type == "run_command":
|
||||
# install.sh installs bin/codeagent-wrapper
|
||||
cmd = op.get("command", "")
|
||||
if "install.sh" in cmd or "install.bat" in cmd:
|
||||
files.add("bin/codeagent-wrapper")
|
||||
files.add("bin")
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def cleanup_shell_config(rc_file: Path, bin_dir: Path) -> bool:
|
||||
"""Remove PATH export added by installer from shell config."""
|
||||
if not rc_file.exists():
|
||||
return False
|
||||
|
||||
content = rc_file.read_text(encoding="utf-8")
|
||||
original = content
|
||||
|
||||
patterns = [
|
||||
r"\n?# Added by myclaude installer\n",
|
||||
rf'\nexport PATH="{re.escape(str(bin_dir))}:\$PATH"\n?',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
content = re.sub(pattern, "\n", content)
|
||||
|
||||
content = re.sub(r"\n{3,}$", "\n\n", content)
|
||||
|
||||
if content != original:
|
||||
rc_file.write_text(content, encoding="utf-8")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def list_installed(install_dir: Path) -> None:
|
||||
"""List installed modules."""
|
||||
status = load_installed_modules(install_dir)
|
||||
modules = status.get("modules", {})
|
||||
|
||||
if not modules:
|
||||
print("No modules installed (installed_modules.json not found or empty)")
|
||||
return
|
||||
|
||||
print(f"Installed modules in {install_dir}:")
|
||||
print(f"{'Module':<15} {'Status':<10} {'Installed At'}")
|
||||
print("-" * 50)
|
||||
for name, info in modules.items():
|
||||
st = info.get("status", "unknown")
|
||||
ts = info.get("installed_at", "unknown")[:19]
|
||||
print(f"{name:<15} {st:<10} {ts}")
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None) -> int:
|
||||
args = parse_args(argv)
|
||||
install_dir = Path(args.install_dir).expanduser().resolve()
|
||||
bin_dir = install_dir / "bin"
|
||||
|
||||
if not install_dir.exists():
|
||||
print(f"Install directory not found: {install_dir}")
|
||||
print("Nothing to uninstall.")
|
||||
return 0
|
||||
|
||||
if args.list:
|
||||
list_installed(install_dir)
|
||||
return 0
|
||||
|
||||
# Load installation status
|
||||
status = load_installed_modules(install_dir)
|
||||
installed_modules = status.get("modules", {})
|
||||
config = load_config(install_dir)
|
||||
|
||||
# Determine which modules to uninstall
|
||||
if args.module:
|
||||
selected = [m.strip() for m in args.module.split(",") if m.strip()]
|
||||
# Validate
|
||||
for m in selected:
|
||||
if m not in installed_modules:
|
||||
print(f"Error: Module '{m}' is not installed")
|
||||
print("Use --list to see installed modules")
|
||||
return 1
|
||||
else:
|
||||
selected = list(installed_modules.keys())
|
||||
|
||||
if not selected and not args.purge:
|
||||
print("No modules to uninstall.")
|
||||
print("Use --list to see installed modules, or --purge to remove everything.")
|
||||
return 0
|
||||
|
||||
# Collect files to remove
|
||||
files_to_remove: Set[str] = set()
|
||||
for module_name in selected:
|
||||
files_to_remove.update(get_module_files(module_name, config))
|
||||
|
||||
# Add installer files if removing all modules
|
||||
if set(selected) == set(installed_modules.keys()):
|
||||
files_to_remove.update(INSTALLER_FILES)
|
||||
|
||||
# Show what will be removed
|
||||
print(f"Install directory: {install_dir}")
|
||||
if args.purge:
|
||||
print(f"\n⚠️ PURGE MODE: Will remove ENTIRE directory including user files!")
|
||||
else:
|
||||
print(f"\nModules to uninstall: {', '.join(selected)}")
|
||||
print(f"\nFiles/directories to remove:")
|
||||
for f in sorted(files_to_remove):
|
||||
path = install_dir / f
|
||||
exists = "✓" if path.exists() else "✗ (not found)"
|
||||
print(f" {f} {exists}")
|
||||
|
||||
# Confirmation
|
||||
if not args.yes and not args.dry_run:
|
||||
prompt = "\nProceed with uninstallation? [y/N] "
|
||||
response = input(prompt).strip().lower()
|
||||
if response not in ("y", "yes"):
|
||||
print("Aborted.")
|
||||
return 0
|
||||
|
||||
if args.dry_run:
|
||||
print("\n[Dry run] No files were removed.")
|
||||
return 0
|
||||
|
||||
print(f"\nUninstalling...")
|
||||
removed: List[str] = []
|
||||
|
||||
if args.purge:
|
||||
shutil.rmtree(install_dir)
|
||||
print(f" ✓ Removed {install_dir}")
|
||||
removed.append(str(install_dir))
|
||||
else:
|
||||
# Remove files/dirs in reverse order (files before parent dirs)
|
||||
for item in sorted(files_to_remove, key=lambda x: x.count("/"), reverse=True):
|
||||
path = install_dir / item
|
||||
if not path.exists():
|
||||
continue
|
||||
try:
|
||||
if path.is_dir():
|
||||
# Only remove if empty or if it's a known module dir
|
||||
if item in ("bin",):
|
||||
# For bin, only remove codeagent-wrapper
|
||||
wrapper = path / "codeagent-wrapper"
|
||||
if wrapper.exists():
|
||||
wrapper.unlink()
|
||||
print(f" ✓ Removed bin/codeagent-wrapper")
|
||||
removed.append("bin/codeagent-wrapper")
|
||||
# Remove bin if empty
|
||||
if path.exists() and not any(path.iterdir()):
|
||||
path.rmdir()
|
||||
print(f" ✓ Removed empty bin/")
|
||||
else:
|
||||
shutil.rmtree(path)
|
||||
print(f" ✓ Removed {item}/")
|
||||
removed.append(item)
|
||||
else:
|
||||
path.unlink()
|
||||
print(f" ✓ Removed {item}")
|
||||
removed.append(item)
|
||||
except OSError as e:
|
||||
print(f" ✗ Failed to remove {item}: {e}", file=sys.stderr)
|
||||
|
||||
# Update installed_modules.json
|
||||
status_file = install_dir / "installed_modules.json"
|
||||
if status_file.exists() and selected != list(installed_modules.keys()):
|
||||
# Partial uninstall: update status file
|
||||
for m in selected:
|
||||
installed_modules.pop(m, None)
|
||||
if installed_modules:
|
||||
with status_file.open("w", encoding="utf-8") as f:
|
||||
json.dump({"modules": installed_modules}, f, indent=2)
|
||||
print(f" ✓ Updated installed_modules.json")
|
||||
|
||||
# Remove install dir if empty
|
||||
if install_dir.exists() and not any(install_dir.iterdir()):
|
||||
install_dir.rmdir()
|
||||
print(f" ✓ Removed empty install directory")
|
||||
|
||||
# Clean shell configs
|
||||
for rc_name in (".bashrc", ".zshrc"):
|
||||
rc_file = Path.home() / rc_name
|
||||
if cleanup_shell_config(rc_file, bin_dir):
|
||||
print(f" ✓ Cleaned PATH from {rc_name}")
|
||||
|
||||
print("")
|
||||
if removed:
|
||||
print(f"✓ Uninstallation complete ({len(removed)} items removed)")
|
||||
else:
|
||||
print("✓ Nothing to remove")
|
||||
|
||||
if install_dir.exists() and any(install_dir.iterdir()):
|
||||
remaining = list(install_dir.iterdir())
|
||||
print(f"\nNote: {len(remaining)} items remain in {install_dir}")
|
||||
print("These are either user files or from other modules.")
|
||||
print("Use --purge to remove everything (DANGEROUS).")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
225
uninstall.sh
225
uninstall.sh
@@ -1,225 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
INSTALL_DIR="${INSTALL_DIR:-$HOME/.claude}"
|
||||
BIN_DIR="${INSTALL_DIR}/bin"
|
||||
STATUS_FILE="${INSTALL_DIR}/installed_modules.json"
|
||||
DRY_RUN=false
|
||||
PURGE=false
|
||||
YES=false
|
||||
LIST_ONLY=false
|
||||
MODULES=""
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
Uninstall myclaude modules.
|
||||
|
||||
Options:
|
||||
--install-dir DIR Installation directory (default: ~/.claude)
|
||||
--module MODULES Comma-separated modules to uninstall (default: all)
|
||||
--list List installed modules and exit
|
||||
--dry-run Show what would be removed without removing
|
||||
--purge Remove entire install directory (DANGEROUS)
|
||||
-y, --yes Skip confirmation prompt
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$0 --list # List installed modules
|
||||
$0 --dry-run # Preview what would be removed
|
||||
$0 --module dev # Uninstall only 'dev' module
|
||||
$0 -y # Uninstall all without confirmation
|
||||
$0 --purge -y # Remove everything (DANGEROUS)
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--install-dir) INSTALL_DIR="$2"; BIN_DIR="${INSTALL_DIR}/bin"; STATUS_FILE="${INSTALL_DIR}/installed_modules.json"; shift 2 ;;
|
||||
--module) MODULES="$2"; shift 2 ;;
|
||||
--list) LIST_ONLY=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--purge) PURGE=true; shift ;;
|
||||
-y|--yes) YES=true; shift ;;
|
||||
-h|--help) usage ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check if install dir exists
|
||||
if [ ! -d "$INSTALL_DIR" ]; then
|
||||
echo "Install directory not found: $INSTALL_DIR"
|
||||
echo "Nothing to uninstall."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# List installed modules
|
||||
list_modules() {
|
||||
if [ ! -f "$STATUS_FILE" ]; then
|
||||
echo "No modules installed (installed_modules.json not found)"
|
||||
return
|
||||
fi
|
||||
echo "Installed modules in $INSTALL_DIR:"
|
||||
echo "Module Status Installed At"
|
||||
echo "--------------------------------------------------"
|
||||
# Parse JSON with basic tools (no jq dependency)
|
||||
python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
with open('$STATUS_FILE') as f:
|
||||
data = json.load(f)
|
||||
for name, info in data.get('modules', {}).items():
|
||||
status = info.get('status', 'unknown')
|
||||
ts = info.get('installed_at', 'unknown')[:19]
|
||||
print(f'{name:<15} {status:<10} {ts}')
|
||||
except Exception as e:
|
||||
print(f'Error reading status file: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
"
|
||||
}
|
||||
|
||||
if [ "$LIST_ONLY" = true ]; then
|
||||
list_modules
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Get installed modules from status file
|
||||
get_installed_modules() {
|
||||
if [ ! -f "$STATUS_FILE" ]; then
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
python3 -c "
|
||||
import json
|
||||
try:
|
||||
with open('$STATUS_FILE') as f:
|
||||
data = json.load(f)
|
||||
print(' '.join(data.get('modules', {}).keys()))
|
||||
except:
|
||||
print('')
|
||||
"
|
||||
}
|
||||
|
||||
INSTALLED=$(get_installed_modules)
|
||||
|
||||
# Determine modules to uninstall
|
||||
if [ -n "$MODULES" ]; then
|
||||
SELECTED="$MODULES"
|
||||
else
|
||||
SELECTED="$INSTALLED"
|
||||
fi
|
||||
|
||||
if [ -z "$SELECTED" ] && [ "$PURGE" != true ]; then
|
||||
echo "No modules to uninstall."
|
||||
echo "Use --list to see installed modules, or --purge to remove everything."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Install directory: $INSTALL_DIR"
|
||||
|
||||
if [ "$PURGE" = true ]; then
|
||||
echo ""
|
||||
echo "⚠️ PURGE MODE: Will remove ENTIRE directory including user files!"
|
||||
else
|
||||
echo ""
|
||||
echo "Modules to uninstall: $SELECTED"
|
||||
echo ""
|
||||
echo "Files/directories that may be removed:"
|
||||
for item in commands agents skills docs bin CLAUDE.md install.log installed_modules.json; do
|
||||
if [ -e "${INSTALL_DIR}/${item}" ]; then
|
||||
echo " $item ✓"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Confirmation
|
||||
if [ "$YES" != true ] && [ "$DRY_RUN" != true ]; then
|
||||
echo ""
|
||||
read -p "Proceed with uninstallation? [y/N] " response
|
||||
case "$response" in
|
||||
[yY]|[yY][eE][sS]) ;;
|
||||
*) echo "Aborted."; exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo ""
|
||||
echo "[Dry run] No files were removed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Uninstalling..."
|
||||
|
||||
if [ "$PURGE" = true ]; then
|
||||
rm -rf "$INSTALL_DIR"
|
||||
echo " ✓ Removed $INSTALL_DIR"
|
||||
else
|
||||
# Remove codeagent-wrapper binary
|
||||
if [ -f "${BIN_DIR}/codeagent-wrapper" ]; then
|
||||
rm -f "${BIN_DIR}/codeagent-wrapper"
|
||||
echo " ✓ Removed bin/codeagent-wrapper"
|
||||
fi
|
||||
|
||||
# Remove bin directory if empty
|
||||
if [ -d "$BIN_DIR" ] && [ -z "$(ls -A "$BIN_DIR" 2>/dev/null)" ]; then
|
||||
rmdir "$BIN_DIR"
|
||||
echo " ✓ Removed empty bin/"
|
||||
fi
|
||||
|
||||
# Remove installed directories
|
||||
for dir in commands agents skills docs; do
|
||||
if [ -d "${INSTALL_DIR}/${dir}" ]; then
|
||||
rm -rf "${INSTALL_DIR}/${dir}"
|
||||
echo " ✓ Removed ${dir}/"
|
||||
fi
|
||||
done
|
||||
|
||||
# Remove installed files
|
||||
for file in CLAUDE.md install.log installed_modules.json installed_modules.json.bak; do
|
||||
if [ -f "${INSTALL_DIR}/${file}" ]; then
|
||||
rm -f "${INSTALL_DIR}/${file}"
|
||||
echo " ✓ Removed ${file}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Remove install directory if empty
|
||||
if [ -d "$INSTALL_DIR" ] && [ -z "$(ls -A "$INSTALL_DIR" 2>/dev/null)" ]; then
|
||||
rmdir "$INSTALL_DIR"
|
||||
echo " ✓ Removed empty install directory"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up PATH from shell config files
|
||||
cleanup_shell_config() {
|
||||
local rc_file="$1"
|
||||
if [ -f "$rc_file" ]; then
|
||||
if grep -q "# Added by myclaude installer" "$rc_file" 2>/dev/null; then
|
||||
# Create backup
|
||||
cp "$rc_file" "${rc_file}.bak"
|
||||
# Remove myclaude lines
|
||||
grep -v "# Added by myclaude installer" "$rc_file" | \
|
||||
grep -v "export PATH=\"${BIN_DIR}:\$PATH\"" > "${rc_file}.tmp"
|
||||
mv "${rc_file}.tmp" "$rc_file"
|
||||
echo " ✓ Cleaned PATH from $(basename "$rc_file")"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup_shell_config "$HOME/.bashrc"
|
||||
cleanup_shell_config "$HOME/.zshrc"
|
||||
|
||||
echo ""
|
||||
echo "✓ Uninstallation complete"
|
||||
|
||||
# Check for remaining files
|
||||
if [ -d "$INSTALL_DIR" ] && [ -n "$(ls -A "$INSTALL_DIR" 2>/dev/null)" ]; then
|
||||
remaining=$(ls -1 "$INSTALL_DIR" 2>/dev/null | wc -l | tr -d ' ')
|
||||
echo ""
|
||||
echo "Note: $remaining items remain in $INSTALL_DIR"
|
||||
echo "These are either user files or from other modules."
|
||||
echo "Use --purge to remove everything (DANGEROUS)."
|
||||
fi
|
||||
Reference in New Issue
Block a user