agenthub/src/core/services/rosterService.ts
chahinebrini df698c9dff feat(agenthub): Rollenwechsel, agent-setup + Push-Channel
roleService haelt roles.*.preferredAgent (Routing-Autoritaet fuer asks,
Watchdog, start) und agents.*.role (Health/Board/MCP-Discovery) konsistent —
bisher konnte nur eine Seite gesetzt werden und der Roster lief auseinander.
Dazu agent-setup-Kommando, Push-Channel im MCP und Selbstheilung im
remoteClient.

Build clean, 304 Tests in 55 Dateien gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113SC5FkaMxcRzXHDBsHmdJ
2026-08-02 21:38:47 +02:00

76 lines
2.8 KiB
TypeScript

import { loadConfig } from '../config.js';
import { listTasks } from './taskService.js';
/** One agent in the team, resolved from the roster (config.agents) with a
* fallback to whatever agents actually touched tasks. */
export interface RosterEntry {
name: string;
role: string;
model?: string;
kind?: string;
description?: string;
budgetEur?: number;
dispatch: 'loop' | 'architect';
}
/** Infer a provider ("kind") from an agent name when the roster doesn't say. */
export function inferKind(name: string): string | undefined {
const n = name.toLowerCase();
if (n.includes('claude') || n === 'backyard' || n === 'architect') return 'anthropic';
if (n.includes('codex') || n.includes('gpt')) return 'openai';
if (n.includes('kimi')) return 'moonshot';
return undefined;
}
/** Rough default display model per provider, for agents missing from the roster. */
function inferModel(kind?: string): string | undefined {
if (kind === 'anthropic') return 'Claude';
if (kind === 'openai') return 'GPT-5 Codex';
if (kind === 'moonshot') return 'Kimi K2';
return undefined;
}
/**
* The team roster: the named agents from `config.agents` if configured,
* otherwise one entry per agent that has touched a task (assignedTo / doneBy),
* so the board/budget views work even before a roster is filled in.
*/
export function getRoster(cwd: string): RosterEntry[] {
const config = loadConfig(cwd);
if (config.agents && Object.keys(config.agents).length > 0) {
const preferredRole = new Map<string, string>();
for (const [role, spec] of Object.entries(config.roles)) {
// Architect is authoritative when one agent is preferred for several
// roles (the default Claude config also prefers it as reviewer).
if (!preferredRole.has(spec.preferredAgent) || role === 'architect') {
preferredRole.set(spec.preferredAgent, role);
}
}
return Object.entries(config.agents).map(([name, a]) => ({
name,
role: preferredRole.get(name) ?? a.role,
model: a.model,
kind: a.kind ?? inferKind(name),
description: a.description,
budgetEur: a.budgetEur,
dispatch: a.dispatch,
}));
}
// Fallback: derive from tasks + the role config's preferred agents.
const names = new Set<string>();
for (const [, cfg] of Object.entries(config.roles)) names.add(cfg.preferredAgent);
const roleByAgent = new Map<string, string>();
for (const [role, cfg] of Object.entries(config.roles)) roleByAgent.set(cfg.preferredAgent, role);
for (const t of listTasks(cwd)) {
if (t.assignedTo) names.add(t.assignedTo);
}
return Array.from(names).map((name) => {
const kind = inferKind(name);
return { name, role: roleByAgent.get(name) ?? 'implementer', kind, model: inferModel(kind), dispatch: 'loop' };
});
}