## 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:
@@ -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,
|
||||
|
||||
@@ -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 }>;
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user