feat(cli): brand all AgentHub console output with "AgentHub:" prefix

Every user-facing line — the watch stream and each command's success
line, on both the local and --server (remote) paths — now carries an
"AgentHub:" prefix so it's recognizable in any agent's console
(Claude / Codex / Kimi), independent of the board.

- watch: formatEvent rewritten to verb-based, branded lines
  ("AgentHub: Task received <id>  <title>  [status]",
   "AgentHub: Task done <id>  by <agent>", "AgentHub: Handoff …"),
  plus an "AgentHub: connected" line on stream start.
- task/handoff/decision/memory/delegate/init + server-listening lines
  branded on the local command path.
- cli/index.ts: same branding on the --server remote path (the path
  agents actually hit), so CLI line and SSE stream now match.
- tests: formatEvent assertions updated to the branded format
  (regex-tolerant of column padding). 106/106 green.

codex + kimi stay implementers by convention (role=implementer +
assignedTo) — no schema change. Bump 0.1.1 -> 0.1.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-06-27 16:09:15 +02:00
parent 722388cb00
commit ff79cbb639
11 changed files with 94 additions and 34 deletions

View File

@ -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",

View File

@ -8,7 +8,7 @@ export async function decisionCreate(cwd: string, options: Partial<Decision> = {
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 {

View File

@ -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}`);

View File

@ -21,7 +21,7 @@ export async function handoffCreate(cwd: string, options: Partial<Handoff> = {})
context,
});
console.log(`Handoff created: ${handoff.id}`);
console.log(`AgentHub: Handoff created ${handoff.id}${handoff.toRole}`);
}
export function handoffRead(cwd: string, id: string): void {

View File

@ -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.');
}

View File

@ -51,7 +51,7 @@ export async function memoryAdd(cwd: string, options: MemoryAddOptions = {}): Pr
by: options.by,
} as Partial<Memory>);
console.log(`Memory saved as ${memory.id}.`);
console.log(`AgentHub: Memory saved ${memory.id} ${memory.title}`);
}
export function memorySearch(cwd: string, query: string): void {

View File

@ -16,7 +16,7 @@ export async function taskCreate(cwd: string, options: Partial<Task> = {}): 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}`);
}

View File

@ -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 = '';

View File

@ -141,7 +141,7 @@ export function createProgram(cwd: string): Command {
duration,
by: options.by as string | undefined,
} as Partial<import('../core/schema.js').Memory>);
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 {

View File

@ -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);

View File

@ -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 <id> <title> [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$/);
});
});