feat: implement runCommand wrapper and refactor command executions across modules
This commit is contained in:
38
AGENTS.md
Normal file
38
AGENTS.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# DevClaw — Agent Instructions
|
||||||
|
|
||||||
|
DevClaw is an OpenClaw plugin that provides multi-project dev/qa pipeline orchestration with GitHub/GitLab integration, developer tiers, and audit logging.
|
||||||
|
|
||||||
|
## Testing Changes
|
||||||
|
|
||||||
|
To verify changes against a live gateway, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build && openclaw gateway restart
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait 3 seconds, then check the logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw logs
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for the plugin registration line and any errors:
|
||||||
|
```
|
||||||
|
[plugins] DevClaw plugin registered (11 tools, 1 CLI command group, 1 service)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- `index.ts` — Plugin entry point, registers all tools/CLI/services
|
||||||
|
- `lib/run-command.ts` — Wrapper around `api.runtime.system.runCommandWithTimeout` (initialized in `register()`)
|
||||||
|
- `lib/dispatch.ts` — Core task dispatch logic (session spawn/reuse, gateway RPC)
|
||||||
|
- `lib/providers/` — GitHub and GitLab issue providers (via `gh`/`glab` CLI)
|
||||||
|
- `lib/services/` — Heartbeat, tick (queue scan), pipeline (completion rules)
|
||||||
|
- `lib/setup/` — Agent creation, model fetching, LLM-powered model selection
|
||||||
|
- `lib/tools/` — All registered tools (work_start, work_finish, task_create, etc.)
|
||||||
|
|
||||||
|
## Key Conventions
|
||||||
|
|
||||||
|
- All external command execution uses `runCommand()` from `lib/run-command.ts` — never import `child_process` directly (the OpenClaw plugin security scanner flags it).
|
||||||
|
- The plugin SDK provides `api.runtime.system.runCommandWithTimeout` which is the sanctioned way to run external commands.
|
||||||
|
- Functions that call `runCommand()` must be async.
|
||||||
3
index.ts
3
index.ts
@@ -12,6 +12,7 @@ import { createOnboardTool } from "./lib/tools/onboard.js";
|
|||||||
import { createAutoConfigureModelsTool } from "./lib/tools/autoconfigure-models.js";
|
import { createAutoConfigureModelsTool } from "./lib/tools/autoconfigure-models.js";
|
||||||
import { registerCli } from "./lib/cli.js";
|
import { registerCli } from "./lib/cli.js";
|
||||||
import { registerHeartbeatService } from "./lib/services/heartbeat.js";
|
import { registerHeartbeatService } from "./lib/services/heartbeat.js";
|
||||||
|
import { initRunCommand } from "./lib/run-command.js";
|
||||||
|
|
||||||
const plugin = {
|
const plugin = {
|
||||||
id: "devclaw",
|
id: "devclaw",
|
||||||
@@ -86,6 +87,8 @@ const plugin = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
register(api: OpenClawPluginApi) {
|
register(api: OpenClawPluginApi) {
|
||||||
|
initRunCommand(api);
|
||||||
|
|
||||||
// Worker lifecycle
|
// Worker lifecycle
|
||||||
api.registerTool(createWorkStartTool(api), { names: ["work_start"] });
|
api.registerTool(createWorkStartTool(api), { names: ["work_start"] });
|
||||||
api.registerTool(createWorkFinishTool(api), { names: ["work_finish"] });
|
api.registerTool(createWorkFinishTool(api), { names: ["work_finish"] });
|
||||||
|
|||||||
@@ -4,11 +4,10 @@
|
|||||||
* Handles: session lookup, spawn/reuse via Gateway RPC, task dispatch via CLI,
|
* Handles: session lookup, spawn/reuse via Gateway RPC, task dispatch via CLI,
|
||||||
* state update (activateWorker), and audit logging.
|
* state update (activateWorker), and audit logging.
|
||||||
*/
|
*/
|
||||||
import { execFile, spawn } from "node:child_process";
|
|
||||||
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 { log as auditLog } from "./audit.js";
|
||||||
|
import { runCommand } from "./run-command.js";
|
||||||
import {
|
import {
|
||||||
type Project,
|
type Project,
|
||||||
activateWorker,
|
activateWorker,
|
||||||
@@ -17,8 +16,6 @@ import {
|
|||||||
} from "./projects.js";
|
} from "./projects.js";
|
||||||
import { resolveModel, levelEmoji } from "./tiers.js";
|
import { resolveModel, levelEmoji } from "./tiers.js";
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
|
||||||
|
|
||||||
export type DispatchOpts = {
|
export type DispatchOpts = {
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
@@ -142,30 +139,31 @@ export async function dispatchTask(
|
|||||||
|
|
||||||
await transitionLabel(issueId, fromLabel, toLabel);
|
await transitionLabel(issueId, fromLabel, toLabel);
|
||||||
|
|
||||||
let sessionKey = existingSessionKey;
|
|
||||||
let dispatched = false;
|
let dispatched = false;
|
||||||
|
let session: { key: string; action: "spawn" | "send" };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sessionKey = await ensureSession(sessionAction, sessionKey, {
|
session = await ensureSession(sessionAction, existingSessionKey, {
|
||||||
agentId, projectName: project.name, role, level, model,
|
agentId, projectName: project.name, role, level, model,
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendToAgent(sessionKey!, taskMessage, {
|
sendToAgent(session.key, taskMessage, {
|
||||||
agentId, projectName: project.name, issueId, role,
|
agentId, projectName: project.name, issueId, role,
|
||||||
orchestratorSessionKey: opts.sessionKey,
|
orchestratorSessionKey: opts.sessionKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatched = true;
|
dispatched = true;
|
||||||
|
|
||||||
|
// Always store session key — a "send" may have fallen back to "spawn"
|
||||||
await recordWorkerState(workspaceDir, groupId, role, {
|
await recordWorkerState(workspaceDir, groupId, role, {
|
||||||
issueId, level, sessionKey: sessionKey!, sessionAction,
|
issueId, level, sessionKey: session.key, sessionAction: session.action,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (dispatched) {
|
if (dispatched) {
|
||||||
await auditLog(workspaceDir, "work_start", {
|
await auditLog(workspaceDir, "work_start", {
|
||||||
project: project.name, groupId, issue: issueId, role,
|
project: project.name, groupId, issue: issueId, role,
|
||||||
warning: "State update failed after successful dispatch",
|
warning: "State update failed after successful dispatch",
|
||||||
error: (err as Error).message, sessionKey,
|
error: (err as Error).message, sessionKey: session!.key,
|
||||||
});
|
});
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`State update failed after successful session dispatch: ${(err as Error).message}. Session is running but projects.json was not updated.`,
|
`State update failed after successful session dispatch: ${(err as Error).message}. Session is running but projects.json was not updated.`,
|
||||||
@@ -179,13 +177,13 @@ export async function dispatchTask(
|
|||||||
|
|
||||||
await auditDispatch(workspaceDir, {
|
await auditDispatch(workspaceDir, {
|
||||||
project: project.name, groupId, issueId, issueTitle,
|
project: project.name, groupId, issueId, issueTitle,
|
||||||
role, level, model, sessionAction, sessionKey: sessionKey!,
|
role, level, model, sessionAction: session.action, sessionKey: session.key,
|
||||||
fromLabel, toLabel,
|
fromLabel, toLabel,
|
||||||
});
|
});
|
||||||
|
|
||||||
const announcement = buildAnnouncement(level, role, sessionAction, issueId, issueTitle, issueUrl);
|
const announcement = buildAnnouncement(level, role, session.action, issueId, issueTitle, issueUrl);
|
||||||
|
|
||||||
return { sessionAction, sessionKey: sessionKey!, level, model, announcement };
|
return { sessionAction: session.action, sessionKey: session.key, level, model, announcement };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -206,16 +204,28 @@ async function ensureSession(
|
|||||||
action: "spawn" | "send",
|
action: "spawn" | "send",
|
||||||
existingKey: string | null,
|
existingKey: string | null,
|
||||||
opts: { agentId?: string; projectName: string; role: string; level: string; model: string },
|
opts: { agentId?: string; projectName: string; role: string; level: string; model: string },
|
||||||
): Promise<string> {
|
): Promise<{ key: string; action: "spawn" | "send" }> {
|
||||||
if (action === "send") return existingKey!;
|
const expectedKey = `agent:${opts.agentId ?? "unknown"}:subagent:${opts.projectName}-${opts.role}-${opts.level}`;
|
||||||
|
|
||||||
const sessionKey = `agent:${opts.agentId ?? "unknown"}:subagent:${opts.projectName}-${opts.role}-${opts.level}`;
|
// Reuse: validate stored key matches expected format, then verify session exists
|
||||||
await execFileAsync(
|
if (action === "send" && existingKey === expectedKey) {
|
||||||
"openclaw",
|
try {
|
||||||
["gateway", "call", "sessions.patch", "--params", JSON.stringify({ key: sessionKey, model: opts.model })],
|
await runCommand(
|
||||||
{ timeout: 30_000 },
|
["openclaw", "gateway", "call", "sessions.patch", "--params", JSON.stringify({ key: existingKey, model: opts.model })],
|
||||||
|
{ timeoutMs: 30_000 },
|
||||||
|
);
|
||||||
|
return { key: existingKey, action: "send" };
|
||||||
|
} catch {
|
||||||
|
// Session gone (deleted, cleanup, etc.) — fall through to spawn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn: create fresh session (also handles stale/mismatched keys)
|
||||||
|
await runCommand(
|
||||||
|
["openclaw", "gateway", "call", "sessions.patch", "--params", JSON.stringify({ key: expectedKey, model: opts.model })],
|
||||||
|
{ timeoutMs: 30_000 },
|
||||||
);
|
);
|
||||||
return sessionKey;
|
return { key: expectedKey, action: "spawn" };
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendToAgent(
|
function sendToAgent(
|
||||||
@@ -231,27 +241,23 @@ function sendToAgent(
|
|||||||
lane: "subagent",
|
lane: "subagent",
|
||||||
...(opts.orchestratorSessionKey ? { spawnedBy: opts.orchestratorSessionKey } : {}),
|
...(opts.orchestratorSessionKey ? { spawnedBy: opts.orchestratorSessionKey } : {}),
|
||||||
});
|
});
|
||||||
const child = spawn(
|
// Fire-and-forget: long-running agent turn, don't await
|
||||||
"openclaw",
|
runCommand(
|
||||||
["gateway", "call", "agent", "--params", gatewayParams, "--expect-final", "--json"],
|
["openclaw", "gateway", "call", "agent", "--params", gatewayParams, "--expect-final", "--json"],
|
||||||
{ detached: true, stdio: "ignore" },
|
{ timeoutMs: 600_000 },
|
||||||
);
|
).catch(() => { /* fire-and-forget */ });
|
||||||
child.unref();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function recordWorkerState(
|
async function recordWorkerState(
|
||||||
workspaceDir: string, groupId: string, role: "dev" | "qa",
|
workspaceDir: string, groupId: string, role: "dev" | "qa",
|
||||||
opts: { issueId: number; level: string; sessionKey: string; sessionAction: "spawn" | "send" },
|
opts: { issueId: number; level: string; sessionKey: string; sessionAction: "spawn" | "send" },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const params: { issueId: string; level: string; sessionKey?: string; startTime: string } = {
|
await activateWorker(workspaceDir, groupId, role, {
|
||||||
issueId: String(opts.issueId),
|
issueId: String(opts.issueId),
|
||||||
level: opts.level,
|
level: opts.level,
|
||||||
|
sessionKey: opts.sessionKey,
|
||||||
startTime: new Date().toISOString(),
|
startTime: new Date().toISOString(),
|
||||||
};
|
});
|
||||||
if (opts.sessionAction === "spawn") {
|
|
||||||
params.sessionKey = opts.sessionKey;
|
|
||||||
}
|
|
||||||
await activateWorker(workspaceDir, groupId, role, params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function auditDispatch(
|
async function auditDispatch(
|
||||||
|
|||||||
@@ -7,12 +7,9 @@
|
|||||||
* - workerStart: Worker spawned/resumed for a task (→ project group)
|
* - workerStart: Worker spawned/resumed for a task (→ project group)
|
||||||
* - workerComplete: Worker completed task (→ project group)
|
* - workerComplete: Worker completed task (→ project group)
|
||||||
*/
|
*/
|
||||||
import { execFile } from "node:child_process";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
import { log as auditLog } from "./audit.js";
|
import { log as auditLog } from "./audit.js";
|
||||||
import type { TickAction } from "./services/tick.js";
|
import type { TickAction } from "./services/tick.js";
|
||||||
|
import { runCommand } from "./run-command.js";
|
||||||
const execFileAsync = promisify(execFile);
|
|
||||||
|
|
||||||
/** Per-event-type toggle. All default to true — set to false to suppress. */
|
/** Per-event-type toggle. All default to true — set to false to suppress. */
|
||||||
export type NotificationConfig = Partial<Record<NotifyEvent["type"], boolean>>;
|
export type NotificationConfig = Partial<Record<NotifyEvent["type"], boolean>>;
|
||||||
@@ -95,9 +92,9 @@ async function sendMessage(
|
|||||||
workspaceDir: string,
|
workspaceDir: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await execFileAsync(
|
await runCommand(
|
||||||
"openclaw",
|
|
||||||
[
|
[
|
||||||
|
"openclaw",
|
||||||
"message",
|
"message",
|
||||||
"send",
|
"send",
|
||||||
"--channel",
|
"--channel",
|
||||||
@@ -108,7 +105,7 @@ async function sendMessage(
|
|||||||
message,
|
message,
|
||||||
"--json",
|
"--json",
|
||||||
],
|
],
|
||||||
{ timeout: 30_000 },
|
{ timeoutMs: 30_000 },
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* GitHubProvider — IssueProvider implementation using gh CLI.
|
* GitHubProvider — IssueProvider implementation using gh CLI.
|
||||||
*/
|
*/
|
||||||
import { execFile } from "node:child_process";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
import { writeFile, unlink } from "node:fs/promises";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import {
|
import {
|
||||||
type IssueProvider,
|
type IssueProvider,
|
||||||
type Issue,
|
type Issue,
|
||||||
@@ -13,8 +8,7 @@ import {
|
|||||||
STATE_LABELS,
|
STATE_LABELS,
|
||||||
LABEL_COLORS,
|
LABEL_COLORS,
|
||||||
} from "./provider.js";
|
} from "./provider.js";
|
||||||
|
import { runCommand } from "../run-command.js";
|
||||||
const execFileAsync = promisify(execFile);
|
|
||||||
|
|
||||||
type GhIssue = {
|
type GhIssue = {
|
||||||
number: number;
|
number: number;
|
||||||
@@ -37,8 +31,8 @@ export class GitHubProvider implements IssueProvider {
|
|||||||
constructor(opts: { repoPath: string }) { this.repoPath = opts.repoPath; }
|
constructor(opts: { repoPath: string }) { this.repoPath = opts.repoPath; }
|
||||||
|
|
||||||
private async gh(args: string[]): Promise<string> {
|
private async gh(args: string[]): Promise<string> {
|
||||||
const { stdout } = await execFileAsync("gh", args, { cwd: this.repoPath, timeout: 30_000 });
|
const result = await runCommand(["gh", ...args], { timeoutMs: 30_000, cwd: this.repoPath });
|
||||||
return stdout.trim();
|
return result.stdout.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureLabel(name: string, color: string): Promise<void> {
|
async ensureLabel(name: string, color: string): Promise<void> {
|
||||||
@@ -51,16 +45,12 @@ export class GitHubProvider implements IssueProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createIssue(title: string, description: string, label: StateLabel, assignees?: string[]): Promise<Issue> {
|
async createIssue(title: string, description: string, label: StateLabel, assignees?: string[]): Promise<Issue> {
|
||||||
const tempFile = join(tmpdir(), `devclaw-issue-${Date.now()}.md`);
|
const args = ["issue", "create", "--title", title, "--body", description, "--label", label];
|
||||||
await writeFile(tempFile, description, "utf-8");
|
if (assignees?.length) args.push("--assignee", assignees.join(","));
|
||||||
try {
|
const url = await this.gh(args);
|
||||||
const args = ["issue", "create", "--title", title, "--body-file", tempFile, "--label", label];
|
const match = url.match(/\/issues\/(\d+)$/);
|
||||||
if (assignees?.length) args.push("--assignee", assignees.join(","));
|
if (!match) throw new Error(`Failed to parse issue URL: ${url}`);
|
||||||
const url = await this.gh(args);
|
return this.getIssue(parseInt(match[1], 10));
|
||||||
const match = url.match(/\/issues\/(\d+)$/);
|
|
||||||
if (!match) throw new Error(`Failed to parse issue URL: ${url}`);
|
|
||||||
return this.getIssue(parseInt(match[1], 10));
|
|
||||||
} finally { try { await unlink(tempFile); } catch { /* ignore */ } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async listIssuesByLabel(label: StateLabel): Promise<Issue[]> {
|
async listIssuesByLabel(label: StateLabel): Promise<Issue[]> {
|
||||||
@@ -111,10 +101,7 @@ export class GitHubProvider implements IssueProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addComment(issueId: number, body: string): Promise<void> {
|
async addComment(issueId: number, body: string): Promise<void> {
|
||||||
const tempFile = join(tmpdir(), `devclaw-comment-${Date.now()}.md`);
|
await this.gh(["issue", "comment", String(issueId), "--body", body]);
|
||||||
await writeFile(tempFile, body, "utf-8");
|
|
||||||
try { await this.gh(["issue", "comment", String(issueId), "--body-file", tempFile]); }
|
|
||||||
finally { try { await unlink(tempFile); } catch { /* ignore */ } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async healthCheck(): Promise<boolean> {
|
async healthCheck(): Promise<boolean> {
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* GitLabProvider — IssueProvider implementation using glab CLI.
|
* GitLabProvider — IssueProvider implementation using glab CLI.
|
||||||
*/
|
*/
|
||||||
import { execFile } from "node:child_process";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
import { writeFile, unlink } from "node:fs/promises";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import {
|
import {
|
||||||
type IssueProvider,
|
type IssueProvider,
|
||||||
type Issue,
|
type Issue,
|
||||||
@@ -13,16 +8,15 @@ import {
|
|||||||
STATE_LABELS,
|
STATE_LABELS,
|
||||||
LABEL_COLORS,
|
LABEL_COLORS,
|
||||||
} from "./provider.js";
|
} from "./provider.js";
|
||||||
|
import { runCommand } from "../run-command.js";
|
||||||
const execFileAsync = promisify(execFile);
|
|
||||||
|
|
||||||
export class GitLabProvider implements IssueProvider {
|
export class GitLabProvider implements IssueProvider {
|
||||||
private repoPath: string;
|
private repoPath: string;
|
||||||
constructor(opts: { repoPath: string }) { this.repoPath = opts.repoPath; }
|
constructor(opts: { repoPath: string }) { this.repoPath = opts.repoPath; }
|
||||||
|
|
||||||
private async glab(args: string[]): Promise<string> {
|
private async glab(args: string[]): Promise<string> {
|
||||||
const { stdout } = await execFileAsync("glab", args, { cwd: this.repoPath, timeout: 30_000 });
|
const result = await runCommand(["glab", ...args], { timeoutMs: 30_000, cwd: this.repoPath });
|
||||||
return stdout.trim();
|
return result.stdout.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureLabel(name: string, color: string): Promise<void> {
|
async ensureLabel(name: string, color: string): Promise<void> {
|
||||||
@@ -35,19 +29,15 @@ export class GitLabProvider implements IssueProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createIssue(title: string, description: string, label: StateLabel, assignees?: string[]): Promise<Issue> {
|
async createIssue(title: string, description: string, label: StateLabel, assignees?: string[]): Promise<Issue> {
|
||||||
const tempFile = join(tmpdir(), `devclaw-issue-${Date.now()}.md`);
|
// Pass description directly as argv — runCommand uses spawn (no shell),
|
||||||
await writeFile(tempFile, description, "utf-8");
|
// so no escaping issues with special characters.
|
||||||
try {
|
const args = ["issue", "create", "--title", title, "--description", description, "--label", label];
|
||||||
const { exec } = await import("node:child_process");
|
if (assignees?.length) args.push("--assignee", assignees.join(","));
|
||||||
const execAsync = promisify(exec);
|
const stdout = await this.glab(args);
|
||||||
let cmd = `glab issue create --title "${title.replace(/"/g, '\\"')}" --description "$(cat ${tempFile})" --label "${label}"`;
|
// glab issue create returns the issue URL
|
||||||
if (assignees?.length) cmd += ` --assignee "${assignees.join(",")}"`;
|
const match = stdout.match(/\/issues\/(\d+)/);
|
||||||
const { stdout } = await execAsync(cmd, { cwd: this.repoPath, timeout: 30_000 });
|
if (!match) throw new Error(`Failed to parse issue URL: ${stdout}`);
|
||||||
// glab issue create returns the issue URL
|
return this.getIssue(parseInt(match[1], 10));
|
||||||
const match = stdout.trim().match(/\/issues\/(\d+)/);
|
|
||||||
if (!match) throw new Error(`Failed to parse issue URL: ${stdout.trim()}`);
|
|
||||||
return this.getIssue(parseInt(match[1], 10));
|
|
||||||
} finally { try { await unlink(tempFile); } catch { /* ignore */ } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async listIssuesByLabel(label: StateLabel): Promise<Issue[]> {
|
async listIssuesByLabel(label: StateLabel): Promise<Issue[]> {
|
||||||
@@ -101,13 +91,8 @@ export class GitLabProvider implements IssueProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addComment(issueId: number, body: string): Promise<void> {
|
async addComment(issueId: number, body: string): Promise<void> {
|
||||||
const tempFile = join(tmpdir(), `devclaw-comment-${Date.now()}.md`);
|
// Pass message directly as argv — no shell escaping needed with spawn
|
||||||
await writeFile(tempFile, body, "utf-8");
|
await this.glab(["issue", "note", String(issueId), "--message", body]);
|
||||||
try {
|
|
||||||
const { exec } = await import("node:child_process");
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
await execAsync(`glab issue note ${issueId} --message "$(cat ${tempFile})"`, { cwd: this.repoPath, timeout: 30_000 });
|
|
||||||
} finally { try { await unlink(tempFile); } catch { /* ignore */ } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async healthCheck(): Promise<boolean> {
|
async healthCheck(): Promise<boolean> {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Provider factory — auto-detects GitHub vs GitLab from git remote.
|
* Provider factory — auto-detects GitHub vs GitLab from git remote.
|
||||||
*/
|
*/
|
||||||
import { execFileSync } from "node:child_process";
|
|
||||||
import type { IssueProvider } from "./provider.js";
|
import type { IssueProvider } from "./provider.js";
|
||||||
import { GitLabProvider } from "./gitlab.js";
|
import { GitLabProvider } from "./gitlab.js";
|
||||||
import { GitHubProvider } from "./github.js";
|
import { GitHubProvider } from "./github.js";
|
||||||
import { resolveRepoPath } from "../projects.js";
|
import { resolveRepoPath } from "../projects.js";
|
||||||
|
import { runCommand } from "../run-command.js";
|
||||||
|
|
||||||
export type ProviderOptions = {
|
export type ProviderOptions = {
|
||||||
provider?: "gitlab" | "github";
|
provider?: "gitlab" | "github";
|
||||||
@@ -18,19 +18,19 @@ export type ProviderWithType = {
|
|||||||
type: "github" | "gitlab";
|
type: "github" | "gitlab";
|
||||||
};
|
};
|
||||||
|
|
||||||
function detectProvider(repoPath: string): "gitlab" | "github" {
|
async function detectProvider(repoPath: string): Promise<"gitlab" | "github"> {
|
||||||
try {
|
try {
|
||||||
const url = execFileSync("git", ["remote", "get-url", "origin"], { cwd: repoPath, timeout: 5_000 }).toString().trim();
|
const result = await runCommand(["git", "remote", "get-url", "origin"], { timeoutMs: 5_000, cwd: repoPath });
|
||||||
return url.includes("github.com") ? "github" : "gitlab";
|
return result.stdout.trim().includes("github.com") ? "github" : "gitlab";
|
||||||
} catch {
|
} catch {
|
||||||
return "gitlab";
|
return "gitlab";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createProvider(opts: ProviderOptions): ProviderWithType {
|
export async function createProvider(opts: ProviderOptions): Promise<ProviderWithType> {
|
||||||
const repoPath = opts.repoPath ?? (opts.repo ? resolveRepoPath(opts.repo) : null);
|
const repoPath = opts.repoPath ?? (opts.repo ? resolveRepoPath(opts.repo) : null);
|
||||||
if (!repoPath) throw new Error("Either repoPath or repo must be provided");
|
if (!repoPath) throw new Error("Either repoPath or repo must be provided");
|
||||||
const type = opts.provider ?? detectProvider(repoPath);
|
const type = opts.provider ?? await detectProvider(repoPath);
|
||||||
const provider = type === "github" ? new GitHubProvider({ repoPath }) : new GitLabProvider({ repoPath });
|
const provider = type === "github" ? new GitHubProvider({ repoPath }) : new GitLabProvider({ repoPath });
|
||||||
return { provider, type };
|
return { provider, type };
|
||||||
}
|
}
|
||||||
|
|||||||
28
lib/run-command.ts
Normal file
28
lib/run-command.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* run-command.ts — Thin wrapper around the plugin SDK's runCommandWithTimeout.
|
||||||
|
*
|
||||||
|
* Initialised once during plugin registration, then available to all modules
|
||||||
|
* without threading the plugin API through every function signature.
|
||||||
|
*/
|
||||||
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
|
||||||
|
type RunCommand = OpenClawPluginApi["runtime"]["system"]["runCommandWithTimeout"];
|
||||||
|
|
||||||
|
let _runCommand: RunCommand | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the plugin SDK's runCommandWithTimeout. Call once in register().
|
||||||
|
*/
|
||||||
|
export function initRunCommand(api: OpenClawPluginApi): void {
|
||||||
|
_runCommand = api.runtime.system.runCommandWithTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run an external command via the plugin SDK.
|
||||||
|
*/
|
||||||
|
export const runCommand: RunCommand = (...args) => {
|
||||||
|
if (!_runCommand) {
|
||||||
|
throw new Error("runCommand not initialised — call initRunCommand(api) first");
|
||||||
|
}
|
||||||
|
return _runCommand(...args);
|
||||||
|
};
|
||||||
@@ -84,7 +84,7 @@ export function registerHeartbeatService(api: OpenClawPluginApi) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const agents = discoverAgents(ctx.config);
|
const agents = discoverAgents(api.config);
|
||||||
if (agents.length === 0) {
|
if (agents.length === 0) {
|
||||||
ctx.logger.warn("work_heartbeat service: no DevClaw agents registered");
|
ctx.logger.warn("work_heartbeat service: no DevClaw agents registered");
|
||||||
return;
|
return;
|
||||||
@@ -117,20 +117,39 @@ export function registerHeartbeatService(api: OpenClawPluginApi) {
|
|||||||
/**
|
/**
|
||||||
* Discover DevClaw agents by scanning which agent workspaces have projects.
|
* Discover DevClaw agents by scanning which agent workspaces have projects.
|
||||||
* Self-discovering: any agent whose workspace contains projects/projects.json is processed.
|
* Self-discovering: any agent whose workspace contains projects/projects.json is processed.
|
||||||
|
* Also checks the default workspace (agents.defaults.workspace) for projects.
|
||||||
*/
|
*/
|
||||||
function discoverAgents(config: ServiceContext["config"]): Agent[] {
|
function discoverAgents(config: {
|
||||||
const agentsList = config.agents?.list || [];
|
agents?: {
|
||||||
|
list?: Array<{ id: string; workspace?: string }>;
|
||||||
|
defaults?: { workspace?: string };
|
||||||
|
};
|
||||||
|
}): Agent[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const agents: Agent[] = [];
|
||||||
|
|
||||||
return agentsList
|
// Check explicit agent list
|
||||||
.filter((a): a is { id: string; workspace: string } => {
|
for (const a of config.agents?.list || []) {
|
||||||
if (!a.workspace) return false;
|
if (!a.workspace) continue;
|
||||||
try {
|
try {
|
||||||
return fs.existsSync(path.join(a.workspace, "projects", "projects.json"));
|
if (fs.existsSync(path.join(a.workspace, "projects", "projects.json"))) {
|
||||||
} catch {
|
agents.push({ agentId: a.id, workspace: a.workspace });
|
||||||
return false;
|
seen.add(a.workspace);
|
||||||
}
|
}
|
||||||
})
|
} catch { /* skip */ }
|
||||||
.map((a) => ({ agentId: a.id, workspace: a.workspace }));
|
}
|
||||||
|
|
||||||
|
// Check default workspace (used when no explicit agents are registered)
|
||||||
|
const defaultWorkspace = config.agents?.defaults?.workspace;
|
||||||
|
if (defaultWorkspace && !seen.has(defaultWorkspace)) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(path.join(defaultWorkspace, "projects", "projects.json"))) {
|
||||||
|
agents.push({ agentId: "main", workspace: defaultWorkspace });
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
return agents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -286,7 +305,7 @@ async function performHealthPass(
|
|||||||
groupId: string,
|
groupId: string,
|
||||||
project: any,
|
project: any,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const { provider } = createProvider({ repo: project.repo });
|
const { provider } = await createProvider({ repo: project.repo });
|
||||||
let fixedCount = 0;
|
let fixedCount = 0;
|
||||||
|
|
||||||
for (const role of ["dev", "qa"] as const) {
|
for (const role of ["dev", "qa"] as const) {
|
||||||
|
|||||||
@@ -3,12 +3,9 @@
|
|||||||
*
|
*
|
||||||
* Replaces 7 if-blocks with a data-driven lookup table.
|
* Replaces 7 if-blocks with a data-driven lookup table.
|
||||||
*/
|
*/
|
||||||
import { execFile } from "node:child_process";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
import type { StateLabel, IssueProvider } from "../providers/provider.js";
|
import type { StateLabel, IssueProvider } from "../providers/provider.js";
|
||||||
import { deactivateWorker } from "../projects.js";
|
import { deactivateWorker } from "../projects.js";
|
||||||
|
import { runCommand } from "../run-command.js";
|
||||||
const execFileAsync = promisify(execFile);
|
|
||||||
|
|
||||||
export type CompletionRule = {
|
export type CompletionRule = {
|
||||||
from: StateLabel;
|
from: StateLabel;
|
||||||
@@ -84,7 +81,7 @@ export async function executeCompletion(opts: {
|
|||||||
// Git pull (dev:done)
|
// Git pull (dev:done)
|
||||||
if (rule.gitPull) {
|
if (rule.gitPull) {
|
||||||
try {
|
try {
|
||||||
await execFileAsync("git", ["pull"], { cwd: repoPath, timeout: 30_000 });
|
await runCommand(["git", "pull"], { timeoutMs: 30_000, cwd: repoPath });
|
||||||
} catch { /* best-effort */ }
|
} catch { /* best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function getRoleForLabel(label: QueueLabel): "dev" | "qa" {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export async function fetchProjectQueues(project: Project): Promise<Record<QueueLabel, Issue[]>> {
|
export async function fetchProjectQueues(project: Project): Promise<Record<QueueLabel, Issue[]>> {
|
||||||
const { provider } = createProvider({ repo: project.repo });
|
const { provider } = await createProvider({ repo: project.repo });
|
||||||
const labels: QueueLabel[] = ["To Improve", "To Test", "To Do"];
|
const labels: QueueLabel[] = ["To Improve", "To Test", "To Do"];
|
||||||
const queues: Record<QueueLabel, Issue[]> = { "To Improve": [], "To Test": [], "To Do": [] };
|
const queues: Record<QueueLabel, Issue[]> = { "To Improve": [], "To Test": [], "To Do": [] };
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export async function projectTick(opts: {
|
|||||||
const project = (await readProjects(workspaceDir)).projects[groupId];
|
const project = (await readProjects(workspaceDir)).projects[groupId];
|
||||||
if (!project) return { pickups: [], skipped: [{ reason: `Project not found: ${groupId}` }] };
|
if (!project) return { pickups: [], skipped: [{ reason: `Project not found: ${groupId}` }] };
|
||||||
|
|
||||||
const provider = opts.provider ?? createProvider({ repo: project.repo }).provider;
|
const provider = opts.provider ?? (await createProvider({ repo: project.repo })).provider;
|
||||||
const roleExecution = project.roleExecution ?? "parallel";
|
const roleExecution = project.roleExecution ?? "parallel";
|
||||||
const roles: Array<"dev" | "qa"> = targetRole ? [targetRole] : ["dev", "qa"];
|
const roles: Array<"dev" | "qa"> = targetRole ? [targetRole] : ["dev", "qa"];
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* setup/agent.ts — Agent creation and workspace resolution.
|
* setup/agent.ts — Agent creation and workspace resolution.
|
||||||
*/
|
*/
|
||||||
import { execFile } from "node:child_process";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||||
|
import { runCommand } from "../run-command.js";
|
||||||
const execFileAsync = promisify(execFile);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new agent via `openclaw agents add`.
|
* Create a new agent via `openclaw agents add`.
|
||||||
@@ -27,7 +24,7 @@ export async function createAgent(
|
|||||||
if (channelBinding) args.push("--bind", channelBinding);
|
if (channelBinding) args.push("--bind", channelBinding);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await execFileAsync("openclaw", args, { timeout: 30_000 });
|
await runCommand(["openclaw", ...args], { timeoutMs: 30_000 });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`Failed to create agent "${name}": ${(err as Error).message}`);
|
throw new Error(`Failed to create agent "${name}": ${(err as Error).message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,7 @@
|
|||||||
*
|
*
|
||||||
* Uses an LLM to understand model capabilities and assign optimal models to DevClaw roles.
|
* Uses an LLM to understand model capabilities and assign optimal models to DevClaw roles.
|
||||||
*/
|
*/
|
||||||
import { execSync } from "node:child_process";
|
import { runCommand } from "../run-command.js";
|
||||||
import { writeFileSync, unlinkSync } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
|
|
||||||
export type ModelAssignment = {
|
export type ModelAssignment = {
|
||||||
dev: {
|
dev: {
|
||||||
@@ -76,27 +73,18 @@ Return ONLY a JSON object in this exact format (no markdown, no explanation):
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
// Write prompt to temp file for safe passing to shell
|
|
||||||
const tmpFile = join(tmpdir(), `devclaw-model-select-${Date.now()}.txt`);
|
|
||||||
writeFileSync(tmpFile, prompt, "utf-8");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call openclaw agent using current session context if available
|
const sessionId = sessionKey ?? "devclaw-model-selection";
|
||||||
const sessionFlag = sessionKey
|
|
||||||
? `--session-id "${sessionKey}"`
|
|
||||||
: `--session-id devclaw-model-selection`;
|
|
||||||
|
|
||||||
const result = execSync(
|
const result = await runCommand(
|
||||||
`openclaw agent --local ${sessionFlag} --message "$(cat ${tmpFile})" --json`,
|
["openclaw", "agent", "--local", "--session-id", sessionId, "--message", prompt, "--json"],
|
||||||
{
|
{ timeoutMs: 30_000 },
|
||||||
encoding: "utf-8",
|
);
|
||||||
timeout: 30000,
|
|
||||||
stdio: ["pipe", "pipe", "ignore"],
|
const output = result.stdout.trim();
|
||||||
},
|
|
||||||
).trim();
|
|
||||||
|
|
||||||
// Parse the response from openclaw agent --json
|
// Parse the response from openclaw agent --json
|
||||||
const lines = result.split("\n");
|
const lines = output.split("\n");
|
||||||
const jsonStartIndex = lines.findIndex((line) => line.trim().startsWith("{"));
|
const jsonStartIndex = lines.findIndex((line) => line.trim().startsWith("{"));
|
||||||
|
|
||||||
if (jsonStartIndex === -1) {
|
if (jsonStartIndex === -1) {
|
||||||
@@ -146,12 +134,5 @@ Return ONLY a JSON object in this exact format (no markdown, no explanation):
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("LLM model selection failed:", (err as Error).message);
|
console.error("LLM model selection failed:", (err as Error).message);
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
|
||||||
// Clean up temp file
|
|
||||||
try {
|
|
||||||
unlinkSync(tmpFile);
|
|
||||||
} catch {
|
|
||||||
// Ignore cleanup errors
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* model-fetcher.ts — Shared helper for fetching OpenClaw models without logging.
|
* model-fetcher.ts — Shared helper for fetching OpenClaw models.
|
||||||
*
|
*
|
||||||
* Uses execSync to bypass OpenClaw's command logging infrastructure.
|
* Uses the plugin SDK's runCommand to run openclaw CLI commands.
|
||||||
*/
|
*/
|
||||||
import { execSync } from "node:child_process";
|
import { runCommand } from "../run-command.js";
|
||||||
|
|
||||||
export type OpenClawModelRow = {
|
export type OpenClawModelRow = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -17,25 +17,19 @@ export type OpenClawModelRow = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all models from OpenClaw without logging.
|
* Fetch all models from OpenClaw.
|
||||||
*
|
*
|
||||||
* @param allModels - If true, fetches all models (--all flag). If false, only authenticated models.
|
* @param allModels - If true, fetches all models (--all flag). If false, only authenticated models.
|
||||||
* @returns Array of model objects from OpenClaw's model registry
|
* @returns Array of model objects from OpenClaw's model registry
|
||||||
*/
|
*/
|
||||||
export function fetchModels(allModels = true): OpenClawModelRow[] {
|
export async function fetchModels(allModels = true): Promise<OpenClawModelRow[]> {
|
||||||
try {
|
try {
|
||||||
const command = allModels
|
const args = allModels
|
||||||
? "openclaw models list --all --json"
|
? ["openclaw", "models", "list", "--all", "--json"]
|
||||||
: "openclaw models list --json";
|
: ["openclaw", "models", "list", "--json"];
|
||||||
|
|
||||||
// Use execSync directly to bypass OpenClaw's command logging
|
const result = await runCommand(args, { timeoutMs: 10_000 });
|
||||||
const output = execSync(command, {
|
const output = result.stdout.trim();
|
||||||
encoding: "utf-8",
|
|
||||||
timeout: 10000,
|
|
||||||
cwd: process.cwd(),
|
|
||||||
// Suppress stderr to avoid any error messages
|
|
||||||
stdio: ["pipe", "pipe", "ignore"],
|
|
||||||
}).trim();
|
|
||||||
|
|
||||||
if (!output) {
|
if (!output) {
|
||||||
throw new Error("Empty output from openclaw models list");
|
throw new Error("Empty output from openclaw models list");
|
||||||
@@ -75,7 +69,6 @@ export function fetchModels(allModels = true): OpenClawModelRow[] {
|
|||||||
/**
|
/**
|
||||||
* Fetch only authenticated models (available: true).
|
* Fetch only authenticated models (available: true).
|
||||||
*/
|
*/
|
||||||
export function fetchAuthenticatedModels(): OpenClawModelRow[] {
|
export async function fetchAuthenticatedModels(): Promise<OpenClawModelRow[]> {
|
||||||
// Use --all flag but suppress logging via stdio in fetchModels()
|
return (await fetchModels(true)).filter((m) => m.available === true);
|
||||||
return fetchModels(true).filter((m) => m.available === true);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export async function resolveProject(
|
|||||||
/**
|
/**
|
||||||
* Create an issue provider for a project.
|
* Create an issue provider for a project.
|
||||||
*/
|
*/
|
||||||
export function resolveProvider(project: Project): ProviderWithType {
|
export async function resolveProvider(project: Project): Promise<ProviderWithType> {
|
||||||
return createProvider({ repo: project.repo });
|
return createProvider({ repo: project.repo });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ async function getAuthenticatedModels(
|
|||||||
api: OpenClawPluginApi,
|
api: OpenClawPluginApi,
|
||||||
): Promise<Array<{ model: string; provider: string; authenticated: boolean }>> {
|
): Promise<Array<{ model: string; provider: string; authenticated: boolean }>> {
|
||||||
try {
|
try {
|
||||||
const models = fetchAuthenticatedModels();
|
const models = await fetchAuthenticatedModels();
|
||||||
|
|
||||||
// Map to the format expected by assignModels()
|
// Map to the format expected by assignModels()
|
||||||
return models.map((m) => {
|
return models.map((m) => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function createHealthTool() {
|
|||||||
for (const pid of projectIds) {
|
for (const pid of projectIds) {
|
||||||
const project = getProject(data, pid);
|
const project = getProject(data, pid);
|
||||||
if (!project) continue;
|
if (!project) continue;
|
||||||
const { provider } = resolveProvider(project);
|
const { provider } = await resolveProvider(project);
|
||||||
|
|
||||||
for (const role of ["dev", "qa"] as const) {
|
for (const role of ["dev", "qa"] as const) {
|
||||||
const fixes = await checkWorkerHealth({
|
const fixes = await checkWorkerHealth({
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export function createProjectRegisterTool() {
|
|||||||
const repoPath = resolveRepoPath(repo);
|
const repoPath = resolveRepoPath(repo);
|
||||||
|
|
||||||
// 3. Create provider and verify it works
|
// 3. Create provider and verify it works
|
||||||
const { provider, type: providerType } = createProvider({ repo });
|
const { provider, type: providerType } = await createProvider({ repo });
|
||||||
|
|
||||||
const healthy = await provider.healthCheck();
|
const healthy = await provider.healthCheck();
|
||||||
if (!healthy) {
|
if (!healthy) {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ Examples:
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { project } = await resolveProject(workspaceDir, groupId);
|
const { project } = await resolveProject(workspaceDir, groupId);
|
||||||
const { provider, type: providerType } = resolveProvider(project);
|
const { provider, type: providerType } = await resolveProvider(project);
|
||||||
|
|
||||||
const issue = await provider.getIssue(issueId);
|
const issue = await provider.getIssue(issueId);
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ Examples:
|
|||||||
const workspaceDir = requireWorkspaceDir(ctx);
|
const workspaceDir = requireWorkspaceDir(ctx);
|
||||||
|
|
||||||
const { project } = await resolveProject(workspaceDir, groupId);
|
const { project } = await resolveProject(workspaceDir, groupId);
|
||||||
const { provider, type: providerType } = resolveProvider(project);
|
const { provider, type: providerType } = await resolveProvider(project);
|
||||||
|
|
||||||
const issue = await provider.createIssue(title, description, label, assignees);
|
const issue = await provider.createIssue(title, description, label, assignees);
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ Examples:
|
|||||||
const workspaceDir = requireWorkspaceDir(ctx);
|
const workspaceDir = requireWorkspaceDir(ctx);
|
||||||
|
|
||||||
const { project } = await resolveProject(workspaceDir, groupId);
|
const { project } = await resolveProject(workspaceDir, groupId);
|
||||||
const { provider, type: providerType } = resolveProvider(project);
|
const { provider, type: providerType } = await resolveProvider(project);
|
||||||
|
|
||||||
const issue = await provider.getIssue(issueId);
|
const issue = await provider.getIssue(issueId);
|
||||||
const currentState = provider.getCurrentStateLabel(issue);
|
const currentState = provider.getCurrentStateLabel(issue);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function createWorkFinishTool(api: OpenClawPluginApi) {
|
|||||||
const issueId = worker.issueId ? Number(worker.issueId.split(",")[0]) : null;
|
const issueId = worker.issueId ? Number(worker.issueId.split(",")[0]) : null;
|
||||||
if (!issueId) throw new Error(`No issueId for active ${role.toUpperCase()} on ${project.name}`);
|
if (!issueId) throw new Error(`No issueId for active ${role.toUpperCase()} on ${project.name}`);
|
||||||
|
|
||||||
const { provider } = resolveProvider(project);
|
const { provider } = await resolveProvider(project);
|
||||||
const repoPath = resolveRepoPath(project.repo);
|
const repoPath = resolveRepoPath(project.repo);
|
||||||
const issue = await provider.getIssue(issueId);
|
const issue = await provider.getIssue(issueId);
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function createWorkStartTool(api: OpenClawPluginApi) {
|
|||||||
|
|
||||||
if (!groupId) throw new Error("projectGroupId is required");
|
if (!groupId) throw new Error("projectGroupId is required");
|
||||||
const { project } = await resolveProject(workspaceDir, groupId);
|
const { project } = await resolveProject(workspaceDir, groupId);
|
||||||
const { provider } = resolveProvider(project);
|
const { provider } = await resolveProvider(project);
|
||||||
|
|
||||||
// Find issue
|
// Find issue
|
||||||
let issue: { iid: number; title: string; description: string; labels: string[]; web_url: string; state: string };
|
let issue: { iid: number; title: string; description: string; labels: string[]; web_url: string; state: string };
|
||||||
|
|||||||
Reference in New Issue
Block a user