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

@@ -1,15 +1,19 @@
/**
* setup/index.ts — DevClaw setup orchestrator.
*
* Coordinates: agent creation → model config → workspace scaffolding.
* Coordinates: agent creation → plugin config → workspace scaffolding → model config.
* Used by both the `setup` tool and the `openclaw devclaw setup` CLI command.
*/
import fs from "node:fs/promises";
import path from "node:path";
import YAML from "yaml";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { getAllDefaultModels } from "../roles/index.js";
import { migrateChannelBinding } from "../binding-manager.js";
import { createAgent, resolveWorkspacePath } from "./agent.js";
import { writePluginConfig } from "./config.js";
import { scaffoldWorkspace } from "./workspace.js";
import { DATA_DIR } from "./migrate-layout.js";
export type ModelConfig = Record<string, Record<string, string>>;
@@ -49,8 +53,9 @@ export type SetupResult = {
* Run the full DevClaw setup.
*
* 1. Create agent (optional) or resolve existing workspace
* 2. Merge model config and write to openclaw.json
* 3. Write workspace files (AGENTS.md, HEARTBEAT.md, roles, memory)
* 2. Write plugin config to openclaw.json (heartbeat, tool restrictions — no models)
* 3. Write workspace files (AGENTS.md, HEARTBEAT.md, workflow.yaml, prompts)
* 4. Write model config to workflow.yaml (single source of truth)
*/
export async function runSetup(opts: SetupOpts): Promise<SetupResult> {
const warnings: string[] = [];
@@ -58,11 +63,13 @@ export async function runSetup(opts: SetupOpts): Promise<SetupResult> {
const { agentId, workspacePath, agentCreated, bindingMigrated } =
await resolveOrCreateAgent(opts, warnings);
const models = buildModelConfig(opts.models);
await writePluginConfig(opts.api, models, agentId, opts.projectExecution);
await writePluginConfig(opts.api, agentId, opts.projectExecution);
const filesWritten = await scaffoldWorkspace(workspacePath);
const models = buildModelConfig(opts.models);
await writeModelsToWorkflow(workspacePath, models);
return { agentId, agentCreated, workspacePath, models, filesWritten, warnings, bindingMigrated };
}
@@ -131,3 +138,32 @@ function buildModelConfig(overrides?: SetupOpts["models"]): ModelConfig {
return result;
}
/**
* Write model configuration to workflow.yaml (single source of truth).
* Reads the existing workflow.yaml, merges model overrides into the roles section, and writes back.
*/
async function writeModelsToWorkflow(workspacePath: string, models: ModelConfig): Promise<void> {
const workflowPath = path.join(workspacePath, DATA_DIR, "workflow.yaml");
let doc: Record<string, unknown> = {};
try {
const content = await fs.readFile(workflowPath, "utf-8");
doc = (YAML.parse(content) as Record<string, unknown>) ?? {};
} catch { /* file doesn't exist yet — start fresh */ }
// Merge models into roles section
if (!doc.roles) doc.roles = {};
const roles = doc.roles as Record<string, unknown>;
for (const [role, levels] of Object.entries(models)) {
if (!roles[role] || roles[role] === false) {
roles[role] = { models: levels };
} else {
const roleObj = roles[role] as Record<string, unknown>;
roleObj.models = levels;
}
}
await fs.writeFile(workflowPath, YAML.stringify(doc, { lineWidth: 120 }), "utf-8");
}