From b249217bc1ae36f72f25d4df7a4f42986fd5be90 Mon Sep 17 00:00:00 2001 From: Lauren ten Hoor Date: Wed, 11 Feb 2026 01:18:13 +0800 Subject: [PATCH] feat: enhance role-tier structure for models and update related configurations --- index.ts | 23 ++++++++++++++++----- lib/dispatch.ts | 2 +- lib/setup/config.ts | 21 +++++++++++++++++-- lib/tiers.ts | 49 ++++++++++++++++++++++++++++++++++++--------- lib/tools/setup.ts | 21 ++++++++++++++----- 5 files changed, 94 insertions(+), 22 deletions(-) diff --git a/index.ts b/index.ts index 3120d50..5d09ac9 100644 --- a/index.ts +++ b/index.ts @@ -23,12 +23,25 @@ const plugin = { properties: { models: { type: "object", - description: "Model mapping per developer tier", + description: "Model mapping per role and tier", properties: { - junior: { type: "string" }, - medior: { type: "string" }, - senior: { type: "string" }, - qa: { type: "string" }, + dev: { + type: "object", + description: "Developer tier models", + properties: { + junior: { type: "string" }, + medior: { type: "string" }, + senior: { type: "string" }, + }, + }, + qa: { + type: "object", + description: "QA tier models", + properties: { + "qa-engineer": { type: "string" }, + "manual-tester": { type: "string" }, + }, + }, }, }, projectExecution: { diff --git a/lib/dispatch.ts b/lib/dispatch.ts index 4d16965..e881794 100644 --- a/lib/dispatch.ts +++ b/lib/dispatch.ts @@ -130,7 +130,7 @@ export async function dispatchTask( transitionLabel, pluginConfig, } = opts; - const model = resolveTierToModel(tier, pluginConfig); + const model = resolveTierToModel(tier, pluginConfig, role); const worker = getWorker(project, role); const existingSessionKey = getSessionForTier(worker, tier); const sessionAction = existingSessionKey ? "send" : "spawn"; diff --git a/lib/setup/config.ts b/lib/setup/config.ts index 5272dd4..668e579 100644 --- a/lib/setup/config.ts +++ b/lib/setup/config.ts @@ -5,13 +5,30 @@ */ import fs from "node:fs/promises"; import path from "node:path"; -import type { Tier } from "../tiers.js"; +import { DEV_TIERS, QA_TIERS, type Tier } from "../tiers.js"; import { HEARTBEAT_DEFAULTS } from "../services/heartbeat.js"; function openclawConfigPath(): string { return path.join(process.env.HOME ?? "/home/lauren", ".openclaw", "openclaw.json"); } +/** + * Convert flat tier map to nested role-tier structure. + */ +function buildRoleTierModels(models: Record): { dev: Record; qa: Record } { + const dev: Record = {}; + const qa: Record = {}; + + for (const tier of DEV_TIERS) { + dev[tier] = models[tier]; + } + for (const tier of QA_TIERS) { + qa[tier] = models[tier]; + } + + return { dev, qa }; +} + /** * Write DevClaw model tier config and devClawAgentIds to openclaw.json plugins section. * @@ -30,7 +47,7 @@ export async function writePluginConfig( const config = JSON.parse(await fs.readFile(configPath, "utf-8")); ensurePluginStructure(config); - config.plugins.entries.devclaw.config.models = { ...models }; + config.plugins.entries.devclaw.config.models = buildRoleTierModels(models); if (projectExecution) { config.plugins.entries.devclaw.config.projectExecution = projectExecution; diff --git a/lib/tiers.ts b/lib/tiers.ts index aed6da6..1ad7642 100644 --- a/lib/tiers.ts +++ b/lib/tiers.ts @@ -6,7 +6,7 @@ */ export const DEV_TIERS = ["junior", "medior", "senior"] as const; -export const QA_TIERS = ["qa"] as const; +export const QA_TIERS = ["qa-engineer", "manual-tester"] as const; export const ALL_TIERS = [...DEV_TIERS, ...QA_TIERS] as const; export type DevTier = (typeof DEV_TIERS)[number]; @@ -17,7 +17,21 @@ export const DEFAULT_MODELS: Record = { junior: "anthropic/claude-haiku-4-5", medior: "anthropic/claude-sonnet-4-5", senior: "anthropic/claude-opus-4-5", - qa: "anthropic/claude-sonnet-4-5", + "qa-engineer": "anthropic/claude-sonnet-4-5", + "manual-tester": "anthropic/claude-haiku-4-5", +}; + +/** Default models by role-tier structure. */ +export const DEFAULT_MODELS_BY_ROLE = { + dev: { + junior: "anthropic/claude-haiku-4-5", + medior: "anthropic/claude-sonnet-4-5", + senior: "anthropic/claude-opus-4-5", + }, + qa: { + "qa-engineer": "anthropic/claude-sonnet-4-5", + "manual-tester": "anthropic/claude-haiku-4-5", + }, }; /** Emoji used in announcements per tier. */ @@ -25,7 +39,8 @@ export const TIER_EMOJI: Record = { junior: "⚡", medior: "🔧", senior: "🧠", - qa: "🔍", + "qa-engineer": "🔍", + "manual-tester": "👀", }; /** Check if a string is a valid tier name. */ @@ -42,16 +57,31 @@ export function isDevTier(value: string): value is DevTier { * Resolve a tier name to a full model ID. * * Resolution order: - * 1. Plugin config `models` map (user overrides) - * 2. DEFAULT_MODELS (hardcoded defaults) - * 3. Treat input as raw model ID (passthrough for non-tier values) + * 1. Plugin config `models..` nested structure (user overrides) + * 2. Plugin config `models.` flat structure (backward compatibility) + * 3. DEFAULT_MODELS (hardcoded defaults) + * 4. Treat input as raw model ID (passthrough for non-tier values) */ export function resolveTierToModel( tier: string, pluginConfig?: Record, + role?: "dev" | "qa", ): string { - const models = (pluginConfig as { models?: Record })?.models; - return models?.[tier] ?? DEFAULT_MODELS[tier as Tier] ?? tier; + const models = (pluginConfig as { models?: Record })?.models; + + // Try nested role-tier structure first + if (role && models && typeof models === "object") { + const roleModels = models[role] as Record | undefined; + if (roleModels?.[tier]) return roleModels[tier]; + } + + // Fall back to flat structure for backward compatibility + if (models && typeof models === "object") { + const flatModel = (models as Record)[tier]; + if (flatModel) return flatModel; + } + + return DEFAULT_MODELS[tier as Tier] ?? tier; } /** @@ -62,5 +92,6 @@ export const TIER_MIGRATION: Record = { haiku: "junior", sonnet: "medior", opus: "senior", - grok: "qa", + grok: "qa-engineer", + qa: "qa-engineer", }; diff --git a/lib/tools/setup.ts b/lib/tools/setup.ts index ee9e3af..08c0f3e 100644 --- a/lib/tools/setup.ts +++ b/lib/tools/setup.ts @@ -22,12 +22,23 @@ export function createSetupTool(api: OpenClawPluginApi) { channelBinding: { type: "string", enum: ["telegram", "whatsapp"], description: "Channel to bind (optional, with newAgentName only)." }, migrateFrom: { type: "string", description: "Agent ID to migrate channel binding from. Check openclaw.json bindings first." }, models: { - type: "object", description: "Model overrides per tier.", + type: "object", description: "Model overrides per role and tier.", properties: { - junior: { type: "string", description: `Default: ${DEFAULT_MODELS.junior}` }, - medior: { type: "string", description: `Default: ${DEFAULT_MODELS.medior}` }, - senior: { type: "string", description: `Default: ${DEFAULT_MODELS.senior}` }, - qa: { type: "string", description: `Default: ${DEFAULT_MODELS.qa}` }, + dev: { + type: "object", description: "Developer tier models", + properties: { + junior: { type: "string", description: `Default: ${DEFAULT_MODELS.junior}` }, + medior: { type: "string", description: `Default: ${DEFAULT_MODELS.medior}` }, + senior: { type: "string", description: `Default: ${DEFAULT_MODELS.senior}` }, + }, + }, + qa: { + type: "object", description: "QA tier models", + properties: { + "qa-engineer": { type: "string", description: `Default: ${DEFAULT_MODELS["qa-engineer"]}` }, + "manual-tester": { type: "string", description: `Default: ${DEFAULT_MODELS["manual-tester"]}` }, + }, + }, }, }, projectExecution: { type: "string", enum: ["parallel", "sequential"], description: "Project execution mode. Default: parallel." },