feat: add TypeScript support and shared types
- Added TypeScript configuration file (tsconfig.json) with strict settings. - Introduced devDependencies for TypeScript in package.json. - Added scripts for type checking and watching for changes. - Created a new types file (lib/types.ts) defining shared types for the DevClaw plugin.
This commit is contained in:
@@ -5,26 +5,24 @@
|
|||||||
* state update (activateWorker), and audit logging.
|
* state update (activateWorker), and audit logging.
|
||||||
*/
|
*/
|
||||||
import { execFile } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
import { log as auditLog } from "./audit.js";
|
||||||
import {
|
import {
|
||||||
type Project,
|
type Project,
|
||||||
type WorkerState,
|
|
||||||
getWorker,
|
|
||||||
getSessionForModel,
|
|
||||||
activateWorker,
|
activateWorker,
|
||||||
|
getSessionForModel,
|
||||||
|
getWorker,
|
||||||
} from "./projects.js";
|
} from "./projects.js";
|
||||||
import { selectModel } from "./model-selector.js";
|
import { TIER_EMOJI, isTier, resolveModel } from "./tiers.js";
|
||||||
import { log as auditLog } from "./audit.js";
|
|
||||||
import { resolveModel, TIER_EMOJI, isTier } from "./tiers.js";
|
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
export type DispatchOpts = {
|
export type DispatchOpts = {
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
agentId: string;
|
agentId?: string;
|
||||||
groupId: string;
|
groupId: string;
|
||||||
project: Project;
|
project: Project;
|
||||||
issueId: number;
|
issueId: number;
|
||||||
@@ -69,12 +67,33 @@ async function buildTaskMessage(opts: {
|
|||||||
baseBranch: string;
|
baseBranch: string;
|
||||||
groupId: string;
|
groupId: string;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const { workspaceDir, projectName, role, issueId, issueTitle, issueDescription, issueUrl, repo, baseBranch, groupId } = opts;
|
const {
|
||||||
|
workspaceDir,
|
||||||
|
projectName,
|
||||||
|
role,
|
||||||
|
issueId,
|
||||||
|
issueTitle,
|
||||||
|
issueDescription,
|
||||||
|
issueUrl,
|
||||||
|
repo,
|
||||||
|
baseBranch,
|
||||||
|
groupId,
|
||||||
|
} = opts;
|
||||||
|
|
||||||
// Read role-specific instructions
|
// Read role-specific instructions
|
||||||
let roleInstructions = "";
|
let roleInstructions = "";
|
||||||
const projectRoleFile = path.join(workspaceDir, "roles", projectName, `${role}.md`);
|
const projectRoleFile = path.join(
|
||||||
const defaultRoleFile = path.join(workspaceDir, "roles", "default", `${role}.md`);
|
workspaceDir,
|
||||||
|
"roles",
|
||||||
|
projectName,
|
||||||
|
`${role}.md`,
|
||||||
|
);
|
||||||
|
const defaultRoleFile = path.join(
|
||||||
|
workspaceDir,
|
||||||
|
"roles",
|
||||||
|
"default",
|
||||||
|
`${role}.md`,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
roleInstructions = await fs.readFile(projectRoleFile, "utf-8");
|
roleInstructions = await fs.readFile(projectRoleFile, "utf-8");
|
||||||
} catch {
|
} catch {
|
||||||
@@ -110,11 +129,23 @@ async function buildTaskMessage(opts: {
|
|||||||
* (with label rollback). Logs warning on state update failure
|
* (with label rollback). Logs warning on state update failure
|
||||||
* (dispatch succeeded, session IS running).
|
* (dispatch succeeded, session IS running).
|
||||||
*/
|
*/
|
||||||
export async function dispatchTask(opts: DispatchOpts): Promise<DispatchResult> {
|
export async function dispatchTask(
|
||||||
|
opts: DispatchOpts,
|
||||||
|
): Promise<DispatchResult> {
|
||||||
const {
|
const {
|
||||||
workspaceDir, agentId, groupId, project, issueId,
|
workspaceDir,
|
||||||
issueTitle, issueDescription, issueUrl,
|
agentId,
|
||||||
role, modelAlias, fromLabel, toLabel, transitionLabel,
|
groupId,
|
||||||
|
project,
|
||||||
|
issueId,
|
||||||
|
issueTitle,
|
||||||
|
issueDescription,
|
||||||
|
issueUrl,
|
||||||
|
role,
|
||||||
|
modelAlias,
|
||||||
|
fromLabel,
|
||||||
|
toLabel,
|
||||||
|
transitionLabel,
|
||||||
pluginConfig,
|
pluginConfig,
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
@@ -146,18 +177,25 @@ export async function dispatchTask(opts: DispatchOpts): Promise<DispatchResult>
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (sessionAction === "spawn") {
|
if (sessionAction === "spawn") {
|
||||||
sessionKey = `agent:${agentId}:subagent:${randomUUID()}`;
|
sessionKey = `agent:${agentId ?? "unknown"}:subagent:${randomUUID()}`;
|
||||||
await execFileAsync("openclaw", [
|
await execFileAsync(
|
||||||
"gateway", "call", "sessions.patch",
|
"openclaw",
|
||||||
"--data", JSON.stringify({ key: sessionKey, model: fullModel }),
|
[
|
||||||
], { timeout: 30_000 });
|
"gateway",
|
||||||
|
"call",
|
||||||
|
"sessions.patch",
|
||||||
|
"--data",
|
||||||
|
JSON.stringify({ key: sessionKey, model: fullModel }),
|
||||||
|
],
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await execFileAsync("openclaw", [
|
await execFileAsync(
|
||||||
"agent",
|
"openclaw",
|
||||||
"--session-id", sessionKey!,
|
["agent", "--session-id", sessionKey!, "--message", taskMessage],
|
||||||
"--message", taskMessage,
|
{ timeout: 60_000 },
|
||||||
], { timeout: 60_000 });
|
);
|
||||||
|
|
||||||
dispatched = true;
|
dispatched = true;
|
||||||
|
|
||||||
@@ -224,7 +262,9 @@ export async function dispatchTask(opts: DispatchOpts): Promise<DispatchResult>
|
|||||||
// Build announcement
|
// Build announcement
|
||||||
const emoji = isTier(modelAlias)
|
const emoji = isTier(modelAlias)
|
||||||
? TIER_EMOJI[modelAlias]
|
? TIER_EMOJI[modelAlias]
|
||||||
: (role === "qa" ? "🔍" : "🔧");
|
: role === "qa"
|
||||||
|
? "🔍"
|
||||||
|
: "🔧";
|
||||||
const actionVerb = sessionAction === "spawn" ? "Spawning" : "Sending";
|
const actionVerb = sessionAction === "spawn" ? "Spawning" : "Sending";
|
||||||
const announcement = `${emoji} ${actionVerb} ${role.toUpperCase()} (${modelAlias}) for #${issueId}: ${issueTitle}`;
|
const announcement = `${emoji} ${actionVerb} ${role.toUpperCase()} (${modelAlias}) for #${issueId}: ${issueTitle}`;
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,16 @@
|
|||||||
* Creates a new agent (optional), configures model tiers,
|
* Creates a new agent (optional), configures model tiers,
|
||||||
* and writes workspace files (AGENTS.md, HEARTBEAT.md, roles, memory).
|
* and writes workspace files (AGENTS.md, HEARTBEAT.md, roles, memory).
|
||||||
*/
|
*/
|
||||||
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
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.js";
|
||||||
import { ALL_TIERS, DEFAULT_MODELS, type Tier } from "../tiers.js";
|
import { ALL_TIERS, DEFAULT_MODELS, type Tier } from "../tiers.js";
|
||||||
|
|
||||||
export function createSetupTool(api: OpenClawPluginApi) {
|
export function createSetupTool(api: OpenClawPluginApi) {
|
||||||
return (ctx: OpenClawPluginToolContext) => ({
|
return (ctx: ToolContext) => ({
|
||||||
name: "devclaw_setup",
|
name: "devclaw_setup",
|
||||||
|
label: "DevClaw Setup",
|
||||||
description: `Set up DevClaw in an agent's workspace. Creates AGENTS.md, HEARTBEAT.md, role templates, memory/projects.json, and writes model tier config to openclaw.json. Optionally creates a new agent. Backs up existing files before overwriting.`,
|
description: `Set up DevClaw in an agent's workspace. Creates AGENTS.md, HEARTBEAT.md, role templates, memory/projects.json, and writes model tier config to openclaw.json. Optionally creates a new agent. Backs up existing files before overwriting.`,
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
@@ -69,16 +72,11 @@ export function createSetupTool(api: OpenClawPluginApi) {
|
|||||||
` 3. Create your first issue and pick it up`,
|
` 3. Create your first issue and pick it up`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return jsonResult({
|
||||||
content: [{
|
|
||||||
type: "text" as const,
|
|
||||||
text: JSON.stringify({
|
|
||||||
success: true,
|
success: true,
|
||||||
...result,
|
...result,
|
||||||
summary: lines.join("\n"),
|
summary: lines.join("\n"),
|
||||||
}, null, 2),
|
});
|
||||||
}],
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
*
|
*
|
||||||
* Replaces the manual steps of running glab/gh label create + editing projects.json.
|
* Replaces the manual steps of running glab/gh label create + editing projects.json.
|
||||||
*/
|
*/
|
||||||
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
import { jsonResult } from "openclaw/plugin-sdk";
|
||||||
|
import type { ToolContext } from "../types.js";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { readProjects, writeProjects, emptyWorkerState } from "../projects.js";
|
import { readProjects, writeProjects, emptyWorkerState } from "../projects.js";
|
||||||
@@ -67,8 +69,9 @@ async function scaffoldRoleFiles(workspaceDir: string, projectName: string): Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createProjectRegisterTool(api: OpenClawPluginApi) {
|
export function createProjectRegisterTool(api: OpenClawPluginApi) {
|
||||||
return (ctx: OpenClawPluginToolContext) => ({
|
return (ctx: ToolContext) => ({
|
||||||
name: "project_register",
|
name: "project_register",
|
||||||
|
label: "Project Register",
|
||||||
description: `Register a new project with DevClaw. Creates all required state labels (idempotent) and adds the project to projects.json. One-time setup per project. Auto-detects GitHub/GitLab from git remote.`,
|
description: `Register a new project with DevClaw. Creates all required state labels (idempotent) and adds the project to projects.json. One-time setup per project. Auto-detects GitHub/GitLab from git remote.`,
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
@@ -186,10 +189,7 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) {
|
|||||||
const rolesNote = rolesCreated ? " Role files scaffolded." : "";
|
const rolesNote = rolesCreated ? " Role files scaffolded." : "";
|
||||||
const announcement = `📋 Project "${name}" registered for group ${groupName}. Labels created.${rolesNote} Ready for tasks.`;
|
const announcement = `📋 Project "${name}" registered for group ${groupName}. Labels created.${rolesNote} Ready for tasks.`;
|
||||||
|
|
||||||
return {
|
return jsonResult({
|
||||||
content: [{
|
|
||||||
type: "text" as const,
|
|
||||||
text: JSON.stringify({
|
|
||||||
success: true,
|
success: true,
|
||||||
project: name,
|
project: name,
|
||||||
groupId,
|
groupId,
|
||||||
@@ -199,9 +199,7 @@ export function createProjectRegisterTool(api: OpenClawPluginApi) {
|
|||||||
labelsCreated: 8,
|
labelsCreated: 8,
|
||||||
rolesScaffolded: rolesCreated,
|
rolesScaffolded: rolesCreated,
|
||||||
announcement,
|
announcement,
|
||||||
}, null, 2),
|
});
|
||||||
}],
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,17 @@
|
|||||||
*
|
*
|
||||||
* Replaces manual GitLab scanning in HEARTBEAT.md.
|
* Replaces manual GitLab scanning in HEARTBEAT.md.
|
||||||
*/
|
*/
|
||||||
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
import { jsonResult } from "openclaw/plugin-sdk";
|
||||||
|
import type { ToolContext } from "../types.js";
|
||||||
import { readProjects, getProject } from "../projects.js";
|
import { readProjects, getProject } from "../projects.js";
|
||||||
import { listIssuesByLabel, resolveRepoPath, type StateLabel } from "../gitlab.js";
|
import { listIssuesByLabel, resolveRepoPath, type StateLabel } from "../gitlab.js";
|
||||||
import { log as auditLog } from "../audit.js";
|
import { log as auditLog } from "../audit.js";
|
||||||
|
|
||||||
export function createQueueStatusTool(api: OpenClawPluginApi) {
|
export function createQueueStatusTool(api: OpenClawPluginApi) {
|
||||||
return (ctx: OpenClawPluginToolContext) => ({
|
return (ctx: ToolContext) => ({
|
||||||
name: "queue_status",
|
name: "queue_status",
|
||||||
|
label: "Queue Status",
|
||||||
description: `Show task queue counts and worker status for all projects (or a specific project). Returns To Improve, To Test, To Do issue counts and active DEV/QA session state.`,
|
description: `Show task queue counts and worker status for all projects (or a specific project). Returns To Improve, To Test, To Do issue counts and active DEV/QA session state.`,
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
@@ -98,14 +101,7 @@ export function createQueueStatusTool(api: OpenClawPluginApi) {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return jsonResult({ projects });
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text" as const,
|
|
||||||
text: JSON.stringify({ projects }, null, 2),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,17 @@
|
|||||||
* Detects zombie sessions (active=true but session dead) and stale workers.
|
* Detects zombie sessions (active=true but session dead) and stale workers.
|
||||||
* Checks the sessions map for each worker's current model.
|
* Checks the sessions map for each worker's current model.
|
||||||
*/
|
*/
|
||||||
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
import { jsonResult } from "openclaw/plugin-sdk";
|
||||||
|
import type { ToolContext } from "../types.js";
|
||||||
import { readProjects, updateWorker, getSessionForModel } from "../projects.js";
|
import { readProjects, updateWorker, getSessionForModel } from "../projects.js";
|
||||||
import { transitionLabel, resolveRepoPath, type StateLabel } from "../gitlab.js";
|
import { transitionLabel, resolveRepoPath, type StateLabel } from "../gitlab.js";
|
||||||
import { log as auditLog } from "../audit.js";
|
import { log as auditLog } from "../audit.js";
|
||||||
|
|
||||||
export function createSessionHealthTool(api: OpenClawPluginApi) {
|
export function createSessionHealthTool(api: OpenClawPluginApi) {
|
||||||
return (ctx: OpenClawPluginToolContext) => ({
|
return (ctx: ToolContext) => ({
|
||||||
name: "session_health",
|
name: "session_health",
|
||||||
|
label: "Session Health",
|
||||||
description: `Check session state consistency across all projects. Detects: active workers with no session in their sessions map, stale workers (>2 hours), and state mismatches. With autoFix=true, clears zombie states and reverts GitLab labels. Pass activeSessions (from sessions_list) so the tool can verify liveness.`,
|
description: `Check session state consistency across all projects. Detects: active workers with no session in their sessions map, stale workers (>2 hours), and state mismatches. With autoFix=true, clears zombie states and reverts GitLab labels. Pass activeSessions (from sessions_list) so the tool can verify liveness.`,
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
@@ -189,9 +192,7 @@ export function createSessionHealthTool(api: OpenClawPluginApi) {
|
|||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return jsonResult(result);
|
||||||
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,45 +8,59 @@
|
|||||||
* - DEV "done" → automatically dispatches QA (qa tier)
|
* - DEV "done" → automatically dispatches QA (qa tier)
|
||||||
* - QA "fail" → automatically dispatches DEV fix (reuses previous DEV tier)
|
* - QA "fail" → automatically dispatches DEV fix (reuses previous DEV tier)
|
||||||
*/
|
*/
|
||||||
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
|
||||||
import {
|
|
||||||
readProjects,
|
|
||||||
getProject,
|
|
||||||
getWorker,
|
|
||||||
getSessionForModel,
|
|
||||||
deactivateWorker,
|
|
||||||
} from "../projects.js";
|
|
||||||
import {
|
|
||||||
getIssue,
|
|
||||||
transitionLabel,
|
|
||||||
closeIssue,
|
|
||||||
reopenIssue,
|
|
||||||
resolveRepoPath,
|
|
||||||
type StateLabel,
|
|
||||||
} from "../gitlab.js";
|
|
||||||
import { log as auditLog } from "../audit.js";
|
|
||||||
import { dispatchTask } from "../dispatch.js";
|
|
||||||
import { execFile } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
import { jsonResult } from "openclaw/plugin-sdk";
|
||||||
|
import { log as auditLog } from "../audit.js";
|
||||||
|
import { dispatchTask } from "../dispatch.js";
|
||||||
|
import {
|
||||||
|
closeIssue,
|
||||||
|
getIssue,
|
||||||
|
reopenIssue,
|
||||||
|
resolveRepoPath,
|
||||||
|
transitionLabel,
|
||||||
|
type StateLabel,
|
||||||
|
} from "../gitlab.js";
|
||||||
|
import {
|
||||||
|
deactivateWorker,
|
||||||
|
getProject,
|
||||||
|
getSessionForModel,
|
||||||
|
getWorker,
|
||||||
|
readProjects,
|
||||||
|
} from "../projects.js";
|
||||||
|
import type { ToolContext } from "../types.js";
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
||||||
return (ctx: OpenClawPluginToolContext) => ({
|
return (ctx: ToolContext) => ({
|
||||||
name: "task_complete",
|
name: "task_complete",
|
||||||
|
label: "Task Complete",
|
||||||
description: `Complete a task: DEV done, QA pass, QA fail, or QA refine. Atomically handles: label transition, projects.json update, issue close/reopen, and audit logging. If the project has autoChain enabled, automatically dispatches the next step (DEV done → QA, QA fail → DEV fix).`,
|
description: `Complete a task: DEV done, QA pass, QA fail, or QA refine. Atomically handles: label transition, projects.json update, issue close/reopen, and audit logging. If the project has autoChain enabled, automatically dispatches the next step (DEV done → QA, QA fail → DEV fix).`,
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["role", "result", "projectGroupId"],
|
required: ["role", "result", "projectGroupId"],
|
||||||
properties: {
|
properties: {
|
||||||
role: { type: "string", enum: ["dev", "qa"], description: "Worker role completing the task" },
|
role: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["dev", "qa"],
|
||||||
|
description: "Worker role completing the task",
|
||||||
|
},
|
||||||
result: {
|
result: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["done", "pass", "fail", "refine"],
|
enum: ["done", "pass", "fail", "refine"],
|
||||||
description: 'Completion result: "done" (DEV finished), "pass" (QA approved), "fail" (QA found issues), "refine" (needs human input)',
|
description:
|
||||||
|
'Completion result: "done" (DEV finished), "pass" (QA approved), "fail" (QA found issues), "refine" (needs human input)',
|
||||||
|
},
|
||||||
|
projectGroupId: {
|
||||||
|
type: "string",
|
||||||
|
description: "Telegram group ID (key in projects.json)",
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
type: "string",
|
||||||
|
description: "Brief summary for Telegram announcement",
|
||||||
},
|
},
|
||||||
projectGroupId: { type: "string", description: "Telegram group ID (key in projects.json)" },
|
|
||||||
summary: { type: "string", description: "Brief summary for Telegram announcement" },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -63,10 +77,14 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
|
|
||||||
// Validate result matches role
|
// Validate result matches role
|
||||||
if (role === "dev" && result !== "done") {
|
if (role === "dev" && result !== "done") {
|
||||||
throw new Error(`DEV can only complete with result "done", got "${result}"`);
|
throw new Error(
|
||||||
|
`DEV can only complete with result "done", got "${result}"`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (role === "qa" && result === "done") {
|
if (role === "qa" && result === "done") {
|
||||||
throw new Error(`QA cannot use result "done". Use "pass", "fail", or "refine".`);
|
throw new Error(
|
||||||
|
`QA cannot use result "done". Use "pass", "fail", or "refine".`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve project
|
// Resolve project
|
||||||
@@ -83,14 +101,20 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const issueId = worker.issueId ? Number(worker.issueId.split(",")[0]) : null;
|
const issueId = worker.issueId
|
||||||
|
? Number(worker.issueId.split(",")[0])
|
||||||
|
: null;
|
||||||
if (!issueId) {
|
if (!issueId) {
|
||||||
throw new Error(`No issueId found for active ${role.toUpperCase()} worker on ${project.name}`);
|
throw new Error(
|
||||||
|
`No issueId found for active ${role.toUpperCase()} worker on ${project.name}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const repoPath = resolveRepoPath(project.repo);
|
const repoPath = resolveRepoPath(project.repo);
|
||||||
const glabOpts = {
|
const glabOpts = {
|
||||||
glabPath: (api.pluginConfig as Record<string, unknown>)?.glabPath as string | undefined,
|
glabPath: (api.pluginConfig as Record<string, unknown>)?.glabPath as
|
||||||
|
| string
|
||||||
|
| undefined,
|
||||||
repoPath,
|
repoPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,7 +130,10 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
// === DEV DONE ===
|
// === DEV DONE ===
|
||||||
if (role === "dev" && result === "done") {
|
if (role === "dev" && result === "done") {
|
||||||
try {
|
try {
|
||||||
await execFileAsync("git", ["pull"], { cwd: repoPath, timeout: 30_000 });
|
await execFileAsync("git", ["pull"], {
|
||||||
|
cwd: repoPath,
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
output.gitPull = "success";
|
output.gitPull = "success";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
output.gitPull = `warning: ${(err as Error).message}`;
|
output.gitPull = `warning: ${(err as Error).message}`;
|
||||||
@@ -120,7 +147,9 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
|
|
||||||
if (project.autoChain) {
|
if (project.autoChain) {
|
||||||
try {
|
try {
|
||||||
const pluginConfig = api.pluginConfig as Record<string, unknown> | undefined;
|
const pluginConfig = api.pluginConfig as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined;
|
||||||
const issue = await getIssue(issueId, glabOpts);
|
const issue = await getIssue(issueId, glabOpts);
|
||||||
const chainResult = await dispatchTask({
|
const chainResult = await dispatchTask({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
@@ -136,7 +165,12 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
fromLabel: "To Test",
|
fromLabel: "To Test",
|
||||||
toLabel: "Testing",
|
toLabel: "Testing",
|
||||||
transitionLabel: (id, from, to) =>
|
transitionLabel: (id, from, to) =>
|
||||||
transitionLabel(id, from as StateLabel, to as StateLabel, glabOpts),
|
transitionLabel(
|
||||||
|
id,
|
||||||
|
from as StateLabel,
|
||||||
|
to as StateLabel,
|
||||||
|
glabOpts,
|
||||||
|
),
|
||||||
pluginConfig,
|
pluginConfig,
|
||||||
});
|
});
|
||||||
output.autoChain = {
|
output.autoChain = {
|
||||||
@@ -147,7 +181,10 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
announcement: chainResult.announcement,
|
announcement: chainResult.announcement,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
output.autoChain = { dispatched: false, error: (err as Error).message };
|
output.autoChain = {
|
||||||
|
dispatched: false,
|
||||||
|
error: (err as Error).message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
output.nextAction = "qa_pickup";
|
output.nextAction = "qa_pickup";
|
||||||
@@ -173,7 +210,9 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
|
|
||||||
const devWorker = getWorker(project, "dev");
|
const devWorker = getWorker(project, "dev");
|
||||||
const devModel = devWorker.model;
|
const devModel = devWorker.model;
|
||||||
const devSessionKey = devModel ? getSessionForModel(devWorker, devModel) : null;
|
const devSessionKey = devModel
|
||||||
|
? getSessionForModel(devWorker, devModel)
|
||||||
|
: null;
|
||||||
|
|
||||||
output.labelTransition = "Testing → To Improve";
|
output.labelTransition = "Testing → To Improve";
|
||||||
output.issueReopened = true;
|
output.issueReopened = true;
|
||||||
@@ -183,7 +222,9 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
|
|
||||||
if (project.autoChain && devModel) {
|
if (project.autoChain && devModel) {
|
||||||
try {
|
try {
|
||||||
const pluginConfig = api.pluginConfig as Record<string, unknown> | undefined;
|
const pluginConfig = api.pluginConfig as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined;
|
||||||
const issue = await getIssue(issueId, glabOpts);
|
const issue = await getIssue(issueId, glabOpts);
|
||||||
const chainResult = await dispatchTask({
|
const chainResult = await dispatchTask({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
@@ -199,7 +240,12 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
fromLabel: "To Improve",
|
fromLabel: "To Improve",
|
||||||
toLabel: "Doing",
|
toLabel: "Doing",
|
||||||
transitionLabel: (id, from, to) =>
|
transitionLabel: (id, from, to) =>
|
||||||
transitionLabel(id, from as StateLabel, to as StateLabel, glabOpts),
|
transitionLabel(
|
||||||
|
id,
|
||||||
|
from as StateLabel,
|
||||||
|
to as StateLabel,
|
||||||
|
glabOpts,
|
||||||
|
),
|
||||||
pluginConfig,
|
pluginConfig,
|
||||||
});
|
});
|
||||||
output.autoChain = {
|
output.autoChain = {
|
||||||
@@ -210,7 +256,10 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
announcement: chainResult.announcement,
|
announcement: chainResult.announcement,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
output.autoChain = { dispatched: false, error: (err as Error).message };
|
output.autoChain = {
|
||||||
|
dispatched: false,
|
||||||
|
error: (err as Error).message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
output.nextAction = "dev_fix";
|
output.nextAction = "dev_fix";
|
||||||
@@ -238,9 +287,7 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
|
|||||||
autoChain: output.autoChain ?? null,
|
autoChain: output.autoChain ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return jsonResult(output);
|
||||||
content: [{ type: "text" as const, text: JSON.stringify(output, null, 2) }],
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,11 @@
|
|||||||
* - A sub-agent finds a bug and needs to file a follow-up issue
|
* - A sub-agent finds a bug and needs to file a follow-up issue
|
||||||
* - Breaking down an epic into smaller tasks
|
* - Breaking down an epic into smaller tasks
|
||||||
*/
|
*/
|
||||||
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
import { readProjects, resolveRepoPath } from "../projects.js";
|
import { jsonResult } from "openclaw/plugin-sdk";
|
||||||
|
import type { ToolContext } from "../types.js";
|
||||||
|
import { readProjects } from "../projects.js";
|
||||||
|
import { resolveRepoPath } from "../gitlab.js";
|
||||||
import { createProvider } from "../providers/index.js";
|
import { createProvider } from "../providers/index.js";
|
||||||
import { log as auditLog } from "../audit.js";
|
import { log as auditLog } from "../audit.js";
|
||||||
import type { StateLabel } from "../issue-provider.js";
|
import type { StateLabel } from "../issue-provider.js";
|
||||||
@@ -27,8 +30,9 @@ const STATE_LABELS: StateLabel[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function createTaskCreateTool(api: OpenClawPluginApi) {
|
export function createTaskCreateTool(api: OpenClawPluginApi) {
|
||||||
return (ctx: OpenClawPluginToolContext) => ({
|
return (ctx: ToolContext) => ({
|
||||||
name: "task_create",
|
name: "task_create",
|
||||||
|
label: "Task Create",
|
||||||
description: `Create a new task (issue) in the project's issue tracker. Use this to file bugs, features, or tasks from chat.
|
description: `Create a new task (issue) in the project's issue tracker. Use this to file bugs, features, or tasks from chat.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -132,12 +136,7 @@ The issue is created with a state label (defaults to "Planning"). Returns the cr
|
|||||||
: `📋 Created #${issue.iid}: "${title}" (${label}).${hasBody ? " With detailed description." : ""} Ready for pickup when needed.`,
|
: `📋 Created #${issue.iid}: "${title}" (${label}).${hasBody ? " With detailed description." : ""} Ready for pickup when needed.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return jsonResult(result);
|
||||||
content: [{
|
|
||||||
type: "text" as const,
|
|
||||||
text: JSON.stringify(result, null, 2),
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,35 +8,44 @@
|
|||||||
* Model selection is LLM-based: the orchestrator passes a `model` param.
|
* Model selection is LLM-based: the orchestrator passes a `model` param.
|
||||||
* A keyword heuristic is used as fallback if no model is specified.
|
* A keyword heuristic is used as fallback if no model is specified.
|
||||||
*/
|
*/
|
||||||
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
import { readProjects, getProject, getWorker } from "../projects.js";
|
import { jsonResult } from "openclaw/plugin-sdk";
|
||||||
|
import { dispatchTask } from "../dispatch.js";
|
||||||
import {
|
import {
|
||||||
getIssue,
|
|
||||||
getCurrentStateLabel,
|
getCurrentStateLabel,
|
||||||
transitionLabel,
|
getIssue,
|
||||||
resolveRepoPath,
|
resolveRepoPath,
|
||||||
|
transitionLabel,
|
||||||
type StateLabel,
|
type StateLabel,
|
||||||
} from "../gitlab.js";
|
} from "../gitlab.js";
|
||||||
import { selectModel } from "../model-selector.js";
|
import { selectModel } from "../model-selector.js";
|
||||||
import { dispatchTask } from "../dispatch.js";
|
import { getProject, getWorker, readProjects } from "../projects.js";
|
||||||
|
import type { ToolContext } from "../types.js";
|
||||||
|
|
||||||
export function createTaskPickupTool(api: OpenClawPluginApi) {
|
export function createTaskPickupTool(api: OpenClawPluginApi) {
|
||||||
return (ctx: OpenClawPluginToolContext) => ({
|
return (ctx: ToolContext) => ({
|
||||||
name: "task_pickup",
|
name: "task_pickup",
|
||||||
|
label: "Task Pickup",
|
||||||
description: `Pick up a task from the issue queue for a DEV or QA worker. Handles everything end-to-end: label transition, tier assignment, session creation/reuse, task dispatch, state update, and audit logging. The orchestrator should analyze the issue and pass the appropriate developer tier. Returns an announcement for the agent to post — no further session actions needed.`,
|
description: `Pick up a task from the issue queue for a DEV or QA worker. Handles everything end-to-end: label transition, tier assignment, session creation/reuse, task dispatch, state update, and audit logging. The orchestrator should analyze the issue and pass the appropriate developer tier. Returns an announcement for the agent to post — no further session actions needed.`,
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["issueId", "role", "projectGroupId"],
|
required: ["issueId", "role", "projectGroupId"],
|
||||||
properties: {
|
properties: {
|
||||||
issueId: { type: "number", description: "Issue ID to pick up" },
|
issueId: { type: "number", description: "Issue ID to pick up" },
|
||||||
role: { type: "string", enum: ["dev", "qa"], description: "Worker role: dev or qa" },
|
role: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["dev", "qa"],
|
||||||
|
description: "Worker role: dev or qa",
|
||||||
|
},
|
||||||
projectGroupId: {
|
projectGroupId: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "Telegram group ID (key in projects.json). Required — pass the group ID from the current conversation.",
|
description:
|
||||||
|
"Telegram group ID (key in projects.json). Required — pass the group ID from the current conversation.",
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "Developer tier (junior, medior, senior, qa). The orchestrator should evaluate the task complexity and choose the right tier. Falls back to keyword heuristic if omitted.",
|
description:
|
||||||
|
"Developer tier (junior, medior, senior, qa). The orchestrator should evaluate the task complexity and choose the right tier. Falls back to keyword heuristic if omitted.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -72,7 +81,9 @@ export function createTaskPickupTool(api: OpenClawPluginApi) {
|
|||||||
// 3. Fetch issue and verify state
|
// 3. Fetch issue and verify state
|
||||||
const repoPath = resolveRepoPath(project.repo);
|
const repoPath = resolveRepoPath(project.repo);
|
||||||
const glabOpts = {
|
const glabOpts = {
|
||||||
glabPath: (api.pluginConfig as Record<string, unknown>)?.glabPath as string | undefined,
|
glabPath: (api.pluginConfig as Record<string, unknown>)?.glabPath as
|
||||||
|
| string
|
||||||
|
| undefined,
|
||||||
repoPath,
|
repoPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,14 +111,20 @@ export function createTaskPickupTool(api: OpenClawPluginApi) {
|
|||||||
modelReason = "LLM-selected by orchestrator";
|
modelReason = "LLM-selected by orchestrator";
|
||||||
modelSource = "llm";
|
modelSource = "llm";
|
||||||
} else {
|
} else {
|
||||||
const selected = selectModel(issue.title, issue.description ?? "", role);
|
const selected = selectModel(
|
||||||
|
issue.title,
|
||||||
|
issue.description ?? "",
|
||||||
|
role,
|
||||||
|
);
|
||||||
modelAlias = selected.tier;
|
modelAlias = selected.tier;
|
||||||
modelReason = selected.reason;
|
modelReason = selected.reason;
|
||||||
modelSource = "heuristic";
|
modelSource = "heuristic";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Dispatch via shared logic
|
// 5. Dispatch via shared logic
|
||||||
const pluginConfig = api.pluginConfig as Record<string, unknown> | undefined;
|
const pluginConfig = api.pluginConfig as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined;
|
||||||
const dispatchResult = await dispatchTask({
|
const dispatchResult = await dispatchTask({
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
agentId: ctx.agentId,
|
agentId: ctx.agentId,
|
||||||
@@ -147,9 +164,7 @@ export function createTaskPickupTool(api: OpenClawPluginApi) {
|
|||||||
result.tokensSavedEstimate = "~50K (session reuse)";
|
result.tokensSavedEstimate = "~50K (session reuse)";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return jsonResult(result);
|
||||||
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
17
lib/types.ts
Normal file
17
lib/types.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Shared types for the DevClaw plugin.
|
||||||
|
*
|
||||||
|
* OpenClawPluginToolContext is declared in the plugin-sdk but not exported.
|
||||||
|
* We define a compatible local type for use in tool factory functions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ToolContext = {
|
||||||
|
config?: Record<string, unknown>;
|
||||||
|
workspaceDir?: string;
|
||||||
|
agentDir?: string;
|
||||||
|
agentId?: string;
|
||||||
|
sessionKey?: string;
|
||||||
|
messageChannel?: string;
|
||||||
|
agentAccountId?: string;
|
||||||
|
sandboxed?: boolean;
|
||||||
|
};
|
||||||
1577
package-lock.json
generated
1577
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,14 @@
|
|||||||
"./index.ts"
|
"./index.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"check": "tsc --noEmit",
|
||||||
|
"watch": "tsc --noEmit --watch"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"openclaw": ">=2026.0.0"
|
"openclaw": ">=2026.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["./**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user