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:
parent
409a7e7256
commit
dfaf523029
74 changed files with 11066 additions and 2046 deletions
70
src/app/api/swarm/close/route.ts
Normal file
70
src/app/api/swarm/close/route.ts
Normal 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 });
|
||||
}
|
||||
78
src/app/api/swarm/create/route.ts
Normal file
78
src/app/api/swarm/create/route.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
29
src/app/api/swarm/formulas/route.ts
Normal file
29
src/app/api/swarm/formulas/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
45
src/app/api/swarm/graph/route.ts
Normal file
45
src/app/api/swarm/graph/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
36
src/app/api/swarm/join/route.ts
Normal file
36
src/app/api/swarm/join/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
42
src/app/api/swarm/launch/route.ts
Normal file
42
src/app/api/swarm/launch/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
36
src/app/api/swarm/leave/route.ts
Normal file
36
src/app/api/swarm/leave/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
44
src/app/api/swarm/list/route.ts
Normal file
44
src/app/api/swarm/list/route.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
29
src/app/api/swarm/prep/route.ts
Normal file
29
src/app/api/swarm/prep/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
45
src/app/api/swarm/status/route.ts
Normal file
45
src/app/api/swarm/status/route.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
7
src/app/api/swarm/templates/route.ts
Normal file
7
src/app/api/swarm/templates/route.ts
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue