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
76 lines
2.8 KiB
TypeScript
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' };
|
|
});
|
|
}
|