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:
Lauren ten Hoor
2026-02-15 20:19:09 +08:00
parent 89245f8ffa
commit a359ffed34
25 changed files with 1035 additions and 207 deletions

View File

@@ -31,9 +31,9 @@ describe("architect tiers", () => {
assert.strictEqual(getDefaultModel("architect", "junior"), "anthropic/claude-sonnet-4-5");
});
it("should resolve architect model from config", () => {
const config = { models: { architect: { senior: "custom/model" } } };
assert.strictEqual(resolveModel("architect", "senior", config), "custom/model");
it("should resolve architect model from resolved role config", () => {
const resolvedRole = { models: { senior: "custom/model" }, levels: ["junior", "senior"], defaultLevel: "junior", emoji: {}, completionResults: [] as string[], enabled: true };
assert.strictEqual(resolveModel("architect", "senior", resolvedRole), "custom/model");
});
it("should have architect emoji", () => {

View File

@@ -14,6 +14,7 @@ import { dispatchTask } from "../dispatch.js";
import { log as auditLog } from "../audit.js";
import { requireWorkspaceDir, resolveProject, resolveProvider, getPluginConfig } from "../tool-helpers.js";
import { loadWorkflow, getActiveLabel, getQueueLabels } from "../workflow.js";
import { loadConfig } from "../config/index.js";
import { selectLevel } from "../model-selector.js";
import { resolveModel } from "../roles/index.js";
@@ -123,7 +124,9 @@ Example:
const level = complexity === "complex"
? selectLevel(title, "system-wide " + description, role).level
: selectLevel(title, description, role).level;
const model = resolveModel(role, level, pluginConfig);
const resolvedConfig = await loadConfig(workspaceDir, project.name);
const resolvedRole = resolvedConfig.roles[role];
const model = resolveModel(role, level, resolvedRole);
if (dryRun) {
return jsonResult({

View File

@@ -26,7 +26,7 @@ export function createOnboardTool(api: OpenClawPluginApi) {
const mode = params.mode ? (params.mode as "first-run" | "reconfigure")
: configured && hasWorkspace ? "reconfigure" : "first-run";
const instructions = mode === "first-run" ? buildOnboardToolContext() : buildReconfigContext(api.pluginConfig as Record<string, unknown>);
const instructions = mode === "first-run" ? buildOnboardToolContext() : buildReconfigContext();
return jsonResult({
success: true, mode, configured, instructions,

View File

@@ -16,18 +16,19 @@ import { createProvider } from "../providers/index.js";
import { log as auditLog } from "../audit.js";
import { getAllRoleIds, getLevelsForRole } from "../roles/index.js";
import { DEFAULT_ROLE_INSTRUCTIONS } from "../templates.js";
import { DATA_DIR } from "../setup/migrate-layout.js";
/**
* Scaffold project-specific prompt files for all registered roles.
* Returns true if files were created, false if they already existed.
*/
async function scaffoldPromptFiles(workspaceDir: string, projectName: string): Promise<boolean> {
const projectDir = path.join(workspaceDir, "projects", "roles", projectName);
await fs.mkdir(projectDir, { recursive: true });
const promptsDir = path.join(workspaceDir, DATA_DIR, "projects", projectName, "prompts");
await fs.mkdir(promptsDir, { recursive: true });
let created = false;
for (const role of getAllRoleIds()) {
const filePath = path.join(projectDir, `${role}.md`);
const filePath = path.join(promptsDir, `${role}.md`);
try {
await fs.access(filePath);
} catch {

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, projects/projects.json, and model level config. Optionally creates a new agent with channel binding. Called after onboard collects configuration.`,
description: `Execute DevClaw setup. Creates AGENTS.md, HEARTBEAT.md, devclaw/projects.json, devclaw/prompts/, and model level config. Optionally creates a new agent with channel binding. Called after onboard collects configuration.`,
parameters: {
type: "object",
properties: {