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(), claimedBy: z.string().optional(), reviewer: 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; 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(), }); /** * One node of the team org chart. Supports arbitrary depth via `parentId`, and * decouples the DISPLAY (label/title/editable) from the linked roster agent, so * org-only nodes (a human CEO, a Product Owner) can sit in the tree without a * running agent. Live state (active/reviewing) is derived from `agent`. */ export const OrgNodeSchema = z.object({ id: z.string().min(1), /** Editable display name. */ label: z.string().min(1), /** Display role/title shown under the name (e.g. "CEO", "Product Owner"). * For linked agents it falls back to the agent's roster role. */ role: z.string().optional(), /** One-line "what this node does" — shown behind the info button. */ title: z.string().optional(), /** Linked roster agent name (for model/provider/logo + live task state). */ agent: z.string().optional(), /** Parent node id; omit/undefined for the root. */ parentId: z.string().optional(), /** Provider override for the icon when there is no linked agent. */ kind: z.string().optional(), }); export type OrgNode = z.infer; 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(), /** Team org chart (arbitrary depth). When present, /team renders this tree. */ org: z.array(OrgNodeSchema).optional(), serverUrl: z.string().url().optional(), }); export type Task = z.infer; export type Handoff = z.infer; export type Decision = z.infer; export type Memory = z.infer; export type Message = z.infer; export type Status = z.infer; export type Config = z.infer;