feat: enhance role-tier structure for models and update related configurations
This commit is contained in:
40
index.ts
40
index.ts
@@ -38,8 +38,8 @@ const plugin = {
|
|||||||
type: "object",
|
type: "object",
|
||||||
description: "QA tier models",
|
description: "QA tier models",
|
||||||
properties: {
|
properties: {
|
||||||
"qa-engineer": { type: "string" },
|
reviewer: { type: "string" },
|
||||||
"manual-tester": { type: "string" },
|
tester: { type: "string" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -47,7 +47,8 @@ const plugin = {
|
|||||||
projectExecution: {
|
projectExecution: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["parallel", "sequential"],
|
enum: ["parallel", "sequential"],
|
||||||
description: "Plugin-level: parallel (each project independent) or sequential (one project at a time)",
|
description:
|
||||||
|
"Plugin-level: parallel (each project independent) or sequential (one project at a time)",
|
||||||
default: "parallel",
|
default: "parallel",
|
||||||
},
|
},
|
||||||
notifications: {
|
notifications: {
|
||||||
@@ -61,11 +62,24 @@ const plugin = {
|
|||||||
},
|
},
|
||||||
work_heartbeat: {
|
work_heartbeat: {
|
||||||
type: "object",
|
type: "object",
|
||||||
description: "Token-free interval-based heartbeat service. Runs health checks + queue dispatch automatically.",
|
description:
|
||||||
|
"Token-free interval-based heartbeat service. Runs health checks + queue dispatch automatically.",
|
||||||
properties: {
|
properties: {
|
||||||
enabled: { type: "boolean", default: true, description: "Enable the heartbeat service." },
|
enabled: {
|
||||||
intervalSeconds: { type: "number", default: 60, description: "Seconds between ticks." },
|
type: "boolean",
|
||||||
maxPickupsPerTick: { type: "number", default: 4, description: "Max worker dispatches per tick." },
|
default: true,
|
||||||
|
description: "Enable the heartbeat service.",
|
||||||
|
},
|
||||||
|
intervalSeconds: {
|
||||||
|
type: "number",
|
||||||
|
default: 60,
|
||||||
|
description: "Seconds between ticks.",
|
||||||
|
},
|
||||||
|
maxPickupsPerTick: {
|
||||||
|
type: "number",
|
||||||
|
default: 4,
|
||||||
|
description: "Max worker dispatches per tick.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -84,10 +98,14 @@ const plugin = {
|
|||||||
// Operations
|
// Operations
|
||||||
api.registerTool(createStatusTool(api), { names: ["status"] });
|
api.registerTool(createStatusTool(api), { names: ["status"] });
|
||||||
api.registerTool(createHealthTool(api), { names: ["health"] });
|
api.registerTool(createHealthTool(api), { names: ["health"] });
|
||||||
api.registerTool(createWorkHeartbeatTool(api), { names: ["work_heartbeat"] });
|
api.registerTool(createWorkHeartbeatTool(api), {
|
||||||
|
names: ["work_heartbeat"],
|
||||||
|
});
|
||||||
|
|
||||||
// Setup & config
|
// Setup & config
|
||||||
api.registerTool(createProjectRegisterTool(api), { names: ["project_register"] });
|
api.registerTool(createProjectRegisterTool(api), {
|
||||||
|
names: ["project_register"],
|
||||||
|
});
|
||||||
api.registerTool(createSetupTool(api), { names: ["setup"] });
|
api.registerTool(createSetupTool(api), { names: ["setup"] });
|
||||||
api.registerTool(createOnboardTool(api), { names: ["onboard"] });
|
api.registerTool(createOnboardTool(api), { names: ["onboard"] });
|
||||||
|
|
||||||
@@ -99,7 +117,9 @@ const plugin = {
|
|||||||
// Services
|
// Services
|
||||||
registerHeartbeatService(api);
|
registerHeartbeatService(api);
|
||||||
|
|
||||||
api.logger.info("DevClaw plugin registered (11 tools, 1 service, 1 CLI command)");
|
api.logger.info(
|
||||||
|
"DevClaw plugin registered (11 tools, 1 service, 1 CLI command)",
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ export function registerCli(program: Command): void {
|
|||||||
.option("--junior <model>", `Junior dev model (default: ${DEFAULT_MODELS.junior})`)
|
.option("--junior <model>", `Junior dev model (default: ${DEFAULT_MODELS.junior})`)
|
||||||
.option("--medior <model>", `Medior dev model (default: ${DEFAULT_MODELS.medior})`)
|
.option("--medior <model>", `Medior dev model (default: ${DEFAULT_MODELS.medior})`)
|
||||||
.option("--senior <model>", `Senior dev model (default: ${DEFAULT_MODELS.senior})`)
|
.option("--senior <model>", `Senior dev model (default: ${DEFAULT_MODELS.senior})`)
|
||||||
.option("--qa <model>", `QA engineer model (default: ${DEFAULT_MODELS.qa})`)
|
.option("--reviewer <model>", `Reviewer model (default: ${DEFAULT_MODELS.reviewer})`)
|
||||||
|
.option("--tester <model>", `Tester model (default: ${DEFAULT_MODELS.tester})`)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
const models: Partial<Record<Tier, string>> = {};
|
const models: Partial<Record<Tier, string>> = {};
|
||||||
if (opts.junior) models.junior = opts.junior;
|
if (opts.junior) models.junior = opts.junior;
|
||||||
if (opts.medior) models.medior = opts.medior;
|
if (opts.medior) models.medior = opts.medior;
|
||||||
if (opts.senior) models.senior = opts.senior;
|
if (opts.senior) models.senior = opts.senior;
|
||||||
if (opts.qa) models.qa = opts.qa;
|
if (opts.reviewer) models.reviewer = opts.reviewer;
|
||||||
|
if (opts.tester) models.tester = opts.tester;
|
||||||
|
|
||||||
const result = await runSetup({
|
const result = await runSetup({
|
||||||
newAgentName: opts.newAgent,
|
newAgentName: opts.newAgent,
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { TIER_MIGRATION } from "./tiers.js";
|
|
||||||
|
|
||||||
export type WorkerState = {
|
export type WorkerState = {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
issueId: string | null;
|
issueId: string | null;
|
||||||
@@ -36,63 +34,13 @@ export type ProjectsData = {
|
|||||||
projects: Record<string, Project>;
|
projects: Record<string, Project>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
function parseWorkerState(worker: Record<string, unknown>): WorkerState {
|
||||||
* Migrate old WorkerState schema to current format.
|
|
||||||
*
|
|
||||||
* Handles three migrations:
|
|
||||||
* 1. Old sessionId field → sessions map (pre-sessions era)
|
|
||||||
* 2. Model-alias session keys → tier-name keys (haiku→junior, sonnet→medior, etc.)
|
|
||||||
* 3. Old "model" field name → "tier" field name
|
|
||||||
*/
|
|
||||||
function migrateWorkerState(worker: Record<string, unknown>): WorkerState {
|
|
||||||
// Read tier from either "tier" (new) or "model" (old) field
|
|
||||||
const rawTier = (worker.tier ?? worker.model) as string | null;
|
|
||||||
|
|
||||||
// Migration 1: old sessionId field → sessions map
|
|
||||||
if (!worker.sessions || typeof worker.sessions !== "object") {
|
|
||||||
const sessionId = worker.sessionId as string | null;
|
|
||||||
const sessions: Record<string, string | null> = {};
|
|
||||||
|
|
||||||
if (sessionId && rawTier) {
|
|
||||||
const tierKey = TIER_MIGRATION[rawTier] ?? rawTier;
|
|
||||||
sessions[tierKey] = sessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active: worker.active as boolean,
|
active: worker.active as boolean,
|
||||||
issueId: worker.issueId as string | null,
|
issueId: worker.issueId as string | null,
|
||||||
startTime: worker.startTime as string | null,
|
startTime: worker.startTime as string | null,
|
||||||
tier: rawTier ? (TIER_MIGRATION[rawTier] ?? rawTier) : null,
|
tier: worker.tier as string | null,
|
||||||
sessions,
|
sessions: (worker.sessions as Record<string, string | null>) ?? {},
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration 2: model-alias session keys → tier-name keys
|
|
||||||
const oldSessions = worker.sessions as Record<string, string | null>;
|
|
||||||
const needsMigration = Object.keys(oldSessions).some((key) => key in TIER_MIGRATION);
|
|
||||||
|
|
||||||
if (needsMigration) {
|
|
||||||
const newSessions: Record<string, string | null> = {};
|
|
||||||
for (const [key, value] of Object.entries(oldSessions)) {
|
|
||||||
const newKey = TIER_MIGRATION[key] ?? key;
|
|
||||||
newSessions[newKey] = value;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
active: worker.active as boolean,
|
|
||||||
issueId: worker.issueId as string | null,
|
|
||||||
startTime: worker.startTime as string | null,
|
|
||||||
tier: rawTier ? (TIER_MIGRATION[rawTier] ?? rawTier) : null,
|
|
||||||
sessions: newSessions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration 3: "model" field → "tier" field (already handled by rawTier above)
|
|
||||||
return {
|
|
||||||
active: worker.active as boolean,
|
|
||||||
issueId: worker.issueId as string | null,
|
|
||||||
startTime: worker.startTime as string | null,
|
|
||||||
tier: rawTier ? (TIER_MIGRATION[rawTier] ?? rawTier) : null,
|
|
||||||
sessions: oldSessions,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,10 +81,10 @@ export async function readProjects(workspaceDir: string): Promise<ProjectsData>
|
|||||||
|
|
||||||
for (const project of Object.values(data.projects)) {
|
for (const project of Object.values(data.projects)) {
|
||||||
project.dev = project.dev
|
project.dev = project.dev
|
||||||
? migrateWorkerState(project.dev as unknown as Record<string, unknown>)
|
? parseWorkerState(project.dev as unknown as Record<string, unknown>)
|
||||||
: emptyWorkerState([]);
|
: emptyWorkerState([]);
|
||||||
project.qa = project.qa
|
project.qa = project.qa
|
||||||
? migrateWorkerState(project.qa as unknown as Record<string, unknown>)
|
? parseWorkerState(project.qa as unknown as Record<string, unknown>)
|
||||||
: emptyWorkerState([]);
|
: emptyWorkerState([]);
|
||||||
if (!project.channel) {
|
if (!project.channel) {
|
||||||
project.channel = "telegram";
|
project.channel = "telegram";
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { createProvider } from "../providers/index.js";
|
|||||||
import { selectTier } from "../model-selector.js";
|
import { selectTier } from "../model-selector.js";
|
||||||
import { getWorker, getSessionForTier, readProjects } from "../projects.js";
|
import { getWorker, getSessionForTier, readProjects } from "../projects.js";
|
||||||
import { dispatchTask } from "../dispatch.js";
|
import { dispatchTask } from "../dispatch.js";
|
||||||
import { ALL_TIERS, type Tier } from "../tiers.js";
|
import { ALL_TIERS, isDevTier, type Tier } from "../tiers.js";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Shared constants + helpers (used by tick, work-start, auto-pickup)
|
// Shared constants + helpers (used by tick, work-start, auto-pickup)
|
||||||
@@ -189,8 +189,10 @@ export async function projectTick(opts: {
|
|||||||
function resolveTierForIssue(issue: Issue, role: "dev" | "qa"): string {
|
function resolveTierForIssue(issue: Issue, role: "dev" | "qa"): string {
|
||||||
const labelTier = detectTierFromLabels(issue.labels);
|
const labelTier = detectTierFromLabels(issue.labels);
|
||||||
if (labelTier) {
|
if (labelTier) {
|
||||||
if (role === "qa" && labelTier !== "qa") return "qa";
|
// QA role but label specifies a dev tier → heuristic picks the right QA tier
|
||||||
if (role === "dev" && labelTier === "qa") return selectTier(issue.title, issue.description ?? "", role).tier;
|
if (role === "qa" && isDevTier(labelTier)) return selectTier(issue.title, issue.description ?? "", role).tier;
|
||||||
|
// DEV role but label specifies a QA tier → heuristic picks the right dev tier
|
||||||
|
if (role === "dev" && !isDevTier(labelTier)) return selectTier(issue.title, issue.description ?? "", role).tier;
|
||||||
return labelTier;
|
return labelTier;
|
||||||
}
|
}
|
||||||
return selectTier(issue.title, issue.description ?? "", role).tier;
|
return selectTier(issue.title, issue.description ?? "", role).tier;
|
||||||
|
|||||||
26
lib/tiers.ts
26
lib/tiers.ts
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const DEV_TIERS = ["junior", "medior", "senior"] as const;
|
export const DEV_TIERS = ["junior", "medior", "senior"] as const;
|
||||||
export const QA_TIERS = ["qa-engineer", "manual-tester"] as const;
|
export const QA_TIERS = ["reviewer", "tester"] as const;
|
||||||
export const ALL_TIERS = [...DEV_TIERS, ...QA_TIERS] as const;
|
export const ALL_TIERS = [...DEV_TIERS, ...QA_TIERS] as const;
|
||||||
|
|
||||||
export type DevTier = (typeof DEV_TIERS)[number];
|
export type DevTier = (typeof DEV_TIERS)[number];
|
||||||
@@ -17,8 +17,8 @@ export const DEFAULT_MODELS: Record<Tier, string> = {
|
|||||||
junior: "anthropic/claude-haiku-4-5",
|
junior: "anthropic/claude-haiku-4-5",
|
||||||
medior: "anthropic/claude-sonnet-4-5",
|
medior: "anthropic/claude-sonnet-4-5",
|
||||||
senior: "anthropic/claude-opus-4-5",
|
senior: "anthropic/claude-opus-4-5",
|
||||||
"qa-engineer": "anthropic/claude-sonnet-4-5",
|
reviewer: "anthropic/claude-sonnet-4-5",
|
||||||
"manual-tester": "anthropic/claude-haiku-4-5",
|
tester: "anthropic/claude-haiku-4-5",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Default models by role-tier structure. */
|
/** Default models by role-tier structure. */
|
||||||
@@ -29,8 +29,8 @@ export const DEFAULT_MODELS_BY_ROLE = {
|
|||||||
senior: "anthropic/claude-opus-4-5",
|
senior: "anthropic/claude-opus-4-5",
|
||||||
},
|
},
|
||||||
qa: {
|
qa: {
|
||||||
"qa-engineer": "anthropic/claude-sonnet-4-5",
|
reviewer: "anthropic/claude-sonnet-4-5",
|
||||||
"manual-tester": "anthropic/claude-haiku-4-5",
|
tester: "anthropic/claude-haiku-4-5",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,8 +39,8 @@ export const TIER_EMOJI: Record<Tier, string> = {
|
|||||||
junior: "⚡",
|
junior: "⚡",
|
||||||
medior: "🔧",
|
medior: "🔧",
|
||||||
senior: "🧠",
|
senior: "🧠",
|
||||||
"qa-engineer": "🔍",
|
reviewer: "🔍",
|
||||||
"manual-tester": "👀",
|
tester: "👀",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Check if a string is a valid tier name. */
|
/** Check if a string is a valid tier name. */
|
||||||
@@ -83,15 +83,3 @@ export function resolveTierToModel(
|
|||||||
|
|
||||||
return DEFAULT_MODELS[tier as Tier] ?? tier;
|
return DEFAULT_MODELS[tier as Tier] ?? tier;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Migration map from old model-alias session keys to tier names.
|
|
||||||
* Used by migrateWorkerState() in projects.ts.
|
|
||||||
*/
|
|
||||||
export const TIER_MIGRATION: Record<string, string> = {
|
|
||||||
haiku: "junior",
|
|
||||||
sonnet: "medior",
|
|
||||||
opus: "senior",
|
|
||||||
grok: "qa-engineer",
|
|
||||||
qa: "qa-engineer",
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -18,56 +18,113 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
|||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
newAgentName: { type: "string", description: "Create a new agent. Omit to configure current workspace." },
|
newAgentName: {
|
||||||
channelBinding: { type: "string", enum: ["telegram", "whatsapp"], description: "Channel to bind (optional, with newAgentName only)." },
|
type: "string",
|
||||||
migrateFrom: { type: "string", description: "Agent ID to migrate channel binding from. Check openclaw.json bindings first." },
|
description:
|
||||||
|
"Create a new agent. Omit to configure current workspace.",
|
||||||
|
},
|
||||||
|
channelBinding: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["telegram", "whatsapp"],
|
||||||
|
description: "Channel to bind (optional, with newAgentName only).",
|
||||||
|
},
|
||||||
|
migrateFrom: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"Agent ID to migrate channel binding from. Check openclaw.json bindings first.",
|
||||||
|
},
|
||||||
models: {
|
models: {
|
||||||
type: "object", description: "Model overrides per role and tier.",
|
type: "object",
|
||||||
|
description: "Model overrides per role and tier.",
|
||||||
properties: {
|
properties: {
|
||||||
dev: {
|
dev: {
|
||||||
type: "object", description: "Developer tier models",
|
type: "object",
|
||||||
|
description: "Developer tier models",
|
||||||
properties: {
|
properties: {
|
||||||
junior: { type: "string", description: `Default: ${DEFAULT_MODELS.junior}` },
|
junior: {
|
||||||
medior: { type: "string", description: `Default: ${DEFAULT_MODELS.medior}` },
|
type: "string",
|
||||||
senior: { type: "string", description: `Default: ${DEFAULT_MODELS.senior}` },
|
description: `Default: ${DEFAULT_MODELS.junior}`,
|
||||||
|
},
|
||||||
|
medior: {
|
||||||
|
type: "string",
|
||||||
|
description: `Default: ${DEFAULT_MODELS.medior}`,
|
||||||
|
},
|
||||||
|
senior: {
|
||||||
|
type: "string",
|
||||||
|
description: `Default: ${DEFAULT_MODELS.senior}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
qa: {
|
qa: {
|
||||||
type: "object", description: "QA tier models",
|
type: "object",
|
||||||
|
description: "QA tier models",
|
||||||
properties: {
|
properties: {
|
||||||
"qa-engineer": { type: "string", description: `Default: ${DEFAULT_MODELS["qa-engineer"]}` },
|
reviewer: {
|
||||||
"manual-tester": { type: "string", description: `Default: ${DEFAULT_MODELS["manual-tester"]}` },
|
type: "string",
|
||||||
|
description: `Default: ${DEFAULT_MODELS.reviewer}`,
|
||||||
|
},
|
||||||
|
tester: {
|
||||||
|
type: "string",
|
||||||
|
description: `Default: ${DEFAULT_MODELS.tester}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
projectExecution: { type: "string", enum: ["parallel", "sequential"], description: "Project execution mode. Default: parallel." },
|
},
|
||||||
|
projectExecution: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["parallel", "sequential"],
|
||||||
|
description: "Project execution mode. Default: parallel.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
async execute(_id: string, params: Record<string, unknown>) {
|
async execute(_id: string, params: Record<string, unknown>) {
|
||||||
const result = await runSetup({
|
const result = await runSetup({
|
||||||
newAgentName: params.newAgentName as string | undefined,
|
newAgentName: params.newAgentName as string | undefined,
|
||||||
channelBinding: (params.channelBinding as "telegram" | "whatsapp") ?? null,
|
channelBinding:
|
||||||
|
(params.channelBinding as "telegram" | "whatsapp") ?? null,
|
||||||
migrateFrom: params.migrateFrom as string | undefined,
|
migrateFrom: params.migrateFrom as string | undefined,
|
||||||
agentId: params.newAgentName ? undefined : ctx.agentId,
|
agentId: params.newAgentName ? undefined : ctx.agentId,
|
||||||
workspacePath: params.newAgentName ? undefined : ctx.workspaceDir,
|
workspacePath: params.newAgentName ? undefined : ctx.workspaceDir,
|
||||||
models: params.models as Partial<Record<Tier, string>> | undefined,
|
models: params.models as Partial<Record<Tier, string>> | undefined,
|
||||||
projectExecution: params.projectExecution as "parallel" | "sequential" | undefined,
|
projectExecution: params.projectExecution as
|
||||||
|
| "parallel"
|
||||||
|
| "sequential"
|
||||||
|
| undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
result.agentCreated ? `Agent "${result.agentId}" created` : `Configured "${result.agentId}"`,
|
result.agentCreated
|
||||||
|
? `Agent "${result.agentId}" created`
|
||||||
|
: `Configured "${result.agentId}"`,
|
||||||
"",
|
"",
|
||||||
];
|
];
|
||||||
if (result.bindingMigrated) {
|
if (result.bindingMigrated) {
|
||||||
lines.push(`✅ Binding migrated: ${result.bindingMigrated.channel} (${result.bindingMigrated.from} → ${result.agentId})`, "");
|
lines.push(
|
||||||
|
`✅ Binding migrated: ${result.bindingMigrated.channel} (${result.bindingMigrated.from} → ${result.agentId})`,
|
||||||
|
"",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
lines.push("Models:", ...ALL_TIERS.map((t) => ` ${t}: ${result.models[t]}`), "", "Files:", ...result.filesWritten.map((f) => ` ${f}`));
|
lines.push(
|
||||||
if (result.warnings.length > 0) lines.push("", "Warnings:", ...result.warnings.map((w) => ` ${w}`));
|
"Models:",
|
||||||
lines.push("", "Next: register a project, then create issues and pick them up.");
|
...ALL_TIERS.map((t) => ` ${t}: ${result.models[t]}`),
|
||||||
|
"",
|
||||||
|
"Files:",
|
||||||
|
...result.filesWritten.map((f) => ` ${f}`),
|
||||||
|
);
|
||||||
|
if (result.warnings.length > 0)
|
||||||
|
lines.push("", "Warnings:", ...result.warnings.map((w) => ` ${w}`));
|
||||||
|
lines.push(
|
||||||
|
"",
|
||||||
|
"Next: register a project, then create issues and pick them up.",
|
||||||
|
);
|
||||||
|
|
||||||
return jsonResult({ success: true, ...result, summary: lines.join("\n") });
|
return jsonResult({
|
||||||
|
success: true,
|
||||||
|
...result,
|
||||||
|
summary: lines.join("\n"),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { getWorker } from "../projects.js";
|
|||||||
import { dispatchTask } from "../dispatch.js";
|
import { dispatchTask } from "../dispatch.js";
|
||||||
import { notify, getNotificationConfig } from "../notify.js";
|
import { notify, getNotificationConfig } from "../notify.js";
|
||||||
import { findNextIssue, detectRoleFromLabel, detectTierFromLabels } from "../services/tick.js";
|
import { findNextIssue, detectRoleFromLabel, detectTierFromLabels } from "../services/tick.js";
|
||||||
|
import { isDevTier } from "../tiers.js";
|
||||||
import { requireWorkspaceDir, resolveContext, resolveProject, resolveProvider, groupOnlyError, getPluginConfig, tickAndNotify } from "../tool-helpers.js";
|
import { requireWorkspaceDir, resolveContext, resolveProject, resolveProvider, groupOnlyError, getPluginConfig, tickAndNotify } from "../tool-helpers.js";
|
||||||
|
|
||||||
export function createWorkStartTool(api: OpenClawPluginApi) {
|
export function createWorkStartTool(api: OpenClawPluginApi) {
|
||||||
@@ -83,8 +84,8 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
|||||||
} else {
|
} else {
|
||||||
const labelTier = detectTierFromLabels(issue.labels);
|
const labelTier = detectTierFromLabels(issue.labels);
|
||||||
if (labelTier) {
|
if (labelTier) {
|
||||||
if (role === "qa" && labelTier !== "qa") { selectedTier = "qa"; tierReason = `QA overrides "${labelTier}"`; tierSource = "role-override"; }
|
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" && labelTier === "qa") { const s = selectTier(issue.title, issue.description ?? "", role); selectedTier = s.tier; tierReason = s.reason; tierSource = "heuristic"; }
|
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"; }
|
else { selectedTier = labelTier; tierReason = `Label: "${labelTier}"`; tierSource = "label"; }
|
||||||
} else {
|
} else {
|
||||||
const s = selectTier(issue.title, issue.description ?? "", role);
|
const s = selectTier(issue.title, issue.description ?? "", role);
|
||||||
|
|||||||
@@ -5,26 +5,55 @@
|
|||||||
"configSchema": {
|
"configSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"devClawAgentIds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Agent IDs recognized as DevClaw orchestrators"
|
||||||
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Model mapping per developer tier (junior, medior, senior, qa)",
|
"description": "Model mapping per role and tier",
|
||||||
"properties": {
|
"properties": {
|
||||||
"junior": {
|
"dev": {
|
||||||
"type": "string",
|
"type": "object",
|
||||||
"description": "Junior dev model (default: anthropic/claude-haiku-4-5)"
|
"description": "Developer tier models",
|
||||||
},
|
"properties": {
|
||||||
"medior": {
|
"junior": { "type": "string" },
|
||||||
"type": "string",
|
"medior": { "type": "string" },
|
||||||
"description": "Medior dev model (default: anthropic/claude-sonnet-4-5)"
|
"senior": { "type": "string" }
|
||||||
},
|
}
|
||||||
"senior": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Senior dev model (default: anthropic/claude-opus-4-5)"
|
|
||||||
},
|
},
|
||||||
"qa": {
|
"qa": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "QA tier models",
|
||||||
|
"properties": {
|
||||||
|
"reviewer": { "type": "string" },
|
||||||
|
"tester": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projectExecution": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "QA engineer model (default: anthropic/claude-sonnet-4-5)"
|
"enum": ["parallel", "sequential"],
|
||||||
}
|
"description": "Project execution mode. Default: parallel."
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Notification settings",
|
||||||
|
"properties": {
|
||||||
|
"heartbeatDm": { "type": "boolean" },
|
||||||
|
"workerStart": { "type": "boolean" },
|
||||||
|
"workerComplete": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"work_heartbeat": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Token-free interval-based heartbeat service.",
|
||||||
|
"properties": {
|
||||||
|
"enabled": { "type": "boolean" },
|
||||||
|
"intervalSeconds": { "type": "number" },
|
||||||
|
"maxPickupsPerTick": { "type": "number" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user