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

@ -31,6 +31,8 @@ export interface AgentRecord {
version: number;
rig?: string;
role_type?: string;
swarm_id?: string;
current_task?: string;
}
export interface RegisterAgentInput {
@ -179,11 +181,22 @@ function validateRole(value: string): AgentCommandError | null {
function mapBdAgentToRecord(bdAgent: any): AgentRecord {
// Extract role from labels if role_type is not set
let role = bdAgent.role_type || 'agent';
if (role === 'agent' && Array.isArray(bdAgent.labels)) {
let swarmId: string | undefined;
let currentTask: string | undefined;
if (Array.isArray(bdAgent.labels)) {
const roleLabel = bdAgent.labels.find((l: string) => l.startsWith('role:'));
if (roleLabel) {
role = roleLabel.split(':')[1];
}
const swarmLabel = bdAgent.labels.find((l: string) => l.startsWith('swarm:'));
if (swarmLabel) {
swarmId = swarmLabel.split(':')[1];
}
const workingLabel = bdAgent.labels.find((l: string) => l.startsWith('working:'));
if (workingLabel) {
currentTask = workingLabel.split(':')[1];
}
}
let rig = bdAgent.rig;
@ -204,6 +217,8 @@ function mapBdAgentToRecord(bdAgent: any): AgentRecord {
version: 1,
rig,
role_type: bdAgent.role_type,
swarm_id: swarmId,
current_task: currentTask,
};
return record;
}

View file

@ -102,10 +102,16 @@ export async function runBdCommand(
const shellCommand = buildShellCommand(command, args);
const mingwBin = 'C:\\msys64\\mingw64\\bin';
const existingPath = deps.env.Path ?? deps.env.PATH ?? '';
const enhancedPath = existingPath.includes('mingw64')
? existingPath
: `${mingwBin};${existingPath}`;
const { stdout, stderr } = await deps.exec(shellCommand, {
cwd,
timeout: timeoutMs,
env: deps.env,
env: { ...deps.env, Path: enhancedPath, PATH: enhancedPath },
});
return {

View file

@ -1,15 +1,115 @@
import fs from 'fs/promises';
import path from 'path';
import { AgentArchetype } from '../types-swarm';
import { AgentArchetype, SwarmTemplate } from '../types-swarm';
const ARCHE_DIR = path.join(process.cwd(), '.beads', 'archetypes');
const TEMPLATE_DIR = path.join(process.cwd(), '.beads', 'templates');
const SEED_ARCHETYPES: AgentArchetype[] = [
{
id: 'architect',
name: 'System Architect',
description: 'Designs complex system structures and writes detailed implementation plans.',
systemPrompt: 'You are a staff-level software architect focused on high-level system design.',
capabilities: ['planning', 'design_docs', 'arch_review'],
color: '#3b82f6',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
isBuiltIn: true
},
{
id: 'coder',
name: 'Implementation Engineer',
description: 'Translates plans into precise, type-safe, and tested code.',
systemPrompt: 'You are a senior software engineer focused on execution and clean code.',
capabilities: ['coding', 'refactoring', 'testing'],
color: '#10b981',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
isBuiltIn: true
}
];
export async function getArchetypes(): Promise<AgentArchetype[]> {
try {
await fs.mkdir(ARCHE_DIR, { recursive: true });
// Minimal mock for now to pass test
return [];
const files = await fs.readdir(ARCHE_DIR);
if (files.filter(f => f.endsWith('.json')).length === 0) {
// Seed defaults
for (const arch of SEED_ARCHETYPES) {
await fs.writeFile(path.join(ARCHE_DIR, `${arch.id}.json`), JSON.stringify(arch, null, 2));
}
return SEED_ARCHETYPES;
}
const archetypes: AgentArchetype[] = [];
for (const file of files) {
if (!file.endsWith('.json')) continue;
try {
const content = await fs.readFile(path.join(ARCHE_DIR, file), 'utf-8');
const parsed = JSON.parse(content);
archetypes.push({
...parsed,
id: file.replace('.json', '')
});
} catch (err) {
console.error(`Failed to parse archetype file: ${file}`, err);
}
}
return archetypes;
} catch (e) {
console.error('Error in getArchetypes:', e);
return [];
}
}
const SEED_TEMPLATES: SwarmTemplate[] = [
{
id: 'standard-app',
name: 'Standard Application Swarm',
description: 'A balanced team of an Architect and two Coders for standard feature development.',
team: [
{ archetypeId: 'architect', count: 1 },
{ archetypeId: 'coder', count: 2 }
],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
isBuiltIn: true
}
];
export async function getTemplates(): Promise<SwarmTemplate[]> {
try {
await fs.mkdir(TEMPLATE_DIR, { recursive: true });
const files = await fs.readdir(TEMPLATE_DIR);
if (files.filter(f => f.endsWith('.json')).length === 0) {
for (const tpl of SEED_TEMPLATES) {
await fs.writeFile(path.join(TEMPLATE_DIR, `${tpl.id}.json`), JSON.stringify(tpl, null, 2));
}
return SEED_TEMPLATES;
}
const templates: SwarmTemplate[] = [];
for (const file of files) {
if (!file.endsWith('.json')) continue;
try {
const content = await fs.readFile(path.join(TEMPLATE_DIR, file), 'utf-8');
const parsed = JSON.parse(content);
templates.push({
...parsed,
id: file.replace('.json', '')
});
} catch (err) {
console.error(`Failed to parse template file: ${file}`, err);
}
}
return templates;
} catch (e) {
console.error('Error in getTemplates:', e);
return [];
}
}

View file

@ -74,20 +74,21 @@ function extractAgents(bead: BeadIssue): AgentInfo[] {
}
export function buildSocialCards(beads: BeadIssue[]): SocialCard[] {
const taskBeads = beads.filter((bead) => bead.issue_type !== 'epic');
const beadMap = new Map<string, BeadIssue>();
for (const bead of beads) {
for (const bead of taskBeads) {
beadMap.set(bead.id, bead);
}
const blocksIncoming = new Map<string, string[]>();
const blocksOutgoing = new Map<string, string[]>();
for (const bead of beads) {
for (const bead of taskBeads) {
blocksIncoming.set(bead.id, []);
blocksOutgoing.set(bead.id, []);
}
for (const bead of beads) {
for (const bead of taskBeads) {
for (const dep of bead.dependencies) {
if (dep.type === 'blocks' && beadMap.has(dep.target)) {
const outgoing = blocksOutgoing.get(bead.id) ?? [];
@ -101,15 +102,30 @@ export function buildSocialCards(beads: BeadIssue[]): SocialCard[] {
}
}
return beads.map((bead) => ({
id: bead.id,
title: bead.title,
status: mapStatus(bead.status),
blocks: blocksOutgoing.get(bead.id) ?? [], // what I block (amber)
unblocks: blocksIncoming.get(bead.id) ?? [], // what blocks me (rose)
agents: extractAgents(bead),
lastActivity: new Date(bead.updated_at),
priority: mapPriority(bead.priority),
}));
}
return taskBeads.map((bead) => {
const explicitStatus = mapStatus(bead.status);
const incomingBlockers = blocksIncoming.get(bead.id) ?? [];
const hasUnresolvedIncomingBlockers = incomingBlockers.some((blockerId) => {
const blocker = beadMap.get(blockerId);
return blocker ? blocker.status !== 'closed' && blocker.status !== 'tombstone' : false;
});
const effectiveStatus: SocialCardStatus =
explicitStatus === 'closed' || explicitStatus === 'in_progress' || explicitStatus === 'blocked'
? explicitStatus
: hasUnresolvedIncomingBlockers
? 'blocked'
: explicitStatus;
return {
id: bead.id,
title: bead.title,
status: effectiveStatus,
blocks: blocksOutgoing.get(bead.id) ?? [], // what I block (amber)
unblocks: incomingBlockers, // what blocks me (rose)
agents: extractAgents(bead),
lastActivity: new Date(bead.updated_at),
priority: mapPriority(bead.priority),
};
});
}

64
src/lib/swarm-api.ts Normal file
View file

@ -0,0 +1,64 @@
export interface SwarmFromApi {
id: string;
title: string;
epic_id: string;
epic_title: string;
status: string;
coordinator: string;
total_issues: number;
completed_issues: number;
active_issues: number;
progress_percent: number;
}
export interface SwarmListResponse {
swarms: SwarmFromApi[];
}
export interface SwarmStatusFromApi {
epic_id: string;
epic_title: string;
total_issues: number;
completed: Array<{ id: string; title: string; status: string }>;
active: Array<{ id: string; title: string; status: string }>;
ready: Array<{ id: string; title: string; status: string }>;
blocked: Array<{ id: string; title: string; status: string }>;
progress_percent: number;
active_count: number;
ready_count: number;
blocked_count: number;
}
export interface SwarmCardData {
swarmId: string;
title: string;
epicId: string;
epicTitle: string;
status: string;
coordinator: string;
totalIssues: number;
completedIssues: number;
activeIssues: number;
readyIssues: number;
blockedIssues: number;
progressPercent: number;
agents: import('./agent-registry').AgentRecord[];
}
export function apiSwarmToCardData(swarm: SwarmFromApi, status?: SwarmStatusFromApi): SwarmCardData {
return {
swarmId: swarm.id,
title: swarm.title,
epicId: swarm.epic_id,
epicTitle: swarm.epic_title,
status: swarm.status,
coordinator: swarm.coordinator,
totalIssues: swarm.total_issues,
completedIssues: swarm.completed_issues,
activeIssues: swarm.active_issues,
readyIssues: status?.ready_count ?? 0,
blockedIssues: status?.blocked_count ?? 0,
progressPercent: swarm.progress_percent,
agents: [], // Populated separately via agent-registry
};
}