feat(swarm): implement Swarm View remake with Operations, Archetypes, and Templates

This commit includes the new SwarmWorkspace with its 3 sub-tabs, the LeftPanel mission picker, and the comprehensive Operations Command Dashboard featuring the live interactive DAG telemetry and task assignment prep flow.
This commit is contained in:
zenchantlive 2026-02-20 22:19:38 -08:00
parent 409a7e7256
commit dfaf523029
74 changed files with 11066 additions and 2046 deletions

View file

@ -0,0 +1,19 @@
import { NextResponse } from 'next/server';
import { listAgents } from '../../../../lib/agent-registry';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot');
if (!projectRoot) {
return NextResponse.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
}
const result = await listAgents({}, { projectRoot });
if (!result.ok) {
return NextResponse.json({ ok: false, error: result.error?.message }, { status: 500 });
}
return NextResponse.json({ ok: true, data: result.data });
}

View file

@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from 'next/server';
import { readInteractionsViaBd } from '../../../../../lib/read-interactions';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
): Promise<NextResponse> {
const { id } = await params;
const projectRoot = request.nextUrl.searchParams.get('projectRoot');
if (!projectRoot) {
return NextResponse.json(
{ ok: false, error: { message: 'projectRoot is required' } },
{ status: 400 }
);
}
try {
const comments = await readInteractionsViaBd(projectRoot, id);
return NextResponse.json({ ok: true, comments });
} catch (error) {
console.error('[API] Failed to fetch comments:', error);
return NextResponse.json(
{ ok: false, error: { message: 'Failed to fetch comments' } },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
import { runBdCommand } from '../../../../../lib/bridge';
export const dynamic = 'force-dynamic';
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot');
if (!projectRoot) {
return NextResponse.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
}
try {
const result = await runBdCommand({
projectRoot,
args: ['swarm', 'status', id, '--json'],
});
if (!result.success) {
return NextResponse.json({ ok: false, error: result.error }, { status: 500 });
}
const topology = JSON.parse(result.stdout);
return NextResponse.json({ ok: true, data: topology });
} catch (e) {
console.error(`Failed to fetch topology for ${id}:`, e);
return NextResponse.json({ ok: false, error: 'Internal server error' }, { status: 500 });
}
}

View file

@ -0,0 +1,40 @@
import { NextResponse } from 'next/server';
import { joinSwarm, leaveSwarm } from '../../../../lib/swarm-molecules';
export async function POST(request: Request) {
try {
const body = await request.json();
const { projectRoot, agentId, missionId, action } = body;
if (!projectRoot || !agentId || !missionId || !action) {
return NextResponse.json(
{ ok: false, error: 'Missing required fields' },
{ status: 400 }
);
}
let result;
if (action === 'join') {
result = await joinSwarm({ agent: agentId, epicId: missionId }, { projectRoot });
} else if (action === 'leave') {
result = await leaveSwarm({ agent: agentId, projectRoot });
} else {
return NextResponse.json({ ok: false, error: 'Invalid action' }, { status: 400 });
}
if (!result.ok) {
return NextResponse.json(
{ ok: false, error: result.error?.message },
{ status: 500 }
);
}
return NextResponse.json({ ok: true, data: result.data });
} catch (e) {
return NextResponse.json(
{ ok: false, error: e instanceof Error ? e.message : 'Unknown error' },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,46 @@
import { NextResponse } from 'next/server';
import { runBdCommand } from '../../../../lib/bridge';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot');
const id = searchParams.get('id');
if (!projectRoot || !id) {
return NextResponse.json({ ok: false, error: 'projectRoot and id are required' }, { status: 400 });
}
// 1. Get the mission/epic bead itself
const headResult = await runBdCommand({
projectRoot,
args: ['show', id, '--json'],
});
// 2. Get children
const childrenResult = await runBdCommand({
projectRoot,
args: ['list', '--parent', id, '--json'],
});
if (!headResult.success) {
return NextResponse.json({ ok: false, error: 'Failed to fetch mission head' }, { status: 500 });
}
try {
const head = JSON.parse(headResult.stdout);
let children = [];
if (childrenResult.success && childrenResult.stdout.trim()) {
children = JSON.parse(childrenResult.stdout);
}
const headObj = Array.isArray(head) ? head[0] : head;
// Transform for graph view (if needed, or just return raw issues and let UI handle it)
// The WorkflowGraph component expects BeadIssue[]
const nodes = [headObj, ...children];
return NextResponse.json({ ok: true, data: { nodes } });
} catch (e) {
return NextResponse.json({ ok: false, error: 'Failed to parse graph data' }, { status: 500 });
}
}

View file

@ -0,0 +1,88 @@
import { NextResponse } from 'next/server';
import { runBdCommand } from '../../../../lib/bridge';
import { listAgents, type AgentRecord } from '../../../../lib/agent-registry';
export const dynamic = 'force-dynamic';
interface SwarmTopology {
completed: { id: string; title: string }[];
active: { id: string; title: string; assignee?: string }[];
ready: { id: string; title: string }[];
blocked: { id: string; title: string; blocked_by: string[] }[];
}
interface Mission {
id: string;
title: string;
description?: string;
status: 'planning' | 'active' | 'blocked' | 'completed';
stats: {
total: number;
done: number;
blocked: number;
active: number;
};
topology?: SwarmTopology;
agents: AgentRecord[];
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot');
if (!projectRoot) {
return NextResponse.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
}
// 1. Fetch Swarms (Molecules)
const swarmResult = await runBdCommand({
projectRoot,
args: ['swarm', 'list', '--json'],
});
if (!swarmResult.success) {
console.warn('Swarm list failed, returning empty:', swarmResult.error);
return NextResponse.json({ ok: true, data: { missions: [] } });
}
// 2. Fetch All Agents
const agentResult = await listAgents({}, { projectRoot });
const allAgents = agentResult.ok ? agentResult.data! : [];
try {
const rawData = JSON.parse(swarmResult.stdout);
const rawSwarms = rawData.swarms || [];
// 3. Transform & Merge
const missions: Mission[] = rawSwarms
.filter((s: any) => !s.title.startsWith('Agent:'))
.map((s: any) => {
const assignedAgents = allAgents.filter(a => a.swarm_id === s.epic_id || a.swarm_id === s.id);
// Map status
let status: Mission['status'] = 'planning';
if (s.status === 'closed') status = 'completed';
else if (assignedAgents.length > 0) status = 'active';
else status = 'planning';
return {
id: s.id,
title: s.title,
description: s.description || s.epic_description || '',
status,
stats: {
total: s.total_issues || 0,
done: s.completed_issues || 0,
active: s.active_issues || 0,
blocked: 0
},
agents: assignedAgents
};
});
return NextResponse.json({ ok: true, data: { missions } });
} catch (e) {
console.error('Mission list parsing error:', e);
return NextResponse.json({ ok: false, error: 'Failed to parse mission data' }, { status: 500 });
}
}

View file

@ -0,0 +1,70 @@
import { NextResponse } from 'next/server';
import { runBdCommand } from '../../../../lib/bridge';
interface CloseSwarmPayload {
projectRoot: string;
swarmId: string;
reason?: string;
}
export async function POST(request: Request): Promise<Response> {
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'Invalid JSON body' } },
{ status: 400 },
);
}
if (!body || typeof body !== 'object') {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'Payload must be a JSON object' } },
{ status: 400 },
);
}
const data = body as Record<string, unknown>;
if (typeof data.projectRoot !== 'string' || !data.projectRoot.trim()) {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'projectRoot is required' } },
{ status: 400 },
);
}
if (typeof data.swarmId !== 'string' || !data.swarmId.trim()) {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'swarmId is required' } },
{ status: 400 },
);
}
const payload: CloseSwarmPayload = {
projectRoot: data.projectRoot.trim(),
swarmId: data.swarmId.trim(),
reason: typeof data.reason === 'string' ? data.reason.trim() : undefined,
};
const args = ['close', payload.swarmId, '--json'];
if (payload.reason) {
args.push('--reason', payload.reason);
}
const result = await runBdCommand({
projectRoot: payload.projectRoot,
args,
});
if (!result.success) {
return NextResponse.json(
{ ok: false, error: { classification: result.classification ?? 'unknown', message: result.error ?? result.stderr } },
{ status: 400 },
);
}
return NextResponse.json({ ok: true, swarmId: payload.swarmId });
}

View file

@ -0,0 +1,78 @@
import { NextResponse } from 'next/server';
import { runBdCommand } from '../../../../lib/bridge';
interface CreateSwarmPayload {
projectRoot: string;
epicId: string;
coordinator?: string;
}
export async function POST(request: Request): Promise<Response> {
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'Invalid JSON body' } },
{ status: 400 },
);
}
if (!body || typeof body !== 'object') {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'Payload must be a JSON object' } },
{ status: 400 },
);
}
const data = body as Record<string, unknown>;
if (typeof data.projectRoot !== 'string' || !data.projectRoot.trim()) {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'projectRoot is required' } },
{ status: 400 },
);
}
if (typeof data.epicId !== 'string' || !data.epicId.trim()) {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'epicId is required' } },
{ status: 400 },
);
}
const payload: CreateSwarmPayload = {
projectRoot: data.projectRoot.trim(),
epicId: data.epicId.trim(),
coordinator: typeof data.coordinator === 'string' ? data.coordinator.trim() : undefined,
};
const args = ['swarm', 'create', payload.epicId, '--json'];
if (payload.coordinator) {
args.push('--coordinator', payload.coordinator);
}
const result = await runBdCommand({
projectRoot: payload.projectRoot,
args,
});
if (!result.success) {
return NextResponse.json(
{ ok: false, error: { classification: result.classification ?? 'unknown', message: result.error ?? result.stderr } },
{ status: 400 },
);
}
try {
const swarm = JSON.parse(result.stdout);
return NextResponse.json({ ok: true, swarm });
} catch {
return NextResponse.json(
{ ok: false, error: { classification: 'unknown', message: 'Failed to parse swarm create output' } },
{ status: 500 },
);
}
}

View file

@ -0,0 +1,29 @@
import { NextResponse } from 'next/server';
import { runBdCommand } from '../../../../lib/bridge';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot');
if (!projectRoot) {
return NextResponse.json({ ok: false, error: 'projectRoot is required' }, { status: 400 });
}
// bd formula list --json
const result = await runBdCommand({
projectRoot,
args: ['formula', 'list', '--json', '--allow-stale'],
});
if (!result.success) {
return NextResponse.json({ ok: false, error: result.error }, { status: 500 });
}
try {
// If output is empty or not JSON array, handle gracefully
const json = JSON.parse(result.stdout || '[]');
return NextResponse.json({ ok: true, data: json });
} catch (e) {
return NextResponse.json({ ok: false, error: 'Failed to parse formulas' }, { status: 500 });
}
}

View file

@ -0,0 +1,45 @@
import { NextResponse } from 'next/server';
import { runBdCommand } from '../../../../lib/bridge';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot');
const epicId = searchParams.get('epic');
if (!projectRoot || !epicId) {
return NextResponse.json({ ok: false, error: 'projectRoot and epic are required' }, { status: 400 });
}
// 1. Get the epic itself
const epicResult = await runBdCommand({
projectRoot,
args: ['show', epicId, '--json'],
});
// 2. Get children
const childrenResult = await runBdCommand({
projectRoot,
args: ['list', '--parent', epicId, '--json'],
});
if (!epicResult.success) {
return NextResponse.json({ ok: false, error: 'Failed to fetch epic' }, { status: 500 });
}
try {
const epic = JSON.parse(epicResult.stdout);
// Handle list returning empty or error gracefully
let children = [];
if (childrenResult.success && childrenResult.stdout.trim()) {
children = JSON.parse(childrenResult.stdout);
// bd list returns array, bd show returns object (or array of 1)
}
const epicObj = Array.isArray(epic) ? epic[0] : epic;
const issues = [epicObj, ...children];
return NextResponse.json({ ok: true, data: issues });
} catch (e) {
return NextResponse.json({ ok: false, error: 'Failed to parse graph data' }, { status: 500 });
}
}

View file

@ -0,0 +1,36 @@
import { NextResponse } from 'next/server';
import { joinSwarm } from '../../../../lib/swarm-molecules';
export async function POST(request: Request) {
try {
const body = await request.json();
const { projectRoot, agentId, swarmId } = body;
if (!projectRoot || !agentId || !swarmId) {
return NextResponse.json(
{ ok: false, error: 'Missing required fields' },
{ status: 400 }
);
}
const result = await joinSwarm(
{ agent: agentId, epicId: swarmId },
{ projectRoot }
);
if (!result.ok) {
return NextResponse.json(
{ ok: false, error: result.error?.message },
{ status: 500 }
);
}
return NextResponse.json({ ok: true, data: result.data });
} catch (e) {
return NextResponse.json(
{ ok: false, error: e instanceof Error ? e.message : 'Unknown error' },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,42 @@
import { NextResponse } from 'next/server';
import { runBdCommand } from '../../../../lib/bridge';
export async function POST(request: Request) {
try {
const body = await request.json();
const { projectRoot, title, proto } = body;
if (!projectRoot || !title || !proto) {
return NextResponse.json(
{ ok: false, error: 'Missing required fields: projectRoot, title, proto' },
{ status: 400 }
);
}
// bd mol pour "Title" --proto <proto> --json
const args = ['mol', 'pour', title, '--proto', proto, '--json'];
// Safety: Ensure proto doesn't contain shell injection
if (!/^[a-zA-Z0-9_\-.]+$/.test(proto)) {
return NextResponse.json({ ok: false, error: 'Invalid proto name' }, { status: 400 });
}
const result = await runBdCommand({
projectRoot,
args,
});
if (!result.success) {
return NextResponse.json({ ok: false, error: result.error || result.stderr }, { status: 500 });
}
const data = JSON.parse(result.stdout);
return NextResponse.json({ ok: true, data });
} catch (e) {
return NextResponse.json(
{ ok: false, error: e instanceof Error ? e.message : 'Unknown error' },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,36 @@
import { NextResponse } from 'next/server';
import { leaveSwarm } from '../../../../lib/swarm-molecules';
export async function POST(request: Request) {
try {
const body = await request.json();
const { projectRoot, agentId } = body;
if (!projectRoot || !agentId) {
return NextResponse.json(
{ ok: false, error: 'Missing required fields' },
{ status: 400 }
);
}
const result = await leaveSwarm(
{ agent: agentId },
{ projectRoot }
);
if (!result.ok) {
return NextResponse.json(
{ ok: false, error: result.error?.message },
{ status: 500 }
);
}
return NextResponse.json({ ok: true, data: result.data });
} catch (e) {
return NextResponse.json(
{ ok: false, error: e instanceof Error ? e.message : 'Unknown error' },
{ status: 500 }
);
}
}

View file

@ -0,0 +1,44 @@
import { NextResponse } from 'next/server';
import { runBdCommand } from '../../../../lib/bridge';
export async function GET(request: Request): Promise<Response> {
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot');
if (!projectRoot) {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'projectRoot is required' } },
{ status: 400 },
);
}
const result = await runBdCommand({
projectRoot,
args: ['swarm', 'list', '--json'],
});
if (!result.success) {
return NextResponse.json(
{ ok: false, error: { classification: result.classification ?? 'unknown', message: result.error ?? result.stderr } },
{ status: 400 },
);
}
try {
const rawData = JSON.parse(result.stdout);
// Filter out items that look like agents (start with "Agent:" or have gt:agent style IDs if discernible)
// Real swarms/molecules usually don't start with "Agent:".
const swarms = (rawData.swarms || []).filter((s: any) =>
!s.title.startsWith('Agent: ') &&
!s.title.startsWith('Agent:')
);
return NextResponse.json({ ok: true, data: { swarms } });
} catch {
return NextResponse.json(
{ ok: false, error: { classification: 'unknown', message: 'Failed to parse swarm list output' } },
{ status: 500 },
);
}
}

View file

@ -0,0 +1,29 @@
import { NextResponse } from 'next/server';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
export async function POST(request: Request) {
try {
const { beadId, archetypeId } = await request.json();
if (!beadId || !archetypeId) {
return NextResponse.json({ error: 'Missing beadId or archetypeId' }, { status: 400 });
}
// Use bd CLI to add the archetype label. We leave it 'open' because Prep just assigns the agent.
const cmd = `bd label add ${beadId} "agent:${archetypeId}"`;
const { stdout, stderr } = await execAsync(cmd);
if (stderr && !stderr.includes('Warning')) {
console.error('bd edit stderr:', stderr);
}
return NextResponse.json({ success: true, message: `Prepped ${beadId} for ${archetypeId}`, output: stdout });
} catch (error: any) {
console.error('Prep task failed:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View file

@ -0,0 +1,45 @@
import { NextResponse } from 'next/server';
import { runBdCommand } from '../../../../lib/bridge';
export async function GET(request: Request): Promise<Response> {
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot');
const epicId = searchParams.get('epic');
if (!projectRoot) {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'projectRoot is required' } },
{ status: 400 },
);
}
if (!epicId) {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'epic is required' } },
{ status: 400 },
);
}
const result = await runBdCommand({
projectRoot,
args: ['swarm', 'status', epicId, '--json'],
});
if (!result.success) {
return NextResponse.json(
{ ok: false, error: { classification: result.classification ?? 'unknown', message: result.error ?? result.stderr } },
{ status: 400 },
);
}
try {
const data = JSON.parse(result.stdout);
return NextResponse.json({ ok: true, data });
} catch {
return NextResponse.json(
{ ok: false, error: { classification: 'unknown', message: 'Failed to parse swarm status output' } },
{ status: 500 },
);
}
}

View file

@ -0,0 +1,7 @@
import { NextResponse } from 'next/server';
import { getTemplates } from '../../../../lib/server/beads-fs';
export async function GET() {
const data = await getTemplates();
return NextResponse.json(data);
}