refactor: migrate role handling from tiers to roles module
- 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.
This commit is contained in:
170
lib/config/loader.ts
Normal file
170
lib/config/loader.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user