import { remoteClient } from '../remoteClient.js'; import { listTasks as svcListTasks, getTask as svcGetTask, claimTask as svcClaimTask, } from '../../core/services/taskService.js'; import { listHandoffs as svcListHandoffs, getHandoff as svcGetHandoff } from '../../core/services/handoffService.js'; export interface AgentContext { serverUrl?: string; projectCwd: string; agent: string; role: string; } interface Listed { id: string; title?: string; status?: string; role?: string; taskId?: string; toAgent?: string; assignedTo?: string; } /** Announce presence (best-effort) and print the joined line. */ export async function announceAgent(serverUrl: string | undefined, agent: string, role: string): Promise { if (serverUrl) { try { await remoteClient.announce(serverUrl, agent, role); } catch { /* presence is best-effort */ } } console.log(`AgentHub: ${agent} joined (${role})`); } async function listOpenRoleTasks(ctx: AgentContext): Promise { return ctx.serverUrl ? await remoteClient.listTasks(ctx.serverUrl, { role: ctx.role, status: 'open' }) : svcListTasks(ctx.projectCwd, { role: ctx.role, status: 'open' }); } /** True for roles that review submitted work rather than implement it. */ export function isReviewerRole(role: string): boolean { const r = role.toLowerCase(); return r === 'architect' || r === 'reviewer'; } /** * Tasks awaiting review — the architect's equivalent of "addressed open tasks". * These are what an architect's `agenthub_work` loop should wake on, so review * submissions reach the architect in realtime instead of needing a manual re-arm. */ export async function listReviewTasks(ctx: AgentContext): Promise { return ctx.serverUrl ? await remoteClient.listTasks(ctx.serverUrl, { status: 'review' }) : svcListTasks(ctx.projectCwd, { status: 'review' }); } /** * Find the open task addressed to this agent. "Addressed" = task title starts * with ":" (delegation convention), OR a handoff for the task has * toAgent === , OR the task is already assignedTo this agent (a reopened * task that came back for rework). Returns the task + the handoff list (so the * caller can print the matching handoff without re-fetching). */ export async function findAddressedOpenTask( ctx: AgentContext, ): Promise<{ task: Listed; handoffs: Listed[] } | undefined> { const a = ctx.agent.toLowerCase(); const tasks = await listOpenRoleTasks(ctx); const handoffs = ctx.serverUrl ? await remoteClient.listHandoffs(ctx.serverUrl) : svcListHandoffs(ctx.projectCwd); const addressedByHandoff = new Set( handoffs .filter((h) => h.toAgent && String(h.toAgent).toLowerCase() === a && h.taskId) .map((h) => String(h.taskId)), ); const mine = tasks.filter( (t) => (t.title ?? '').toLowerCase().startsWith(`${a}:`) || addressedByHandoff.has(t.id) || (t.assignedTo && String(t.assignedTo).toLowerCase() === a), ); if (mine.length === 0) return undefined; return { task: mine[0], handoffs }; } /** Claim the task and print its body + handoff + the review-gate next step. */ export async function claimAndPrintTask(ctx: AgentContext, task: Listed, handoffs: Listed[]): Promise { if (ctx.serverUrl) await remoteClient.claimTask(ctx.serverUrl, task.id, ctx.agent); else svcClaimTask(ctx.projectCwd, task.id, ctx.agent); console.log(`AgentHub: Task claimed ${task.id} ${task.title ?? ''}`); try { const detail = ctx.serverUrl ? await remoteClient.getTask(ctx.serverUrl, task.id) : svcGetTask(ctx.projectCwd, task.id); if (detail.body && detail.body.trim()) { console.log(`\n─ Task ${task.id} ──────────────`); console.log(detail.body.trim()); } } catch { /* body is optional */ } const hof = handoffs.find((h) => h.taskId === task.id); if (hof) { try { const hd = ctx.serverUrl ? await remoteClient.getHandoff(ctx.serverUrl, hof.id) : svcGetHandoff(ctx.projectCwd, hof.id); console.log(`\n─ Handoff ${hof.id} ──────────────`); console.log(hd.handoff.summary); if (hd.body && hd.body.trim()) console.log(hd.body.trim()); } catch { /* handoff is optional */ } } console.log(`\n─ Next ──────────────`); console.log(`Implement the task, then submit for review: agenthub task review ${task.id}`); console.log(`Report what you did: agenthub memory add --title "${task.id} result" --category implementation --content "…"`); console.log(`Only the architect closes a task (\`done\`). If reopened, address the feedback and review again.`); } /** * `agenthub start --agent --role ` — one-shot onboarding. Announce, * claim the addressed task and print it; if none is addressed, list the open * role tasks so the agent can pick one. */ export async function startAgent(ctx: AgentContext): Promise { await announceAgent(ctx.serverUrl, ctx.agent, ctx.role); const found = await findAddressedOpenTask(ctx); if (found) { await claimAndPrintTask(ctx, found.task, found.handoffs); return; } const tasks = await listOpenRoleTasks(ctx); if (tasks.length === 0) { console.log(`AgentHub: no open ${ctx.role} tasks — waiting for the architect to delegate.`); } else { console.log(`AgentHub: no task addressed to ${ctx.agent}. Open ${ctx.role} tasks:`); for (const t of tasks) console.log(` ${t.id} ${t.title ?? ''}`); console.log(`→ claim one yourself: agenthub task claim --agent ${ctx.agent}`); } }