diff --git a/app/api/tasks/route.ts b/app/api/tasks/route.ts index e3cedf2..3a5772c 100644 --- a/app/api/tasks/route.ts +++ b/app/api/tasks/route.ts @@ -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 }); diff --git a/lib/agents.ts b/lib/agents.ts index be7880c..3d01ead 100644 --- a/lib/agents.ts +++ b/lib/agents.ts @@ -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() { diff --git a/lib/dispatch.ts b/lib/dispatch.ts index 0f73434..8f0d270 100644 --- a/lib/dispatch.ts +++ b/lib/dispatch.ts @@ -450,7 +450,7 @@ async function dispatchOpenClawTask(taskId: number): Promise { 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);