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:
parent
722388cb00
commit
ff79cbb639
@ -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",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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}`);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.');
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
|
||||
@ -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 = '';
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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$/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user