[taskboard] auto-dispatch telegram swarm tasks

This commit is contained in:
2026-03-07 15:05:05 -08:00
parent 53259f6b37
commit 07c42e92a9
3 changed files with 79 additions and 10 deletions

View File

@@ -3,6 +3,11 @@ import { NextResponse } from "next/server";
import { findAgentByAssignmentKey } from "@/lib/agents";
import { createTask, listTasks, validateTaskPayload } from "@/lib/tasks";
function extractTagValue(tags: string[], prefix: string) {
const match = tags.find((tag) => tag.startsWith(prefix));
return match ? match.slice(prefix.length) : null;
}
export async function GET() {
return NextResponse.json(await listTasks());
}
@@ -15,28 +20,44 @@ export async function POST(request: Request) {
}
const assignee = typeof payload.assignee === "string" ? payload.assignee : "";
const tags = Array.isArray(payload.tags) ? payload.tags.filter((tag) => typeof tag === "string") : [];
const assigneeAgent = assignee ? await findAgentByAssignmentKey(assignee) : null;
const derivedRepoSlug = typeof payload.repo_slug === "string" ? payload.repo_slug : extractTagValue(tags, "repo:");
const derivedPreferredAgent =
typeof payload.preferred_agent === "string" ? payload.preferred_agent : extractTagValue(tags, "agent:");
const requestedFamily = payload.family === null ? null : (payload.family as never);
const requestedDispatchMethod = payload.dispatch_method as never;
const wantsOpenClawSwarm =
requestedFamily === "openclaw" ||
requestedDispatchMethod === "openclaw-swarm" ||
tags.includes("swarm") ||
Boolean(derivedRepoSlug) ||
Boolean(derivedPreferredAgent && ["codex", "opencode", "gemini"].includes(derivedPreferredAgent));
const task = await createTask({
title: String(payload.title),
description: typeof payload.description === "string" ? payload.description : "",
assignee,
family: (payload.family as never) || assigneeAgent?.family || null,
target_host: typeof payload.target_host === "string" ? payload.target_host : assigneeAgent?.host || "",
family: requestedFamily || assigneeAgent?.family || (wantsOpenClawSwarm ? "openclaw" : null),
target_host:
typeof payload.target_host === "string"
? payload.target_host
: assigneeAgent?.host || (wantsOpenClawSwarm ? "ubuntu" : ""),
target_channel:
typeof payload.target_channel === "string"
? payload.target_channel
: assigneeAgent?.channels[0]?.value || "",
: assigneeAgent?.channels[0]?.value || (wantsOpenClawSwarm ? "OpenClaw swarm registry" : ""),
dispatch_method:
(payload.dispatch_method as never) || assigneeAgent?.defaultDispatchMethod || "manual",
requestedDispatchMethod || assigneeAgent?.defaultDispatchMethod || (wantsOpenClawSwarm ? "openclaw-swarm" : "manual"),
template_key: typeof payload.template_key === "string" ? payload.template_key : null,
repo_slug: typeof payload.repo_slug === "string" ? payload.repo_slug : null,
repo_slug: derivedRepoSlug,
base_branch: typeof payload.base_branch === "string" ? payload.base_branch : null,
preferred_agent: typeof payload.preferred_agent === "string" ? payload.preferred_agent : null,
preferred_agent: derivedPreferredAgent,
reasoning_effort: typeof payload.reasoning_effort === "string" ? payload.reasoning_effort : null,
model_hint: typeof payload.model_hint === "string" ? payload.model_hint : null,
priority: payload.priority as never,
status: (payload.status as never) || "Backlog",
tags: Array.isArray(payload.tags) ? payload.tags.filter((tag) => typeof tag === "string") : [],
tags,
});
return NextResponse.json(task, { status: 201 });

View File

@@ -205,8 +205,7 @@ function deriveStatus(activeTaskCount: number, heartbeatAt: string | null): Agen
async function buildOpenClawAgents() {
const config = readOpenClawConfig();
const agents = config.agents?.list || [];
return Promise.all(
const concreteAgents = await Promise.all(
agents.map(async (agentConfig) => {
const agentRoot = path.join(OPENCLAW_AGENTS_DIR, agentConfig.id);
const workspace = readWorkspaceAgent(agentRoot, agentConfig.identity?.name || agentConfig.id);
@@ -248,6 +247,55 @@ async function buildOpenClawAgents() {
} satisfies FleetAgent;
}),
);
const swarmAliases = ["openclaw", "openclaw-swarm", "codex", "opencode", "gemini"];
const swarmTaskBuckets = await fetchTaskBuckets(swarmAliases);
const swarmEventSummary = await fetchAgentEventSummary(swarmAliases);
const runtimeWorkspace = path.join(OPENCLAW_RUNTIME_ROOT, "workspace");
const heartbeatAt = fs.existsSync(runtimeWorkspace)
? fs.statSync(runtimeWorkspace).mtime.toISOString()
: swarmEventSummary.lastEvent?.created_at || null;
const swarmAgent = {
slug: "openclaw-swarm",
assignmentKey: "openclaw",
aliases: swarmAliases,
family: "openclaw" as const,
name: "OpenClaw Swarm",
host: "ubuntu",
role: "Swarm execution queue and runner aliases for ubuntu-local OpenClaw work.",
runtimePath: OPENCLAW_RUNTIME_ROOT,
configPath: OPENCLAW_CONFIG_PATH,
defaultDispatchMethod: "openclaw-swarm" as const,
model: null,
emoji: "O",
channels: [
{ label: "Family", value: "OpenClaw swarm queue" },
{ label: "Queue", value: "OpenClaw swarm registry" },
],
tools: ["git worktree", "tmux", "taskboard callbacks", "swarm registry"],
capabilities: [
"Queue and launch swarm tasks backed by git worktrees.",
"Map runner aliases like codex, opencode, and gemini into the shared swarm executor.",
"Report acknowledgement, completion, and failure back to the taskboard.",
],
files: [],
status: deriveStatus(swarmTaskBuckets.activeTasks.length, heartbeatAt),
workload: swarmTaskBuckets.activeTasks.length,
activeTasks: swarmTaskBuckets.activeTasks,
completedTasks: swarmTaskBuckets.completedTasks,
currentTask: swarmTaskBuckets.activeTasks[0]?.title || null,
heartbeatAt,
heartbeatAgeMinutes: deriveHeartbeatAgeMinutes(heartbeatAt),
lastEvent: swarmEventSummary.lastEvent,
failureStreak: swarmEventSummary.failureStreak,
notes: [
"Synthetic fleet agent representing the OpenClaw swarm dispatcher.",
"Covers runner aliases used by Telegram and task templates.",
],
} satisfies FleetAgent;
return [...concreteAgents, swarmAgent];
}
async function buildZeroClawAgents() {

View File

@@ -450,7 +450,7 @@ async function dispatchOpenClawTask(taskId: number): Promise<DispatchResult> {
await execFileAsync("git", ["config", "--global", "--add", "safe.directory", repoPath]);
const agentName = task.preferred_agent || "codex";
const agentName = task.preferred_agent || task.assignee || "codex";
const taskKey = `taskboard-${task.id}`;
const repoName = path.basename(repoPath);
const worktree = path.join(SWARM_WORKTREES_DIR, repoName, taskKey);