- Removed the deprecated tiers.ts file and migrated all related functionality to roles/index.js. - Updated tests and tools to reflect the new role structure, replacing references to "dev", "qa", and "architect" with "developer", "tester", and "architect". - Adjusted workflow configurations and state management to accommodate the new role naming conventions. - Enhanced project registration and health check tools to support dynamic role handling. - Updated task creation, update, and completion processes to align with the new role definitions. - Improved documentation and comments to clarify role responsibilities and usage.
171 lines
5.5 KiB
TypeScript
171 lines
5.5 KiB
TypeScript
/**
|
|
* config/loader.ts — Three-layer config loading.
|
|
*
|
|
* Resolution order:
|
|
* 1. Built-in defaults (ROLE_REGISTRY + DEFAULT_WORKFLOW)
|
|
* 2. Workspace: <workspace>/projects/config.yaml
|
|
* 3. Project: <workspace>/projects/<project>/config.yaml
|
|
*
|
|
* Also supports legacy workflow.yaml files (merged into the workflow section).
|
|
*/
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import YAML from "yaml";
|
|
import { ROLE_REGISTRY } from "../roles/registry.js";
|
|
import { DEFAULT_WORKFLOW, type WorkflowConfig } from "../workflow.js";
|
|
import { mergeConfig } from "./merge.js";
|
|
import type { DevClawConfig, ResolvedConfig, ResolvedRoleConfig, RoleOverride } from "./types.js";
|
|
|
|
/**
|
|
* Load and resolve the full DevClaw config for a project.
|
|
*
|
|
* Merges: built-in → workspace config.yaml → project config.yaml.
|
|
* Also picks up legacy workflow.yaml files if no workflow section in config.yaml.
|
|
*/
|
|
export async function loadConfig(
|
|
workspaceDir: string,
|
|
projectName?: string,
|
|
): Promise<ResolvedConfig> {
|
|
const projectsDir = path.join(workspaceDir, "projects");
|
|
|
|
// Layer 1: built-in defaults
|
|
const builtIn = buildDefaultConfig();
|
|
|
|
// Layer 2: workspace config.yaml
|
|
let merged = builtIn;
|
|
const workspaceConfig = await readConfigFile(projectsDir);
|
|
if (workspaceConfig) {
|
|
merged = mergeConfig(merged, workspaceConfig);
|
|
}
|
|
|
|
// Legacy: workspace workflow.yaml (only if no workflow in config.yaml)
|
|
if (!workspaceConfig?.workflow) {
|
|
const legacyWorkflow = await readWorkflowYaml(projectsDir);
|
|
if (legacyWorkflow) {
|
|
merged = mergeConfig(merged, { workflow: legacyWorkflow });
|
|
}
|
|
}
|
|
|
|
// Layer 3: project config.yaml
|
|
if (projectName) {
|
|
const projectDir = path.join(projectsDir, projectName);
|
|
const projectConfig = await readConfigFile(projectDir);
|
|
if (projectConfig) {
|
|
merged = mergeConfig(merged, projectConfig);
|
|
}
|
|
|
|
// Legacy: project workflow.yaml
|
|
if (!projectConfig?.workflow) {
|
|
const legacyWorkflow = await readWorkflowYaml(projectDir);
|
|
if (legacyWorkflow) {
|
|
merged = mergeConfig(merged, { workflow: legacyWorkflow });
|
|
}
|
|
}
|
|
}
|
|
|
|
return resolve(merged);
|
|
}
|
|
|
|
/**
|
|
* Build the default config from the built-in ROLE_REGISTRY and DEFAULT_WORKFLOW.
|
|
*/
|
|
function buildDefaultConfig(): DevClawConfig {
|
|
const roles: Record<string, RoleOverride> = {};
|
|
for (const [id, reg] of Object.entries(ROLE_REGISTRY)) {
|
|
roles[id] = {
|
|
levels: [...reg.levels],
|
|
defaultLevel: reg.defaultLevel,
|
|
models: { ...reg.models },
|
|
emoji: { ...reg.emoji },
|
|
completionResults: [...reg.completionResults],
|
|
};
|
|
}
|
|
return { roles, workflow: DEFAULT_WORKFLOW };
|
|
}
|
|
|
|
/**
|
|
* Resolve a merged DevClawConfig into a fully-typed ResolvedConfig.
|
|
*/
|
|
function resolve(config: DevClawConfig): ResolvedConfig {
|
|
const roles: Record<string, ResolvedRoleConfig> = {};
|
|
|
|
if (config.roles) {
|
|
for (const [id, override] of Object.entries(config.roles)) {
|
|
if (override === false) {
|
|
// Disabled role — include with enabled: false for visibility
|
|
const reg = ROLE_REGISTRY[id];
|
|
roles[id] = {
|
|
levels: reg ? [...reg.levels] : [],
|
|
defaultLevel: reg?.defaultLevel ?? "",
|
|
models: reg ? { ...reg.models } : {},
|
|
emoji: reg ? { ...reg.emoji } : {},
|
|
completionResults: reg ? [...reg.completionResults] : [],
|
|
enabled: false,
|
|
};
|
|
continue;
|
|
}
|
|
|
|
const reg = ROLE_REGISTRY[id];
|
|
roles[id] = {
|
|
levels: override.levels ?? (reg ? [...reg.levels] : []),
|
|
defaultLevel: override.defaultLevel ?? reg?.defaultLevel ?? "",
|
|
models: { ...(reg?.models ?? {}), ...(override.models ?? {}) },
|
|
emoji: { ...(reg?.emoji ?? {}), ...(override.emoji ?? {}) },
|
|
completionResults: override.completionResults ?? (reg ? [...reg.completionResults] : []),
|
|
enabled: true,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Ensure all built-in roles exist even if not in config
|
|
for (const [id, reg] of Object.entries(ROLE_REGISTRY)) {
|
|
if (!roles[id]) {
|
|
roles[id] = {
|
|
levels: [...reg.levels],
|
|
defaultLevel: reg.defaultLevel,
|
|
models: { ...reg.models },
|
|
emoji: { ...reg.emoji },
|
|
completionResults: [...reg.completionResults],
|
|
enabled: true,
|
|
};
|
|
}
|
|
}
|
|
|
|
const workflow: WorkflowConfig = {
|
|
initial: config.workflow?.initial ?? DEFAULT_WORKFLOW.initial,
|
|
states: { ...DEFAULT_WORKFLOW.states, ...config.workflow?.states },
|
|
};
|
|
|
|
return { roles, workflow };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// File reading helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function readConfigFile(dir: string): Promise<DevClawConfig | null> {
|
|
try {
|
|
const content = await fs.readFile(path.join(dir, "config.yaml"), "utf-8");
|
|
return YAML.parse(content) as DevClawConfig;
|
|
} catch { /* not found */ }
|
|
return null;
|
|
}
|
|
|
|
async function readWorkflowYaml(dir: string): Promise<Partial<WorkflowConfig> | null> {
|
|
try {
|
|
const content = await fs.readFile(path.join(dir, "workflow.yaml"), "utf-8");
|
|
return YAML.parse(content) as Partial<WorkflowConfig>;
|
|
} catch { /* not found */ }
|
|
|
|
// Legacy JSON fallback
|
|
try {
|
|
const content = await fs.readFile(path.join(dir, "workflow.json"), "utf-8");
|
|
const parsed = JSON.parse(content) as
|
|
| Partial<WorkflowConfig>
|
|
| { workflow?: Partial<WorkflowConfig> };
|
|
return (parsed as any).workflow ?? parsed;
|
|
} catch { /* not found */ }
|
|
|
|
return null;
|
|
}
|