diff --git a/src/cli/commands/startAgent.ts b/src/cli/commands/startAgent.ts index ea47113..72635d2 100644 --- a/src/cli/commands/startAgent.ts +++ b/src/cli/commands/startAgent.ts @@ -56,20 +56,105 @@ function briefing(agent: string, role: string): string { ].join('\n'); } -export async function startAgentSession(options: StartAgentOptions): Promise { +/** + * Der Architekt startet anders als ein Implementer: er kann prinzipiell NICHT + * in `agenthub_work` blockieren (er ist die interaktive Gegenstelle des + * Menschen), also ist Pollen für ihn kein Workaround, sondern der richtige Weg. + * Sein Sessionstart ist deshalb ein Lagebericht, kein Loop-Einstieg. + */ +function architectBriefing(agent: string): string { + return [ + `Du bist "${agent}" — Architekt im AgentHub.`, + '', + 'DEIN UNTERSCHIED ZU DEN IMPLEMENTERN:', + 'Du gehst NICHT in agenthub_work. Ein blockierender Loop ist für dich', + 'unmöglich, weil du gleichzeitig mit dem Menschen sprichst. Stattdessen', + 'prüfst du den Stand BEI JEDEM ZUG selbst:', + ' GET /architect/pulse?since= → Reviews, dormante Agenten,', + ' neue Nachrichten, Änderungen. Klein genug für jeden Zug.', + '', + 'DEINE GATES:', + ' - Review/Approve ist DEIN Gate. Du fragst dafür niemanden um Erlaubnis.', + ' - Push, Release-Builds und Grundsatzentscheidungen gehören dem Menschen.', + ' - Was auf `review` steht, wartet auf DICH — melde es ungefragt.', + ' - Agenten mit offener Review sind gebunden (DEC-0035): erst dein done', + ' oder reopen gibt sie frei.', + '', + 'WENN ETWAS STILL AUSSIEHT:', + 'Ein Agent, der arbeitet, empfängt nichts — er ist taub bis zu seinem', + 'nächsten Check-in. "Kein Log seit X" heißt also nicht "tot". Prüfe erst', + 'das Task-Log, bevor du jemanden für dormant erklärst.', + ].join('\n'); +} + +async function architectHealthReport(serverUrl: string | undefined): Promise { + const out: string[] = []; + if (!serverUrl) { + out.push('HUB: kein Server erreichbar — lokaler Dateimodus.'); + return out; + } + try { + const health = await remoteClient.getHealth(serverUrl); + const c = health.counts; + out.push(`HUB: ${health.status} · v${health.version} · seit ${Math.round(health.uptimeSec / 60)} min`); + out.push(`BOARD: ${c.open} offen · ${c.inProgress} in Arbeit · ${c.review} in Review`); + + const busy = health.agents.filter((a) => a.state === 'busy'); + const waiting = health.agents.filter((a) => a.pendingCount > 0 && a.state !== 'busy'); + if (busy.length) { + out.push('IN ARBEIT:'); + for (const a of busy) { + const deaf = a.deafForSec != null ? ` · kein Check-in seit ${Math.round(a.deafForSec / 60)} min` : ''; + out.push(` ${a.name} → ${a.taskId ?? '?'}${deaf}`); + } + } + if (waiting.length) { + out.push('WARTET AUF ÜBERNAHME:'); + for (const a of waiting) out.push(` ${a.name}: ${a.pendingCount} Task(s)`); + } + if (c.review > 0) out.push(`⚠️ ${c.review} Task(s) warten auf DEIN Review — zuerst erledigen.`); + if (!busy.length && !waiting.length && !c.review) out.push('Nichts hängt. Sauberer Start.'); + } catch { + out.push('HUB: nicht erreichbar. Läuft `agenthub server start --host 0.0.0.0` aus dem Projekt-Root?'); + } + return out; +} + +/** Gibt die aufgelöste Rolle zurück, damit der Aufrufer weiß, ob noch ein + * Implementer-Onboarding (Task claimen) folgen soll. */ +export async function startAgentSession(options: StartAgentOptions): Promise { const { projectCwd, serverUrl } = options; let agent = options.agent; let role = options.role; - try { - agent = resolveAgentName(projectCwd, options.agent); - if (!role) { + + // Rolle IMMER aus dem Roster des Hubs, wenn einer erreichbar ist: der Server + // hält die Wahrheit. Der lokale Weg greift nur ohne Hub — sonst hängt das + // Ergebnis davon ab, aus welchem Verzeichnis der Befehl gestartet wurde + // (das Quellrepo hat z. B. ein .agenthub OHNE Config, was die lokale + // Auflösung stumm auf "implementer" zurückfallen ließ). + if (!role && serverUrl) { + try { + const health = await remoteClient.getHealth(serverUrl); + const match = health.agents.find((a) => a.name.toLowerCase() === agent.toLowerCase()); + if (match) { + agent = match.name; + role = match.role; + } + } catch { + // Hub nicht erreichbar — lokaler Weg unten. + } + } + + if (!role) { + try { + agent = resolveAgentName(projectCwd, options.agent); const config = loadConfig(projectCwd); const architect = config.roles?.architect?.preferredAgent; role = agent === architect ? 'architect' : (config.agents?.[agent]?.role ?? 'implementer'); + } catch { + role = 'implementer'; } - } catch { - role = role ?? 'implementer'; } if (serverUrl) { @@ -81,6 +166,14 @@ export async function startAgentSession(options: StartAgentOptions): Promise startAgent({ serverUrl, projectCwd, agent, role })); } else {