feat: refactor model selection to use developer tiers
- Replaced raw model aliases with developer tiers (junior, medior, senior, qa) in dispatch and model selection logic. - Updated `dispatchTask` to resolve models based on tiers and plugin configuration. - Modified `selectModel` to return tier names instead of model aliases based on task description. - Implemented migration logic for transitioning from old model aliases to new tier names in worker state. - Added setup logic for agent creation and model configuration in `setup.ts`. - Created shared templates for workspace files and instructions for DEV/QA workers. - Enhanced project registration to scaffold role files based on developer tiers. - Updated task management tools to reflect changes in model selection and tier assignment. - Introduced a new `devclaw_setup` tool for agent-driven setup and configuration. - Updated plugin configuration schema to support model mapping per developer tier.
This commit is contained in:
114
README.md
114
README.md
@@ -14,9 +14,22 @@ DevClaw fills that gap with guardrails. It gives the orchestrator atomic tools t
|
||||
|
||||
## The idea
|
||||
|
||||
One orchestrator agent manages all your projects. It reads task backlogs, creates issues, decides priorities, and delegates work. For each task, DevClaw creates (or reuses) a **DEV** worker session to write code or a **QA** worker session to review it. Every Telegram group is a separate project — the orchestrator keeps them completely isolated while managing them all from a single process.
|
||||
One orchestrator agent manages all your projects. It reads task backlogs, creates issues, decides priorities, and delegates work. For each task, DevClaw assigns a developer from your **team** — a junior, medior, or senior dev writes the code, then a QA engineer reviews it. Every Telegram group is a separate project — the orchestrator keeps them completely isolated while managing them all from a single process.
|
||||
|
||||
DevClaw gives the orchestrator six tools that replace hundreds of lines of manual orchestration logic. Instead of following a 10-step checklist per task (fetch issue, check labels, pick model, check for existing session, transition label, dispatch task, update state, log audit event...), it calls `task_pickup` and the plugin handles everything atomically — including session dispatch. Workers call `task_complete` themselves for atomic state updates, and can file follow-up issues via `task_create`.
|
||||
DevClaw gives the orchestrator seven tools that replace hundreds of lines of manual orchestration logic. Instead of following a 10-step checklist per task (fetch issue, check labels, pick model, check for existing session, transition label, dispatch task, update state, log audit event...), it calls `task_pickup` and the plugin handles everything atomically — including session dispatch. Workers call `task_complete` themselves for atomic state updates, and can file follow-up issues via `task_create`.
|
||||
|
||||
## Developer tiers
|
||||
|
||||
DevClaw uses a developer seniority model. Each tier maps to a configurable LLM model:
|
||||
|
||||
| Tier | Role | Default model | Assigns to |
|
||||
|------|------|---------------|------------|
|
||||
| **junior** | Junior developer | `anthropic/claude-haiku-4-5` | Typos, single-file fixes, simple changes |
|
||||
| **medior** | Mid-level developer | `anthropic/claude-sonnet-4-5` | Features, bug fixes, multi-file changes |
|
||||
| **senior** | Senior developer | `anthropic/claude-opus-4-5` | Architecture, migrations, system-wide refactoring |
|
||||
| **qa** | QA engineer | `anthropic/claude-sonnet-4-5` | Code review, test validation |
|
||||
|
||||
Configure which model each tier uses during setup or in `openclaw.json` plugin config.
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -93,15 +106,15 @@ Workers (DEV/QA sub-agent sessions) call `task_complete` directly when they fini
|
||||
### Auto-chaining
|
||||
|
||||
When a project has `autoChain: true`, `task_complete` automatically dispatches the next step:
|
||||
- **DEV "done"** → QA is dispatched immediately (default model: grok)
|
||||
- **QA "fail"** → DEV fix is dispatched immediately (reuses previous DEV model)
|
||||
- **DEV "done"** → QA is dispatched immediately (using the qa tier)
|
||||
- **QA "fail"** → DEV fix is dispatched immediately (reuses previous DEV tier)
|
||||
- **QA "pass" / "refine"** → no chaining (pipeline done or needs human input)
|
||||
|
||||
When `autoChain` is false, `task_complete` returns a `nextAction` hint for the orchestrator to act on.
|
||||
|
||||
## Session reuse
|
||||
|
||||
Worker sessions are expensive to start — each new spawn requires the session to read the full codebase (~50K tokens). DevClaw maintains **separate sessions per model per role** (session-per-model design). When a DEV finishes task A and picks up task B on the same project with the same model, the plugin detects the existing session and sends the task directly — no new session needed.
|
||||
Worker sessions are expensive to start — each new spawn requires the session to read the full codebase (~50K tokens). DevClaw maintains **separate sessions per tier per role** (session-per-tier design). When a medior dev finishes task A and picks up task B on the same project, the plugin detects the existing session and sends the task directly — no new session needed.
|
||||
|
||||
The plugin handles session dispatch internally via OpenClaw CLI. The orchestrator agent never calls `sessions_spawn` or `sessions_send` — it just calls `task_pickup` and the plugin does the rest.
|
||||
|
||||
@@ -114,26 +127,26 @@ sequenceDiagram
|
||||
|
||||
O->>DC: task_pickup({ issueId: 42, role: "dev" })
|
||||
DC->>GL: Fetch issue, verify label
|
||||
DC->>DC: Select model (haiku/sonnet/opus)
|
||||
DC->>DC: Check existing session for selected model
|
||||
DC->>DC: Assign tier (junior/medior/senior)
|
||||
DC->>DC: Check existing session for assigned tier
|
||||
DC->>GL: Transition label (To Do → Doing)
|
||||
DC->>S: Dispatch task via CLI (create or reuse session)
|
||||
DC->>DC: Update projects.json, write audit log
|
||||
DC-->>O: { success: true, announcement: "🔧 DEV (sonnet) picking up #42" }
|
||||
DC-->>O: { success: true, announcement: "🔧 DEV (medior) picking up #42" }
|
||||
```
|
||||
|
||||
## Model selection
|
||||
## Developer assignment
|
||||
|
||||
The orchestrator LLM analyzes each issue's title, description, and labels to choose the appropriate model tier, then passes it to `task_pickup` via the `model` parameter. This gives the LLM full context for the decision — it can weigh factors like codebase familiarity, task dependencies, and recent failure history that keyword matching would miss.
|
||||
The orchestrator LLM evaluates each issue's title, description, and labels to assign the appropriate developer tier, then passes it to `task_pickup` via the `model` parameter. This gives the LLM full context for the decision — it can weigh factors like codebase familiarity, task dependencies, and recent failure history that keyword matching would miss.
|
||||
|
||||
The keyword heuristic in `model-selector.ts` serves as a **fallback only**, used when the orchestrator omits the `model` parameter.
|
||||
|
||||
| Complexity | Model | When |
|
||||
|------------|-------|------|
|
||||
| Simple | Haiku | Typos, CSS, renames, copy changes |
|
||||
| Standard | Sonnet | Features, bug fixes, multi-file changes |
|
||||
| Complex | Opus | Architecture, migrations, security, system-wide refactoring |
|
||||
| QA | Grok | All QA tasks (code review, test validation) |
|
||||
| Tier | Role | When |
|
||||
|------|------|------|
|
||||
| junior | Junior developer | Typos, CSS, renames, copy changes |
|
||||
| medior | Mid-level developer | Features, bug fixes, multi-file changes |
|
||||
| senior | Senior developer | Architecture, migrations, security, system-wide refactoring |
|
||||
| qa | QA engineer | All QA tasks (code review, test validation) |
|
||||
|
||||
## State management
|
||||
|
||||
@@ -151,19 +164,19 @@ All project state lives in a single `memory/projects.json` file in the orchestra
|
||||
"dev": {
|
||||
"active": false,
|
||||
"issueId": null,
|
||||
"model": "haiku",
|
||||
"model": "medior",
|
||||
"sessions": {
|
||||
"haiku": "agent:orchestrator:subagent:a9e4d078-...",
|
||||
"sonnet": "agent:orchestrator:subagent:b3f5c912-...",
|
||||
"opus": null
|
||||
"junior": "agent:orchestrator:subagent:a9e4d078-...",
|
||||
"medior": "agent:orchestrator:subagent:b3f5c912-...",
|
||||
"senior": null
|
||||
}
|
||||
},
|
||||
"qa": {
|
||||
"active": false,
|
||||
"issueId": null,
|
||||
"model": "grok",
|
||||
"model": "qa",
|
||||
"sessions": {
|
||||
"grok": "agent:orchestrator:subagent:18707821-..."
|
||||
"qa": "agent:orchestrator:subagent:18707821-..."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,7 +185,7 @@ All project state lives in a single `memory/projects.json` file in the orchestra
|
||||
```
|
||||
|
||||
Key design decisions:
|
||||
- **Session-per-model** — each model gets its own worker session, accumulating context independently. Model selection maps directly to a session key.
|
||||
- **Session-per-tier** — each tier gets its own worker session, accumulating context independently. Tier selection maps directly to a session key.
|
||||
- **Sessions preserved on completion** — when a worker completes a task, `sessions` map is **preserved** (only `active` and `issueId` are cleared). This enables session reuse on the next pickup.
|
||||
- **Plugin-controlled dispatch** — the plugin creates and dispatches to sessions via OpenClaw CLI (`sessions.patch` + `openclaw agent`). The orchestrator agent never calls `sessions_spawn` or `sessions_send`.
|
||||
- **Sessions persist indefinitely** — no auto-cleanup. `session_health` handles manual cleanup when needed.
|
||||
@@ -181,27 +194,35 @@ All writes go through atomic temp-file-then-rename to prevent corruption.
|
||||
|
||||
## Tools
|
||||
|
||||
### `task_pickup`
|
||||
### `devclaw_setup`
|
||||
|
||||
Pick up a task from the GitLab queue for a DEV or QA worker.
|
||||
Set up DevClaw in an agent's workspace. Creates AGENTS.md, HEARTBEAT.md, role templates, and configures models. Can optionally create a new agent.
|
||||
|
||||
**Parameters:**
|
||||
- `issueId` (number, required) — GitLab issue ID
|
||||
- `newAgentName` (string, optional) — Create a new agent with this name
|
||||
- `models` (object, optional) — Model overrides per tier: `{ junior, medior, senior, qa }`
|
||||
|
||||
### `task_pickup`
|
||||
|
||||
Pick up a task from the issue queue for a DEV or QA worker.
|
||||
|
||||
**Parameters:**
|
||||
- `issueId` (number, required) — Issue ID
|
||||
- `role` ("dev" | "qa", required) — Worker role
|
||||
- `projectGroupId` (string, required) — Telegram group ID
|
||||
- `model` (string, optional) — Model alias to use (e.g. haiku, sonnet, opus, grok). The orchestrator should analyze the issue complexity and choose. Falls back to keyword heuristic if omitted.
|
||||
- `model` (string, optional) — Developer tier (junior, medior, senior, qa). The orchestrator should evaluate the task complexity and choose. Falls back to keyword heuristic if omitted.
|
||||
|
||||
**What it does atomically:**
|
||||
1. Resolves project from `projects.json`
|
||||
2. Validates no active worker for this role
|
||||
3. Fetches issue from issue tracker, verifies correct label state
|
||||
4. Selects model (LLM-chosen via `model` param, keyword heuristic fallback)
|
||||
4. Assigns tier (LLM-chosen via `model` param, keyword heuristic fallback)
|
||||
5. Loads role instructions from `roles/<project>/<role>.md` (fallback: `roles/default/<role>.md`)
|
||||
6. Looks up existing session for selected model (session-per-model)
|
||||
6. Looks up existing session for assigned tier (session-per-tier)
|
||||
7. Transitions label (e.g. `To Do` → `Doing`)
|
||||
8. Creates session via Gateway RPC if new (`sessions.patch`)
|
||||
9. Dispatches task to worker session via CLI (`openclaw agent`) with role instructions appended
|
||||
10. Updates `projects.json` state (active, issueId, model, session key)
|
||||
10. Updates `projects.json` state (active, issueId, tier, session key)
|
||||
11. Writes audit log entry
|
||||
12. Returns announcement text for the orchestrator to post
|
||||
|
||||
@@ -216,9 +237,9 @@ Complete a task with one of four results. Called by workers (DEV/QA sub-agent se
|
||||
- `summary` (string, optional) — For the Telegram announcement
|
||||
|
||||
**Results:**
|
||||
- **DEV "done"** — Pulls latest code, moves label `Doing` → `To Test`, deactivates worker. If `autoChain` enabled, automatically dispatches QA (grok).
|
||||
- **DEV "done"** — Pulls latest code, moves label `Doing` → `To Test`, deactivates worker. If `autoChain` enabled, automatically dispatches QA.
|
||||
- **QA "pass"** — Moves label `Testing` → `Done`, closes issue, deactivates worker
|
||||
- **QA "fail"** — Moves label `Testing` → `To Improve`, reopens issue. If `autoChain` enabled, automatically dispatches DEV fix (reuses previous model).
|
||||
- **QA "fail"** — Moves label `Testing` → `To Improve`, reopens issue. If `autoChain` enabled, automatically dispatches DEV fix (reuses previous DEV tier).
|
||||
- **QA "refine"** — Moves label `Testing` → `Refining`, awaits human decision
|
||||
|
||||
### `task_create`
|
||||
@@ -284,24 +305,29 @@ Register a new project with DevClaw. Creates all required issue tracker labels (
|
||||
Every tool call automatically appends an NDJSON entry to `memory/audit.log`. No manual logging required from the orchestrator agent.
|
||||
|
||||
```jsonl
|
||||
{"ts":"2026-02-08T10:30:00Z","event":"task_pickup","project":"my-webapp","issue":42,"role":"dev","model":"sonnet","sessionAction":"send"}
|
||||
{"ts":"2026-02-08T10:30:01Z","event":"model_selection","issue":42,"role":"dev","selected":"sonnet","reason":"Standard dev task"}
|
||||
{"ts":"2026-02-08T10:30:00Z","event":"task_pickup","project":"my-webapp","issue":42,"role":"dev","tier":"medior","sessionAction":"send"}
|
||||
{"ts":"2026-02-08T10:30:01Z","event":"model_selection","issue":42,"role":"dev","tier":"medior","reason":"Standard dev task"}
|
||||
{"ts":"2026-02-08T10:45:00Z","event":"task_complete","project":"my-webapp","issue":42,"role":"dev","result":"done"}
|
||||
```
|
||||
|
||||
## Installation
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Local (place in extensions directory — auto-discovered)
|
||||
# 1. Install the plugin
|
||||
cp -r devclaw ~/.openclaw/extensions/
|
||||
|
||||
# From npm (future)
|
||||
openclaw plugins install @openclaw/devclaw
|
||||
# 2. Run setup (interactive — creates agent, configures models, writes workspace files)
|
||||
openclaw devclaw setup
|
||||
|
||||
# 3. Add bot to Telegram group, then register a project
|
||||
# (via the agent in Telegram)
|
||||
```
|
||||
|
||||
See the [Onboarding Guide](docs/ONBOARDING.md) for detailed instructions.
|
||||
|
||||
## Configuration
|
||||
|
||||
Optional config in `openclaw.json`:
|
||||
Model tier configuration in `openclaw.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -309,7 +335,12 @@ Optional config in `openclaw.json`:
|
||||
"entries": {
|
||||
"devclaw": {
|
||||
"config": {
|
||||
"glabPath": "/usr/local/bin/glab"
|
||||
"models": {
|
||||
"junior": "anthropic/claude-haiku-4-5",
|
||||
"medior": "anthropic/claude-sonnet-4-5",
|
||||
"senior": "anthropic/claude-opus-4-5",
|
||||
"qa": "anthropic/claude-sonnet-4-5"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,7 +356,7 @@ Restrict tools to your orchestrator agent only:
|
||||
"list": [{
|
||||
"id": "my-orchestrator",
|
||||
"tools": {
|
||||
"allow": ["task_pickup", "task_complete", "task_create", "queue_status", "session_health", "project_register"]
|
||||
"allow": ["devclaw_setup", "task_pickup", "task_complete", "task_create", "queue_status", "session_health", "project_register"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
@@ -359,7 +390,6 @@ workspace/
|
||||
- [OpenClaw](https://openclaw.ai)
|
||||
- Node.js >= 20
|
||||
- [`glab`](https://gitlab.com/gitlab-org/cli) CLI installed and authenticated (GitLab provider), or [`gh`](https://cli.github.com) CLI (GitHub provider)
|
||||
- A `memory/projects.json` in the orchestrator agent's workspace
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -8,28 +8,28 @@ Understanding the OpenClaw model is key to understanding how DevClaw works:
|
||||
- **Session** — A runtime conversation instance. Each session has its own context window and conversation history, stored as a `.jsonl` transcript file.
|
||||
- **Sub-agent session** — A session created under the orchestrator agent for a specific worker role. NOT a separate agent — it's a child session running under the same agent, with its own isolated context. Format: `agent:<parent>:subagent:<uuid>`.
|
||||
|
||||
### Session-per-model design
|
||||
### Session-per-tier design
|
||||
|
||||
Each project maintains **separate sessions per model per role**. A project's DEV might have a Haiku session, a Sonnet session, and an Opus session — each accumulating its own codebase context over time.
|
||||
Each project maintains **separate sessions per developer tier per role**. A project's DEV might have a junior session, a medior session, and a senior session — each accumulating its own codebase context over time.
|
||||
|
||||
```
|
||||
Orchestrator Agent (configured in openclaw.json)
|
||||
└─ Main session (long-lived, handles all projects)
|
||||
│
|
||||
├─ Project A
|
||||
│ ├─ DEV sessions: { haiku: <uuid>, sonnet: <uuid>, opus: null }
|
||||
│ └─ QA sessions: { grok: <uuid> }
|
||||
│ ├─ DEV sessions: { junior: <uuid>, medior: <uuid>, senior: null }
|
||||
│ └─ QA sessions: { qa: <uuid> }
|
||||
│
|
||||
└─ Project B
|
||||
├─ DEV sessions: { haiku: null, sonnet: <uuid>, opus: null }
|
||||
└─ QA sessions: { grok: <uuid> }
|
||||
├─ DEV sessions: { junior: null, medior: <uuid>, senior: null }
|
||||
└─ QA sessions: { qa: <uuid> }
|
||||
```
|
||||
|
||||
Why per-model instead of switching models on one session:
|
||||
Why per-tier instead of switching models on one session:
|
||||
- **No model switching overhead** — each session always uses the same model
|
||||
- **Accumulated context** — a Haiku session that's done 20 typo fixes knows the project well; a Sonnet session that's done 5 features knows it differently
|
||||
- **Accumulated context** — a junior session that's done 20 typo fixes knows the project well; a medior session that's done 5 features knows it differently
|
||||
- **No cross-model confusion** — conversation history stays with the model that generated it
|
||||
- **Deterministic reuse** — model selection directly maps to a session key, no patching needed
|
||||
- **Deterministic reuse** — tier selection directly maps to a session key, no patching needed
|
||||
|
||||
### Plugin-controlled session lifecycle
|
||||
|
||||
@@ -37,14 +37,14 @@ DevClaw controls the **full** session lifecycle end-to-end. The orchestrator age
|
||||
|
||||
```
|
||||
Plugin dispatch (inside task_pickup):
|
||||
1. Select model, look up session, decide spawn vs send
|
||||
1. Assign tier, look up session, decide spawn vs send
|
||||
2. New session: openclaw gateway call sessions.patch → create entry + set model
|
||||
openclaw agent --session-id <key> --message "task..."
|
||||
3. Existing: openclaw agent --session-id <key> --message "task..."
|
||||
4. Return result to orchestrator (announcement text, no session instructions)
|
||||
```
|
||||
|
||||
The agent's only job after `task_pickup` returns is to post the announcement to Telegram. Everything else — model selection, session creation, task dispatch, state update, audit logging — is deterministic plugin code.
|
||||
The agent's only job after `task_pickup` returns is to post the announcement to Telegram. Everything else — tier assignment, session creation, task dispatch, state update, audit logging — is deterministic plugin code.
|
||||
|
||||
**Why this matters:** Previously the plugin returned instructions like `{ sessionAction: "spawn", model: "sonnet" }` and the agent had to correctly call `sessions_spawn` with the right params. This was the fragile handoff point where agents would forget `cleanup: "keep"`, use wrong models, or corrupt session state. Moving dispatch into the plugin eliminates that entire class of errors.
|
||||
|
||||
@@ -75,10 +75,10 @@ graph TB
|
||||
MS[Main Session<br/>orchestrator agent]
|
||||
GW[Gateway RPC<br/>sessions.patch / sessions.list]
|
||||
CLI[openclaw agent CLI]
|
||||
DEV_H[DEV session<br/>haiku]
|
||||
DEV_S[DEV session<br/>sonnet]
|
||||
DEV_O[DEV session<br/>opus]
|
||||
QA_G[QA session<br/>grok]
|
||||
DEV_J[DEV session<br/>junior]
|
||||
DEV_M[DEV session<br/>medior]
|
||||
DEV_S[DEV session<br/>senior]
|
||||
QA_E[QA session<br/>qa]
|
||||
end
|
||||
|
||||
subgraph "DevClaw Plugin"
|
||||
@@ -88,13 +88,14 @@ graph TB
|
||||
QS[queue_status]
|
||||
SH[session_health]
|
||||
PR[project_register]
|
||||
MS_SEL[Model Selector]
|
||||
DS[devclaw_setup]
|
||||
TIER[Tier Resolver]
|
||||
PJ[projects.json]
|
||||
AL[audit.log]
|
||||
end
|
||||
|
||||
subgraph "External"
|
||||
GL[GitLab]
|
||||
GL[Issue Tracker]
|
||||
REPO[Git Repository]
|
||||
end
|
||||
|
||||
@@ -108,8 +109,9 @@ graph TB
|
||||
MS -->|calls| QS
|
||||
MS -->|calls| SH
|
||||
MS -->|calls| PR
|
||||
MS -->|calls| DS
|
||||
|
||||
TP -->|selects model| MS_SEL
|
||||
TP -->|resolves tier| TIER
|
||||
TP -->|transitions labels| GL
|
||||
TP -->|reads/writes| PJ
|
||||
TP -->|appends| AL
|
||||
@@ -139,15 +141,15 @@ graph TB
|
||||
PR -->|writes entry| PJ
|
||||
PR -->|appends| AL
|
||||
|
||||
CLI -->|sends task| DEV_H
|
||||
CLI -->|sends task| DEV_J
|
||||
CLI -->|sends task| DEV_M
|
||||
CLI -->|sends task| DEV_S
|
||||
CLI -->|sends task| DEV_O
|
||||
CLI -->|sends task| QA_G
|
||||
CLI -->|sends task| QA_E
|
||||
|
||||
DEV_H -->|writes code, creates MRs| REPO
|
||||
DEV_J -->|writes code, creates MRs| REPO
|
||||
DEV_M -->|writes code, creates MRs| REPO
|
||||
DEV_S -->|writes code, creates MRs| REPO
|
||||
DEV_O -->|writes code, creates MRs| REPO
|
||||
QA_G -->|reviews code, tests| REPO
|
||||
QA_E -->|reviews code, tests| REPO
|
||||
```
|
||||
|
||||
## End-to-end flow: human to sub-agent
|
||||
@@ -162,8 +164,8 @@ sequenceDiagram
|
||||
participant DC as DevClaw Plugin
|
||||
participant GW as Gateway RPC
|
||||
participant CLI as openclaw agent CLI
|
||||
participant DEV as DEV Session<br/>(sonnet)
|
||||
participant GL as GitLab
|
||||
participant DEV as DEV Session<br/>(medior)
|
||||
participant GL as Issue Tracker
|
||||
|
||||
Note over H,GL: Issue exists in queue (To Do)
|
||||
|
||||
@@ -173,49 +175,49 @@ sequenceDiagram
|
||||
DC->>GL: glab issue list --label "To Do"
|
||||
DC-->>MS: { toDo: [#42], dev: idle }
|
||||
|
||||
Note over MS: Decides to pick up #42 for DEV
|
||||
Note over MS: Decides to pick up #42 for DEV as medior
|
||||
|
||||
MS->>DC: task_pickup({ issueId: 42, role: "dev", ... })
|
||||
DC->>DC: selectModel → "sonnet"
|
||||
DC->>DC: lookup dev.sessions.sonnet → null (first time)
|
||||
MS->>DC: task_pickup({ issueId: 42, role: "dev", model: "medior", ... })
|
||||
DC->>DC: resolve tier "medior" → model ID
|
||||
DC->>DC: lookup dev.sessions.medior → null (first time)
|
||||
DC->>GL: glab issue update 42 --unlabel "To Do" --label "Doing"
|
||||
DC->>GW: sessions.patch({ key: new-session-key, model: "sonnet" })
|
||||
DC->>GW: sessions.patch({ key: new-session-key, model: "anthropic/claude-sonnet-4-5" })
|
||||
DC->>CLI: openclaw agent --session-id <key> --message "Build login page for #42..."
|
||||
CLI->>DEV: creates session, delivers task
|
||||
DC->>DC: store session key in projects.json + append audit.log
|
||||
DC-->>MS: { success: true, announcement: "🔧 DEV (sonnet) picking up #42" }
|
||||
DC-->>MS: { success: true, announcement: "🔧 DEV (medior) picking up #42" }
|
||||
|
||||
MS->>TG: "🔧 DEV (sonnet) picking up #42: Add login page"
|
||||
MS->>TG: "🔧 DEV (medior) picking up #42: Add login page"
|
||||
TG->>H: sees announcement
|
||||
|
||||
Note over DEV: Works autonomously — reads code, writes code, creates MR
|
||||
Note over MS: Heartbeat detects DEV session idle → triggers task_complete
|
||||
Note over DEV: Calls task_complete when done
|
||||
|
||||
MS->>DC: task_complete({ role: "dev", result: "done", ... })
|
||||
DEV->>DC: task_complete({ role: "dev", result: "done", ... })
|
||||
DC->>GL: glab issue update 42 --unlabel "Doing" --label "To Test"
|
||||
DC->>DC: deactivate worker (sessions preserved)
|
||||
DC-->>MS: { announcement: "✅ DEV done #42" }
|
||||
DC-->>DEV: { announcement: "✅ DEV done #42" }
|
||||
|
||||
MS->>TG: "✅ DEV done #42 — moved to QA queue"
|
||||
TG->>H: sees announcement
|
||||
```
|
||||
|
||||
On the **next DEV task** for this project that also selects Sonnet:
|
||||
On the **next DEV task** for this project that also assigns medior:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant MS as Main Session
|
||||
participant DC as DevClaw Plugin
|
||||
participant CLI as openclaw agent CLI
|
||||
participant DEV as DEV Session<br/>(sonnet, existing)
|
||||
participant DEV as DEV Session<br/>(medior, existing)
|
||||
|
||||
MS->>DC: task_pickup({ issueId: 57, role: "dev", ... })
|
||||
DC->>DC: selectModel → "sonnet"
|
||||
DC->>DC: lookup dev.sessions.sonnet → existing key!
|
||||
MS->>DC: task_pickup({ issueId: 57, role: "dev", model: "medior", ... })
|
||||
DC->>DC: resolve tier "medior" → model ID
|
||||
DC->>DC: lookup dev.sessions.medior → existing key!
|
||||
Note over DC: No sessions.patch needed — session already exists
|
||||
DC->>CLI: openclaw agent --session-id <key> --message "Fix validation for #57..."
|
||||
CLI->>DEV: delivers task to existing session (has full codebase context)
|
||||
DC-->>MS: { success: true, announcement: "⚡ DEV (sonnet) picking up #57" }
|
||||
DC-->>MS: { success: true, announcement: "⚡ DEV (medior) picking up #57" }
|
||||
```
|
||||
|
||||
Session reuse saves ~50K tokens per task by not re-reading the codebase.
|
||||
@@ -229,10 +231,10 @@ This traces a single issue from creation to completion, showing every component
|
||||
Issues are created by the orchestrator agent or by sub-agent sessions via `glab`. The orchestrator can create issues based on user requests in Telegram, backlog planning, or QA feedback. Sub-agents can also create issues when they discover bugs or related work during development.
|
||||
|
||||
```
|
||||
Orchestrator Agent → GitLab: creates issue #42 with label "To Do"
|
||||
Orchestrator Agent → Issue Tracker: creates issue #42 with label "To Do"
|
||||
```
|
||||
|
||||
**State:** GitLab has issue #42 labeled "To Do". Nothing in DevClaw yet.
|
||||
**State:** Issue tracker has issue #42 labeled "To Do". Nothing in DevClaw yet.
|
||||
|
||||
### Phase 2: Heartbeat detects work
|
||||
|
||||
@@ -244,7 +246,7 @@ Heartbeat triggers → Orchestrator calls queue_status()
|
||||
sequenceDiagram
|
||||
participant A as Orchestrator
|
||||
participant QS as queue_status
|
||||
participant GL as GitLab
|
||||
participant GL as Issue Tracker
|
||||
participant PJ as projects.json
|
||||
participant AL as audit.log
|
||||
|
||||
@@ -261,33 +263,33 @@ sequenceDiagram
|
||||
QS-->>A: { dev: idle, queue: { toDo: [#42] } }
|
||||
```
|
||||
|
||||
**Orchestrator decides:** DEV is idle, issue #42 is in To Do → pick it up.
|
||||
**Orchestrator decides:** DEV is idle, issue #42 is in To Do → pick it up. Evaluates complexity → assigns medior tier.
|
||||
|
||||
### Phase 3: DEV pickup
|
||||
|
||||
The plugin handles everything end-to-end — model selection, session lookup, label transition, state update, **and** task dispatch to the worker session. The agent's only job after is to post the announcement.
|
||||
The plugin handles everything end-to-end — tier resolution, session lookup, label transition, state update, **and** task dispatch to the worker session. The agent's only job after is to post the announcement.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Orchestrator
|
||||
participant TP as task_pickup
|
||||
participant GL as GitLab
|
||||
participant MS as Model Selector
|
||||
participant GL as Issue Tracker
|
||||
participant TIER as Tier Resolver
|
||||
participant GW as Gateway RPC
|
||||
participant CLI as openclaw agent CLI
|
||||
participant PJ as projects.json
|
||||
participant AL as audit.log
|
||||
|
||||
A->>TP: task_pickup({ issueId: 42, role: "dev", projectGroupId: "-123" })
|
||||
A->>TP: task_pickup({ issueId: 42, role: "dev", projectGroupId: "-123", model: "medior" })
|
||||
TP->>PJ: readProjects()
|
||||
TP->>GL: glab issue view 42 --output json
|
||||
GL-->>TP: { title: "Add login page", labels: ["To Do"] }
|
||||
TP->>TP: Verify label is "To Do" ✓
|
||||
TP->>TP: model from agent param (LLM-selected) or fallback heuristic
|
||||
TP->>PJ: lookup dev.sessions.sonnet
|
||||
TP->>TIER: resolve "medior" → "anthropic/claude-sonnet-4-5"
|
||||
TP->>PJ: lookup dev.sessions.medior
|
||||
TP->>GL: glab issue update 42 --unlabel "To Do" --label "Doing"
|
||||
alt New session
|
||||
TP->>GW: sessions.patch({ key: new-key, model: "sonnet" })
|
||||
TP->>GW: sessions.patch({ key: new-key, model: "anthropic/claude-sonnet-4-5" })
|
||||
end
|
||||
TP->>CLI: openclaw agent --session-id <key> --message "task..."
|
||||
TP->>PJ: activateWorker + store session key
|
||||
@@ -296,8 +298,8 @@ sequenceDiagram
|
||||
```
|
||||
|
||||
**Writes:**
|
||||
- `GitLab`: label "To Do" → "Doing"
|
||||
- `projects.json`: dev.active=true, dev.issueId="42", dev.model="sonnet", dev.sessions.sonnet=key
|
||||
- `Issue Tracker`: label "To Do" → "Doing"
|
||||
- `projects.json`: dev.active=true, dev.issueId="42", dev.model="medior", dev.sessions.medior=key
|
||||
- `audit.log`: 2 entries (task_pickup, model_selection)
|
||||
- `Session`: task message delivered to worker session via CLI
|
||||
|
||||
@@ -316,7 +318,7 @@ This happens inside the OpenClaw session. The worker calls `task_complete` direc
|
||||
sequenceDiagram
|
||||
participant DEV as DEV Session
|
||||
participant TC as task_complete
|
||||
participant GL as GitLab
|
||||
participant GL as Issue Tracker
|
||||
participant PJ as projects.json
|
||||
participant AL as audit.log
|
||||
participant REPO as Git Repo
|
||||
@@ -333,7 +335,7 @@ sequenceDiagram
|
||||
|
||||
alt autoChain enabled
|
||||
TC->>GL: transition label "To Test" → "Testing"
|
||||
TC->>QA: dispatchTask(role: "qa", model: "grok")
|
||||
TC->>QA: dispatchTask(role: "qa", tier: "qa")
|
||||
TC->>PJ: activateWorker(-123, qa)
|
||||
TC-->>DEV: { announcement: "✅ DEV done #42", autoChain: { dispatched: true, role: "qa" } }
|
||||
else autoChain disabled
|
||||
@@ -344,12 +346,12 @@ sequenceDiagram
|
||||
**Writes:**
|
||||
- `Git repo`: pulled latest (has DEV's merged code)
|
||||
- `projects.json`: dev.active=false, dev.issueId=null (sessions map preserved for reuse)
|
||||
- `GitLab`: label "Doing" → "To Test" (+ "To Test" → "Testing" if auto-chain)
|
||||
- `Issue Tracker`: label "Doing" → "To Test" (+ "To Test" → "Testing" if auto-chain)
|
||||
- `audit.log`: 1 entry (task_complete) + optional auto-chain entries
|
||||
|
||||
### Phase 6: QA pickup
|
||||
|
||||
Same as Phase 3, but with `role: "qa"`. Label transitions "To Test" → "Testing". Model defaults to Grok for QA.
|
||||
Same as Phase 3, but with `role: "qa"`. Label transitions "To Test" → "Testing". Uses the qa tier.
|
||||
|
||||
### Phase 7: QA result (3 possible outcomes)
|
||||
|
||||
@@ -359,7 +361,7 @@ Same as Phase 3, but with `role: "qa"`. Label transitions "To Test" → "Testing
|
||||
sequenceDiagram
|
||||
participant A as Orchestrator
|
||||
participant TC as task_complete
|
||||
participant GL as GitLab
|
||||
participant GL as Issue Tracker
|
||||
participant PJ as projects.json
|
||||
participant AL as audit.log
|
||||
|
||||
@@ -379,8 +381,7 @@ sequenceDiagram
|
||||
sequenceDiagram
|
||||
participant A as Orchestrator
|
||||
participant TC as task_complete
|
||||
participant GL as GitLab
|
||||
participant MS as Model Selector
|
||||
participant GL as Issue Tracker
|
||||
participant PJ as projects.json
|
||||
participant AL as audit.log
|
||||
|
||||
@@ -421,12 +422,12 @@ sequenceDiagram
|
||||
A->>QS: queue_status()
|
||||
QS-->>A: { projects: [{ dev: idle, queue: { toDo: [#43], toTest: [#44] } }] }
|
||||
|
||||
Note over A: DEV idle + To Do #43 → pick up
|
||||
A->>TP: task_pickup({ issueId: 43, role: "dev", ... })
|
||||
Note over TP: Plugin handles everything:<br/>model select → session lookup →<br/>label transition → dispatch task →<br/>state update → audit log
|
||||
Note over A: DEV idle + To Do #43 → assign medior
|
||||
A->>TP: task_pickup({ issueId: 43, role: "dev", model: "medior", ... })
|
||||
Note over TP: Plugin handles everything:<br/>tier resolve → session lookup →<br/>label transition → dispatch task →<br/>state update → audit log
|
||||
|
||||
Note over A: QA idle + To Test #44 → pick up
|
||||
A->>TP: task_pickup({ issueId: 44, role: "qa", ... })
|
||||
Note over A: QA idle + To Test #44 → assign qa
|
||||
A->>TP: task_pickup({ issueId: 44, role: "qa", model: "qa", ... })
|
||||
```
|
||||
|
||||
## Data flow map
|
||||
@@ -447,7 +448,8 @@ Every piece of data and where it lives:
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ DevClaw Plugin (orchestration logic) │
|
||||
│ │
|
||||
│ task_pickup → model + label + dispatch + role instr (e2e) │
|
||||
│ devclaw_setup → agent creation + workspace + model config │
|
||||
│ task_pickup → tier + label + dispatch + role instr (e2e) │
|
||||
│ task_complete → label + state + git pull + auto-chain │
|
||||
│ task_create → create issue in tracker │
|
||||
│ queue_status → read labels + read state │
|
||||
@@ -462,13 +464,13 @@ Every piece of data and where it lives:
|
||||
│ dev: │ │ openclaw gateway call │
|
||||
│ active, issueId, model │ │ sessions.patch → create │
|
||||
│ sessions: │ │ sessions.list → health │
|
||||
│ haiku: <key> │ │ sessions.delete → cleanup │
|
||||
│ sonnet: <key> │ │ │
|
||||
│ opus: <key> │ │ openclaw agent │
|
||||
│ junior: <key> │ │ sessions.delete → cleanup │
|
||||
│ medior: <key> │ │ │
|
||||
│ senior: <key> │ │ openclaw agent │
|
||||
│ qa: │ │ --session-id <key> │
|
||||
│ active, issueId, model │ │ --message "task..." │
|
||||
│ sessions: │ │ → dispatches to session │
|
||||
│ grok: <key> │ │ │
|
||||
│ qa: <key> │ │ │
|
||||
└────────────────────────────────┘ └──────────────────────────────┘
|
||||
↕ append-only
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
@@ -477,7 +479,7 @@ Every piece of data and where it lives:
|
||||
│ NDJSON, one line per event: │
|
||||
│ task_pickup, task_complete, model_selection, │
|
||||
│ queue_status, health_check, session_spawn, session_reuse, │
|
||||
│ project_register │
|
||||
│ project_register, devclaw_setup │
|
||||
│ │
|
||||
│ Query with: cat audit.log | jq 'select(.event=="task_pickup")' │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
@@ -486,8 +488,8 @@ Every piece of data and where it lives:
|
||||
│ Telegram (user-facing messages) │
|
||||
│ │
|
||||
│ Per group chat: │
|
||||
│ "🔧 Spawning DEV (sonnet) for #42: Add login page" │
|
||||
│ "⚡ Sending DEV (sonnet) for #57: Fix validation" │
|
||||
│ "🔧 Spawning DEV (medior) for #42: Add login page" │
|
||||
│ "⚡ Sending DEV (medior) for #57: Fix validation" │
|
||||
│ "✅ DEV done #42 — Login page with OAuth. Moved to QA queue."│
|
||||
│ "🎉 QA PASS #42. Issue closed." │
|
||||
│ "❌ QA FAIL #42 — OAuth redirect broken. Sent back to DEV." │
|
||||
@@ -512,6 +514,7 @@ graph LR
|
||||
L[Label transitions]
|
||||
S[Worker state]
|
||||
PR[Project registration]
|
||||
SETUP[Agent + workspace setup]
|
||||
SD[Session dispatch<br/>create + send via CLI]
|
||||
AC[Auto-chaining<br/>DEV→QA, QA fail→DEV]
|
||||
RI[Role instructions<br/>loaded per project]
|
||||
@@ -523,7 +526,7 @@ graph LR
|
||||
MSG[Telegram announcements]
|
||||
HB[Heartbeat scheduling]
|
||||
DEC[Task prioritization]
|
||||
M[Model selection]
|
||||
M[Developer assignment<br/>junior/medior/senior]
|
||||
end
|
||||
|
||||
subgraph "Sub-agent sessions handle"
|
||||
@@ -565,7 +568,7 @@ Provider selection is handled by `createProvider()` in `lib/providers/index.ts`.
|
||||
|
||||
| Failure | Detection | Recovery |
|
||||
|---|---|---|
|
||||
| Session dies mid-task | `session_health` checks via `sessions.list` Gateway RPC | `autoFix`: reverts label, clears active state, removes dead session from sessions map. Next heartbeat picks up task again (creates fresh session for that model). |
|
||||
| Session dies mid-task | `session_health` checks via `sessions.list` Gateway RPC | `autoFix`: reverts label, clears active state, removes dead session from sessions map. Next heartbeat picks up task again (creates fresh session for that tier). |
|
||||
| glab command fails | Plugin tool throws error, returns to agent | Agent retries or reports to Telegram group |
|
||||
| `openclaw agent` CLI fails | Plugin catches error during dispatch | Plugin rolls back: reverts label, clears active state. Returns error to agent for reporting. |
|
||||
| `sessions.patch` fails | Plugin catches error during session creation | Plugin rolls back label transition. Returns error. No orphaned state. |
|
||||
@@ -581,7 +584,7 @@ Provider selection is handled by `createProvider()` in `lib/providers/index.ts`.
|
||||
|---|---|---|
|
||||
| Plugin source | `~/.openclaw/extensions/devclaw/` | Plugin code |
|
||||
| Plugin manifest | `~/.openclaw/extensions/devclaw/openclaw.plugin.json` | Plugin registration |
|
||||
| Agent config | `~/.openclaw/openclaw.json` | Agent definition + tool permissions |
|
||||
| Agent config | `~/.openclaw/openclaw.json` | Agent definition + tool permissions + tier config |
|
||||
| Worker state | `~/.openclaw/workspace-<agent>/memory/projects.json` | Per-project DEV/QA state |
|
||||
| Audit log | `~/.openclaw/workspace-<agent>/memory/audit.log` | NDJSON event log |
|
||||
| Session transcripts | `~/.openclaw/agents/<agent>/sessions/<uuid>.jsonl` | Conversation history per session |
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
| [`glab`](https://gitlab.com/gitlab-org/cli) or [`gh`](https://cli.github.com) CLI | Issue tracker provider (auto-detected from remote) | `glab --version` or `gh --version` |
|
||||
| CLI authenticated | Plugin calls glab/gh for every label transition | `glab auth status` or `gh auth status` |
|
||||
| A GitLab/GitHub repo with issues | The task backlog lives in the issue tracker | `glab issue list` or `gh issue list` from your repo |
|
||||
| An OpenClaw agent with Telegram | The orchestrator agent that will manage projects | Agent defined in `openclaw.json` |
|
||||
|
||||
## Setup steps
|
||||
## Setup
|
||||
|
||||
### 1. Install the plugin
|
||||
|
||||
@@ -26,35 +25,38 @@ openclaw plugins list
|
||||
# Should show: DevClaw | devclaw | loaded
|
||||
```
|
||||
|
||||
### 2. Configure your orchestrator agent
|
||||
### 2. Run setup
|
||||
|
||||
In `openclaw.json`, your orchestrator agent needs access to the DevClaw tools:
|
||||
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"list": [{
|
||||
"id": "my-orchestrator",
|
||||
"name": "Dev Orchestrator",
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"tools": {
|
||||
"allow": [
|
||||
"task_pickup",
|
||||
"task_complete",
|
||||
"task_create",
|
||||
"queue_status",
|
||||
"session_health",
|
||||
"project_register"
|
||||
]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```bash
|
||||
openclaw devclaw setup
|
||||
```
|
||||
|
||||
The agent needs the six DevClaw tools. Session management (`sessions_spawn`, `sessions_send`) is **not needed** — the plugin handles session creation and task dispatch internally via OpenClaw CLI. Workers (DEV/QA sub-agent sessions) also use `task_complete` and `task_create` directly for atomic self-reporting.
|
||||
The setup wizard walks you through:
|
||||
|
||||
### 3. Register your project
|
||||
1. **Agent** — Create a new orchestrator agent or configure an existing one
|
||||
2. **Developer team** — Choose which LLM model powers each developer tier:
|
||||
- **Junior** (fast, cheap tasks) — default: `anthropic/claude-haiku-4-5`
|
||||
- **Medior** (standard tasks) — default: `anthropic/claude-sonnet-4-5`
|
||||
- **Senior** (complex tasks) — default: `anthropic/claude-opus-4-5`
|
||||
- **QA** (code review) — default: `anthropic/claude-sonnet-4-5`
|
||||
3. **Workspace** — Writes AGENTS.md, HEARTBEAT.md, role templates, and initializes memory
|
||||
|
||||
Non-interactive mode:
|
||||
```bash
|
||||
# Create new agent with default models
|
||||
openclaw devclaw setup --new-agent "My Dev Orchestrator" --non-interactive
|
||||
|
||||
# Configure existing agent with custom models
|
||||
openclaw devclaw setup --agent my-orchestrator \
|
||||
--junior "anthropic/claude-haiku-4-5" \
|
||||
--senior "anthropic/claude-opus-4-5"
|
||||
```
|
||||
|
||||
### 3. Add the agent to the Telegram group
|
||||
|
||||
Add your orchestrator bot to the Telegram group for the project. The agent will now receive messages from this group and can operate on the linked project.
|
||||
|
||||
### 4. Register your project
|
||||
|
||||
Tell the orchestrator agent to register a new project:
|
||||
|
||||
@@ -83,14 +85,14 @@ The agent calls `project_register`, which atomically:
|
||||
"issueId": null,
|
||||
"startTime": null,
|
||||
"model": null,
|
||||
"sessions": { "haiku": null, "sonnet": null, "opus": null }
|
||||
"sessions": { "junior": null, "medior": null, "senior": null }
|
||||
},
|
||||
"qa": {
|
||||
"active": false,
|
||||
"issueId": null,
|
||||
"startTime": null,
|
||||
"model": null,
|
||||
"sessions": { "grok": null }
|
||||
"sessions": { "qa": null }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,10 +103,6 @@ The agent calls `project_register`, which atomically:
|
||||
|
||||
**Finding the Telegram group ID:** The group ID is the numeric ID of your Telegram supergroup (a negative number like `-1234567890`). You can find it via the Telegram bot API or from message metadata in OpenClaw logs.
|
||||
|
||||
### 4. Add the agent to the Telegram group
|
||||
|
||||
Add your orchestrator bot to the Telegram group for the project. The agent will now receive messages from this group and can operate on the linked project.
|
||||
|
||||
### 5. Create your first issue
|
||||
|
||||
Issues can be created in multiple ways:
|
||||
@@ -123,7 +121,7 @@ The agent should call `queue_status` and report the "To Do" issue. Then:
|
||||
|
||||
> "Pick up issue #1 for DEV"
|
||||
|
||||
The agent calls `task_pickup`, which selects a model, transitions the label to "Doing", creates or reuses a worker session, and dispatches the task — all in one call. The agent just posts the announcement.
|
||||
The agent calls `task_pickup`, which assigns a developer tier, transitions the label to "Doing", creates or reuses a worker session, and dispatches the task — all in one call. The agent just posts the announcement.
|
||||
|
||||
## Adding more projects
|
||||
|
||||
@@ -131,18 +129,50 @@ Tell the agent to register a new project (step 3) and add the bot to the new Tel
|
||||
|
||||
Each project is fully isolated — separate queue, separate workers, separate state.
|
||||
|
||||
## Developer tiers
|
||||
|
||||
DevClaw assigns tasks to developer tiers instead of raw model names. This makes the system intuitive — you're assigning a "junior dev" to fix a typo, not configuring model parameters.
|
||||
|
||||
| Tier | Role | Default model | When to assign |
|
||||
|------|------|---------------|----------------|
|
||||
| **junior** | Junior developer | `anthropic/claude-haiku-4-5` | Typos, single-file fixes, CSS changes |
|
||||
| **medior** | Mid-level developer | `anthropic/claude-sonnet-4-5` | Features, bug fixes, multi-file changes |
|
||||
| **senior** | Senior developer | `anthropic/claude-opus-4-5` | Architecture, migrations, system-wide refactoring |
|
||||
| **qa** | QA engineer | `anthropic/claude-sonnet-4-5` | Code review, test validation |
|
||||
|
||||
Change which model powers each tier in `openclaw.json`:
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"devclaw": {
|
||||
"config": {
|
||||
"models": {
|
||||
"junior": "anthropic/claude-haiku-4-5",
|
||||
"medior": "anthropic/claude-sonnet-4-5",
|
||||
"senior": "anthropic/claude-opus-4-5",
|
||||
"qa": "anthropic/claude-sonnet-4-5"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What the plugin handles vs. what you handle
|
||||
|
||||
| Responsibility | Who | Details |
|
||||
|---|---|---|
|
||||
| Plugin installation | You (once) | `cp -r devclaw ~/.openclaw/extensions/` |
|
||||
| Agent + workspace setup | Plugin (`devclaw setup`) | Creates agent, configures models, writes workspace files |
|
||||
| Label setup | Plugin (`project_register`) | 8 labels, created idempotently via `IssueProvider` |
|
||||
| Role file scaffolding | Plugin (`project_register`) | Creates `roles/<project>/dev.md` and `qa.md` from defaults |
|
||||
| Project registration | Plugin (`project_register`) | Entry in `projects.json` with empty worker state |
|
||||
| Agent definition | You (once) | Agent in `openclaw.json` with tool permissions |
|
||||
| Telegram group setup | You (once per project) | Add bot to group |
|
||||
| Issue creation | Plugin (`task_create`) | Orchestrator or workers create issues from chat |
|
||||
| Label transitions | Plugin | Atomic label transitions via issue tracker CLI |
|
||||
| Model selection | Plugin | LLM-selected by orchestrator, keyword heuristic fallback |
|
||||
| Developer assignment | Plugin | LLM-selected tier by orchestrator, keyword heuristic fallback |
|
||||
| State management | Plugin | Atomic read/write to `projects.json` |
|
||||
| Session management | Plugin | Creates, reuses, and dispatches to sessions via CLI. Agent never touches session tools. |
|
||||
| Task completion | Plugin (`task_complete`) | Workers self-report. Auto-chains if enabled. |
|
||||
|
||||
41
index.ts
41
index.ts
@@ -5,13 +5,37 @@ import { createQueueStatusTool } from "./lib/tools/queue-status.js";
|
||||
import { createSessionHealthTool } from "./lib/tools/session-health.js";
|
||||
import { createProjectRegisterTool } from "./lib/tools/project-register.js";
|
||||
import { createTaskCreateTool } from "./lib/tools/task-create.js";
|
||||
import { createSetupTool } from "./lib/tools/devclaw-setup.js";
|
||||
import { runCli } from "./lib/cli.js";
|
||||
|
||||
const plugin = {
|
||||
id: "devclaw",
|
||||
name: "DevClaw",
|
||||
description:
|
||||
"Multi-project dev/qa pipeline orchestration with GitHub/GitLab integration, model selection, and audit logging.",
|
||||
configSchema: {},
|
||||
"Multi-project dev/qa pipeline orchestration with GitHub/GitLab integration, developer tiers, and audit logging.",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
models: {
|
||||
type: "object",
|
||||
description: "Model mapping per developer tier",
|
||||
properties: {
|
||||
junior: { type: "string", description: "Junior dev model" },
|
||||
medior: { type: "string", description: "Medior dev model" },
|
||||
senior: { type: "string", description: "Senior dev model" },
|
||||
qa: { type: "string", description: "QA engineer model" },
|
||||
},
|
||||
},
|
||||
glabPath: {
|
||||
type: "string",
|
||||
description: "Path to glab CLI binary. Defaults to 'glab' on PATH.",
|
||||
},
|
||||
ghPath: {
|
||||
type: "string",
|
||||
description: "Path to gh CLI binary. Defaults to 'gh' on PATH.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
register(api: OpenClawPluginApi) {
|
||||
// Agent tools (primary interface — agent calls these directly)
|
||||
@@ -33,8 +57,19 @@ const plugin = {
|
||||
api.registerTool(createTaskCreateTool(api), {
|
||||
names: ["task_create"],
|
||||
});
|
||||
api.registerTool(createSetupTool(api), {
|
||||
names: ["devclaw_setup"],
|
||||
});
|
||||
|
||||
api.logger.info("DevClaw plugin registered (6 tools)");
|
||||
// CLI commands
|
||||
api.registerCli("setup", {
|
||||
description: "Set up DevClaw: create agent, configure models, write workspace files",
|
||||
run: async (argv: string[]) => {
|
||||
await runCli(argv);
|
||||
},
|
||||
});
|
||||
|
||||
api.logger.info("DevClaw plugin registered (7 tools, 1 CLI command)");
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
208
lib/cli.ts
Normal file
208
lib/cli.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* cli.ts — CLI command for `openclaw devclaw setup`.
|
||||
*
|
||||
* Interactive and non-interactive modes for onboarding.
|
||||
*/
|
||||
import { createInterface } from "node:readline";
|
||||
import { runSetup, type SetupOpts } from "./setup.js";
|
||||
import { ALL_TIERS, DEFAULT_MODELS, type Tier } from "./tiers.js";
|
||||
|
||||
type CliArgs = {
|
||||
/** Create a new agent */
|
||||
newAgent?: string;
|
||||
/** Use existing agent */
|
||||
agent?: string;
|
||||
/** Direct workspace path */
|
||||
workspace?: string;
|
||||
/** Model overrides */
|
||||
junior?: string;
|
||||
medior?: string;
|
||||
senior?: string;
|
||||
qa?: string;
|
||||
/** Skip prompts */
|
||||
nonInteractive?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse CLI arguments from argv-style array.
|
||||
* Expects: ["setup", "--new-agent", "name", "--junior", "model", ...]
|
||||
*/
|
||||
export function parseArgs(argv: string[]): CliArgs {
|
||||
const args: CliArgs = {};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
const next = argv[i + 1];
|
||||
switch (arg) {
|
||||
case "--new-agent":
|
||||
args.newAgent = next;
|
||||
i++;
|
||||
break;
|
||||
case "--agent":
|
||||
args.agent = next;
|
||||
i++;
|
||||
break;
|
||||
case "--workspace":
|
||||
args.workspace = next;
|
||||
i++;
|
||||
break;
|
||||
case "--junior":
|
||||
args.junior = next;
|
||||
i++;
|
||||
break;
|
||||
case "--medior":
|
||||
args.medior = next;
|
||||
i++;
|
||||
break;
|
||||
case "--senior":
|
||||
args.senior = next;
|
||||
i++;
|
||||
break;
|
||||
case "--qa":
|
||||
args.qa = next;
|
||||
i++;
|
||||
break;
|
||||
case "--non-interactive":
|
||||
args.nonInteractive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the interactive setup wizard.
|
||||
*/
|
||||
async function interactiveSetup(): Promise<SetupOpts> {
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const ask = (question: string): Promise<string> =>
|
||||
new Promise((resolve) => rl.question(question, resolve));
|
||||
|
||||
console.log("");
|
||||
console.log("DevClaw Setup");
|
||||
console.log("=============");
|
||||
console.log("");
|
||||
|
||||
// Step 1: Agent
|
||||
console.log("Step 1: Agent");
|
||||
console.log("─────────────");
|
||||
const agentChoice = await ask(
|
||||
"Create a new agent or use an existing one? [new/existing]: ",
|
||||
);
|
||||
|
||||
let newAgentName: string | undefined;
|
||||
let agentId: string | undefined;
|
||||
|
||||
if (agentChoice.toLowerCase().startsWith("n")) {
|
||||
newAgentName = await ask("Agent name: ");
|
||||
if (!newAgentName.trim()) {
|
||||
rl.close();
|
||||
throw new Error("Agent name cannot be empty");
|
||||
}
|
||||
newAgentName = newAgentName.trim();
|
||||
} else {
|
||||
agentId = await ask("Agent ID: ");
|
||||
if (!agentId.trim()) {
|
||||
rl.close();
|
||||
throw new Error("Agent ID cannot be empty");
|
||||
}
|
||||
agentId = agentId.trim();
|
||||
}
|
||||
|
||||
// Step 2: Models
|
||||
console.log("");
|
||||
console.log("Step 2: Developer Team (models)");
|
||||
console.log("───────────────────────────────");
|
||||
console.log("Press Enter to accept defaults.");
|
||||
console.log("");
|
||||
|
||||
const models: Partial<Record<Tier, string>> = {};
|
||||
for (const tier of ALL_TIERS) {
|
||||
const label =
|
||||
tier === "junior"
|
||||
? "Junior dev (fast, cheap tasks)"
|
||||
: tier === "medior"
|
||||
? "Medior dev (standard tasks)"
|
||||
: tier === "senior"
|
||||
? "Senior dev (complex tasks)"
|
||||
: "QA engineer (code review)";
|
||||
const answer = await ask(` ${label} [${DEFAULT_MODELS[tier]}]: `);
|
||||
if (answer.trim()) {
|
||||
models[tier] = answer.trim();
|
||||
}
|
||||
}
|
||||
|
||||
rl.close();
|
||||
|
||||
console.log("");
|
||||
console.log("Step 3: Workspace");
|
||||
console.log("─────────────────");
|
||||
|
||||
return { newAgentName, agentId, models };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main CLI entry point.
|
||||
*/
|
||||
export async function runCli(argv: string[]): Promise<void> {
|
||||
const args = parseArgs(argv);
|
||||
|
||||
let opts: SetupOpts;
|
||||
|
||||
if (args.nonInteractive || args.newAgent || args.agent || args.workspace) {
|
||||
// Non-interactive mode
|
||||
const models: Partial<Record<Tier, string>> = {};
|
||||
if (args.junior) models.junior = args.junior;
|
||||
if (args.medior) models.medior = args.medior;
|
||||
if (args.senior) models.senior = args.senior;
|
||||
if (args.qa) models.qa = args.qa;
|
||||
|
||||
opts = {
|
||||
newAgentName: args.newAgent,
|
||||
agentId: args.agent,
|
||||
workspacePath: args.workspace,
|
||||
models: Object.keys(models).length > 0 ? models : undefined,
|
||||
};
|
||||
} else {
|
||||
// Interactive mode
|
||||
opts = await interactiveSetup();
|
||||
}
|
||||
|
||||
console.log("");
|
||||
const result = await runSetup(opts);
|
||||
|
||||
// Print results
|
||||
if (result.agentCreated) {
|
||||
console.log(` Agent "${result.agentId}" created`);
|
||||
}
|
||||
|
||||
console.log(` Models configured:`);
|
||||
for (const tier of ALL_TIERS) {
|
||||
console.log(` ${tier}: ${result.models[tier]}`);
|
||||
}
|
||||
|
||||
console.log(` Files written:`);
|
||||
for (const file of result.filesWritten) {
|
||||
console.log(` ${file}`);
|
||||
}
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
console.log("");
|
||||
console.log(" Warnings:");
|
||||
for (const w of result.warnings) {
|
||||
console.log(` ${w}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log("Done! Next steps:");
|
||||
console.log(" 1. Add bot to a Telegram group");
|
||||
console.log(
|
||||
' 2. Register a project: "Register project <name> at <repo> for group <id>"',
|
||||
);
|
||||
console.log(" 3. Create your first issue and pick it up");
|
||||
console.log("");
|
||||
}
|
||||
@@ -18,16 +18,10 @@ import {
|
||||
} from "./projects.js";
|
||||
import { selectModel } from "./model-selector.js";
|
||||
import { log as auditLog } from "./audit.js";
|
||||
import { resolveModel, TIER_EMOJI, isTier } from "./tiers.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const MODEL_MAP: Record<string, string> = {
|
||||
haiku: "anthropic/claude-haiku-4-5",
|
||||
sonnet: "anthropic/claude-sonnet-4-5",
|
||||
opus: "anthropic/claude-opus-4-5",
|
||||
grok: "github-copilot/grok-code-fast-1",
|
||||
};
|
||||
|
||||
export type DispatchOpts = {
|
||||
workspaceDir: string;
|
||||
agentId: string;
|
||||
@@ -38,6 +32,7 @@ export type DispatchOpts = {
|
||||
issueDescription: string;
|
||||
issueUrl: string;
|
||||
role: "dev" | "qa";
|
||||
/** Developer tier (junior, medior, senior, qa) or raw model ID */
|
||||
modelAlias: string;
|
||||
/** Label to transition FROM (e.g. "To Do", "To Test", "To Improve") */
|
||||
fromLabel: string;
|
||||
@@ -45,6 +40,8 @@ export type DispatchOpts = {
|
||||
toLabel: string;
|
||||
/** Function to transition labels (injected to avoid gitlab.ts dependency) */
|
||||
transitionLabel: (issueId: number, from: string, to: string) => Promise<void>;
|
||||
/** Plugin config for model resolution */
|
||||
pluginConfig?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type DispatchResult = {
|
||||
@@ -118,9 +115,10 @@ export async function dispatchTask(opts: DispatchOpts): Promise<DispatchResult>
|
||||
workspaceDir, agentId, groupId, project, issueId,
|
||||
issueTitle, issueDescription, issueUrl,
|
||||
role, modelAlias, fromLabel, toLabel, transitionLabel,
|
||||
pluginConfig,
|
||||
} = opts;
|
||||
|
||||
const fullModel = MODEL_MAP[modelAlias] ?? modelAlias;
|
||||
const fullModel = resolveModel(modelAlias, pluginConfig);
|
||||
const worker = getWorker(project, role);
|
||||
const existingSessionKey = getSessionForModel(worker, modelAlias);
|
||||
const sessionAction = existingSessionKey ? "send" : "spawn";
|
||||
@@ -210,7 +208,7 @@ export async function dispatchTask(opts: DispatchOpts): Promise<DispatchResult>
|
||||
issue: issueId,
|
||||
issueTitle,
|
||||
role,
|
||||
model: modelAlias,
|
||||
tier: modelAlias,
|
||||
sessionAction,
|
||||
sessionKey,
|
||||
labelTransition: `${fromLabel} → ${toLabel}`,
|
||||
@@ -219,14 +217,14 @@ export async function dispatchTask(opts: DispatchOpts): Promise<DispatchResult>
|
||||
await auditLog(workspaceDir, "model_selection", {
|
||||
issue: issueId,
|
||||
role,
|
||||
selected: modelAlias,
|
||||
tier: modelAlias,
|
||||
fullModel,
|
||||
});
|
||||
|
||||
// Build announcement
|
||||
const emoji = role === "dev"
|
||||
? (modelAlias === "haiku" ? "⚡" : modelAlias === "opus" ? "🧠" : "🔧")
|
||||
: "🔍";
|
||||
const emoji = isTier(modelAlias)
|
||||
? TIER_EMOJI[modelAlias]
|
||||
: (role === "qa" ? "🔍" : "🔧");
|
||||
const actionVerb = sessionAction === "spawn" ? "Spawning" : "Sending";
|
||||
const announcement = `${emoji} ${actionVerb} ${role.toUpperCase()} (${modelAlias}) for #${issueId}: ${issueTitle}`;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Model selection for dev/qa tasks.
|
||||
* MVP: Simple heuristic-based selection. LLM-based analysis can be added later.
|
||||
* Keyword heuristic fallback — used when the orchestrator doesn't specify a tier.
|
||||
* Returns tier names (junior, medior, senior, qa) instead of model aliases.
|
||||
*/
|
||||
|
||||
export type ModelRecommendation = {
|
||||
model: string;
|
||||
alias: string;
|
||||
tier: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
@@ -39,13 +39,13 @@ const COMPLEX_KEYWORDS = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Select appropriate model based on task description.
|
||||
* Select appropriate developer tier based on task description.
|
||||
*
|
||||
* Model tiers:
|
||||
* - haiku: very simple (typos, single-file fixes, CSS tweaks)
|
||||
* - grok: default QA (code inspection, validation, test runs)
|
||||
* - sonnet: default DEV (features, bug fixes, multi-file changes)
|
||||
* - opus: deep/architectural (system-wide refactoring, novel design)
|
||||
* Developer tiers:
|
||||
* - junior: very simple (typos, single-file fixes, CSS tweaks)
|
||||
* - medior: standard DEV (features, bug fixes, multi-file changes)
|
||||
* - senior: deep/architectural (system-wide refactoring, novel design)
|
||||
* - qa: all QA tasks (code inspection, validation, test runs)
|
||||
*/
|
||||
export function selectModel(
|
||||
issueTitle: string,
|
||||
@@ -54,9 +54,8 @@ export function selectModel(
|
||||
): ModelRecommendation {
|
||||
if (role === "qa") {
|
||||
return {
|
||||
model: "github-copilot/grok-code-fast-1",
|
||||
alias: "grok",
|
||||
reason: "Default QA model for code inspection and validation",
|
||||
tier: "qa",
|
||||
reason: "Default QA tier for code inspection and validation",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,8 +66,7 @@ export function selectModel(
|
||||
const isSimple = SIMPLE_KEYWORDS.some((kw) => text.includes(kw));
|
||||
if (isSimple && wordCount < 100) {
|
||||
return {
|
||||
model: "anthropic/claude-haiku-4-5",
|
||||
alias: "haiku",
|
||||
tier: "junior",
|
||||
reason: `Simple task detected (keywords: ${SIMPLE_KEYWORDS.filter((kw) => text.includes(kw)).join(", ")})`,
|
||||
};
|
||||
}
|
||||
@@ -77,16 +75,14 @@ export function selectModel(
|
||||
const isComplex = COMPLEX_KEYWORDS.some((kw) => text.includes(kw));
|
||||
if (isComplex || wordCount > 500) {
|
||||
return {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
alias: "opus",
|
||||
tier: "senior",
|
||||
reason: `Complex task detected (${isComplex ? "keywords: " + COMPLEX_KEYWORDS.filter((kw) => text.includes(kw)).join(", ") : "long description"})`,
|
||||
};
|
||||
}
|
||||
|
||||
// Default: sonnet for standard dev work
|
||||
// Default: medior for standard dev work
|
||||
return {
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
alias: "sonnet",
|
||||
tier: "medior",
|
||||
reason: "Standard dev task — multi-file changes, features, bug fixes",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { TIER_MIGRATION } from "./tiers.js";
|
||||
|
||||
export type WorkerState = {
|
||||
active: boolean;
|
||||
@@ -30,41 +31,64 @@ export type ProjectsData = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrate old WorkerState schema (sessionId field) to new sessions map.
|
||||
* Called transparently on read — old data is converted in memory,
|
||||
* persisted on next write.
|
||||
* Migrate old WorkerState schema to current format.
|
||||
*
|
||||
* Handles two migrations:
|
||||
* 1. Old sessionId field → sessions map (pre-sessions era)
|
||||
* 2. Model-alias session keys → tier-name keys (haiku→junior, sonnet→medior, etc.)
|
||||
*/
|
||||
function migrateWorkerState(worker: Record<string, unknown>): WorkerState {
|
||||
// Already migrated — has sessions map
|
||||
if (worker.sessions && typeof worker.sessions === "object") {
|
||||
return worker as unknown as WorkerState;
|
||||
}
|
||||
|
||||
// Old schema: { sessionId, model, ... }
|
||||
// Migration 1: old sessionId field → sessions map
|
||||
if (!worker.sessions || typeof worker.sessions !== "object") {
|
||||
const sessionId = worker.sessionId as string | null;
|
||||
const model = worker.model as string | null;
|
||||
const sessions: Record<string, string | null> = {};
|
||||
|
||||
if (sessionId && model) {
|
||||
sessions[model] = sessionId;
|
||||
// Apply tier migration to the model key too
|
||||
const tierKey = TIER_MIGRATION[model] ?? model;
|
||||
sessions[tierKey] = sessionId;
|
||||
}
|
||||
|
||||
return {
|
||||
active: worker.active as boolean,
|
||||
issueId: worker.issueId as string | null,
|
||||
startTime: worker.startTime as string | null,
|
||||
model,
|
||||
model: model ? (TIER_MIGRATION[model] ?? model) : null,
|
||||
sessions,
|
||||
};
|
||||
}
|
||||
|
||||
// Migration 2: model-alias session keys → tier-name keys
|
||||
const oldSessions = worker.sessions as Record<string, string | null>;
|
||||
const needsMigration = Object.keys(oldSessions).some((key) => key in TIER_MIGRATION);
|
||||
|
||||
if (needsMigration) {
|
||||
const newSessions: Record<string, string | null> = {};
|
||||
for (const [key, value] of Object.entries(oldSessions)) {
|
||||
const newKey = TIER_MIGRATION[key] ?? key;
|
||||
newSessions[newKey] = value;
|
||||
}
|
||||
const model = worker.model as string | null;
|
||||
return {
|
||||
active: worker.active as boolean,
|
||||
issueId: worker.issueId as string | null,
|
||||
startTime: worker.startTime as string | null,
|
||||
model: model ? (TIER_MIGRATION[model] ?? model) : null,
|
||||
sessions: newSessions,
|
||||
};
|
||||
}
|
||||
|
||||
return worker as unknown as WorkerState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a blank WorkerState with null sessions for given model aliases.
|
||||
* Create a blank WorkerState with null sessions for given tier names.
|
||||
*/
|
||||
export function emptyWorkerState(aliases: string[]): WorkerState {
|
||||
export function emptyWorkerState(tiers: string[]): WorkerState {
|
||||
const sessions: Record<string, string | null> = {};
|
||||
for (const alias of aliases) {
|
||||
sessions[alias] = null;
|
||||
for (const tier of tiers) {
|
||||
sessions[tier] = null;
|
||||
}
|
||||
return {
|
||||
active: false,
|
||||
@@ -76,13 +100,13 @@ export function emptyWorkerState(aliases: string[]): WorkerState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session key for a specific model alias from a worker's sessions map.
|
||||
* Get session key for a specific tier from a worker's sessions map.
|
||||
*/
|
||||
export function getSessionForModel(
|
||||
worker: WorkerState,
|
||||
modelAlias: string,
|
||||
tier: string,
|
||||
): string | null {
|
||||
return worker.sessions[modelAlias] ?? null;
|
||||
return worker.sessions[tier] ?? null;
|
||||
}
|
||||
|
||||
function projectsPath(workspaceDir: string): string {
|
||||
@@ -163,7 +187,7 @@ export async function updateWorker(
|
||||
|
||||
/**
|
||||
* Mark a worker as active with a new task.
|
||||
* Sets active=true, issueId, model. Stores session key in sessions[model].
|
||||
* Sets active=true, issueId, model (tier). Stores session key in sessions[tier].
|
||||
*/
|
||||
export async function activateWorker(
|
||||
workspaceDir: string,
|
||||
@@ -181,7 +205,7 @@ export async function activateWorker(
|
||||
issueId: params.issueId,
|
||||
model: params.model,
|
||||
};
|
||||
// Store session key in the sessions map for this model
|
||||
// Store session key in the sessions map for this tier
|
||||
if (params.sessionKey !== undefined) {
|
||||
updates.sessions = { [params.model]: params.sessionKey };
|
||||
}
|
||||
|
||||
263
lib/setup.ts
Normal file
263
lib/setup.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* setup.ts — Shared setup logic for DevClaw onboarding.
|
||||
*
|
||||
* Used by both the `devclaw_setup` tool and the `openclaw devclaw setup` CLI command.
|
||||
* Handles: agent creation, model configuration, workspace file writes.
|
||||
*/
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { ALL_TIERS, DEFAULT_MODELS, type Tier } from "./tiers.js";
|
||||
import {
|
||||
AGENTS_MD_TEMPLATE,
|
||||
HEARTBEAT_MD_TEMPLATE,
|
||||
DEFAULT_DEV_INSTRUCTIONS,
|
||||
DEFAULT_QA_INSTRUCTIONS,
|
||||
} from "./templates.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export type SetupOpts = {
|
||||
/** Create a new agent with this name. Mutually exclusive with agentId. */
|
||||
newAgentName?: string;
|
||||
/** Use an existing agent by ID. Mutually exclusive with newAgentName. */
|
||||
agentId?: string;
|
||||
/** Override workspace path (auto-detected from agent if not given). */
|
||||
workspacePath?: string;
|
||||
/** Model overrides per tier. Missing tiers use defaults. */
|
||||
models?: Partial<Record<Tier, string>>;
|
||||
};
|
||||
|
||||
export type SetupResult = {
|
||||
agentId: string;
|
||||
agentCreated: boolean;
|
||||
workspacePath: string;
|
||||
models: Record<Tier, string>;
|
||||
filesWritten: string[];
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Run the full DevClaw setup.
|
||||
*
|
||||
* 1. Create agent (optional) or resolve existing workspace
|
||||
* 2. Merge model config and write to openclaw.json
|
||||
* 3. Write workspace files (AGENTS.md, HEARTBEAT.md, roles, memory)
|
||||
*/
|
||||
export async function runSetup(opts: SetupOpts): Promise<SetupResult> {
|
||||
const warnings: string[] = [];
|
||||
const filesWritten: string[] = [];
|
||||
let agentId: string;
|
||||
let agentCreated = false;
|
||||
let workspacePath: string;
|
||||
|
||||
// --- Step 1: Agent ---
|
||||
if (opts.newAgentName) {
|
||||
const result = await createAgent(opts.newAgentName);
|
||||
agentId = result.agentId;
|
||||
workspacePath = result.workspacePath;
|
||||
agentCreated = true;
|
||||
} else if (opts.agentId) {
|
||||
agentId = opts.agentId;
|
||||
workspacePath = opts.workspacePath ?? await resolveWorkspacePath(agentId);
|
||||
} else if (opts.workspacePath) {
|
||||
agentId = "unknown";
|
||||
workspacePath = opts.workspacePath;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Setup requires either newAgentName, agentId, or workspacePath",
|
||||
);
|
||||
}
|
||||
|
||||
// --- Step 2: Models ---
|
||||
const models = { ...DEFAULT_MODELS };
|
||||
if (opts.models) {
|
||||
for (const [tier, model] of Object.entries(opts.models)) {
|
||||
if (model && (ALL_TIERS as readonly string[]).includes(tier)) {
|
||||
models[tier as Tier] = model;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write plugin config to openclaw.json
|
||||
await writePluginConfig(models);
|
||||
|
||||
// --- Step 3: Workspace files ---
|
||||
|
||||
// AGENTS.md (backup existing)
|
||||
const agentsMdPath = path.join(workspacePath, "AGENTS.md");
|
||||
await backupAndWrite(agentsMdPath, AGENTS_MD_TEMPLATE);
|
||||
filesWritten.push("AGENTS.md");
|
||||
|
||||
// HEARTBEAT.md
|
||||
const heartbeatPath = path.join(workspacePath, "HEARTBEAT.md");
|
||||
await backupAndWrite(heartbeatPath, HEARTBEAT_MD_TEMPLATE);
|
||||
filesWritten.push("HEARTBEAT.md");
|
||||
|
||||
// roles/default/dev.md and qa.md
|
||||
const rolesDefaultDir = path.join(workspacePath, "roles", "default");
|
||||
await fs.mkdir(rolesDefaultDir, { recursive: true });
|
||||
|
||||
const devRolePath = path.join(rolesDefaultDir, "dev.md");
|
||||
const qaRolePath = path.join(rolesDefaultDir, "qa.md");
|
||||
|
||||
if (!await fileExists(devRolePath)) {
|
||||
await fs.writeFile(devRolePath, DEFAULT_DEV_INSTRUCTIONS, "utf-8");
|
||||
filesWritten.push("roles/default/dev.md");
|
||||
}
|
||||
if (!await fileExists(qaRolePath)) {
|
||||
await fs.writeFile(qaRolePath, DEFAULT_QA_INSTRUCTIONS, "utf-8");
|
||||
filesWritten.push("roles/default/qa.md");
|
||||
}
|
||||
|
||||
// memory/projects.json
|
||||
const memoryDir = path.join(workspacePath, "memory");
|
||||
await fs.mkdir(memoryDir, { recursive: true });
|
||||
const projectsJsonPath = path.join(memoryDir, "projects.json");
|
||||
if (!await fileExists(projectsJsonPath)) {
|
||||
await fs.writeFile(
|
||||
projectsJsonPath,
|
||||
JSON.stringify({ projects: {} }, null, 2) + "\n",
|
||||
"utf-8",
|
||||
);
|
||||
filesWritten.push("memory/projects.json");
|
||||
}
|
||||
|
||||
return {
|
||||
agentId,
|
||||
agentCreated,
|
||||
workspacePath,
|
||||
models,
|
||||
filesWritten,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new agent via `openclaw agents add`.
|
||||
*/
|
||||
async function createAgent(
|
||||
name: string,
|
||||
): Promise<{ agentId: string; workspacePath: string }> {
|
||||
// Generate ID from name (lowercase, hyphenated)
|
||||
const agentId = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
|
||||
const workspacePath = path.join(
|
||||
process.env.HOME ?? "/home/lauren",
|
||||
".openclaw",
|
||||
`workspace-${agentId}`,
|
||||
);
|
||||
|
||||
try {
|
||||
await execFileAsync("openclaw", [
|
||||
"agents",
|
||||
"add",
|
||||
agentId,
|
||||
"--name",
|
||||
name,
|
||||
"--workspace",
|
||||
workspacePath,
|
||||
"--non-interactive",
|
||||
], { timeout: 30_000 });
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to create agent "${name}": ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
|
||||
// openclaw agents add creates a .git dir in the workspace — remove it
|
||||
const gitDir = path.join(workspacePath, ".git");
|
||||
try {
|
||||
await fs.rm(gitDir, { recursive: true });
|
||||
} catch {
|
||||
// May not exist — that's fine
|
||||
}
|
||||
|
||||
return { agentId, workspacePath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve workspace path from an agent ID by reading openclaw.json.
|
||||
*/
|
||||
async function resolveWorkspacePath(agentId: string): Promise<string> {
|
||||
const configPath = path.join(
|
||||
process.env.HOME ?? "/home/lauren",
|
||||
".openclaw",
|
||||
"openclaw.json",
|
||||
);
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
const agent = config.agents?.list?.find(
|
||||
(a: { id: string }) => a.id === agentId,
|
||||
);
|
||||
if (!agent?.workspace) {
|
||||
throw new Error(
|
||||
`Agent "${agentId}" not found in openclaw.json or has no workspace configured.`,
|
||||
);
|
||||
}
|
||||
|
||||
return agent.workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write DevClaw model tier config to openclaw.json plugins section.
|
||||
* Read-modify-write to preserve existing config.
|
||||
*/
|
||||
async function writePluginConfig(
|
||||
models: Record<Tier, string>,
|
||||
): Promise<void> {
|
||||
const configPath = path.join(
|
||||
process.env.HOME ?? "/home/lauren",
|
||||
".openclaw",
|
||||
"openclaw.json",
|
||||
);
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
// Ensure plugins.entries.devclaw.config.models exists
|
||||
if (!config.plugins) config.plugins = {};
|
||||
if (!config.plugins.entries) config.plugins.entries = {};
|
||||
if (!config.plugins.entries.devclaw) config.plugins.entries.devclaw = {};
|
||||
if (!config.plugins.entries.devclaw.config)
|
||||
config.plugins.entries.devclaw.config = {};
|
||||
|
||||
config.plugins.entries.devclaw.config.models = { ...models };
|
||||
|
||||
// Atomic write
|
||||
const tmpPath = configPath + ".tmp";
|
||||
await fs.writeFile(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
||||
await fs.rename(tmpPath, configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup existing file (if any) and write new content.
|
||||
*/
|
||||
async function backupAndWrite(
|
||||
filePath: string,
|
||||
content: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
// File exists — backup
|
||||
const bakPath = filePath + ".bak";
|
||||
await fs.copyFile(filePath, bakPath);
|
||||
} catch {
|
||||
// File doesn't exist — ensure directory
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
}
|
||||
await fs.writeFile(filePath, content, "utf-8");
|
||||
}
|
||||
|
||||
async function fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
179
lib/templates.ts
Normal file
179
lib/templates.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Shared templates for workspace files.
|
||||
* Used by setup and project_register.
|
||||
*/
|
||||
|
||||
export const DEFAULT_DEV_INSTRUCTIONS = `# DEV Worker Instructions
|
||||
|
||||
- Work in a git worktree (never switch branches in the main repo)
|
||||
- Run tests before completing
|
||||
- Create an MR/PR to the base branch and merge it
|
||||
- Clean up the worktree after merging
|
||||
- When done, call task_complete with role "dev", result "done", and a brief summary
|
||||
- If you discover unrelated bugs, call task_create to file them
|
||||
- Do NOT call task_pickup, queue_status, session_health, or project_register
|
||||
`;
|
||||
|
||||
export const DEFAULT_QA_INSTRUCTIONS = `# QA Worker Instructions
|
||||
|
||||
- Pull latest from the base branch
|
||||
- Run tests and linting
|
||||
- Verify the changes address the issue requirements
|
||||
- Check for regressions in related functionality
|
||||
- When done, call task_complete with role "qa" and one of:
|
||||
- result "pass" if everything looks good
|
||||
- result "fail" with specific issues if problems found
|
||||
- result "refine" if you need human input to decide
|
||||
- If you discover unrelated bugs, call task_create to file them
|
||||
- Do NOT call task_pickup, queue_status, session_health, or project_register
|
||||
`;
|
||||
|
||||
export const AGENTS_MD_TEMPLATE = `# AGENTS.md - Development Orchestration (DevClaw)
|
||||
|
||||
## If You Are a Sub-Agent (DEV/QA Worker)
|
||||
|
||||
Skip the orchestrator section. Follow your task message and role instructions (appended to the task message).
|
||||
|
||||
### Conventions
|
||||
|
||||
- Conventional commits: \`feat:\`, \`fix:\`, \`chore:\`, \`refactor:\`, \`test:\`, \`docs:\`
|
||||
- Include issue number: \`feat: add user authentication (#12)\`
|
||||
- Branch naming: \`feature/<id>-<slug>\` or \`fix/<id>-<slug>\`
|
||||
- **DEV always works in a git worktree** (never switch branches in the main repo)
|
||||
- **DEV must merge to base branch** before announcing completion
|
||||
- **QA tests on the deployed version** and inspects code on the base branch
|
||||
- Always run tests before completing
|
||||
|
||||
### Completing Your Task
|
||||
|
||||
When you are done, **call \`task_complete\` yourself** — do not just announce in text.
|
||||
|
||||
- **DEV done:** \`task_complete({ role: "dev", result: "done", projectGroupId: "<from task message>", summary: "<brief summary>" })\`
|
||||
- **QA pass:** \`task_complete({ role: "qa", result: "pass", projectGroupId: "<from task message>", summary: "<brief summary>" })\`
|
||||
- **QA fail:** \`task_complete({ role: "qa", result: "fail", projectGroupId: "<from task message>", summary: "<specific issues>" })\`
|
||||
- **QA refine:** \`task_complete({ role: "qa", result: "refine", projectGroupId: "<from task message>", summary: "<what needs human input>" })\`
|
||||
|
||||
The \`projectGroupId\` is included in your task message.
|
||||
|
||||
### Filing Follow-Up Issues
|
||||
|
||||
If you discover unrelated bugs or needed improvements during your work, call \`task_create\` to file them:
|
||||
|
||||
\`task_create({ projectGroupId: "<from task message>", title: "Bug: ...", description: "..." })\`
|
||||
|
||||
### Tools You Should NOT Use
|
||||
|
||||
These are orchestrator-only tools. Do not call them:
|
||||
- \`task_pickup\`, \`queue_status\`, \`session_health\`, \`project_register\`
|
||||
|
||||
---
|
||||
|
||||
## Orchestrator
|
||||
|
||||
You are a **development orchestrator**. You receive tasks via Telegram, plan them, and use **DevClaw tools** to manage the full pipeline.
|
||||
|
||||
### DevClaw Tools
|
||||
|
||||
All orchestration goes through these tools. You do NOT manually manage sessions, labels, or projects.json.
|
||||
|
||||
| Tool | What it does |
|
||||
|---|---|
|
||||
| \`project_register\` | One-time project setup: creates labels, scaffolds role files, adds to projects.json |
|
||||
| \`task_create\` | Create issues from chat (bugs, features, tasks) |
|
||||
| \`queue_status\` | Scans issue queue (To Do, To Test, To Improve) + shows worker state |
|
||||
| \`task_pickup\` | End-to-end: label transition, tier assignment, session create/reuse, dispatch with role instructions, state update |
|
||||
| \`task_complete\` | End-to-end: label transition, state update, issue close/reopen. Auto-chains if enabled. |
|
||||
| \`session_health\` | Detects zombie workers, stale sessions. Can auto-fix. |
|
||||
|
||||
### Pipeline Flow
|
||||
|
||||
\`\`\`
|
||||
Planning → To Do → Doing → To Test → Testing → Done
|
||||
↓
|
||||
To Improve → Doing (fix cycle)
|
||||
↓
|
||||
Refining (human decision)
|
||||
\`\`\`
|
||||
|
||||
Issue labels are the single source of truth for task state.
|
||||
|
||||
### Developer Assignment
|
||||
|
||||
Evaluate each task and pass the appropriate developer tier to \`task_pickup\`:
|
||||
|
||||
- **junior** — trivial: typos, single-file fix, quick change
|
||||
- **medior** — standard: features, bug fixes, multi-file changes
|
||||
- **senior** — complex: architecture, system-wide refactoring, 5+ services
|
||||
- **qa** — review: code inspection, validation, test runs
|
||||
|
||||
### Picking Up Work
|
||||
|
||||
1. Use \`queue_status\` to see what's available
|
||||
2. Priority: \`To Improve\` (fix failures) > \`To Test\` (QA) > \`To Do\` (new work)
|
||||
3. Evaluate complexity, choose developer tier
|
||||
4. Call \`task_pickup\` with \`issueId\`, \`role\`, \`projectGroupId\`, \`model\` (tier name)
|
||||
5. Post the \`announcement\` from the tool response to Telegram
|
||||
|
||||
### When Work Completes
|
||||
|
||||
Workers call \`task_complete\` themselves — the label transition, state update, and audit log happen atomically.
|
||||
|
||||
**If \`autoChain\` is enabled on the project:**
|
||||
- DEV "done" → QA is dispatched automatically (qa tier)
|
||||
- QA "fail" → DEV fix is dispatched automatically (reuses previous DEV tier)
|
||||
- QA "pass" / "refine" → pipeline done or needs human input, no chaining
|
||||
|
||||
**If \`autoChain\` is disabled:**
|
||||
- The \`task_complete\` response includes a \`nextAction\` hint
|
||||
- \`"qa_pickup"\` → pick up QA for this issue
|
||||
- \`"dev_fix"\` → pick up DEV to fix
|
||||
- absent → pipeline done or needs human input
|
||||
|
||||
Post the \`announcement\` from the tool response to Telegram.
|
||||
|
||||
### Role Instructions
|
||||
|
||||
Workers receive role-specific instructions appended to their task message. These are loaded from \`roles/<project-name>/<role>.md\` in the workspace (with fallback to \`roles/default/<role>.md\`). \`project_register\` scaffolds these files automatically — edit them to customize worker behavior per project.
|
||||
|
||||
### Heartbeats
|
||||
|
||||
On heartbeat, follow \`HEARTBEAT.md\`.
|
||||
|
||||
### Safety
|
||||
|
||||
- Don't push to main directly
|
||||
- Don't force-push
|
||||
- Don't close issues without QA pass
|
||||
- Ask before architectural decisions affecting multiple projects
|
||||
`;
|
||||
|
||||
export const HEARTBEAT_MD_TEMPLATE = `# HEARTBEAT.md
|
||||
|
||||
On each heartbeat, run these checks using DevClaw tools:
|
||||
|
||||
## 1. Health Check
|
||||
|
||||
Call \`session_health\` with \`projectGroupId\` and \`autoFix: true\`.
|
||||
- Detects zombie workers (active but session dead)
|
||||
- Auto-fixes stale state in projects.json
|
||||
|
||||
## 2. Queue Scan
|
||||
|
||||
Call \`queue_status\` with \`projectGroupId\`.
|
||||
- Shows issues in To Do, To Test, To Improve
|
||||
- Shows current worker state (active/idle)
|
||||
|
||||
## 3. Pick Up Work (if slots free)
|
||||
|
||||
If a worker slot is free (DEV or QA not active), pick up work by priority:
|
||||
|
||||
1. \`To Improve\` issues → \`task_pickup\` with role \`dev\`
|
||||
2. \`To Test\` issues → \`task_pickup\` with role \`qa\`
|
||||
3. \`To Do\` issues → \`task_pickup\` with role \`dev\`
|
||||
|
||||
Choose the developer tier based on task complexity (see AGENTS.md developer assignment guide).
|
||||
|
||||
## 4. Nothing to do?
|
||||
|
||||
If no issues in queue and no active workers → reply \`HEARTBEAT_OK\`.
|
||||
`;
|
||||
66
lib/tiers.ts
Normal file
66
lib/tiers.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Developer tier definitions and model resolution.
|
||||
*
|
||||
* Tasks are assigned to developer tiers (junior, medior, senior, qa)
|
||||
* instead of raw model names. Each tier maps to a configurable LLM model.
|
||||
*/
|
||||
|
||||
export const DEV_TIERS = ["junior", "medior", "senior"] as const;
|
||||
export const QA_TIERS = ["qa"] as const;
|
||||
export const ALL_TIERS = [...DEV_TIERS, ...QA_TIERS] as const;
|
||||
|
||||
export type DevTier = (typeof DEV_TIERS)[number];
|
||||
export type QaTier = (typeof QA_TIERS)[number];
|
||||
export type Tier = (typeof ALL_TIERS)[number];
|
||||
|
||||
export const DEFAULT_MODELS: Record<Tier, string> = {
|
||||
junior: "anthropic/claude-haiku-4-5",
|
||||
medior: "anthropic/claude-sonnet-4-5",
|
||||
senior: "anthropic/claude-opus-4-5",
|
||||
qa: "anthropic/claude-sonnet-4-5",
|
||||
};
|
||||
|
||||
/** Emoji used in announcements per tier. */
|
||||
export const TIER_EMOJI: Record<Tier, string> = {
|
||||
junior: "⚡",
|
||||
medior: "🔧",
|
||||
senior: "🧠",
|
||||
qa: "🔍",
|
||||
};
|
||||
|
||||
/** Check if a string is a valid tier name. */
|
||||
export function isTier(value: string): value is Tier {
|
||||
return (ALL_TIERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/** Check if a string is a valid dev tier name. */
|
||||
export function isDevTier(value: string): value is DevTier {
|
||||
return (DEV_TIERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a tier name to a full model ID.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. Plugin config `models` map (user overrides)
|
||||
* 2. DEFAULT_MODELS (hardcoded defaults)
|
||||
* 3. Treat input as raw model ID (passthrough for non-tier values)
|
||||
*/
|
||||
export function resolveModel(
|
||||
tier: string,
|
||||
pluginConfig?: Record<string, unknown>,
|
||||
): string {
|
||||
const models = (pluginConfig as { models?: Record<string, string> })?.models;
|
||||
return models?.[tier] ?? DEFAULT_MODELS[tier as Tier] ?? tier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration map from old model-alias session keys to tier names.
|
||||
* Used by migrateWorkerState() in projects.ts.
|
||||
*/
|
||||
export const TIER_MIGRATION: Record<string, string> = {
|
||||
haiku: "junior",
|
||||
sonnet: "medior",
|
||||
opus: "senior",
|
||||
grok: "qa",
|
||||
};
|
||||
84
lib/tools/devclaw-setup.ts
Normal file
84
lib/tools/devclaw-setup.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* devclaw_setup — Agent-driven setup tool.
|
||||
*
|
||||
* Creates a new agent (optional), configures model tiers,
|
||||
* and writes workspace files (AGENTS.md, HEARTBEAT.md, roles, memory).
|
||||
*/
|
||||
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
||||
import { runSetup } from "../setup.js";
|
||||
import { ALL_TIERS, DEFAULT_MODELS, type Tier } from "../tiers.js";
|
||||
|
||||
export function createSetupTool(api: OpenClawPluginApi) {
|
||||
return (ctx: OpenClawPluginToolContext) => ({
|
||||
name: "devclaw_setup",
|
||||
description: `Set up DevClaw in an agent's workspace. Creates AGENTS.md, HEARTBEAT.md, role templates, memory/projects.json, and writes model tier config to openclaw.json. Optionally creates a new agent. Backs up existing files before overwriting.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
newAgentName: {
|
||||
type: "string",
|
||||
description: "Create a new agent with this name. If omitted, configures the current agent's workspace.",
|
||||
},
|
||||
models: {
|
||||
type: "object",
|
||||
description: `Model overrides per tier. Missing tiers use defaults. Example: { "junior": "anthropic/claude-haiku-4-5", "senior": "anthropic/claude-opus-4-5" }`,
|
||||
properties: {
|
||||
junior: { type: "string", description: `Junior dev model (default: ${DEFAULT_MODELS.junior})` },
|
||||
medior: { type: "string", description: `Medior dev model (default: ${DEFAULT_MODELS.medior})` },
|
||||
senior: { type: "string", description: `Senior dev model (default: ${DEFAULT_MODELS.senior})` },
|
||||
qa: { type: "string", description: `QA engineer model (default: ${DEFAULT_MODELS.qa})` },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
async execute(_id: string, params: Record<string, unknown>) {
|
||||
const newAgentName = params.newAgentName as string | undefined;
|
||||
const modelsParam = params.models as Partial<Record<Tier, string>> | undefined;
|
||||
const workspaceDir = ctx.workspaceDir;
|
||||
|
||||
const result = await runSetup({
|
||||
newAgentName,
|
||||
// If no new agent name, use the current agent's workspace
|
||||
agentId: newAgentName ? undefined : ctx.agentId,
|
||||
workspacePath: newAgentName ? undefined : workspaceDir,
|
||||
models: modelsParam,
|
||||
});
|
||||
|
||||
const lines = [
|
||||
result.agentCreated
|
||||
? `Agent "${result.agentId}" created`
|
||||
: `Configured workspace for agent "${result.agentId}"`,
|
||||
``,
|
||||
`Models:`,
|
||||
...ALL_TIERS.map((t) => ` ${t}: ${result.models[t]}`),
|
||||
``,
|
||||
`Files written:`,
|
||||
...result.filesWritten.map((f) => ` ${f}`),
|
||||
];
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
lines.push(``, `Warnings:`, ...result.warnings.map((w) => ` ${w}`));
|
||||
}
|
||||
|
||||
lines.push(
|
||||
``,
|
||||
`Next steps:`,
|
||||
` 1. Add bot to a Telegram group`,
|
||||
` 2. Register a project: "Register project <name> at <repo> for group <id>"`,
|
||||
` 3. Create your first issue and pick it up`,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
...result,
|
||||
summary: lines.join("\n"),
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import { readProjects, writeProjects, emptyWorkerState } from "../projects.js";
|
||||
import { resolveRepoPath } from "../gitlab.js";
|
||||
import { createProvider } from "../providers/index.js";
|
||||
import { log as auditLog } from "../audit.js";
|
||||
import { DEV_TIERS, QA_TIERS } from "../tiers.js";
|
||||
import { DEFAULT_DEV_INSTRUCTIONS, DEFAULT_QA_INSTRUCTIONS } from "../templates.js";
|
||||
|
||||
/**
|
||||
* Ensure default role files exist, then copy them into the project's role directory.
|
||||
@@ -64,31 +66,6 @@ async function scaffoldRoleFiles(workspaceDir: string, projectName: string): Pro
|
||||
return created;
|
||||
}
|
||||
|
||||
const DEFAULT_DEV_INSTRUCTIONS = `# DEV Worker Instructions
|
||||
|
||||
- Work in a git worktree (never switch branches in the main repo)
|
||||
- Run tests before completing
|
||||
- Create an MR/PR to the base branch and merge it
|
||||
- Clean up the worktree after merging
|
||||
- When done, call task_complete with role "dev", result "done", and a brief summary
|
||||
- If you discover unrelated bugs, call task_create to file them
|
||||
- Do NOT call task_pickup, queue_status, session_health, or project_register
|
||||
`;
|
||||
|
||||
const DEFAULT_QA_INSTRUCTIONS = `# QA Worker Instructions
|
||||
|
||||
- Pull latest from the base branch
|
||||
- Run tests and linting
|
||||
- Verify the changes address the issue requirements
|
||||
- Check for regressions in related functionality
|
||||
- When done, call task_complete with role "qa" and one of:
|
||||
- result "pass" if everything looks good
|
||||
- result "fail" with specific issues if problems found
|
||||
- result "refine" if you need human input to decide
|
||||
- If you discover unrelated bugs, call task_create to file them
|
||||
- Do NOT call task_pickup, queue_status, session_health, or project_register
|
||||
`;
|
||||
|
||||
export function createProjectRegisterTool(api: OpenClawPluginApi) {
|
||||
return (ctx: OpenClawPluginToolContext) => ({
|
||||
name: "project_register",
|
||||
@@ -186,8 +163,8 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) {
|
||||
baseBranch,
|
||||
deployBranch,
|
||||
autoChain: false,
|
||||
dev: emptyWorkerState(["haiku", "sonnet", "opus"]),
|
||||
qa: emptyWorkerState(["grok"]),
|
||||
dev: emptyWorkerState([...DEV_TIERS]),
|
||||
qa: emptyWorkerState([...QA_TIERS]),
|
||||
};
|
||||
|
||||
await writeProjects(workspaceDir, data);
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* issue close/reopen, audit logging, and optional auto-chaining.
|
||||
*
|
||||
* When project.autoChain is true:
|
||||
* - DEV "done" → automatically dispatches QA (default model: grok)
|
||||
* - QA "fail" → automatically dispatches DEV fix (reuses previous DEV model)
|
||||
* - DEV "done" → automatically dispatches QA (qa tier)
|
||||
* - QA "fail" → automatically dispatches DEV fix (reuses previous DEV tier)
|
||||
*/
|
||||
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
||||
import {
|
||||
@@ -120,6 +120,7 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
||||
|
||||
if (project.autoChain) {
|
||||
try {
|
||||
const pluginConfig = api.pluginConfig as Record<string, unknown> | undefined;
|
||||
const issue = await getIssue(issueId, glabOpts);
|
||||
const chainResult = await dispatchTask({
|
||||
workspaceDir,
|
||||
@@ -131,11 +132,12 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
||||
issueDescription: issue.description ?? "",
|
||||
issueUrl: issue.web_url,
|
||||
role: "qa",
|
||||
modelAlias: "grok",
|
||||
modelAlias: "qa",
|
||||
fromLabel: "To Test",
|
||||
toLabel: "Testing",
|
||||
transitionLabel: (id, from, to) =>
|
||||
transitionLabel(id, from as StateLabel, to as StateLabel, glabOpts),
|
||||
pluginConfig,
|
||||
});
|
||||
output.autoChain = {
|
||||
dispatched: true,
|
||||
@@ -181,6 +183,7 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
||||
|
||||
if (project.autoChain && devModel) {
|
||||
try {
|
||||
const pluginConfig = api.pluginConfig as Record<string, unknown> | undefined;
|
||||
const issue = await getIssue(issueId, glabOpts);
|
||||
const chainResult = await dispatchTask({
|
||||
workspaceDir,
|
||||
@@ -197,6 +200,7 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
||||
toLabel: "Doing",
|
||||
transitionLabel: (id, from, to) =>
|
||||
transitionLabel(id, from as StateLabel, to as StateLabel, glabOpts),
|
||||
pluginConfig,
|
||||
});
|
||||
output.autoChain = {
|
||||
dispatched: true,
|
||||
|
||||
@@ -23,7 +23,7 @@ import { dispatchTask } from "../dispatch.js";
|
||||
export function createTaskPickupTool(api: OpenClawPluginApi) {
|
||||
return (ctx: OpenClawPluginToolContext) => ({
|
||||
name: "task_pickup",
|
||||
description: `Pick up a task from the issue queue for a DEV or QA worker. Handles everything end-to-end: label transition, model selection, session creation/reuse, task dispatch, state update, and audit logging. The orchestrator should analyze the issue and pass the appropriate model. Returns an announcement for the agent to post — no further session actions needed.`,
|
||||
description: `Pick up a task from the issue queue for a DEV or QA worker. Handles everything end-to-end: label transition, tier assignment, session creation/reuse, task dispatch, state update, and audit logging. The orchestrator should analyze the issue and pass the appropriate developer tier. Returns an announcement for the agent to post — no further session actions needed.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
required: ["issueId", "role", "projectGroupId"],
|
||||
@@ -36,7 +36,7 @@ export function createTaskPickupTool(api: OpenClawPluginApi) {
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
description: "Model alias to use (e.g. haiku, sonnet, opus, grok). The orchestrator should analyze the issue complexity and choose. Falls back to keyword heuristic if omitted.",
|
||||
description: "Developer tier (junior, medior, senior, qa). The orchestrator should evaluate the task complexity and choose the right tier. Falls back to keyword heuristic if omitted.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -101,12 +101,13 @@ export function createTaskPickupTool(api: OpenClawPluginApi) {
|
||||
modelSource = "llm";
|
||||
} else {
|
||||
const selected = selectModel(issue.title, issue.description ?? "", role);
|
||||
modelAlias = selected.alias;
|
||||
modelAlias = selected.tier;
|
||||
modelReason = selected.reason;
|
||||
modelSource = "heuristic";
|
||||
}
|
||||
|
||||
// 5. Dispatch via shared logic
|
||||
const pluginConfig = api.pluginConfig as Record<string, unknown> | undefined;
|
||||
const dispatchResult = await dispatchTask({
|
||||
workspaceDir,
|
||||
agentId: ctx.agentId,
|
||||
@@ -122,6 +123,7 @@ export function createTaskPickupTool(api: OpenClawPluginApi) {
|
||||
toLabel: targetLabel,
|
||||
transitionLabel: (id, from, to) =>
|
||||
transitionLabel(id, from as StateLabel, to as StateLabel, glabOpts),
|
||||
pluginConfig,
|
||||
});
|
||||
|
||||
// 6. Build result
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
{
|
||||
"id": "devclaw",
|
||||
"name": "DevClaw",
|
||||
"description": "Multi-project dev/qa pipeline orchestration for OpenClaw. Atomic task pickup, completion, queue status, and session health tools.",
|
||||
"description": "Multi-project dev/qa pipeline orchestration for OpenClaw. Developer tiers, atomic task management, session health, and audit logging.",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"modelSelection": {
|
||||
"models": {
|
||||
"type": "object",
|
||||
"description": "Model mapping per developer tier (junior, medior, senior, qa)",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"analyzerModel": { "type": "string" }
|
||||
"junior": { "type": "string", "description": "Junior dev model (default: anthropic/claude-haiku-4-5)" },
|
||||
"medior": { "type": "string", "description": "Medior dev model (default: anthropic/claude-sonnet-4-5)" },
|
||||
"senior": { "type": "string", "description": "Senior dev model (default: anthropic/claude-opus-4-5)" },
|
||||
"qa": { "type": "string", "description": "QA engineer model (default: anthropic/claude-sonnet-4-5)" }
|
||||
}
|
||||
},
|
||||
"glabPath": {
|
||||
"type": "string",
|
||||
"description": "Path to glab CLI binary. Defaults to 'glab' on PATH."
|
||||
},
|
||||
"ghPath": {
|
||||
"type": "string",
|
||||
"description": "Path to gh CLI binary. Defaults to 'gh' on PATH."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user