refactor: rename 'tier' to 'level' across the codebase
- Updated WorkerState type to use 'level' instead of 'tier'. - Modified functions related to worker state management, including parseWorkerState, emptyWorkerState, getSessionForLevel, activateWorker, and deactivateWorker to reflect the new terminology. - Adjusted health check logic to utilize 'level' instead of 'tier'. - Refactored tick and setup tools to accommodate the change from 'tier' to 'level', including model configuration and workspace scaffolding. - Updated tests to ensure consistency with the new 'level' terminology. - Revised documentation and comments to reflect the changes in terminology from 'tier' to 'level'.
This commit is contained in:
@@ -15,7 +15,7 @@ import { readProjects, writeProjects, emptyWorkerState } from "../projects.js";
|
||||
import { resolveRepoPath } from "../projects.js";
|
||||
import { createProvider } from "../providers/index.js";
|
||||
import { log as auditLog } from "../audit.js";
|
||||
import { DEV_TIERS, QA_TIERS } from "../tiers.js";
|
||||
import { DEV_LEVELS, QA_LEVELS } from "../tiers.js";
|
||||
import { DEFAULT_DEV_INSTRUCTIONS, DEFAULT_QA_INSTRUCTIONS } from "../templates.js";
|
||||
import { detectContext, generateGuardrails } from "../context-guard.js";
|
||||
|
||||
@@ -24,7 +24,7 @@ import { detectContext, generateGuardrails } from "../context-guard.js";
|
||||
* Returns true if files were created, false if they already existed.
|
||||
*/
|
||||
async function scaffoldPromptFiles(workspaceDir: string, projectName: string): Promise<boolean> {
|
||||
const projectDir = path.join(workspaceDir, "projects", "prompts", projectName);
|
||||
const projectDir = path.join(workspaceDir, "projects", "roles", projectName);
|
||||
await fs.mkdir(projectDir, { recursive: true });
|
||||
|
||||
const projectDev = path.join(projectDir, "dev.md");
|
||||
@@ -185,8 +185,8 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) {
|
||||
deployBranch,
|
||||
channel: context.channel,
|
||||
roleExecution,
|
||||
dev: emptyWorkerState([...DEV_TIERS]),
|
||||
qa: emptyWorkerState([...QA_TIERS]),
|
||||
dev: emptyWorkerState([...DEV_LEVELS]),
|
||||
qa: emptyWorkerState([...QA_LEVELS]),
|
||||
};
|
||||
|
||||
await writeProjects(workspaceDir, data);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/**
|
||||
* setup — Agent-driven DevClaw setup.
|
||||
*
|
||||
* Creates agent, configures model tiers, writes workspace files.
|
||||
* Creates agent, configures model levels, writes workspace files.
|
||||
* Thin wrapper around lib/setup/.
|
||||
*/
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { jsonResult } from "openclaw/plugin-sdk";
|
||||
import type { ToolContext } from "../types.js";
|
||||
import { runSetup } from "../setup/index.js";
|
||||
import { ALL_TIERS, DEFAULT_MODELS, type Tier } from "../tiers.js";
|
||||
import { runSetup, type SetupOpts } from "../setup/index.js";
|
||||
import { DEV_LEVELS, QA_LEVELS, DEFAULT_MODELS } from "../tiers.js";
|
||||
|
||||
export function createSetupTool(api: OpenClawPluginApi) {
|
||||
return (ctx: ToolContext) => ({
|
||||
name: "setup",
|
||||
label: "Setup",
|
||||
description: `Execute DevClaw setup. Creates AGENTS.md, HEARTBEAT.md, projects/projects.json, and model tier config. Optionally creates a new agent with channel binding. Called after onboard collects configuration.`,
|
||||
description: `Execute DevClaw setup. Creates AGENTS.md, HEARTBEAT.md, projects/projects.json, and model level config. Optionally creates a new agent with channel binding. Called after onboard collects configuration.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -35,11 +35,11 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
||||
},
|
||||
models: {
|
||||
type: "object",
|
||||
description: "Model overrides per role and tier.",
|
||||
description: "Model overrides per role and level.",
|
||||
properties: {
|
||||
dev: {
|
||||
type: "object",
|
||||
description: "Developer tier models",
|
||||
description: "Developer level models",
|
||||
properties: {
|
||||
junior: {
|
||||
type: "string",
|
||||
@@ -57,7 +57,7 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
||||
},
|
||||
qa: {
|
||||
type: "object",
|
||||
description: "QA tier models",
|
||||
description: "QA level models",
|
||||
properties: {
|
||||
reviewer: {
|
||||
type: "string",
|
||||
@@ -87,7 +87,7 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
||||
migrateFrom: params.migrateFrom as string | undefined,
|
||||
agentId: params.newAgentName ? undefined : ctx.agentId,
|
||||
workspacePath: params.newAgentName ? undefined : ctx.workspaceDir,
|
||||
models: params.models as Partial<Record<Tier, string>> | undefined,
|
||||
models: params.models as SetupOpts["models"],
|
||||
projectExecution: params.projectExecution as
|
||||
| "parallel"
|
||||
| "sequential"
|
||||
@@ -108,7 +108,8 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
||||
}
|
||||
lines.push(
|
||||
"Models:",
|
||||
...ALL_TIERS.map((t) => ` ${t}: ${result.models[t]}`),
|
||||
...DEV_LEVELS.map((t) => ` dev.${t}: ${result.models.dev[t]}`),
|
||||
...QA_LEVELS.map((t) => ` qa.${t}: ${result.models.qa[t]}`),
|
||||
"",
|
||||
"Files:",
|
||||
...result.filesWritten.map((f) => ` ${f}`),
|
||||
|
||||
@@ -62,8 +62,8 @@ export function createStatusTool(api: OpenClawPluginApi) {
|
||||
name: project.name,
|
||||
groupId: pid,
|
||||
roleExecution: project.roleExecution ?? "parallel",
|
||||
dev: { active: project.dev.active, issueId: project.dev.issueId, tier: project.dev.tier, startTime: project.dev.startTime },
|
||||
qa: { active: project.qa.active, issueId: project.qa.issueId, tier: project.qa.tier, startTime: project.qa.startTime },
|
||||
dev: { active: project.dev.active, issueId: project.dev.issueId, level: project.dev.level, startTime: project.dev.startTime },
|
||||
qa: { active: project.qa.active, issueId: project.qa.issueId, level: project.qa.level, startTime: project.qa.startTime },
|
||||
queue: { toImprove: count("To Improve"), toTest: count("To Test"), toDo: count("To Do") },
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -22,17 +22,17 @@ import type { StateLabel } from "../providers/provider.js";
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const INACTIVE_WORKER: WorkerState = {
|
||||
active: false, issueId: null, startTime: null, tier: null, sessions: {},
|
||||
active: false, issueId: null, startTime: null, level: null, sessions: {},
|
||||
};
|
||||
|
||||
const ACTIVE_DEV: WorkerState = {
|
||||
active: true, issueId: "42", startTime: new Date().toISOString(), tier: "dev.medior",
|
||||
sessions: { "dev.medior": "session-dev-42" },
|
||||
active: true, issueId: "42", startTime: new Date().toISOString(), level: "medior",
|
||||
sessions: { medior: "session-dev-42" },
|
||||
};
|
||||
|
||||
const ACTIVE_QA: WorkerState = {
|
||||
active: true, issueId: "42", startTime: new Date().toISOString(), tier: "qa.reviewer",
|
||||
sessions: { "qa.reviewer": "session-qa-42" },
|
||||
active: true, issueId: "42", startTime: new Date().toISOString(), level: "reviewer",
|
||||
sessions: { reviewer: "session-qa-42" },
|
||||
};
|
||||
|
||||
function makeProject(overrides: Partial<Project> = {}): Project {
|
||||
@@ -280,11 +280,11 @@ describe("work_heartbeat: worker slot guards", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("work_heartbeat: tier assignment", () => {
|
||||
describe("work_heartbeat: level assignment", () => {
|
||||
afterEach(async () => { if (tmpDir) await fs.rm(tmpDir, { recursive: true }).catch(() => {}); });
|
||||
|
||||
it("uses label-based tier when present", async () => {
|
||||
// Given: issue with "dev.senior" label → tier should be "dev.senior"
|
||||
it("uses label-based level when present", async () => {
|
||||
// Given: issue with "dev.senior" label → level should be "senior"
|
||||
const workspaceDir = await setupWorkspace({
|
||||
"-100": makeProject({ name: "Alpha", repo: "https://github.com/test/alpha" }),
|
||||
});
|
||||
@@ -299,12 +299,12 @@ describe("work_heartbeat: tier assignment", () => {
|
||||
|
||||
const pickup = result.pickups.find((p) => p.role === "dev");
|
||||
assert.ok(pickup);
|
||||
assert.strictEqual(pickup.tier, "dev.senior", "Should use label-based tier");
|
||||
assert.strictEqual(pickup.level, "senior", "Should use label-based level");
|
||||
});
|
||||
|
||||
it("overrides to reviewer tier for qa role regardless of label", async () => {
|
||||
it("overrides to reviewer level 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)
|
||||
// Expected: level = "reviewer" (QA always uses reviewer level)
|
||||
const workspaceDir = await setupWorkspace({
|
||||
"-100": makeProject({ name: "Alpha", repo: "https://github.com/test/alpha" }),
|
||||
});
|
||||
@@ -319,11 +319,11 @@ describe("work_heartbeat: tier assignment", () => {
|
||||
|
||||
const qaPickup = result.pickups.find((p) => p.role === "qa");
|
||||
assert.ok(qaPickup);
|
||||
assert.strictEqual(qaPickup.tier, "qa.reviewer", "QA always uses reviewer tier regardless of issue label");
|
||||
assert.strictEqual(qaPickup.level, "reviewer", "QA always uses reviewer level regardless of issue label");
|
||||
});
|
||||
|
||||
it("falls back to heuristic when no tier label", async () => {
|
||||
// Given: issue with no tier label → heuristic selects based on title/description
|
||||
it("falls back to heuristic when no level label", async () => {
|
||||
// Given: issue with no level label → heuristic selects based on title/description
|
||||
const workspaceDir = await setupWorkspace({
|
||||
"-100": makeProject({ name: "Alpha", repo: "https://github.com/test/alpha" }),
|
||||
});
|
||||
@@ -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, "dev.junior", "Heuristic should assign junior for simple typo fix");
|
||||
assert.strictEqual(pickup.level, "junior", "Heuristic should assign junior for simple typo fix");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -394,7 +394,7 @@ describe("work_heartbeat: TickAction output shape", () => {
|
||||
assert.strictEqual(pickup.issueTitle, "Build feature");
|
||||
assert.strictEqual(pickup.issueUrl, "https://github.com/test/alpha/issues/10");
|
||||
assert.ok(["dev", "qa"].includes(pickup.role));
|
||||
assert.ok(typeof pickup.tier === "string");
|
||||
assert.ok(typeof pickup.level === "string");
|
||||
assert.ok(["spawn", "send"].includes(pickup.sessionAction));
|
||||
assert.ok(pickup.announcement.includes("[DRY RUN]"));
|
||||
});
|
||||
|
||||
@@ -2,33 +2,33 @@
|
||||
* work_start — Pick up a task from the issue queue.
|
||||
*
|
||||
* Context-aware: ONLY works in project group chats.
|
||||
* Auto-detects: projectGroupId, role, tier, issueId.
|
||||
* Auto-detects: projectGroupId, role, level, issueId.
|
||||
* After dispatch, ticks the project queue to fill parallel slots.
|
||||
*/
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { jsonResult } from "openclaw/plugin-sdk";
|
||||
import type { ToolContext } from "../types.js";
|
||||
import type { StateLabel } from "../providers/provider.js";
|
||||
import { selectTier } from "../model-selector.js";
|
||||
import { selectLevel } from "../model-selector.js";
|
||||
import { getWorker } from "../projects.js";
|
||||
import { dispatchTask } from "../dispatch.js";
|
||||
import { notify, getNotificationConfig } from "../notify.js";
|
||||
import { findNextIssue, detectRoleFromLabel, detectTierFromLabels } from "../services/tick.js";
|
||||
import { isDevTier } from "../tiers.js";
|
||||
import { findNextIssue, detectRoleFromLabel, detectLevelFromLabels } from "../services/tick.js";
|
||||
import { isDevLevel } from "../tiers.js";
|
||||
import { requireWorkspaceDir, resolveContext, resolveProject, resolveProvider, groupOnlyError, getPluginConfig, tickAndNotify } from "../tool-helpers.js";
|
||||
|
||||
export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||
return (ctx: ToolContext) => ({
|
||||
name: "work_start",
|
||||
label: "Work Start",
|
||||
description: `Pick up a task from the issue queue. ONLY works in project group chats. Handles label transition, tier assignment, session creation, dispatch, audit, and ticks the queue to fill parallel slots.`,
|
||||
description: `Pick up a task from the issue queue. ONLY works in project group chats. Handles label transition, level assignment, session creation, dispatch, audit, and ticks the queue to fill parallel slots.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
issueId: { type: "number", description: "Issue ID. If omitted, picks next by priority." },
|
||||
role: { type: "string", enum: ["dev", "qa"], description: "Worker role. Auto-detected from label if omitted." },
|
||||
projectGroupId: { type: "string", description: "Project group ID. Auto-detected from group context." },
|
||||
tier: { type: "string", description: "Developer tier (junior/medior/senior/qa). Auto-detected if omitted." },
|
||||
level: { type: "string", description: "Developer level (junior/medior/senior/reviewer). Auto-detected if omitted." },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||
const issueIdParam = params.issueId as number | undefined;
|
||||
const roleParam = params.role as "dev" | "qa" | undefined;
|
||||
const groupIdParam = params.projectGroupId as string | undefined;
|
||||
const tierParam = params.tier as string | undefined;
|
||||
const levelParam = (params.level ?? params.tier) as string | undefined;
|
||||
const workspaceDir = requireWorkspaceDir(ctx);
|
||||
|
||||
// Context guard: group only
|
||||
@@ -76,20 +76,20 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||
if (getWorker(project, other).active) throw new Error(`Sequential roleExecution: ${other.toUpperCase()} is active`);
|
||||
}
|
||||
|
||||
// Select tier
|
||||
// Select level
|
||||
const targetLabel: StateLabel = role === "dev" ? "Doing" : "Testing";
|
||||
let selectedTier: string, tierReason: string, tierSource: string;
|
||||
if (tierParam) {
|
||||
selectedTier = tierParam; tierReason = "LLM-selected"; tierSource = "llm";
|
||||
let selectedLevel: string, levelReason: string, levelSource: string;
|
||||
if (levelParam) {
|
||||
selectedLevel = levelParam; levelReason = "LLM-selected"; levelSource = "llm";
|
||||
} else {
|
||||
const labelTier = detectTierFromLabels(issue.labels);
|
||||
if (labelTier) {
|
||||
if (role === "qa" && isDevTier(labelTier)) { const s = selectTier(issue.title, issue.description ?? "", role); selectedTier = s.tier; tierReason = `QA overrides dev tier "${labelTier}"`; tierSource = "role-override"; }
|
||||
else if (role === "dev" && !isDevTier(labelTier)) { const s = selectTier(issue.title, issue.description ?? "", role); selectedTier = s.tier; tierReason = s.reason; tierSource = "heuristic"; }
|
||||
else { selectedTier = labelTier; tierReason = `Label: "${labelTier}"`; tierSource = "label"; }
|
||||
const labelLevel = detectLevelFromLabels(issue.labels);
|
||||
if (labelLevel) {
|
||||
if (role === "qa" && isDevLevel(labelLevel)) { const s = selectLevel(issue.title, issue.description ?? "", role); selectedLevel = s.level; levelReason = `QA overrides dev level "${labelLevel}"`; levelSource = "role-override"; }
|
||||
else if (role === "dev" && !isDevLevel(labelLevel)) { const s = selectLevel(issue.title, issue.description ?? "", role); selectedLevel = s.level; levelReason = s.reason; levelSource = "heuristic"; }
|
||||
else { selectedLevel = labelLevel; levelReason = `Label: "${labelLevel}"`; levelSource = "label"; }
|
||||
} else {
|
||||
const s = selectTier(issue.title, issue.description ?? "", role);
|
||||
selectedTier = s.tier; tierReason = s.reason; tierSource = "heuristic";
|
||||
const s = selectLevel(issue.title, issue.description ?? "", role);
|
||||
selectedLevel = s.level; levelReason = s.reason; levelSource = "heuristic";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||
const dr = await dispatchTask({
|
||||
workspaceDir, agentId: ctx.agentId, groupId, project, issueId: issue.iid,
|
||||
issueTitle: issue.title, issueDescription: issue.description ?? "", issueUrl: issue.web_url,
|
||||
role, tier: selectedTier, fromLabel: currentLabel, toLabel: targetLabel,
|
||||
role, level: selectedLevel, fromLabel: currentLabel, toLabel: targetLabel,
|
||||
transitionLabel: (id, from, to) => provider.transitionLabel(id, from as StateLabel, to as StateLabel),
|
||||
pluginConfig, sessionKey: ctx.sessionKey,
|
||||
});
|
||||
@@ -106,7 +106,7 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||
// Notify
|
||||
const notifyConfig = getNotificationConfig(pluginConfig);
|
||||
await notify(
|
||||
{ type: "workerStart", project: project.name, groupId, issueId: issue.iid, issueTitle: issue.title, issueUrl: issue.web_url, role, tier: dr.tier, sessionAction: dr.sessionAction },
|
||||
{ type: "workerStart", project: project.name, groupId, issueId: issue.iid, issueTitle: issue.title, issueUrl: issue.web_url, role, level: dr.level, sessionAction: dr.sessionAction },
|
||||
{ workspaceDir, config: notifyConfig, groupId, channel: context.channel },
|
||||
);
|
||||
|
||||
@@ -119,10 +119,10 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||
|
||||
const output: Record<string, unknown> = {
|
||||
success: true, project: project.name, groupId, issueId: issue.iid, issueTitle: issue.title,
|
||||
role, tier: dr.tier, model: dr.model, sessionAction: dr.sessionAction,
|
||||
role, level: dr.level, model: dr.model, sessionAction: dr.sessionAction,
|
||||
announcement: dr.announcement, labelTransition: `${currentLabel} → ${targetLabel}`,
|
||||
tierReason, tierSource,
|
||||
autoDetected: { projectGroupId: !groupIdParam, role: !roleParam, issueId: issueIdParam === undefined, tier: !tierParam },
|
||||
levelReason, levelSource,
|
||||
autoDetected: { projectGroupId: !groupIdParam, role: !roleParam, issueId: issueIdParam === undefined, level: !levelParam },
|
||||
};
|
||||
if (tickPickups.length) output.tickPickups = tickPickups;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user