Board (/board): - Drag an AGENT chip onto a task card to (re)assign it (realtime-notified). - Drag a task card to a column to change status; open→in_progress auto-assigns from the title's "<agent>:" prefix — no manual agent picking. - "+ New task" composer (agent dropdown removed; agent comes from the title). - Live Cost & Budget panel. Team (/team): live SSE sync of busy state + always-on ambient animation (connector shimmer, idle glow) that brightens to a busy pulse when an agent works. Org-chart hierarchy stays. Token accounting: new budgetService/rosterService + GET /budget and /agents. Real doneTokens + time-on-task estimate capped at 45 min/task (avoids the wall-clock overcount that produced multi-million-token totals), blended per-model EUR cost + optional budget bars. All estimates flagged "~". Autostart: `agent setup` writes deterministic SessionStart hooks for Codex (~/.codex/config.toml) and Kimi (~/.kimi-code/config.toml), not just Claude Code. Verified: both auto-enter the agenthub_work loop. Realtime architect review: agenthub_work is role-aware — architect/reviewer blocks on SSE and wakes when a task hits review (no manual watcher re-arm). mDNS: server advertises agenthub.local (bonjour-service) so the hub is reachable in a browser on the LAN without an IP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
5.5 KiB
TypeScript
148 lines
5.5 KiB
TypeScript
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<void> {
|
|
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<Listed[]> {
|
|
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<Listed[]> {
|
|
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 "<agent>:" (delegation convention), OR a handoff for the task has
|
|
* toAgent === <agent>, 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<void> {
|
|
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 <name> --role <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<void> {
|
|
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 <id> --agent ${ctx.agent}`);
|
|
}
|
|
}
|