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
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
{
@@ -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
3. Fetches issue from issue tracker, verifies correct label state
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)
7. Transitions label (e.g. `To Do``Doing`)
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
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`
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
7. Returns announcement text
## 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
{"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.
## Role instructions
## Prompt instructions
Workers receive role-specific instructions appended to their task message. `project_register` scaffolds editable files:
```
workspace/
├── roles/
│ ├── default/ ← sensible defaults (created once)
│ ├── dev.md
── qa.md
│ ├── my-webapp/ ← per-project overrides (edit to customize)
├── projects/
│ ├── projects.json ← project state
└── prompts/
── my-webapp/ ← per-project prompts (edit to customize)
│ │ ├── dev.md
│ │ └── qa.md
│ └── another-project/
│ ├── dev.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

View File

@@ -473,11 +473,11 @@ Every piece of data and where it lives:
│ task_create → create issue in tracker │
│ queue_status → read labels + read state │
│ 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)
┌────────────────────────────────┐ ┌──────────────────────────────┐
memory/projects.json │ │ OpenClaw Gateway + CLI │
projects/projects.json │ │ OpenClaw Gateway + CLI │
│ │ │ (called by plugin, not agent)│
│ Per project: │ │ │
│ dev: │ │ openclaw gateway call │
@@ -493,7 +493,7 @@ Every piece of data and where it lives:
└────────────────────────────────┘ └──────────────────────────────┘
↕ append-only
┌─────────────────────────────────────────────────────────────────┐
memory/audit.log (observability) │
log/audit.log (observability)
│ │
│ NDJSON, one line per event: │
│ 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 manifest | `~/.openclaw/extensions/devclaw/openclaw.plugin.json` | Plugin registration |
| 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 |
| Worker state | `~/.openclaw/workspace-<agent>/projects/projects.json` | Per-project DEV/QA state |
| Audit log | `~/.openclaw/workspace-<agent>/log/audit.log` | NDJSON event log |
| Session transcripts | `~/.openclaw/agents/<agent>/sessions/<uuid>.jsonl` | Conversation history per session |
| 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:
- Validates the repo and auto-detects GitHub/GitLab from remote
- 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`
- 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 migration | Plugin (`devclaw_setup` with `migrateFrom`) | Automatically moves channel-wide bindings between agents |
| 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 |
| Telegram group setup | You (once per project) | Add bot to group |
| 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` |
| 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. |
| 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 |
| Zombie detection | Plugin | `session_health` checks active vs alive |
| 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
- Document results, environment, and any notes
Role templates affected:
- `roles/default/qa.md`
- `roles/devclaw/qa.md`
Prompt templates affected:
- `projects/prompts/<project>/qa.md`
- All project-specific QA templates should follow this pattern
## Best Practices

View File

@@ -35,7 +35,7 @@ The pipeline definition replaces the hardcoded `Doing → To Test → Testing
### Open questions
- 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)?
---

View File

@@ -29,7 +29,7 @@ npm run test:ui
**What's tested:**
- First-time agent creation with default models
- 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
- Error handling: channel not configured
- Error handling: channel disabled

View File

@@ -10,7 +10,7 @@ export async function log(
event: string,
data: Record<string, unknown>,
): Promise<void> {
const filePath = join(workspaceDir, "memory", "audit.log");
const filePath = join(workspaceDir, "log", "audit.log");
const entry = JSON.stringify({
ts: new Date().toISOString(),
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.
*/
async function findProjectByGroupId(
@@ -150,7 +150,7 @@ async function findProjectByGroupId(
if (!workspaceDir) return undefined;
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 data = JSON.parse(raw) as {
projects: Record<string, { name: string }>;

View File

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

View File

@@ -72,7 +72,7 @@ export function getSessionForTier(
}
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> {

View File

@@ -8,8 +8,6 @@ import path from "node:path";
import {
AGENTS_MD_TEMPLATE,
HEARTBEAT_MD_TEMPLATE,
DEFAULT_DEV_INSTRUCTIONS,
DEFAULT_QA_INSTRUCTIONS,
} 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);
filesWritten.push("HEARTBEAT.md");
// roles/default/dev.md and qa.md
const rolesDir = path.join(workspacePath, "roles", "default");
await fs.mkdir(rolesDir, { recursive: true });
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");
// projects/projects.json
const projectsDir = path.join(workspacePath, "projects");
await fs.mkdir(projectsDir, { recursive: true });
const projectsJsonPath = path.join(projectsDir, "projects.json");
if (!await fileExists(projectsJsonPath)) {
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;
}

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.
### 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

View File

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

View File

@@ -14,7 +14,7 @@ export function createSetupTool(api: OpenClawPluginApi) {
return (ctx: ToolContext) => ({
name: "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: {
type: "object",
properties: {