Refactor setup and tool helpers for improved modularity and clarity

- Moved setup logic into dedicated files: agent.ts, config.ts, index.ts, workspace.ts.
- Introduced tool-helpers.ts for shared functions across tools, reducing boilerplate.
- Updated tools (status, task-comment, task-create, task-update, work-finish, work-start) to utilize new helper functions for workspace resolution and provider creation.
- Enhanced error handling and context detection in tools.
- Improved project resolution logic to streamline tool execution.
- Added new functionality for agent creation and configuration management in setup.
This commit is contained in:
Lauren ten Hoor
2026-02-10 22:51:35 +08:00
parent 55b062ac76
commit 70af40e986
22 changed files with 768 additions and 953 deletions

View File

@@ -7,13 +7,12 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { jsonResult } from "openclaw/plugin-sdk";
import type { ToolContext } from "../types.js";
import { createProvider } from "../providers/index.js";
import { readProjects } from "../projects.js";
import { detectContext } from "../context-guard.js";
import { log as auditLog } from "../audit.js";
import { notify, getNotificationConfig } from "../notify.js";
import { checkWorkerHealth, type HealthFix } from "../services/health.js";
import { projectTick, type TickAction } from "../services/tick.js";
import { requireWorkspaceDir, resolveContext, resolveProvider, getPluginConfig } from "../tool-helpers.js";
type ExecutionMode = "parallel" | "sequential";
@@ -37,10 +36,9 @@ export function createAutoPickupTool(api: OpenClawPluginApi) {
const dryRun = (params.dryRun as boolean) ?? false;
const maxPickups = params.maxPickups as number | undefined;
const activeSessions = (params.activeSessions as string[]) ?? [];
const workspaceDir = ctx.workspaceDir;
if (!workspaceDir) throw new Error("No workspace directory available");
const workspaceDir = requireWorkspaceDir(ctx);
const pluginConfig = api.pluginConfig as Record<string, unknown> | undefined;
const pluginConfig = getPluginConfig(api);
const projectExecution: ExecutionMode = (pluginConfig?.projectExecution as ExecutionMode) ?? "parallel";
const data = await readProjects(workspaceDir);
@@ -59,7 +57,7 @@ export function createAutoPickupTool(api: OpenClawPluginApi) {
// Pass 1: health checks
for (const [groupId, project] of projectEntries) {
const { provider } = createProvider({ repo: project.repo });
const { provider } = resolveProvider(project);
for (const role of ["dev", "qa"] as const) {
const fixes = await checkWorkerHealth({ workspaceDir, groupId, project, role, activeSessions, autoFix: !dryRun, provider });
healthFixes.push(...fixes.map((f) => ({ ...f, project: project.name, role })));
@@ -105,8 +103,7 @@ export function createAutoPickupTool(api: OpenClawPluginApi) {
});
// Notify
const devClawAgentIds = ((api.pluginConfig as Record<string, unknown>)?.devClawAgentIds as string[] | undefined) ?? [];
const context = await detectContext(ctx, devClawAgentIds);
const context = await resolveContext(ctx, api);
const notifyConfig = getNotificationConfig(pluginConfig);
await notify(
{ type: "heartbeat", projectsScanned: projectEntries.length, healthFixes: healthFixes.length, pickups: pickups.length, skipped: skipped.length, dryRun, pickupDetails: pickups.map((p) => ({ project: p.project, issueId: p.issueId, role: p.role })) },

View File

@@ -2,12 +2,12 @@
* setup — Agent-driven DevClaw setup.
*
* Creates agent, configures model tiers, writes workspace files.
* Thin wrapper around lib/setup.ts.
* 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.js";
import { runSetup } from "../setup/index.js";
import { ALL_TIERS, DEFAULT_MODELS, type Tier } from "../tiers.js";
export function createSetupTool(api: OpenClawPluginApi) {

View File

@@ -8,14 +8,14 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { jsonResult } from "openclaw/plugin-sdk";
import type { ToolContext } from "../types.js";
import { readProjects, getProject, type Project } from "../projects.js";
import { detectContext, generateGuardrails } from "../context-guard.js";
import { generateGuardrails } from "../context-guard.js";
import { log as auditLog } from "../audit.js";
import { checkWorkerHealth } from "../services/health.js";
import {
fetchProjectQueues, buildParallelProjectSequences, buildGlobalTaskSequence,
formatProjectQueues, type ProjectQueues, type ProjectExecutionConfig,
} from "../services/queue.js";
import { createProvider } from "../providers/index.js";
import { requireWorkspaceDir, resolveContext, resolveProvider, getPluginConfig } from "../tool-helpers.js";
export function createStatusTool(api: OpenClawPluginApi) {
return (ctx: ToolContext) => ({
@@ -32,16 +32,11 @@ export function createStatusTool(api: OpenClawPluginApi) {
},
async execute(_id: string, params: Record<string, unknown>) {
const workspaceDir = ctx.workspaceDir;
if (!workspaceDir) throw new Error("No workspace directory available");
const workspaceDir = requireWorkspaceDir(ctx);
const includeHealth = (params.includeHealth as boolean) ?? true;
const activeSessions = (params.activeSessions as string[]) ?? [];
// Context detection
const devClawAgentIds = ((api.pluginConfig as Record<string, unknown>)?.devClawAgentIds as string[] | undefined) ?? [];
const context = await detectContext(ctx, devClawAgentIds);
const context = await resolveContext(ctx, api);
if (context.type === "via-agent") {
return jsonResult({
success: false,
@@ -55,7 +50,7 @@ export function createStatusTool(api: OpenClawPluginApi) {
let groupId = params.projectGroupId as string | undefined;
if (context.type === "group" && !groupId) groupId = context.groupId;
const pluginConfig = api.pluginConfig as Record<string, unknown> | undefined;
const pluginConfig = getPluginConfig(api);
const projectExecution = (pluginConfig?.projectExecution as "parallel" | "sequential") ?? "parallel";
const data = await readProjects(workspaceDir);
@@ -81,7 +76,7 @@ export function createStatusTool(api: OpenClawPluginApi) {
const healthIssues: Array<Record<string, unknown>> = [];
if (includeHealth) {
for (const { id, project } of projectList) {
const { provider } = createProvider({ repo: project.repo });
const { provider } = resolveProvider(project);
for (const role of ["dev", "qa"] as const) {
const fixes = await checkWorkerHealth({
workspaceDir, groupId: id, project, role, activeSessions,
@@ -108,8 +103,8 @@ export function createStatusTool(api: OpenClawPluginApi) {
// Build project details
const projects = projectQueues.map(({ projectId, project, queues }) => ({
name: project.name, groupId: projectId,
dev: { active: project.dev.active, issueId: project.dev.issueId, model: project.dev.model, sessions: project.dev.sessions },
qa: { active: project.qa.active, issueId: project.qa.issueId, model: project.qa.model, sessions: project.qa.sessions },
dev: { active: project.dev.active, issueId: project.dev.issueId, tier: project.dev.tier, sessions: project.dev.sessions },
qa: { active: project.qa.active, issueId: project.qa.issueId, tier: project.qa.tier, sessions: project.qa.sessions },
queue: formatProjectQueues(queues),
}));

View File

@@ -9,9 +9,8 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { jsonResult } from "openclaw/plugin-sdk";
import type { ToolContext } from "../types.js";
import { readProjects } from "../projects.js";
import { createProvider } from "../providers/index.js";
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;
@@ -62,73 +61,46 @@ Examples:
const issueId = params.issueId as number;
const body = params.body as string;
const authorRole = (params.authorRole as AuthorRole) ?? undefined;
const workspaceDir = ctx.workspaceDir;
const workspaceDir = requireWorkspaceDir(ctx);
if (!workspaceDir) {
throw new Error("No workspace directory available in tool context");
}
// Validate body is not empty
if (!body || body.trim().length === 0) {
throw new Error("Comment body cannot be empty.");
}
// 1. Resolve project
const data = await readProjects(workspaceDir);
const project = data.projects[groupId];
if (!project) {
throw new Error(
`Project not found for groupId ${groupId}. Run project_register first.`,
);
}
const { project } = await resolveProject(workspaceDir, groupId);
const { provider, type: providerType } = resolveProvider(project);
// 2. Create provider
const { provider, type: providerType } = createProvider({
repo: project.repo,
});
// 3. Fetch issue to verify it exists and get title
const issue = await provider.getIssue(issueId);
// 4. Prepare comment body with optional attribution header
let commentBody = body;
if (authorRole) {
const roleEmoji: Record<AuthorRole, string> = {
dev: "👨‍💻",
qa: "🔍",
orchestrator: "🎛️",
};
commentBody = `${roleEmoji[authorRole]} **${authorRole.toUpperCase()}**: ${body}`;
}
const commentBody = authorRole
? `${ROLE_EMOJI[authorRole]} **${authorRole.toUpperCase()}**: ${body}`
: body;
// 5. Add the comment
await provider.addComment(issueId, commentBody);
// 6. Audit log
await auditLog(workspaceDir, "task_comment", {
project: project.name,
groupId,
issueId,
project: project.name, groupId, issueId,
authorRole: authorRole ?? null,
bodyPreview: body.slice(0, 100) + (body.length > 100 ? "..." : ""),
provider: providerType,
});
// 7. Build response
const result = {
success: true,
issueId,
issueTitle: issue.title,
issueUrl: issue.web_url,
commentAdded: true,
authorRole: authorRole ?? null,
bodyLength: body.length,
project: project.name,
provider: providerType,
return jsonResult({
success: true, issueId, issueTitle: issue.title, issueUrl: issue.web_url,
commentAdded: true, authorRole: authorRole ?? null, bodyLength: body.length,
project: project.name, provider: providerType,
announcement: `💬 Comment added to #${issueId}${authorRole ? ` by ${authorRole.toUpperCase()}` : ""}`,
};
return jsonResult(result);
});
},
});
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
const ROLE_EMOJI: Record<AuthorRole, string> = {
dev: "👨‍💻",
qa: "🔍",
orchestrator: "🎛️",
};

View File

@@ -12,21 +12,9 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { jsonResult } from "openclaw/plugin-sdk";
import type { ToolContext } from "../types.js";
import { readProjects } from "../projects.js";
import { createProvider } from "../providers/index.js";
import { log as auditLog } from "../audit.js";
import type { StateLabel } from "../providers/provider.js";
const STATE_LABELS: StateLabel[] = [
"Planning",
"To Do",
"Doing",
"To Test",
"Testing",
"Done",
"To Improve",
"Refining",
];
import { STATE_LABELS, type StateLabel } from "../providers/provider.js";
import { requireWorkspaceDir, resolveProject, resolveProvider } from "../tool-helpers.js";
export function createTaskCreateTool(api: OpenClawPluginApi) {
return (ctx: ToolContext) => ({
@@ -80,69 +68,29 @@ The issue is created with a state label (defaults to "Planning"). Returns the cr
const label = (params.label as StateLabel) ?? "Planning";
const assignees = (params.assignees as string[] | undefined) ?? [];
const pickup = (params.pickup as boolean) ?? false;
const workspaceDir = ctx.workspaceDir;
const workspaceDir = requireWorkspaceDir(ctx);
if (!workspaceDir) {
throw new Error("No workspace directory available in tool context");
}
const { project } = await resolveProject(workspaceDir, groupId);
const { provider, type: providerType } = resolveProvider(project);
// 1. Resolve project
const data = await readProjects(workspaceDir);
const project = data.projects[groupId];
if (!project) {
throw new Error(`Project not found for groupId ${groupId}. Run project_register first.`);
}
// 2. Create provider
const { provider, type: providerType } = createProvider({
repo: project.repo,
});
// 3. Create the issue
const issue = await provider.createIssue(title, description, label, assignees);
// 4. Audit log
await auditLog(workspaceDir, "task_create", {
project: project.name,
groupId,
issueId: issue.iid,
title,
label,
provider: providerType,
pickup,
project: project.name, groupId, issueId: issue.iid,
title, label, provider: providerType, pickup,
});
// 5. Build response
const hasBody = description && description.trim().length > 0;
// Build announcement with URL
let announcement = `📋 Created #${issue.iid}: "${title}" (${label})`;
if (hasBody) {
announcement += "\nWith detailed description.";
}
if (hasBody) announcement += "\nWith detailed description.";
announcement += `\n🔗 ${issue.web_url}`;
if (pickup) {
announcement += "\nPicking up for DEV...";
} else {
announcement += "\nReady for pickup when needed.";
}
const result = {
success: true,
issue: {
id: issue.iid,
title: issue.title,
body: hasBody ? description : null,
url: issue.web_url,
label,
},
project: project.name,
provider: providerType,
pickup,
announcement,
};
announcement += pickup ? "\nPicking up for DEV..." : "\nReady for pickup when needed.";
return jsonResult(result);
return jsonResult({
success: true,
issue: { id: issue.iid, title: issue.title, body: hasBody ? description : null, url: issue.web_url, label },
project: project.name, provider: providerType, pickup, announcement,
});
},
});
}

View File

@@ -9,21 +9,9 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { jsonResult } from "openclaw/plugin-sdk";
import type { ToolContext } from "../types.js";
import { readProjects } from "../projects.js";
import { createProvider } from "../providers/index.js";
import { log as auditLog } from "../audit.js";
import type { StateLabel } from "../providers/provider.js";
const STATE_LABELS: StateLabel[] = [
"Planning",
"To Do",
"Doing",
"To Test",
"Testing",
"Done",
"To Improve",
"Refining",
];
import { STATE_LABELS, type StateLabel } from "../providers/provider.js";
import { requireWorkspaceDir, resolveProject, resolveProvider } from "../tool-helpers.js";
export function createTaskUpdateTool(api: OpenClawPluginApi) {
return (ctx: ToolContext) => ({
@@ -69,76 +57,40 @@ Examples:
const issueId = params.issueId as number;
const newState = params.state as StateLabel;
const reason = (params.reason as string) ?? undefined;
const workspaceDir = ctx.workspaceDir;
const workspaceDir = requireWorkspaceDir(ctx);
if (!workspaceDir) {
throw new Error("No workspace directory available in tool context");
}
const { project } = await resolveProject(workspaceDir, groupId);
const { provider, type: providerType } = resolveProvider(project);
// 1. Resolve project
const data = await readProjects(workspaceDir);
const project = data.projects[groupId];
if (!project) {
throw new Error(
`Project not found for groupId ${groupId}. Run project_register first.`,
);
}
// 2. Create provider
const { provider, type: providerType } = createProvider({
repo: project.repo,
});
// 3. Fetch current issue to get current state
const issue = await provider.getIssue(issueId);
const currentState = provider.getCurrentStateLabel(issue);
if (!currentState) {
throw new Error(
`Issue #${issueId} has no recognized state label. Cannot perform transition.`,
);
throw new Error(`Issue #${issueId} has no recognized state label. Cannot perform transition.`);
}
if (currentState === newState) {
return jsonResult({
success: true,
issueId,
state: newState,
changed: false,
success: true, issueId, state: newState, changed: false,
message: `Issue #${issueId} is already in state "${newState}".`,
project: project.name,
provider: providerType,
project: project.name, provider: providerType,
});
}
// 4. Perform the transition
await provider.transitionLabel(issueId, currentState, newState);
// 5. Audit log
await auditLog(workspaceDir, "task_update", {
project: project.name,
groupId,
issueId,
fromState: currentState,
toState: newState,
reason: reason ?? null,
provider: providerType,
project: project.name, groupId, issueId,
fromState: currentState, toState: newState,
reason: reason ?? null, provider: providerType,
});
// 6. Build response
const result = {
success: true,
issueId,
issueTitle: issue.title,
state: newState,
changed: true,
return jsonResult({
success: true, issueId, issueTitle: issue.title,
state: newState, changed: true,
labelTransition: `${currentState}${newState}`,
project: project.name,
provider: providerType,
project: project.name, provider: providerType,
announcement: `🔄 Updated #${issueId}: "${currentState}" → "${newState}"${reason ? ` (${reason})` : ""}`,
};
return jsonResult(result);
});
},
});
}

View File

@@ -7,12 +7,12 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { jsonResult } from "openclaw/plugin-sdk";
import type { ToolContext } from "../types.js";
import { readProjects, getProject, getWorker, resolveRepoPath } from "../projects.js";
import { createProvider } from "../providers/index.js";
import { getWorker, resolveRepoPath } from "../projects.js";
import { executeCompletion, getRule, NEXT_STATE } from "../services/pipeline.js";
import { projectTick, type TickResult } from "../services/tick.js";
import { log as auditLog } from "../audit.js";
import { notify, getNotificationConfig } from "../notify.js";
import { requireWorkspaceDir, resolveProject, resolveProvider, getPluginConfig } from "../tool-helpers.js";
export function createWorkFinishTool(api: OpenClawPluginApi) {
return (ctx: ToolContext) => ({
@@ -37,9 +37,7 @@ export function createWorkFinishTool(api: OpenClawPluginApi) {
const groupId = params.projectGroupId as string;
const summary = params.summary as string | undefined;
const prUrl = params.prUrl as string | undefined;
const workspaceDir = ctx.workspaceDir;
if (!workspaceDir) throw new Error("No workspace directory available");
const workspaceDir = requireWorkspaceDir(ctx);
// Validate role:result
if (role === "dev" && result !== "done" && result !== "blocked")
@@ -50,17 +48,14 @@ export function createWorkFinishTool(api: OpenClawPluginApi) {
throw new Error(`Invalid completion: ${role}:${result}`);
// Resolve project + worker
const data = await readProjects(workspaceDir);
const project = getProject(data, groupId);
if (!project) throw new Error(`Project not found for groupId: ${groupId}`);
const { project } = await resolveProject(workspaceDir, groupId);
const worker = getWorker(project, role);
if (!worker.active) throw new Error(`${role.toUpperCase()} worker not active on ${project.name}`);
const issueId = worker.issueId ? Number(worker.issueId.split(",")[0]) : null;
if (!issueId) throw new Error(`No issueId for active ${role.toUpperCase()} on ${project.name}`);
const { provider } = createProvider({ repo: project.repo });
const { provider } = resolveProvider(project);
const repoPath = resolveRepoPath(project.repo);
// Execute completion (pipeline service)
@@ -74,7 +69,7 @@ export function createWorkFinishTool(api: OpenClawPluginApi) {
};
// Tick: fill free slots after completion
const pluginConfig = api.pluginConfig as Record<string, unknown> | undefined;
const pluginConfig = getPluginConfig(api);
let tickResult: TickResult | null = null;
try {
tickResult = await projectTick({

View File

@@ -2,20 +2,19 @@
* work_start — Pick up a task from the issue queue.
*
* Context-aware: ONLY works in project group chats.
* Auto-detects: projectGroupId, role, model, issueId.
* Auto-detects: projectGroupId, role, tier, 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 { createProvider } from "../providers/index.js";
import { selectModel } from "../model-selector.js";
import { activateWorker, getProject, getWorker, readProjects } from "../projects.js";
import { selectTier } from "../model-selector.js";
import { getWorker } from "../projects.js";
import { dispatchTask } from "../dispatch.js";
import { detectContext, generateGuardrails } from "../context-guard.js";
import { notify, getNotificationConfig } from "../notify.js";
import { findNextIssue, detectRoleFromLabel, detectTierFromLabels, projectTick, type TickResult } from "../services/tick.js";
import { requireWorkspaceDir, resolveContext, resolveProject, resolveProvider, groupOnlyError, getPluginConfig } from "../tool-helpers.js";
export function createWorkStartTool(api: OpenClawPluginApi) {
return (ctx: ToolContext) => ({
@@ -28,7 +27,7 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
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." },
model: { type: "string", description: "Developer tier (junior/medior/senior/qa). Auto-detected if omitted." },
tier: { type: "string", description: "Developer tier (junior/medior/senior/qa). Auto-detected if omitted." },
},
},
@@ -36,28 +35,16 @@ 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 modelParam = params.model as string | undefined;
const workspaceDir = ctx.workspaceDir;
if (!workspaceDir) throw new Error("No workspace directory available");
const tierParam = params.tier as string | undefined;
const workspaceDir = requireWorkspaceDir(ctx);
// Context guard: group only
const devClawAgentIds = ((api.pluginConfig as Record<string, unknown>)?.devClawAgentIds as string[] | undefined) ?? [];
const context = await detectContext(ctx, devClawAgentIds);
if (context.type !== "group") {
return jsonResult({
success: false,
error: "work_start can only be used in project group chats.",
recommendation: context.type === "via-agent" ? "Use onboard instead for setup." : "Use the relevant project group.",
contextGuidance: generateGuardrails(context),
});
}
const context = await resolveContext(ctx, api);
if (context.type !== "group") return groupOnlyError("work_start", context);
const groupId = groupIdParam ?? context.groupId;
const data = await readProjects(workspaceDir);
const project = getProject(data, groupId);
if (!project) throw new Error(`Project not found for groupId: ${groupId}`);
const { provider } = createProvider({ repo: project.repo });
const { project } = await resolveProject(workspaceDir, groupId);
const { provider } = resolveProvider(project);
// Find issue
let issue: { iid: number; title: string; description: string; labels: string[]; web_url: string; state: string };
@@ -88,47 +75,37 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
if (getWorker(project, other).active) throw new Error(`Sequential roleExecution: ${other.toUpperCase()} is active`);
}
// Select model
// Select tier
const targetLabel: StateLabel = role === "dev" ? "Doing" : "Testing";
let modelAlias: string, modelReason: string, modelSource: string;
if (modelParam) {
modelAlias = modelParam; modelReason = "LLM-selected"; modelSource = "llm";
let selectedTier: string, tierReason: string, tierSource: string;
if (tierParam) {
selectedTier = tierParam; tierReason = "LLM-selected"; tierSource = "llm";
} else {
const tier = detectTierFromLabels(issue.labels);
if (tier) {
if (role === "qa" && tier !== "qa") { modelAlias = "qa"; modelReason = `QA overrides "${tier}"`; modelSource = "role-override"; }
else if (role === "dev" && tier === "qa") { const s = selectModel(issue.title, issue.description ?? "", role); modelAlias = s.tier; modelReason = s.reason; modelSource = "heuristic"; }
else { modelAlias = tier; modelReason = `Label: "${tier}"`; modelSource = "label"; }
const labelTier = detectTierFromLabels(issue.labels);
if (labelTier) {
if (role === "qa" && labelTier !== "qa") { selectedTier = "qa"; tierReason = `QA overrides "${labelTier}"`; tierSource = "role-override"; }
else if (role === "dev" && labelTier === "qa") { const s = selectTier(issue.title, issue.description ?? "", role); selectedTier = s.tier; tierReason = s.reason; tierSource = "heuristic"; }
else { selectedTier = labelTier; tierReason = `Label: "${labelTier}"`; tierSource = "label"; }
} else {
const s = selectModel(issue.title, issue.description ?? "", role);
modelAlias = s.tier; modelReason = s.reason; modelSource = "heuristic";
const s = selectTier(issue.title, issue.description ?? "", role);
selectedTier = s.tier; tierReason = s.reason; tierSource = "heuristic";
}
}
// Dispatch
const pluginConfig = api.pluginConfig as Record<string, unknown> | undefined;
const pluginConfig = getPluginConfig(api);
const dr = await dispatchTask({
workspaceDir, agentId: ctx.agentId, groupId, project, issueId: issue.iid,
issueTitle: issue.title, issueDescription: issue.description ?? "", issueUrl: issue.web_url,
role, modelAlias, fromLabel: currentLabel, toLabel: targetLabel,
role, tier: selectedTier, fromLabel: currentLabel, toLabel: targetLabel,
transitionLabel: (id, from, to) => provider.transitionLabel(id, from as StateLabel, to as StateLabel),
pluginConfig, sessionKey: ctx.sessionKey,
});
// Ensure worker state
const stateUpdate: { issueId: string; model: string; sessionKey?: string; startTime?: string } = {
issueId: String(issue.iid), model: modelAlias,
};
if (dr.sessionAction === "spawn") {
stateUpdate.sessionKey = dr.sessionKey;
stateUpdate.startTime = new Date().toISOString();
}
await activateWorker(workspaceDir, groupId, role, stateUpdate);
// Notify
const notifyConfig = getNotificationConfig(pluginConfig);
await notify(
{ type: "workerStart", project: project.name, groupId, issueId: issue.iid, issueTitle: issue.title, issueUrl: issue.web_url, role, model: dr.modelAlias, sessionAction: dr.sessionAction },
{ type: "workerStart", project: project.name, groupId, issueId: issue.iid, issueTitle: issue.title, issueUrl: issue.web_url, role, tier: dr.tier, sessionAction: dr.sessionAction },
{ workspaceDir, config: notifyConfig, groupId, channel: context.channel },
);
@@ -143,10 +120,10 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
const output: Record<string, unknown> = {
success: true, project: project.name, groupId, issueId: issue.iid, issueTitle: issue.title,
role, model: dr.modelAlias, fullModel: dr.fullModel, sessionAction: dr.sessionAction,
role, tier: dr.tier, model: dr.model, sessionAction: dr.sessionAction,
announcement: dr.announcement, labelTransition: `${currentLabel}${targetLabel}`,
modelReason, modelSource,
autoDetected: { projectGroupId: !groupIdParam, role: !roleParam, issueId: issueIdParam === undefined, model: !modelParam },
tierReason, tierSource,
autoDetected: { projectGroupId: !groupIdParam, role: !roleParam, issueId: issueIdParam === undefined, tier: !tierParam },
};
if (tickResult?.pickups.length) output.tickPickups = tickResult.pickups;