feat(cli): agenthub start <agent> kennt den Architekten — Lagebericht statt Loop
Der Architekt kann prinzipiell nicht in agenthub_work blockieren (er ist die interaktive Gegenstelle des Menschen). Sein Sessionstart ist deshalb ein Lagebericht: Hub-Status, Board, wer woran arbeitet inkl. "kein Check-in seit", wer auf Übernahme wartet, und eine ausdrückliche Warnung, wenn etwas auf sein Review wartet. Dazu das rollenspezifische Briefing (pollen statt blockieren, Approve ist sein Gate, Push gehört dem Menschen, "still" ≠ "tot"). BUGFIX dabei: Die Rolle wurde aus der LOKALEN Config gelesen und hing damit am Startverzeichnis — das Quellrepo hat ein .agenthub OHNE Config, wodurch loadConfig warf und die Auflösung stumm auf "implementer" zurückfiel. Der Architekt bekam so das Implementer-Briefing und hätte sich einen Task geclaimt. Jetzt kommt die Rolle vom Hub-Roster, wenn ein Hub erreichbar ist; der lokale Weg greift nur noch ohne Server. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b5533359d5
commit
e2bbae2044
@ -56,20 +56,105 @@ function briefing(agent: string, role: string): string {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export async function startAgentSession(options: StartAgentOptions): Promise<void> {
|
||||
/**
|
||||
* 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=<nextSeq> → 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<string[]> {
|
||||
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<string> {
|
||||
const { projectCwd, serverUrl } = options;
|
||||
|
||||
let agent = options.agent;
|
||||
let role = options.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);
|
||||
if (!role) {
|
||||
const config = loadConfig(projectCwd);
|
||||
const architect = config.roles?.architect?.preferredAgent;
|
||||
role = agent === architect ? 'architect' : (config.agents?.[agent]?.role ?? 'implementer');
|
||||
}
|
||||
} catch {
|
||||
role = role ?? 'implementer';
|
||||
role = 'implementer';
|
||||
}
|
||||
}
|
||||
|
||||
if (serverUrl) {
|
||||
@ -81,6 +166,14 @@ export async function startAgentSession(options: StartAgentOptions): Promise<voi
|
||||
}
|
||||
}
|
||||
|
||||
// Architekt: Lagebericht statt Loop-Einstieg.
|
||||
if (role === 'architect') {
|
||||
console.log(architectBriefing(agent));
|
||||
console.log('');
|
||||
for (const line of await architectHealthReport(serverUrl)) console.log(line);
|
||||
return 'architect';
|
||||
}
|
||||
|
||||
console.log(briefing(agent, role ?? 'implementer'));
|
||||
|
||||
// Was liegt gerade an? Damit die Session nicht blind in den Loop geht.
|
||||
@ -98,4 +191,5 @@ export async function startAgentSession(options: StartAgentOptions): Promise<voi
|
||||
console.log('');
|
||||
console.log('AKTUELL: Status nicht abrufbar — geh trotzdem in den Loop.');
|
||||
}
|
||||
return role ?? 'implementer';
|
||||
}
|
||||
|
||||
@ -885,11 +885,13 @@ export function createProgram(cwd: string): Command {
|
||||
process.exit(1);
|
||||
}
|
||||
const { serverUrl, projectCwd } = await resolveContext(program, cwd);
|
||||
let role = options.role ?? 'implementer';
|
||||
if (options.briefing !== false) {
|
||||
await startAgentSession({ serverUrl, projectCwd, agent, role: options.role });
|
||||
role = await startAgentSession({ serverUrl, projectCwd, agent, role: options.role });
|
||||
console.log('');
|
||||
}
|
||||
const role = options.role ?? 'implementer';
|
||||
// Der Architekt claimt nichts — sein Start ist der Lagebericht.
|
||||
if (role === 'architect') return;
|
||||
if (serverUrl) {
|
||||
await runRemote(serverUrl, () => startAgent({ serverUrl, projectCwd, agent, role }));
|
||||
} else {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user