feat: update role-tier structure to include prefixes for models and enhance related configurations
This commit is contained in:
20
lib/cli.ts
20
lib/cli.ts
@@ -21,18 +21,18 @@ export function registerCli(program: Command): void {
|
||||
.option("--new-agent <name>", "Create a new agent with this name")
|
||||
.option("--agent <id>", "Use an existing agent by ID")
|
||||
.option("--workspace <path>", "Direct workspace path")
|
||||
.option("--junior <model>", `Junior dev model (default: ${DEFAULT_MODELS.junior})`)
|
||||
.option("--medior <model>", `Medior dev model (default: ${DEFAULT_MODELS.medior})`)
|
||||
.option("--senior <model>", `Senior dev model (default: ${DEFAULT_MODELS.senior})`)
|
||||
.option("--reviewer <model>", `Reviewer model (default: ${DEFAULT_MODELS.reviewer})`)
|
||||
.option("--tester <model>", `Tester model (default: ${DEFAULT_MODELS.tester})`)
|
||||
.option("--junior <model>", `Junior dev model (default: ${DEFAULT_MODELS.dev.junior})`)
|
||||
.option("--medior <model>", `Medior dev model (default: ${DEFAULT_MODELS.dev.medior})`)
|
||||
.option("--senior <model>", `Senior dev model (default: ${DEFAULT_MODELS.dev.senior})`)
|
||||
.option("--reviewer <model>", `Reviewer model (default: ${DEFAULT_MODELS.qa.reviewer})`)
|
||||
.option("--tester <model>", `Tester model (default: ${DEFAULT_MODELS.qa.tester})`)
|
||||
.action(async (opts) => {
|
||||
const models: Partial<Record<Tier, string>> = {};
|
||||
if (opts.junior) models.junior = opts.junior;
|
||||
if (opts.medior) models.medior = opts.medior;
|
||||
if (opts.senior) models.senior = opts.senior;
|
||||
if (opts.reviewer) models.reviewer = opts.reviewer;
|
||||
if (opts.tester) models.tester = opts.tester;
|
||||
if (opts.junior) models["dev.junior"] = opts.junior;
|
||||
if (opts.medior) models["dev.medior"] = opts.medior;
|
||||
if (opts.senior) models["dev.senior"] = opts.senior;
|
||||
if (opts.reviewer) models["qa.reviewer"] = opts.reviewer;
|
||||
if (opts.tester) models["qa.tester"] = opts.tester;
|
||||
|
||||
const result = await runSetup({
|
||||
newAgentName: opts.newAgent,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
getSessionForTier,
|
||||
getWorker,
|
||||
} from "./projects.js";
|
||||
import { TIER_EMOJI, isTier, resolveTierToModel } from "./tiers.js";
|
||||
import { tierEmoji, resolveTierToModel } from "./tiers.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -129,7 +129,7 @@ export async function dispatchTask(
|
||||
transitionLabel, pluginConfig,
|
||||
} = opts;
|
||||
|
||||
const model = resolveTierToModel(tier, pluginConfig, role);
|
||||
const model = resolveTierToModel(tier, pluginConfig);
|
||||
const worker = getWorker(project, role);
|
||||
const existingSessionKey = getSessionForTier(worker, tier);
|
||||
const sessionAction = existingSessionKey ? "send" : "spawn";
|
||||
@@ -276,7 +276,7 @@ function buildAnnouncement(
|
||||
tier: string, role: string, sessionAction: "spawn" | "send",
|
||||
issueId: number, issueTitle: string, issueUrl: string,
|
||||
): string {
|
||||
const emoji = isTier(tier) ? TIER_EMOJI[tier] : role === "qa" ? "🔍" : "🔧";
|
||||
const emoji = tierEmoji(tier) ?? (role === "qa" ? "🔍" : "🔧");
|
||||
const actionVerb = sessionAction === "spawn" ? "Spawning" : "Sending";
|
||||
return `${emoji} ${actionVerb} ${role.toUpperCase()} (${tier}) for #${issueId}: ${issueTitle}\n🔗 ${issueUrl}`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Model selection for dev/qa tasks.
|
||||
* Keyword heuristic fallback — used when the orchestrator doesn't specify a tier.
|
||||
* Returns tier names (junior, medior, senior, qa) instead of model aliases.
|
||||
* Returns full tier names (dev.junior, dev.medior, dev.senior, qa.reviewer, qa.tester).
|
||||
*/
|
||||
|
||||
export type TierRecommendation = {
|
||||
@@ -42,10 +42,11 @@ const COMPLEX_KEYWORDS = [
|
||||
* Select appropriate developer tier based on task description.
|
||||
*
|
||||
* Developer tiers:
|
||||
* - junior: very simple (typos, single-file fixes, CSS tweaks)
|
||||
* - medior: standard DEV (features, bug fixes, multi-file changes)
|
||||
* - senior: deep/architectural (system-wide refactoring, novel design)
|
||||
* - qa: all QA tasks (code inspection, validation, test runs)
|
||||
* - dev.junior: very simple (typos, single-file fixes, CSS tweaks)
|
||||
* - dev.medior: standard DEV (features, bug fixes, multi-file changes)
|
||||
* - dev.senior: deep/architectural (system-wide refactoring, novel design)
|
||||
* - qa.reviewer: QA code inspection and validation
|
||||
* - qa.tester: QA manual testing
|
||||
*/
|
||||
export function selectTier(
|
||||
issueTitle: string,
|
||||
@@ -54,7 +55,7 @@ export function selectTier(
|
||||
): TierRecommendation {
|
||||
if (role === "qa") {
|
||||
return {
|
||||
tier: "qa",
|
||||
tier: "qa.reviewer",
|
||||
reason: "Default QA tier for code inspection and validation",
|
||||
};
|
||||
}
|
||||
@@ -66,7 +67,7 @@ export function selectTier(
|
||||
const isSimple = SIMPLE_KEYWORDS.some((kw) => text.includes(kw));
|
||||
if (isSimple && wordCount < 100) {
|
||||
return {
|
||||
tier: "junior",
|
||||
tier: "dev.junior",
|
||||
reason: `Simple task detected (keywords: ${SIMPLE_KEYWORDS.filter((kw) => text.includes(kw)).join(", ")})`,
|
||||
};
|
||||
}
|
||||
@@ -75,14 +76,14 @@ export function selectTier(
|
||||
const isComplex = COMPLEX_KEYWORDS.some((kw) => text.includes(kw));
|
||||
if (isComplex || wordCount > 500) {
|
||||
return {
|
||||
tier: "senior",
|
||||
tier: "dev.senior",
|
||||
reason: `Complex task detected (${isComplex ? "keywords: " + COMPLEX_KEYWORDS.filter((kw) => text.includes(kw)).join(", ") : "long description"})`,
|
||||
};
|
||||
}
|
||||
|
||||
// Default: medior for standard dev work
|
||||
return {
|
||||
tier: "medior",
|
||||
tier: "dev.medior",
|
||||
reason: "Standard dev task — multi-file changes, features, bug fixes",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { ALL_TIERS, DEFAULT_MODELS, type Tier } from "./tiers.js";
|
||||
import { ALL_TIERS, defaultModel } from "./tiers.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detection
|
||||
@@ -42,7 +42,7 @@ function buildModelTable(pluginConfig?: Record<string, unknown>): string {
|
||||
(pluginConfig as { models?: Record<string, string> })?.models ?? {};
|
||||
return ALL_TIERS.map(
|
||||
(t) =>
|
||||
` - **${t}**: ${models[t] || DEFAULT_MODELS[t as Tier]} (default: ${DEFAULT_MODELS[t as Tier]})`,
|
||||
` - **${t}**: ${models[t] || defaultModel(t)} (default: ${defaultModel(t)})`,
|
||||
).join("\n");
|
||||
}
|
||||
|
||||
@@ -96,10 +96,11 @@ Show the default tier-to-model mapping and ask if they want to customize:
|
||||
|
||||
| Tier | Default Model | Purpose |
|
||||
|------|---------------|---------|
|
||||
| junior | anthropic/claude-haiku-4-5 | Typos, single-file fixes |
|
||||
| medior | anthropic/claude-sonnet-4-5 | Features, bug fixes |
|
||||
| senior | anthropic/claude-opus-4-5 | Architecture, refactoring |
|
||||
| qa | anthropic/claude-sonnet-4-5 | Code review, testing |
|
||||
| dev.junior | anthropic/claude-haiku-4-5 | Typos, single-file fixes |
|
||||
| dev.medior | anthropic/claude-sonnet-4-5 | Features, bug fixes |
|
||||
| dev.senior | anthropic/claude-opus-4-5 | Architecture, refactoring |
|
||||
| qa.reviewer | anthropic/claude-sonnet-4-5 | Code review |
|
||||
| qa.tester | anthropic/claude-haiku-4-5 | Testing |
|
||||
|
||||
If the defaults are fine, proceed. If customizing, ask which tiers to change.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { DEV_TIERS, QA_TIERS, type Tier } from "../tiers.js";
|
||||
import { DEV_TIERS, QA_TIERS, tierName, type Tier } from "../tiers.js";
|
||||
import { HEARTBEAT_DEFAULTS } from "../services/heartbeat.js";
|
||||
|
||||
function openclawConfigPath(): string {
|
||||
@@ -20,10 +20,10 @@ function buildRoleTierModels(models: Record<Tier, string>): { dev: Record<string
|
||||
const qa: Record<string, string> = {};
|
||||
|
||||
for (const tier of DEV_TIERS) {
|
||||
dev[tier] = models[tier];
|
||||
dev[tierName(tier)] = models[tier];
|
||||
}
|
||||
for (const tier of QA_TIERS) {
|
||||
qa[tier] = models[tier];
|
||||
qa[tierName(tier)] = models[tier];
|
||||
}
|
||||
|
||||
return { dev, qa };
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Coordinates: agent creation → model config → workspace scaffolding.
|
||||
* Used by both the `setup` tool and the `openclaw devclaw setup` CLI command.
|
||||
*/
|
||||
import { ALL_TIERS, DEFAULT_MODELS, type Tier } from "../tiers.js";
|
||||
import { ALL_TIERS, allDefaultModels, type Tier } from "../tiers.js";
|
||||
import { migrateChannelBinding } from "../binding-manager.js";
|
||||
import { createAgent, resolveWorkspacePath } from "./agent.js";
|
||||
import { writePluginConfig } from "./config.js";
|
||||
@@ -108,7 +108,7 @@ async function tryMigrateBinding(
|
||||
}
|
||||
|
||||
function buildModelConfig(overrides?: Partial<Record<Tier, string>>): Record<Tier, string> {
|
||||
const models = { ...DEFAULT_MODELS };
|
||||
const models = allDefaultModels();
|
||||
if (overrides) {
|
||||
for (const [tier, model] of Object.entries(overrides)) {
|
||||
if (model && (ALL_TIERS as readonly string[]).includes(tier)) {
|
||||
|
||||
104
lib/tiers.ts
104
lib/tiers.ts
@@ -1,28 +1,20 @@
|
||||
/**
|
||||
* Developer tier definitions and model resolution.
|
||||
*
|
||||
* Tasks are assigned to developer tiers (junior, medior, senior, qa)
|
||||
* instead of raw model names. Each tier maps to a configurable LLM model.
|
||||
* Tier names always include the role prefix: "dev.junior", "qa.reviewer", etc.
|
||||
* This makes tier names globally unique and self-documenting.
|
||||
*/
|
||||
|
||||
export const DEV_TIERS = ["junior", "medior", "senior"] as const;
|
||||
export const QA_TIERS = ["reviewer", "tester"] as const;
|
||||
export const DEV_TIERS = ["dev.junior", "dev.medior", "dev.senior"] as const;
|
||||
export const QA_TIERS = ["qa.reviewer", "qa.tester"] as const;
|
||||
export const ALL_TIERS = [...DEV_TIERS, ...QA_TIERS] as const;
|
||||
|
||||
export type DevTier = (typeof DEV_TIERS)[number];
|
||||
export type QaTier = (typeof QA_TIERS)[number];
|
||||
export type Tier = (typeof ALL_TIERS)[number];
|
||||
|
||||
export const DEFAULT_MODELS: Record<Tier, string> = {
|
||||
junior: "anthropic/claude-haiku-4-5",
|
||||
medior: "anthropic/claude-sonnet-4-5",
|
||||
senior: "anthropic/claude-opus-4-5",
|
||||
reviewer: "anthropic/claude-sonnet-4-5",
|
||||
tester: "anthropic/claude-haiku-4-5",
|
||||
};
|
||||
|
||||
/** Default models by role-tier structure. */
|
||||
export const DEFAULT_MODELS_BY_ROLE = {
|
||||
/** Default models, nested by role. */
|
||||
export const DEFAULT_MODELS = {
|
||||
dev: {
|
||||
junior: "anthropic/claude-haiku-4-5",
|
||||
medior: "anthropic/claude-sonnet-4-5",
|
||||
@@ -34,13 +26,17 @@ export const DEFAULT_MODELS_BY_ROLE = {
|
||||
},
|
||||
};
|
||||
|
||||
/** Emoji used in announcements per tier. */
|
||||
export const TIER_EMOJI: Record<Tier, string> = {
|
||||
junior: "⚡",
|
||||
medior: "🔧",
|
||||
senior: "🧠",
|
||||
reviewer: "🔍",
|
||||
tester: "👀",
|
||||
/** Emoji used in announcements, nested by role. */
|
||||
export const TIER_EMOJI = {
|
||||
dev: {
|
||||
junior: "⚡",
|
||||
medior: "🔧",
|
||||
senior: "🧠",
|
||||
},
|
||||
qa: {
|
||||
reviewer: "🔍",
|
||||
tester: "👀",
|
||||
},
|
||||
};
|
||||
|
||||
/** Check if a string is a valid tier name. */
|
||||
@@ -48,38 +44,72 @@ export function isTier(value: string): value is Tier {
|
||||
return (ALL_TIERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/** Check if a string is a valid dev tier name. */
|
||||
/** Check if a tier belongs to the dev role. */
|
||||
export function isDevTier(value: string): value is DevTier {
|
||||
return (DEV_TIERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/** Extract the role from a tier name (e.g. "dev.junior" → "dev"). */
|
||||
export function tierRole(tier: string): "dev" | "qa" | undefined {
|
||||
if (tier.startsWith("dev.")) return "dev";
|
||||
if (tier.startsWith("qa.")) return "qa";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Extract the short name from a tier (e.g. "dev.junior" → "junior"). */
|
||||
export function tierName(tier: string): string {
|
||||
const dot = tier.indexOf(".");
|
||||
return dot >= 0 ? tier.slice(dot + 1) : tier;
|
||||
}
|
||||
|
||||
/** Look up a value from a nested role structure using a full tier name. */
|
||||
function lookupNested<T>(map: Record<string, Record<string, T>>, tier: string): T | undefined {
|
||||
const role = tierRole(tier);
|
||||
if (!role) return undefined;
|
||||
return map[role]?.[tierName(tier)];
|
||||
}
|
||||
|
||||
/** Get the default model for a tier. */
|
||||
export function defaultModel(tier: string): string | undefined {
|
||||
return lookupNested(DEFAULT_MODELS, tier);
|
||||
}
|
||||
|
||||
/** Get the emoji for a tier. */
|
||||
export function tierEmoji(tier: string): string | undefined {
|
||||
return lookupNested(TIER_EMOJI, tier);
|
||||
}
|
||||
|
||||
/** Build a flat Record<Tier, string> of all default models. */
|
||||
export function allDefaultModels(): Record<Tier, string> {
|
||||
const result = {} as Record<Tier, string>;
|
||||
for (const tier of ALL_TIERS) {
|
||||
result[tier] = defaultModel(tier)!;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a tier name to a full model ID.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. Plugin config `models.<role>.<tier>` nested structure (user overrides)
|
||||
* 2. Plugin config `models.<tier>` flat structure (backward compatibility)
|
||||
* 3. DEFAULT_MODELS (hardcoded defaults)
|
||||
* 4. Treat input as raw model ID (passthrough for non-tier values)
|
||||
* 1. Parse "role.name" → look up config `models.<role>.<name>`
|
||||
* 2. DEFAULT_MODELS[role][name]
|
||||
* 3. Passthrough (treat as raw model ID)
|
||||
*/
|
||||
export function resolveTierToModel(
|
||||
tier: string,
|
||||
pluginConfig?: Record<string, unknown>,
|
||||
role?: "dev" | "qa",
|
||||
): string {
|
||||
const models = (pluginConfig as { models?: Record<string, unknown> })?.models;
|
||||
|
||||
// Try nested role-tier structure first
|
||||
if (role && models && typeof models === "object") {
|
||||
const roleModels = models[role] as Record<string, string> | 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<string, string>)[tier];
|
||||
if (flatModel) return flatModel;
|
||||
const role = tierRole(tier);
|
||||
const name = tierName(tier);
|
||||
if (role) {
|
||||
const roleModels = models[role] as Record<string, string> | undefined;
|
||||
if (roleModels?.[name]) return roleModels[name];
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_MODELS[tier as Tier] ?? tier;
|
||||
return defaultModel(tier) ?? tier;
|
||||
}
|
||||
|
||||
@@ -43,15 +43,15 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
||||
properties: {
|
||||
junior: {
|
||||
type: "string",
|
||||
description: `Default: ${DEFAULT_MODELS.junior}`,
|
||||
description: `Default: ${DEFAULT_MODELS.dev.junior}`,
|
||||
},
|
||||
medior: {
|
||||
type: "string",
|
||||
description: `Default: ${DEFAULT_MODELS.medior}`,
|
||||
description: `Default: ${DEFAULT_MODELS.dev.medior}`,
|
||||
},
|
||||
senior: {
|
||||
type: "string",
|
||||
description: `Default: ${DEFAULT_MODELS.senior}`,
|
||||
description: `Default: ${DEFAULT_MODELS.dev.senior}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -61,11 +61,11 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
||||
properties: {
|
||||
reviewer: {
|
||||
type: "string",
|
||||
description: `Default: ${DEFAULT_MODELS.reviewer}`,
|
||||
description: `Default: ${DEFAULT_MODELS.qa.reviewer}`,
|
||||
},
|
||||
tester: {
|
||||
type: "string",
|
||||
description: `Default: ${DEFAULT_MODELS.tester}`,
|
||||
description: `Default: ${DEFAULT_MODELS.qa.tester}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,13 +26,13 @@ const INACTIVE_WORKER: WorkerState = {
|
||||
};
|
||||
|
||||
const ACTIVE_DEV: WorkerState = {
|
||||
active: true, issueId: "42", startTime: new Date().toISOString(), tier: "medior",
|
||||
sessions: { medior: "session-dev-42" },
|
||||
active: true, issueId: "42", startTime: new Date().toISOString(), tier: "dev.medior",
|
||||
sessions: { "dev.medior": "session-dev-42" },
|
||||
};
|
||||
|
||||
const ACTIVE_QA: WorkerState = {
|
||||
active: true, issueId: "42", startTime: new Date().toISOString(), tier: "qa",
|
||||
sessions: { qa: "session-qa-42" },
|
||||
active: true, issueId: "42", startTime: new Date().toISOString(), tier: "qa.reviewer",
|
||||
sessions: { "qa.reviewer": "session-qa-42" },
|
||||
};
|
||||
|
||||
function makeProject(overrides: Partial<Project> = {}): Project {
|
||||
@@ -67,10 +67,10 @@ let tmpDir: string;
|
||||
|
||||
async function setupWorkspace(projects: Record<string, Project>): Promise<string> {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "devclaw-test-"));
|
||||
const memoryDir = path.join(tmpDir, "memory");
|
||||
await fs.mkdir(memoryDir, { recursive: true });
|
||||
const projectsDir = path.join(tmpDir, "projects");
|
||||
await fs.mkdir(projectsDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(memoryDir, "projects.json"),
|
||||
path.join(projectsDir, "projects.json"),
|
||||
JSON.stringify({ projects }, null, 2) + "\n",
|
||||
"utf-8",
|
||||
);
|
||||
@@ -284,13 +284,13 @@ describe("work_heartbeat: tier assignment", () => {
|
||||
afterEach(async () => { if (tmpDir) await fs.rm(tmpDir, { recursive: true }).catch(() => {}); });
|
||||
|
||||
it("uses label-based tier when present", async () => {
|
||||
// Given: issue with "senior" label → tier should be "senior"
|
||||
// Given: issue with "dev.senior" label → tier should be "dev.senior"
|
||||
const workspaceDir = await setupWorkspace({
|
||||
"-100": makeProject({ name: "Alpha", repo: "https://github.com/test/alpha" }),
|
||||
});
|
||||
|
||||
const provider = mockProvider({
|
||||
"To Do": [{ iid: 10, title: "Refactor auth", description: "", labels: ["To Do", "senior"], web_url: "https://github.com/test/alpha/issues/10", state: "opened" }],
|
||||
"To Do": [{ iid: 10, title: "Refactor auth", description: "", labels: ["To Do", "dev.senior"], web_url: "https://github.com/test/alpha/issues/10", state: "opened" }],
|
||||
});
|
||||
|
||||
const result = await projectTick({
|
||||
@@ -299,18 +299,18 @@ describe("work_heartbeat: tier assignment", () => {
|
||||
|
||||
const pickup = result.pickups.find((p) => p.role === "dev");
|
||||
assert.ok(pickup);
|
||||
assert.strictEqual(pickup.tier, "senior", "Should use label-based tier");
|
||||
assert.strictEqual(pickup.tier, "dev.senior", "Should use label-based tier");
|
||||
});
|
||||
|
||||
it("overrides to qa tier for qa role regardless of label", async () => {
|
||||
// Given: issue with "senior" label but picked up by QA
|
||||
// Expected: tier = "qa" (QA always uses qa tier)
|
||||
it("overrides to reviewer tier for qa role regardless of label", async () => {
|
||||
// Given: issue with "dev.senior" label but picked up by QA
|
||||
// Expected: tier = "qa.reviewer" (QA always uses reviewer tier)
|
||||
const workspaceDir = await setupWorkspace({
|
||||
"-100": makeProject({ name: "Alpha", repo: "https://github.com/test/alpha" }),
|
||||
});
|
||||
|
||||
const provider = mockProvider({
|
||||
"To Test": [{ iid: 10, title: "Review auth", description: "", labels: ["To Test", "senior"], web_url: "https://github.com/test/alpha/issues/10", state: "opened" }],
|
||||
"To Test": [{ iid: 10, title: "Review auth", description: "", labels: ["To Test", "dev.senior"], web_url: "https://github.com/test/alpha/issues/10", state: "opened" }],
|
||||
});
|
||||
|
||||
const result = await projectTick({
|
||||
@@ -319,7 +319,7 @@ describe("work_heartbeat: tier assignment", () => {
|
||||
|
||||
const qaPickup = result.pickups.find((p) => p.role === "qa");
|
||||
assert.ok(qaPickup);
|
||||
assert.strictEqual(qaPickup.tier, "qa", "QA always uses qa tier regardless of issue label");
|
||||
assert.strictEqual(qaPickup.tier, "qa.reviewer", "QA always uses reviewer tier regardless of issue label");
|
||||
});
|
||||
|
||||
it("falls back to heuristic when no tier label", async () => {
|
||||
@@ -339,7 +339,7 @@ describe("work_heartbeat: tier assignment", () => {
|
||||
const pickup = result.pickups.find((p) => p.role === "dev");
|
||||
assert.ok(pickup);
|
||||
// Heuristic should select junior for a typo fix
|
||||
assert.strictEqual(pickup.tier, "junior", "Heuristic should assign junior for simple typo fix");
|
||||
assert.strictEqual(pickup.tier, "dev.junior", "Heuristic should assign junior for simple typo fix");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user