Compare commits

...

7 Commits

Author SHA1 Message Date
cexll
1dd7b23942 feat harness skill 2026-02-14 23:58:38 +08:00
cexll
664d82795a fixed do worktree use error 2026-02-14 23:58:20 +08:00
cexll
7cc7f50f46 docs: add bilingual README (EN + CN) reflecting current codebase
Rewrite README.md in English and add README_CN.md in Chinese with
language switcher links. Updated to cover all recent features:
worktree isolation, skill auto-detection, dynamic agents, per-backend
config, allowed/disallowed tools, stderr noise filtering, cross-platform
support, and modular internal/ project structure.

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2026-02-10 23:13:06 +08:00
cexll
ebd795c583 feat(install): per-module agent merge and documentation overhaul
- Add per-module agent merge/unmerge for ~/.codeagent/models.json with
  __module__ tracking, user-customization protection, and agent restore
  on uninstall when shared by multiple modules
- Add post-install verification (wrapper version, PATH, backend CLIs)
- Install CLAUDE.md by default, best-effort (never crashes main flow)
- Fix 7-phase → 5-phase references across all docs
- Document 9 skills, 11 commands, claudekit module, OpenCode backend
- Add templates/models.json.example with all agent presets (do + omo)
- Fix empty parent directory cleanup on copy_file uninstall
- Update USER_GUIDE.md with 13 CLI flags and OpenCode backend

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2026-02-10 15:26:33 +08:00
cexll
8db49f198e fix(test): set USERPROFILE on Windows for skills tests
os.UserHomeDir() uses USERPROFILE on Windows, not HOME. Add
setTestHome helper that sets both env vars for cross-platform
compatibility in CI.

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2026-02-09 11:16:33 +08:00
cexll
97dfa907d9 feat(skills): add per-task skill spec auto-detection and injection
Replace external inject-spec.py hook with built-in zero-config skill
detection in codeagent-wrapper. The system auto-detects project type
from fingerprint files (go.mod, package.json, etc.), maps to installed
skills, and injects SKILL.md content directly into sub-agent prompts.

Key changes:
- Add DetectProjectSkills/ResolveSkillContent in executor/prompt.go
- Add Skills field to TaskSpec with parallel config parsing
- Add --skills CLI flag for explicit override
- Update /do SKILL.md Phase 4 with per-task skill examples
- Remove on-stop.py global hook (not needed)
- Replace inject-spec.py with no-op (detection now internal)
- Add 20 unit tests covering detection, resolution, budget, security

Security: path traversal protection via validSkillName regex,
16K char budget with tag overhead accounting, CRLF normalization.

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2026-02-09 11:06:36 +08:00
cexll
5853539cab fix(do): reuse worktree across phases via DO_WORKTREE_DIR env var
Previously, each codeagent-wrapper --worktree call created a new worktree,
causing multiple worktrees per /do task (one per phase).

Changes:
- setup-do.py: create worktree at initialization, export DO_WORKTREE_DIR
- executor.go: check DO_WORKTREE_DIR first, reuse if set
- SKILL.md: update documentation for new behavior

Generated with SWE-Agent.ai

Co-Authored-By: SWE-Agent.ai <noreply@swe-agent.ai>
2026-02-05 23:32:52 +08:00
34 changed files with 3044 additions and 492 deletions

View File

@@ -2,6 +2,47 @@
All notable changes to this project will be documented in this file.
## [6.7.0] - 2026-02-10
### 🚀 Features
- feat(install): per-module agent merge/unmerge for ~/.codeagent/models.json
- feat(install): post-install verification (wrapper version, PATH, backend CLIs)
- feat(install): install CLAUDE.md by default
- feat(docs): document 9 skills, 11 commands, claudekit module, OpenCode backend
### 🐛 Bug Fixes
- fix(docs): correct 7-phase → 5-phase for do skill across all docs
- fix(install): best-effort default config install (never crashes main flow)
- fix(install): interactive quit no longer triggers post-install actions
- fix(install): empty parent directory cleanup on copy_file uninstall
- fix(install): agent restore on uninstall when shared by multiple modules
- fix(docs): remove non-existent on-stop hook references
### 📚 Documentation
- Updated USER_GUIDE.md with 13 CLI flags and OpenCode backend
- Updated README.md/README_CN.md with complete module and skill listings
- Added templates/models.json.example with all agent presets (do + omo)
## [6.6.0] - 2026-02-10
### 🚀 Features
- feat(skills): add per-task skill spec auto-detection and injection
- feat: add worktree support and refactor do skill to Python
### 🐛 Bug Fixes
- fix(test): set USERPROFILE on Windows for skills tests
- fix(do): reuse worktree across phases via DO_WORKTREE_DIR env var
- fix(release): auto-generate release notes from git history
### 📚 Documentation
- audit and fix documentation, installation scripts, and default configuration
## [6.0.0] - 2026-01-26
### 🚀 Features

View File

@@ -19,13 +19,30 @@ npx github:cexll/myclaude
| Module | Description | Documentation |
|--------|-------------|---------------|
| [do](skills/do/README.md) | **Recommended** - 7-phase feature development with codeagent orchestration | `/do` command |
| [do](skills/do/README.md) | **Recommended** - 5-phase feature development with codeagent orchestration | `/do` command |
| [omo](skills/omo/README.md) | Multi-agent orchestration with intelligent routing | `/omo` command |
| [bmad](agents/bmad/README.md) | BMAD agile workflow with 6 specialized agents | `/bmad-pilot` command |
| [requirements](agents/requirements/README.md) | Lightweight requirements-to-code pipeline | `/requirements-pilot` command |
| [essentials](agents/development-essentials/README.md) | Core development commands and utilities | `/code`, `/debug`, etc. |
| [essentials](agents/development-essentials/README.md) | 11 core dev commands: ask, bugfix, code, debug, docs, enhance-prompt, optimize, refactor, review, test, think | `/code`, `/debug`, etc. |
| [sparv](skills/sparv/README.md) | SPARV workflow (Specify→Plan→Act→Review→Vault) | `/sparv` command |
| course | Course development (combines dev + product-requirements + test-cases) | Composite module |
| claudekit | ClaudeKit: do skill + global hooks (pre-bash, inject-spec, log-prompt) | Composite module |
### Available Skills
Individual skills can be installed separately via `npx github:cexll/myclaude --list` (skills bundled in modules like do, omo, sparv are listed above):
| Skill | Description |
|-------|-------------|
| browser | Browser automation for web testing and data extraction |
| codeagent | codeagent-wrapper invocation for multi-backend AI code tasks |
| codex | Direct Codex backend execution |
| dev | Lightweight end-to-end development workflow |
| gemini | Direct Gemini backend execution |
| product-requirements | Interactive PRD generation with quality scoring |
| prototype-prompt-generator | Structured UI/UX prototype prompt generation |
| skill-install | Install skills from GitHub with security scanning |
| test-cases | Comprehensive test case generation from requirements |
## Installation
@@ -87,17 +104,20 @@ Edit `config.json` to enable/disable modules:
| Codex | `codex e`, `--json`, `-C`, `resume` |
| Claude | `--output-format stream-json`, `-r` |
| Gemini | `-o stream-json`, `-y`, `-r` |
| OpenCode | `opencode`, stdin mode |
## Directory Structure After Installation
```
~/.claude/
├── bin/codeagent-wrapper
├── CLAUDE.md
├── commands/
├── agents/
├── skills/
── config.json
├── CLAUDE.md (installed by default)
├── commands/ (from essentials module)
├── agents/ (from bmad/requirements modules)
├── skills/ (from do/omo/sparv/course modules)
── hooks/ (from claudekit module)
├── settings.json (auto-generated, hooks config)
└── installed_modules.json (auto-generated, tracks modules)
```
## Documentation

View File

@@ -16,13 +16,30 @@ npx github:cexll/myclaude
| 模块 | 描述 | 文档 |
|------|------|------|
| [do](skills/do/README.md) | **推荐** - 7 阶段功能开发 + codeagent 编排 | `/do` 命令 |
| [do](skills/do/README.md) | **推荐** - 5 阶段功能开发 + codeagent 编排 | `/do` 命令 |
| [omo](skills/omo/README.md) | 多智能体编排 + 智能路由 | `/omo` 命令 |
| [bmad](agents/bmad/README.md) | BMAD 敏捷工作流 + 6 个专业智能体 | `/bmad-pilot` 命令 |
| [requirements](agents/requirements/README.md) | 轻量级需求到代码流水线 | `/requirements-pilot` 命令 |
| [essentials](agents/development-essentials/README.md) | 核心开发命令和工具 | `/code`, `/debug` 等 |
| [essentials](agents/development-essentials/README.md) | 11 个核心开发命令ask、bugfix、code、debug、docs、enhance-prompt、optimize、refactor、review、test、think | `/code`, `/debug` 等 |
| [sparv](skills/sparv/README.md) | SPARV 工作流 (Specify→Plan→Act→Review→Vault) | `/sparv` 命令 |
| course | 课程开发(组合 dev + product-requirements + test-cases | 组合模块 |
| claudekit | ClaudeKitdo 技能 + 全局钩子pre-bash、inject-spec、log-prompt| 组合模块 |
### 可用技能
可通过 `npx github:cexll/myclaude --list` 单独安装技能(模块内置技能如 do、omo、sparv 见上表):
| 技能 | 描述 |
|------|------|
| browser | 浏览器自动化测试和数据提取 |
| codeagent | codeagent-wrapper 多后端 AI 代码任务调用 |
| codex | Codex 后端直接执行 |
| dev | 轻量级端到端开发工作流 |
| gemini | Gemini 后端直接执行 |
| product-requirements | 交互式 PRD 生成(含质量评分)|
| prototype-prompt-generator | 结构化 UI/UX 原型提示词生成 |
| skill-install | 从 GitHub 安装技能(含安全扫描)|
| test-cases | 从需求生成全面测试用例 |
## 核心架构
@@ -35,22 +52,20 @@ npx github:cexll/myclaude
### do 工作流(推荐)
7 阶段功能开发,通过 codeagent-wrapper 编排多个智能体。**大多数功能开发任务的首选工作流。**
5 阶段功能开发,通过 codeagent-wrapper 编排多个智能体。**大多数功能开发任务的首选工作流。**
```bash
/do "添加用户登录功能"
```
**7 阶段:**
**5 阶段:**
| 阶段 | 名称 | 目标 |
|------|------|------|
| 1 | Discovery | 理解需求 |
| 2 | Exploration | 映射代码库模式 |
| 3 | Clarification | 解决歧义(**强制**|
| 4 | Architecture | 设计实现方案 |
| 5 | Implementation | 构建功能(**需审批**|
| 6 | Review | 捕获缺陷 |
| 7 | Summary | 记录结果 |
| 1 | Understand | 并行探索理解需求和映射代码库 |
| 2 | Clarify | 解决阻塞性歧义(条件触发)|
| 3 | Design | 产出最小变更实现方案 |
| 4 | Implement + Review | 构建功能并审查 |
| 5 | Complete | 记录构建结果 |
**智能体:**
- `code-explorer` - 代码追踪、架构映射
@@ -162,6 +177,10 @@ npx github:cexll/myclaude
| `/optimize` | 性能优化 |
| `/refactor` | 代码重构 |
| `/docs` | 编写文档 |
| `/ask` | 提问和咨询 |
| `/bugfix` | Bug 修复 |
| `/enhance-prompt` | 提示词优化 |
| `/think` | 深度思考分析 |
---
@@ -218,6 +237,7 @@ npx github:cexll/myclaude --install-dir ~/.claude --force
| Codex | `codex e`, `--json`, `-C`, `resume` |
| Claude | `--output-format stream-json`, `-r` |
| Gemini | `-o stream-json`, `-y`, `-r` |
| OpenCode | `opencode`, stdin 模式 |
## 故障排查

View File

@@ -8,7 +8,7 @@ const os = require("os");
const path = require("path");
const readline = require("readline");
const zlib = require("zlib");
const { spawn } = require("child_process");
const { spawn, spawnSync } = require("child_process");
const REPO = { owner: "cexll", name: "myclaude" };
const API_HEADERS = {
@@ -931,6 +931,63 @@ async function uninstallModule(moduleName, config, repoRoot, installDir, dryRun)
deleteModuleStatus(installDir, moduleName);
}
async function installDefaultConfigs(installDir, repoRoot) {
try {
const claudeMdTarget = path.join(installDir, "CLAUDE.md");
const claudeMdSrc = path.join(repoRoot, "memorys", "CLAUDE.md");
if (!fs.existsSync(claudeMdTarget) && fs.existsSync(claudeMdSrc)) {
await fs.promises.copyFile(claudeMdSrc, claudeMdTarget);
process.stdout.write(`Installed CLAUDE.md to ${claudeMdTarget}\n`);
}
} catch (err) {
process.stderr.write(`Warning: could not install default configs: ${err.message}\n`);
}
}
function printPostInstallInfo(installDir) {
process.stdout.write("\n");
// Check codeagent-wrapper version
const wrapperBin = path.join(installDir, "bin", "codeagent-wrapper");
let wrapperVersion = null;
try {
const r = spawnSync(wrapperBin, ["--version"], { timeout: 5000 });
if (r.status === 0 && r.stdout) {
wrapperVersion = r.stdout.toString().trim();
}
} catch {}
// Check PATH
const binDir = path.join(installDir, "bin");
const envPath = process.env.PATH || "";
const pathOk = envPath.split(path.delimiter).some((p) => {
try { return fs.realpathSync(p) === fs.realpathSync(binDir); } catch { return p === binDir; }
});
// Check backend CLIs
const whichCmd = process.platform === "win32" ? "where" : "which";
const backends = ["codex", "claude", "gemini", "opencode"];
const detected = {};
for (const name of backends) {
try {
const r = spawnSync(whichCmd, [name], { timeout: 3000 });
detected[name] = r.status === 0;
} catch {
detected[name] = false;
}
}
process.stdout.write("Setup Complete!\n");
process.stdout.write(` codeagent-wrapper: ${wrapperVersion || "(not found)"} ${wrapperVersion ? "✓" : "✗"}\n`);
process.stdout.write(` PATH: ${binDir} ${pathOk ? "✓" : "✗ (not in PATH)"}\n`);
process.stdout.write("\nBackend CLIs detected:\n");
process.stdout.write(" " + backends.map((b) => `${b} ${detected[b] ? "✓" : "✗"}`).join(" | ") + "\n");
process.stdout.write("\nNext steps:\n");
process.stdout.write(" 1. Configure API keys in ~/.codeagent/models.json\n");
process.stdout.write(' 2. Try: /do "your first task"\n');
process.stdout.write("\n");
}
async function installSelected(picks, tag, config, installDir, force, dryRun) {
const needRepo = picks.some((p) => p.kind !== "wrapper");
const needWrapper = picks.some((p) => p.kind === "wrapper");
@@ -985,6 +1042,9 @@ async function installSelected(picks, tag, config, installDir, force, dryRun) {
);
}
}
await installDefaultConfigs(installDir, repoRoot);
printPostInstallInfo(installDir);
} finally {
await rmTree(tmp);
}

View File

@@ -1,97 +1,158 @@
# codeagent-wrapper
`codeagent-wrapper` 是一个用 Go 编写的“多后端 AI 代码代理”命令行包装器:用统一的 CLI 入口封装不同的 AI 工具后端Codex / Claude / Gemini / Opencode并提供一致的参数、配置与会话恢复体验。
[English](README.md) | [中文](README_CN.md)
入口:`cmd/codeagent/main.go`(生成二进制名:`codeagent`)和 `cmd/codeagent-wrapper/main.go`(生成二进制名:`codeagent-wrapper`)。两者行为一致。
A multi-backend AI code agent CLI wrapper written in Go. Provides a unified CLI entry point wrapping different AI tool backends (Codex / Claude / Gemini / OpenCode) with consistent flags, configuration, skill injection, and session resumption.
## 功能特性
Entry point: `cmd/codeagent-wrapper/main.go` (binary: `codeagent-wrapper`).
- 多后端支持:`codex` / `claude` / `gemini` / `opencode`
- 统一命令行:`codeagent [flags] <task>` / `codeagent resume <session_id> <task> [workdir]`
- 自动 stdin遇到换行/特殊字符/超长任务自动走 stdin避免 shell quoting 地狱;也可显式使用 `-`
- 配置合并:支持配置文件与 `CODEAGENT_*` 环境变量viper
- Agent 预设:从 `~/.codeagent/models.json` 读取 backend/model/prompt 等预设
- 并行执行:`--parallel` 从 stdin 读取多任务配置,支持依赖拓扑并发执行
- 日志清理:`codeagent cleanup` 清理旧日志(日志写入系统临时目录)
## Features
## 安装
- **Multi-backend support**: `codex` / `claude` / `gemini` / `opencode`
- **Unified CLI**: `codeagent-wrapper [flags] <task>` / `codeagent-wrapper resume <session_id> <task> [workdir]`
- **Auto stdin**: Automatically pipes via stdin when task contains newlines, special characters, or exceeds length; also supports explicit `-`
- **Config merging**: Config files + `CODEAGENT_*` environment variables (viper)
- **Agent presets**: Read backend/model/prompt/reasoning/yolo/allowed_tools from `~/.codeagent/models.json`
- **Dynamic agents**: Place a `{name}.md` prompt file in `~/.codeagent/agents/` to use as an agent
- **Skill auto-injection**: `--skills` for manual specification, or auto-detect from project tech stack (Go/Rust/Python/Node.js/Vue)
- **Git worktree isolation**: `--worktree` executes tasks in an isolated git worktree with auto-generated task_id and branch
- **Parallel execution**: `--parallel` reads multi-task config from stdin with dependency-aware topological concurrent execution and structured summary reports
- **Backend config**: `backends` section in `models.json` supports per-backend `base_url` / `api_key` injection
- **Claude tool control**: `allowed_tools` / `disallowed_tools` to restrict available tools for Claude backend
- **Stderr noise filtering**: Automatically filters noisy stderr output from Gemini and Codex backends
- **Log cleanup**: `codeagent-wrapper cleanup` cleans old logs (logs written to system temp directory)
- **Cross-platform**: macOS / Linux / Windows
要求Go 1.21+。
## Installation
在仓库根目录执行:
### Recommended (interactive installer)
```bash
go install ./cmd/codeagent
go install ./cmd/codeagent-wrapper
npx github:cexll/myclaude
```
安装后确认:
Select the `codeagent-wrapper` module to install.
### Manual build
Requires: Go 1.21+.
```bash
codeagent version
codeagent-wrapper version
# Build from source
make build
# Or install to $GOPATH/bin
make install
```
## 使用示例
最简单用法(默认后端:`codex`
Verify installation:
```bash
codeagent "分析 internal/app/cli.go 的入口逻辑,给出改进建议"
codeagent-wrapper --version
```
指定后端:
## Usage
Basic usage (default backend: `codex`):
```bash
codeagent --backend claude "解释 internal/executor/parallel_config.go 的并行配置格式"
codeagent-wrapper "analyze the entry logic of internal/app/cli.go"
```
指定工作目录(第 2 个位置参数):
Specify backend:
```bash
codeagent "在当前 repo 下搜索潜在数据竞争" .
codeagent-wrapper --backend claude "explain the parallel config format in internal/executor/parallel_config.go"
```
显式从 stdin 读取 task使用 `-`
Specify working directory (2nd positional argument):
```bash
cat task.txt | codeagent -
codeagent-wrapper "search for potential data races in this repo" .
```
恢复会话:
Explicit stdin (using `-`):
```bash
codeagent resume <session_id> "继续上次任务"
cat task.txt | codeagent-wrapper -
```
并行模式(从 stdin 读取任务配置;禁止位置参数):
HEREDOC (recommended for multi-line tasks):
```bash
codeagent --parallel <<'EOF'
codeagent-wrapper --backend claude - <<'EOF'
Implement user authentication:
- JWT tokens
- bcrypt password hashing
- Session management
EOF
```
Resume session:
```bash
codeagent-wrapper resume <session_id> "continue the previous task"
```
Execute in isolated git worktree:
```bash
codeagent-wrapper --worktree "refactor the auth module"
```
Manual skill injection:
```bash
codeagent-wrapper --skills golang-base-practices "optimize database queries"
```
Parallel mode (task config from stdin):
```bash
codeagent-wrapper --parallel <<'EOF'
---TASK---
id: t1
workdir: .
backend: codex
---CONTENT---
列出本项目的主要模块以及它们的职责。
List the main modules and their responsibilities.
---TASK---
id: t2
dependencies: t1
backend: claude
---CONTENT---
基于 t1 的结论,提出重构风险点与建议。
Based on t1's findings, identify refactoring risks and suggestions.
EOF
```
## 配置说明
## CLI Flags
### 配置文件
| Flag | Description |
|------|-------------|
| `--backend <name>` | Backend selection (codex/claude/gemini/opencode) |
| `--model <name>` | Model override |
| `--agent <name>` | Agent preset name (from models.json or ~/.codeagent/agents/) |
| `--prompt-file <path>` | Read prompt from file |
| `--skills <names>` | Comma-separated skill names for spec injection |
| `--reasoning-effort <level>` | Reasoning effort (backend-specific) |
| `--skip-permissions` | Skip permission prompts |
| `--dangerously-skip-permissions` | Alias for `--skip-permissions` |
| `--worktree` | Execute in a new git worktree (auto-generates task_id) |
| `--parallel` | Parallel task mode (config from stdin) |
| `--full-output` | Full output in parallel mode (default: summary only) |
| `--config <path>` | Config file path (default: `$HOME/.codeagent/config.*`) |
| `--version`, `-v` | Print version |
| `--cleanup` | Clean up old logs |
默认查找路径(当 `--config` 为空时):
## Configuration
### Config File
Default search path (when `--config` is empty):
- `$HOME/.codeagent/config.(yaml|yml|json|toml|...)`
示例(YAML
Example (YAML):
```yaml
backend: codex
@@ -99,59 +160,113 @@ model: gpt-4.1
skip-permissions: false
```
也可以通过 `--config /path/to/config.yaml` 显式指定。
Can also be specified explicitly via `--config /path/to/config.yaml`.
### 环境变量(`CODEAGENT_*`
### Environment Variables (`CODEAGENT_*`)
通过 viper 读取并自动映射 `-` `_`,常用项:
Read via viper with automatic `-` to `_` mapping:
- `CODEAGENT_BACKEND``codex|claude|gemini|opencode`
- `CODEAGENT_MODEL`
- `CODEAGENT_AGENT`
- `CODEAGENT_PROMPT_FILE`
- `CODEAGENT_REASONING_EFFORT`
- `CODEAGENT_SKIP_PERMISSIONS`
- `CODEAGENT_FULL_OUTPUT`(并行模式 legacy 输出)
- `CODEAGENT_MAX_PARALLEL_WORKERS`0 表示不限制,上限 100
| Variable | Description |
|----------|-------------|
| `CODEAGENT_BACKEND` | Backend name (codex/claude/gemini/opencode) |
| `CODEAGENT_MODEL` | Model name |
| `CODEAGENT_AGENT` | Agent preset name |
| `CODEAGENT_PROMPT_FILE` | Prompt file path |
| `CODEAGENT_REASONING_EFFORT` | Reasoning effort |
| `CODEAGENT_SKIP_PERMISSIONS` | Skip permission prompts (default true; set `false` to disable) |
| `CODEAGENT_FULL_OUTPUT` | Full output in parallel mode |
| `CODEAGENT_MAX_PARALLEL_WORKERS` | Parallel worker count (0=unlimited, max 100) |
| `CODEAGENT_TMPDIR` | Custom temp directory (for macOS permission issues) |
| `CODEX_TIMEOUT` | Timeout in ms (default 7200000 = 2 hours) |
| `CODEX_BYPASS_SANDBOX` | Codex sandbox bypass (default true; set `false` to disable) |
| `DO_WORKTREE_DIR` | Reuse existing worktree directory (set by /do workflow) |
### Agent 预设(`~/.codeagent/models.json`
可在 `~/.codeagent/models.json` 定义 agent → backend/model/prompt 等映射,用 `--agent <name>` 选择:
### Agent Presets (`~/.codeagent/models.json`)
```json
{
"default_backend": "opencode",
"default_model": "opencode/grok-code",
"default_backend": "codex",
"default_model": "gpt-4.1",
"backends": {
"codex": { "api_key": "..." },
"claude": { "base_url": "http://localhost:23001", "api_key": "..." }
},
"agents": {
"develop": {
"backend": "codex",
"model": "gpt-4.1",
"prompt_file": "~/.codeagent/prompts/develop.md",
"description": "Code development"
"reasoning": "high",
"yolo": true,
"allowed_tools": ["Read", "Write", "Bash"],
"disallowed_tools": ["WebFetch"]
}
}
}
```
## 支持的后端
Use `--agent <name>` to select a preset. Agents inherit `base_url` / `api_key` from the corresponding `backends` entry.
该项目本身不内置模型能力,依赖你本机安装并可在 `PATH` 中找到对应 CLI
### Dynamic Agents
- `codex`:执行 `codex e ...`(默认会添加 `--dangerously-bypass-approvals-and-sandbox`;如需关闭请设置 `CODEX_BYPASS_SANDBOX=false`
- `claude`:执行 `claude -p ... --output-format stream-json`(默认会跳过权限提示;如需开启请设置 `CODEAGENT_SKIP_PERMISSIONS=false`
- `gemini`:执行 `gemini ... -o stream-json`(可从 `~/.gemini/.env` 加载环境变量)
- `opencode`:执行 `opencode run --format json`
Place a `{name}.md` file in `~/.codeagent/agents/` to use it via `--agent {name}`. The Markdown file is read as the prompt, using `default_backend` and `default_model`.
## 开发
### Skill Auto-Detection
```bash
make build
make test
make lint
make clean
When no skills are specified via `--skills`, codeagent-wrapper auto-detects the tech stack from files in the working directory:
| Detected Files | Injected Skills |
|----------------|-----------------|
| `go.mod` / `go.sum` | `golang-base-practices` |
| `Cargo.toml` | `rust-best-practices` |
| `pyproject.toml` / `setup.py` / `requirements.txt` | `python-best-practices` |
| `package.json` | `vercel-react-best-practices`, `frontend-design` |
| `vue.config.js` / `vite.config.ts` / `nuxt.config.ts` | `vue-web-app` |
Skill specs are read from `~/.claude/skills/{name}/SKILL.md`, subject to a 16000-character budget.
## Supported Backends
This project does not embed model capabilities. It requires the corresponding CLI tools installed and available in `PATH`:
| Backend | Command | Notes |
|---------|---------|-------|
| `codex` | `codex e ...` | Adds `--dangerously-bypass-approvals-and-sandbox` by default; set `CODEX_BYPASS_SANDBOX=false` to disable |
| `claude` | `claude -p ... --output-format stream-json` | Skips permissions and disables setting-sources to prevent recursion; set `CODEAGENT_SKIP_PERMISSIONS=false` to enable prompts; auto-reads env and model from `~/.claude/settings.json` |
| `gemini` | `gemini -o stream-json -y ...` | Auto-loads env vars from `~/.gemini/.env` (GEMINI_API_KEY, GEMINI_MODEL, etc.) |
| `opencode` | `opencode run --format json` | — |
## Project Structure
```
cmd/codeagent-wrapper/main.go # CLI entry point
internal/
app/ # CLI command definitions, argument parsing, main orchestration
backend/ # Backend abstraction and implementations (codex/claude/gemini/opencode)
config/ # Config loading, agent resolution, viper bindings
executor/ # Task execution engine: single/parallel/worktree/skill injection
logger/ # Structured logging system
parser/ # JSON stream parser
utils/ # Common utility functions
worktree/ # Git worktree management
```
## 故障排查
## Development
- macOS 下如果看到临时目录相关的 `permission denied`(例如临时可执行文件无法在 `/var/folders/.../T` 执行),可设置一个可执行的临时目录:`CODEAGENT_TMPDIR=$HOME/.codeagent/tmp`
- `claude` 后端的 `base_url/api_key`(来自 `~/.codeagent/models.json`)会注入到子进程环境变量:`ANTHROPIC_BASE_URL` / `ANTHROPIC_API_KEY`。若 `base_url` 指向本地代理(如 `localhost:23001`),请确认代理进程在运行。
```bash
make build # Build binary
make test # Run tests
make lint # golangci-lint + staticcheck
make clean # Clean build artifacts
make install # Install to $GOPATH/bin
```
CI uses GitHub Actions with Go 1.21 / 1.22 matrix testing.
## Troubleshooting
- On macOS, if you see `permission denied` related to temp directories, set: `CODEAGENT_TMPDIR=$HOME/.codeagent/tmp`
- `claude` backend's `base_url` / `api_key` (from `~/.codeagent/models.json` `backends.claude`) are injected as `ANTHROPIC_BASE_URL` / `ANTHROPIC_API_KEY` env vars
- `gemini` backend's API key is loaded from `~/.gemini/.env`, injected as `GEMINI_API_KEY` with `GEMINI_API_KEY_AUTH_MECHANISM=bearer` auto-set
- Exit codes: 127 = backend not found, 124 = timeout, 130 = interrupted
- Parallel mode outputs structured summary by default; use `--full-output` for complete output when debugging

View File

@@ -0,0 +1,272 @@
# codeagent-wrapper
[English](README.md) | [中文](README_CN.md)
`codeagent-wrapper` 是一个用 Go 编写的多后端 AI 代码代理命令行包装器:用统一的 CLI 入口封装不同的 AI 工具后端Codex / Claude / Gemini / OpenCode并提供一致的参数、配置、技能注入与会话恢复体验。
入口:`cmd/codeagent-wrapper/main.go`(生成二进制名:`codeagent-wrapper`)。
## 功能特性
- **多后端支持**`codex` / `claude` / `gemini` / `opencode`
- **统一命令行**`codeagent-wrapper [flags] <task>` / `codeagent-wrapper resume <session_id> <task> [workdir]`
- **自动 stdin**:遇到换行/特殊字符/超长任务自动走 stdin避免 shell quoting 问题;也可显式使用 `-`
- **配置合并**:支持配置文件与 `CODEAGENT_*` 环境变量viper
- **Agent 预设**:从 `~/.codeagent/models.json` 读取 backend/model/prompt/reasoning/yolo/allowed_tools 等预设
- **动态 Agent**:在 `~/.codeagent/agents/{name}.md` 放置 prompt 文件即可作为 agent 使用
- **技能自动注入**`--skills` 手动指定或根据项目技术栈自动检测Go/Rust/Python/Node.js/Vue并注入对应技能规范
- **Git Worktree 隔离**`--worktree` 在独立 git worktree 中执行任务,自动生成 task_id 和分支
- **并行执行**`--parallel` 从 stdin 读取多任务配置,支持依赖拓扑并发执行,带结构化摘要报告
- **后端配置**`models.json``backends` 节支持 per-backend 的 `base_url` / `api_key` 注入
- **Claude 工具控制**`allowed_tools` / `disallowed_tools` 限制 Claude 后端可用工具
- **Stderr 降噪**:自动过滤 Gemini 和 Codex 后端的噪声 stderr 输出
- **日志清理**`codeagent-wrapper cleanup` 清理旧日志(日志写入系统临时目录)
- **跨平台**:支持 macOS / Linux / Windows
## 安装
### 推荐方式(交互式安装器)
```bash
npx github:cexll/myclaude
```
选择 `codeagent-wrapper` 模块进行安装。
### 手动构建
要求Go 1.21+。
```bash
# 从源码构建
make build
# 或直接安装到 $GOPATH/bin
make install
```
安装后确认:
```bash
codeagent-wrapper --version
```
## 使用示例
最简单用法(默认后端:`codex`
```bash
codeagent-wrapper "分析 internal/app/cli.go 的入口逻辑,给出改进建议"
```
指定后端:
```bash
codeagent-wrapper --backend claude "解释 internal/executor/parallel_config.go 的并行配置格式"
```
指定工作目录(第 2 个位置参数):
```bash
codeagent-wrapper "在当前 repo 下搜索潜在数据竞争" .
```
显式从 stdin 读取 task使用 `-`
```bash
cat task.txt | codeagent-wrapper -
```
使用 HEREDOC推荐用于多行任务
```bash
codeagent-wrapper --backend claude - <<'EOF'
实现用户认证系统:
- JWT 令牌
- bcrypt 密码哈希
- 会话管理
EOF
```
恢复会话:
```bash
codeagent-wrapper resume <session_id> "继续上次任务"
```
在 git worktree 中隔离执行:
```bash
codeagent-wrapper --worktree "重构认证模块"
```
手动指定技能注入:
```bash
codeagent-wrapper --skills golang-base-practices "优化数据库查询"
```
并行模式(从 stdin 读取任务配置):
```bash
codeagent-wrapper --parallel <<'EOF'
---TASK---
id: t1
workdir: .
backend: codex
---CONTENT---
列出本项目的主要模块以及它们的职责。
---TASK---
id: t2
dependencies: t1
backend: claude
---CONTENT---
基于 t1 的结论,提出重构风险点与建议。
EOF
```
## CLI 参数
| 参数 | 说明 |
|------|------|
| `--backend <name>` | 后端选择codex/claude/gemini/opencode |
| `--model <name>` | 覆盖模型 |
| `--agent <name>` | Agent 预设名(来自 models.json 或 ~/.codeagent/agents/ |
| `--prompt-file <path>` | 从文件读取 prompt |
| `--skills <names>` | 逗号分隔的技能名,注入对应规范 |
| `--reasoning-effort <level>` | 推理力度(后端相关) |
| `--skip-permissions` | 跳过权限提示 |
| `--dangerously-skip-permissions` | `--skip-permissions` 的别名 |
| `--worktree` | 在新 git worktree 中执行(自动生成 task_id |
| `--parallel` | 并行任务模式(从 stdin 读取配置) |
| `--full-output` | 并行模式下输出完整消息(默认仅输出摘要) |
| `--config <path>` | 配置文件路径(默认:`$HOME/.codeagent/config.*` |
| `--version`, `-v` | 打印版本号 |
| `--cleanup` | 清理旧日志 |
## 配置说明
### 配置文件
默认查找路径(当 `--config` 为空时):
- `$HOME/.codeagent/config.(yaml|yml|json|toml|...)`
示例YAML
```yaml
backend: codex
model: gpt-4.1
skip-permissions: false
```
也可以通过 `--config /path/to/config.yaml` 显式指定。
### 环境变量(`CODEAGENT_*`
通过 viper 读取并自动映射 `-``_`,常用项:
| 变量 | 说明 |
|------|------|
| `CODEAGENT_BACKEND` | 后端名codex/claude/gemini/opencode |
| `CODEAGENT_MODEL` | 模型名 |
| `CODEAGENT_AGENT` | Agent 预设名 |
| `CODEAGENT_PROMPT_FILE` | Prompt 文件路径 |
| `CODEAGENT_REASONING_EFFORT` | 推理力度 |
| `CODEAGENT_SKIP_PERMISSIONS` | 跳过权限提示(默认 true`false` 关闭) |
| `CODEAGENT_FULL_OUTPUT` | 并行模式完整输出 |
| `CODEAGENT_MAX_PARALLEL_WORKERS` | 并行 worker 数0=不限制,上限 100 |
| `CODEAGENT_TMPDIR` | 自定义临时目录macOS 权限问题时使用) |
| `CODEX_TIMEOUT` | 超时(毫秒,默认 7200000 即 2 小时) |
| `CODEX_BYPASS_SANDBOX` | Codex sandbox bypass默认 true`false` 关闭) |
| `DO_WORKTREE_DIR` | 复用已有 worktree 目录(由 /do 工作流设置) |
### Agent 预设(`~/.codeagent/models.json`
```json
{
"default_backend": "codex",
"default_model": "gpt-4.1",
"backends": {
"codex": { "api_key": "..." },
"claude": { "base_url": "http://localhost:23001", "api_key": "..." }
},
"agents": {
"develop": {
"backend": "codex",
"model": "gpt-4.1",
"prompt_file": "~/.codeagent/prompts/develop.md",
"reasoning": "high",
"yolo": true,
"allowed_tools": ["Read", "Write", "Bash"],
"disallowed_tools": ["WebFetch"]
}
}
}
```
`--agent <name>` 选择预设agent 会继承 `backends` 下对应后端的 `base_url` / `api_key`
### 动态 Agent
`~/.codeagent/agents/` 目录放置 `{name}.md` 文件,即可通过 `--agent {name}` 使用,自动读取该 Markdown 作为 prompt使用 `default_backend``default_model`
### 技能自动检测
当未通过 `--skills` 显式指定技能时codeagent-wrapper 会根据工作目录中的文件自动检测技术栈:
| 检测文件 | 注入技能 |
|----------|----------|
| `go.mod` / `go.sum` | `golang-base-practices` |
| `Cargo.toml` | `rust-best-practices` |
| `pyproject.toml` / `setup.py` / `requirements.txt` | `python-best-practices` |
| `package.json` | `vercel-react-best-practices`, `frontend-design` |
| `vue.config.js` / `vite.config.ts` / `nuxt.config.ts` | `vue-web-app` |
技能规范从 `~/.claude/skills/{name}/SKILL.md` 读取,受 16000 字符预算限制。
## 支持的后端
该项目本身不内置模型能力,依赖本机安装并可在 `PATH` 中找到对应 CLI
| 后端 | 执行命令 | 说明 |
|------|----------|------|
| `codex` | `codex e ...` | 默认添加 `--dangerously-bypass-approvals-and-sandbox`;设 `CODEX_BYPASS_SANDBOX=false` 关闭 |
| `claude` | `claude -p ... --output-format stream-json` | 默认跳过权限并禁用 setting-sources 防止递归;设 `CODEAGENT_SKIP_PERMISSIONS=false` 开启权限;自动读取 `~/.claude/settings.json` 中的 env 和 model |
| `gemini` | `gemini -o stream-json -y ...` | 自动从 `~/.gemini/.env` 加载环境变量GEMINI_API_KEY, GEMINI_MODEL 等) |
| `opencode` | `opencode run --format json` | — |
## 项目结构
```
cmd/codeagent-wrapper/main.go # CLI 入口
internal/
app/ # CLI 命令定义、参数解析、主逻辑编排
backend/ # 后端抽象与实现codex/claude/gemini/opencode
config/ # 配置加载、agent 解析、viper 绑定
executor/ # 任务执行引擎:单任务/并行/worktree/技能注入
logger/ # 结构化日志系统
parser/ # JSON stream 解析器
utils/ # 通用工具函数
worktree/ # Git worktree 管理
```
## 开发
```bash
make build # 构建
make test # 运行测试
make lint # golangci-lint + staticcheck
make clean # 清理构建产物
make install # 安装到 $GOPATH/bin
```
CI 使用 GitHub ActionsGo 1.21 / 1.22 矩阵测试。
## 故障排查
- macOS 下如果看到临时目录相关的 `permission denied`,可设置:`CODEAGENT_TMPDIR=$HOME/.codeagent/tmp`
- `claude` 后端的 `base_url` / `api_key`(来自 `~/.codeagent/models.json``backends.claude`)会注入到子进程环境变量 `ANTHROPIC_BASE_URL` / `ANTHROPIC_API_KEY`
- `gemini` 后端的 API key 从 `~/.gemini/.env` 加载,注入 `GEMINI_API_KEY` 并自动设置 `GEMINI_API_KEY_AUTH_MECHANISM=bearer`
- 后端命令未找到时返回退出码 127超时返回 124中断返回 130
- 并行模式默认输出结构化摘要,使用 `--full-output` 查看完整输出以便调试

View File

@@ -1,11 +1,11 @@
# Codeagent-Wrapper User Guide
Multi-backend AI code execution wrapper supporting Codex, Claude, and Gemini.
Multi-backend AI code execution wrapper supporting Codex, Claude, Gemini, and OpenCode.
## Overview
`codeagent-wrapper` is a Go-based CLI tool that provides a unified interface to multiple AI coding backends. It handles:
- Multi-backend execution (Codex, Claude, Gemini)
- Multi-backend execution (Codex, Claude, Gemini, OpenCode)
- JSON stream parsing and output formatting
- Session management and resumption
- Parallel task execution with dependency resolution
@@ -42,6 +42,24 @@ Implement user authentication:
EOF
```
### CLI Flags
| Flag | Description |
|------|-------------|
| `--backend <name>` | Select backend (codex/claude/gemini/opencode) |
| `--model <name>` | Override model for this invocation |
| `--agent <name>` | Agent preset name (from ~/.codeagent/models.json) |
| `--config <path>` | Path to models.json config file |
| `--cleanup` | Clean up log files on startup |
| `--worktree` | Execute in a new git worktree (auto-generates task ID) |
| `--skills <names>` | Comma-separated skill names for spec injection |
| `--prompt-file <path>` | Read prompt from file |
| `--reasoning-effort <level>` | Set reasoning effort (low/medium/high) |
| `--skip-permissions` | Skip permission prompts |
| `--parallel` | Enable parallel task execution |
| `--full-output` | Show full output in parallel mode |
| `--version`, `-v` | Print version and exit |
### Backend Selection
| Backend | Command | Best For |
@@ -49,6 +67,7 @@ EOF
| **Codex** | `--backend codex` | General code tasks (default) |
| **Claude** | `--backend claude` | Complex reasoning, architecture |
| **Gemini** | `--backend gemini` | Fast iteration, prototyping |
| **OpenCode** | `--backend opencode` | Open-source alternative |
## Core Features

View File

@@ -29,6 +29,7 @@ type cliOptions struct {
ReasoningEffort string
Agent string
PromptFile string
Skills string
SkipPermissions bool
Worktree bool
@@ -134,6 +135,7 @@ func addRootFlags(fs *pflag.FlagSet, opts *cliOptions) {
fs.StringVar(&opts.ReasoningEffort, "reasoning-effort", "", "Reasoning effort (backend-specific)")
fs.StringVar(&opts.Agent, "agent", "", "Agent preset name (from ~/.codeagent/models.json)")
fs.StringVar(&opts.PromptFile, "prompt-file", "", "Prompt file path")
fs.StringVar(&opts.Skills, "skills", "", "Comma-separated skill names for spec injection")
fs.BoolVar(&opts.SkipPermissions, "skip-permissions", false, "Skip permissions prompts (also via CODEAGENT_SKIP_PERMISSIONS)")
fs.BoolVar(&opts.SkipPermissions, "dangerously-skip-permissions", false, "Alias for --skip-permissions")
@@ -339,6 +341,16 @@ func buildSingleConfig(cmd *cobra.Command, args []string, rawArgv []string, opts
return nil, fmt.Errorf("task required")
}
var skills []string
if cmd.Flags().Changed("skills") {
for _, s := range strings.Split(opts.Skills, ",") {
s = strings.TrimSpace(s)
if s != "" {
skills = append(skills, s)
}
}
}
cfg := &Config{
WorkDir: defaultWorkdir,
Backend: backendName,
@@ -352,6 +364,7 @@ func buildSingleConfig(cmd *cobra.Command, args []string, rawArgv []string, opts
MaxParallelWorkers: config.ResolveMaxParallelWorkers(),
AllowedTools: resolvedAllowedTools,
DisallowedTools: resolvedDisallowedTools,
Skills: skills,
Worktree: opts.Worktree,
}
@@ -418,7 +431,7 @@ func runParallelMode(cmd *cobra.Command, args []string, opts *cliOptions, v *vip
return 1
}
if cmd.Flags().Changed("agent") || cmd.Flags().Changed("prompt-file") || cmd.Flags().Changed("reasoning-effort") {
if cmd.Flags().Changed("agent") || cmd.Flags().Changed("prompt-file") || cmd.Flags().Changed("reasoning-effort") || cmd.Flags().Changed("skills") {
fmt.Fprintln(os.Stderr, "ERROR: --parallel reads its task configuration from stdin; only --backend, --model, --full-output and --skip-permissions are allowed.")
return 1
}
@@ -585,6 +598,17 @@ func runSingleMode(cfg *Config, name string) int {
taskText = wrapTaskWithAgentPrompt(prompt, taskText)
}
// Resolve skills: explicit > auto-detect from workdir
skills := cfg.Skills
if len(skills) == 0 {
skills = detectProjectSkills(cfg.WorkDir)
}
if len(skills) > 0 {
if content := resolveSkillContent(skills, 0); content != "" {
taskText = taskText + "\n\n# Domain Best Practices\n\n" + content
}
}
useStdin := cfg.ExplicitStdin || shouldUseStdin(taskText, piped)
targetArg := taskText

View File

@@ -52,3 +52,11 @@ func runCodexProcess(parentCtx context.Context, codexArgs []string, taskText str
func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backend Backend, customArgs []string, useCustomArgs bool, silent bool, timeoutSec int) TaskResult {
return executor.RunCodexTaskWithContext(parentCtx, taskSpec, backend, codexCommand, buildCodexArgsFn, customArgs, useCustomArgs, silent, timeoutSec)
}
func detectProjectSkills(workDir string) []string {
return executor.DetectProjectSkills(workDir)
}
func resolveSkillContent(skills []string, maxBudget int) string {
return executor.ResolveSkillContent(skills, maxBudget)
}

View File

@@ -26,6 +26,7 @@ type Config struct {
MaxParallelWorkers int
AllowedTools []string
DisallowedTools []string
Skills []string
Worktree bool // Execute in a new git worktree
}

View File

@@ -337,6 +337,16 @@ func DefaultRunCodexTaskFn(task TaskSpec, timeout int) TaskResult {
}
task.Task = WrapTaskWithAgentPrompt(prompt, task.Task)
}
// Resolve skills: explicit > auto-detect from workdir
skills := task.Skills
if len(skills) == 0 {
skills = DetectProjectSkills(task.WorkDir)
}
if len(skills) > 0 {
if content := ResolveSkillContent(skills, 0); content != "" {
task.Task = task.Task + "\n\n# Domain Best Practices\n\n" + content
}
}
if task.UseStdin || ShouldUseStdin(task.Task, false) {
task.UseStdin = true
}
@@ -941,8 +951,13 @@ func RunCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
cfg.WorkDir = defaultWorkdir
}
// Handle worktree mode: create a new git worktree and update cfg.WorkDir
if taskSpec.Worktree {
// Handle worktree mode: check DO_WORKTREE_DIR env var first, then create if needed
if worktreeDir := os.Getenv("DO_WORKTREE_DIR"); worktreeDir != "" {
// Use existing worktree from /do setup
cfg.WorkDir = worktreeDir
logInfo(fmt.Sprintf("Using existing worktree from DO_WORKTREE_DIR: %s", worktreeDir))
} else if taskSpec.Worktree {
// Create new worktree (backward compatibility for standalone --worktree usage)
paths, err := createWorktreeFn(cfg.WorkDir)
if err != nil {
result.ExitCode = 1

View File

@@ -88,6 +88,13 @@ func ParseParallelConfig(data []byte) (*ParallelConfig, error) {
task.Dependencies = append(task.Dependencies, dep)
}
}
case "skills":
for _, s := range strings.Split(value, ",") {
s = strings.TrimSpace(s)
if s != "" {
task.Skills = append(task.Skills, s)
}
}
}
}
@@ -99,17 +106,17 @@ func ParseParallelConfig(data []byte) (*ParallelConfig, error) {
if strings.TrimSpace(task.Agent) == "" {
return nil, fmt.Errorf("task block #%d has empty agent field", taskIndex)
}
if err := config.ValidateAgentName(task.Agent); err != nil {
return nil, fmt.Errorf("task block #%d invalid agent name: %w", taskIndex, err)
}
backend, model, promptFile, reasoning, _, _, _, allowedTools, disallowedTools, err := config.ResolveAgentConfig(task.Agent)
if err != nil {
return nil, fmt.Errorf("task block #%d failed to resolve agent %q: %w", taskIndex, task.Agent, err)
}
if task.Backend == "" {
task.Backend = backend
}
if task.Model == "" {
if err := config.ValidateAgentName(task.Agent); err != nil {
return nil, fmt.Errorf("task block #%d invalid agent name: %w", taskIndex, err)
}
backend, model, promptFile, reasoning, _, _, _, allowedTools, disallowedTools, err := config.ResolveAgentConfig(task.Agent)
if err != nil {
return nil, fmt.Errorf("task block #%d failed to resolve agent %q: %w", taskIndex, task.Agent, err)
}
if task.Backend == "" {
task.Backend = backend
}
if task.Model == "" {
task.Model = model
}
if task.ReasoningEffort == "" {

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
@@ -128,3 +129,116 @@ func ReadAgentPromptFile(path string, allowOutsideClaudeDir bool) (string, error
func WrapTaskWithAgentPrompt(prompt string, task string) string {
return "<agent-prompt>\n" + prompt + "\n</agent-prompt>\n\n" + task
}
// techSkillMap maps file-existence fingerprints to skill names.
var techSkillMap = []struct {
Files []string // any of these files → this tech
Skills []string
}{
{Files: []string{"go.mod", "go.sum"}, Skills: []string{"golang-base-practices"}},
{Files: []string{"Cargo.toml"}, Skills: []string{"rust-best-practices"}},
{Files: []string{"pyproject.toml", "setup.py", "requirements.txt", "Pipfile"}, Skills: []string{"python-best-practices"}},
{Files: []string{"package.json"}, Skills: []string{"vercel-react-best-practices", "frontend-design"}},
{Files: []string{"vue.config.js", "vite.config.ts", "nuxt.config.ts"}, Skills: []string{"vue-web-app"}},
}
// DetectProjectSkills scans workDir for tech-stack fingerprints and returns
// skill names that are both detected and installed at ~/.claude/skills/{name}/SKILL.md.
func DetectProjectSkills(workDir string) []string {
home, err := os.UserHomeDir()
if err != nil {
return nil
}
var detected []string
seen := make(map[string]bool)
for _, entry := range techSkillMap {
for _, f := range entry.Files {
if _, err := os.Stat(filepath.Join(workDir, f)); err == nil {
for _, skill := range entry.Skills {
if seen[skill] {
continue
}
skillPath := filepath.Join(home, ".claude", "skills", skill, "SKILL.md")
if _, err := os.Stat(skillPath); err == nil {
detected = append(detected, skill)
seen[skill] = true
}
}
break // one matching file is enough for this entry
}
}
}
return detected
}
const defaultSkillBudget = 16000 // chars, ~4K tokens
// validSkillName ensures skill names contain only safe characters to prevent path traversal
var validSkillName = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
// ResolveSkillContent reads SKILL.md files for the given skill names,
// strips YAML frontmatter, wraps each in <skill> tags, and enforces a
// character budget to prevent context bloat.
func ResolveSkillContent(skills []string, maxBudget int) string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
if maxBudget <= 0 {
maxBudget = defaultSkillBudget
}
var sections []string
remaining := maxBudget
for _, name := range skills {
name = strings.TrimSpace(name)
if name == "" {
continue
}
if !validSkillName.MatchString(name) {
logWarn(fmt.Sprintf("skill %q: invalid name (must contain only [a-zA-Z0-9_-]), skipping", name))
continue
}
path := filepath.Join(home, ".claude", "skills", name, "SKILL.md")
data, err := os.ReadFile(path)
if err != nil || len(data) == 0 {
logWarn(fmt.Sprintf("skill %q: SKILL.md not found or empty, skipping", name))
continue
}
body := stripYAMLFrontmatter(strings.TrimSpace(string(data)))
tagOverhead := len("<skill name=\"\">") + len(name) + len("\n") + len("\n</skill>")
bodyBudget := remaining - tagOverhead
if bodyBudget <= 0 {
logWarn(fmt.Sprintf("skill %q: skipped, insufficient budget for tags", name))
break
}
if len(body) > bodyBudget {
logWarn(fmt.Sprintf("skill %q: truncated from %d to %d chars (budget)", name, len(body), bodyBudget))
body = body[:bodyBudget]
}
remaining -= len(body) + tagOverhead
sections = append(sections, "<skill name=\""+name+"\">\n"+body+"\n</skill>")
if remaining <= 0 {
break
}
}
if len(sections) == 0 {
return ""
}
return strings.Join(sections, "\n\n")
}
func stripYAMLFrontmatter(s string) string {
s = strings.ReplaceAll(s, "\r\n", "\n")
if !strings.HasPrefix(s, "---") {
return s
}
idx := strings.Index(s[3:], "\n---")
if idx < 0 {
return s
}
result := s[3+idx+4:]
if len(result) > 0 && result[0] == '\n' {
result = result[1:]
}
return strings.TrimSpace(result)
}

View File

@@ -0,0 +1,343 @@
package executor
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
// setTestHome overrides the home directory for both Unix (HOME) and Windows (USERPROFILE).
func setTestHome(t *testing.T, home string) {
t.Helper()
t.Setenv("HOME", home)
if runtime.GOOS == "windows" {
t.Setenv("USERPROFILE", home)
}
}
// --- helper: create a temp skill dir with SKILL.md ---
func createTempSkill(t *testing.T, name, content string) string {
t.Helper()
home := t.TempDir()
skillDir := filepath.Join(home, ".claude", "skills", name)
if err := os.MkdirAll(skillDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(content), 0644); err != nil {
t.Fatal(err)
}
return home
}
// --- ParseParallelConfig skills parsing tests ---
func TestParseParallelConfig_SkillsField(t *testing.T) {
tests := []struct {
name string
input string
taskIdx int
expectedSkills []string
}{
{
name: "single skill",
input: `---TASK---
id: t1
workdir: .
skills: golang-base-practices
---CONTENT---
Do something.
`,
taskIdx: 0,
expectedSkills: []string{"golang-base-practices"},
},
{
name: "multiple comma-separated skills",
input: `---TASK---
id: t1
workdir: .
skills: golang-base-practices, vercel-react-best-practices
---CONTENT---
Do something.
`,
taskIdx: 0,
expectedSkills: []string{"golang-base-practices", "vercel-react-best-practices"},
},
{
name: "no skills field",
input: `---TASK---
id: t1
workdir: .
---CONTENT---
Do something.
`,
taskIdx: 0,
expectedSkills: nil,
},
{
name: "empty skills value",
input: `---TASK---
id: t1
workdir: .
skills:
---CONTENT---
Do something.
`,
taskIdx: 0,
expectedSkills: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := ParseParallelConfig([]byte(tt.input))
if err != nil {
t.Fatalf("ParseParallelConfig error: %v", err)
}
got := cfg.Tasks[tt.taskIdx].Skills
if len(got) != len(tt.expectedSkills) {
t.Fatalf("skills: got %v, want %v", got, tt.expectedSkills)
}
for i := range got {
if got[i] != tt.expectedSkills[i] {
t.Errorf("skills[%d]: got %q, want %q", i, got[i], tt.expectedSkills[i])
}
}
})
}
}
// --- stripYAMLFrontmatter tests ---
func TestStripYAMLFrontmatter(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "with frontmatter",
input: "---\nname: test\ndescription: foo\n---\n\n# Body\nContent here.",
expected: "# Body\nContent here.",
},
{
name: "no frontmatter",
input: "# Just a body\nNo frontmatter.",
expected: "# Just a body\nNo frontmatter.",
},
{
name: "empty",
input: "",
expected: "",
},
{
name: "only frontmatter",
input: "---\nname: test\n---",
expected: "",
},
{
name: "frontmatter with allowed-tools",
input: "---\nname: do\nallowed-tools: [\"Bash\"]\n---\n\n# Skill content",
expected: "# Skill content",
},
{
name: "CRLF line endings",
input: "---\r\nname: test\r\n---\r\n\r\n# Body\r\nContent.",
expected: "# Body\nContent.",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := stripYAMLFrontmatter(tt.input)
if got != tt.expected {
t.Errorf("got %q, want %q", got, tt.expected)
}
})
}
}
// --- DetectProjectSkills tests ---
func TestDetectProjectSkills_GoProject(t *testing.T) {
tmpDir := t.TempDir()
os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module test"), 0644)
skills := DetectProjectSkills(tmpDir)
// Result depends on whether golang-base-practices is installed locally
t.Logf("detected skills for Go project: %v", skills)
}
func TestDetectProjectSkills_NoFingerprints(t *testing.T) {
tmpDir := t.TempDir()
skills := DetectProjectSkills(tmpDir)
if len(skills) != 0 {
t.Errorf("expected no skills for empty dir, got %v", skills)
}
}
func TestDetectProjectSkills_FullStack(t *testing.T) {
tmpDir := t.TempDir()
os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module test"), 0644)
os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte(`{"name":"test"}`), 0644)
skills := DetectProjectSkills(tmpDir)
t.Logf("detected skills for fullstack project: %v", skills)
seen := make(map[string]bool)
for _, s := range skills {
if seen[s] {
t.Errorf("duplicate skill detected: %s", s)
}
seen[s] = true
}
}
func TestDetectProjectSkills_NonexistentDir(t *testing.T) {
skills := DetectProjectSkills("/nonexistent/path/xyz")
if len(skills) != 0 {
t.Errorf("expected no skills for nonexistent dir, got %v", skills)
}
}
// --- ResolveSkillContent tests (CI-friendly with temp dirs) ---
func TestResolveSkillContent_ValidSkill(t *testing.T) {
home := createTempSkill(t, "test-skill", "---\nname: test\n---\n\n# Test Skill\nBest practices here.")
setTestHome(t, home)
result := ResolveSkillContent([]string{"test-skill"}, 0)
if result == "" {
t.Fatal("expected non-empty content")
}
if !strings.Contains(result, `<skill name="test-skill">`) {
t.Error("missing opening <skill> tag")
}
if !strings.Contains(result, "</skill>") {
t.Error("missing closing </skill> tag")
}
if !strings.Contains(result, "# Test Skill") {
t.Error("missing skill body content")
}
if strings.Contains(result, "name: test") {
t.Error("frontmatter was not stripped")
}
}
func TestResolveSkillContent_NonexistentSkill(t *testing.T) {
home := t.TempDir()
setTestHome(t, home)
result := ResolveSkillContent([]string{"nonexistent-skill-xyz"}, 0)
if result != "" {
t.Errorf("expected empty for nonexistent skill, got %d bytes", len(result))
}
}
func TestResolveSkillContent_Empty(t *testing.T) {
if result := ResolveSkillContent(nil, 0); result != "" {
t.Errorf("expected empty for nil, got %q", result)
}
if result := ResolveSkillContent([]string{}, 0); result != "" {
t.Errorf("expected empty for empty, got %q", result)
}
}
func TestResolveSkillContent_Budget(t *testing.T) {
longBody := strings.Repeat("x", 500)
home := createTempSkill(t, "big-skill", "---\nname: big\n---\n\n"+longBody)
setTestHome(t, home)
result := ResolveSkillContent([]string{"big-skill"}, 200)
if result == "" {
t.Fatal("expected non-empty even with small budget")
}
if len(result) > 200 {
t.Errorf("result %d bytes exceeds budget 200", len(result))
}
t.Logf("budget=200, result=%d bytes", len(result))
}
func TestResolveSkillContent_MultipleSkills(t *testing.T) {
home := t.TempDir()
for _, name := range []string{"skill-a", "skill-b"} {
skillDir := filepath.Join(home, ".claude", "skills", name)
os.MkdirAll(skillDir, 0755)
os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte("# "+name+"\nContent."), 0644)
}
setTestHome(t, home)
result := ResolveSkillContent([]string{"skill-a", "skill-b"}, 0)
if result == "" {
t.Fatal("expected non-empty for multiple skills")
}
if !strings.Contains(result, `<skill name="skill-a">`) {
t.Error("missing skill-a tag")
}
if !strings.Contains(result, `<skill name="skill-b">`) {
t.Error("missing skill-b tag")
}
}
func TestResolveSkillContent_PathTraversal(t *testing.T) {
home := t.TempDir()
setTestHome(t, home)
result := ResolveSkillContent([]string{"../../../etc/passwd"}, 0)
if result != "" {
t.Errorf("expected empty for path traversal name, got %d bytes", len(result))
}
}
func TestResolveSkillContent_InvalidNames(t *testing.T) {
home := t.TempDir()
setTestHome(t, home)
tests := []string{"../bad", "foo/bar", "skill name", "skill.name", "a b"}
for _, name := range tests {
result := ResolveSkillContent([]string{name}, 0)
if result != "" {
t.Errorf("expected empty for invalid name %q, got %d bytes", name, len(result))
}
}
}
func TestResolveSkillContent_ValidNamePattern(t *testing.T) {
if !validSkillName.MatchString("golang-base-practices") {
t.Error("golang-base-practices should be valid")
}
if !validSkillName.MatchString("my_skill_v2") {
t.Error("my_skill_v2 should be valid")
}
if validSkillName.MatchString("../bad") {
t.Error("../bad should be invalid")
}
if validSkillName.MatchString("") {
t.Error("empty should be invalid")
}
}
// --- Integration: skill injection format test ---
func TestSkillInjectionFormat(t *testing.T) {
home := createTempSkill(t, "test-go", "---\nname: go\n---\n\n# Go Best Practices\nUse gofmt.")
setTestHome(t, home)
taskText := "Implement the feature."
content := ResolveSkillContent([]string{"test-go"}, 0)
injected := taskText + "\n\n# Domain Best Practices\n\n" + content
if !strings.Contains(injected, "Implement the feature.") {
t.Error("original task text lost")
}
if !strings.Contains(injected, "# Domain Best Practices") {
t.Error("missing section header")
}
if !strings.Contains(injected, `<skill name="test-go">`) {
t.Error("missing <skill> tag")
}
if !strings.Contains(injected, "Use gofmt.") {
t.Error("missing skill body")
}
}

View File

@@ -24,6 +24,7 @@ type TaskSpec struct {
Worktree bool `json:"worktree,omitempty"`
AllowedTools []string `json:"allowed_tools,omitempty"`
DisallowedTools []string `json:"disallowed_tools,omitempty"`
Skills []string `json:"skills,omitempty"`
Mode string `json:"-"`
UseStdin bool `json:"-"`
Context context.Context `json:"-"`

View File

@@ -19,7 +19,7 @@ func TestTruncate(t *testing.T) {
{"zero maxLen", "hello", 0, "..."},
{"negative maxLen", "hello", -1, ""},
{"maxLen 1", "hello", 1, "h..."},
{"unicode bytes truncate", "你好世界", 10, "你好世\xe7..."}, // Truncate works on bytes, not runes
{"unicode bytes truncate", "你好世界", 10, "你好世\xe7..."}, // Truncate works on bytes, not runes
{"mixed truncate", "hello世界abc", 7, "hello\xe4\xb8..."}, // byte-based truncation
}

View File

@@ -39,6 +39,36 @@
"omo": {
"enabled": false,
"description": "OmO multi-agent orchestration with Sisyphus coordinator",
"agents": {
"oracle": {
"backend": "claude",
"model": "claude-opus-4-5-20251101",
"yolo": true
},
"librarian": {
"backend": "claude",
"model": "claude-sonnet-4-5-20250929",
"yolo": true
},
"explore": {
"backend": "opencode",
"model": "opencode/grok-code"
},
"develop": {
"backend": "codex",
"model": "gpt-5.2",
"reasoning": "xhigh",
"yolo": true
},
"frontend-ui-ux-engineer": {
"backend": "gemini",
"model": "gemini-3-pro-preview"
},
"document-writer": {
"backend": "gemini",
"model": "gemini-3-flash-preview"
}
},
"operations": [
{
"type": "copy_file",
@@ -98,7 +128,27 @@
},
"do": {
"enabled": true,
"description": "7-phase feature development workflow with codeagent orchestration",
"description": "5-phase feature development workflow with codeagent orchestration",
"agents": {
"develop": {
"backend": "codex",
"model": "gpt-4.1",
"reasoning": "high",
"yolo": true
},
"code-explorer": {
"backend": "opencode",
"model": ""
},
"code-architect": {
"backend": "claude",
"model": ""
},
"code-reviewer": {
"backend": "claude",
"model": ""
}
},
"operations": [
{
"type": "copy_dir",
@@ -145,6 +195,24 @@
}
}
]
},
"claudekit": {
"enabled": false,
"description": "ClaudeKit workflow: skills/do + global hooks (pre-bash, inject-spec, log-prompt)",
"operations": [
{
"type": "copy_dir",
"source": "skills/do",
"target": "skills/do",
"description": "Install do skill with 5-phase workflow"
},
{
"type": "copy_dir",
"source": "hooks",
"target": "hooks",
"description": "Install global hooks (pre-bash, inject-spec, log-prompt)"
}
]
}
}
}

30
hooks/hooks.json Normal file
View File

@@ -0,0 +1,30 @@
{
"description": "ClaudeKit global hooks: dangerous command blocker, spec injection, prompt logging, session review",
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/pre-bash.py \"$CLAUDE_TOOL_INPUT\""
},
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/inject-spec.py"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/log-prompt.py"
}
]
}
]
}
}

13
hooks/inject-spec.py Normal file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env python3
"""
Global Spec Injection Hook (DEPRECATED).
Spec injection is now handled internally by codeagent-wrapper via the
per-task `skills:` field in parallel config and the `--skills` CLI flag.
This hook is kept as a no-op for backward compatibility.
"""
import sys
sys.exit(0)

55
hooks/log-prompt.py Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""
Log Prompt Hook - Record user prompts to session-specific log files.
Used for review on Stop.
Uses session-isolated logs to handle concurrency.
"""
import json
import os
import sys
from datetime import datetime
from pathlib import Path
def get_session_id() -> str:
"""Get unique session identifier."""
return os.environ.get("CLAUDE_CODE_SSE_PORT", "default")
def write_log(prompt: str) -> None:
"""Write prompt to session log file."""
log_dir = Path(".claude/state")
session_id = get_session_id()
log_file = log_dir / f"session-{session_id}.log"
log_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().isoformat()
entry = f"[{timestamp}] {prompt[:500]}\n"
with open(log_file, "a", encoding="utf-8") as f:
f.write(entry)
def main():
input_data = ""
if not sys.stdin.isatty():
try:
input_data = sys.stdin.read()
except Exception:
pass
prompt = ""
try:
data = json.loads(input_data)
prompt = data.get("prompt", "")
except json.JSONDecodeError:
prompt = input_data.strip()
if prompt:
write_log(prompt)
if __name__ == "__main__":
main()

30
hooks/pre-bash.py Normal file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""
Pre-Bash Hook - Block dangerous commands before execution.
"""
import sys
DANGEROUS_PATTERNS = [
'rm -rf /',
'rm -rf ~',
'dd if=',
':(){:|:&};:',
'mkfs.',
'> /dev/sd',
]
def main():
command = sys.argv[1] if len(sys.argv) > 1 else ''
for pattern in DANGEROUS_PATTERNS:
if pattern in command:
print(f"[CWF] BLOCKED: Dangerous command detected: {pattern}", file=sys.stderr)
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -126,35 +126,44 @@ def save_settings(ctx: Dict[str, Any], settings: Dict[str, Any]) -> None:
_save_json(settings_path, settings)
def find_module_hooks(module_name: str, cfg: Dict[str, Any], ctx: Dict[str, Any]) -> Optional[tuple]:
"""Find hooks.json for a module if it exists.
def find_module_hooks(module_name: str, cfg: Dict[str, Any], ctx: Dict[str, Any]) -> List[tuple]:
"""Find all hooks.json files for a module.
Returns tuple of (hooks_config, plugin_root_path) or None.
Returns list of tuples (hooks_config, plugin_root_path).
Searches in order for each copy_dir operation:
1. {target_dir}/hooks/hooks.json (for skills with hooks subdirectory)
2. {target_dir}/hooks.json (for hooks directory itself)
"""
results = []
seen_paths = set()
# Check for hooks in operations (copy_dir targets)
for op in cfg.get("operations", []):
if op.get("type") == "copy_dir":
target_dir = ctx["install_dir"] / op["target"]
hooks_file = target_dir / "hooks" / "hooks.json"
if hooks_file.exists():
try:
return (_load_json(hooks_file), str(target_dir))
except (ValueError, FileNotFoundError):
pass
# Also check source directory during install
for op in cfg.get("operations", []):
if op.get("type") == "copy_dir":
target_dir = ctx["install_dir"] / op["target"]
source_dir = ctx["config_dir"] / op["source"]
hooks_file = source_dir / "hooks" / "hooks.json"
if hooks_file.exists():
try:
return (_load_json(hooks_file), str(target_dir))
except (ValueError, FileNotFoundError):
pass
return None
# Check both target and source directories
for base_dir, plugin_root in [(target_dir, str(target_dir)), (source_dir, str(target_dir))]:
# First check {dir}/hooks/hooks.json (for skills)
hooks_file = base_dir / "hooks" / "hooks.json"
if hooks_file.exists() and str(hooks_file) not in seen_paths:
try:
results.append((_load_json(hooks_file), plugin_root))
seen_paths.add(str(hooks_file))
except (ValueError, FileNotFoundError):
pass
# Then check {dir}/hooks.json (for hooks directory itself)
hooks_file = base_dir / "hooks.json"
if hooks_file.exists() and str(hooks_file) not in seen_paths:
try:
results.append((_load_json(hooks_file), plugin_root))
seen_paths.add(str(hooks_file))
except (ValueError, FileNotFoundError):
pass
return results
def _create_hook_marker(module_name: str) -> str:
@@ -235,6 +244,112 @@ def unmerge_hooks_from_settings(module_name: str, ctx: Dict[str, Any]) -> None:
write_log({"level": "INFO", "message": f"Removed hooks for module: {module_name}"}, ctx)
def merge_agents_to_models(module_name: str, agents: Dict[str, Any], ctx: Dict[str, Any]) -> None:
"""Merge module agent configs into ~/.codeagent/models.json."""
models_path = Path.home() / ".codeagent" / "models.json"
models_path.parent.mkdir(parents=True, exist_ok=True)
if models_path.exists():
with models_path.open("r", encoding="utf-8") as fh:
models = json.load(fh)
else:
template = ctx["config_dir"] / "templates" / "models.json.example"
if template.exists():
with template.open("r", encoding="utf-8") as fh:
models = json.load(fh)
# Clear template agents so modules populate with __module__ tags
models["agents"] = {}
else:
models = {
"default_backend": "codex",
"default_model": "gpt-4.1",
"backends": {},
"agents": {},
}
models.setdefault("agents", {})
for agent_name, agent_cfg in agents.items():
entry = dict(agent_cfg)
entry["__module__"] = module_name
existing = models["agents"].get(agent_name, {})
if not existing or existing.get("__module__"):
models["agents"][agent_name] = entry
with models_path.open("w", encoding="utf-8") as fh:
json.dump(models, fh, indent=2, ensure_ascii=False)
write_log(
{
"level": "INFO",
"message": (
f"Merged {len(agents)} agent(s) from {module_name} "
"into models.json"
),
},
ctx,
)
def unmerge_agents_from_models(module_name: str, ctx: Dict[str, Any]) -> None:
"""Remove module's agent configs from ~/.codeagent/models.json.
If another installed module also declares a removed agent, restore that
module's version so shared agents (e.g. 'develop') are not lost.
"""
models_path = Path.home() / ".codeagent" / "models.json"
if not models_path.exists():
return
with models_path.open("r", encoding="utf-8") as fh:
models = json.load(fh)
agents = models.get("agents", {})
to_remove = [
name
for name, cfg in agents.items()
if isinstance(cfg, dict) and cfg.get("__module__") == module_name
]
if not to_remove:
return
# Load config to find other modules that declare the same agents
config_path = ctx["config_dir"] / "config.json"
config = _load_json(config_path) if config_path.exists() else {}
installed = load_installed_status(ctx).get("modules", {})
for name in to_remove:
del agents[name]
# Check if another installed module also declares this agent
for other_mod, other_status in installed.items():
if other_mod == module_name:
continue
if other_status.get("status") != "success":
continue
other_cfg = config.get("modules", {}).get(other_mod, {})
other_agents = other_cfg.get("agents", {})
if name in other_agents:
restored = dict(other_agents[name])
restored["__module__"] = other_mod
agents[name] = restored
break
with models_path.open("w", encoding="utf-8") as fh:
json.dump(models, fh, indent=2, ensure_ascii=False)
write_log(
{
"level": "INFO",
"message": (
f"Removed {len(to_remove)} agent(s) from {module_name} "
"in models.json"
),
},
ctx,
)
def _hooks_equal(hook1: Dict[str, Any], hook2: Dict[str, Any]) -> bool:
"""Compare two hooks ignoring the __module__ marker."""
h1 = {k: v for k, v in hook1.items() if k != "__module__"}
@@ -536,6 +651,14 @@ def uninstall_module(name: str, cfg: Dict[str, Any], ctx: Dict[str, Any]) -> Dic
target.unlink()
removed_paths.append(str(target))
write_log({"level": "INFO", "message": f"Removed: {target}"}, ctx)
# Clean up empty parent directories up to install_dir
parent = target.parent
while parent != install_dir and parent.exists():
try:
parent.rmdir()
except OSError:
break
parent = parent.parent
elif op_type == "merge_dir":
if not merge_dir_files:
write_log(
@@ -595,6 +718,13 @@ def uninstall_module(name: str, cfg: Dict[str, Any], ctx: Dict[str, Any]) -> Dic
except Exception as exc:
write_log({"level": "WARNING", "message": f"Failed to remove hooks for {name}: {exc}"}, ctx)
# Remove module agents from ~/.codeagent/models.json
try:
unmerge_agents_from_models(name, ctx)
result["agents_removed"] = True
except Exception as exc:
write_log({"level": "WARNING", "message": f"Failed to remove agents for {name}: {exc}"}, ctx)
result["removed_paths"] = removed_paths
return result
@@ -617,7 +747,9 @@ def update_status_after_uninstall(uninstalled_modules: List[str], ctx: Dict[str,
def interactive_manage(config: Dict[str, Any], ctx: Dict[str, Any]) -> int:
"""Interactive module management menu."""
"""Interactive module management menu. Returns 0 on success, 1 on error.
Sets ctx['_did_install'] = True if any module was installed."""
ctx.setdefault("_did_install", False)
while True:
installed_status = get_installed_modules(config, ctx)
modules = config.get("modules", {})
@@ -686,6 +818,7 @@ def interactive_manage(config: Dict[str, Any], ctx: Dict[str, Any]) -> int:
for r in results:
if r.get("status") == "success":
current_status.setdefault("modules", {})[r["module"]] = r
ctx["_did_install"] = True
current_status["updated_at"] = datetime.now().isoformat()
with Path(ctx["status_file"]).open("w", encoding="utf-8") as fh:
json.dump(current_status, fh, indent=2, ensure_ascii=False)
@@ -799,16 +932,27 @@ def execute_module(name: str, cfg: Dict[str, Any], ctx: Dict[str, Any]) -> Dict[
raise
# Handle hooks: find and merge module hooks into settings.json
hooks_result = find_module_hooks(name, cfg, ctx)
if hooks_result:
hooks_config, plugin_root = hooks_result
hooks_results = find_module_hooks(name, cfg, ctx)
if hooks_results:
for hooks_config, plugin_root in hooks_results:
try:
merge_hooks_to_settings(name, hooks_config, ctx, plugin_root)
result["operations"].append({"type": "merge_hooks", "status": "success"})
result["has_hooks"] = True
except Exception as exc:
write_log({"level": "WARNING", "message": f"Failed to merge hooks for {name}: {exc}"}, ctx)
result["operations"].append({"type": "merge_hooks", "status": "failed", "error": str(exc)})
# Handle agents: merge module agent configs into ~/.codeagent/models.json
module_agents = cfg.get("agents", {})
if module_agents:
try:
merge_hooks_to_settings(name, hooks_config, ctx, plugin_root)
result["operations"].append({"type": "merge_hooks", "status": "success"})
result["has_hooks"] = True
merge_agents_to_models(name, module_agents, ctx)
result["operations"].append({"type": "merge_agents", "status": "success"})
result["has_agents"] = True
except Exception as exc:
write_log({"level": "WARNING", "message": f"Failed to merge hooks for {name}: {exc}"}, ctx)
result["operations"].append({"type": "merge_hooks", "status": "failed", "error": str(exc)})
write_log({"level": "WARNING", "message": f"Failed to merge agents for {name}: {exc}"}, ctx)
result["operations"].append({"type": "merge_agents", "status": "failed", "error": str(exc)})
return result
@@ -1051,6 +1195,67 @@ def write_status(results: List[Dict[str, Any]], ctx: Dict[str, Any]) -> None:
json.dump(status, fh, indent=2, ensure_ascii=False)
def install_default_configs(ctx: Dict[str, Any]) -> None:
"""Copy default config files if they don't already exist. Best-effort: never raises."""
try:
install_dir = ctx["install_dir"]
config_dir = ctx["config_dir"]
# Copy memorys/CLAUDE.md -> {install_dir}/CLAUDE.md
claude_md_src = config_dir / "memorys" / "CLAUDE.md"
claude_md_dst = install_dir / "CLAUDE.md"
if not claude_md_dst.exists() and claude_md_src.exists():
shutil.copy2(claude_md_src, claude_md_dst)
print(f" Installed CLAUDE.md to {claude_md_dst}")
write_log({"level": "INFO", "message": f"Installed CLAUDE.md to {claude_md_dst}"}, ctx)
except Exception as exc:
print(f" Warning: could not install default configs: {exc}", file=sys.stderr)
def print_post_install_info(ctx: Dict[str, Any]) -> None:
"""Print post-install verification and setup guidance."""
install_dir = ctx["install_dir"]
# Check codeagent-wrapper version
wrapper_bin = install_dir / "bin" / "codeagent-wrapper"
wrapper_version = None
try:
result = subprocess.run(
[str(wrapper_bin), "--version"],
capture_output=True, text=True, timeout=5,
)
if result.returncode == 0:
wrapper_version = result.stdout.strip()
except Exception:
pass
# Check PATH
bin_dir = str(install_dir / "bin")
env_path = os.environ.get("PATH", "")
path_ok = any(
os.path.realpath(p) == os.path.realpath(bin_dir)
if os.path.exists(p) else p == bin_dir
for p in env_path.split(os.pathsep)
)
# Check backend CLIs
backends = ["codex", "claude", "gemini", "opencode"]
detected = {name: shutil.which(name) is not None for name in backends}
print("\nSetup Complete!")
v_mark = "" if wrapper_version else ""
print(f" codeagent-wrapper: {wrapper_version or '(not found)'} {v_mark}")
p_mark = "" if path_ok else "✗ (not in PATH)"
print(f" PATH: {bin_dir} {p_mark}")
print("\nBackend CLIs detected:")
cli_parts = [f"{b} {'' if detected[b] else ''}" for b in backends]
print(" " + " | ".join(cli_parts))
print("\nNext steps:")
print(" 1. Configure API keys in ~/.codeagent/models.json")
print(' 2. Try: /do "your first task"')
print()
def prepare_status_backup(ctx: Dict[str, Any]) -> None:
status_path = Path(ctx["status_file"])
if status_path.exists():
@@ -1199,6 +1404,8 @@ def main(argv: Optional[Iterable[str]] = None) -> int:
failed = len(results) - success
if failed == 0:
print(f"\n✓ Update complete: {success} module(s) updated")
install_default_configs(ctx)
print_post_install_info(ctx)
else:
print(f"\n⚠ Update finished with errors: {success} success, {failed} failed")
if not args.force:
@@ -1212,7 +1419,11 @@ def main(argv: Optional[Iterable[str]] = None) -> int:
except Exception as exc:
print(f"Failed to prepare install dir: {exc}", file=sys.stderr)
return 1
return interactive_manage(config, ctx)
result = interactive_manage(config, ctx)
if result == 0 and ctx.get("_did_install"):
install_default_configs(ctx)
print_post_install_info(ctx)
return result
# Install specified modules
modules = select_modules(config, args.module)
@@ -1271,6 +1482,10 @@ def main(argv: Optional[Iterable[str]] = None) -> int:
if not args.force:
return 1
if failed == 0:
install_default_configs(ctx)
print_post_install_info(ctx)
return 0

View File

@@ -1,12 +1,12 @@
Adopt First Principles Thinking as the mandatory core reasoning method. Never rely on analogy, convention, "best practices", or "what others do". Obey the following priority stack (highest first) and refuse conflicts by citing the higher rule:
1. Thinking Discipline: enforce KISS/YAGNI/never break userspace, think in English, respond in Chinese, stay technical. Reject analogical shortcuts—always trace back to fundamental truths.
2. Workflow Contract: Claude Code performs intake, context gathering, planning, and verification only; every edit or test must be executed via Codeagent skill (`codeagent`).
1. Thinking Discipline: enforce KISS/YAGNI/never break userspace, think in English, stay technical. Reject analogical shortcuts—always trace back to fundamental truths.
2. Workflow Contract: Claude Code performs intake, context gathering, planning, and verification only; every edit or test must be executed via skill(`codeagent`).
3. Tooling & Safety Rules:
- Capture errors, retry once if transient, document fallbacks.
4. Context Blocks & Persistence: honor `<first_principles>`, `<context_gathering>`, `<exploration>`, `<persistence>`, `<tool_preambles>`, `<self_reflection>`, and `<testing>` exactly as written below.
5. Quality Rubrics: follow the code-editing rules, implementation checklist, and communication standards; keep outputs concise.
6. Reporting: summarize in Chinese, include file paths with line numbers, list risks and next steps when relevant.
6. Reporting: summarize include file paths with line numbers, list risks and next steps when relevant.
<first_principles>
For every non-trivial problem, execute this mandatory reasoning chain:
@@ -33,8 +33,8 @@ Trigger conditions:
- User explicitly requests deep analysis
Process:
- Requirements: Break the ask into explicit requirements, unclear areas, and hidden assumptions. Apply <first_principles> step 1 here.
- Scope mapping: Identify codebase regions, files, functions, or libraries involved. Perform targeted parallel searches before planning. For complex call chains, delegate to Codeagent skill.
- Dependencies: Identify frameworks, APIs, configs, data formats. For complex internals, delegate to Codeagent skill.
- Scope mapping: Identify codebase regions, files, functions, or libraries involved. Perform targeted parallel searches before planning. For complex call chains, delegate to skill(`codeagent`).
- Dependencies: Identify frameworks, APIs, configs, data formats. For complex internals, delegate to skill(`codeagent`).
- Ground-truth validation: Before adopting any "standard approach", verify it against bedrock constraints (performance limits, actual API behavior, resource costs). Apply <first_principles> steps 2-3.
- Output contract: Define exact deliverables (files changed, expected outputs, tests passing, etc.).
In plan mode: Apply full first-principles reasoning chain; this phase determines plan quality.
@@ -85,6 +85,5 @@ Code Editing Rules:
- Enforce accessibility, consistent spacing (multiples of 4), ≤2 accent colors.
- Use semantic HTML and accessible components.
Communication:
- Think in English, respond in Chinese, stay terse.
- Lead with findings before summaries; critique code, not people.
- Provide next steps only when they naturally follow from the work.

View File

@@ -1,6 +1,6 @@
{
"name": "myclaude",
"version": "0.0.0",
"version": "6.7.0",
"private": true,
"description": "Claude Code multi-agent workflows (npx installer)",
"license": "AGPL-3.0",
@@ -13,6 +13,7 @@
"agents/",
"skills/",
"memorys/",
"templates/",
"codeagent-wrapper/",
"config.json",
"install.py",

View File

@@ -52,7 +52,7 @@ To customize agents, create same-named files in `~/.codeagent/agents/` to overri
3. **Phase 4 requires approval** - stop after Phase 3 if not approved
4. **Pass complete context forward** - every agent gets the Context Pack
5. **Parallel-first** - run independent tasks via `codeagent-wrapper --parallel`
6. **Update state after each phase** - keep `.claude/do.{task_id}.local.md` current
6. **Update state after each phase** - keep `.claude/do-tasks/{task_id}/task.json` current
## Context Pack Template
@@ -78,16 +78,34 @@ To customize agents, create same-named files in `~/.codeagent/agents/` to overri
## Loop State Management
When triggered via `/do <task>`, initializes `.claude/do.{task_id}.local.md` with:
- `active: true`
- `current_phase: 1`
- `max_phases: 5`
- `completion_promise: "<promise>DO_COMPLETE</promise>"`
After each phase, update frontmatter:
When triggered via `/do <task>`, initializes `.claude/do-tasks/{task_id}/task.md` with YAML frontmatter:
```yaml
current_phase: <next phase number>
phase_name: "<next phase name>"
---
id: "<task_id>"
title: "<task description>"
status: "in_progress"
current_phase: 1
phase_name: "Understand"
max_phases: 5
use_worktree: false
created_at: "<ISO timestamp>"
completion_promise: "<promise>DO_COMPLETE</promise>"
---
# Requirements
<task description>
## Context
## Progress
```
The current task is tracked in `.claude/do-tasks/.current-task`.
After each phase, update `task.md` frontmatter via:
```bash
python3 ".claude/skills/do/scripts/task.py" update-phase <N>
```
When all 5 phases complete, output:
@@ -95,17 +113,17 @@ When all 5 phases complete, output:
<promise>DO_COMPLETE</promise>
```
To abort early, set `active: false` in the state file.
To abort early, manually edit `task.md` and set `status: "cancelled"` in the frontmatter.
## Stop Hook
A Stop hook is registered after installation:
1. Creates `.claude/do.{task_id}.local.md` state file
2. Updates `current_phase` after each phase
1. Creates `.claude/do-tasks/{task_id}/task.md` state file
2. Updates `current_phase` in frontmatter after each phase
3. Stop hook checks state, blocks exit if incomplete
4. Outputs `<promise>DO_COMPLETE</promise>` when finished
Manual exit: Set `active` to `false` in the state file.
Manual exit: Edit `task.md` and set `status: "cancelled"` in the frontmatter.
## Parallel Execution Examples

View File

@@ -1,7 +1,7 @@
---
name: do
description: This skill should be used for structured feature development with codebase understanding. Triggers on /do command. Provides a 5-phase workflow (Understand, Clarify, Design, Implement, Complete) using codeagent-wrapper to orchestrate code-explorer, code-architect, code-reviewer, and develop agents in parallel.
allowed-tools: ["Bash(${SKILL_DIR}/scripts/setup-do.py:*)"]
allowed-tools: ["Bash(.claude/skills/do/scripts/setup-do.py:*)", "Bash(.claude/skills/do/scripts/task.py:*)"]
---
# do - Feature Development Orchestrator
@@ -10,82 +10,60 @@ An orchestrator for systematic feature development. Invoke agents via `codeagent
## Loop Initialization (REQUIRED)
When triggered via `/do <task>`, follow these steps:
### Step 1: Ask about worktree mode
Use AskUserQuestion to ask:
```
Develop in a separate worktree? (Isolates changes from main branch)
- Yes (Recommended for larger changes)
- No (Work directly in current directory)
```
### Step 2: Initialize state
When triggered via `/do <task>`, initialize the task directory immediately without asking about worktree:
```bash
# If worktree mode selected:
python3 "${SKILL_DIR}/scripts/setup-do.py" --worktree "<task description>"
# If no worktree:
python3 "${SKILL_DIR}/scripts/setup-do.py" "<task description>"
python3 ".claude/skills/do/scripts/setup-do.py" "<task description>"
```
This creates `.claude/do.{task_id}.local.md` with:
- `active: true`
- `current_phase: 1`
- `max_phases: 5`
- `completion_promise: "<promise>DO_COMPLETE</promise>"`
- `use_worktree: true/false`
This creates a task directory under `.claude/do-tasks/` with:
- `task.md`: Single file containing YAML frontmatter (metadata) + Markdown body (requirements/context)
**Worktree decision is deferred until Phase 4 (Implement).** Phases 1-3 are read-only and do not require worktree isolation.
## Task Directory Management
Use `task.py` to manage task state:
```bash
# Update phase
python3 ".claude/skills/do/scripts/task.py" update-phase 2
# Check status
python3 ".claude/skills/do/scripts/task.py" status
# List all tasks
python3 ".claude/skills/do/scripts/task.py" list
```
## Worktree Mode
When `use_worktree: true` in state file, ALL `codeagent-wrapper` calls that modify code MUST include `--worktree`:
The worktree is created **only when needed** (right before Phase 4: Implement). If the user chooses worktree mode:
1. Run setup with `--worktree` flag to create the worktree:
```bash
python3 ".claude/skills/do/scripts/setup-do.py" --worktree "<task description>"
```
2. Use the `DO_WORKTREE_DIR` environment variable to direct `codeagent-wrapper` develop agent into the worktree. **Do NOT pass `--worktree` to subsequent calls** — that creates a new worktree each time.
```bash
# With worktree mode enabled
codeagent-wrapper --worktree --agent develop - . <<'EOF'
...
EOF
# Parallel tasks with worktree
codeagent-wrapper --worktree --parallel <<'EOF'
---TASK---
id: task1
agent: develop
workdir: .
---CONTENT---
# Save the worktree path from setup output, then prefix all develop calls:
DO_WORKTREE_DIR=<worktree_dir> codeagent-wrapper --agent develop - . <<'EOF'
...
EOF
```
The `--worktree` flag tells codeagent-wrapper to create/use a worktree internally. Read-only agents (code-explorer, code-architect, code-reviewer) do NOT need `--worktree`.
## Loop State Management
After each phase, update `.claude/do.{task_id}.local.md` frontmatter:
```yaml
current_phase: <next phase number>
phase_name: "<next phase name>"
```
When all 5 phases complete, output the completion signal:
```
<promise>DO_COMPLETE</promise>
```
To abort early, set `active: false` in the state file.
Read-only agents (code-explorer, code-architect, code-reviewer) do NOT need `DO_WORKTREE_DIR`.
## Hard Constraints
1. **Never write code directly.** Delegate all code changes to `codeagent-wrapper` agents.
2. **Pass complete context forward.** Every agent invocation includes the Context Pack.
3. **Parallel-first.** Run independent tasks via `codeagent-wrapper --parallel`.
4. **Update state after each phase.** Keep `.claude/do.{task_id}.local.md` current.
5. **Expect long-running `codeagent-wrapper` calls.** High-reasoning modes can take a long time; stay in the orchestrator role and wait for agents to complete.
6. **Timeouts are not an escape hatch.** If a `codeagent-wrapper` invocation times out/errors, retry (split/narrow the task if needed); never switch to direct implementation.
7. **Respect worktree setting.** If `use_worktree: true`, always pass `--worktree` to develop agent calls.
2. **Parallel-first.** Run independent tasks via `codeagent-wrapper --parallel`.
3. **Update phase after each phase.** Use `task.py update-phase <N>`.
4. **Expect long-running `codeagent-wrapper` calls.** High-reasoning modes can take a long time.
5. **Timeouts are not an escape hatch.** If a call times out, retry with narrower scope.
6. **Defer worktree decision until Phase 4.** Only ask about worktree mode right before implementation. If enabled, prefix develop agent calls with `DO_WORKTREE_DIR=<path>`. Never pass `--worktree` after initialization.
## Agents
@@ -94,7 +72,7 @@ To abort early, set `active: false` in the state file.
| `code-explorer` | Trace code, map architecture, find patterns | No (read-only) |
| `code-architect` | Design approaches, file plans, build sequences | No (read-only) |
| `code-reviewer` | Review for bugs, simplicity, conventions | No (read-only) |
| `develop` | Implement code, run tests | **Yes** (if worktree enabled) |
| `develop` | Implement code, run tests | **Yes** — use `DO_WORKTREE_DIR` env prefix |
## Issue Severity Definitions
@@ -110,28 +88,6 @@ To abort early, set `active: false` in the state file.
- Missing documentation
- Non-critical test coverage gaps
## Context Pack Template
```text
## Original User Request
<verbatim request>
## Context Pack
- Phase: <1-5 name>
- Decisions: <requirements/constraints/choices>
- Code-explorer output: <paste or "None">
- Code-architect output: <paste or "None">
- Code-reviewer output: <paste or "None">
- Develop output: <paste or "None">
- Open questions: <list or "None">
## Current Task
<specific task>
## Acceptance Criteria
<checkable outputs>
```
## 5-Phase Workflow
### Phase 1: Understand (Parallel, No Interaction)
@@ -147,70 +103,37 @@ id: p1_requirements
agent: code-architect
workdir: .
---CONTENT---
## Original User Request
/do <request>
## Context Pack
- Code-explorer output: None
- Code-architect output: None
## Current Task
1. Analyze requirements completeness (score 1-10)
2. Extract explicit requirements, constraints, acceptance criteria
3. Identify blocking questions (issues that prevent implementation)
4. Identify minor clarifications (nice-to-have but can proceed without)
Analyze requirements completeness (score 1-10):
1. Extract explicit requirements, constraints, acceptance criteria
2. Identify blocking questions (issues that prevent implementation)
3. Identify minor clarifications (nice-to-have but can proceed without)
Output format:
- Completeness score: X/10
- Requirements: [list]
- Non-goals: [list]
- Blocking questions: [list, if any]
- Minor clarifications: [list, if any]
## Acceptance Criteria
Concrete checklist; blocking vs minor questions clearly separated.
---TASK---
id: p1_similar_features
agent: code-explorer
workdir: .
---CONTENT---
## Original User Request
/do <request>
## Current Task
Find 1-3 similar features, trace end-to-end. Return: key files with line numbers, call flow, extension points.
## Acceptance Criteria
Concrete file:line map + reuse points.
---TASK---
id: p1_architecture
agent: code-explorer
workdir: .
---CONTENT---
## Original User Request
/do <request>
## Current Task
Map architecture for relevant subsystem. Return: module map + 5-10 key files.
## Acceptance Criteria
Clear boundaries; file:line references.
---TASK---
id: p1_conventions
agent: code-explorer
workdir: .
---CONTENT---
## Original User Request
/do <request>
## Current Task
Identify testing patterns, conventions, config. Return: test commands + file locations.
## Acceptance Criteria
Test commands + relevant test file paths.
EOF
```
@@ -221,75 +144,108 @@ EOF
**Actions:**
1. Review `p1_requirements` output for blocking questions
2. **IF blocking questions exist** → Use AskUserQuestion
3. **IF no blocking questions (completeness >= 8)** → Skip to Phase 3, log "Requirements clear, proceeding"
```bash
# Only if blocking questions exist:
# Use AskUserQuestion with the blocking questions from Phase 1
```
3. **IF no blocking questions (completeness >= 8)** → Skip to Phase 3
### Phase 3: Design (No Interaction)
**Goal:** Produce minimal-change implementation plan.
**Actions:** Invoke `code-architect` with all Phase 1 context to generate a single implementation plan.
```bash
codeagent-wrapper --agent code-architect - . <<'EOF'
## Original User Request
/do <request>
## Context Pack
- Code-explorer output: <ALL Phase 1 explorer outputs>
- Code-architect output: <Phase 1 requirements + Phase 2 answers if any>
## Current Task
Design minimal-change implementation:
- Reuse existing abstractions
- Minimize new files
- Follow established patterns from code-explorer output
- Follow established patterns from Phase 1 exploration
Output:
- File touch list with specific changes
- Build sequence
- Test plan
- Risks and mitigations
## Acceptance Criteria
Concrete, implementable blueprint with minimal moving parts.
EOF
```
### Phase 4: Implement + Review (Single Interaction Point)
### Phase 4: Implement + Review
**Goal:** Build feature and review in one phase.
**Actions:**
**Step 1: Decide on worktree mode (ONLY NOW)**
1. Invoke `develop` to implement (add `--worktree` if `use_worktree: true`):
Use AskUserQuestion to ask:
```
Develop in a separate worktree? (Isolates changes from main branch)
- Yes (Recommended for larger changes)
- No (Work directly in current directory)
```
If user chooses worktree:
```bash
python3 ".claude/skills/do/scripts/setup-do.py" --worktree "<task description>"
# Save the worktree path from output for DO_WORKTREE_DIR
```
**Step 2: Invoke develop agent**
For full-stack projects, split into backend/frontend tasks with per-task `skills:` injection. Use `--parallel` when tasks can be split; use single agent when the change is small or single-domain.
**Single-domain example** (prefix with `DO_WORKTREE_DIR` if worktree enabled):
```bash
# Check use_worktree from state file, add --worktree if true
codeagent-wrapper --worktree --agent develop - . <<'EOF'
## Original User Request
/do <request>
## Context Pack
- Code-explorer output: <ALL Phase 1 outputs>
- Code-architect output: <Phase 3 blueprint>
## Current Task
Implement with minimal change set following the blueprint.
# With worktree:
DO_WORKTREE_DIR=<worktree_dir> codeagent-wrapper --agent develop --skills golang-base-practices - . <<'EOF'
Implement with minimal change set following the Phase 3 blueprint.
- Follow Phase 1 patterns
- Add/adjust tests per Phase 3 plan
- Run narrowest relevant tests
EOF
## Acceptance Criteria
Feature works end-to-end; tests pass; diff is minimal.
# Without worktree:
codeagent-wrapper --agent develop --skills golang-base-practices - . <<'EOF'
Implement with minimal change set following the Phase 3 blueprint.
- Follow Phase 1 patterns
- Add/adjust tests per Phase 3 plan
- Run narrowest relevant tests
EOF
```
2. Run parallel reviews (no --worktree needed, read-only):
**Full-stack parallel example** (adapt task IDs, skills, and content based on Phase 3 design):
```bash
# With worktree:
DO_WORKTREE_DIR=<worktree_dir> codeagent-wrapper --parallel <<'EOF'
---TASK---
id: p4_backend
agent: develop
workdir: .
skills: golang-base-practices
---CONTENT---
Implement backend changes following Phase 3 blueprint.
- Follow Phase 1 patterns
- Add/adjust tests per Phase 3 plan
---TASK---
id: p4_frontend
agent: develop
workdir: .
skills: frontend-design,vercel-react-best-practices
dependencies: p4_backend
---CONTENT---
Implement frontend changes following Phase 3 blueprint.
- Follow Phase 1 patterns
- Add/adjust tests per Phase 3 plan
EOF
# Without worktree: remove DO_WORKTREE_DIR prefix
```
Note: Choose which skills to inject based on Phase 3 design output. Only inject skills relevant to each task's domain.
**Step 3: Review**
**Step 3: Review**
Run parallel reviews:
```bash
codeagent-wrapper --parallel <<'EOF'
@@ -298,71 +254,36 @@ id: p4_correctness
agent: code-reviewer
workdir: .
---CONTENT---
## Original User Request
/do <request>
## Context Pack
- Code-architect output: <Phase 3 blueprint>
- Develop output: <implementation output>
## Current Task
Review for correctness, edge cases, failure modes.
Classify each issue as BLOCKING or MINOR.
## Acceptance Criteria
Issues with file:line references, severity, and concrete fixes.
---TASK---
id: p4_simplicity
agent: code-reviewer
workdir: .
---CONTENT---
## Original User Request
/do <request>
## Context Pack
- Code-architect output: <Phase 3 blueprint>
- Develop output: <implementation output>
## Current Task
Review for KISS: remove bloat, collapse needless abstractions.
Classify each issue as BLOCKING or MINOR.
## Acceptance Criteria
Actionable simplifications with severity and justification.
EOF
```
3. Handle review results:
- **MINOR issues only** → Auto-fix via `develop` (with `--worktree` if enabled), no user interaction
- **BLOCKING issues** → Use AskUserQuestion: "Fix now / Proceed as-is"
**Step 4: Handle review results**
- **MINOR issues only** → Auto-fix via `develop`, no user interaction
- **BLOCKING issues** → Use AskUserQuestion: "Fix now / Proceed as-is"
### Phase 5: Complete (No Interaction)
**Goal:** Document what was built.
**Actions:** Invoke `code-reviewer` to produce summary:
```bash
codeagent-wrapper --agent code-reviewer - . <<'EOF'
## Original User Request
/do <request>
## Context Pack
- Code-architect output: <Phase 3 blueprint>
- Code-reviewer output: <Phase 4 review outcomes>
- Develop output: <Phase 4 implementation + fixes>
## Current Task
Write completion summary:
- What was built
- Key decisions/tradeoffs
- Files modified (paths)
- How to verify (commands)
- Follow-ups (optional)
## Acceptance Criteria
Short, technical, actionable summary.
EOF
```

View File

@@ -1,5 +1,5 @@
{
"description": "do loop hook for 5-phase workflow",
"description": "do loop hooks for 5-phase workflow",
"hooks": {
"Stop": [
{
@@ -10,6 +10,17 @@
}
]
}
],
"SubagentStop": [
{
"matcher": "code-reviewer",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/verify-loop.py"
}
]
}
]
}
}

View File

@@ -1,10 +1,20 @@
#!/usr/bin/env python3
"""
Stop hook for do skill workflow.
Checks if the do loop is complete before allowing exit.
Uses the new task directory structure under .claude/do-tasks/.
"""
import glob
import json
import os
import re
import sys
DIR_TASKS = ".claude/do-tasks"
FILE_CURRENT_TASK = ".current-task"
FILE_TASK_JSON = "task.json"
PHASE_NAMES = {
1: "Understand",
2: "Clarify",
@@ -13,98 +23,69 @@ PHASE_NAMES = {
5: "Complete",
}
def phase_name_for(n: int) -> str:
return PHASE_NAMES.get(n, f"Phase {n}")
def frontmatter_get(file_path: str, key: str) -> str:
def get_current_task(project_dir: str) -> str | None:
"""Read current task directory path."""
current_task_file = os.path.join(project_dir, DIR_TASKS, FILE_CURRENT_TASK)
if not os.path.exists(current_task_file):
return None
try:
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
with open(current_task_file, "r", encoding="utf-8") as f:
content = f.read().strip()
return content if content else None
except Exception:
return ""
return None
if not lines or lines[0].strip() != "---":
return ""
for i, line in enumerate(lines[1:], start=1):
if line.strip() == "---":
break
match = re.match(rf"^{re.escape(key)}:\s*(.*)$", line)
if match:
value = match.group(1).strip()
if value.startswith('"') and value.endswith('"'):
value = value[1:-1]
return value
return ""
def get_body(file_path: str) -> str:
def get_task_info(project_dir: str, task_dir: str) -> dict | None:
"""Read task.json data."""
task_json_path = os.path.join(project_dir, task_dir, FILE_TASK_JSON)
if not os.path.exists(task_json_path):
return None
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
with open(task_json_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return None
def check_task_complete(project_dir: str, task_dir: str) -> str:
"""Check if task is complete. Returns blocking reason or empty string."""
task_info = get_task_info(project_dir, task_dir)
if not task_info:
return ""
parts = content.split("---", 2)
if len(parts) >= 3:
return parts[2]
return ""
def check_state_file(state_file: str, stdin_payload: str) -> str:
active_raw = frontmatter_get(state_file, "active")
active_lc = active_raw.lower()
if active_lc not in ("true", "1", "yes", "on"):
status = task_info.get("status", "")
if status == "completed":
return ""
current_phase_raw = frontmatter_get(state_file, "current_phase")
max_phases_raw = frontmatter_get(state_file, "max_phases")
phase_name = frontmatter_get(state_file, "phase_name")
completion_promise = frontmatter_get(state_file, "completion_promise")
current_phase = task_info.get("current_phase", 1)
max_phases = task_info.get("max_phases", 5)
phase_name = task_info.get("phase_name", phase_name_for(current_phase))
completion_promise = task_info.get("completion_promise", "<promise>DO_COMPLETE</promise>")
try:
current_phase = int(current_phase_raw)
except (ValueError, TypeError):
current_phase = 1
try:
max_phases = int(max_phases_raw)
except (ValueError, TypeError):
max_phases = 5
if not phase_name:
phase_name = phase_name_for(current_phase)
if not completion_promise:
completion_promise = "<promise>DO_COMPLETE</promise>"
phases_done = current_phase >= max_phases
if phases_done:
# 阶段已完成,清理状态文件并允许退出
# promise 检测作为可选确认,不阻止退出
try:
os.remove(state_file)
except Exception:
pass
if current_phase >= max_phases:
# Task is at final phase, allow exit
return ""
return (f"do loop incomplete: current phase {current_phase}/{max_phases} ({phase_name}). "
f"Continue with remaining phases; update {state_file} current_phase/phase_name after each phase. "
f"Include completion_promise in final output when done: {completion_promise}. "
f"To exit early, set active to false.")
return (
f"do loop incomplete: current phase {current_phase}/{max_phases} ({phase_name}). "
f"Continue with remaining phases; use 'task.py update-phase <N>' after each phase. "
f"Include completion_promise in final output when done: {completion_promise}. "
f"To exit early, set status to 'completed' in task.json."
)
def main():
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
state_dir = os.path.join(project_dir, ".claude")
do_task_id = os.environ.get("DO_TASK_ID", "")
if do_task_id:
candidate = os.path.join(state_dir, f"do.{do_task_id}.local.md")
state_files = [candidate] if os.path.isfile(candidate) else []
else:
state_files = glob.glob(os.path.join(state_dir, "do.*.local.md"))
if not state_files:
task_dir = get_current_task(project_dir)
if not task_dir:
# No active task, allow exit
sys.exit(0)
stdin_payload = ""
@@ -114,18 +95,13 @@ def main():
except Exception:
pass
blocking_reasons = []
for state_file in state_files:
reason = check_state_file(state_file, stdin_payload)
if reason:
blocking_reasons.append(reason)
if not blocking_reasons:
reason = check_task_complete(project_dir, task_dir)
if not reason:
sys.exit(0)
combined_reason = " ".join(blocking_reasons)
print(json.dumps({"decision": "block", "reason": combined_reason}))
print(json.dumps({"decision": "block", "reason": reason}))
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""
Verify Loop Hook for do skill workflow.
SubagentStop hook that intercepts when code-reviewer agent tries to stop.
Runs verification commands to ensure code quality before allowing exit.
Mechanism:
- Intercepts SubagentStop event for code-reviewer agent
- Runs verify commands from task.json if configured
- Blocks stopping until verification passes
- Has max iterations as safety limit (MAX_ITERATIONS=5)
State file: .claude/do-tasks/.verify-state.json
"""
import json
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path
# Configuration
MAX_ITERATIONS = 5
STATE_TIMEOUT_MINUTES = 30
DIR_TASKS = ".claude/do-tasks"
FILE_CURRENT_TASK = ".current-task"
FILE_TASK_JSON = "task.json"
STATE_FILE = ".claude/do-tasks/.verify-state.json"
# Only control loop for code-reviewer agent
TARGET_AGENTS = {"code-reviewer"}
def get_project_root(cwd: str) -> str | None:
"""Find project root (directory with .claude folder)."""
current = Path(cwd).resolve()
while current != current.parent:
if (current / ".claude").exists():
return str(current)
current = current.parent
return None
def get_current_task(project_root: str) -> str | None:
"""Read current task directory path."""
current_task_file = os.path.join(project_root, DIR_TASKS, FILE_CURRENT_TASK)
if not os.path.exists(current_task_file):
return None
try:
with open(current_task_file, "r", encoding="utf-8") as f:
content = f.read().strip()
return content if content else None
except Exception:
return None
def get_task_info(project_root: str, task_dir: str) -> dict | None:
"""Read task.json data."""
task_json_path = os.path.join(project_root, task_dir, FILE_TASK_JSON)
if not os.path.exists(task_json_path):
return None
try:
with open(task_json_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return None
def get_verify_commands(task_info: dict) -> list[str]:
"""Get verify commands from task.json."""
return task_info.get("verify_commands", [])
def run_verify_commands(project_root: str, commands: list[str]) -> tuple[bool, str]:
"""Run verify commands and return (success, message)."""
for cmd in commands:
try:
result = subprocess.run(
cmd,
shell=True,
cwd=project_root,
capture_output=True,
timeout=120,
)
if result.returncode != 0:
stderr = result.stderr.decode("utf-8", errors="replace")
stdout = result.stdout.decode("utf-8", errors="replace")
error_output = stderr or stdout
if len(error_output) > 500:
error_output = error_output[:500] + "..."
return False, f"Command failed: {cmd}\n{error_output}"
except subprocess.TimeoutExpired:
return False, f"Command timed out: {cmd}"
except Exception as e:
return False, f"Command error: {cmd} - {str(e)}"
return True, "All verify commands passed"
def load_state(project_root: str) -> dict:
"""Load verify loop state."""
state_path = os.path.join(project_root, STATE_FILE)
if not os.path.exists(state_path):
return {"task": None, "iteration": 0, "started_at": None}
try:
with open(state_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {"task": None, "iteration": 0, "started_at": None}
def save_state(project_root: str, state: dict) -> None:
"""Save verify loop state."""
state_path = os.path.join(project_root, STATE_FILE)
try:
os.makedirs(os.path.dirname(state_path), exist_ok=True)
with open(state_path, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2, ensure_ascii=False)
except Exception:
pass
def main():
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError:
sys.exit(0)
hook_event = input_data.get("hook_event_name", "")
if hook_event != "SubagentStop":
sys.exit(0)
subagent_type = input_data.get("subagent_type", "")
agent_output = input_data.get("agent_output", "")
cwd = input_data.get("cwd", os.getcwd())
if subagent_type not in TARGET_AGENTS:
sys.exit(0)
project_root = get_project_root(cwd)
if not project_root:
sys.exit(0)
task_dir = get_current_task(project_root)
if not task_dir:
sys.exit(0)
task_info = get_task_info(project_root, task_dir)
if not task_info:
sys.exit(0)
verify_commands = get_verify_commands(task_info)
if not verify_commands:
# No verify commands configured, allow exit
sys.exit(0)
# Load state
state = load_state(project_root)
# Reset state if task changed or too old
should_reset = False
if state.get("task") != task_dir:
should_reset = True
elif state.get("started_at"):
try:
started = datetime.fromisoformat(state["started_at"])
if (datetime.now() - started).total_seconds() > STATE_TIMEOUT_MINUTES * 60:
should_reset = True
except (ValueError, TypeError):
should_reset = True
if should_reset:
state = {
"task": task_dir,
"iteration": 0,
"started_at": datetime.now().isoformat(),
}
# Increment iteration
state["iteration"] = state.get("iteration", 0) + 1
current_iteration = state["iteration"]
save_state(project_root, state)
# Safety check: max iterations
if current_iteration >= MAX_ITERATIONS:
state["iteration"] = 0
save_state(project_root, state)
output = {
"decision": "allow",
"reason": f"Max iterations ({MAX_ITERATIONS}) reached. Stopping to prevent infinite loop.",
}
print(json.dumps(output, ensure_ascii=False))
sys.exit(0)
# Run verify commands
passed, message = run_verify_commands(project_root, verify_commands)
if passed:
state["iteration"] = 0
save_state(project_root, state)
output = {
"decision": "allow",
"reason": "All verify commands passed. Review phase complete.",
}
print(json.dumps(output, ensure_ascii=False))
sys.exit(0)
else:
output = {
"decision": "block",
"reason": f"Iteration {current_iteration}/{MAX_ITERATIONS}. Verification failed:\n{message}\n\nPlease fix the issues and try again.",
}
print(json.dumps(output, ensure_ascii=False))
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
Get context for current task.
Reads the current task's jsonl files and returns context for specified agent.
Used by inject-context hook to build agent prompts.
"""
import json
import os
import sys
from pathlib import Path
DIR_TASKS = ".claude/do-tasks"
FILE_CURRENT_TASK = ".current-task"
FILE_TASK_JSON = "task.json"
def get_project_root() -> str:
return os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
def get_current_task(project_root: str) -> str | None:
current_task_file = os.path.join(project_root, DIR_TASKS, FILE_CURRENT_TASK)
if not os.path.exists(current_task_file):
return None
try:
with open(current_task_file, "r", encoding="utf-8") as f:
content = f.read().strip()
return content if content else None
except Exception:
return None
def read_file_content(base_path: str, file_path: str) -> str | None:
full_path = os.path.join(base_path, file_path)
if os.path.exists(full_path) and os.path.isfile(full_path):
try:
with open(full_path, "r", encoding="utf-8") as f:
return f.read()
except Exception:
return None
return None
def read_jsonl_entries(base_path: str, jsonl_path: str) -> list[tuple[str, str]]:
full_path = os.path.join(base_path, jsonl_path)
if not os.path.exists(full_path):
return []
results = []
try:
with open(full_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
item = json.loads(line)
file_path = item.get("file") or item.get("path")
if not file_path:
continue
content = read_file_content(base_path, file_path)
if content:
results.append((file_path, content))
except json.JSONDecodeError:
continue
except Exception:
pass
return results
def get_agent_context(project_root: str, task_dir: str, agent_type: str) -> str:
"""Get complete context for specified agent."""
context_parts = []
# Read agent-specific jsonl
agent_jsonl = os.path.join(task_dir, f"{agent_type}.jsonl")
agent_entries = read_jsonl_entries(project_root, agent_jsonl)
for file_path, content in agent_entries:
context_parts.append(f"=== {file_path} ===\n{content}")
# Read prd.md
prd_content = read_file_content(project_root, os.path.join(task_dir, "prd.md"))
if prd_content:
context_parts.append(f"=== {task_dir}/prd.md (Requirements) ===\n{prd_content}")
return "\n\n".join(context_parts)
def get_task_info(project_root: str, task_dir: str) -> dict | None:
"""Get task.json data."""
task_json_path = os.path.join(project_root, task_dir, FILE_TASK_JSON)
if not os.path.exists(task_json_path):
return None
try:
with open(task_json_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return None
def main():
import argparse
parser = argparse.ArgumentParser(description="Get context for current task")
parser.add_argument("agent", nargs="?", choices=["implement", "check", "debug"],
help="Agent type (optional, returns task info if not specified)")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
project_root = get_project_root()
task_dir = get_current_task(project_root)
if not task_dir:
if args.json:
print(json.dumps({"error": "No active task"}))
else:
print("No active task.", file=sys.stderr)
sys.exit(1)
task_info = get_task_info(project_root, task_dir)
if not args.agent:
if args.json:
print(json.dumps({"task_dir": task_dir, "task_info": task_info}))
else:
print(f"Task: {task_dir}")
if task_info:
print(f"Title: {task_info.get('title', 'N/A')}")
print(f"Phase: {task_info.get('current_phase', '?')}/{task_info.get('max_phases', 5)}")
sys.exit(0)
context = get_agent_context(project_root, task_dir, args.agent)
if args.json:
print(json.dumps({
"task_dir": task_dir,
"agent": args.agent,
"context": context,
"task_info": task_info,
}))
else:
print(context)
if __name__ == "__main__":
main()

View File

@@ -1,28 +1,27 @@
#!/usr/bin/env python3
"""
Initialize do skill workflow - wrapper around task.py.
Creates a task directory under .claude/do-tasks/ with:
- task.md: Task metadata (YAML frontmatter) + requirements (Markdown body)
If --worktree is specified, also creates a git worktree for isolated development.
"""
import argparse
import os
import secrets
import sys
import time
PHASE_NAMES = {
1: "Understand",
2: "Clarify",
3: "Design",
4: "Implement",
5: "Complete",
}
from task import create_task, PHASE_NAMES
def phase_name_for(n: int) -> str:
return PHASE_NAMES.get(n, f"Phase {n}")
def die(msg: str):
print(f" {msg}", file=sys.stderr)
print(f"Error: {msg}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="Creates (or overwrites) project state file: .claude/do.local.md"
description="Initialize do skill workflow with task directory"
)
parser.add_argument("--max-phases", type=int, default=5, help="Default: 5")
parser.add_argument(
@@ -34,52 +33,26 @@ def main():
parser.add_argument("prompt", nargs="+", help="Task description")
args = parser.parse_args()
max_phases = args.max_phases
completion_promise = args.completion_promise
use_worktree = args.worktree
prompt = " ".join(args.prompt)
if max_phases < 1:
if args.max_phases < 1:
die("--max-phases must be a positive integer")
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
state_dir = os.path.join(project_dir, ".claude")
prompt = " ".join(args.prompt)
result = create_task(title=prompt, use_worktree=args.worktree)
task_id = f"{int(time.time())}-{os.getpid()}-{secrets.token_hex(4)}"
state_file = os.path.join(state_dir, f"do.{task_id}.local.md")
task_data = result["task_data"]
worktree_dir = result.get("worktree_dir", "")
os.makedirs(state_dir, exist_ok=True)
print(f"Initialized: {result['relative_path']}")
print(f"task_id: {task_data['id']}")
print(f"phase: 1/{task_data['max_phases']} ({PHASE_NAMES[1]})")
print(f"completion_promise: {task_data['completion_promise']}")
print(f"use_worktree: {task_data['use_worktree']}")
print(f"export DO_TASK_DIR={result['relative_path']}")
phase_name = phase_name_for(1)
if worktree_dir:
print(f"worktree_dir: {worktree_dir}")
print(f"export DO_WORKTREE_DIR={worktree_dir}")
content = f"""---
active: true
current_phase: 1
phase_name: "{phase_name}"
max_phases: {max_phases}
completion_promise: "{completion_promise}"
use_worktree: {str(use_worktree).lower()}
---
# do loop state
## Prompt
{prompt}
## Notes
- Update frontmatter current_phase/phase_name as you progress
- When complete, include the frontmatter completion_promise in your final output
"""
with open(state_file, "w", encoding="utf-8") as f:
f.write(content)
print(f"Initialized: {state_file}")
print(f"task_id: {task_id}")
print(f"phase: 1/{max_phases} ({phase_name})")
print(f"completion_promise: {completion_promise}")
print(f"use_worktree: {use_worktree}")
print(f"export DO_TASK_ID={task_id}")
if __name__ == "__main__":
main()

434
skills/do/scripts/task.py Normal file
View File

@@ -0,0 +1,434 @@
#!/usr/bin/env python3
"""
Task Directory Management CLI for do skill workflow.
Commands:
create <title> - Create a new task directory with task.md
start <task-dir> - Set current task pointer
finish - Clear current task pointer
list - List active tasks
status - Show current task status
update-phase <N> - Update current phase
"""
import argparse
import os
import random
import re
import string
import subprocess
import sys
from datetime import datetime
from pathlib import Path
# Directory constants
DIR_TASKS = ".claude/do-tasks"
FILE_CURRENT_TASK = ".current-task"
FILE_TASK_MD = "task.md"
PHASE_NAMES = {
1: "Understand",
2: "Clarify",
3: "Design",
4: "Implement",
5: "Complete",
}
def get_project_root() -> str:
"""Get project root from env or cwd."""
return os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
def get_tasks_dir(project_root: str) -> str:
"""Get tasks directory path."""
return os.path.join(project_root, DIR_TASKS)
def get_current_task_file(project_root: str) -> str:
"""Get current task pointer file path."""
return os.path.join(project_root, DIR_TASKS, FILE_CURRENT_TASK)
def generate_task_id() -> str:
"""Generate short task ID: MMDD-XXXX format."""
date_part = datetime.now().strftime("%m%d")
random_part = ''.join(random.choices(string.ascii_lowercase + string.digits, k=4))
return f"{date_part}-{random_part}"
def read_task_md(task_md_path: str) -> dict | None:
"""Read task.md and parse YAML frontmatter + body."""
if not os.path.exists(task_md_path):
return None
try:
with open(task_md_path, "r", encoding="utf-8") as f:
content = f.read()
except Exception:
return None
# Parse YAML frontmatter
match = re.match(r'^---\n(.*?)\n---\n(.*)$', content, re.DOTALL)
if not match:
return None
frontmatter_str = match.group(1)
body = match.group(2)
# Simple YAML parsing (no external deps)
frontmatter = {}
for line in frontmatter_str.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip()
# Handle quoted strings
if value.startswith('"') and value.endswith('"'):
value = value[1:-1]
elif value == 'true':
value = True
elif value == 'false':
value = False
elif value.isdigit():
value = int(value)
frontmatter[key] = value
return {"frontmatter": frontmatter, "body": body}
def write_task_md(task_md_path: str, frontmatter: dict, body: str) -> bool:
"""Write task.md with YAML frontmatter + body."""
try:
lines = ["---"]
for key, value in frontmatter.items():
if isinstance(value, bool):
lines.append(f"{key}: {str(value).lower()}")
elif isinstance(value, int):
lines.append(f"{key}: {value}")
elif isinstance(value, str) and ('<' in value or '>' in value or ':' in value):
lines.append(f'{key}: "{value}"')
else:
lines.append(f'{key}: "{value}"' if isinstance(value, str) else f"{key}: {value}")
lines.append("---")
lines.append("")
lines.append(body)
with open(task_md_path, "w", encoding="utf-8") as f:
f.write('\n'.join(lines))
return True
except Exception:
return False
def create_worktree(project_root: str, task_id: str) -> str:
"""Create a git worktree for the task. Returns the worktree directory path."""
# Get git root
result = subprocess.run(
["git", "-C", project_root, "rev-parse", "--show-toplevel"],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"Not a git repository: {project_root}")
git_root = result.stdout.strip()
# Calculate paths
worktree_dir = os.path.join(git_root, ".worktrees", f"do-{task_id}")
branch_name = f"do/{task_id}"
# Create worktree with new branch
result = subprocess.run(
["git", "-C", git_root, "worktree", "add", "-b", branch_name, worktree_dir],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"Failed to create worktree: {result.stderr}")
return worktree_dir
def create_task(title: str, use_worktree: bool = False) -> dict:
"""Create a new task directory with task.md."""
project_root = get_project_root()
tasks_dir = get_tasks_dir(project_root)
os.makedirs(tasks_dir, exist_ok=True)
task_id = generate_task_id()
task_dir = os.path.join(tasks_dir, task_id)
os.makedirs(task_dir, exist_ok=True)
# Create worktree if requested
worktree_dir = ""
if use_worktree:
try:
worktree_dir = create_worktree(project_root, task_id)
except RuntimeError as e:
print(f"Warning: {e}", file=sys.stderr)
use_worktree = False
frontmatter = {
"id": task_id,
"title": title,
"status": "in_progress",
"current_phase": 1,
"phase_name": PHASE_NAMES[1],
"max_phases": 5,
"use_worktree": use_worktree,
"worktree_dir": worktree_dir,
"created_at": datetime.now().isoformat(),
"completion_promise": "<promise>DO_COMPLETE</promise>",
}
body = f"""# Requirements
{title}
## Context
## Progress
"""
task_md_path = os.path.join(task_dir, FILE_TASK_MD)
write_task_md(task_md_path, frontmatter, body)
current_task_file = get_current_task_file(project_root)
relative_task_dir = os.path.relpath(task_dir, project_root)
with open(current_task_file, "w", encoding="utf-8") as f:
f.write(relative_task_dir)
return {
"task_dir": task_dir,
"relative_path": relative_task_dir,
"task_data": frontmatter,
"worktree_dir": worktree_dir,
}
def get_current_task(project_root: str) -> str | None:
"""Read current task directory path."""
current_task_file = get_current_task_file(project_root)
if not os.path.exists(current_task_file):
return None
try:
with open(current_task_file, "r", encoding="utf-8") as f:
content = f.read().strip()
return content if content else None
except Exception:
return None
def start_task(task_dir: str) -> bool:
"""Set current task pointer."""
project_root = get_project_root()
tasks_dir = get_tasks_dir(project_root)
if os.path.isabs(task_dir):
full_path = task_dir
relative_path = os.path.relpath(task_dir, project_root)
else:
if not task_dir.startswith(DIR_TASKS):
full_path = os.path.join(tasks_dir, task_dir)
relative_path = os.path.join(DIR_TASKS, task_dir)
else:
full_path = os.path.join(project_root, task_dir)
relative_path = task_dir
if not os.path.exists(full_path):
print(f"Error: Task directory not found: {full_path}", file=sys.stderr)
return False
current_task_file = get_current_task_file(project_root)
os.makedirs(os.path.dirname(current_task_file), exist_ok=True)
with open(current_task_file, "w", encoding="utf-8") as f:
f.write(relative_path)
return True
def finish_task() -> bool:
"""Clear current task pointer."""
project_root = get_project_root()
current_task_file = get_current_task_file(project_root)
if os.path.exists(current_task_file):
os.remove(current_task_file)
return True
def list_tasks() -> list[dict]:
"""List all task directories."""
project_root = get_project_root()
tasks_dir = get_tasks_dir(project_root)
if not os.path.exists(tasks_dir):
return []
tasks = []
current_task = get_current_task(project_root)
for entry in sorted(os.listdir(tasks_dir), reverse=True):
entry_path = os.path.join(tasks_dir, entry)
if not os.path.isdir(entry_path):
continue
task_md_path = os.path.join(entry_path, FILE_TASK_MD)
if not os.path.exists(task_md_path):
continue
parsed = read_task_md(task_md_path)
if parsed:
task_data = parsed["frontmatter"]
else:
task_data = {"id": entry, "title": entry, "status": "unknown"}
relative_path = os.path.join(DIR_TASKS, entry)
task_data["path"] = relative_path
task_data["is_current"] = current_task == relative_path
tasks.append(task_data)
return tasks
def get_status() -> dict | None:
"""Get current task status."""
project_root = get_project_root()
current_task = get_current_task(project_root)
if not current_task:
return None
task_dir = os.path.join(project_root, current_task)
task_md_path = os.path.join(task_dir, FILE_TASK_MD)
parsed = read_task_md(task_md_path)
if not parsed:
return None
task_data = parsed["frontmatter"]
task_data["path"] = current_task
return task_data
def update_phase(phase: int) -> bool:
"""Update current task phase."""
project_root = get_project_root()
current_task = get_current_task(project_root)
if not current_task:
print("Error: No active task.", file=sys.stderr)
return False
task_dir = os.path.join(project_root, current_task)
task_md_path = os.path.join(task_dir, FILE_TASK_MD)
parsed = read_task_md(task_md_path)
if not parsed:
print("Error: task.md not found or invalid.", file=sys.stderr)
return False
frontmatter = parsed["frontmatter"]
frontmatter["current_phase"] = phase
frontmatter["phase_name"] = PHASE_NAMES.get(phase, f"Phase {phase}")
if not write_task_md(task_md_path, frontmatter, parsed["body"]):
print("Error: Failed to write task.md.", file=sys.stderr)
return False
return True
def main():
parser = argparse.ArgumentParser(
description="Task directory management for do skill workflow"
)
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# create command
create_parser = subparsers.add_parser("create", help="Create a new task")
create_parser.add_argument("title", nargs="+", help="Task title")
create_parser.add_argument("--worktree", action="store_true", help="Enable worktree mode")
# start command
start_parser = subparsers.add_parser("start", help="Set current task")
start_parser.add_argument("task_dir", help="Task directory path")
# finish command
subparsers.add_parser("finish", help="Clear current task")
# list command
subparsers.add_parser("list", help="List all tasks")
# status command
subparsers.add_parser("status", help="Show current task status")
# update-phase command
phase_parser = subparsers.add_parser("update-phase", help="Update current phase")
phase_parser.add_argument("phase", type=int, help="Phase number (1-5)")
args = parser.parse_args()
if args.command == "create":
title = " ".join(args.title)
result = create_task(title, args.worktree)
print(f"Created task: {result['relative_path']}")
print(f"Task ID: {result['task_data']['id']}")
print(f"Phase: 1/{result['task_data']['max_phases']} (Understand)")
print(f"Worktree: {result['task_data']['use_worktree']}")
elif args.command == "start":
if start_task(args.task_dir):
print(f"Started task: {args.task_dir}")
else:
sys.exit(1)
elif args.command == "finish":
if finish_task():
print("Task finished, current task cleared.")
else:
sys.exit(1)
elif args.command == "list":
tasks = list_tasks()
if not tasks:
print("No tasks found.")
else:
for task in tasks:
marker = "* " if task.get("is_current") else " "
phase = task.get("current_phase", "?")
max_phase = task.get("max_phases", 5)
status = task.get("status", "unknown")
print(f"{marker}{task['id']} [{status}] phase {phase}/{max_phase}")
print(f" {task.get('title', 'No title')}")
elif args.command == "status":
status = get_status()
if not status:
print("No active task.")
else:
print(f"Task: {status['id']}")
print(f"Title: {status.get('title', 'No title')}")
print(f"Status: {status.get('status', 'unknown')}")
print(f"Phase: {status.get('current_phase', '?')}/{status.get('max_phases', 5)}")
print(f"Worktree: {status.get('use_worktree', False)}")
print(f"Path: {status['path']}")
elif args.command == "update-phase":
if update_phase(args.phase):
phase_name = PHASE_NAMES.get(args.phase, f"Phase {args.phase}")
print(f"Updated to phase {args.phase} ({phase_name})")
else:
sys.exit(1)
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
main()

329
skills/harness/SKILL.md Normal file
View File

@@ -0,0 +1,329 @@
---
name: harness
description: "This skill should be used for multi-session autonomous agent work requiring progress checkpointing, failure recovery, and task dependency management. Triggers on '/harness' command, or when a task involves many subtasks needing progress persistence, sleep/resume cycles across context windows, recovery from mid-task failures with partial state, or distributed work across multiple agent sessions. Synthesized from Anthropic and OpenAI engineering practices for long-running agents."
---
# Harness — Long-Running Agent Framework
Executable protocol enabling any agent task to run continuously across multiple sessions with automatic progress recovery, task dependency resolution, failure rollback, and standardized error handling.
## Design Principles
1. **Design for the agent, not the human** — Test output, docs, and task structure are the agent's primary interface
2. **Progress files ARE the context** — When context window resets, progress files + git history = full recovery
3. **Premature completion is the #1 failure mode** — Structured task lists with explicit completion criteria prevent declaring victory early
4. **Standardize everything grep-able** — ERROR on same line, structured timestamps, consistent prefixes
5. **Fast feedback loops** — Pre-compute stats, run smoke tests before full validation
6. **Idempotent everything** — Init scripts, task execution, environment setup must all be safe to re-run
7. **Fail safe, not fail silent** — Every failure must have an explicit recovery strategy
## Commands
```
/harness init <project-path> # Initialize harness files in project
/harness run # Start/resume the infinite loop
/harness status # Show current progress and stats
/harness add "task description" # Add a task to the list
```
## Progress Persistence (Dual-File System)
Maintain two files in the project working directory:
### harness-progress.txt (Append-Only Log)
Free-text log of all agent actions across sessions. Never truncate.
```
[2025-07-01T10:00:00Z] [SESSION-1] INIT Harness initialized for project /path/to/project
[2025-07-01T10:00:05Z] [SESSION-1] INIT Environment health check: PASS
[2025-07-01T10:00:10Z] [SESSION-1] LOCK acquired (pid=12345)
[2025-07-01T10:00:11Z] [SESSION-1] Starting [task-001] Implement user authentication (base=def5678)
[2025-07-01T10:05:00Z] [SESSION-1] CHECKPOINT [task-001] step=2/4 "auth routes created, tests pending"
[2025-07-01T10:15:30Z] [SESSION-1] Completed [task-001] (commit abc1234)
[2025-07-01T10:15:31Z] [SESSION-1] Starting [task-002] Add rate limiting (base=abc1234)
[2025-07-01T10:20:00Z] [SESSION-1] ERROR [task-002] [TASK_EXEC] Redis connection refused
[2025-07-01T10:20:01Z] [SESSION-1] ROLLBACK [task-002] git reset --hard abc1234
[2025-07-01T10:20:02Z] [SESSION-1] STATS tasks_total=5 completed=1 failed=1 pending=3 blocked=0 attempts_total=2 checkpoints=1
```
### harness-tasks.json (Structured State)
```json
{
"version": 2,
"created": "2025-07-01T10:00:00Z",
"session_config": {
"max_tasks_per_session": 20,
"max_sessions": 50
},
"tasks": [
{
"id": "task-001",
"title": "Implement user authentication",
"status": "completed",
"priority": "P0",
"depends_on": [],
"attempts": 1,
"max_attempts": 3,
"started_at_commit": "def5678",
"validation": {
"command": "npm test -- --testPathPattern=auth",
"timeout_seconds": 300
},
"on_failure": {
"cleanup": null
},
"error_log": [],
"checkpoints": [],
"completed_at": "2025-07-01T10:15:30Z"
},
{
"id": "task-002",
"title": "Add rate limiting",
"status": "failed",
"priority": "P1",
"depends_on": [],
"attempts": 1,
"max_attempts": 3,
"started_at_commit": "abc1234",
"validation": {
"command": "npm test -- --testPathPattern=rate-limit",
"timeout_seconds": 120
},
"on_failure": {
"cleanup": "docker compose down redis"
},
"error_log": ["[TASK_EXEC] Redis connection refused"],
"checkpoints": [],
"completed_at": null
},
{
"id": "task-003",
"title": "Add OAuth providers",
"status": "pending",
"priority": "P1",
"depends_on": ["task-001"],
"attempts": 0,
"max_attempts": 3,
"started_at_commit": null,
"validation": {
"command": "npm test -- --testPathPattern=oauth",
"timeout_seconds": 180
},
"on_failure": {
"cleanup": null
},
"error_log": [],
"checkpoints": [],
"completed_at": null
}
],
"session_count": 1,
"last_session": "2025-07-01T10:20:02Z"
}
```
Task statuses: `pending``in_progress` (transient, set only during active execution) → `completed` or `failed`. A task found as `in_progress` at session start means the previous session was interrupted — handle via Context Window Recovery Protocol.
**Session boundary**: A session starts when the agent begins executing the Session Start protocol and ends when a Stopping Condition is met or the context window resets. Each session gets a unique `SESSION-N` identifier (N = `session_count` after increment).
## Concurrency Control
Before modifying `harness-tasks.json`, acquire an exclusive lock using portable `mkdir` (atomic on all POSIX systems, works on both macOS and Linux):
```bash
# Acquire lock (fail fast if another agent is running)
LOCKDIR="/tmp/harness-$(printf '%s' "$(pwd)" | shasum -a 256 2>/dev/null || sha256sum | cut -c1-8).lock"
if ! mkdir "$LOCKDIR" 2>/dev/null; then
# Check if lock holder is still alive
LOCK_PID=$(cat "$LOCKDIR/pid" 2>/dev/null)
if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then
echo "ERROR: Another harness session is active (pid=$LOCK_PID)"; exit 1
fi
# Stale lock — atomically reclaim via mv to avoid TOCTOU race
STALE="$LOCKDIR.stale.$$"
if mv "$LOCKDIR" "$STALE" 2>/dev/null; then
rm -rf "$STALE"
mkdir "$LOCKDIR" || { echo "ERROR: Lock contention"; exit 1; }
echo "WARN: Removed stale lock${LOCK_PID:+ from pid=$LOCK_PID}"
else
echo "ERROR: Another agent reclaimed the lock"; exit 1
fi
fi
echo "$$" > "$LOCKDIR/pid"
trap 'rm -rf "$LOCKDIR"' EXIT
```
Log lock acquisition: `[timestamp] [SESSION-N] LOCK acquired (pid=<PID>)`
Log lock release: `[timestamp] [SESSION-N] LOCK released`
The lock is held for the entire session. The `trap EXIT` handler releases it automatically on normal exit, errors, or signals. Never release the lock between tasks within a session.
## Infinite Loop Protocol
### Session Start (Execute Every Time)
1. **Read state**: Read last 200 lines of `harness-progress.txt` + full `harness-tasks.json`. If JSON is unparseable, see JSON corruption recovery in Error Handling.
2. **Read git**: Run `git log --oneline -20` and `git diff --stat` to detect uncommitted work
3. **Acquire lock**: Fail if another session is active
4. **Recover interrupted tasks** (see Context Window Recovery below)
5. **Health check**: Run `harness-init.sh` if it exists
6. **Track session**: Increment `session_count` in JSON. Check `session_count` against `max_sessions` — if reached, log STATS and STOP. Initialize per-session task counter to 0.
7. **Pick next task** using Task Selection Algorithm below
### Task Selection Algorithm
Before selecting, run dependency validation:
1. **Cycle detection**: For each non-completed task, walk `depends_on` transitively. If any task appears in its own chain, mark it `failed` with `[DEPENDENCY] Circular dependency detected: task-A -> task-B -> task-A`. Self-references (`depends_on` includes own id) are also cycles.
2. **Blocked propagation**: If a task's `depends_on` includes a task that is `failed` and will never be retried (either `attempts >= max_attempts` OR its `error_log` contains a `[DEPENDENCY]` entry), mark the blocked task as `failed` with `[DEPENDENCY] Blocked by failed task-XXX`. Repeat until no more tasks can be propagated.
Then pick the next task in this priority order:
1. Tasks with `status: "pending"` where ALL `depends_on` tasks are `completed` — sorted by `priority` (P0 > P1 > P2), then by `id` (lowest first)
2. Tasks with `status: "failed"` where `attempts < max_attempts` and ALL `depends_on` are `completed` — sorted by priority, then oldest failure first
3. If no eligible tasks remain → log final STATS → STOP
### Task Execution Cycle
For each task, execute this exact sequence:
1. **Claim**: Record `started_at_commit` = current HEAD hash. Set status to `in_progress`, log `Starting [<task-id>] <title> (base=<hash>)`
2. **Execute with checkpoints**: Perform the work. After each significant step, log:
```
[timestamp] [SESSION-N] CHECKPOINT [task-id] step=M/N "description of what was done"
```
Also append to the task's `checkpoints` array: `{ "step": M, "total": N, "description": "...", "timestamp": "ISO" }`
3. **Validate**: Run the task's `validation.command` wrapped with `timeout`: `timeout <timeout_seconds> <command>`. If no validation command, skip. Before running, verify the command exists (e.g., `command -v <binary>`) — if missing, treat as `ENV_SETUP` error.
- Command exits 0 → PASS
- Command exits non-zero → FAIL
- Command exceeds timeout → TIMEOUT
4. **Record outcome**:
- **Success**: status=`completed`, set `completed_at`, log `Completed [<task-id>] (commit <hash>)`, git commit
- **Failure**: increment `attempts`, append error to `error_log`. Verify `started_at_commit` exists via `git cat-file -t <hash>` — if missing, mark failed at max_attempts. Otherwise execute `git reset --hard <started_at_commit>` and `git clean -fd` to rollback ALL commits and remove untracked files. Execute `on_failure.cleanup` if defined. Log `ERROR [<task-id>] [<category>] <message>`. Set status=`failed` (Task Selection Algorithm pass 2 handles retries when attempts < max_attempts)
5. **Track**: Increment per-session task counter. If `max_tasks_per_session` reached, log STATS and STOP.
6. **Continue**: Immediately pick next task (zero idle time)
### Stopping Conditions
- All tasks `completed`
- All remaining tasks `failed` at max_attempts or blocked by failed dependencies
- `session_config.max_tasks_per_session` reached for this session
- `session_config.max_sessions` reached across all sessions
- User interrupts
## Context Window Recovery Protocol
When a new session starts and finds a task with `status: "in_progress"`:
1. **Check git state**:
```bash
git diff --stat # Uncommitted changes?
git log --oneline -5 # Recent commits since task started?
git stash list # Any stashed work?
```
2. **Check checkpoints**: Read the task's `checkpoints` array to determine last completed step
3. **Decision matrix** (verify recent commits belong to this task by checking commit messages for the task-id):
| Uncommitted? | Recent task commits? | Checkpoints? | Action |
|---|---|---|---|
| No | No | None | Mark `failed` with `[SESSION_TIMEOUT] No progress detected`, increment attempts |
| No | No | Some | Verify file state matches checkpoint claims. If files reflect checkpoint progress, resume from last step. If not, mark `failed` — work was lost |
| No | Yes | Any | Run `validation.command`. If passes → mark `completed`. If fails → `git reset --hard <started_at_commit>`, mark `failed` |
| Yes | No | Any | Run validation WITH uncommitted changes present. If passes → commit, mark `completed`. If fails → `git reset --hard <started_at_commit>` + `git clean -fd`, mark `failed` |
| Yes | Yes | Any | Commit uncommitted changes, run `validation.command`. If passes → mark `completed`. If fails → `git reset --hard <started_at_commit>` + `git clean -fd`, mark `failed` |
4. **Log recovery**: `[timestamp] [SESSION-N] RECOVERY [task-id] action="<action taken>" reason="<reason>"`
## Error Handling & Recovery Strategies
Each error category has a default recovery strategy:
| Category | Default Recovery | Agent Action |
|----------|-----------------|--------------|
| `ENV_SETUP` | Re-run init, then STOP if still failing | Run `harness-init.sh` again immediately. If fails twice, log and stop — environment is broken |
| `TASK_EXEC` | Rollback via `git reset --hard <started_at_commit>`, retry | Verify `started_at_commit` exists (`git cat-file -t <hash>`). If missing, mark failed at max_attempts. Otherwise reset, run `on_failure.cleanup` if defined, retry if attempts < max_attempts |
| `TEST_FAIL` | Rollback via `git reset --hard <started_at_commit>`, retry | Reset to `started_at_commit`, analyze test output to identify fix, retry with targeted changes |
| `TIMEOUT` | Kill process, execute cleanup, retry | Wrap validation with `timeout <seconds> <command>`. On timeout, run `on_failure.cleanup`, retry (consider splitting task if repeated) |
| `DEPENDENCY` | Skip task, mark blocked | Log which dependency failed, mark task as `failed` with dependency reason |
| `SESSION_TIMEOUT` | Use Context Window Recovery Protocol | New session assesses partial progress via Recovery Protocol — may result in completion or failure depending on validation |
**JSON corruption**: If `harness-tasks.json` cannot be parsed, check for `harness-tasks.json.bak` (written before each modification). If backup exists and is valid, restore from it. If no valid backup, log `ERROR [ENV_SETUP] harness-tasks.json corrupted and unrecoverable` and STOP — task metadata (validation commands, dependencies, cleanup) cannot be reconstructed from logs alone.
**Backup protocol**: Before every write to `harness-tasks.json`, copy the current file to `harness-tasks.json.bak`.
## Environment Initialization
If `harness-init.sh` exists in the project root, run it at every session start. The script must be idempotent.
Example `harness-init.sh`:
```bash
#!/bin/bash
set -e
npm install 2>/dev/null || pip install -r requirements.txt 2>/dev/null || true
curl -sf http://localhost:5432 >/dev/null 2>&1 || echo "WARN: DB not reachable"
npm test -- --bail --silent 2>/dev/null || echo "WARN: Smoke test failed"
echo "Environment health check complete"
```
## Standardized Log Format
All log entries use grep-friendly format on a single line:
```
[ISO-timestamp] [SESSION-N] <TYPE> [task-id]? [category]? message
```
`[task-id]` and `[category]` are included when applicable (task-scoped entries). Session-level entries (`INIT`, `LOCK`, `STATS`) omit them.
Types: `INIT`, `Starting`, `Completed`, `ERROR`, `CHECKPOINT`, `ROLLBACK`, `RECOVERY`, `STATS`, `LOCK`, `WARN`
Error categories: `ENV_SETUP`, `TASK_EXEC`, `TEST_FAIL`, `TIMEOUT`, `DEPENDENCY`, `SESSION_TIMEOUT`
Filtering:
```bash
grep "ERROR" harness-progress.txt # All errors
grep "ERROR" harness-progress.txt | grep "TASK_EXEC" # Execution errors only
grep "SESSION-3" harness-progress.txt # All session 3 activity
grep "STATS" harness-progress.txt # All session summaries
grep "CHECKPOINT" harness-progress.txt # All checkpoints
grep "RECOVERY" harness-progress.txt # All recovery actions
```
## Session Statistics
At session end, update `harness-tasks.json`: increment `session_count`, set `last_session` to current timestamp. Then append:
```
[timestamp] [SESSION-N] STATS tasks_total=10 completed=7 failed=1 pending=2 blocked=0 attempts_total=12 checkpoints=23
```
`blocked` is computed at stats time: count of pending tasks whose `depends_on` includes a permanently failed task. It is not a stored status value.
## Init Command (`/harness init`)
1. Create `harness-progress.txt` with initialization entry
2. Create `harness-tasks.json` with empty task list and default `session_config`
3. Optionally create `harness-init.sh` template (chmod +x)
4. Ask user: add harness files to `.gitignore`?
## Status Command (`/harness status`)
Read `harness-tasks.json` and `harness-progress.txt`, then display:
1. Task summary: count by status (completed, failed, pending, blocked). `blocked` = pending tasks whose `depends_on` includes a permanently failed task (computed, not a stored status).
2. Per-task one-liner: `[status] task-id: title (attempts/max_attempts)`
3. Last 5 lines from `harness-progress.txt`
4. Session count and last session timestamp
Does NOT acquire the lock (read-only operation).
## Add Command (`/harness add`)
Append a new task to `harness-tasks.json` with auto-incremented id (`task-NNN`), status `pending`, default `max_attempts: 3`, empty `depends_on`, and no validation command. Prompt user for optional fields: `priority`, `depends_on`, `validation.command`, `timeout_seconds`. Requires lock acquisition (modifies JSON).
## Tool Dependencies
Requires: Bash, file read/write, git. All harness operations must be executed from the project root directory.
Does NOT require: specific MCP servers, programming languages, or test frameworks.

View File

@@ -0,0 +1,52 @@
{
"default_backend": "codex",
"default_model": "gpt-5.2",
"backends": {
"codex": { "api_key": "" },
"claude": { "api_key": "" },
"gemini": { "api_key": "" },
"opencode": { "api_key": "" }
},
"agents": {
"develop": {
"backend": "codex",
"model": "gpt-5.2",
"reasoning": "xhigh",
"yolo": true
},
"code-explorer": {
"backend": "opencode",
"model": ""
},
"code-architect": {
"backend": "claude",
"model": ""
},
"code-reviewer": {
"backend": "claude",
"model": ""
},
"oracle": {
"backend": "claude",
"model": "claude-opus-4-5-20251101",
"yolo": true
},
"librarian": {
"backend": "claude",
"model": "claude-sonnet-4-5-20250929",
"yolo": true
},
"explore": {
"backend": "opencode",
"model": "opencode/grok-code"
},
"frontend-ui-ux-engineer": {
"backend": "gemini",
"model": "gemini-3-pro-preview"
},
"document-writer": {
"backend": "gemini",
"model": "gemini-3-flash-preview"
}
}
}