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');
|
].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;
|
const { projectCwd, serverUrl } = options;
|
||||||
|
|
||||||
let agent = options.agent;
|
let agent = options.agent;
|
||||||
let role = options.role;
|
let role = options.role;
|
||||||
try {
|
|
||||||
agent = resolveAgentName(projectCwd, options.agent);
|
// Rolle IMMER aus dem Roster des Hubs, wenn einer erreichbar ist: der Server
|
||||||
if (!role) {
|
// 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 config = loadConfig(projectCwd);
|
||||||
const architect = config.roles?.architect?.preferredAgent;
|
const architect = config.roles?.architect?.preferredAgent;
|
||||||
role = agent === architect ? 'architect' : (config.agents?.[agent]?.role ?? 'implementer');
|
role = agent === architect ? 'architect' : (config.agents?.[agent]?.role ?? 'implementer');
|
||||||
|
} catch {
|
||||||
|
role = 'implementer';
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
role = role ?? 'implementer';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverUrl) {
|
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'));
|
console.log(briefing(agent, role ?? 'implementer'));
|
||||||
|
|
||||||
// Was liegt gerade an? Damit die Session nicht blind in den Loop geht.
|
// 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('');
|
||||||
console.log('AKTUELL: Status nicht abrufbar — geh trotzdem in den Loop.');
|
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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const { serverUrl, projectCwd } = await resolveContext(program, cwd);
|
const { serverUrl, projectCwd } = await resolveContext(program, cwd);
|
||||||
|
let role = options.role ?? 'implementer';
|
||||||
if (options.briefing !== false) {
|
if (options.briefing !== false) {
|
||||||
await startAgentSession({ serverUrl, projectCwd, agent, role: options.role });
|
role = await startAgentSession({ serverUrl, projectCwd, agent, role: options.role });
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
const role = options.role ?? 'implementer';
|
// Der Architekt claimt nichts — sein Start ist der Lagebericht.
|
||||||
|
if (role === 'architect') return;
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
await runRemote(serverUrl, () => startAgent({ serverUrl, projectCwd, agent, role }));
|
await runRemote(serverUrl, () => startAgent({ serverUrl, projectCwd, agent, role }));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user