refactor: migrate role handling from tiers to roles module
- Removed the deprecated tiers.ts file and migrated all related functionality to roles/index.js. - Updated tests and tools to reflect the new role structure, replacing references to "dev", "qa", and "architect" with "developer", "tester", and "architect". - Adjusted workflow configurations and state management to accommodate the new role naming conventions. - Enhanced project registration and health check tools to support dynamic role handling. - Updated task creation, update, and completion processes to align with the new role definitions. - Improved documentation and comments to clarify role responsibilities and usage.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { parseDevClawSessionKey } from "../bootstrap-hook.js";
|
||||
import { isArchitectLevel, levelRole, resolveModel, defaultModel, levelEmoji } from "../tiers.js";
|
||||
import { isLevelForRole, roleForLevel, resolveModel, getDefaultModel, getEmoji } from "../roles/index.js";
|
||||
import { selectLevel } from "../model-selector.js";
|
||||
import {
|
||||
DEFAULT_WORKFLOW, getQueueLabels, getActiveLabel, getCompletionRule,
|
||||
@@ -14,21 +14,21 @@ import {
|
||||
|
||||
describe("architect tiers", () => {
|
||||
it("should recognize architect levels", () => {
|
||||
assert.strictEqual(isArchitectLevel("junior"), true);
|
||||
assert.strictEqual(isArchitectLevel("senior"), true);
|
||||
assert.strictEqual(isArchitectLevel("mid"), false);
|
||||
assert.strictEqual(isLevelForRole("junior", "architect"), true);
|
||||
assert.strictEqual(isLevelForRole("senior", "architect"), true);
|
||||
assert.strictEqual(isLevelForRole("medior", "architect"), false);
|
||||
});
|
||||
|
||||
it("should map architect levels to role", () => {
|
||||
// "junior" and "senior" appear in dev first (registry order), so roleForLevel returns "dev"
|
||||
// This is expected — use isArchitectLevel for architect-specific checks
|
||||
assert.strictEqual(levelRole("junior"), "dev");
|
||||
assert.strictEqual(levelRole("senior"), "dev");
|
||||
// "junior" and "senior" appear in developer first (registry order), so roleForLevel returns "developer"
|
||||
// This is expected — use isLevelForRole for role-specific checks
|
||||
assert.strictEqual(roleForLevel("junior"), "developer");
|
||||
assert.strictEqual(roleForLevel("senior"), "developer");
|
||||
});
|
||||
|
||||
it("should resolve default architect models", () => {
|
||||
assert.strictEqual(defaultModel("architect", "senior"), "anthropic/claude-opus-4-5");
|
||||
assert.strictEqual(defaultModel("architect", "junior"), "anthropic/claude-sonnet-4-5");
|
||||
assert.strictEqual(getDefaultModel("architect", "senior"), "anthropic/claude-opus-4-5");
|
||||
assert.strictEqual(getDefaultModel("architect", "junior"), "anthropic/claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
it("should resolve architect model from config", () => {
|
||||
@@ -37,8 +37,8 @@ describe("architect tiers", () => {
|
||||
});
|
||||
|
||||
it("should have architect emoji", () => {
|
||||
assert.strictEqual(levelEmoji("architect", "senior"), "🏗️");
|
||||
assert.strictEqual(levelEmoji("architect", "junior"), "📐");
|
||||
assert.strictEqual(getEmoji("architect", "senior"), "🏗️");
|
||||
assert.strictEqual(getEmoji("architect", "junior"), "📐");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,8 +76,9 @@ describe("architect workflow states", () => {
|
||||
assert.strictEqual(rule!.to, "Refining");
|
||||
});
|
||||
|
||||
it("should have architect completion emoji", () => {
|
||||
assert.strictEqual(getCompletionEmoji("architect", "done"), "🏗️");
|
||||
it("should have completion emoji by result type", () => {
|
||||
// Emoji is now keyed by result, not role:result
|
||||
assert.strictEqual(getCompletionEmoji("architect", "done"), "✅");
|
||||
assert.strictEqual(getCompletionEmoji("architect", "blocked"), "🚫");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,9 @@ import { getWorker } from "../projects.js";
|
||||
import { dispatchTask } from "../dispatch.js";
|
||||
import { log as auditLog } from "../audit.js";
|
||||
import { requireWorkspaceDir, resolveProject, resolveProvider, getPluginConfig } from "../tool-helpers.js";
|
||||
import { DEFAULT_WORKFLOW, getActiveLabel } from "../workflow.js";
|
||||
import { loadWorkflow, getActiveLabel, getQueueLabels } from "../workflow.js";
|
||||
import { selectLevel } from "../model-selector.js";
|
||||
import { resolveModel } from "../roles/index.js";
|
||||
|
||||
export function createDesignTaskTool(api: OpenClawPluginApi) {
|
||||
return (ctx: ToolContext) => ({
|
||||
@@ -81,6 +83,14 @@ Example:
|
||||
|
||||
const { project } = await resolveProject(workspaceDir, groupId);
|
||||
const { provider } = await resolveProvider(project);
|
||||
const pluginConfig = getPluginConfig(api);
|
||||
|
||||
// Derive labels from workflow config
|
||||
const workflow = await loadWorkflow(workspaceDir, project.name);
|
||||
const role = "architect";
|
||||
const queueLabels = getQueueLabels(workflow, role);
|
||||
const queueLabel = queueLabels[0];
|
||||
if (!queueLabel) throw new Error(`No queue state found for role "${role}" in workflow`);
|
||||
|
||||
// Build issue body with focus areas
|
||||
const bodyParts = [description];
|
||||
@@ -101,51 +111,48 @@ Example:
|
||||
);
|
||||
const issueBody = bodyParts.join("\n");
|
||||
|
||||
// Create issue in To Design state
|
||||
const issue = await provider.createIssue(title, issueBody, "To Design" as StateLabel);
|
||||
// Create issue in queue state
|
||||
const issue = await provider.createIssue(title, issueBody, queueLabel as StateLabel);
|
||||
|
||||
await auditLog(workspaceDir, "design_task", {
|
||||
project: project.name, groupId, issueId: issue.iid,
|
||||
title, complexity, focusAreas, dryRun,
|
||||
});
|
||||
|
||||
// Select level based on complexity
|
||||
const level = complexity === "complex" ? "senior" : "junior";
|
||||
// Select level: use complexity hint to guide the heuristic
|
||||
const level = complexity === "complex"
|
||||
? selectLevel(title, "system-wide " + description, role).level
|
||||
: selectLevel(title, description, role).level;
|
||||
const model = resolveModel(role, level, pluginConfig);
|
||||
|
||||
if (dryRun) {
|
||||
return jsonResult({
|
||||
success: true,
|
||||
dryRun: true,
|
||||
issue: { id: issue.iid, title: issue.title, url: issue.web_url, label: "To Design" },
|
||||
design: {
|
||||
level,
|
||||
model: complexity === "complex" ? "anthropic/claude-opus-4-5" : "anthropic/claude-sonnet-4-5",
|
||||
status: "dry_run",
|
||||
},
|
||||
announcement: `📐 [DRY RUN] Would spawn architect (${level}) for #${issue.iid}: ${title}\n🔗 ${issue.web_url}`,
|
||||
issue: { id: issue.iid, title: issue.title, url: issue.web_url, label: queueLabel },
|
||||
design: { level, model, status: "dry_run" },
|
||||
announcement: `📐 [DRY RUN] Would spawn ${role} (${level}) for #${issue.iid}: ${title}\n🔗 ${issue.web_url}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check architect availability
|
||||
const worker = getWorker(project, "architect");
|
||||
// Check worker availability
|
||||
const worker = getWorker(project, role);
|
||||
if (worker.active) {
|
||||
// Issue created but can't dispatch yet — will be picked up by heartbeat
|
||||
return jsonResult({
|
||||
success: true,
|
||||
issue: { id: issue.iid, title: issue.title, url: issue.web_url, label: "To Design" },
|
||||
issue: { id: issue.iid, title: issue.title, url: issue.web_url, label: queueLabel },
|
||||
design: {
|
||||
level,
|
||||
status: "queued",
|
||||
reason: `Architect already active on #${worker.issueId}. Issue queued for pickup.`,
|
||||
reason: `${role.toUpperCase()} already active on #${worker.issueId}. Issue queued for pickup.`,
|
||||
},
|
||||
announcement: `📐 Created design task #${issue.iid}: ${title} (queued — architect busy)\n🔗 ${issue.web_url}`,
|
||||
announcement: `📐 Created design task #${issue.iid}: ${title} (queued — ${role} busy)\n🔗 ${issue.web_url}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Dispatch architect
|
||||
const workflow = DEFAULT_WORKFLOW;
|
||||
const targetLabel = getActiveLabel(workflow, "architect");
|
||||
const pluginConfig = getPluginConfig(api);
|
||||
// Dispatch worker
|
||||
const targetLabel = getActiveLabel(workflow, role);
|
||||
|
||||
const dr = await dispatchTask({
|
||||
workspaceDir,
|
||||
@@ -156,9 +163,9 @@ Example:
|
||||
issueTitle: issue.title,
|
||||
issueDescription: issueBody,
|
||||
issueUrl: issue.web_url,
|
||||
role: "architect",
|
||||
role,
|
||||
level,
|
||||
fromLabel: "To Design",
|
||||
fromLabel: queueLabel,
|
||||
toLabel: targetLabel,
|
||||
transitionLabel: (id, from, to) => provider.transitionLabel(id, from as StateLabel, to as StateLabel),
|
||||
provider,
|
||||
|
||||
@@ -18,7 +18,6 @@ import { readProjects, getProject } from "../projects.js";
|
||||
import { log as auditLog } from "../audit.js";
|
||||
import { checkWorkerHealth, scanOrphanedLabels, fetchGatewaySessions, type HealthFix } from "../services/health.js";
|
||||
import { requireWorkspaceDir, resolveProvider } from "../tool-helpers.js";
|
||||
import { getAllRoleIds } from "../roles/index.js";
|
||||
|
||||
export function createHealthTool() {
|
||||
return (ctx: ToolContext) => ({
|
||||
@@ -52,13 +51,13 @@ export function createHealthTool() {
|
||||
if (!project) continue;
|
||||
const { provider } = await resolveProvider(project);
|
||||
|
||||
for (const role of getAllRoleIds()) {
|
||||
for (const role of Object.keys(project.workers)) {
|
||||
// Worker health check (session liveness, label consistency, etc)
|
||||
const healthFixes = await checkWorkerHealth({
|
||||
workspaceDir,
|
||||
groupId: pid,
|
||||
project,
|
||||
role: role as any,
|
||||
role,
|
||||
sessions,
|
||||
autoFix: fix,
|
||||
provider,
|
||||
@@ -70,7 +69,7 @@ export function createHealthTool() {
|
||||
workspaceDir,
|
||||
groupId: pid,
|
||||
project,
|
||||
role: role as any,
|
||||
role,
|
||||
autoFix: fix,
|
||||
provider,
|
||||
});
|
||||
|
||||
@@ -15,40 +15,26 @@ import { resolveRepoPath } from "../projects.js";
|
||||
import { createProvider } from "../providers/index.js";
|
||||
import { log as auditLog } from "../audit.js";
|
||||
import { getAllRoleIds, getLevelsForRole } from "../roles/index.js";
|
||||
import { DEFAULT_DEV_INSTRUCTIONS, DEFAULT_QA_INSTRUCTIONS, DEFAULT_ARCHITECT_INSTRUCTIONS } from "../templates.js";
|
||||
import { DEFAULT_ROLE_INSTRUCTIONS } from "../templates.js";
|
||||
|
||||
/**
|
||||
* Scaffold project-specific prompt files.
|
||||
* Scaffold project-specific prompt files for all registered roles.
|
||||
* 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", "roles", projectName);
|
||||
await fs.mkdir(projectDir, { recursive: true });
|
||||
|
||||
const projectDev = path.join(projectDir, "dev.md");
|
||||
const projectQa = path.join(projectDir, "qa.md");
|
||||
let created = false;
|
||||
|
||||
try {
|
||||
await fs.access(projectDev);
|
||||
} catch {
|
||||
await fs.writeFile(projectDev, DEFAULT_DEV_INSTRUCTIONS, "utf-8");
|
||||
created = true;
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.access(projectQa);
|
||||
} catch {
|
||||
await fs.writeFile(projectQa, DEFAULT_QA_INSTRUCTIONS, "utf-8");
|
||||
created = true;
|
||||
}
|
||||
|
||||
const projectArchitect = path.join(projectDir, "architect.md");
|
||||
try {
|
||||
await fs.access(projectArchitect);
|
||||
} catch {
|
||||
await fs.writeFile(projectArchitect, DEFAULT_ARCHITECT_INSTRUCTIONS, "utf-8");
|
||||
created = true;
|
||||
for (const role of getAllRoleIds()) {
|
||||
const filePath = path.join(projectDir, `${role}.md`);
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch {
|
||||
const content = DEFAULT_ROLE_INSTRUCTIONS[role] ?? `# ${role.toUpperCase()} Worker Instructions\n\nAdd role-specific instructions here.\n`;
|
||||
await fs.writeFile(filePath, content, "utf-8");
|
||||
created = true;
|
||||
}
|
||||
}
|
||||
|
||||
return created;
|
||||
@@ -122,7 +108,8 @@ export function createProjectRegisterTool() {
|
||||
// 1. Check project not already registered (allow re-register if incomplete)
|
||||
const data = await readProjects(workspaceDir);
|
||||
const existing = data.projects[groupId];
|
||||
if (existing && existing.dev?.sessions && Object.keys(existing.dev.sessions).length > 0) {
|
||||
const existingWorkers = existing?.workers ?? {};
|
||||
if (existing && Object.values(existingWorkers).some(w => w.sessions && Object.keys(w.sessions).length > 0)) {
|
||||
throw new Error(
|
||||
`Project already registered for this group: "${existing.name}". Remove the existing entry first or use a different group.`,
|
||||
);
|
||||
@@ -153,6 +140,12 @@ export function createProjectRegisterTool() {
|
||||
await provider.ensureAllStateLabels();
|
||||
|
||||
// 5. Add project to projects.json
|
||||
// Build workers map from all registered roles
|
||||
const workers: Record<string, import("../projects.js").WorkerState> = {};
|
||||
for (const role of getAllRoleIds()) {
|
||||
workers[role] = emptyWorkerState([...getLevelsForRole(role)]);
|
||||
}
|
||||
|
||||
data.projects[groupId] = {
|
||||
name,
|
||||
repo,
|
||||
@@ -163,9 +156,7 @@ export function createProjectRegisterTool() {
|
||||
channel,
|
||||
provider: providerType,
|
||||
roleExecution,
|
||||
dev: emptyWorkerState([...getLevelsForRole("dev")]),
|
||||
qa: emptyWorkerState([...getLevelsForRole("qa")]),
|
||||
architect: emptyWorkerState([...getLevelsForRole("architect")]),
|
||||
workers,
|
||||
};
|
||||
|
||||
await writeProjects(workspaceDir, data);
|
||||
|
||||
@@ -15,18 +15,18 @@ describe("status execution-aware sequencing", () => {
|
||||
});
|
||||
|
||||
describe("role assignment", () => {
|
||||
it("should assign To Improve to dev", () => {
|
||||
// To Improve = dev work
|
||||
it("should assign To Improve to developer", () => {
|
||||
// To Improve = developer work
|
||||
assert.ok(true);
|
||||
});
|
||||
|
||||
it("should assign To Do to dev", () => {
|
||||
// To Do = dev work
|
||||
it("should assign To Do to developer", () => {
|
||||
// To Do = developer work
|
||||
assert.ok(true);
|
||||
});
|
||||
|
||||
it("should assign To Test to qa", () => {
|
||||
// To Test = qa work
|
||||
it("should assign To Test to tester", () => {
|
||||
// To Test = tester work
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
||||
@@ -43,12 +43,12 @@ describe("status execution-aware sequencing", () => {
|
||||
});
|
||||
|
||||
it("should support parallel role execution within project", () => {
|
||||
// DEV and QA can run simultaneously
|
||||
// Developer and Tester can run simultaneously
|
||||
assert.ok(true);
|
||||
});
|
||||
|
||||
it("should support sequential role execution within project", () => {
|
||||
// DEV and QA alternate
|
||||
// Developer and Tester alternate
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { jsonResult } from "openclaw/plugin-sdk";
|
||||
import type { ToolContext } from "../types.js";
|
||||
import { runSetup, type SetupOpts } from "../setup/index.js";
|
||||
import { DEFAULT_MODELS } from "../tiers.js";
|
||||
import { getLevelsForRole } from "../roles/index.js";
|
||||
import { getAllDefaultModels, getAllRoleIds, getLevelsForRole } from "../roles/index.js";
|
||||
|
||||
export function createSetupTool(api: OpenClawPluginApi) {
|
||||
return (ctx: ToolContext) => ({
|
||||
@@ -37,44 +36,18 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
||||
models: {
|
||||
type: "object",
|
||||
description: "Model overrides per role and level.",
|
||||
properties: {
|
||||
dev: {
|
||||
properties: Object.fromEntries(
|
||||
getAllRoleIds().map((role) => [role, {
|
||||
type: "object",
|
||||
description: "Developer level models",
|
||||
properties: {
|
||||
junior: {
|
||||
description: `${role.toUpperCase()} level models`,
|
||||
properties: Object.fromEntries(
|
||||
getLevelsForRole(role).map((level) => [level, {
|
||||
type: "string",
|
||||
description: `Default: ${DEFAULT_MODELS.dev.junior}`,
|
||||
},
|
||||
mid: {
|
||||
type: "string",
|
||||
description: `Default: ${DEFAULT_MODELS.dev.mid}`,
|
||||
},
|
||||
senior: {
|
||||
type: "string",
|
||||
description: `Default: ${DEFAULT_MODELS.dev.senior}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
qa: {
|
||||
type: "object",
|
||||
description: "QA level models",
|
||||
properties: {
|
||||
junior: {
|
||||
type: "string",
|
||||
description: `Default: ${DEFAULT_MODELS.qa.junior}`,
|
||||
},
|
||||
mid: {
|
||||
type: "string",
|
||||
description: `Default: ${DEFAULT_MODELS.qa.mid}`,
|
||||
},
|
||||
senior: {
|
||||
type: "string",
|
||||
description: `Default: ${DEFAULT_MODELS.qa.senior}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
description: `Default: ${getAllDefaultModels()[role]?.[level] ?? "auto"}`,
|
||||
}]),
|
||||
),
|
||||
}]),
|
||||
),
|
||||
},
|
||||
projectExecution: {
|
||||
type: "string",
|
||||
@@ -112,13 +85,13 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
||||
"",
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
"Models:",
|
||||
...getLevelsForRole("dev").map((t) => ` dev.${t}: ${result.models.dev[t]}`),
|
||||
...getLevelsForRole("qa").map((t) => ` qa.${t}: ${result.models.qa[t]}`),
|
||||
...getLevelsForRole("architect").map((t) => ` architect.${t}: ${result.models.architect[t]}`),
|
||||
"",
|
||||
);
|
||||
lines.push("Models:");
|
||||
for (const [role, levels] of Object.entries(result.models)) {
|
||||
for (const [level, model] of Object.entries(levels)) {
|
||||
lines.push(` ${role}.${level}: ${model}`);
|
||||
}
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
lines.push("Files:", ...result.filesWritten.map((f) => ` ${f}`));
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { readProjects, getProject } from "../projects.js";
|
||||
import { log as auditLog } from "../audit.js";
|
||||
import { fetchProjectQueues, getTotalQueuedCount, getQueueLabelsWithPriority } from "../services/queue.js";
|
||||
import { requireWorkspaceDir, getPluginConfig } from "../tool-helpers.js";
|
||||
import { DEFAULT_WORKFLOW } from "../workflow.js";
|
||||
import { loadWorkflow } from "../workflow.js";
|
||||
|
||||
export function createStatusTool(api: OpenClawPluginApi) {
|
||||
return (ctx: ToolContext) => ({
|
||||
@@ -32,8 +32,8 @@ export function createStatusTool(api: OpenClawPluginApi) {
|
||||
const pluginConfig = getPluginConfig(api);
|
||||
const projectExecution = (pluginConfig?.projectExecution as string) ?? "parallel";
|
||||
|
||||
// TODO: Load per-project workflow when supported
|
||||
const workflow = DEFAULT_WORKFLOW;
|
||||
// Load workspace-level workflow (per-project loaded inside map)
|
||||
const workflow = await loadWorkflow(workspaceDir);
|
||||
|
||||
const data = await readProjects(workspaceDir);
|
||||
const projectIds = groupId ? [groupId] : Object.keys(data.projects);
|
||||
@@ -52,28 +52,22 @@ export function createStatusTool(api: OpenClawPluginApi) {
|
||||
queueCounts[label] = issues.length;
|
||||
}
|
||||
|
||||
// Build dynamic workers summary
|
||||
const workers: Record<string, { active: boolean; issueId: string | null; level: string | null; startTime: string | null }> = {};
|
||||
for (const [role, worker] of Object.entries(project.workers)) {
|
||||
workers[role] = {
|
||||
active: worker.active,
|
||||
issueId: worker.issueId,
|
||||
level: worker.level,
|
||||
startTime: worker.startTime,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: project.name,
|
||||
groupId: pid,
|
||||
roleExecution: project.roleExecution ?? "parallel",
|
||||
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,
|
||||
},
|
||||
architect: {
|
||||
active: project.architect.active,
|
||||
issueId: project.architect.issueId,
|
||||
level: project.architect.level,
|
||||
startTime: project.architect.startTime,
|
||||
},
|
||||
workers,
|
||||
queue: queueCounts,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* task_comment — Add review comments or notes to an issue.
|
||||
*
|
||||
* Use cases:
|
||||
* - QA worker adds review feedback without blocking pass/fail
|
||||
* - DEV worker posts implementation notes
|
||||
* - Tester worker adds review feedback without blocking pass/fail
|
||||
* - Developer worker posts implementation notes
|
||||
* - Orchestrator adds summary comments
|
||||
*/
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
@@ -13,7 +13,7 @@ import { log as auditLog } from "../audit.js";
|
||||
import { requireWorkspaceDir, resolveProject, resolveProvider } from "../tool-helpers.js";
|
||||
|
||||
/** Valid author roles for attribution */
|
||||
const AUTHOR_ROLES = ["dev", "qa", "orchestrator"] as const;
|
||||
const AUTHOR_ROLES = ["developer", "tester", "orchestrator"] as const;
|
||||
type AuthorRole = (typeof AUTHOR_ROLES)[number];
|
||||
|
||||
export function createTaskCommentTool(api: OpenClawPluginApi) {
|
||||
@@ -23,15 +23,15 @@ export function createTaskCommentTool(api: OpenClawPluginApi) {
|
||||
description: `Add a comment to an issue. Use this for review feedback, implementation notes, or any discussion that doesn't require a state change.
|
||||
|
||||
Use cases:
|
||||
- QA adds review feedback without blocking pass/fail
|
||||
- DEV posts implementation notes or progress updates
|
||||
- Tester adds review feedback without blocking pass/fail
|
||||
- Developer posts implementation notes or progress updates
|
||||
- Orchestrator adds summary comments
|
||||
- Cross-referencing related issues or PRs
|
||||
|
||||
Examples:
|
||||
- Simple: { projectGroupId: "-123456789", issueId: 42, body: "Found an edge case with null inputs" }
|
||||
- With role: { projectGroupId: "-123456789", issueId: 42, body: "LGTM!", authorRole: "qa" }
|
||||
- Detailed: { projectGroupId: "-123456789", issueId: 42, body: "## Notes\\n\\n- Tested on staging\\n- All checks passing", authorRole: "dev" }`,
|
||||
- With role: { projectGroupId: "-123456789", issueId: 42, body: "LGTM!", authorRole: "tester" }
|
||||
- Detailed: { projectGroupId: "-123456789", issueId: 42, body: "## Notes\\n\\n- Tested on staging\\n- All checks passing", authorRole: "developer" }`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
required: ["projectGroupId", "issueId", "body"],
|
||||
@@ -100,7 +100,7 @@ Examples:
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ROLE_EMOJI: Record<AuthorRole, string> = {
|
||||
dev: "👨💻",
|
||||
qa: "🔍",
|
||||
developer: "👨💻",
|
||||
tester: "🔍",
|
||||
orchestrator: "🎛️",
|
||||
};
|
||||
|
||||
@@ -13,7 +13,8 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { jsonResult } from "openclaw/plugin-sdk";
|
||||
import type { ToolContext } from "../types.js";
|
||||
import { log as auditLog } from "../audit.js";
|
||||
import { STATE_LABELS, type StateLabel } from "../providers/provider.js";
|
||||
import type { StateLabel } from "../providers/provider.js";
|
||||
import { DEFAULT_WORKFLOW, getStateLabels } from "../workflow.js";
|
||||
import { requireWorkspaceDir, resolveProject, resolveProvider } from "../tool-helpers.js";
|
||||
|
||||
export function createTaskCreateTool(api: OpenClawPluginApi) {
|
||||
@@ -46,7 +47,7 @@ Examples:
|
||||
label: {
|
||||
type: "string",
|
||||
description: `State label. Defaults to "Planning" — only use "To Do" when the user explicitly asks to start work immediately.`,
|
||||
enum: STATE_LABELS,
|
||||
enum: getStateLabels(DEFAULT_WORKFLOW),
|
||||
},
|
||||
assignees: {
|
||||
type: "array",
|
||||
|
||||
@@ -10,7 +10,8 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { jsonResult } from "openclaw/plugin-sdk";
|
||||
import type { ToolContext } from "../types.js";
|
||||
import { log as auditLog } from "../audit.js";
|
||||
import { STATE_LABELS, type StateLabel } from "../providers/provider.js";
|
||||
import type { StateLabel } from "../providers/provider.js";
|
||||
import { DEFAULT_WORKFLOW, getStateLabels } from "../workflow.js";
|
||||
import { requireWorkspaceDir, resolveProject, resolveProvider } from "../tool-helpers.js";
|
||||
|
||||
export function createTaskUpdateTool(api: OpenClawPluginApi) {
|
||||
@@ -42,8 +43,8 @@ Examples:
|
||||
},
|
||||
state: {
|
||||
type: "string",
|
||||
enum: STATE_LABELS,
|
||||
description: `New state for the issue. One of: ${STATE_LABELS.join(", ")}`,
|
||||
enum: getStateLabels(DEFAULT_WORKFLOW),
|
||||
description: `New state for the issue. One of: ${getStateLabels(DEFAULT_WORKFLOW).join(", ")}`,
|
||||
},
|
||||
reason: {
|
||||
type: "string",
|
||||
|
||||
@@ -8,16 +8,17 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { jsonResult } from "openclaw/plugin-sdk";
|
||||
import type { ToolContext } from "../types.js";
|
||||
import { getWorker, resolveRepoPath } from "../projects.js";
|
||||
import { executeCompletion, getRule, NEXT_STATE } from "../services/pipeline.js";
|
||||
import { executeCompletion, getRule } from "../services/pipeline.js";
|
||||
import { log as auditLog } from "../audit.js";
|
||||
import { requireWorkspaceDir, resolveProject, resolveProvider, getPluginConfig } from "../tool-helpers.js";
|
||||
import { getAllRoleIds, isValidResult, getCompletionResults } from "../roles/index.js";
|
||||
import { loadWorkflow } from "../workflow.js";
|
||||
|
||||
export function createWorkFinishTool(api: OpenClawPluginApi) {
|
||||
return (ctx: ToolContext) => ({
|
||||
name: "work_finish",
|
||||
label: "Work Finish",
|
||||
description: `Complete a task: DEV done/blocked, QA pass/fail/refine/blocked. Handles label transition, state update, issue close/reopen, notifications, and audit logging.`,
|
||||
description: `Complete a task: Developer done/blocked, Tester pass/fail/refine/blocked. Handles label transition, state update, issue close/reopen, notifications, and audit logging.`,
|
||||
parameters: {
|
||||
type: "object",
|
||||
required: ["role", "result", "projectGroupId"],
|
||||
@@ -31,7 +32,7 @@ export function createWorkFinishTool(api: OpenClawPluginApi) {
|
||||
},
|
||||
|
||||
async execute(_id: string, params: Record<string, unknown>) {
|
||||
const role = params.role as "dev" | "qa" | "architect";
|
||||
const role = params.role as string;
|
||||
const result = params.result as string;
|
||||
const groupId = params.projectGroupId as string;
|
||||
const summary = params.summary as string | undefined;
|
||||
@@ -59,6 +60,7 @@ export function createWorkFinishTool(api: OpenClawPluginApi) {
|
||||
const issue = await provider.getIssue(issueId);
|
||||
|
||||
const pluginConfig = getPluginConfig(api);
|
||||
const workflow = await loadWorkflow(workspaceDir, project.name);
|
||||
|
||||
// Execute completion (pipeline service handles notification with runtime)
|
||||
const completion = await executeCompletion({
|
||||
@@ -67,6 +69,7 @@ export function createWorkFinishTool(api: OpenClawPluginApi) {
|
||||
channel: project.channel,
|
||||
pluginConfig,
|
||||
runtime: api.runtime,
|
||||
workflow,
|
||||
});
|
||||
|
||||
const output: Record<string, unknown> = {
|
||||
|
||||
@@ -13,10 +13,9 @@ import { selectLevel } from "../model-selector.js";
|
||||
import { getWorker } from "../projects.js";
|
||||
import { dispatchTask } from "../dispatch.js";
|
||||
import { findNextIssue, detectRoleFromLabel, detectLevelFromLabels } from "../services/tick.js";
|
||||
import { isDevLevel } from "../tiers.js";
|
||||
import { getAllRoleIds } from "../roles/index.js";
|
||||
import { getAllRoleIds, isLevelForRole } from "../roles/index.js";
|
||||
import { requireWorkspaceDir, resolveProject, resolveProvider, getPluginConfig } from "../tool-helpers.js";
|
||||
import { DEFAULT_WORKFLOW, getActiveLabel } from "../workflow.js";
|
||||
import { loadWorkflow, getActiveLabel } from "../workflow.js";
|
||||
|
||||
export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||
return (ctx: ToolContext) => ({
|
||||
@@ -36,7 +35,7 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||
|
||||
async execute(_id: string, params: Record<string, unknown>) {
|
||||
const issueIdParam = params.issueId as number | undefined;
|
||||
const roleParam = params.role as "dev" | "qa" | "architect" | undefined;
|
||||
const roleParam = params.role as string | undefined;
|
||||
const groupId = params.projectGroupId as string;
|
||||
const levelParam = (params.level ?? params.tier) as string | undefined;
|
||||
const workspaceDir = requireWorkspaceDir(ctx);
|
||||
@@ -45,8 +44,7 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||
const { project } = await resolveProject(workspaceDir, groupId);
|
||||
const { provider } = await resolveProvider(project);
|
||||
|
||||
// TODO: Load per-project workflow when supported
|
||||
const workflow = DEFAULT_WORKFLOW;
|
||||
const workflow = await loadWorkflow(workspaceDir, project.name);
|
||||
|
||||
// Find issue
|
||||
let issue: { iid: number; title: string; description: string; labels: string[]; web_url: string; state: string };
|
||||
@@ -73,8 +71,11 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||
const worker = getWorker(project, role);
|
||||
if (worker.active) throw new Error(`${role.toUpperCase()} already active on ${project.name} (issue: ${worker.issueId})`);
|
||||
if ((project.roleExecution ?? "parallel") === "sequential") {
|
||||
const other = role === "dev" ? "qa" : "dev";
|
||||
if (getWorker(project, other).active) throw new Error(`Sequential roleExecution: ${other.toUpperCase()} is active`);
|
||||
for (const [otherRole, otherWorker] of Object.entries(project.workers)) {
|
||||
if (otherRole !== role && otherWorker.active) {
|
||||
throw new Error(`Sequential roleExecution: ${otherRole.toUpperCase()} is active`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get target label from workflow
|
||||
@@ -87,9 +88,13 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||
} else {
|
||||
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"; }
|
||||
if (!isLevelForRole(labelLevel, role)) {
|
||||
// Label level belongs to a different role — use heuristic for this role
|
||||
const s = selectLevel(issue.title, issue.description ?? "", role);
|
||||
selectedLevel = s.level; levelReason = `${role} overrides other role's level "${labelLevel}"`; levelSource = "role-override";
|
||||
} else {
|
||||
selectedLevel = labelLevel; levelReason = `Label: "${labelLevel}"`; levelSource = "label";
|
||||
}
|
||||
} else {
|
||||
const s = selectLevel(issue.title, issue.description ?? "", role);
|
||||
selectedLevel = s.level; levelReason = s.reason; levelSource = "heuristic";
|
||||
|
||||
Reference in New Issue
Block a user