feat(cli): agenthub start <agent> — ein Einzeiler statt Copy-Paste-Prompt

Der Mensch soll pro Session eine Zeile tippen, nicht einen Textblock
einfügen (CEO: „die Prompts sind mega lang"). `agenthub start codex` druckt
jetzt das verbindliche Briefing selbst — Loop-Pflicht, Taubheit während der
Arbeit, pending-Block als einziger Empfangsweg, DEC-0035 — und zeigt
anschließend, was gerade auf den Agenten wartet.

Kein zweites Kommando: das bestehende `start` (Onboarding: announce, Task
claimen, Handoff drucken) nimmt jetzt ein positionales Agent-Argument und
stellt das Briefing voran. `--agent` bleibt für Hooks/Skripte, `--no-briefing`
gibt das alte Verhalten.

Die Rolle kommt aus dem Roster (Architekt wird als solcher erkannt), sodass
`agenthub start <name>` für jeden Agenten ohne weitere Flags reicht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-07-29 18:34:48 +02:00
parent 1f3126b3b5
commit b5533359d5
2 changed files with 125 additions and 7 deletions

View File

@ -0,0 +1,101 @@
import { loadConfig } from '../../core/config.js';
import { resolveAgentName } from '../../core/services/identityService.js';
import { resolvePending } from '../../core/services/checkinService.js';
import { remoteClient } from '../remoteClient.js';
/**
* `agenthub start <agent>` der EINE Befehl, mit dem eine frische
* Agenten-Session anfängt.
*
* Motivation (CEO 2026-07-29): Die Prompts sind mega lang, ich brauche eine
* Version, die ich immer verwenden kann." Statt einen Textblock in jede
* Session zu kopieren, druckt dieser Befehl das verbindliche Briefing selbst
* inklusive dessen, was gerade auf den Agenten wartet. Der Mensch tippt eine
* Zeile, der Agent liest den Rest hier.
*
* Absichtlich reiner Text (kein JSON): die Ausgabe landet im Kontext eines
* LLM-Agenten und muss als Anweisung lesbar sein.
*/
export interface StartAgentOptions {
serverUrl?: string;
projectCwd: string;
agent: string;
role?: string;
}
function briefing(agent: string, role: string): string {
return [
`Du bist "${agent}" im AgentHub (Rolle: ${role}).`,
'',
'SO ARBEITEST DU:',
`1. Ruf JETZT agenthub_work(agent="${agent}", role="${role}", timeoutSec=50) auf.`,
' Danach nach JEDEM abgeschlossenen Schritt erneut — auch nach agenthub_task_review.',
' Beende deinen Turn nie, ohne agenthub_work neu gestartet zu haben.',
'2. Deine Aufgabe steht IMMER im Handoff des Tasks. Lies ihn, bevor du baust.',
`3. Logge nach jedem Teilschritt: agenthub_task_log(id, text, agent="${agent}").`,
'',
'WÄHREND DER ARBEIT BIST DU TAUB — das ist der wichtigste Punkt:',
'Solange du einen Task ausführst, erreichen dich KEINE Events. Der einzige',
'Draht nach außen ist die ANTWORT von agenthub_task_log. Deshalb ist Loggen',
'keine Höflichkeit, sondern dein Empfang.',
'',
' - Enthält die Antwort `pending.interrupted`, wurde dir der Task',
' zurückgegeben, abgebrochen oder entzogen: SOFORT aufhören, NICHT',
' einreichen, `pending.interrupted.action` befolgen.',
' - Enthält sie ungelesene Nachrichten: mit agenthub_inbox lesen und antworten.',
` - Für lange Strecken ohne Log-Zeile: agenthub_checkin(agent="${agent}", taskId="<dein Task>").`,
'',
'NACH DEM EINREICHEN (DEC-0035):',
'Mit agenthub_task_review bist du GEBUNDEN, bis der Architekt abnimmt oder',
'zurückweist. Du bekommst bewusst keine neue Task — das ist kein Fehler.',
'Bleib im Loop, du wirst geweckt.',
'',
'GRENZEN: Du markierst NIE selbst done (das ist das Gate des Architekten).',
'Du pushst nie ohne Freigabe. Bei Unklarheit: agenthub_ask statt raten.',
].join('\n');
}
export async function startAgentSession(options: StartAgentOptions): Promise<void> {
const { projectCwd, serverUrl } = options;
let agent = options.agent;
let role = options.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';
}
if (serverUrl) {
try {
await remoteClient.announce(serverUrl, agent, role);
} catch {
// Präsenz ist best-effort — ein nicht erreichbarer Hub darf den Start
// nicht verhindern, das Briefing gilt trotzdem.
}
}
console.log(briefing(agent, role ?? 'implementer'));
// Was liegt gerade an? Damit die Session nicht blind in den Loop geht.
try {
const pending = serverUrl
? ((await remoteClient.getPending(serverUrl, agent)) as ReturnType<typeof resolvePending>)
: resolvePending(projectCwd, agent);
const lines: string[] = [];
if (pending.waitingTasks.length) lines.push(`Offen für dich: ${pending.waitingTasks.join(', ')}`);
if (pending.awaitingReview.length) lines.push(`Wartet auf Architekten-Review: ${pending.awaitingReview.join(', ')} — du bist so lange gebunden.`);
if (pending.unreadCount) lines.push(`Ungelesene Nachrichten: ${pending.unreadCount}`);
console.log('');
console.log(lines.length ? `AKTUELL:\n ${lines.join('\n ')}` : 'AKTUELL: nichts offen — geh in den Loop und warte.');
} catch {
console.log('');
console.log('AKTUELL: Status nicht abrufbar — geh trotzdem in den Loop.');
}
}

View File

@ -15,6 +15,7 @@ import { update } from './commands/update.js';
import { watchEvents } from './commands/watch.js'; import { watchEvents } from './commands/watch.js';
import { startAgent } from './commands/start.js'; import { startAgent } from './commands/start.js';
import { workAgent } from './commands/work.js'; import { workAgent } from './commands/work.js';
import { startAgentSession } from './commands/startAgent.js';
import { startMcpServer } from '../mcp/server.js'; import { startMcpServer } from '../mcp/server.js';
import { installMcp } from '../mcp/install.js'; import { installMcp } from '../mcp/install.js';
import { loadConfig, saveConfig } from '../core/config.js'; import { loadConfig, saveConfig } from '../core/config.js';
@ -866,17 +867,33 @@ export function createProgram(cwd: string): Command {
}); });
// ─── start (onboarding) ──────────────────────────────────────────────────── // ─── start (onboarding) ────────────────────────────────────────────────────
// Session-Einstieg für einen Agenten. `agenthub start codex` (positional) ist
// die Form, die ein Mensch in eine frische Session tippt — sie druckt zuerst
// das verbindliche Briefing (Loop, Check-in-Kanal, DEC-0035) und macht dann
// das Onboarding (anmelden, adressierten Task claimen, Handoff zeigen).
// `--agent` bleibt für Skripte/Hooks erhalten.
program program
.command('start') .command('start [agent]')
.description('Onboard: announce, claim the task addressed to you, print it + its handoff') .description('Start an agent session: briefing + announce + claim the task addressed to you')
.requiredOption('--agent <name>', 'Agent name') .option('--agent <name>', 'Agent name (alternative to the positional argument)')
.option('--role <role>', 'Role (default: implementer)', 'implementer') .option('--role <role>', 'Role (default: from the roster, else implementer)')
.action(async (options) => { .option('--no-briefing', 'Skip the standing briefing (onboarding output only)')
.action(async (positional: string | undefined, options: { agent?: string; role?: string; briefing?: boolean }) => {
const agent = positional ?? options.agent;
if (!agent) {
console.error('Which agent? Usage: agenthub start <agent> (e.g. agenthub start codex)');
process.exit(1);
}
const { serverUrl, projectCwd } = await resolveContext(program, cwd); const { serverUrl, projectCwd } = await resolveContext(program, cwd);
if (options.briefing !== false) {
await startAgentSession({ serverUrl, projectCwd, agent, role: options.role });
console.log('');
}
const role = options.role ?? 'implementer';
if (serverUrl) { if (serverUrl) {
await runRemote(serverUrl, () => startAgent({ serverUrl, projectCwd, agent: options.agent, role: options.role })); await runRemote(serverUrl, () => startAgent({ serverUrl, projectCwd, agent, role }));
} else { } else {
await startAgent({ projectCwd, agent: options.agent, role: options.role }); await startAgent({ projectCwd, agent, role });
} }
}); });