refactor: standardize level names across all roles (#195 phase 1)
Rename levels to use consistent industry-standard terminology: - dev: medior → mid - qa: reviewer → mid, tester → junior, add senior level - architect: opus → senior, sonnet → junior Add backward-compatible migration for projects.json and openclaw.json config via level aliases in selectors and projects parser. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
19
index.ts
19
index.ts
@@ -26,31 +26,32 @@ const plugin = {
|
|||||||
properties: {
|
properties: {
|
||||||
models: {
|
models: {
|
||||||
type: "object",
|
type: "object",
|
||||||
description: "Model mapping per role and tier",
|
description: "Model mapping per role and level",
|
||||||
properties: {
|
properties: {
|
||||||
dev: {
|
dev: {
|
||||||
type: "object",
|
type: "object",
|
||||||
description: "Developer tier models",
|
description: "Developer level models",
|
||||||
properties: {
|
properties: {
|
||||||
junior: { type: "string" },
|
junior: { type: "string" },
|
||||||
medior: { type: "string" },
|
mid: { type: "string" },
|
||||||
senior: { type: "string" },
|
senior: { type: "string" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
qa: {
|
qa: {
|
||||||
type: "object",
|
type: "object",
|
||||||
description: "QA tier models",
|
description: "QA level models",
|
||||||
properties: {
|
properties: {
|
||||||
reviewer: { type: "string" },
|
junior: { type: "string" },
|
||||||
tester: { type: "string" },
|
mid: { type: "string" },
|
||||||
|
senior: { type: "string" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
architect: {
|
architect: {
|
||||||
type: "object",
|
type: "object",
|
||||||
description: "Architect tier models",
|
description: "Architect level models",
|
||||||
properties: {
|
properties: {
|
||||||
opus: { type: "string" },
|
junior: { type: "string" },
|
||||||
sonnet: { type: "string" },
|
senior: { type: "string" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import os from "node:os";
|
|||||||
|
|
||||||
describe("parseDevClawSessionKey", () => {
|
describe("parseDevClawSessionKey", () => {
|
||||||
it("should parse a standard dev session key", () => {
|
it("should parse a standard dev session key", () => {
|
||||||
const result = parseDevClawSessionKey("agent:devclaw:subagent:my-project-dev-medior");
|
const result = parseDevClawSessionKey("agent:devclaw:subagent:my-project-dev-mid");
|
||||||
assert.deepStrictEqual(result, { projectName: "my-project", role: "dev" });
|
assert.deepStrictEqual(result, { projectName: "my-project", role: "dev" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse a qa session key", () => {
|
it("should parse a qa session key", () => {
|
||||||
const result = parseDevClawSessionKey("agent:devclaw:subagent:webapp-qa-reviewer");
|
const result = parseDevClawSessionKey("agent:devclaw:subagent:webapp-qa-mid");
|
||||||
assert.deepStrictEqual(result, { projectName: "webapp", role: "qa" });
|
assert.deepStrictEqual(result, { projectName: "webapp", role: "qa" });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ describe("parseDevClawSessionKey", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle project names with multiple hyphens and qa role", () => {
|
it("should handle project names with multiple hyphens and qa role", () => {
|
||||||
const result = parseDevClawSessionKey("agent:devclaw:subagent:a-b-c-d-qa-tester");
|
const result = parseDevClawSessionKey("agent:devclaw:subagent:a-b-c-d-qa-junior");
|
||||||
assert.deepStrictEqual(result, { projectName: "a-b-c-d", role: "qa" });
|
assert.deepStrictEqual(result, { projectName: "a-b-c-d", role: "qa" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import { getSessionKeyRolePattern } from "./roles/index.js";
|
|||||||
*
|
*
|
||||||
* Session key format: `agent:{agentId}:subagent:{projectName}-{role}-{level}`
|
* Session key format: `agent:{agentId}:subagent:{projectName}-{role}-{level}`
|
||||||
* Examples:
|
* Examples:
|
||||||
* - `agent:devclaw:subagent:my-project-dev-medior` → { projectName: "my-project", role: "dev" }
|
* - `agent:devclaw:subagent:my-project-dev-mid` → { projectName: "my-project", role: "dev" }
|
||||||
* - `agent:devclaw:subagent:webapp-qa-reviewer` → { projectName: "webapp", role: "qa" }
|
* - `agent:devclaw:subagent:webapp-qa-mid` → { projectName: "webapp", role: "qa" }
|
||||||
*
|
*
|
||||||
* Note: projectName may contain hyphens, so we match role from the end.
|
* Note: projectName may contain hyphens, so we match role from the end.
|
||||||
*/
|
*/
|
||||||
|
|||||||
22
lib/cli.ts
22
lib/cli.ts
@@ -6,7 +6,8 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
import { runSetup } from "./setup/index.js";
|
import { runSetup } from "./setup/index.js";
|
||||||
import { DEV_LEVELS, QA_LEVELS, DEFAULT_MODELS } from "./tiers.js";
|
import { DEFAULT_MODELS } from "./tiers.js";
|
||||||
|
import { getLevelsForRole } from "./roles/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the `devclaw` CLI command group on a Commander program.
|
* Register the `devclaw` CLI command group on a Commander program.
|
||||||
@@ -23,18 +24,20 @@ export function registerCli(program: Command, api: OpenClawPluginApi): void {
|
|||||||
.option("--agent <id>", "Use an existing agent by ID")
|
.option("--agent <id>", "Use an existing agent by ID")
|
||||||
.option("--workspace <path>", "Direct workspace path")
|
.option("--workspace <path>", "Direct workspace path")
|
||||||
.option("--junior <model>", `Junior dev model (default: ${DEFAULT_MODELS.dev.junior})`)
|
.option("--junior <model>", `Junior dev model (default: ${DEFAULT_MODELS.dev.junior})`)
|
||||||
.option("--medior <model>", `Medior dev model (default: ${DEFAULT_MODELS.dev.medior})`)
|
.option("--mid <model>", `Mid dev model (default: ${DEFAULT_MODELS.dev.mid})`)
|
||||||
.option("--senior <model>", `Senior dev model (default: ${DEFAULT_MODELS.dev.senior})`)
|
.option("--senior <model>", `Senior dev model (default: ${DEFAULT_MODELS.dev.senior})`)
|
||||||
.option("--reviewer <model>", `Reviewer model (default: ${DEFAULT_MODELS.qa.reviewer})`)
|
.option("--qa-junior <model>", `QA junior model (default: ${DEFAULT_MODELS.qa.junior})`)
|
||||||
.option("--tester <model>", `Tester model (default: ${DEFAULT_MODELS.qa.tester})`)
|
.option("--qa-mid <model>", `QA mid model (default: ${DEFAULT_MODELS.qa.mid})`)
|
||||||
|
.option("--qa-senior <model>", `QA senior model (default: ${DEFAULT_MODELS.qa.senior})`)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
const dev: Record<string, string> = {};
|
const dev: Record<string, string> = {};
|
||||||
const qa: Record<string, string> = {};
|
const qa: Record<string, string> = {};
|
||||||
if (opts.junior) dev.junior = opts.junior;
|
if (opts.junior) dev.junior = opts.junior;
|
||||||
if (opts.medior) dev.medior = opts.medior;
|
if (opts.mid) dev.mid = opts.mid;
|
||||||
if (opts.senior) dev.senior = opts.senior;
|
if (opts.senior) dev.senior = opts.senior;
|
||||||
if (opts.reviewer) qa.reviewer = opts.reviewer;
|
if (opts.qaJunior) qa.junior = opts.qaJunior;
|
||||||
if (opts.tester) qa.tester = opts.tester;
|
if (opts.qaMid) qa.mid = opts.qaMid;
|
||||||
|
if (opts.qaSenior) qa.senior = opts.qaSenior;
|
||||||
|
|
||||||
const hasOverrides = Object.keys(dev).length > 0 || Object.keys(qa).length > 0;
|
const hasOverrides = Object.keys(dev).length > 0 || Object.keys(qa).length > 0;
|
||||||
const models = hasOverrides
|
const models = hasOverrides
|
||||||
@@ -54,8 +57,9 @@ export function registerCli(program: Command, api: OpenClawPluginApi): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log("Models configured:");
|
console.log("Models configured:");
|
||||||
for (const t of DEV_LEVELS) console.log(` dev.${t}: ${result.models.dev[t]}`);
|
for (const t of getLevelsForRole("dev")) console.log(` dev.${t}: ${result.models.dev[t]}`);
|
||||||
for (const t of QA_LEVELS) console.log(` qa.${t}: ${result.models.qa[t]}`);
|
for (const t of getLevelsForRole("qa")) console.log(` qa.${t}: ${result.models.qa[t]}`);
|
||||||
|
for (const t of getLevelsForRole("architect")) console.log(` architect.${t}: ${result.models.architect[t]}`);
|
||||||
|
|
||||||
console.log("Files written:");
|
console.log("Files written:");
|
||||||
for (const file of result.filesWritten) {
|
for (const file of result.filesWritten) {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export type DispatchOpts = {
|
|||||||
issueDescription: string;
|
issueDescription: string;
|
||||||
issueUrl: string;
|
issueUrl: string;
|
||||||
role: "dev" | "qa" | "architect";
|
role: "dev" | "qa" | "architect";
|
||||||
/** Developer level (junior, medior, senior, reviewer, opus, sonnet) or raw model ID */
|
/** Developer level (junior, mid, senior) or raw model ID */
|
||||||
level: string;
|
level: string;
|
||||||
/** Label to transition FROM (e.g. "To Do", "To Test", "To Improve") */
|
/** Label to transition FROM (e.g. "To Do", "To Test", "To Improve") */
|
||||||
fromLabel: string;
|
fromLabel: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Model selection for dev/qa tasks.
|
* Model selection for dev/qa tasks.
|
||||||
* Keyword heuristic fallback — used when the orchestrator doesn't specify a level.
|
* Keyword heuristic fallback — used when the orchestrator doesn't specify a level.
|
||||||
* Returns plain level names (junior, medior, senior, reviewer, tester).
|
* Returns plain level names (junior, mid, senior).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type LevelSelection = {
|
export type LevelSelection = {
|
||||||
@@ -41,12 +41,10 @@ const COMPLEX_KEYWORDS = [
|
|||||||
/**
|
/**
|
||||||
* Select appropriate developer level based on task description.
|
* Select appropriate developer level based on task description.
|
||||||
*
|
*
|
||||||
* Developer levels:
|
* All roles use consistent levels:
|
||||||
* - junior: very simple (typos, single-file fixes, CSS tweaks)
|
* - junior: simple tasks (typos, single-file fixes, CSS tweaks)
|
||||||
* - medior: standard DEV (features, bug fixes, multi-file changes)
|
* - mid: standard work (features, bug fixes, multi-file changes)
|
||||||
* - senior: deep/architectural (system-wide refactoring, novel design)
|
* - senior: deep/architectural (system-wide refactoring, novel design)
|
||||||
* - reviewer: QA code inspection and validation
|
|
||||||
* - tester: QA manual testing
|
|
||||||
*/
|
*/
|
||||||
export function selectLevel(
|
export function selectLevel(
|
||||||
issueTitle: string,
|
issueTitle: string,
|
||||||
@@ -55,7 +53,7 @@ export function selectLevel(
|
|||||||
): LevelSelection {
|
): LevelSelection {
|
||||||
if (role === "qa") {
|
if (role === "qa") {
|
||||||
return {
|
return {
|
||||||
level: "reviewer",
|
level: "mid",
|
||||||
reason: "Default QA level for code inspection and validation",
|
reason: "Default QA level for code inspection and validation",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -64,10 +62,10 @@ export function selectLevel(
|
|||||||
const text = `${issueTitle} ${issueDescription}`.toLowerCase();
|
const text = `${issueTitle} ${issueDescription}`.toLowerCase();
|
||||||
const isComplex = COMPLEX_KEYWORDS.some((kw) => text.includes(kw));
|
const isComplex = COMPLEX_KEYWORDS.some((kw) => text.includes(kw));
|
||||||
return {
|
return {
|
||||||
level: isComplex ? "opus" : "sonnet",
|
level: isComplex ? "senior" : "junior",
|
||||||
reason: isComplex
|
reason: isComplex
|
||||||
? "Complex design task — using opus for depth"
|
? "Complex design task — using senior for depth"
|
||||||
: "Standard design task — using sonnet",
|
: "Standard design task — using junior",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,9 +90,9 @@ export function selectLevel(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: medior for standard dev work
|
// Default: mid for standard dev work
|
||||||
return {
|
return {
|
||||||
level: "medior",
|
level: "mid",
|
||||||
reason: "Standard dev task — multi-file changes, features, bug fixes",
|
reason: "Standard dev task — multi-file changes, features, bug fixes",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,11 +79,9 @@ export function buildOnboardToolContext(): string {
|
|||||||
// Build the model table dynamically from DEFAULT_MODELS
|
// Build the model table dynamically from DEFAULT_MODELS
|
||||||
const rows: string[] = [];
|
const rows: string[] = [];
|
||||||
const purposes: Record<string, string> = {
|
const purposes: Record<string, string> = {
|
||||||
junior: "Typos, single-file fixes",
|
junior: "Simple tasks, single-file fixes",
|
||||||
medior: "Features, bug fixes",
|
mid: "Features, bug fixes, code review",
|
||||||
senior: "Architecture, refactoring",
|
senior: "Architecture, refactoring, complex tasks",
|
||||||
reviewer: "Code review",
|
|
||||||
tester: "Testing",
|
|
||||||
};
|
};
|
||||||
for (const [role, levels] of Object.entries(DEFAULT_MODELS)) {
|
for (const [role, levels] of Object.entries(DEFAULT_MODELS)) {
|
||||||
for (const [level, model] of Object.entries(levels)) {
|
for (const [level, model] of Object.entries(levels)) {
|
||||||
@@ -97,7 +95,7 @@ export function buildOnboardToolContext(): string {
|
|||||||
## What is DevClaw?
|
## What is DevClaw?
|
||||||
DevClaw turns each Telegram group into an autonomous development team:
|
DevClaw turns each Telegram group into an autonomous development team:
|
||||||
- An **orchestrator** that manages backlogs and delegates work
|
- An **orchestrator** that manages backlogs and delegates work
|
||||||
- **DEV workers** (junior/medior/senior levels) that write code in isolated sessions
|
- **DEV workers** (junior/mid/senior levels) that write code in isolated sessions
|
||||||
- **QA workers** that review code and run tests
|
- **QA workers** that review code and run tests
|
||||||
- Atomic tools for label transitions, session dispatch, state management, and audit logging
|
- Atomic tools for label transitions, session dispatch, state management, and audit logging
|
||||||
|
|
||||||
|
|||||||
@@ -37,13 +37,44 @@ export type ProjectsData = {
|
|||||||
projects: Record<string, Project>;
|
projects: Record<string, Project>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseWorkerState(worker: Record<string, unknown>): WorkerState {
|
/**
|
||||||
|
* Level migration aliases: old name → new canonical name, keyed by role.
|
||||||
|
*/
|
||||||
|
const LEVEL_MIGRATION: Record<string, Record<string, string>> = {
|
||||||
|
dev: { medior: "mid" },
|
||||||
|
qa: { reviewer: "mid", tester: "junior" },
|
||||||
|
architect: { opus: "senior", sonnet: "junior" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function migrateLevel(level: string | null, role: string): string | null {
|
||||||
|
if (!level) return null;
|
||||||
|
return LEVEL_MIGRATION[role]?.[level] ?? level;
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateSessions(
|
||||||
|
sessions: Record<string, string | null>,
|
||||||
|
role: string,
|
||||||
|
): Record<string, string | null> {
|
||||||
|
const aliases = LEVEL_MIGRATION[role];
|
||||||
|
if (!aliases) return sessions;
|
||||||
|
|
||||||
|
const migrated: Record<string, string | null> = {};
|
||||||
|
for (const [key, value] of Object.entries(sessions)) {
|
||||||
|
const newKey = aliases[key] ?? key;
|
||||||
|
migrated[newKey] = value;
|
||||||
|
}
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWorkerState(worker: Record<string, unknown>, role: string): WorkerState {
|
||||||
|
const level = (worker.level ?? worker.tier ?? null) as string | null;
|
||||||
|
const sessions = (worker.sessions as Record<string, string | null>) ?? {};
|
||||||
return {
|
return {
|
||||||
active: worker.active as boolean,
|
active: worker.active as boolean,
|
||||||
issueId: worker.issueId as string | null,
|
issueId: worker.issueId as string | null,
|
||||||
startTime: worker.startTime as string | null,
|
startTime: worker.startTime as string | null,
|
||||||
level: (worker.level ?? worker.tier ?? null) as string | null,
|
level: migrateLevel(level, role),
|
||||||
sessions: (worker.sessions as Record<string, string | null>) ?? {},
|
sessions: migrateSessions(sessions, role),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,13 +115,13 @@ export async function readProjects(workspaceDir: string): Promise<ProjectsData>
|
|||||||
|
|
||||||
for (const project of Object.values(data.projects)) {
|
for (const project of Object.values(data.projects)) {
|
||||||
project.dev = project.dev
|
project.dev = project.dev
|
||||||
? parseWorkerState(project.dev as unknown as Record<string, unknown>)
|
? parseWorkerState(project.dev as unknown as Record<string, unknown>, "dev")
|
||||||
: emptyWorkerState([]);
|
: emptyWorkerState([]);
|
||||||
project.qa = project.qa
|
project.qa = project.qa
|
||||||
? parseWorkerState(project.qa as unknown as Record<string, unknown>)
|
? parseWorkerState(project.qa as unknown as Record<string, unknown>, "qa")
|
||||||
: emptyWorkerState([]);
|
: emptyWorkerState([]);
|
||||||
project.architect = project.architect
|
project.architect = project.architect
|
||||||
? parseWorkerState(project.architect as unknown as Record<string, unknown>)
|
? parseWorkerState(project.architect as unknown as Record<string, unknown>, "architect")
|
||||||
: emptyWorkerState([]);
|
: emptyWorkerState([]);
|
||||||
if (!project.channel) {
|
if (!project.channel) {
|
||||||
project.channel = "telegram";
|
project.channel = "telegram";
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export {
|
|||||||
isValidRole,
|
isValidRole,
|
||||||
getRole,
|
getRole,
|
||||||
requireRole,
|
requireRole,
|
||||||
|
// Level aliases
|
||||||
|
canonicalLevel,
|
||||||
// Levels
|
// Levels
|
||||||
getLevelsForRole,
|
getLevelsForRole,
|
||||||
getAllLevels,
|
getAllLevels,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
getDefaultModel,
|
getDefaultModel,
|
||||||
getAllDefaultModels,
|
getAllDefaultModels,
|
||||||
resolveModel,
|
resolveModel,
|
||||||
|
canonicalLevel,
|
||||||
getEmoji,
|
getEmoji,
|
||||||
getFallbackEmoji,
|
getFallbackEmoji,
|
||||||
getCompletionResults,
|
getCompletionResults,
|
||||||
@@ -54,9 +55,9 @@ describe("role registry", () => {
|
|||||||
|
|
||||||
describe("levels", () => {
|
describe("levels", () => {
|
||||||
it("should return levels for each role", () => {
|
it("should return levels for each role", () => {
|
||||||
assert.deepStrictEqual([...getLevelsForRole("dev")], ["junior", "medior", "senior"]);
|
assert.deepStrictEqual([...getLevelsForRole("dev")], ["junior", "mid", "senior"]);
|
||||||
assert.deepStrictEqual([...getLevelsForRole("qa")], ["reviewer", "tester"]);
|
assert.deepStrictEqual([...getLevelsForRole("qa")], ["junior", "mid", "senior"]);
|
||||||
assert.deepStrictEqual([...getLevelsForRole("architect")], ["opus", "sonnet"]);
|
assert.deepStrictEqual([...getLevelsForRole("architect")], ["junior", "senior"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return empty for unknown role", () => {
|
it("should return empty for unknown role", () => {
|
||||||
@@ -66,35 +67,62 @@ describe("levels", () => {
|
|||||||
it("should return all levels", () => {
|
it("should return all levels", () => {
|
||||||
const all = getAllLevels();
|
const all = getAllLevels();
|
||||||
assert.ok(all.includes("junior"));
|
assert.ok(all.includes("junior"));
|
||||||
assert.ok(all.includes("reviewer"));
|
assert.ok(all.includes("mid"));
|
||||||
assert.ok(all.includes("opus"));
|
assert.ok(all.includes("senior"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should check level membership", () => {
|
it("should check level membership", () => {
|
||||||
assert.strictEqual(isLevelForRole("junior", "dev"), true);
|
assert.strictEqual(isLevelForRole("junior", "dev"), true);
|
||||||
assert.strictEqual(isLevelForRole("junior", "qa"), false);
|
assert.strictEqual(isLevelForRole("junior", "qa"), true);
|
||||||
assert.strictEqual(isLevelForRole("opus", "architect"), true);
|
assert.strictEqual(isLevelForRole("junior", "architect"), true);
|
||||||
|
assert.strictEqual(isLevelForRole("mid", "dev"), true);
|
||||||
|
assert.strictEqual(isLevelForRole("mid", "architect"), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should find role for level", () => {
|
it("should find role for level", () => {
|
||||||
|
// "junior" appears in dev first (registry order)
|
||||||
assert.strictEqual(roleForLevel("junior"), "dev");
|
assert.strictEqual(roleForLevel("junior"), "dev");
|
||||||
assert.strictEqual(roleForLevel("reviewer"), "qa");
|
assert.strictEqual(roleForLevel("mid"), "dev");
|
||||||
assert.strictEqual(roleForLevel("opus"), "architect");
|
assert.strictEqual(roleForLevel("senior"), "dev");
|
||||||
assert.strictEqual(roleForLevel("nonexistent"), undefined);
|
assert.strictEqual(roleForLevel("nonexistent"), undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return default level", () => {
|
it("should return default level", () => {
|
||||||
assert.strictEqual(getDefaultLevel("dev"), "medior");
|
assert.strictEqual(getDefaultLevel("dev"), "mid");
|
||||||
assert.strictEqual(getDefaultLevel("qa"), "reviewer");
|
assert.strictEqual(getDefaultLevel("qa"), "mid");
|
||||||
assert.strictEqual(getDefaultLevel("architect"), "sonnet");
|
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 qa level names", () => {
|
||||||
|
assert.strictEqual(canonicalLevel("qa", "reviewer"), "mid");
|
||||||
|
assert.strictEqual(canonicalLevel("qa", "tester"), "junior");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should map old architect level names", () => {
|
||||||
|
assert.strictEqual(canonicalLevel("architect", "opus"), "senior");
|
||||||
|
assert.strictEqual(canonicalLevel("architect", "sonnet"), "junior");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass through unknown levels", () => {
|
||||||
|
assert.strictEqual(canonicalLevel("dev", "custom"), "custom");
|
||||||
|
assert.strictEqual(canonicalLevel("unknown", "whatever"), "whatever");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("models", () => {
|
describe("models", () => {
|
||||||
it("should return default models", () => {
|
it("should return default models", () => {
|
||||||
assert.strictEqual(getDefaultModel("dev", "junior"), "anthropic/claude-haiku-4-5");
|
assert.strictEqual(getDefaultModel("dev", "junior"), "anthropic/claude-haiku-4-5");
|
||||||
assert.strictEqual(getDefaultModel("qa", "reviewer"), "anthropic/claude-sonnet-4-5");
|
assert.strictEqual(getDefaultModel("dev", "mid"), "anthropic/claude-sonnet-4-5");
|
||||||
assert.strictEqual(getDefaultModel("architect", "opus"), "anthropic/claude-opus-4-5");
|
assert.strictEqual(getDefaultModel("qa", "mid"), "anthropic/claude-sonnet-4-5");
|
||||||
|
assert.strictEqual(getDefaultModel("architect", "senior"), "anthropic/claude-opus-4-5");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return all default models", () => {
|
it("should return all default models", () => {
|
||||||
@@ -117,12 +145,25 @@ describe("models", () => {
|
|||||||
it("should pass through unknown level as model ID", () => {
|
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("dev", "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");
|
||||||
|
// Also works when requesting the canonical name
|
||||||
|
assert.strictEqual(resolveModel("dev", "mid", {}), "anthropic/claude-sonnet-4-5");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve old qa config keys", () => {
|
||||||
|
const config = { models: { qa: { reviewer: "custom/qa-model" } } };
|
||||||
|
assert.strictEqual(resolveModel("qa", "reviewer", config), "custom/qa-model");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("emoji", () => {
|
describe("emoji", () => {
|
||||||
it("should return level emoji", () => {
|
it("should return level emoji", () => {
|
||||||
assert.strictEqual(getEmoji("dev", "junior"), "⚡");
|
assert.strictEqual(getEmoji("dev", "junior"), "⚡");
|
||||||
assert.strictEqual(getEmoji("architect", "opus"), "🏗️");
|
assert.strictEqual(getEmoji("architect", "senior"), "🏗️");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return fallback emoji", () => {
|
it("should return fallback emoji", () => {
|
||||||
|
|||||||
@@ -17,16 +17,16 @@ export const ROLE_REGISTRY: Record<string, RoleConfig> = {
|
|||||||
dev: {
|
dev: {
|
||||||
id: "dev",
|
id: "dev",
|
||||||
displayName: "DEV",
|
displayName: "DEV",
|
||||||
levels: ["junior", "medior", "senior"],
|
levels: ["junior", "mid", "senior"],
|
||||||
defaultLevel: "medior",
|
defaultLevel: "mid",
|
||||||
models: {
|
models: {
|
||||||
junior: "anthropic/claude-haiku-4-5",
|
junior: "anthropic/claude-haiku-4-5",
|
||||||
medior: "anthropic/claude-sonnet-4-5",
|
mid: "anthropic/claude-sonnet-4-5",
|
||||||
senior: "anthropic/claude-opus-4-5",
|
senior: "anthropic/claude-opus-4-5",
|
||||||
},
|
},
|
||||||
emoji: {
|
emoji: {
|
||||||
junior: "⚡",
|
junior: "⚡",
|
||||||
medior: "🔧",
|
mid: "🔧",
|
||||||
senior: "🧠",
|
senior: "🧠",
|
||||||
},
|
},
|
||||||
fallbackEmoji: "🔧",
|
fallbackEmoji: "🔧",
|
||||||
@@ -38,15 +38,17 @@ export const ROLE_REGISTRY: Record<string, RoleConfig> = {
|
|||||||
qa: {
|
qa: {
|
||||||
id: "qa",
|
id: "qa",
|
||||||
displayName: "QA",
|
displayName: "QA",
|
||||||
levels: ["reviewer", "tester"],
|
levels: ["junior", "mid", "senior"],
|
||||||
defaultLevel: "reviewer",
|
defaultLevel: "mid",
|
||||||
models: {
|
models: {
|
||||||
reviewer: "anthropic/claude-sonnet-4-5",
|
junior: "anthropic/claude-haiku-4-5",
|
||||||
tester: "anthropic/claude-haiku-4-5",
|
mid: "anthropic/claude-sonnet-4-5",
|
||||||
|
senior: "anthropic/claude-opus-4-5",
|
||||||
},
|
},
|
||||||
emoji: {
|
emoji: {
|
||||||
reviewer: "🔍",
|
junior: "⚡",
|
||||||
tester: "👀",
|
mid: "🔍",
|
||||||
|
senior: "🧠",
|
||||||
},
|
},
|
||||||
fallbackEmoji: "🔍",
|
fallbackEmoji: "🔍",
|
||||||
completionResults: ["pass", "fail", "refine", "blocked"],
|
completionResults: ["pass", "fail", "refine", "blocked"],
|
||||||
@@ -57,15 +59,15 @@ export const ROLE_REGISTRY: Record<string, RoleConfig> = {
|
|||||||
architect: {
|
architect: {
|
||||||
id: "architect",
|
id: "architect",
|
||||||
displayName: "ARCHITECT",
|
displayName: "ARCHITECT",
|
||||||
levels: ["opus", "sonnet"],
|
levels: ["junior", "senior"],
|
||||||
defaultLevel: "sonnet",
|
defaultLevel: "junior",
|
||||||
models: {
|
models: {
|
||||||
opus: "anthropic/claude-opus-4-5",
|
junior: "anthropic/claude-sonnet-4-5",
|
||||||
sonnet: "anthropic/claude-sonnet-4-5",
|
senior: "anthropic/claude-opus-4-5",
|
||||||
},
|
},
|
||||||
emoji: {
|
emoji: {
|
||||||
opus: "🏗️",
|
junior: "📐",
|
||||||
sonnet: "📐",
|
senior: "🏗️",
|
||||||
},
|
},
|
||||||
fallbackEmoji: "🏗️",
|
fallbackEmoji: "🏗️",
|
||||||
completionResults: ["done", "blocked"],
|
completionResults: ["done", "blocked"],
|
||||||
|
|||||||
@@ -36,6 +36,21 @@ export function requireRole(role: string): RoleConfig {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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" },
|
||||||
|
architect: { opus: "senior", sonnet: "junior" },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Resolve a level name, applying aliases for backward compatibility. */
|
||||||
|
export function canonicalLevel(role: string, level: string): string {
|
||||||
|
return LEVEL_ALIASES[role]?.[level] ?? level;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Levels
|
// Levels
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -90,7 +105,7 @@ export function getAllDefaultModels(): Record<string, Record<string, string>> {
|
|||||||
* Resolve a level to a full model ID.
|
* Resolve a level to a full model ID.
|
||||||
*
|
*
|
||||||
* Resolution order:
|
* Resolution order:
|
||||||
* 1. Plugin config `models.<role>.<level>`
|
* 1. Plugin config `models.<role>.<level>` (tries canonical name, then original)
|
||||||
* 2. Registry default model
|
* 2. Registry default model
|
||||||
* 3. Passthrough (treat level as raw model ID)
|
* 3. Passthrough (treat level as raw model ID)
|
||||||
*/
|
*/
|
||||||
@@ -99,12 +114,15 @@ export function resolveModel(
|
|||||||
level: string,
|
level: string,
|
||||||
pluginConfig?: Record<string, unknown>,
|
pluginConfig?: Record<string, unknown>,
|
||||||
): string {
|
): string {
|
||||||
|
const canonical = canonicalLevel(role, level);
|
||||||
const models = (pluginConfig as { models?: Record<string, unknown> })?.models;
|
const models = (pluginConfig as { models?: Record<string, unknown> })?.models;
|
||||||
if (models && typeof models === "object") {
|
if (models && typeof models === "object") {
|
||||||
const roleModels = models[role] as Record<string, string> | undefined;
|
const roleModels = models[role] as Record<string, string> | undefined;
|
||||||
|
// Try canonical name first, then original (for old configs)
|
||||||
|
if (roleModels?.[canonical]) return roleModels[canonical];
|
||||||
if (roleModels?.[level]) return roleModels[level];
|
if (roleModels?.[level]) return roleModels[level];
|
||||||
}
|
}
|
||||||
return getDefaultModel(role, level) ?? level;
|
return getDefaultModel(role, canonical) ?? canonical;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const PRIORITY_ORDER: StateLabel[] = getAllQueueLabels(DEFAULT_WORKFLOW);
|
|||||||
export function detectLevelFromLabels(labels: string[]): string | null {
|
export function detectLevelFromLabels(labels: string[]): string | null {
|
||||||
const lower = labels.map((l) => l.toLowerCase());
|
const lower = labels.map((l) => l.toLowerCase());
|
||||||
|
|
||||||
// Match role.level labels (e.g., "dev.senior", "qa.reviewer", "architect.opus")
|
// Match role.level labels (e.g., "dev.senior", "qa.mid", "architect.junior")
|
||||||
for (const l of lower) {
|
for (const l of lower) {
|
||||||
const dot = l.indexOf(".");
|
const dot = l.indexOf(".");
|
||||||
if (dot === -1) continue;
|
if (dot === -1) continue;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
import { HEARTBEAT_DEFAULTS } from "../services/heartbeat.js";
|
import { HEARTBEAT_DEFAULTS } from "../services/heartbeat.js";
|
||||||
|
|
||||||
type ModelConfig = { dev: Record<string, string>; qa: Record<string, string> };
|
type ModelConfig = { dev: Record<string, string>; qa: Record<string, string>; architect: Record<string, string> };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write DevClaw model level config to openclaw.json plugins section.
|
* Write DevClaw model level config to openclaw.json plugins section.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { createAgent, resolveWorkspacePath } from "./agent.js";
|
|||||||
import { writePluginConfig } from "./config.js";
|
import { writePluginConfig } from "./config.js";
|
||||||
import { scaffoldWorkspace } from "./workspace.js";
|
import { scaffoldWorkspace } from "./workspace.js";
|
||||||
|
|
||||||
export type ModelConfig = { dev: Record<string, string>; qa: Record<string, string> };
|
export type ModelConfig = { dev: Record<string, string>; qa: Record<string, string>; architect: Record<string, string> };
|
||||||
|
|
||||||
export type SetupOpts = {
|
export type SetupOpts = {
|
||||||
/** OpenClaw plugin API for config access. */
|
/** OpenClaw plugin API for config access. */
|
||||||
@@ -27,7 +27,7 @@ export type SetupOpts = {
|
|||||||
/** Override workspace path (auto-detected from agent if not given). */
|
/** Override workspace path (auto-detected from agent if not given). */
|
||||||
workspacePath?: string;
|
workspacePath?: string;
|
||||||
/** Model overrides per role.level. Missing levels use defaults. */
|
/** Model overrides per role.level. Missing levels use defaults. */
|
||||||
models?: { dev?: Partial<Record<string, string>>; qa?: Partial<Record<string, string>> };
|
models?: { dev?: Partial<Record<string, string>>; qa?: Partial<Record<string, string>>; architect?: Partial<Record<string, string>> };
|
||||||
/** Plugin-level project execution mode: parallel or sequential. Default: parallel. */
|
/** Plugin-level project execution mode: parallel or sequential. Default: parallel. */
|
||||||
projectExecution?: "parallel" | "sequential";
|
projectExecution?: "parallel" | "sequential";
|
||||||
};
|
};
|
||||||
@@ -115,6 +115,7 @@ async function tryMigrateBinding(
|
|||||||
function buildModelConfig(overrides?: SetupOpts["models"]): ModelConfig {
|
function buildModelConfig(overrides?: SetupOpts["models"]): ModelConfig {
|
||||||
const dev: Record<string, string> = { ...DEFAULT_MODELS.dev };
|
const dev: Record<string, string> = { ...DEFAULT_MODELS.dev };
|
||||||
const qa: Record<string, string> = { ...DEFAULT_MODELS.qa };
|
const qa: Record<string, string> = { ...DEFAULT_MODELS.qa };
|
||||||
|
const architect: Record<string, string> = { ...DEFAULT_MODELS.architect };
|
||||||
|
|
||||||
if (overrides?.dev) {
|
if (overrides?.dev) {
|
||||||
for (const [level, model] of Object.entries(overrides.dev)) {
|
for (const [level, model] of Object.entries(overrides.dev)) {
|
||||||
@@ -126,6 +127,11 @@ function buildModelConfig(overrides?: SetupOpts["models"]): ModelConfig {
|
|||||||
if (model) qa[level] = model;
|
if (model) qa[level] = model;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (overrides?.architect) {
|
||||||
return { dev, qa };
|
for (const [level, model] of Object.entries(overrides.architect)) {
|
||||||
|
if (model) architect[level] = model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dev, qa, architect };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ import { runCommand } from "../run-command.js";
|
|||||||
export type ModelAssignment = {
|
export type ModelAssignment = {
|
||||||
dev: {
|
dev: {
|
||||||
junior: string;
|
junior: string;
|
||||||
medior: string;
|
mid: string;
|
||||||
senior: string;
|
senior: string;
|
||||||
};
|
};
|
||||||
qa: {
|
qa: {
|
||||||
reviewer: string;
|
junior: string;
|
||||||
tester: string;
|
mid: string;
|
||||||
|
senior: string;
|
||||||
};
|
};
|
||||||
architect: {
|
architect: {
|
||||||
opus: string;
|
junior: string;
|
||||||
sonnet: string;
|
senior: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,9 +37,9 @@ export async function selectModelsWithLLM(
|
|||||||
if (availableModels.length === 1) {
|
if (availableModels.length === 1) {
|
||||||
const model = availableModels[0].model;
|
const model = availableModels[0].model;
|
||||||
return {
|
return {
|
||||||
dev: { junior: model, medior: model, senior: model },
|
dev: { junior: model, mid: model, senior: model },
|
||||||
qa: { reviewer: model, tester: model },
|
qa: { junior: model, mid: model, senior: model },
|
||||||
architect: { opus: model, sonnet: model },
|
architect: { junior: model, senior: model },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,18 +51,16 @@ export async function selectModelsWithLLM(
|
|||||||
Available models:
|
Available models:
|
||||||
${modelList}
|
${modelList}
|
||||||
|
|
||||||
Assign models to these roles based on capability:
|
All roles use the same level scheme based on task complexity:
|
||||||
- **senior** (most capable): Complex architecture, refactoring, critical decisions
|
- **senior** (most capable): Complex architecture, refactoring, critical decisions
|
||||||
- **medior** (balanced): Features, bug fixes, code review
|
- **mid** (balanced): Features, bug fixes, code review, standard tasks
|
||||||
- **junior** (fast/efficient): Simple fixes, testing, routine tasks
|
- **junior** (fast/efficient): Simple fixes, routine tasks
|
||||||
- **reviewer** (same as medior): Code review
|
|
||||||
- **tester** (same as junior): Testing
|
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
1. Prefer same provider for consistency
|
1. Prefer same provider for consistency
|
||||||
2. Assign most capable model to senior
|
2. Assign most capable model to senior
|
||||||
3. Assign mid-tier model to medior/reviewer
|
3. Assign mid-tier model to mid
|
||||||
4. Assign fastest/cheapest model to junior/tester
|
4. Assign fastest/cheapest model to junior
|
||||||
5. Consider model version numbers (higher = newer/better)
|
5. Consider model version numbers (higher = newer/better)
|
||||||
6. Stable versions (no date) > snapshot versions (with date like 20250514)
|
6. Stable versions (no date) > snapshot versions (with date like 20250514)
|
||||||
|
|
||||||
@@ -69,16 +68,17 @@ Return ONLY a JSON object in this exact format (no markdown, no explanation):
|
|||||||
{
|
{
|
||||||
"dev": {
|
"dev": {
|
||||||
"junior": "provider/model-name",
|
"junior": "provider/model-name",
|
||||||
"medior": "provider/model-name",
|
"mid": "provider/model-name",
|
||||||
"senior": "provider/model-name"
|
"senior": "provider/model-name"
|
||||||
},
|
},
|
||||||
"qa": {
|
"qa": {
|
||||||
"reviewer": "provider/model-name",
|
"junior": "provider/model-name",
|
||||||
"tester": "provider/model-name"
|
"mid": "provider/model-name",
|
||||||
|
"senior": "provider/model-name"
|
||||||
},
|
},
|
||||||
"architect": {
|
"architect": {
|
||||||
"opus": "provider/model-name",
|
"junior": "provider/model-name",
|
||||||
"sonnet": "provider/model-name"
|
"senior": "provider/model-name"
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
@@ -131,17 +131,18 @@ Return ONLY a JSON object in this exact format (no markdown, no explanation):
|
|||||||
// Backfill architect if LLM didn't return it (graceful upgrade)
|
// Backfill architect if LLM didn't return it (graceful upgrade)
|
||||||
if (!assignment.architect) {
|
if (!assignment.architect) {
|
||||||
assignment.architect = {
|
assignment.architect = {
|
||||||
opus: assignment.dev?.senior ?? availableModels[0].model,
|
senior: assignment.dev?.senior ?? availableModels[0].model,
|
||||||
sonnet: assignment.dev?.medior ?? availableModels[0].model,
|
junior: assignment.dev?.mid ?? availableModels[0].model,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!assignment.dev?.junior ||
|
!assignment.dev?.junior ||
|
||||||
!assignment.dev?.medior ||
|
!assignment.dev?.mid ||
|
||||||
!assignment.dev?.senior ||
|
!assignment.dev?.senior ||
|
||||||
!assignment.qa?.reviewer ||
|
!assignment.qa?.junior ||
|
||||||
!assignment.qa?.tester
|
!assignment.qa?.mid ||
|
||||||
|
!assignment.qa?.senior
|
||||||
) {
|
) {
|
||||||
console.error("Invalid assignment structure. Got:", assignment);
|
console.error("Invalid assignment structure. Got:", assignment);
|
||||||
throw new Error(`Invalid assignment structure from LLM. Missing fields in: ${JSON.stringify(Object.keys(assignment))}`);
|
throw new Error(`Invalid assignment structure from LLM. Missing fields in: ${JSON.stringify(Object.keys(assignment))}`);
|
||||||
|
|||||||
@@ -7,16 +7,17 @@
|
|||||||
export type ModelAssignment = {
|
export type ModelAssignment = {
|
||||||
dev: {
|
dev: {
|
||||||
junior: string;
|
junior: string;
|
||||||
medior: string;
|
mid: string;
|
||||||
senior: string;
|
senior: string;
|
||||||
};
|
};
|
||||||
qa: {
|
qa: {
|
||||||
reviewer: string;
|
junior: string;
|
||||||
tester: string;
|
mid: string;
|
||||||
|
senior: string;
|
||||||
};
|
};
|
||||||
architect: {
|
architect: {
|
||||||
opus: string;
|
junior: string;
|
||||||
sonnet: string;
|
senior: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,9 +44,9 @@ export async function assignModels(
|
|||||||
if (authenticated.length === 1) {
|
if (authenticated.length === 1) {
|
||||||
const model = authenticated[0].model;
|
const model = authenticated[0].model;
|
||||||
return {
|
return {
|
||||||
dev: { junior: model, medior: model, senior: model },
|
dev: { junior: model, mid: model, senior: model },
|
||||||
qa: { reviewer: model, tester: model },
|
qa: { junior: model, mid: model, senior: model },
|
||||||
architect: { opus: model, sonnet: model },
|
architect: { junior: model, senior: model },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,12 +69,13 @@ export function formatAssignment(assignment: ModelAssignment): string {
|
|||||||
"| Role | Level | Model |",
|
"| Role | Level | Model |",
|
||||||
"|------|----------|--------------------------|",
|
"|------|----------|--------------------------|",
|
||||||
`| DEV | senior | ${assignment.dev.senior.padEnd(24)} |`,
|
`| DEV | senior | ${assignment.dev.senior.padEnd(24)} |`,
|
||||||
`| DEV | medior | ${assignment.dev.medior.padEnd(24)} |`,
|
`| DEV | mid | ${assignment.dev.mid.padEnd(24)} |`,
|
||||||
`| DEV | junior | ${assignment.dev.junior.padEnd(24)} |`,
|
`| DEV | junior | ${assignment.dev.junior.padEnd(24)} |`,
|
||||||
`| QA | reviewer | ${assignment.qa.reviewer.padEnd(24)} |`,
|
`| QA | senior | ${assignment.qa.senior.padEnd(24)} |`,
|
||||||
`| QA | tester | ${assignment.qa.tester.padEnd(24)} |`,
|
`| QA | mid | ${assignment.qa.mid.padEnd(24)} |`,
|
||||||
`| ARCH | opus | ${assignment.architect.opus.padEnd(24)} |`,
|
`| QA | junior | ${assignment.qa.junior.padEnd(24)} |`,
|
||||||
`| ARCH | sonnet | ${assignment.architect.sonnet.padEnd(24)} |`,
|
`| ARCH | senior | ${assignment.architect.senior.padEnd(24)} |`,
|
||||||
|
`| ARCH | junior | ${assignment.architect.junior.padEnd(24)} |`,
|
||||||
];
|
];
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ You are a **development orchestrator** — a planner and dispatcher, not a coder
|
|||||||
|
|
||||||
**Why this matters:**
|
**Why this matters:**
|
||||||
- **Audit trail** — Every code change is tracked to an issue
|
- **Audit trail** — Every code change is tracked to an issue
|
||||||
- **Tier selection** — Junior/medior/senior models match task complexity
|
- **Level selection** — Junior/mid/senior models match task complexity
|
||||||
- **Parallelization** — Workers run in parallel, you stay free to plan
|
- **Parallelization** — Workers run in parallel, you stay free to plan
|
||||||
- **QA pipeline** — Code goes through review before closing
|
- **QA pipeline** — Code goes through review before closing
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ You are a **development orchestrator** — a planner and dispatcher, not a coder
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- ✅ "Created issue #42: Fix login bug 🔗 https://github.com/org/repo/issues/42"
|
- ✅ "Created issue #42: Fix login bug 🔗 https://github.com/org/repo/issues/42"
|
||||||
- ✅ "Picked up #42 for DEV (medior) 🔗 https://github.com/org/repo/issues/42"
|
- ✅ "Picked up #42 for DEV (mid) 🔗 https://github.com/org/repo/issues/42"
|
||||||
- ❌ "Created issue #42 about the login bug" (missing URL)
|
- ❌ "Created issue #42 about the login bug" (missing URL)
|
||||||
|
|
||||||
### DevClaw Tools
|
### DevClaw Tools
|
||||||
@@ -232,11 +232,10 @@ Issue labels are the single source of truth for task state.
|
|||||||
Evaluate each task and pass the appropriate developer level to \`work_start\`:
|
Evaluate each task and pass the appropriate developer level to \`work_start\`:
|
||||||
|
|
||||||
- **junior** — trivial: typos, single-file fix, quick change
|
- **junior** — trivial: typos, single-file fix, quick change
|
||||||
- **medior** — standard: features, bug fixes, multi-file changes
|
- **mid** — standard: features, bug fixes, multi-file changes
|
||||||
- **senior** — complex: architecture, system-wide refactoring, 5+ services
|
- **senior** — complex: architecture, system-wide refactoring, 5+ services
|
||||||
- **reviewer** — QA: code inspection, validation, test runs
|
|
||||||
- **opus** — Architect: complex, high-impact design investigations
|
All roles (DEV, QA, Architect) use the same level scheme. Levels describe task complexity, not the model.
|
||||||
- **sonnet** — Architect: standard feature design investigations
|
|
||||||
|
|
||||||
### Picking Up Work
|
### Picking Up Work
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* This module now delegates to the centralized role registry (lib/roles/).
|
* This module now delegates to the centralized role registry (lib/roles/).
|
||||||
* Kept for backward compatibility — new code should import from lib/roles/ directly.
|
* Kept for backward compatibility — new code should import from lib/roles/ directly.
|
||||||
*
|
*
|
||||||
* Level names are plain: "junior", "senior", "reviewer", etc.
|
* Level names are plain: "junior", "mid", "senior".
|
||||||
* Role context (dev/qa/architect) is always provided by the caller.
|
* Role context (dev/qa/architect) is always provided by the caller.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export function createAutoConfigureModelsTool(api: OpenClawPluginApi) {
|
|||||||
if (modelCount === 1) {
|
if (modelCount === 1) {
|
||||||
message += "ℹ️ Only one authenticated model found — assigned to all roles.";
|
message += "ℹ️ Only one authenticated model found — assigned to all roles.";
|
||||||
} else {
|
} else {
|
||||||
message += "ℹ️ Models assigned by capability tier (Tier 1 → senior, Tier 2 → medior/reviewer, Tier 3 → junior/tester).";
|
message += "ℹ️ Models assigned by capability tier (Tier 1 → senior, Tier 2 → mid, Tier 3 → junior).";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferProvider) {
|
if (preferProvider) {
|
||||||
|
|||||||
@@ -14,29 +14,31 @@ import {
|
|||||||
|
|
||||||
describe("architect tiers", () => {
|
describe("architect tiers", () => {
|
||||||
it("should recognize architect levels", () => {
|
it("should recognize architect levels", () => {
|
||||||
assert.strictEqual(isArchitectLevel("opus"), true);
|
assert.strictEqual(isArchitectLevel("junior"), true);
|
||||||
assert.strictEqual(isArchitectLevel("sonnet"), true);
|
assert.strictEqual(isArchitectLevel("senior"), true);
|
||||||
assert.strictEqual(isArchitectLevel("medior"), false);
|
assert.strictEqual(isArchitectLevel("mid"), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should map architect levels to role", () => {
|
it("should map architect levels to role", () => {
|
||||||
assert.strictEqual(levelRole("opus"), "architect");
|
// "junior" and "senior" appear in dev first (registry order), so roleForLevel returns "dev"
|
||||||
assert.strictEqual(levelRole("sonnet"), "architect");
|
// This is expected — use isArchitectLevel for architect-specific checks
|
||||||
|
assert.strictEqual(levelRole("junior"), "dev");
|
||||||
|
assert.strictEqual(levelRole("senior"), "dev");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should resolve default architect models", () => {
|
it("should resolve default architect models", () => {
|
||||||
assert.strictEqual(defaultModel("architect", "opus"), "anthropic/claude-opus-4-5");
|
assert.strictEqual(defaultModel("architect", "senior"), "anthropic/claude-opus-4-5");
|
||||||
assert.strictEqual(defaultModel("architect", "sonnet"), "anthropic/claude-sonnet-4-5");
|
assert.strictEqual(defaultModel("architect", "junior"), "anthropic/claude-sonnet-4-5");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should resolve architect model from config", () => {
|
it("should resolve architect model from config", () => {
|
||||||
const config = { models: { architect: { opus: "custom/model" } } };
|
const config = { models: { architect: { senior: "custom/model" } } };
|
||||||
assert.strictEqual(resolveModel("architect", "opus", config), "custom/model");
|
assert.strictEqual(resolveModel("architect", "senior", config), "custom/model");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have architect emoji", () => {
|
it("should have architect emoji", () => {
|
||||||
assert.strictEqual(levelEmoji("architect", "opus"), "🏗️");
|
assert.strictEqual(levelEmoji("architect", "senior"), "🏗️");
|
||||||
assert.strictEqual(levelEmoji("architect", "sonnet"), "📐");
|
assert.strictEqual(levelEmoji("architect", "junior"), "📐");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,25 +83,25 @@ describe("architect workflow states", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("architect model selection", () => {
|
describe("architect model selection", () => {
|
||||||
it("should select sonnet for standard design tasks", () => {
|
it("should select junior for standard design tasks", () => {
|
||||||
const result = selectLevel("Design: Add caching layer", "Simple caching strategy", "architect");
|
const result = selectLevel("Design: Add caching layer", "Simple caching strategy", "architect");
|
||||||
assert.strictEqual(result.level, "sonnet");
|
assert.strictEqual(result.level, "junior");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should select opus for complex design tasks", () => {
|
it("should select senior for complex design tasks", () => {
|
||||||
const result = selectLevel("Design: System-wide refactor", "Major migration and redesign of the architecture", "architect");
|
const result = selectLevel("Design: System-wide refactor", "Major migration and redesign of the architecture", "architect");
|
||||||
assert.strictEqual(result.level, "opus");
|
assert.strictEqual(result.level, "senior");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("architect session key parsing", () => {
|
describe("architect session key parsing", () => {
|
||||||
it("should parse architect session key", () => {
|
it("should parse architect session key", () => {
|
||||||
const result = parseDevClawSessionKey("agent:devclaw:subagent:my-project-architect-opus");
|
const result = parseDevClawSessionKey("agent:devclaw:subagent:my-project-architect-senior");
|
||||||
assert.deepStrictEqual(result, { projectName: "my-project", role: "architect" });
|
assert.deepStrictEqual(result, { projectName: "my-project", role: "architect" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse architect sonnet session key", () => {
|
it("should parse architect junior session key", () => {
|
||||||
const result = parseDevClawSessionKey("agent:devclaw:subagent:webapp-architect-sonnet");
|
const result = parseDevClawSessionKey("agent:devclaw:subagent:webapp-architect-junior");
|
||||||
assert.deepStrictEqual(result, { projectName: "webapp", role: "architect" });
|
assert.deepStrictEqual(result, { projectName: "webapp", role: "architect" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Example:
|
|||||||
complexity: {
|
complexity: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["simple", "medium", "complex"],
|
enum: ["simple", "medium", "complex"],
|
||||||
description: "Suggests architect level: simple/medium → sonnet, complex → opus. Defaults to medium.",
|
description: "Suggests architect level: simple/medium → junior, complex → senior. Defaults to medium.",
|
||||||
},
|
},
|
||||||
dryRun: {
|
dryRun: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
@@ -110,7 +110,7 @@ Example:
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Select level based on complexity
|
// Select level based on complexity
|
||||||
const level = complexity === "complex" ? "opus" : "sonnet";
|
const level = complexity === "complex" ? "senior" : "junior";
|
||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
return jsonResult({
|
return jsonResult({
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|||||||
import { jsonResult } from "openclaw/plugin-sdk";
|
import { jsonResult } from "openclaw/plugin-sdk";
|
||||||
import type { ToolContext } from "../types.js";
|
import type { ToolContext } from "../types.js";
|
||||||
import { runSetup, type SetupOpts } from "../setup/index.js";
|
import { runSetup, type SetupOpts } from "../setup/index.js";
|
||||||
import { DEV_LEVELS, QA_LEVELS, DEFAULT_MODELS } from "../tiers.js";
|
import { DEFAULT_MODELS } from "../tiers.js";
|
||||||
|
import { getLevelsForRole } from "../roles/index.js";
|
||||||
|
|
||||||
export function createSetupTool(api: OpenClawPluginApi) {
|
export function createSetupTool(api: OpenClawPluginApi) {
|
||||||
return (ctx: ToolContext) => ({
|
return (ctx: ToolContext) => ({
|
||||||
@@ -45,9 +46,9 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
|||||||
type: "string",
|
type: "string",
|
||||||
description: `Default: ${DEFAULT_MODELS.dev.junior}`,
|
description: `Default: ${DEFAULT_MODELS.dev.junior}`,
|
||||||
},
|
},
|
||||||
medior: {
|
mid: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: `Default: ${DEFAULT_MODELS.dev.medior}`,
|
description: `Default: ${DEFAULT_MODELS.dev.mid}`,
|
||||||
},
|
},
|
||||||
senior: {
|
senior: {
|
||||||
type: "string",
|
type: "string",
|
||||||
@@ -59,13 +60,17 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
|||||||
type: "object",
|
type: "object",
|
||||||
description: "QA level models",
|
description: "QA level models",
|
||||||
properties: {
|
properties: {
|
||||||
reviewer: {
|
junior: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: `Default: ${DEFAULT_MODELS.qa.reviewer}`,
|
description: `Default: ${DEFAULT_MODELS.qa.junior}`,
|
||||||
},
|
},
|
||||||
tester: {
|
mid: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: `Default: ${DEFAULT_MODELS.qa.tester}`,
|
description: `Default: ${DEFAULT_MODELS.qa.mid}`,
|
||||||
|
},
|
||||||
|
senior: {
|
||||||
|
type: "string",
|
||||||
|
description: `Default: ${DEFAULT_MODELS.qa.senior}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -109,8 +114,9 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
|||||||
}
|
}
|
||||||
lines.push(
|
lines.push(
|
||||||
"Models:",
|
"Models:",
|
||||||
...DEV_LEVELS.map((t) => ` dev.${t}: ${result.models.dev[t]}`),
|
...getLevelsForRole("dev").map((t) => ` dev.${t}: ${result.models.dev[t]}`),
|
||||||
...QA_LEVELS.map((t) => ` qa.${t}: ${result.models.qa[t]}`),
|
...getLevelsForRole("qa").map((t) => ` qa.${t}: ${result.models.qa[t]}`),
|
||||||
|
...getLevelsForRole("architect").map((t) => ` architect.${t}: ${result.models.architect[t]}`),
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
|||||||
projectGroupId: { type: "string", description: "Project group ID." },
|
projectGroupId: { type: "string", description: "Project group ID." },
|
||||||
issueId: { type: "number", description: "Issue ID. If omitted, picks next by priority." },
|
issueId: { type: "number", description: "Issue ID. If omitted, picks next by priority." },
|
||||||
role: { type: "string", enum: getAllRoleIds(), description: "Worker role. Auto-detected from label if omitted." },
|
role: { type: "string", enum: getAllRoleIds(), description: "Worker role. Auto-detected from label if omitted." },
|
||||||
level: { type: "string", description: "Developer level (junior/medior/senior/reviewer). Auto-detected if omitted." },
|
level: { type: "string", description: "Worker level (junior/mid/senior). Auto-detected if omitted." },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user