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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
64
src/lib/swarm-api.ts
Normal 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
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue