agenthub/src/core/schema.ts
chahinebrini 7ca6c859f2 feat(board+team): interactive board, animated team tree, token budget, mDNS, realtime architect review + codex/kimi autostart
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>
2026-07-08 03:23:48 +02:00

148 lines
5.5 KiB
TypeScript

import { z } from 'zod';
export const TaskStatus = z.enum(['open', 'in_progress', 'review', 'done', 'cancelled']);
export const DecisionStatus = z.enum(['proposed', 'accepted', 'rejected', 'superseded']);
export const MemoryCategory = z.enum(['architecture', 'product', 'technical', 'implementation', 'lesson']);
export const Priority = z.enum(['low', 'medium', 'high', 'critical']);
export const Role = z.enum(['architect', 'implementer', 'reviewer', 'tester']);
export const DelegationMode = z.enum(['manual', 'suggest', 'auto']);
export const TaskSchema = z.object({
id: z.string().regex(/^TSK-\d{4}$/),
title: z.string().min(1),
description: z.string().default(''),
status: TaskStatus,
priority: Priority.default('medium'),
role: Role.optional(),
assignedTo: z.string().optional(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
dueDate: z.string().datetime().optional(),
tags: z.array(z.string()).default([]),
acceptanceCriteria: z.array(z.string()).default([]),
sourceHandoff: z.string().optional(),
// Optional completion metadata (set via `task done --tokens --duration --by`)
doneBy: z.string().optional(),
doneTokens: z.number().int().nonnegative().optional(),
doneDuration: z.number().int().nonnegative().optional(),
});
export const HandoffSchema = z.object({
id: z.string().regex(/^HOF-\d{4}$/),
fromRole: z.string().min(1),
toRole: z.string().min(1),
fromAgent: z.string().optional(),
toAgent: z.string().optional(),
taskId: z.string().optional(),
summary: z.string().min(1),
context: z.string().default(''),
decisions: z.array(z.string()).default([]),
openQuestions: z.array(z.string()).default([]),
nextSteps: z.array(z.string()).default([]),
createdAt: z.string().datetime(),
});
export const DecisionSchema = z.object({
id: z.string().regex(/^DEC-\d{4}$/),
title: z.string().min(1),
status: DecisionStatus.default('accepted'),
context: z.string().default(''),
decision: z.string().min(1),
consequences: z.array(z.string()).default([]),
alternatives: z.array(z.string()).default([]),
relatedDecisions: z.array(z.string()).default([]),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
export const MemorySchema = z.object({
id: z.string().min(1),
title: z.string().min(1),
category: MemoryCategory.default('technical'),
content: z.string().min(1),
tags: z.array(z.string()).default([]),
relatedTasks: z.array(z.string()).default([]),
relatedDecisions: z.array(z.string()).default([]),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
// Optional agent-supplied metadata (set via `memory add --tokens --duration --by`)
tokens: z.number().int().nonnegative().optional(),
duration: z.number().int().nonnegative().optional(),
by: z.string().optional(),
});
/**
* Direct message between agents (architect↔implementer, or agent↔agent),
* routed through the hub. Lightweight, non-task-bound — for questions,
* clarifications and pings the handoff/memory entities don't cover.
*/
export const MessageSchema = z.object({
id: z.string().min(1),
from: z.string().min(1),
to: z.string().min(1),
text: z.string().min(1),
taskId: z.string().optional(),
status: z.enum(['unread', 'read']).default('unread'),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
// Activity timeline item returned by GET /tasks/:id/activity
export const ActivityItemSchema = z.object({
at: z.string().datetime(),
kind: z.enum(['created', 'handoff', 'result', 'status']),
actor: z.string(),
summary: z.string(),
meta: z.record(z.unknown()).optional(),
});
export type ActivityItem = z.infer<typeof ActivityItemSchema>;
export const StatusSchema = z.object({
generatedAt: z.string().datetime(),
activeTasks: z.array(z.string()).default([]),
blockedTasks: z.array(z.string()).default([]),
recentDecisions: z.array(z.string()).default([]),
recentHandoffs: z.array(z.string()).default([]),
summary: z.string().default(''),
});
export const RoleConfigSchema = z.object({
preferredAgent: z.string().min(1),
description: z.string().optional(),
});
/**
* A named agent in the team roster. Lets the team view show agents (by name,
* like backyard / mo / zied) with a fixed role + the model behind them — instead
* of guessing role/identity from whatever tasks the agent happened to touch.
*/
export const AgentConfigSchema = z.object({
role: Role,
/** Display model, e.g. "Opus 4.8", "Sonnet 4.6", "Kimi K2", "GPT-5 Codex". */
model: z.string().optional(),
/** Provider/company for the logo: "anthropic" | "openai" | "moonshot" | … */
kind: z.string().optional(),
description: z.string().optional(),
/** Optional monthly cost budget in EUR — drives the budget bar on the board. */
budgetEur: z.number().nonnegative().optional(),
});
export const ConfigSchema = z.object({
version: z.literal('1'),
projectName: z.string().min(1),
delegationMode: DelegationMode.default('suggest'),
roles: z.record(z.string(), RoleConfigSchema),
/** Named team roster: agent name → role + model + provider. */
agents: z.record(z.string(), AgentConfigSchema).optional(),
serverUrl: z.string().url().optional(),
});
export type Task = z.infer<typeof TaskSchema>;
export type Handoff = z.infer<typeof HandoffSchema>;
export type Decision = z.infer<typeof DecisionSchema>;
export type Memory = z.infer<typeof MemorySchema>;
export type Message = z.infer<typeof MessageSchema>;
export type Status = z.infer<typeof StatusSchema>;
export type Config = z.infer<typeof ConfigSchema>;