diff --git a/package.json b/package.json index ea8c4ad..afb6565 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agenthub", - "version": "0.1.1", + "version": "0.1.2", "description": "Local coordination layer for AI coding agents", "type": "module", "main": "./dist/index.js", diff --git a/src/cli/commands/decision.ts b/src/cli/commands/decision.ts index f3b162a..7544e1e 100644 --- a/src/cli/commands/decision.ts +++ b/src/cli/commands/decision.ts @@ -8,7 +8,7 @@ export async function decisionCreate(cwd: string, options: Partial = { const decision = options.decision ?? await input({ message: 'Decision:' }); const record = createDecision(cwd, { title, context, decision }); - console.log(`Decision recorded: ${record.id}`); + console.log(`AgentHub: Decision recorded ${record.id}`); } export function decisionList(cwd: string): void { diff --git a/src/cli/commands/delegate.ts b/src/cli/commands/delegate.ts index 9d3b1c0..897f8bc 100644 --- a/src/cli/commands/delegate.ts +++ b/src/cli/commands/delegate.ts @@ -16,7 +16,7 @@ export async function delegate(cwd: string, options: { auto?: boolean } = {}): P if (config.delegationMode === 'auto' || options.auto) { autoDelegate(cwd); - console.log('Handoff created automatically.'); + console.log('AgentHub: Handoff created automatically'); } else { console.log('Run with --auto to create the handoff, or run:'); console.log(` agenthub handoff create --taskId ${suggestion.task.id}`); diff --git a/src/cli/commands/handoff.ts b/src/cli/commands/handoff.ts index 841fbec..c1b356b 100644 --- a/src/cli/commands/handoff.ts +++ b/src/cli/commands/handoff.ts @@ -21,7 +21,7 @@ export async function handoffCreate(cwd: string, options: Partial = {}) context, }); - console.log(`Handoff created: ${handoff.id}`); + console.log(`AgentHub: Handoff created ${handoff.id} → ${handoff.toRole}`); } export function handoffRead(cwd: string, id: string): void { diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index e3af6b2..4be6d3f 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -45,9 +45,9 @@ export async function init( if (codex) writeFileSync(join(cwd, 'CODEX.md'), codexMd(), 'utf-8'); if (kimi) writeFileSync(join(cwd, 'KIMI.md'), kimiMd(), 'utf-8'); - console.log(`AgentHub initialized for "${projectName}".`); + console.log(`AgentHub: initialized for "${projectName}".`); if (serverUrl) { - console.log(`Connected to remote server at ${serverUrl}.`); + console.log(`AgentHub: connected to remote server at ${serverUrl}.`); } console.log('Run `agenthub status` to see the current project state.'); } diff --git a/src/cli/commands/memory.ts b/src/cli/commands/memory.ts index 36d0597..de284f1 100644 --- a/src/cli/commands/memory.ts +++ b/src/cli/commands/memory.ts @@ -51,7 +51,7 @@ export async function memoryAdd(cwd: string, options: MemoryAddOptions = {}): Pr by: options.by, } as Partial); - console.log(`Memory saved as ${memory.id}.`); + console.log(`AgentHub: Memory saved ${memory.id} ${memory.title}`); } export function memorySearch(cwd: string, query: string): void { diff --git a/src/cli/commands/task.ts b/src/cli/commands/task.ts index c272e7f..eea1c45 100644 --- a/src/cli/commands/task.ts +++ b/src/cli/commands/task.ts @@ -16,7 +16,7 @@ export async function taskCreate(cwd: string, options: Partial = {}): Prom const priority = options.priority ?? 'medium'; const task = createTask(cwd, { title, role, priority }); - console.log(`Created ${task.id}: ${task.title}`); + console.log(`AgentHub: Task created ${task.id} ${task.title}`); } export function taskList(cwd: string, filters?: { status?: string; role?: string }): void { @@ -39,7 +39,7 @@ export function taskShow(cwd: string, id: string): void { export function taskClaim(cwd: string, id: string, agentName: string): void { claimTask(cwd, id, agentName); - console.log(`${id} claimed by ${agentName}.`); + console.log(`AgentHub: Task claimed ${id} by ${agentName}`); } export function taskDone( @@ -52,5 +52,5 @@ export function taskDone( doneTokens: meta?.tokens, doneDuration: meta?.duration, }); - console.log(`${id} marked as done.`); + console.log(`AgentHub: Task done ${id}`); } diff --git a/src/cli/commands/watch.ts b/src/cli/commands/watch.ts index b877a43..7c89bd8 100644 --- a/src/cli/commands/watch.ts +++ b/src/cli/commands/watch.ts @@ -47,16 +47,54 @@ export function parseSSEBuffer(buffer: string): { events: AgentHubEvent[]; remai } /** - * Format an event as a single compact log line, e.g.: - * [task/updated] TSK-0003 My task title status=done (windows-claude) + * Map an event to a human label + trailing detail. Keeps the wire-level + * type/action out of the user-facing line in favor of a verb the user reads + * at a glance ("Task received", "Task done", "Handoff", ...). + */ +function describeEvent(event: AgentHubEvent): { label: string; detail: string } { + switch (event.type) { + case 'task': { + if (event.action === 'created') { + const status = event.status ? `[${event.status}]` : ''; + return { label: 'Task received', detail: [event.title, status].filter(Boolean).join(' ') }; + } + switch (event.status) { + case 'done': + return { label: 'Task done', detail: event.assignedTo ? `by ${event.assignedTo}` : '' }; + case 'in_progress': + return { label: 'Task claimed', detail: event.assignedTo ? `by ${event.assignedTo}` : '' }; + case 'review': + return { label: 'Task review', detail: event.assignedTo ? `by ${event.assignedTo}` : '' }; + case 'cancelled': + return { label: 'Task cancelled', detail: '' }; + case 'open': + return { label: 'Task reopened', detail: '' }; + default: + return { label: 'Task updated', detail: event.status ? `status=${event.status}` : '' }; + } + } + case 'handoff': + return { label: 'Handoff', detail: [event.title, event.role ? `→ ${event.role}` : ''].filter(Boolean).join(' ') }; + case 'decision': + return { label: 'Decision', detail: event.title ?? '' }; + case 'memory': + return { label: 'Memory', detail: event.title ?? '' }; + } +} + +/** + * Format an event as a single branded log line, e.g.: + * AgentHub: Task received TSK-0019 codex: Magic-App Audit [open] + * AgentHub: Task done TSK-0014 by windows-claude + * AgentHub: Handoff HOF-0012 → implementer + * + * Every line carries the `AgentHub:` prefix so it's recognizable in any + * agent's console (Claude / Codex / Kimi) regardless of surrounding output. */ export function formatEvent(event: AgentHubEvent): string { - const parts: string[] = [`[${event.type}/${event.action}]`, event.id]; - if (event.title) parts.push(event.title); - if (event.status) parts.push(`status=${event.status}`); - if (event.role) parts.push(`role=${event.role}`); - if (event.assignedTo) parts.push(`(${event.assignedTo})`); - return parts.join(' '); + const { label, detail } = describeEvent(event); + const head = `AgentHub: ${label.padEnd(14)} ${event.id}`; + return detail ? `${head} ${detail}` : head; } /** @@ -94,6 +132,8 @@ export async function watchEvents( return; } + console.log(options.role ? `AgentHub: connected (${options.role})` : 'AgentHub: connected'); + const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; diff --git a/src/cli/index.ts b/src/cli/index.ts index c6dde62..a9a7b6c 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -141,7 +141,7 @@ export function createProgram(cwd: string): Command { duration, by: options.by as string | undefined, } as Partial); - console.log(`Memory saved as ${memory.id}.`); + console.log(`AgentHub: Memory saved ${memory.id} ${memory.title}`); }); } else { await memoryAdd(projectCwd, { @@ -199,7 +199,7 @@ export function createProgram(cwd: string): Command { if (serverUrl) { await runRemote(serverUrl, async () => { const task = await remoteClient.createTask(serverUrl, options); - console.log(`Created ${task.id}: ${task.title}`); + console.log(`AgentHub: Task created ${task.id} ${task.title}`); }); } else { await taskCreate(projectCwd, options); @@ -247,7 +247,7 @@ export function createProgram(cwd: string): Command { if (serverUrl) { await runRemote(serverUrl, async () => { await remoteClient.claimTask(serverUrl, id, options.agent); - console.log(`${id} claimed by ${options.agent}.`); + console.log(`AgentHub: Task claimed ${id} by ${options.agent}`); }); } else { taskClaim(projectCwd, id, options.agent); @@ -269,7 +269,7 @@ export function createProgram(cwd: string): Command { if (serverUrl) { await runRemote(serverUrl, async () => { await remoteClient.doneTask(serverUrl, id, meta); - console.log(`${id} marked as done.`); + console.log(`AgentHub: Task done ${id}`); }); } else { taskDone(projectCwd, id, meta); @@ -291,7 +291,7 @@ export function createProgram(cwd: string): Command { if (serverUrl) { await runRemote(serverUrl, async () => { const handoff = await remoteClient.createHandoff(serverUrl, options); - console.log(`Handoff created: ${handoff.id}`); + console.log(`AgentHub: Handoff created ${handoff.id} → ${handoff.toRole}`); }); } else { await handoffCreate(projectCwd, options); @@ -343,7 +343,7 @@ export function createProgram(cwd: string): Command { if (serverUrl) { await runRemote(serverUrl, async () => { const decision = await remoteClient.createDecision(serverUrl, options); - console.log(`Decision recorded: ${decision.id}`); + console.log(`AgentHub: Decision recorded ${decision.id}`); }); } else { await decisionCreate(projectCwd, options); @@ -381,7 +381,7 @@ export function createProgram(cwd: string): Command { console.log(` Task: ${s.task.id} — ${s.task.title}`); console.log(` Role: ${s.role}`); console.log(` Preferred agent: ${s.preferredAgent}`); - if (result.handoff) console.log('Handoff created automatically.'); + if (result.handoff) console.log('AgentHub: Handoff created automatically'); else console.log('Run with --auto to create the handoff.'); }); } else { diff --git a/src/server/index.ts b/src/server/index.ts index bf90d01..c175ba7 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -27,7 +27,7 @@ export async function startServer(cwd: string, options: { port?: number; host?: const address = app.server.address(); const actualPort = typeof address === 'string' ? port : (address?.port ?? port); const url = `http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${actualPort}`; - console.log(`AgentHub server listening on ${url}`); + console.log(`AgentHub: server listening on ${url}`); const advertiseUrl = resolveAdvertiseUrl(host, actualPort); broadcaster = startDiscoveryBroadcaster(advertiseUrl); diff --git a/tests/sse.test.ts b/tests/sse.test.ts index 46d110c..98bf27b 100644 --- a/tests/sse.test.ts +++ b/tests/sse.test.ts @@ -78,13 +78,23 @@ describe('parseSSEBuffer', () => { // ─── 2. Unit: formatEvent ──────────────────────────────────────────────────── -describe('formatEvent', () => { - it('formats a minimal event (no optional fields)', () => { +describe('formatEvent (AgentHub-branded)', () => { + it('every line starts with the AgentHub: prefix', () => { const ev: AgentHubEvent = { type: 'decision', action: 'created', id: 'DEC-0001' }; - expect(formatEvent(ev)).toBe('[decision/created] DEC-0001'); + expect(formatEvent(ev).startsWith('AgentHub: ')).toBe(true); }); - it('formats a full task event matching the spec example', () => { + it('formats a created task as "Task received [status]"', () => { + const ev: AgentHubEvent = { type: 'task', action: 'created', id: 'TSK-0019', title: 'Magic-App Audit', status: 'open' }; + expect(formatEvent(ev)).toMatch(/^AgentHub: Task received\s+TSK-0019\s+Magic-App Audit\s+\[open\]$/); + }); + + it('formats a created task with no title as "Task received <id> [status]"', () => { + const ev: AgentHubEvent = { type: 'task', action: 'created', id: 'TSK-0001', status: 'open' }; + expect(formatEvent(ev)).toMatch(/^AgentHub: Task received\s+TSK-0001\s+\[open\]$/); + }); + + it('formats a completed task as "Task done <id> by <agent>"', () => { const ev: AgentHubEvent = { type: 'task', action: 'updated', @@ -94,12 +104,22 @@ describe('formatEvent', () => { role: 'implementer', assignedTo: 'windows-claude', }; - expect(formatEvent(ev)).toBe('[task/updated] TSK-0003 Implement auth status=done role=implementer (windows-claude)'); + expect(formatEvent(ev)).toMatch(/^AgentHub: Task done\s+TSK-0003\s+by windows-claude$/); }); - it('omits absent optional fields', () => { - const ev: AgentHubEvent = { type: 'task', action: 'created', id: 'TSK-0001', status: 'open' }; - expect(formatEvent(ev)).toBe('[task/created] TSK-0001 status=open'); + it('formats a claimed task as "Task claimed <id> by <agent>"', () => { + const ev: AgentHubEvent = { type: 'task', action: 'updated', id: 'TSK-0014', status: 'in_progress', assignedTo: 'codex' }; + expect(formatEvent(ev)).toMatch(/^AgentHub: Task claimed\s+TSK-0014\s+by codex$/); + }); + + it('formats a handoff as "Handoff <id> → <role>"', () => { + const ev: AgentHubEvent = { type: 'handoff', action: 'created', id: 'HOF-0012', title: 'For codex', role: 'implementer' }; + expect(formatEvent(ev)).toMatch(/^AgentHub: Handoff\s+HOF-0012\s+.*→ implementer$/); + }); + + it('formats a decision with no detail as just the prefix + id', () => { + const ev: AgentHubEvent = { type: 'decision', action: 'created', id: 'DEC-0001' }; + expect(formatEvent(ev)).toMatch(/^AgentHub: Decision\s+DEC-0001$/); }); });