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:
@@ -13,7 +13,11 @@ export {
|
||||
isValidRole,
|
||||
getRole,
|
||||
requireRole,
|
||||
// Role aliases
|
||||
ROLE_ALIASES,
|
||||
canonicalRole,
|
||||
// Level aliases
|
||||
LEVEL_ALIASES,
|
||||
canonicalLevel,
|
||||
// Levels
|
||||
getLevelsForRole,
|
||||
|
||||
@@ -29,23 +29,23 @@ import {
|
||||
describe("role registry", () => {
|
||||
it("should have all expected roles", () => {
|
||||
const ids = getAllRoleIds();
|
||||
assert.ok(ids.includes("dev"));
|
||||
assert.ok(ids.includes("qa"));
|
||||
assert.ok(ids.includes("developer"));
|
||||
assert.ok(ids.includes("tester"));
|
||||
assert.ok(ids.includes("architect"));
|
||||
});
|
||||
|
||||
it("should validate role IDs", () => {
|
||||
assert.strictEqual(isValidRole("dev"), true);
|
||||
assert.strictEqual(isValidRole("qa"), true);
|
||||
assert.strictEqual(isValidRole("developer"), true);
|
||||
assert.strictEqual(isValidRole("tester"), true);
|
||||
assert.strictEqual(isValidRole("architect"), true);
|
||||
assert.strictEqual(isValidRole("nonexistent"), false);
|
||||
});
|
||||
|
||||
it("should get role config", () => {
|
||||
const dev = getRole("dev");
|
||||
const dev = getRole("developer");
|
||||
assert.ok(dev);
|
||||
assert.strictEqual(dev.id, "dev");
|
||||
assert.strictEqual(dev.displayName, "DEV");
|
||||
assert.strictEqual(dev.id, "developer");
|
||||
assert.strictEqual(dev.displayName, "DEVELOPER");
|
||||
});
|
||||
|
||||
it("should throw for unknown role in requireRole", () => {
|
||||
@@ -55,8 +55,8 @@ describe("role registry", () => {
|
||||
|
||||
describe("levels", () => {
|
||||
it("should return levels for each role", () => {
|
||||
assert.deepStrictEqual([...getLevelsForRole("dev")], ["junior", "mid", "senior"]);
|
||||
assert.deepStrictEqual([...getLevelsForRole("qa")], ["junior", "mid", "senior"]);
|
||||
assert.deepStrictEqual([...getLevelsForRole("developer")], ["junior", "medior", "senior"]);
|
||||
assert.deepStrictEqual([...getLevelsForRole("tester")], ["junior", "medior", "senior"]);
|
||||
assert.deepStrictEqual([...getLevelsForRole("architect")], ["junior", "senior"]);
|
||||
});
|
||||
|
||||
@@ -67,42 +67,49 @@ describe("levels", () => {
|
||||
it("should return all levels", () => {
|
||||
const all = getAllLevels();
|
||||
assert.ok(all.includes("junior"));
|
||||
assert.ok(all.includes("mid"));
|
||||
assert.ok(all.includes("medior"));
|
||||
assert.ok(all.includes("senior"));
|
||||
});
|
||||
|
||||
it("should check level membership", () => {
|
||||
assert.strictEqual(isLevelForRole("junior", "dev"), true);
|
||||
assert.strictEqual(isLevelForRole("junior", "qa"), true);
|
||||
assert.strictEqual(isLevelForRole("junior", "developer"), true);
|
||||
assert.strictEqual(isLevelForRole("junior", "tester"), true);
|
||||
assert.strictEqual(isLevelForRole("junior", "architect"), true);
|
||||
assert.strictEqual(isLevelForRole("mid", "dev"), true);
|
||||
assert.strictEqual(isLevelForRole("mid", "architect"), false);
|
||||
assert.strictEqual(isLevelForRole("medior", "developer"), true);
|
||||
assert.strictEqual(isLevelForRole("medior", "architect"), false);
|
||||
});
|
||||
|
||||
it("should find role for level", () => {
|
||||
// "junior" appears in dev first (registry order)
|
||||
assert.strictEqual(roleForLevel("junior"), "dev");
|
||||
assert.strictEqual(roleForLevel("mid"), "dev");
|
||||
assert.strictEqual(roleForLevel("senior"), "dev");
|
||||
// "junior" appears in developer first (registry order)
|
||||
assert.strictEqual(roleForLevel("junior"), "developer");
|
||||
assert.strictEqual(roleForLevel("medior"), "developer");
|
||||
assert.strictEqual(roleForLevel("senior"), "developer");
|
||||
assert.strictEqual(roleForLevel("nonexistent"), undefined);
|
||||
});
|
||||
|
||||
it("should return default level", () => {
|
||||
assert.strictEqual(getDefaultLevel("dev"), "mid");
|
||||
assert.strictEqual(getDefaultLevel("qa"), "mid");
|
||||
assert.strictEqual(getDefaultLevel("developer"), "medior");
|
||||
assert.strictEqual(getDefaultLevel("tester"), "medior");
|
||||
assert.strictEqual(getDefaultLevel("architect"), "junior");
|
||||
});
|
||||
});
|
||||
|
||||
describe("level aliases", () => {
|
||||
it("should map old dev level names", () => {
|
||||
assert.strictEqual(canonicalLevel("dev", "medior"), "mid");
|
||||
assert.strictEqual(canonicalLevel("dev", "junior"), "junior");
|
||||
assert.strictEqual(canonicalLevel("dev", "senior"), "senior");
|
||||
it("should map old developer level names", () => {
|
||||
assert.strictEqual(canonicalLevel("developer", "mid"), "medior");
|
||||
assert.strictEqual(canonicalLevel("developer", "junior"), "junior");
|
||||
assert.strictEqual(canonicalLevel("developer", "senior"), "senior");
|
||||
});
|
||||
|
||||
it("should map old qa level names", () => {
|
||||
assert.strictEqual(canonicalLevel("qa", "reviewer"), "mid");
|
||||
it("should map old dev role level names", () => {
|
||||
assert.strictEqual(canonicalLevel("dev", "mid"), "medior");
|
||||
assert.strictEqual(canonicalLevel("dev", "medior"), "medior");
|
||||
});
|
||||
|
||||
it("should map old qa/tester level names", () => {
|
||||
assert.strictEqual(canonicalLevel("tester", "mid"), "medior");
|
||||
assert.strictEqual(canonicalLevel("tester", "reviewer"), "medior");
|
||||
assert.strictEqual(canonicalLevel("qa", "reviewer"), "medior");
|
||||
assert.strictEqual(canonicalLevel("qa", "tester"), "junior");
|
||||
});
|
||||
|
||||
@@ -112,63 +119,69 @@ describe("level aliases", () => {
|
||||
});
|
||||
|
||||
it("should pass through unknown levels", () => {
|
||||
assert.strictEqual(canonicalLevel("dev", "custom"), "custom");
|
||||
assert.strictEqual(canonicalLevel("developer", "custom"), "custom");
|
||||
assert.strictEqual(canonicalLevel("unknown", "whatever"), "whatever");
|
||||
});
|
||||
});
|
||||
|
||||
describe("models", () => {
|
||||
it("should return default models", () => {
|
||||
assert.strictEqual(getDefaultModel("dev", "junior"), "anthropic/claude-haiku-4-5");
|
||||
assert.strictEqual(getDefaultModel("dev", "mid"), "anthropic/claude-sonnet-4-5");
|
||||
assert.strictEqual(getDefaultModel("qa", "mid"), "anthropic/claude-sonnet-4-5");
|
||||
assert.strictEqual(getDefaultModel("developer", "junior"), "anthropic/claude-haiku-4-5");
|
||||
assert.strictEqual(getDefaultModel("developer", "medior"), "anthropic/claude-sonnet-4-5");
|
||||
assert.strictEqual(getDefaultModel("tester", "medior"), "anthropic/claude-sonnet-4-5");
|
||||
assert.strictEqual(getDefaultModel("architect", "senior"), "anthropic/claude-opus-4-5");
|
||||
});
|
||||
|
||||
it("should return all default models", () => {
|
||||
const models = getAllDefaultModels();
|
||||
assert.ok(models.dev);
|
||||
assert.ok(models.qa);
|
||||
assert.ok(models.developer);
|
||||
assert.ok(models.tester);
|
||||
assert.ok(models.architect);
|
||||
assert.strictEqual(models.dev.junior, "anthropic/claude-haiku-4-5");
|
||||
assert.strictEqual(models.developer.junior, "anthropic/claude-haiku-4-5");
|
||||
});
|
||||
|
||||
it("should resolve from config override", () => {
|
||||
const config = { models: { dev: { junior: "custom/model" } } };
|
||||
assert.strictEqual(resolveModel("dev", "junior", config), "custom/model");
|
||||
const config = { models: { developer: { junior: "custom/model" } } };
|
||||
assert.strictEqual(resolveModel("developer", "junior", config), "custom/model");
|
||||
});
|
||||
|
||||
it("should fall back to default", () => {
|
||||
assert.strictEqual(resolveModel("dev", "junior"), "anthropic/claude-haiku-4-5");
|
||||
assert.strictEqual(resolveModel("developer", "junior"), "anthropic/claude-haiku-4-5");
|
||||
});
|
||||
|
||||
it("should pass through unknown level as model ID", () => {
|
||||
assert.strictEqual(resolveModel("dev", "anthropic/claude-opus-4-5"), "anthropic/claude-opus-4-5");
|
||||
assert.strictEqual(resolveModel("developer", "anthropic/claude-opus-4-5"), "anthropic/claude-opus-4-5");
|
||||
});
|
||||
|
||||
it("should resolve old config keys via aliases", () => {
|
||||
// Old config uses "medior" key — should still resolve
|
||||
const config = { models: { dev: { medior: "custom/old-config-model" } } };
|
||||
assert.strictEqual(resolveModel("dev", "medior", config), "custom/old-config-model");
|
||||
// Old config uses "mid" key — should still resolve via alias
|
||||
const config = { models: { developer: { mid: "custom/old-config-model" } } };
|
||||
assert.strictEqual(resolveModel("developer", "mid", config), "custom/old-config-model");
|
||||
// Also works when requesting the canonical name
|
||||
assert.strictEqual(resolveModel("dev", "mid", {}), "anthropic/claude-sonnet-4-5");
|
||||
assert.strictEqual(resolveModel("developer", "medior", {}), "anthropic/claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
it("should resolve old role name config keys", () => {
|
||||
// Old config uses "dev" role key — should still resolve via role alias
|
||||
const config = { models: { dev: { junior: "custom/model" } } };
|
||||
assert.strictEqual(resolveModel("developer", "junior", config), "custom/model");
|
||||
});
|
||||
|
||||
it("should resolve old qa config keys", () => {
|
||||
const config = { models: { qa: { reviewer: "custom/qa-model" } } };
|
||||
assert.strictEqual(resolveModel("qa", "reviewer", config), "custom/qa-model");
|
||||
assert.strictEqual(resolveModel("tester", "reviewer", config), "custom/qa-model");
|
||||
});
|
||||
});
|
||||
|
||||
describe("emoji", () => {
|
||||
it("should return level emoji", () => {
|
||||
assert.strictEqual(getEmoji("dev", "junior"), "⚡");
|
||||
assert.strictEqual(getEmoji("developer", "junior"), "⚡");
|
||||
assert.strictEqual(getEmoji("architect", "senior"), "🏗️");
|
||||
});
|
||||
|
||||
it("should return fallback emoji", () => {
|
||||
assert.strictEqual(getFallbackEmoji("dev"), "🔧");
|
||||
assert.strictEqual(getFallbackEmoji("qa"), "🔍");
|
||||
assert.strictEqual(getFallbackEmoji("developer"), "🔧");
|
||||
assert.strictEqual(getFallbackEmoji("tester"), "🔍");
|
||||
assert.strictEqual(getFallbackEmoji("architect"), "🏗️");
|
||||
assert.strictEqual(getFallbackEmoji("nonexistent"), "📋");
|
||||
});
|
||||
@@ -176,32 +189,32 @@ describe("emoji", () => {
|
||||
|
||||
describe("completion results", () => {
|
||||
it("should return valid results per role", () => {
|
||||
assert.deepStrictEqual([...getCompletionResults("dev")], ["done", "blocked"]);
|
||||
assert.deepStrictEqual([...getCompletionResults("qa")], ["pass", "fail", "refine", "blocked"]);
|
||||
assert.deepStrictEqual([...getCompletionResults("developer")], ["done", "blocked"]);
|
||||
assert.deepStrictEqual([...getCompletionResults("tester")], ["pass", "fail", "refine", "blocked"]);
|
||||
assert.deepStrictEqual([...getCompletionResults("architect")], ["done", "blocked"]);
|
||||
});
|
||||
|
||||
it("should validate results", () => {
|
||||
assert.strictEqual(isValidResult("dev", "done"), true);
|
||||
assert.strictEqual(isValidResult("dev", "pass"), false);
|
||||
assert.strictEqual(isValidResult("qa", "pass"), true);
|
||||
assert.strictEqual(isValidResult("qa", "done"), false);
|
||||
assert.strictEqual(isValidResult("developer", "done"), true);
|
||||
assert.strictEqual(isValidResult("developer", "pass"), false);
|
||||
assert.strictEqual(isValidResult("tester", "pass"), true);
|
||||
assert.strictEqual(isValidResult("tester", "done"), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("session key pattern", () => {
|
||||
it("should generate pattern matching all roles", () => {
|
||||
const pattern = getSessionKeyRolePattern();
|
||||
assert.ok(pattern.includes("dev"));
|
||||
assert.ok(pattern.includes("qa"));
|
||||
assert.ok(pattern.includes("developer"));
|
||||
assert.ok(pattern.includes("tester"));
|
||||
assert.ok(pattern.includes("architect"));
|
||||
});
|
||||
|
||||
it("should work as regex", () => {
|
||||
const pattern = getSessionKeyRolePattern();
|
||||
const regex = new RegExp(`(${pattern})`);
|
||||
assert.ok(regex.test("dev"));
|
||||
assert.ok(regex.test("qa"));
|
||||
assert.ok(regex.test("developer"));
|
||||
assert.ok(regex.test("tester"));
|
||||
assert.ok(regex.test("architect"));
|
||||
assert.ok(!regex.test("nonexistent"));
|
||||
});
|
||||
|
||||
@@ -14,45 +14,45 @@
|
||||
import type { RoleConfig } from "./types.js";
|
||||
|
||||
export const ROLE_REGISTRY: Record<string, RoleConfig> = {
|
||||
dev: {
|
||||
id: "dev",
|
||||
displayName: "DEV",
|
||||
levels: ["junior", "mid", "senior"],
|
||||
defaultLevel: "mid",
|
||||
developer: {
|
||||
id: "developer",
|
||||
displayName: "DEVELOPER",
|
||||
levels: ["junior", "medior", "senior"],
|
||||
defaultLevel: "medior",
|
||||
models: {
|
||||
junior: "anthropic/claude-haiku-4-5",
|
||||
mid: "anthropic/claude-sonnet-4-5",
|
||||
medior: "anthropic/claude-sonnet-4-5",
|
||||
senior: "anthropic/claude-opus-4-5",
|
||||
},
|
||||
emoji: {
|
||||
junior: "⚡",
|
||||
mid: "🔧",
|
||||
medior: "🔧",
|
||||
senior: "🧠",
|
||||
},
|
||||
fallbackEmoji: "🔧",
|
||||
completionResults: ["done", "blocked"],
|
||||
sessionKeyPattern: "dev",
|
||||
sessionKeyPattern: "developer",
|
||||
notifications: { onStart: true, onComplete: true },
|
||||
},
|
||||
|
||||
qa: {
|
||||
id: "qa",
|
||||
displayName: "QA",
|
||||
levels: ["junior", "mid", "senior"],
|
||||
defaultLevel: "mid",
|
||||
tester: {
|
||||
id: "tester",
|
||||
displayName: "TESTER",
|
||||
levels: ["junior", "medior", "senior"],
|
||||
defaultLevel: "medior",
|
||||
models: {
|
||||
junior: "anthropic/claude-haiku-4-5",
|
||||
mid: "anthropic/claude-sonnet-4-5",
|
||||
medior: "anthropic/claude-sonnet-4-5",
|
||||
senior: "anthropic/claude-opus-4-5",
|
||||
},
|
||||
emoji: {
|
||||
junior: "⚡",
|
||||
mid: "🔍",
|
||||
medior: "🔍",
|
||||
senior: "🧠",
|
||||
},
|
||||
fallbackEmoji: "🔍",
|
||||
completionResults: ["pass", "fail", "refine", "blocked"],
|
||||
sessionKeyPattern: "qa",
|
||||
sessionKeyPattern: "tester",
|
||||
notifications: { onStart: true, onComplete: true },
|
||||
},
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import { ROLE_REGISTRY } from "./registry.js";
|
||||
import type { RoleConfig } from "./types.js";
|
||||
import type { ResolvedRoleConfig } from "../config/types.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Role IDs
|
||||
@@ -36,13 +37,31 @@ export function requireRole(role: string): RoleConfig {
|
||||
return config;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Role aliases — maps old role IDs to new canonical IDs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Maps old role IDs to canonical IDs. Used for backward compatibility. */
|
||||
export const ROLE_ALIASES: Record<string, string> = {
|
||||
dev: "developer",
|
||||
qa: "tester",
|
||||
};
|
||||
|
||||
/** Resolve a role ID, applying aliases for backward compatibility. */
|
||||
export function canonicalRole(role: string): string {
|
||||
return ROLE_ALIASES[role] ?? role;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Level aliases — maps old level names to new canonical names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LEVEL_ALIASES: Record<string, Record<string, string>> = {
|
||||
dev: { medior: "mid" },
|
||||
qa: { reviewer: "mid", tester: "junior" },
|
||||
/** Maps old level names to canonical names, per role. Used for backward compatibility. */
|
||||
export const LEVEL_ALIASES: Record<string, Record<string, string>> = {
|
||||
developer: { mid: "medior", medior: "medior" },
|
||||
dev: { mid: "medior", medior: "medior" },
|
||||
tester: { mid: "medior", reviewer: "medior", tester: "junior" },
|
||||
qa: { mid: "medior", reviewer: "medior", tester: "junior" },
|
||||
architect: { opus: "senior", sonnet: "junior" },
|
||||
};
|
||||
|
||||
@@ -105,23 +124,32 @@ export function getAllDefaultModels(): Record<string, Record<string, string>> {
|
||||
* Resolve a level to a full model ID.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. Plugin config `models.<role>.<level>` (tries canonical name, then original)
|
||||
* 2. Registry default model
|
||||
* 3. Passthrough (treat level as raw model ID)
|
||||
* 1. Plugin config `models.<role>.<level>` in openclaw.json (highest precedence)
|
||||
* 2. Resolved config from config.yaml (if provided)
|
||||
* 3. Registry default model
|
||||
* 4. Passthrough (treat level as raw model ID)
|
||||
*/
|
||||
export function resolveModel(
|
||||
role: string,
|
||||
level: string,
|
||||
pluginConfig?: Record<string, unknown>,
|
||||
resolvedRole?: ResolvedRoleConfig,
|
||||
): string {
|
||||
const canonical = canonicalLevel(role, level);
|
||||
|
||||
// 1. Plugin config override (openclaw.json) — check canonical role + old aliases
|
||||
const models = (pluginConfig as { models?: Record<string, unknown> })?.models;
|
||||
if (models && typeof models === "object") {
|
||||
const roleModels = models[role] as Record<string, string> | undefined;
|
||||
// Try canonical name first, then original (for old configs)
|
||||
// Check canonical role name, then fall back to old aliases (e.g., "dev" for "developer")
|
||||
const roleModels = (models[role] ?? models[Object.entries(ROLE_ALIASES).find(([, v]) => v === role)?.[0] ?? ""]) as Record<string, string> | undefined;
|
||||
if (roleModels?.[canonical]) return roleModels[canonical];
|
||||
if (roleModels?.[level]) return roleModels[level];
|
||||
}
|
||||
|
||||
// 2. Resolved config (config.yaml)
|
||||
if (resolvedRole?.models[canonical]) return resolvedRole.models[canonical];
|
||||
|
||||
// 3. Built-in registry default
|
||||
return getDefaultModel(role, canonical) ?? canonical;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
/** Configuration for a single worker role. */
|
||||
export type RoleConfig = {
|
||||
/** Unique role identifier (e.g., "dev", "qa", "architect"). */
|
||||
/** Unique role identifier (e.g., "developer", "tester", "architect"). */
|
||||
id: string;
|
||||
/** Human-readable display name. */
|
||||
displayName: string;
|
||||
@@ -23,7 +23,7 @@ export type RoleConfig = {
|
||||
fallbackEmoji: string;
|
||||
/** Valid completion results for this role. */
|
||||
completionResults: readonly string[];
|
||||
/** Regex pattern fragment for session key matching (e.g., "dev|qa|architect"). */
|
||||
/** Regex pattern fragment for session key matching (e.g., "developer|tester|architect"). */
|
||||
sessionKeyPattern: string;
|
||||
/** Notification config per event type. */
|
||||
notifications: {
|
||||
|
||||
Reference in New Issue
Block a user