feat(migration): implement workspace layout migration and testing
- Added `migrate-layout.ts` to handle migration from old workspace layouts to the new `devclaw/` structure. - Introduced `migrate-layout.test.ts` for comprehensive tests covering various migration scenarios. - Updated `workspace.ts` to ensure default files are created post-migration, including `workflow.yaml` and role-specific prompts. - Refactored role instruction handling to accommodate new directory structure. - Enhanced project registration to scaffold prompt files in the new `devclaw/projects/<project>/prompts/` directory. - Adjusted setup tool descriptions and logic to reflect changes in file structure. - Updated templates to align with the new workflow configuration and role instructions.
This commit is contained in:
@@ -1,66 +1,77 @@
|
||||
/**
|
||||
* setup/workspace.ts — Workspace file scaffolding.
|
||||
*
|
||||
* Writes AGENTS.md, HEARTBEAT.md, default role instructions, and projects.json.
|
||||
* Writes AGENTS.md, HEARTBEAT.md, default role prompts, and projects.json.
|
||||
*/
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
AGENTS_MD_TEMPLATE,
|
||||
HEARTBEAT_MD_TEMPLATE,
|
||||
DEFAULT_DEV_INSTRUCTIONS,
|
||||
DEFAULT_QA_INSTRUCTIONS,
|
||||
DEFAULT_ARCHITECT_INSTRUCTIONS,
|
||||
WORKFLOW_YAML_TEMPLATE,
|
||||
DEFAULT_ROLE_INSTRUCTIONS,
|
||||
} from "../templates.js";
|
||||
import { getAllRoleIds } from "../roles/index.js";
|
||||
import { migrateWorkspaceLayout, DATA_DIR } from "./migrate-layout.js";
|
||||
|
||||
/**
|
||||
* Ensure default data files exist in the workspace.
|
||||
* Only creates files that are missing — never overwrites existing ones.
|
||||
* Called automatically after migration (via ensureWorkspaceMigrated).
|
||||
*/
|
||||
export async function ensureDefaultFiles(workspacePath: string): Promise<void> {
|
||||
const dataDir = path.join(workspacePath, DATA_DIR);
|
||||
|
||||
// devclaw/workflow.yaml
|
||||
const workflowPath = path.join(dataDir, "workflow.yaml");
|
||||
if (!await fileExists(workflowPath)) {
|
||||
await fs.mkdir(dataDir, { recursive: true });
|
||||
await fs.writeFile(workflowPath, WORKFLOW_YAML_TEMPLATE, "utf-8");
|
||||
}
|
||||
|
||||
// devclaw/projects.json
|
||||
const projectsJsonPath = path.join(dataDir, "projects.json");
|
||||
if (!await fileExists(projectsJsonPath)) {
|
||||
await fs.mkdir(dataDir, { recursive: true });
|
||||
await fs.writeFile(projectsJsonPath, JSON.stringify({ projects: {} }, null, 2) + "\n", "utf-8");
|
||||
}
|
||||
|
||||
// devclaw/projects/ directory
|
||||
await fs.mkdir(path.join(dataDir, "projects"), { recursive: true });
|
||||
|
||||
// devclaw/prompts/ — default role instructions
|
||||
const promptsDir = path.join(dataDir, "prompts");
|
||||
await fs.mkdir(promptsDir, { recursive: true });
|
||||
for (const role of getAllRoleIds()) {
|
||||
const rolePath = path.join(promptsDir, `${role}.md`);
|
||||
if (!await fileExists(rolePath)) {
|
||||
const content = DEFAULT_ROLE_INSTRUCTIONS[role] ?? `# ${role.toUpperCase()} Worker Instructions\n\nAdd role-specific instructions here.\n`;
|
||||
await fs.writeFile(rolePath, content, "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
// devclaw/log/ directory (audit.log created on first write)
|
||||
await fs.mkdir(path.join(dataDir, "log"), { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Write all workspace files for a DevClaw agent.
|
||||
* Returns the list of files that were written (skips files that already exist).
|
||||
*/
|
||||
export async function scaffoldWorkspace(workspacePath: string): Promise<string[]> {
|
||||
const filesWritten: string[] = [];
|
||||
// Migrate old layout if detected
|
||||
await migrateWorkspaceLayout(workspacePath);
|
||||
|
||||
// AGENTS.md (backup existing)
|
||||
// AGENTS.md (backup existing — stays at workspace root)
|
||||
await backupAndWrite(path.join(workspacePath, "AGENTS.md"), AGENTS_MD_TEMPLATE);
|
||||
filesWritten.push("AGENTS.md");
|
||||
|
||||
// HEARTBEAT.md
|
||||
// HEARTBEAT.md (stays at workspace root)
|
||||
await backupAndWrite(path.join(workspacePath, "HEARTBEAT.md"), HEARTBEAT_MD_TEMPLATE);
|
||||
filesWritten.push("HEARTBEAT.md");
|
||||
|
||||
// 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("projects/projects.json");
|
||||
}
|
||||
// Ensure all data-dir defaults (workflow.yaml, prompts, etc.)
|
||||
await ensureDefaultFiles(workspacePath);
|
||||
|
||||
// projects/roles/default/ (fallback role instructions)
|
||||
const defaultRolesDir = path.join(projectsDir, "roles", "default");
|
||||
await fs.mkdir(defaultRolesDir, { recursive: true });
|
||||
const devRolePath = path.join(defaultRolesDir, "dev.md");
|
||||
if (!await fileExists(devRolePath)) {
|
||||
await fs.writeFile(devRolePath, DEFAULT_DEV_INSTRUCTIONS, "utf-8");
|
||||
filesWritten.push("projects/roles/default/dev.md");
|
||||
}
|
||||
const qaRolePath = path.join(defaultRolesDir, "qa.md");
|
||||
if (!await fileExists(qaRolePath)) {
|
||||
await fs.writeFile(qaRolePath, DEFAULT_QA_INSTRUCTIONS, "utf-8");
|
||||
filesWritten.push("projects/roles/default/qa.md");
|
||||
}
|
||||
const architectRolePath = path.join(defaultRolesDir, "architect.md");
|
||||
if (!await fileExists(architectRolePath)) {
|
||||
await fs.writeFile(architectRolePath, DEFAULT_ARCHITECT_INSTRUCTIONS, "utf-8");
|
||||
filesWritten.push("projects/roles/default/architect.md");
|
||||
}
|
||||
|
||||
// log/ directory (audit.log created on first write)
|
||||
const logDir = path.join(workspacePath, "log");
|
||||
await fs.mkdir(logDir, { recursive: true });
|
||||
|
||||
return filesWritten;
|
||||
return ["AGENTS.md", "HEARTBEAT.md"];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user