refactor: restructure workspace file organization (#121) (#122)

## Path Changes
- audit.log: memory/audit.log → log/audit.log
- projects.json: memory/projects.json → projects/projects.json
- prompts: roles/<project>/<role>.md → projects/prompts/<project>/<role>.md

## Files Updated
- lib/audit.ts - new audit log path
- lib/projects.ts - new projects.json path
- lib/dispatch.ts - new prompt instructions path
- lib/tools/project-register.ts - prompt scaffolding path
- lib/setup/workspace.ts - workspace scaffolding paths
- lib/context-guard.ts - projects.json path
- lib/tools/setup.ts - tool description
- lib/templates.ts - AGENTS.md template path references

## Documentation Updated
- README.md
- docs/ARCHITECTURE.md
- docs/ONBOARDING.md
- docs/QA_WORKFLOW.md
- docs/ROADMAP.md
- docs/TESTING.md

Addresses issue #121
This commit is contained in:
Lauren ten Hoor
2026-02-11 01:55:26 +08:00
committed by GitHub
parent 2450181482
commit 862813e6d3
14 changed files with 58 additions and 96 deletions

View File

@@ -164,7 +164,7 @@ The keyword heuristic in `model-selector.ts` serves as a **fallback only**, used
## State management ## State management
All project state lives in a single `memory/projects.json` file in the orchestrator's workspace, keyed by Telegram group ID: All project state lives in a single `projects/projects.json` file in the orchestrator's workspace, keyed by Telegram group ID:
```json ```json
{ {
@@ -235,7 +235,7 @@ Pick up a task from the issue queue for a DEV or QA worker.
2. Validates no active worker for this role 2. Validates no active worker for this role
3. Fetches issue from issue tracker, verifies correct label state 3. Fetches issue from issue tracker, verifies correct label state
4. Assigns tier (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`) 5. Loads prompt instructions from `projects/prompts/<project>/<role>.md`
6. Looks up existing session for assigned tier (session-per-tier) 6. Looks up existing session for assigned tier (session-per-tier)
7. Transitions label (e.g. `To Do``Doing`) 7. Transitions label (e.g. `To Do``Doing`)
8. Creates session via Gateway RPC if new (`sessions.patch`) 8. Creates session via Gateway RPC if new (`sessions.patch`)
@@ -357,13 +357,13 @@ Register a new project with DevClaw. Creates all required issue tracker labels (
2. Resolves repo path, auto-detects GitHub/GitLab, and verifies access 2. Resolves repo path, auto-detects GitHub/GitLab, and verifies access
3. Creates all 8 state labels (idempotent — safe to run on existing projects) 3. Creates all 8 state labels (idempotent — safe to run on existing projects)
4. Adds project entry to `projects.json` with empty worker state and `autoChain: false` 4. Adds project entry to `projects.json` with empty worker state and `autoChain: false`
5. Scaffolds role instruction files: `roles/<project>/dev.md` and `roles/<project>/qa.md` (copied from `roles/default/`) 5. Scaffolds prompt instruction files: `projects/prompts/<project>/dev.md` and `projects/prompts/<project>/qa.md`
6. Writes audit log entry 6. Writes audit log entry
7. Returns announcement text 7. Returns announcement text
## Audit logging ## Audit logging
Every tool call automatically appends an NDJSON entry to `memory/audit.log`. No manual logging required from the orchestrator agent. Every tool call automatically appends an NDJSON entry to `log/audit.log`. No manual logging required from the orchestrator agent.
```jsonl ```jsonl
{"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:00Z","event":"task_pickup","project":"my-webapp","issue":42,"role":"dev","tier":"medior","sessionAction":"send"}
@@ -438,25 +438,26 @@ Restrict tools to your orchestrator agent only:
> DevClaw uses an `IssueProvider` interface to abstract issue tracker operations. GitLab (via `glab` CLI) and GitHub (via `gh` CLI) are supported — the provider is auto-detected from the git remote URL. Jira is planned. > DevClaw uses an `IssueProvider` interface to abstract issue tracker operations. GitLab (via `glab` CLI) and GitHub (via `gh` CLI) are supported — the provider is auto-detected from the git remote URL. Jira is planned.
## Role instructions ## Prompt instructions
Workers receive role-specific instructions appended to their task message. `project_register` scaffolds editable files: Workers receive role-specific instructions appended to their task message. `project_register` scaffolds editable files:
``` ```
workspace/ workspace/
├── roles/ ├── projects/
│ ├── default/ ← sensible defaults (created once) │ ├── projects.json ← project state
│ ├── dev.md └── prompts/
── qa.md ── my-webapp/ ← per-project prompts (edit to customize)
│ ├── my-webapp/ ← per-project overrides (edit to customize)
│ │ ├── dev.md │ │ ├── dev.md
│ │ └── qa.md │ │ └── qa.md
│ └── another-project/ │ └── another-project/
│ ├── dev.md │ ├── dev.md
│ └── qa.md │ └── qa.md
├── log/
│ └── audit.log ← NDJSON event log
``` ```
`task_pickup` loads `roles/<project>/<role>.md` with fallback to `roles/default/<role>.md`. Edit the per-project files to customize worker behavior — for example, adding project-specific deployment steps or test commands. `task_pickup` loads `projects/prompts/<project>/<role>.md`. Edit these files to customize worker behavior per project — for example, adding project-specific deployment steps or test commands.
## Requirements ## Requirements

View File

@@ -473,11 +473,11 @@ Every piece of data and where it lives:
│ task_create → create issue in tracker │ │ task_create → create issue in tracker │
│ queue_status → read labels + read state │ │ queue_status → read labels + read state │
│ session_health → check sessions + fix zombies │ │ session_health → check sessions + fix zombies │
│ project_register → labels + roles + state init (one-time) │ project_register → labels + prompts + state init (one-time) │
└─────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
↕ atomic file I/O ↕ OpenClaw CLI (plugin shells out) ↕ atomic file I/O ↕ OpenClaw CLI (plugin shells out)
┌────────────────────────────────┐ ┌──────────────────────────────┐ ┌────────────────────────────────┐ ┌──────────────────────────────┐
memory/projects.json │ │ OpenClaw Gateway + CLI │ projects/projects.json │ │ OpenClaw Gateway + CLI │
│ │ │ (called by plugin, not agent)│ │ │ │ (called by plugin, not agent)│
│ Per project: │ │ │ │ Per project: │ │ │
│ dev: │ │ openclaw gateway call │ │ dev: │ │ openclaw gateway call │
@@ -493,7 +493,7 @@ Every piece of data and where it lives:
└────────────────────────────────┘ └──────────────────────────────┘ └────────────────────────────────┘ └──────────────────────────────┘
↕ append-only ↕ append-only
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
memory/audit.log (observability) │ log/audit.log (observability)
│ │ │ │
│ NDJSON, one line per event: │ │ NDJSON, one line per event: │
│ task_pickup, task_complete, model_selection, │ │ task_pickup, task_complete, model_selection, │
@@ -607,7 +607,7 @@ Provider selection is handled by `createProvider()` in `lib/providers/index.ts`.
| Plugin source | `~/.openclaw/extensions/devclaw/` | Plugin code | | Plugin source | `~/.openclaw/extensions/devclaw/` | Plugin code |
| Plugin manifest | `~/.openclaw/extensions/devclaw/openclaw.plugin.json` | Plugin registration | | Plugin manifest | `~/.openclaw/extensions/devclaw/openclaw.plugin.json` | Plugin registration |
| Agent config | `~/.openclaw/openclaw.json` | Agent definition + tool permissions + tier config | | Agent config | `~/.openclaw/openclaw.json` | Agent definition + tool permissions + tier config |
| Worker state | `~/.openclaw/workspace-<agent>/memory/projects.json` | Per-project DEV/QA state | | Worker state | `~/.openclaw/workspace-<agent>/projects/projects.json` | Per-project DEV/QA state |
| Audit log | `~/.openclaw/workspace-<agent>/memory/audit.log` | NDJSON event log | | Audit log | `~/.openclaw/workspace-<agent>/log/audit.log` | NDJSON event log |
| Session transcripts | `~/.openclaw/agents/<agent>/sessions/<uuid>.jsonl` | Conversation history per session | | Session transcripts | `~/.openclaw/agents/<agent>/sessions/<uuid>.jsonl` | Conversation history per session |
| Git repos | `~/git/<project>/` | Project source code | | Git repos | `~/git/<project>/` | Project source code |

View File

@@ -147,7 +147,7 @@ Tell the orchestrator agent to register a new project:
The agent calls `project_register`, which atomically: The agent calls `project_register`, which atomically:
- Validates the repo and auto-detects GitHub/GitLab from remote - Validates the repo and auto-detects GitHub/GitLab from remote
- Creates all 8 state labels (idempotent) - Creates all 8 state labels (idempotent)
- Scaffolds role instruction files (`roles/<project>/dev.md` and `qa.md`) - Scaffolds prompt instruction files (`projects/prompts/<project>/dev.md` and `qa.md`)
- Adds the project entry to `projects.json` with `autoChain: false` - Adds the project entry to `projects.json` with `autoChain: false`
- Logs the registration event - Logs the registration event
@@ -251,7 +251,7 @@ Change which model powers each tier in `openclaw.json`:
| Channel binding analysis | Plugin (`analyze_channel_bindings`) | Detects channel conflicts, validates channel configuration | | Channel binding analysis | Plugin (`analyze_channel_bindings`) | Detects channel conflicts, validates channel configuration |
| Channel binding migration | Plugin (`devclaw_setup` with `migrateFrom`) | Automatically moves channel-wide bindings between agents | | Channel binding migration | Plugin (`devclaw_setup` with `migrateFrom`) | Automatically moves channel-wide bindings between agents |
| Label setup | Plugin (`project_register`) | 8 labels, created idempotently via `IssueProvider` | | 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 | | Prompt file scaffolding | Plugin (`project_register`) | Creates `projects/prompts/<project>/dev.md` and `qa.md` |
| Project registration | Plugin (`project_register`) | Entry in `projects.json` with empty worker state | | Project registration | Plugin (`project_register`) | Entry in `projects.json` with empty worker state |
| Telegram group setup | You (once per project) | Add bot to group | | Telegram group setup | You (once per project) | Add bot to group |
| Issue creation | Plugin (`task_create`) | Orchestrator or workers create issues from chat | | Issue creation | Plugin (`task_create`) | Orchestrator or workers create issues from chat |
@@ -260,7 +260,7 @@ Change which model powers each tier in `openclaw.json`:
| State management | Plugin | Atomic read/write to `projects.json` | | 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. | | 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. | | Task completion | Plugin (`task_complete`) | Workers self-report. Auto-chains if enabled. |
| Role instructions | Plugin (`task_pickup`) | Loaded from `roles/<project>/<role>.md`, appended to task message | | Prompt instructions | Plugin (`task_pickup`) | Loaded from `projects/prompts/<project>/<role>.md`, appended to task message |
| Audit logging | Plugin | Automatic NDJSON append per tool call | | Audit logging | Plugin | Automatic NDJSON append per tool call |
| Zombie detection | Plugin | `session_health` checks active vs alive | | Zombie detection | Plugin | `session_health` checks active vs alive |
| Queue scanning | Plugin | `queue_status` queries issue tracker per project | | Queue scanning | Plugin | `queue_status` queries issue tracker per project |

View File

@@ -95,9 +95,8 @@ As of [current date], QA workers are instructed via role templates to:
- Include specific details about what was tested - Include specific details about what was tested
- Document results, environment, and any notes - Document results, environment, and any notes
Role templates affected: Prompt templates affected:
- `roles/default/qa.md` - `projects/prompts/<project>/qa.md`
- `roles/devclaw/qa.md`
- All project-specific QA templates should follow this pattern - All project-specific QA templates should follow this pattern
## Best Practices ## Best Practices

View File

@@ -35,7 +35,7 @@ The pipeline definition replaces the hardcoded `Doing → To Test → Testing
### Open questions ### Open questions
- How do custom labels map? Generate from role names, or let users define? - How do custom labels map? Generate from role names, or let users define?
- Should roles have their own instruction files (`roles/<project>/<role>.md`) — yes, this already works - Should roles have their own instruction files (`projects/prompts/<project>/<role>.md`) — yes, this already works
- How to handle parallel roles (e.g. frontend + backend DEV in parallel before QA)? - How to handle parallel roles (e.g. frontend + backend DEV in parallel before QA)?
--- ---

View File

@@ -29,7 +29,7 @@ npm run test:ui
**What's tested:** **What's tested:**
- First-time agent creation with default models - First-time agent creation with default models
- Channel binding creation (telegram/whatsapp) - Channel binding creation (telegram/whatsapp)
- Workspace file generation (AGENTS.md, HEARTBEAT.md, roles/, memory/) - Workspace file generation (AGENTS.md, HEARTBEAT.md, projects/, log/)
- Plugin configuration initialization - Plugin configuration initialization
- Error handling: channel not configured - Error handling: channel not configured
- Error handling: channel disabled - Error handling: channel disabled

View File

@@ -10,7 +10,7 @@ export async function log(
event: string, event: string,
data: Record<string, unknown>, data: Record<string, unknown>,
): Promise<void> { ): Promise<void> {
const filePath = join(workspaceDir, "memory", "audit.log"); const filePath = join(workspaceDir, "log", "audit.log");
const entry = JSON.stringify({ const entry = JSON.stringify({
ts: new Date().toISOString(), ts: new Date().toISOString(),
event, event,

View File

@@ -140,7 +140,7 @@ You're in a **Telegram/WhatsApp group** bound to ${context.projectName ? `projec
} }
/** /**
* Find project name by matching groupId in memory/projects.json. * Find project name by matching groupId in projects/projects.json.
* The groupId (Telegram or WhatsApp) is the KEY in the projects Record. * The groupId (Telegram or WhatsApp) is the KEY in the projects Record.
*/ */
async function findProjectByGroupId( async function findProjectByGroupId(
@@ -150,7 +150,7 @@ async function findProjectByGroupId(
if (!workspaceDir) return undefined; if (!workspaceDir) return undefined;
try { try {
const projectsPath = path.join(workspaceDir, "memory", "projects.json"); const projectsPath = path.join(workspaceDir, "projects", "projects.json");
const raw = await fs.readFile(projectsPath, "utf-8"); const raw = await fs.readFile(projectsPath, "utf-8");
const data = JSON.parse(raw) as { const data = JSON.parse(raw) as {
projects: Record<string, { name: string }>; projects: Record<string, { name: string }>;

View File

@@ -53,8 +53,7 @@ export type DispatchResult = {
/** /**
* Build the task message sent to a worker session. * Build the task message sent to a worker session.
* Reads role-specific instructions from workspace/roles/<project>/<role>.md * Reads role-specific instructions from workspace/projects/prompts/<project>/<role>.md.
* with fallback to workspace/roles/default/<role>.md.
*/ */
export async function buildTaskMessage(opts: { export async function buildTaskMessage(opts: {
workspaceDir: string; workspaceDir: string;
@@ -196,10 +195,8 @@ export async function dispatchTask(
async function loadRoleInstructions( async function loadRoleInstructions(
workspaceDir: string, projectName: string, role: "dev" | "qa", workspaceDir: string, projectName: string, role: "dev" | "qa",
): Promise<string> { ): Promise<string> {
const projectFile = path.join(workspaceDir, "roles", projectName, `${role}.md`); const projectFile = path.join(workspaceDir, "projects", "prompts", projectName, `${role}.md`);
const defaultFile = path.join(workspaceDir, "roles", "default", `${role}.md`); try { return await fs.readFile(projectFile, "utf-8"); } catch { /* none */ }
try { return await fs.readFile(projectFile, "utf-8"); } catch { /* fallback */ }
try { return await fs.readFile(defaultFile, "utf-8"); } catch { /* none */ }
return ""; return "";
} }

View File

@@ -72,7 +72,7 @@ export function getSessionForTier(
} }
function projectsPath(workspaceDir: string): string { function projectsPath(workspaceDir: string): string {
return path.join(workspaceDir, "memory", "projects.json"); return path.join(workspaceDir, "projects", "projects.json");
} }
export async function readProjects(workspaceDir: string): Promise<ProjectsData> { export async function readProjects(workspaceDir: string): Promise<ProjectsData> {

View File

@@ -8,8 +8,6 @@ import path from "node:path";
import { import {
AGENTS_MD_TEMPLATE, AGENTS_MD_TEMPLATE,
HEARTBEAT_MD_TEMPLATE, HEARTBEAT_MD_TEMPLATE,
DEFAULT_DEV_INSTRUCTIONS,
DEFAULT_QA_INSTRUCTIONS,
} from "../templates.js"; } from "../templates.js";
/** /**
@@ -27,31 +25,19 @@ export async function scaffoldWorkspace(workspacePath: string): Promise<string[]
await backupAndWrite(path.join(workspacePath, "HEARTBEAT.md"), HEARTBEAT_MD_TEMPLATE); await backupAndWrite(path.join(workspacePath, "HEARTBEAT.md"), HEARTBEAT_MD_TEMPLATE);
filesWritten.push("HEARTBEAT.md"); filesWritten.push("HEARTBEAT.md");
// roles/default/dev.md and qa.md // projects/projects.json
const rolesDir = path.join(workspacePath, "roles", "default"); const projectsDir = path.join(workspacePath, "projects");
await fs.mkdir(rolesDir, { recursive: true }); await fs.mkdir(projectsDir, { recursive: true });
const projectsJsonPath = path.join(projectsDir, "projects.json");
const devRolePath = path.join(rolesDir, "dev.md");
if (!await fileExists(devRolePath)) {
await fs.writeFile(devRolePath, DEFAULT_DEV_INSTRUCTIONS, "utf-8");
filesWritten.push("roles/default/dev.md");
}
const qaRolePath = path.join(rolesDir, "qa.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)) { if (!await fileExists(projectsJsonPath)) {
await fs.writeFile(projectsJsonPath, JSON.stringify({ projects: {} }, null, 2) + "\n", "utf-8"); await fs.writeFile(projectsJsonPath, JSON.stringify({ projects: {} }, null, 2) + "\n", "utf-8");
filesWritten.push("memory/projects.json"); filesWritten.push("projects/projects.json");
} }
// log/ directory (audit.log created on first write)
const logDir = path.join(workspacePath, "log");
await fs.mkdir(logDir, { recursive: true });
return filesWritten; return filesWritten;
} }

View File

@@ -144,9 +144,9 @@ Workers call \`work_finish\` themselves — the label transition, state update,
The response includes \`tickPickups\` showing any tasks that were auto-dispatched. Post announcements from the tool response to Telegram. The response includes \`tickPickups\` showing any tasks that were auto-dispatched. Post announcements from the tool response to Telegram.
### Role Instructions ### Prompt 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. Workers receive role-specific instructions appended to their task message. These are loaded from \`projects/prompts/<project-name>/<role>.md\` in the workspace. \`project_register\` scaffolds these files automatically — edit them to customize worker behavior per project.
### Heartbeats ### Heartbeats

View File

@@ -20,32 +20,11 @@ import { DEFAULT_DEV_INSTRUCTIONS, DEFAULT_QA_INSTRUCTIONS } from "../templates.
import { detectContext, generateGuardrails } from "../context-guard.js"; import { detectContext, generateGuardrails } from "../context-guard.js";
/** /**
* Ensure default role files exist, then copy them into the project's role directory. * Scaffold project-specific prompt files.
* Returns true if files were created, false if they already existed. * Returns true if files were created, false if they already existed.
*/ */
async function scaffoldRoleFiles(workspaceDir: string, projectName: string): Promise<boolean> { async function scaffoldPromptFiles(workspaceDir: string, projectName: string): Promise<boolean> {
const defaultDir = path.join(workspaceDir, "roles", "default"); const projectDir = path.join(workspaceDir, "projects", "prompts", projectName);
const projectDir = path.join(workspaceDir, "roles", projectName);
// Ensure default role files exist
await fs.mkdir(defaultDir, { recursive: true });
const defaultDev = path.join(defaultDir, "dev.md");
const defaultQa = path.join(defaultDir, "qa.md");
try {
await fs.access(defaultDev);
} catch {
await fs.writeFile(defaultDev, DEFAULT_DEV_INSTRUCTIONS, "utf-8");
}
try {
await fs.access(defaultQa);
} catch {
await fs.writeFile(defaultQa, DEFAULT_QA_INSTRUCTIONS, "utf-8");
}
// Create project-specific role files (copy from default if not exist)
await fs.mkdir(projectDir, { recursive: true }); await fs.mkdir(projectDir, { recursive: true });
const projectDev = path.join(projectDir, "dev.md"); const projectDev = path.join(projectDir, "dev.md");
@@ -55,14 +34,14 @@ async function scaffoldRoleFiles(workspaceDir: string, projectName: string): Pro
try { try {
await fs.access(projectDev); await fs.access(projectDev);
} catch { } catch {
await fs.copyFile(defaultDev, projectDev); await fs.writeFile(projectDev, DEFAULT_DEV_INSTRUCTIONS, "utf-8");
created = true; created = true;
} }
try { try {
await fs.access(projectQa); await fs.access(projectQa);
} catch { } catch {
await fs.copyFile(defaultQa, projectQa); await fs.writeFile(projectQa, DEFAULT_QA_INSTRUCTIONS, "utf-8");
created = true; created = true;
} }
@@ -212,8 +191,8 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) {
await writeProjects(workspaceDir, data); await writeProjects(workspaceDir, data);
// 6. Scaffold role files // 6. Scaffold prompt files
const rolesCreated = await scaffoldRoleFiles(workspaceDir, name); const promptsCreated = await scaffoldPromptFiles(workspaceDir, name);
// 7. Audit log // 7. Audit log
await auditLog(workspaceDir, "project_register", { await auditLog(workspaceDir, "project_register", {
@@ -226,8 +205,8 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) {
}); });
// 8. Return announcement // 8. Return announcement
const rolesNote = rolesCreated ? " Role files scaffolded." : ""; const promptsNote = promptsCreated ? " Prompt files scaffolded." : "";
const announcement = `📋 Project "${name}" registered for group ${groupName}. Labels created.${rolesNote} Ready for tasks.`; const announcement = `📋 Project "${name}" registered for group ${groupName}. Labels created.${promptsNote} Ready for tasks.`;
return jsonResult({ return jsonResult({
success: true, success: true,
@@ -237,7 +216,7 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) {
baseBranch, baseBranch,
deployBranch, deployBranch,
labelsCreated: 8, labelsCreated: 8,
rolesScaffolded: rolesCreated, promptsScaffolded: promptsCreated,
announcement, announcement,
...(contextInfo && { contextInfo }), ...(contextInfo && { contextInfo }),
contextGuidance: generateGuardrails(context), contextGuidance: generateGuardrails(context),

View File

@@ -14,7 +14,7 @@ export function createSetupTool(api: OpenClawPluginApi) {
return (ctx: ToolContext) => ({ return (ctx: ToolContext) => ({
name: "setup", name: "setup",
label: "Setup", label: "Setup",
description: `Execute DevClaw setup. Creates AGENTS.md, HEARTBEAT.md, roles, memory/projects.json, and model tier config. Optionally creates a new agent with channel binding. Called after onboard collects configuration.`, description: `Execute DevClaw setup. Creates AGENTS.md, HEARTBEAT.md, projects/projects.json, and model tier config. Optionally creates a new agent with channel binding. Called after onboard collects configuration.`,
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {